refactor: migrate to Tencent Cloud backend
这个提交包含在:
@@ -0,0 +1,10 @@
|
||||
const ENV = 'test'
|
||||
|
||||
const API_BASES = {
|
||||
local: 'http://localhost:3000',
|
||||
test: 'https://1426323813-ilxkhlxf4p.ap-guangzhou.tencentscf.com',
|
||||
prod: 'https://replace-with-scf-prod-url'
|
||||
}
|
||||
|
||||
export { ENV, API_BASES }
|
||||
export default { ENV, API_BASE: API_BASES[ENV] }
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "光子美容仪后台",
|
||||
"appid": "__UNI__HOXADMIN",
|
||||
"description": "Hox beauty device admin console",
|
||||
"versionName": "1.0.0",
|
||||
"versionCode": "100",
|
||||
"h5": {
|
||||
"title": "光子美容仪后台",
|
||||
"router": {
|
||||
"mode": "hash"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,8 @@
|
||||
@confirm="onSearch"
|
||||
/>
|
||||
<button class="btn-primary btn-sm" @click="onSearch">搜索</button>
|
||||
<button class="btn-default btn-sm">导出</button>
|
||||
<button class="btn-primary btn-sm" @click="onCreateDevice">预生成产品码</button>
|
||||
<button class="btn-default btn-sm" @click="onExport">导出</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -19,6 +20,7 @@
|
||||
<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>
|
||||
<text class="t-th flex1">固件版本</text>
|
||||
@@ -30,9 +32,10 @@
|
||||
<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.product_id || 'HOX_LIGHT_MASK' }}</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 flex1">{{ item.firmware_version || 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>
|
||||
@@ -56,7 +59,8 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { get } from '../../utils/request'
|
||||
import { get, post } from '../../utils/request'
|
||||
import { exportCSV } from '../../utils/export'
|
||||
import AdminLayout from '../../components/AdminLayout.vue'
|
||||
|
||||
export default {
|
||||
@@ -98,12 +102,45 @@ export default {
|
||||
onDetail(deviceId) {
|
||||
uni.navigateTo({ url: '/pages/device-detail/index?device_id=' + deviceId })
|
||||
},
|
||||
onCreateDevice() {
|
||||
const self = this
|
||||
uni.showModal({
|
||||
title: '预生成产品码',
|
||||
editable: true,
|
||||
placeholderText: '请输入设备MAC/设备编号',
|
||||
success: async function (res) {
|
||||
if (!res.confirm || !res.content) return
|
||||
try {
|
||||
await post('/api/v1/admin/devices', { device_id: res.content.trim(), product_id: 'HOX_LIGHT_MASK' })
|
||||
uni.showToast({ title: '创建成功', icon: 'success' })
|
||||
self.loadDevices()
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '创建失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
statusBadge(status) {
|
||||
const map = { 2: 'badge badge-success', 3: 'badge badge-warning', 1: 'badge badge-blue', 4: 'badge badge-error' }
|
||||
return map[status] || 'badge badge-default'
|
||||
},
|
||||
formatDate(d) {
|
||||
return d ? d.slice(0, 10) : '-'
|
||||
},
|
||||
async onExport() {
|
||||
try {
|
||||
const data = await get('/api/v1/admin/devices', { page: 1, page_size: 9999, keyword: this.keyword })
|
||||
const records = data.records || []
|
||||
exportCSV('devices_' + new Date().toISOString().slice(0, 10) + '.csv',
|
||||
['设备编号', '产品编号', '绑定用户', '电量', '固件版本', '绑定时间', '状态'],
|
||||
records.map(function (r) {
|
||||
return [r.device_id, r.product_id || 'HOX_LIGHT_MASK', r.bound_user || '', r.battery != null ? r.battery + '%' : '', r.firmware_version || r.fw_version || '', r.activated_at ? String(r.activated_at).slice(0, 10) : '', r.status]
|
||||
})
|
||||
)
|
||||
uni.showToast({ title: '导出成功', icon: 'success' })
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '导出失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
placeholder-class="input-placeholder"
|
||||
/>
|
||||
<button class="btn-primary btn-sm" @click="onSearch">搜索</button>
|
||||
<button class="btn-default btn-sm">导出</button>
|
||||
<button class="btn-default btn-sm" @click="onExport">导出</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
|
||||
<script>
|
||||
import { get } from '../../utils/request'
|
||||
import { exportCSV } from '../../utils/export'
|
||||
import AdminLayout from '../../components/AdminLayout.vue'
|
||||
|
||||
export default {
|
||||
@@ -97,6 +98,23 @@ export default {
|
||||
},
|
||||
formatDate(d) {
|
||||
return d ? d.slice(0, 19).replace('T', ' ') : '-'
|
||||
},
|
||||
async onExport() {
|
||||
try {
|
||||
var params = { page: 1, page_size: 9999 }
|
||||
if (this.filters.type) params.type = this.filters.type
|
||||
const data = await get('/api/v1/admin/logs', params)
|
||||
const records = data.records || []
|
||||
exportCSV('logs_' + new Date().toISOString().slice(0, 10) + '.csv',
|
||||
['时间', '操作类型', '操作详情', '操作者'],
|
||||
records.map(function (r) {
|
||||
return [r.created_at ? r.created_at.slice(0, 19).replace('T', ' ') : '', r.action || '', r.detail || '', r.openid || '']
|
||||
})
|
||||
)
|
||||
uni.showToast({ title: '导出成功', icon: 'success' })
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '导出失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ import { useUserStore } from '../../store/user'
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
form: { username: '', password: '' },
|
||||
form: { username: 'admin', password: 'admin' },
|
||||
loading: false,
|
||||
remember: false
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
@confirm="onSearch"
|
||||
/>
|
||||
<button class="btn-primary btn-sm" @click="onSearch">搜索</button>
|
||||
<button class="btn-default btn-sm">导出</button>
|
||||
<button class="btn-default btn-sm" @click="onExport">导出</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -63,6 +63,7 @@
|
||||
|
||||
<script>
|
||||
import { get } from '../../utils/request'
|
||||
import { exportCSV } from '../../utils/export'
|
||||
import AdminLayout from '../../components/AdminLayout.vue'
|
||||
|
||||
var REGION_MAP = {
|
||||
@@ -125,6 +126,26 @@ export default {
|
||||
},
|
||||
formatDate(d) {
|
||||
return d ? d.slice(0, 10) : '-'
|
||||
},
|
||||
async onExport() {
|
||||
try {
|
||||
const data = await get('/api/v1/admin/records', { page: 1, page_size: 9999, keyword: this.keyword, date_from: this.dateFrom, date_to: this.dateTo })
|
||||
var self = this
|
||||
var REGION_MAP = { 1: '左脸', 2: '右脸', 4: '额头', 8: '下巴', 16: '鼻', 32: '左眼', 64: '右眼' }
|
||||
const records = data.records || []
|
||||
exportCSV('records_' + new Date().toISOString().slice(0, 10) + '.csv',
|
||||
['记录ID', '用户', '设备编号', '护理模式', '护理区域', '护理时长(分钟)', '护理时间'],
|
||||
records.map(function (r) {
|
||||
var regions = []
|
||||
var bits = [1, 2, 4, 8, 16, 32, 64]
|
||||
for (var i = 0; i < bits.length; i++) { if (r.regions & bits[i]) regions.push(REGION_MAP[bits[i]]) }
|
||||
return [r.record_id || '', r.nickname || r.user_id || '', r.device_id || '', r.mode === 'smart' ? '智能模式' : '普通模式', regions.join('+'), Math.floor((r.total_duration_ms || 0) / 60000), r.start_time ? r.start_time.slice(0, 10) : '']
|
||||
})
|
||||
)
|
||||
uni.showToast({ title: '导出成功', icon: 'success' })
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '导出失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<AdminLayout currentPage="/pages/subscription/index">
|
||||
<view class="toolbar">
|
||||
<view class="header-actions">
|
||||
<button class="btn-default btn-sm">导出报表</button>
|
||||
<button class="btn-default btn-sm" @click="onExport">导出报表</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -102,6 +102,7 @@
|
||||
|
||||
<script>
|
||||
import { get, post } from '../../utils/request'
|
||||
import { exportCSV } from '../../utils/export'
|
||||
import AdminLayout from '../../components/AdminLayout.vue'
|
||||
|
||||
export default {
|
||||
@@ -176,6 +177,23 @@ export default {
|
||||
},
|
||||
formatDate(d) {
|
||||
return d ? d.slice(0, 10) : '-'
|
||||
},
|
||||
async onExport() {
|
||||
try {
|
||||
const data = await get('/api/v1/admin/subscriptions', { page: 1, page_size: 9999, tab: this.activeTab })
|
||||
const records = data.records || []
|
||||
var planMap = { monthly: '月卡', quarterly: '季卡', yearly: '年卡', trial: '试用' }
|
||||
var statusMap = { 1: '生效中', 2: '试用中', 3: '已过期' }
|
||||
exportCSV('subscriptions_' + new Date().toISOString().slice(0, 10) + '.csv',
|
||||
['用户', '订阅类型', '订单金额', '开始日期', '到期日期', '状态'],
|
||||
records.map(function (r) {
|
||||
return [r.nickname || r.user_id || '', planMap[r.plan] || r.plan || '', r.amount || '', r.started_at ? r.started_at.slice(0, 10) : '', r.expired_at ? r.expired_at.slice(0, 10) : '', statusMap[r.status] || '']
|
||||
})
|
||||
)
|
||||
uni.showToast({ title: '导出成功', icon: 'success' })
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '导出失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
@confirm="onSearch"
|
||||
/>
|
||||
<button class="btn-primary btn-sm" @click="onSearch">搜索</button>
|
||||
<button class="btn-default btn-sm">导出用户</button>
|
||||
<button class="btn-default btn-sm" @click="onExport">导出用户</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
|
||||
<script>
|
||||
import { get } from '../../utils/request'
|
||||
import { exportCSV } from '../../utils/export'
|
||||
import AdminLayout from '../../components/AdminLayout.vue'
|
||||
|
||||
export default {
|
||||
@@ -117,6 +118,21 @@ export default {
|
||||
},
|
||||
formatDate(d) {
|
||||
return d ? d.slice(0, 10) : '-'
|
||||
},
|
||||
async onExport() {
|
||||
try {
|
||||
const data = await get('/api/v1/admin/users', { page: 1, page_size: 9999, keyword: this.keyword })
|
||||
const records = data.records || []
|
||||
exportCSV('users_' + new Date().toISOString().slice(0, 10) + '.csv',
|
||||
['用户ID', '昵称', '手机号', '绑定设备数', '护理次数', '订阅状态', '注册时间'],
|
||||
records.map(function (r) {
|
||||
return [r._id, r.nickname || '', r.phone || '', r.device_count || 0, r.treatment_count || 0, r.subscription_status || '', r.created_at ? r.created_at.slice(0, 10) : '']
|
||||
})
|
||||
)
|
||||
uni.showToast({ title: '导出成功', icon: 'success' })
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '导出失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
function escapeCSV(val) {
|
||||
if (val == null) return ''
|
||||
var s = String(val)
|
||||
if (s.indexOf(',') > -1 || s.indexOf('"') > -1 || s.indexOf('\n') > -1) {
|
||||
return '"' + s.replace(/"/g, '""') + '"'
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
function exportCSV(filename, headers, rows) {
|
||||
var lines = [headers.map(escapeCSV).join(',')]
|
||||
for (var i = 0; i < rows.length; i++) {
|
||||
lines.push(rows[i].map(escapeCSV).join(','))
|
||||
}
|
||||
var csv = '\uFEFF' + lines.join('\n')
|
||||
var blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' })
|
||||
var url = URL.createObjectURL(blob)
|
||||
var a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = filename
|
||||
a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
export { exportCSV }
|
||||
@@ -1,5 +1,7 @@
|
||||
const BASE_URL = 'https://api.lightmask.com'
|
||||
const USE_MOCK = true
|
||||
import env from '../config/env'
|
||||
|
||||
const BASE_URL = env.API_BASE
|
||||
const USE_MOCK = false
|
||||
|
||||
const MOCK_DATA = {
|
||||
'/api/v1/admin/devices/AABBCCDDEEFF0011': {
|
||||
@@ -67,6 +69,12 @@ const MOCK_DATA = {
|
||||
}
|
||||
|
||||
function getMockData(url, data) {
|
||||
if (url.includes('/api/v1/admin/login')) {
|
||||
if (data && data.username === 'admin' && data.password === 'admin123') {
|
||||
return { token: 'mock_token_admin', admin_id: 'admin_001', username: 'admin', real_name: '管理员', role: 'admin' }
|
||||
}
|
||||
throw { code: 1001, message: '用户名或密码错误' }
|
||||
}
|
||||
if (url.startsWith('/api/v1/admin/users/') && !url.includes('users?')) {
|
||||
const id = url.split('/').pop()
|
||||
return MOCK_DATA['/api/v1/admin/users/u1'] || { _id: id, nickname: '未知用户' }
|
||||
@@ -92,8 +100,8 @@ function request(options) {
|
||||
method: options.method || 'GET',
|
||||
data: options.data || {},
|
||||
header: {
|
||||
'Authorization': token ? 'Bearer ' + token : '',
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': token ? 'Bearer ' + token : '',
|
||||
...options.header
|
||||
},
|
||||
success(res) {
|
||||
|
||||
在新工单中引用
屏蔽一个用户