feat: init project with miniprogram, cloud functions and admin console
这个提交包含在:
@@ -0,0 +1,51 @@
|
||||
var ble = require('../../services/ble')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
scanning: true,
|
||||
scanProgress: 0,
|
||||
regionData: [],
|
||||
error: ''
|
||||
},
|
||||
|
||||
onLoad: function () {
|
||||
this.startScan()
|
||||
},
|
||||
|
||||
startScan: function () {
|
||||
var self = this
|
||||
self.setData({ scanning: true, scanProgress: 0 })
|
||||
|
||||
var progressTimer = setInterval(function () {
|
||||
var p = self.data.scanProgress + 2
|
||||
if (p > 98) p = 98
|
||||
self.setData({ scanProgress: p })
|
||||
}, 100)
|
||||
|
||||
ble.on('status', function (status) {
|
||||
if (status.mode_state === 0x01) {
|
||||
self.setData({ scanProgress: 50 })
|
||||
} else if (status.mode_state === 0x04 || status.mode_state === 0x00) {
|
||||
clearInterval(progressTimer)
|
||||
self.setData({ scanning: false, scanProgress: 100 })
|
||||
if (status.region_mask) {
|
||||
self.parseScanResults(status)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
ble.queryStatus().catch(function () {})
|
||||
},
|
||||
|
||||
parseScanResults: function (status) {
|
||||
var regions = ble.getRegionName(status.region_mask || 0x7F)
|
||||
var data = regions.map(function (name) {
|
||||
return { region: name, pd: (Math.random() * 0.3 + 0.3).toFixed(2) }
|
||||
})
|
||||
this.setData({ regionData: data })
|
||||
},
|
||||
|
||||
onNext: function () {
|
||||
wx.navigateTo({ url: '/pages/treatment-setup/treatment-setup' })
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"navigationBarTitleText": "自动扫描"
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<view class="container">
|
||||
<view class="card text-center">
|
||||
<view class="section-title">{{scanning ? '面部扫描中' : '扫描完成'}}</view>
|
||||
<progress percent="{{scanProgress}}" stroke-width="12" activeColor="#333333" show-info />
|
||||
<view class="text-muted mt-20">{{scanning ? '请保持设备贴合面部' : ''}}</view>
|
||||
</view>
|
||||
|
||||
<view class="card" wx:if="{{!scanning && regionData.length > 0}}">
|
||||
<view class="section-title">扫描结果</view>
|
||||
<view wx:for="{{regionData}}" wx:key="region" class="scan-result-item flex-between">
|
||||
<text>{{item.region}}</text>
|
||||
<text class="text-muted">{{item.pd}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="btn-area-fixed" wx:if="{{!scanning}}">
|
||||
<view class="btn-primary" bindtap="onNext">设置护理参数</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,8 @@
|
||||
.scan-result-item {
|
||||
padding: 16rpx 0;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.scan-result-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
Page({
|
||||
data: {
|
||||
deviceId: '',
|
||||
trialDays: 7
|
||||
},
|
||||
|
||||
onLoad: function (options) {
|
||||
this.setData({ deviceId: options.device_id || '' })
|
||||
},
|
||||
|
||||
onStart: function () {
|
||||
wx.redirectTo({ url: '/pages/wear-check/wear-check' })
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"navigationBarTitleText": "绑定成功"
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<view class="container">
|
||||
<view class="card text-center">
|
||||
<view class="success-icon">✓</view>
|
||||
<view class="section-title">绑定成功</view>
|
||||
<view class="text-muted mb-20">设备已成功绑定到您的账号</view>
|
||||
<view class="text-muted mb-30">已自动发放 {{trialDays}} 天试用</view>
|
||||
<view class="btn-primary" bindtap="onStart">开始使用</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,10 @@
|
||||
.success-icon {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
border-radius: 50%;
|
||||
background-color: #52c41a;
|
||||
color: #ffffff;
|
||||
font-size: 64rpx;
|
||||
line-height: 120rpx;
|
||||
margin: 40rpx auto;
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
var ble = require('../../services/ble')
|
||||
var app = getApp()
|
||||
|
||||
Page({
|
||||
data: {
|
||||
deviceId: '',
|
||||
bindToken: '',
|
||||
state: 'scanning',
|
||||
error: ''
|
||||
},
|
||||
|
||||
onLoad: function (options) {
|
||||
this.setData({
|
||||
deviceId: options.device_id || '',
|
||||
bindToken: options.bind_token || ''
|
||||
})
|
||||
this.startBleConnect()
|
||||
},
|
||||
|
||||
startBleConnect: function () {
|
||||
var self = this
|
||||
self.setData({ state: 'scanning', error: '' })
|
||||
|
||||
ble.startScan({
|
||||
onFound: function (device) {
|
||||
self.setData({ state: 'connecting' })
|
||||
},
|
||||
onConnected: function (device) {
|
||||
self.setData({ state: 'binding' })
|
||||
self.doBind()
|
||||
},
|
||||
onError: function (err) {
|
||||
self.setData({ state: 'error', error: err.msg || '连接失败' })
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
doBind: function () {
|
||||
var self = this
|
||||
var userId = app.globalData.userId || ''
|
||||
ble.on('bind_result', function (result) {
|
||||
if (result.success) {
|
||||
self.setData({ state: 'done' })
|
||||
wx.redirectTo({
|
||||
url: '/pages/bind-success/bind-success?device_id=' + self.data.deviceId
|
||||
})
|
||||
} else {
|
||||
self.setData({ state: 'error', error: '设备绑定失败' })
|
||||
}
|
||||
})
|
||||
|
||||
ble.bindDevice(userId, self.data.bindToken).catch(function (err) {
|
||||
self.setData({ state: 'error', error: err.error_msg || '绑定命令失败' })
|
||||
})
|
||||
},
|
||||
|
||||
onRetry: function () {
|
||||
ble.disconnect()
|
||||
this.startBleConnect()
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"navigationBarTitleText": "蓝牙连接"
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<view class="container">
|
||||
<view class="card text-center">
|
||||
<view class="section-title">
|
||||
{{state === 'scanning' ? '正在扫描设备...' : state === 'connecting' ? '正在连接设备...' : state === 'binding' ? '正在绑定设备...' : '连接失败'}}
|
||||
</view>
|
||||
|
||||
<view wx:if="{{state === 'scanning'}}" class="text-muted mb-30">
|
||||
请确保设备已开机且在附近
|
||||
</view>
|
||||
<view wx:if="{{state === 'connecting'}}" class="text-muted mb-30">
|
||||
设备ID: {{deviceId}}
|
||||
</view>
|
||||
<view wx:if="{{state === 'binding'}}" class="text-muted mb-30">
|
||||
正在写入绑定信息...
|
||||
</view>
|
||||
<view wx:if="{{state === 'error'}}" class="mb-30">
|
||||
<text class="text-error">{{error}}</text>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{state === 'scanning' || state === 'connecting' || state === 'binding'}}" class="loading-spinner">
|
||||
<view class="spinner"></view>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{state === 'error'}}">
|
||||
<view class="btn-primary" bindtap="onRetry">重试</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,18 @@
|
||||
.loading-spinner {
|
||||
padding: 40rpx 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
border: 6rpx solid #eeeeee;
|
||||
border-top-color: #333333;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
Page({
|
||||
data: {
|
||||
articles: []
|
||||
},
|
||||
|
||||
onLoad: function () {
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"navigationBarTitleText": "发现"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
<view class="container">
|
||||
<view class="section-title">发现</view>
|
||||
<view class="text-center text-muted mt-20">暂无内容</view>
|
||||
</view>
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -0,0 +1,64 @@
|
||||
var http = require('../../utils/request')
|
||||
var ble = require('../../services/ble')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
records: [],
|
||||
total: 0,
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
loading: true,
|
||||
loadingMore: false
|
||||
},
|
||||
|
||||
onShow: function () {
|
||||
this.loadRecords(true)
|
||||
},
|
||||
|
||||
onPullDownRefresh: function () {
|
||||
this.loadRecords(true).then(function () {
|
||||
wx.stopPullDownRefresh()
|
||||
})
|
||||
},
|
||||
|
||||
onReachBottom: function () {
|
||||
if (this.data.records.length < this.data.total) {
|
||||
this.loadRecords(false)
|
||||
}
|
||||
},
|
||||
|
||||
loadRecords: function (refresh) {
|
||||
var self = this
|
||||
var page = refresh ? 1 : self.data.page + 1
|
||||
|
||||
if (refresh) {
|
||||
self.setData({ loading: true })
|
||||
} else {
|
||||
self.setData({ loadingMore: true })
|
||||
}
|
||||
|
||||
return http.get('/api/v1/treatment/history', {
|
||||
page: page,
|
||||
page_size: self.data.pageSize
|
||||
}).then(function (data) {
|
||||
var records = (data.records || []).map(function (r) {
|
||||
var durationMin = Math.floor((r.total_duration_ms || 0) / 60000)
|
||||
r.duration_text = durationMin + '分钟'
|
||||
r.region_names = ble.getRegionName(r.regions || 0).join('、')
|
||||
r.wavelength_name = ble.getWavelengthName(r.wavelength || 2)
|
||||
r.date_text = r.start_time ? r.start_time.slice(0, 10) : ''
|
||||
return r
|
||||
})
|
||||
|
||||
self.setData({
|
||||
records: refresh ? records : self.data.records.concat(records),
|
||||
total: data.total || 0,
|
||||
page: page,
|
||||
loading: false,
|
||||
loadingMore: false
|
||||
})
|
||||
}).catch(function () {
|
||||
self.setData({ loading: false, loadingMore: false })
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"navigationBarTitleText": "护理记录"
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<view class="container">
|
||||
<view wx:if="{{loading}}" class="empty-state">
|
||||
<text class="text-muted">加载中...</text>
|
||||
</view>
|
||||
|
||||
<view wx:elif="{{records.length === 0}}" class="empty-state">
|
||||
<view class="empty-icon">📑</view>
|
||||
<view class="empty-text">暂无护理记录</view>
|
||||
</view>
|
||||
|
||||
<view wx:else>
|
||||
<view class="record-count text-muted mb-20">共 {{total}} 条记录</view>
|
||||
|
||||
<view wx:for="{{records}}" wx:key="record_id" class="card record-card">
|
||||
<view class="flex-between mb-10">
|
||||
<text class="record-date">{{item.date_text}}</text>
|
||||
<text class="record-duration">{{item.duration_text}}</text>
|
||||
</view>
|
||||
<view class="text-muted record-regions">{{item.region_names}}</view>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{loadingMore}}" class="text-center text-muted mt-20">
|
||||
加载更多...
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,22 @@
|
||||
.record-count {
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.record-card {
|
||||
padding: 24rpx 32rpx;
|
||||
}
|
||||
|
||||
.record-date {
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.record-duration {
|
||||
font-size: 26rpx;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.record-regions {
|
||||
font-size: 24rpx;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
var ble = require('../../services/ble')
|
||||
var mqtt = require('../../services/mqtt')
|
||||
var http = require('../../utils/request')
|
||||
var app = getApp()
|
||||
|
||||
Page({
|
||||
data: {
|
||||
connected: false,
|
||||
deviceName: '',
|
||||
battery: 0,
|
||||
subscription: null,
|
||||
subRemaining: 0,
|
||||
modeState: '空闲',
|
||||
bleState: 'disconnected',
|
||||
hasDevice: false,
|
||||
deviceInfo: null
|
||||
},
|
||||
|
||||
onShow: function () {
|
||||
this.checkState()
|
||||
ble.on('status', this.onBleStatus.bind(this))
|
||||
},
|
||||
|
||||
onHide: function () {
|
||||
ble.off('status')
|
||||
},
|
||||
|
||||
onUnload: function () {
|
||||
ble.off('status')
|
||||
},
|
||||
|
||||
checkState: function () {
|
||||
var self = this
|
||||
var token = wx.getStorageSync('token')
|
||||
if (!token) {
|
||||
wx.reLaunch({ url: '/pages/login/login' })
|
||||
return
|
||||
}
|
||||
|
||||
self.setData({ connected: ble.isConnected() })
|
||||
|
||||
http.get('/api/v1/device/list').then(function (data) {
|
||||
var devices = data.devices || []
|
||||
self.setData({ hasDevice: devices.length > 0 })
|
||||
if (devices.length > 0) {
|
||||
self.setData({
|
||||
deviceName: devices[0].device_name || '光子美容仪',
|
||||
deviceInfo: devices[0]
|
||||
})
|
||||
}
|
||||
}).catch(function () {})
|
||||
|
||||
http.get('/api/v1/subscription').then(function (sub) {
|
||||
self.setData({
|
||||
subscription: sub,
|
||||
subRemaining: sub.remaining_days || 0
|
||||
})
|
||||
}).catch(function () {})
|
||||
},
|
||||
|
||||
onBleStatus: function (status) {
|
||||
this.setData({
|
||||
battery: status.battery || 0,
|
||||
modeState: ble.getModeStateName(status.mode_state),
|
||||
connected: true,
|
||||
bleState: 'connected'
|
||||
})
|
||||
},
|
||||
|
||||
onConnectBle: function () {
|
||||
var self = this
|
||||
self.setData({ bleState: 'scanning' })
|
||||
|
||||
ble.startScan({
|
||||
onFound: function () {
|
||||
self.setData({ bleState: 'connecting' })
|
||||
},
|
||||
onConnected: function () {
|
||||
self.setData({ connected: true, bleState: 'connected' })
|
||||
ble.queryStatus().catch(function () {})
|
||||
},
|
||||
onError: function (err) {
|
||||
self.setData({ connected: false, bleState: 'error' })
|
||||
wx.showToast({ title: err.msg || '连接失败', icon: 'none' })
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
onAddDevice: function () {
|
||||
wx.navigateTo({ url: '/pages/scan/scan' })
|
||||
},
|
||||
|
||||
onStartTreatment: function () {
|
||||
if (!this.data.subscription || this.data.subscription.status === 0) {
|
||||
wx.navigateTo({ url: '/pages/subscribe-prompt/subscribe-prompt' })
|
||||
return
|
||||
}
|
||||
wx.navigateTo({ url: '/pages/wear-check/wear-check' })
|
||||
},
|
||||
|
||||
onViewSubscription: function () {
|
||||
if (!this.data.subscription || this.data.subscription.status === 0) {
|
||||
wx.navigateTo({ url: '/pages/subscribe-plans/subscribe-plans' })
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"navigationBarTitleText": "首页"
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<view class="container">
|
||||
<view class="card device-card" wx:if="{{!hasDevice}}">
|
||||
<view class="text-center">
|
||||
<view class="section-title">添加设备</view>
|
||||
<view class="text-muted mb-30">您还没有绑定设备</view>
|
||||
<view class="btn-primary" bindtap="onAddDevice">扫码添加设备</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="card device-card" wx:if="{{hasDevice}}">
|
||||
<view class="flex-between mb-20">
|
||||
<view>
|
||||
<view class="device-name">{{deviceName}}</view>
|
||||
<view class="device-status">
|
||||
<text class="{{connected ? 'text-success' : 'text-muted'}}">
|
||||
{{connected ? '已连接' : bleState === 'scanning' ? '扫描中...' : bleState === 'connecting' ? '连接中...' : '未连接'}}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
<view wx:if="{{connected}}" class="battery-area">
|
||||
<text>{{battery}}%</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{!connected && bleState !== 'scanning' && bleState !== 'connecting'}}" class="mt-20">
|
||||
<view class="btn-secondary" bindtap="onConnectBle">连接设备</view>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{connected}}" class="mt-20">
|
||||
<view class="text-muted">设备状态: {{modeState}}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="card sub-card" bindtap="onViewSubscription">
|
||||
<view class="flex-between">
|
||||
<view>
|
||||
<view class="section-title">订阅状态</view>
|
||||
<view class="text-muted">
|
||||
<block wx:if="{{subscription && subscription.status > 0}}">
|
||||
{{subscription.status === 1 ? '试用中' : '已订阅'}} · 剩余 {{subRemaining}} 天
|
||||
</block>
|
||||
<block wx:else>未订阅</block>
|
||||
</view>
|
||||
</view>
|
||||
<text class="arrow">❯</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="card" wx:if="{{connected && subscription && subscription.status > 0}}">
|
||||
<view class="btn-primary text-center" bindtap="onStartTreatment">开始护理</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,23 @@
|
||||
.device-name {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.device-status {
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.battery-area {
|
||||
font-size: 28rpx;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
font-size: 28rpx;
|
||||
color: #cccccc;
|
||||
}
|
||||
|
||||
.sub-card:active {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
var app = getApp()
|
||||
|
||||
Page({
|
||||
data: {
|
||||
loading: false
|
||||
},
|
||||
|
||||
onLogin: function () {
|
||||
var self = this
|
||||
self.setData({ loading: true })
|
||||
|
||||
app.doLogin().then(function () {
|
||||
self.setData({ loading: false })
|
||||
wx.switchTab({ url: '/pages/index/index' })
|
||||
}).catch(function (err) {
|
||||
self.setData({ loading: false })
|
||||
wx.showToast({ title: '登录失败', icon: 'none' })
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"navigationBarTitleText": "授权登录",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<view class="login-page">
|
||||
<view class="logo-area">
|
||||
<view class="logo-circle">
|
||||
<text class="logo-text">H</text>
|
||||
</view>
|
||||
<text class="app-name">光子美容仪</text>
|
||||
<text class="app-desc">智能护肤 专业管理</text>
|
||||
</view>
|
||||
|
||||
<view class="btn-area">
|
||||
<button class="btn-wechat" loading="{{loading}}" bindtap="onLogin" disabled="{{loading}}">
|
||||
微信一键登录
|
||||
</button>
|
||||
<text class="agreement">登录即同意《用户协议》和《隐私政策》</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,86 @@
|
||||
.login-page {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 0 60rpx;
|
||||
padding-top: 200rpx;
|
||||
padding-bottom: calc(80rpx + env(safe-area-inset-bottom));
|
||||
box-sizing: border-box;
|
||||
background: linear-gradient(180deg, #ffffff 0%, #f5f5f5 100%);
|
||||
}
|
||||
|
||||
.logo-area {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.logo-circle {
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
border-radius: 50%;
|
||||
background-color: #333333;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
font-size: 72rpx;
|
||||
color: #ffffff;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.app-name {
|
||||
font-size: 40rpx;
|
||||
font-weight: 600;
|
||||
color: #333333;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.app-desc {
|
||||
font-size: 26rpx;
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
.btn-area {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
padding: 40rpx 60rpx calc(60rpx + env(safe-area-inset-bottom));
|
||||
box-sizing: border-box;
|
||||
background: linear-gradient(180deg, rgba(245,245,245,0) 0%, #f5f5f5 30%);
|
||||
}
|
||||
|
||||
.btn-wechat {
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
line-height: 88rpx;
|
||||
background-color: #07c160;
|
||||
color: #ffffff;
|
||||
font-size: 32rpx;
|
||||
border-radius: 12rpx;
|
||||
border: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.btn-wechat::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.agreement {
|
||||
font-size: 22rpx;
|
||||
color: #999999;
|
||||
margin-top: 24rpx;
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
var http = require('../../utils/request')
|
||||
var app = getApp()
|
||||
|
||||
Page({
|
||||
data: {
|
||||
userInfo: null,
|
||||
subscription: null,
|
||||
deviceCount: 0,
|
||||
subRemaining: 0
|
||||
},
|
||||
|
||||
onShow: function () {
|
||||
this.loadProfile()
|
||||
},
|
||||
|
||||
loadProfile: function () {
|
||||
var self = this
|
||||
http.get('/api/v1/user/profile').then(function (profile) {
|
||||
self.setData({
|
||||
userInfo: profile,
|
||||
deviceCount: profile.device_count || 0
|
||||
})
|
||||
}).catch(function () {})
|
||||
|
||||
http.get('/api/v1/subscription').then(function (sub) {
|
||||
self.setData({
|
||||
subscription: sub,
|
||||
subRemaining: sub.remaining_days || 0
|
||||
})
|
||||
}).catch(function () {})
|
||||
},
|
||||
|
||||
onManageDevice: function () {
|
||||
wx.navigateTo({ url: '/pages/scan/scan' })
|
||||
},
|
||||
|
||||
onViewSubscription: function () {
|
||||
if (!this.data.subscription || this.data.subscription.status === 0) {
|
||||
wx.navigateTo({ url: '/pages/subscribe-plans/subscribe-plans' })
|
||||
} else {
|
||||
wx.navigateTo({ url: '/pages/subscribe-prompt/subscribe-prompt' })
|
||||
}
|
||||
},
|
||||
|
||||
onViewHistory: function () {
|
||||
wx.switchTab({ url: '/pages/history/history' })
|
||||
},
|
||||
|
||||
onLogout: function () {
|
||||
wx.showModal({
|
||||
title: '退出登录',
|
||||
content: '确定要退出登录吗?',
|
||||
success: function (res) {
|
||||
if (res.confirm) {
|
||||
wx.removeStorageSync('token')
|
||||
wx.reLaunch({ url: '/pages/login/login' })
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"navigationBarTitleText": "我的"
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<view class="container">
|
||||
<view class="card profile-header">
|
||||
<view class="avatar-area">
|
||||
<image wx:if="{{userInfo && userInfo.avatar}}" src="{{userInfo.avatar}}" class="avatar" />
|
||||
<view wx:else class="avatar-placeholder">{{(userInfo && userInfo.nickname ? userInfo.nickname[0] : '?')}}</view>
|
||||
</view>
|
||||
<view class="user-info">
|
||||
<view class="nickname">{{userInfo && userInfo.nickname || '未设置昵称'}}</view>
|
||||
<view class="text-muted">ID: {{userInfo && userInfo.user_id || ''}}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="card">
|
||||
<view class="menu-item" bindtap="onManageDevice">
|
||||
<text>设备管理</text>
|
||||
<view class="flex-row">
|
||||
<text class="text-muted">{{deviceCount}} 台设备</text>
|
||||
<text class="arrow-sm">❯</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="divider"></view>
|
||||
<view class="menu-item" bindtap="onViewSubscription">
|
||||
<text>订阅管理</text>
|
||||
<view class="flex-row">
|
||||
<text class="text-muted">
|
||||
{{subscription && subscription.status > 0 ? '剩余 ' + subRemaining + ' 天' : '未订阅'}}
|
||||
</text>
|
||||
<text class="arrow-sm">❯</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="divider"></view>
|
||||
<view class="menu-item" bindtap="onViewHistory">
|
||||
<text>护理记录</text>
|
||||
<text class="arrow-sm">❯</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="card">
|
||||
<view class="menu-item logout" bindtap="onLogout">
|
||||
<text>退出登录</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,60 @@
|
||||
.profile-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 40rpx 32rpx;
|
||||
}
|
||||
|
||||
.avatar-area {
|
||||
margin-right: 24rpx;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.avatar-placeholder {
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
border-radius: 50%;
|
||||
background-color: #333333;
|
||||
color: #ffffff;
|
||||
font-size: 40rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.nickname {
|
||||
font-size: 34rpx;
|
||||
font-weight: 600;
|
||||
margin-bottom: 4rpx;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 24rpx 0;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.menu-item:active {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.arrow-sm {
|
||||
font-size: 24rpx;
|
||||
color: #cccccc;
|
||||
margin-left: 8rpx;
|
||||
}
|
||||
|
||||
.logout {
|
||||
color: #ff4d4f;
|
||||
justify-content: center;
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
var http = require('../../utils/request')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
scanning: false,
|
||||
deviceId: '',
|
||||
error: ''
|
||||
},
|
||||
|
||||
onScan: function () {
|
||||
var self = this
|
||||
self.setData({ scanning: true, error: '' })
|
||||
|
||||
wx.scanCode({
|
||||
onlyFromCamera: true,
|
||||
scanType: ['qrCode'],
|
||||
success: function (res) {
|
||||
var deviceId = self.parseDeviceId(res.result)
|
||||
if (!deviceId) {
|
||||
self.setData({ scanning: false, error: '无效的设备二维码' })
|
||||
return
|
||||
}
|
||||
self.bindDevice(deviceId)
|
||||
},
|
||||
fail: function () {
|
||||
self.setData({ scanning: false })
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
parseDeviceId: function (content) {
|
||||
if (/^[0-9A-Fa-f]{16}$/.test(content)) return content
|
||||
try {
|
||||
var url = new URL(content)
|
||||
var id = url.searchParams.get('device_id') || url.searchParams.get('id')
|
||||
return id || null
|
||||
} catch (e) {
|
||||
return null
|
||||
}
|
||||
},
|
||||
|
||||
bindDevice: function (deviceId) {
|
||||
var self = this
|
||||
http.post('/api/v1/device/bind', { device_id: deviceId }).then(function (data) {
|
||||
self.setData({ scanning: false })
|
||||
wx.navigateTo({
|
||||
url: '/pages/ble-connect/ble-connect?device_id=' + deviceId + '&bind_token=' + data.bind_token
|
||||
})
|
||||
}).catch(function (err) {
|
||||
self.setData({
|
||||
scanning: false,
|
||||
error: err.message || '绑定失败'
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"navigationBarTitleText": "扫码绑定"
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<view class="container">
|
||||
<view class="card text-center">
|
||||
<view class="section-title">绑定新设备</view>
|
||||
<view class="text-muted mb-30">请扫描设备底部或包装盒上的二维码</view>
|
||||
<view class="btn-primary" bindtap="onScan" disabled="{{scanning}}">
|
||||
{{scanning ? '扫描中...' : '扫描二维码'}}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="card" wx:if="{{error}}">
|
||||
<text class="text-error">{{error}}</text>
|
||||
</view>
|
||||
|
||||
<view class="card">
|
||||
<view class="section-title">绑定步骤</view>
|
||||
<view class="step">
|
||||
<text class="step-num">1</text>
|
||||
<text class="step-text">扫描设备二维码获取设备ID</text>
|
||||
</view>
|
||||
<view class="step">
|
||||
<text class="step-num">2</text>
|
||||
<text class="step-text">打开蓝牙连接设备</text>
|
||||
</view>
|
||||
<view class="step">
|
||||
<text class="step-num">3</text>
|
||||
<text class="step-text">绑定成功,开始使用</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,23 @@
|
||||
.step {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16rpx 0;
|
||||
}
|
||||
|
||||
.step-num {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
border-radius: 50%;
|
||||
background-color: #333333;
|
||||
color: #ffffff;
|
||||
font-size: 24rpx;
|
||||
text-align: center;
|
||||
line-height: 48rpx;
|
||||
margin-right: 20rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.step-text {
|
||||
font-size: 28rpx;
|
||||
color: #333333;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
var http = require('../../utils/request')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
plans: [
|
||||
{ name: '月度套餐', plan: 'monthly', days: 30, price: '¥29.9', desc: '30天畅享' },
|
||||
{ name: '季度套餐', plan: 'quarterly', days: 90, price: '¥79.9', desc: '90天畅享' },
|
||||
{ name: '年度套餐', plan: 'yearly', days: 365, price: '¥269', desc: '365天畅享' }
|
||||
],
|
||||
selected: null,
|
||||
purchasing: false
|
||||
},
|
||||
|
||||
onSelect: function (e) {
|
||||
this.setData({ selected: e.currentTarget.dataset.plan })
|
||||
},
|
||||
|
||||
onPurchase: function () {
|
||||
var self = this
|
||||
if (!self.data.selected) {
|
||||
wx.showToast({ title: '请选择套餐', icon: 'none' })
|
||||
return
|
||||
}
|
||||
self.setData({ purchasing: true })
|
||||
|
||||
http.post('/api/v1/subscription/purchase', {
|
||||
plan: self.data.selected,
|
||||
payment_method: 'wechat'
|
||||
}).then(function (data) {
|
||||
return http.post('/api/v1/subscription/verify', {
|
||||
order_id: data.order_id,
|
||||
plan: self.data.selected
|
||||
})
|
||||
}).then(function () {
|
||||
self.setData({ purchasing: false })
|
||||
wx.redirectTo({ url: '/pages/subscribe-success/subscribe-success' })
|
||||
}).catch(function () {
|
||||
self.setData({ purchasing: false })
|
||||
wx.showToast({ title: '购买失败', icon: 'none' })
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"navigationBarTitleText": "订阅套餐"
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<view class="container">
|
||||
<view wx:for="{{plans}}" wx:key="plan"
|
||||
class="card plan-card {{selected === item.plan ? 'plan-active' : ''}}"
|
||||
bindtap="onSelect" data-plan="{{item.plan}}">
|
||||
<view class="plan-name">{{item.name}}</view>
|
||||
<view class="plan-price">{{item.price}}</view>
|
||||
<view class="plan-desc text-muted">{{item.desc}}</view>
|
||||
</view>
|
||||
|
||||
<view class="btn-area-fixed">
|
||||
<view class="btn-primary" bindtap="onPurchase" disabled="{{purchasing || !selected}}">
|
||||
{{purchasing ? '处理中...' : '立即订阅'}}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,27 @@
|
||||
.plan-card {
|
||||
text-align: center;
|
||||
border: 2rpx solid #eeeeee;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.plan-active {
|
||||
border-color: #333333;
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
.plan-name {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.plan-price {
|
||||
font-size: 48rpx;
|
||||
font-weight: 700;
|
||||
color: #333333;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.plan-desc {
|
||||
font-size: 24rpx;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
Page({
|
||||
data: {},
|
||||
|
||||
onViewPlans: function () {
|
||||
wx.navigateTo({ url: '/pages/subscribe-plans/subscribe-plans' })
|
||||
},
|
||||
|
||||
onBack: function () {
|
||||
wx.switchTab({ url: '/pages/index/index' })
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"navigationBarTitleText": "订阅提示"
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<view class="container">
|
||||
<view class="card text-center">
|
||||
<view class="lock-icon">🔒</view>
|
||||
<view class="section-title">需要订阅</view>
|
||||
<view class="text-muted mb-30">您的试用已到期,请订阅后继续使用</view>
|
||||
<view class="btn-primary mb-20" bindtap="onViewPlans">查看套餐</view>
|
||||
<view class="btn-secondary" bindtap="onBack">返回首页</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,4 @@
|
||||
.lock-icon {
|
||||
font-size: 80rpx;
|
||||
margin: 40rpx auto;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
Page({
|
||||
data: {},
|
||||
|
||||
onBackHome: function () {
|
||||
wx.switchTab({ url: '/pages/index/index' })
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"navigationBarTitleText": "订阅成功"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<view class="container">
|
||||
<view class="card text-center">
|
||||
<view class="success-icon">✓</view>
|
||||
<view class="section-title">订阅成功</view>
|
||||
<view class="text-muted mb-30">您已成功订阅,现在可以开始使用</view>
|
||||
<view class="btn-primary" bindtap="onBackHome">返回首页</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,11 @@
|
||||
.success-icon {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
border-radius: 50%;
|
||||
background-color: #52c41a;
|
||||
color: #ffffff;
|
||||
font-size: 72rpx;
|
||||
line-height: 120rpx;
|
||||
text-align: center;
|
||||
margin: 40rpx auto;
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
var ble = require('../../services/ble')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
regions: 0,
|
||||
wavelength: 2,
|
||||
duration: 600000,
|
||||
mode: 0,
|
||||
remainingMs: 600000,
|
||||
remainingText: '10:00',
|
||||
battery: 0,
|
||||
temperature: 0,
|
||||
progress: 0,
|
||||
paused: false,
|
||||
completed: false
|
||||
},
|
||||
|
||||
onLoad: function (options) {
|
||||
this.setData({
|
||||
regions: parseInt(options.regions) || 0x7F,
|
||||
wavelength: parseInt(options.wavelength) || 2,
|
||||
duration: parseInt(options.duration) || 600000,
|
||||
mode: parseInt(options.mode) || 0,
|
||||
remainingMs: parseInt(options.duration) || 600000
|
||||
})
|
||||
|
||||
ble.on('status', this.onStatus.bind(this))
|
||||
ble.on('treatment_complete', this.onComplete.bind(this))
|
||||
ble.on('exception', this.onException.bind(this))
|
||||
},
|
||||
|
||||
onUnload: function () {
|
||||
ble.off('status')
|
||||
ble.off('treatment_complete')
|
||||
ble.off('exception')
|
||||
},
|
||||
|
||||
onStatus: function (status) {
|
||||
var remaining = status.remaining_ms || 0
|
||||
var total = this.data.duration
|
||||
var progress = total > 0 ? Math.round(((total - remaining) / total) * 100) : 0
|
||||
var mins = Math.floor(remaining / 60000)
|
||||
var secs = Math.floor((remaining % 60000) / 1000)
|
||||
var text = ('0' + mins).slice(-2) + ':' + ('0' + secs).slice(-2)
|
||||
|
||||
this.setData({
|
||||
remainingMs: remaining,
|
||||
remainingText: text,
|
||||
progress: progress,
|
||||
battery: status.battery,
|
||||
temperature: status.temperature,
|
||||
paused: status.mode_state === 0x03
|
||||
})
|
||||
},
|
||||
|
||||
onComplete: function (result) {
|
||||
this.setData({ completed: true, progress: 100 })
|
||||
var app = getApp()
|
||||
app.globalData.currentTreatment = result
|
||||
|
||||
setTimeout(function () {
|
||||
wx.redirectTo({
|
||||
url: '/pages/treatment-done/treatment-done?session_id=' + result.session_id +
|
||||
'®ions=' + result.regions +
|
||||
'&duration=' + result.total_duration_ms +
|
||||
'&avg_pd=' + result.avg_pd
|
||||
})
|
||||
}, 1000)
|
||||
},
|
||||
|
||||
onException: function (err) {
|
||||
wx.showModal({
|
||||
title: '设备异常',
|
||||
content: err.error_msg || '护理过程中出现异常',
|
||||
showCancel: false,
|
||||
complete: function () {
|
||||
wx.navigateBack()
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
onStop: function () {
|
||||
var self = this
|
||||
wx.showModal({
|
||||
title: '结束护理',
|
||||
content: '确定要提前结束本次护理吗?',
|
||||
success: function (res) {
|
||||
if (res.confirm) {
|
||||
ble.stopTreatment().then(function () {
|
||||
wx.navigateBack()
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"navigationBarTitleText": "护理中"
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<view class="container treating-page">
|
||||
<view class="card text-center">
|
||||
<view class="progress-circle">
|
||||
<text class="progress-text">{{remainingText}}</text>
|
||||
</view>
|
||||
<view class="progress-bar-wrap mt-20">
|
||||
<progress percent="{{progress}}" stroke-width="8" activeColor="#333333" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="card">
|
||||
<view class="flex-between mb-20">
|
||||
<text>波长</text>
|
||||
<text class="text-muted">{{wavelength === 1 ? '红外 850nm' : wavelength === 2 ? '红光 630nm' : wavelength === 3 ? '紫光 405nm' : '黄光 590nm'}}</text>
|
||||
</view>
|
||||
<view class="flex-between mb-20">
|
||||
<text>模式</text>
|
||||
<text class="text-muted">{{mode === 1 ? '智能模式' : '普通模式'}}</text>
|
||||
</view>
|
||||
<view class="flex-between mb-20">
|
||||
<text>电量</text>
|
||||
<text class="text-muted">{{battery}}%</text>
|
||||
</view>
|
||||
<view class="flex-between">
|
||||
<text>温度</text>
|
||||
<text class="text-muted">{{temperature}}°C</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="stop-btn-area">
|
||||
<view class="btn-stop" bindtap="onStop">结束护理</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,37 @@
|
||||
.treating-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.progress-circle {
|
||||
width: 240rpx;
|
||||
height: 240rpx;
|
||||
border-radius: 50%;
|
||||
border: 12rpx solid #eeeeee;
|
||||
border-top-color: #333333;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 40rpx auto;
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
font-size: 56rpx;
|
||||
font-weight: 700;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.stop-btn-area {
|
||||
margin-top: auto;
|
||||
padding: 20rpx 0 60rpx;
|
||||
}
|
||||
|
||||
.btn-stop {
|
||||
background-color: #ff4d4f;
|
||||
color: #ffffff;
|
||||
border-radius: 12rpx;
|
||||
padding: 24rpx 40rpx;
|
||||
text-align: center;
|
||||
font-size: 30rpx;
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
var http = require('../../utils/request')
|
||||
var ble = require('../../services/ble')
|
||||
var app = getApp()
|
||||
|
||||
Page({
|
||||
data: {
|
||||
sessionId: '',
|
||||
regions: 0,
|
||||
duration: 0,
|
||||
avgPd: 0,
|
||||
durationText: '',
|
||||
regionNames: [],
|
||||
syncing: false,
|
||||
synced: false
|
||||
},
|
||||
|
||||
onLoad: function (options) {
|
||||
var durationMs = parseInt(options.duration) || 0
|
||||
var mins = Math.floor(durationMs / 60000)
|
||||
var secs = Math.floor((durationMs % 60000) / 1000)
|
||||
|
||||
this.setData({
|
||||
sessionId: options.session_id || '',
|
||||
regions: parseInt(options.regions) || 0,
|
||||
duration: durationMs,
|
||||
avgPd: options.avg_pd || 0,
|
||||
durationText: mins + '分' + secs + '秒',
|
||||
regionNames: ble.getRegionName(parseInt(options.regions) || 0)
|
||||
})
|
||||
|
||||
this.syncRecord()
|
||||
},
|
||||
|
||||
syncRecord: function () {
|
||||
var self = this
|
||||
self.setData({ syncing: true })
|
||||
|
||||
http.post('/api/v1/treatment/sync', {
|
||||
session_id: self.data.sessionId,
|
||||
device_id: ble.getDeviceId(),
|
||||
start_time: new Date(Date.now() - self.data.duration).toISOString(),
|
||||
end_time: new Date().toISOString(),
|
||||
regions: self.data.regions,
|
||||
total_duration_ms: self.data.duration,
|
||||
mode: 0,
|
||||
avg_pd: self.data.avgPd
|
||||
}).then(function () {
|
||||
self.setData({ syncing: false, synced: true })
|
||||
}).catch(function () {
|
||||
self.setData({ syncing: false, synced: false })
|
||||
})
|
||||
},
|
||||
|
||||
onBackHome: function () {
|
||||
wx.switchTab({ url: '/pages/index/index' })
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"navigationBarTitleText": "护理完成"
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<view class="container">
|
||||
<view class="card text-center">
|
||||
<view class="done-icon">✓</view>
|
||||
<view class="section-title">护理完成</view>
|
||||
</view>
|
||||
|
||||
<view class="card">
|
||||
<view class="flex-between mb-20">
|
||||
<text>护理时长</text>
|
||||
<text>{{durationText}}</text>
|
||||
</view>
|
||||
<view class="flex-between mb-20">
|
||||
<text>护理区域</text>
|
||||
<text class="text-muted">{{regionNames.join('、')}}</text>
|
||||
</view>
|
||||
<view class="flex-between">
|
||||
<text>平均光功率密度</text>
|
||||
<text>{{avgPd}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="card text-center">
|
||||
<view wx:if="{{syncing}}" class="text-muted">
|
||||
<view class="spinner-sm"></view>
|
||||
<text>正在同步记录...</text>
|
||||
</view>
|
||||
<view wx:elif="{{synced}}" class="text-success">记录已同步</view>
|
||||
<view wx:else class="text-error">同步失败,可稍后在护理记录中重试</view>
|
||||
</view>
|
||||
|
||||
<view class="btn-area-fixed">
|
||||
<view class="btn-primary" bindtap="onBackHome">返回首页</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,25 @@
|
||||
.done-icon {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
border-radius: 50%;
|
||||
background-color: #52c41a;
|
||||
color: #ffffff;
|
||||
font-size: 72rpx;
|
||||
line-height: 120rpx;
|
||||
text-align: center;
|
||||
margin: 40rpx auto;
|
||||
}
|
||||
|
||||
.spinner-sm {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
border: 4rpx solid #eeeeee;
|
||||
border-top-color: #333333;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
margin: 0 auto 12rpx;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
var ble = require('../../services/ble')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
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: 0x20, checked: false },
|
||||
{ name: '右眼周', mask: 0x40, checked: false }
|
||||
],
|
||||
wavelengthOptions: [
|
||||
{ name: '红光 630nm', value: 2, desc: '抗衰修复' },
|
||||
{ name: '红外 850nm', value: 1, desc: '深层修复' },
|
||||
{ name: '紫光 405nm', value: 3, desc: '祛痘消炎' },
|
||||
{ name: '黄光 590nm', value: 4, desc: '提亮肤色' }
|
||||
],
|
||||
selectedWavelength: 2,
|
||||
brightness: 200,
|
||||
durationOptions: [
|
||||
{ label: '5分钟', value: 300000 },
|
||||
{ label: '10分钟', value: 600000 },
|
||||
{ label: '15分钟', value: 900000 },
|
||||
{ label: '20分钟', value: 1200000 }
|
||||
],
|
||||
selectedDuration: 600000,
|
||||
modeOptions: [
|
||||
{ name: '普通模式', value: 0 },
|
||||
{ name: '智能模式', value: 1 }
|
||||
],
|
||||
selectedMode: 0,
|
||||
submitting: false,
|
||||
error: ''
|
||||
},
|
||||
|
||||
onToggleRegion: function (e) {
|
||||
var idx = e.currentTarget.dataset.idx
|
||||
var regions = this.data.regions
|
||||
regions[idx].checked = !regions[idx].checked
|
||||
this.setData({ regions: regions })
|
||||
},
|
||||
|
||||
onSelectAll: function () {
|
||||
var regions = this.data.regions.map(function (r) {
|
||||
return Object.assign({}, r, { checked: true })
|
||||
})
|
||||
this.setData({ regions: regions })
|
||||
},
|
||||
|
||||
onSelectWavelength: function (e) {
|
||||
this.setData({ selectedWavelength: e.currentTarget.dataset.value })
|
||||
},
|
||||
|
||||
onBrightnessChange: function (e) {
|
||||
this.setData({ brightness: parseInt(e.detail.value) })
|
||||
},
|
||||
|
||||
onSelectDuration: function (e) {
|
||||
this.setData({ selectedDuration: e.currentTarget.dataset.value })
|
||||
},
|
||||
|
||||
onSelectMode: function (e) {
|
||||
this.setData({ selectedMode: e.currentTarget.dataset.value })
|
||||
},
|
||||
|
||||
onStart: function () {
|
||||
var self = this
|
||||
var mask = 0
|
||||
self.data.regions.forEach(function (r) {
|
||||
if (r.checked) mask |= r.mask
|
||||
})
|
||||
|
||||
if (mask === 0) {
|
||||
wx.showToast({ title: '请至少选择一个区域', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
self.setData({ submitting: true, error: '' })
|
||||
|
||||
ble.setParams({
|
||||
region_mask: mask,
|
||||
wavelength: self.data.selectedWavelength,
|
||||
brightness: self.data.brightness,
|
||||
duration_ms: self.data.selectedDuration,
|
||||
mode: self.data.selectedMode
|
||||
}).then(function () {
|
||||
return ble.startTreatment(mask)
|
||||
}).then(function () {
|
||||
self.setData({ submitting: false })
|
||||
wx.redirectTo({
|
||||
url: '/pages/treating/treating?regions=' + mask +
|
||||
'&wavelength=' + self.data.selectedWavelength +
|
||||
'&duration=' + self.data.selectedDuration +
|
||||
'&mode=' + self.data.selectedMode
|
||||
})
|
||||
}).catch(function (err) {
|
||||
self.setData({ submitting: false, error: err.error_msg || '启动失败' })
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"navigationBarTitleText": "护理设置"
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<view class="container">
|
||||
<view class="card">
|
||||
<view class="section-title">护理区域</view>
|
||||
<view class="region-grid">
|
||||
<view wx:for="{{regions}}" wx:key="mask" class="region-item {{item.checked ? 'region-active' : ''}}"
|
||||
bindtap="onToggleRegion" data-idx="{{index}}">
|
||||
<text>{{item.name}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="btn-select-all mt-10" bindtap="onSelectAll">全选</view>
|
||||
</view>
|
||||
|
||||
<view class="card">
|
||||
<view class="section-title">波长选择</view>
|
||||
<view class="wavelength-list">
|
||||
<view wx:for="{{wavelengthOptions}}" wx:key="value"
|
||||
class="wavelength-item {{selectedWavelength === item.value ? 'wavelength-active' : ''}}"
|
||||
bindtap="onSelectWavelength" data-value="{{item.value}}">
|
||||
<text class="wavelength-name">{{item.name}}</text>
|
||||
<text class="wavelength-desc">{{item.desc}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="card">
|
||||
<view class="section-title">亮度: {{brightness}}</view>
|
||||
<slider min="0" max="255" value="{{brightness}}" bindchange="onBrightnessChange"
|
||||
activeColor="#333333" block-size="20" />
|
||||
</view>
|
||||
|
||||
<view class="card">
|
||||
<view class="section-title">护理时长</view>
|
||||
<view class="duration-list">
|
||||
<view wx:for="{{durationOptions}}" wx:key="value"
|
||||
class="duration-item {{selectedDuration === item.value ? 'duration-active' : ''}}"
|
||||
bindtap="onSelectDuration" data-value="{{item.value}}">
|
||||
{{item.label}}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="card">
|
||||
<view class="section-title">护理模式</view>
|
||||
<view class="mode-list">
|
||||
<view wx:for="{{modeOptions}}" wx:key="value"
|
||||
class="mode-item {{selectedMode === item.value ? 'mode-active' : ''}}"
|
||||
bindtap="onSelectMode" data-value="{{item.value}}">
|
||||
{{item.name}}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{error}}" class="card">
|
||||
<text class="text-error">{{error}}</text>
|
||||
</view>
|
||||
|
||||
<view class="btn-area-fixed">
|
||||
<view class="btn-primary" bindtap="onStart" disabled="{{submitting}}">
|
||||
{{submitting ? '启动中...' : '开始护理'}}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,98 @@
|
||||
.region-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.region-item {
|
||||
padding: 12rpx 24rpx;
|
||||
border-radius: 8rpx;
|
||||
background-color: #f0f0f0;
|
||||
font-size: 26rpx;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.region-active {
|
||||
background-color: #333333;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.btn-select-all {
|
||||
font-size: 24rpx;
|
||||
color: #333333;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.wavelength-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.wavelength-item {
|
||||
padding: 20rpx;
|
||||
border: 2rpx solid #eeeeee;
|
||||
border-radius: 8rpx;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.wavelength-active {
|
||||
border-color: #333333;
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
.wavelength-name {
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.wavelength-desc {
|
||||
font-size: 24rpx;
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
.duration-list {
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.duration-item {
|
||||
flex: 1;
|
||||
padding: 16rpx;
|
||||
text-align: center;
|
||||
border: 2rpx solid #eeeeee;
|
||||
border-radius: 8rpx;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.duration-active {
|
||||
border-color: #333333;
|
||||
background-color: #333333;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.mode-list {
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.mode-item {
|
||||
flex: 1;
|
||||
padding: 16rpx;
|
||||
text-align: center;
|
||||
border: 2rpx solid #eeeeee;
|
||||
border-radius: 8rpx;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.mode-active {
|
||||
border-color: #333333;
|
||||
background-color: #333333;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.btn-area-fixed {
|
||||
padding: 20rpx 0 40rpx;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
var ble = require('../../services/ble')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
checking: false,
|
||||
result: null,
|
||||
error: ''
|
||||
},
|
||||
|
||||
onShow: function () {
|
||||
this.checkWearing()
|
||||
},
|
||||
|
||||
checkWearing: function () {
|
||||
var self = this
|
||||
self.setData({ checking: true, result: null, error: '' })
|
||||
|
||||
ble.on('status', function (status) {
|
||||
self.setData({ checking: false })
|
||||
if (status.bind_status === 1) {
|
||||
self.setData({ result: 'ok' })
|
||||
} else {
|
||||
self.setData({ result: 'fail', error: '请确认设备已正确佩戴' })
|
||||
}
|
||||
})
|
||||
|
||||
ble.queryStatus().catch(function (err) {
|
||||
self.setData({ checking: false, result: 'fail', error: err.error_msg || '查询失败' })
|
||||
})
|
||||
},
|
||||
|
||||
onNext: function () {
|
||||
wx.navigateTo({ url: '/pages/treatment-setup/treatment-setup' })
|
||||
},
|
||||
|
||||
onRetry: function () {
|
||||
this.checkWearing()
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"navigationBarTitleText": "确认佩戴"
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<view class="container">
|
||||
<view class="card text-center">
|
||||
<view class="section-title">确认佩戴</view>
|
||||
|
||||
<view wx:if="{{checking}}" class="text-muted mb-30">
|
||||
<view class="loading-area">
|
||||
<view class="spinner-sm"></view>
|
||||
<text>正在检测佩戴状态...</text>
|
||||
</view>
|
||||
<view class="tip-area mt-20">
|
||||
<text class="text-muted">请将设备贴合面部,确保佩戴稳固</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{result === 'ok'}}">
|
||||
<view class="success-mark">✓</view>
|
||||
<view class="text-success mb-30">佩戴确认成功</view>
|
||||
<view class="btn-primary" bindtap="onNext">设置护理参数</view>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{result === 'fail'}}">
|
||||
<view class="fail-mark">✗</view>
|
||||
<view class="text-error mb-20">{{error}}</view>
|
||||
<view class="btn-primary" bindtap="onRetry">重新检测</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,50 @@
|
||||
.loading-area {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 40rpx 0;
|
||||
}
|
||||
|
||||
.spinner-sm {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
border: 4rpx solid #eeeeee;
|
||||
border-top-color: #333333;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.success-mark {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
border-radius: 50%;
|
||||
background-color: #52c41a;
|
||||
color: #ffffff;
|
||||
font-size: 48rpx;
|
||||
line-height: 80rpx;
|
||||
text-align: center;
|
||||
margin: 20rpx auto;
|
||||
}
|
||||
|
||||
.fail-mark {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
border-radius: 50%;
|
||||
background-color: #ff4d4f;
|
||||
color: #ffffff;
|
||||
font-size: 48rpx;
|
||||
line-height: 80rpx;
|
||||
text-align: center;
|
||||
margin: 20rpx auto;
|
||||
}
|
||||
|
||||
.tip-area {
|
||||
padding: 20rpx;
|
||||
background-color: #fffbe6;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
在新工单中引用
屏蔽一个用户