perf: 完善订单管理

This commit is contained in:
richarjiang
2026-04-05 21:03:18 +08:00
parent fdb13c32c2
commit 4633ceea8c
29 changed files with 1000 additions and 261 deletions

View File

@@ -1,16 +1,20 @@
<template>
<view class="page" :style="{ paddingTop: navBarHeight }">
<CustomNavBar title="会员管理" show-back />
<!-- Search bar -->
<view class="filter-bar">
<input
class="search-input"
v-model="searchQuery"
placeholder="搜索昵称或手机号"
placeholder="搜索昵称或 OpenID"
placeholder-style="color:#bbb"
@confirm="onSearch"
confirm-type="search"
/>
<view v-if="searchQuery" class="search-clear" @tap="onClear">
<text class="search-clear-icon">×</text>
</view>
<view class="search-btn" @tap="onSearch">
<text class="search-btn-text">搜索</text>
</view>
@@ -31,8 +35,10 @@
<!-- Empty -->
<view v-else-if="!loading && !members.length" class="empty-state">
<text class="empty-icon">👥</text>
<text class="empty-text">暂无会员数据</text>
<view class="empty-icon-wrap">
<view class="empty-icon-person" />
</view>
<text class="empty-text">{{ searchQuery ? '未找到匹配的会员' : '暂无会员数据' }}</text>
</view>
<!-- Member list -->
@@ -51,7 +57,7 @@
</view>
<view class="member-info">
<text class="member-name">{{ m.nickname || '未知用户' }}</text>
<text class="member-phone">{{ m.phone || '未绑定手机' }}</text>
<text class="member-openid">{{ m.openid }}</text>
</view>
<view class="member-stats">
<text class="member-stat-value">{{ m.totalBookings }}</text>
@@ -61,9 +67,11 @@
</view>
</view>
<!-- Load more -->
<view v-if="hasMore" class="load-more" @tap="loadMore">
<text class="load-more-text">{{ loading ? '加载中...' : '加载更多' }}</text>
<!-- Bottom status -->
<view v-if="members.length" class="list-footer">
<text class="list-footer-text">
{{ loading ? '加载中...' : hasMore ? '上拉加载更多' : '— 已加载全部 —' }}
</text>
</view>
<!-- Detail modal -->
@@ -77,6 +85,9 @@
</view>
</view>
<text class="detail-name">{{ detailMember.nickname || '未知用户' }}</text>
<text class="detail-openid" @tap="copyOpenid(detailMember.openid)">
{{ detailMember.openid }}
</text>
<text class="detail-phone">{{ detailMember.phone || '未绑定手机' }}</text>
</view>
@@ -105,7 +116,9 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { onReachBottom } from '@dcloudio/uni-app'
import CustomNavBar from '../../components/CustomNavBar.vue'
import { getSystemLayout } from '../../utils/system'
import { useAdminStore } from '../../stores/admin'
import type { MemberSummary } from '../../stores/admin'
@@ -113,8 +126,7 @@ const adminStore = useAdminStore()
const navBarHeight = ref('64px')
onMounted(() => {
const sys = uni.getSystemInfoSync()
navBarHeight.value = `${(sys.statusBarHeight ?? 20) + Math.round(88 * sys.windowWidth / 750)}px`
navBarHeight.value = `${getSystemLayout().navBarHeight}px`
})
const members = ref<MemberSummary[]>([])
@@ -136,15 +148,16 @@ async function loadMembers(reset = false) {
}
loading.value = true
try {
const search = searchQuery.value.trim()
const result = await adminStore.fetchMembers({
page: page.value,
limit: LIMIT,
search: searchQuery.value.trim() || undefined,
...(search ? { search } : {}),
})
if (reset) {
members.value = [...result.items]
} else {
members.value.push(...result.items)
members.value = [...members.value, ...result.items]
}
total.value = result.total
hasMore.value = members.value.length < result.total
@@ -159,24 +172,37 @@ function onSearch() {
loadMembers(true)
}
function loadMore() {
function onClear() {
searchQuery.value = ''
loadMembers(true)
}
// Scroll to bottom → load next page
onReachBottom(() => {
if (!hasMore.value || loading.value) return
page.value++
loadMembers(false)
}
})
function openDetail(m: MemberSummary) {
detailMember.value = m
showDetail.value = true
}
function copyOpenid(openid: string) {
uni.setClipboardData({
data: openid,
success: () => uni.showToast({ title: '已复制 OpenID', icon: 'success' }),
})
}
onMounted(() => loadMembers(true))
</script>
<style lang="scss" scoped>
.page {
min-height: 100vh;
background: #f5f3f0;
background: $bg-page;
padding-bottom: 40rpx;
}
@@ -186,27 +212,44 @@ onMounted(() => loadMembers(true))
align-items: center;
gap: 16rpx;
padding: 24rpx;
background: #ffffff;
border-bottom: 1rpx solid #eee;
background: $bg-card;
border-bottom: 1rpx solid $border-color;
position: relative;
}
.search-input {
flex: 1;
height: 72rpx;
background: #f5f3f0;
background: $bg-page;
border-radius: 36rpx;
padding: 0 28rpx;
font-size: 26rpx;
color: #333;
color: $text-primary;
}
.search-clear {
position: absolute;
right: 168rpx;
width: 44rpx;
height: 44rpx;
display: flex;
align-items: center;
justify-content: center;
}
.search-clear-icon {
font-size: 32rpx;
color: $text-hint;
line-height: 1;
}
.search-btn {
background: #1a1a2e;
background: $brand-color;
border-radius: 36rpx;
padding: 16rpx 32rpx;
}
.search-btn-text { font-size: 26rpx; font-weight: 600; color: #c9a87c; }
.search-btn-text { font-size: 26rpx; font-weight: 600; color: $accent-color; }
/* ── Stats row ───────────────────────────── */
.stats-row {
@@ -215,17 +258,17 @@ onMounted(() => loadMembers(true))
}
.stat-item { display: flex; align-items: baseline; gap: 8rpx; }
.stat-value { font-size: 36rpx; font-weight: 800; color: #c9a87c; }
.stat-label { font-size: 24rpx; color: #999; }
.stat-value { font-size: 36rpx; font-weight: 800; color: $accent-color; }
.stat-label { font-size: 24rpx; color: $text-hint; }
/* ── Skeleton ────────────────────────────── */
.skeleton-list { padding: 0 24rpx; }
.skeleton-item {
height: 100rpx;
border-radius: 16rpx;
height: 120rpx;
border-radius: $radius-md;
margin-bottom: 16rpx;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background: linear-gradient(90deg, #f0f0f0 25%, #e8e8e8 50%, #f0f0f0 75%);
background-size: 400% 100%;
animation: shimmer 1.4s infinite;
}
@@ -240,25 +283,63 @@ onMounted(() => loadMembers(true))
display: flex;
flex-direction: column;
align-items: center;
padding: 100rpx 0;
gap: 20rpx;
padding: 120rpx 0;
gap: 24rpx;
}
.empty-icon { font-size: 80rpx; }
.empty-text { font-size: 28rpx; color: #bbb; }
.empty-icon-wrap {
width: 96rpx;
height: 96rpx;
border-radius: 50%;
background: rgba($brand-color, 0.06);
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.empty-icon-person {
&::before {
content: '';
width: 20rpx;
height: 20rpx;
border: 3rpx solid $text-hint;
border-radius: 50%;
position: absolute;
top: 22rpx;
left: 50%;
transform: translateX(-50%);
box-sizing: border-box;
}
&::after {
content: '';
width: 36rpx;
height: 16rpx;
border: 3rpx solid $text-hint;
border-bottom: none;
border-radius: 20rpx 20rpx 0 0;
position: absolute;
bottom: 20rpx;
left: 50%;
transform: translateX(-50%);
box-sizing: border-box;
}
}
.empty-text { font-size: 28rpx; color: $text-hint; }
/* ── Member list ─────────────────────────── */
.member-list { padding: 0 24rpx; padding-top: 8rpx; }
.member-row {
background: #ffffff;
border-radius: 16rpx;
background: $bg-card;
border-radius: $radius-md;
padding: 24rpx;
display: flex;
align-items: center;
gap: 20rpx;
margin-bottom: 16rpx;
box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.05);
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
}
.member-avatar {
@@ -275,7 +356,7 @@ onMounted(() => loadMembers(true))
width: 80rpx;
height: 80rpx;
border-radius: 50%;
background: #1a1a2e;
background: $brand-color;
display: flex;
align-items: center;
justify-content: center;
@@ -289,34 +370,53 @@ onMounted(() => loadMembers(true))
.avatar-text {
font-size: 32rpx;
font-weight: 700;
color: #c9a87c;
color: $accent-color;
}
.avatar-text--lg { font-size: 48rpx; }
.member-info { flex: 1; display: flex; flex-direction: column; gap: 8rpx; }
.member-name { font-size: 28rpx; font-weight: 600; color: #1a1a2e; }
.member-phone { font-size: 22rpx; color: #999; }
.member-stats { display: flex; flex-direction: column; align-items: flex-end; gap: 4rpx; }
.member-stat-value { font-size: 32rpx; font-weight: 700; color: #c9a87c; }
.member-stat-label { font-size: 20rpx; color: #bbb; }
.member-arrow { font-size: 36rpx; color: #ccc; }
/* ── Load more ───────────────────────────── */
.load-more {
text-align: center;
padding: 32rpx;
.member-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 6rpx;
min-width: 0;
}
.load-more-text { font-size: 26rpx; color: #c9a87c; }
.member-name {
font-size: 28rpx;
font-weight: 600;
color: $brand-color;
}
.member-openid {
font-size: 20rpx;
color: $text-hint;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-family: Menlo, Monaco, Consolas, monospace;
}
.member-stats { display: flex; flex-direction: column; align-items: flex-end; gap: 4rpx; flex-shrink: 0; }
.member-stat-value { font-size: 32rpx; font-weight: 700; color: $accent-color; }
.member-stat-label { font-size: 20rpx; color: $text-hint; }
.member-arrow { font-size: 36rpx; color: $text-hint; transform: scaleX(0.6); }
/* ── List footer ─────────────────────────── */
.list-footer {
text-align: center;
padding: 28rpx 0 16rpx;
}
.list-footer-text { font-size: 24rpx; color: $text-hint; }
/* ── Detail modal ────────────────────────── */
.modal-mask {
position: fixed;
inset: 0;
background: rgba(0,0,0,0.5);
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: flex-end;
z-index: 100;
@@ -324,8 +424,8 @@ onMounted(() => loadMembers(true))
.modal {
width: 100%;
background: #ffffff;
border-radius: 24rpx 24rpx 0 0;
background: $bg-card;
border-radius: $radius-lg $radius-lg 0 0;
padding: 48rpx 32rpx 60rpx;
}
@@ -333,7 +433,7 @@ onMounted(() => loadMembers(true))
display: flex;
flex-direction: column;
align-items: center;
gap: 16rpx;
gap: 12rpx;
margin-bottom: 40rpx;
}
@@ -342,33 +442,44 @@ onMounted(() => loadMembers(true))
height: 120rpx;
border-radius: 50%;
overflow: hidden;
margin-bottom: 8rpx;
}
.detail-name { font-size: 32rpx; font-weight: 700; color: #1a1a2e; }
.detail-phone { font-size: 26rpx; color: #888; }
.detail-name { font-size: 32rpx; font-weight: 700; color: $brand-color; }
.detail-openid {
font-size: 22rpx;
color: $accent-color;
font-family: Menlo, Monaco, Consolas, monospace;
padding: 6rpx 16rpx;
background: rgba($accent-color, 0.08);
border-radius: 8rpx;
}
.detail-phone { font-size: 26rpx; color: $text-secondary; }
.detail-stats {
display: flex;
justify-content: space-around;
background: #f5f3f0;
border-radius: 16rpx;
background: $bg-page;
border-radius: $radius-md;
padding: 28rpx;
margin-bottom: 32rpx;
}
.detail-stat { display: flex; flex-direction: column; align-items: center; gap: 8rpx; }
.detail-stat-value { font-size: 40rpx; font-weight: 800; color: #c9a87c; }
.detail-stat-label { font-size: 22rpx; color: #999; }
.detail-stat-value { font-size: 40rpx; font-weight: 800; color: $accent-color; }
.detail-stat-label { font-size: 22rpx; color: $text-hint; }
.modal-close {
width: 100%;
height: 88rpx;
background: #f0f0f0;
background: $bg-page;
border-radius: 44rpx;
display: flex;
align-items: center;
justify-content: center;
}
.modal-close-text { font-size: 28rpx; color: #555; }
.modal-close-text { font-size: 28rpx; color: $text-secondary; }
</style>