feat: add password change, trial subscription, batch import, UX improvements
Server: - POST /api/v1/admin/password: admin password change with bcrypt migration - POST /api/v1/subscription/trial: user trial activation, one per user - POST /api/v1/admin/devices/batch: bulk device import (up to 500) - Add trial plan (7 days, free) to PLANS constant Admin console: - Settings page: add password change form with validation - Device page: add batch import modal with textarea input Miniprogram: - Treating page: add back button with stop-treatment confirmation - Index page: add mock device bind button (dev mode only)
这个提交包含在:
@@ -11,6 +11,7 @@
|
||||
/>
|
||||
<button class="btn-primary btn-sm" @click="onSearch">搜索</button>
|
||||
<button class="btn-primary btn-sm" @click="onCreateDevice">预生成产品码</button>
|
||||
<button class="btn-default btn-sm" @click="showBatchImport = true">批量导入</button>
|
||||
<button class="btn-default btn-sm" @click="onExport">导出</button>
|
||||
</view>
|
||||
</view>
|
||||
@@ -55,6 +56,21 @@
|
||||
<text class="page-info">共 {{ totalPages }} 页</text>
|
||||
</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>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
@@ -73,7 +89,10 @@ export default {
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
keyword: '',
|
||||
statusMap: { 1: '未激活', 2: '在线', 3: '离线', 4: '故障' }
|
||||
statusMap: { 1: '未激活', 2: '在线', 3: '离线', 4: '故障' },
|
||||
showBatchImport: false,
|
||||
batchDeviceIds: '',
|
||||
batchImporting: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -146,6 +165,29 @@ export default {
|
||||
return map[status] || 'badge badge-default'
|
||||
},
|
||||
formatDate: formatDateShort,
|
||||
async onBatchImport() {
|
||||
const ids = this.batchDeviceIds.split('\n').map(s => s.trim()).filter(Boolean)
|
||||
if (ids.length === 0) {
|
||||
uni.showToast({ title: '请输入设备编号', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (ids.length > 500) {
|
||||
uni.showToast({ title: '最多 500 个', icon: 'none' })
|
||||
return
|
||||
}
|
||||
this.batchImporting = true
|
||||
try {
|
||||
const 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()
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '导入失败', icon: 'none' })
|
||||
} finally {
|
||||
this.batchImporting = false
|
||||
}
|
||||
},
|
||||
async onExport() {
|
||||
try {
|
||||
const data = await get('/api/v1/admin/devices', { page: 1, page_size: 9999, keyword: this.keyword })
|
||||
@@ -171,4 +213,61 @@ export default {
|
||||
.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>
|
||||
|
||||
@@ -1,6 +1,28 @@
|
||||
<template>
|
||||
<AdminLayout currentPage="/pages/settings/index">
|
||||
<view class="page-card">
|
||||
<view class="section-card">
|
||||
<view class="section-title">修改密码</view>
|
||||
<view class="form-grid">
|
||||
<view class="form-row">
|
||||
<text class="form-label">当前密码</text>
|
||||
<input class="form-input" v-model="passwordForm.old_password" type="password" placeholder="请输入当前密码" />
|
||||
</view>
|
||||
<view class="form-row">
|
||||
<text class="form-label">新密码</text>
|
||||
<input class="form-input" v-model="passwordForm.new_password" type="password" placeholder="请输入新密码(≥6位)" />
|
||||
</view>
|
||||
<view class="form-row">
|
||||
<text class="form-label">确认新密码</text>
|
||||
<input class="form-input" v-model="passwordForm.confirm_password" type="password" placeholder="请再次输入新密码" />
|
||||
</view>
|
||||
<view class="form-row">
|
||||
<text class="form-label"></text>
|
||||
<button class="btn-primary btn-sm" @click="onChangePassword">修改密码</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section-card">
|
||||
<view class="section-title">基础配置</view>
|
||||
<view class="form-grid">
|
||||
@@ -123,7 +145,8 @@ export default {
|
||||
enable_free_mode: true,
|
||||
maintenance_mode: false
|
||||
},
|
||||
timezoneOptions: ['Asia/Shanghai', 'Asia/Tokyo', 'America/New_York', 'Europe/London']
|
||||
timezoneOptions: ['Asia/Shanghai', 'Asia/Tokyo', 'America/New_York', 'Europe/London'],
|
||||
passwordForm: { old_password: '', new_password: '', confirm_password: '' }
|
||||
}
|
||||
},
|
||||
onShow() {
|
||||
@@ -153,6 +176,30 @@ export default {
|
||||
},
|
||||
onTimezoneChange(e) {
|
||||
this.settings.timezone = this.timezoneOptions[e.detail.value]
|
||||
},
|
||||
async onChangePassword() {
|
||||
if (!this.passwordForm.old_password || !this.passwordForm.new_password) {
|
||||
uni.showToast({ title: '请填写完整', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (this.passwordForm.new_password.length < 6) {
|
||||
uni.showToast({ title: '新密码至少6位', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (this.passwordForm.new_password !== this.passwordForm.confirm_password) {
|
||||
uni.showToast({ title: '两次密码不一致', icon: 'none' })
|
||||
return
|
||||
}
|
||||
try {
|
||||
await post('/api/v1/admin/password', {
|
||||
old_password: this.passwordForm.old_password,
|
||||
new_password: this.passwordForm.new_password
|
||||
})
|
||||
uni.showToast({ title: '密码修改成功', icon: 'success' })
|
||||
this.passwordForm = { old_password: '', new_password: '', confirm_password: '' }
|
||||
} catch (e) {
|
||||
uni.showToast({ title: e.message || '修改失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -338,4 +385,11 @@ export default {
|
||||
line-height: 36px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
height: 32px;
|
||||
padding: 0 16px;
|
||||
font-size: 14px;
|
||||
line-height: 32px;
|
||||
}
|
||||
</style>
|
||||
|
||||
在新工单中引用
屏蔽一个用户