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 行删除
+217 -35
查看文件
@@ -1,40 +1,67 @@
<template>
<view class="container">
<view class="toolbar">
<u-search v-model="keyword" placeholder="搜索用户昵称" @search="onSearch" @clear="onSearch" />
</view>
<AdminLayout currentPage="/pages/user/index">
<view class="page-card">
<view class="page-header">
<text class="page-title">👥 用户管理</text>
<view class="header-actions">
<input
class="search-input"
v-model="keyword"
placeholder="搜索用户昵称"
placeholder-class="input-placeholder"
@confirm="onSearch"
/>
<button class="btn-primary btn-sm" @click="onSearch">搜索</button>
<button class="btn-default btn-sm">导出用户</button>
</view>
</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-tr>
<u-tr v-for="item in users" :key="item.user_id" @click="onDetail(item.user_id)">
<u-td>{{ item.user_id }}</u-td>
<u-td>{{ item.nickname || '-' }}</u-td>
<u-td>{{ item.device_count || 0 }}</u-td>
<u-td>{{ formatDate(item.subscription_expire) }}</u-td>
<u-td>{{ formatDate(item.created_at) }}</u-td>
</u-tr>
</u-table>
</view>
<view class="data-table">
<view class="t-header">
<view class="t-row">
<text class="t-th flex2">用户</text>
<text class="t-th flex2">手机号</text>
<text class="t-th flex1">绑定设备</text>
<text class="t-th flex1">护理次数</text>
<text class="t-th flex1">订阅状态</text>
<text class="t-th flex2">注册时间</text>
<text class="t-th flex1">操作</text>
</view>
</view>
<view class="t-body">
<view class="t-row" v-for="item in users" :key="item.user_id">
<text class="t-td flex2">👤 {{ item.nickname || '-' }}</text>
<text class="t-td flex2">{{ item.phone || '-' }}</text>
<text class="t-td flex1">{{ item.device_count || 0 }}</text>
<text class="t-td flex1">{{ item.treatment_count || 0 }}</text>
<text class="t-td flex1">
<text :class="subBadge(item.subscription_status)">{{ subStatusText(item.subscription_status) }}</text>
</text>
<text class="t-td flex2">{{ formatDate(item.created_at) }}</text>
<text class="t-td flex1">
<text class="action-link" @click="onDetail(item.user_id)">详情</text>
<text class="action-link" @click="onDetail(item.user_id)">记录</text>
</text>
</view>
</view>
</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 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>
</AdminLayout>
</template>
<script>
import { get } from '../../utils/request'
import AdminLayout from '../../components/AdminLayout.vue'
export default {
components: { AdminLayout },
data() {
return {
users: [],
@@ -55,7 +82,7 @@ export default {
methods: {
async loadUsers() {
try {
const data = await get('/api/v1/admin/users', { page: this.page, page_size: this.pageSize })
const data = await get('/api/v1/admin/users', { page: this.page, page_size: this.pageSize, keyword: this.keyword })
this.users = data.records || []
this.total = data.total || 0
} catch (e) {}
@@ -71,6 +98,19 @@ export default {
onDetail(userId) {
uni.navigateTo({ url: '/pages/user-detail/index?user_id=' + userId })
},
subStatusText(status) {
const map = { yearly: '年卡', monthly: '月卡', trial: '试用中', none: '未订阅' }
return map[status] || '未订阅'
},
subBadge(status) {
const map = {
yearly: 'badge badge-success',
monthly: 'badge badge-success',
trial: 'badge badge-warning',
none: 'badge badge-default'
}
return map[status] || 'badge badge-default'
},
formatDate(d) {
return d ? d.slice(0, 10) : '-'
}
@@ -79,24 +119,166 @@ export default {
</script>
<style scoped>
.container {
padding: 16px;
.page-card {
background: #fff;
border-radius: 8px;
padding: 20px;
}
.toolbar {
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;
}
.header-actions {
display: flex;
align-items: center;
gap: 8px;
}
.search-input {
width: 220px;
height: 32px;
border: 1px solid #d9d9d9;
border-radius: 6px;
padding: 0 12px;
font-size: 14px;
box-sizing: border-box;
}
.input-placeholder {
color: #bfbfbf;
}
.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;
}
.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: 12px;
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;
}
</style>