Authored by qxm

训计页面新增,我的。

Showing 62 changed files with 2280 additions and 185 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,6 +178,38 @@ @@ -178,6 +178,38 @@
178 "navigationBarTitleText": "个人资料", 178 "navigationBarTitleText": "个人资料",
179 "navigationStyle": "default" 179 "navigationStyle": "default"
180 } 180 }
  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 + }
181 } 213 }
182 ] 214 ]
183 } 215 }
@@ -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 || 5 + <image :src="formData.avatar ||
8 'https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260526/默认头像_1779779926983.png' 6 'https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260526/默认头像_1779779926983.png'
9 - "  
10 - mode="aspectFill"  
11 - class="avatar-image"  
12 - ></image> 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,26 +93,26 @@ @@ -128,26 +93,26 @@
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({ 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({
151 avatar: '', 116 avatar: '',
152 nickname: '', 117 nickname: '',
153 sex: 0, 118 sex: 0,
@@ -157,24 +122,24 @@ @@ -157,24 +122,24 @@
157 signature: '', 122 signature: '',
158 createTime: '', 123 createTime: '',
159 isAllowUpdSex: false, 124 isAllowUpdSex: false,
160 - }); 125 +});
161 126
162 - // 深度解耦的弹窗缓存数据  
163 - const editCacheData = reactive({ 127 +// 深度解耦的弹窗缓存数据
  128 +const editCacheData = reactive({
164 nickname: '', 129 nickname: '',
165 sex: 0, 130 sex: 0,
166 - }); 131 +});
167 132
168 - // 省市区级联选择器状态流  
169 - const addressTree = ref([]);  
170 - const multiArray = ref([[], [], []]);  
171 - const multiIndex = ref([0, 0, 0]);  
172 - const selectedAddress = ref(''); 133 +// 省市区级联选择器状态流
  134 +const addressTree = ref([]);
  135 +const multiArray = ref([[], [], []]);
  136 +const multiIndex = ref([0, 0, 0]);
  137 +const selectedAddress = ref('');
