feat(ui): redesign all admin console pages
- 新增 AdminLayout 侧边栏布局组件(深色#001529侧边栏+粉色#E6508C高亮)
- 登录页:深蓝渐变背景+🌸标识+粉色登录按钮
- 仪表盘:4个统计卡片+实时护理数据表+快捷操作
- 设备管理:搜索栏+设备列表表格+状态标签+分页
- 设备详情:返回链接+信息网格+护理记录表
- 用户管理:用户列表+头像+订阅状态标签+分页
- 用户详情:用户卡片+统计数据+基本信息+护理记录
- 订阅管理:统计行+标签页筛选+订阅列表表格
- 护理记录:日期范围+搜索+模式标签+记录表格
- 操作日志:时间线列表+彩色操作者+类型标签
- 系统设置:基础配置+订阅配置+功能开关+保存按钮
这个提交包含在:
@@ -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>
|
||||
|
||||
在新工单中引用
屏蔽一个用户