feat: production readiness — feature gaps + config hardening
Miniprogram: - Add BLE reconnect button on home page when disconnected - Add loading states to index, profile, subscribe-plans pages - Add profile editing (avatar + nickname) with COS upload - Enable pull-to-refresh on history page - Fix auto-scan self.options → self.data inconsistency - Add console.error to silent catch blocks - Gate 'Simple Peripheral' BLE scan behind __DEV__ flag - Add production config comment to env.js Admin console: - Add empty state '暂无数据' to all 5 list views - Replace plain text plan input with select dropdown - Add type="date" to record date filters - Add production config comment Server: - CORS origin restricted in production (env CORS_ORIGIN) - DB pool size configurable via DB_POOL_SIZE env var - .env.example updated with WeChat Pay + production fields
这个提交包含在:
@@ -1,4 +1,5 @@
|
||||
var api = require('../../utils/api')
|
||||
var config = require('../../config/env')
|
||||
var app = getApp()
|
||||
|
||||
Page({
|
||||
@@ -6,7 +7,12 @@ Page({
|
||||
userInfo: null,
|
||||
subscription: null,
|
||||
deviceCount: 0,
|
||||
subRemaining: 0
|
||||
subRemaining: 0,
|
||||
loading: true,
|
||||
editing: false,
|
||||
editNickname: '',
|
||||
editAvatarUrl: '',
|
||||
saving: false
|
||||
},
|
||||
|
||||
onShow: function () {
|
||||
@@ -15,19 +21,29 @@ Page({
|
||||
|
||||
loadProfile: function () {
|
||||
var self = this
|
||||
api.getProfile().then(function (profile) {
|
||||
self.setData({ loading: true })
|
||||
|
||||
var p1 = api.getProfile().then(function (profile) {
|
||||
self.setData({
|
||||
userInfo: profile,
|
||||
deviceCount: profile.device_count || 0
|
||||
})
|
||||
}).catch(function () {})
|
||||
}).catch(function (err) {
|
||||
console.error('getProfile failed', err)
|
||||
})
|
||||
|
||||
api.getSubscription().then(function (sub) {
|
||||
var p2 = api.getSubscription().then(function (sub) {
|
||||
self.setData({
|
||||
subscription: sub,
|
||||
subRemaining: sub.remaining_days || 0
|
||||
})
|
||||
}).catch(function () {})
|
||||
}).catch(function (err) {
|
||||
console.error('getSubscription failed', err)
|
||||
})
|
||||
|
||||
Promise.all([p1, p2]).then(function () {
|
||||
self.setData({ loading: false })
|
||||
})
|
||||
},
|
||||
|
||||
onManageDevice: function () {
|
||||
@@ -75,6 +91,93 @@ Page({
|
||||
wx.navigateTo({ url: '/pages/contact/contact' })
|
||||
},
|
||||
|
||||
onEditProfile: function () {
|
||||
var info = this.data.userInfo || {}
|
||||
this.setData({
|
||||
editing: true,
|
||||
editNickname: info.nickname || '',
|
||||
editAvatarUrl: info.avatar || ''
|
||||
})
|
||||
},
|
||||
|
||||
onCancelEdit: function () {
|
||||
this.setData({ editing: false })
|
||||
},
|
||||
|
||||
onEditNicknameInput: function (e) {
|
||||
this.setData({ editNickname: e.detail.value || '' })
|
||||
},
|
||||
|
||||
onPickAvatar: function () {
|
||||
var self = this
|
||||
wx.chooseMedia({
|
||||
count: 1,
|
||||
mediaType: ['image'],
|
||||
sourceType: ['album', 'camera'],
|
||||
success: function (res) {
|
||||
if (res.tempFiles && res.tempFiles[0]) {
|
||||
self.setData({ editAvatarUrl: res.tempFiles[0].tempFilePath })
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
uploadAvatar: function (tempPath) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
var token = wx.getStorageSync('token')
|
||||
wx.uploadFile({
|
||||
url: config.API_BASE + '/api/v1/user/avatar',
|
||||
filePath: tempPath,
|
||||
name: 'file',
|
||||
header: { Authorization: 'Bearer ' + token },
|
||||
success: function (res) {
|
||||
try {
|
||||
var data = JSON.parse(res.data)
|
||||
if (data.code === 0 && data.data && data.data.avatar) {
|
||||
resolve(data.data.avatar)
|
||||
} else {
|
||||
reject(new Error(data.message || '上传失败'))
|
||||
}
|
||||
} catch (e) {
|
||||
reject(new Error('上传失败'))
|
||||
}
|
||||
},
|
||||
fail: function () { reject(new Error('上传失败')) }
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
onSaveProfile: function () {
|
||||
var self = this
|
||||
var nickname = (self.data.editNickname || '').trim()
|
||||
if (!nickname) {
|
||||
wx.showToast({ title: '昵称不能为空', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
self.setData({ saving: true })
|
||||
|
||||
var avatarUrl = self.data.editAvatarUrl
|
||||
var isLocalFile = avatarUrl && (avatarUrl.indexOf('wxfile://') === 0 || avatarUrl.indexOf('http://tmp') === 0)
|
||||
var avatarPromise = isLocalFile
|
||||
? self.uploadAvatar(avatarUrl)
|
||||
: Promise.resolve(avatarUrl || '')
|
||||
|
||||
avatarPromise.then(function (permanentUrl) {
|
||||
return api.updateProfile({
|
||||
nickname: nickname,
|
||||
avatar: permanentUrl || ''
|
||||
})
|
||||
}).then(function () {
|
||||
self.setData({ saving: false, editing: false })
|
||||
wx.showToast({ title: '保存成功', icon: 'success' })
|
||||
self.loadProfile()
|
||||
}).catch(function (err) {
|
||||
self.setData({ saving: false })
|
||||
wx.showToast({ title: err.message || '保存失败', icon: 'none' })
|
||||
})
|
||||
},
|
||||
|
||||
onLogout: function () {
|
||||
wx.showModal({
|
||||
title: '退出登录',
|
||||
|
||||
在新工单中引用
屏蔽一个用户