173 138
174 - /** 139 +/**
175 * 宿主生命周期钩子流 140 * 宿主生命周期钩子流
176 */ 141 */
177 - onShow(async () => { 142 +onShow(async () => {
178 try { 143 try {
179 // 1. 同步加载行政地区三级树(内部已作 Session 缓存,避免重复请求) 144 // 1. 同步加载行政地区三级树(内部已作 Session 缓存,避免重复请求)
180 await fetchAreaTree(); 145 await fetchAreaTree();
@@ -187,12 +152,12 @@ @@ -187,12 +152,12 @@
187 } catch (error) { 152 } catch (error) {
188 console.error('[Init Error] 初始化个人信息数据流失败:', error); 153 console.error('[Init Error] 初始化个人信息数据流失败:', error);
189 } 154 }
190 - }); 155 +});
191 156
192 - /** 157 +/**
193 * 拦截非保存下的非正常退出,防止用户误触返回造成数据丢失 158 * 拦截非保存下的非正常退出,防止用户误触返回造成数据丢失
194 */ 159 */
195 - onBackPress((options) => { 160 +onBackPress((options) => {
196 // 监听表单内容变动(仅在未保存且已有修改时提示) 161 // 监听表单内容变动(仅在未保存且已有修改时提示)
197 const isUnsaved = JSON.stringify(formData.value) !== JSON.stringify(userStore.userInfo); 162 const isUnsaved = JSON.stringify(formData.value) !== JSON.stringify(userStore.userInfo);
198 if (isUnsaved && !saveLoading.value) { 163 if (isUnsaved && !saveLoading.value) {
@@ -209,15 +174,18 @@ @@ -209,15 +174,18 @@
209 return true; // 拦截返回键 174 return true; // 拦截返回键
210 } 175 }
211 return false; 176 return false;
212 - }); 177 +});
213 178
214 - /** 179 +/**
215 * 头像上传逻辑:增加微信ChooseAvatar路径格式预检与文件防崩溃重试 180 * 头像上传逻辑:增加微信ChooseAvatar路径格式预检与文件防崩溃重试
216 */ 181 */
217 - const handleChooseAvatar = async (e) => { 182 +const handleChooseAvatar = async (e) => {
218 const { avatarUrl } = e.detail; 183 const { avatarUrl } = e.detail;
  184 +
  185 +
219 if (!avatarUrl) return; 186 if (!avatarUrl) return;
220 187
  188 +
221 // 前置性能防御:对 H5/App 端选择本地大图片进行预警(微信自带ChooseAvatar主要回传临时压缩图,此处作跨端边界防守) 189 // 前置性能防御:对 H5/App 端选择本地大图片进行预警(微信自带ChooseAvatar主要回传临时压缩图,此处作跨端边界防守)
222 // #ifndef MP-WEIXIN 190 // #ifndef MP-WEIXIN
223 uni.getFileInfo({ 191 uni.getFileInfo({
@@ -232,35 +200,33 @@ @@ -232,35 +200,33 @@
232 }); 200 });
233 // #endif 201 // #endif
234 202
235 - uni.showLoading({ title: '头像上传中...', mask: true }); 203 +
236 try { 204 try {
237 const res = await FileApi.uploadFile(avatarUrl); 205 const res = await FileApi.uploadFile(avatarUrl);
238 - if (res?.data) { 206 +
  207 + console.log('res----------', res);
  208 +
  209 +
239 formData.value.avatar = res.data; 210 formData.value.avatar = res.data;
240 - uni.showToast({ title: '头像上传成功', icon: 'success' });  
241 - } else {  
242 - throw new Error('服务器未返回有效的图片URL路径');  
243 - } 211 +
244 } catch (err) { 212 } catch (err) {
245 - console.error('[Upload Error] 临时路径转存服务器失败:', err);  
246 - uni.showToast({ title: '头像保存失败,请稍后重试', icon: 'none' });  
247 - } finally {  
248 - uni.hideLoading(); 213 + console.error(err);
  214 +
249 } 215 }
250 - }; 216 +};
251 217
252 - /** 218 +/**
253 * 唤起并初始化昵称弹窗缓存 219 * 唤起并初始化昵称弹窗缓存
254 */ 220 */
255 - const gotoEditNickname = () => { 221 +const gotoEditNickname = () => {
256 editCacheData.nickname = formData.value.nickname || ''; 222 editCacheData.nickname = formData.value.nickname || '';
257 nicknamePopupRef.value.open(); 223 nicknamePopupRef.value.open();
258 - }; 224 +};
259 225
260 - /** 226 +/**
261 * 确认编辑昵称 227 * 确认编辑昵称
262 */ 228 */
263 - const confirmEditNickname = () => { 229 +const confirmEditNickname = () => {
264 const targetNickname = editCacheData.nickname ? editCacheData.nickname.trim() : ''; 230 const targetNickname = editCacheData.nickname ? editCacheData.nickname.trim() : '';
265 if (!targetNickname) { 231 if (!targetNickname) {
266 uni.showToast({ title: '昵称不能为空', icon: 'none' }); 232 uni.showToast({ title: '昵称不能为空', icon: 'none' });
@@ -268,45 +234,45 @@ @@ -268,45 +234,45 @@
268 } 234 }
269 formData.value.nickname = targetNickname; 235 formData.value.nickname = targetNickname;
270 nicknamePopupRef.value.close(); 236 nicknamePopupRef.value.close();
271 - }; 237 +};
272 238
273 - /** 239 +/**
274 * 唤起并初始化性别弹窗 240 * 唤起并初始化性别弹窗
275 */ 241 */
276 - const gotoSelectGender = () => { 242 +const gotoSelectGender = () => {
277 if (!formData.value.isAllowUpdSex) { 243 if (!formData.value.isAllowUpdSex) {
278 uni.showToast({ title: '性别暂不支持多次修改', icon: 'none' }); 244 uni.showToast({ title: '性别暂不支持多次修改', icon: 'none' });
279 return; 245 return;
280 } 246 }
281 editCacheData.sex = formData.value.sex || 0; 247 editCacheData.sex = formData.value.sex || 0;
282 genderPopupRef.value.open(); 248 genderPopupRef.value.open();
283 - }; 249 +};
284 250
285 - /** 251 +/**
286 * 优化:解耦的性别变更逻辑 252 * 优化:解耦的性别变更逻辑
287 */ 253 */
288 - const selectGender = (sex) => { 254 +const selectGender = (sex) => {
289 // 1. 临时更改缓存态而非直接写回 formData 255 // 1. 临时更改缓存态而非直接写回 formData
290 editCacheData.sex = sex; 256 editCacheData.sex = sex;
291 formData.value.sex = sex; // 仅在用户明确选定后回写,并带有 200ms 的视觉缓冲反馈 257 formData.value.sex = sex; // 仅在用户明确选定后回写,并带有 200ms 的视觉缓冲反馈
292 setTimeout(() => { 258 setTimeout(() => {
293 genderPopupRef.value.close(); 259 genderPopupRef.value.close();
294 }, 200); 260 }, 200);
295 - }; 261 +};
296 262
297 - /** 263 +/**
298 * 弹窗关闭时彻底释放和清理临时的输入脏状态 264 * 弹窗关闭时彻底释放和清理临时的输入脏状态
299 */ 265 */
300 - const onNicknamePopupChange = (e) => { 266 +const onNicknamePopupChange = (e) => {
301 if (!e.show) { 267 if (!e.show) {
302 editCacheData.nickname = ''; 268 editCacheData.nickname = '';
303 } 269 }
304 - }; 270 +};
305 271
306 - /** 272 +/**
307 * 优化:实现会话级局部静态缓存,解决 onShow 高频请求带来的服务器资源浪费 273 * 优化:实现会话级局部静态缓存,解决 onShow 高频请求带来的服务器资源浪费
308 */ 274 */
309 - const fetchAreaTree = async () => { 275 +const fetchAreaTree = async () => {
310 if (addressTree.value && addressTree.value.length > 0) return; 276 if (addressTree.value && addressTree.value.length > 0) return;
311 277
312 // 尝试拉取框架/系统底层会话级存储或内存,确保单次生命周期内仅调用一次 API 278 // 尝试拉取框架/系统底层会话级存储或内存,确保单次生命周期内仅调用一次 API
@@ -325,12 +291,12 @@ @@ -325,12 +291,12 @@
325 } catch (error) { 291 } catch (error) {
326 console.error('[API Error] 获取省市区行政结构树失败:', error); 292 console.error('[API Error] 获取省市区行政结构树失败:', error);
327 } 293 }
328 - }; 294 +};
329 295
330 - /** 296 +/**
331 * 安全地同步极简Picker的三列静态节点关联数据 297 * 安全地同步极简Picker的三列静态节点关联数据
332 */ 298 */
333 - const syncPickerColumnData = (pIdx = 0, cIdx = 0) => { 299 +const syncPickerColumnData = (pIdx = 0, cIdx = 0) => {
334 if (!addressTree.value || addressTree.value.length === 0) return; 300 if (!addressTree.value || addressTree.value.length === 0) return;
335 301
336 const provinces = addressTree.value; 302 const provinces = addressTree.value;
@@ -340,12 +306,12 @@ @@ -340,12 +306,12 @@
340 const regions = cSelected?.children || []; 306 const regions = cSelected?.children || [];
341 307
342 multiArray.value = [provinces, cities, regions]; 308 multiArray.value = [provinces, cities, regions];
343 - }; 309 +};
344 310
345 - /** 311 +/**
346 * 【关键越界重构】:修正真机由于异步滚动的时差引发的越界闪退 312 * 【关键越界重构】:修正真机由于异步滚动的时差引发的越界闪退
347 */ 313 */
348 - const pickerColumnChange = (e) => { 314 +const pickerColumnChange = (e) => {
349 const { column, value } = e.detail; 315 const { column, value } = e.detail;
350 316
351 // 1. 原子化本地临时深拷贝索引指针 317 // 1. 原子化本地临时深拷贝索引指针
@@ -376,12 +342,12 @@ @@ -376,12 +342,12 @@
376 nextTick(() => { 342 nextTick(() => {
377 multiIndex.value = nextIndex; 343 multiIndex.value = nextIndex;
378 }); 344 });
379 - }; 345 +};
380 346
381 - /** 347 +/**
382 * 确认选择省市区,绑定数据映射 348 * 确认选择省市区,绑定数据映射
383 */ 349 */
384 - const pickerChange = (e) => { 350 +const pickerChange = (e) => {
385 const indices = e.detail.value; 351 const indices = e.detail.value;
386 multiIndex.value = [...indices]; 352 multiIndex.value = [...indices];
387 353
@@ -394,12 +360,12 @@ @@ -394,12 +360,12 @@
394 formData.value.region = rObj?.id || ''; 360 formData.value.region = rObj?.id || '';
395 361
396 selectedAddress.value = [pObj?.name, cObj?.name, rObj?.name].filter(Boolean).join(' '); 362 selectedAddress.value = [pObj?.name, cObj?.name, rObj?.name].filter(Boolean).join(' ');
397 - }; 363 +};
398 364
399 - /** 365 +/**
400 * 数据回显定位器:精准利用底层树定位当前用户已有地区信息 366 * 数据回显定位器:精准利用底层树定位当前用户已有地区信息
401 */ 367 */
402 - const initEchoAddress = () => { 368 +const initEchoAddress = () => {
403 const { province, city, region } = formData.value; 369 const { province, city, region } = formData.value;
404 if (!province || !addressTree.value || addressTree.value.length === 0) { 370 if (!province || !addressTree.value || addressTree.value.length === 0) {
405 syncPickerColumnData(0, 0); 371 syncPickerColumnData(0, 0);
@@ -438,12 +404,12 @@ @@ -438,12 +404,12 @@
438 ] 404 ]
439 .filter(Boolean) 405 .filter(Boolean)
440 .join(' '); 406 .join(' ');
441 - }; 407 +};
442 408
443 - /** 409 +/**
444 * 保存修改逻辑:进行脏字段过滤净化后,更新用户信息 410 * 保存修改逻辑:进行脏字段过滤净化后,更新用户信息
445 */ 411 */
446 - const saveProfile = async () => { 412 +const saveProfile = async () => {
447 if (saveLoading.value) return; 413 if (saveLoading.value) return;
448 414
449 // 前置轻校验:去除尾部空隙并校验合法性 415 // 前置轻校验:去除尾部空隙并校验合法性
@@ -485,38 +451,38 @@ @@ -485,38 +451,38 @@
485 uni.hideLoading(); 451 uni.hideLoading();
486 saveLoading.value = false; 452 saveLoading.value = false;
487 } 453 }
488 - }; 454 +};
489 455
490 - // 格式化展示原子转换工具  
491 - const formatGender = (sex) => { 456 +// 格式化展示原子转换工具
  457 +const formatGender = (sex) => {
492 if (sex === 1) return '男'; 458 if (sex === 1) return '男';
493 if (sex === 2) return '女'; 459 if (sex === 2) return '女';
494 return '保密'; 460 return '保密';
495 - }; 461 +};
496 462
497 - const formatRegisterTime = (time) => { 463 +const formatRegisterTime = (time) => {
498 if (!time) return '--'; 464 if (!time) return '--';
499 return dayjs(time).format('YYYY-MM-DD'); 465 return dayjs(time).format('YYYY-MM-DD');
500 - }; 466 +};
501 </script> 467 </script>
502 468
503 <style scoped lang="scss"> 469 <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 { 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 {
520 background-color: $color-bg; 486 background-color: $color-bg;
521 height: 100vh; 487 height: 100vh;
522 // #ifdef H5 488 // #ifdef H5
@@ -536,6 +502,7 @@ @@ -536,6 +502,7 @@
536 padding: 0; 502 padding: 0;
537 line-height: 0; 503 line-height: 0;
538 border-radius: 50%; 504 border-radius: 50%;
  505 +
539 &::after { 506 &::after {
540 content: none; 507 content: none;
541 } 508 }
@@ -630,10 +597,10 @@ @@ -630,10 +597,10 @@
630 } 597 }
631 } 598 }
632 } 599 }
633 - } 600 +}
634 601
635 - // 弹出层统一样式  
636 - .popup-container { 602 +// 弹出层统一样式
  603 +.popup-container {
637 background-color: $color-white; 604 background-color: $color-white;
638 border-radius: $radius-lg $radius-lg 0 0; 605 border-radius: $radius-lg $radius-lg 0 0;
639 padding-bottom: calc(30rpx + env(safe-area-inset-bottom)); 606 padding-bottom: calc(30rpx + env(safe-area-inset-bottom));
@@ -644,6 +611,7 @@ @@ -644,6 +611,7 @@
644 justify-content: center; 611 justify-content: center;
645 padding: $spacing-md $spacing-sm; 612 padding: $spacing-md $spacing-sm;
646 border-bottom: 1rpx solid #fcfcfc; 613 border-bottom: 1rpx solid #fcfcfc;
  614 +
647 .popup-title { 615 .popup-title {
648 font-size: 32rpx; 616 font-size: 32rpx;
649 color: $color-text-dark; 617 color: $color-text-dark;
@@ -697,6 +665,7 @@ @@ -697,6 +665,7 @@
697 665
698 .popup-footer { 666 .popup-footer {
699 padding: 0 $spacing-sm; 667 padding: 0 $spacing-sm;
  668 +
700 .confirm-btn { 669 .confirm-btn {
701 width: 100%; 670 width: 100%;
702 height: 88rpx; 671 height: 88rpx;
@@ -706,15 +675,17 @@ @@ -706,15 +675,17 @@
706 font-size: 28rpx; 675 font-size: 28rpx;
707 font-weight: 500; 676 font-weight: 500;
708 border-radius: $radius; 677 border-radius: $radius;
  678 +
709 &:after { 679 &:after {
710 content: none; 680 content: none;
711 } 681 }
712 } 682 }
713 } 683 }
714 - } 684 +}
715 685
716 - .button-section { 686 +.button-section {
717 padding: $spacing-lg $spacing-sm; 687 padding: $spacing-lg $spacing-sm;
  688 +
718 .save-btn { 689 .save-btn {
719 width: 100%; 690 width: 100%;
720 height: 88rpx; 691 height: 88rpx;
@@ -724,9 +695,10 @@ @@ -724,9 +695,10 @@
724 font-size: 28rpx; 695 font-size: 28rpx;
725 font-weight: 500; 696 font-weight: 500;
726 border-radius: $radius; 697 border-radius: $radius;
  698 +
727 &:after { 699 &:after {
728 content: none; 700 content: none;
729 } 701 }
730 } 702 }
731 - } 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,