Files
mp-pilates/packages/app/src/pages/admin/index.vue
2026-04-05 21:35:30 +08:00

349 lines
9.2 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="page" :style="{ paddingTop: navBarHeight }">
<CustomNavBar title="管理中心" show-back />
<!-- Stats summary card -->
<view class="stats-card-wrap">
<view class="stats-card">
<view v-if="statsLoading" class="stats-loading">
<view v-for="i in 3" :key="i" class="stat-skeleton" />
</view>
<template v-else>
<view class="stat-block">
<text class="stat-num">{{ stats.todayBookings }}</text>
<text class="stat-sub">今日预约</text>
</view>
<view class="stat-sep" />
<view class="stat-block">
<text class="stat-num">{{ stats.totalOrders }}</text>
<text class="stat-sub">总订单</text>
</view>
<view class="stat-sep" />
<view class="stat-block">
<text class="stat-num">{{ stats.totalBookings }}</text>
<text class="stat-sub">总预约</text>
</view>
</template>
</view>
</view>
<!-- Section header: 课程管理 -->
<view class="section-header">
<text class="section-title">课程管理</text>
</view>
<!-- List: schedule -->
<view class="list">
<view class="list-item" @tap="navigate('/pages/admin/schedule')">
<view class="item-left">
<view class="item-icon-wrap icon--schedule">
<text class="item-icon-text"></text>
</view>
<view class="item-text-group">
<text class="item-title">排课管理</text>
<text class="item-desc">管理每周课程时段</text>
</view>
</view>
<view class="item-arrow">
<text class="arrow-text"></text>
</view>
</view>
<view class="list-item" @tap="navigate('/pages/admin/week-template')">
<view class="item-left">
<view class="item-icon-wrap icon--template">
<text class="item-icon-text"></text>
</view>
<view class="item-text-group">
<text class="item-title">排课模板</text>
<text class="item-desc">设置每周课程模板</text>
</view>
</view>
<view class="item-arrow">
<text class="arrow-text"></text>
</view>
</view>
</view>
<!-- Section header: 会员与订单 -->
<view class="section-header">
<text class="section-title">会员与订单</text>
</view>
<!-- List: members & orders -->
<view class="list">
<view class="list-item" @tap="navigate('/pages/admin/members')">
<view class="item-left">
<view class="item-icon-wrap icon--members">
<text class="item-icon-text"></text>
</view>
<view class="item-text-group">
<text class="item-title">会员管理</text>
<text class="item-desc">查看所有会员信息</text>
</view>
</view>
<view class="item-arrow">
<text class="arrow-text"></text>
</view>
</view>
<view class="list-item" @tap="navigate('/pages/admin/orders')">
<view class="item-left">
<view class="item-icon-wrap icon--orders">
<text class="item-icon-text"></text>
</view>
<view class="item-text-group">
<text class="item-title">订单管理</text>
<text class="item-desc">查看所有订单记录</text>
</view>
</view>
<view class="item-arrow">
<text class="arrow-text"></text>
</view>
</view>
<view class="list-item" @tap="navigate('/pages/admin/card-types')">
<view class="item-left">
<view class="item-icon-wrap icon--card">
<text class="item-icon-text"></text>
</view>
<view class="item-text-group">
<text class="item-title">卡种管理</text>
<text class="item-desc">设置会员卡类型</text>
</view>
</view>
<view class="item-arrow">
<text class="arrow-text"></text>
</view>
</view>
</view>
<!-- Section header: 系统 -->
<view class="section-header">
<text class="section-title">系统</text>
</view>
<!-- List: settings -->
<view class="list">
<view class="list-item" @tap="navigate('/pages/admin/studio')">
<view class="item-left">
<view class="item-icon-wrap icon--studio">
<text class="item-icon-text"></text>
</view>
<view class="item-text-group">
<text class="item-title">工作室设置</text>
<text class="item-desc">工作室信息与配置</text>
</view>
</view>
<view class="item-arrow">
<text class="arrow-text"></text>
</view>
</view>
</view>
<view style="height: 40rpx" />
</view>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import CustomNavBar from '../../components/CustomNavBar.vue'
import { getSystemLayout } from '../../utils/system'
import { useAdminStore } from '../../stores/admin'
import type { AdminStats } from '../../stores/admin'
const navBarHeight = ref('64px')
const adminStore = useAdminStore()
const statsLoading = ref(false)
const stats = ref<AdminStats>({ todayBookings: 0, totalOrders: 0, totalBookings: 0 })
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(() => {
navBarHeight.value = `${getSystemLayout().navBarHeight}px`
loadStats()
})
</script>
<style lang="scss" scoped>
/* ── Page ───────────────────────────────────── */
.page {
min-height: 100vh;
padding-bottom: 40rpx;
}
/* ── Stats card ─────────────────────────────── */
.stats-card-wrap {
padding: 24rpx 24rpx 8rpx;
}
.stats-card {
background: #FFFFFF;
border-radius: 20rpx;
padding: 32rpx 24rpx;
display: flex;
align-items: center;
box-shadow: 0 4rpx 20rpx rgba(180, 160, 130, 0.10);
border: 1rpx solid rgba(180, 160, 130, 0.12);
}
.stats-loading {
width: 100%;
display: flex;
align-items: center;
justify-content: space-around;
}
.stat-skeleton {
width: 100rpx;
height: 64rpx;
border-radius: 12rpx;
background: linear-gradient(90deg, $primary-border 25%, $primary-light 50%, $primary-border 75%);
background-size: 400% 100%;
animation: shimmer 1.6s ease infinite;
}
.stat-block {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 8rpx;
}
.stat-num {
font-size: 44rpx;
font-weight: 700;
color: #4A4035;
line-height: 1;
font-family: 'DIN Alternate', 'Helvetica Neue', Arial, sans-serif;
}
.stat-sub {
font-size: 22rpx;
color: #A09080;
letter-spacing: 0.5rpx;
}
.stat-sep {
width: 1rpx;
height: 56rpx;
background: rgba(180, 160, 130, 0.2);
}
/* ── Section header ─────────────────────────── */
.section-header {
padding: 32rpx 24rpx 12rpx;
}
.section-title {
font-size: 22rpx;
font-weight: 600;
color: #A09080;
letter-spacing: 2rpx;
text-transform: uppercase;
}
/* ── List ───────────────────────────────────── */
.list {
background: #FFFFFF;
margin: 0 24rpx;
border-radius: 20rpx;
overflow: hidden;
box-shadow: 0 4rpx 20rpx rgba(180, 160, 130, 0.08);
border: 1rpx solid rgba(180, 160, 130, 0.1);
}
.list-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 28rpx 24rpx;
border-bottom: 1rpx solid rgba(180, 160, 130, 0.1);
transition: background 0.15s ease;
&:last-child {
border-bottom: none;
}
&:active {
background: rgba(180, 160, 130, 0.05);
}
}
.item-left {
display: flex;
align-items: center;
gap: 20rpx;
}
.item-icon-wrap {
width: 72rpx;
height: 72rpx;
border-radius: 18rpx;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.item-icon-text {
font-size: 32rpx;
color: #FFFFFF;
line-height: 1;
}
/* Icon variants — warm muted tones */
.icon--schedule { background: linear-gradient(135deg, #8B9E7E, #7A8E6E); }
.icon--template { background: linear-gradient(135deg, #A090C0, #9080B0); }
.icon--members { background: linear-gradient(135deg, $primary-color, $primary-dark); }
.icon--orders { background: linear-gradient(135deg, #7E9EC4, #6E8EB4); }
.icon--card { background: linear-gradient(135deg, #C48E7E, #B47E6E); }
.icon--studio { background: linear-gradient(135deg, #9E9E7E, #8E8E6E); }
.item-text-group {
display: flex;
flex-direction: column;
gap: 4rpx;
}
.item-title {
font-size: 30rpx;
font-weight: 600;
color: #4A4035;
letter-spacing: 0.5rpx;
}
.item-desc {
font-size: 24rpx;
color: #A09080;
}
.item-arrow {
flex-shrink: 0;
padding-left: 16rpx;
}
.arrow-text {
font-size: 40rpx;
color: rgba(180, 160, 130, 0.5);
font-weight: 300;
line-height: 1;
}
</style>