Authored by qxm

训计页面新增,我的。

Showing 62 changed files with 2707 additions and 612 deletions
@@ -4,11 +4,12 @@ SHOPRO_VERSION=v2.4.1 @@ -4,11 +4,12 @@ SHOPRO_VERSION=v2.4.1
4 # 后端接口 - 正式环境(通过 process.env.NODE_ENV 非 development) 4 # 后端接口 - 正式环境(通过 process.env.NODE_ENV 非 development)
5 # SHOPRO_BASE_URL=http://api-dashboard.yudao.iocoder.cn 5 # SHOPRO_BASE_URL=http://api-dashboard.yudao.iocoder.cn
6 # SHOPRO_BASE_URL=http://mall.hcxtec.com 6 # SHOPRO_BASE_URL=http://mall.hcxtec.com
  7 +SHOPRO_BASE_URL=https://xunji.geaktec.com
7 8
8 # 后端接口 - 测试环境(通过 process.env.NODE_ENV = development) 9 # 后端接口 - 测试环境(通过 process.env.NODE_ENV = development)
9 -#SHOPRO_DEV_BASE_URL=http://192.168.1.200:48081 10 +# SHOPRO_DEV_BASE_URL=http://192.168.1.200:48081
10 # SHOPRO_DEV_BASE_URL=http://192.168.1.85:48080 11 # SHOPRO_DEV_BASE_URL=http://192.168.1.85:48080
11 -SHOPRO_DEV_BASE_URL=https:/xunji.geaktec.com 12 +SHOPRO_DEV_BASE_URL=https://xunji.geaktec.com
12 # SHOPRO_DEV_BASE_URL=http://api-dashboard.yudao.iocoder.cn/ 13 # SHOPRO_DEV_BASE_URL=http://api-dashboard.yudao.iocoder.cn/
13 ### SHOPRO_DEV_BASE_URL=http://10.171.1.188:48080 14 ### SHOPRO_DEV_BASE_URL=http://10.171.1.188:48080
14 ### SHOPRO_DEV_BASE_URL = http://yunai.natapp1.cc 15 ### SHOPRO_DEV_BASE_URL = http://yunai.natapp1.cc
@@ -178,7 +178,39 @@ @@ -178,7 +178,39 @@
178 "navigationBarTitleText": "个人资料", 178 "navigationBarTitleText": "个人资料",
179 "navigationStyle": "default" 179 "navigationStyle": "default"
180 } 180 }
181 - } 181 + },
  182 + {
  183 + "path": "pages/user/wode-changjian-wenti",
  184 + "style": {
  185 + "navigationBarTitleText": "我的常见问题"
  186 + }
  187 + },
  188 +
  189 + {
  190 + "path": "pages/user/wode-guanyu-hongxing",
  191 + "style": {
  192 + "navigationBarTitleText": "我的关于鸿星"
  193 + }
  194 + },
  195 + {
  196 + "path": "pages/user/wode-jiankang-ziliao",
  197 + "style": {
  198 + "navigationBarTitleText": "我的健康资料"
  199 + }
  200 + },
  201 + {
  202 + "path": "pages/user/wode-lianxi-kefu",
  203 + "style": {
  204 + "navigationBarTitleText": "我的联系客服"
  205 + }
  206 + },
  207 + {
  208 + "path": "pages/user/wode-shezhi",
  209 + "style": {
  210 + "navigationBarTitleText": "设置",
  211 + "navigationStyle": "default"
  212 + }
  213 + }
182 ] 214 ]
183 } 215 }
184 ], 216 ],
@@ -26,7 +26,7 @@ @@ -26,7 +26,7 @@
26 </view> 26 </view>
27 27
28 <!-- --> 28 <!-- -->
29 - <view class="vip-banner" hover-class="opacity-hover" @click="goAddVip"> 29 + <!-- <view class="vip-banner" hover-class="opacity-hover" @click="goAddVip">
30 <view class="vip-info"> 30 <view class="vip-info">
31 <uni-icons type="vip-filled" size="22" color="#f1c40f" /> 31 <uni-icons type="vip-filled" size="22" color="#f1c40f" />
32 <text class="vip-text"> 32 <text class="vip-text">
@@ -34,25 +34,25 @@ @@ -34,25 +34,25 @@
34 </text> 34 </text>
35 </view> 35 </view>
36 <view class="vip-btn">{{ userInfo.deposit === 1 ? '查看权益' : '立即开通' }}</view> 36 <view class="vip-btn">{{ userInfo.deposit === 1 ? '查看权益' : '立即开通' }}</view>
37 - </view> 37 + </view> -->
38 38
39 <!-- 课程状态快速入口 --> 39 <!-- 课程状态快速入口 -->
40 - <view class="section-card quick-entry"> 40 + <!-- <view class="section-card quick-entry">
41 <view v-for="entry in quickEntryConfig" :key="entry.type" class="entry-item" hover-class="opacity-hover" 41 <view v-for="entry in quickEntryConfig" :key="entry.type" class="entry-item" hover-class="opacity-hover"
42 @click="handleQuickEntry(entry.type)"> 42 @click="handleQuickEntry(entry.type)">
43 <text class="num">{{ userInfo[entry.key] || 0 }}</text> 43 <text class="num">{{ userInfo[entry.key] || 0 }}</text>
44 <text class="label">{{ entry.label }}</text> 44 <text class="label">{{ entry.label }}</text>
45 </view> 45 </view>
46 - </view> 46 + </view> -->
47 47
48 <!-- 广告位 --> 48 <!-- 广告位 -->
49 - <view class="banner-box" @click="goJiamen"> 49 + <!-- <view class="banner-box" @click="goJiamen">
50 <image src="https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/4_1773627891703.png" 50 <image src="https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/4_1773627891703.png"
51 mode="aspectFill" class="banner-img" /> 51 mode="aspectFill" class="banner-img" />
52 - </view> 52 + </view> -->
53 53
54 <!-- 核心应用区 --> 54 <!-- 核心应用区 -->
55 - <view class="section-card apply-section"> 55 + <!-- <view class="section-card apply-section">
56 <view v-for="(item, index) in APPLY_CONFIG_LIST" :key="index" class="apply-item" 56 <view v-for="(item, index) in APPLY_CONFIG_LIST" :key="index" class="apply-item"
57 @click="authNavigateTo(item.url)"> 57 @click="authNavigateTo(item.url)">
58 <view class="icon-bg"> 58 <view class="icon-bg">
@@ -60,10 +60,10 @@ @@ -60,10 +60,10 @@
60 </view> 60 </view>
61 <text class="text">{{ item.text }}</text> 61 <text class="text">{{ item.text }}</text>
62 </view> 62 </view>
63 - </view> 63 + </view> -->
64 64
65 <!-- 资产账户网格区 --> 65 <!-- 资产账户网格区 -->
66 - <view v-if="userStore.isLogin" class="section-card"> 66 + <!-- <view v-if="userStore.isLogin" class="section-card">
67 <view class="account-grid"> 67 <view class="account-grid">
68 <view v-for="(acc, idx) in accountConfig" :key="idx" class="account-item" @click="authNavigateTo(acc.url)"> 68 <view v-for="(acc, idx) in accountConfig" :key="idx" class="account-item" @click="authNavigateTo(acc.url)">
69 <text class="acc-lab">{{ acc.label }}</text> 69 <text class="acc-lab">{{ acc.label }}</text>
@@ -73,7 +73,7 @@ @@ -73,7 +73,7 @@
73 </text> 73 </text>
74 </view> 74 </view>
75 </view> 75 </view>
76 - </view> 76 + </view> -->
77 77
78 <!-- 功能矩阵九宫格 --> 78 <!-- 功能矩阵九宫格 -->
79 <view class="section-card icon-grid-box"> 79 <view class="section-card icon-grid-box">
@@ -100,10 +100,10 @@ @@ -100,10 +100,10 @@
100 <text class="setting-text">个人设置</text> 100 <text class="setting-text">个人设置</text>
101 <uni-icons type="right" size="14" color="#E0E0E0" /> 101 <uni-icons type="right" size="14" color="#E0E0E0" />
102 </view> 102 </view>
103 - <view class="setting-item" @click="authNavigateTo('/pages5/pages/user/wode-yinsishezhi')"> 103 + <!-- <view class="setting-item" @click="authNavigateTo('/pages5/pages/user/wode-yinsishezhi')">
104 <text class="setting-text">隐私中心</text> 104 <text class="setting-text">隐私中心</text>
105 <uni-icons type="right" size="14" color="#E0E0E0" /> 105 <uni-icons type="right" size="14" color="#E0E0E0" />
106 - </view> 106 + </view> -->
107 </view> 107 </view>
108 108
109 <Tabbar /> 109 <Tabbar />
@@ -130,7 +130,30 @@ const memberLevelName = ref(''); @@ -130,7 +130,30 @@ const memberLevelName = ref('');
130 // 固定的 UI 配置 130 // 固定的 UI 配置
131 const APPLY_CONFIG_LIST = []; 131 const APPLY_CONFIG_LIST = [];
132 132
133 -const FUNCTION_CONFIG_LIST = []; 133 +const FUNCTION_CONFIG_LIST = [
  134 + {
  135 + text: '健康资料',
  136 + url: '/pages5/pages/user/wode-jiankang-ziliao',
  137 + icon: '/static/icons/md-folder_open 1@1x.png',
  138 + },
  139 + {
  140 + text: '常见问题',
  141 + url: '/pages5/pages/user/wode-changjian-wenti',
  142 + icon: '/static/icons/iconPark-helpcenter 1@1x.png',
  143 + },
  144 + {
  145 + text: '联系客服',
  146 + url: '/pages5/pages/user/wode-lianxi-kefu',
  147 + icon: '/static/icons/riLine-customer-service-2-line 1@1x.png',
  148 + },
  149 +
  150 + {
  151 + text: '关于鸿星',
  152 + url: '/pages5/pages/user/wode-guanyu-hongxing',
  153 + icon: '/static/icons/antOutline-exclamation-circle 1@1x.png',
  154 + },
  155 +];
  156 +
