Parcourir la source

Merge remote-tracking branch 'origin/dev' into jiapu_aijiuyi

liyuliangjiazai il y a 3 mois
Parent
commit
9ac26dd72e

+ 169 - 3
code/ajyApp/pages/device/deviceInfo/deviceInfo.nvue

@@ -47,6 +47,20 @@
 					</view>
 				</view>
 
+				<!-- 方案选择 -->
+				<view class="planSection" v-if="modeType != 0 && picMode == 1">
+					<text class="planSectionTitle">选择方案</text>
+					<view class="planCard" @click="showPlanDrawer">
+						<view class="planCardLeft">
+							<text class="planCardName">{{selectedPlan ? selectedPlan.name : '点击选择方案'}}</text>
+							<text class="planCardDesc" v-if="selectedPlan && selectedPlan.description">{{selectedPlan.description}}</text>
+						</view>
+						<view class="planCardRight">
+							<text class="planCardArrow">›</text>
+						</view>
+					</view>
+				</view>
+
 				<!-- 其他功能 -->
 				<view class="otherSetting" v-if="modeType != 0 && picMode == 1">
 					<text class="otherTxt">其他功能</text>
@@ -241,6 +255,36 @@
 			</view>
 		</customPopup>
 
+		<!-- 方案选择弹框 -->
+		<customPopup :isShow="isShowPlanDrawer" @closePop="isShowPlanDrawer=false">
+			<text class="planDrawerTitle">选择方案</text>
+			<text class="planDrawerSub">{{modelist[modeType]}} - 可用方案</text>
+			<scroll-view class="planDrawerList" scroll-y="true">
+				<view class="planDrawerEmpty" v-if="planList.length === 0 && !planLoading">
+					<text class="planEmptyTxt">暂无可用方案</text>
+				</view>
+				<view class="planDrawerEmpty" v-if="planLoading">
+					<text class="planEmptyTxt">加载中...</text>
+				</view>
+				<view class="planItem" :class="selectedPlan && selectedPlan.id === item.id ? 'planItemActive' : ''"
+					v-for="(item, index) in planList" :key="index" @click="selectPlan(item)">
+					<view class="planItemInfo">
+						<text class="planItemName">{{item.name}}</text>
+						<text class="planItemDesc" v-if="item.description">{{item.description}}</text>
+						<view class="planItemMeta" v-if="item.steps || item.duration">
+							<text class="planMetaTxt" v-if="item.steps">{{item.steps}}步</text>
+							<text class="planMetaTxt" v-if="item.duration">约{{item.duration}}分钟</text>
+						</view>
+					</view>
+					<view class="planItemCheck">
+						<view class="checkboxCustom">
+							<view v-if="selectedPlan && selectedPlan.id === item.id" class="checkboxCircle"></view>
+						</view>
+					</view>
+				</view>
+			</scroll-view>
+		</customPopup>
+
 		<!-- 时间配置器 -->
 		<customPopup :isShow="isShowTimeDrawer" @closePop="isShowTimeDrawer=false">
 			<text class="aijiuTime">选择灸疗时间</text>
@@ -260,11 +304,11 @@
 		</customPopup>
 
 		<!-- 重连弹框 -->
-		<view class="maskbg" v-if="reconnectDrawer">
+		<view class="maskbg" v-if="reconnectDrawer" @click.stop="">
 			<view class="maskbox">
 				<text class="maskTxt">蓝牙连接断开,第{{reconnectCount}}次重连中...</text>
 			</view>
-		</view>
+		</view>	
 
 		<ay-toast ref="ayToast" />
 	</view>
@@ -277,6 +321,7 @@ import lSliderRange from '@/components/l-sliderRange/l-sliderRange.nvue'
 import bleMixin from './mixins/ble-mixin.js'
 import audioMixin from './mixins/audio-mixin.js'
 import acupointMixin from './mixins/acupoint-mixin.js'
