feat: init project with miniprogram, cloud functions and admin console
这个提交包含在:
@@ -0,0 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>光子美容仪 - 管理后台</title>
|
||||
<!--preload-links-->
|
||||
<!--app-context-->
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"><!--app-html--></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "hox-admin-console",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "uni",
|
||||
"build:h5": "uni build -p h5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dcloudio/uni-app": "3.0.0-4020920240930001",
|
||||
"@dcloudio/uni-app-plus": "3.0.0-4020920240930001",
|
||||
"@dcloudio/uni-components": "3.0.0-4020920240930001",
|
||||
"@dcloudio/uni-h5": "3.0.0-4020920240930001",
|
||||
"vue": "^3.4.0",
|
||||
"uview-plus": "^3.2.0",
|
||||
"pinia": "^2.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@dcloudio/uni-automator": "3.0.0-4020920240930001",
|
||||
"@dcloudio/uni-cli-shared": "3.0.0-4020920240930001",
|
||||
"@dcloudio/vite-plugin-uni": "3.0.0-4020920240930001",
|
||||
"vite": "^5.0.0",
|
||||
"sass": "^1.77.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<script>
|
||||
import { useUserStore } from './store/user'
|
||||
|
||||
export default {
|
||||
onLaunch() {
|
||||
const userStore = useUserStore()
|
||||
if (!userStore.isLoggedIn()) {
|
||||
uni.reLaunch({ url: '/pages/login/index' })
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
page {
|
||||
background-color: #f5f5f5;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
color: #333333;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,10 @@
|
||||
import { createSSRApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import App from './App.vue'
|
||||
|
||||
export function createApp() {
|
||||
const app = createSSRApp(App)
|
||||
const pinia = createPinia()
|
||||
app.use(pinia)
|
||||
return { app }
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"pages": [
|
||||
{
|
||||
"path": "pages/login/index",
|
||||
"style": { "navigationBarTitleText": "管理员登录", "navigationStyle": "custom" }
|
||||
},
|
||||
{
|
||||
"path": "pages/dashboard/index",
|
||||
"style": { "navigationBarTitleText": "仪表盘" }
|
||||
},
|
||||
{
|
||||
"path": "pages/device/index",
|
||||
"style": { "navigationBarTitleText": "设备管理" }
|
||||
},
|
||||
{
|
||||
"path": "pages/device-detail/index",
|
||||
"style": { "navigationBarTitleText": "设备详情" }
|
||||
},
|
||||
{
|
||||
"path": "pages/user/index",
|
||||
"style": { "navigationBarTitleText": "用户管理" }
|
||||
},
|
||||
{
|
||||
"path": "pages/user-detail/index",
|
||||
"style": { "navigationBarTitleText": "用户详情" }
|
||||
},
|
||||
{
|
||||
"path": "pages/subscription/index",
|
||||
"style": { "navigationBarTitleText": "订阅管理" }
|
||||
},
|
||||
{
|
||||
"path": "pages/record/index",
|
||||
"style": { "navigationBarTitleText": "护理记录" }
|
||||
},
|
||||
{
|
||||
"path": "pages/log/index",
|
||||
"style": { "navigationBarTitleText": "操作日志" }
|
||||
},
|
||||
{
|
||||
"path": "pages/settings/index",
|
||||
"style": { "navigationBarTitleText": "系统设置" }
|
||||
}
|
||||
],
|
||||
"globalStyle": {
|
||||
"navigationBarTextStyle": "black",
|
||||
"navigationBarTitleText": "光子美容仪后台",
|
||||
"navigationBarBackgroundColor": "#ffffff",
|
||||
"backgroundColor": "#f5f5f5"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
<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>
|
||||
|
||||
<view class="stat-grid">
|
||||
<view class="stat-card">
|
||||
<text class="stat-value">{{ stats.user_count || 0 }}</text>
|
||||
<text class="stat-label">用户数</text>
|
||||
</view>
|
||||
<view class="stat-card">
|
||||
<text class="stat-value">{{ stats.device_count || 0 }}</text>
|
||||
<text class="stat-label">设备数</text>
|
||||
</view>
|
||||
<view class="stat-card">
|
||||
<text class="stat-value">{{ stats.active_device_count || 0 }}</text>
|
||||
<text class="stat-label">激活设备</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>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { get } from '../../utils/request'
|
||||
import { useUserStore } from '../../store/user'
|
||||
|
||||
export default {
|
||||
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: '设置' }
|
||||
]
|
||||
}
|
||||
},
|
||||
onShow() {
|
||||
this.loadDashboard()
|
||||
},
|
||||
methods: {
|
||||
async loadDashboard() {
|
||||
try {
|
||||
this.stats = await get('/api/v1/admin/dashboard')
|
||||
} 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' })
|
||||
}
|
||||
}
|
||||
}
|
||||
</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);
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
padding: 24px 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
display: block;
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
color: #333333;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 13px;
|
||||
color: #999999;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,143 @@
|
||||
<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>
|
||||
|
||||
<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>
|
||||
<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">
|
||||
<text class="info-label">状态</text>
|
||||
<text :class="b.status === 1 ? 'text-success' : 'text-muted'">
|
||||
{{ b.status === 1 ? '有效' : '已解绑' }}
|
||||
</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>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { get, post } from '../../utils/request'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
device: null,
|
||||
deviceId: '',
|
||||
statusMap: { 1: '未激活', 2: '已激活', 3: '禁用', 4: '故障' }
|
||||
}
|
||||
},
|
||||
onLoad(options) {
|
||||
this.deviceId = options.device_id || ''
|
||||
this.loadDevice()
|
||||
},
|
||||
methods: {
|
||||
async loadDevice() {
|
||||
try {
|
||||
this.device = await get('/api/v1/admin/devices/' + this.deviceId)
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||
}
|
||||
},
|
||||
async onCommand(type) {
|
||||
try {
|
||||
await post('/api/v1/admin/devices/' + this.deviceId + '/command', { opcode: type })
|
||||
uni.showToast({ title: '指令已发送', icon: 'success' })
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '发送失败', icon: 'none' })
|
||||
}
|
||||
},
|
||||
formatDate(d) {
|
||||
return d ? d.slice(0, 19).replace('T', ' ') : '-'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 8px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
.divider {
|
||||
height: 1px;
|
||||
background: #eeeeee;
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.text-success {
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
color: #999999;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,116 @@
|
||||
<template>
|
||||
<view class="container">
|
||||
<view class="toolbar">
|
||||
<u-search v-model="keyword" placeholder="搜索设备ID或名称" @search="onSearch" @clear="onSearch" />
|
||||
</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] || '未知' }}
|
||||
</text>
|
||||
</u-td>
|
||||
<u-td>{{ item.bound_user || '-' }}</u-td>
|
||||
<u-td>{{ formatDate(item.activated_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>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { get } from '../../utils/request'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
devices: [],
|
||||
total: 0,
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
keyword: '',
|
||||
statusMap: { 1: '未激活', 2: '已激活', 3: '禁用', 4: '故障' }
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
totalPages() {
|
||||
return Math.ceil(this.total / this.pageSize) || 1
|
||||
}
|
||||
},
|
||||
onShow() {
|
||||
this.loadDevices()
|
||||
},
|
||||
methods: {
|
||||
async loadDevices() {
|
||||
try {
|
||||
const data = await get('/api/v1/admin/devices', { page: this.page, page_size: this.pageSize })
|
||||
this.devices = data.records || []
|
||||
this.total = data.total || 0
|
||||
} catch (e) {}
|
||||
},
|
||||
onSearch() {
|
||||
this.page = 1
|
||||
this.loadDevices()
|
||||
},
|
||||
onPage(p) {
|
||||
this.page = p
|
||||
this.loadDevices()
|
||||
},
|
||||
onDetail(deviceId) {
|
||||
uni.navigateTo({ url: '/pages/device-detail/index?device_id=' + deviceId })
|
||||
},
|
||||
formatDate(d) {
|
||||
return d ? d.slice(0, 10) : '-'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
margin-top: 16px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.page-info {
|
||||
font-size: 13px;
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
.text-success {
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
color: #999999;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,112 @@
|
||||
<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>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { get } from '../../utils/request'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
logs: [],
|
||||
total: 0,
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
filters: { type: '', device_id: '' }
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
totalPages() {
|
||||
return Math.ceil(this.total / this.pageSize) || 1
|
||||
}
|
||||
},
|
||||
onShow() {
|
||||
this.loadLogs()
|
||||
},
|
||||
methods: {
|
||||
async loadLogs() {
|
||||
try {
|
||||
var params = { page: this.page, page_size: this.pageSize }
|
||||
if (this.filters.type) params.type = this.filters.type
|
||||
if (this.filters.device_id) params.device_id = this.filters.device_id
|
||||
const data = await get('/api/v1/admin/logs', params)
|
||||
this.logs = data.records || []
|
||||
this.total = data.total || 0
|
||||
} catch (e) {}
|
||||
},
|
||||
onSearch() {
|
||||
this.page = 1
|
||||
this.loadLogs()
|
||||
},
|
||||
onPage(p) {
|
||||
this.page = p
|
||||
this.loadLogs()
|
||||
},
|
||||
formatDate(d) {
|
||||
return d ? d.slice(0, 19).replace('T', ' ') : '-'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.filter-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.filter-input {
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.page-info {
|
||||
font-size: 13px;
|
||||
color: #999999;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,86 @@
|
||||
<template>
|
||||
<view class="login-page">
|
||||
<view class="login-card">
|
||||
<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>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useUserStore } from '../../store/user'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
form: { username: '', password: '' },
|
||||
loading: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async onLogin() {
|
||||
if (!this.form.username || !this.form.password) {
|
||||
uni.showToast({ title: '请输入用户名和密码', icon: 'none' })
|
||||
return
|
||||
}
|
||||
this.loading = true
|
||||
try {
|
||||
const userStore = useUserStore()
|
||||
await userStore.login(this.form)
|
||||
uni.reLaunch({ url: '/pages/dashboard/index' })
|
||||
} catch (e) {
|
||||
uni.showToast({ title: e.message || '登录失败', icon: 'none' })
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.login-page {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #333333 0%, #666666 100%);
|
||||
}
|
||||
|
||||
.login-card {
|
||||
width: 400px;
|
||||
background: #ffffff;
|
||||
border-radius: 12px;
|
||||
padding: 48px 32px;
|
||||
}
|
||||
|
||||
.login-title {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.login-subtitle {
|
||||
font-size: 16px;
|
||||
color: #999999;
|
||||
text-align: center;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
margin-top: 24px;
|
||||
background-color: #333333;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,112 @@
|
||||
<template>
|
||||
<view class="container">
|
||||
<view class="toolbar flex-between">
|
||||
<text class="total-text">共 {{ total }} 条记录</text>
|
||||
</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="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>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { get } from '../../utils/request'
|
||||
|
||||
var REGION_MAP = {
|
||||
1: '左脸', 2: '右脸', 4: '额头', 8: '下巴', 16: '鼻', 32: '左眼', 64: '右眼'
|
||||
}
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
records: [],
|
||||
total: 0,
|
||||
page: 1,
|
||||
pageSize: 20
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
totalPages() {
|
||||
return Math.ceil(this.total / this.pageSize) || 1
|
||||
}
|
||||
},
|
||||
onShow() {
|
||||
this.loadRecords()
|
||||
},
|
||||
methods: {
|
||||
async loadRecords() {
|
||||
try {
|
||||
const data = await get('/api/v1/admin/logs', {
|
||||
type: 'treatment',
|
||||
page: this.page,
|
||||
page_size: this.pageSize
|
||||
})
|
||||
this.records = data.records || []
|
||||
this.total = data.total || 0
|
||||
} catch (e) {}
|
||||
},
|
||||
onPage(p) {
|
||||
this.page = p
|
||||
this.loadRecords()
|
||||
},
|
||||
formatRegions(mask) {
|
||||
if (!mask) return '-'
|
||||
var names = []
|
||||
var bits = [1, 2, 4, 8, 16, 32, 64]
|
||||
for (var i = 0; i < bits.length; i++) {
|
||||
if (mask & bits[i]) names.push(REGION_MAP[bits[i]])
|
||||
}
|
||||
return names.join(',') || '-'
|
||||
},
|
||||
formatDate(d) {
|
||||
return d ? d.slice(0, 10) : '-'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.total-text {
|
||||
font-size: 13px;
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.page-info {
|
||||
font-size: 13px;
|
||||
color: #999999;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<view class="container">
|
||||
<view class="card">
|
||||
<view class="card-title">系统设置</view>
|
||||
|
||||
<view class="setting-section">
|
||||
<view class="section-label">管理员管理</view>
|
||||
<view class="info-row">
|
||||
<text>当前仅支持通过数据库管理管理员账号</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="divider"></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>
|
||||
</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>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.setting-section {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.section-label {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 8px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
.divider {
|
||||
height: 1px;
|
||||
background: #eeeeee;
|
||||
margin: 16px 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,165 @@
|
||||
<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>
|
||||
</view>
|
||||
</u-popup>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { get, post } from '../../utils/request'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
subscriptions: [],
|
||||
total: 0,
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
showCreate: false,
|
||||
creating: false,
|
||||
createForm: { user_id: '', plan: 'monthly', days: 30 },
|
||||
subTypeMap: { 1: '试用', 2: '正式', 3: '赠送' },
|
||||
subStatusMap: { 1: '有效', 2: '已过期', 3: '已停用' }
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
totalPages() {
|
||||
return Math.ceil(this.total / this.pageSize) || 1
|
||||
}
|
||||
},
|
||||
onShow() {
|
||||
this.loadSubscriptions()
|
||||
},
|
||||
methods: {
|
||||
async loadSubscriptions() {
|
||||
try {
|
||||
const data = await get('/api/v1/admin/subscriptions', { page: this.page, page_size: this.pageSize })
|
||||
this.subscriptions = data.records || []
|
||||
this.total = data.total || 0
|
||||
} catch (e) {}
|
||||
},
|
||||
onPage(p) {
|
||||
this.page = p
|
||||
this.loadSubscriptions()
|
||||
},
|
||||
async onCreate() {
|
||||
this.creating = true
|
||||
try {
|
||||
await post('/api/v1/admin/subscriptions', {
|
||||
user_id: parseInt(this.createForm.user_id),
|
||||
plan: this.createForm.plan,
|
||||
days: parseInt(this.createForm.days)
|
||||
})
|
||||
this.showCreate = false
|
||||
this.loadSubscriptions()
|
||||
uni.showToast({ title: '创建成功', icon: 'success' })
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '创建失败', icon: 'none' })
|
||||
} finally {
|
||||
this.creating = false
|
||||
}
|
||||
},
|
||||
formatDate(d) {
|
||||
return d ? d.slice(0, 10) : '-'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.total-text {
|
||||
font-size: 13px;
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.page-info {
|
||||
font-size: 13px;
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
.create-form {
|
||||
padding: 24px;
|
||||
width: 400px;
|
||||
}
|
||||
|
||||
.form-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.text-success {
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
color: #999999;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,152 @@
|
||||
<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>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">昵称</text>
|
||||
<text>{{ user.nickname || '-' }}</text>
|
||||
</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>
|
||||
</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>
|
||||
<view class="info-row">
|
||||
<text class="info-label">绑定时间</text>
|
||||
<text>{{ formatDate(d.bound_at) }}</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>
|
||||
<view class="info-row">
|
||||
<text class="info-label">状态</text>
|
||||
<text :class="s.status === 1 ? 'text-success' : 'text-muted'">
|
||||
{{ subStatusMap[s.status] || '未知' }}
|
||||
</text>
|
||||
</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>
|
||||
<view v-if="user.recent_treatments.length === 0" class="text-muted">暂无记录</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { get } from '../../utils/request'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
user: null,
|
||||
userId: '',
|
||||
subTypeMap: { 1: '试用', 2: '正式', 3: '赠送' },
|
||||
subStatusMap: { 1: '有效', 2: '已过期', 3: '已停用' }
|
||||
}
|
||||
},
|
||||
onLoad(options) {
|
||||
this.userId = options.user_id || ''
|
||||
this.loadUser()
|
||||
},
|
||||
methods: {
|
||||
async loadUser() {
|
||||
try {
|
||||
this.user = await get('/api/v1/admin/users/' + this.userId)
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||
}
|
||||
},
|
||||
formatDate(d) {
|
||||
return d ? d.slice(0, 10) : '-'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 8px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
.divider {
|
||||
height: 1px;
|
||||
background: #eeeeee;
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.text-success {
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
color: #999999;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,102 @@
|
||||
<template>
|
||||
<view class="container">
|
||||
<view class="toolbar">
|
||||
<u-search v-model="keyword" placeholder="搜索用户昵称" @search="onSearch" @clear="onSearch" />
|
||||
</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="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>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { get } from '../../utils/request'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
users: [],
|
||||
total: 0,
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
keyword: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
totalPages() {
|
||||
return Math.ceil(this.total / this.pageSize) || 1
|
||||
}
|
||||
},
|
||||
onShow() {
|
||||
this.loadUsers()
|
||||
},
|
||||
methods: {
|
||||
async loadUsers() {
|
||||
try {
|
||||
const data = await get('/api/v1/admin/users', { page: this.page, page_size: this.pageSize })
|
||||
this.users = data.records || []
|
||||
this.total = data.total || 0
|
||||
} catch (e) {}
|
||||
},
|
||||
onSearch() {
|
||||
this.page = 1
|
||||
this.loadUsers()
|
||||
},
|
||||
onPage(p) {
|
||||
this.page = p
|
||||
this.loadUsers()
|
||||
},
|
||||
onDetail(userId) {
|
||||
uni.navigateTo({ url: '/pages/user-detail/index?user_id=' + userId })
|
||||
},
|
||||
formatDate(d) {
|
||||
return d ? d.slice(0, 10) : '-'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.page-info {
|
||||
font-size: 13px;
|
||||
color: #999999;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,37 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { post } from '../utils/request'
|
||||
|
||||
export const useUserStore = defineStore('user', () => {
|
||||
const token = ref(uni.getStorageSync('admin_token') || '')
|
||||
const adminInfo = ref(null)
|
||||
|
||||
function setToken(val) {
|
||||
token.value = val
|
||||
uni.setStorageSync('admin_token', val)
|
||||
}
|
||||
|
||||
function clearToken() {
|
||||
token.value = ''
|
||||
adminInfo.value = null
|
||||
uni.removeStorageSync('admin_token')
|
||||
}
|
||||
|
||||
async function login(credentials) {
|
||||
const data = await post('/api/v1/admin/login', credentials)
|
||||
setToken(data.token)
|
||||
adminInfo.value = {
|
||||
admin_id: data.admin_id,
|
||||
username: data.username,
|
||||
real_name: data.real_name,
|
||||
role: data.role
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
function isLoggedIn() {
|
||||
return !!token.value
|
||||
}
|
||||
|
||||
return { token, adminInfo, setToken, clearToken, login, isLoggedIn }
|
||||
})
|
||||
@@ -0,0 +1,46 @@
|
||||
const BASE_URL = 'https://api.lightmask.com'
|
||||
|
||||
function request(options) {
|
||||
const token = uni.getStorageSync('admin_token')
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.request({
|
||||
url: BASE_URL + options.url,
|
||||
method: options.method || 'GET',
|
||||
data: options.data || {},
|
||||
header: {
|
||||
'Authorization': token ? 'Bearer ' + token : '',
|
||||
'Content-Type': 'application/json',
|
||||
...options.header
|
||||
},
|
||||
success(res) {
|
||||
if (res.data && res.data.code === 0) {
|
||||
resolve(res.data.data)
|
||||
} else if (res.data && (res.data.code === 1001 || res.data.code === 1002)) {
|
||||
uni.removeStorageSync('admin_token')
|
||||
uni.reLaunch({ url: '/pages/login/index' })
|
||||
reject(res.data)
|
||||
} else {
|
||||
reject(res.data || { code: -1, message: '请求失败' })
|
||||
}
|
||||
},
|
||||
fail(err) {
|
||||
reject({ code: -1, message: err.errMsg || '网络异常' })
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function get(url, data) {
|
||||
return request({ url, method: 'GET', data })
|
||||
}
|
||||
|
||||
function post(url, data) {
|
||||
return request({ url, method: 'POST', data })
|
||||
}
|
||||
|
||||
function put(url, data) {
|
||||
return request({ url, method: 'PUT', data })
|
||||
}
|
||||
|
||||
export { request, get, post, put, BASE_URL }
|
||||
@@ -0,0 +1,6 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import uni from '@dcloudio/vite-plugin-uni'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [uni()]
|
||||
})
|
||||
在新工单中引用
屏蔽一个用户