Files
mp-pilates/packages/app/src/pages/admin/index.vue
richarjiang 7a06b5e336 feat(app): implement all sub-pages and admin management pages
Sub-pages: card purchase with WeChat Pay flow, my memberships with
progress bars, my bookings with tabs, personal info editor
Admin: management center grid, week template CRUD, slot adjustment,
member management with search, order list with filters, card type
CRUD with form modal, studio settings editor
Admin Pinia store for all admin API calls
2026-04-02 15:25:57 +08:00

177 lines
4.1 KiB
Vue

<template>
<view class="page">
<!-- Stats row -->
<view class="stats-row">
<view v-if="statsLoading" class="stats-shimmer-wrap">
<view v-for="i in 3" :key="i" class="stats-shimmer" />
</view>
<template v-else>
<view class="stat-item">
<text class="stat-value">{{ stats.todayBookings }}</text>
<text class="stat-label">今日预约</text>
</view>
<view class="stat-divider" />
<view class="stat-item">
<text class="stat-value">{{ stats.totalOrders }}</text>
<text class="stat-label">总订单</text>
</view>
<view class="stat-divider" />
<view class="stat-item">
<text class="stat-value">{{ stats.totalBookings }}</text>
<text class="stat-label">总预约</text>
</view>
</template>
</view>
<!-- Nav grid -->
<view class="nav-grid">
<view
v-for="item in navItems"
:key="item.path"
class="nav-item"
@tap="navigate(item.path)"
>
<text class="nav-icon">{{ item.icon }}</text>
<text class="nav-label">{{ item.label }}</text>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useAdminStore } from '../../stores/admin'
import type { AdminStats } from '../../stores/admin'
const adminStore = useAdminStore()
const statsLoading = ref(false)
const stats = ref<AdminStats>({ todayBookings: 0, totalOrders: 0, totalBookings: 0 })
const navItems = [
{ icon: '📅', label: '排课设置', path: '/pages/admin/week-template' },
{ icon: '🔧', label: '临时调整', path: '/pages/admin/slot-adjust' },
{ icon: '👥', label: '会员管理', path: '/pages/admin/members' },
{ icon: '📋', label: '订单管理', path: '/pages/admin/orders' },
{ icon: '💳', label: '卡种管理', path: '/pages/admin/card-types' },
{ icon: '🏢', label: '工作室设置', path: '/pages/admin/studio' },
]
function navigate(path: string) {
uni.navigateTo({ url: path })
}
async function loadStats() {
statsLoading.value = true
try {
stats.value = await adminStore.fetchDashboardStats()
} catch {
// fail silently — stats are non-critical
} finally {
statsLoading.value = false
}
}
onMounted(loadStats)
</script>
<style lang="scss" scoped>
.page {
min-height: 100vh;
background: #1a1a2e;
padding-bottom: 60rpx;
}
/* ── Stats row ───────────────────────────── */
.stats-row {
display: flex;
align-items: center;
justify-content: space-around;
background: rgba(255, 255, 255, 0.06);
margin: 24rpx 24rpx 32rpx;
border-radius: 20rpx;
padding: 32rpx 16rpx;
}
.stats-shimmer-wrap {
display: flex;
width: 100%;
justify-content: space-around;
align-items: center;
}
.stats-shimmer {
width: 120rpx;
height: 60rpx;
border-radius: 12rpx;
background: linear-gradient(90deg, rgba(255,255,255,0.08) 25%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.08) 75%);
background-size: 400% 100%;
animation: shimmer 1.4s infinite;
}
@keyframes shimmer {
0% { background-position: 100% 0; }
100% { background-position: -100% 0; }
}
.stat-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 8rpx;
flex: 1;
}
.stat-value {
font-size: 44rpx;
font-weight: 800;
color: #c9a87c;
line-height: 1;
}
.stat-label {
font-size: 22rpx;
color: rgba(255, 255, 255, 0.5);
}
.stat-divider {
width: 1rpx;
height: 60rpx;
background: rgba(255, 255, 255, 0.12);
}
/* ── Nav grid ────────────────────────────── */
.nav-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20rpx;
padding: 0 24rpx;
}
.nav-item {
background: rgba(255, 255, 255, 0.06);
border-radius: 20rpx;
padding: 40rpx 0;
display: flex;
flex-direction: column;
align-items: center;
gap: 16rpx;
border: 1rpx solid rgba(201, 168, 124, 0.15);
&:active {
opacity: 0.7;
}
}
.nav-icon {
font-size: 56rpx;
}
.nav-label {
font-size: 28rpx;
font-weight: 600;
color: #ffffff;
letter-spacing: 1rpx;
}
</style>