Quellcode durchsuchen

我的信息新增性别、肩高、肩宽等字段。
优化编辑用户信息样式。

liyuliangjiazai vor 3 Monaten
Ursprung
Commit
a54265e74f

+ 147 - 109
code/ajyApp/pages/device/acupointEdit/acupointEdit.vue

@@ -13,37 +13,39 @@
 		</view>
 
 		<!-- 人体模型 + 穴位标注 -->
-		<view class="body-container">
-			<view class="body-wrapper">
-				<!-- 中线参考 -->
-				<view class="midline"></view>
-
-				<!-- 人体背部剪影 -->
-				<image
-					src="/static/device/body-back-silhouette.svg"
-					mode="aspectFit"
-					class="body-img"
-					@load="onImageLoad"
-				></image>
-
-				<!-- 穴位点 -->
-				<view
-					v-for="(point, index) in positionedAcupoints"
-					:key="point.id"
-					class="acupoint-dot"
-					:class="{
-						'acupoint-active': selectedIndex === index
-					}"
-					:style="{
-						left: point.screenX + 'px',
-						top: point.screenY + 'px'
-					}"
-					@click="onAcupointTap(index)"
-				>
-					<view class="dot-inner"></view>
+		<scroll-view class="body-scroll" scroll-y :scroll-top="scrollTop" @scroll="onScroll" scroll-with-animation>
+			<view class="body-content">
+				<view class="body-wrapper">
+					<!-- 中线参考 -->
+					<view class="midline"></view>
+
+					<!-- 人体背部剪影 -->
+					<image
+						src="/static/device/body-back-silhouette.svg"
+						mode="aspectFit"
+						class="body-img"
+						@load="onImageLoad"
+					></image>
+
+					<!-- 穴位点 -->
+					<view
+						v-for="(point, index) in positionedAcupoints"
+						:key="point.id"
+						class="acupoint-dot"
+						:class="{
+							'acupoint-active': selectedIndex === index
+						}"
+						:style="{
+							left: point.screenX + 'px',
+							top: point.screenY + 'px'
+						}"
+						@click="onAcupointTap(index)"
+					>
+						<view class="dot-inner"></view>
+					</view>
 				</view>
 			</view>
-		</view>
+		</scroll-view>
 
 		<!-- 穴位列表 -->
 		<view class="list-section">
@@ -51,31 +53,21 @@
 				<text class="list-title">穴位清单</text>
 			</view>
 			<scroll-view class="list-scroll" scroll-y>
-				<view
-					v-for="(point, index) in acupointList"
-					:key="point.id"
-					class="list-item"
-					:class="{'list-item-active': selectedIndex === index}"
-					@click="onListItemTap(index)"
-				>
-					<view class="item-left">
+				<view class="list-grid">
+					<view
+						v-for="(point, index) in acupointList"
+						:key="point.id"
+						class="list-item"
+						:class="{
+							'list-item-active': selectedIndex === index,
+							'list-item-full': point.isSingle
+						}"
+						@click="onListItemTap(index)"
+					>
 						<view class="item-index" :class="{'item-index-active': selectedIndex === index}">
 							<text class="index-text">{{index + 1}}</text>
 						</view>
-						<view class="item-info">
-							<text class="item-name">{{point.acupointName}}</text>
-							<text class="item-code">{{point.acupointCode}}</text>
-						</view>
-					</view>
-					<view class="item-right">
-						<text class="item-side" v-if="point.customSide || point.acupointSide !== 'center'">{{ point.customSide || sideLabel(point.acupointSide) }}</text>
-						<view class="item-params" v-if="point.method">
-							<text class="param-text">{{point.method}} {{point.temperature}}°C {{point.duration}}min</text>
-						</view>
-						<view class="item-coord-box" v-else>
-							<text class="item-coord">{{point.coordinateX}},{{point.coordinateY}}</text>
-							<text class="item-unit">mm</text>
-						</view>
+						<text class="item-name">{{point.acupointName}}</text>
 					</view>
 				</view>
 			</scroll-view>
@@ -192,7 +184,10 @@ export default {
 				duration: 15
 			},
 			sideOptions: ['双侧', '中心', '左侧', '右侧'],
