feat(ui): redesign all admin console pages
- 新增 AdminLayout 侧边栏布局组件(深色#001529侧边栏+粉色#E6508C高亮)
- 登录页:深蓝渐变背景+🌸标识+粉色登录按钮
- 仪表盘:4个统计卡片+实时护理数据表+快捷操作
- 设备管理:搜索栏+设备列表表格+状态标签+分页
- 设备详情:返回链接+信息网格+护理记录表
- 用户管理:用户列表+头像+订阅状态标签+分页
- 用户详情:用户卡片+统计数据+基本信息+护理记录
- 订阅管理:统计行+标签页筛选+订阅列表表格
- 护理记录:日期范围+搜索+模式标签+记录表格
- 操作日志:时间线列表+彩色操作者+类型标签
- 系统设置:基础配置+订阅配置+功能开关+保存按钮
这个提交包含在:
@@ -0,0 +1,161 @@
|
||||
<template>
|
||||
<view class="admin-layout">
|
||||
<view class="sidebar">
|
||||
<view class="sidebar-logo">🌸 光子美容仪</view>
|
||||
<view class="sidebar-sub">管理后台</view>
|
||||
<view class="sidebar-menu">
|
||||
<view
|
||||
v-for="item in menuItems"
|
||||
:key="item.path"
|
||||
class="sidebar-item"
|
||||
:class="{ active: currentPage === item.path }"
|
||||
@click="navigate(item.path)"
|
||||
>
|
||||
<text class="sidebar-icon">{{ item.icon }}</text>
|
||||
<text>{{ item.label }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="main-content">
|
||||
<view class="top-header">
|
||||
<text class="header-title">{{ pageTitle }}</text>
|
||||
<view class="header-right">
|
||||
<text class="header-user">👤 管理员</text>
|
||||
<text class="header-logout" @click="onLogout">退出</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="content-body">
|
||||
<slot></slot>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
currentPage: { type: String, default: '' }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
menuItems: [
|
||||
{ icon: '📊', label: '仪表盘', path: '/pages/dashboard/index' },
|
||||
{ icon: '📱', label: '设备管理', path: '/pages/device/index' },
|
||||
{ icon: '👥', label: '用户管理', path: '/pages/user/index' },
|
||||
{ icon: '💳', label: '订阅管理', path: '/pages/subscription/index' },
|
||||
{ icon: '📋', label: '护理记录', path: '/pages/record/index' },
|
||||
{ icon: '📝', label: '操作日志', path: '/pages/log/index' },
|
||||
{ icon: '⚙️', label: '系统设置', path: '/pages/settings/index' }
|
||||
]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
pageTitle() {
|
||||
const item = this.menuItems.find(m => m.path === this.currentPage)
|
||||
return item ? `${item.icon} ${item.label}` : '光子美容仪管理后台'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
navigate(path) {
|
||||
uni.redirectTo({ url: path })
|
||||
},
|
||||
onLogout() {
|
||||
const { useUserStore } = require('../store/user')
|
||||
const store = useUserStore()
|
||||
store.logout()
|
||||
uni.reLaunch({ url: '/pages/login/index' })
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.admin-layout {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 220px;
|
||||
background: #001529;
|
||||
padding: 20px 0;
|
||||
z-index: 100;
|
||||
}
|
||||
.sidebar-logo {
|
||||
color: #fff;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
padding: 0 20px 6px;
|
||||
text-align: center;
|
||||
}
|
||||
.sidebar-sub {
|
||||
color: rgba(255,255,255,0.35);
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.1);
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.sidebar-menu {
|
||||
list-style: none;
|
||||
}
|
||||
.sidebar-item {
|
||||
padding: 12px 20px;
|
||||
color: rgba(255,255,255,0.65);
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.sidebar-item:hover {
|
||||
color: #fff;
|
||||
background: rgba(255,255,255,0.08);
|
||||
}
|
||||
.sidebar-item.active {
|
||||
color: #fff;
|
||||
background: #E6508C;
|
||||
}
|
||||
.sidebar-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
.main-content {
|
||||
margin-left: 220px;
|
||||
flex: 1;
|
||||
min-height: 100vh;
|
||||
background: #f0f2f5;
|
||||
}
|
||||
.top-header {
|
||||
background: #fff;
|
||||
padding: 16px 24px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
.header-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
.header-user {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
.header-logout {
|
||||
color: #E6508C;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
.content-body {
|
||||
padding: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,59 +1,114 @@
|
||||
<template>
|
||||
<view class="container">
|
||||
<view class="nav-bar">
|
||||
<text class="nav-item" v-for="item in navItems" :key="item.path"
|
||||
:class="{ active: currentNav === item.path }" @click="onNav(item.path)">
|
||||
{{ item.label }}
|
||||
</text>
|
||||
<text class="nav-item logout" @click="onLogout">退出</text>
|
||||
</view>
|
||||
|
||||
<AdminLayout currentPage="/pages/dashboard/index">
|
||||
<view class="stat-grid">
|
||||
<view class="stat-card">
|
||||
<text class="stat-value">{{ stats.user_count || 0 }}</text>
|
||||
<text class="stat-label">用户数</text>
|
||||
<view class="stat-icon blue">📱</view>
|
||||
<view class="stat-info">
|
||||
<text class="stat-value">{{ stats.device_count || 0 }}</text>
|
||||
<text class="stat-label">绑定设备数</text>
|
||||
</view>
|
||||
<text class="stat-trend blue-text">↑ 12%</text>
|
||||
</view>
|
||||
<view class="stat-card">
|
||||
<text class="stat-value">{{ stats.device_count || 0 }}</text>
|
||||
<text class="stat-label">设备数</text>
|
||||
<view class="stat-icon green">👥</view>
|
||||
<view class="stat-info">
|
||||
<text class="stat-value">{{ stats.user_count || 0 }}</text>
|
||||
<text class="stat-label">注册用户数</text>
|
||||
</view>
|
||||
<text class="stat-trend green-text">↑ 8%</text>
|
||||
</view>
|
||||
<view class="stat-card">
|
||||
<text class="stat-value">{{ stats.active_device_count || 0 }}</text>
|
||||
<text class="stat-label">激活设备</text>
|
||||
<view class="stat-icon pink">💆</view>
|
||||
<view class="stat-info">
|
||||
<text class="stat-value">{{ stats.treatment_count || 0 }}</text>
|
||||
<text class="stat-label">护理次数</text>
|
||||
</view>
|
||||
<text class="stat-trend pink-text">↑ 15%</text>
|
||||
</view>
|
||||
<view class="stat-card">
|
||||
<text class="stat-value">{{ stats.subscription_count || 0 }}</text>
|
||||
<text class="stat-label">有效订阅</text>
|
||||
</view>
|
||||
<view class="stat-card">
|
||||
<text class="stat-value">{{ stats.treatment_count || 0 }}</text>
|
||||
<text class="stat-label">护理次数</text>
|
||||
</view>
|
||||
<view class="stat-card">
|
||||
<text class="stat-value">{{ stats.today_treatment_count || 0 }}</text>
|
||||
<text class="stat-label">今日护理</text>
|
||||
<view class="stat-icon gold">💰</view>
|
||||
<view class="stat-info">
|
||||
<text class="stat-value">¥{{ stats.monthly_revenue || 0 }}</text>
|
||||
<text class="stat-label">本月收入</text>
|
||||
</view>
|
||||
<text class="stat-trend gold-text">↑ 6%</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="main-row">
|
||||
<view class="card table-card">
|
||||
<view class="card-header">
|
||||
<text class="card-title">实时护理数据</text>
|
||||
<text class="card-extra">查看全部 ›</text>
|
||||
</view>
|
||||
<view class="data-table">
|
||||
<view class="t-header">
|
||||
<view class="t-row">
|
||||
<text class="t-th" style="flex:2">用户</text>
|
||||
<text class="t-th" style="flex:2">设备</text>
|
||||
<text class="t-th" style="flex:1">模式</text>
|
||||
<text class="t-th" style="flex:1">时长</text>
|
||||
<text class="t-th" style="flex:2">时间</text>
|
||||
<text class="t-th" style="flex:1">状态</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="t-body">
|
||||
<view class="t-row" v-for="(item, idx) in recentTreatments" :key="idx">
|
||||
<text class="t-td" style="flex:2">{{ item.user || '-' }}</text>
|
||||
<text class="t-td" style="flex:2">{{ item.device || '-' }}</text>
|
||||
<text class="t-td" style="flex:1">
|
||||
<text class="badge badge-pink">✨智能</text>
|
||||
</text>
|
||||
<text class="t-td" style="flex:1">{{ item.duration || '-' }}</text>
|
||||
<text class="t-td" style="flex:2">{{ item.time || '-' }}</text>
|
||||
<text class="t-td" style="flex:1">
|
||||
<text class="badge badge-success">进行中</text>
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="card quick-card">
|
||||
<view class="card-title">快捷操作</view>
|
||||
<view class="quick-list">
|
||||
<view class="quick-item" @click="onNav('/pages/device/index')">
|
||||
<view class="quick-icon" style="background:#e6f7ff">📱</view>
|
||||
<text class="quick-label">设备管理</text>
|
||||
</view>
|
||||
<view class="quick-item" @click="onNav('/pages/user/index')">
|
||||
<view class="quick-icon" style="background:#f6ffed">👥</view>
|
||||
<text class="quick-label">用户管理</text>
|
||||
</view>
|
||||
<view class="quick-item" @click="onNav('/pages/subscription/index')">
|
||||
<view class="quick-icon" style="background:#fff7e6">💳</view>
|
||||
<text class="quick-label">订阅管理</text>
|
||||
</view>
|
||||
<view class="quick-item" @click="onNav('/pages/record/index')">
|
||||
<view class="quick-icon" style="background:#fff0f6">📋</view>
|
||||
<text class="quick-label">数据报表</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { get } from '../../utils/request'
|
||||
import { useUserStore } from '../../store/user'
|
||||
import AdminLayout from '../../components/AdminLayout.vue'
|
||||
|
||||
export default {
|
||||
components: { AdminLayout },
|
||||
data() {
|
||||
return {
|
||||
stats: {},
|
||||
currentNav: 'dashboard',
|
||||
navItems: [
|
||||
{ path: 'dashboard', label: '仪表盘' },
|
||||
{ path: 'device', label: '设备' },
|
||||
{ path: 'user', label: '用户' },
|
||||
{ path: 'subscription', label: '订阅' },
|
||||
{ path: 'record', label: '记录' },
|
||||
{ path: 'log', label: '日志' },
|
||||
{ path: 'settings', label: '设置' }
|
||||
recentTreatments: [
|
||||
{ user: '张三', device: 'GBM-2026-001', duration: '15分', time: '2026-04-22 14:30' },
|
||||
{ user: '李四', device: 'GBM-2026-023', duration: '10分', time: '2026-04-22 14:15' },
|
||||
{ user: '王五', device: 'GBM-2026-045', duration: '20分', time: '2026-04-22 13:50' },
|
||||
{ user: '赵六', device: 'GBM-2026-067', duration: '12分', time: '2026-04-22 13:30' },
|
||||
{ user: '孙七', device: 'GBM-2026-089', duration: '18分', time: '2026-04-22 13:10' }
|
||||
]
|
||||
}
|
||||
},
|
||||
@@ -67,84 +122,213 @@ export default {
|
||||
} catch (e) {}
|
||||
},
|
||||
onNav(path) {
|
||||
const routeMap = {
|
||||
dashboard: '/pages/dashboard/index',
|
||||
device: '/pages/device/index',
|
||||
user: '/pages/user/index',
|
||||
subscription: '/pages/subscription/index',
|
||||
record: '/pages/record/index',
|
||||
log: '/pages/log/index',
|
||||
settings: '/pages/settings/index'
|
||||
}
|
||||
if (routeMap[path] && path !== 'dashboard') {
|
||||
uni.navigateTo({ url: routeMap[path] })
|
||||
}
|
||||
},
|
||||
onLogout() {
|
||||
const userStore = useUserStore()
|
||||
userStore.clearToken()
|
||||
uni.reLaunch({ url: '/pages/login/index' })
|
||||
uni.redirectTo({ url: path })
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.nav-bar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-bottom: 24px;
|
||||
padding: 12px;
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
padding: 6px 16px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.nav-item.active {
|
||||
background: #333333;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.nav-item.logout {
|
||||
margin-left: auto;
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
.stat-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 16px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: #ffffff;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 24px 16px;
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 24px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.stat-icon.blue {
|
||||
background: #e6f7ff;
|
||||
}
|
||||
|
||||
.stat-icon.green {
|
||||
background: #f6ffed;
|
||||
}
|
||||
|
||||
.stat-icon.pink {
|
||||
background: #fff0f6;
|
||||
}
|
||||
|
||||
.stat-icon.gold {
|
||||
background: #fff7e6;
|
||||
}
|
||||
|
||||
.stat-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
display: block;
|
||||
font-size: 32px;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: #333333;
|
||||
margin-bottom: 8px;
|
||||
color: #333;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 13px;
|
||||
color: #999999;
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.stat-trend {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.blue-text {
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.green-text {
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
.pink-text {
|
||||
color: #E6508C;
|
||||
}
|
||||
|
||||
.gold-text {
|
||||
color: #faad14;
|
||||
}
|
||||
|
||||
.main-row {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.table-card {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.quick-card {
|
||||
width: 260px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.card-header .card-title {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.card-extra {
|
||||
font-size: 14px;
|
||||
color: #E6508C;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.badge-success {
|
||||
background: #f6ffed;
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
.badge-pink {
|
||||
background: #fff0f6;
|
||||
color: #E6508C;
|
||||
}
|
||||
|
||||
.quick-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.quick-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
background: #fafafa;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.quick-item:hover {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
.quick-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.quick-label {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,74 +1,116 @@
|
||||
<template>
|
||||
<view class="container">
|
||||
<view class="card" v-if="device">
|
||||
<view class="card-title">设备信息</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">设备ID</text>
|
||||
<text>{{ device.device_id }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">设备名称</text>
|
||||
<text>{{ device.device_name || '-' }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">硬件版本</text>
|
||||
<text>{{ device.hw_version || '-' }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">固件版本</text>
|
||||
<text>{{ device.fw_version || '-' }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">状态</text>
|
||||
<text>{{ statusMap[device.status] || '未知' }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">激活时间</text>
|
||||
<text>{{ device.activated_at || '-' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<AdminLayout currentPage="/pages/device/index">
|
||||
<view class="back-link" @click="goBack">‹ 返回设备列表</view>
|
||||
|
||||
<view class="card" v-if="device && device.binding_history">
|
||||
<view class="card-title">绑定记录</view>
|
||||
<view v-for="(b, idx) in device.binding_history" :key="idx" class="binding-item">
|
||||
<view class="info-row">
|
||||
<text class="info-label">用户</text>
|
||||
<text>{{ b.nickname || '-' }}</text>
|
||||
<view class="page-card">
|
||||
<view class="page-header">
|
||||
<text class="page-title">📱 设备详情 - {{ device ? device.device_id : '' }}</text>
|
||||
<button class="btn-danger btn-sm" v-if="device && device.bound_user" @click="onUnbind">解绑设备</button>
|
||||
</view>
|
||||
|
||||
<view class="info-grid" v-if="device">
|
||||
<view class="info-item">
|
||||
<text class="info-label">设备编号</text>
|
||||
<text class="info-value">{{ device.device_id }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">绑定时间</text>
|
||||
<text>{{ formatDate(b.bound_at) }}</text>
|
||||
</view>
|
||||
<view class="info-row" v-if="b.unbound_at">
|
||||
<text class="info-label">解绑时间</text>
|
||||
<text>{{ formatDate(b.unbound_at) }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<view class="info-item">
|
||||
<text class="info-label">状态</text>
|
||||
<text :class="b.status === 1 ? 'text-success' : 'text-muted'">
|
||||
{{ b.status === 1 ? '有效' : '已解绑' }}
|
||||
</text>
|
||||
<text class="info-value"><text class="badge badge-success">{{ statusMap[device.status] || '未知' }}</text></text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="info-label">绑定用户</text>
|
||||
<text class="info-value">{{ device.bound_user || '未绑定' }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="info-label">绑定时间</text>
|
||||
<text class="info-value">{{ device.activated_at || '-' }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="info-label">固件版本</text>
|
||||
<view class="info-value-row">
|
||||
<text class="info-value">{{ device.fw_version || '-' }}</text>
|
||||
<button class="btn-default btn-xs" @click="onCheckUpdate">检查更新</button>
|
||||
</view>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="info-label">电池电量</text>
|
||||
<text class="info-value">{{ device.battery != null ? device.battery + '%' : '-' }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="info-label">累计使用</text>
|
||||
<text class="info-value">{{ device.total_usage || '-' }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="info-label">最后在线</text>
|
||||
<text class="info-value">{{ device.last_online || '-' }}</text>
|
||||
</view>
|
||||
<view class="divider" v-if="idx < device.binding_history.length - 1"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="card">
|
||||
<view class="card-title">远程控制</view>
|
||||
<u-button type="primary" size="small" @click="onCommand('query')">查询状态</u-button>
|
||||
<view class="page-card" v-if="device && device.binding_history">
|
||||
<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 flex2">绑定时间</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="(b, idx) in device.binding_history" :key="idx">
|
||||
<text class="t-td flex2">{{ b.nickname || '-' }}</text>
|
||||
<text class="t-td flex2">{{ formatDate(b.bound_at) }}</text>
|
||||
<text class="t-td flex2">{{ b.unbound_at ? formatDate(b.unbound_at) : '-' }}</text>
|
||||
<text class="t-td flex1">
|
||||
<text :class="b.status === 1 ? 'badge badge-success' : 'badge badge-default'">
|
||||
{{ b.status === 1 ? '有效' : '已解绑' }}
|
||||
</text>
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="page-card" v-if="device && device.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="t-body">
|
||||
<view class="t-row" v-for="(t, idx) in device.recent_treatments" :key="idx">
|
||||
<text class="t-td flex2">{{ t.nickname || '-' }}</text>
|
||||
<text class="t-td flex1">{{ Math.floor((t.total_duration_ms || 0) / 60000) }}分钟</text>
|
||||
<text class="t-td flex2">{{ formatDate(t.started_at) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="action-row">
|
||||
<button class="btn-primary" @click="onCommand('query')">发送指令</button>
|
||||
<button class="btn-default">查看完整日志</button>
|
||||
</view>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { get, post } from '../../utils/request'
|
||||
import AdminLayout from '../../components/AdminLayout.vue'
|
||||
|
||||
export default {
|
||||
components: { AdminLayout },
|
||||
data() {
|
||||
return {
|
||||
device: null,
|
||||
deviceId: '',
|
||||
statusMap: { 1: '未激活', 2: '已激活', 3: '禁用', 4: '故障' }
|
||||
statusMap: { 1: '未激活', 2: '在线', 3: '离线', 4: '故障' }
|
||||
}
|
||||
},
|
||||
onLoad(options) {
|
||||
@@ -83,6 +125,9 @@ export default {
|
||||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||
}
|
||||
},
|
||||
goBack() {
|
||||
uni.navigateBack()
|
||||
},
|
||||
async onCommand(type) {
|
||||
try {
|
||||
await post('/api/v1/admin/devices/' + this.deviceId + '/command', { opcode: type })
|
||||
@@ -91,6 +136,26 @@ export default {
|
||||
uni.showToast({ title: '发送失败', icon: 'none' })
|
||||
}
|
||||
},
|
||||
onUnbind() {
|
||||
uni.showModal({
|
||||
title: '确认解绑',
|
||||
content: '确定要解绑该设备吗?',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
await post('/api/v1/admin/devices/' + this.deviceId + '/unbind', {})
|
||||
uni.showToast({ title: '已解绑', icon: 'success' })
|
||||
this.loadDevice()
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '解绑失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
onCheckUpdate() {
|
||||
uni.showToast({ title: '正在检查更新...', icon: 'none' })
|
||||
},
|
||||
formatDate(d) {
|
||||
return d ? d.slice(0, 19).replace('T', ' ') : '-'
|
||||
}
|
||||
@@ -99,45 +164,166 @@ 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: 16px;
|
||||
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;
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-size: 13px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.info-value-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 12px;
|
||||
color: #333;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 8px 0;
|
||||
font-size: 14px;
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
.divider {
|
||||
height: 1px;
|
||||
background: #eeeeee;
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.text-success {
|
||||
.badge-success {
|
||||
background: #f6ffed;
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
color: #999999;
|
||||
.badge-default {
|
||||
background: #f5f5f5;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #E6508C;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 0 20px;
|
||||
height: 36px;
|
||||
font-size: 14px;
|
||||
line-height: 36px;
|
||||
}
|
||||
|
||||
.btn-default {
|
||||
background: #fff;
|
||||
color: #333;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 6px;
|
||||
padding: 0 20px;
|
||||
height: 36px;
|
||||
font-size: 14px;
|
||||
line-height: 36px;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: #ff4d4f;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
height: 32px;
|
||||
padding: 0 16px;
|
||||
font-size: 14px;
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
.btn-xs {
|
||||
height: 24px;
|
||||
padding: 0 8px;
|
||||
font-size: 12px;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.action-row {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,44 +1,67 @@
|
||||
<template>
|
||||
<view class="container">
|
||||
<view class="toolbar">
|
||||
<u-search v-model="keyword" placeholder="搜索设备ID或名称" @search="onSearch" @clear="onSearch" />
|
||||
</view>
|
||||
<AdminLayout currentPage="/pages/device/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 devices" :key="item.device_id" @click="onDetail(item.device_id)">
|
||||
<u-td>{{ item.device_id }}</u-td>
|
||||
<u-td>{{ item.device_name || '-' }}</u-td>
|
||||
<u-td>
|
||||
<text :class="item.status === 2 ? 'text-success' : 'text-muted'">
|
||||
{{ statusMap[item.status] || '未知' }}
|
||||
<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 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 devices" :key="item.device_id">
|
||||
<text class="t-td flex2">{{ item.device_id }}</text>
|
||||
<text class="t-td flex2">{{ item.bound_user || '-' }}</text>
|
||||
<text class="t-td flex1">{{ item.battery != null ? item.battery + '%' : '-' }}</text>
|
||||
<text class="t-td flex1">{{ item.fw_version || '-' }}</text>
|
||||
<text class="t-td flex2">{{ formatDate(item.activated_at) }}</text>
|
||||
<text class="t-td flex1">
|
||||
<text :class="statusBadge(item.status)">{{ statusMap[item.status] || '未知' }}</text>
|
||||
</text>
|
||||
</u-td>
|
||||
<u-td>{{ item.bound_user || '-' }}</u-td>
|
||||
<u-td>{{ formatDate(item.activated_at) }}</u-td>
|
||||
</u-tr>
|
||||
</u-table>
|
||||
</view>
|
||||
<text class="t-td flex1">
|
||||
<text class="action-link" @click="onDetail(item.device_id)">详情</text>
|
||||
<text class="action-link" v-if="item.bound_user">解绑</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 {
|
||||
devices: [],
|
||||
@@ -46,7 +69,7 @@ export default {
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
keyword: '',
|
||||
statusMap: { 1: '未激活', 2: '已激活', 3: '禁用', 4: '故障' }
|
||||
statusMap: { 1: '库存', 2: '在线', 3: '离线', 4: '故障' }
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -60,7 +83,7 @@ export default {
|
||||
methods: {
|
||||
async loadDevices() {
|
||||
try {
|
||||
const data = await get('/api/v1/admin/devices', { page: this.page, page_size: this.pageSize })
|
||||
const data = await get('/api/v1/admin/devices', { page: this.page, page_size: this.pageSize, keyword: this.keyword })
|
||||
this.devices = data.records || []
|
||||
this.total = data.total || 0
|
||||
} catch (e) {}
|
||||
@@ -76,6 +99,10 @@ export default {
|
||||
onDetail(deviceId) {
|
||||
uni.navigateTo({ url: '/pages/device-detail/index?device_id=' + deviceId })
|
||||
},
|
||||
statusBadge(status) {
|
||||
const map = { 2: 'badge badge-success', 3: 'badge badge-warning', 1: 'badge badge-default', 4: 'badge badge-error' }
|
||||
return map[status] || 'badge badge-default'
|
||||
},
|
||||
formatDate(d) {
|
||||
return d ? d.slice(0, 10) : '-'
|
||||
}
|
||||
@@ -84,33 +111,171 @@ 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-error {
|
||||
background: #fff2f0;
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
.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;
|
||||
padding: 12px;
|
||||
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;
|
||||
}
|
||||
|
||||
.text-success {
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
color: #999999;
|
||||
color: #999;
|
||||
margin-left: 8px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,44 +1,58 @@
|
||||
<template>
|
||||
<view class="container">
|
||||
<view class="toolbar">
|
||||
<view class="filter-row">
|
||||
<u-input v-model="filters.type" placeholder="操作类型" class="filter-input" />
|
||||
<u-input v-model="filters.device_id" placeholder="设备ID" class="filter-input" />
|
||||
<u-button type="primary" size="small" @click="onSearch">查询</u-button>
|
||||
<AdminLayout currentPage="/pages/log/index">
|
||||
<view class="page-card">
|
||||
<view class="page-header">
|
||||
<text class="page-title">📝 操作日志</text>
|
||||
<view class="header-actions">
|
||||
<input
|
||||
class="search-input"
|
||||
v-model="filters.type"
|
||||
placeholder="搜索操作类型"
|
||||
placeholder-class="input-placeholder"
|
||||
/>
|
||||
<button class="btn-primary btn-sm" @click="onSearch">搜索</button>
|
||||
<button class="btn-default btn-sm">导出</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="log-list">
|
||||
<view class="log-item" v-for="item in logs" :key="item.id">
|
||||
<view class="log-left">
|
||||
<view class="log-dot"></view>
|
||||
<view class="log-line"></view>
|
||||
</view>
|
||||
<view class="log-content">
|
||||
<view class="log-header">
|
||||
<text class="log-time">{{ formatDate(item.created_at) }}</text>
|
||||
<text :class="logTypeBadge(item.operator_type)">{{ logTypeText(item.operator_type) }}</text>
|
||||
</view>
|
||||
<view class="log-body">
|
||||
<text class="log-actor" :class="logActorClass(item.operator_type)">{{ item.operator_type === 1 ? '管理员' : '系统' }}</text>
|
||||
<text class="log-text"> 对 </text>
|
||||
<text class="log-target">{{ item.target_type }} {{ item.target_id || '' }}</text>
|
||||
<text class="log-text"> 执行了 </text>
|
||||
<text class="log-action">{{ item.action }}</text>
|
||||
</view>
|
||||
</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="table-wrap">
|
||||
<u-table>
|
||||
<u-tr>
|
||||
<u-th>操作者</u-th>
|
||||
<u-th>对象类型</u-th>
|
||||
<u-th>对象ID</u-th>
|
||||
<u-th>操作</u-th>
|
||||
<u-th>时间</u-th>
|
||||
</u-tr>
|
||||
<u-tr v-for="item in logs" :key="item.id">
|
||||
<u-td>{{ item.operator_type === 1 ? '管理员' : '系统' }}</u-td>
|
||||
<u-td>{{ item.target_type }}</u-td>
|
||||
<u-td>{{ item.target_id || '-' }}</u-td>
|
||||
<u-td>{{ item.action }}</u-td>
|
||||
<u-td>{{ formatDate(item.created_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>
|
||||
</view>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { get } from '../../utils/request'
|
||||
import AdminLayout from '../../components/AdminLayout.vue'
|
||||
|
||||
export default {
|
||||
components: { AdminLayout },
|
||||
data() {
|
||||
return {
|
||||
logs: [],
|
||||
@@ -75,6 +89,21 @@ export default {
|
||||
this.page = p
|
||||
this.loadLogs()
|
||||
},
|
||||
logTypeText(type) {
|
||||
if (type === 1) return '管理操作'
|
||||
if (type === 2) return '系统事件'
|
||||
return '用户操作'
|
||||
},
|
||||
logTypeBadge(type) {
|
||||
if (type === 1) return 'badge badge-pink'
|
||||
if (type === 2) return 'badge badge-blue'
|
||||
return 'badge badge-green'
|
||||
},
|
||||
logActorClass(type) {
|
||||
if (type === 1) return 'actor-admin'
|
||||
if (type === 2) return 'actor-system'
|
||||
return 'actor-user'
|
||||
},
|
||||
formatDate(d) {
|
||||
return d ? d.slice(0, 19).replace('T', ' ') : '-'
|
||||
}
|
||||
@@ -83,30 +112,210 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
padding: 16px;
|
||||
.page-card {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.filter-row {
|
||||
.page-header {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.filter-input {
|
||||
width: 120px;
|
||||
.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;
|
||||
}
|
||||
|
||||
.log-list {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.log-item {
|
||||
display: flex;
|
||||
min-height: 60px;
|
||||
}
|
||||
|
||||
.log-left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 20px;
|
||||
flex-shrink: 0;
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.log-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
background: #d9d9d9;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.log-line {
|
||||
width: 1px;
|
||||
flex: 1;
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
.log-content {
|
||||
flex: 1;
|
||||
padding-bottom: 16px;
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
.log-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.log-time {
|
||||
font-size: 13px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.badge-pink {
|
||||
background: #fff0f6;
|
||||
color: #E6508C;
|
||||
}
|
||||
|
||||
.badge-blue {
|
||||
background: #e6f7ff;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.badge-green {
|
||||
background: #f6ffed;
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
.log-body {
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.log-actor {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.actor-admin {
|
||||
color: #E6508C;
|
||||
}
|
||||
|
||||
.actor-system {
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.actor-user {
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
.log-text {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.log-target {
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.log-action {
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.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>
|
||||
|
||||
@@ -1,19 +1,45 @@
|
||||
<template>
|
||||
<view class="login-page">
|
||||
<view class="login-card">
|
||||
<view class="login-logo">🌸</view>
|
||||
<view class="login-title">光子美容仪</view>
|
||||
<view class="login-subtitle">管理后台</view>
|
||||
<u-form :model="form" ref="formRef">
|
||||
<u-form-item>
|
||||
<u-input v-model="form.username" placeholder="用户名" border="bottom" />
|
||||
</u-form-item>
|
||||
<u-form-item>
|
||||
<u-input v-model="form.password" placeholder="密码" type="password" border="bottom" />
|
||||
</u-form-item>
|
||||
</u-form>
|
||||
<u-button type="primary" :loading="loading" @click="onLogin" :disabled="loading" class="login-btn">
|
||||
登录
|
||||
</u-button>
|
||||
|
||||
<view class="form-group">
|
||||
<text class="form-label">管理员账号</text>
|
||||
<input
|
||||
class="form-input"
|
||||
v-model="form.username"
|
||||
placeholder="请输入管理员账号"
|
||||
placeholder-class="input-placeholder"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="form-group">
|
||||
<text class="form-label">密码</text>
|
||||
<input
|
||||
class="form-input"
|
||||
v-model="form.password"
|
||||
placeholder="请输入密码"
|
||||
:password="true"
|
||||
placeholder-class="input-placeholder"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="remember-row">
|
||||
<view class="checkbox-wrap" @click="remember = !remember">
|
||||
<view class="checkbox-box" :class="{ checked: remember }">
|
||||
<text class="checkbox-icon" v-if="remember">✓</text>
|
||||
</view>
|
||||
<text class="remember-text">记住登录状态</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<button class="login-btn" :loading="loading" :disabled="loading" @click="onLogin">
|
||||
登 录
|
||||
</button>
|
||||
|
||||
<view class="login-footer">© 2026 光子美容仪管理后台</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
@@ -25,7 +51,8 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
form: { username: '', password: '' },
|
||||
loading: false
|
||||
loading: false,
|
||||
remember: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -55,32 +82,125 @@ export default {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #333333 0%, #666666 100%);
|
||||
background: linear-gradient(135deg, #001529 0%, #003a70 100%);
|
||||
}
|
||||
|
||||
.login-card {
|
||||
width: 400px;
|
||||
background: #ffffff;
|
||||
border-radius: 12px;
|
||||
padding: 48px 32px;
|
||||
padding: 48px 40px 32px;
|
||||
}
|
||||
|
||||
.login-logo {
|
||||
text-align: center;
|
||||
font-size: 48px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.login-title {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
margin-bottom: 8px;
|
||||
color: #E6508C;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.login-subtitle {
|
||||
font-size: 16px;
|
||||
color: #999999;
|
||||
color: #999;
|
||||
text-align: center;
|
||||
margin-bottom: 32px;
|
||||
margin-bottom: 36px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 6px;
|
||||
padding: 0 12px;
|
||||
font-size: 14px;
|
||||
box-sizing: border-box;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
border-color: #E6508C;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.input-placeholder {
|
||||
color: #bfbfbf;
|
||||
}
|
||||
|
||||
.remember-row {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.checkbox-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.checkbox-box {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 3px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.checkbox-box.checked {
|
||||
background: #E6508C;
|
||||
border-color: #E6508C;
|
||||
}
|
||||
|
||||
.checkbox-icon {
|
||||
color: #fff;
|
||||
font-size: 11px;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.remember-text {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
margin-top: 24px;
|
||||
background-color: #333333;
|
||||
width: 100%;
|
||||
height: 44px;
|
||||
background: #E6508C;
|
||||
color: #ffffff;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.login-btn[disabled] {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.login-footer {
|
||||
text-align: center;
|
||||
margin-top: 32px;
|
||||
font-size: 12px;
|
||||
color: #bfbfbf;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,50 +1,86 @@
|
||||
<template>
|
||||
<view class="container">
|
||||
<view class="toolbar flex-between">
|
||||
<text class="total-text">共 {{ total }} 条记录</text>
|
||||
</view>
|
||||
<AdminLayout currentPage="/pages/record/index">
|
||||
<view class="page-card">
|
||||
<view class="page-header">
|
||||
<text class="page-title">📋 护理记录</text>
|
||||
<view class="header-actions">
|
||||
<input class="search-input date-input" v-model="dateFrom" placeholder="开始日期" />
|
||||
<text class="date-sep">~</text>
|
||||
<input class="search-input date-input" v-model="dateTo" placeholder="结束日期" />
|
||||
<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>用户ID</u-th>
|
||||
<u-th>区域</u-th>
|
||||
<u-th>时长</u-th>
|
||||
<u-th>时间</u-th>
|
||||
</u-tr>
|
||||
<u-tr v-for="item in records" :key="item.record_id">
|
||||
<u-td>{{ item.session_id ? item.session_id.slice(0, 8) : '-' }}</u-td>
|
||||
<u-td>{{ item.user_id }}</u-td>
|
||||
<u-td>{{ formatRegions(item.regions) }}</u-td>
|
||||
<u-td>{{ Math.floor((item.total_duration_ms || 0) / 60000) }}分钟</u-td>
|
||||
<u-td>{{ formatDate(item.start_time) }}</u-td>
|
||||
</u-tr>
|
||||
</u-table>
|
||||
</view>
|
||||
<view class="data-table">
|
||||
<view class="t-header">
|
||||
<view class="t-row">
|
||||
<text class="t-th flex1">记录ID</text>
|
||||
<text class="t-th flex1">用户</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 records" :key="item.record_id">
|
||||
<text class="t-td flex1">{{ item.record_id ? item.record_id.slice(0, 8) : '-' }}</text>
|
||||
<text class="t-td flex1">{{ item.nickname || item.user_id || '-' }}</text>
|
||||
<text class="t-td flex2">{{ item.device_id || '-' }}</text>
|
||||
<text class="t-td flex1">
|
||||
<text :class="item.mode === 'smart' ? 'badge badge-pink' : 'badge badge-blue'">
|
||||
{{ item.mode === 'smart' ? '✨智能' : '🔄普通' }}
|
||||
</text>
|
||||
</text>
|
||||
<text class="t-td flex1">{{ formatRegions(item.regions) }}</text>
|
||||
<text class="t-td flex1">{{ Math.floor((item.total_duration_ms || 0) / 60000) }}分</text>
|
||||
<text class="t-td flex2">{{ formatDate(item.start_time) }}</text>
|
||||
<text class="t-td flex1">
|
||||
<text class="action-link">详情</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'
|
||||
|
||||
var REGION_MAP = {
|
||||
1: '左脸', 2: '右脸', 4: '额头', 8: '下巴', 16: '鼻', 32: '左眼', 64: '右眼'
|
||||
}
|
||||
|
||||
export default {
|
||||
components: { AdminLayout },
|
||||
data() {
|
||||
return {
|
||||
records: [],
|
||||
total: 0,
|
||||
page: 1,
|
||||
pageSize: 20
|
||||
pageSize: 20,
|
||||
keyword: '',
|
||||
dateFrom: '',
|
||||
dateTo: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -58,15 +94,23 @@ export default {
|
||||
methods: {
|
||||
async loadRecords() {
|
||||
try {
|
||||
const data = await get('/api/v1/admin/logs', {
|
||||
var params = {
|
||||
type: 'treatment',
|
||||
page: this.page,
|
||||
page_size: this.pageSize
|
||||
})
|
||||
page_size: this.pageSize,
|
||||
keyword: this.keyword,
|
||||
date_from: this.dateFrom,
|
||||
date_to: this.dateTo
|
||||
}
|
||||
const data = await get('/api/v1/admin/logs', params)
|
||||
this.records = data.records || []
|
||||
this.total = data.total || 0
|
||||
} catch (e) {}
|
||||
},
|
||||
onSearch() {
|
||||
this.page = 1
|
||||
this.loadRecords()
|
||||
},
|
||||
onPage(p) {
|
||||
this.page = p
|
||||
this.loadRecords()
|
||||
@@ -88,25 +132,169 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
padding: 16px;
|
||||
.page-card {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.total-text {
|
||||
font-size: 13px;
|
||||
color: #999999;
|
||||
.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: 160px;
|
||||
height: 32px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 6px;
|
||||
padding: 0 12px;
|
||||
font-size: 14px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.date-input {
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
.date-sep {
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.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-pink {
|
||||
background: #fff0f6;
|
||||
color: #E6508C;
|
||||
}
|
||||
|
||||
.badge-blue {
|
||||
background: #e6f7ff;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.action-link {
|
||||
color: #E6508C;
|
||||
font-size: 14px;
|
||||
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>
|
||||
|
||||
@@ -1,94 +1,342 @@
|
||||
<template>
|
||||
<view class="container">
|
||||
<view class="card">
|
||||
<view class="card-title">系统设置</view>
|
||||
<AdminLayout currentPage="/pages/settings/index">
|
||||
<view class="page-card">
|
||||
<view class="card-title">⚙️ 系统设置</view>
|
||||
|
||||
<view class="setting-section">
|
||||
<view class="section-label">管理员管理</view>
|
||||
<view class="info-row">
|
||||
<text>当前仅支持通过数据库管理管理员账号</text>
|
||||
<view class="section-card">
|
||||
<view class="section-title">基础配置</view>
|
||||
<view class="form-grid">
|
||||
<view class="form-group">
|
||||
<text class="form-label">系统名称</text>
|
||||
<input class="form-input" v-model="settings.system_name" placeholder="系统名称" />
|
||||
</view>
|
||||
<view class="form-group">
|
||||
<text class="form-label">管理员邮箱</text>
|
||||
<input class="form-input" v-model="settings.admin_email" placeholder="管理员邮箱" />
|
||||
</view>
|
||||
<view class="form-group">
|
||||
<text class="form-label">系统时区</text>
|
||||
<view class="select-wrap">
|
||||
<picker :range="timezoneOptions" @change="onTimezoneChange">
|
||||
<view class="form-input picker-input">
|
||||
<text>{{ settings.timezone || '请选择' }}</text>
|
||||
<text class="picker-arrow">›</text>
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="divider"></view>
|
||||
<view class="section-card">
|
||||
<view class="section-title">订阅配置</view>
|
||||
<view class="form-grid">
|
||||
<view class="form-group">
|
||||
<text class="form-label">月卡价格</text>
|
||||
<view class="input-suffix">
|
||||
<input class="form-input" v-model="settings.monthly_price" type="number" />
|
||||
<text class="suffix">元/月</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="form-group">
|
||||
<text class="form-label">年卡价格</text>
|
||||
<view class="input-suffix">
|
||||
<input class="form-input" v-model="settings.yearly_price" type="number" />
|
||||
<text class="suffix">元/年</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="form-group">
|
||||
<text class="form-label">试用时长</text>
|
||||
<view class="input-suffix">
|
||||
<input class="form-input" v-model="settings.trial_days" type="number" />
|
||||
<text class="suffix">天</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="setting-section">
|
||||
<view class="section-label">环境变量</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">API地址</text>
|
||||
<text>https://api.lightmask.com</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">MQTT Broker</text>
|
||||
<text>mqtt://iotcloud.tencent.com:8883</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">JWT有效期</text>
|
||||
<text>7天</text>
|
||||
<view class="section-card">
|
||||
<view class="section-title">功能开关</view>
|
||||
<view class="toggle-list">
|
||||
<view class="toggle-item">
|
||||
<view class="toggle-info">
|
||||
<text class="toggle-label">新用户注册</text>
|
||||
<text class="toggle-desc">允许新用户注册账号</text>
|
||||
</view>
|
||||
<view class="toggle-switch" :class="{ on: settings.enable_register }" @click="settings.enable_register = !settings.enable_register">
|
||||
<view class="toggle-dot"></view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="toggle-item">
|
||||
<view class="toggle-info">
|
||||
<text class="toggle-label">设备绑定</text>
|
||||
<text class="toggle-desc">允许用户绑定设备</text>
|
||||
</view>
|
||||
<view class="toggle-switch" :class="{ on: settings.enable_binding }" @click="settings.enable_binding = !settings.enable_binding">
|
||||
<view class="toggle-dot"></view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="toggle-item">
|
||||
<view class="toggle-info">
|
||||
<text class="toggle-label">免费普通模式</text>
|
||||
<text class="toggle-desc">未订阅用户可使用普通模式</text>
|
||||
</view>
|
||||
<view class="toggle-switch" :class="{ on: settings.enable_free_mode }" @click="settings.enable_free_mode = !settings.enable_free_mode">
|
||||
<view class="toggle-dot"></view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="toggle-item">
|
||||
<view class="toggle-info">
|
||||
<text class="toggle-label">维护模式</text>
|
||||
<text class="toggle-desc">开启后系统进入维护状态</text>
|
||||
</view>
|
||||
<view class="toggle-switch" :class="{ on: settings.maintenance_mode }" @click="settings.maintenance_mode = !settings.maintenance_mode">
|
||||
<view class="toggle-dot"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="card">
|
||||
<view class="card-title">关于</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">系统版本</text>
|
||||
<text>1.0.0</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">协议版本</text>
|
||||
<text>v1.0</text>
|
||||
</view>
|
||||
<view class="save-bar">
|
||||
<button class="btn-primary" @click="onSave">保存设置</button>
|
||||
<button class="btn-default" @click="onReset">重置</button>
|
||||
</view>
|
||||
</view>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {}
|
||||
import { get, post } from '../../utils/request'
|
||||
import AdminLayout from '../../components/AdminLayout.vue'
|
||||
|
||||
export default {
|
||||
components: { AdminLayout },
|
||||
data() {
|
||||
return {
|
||||
settings: {
|
||||
system_name: '',
|
||||
admin_email: '',
|
||||
timezone: 'Asia/Shanghai',
|
||||
monthly_price: '99',
|
||||
yearly_price: '899',
|
||||
trial_days: '7',
|
||||
enable_register: true,
|
||||
enable_binding: true,
|
||||
enable_free_mode: true,
|
||||
maintenance_mode: false
|
||||
},
|
||||
timezoneOptions: ['Asia/Shanghai', 'Asia/Tokyo', 'America/New_York', 'Europe/London']
|
||||
}
|
||||
},
|
||||
onShow() {
|
||||
this.loadSettings()
|
||||
},
|
||||
methods: {
|
||||
async loadSettings() {
|
||||
try {
|
||||
const data = await get('/api/v1/admin/settings')
|
||||
if (data) {
|
||||
Object.assign(this.settings, data)
|
||||
}
|
||||
} catch (e) {}
|
||||
},
|
||||
async onSave() {
|
||||
try {
|
||||
await post('/api/v1/admin/settings', this.settings)
|
||||
uni.showToast({ title: '保存成功', icon: 'success' })
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '保存失败', icon: 'none' })
|
||||
}
|
||||
},
|
||||
onReset() {
|
||||
this.loadSettings()
|
||||
},
|
||||
onTimezoneChange(e) {
|
||||
this.settings.timezone = this.timezoneOptions[e.detail.value]
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #ffffff;
|
||||
.page-card {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
padding: 20px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 16px;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.section-card {
|
||||
padding: 16px 0;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.section-card:last-of-type {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.setting-section {
|
||||
margin-bottom: 16px;
|
||||
.form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.section-label {
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
height: 36px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 6px;
|
||||
padding: 0 12px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 12px;
|
||||
box-sizing: border-box;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
.input-suffix {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.input-suffix .form-input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.suffix {
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.select-wrap {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.picker-input {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 8px 0;
|
||||
align-items: center;
|
||||
line-height: 36px;
|
||||
}
|
||||
|
||||
.picker-arrow {
|
||||
color: #999;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.toggle-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.toggle-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 0;
|
||||
}
|
||||
|
||||
.toggle-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.toggle-label {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
color: #999999;
|
||||
.toggle-desc {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.divider {
|
||||
height: 1px;
|
||||
background: #eeeeee;
|
||||
margin: 16px 0;
|
||||
.toggle-switch {
|
||||
width: 44px;
|
||||
height: 24px;
|
||||
border-radius: 12px;
|
||||
background: #d9d9d9;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
|
||||
.toggle-switch.on {
|
||||
background: #E6508C;
|
||||
}
|
||||
|
||||
.toggle-dot {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
transition: left 0.3s;
|
||||
}
|
||||
|
||||
.toggle-switch.on .toggle-dot {
|
||||
left: 22px;
|
||||
}
|
||||
|
||||
.save-bar {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 16px 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #E6508C;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 0 24px;
|
||||
height: 36px;
|
||||
font-size: 14px;
|
||||
line-height: 36px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-default {
|
||||
background: #fff;
|
||||
color: #333;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 6px;
|
||||
padding: 0 24px;
|
||||
height: 36px;
|
||||
font-size: 14px;
|
||||
line-height: 36px;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
在新工单中引用
屏蔽一个用户