134 157
135 // 课程计数状态配置映射 158 // 课程计数状态配置映射
136 const quickEntryConfig = [ 159 const quickEntryConfig = [
@@ -191,22 +214,23 @@ const handleGridItemClick = (item) => { @@ -191,22 +214,23 @@ const handleGridItemClick = (item) => {
191 */ 214 */
192 const fetchPageData = async () => { 215 const fetchPageData = async () => {
193 try { 216 try {
194 - const [userRes, levelRes] = await Promise.all([  
195 - UserApi.getUserInfo(),  
196 - MemberApi.getMemberLevel(),  
197 - ]); 217 + // const [userRes, levelRes] = await Promise.all([
  218 + // UserApi.getUserInfo(),
  219 + // // MemberApi.getMemberLevel(),
  220 + // ]);
  221 + const userRes = UserApi.getUserInfo();
198 222
199 const rawUser = userRes.data || {}; 223 const rawUser = userRes.data || {};
200 userInfo.value = rawUser; 224 userInfo.value = rawUser;
201 225
202 // 架构重构:数据拉取后一次性计算出等级映射结果,拒绝在 computed 内部循环执行实例化 226 // 架构重构:数据拉取后一次性计算出等级映射结果,拒绝在 computed 内部循环执行实例化
203 - const levels = levelRes.data?.detailList || [];  
204 - if (rawUser.level !== undefined && levels.length > 0) {  
205 - const target = levels.find((item) => item.id === rawUser.level);  
206 - memberLevelName.value = target ? target.name : '';  
207 - } else {  
208 - memberLevelName.value = '';  
209 - } 227 + // const levels = levelRes.data?.detailList || [];
  228 + // if (rawUser.level !== undefined) {
  229 + // // const target = levels.find((item) => item.id === rawUser.level);
  230 + // // memberLevelName.value = target ? target.name : '';
  231 + // } else {
  232 + // // memberLevelName.value = '';
  233 + // }
210 } catch (error) { 234 } catch (error) {
211 console.error('[API Error] 拉取个人资产信息流失败:', error); 235 console.error('[API Error] 拉取个人资产信息流失败:', error);
212 } 236 }
1 <template> 1 <template>
2 <view class="home-page" v-if="userStore.isLogin"> 2 <view class="home-page" v-if="userStore.isLogin">
3 <view class="tab-bar" :style="{ paddingTop: menuButtonHeight + topSafeArea + 'px' }"> 3 <view class="tab-bar" :style="{ paddingTop: menuButtonHeight + topSafeArea + 'px' }">
4 - <view class="tab-item" :class="{ active: currentTab === 0 }" @click="handleTabClick(0)">训记</view> 4 + <!-- <view class="tab-item" :class="{ active: currentTab === 0 }" @click="handleTabClick(0)">训记</view> -->
5 <view class="tab-item" :class="{ active: currentTab === 1 }" @click="handleTabClick(1)">计划</view> 5 <view class="tab-item" :class="{ active: currentTab === 1 }" @click="handleTabClick(1)">计划</view>
6 <view class="tab-item" :class="{ active: currentTab === 2 }" @click="handleTabClick(2)">日历</view> 6 <view class="tab-item" :class="{ active: currentTab === 2 }" @click="handleTabClick(2)">日历</view>
7 <view class="tab-item" :class="{ active: currentTab === 3 }" @click="handleTabClick(3)">动作</view> 7 <view class="tab-item" :class="{ active: currentTab === 3 }" @click="handleTabClick(3)">动作</view>
@@ -13,7 +13,7 @@ @@ -13,7 +13,7 @@
13 </view> 13 </view>
14 14
15 <view class="content"> 15 <view class="content">
16 - <xunjiXunji v-if="currentTab === 0" /> 16 + <!-- <xunjiXunji v-if="currentTab === 0" /> -->
17 <xunjiXunlianjihua v-if="currentTab === 1" /> 17 <xunjiXunlianjihua v-if="currentTab === 1" />
18 <xunjiRili v-if="currentTab === 2" /> 18 <xunjiRili v-if="currentTab === 2" />
19 <xunjiDongzuo v-if="currentTab === 3" /> 19 <xunjiDongzuo v-if="currentTab === 3" />
@@ -105,7 +105,7 @@ const trainingStore = useTrainingStore(); @@ -105,7 +105,7 @@ const trainingStore = useTrainingStore();
105 const userStore = useUserStore(); 105 const userStore = useUserStore();
106 106
107 // --- 补全缺失的响应式变量定义 --- 107 // --- 补全缺失的响应式变量定义 ---
108 -const currentTab = ref(0); 108 +const currentTab = ref(1);
109 const currentComponent = shallowRef(xunjiXunji); // 用于 H5 端动态组件切换(如保留原功能逻辑) 109 const currentComponent = shallowRef(xunjiXunji); // 用于 H5 端动态组件切换(如保留原功能逻辑)
110 const drawer = ref(null); // uni-drawer 的组件实例引用 110 const drawer = ref(null); // uni-drawer 的组件实例引用
111 111
@@ -13,7 +13,7 @@ @@ -13,7 +13,7 @@
13 <view v-if="showMusclePicker" class="popup-overlay" @click="closeAllPopups"> 13 <view v-if="showMusclePicker" class="popup-overlay" @click="closeAllPopups">
14 <view class="popup-content" @click.stop> 14 <view class="popup-content" @click.stop>
15 <view class="popup-header"> 15 <view class="popup-header">
16 - <view class="close-btn" @click="closeAllPopups">×</view> 16 + <view class="close-btn" @click="closeAllPopups">x</view>
17 <view class="title">选择训练部位</view> 17 <view class="title">选择训练部位</view>
18 <view class="confirm-btn" @click="confirmMuscleSelection">完成</view> 18 <view class="confirm-btn" @click="confirmMuscleSelection">完成</view>
19 </view> 19 </view>
@@ -323,7 +323,11 @@ const openCoverSelector = () => { @@ -323,7 +323,11 @@ const openCoverSelector = () => {
323 // 封面上传 323 // 封面上传
324 try { 324 try {
325 const result = await FileApi.uploadFile(path); 325 const result = await FileApi.uploadFile(path);
  326 + console.log(res, 'res');
  327 +
326 coverImagePath.value = result.data; 328 coverImagePath.value = result.data;
  329 + console.log('coverImagePath', coverImagePath.value);
  330 +
327 coverUploaded.value = true; 331 coverUploaded.value = true;
328 uni.showToast({ 332 uni.showToast({
329 title: "封面上传成功", 333 title: "封面上传成功",
  1 +<template>
  2 + <view class="level-page">
  3 + <!-- 导航栏 -->
  4 + <uni-nav-bar title="华创信" left-icon="left" @click-left="goBack" :fixed="true" :status-bar="true" />
  5 + <view class="top-right">
  6 + <!-- <u-icon name="document" size="24" color="#fff" @click="showRules"></u-icon> -->
  7 + <text class="right-text">华创信</text>
  8 + </view>
  9 +
  10 + <!-- 用户信息 -->
  11 + <view class="user-info">
  12 + <image src="https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/order-empty_1773628059920.png"
  13 + mode="aspectFill" class="avatar"></image>
  14 + <text class="username">华创信开发者</text>
  15 + </view>
  16 +
  17 + <!-- 更多权益 -->
  18 + <view class="more-benefits">
  19 + <view class="more-text">华创信开发者</view>
  20 + <view class="more-text">系统开发中</view>
  21 + <view class="more-text">敬请期待~</view>
  22 + </view>
  23 + </view>
  24 +</template>
  25 +
  26 +<script>
  27 +export default {
  28 + methods: {
  29 + goBack() {
  30 + uni.navigateBack();
  31 + },
  32 + showRules() {
  33 + uni.showToast({ title: '查看开发内容', icon: 'success' });
  34 + },
  35 + },
  36 +};
  37 +</script>
  38 +
  39 +<style scoped>
  40 +.level-page {
  41 + position: relative;
  42 + width: 100%;
  43 + min-height: 100vh;
  44 + background-color: #1a1a1a;
  45 + color: white;
  46 + padding-bottom: 40rpx;
  47 +}
  48 +
  49 +.top-right {
  50 + position: absolute;
  51 + top: 20rpx;
  52 + right: 20rpx;
  53 + display: flex;
  54 + align-items: center;
  55 + gap: 10rpx;
  56 + color: white;
  57 + font-size: 24rpx;
  58 +}
  59 +
  60 +.user-info {
  61 + display: flex;
  62 + align-items: center;
  63 + gap: 20rpx;
  64 + padding: 20rpx;
  65 + margin-top: 20rpx;
  66 +}
  67 +
  68 +.avatar {
  69 + width: 60rpx;
  70 + height: 60rpx;
  71 + border-radius: 50%;
  72 + object-fit: cover;
  73 +}
  74 +
  75 +.username {
  76 + font-size: 28rpx;
  77 +}
  78 +
  79 +.level-card {
  80 + width: 600rpx;
  81 + height: 300rpx;
  82 + background-color: rgba(173, 216, 230, 0.3);
  83 + border-radius: 20rpx;
  84 + padding: 30rpx;
  85 + margin: 20rpx auto;
  86 + position: relative;
  87 + overflow: hidden;
  88 +}
  89 +
  90 +.level-name {
  91 + font-size: 40rpx;
  92 + font-weight: bold;
  93 + margin-bottom: 10rpx;
  94 +}
  95 +
  96 +.level-en {
  97 + font-size: 36rpx;
  98 + font-weight: bold;
  99 + margin-bottom: 20rpx;
  100 +}
  101 +
  102 +.require {
  103 + font-size: 24rpx;
  104 + margin-bottom: 20rpx;
  105 +}
  106 +
  107 +.detail {
  108 + font-size: 24rpx;
  109 + color: #ccc;
  110 +}
  111 +
  112 +.progress-bar {
  113 + width: 600rpx;
  114 + height: 20rpx;
  115 + background-color: #333;
  116 + border-radius: 10rpx;
  117 + margin: 20rpx auto;
  118 + position: relative;
  119 + overflow: hidden;
  120 +}
  121 +
  122 +.progress-track {
  123 + width: 100%;
  124 + height: 100%;
  125 + background-color: #333;
  126 + border-radius: 10rpx;
  127 +}
  128 +
  129 +.progress-fill {
  130 + width: 20%;
  131 + height: 100%;
  132 + background-color: #007aff;
  133 + border-radius: 10rpx;
  134 +}
  135 +
  136 +.progress-dot {
  137 + position: absolute;
  138 + top: 50%;
  139 + left: 20%;
  140 + transform: translate(-50%, -50%);
  141 + width: 12rpx;
  142 + height: 12rpx;
  143 + background-color: #007aff;
  144 + border-radius: 50%;
  145 +}
  146 +
  147 +.benefits-section {
  148 + margin: 20rpx;
  149 + padding: 0 20rpx;
  150 +}
  151 +
  152 +.section-title {
  153 + font-size: 32rpx;
  154 + margin-bottom: 20rpx;
  155 + padding-left: 10rpx;
  156 +}
  157 +
  158 +.benefit-list {
  159 + display: flex;
  160 + flex-direction: row;
  161 + justify-content: space-between;
  162 + gap: 16rpx;
  163 +}
  164 +
  165 +.benefit-item {
  166 + width: 180rpx;
  167 + background-color: rgba(255, 255, 255, 0.1);
  168 + border-radius: 12rpx;
  169 + padding: 16rpx 12rpx;
  170 + text-align: center;
  171 + box-sizing: border-box;
  172 +}
  173 +
  174 +.icon {
  175 + width: 36rpx;
  176 + height: 36rpx;
  177 + object-fit: cover;
  178 + margin-bottom: 8rpx;
  179 +}
  180 +
  181 +.benefit-name {
  182 + font-size: 22rpx;
  183 + margin-bottom: 4rpx;
  184 + font-weight: bold;
  185 +}
  186 +
  187 +.benefit-desc {
  188 + font-size: 18rpx;
  189 + color: #ccc;
  190 + line-height: 1.4;
  191 +}
  192 +
  193 +.time-tag {
  194 + font-size: 18rpx;
  195 + color: white;
  196 + background-color: #ff6b00;
  197 + padding: 4rpx 8rpx;
  198 + border-radius: 8rpx;
  199 + margin-top: 8rpx;
  200 + display: inline-block;
  201 +}
  202 +
  203 +.more-benefits {
  204 + margin: 30rpx 20rpx 0;
  205 + padding: 20rpx;
  206 + background-color: rgba(255, 255, 255, 0.1);
  207 + border-radius: 12rpx;
  208 + text-align: center;
  209 +}
  210 +
  211 +.more-text {
  212 + font-size: 24rpx;
  213 + color: #ccc;
  214 + margin-bottom: 10rpx;
  215 +}
  216 +</style>
@@ -2,14 +2,9 @@ @@ -2,14 +2,9 @@
2 <view class="profile-page"> 2 <view class="profile-page">
3 <view class="avatar-section"> 3 <view class="avatar-section">
4 <button class="avatar-edit-btn" open-type="chooseAvatar" @chooseavatar="handleChooseAvatar"> 4 <button class="avatar-edit-btn" open-type="chooseAvatar" @chooseavatar="handleChooseAvatar">
5 - <image  
6 - :src="  
7 - formData.avatar ||  
8 - 'https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260526/默认头像_1779779926983.png'  
9 - "  
10 - mode="aspectFill"  
11 - class="avatar-image"  
12 - ></image> 5 + <image :src="formData.avatar ||
  6 + 'https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260526/默认头像_1779779926983.png'
  7 + " mode="aspectFill" class="avatar-image"></image>
13 </button> 8 </button>
14 </view> 9 </view>
15 10
@@ -23,25 +18,13 @@ @@ -23,25 +18,13 @@
23 <view class="list-item" @click="gotoSelectGender"> 18 <view class="list-item" @click="gotoSelectGender">
24 <text class="item-title">性别</text> 19 <text class="item-title">性别</text>
25 <view class="item-value">{{ formatGender(formData.sex) }}</view> 20 <view class="item-value">{{ formatGender(formData.sex) }}</view>
26 - <uni-icons  
27 - v-if="formData.isAllowUpdSex"  
28 - type="right"  
29 - color="#999"  
30 - size="20"  
31 - class="arrow-icon"  
32 - ></uni-icons> 21 + <uni-icons v-if="formData.isAllowUpdSex" type="right" color="#999" size="20" class="arrow-icon"></uni-icons>
33 </view> 22 </view>
34 23
35 <view class="list-item"> 24 <view class="list-item">
36 <text class="item-title">地区</text> 25 <text class="item-title">地区</text>
37 - <picker  
38 - mode="multiSelector"  
39 - :range="multiArray"  
40 - range-key="name"  
41 - :value="multiIndex"  
42 - @change="pickerChange"  
43 - @columnchange="pickerColumnChange"  
44 - > 26 + <picker mode="multiSelector" :range="multiArray" range-key="name" :value="multiIndex" @change="pickerChange"
  27 + @columnchange="pickerColumnChange">
45 <view class="picker" :class="{ 'placeholder-txt': !selectedAddress }"> 28 <view class="picker" :class="{ 'placeholder-txt': !selectedAddress }">
46 {{ selectedAddress || '请选择省市区' }} 29 {{ selectedAddress || '请选择省市区' }}
47 </view> 30 </view>
@@ -51,13 +34,8 @@ @@ -51,13 +34,8 @@
51 <view class="list-item signature-item"> 34 <view class="list-item signature-item">
52 <text class="item-title">个性签名</text> 35 <text class="item-title">个性签名</text>
53 <view class="signature-input-wrapper"> 36 <view class="signature-input-wrapper">
54 - <textarea  
55 - v-model="formData.signature"  
56 - placeholder="请输入个性签名..."  
57 - :maxlength="50"  
58 - class="signature-input"  
59 - auto-height  
60 - /> 37 + <textarea v-model="formData.signature" placeholder="请输入个性签名..." :maxlength="50" class="signature-input"
  38 + auto-height />
61 <text class="char-count">{{ formData.signature?.length || 0 }}/50</text> 39 <text class="char-count">{{ formData.signature?.length || 0 }}/50</text>
62 </view> 40 </view>
63 </view> 41 </view>
@@ -80,13 +58,8 @@ @@ -80,13 +58,8 @@
80 </view> 58 </view>
81 <view class="popup-body"> 59 <view class="popup-body">
82 <view class="input-wrap"> 60 <view class="input-wrap">
83 - <input  
84 - class="nickname-input"  
85 - placeholder="请输入新昵称"  
86 - type="nickname"  
87 - v-model="editCacheData.nickname"  
88 - maxlength="20"  
89 - /> 61 + <input class="nickname-input" placeholder="请输入新昵称" type="nickname" v-model="editCacheData.nickname"
  62 + maxlength="20" />
90 </view> 63 </view>
91 </view> 64 </view>
92 <view class="popup-footer"> 65 <view class="popup-footer">
@@ -105,18 +78,10 @@ @@ -105,18 +78,10 @@
105 </view> 78 </view>
106 <view class="popup-body"> 79 <view class="popup-body">
107 <view class="gender-options"> 80 <view class="gender-options">
108 - <view  
109 - class="gender-item"  
110 - :class="{ selected: editCacheData.sex === 1 }"  
111 - @click="selectGender(1)"  
112 - > 81 + <view class="gender-item" :class="{ selected: editCacheData.sex === 1 }" @click="selectGender(1)">
113 <text class="gender-text">男</text> 82 <text class="gender-text">男</text>
114 </view> 83 </view>
115 - <view  
116 - class="gender-item"  
117 - :class="{ selected: editCacheData.sex === 2 }"  
118 - @click="selectGender(2)"  
119 - > 84 + <view class="gender-item" :class="{ selected: editCacheData.sex === 2 }" @click="selectGender(2)">
120 <text class="gender-text">女</text> 85 <text class="gender-text">女</text>
121 </view> 86 </view>
122 </view> 87 </view>
@@ -128,594 +93,580 @@ @@ -128,594 +93,580 @@
128 </template> 93 </template>
129 94
130 <script setup> 95 <script setup>
131 - import { ref, reactive, nextTick } from 'vue';  
132 - import { onShow, onBackPress } from '@dcloudio/uni-app';  
133 - import dayjs from 'dayjs';  
134 - import UserApi from '@/sheep/api/member/user';  
135 - import AreaApi from '@/sheep/api/system/area';  
136 - import FileApi from '@/sheep/api/infra/file';  
137 - import useUserStore from '@/sheep/store/user';  
138 -  
139 - const userStore = useUserStore();  
140 -  
141 - // 弹窗 DOM 引用声明  
142 - const nicknamePopupRef = ref(null);  
143 - const genderPopupRef = ref(null);  
144 -  
145 - // 状态与防抖  
146 - const saveLoading = ref(false);  
147 - const hasModified = ref(false); // 侦听变更状态  
148 -  
149 - // 核心业务隔离表单  
150 - const formData = ref({  
151 - avatar: '',  
152 - nickname: '',  
153 - sex: 0,  
154 - province: '',  
155 - city: '',  
156 - region: '',  
157 - signature: '',  
158 - createTime: '',  
159 - isAllowUpdSex: false,  
160 - }); 96 +import { ref, reactive, nextTick } from 'vue';
  97 +import { onShow, onBackPress } from '@dcloudio/uni-app';
  98 +import dayjs from 'dayjs';
  99 +import UserApi from '@/sheep/api/member/user';
  100 +import AreaApi from '@/sheep/api/system/area';
  101 +import FileApi from '@/sheep/api/infra/file';
  102 +import useUserStore from '@/sheep/store/user';
  103 +
  104 +const userStore = useUserStore();
  105 +
  106 +// 弹窗 DOM 引用声明
  107 +const nicknamePopupRef = ref(null);
  108 +const genderPopupRef = ref(null);
  109 +
  110 +// 状态与防抖
  111 +const saveLoading = ref(false);
  112 +const hasModified = ref(false); // 侦听变更状态
  113 +
  114 +// 核心业务隔离表单
  115 +const formData = ref({
  116 + avatar: '',
  117 + nickname: '',
  118 + sex: 0,
  119 + province: '',
  120 + city: '',
  121 + region: '',
  122 + signature: '',
  123 + createTime: '',
  124 + isAllowUpdSex: false,
  125 +});
  126 +
  127 +// 深度解耦的弹窗缓存数据
  128 +const editCacheData = reactive({
  129 + nickname: '',
  130 + sex: 0,
  131 +});
  132 +
  133 +// 省市区级联选择器状态流
  134 +const addressTree = ref([]);
  135 +const multiArray = ref([[], [], []]);
  136 +const multiIndex = ref([0, 0, 0]);
  137 +const selectedAddress = ref('');
  138 +
  139 +/**
  140 + * 宿主生命周期钩子流
  141 + */
  142 +onShow(async () => {
  143 + try {
  144 + // 1. 同步加载行政地区三级树(内部已作 Session 缓存,避免重复请求)
  145 + await fetchAreaTree();
  146 +
  147 + // 2. 深度克隆 Store 数据,隔离表单与 Store 的直接联动,保持单向数据流
  148 + if (userStore.userInfo) {
  149 + formData.value = JSON.parse(JSON.stringify(userStore.userInfo));
  150 + initEchoAddress();
  151 + }
  152 + } catch (error) {
  153 + console.error('[Init Error] 初始化个人信息数据流失败:', error);
  154 + }
  155 +});
  156 +
  157 +/**
  158 + * 拦截非保存下的非正常退出,防止用户误触返回造成数据丢失
  159 + */
  160 +onBackPress((options) => {
  161 + // 监听表单内容变动(仅在未保存且已有修改时提示)
  162 + const isUnsaved = JSON.stringify(formData.value) !== JSON.stringify(userStore.userInfo);
  163 + if (isUnsaved && !saveLoading.value) {
  164 + uni.showModal({
  165 + title: '提示',
  166 + content: '您有尚未保存的修改,确定要离开吗?',
  167 + success: (res) => {
  168 + if (res.confirm) {
  169 + hasModified.value = false;
  170 + uni.navigateBack({ delta: 1 });
  171 + }
  172 + },
  173 + });
  174 + return true; // 拦截返回键
  175 + }
  176 + return false;
  177 +});
  178 +
  179 +/**
  180 + * 头像上传逻辑:增加微信ChooseAvatar路径格式预检与文件防崩溃重试
  181 + */
  182 +const handleChooseAvatar = async (e) => {
  183 + const { avatarUrl } = e.detail;
  184 +
  185 +
  186 + if (!avatarUrl) return;
161 187
162 - // 深度解耦的弹窗缓存数据  
163 - const editCacheData = reactive({  
164 - nickname: '',  
165 - sex: 0,  
166 - });  
167 188
168 - // 省市区级联选择器状态流  
169 - const addressTree = ref([]);  
170 - const multiArray = ref([[], [], []]);  
171 - const multiIndex = ref([0, 0, 0]);  
172 - const selectedAddress = ref('');  
173 -  
174 - /**  
175 - * 宿主生命周期钩子流  
176 - */  
177 - onShow(async () => {  
178 - try {  
179 - // 1. 同步加载行政地区三级树(内部已作 Session 缓存,避免重复请求)  
180 - await fetchAreaTree();  
181 -  
182 - // 2. 深度克隆 Store 数据,隔离表单与 Store 的直接联动,保持单向数据流  
183 - if (userStore.userInfo) {  
184 - formData.value = JSON.parse(JSON.stringify(userStore.userInfo));  
185 - initEchoAddress(); 189 + // 前置性能防御:对 H5/App 端选择本地大图片进行预警(微信自带ChooseAvatar主要回传临时压缩图,此处作跨端边界防守)
  190 + // #ifndef MP-WEIXIN
  191 + uni.getFileInfo({
  192 + filePath: avatarUrl,
  193 + success: (fileInfo) => {
  194 + const sizeMb = fileInfo.size / 1024 / 1024;
  195 + if (sizeMb > 5) {
  196 + uni.showToast({ title: '图片大小不能超过5MB', icon: 'none' });
  197 + return;
186 } 198 }
187 - } catch (error) {  
188 - console.error('[Init Error] 初始化个人信息数据流失败:', error);  
189 - } 199 + },
190 }); 200 });
  201 + // #endif
191 202
192 - /**  
193 - * 拦截非保存下的非正常退出,防止用户误触返回造成数据丢失  
194 - */  
195 - onBackPress((options) => {  
196 - // 监听表单内容变动(仅在未保存且已有修改时提示)  
197 - const isUnsaved = JSON.stringify(formData.value) !== JSON.stringify(userStore.userInfo);  
198 - if (isUnsaved && !saveLoading.value) {  
199 - uni.showModal({  
200 - title: '提示',  
201 - content: '您有尚未保存的修改,确定要离开吗?',  
202 - success: (res) => {  
203 - if (res.confirm) {  
204 - hasModified.value = false;  
205 - uni.navigateBack({ delta: 1 });  
206 - }  
207 - },  
208 - });  
209 - return true; // 拦截返回键  
210 - }  
211 - return false;  
212 - });  
213 203
214 - /**  
215 - * 头像上传逻辑:增加微信ChooseAvatar路径格式预检与文件防崩溃重试  
216 - */  
217 - const handleChooseAvatar = async (e) => {  
218 - const { avatarUrl } = e.detail;  
219 - if (!avatarUrl) return;  
220 -  
221 - // 前置性能防御:对 H5/App 端选择本地大图片进行预警(微信自带ChooseAvatar主要回传临时压缩图,此处作跨端边界防守)  
222 - // #ifndef MP-WEIXIN  
223 - uni.getFileInfo({  
224 - filePath: avatarUrl,  
225 - success: (fileInfo) => {  
226 - const sizeMb = fileInfo.size / 1024 / 1024;  
227 - if (sizeMb > 5) {  
228 - uni.showToast({ title: '图片大小不能超过5MB', icon: 'none' });  
229 - return;  
230 - }  
231 - },  
232 - });  
233 - // #endif  
234 -  
235 - uni.showLoading({ title: '头像上传中...', mask: true });  
236 - try {  
237 - const res = await FileApi.uploadFile(avatarUrl);  
238 - if (res?.data) {  
239 - formData.value.avatar = res.data;  
240 - uni.showToast({ title: '头像上传成功', icon: 'success' });  
241 - } else {  
242 - throw new Error('服务器未返回有效的图片URL路径');  
243 - }  
244 - } catch (err) {  
245 - console.error('[Upload Error] 临时路径转存服务器失败:', err);  
246 - uni.showToast({ title: '头像保存失败,请稍后重试', icon: 'none' });  
247 - } finally {  
248 - uni.hideLoading();  
249 - }  
250 - };  
251 -  
252 - /**  
253 - * 唤起并初始化昵称弹窗缓存  
254 - */  
255 - const gotoEditNickname = () => {  
256 - editCacheData.nickname = formData.value.nickname || '';  
257 - nicknamePopupRef.value.open();  
258 - };  
259 -  
260 - /**  
261 - * 确认编辑昵称  
262 - */  
263 - const confirmEditNickname = () => {  
264 - const targetNickname = editCacheData.nickname ? editCacheData.nickname.trim() : '';  
265 - if (!targetNickname) {  
266 - uni.showToast({ title: '昵称不能为空', icon: 'none' });  
267 - return;  
268 - }  
269 - formData.value.nickname = targetNickname;  
270 - nicknamePopupRef.value.close();  
271 - };  
272 -  
273 - /**  
274 - * 唤起并初始化性别弹窗  
275 - */  
276 - const gotoSelectGender = () => {  
277 - if (!formData.value.isAllowUpdSex) {  
278 - uni.showToast({ title: '性别暂不支持多次修改', icon: 'none' });  
279 - return;  
280 - }  
281 - editCacheData.sex = formData.value.sex || 0;  
282 - genderPopupRef.value.open();  
283 - };  
284 -  
285 - /**  
286 - * 优化:解耦的性别变更逻辑  
287 - */  
288 - const selectGender = (sex) => {  
289 - // 1. 临时更改缓存态而非直接写回 formData  
290 - editCacheData.sex = sex;  
291 - formData.value.sex = sex; // 仅在用户明确选定后回写,并带有 200ms 的视觉缓冲反馈  
292 - setTimeout(() => {  
293 - genderPopupRef.value.close();  
294 - }, 200);  
295 - };  
296 -  
297 - /**  
298 - * 弹窗关闭时彻底释放和清理临时的输入脏状态  
299 - */  
300 - const onNicknamePopupChange = (e) => {  
301 - if (!e.show) {  
302 - editCacheData.nickname = '';  
303 - }  
304 - };  
305 -  
306 - /**  
307 - * 优化:实现会话级局部静态缓存,解决 onShow 高频请求带来的服务器资源浪费  
308 - */  
309 - const fetchAreaTree = async () => {  
310 - if (addressTree.value && addressTree.value.length > 0) return;  
311 -  
312 - // 尝试拉取框架/系统底层会话级存储或内存,确保单次生命周期内仅调用一次 API  
313 - const cachedTree = uni.getStorageSync('sys_area_tree');  
314 - if (cachedTree) {  
315 - addressTree.value = cachedTree;  
316 - return;  
317 - } 204 + try {
  205 + const res = await FileApi.uploadFile(avatarUrl);
318 206
319 - try {  
320 - const res = await AreaApi.getAreaTree();  
321 - if (res?.data) {  
322 - addressTree.value = res.data;  
323 - uni.setStorageSync('sys_area_tree', res.data); // 写入临时磁盘缓存,生命周期内有效  
324 - }  
325 - } catch (error) {  
326 - console.error('[API Error] 获取省市区行政结构树失败:', error);  
327 - }  
328 - };  
329 -  
330 - /**  
331 - * 安全地同步极简Picker的三列静态节点关联数据  
332 - */  
333 - const syncPickerColumnData = (pIdx = 0, cIdx = 0) => {  
334 - if (!addressTree.value || addressTree.value.length === 0) return;  
335 -  
336 - const provinces = addressTree.value;  
337 - const pSelected = provinces[pIdx] || provinces[0]; // 极端越界安全垫付  
338 - const cities = pSelected?.children || [];  
339 - const cSelected = cities[cIdx] || cities[0];  
340 - const regions = cSelected?.children || [];  
341 -  
342 - multiArray.value = [provinces, cities, regions];  
343 - };  
344 -  
345 - /**  
346 - * 【关键越界重构】:修正真机由于异步滚动的时差引发的越界闪退  
347 - */  
348 - const pickerColumnChange = (e) => {  
349 - const { column, value } = e.detail;  
350 -  
351 - // 1. 原子化本地临时深拷贝索引指针  
352 - const nextIndex = [...multiIndex.value];  
353 - nextIndex[column] = value;  
354 -  
355 - if (column === 0) {  
356 - // 2. 变动第一列(省):联动重置市、区索引指针归零  
357 - nextIndex[1] = 0;  
358 - nextIndex[2] = 0;  
359 -  
360 - // 3. 拦截防御:确保选择的省份在当前最新行政数组内  
361 - const safeProvinceIdx = Math.min(value, addressTree.value.length - 1);  
362 - syncPickerColumnData(safeProvinceIdx, 0);  
363 - } else if (column === 1) {  
364 - // 4. 变动第二列(市):联动重置区索引指针归零  
365 - nextIndex[2] = 0;  
366 -  
367 - const safeProvinceIdx = Math.min(nextIndex[0], addressTree.value.length - 1);  
368 - const safeProvince = addressTree.value[safeProvinceIdx];  
369 - const cities = safeProvince?.children || [];  
370 - const safeCityIdx = Math.min(value, cities.length - 1);  
371 -  
372 - syncPickerColumnData(safeProvinceIdx, safeCityIdx);  
373 - } 207 + console.log('res----------', res);
374 208
375 - // 5. 在同一视图更新 tick 下完成覆盖更新,避免多路联动逻辑竞态导致的重叠渲染  
376 - nextTick(() => {  
377 - multiIndex.value = nextIndex;  
378 - });  
379 - };  
380 -  
381 - /**  
382 - * 确认选择省市区,绑定数据映射  
383 - */  
384 - const pickerChange = (e) => {  
385 - const indices = e.detail.value;  
386 - multiIndex.value = [...indices];  
387 -  
388 - const pObj = multiArray.value[0][indices[0]];  
389 - const cObj = multiArray.value[1][indices[1]];  
390 - const rObj = multiArray.value[2][indices[2]];  
391 -  
392 - formData.value.province = pObj?.id || '';  
393 - formData.value.city = cObj?.id || '';  
394 - formData.value.region = rObj?.id || '';  
395 -  
396 - selectedAddress.value = [pObj?.name, cObj?.name, rObj?.name].filter(Boolean).join(' ');  
397 - };  
398 -  
399 - /**  
400 - * 数据回显定位器:精准利用底层树定位当前用户已有地区信息  
401 - */  
402 - const initEchoAddress = () => {  
403 - const { province, city, region } = formData.value;  
404 - if (!province || !addressTree.value || addressTree.value.length === 0) {  
405 - syncPickerColumnData(0, 0);  
406 - return;  
407 - }  
408 209
409 - // 1. 查找省份物理位置  
410 - const pIdx = addressTree.value.findIndex((p) => String(p.id) === String(province));  
411 - if (pIdx === -1) {  
412 - syncPickerColumnData(0, 0);  
413 - return;  
414 - } 210 + formData.value.avatar = res.data;
415 211
416 - // 2. 查找地级市物理位置  
417 - const cities = addressTree.value[pIdx]?.children || [];  
418 - const cIdx = cities.findIndex((c) => String(c.id) === String(city));  
419 -  
420 - // 3. 查找区县物理位置  
421 - const regions = cIdx !== -1 ? cities[cIdx]?.children || [] : [];  
422 - const rIdx = regions.findIndex((r) => String(r.id) === String(region));  
423 -  
424 - // 4. 组装安全的映射矩阵索引  
425 - const safePIdx = pIdx;  
426 - const safeCIdx = cIdx !== -1 ? cIdx : 0;  
427 - const safeRIdx = rIdx !== -1 ? rIdx : 0;  
428 -  
429 - multiIndex.value = [safePIdx, safeCIdx, safeRIdx];  
430 -  
431 - // 5. 更新数据池并刷新回显文字描述  
432 - syncPickerColumnData(safePIdx, safeCIdx);  
433 -  
434 - selectedAddress.value = [  
435 - addressTree.value[safePIdx]?.name,  
436 - cities[safeCIdx]?.name,  
437 - regions[safeRIdx]?.name,  
438 - ]  
439 - .filter(Boolean)  
440 - .join(' ');  
441 - };  
442 -  
443 - /**  
444 - * 保存修改逻辑:进行脏字段过滤净化后,更新用户信息  
445 - */  
446 - const saveProfile = async () => {  
447 - if (saveLoading.value) return;  
448 -  
449 - // 前置轻校验:去除尾部空隙并校验合法性  
450 - if (!formData.value.nickname || !formData.value.nickname.trim()) {  
451 - uni.showToast({ title: '昵称不能为空', icon: 'none' });  
452 - return;  
453 - } 212 + } catch (err) {
  213 + console.error(err);
454 214
455 - saveLoading.value = true;  
456 - uni.showLoading({ title: '正在保存资料...', mask: true }); 215 + }
  216 +};
  217 +
  218 +/**
  219 + * 唤起并初始化昵称弹窗缓存
  220 + */
  221 +const gotoEditNickname = () => {
  222 + editCacheData.nickname = formData.value.nickname || '';
  223 + nicknamePopupRef.value.open();
  224 +};
  225 +
  226 +/**
  227 + * 确认编辑昵称
  228 + */
  229 +const confirmEditNickname = () => {
  230 + const targetNickname = editCacheData.nickname ? editCacheData.nickname.trim() : '';
  231 + if (!targetNickname) {
  232 + uni.showToast({ title: '昵称不能为空', icon: 'none' });
  233 + return;
  234 + }
  235 + formData.value.nickname = targetNickname;
  236 + nicknamePopupRef.value.close();
  237 +};
  238 +
  239 +/**
  240 + * 唤起并初始化性别弹窗
  241 + */
  242 +const gotoSelectGender = () => {
  243 + if (!formData.value.isAllowUpdSex) {
  244 + uni.showToast({ title: '性别暂不支持多次修改', icon: 'none' });
  245 + return;
  246 + }
  247 + editCacheData.sex = formData.value.sex || 0;
  248 + genderPopupRef.value.open();
  249 +};
  250 +
  251 +/**
  252 + * 优化:解耦的性别变更逻辑
  253 + */
  254 +const selectGender = (sex) => {
  255 + // 1. 临时更改缓存态而非直接写回 formData
  256 + editCacheData.sex = sex;
  257 + formData.value.sex = sex; // 仅在用户明确选定后回写,并带有 200ms 的视觉缓冲反馈
  258 + setTimeout(() => {
  259 + genderPopupRef.value.close();
  260 + }, 200);
  261 +};
  262 +
  263 +/**
  264 + * 弹窗关闭时彻底释放和清理临时的输入脏状态
  265 + */
  266 +const onNicknamePopupChange = (e) => {
  267 + if (!e.show) {
  268 + editCacheData.nickname = '';
  269 + }
  270 +};
  271 +
  272 +/**
  273 + * 优化:实现会话级局部静态缓存,解决 onShow 高频请求带来的服务器资源浪费
  274 + */
  275 +const fetchAreaTree = async () => {
  276 + if (addressTree.value && addressTree.value.length > 0) return;
  277 +
  278 + // 尝试拉取框架/系统底层会话级存储或内存,确保单次生命周期内仅调用一次 API
  279 + const cachedTree = uni.getStorageSync('sys_area_tree');
  280 + if (cachedTree) {
  281 + addressTree.value = cachedTree;
  282 + return;
  283 + }
457 284
458 - try {  
459 - // 脏数据深度过滤净化:只保留后端允许更新的可写属性,剥离统计及只读系统字段  
460 - const { createTime, isAllowUpdSex, registerIp, id, userId, mobile, ...payload } =  
461 - formData.value; 285 + try {
  286 + const res = await AreaApi.getAreaTree();
  287 + if (res?.data) {
  288 + addressTree.value = res.data;
  289 + uni.setStorageSync('sys_area_tree', res.data); // 写入临时磁盘缓存,生命周期内有效
  290 + }
  291 + } catch (error) {
  292 + console.error('[API Error] 获取省市区行政结构树失败:', error);
  293 + }
  294 +};
  295 +
  296 +/**
  297 + * 安全地同步极简Picker的三列静态节点关联数据
  298 + */
  299 +const syncPickerColumnData = (pIdx = 0, cIdx = 0) => {
  300 + if (!addressTree.value || addressTree.value.length === 0) return;
  301 +
  302 + const provinces = addressTree.value;
  303 + const pSelected = provinces[pIdx] || provinces[0]; // 极端越界安全垫付
  304 + const cities = pSelected?.children || [];
  305 + const cSelected = cities[cIdx] || cities[0];
  306 + const regions = cSelected?.children || [];
  307 +
  308 + multiArray.value = [provinces, cities, regions];
  309 +};
  310 +
  311 +/**
  312 + * 【关键越界重构】:修正真机由于异步滚动的时差引发的越界闪退
  313 + */
  314 +const pickerColumnChange = (e) => {
  315 + const { column, value } = e.detail;
  316 +
  317 + // 1. 原子化本地临时深拷贝索引指针
  318 + const nextIndex = [...multiIndex.value];
  319 + nextIndex[column] = value;
  320 +
  321 + if (column === 0) {
  322 + // 2. 变动第一列(省):联动重置市、区索引指针归零
  323 + nextIndex[1] = 0;
  324 + nextIndex[2] = 0;
  325 +
  326 + // 3. 拦截防御:确保选择的省份在当前最新行政数组内
  327 + const safeProvinceIdx = Math.min(value, addressTree.value.length - 1);
  328 + syncPickerColumnData(safeProvinceIdx, 0);
  329 + } else if (column === 1) {
  330 + // 4. 变动第二列(市):联动重置区索引指针归零
  331 + nextIndex[2] = 0;
  332 +
  333 + const safeProvinceIdx = Math.min(nextIndex[0], addressTree.value.length - 1);
  334 + const safeProvince = addressTree.value[safeProvinceIdx];
  335 + const cities = safeProvince?.children || [];
  336 + const safeCityIdx = Math.min(value, cities.length - 1);
  337 +
  338 + syncPickerColumnData(safeProvinceIdx, safeCityIdx);
  339 + }
462 340
463 - // 保证提交数据的值是修剪过左右空格的  
464 - payload.nickname = payload.nickname.trim();  
465 - if (payload.signature) {  
466 - payload.signature = payload.signature.trim();  
467 - } 341 + // 5. 在同一视图更新 tick 下完成覆盖更新,避免多路联动逻辑竞态导致的重叠渲染
  342 + nextTick(() => {
  343 + multiIndex.value = nextIndex;
  344 + });
  345 +};
  346 +
  347 +/**
  348 + * 确认选择省市区,绑定数据映射
  349 + */
  350 +const pickerChange = (e) => {
  351 + const indices = e.detail.value;
  352 + multiIndex.value = [...indices];
  353 +
  354 + const pObj = multiArray.value[0][indices[0]];
  355 + const cObj = multiArray.value[1][indices[1]];
  356 + const rObj = multiArray.value[2][indices[2]];
  357 +
  358 + formData.value.province = pObj?.id || '';
  359 + formData.value.city = cObj?.id || '';
  360 + formData.value.region = rObj?.id || '';
  361 +
  362 + selectedAddress.value = [pObj?.name, cObj?.name, rObj?.name].filter(Boolean).join(' ');
  363 +};
  364 +
  365 +/**
  366 + * 数据回显定位器:精准利用底层树定位当前用户已有地区信息
  367 + */
  368 +const initEchoAddress = () => {
  369 + const { province, city, region } = formData.value;
  370 + if (!province || !addressTree.value || addressTree.value.length === 0) {
  371 + syncPickerColumnData(0, 0);
  372 + return;
  373 + }
468 374
469 - await UserApi.updateUser(payload); 375 + // 1. 查找省份物理位置
  376 + const pIdx = addressTree.value.findIndex((p) => String(p.id) === String(province));
  377 + if (pIdx === -1) {
  378 + syncPickerColumnData(0, 0);
  379 + return;
  380 + }
470 381
471 - // 异步刷新全局 Pinia 的 userInfo 用户属性,使全局头像和昵称视图无痛同步  
472 - if (userStore.getUserInfo) {  
473 - await userStore.getUserInfo();  
474 - } 382 + // 2. 查找地级市物理位置
  383 + const cities = addressTree.value[pIdx]?.children || [];
  384 + const cIdx = cities.findIndex((c) => String(c.id) === String(city));
  385 +
  386 + // 3. 查找区县物理位置
  387 + const regions = cIdx !== -1 ? cities[cIdx]?.children || [] : [];
  388 + const rIdx = regions.findIndex((r) => String(r.id) === String(region));
  389 +
  390 + // 4. 组装安全的映射矩阵索引
  391 + const safePIdx = pIdx;
  392 + const safeCIdx = cIdx !== -1 ? cIdx : 0;
  393 + const safeRIdx = rIdx !== -1 ? rIdx : 0;
  394 +
  395 + multiIndex.value = [safePIdx, safeCIdx, safeRIdx];
  396 +
  397 + // 5. 更新数据池并刷新回显文字描述
  398 + syncPickerColumnData(safePIdx, safeCIdx);
  399 +
  400 + selectedAddress.value = [
  401 + addressTree.value[safePIdx]?.name,
  402 + cities[safeCIdx]?.name,
  403 + regions[safeRIdx]?.name,
  404 + ]
  405 + .filter(Boolean)
  406 + .join(' ');
  407 +};
  408 +
  409 +/**
  410 + * 保存修改逻辑:进行脏字段过滤净化后,更新用户信息
  411 + */
  412 +const saveProfile = async () => {
  413 + if (saveLoading.value) return;
  414 +
  415 + // 前置轻校验:去除尾部空隙并校验合法性
  416 + if (!formData.value.nickname || !formData.value.nickname.trim()) {
  417 + uni.showToast({ title: '昵称不能为空', icon: 'none' });
  418 + return;
  419 + }
475 420
476 - uni.showToast({ title: '保存成功', icon: 'success' });  
477 -  
478 - setTimeout(() => {  
479 - uni.navigateBack({ delta: 1 });  
480 - }, 1200);  
481 - } catch (err) {  
482 - console.error('[API Error] 保存个人信息更新请求失败:', err);  
483 - uni.showToast({ title: err.msg || '保存失败,请稍后重试', icon: 'none' });  
484 - } finally {  
485 - uni.hideLoading();  
486 - saveLoading.value = false;  
487 - }  
488 - };  
489 -  
490 - // 格式化展示原子转换工具  
491 - const formatGender = (sex) => {  
492 - if (sex === 1) return '男';  
493 - if (sex === 2) return '女';  
494 - return '保密';  
495 - };  
496 -  
497 - const formatRegisterTime = (time) => {  
498 - if (!time) return '--';  
499 - return dayjs(time).format('YYYY-MM-DD');  
500 - };  
501 -</script> 421 + saveLoading.value = true;
  422 + uni.showLoading({ title: '正在保存资料...', mask: true });
502 423
503 -<style scoped lang="scss">  
504 - $color-bg: #f5f5f5;  
505 - $color-white: #ffffff;  
506 - $color-text-dark: #333;  
507 - $color-text-middle: #666;  
508 - $color-text-light: #999;  
509 - $color-primary: #fdd511;  
510 - $color-border: #eee;  
511 - $radius: 12rpx;  
512 - $radius-lg: 24rpx;  
513 - $shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);  
514 - $spacing-sm: 20rpx;  
515 - $spacing-md: 24rpx;  
516 - $spacing-lg: 32rpx;  
517 - $spacing-xl: 60rpx;  
518 -  
519 - .profile-page {  
520 - background-color: $color-bg;  
521 - height: 100vh;  
522 - // #ifdef H5  
523 - height: calc(100vh - 44px);  
524 - // #endif  
525 - padding-bottom: env(safe-area-inset-bottom);  
526 -  
527 - .avatar-section {  
528 - display: flex;  
529 - justify-content: center;  
530 - align-items: center;  
531 - padding: $spacing-xl 0 $spacing-lg;  
532 -  
533 - .avatar-edit-btn {  
534 - background-color: transparent;  
535 - border: none;  
536 - padding: 0;  
537 - line-height: 0;  
538 - border-radius: 50%;  
539 - &::after {  
540 - content: none;  
541 - }  
542 - } 424 + try {
  425 + // 脏数据深度过滤净化:只保留后端允许更新的可写属性,剥离统计及只读系统字段
  426 + const { createTime, isAllowUpdSex, registerIp, id, userId, mobile, ...payload } =
  427 + formData.value;
543 428
544 - .avatar-image {  
545 - width: 160rpx;  
546 - height: 160rpx;  
547 - border-radius: 50%;  
548 - background-color: #e1e1e1;  
549 - } 429 + // 保证提交数据的值是修剪过左右空格的
  430 + payload.nickname = payload.nickname.trim();
  431 + if (payload.signature) {
  432 + payload.signature = payload.signature.trim();
550 } 433 }
551 434
552 - .info-list {  
553 - padding: 0 $spacing-sm; 435 + await UserApi.updateUser(payload);
  436 +
  437 + // 异步刷新全局 Pinia 的 userInfo 用户属性,使全局头像和昵称视图无痛同步
  438 + if (userStore.getUserInfo) {
  439 + await userStore.getUserInfo();
554 } 440 }
555 441
556 - .list-item {  
557 - display: flex;  
558 - align-items: center;  
559 - justify-content: space-between;  
560 - background-color: $color-white;  
561 - border-radius: $radius;  
562 - padding: $spacing-md $spacing-sm;  
563 - margin-bottom: $spacing-sm;  
564 - box-shadow: $shadow; 442 + uni.showToast({ title: '保存成功', icon: 'success' });
565 443
566 - .item-title {  
567 - font-size: 28rpx;  
568 - color: $color-text-dark;  
569 - flex-shrink: 0;  
570 - width: 140rpx;  
571 - } 444 + setTimeout(() => {
  445 + uni.navigateBack({ delta: 1 });
  446 + }, 1200);
  447 + } catch (err) {
  448 + console.error('[API Error] 保存个人信息更新请求失败:', err);
  449 + uni.showToast({ title: err.msg || '保存失败,请稍后重试', icon: 'none' });
  450 + } finally {
  451 + uni.hideLoading();
  452 + saveLoading.value = false;
  453 + }
  454 +};
  455 +
  456 +// 格式化展示原子转换工具
  457 +const formatGender = (sex) => {
  458 + if (sex === 1) return '男';
  459 + if (sex === 2) return '女';
  460 + return '保密';
  461 +};
  462 +
  463 +const formatRegisterTime = (time) => {
  464 + if (!time) return '--';
  465 + return dayjs(time).format('YYYY-MM-DD');
  466 +};
  467 +</script>
572 468
573 - .item-value {  
574 - font-size: 26rpx;  
575 - color: $color-text-middle;  
576 - margin-right: $spacing-sm;  
577 - text-align: right;  
578 - flex: 1;  
579 - white-space: nowrap;  
580 - overflow: hidden;  
581 - text-overflow: ellipsis; 469 +<style scoped lang="scss">
  470 +$color-bg: #f5f5f5;
  471 +$color-white: #ffffff;
  472 +$color-text-dark: #333;
  473 +$color-text-middle: #666;
  474 +$color-text-light: #999;
  475 +$color-primary: #fdd511;
  476 +$color-border: #eee;
  477 +$radius: 12rpx;
  478 +$radius-lg: 24rpx;
  479 +$shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
  480 +$spacing-sm: 20rpx;
  481 +$spacing-md: 24rpx;
  482 +$spacing-lg: 32rpx;
  483 +$spacing-xl: 60rpx;
  484 +
  485 +.profile-page {
  486 + background-color: $color-bg;
  487 + height: 100vh;
  488 + // #ifdef H5
  489 + height: calc(100vh - 44px);
  490 + // #endif
  491 + padding-bottom: env(safe-area-inset-bottom);
  492 +
  493 + .avatar-section {
  494 + display: flex;
  495 + justify-content: center;
  496 + align-items: center;
  497 + padding: $spacing-xl 0 $spacing-lg;
  498 +
  499 + .avatar-edit-btn {
  500 + background-color: transparent;
  501 + border: none;
  502 + padding: 0;
  503 + line-height: 0;
  504 + border-radius: 50%;
  505 +
  506 + &::after {
  507 + content: none;
582 } 508 }
  509 + }
583 510
584 - .picker {  
585 - font-size: 26rpx;  
586 - color: $color-text-middle;  
587 - text-align: right;  
588 - flex: 1;  
589 - padding-right: 10rpx; 511 + .avatar-image {
  512 + width: 160rpx;
  513 + height: 160rpx;
  514 + border-radius: 50%;
  515 + background-color: #e1e1e1;
  516 + }
  517 + }
590 518
591 - &.placeholder-txt {  
592 - color: $color-text-light;  
593 - }  
594 - } 519 + .info-list {
  520 + padding: 0 $spacing-sm;
  521 + }
595 522
596 - .arrow-icon {  
597 - flex-shrink: 0;  
598 - } 523 + .list-item {
  524 + display: flex;
  525 + align-items: center;
  526 + justify-content: space-between;
  527 + background-color: $color-white;
  528 + border-radius: $radius;
  529 + padding: $spacing-md $spacing-sm;
  530 + margin-bottom: $spacing-sm;
  531 + box-shadow: $shadow;
599 532
600 - &.signature-item {  
601 - flex-direction: column;  
602 - align-items: flex-start;  
603 - gap: 16rpx; 533 + .item-title {
  534 + font-size: 28rpx;
  535 + color: $color-text-dark;
  536 + flex-shrink: 0;
  537 + width: 140rpx;
  538 + }
604 539
605 - .signature-input-wrapper {  
606 - width: 100%;  
607 - position: relative;  
608 -  
609 - .signature-input {  
610 - width: 100%;  
611 - min-height: 120rpx;  
612 - max-height: 200rpx;  
613 - padding: 16rpx 20rpx 40rpx;  
614 - font-size: 26rpx;  
615 - color: $color-text-dark;  
616 - background-color: #f9f9f9;  
617 - border-radius: $radius;  
618 - border: 1rpx solid $color-border;  
619 - box-sizing: border-box;  
620 - line-height: 1.5;  
621 - }  
622 -  
623 - .char-count {  
624 - position: absolute;  
625 - right: 20rpx;  
626 - bottom: 12rpx;  
627 - font-size: 22rpx;  
628 - color: $color-text-light;  
629 - }  
630 - }  
631 - } 540 + .item-value {
  541 + font-size: 26rpx;
  542 + color: $color-text-middle;
  543 + margin-right: $spacing-sm;
  544 + text-align: right;
  545 + flex: 1;
  546 + white-space: nowrap;
  547 + overflow: hidden;
  548 + text-overflow: ellipsis;
632 } 549 }
633 - }  
634 550
635 - // 弹出层统一样式  
636 - .popup-container {  
637 - background-color: $color-white;  
638 - border-radius: $radius-lg $radius-lg 0 0;  
639 - padding-bottom: calc(30rpx + env(safe-area-inset-bottom)); 551 + .picker {
  552 + font-size: 26rpx;
  553 + color: $color-text-middle;
  554 + text-align: right;
  555 + flex: 1;
  556 + padding-right: 10rpx;
640 557
641 - .popup-header {  
642 - display: flex;  
643 - align-items: center;  
644 - justify-content: center;  
645 - padding: $spacing-md $spacing-sm;  
646 - border-bottom: 1rpx solid #fcfcfc;  
647 - .popup-title {  
648 - font-size: 32rpx;  
649 - color: $color-text-dark;  
650 - font-weight: 600; 558 + &.placeholder-txt {
  559 + color: $color-text-light;
651 } 560 }
652 } 561 }
653 562
654 - .popup-body {  
655 - padding: 30rpx $spacing-sm; 563 + .arrow-icon {
  564 + flex-shrink: 0;
  565 + }
  566 +
  567 + &.signature-item {
  568 + flex-direction: column;
  569 + align-items: flex-start;
  570 + gap: 16rpx;
  571 +
  572 + .signature-input-wrapper {
  573 + width: 100%;
  574 + position: relative;
656 575
657 - .input-wrap {  
658 - .nickname-input { 576 + .signature-input {
659 width: 100%; 577 width: 100%;
660 - height: 90rpx; 578 + min-height: 120rpx;
  579 + max-height: 200rpx;
  580 + padding: 16rpx 20rpx 40rpx;
  581 + font-size: 26rpx;
  582 + color: $color-text-dark;
661 background-color: #f9f9f9; 583 background-color: #f9f9f9;
662 - border: 1rpx solid $color-border;  
663 border-radius: $radius; 584 border-radius: $radius;
664 - padding: 0 $spacing-sm;  
665 - font-size: 28rpx; 585 + border: 1rpx solid $color-border;
666 box-sizing: border-box; 586 box-sizing: border-box;
  587 + line-height: 1.5;
667 } 588 }
668 - }  
669 -  
670 - .gender-options {  
671 - display: flex;  
672 - flex-direction: column;  
673 - gap: $spacing-md;  
674 -  
675 - .gender-item {  
676 - position: relative;  
677 - display: flex;  
678 - align-items: center;  
679 - justify-content: center;  
680 - height: 96rpx;  
681 - font-size: 30rpx;  
682 - color: $color-text-middle;  
683 - border-radius: $radius;  
684 - background-color: #fafafa;  
685 - border: 2rpx solid $color-border;  
686 - box-sizing: border-box;  
687 589
688 - &.selected {  
689 - background-color: rgba($color-primary, 0.15);  
690 - color: #333;  
691 - border-color: $color-primary;  
692 - font-weight: bold;  
693 - } 590 + .char-count {
  591 + position: absolute;
  592 + right: 20rpx;
  593 + bottom: 12rpx;
  594 + font-size: 22rpx;
  595 + color: $color-text-light;
694 } 596 }
695 } 597 }
696 } 598 }
  599 + }
  600 +}
  601 +
  602 +// 弹出层统一样式
  603 +.popup-container {
  604 + background-color: $color-white;
  605 + border-radius: $radius-lg $radius-lg 0 0;
  606 + padding-bottom: calc(30rpx + env(safe-area-inset-bottom));
  607 +
  608 + .popup-header {
  609 + display: flex;
  610 + align-items: center;
  611 + justify-content: center;
  612 + padding: $spacing-md $spacing-sm;
  613 + border-bottom: 1rpx solid #fcfcfc;
  614 +
  615 + .popup-title {
  616 + font-size: 32rpx;
  617 + color: $color-text-dark;
  618 + font-weight: 600;
  619 + }
  620 + }
697 621
698 - .popup-footer {  
699 - padding: 0 $spacing-sm;  
700 - .confirm-btn { 622 + .popup-body {
  623 + padding: 30rpx $spacing-sm;
  624 +
  625 + .input-wrap {
  626 + .nickname-input {
701 width: 100%; 627 width: 100%;
702 - height: 88rpx;  
703 - line-height: 88rpx;  
704 - background-color: $color-primary;  
705 - color: #333; 628 + height: 90rpx;
  629 + background-color: #f9f9f9;
  630 + border: 1rpx solid $color-border;
  631 + border-radius: $radius;
  632 + padding: 0 $spacing-sm;
706 font-size: 28rpx; 633 font-size: 28rpx;
707 - font-weight: 500; 634 + box-sizing: border-box;
  635 + }
  636 + }
  637 +
  638 + .gender-options {
  639 + display: flex;
  640 + flex-direction: column;
  641 + gap: $spacing-md;
  642 +
  643 + .gender-item {
  644 + position: relative;
  645 + display: flex;
  646 + align-items: center;
  647 + justify-content: center;
  648 + height: 96rpx;
  649 + font-size: 30rpx;
  650 + color: $color-text-middle;
708 border-radius: $radius; 651 border-radius: $radius;
709 - &:after {  
710 - content: none; 652 + background-color: #fafafa;
  653 + border: 2rpx solid $color-border;
  654 + box-sizing: border-box;
  655 +
  656 + &.selected {
  657 + background-color: rgba($color-primary, 0.15);
  658 + color: #333;
  659 + border-color: $color-primary;
  660 + font-weight: bold;
711 } 661 }
712 } 662 }
713 } 663 }
714 } 664 }
715 665
716 - .button-section {  
717 - padding: $spacing-lg $spacing-sm;  
718 - .save-btn { 666 + .popup-footer {
  667 + padding: 0 $spacing-sm;
  668 +
  669 + .confirm-btn {
719 width: 100%; 670 width: 100%;
720 height: 88rpx; 671 height: 88rpx;
721 line-height: 88rpx; 672 line-height: 88rpx;
@@ -724,9 +675,30 @@ @@ -724,9 +675,30 @@
724 font-size: 28rpx; 675 font-size: 28rpx;
725 font-weight: 500; 676 font-weight: 500;
726 border-radius: $radius; 677 border-radius: $radius;
  678 +
727 &:after { 679 &:after {
728 content: none; 680 content: none;
729 } 681 }
730 } 682 }
731 } 683 }
  684 +}
  685 +
  686 +.button-section {
  687 + padding: $spacing-lg $spacing-sm;
  688 +
  689 + .save-btn {
  690 + width: 100%;
  691 + height: 88rpx;
  692 + line-height: 88rpx;
  693 + background-color: $color-primary;
  694 + color: #333;
  695 + font-size: 28rpx;
  696 + font-weight: 500;
  697 + border-radius: $radius;
  698 +
  699 + &:after {
  700 + content: none;
  701 + }
  702 + }
  703 +}
732 </style> 704 </style>
  1 +<template>
  2 + <view class="level-page">
  3 + <!-- 导航栏 -->
  4 + <uni-nav-bar title="华创信" left-icon="left" @click-left="goBack" :fixed="true" :status-bar="true" />
  5 + <view class="top-right">
  6 + <!-- <u-icon name="document" size="24" color="#fff" @click="showRules"></u-icon> -->
  7 + <text class="right-text">华创信</text>
  8 + </view>
  9 +
  10 + <!-- 用户信息 -->
  11 + <view class="user-info">
  12 + <image src="https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/order-empty_1773628059920.png"
  13 + mode="aspectFill" class="avatar"></image>
  14 + <text class="username">华创信开发者</text>
  15 + </view>
  16 +
  17 + <!-- 更多权益 -->
  18 + <view class="more-benefits">
  19 + <view class="more-text">华创信开发者</view>
  20 + <view class="more-text">系统开发中</view>
  21 + <view class="more-text">敬请期待~</view>
  22 + </view>
  23 + </view>
  24 +</template>
  25 +
  26 +<script>
  27 +export default {
  28 + methods: {
  29 + goBack() {
  30 + uni.navigateBack();
  31 + },
  32 + showRules() {
  33 + uni.showToast({ title: '查看开发内容', icon: 'success' });
  34 + },
  35 + },
  36 +};
  37 +</script>
  38 +
  39 +<style scoped>
  40 +.level-page {
  41 + position: relative;
  42 + width: 100%;
  43 + min-height: 100vh;
  44 + background-color: #1a1a1a;
  45 + color: white;
  46 + padding-bottom: 40rpx;
  47 +}
  48 +
  49 +.top-right {
  50 + position: absolute;
  51 + top: 20rpx;
  52 + right: 20rpx;
  53 + display: flex;
  54 + align-items: center;
  55 + gap: 10rpx;
  56 + color: white;
  57 + font-size: 24rpx;
  58 +}
  59 +
  60 +.user-info {
  61 + display: flex;
  62 + align-items: center;
  63 + gap: 20rpx;
  64 + padding: 20rpx;
  65 + margin-top: 20rpx;
  66 +}
  67 +
  68 +.avatar {
  69 + width: 60rpx;
  70 + height: 60rpx;
  71 + border-radius: 50%;
  72 + object-fit: cover;
  73 +}
  74 +
  75 +.username {
  76 + font-size: 28rpx;
  77 +}
  78 +
  79 +.level-card {
  80 + width: 600rpx;
  81 + height: 300rpx;
  82 + background-color: rgba(173, 216, 230, 0.3);
  83 + border-radius: 20rpx;
  84 + padding: 30rpx;
  85 + margin: 20rpx auto;
  86 + position: relative;
  87 + overflow: hidden;
  88 +}
  89 +
  90 +.level-name {
  91 + font-size: 40rpx;
  92 + font-weight: bold;
  93 + margin-bottom: 10rpx;
  94 +}
  95 +
  96 +.level-en {
  97 + font-size: 36rpx;
  98 + font-weight: bold;
  99 + margin-bottom: 20rpx;
  100 +}
  101 +
  102 +.require {
  103 + font-size: 24rpx;
  104 + margin-bottom: 20rpx;
  105 +}
  106 +
  107 +.detail {
  108 + font-size: 24rpx;
  109 + color: #ccc;
  110 +}
  111 +
  112 +.progress-bar {
  113 + width: 600rpx;
  114 + height: 20rpx;
  115 + background-color: #333;
  116 + border-radius: 10rpx;
  117 + margin: 20rpx auto;
  118 + position: relative;
  119 + overflow: hidden;
  120 +}
  121 +
  122 +.progress-track {
  123 + width: 100%;
  124 + height: 100%;
  125 + background-color: #333;
  126 + border-radius: 10rpx;
  127 +}
  128 +
  129 +.progress-fill {
  130 + width: 20%;
  131 + height: 100%;
  132 + background-color: #007aff;
  133 + border-radius: 10rpx;
  134 +}
  135 +
  136 +.progress-dot {
  137 + position: absolute;
  138 + top: 50%;
  139 + left: 20%;
  140 + transform: translate(-50%, -50%);
  141 + width: 12rpx;
  142 + height: 12rpx;
  143 + background-color: #007aff;
  144 + border-radius: 50%;
  145 +}
  146 +
  147 +.benefits-section {
  148 + margin: 20rpx;
  149 + padding: 0 20rpx;
  150 +}
  151 +
  152 +.section-title {
  153 + font-size: 32rpx;
  154 + margin-bottom: 20rpx;
  155 + padding-left: 10rpx;
  156 +}
  157 +
  158 +.benefit-list {
  159 + display: flex;
  160 + flex-direction: row;
  161 + justify-content: space-between;
  162 + gap: 16rpx;
  163 +}
  164 +
  165 +.benefit-item {
  166 + width: 180rpx;
  167 + background-color: rgba(255, 255, 255, 0.1);
  168 + border-radius: 12rpx;
  169 + padding: 16rpx 12rpx;
  170 + text-align: center;
  171 + box-sizing: border-box;
  172 +}
  173 +
  174 +.icon {
  175 + width: 36rpx;
  176 + height: 36rpx;
  177 + object-fit: cover;
  178 + margin-bottom: 8rpx;
  179 +}
  180 +
  181 +.benefit-name {
  182 + font-size: 22rpx;
  183 + margin-bottom: 4rpx;
  184 + font-weight: bold;
  185 +}
  186 +
  187 +.benefit-desc {
  188 + font-size: 18rpx;
  189 + color: #ccc;
  190 + line-height: 1.4;
  191 +}
  192 +
  193 +.time-tag {
  194 + font-size: 18rpx;
  195 + color: white;
  196 + background-color: #ff6b00;
  197 + padding: 4rpx 8rpx;
  198 + border-radius: 8rpx;
  199 + margin-top: 8rpx;
  200 + display: inline-block;
  201 +}
  202 +
  203 +.more-benefits {
  204 + margin: 30rpx 20rpx 0;
  205 + padding: 20rpx;
  206 + background-color: rgba(255, 255, 255, 0.1);
  207 + border-radius: 12rpx;
  208 + text-align: center;
  209 +}
  210 +
  211 +.more-text {
  212 + font-size: 24rpx;
  213 + color: #ccc;
  214 + margin-bottom: 10rpx;
  215 +}
  216 +</style>
  1 +<template>
  2 + <view class="profile-page">
  3 + <up-navbar bgColor="transparent" :z-index="999" :autoBack="true">
  4 + <template #left>
  5 + <view class="u-nav-slot">
  6 + <up-icon name="arrow-left" color="#333" size="15"></up-icon>
  7 + </view>
  8 + </template>
  9 + </up-navbar>
  10 +
  11 + <view class="header-section">
  12 + <view class="header-content">
  13 + <view class="title">我的运动资料</view>
  14 + <view class="subtitle">匹配专属训练计划</view>
  15 + </view>
  16 + <view class="header-card">
  17 + <view class="line"></view>
  18 + <view class="line"></view>
  19 + <view class="line"></view>
  20 + <view class="line"></view>
  21 + </view>
  22 + </view>
  23 +
  24 + <view class="form-card-container">
  25 + <view class="form-list">
  26 + <view class="form-item" @click="openRadioPopup('gender', '性别')">
  27 + <text class="label">性别</text>
  28 + <view class="right">
  29 + <text class="value" v-if="pdata.gender">{{ pdata.gender == 1 ? '男' : '女' }}</text>
  30 + <text class="placeholder-text" v-else>请选择</text>
  31 + <up-icon name="arrow-right" size="12" color="#cccccc"></up-icon>
  32 + </view>
  33 + </view>
  34 +
  35 + <view class="form-item" @click="Dateshow = true">
  36 + <text class="label">生日</text>
  37 + <view class="right">
  38 + <text class="value" v-if="pdata.birthday">{{ formatDate(pdata.birthday) }}</text>
  39 + <text class="placeholder-text" v-else>请选择</text>
  40 + <up-icon name="arrow-right" size="12" color="#cccccc"></up-icon>
  41 + </view>
  42 + </view>
  43 +
  44 + <view class="form-item" @click="openInputPopup('height', '身高')">
  45 + <text class="label">身高</text>
  46 + <view class="right">
  47 + <text class="value" v-if="pdata.height">{{ pdata.height }} cm</text>
  48 + <text class="placeholder-text" v-else>未填写</text>
  49 + <up-icon name="arrow-right" size="12" color="#cccccc"></up-icon>
  50 + </view>
  51 + </view>
  52 +
  53 + <view class="form-item" @click="openInputPopup('weight', '当前体重')">
  54 + <text class="label">当前体重</text>
  55 + <view class="right">
  56 + <text class="value" v-if="pdata.weight">{{ pdata.weight }} kg</text>
  57 + <text class="placeholder-text" v-else>未填写</text>
  58 + <up-icon name="arrow-right" size="12" color="#cccccc"></up-icon>
  59 + </view>
  60 + </view>
  61 +
  62 + <view class="form-item" @click="openInputPopup('targetWeight', '目标体重')">
  63 + <text class="label">目标体重</text>
  64 + <view class="right">
  65 + <text class="value" v-if="pdata.targetWeight">{{ pdata.targetWeight }} kg</text>
  66 + <text class="placeholder-text" v-else>未填写</text>
  67 + <up-icon name="arrow-right" size="12" color="#cccccc"></up-icon>
  68 + </view>
  69 + </view>
  70 +
  71 + <view class="form-item" @click="openRadioPopup('hasFitnessFoundation', '健身基础')">
  72 + <text class="label">健身基础</text>
  73 + <view class="right">
  74 + <text class="value" v-if="pdata.hasFitnessFoundation">{{
  75 + pdata.hasFitnessFoundation == 1 ? '有' : '无'
  76 + }}</text>
  77 + <text class="placeholder-text" v-else>请选择</text>
  78 + <up-icon name="arrow-right" size="12" color="#cccccc"></up-icon>
  79 + </view>
  80 + </view>
  81 +
  82 + <view class="form-item" @click="openRadioPopup('acceptableTrainingFrequency', '可接受的训练频次')">
  83 + <text class="label">可接受的训练频次</text>
  84 + <view class="right">
  85 + <text class="value" v-if="pdata.acceptableTrainingFrequency">
  86 + {{ pdata.acceptableTrainingFrequency == 1 ? '1练/2练/3练' : '4练/5练/6练' }}
  87 + </text>
  88 + <text class="placeholder-text" v-else>请选择</text>
  89 + <up-icon name="arrow-right" size="12" color="#cccccc"></up-icon>
  90 + </view>
  91 + </view>
  92 +
  93 + <view class="form-item" @click="openCheckPopup('targetMuscleParts', '想锻炼的肌肉部位')">
  94 + <text class="label">想锻炼的肌肉部位</text>
  95 + <view class="right">
  96 + <text class="value" v-if="pdata.targetMuscleParts && pdata.targetMuscleParts.length">
  97 + {{ pdata.targetMuscleParts.join('、') }}
  98 + </text>
  99 + <text class="placeholder-text" v-else>请选择(多选)</text>
  100 + <up-icon name="arrow-right" size="12" color="#cccccc"></up-icon>
  101 + </view>
  102 + </view>
  103 +
  104 + <view class="form-item" @click="openRadioPopup('trainingGoal', '训练目标')">
  105 + <text class="label">训练目标</text>
  106 + <view class="right">
  107 + <text class="value" v-if="pdata.trainingGoal">{{
  108 + formatGoal(pdata.trainingGoal)
  109 + }}</text>
  110 + <text class="placeholder-text" v-else>请选择</text>
  111 + <up-icon name="arrow-right" size="12" color="#cccccc"></up-icon>
  112 + </view>
  113 + </view>
  114 +
  115 + <view class="form-item" @click="openCheckPopup('painAreas', '身体疼痛部位')">
  116 + <text class="label">身体疼痛部位</text>
  117 + <view class="right">
  118 + <text class="value" v-if="pdata.painAreas && pdata.painAreas.length">
  119 + {{ pdata.painAreas.join('、') }}
  120 + </text>
  121 + <text class="placeholder-text" v-else>无明显疼痛</text>
  122 + <up-icon name="arrow-right" size="12" color="#cccccc"></up-icon>
  123 + </view>
  124 + </view>
  125 +
  126 + <view class="form-item" @click="openRadioPopup('hasDisease', '当前是否存在疾病')">
  127 + <text class="label">当前是否存在疾病</text>
  128 + <view class="right">
  129 + <text class="value" v-if="pdata.hasDisease">{{
  130 + pdata.hasDisease == 1 ? '有' : '无'
  131 + }}</text>
  132 + <text class="placeholder-text" v-else>请选择</text>
  133 + <up-icon name="arrow-right" size="12" color="#cccccc"></up-icon>
  134 + </view>
  135 + </view>
  136 +
  137 + <view class="form-item" @click="openRadioPopup('isTakingMedication', '当前是否在服药')">
  138 + <text class="label">当前是否在服药</text>
  139 + <view class="right">
  140 + <text class="value" v-if="pdata.isTakingMedication">{{
  141 + pdata.isTakingMedication == 1 ? '是' : '否'
  142 + }}</text>
  143 + <text class="placeholder-text" v-else>请选择</text>
  144 + <up-icon name="arrow-right" size="12" color="#cccccc"></up-icon>
  145 + </view>
  146 + </view>
  147 +
  148 + <view class="form-item" @click="openRadioPopup('rewardMethod', '达成目标如何奖励自己')">
  149 + <text class="label">达成目标如何奖励自己</text>
  150 + <view class="right">
  151 + <text class="value" v-if="pdata.rewardMethod">{{
  152 + formatReward(pdata.rewardMethod)
  153 + }}</text>
  154 + <text class="placeholder-text" v-else>请选择</text>
  155 + <up-icon name="arrow-right" size="12" color="#cccccc"></up-icon>
  156 + </view>
  157 + </view>
  158 +
  159 + <view class="form-item" @click="openRadioPopup('fitnessScene', '平常在哪里健身')">
  160 + <text class="label">平常在哪里健身</text>
  161 + <view class="right">
  162 + <text class="value" v-if="pdata.fitnessScene">{{
  163 + formatScene(pdata.fitnessScene)
  164 + }}</text>
  165 + <text class="placeholder-text" v-else>请选择</text>
  166 + <up-icon name="arrow-right" size="12" color="#cccccc"></up-icon>
  167 + </view>
  168 + </view>
  169 + </view>
  170 + </view>
  171 +
  172 + <view class="footer-action-bar">
  173 + <button class="confirm-btn" :loading="isSaving" @click="saveHealthInfo">
  174 + 生成专属训练计划
  175 + </button>
  176 + </view>
  177 +
  178 + <up-popup :show="radioPopup" @close="closeRadioPopup" round="16rpx" mode="bottom" closeable>
  179 + <view class="popup-box">
  180 + <view class="popup-header">{{ currentTitle }}</view>
  181 + <scroll-view scroll-y class="popup-scroll-area">
  182 + <view class="select-list">
  183 + <view class="select-item" :class="{ active: item.value === bufferValue }"
  184 + v-for="(item, index) in formOptions[currentKey]" :key="index" @click="bufferValue = item.value">
  185 + <text class="item-text">{{ item.label }}</text>
  186 + <up-icon v-if="item.value === bufferValue" name="checkbox-mark" color="#ffde00" size="18"></up-icon>
  187 + </view>
  188 + </view>
  189 + </scroll-view>
  190 + <view class="popup-footer">
  191 + <button class="action-btn" @click="submitRadio">确 定</button>
  192 + </view>
  193 + </view>
  194 + </up-popup>
  195 +
  196 + <up-popup :show="inputPopup" @close="closeInputPopup" round="16rpx" mode="bottom" closeable>
  197 + <view class="popup-box">
  198 + <view class="popup-header">{{ currentTitle }}</view>
  199 + <view class="input-content-area">
  200 + <up-input placeholder="请输入有效数字" border="surround" type="digit" v-model="bufferValue" clearable
  201 + customStyle="background-color: #f9f9f9; padding: 24rpx;"></up-input>
  202 + <text class="unit-text">{{ currentKey === 'height' ? 'cm' : 'kg' }}</text>
  203 + </view>
  204 + <view class="popup-footer">
  205 + <button class="action-btn" @click="submitInput">确 定</button>
  206 + </view>
  207 + </view>
  208 + </up-popup>
  209 +
  210 + <up-popup :show="checkPopup" @close="closeCheckPopup" round="16rpx" mode="bottom" closeable>
  211 + <view class="popup-box">
  212 + <view class="popup-header">{{ currentTitle }}</view>
  213 + <scroll-view scroll-y class="popup-scroll-area">
  214 + <view class="select-list grid-layout">
  215 + <view class="select-item-chip" :class="{ active: bufferArray.includes(item.value) }"
  216 + v-for="(item, index) in formOptions[currentKey]" :key="index" @click="toggleCheckItem(item.value)">
  217 + {{ item.label }}
  218 + </view>
  219 + </view>
  220 + </scroll-view>
  221 + <view class="popup-footer">
  222 + <button class="action-btn" @click="submitCheck">确 定</button>
  223 + </view>
  224 + </view>
  225 + </up-popup>
  226 +
  227 + <up-datetime-picker :show="Dateshow" v-model="datePickerValue" mode="date" title="选择生日" @cancel="Dateshow = false"
  228 + @confirm="handleSelectDate" :minDate="-1577836800000" :maxDate="Date.now()"></up-datetime-picker>
  229 + </view>
  230 +</template>
  231 +
  232 +<script setup>
  233 +import { ref, reactive, toRaw } from 'vue';
  234 +import { onShow } from '@dcloudio/uni-app';
  235 +import UserApi from '@/sheep/api/member/user';
  236 +import dayjs from 'dayjs';
  237 +
  238 +// ========================================================
  239 +// 响应式单态核心数据集
  240 +// ========================================================
  241 +
  242 +const Dateshow = ref(false);
  243 +const radioPopup = ref(false);
  244 +const inputPopup = ref(false);
  245 +const checkPopup = ref(false);
  246 +const isSaving = ref(false);
  247 +
  248 +// 状态机寄存指针管理
  249 +const currentKey = ref('');
  250 +const currentTitle = ref('');
  251 +const bufferValue = ref(null); // 纯量缓冲区
  252 +const bufferArray = ref([]); // 数组队列多选缓冲区
  253 +const datePickerValue = ref(Date.now());
  254 +
  255 +const pdata = reactive({
  256 + id: '',
  257 + birthday: '',
  258 + gender: 0,
  259 + height: '',
  260 + weight: '',
  261 + targetWeight: '',
  262 + hasFitnessFoundation: 0,
  263 + acceptableTrainingFrequency: 0,
  264 + targetMuscleParts: [],
  265 + trainingGoal: 0,
  266 + painAreas: [],
  267 + hasDisease: 0,
  268 + isTakingMedication: 0,
  269 + rewardMethod: 0,
  270 + fitnessScene: 0,
  271 +});
  272 +
  273 +// 数据字典隔离配置
  274 +const formOptions = {
  275 + gender: [
  276 + { label: '男', value: 1 },
  277 + { label: '女', value: 2 },
  278 + ],
  279 + hasFitnessFoundation: [
  280 + { label: '有', value: 1 },
  281 + { label: '无', value: 2 },
  282 + ],
  283 + acceptableTrainingFrequency: [
  284 + { label: '1练/2练/3练', value: 1 },
  285 + { label: '4练/5练/6练', value: 2 },
  286 + ],
  287 + targetMuscleParts: [
  288 + { label: '肩颈', value: '肩颈' },
  289 + { label: '斜方肌', value: '斜方肌' },
  290 + { label: '手臂', value: '手臂' },
  291 + { label: '胸部', value: '胸部' },
  292 + { label: '背部', value: '背部' },
  293 + { label: '腹部', value: '腹部' },
  294 + { label: '臀部', value: '臀部' },
  295 + { label: '腿部', value: '腿部' },
  296 + ],
  297 + trainingGoal: [
  298 + { label: '减脂', value: 1 },
  299 + { label: '增肌', value: 2 },
  300 + { label: '塑形', value: 3 },
  301 + { label: '拉伸/体态调整', value: 4 },
  302 + ],
  303 + painAreas: [
  304 + { label: '肩颈', value: '肩颈' },
  305 + { label: '手腕', value: '手腕' },
  306 + { label: '腰部', value: '腰部' },
  307 + { label: '脚踝', value: '脚踝' },
  308 + { label: '膝盖', value: '膝盖' },
  309 + ],
  310 + hasDisease: [
  311 + { label: '有', value: 1 },
  312 + { label: '无', value: 2 },
  313 + ],
  314 + isTakingMedication: [
  315 + { label: '是', value: 1 },
  316 + { label: '否', value: 2 },
  317 + ],
  318 + rewardMethod: [
  319 + { label: '买件新衣服/装备', value: 1 },
  320 + { label: '去旅行', value: 2 },
  321 + { label: '和朋友聚会', value: 3 },
  322 + { label: '其他', value: 4 },
  323 + ],
  324 + fitnessScene: [
  325 + { label: '健身房', value: 1 },
  326 + { label: '家', value: 2 },
  327 + { label: '宿舍', value: 3 },
  328 + ],
  329 +};
  330 +
  331 +// ========================================================
  332 +// 文本高阶转义映射器
  333 +// ========================================================
  334 +const formatDate = (val) => (val ? dayjs(val).format('YYYY-MM-DD') : '');
  335 +const formatGoal = (val) => ({ 1: '减脂', 2: '增肌', 3: '塑形', 4: '拉伸/体态调整' }[val] || '');
  336 +const formatReward = (val) =>
  337 + ({ 1: '买件新衣服/装备', 2: '去旅行', 3: '和朋友聚会', 4: '其他' }[val] || '');
  338 +const formatScene = (val) => ({ 1: '健身房', 2: '家', 3: '宿舍' }[val] || '');
  339 +
  340 +// ========================================================
  341 +// 核心弹窗调度器(精细化防污染管控)
  342 +// ========================================================
  343 +
  344 +const openRadioPopup = (key, title) => {
  345 + currentKey.value = key;
  346 + currentTitle.value = title;
  347 + bufferValue.value = pdata[key] || null; // 载入安全缓冲区
  348 + radioPopup.value = true;
  349 +};
  350 +
  351 +const closeRadioPopup = () => {
  352 + radioPopup.value = false;
  353 +};
  354 +
  355 +const submitRadio = () => {
  356 + if (bufferValue.value !== null) {
  357 + pdata[currentKey.value] = bufferValue.value; // 仅在此处准许回填修改
  358 + }
  359 + radioPopup.value = false;
  360 +};
  361 +
  362 +const openInputPopup = (key, title) => {
  363 + currentKey.value = key;
  364 + currentTitle.value = title;
  365 + bufferValue.value = pdata[key] !== '' ? String(pdata[key]) : '';
  366 + inputPopup.value = true;
  367 +};
  368 +
  369 +const closeInputPopup = () => {
  370 + inputPopup.value = false;
  371 +};
  372 +
  373 +const submitInput = () => {
  374 + const num = parseFloat(bufferValue.value);
  375 + // 安全阈值边界拦截校验
  376 + if (isNaN(num) || num <= 0 || num > 300) {
  377 + uni.showToast({ title: '请输入合理区间值', icon: 'none' });
  378 + return;
  379 + }
  380 + pdata[currentKey.value] = num.toFixed(1);
  381 + inputPopup.value = false;
  382 +};
  383 +
  384 +const openCheckPopup = (key, title) => {
  385 + currentKey.value = key;
  386 + currentTitle.value = title;
  387 + bufferArray.value = Array.isArray(pdata[key]) ? [...pdata[key]] : []; // 解耦深拷贝
  388 + checkPopup.value = true;
  389 +};
  390 +
  391 +const toggleCheckItem = (val) => {
  392 + const idx = bufferArray.value.indexOf(val);
  393 + if (idx > -1) {
  394 + bufferArray.value.splice(idx, 1);
  395 + } else {
  396 + bufferArray.value.push(val);
  397 + }
  398 +};
  399 +
  400 +const closeCheckPopup = () => {
  401 + checkPopup.value = false;
  402 +};
  403 +
  404 +const submitCheck = () => {
  405 + pdata[currentKey.value] = [...bufferArray.value];
  406 + checkPopup.value = false;
  407 +};
  408 +
  409 +const handleSelectDate = (e) => {
  410 + Dateshow.value = false;
  411 + pdata.birthday = dayjs(e.value).format('YYYY-MM-DD');
  412 +};
  413 +
  414 +// ========================================================
  415 +// 网络数据交互编排
  416 +// ========================================================
  417 +
  418 +const getHealthData = async () => {
  419 + try {
  420 + const res = await UserApi.getHealthInfo();
  421 + const data = res?.data || {};
  422 +
  423 + Object.keys(pdata).forEach((key) => {
  424 + if (key === 'hasDisease' || key === 'isTakingMedication') {
  425 + pdata[key] = data[key] === true ? 1 : data[key] === false ? 2 : 0;
  426 + } else if (key === 'targetMuscleParts' || key === 'painAreas') {
  427 + pdata[key] = Array.isArray(data[key]) ? data[key] : [];
  428 + } else {
  429 + pdata[key] = data[key] !== undefined && data[key] !== null ? data[key] : '';
  430 + }
  431 + });
  432 +
  433 + if (pdata.birthday) {
  434 + datePickerValue.value = dayjs(pdata.birthday).valueOf();
  435 + }
  436 + } catch (error) {
  437 + console.error('拉取档案失败:', error);
  438 + }
  439 +};
  440 +
  441 +const saveHealthInfo = async () => {
  442 + // 基础必填拦截防空检测
  443 + if (!pdata.gender || !pdata.birthday || !pdata.height || !pdata.weight) {
  444 + uni.showToast({ title: '请完整填写真实核心资料', icon: 'none' });
  445 + return;
  446 + }
  447 +
  448 + if (isSaving.value) return;
  449 + isSaving.value = true;
  450 +
  451 + try {
  452 + const payload = {
  453 + ...toRaw(pdata),
  454 + hasDisease: pdata.hasDisease === 1,
  455 + isTakingMedication: pdata.isTakingMedication === 1,
  456 + };
  457 +
  458 + if (pdata.id) {
  459 + await UserApi.UpdateHealthInfo(payload);
  460 + } else {
  461 + await UserApi.createHealthInfo(payload);
  462 + }
  463 +
  464 + uni.showToast({ title: '专属计划已生成', icon: 'success' });
  465 + setTimeout(() => {
  466 + uni.navigateBack();
  467 + }, 1500);
  468 + } catch (error) {
  469 + console.error('提交健康资料异常:', error);
  470 + uni.showToast({ title: '保存失败,请重试', icon: 'none' });
  471 + } finally {
  472 + isSaving.value = false;
  473 + }
  474 +};
  475 +
  476 +onShow(() => {
  477 + getHealthData();
  478 +});
  479 +</script>
  480 +
  481 +<style lang="scss" scoped>
  482 +.profile-page {
  483 + background-color: #f7f8fa;
  484 + min-height: 100vh;
  485 + /* 解决长页面底部被悬浮动作条覆盖的问题 */
  486 + padding-bottom: 180rpx;
  487 + box-sizing: border-box;
  488 +
  489 + .u-nav-slot {
  490 + background-color: rgba(255, 255, 255, 0.8);
  491 + backdrop-filter: blur(4px);
  492 + padding: 12rpx;
  493 + border-radius: 50%;
  494 + display: flex;
  495 + align-items: center;
  496 + justify-content: center;
  497 + }
  498 +
  499 + .header-section {
  500 + width: 100%;
  501 + height: 22vh;
  502 + /* #ifdef MP-WEIXIN */
  503 + height: 26vh;
  504 + /* #endif */
  505 + background: linear-gradient(135deg, #ffd166 0%, #ffb74d 100%);
  506 + position: relative;
  507 + padding-left: 44rpx;
  508 + padding-bottom: 50rpx;
  509 + display: flex;
  510 + align-items: flex-end;
  511 + box-sizing: border-box;
  512 +
  513 + .header-content {
  514 + display: flex;
  515 + flex-direction: column;
  516 + z-index: 2;
  517 +
  518 + .title {
  519 + font-size: 46rpx;
  520 + font-weight: bold;
  521 + color: #4a2c00;
  522 + margin-bottom: 8rpx;
  523 + }
  524 +
  525 + .subtitle {
  526 + font-size: 26rpx;
  527 + color: rgba(74, 44, 0, 0.7);
  528 + }
  529 + }
  530 +
  531 + .header-card {
  532 + position: absolute;
  533 + right: 30rpx;
  534 + bottom: -10rpx;
  535 + width: 150rpx;
  536 + height: 180rpx;
  537 + background: linear-gradient(135deg, #ff6b6b, #ff8e53);
  538 + border-radius: 20rpx;
  539 + display: flex;
  540 + flex-direction: column;
  541 + align-items: center;
  542 + justify-content: center;
  543 + gap: 16rpx;
  544 + transform: rotate(12deg);
  545 + box-shadow: 0 12rpx 24rpx rgba(255, 107, 107, 0.25);
  546 + z-index: 1;
  547 +
  548 + .line {
  549 + width: 55%;
  550 + height: 8rpx;
  551 + background-color: rgba(255, 255, 255, 0.9);
  552 + border-radius: 6rpx;
  553 + }
  554 + }
  555 + }
  556 +
  557 + /* 修复:移除有隐患的全局 translateY 移动,改用规范的卡片间距布局 */
  558 + .form-card-container {
  559 + padding: 0 30rpx;
  560 + margin-top: 30rpx;
  561 +
  562 + .form-list {
  563 + background-color: #ffffff;
  564 + border-radius: 24rpx;
  565 + box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.015);
  566 + overflow: hidden;
  567 + /* 完美约束子项按圆角外接圆渲染 */
  568 +
  569 + .form-item {
  570 + display: flex;
  571 + align-items: center;
  572 + justify-content: space-between;
  573 + padding: 36rpx 30rpx;
  574 + border-bottom: 1rpx solid #f2f3f5;
  575 + transition: background-color 0.2s;
  576 +
  577 + &:active {
  578 + background-color: #f9f9f9;
  579 + }
  580 +
  581 + &:last-child {
  582 + border-bottom: none;
  583 + }
  584 +
  585 + .label {
  586 + font-size: 28rpx;
  587 + color: #323233;
  588 + font-weight: 600;
  589 + }
  590 +
  591 + .right {
  592 + display: flex;
  593 + align-items: center;
  594 + gap: 12rpx;
  595 + max-width: 65%;
  596 +
  597 + .value {
  598 + font-size: 28rpx;
  599 + color: #323233;
  600 + text-align: right;
  601 + overflow: hidden;
  602 + text-overflow: ellipsis;
  603 + white-space: nowrap;
  604 + }
  605 +
  606 + .placeholder-text {
  607 + font-size: 28rpx;
  608 + color: #c8c9cc;
  609 + }
  610 + }
  611 + }
  612 + }
  613 + }
  614 +
  615 + /* 固定悬浮底部动作栏 */
  616 + .footer-action-bar {
  617 + position: fixed;
  618 + bottom: 0;
  619 + left: 0;
  620 + width: 100vw;
  621 + background-color: #ffffff;
  622 + padding: 24rpx 40rpx calc(24rpx + env(safe-area-inset-bottom));
  623 + box-sizing: border-box;
  624 + box-shadow: 0 -4rpx 24rpx rgba(0, 0, 0, 0.04);
  625 + z-index: 99;
  626 +
  627 + .confirm-btn {
  628 + width: 100%;
  629 + height: 88rpx;
  630 + line-height: 88rpx;
  631 + background: linear-gradient(90deg, #ffe100 0%, #ffc400 100%);
  632 + color: #323233;
  633 + font-size: 32rpx;
  634 + font-weight: bold;
  635 + border-radius: 44rpx;
  636 + border: none;
  637 +
  638 + &::after {
  639 + border: none;
  640 + }
  641 + }
  642 + }
  643 +
  644 + /* 弹出层统一样式规范 */
  645 + .popup-box {
  646 + background-color: #ffffff;
  647 + border-radius: 24rpx 24rpx 0 0;
  648 + padding: 30rpx 40rpx calc(30rpx + env(safe-area-inset-bottom));
  649 + box-sizing: border-box;
  650 +
  651 + .popup-header {
  652 + font-size: 32rpx;
  653 + font-weight: bold;
  654 + color: #323233;
  655 + text-align: center;
  656 + padding-bottom: 30rpx;
  657 + }
  658 +
  659 + .popup-scroll-area {
  660 + max-height: 40vh;
  661 + }
  662 +
  663 + .input-content-area {
  664 + display: flex;
  665 + align-items: center;
  666 + gap: 20rpx;
  667 + padding: 20rpx 0 40rpx;
  668 +
  669 + .unit-text {
  670 + font-size: 30rpx;
  671 + color: #323233;
  672 + font-weight: bold;
  673 + }
  674 + }
  675 +
  676 + .select-list {
  677 + display: flex;
  678 + flex-direction: column;
  679 + gap: 16rpx;
  680 +
  681 + &.grid-layout {
  682 + flex-direction: row;
  683 + flex-wrap: wrap;
  684 + gap: 20rpx;
  685 + }
  686 +
  687 + .select-item {
  688 + display: flex;
  689 + justify-content: space-between;
  690 + align-items: center;
  691 + background-color: #f7f8fa;
  692 + padding: 30rpx 40rpx;
  693 + border-radius: 16rpx;
  694 + transition: all 0.2s;
  695 +
  696 + &.active {
  697 + background-color: #fffdf0;
  698 + border: 2rpx solid #ffe100;
  699 + }
  700 +
  701 + .item-text {
  702 + font-size: 28rpx;
  703 + color: #323233;
  704 + font-weight: 500;
  705 + }
  706 + }
  707 +
  708 + /* 多选网格纸片样式 */
  709 + .select-item-chip {
  710 + padding: 20rpx 36rpx;
  711 + background-color: #f7f8fa;
  712 + border-radius: 40rpx;
  713 + font-size: 26rpx;
  714 + color: #646566;
  715 + transition: all 0.2s;
  716 + border: 2rpx solid transparent;
  717 +
  718 + &.active {
  719 + background-color: #fffdf0;
  720 + color: #323233;
  721 + font-weight: bold;
  722 + border-color: #ffe100;
  723 + }
  724 + }
  725 + }
  726 +
  727 + .popup-footer {
  728 + margin-top: 40rpx;
  729 +
  730 + .action-btn {
  731 + width: 100%;
  732 + height: 80rpx;
  733 + line-height: 80rpx;
  734 + background-color: #ffe100;
  735 + color: #323233;
  736 + font-size: 28rpx;
  737 + font-weight: bold;
  738 + border-radius: 40rpx;
  739 + border: none;
  740 +
  741 + &::after {
  742 + border: none;
  743 + }
  744 + }
  745 + }
  746 + }
  747 +}
  748 +</style>
  1 +<template>
  2 + <view class="level-page">
  3 + <!-- 导航栏 -->
  4 + <uni-nav-bar title="华创信" left-icon="left" @click-left="goBack" :fixed="true" :status-bar="true" />
  5 + <view class="top-right">
  6 + <!-- <u-icon name="document" size="24" color="#fff" @click="showRules"></u-icon> -->
  7 + <text class="right-text">华创信</text>
  8 + </view>
  9 +
  10 + <!-- 用户信息 -->
  11 + <view class="user-info">
  12 + <image src="https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/order-empty_1773628059920.png"
  13 + mode="aspectFill" class="avatar"></image>
  14 + <text class="username">华创信开发者</text>
  15 + </view>
  16 +
  17 + <!-- 更多权益 -->
  18 + <view class="more-benefits">
  19 + <view class="more-text">华创信开发者</view>
  20 + <view class="more-text">系统开发中</view>
  21 + <view class="more-text">敬请期待~</view>
  22 + </view>
  23 + </view>
  24 +</template>
  25 +
  26 +<script>
  27 +export default {
  28 + methods: {
  29 + goBack() {
  30 + uni.navigateBack();
  31 + },
  32 + showRules() {
  33 + uni.showToast({ title: '查看开发内容', icon: 'success' });
  34 + },
  35 + },
  36 +};
  37 +</script>
  38 +
  39 +<style scoped>
  40 +.level-page {
  41 + position: relative;
  42 + width: 100%;
  43 + min-height: 100vh;
  44 + background-color: #1a1a1a;
  45 + color: white;
  46 + padding-bottom: 40rpx;
  47 +}
  48 +
  49 +.top-right {
  50 + position: absolute;
  51 + top: 20rpx;
  52 + right: 20rpx;
  53 + display: flex;
  54 + align-items: center;
  55 + gap: 10rpx;
  56 + color: white;
  57 + font-size: 24rpx;
  58 +}
  59 +
  60 +.user-info {
  61 + display: flex;
  62 + align-items: center;
  63 + gap: 20rpx;
  64 + padding: 20rpx;
  65 + margin-top: 20rpx;
  66 +}
  67 +
  68 +.avatar {
  69 + width: 60rpx;
  70 + height: 60rpx;
  71 + border-radius: 50%;
  72 + object-fit: cover;
  73 +}
  74 +
  75 +.username {
  76 + font-size: 28rpx;
  77 +}
  78 +
  79 +.level-card {
  80 + width: 600rpx;
  81 + height: 300rpx;
  82 + background-color: rgba(173, 216, 230, 0.3);
  83 + border-radius: 20rpx;
  84 + padding: 30rpx;
  85 + margin: 20rpx auto;
  86 + position: relative;
  87 + overflow: hidden;
  88 +}
  89 +
  90 +.level-name {
  91 + font-size: 40rpx;
  92 + font-weight: bold;
  93 + margin-bottom: 10rpx;
  94 +}
  95 +
  96 +.level-en {
  97 + font-size: 36rpx;
  98 + font-weight: bold;
  99 + margin-bottom: 20rpx;
  100 +}
  101 +
  102 +.require {
  103 + font-size: 24rpx;
  104 + margin-bottom: 20rpx;
  105 +}
  106 +
  107 +.detail {
  108 + font-size: 24rpx;
  109 + color: #ccc;
  110 +}
  111 +
  112 +.progress-bar {
  113 + width: 600rpx;
  114 + height: 20rpx;
  115 + background-color: #333;
  116 + border-radius: 10rpx;
  117 + margin: 20rpx auto;
  118 + position: relative;
  119 + overflow: hidden;
  120 +}
  121 +
  122 +.progress-track {
  123 + width: 100%;
  124 + height: 100%;
  125 + background-color: #333;
  126 + border-radius: 10rpx;
  127 +}
  128 +
  129 +.progress-fill {
  130 + width: 20%;
  131 + height: 100%;
  132 + background-color: #007aff;
  133 + border-radius: 10rpx;
  134 +}
  135 +
  136 +.progress-dot {
  137 + position: absolute;
  138 + top: 50%;
  139 + left: 20%;
  140 + transform: translate(-50%, -50%);
  141 + width: 12rpx;
  142 + height: 12rpx;
  143 + background-color: #007aff;
  144 + border-radius: 50%;
  145 +}
  146 +
  147 +.benefits-section {
  148 + margin: 20rpx;
  149 + padding: 0 20rpx;
  150 +}
  151 +
  152 +.section-title {
  153 + font-size: 32rpx;
  154 + margin-bottom: 20rpx;
  155 + padding-left: 10rpx;
  156 +}
  157 +
  158 +.benefit-list {
  159 + display: flex;
  160 + flex-direction: row;
  161 + justify-content: space-between;
  162 + gap: 16rpx;
  163 +}
  164 +
  165 +.benefit-item {
  166 + width: 180rpx;
  167 + background-color: rgba(255, 255, 255, 0.1);
  168 + border-radius: 12rpx;
  169 + padding: 16rpx 12rpx;
  170 + text-align: center;
  171 + box-sizing: border-box;
  172 +}
  173 +
  174 +.icon {
  175 + width: 36rpx;
  176 + height: 36rpx;
  177 + object-fit: cover;
  178 + margin-bottom: 8rpx;
  179 +}
  180 +
  181 +.benefit-name {
  182 + font-size: 22rpx;
  183 + margin-bottom: 4rpx;
  184 + font-weight: bold;
  185 +}
  186 +
  187 +.benefit-desc {
  188 + font-size: 18rpx;
  189 + color: #ccc;
  190 + line-height: 1.4;
  191 +}
  192 +
  193 +.time-tag {
  194 + font-size: 18rpx;
  195 + color: white;
  196 + background-color: #ff6b00;
  197 + padding: 4rpx 8rpx;
  198 + border-radius: 8rpx;
  199 + margin-top: 8rpx;
  200 + display: inline-block;
  201 +}
  202 +
  203 +.more-benefits {
  204 + margin: 30rpx 20rpx 0;
  205 + padding: 20rpx;
  206 + background-color: rgba(255, 255, 255, 0.1);
  207 + border-radius: 12rpx;
  208 + text-align: center;
  209 +}
  210 +
  211 +.more-text {
  212 + font-size: 24rpx;
  213 + color: #ccc;
  214 + margin-bottom: 10rpx;
  215 +}
  216 +</style>
  1 +<template>
  2 + <view class="settings-page">
  3 + <!-- 手机号设置 -->
  4 + <view class="setting-item">
  5 + <view class="setting-label">手机号</view>
  6 + <view class="setting-value">{{ phone || '未设置' }}</view>
  7 + </view>
  8 +
  9 + <!-- 微信绑定状态 -->
  10 + <view class="setting-item">
  11 + <view class="setting-label">微信绑定</view>
  12 + <view class="setting-status" :class="{ bound: isBindWx === 1, unbound: isBindWx === 0 }">
  13 + {{ isBindWx === 1 ? '已绑定' : '未绑定' }}
  14 + </view>
  15 + <!-- <button v-if="isBindWx === 0" class="bind-btn" @click="bindWeChat">绑定微信</button>
  16 + <button v-else class="unbind-btn" @click="unbindWeChat">解绑</button> -->
  17 + </view>
  18 +
  19 + <!-- 推送通知开关 -->
  20 + <!-- <view class="setting-item notice">
  21 + <view class="content">
  22 + <view class="setting-label">推送通知</view>
  23 + <switch :checked="pushNoticeSwitch === 1" @change="onPushNoticeChange" color="#007AFF" />
  24 + </view>
  25 + <view class="tip">包含订单状态、优惠促销等重要信息的推送</view>
  26 + </view> -->
  27 +
  28 + <!-- 个性化推荐开关 -->
  29 + <!-- <view class="setting-item">
  30 + <view class="setting-label">个性化推荐</view>
  31 + <switch :checked="personRecommendSwitch === 1" @change="onRecommendChange" color="#007AFF" />
  32 + </view> -->
  33 +
  34 + <!-- 退出登录 -->
  35 + <view class="logout-section">
  36 + <button class="logout-btn" @click="logout">退出登录</button>
  37 + </view>
  38 + </view>
  39 +</template>
  40 +
  41 +<script setup>
  42 +import { ref } from 'vue';
  43 +import { onShow } from '@dcloudio/uni-app';
  44 +import SettingApi from '@/sheep/api/setting/setting';
  45 +import AuthUtil from '@/sheep/api/member/auth';
  46 +import useUserStore from '@/sheep/store/user';
  47 +import UserApi from '@/sheep/api/member/user';
  48 +
  49 +// 数据状态
  50 +const phone = ref('');
  51 +const isBindWx = ref(0); // 0:未绑定, 1:已绑定
  52 +const pushNoticeSwitch = ref(0); // 0:关闭, 1:开启
  53 +const personRecommendSwitch = ref(0); // 0:关闭, 1:开启
  54 +const userStore = useUserStore();
  55 +// 生命周期
  56 +onShow(() => {
  57 + loadUserSettings();
  58 +});
  59 +
  60 +// 加载用户设置
  61 +const loadUserSettings = async () => {
  62 + // 这里调用后端接口获取设置数据
  63 + const res = await SettingApi.getSetting();
  64 + phone.value = res.data.phone;
  65 + isBindWx.value = res.data.isBindWx;
  66 + pushNoticeSwitch.value = res.data.pushNoticeSwitch;
  67 + personRecommendSwitch.value = res.data.personRecommendSwitch;
  68 +};
  69 +
  70 +// // 微信绑定
  71 +// const bindWeChat = () => {
  72 +// uni.showModal({
  73 +// title: '绑定微信',
  74 +// content: '确定要绑定微信吗?',
  75 +// success: async (res) => {
  76 +// if (res.confirm) {
  77 +// try {
  78 +// // 这里调用绑定微信接口
  79 +// // await uni.request({
  80 +// // url: '/api/user/bind-wechat',
  81 +// // method: 'POST'
  82 +// // })
  83 +
  84 +// isBindWx.value = 1;
  85 +// uni.showToast({
  86 +// title: '绑定成功',
  87 +// icon: 'success',
  88 +// });
  89 +// } catch (error) {
  90 +// console.error('绑定失败:', error);
  91 +// uni.showToast({
  92 +// title: '绑定失败',
  93 +// icon: 'none',
  94 +// });
  95 +// }
  96 +// }
  97 +// },
  98 +// });
  99 +// };
  100 +
  101 +// // 微信解绑
  102 +// const unbindWeChat = () => {
  103 +// uni.showModal({
  104 +// title: '解绑微信',
  105 +// content: '确定要解绑微信吗?',
  106 +// success: async (res) => {
  107 +// if (res.confirm) {
  108 +// try {
  109 +// // 这里调用解绑微信接口
  110 +// // await uni.request({
  111 +// // url: '/api/user/unbind-wechat',
  112 +// // method: 'POST'
  113 +// // })
  114 +
  115 +// isBindWx.value = 0;
  116 +// uni.showToast({
  117 +// title: '解绑成功',
  118 +// icon: 'success',
  119 +// });
  120 +// } catch (error) {
  121 +// console.error('解绑失败:', error);
  122 +// uni.showToast({
  123 +// title: '解绑失败',
  124 +// icon: 'none',
  125 +// });
  126 +// }
  127 +// }
  128 +// },
  129 +// });
  130 +// };
  131 +
  132 +// 推送通知开关改变
  133 +const onPushNoticeChange = async (e) => {
  134 + const newValue = e.detail.value ? 1 : 0;
  135 + try {
  136 + // 这里调用更新设置接口
  137 + await SettingApi.updateNoticeSetting({
  138 + pushNoticeSwitch: newValue,
  139 + personRecommendSwitch: personRecommendSwitch.value,
  140 + });
  141 +
  142 + pushNoticeSwitch.value = newValue;
  143 + uni.showToast({
  144 + title: newValue ? '已开启推送' : '已关闭推送',
  145 + icon: 'success',
  146 + });
  147 + } catch (error) {
  148 + console.error('更新推送设置失败:', error);
  149 + // 恢复原状态
  150 + pushNoticeSwitch.value = pushNoticeSwitch.value === 1 ? 0 : 1;
  151 + uni.showToast({
  152 + title: '设置失败',
  153 + icon: 'none',
  154 + });
  155 + }
  156 +};
  157 +
  158 +// 个性化推荐开关改变
  159 +const onRecommendChange = async (e) => {
  160 + const newValue = e.detail.value ? 1 : 0;
  161 + try {
  162 + // 这里调用更新设置接口
  163 + await SettingApi.updateNoticeSetting({
  164 + personRecommendSwitch: newValue,
  165 + pushNoticeSwitch: pushNoticeSwitch.value,
  166 + });
  167 +
  168 + personRecommendSwitch.value = newValue;
  169 + uni.showToast({
  170 + title: newValue ? '已开启推荐' : '已关闭推荐',
  171 + icon: 'success',
  172 + });
  173 + } catch (error) {
  174 + console.error('更新推荐设置失败:', error);
  175 + // 恢复原状态
  176 + personRecommendSwitch.value = personRecommendSwitch.value === 1 ? 0 : 1;
  177 + uni.showToast({
  178 + title: '设置失败',
  179 + icon: 'none',
  180 + });
  181 + }
  182 +};
  183 +
  184 +// 退出登录
  185 +const logout = () => {
  186 + uni.showModal({
  187 + title: '提示',
  188 + content: '确定要退出登录吗?',
  189 + success: (res) => {
  190 + if (res.confirm) {
  191 + // 调用退出登录接口
  192 + AuthUtil.logout().then(() => {
  193 + // 清空用户信息
  194 + userStore.logout();
  195 +
  196 + // 跳转到登录页
  197 + uni.reLaunch({ url: '/pages/index/index' });
  198 + });
  199 + }
  200 + },
  201 + });
  202 +};
  203 +</script>
  204 +
  205 +<style lang="scss" scoped>
  206 +// 定义全局变量,方便统一管理
  207 +$primary-color: #007aff;
  208 +$danger-color: #ff4757;
  209 +$danger-active-color: #e63946;
  210 +$bg-color: #f5f5f5;
  211 +$white-color: #ffffff;
  212 +$text-primary: #333333;
  213 +$text-secondary: #666666;
  214 +$text-tertiary: #999999;
  215 +
  216 +$border-radius-base: 16rpx;
  217 +$border-radius-small: 8rpx;
  218 +$padding-base: 30rpx;
  219 +$padding-small: 20rpx;
  220 +
  221 +.settings-page {
  222 + background-color: $bg-color;
  223 + padding: $padding-small;
  224 +
  225 + // 设置项通用样式
  226 + .setting-item {
  227 + background-color: $white-color;
  228 + margin-bottom: $padding-small;
  229 + padding: $padding-base;
  230 + border-radius: $border-radius-base;
  231 + display: flex;
  232 + align-items: center;
  233 + justify-content: space-between;
  234 + box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
  235 +
  236 + // 标签样式
  237 + .setting-label {
  238 + font-size: 32rpx;
  239 + color: $text-primary;
  240 + font-weight: 500;
  241 + }
  242 +
  243 + // 值样式
  244 + .setting-value {
  245 + font-size: 28rpx;
  246 + color: $text-secondary;
  247 + }
  248 +
  249 + // 状态样式
  250 + .setting-status {
  251 + font-size: 28rpx;
  252 + font-weight: 500;
  253 + margin-right: 20rpx;
  254 +
  255 + &.bound {
  256 + color: $primary-color;
  257 + }
  258 +
  259 + &.unbound {
  260 + color: $text-tertiary;
  261 + }
  262 + }
  263 +
  264 + // 绑定/解绑按钮
  265 + .bind-btn,
  266 + .unbind-btn {
  267 + background-color: $primary-color;
  268 + color: $white-color;
  269 + border: none;
  270 + border-radius: $border-radius-small;
  271 + padding: 16rpx 24rpx;
  272 + font-size: 26rpx;
  273 +
  274 + &.unbind-btn {
  275 + background-color: $danger-color;
  276 + }
  277 + }
  278 +
  279 + // 通知设置特殊样式
  280 + &.notice {
  281 + display: block;
  282 +
  283 + .content {
  284 + display: flex;
  285 + align-items: center;
  286 + justify-content: space-between;
  287 + margin-bottom: 16rpx;
  288 + }
  289 +
  290 + .tip {
  291 + font-size: 24rpx;
  292 + color: $text-tertiary;
  293 + line-height: 1.4;
  294 + }
  295 + }
  296 + }
  297 +
  298 + .notice {
  299 + display: flex;
  300 + flex-direction: column;
  301 + }
  302 +
  303 + // 退出登录区域
  304 + .logout-section {
  305 + margin-top: 60rpx;
  306 + padding: $padding-small;
  307 +
  308 + .logout-btn {
  309 + width: 100%;
  310 + background-color: $danger-color;
  311 + color: $white-color;
  312 + border: none;
  313 + border-radius: $border-radius-base;
  314 + padding: $padding-base;
  315 + font-size: 32rpx;
  316 + font-weight: 500;
  317 +
  318 + &:active {
  319 + background-color: $danger-active-color;
  320 + }
  321 + }
  322 + }
  323 +}
  324 +</style>
  1 +<template>
  2 + <view class="privacy-settings-page">
  3 + <!-- 隐私设置列表 -->
  4 + <view class="settings-container">
  5 + <view class="section">
  6 + <view class="section-title">主页展示</view>
  7 + <!-- 运动轨迹 -->
  8 + <view class="setting-item">
  9 + <view class="setting-info">
  10 + <view class="setting-name">运动轨迹</view>
  11 + <view class="setting-desc">开启后,将对其他用户展示您近半年内的运动活跃程度</view>
  12 + </view>
  13 + <switch :checked="movementLocusSwitch === 1" @change="onMovementLocusChange" color="#007AFF" />
  14 + </view>
  15 +
  16 + <!-- 共同喜好 -->
  17 + <view class="setting-item">
  18 + <view class="setting-info">
  19 + <view class="setting-name">共同喜好</view>
  20 + <view class="setting-desc">开启后,将对其他用户显示你们共同喜好的教练和课程</view>
  21 + </view>
  22 + <switch :checked="commonInterestSwitch === 1" @change="onCommonInterestChange" color="#007AFF" />
  23 + </view>
  24 + </view>
  25 + <view class="section">
  26 + <view class="section-title">门店TV展示</view>
  27 + <!-- 勋章和好友竞赛 -->
  28 + <view class="setting-item">
  29 + <view class="setting-info">
  30 + <view class="setting-name">勋章和好友竞赛</view>
  31 + <view class="setting-desc">开启后,门店TV电视将展示你的勋章排行、获得播报、好友竞赛情况等信息</view>
  32 + </view>
  33 + <switch :checked="medalFriendSwitch === 1" @change="onMedalFriendChange" color="#007AFF" />
  34 + </view>
  35 + </view>
  36 + <view class="section">
  37 + <view class="section-title">课程预约</view>
  38 + <!-- 团课约课信息 -->
  39 + <view class="setting-item">
  40 + <view class="setting-info">
  41 + <view class="setting-name">团课约课信息</view>
  42 + <view class="setting-desc">开启后,预约团课时将对外展示您的头像和昵称</view>
  43 + </view>
  44 + <switch :checked="leagueClassSwitch === 1" @change="onLeagueClassChange" color="#007AFF" />
  45 + </view>
  46 +
  47 + <!-- 私教训练报告 -->
  48 + <view class="setting-item">
  49 + <view class="setting-info">
  50 + <view class="setting-name">私教训练报告</view>
  51 + <view class="setting-desc">开启后,将允许教练对外分享您的训练报告</view>
  52 + </view>
  53 + <switch :checked="personalTrainingSwitch === 1" @change="onPersonalTrainingChange" color="#007AFF" />
  54 + </view>
  55 +
  56 + <!-- 小班课战队信息 -->
  57 + <view class="setting-item">
  58 + <view class="setting-info">
  59 + <view class="setting-name">小班课战队信息</view>
  60 + <view class="setting-desc">开启后,战队详情将不对外展示您的信息</view>
  61 + </view>
  62 + <switch :checked="miniClassTeamSwitch === 1" @change="onMiniClassTeamChange" color="#007AFF" />
  63 + </view>
  64 + </view>
  65 + <view class="section">
  66 + <view class="section-title">账号设置</view>
  67 + <!-- 主页私密账户 -->
  68 + <view class="setting-item">
  69 + <view class="setting-info">
  70 + <view class="setting-name">主页私密账户</view>
  71 + <view class="setting-desc">开启后,您的会员主页将不对外展示任何信息</view>
  72 + </view>
  73 + <switch :checked="homepagePrivacySwitch === 1" @change="onHomepagePrivacyChange" color="#007AFF" />
  74 + </view>
  75 +
  76 + <!-- 排行榜 -->
  77 + <view class="setting-item">
  78 + <view class="setting-info">
  79 + <view class="setting-name">排行榜</view>
  80 + <view class="setting-desc">开启后,在会员排行榜上将展示您的头像和昵称</view>
  81 + </view>
  82 + <switch :checked="rankingSwitch === 1" @change="onRankingChange" color="#007AFF" />
  83 + </view>
  84 + </view>
  85 + </view>
  86 + </view>
  87 +</template>
  88 +
  89 +<script setup>
  90 +import SettingApi from '@/sheep/api/setting/setting';
  91 +import { onShow, onUnload } from '@dcloudio/uni-app';
  92 +import { ref, computed } from 'vue';
  93 +
  94 +// 隐私设置数据状态
  95 +const movementLocusSwitch = ref(0); // 运动轨迹
  96 +const commonInterestSwitch = ref(0); // 共同喜好
  97 +const medalFriendSwitch = ref(0); // 勋章和好友竞赛
  98 +const leagueClassSwitch = ref(0); // 团课约课信息
  99 +const personalTrainingSwitch = ref(0); // 私教训练报告
  100 +const miniClassTeamSwitch = ref(0); // 小班课战队信息
  101 +const homepagePrivacySwitch = ref(0); // 主页私密账户
  102 +const rankingSwitch = ref(0); // 排行榜
  103 +
  104 +// 生命周期
  105 +onShow(() => {
  106 + loadPrivacySettings();
  107 +});
  108 +// 页面隐藏时保存变更
  109 +onUnload(() => {
  110 + saveSettings();
  111 +});
  112 +// 获取当前设置
  113 +const getCurrentSettings = () => ({
  114 + movementLocusSwitch: movementLocusSwitch.value,
  115 + commonInterestSwitch: commonInterestSwitch.value,
  116 + medalFriendSwitch: medalFriendSwitch.value,
  117 + leagueClassSwitch: leagueClassSwitch.value,
  118 + personalTrainingSwitch: personalTrainingSwitch.value,
  119 + miniClassTeamSwitch: miniClassTeamSwitch.value,
  120 + homepagePrivacySwitch: homepagePrivacySwitch.value,
  121 + rankingSwitch: rankingSwitch.value,
  122 +});
  123 +
  124 +// 加载隐私设置
  125 +const loadPrivacySettings = async () => {
  126 + try {
  127 + // 这里调用后端接口获取隐私设置
  128 + const res = await SettingApi.getPrivacySetting();
  129 + movementLocusSwitch.value = res.data.movementLocusSwitch || 0; // 运动轨迹
  130 + commonInterestSwitch.value = res.data.commonInterestSwitch || 0; // 共同喜好
  131 + medalFriendSwitch.value = res.data.medalFriendSwitch || 0; // 勋章和好友竞赛
  132 + leagueClassSwitch.value = res.data.leagueClassSwitch || 0; // 团课约课信息
  133 + personalTrainingSwitch.value = res.data.personalTrainingSwitch || 0; // 私教训练报告
  134 + miniClassTeamSwitch.value = res.data.miniClassTeamSwitch || 0; // 小班课战队信息
  135 + homepagePrivacySwitch.value = res.data.homepagePrivacySwitch || 0; // 主页私密账户
  136 + rankingSwitch.value = res.data.rankingSwitch || 0; // 排行榜
  137 + } catch (error) {
  138 + console.error('加载隐私设置失败:', error);
  139 + uni.showToast({
  140 + title: '加载设置失败',
  141 + icon: 'none',
  142 + });
  143 + }
  144 +};
  145 +
  146 +// 保存隐私设置
  147 +const saveSettings = async () => {
  148 + const settings = getCurrentSettings();
  149 + try {
  150 + // 这里调用后端接口保存隐私设置
  151 + await SettingApi.updatePrivacySetting(settings);
  152 + } catch (error) {
  153 + console.error('保存隐私设置失败:', error);
  154 + uni.showToast({
  155 + title: '保存失败',
  156 + icon: 'none',
  157 + });
  158 + }
  159 +};
  160 +// 各个开关的变更处理函数
  161 +const onMovementLocusChange = (e) => {
  162 + const newValue = e.detail.value ? 1 : 0;
  163 + updateSetting('movementLocusSwitch', newValue);
  164 +};
  165 +
  166 +const onCommonInterestChange = (e) => {
  167 + const newValue = e.detail.value ? 1 : 0;
  168 + updateSetting('commonInterestSwitch', newValue);
  169 +};
  170 +
  171 +const onMedalFriendChange = (e) => {
  172 + const newValue = e.detail.value ? 1 : 0;
  173 + updateSetting('medalFriendSwitch', newValue);
  174 +};
  175 +
  176 +const onLeagueClassChange = (e) => {
  177 + const newValue = e.detail.value ? 1 : 0;
  178 + updateSetting('leagueClassSwitch', newValue);
  179 +};
  180 +
  181 +const onPersonalTrainingChange = (e) => {
  182 + const newValue = e.detail.value ? 1 : 0;
  183 + updateSetting('personalTrainingSwitch', newValue);
  184 +};
  185 +
  186 +const onMiniClassTeamChange = (e) => {
  187 + const newValue = e.detail.value ? 1 : 0;
  188 + updateSetting('miniClassTeamSwitch', newValue);
  189 +};
  190 +
  191 +const onHomepagePrivacyChange = (e) => {
  192 + const newValue = e.detail.value ? 1 : 0;
  193 + updateSetting('homepagePrivacySwitch', newValue);
  194 +};
  195 +
  196 +const onRankingChange = (e) => {
  197 + const newValue = e.detail.value ? 1 : 0;
  198 + updateSetting('rankingSwitch', newValue);
  199 +};
  200 +
  201 +// 更新设置的通用方法
  202 +const updateSetting = (key, value) => {
  203 + const refMap = {
  204 + movementLocusSwitch: movementLocusSwitch,
  205 + commonInterestSwitch: commonInterestSwitch,
  206 + medalFriendSwitch: medalFriendSwitch,
  207 + leagueClassSwitch: leagueClassSwitch,
  208 + personalTrainingSwitch: personalTrainingSwitch,
  209 + miniClassTeamSwitch: miniClassTeamSwitch,
  210 + homepagePrivacySwitch: homepagePrivacySwitch,
  211 + rankingSwitch: rankingSwitch,
  212 + };
  213 +
  214 + if (refMap[key]) {
  215 + refMap[key].value = value;
  216 + }
  217 +};
  218 +
  219 +// 返回上一页
  220 +// const goBack = () => {
  221 +// if (hasChanges.value) {
  222 +// uni.showModal({
  223 +// title: '提示',
  224 +// content: '您有未保存的修改,确定要离开吗?',
  225 +// success: (res) => {
  226 +// if (res.confirm) {
  227 +// uni.navigateBack();
  228 +// }
  229 +// },
  230 +// });
  231 +// } else {
  232 +// uni.navigateBack();
  233 +// }
  234 +// };
  235 +</script>
  236 +
  237 +<style lang="scss" scoped>
  238 +// 定义全局样式变量,便于统一管理和修改
  239 +$bg-color: #f5f5f5;
  240 +$white-color: #ffffff;
  241 +$text-primary: #333333;
  242 +$text-secondary: #666666;
  243 +$text-tertiary: #999999;
  244 +$primary-color: #007aff;
  245 +
  246 +$border-radius-base: 16rpx;
  247 +$border-radius-large: 50rpx;
  248 +$padding-base: 30rpx;
  249 +$padding-small: 20rpx;
  250 +$padding-xs: 10rpx;
  251 +
  252 +.privacy-settings-page {
  253 + background-color: $bg-color;
  254 + min-height: 100vh;
  255 +
  256 + // 设置容器样式
  257 + .settings-container {
  258 + padding: $padding-small;
  259 +
  260 + // 分区标题
  261 + .section-title {
  262 + font-size: 28rpx;
  263 + color: $text-tertiary;
  264 + margin-bottom: $padding-small;
  265 + padding-left: $padding-xs;
  266 + }
  267 +
  268 + // 设置项样式
  269 + .setting-item {
  270 + background-color: $white-color;
  271 + margin-bottom: $padding-small;
  272 + padding: $padding-base;
  273 + border-radius: $border-radius-base;
  274 + display: flex;
  275 + align-items: center;
  276 + justify-content: space-between;
  277 + box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
  278 +
  279 + // 设置项信息区域
  280 + .setting-info {
  281 + flex: 1;
  282 + margin-right: $padding-small;
  283 +
  284 + // 设置项名称
  285 + .setting-name {
  286 + font-size: 32rpx;
  287 + color: $text-primary;
  288 + font-weight: 500;
  289 + margin-bottom: 8rpx;
  290 + }
  291 +
  292 + // 设置项描述
  293 + .setting-desc {
  294 + font-size: 26rpx;
  295 + color: $text-tertiary;
  296 + line-height: 1.4;
  297 + }
  298 + }
  299 + }
  300 + }
  301 +
  302 + // 保存提示框样式
  303 + // .save-notice {
  304 + // position: fixed;
  305 + // bottom: 60rpx;
  306 + // left: 40rpx;
  307 + // right: 40rpx;
  308 + // background-color: $white-color;
  309 + // border-radius: $border-radius-large;
  310 + // padding: $padding-small $padding-base;
  311 + // display: flex;
  312 + // align-items: center;
  313 + // justify-content: space-between;
  314 + // box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
  315 +
  316 + // .notice-content {
  317 + // display: flex;
  318 + // align-items: center;
  319 +
  320 + // .notice-icon {
  321 + // font-size: 32rpx;
  322 + // margin-right: 16rpx;
  323 + // }
  324 +
  325 + // .notice-text {
  326 + // font-size: 28rpx;
  327 + // color: $text-secondary;
  328 + // }
  329 + // }
  330 +
  331 + // .save-text {
  332 + // font-size: 28rpx;
  333 + // color: $primary-color;
  334 + // font-weight: 500;
  335 + // }
  336 + // }
  337 +}
  338 +</style>
@@ -7,6 +7,8 @@ const FileApi = { @@ -7,6 +7,8 @@ const FileApi = {
7 uni.showLoading({ 7 uni.showLoading({
8 title: '上传中', 8 title: '上传中',
9 }); 9 });
  10 + console.log(file, 'file');
  11 +
10 return new Promise((resolve, reject) => { 12 return new Promise((resolve, reject) => {
11 uni.uploadFile({ 13 uni.uploadFile({
12 url: baseUrl + apiPath + '/infra/file/upload', 14 url: baseUrl + apiPath + '/infra/file/upload',
@@ -21,6 +23,8 @@ const FileApi = { @@ -21,6 +23,8 @@ const FileApi = {
21 directory, 23 directory,
22 }, 24 },
23 success: (uploadFileRes) => { 25 success: (uploadFileRes) => {
  26 + console.log(uploadFileRes, 'uploadFileRes-');
  27 +
24 let result = JSON.parse(uploadFileRes.data); 28 let result = JSON.parse(uploadFileRes.data);
25 if (result.error === 1) { 29 if (result.error === 1) {
26 uni.showToast({ 30 uni.showToast({
@@ -250,7 +250,7 @@ const UserApi = { @@ -250,7 +250,7 @@ const UserApi = {
250 // 获得我的信息 250 // 获得我的信息
251 getUserInfo: () => { 251 getUserInfo: () => {
252 return request({ 252 return request({
253 - url: '/app/student/myInfo', 253 + url: '/app/user/myInfo',
254 method: 'GET', 254 method: 'GET',
255 custom: { 255 custom: {
256 showLoading: false, 256 showLoading: false,