refactor: restructure entire project for human maintainability
Server: - Add Express framework, replace custom router/request parser - Create DAO layer (12 files) centralizing all 73 SQL queries - Rewrite 7 route files as thin Express controllers calling DAOs - Add SCF-to-Express adapter (lib/serverless.js) - Add auth middleware (middleware/auth.js) - Remove dead code from lib/auth.js Admin console: - Extract DataTable component (table + pagination) - Extract ConfirmModal component (modal + form styles) - Create listMixin for paginated list pages - Move form styles to common.css for slot compatibility - Refactor device + subscription pages as examples Miniprogram: - Split 734-line BLE monolith into 4 focused modules (protocol, connection, commands, barrel index) - Create API module (utils/api.js) with named functions - Create page utilities (utils/page.js) - Refactor index + profile pages to use API module
这个提交包含在:
@@ -16,61 +16,46 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="page-card">
|
||||
<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>
|
||||
<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>
|
||||
<DataTable
|
||||
:columns="columns"
|
||||
:records="records"
|
||||
:page="page"
|
||||
:totalPages="totalPages"
|
||||
@page="onPage"
|
||||
>
|
||||
<template #rows>
|
||||
<view class="t-row" v-for="item in records" :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.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>
|
||||
</text>
|
||||
<text class="t-td flex1">
|
||||
<text class="action-link" @click="onDetail(item.device_id)">详情</text>
|
||||
<text class="action-link" v-if="item.bound_user" @click="onUnbind(item)">解绑</text>
|
||||
</text>
|
||||
</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.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.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>
|
||||
</text>
|
||||
<text class="t-td flex1">
|
||||
<text class="action-link" @click="onDetail(item.device_id)">详情</text>
|
||||
<text class="action-link" v-if="item.bound_user" @click="onUnbind(item)">解绑</text>
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</DataTable>
|
||||
|
||||
<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>
|
||||
<ConfirmModal
|
||||
:visible="showBatchImport"
|
||||
title="批量导入设备"
|
||||
confirmText="导入"
|
||||
:loading="batchImporting"
|
||||
@close="showBatchImport = false"
|
||||
@confirm="onBatchImport"
|
||||
>
|
||||
<view class="form-group">
|
||||
<text class="form-label">设备编号(每行一个)</text>
|
||||
<textarea class="form-textarea" v-model="batchDeviceIds" placeholder="输入设备编号,每行一个 例如: HOX001 HOX002 HOX003" :maxlength="-1"></textarea>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="modal-mask" v-if="showBatchImport" @click="showBatchImport = false">
|
||||
<view class="modal-content" @click.stop>
|
||||
<view class="modal-title">批量导入设备</view>
|
||||
<view class="form-group">
|
||||
<text class="form-label">设备编号(每行一个)</text>
|
||||
<textarea class="form-textarea" v-model="batchDeviceIds" placeholder="输入设备编号,每行一个 例如: HOX001 HOX002 HOX003" :maxlength="-1"></textarea>
|
||||
</view>
|
||||
<view class="batch-hint">最多 500 个设备</view>
|
||||
<view class="modal-actions">
|
||||
<button class="btn-default" @click="showBatchImport = false">取消</button>
|
||||
<button class="btn-primary" @click="onBatchImport" :loading="batchImporting">导入</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="batch-hint">最多 500 个设备</view>
|
||||
</ConfirmModal>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
@@ -78,54 +63,48 @@
|
||||
import { get, post } from '../../utils/request'
|
||||
import { exportCSV } from '../../utils/export'
|
||||
import { formatDateShort } from '../../utils/format'
|
||||
import { listMixin } from '../../utils/useList'
|
||||
import AdminLayout from '../../components/AdminLayout.vue'
|
||||
import DataTable from '../../components/DataTable.vue'
|
||||
import ConfirmModal from '../../components/ConfirmModal.vue'
|
||||
|
||||
var STATUS_MAP = { 1: '未激活', 2: '在线', 3: '离线', 4: '故障' }
|
||||
var STATUS_BADGE = { 2: 'badge badge-success', 3: 'badge badge-warning', 1: 'badge badge-blue', 4: 'badge badge-error' }
|
||||
|
||||
export default {
|
||||
components: { AdminLayout },
|
||||
components: { AdminLayout, DataTable, ConfirmModal },
|
||||
mixins: [listMixin('/api/v1/admin/devices')],
|
||||
data() {
|
||||
return {
|
||||
devices: [],
|
||||
total: 0,
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
keyword: '',
|
||||
statusMap: { 1: '未激活', 2: '在线', 3: '离线', 4: '故障' },
|
||||
statusMap: STATUS_MAP,
|
||||
columns: [
|
||||
{ key: 'device_id', label: '设备编号', flex: 2 },
|
||||
{ key: 'product_id', label: '产品编号', flex: 2 },
|
||||
{ key: 'bound_user', label: '绑定用户', flex: 2 },
|
||||
{ key: 'battery', label: '电量', flex: 1 },
|
||||
{ key: 'fw', label: '固件版本', flex: 1 },
|
||||
{ key: 'activated_at', label: '绑定时间', flex: 2 },
|
||||
{ key: 'status', label: '状态', flex: 1 },
|
||||
{ key: 'actions', label: '操作', flex: 1 }
|
||||
],
|
||||
showBatchImport: false,
|
||||
batchDeviceIds: '',
|
||||
batchImporting: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
totalPages() {
|
||||
return Math.ceil(this.total / this.pageSize) || 1
|
||||
}
|
||||
},
|
||||
onShow() {
|
||||
this.loadDevices()
|
||||
this.reload()
|
||||
},
|
||||
methods: {
|
||||
async loadDevices() {
|
||||
try {
|
||||
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) {
|
||||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||
}
|
||||
},
|
||||
onSearch() {
|
||||
this.page = 1
|
||||
this.loadDevices()
|
||||
},
|
||||
onPage(p) {
|
||||
this.page = p
|
||||
this.loadDevices()
|
||||
reload() {
|
||||
this.loadList({ keyword: this.keyword })
|
||||
},
|
||||
onDetail(deviceId) {
|
||||
uni.navigateTo({ url: '/pages/device-detail/index?device_id=' + deviceId })
|
||||
},
|
||||
onCreateDevice() {
|
||||
const self = this
|
||||
var self = this
|
||||
uni.showModal({
|
||||
title: '预生成产品码',
|
||||
editable: true,
|
||||
@@ -135,7 +114,7 @@ export default {
|
||||
try {
|
||||
await post('/api/v1/admin/devices', { device_id: res.content.trim(), product_id: 'HOX_LIGHT_MASK' })
|
||||
uni.showToast({ title: '创建成功', icon: 'success' })
|
||||
self.loadDevices()
|
||||
self.reload()
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '创建失败', icon: 'none' })
|
||||
}
|
||||
@@ -143,7 +122,7 @@ export default {
|
||||
})
|
||||
},
|
||||
onUnbind(item) {
|
||||
const self = this
|
||||
var self = this
|
||||
uni.showModal({
|
||||
title: '确认解绑',
|
||||
content: '确定要解绑设备 ' + item.device_id + ' 吗?',
|
||||
@@ -152,7 +131,7 @@ export default {
|
||||
try {
|
||||
await post('/api/v1/admin/devices/' + item.device_id + '/unbind', {})
|
||||
uni.showToast({ title: '解绑成功', icon: 'success' })
|
||||
self.loadDevices()
|
||||
self.reload()
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '解绑失败', icon: 'none' })
|
||||
}
|
||||
@@ -161,12 +140,11 @@ export default {
|
||||
})
|
||||
},
|
||||
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'
|
||||
return STATUS_BADGE[status] || 'badge badge-default'
|
||||
},
|
||||
formatDate: formatDateShort,
|
||||
async onBatchImport() {
|
||||
const ids = this.batchDeviceIds.split('\n').map(s => s.trim()).filter(Boolean)
|
||||
var ids = this.batchDeviceIds.split('\n').map(function (s) { return s.trim() }).filter(Boolean)
|
||||
if (ids.length === 0) {
|
||||
uni.showToast({ title: '请输入设备编号', icon: 'none' })
|
||||
return
|
||||
@@ -177,11 +155,11 @@ export default {
|
||||
}
|
||||
this.batchImporting = true
|
||||
try {
|
||||
const result = await post('/api/v1/admin/devices/batch', { device_ids: ids })
|
||||
var result = await post('/api/v1/admin/devices/batch', { device_ids: ids })
|
||||
uni.showToast({ title: '导入 ' + result.created + ' 个设备', icon: 'success' })
|
||||
this.showBatchImport = false
|
||||
this.batchDeviceIds = ''
|
||||
this.loadDevices()
|
||||
this.reload()
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '导入失败', icon: 'none' })
|
||||
} finally {
|
||||
@@ -190,8 +168,8 @@ export default {
|
||||
},
|
||||
async onExport() {
|
||||
try {
|
||||
const data = await get('/api/v1/admin/devices', { page: 1, page_size: 9999, keyword: this.keyword })
|
||||
const records = data.records || []
|
||||
var data = await get('/api/v1/admin/devices', { page: 1, page_size: 9999, keyword: this.keyword })
|
||||
var records = data.records || []
|
||||
exportCSV('devices_' + new Date().toISOString().slice(0, 10) + '.csv',
|
||||
['设备编号', '产品编号', '绑定用户', '电量', '固件版本', '绑定时间', '状态'],
|
||||
records.map(function (r) {
|
||||
@@ -210,64 +188,9 @@ export default {
|
||||
<style scoped>
|
||||
@import '../../styles/common.css';
|
||||
|
||||
.pagination {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.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: 480px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.form-textarea {
|
||||
width: 100%;
|
||||
height: 160px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 6px;
|
||||
padding: 12px;
|
||||
font-size: 14px;
|
||||
box-sizing: border-box;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.batch-hint {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -28,76 +28,64 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="page-card">
|
||||
<view class="tabs">
|
||||
<view class="tab" :class="{ active: activeTab === 'all' }" @click="onTabFilter('all')">全部订阅</view>
|
||||
<view class="tab" :class="{ active: activeTab === 'monthly' }" @click="onTabFilter('monthly')">月卡</view>
|
||||
<view class="tab" :class="{ active: activeTab === 'yearly' }" @click="onTabFilter('yearly')">年卡</view>
|
||||
<view class="tab" :class="{ active: activeTab === 'trial' }" @click="onTabFilter('trial')">试用中</view>
|
||||
<view class="tab" :class="{ active: activeTab === 'expired' }" @click="onTabFilter('expired')">已过期</view>
|
||||
</view>
|
||||
<DataTable
|
||||
:columns="columns"
|
||||
:records="records"
|
||||
:page="page"
|
||||
:totalPages="totalPages"
|
||||
@page="onPage"
|
||||
>
|
||||
<template #toolbar>
|
||||
<view class="tabs">
|
||||
<view class="tab" :class="{ active: activeTab === 'all' }" @click="onTabFilter('all')">全部订阅</view>
|
||||
<view class="tab" :class="{ active: activeTab === 'monthly' }" @click="onTabFilter('monthly')">月卡</view>
|
||||
<view class="tab" :class="{ active: activeTab === 'yearly' }" @click="onTabFilter('yearly')">年卡</view>
|
||||
<view class="tab" :class="{ active: activeTab === 'trial' }" @click="onTabFilter('trial')">试用中</view>
|
||||
<view class="tab" :class="{ active: activeTab === 'expired' }" @click="onTabFilter('expired')">已过期</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<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>
|
||||
<template #rows>
|
||||
<view class="t-row" v-for="item in records" :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.start_time) }}</text>
|
||||
<text class="t-td flex2">{{ formatDate(item.expire_time) }}</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" @click="onDetail(item)">详情</text>
|
||||
<text class="action-link" v-if="item.status === 1" @click="onExtend(item)">延期</text>
|
||||
<text class="action-link" v-if="item.status === 1" @click="onCancel(item)">取消</text>
|
||||
<text class="action-link" v-if="item.status === 2 || item.status === 3" @click="onRenew(item)">续费</text>
|
||||
</text>
|
||||
</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.start_time) }}</text>
|
||||
<text class="t-td flex2">{{ formatDate(item.expire_time) }}</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" @click="onDetail(item)">详情</text>
|
||||
<text class="action-link" v-if="item.status === 1" @click="onExtend(item)">延期</text>
|
||||
<text class="action-link" v-if="item.status === 1" @click="onCancel(item)">取消</text>
|
||||
<text class="action-link" v-if="item.status === 2 || item.status === 3" @click="onRenew(item)">续费</text>
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</DataTable>
|
||||
|
||||
<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>
|
||||
<ConfirmModal
|
||||
:visible="showCreate"
|
||||
title="创建订阅"
|
||||
confirmText="创建"
|
||||
:loading="creating"
|
||||
@close="showCreate = false"
|
||||
@confirm="onCreate"
|
||||
>
|
||||
<view class="form-group">
|
||||
<text class="form-label">用户ID</text>
|
||||
<input class="form-input" v-model="createForm.user_id" placeholder="输入用户ID" />
|
||||
</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 class="form-group">
|
||||
<text class="form-label">方案</text>
|
||||
<input class="form-input" v-model="createForm.plan" placeholder="monthly/quarterly/yearly" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="form-group">
|
||||
<text class="form-label">天数</text>
|
||||
<input class="form-input" v-model="createForm.days" placeholder="订阅天数" type="number" />
|
||||
</view>
|
||||
</ConfirmModal>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
@@ -105,50 +93,50 @@
|
||||
import { get, post } from '../../utils/request'
|
||||
import { exportCSV } from '../../utils/export'
|
||||
import { formatDateShort } from '../../utils/format'
|
||||
import { listMixin } from '../../utils/useList'
|
||||
import AdminLayout from '../../components/AdminLayout.vue'
|
||||
import DataTable from '../../components/DataTable.vue'
|
||||
import ConfirmModal from '../../components/ConfirmModal.vue'
|
||||
|
||||
var PLAN_MAP = { monthly: '月卡', quarterly: '季卡', yearly: '年卡', trial: '试用' }
|
||||
var STATUS_TEXT = { 1: '生效中', 2: '已过期', 3: '已取消' }
|
||||
var STATUS_BADGE = { 1: 'badge badge-success', 2: 'badge badge-error', 3: 'badge badge-default' }
|
||||
|
||||
export default {
|
||||
components: { AdminLayout },
|
||||
components: { AdminLayout, DataTable, ConfirmModal },
|
||||
mixins: [listMixin('/api/v1/admin/subscriptions')],
|
||||
data() {
|
||||
return {
|
||||
subscriptions: [],
|
||||
total: 0,
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
stats: {},
|
||||
activeTab: 'all',
|
||||
columns: [
|
||||
{ key: 'user', label: '用户', flex: 2 },
|
||||
{ key: 'plan', label: '订阅类型', flex: 1 },
|
||||
{ key: 'amount', label: '订单金额', flex: 1 },
|
||||
{ key: 'start', label: '开始日期', flex: 2 },
|
||||
{ key: 'expire', label: '到期日期', flex: 2 },
|
||||
{ key: 'status', label: '状态', flex: 1 },
|
||||
{ key: 'actions', label: '操作', flex: 1 }
|
||||
],
|
||||
showCreate: false,
|
||||
creating: false,
|
||||
createForm: { user_id: '', plan: 'monthly', days: 30 },
|
||||
activeTab: 'all'
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
totalPages() {
|
||||
return Math.ceil(this.total / this.pageSize) || 1
|
||||
createForm: { user_id: '', plan: 'monthly', days: 30 }
|
||||
}
|
||||
},
|
||||
onShow() {
|
||||
this.loadSubscriptions()
|
||||
this.reload()
|
||||
},
|
||||
methods: {
|
||||
async loadSubscriptions() {
|
||||
try {
|
||||
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) {
|
||||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||
}
|
||||
reload() {
|
||||
this.loadList({ tab: this.activeTab })
|
||||
},
|
||||
onListLoaded(data) {
|
||||
if (data.stats) this.stats = data.stats
|
||||
},
|
||||
onTabFilter(tab) {
|
||||
this.activeTab = tab
|
||||
this.page = 1
|
||||
this.loadSubscriptions()
|
||||
},
|
||||
onPage(p) {
|
||||
this.page = p
|
||||
this.loadSubscriptions()
|
||||
this.reload()
|
||||
},
|
||||
async onCreate() {
|
||||
this.creating = true
|
||||
@@ -159,7 +147,7 @@ export default {
|
||||
days: parseInt(this.createForm.days)
|
||||
})
|
||||
this.showCreate = false
|
||||
this.loadSubscriptions()
|
||||
this.reload()
|
||||
uni.showToast({ title: '创建成功', icon: 'success' })
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '创建失败', icon: 'none' })
|
||||
@@ -186,7 +174,7 @@ export default {
|
||||
days: 30
|
||||
})
|
||||
uni.showToast({ title: '延期成功', icon: 'success' })
|
||||
self.loadSubscriptions()
|
||||
self.reload()
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '操作失败', icon: 'none' })
|
||||
}
|
||||
@@ -206,7 +194,7 @@ export default {
|
||||
subscription_id: item.subscription_id
|
||||
})
|
||||
uni.showToast({ title: '已取消', icon: 'success' })
|
||||
self.loadSubscriptions()
|
||||
self.reload()
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '操作失败', icon: 'none' })
|
||||
}
|
||||
@@ -220,29 +208,18 @@ export default {
|
||||
this.createForm.days = 30
|
||||
this.showCreate = true
|
||||
},
|
||||
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-error', 3: 'badge badge-default' }
|
||||
return map[status] || 'badge badge-default'
|
||||
},
|
||||
planText(plan) { return PLAN_MAP[plan] || plan || '-' },
|
||||
statusText(status) { return STATUS_TEXT[status] || '-' },
|
||||
statusBadge(status) { return STATUS_BADGE[status] || 'badge badge-default' },
|
||||
formatDate: formatDateShort,
|
||||
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: '已取消' }
|
||||
var data = await get('/api/v1/admin/subscriptions', { page: 1, page_size: 9999, tab: this.activeTab })
|
||||
var records = data.records || []
|
||||
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.start_time ? String(r.start_time).slice(0, 10) : '', r.expire_time ? String(r.expire_time).slice(0, 10) : '', statusMap[r.status] || '']
|
||||
return [r.nickname || r.user_id || '', PLAN_MAP[r.plan] || r.plan || '', r.amount || '', r.start_time ? String(r.start_time).slice(0, 10) : '', r.expire_time ? String(r.expire_time).slice(0, 10) : '', STATUS_TEXT[r.status] || '']
|
||||
})
|
||||
)
|
||||
uni.showToast({ title: '导出成功', icon: 'success' })
|
||||
@@ -308,58 +285,4 @@ export default {
|
||||
.action-link {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.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>
|
||||
|
||||
在新工单中引用
屏蔽一个用户