feat(ui): redesign all admin console pages
- 新增 AdminLayout 侧边栏布局组件(深色#001529侧边栏+粉色#E6508C高亮)
- 登录页:深蓝渐变背景+🌸标识+粉色登录按钮
- 仪表盘:4个统计卡片+实时护理数据表+快捷操作
- 设备管理:搜索栏+设备列表表格+状态标签+分页
- 设备详情:返回链接+信息网格+护理记录表
- 用户管理:用户列表+头像+订阅状态标签+分页
- 用户详情:用户卡片+统计数据+基本信息+护理记录
- 订阅管理:统计行+标签页筛选+订阅列表表格
- 护理记录:日期范围+搜索+模式标签+记录表格
- 操作日志:时间线列表+彩色操作者+类型标签
- 系统设置:基础配置+订阅配置+功能开关+保存按钮
这个提交包含在:
@@ -1,91 +1,95 @@
|
||||
<template>
|
||||
<view class="container">
|
||||
<view class="card" v-if="user">
|
||||
<view class="card-title">用户信息</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">用户ID</text>
|
||||
<text>{{ user.user_id }}</text>
|
||||
<AdminLayout currentPage="/pages/user/index">
|
||||
<view class="back-link" @click="goBack">‹ 返回用户列表</view>
|
||||
|
||||
<view class="page-card" v-if="user">
|
||||
<view class="page-header">
|
||||
<text class="page-title">👥 用户详情 - {{ user.nickname || user.user_id }}</text>
|
||||
<button class="btn-primary btn-sm">发送通知</button>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">昵称</text>
|
||||
<text>{{ user.nickname || '-' }}</text>
|
||||
|
||||
<view class="user-profile">
|
||||
<view class="avatar">👤</view>
|
||||
<view class="profile-info">
|
||||
<text class="user-name">{{ user.nickname || '-' }}</text>
|
||||
<text class="user-phone">{{ user.phone || '-' }}</text>
|
||||
<text class="badge badge-success">{{ subStatusText(user.subscription_status) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">手机号</text>
|
||||
<text>{{ user.phone || '-' }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">注册时间</text>
|
||||
<text>{{ formatDate(user.created_at) }}</text>
|
||||
|
||||
<view class="stats-row">
|
||||
<view class="stat-item pink-bg">
|
||||
<text class="stat-value">{{ user.treatment_count || 0 }}</text>
|
||||
<text class="stat-label">护理次数</text>
|
||||
</view>
|
||||
<view class="stat-item">
|
||||
<text class="stat-value">{{ user.total_duration || '0h' }}</text>
|
||||
<text class="stat-label">累计时长</text>
|
||||
</view>
|
||||
<view class="stat-item">
|
||||
<text class="stat-value">¥{{ user.total_spent || 0 }}</text>
|
||||
<text class="stat-label">累计消费</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="card" v-if="user && user.devices">
|
||||
<view class="card-title">绑定设备</view>
|
||||
<view v-for="(d, idx) in user.devices" :key="idx" class="device-item">
|
||||
<view class="info-row">
|
||||
<text class="info-label">设备</text>
|
||||
<text>{{ d.device_name || d.device_id }}</text>
|
||||
<view class="page-card" v-if="user">
|
||||
<view class="card-title">基本信息</view>
|
||||
<view class="info-grid">
|
||||
<view class="info-item">
|
||||
<text class="info-label">绑定设备</text>
|
||||
<text class="info-value">{{ user.devices && user.devices.length > 0 ? user.devices[0].device_name || user.devices[0].device_id : '无' }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">绑定时间</text>
|
||||
<text>{{ formatDate(d.bound_at) }}</text>
|
||||
<view class="info-item">
|
||||
<text class="info-label">注册时间</text>
|
||||
<text class="info-value">{{ formatDate(user.created_at) }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="info-label">订阅类型</text>
|
||||
<text class="info-value">{{ subTypeText(user.subscription_type) }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="info-label">订阅到期</text>
|
||||
<text class="info-value">{{ formatDate(user.subscription_expire) }}</text>
|
||||
</view>
|
||||
<view class="divider" v-if="idx < user.devices.length - 1"></view>
|
||||
</view>
|
||||
<view v-if="user.devices.length === 0" class="text-muted">暂无绑定设备</view>
|
||||
</view>
|
||||
|
||||
<view class="card" v-if="user && user.subscriptions">
|
||||
<view class="card-title">订阅记录</view>
|
||||
<view v-for="(s, idx) in user.subscriptions" :key="idx">
|
||||
<view class="info-row">
|
||||
<text class="info-label">类型</text>
|
||||
<text>{{ subTypeMap[s.type] || '未知' }}</text>
|
||||
<view class="page-card" v-if="user && user.recent_treatments">
|
||||
<view class="card-title">护理记录</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 flex2">设备</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">状态</text>
|
||||
<text :class="s.status === 1 ? 'text-success' : 'text-muted'">
|
||||
{{ subStatusMap[s.status] || '未知' }}
|
||||
</text>
|
||||
<view class="t-body">
|
||||
<view class="t-row" v-for="(t, idx) in user.recent_treatments" :key="idx">
|
||||
<text class="t-td flex2">{{ formatDate(t.started_at) }}</text>
|
||||
<text class="t-td flex1">{{ Math.floor((t.total_duration_ms || 0) / 60000) }}分钟</text>
|
||||
<text class="t-td flex2">{{ t.device_id || '-' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">有效期</text>
|
||||
<text>{{ formatDate(s.started_at) }} ~ {{ formatDate(s.expired_at) }}</text>
|
||||
</view>
|
||||
<view class="divider" v-if="idx < user.subscriptions.length - 1"></view>
|
||||
</view>
|
||||
<view v-if="user.subscriptions.length === 0" class="text-muted">暂无订阅</view>
|
||||
</view>
|
||||
|
||||
<view class="card" v-if="user && user.recent_treatments">
|
||||
<view class="card-title">最近护理</view>
|
||||
<view v-for="(t, idx) in user.recent_treatments" :key="idx">
|
||||
<view class="info-row">
|
||||
<text class="info-label">时间</text>
|
||||
<text>{{ formatDate(t.started_at) }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">时长</text>
|
||||
<text>{{ Math.floor((t.total_duration_ms || 0) / 60000) }}分钟</text>
|
||||
</view>
|
||||
<view class="divider" v-if="idx < user.recent_treatments.length - 1"></view>
|
||||
<view class="view-all" v-if="user.treatment_count > 5">
|
||||
<text class="view-all-link" @click="goRecords">查看全部{{ user.treatment_count }}条记录 ›</text>
|
||||
</view>
|
||||
<view v-if="user.recent_treatments.length === 0" class="text-muted">暂无记录</view>
|
||||
</view>
|
||||
</view>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { get } from '../../utils/request'
|
||||
import AdminLayout from '../../components/AdminLayout.vue'
|
||||
|
||||
export default {
|
||||
components: { AdminLayout },
|
||||
data() {
|
||||
return {
|
||||
user: null,
|
||||
userId: '',
|
||||
subTypeMap: { 1: '试用', 2: '正式', 3: '赠送' },
|
||||
subStatusMap: { 1: '有效', 2: '已过期', 3: '已停用' }
|
||||
userId: ''
|
||||
}
|
||||
},
|
||||
onLoad(options) {
|
||||
@@ -100,6 +104,20 @@ export default {
|
||||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||
}
|
||||
},
|
||||
goBack() {
|
||||
uni.navigateBack()
|
||||
},
|
||||
goRecords() {
|
||||
uni.navigateTo({ url: '/pages/record/index?user_id=' + this.userId })
|
||||
},
|
||||
subStatusText(status) {
|
||||
const map = { yearly: '年卡会员', monthly: '月卡会员', trial: '试用中', none: '未订阅' }
|
||||
return map[status] || '未订阅'
|
||||
},
|
||||
subTypeText(type) {
|
||||
const map = { yearly: '年卡', monthly: '月卡', trial: '试用' }
|
||||
return map[type] || '-'
|
||||
},
|
||||
formatDate(d) {
|
||||
return d ? d.slice(0, 10) : '-'
|
||||
}
|
||||
@@ -108,45 +126,203 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
padding: 16px;
|
||||
.back-link {
|
||||
color: #E6508C;
|
||||
font-size: 14px;
|
||||
margin-bottom: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #ffffff;
|
||||
.page-card {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.user-profile {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 50%;
|
||||
background: #f0f0f0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
.profile-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.user-phone {
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.badge-success {
|
||||
background: #f6ffed;
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
.stats-row {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
flex: 1;
|
||||
background: #fafafa;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin-bottom: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-item.pink-bg {
|
||||
background: #fff0f6;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
display: block;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: #333;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.pink-bg .stat-value {
|
||||
color: #E6508C;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 13px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 12px;
|
||||
color: #333;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
.info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 8px 0;
|
||||
font-size: 14px;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
color: #999999;
|
||||
font-size: 13px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.divider {
|
||||
height: 1px;
|
||||
background: #eeeeee;
|
||||
margin: 8px 0;
|
||||
.info-value {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.text-success {
|
||||
color: #52c41a;
|
||||
.data-table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
color: #999999;
|
||||
.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;
|
||||
}
|
||||
|
||||
.view-all {
|
||||
text-align: center;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.view-all-link {
|
||||
color: #E6508C;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #E6508C;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
height: 32px;
|
||||
padding: 0 16px;
|
||||
font-size: 14px;
|
||||
line-height: 32px;
|
||||
}
|
||||
</style>
|
||||
|
||||
在新工单中引用
屏蔽一个用户