feat: init project with miniprogram, cloud functions and admin console

这个提交包含在:
Guoguo
2026-04-22 21:24:20 +08:00
当前提交 a84e37a6e2
修改 106 个文件,包含 5902 行新增0 行删除
+57
查看文件
@@ -0,0 +1,57 @@
var http = require('./utils/request')
App({
globalData: {
userInfo: null,
userId: null,
connectedDevice: null,
currentTreatment: null
},
onLaunch: function () {
this.checkLogin()
},
checkLogin: function () {
var token = wx.getStorageSync('token')
if (!token) {
this.globalData.userInfo = null
return
}
this.loadProfile()
},
loadProfile: function () {
var self = this
http.get('/api/v1/user/profile').then(function (profile) {
self.globalData.userInfo = profile
self.globalData.userId = String(profile.user_id)
}).catch(function () {
wx.removeStorageSync('token')
wx.reLaunch({ url: '/pages/login/login' })
})
},
doLogin: function () {
return new Promise(function (resolve, reject) {
wx.login({
success: function (res) {
if (!res.code) {
reject({ message: 'wx.login failed' })
return
}
http.post('/api/v1/auth/login', {
code: res.code
}).then(function (data) {
wx.setStorageSync('token', data.token)
var app = getApp()
app.globalData.userInfo = data.user_info
app.globalData.userId = data.user_id
resolve(data)
}).catch(reject)
},
fail: reject
})
})
}
})
+62
查看文件
@@ -0,0 +1,62 @@
{
"pages": [
"pages/index/index",
"pages/login/login",
"pages/scan/scan",
"pages/ble-connect/ble-connect",
"pages/bind-success/bind-success",
"pages/wear-check/wear-check",
"pages/treatment-setup/treatment-setup",
"pages/auto-scan/auto-scan",
"pages/treating/treating",
"pages/treatment-done/treatment-done",
"pages/subscribe-prompt/subscribe-prompt",
"pages/subscribe-plans/subscribe-plans",
"pages/subscribe-success/subscribe-success",
"pages/profile/profile",
"pages/history/history"
],
"window": {
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTitleText": "光子美容仪",
"navigationBarTextStyle": "black",
"backgroundColor": "#f5f5f5"
},
"tabBar": {
"color": "#999999",
"selectedColor": "#333333",
"backgroundColor": "#ffffff",
"borderStyle": "black",
"list": [
{
"pagePath": "pages/index/index",
"text": "首页"
},
{
"pagePath": "pages/history/history",
"text": "护理记录"
},
{
"pagePath": "pages/profile/profile",
"text": "我的"
}
]
},
"permission": {
"scope.bluetooth": {
"desc": "用于连接光子美容仪设备"
},
"scope.userLocation": {
"desc": "蓝牙连接需要位置权限"
},
"scope.camera": {
"desc": "用于扫描设备二维码"
}
},
"requiredPrivateInfos": [
"getLocation",
"chooseLocation"
],
"style": "v2",
"sitemapLocation": "sitemap.json"
}
+144
查看文件
@@ -0,0 +1,144 @@
page {
background-color: #f5f5f5;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
font-size: 28rpx;
color: #333333;
}
.container {
padding: 24rpx;
min-height: 100vh;
}
.section-title {
font-size: 32rpx;
font-weight: 600;
margin-bottom: 20rpx;
}
.card {
background-color: #ffffff;
border-radius: 16rpx;
padding: 32rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
}
.btn-primary {
background-color: #333333;
color: #ffffff;
border-radius: 12rpx;
padding: 24rpx 40rpx;
text-align: center;
font-size: 30rpx;
font-weight: 500;
}
.btn-primary:active {
opacity: 0.8;
}
.btn-secondary {
background-color: #ffffff;
color: #333333;
border: 2rpx solid #333333;
border-radius: 12rpx;
padding: 24rpx 40rpx;
text-align: center;
font-size: 30rpx;
}
.text-muted {
color: #999999;
}
.text-center {
text-align: center;
}
.text-primary {
color: #333333;
}
.text-success {
color: #52c41a;
}
.text-warning {
color: #faad14;
}
.text-error {
color: #ff4d4f;
}
.mt-10 { margin-top: 10rpx; }
.mt-20 { margin-top: 20rpx; }
.mt-30 { margin-top: 30rpx; }
.mb-10 { margin-bottom: 10rpx; }
.mb-20 { margin-bottom: 20rpx; }
.mb-30 { margin-bottom: 30rpx; }
.p-20 { padding: 20rpx; }
.flex-row {
display: flex;
flex-direction: row;
align-items: center;
}
.flex-between {
display: flex;
justify-content: space-between;
align-items: center;
}
.flex-center {
display: flex;
justify-content: center;
align-items: center;
}
.flex-1 {
flex: 1;
}
.divider {
height: 1rpx;
background-color: #eeeeee;
margin: 20rpx 0;
}
.badge {
display: inline-block;
padding: 4rpx 16rpx;
border-radius: 8rpx;
font-size: 22rpx;
color: #ffffff;
}
.badge-success {
background-color: #52c41a;
}
.badge-error {
background-color: #ff4d4f;
}
.badge-warning {
background-color: #faad14;
}
.empty-state {
padding: 120rpx 40rpx;
text-align: center;
}
.empty-state .empty-icon {
font-size: 80rpx;
margin-bottom: 20rpx;
}
.empty-state .empty-text {
color: #999999;
font-size: 28rpx;
}
+51
查看文件
@@ -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">&#10003;</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); }
}
+8
查看文件
@@ -0,0 +1,8 @@
Page({
data: {
articles: []
},
onLoad: function () {
}
})
+3
查看文件
@@ -0,0 +1,3 @@
{
"navigationBarTitleText": "发现"
}
+4
查看文件
@@ -0,0 +1,4 @@
<view class="container">
<view class="section-title">发现</view>
<view class="text-center text-muted mt-20">暂无内容</view>
</view>
+1
查看文件
@@ -0,0 +1 @@
{}
+64
查看文件
@@ -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 })
})
}
})
+3
查看文件
@@ -0,0 +1,3 @@
{
"navigationBarTitleText": "护理记录"
}
+26
查看文件
@@ -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">&#128209;</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>
+22
查看文件
@@ -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;
}
+106
查看文件
@@ -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' })
}
}
})
+3
查看文件
@@ -0,0 +1,3 @@
{
"navigationBarTitleText": "首页"
}
+52
查看文件
@@ -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">&#10095;</text>
</view>
</view>
<view class="card" wx:if="{{connected && subscription && subscription.status > 0}}">
<view class="btn-primary text-center" bindtap="onStartTreatment">开始护理</view>
</view>
</view>
+23
查看文件
@@ -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;
}
+20
查看文件
@@ -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' })
})
}
})
+4
查看文件
@@ -0,0 +1,4 @@
{
"navigationBarTitleText": "授权登录",
"navigationStyle": "custom"
}
+16
查看文件
@@ -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>
+86
查看文件
@@ -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;
}
+61
查看文件
@@ -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' })
}
}
})
}
})
+3
查看文件
@@ -0,0 +1,3 @@
{
"navigationBarTitleText": "我的"
}
+43
查看文件
@@ -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">&#10095;</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">&#10095;</text>
</view>
</view>
<view class="divider"></view>
<view class="menu-item" bindtap="onViewHistory">
<text>护理记录</text>
<text class="arrow-sm">&#10095;</text>
</view>
</view>
<view class="card">
<view class="menu-item logout" bindtap="onLogout">
<text>退出登录</text>
</view>
</view>
</view>
+60
查看文件
@@ -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;
}
+56
查看文件
@@ -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 || '绑定失败'
})
})
}
})
+3
查看文件
@@ -0,0 +1,3 @@
{
"navigationBarTitleText": "扫码绑定"
}
+29
查看文件
@@ -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>
+23
查看文件
@@ -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">&#128274;</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">&#10003;</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;
}
+96
查看文件
@@ -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 +
'&regions=' + 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()
})
}
}
})
}
})
+3
查看文件
@@ -0,0 +1,3 @@
{
"navigationBarTitleText": "护理中"
}
+33
查看文件
@@ -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>
+37
查看文件
@@ -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">&#10003;</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">&#10003;</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">&#10007;</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;
}
+56
查看文件
@@ -0,0 +1,56 @@
{
"description": "项目配置文件",
"packOptions": {
"ignore": [],
"include": []
},
"setting": {
"bundle": false,
"userConfirmedBundleSwitch": false,
"urlCheck": true,
"scopeDataCheck": false,
"coverView": true,
"es6": true,
"postcss": true,
"compileHotReLoad": false,
"lazyloadPlaceholderEnable": false,
"preloadBackgroundData": false,
"minified": true,
"autoAudits": false,
"newFeature": false,
"uglifyFileName": false,
"uploadWithSourceMap": true,
"useIsolateContext": true,
"nodeModules": false,
"enhance": true,
"useMultiFrameRuntime": true,
"useApiHook": true,
"useApiHostProcess": true,
"showShadowRootInWxmlPanel": true,
"packNpmManually": false,
"enableEngineNative": false,
"packNpmRelationList": [],
"minifyWXSS": true,
"showES6CompileOption": false,
"minifyWXML": true,
"babelSetting": {
"ignore": [],
"disablePlugins": [],
"outputPath": ""
},
"compileWorklet": false,
"localPlugins": false,
"disableUseStrict": false,
"useCompilerPlugins": false,
"condition": false,
"swc": false,
"disableSWC": true
},
"compileType": "miniprogram",
"libVersion": "3.15.2",
"appid": "wxc4045074ef298510",
"projectname": "hox-beauty",
"condition": {},
"simulatorPluginLibVersion": {},
"editorSetting": {}
}
+23
查看文件
@@ -0,0 +1,23 @@
{
"libVersion": "3.15.2",
"projectname": "miniprogram",
"condition": {},
"setting": {
"urlCheck": true,
"coverView": true,
"lazyloadPlaceholderEnable": false,
"skylineRenderEnable": false,
"preloadBackgroundData": false,
"autoAudits": false,
"useApiHook": true,
"showShadowRootInWxmlPanel": true,
"useStaticServer": false,
"useLanDebug": false,
"showES6CompileOption": false,
"compileHotReLoad": true,
"checkInvalidKey": true,
"ignoreDevUnusedFiles": true,
"bigPackageSizeSupport": false,
"useIsolateContext": true
}
}
+620
查看文件
@@ -0,0 +1,620 @@
var SERVICE = {
DEVICE_INFO: 'FFE0',
DATA_COMM: 'FFE1',
OTA: 'FFE2'
}
var CHAR = {
DEVICE_INFO: 'FFE3',
COMMAND: 'FFE4',
STATUS: 'FFE5',
BOND_INFO: 'FFE6',
OTA_CONTROL: 'FFE7',
OTA_DATA: 'FFE8',
OTA_STATUS: 'FFE9'
}
var CMD = {
SET_PARAMS: 0x01,
START: 0x02,
STOP: 0x03,
QUERY_STATUS: 0x04,
BIND: 0x05,
UNBIND: 0x06
}
var NOTIFY = {
STATUS_REPORT: 0x21,
ACK: 0x22,
TREATMENT_COMPLETE: 0x31,
EXCEPTION: 0x32,
BIND_SUCCESS: 0x33
}
var MODE_STATE = {
IDLE: 0x00,
SCANNING: 0x01,
ACTIVE: 0x02,
PAUSED: 0x03,
COMPLETED: 0x04,
ERROR: 0x05,
OTA: 0x06
}
var WAVELENGTH = {
IR: 1,
R: 2,
UV: 3,
Y: 4
}
var TREAT_MODE = {
NORMAL: 0,
SMART: 1
}
var REGION = {
LEFT_CHEEK: 0x01,
RIGHT_CHEEK: 0x02,
FOREHEAD: 0x04,
CHIN: 0x08,
NOSE: 0x10,
LEFT_EYE: 0x20,
RIGHT_EYE: 0x40,
FULL_FACE: 0x7F
}
var REGION_NAMES = ['left_cheek', 'right_cheek', 'forehead', 'chin', 'nose', 'left_eye', 'right_eye']
var DEVICE_ERR = {
0x00: 'SUCCESS',
0x01: 'ERR_REGION_INVALID',
0x02: 'ERR_REGION_EMPTY',
0x03: 'ERR_BRIGHTNESS_INVALID',
0x04: 'ERR_DURATION_INVALID',
0x05: 'ERR_NOT_BOUND',
0x06: 'ERR_NO_SUBSCRIPTION',
0x07: 'ERR_TEMP_HIGH',
0x08: 'ERR_BATTERY_LOW',
0x09: 'ERR_ALREADY_RUNNING',
0x0A: 'ERR_NOT_RUNNING',
0x0B: 'ERR_OTA_FAILED',
0x0C: 'ERR_BLE_DISCONNECTED'
}
var _deviceId = null
var _connected = false
var _chars = {}
var _cmdSeq = 0
var _pendingAcks = {}
var _listeners = {}
function nextSeq() {
_cmdSeq = (_cmdSeq + 1) % 256
return _cmdSeq
}
function bufferToBytes(buffer) {
var arr = new Uint8Array(buffer)
var bytes = []
for (var i = 0; i < arr.length; i++) {
bytes.push(arr[i])
}
return bytes
}
function bytesToBuffer(bytes) {
var buffer = new ArrayBuffer(bytes.length)
var view = new Uint8Array(buffer)
for (var i = 0; i < bytes.length; i++) {
view[i] = bytes[i]
}
return buffer
}
function xorChecksum(bytes) {
var result = 0
for (var i = 0; i < bytes.length; i++) {
result ^= bytes[i]
}
return result
}
function buildFrame(type, payload) {
var len = payload ? payload.length : 0
var frame = [0xAA, 0x55, len, type]
if (payload && payload.length > 0) {
frame = frame.concat(payload)
}
var checkBytes = frame.slice(2)
frame.push(xorChecksum(checkBytes))
return bytesToBuffer(frame)
}
function parseFrame(buffer) {
var bytes = bufferToBytes(buffer)
if (bytes.length < 5) return null
if (bytes[0] !== 0xAA || bytes[1] !== 0x55) return null
var len = bytes[2]
if (bytes.length < 5 + len) return null
var type = bytes[3]
var payload = bytes.slice(4, 4 + len)
var checksum = bytes[4 + len]
var expected = xorChecksum(bytes.slice(2, 4 + len))
if (checksum !== expected) return null
return { type: type, payload: payload, seq: payload.length > 0 ? payload[payload.length - 1] : 0 }
}
function uint32ToBytes(value) {
return [
(value >> 24) & 0xFF,
(value >> 16) & 0xFF,
(value >> 8) & 0xFF,
value & 0xFF
]
}
function bytesToUint32(bytes, offset) {
return (bytes[offset] << 24) | (bytes[offset + 1] << 16) | (bytes[offset + 2] << 8) | bytes[offset + 3]
}
function hexToBytes(hex) {
var bytes = []
for (var i = 0; i < hex.length; i += 2) {
bytes.push(parseInt(hex.substr(i, 2), 16))
}
return bytes
}
function bytesToHex(bytes) {
var hex = ''
for (var i = 0; i < bytes.length; i++) {
hex += ('0' + bytes[i].toString(16)).slice(-2)
}
return hex.toUpperCase()
}
function findCharUuid(chars, shortUuid) {
for (var i = 0; i < chars.length; i++) {
if (chars[i].uuid.indexOf(shortUuid) !== -1) {
return chars[i].uuid
}
}
return null
}
function on(event, callback) {
if (!_listeners[event]) _listeners[event] = []
_listeners[event].push(callback)
}
function off(event, callback) {
if (!_listeners[event]) return
if (callback) {
_listeners[event] = _listeners[event].filter(function (cb) { return cb !== callback })
} else {
_listeners[event] = []
}
}
function emit(event, data) {
if (!_listeners[event]) return
_listeners[event].forEach(function (cb) {
try { cb(data) } catch (e) { console.error('ble emit error:', e) }
})
}
function handleNotification(frame) {
switch (frame.type) {
case NOTIFY.STATUS_REPORT:
emit('status', parseStatusReport(frame.payload))
break
case NOTIFY.ACK:
var ack = parseAck(frame.payload)
emit('ack', ack)
if (_pendingAcks[ack.seq]) {
if (ack.error_code === 0) {
_pendingAcks[ack.seq].resolve(ack)
} else {
_pendingAcks[ack.seq].reject(ack)
}
delete _pendingAcks[ack.seq]
}
break
case NOTIFY.TREATMENT_COMPLETE:
emit('treatment_complete', parseTreatmentComplete(frame.payload))
break
case NOTIFY.EXCEPTION:
emit('exception', parseException(frame.payload))
break
case NOTIFY.BIND_SUCCESS:
emit('bind_result', { success: frame.payload[0] === 0x00 })
break
}
}
function parseStatusReport(payload) {
if (payload.length < 14) return null
return {
mode_state: payload[0],
region_mask: payload[1],
wavelength: payload[2],
brightness: payload[3],
remaining_ms: bytesToUint32(payload, 4),
error_code: payload[8],
command_seq: payload[9],
battery: payload[10],
temperature: payload[11],
bind_status: payload[12],
subscription: payload[13]
}
}
function parseAck(payload) {
return {
seq: payload[0],
error_code: payload.length > 1 ? payload[1] : 0,
error_msg: DEVICE_ERR[payload.length > 1 ? payload[1] : 0] || 'UNKNOWN'
}
}
function parseTreatmentComplete(payload) {
return {
session_id: bytesToHex(payload.slice(0, 8)),
regions: payload[8],
total_duration_ms: bytesToUint32(payload, 9),
avg_pd: payload[13]
}
}
function parseException(payload) {
return {
error_code: payload[0],
error_msg: DEVICE_ERR[payload[0]] || 'UNKNOWN',
temperature: payload.length > 1 ? payload[1] : 0
}
}
function isConnected() {
return _connected && _deviceId !== null
}
function getDeviceId() {
return _deviceId
}
function startScan(callbacks) {
wx.openBluetoothAdapter({
success: function () {
wx.startBluetoothDevicesDiscovery({
allowDuplicatesKey: false,
success: function () {
wx.onBluetoothDeviceFound(function (res) {
var devices = res.devices || []
for (var i = 0; i < devices.length; i++) {
var d = devices[i]
var name = (d.name || '').toUpperCase()
var localName = (d.localName || '').toUpperCase()
if (name.indexOf('HOX') !== -1 || localName.indexOf('HOX') !== -1 ||
name.indexOf('LIGHTMASK') !== -1 || localName.indexOf('LIGHTMASK') !== -1) {
wx.stopBluetoothDevicesDiscovery({})
if (callbacks.onFound) callbacks.onFound(d)
connect(d.deviceId, callbacks)
return
}
}
})
},
fail: function () {
if (callbacks.onError) callbacks.onError({ msg: '扫描失败' })
}
})
},
fail: function () {
if (callbacks.onError) callbacks.onError({ msg: '请开启蓝牙' })
}
})
}
function connect(deviceId, callbacks) {
_deviceId = deviceId
_chars = {}
wx.createBLEConnection({
deviceId: deviceId,
timeout: 10000,
success: function () {
_connected = true
discoverServices(deviceId, callbacks)
},
fail: function () {
_connected = false
if (callbacks.onError) callbacks.onError({ msg: '连接失败' })
}
})
}
function discoverServices(deviceId, callbacks) {
wx.getBLEDeviceServices({
deviceId: deviceId,
success: function (res) {
var services = res.services
var serviceMap = {}
for (var i = 0; i < services.length; i++) {
var uuid = services[i].uuid.toUpperCase()
if (uuid.indexOf(SERVICE.DEVICE_INFO) !== -1) {
serviceMap.deviceInfo = services[i].uuid
} else if (uuid.indexOf(SERVICE.DATA_COMM) !== -1) {
serviceMap.dataComm = services[i].uuid
} else if (uuid.indexOf(SERVICE.OTA) !== -1) {
serviceMap.ota = services[i].uuid
}
}
var tasks = []
if (serviceMap.deviceInfo) {
tasks.push(discoverChars(deviceId, serviceMap.deviceInfo, 'deviceInfo'))
}
if (serviceMap.dataComm) {
tasks.push(discoverChars(deviceId, serviceMap.dataComm, 'dataComm'))
}
Promise.all(tasks).then(function () {
subscribeToNotifications(deviceId, serviceMap.dataComm).then(function () {
if (callbacks.onConnected) callbacks.onConnected({ deviceId: deviceId })
})
})
},
fail: function () {
if (callbacks.onError) callbacks.onError({ msg: '服务发现失败' })
}
})
}
function discoverChars(deviceId, serviceId, group) {
return new Promise(function (resolve) {
wx.getBLEDeviceCharacteristics({
deviceId: deviceId,
serviceId: serviceId,
success: function (res) {
var chars = res.characteristics || []
for (var i = 0; i < chars.length; i++) {
var c = chars[i]
var uuid = c.uuid.toUpperCase()
if (uuid.indexOf(CHAR.DEVICE_INFO) !== -1) _chars.deviceInfo = { uuid: c.uuid, serviceId: serviceId }
if (uuid.indexOf(CHAR.COMMAND) !== -1) _chars.command = { uuid: c.uuid, serviceId: serviceId }
if (uuid.indexOf(CHAR.STATUS) !== -1) _chars.status = { uuid: c.uuid, serviceId: serviceId }
if (uuid.indexOf(CHAR.BOND_INFO) !== -1) _chars.bondInfo = { uuid: c.uuid, serviceId: serviceId }
if (uuid.indexOf(CHAR.OTA_CONTROL) !== -1) _chars.otaControl = { uuid: c.uuid, serviceId: serviceId }
if (uuid.indexOf(CHAR.OTA_DATA) !== -1) _chars.otaData = { uuid: c.uuid, serviceId: serviceId }
if (uuid.indexOf(CHAR.OTA_STATUS) !== -1) _chars.otaStatus = { uuid: c.uuid, serviceId: serviceId }
}
resolve()
},
fail: function () { resolve() }
})
})
}
function subscribeToNotifications(deviceId, serviceId) {
return new Promise(function (resolve) {
if (!_chars.status) { resolve(); return }
wx.notifyBLECharacteristicValueChange({
deviceId: deviceId,
serviceId: _chars.status.serviceId,
characteristicId: _chars.status.uuid,
state: true,
success: function () {
wx.onBLECharacteristicValueChange(function (res) {
var frame = parseFrame(res.value)
if (frame) handleNotification(frame)
})
resolve()
},
fail: function () { resolve() }
})
})
}
function writeCommand(type, payload) {
return new Promise(function (resolve, reject) {
if (!_connected || !_deviceId) {
reject({ error_code: 0x0C, error_msg: 'ERR_BLE_DISCONNECTED' })
return
}
if (!_chars.command) {
reject({ error_code: 0xFF, error_msg: 'command characteristic not found' })
return
}
var seq = nextSeq()
var payloadWithSeq = (payload || []).concat([seq])
var buffer = buildFrame(type, payloadWithSeq)
_pendingAcks[seq] = { resolve: resolve, reject: reject }
setTimeout(function () {
if (_pendingAcks[seq]) {
_pendingAcks[seq].reject({ error_code: 0xFF, error_msg: 'ACK timeout' })
delete _pendingAcks[seq]
}
}, 5000)
wx.writeBLECharacteristicValue({
deviceId: _deviceId,
serviceId: _chars.command.serviceId,
characteristicId: _chars.command.uuid,
value: buffer,
success: function () {},
fail: function () {
delete _pendingAcks[seq]
reject({ error_code: 0x0C, error_msg: 'ERR_BLE_DISCONNECTED' })
}
})
})
}
function readDeviceInfo() {
return new Promise(function (resolve, reject) {
if (!_connected || !_deviceId || !_chars.deviceInfo) {
reject({ msg: '设备未连接或特征值未就绪' })
return
}
wx.readBLECharacteristicValue({
deviceId: _deviceId,
serviceId: _chars.deviceInfo.serviceId,
characteristicId: _chars.deviceInfo.uuid,
success: function () {},
fail: function () { reject({ msg: '读取设备信息失败' }) }
})
var handler = function (res) {
if (res.characteristicId.toUpperCase().indexOf(CHAR.DEVICE_INFO) !== -1) {
wx.offBLECharacteristicValueChange(handler)
var bytes = bufferToBytes(res.value)
if (bytes.length >= 14) {
resolve({
hw_version: (bytes[0] << 8) | bytes[1],
fw_version: (bytes[2] << 8) | bytes[3],
device_type: (bytes[4] << 8) | bytes[5],
device_id: bytesToHex(bytes.slice(6, 14))
})
} else {
reject({ msg: '设备信息格式错误' })
}
}
}
wx.onBLECharacteristicValueChange(handler)
})
}
function setParams(options) {
var regionMask = options.region_mask || REGION.FULL_FACE
var wavelength = options.wavelength || WAVELENGTH.R
var brightness = options.brightness || 200
var durationMs = options.duration_ms || 600000
var mode = options.mode !== undefined ? options.mode : TREAT_MODE.NORMAL
var payload = [
regionMask,
wavelength,
brightness,
uint32ToBytes(durationMs),
mode
].reduce(function (a, b) {
return a.concat(Array.isArray(b) ? b : [b])
}, [])
return writeCommand(CMD.SET_PARAMS, payload)
}
function startTreatment(regionMask) {
var mask = regionMask || REGION.FULL_FACE
return writeCommand(CMD.START, [mask])
}
function stopTreatment() {
return writeCommand(CMD.STOP, [])
}
function queryStatus() {
return writeCommand(CMD.QUERY_STATUS, [])
}
function bindDevice(userId, bindToken) {
var userBytes = hexToBytes(userId)
var tokenBytes = hexToBytes(bindToken)
var ts = Math.floor(Date.now() / 1000)
var tsBytes = uint32ToBytes(ts)
var payload = [0x01].concat(userBytes).concat(tokenBytes).concat(tsBytes)
return writeCommand(CMD.BIND, payload)
}
function unbindDevice(userId) {
var userBytes = hexToBytes(userId)
var payload = [0x02].concat(userBytes)
return writeCommand(CMD.UNBIND, payload)
}
function disconnect() {
if (_deviceId) {
wx.closeBLEConnection({ deviceId: _deviceId })
_deviceId = null
}
_connected = false
_chars = {}
_pendingAcks = {}
_listeners = {}
wx.closeBluetoothAdapter({})
}
function getRegionName(mask) {
var names = []
var bits = [
[0x01, '左脸颊'], [0x02, '右脸颊'], [0x04, '额头'],
[0x08, '下巴'], [0x10, '鼻部'], [0x20, '左眼周'], [0x40, '右眼周']
]
for (var i = 0; i < bits.length; i++) {
if (mask & bits[i][0]) names.push(bits[i][1])
}
return names
}
function getWavelengthName(code) {
var map = { 1: '红外 850nm', 2: '红光 630nm', 3: '紫光 405nm', 4: '黄光 590nm' }
return map[code] || '未知'
}
function getModeStateName(code) {
var map = {
0x00: '空闲', 0x01: '扫描中', 0x02: '护理中',
0x03: '已暂停', 0x04: '已完成', 0x05: '异常', 0x06: 'OTA升级中'
}
return map[code] || '未知'
}
module.exports = {
SERVICE: SERVICE,
CHAR: CHAR,
CMD: CMD,
NOTIFY: NOTIFY,
MODE_STATE: MODE_STATE,
WAVELENGTH: WAVELENGTH,
TREAT_MODE: TREAT_MODE,
REGION: REGION,
DEVICE_ERR: DEVICE_ERR,
isConnected: isConnected,
getDeviceId: getDeviceId,
startScan: startScan,
connect: connect,
disconnect: disconnect,
on: on,
off: off,
readDeviceInfo: readDeviceInfo,
setParams: setParams,
startTreatment: startTreatment,
stopTreatment: stopTreatment,
queryStatus: queryStatus,
bindDevice: bindDevice,
unbindDevice: unbindDevice,
buildFrame: buildFrame,
parseFrame: parseFrame,
parseStatusReport: parseStatusReport,
parseAck: parseAck,
parseTreatmentComplete: parseTreatmentComplete,
parseException: parseException,
getRegionName: getRegionName,
getWavelengthName: getWavelengthName,
getModeStateName: getModeStateName,
bufferToBytes: bufferToBytes,
bytesToBuffer: bytesToBuffer,
hexToBytes: hexToBytes,
bytesToHex: bytesToHex
}
+172
查看文件
@@ -0,0 +1,172 @@
var request = require('../utils/request')
var MQTT_BROKER = 'wxs://iotcloud.tencent.com/socket/mqtt'
var _socketTask = null
var _connected = false
var _subscriptions = {}
var _listeners = {}
var _reconnectTimer = null
var _heartbeatTimer = null
var _userId = null
function on(event, callback) {
if (!_listeners[event]) _listeners[event] = []
_listeners[event].push(callback)
}
function off(event, callback) {
if (!_listeners[event]) return
if (callback) {
_listeners[event] = _listeners[event].filter(function (cb) { return cb !== callback })
} else {
_listeners[event] = []
}
}
function emit(event, data) {
if (!_listeners[event]) return
_listeners[event].forEach(function (cb) {
try { cb(data) } catch (e) { console.error('mqtt emit error:', e) }
})
}
function connect(userId) {
_userId = userId
var clientId = 'user_' + userId
var token = wx.getStorageSync('token')
_socketTask = wx.connectSocket({
url: MQTT_BROKER,
header: {
'Authorization': 'Bearer ' + token
},
success: function () {
console.log('mqtt connecting...')
},
fail: function () {
emit('error', { msg: 'MQTT连接失败' })
scheduleReconnect()
}
})
_socketTask.onOpen(function () {
_connected = true
startHeartbeat()
subscribeUserTopics()
emit('connected')
})
_socketTask.onMessage(function (res) {
handleMessage(res.data)
})
_socketTask.onClose(function () {
_connected = false
stopHeartbeat()
emit('disconnected')
scheduleReconnect()
})
_socketTask.onError(function () {
_connected = false
emit('error', { msg: 'MQTT连接异常' })
})
}
function subscribeUserTopics() {
if (!_userId) return
subscribe('users/' + _userId + '/subscription')
subscribe('users/' + _userId + '/devices')
subscribe('users/' + _userId + '/notification')
}
function subscribe(topic) {
_subscriptions[topic] = true
}
function unsubscribe(topic) {
delete _subscriptions[topic]
}
function handleMessage(data) {
try {
var msg = JSON.parse(data)
var topic = msg.topic
if (topic && _subscriptions[topic]) {
if (topic.indexOf('/subscription') !== -1) {
emit('subscription_change', msg.payload || msg)
} else if (topic.indexOf('/devices') !== -1) {
emit('devices_change', msg.payload || msg)
} else if (topic.indexOf('/notification') !== -1) {
emit('notification', msg.payload || msg)
} else {
emit('message', msg.payload || msg)
}
}
} catch (e) {
console.error('mqtt parse error:', e)
}
}
function publish(topic, payload) {
if (!_connected || !_socketTask) return
_socketTask.send({
data: JSON.stringify({ topic: topic, payload: payload })
})
}
function startHeartbeat() {
stopHeartbeat()
_heartbeatTimer = setInterval(function () {
if (_connected && _socketTask) {
_socketTask.send({ data: JSON.stringify({ type: 'ping' }) })
}
}, 30000)
}
function stopHeartbeat() {
if (_heartbeatTimer) {
clearInterval(_heartbeatTimer)
_heartbeatTimer = null
}
}
function scheduleReconnect() {
if (_reconnectTimer) return
_reconnectTimer = setTimeout(function () {
_reconnectTimer = null
if (_userId) connect(_userId)
}, 5000)
}
function disconnect() {
if (_reconnectTimer) {
clearTimeout(_reconnectTimer)
_reconnectTimer = null
}
stopHeartbeat()
if (_socketTask) {
_socketTask.close({})
_socketTask = null
}
_connected = false
_subscriptions = {}
_listeners = {}
_userId = null
}
function isConnected() {
return _connected
}
module.exports = {
connect: connect,
disconnect: disconnect,
subscribe: subscribe,
unsubscribe: unsubscribe,
publish: publish,
on: on,
off: off,
isConnected: isConnected
}
+9
查看文件
@@ -0,0 +1,9 @@
{
"desc": "关于本文件的更多信息,请参考文档 https://developers.weixin.qq.com/miniprogram/dev/framework/sitemap.html",
"rules": [
{
"action": "allow",
"page": "*"
}
]
}
+145
查看文件
@@ -0,0 +1,145 @@
var MOCK_TOKEN = 'mock_token_dev_' + Date.now()
var MOCK_USER = {
user_id: 10001,
open_id: 'mock_open_id_12345',
nickname: '测试用户',
avatar_url: '',
phone: '',
gender: 0,
created_at: '2025-01-01T00:00:00.000Z'
}
var MOCK_DEVICE = {
device_id: 'DEV001',
name: '我的光子美容仪',
firmware_version: '1.0.0',
battery_level: 85,
status: 'online',
bind_time: '2025-01-01T00:00:00.000Z'
}
var MOCK_SUBSCRIPTION = {
plan_type: 'trial',
status: 'active',
start_time: '2025-01-01T00:00:00.000Z',
end_time: '2025-02-01T00:00:00.000Z',
remaining_days: 30
}
var MOCK_TREATMENTS = [
{
session_id: 'S20250101001',
device_id: 'DEV001',
regions: [1, 2],
wavelength: 630,
brightness: 5,
duration: 600000,
mode: 1,
avg_pd: 128,
created_at: '2025-01-15T10:30:00.000Z'
},
{
session_id: 'S20250102002',
device_id: 'DEV001',
regions: [1],
wavelength: 850,
brightness: 3,
duration: 300000,
mode: 2,
avg_pd: 96,
created_at: '2025-01-14T08:00:00.000Z'
}
]
var mockHandlers = {
'POST /api/v1/auth/login': function () {
return {
code: 0,
data: {
token: MOCK_TOKEN,
user_id: MOCK_USER.user_id,
user_info: MOCK_USER
}
}
},
'POST /api/v1/auth/refresh': function () {
return { code: 0, data: { token: MOCK_TOKEN } }
},
'GET /api/v1/user/profile': function () {
return { code: 0, data: MOCK_USER }
},
'PUT /api/v1/user/profile': function (data) {
Object.assign(MOCK_USER, data)
return { code: 0, data: MOCK_USER }
},
'POST /api/v1/device/bind': function (data) {
var d = Object.assign({}, MOCK_DEVICE, { device_id: data.device_id || 'DEV001' })
return {
code: 0,
data: {
device: d,
bind_token: 'mock_bind_token_' + Date.now(),
subscription: MOCK_SUBSCRIPTION
}
}
},
'POST /api/v1/device/unbind': function () {
return { code: 0, data: {} }
},
'GET /api/v1/device/list': function () {
return { code: 0, data: { devices: [MOCK_DEVICE], total: 1 } }
},
'GET /api/v1/device/detail': function () {
return { code: 0, data: MOCK_DEVICE }
},
'GET /api/v1/subscription/status': function () {
return { code: 0, data: MOCK_SUBSCRIPTION }
},
'POST /api/v1/subscription/purchase': function (data) {
return {
code: 0,
data: {
order_id: 'ORD' + Date.now(),
plan_type: data.plan_type,
status: 'paid'
}
}
},
'POST /api/v1/subscription/verify': function () {
return { code: 0, data: { valid: true } }
},
'POST /api/v1/treatment/sync': function (data) {
return { code: 0, data: { session_id: data.session_id || 'S' + Date.now() } }
},
'GET /api/v1/treatment/history': function () {
return { code: 0, data: { records: MOCK_TREATMENTS, total: MOCK_TREATMENTS.length } }
}
}
function handle(method, path, data) {
var key = method + ' ' + path
var handler = mockHandlers[key]
if (!handler) {
return { code: -1, message: 'mock: 未定义的接口 ' + key }
}
return handler(data)
}
module.exports = {
handle: handle,
MOCK_TOKEN: MOCK_TOKEN,
enabled: true
}
+70
查看文件
@@ -0,0 +1,70 @@
var API_BASE = 'https://api.lightmask.com'
var USE_MOCK = true
var mock = null
if (USE_MOCK) {
mock = require('./mock')
}
function request(options) {
if (USE_MOCK && mock && mock.enabled) {
return new Promise(function (resolve) {
setTimeout(function () {
var result = mock.handle(options.method || 'GET', options.path, options.data)
if (result.code === 0) {
resolve(result.data)
}
}, 200)
})
}
var token = wx.getStorageSync('token')
return new Promise(function (resolve, reject) {
wx.request({
url: API_BASE + options.path,
method: options.method || 'GET',
data: options.data || {},
header: Object.assign({
'Authorization': token ? 'Bearer ' + token : '',
'Content-Type': 'application/json',
'X-App-Version': '1.0.0',
'X-Platform': 'wechat'
}, options.header || {}),
success: function (res) {
if (res.data && res.data.code === 0) {
resolve(res.data.data)
} else if (res.data && res.data.code === 1001 || res.data && res.data.code === 1002) {
wx.removeStorageSync('token')
wx.reLaunch({ url: '/pages/login/login' })
reject(res.data)
} else {
reject(res.data || { code: -1, message: '请求失败' })
}
},
fail: function (err) {
reject({ code: 2002, message: err.errMsg || '网络异常' })
}
})
})
}
function get(path, data) {
return request({ path: path, method: 'GET', data: data })
}
function post(path, data) {
return request({ path: path, method: 'POST', data: data })
}
function put(path, data) {
return request({ path: path, method: 'PUT', data: data })
}
module.exports = {
request: request,
get: get,
post: post,
put: put,
API_BASE: API_BASE
}