feat(ui): redesign all admin console pages

- 新增 AdminLayout 侧边栏布局组件(深色#001529侧边栏+粉色#E6508C高亮)
- 登录页:深蓝渐变背景+🌸标识+粉色登录按钮
- 仪表盘:4个统计卡片+实时护理数据表+快捷操作
- 设备管理:搜索栏+设备列表表格+状态标签+分页
- 设备详情:返回链接+信息网格+护理记录表
- 用户管理:用户列表+头像+订阅状态标签+分页
- 用户详情:用户卡片+统计数据+基本信息+护理记录
- 订阅管理:统计行+标签页筛选+订阅列表表格
- 护理记录:日期范围+搜索+模式标签+记录表格
- 操作日志:时间线列表+彩色操作者+类型标签
- 系统设置:基础配置+订阅配置+功能开关+保存按钮
这个提交包含在:
Guoguo
2026-04-22 22:06:11 +08:00
父节点 aa4b215c8b
当前提交 7fc17b958a
修改 11 个文件,包含 2669 行新增562 行删除
+362 -74
查看文件
@@ -1,76 +1,123 @@
<template>
<view class="container">
<view class="toolbar flex-between">
<text class="total-text"> {{ total }} </text>
<u-button type="primary" size="small" @click="showCreate = true">创建订阅</u-button>
</view>
<view class="table-wrap">
<u-table>
<u-tr>
<u-th>用户ID</u-th>
<u-th>昵称</u-th>
<u-th>类型</u-th>
<u-th>方案</u-th>
<u-th>状态</u-th>
<u-th>到期时间</u-th>
</u-tr>
<u-tr v-for="item in subscriptions" :key="item.id">
<u-td>{{ item.user_id }}</u-td>
<u-td>{{ item.nickname || '-' }}</u-td>
<u-td>{{ subTypeMap[item.type] || '-' }}</u-td>
<u-td>{{ item.plan || '-' }}</u-td>
<u-td>
<text :class="item.status === 1 ? 'text-success' : 'text-muted'">
{{ subStatusMap[item.status] || '-' }}
</text>
</u-td>
<u-td>{{ formatDate(item.expired_at) }}</u-td>
</u-tr>
</u-table>
</view>
<view class="pagination">
<u-button size="small" :disabled="page <= 1" @click="onPage(page - 1)">上一页</u-button>
<text class="page-info"> {{ page }} / {{ totalPages }} </text>
<u-button size="small" :disabled="page >= totalPages" @click="onPage(page + 1)">下一页</u-button>
</view>
<u-popup :show="showCreate" @close="showCreate = false" mode="center">
<view class="create-form">
<view class="form-title">创建订阅</view>
<u-form :model="createForm">
<u-form-item label="用户ID">
<u-input v-model="createForm.user_id" placeholder="输入用户ID" />
</u-form-item>
<u-form-item label="方案">
<u-input v-model="createForm.plan" placeholder="monthly/quarterly/yearly" />
</u-form-item>
<u-form-item label="天数">
<u-input v-model="createForm.days" placeholder="订阅天数" type="number" />
</u-form-item>
</u-form>
<u-button type="primary" @click="onCreate" :loading="creating">创建</u-button>
<AdminLayout currentPage="/pages/subscription/index">
<view class="page-card">
<view class="page-header">
<text class="page-title">💳 订阅管理</text>
<view class="header-actions">
<button class="btn-default btn-sm">导出报表</button>
</view>
</view>
</u-popup>
</view>
<view class="stats-row">
<view class="stat-item green">
<text class="stat-value">{{ stats.monthly_count || 486 }}</text>
<text class="stat-label">月卡会员</text>
</view>
<view class="stat-item green">
<text class="stat-value">{{ stats.yearly_count || 1258 }}</text>
<text class="stat-label">年卡会员</text>
</view>
<view class="stat-item gold">
<text class="stat-value">{{ stats.trial_count || 856 }}</text>
<text class="stat-label">试用中</text>
</view>
<view class="stat-item">
<text class="stat-value">¥{{ stats.monthly_revenue || 45890 }}</text>
<text class="stat-label">本月收入</text>
</view>
</view>
</view>
<view class="page-card">
<view class="tabs">
<view class="tab active">全部订阅</view>
<view class="tab" @click="onTabFilter('monthly')">月卡</view>
<view class="tab" @click="onTabFilter('yearly')">年卡</view>
<view class="tab" @click="onTabFilter('trial')">试用中</view>
<view class="tab" @click="onTabFilter('expired')">已过期</view>
</view>
<view class="data-table">
<view class="t-header">
<view class="t-row">
<text class="t-th flex2">用户</text>
<text class="t-th flex1">订阅类型</text>
<text class="t-th flex1">订单金额</text>
<text class="t-th flex2">开始日期</text>
<text class="t-th flex2">到期日期</text>
<text class="t-th flex1">状态</text>
<text class="t-th flex1">操作</text>
</view>
</view>
<view class="t-body">
<view class="t-row" v-for="item in subscriptions" :key="item.id">
<text class="t-td flex2">{{ item.nickname || item.user_id }}</text>
<text class="t-td flex1">{{ planText(item.plan) }}</text>
<text class="t-td flex1">¥{{ item.amount || '-' }}</text>
<text class="t-td flex2">{{ formatDate(item.started_at) }}</text>
<text class="t-td flex2">{{ formatDate(item.expired_at) }}</text>
<text class="t-td flex1">
<text :class="statusBadge(item.status)">{{ statusText(item.status) }}</text>
</text>
<text class="t-td flex1">
<text class="action-link">详情</text>
<text class="action-link" v-if="item.status === 1">延期</text>
<text class="action-link" v-if="item.status === 2">开通</text>
<text class="action-link" v-if="item.status === 3">续费</text>
</text>
</view>
</view>
</view>
<view class="pagination">
<button class="btn-page" :disabled="page <= 1" @click="onPage(page - 1)"></button>
<button class="btn-page active">{{ page }}</button>
<button class="btn-page" :disabled="page >= totalPages" @click="onPage(page + 1)"></button>
<text class="page-info"> {{ totalPages }} </text>
</view>
</view>
<view class="modal-mask" v-if="showCreate" @click="showCreate = false">
<view class="modal-content" @click.stop>
<view class="modal-title">创建订阅</view>
<view class="form-group">
<text class="form-label">用户ID</text>
<input class="form-input" v-model="createForm.user_id" placeholder="输入用户ID" />
</view>
<view class="form-group">
<text class="form-label">方案</text>
<input class="form-input" v-model="createForm.plan" placeholder="monthly/quarterly/yearly" />
</view>
<view class="form-group">
<text class="form-label">天数</text>
<input class="form-input" v-model="createForm.days" placeholder="订阅天数" type="number" />
</view>
<view class="modal-actions">
<button class="btn-default" @click="showCreate = false">取消</button>
<button class="btn-primary" @click="onCreate" :loading="creating">创建</button>
</view>
</view>
</view>
</AdminLayout>
</template>
<script>
import { get, post } from '../../utils/request'
import AdminLayout from '../../components/AdminLayout.vue'
export default {
components: { AdminLayout },
data() {
return {
subscriptions: [],
total: 0,
page: 1,
pageSize: 20,
stats: {},
showCreate: false,
creating: false,
createForm: { user_id: '', plan: 'monthly', days: 30 },
subTypeMap: { 1: '试用', 2: '正式', 3: '赠送' },
subStatusMap: { 1: '有效', 2: '已过期', 3: '已停用' }
activeTab: 'all'
}
},
computed: {
@@ -84,11 +131,17 @@ export default {
methods: {
async loadSubscriptions() {
try {
const data = await get('/api/v1/admin/subscriptions', { page: this.page, page_size: this.pageSize })
const data = await get('/api/v1/admin/subscriptions', { page: this.page, page_size: this.pageSize, tab: this.activeTab })
this.subscriptions = data.records || []
this.total = data.total || 0
if (data.stats) this.stats = data.stats
} catch (e) {}
},
onTabFilter(tab) {
this.activeTab = tab
this.page = 1
this.loadSubscriptions()
},
onPage(p) {
this.page = p
this.loadSubscriptions()
@@ -110,6 +163,18 @@ export default {
this.creating = false
}
},
planText(plan) {
const map = { monthly: '月卡', quarterly: '季卡', yearly: '年卡', trial: '试用' }
return map[plan] || plan || '-'
},
statusText(status) {
const map = { 1: '生效中', 2: '试用中', 3: '已过期' }
return map[status] || '-'
},
statusBadge(status) {
const map = { 1: 'badge badge-success', 2: 'badge badge-warning', 3: 'badge badge-default' }
return map[status] || 'badge badge-default'
},
formatDate(d) {
return d ? d.slice(0, 10) : '-'
}
@@ -118,48 +183,271 @@ export default {
</script>
<style scoped>
.container {
padding: 16px;
}
.toolbar {
.page-card {
background: #fff;
border-radius: 8px;
padding: 20px;
margin-bottom: 16px;
}
.total-text {
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.page-title {
font-size: 18px;
font-weight: 600;
color: #333;
}
.header-actions {
display: flex;
align-items: center;
gap: 8px;
}
.stats-row {
display: flex;
gap: 16px;
margin-bottom: 4px;
}
.stat-item {
flex: 1;
background: #fafafa;
border-radius: 8px;
padding: 16px;
text-align: center;
}
.stat-item.green {
background: #f6ffed;
}
.stat-item.gold {
background: #fffbe6;
}
.stat-value {
display: block;
font-size: 22px;
font-weight: 700;
color: #333;
margin-bottom: 4px;
}
.green .stat-value {
color: #52c41a;
}
.gold .stat-value {
color: #faad14;
}
.stat-label {
font-size: 13px;
color: #999999;
color: #999;
}
.tabs {
display: flex;
gap: 0;
margin-bottom: 16px;
border-bottom: 1px solid #f0f0f0;
}
.tab {
padding: 10px 20px;
font-size: 14px;
color: #666;
cursor: pointer;
border-bottom: 2px solid transparent;
}
.tab.active {
color: #E6508C;
border-bottom-color: #E6508C;
font-weight: 500;
}
.data-table {
width: 100%;
}
.t-header {
background: #fafafa;
}
.t-row {
display: flex;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid #f0f0f0;
}
.t-th {
font-size: 14px;
color: #666;
font-weight: 500;
}
.t-td {
font-size: 14px;
color: #333;
}
.flex1 {
flex: 1;
}
.flex2 {
flex: 2;
}
.badge {
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
}
.badge-success {
background: #f6ffed;
color: #52c41a;
}
.badge-warning {
background: #fffbe6;
color: #faad14;
}
.badge-default {
background: #f5f5f5;
color: #999;
}
.action-link {
color: #E6508C;
font-size: 14px;
margin-right: 8px;
cursor: pointer;
}
.pagination {
display: flex;
align-items: center;
justify-content: center;
gap: 16px;
margin-top: 16px;
gap: 8px;
margin-top: 20px;
}
.btn-page {
width: 32px;
height: 32px;
border: 1px solid #d9d9d9;
border-radius: 6px;
background: #fff;
color: #333;
font-size: 14px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
.btn-page.active {
background: #E6508C;
color: #fff;
border-color: #E6508C;
}
.btn-page[disabled] {
opacity: 0.4;
}
.page-info {
font-size: 13px;
color: #999999;
color: #999;
margin-left: 8px;
}
.create-form {
.btn-primary {
background: #E6508C;
color: #fff;
border: none;
border-radius: 6px;
cursor: pointer;
}
.btn-default {
background: #fff;
color: #333;
border: 1px solid #d9d9d9;
border-radius: 6px;
cursor: pointer;
}
.btn-sm {
height: 32px;
padding: 0 16px;
font-size: 14px;
line-height: 32px;
}
.modal-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.45);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-content {
width: 440px;
background: #fff;
border-radius: 8px;
padding: 24px;
width: 400px;
}
.form-title {
.modal-title {
font-size: 18px;
font-weight: 600;
margin-bottom: 20px;
}
.form-group {
margin-bottom: 16px;
}
.text-success {
color: #52c41a;
.form-label {
display: block;
font-size: 14px;
color: #333;
margin-bottom: 6px;
}
.text-muted {
color: #999999;
.form-input {
width: 100%;
height: 36px;
border: 1px solid #d9d9d9;
border-radius: 6px;
padding: 0 12px;
font-size: 14px;
box-sizing: border-box;
}
.modal-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
margin-top: 20px;
}
</style>