feat: complete feature gaps across all modules
Server: - Add settings-cache with 60s TTL for feature toggle checks - Enforce maintenance_mode on login, enable_binding on device bind Admin console: - Remove dead "发送通知" button from user detail - Firmware check calls real API and compares versions Miniprogram: - Wear-check: dynamic battery/connected from BLE state - Login: hide non-functional phone auth button - Treating: show actual selected regions instead of hardcoded "全脸" - Index: display subscription status with tap to manage - Auto-scan: show "待检测" instead of "--" for PD data - Agreements: tap shows "内容建设中" modal
这个提交包含在:
@@ -162,8 +162,25 @@ export default {
|
||||
onViewLogs() {
|
||||
this.$emit('navigate', 'log', { device_id: this.deviceId })
|
||||
},
|
||||
onCheckUpdate() {
|
||||
async onCheckUpdate() {
|
||||
uni.showToast({ title: '正在检查更新...', icon: 'none' })
|
||||
try {
|
||||
const data = await get('/api/v1/admin/firmware')
|
||||
const records = data.records || []
|
||||
const latest = records.filter(r => r.status === 1).sort((a, b) => b.firmware_id - a.firmware_id)[0]
|
||||
if (!latest) {
|
||||
uni.showToast({ title: '暂无固件信息', icon: 'none' })
|
||||
return
|
||||
}
|
||||
const current = this.device && this.device.firmware_version
|
||||
if (current && current === latest.version) {
|
||||
uni.showToast({ title: '当前已是最新版本', icon: 'success' })
|
||||
} else {
|
||||
uni.showToast({ title: '发现新版本 v' + latest.version, icon: 'none', duration: 3000 })
|
||||
}
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '检查更新失败', icon: 'none' })
|
||||
}
|
||||
},
|
||||
formatDate: formatDateUtil
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
<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="user-profile">
|
||||
|
||||
@@ -65,7 +65,7 @@ Page({
|
||||
|
||||
parseScanResults: function (regions) {
|
||||
return regions.map(function (name) {
|
||||
return { region: name, pd: '--' }
|
||||
return { region: name, pd: '待检测' }
|
||||
})
|
||||
},
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
</view>
|
||||
|
||||
<view class="scan-hint">请将面罩对准面部</view>
|
||||
<view class="scan-hint" style="font-size:24rpx;color:#999;margin-top:8rpx;">连接设备后可获取皮肤数据</view>
|
||||
|
||||
<view class="debug-actions" wx:if="{{devMode}}">
|
||||
<view class="debug-label">🔧 调试工具</view>
|
||||
|
||||
@@ -23,6 +23,15 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="sub-info" bindtap="onViewSubscription">
|
||||
<view class="sub-info-left">
|
||||
<text class="sub-info-icon">💳</text>
|
||||
<text wx:if="{{subscription && subscription.status === 'active'}}">智能模式 · 剩余{{subRemaining}}天</text>
|
||||
<text wx:else>未订阅智能模式</text>
|
||||
</view>
|
||||
<text class="sub-info-arrow">›</text>
|
||||
</view>
|
||||
|
||||
<button class="btn-primary" bindtap="onStartTreatment">开始护理</button>
|
||||
<button class="btn-secondary" bindtap="onManageDevice">设备管理</button>
|
||||
</view>
|
||||
|
||||
@@ -18,3 +18,27 @@
|
||||
.btn-secondary::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.sub-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 24rpx;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
.sub-info-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
}
|
||||
.sub-info-icon {
|
||||
font-size: 32rpx;
|
||||
}
|
||||
.sub-info-arrow {
|
||||
color: #999;
|
||||
font-size: 32rpx;
|
||||
}
|
||||
|
||||
@@ -65,6 +65,10 @@ Page({
|
||||
})
|
||||
},
|
||||
|
||||
onAgreement: function () {
|
||||
wx.showModal({ title: '提示', content: '用户协议和隐私政策内容建设中', showCancel: false })
|
||||
},
|
||||
|
||||
onGetPhoneNumber: function (e) {
|
||||
var self = this
|
||||
if (!e.detail || !e.detail.code) {
|
||||
|
||||
@@ -18,10 +18,10 @@
|
||||
</button>
|
||||
|
||||
<!-- 手机号授权已预留。需要时展示该按钮并隐藏上面的普通登录按钮。 -->
|
||||
<button class="phone-btn disabled-phone-btn" open-type="getPhoneNumber" bindgetphonenumber="onGetPhoneNumber" disabled="{{loading}}">
|
||||
<button wx:if="{{false}}" class="phone-btn disabled-phone-btn" open-type="getPhoneNumber" bindgetphonenumber="onGetPhoneNumber" disabled="{{loading}}">
|
||||
授权手机号登录(预留)
|
||||
</button>
|
||||
|
||||
<view class="agreement">登录即表示同意《用户协议》和《隐私政策》</view>
|
||||
<view class="agreement" bindtap="onAgreement">登录即表示同意《用户协议》和《隐私政策》</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -91,6 +91,10 @@ Page({
|
||||
}).catch(function () {})
|
||||
},
|
||||
|
||||
onAgreement: function () {
|
||||
wx.showModal({ title: '提示', content: '订阅协议内容建设中', showCancel: false })
|
||||
},
|
||||
|
||||
onSelect: function (e) {
|
||||
this.setData({ selected: e.currentTarget.dataset.plan })
|
||||
},
|
||||
|
||||
@@ -40,6 +40,6 @@
|
||||
<button class="btn-primary btn-primary-gold" bindtap="onPurchase" disabled="{{purchasing}}" loading="{{purchasing}}">
|
||||
{{subscription && subscription.status === 'active' && subRemaining > 0 ? '续费' : '确认支付'}}
|
||||
</button>
|
||||
<view class="agreement">支付即表示同意《订阅协议》</view>
|
||||
<view class="agreement" bindtap="onAgreement">支付即表示同意《订阅协议》</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -18,19 +18,32 @@ Page({
|
||||
completed: false,
|
||||
startedAt: 0,
|
||||
devMode: false,
|
||||
regionText: '全脸护理',
|
||||
},
|
||||
|
||||
_getRegionText: function (regionMask) {
|
||||
var REGION_MAP = {1:'左脸', 2:'右脸', 4:'额头', 8:'下巴', 16:'鼻', 32:'左眼', 64:'右眼'}
|
||||
if (regionMask === 0x7F) return '全脸护理'
|
||||
var names = []
|
||||
for (var bit in REGION_MAP) {
|
||||
if (regionMask & parseInt(bit)) names.push(REGION_MAP[bit])
|
||||
}
|
||||
return names.length > 0 ? names.join('+') + '护理' : '全脸护理'
|
||||
},
|
||||
|
||||
onLoad: function (options) {
|
||||
var app = getApp()
|
||||
this.setData({ statusBarHeight: app.globalData.statusBarHeight, devMode: config.__DEV__ || false })
|
||||
this._deviceId = ble.getDeviceId() || (app.globalData.currentDevice && app.globalData.currentDevice.device_id) || ''
|
||||
var regions = parseInt(options.regions) || 0x7F
|
||||
this.setData({
|
||||
regions: parseInt(options.regions) || 0x7F,
|
||||
regions: regions,
|
||||
wavelength: parseInt(options.wavelength) || 2,
|
||||
duration: parseInt(options.duration) || 600000,
|
||||
mode: parseInt(options.mode) || 0,
|
||||
remainingMs: parseInt(options.duration) || 600000,
|
||||
startedAt: Date.now()
|
||||
startedAt: Date.now(),
|
||||
regionText: this._getRegionText(regions)
|
||||
})
|
||||
|
||||
this._onStatus = this.onStatus.bind(this)
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
</view>
|
||||
|
||||
<view class="mode-tag-area">
|
||||
<text class="mode-tag">{{mode === 1 ? '✨ 智能模式' : '🔄 普通模式'}} · 全脸护理</text>
|
||||
<text class="mode-tag">{{mode === 1 ? '✨ 智能模式' : '🔄 普通模式'}} · {{regionText}}</text>
|
||||
</view>
|
||||
|
||||
<view class="relax-card">
|
||||
|
||||
@@ -7,7 +7,9 @@ Page({
|
||||
checking: false,
|
||||
result: null,
|
||||
error: '',
|
||||
devMode: false
|
||||
devMode: false,
|
||||
battery: 0,
|
||||
connected: false
|
||||
},
|
||||
|
||||
onBack: function () {
|
||||
@@ -21,6 +23,11 @@ Page({
|
||||
onLoad: function () {
|
||||
var app = getApp()
|
||||
this.setData({ statusBarHeight: app.globalData.statusBarHeight, devMode: config.__DEV__ || false })
|
||||
var device = app.globalData.currentDevice || {}
|
||||
this.setData({
|
||||
battery: device.battery || 0,
|
||||
connected: ble.isConnected()
|
||||
})
|
||||
},
|
||||
|
||||
onShow: function () {
|
||||
@@ -34,6 +41,9 @@ Page({
|
||||
if (this._onStatus) ble.off('status', this._onStatus)
|
||||
this._onStatus = function (status) {
|
||||
self.setData({ checking: false })
|
||||
if (status.battery !== undefined) {
|
||||
self.setData({ battery: status.battery, connected: true })
|
||||
}
|
||||
if (status.bind_status === 1) {
|
||||
self.setData({ result: 'ok' })
|
||||
} else {
|
||||
|
||||
@@ -11,9 +11,10 @@
|
||||
<view class="device-row-info">
|
||||
<view class="device-row-name">
|
||||
光子美容仪
|
||||
<text class="status-badge status-badge-green">已连接</text>
|
||||
<text class="status-badge status-badge-green" wx:if="{{connected}}">已连接</text>
|
||||
<text class="status-badge" style="background:#ccc;" wx:else>未连接</text>
|
||||
</view>
|
||||
<view class="device-row-battery">电量 85%</view>
|
||||
<view class="device-row-battery">电量 {{battery}}%</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
const settingsDao = require('../dao/settings.dao')
|
||||
|
||||
let cache = null
|
||||
let cacheTime = 0
|
||||
const TTL = 60000 // 1 minute
|
||||
|
||||
async function getSettings() {
|
||||
if (cache && Date.now() - cacheTime < TTL) return cache
|
||||
cache = await settingsDao.getAll()
|
||||
cacheTime = Date.now()
|
||||
return cache
|
||||
}
|
||||
|
||||
module.exports = { getSettings }
|
||||
@@ -3,6 +3,7 @@ const router = require('express').Router()
|
||||
const { ok, fail } = require('../lib/response')
|
||||
const { signUser, readBearer } = require('../lib/auth')
|
||||
const { code2Session } = require('../lib/wechat')
|
||||
const { getSettings } = require('../lib/settings-cache')
|
||||
const config = require('../config')
|
||||
const userDao = require('../dao/user.dao')
|
||||
const logDao = require('../dao/log.dao')
|
||||
@@ -10,6 +11,9 @@ const logDao = require('../dao/log.dao')
|
||||
const wrap = fn => (req, res, next) => fn(req, res, next).catch(next)
|
||||
|
||||
router.post('/auth/login', wrap(async (req, res) => {
|
||||
const settings = await getSettings()
|
||||
if (settings.maintenance_mode === true) return res.json(fail(2001, '系统维护中,请稍后再试'))
|
||||
|
||||
const session = await code2Session(req.body.code || '')
|
||||
let user = await userDao.findByOpenid(session.openid)
|
||||
if (!user) {
|
||||
|
||||
@@ -2,6 +2,7 @@ const router = require('express').Router()
|
||||
const { ok, fail } = require('../lib/response')
|
||||
const { requireUser } = require('../middleware/auth')
|
||||
const { randomHex } = require('../lib/auth')
|
||||
const { getSettings } = require('../lib/settings-cache')
|
||||
const bindingDao = require('../dao/binding.dao')
|
||||
const deviceDao = require('../dao/device.dao')
|
||||
const commandDao = require('../dao/command.dao')
|
||||
@@ -12,6 +13,9 @@ const logDao = require('../dao/log.dao')
|
||||
const wrap = fn => (req, res, next) => fn(req, res, next).catch(next)
|
||||
|
||||
router.post('/device/bind', requireUser, wrap(async (req, res) => {
|
||||
const settings = await getSettings()
|
||||
if (settings.enable_binding === false) return res.json(fail(2001, '设备绑定已关闭'))
|
||||
|
||||
const deviceId = String(req.body.device_id || '').trim()
|
||||
if (!deviceId) return res.json(fail(2001, 'device_id required'))
|
||||
|
||||
|
||||
在新工单中引用
屏蔽一个用户