+import planMixin from './mixins/plan-mixin.js'
 import bleManager, { CHAIR_ANGLE } from '@/utils/ble'
 
 export default {
@@ -285,7 +330,7 @@ export default {
 		ayToast,
 		lSliderRange
 	},
-	mixins: [bleMixin, audioMixin, acupointMixin],
+	mixins: [bleMixin, audioMixin, acupointMixin, planMixin],
 	data() {
 		return {
 			statusBarHeight: 44,
@@ -798,6 +843,127 @@ export default {
 .empty {
 	height: 180rpx;
 }
+/* 方案选择区 */
+.planSection {
+	margin-left: 38rpx;
+	margin-right: 38rpx;
+	margin-top: 20rpx;
+	background-color: rgba(255,255,255,0.9);
+	border-radius: 16rpx;
+	padding: 30rpx;
+}
+.planSectionTitle {
+	font-size: 28rpx;
+	color: #9BA5B7;
+	margin-bottom: 16rpx;
+}
+.planCard {
+	flex-direction: row;
+	justify-content: space-between;
+	align-items: center;
+	background-color: #389588;
+	border-radius: 40rpx;
+	padding-left: 30rpx;
+	padding-right: 24rpx;
+	padding-top: 20rpx;
+	padding-bottom: 20rpx;
+}
+.planCardLeft {
+	flex: 1;
+}
+.planCardName {
+	font-size: 30rpx;
+	font-weight: bold;
+	color: #FFFFFF;
+}
+.planCardDesc {
+	font-size: 22rpx;
+	color: rgba(255,255,255,0.7);
+	margin-top: 6rpx;
+}
+.planCardRight {
+	width: 40rpx;
+	align-items: center;
+}
+.planCardArrow {
+	font-size: 40rpx;
+	color: #FFFFFF;
+}
+/* 方案选择弹框 */
+.planDrawerTitle {
+	font-size: 32rpx;
+	color: #545F71;
+	font-weight: bold;
+	text-align: center;
+}
+.planDrawerSub {
+	font-size: 24rpx;
+	color: #9BA5B7;
+	text-align: center;
+	margin-top: 8rpx;
+	margin-bottom: 24rpx;
+}
+.planDrawerList {
+	max-height: 600rpx;
+}
+.planDrawerEmpty {
+	align-items: center;
+	padding-top: 60rpx;
+	padding-bottom: 60rpx;
+}
+.planEmptyTxt {
+	font-size: 28rpx;
+	color: #9BA5B7;
+}
+.planItem {
+	flex-direction: row;
+	justify-content: space-between;
+	align-items: center;
+	padding-left: 20rpx;
+	padding-right: 20rpx;
+	padding-top: 24rpx;
+	padding-bottom: 24rpx;
+	border-bottom-width: 1px;
+	border-bottom-style: solid;
+	border-bottom-color: #EEF1F4;
+	border-radius: 12rpx;
+}
+.planItemActive {
+	background-color: #F0F8F7;
+}
+.planItemInfo {
+	flex: 1;
+	margin-right: 20rpx;
+}
+.planItemName {
+	font-size: 30rpx;
+	color: #545F71;
+	font-weight: bold;
+}
+.planItemDesc {
+	font-size: 24rpx;
+	color: #9BA5B7;
+	margin-top: 8rpx;
+}
+.planItemMeta {
+	flex-direction: row;
+	margin-top: 10rpx;
+}
+.planMetaTxt {
+	font-size: 22rpx;
+	color: #389588;
+	margin-right: 20rpx;
+	background-color: rgba(56,149,136,0.1);
+	padding-left: 12rpx;
+	padding-right: 12rpx;
+	padding-top: 4rpx;
+	padding-bottom: 4rpx;
+	border-radius: 8rpx;
+}
+.planItemCheck {
+	width: 50rpx;
+	align-items: center;
+}
 /* 弹框样式 */
 .tip1 {
 	font-size: 28rpx;

+ 12 - 18
code/ajyApp/pages/device/deviceInfo/mixins/ble-mixin.js

@@ -47,23 +47,30 @@ export default {
 			this._unbinders = [
 				bleManager.on('state', (s) => {
 					this.linked = (s === BLE_STATE.READY_COMM)
-					if (s === BLE_STATE.DISCONNECTED) {
-						this.linked = false
-						this._handleBleDisconnect()
-					}
 				}),
 				bleManager.on('disconnected', (info) => {
 					this.linked = false
 					if (!info.manual) {
-						this._handleBleDisconnect()
+						this.reconnectDrawer = true
 					}
 				}),
+				bleManager.on('reconnecting', ({ count }) => {
+					this.reconnectCount = count
+					this.reconnectDrawer = true
+				}),
 				bleManager.on('reconnected', () => {
 					this.reconnectDrawer = false
 					this.reconnectCount = 0
 					this.linked = true
 					this.$refs.ayToast.success('重连成功')
 				}),
+				bleManager.on('reconnectFailed', () => {
+					this.reconnectDrawer = false
+					this.$refs.ayToast.error('连接失败,请返回重试')
+					setTimeout(() => {
+						uni.navigateBack()
+					}, 1500)
+				}),
 				bleManager.on('report:GROUP_1', (data) => {
 					this._handleGroup1(data)
 				}),
@@ -94,19 +101,6 @@ export default {
 			}
 		},
 
-		_handleBleDisconnect() {
-			this.reconnectCount++
-			if (this.reconnectCount >= 4) {
-				this.reconnectDrawer = false
-				this.$refs.ayToast.error('连接失败,请返回重试')
-				setTimeout(() => {
-					uni.navigateBack()
-				}, 1500)
-			} else {
-				this.reconnectDrawer = true
-			}
-		},
-
 		// ========== 设备上报数据解析 ==========
 		_handleGroup1(data) {
 			// 参数组1: 设备状态

+ 107 - 0
code/ajyApp/pages/device/deviceInfo/mixins/plan-mixin.js

@@ -0,0 +1,107 @@
+/**
+ * 方案管理 mixin
+ * 负责:方案列表加载、子方案选择、模式切换时刷新方案
+ */
+import { getCareList, getCareDetail } from '@/utils/api/care.js'
+
+export default {
+	data() {
+		return {
+			planList: [], // 当前模式下的方案列表
+			selectedPlan: null, // 当前选中的方案 { id, name, description }
+			isShowPlanDrawer: false, // 方案选择弹窗
+			planLoading: false
+		}
+	},
+	methods: {
+		/**
+		 * 切换模式 - 覆盖原始changeMode,加入方案加载逻辑
+		 */
+		changeMode(mode) {
+			this.isShowDrawer2 = false
+			if (this.modeType === mode) return
+			this.modeType = mode
+			// 切换模式时重置方案选择
+			this.selectedPlan = null
+			this.planList = []
+			// 非休闲模式加载对应方案列表
+			if (mode !== 0) {
+				this.loadPlanList(mode)
+			}
+			// 停止音频(休闲模式专属)
+			if (mode !== 0 && this._stopAudioOnModeChange) {
+				this._stopAudioOnModeChange()
+			}
+		},
+
+		/**
+		 * 加载指定模式的方案列表
+		 * @param {Number} mode - 1专业 2自定义 3专家
+		 */
+		async loadPlanList(mode) {
+			this.planLoading = true
+			try {
+				// modeType映射为后端模式标识
+				const modeMap = {
+					1: 'PROFESSIONAL',
+					2: 'CUSTOM',
+					3: 'EXPERT'
+				}
+				const res = await getCareList({ modeType: modeMap[mode] || '' })
+				if (res && res.records) {
+					this.planList = res.records.map(item => ({
+						id: item.id,
+						name: item.title || item.name || '未命名方案',
+						description: item.description || item.remark || '',
+						steps: item.stepCount || item.steps || 0,
+						duration: item.totalDuration || 0
+					}))
+				} else {
+					this.planList = []
+				}
+			} catch (e) {
+				console.error('加载方案列表失败', e)
+				this.planList = []
+			}
+			this.planLoading = false
+		},
+
+		/** 打开方案选择弹窗 */
+		showPlanDrawer() {
+			if (this.planList.length === 0 && this.modeType !== 0) {
+				this.loadPlanList(this.modeType)
+			}
+			this.isShowPlanDrawer = true
+		},
+
+		/** 选择方案 */
+		selectPlan(plan) {
+			this.selectedPlan = plan
+			this.isShowPlanDrawer = false
+			// 选中方案后,可在此触发方案数据加载到curCase
+			this._applyPlanToCase(plan)
+		},
+
+		/**
+		 * 将方案数据应用到当前穴位配置
+		 * TODO: 后续对接方案详情接口,获取具体步骤和穴位坐标
+		 */
+		async _applyPlanToCase(plan) {
+			if (!plan || !plan.id) return
+			try {
+				const res = await getCareDetail(plan.id)
+				if (res && res.steps) {
+					this.curCase = res.steps.map(step => ({
+						id: step.acupointId || step.id,
+						time: step.duration || 15,
+						_x: step.x || 0,
+						_y: step.y || 0
+					}))
+					this.curCaseIndex = 0
+				}
+			} catch (e) {
+				console.error('加载方案详情失败', e)
+			}
+		}
+	}
+}

+ 58 - 28
code/ajyApp/pages/device/userManage/userManage.nvue

@@ -14,32 +14,37 @@
 			</view>
 			<text class="tipTxt">最多可添加10个用户,点击卡片切换用户</text>
 			<scroll-view class="userList" scroll-y="true">
-				<view class="userCard" :class="{'curUserCard': item.id === currentUserId}" 
+				<view class="listInner">
+				<view class="cardWrap" :class="{'curCardWrap': item.id === currentUserId}"
 					v-for="(item, index) in userList" :key="item.id" @click="switchUser(item)">
-					<view class="userInfo">
-						<view class="avatarWrap">
-							<text class="avatarTxt">{{item.name ? item.name.substring(0,1) : '用'}}</text>
-						</view>
-						<view class="infoDetail">
-							<view class="nameRow">
-								<text class="userName">{{item.name}}</text>
-								<text class="userGender">{{item.gender === 1 ? '男' : '女'}}</text>
-								<text class="userAge" v-if="item.age">{{item.age}}岁</text>
+					<view class="userCard">
+						<view class="userInfo">
+							<view class="avatarWrap">
+								<text class="avatarTxt">{{item.name ? item.name.substring(0,1) : '用'}}</text>
 							</view>
-							<view class="bodyRow">
-								<text class="bodyTxt">肩高:{{item.shoulderHeight || '--'}}cm</text>
-								<text class="bodyTxt">肩宽:{{item.shoulderWidth || '--'}}cm</text>
-								<text class="bodyTxt">体重:{{item.weight || '--'}}kg</text>
+							<view class="infoDetail">
+								<view class="nameRow">
+									<text class="userName">{{item.name}}</text>
+									<text class="userGender">{{item.gender === 1 ? '男' : '女'}}</text>
+									<text class="userAge" v-if="item.age">{{item.age}}岁</text>
+								</view>
+								<view class="bodyRow">
+									<text class="bodyTxt">肩高:{{item.shoulderHeight || '--'}}cm</text>
+									<text class="bodyTxt">肩宽:{{item.shoulderWidth || '--'}}cm</text>
+									<text class="bodyTxt">体重:{{item.weight || '--'}}kg</text>
+								</view>
 							</view>
 						</view>
+						<view class="cardBtns">
+							<text class="fingerBtn" @click.stop="enrollFingerprint(item)">录入指纹</text>
+							<text class="editBtn" @click.stop="editUser(item)">编辑</text>
+							<text class="deleteBtn" @click.stop="confirmDelete(item)">删除</text>
+						</view>
+						<view class="curTag" v-if="item.id === currentUserId">
+							<text class="curTagTxt">当前</text>
+						</view>
 					</view>
-					<view class="cardBtns">
-						<text class="editBtn" @click.stop="editUser(item)">编辑</text>
-						<text class="deleteBtn" @click.stop="confirmDelete(item)">删除</text>
-					</view>
-					<view class="curTag" v-if="item.id === currentUserId">
-						<text class="curTagTxt">当前</text>
-					</view>
+				</view>
 				</view>
 			</scroll-view>
 		</view>
@@ -139,6 +144,11 @@ export default {
 			this.deleteTarget = item
 			this.showDeletePopup = true
 		},
+		/** 录入指纹 */
+		enrollFingerprint(item) {
+			this.$refs.ayToast.loading("指纹录入中", { mask: false })
+
+		},
 		/** 执行删除 */
 		doDelete() {
 			if (!this.deleteTarget) return
@@ -222,21 +232,28 @@ export default {
 }
 .userList {
 	flex: 1;
+}
+.listInner {
 	padding-left: 30rpx;
 	padding-right: 30rpx;
+	padding-top: 20rpx;
+	padding-bottom: 30rpx;
+}
+.cardWrap {
+	border-radius: 20rpx;
+	overflow: hidden;
+	margin-bottom: 24rpx;
+	background-color: #389588;
+}
+.curCardWrap {
+
+	border-radius: 20rpx;
 }
 .userCard {
 	background-color: #FFFFFF;
 	border-radius: 20rpx;
 	padding: 30rpx;
-	margin-bottom: 24rpx;
 	position: relative;
-	overflow: hidden;
-}
-.curUserCard {
-	border-width: 2px;
-	border-style: solid;
-	border-color: #389588;
 }
 .userInfo {
 	flex-direction: row;
@@ -292,6 +309,17 @@ export default {
 	justify-content: flex-end;
 	margin-top: 20rpx;
 }
+.fingerBtn {
+	font-size: 26rpx;
+	color: #E6A23C;
+	padding-left: 24rpx;
+	padding-right: 24rpx;
+	padding-top: 10rpx;
+	padding-bottom: 10rpx;
+	border-radius: 24rpx;
+	background-color: #FDF6EC;
+	margin-right: 20rpx;
+}
 .editBtn {
 	font-size: 26rpx;
 	color: #389588;
@@ -323,6 +351,8 @@ export default {
 	padding-top: 4rpx;
 	padding-bottom: 4rpx;
 	border-bottom-left-radius: 12rpx;
+	border-top-right-radius: 18rpx;
+
 }
 .curTagTxt {
 	font-size: 20rpx;

+ 29 - 0
code/ajyApp/utils/api/care.js

@@ -0,0 +1,29 @@
+/**
+ * 调理方案模块 API
+ */
+import http from '@/utils/http'
+
+/**
+ * 获取调理方案列表(分页)
+ * @param {Object} params - { modeType, page, size }
+ * @param {string} params.modeType - 模式标识: PROFESSIONAL / CUSTOM / EXPERT
+ * @param {number} [params.page=0] - 页码
+ * @param {number} [params.size=100] - 每页数量
+ * @returns {Promise<{records, total}>}
+ */
+export function getCareList(params = {}) {
+	return http.post('/userCare/careList', {
+		page: params.page || 0,
+		size: params.size || 100,
+		modeType: params.modeType || ''
+	})
+}
+
+/**
+ * 获取调理方案详情
+ * @param {string|number} id - 方案ID
+ * @returns {Promise<{steps: Array}>}
+ */
+export function getCareDetail(id) {
+	return http.get('/userCare/careDetail', { data: { id } })
+}

+ 23 - 14
code/ajyApp/utils/ble/BleManager.js

@@ -325,21 +325,30 @@ class BleManager extends EventEmitter {
     this.emit('disconnected', { manual: false, reason })
     this._parser.reset()
     // 尝试自动重连
-    if (this.config.autoReconnect && device && this._reconnectCount < this.config.maxReconnect) {
-      this._reconnectCount++
-      const delay = Math.min(1000 * this._reconnectCount, 5000)
-      logger.warn(`reconnect in ${delay}ms (${this._reconnectCount}/${this.config.maxReconnect})`)
-      this._reconnectTimer = setTimeout(async () => {
-        try {
-          await this.init()
-          await this.connect(device.deviceId)
-          this.emit('reconnected', this._device)
-        } catch (e) {
-          logger.error('reconnect fail', e)
-          this._handleDisconnected(reason)
-        }
-      }, delay)
+    this._attemptReconnect(device, reason)
+  }
+
+  _attemptReconnect(device, reason) {
+    if (!this.config.autoReconnect || !device || this._reconnectCount >= this.config.maxReconnect) {
+      if (this._reconnectCount >= this.config.maxReconnect) {
+        this.emit('reconnectFailed')
+      }
+      return
     }
+    this._reconnectCount++
+    const delay = Math.min(1000 * this._reconnectCount, 5000)
+    logger.warn(`reconnect in ${delay}ms (${this._reconnectCount}/${this.config.maxReconnect})`)
+    this.emit('reconnecting', { count: this._reconnectCount, max: this.config.maxReconnect })
+    this._reconnectTimer = setTimeout(async () => {
+      try {
+        await this.init()
+        await this.connect(device.deviceId)
+        this.emit('reconnected', this._device)
+      } catch (e) {
+        logger.error('reconnect fail', e)
+        this._attemptReconnect(device, reason)
+      }
+    }, delay)
   }
 
   _clearReconnect() {