比较提交
14
次代码提交
| 作者 | SHA1 | 提交日期 | |
|---|---|---|---|
|
|
b5416f56af | ||
|
|
4466c53c7b | ||
|
|
f5eaf05067 | ||
|
|
28c711cd00 | ||
|
|
8e11b2da10 | ||
|
|
a7299708e2 | ||
|
|
f2ee086cc8 | ||
|
|
d8eb6677c6 | ||
|
|
cc5159ebb7 | ||
|
|
cdc6348ac4 | ||
|
|
a53e6c521c | ||
|
|
5775568cd6 | ||
|
|
dae50a74fd | ||
|
|
0341e62726 |
@@ -105,6 +105,15 @@
|
||||
<view class="toggle-dot"></view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="toggle-item">
|
||||
<view class="toggle-info">
|
||||
<text class="toggle-label">智能模式免费</text>
|
||||
<text class="toggle-desc">开启后所有用户可免费使用智能模式</text>
|
||||
</view>
|
||||
<view class="toggle-switch" :class="{ on: settings.smart_mode_free }" @click="settings.smart_mode_free = !settings.smart_mode_free">
|
||||
<view class="toggle-dot"></view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="toggle-item">
|
||||
<view class="toggle-info">
|
||||
<text class="toggle-label">维护模式</text>
|
||||
@@ -116,6 +125,12 @@
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section-card">
|
||||
<view class="section-title">智能诊断阈值(diagnosis_config)</view>
|
||||
<view class="config-desc">下发给小程序的实时诊断阈值,改此处即可调参无需发版。levels 为归一化吸收阈值;problem_wavelength 将问题类型映射到波长码 IR=1,R=2,UV=3,Y=4。保存前会校验 JSON 格式。</view>
|
||||
<textarea class="config-editor" v-model="diagnosisConfigText" placeholder='{"calibrated": false, ...}' />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="save-bar">
|
||||
@@ -142,10 +157,12 @@ export default {
|
||||
enable_register: true,
|
||||
enable_binding: true,
|
||||
enable_free_mode: true,
|
||||
smart_mode_free: true,
|
||||
maintenance_mode: false
|
||||
},
|
||||
timezoneOptions: ['Asia/Shanghai', 'Asia/Tokyo', 'America/New_York', 'Europe/London'],
|
||||
passwordForm: { old_password: '', new_password: '', confirm_password: '' }
|
||||
passwordForm: { old_password: '', new_password: '', confirm_password: '' },
|
||||
diagnosisConfigText: ''
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
@@ -156,15 +173,25 @@ export default {
|
||||
try {
|
||||
const data = await get('/api/v1/admin/settings')
|
||||
if (data) {
|
||||
Object.assign(this.settings, data)
|
||||
const { diagnosis_config, ...rest } = data
|
||||
Object.assign(this.settings, rest)
|
||||
this.diagnosisConfigText = JSON.stringify(diagnosis_config || {}, null, 2)
|
||||
}
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||
}
|
||||
},
|
||||
async onSave() {
|
||||
let diagnosisConfig
|
||||
try {
|
||||
await post('/api/v1/admin/settings', this.settings)
|
||||
diagnosisConfig = JSON.parse(this.diagnosisConfigText || '{}')
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '诊断阈值 JSON 格式错误', icon: 'none' })
|
||||
return
|
||||
}
|
||||
try {
|
||||
const payload = { ...this.settings, diagnosis_config: diagnosisConfig }
|
||||
await post('/api/v1/admin/settings', payload)
|
||||
uni.showToast({ title: '保存成功', icon: 'success' })
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '保存失败', icon: 'none' })
|
||||
@@ -293,6 +320,26 @@ export default {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.config-desc {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.config-editor {
|
||||
width: 100%;
|
||||
min-height: 200px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 6px;
|
||||
padding: 12px;
|
||||
font-size: 13px;
|
||||
font-family: 'Menlo', 'Consolas', monospace;
|
||||
color: #333;
|
||||
box-sizing: border-box;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.toggle-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
var http = require('./utils/request')
|
||||
var i18n = require('./i18n/index')
|
||||
|
||||
App({
|
||||
globalData: {
|
||||
@@ -12,9 +13,20 @@ App({
|
||||
onLaunch: function () {
|
||||
var sysInfo = wx.getSystemInfoSync()
|
||||
this.globalData.statusBarHeight = sysInfo.statusBarHeight || 44
|
||||
i18n.getLocale()
|
||||
i18n.applyTabBar()
|
||||
this.checkLogin()
|
||||
},
|
||||
|
||||
// Safety net: if the framework ever tries to open an unknown/empty route
|
||||
// (e.g. a stale page stack after the pages list changed), fall back to home
|
||||
// instead of surfacing a hard "page not found" crash.
|
||||
onPageNotFound: function (res) {
|
||||
if (res && res.isEntryPage) {
|
||||
wx.reLaunch({ url: '/pages/index/index', fail: function () {} })
|
||||
}
|
||||
},
|
||||
|
||||
checkLogin: function () {
|
||||
var token = wx.getStorageSync('token')
|
||||
if (!token) {
|
||||
|
||||
+4
-1
@@ -17,7 +17,10 @@
|
||||
"pages/profile/profile",
|
||||
"pages/history/history",
|
||||
"pages/help/help",
|
||||
"pages/contact/contact"
|
||||
"pages/contact/contact",
|
||||
"pages/light-info/light-info",
|
||||
"pages/manual-treatment/manual-treatment",
|
||||
"pages/scan-report/scan-report"
|
||||
],
|
||||
"window": {
|
||||
"navigationBarBackgroundColor": "#ffffff",
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
// Lightweight i18n for WeChat Mini Program (no official i18n API).
|
||||
//
|
||||
// Usage in a page:
|
||||
// var i18n = require('../../i18n/index')
|
||||
// onShow: function () { i18n.bind(this) } // injects `i18n` + `locale` into page data
|
||||
// WXML: {{i18n.common.loading}} / {{i18n.profile.title}}
|
||||
// JS strings: i18n.t('common.loading')
|
||||
//
|
||||
// Locale is persisted in storage; switching locale notifies listeners and the
|
||||
// convention is that every page re-runs i18n.bind(this) in onShow, so tab pages
|
||||
// pick up the new language the next time they are shown.
|
||||
|
||||
var locales = require('./locales/index')
|
||||
|
||||
var STORAGE_KEY = 'app_locale'
|
||||
var DEFAULT_LOCALE = 'zh'
|
||||
var SUPPORTED = ['zh', 'en']
|
||||
var LOCALE_NAMES = { zh: '中文', en: 'English' }
|
||||
|
||||
var _listeners = []
|
||||
var _current = null
|
||||
|
||||
function getSystemLocale() {
|
||||
try {
|
||||
var sys = wx.getSystemInfoSync()
|
||||
var lang = (sys.language || '').toLowerCase()
|
||||
return lang.indexOf('en') === 0 ? 'en' : 'zh'
|
||||
} catch (e) {
|
||||
return DEFAULT_LOCALE
|
||||
}
|
||||
}
|
||||
|
||||
function getLocale() {
|
||||
if (_current) return _current
|
||||
var saved = ''
|
||||
try { saved = wx.getStorageSync(STORAGE_KEY) } catch (e) {}
|
||||
_current = SUPPORTED.indexOf(saved) !== -1 ? saved : getSystemLocale()
|
||||
return _current
|
||||
}
|
||||
|
||||
function setLocale(locale) {
|
||||
if (SUPPORTED.indexOf(locale) === -1 || locale === _current) return
|
||||
_current = locale
|
||||
try { wx.setStorageSync(STORAGE_KEY, locale) } catch (e) {}
|
||||
applyTabBar()
|
||||
for (var i = 0; i < _listeners.length; i++) {
|
||||
try { _listeners[i](locale) } catch (e) {}
|
||||
}
|
||||
}
|
||||
|
||||
function onChange(fn) { if (typeof fn === 'function') _listeners.push(fn) }
|
||||
function offChange(fn) {
|
||||
_listeners = _listeners.filter(function (f) { return f !== fn })
|
||||
}
|
||||
|
||||
function getDict() {
|
||||
var loc = getLocale()
|
||||
return locales[loc] || locales[DEFAULT_LOCALE]
|
||||
}
|
||||
|
||||
function _lookup(dict, key) {
|
||||
if (!dict) return undefined
|
||||
var parts = key.split('.')
|
||||
var cur = dict
|
||||
for (var i = 0; i < parts.length; i++) {
|
||||
if (cur == null) return undefined
|
||||
cur = cur[parts[i]]
|
||||
}
|
||||
return cur
|
||||
}
|
||||
|
||||
// t('profile.title', {name: 'x'}) — falls back to zh, then to the raw key.
|
||||
function t(key, params) {
|
||||
var loc = getLocale()
|
||||
var val = _lookup(locales[loc], key)
|
||||
if (val === undefined) val = _lookup(locales[DEFAULT_LOCALE], key)
|
||||
if (typeof val !== 'string') return val === undefined ? key : val
|
||||
if (params) {
|
||||
val = val.replace(/\{(\w+)\}/g, function (m, k) {
|
||||
return params[k] !== undefined ? params[k] : m
|
||||
})
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
// Inject the full dictionary + current locale into a page/component's data.
|
||||
function bind(page) {
|
||||
if (page && typeof page.setData === 'function') {
|
||||
page.setData({ i18n: getDict(), locale: getLocale() })
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh the bottom tabBar labels to the current locale.
|
||||
function applyTabBar() {
|
||||
var d = getDict()
|
||||
var tab = (d && d.common && d.common.tab) || {}
|
||||
var items = [
|
||||
{ index: 0, text: tab.home },
|
||||
{ index: 1, text: tab.records },
|
||||
{ index: 2, text: tab.mine }
|
||||
]
|
||||
items.forEach(function (it) {
|
||||
if (!it.text) return
|
||||
wx.setTabBarItem({ index: it.index, text: it.text, fail: function () {} })
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
SUPPORTED: SUPPORTED,
|
||||
LOCALE_NAMES: LOCALE_NAMES,
|
||||
getLocale: getLocale,
|
||||
setLocale: setLocale,
|
||||
getSystemLocale: getSystemLocale,
|
||||
onChange: onChange,
|
||||
offChange: offChange,
|
||||
getDict: getDict,
|
||||
bind: bind,
|
||||
applyTabBar: applyTabBar,
|
||||
t: t
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
// Strings for the auto-scan (smart scan) page. Keep zh/en with identical key structure.
|
||||
// Pre/Post pairs are concatenated around a frame count in WXML (WXML cannot do {param}).
|
||||
module.exports = {
|
||||
zh: {
|
||||
title: '智能检测',
|
||||
subReady: '准备就绪',
|
||||
subWaiting: '等待佩戴贴合...',
|
||||
subCollectingPre: '采集数据中 (',
|
||||
subCollectingPost: '帧)...',
|
||||
subDone: '检测完成,正在启动...',
|
||||
readyHint: '设备已佩戴,点击下方按钮开始智能检测',
|
||||
startBtn: '开始检测',
|
||||
waitingStatus: '正在检测面膜贴合状态,请确保设备正确佩戴...',
|
||||
collectingStatusPre: '已贴合,正在采集光谱数据... 已收到 ',
|
||||
collectingStatusPost: ' 帧',
|
||||
doneStatusPre: '检测完成(共 ',
|
||||
doneStatusPost: ' 帧),正在启动治疗...',
|
||||
notWorn: '面膜未佩戴好,请重新佩戴后再试',
|
||||
timeout: '检测超时,未收到数据,请重试',
|
||||
cmdFailed: '检测命令发送失败'
|
||||
},
|
||||
en: {
|
||||
title: 'Smart Scan',
|
||||
subReady: 'Ready',
|
||||
subWaiting: 'Waiting for fit...',
|
||||
subCollectingPre: 'Collecting data (',
|
||||
subCollectingPost: ' frames)...',
|
||||
subDone: 'Scan complete, starting...',
|
||||
readyHint: 'Device is on. Tap the button below to start the smart scan',
|
||||
startBtn: 'Start Scan',
|
||||
waitingStatus: 'Checking mask fit, please make sure the device is worn correctly...',
|
||||
collectingStatusPre: 'Fitted. Collecting spectral data... received ',
|
||||
collectingStatusPost: ' frames',
|
||||
doneStatusPre: 'Scan complete (',
|
||||
doneStatusPost: ' frames), starting treatment...',
|
||||
notWorn: 'The mask is not worn properly. Please re-fit and try again',
|
||||
timeout: 'Scan timed out with no data. Please try again',
|
||||
cmdFailed: 'Failed to send scan command'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// Strings for the bindSuccess page. Fill zh/en with identical key structure.
|
||||
module.exports = {
|
||||
zh: {
|
||||
title: '绑定成功',
|
||||
subtitle: '设备已成功绑定',
|
||||
successTitle: '绑定成功!',
|
||||
freeModeOpen: '智能模式已开放',
|
||||
trialGranted: '恭喜获得{days}天智能模式试用',
|
||||
trialSub: '体验结束后可订阅继续使用',
|
||||
startUsing: '开始使用',
|
||||
goHome: '返回首页'
|
||||
},
|
||||
en: {
|
||||
title: 'Bound Successfully',
|
||||
subtitle: 'Device bound successfully',
|
||||
successTitle: 'Bound Successfully!',
|
||||
freeModeOpen: 'Smart mode unlocked',
|
||||
trialGranted: 'You got a {days}-day Smart mode trial',
|
||||
trialSub: 'Subscribe to keep using it after the trial ends',
|
||||
startUsing: 'Get Started',
|
||||
goHome: 'Back to Home'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// Strings for the bleConnect page. Fill zh/en with identical key structure.
|
||||
module.exports = {
|
||||
zh: {
|
||||
title: '蓝牙连接',
|
||||
searchingSub: '正在搜索设备...',
|
||||
connectingSub: '正在连接设备...',
|
||||
bindingSub: '正在绑定...',
|
||||
failedSub: '连接失败',
|
||||
bluetoothTip: '请确保手机蓝牙已开启',
|
||||
searching: '搜索中...',
|
||||
pairing: '正在配对...',
|
||||
failed: '连接失败',
|
||||
scanTimeout: '搜索超时,请确认设备已开机并在附近',
|
||||
confirmFailed: '后台确认绑定失败',
|
||||
bindFailed: '设备绑定失败',
|
||||
bindCmdFailed: '绑定命令失败'
|
||||
},
|
||||
en: {
|
||||
title: 'Bluetooth Connection',
|
||||
searchingSub: 'Searching for device...',
|
||||
connectingSub: 'Connecting to device...',
|
||||
bindingSub: 'Binding...',
|
||||
failedSub: 'Connection failed',
|
||||
bluetoothTip: 'Please make sure Bluetooth is enabled on your phone',
|
||||
searching: 'Searching...',
|
||||
pairing: 'Pairing...',
|
||||
failed: 'Connection failed',
|
||||
scanTimeout: 'Search timed out. Please make sure the device is powered on and nearby',
|
||||
confirmFailed: 'Failed to confirm binding on the server',
|
||||
bindFailed: 'Device binding failed',
|
||||
bindCmdFailed: 'Binding command failed'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
// Shared strings used across many pages.
|
||||
module.exports = {
|
||||
zh: {
|
||||
tab: { home: '首页', records: '记录', mine: '我的' },
|
||||
loading: '加载中...',
|
||||
confirm: '确定',
|
||||
cancel: '取消',
|
||||
save: '保存',
|
||||
saving: '保存中...',
|
||||
back: '返回',
|
||||
retry: '重试',
|
||||
close: '关闭',
|
||||
done: '完成',
|
||||
start: '开始',
|
||||
stop: '停止',
|
||||
unknown: '未知',
|
||||
saveSuccess: '保存成功',
|
||||
saveFailed: '保存失败',
|
||||
networkError: '网络异常,请重试',
|
||||
deviceNotConnected: '设备未连接',
|
||||
regions: {
|
||||
right: '右区',
|
||||
left: '左区',
|
||||
top: '上区',
|
||||
middle: '中区',
|
||||
bottom: '下区'
|
||||
},
|
||||
wavelengths: {
|
||||
red: '红光',
|
||||
infrared: '红外光',
|
||||
uv: '紫外光',
|
||||
yellow: '黄光'
|
||||
}
|
||||
},
|
||||
en: {
|
||||
tab: { home: 'Home', records: 'Records', mine: 'Me' },
|
||||
loading: 'Loading...',
|
||||
confirm: 'OK',
|
||||
cancel: 'Cancel',
|
||||
save: 'Save',
|
||||
saving: 'Saving...',
|
||||
back: 'Back',
|
||||
retry: 'Retry',
|
||||
close: 'Close',
|
||||
done: 'Done',
|
||||
start: 'Start',
|
||||
stop: 'Stop',
|
||||
unknown: 'Unknown',
|
||||
saveSuccess: 'Saved',
|
||||
saveFailed: 'Save failed',
|
||||
networkError: 'Network error, please retry',
|
||||
deviceNotConnected: 'Device not connected',
|
||||
regions: {
|
||||
right: 'Right',
|
||||
left: 'Left',
|
||||
top: 'Top',
|
||||
middle: 'Center',
|
||||
bottom: 'Bottom'
|
||||
},
|
||||
wavelengths: {
|
||||
red: 'Red',
|
||||
infrared: 'Infrared',
|
||||
uv: 'UV',
|
||||
yellow: 'Yellow'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// Strings for the contact page. Fill zh/en with identical key structure.
|
||||
module.exports = {
|
||||
zh: {
|
||||
title: '联系我们',
|
||||
placeholderText: '内容建设中',
|
||||
placeholderSub: '联系方式正在整理,敬请期待'
|
||||
},
|
||||
en: {
|
||||
title: 'Contact Us',
|
||||
placeholderText: 'Coming soon',
|
||||
placeholderSub: 'Contact details are being prepared. Stay tuned.'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// Strings for the help page. Fill zh/en with identical key structure.
|
||||
module.exports = {
|
||||
zh: {
|
||||
title: '使用帮助',
|
||||
placeholderText: '内容建设中',
|
||||
placeholderSub: '帮助文档正在编写,敬请期待'
|
||||
},
|
||||
en: {
|
||||
title: 'Help',
|
||||
placeholderText: 'Coming soon',
|
||||
placeholderSub: 'The help guide is being written. Stay tuned.'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// "记录" (history) page strings.
|
||||
module.exports = {
|
||||
zh: {
|
||||
statTotal: '累计次数',
|
||||
statHours: '累计时长',
|
||||
statMonth: '本月次数',
|
||||
recentTitle: '最近记录',
|
||||
smart: '✨ 智能',
|
||||
normal: '🔄 普通',
|
||||
empty: '暂无使用记录',
|
||||
loadingMore: '加载更多...',
|
||||
durationMin: '{min}分钟',
|
||||
today: '今天',
|
||||
yesterday: '昨天',
|
||||
dateMd: '{m}月{d}日'
|
||||
},
|
||||
en: {
|
||||
statTotal: 'Total Sessions',
|
||||
statHours: 'Total Hours',
|
||||
statMonth: 'This Month',
|
||||
recentTitle: 'Recent Records',
|
||||
smart: '✨ Smart',
|
||||
normal: '🔄 Normal',
|
||||
empty: 'No records yet',
|
||||
loadingMore: 'Loading more...',
|
||||
durationMin: '{min} min',
|
||||
today: 'Today',
|
||||
yesterday: 'Yesterday',
|
||||
dateMd: '{m}/{d}'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
// "首页" (index) page strings.
|
||||
module.exports = {
|
||||
zh: {
|
||||
noDevice: '还没有绑定设备',
|
||||
addDevice: '扫码添加设备',
|
||||
myDevice: '我的设备',
|
||||
connected: '已连接',
|
||||
disconnected: '未连接',
|
||||
reconnect: '重新连接',
|
||||
smartRemain: '智能模式 · 剩余{days}天',
|
||||
notSubscribed: '未订阅智能模式',
|
||||
smartOpen: '智能模式已开放',
|
||||
start: '开始使用',
|
||||
deviceManage: '设备管理',
|
||||
connectFailed: '连接失败',
|
||||
unbindAction: '解绑当前设备',
|
||||
unbindConfirmTitle: '确认解绑',
|
||||
unbindConfirmText: '解绑后将无法使用该设备,确定要解绑吗?',
|
||||
unbindLoading: '解绑中...',
|
||||
unbindDone: '已解绑',
|
||||
unbindFailed: '解绑失败'
|
||||
},
|
||||
en: {
|
||||
noDevice: 'No device bound yet',
|
||||
addDevice: 'Scan to add device',
|
||||
myDevice: 'My Device',
|
||||
connected: 'Connected',
|
||||
disconnected: 'Disconnected',
|
||||
reconnect: 'Reconnect',
|
||||
smartRemain: 'Smart Mode · {days} days left',
|
||||
notSubscribed: 'Smart Mode not subscribed',
|
||||
smartOpen: 'Smart Mode available',
|
||||
start: 'Start',
|
||||
deviceManage: 'Manage Device',
|
||||
connectFailed: 'Connection failed',
|
||||
unbindAction: 'Unbind current device',
|
||||
unbindConfirmTitle: 'Confirm Unbind',
|
||||
unbindConfirmText: 'You will not be able to use this device after unbinding. Continue?',
|
||||
unbindLoading: 'Unbinding...',
|
||||
unbindDone: 'Unbound',
|
||||
unbindFailed: 'Unbind failed'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// Merges per-namespace dictionaries into { zh: {...}, en: {...} }.
|
||||
// Each namespace file exports { zh: {...}, en: {...} } and owns its own strings,
|
||||
// so parallel work on different features never touches the same file.
|
||||
|
||||
var common = require('./common')
|
||||
var profile = require('./profile')
|
||||
var lightInfo = require('./lightInfo')
|
||||
var manual = require('./manual')
|
||||
var report = require('./report')
|
||||
var home = require('./home')
|
||||
var history = require('./history')
|
||||
var login = require('./login')
|
||||
var register = require('./register')
|
||||
var scan = require('./scan')
|
||||
var bleConnect = require('./bleConnect')
|
||||
var bindSuccess = require('./bindSuccess')
|
||||
var wearCheck = require('./wearCheck')
|
||||
var autoScan = require('./autoScan')
|
||||
var setup = require('./setup')
|
||||
var treating = require('./treating')
|
||||
var treatmentDone = require('./treatmentDone')
|
||||
var subPrompt = require('./subPrompt')
|
||||
var subPlans = require('./subPlans')
|
||||
var subSuccess = require('./subSuccess')
|
||||
var help = require('./help')
|
||||
var contact = require('./contact')
|
||||
|
||||
var namespaces = {
|
||||
common: common,
|
||||
profile: profile,
|
||||
lightInfo: lightInfo,
|
||||
manual: manual,
|
||||
report: report,
|
||||
home: home,
|
||||
history: history,
|
||||
login: login,
|
||||
register: register,
|
||||
scan: scan,
|
||||
bleConnect: bleConnect,
|
||||
bindSuccess: bindSuccess,
|
||||
wearCheck: wearCheck,
|
||||
autoScan: autoScan,
|
||||
setup: setup,
|
||||
treating: treating,
|
||||
treatmentDone: treatmentDone,
|
||||
subPrompt: subPrompt,
|
||||
subPlans: subPlans,
|
||||
subSuccess: subSuccess,
|
||||
help: help,
|
||||
contact: contact
|
||||
}
|
||||
|
||||
var zh = {}
|
||||
var en = {}
|
||||
for (var ns in namespaces) {
|
||||
if (!namespaces.hasOwnProperty(ns)) continue
|
||||
zh[ns] = namespaces[ns].zh || {}
|
||||
en[ns] = namespaces[ns].en || {}
|
||||
}
|
||||
|
||||
module.exports = { zh: zh, en: en }
|
||||
@@ -0,0 +1,95 @@
|
||||
// Strings for the light-therapy efficacy intro page (pages/light-info).
|
||||
// Owned by the light-info feature. Keep zh/en keys in sync.
|
||||
// Lifestyle / non-medical wording on purpose (广告法 / 微信审核 compliance).
|
||||
module.exports = {
|
||||
zh: {
|
||||
navTitle: '光疗功效介绍',
|
||||
headerSubtitle: '认识四种光与护理模式',
|
||||
lightsSectionTitle: '四种光',
|
||||
suitsLabel: '适合',
|
||||
lights: {
|
||||
red: {
|
||||
name: '红光',
|
||||
wavelength: '630–640nm',
|
||||
effect: '嫩肤焕亮',
|
||||
desc: '改善暗沉、细纹与松弛感,帮助肌肤紧致提亮。',
|
||||
suits: '初老、暗沉肌'
|
||||
},
|
||||
uv: {
|
||||
name: '紫光',
|
||||
wavelength: '415–425nm',
|
||||
effect: '净痘控油',
|
||||
desc: '改善痘痘肌、油光与粗大毛孔,帮助肌肤清爽。',
|
||||
suits: '油痘肌'
|
||||
},
|
||||
yellow: {
|
||||
name: '黄光',
|
||||
wavelength: '585–595nm',
|
||||
effect: '匀净提亮',
|
||||
desc: '改善暗黄、色素不均与泛红,帮助肤色更均匀。',
|
||||
suits: '暗黄、敏感泛红肌'
|
||||
},
|
||||
infrared: {
|
||||
name: '红外光',
|
||||
wavelength: '830–840nm',
|
||||
effect: '深层焕活',
|
||||
desc: '促进循环、舒缓疲劳感,帮助肌肤放松焕活。',
|
||||
suits: '疲劳、需深层舒缓的肌肤'
|
||||
}
|
||||
},
|
||||
modesSectionTitle: '护理模式',
|
||||
modes: [
|
||||
{ name: '嫩肤焕亮', desc: '红光护理' },
|
||||
{ name: '净痘控油', desc: '紫光护理' },
|
||||
{ name: '匀净提亮', desc: '黄光护理' },
|
||||
{ name: '深层焕活', desc: '红外护理' },
|
||||
{ name: '舒缓修护', desc: '红+黄复合光,温和护理敏感肌' }
|
||||
],
|
||||
disclaimer: '本页内容为光护理科普,不构成医疗建议。'
|
||||
},
|
||||
en: {
|
||||
navTitle: 'Light Care Guide',
|
||||
headerSubtitle: 'Meet the four lights and the care modes',
|
||||
lightsSectionTitle: 'Four Lights',
|
||||
suitsLabel: 'Best for',
|
||||
lights: {
|
||||
red: {
|
||||
name: 'Red',
|
||||
wavelength: '630–640nm',
|
||||
effect: 'Glow & Firm',
|
||||
desc: 'Softens dullness, fine lines and looseness for firmer, brighter-looking skin.',
|
||||
suits: 'Early-aging, dull skin'
|
||||
},
|
||||
uv: {
|
||||
name: 'Violet',
|
||||
wavelength: '415–425nm',
|
||||
effect: 'Clear & Balance',
|
||||
desc: 'Helps blemish-prone, oily skin and visible pores feel fresher.',
|
||||
suits: 'Oily, blemish-prone skin'
|
||||
},
|
||||
yellow: {
|
||||
name: 'Yellow',
|
||||
wavelength: '585–595nm',
|
||||
effect: 'Even & Brighten',
|
||||
desc: 'Eases sallowness, uneven tone and redness for a more even look.',
|
||||
suits: 'Sallow, sensitive, easily-red skin'
|
||||
},
|
||||
infrared: {
|
||||
name: 'Infrared',
|
||||
wavelength: '830–840nm',
|
||||
effect: 'Deep Revive',
|
||||
desc: 'Supports circulation and soothes tired-feeling skin for a relaxed, revived look.',
|
||||
suits: 'Tired skin needing deep comfort'
|
||||
}
|
||||
},
|
||||
modesSectionTitle: 'Care Modes',
|
||||
modes: [
|
||||
{ name: 'Glow & Firm', desc: 'Red light care' },
|
||||
{ name: 'Clear & Balance', desc: 'Violet light care' },
|
||||
{ name: 'Even & Brighten', desc: 'Yellow light care' },
|
||||
{ name: 'Deep Revive', desc: 'Infrared care' },
|
||||
{ name: 'Soothe & Repair', desc: 'Red + yellow blend, gentle care for sensitive skin' }
|
||||
],
|
||||
disclaimer: 'This guide is for light-care education only and is not medical advice.'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// Strings for the login page. Fill zh/en with identical key structure.
|
||||
module.exports = {
|
||||
zh: {
|
||||
wechatLogin: '微信登录',
|
||||
welcome: '欢迎使用LumiFlow',
|
||||
desc: '登录后即可使用全部功能',
|
||||
agreement: '登录即表示同意《用户协议》和《隐私政策》',
|
||||
loginFailed: '登录失败',
|
||||
agreementTitle: '提示',
|
||||
agreementContent: '用户协议和隐私政策内容建设中'
|
||||
},
|
||||
en: {
|
||||
wechatLogin: 'WeChat Login',
|
||||
welcome: 'Welcome to LumiFlow',
|
||||
desc: 'Log in to unlock all features',
|
||||
agreement: 'By logging in, you agree to the Terms of Service and Privacy Policy',
|
||||
loginFailed: 'Login failed',
|
||||
agreementTitle: 'Notice',
|
||||
agreementContent: 'Terms of Service and Privacy Policy are coming soon'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
// Strings for the manual treatment (direct wavelength/function select) page
|
||||
// (pages/manual-treatment). Owned by the manual-mode feature. Keep zh/en in sync.
|
||||
module.exports = {
|
||||
zh: {
|
||||
navTitle: '手动护理',
|
||||
navSubtitle: '直接选择护理功能,快速开始',
|
||||
functionTitle: '选择护理功能',
|
||||
functions: {
|
||||
rejuvenate: { name: '嫩肤焕亮', benefit: '红光温和促进胶原,改善暗沉' },
|
||||
acne: { name: '净痘控油', benefit: '紫光清洁抑菌,平衡油脂' },
|
||||
eventone: { name: '匀净提亮', benefit: '黄光舒缓肌肤,提亮肤色' },
|
||||
revitalize: { name: '深层焕活', benefit: '红外深层导入,焕活紧致' }
|
||||
},
|
||||
regionTitle: '选择区域',
|
||||
durationTitle: '选择时长',
|
||||
durationUnit: '{min} 分钟',
|
||||
safetyTip: '单次护理最长 10 分钟,亮度已设为护理中档,请佩戴护目镜',
|
||||
start: '开始护理',
|
||||
starting: '启动中...',
|
||||
noRegionSelected: '请至少选择一个区域',
|
||||
startFailed: '启动失败',
|
||||
entryLink: '手动选择波长护理 ›'
|
||||
},
|
||||
en: {
|
||||
navTitle: 'Manual Care',
|
||||
navSubtitle: 'Pick a care function and start right away',
|
||||
functionTitle: 'Select Care Function',
|
||||
functions: {
|
||||
rejuvenate: { name: 'Rejuvenate & Brighten', benefit: 'Red light boosts collagen, eases dullness' },
|
||||
acne: { name: 'Clear & Oil Control', benefit: 'UV light cleanses and balances oil' },
|
||||
eventone: { name: 'Even & Brighten', benefit: 'Yellow light soothes and brightens tone' },
|
||||
revitalize: { name: 'Deep Revitalize', benefit: 'Infrared penetrates deep to firm skin' }
|
||||
},
|
||||
regionTitle: 'Select Regions',
|
||||
durationTitle: 'Select Duration',
|
||||
durationUnit: '{min} min',
|
||||
safetyTip: 'Sessions last up to 10 min at a safe medium brightness. Wear eye protection.',
|
||||
start: 'Start Care',
|
||||
starting: 'Starting...',
|
||||
noRegionSelected: 'Please select at least one region',
|
||||
startFailed: 'Failed to start',
|
||||
entryLink: 'Manual wavelength care ›'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
// "我的" page strings + language switcher.
|
||||
module.exports = {
|
||||
zh: {
|
||||
notLoggedIn: '未登录',
|
||||
editProfile: '编辑资料',
|
||||
editProfileTitle: '编辑资料',
|
||||
changeAvatar: '点击更换头像',
|
||||
nickname: '昵称',
|
||||
nicknamePlaceholder: '请输入昵称',
|
||||
nicknameRequired: '昵称不能为空',
|
||||
smartMode: '智能模式',
|
||||
smartModeOpen: '已开放',
|
||||
free: '免费',
|
||||
inUse: '使用中',
|
||||
remainDays: '剩余 {days}天',
|
||||
notSubscribed: '未订阅',
|
||||
goSubscribe: '去订阅 ›',
|
||||
menuDevice: '我的设备',
|
||||
bound: '已绑定',
|
||||
unbound: '未绑定',
|
||||
menuHistory: '使用记录',
|
||||
menuSubscription: '订阅管理',
|
||||
menuHelp: '使用帮助',
|
||||
menuContact: '联系我们',
|
||||
menuLightInfo: '光疗功效介绍',
|
||||
language: '语言 / Language',
|
||||
logout: '退出登录',
|
||||
logoutConfirmTitle: '退出登录',
|
||||
logoutConfirmText: '确定要退出登录吗?',
|
||||
unbindConfirmTitle: '确认解绑',
|
||||
unbindConfirmText: '解绑后将无法使用该设备,确定要解绑吗?',
|
||||
unbindAction: '解绑当前设备',
|
||||
unbindDone: '已解绑',
|
||||
unbindFailed: '解绑失败'
|
||||
},
|
||||
en: {
|
||||
notLoggedIn: 'Not logged in',
|
||||
editProfile: 'Edit',
|
||||
editProfileTitle: 'Edit Profile',
|
||||
changeAvatar: 'Tap to change avatar',
|
||||
nickname: 'Nickname',
|
||||
nicknamePlaceholder: 'Enter nickname',
|
||||
nicknameRequired: 'Nickname is required',
|
||||
smartMode: 'Smart Mode',
|
||||
smartModeOpen: 'Available',
|
||||
free: 'Free',
|
||||
inUse: 'Active',
|
||||
remainDays: '{days} days left',
|
||||
notSubscribed: 'Not subscribed',
|
||||
goSubscribe: 'Subscribe ›',
|
||||
menuDevice: 'My Device',
|
||||
bound: 'Bound',
|
||||
unbound: 'Not bound',
|
||||
menuHistory: 'Usage Records',
|
||||
menuSubscription: 'Subscription',
|
||||
menuHelp: 'Help',
|
||||
menuContact: 'Contact Us',
|
||||
menuLightInfo: 'Light Therapy Guide',
|
||||
language: '语言 / Language',
|
||||
logout: 'Log Out',
|
||||
logoutConfirmTitle: 'Log Out',
|
||||
logoutConfirmText: 'Are you sure you want to log out?',
|
||||
unbindConfirmTitle: 'Confirm Unbind',
|
||||
unbindConfirmText: 'You will not be able to use this device after unbinding. Continue?',
|
||||
unbindAction: 'Unbind current device',
|
||||
unbindDone: 'Unbound',
|
||||
unbindFailed: 'Unbind failed'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
// Strings for the register page. Fill zh/en with identical key structure.
|
||||
module.exports = {
|
||||
zh: {
|
||||
subtitle: '完善个人资料',
|
||||
avatarSection: '设置头像',
|
||||
changeAvatar: '点击更换',
|
||||
nicknameSection: '设置昵称',
|
||||
nicknamePlaceholder: '请输入昵称',
|
||||
authorizePhone: '授权手机号',
|
||||
phoneHint: '手机号用于接收服务通知,必须授权',
|
||||
submit: '完成注册',
|
||||
agreement: '注册即表示同意《用户协议》和《隐私政策》',
|
||||
nicknamePrefix: '用户',
|
||||
phoneRequired: '需要授权手机号才能完成注册',
|
||||
phoneAuthSuccess: '手机号授权成功',
|
||||
phoneAuthFailed: '手机号授权失败',
|
||||
authorized: '已授权',
|
||||
uploadFailed: '上传失败',
|
||||
phoneFirst: '请先授权手机号',
|
||||
nicknameRequired: '请输入昵称',
|
||||
agreementTitle: '提示',
|
||||
agreementContent: '用户协议和隐私政策内容建设中'
|
||||
},
|
||||
en: {
|
||||
subtitle: 'Complete Your Profile',
|
||||
avatarSection: 'Set Avatar',
|
||||
changeAvatar: 'Tap to change',
|
||||
nicknameSection: 'Set Nickname',
|
||||
nicknamePlaceholder: 'Enter a nickname',
|
||||
authorizePhone: 'Authorize Phone Number',
|
||||
phoneHint: 'Your phone number is used to receive service notifications and is required',
|
||||
submit: 'Complete Registration',
|
||||
agreement: 'By registering, you agree to the Terms of Service and Privacy Policy',
|
||||
nicknamePrefix: 'User',
|
||||
phoneRequired: 'Phone number authorization is required to complete registration',
|
||||
phoneAuthSuccess: 'Phone number authorized',
|
||||
phoneAuthFailed: 'Phone authorization failed',
|
||||
authorized: 'Authorized',
|
||||
uploadFailed: 'Upload failed',
|
||||
phoneFirst: 'Please authorize your phone number first',
|
||||
nicknameRequired: 'Please enter a nickname',
|
||||
agreementTitle: 'Notice',
|
||||
agreementContent: 'Terms of Service and Privacy Policy are coming soon'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
// Strings for the scan report page (pages/scan-report).
|
||||
module.exports = {
|
||||
zh: {
|
||||
title: '肌肤扫描报告',
|
||||
subtitle: '基于多光谱反射的肤质倾向参考',
|
||||
scannedAt: '扫描时间',
|
||||
overallTitle: '整体倾向',
|
||||
regionTitle: '分区分析',
|
||||
recommendTitle: '推荐护理',
|
||||
noProblem: '未见明显倾向',
|
||||
startTreatment: '开始推荐护理',
|
||||
manualInstead: '我自己选',
|
||||
disclaimer: '本报告依据光反射信号给出肤质倾向参考,不构成医疗诊断。实际护理请结合自身情况。',
|
||||
conceptNotice: '当前诊断依据尚在标定中,结果仅供参考',
|
||||
level: { low: '轻度', mid: '中度', high: '明显' },
|
||||
problems: {
|
||||
aging: '光老化 / 暗沉',
|
||||
acne: '痘痘 / 出油',
|
||||
pigment: '色素 / 暗黄',
|
||||
deep: '深层循环 / 疲劳'
|
||||
},
|
||||
regionNames: {
|
||||
right: '右脸',
|
||||
left: '左脸',
|
||||
top: '额头',
|
||||
middle: '鼻区',
|
||||
bottom: '下巴'
|
||||
}
|
||||
},
|
||||
en: {
|
||||
title: 'Skin Scan Report',
|
||||
subtitle: 'Skin-tendency reference from multispectral reflection',
|
||||
scannedAt: 'Scanned at',
|
||||
overallTitle: 'Overall Tendency',
|
||||
regionTitle: 'Zone Analysis',
|
||||
recommendTitle: 'Recommended Care',
|
||||
noProblem: 'No notable tendency',
|
||||
startTreatment: 'Start Recommended Care',
|
||||
manualInstead: 'Choose Myself',
|
||||
disclaimer: 'This report gives a skin-tendency reference from light-reflection signals and is not a medical diagnosis. Please consider your own condition.',
|
||||
conceptNotice: 'Diagnosis thresholds are still being calibrated; results are for reference only',
|
||||
level: { low: 'Mild', mid: 'Moderate', high: 'Notable' },
|
||||
problems: {
|
||||
aging: 'Photoaging / Dullness',
|
||||
acne: 'Acne / Oiliness',
|
||||
pigment: 'Pigment / Sallowness',
|
||||
deep: 'Deep circulation / Fatigue'
|
||||
},
|
||||
regionNames: {
|
||||
right: 'Right cheek',
|
||||
left: 'Left cheek',
|
||||
top: 'Forehead',
|
||||
middle: 'Nose',
|
||||
bottom: 'Chin'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// Strings for the scan page. Fill zh/en with identical key structure.
|
||||
module.exports = {
|
||||
zh: {
|
||||
title: '扫码绑定',
|
||||
subtitle: '扫描设备底部二维码',
|
||||
scanText: '扫描设备二维码',
|
||||
scan: '扫码',
|
||||
scanning: '扫描中...',
|
||||
manualInput: '手动输入设备号',
|
||||
manualPlaceholder: '请输入设备ID,如 672B6D5A 4DCD861',
|
||||
invalidQr: '无效的设备二维码',
|
||||
alreadyBound: '已绑定设备',
|
||||
bindFailed: '绑定失败',
|
||||
invalidFormat: '设备号格式不正确'
|
||||
},
|
||||
en: {
|
||||
title: 'Scan to Bind',
|
||||
subtitle: 'Scan the QR code on the bottom of the device',
|
||||
scanText: 'Scan device QR code',
|
||||
scan: 'Scan',
|
||||
scanning: 'Scanning...',
|
||||
manualInput: 'Enter Device ID Manually',
|
||||
manualPlaceholder: 'Enter device ID, e.g. 672B6D5A 4DCD861',
|
||||
invalidQr: 'Invalid device QR code',
|
||||
alreadyBound: 'Device already bound',
|
||||
bindFailed: 'Binding failed',
|
||||
invalidFormat: 'Invalid device ID format'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// Strings for the treatment-setup page. Keep zh/en with identical key structure.
|
||||
module.exports = {
|
||||
zh: {
|
||||
title: '使用设置',
|
||||
subtitle: '选择模式和区域',
|
||||
selectMode: '选择模式',
|
||||
normalMode: '普通模式',
|
||||
normalModeSub: '6色循环',
|
||||
smartMode: '智能模式',
|
||||
remainDays: '剩余{days}天',
|
||||
subscribeUnlock: '订阅解锁',
|
||||
selectRegion: '选择区域',
|
||||
normalTip: '普通模式固定 10 分钟,使用区域默认全区域,不可调整',
|
||||
startBtn: '开始使用',
|
||||
noRegion: '请至少选择一个区域',
|
||||
startFailed: '启动失败'
|
||||
},
|
||||
en: {
|
||||
title: 'Session Setup',
|
||||
subtitle: 'Choose mode and regions',
|
||||
selectMode: 'Select Mode',
|
||||
normalMode: 'Normal Mode',
|
||||
normalModeSub: '6-color cycle',
|
||||
smartMode: 'Smart Mode',
|
||||
remainDays: '{days} days left',
|
||||
subscribeUnlock: 'Subscribe to unlock',
|
||||
selectRegion: 'Select Regions',
|
||||
normalTip: 'Normal mode runs a fixed 10 minutes over all regions and cannot be adjusted',
|
||||
startBtn: 'Start',
|
||||
noRegion: 'Please select at least one region',
|
||||
startFailed: 'Failed to start'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
// Strings for the subPlans page. Fill zh/en with identical key structure.
|
||||
module.exports = {
|
||||
zh: {
|
||||
title: '订阅服务',
|
||||
subtitle: '解锁智能模式',
|
||||
currentPlanLabel: '当前套餐',
|
||||
remainDaysLabel: '剩余天数',
|
||||
daysValue: '{days}天',
|
||||
choosePlan: '选择订阅套餐',
|
||||
loadingPlans: '加载套餐中...',
|
||||
smartMode: '智能模式',
|
||||
smartModeDesc: '智能自动调节',
|
||||
renew: '续费',
|
||||
confirmPay: '确认支付',
|
||||
agreement: '支付即表示同意《订阅协议》',
|
||||
// plan names (also used as fallback plan labels)
|
||||
planTrial: '试用',
|
||||
planMonthly: '月卡',
|
||||
planYearly: '年卡',
|
||||
// computed plan descriptions
|
||||
free: '免费',
|
||||
descTrial: '{days}天免费体验',
|
||||
descSave: '省¥{amount}',
|
||||
descDaily: '约{price}元/天',
|
||||
tagRecommend: '推荐',
|
||||
tagUsed: '已使用',
|
||||
// modals / toasts
|
||||
tipTitle: '提示',
|
||||
agreementBuilding: '订阅协议内容建设中',
|
||||
activateTrialTitle: '激活试用',
|
||||
activateTrialConfirm: '免费试用 {desc}?',
|
||||
confirmRenewTitle: '确认续费',
|
||||
confirmPayTitle: '确认支付',
|
||||
renewContent: '在现有订阅基础上延长{days}天,支付 ¥{price}',
|
||||
payContent: '支付 ¥{price}',
|
||||
trialActivated: '试用已激活',
|
||||
activateFailed: '激活失败',
|
||||
payUnavailable: '支付服务暂不可用',
|
||||
payCancelled: '已取消支付',
|
||||
payFailed: '支付失败',
|
||||
createOrderFailed: '创建订单失败',
|
||||
paySuccess: '支付成功',
|
||||
payProcessing: '支付处理中,请稍后在订阅页查看'
|
||||
},
|
||||
en: {
|
||||
title: 'Subscription',
|
||||
subtitle: 'Unlock Smart Mode',
|
||||
currentPlanLabel: 'Current plan',
|
||||
remainDaysLabel: 'Days remaining',
|
||||
daysValue: '{days} days',
|
||||
choosePlan: 'Choose a plan',
|
||||
loadingPlans: 'Loading plans...',
|
||||
smartMode: 'Smart Mode',
|
||||
smartModeDesc: 'Automatic smart adjustment',
|
||||
renew: 'Renew',
|
||||
confirmPay: 'Pay Now',
|
||||
agreement: 'By paying you agree to the Subscription Agreement',
|
||||
// plan names (also used as fallback plan labels)
|
||||
planTrial: 'Trial',
|
||||
planMonthly: 'Monthly',
|
||||
planYearly: 'Yearly',
|
||||
// computed plan descriptions
|
||||
free: 'Free',
|
||||
descTrial: '{days}-day free trial',
|
||||
descSave: 'Save ¥{amount}',
|
||||
descDaily: 'About ¥{price}/day',
|
||||
tagRecommend: 'Best value',
|
||||
tagUsed: 'Used',
|
||||
// modals / toasts
|
||||
tipTitle: 'Notice',
|
||||
agreementBuilding: 'The subscription agreement is being prepared',
|
||||
activateTrialTitle: 'Activate trial',
|
||||
activateTrialConfirm: 'Start free trial: {desc}?',
|
||||
confirmRenewTitle: 'Confirm renewal',
|
||||
confirmPayTitle: 'Confirm payment',
|
||||
renewContent: 'Extend your current subscription by {days} days for ¥{price}',
|
||||
payContent: 'Pay ¥{price}',
|
||||
trialActivated: 'Trial activated',
|
||||
activateFailed: 'Activation failed',
|
||||
payUnavailable: 'Payment service is unavailable',
|
||||
payCancelled: 'Payment cancelled',
|
||||
payFailed: 'Payment failed',
|
||||
createOrderFailed: 'Failed to create order',
|
||||
paySuccess: 'Payment successful',
|
||||
payProcessing: 'Payment is processing. Please check the subscription page later.'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// Strings for the subPrompt page. Fill zh/en with identical key structure.
|
||||
module.exports = {
|
||||
zh: {
|
||||
title: '使用设置',
|
||||
subtitle: '试用期已结束',
|
||||
selectMode: '选择模式',
|
||||
normalMode: '普通模式',
|
||||
normalModeSub: '6色循环',
|
||||
smartMode: '智能模式',
|
||||
smartModeSub: '订阅解锁',
|
||||
trialEndedTitle: '试用已结束',
|
||||
trialEndedSub: '订阅后继续使用智能模式',
|
||||
subscribeNow: '立即订阅',
|
||||
useNormalFirst: '先使用普通模式'
|
||||
},
|
||||
en: {
|
||||
title: 'Settings',
|
||||
subtitle: 'Your free trial has ended',
|
||||
selectMode: 'Choose a mode',
|
||||
normalMode: 'Standard Mode',
|
||||
normalModeSub: '6-color cycle',
|
||||
smartMode: 'Smart Mode',
|
||||
smartModeSub: 'Unlock with subscription',
|
||||
trialEndedTitle: 'Trial ended',
|
||||
trialEndedSub: 'Subscribe to keep using Smart Mode',
|
||||
subscribeNow: 'Subscribe Now',
|
||||
useNormalFirst: 'Use Standard Mode for now'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// Strings for the subSuccess page. Fill zh/en with identical key structure.
|
||||
module.exports = {
|
||||
zh: {
|
||||
title: '订阅成功',
|
||||
subtitle: '欢迎使用智能模式',
|
||||
successTitle: '订阅成功!',
|
||||
subscribed: '已订阅',
|
||||
validUntil: '有效期至:',
|
||||
startSmart: '立即体验智能模式',
|
||||
backHome: '返回首页',
|
||||
planTrial: '试用会员',
|
||||
planMonthly: '月卡会员',
|
||||
planYearly: '年卡会员',
|
||||
planDefault: '会员'
|
||||
},
|
||||
en: {
|
||||
title: 'Subscription Successful',
|
||||
subtitle: 'Welcome to Smart Mode',
|
||||
successTitle: 'Subscription successful!',
|
||||
subscribed: 'Subscribed',
|
||||
validUntil: 'Valid until: ',
|
||||
startSmart: 'Try Smart Mode now',
|
||||
backHome: 'Back to Home',
|
||||
planTrial: 'Trial Member',
|
||||
planMonthly: 'Monthly Member',
|
||||
planYearly: 'Yearly Member',
|
||||
planDefault: 'Member'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
// Strings for the treating (live treatment) page. Keep zh/en with identical key structure.
|
||||
module.exports = {
|
||||
zh: {
|
||||
title: '使用中',
|
||||
subtitle: '正在使用...',
|
||||
remainingLabel: '剩余时间',
|
||||
smartMode: '✨ 智能模式',
|
||||
normalMode: '🔄 普通模式',
|
||||
allRegions: '全区域',
|
||||
regionTopLeft: '左上区',
|
||||
regionTopRight: '右上区',
|
||||
relax1: '请放松心情',
|
||||
relax2: '享受美好时光',
|
||||
stopBtn: '停止使用',
|
||||
fittingLost: '检测到离肤,请重新佩戴设备',
|
||||
fittingRestored: '已重新贴合,继续使用',
|
||||
faultTitle: '设备故障',
|
||||
faultContent: '设备上报故障,已自动停止。',
|
||||
lowBatteryTitle: '电量过低',
|
||||
lowBatteryContent: '设备电量不足5%,已自动停止。请及时充电。',
|
||||
lowBatteryWarn: '电量低,请及时充电',
|
||||
exceptionTitle: '设备异常',
|
||||
exceptionContent: '使用过程中出现异常',
|
||||
disconnected: '设备连接断开,正在重连...',
|
||||
reconnected: '设备已重新连接',
|
||||
reconnectFailedTitle: '连接丢失',
|
||||
reconnectFailedContent: '设备蓝牙连接已断开,无法恢复。本次使用数据将保存。',
|
||||
stopTitle: '结束使用',
|
||||
stopContent: '确定要提前结束本次使用吗?'
|
||||
},
|
||||
en: {
|
||||
title: 'In Progress',
|
||||
subtitle: 'Treatment in progress...',
|
||||
remainingLabel: 'Time Left',
|
||||
smartMode: '✨ Smart Mode',
|
||||
normalMode: '🔄 Normal Mode',
|
||||
allRegions: 'All regions',
|
||||
regionTopLeft: 'Top-left',
|
||||
regionTopRight: 'Top-right',
|
||||
relax1: 'Relax and unwind',
|
||||
relax2: 'Enjoy your moment',
|
||||
stopBtn: 'Stop',
|
||||
fittingLost: 'Device lifted off skin. Please put it back on',
|
||||
fittingRestored: 'Re-fitted, continuing',
|
||||
faultTitle: 'Device Fault',
|
||||
faultContent: 'The device reported a fault and stopped automatically.',
|
||||
lowBatteryTitle: 'Battery Too Low',
|
||||
lowBatteryContent: 'Battery is below 5%. Stopped automatically. Please charge soon.',
|
||||
lowBatteryWarn: 'Low battery, please charge soon',
|
||||
exceptionTitle: 'Device Error',
|
||||
exceptionContent: 'Something went wrong during the session',
|
||||
disconnected: 'Device disconnected, reconnecting...',
|
||||
reconnected: 'Device reconnected',
|
||||
reconnectFailedTitle: 'Connection Lost',
|
||||
reconnectFailedContent: 'The Bluetooth connection was lost and cannot be restored. This session will be saved.',
|
||||
stopTitle: 'End Session',
|
||||
stopContent: 'Are you sure you want to end this session early?'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// Strings for the treatment-done page. Keep zh/en with identical key structure.
|
||||
module.exports = {
|
||||
zh: {
|
||||
title: '使用完成',
|
||||
headerSub: '本次使用已结束',
|
||||
resultTitle: '使用完成!',
|
||||
durationLabel: '使用时长',
|
||||
regionLabel: '使用区域',
|
||||
recorded: '本次使用已记录',
|
||||
backHome: '返回首页',
|
||||
durationMin: '{min}分钟',
|
||||
durationSec: '{sec}秒'
|
||||
},
|
||||
en: {
|
||||
title: 'Complete',
|
||||
headerSub: 'This session has ended',
|
||||
resultTitle: 'Session Complete!',
|
||||
durationLabel: 'Duration',
|
||||
regionLabel: 'Regions',
|
||||
recorded: 'This session has been recorded',
|
||||
backHome: 'Back to Home',
|
||||
durationMin: '{min} min',
|
||||
durationSec: '{sec} s'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
// Strings for the wear-check page. Keep zh/en with identical key structure.
|
||||
module.exports = {
|
||||
zh: {
|
||||
title: '确认佩戴',
|
||||
subtitle: '请确保设备正确佩戴',
|
||||
connected: '已连接',
|
||||
disconnected: '未连接',
|
||||
battery: '电量',
|
||||
guideTitle: '佩戴指引',
|
||||
guideImg: '设备佩戴示意图',
|
||||
step1: '1. 取出设备,展开弹性绑带',
|
||||
step2: '2. 将设备正确佩戴',
|
||||
step3: '3. 调节绑带至舒适位置',
|
||||
confirmBtn: '确认已佩戴,开始检测',
|
||||
reconnect: '重新连接',
|
||||
connecting: '正在连接...',
|
||||
connectSuccess: '连接成功',
|
||||
connectFailed: '连接失败',
|
||||
searchTimeout: '搜索超时'
|
||||
},
|
||||
en: {
|
||||
title: 'Confirm Fit',
|
||||
subtitle: 'Make sure the device is worn correctly',
|
||||
connected: 'Connected',
|
||||
disconnected: 'Not connected',
|
||||
battery: 'Battery',
|
||||
guideTitle: 'How to Wear',
|
||||
guideImg: 'Wearing diagram',
|
||||
step1: '1. Take out the device and unfold the strap',
|
||||
step2: '2. Put the device on correctly',
|
||||
step3: '3. Adjust the strap to a comfortable fit',
|
||||
confirmBtn: 'Confirm Fit & Continue',
|
||||
reconnect: 'Reconnect',
|
||||
connecting: 'Connecting...',
|
||||
connectSuccess: 'Connected',
|
||||
connectFailed: 'Connection failed',
|
||||
searchTimeout: 'Search timed out'
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
var ble = require('../../services/ble')
|
||||
var api = require('../../utils/api')
|
||||
var diagnosis = require('../../services/diagnosis')
|
||||
var i18n = require('../../i18n/index')
|
||||
var SCAN_TIMEOUT = 180000
|
||||
|
||||
Page({
|
||||
@@ -29,6 +32,10 @@ Page({
|
||||
})
|
||||
},
|
||||
|
||||
onShow: function () {
|
||||
i18n.bind(this)
|
||||
},
|
||||
|
||||
_cleanup: function () {
|
||||
if (this._timeoutTimer) { clearTimeout(this._timeoutTimer); this._timeoutTimer = null }
|
||||
if (this._onAdc) { ble.off('adc', this._onAdc); this._onAdc = null }
|
||||
@@ -40,7 +47,7 @@ Page({
|
||||
var self = this
|
||||
|
||||
if (!ble.isConnected()) {
|
||||
self.setData({ error: '设备未连接' })
|
||||
self.setData({ error: i18n.t('common.deviceNotConnected') })
|
||||
return
|
||||
}
|
||||
|
||||
@@ -88,7 +95,7 @@ Page({
|
||||
console.log('[SCAN] 未贴合,检测失败')
|
||||
self._completed = true
|
||||
self._cleanup()
|
||||
self.setData({ phase: 'ready', error: '面膜未佩戴好,请重新佩戴后再试' })
|
||||
self.setData({ phase: 'ready', error: i18n.t('autoScan.notWorn') })
|
||||
return
|
||||
}
|
||||
|
||||
@@ -127,7 +134,7 @@ Page({
|
||||
}).catch(function (err) {
|
||||
console.error('[SCAN] 检测命令发送失败:', err)
|
||||
self._cleanup()
|
||||
self.setData({ phase: 'ready', error: '检测命令发送失败' })
|
||||
self.setData({ phase: 'ready', error: i18n.t('autoScan.cmdFailed') })
|
||||
})
|
||||
|
||||
self._timeoutTimer = setTimeout(function () {
|
||||
@@ -138,7 +145,7 @@ Page({
|
||||
} else {
|
||||
self._completed = true
|
||||
self._cleanup()
|
||||
self.setData({ phase: 'ready', error: '检测超时,未收到数据,请重试' })
|
||||
self.setData({ phase: 'ready', error: i18n.t('autoScan.timeout') })
|
||||
}
|
||||
}, SCAN_TIMEOUT)
|
||||
},
|
||||
@@ -158,39 +165,50 @@ Page({
|
||||
getApp().globalData.lastScanPdAvg = avg
|
||||
console.log('[SCAN] PD平均值:', avg, '帧数:', count)
|
||||
|
||||
this._startTreatment()
|
||||
this._buildAndGoReport(avg)
|
||||
},
|
||||
|
||||
_startTreatment: function () {
|
||||
// Scan finished -> run the (placeholder) diagnosis, store + persist a report, and
|
||||
// navigate to the report page so the user sees their skin tendencies and chooses
|
||||
// care. This replaces the previous auto-start-treatment behavior.
|
||||
//
|
||||
// NOTE: today only single-wavelength red PD is collected, and the thresholds +
|
||||
// PD->region mapping in services/diagnosis.js are placeholders pending calibration.
|
||||
// Report + persistence work end to end regardless; the numbers get real after
|
||||
// 4-wavelength scan collection and calibration land.
|
||||
_buildAndGoReport: function (avg) {
|
||||
var self = this
|
||||
var app = getApp()
|
||||
var mask = self.data.regions || 0x1F
|
||||
var avg = self.data.pdAvg
|
||||
var params = self._calculateTreatParams(mask, avg)
|
||||
// Same device-id source the treating page uses (services/ble getDeviceId + currentDevice fallback).
|
||||
var deviceId = ble.getDeviceId() || (app.globalData.currentDevice && app.globalData.currentDevice.device_id) || null
|
||||
var scannedAt = Date.now()
|
||||
|
||||
console.log('[SCAN] 自动发送治疗命令:', JSON.stringify(params))
|
||||
ble.setParams(params).then(function () {
|
||||
return ble.startTreatment(mask)
|
||||
}).then(function () {
|
||||
wx.redirectTo({
|
||||
url: '/pages/treating/treating?regions=' + mask +
|
||||
'&wavelength=2&duration=600000&mode=1'
|
||||
})
|
||||
}).catch(function (err) {
|
||||
self.setData({ error: err.error_msg || '启动治疗失败' })
|
||||
})
|
||||
},
|
||||
function proceed(config) {
|
||||
var scanData = diagnosis.fromRedScan(avg, mask, deviceId, scannedAt)
|
||||
var report = diagnosis.analyze(scanData, config || diagnosis.DEFAULT_CONFIG)
|
||||
app.globalData.lastScanReport = report
|
||||
|
||||
_calculateTreatParams: function (mask, pdAvg) {
|
||||
// TODO: 接入完整诊断算法(NR 归一化吸收率 → 匹配光谱+强度)
|
||||
// 需要 PD→区域映射确认后实现
|
||||
return {
|
||||
region_mask: mask,
|
||||
wavelength: 2,
|
||||
brightness: 200,
|
||||
duration_ms: 600000,
|
||||
mode: 1,
|
||||
control: 0x02
|
||||
// Best-effort persist (also seeds the calibration dataset via raw_pd); never blocks navigation.
|
||||
api.saveReport({
|
||||
device_id: report.device_id,
|
||||
scanned_at: report.scanned_at,
|
||||
regions: report.regions,
|
||||
overall: report.overall,
|
||||
recommend_mask: report.recommend_mask,
|
||||
recommend_plan: report.recommend_plan,
|
||||
raw_pd: report.raw_pd,
|
||||
calibrated: report.calibrated
|
||||
}).catch(function () {})
|
||||
|
||||
wx.redirectTo({ url: '/pages/scan-report/scan-report' })
|
||||
}
|
||||
|
||||
api.getDiagnosisConfig().then(function (cfg) {
|
||||
proceed(cfg && cfg.levels ? cfg : diagnosis.DEFAULT_CONFIG)
|
||||
}).catch(function () {
|
||||
proceed(diagnosis.DEFAULT_CONFIG)
|
||||
})
|
||||
},
|
||||
|
||||
onUnload: function () {
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
<view class="page">
|
||||
<view class="page-header page-header-pink" style="padding-top: {{statusBarHeight + 24}}px;">
|
||||
<view class="nav-back" bindtap="onBack">‹ 返回</view>
|
||||
<view class="page-header-title">智能检测</view>
|
||||
<view class="page-header-subtitle">{{phase === 'ready' ? '准备就绪' : phase === 'waiting_fit' ? '等待佩戴贴合...' : phase === 'collecting' ? '采集数据中 (' + frameCount + '帧)...' : '检测完成,正在启动...'}}</view>
|
||||
<view class="nav-back" bindtap="onBack">‹ {{i18n.common.back}}</view>
|
||||
<view class="page-header-title">{{i18n.autoScan.title}}</view>
|
||||
<view class="page-header-subtitle">{{phase === 'ready' ? i18n.autoScan.subReady : phase === 'waiting_fit' ? i18n.autoScan.subWaiting : phase === 'collecting' ? i18n.autoScan.subCollectingPre + frameCount + i18n.autoScan.subCollectingPost : i18n.autoScan.subDone}}</view>
|
||||
</view>
|
||||
|
||||
<view class="page-content">
|
||||
<!-- 待开始 -->
|
||||
<view wx:if="{{phase === 'ready'}}">
|
||||
<view class="scan-hint">设备已佩戴,点击下方按钮开始智能检测</view>
|
||||
<view class="scan-hint sub-hint">检测过程约需 2 分钟,请保持设备贴合</view>
|
||||
<button class="btn-primary mt-30" bindtap="onStartScan">开始检测</button>
|
||||
<view class="scan-hint">{{i18n.autoScan.readyHint}}</view>
|
||||
<button class="btn-primary mt-30" bindtap="onStartScan">{{i18n.autoScan.startBtn}}</button>
|
||||
</view>
|
||||
|
||||
<!-- 等待贴合 -->
|
||||
@@ -20,7 +19,7 @@
|
||||
<view class="scan-line"></view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="scan-status">正在检测面膜贴合状态,请确保设备正确佩戴...</view>
|
||||
<view class="scan-status">{{i18n.autoScan.waitingStatus}}</view>
|
||||
</view>
|
||||
|
||||
<!-- 采集中 -->
|
||||
@@ -30,12 +29,12 @@
|
||||
<view class="scan-line"></view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="scan-status">已贴合,正在采集光谱数据... 已收到 {{frameCount}} 帧</view>
|
||||
<view class="scan-status">{{i18n.autoScan.collectingStatusPre}}{{frameCount}}{{i18n.autoScan.collectingStatusPost}}</view>
|
||||
</view>
|
||||
|
||||
<!-- 完成,自动跳转 -->
|
||||
<view wx:if="{{phase === 'done'}}">
|
||||
<view class="scan-status">检测完成(共 {{frameCount}} 帧),正在启动治疗...</view>
|
||||
<view class="scan-status">{{i18n.autoScan.doneStatusPre}}{{frameCount}}{{i18n.autoScan.doneStatusPost}}</view>
|
||||
</view>
|
||||
|
||||
<view class="scan-error" wx:if="{{error}}">{{error}}</view>
|
||||
|
||||
@@ -1,8 +1,24 @@
|
||||
var i18n = require('../../i18n/index')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
statusBarHeight: 44,
|
||||
deviceId: '',
|
||||
trialDays: 7
|
||||
trialDays: 7,
|
||||
isFreeMode: false,
|
||||
tipTitle: ''
|
||||
},
|
||||
|
||||
onShow: function () {
|
||||
i18n.bind(this)
|
||||
},
|
||||
|
||||
updateTip: function () {
|
||||
this.setData({
|
||||
tipTitle: this.data.isFreeMode
|
||||
? i18n.t('bindSuccess.freeModeOpen')
|
||||
: i18n.t('bindSuccess.trialGranted', { days: this.data.trialDays })
|
||||
})
|
||||
},
|
||||
|
||||
onBack: function () {
|
||||
@@ -17,6 +33,15 @@ Page({
|
||||
var app = getApp()
|
||||
this.setData({ statusBarHeight: app.globalData.statusBarHeight })
|
||||
this.setData({ deviceId: options.device_id || '' })
|
||||
this.updateTip()
|
||||
var self = this
|
||||
var http = require('../../utils/request')
|
||||
http.get('/api/v1/subscription').then(function (sub) {
|
||||
if (sub && sub.plan === 'free') {
|
||||
self.setData({ isFreeMode: true })
|
||||
self.updateTip()
|
||||
}
|
||||
}).catch(function () {})
|
||||
},
|
||||
|
||||
onStart: function () {
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
<view class="page">
|
||||
<view class="page-header page-header-green" style="padding-top: {{statusBarHeight + 24}}px;">
|
||||
<view class="nav-back" bindtap="onBack">‹ 返回</view>
|
||||
<view class="page-header-title">绑定成功</view>
|
||||
<view class="page-header-subtitle">设备已成功绑定</view>
|
||||
<view class="nav-back" bindtap="onBack">‹ {{i18n.common.back}}</view>
|
||||
<view class="page-header-title">{{i18n.bindSuccess.title}}</view>
|
||||
<view class="page-header-subtitle">{{i18n.bindSuccess.subtitle}}</view>
|
||||
</view>
|
||||
|
||||
<view class="page-content">
|
||||
<view class="success-area">
|
||||
<view class="success-icon">✓</view>
|
||||
<view class="success-title">绑定成功!</view>
|
||||
<view class="success-title">{{i18n.bindSuccess.successTitle}}</view>
|
||||
</view>
|
||||
|
||||
<view class="tip-card">
|
||||
<text class="tip-card-icon">🎁</text>
|
||||
<view class="tip-card-body">
|
||||
<view class="tip-card-title">恭喜获得7天智能模式试用</view>
|
||||
<view class="tip-card-sub">体验结束后可订阅继续使用</view>
|
||||
<view class="tip-card-title">{{tipTitle}}</view>
|
||||
<view class="tip-card-sub" wx:if="{{!isFreeMode}}">{{i18n.bindSuccess.trialSub}}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<button class="btn-primary" bindtap="onStart">开始使用</button>
|
||||
<button class="btn-secondary" bindtap="onGoHome">返回首页</button>
|
||||
<button class="btn-primary" bindtap="onStart">{{i18n.bindSuccess.startUsing}}</button>
|
||||
<button class="btn-secondary" bindtap="onGoHome">{{i18n.bindSuccess.goHome}}</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
var ble = require('../../services/ble')
|
||||
var http = require('../../utils/request')
|
||||
var i18n = require('../../i18n/index')
|
||||
var app = getApp()
|
||||
var SCAN_TIMEOUT = 15000
|
||||
|
||||
@@ -12,6 +13,10 @@ Page({
|
||||
error: '',
|
||||
},
|
||||
|
||||
onShow: function () {
|
||||
i18n.bind(this)
|
||||
},
|
||||
|
||||
onBack: function () {
|
||||
wx.navigateBack({
|
||||
fail: function () {
|
||||
@@ -48,7 +53,7 @@ Page({
|
||||
|
||||
self._scanTimer = setTimeout(function () {
|
||||
ble.stopScan()
|
||||
self.setData({ state: 'error', error: '搜索超时,请确认设备已开机并在附近' })
|
||||
self.setData({ state: 'error', error: i18n.t('bleConnect.scanTimeout') })
|
||||
}, SCAN_TIMEOUT)
|
||||
|
||||
ble.startScan({
|
||||
@@ -64,7 +69,7 @@ Page({
|
||||
},
|
||||
onError: function (err) {
|
||||
clearTimeout(self._scanTimer)
|
||||
self.setData({ state: 'error', error: err.msg || '连接失败' })
|
||||
self.setData({ state: 'error', error: err.msg || i18n.t('bleConnect.failed') })
|
||||
}
|
||||
})
|
||||
},
|
||||
@@ -83,16 +88,16 @@ Page({
|
||||
url: '/pages/bind-success/bind-success?device_id=' + self.data.deviceId
|
||||
})
|
||||
}).catch(function (err) {
|
||||
self.setData({ state: 'error', error: err.message || '后台确认绑定失败' })
|
||||
self.setData({ state: 'error', error: err.message || i18n.t('bleConnect.confirmFailed') })
|
||||
})
|
||||
} else {
|
||||
self.setData({ state: 'error', error: '设备绑定失败' })
|
||||
self.setData({ state: 'error', error: i18n.t('bleConnect.bindFailed') })
|
||||
}
|
||||
}
|
||||
ble.on('bind_result', this._onBindResult)
|
||||
|
||||
ble.bindDevice(userId, self.data.bindToken).catch(function (err) {
|
||||
self.setData({ state: 'error', error: err.error_msg || '绑定命令失败' })
|
||||
self.setData({ state: 'error', error: err.error_msg || i18n.t('bleConnect.bindCmdFailed') })
|
||||
})
|
||||
},
|
||||
|
||||
|
||||
@@ -1,31 +1,31 @@
|
||||
<view class="page">
|
||||
<view class="page-header page-header-pink" style="padding-top: {{statusBarHeight + 24}}px;">
|
||||
<view class="nav-back" bindtap="onBack">‹ 返回</view>
|
||||
<view class="page-header-title">蓝牙连接</view>
|
||||
<view class="page-header-subtitle">{{state === 'scanning' ? '正在搜索设备...' : state === 'connecting' ? '正在连接设备...' : state === 'binding' ? '正在绑定...' : '连接失败'}}</view>
|
||||
<view class="nav-back" bindtap="onBack">‹ {{i18n.common.back}}</view>
|
||||
<view class="page-header-title">{{i18n.bleConnect.title}}</view>
|
||||
<view class="page-header-subtitle">{{state === 'scanning' ? i18n.bleConnect.searchingSub : state === 'connecting' ? i18n.bleConnect.connectingSub : state === 'binding' ? i18n.bleConnect.bindingSub : i18n.bleConnect.failedSub}}</view>
|
||||
</view>
|
||||
|
||||
<view class="page-content">
|
||||
<view class="info-tip">
|
||||
<text>📡</text>
|
||||
<text>请确保手机蓝牙已开启</text>
|
||||
<text>{{i18n.bleConnect.bluetoothTip}}</text>
|
||||
</view>
|
||||
|
||||
<view class="search-card" wx:if="{{state === 'scanning' || state === 'connecting'}}">
|
||||
<view class="search-icon pulsing">⏳</view>
|
||||
<view class="search-text">搜索中...</view>
|
||||
<view class="search-text">{{i18n.bleConnect.searching}}</view>
|
||||
</view>
|
||||
|
||||
<view class="found-card" wx:if="{{state === 'binding'}}">
|
||||
<view class="found-icon">💆</view>
|
||||
<view class="found-name">LumiFlow-{{deviceId}}</view>
|
||||
<text class="status-badge status-badge-green">正在配对...</text>
|
||||
<text class="status-badge status-badge-green">{{i18n.bleConnect.pairing}}</text>
|
||||
</view>
|
||||
|
||||
<view class="found-card" wx:if="{{state === 'error'}}">
|
||||
<view class="search-icon">⚠️</view>
|
||||
<view class="found-name">{{error || '连接失败'}}</view>
|
||||
<button class="retry-btn" bindtap="onRetry">重试</button>
|
||||
<view class="found-name">{{error || i18n.bleConnect.failed}}</view>
|
||||
<button class="retry-btn" bindtap="onRetry">{{i18n.common.retry}}</button>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
var i18n = require('../../i18n/index')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
statusBarHeight: 44
|
||||
},
|
||||
|
||||
onShow: function () {
|
||||
i18n.bind(this)
|
||||
},
|
||||
|
||||
onLoad: function () {
|
||||
var app = getApp()
|
||||
this.setData({ statusBarHeight: app.globalData.statusBarHeight })
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<view class="page">
|
||||
<view class="page-header page-header-pink" style="padding-top: {{statusBarHeight + 24}}px;">
|
||||
<view class="nav-back" bindtap="onBack">‹ 返回</view>
|
||||
<view class="page-header-title">联系我们</view>
|
||||
<view class="nav-back" bindtap="onBack">‹ {{i18n.common.back}}</view>
|
||||
<view class="page-header-title">{{i18n.contact.title}}</view>
|
||||
</view>
|
||||
<view class="page-content">
|
||||
<view class="placeholder-container">
|
||||
<view class="placeholder-icon">📬</view>
|
||||
<view class="placeholder-text">内容建设中</view>
|
||||
<view class="placeholder-sub">联系方式正在整理,敬请期待</view>
|
||||
<view class="placeholder-text">{{i18n.contact.placeholderText}}</view>
|
||||
<view class="placeholder-sub">{{i18n.contact.placeholderSub}}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
var i18n = require('../../i18n/index')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
statusBarHeight: 44
|
||||
},
|
||||
|
||||
onShow: function () {
|
||||
i18n.bind(this)
|
||||
},
|
||||
|
||||
onLoad: function () {
|
||||
var app = getApp()
|
||||
this.setData({ statusBarHeight: app.globalData.statusBarHeight })
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<view class="page">
|
||||
<view class="page-header page-header-pink" style="padding-top: {{statusBarHeight + 24}}px;">
|
||||
<view class="nav-back" bindtap="onBack">‹ 返回</view>
|
||||
<view class="page-header-title">使用帮助</view>
|
||||
<view class="nav-back" bindtap="onBack">‹ {{i18n.common.back}}</view>
|
||||
<view class="page-header-title">{{i18n.help.title}}</view>
|
||||
</view>
|
||||
<view class="page-content">
|
||||
<view class="placeholder-container">
|
||||
<view class="placeholder-icon">📖</view>
|
||||
<view class="placeholder-text">内容建设中</view>
|
||||
<view class="placeholder-sub">帮助文档正在编写,敬请期待</view>
|
||||
<view class="placeholder-text">{{i18n.help.placeholderText}}</view>
|
||||
<view class="placeholder-sub">{{i18n.help.placeholderSub}}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
var http = require('../../utils/request')
|
||||
var ble = require('../../services/ble')
|
||||
var i18n = require('../../i18n/index')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
@@ -15,6 +16,7 @@ Page({
|
||||
},
|
||||
|
||||
onShow: function () {
|
||||
i18n.bind(this)
|
||||
this.loadRecords(true)
|
||||
},
|
||||
|
||||
@@ -52,7 +54,7 @@ Page({
|
||||
var records = (data.records || []).map(function (r) {
|
||||
var durationMin = Math.floor((r.total_duration_ms || 0) / 60000)
|
||||
pageMs += (r.total_duration_ms || 0)
|
||||
r.duration_text = durationMin + '分钟'
|
||||
r.duration_text = i18n.t('history.durationMin', { min: durationMin })
|
||||
r.region_names = ble.getRegionName(r.regions || 0).join('、')
|
||||
r.wavelength_name = ble.getWavelengthName(r.wavelength || 2)
|
||||
r.date_text = self.formatDate(r.start_time || r.created_at)
|
||||
@@ -89,8 +91,8 @@ Page({
|
||||
var h = ('0' + d.getHours()).slice(-2)
|
||||
var m = ('0' + d.getMinutes()).slice(-2)
|
||||
|
||||
if (d >= today) return '今天 ' + h + ':' + m
|
||||
if (d >= yesterday) return '昨天 ' + h + ':' + m
|
||||
return (d.getMonth() + 1) + '月' + d.getDate() + '日 ' + h + ':' + m
|
||||
if (d >= today) return i18n.t('history.today') + ' ' + h + ':' + m
|
||||
if (d >= yesterday) return i18n.t('history.yesterday') + ' ' + h + ':' + m
|
||||
return i18n.t('history.dateMd', { m: d.getMonth() + 1, d: d.getDate() }) + ' ' + h + ':' + m
|
||||
}
|
||||
})
|
||||
|
||||
@@ -3,31 +3,31 @@
|
||||
<view class="stats-row">
|
||||
<view class="stat-card">
|
||||
<view class="stat-value">{{total}}</view>
|
||||
<view class="stat-label">累计次数</view>
|
||||
<view class="stat-label">{{i18n.history.statTotal}}</view>
|
||||
</view>
|
||||
<view class="stat-card">
|
||||
<view class="stat-value">{{totalHours}}h</view>
|
||||
<view class="stat-label">累计时长</view>
|
||||
<view class="stat-label">{{i18n.history.statHours}}</view>
|
||||
</view>
|
||||
<view class="stat-card">
|
||||
<view class="stat-value">{{monthCount}}</view>
|
||||
<view class="stat-label">本月次数</view>
|
||||
<view class="stat-label">{{i18n.history.statMonth}}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section-title">最近记录</view>
|
||||
<view class="section-title">{{i18n.history.recentTitle}}</view>
|
||||
|
||||
<view class="record-item" wx:for="{{records}}" wx:key="session_id">
|
||||
<view class="record-date">{{item.date_text}}</view>
|
||||
<text class="record-mode {{item.mode === 1 ? 'smart' : 'normal'}}">{{item.mode === 1 ? '✨ 智能' : '🔄 普通'}}</text>
|
||||
<text class="record-mode {{item.mode === 1 ? 'smart' : 'normal'}}">{{item.mode === 1 ? i18n.history.smart : i18n.history.normal}}</text>
|
||||
<view class="record-details">{{item.duration_text}}</view>
|
||||
</view>
|
||||
|
||||
<view class="empty-state" wx:if="{{!loading && records.length === 0}}">
|
||||
<text class="empty-icon">📃</text>
|
||||
<view class="empty-text">暂无使用记录</view>
|
||||
<view class="empty-text">{{i18n.history.empty}}</view>
|
||||
</view>
|
||||
|
||||
<view class="loading-more" wx:if="{{loadingMore}}">加载更多...</view>
|
||||
<view class="loading-more" wx:if="{{loadingMore}}">{{i18n.history.loadingMore}}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
var ble = require('../../services/ble')
|
||||
var api = require('../../utils/api')
|
||||
var i18n = require('../../i18n/index')
|
||||
var app = getApp()
|
||||
|
||||
Page({
|
||||
@@ -17,6 +18,7 @@ Page({
|
||||
},
|
||||
|
||||
onShow: function () {
|
||||
i18n.bind(this)
|
||||
this.checkState()
|
||||
this._onStatus = this.onBleStatus.bind(this)
|
||||
this._onBattery = this.onBleBattery.bind(this)
|
||||
@@ -69,9 +71,11 @@ Page({
|
||||
})
|
||||
|
||||
var p2 = api.getSubscription().then(function (sub) {
|
||||
var remaining = sub.remaining_days || 0
|
||||
self.setData({
|
||||
subscription: sub,
|
||||
subRemaining: sub.remaining_days || 0
|
||||
subRemaining: remaining,
|
||||
subRemainText: i18n.t('home.smartRemain', { days: remaining })
|
||||
})
|
||||
}).catch(function (err) {
|
||||
console.error('getSubscription failed', err)
|
||||
@@ -111,7 +115,7 @@ Page({
|
||||
},
|
||||
onError: function (err) {
|
||||
self.setData({ connected: false, bleState: 'error' })
|
||||
wx.showToast({ title: err.msg || '连接失败', icon: 'none' })
|
||||
wx.showToast({ title: err.msg || i18n.t('home.connectFailed'), icon: 'none' })
|
||||
}
|
||||
})
|
||||
},
|
||||
@@ -123,12 +127,12 @@ Page({
|
||||
onManageDevice: function () {
|
||||
var self = this
|
||||
wx.showActionSheet({
|
||||
itemList: ['解绑当前设备'],
|
||||
itemList: [i18n.t('home.unbindAction')],
|
||||
success: function (res) {
|
||||
if (res.tapIndex === 0) {
|
||||
wx.showModal({
|
||||
title: '确认解绑',
|
||||
content: '解绑后将无法使用该设备,确定要解绑吗?',
|
||||
title: i18n.t('home.unbindConfirmTitle'),
|
||||
content: i18n.t('home.unbindConfirmText'),
|
||||
success: function (modalRes) {
|
||||
if (modalRes.confirm) {
|
||||
self.doUnbind()
|
||||
@@ -142,7 +146,7 @@ Page({
|
||||
|
||||
doUnbind: function () {
|
||||
var self = this
|
||||
wx.showLoading({ title: '解绑中...' })
|
||||
wx.showLoading({ title: i18n.t('home.unbindLoading') })
|
||||
ble.disconnect()
|
||||
api.unbindDevice().then(function () {
|
||||
wx.hideLoading()
|
||||
@@ -152,10 +156,10 @@ Page({
|
||||
deviceInfo: null,
|
||||
connected: false
|
||||
})
|
||||
wx.showToast({ title: '已解绑', icon: 'success' })
|
||||
wx.showToast({ title: i18n.t('home.unbindDone'), icon: 'success' })
|
||||
}).catch(function (err) {
|
||||
wx.hideLoading()
|
||||
wx.showToast({ title: err.message || '解绑失败', icon: 'none' })
|
||||
wx.showToast({ title: err.message || i18n.t('home.unbindFailed'), icon: 'none' })
|
||||
})
|
||||
},
|
||||
|
||||
|
||||
@@ -1,41 +1,47 @@
|
||||
<view class="page">
|
||||
<view class="loading-container" wx:if="{{loading}}">
|
||||
<view class="loading-spinner"></view>
|
||||
<text class="loading-text">加载中...</text>
|
||||
<text class="loading-text">{{i18n.common.loading}}</text>
|
||||
</view>
|
||||
|
||||
<view class="page-content" wx:if="{{!loading && !hasDevice}}">
|
||||
<view class="empty-device">
|
||||
<text class="empty-icon">📱</text>
|
||||
<view class="empty-text">还没有绑定设备</view>
|
||||
<button class="btn-primary mt-30" bindtap="onAddDevice">扫码添加设备</button>
|
||||
<view class="empty-text">{{i18n.home.noDevice}}</view>
|
||||
<button class="btn-primary mt-30" bindtap="onAddDevice">{{i18n.home.addDevice}}</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="page-content" wx:if="{{!loading && hasDevice}}">
|
||||
<view class="device-card">
|
||||
<view class="device-card-icon">💆</view>
|
||||
<view class="device-card-name">{{deviceName || '我的设备'}}</view>
|
||||
<view class="device-card-name">{{deviceName || i18n.home.myDevice}}</view>
|
||||
<view class="device-card-status">
|
||||
<text class="status-badge status-badge-green" wx:if="{{connected}}">已连接</text>
|
||||
<text class="status-badge" style="background:#ccc;" wx:else>未连接</text>
|
||||
<text class="status-badge status-badge-green" wx:if="{{connected}}">{{i18n.home.connected}}</text>
|
||||
<text class="status-badge" style="background:#ccc;" wx:else>{{i18n.home.disconnected}}</text>
|
||||
<text class="device-card-battery" wx:if="{{connected}}">🔋 {{battery}}%</text>
|
||||
</view>
|
||||
<button class="btn-reconnect" bindtap="onConnectBle" wx:if="{{!connected}}">重新连接</button>
|
||||
<button class="btn-reconnect" bindtap="onConnectBle" wx:if="{{!connected}}">{{i18n.home.reconnect}}</button>
|
||||
</view>
|
||||
|
||||
<view class="sub-info" bindtap="onViewSubscription">
|
||||
<view class="sub-info" bindtap="onViewSubscription" wx:if="{{!subscription || subscription.plan !== 'free'}}">
|
||||
<view class="sub-info-left">
|
||||
<text class="sub-info-icon">💳</text>
|
||||
<text wx:if="{{subscription && subscription.status === 'active'}}">智能模式 · 剩余{{subRemaining}}天</text>
|
||||
<text wx:else>未订阅智能模式</text>
|
||||
<text wx:if="{{subscription && subscription.status === 'active'}}">{{subRemainText}}</text>
|
||||
<text wx:else>{{i18n.home.notSubscribed}}</text>
|
||||
</view>
|
||||
<text class="sub-info-arrow">›</text>
|
||||
</view>
|
||||
<view class="sub-info" wx:if="{{subscription && subscription.plan === 'free'}}">
|
||||
<view class="sub-info-left">
|
||||
<text class="sub-info-icon">✨</text>
|
||||
<text>{{i18n.home.smartOpen}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="action-buttons">
|
||||
<button class="btn-primary" bindtap="onStartTreatment">开始使用</button>
|
||||
<button class="btn-secondary" bindtap="onManageDevice">设备管理</button>
|
||||
<button class="btn-primary" bindtap="onStartTreatment">{{i18n.home.start}}</button>
|
||||
<button class="btn-secondary" bindtap="onManageDevice">{{i18n.home.deviceManage}}</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
var i18n = require('../../i18n/index')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
statusBarHeight: 44,
|
||||
// Ordered list of lights; strings live in i18n under lightInfo.lights[key],
|
||||
// colors are presentation-only and stay in JS.
|
||||
lights: [
|
||||
{ key: 'red', color: '#E8503A' },
|
||||
{ key: 'uv', color: '#7C4DFF' },
|
||||
{ key: 'yellow', color: '#F5B841' },
|
||||
{ key: 'infrared', color: '#8B2E3C' }
|
||||
],
|
||||
// Parallel to i18n.lightInfo.modes (matched by index).
|
||||
modeColors: ['#E8503A', '#7C4DFF', '#F5B841', '#8B2E3C', '#F08AA8']
|
||||
},
|
||||
|
||||
onLoad: function () {
|
||||
var app = getApp()
|
||||
this.setData({ statusBarHeight: app.globalData.statusBarHeight })
|
||||
},
|
||||
|
||||
onShow: function () {
|
||||
i18n.bind(this)
|
||||
},
|
||||
|
||||
onBack: function () {
|
||||
wx.navigateBack({
|
||||
fail: function () {
|
||||
wx.switchTab({ url: '/pages/profile/profile' })
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": "光疗功效介绍"
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<view class="page">
|
||||
<view class="page-header page-header-gradient" style="padding-top: {{statusBarHeight + 24}}px;">
|
||||
<view class="nav-back nav-back-light" bindtap="onBack">‹ {{i18n.common.back}}</view>
|
||||
<view class="page-header-title">{{i18n.lightInfo.navTitle}}</view>
|
||||
<view class="page-header-subtitle">{{i18n.lightInfo.headerSubtitle}}</view>
|
||||
</view>
|
||||
|
||||
<view class="page-content">
|
||||
<!-- 四种光 -->
|
||||
<view class="section-title">{{i18n.lightInfo.lightsSectionTitle}}</view>
|
||||
<view class="light-card" wx:for="{{lights}}" wx:key="key">
|
||||
<view class="light-card-top">
|
||||
<view class="swatch" style="background: {{item.color}};"></view>
|
||||
<view class="light-head">
|
||||
<view class="light-name">{{i18n.lightInfo.lights[item.key].name}}</view>
|
||||
<view class="light-wave">{{i18n.lightInfo.lights[item.key].wavelength}}</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="light-effect" style="color: {{item.color}};">{{i18n.lightInfo.lights[item.key].effect}}</view>
|
||||
<view class="light-desc">{{i18n.lightInfo.lights[item.key].desc}}</view>
|
||||
<view class="light-suits">
|
||||
<text class="suits-label">{{i18n.lightInfo.suitsLabel}}</text>
|
||||
<text class="suits-text">{{i18n.lightInfo.lights[item.key].suits}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 护理模式 -->
|
||||
<view class="section-title section-title-gap">{{i18n.lightInfo.modesSectionTitle}}</view>
|
||||
<view class="mode-grid">
|
||||
<view class="mode-card" wx:for="{{i18n.lightInfo.modes}}" wx:for-item="mode" wx:key="name">
|
||||
<view class="mode-dot" style="background: {{modeColors[index]}};"></view>
|
||||
<view class="mode-name">{{mode.name}}</view>
|
||||
<view class="mode-desc">{{mode.desc}}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 免责声明 -->
|
||||
<view class="disclaimer">{{i18n.lightInfo.disclaimer}}</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,125 @@
|
||||
.section-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #333333;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.section-title-gap {
|
||||
margin-top: 44rpx;
|
||||
}
|
||||
|
||||
/* 光卡片 */
|
||||
.light-card {
|
||||
background: #ffffff;
|
||||
border-radius: 24rpx;
|
||||
padding: 32rpx;
|
||||
margin-bottom: 24rpx;
|
||||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.light-card-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.swatch {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.light-head {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6rpx;
|
||||
}
|
||||
|
||||
.light-name {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.light-wave {
|
||||
font-size: 24rpx;
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
.light-effect {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.light-desc {
|
||||
font-size: 26rpx;
|
||||
color: #666666;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.light-suits {
|
||||
font-size: 24rpx;
|
||||
line-height: 1.5;
|
||||
padding-top: 16rpx;
|
||||
border-top: 2rpx solid #f2f2f2;
|
||||
}
|
||||
|
||||
.suits-label {
|
||||
color: #999999;
|
||||
margin-right: 12rpx;
|
||||
}
|
||||
|
||||
.suits-text {
|
||||
color: #555555;
|
||||
}
|
||||
|
||||
/* 护理模式 */
|
||||
.mode-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.mode-card {
|
||||
width: calc(50% - 10rpx);
|
||||
box-sizing: border-box;
|
||||
background: #ffffff;
|
||||
border-radius: 20rpx;
|
||||
padding: 28rpx 24rpx;
|
||||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.mode-dot {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
border-radius: 50%;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.mode-name {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #333333;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.mode-desc {
|
||||
font-size: 24rpx;
|
||||
color: #888888;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* 免责声明 */
|
||||
.disclaimer {
|
||||
margin-top: 44rpx;
|
||||
font-size: 22rpx;
|
||||
color: #aaaaaa;
|
||||
line-height: 1.6;
|
||||
text-align: center;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
var app = getApp()
|
||||
var i18n = require('../../i18n/index')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
@@ -6,6 +7,10 @@ Page({
|
||||
loading: false
|
||||
},
|
||||
|
||||
onShow: function () {
|
||||
i18n.bind(this)
|
||||
},
|
||||
|
||||
onLoad: function () {
|
||||
this.setData({ statusBarHeight: app.globalData.statusBarHeight })
|
||||
},
|
||||
@@ -23,11 +28,11 @@ Page({
|
||||
}
|
||||
}).catch(function (err) {
|
||||
self.setData({ loading: false })
|
||||
wx.showToast({ title: err.message || '登录失败', icon: 'none' })
|
||||
wx.showToast({ title: err.message || i18n.t('login.loginFailed'), icon: 'none' })
|
||||
})
|
||||
},
|
||||
|
||||
onAgreement: function () {
|
||||
wx.showModal({ title: '提示', content: '用户协议和隐私政策内容建设中', showCancel: false })
|
||||
wx.showModal({ title: i18n.t('login.agreementTitle'), content: i18n.t('login.agreementContent'), showCancel: false })
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
<view class="page">
|
||||
<view class="page-header page-header-pink" style="padding-top: {{statusBarHeight + 24}}px;">
|
||||
<view class="page-header-title">LumiFlow</view>
|
||||
<view class="page-header-subtitle">微信登录</view>
|
||||
<view class="page-header-subtitle">{{i18n.login.wechatLogin}}</view>
|
||||
</view>
|
||||
|
||||
<view class="page-content">
|
||||
<view class="welcome-text">欢迎使用LumiFlow</view>
|
||||
<view class="welcome-text">{{i18n.login.welcome}}</view>
|
||||
|
||||
<view class="flower-area">
|
||||
<text class="flower">💎</text>
|
||||
<view class="desc">登录后即可使用全部功能</view>
|
||||
<view class="desc">{{i18n.login.desc}}</view>
|
||||
</view>
|
||||
|
||||
<button class="login-btn" loading="{{loading}}" bindtap="onLogin" disabled="{{loading}}">
|
||||
微信登录
|
||||
{{i18n.login.wechatLogin}}
|
||||
</button>
|
||||
|
||||
<view class="agreement" bindtap="onAgreement">登录即表示同意《用户协议》和《隐私政策》</view>
|
||||
<view class="agreement" bindtap="onAgreement">{{i18n.login.agreement}}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
var ble = require('../../services/ble')
|
||||
var i18n = require('../../i18n/index')
|
||||
|
||||
// Manual care page: user directly picks a care function (-> single light
|
||||
// wavelength), regions and duration, then starts — skipping the smart scan.
|
||||
//
|
||||
// Care function -> wavelength (protocol WAVELENGTH: IR=1, R=2, UV=3, Y=4).
|
||||
// SINGLE light per session: buildVendorCommand only lights one wavelength.
|
||||
// FUTURE EXTENSION: a composite/multi-light care mode would require the vendor
|
||||
// command builder to accept several wavelengths at once — not implemented here.
|
||||
Page({
|
||||
data: {
|
||||
statusBarHeight: 44,
|
||||
// fixed safe brightness for care sessions: 140 ≈ 55% (护理中档位).
|
||||
// Do NOT expose a brightness slider (safety); the hardware also enforces
|
||||
// its own current/thermal limits regardless of this value.
|
||||
brightness: 140,
|
||||
functions: [
|
||||
{ id: 'rejuvenate', wavelength: 2, wl: 'red', color: '#E8503A' },
|
||||
{ id: 'acne', wavelength: 3, wl: 'uv', color: '#7C4DFF' },
|
||||
{ id: 'eventone', wavelength: 4, wl: 'yellow', color: '#F5B841' },
|
||||
{ id: 'revitalize', wavelength: 1, wl: 'infrared', color: '#8B2E3C' }
|
||||
],
|
||||
selectedFunctionId: 'rejuvenate',
|
||||
selectedWavelength: 2,
|
||||
// region mask mapping identical to treatment-setup: 右=0x01 左=0x02 上=0x04 中=0x08 下=0x10
|
||||
regions: [
|
||||
{ key: 'right', mask: 0x01, checked: true },
|
||||
{ key: 'left', mask: 0x02, checked: true },
|
||||
{ key: 'top', mask: 0x04, checked: true },
|
||||
{ key: 'middle', mask: 0x08, checked: true },
|
||||
{ key: 'bottom', mask: 0x10, checked: true }
|
||||
],
|
||||
// safety: a single session must NOT exceed 10 minutes (hardware caps at 10).
|
||||
durations: [5, 10],
|
||||
selectedMinutes: 10,
|
||||
durationOptions: [],
|
||||
submitting: false,
|
||||
error: ''
|
||||
},
|
||||
|
||||
onLoad: function () {
|
||||
var app = getApp()
|
||||
if (app && app.globalData && app.globalData.statusBarHeight) {
|
||||
this.setData({ statusBarHeight: app.globalData.statusBarHeight })
|
||||
}
|
||||
},
|
||||
|
||||
onShow: function () {
|
||||
i18n.bind(this)
|
||||
this.refreshDurationOptions()
|
||||
},
|
||||
|
||||
// Duration labels carry a dynamic number, which WXML cannot substitute, so
|
||||
// build the final strings in JS. Re-run on every onShow to follow locale.
|
||||
refreshDurationOptions: function () {
|
||||
var options = this.data.durations.map(function (min) {
|
||||
return { min: min, label: i18n.t('manual.durationUnit', { min: min }) }
|
||||
})
|
||||
this.setData({ durationOptions: options })
|
||||
},
|
||||
|
||||
onBack: function () {
|
||||
wx.navigateBack({
|
||||
fail: function () {
|
||||
wx.reLaunch({ url: '/pages/index/index' })
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
onSelectFunction: function (e) {
|
||||
var id = e.currentTarget.dataset.id
|
||||
var funcs = this.data.functions
|
||||
var wavelength = this.data.selectedWavelength
|
||||
for (var i = 0; i < funcs.length; i++) {
|
||||
if (funcs[i].id === id) {
|
||||
wavelength = funcs[i].wavelength
|
||||
break
|
||||
}
|
||||
}
|
||||
this.setData({ selectedFunctionId: id, selectedWavelength: wavelength })
|
||||
},
|
||||
|
||||
onToggleRegion: function (e) {
|
||||
var idx = e.currentTarget.dataset.idx
|
||||
var regions = this.data.regions
|
||||
regions[idx].checked = !regions[idx].checked
|
||||
this.setData({ regions: regions })
|
||||
},
|
||||
|
||||
onSelectDuration: function (e) {
|
||||
var min = parseInt(e.currentTarget.dataset.min, 10)
|
||||
this.setData({ selectedMinutes: min })
|
||||
},
|
||||
|
||||
onStart: function () {
|
||||
var self = this
|
||||
|
||||
if (!ble.isConnected()) {
|
||||
self.setData({ error: i18n.t('common.deviceNotConnected') })
|
||||
return
|
||||
}
|
||||
|
||||
var mask = 0
|
||||
self.data.regions.forEach(function (r) {
|
||||
if (r.checked) mask |= r.mask
|
||||
})
|
||||
|
||||
if (mask === 0) {
|
||||
wx.showToast({ title: i18n.t('manual.noRegionSelected'), icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
var wavelength = self.data.selectedWavelength
|
||||
var durationMs = self.data.selectedMinutes * 60000
|
||||
|
||||
self.setData({ submitting: true, error: '' })
|
||||
|
||||
ble.setParams({
|
||||
region_mask: mask,
|
||||
wavelength: wavelength,
|
||||
brightness: self.data.brightness,
|
||||
duration_ms: durationMs,
|
||||
mode: 0,
|
||||
control: 0x02
|
||||
}).then(function () {
|
||||
return ble.startTreatment(mask)
|
||||
}).then(function () {
|
||||
self.setData({ submitting: false })
|
||||
wx.redirectTo({
|
||||
url: '/pages/treating/treating?regions=' + mask +
|
||||
'&wavelength=' + wavelength +
|
||||
'&duration=' + durationMs +
|
||||
'&mode=0'
|
||||
})
|
||||
}).catch(function (err) {
|
||||
self.setData({
|
||||
submitting: false,
|
||||
error: (err && err.error_msg) || i18n.t('manual.startFailed')
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": "手动护理"
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<view class="page">
|
||||
<view class="page-header page-header-pink" style="padding-top: {{statusBarHeight + 24}}px;">
|
||||
<view class="nav-back" bindtap="onBack">‹ {{i18n.common.back}}</view>
|
||||
<view class="page-header-title">{{i18n.manual.navTitle}}</view>
|
||||
<view class="page-header-subtitle">{{i18n.manual.navSubtitle}}</view>
|
||||
</view>
|
||||
|
||||
<view class="page-content">
|
||||
<view class="page-title">{{i18n.manual.functionTitle}}</view>
|
||||
|
||||
<view class="func-grid">
|
||||
<view
|
||||
wx:for="{{functions}}"
|
||||
wx:key="id"
|
||||
class="func-card {{selectedFunctionId === item.id ? 'active' : ''}}"
|
||||
style="{{selectedFunctionId === item.id ? 'border-color:' + item.color + ';' : ''}}"
|
||||
bindtap="onSelectFunction"
|
||||
data-id="{{item.id}}">
|
||||
<view class="func-dot" style="background: {{item.color}};"></view>
|
||||
<view class="func-name">{{i18n.manual.functions[item.id].name}}</view>
|
||||
<view class="func-wl" style="color: {{item.color}};">{{i18n.common.wavelengths[item.wl]}}</view>
|
||||
<view class="func-benefit">{{i18n.manual.functions[item.id].benefit}}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="page-title section-title">{{i18n.manual.regionTitle}}</view>
|
||||
<view class="region-grid">
|
||||
<view
|
||||
wx:for="{{regions}}"
|
||||
wx:key="key"
|
||||
class="region-chip {{item.checked ? 'active' : ''}}"
|
||||
bindtap="onToggleRegion"
|
||||
data-idx="{{index}}">
|
||||
{{i18n.common.regions[item.key]}}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="page-title section-title">{{i18n.manual.durationTitle}}</view>
|
||||
<view class="segmented">
|
||||
<view
|
||||
wx:for="{{durationOptions}}"
|
||||
wx:key="min"
|
||||
class="segment {{selectedMinutes === item.min ? 'active' : ''}}"
|
||||
bindtap="onSelectDuration"
|
||||
data-min="{{item.min}}">
|
||||
{{item.label}}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="safety-tip">{{i18n.manual.safetyTip}}</view>
|
||||
|
||||
<button class="btn-primary" bindtap="onStart" disabled="{{submitting}}" loading="{{submitting}}">
|
||||
{{submitting ? i18n.manual.starting : i18n.manual.start}}
|
||||
</button>
|
||||
|
||||
<view class="error-text" wx:if="{{error}}">{{error}}</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,125 @@
|
||||
.nav-back {
|
||||
position: absolute;
|
||||
left: 32rpx;
|
||||
font-size: 30rpx;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 28rpx;
|
||||
margin-top: 40rpx;
|
||||
}
|
||||
|
||||
/* care function cards */
|
||||
.func-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.func-card {
|
||||
width: calc(50% - 10rpx);
|
||||
box-sizing: border-box;
|
||||
padding: 28rpx 24rpx;
|
||||
background: #ffffff;
|
||||
border: 4rpx solid #e5e5e5;
|
||||
border-radius: 24rpx;
|
||||
}
|
||||
|
||||
.func-card.active {
|
||||
background: #fdf2f8;
|
||||
}
|
||||
|
||||
.func-dot {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
border-radius: 50%;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.func-name {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #333333;
|
||||
margin-bottom: 6rpx;
|
||||
}
|
||||
|
||||
.func-wl {
|
||||
font-size: 24rpx;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.func-benefit {
|
||||
font-size: 22rpx;
|
||||
color: #999999;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* region checkbox grid */
|
||||
.region-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 20rpx;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.region-chip {
|
||||
min-width: 140rpx;
|
||||
padding: 20rpx 0;
|
||||
text-align: center;
|
||||
border: 4rpx solid #e5e5e5;
|
||||
border-radius: 16rpx;
|
||||
font-size: 26rpx;
|
||||
color: #999999;
|
||||
flex: 1 1 26%;
|
||||
}
|
||||
|
||||
.region-chip.active {
|
||||
border-color: #E6508C;
|
||||
background: rgba(230, 80, 140, 0.12);
|
||||
color: #E6508C;
|
||||
}
|
||||
|
||||
/* duration segmented control */
|
||||
.segmented {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.segment {
|
||||
flex: 1;
|
||||
padding: 24rpx 0;
|
||||
text-align: center;
|
||||
border: 4rpx solid #e5e5e5;
|
||||
border-radius: 16rpx;
|
||||
font-size: 28rpx;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.segment.active {
|
||||
border-color: #E6508C;
|
||||
background: #fdf2f8;
|
||||
color: #E6508C;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.safety-tip {
|
||||
margin: 32rpx 0 24rpx;
|
||||
padding: 20rpx 24rpx;
|
||||
background: #fff7e6;
|
||||
border-radius: 16rpx;
|
||||
font-size: 24rpx;
|
||||
color: #ad6800;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.error-text {
|
||||
text-align: center;
|
||||
color: #ff4d4f;
|
||||
font-size: 26rpx;
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
.btn-primary::after {
|
||||
border: none;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
var api = require('../../utils/api')
|
||||
var config = require('../../config/env')
|
||||
var i18n = require('../../i18n/index')
|
||||
var app = getApp()
|
||||
|
||||
Page({
|
||||
@@ -16,9 +17,30 @@ Page({
|
||||
},
|
||||
|
||||
onShow: function () {
|
||||
i18n.bind(this)
|
||||
this.loadProfile()
|
||||
},
|
||||
|
||||
onLightInfo: function () {
|
||||
wx.navigateTo({ url: '/pages/light-info/light-info' })
|
||||
},
|
||||
|
||||
onSwitchLanguage: function () {
|
||||
var self = this
|
||||
var supported = i18n.SUPPORTED
|
||||
var names = supported.map(function (l) { return i18n.LOCALE_NAMES[l] || l })
|
||||
wx.showActionSheet({
|
||||
itemList: names,
|
||||
success: function (res) {
|
||||
var target = supported[res.tapIndex]
|
||||
if (target && target !== i18n.getLocale()) {
|
||||
i18n.setLocale(target)
|
||||
i18n.bind(self)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
loadProfile: function () {
|
||||
var self = this
|
||||
self.setData({ loading: true })
|
||||
@@ -33,9 +55,11 @@ Page({
|
||||
})
|
||||
|
||||
var p2 = api.getSubscription().then(function (sub) {
|
||||
var remaining = sub.remaining_days || 0
|
||||
self.setData({
|
||||
subscription: sub,
|
||||
subRemaining: sub.remaining_days || 0
|
||||
subRemaining: remaining,
|
||||
subRemainingText: i18n.t('profile.remainDays', { days: remaining })
|
||||
})
|
||||
}).catch(function (err) {
|
||||
console.error('getSubscription failed', err)
|
||||
@@ -50,19 +74,19 @@ Page({
|
||||
var self = this
|
||||
if (this.data.deviceCount > 0) {
|
||||
wx.showActionSheet({
|
||||
itemList: ['解绑当前设备'],
|
||||
itemList: [i18n.t('profile.unbindAction')],
|
||||
success: function (res) {
|
||||
if (res.tapIndex === 0) {
|
||||
wx.showModal({
|
||||
title: '确认解绑',
|
||||
content: '解绑后将无法使用该设备,确定要解绑吗?',
|
||||
title: i18n.t('profile.unbindConfirmTitle'),
|
||||
content: i18n.t('profile.unbindConfirmText'),
|
||||
success: function (modalRes) {
|
||||
if (modalRes.confirm) {
|
||||
api.unbindDevice().then(function () {
|
||||
wx.showToast({ title: '已解绑', icon: 'success' })
|
||||
wx.showToast({ title: i18n.t('profile.unbindDone'), icon: 'success' })
|
||||
self.loadProfile()
|
||||
}).catch(function (err) {
|
||||
wx.showToast({ title: err.message || '解绑失败', icon: 'none' })
|
||||
wx.showToast({ title: err.message || i18n.t('profile.unbindFailed'), icon: 'none' })
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -151,7 +175,7 @@ Page({
|
||||
var self = this
|
||||
var nickname = (self.data.editNickname || '').trim()
|
||||
if (!nickname) {
|
||||
wx.showToast({ title: '昵称不能为空', icon: 'none' })
|
||||
wx.showToast({ title: i18n.t('profile.nicknameRequired'), icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
@@ -170,18 +194,18 @@ Page({
|
||||
})
|
||||
}).then(function () {
|
||||
self.setData({ saving: false, editing: false })
|
||||
wx.showToast({ title: '保存成功', icon: 'success' })
|
||||
wx.showToast({ title: i18n.t('common.saveSuccess'), icon: 'success' })
|
||||
self.loadProfile()
|
||||
}).catch(function (err) {
|
||||
self.setData({ saving: false })
|
||||
wx.showToast({ title: err.message || '保存失败', icon: 'none' })
|
||||
wx.showToast({ title: err.message || i18n.t('common.saveFailed'), icon: 'none' })
|
||||
})
|
||||
},
|
||||
|
||||
onLogout: function () {
|
||||
wx.showModal({
|
||||
title: '退出登录',
|
||||
content: '确定要退出登录吗?',
|
||||
title: i18n.t('profile.logoutConfirmTitle'),
|
||||
content: i18n.t('profile.logoutConfirmText'),
|
||||
success: function (res) {
|
||||
if (res.confirm) {
|
||||
wx.removeStorageSync('token')
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<view class="page">
|
||||
<view class="loading-container" wx:if="{{loading}}">
|
||||
<view class="loading-spinner"></view>
|
||||
<text class="loading-text">加载中...</text>
|
||||
<text class="loading-text">{{i18n.common.loading}}</text>
|
||||
</view>
|
||||
|
||||
<view class="page-content" wx:if="{{!loading}}">
|
||||
@@ -9,78 +9,96 @@
|
||||
<image class="user-avatar-img" wx:if="{{userInfo.avatar}}" src="{{userInfo.avatar}}" mode="aspectFill"></image>
|
||||
<view class="user-avatar" wx:else>👤</view>
|
||||
<view class="user-info">
|
||||
<view class="user-name">{{userInfo.nickname || '未登录'}}</view>
|
||||
<view class="user-name">{{userInfo.nickname || i18n.profile.notLoggedIn}}</view>
|
||||
<view class="user-phone">{{userInfo.phone || ''}}</view>
|
||||
</view>
|
||||
<view class="edit-profile-btn" bindtap="onEditProfile">编辑资料</view>
|
||||
<view class="edit-profile-btn" bindtap="onEditProfile">{{i18n.profile.editProfile}}</view>
|
||||
</view>
|
||||
|
||||
<view class="sub-card" wx:if="{{subscription && subscription.status === 'active' && subRemaining > 0}}">
|
||||
<view class="sub-card" wx:if="{{subscription && subscription.plan === 'free'}}">
|
||||
<view class="sub-left">
|
||||
<view class="sub-name">✨ 智能模式</view>
|
||||
<view class="sub-remain">剩余 {{subRemaining}}天</view>
|
||||
<view class="sub-name">✨ {{i18n.profile.smartMode}}</view>
|
||||
<view class="sub-remain">{{i18n.profile.smartModeOpen}}</view>
|
||||
</view>
|
||||
<text class="status-badge status-badge-gold">使用中</text>
|
||||
<text class="status-badge status-badge-gold">{{i18n.profile.free}}</text>
|
||||
</view>
|
||||
<view class="sub-card" wx:elif="{{subscription && subscription.status === 'active' && subRemaining > 0}}">
|
||||
<view class="sub-left">
|
||||
<view class="sub-name">✨ {{i18n.profile.smartMode}}</view>
|
||||
<view class="sub-remain">{{subRemainingText}}</view>
|
||||
</view>
|
||||
<text class="status-badge status-badge-gold">{{i18n.profile.inUse}}</text>
|
||||
</view>
|
||||
<view class="sub-card sub-card-inactive" wx:else bindtap="onViewSubscription">
|
||||
<view class="sub-left">
|
||||
<view class="sub-name">智能模式</view>
|
||||
<view class="sub-remain">未订阅</view>
|
||||
<view class="sub-name">{{i18n.profile.smartMode}}</view>
|
||||
<view class="sub-remain">{{i18n.profile.notSubscribed}}</view>
|
||||
</view>
|
||||
<text class="status-badge" style="background:#eee;color:#999;">去订阅 ›</text>
|
||||
<text class="status-badge" style="background:#eee;color:#999;">{{i18n.profile.goSubscribe}}</text>
|
||||
</view>
|
||||
|
||||
<view class="menu-list">
|
||||
<view class="menu-item" bindtap="onManageDevice">
|
||||
<view class="menu-icon">📱</view>
|
||||
<view class="menu-text">我的设备</view>
|
||||
<text class="menu-device-status" wx:if="{{deviceCount > 0}}">已绑定</text>
|
||||
<text class="menu-device-status menu-device-unbound" wx:else>未绑定</text>
|
||||
<view class="menu-text">{{i18n.profile.menuDevice}}</view>
|
||||
<text class="menu-device-status" wx:if="{{deviceCount > 0}}">{{i18n.profile.bound}}</text>
|
||||
<text class="menu-device-status menu-device-unbound" wx:else>{{i18n.profile.unbound}}</text>
|
||||
<text class="menu-arrow">›</text>
|
||||
</view>
|
||||
<view class="menu-item" bindtap="onViewHistory">
|
||||
<view class="menu-icon">📋</view>
|
||||
<view class="menu-text">使用记录</view>
|
||||
<view class="menu-text">{{i18n.profile.menuHistory}}</view>
|
||||
<text class="menu-arrow">›</text>
|
||||
</view>
|
||||
<view class="menu-item" bindtap="onViewSubscription">
|
||||
<view class="menu-item" bindtap="onViewSubscription" wx:if="{{!subscription || subscription.plan !== 'free'}}">
|
||||
<view class="menu-icon">💳</view>
|
||||
<view class="menu-text">订阅管理</view>
|
||||
<view class="menu-text">{{i18n.profile.menuSubscription}}</view>
|
||||
<text class="menu-arrow">›</text>
|
||||
</view>
|
||||
<view class="menu-item" bindtap="onLightInfo">
|
||||
<view class="menu-icon">💡</view>
|
||||
<view class="menu-text">{{i18n.profile.menuLightInfo}}</view>
|
||||
<text class="menu-arrow">›</text>
|
||||
</view>
|
||||
<view class="menu-item" bindtap="onHelp">
|
||||
<view class="menu-icon">❓</view>
|
||||
<view class="menu-text">使用帮助</view>
|
||||
<view class="menu-text">{{i18n.profile.menuHelp}}</view>
|
||||
<text class="menu-arrow">›</text>
|
||||
</view>
|
||||
<view class="menu-item" bindtap="onContact">
|
||||
<view class="menu-icon">📞</view>
|
||||
<view class="menu-text">联系我们</view>
|
||||
<view class="menu-text">{{i18n.profile.menuContact}}</view>
|
||||
<text class="menu-arrow">›</text>
|
||||
</view>
|
||||
<view class="menu-item" bindtap="onSwitchLanguage">
|
||||
<view class="menu-icon">🌐</view>
|
||||
<view class="menu-text">{{i18n.profile.language}}</view>
|
||||
<text class="menu-device-status">{{locale === 'en' ? 'English' : '中文'}}</text>
|
||||
<text class="menu-arrow">›</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="logout-btn" bindtap="onLogout">退出登录</view>
|
||||
<view class="logout-btn" bindtap="onLogout">{{i18n.profile.logout}}</view>
|
||||
|
||||
<view class="version-text">v0.1.8</view>
|
||||
<view class="version-text">v0.2.0</view>
|
||||
</view>
|
||||
|
||||
<!-- Edit profile modal -->
|
||||
<view class="edit-mask" wx:if="{{editing}}" bindtap="onCancelEdit"></view>
|
||||
<view class="edit-modal" wx:if="{{editing}}">
|
||||
<view class="edit-modal-title">编辑资料</view>
|
||||
<view class="edit-modal-title">{{i18n.profile.editProfileTitle}}</view>
|
||||
<view class="edit-avatar-row" bindtap="onPickAvatar">
|
||||
<image class="edit-avatar-img" wx:if="{{editAvatarUrl}}" src="{{editAvatarUrl}}" mode="aspectFill"></image>
|
||||
<view class="edit-avatar-placeholder" wx:else>👤</view>
|
||||
<text class="edit-avatar-hint">点击更换头像</text>
|
||||
<text class="edit-avatar-hint">{{i18n.profile.changeAvatar}}</text>
|
||||
</view>
|
||||
<view class="edit-field">
|
||||
<text class="edit-label">昵称</text>
|
||||
<input class="edit-input" value="{{editNickname}}" bindinput="onEditNicknameInput" placeholder="请输入昵称" maxlength="20" />
|
||||
<text class="edit-label">{{i18n.profile.nickname}}</text>
|
||||
<input class="edit-input" value="{{editNickname}}" bindinput="onEditNicknameInput" placeholder="{{i18n.profile.nicknamePlaceholder}}" maxlength="20" />
|
||||
</view>
|
||||
<view class="edit-actions">
|
||||
<button class="btn-secondary edit-btn" bindtap="onCancelEdit">取消</button>
|
||||
<button class="btn-primary edit-btn" bindtap="onSaveProfile" disabled="{{saving}}" loading="{{saving}}">保存</button>
|
||||
<button class="btn-secondary edit-btn" bindtap="onCancelEdit">{{i18n.common.cancel}}</button>
|
||||
<button class="btn-primary edit-btn" bindtap="onSaveProfile" disabled="{{saving}}" loading="{{saving}}">{{i18n.common.save}}</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
var app = getApp()
|
||||
var http = require('../../utils/request')
|
||||
var config = require('../../config/env')
|
||||
var i18n = require('../../i18n/index')
|
||||
|
||||
var DEFAULT_AVATAR = 'https://mmbiz.qpic.cn/mmbiz/icTdbqWNOwNRna42FI9t2NHfLvvMiaiaCJQn3KP2P0vAHp1QBP0piavhGfhKCARlGd6z1EbR58ibJXKCp5WVlNnojwA/640'
|
||||
|
||||
@@ -9,7 +10,7 @@ function randomNickname() {
|
||||
for (var i = 0; i < 4; i++) {
|
||||
digits += Math.floor(Math.random() * 10)
|
||||
}
|
||||
return '用户' + digits
|
||||
return i18n.t('register.nicknamePrefix') + digits
|
||||
}
|
||||
|
||||
Page({
|
||||
@@ -21,6 +22,10 @@ Page({
|
||||
phone: ''
|
||||
},
|
||||
|
||||
onShow: function () {
|
||||
i18n.bind(this)
|
||||
},
|
||||
|
||||
onLoad: function () {
|
||||
this.setData({
|
||||
statusBarHeight: app.globalData.statusBarHeight,
|
||||
@@ -58,19 +63,19 @@ Page({
|
||||
onGetPhoneNumber: function (e) {
|
||||
var self = this
|
||||
if (!e.detail || !e.detail.code) {
|
||||
wx.showToast({ title: '需要授权手机号才能完成注册', icon: 'none' })
|
||||
wx.showToast({ title: i18n.t('register.phoneRequired'), icon: 'none' })
|
||||
return
|
||||
}
|
||||
self.setData({ loading: true })
|
||||
http.post('/api/v1/user/phone', { code: e.detail.code }).then(function (data) {
|
||||
self.setData({
|
||||
loading: false,
|
||||
phone: '已授权'
|
||||
phone: i18n.t('register.authorized')
|
||||
})
|
||||
wx.showToast({ title: '手机号授权成功', icon: 'success' })
|
||||
wx.showToast({ title: i18n.t('register.phoneAuthSuccess'), icon: 'success' })
|
||||
}).catch(function (err) {
|
||||
self.setData({ loading: false })
|
||||
wx.showToast({ title: err.message || '手机号授权失败', icon: 'none' })
|
||||
wx.showToast({ title: err.message || i18n.t('register.phoneAuthFailed'), icon: 'none' })
|
||||
})
|
||||
},
|
||||
|
||||
@@ -88,13 +93,13 @@ Page({
|
||||
if (data.code === 0 && data.data && data.data.avatar) {
|
||||
resolve(data.data.avatar)
|
||||
} else {
|
||||
reject(new Error(data.message || '上传失败'))
|
||||
reject(new Error(data.message || i18n.t('register.uploadFailed')))
|
||||
}
|
||||
} catch (e) {
|
||||
reject(new Error('上传失败'))
|
||||
reject(new Error(i18n.t('register.uploadFailed')))
|
||||
}
|
||||
},
|
||||
fail: function () { reject(new Error('上传失败')) }
|
||||
fail: function () { reject(new Error(i18n.t('register.uploadFailed'))) }
|
||||
})
|
||||
})
|
||||
},
|
||||
@@ -105,12 +110,12 @@ Page({
|
||||
var avatarUrl = self.data.avatarUrl
|
||||
|
||||
if (!self.data.phone) {
|
||||
wx.showToast({ title: '请先授权手机号', icon: 'none' })
|
||||
wx.showToast({ title: i18n.t('register.phoneFirst'), icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
if (!nickname) {
|
||||
wx.showToast({ title: '请输入昵称', icon: 'none' })
|
||||
wx.showToast({ title: i18n.t('register.nicknameRequired'), icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
@@ -132,11 +137,11 @@ Page({
|
||||
wx.switchTab({ url: '/pages/index/index' })
|
||||
}).catch(function (err) {
|
||||
self.setData({ loading: false })
|
||||
wx.showToast({ title: err.message || '保存失败', icon: 'none' })
|
||||
wx.showToast({ title: err.message || i18n.t('common.saveFailed'), icon: 'none' })
|
||||
})
|
||||
},
|
||||
|
||||
onAgreement: function () {
|
||||
wx.showModal({ title: '提示', content: '用户协议和隐私政策内容建设中', showCancel: false })
|
||||
wx.showModal({ title: i18n.t('register.agreementTitle'), content: i18n.t('register.agreementContent'), showCancel: false })
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
<view class="page">
|
||||
<view class="page-header page-header-pink" style="padding-top: {{statusBarHeight + 24}}px;">
|
||||
<view class="page-header-title">LumiFlow</view>
|
||||
<view class="page-header-subtitle">完善个人资料</view>
|
||||
<view class="page-header-subtitle">{{i18n.register.subtitle}}</view>
|
||||
</view>
|
||||
|
||||
<view class="page-content">
|
||||
<view class="section-title">设置头像</view>
|
||||
<view class="section-title">{{i18n.register.avatarSection}}</view>
|
||||
<view class="avatar-area">
|
||||
<view class="avatar-wrap" bindtap="onPickAvatar">
|
||||
<image class="avatar-img" src="{{avatarUrl}}" mode="aspectFill"></image>
|
||||
<view class="avatar-edit-hint">点击更换</view>
|
||||
<view class="avatar-edit-hint">{{i18n.register.changeAvatar}}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section-title">设置昵称</view>
|
||||
<view class="section-title">{{i18n.register.nicknameSection}}</view>
|
||||
<view class="input-wrap">
|
||||
<input type="nickname" class="weui-input nickname-input" value="{{nickname}}" bindinput="onNicknameInput" bindblur="onNicknameBlur" placeholder="请输入昵称" />
|
||||
<input type="nickname" class="weui-input nickname-input" value="{{nickname}}" bindinput="onNicknameInput" bindblur="onNicknameBlur" placeholder="{{i18n.register.nicknamePlaceholder}}" />
|
||||
</view>
|
||||
|
||||
<view class="section-title">授权手机号</view>
|
||||
<view class="section-title">{{i18n.register.authorizePhone}}</view>
|
||||
<view class="phone-area">
|
||||
<block wx:if="{{phone}}">
|
||||
<view class="phone-done">
|
||||
@@ -28,16 +28,16 @@
|
||||
</block>
|
||||
<block wx:else>
|
||||
<button class="phone-btn" open-type="getPhoneNumber" bindgetphonenumber="onGetPhoneNumber" disabled="{{loading}}">
|
||||
授权手机号
|
||||
{{i18n.register.authorizePhone}}
|
||||
</button>
|
||||
<view class="phone-hint">手机号用于接收服务通知,必须授权</view>
|
||||
<view class="phone-hint">{{i18n.register.phoneHint}}</view>
|
||||
</block>
|
||||
</view>
|
||||
|
||||
<button class="submit-btn" bindtap="onSubmit" loading="{{loading}}" disabled="{{loading}}">
|
||||
完成注册
|
||||
{{i18n.register.submit}}
|
||||
</button>
|
||||
|
||||
<view class="agreement" bindtap="onAgreement">注册即表示同意《用户协议》和《隐私政策》</view>
|
||||
<view class="agreement" bindtap="onAgreement">{{i18n.register.agreement}}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
var api = require('../../utils/api')
|
||||
var ble = require('../../services/ble')
|
||||
var diagnosis = require('../../services/diagnosis')
|
||||
var i18n = require('../../i18n/index')
|
||||
|
||||
var WAVE_CODE_KEY = { 1: 'infrared', 2: 'red', 3: 'uv', 4: 'yellow' }
|
||||
|
||||
function pad2(n) { return (n < 10 ? '0' : '') + n }
|
||||
|
||||
function formatTime(ms) {
|
||||
if (!ms) ms = Date.now()
|
||||
var d = new Date(ms)
|
||||
return d.getFullYear() + '-' + pad2(d.getMonth() + 1) + '-' + pad2(d.getDate()) +
|
||||
' ' + pad2(d.getHours()) + ':' + pad2(d.getMinutes())
|
||||
}
|
||||
|
||||
Page({
|
||||
data: {
|
||||
statusBarHeight: 44,
|
||||
loading: true,
|
||||
calibrated: true,
|
||||
hasProblem: false,
|
||||
scannedAtText: '',
|
||||
overallList: [],
|
||||
regionList: [],
|
||||
recommendList: [],
|
||||
starting: false,
|
||||
error: ''
|
||||
},
|
||||
|
||||
onLoad: function (options) {
|
||||
var app = getApp()
|
||||
this.setData({ statusBarHeight: app.globalData.statusBarHeight })
|
||||
this._options = options || {}
|
||||
},
|
||||
|
||||
onShow: function () {
|
||||
i18n.bind(this)
|
||||
this._loadReport()
|
||||
},
|
||||
|
||||
onBack: function () {
|
||||
wx.navigateBack({ fail: function () { wx.reLaunch({ url: '/pages/index/index' }) } })
|
||||
},
|
||||
|
||||
_loadReport: function () {
|
||||
var self = this
|
||||
var app = getApp()
|
||||
var opts = self._options || {}
|
||||
|
||||
if (opts.report_id) {
|
||||
self.setData({ loading: true })
|
||||
api.getReport(opts.report_id).then(function (rep) {
|
||||
self._render(self._normalize(rep))
|
||||
}).catch(function () { self._fallback() })
|
||||
return
|
||||
}
|
||||
if (app.globalData.lastScanReport) {
|
||||
self._render(app.globalData.lastScanReport)
|
||||
return
|
||||
}
|
||||
self.setData({ loading: true })
|
||||
api.getLatestReport().then(function (rep) {
|
||||
// Backend ok() coerces null -> {}, so treat an empty payload as "no report".
|
||||
if (self._hasContent(rep)) self._render(self._normalize(rep))
|
||||
else self._fallback()
|
||||
}).catch(function () { self._fallback() })
|
||||
},
|
||||
|
||||
_hasContent: function (rep) {
|
||||
if (!rep) return false
|
||||
return !!((rep.regions && rep.regions.length) ||
|
||||
(rep.overall && rep.overall.length) ||
|
||||
(rep.recommend_plan && rep.recommend_plan.length))
|
||||
},
|
||||
|
||||
_fallback: function () {
|
||||
// Preview/demo when no device data is available yet.
|
||||
this._render(diagnosis.mockReport())
|
||||
},
|
||||
|
||||
_normalize: function (rep) {
|
||||
if (!rep) return diagnosis.mockReport()
|
||||
var scannedAt = 0
|
||||
if (typeof rep.scanned_at === 'number') scannedAt = rep.scanned_at
|
||||
else if (rep.scanned_at) scannedAt = Date.parse(rep.scanned_at) || 0
|
||||
return {
|
||||
device_id: rep.device_id || null,
|
||||
scanned_at: scannedAt,
|
||||
regions: rep.regions || [],
|
||||
overall: rep.overall || [],
|
||||
recommend_mask: rep.recommend_mask || 0,
|
||||
recommend_plan: rep.recommend_plan || [],
|
||||
raw_pd: rep.raw_pd || {},
|
||||
calibrated: !!rep.calibrated
|
||||
}
|
||||
},
|
||||
|
||||
_render: function (report) {
|
||||
var d = i18n.getDict().report
|
||||
var common = i18n.getDict().common
|
||||
|
||||
function levelLabel(l) {
|
||||
if (l >= 3) return d.level.high
|
||||
if (l === 2) return d.level.mid
|
||||
if (l === 1) return d.level.low
|
||||
return d.noProblem
|
||||
}
|
||||
|
||||
var overallList = (report.overall || []).map(function (o) {
|
||||
return { label: d.problems[o.type] || o.type, level: o.level, levelLabel: levelLabel(o.level) }
|
||||
})
|
||||
|
||||
var regionList = (report.regions || []).map(function (r) {
|
||||
return {
|
||||
name: d.regionNames[r.region] || r.region,
|
||||
level: r.level,
|
||||
levelLabel: levelLabel(r.level),
|
||||
problemLabel: r.top_problem ? (d.problems[r.top_problem] || r.top_problem) : d.noProblem,
|
||||
hasProblem: !!r.top_problem
|
||||
}
|
||||
})
|
||||
|
||||
var defs = diagnosis.REGION_DEFS
|
||||
var recommendList = (report.recommend_plan || []).map(function (p) {
|
||||
var names = []
|
||||
for (var i = 0; i < defs.length; i++) {
|
||||
if (p.mask & defs[i].mask) names.push(d.regionNames[defs[i].key] || defs[i].key)
|
||||
}
|
||||
var waveKey = WAVE_CODE_KEY[p.wavelength]
|
||||
return {
|
||||
label: d.problems[p.label_key] || '',
|
||||
wavelengthLabel: (waveKey && common.wavelengths[waveKey]) || '',
|
||||
regionsLabel: names.join('、'),
|
||||
wavelength: p.wavelength,
|
||||
mask: p.mask
|
||||
}
|
||||
})
|
||||
|
||||
this._report = report
|
||||
this.setData({
|
||||
loading: false,
|
||||
calibrated: !!report.calibrated,
|
||||
hasProblem: overallList.length > 0,
|
||||
scannedAtText: formatTime(report.scanned_at),
|
||||
overallList: overallList,
|
||||
regionList: regionList,
|
||||
recommendList: recommendList,
|
||||
error: ''
|
||||
})
|
||||
},
|
||||
|
||||
onStartRecommend: function () {
|
||||
var self = this
|
||||
var report = self._report || {}
|
||||
if (!ble.isConnected()) {
|
||||
self.setData({ error: i18n.t('common.deviceNotConnected') })
|
||||
return
|
||||
}
|
||||
var plan = (report.recommend_plan && report.recommend_plan[0]) || null
|
||||
var mask = plan ? plan.mask : (report.recommend_mask || 0x1F)
|
||||
var wl = plan ? plan.wavelength : 2
|
||||
if (!mask) mask = 0x1F
|
||||
|
||||
self.setData({ starting: true, error: '' })
|
||||
ble.setParams({
|
||||
region_mask: mask, wavelength: wl, brightness: 140,
|
||||
duration_ms: 600000, mode: 1, control: 0x02
|
||||
}).then(function () {
|
||||
return ble.startTreatment(mask)
|
||||
}).then(function () {
|
||||
wx.redirectTo({
|
||||
url: '/pages/treating/treating?regions=' + mask +
|
||||
'&wavelength=' + wl + '&duration=600000&mode=1'
|
||||
})
|
||||
}).catch(function (err) {
|
||||
self.setData({ starting: false, error: (err && err.error_msg) || i18n.t('common.networkError') })
|
||||
})
|
||||
},
|
||||
|
||||
onManual: function () {
|
||||
wx.redirectTo({ url: '/pages/manual-treatment/manual-treatment' })
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": "扫描报告"
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<view class="page">
|
||||
<view class="page-header page-header-gradient" style="padding-top:{{statusBarHeight + 8}}px;">
|
||||
<view class="nav-back nav-back-light" bindtap="onBack">‹ {{i18n.common.back}}</view>
|
||||
<view class="page-header-title">{{i18n.report.title}}</view>
|
||||
<view class="page-header-subtitle">{{i18n.report.subtitle}}</view>
|
||||
</view>
|
||||
|
||||
<view class="loading-container" wx:if="{{loading}}">
|
||||
<view class="loading-spinner"></view>
|
||||
<text class="loading-text">{{i18n.common.loading}}</text>
|
||||
</view>
|
||||
|
||||
<view class="page-content" wx:if="{{!loading}}">
|
||||
<view class="concept-banner" wx:if="{{!calibrated}}">
|
||||
<text class="concept-icon">ⓘ</text>
|
||||
<text class="concept-text">{{i18n.report.conceptNotice}}</text>
|
||||
</view>
|
||||
|
||||
<view class="scan-time">{{i18n.report.scannedAt}}:{{scannedAtText}}</view>
|
||||
|
||||
<!-- Overall -->
|
||||
<view class="card">
|
||||
<view class="card-title">{{i18n.report.overallTitle}}</view>
|
||||
<view class="chip-row" wx:if="{{hasProblem}}">
|
||||
<view class="chip chip-lv{{item.level}}" wx:for="{{overallList}}" wx:key="label">
|
||||
<text class="chip-name">{{item.label}}</text>
|
||||
<text class="chip-lv">{{item.levelLabel}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="empty-hint" wx:else>{{i18n.report.noProblem}}</view>
|
||||
</view>
|
||||
|
||||
<!-- Per-region -->
|
||||
<view class="card">
|
||||
<view class="card-title">{{i18n.report.regionTitle}}</view>
|
||||
<view class="region-row" wx:for="{{regionList}}" wx:key="name">
|
||||
<text class="region-name">{{item.name}}</text>
|
||||
<view class="region-right">
|
||||
<text class="region-problem {{item.hasProblem ? '' : 'region-problem-none'}}">{{item.problemLabel}}</text>
|
||||
<text class="region-lv region-lv{{item.level}}" wx:if="{{item.hasProblem}}">{{item.levelLabel}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Recommendation -->
|
||||
<view class="card" wx:if="{{recommendList.length > 0}}">
|
||||
<view class="card-title">{{i18n.report.recommendTitle}}</view>
|
||||
<view class="rec-row" wx:for="{{recommendList}}" wx:key="mask">
|
||||
<view class="rec-dot"></view>
|
||||
<view class="rec-body">
|
||||
<text class="rec-label">{{item.label}}</text>
|
||||
<text class="rec-meta">{{item.wavelengthLabel}} · {{item.regionsLabel}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="error-text" wx:if="{{error}}">{{error}}</view>
|
||||
|
||||
<view class="actions">
|
||||
<button class="btn-primary" bindtap="onStartRecommend" disabled="{{starting}}" loading="{{starting}}" wx:if="{{recommendList.length > 0}}">{{i18n.report.startTreatment}}</button>
|
||||
<button class="btn-secondary" bindtap="onManual">{{i18n.report.manualInstead}}</button>
|
||||
</view>
|
||||
|
||||
<view class="disclaimer">{{i18n.report.disclaimer}}</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,100 @@
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding-top: 120rpx;
|
||||
}
|
||||
.loading-spinner {
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
border: 6rpx solid #eee;
|
||||
border-top-color: #E8503A;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
.loading-text { margin-top: 20rpx; color: #999; font-size: 26rpx; }
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
|
||||
.concept-banner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #FFF7E6;
|
||||
border: 1rpx solid #FFE1A8;
|
||||
border-radius: 14rpx;
|
||||
padding: 18rpx 22rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
.concept-icon { color: #E8971A; margin-right: 12rpx; font-size: 30rpx; }
|
||||
.concept-text { color: #A66908; font-size: 24rpx; line-height: 1.4; flex: 1; }
|
||||
|
||||
.scan-time { color: #999; font-size: 24rpx; margin: 0 6rpx 20rpx; }
|
||||
|
||||
.card {
|
||||
background: #fff;
|
||||
border-radius: 18rpx;
|
||||
padding: 28rpx 26rpx;
|
||||
margin-bottom: 22rpx;
|
||||
box-shadow: 0 4rpx 18rpx rgba(0,0,0,0.04);
|
||||
}
|
||||
.card-title { font-size: 30rpx; font-weight: 600; color: #333; margin-bottom: 20rpx; }
|
||||
|
||||
.chip-row { display: flex; flex-wrap: wrap; }
|
||||
.chip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: 999rpx;
|
||||
padding: 10rpx 22rpx;
|
||||
margin: 0 16rpx 16rpx 0;
|
||||
background: #F3F4F6;
|
||||
}
|
||||
.chip-name { font-size: 26rpx; color: #333; }
|
||||
.chip-lv { font-size: 22rpx; color: #fff; margin-left: 12rpx; padding: 2rpx 14rpx; border-radius: 999rpx; background: #bbb; }
|
||||
.chip-lv1 .chip-lv { background: #6BBF59; }
|
||||
.chip-lv2 .chip-lv { background: #F5B841; }
|
||||
.chip-lv3 .chip-lv { background: #E8503A; }
|
||||
|
||||
.empty-hint { color: #999; font-size: 26rpx; }
|
||||
|
||||
.region-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 20rpx 0;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
.region-row:last-child { border-bottom: none; }
|
||||
.region-name { font-size: 28rpx; color: #333; }
|
||||
.region-right { display: flex; align-items: center; }
|
||||
.region-problem { font-size: 26rpx; color: #555; margin-right: 14rpx; }
|
||||
.region-problem-none { color: #aaa; }
|
||||
.region-lv { font-size: 22rpx; color: #fff; padding: 2rpx 14rpx; border-radius: 999rpx; background: #bbb; }
|
||||
.region-lv1 { background: #6BBF59; }
|
||||
.region-lv2 { background: #F5B841; }
|
||||
.region-lv3 { background: #E8503A; }
|
||||
|
||||
.rec-row { display: flex; align-items: flex-start; padding: 16rpx 0; }
|
||||
.rec-dot {
|
||||
width: 14rpx; height: 14rpx; border-radius: 50%;
|
||||
background: #E8503A; margin-top: 12rpx; margin-right: 18rpx; flex-shrink: 0;
|
||||
}
|
||||
.rec-body { display: flex; flex-direction: column; }
|
||||
.rec-label { font-size: 28rpx; color: #333; font-weight: 500; }
|
||||
.rec-meta { font-size: 24rpx; color: #999; margin-top: 6rpx; }
|
||||
|
||||
.error-text { color: #E8503A; font-size: 26rpx; text-align: center; margin: 10rpx 0; }
|
||||
|
||||
.actions { margin-top: 12rpx; }
|
||||
.actions .btn-primary, .actions .btn-secondary { margin-bottom: 20rpx; }
|
||||
|
||||
.disclaimer {
|
||||
color: #b0b0b0;
|
||||
font-size: 22rpx;
|
||||
line-height: 1.5;
|
||||
text-align: center;
|
||||
margin: 20rpx 10rpx 40rpx;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
var http = require('../../utils/request')
|
||||
var i18n = require('../../i18n/index')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
@@ -8,6 +9,10 @@ Page({
|
||||
error: ''
|
||||
},
|
||||
|
||||
onShow: function () {
|
||||
i18n.bind(this)
|
||||
},
|
||||
|
||||
onBack: function () {
|
||||
wx.navigateBack({
|
||||
fail: function () {
|
||||
@@ -31,7 +36,7 @@ Page({
|
||||
success: function (res) {
|
||||
var deviceId = self.parseDeviceId(res.result)
|
||||
if (!deviceId) {
|
||||
self.setData({ scanning: false, error: '无效的设备二维码' })
|
||||
self.setData({ scanning: false, error: i18n.t('scan.invalidQr') })
|
||||
return
|
||||
}
|
||||
self.bindDevice(deviceId)
|
||||
@@ -66,12 +71,12 @@ Page({
|
||||
}).catch(function (err) {
|
||||
self.setData({ scanning: false })
|
||||
if (err && err.code === 2001) {
|
||||
wx.showToast({ title: '已绑定设备', icon: 'none' })
|
||||
wx.showToast({ title: i18n.t('scan.alreadyBound'), icon: 'none' })
|
||||
setTimeout(function () {
|
||||
wx.navigateBack()
|
||||
}, 1500)
|
||||
} else {
|
||||
self.setData({ error: err.message || '绑定失败' })
|
||||
self.setData({ error: err.message || i18n.t('scan.bindFailed') })
|
||||
}
|
||||
})
|
||||
},
|
||||
@@ -79,16 +84,16 @@ Page({
|
||||
onManualInput: function () {
|
||||
var self = this
|
||||
wx.showModal({
|
||||
title: '手动输入设备号',
|
||||
title: i18n.t('scan.manualInput'),
|
||||
editable: true,
|
||||
placeholderText: '请输入设备ID,如 672B6D5A 4DCD861',
|
||||
placeholderText: i18n.t('scan.manualPlaceholder'),
|
||||
success: function (res) {
|
||||
if (res.confirm && res.content) {
|
||||
var deviceId = self.parseDeviceId(res.content)
|
||||
if (deviceId) {
|
||||
self.bindDevice(deviceId)
|
||||
} else {
|
||||
wx.showToast({ title: '设备号格式不正确', icon: 'none' })
|
||||
wx.showToast({ title: i18n.t('scan.invalidFormat'), icon: 'none' })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
<view class="page">
|
||||
<view class="page-header page-header-pink" style="padding-top: {{statusBarHeight + 24}}px;">
|
||||
<view class="nav-back" bindtap="onBack">‹ 返回</view>
|
||||
<view class="page-header-title">扫码绑定</view>
|
||||
<view class="page-header-subtitle">扫描设备底部二维码</view>
|
||||
<view class="nav-back" bindtap="onBack">‹ {{i18n.common.back}}</view>
|
||||
<view class="page-header-title">{{i18n.scan.title}}</view>
|
||||
<view class="page-header-subtitle">{{i18n.scan.subtitle}}</view>
|
||||
</view>
|
||||
|
||||
<view class="page-content">
|
||||
<view class="scan-area">
|
||||
<text class="scan-icon">📷</text>
|
||||
<text class="scan-text">扫描设备二维码</text>
|
||||
<text class="scan-text">{{i18n.scan.scanText}}</text>
|
||||
</view>
|
||||
|
||||
<button class="btn-primary" bindtap="onScan" disabled="{{scanning}}">
|
||||
{{scanning ? '扫描中...' : '扫码'}}
|
||||
{{scanning ? i18n.scan.scanning : i18n.scan.scan}}
|
||||
</button>
|
||||
<button class="btn-secondary" bindtap="onManualInput">手动输入设备号</button>
|
||||
<button class="btn-secondary" bindtap="onManualInput">{{i18n.scan.manualInput}}</button>
|
||||
|
||||
<view class="error-text" wx:if="{{error}}">{{error}}</view>
|
||||
</view>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
var api = require('../../utils/api')
|
||||
var i18n = require('../../i18n/index')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
@@ -8,6 +9,7 @@ Page({
|
||||
purchasing: false,
|
||||
subscription: null,
|
||||
subRemaining: 0,
|
||||
subRemainingText: '',
|
||||
loadingPlans: true
|
||||
},
|
||||
|
||||
@@ -25,10 +27,18 @@ Page({
|
||||
},
|
||||
|
||||
onShow: function () {
|
||||
i18n.bind(this)
|
||||
this.loadPlans()
|
||||
this.loadSubscription()
|
||||
},
|
||||
|
||||
planName: function (key) {
|
||||
if (key === 'trial') return i18n.t('subPlans.planTrial')
|
||||
if (key === 'monthly') return i18n.t('subPlans.planMonthly')
|
||||
if (key === 'yearly') return i18n.t('subPlans.planYearly')
|
||||
return key
|
||||
},
|
||||
|
||||
loadPlans: function () {
|
||||
var self = this
|
||||
self.setData({ loadingPlans: true })
|
||||
@@ -41,16 +51,16 @@ Page({
|
||||
var plans = serverPlans.map(function (p) {
|
||||
var daily = p.days > 0 ? (p.price / p.days).toFixed(1) : 0
|
||||
var desc = ''
|
||||
if (p.key === 'trial') desc = p.days + '天免费体验'
|
||||
else if (p.key === 'yearly') desc = '省¥' + Math.round(monthlyPrice * 12 - p.price)
|
||||
else desc = '约' + daily + '元/天'
|
||||
if (p.key === 'trial') desc = i18n.t('subPlans.descTrial', { days: p.days })
|
||||
else if (p.key === 'yearly') desc = i18n.t('subPlans.descSave', { amount: Math.round(monthlyPrice * 12 - p.price) })
|
||||
else desc = i18n.t('subPlans.descDaily', { price: daily })
|
||||
return {
|
||||
key: p.key,
|
||||
name: p.name,
|
||||
name: self.planName(p.key),
|
||||
price: p.price,
|
||||
priceLabel: p.key === 'trial' ? '免费' : '¥' + p.price,
|
||||
priceLabel: p.key === 'trial' ? i18n.t('subPlans.free') : '¥' + p.price,
|
||||
desc: desc,
|
||||
tag: p.key === 'yearly' ? '推荐' : '',
|
||||
tag: p.key === 'yearly' ? i18n.t('subPlans.tagRecommend') : '',
|
||||
disabled: false
|
||||
}
|
||||
})
|
||||
@@ -61,9 +71,9 @@ Page({
|
||||
self.setData({
|
||||
loadingPlans: false,
|
||||
plans: [
|
||||
{ key: 'trial', name: '试用', price: 0, priceLabel: '免费', desc: '7天免费体验', disabled: false },
|
||||
{ key: 'monthly', name: '月卡', price: 99, priceLabel: '¥99', desc: '约3.3元/天' },
|
||||
{ key: 'yearly', name: '年卡', price: 899, priceLabel: '¥899', desc: '省¥289', tag: '推荐' }
|
||||
{ key: 'trial', name: self.planName('trial'), price: 0, priceLabel: i18n.t('subPlans.free'), desc: i18n.t('subPlans.descTrial', { days: 7 }), disabled: false },
|
||||
{ key: 'monthly', name: self.planName('monthly'), price: 99, priceLabel: '¥99', desc: i18n.t('subPlans.descDaily', { price: '3.3' }) },
|
||||
{ key: 'yearly', name: self.planName('yearly'), price: 899, priceLabel: '¥899', desc: i18n.t('subPlans.descSave', { amount: 289 }), tag: i18n.t('subPlans.tagRecommend') }
|
||||
]
|
||||
})
|
||||
self.checkTrialUsed()
|
||||
@@ -75,7 +85,7 @@ Page({
|
||||
var sub = self.data.subscription
|
||||
if (sub && sub.trial_used) {
|
||||
var plans = self.data.plans.map(function (p) {
|
||||
if (p.key === 'trial') return Object.assign({}, p, { disabled: true, tag: '已使用' })
|
||||
if (p.key === 'trial') return Object.assign({}, p, { disabled: true, tag: i18n.t('subPlans.tagUsed') })
|
||||
return p
|
||||
})
|
||||
self.setData({ plans: plans })
|
||||
@@ -86,16 +96,18 @@ Page({
|
||||
loadSubscription: function () {
|
||||
var self = this
|
||||
api.getSubscription().then(function (sub) {
|
||||
var remaining = sub.remaining_days || 0
|
||||
self.setData({
|
||||
subscription: sub,
|
||||
subRemaining: sub.remaining_days || 0
|
||||
subRemaining: remaining,
|
||||
subRemainingText: i18n.t('subPlans.daysValue', { days: remaining })
|
||||
})
|
||||
self.checkTrialUsed()
|
||||
}).catch(function () {})
|
||||
},
|
||||
|
||||
onAgreement: function () {
|
||||
wx.showModal({ title: '提示', content: '订阅协议内容建设中', showCancel: false })
|
||||
wx.showModal({ title: i18n.t('subPlans.tipTitle'), content: i18n.t('subPlans.agreementBuilding'), showCancel: false })
|
||||
},
|
||||
|
||||
onSelect: function (e) {
|
||||
@@ -116,8 +128,8 @@ Page({
|
||||
|
||||
if (plan.key === 'trial') {
|
||||
wx.showModal({
|
||||
title: '激活试用',
|
||||
content: '免费试用 ' + plan.desc + '?',
|
||||
title: i18n.t('subPlans.activateTrialTitle'),
|
||||
content: i18n.t('subPlans.activateTrialConfirm', { desc: plan.desc }),
|
||||
success: function (res) {
|
||||
if (res.confirm) self.doActivateTrial()
|
||||
}
|
||||
@@ -127,10 +139,10 @@ Page({
|
||||
|
||||
var isRenew = self.data.subscription && self.data.subscription.status === 'active' && self.data.subRemaining > 0
|
||||
var planDays = plan.key === 'yearly' ? 365 : 30
|
||||
var title = isRenew ? '确认续费' : '确认支付'
|
||||
var title = isRenew ? i18n.t('subPlans.confirmRenewTitle') : i18n.t('subPlans.confirmPayTitle')
|
||||
var content = isRenew
|
||||
? '在现有订阅基础上延长' + planDays + '天,支付 ¥' + plan.price
|
||||
: '支付 ¥' + plan.price
|
||||
? i18n.t('subPlans.renewContent', { days: planDays, price: plan.price })
|
||||
: i18n.t('subPlans.payContent', { price: plan.price })
|
||||
|
||||
wx.showModal({
|
||||
title: title,
|
||||
@@ -148,13 +160,13 @@ Page({
|
||||
self.setData({ purchasing: true })
|
||||
api.activateTrial().then(function () {
|
||||
self.setData({ purchasing: false })
|
||||
wx.showToast({ title: '试用已激活', icon: 'success' })
|
||||
wx.showToast({ title: i18n.t('subPlans.trialActivated'), icon: 'success' })
|
||||
setTimeout(function () {
|
||||
wx.redirectTo({ url: '/pages/subscribe-success/subscribe-success?plan=trial' })
|
||||
}, 1000)
|
||||
}).catch(function (err) {
|
||||
self.setData({ purchasing: false })
|
||||
wx.showToast({ title: err.message || '激活失败', icon: 'none' })
|
||||
wx.showToast({ title: err.message || i18n.t('subPlans.activateFailed'), icon: 'none' })
|
||||
})
|
||||
},
|
||||
|
||||
@@ -164,7 +176,7 @@ Page({
|
||||
api.purchase(plan.key).then(function (data) {
|
||||
if (!data.payment_params) {
|
||||
self.setData({ purchasing: false })
|
||||
wx.showToast({ title: '支付服务暂不可用', icon: 'none' })
|
||||
wx.showToast({ title: i18n.t('subPlans.payUnavailable'), icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
@@ -182,13 +194,13 @@ Page({
|
||||
},
|
||||
fail: function (err) {
|
||||
self.setData({ purchasing: false })
|
||||
var msg = (err.errMsg || '').indexOf('cancel') > -1 ? '已取消支付' : '支付失败'
|
||||
var msg = (err.errMsg || '').indexOf('cancel') > -1 ? i18n.t('subPlans.payCancelled') : i18n.t('subPlans.payFailed')
|
||||
wx.showToast({ title: msg, icon: 'none' })
|
||||
}
|
||||
})
|
||||
}).catch(function (err) {
|
||||
self.setData({ purchasing: false })
|
||||
wx.showToast({ title: err.message || '创建订单失败', icon: 'none' })
|
||||
wx.showToast({ title: err.message || i18n.t('subPlans.createOrderFailed'), icon: 'none' })
|
||||
})
|
||||
},
|
||||
|
||||
@@ -201,7 +213,7 @@ Page({
|
||||
api.syncPaymentOrder(orderId).then(function (result) {
|
||||
if (result.status === 'paid') {
|
||||
self.setData({ purchasing: false })
|
||||
wx.showToast({ title: '支付成功', icon: 'success' })
|
||||
wx.showToast({ title: i18n.t('subPlans.paySuccess'), icon: 'success' })
|
||||
setTimeout(function () {
|
||||
wx.redirectTo({ url: '/pages/subscribe-success/subscribe-success?plan=' + planKey })
|
||||
}, 1000)
|
||||
@@ -210,7 +222,7 @@ Page({
|
||||
setTimeout(pollSync, 2000)
|
||||
} else {
|
||||
self.setData({ purchasing: false })
|
||||
wx.showToast({ title: '支付处理中,请稍后在订阅页查看', icon: 'none' })
|
||||
wx.showToast({ title: i18n.t('subPlans.payProcessing'), icon: 'none' })
|
||||
}
|
||||
}).catch(function () {
|
||||
if (retryCount < maxRetries) {
|
||||
@@ -218,7 +230,7 @@ Page({
|
||||
setTimeout(pollSync, 2000)
|
||||
} else {
|
||||
self.setData({ purchasing: false })
|
||||
wx.showToast({ title: '支付处理中,请稍后在订阅页查看', icon: 'none' })
|
||||
wx.showToast({ title: i18n.t('subPlans.payProcessing'), icon: 'none' })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,35 +1,35 @@
|
||||
<view class="page">
|
||||
<view class="page-header page-header-gold" style="padding-top: {{statusBarHeight + 24}}px;">
|
||||
<view class="nav-back" bindtap="onBack">‹ 返回</view>
|
||||
<view class="page-header-title">订阅服务</view>
|
||||
<view class="page-header-subtitle">解锁智能模式</view>
|
||||
<view class="nav-back" bindtap="onBack">‹ {{i18n.common.back}}</view>
|
||||
<view class="page-header-title">{{i18n.subPlans.title}}</view>
|
||||
<view class="page-header-subtitle">{{i18n.subPlans.subtitle}}</view>
|
||||
</view>
|
||||
|
||||
<view class="page-content">
|
||||
<!-- Current subscription status -->
|
||||
<view class="current-sub" wx:if="{{subscription && subscription.status === 'active' && subRemaining > 0}}">
|
||||
<view class="current-sub-row">
|
||||
<text class="current-sub-label">当前套餐</text>
|
||||
<text class="current-sub-value">{{subscription.plan === 'trial' ? '试用' : subscription.plan === 'monthly' ? '月卡' : subscription.plan === 'yearly' ? '年卡' : subscription.plan}}</text>
|
||||
<text class="current-sub-label">{{i18n.subPlans.currentPlanLabel}}</text>
|
||||
<text class="current-sub-value">{{subscription.plan === 'trial' ? i18n.subPlans.planTrial : subscription.plan === 'monthly' ? i18n.subPlans.planMonthly : subscription.plan === 'yearly' ? i18n.subPlans.planYearly : subscription.plan}}</text>
|
||||
</view>
|
||||
<view class="current-sub-row">
|
||||
<text class="current-sub-label">剩余天数</text>
|
||||
<text class="current-sub-value">{{subRemaining}}天</text>
|
||||
<text class="current-sub-label">{{i18n.subPlans.remainDaysLabel}}</text>
|
||||
<text class="current-sub-value">{{subRemainingText}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="page-title">选择订阅套餐</view>
|
||||
<view class="page-title">{{i18n.subPlans.choosePlan}}</view>
|
||||
|
||||
<view class="loading-plans" wx:if="{{loadingPlans}}">
|
||||
<view class="loading-spinner"></view>
|
||||
<text class="loading-text">加载套餐中...</text>
|
||||
<text class="loading-text">{{i18n.subPlans.loadingPlans}}</text>
|
||||
</view>
|
||||
|
||||
<view class="service-card" wx:if="{{!loadingPlans}}">
|
||||
<view class="service-icon">✨</view>
|
||||
<view class="service-info">
|
||||
<view class="service-name">智能模式</view>
|
||||
<view class="service-desc">智能自动调节</view>
|
||||
<view class="service-name">{{i18n.subPlans.smartMode}}</view>
|
||||
<view class="service-desc">{{i18n.subPlans.smartModeDesc}}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -44,9 +44,9 @@
|
||||
|
||||
<block wx:if="{{!loadingPlans}}">
|
||||
<button class="btn-primary btn-primary-gold" bindtap="onPurchase" disabled="{{purchasing}}" loading="{{purchasing}}">
|
||||
{{subscription && subscription.status === 'active' && subRemaining > 0 ? '续费' : '确认支付'}}
|
||||
{{subscription && subscription.status === 'active' && subRemaining > 0 ? i18n.subPlans.renew : i18n.subPlans.confirmPay}}
|
||||
</button>
|
||||
<view class="agreement" bindtap="onAgreement">支付即表示同意《订阅协议》</view>
|
||||
<view class="agreement" bindtap="onAgreement">{{i18n.subPlans.agreement}}</view>
|
||||
</block>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
var i18n = require('../../i18n/index')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
statusBarHeight: 44
|
||||
},
|
||||
|
||||
onShow: function () {
|
||||
i18n.bind(this)
|
||||
},
|
||||
|
||||
onLoad: function () {
|
||||
var app = getApp()
|
||||
this.setData({ statusBarHeight: app.globalData.statusBarHeight })
|
||||
|
||||
@@ -1,35 +1,35 @@
|
||||
<view class="page">
|
||||
<view class="page-header page-header-pink" style="padding-top: {{statusBarHeight + 24}}px;">
|
||||
<view class="nav-back" bindtap="onBack">‹ 返回</view>
|
||||
<view class="page-header-title">使用设置</view>
|
||||
<view class="page-header-subtitle">试用期已结束</view>
|
||||
<view class="nav-back" bindtap="onBack">‹ {{i18n.common.back}}</view>
|
||||
<view class="page-header-title">{{i18n.subPrompt.title}}</view>
|
||||
<view class="page-header-subtitle">{{i18n.subPrompt.subtitle}}</view>
|
||||
</view>
|
||||
|
||||
<view class="page-content">
|
||||
<view class="page-title">选择模式</view>
|
||||
<view class="page-title">{{i18n.subPrompt.selectMode}}</view>
|
||||
|
||||
<view class="mode-selector">
|
||||
<view class="mode-option active">
|
||||
<text class="mode-icon">🔄</text>
|
||||
<view class="mode-label">普通模式</view>
|
||||
<view class="mode-sub">6色循环</view>
|
||||
<view class="mode-label">{{i18n.subPrompt.normalMode}}</view>
|
||||
<view class="mode-sub">{{i18n.subPrompt.normalModeSub}}</view>
|
||||
</view>
|
||||
<view class="mode-option locked">
|
||||
<text class="mode-icon">🔒</text>
|
||||
<view class="mode-label">智能模式</view>
|
||||
<view class="mode-sub">订阅解锁</view>
|
||||
<view class="mode-label">{{i18n.subPrompt.smartMode}}</view>
|
||||
<view class="mode-sub">{{i18n.subPrompt.smartModeSub}}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="tip-card">
|
||||
<text class="tip-card-icon">⏰</text>
|
||||
<view class="tip-card-body">
|
||||
<view class="tip-card-title">试用已结束</view>
|
||||
<view class="tip-card-sub">订阅后继续使用智能模式</view>
|
||||
<view class="tip-card-title">{{i18n.subPrompt.trialEndedTitle}}</view>
|
||||
<view class="tip-card-sub">{{i18n.subPrompt.trialEndedSub}}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<button class="btn-primary btn-primary-gold" bindtap="onViewPlans">立即订阅</button>
|
||||
<button class="btn-secondary" bindtap="onUseFreeMode">先使用普通模式</button>
|
||||
<button class="btn-primary btn-primary-gold" bindtap="onViewPlans">{{i18n.subPrompt.subscribeNow}}</button>
|
||||
<button class="btn-secondary" bindtap="onUseFreeMode">{{i18n.subPrompt.useNormalFirst}}</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
var api = require('../../utils/api')
|
||||
var i18n = require('../../i18n/index')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
statusBarHeight: 44,
|
||||
planName: '年卡会员',
|
||||
planName: '',
|
||||
expiryDate: ''
|
||||
},
|
||||
|
||||
@@ -15,33 +16,56 @@ Page({
|
||||
})
|
||||
},
|
||||
|
||||
onShow: function () {
|
||||
i18n.bind(this)
|
||||
this.render()
|
||||
},
|
||||
|
||||
// Recompute locale-dependent display strings from stored raw values.
|
||||
render: function () {
|
||||
this.setData({
|
||||
planName: this.planNameFor(this._plan),
|
||||
expiryDate: this._expireTime ? this.formatDate(this._expireTime) : ''
|
||||
})
|
||||
},
|
||||
|
||||
planNameFor: function (plan) {
|
||||
if (plan === 'trial') return i18n.t('subSuccess.planTrial')
|
||||
if (plan === 'monthly') return i18n.t('subSuccess.planMonthly')
|
||||
if (plan === 'yearly') return i18n.t('subSuccess.planYearly')
|
||||
return i18n.t('subSuccess.planDefault')
|
||||
},
|
||||
|
||||
formatDate: function (raw) {
|
||||
var date = new Date(raw)
|
||||
var y = date.getFullYear()
|
||||
var m = ('0' + (date.getMonth() + 1)).slice(-2)
|
||||
var d = ('0' + date.getDate()).slice(-2)
|
||||
if (i18n.getLocale() === 'en') return y + '-' + m + '-' + d
|
||||
return y + '年' + m + '月' + d + '日'
|
||||
},
|
||||
|
||||
onLoad: function (options) {
|
||||
var self = this
|
||||
var app = getApp()
|
||||
self.setData({ statusBarHeight: app.globalData.statusBarHeight })
|
||||
|
||||
var planMap = { yearly: '年卡会员', monthly: '月卡会员', trial: '试用会员' }
|
||||
var plan = options.plan || 'yearly'
|
||||
self.setData({ planName: planMap[plan] || '会员' })
|
||||
self._plan = plan
|
||||
|
||||
// Fetch actual subscription expiry from server instead of computing client-side
|
||||
api.getSubscription().then(function (res) {
|
||||
if (res && res.expire_time) {
|
||||
var date = new Date(res.expire_time)
|
||||
var y = date.getFullYear()
|
||||
var m = ('0' + (date.getMonth() + 1)).slice(-2)
|
||||
var d = ('0' + date.getDate()).slice(-2)
|
||||
self.setData({ expiryDate: y + '年' + m + '月' + d + '日' })
|
||||
self._expireTime = res.expire_time
|
||||
}
|
||||
self.render()
|
||||
}).catch(function () {
|
||||
// Fallback: compute from current date if server call fails
|
||||
var durationMap = { yearly: 365, monthly: 30, trial: 7 }
|
||||
var now = new Date()
|
||||
now.setDate(now.getDate() + (durationMap[plan] || 365))
|
||||
var y = now.getFullYear()
|
||||
var m = ('0' + (now.getMonth() + 1)).slice(-2)
|
||||
var d = ('0' + now.getDate()).slice(-2)
|
||||
self.setData({ expiryDate: y + '年' + m + '月' + d + '日' })
|
||||
self._expireTime = now.getTime()
|
||||
self.render()
|
||||
})
|
||||
},
|
||||
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
<view class="page">
|
||||
<view class="page-header page-header-green" style="padding-top: {{statusBarHeight + 24}}px;">
|
||||
<view class="nav-back" bindtap="onBack">‹ 返回</view>
|
||||
<view class="page-header-title">订阅成功</view>
|
||||
<view class="page-header-subtitle">欢迎使用智能模式</view>
|
||||
<view class="nav-back" bindtap="onBack">‹ {{i18n.common.back}}</view>
|
||||
<view class="page-header-title">{{i18n.subSuccess.title}}</view>
|
||||
<view class="page-header-subtitle">{{i18n.subSuccess.subtitle}}</view>
|
||||
</view>
|
||||
|
||||
<view class="page-content">
|
||||
<view class="success-area">
|
||||
<view class="success-icon">✓</view>
|
||||
<view class="success-title">订阅成功!</view>
|
||||
<view class="success-title">{{i18n.subSuccess.successTitle}}</view>
|
||||
</view>
|
||||
|
||||
<view class="sub-card">
|
||||
<view class="sub-header">
|
||||
<text class="sub-name">✨ {{planName}}</text>
|
||||
<text class="sub-status status-badge status-badge-green">已订阅</text>
|
||||
<text class="sub-status status-badge status-badge-green">{{i18n.subSuccess.subscribed}}</text>
|
||||
</view>
|
||||
<view class="sub-info">有效期至:{{expiryDate}}</view>
|
||||
<view class="sub-info">{{i18n.subSuccess.validUntil}}{{expiryDate}}</view>
|
||||
</view>
|
||||
|
||||
<button class="btn-primary btn-primary-gold" bindtap="onStartSmart">立即体验智能模式</button>
|
||||
<button class="btn-secondary btn-secondary-gold" bindtap="onBackHome">返回首页</button>
|
||||
<button class="btn-primary btn-primary-gold" bindtap="onStartSmart">{{i18n.subSuccess.startSmart}}</button>
|
||||
<button class="btn-secondary btn-secondary-gold" bindtap="onBackHome">{{i18n.subSuccess.backHome}}</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
var ble = require('../../services/ble')
|
||||
var api = require('../../utils/api')
|
||||
var commandSync = require('../../services/command-sync')
|
||||
var i18n = require('../../i18n/index')
|
||||
Page({
|
||||
data: {
|
||||
statusBarHeight: 44,
|
||||
@@ -16,19 +17,31 @@ Page({
|
||||
paused: false,
|
||||
completed: false,
|
||||
startedAt: 0,
|
||||
regionText: '全区域',
|
||||
regionText: '',
|
||||
fittingLost: false,
|
||||
deviceFault: false
|
||||
},
|
||||
|
||||
_getRegionText: function (regionMask) {
|
||||
var REGION_MAP = {1:'右区', 2:'左区', 4:'上区', 8:'中区', 16:'下区', 32:'左上区', 64:'右上区'}
|
||||
if (regionMask === 0x7F) return '全区域'
|
||||
var REGION_MAP = {
|
||||
1: 'common.regions.right',
|
||||
2: 'common.regions.left',
|
||||
4: 'common.regions.top',
|
||||
8: 'common.regions.middle',
|
||||
16: 'common.regions.bottom',
|
||||
32: 'treating.regionTopLeft',
|
||||
64: 'treating.regionTopRight'
|
||||
}
|
||||
if (regionMask === 0x7F) return i18n.t('treating.allRegions')
|
||||
var names = []
|
||||
for (var bit in REGION_MAP) {
|
||||
if (regionMask & parseInt(bit)) names.push(REGION_MAP[bit])
|
||||
if (regionMask & parseInt(bit)) names.push(i18n.t(REGION_MAP[bit]))
|
||||
}
|
||||
return names.length > 0 ? names.join('+') : '全区域'
|
||||
return names.length > 0 ? names.join('+') : i18n.t('treating.allRegions')
|
||||
},
|
||||
|
||||
onShow: function () {
|
||||
i18n.bind(this)
|
||||
},
|
||||
|
||||
onLoad: function (options) {
|
||||
@@ -91,10 +104,10 @@ Page({
|
||||
if (this.data.completed) return
|
||||
if (data.fitting === 0 && !this.data.fittingLost) {
|
||||
this.setData({ fittingLost: true, paused: true })
|
||||
wx.showToast({ title: '检测到离肤,请重新佩戴设备', icon: 'none', duration: 3000 })
|
||||
wx.showToast({ title: i18n.t('treating.fittingLost'), icon: 'none', duration: 3000 })
|
||||
} else if (data.fitting === 1 && this.data.fittingLost) {
|
||||
this.setData({ fittingLost: false, paused: false })
|
||||
wx.showToast({ title: '已重新贴合,继续使用', icon: 'success', duration: 2000 })
|
||||
wx.showToast({ title: i18n.t('treating.fittingRestored'), icon: 'success', duration: 2000 })
|
||||
}
|
||||
},
|
||||
|
||||
@@ -112,8 +125,8 @@ Page({
|
||||
this.setData({ deviceFault: true })
|
||||
var self = this
|
||||
wx.showModal({
|
||||
title: '设备故障',
|
||||
content: '设备上报故障,已自动停止。',
|
||||
title: i18n.t('treating.faultTitle'),
|
||||
content: i18n.t('treating.faultContent'),
|
||||
showCancel: false,
|
||||
complete: function () { self.finishAsComplete() }
|
||||
})
|
||||
@@ -136,8 +149,8 @@ Page({
|
||||
if (self.data.battery > 0 && self.data.battery <= 5) {
|
||||
clearInterval(timer)
|
||||
wx.showModal({
|
||||
title: '电量过低',
|
||||
content: '设备电量不足5%,已自动停止。请及时充电。',
|
||||
title: i18n.t('treating.lowBatteryTitle'),
|
||||
content: i18n.t('treating.lowBatteryContent'),
|
||||
showCancel: false,
|
||||
complete: function () { self.finishAsComplete() }
|
||||
})
|
||||
@@ -146,7 +159,7 @@ Page({
|
||||
|
||||
if (self.data.battery > 0 && self.data.battery <= 10 && !self._lowBatteryWarned) {
|
||||
self._lowBatteryWarned = true
|
||||
wx.showToast({ title: '电量低,请及时充电', icon: 'none', duration: 3000 })
|
||||
wx.showToast({ title: i18n.t('treating.lowBatteryWarn'), icon: 'none', duration: 3000 })
|
||||
}
|
||||
|
||||
if (remaining <= 0) {
|
||||
@@ -262,8 +275,8 @@ Page({
|
||||
|
||||
onException: function (err) {
|
||||
wx.showModal({
|
||||
title: '设备异常',
|
||||
content: err.error_msg || '使用过程中出现异常',
|
||||
title: i18n.t('treating.exceptionTitle'),
|
||||
content: err.error_msg || i18n.t('treating.exceptionContent'),
|
||||
showCancel: false,
|
||||
complete: function () {
|
||||
wx.navigateBack()
|
||||
@@ -274,7 +287,7 @@ Page({
|
||||
onDisconnect: function () {
|
||||
this.setData({ paused: true })
|
||||
wx.showToast({
|
||||
title: '设备连接断开,正在重连...',
|
||||
title: i18n.t('treating.disconnected'),
|
||||
icon: 'none',
|
||||
duration: 3000
|
||||
})
|
||||
@@ -283,7 +296,7 @@ Page({
|
||||
onReconnect: function () {
|
||||
this.setData({ paused: false })
|
||||
wx.showToast({
|
||||
title: '设备已重新连接',
|
||||
title: i18n.t('treating.reconnected'),
|
||||
icon: 'success',
|
||||
duration: 2000
|
||||
})
|
||||
@@ -296,8 +309,8 @@ Page({
|
||||
this._localTimer = null
|
||||
}
|
||||
wx.showModal({
|
||||
title: '连接丢失',
|
||||
content: '设备蓝牙连接已断开,无法恢复。本次使用数据将保存。',
|
||||
title: i18n.t('treating.reconnectFailedTitle'),
|
||||
content: i18n.t('treating.reconnectFailedContent'),
|
||||
showCancel: false,
|
||||
complete: function () {
|
||||
self.finishAsComplete()
|
||||
@@ -312,8 +325,8 @@ Page({
|
||||
onStop: function () {
|
||||
var self = this
|
||||
wx.showModal({
|
||||
title: '结束使用',
|
||||
content: '确定要提前结束本次使用吗?',
|
||||
title: i18n.t('treating.stopTitle'),
|
||||
content: i18n.t('treating.stopContent'),
|
||||
success: function (res) {
|
||||
if (res.confirm) {
|
||||
ble.stopTreatment().then(function (res) {
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<view class="page">
|
||||
<view class="page-header page-header-blue" style="padding-top: {{statusBarHeight + 24}}px;">
|
||||
<view class="nav-back nav-back-light" bindtap="onBack">‹ 返回</view>
|
||||
<view class="page-header-title">使用中</view>
|
||||
<view class="page-header-subtitle">正在使用...</view>
|
||||
<view class="nav-back nav-back-light" bindtap="onBack">‹ {{i18n.common.back}}</view>
|
||||
<view class="page-header-title">{{i18n.treating.title}}</view>
|
||||
<view class="page-header-subtitle">{{i18n.treating.subtitle}}</view>
|
||||
</view>
|
||||
|
||||
<view class="page-content">
|
||||
<view class="countdown-area">
|
||||
<text class="countdown-time">{{remainingText}}</text>
|
||||
<view class="countdown-label">剩余时间</view>
|
||||
<view class="countdown-label">{{i18n.treating.remainingLabel}}</view>
|
||||
</view>
|
||||
|
||||
<view class="progress-bar">
|
||||
@@ -16,17 +16,17 @@
|
||||
</view>
|
||||
|
||||
<view class="mode-tag-area">
|
||||
<text class="mode-tag">{{mode === 1 ? '✨ 智能模式' : '🔄 普通模式'}} · {{regionText}}</text>
|
||||
<text class="mode-tag">{{mode === 1 ? i18n.treating.smartMode : i18n.treating.normalMode}} · {{regionText}}</text>
|
||||
</view>
|
||||
|
||||
<view class="relax-card">
|
||||
<text class="relax-icon">💆</text>
|
||||
<view class="relax-text">
|
||||
<text>请放松心情</text>
|
||||
<text>享受美好时光</text>
|
||||
<text>{{i18n.treating.relax1}}</text>
|
||||
<text>{{i18n.treating.relax2}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<button class="btn-primary btn-primary-red" bindtap="onStop">停止使用</button>
|
||||
<button class="btn-primary btn-primary-red" bindtap="onStop">{{i18n.treating.stopBtn}}</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
var api = require('../../utils/api')
|
||||
var ble = require('../../services/ble')
|
||||
var i18n = require('../../i18n/index')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
@@ -15,6 +16,10 @@ Page({
|
||||
synced: false
|
||||
},
|
||||
|
||||
onShow: function () {
|
||||
i18n.bind(this)
|
||||
},
|
||||
|
||||
onBack: function () {
|
||||
wx.reLaunch({ url: '/pages/index/index' })
|
||||
},
|
||||
@@ -33,7 +38,7 @@ Page({
|
||||
regions: parseInt(options.regions) || 0,
|
||||
duration: durationMs,
|
||||
avgPd: options.avg_pd || 0,
|
||||
durationText: mins > 0 ? mins + '分钟' : secs + '秒',
|
||||
durationText: mins > 0 ? i18n.t('treatmentDone.durationMin', { min: mins }) : i18n.t('treatmentDone.durationSec', { sec: secs }),
|
||||
mode: parseInt(options.mode) || 0,
|
||||
regionNames: ble.getRegionName(parseInt(options.regions) || 0)
|
||||
})
|
||||
|
||||
@@ -1,30 +1,30 @@
|
||||
<view class="page">
|
||||
<view class="page-header page-header-green" style="padding-top: {{statusBarHeight + 24}}px;">
|
||||
<view class="nav-back" bindtap="onBack">‹ 返回</view>
|
||||
<view class="page-header-title">使用完成</view>
|
||||
<view class="page-header-subtitle">本次使用已结束</view>
|
||||
<view class="nav-back" bindtap="onBack">‹ {{i18n.common.back}}</view>
|
||||
<view class="page-header-title">{{i18n.treatmentDone.title}}</view>
|
||||
<view class="page-header-subtitle">{{i18n.treatmentDone.headerSub}}</view>
|
||||
</view>
|
||||
|
||||
<view class="page-content">
|
||||
<view class="result-icon">✓</view>
|
||||
<view class="result-title">使用完成!</view>
|
||||
<view class="result-title">{{i18n.treatmentDone.resultTitle}}</view>
|
||||
|
||||
<view class="result-data">
|
||||
<view class="result-item">
|
||||
<view class="result-value">{{durationText}}</view>
|
||||
<view class="result-label">使用时长</view>
|
||||
<view class="result-label">{{i18n.treatmentDone.durationLabel}}</view>
|
||||
</view>
|
||||
<view class="result-item">
|
||||
<view class="result-value">{{regionNames.length}}</view>
|
||||
<view class="result-label">使用区域</view>
|
||||
<view class="result-label">{{i18n.treatmentDone.regionLabel}}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="result-tip">
|
||||
<text class="result-tip-icon">✨</text>
|
||||
<text class="result-tip-text">本次使用已记录</text>
|
||||
<text class="result-tip-text">{{i18n.treatmentDone.recorded}}</text>
|
||||
</view>
|
||||
|
||||
<button class="btn-primary" bindtap="onBackHome">返回首页</button>
|
||||
<button class="btn-primary" bindtap="onBackHome">{{i18n.treatmentDone.backHome}}</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
var ble = require('../../services/ble')
|
||||
var i18n = require('../../i18n/index')
|
||||
Page({
|
||||
data: {
|
||||
statusBarHeight: 44,
|
||||
regions: [
|
||||
{ name: '右区', mask: 0x01, checked: true },
|
||||
{ name: '左区', mask: 0x02, checked: true },
|
||||
{ name: '上区', mask: 0x04, checked: true },
|
||||
{ name: '中区', mask: 0x08, checked: true },
|
||||
{ name: '下区', mask: 0x10, checked: true }
|
||||
{ name: '', mask: 0x01, checked: true },
|
||||
{ name: '', mask: 0x02, checked: true },
|
||||
{ name: '', mask: 0x04, checked: true },
|
||||
{ name: '', mask: 0x08, checked: true },
|
||||
{ name: '', mask: 0x10, checked: true }
|
||||
],
|
||||
selectedMode: 0,
|
||||
subExpired: false,
|
||||
@@ -25,9 +26,23 @@ Page({
|
||||
})
|
||||
},
|
||||
|
||||
onManual: function () {
|
||||
wx.navigateTo({ url: '/pages/manual-treatment/manual-treatment' })
|
||||
},
|
||||
|
||||
onLoad: function () {
|
||||
var app = getApp()
|
||||
this.setData({ statusBarHeight: app.globalData.statusBarHeight })
|
||||
i18n.bind(this)
|
||||
var regionKeys = ['right', 'left', 'top', 'middle', 'bottom']
|
||||
var regions = this.data.regions
|
||||
regions.forEach(function (r, i) {
|
||||
r.name = i18n.t('common.regions.' + regionKeys[i])
|
||||
})
|
||||
this.setData({
|
||||
regions: regions,
|
||||
subDaysText: i18n.t('setup.remainDays', { days: this.data.subDays })
|
||||
})
|
||||
this.checkSubscription()
|
||||
},
|
||||
|
||||
@@ -37,7 +52,8 @@ Page({
|
||||
http.get('/api/v1/subscription').then(function (sub) {
|
||||
self.setData({
|
||||
subExpired: sub.status !== 'active',
|
||||
subDays: sub.remaining_days || 0
|
||||
subDays: sub.remaining_days || 0,
|
||||
subDaysText: i18n.t('setup.remainDays', { days: sub.remaining_days || 0 })
|
||||
})
|
||||
}).catch(function () {})
|
||||
},
|
||||
@@ -73,7 +89,7 @@ Page({
|
||||
})
|
||||
|
||||
if (mask === 0) {
|
||||
wx.showToast({ title: '请至少选择一个区域', icon: 'none' })
|
||||
wx.showToast({ title: i18n.t('setup.noRegion'), icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
@@ -103,7 +119,7 @@ Page({
|
||||
'&mode=' + self.data.selectedMode
|
||||
})
|
||||
}).catch(function (err) {
|
||||
self.setData({ submitting: false, error: err.error_msg || '启动失败' })
|
||||
self.setData({ submitting: false, error: err.error_msg || i18n.t('setup.startFailed') })
|
||||
})
|
||||
},
|
||||
|
||||
|
||||
@@ -1,42 +1,44 @@
|
||||
<view class="page">
|
||||
<view class="page-header page-header-pink" style="padding-top: {{statusBarHeight + 24}}px;">
|
||||
<view class="nav-back" bindtap="onBack">‹ 返回</view>
|
||||
<view class="page-header-title">使用设置</view>
|
||||
<view class="page-header-subtitle">选择模式和区域</view>
|
||||
<view class="nav-back" bindtap="onBack">‹ {{i18n.common.back}}</view>
|
||||
<view class="page-header-title">{{i18n.setup.title}}</view>
|
||||
<view class="page-header-subtitle">{{i18n.setup.subtitle}}</view>
|
||||
</view>
|
||||
|
||||
<view class="page-content">
|
||||
<view class="page-title">选择模式</view>
|
||||
<view class="page-title">{{i18n.setup.selectMode}}</view>
|
||||
|
||||
<view class="mode-selector">
|
||||
<view class="mode-option {{selectedMode === 0 ? 'active' : ''}}" bindtap="onSelectMode" data-value="0">
|
||||
<text class="mode-icon">🔄</text>
|
||||
<view class="mode-label">普通模式</view>
|
||||
<view class="mode-sub">6色循环</view>
|
||||
<view class="mode-label">{{i18n.setup.normalMode}}</view>
|
||||
<view class="mode-sub">{{i18n.setup.normalModeSub}}</view>
|
||||
</view>
|
||||
<view class="mode-option {{selectedMode === 1 ? 'active' : ''}} {{subExpired ? 'locked' : ''}}" bindtap="onSelectMode" data-value="1">
|
||||
<text class="mode-icon">✨</text>
|
||||
<view class="mode-label">智能模式</view>
|
||||
<view class="mode-sub" wx:if="{{!subExpired}}">剩余{{subDays}}天</view>
|
||||
<view class="mode-sub" wx:else>订阅解锁</view>
|
||||
<view class="mode-label">{{i18n.setup.smartMode}}</view>
|
||||
<view class="mode-sub" wx:if="{{!subExpired}}">{{subDaysText}}</view>
|
||||
<view class="mode-sub" wx:else>{{i18n.setup.subscribeUnlock}}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="page-title" style="font-size:28rpx;">选择区域</view>
|
||||
<view class="mode-tip" wx:if="{{selectedMode === 0}}">普通模式固定 10 分钟,使用区域默认全区域,不可调整</view>
|
||||
<view class="page-title" style="font-size:28rpx;">{{i18n.setup.selectRegion}}</view>
|
||||
<view class="mode-tip" wx:if="{{selectedMode === 0}}">{{i18n.setup.normalTip}}</view>
|
||||
|
||||
<view class="face-map">
|
||||
<view class="face-region forehead {{regions[2].checked ? 'active' : ''}} {{selectedMode === 0 ? 'disabled' : ''}}" bindtap="onToggleRegion" data-idx="2">上区</view>
|
||||
<view class="face-region left-cheek {{regions[1].checked ? 'active' : ''}} {{selectedMode === 0 ? 'disabled' : ''}}" bindtap="onToggleRegion" data-idx="1">左区</view>
|
||||
<view class="face-region right-cheek {{regions[0].checked ? 'active' : ''}} {{selectedMode === 0 ? 'disabled' : ''}}" bindtap="onToggleRegion" data-idx="0">右区</view>
|
||||
<view class="face-region nose {{regions[3].checked ? 'active' : ''}} {{selectedMode === 0 ? 'disabled' : ''}}" bindtap="onToggleRegion" data-idx="3">中区</view>
|
||||
<view class="face-region chin {{regions[4].checked ? 'active' : ''}} {{selectedMode === 0 ? 'disabled' : ''}}" bindtap="onToggleRegion" data-idx="4">下区</view>
|
||||
<view class="face-region forehead {{regions[2].checked ? 'active' : ''}} {{selectedMode === 0 ? 'disabled' : ''}}" bindtap="onToggleRegion" data-idx="2">{{i18n.common.regions.top}}</view>
|
||||
<view class="face-region left-cheek {{regions[1].checked ? 'active' : ''}} {{selectedMode === 0 ? 'disabled' : ''}}" bindtap="onToggleRegion" data-idx="1">{{i18n.common.regions.left}}</view>
|
||||
<view class="face-region right-cheek {{regions[0].checked ? 'active' : ''}} {{selectedMode === 0 ? 'disabled' : ''}}" bindtap="onToggleRegion" data-idx="0">{{i18n.common.regions.right}}</view>
|
||||
<view class="face-region nose {{regions[3].checked ? 'active' : ''}} {{selectedMode === 0 ? 'disabled' : ''}}" bindtap="onToggleRegion" data-idx="3">{{i18n.common.regions.middle}}</view>
|
||||
<view class="face-region chin {{regions[4].checked ? 'active' : ''}} {{selectedMode === 0 ? 'disabled' : ''}}" bindtap="onToggleRegion" data-idx="4">{{i18n.common.regions.bottom}}</view>
|
||||
</view>
|
||||
|
||||
<button class="btn-primary" bindtap="onStart" disabled="{{submitting}}" loading="{{submitting}}">
|
||||
开始使用
|
||||
{{i18n.setup.startBtn}}
|
||||
</button>
|
||||
|
||||
<view class="manual-entry" bindtap="onManual" style="text-align:center;color:#E8503A;font-size:26rpx;margin-top:28rpx;">{{i18n.manual.entryLink}}</view>
|
||||
|
||||
<view class="error-text" wx:if="{{error}}">{{error}}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
var ble = require('../../services/ble')
|
||||
var i18n = require('../../i18n/index')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
statusBarHeight: 44,
|
||||
battery: 0,
|
||||
connected: false
|
||||
connected: false,
|
||||
reconnecting: false
|
||||
},
|
||||
|
||||
onShow: function () {
|
||||
i18n.bind(this)
|
||||
},
|
||||
|
||||
onBack: function () {
|
||||
@@ -27,7 +33,7 @@ Page({
|
||||
|
||||
onNext: function () {
|
||||
if (!ble.isConnected()) {
|
||||
wx.showToast({ title: '设备未连接', icon: 'none' })
|
||||
wx.showToast({ title: i18n.t('common.deviceNotConnected'), icon: 'none' })
|
||||
return
|
||||
}
|
||||
wx.redirectTo({
|
||||
@@ -37,29 +43,32 @@ Page({
|
||||
|
||||
onRetry: function () {
|
||||
var self = this
|
||||
if (self.data.reconnecting) return
|
||||
if (ble.isConnected()) {
|
||||
self.setData({ connected: true })
|
||||
return
|
||||
}
|
||||
self.setData({ connected: false })
|
||||
self.setData({ connected: false, reconnecting: true })
|
||||
ble.disconnect()
|
||||
setTimeout(function () {
|
||||
ble.startScan({
|
||||
onFound: function () {},
|
||||
onConnected: function () {
|
||||
clearTimeout(self._reconnectTimer)
|
||||
self.setData({ connected: true })
|
||||
wx.showToast({ title: '连接成功', icon: 'success' })
|
||||
self.setData({ connected: true, reconnecting: false })
|
||||
wx.showToast({ title: i18n.t('wearCheck.connectSuccess'), icon: 'success' })
|
||||
},
|
||||
onError: function (err) {
|
||||
clearTimeout(self._reconnectTimer)
|
||||
wx.showToast({ title: err.msg || '连接失败', icon: 'none' })
|
||||
self.setData({ reconnecting: false })
|
||||
wx.showToast({ title: err.msg || i18n.t('wearCheck.connectFailed'), icon: 'none' })
|
||||
}
|
||||
})
|
||||
self._reconnectTimer = setTimeout(function () {
|
||||
ble.stopScan()
|
||||
wx.showToast({ title: '搜索超时', icon: 'none' })
|
||||
}, 15000)
|
||||
self.setData({ reconnecting: false })
|
||||
wx.showToast({ title: i18n.t('wearCheck.searchTimeout'), icon: 'none' })
|
||||
}, 5000)
|
||||
}, 500)
|
||||
},
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<view class="page">
|
||||
<view class="page-header page-header-pink" style="padding-top: {{statusBarHeight + 24}}px;">
|
||||
<view class="nav-back" bindtap="onBack">‹ 返回</view>
|
||||
<view class="page-header-title">确认佩戴</view>
|
||||
<view class="page-header-subtitle">请确保设备正确佩戴</view>
|
||||
<view class="nav-back" bindtap="onBack">‹ {{i18n.common.back}}</view>
|
||||
<view class="page-header-title">{{i18n.wearCheck.title}}</view>
|
||||
<view class="page-header-subtitle">{{i18n.wearCheck.subtitle}}</view>
|
||||
</view>
|
||||
|
||||
<view class="page-content">
|
||||
@@ -11,24 +11,24 @@
|
||||
<view class="device-row-info">
|
||||
<view class="device-row-name">
|
||||
LumiFlow
|
||||
<text class="status-badge status-badge-green" wx:if="{{connected}}">已连接</text>
|
||||
<text class="status-badge" style="background:#ccc;" wx:else>未连接</text>
|
||||
<text class="status-badge status-badge-green" wx:if="{{connected}}">{{i18n.wearCheck.connected}}</text>
|
||||
<text class="status-badge" style="background:#ccc;" wx:else>{{i18n.wearCheck.disconnected}}</text>
|
||||
</view>
|
||||
<view class="device-row-battery">电量 {{battery}}%</view>
|
||||
<view class="device-row-battery">{{i18n.wearCheck.battery}} {{battery}}%</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="guide-card">
|
||||
<view class="guide-title">佩戴指引</view>
|
||||
<view class="guide-img">设备佩戴示意图</view>
|
||||
<view class="guide-title">{{i18n.wearCheck.guideTitle}}</view>
|
||||
<view class="guide-img">{{i18n.wearCheck.guideImg}}</view>
|
||||
<view class="guide-steps">
|
||||
<view class="guide-step">1. 取出设备,展开弹性绑带</view>
|
||||
<view class="guide-step">2. 将设备正确佩戴</view>
|
||||
<view class="guide-step">3. 调节绑带至舒适位置</view>
|
||||
<view class="guide-step">{{i18n.wearCheck.step1}}</view>
|
||||
<view class="guide-step">{{i18n.wearCheck.step2}}</view>
|
||||
<view class="guide-step">{{i18n.wearCheck.step3}}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<button class="btn-primary" wx:if="{{connected}}" bindtap="onNext">确认已佩戴,开始检测</button>
|
||||
<button class="btn-secondary" wx:if="{{!connected}}" bindtap="onRetry">重新连接</button>
|
||||
<button class="btn-primary" wx:if="{{connected}}" bindtap="onNext">{{i18n.wearCheck.confirmBtn}}</button>
|
||||
<button class="btn-secondary" wx:if="{{!connected}}" bindtap="onRetry" disabled="{{reconnecting}}" loading="{{reconnecting}}">{{reconnecting ? i18n.wearCheck.connecting : i18n.wearCheck.reconnect}}</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
// Skin-tendency diagnosis — FRAMEWORK / placeholder implementation.
|
||||
//
|
||||
// STATUS: the diagnosis 依据 is NOT yet calibrated (客户也认为"还只是概念").
|
||||
// This module defines the full data pipeline so the report UI + storage work end
|
||||
// to end today, but the thresholds and the PD->region mapping are PLACEHOLDERS to
|
||||
// be replaced after real-device calibration. It degrades gracefully when only
|
||||
// partial scan data (currently single-wavelength red) is available.
|
||||
//
|
||||
// Two things here need hardware/calibration confirmation before this is "real":
|
||||
// 1) PD_REGION_MAP — which of the 7 PDs feed which facial region (docs disagree).
|
||||
// 2) levels thresholds — normalized-absorption cutoffs per severity level.
|
||||
// Both can be overridden at runtime by the server-side diagnosis_config, so tuning
|
||||
// after calibration does NOT require an app release.
|
||||
|
||||
// Problem type -> recommended care wavelength (IR=1, R=2, UV=3, Y=4).
|
||||
var PROBLEM_WAVELENGTH = { aging: 2, acne: 3, pigment: 4, deep: 1 }
|
||||
// Scanned wavelength code -> the problem it reveals (inverse of the above).
|
||||
var WAVELENGTH_PROBLEM = { 1: 'deep', 2: 'aging', 3: 'acne', 4: 'pigment' }
|
||||
// Dictionary wave-key -> wavelength code.
|
||||
var WAVE_KEY_CODE = { red: 2, uv: 3, yellow: 4, ir: 1 }
|
||||
|
||||
// 5 regions in the app's neutral convention (right/left/top/middle/bottom),
|
||||
// matching treatment-setup.js masks.
|
||||
var REGION_DEFS = [
|
||||
{ key: 'right', mask: 0x01 },
|
||||
{ key: 'left', mask: 0x02 },
|
||||
{ key: 'top', mask: 0x04 },
|
||||
{ key: 'middle', mask: 0x08 },
|
||||
{ key: 'bottom', mask: 0x10 }
|
||||
]
|
||||
|
||||
// PLACEHOLDER PD->region map. Flow doc: 7 PDs = 1 special region (3 PDs) + 4 regions
|
||||
// (1 PD each). The exact assignment is UNCONFIRMED — replace after hardware confirms
|
||||
// the PD physical layout. Each region lists the PD index(es) that feed its signal.
|
||||
var PD_REGION_MAP = {
|
||||
right: [0], left: [1], top: [2], middle: [3, 5, 6], bottom: [4]
|
||||
}
|
||||
|
||||
var MAX_ADC = 4095
|
||||
|
||||
var DEFAULT_CONFIG = {
|
||||
calibrated: false,
|
||||
levels: { low: 0.15, mid: 0.35, high: 0.55 },
|
||||
problem_wavelength: PROBLEM_WAVELENGTH
|
||||
}
|
||||
|
||||
function _avg(arr) {
|
||||
if (!arr || !arr.length) return 0
|
||||
var s = 0
|
||||
for (var i = 0; i < arr.length; i++) s += arr[i]
|
||||
return s / arr.length
|
||||
}
|
||||
|
||||
// Normalized absorption in [0,1]: the more the light is absorbed (lower reflected PD
|
||||
// relative to the底噪 baseline), the stronger the tendency signal.
|
||||
function _absorption(pdValue, baseline) {
|
||||
if (!baseline || baseline <= 0) return 0
|
||||
var a = (baseline - pdValue) / baseline
|
||||
if (a < 0) a = 0
|
||||
if (a > 1) a = 1
|
||||
return a
|
||||
}
|
||||
|
||||
function _levelOf(score, levels) {
|
||||
if (score >= levels.high) return 3
|
||||
if (score >= levels.mid) return 2
|
||||
if (score >= levels.low) return 1
|
||||
return 0
|
||||
}
|
||||
|
||||
// scanData: {
|
||||
// device_id, scanned_at,
|
||||
// region_mask, // which regions were scanned (default 0x1F)
|
||||
// noise: [7], // per-PD 底噪 baseline (optional; defaults MAX_ADC)
|
||||
// waves: { red:[7], uv:[7], yellow:[7], ir:[7] } // any subset; each = 7 PD values
|
||||
// }
|
||||
function analyze(scanData, config) {
|
||||
config = config || DEFAULT_CONFIG
|
||||
var levels = config.levels || DEFAULT_CONFIG.levels
|
||||
var problemWave = config.problem_wavelength || PROBLEM_WAVELENGTH
|
||||
scanData = scanData || {}
|
||||
var waves = scanData.waves || {}
|
||||
var noise = scanData.noise
|
||||
var scannedMask = scanData.region_mask || 0x1F
|
||||
|
||||
var regions = []
|
||||
var overallAcc = {}
|
||||
|
||||
for (var r = 0; r < REGION_DEFS.length; r++) {
|
||||
var def = REGION_DEFS[r]
|
||||
if ((scannedMask & def.mask) === 0) continue
|
||||
var pdIdx = PD_REGION_MAP[def.key] || []
|
||||
var problems = []
|
||||
|
||||
for (var wk in waves) {
|
||||
if (!waves.hasOwnProperty(wk)) continue
|
||||
var code = WAVE_KEY_CODE[wk]
|
||||
var problemType = WAVELENGTH_PROBLEM[code]
|
||||
if (!problemType) continue
|
||||
|
||||
var pdVals = []
|
||||
var baseVals = []
|
||||
for (var k = 0; k < pdIdx.length; k++) {
|
||||
var idx = pdIdx[k]
|
||||
if (waves[wk][idx] !== undefined) pdVals.push(waves[wk][idx])
|
||||
baseVals.push(noise && noise[idx] !== undefined ? noise[idx] : MAX_ADC)
|
||||
}
|
||||
if (!pdVals.length) continue
|
||||
|
||||
var score = _absorption(_avg(pdVals), _avg(baseVals))
|
||||
var level = _levelOf(score, levels)
|
||||
if (level > 0) {
|
||||
problems.push({ type: problemType, level: level, score: Math.round(score * 100) / 100 })
|
||||
if (!overallAcc[problemType] || level > overallAcc[problemType]) {
|
||||
overallAcc[problemType] = level
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
problems.sort(function (a, b) { return (b.level - a.level) || (b.score - a.score) })
|
||||
var top = problems.length ? problems[0] : null
|
||||
regions.push({
|
||||
region: def.key,
|
||||
mask: def.mask,
|
||||
top_problem: top ? top.type : null,
|
||||
level: top ? top.level : 0,
|
||||
problems: problems
|
||||
})
|
||||
}
|
||||
|
||||
var overall = []
|
||||
for (var pt in overallAcc) {
|
||||
if (overallAcc.hasOwnProperty(pt)) overall.push({ type: pt, level: overallAcc[pt] })
|
||||
}
|
||||
overall.sort(function (a, b) { return b.level - a.level })
|
||||
|
||||
// Recommendation: care each region-with-a-top-problem using that problem's wavelength.
|
||||
// One BLE command carries one wavelength, so plan entries are grouped by wavelength.
|
||||
var planByWave = {}
|
||||
var recommendMask = 0
|
||||
for (var i = 0; i < regions.length; i++) {
|
||||
var reg = regions[i]
|
||||
if (!reg.top_problem) continue
|
||||
var wl = problemWave[reg.top_problem]
|
||||
if (!wl) continue
|
||||
planByWave[wl] = (planByWave[wl] || 0) | reg.mask
|
||||
recommendMask |= reg.mask
|
||||
}
|
||||
var recommendPlan = []
|
||||
for (var w in planByWave) {
|
||||
if (!planByWave.hasOwnProperty(w)) continue
|
||||
var code2 = parseInt(w, 10)
|
||||
recommendPlan.push({ wavelength: code2, mask: planByWave[w], label_key: WAVELENGTH_PROBLEM[code2] })
|
||||
}
|
||||
|
||||
return {
|
||||
device_id: scanData.device_id || null,
|
||||
scanned_at: scanData.scanned_at || 0,
|
||||
regions: regions,
|
||||
overall: overall,
|
||||
recommend_mask: recommendMask,
|
||||
recommend_plan: recommendPlan,
|
||||
raw_pd: waves,
|
||||
calibrated: !!config.calibrated
|
||||
}
|
||||
}
|
||||
|
||||
// Adapt the current (single-wavelength red) auto-scan result into scanData, so the
|
||||
// pipeline works today. pdAvg = 7 averaged PD values under red light.
|
||||
function fromRedScan(pdAvg, regionMask, deviceId, scannedAt, noise) {
|
||||
return {
|
||||
device_id: deviceId || null,
|
||||
scanned_at: scannedAt || 0,
|
||||
region_mask: regionMask || 0x1F,
|
||||
noise: noise || null,
|
||||
waves: { red: (pdAvg || []).slice(0, 7) }
|
||||
}
|
||||
}
|
||||
|
||||
// Deterministic mock report for UI development / demo when there is no device data.
|
||||
function mockReport() {
|
||||
return analyze({
|
||||
device_id: 'MOCK',
|
||||
scanned_at: 0,
|
||||
region_mask: 0x1F,
|
||||
noise: [4000, 4000, 4000, 4000, 4000, 4000, 4000],
|
||||
waves: {
|
||||
red: [3500, 3600, 1900, 3400, 3450, 3500, 3500],
|
||||
uv: [3700, 3750, 3600, 1700, 3700, 1800, 1900],
|
||||
yellow: [3500, 3550, 3400, 3450, 2300, 3500, 3520],
|
||||
ir: [3600, 3600, 3550, 3580, 3560, 3600, 3600]
|
||||
}
|
||||
}, DEFAULT_CONFIG)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
DEFAULT_CONFIG: DEFAULT_CONFIG,
|
||||
PROBLEM_WAVELENGTH: PROBLEM_WAVELENGTH,
|
||||
WAVELENGTH_PROBLEM: WAVELENGTH_PROBLEM,
|
||||
REGION_DEFS: REGION_DEFS,
|
||||
analyze: analyze,
|
||||
fromRedScan: fromRedScan,
|
||||
mockReport: mockReport
|
||||
}
|
||||
@@ -30,6 +30,12 @@ module.exports = {
|
||||
syncTreatment: function (data) { return http.post('/api/v1/treatment/sync', data) },
|
||||
getRecordDetail: function (id) { return http.get('/api/v1/treatment/' + id) },
|
||||
|
||||
// Scan report + diagnosis config (backend contract; see server scan-report routes)
|
||||
saveReport: function (data) { return http.post('/api/v1/treatment/report', data) },
|
||||
getLatestReport: function (deviceId) { return http.get('/api/v1/treatment/report/latest', deviceId ? { device_id: deviceId } : {}) },
|
||||
getReport: function (id) { return http.get('/api/v1/treatment/report/' + id) },
|
||||
getDiagnosisConfig: function () { return http.get('/api/v1/treatment/diagnosis-config') },
|
||||
|
||||
// Device commands
|
||||
getPendingCommands: function (deviceId) { return http.get('/api/v1/device/command/pending', { device_id: deviceId }) },
|
||||
reportCommandResult: function (data) { return http.post('/api/v1/device/command/result', data) }
|
||||
|
||||
+20
-1
@@ -84,6 +84,24 @@ CREATE TABLE IF NOT EXISTS treatment_records (
|
||||
CONSTRAINT fk_treatment_user FOREIGN KEY (user_id) REFERENCES users (user_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS scan_reports (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
user_id BIGINT UNSIGNED NOT NULL,
|
||||
device_id VARCHAR(32) NULL,
|
||||
scanned_at DATETIME NULL,
|
||||
regions JSON NULL COMMENT 'array of per-region analysis',
|
||||
overall JSON NULL COMMENT 'array of overall tendencies',
|
||||
recommend_mask INT NOT NULL DEFAULT 0 COMMENT 'bitmask of regions recommended for care',
|
||||
recommend_plan JSON NULL COMMENT 'array of {wavelength, mask, label_key}',
|
||||
raw_pd JSON NULL COMMENT 'raw PD samples for the calibration dataset (may be partial)',
|
||||
calibrated TINYINT NOT NULL DEFAULT 0 COMMENT 'whether thresholds were calibrated',
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_scan_reports_user_created (user_id, created_at),
|
||||
KEY idx_scan_reports_device_created (device_id, created_at),
|
||||
CONSTRAINT fk_scan_reports_user FOREIGN KEY (user_id) REFERENCES users (user_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS device_events (
|
||||
event_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
device_id VARCHAR(32) NOT NULL,
|
||||
@@ -185,4 +203,5 @@ INSERT IGNORE INTO system_settings (setting_key, setting_value) VALUES
|
||||
('enable_register', 'true'),
|
||||
('enable_binding', 'true'),
|
||||
('enable_free_mode', 'true'),
|
||||
('maintenance_mode', 'false');
|
||||
('maintenance_mode', 'false'),
|
||||
('diagnosis_config', '{"calibrated": false, "note": "占位阈值,待真机标定后调整", "levels": {"low": 0.15, "mid": 0.35, "high": 0.55}, "problem_wavelength": {"aging": 2, "acne": 3, "pigment": 4, "deep": 1}}');
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
const { query, one, limitClause } = require('../lib/db')
|
||||
|
||||
/**
|
||||
* Create a scan report
|
||||
* @param {Object} report
|
||||
* @param {number} report.user_id
|
||||
* @param {string|null} report.device_id
|
||||
* @param {string|null} report.scanned_at - MySQL datetime string
|
||||
* @param {string} report.regions - JSON string (array)
|
||||
* @param {string} report.overall - JSON string (array)
|
||||
* @param {number} report.recommend_mask
|
||||
* @param {string} report.recommend_plan - JSON string (array)
|
||||
* @param {string} report.raw_pd - JSON string
|
||||
* @param {number} report.calibrated - 0/1
|
||||
* @returns {Promise<Object>} query result (has insertId)
|
||||
*/
|
||||
async function create(report) {
|
||||
return query(
|
||||
`INSERT INTO scan_reports
|
||||
(user_id, device_id, scanned_at, regions, overall, recommend_mask, recommend_plan, raw_pd, calibrated)
|
||||
VALUES (:user_id, :device_id, :scanned_at, :regions, :overall, :recommend_mask, :recommend_plan, :raw_pd, :calibrated)`,
|
||||
report
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a scan report by id
|
||||
* @param {number} id
|
||||
* @returns {Promise<Object|null>}
|
||||
*/
|
||||
async function findById(id) {
|
||||
return one('SELECT * FROM scan_reports WHERE id = :id', { id })
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the latest scan report for a user, optionally filtered by device
|
||||
* @param {number} userId
|
||||
* @param {string} [deviceId]
|
||||
* @returns {Promise<Object|null>}
|
||||
*/
|
||||
async function findLatestByUser(userId, deviceId) {
|
||||
if (deviceId) {
|
||||
return one(
|
||||
'SELECT * FROM scan_reports WHERE user_id = :user_id AND device_id = :device_id ORDER BY created_at DESC, id DESC LIMIT 1',
|
||||
{ user_id: userId, device_id: deviceId }
|
||||
)
|
||||
}
|
||||
return one(
|
||||
'SELECT * FROM scan_reports WHERE user_id = :user_id ORDER BY created_at DESC, id DESC LIMIT 1',
|
||||
{ user_id: userId }
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* List scan reports for a user with pagination
|
||||
* @param {number} userId
|
||||
* @param {Object} opts
|
||||
* @param {number} opts.pageSize
|
||||
* @param {number} opts.offset
|
||||
* @returns {Promise<{records: Array, total: number}>}
|
||||
*/
|
||||
async function listByUser(userId, { pageSize, offset }) {
|
||||
const total = await query(
|
||||
'SELECT COUNT(*) AS total FROM scan_reports WHERE user_id = :user_id',
|
||||
{ user_id: userId }
|
||||
)
|
||||
const records = await query(
|
||||
'SELECT * FROM scan_reports WHERE user_id = :user_id ORDER BY created_at DESC, id DESC' + limitClause(pageSize, offset),
|
||||
{ user_id: userId }
|
||||
)
|
||||
return { records, total: total[0].total }
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
create,
|
||||
findById,
|
||||
findLatestByUser,
|
||||
listByUser
|
||||
}
|
||||
@@ -257,7 +257,7 @@ router.get('/settings', requireAdmin, wrap(async (req, res) => {
|
||||
}))
|
||||
|
||||
router.post('/settings', requireAdmin, wrap(async (req, res) => {
|
||||
const ALLOWED_KEYS = ['system_name', 'admin_email', 'timezone', 'monthly_price', 'yearly_price', 'trial_days', 'enable_register', 'enable_binding', 'enable_free_mode', 'enable_smart_mode', 'maintenance_mode']
|
||||
const ALLOWED_KEYS = ['system_name', 'admin_email', 'timezone', 'monthly_price', 'yearly_price', 'trial_days', 'enable_register', 'enable_binding', 'enable_free_mode', 'enable_smart_mode', 'maintenance_mode', 'diagnosis_config']
|
||||
for (const key of Object.keys(req.body || {})) {
|
||||
if (!ALLOWED_KEYS.includes(key)) continue
|
||||
await settingsDao.update(key, req.body[key])
|
||||
|
||||
@@ -28,6 +28,10 @@ router.get('/subscription/plans', wrap(async (req, res) => {
|
||||
}))
|
||||
|
||||
router.get('/subscription', requireUser, wrap(async (req, res) => {
|
||||
const settings = await getSettings()
|
||||
if (settings.smart_mode_free === true || settings.smart_mode_free === 'true') {
|
||||
return res.json(ok({ status: 'active', plan: 'free', remaining_days: 9999, trial_used: false }))
|
||||
}
|
||||
const sub = await subscriptionDao.findActive(req.user.user_id)
|
||||
const trialUsed = await subscriptionDao.findTrial(req.user.user_id)
|
||||
if (!sub) return res.json(ok({ status: 'inactive', plan: 'none', remaining_days: 0, trial_used: !!trialUsed }))
|
||||
|
||||
@@ -3,11 +3,21 @@ const { ok, fail } = require('../lib/response')
|
||||
const { requireUser } = require('../middleware/auth')
|
||||
const { toMysqlDate } = require('../lib/utils')
|
||||
const treatmentDao = require('../dao/treatment.dao')
|
||||
const scanReportDao = require('../dao/scan-report.dao')
|
||||
const bindingDao = require('../dao/binding.dao')
|
||||
const logDao = require('../dao/log.dao')
|
||||
const { getSettings } = require('../lib/settings-cache')
|
||||
|
||||
const wrap = fn => (req, res, next) => fn(req, res, next).catch(next)
|
||||
|
||||
// Fallback used if diagnosis_config has not been seeded in system_settings yet
|
||||
const DEFAULT_DIAGNOSIS_CONFIG = {
|
||||
calibrated: false,
|
||||
note: '占位阈值,待真机标定后调整',
|
||||
levels: { low: 0.15, mid: 0.35, high: 0.55 },
|
||||
problem_wavelength: { aging: 2, acne: 3, pigment: 4, deep: 1 }
|
||||
}
|
||||
|
||||
router.get('/treatment/history', requireUser, wrap(async (req, res) => {
|
||||
const page = Math.max(1, parseInt(req.query.page, 10) || 1)
|
||||
const pageSize = Math.min(Math.max(1, parseInt(req.query.page_size, 10) || 20), 100)
|
||||
@@ -49,6 +59,43 @@ router.post('/treatment/sync', requireUser, wrap(async (req, res) => {
|
||||
res.json(ok({ record_id: sessionId }))
|
||||
}))
|
||||
|
||||
// --- Scan reports ---
|
||||
|
||||
router.post('/treatment/report', requireUser, wrap(async (req, res) => {
|
||||
const d = req.body || {}
|
||||
const result = await scanReportDao.create({
|
||||
user_id: req.user.user_id,
|
||||
device_id: d.device_id == null ? null : String(d.device_id),
|
||||
scanned_at: toMysqlDate(d.scanned_at),
|
||||
regions: JSON.stringify(d.regions || []),
|
||||
overall: JSON.stringify(d.overall || []),
|
||||
recommend_mask: parseInt(d.recommend_mask, 10) || 0,
|
||||
recommend_plan: JSON.stringify(d.recommend_plan || []),
|
||||
raw_pd: JSON.stringify(d.raw_pd || {}),
|
||||
calibrated: d.calibrated ? 1 : 0
|
||||
})
|
||||
await logDao.write({ user_id: req.user.user_id, action: 'scan_report_create', detail: '保存扫描报告: #' + result.insertId, ip: req.ip })
|
||||
res.json(ok({ report_id: result.insertId }))
|
||||
}))
|
||||
|
||||
router.get('/treatment/report/latest', requireUser, wrap(async (req, res) => {
|
||||
const report = await scanReportDao.findLatestByUser(req.user.user_id, req.query.device_id)
|
||||
res.json(ok(report))
|
||||
}))
|
||||
|
||||
router.get('/treatment/report/:id', requireUser, wrap(async (req, res) => {
|
||||
const report = await scanReportDao.findById(req.params.id)
|
||||
if (!report || String(report.user_id) !== String(req.user.user_id)) return res.json(fail(1005, 'record_not_found'))
|
||||
res.json(ok(report))
|
||||
}))
|
||||
|
||||
// --- Server-driven diagnosis thresholds ---
|
||||
|
||||
router.get('/treatment/diagnosis-config', requireUser, wrap(async (req, res) => {
|
||||
const settings = await getSettings()
|
||||
res.json(ok(settings.diagnosis_config || DEFAULT_DIAGNOSIS_CONFIG))
|
||||
}))
|
||||
|
||||
router.get('/treatment/:record_id', requireUser, wrap(async (req, res) => {
|
||||
const record = await treatmentDao.findBySession(req.params.record_id, req.user.user_id)
|
||||
if (!record) return res.json(fail(1005, 'record_not_found'))
|
||||
|
||||
在新工单中引用
屏蔽一个用户