-			methodOptions: ['温和灸', '雀啄灸', '回旋灸']
+			methodOptions: ['温和灸', '雀啄灸', '回旋灸'],
+			
+			scrollTop: 0,
+			oldScrollTop: 0
 		}
 	},
 	computed: {
@@ -230,6 +225,10 @@ export default {
 			uni.navigateBack()
 		},
 
+		onScroll(e) {
+			this.oldScrollTop = e.detail.scrollTop
+		},
+
 		/** 图片加载完成 */
 		onImageLoad() {
 			this.imageReady = true
@@ -241,7 +240,42 @@ export default {
 			try {
 				const data = await getBackAcupoints(4)
 				if (Array.isArray(data)) {
-					this.acupointList = data
+					// 按名称分组并计算平均 Y 坐标,以处理对称穴位
+					const nameMap = new Map()
+					data.forEach(point => {
+						if (!nameMap.has(point.acupointName)) {
+							nameMap.set(point.acupointName, {
+								name: point.acupointName,
+								points: [],
+								sumY: 0
+							})
+						}
+						const group = nameMap.get(point.acupointName)
+						group.points.push(point)
+						group.sumY += point.coordinateY
+					})
+					
+					const groups = Array.from(nameMap.values())
+					groups.forEach(group => {
+						group.avgY = group.sumY / group.points.length
+						// 同名穴位按 X 坐标从左到右排序
+						group.points.sort((a, b) => a.coordinateX - b.coordinateX)
+					})
+					
+					// 组之间按平均 Y 坐标从上到下排序
+					groups.sort((a, b) => a.avgY - b.avgY)
+					
+					// 重新打平为一维数组,并标记是否为单穴(用于占满整行)
+					const sortedData = []
+					groups.forEach(group => {
+						const isSingle = group.points.length === 1
+						group.points.forEach(p => {
+							p.isSingle = isSingle
+						})
+						sortedData.push(...group.points)
+					})
+					
+					this.acupointList = sortedData
 				} else {
 					this.acupointList = []
 				}
@@ -282,6 +316,36 @@ export default {
 				duration: point.duration || 15
 			}
 			this.showPopup = true
+
+			this.scrollToAcupoint()
+		},
+
+		/** 滚动到选中的穴位 */
+		scrollToAcupoint() {
+			this.$nextTick(() => {
+				const query = uni.createSelectorQuery().in(this)
+				query.select('.body-scroll').boundingClientRect()
+				query.select('.acupoint-active').boundingClientRect()
+				query.exec(res => {
+					const scrollRes = res[0]
+					const dotRes = res[1]
+					
+					if (scrollRes && dotRes) {
+						const relativeTop = dotRes.top - scrollRes.top
+						const offset = relativeTop - (scrollRes.height / 2) + (dotRes.height / 2)
+						
+						if (Math.abs(offset) > 5) {
+							let newScrollTop = this.oldScrollTop + offset
+							if (newScrollTop < 0) newScrollTop = 0
+							
+							this.scrollTop = this.oldScrollTop
+							this.$nextTick(() => {
+								this.scrollTop = newScrollTop
+							})
+						}
+					}
+				})
+			})
 		},
 
 		/** 关闭弹窗 */
@@ -364,14 +428,18 @@ export default {
 }
 
 /* 人体模型容器 */
-.body-container {
+.body-scroll {
 	flex: 1;
+	min-height: 0;
+	width: 100%;
+}
+.body-content {
 	display: flex;
 	justify-content: center;
 	align-items: flex-start; /* 改为顶部对齐,防止高度不足时头部被居中截断 */
 	padding: 20px 10px; /* 增加顶部间距,保护头部不贴边 */
-	overflow-y: auto; /* 允许垂直滚动,适配屏幕高度不足的情况 */
-	min-height: 0;
+	min-height: 100%;
+	box-sizing: border-box;
 }
 .body-wrapper {
 	position: relative;
@@ -441,7 +509,7 @@ export default {
 .list-section {
 	background: #FFFFFF;
 	border-radius: 28px 28px 0 0;
-	padding: 20px 20px 0;
+	padding: 20px 12px 0;
 	max-height: 40vh;
 	display: flex;
 	flex-direction: column;
@@ -461,41 +529,46 @@ export default {
 .list-scroll {
 	flex: 1;
 	overflow-y: auto;
-	padding-bottom: 20px;
+	padding: 4px 8px 20px 8px;
+}
+.list-grid {
+	display: flex;
+	flex-direction: row;
+	flex-wrap: wrap;
+	gap: 10px;
 }
 .list-item {
 	display: flex;
 	flex-direction: row;
-	justify-content: space-between;
 	align-items: center;
-	padding: 14px 16px;
-	margin-bottom: 10px;
-	border-radius: 16px;
+	padding: 10px 12px;
+	border-radius: 12px;
 	background: #F8FAFA;
 	border: 1px solid rgba(56, 149, 136, 0.05);
 	transition: all 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
+	width: calc(50% - 5px);
+	box-sizing: border-box;
 }
 .list-item-active {
 	background: rgba(56, 149, 136, 0.08);
 	border-color: rgba(56, 149, 136, 0.3);
 	box-shadow: 0 4px 12px rgba(56, 149, 136, 0.1);
-	transform: scale(1.02);
+	transform: scale(1);
 }
-.item-left {
-	display: flex;
-	flex-direction: row;
-	align-items: center;
+.list-item-full {
+	margin-right: 50%;
 }
 .item-index {
-	width: 28px;
-	height: 28px;
-	border-radius: 10px;
+	width: 24px;
+	height: 24px;
+	border-radius: 8px;
 	background: rgba(56, 149, 136, 0.1);
 	display: flex;
 	justify-content: center;
 	align-items: center;
-	margin-right: 12px;
+	margin-right: 8px;
 	transition: all 0.25s;
+	flex-shrink: 0;
 }
 .item-index-active {
 	background: rgba(246, 82, 82, 0.1);
@@ -508,49 +581,14 @@ export default {
 .item-index-active .index-text {
 	color: #F65252;
 }
-.item-info {
-	display: flex;
-	flex-direction: column;
-}
 .item-name {
 	font-size: 13px;
 	color: #545F71;
 	font-weight: 500;
 	line-height: 1.3;
-}
-.item-code {
-	font-size: 10px;
-	color: #9BA5B7;
-	margin-top: 2px;
-	font-family: monospace;
-}
-.item-right {
-	display: flex;
-	flex-direction: row;
-	align-items: center;
-	gap: 8px;
-}
-.item-side {
-	font-size: 10px;
-	color: #389588;
-	padding: 2px 6px;
-	border-radius: 4px;
-	background: rgba(56, 149, 136, 0.08);
-}
-.item-coord-box {
-	display: flex;
-	flex-direction: row;
-	align-items: baseline;
-}
-.item-coord {
-	font-size: 11px;
-	color: #9BA5B7;
-	font-family: monospace;
-}
-.item-unit {
-	font-size: 9px;
-	color: #c0c0c0;
-	margin-left: 2px;
+	overflow: hidden;
+	text-overflow: ellipsis;
+	white-space: nowrap;
 }
 
 /* 加载遮罩 */

+ 4 - 36
code/ajyApp/pages/device/bodyParams/bodyParams.nvue

@@ -12,6 +12,7 @@
 			</view>
 			<text class="tipTxt">请录入体形参数,用于精准定位穴位</text>
 			<scroll-view class="formArea" scroll-y="true">
+				<view class="formInner">
 				<!-- 用户姓名 -->
 				<view class="formItem">
 					<text class="formLabel">姓名</text>
@@ -49,15 +50,6 @@
 						<input class="formInput" v-model="form.shoulderHeight" type="digit" placeholder="例如:135" placeholder-class="pholder" />
 						<text class="unitTxt">cm</text>
 					</view>
-					<view class="sliderWrap">
-						<slider :value="Number(form.shoulderHeight) || 100" :min="50" :max="250" :step="1" 
-							activeColor="#389588" backgroundColor="#E8E8E8" block-size="20"
-							@change="onShoulderHeightChange" />
-						<view class="sliderRange">
-							<text class="rangeTxt">50cm</text>
-							<text class="rangeTxt">250cm</text>
-						</view>
-					</view>
 				</view>
 
 				<!-- 肩宽 -->
@@ -67,15 +59,6 @@
 						<input class="formInput" v-model="form.shoulderWidth" type="digit" placeholder="例如:42" placeholder-class="pholder" />
 						<text class="unitTxt">cm</text>
 					</view>
-					<view class="sliderWrap">
-						<slider :value="Number(form.shoulderWidth) || 40" :min="20" :max="80" :step="1"
-							activeColor="#389588" backgroundColor="#E8E8E8" block-size="20"
-							@change="onShoulderWidthChange" />
-						<view class="sliderRange">
-							<text class="rangeTxt">20cm</text>
-							<text class="rangeTxt">80cm</text>
-						</view>
-					</view>
 				</view>
 
 				<!-- 体重 -->
@@ -93,6 +76,7 @@
 				</view>
 
 				<view class="bottomSpace"></view>
+				</view>
 			</scroll-view>
 		</view>
 		<ay-toast ref="ayToast" />
@@ -147,12 +131,6 @@ export default {
 				this.form.weight = user.weight ? String(user.weight) : ''
 			}
 		},
-		onShoulderHeightChange(e) {
-			this.form.shoulderHeight = String(e.detail.value)
-		},
-		onShoulderWidthChange(e) {
-			this.form.shoulderWidth = String(e.detail.value)
-		},
 		/** 校验并保存 */
 		onSave() {
 			// 校验
@@ -289,6 +267,8 @@ export default {
 }
 .formArea {
 	flex: 1;
+}
+.formInner {
 	padding-left: 30rpx;
 	padding-right: 30rpx;
 }
@@ -340,18 +320,6 @@ export default {
 .genderTxt {
 	font-size: 28rpx;
 }
-.sliderWrap {
-	margin-top: 20rpx;
-}
-.sliderRange {
-	flex-direction: row;
-	justify-content: space-between;
-	margin-top: 4rpx;
-}
-.rangeTxt {
-	font-size: 22rpx;
-	color: #9BA5B7;
-}
 .saveBtn {
 	background-color: #389588;
 	border-radius: 44rpx;

+ 141 - 1
code/ajyApp/pages/my/info/info.nvue

@@ -20,13 +20,48 @@
 					<image class="item-arrow" src="/static/my/arrowright.png" mode="aspectFill"></image>
 				</view>
 			</view>
+			<view class="info-item" @click="editGender">
+				<text class="item-label">性别</text>
+				<view class="item-right">
+					<text class="item-value">{{gender === 1 ? '男' : '女'}}</text>
+					<image class="item-arrow" src="/static/my/arrowright.png" mode="aspectFill"></image>
+				</view>
+			</view>
+			<view class="info-item" @click="editNumberField('age')">
+				<text class="item-label">年龄</text>
+				<view class="item-right">
+					<text class="item-value">{{age ? age + '岁' : '去设置'}}</text>
+					<image class="item-arrow" src="/static/my/arrowright.png" mode="aspectFill"></image>
+				</view>
+			</view>
+			<view class="info-item" @click="editNumberField('shoulderHeight')">
+				<text class="item-label">肩高</text>
+				<view class="item-right">
+					<text class="item-value">{{shoulderHeight ? shoulderHeight + 'cm' : '去设置'}}</text>
+					<image class="item-arrow" src="/static/my/arrowright.png" mode="aspectFill"></image>
+				</view>
+			</view>
+			<view class="info-item" @click="editNumberField('shoulderWidth')">
+				<text class="item-label">肩宽</text>
+				<view class="item-right">
+					<text class="item-value">{{shoulderWidth ? shoulderWidth + 'cm' : '去设置'}}</text>
+					<image class="item-arrow" src="/static/my/arrowright.png" mode="aspectFill"></image>
+				</view>
+			</view>
+			<view class="info-item" @click="editNumberField('weight')">
+				<text class="item-label">体重</text>
+				<view class="item-right">
+					<text class="item-value">{{weight ? weight + 'kg' : '去设置'}}</text>
+					<image class="item-arrow" src="/static/my/arrowright.png" mode="aspectFill"></image>
+				</view>
+			</view>
 			<view class="info-item">
 				<text class="item-label">手机号</text>
 				<view class="item-right">
 					<text class="item-value">{{phone}}</text>
 				</view>
 			</view>
-			<view class="info-item">
+			<view class="info-item" style="border-bottom-width: 0;">
 				<text class="item-label">AppId</text>
 				<view class="item-right">
 					<text class="item-value">{{appid}}</text>
@@ -54,6 +89,27 @@
 				</view>
 			</view>
 		</view>
+		<!-- 数字输入弹窗 -->
+		<view class="nickname-overlay" v-if="showNumberPopup" @click="closeNumberPopup">
+			<view class="nickname-dialog" @click.stop="" :style="{marginBottom: keyboardHeight + 'px'}">
+				<text class="dialog-title">修改{{ getNumberFieldLabel() }}</text>
+				<view class="input-box">
+					<input class="nickname-input" v-model="numberInput" :type="currentEditField === 'age' ? 'number' : 'digit'" :placeholder="'请输入' + getNumberFieldLabel()" placeholder-class="input-placeholder"
+						:adjust-position="false" :focus="showNumberPopup"
+						@keyboardheightchange="onKeyboardHeight" />
+				</view>
+				<view class="dialog-btns">
+					<view class="dialog-btn" @click="closeNumberPopup">
+						<text class="dialog-cancel">取消</text>
+					</view>
+					<view class="dialog-divider"></view>
+					<view class="dialog-btn" @click="saveNumberField">
+						<text class="dialog-confirm">确定</text>
+					</view>
+				</view>
+			</view>
+		</view>
+
 		<ay-toast ref="ayToast" />
 	</view>
 </template>
@@ -72,8 +128,17 @@ export default {
 			nickname: '用户',
 			phone: '',
 			appid: '',
+			gender: 1, // 1男 2女
+			age: '',
+			shoulderHeight: '',
+			shoulderWidth: '',
+			weight: '',
 			showNicknamePopup: false,
+			showGenderPopup: false,
+			showNumberPopup: false,
 			nicknameInput: '',
+			numberInput: '',
+			currentEditField: '', // 'age' | 'shoulderHeight' | 'shoulderWidth' | 'weight'
 			keyboardHeight: 0
 		}
 	},
@@ -86,10 +151,17 @@ export default {
 			const nickname = uni.getStorageSync('nickname') || '用户'
 			const appid = uni.getStorageSync('appId') || ''
 			const avatar = uni.getStorageSync('avatar') || '/static/144x144.png'
+			
 			this.phone = phone
 			this.nickname = nickname
 			this.appid = appid
 			this.avatar = avatar
+			
+			this.gender = uni.getStorageSync('gender') || 1
+			this.age = uni.getStorageSync('age') || ''
+			this.shoulderHeight = uni.getStorageSync('shoulderHeight') || ''
+			this.shoulderWidth = uni.getStorageSync('shoulderWidth') || ''
+			this.weight = uni.getStorageSync('weight') || ''
 		},
 		changeAvatar() {
 			uni.chooseImage({
@@ -145,6 +217,74 @@ export default {
 			} catch (e) {
 				// 错误已由 http 层统一提示
 			}
+		},
+		editGender() {
+			uni.showActionSheet({
+				itemList: ['男', '女'],
+				success: (res) => {
+					const val = res.tapIndex === 0 ? 1 : 2
+					this.gender = val
+					uni.setStorageSync('gender', val)
+					// TODO: 如果后端支持,在这里调用 updateProfile({ gender: val })
+					this.$refs.ayToast.success('修改成功')
+				}
+			})
+		},
+		editNumberField(field) {
+			this.currentEditField = field
+			this.numberInput = String(this[field] || '')
+			this.showNumberPopup = true
+		},
+		closeNumberPopup() {
+			this.showNumberPopup = false
+			this.keyboardHeight = 0
+		},
+		getNumberFieldLabel() {
+			const map = {
+				age: '年龄',
+				shoulderHeight: '肩高',
+				shoulderWidth: '肩宽',
+				weight: '体重'
+			}
+			return map[this.currentEditField] || ''
+		},
+		saveNumberField() {
+			const val = this.numberInput.trim()
+			if (!val) {
+				this.$refs.ayToast.error(`请输入${this.getNumberFieldLabel()}`)
+				return
+			}
+			
+			const num = Number(val)
+			if (isNaN(num) || num <= 0) {
+				this.$refs.ayToast.error('请输入有效的数字')
+				return
+			}
+			
+			if (this.currentEditField === 'shoulderHeight' && (num < 50 || num > 250)) {
+				this.$refs.ayToast.error('肩高范围为50-250cm')
+				return
+			}
+			if (this.currentEditField === 'shoulderWidth' && (num < 20 || num > 80)) {
+				this.$refs.ayToast.error('肩宽范围为20-80cm')
+				return
+			}
+			if (this.currentEditField === 'weight' && (num < 10 || num > 300)) {
+				this.$refs.ayToast.error('体重范围为10-300kg')
+				return
+			}
+			if (this.currentEditField === 'age' && (num < 1 || num > 150)) {
+				this.$refs.ayToast.error('年龄范围为1-150岁')
+				return
+			}
+			
+			this[this.currentEditField] = num
+			uni.setStorageSync(this.currentEditField, num)
+			// TODO: 如果后端支持,在这里调用 updateProfile({ [this.currentEditField]: num })
+			
+			this.showNumberPopup = false
+			this.keyboardHeight = 0
+			this.$refs.ayToast.success('修改成功')
 		}
 	},
 	onShow() {

+ 2 - 2
code/ajyApp/utils/http/config.js

@@ -11,9 +11,9 @@ baseUrl = '/api' // H5 走代理
 // #endif
 
 // #ifdef APP-PLUS || MP-WEIXIN || MP-ALIPAY
-baseUrl = 'http://192.168.1.4:18888/api' // TODO: 替换为实际接口域名
+baseUrl = 'http://110.177.148.159:18888/api' // TODO: 替换为实际接口域名
 // #endif
-// http://ajy.iwintrue.com/api
+ // baseUrl = 'http://ajy.iwintrue.com/api'
 export default {
   // 接口基础地址
   baseUrl,