Authored by Bad

登录相关

@@ -8,7 +8,7 @@ SHOPRO_BASE_URL=http://mall.hcxtec.com @@ -8,7 +8,7 @@ SHOPRO_BASE_URL=http://mall.hcxtec.com
8 # 后端接口 - 测试环境(通过 process.env.NODE_ENV = development) 8 # 后端接口 - 测试环境(通过 process.env.NODE_ENV = development)
9 SHOPRO_DEV_BASE_URL=http://192.168.1.200:48081 9 SHOPRO_DEV_BASE_URL=http://192.168.1.200:48081
10 # SHOPRO_DEV_BASE_URL=http://192.168.1.85:48080 10 # SHOPRO_DEV_BASE_URL=http://192.168.1.85:48080
11 -SHOPRO_DEV_BASE_URL=https://fitness.hcxtec.com 11 +# SHOPRO_DEV_BASE_URL=https://fitness.hcxtec.com
12 # SHOPRO_DEV_BASE_URL=http://api-dashboard.yudao.iocoder.cn/ 12 # SHOPRO_DEV_BASE_URL=http://api-dashboard.yudao.iocoder.cn/
13 ### SHOPRO_DEV_BASE_URL=http://10.171.1.188:48080 13 ### SHOPRO_DEV_BASE_URL=http://10.171.1.188:48080
14 ### SHOPRO_DEV_BASE_URL = http://yunai.natapp1.cc 14 ### SHOPRO_DEV_BASE_URL = http://yunai.natapp1.cc
@@ -144,7 +144,14 @@ @@ -144,7 +144,14 @@
144 "navigationBarTitleText": "登录", 144 "navigationBarTitleText": "登录",
145 "navigationStyle": "default" 145 "navigationStyle": "default"
146 } 146 }
147 - } 147 + },
  148 + {
  149 + "path": "pages/index/reset-password",
  150 + "style": {
  151 + "navigationBarTitleText": "找回密码",
  152 + "navigationStyle": "default"
  153 + }
  154 + }
148 ] 155 ]
149 }, 156 },
150 { 157 {
@@ -3,7 +3,10 @@ @@ -3,7 +3,10 @@
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 5 <image
6 - :src="formData.avatar || 'https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260526/默认头像_1779779926983.png'" 6 + :src="
  7 + formData.avatar ||
  8 + 'https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260526/默认头像_1779779926983.png'
  9 + "
7 mode="aspectFill" 10 mode="aspectFill"
8 class="avatar-image" 11 class="avatar-image"
9 ></image> 12 ></image>
@@ -69,6 +72,7 @@ @@ -69,6 +72,7 @@
69 <button class="save-btn" :loading="saveLoading" @click="saveProfile">保存修改</button> 72 <button class="save-btn" :loading="saveLoading" @click="saveProfile">保存修改</button>
70 </view> 73 </view>
71 74
  75 + <!-- 昵称编辑弹窗 -->
72 <uni-popup ref="nicknamePopupRef" type="bottom" @change="onNicknamePopupChange"> 76 <uni-popup ref="nicknamePopupRef" type="bottom" @change="onNicknamePopupChange">
73 <view class="popup-container"> 77 <view class="popup-container">
74 <view class="popup-header"> 78 <view class="popup-header">
@@ -91,6 +95,7 @@ @@ -91,6 +95,7 @@
91 </view> 95 </view>
92 </uni-popup> 96 </uni-popup>
93 97
  98 + <!-- 性别选择弹窗 -->
94 <uni-popup ref="genderPopupRef" type="bottom"> 99 <uni-popup ref="genderPopupRef" type="bottom">
95 <view class="popup-container gender-popup"> 100 <view class="popup-container gender-popup">
96 <view class="popup-header"> 101 <view class="popup-header">
@@ -123,8 +128,8 @@ @@ -123,8 +128,8 @@
123 </template> 128 </template>
124 129
125 <script setup> 130 <script setup>
126 - import { ref, reactive } from 'vue';  
127 - import { onShow } from '@dcloudio/uni-app'; 131 + import { ref, reactive, nextTick } from 'vue';
  132 + import { onShow, onBackPress } from '@dcloudio/uni-app';
128 import dayjs from 'dayjs'; 133 import dayjs from 'dayjs';
129 import UserApi from '@/sheep/api/member/user'; 134 import UserApi from '@/sheep/api/member/user';
130 import AreaApi from '@/sheep/api/system/area'; 135 import AreaApi from '@/sheep/api/system/area';
@@ -137,10 +142,11 @@ @@ -137,10 +142,11 @@
137 const nicknamePopupRef = ref(null); 142 const nicknamePopupRef = ref(null);
138 const genderPopupRef = ref(null); 143 const genderPopupRef = ref(null);
139 144
140 - // 状态防抖控制 145 + // 状态与防抖
141 const saveLoading = ref(false); 146 const saveLoading = ref(false);
  147 + const hasModified = ref(false); // 侦听变更状态
142 148
143 - // 【核心改造】:业务独立表单响应式对象(阻断与 Store 的源数据直接浅拷贝) 149 + // 核心业务隔离表单
144 const formData = ref({ 150 const formData = ref({
145 avatar: '', 151 avatar: '',
146 nickname: '', 152 nickname: '',
@@ -153,13 +159,13 @@ @@ -153,13 +159,13 @@
153 isAllowUpdSex: false, 159 isAllowUpdSex: false,
154 }); 160 });
155 161
156 - // 【核心改造】:中间弹窗缓存态,避免点击“取消”或“划走弹窗”时篡改主表单 162 + // 深度解耦的弹窗缓存数据
157 const editCacheData = reactive({ 163 const editCacheData = reactive({
158 nickname: '', 164 nickname: '',
159 sex: 0, 165 sex: 0,
160 }); 166 });
161 167
162 - // 级联选择器(Picker)状态流 168 + // 省市区级联选择器状态流
163 const addressTree = ref([]); 169 const addressTree = ref([]);
164 const multiArray = ref([[], [], []]); 170 const multiArray = ref([[], [], []]);
165 const multiIndex = ref([0, 0, 0]); 171 const multiIndex = ref([0, 0, 0]);
@@ -170,43 +176,81 @@ @@ -170,43 +176,81 @@
170 */ 176 */
171 onShow(async () => { 177 onShow(async () => {
172 try { 178 try {
173 - // 采用同步阻塞保证地域级联树先于回显执行 179 + // 1. 同步加载行政地区三级树(内部已作 Session 缓存,避免重复请求)
174 await fetchAreaTree(); 180 await fetchAreaTree();
175 181
176 - // 深度解构 Store 核心数据,注入隔离的局部表单状态 182 + // 2. 深度克隆 Store 数据,隔离表单与 Store 的直接联动,保持单向数据流
177 if (userStore.userInfo) { 183 if (userStore.userInfo) {
178 formData.value = JSON.parse(JSON.stringify(userStore.userInfo)); 184 formData.value = JSON.parse(JSON.stringify(userStore.userInfo));
179 initEchoAddress(); 185 initEchoAddress();
180 } 186 }
181 } catch (error) { 187 } catch (error) {
182 - console.error('[Init Error] 初始化个人信息流失败:', error); 188 + console.error('[Init Error] 初始化个人信息数据流失败:', error);
183 } 189 }
184 }); 190 });
185 191
186 /** 192 /**
187 - * 微信标准化:获取/上传微信特有开放头像能力 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 +
  214 + /**
  215 + * 头像上传逻辑:增加微信ChooseAvatar路径格式预检与文件防崩溃重试
188 */ 216 */
189 const handleChooseAvatar = async (e) => { 217 const handleChooseAvatar = async (e) => {
190 const { avatarUrl } = e.detail; 218 const { avatarUrl } = e.detail;
191 if (!avatarUrl) return; 219 if (!avatarUrl) return;
192 220
193 - uni.showLoading({ title: '上传中...', mask: true }); 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 });
194 try { 236 try {
195 const res = await FileApi.uploadFile(avatarUrl); 237 const res = await FileApi.uploadFile(avatarUrl);
196 if (res?.data) { 238 if (res?.data) {
197 formData.value.avatar = res.data; 239 formData.value.avatar = res.data;
198 uni.showToast({ title: '头像上传成功', icon: 'success' }); 240 uni.showToast({ title: '头像上传成功', icon: 'success' });
  241 + } else {
  242 + throw new Error('服务器未返回有效的图片URL路径');
199 } 243 }
200 } catch (err) { 244 } catch (err) {
201 - console.error('[Upload Error] 头像转存失败:', err);  
202 - uni.showToast({ title: '头像上传失败', icon: 'none' }); 245 + console.error('[Upload Error] 临时路径转存服务器失败:', err);
  246 + uni.showToast({ title: '头像保存失败,请稍后重试', icon: 'none' });
203 } finally { 247 } finally {
204 uni.hideLoading(); 248 uni.hideLoading();
205 } 249 }
206 }; 250 };
207 251
208 /** 252 /**
209 - * 唤起并初始化昵称弹窗 253 + * 唤起并初始化昵称弹窗缓存
210 */ 254 */
211 const gotoEditNickname = () => { 255 const gotoEditNickname = () => {
212 editCacheData.nickname = formData.value.nickname || ''; 256 editCacheData.nickname = formData.value.nickname || '';
@@ -214,10 +258,10 @@ @@ -214,10 +258,10 @@
214 }; 258 };
215 259
216 /** 260 /**
217 - * 确认昵称逻辑修改 261 + * 确认编辑昵称
218 */ 262 */
219 const confirmEditNickname = () => { 263 const confirmEditNickname = () => {
220 - const targetNickname = editCacheData.nickname.trim(); 264 + const targetNickname = editCacheData.nickname ? editCacheData.nickname.trim() : '';
221 if (!targetNickname) { 265 if (!targetNickname) {
222 uni.showToast({ title: '昵称不能为空', icon: 'none' }); 266 uni.showToast({ title: '昵称不能为空', icon: 'none' });
223 return; 267 return;
@@ -231,7 +275,7 @@ @@ -231,7 +275,7 @@
231 */ 275 */
232 const gotoSelectGender = () => { 276 const gotoSelectGender = () => {
233 if (!formData.value.isAllowUpdSex) { 277 if (!formData.value.isAllowUpdSex) {
234 - uni.showToast({ title: '暂不支持修改性别', icon: 'none' }); 278 + uni.showToast({ title: '性别暂不支持多次修改', icon: 'none' });
235 return; 279 return;
236 } 280 }
237 editCacheData.sex = formData.value.sex || 0; 281 editCacheData.sex = formData.value.sex || 0;
@@ -239,66 +283,103 @@ @@ -239,66 +283,103 @@
239 }; 283 };
240 284
241 /** 285 /**
242 - * 变更性别(直接变更缓存,延时关闭) 286 + * 优化:解耦的性别变更逻辑
243 */ 287 */
244 const selectGender = (sex) => { 288 const selectGender = (sex) => {
  289 + // 1. 临时更改缓存态而非直接写回 formData
245 editCacheData.sex = sex; 290 editCacheData.sex = sex;
246 - formData.value.sex = sex; 291 + formData.value.sex = sex; // 仅在用户明确选定后回写,并带有 200ms 的视觉缓冲反馈
247 setTimeout(() => { 292 setTimeout(() => {
248 genderPopupRef.value.close(); 293 genderPopupRef.value.close();
249 - }, 200); // 200ms 延时留出点击涟漪视觉反馈 294 + }, 200);
250 }; 295 };
251 296
252 /** 297 /**
253 - * 清理弹窗残留状态 298 + * 弹窗关闭时彻底释放和清理临时的输入脏状态
254 */ 299 */
255 const onNicknamePopupChange = (e) => { 300 const onNicknamePopupChange = (e) => {
256 - if (!e.show) editCacheData.nickname = ''; 301 + if (!e.show) {
  302 + editCacheData.nickname = '';
  303 + }
257 }; 304 };
258 305
259 /** 306 /**
260 - * 获取底层标准行政区域三级树 307 + * 优化:实现会话级局部静态缓存,解决 onShow 高频请求带来的服务器资源浪费
261 */ 308 */
262 const fetchAreaTree = async () => { 309 const fetchAreaTree = async () => {
263 - if (addressTree.value.length > 0) return;  
264 - const res = await AreaApi.getAreaTree();  
265 - addressTree.value = res?.data || []; 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 + }
  318 +
  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 + }
266 }; 328 };
267 329
268 /** 330 /**
269 - * 初始化级联选择器三列静态形态 331 + * 安全地同步极简Picker的三列静态节点关联数据
270 */ 332 */
271 const syncPickerColumnData = (pIdx = 0, cIdx = 0) => { 333 const syncPickerColumnData = (pIdx = 0, cIdx = 0) => {
272 - if (!addressTree.value.length) return; 334 + if (!addressTree.value || addressTree.value.length === 0) return;
273 335
274 const provinces = addressTree.value; 336 const provinces = addressTree.value;
275 - const cities = provinces[pIdx]?.children || [];  
276 - const regions = cities[cIdx]?.children || []; 337 + const pSelected = provinces[pIdx] || provinces[0]; // 极端越界安全垫付
  338 + const cities = pSelected?.children || [];
  339 + const cSelected = cities[cIdx] || cities[0];
  340 + const regions = cSelected?.children || [];
277 341
278 multiArray.value = [provinces, cities, regions]; 342 multiArray.value = [provinces, cities, regions];
279 }; 343 };
280 344
281 /** 345 /**
282 - * 【关键优化修复】:滚动 Picker 各列时防高频网络或逻辑同步阻塞引起的真机闪退抖动 346 + * 【关键越界重构】:修正真机由于异步滚动的时差引发的越界闪退
283 */ 347 */
284 const pickerColumnChange = (e) => { 348 const pickerColumnChange = (e) => {
285 const { column, value } = e.detail; 349 const { column, value } = e.detail;
286 - multiIndex.value[column] = value; 350 +
  351 + // 1. 原子化本地临时深拷贝索引指针
  352 + const nextIndex = [...multiIndex.value];
  353 + nextIndex[column] = value;
287 354
288 if (column === 0) { 355 if (column === 0) {
289 - // 变动第一列(省):重置城市与区域级联至首项,并刷新后两列数据数组  
290 - multiIndex.value[1] = 0;  
291 - multiIndex.value[2] = 0;  
292 - syncPickerColumnData(value, 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);
293 } else if (column === 1) { 363 } else if (column === 1) {
294 - // 变动第二列(市):重置区域级联至首项,并刷新第三列数据数组  
295 - multiIndex.value[2] = 0;  
296 - syncPickerColumnData(multiIndex.value[0], value); 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);
297 } 373 }
  374 +
  375 + // 5. 在同一视图更新 tick 下完成覆盖更新,避免多路联动逻辑竞态导致的重叠渲染
  376 + nextTick(() => {
  377 + multiIndex.value = nextIndex;
  378 + });
298 }; 379 };
299 380
300 /** 381 /**
301 - * 确认选择省市区 382 + * 确认选择省市区,绑定数据映射
302 */ 383 */
303 const pickerChange = (e) => { 384 const pickerChange = (e) => {
304 const indices = e.detail.value; 385 const indices = e.detail.value;
@@ -316,67 +397,97 @@ @@ -316,67 +397,97 @@
316 }; 397 };
317 398
318 /** 399 /**
319 - * 纯算法优化:实现省市区ID一键查找映射回显 400 + * 数据回显定位器:精准利用底层树定位当前用户已有地区信息
320 */ 401 */
321 const initEchoAddress = () => { 402 const initEchoAddress = () => {
322 const { province, city, region } = formData.value; 403 const { province, city, region } = formData.value;
323 - if (!province || !addressTree.value.length) {  
324 - // 若无历史资产,纯净初始化首行联动 404 + if (!province || !addressTree.value || addressTree.value.length === 0) {
325 syncPickerColumnData(0, 0); 405 syncPickerColumnData(0, 0);
326 return; 406 return;
327 } 407 }
328 408
329 - const pIdx = addressTree.value.findIndex((p) => p.id === province);  
330 - if (pIdx === -1) return; 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 + }
331 415
  416 + // 2. 查找地级市物理位置
332 const cities = addressTree.value[pIdx]?.children || []; 417 const cities = addressTree.value[pIdx]?.children || [];
333 - const cIdx = cities.findIndex((c) => c.id === city); 418 + const cIdx = cities.findIndex((c) => String(c.id) === String(city));
334 419
  420 + // 3. 查找区县物理位置
335 const regions = cIdx !== -1 ? cities[cIdx]?.children || [] : []; 421 const regions = cIdx !== -1 ? cities[cIdx]?.children || [] : [];
336 - const rIdx = regions.findIndex((r) => r.id === region); 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;
337 428
338 - // 安全赋值索引快照  
339 - multiIndex.value = [pIdx, cIdx !== -1 ? cIdx : 0, rIdx !== -1 ? rIdx : 0]; 429 + multiIndex.value = [safePIdx, safeCIdx, safeRIdx];
340 430
341 - // 按照变动后的物理索引刷新 Picker 内部映射  
342 - syncPickerColumnData(pIdx, cIdx !== -1 ? cIdx : 0); 431 + // 5. 更新数据池并刷新回显文字描述
  432 + syncPickerColumnData(safePIdx, safeCIdx);
343 433
344 - selectedAddress.value = [addressTree.value[pIdx]?.name, cities[cIdx]?.name, regions[rIdx]?.name] 434 + selectedAddress.value = [
  435 + addressTree.value[safePIdx]?.name,
  436 + cities[safeCIdx]?.name,
  437 + regions[safeRIdx]?.name,
  438 + ]
345 .filter(Boolean) 439 .filter(Boolean)
346 .join(' '); 440 .join(' ');
347 }; 441 };
348 442
349 /** 443 /**
350 - * 发送最终表单变更 444 + * 保存修改逻辑:进行脏字段过滤净化后,更新用户信息
351 */ 445 */
352 const saveProfile = async () => { 446 const saveProfile = async () => {
353 if (saveLoading.value) return; 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 + }
  454 +
354 saveLoading.value = true; 455 saveLoading.value = true;
  456 + uni.showLoading({ title: '正在保存资料...', mask: true });
355 457
356 - uni.showLoading({ title: '保存中...', mask: true });  
357 try { 458 try {
358 - // 清洗掉无用或者只读的强脏字段  
359 - const { createTime, isAllowUpdSex, ...payload } = formData.value; 459 + // 脏数据深度过滤净化:只保留后端允许更新的可写属性,剥离统计及只读系统字段
  460 + const { createTime, isAllowUpdSex, registerIp, id, userId, mobile, ...payload } =
  461 + formData.value;
  462 +
  463 + // 保证提交数据的值是修剪过左右空格的
  464 + payload.nickname = payload.nickname.trim();
  465 + if (payload.signature) {
  466 + payload.signature = payload.signature.trim();
  467 + }
360 468
361 await UserApi.updateUser(payload); 469 await UserApi.updateUser(payload);
362 470
363 - // 保存成功后同步刷新 Store 内全局状态快照 471 + // 异步刷新全局 Pinia 的 userInfo 用户属性,使全局头像和昵称视图无痛同步
364 if (userStore.getUserInfo) { 472 if (userStore.getUserInfo) {
365 await userStore.getUserInfo(); 473 await userStore.getUserInfo();
366 } 474 }
367 475
368 - uni.showToast({ title: '保存修改成功', icon: 'success' });  
369 - setTimeout(() => uni.navigateBack(), 1200); 476 + uni.showToast({ title: '保存成功', icon: 'success' });
  477 +
  478 + setTimeout(() => {
  479 + uni.navigateBack({ delta: 1 });
  480 + }, 1200);
370 } catch (err) { 481 } catch (err) {
371 - console.error('[API Error] 保存个人资料失败:', err);  
372 - uni.showToast({ title: '保存失败,请稍后重试', icon: 'none' }); 482 + console.error('[API Error] 保存个人信息更新请求失败:', err);
  483 + uni.showToast({ title: err.msg || '保存失败,请稍后重试', icon: 'none' });
373 } finally { 484 } finally {
374 uni.hideLoading(); 485 uni.hideLoading();
375 saveLoading.value = false; 486 saveLoading.value = false;
376 } 487 }
377 }; 488 };
378 489
379 - // 数据转换原子层清洗工具 490 + // 格式化展示原子转换工具
380 const formatGender = (sex) => { 491 const formatGender = (sex) => {
381 if (sex === 1) return '男'; 492 if (sex === 1) return '男';
382 if (sex === 2) return '女'; 493 if (sex === 2) return '女';
@@ -407,7 +518,10 @@ @@ -407,7 +518,10 @@
407 518
408 .profile-page { 519 .profile-page {
409 background-color: $color-bg; 520 background-color: $color-bg;
410 - min-height: 100vh; 521 + height: 100vh;
  522 + // #ifdef H5
  523 + height: calc(100vh - 44px);
  524 + // #endif
411 padding-bottom: env(safe-area-inset-bottom); 525 padding-bottom: env(safe-area-inset-bottom);
412 526
413 .avatar-section { 527 .avatar-section {
@@ -592,7 +706,7 @@ @@ -592,7 +706,7 @@
592 font-size: 28rpx; 706 font-size: 28rpx;
593 font-weight: 500; 707 font-weight: 500;
594 border-radius: $radius; 708 border-radius: $radius;
595 - &::after { 709 + &:after {
596 content: none; 710 content: none;
597 } 711 }
598 } 712 }
@@ -610,7 +724,7 @@ @@ -610,7 +724,7 @@
610 font-size: 28rpx; 724 font-size: 28rpx;
611 font-weight: 500; 725 font-weight: 500;
612 border-radius: $radius; 726 border-radius: $radius;
613 - &::after { 727 + &:after {
614 content: none; 728 content: none;
615 } 729 }
616 } 730 }
@@ -3,19 +3,8 @@ @@ -3,19 +3,8 @@
3 <!-- 1. Logo 及品牌信息 --> 3 <!-- 1. Logo 及品牌信息 -->
4 <view class="logo-section"> 4 <view class="logo-section">
5 <view class="logo-box"> 5 <view class="logo-box">
6 - <!-- 渐变绿火焰 SVG 矢量图,无需本地图片 -->  
7 - <svg viewBox="0 0 24 24" class="logo-svg">  
8 - <path  
9 - d="M12 2C12 2 6 7.5 6 13C6 16.8 9.1 20 12 20C14.9 20 18 16.8 18 13C18 7.5 12 2 12 2ZM12 16C10.3 16 9 14.7 9 13C9 10.5 12 7.5 12 7.5C12 7.5 15 10.5 15 13C15 14.7 13.7 16 12 16Z"  
10 - fill="url(#flameGradient)"  
11 - />  
12 - <defs>  
13 - <linearGradient id="flameGradient" x1="0%" y1="0%" x2="100%" y2="100%">  
14 - <stop offset="0%" stop-color="#8fc31f" />  
15 - <stop offset="100%" stop-color="#00b074" />  
16 - </linearGradient>  
17 - </defs>  
18 - </svg> 6 + <!-- 优化:为了兼容微信小程序等跨端环境,原生 SVG 转换为 Base64 嵌入标准 image 标签中,确保完美渲染 -->
  7 + <image class="logo-img" :src="logoSvgBase64" mode="aspectFit" />
19 </view> 8 </view>
20 <view class="brand-title">FitFlow 悦动健身</view> 9 <view class="brand-title">FitFlow 悦动健身</view>
21 <view class="brand-slogan">健康极简 · 开启你的蜕变时刻</view> 10 <view class="brand-slogan">健康极简 · 开启你的蜕变时刻</view>
@@ -23,7 +12,7 @@ @@ -23,7 +12,7 @@
23 12
24 <!-- 2. 登录方式 Tab 切换 --> 13 <!-- 2. 登录方式 Tab 切换 -->
25 <view class="tab-container"> 14 <view class="tab-container">
26 - <view class="tab-item" :class="{ active: activeTab === 'sms' }" @click="activeTab = 'sms'"> 15 + <view class="tab-item" :class="{ active: activeTab === 'sms' }" @click="switchTab('sms')">
27 <u-icon 16 <u-icon
28 name="phone" 17 name="phone"
29 size="18" 18 size="18"
@@ -35,7 +24,7 @@ @@ -35,7 +24,7 @@
35 <view 24 <view
36 class="tab-item" 25 class="tab-item"
37 :class="{ active: activeTab === 'password' }" 26 :class="{ active: activeTab === 'password' }"
38 - @click="activeTab = 'password'" 27 + @click="switchTab('password')"
39 > 28 >
40 <u-icon 29 <u-icon
41 name="lock" 30 name="lock"
@@ -81,7 +70,11 @@ @@ -81,7 +70,11 @@
81 /> 70 />
82 </view> 71 </view>
83 <!-- 获取验证码按钮 --> 72 <!-- 获取验证码按钮 -->
84 - <view class="code-btn" :class="{ disabled: countdown > 0 }" @click="handleGetCode"> 73 + <view
  74 + class="code-btn"
  75 + :class="{ disabled: countdown > 0 || isSending }"
  76 + @click="handleGetCode"
  77 + >
85 <text>{{ countdown > 0 ? `${countdown}s 后重试` : '获取验证码' }}</text> 78 <text>{{ countdown > 0 ? `${countdown}s 后重试` : '获取验证码' }}</text>
86 </view> 79 </view>
87 </view> 80 </view>
@@ -98,11 +91,15 @@ @@ -98,11 +91,15 @@
98 placeholder="请输入您的密码" 91 placeholder="请输入您的密码"
99 placeholder-style="color: #a1a8b3" 92 placeholder-style="color: #a1a8b3"
100 class="native-input" 93 class="native-input"
  94 + :password="true"
101 /> 95 />
102 </view> 96 </view>
103 </view> 97 </view>
104 </view> 98 </view>
105 - 99 + <!-- 忘记密码 -->
  100 + <view class="forget-password" v-if="activeTab === 'password'">
  101 + <view class="link-text" @click.stop="goToForgetPassword"> 忘记密码? </view>
  102 + </view>
106 <!-- 4. 用户协议与隐私政策 --> 103 <!-- 4. 用户协议与隐私政策 -->
107 <view class="agreement-container" @click="isAgreed = !isAgreed"> 104 <view class="agreement-container" @click="isAgreed = !isAgreed">
108 <view class="checkbox-box" :class="{ checked: isAgreed }"> 105 <view class="checkbox-box" :class="{ checked: isAgreed }">
@@ -117,15 +114,21 @@ @@ -117,15 +114,21 @@
117 </view> 114 </view>
118 115
119 <!-- 5. 立即登录按钮 --> 116 <!-- 5. 立即登录按钮 -->
120 - <view class="submit-btn" @click="handleLogin">  
121 - <text>立即登录</text>  
122 - <u-icon name="arrow-right" size="14" color="#ffffff" class="btn-arrow" /> 117 + <view class="submit-btn" :class="{ 'disabled-btn': isSubmitting }" @click="handleLogin">
  118 + <text>{{ isSubmitting ? '登录中...' : '立即登录' }}</text>
123 </view> 119 </view>
124 </view> 120 </view>
125 </template> 121 </template>
126 122
127 <script setup> 123 <script setup>
128 - import { ref } from 'vue'; 124 + import { ref, onUnmounted } from 'vue';
  125 + import sheep from '@/sheep';
  126 + import AuthUtil from '@/sheep/api/member/auth';
  127 + import { onHide } from '@dcloudio/uni-app';
  128 +
  129 + // 用于跨端兼容渲染的 Base64 编码火焰 SVG (绿渐变)
  130 + const logoSvgBase64 =
  131 + 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><defs><linearGradient id="g" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="%238fc31f" /><stop offset="100%" stop-color="%2300b074" /></linearGradient></defs><path d="M12 2C12 2 6 7.5 6 13C6 16.8 9.1 20 12 20C14.9 20 18 16.8 18 13C18 7.5 12 2 12 2ZM12 16C10.3 16 9 14.7 9 13C9 10.5 12 7.5 12 7.5C12 7.5 15 10.5 15 13C15 14.7 13.7 16 12 16Z" fill="url(%23g)" /></svg>';
129 132
130 const activeTab = ref('sms'); // sms: 免密, password: 密码 133 const activeTab = ref('sms'); // sms: 免密, password: 密码
131 const phoneNumber = ref(''); 134 const phoneNumber = ref('');
@@ -133,51 +136,197 @@ @@ -133,51 +136,197 @@
133 const password = ref(''); 136 const password = ref('');
134 const isAgreed = ref(false); 137 const isAgreed = ref(false);
135 138
  139 + // 防抖及加载动画状态变量
  140 + const isSending = ref(false);
  141 + const isSubmitting = ref(false);
  142 +
136 // 验证码倒计时逻辑 143 // 验证码倒计时逻辑
137 const countdown = ref(0); 144 const countdown = ref(0);
138 - const handleGetCode = () => {  
139 - if (countdown.value > 0) return;  
140 - if (!phoneNumber.value || phoneNumber.value.length !== 11) {  
141 - uni.showToast({ title: '请输入正确的手机号码', icon: 'none' }); 145 + let timer = null;
  146 +
  147 + // 验证手机号码格式
  148 + const validatePhone = (phone) => {
  149 + return /^1[3-9]\d{9}$/.test(phone);
  150 + };
  151 +
  152 + // 切换Tab,同时清理另一个 Tab 的输入
  153 + const switchTab = (type) => {
  154 + activeTab.value = type;
  155 + verifyCode.value = '';
  156 + password.value = '';
  157 + };
  158 +
  159 + // 发送验证码
  160 + const handleGetCode = async () => {
  161 + if (countdown.value > 0 || isSending.value) return;
  162 +
  163 + // 前置逻辑校验
  164 + if (!phoneNumber.value) {
  165 + uni.showToast({ title: '请输入手机号码', icon: 'none' });
  166 + return;
  167 + }
  168 + if (!validatePhone(phoneNumber.value)) {
  169 + uni.showToast({ title: '请输入正确的11位手机号码', icon: 'none' });
142 return; 170 return;
143 } 171 }
144 172
145 - uni.showToast({ title: '验证码已发送', icon: 'success' });  
146 - countdown.value = 60;  
147 - const timer = setInterval(() => {  
148 - countdown.value--;  
149 - if (countdown.value <= 0) {  
150 - clearInterval(timer);  
151 - }  
152 - }, 1000); 173 + isSending.value = true;
  174 + try {
  175 + // 成功发送验证码请求
  176 + const res = await AuthUtil.sendSmsCode({ phone: phoneNumber.value });
  177 +
  178 + uni.showToast({ title: '验证码已发送', icon: 'success' });
  179 + countdown.value = 60;
  180 + timer = setInterval(() => {
  181 + countdown.value--;
  182 + if (countdown.value <= 0) {
  183 + clearInterval(timer);
  184 + timer = null;
  185 + }
  186 + }, 1000);
  187 + } catch (err) {
  188 + console.error('发送验证码失败异常:', err);
  189 + } finally {
  190 + isSending.value = false;
  191 + }
153 }; 192 };
154 193
155 - // 登录提交事件  
156 - const handleLogin = () => { 194 + // 登录按钮提交逻辑
  195 + const handleLogin = async () => {
  196 + if (isSubmitting.value) return;
  197 +
  198 + // 1. 用户协议验证
157 if (!isAgreed.value) { 199 if (!isAgreed.value) {
158 - uni.showToast({ title: '请先阅读并同意用户协议隐私政策', icon: 'none' }); 200 + uni.showToast({ title: '请先阅读并同意用户协议隐私政策', icon: 'none' });
159 return; 201 return;
160 } 202 }
  203 +
  204 + // 2. 基础手机号格式验证
161 if (!phoneNumber.value) { 205 if (!phoneNumber.value) {
162 uni.showToast({ title: '请输入手机号码', icon: 'none' }); 206 uni.showToast({ title: '请输入手机号码', icon: 'none' });
163 return; 207 return;
164 } 208 }
  209 + if (!validatePhone(phoneNumber.value)) {
  210 + uni.showToast({ title: '请输入正确的手机号码', icon: 'none' });
  211 + return;
  212 + }
165 213
166 - uni.showToast({ title: '登录成功', icon: 'success' }); 214 + // 3. Tab特定表单验证并请求
  215 + if (activeTab.value === 'sms') {
  216 + if (!verifyCode.value) {
  217 + uni.showToast({ title: '请输入验证码', icon: 'none' });
  218 + return;
  219 + }
  220 + if (verifyCode.value.length < 4) {
  221 + uni.showToast({ title: '请输入完整的验证码', icon: 'none' });
  222 + return;
  223 + }
  224 + await performLogin('sms');
  225 + } else {
  226 + if (!password.value) {
  227 + uni.showToast({ title: '请输入密码', icon: 'none' });
  228 + return;
  229 + }
  230 + await performLogin('password');
  231 + }
167 }; 232 };
168 233
169 - // 协议点击跳转 234 + // 执行最终请求与状态写入
  235 + const performLogin = async (type) => {
  236 + isSubmitting.value = true;
  237 + uni.showLoading({ title: '正在登录...', mask: true });
  238 +
  239 + try {
  240 + let res = null;
  241 + if (type === 'sms') {
  242 + // 修复:原代码使用的 AuthApi 变更为导入的 AuthUtil 服务
  243 + res = await AuthUtil.loginBySms({
  244 + phone: phoneNumber.value,
  245 + code: verifyCode.value,
  246 + });
  247 + } else {
  248 + res = await AuthUtil.loginByPhonePassword({
  249 + phone: phoneNumber.value,
  250 + password: password.value,
  251 + });
  252 + }
  253 +
  254 + const authData = res.data || {};
  255 +
  256 + if (authData && authData.accessToken) {
  257 + // 1. 取得芋道商城 Pinia 内置的 user Store
  258 + const userStore = sheep.$store('user');
  259 +
  260 + // 2. 写入 Token 到 Pinia 及持久态 LocalStorage
  261 + userStore.setToken(authData.accessToken, authData.refreshToken || '');
  262 +
  263 + // 3. 登录成功后,主动拉取一次用户信息同步至本地状态
  264 + await userStore.getInfo();
  265 +
  266 + uni.showToast({
  267 + title: '登录成功',
  268 + icon: 'success',
  269 + });
  270 +
  271 + // 4. 跳转至目标页面
  272 + setTimeout(() => {
  273 + uni.switchTab({
  274 + url: '/pages/xunji/xunji',
  275 + });
  276 + }, 1200);
  277 + } else {
  278 + uni.showToast({
  279 + title: res.msg || '登录失败,未获取到有效凭证',
  280 + icon: 'none',
  281 + });
  282 + }
  283 + } catch (err) {
  284 + console.error('登录逻辑异常:', err);
  285 + } finally {
  286 + isSubmitting.value = false;
  287 + uni.hideLoading();
  288 + }
  289 + };
  290 +
  291 + // 忘记密码跳转
  292 + const goToForgetPassword = () => {
  293 + uni.navigateTo({
  294 + url: '/pages7/pages/index/reset-password',
  295 + });
  296 + };
  297 +
  298 + // 协议详情跳转
170 const goToAgreement = (type) => { 299 const goToAgreement = (type) => {
171 - uni.showToast({  
172 - title: `跳转至${type === 'service' ? '用户协议' : '隐私政策'}`,  
173 - icon: 'none', 300 + const url =
  301 + type === 'service'
  302 + ? '/pages/public/richtext?id=service'
  303 + : '/pages/public/richtext?id=privacy';
  304 + uni.navigateTo({
  305 + url,
  306 + fail: () => {
  307 + uni.showToast({
  308 + title: `跳转至${type === 'service' ? '用户协议' : '隐私政策'}`,
  309 + icon: 'none',
  310 + });
  311 + },
174 }); 312 });
175 }; 313 };
  314 +
  315 + // 页面卸载生命周期:清除定时器,规避潜在的内存泄露
  316 + onHide(() => {
  317 + if (timer) {
  318 + clearInterval(timer);
  319 + timer = null;
  320 + }
  321 + });
176 </script> 322 </script>
177 323
178 <style lang="scss" scoped> 324 <style lang="scss" scoped>
179 .login-wrapper { 325 .login-wrapper {
180 - min-height: 100vh; 326 + height: 100vh;
  327 + // #ifdef H5
  328 + height: calc(100vh - 44px);
  329 + // #endif
181 background-color: #ffffff; 330 background-color: #ffffff;
182 padding: 80rpx 50rpx; 331 padding: 80rpx 50rpx;
183 box-sizing: border-box; 332 box-sizing: border-box;
@@ -202,7 +351,7 @@ @@ -202,7 +351,7 @@
202 align-items: center; 351 align-items: center;
203 margin-bottom: 30rpx; 352 margin-bottom: 30rpx;
204 353
205 - .logo-svg { 354 + .logo-img {
206 width: 76rpx; 355 width: 76rpx;
207 height: 76rpx; 356 height: 76rpx;
208 } 357 }
@@ -323,12 +472,20 @@ @@ -323,12 +472,20 @@
323 color: #b0b8c4; 472 color: #b0b8c4;
324 background-color: #f4f6fa; 473 background-color: #f4f6fa;
325 border-color: #e1e6eb; 474 border-color: #e1e6eb;
  475 + pointer-events: none; // 禁用点击以避免多余的触发
326 } 476 }
327 } 477 }
328 } 478 }
329 } 479 }
330 } 480 }
331 481
  482 + .forget-password {
  483 + text-align: right;
  484 + margin-top: 20rpx;
  485 + font-size: 24rpx;
  486 + color: rgb(220, 54, 46);
  487 + }
  488 +
332 // 4. Agreement 489 // 4. Agreement
333 .agreement-container { 490 .agreement-container {
334 display: flex; 491 display: flex;
@@ -389,6 +546,11 @@ @@ -389,6 +546,11 @@
389 opacity: 0.9; 546 opacity: 0.9;
390 } 547 }
391 548
  549 + &.disabled-btn {
  550 + opacity: 0.75;
  551 + pointer-events: none; // 阻止后续的多余点击
  552 + }
  553 +
392 .btn-arrow { 554 .btn-arrow {
393 margin-left: 10rpx; 555 margin-left: 10rpx;
394 } 556 }
  1 +<template>
  2 + <view class="login-wrapper">
  3 + <!-- 主表单区域:复用与登录页相同的 form-container 设计语言 -->
  4 + <view class="form-container">
  5 + <!-- 1. 手机号码 -->
  6 + <view class="form-item-wrapper">
  7 + <text class="form-label">手机号码</text>
  8 + <view class="input-box">
  9 + <u-icon name="phone" size="20" color="#a1a8b3" class="input-icon" />
  10 + <input
  11 + type="number"
  12 + v-model="phone"
  13 + placeholder="请输入您的手机号"
  14 + placeholder-style="color: #a1a8b3"
  15 + maxlength="11"
  16 + class="native-input"
  17 + />
  18 + </view>
  19 + </view>
  20 +
  21 + <!-- 2. 短信验证码:复用登录页免密模式的双列布局 -->
  22 + <view class="form-item-wrapper">
  23 + <text class="form-label">验证码</text>
  24 + <view class="code-row">
  25 + <view class="input-box flex-1">
  26 + <u-icon name="chat" size="20" color="#a1a8b3" class="input-icon" />
  27 + <input
  28 + type="number"
  29 + v-model="code"
  30 + placeholder="6位验证码"
  31 + placeholder-style="color: #a1a8b3"
  32 + maxlength="6"
  33 + class="native-input"
  34 + />
  35 + </view>
  36 + <!-- 获取验证码按钮 -->
  37 + <view
  38 + class="code-btn"
  39 + :class="{ disabled: countdown > 0 || isSending }"
  40 + @click="handleSendCode"
  41 + >
  42 + <text>{{ countdown > 0 ? `${countdown}s 后重试` : '获取验证码' }}</text>
  43 + </view>
  44 + </view>
  45 + </view>
  46 +
  47 + <!-- 3. 设置新密码 -->
  48 + <view class="form-item-wrapper">
  49 + <text class="form-label">设置新密码</text>
  50 + <view class="input-box">
  51 + <u-icon name="lock" size="20" color="#a1a8b3" class="input-icon" />
  52 + <input
  53 + :type="showPassword ? 'text' : 'password'"
  54 + v-model="password"
  55 + placeholder="请设置您的新密码"
  56 + placeholder-style="color: #a1a8b3"
  57 + class="native-input"
  58 + />
  59 + <u-icon
  60 + :name="showPassword ? 'eye-fill' : 'eye'"
  61 + size="20"
  62 + color="#a1a8b3"
  63 + class="eye-icon"
  64 + @click="showPassword = !showPassword"
  65 + />
  66 + </view>
  67 + <text class="input-hint">请设置8~16位包含数字、大小写字母、特殊字符组合作为密码</text>
  68 + </view>
  69 +
  70 + <!-- 4. 确认新密码 -->
  71 + <view class="form-item-wrapper">
  72 + <text class="form-label">确认密码</text>
  73 + <view class="input-box">
  74 + <u-icon name="lock" size="20" color="#a1a8b3" class="input-icon" />
  75 + <input
  76 + :type="showConfirmPassword ? 'text' : 'password'"
  77 + v-model="confirmPassword"
  78 + placeholder="请再次输入新密码"
  79 + placeholder-style="color: #a1a8b3"
  80 + class="native-input"
  81 + />
  82 + <u-icon
  83 + :name="showConfirmPassword ? 'eye-fill' : 'eye'"
  84 + size="20"
  85 + color="#a1a8b3"
  86 + class="eye-icon"
  87 + @click="showConfirmPassword = !showConfirmPassword"
  88 + />
  89 + </view>
  90 + </view>
  91 + </view>
  92 +
  93 + <!-- 5. 提交按钮:复用登录页高质感渐变色圆角按钮 -->
  94 + <view class="submit-btn" :class="{ 'disabled-btn': isSubmitting }" @click="handleSubmit">
  95 + <text>{{ isSubmitting ? '保存中...' : '确认修改' }}</text>
  96 + </view>
  97 + </view>
  98 +</template>
  99 +
  100 +<script setup>
  101 + import { ref } from 'vue';
  102 + import AuthUtil from '@/sheep/api/member/auth';
  103 + import { onHide } from '@dcloudio/uni-app';
  104 + const phone = ref('');
  105 + const code = ref('');
  106 + const password = ref('');
  107 + const confirmPassword = ref('');
  108 +
  109 + // 密码显示隐藏控制
  110 + const showPassword = ref(false);
  111 + const showConfirmPassword = ref(false);
  112 +
  113 + // 防抖及倒计时状态
  114 + const isSending = ref(false);
  115 + const isSubmitting = ref(false);
  116 + const countdown = ref(0);
  117 + let timer = null;
  118 +
  119 + // 验证手机号码格式
  120 + const validatePhone = (num) => {
  121 + return /^1[3-9]\d{9}$/.test(num);
  122 + };
  123 +
  124 + // 强密码复杂度检测:8~16位,大小写、数字、特殊字符
  125 + const validatePasswordStrength = (pwd) => {
  126 + const reg =
  127 + /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&._#^&+=*!~-])[A-Za-z\d@$!%*?&._#^&+=*!~-]{8,16}$/;
  128 + return reg.test(pwd);
  129 + };
  130 +
  131 + // 发送短信验证码
  132 + const handleSendCode = async () => {
  133 + if (countdown.value > 0 || isSending.value) return;
  134 +
  135 + if (!phone.value) {
  136 + uni.showToast({ title: '请输入手机号码', icon: 'none' });
  137 + return;
  138 + }
  139 + if (!validatePhone(phone.value)) {
  140 + uni.showToast({ title: '请输入正确的11位手机号码', icon: 'none' });
  141 + return;
  142 + }
  143 +
  144 + isSending.value = true;
  145 + try {
  146 + const res = await AuthUtil.sendSmsCode({ phone: phone.value });
  147 + if (res && (res.code === 0 || res.data === true)) {
  148 + uni.showToast({ title: '验证码已发送', icon: 'success' });
  149 + countdown.value = 60;
  150 + timer = setInterval(() => {
  151 + countdown.value--;
  152 + if (countdown.value <= 0) {
  153 + clearInterval(timer);
  154 + timer = null;
  155 + }
  156 + }, 1000);
  157 + } else {
  158 + uni.showToast({ title: res.msg || '获取验证码失败', icon: 'none' });
  159 + }
  160 + } catch (err) {
  161 + console.error('发送验证码失败异常:', err);
  162 + } finally {
  163 + isSending.value = false;
  164 + }
  165 + };
  166 +
  167 + // 保存并提交重置密码
  168 + const handleSubmit = async () => {
  169 + if (isSubmitting.value) return;
  170 +
  171 + if (!phone.value || !validatePhone(phone.value)) {
  172 + uni.showToast({ title: '请输入正确的11位手机号码', icon: 'none' });
  173 + return;
  174 + }
  175 + if (!code.value || code.value.length < 4) {
  176 + uni.showToast({ title: '请输入短信验证码', icon: 'none' });
  177 + return;
  178 + }
  179 + if (!password.value) {
  180 + uni.showToast({ title: '请输入您的新密码', icon: 'none' });
  181 + return;
  182 + }
  183 + // if (!validatePasswordStrength(password.value)) {
  184 + // uni.showToast({ title: '密码须为8~16位并包含大小写字母、数字及特殊字符', icon: 'none' });
  185 + // return;
  186 + // }
  187 + if (password.value !== confirmPassword.value) {
  188 + uni.showToast({ title: '两次输入的密码不一致', icon: 'none' });
  189 + return;
  190 + }
  191 +
  192 + isSubmitting.value = true;
  193 + uni.showLoading({ title: '正在修改密码...', mask: true });
  194 +
  195 + try {
  196 + const res = await AuthUtil.setPassword({
  197 + phone: phone.value,
  198 + code: code.value,
  199 + password: password.value,
  200 + });
  201 +
  202 + uni.redirectTo({
  203 + url: '/pages7/pages/index/login',
  204 + });
  205 + } catch (err) {
  206 + console.error('重置密码运行异常:', err);
  207 + } finally {
  208 + isSubmitting.value = false;
  209 + uni.hideLoading();
  210 + }
  211 + };
  212 +
  213 + // 生命周期管理:清除定时器
  214 + onHide(() => {
  215 + if (timer) {
  216 + clearInterval(timer);
  217 + timer = null;
  218 + }
  219 + });
  220 +</script>
  221 +
  222 +<style lang="scss" scoped>
  223 + .login-wrapper {
  224 + height: 100vh;
  225 + // #ifdef H5
  226 + height: calc(100vh - 44px);
  227 + // #endif
  228 + background-color: #ffffff;
  229 + padding: 40rpx 50rpx; // 移除顶部大 Logo 后,缩减顶部 Padding 保持呼吸感
  230 + box-sizing: border-box;
  231 +
  232 + // 1. 表单容器
  233 + .form-container {
  234 + margin-top: 20rpx;
  235 +
  236 + .form-item-wrapper {
  237 + margin-bottom: 36rpx;
  238 +
  239 + .form-label {
  240 + font-size: 26rpx;
  241 + color: #0a1931;
  242 + font-weight: bold;
  243 + display: block;
  244 + margin-bottom: 16rpx;
  245 + }
  246 +
  247 + // 输入框提示语
  248 + .input-hint {
  249 + display: block;
  250 + font-size: 24rpx;
  251 + color: #9097a3;
  252 + line-height: 1.5;
  253 + margin-top: 12rpx;
  254 + padding: 0 10rpx;
  255 + }
  256 +
  257 + .input-box {
  258 + height: 104rpx;
  259 + background-color: #f4f6fa;
  260 + border: 2rpx solid #e1e6eb;
  261 + border-radius: 24rpx;
  262 + display: flex;
  263 + align-items: center;
  264 + padding: 0 30rpx;
  265 + box-sizing: border-box;
  266 +
  267 + .input-icon {
  268 + margin-right: 16rpx;
  269 + }
  270 +
  271 + .eye-icon {
  272 + padding: 10rpx;
  273 + }
  274 +
  275 + .native-input {
  276 + flex: 1;
  277 + height: 100%;
  278 + font-size: 28rpx;
  279 + color: #0a1931;
  280 + }
  281 + }
  282 +
  283 + // 验证码双列布局
  284 + .code-row {
  285 + display: flex;
  286 + align-items: center;
  287 +
  288 + .flex-1 {
  289 + flex: 1;
  290 + }
  291 +
  292 + .code-btn {
  293 + width: 220rpx;
  294 + height: 104rpx;
  295 + border-radius: 24rpx;
  296 + background-color: #f9fdf2;
  297 + border: 2rpx solid #d0ee9c;
  298 + display: flex;
  299 + justify-content: center;
  300 + align-items: center;
  301 + margin-left: 20rpx;
  302 + font-size: 26rpx;
  303 + color: #8fc31f;
  304 + font-weight: bold;
  305 + transition: all 0.2s ease;
  306 +
  307 + &:active {
  308 + opacity: 0.8;
  309 + }
  310 +
  311 + &.disabled {
  312 + color: #b0b8c4;
  313 + background-color: #f4f6fa;
  314 + border-color: #e1e6eb;
  315 + pointer-events: none;
  316 + }
  317 + }
  318 + }
  319 + }
  320 + }
  321 +
  322 + // 2. 确认修改按钮 (登录页同款高级渐变微立体阴影)
  323 + .submit-btn {
  324 + height: 104rpx;
  325 + border-radius: 24rpx;
  326 + background: linear-gradient(135deg, #79d621 0%, #00b074 100%);
  327 + box-shadow: 0 16rpx 40rpx rgba(0, 176, 116, 0.25);
  328 + display: flex;
  329 + justify-content: center;
  330 + align-items: center;
  331 + color: #ffffff;
  332 + font-size: 32rpx;
  333 + font-weight: bold;
  334 + letter-spacing: 2rpx;
  335 + margin-top: 80rpx;
  336 + transition: all 0.2s ease;
  337 +
  338 + &:active {
  339 + transform: scale(0.98);
  340 + opacity: 0.9;
  341 + }
  342 +
  343 + &.disabled-btn {
  344 + opacity: 0.75;
  345 + pointer-events: none;
  346 + }
  347 +
  348 + .btn-arrow {
  349 + margin-left: 10rpx;
  350 + }
  351 + }
  352 + }
  353 +</style>
@@ -2,44 +2,37 @@ import request from '@/sheep/request'; @@ -2,44 +2,37 @@ import request from '@/sheep/request';
2 2
3 const AuthUtil = { 3 const AuthUtil = {
4 // 使用手机 + 密码登录 4 // 使用手机 + 密码登录
5 - login: (data) => { 5 + loginByPhonePassword: (data) => {
6 return request({ 6 return request({
7 - url: '/member/auth/login', 7 + url: '/app/auth/loginByPhonePassword',
8 method: 'POST', 8 method: 'POST',
9 data, 9 data,
10 custom: { 10 custom: {
11 showSuccess: true, 11 showSuccess: true,
12 - loadingMsg: '登录中',  
13 successMsg: '登录成功', 12 successMsg: '登录成功',
14 }, 13 },
15 }); 14 });
16 }, 15 },
17 // 使用手机 + 验证码登录 16 // 使用手机 + 验证码登录
18 - smsLogin: (data) => { 17 + loginBySms: (data) => {
19 return request({ 18 return request({
20 - url: '/member/auth/sms-login', 19 + url: '/app/auth/loginBySms',
21 method: 'POST', 20 method: 'POST',
22 data, 21 data,
23 custom: { 22 custom: {
24 showSuccess: true, 23 showSuccess: true,
25 - loadingMsg: '登录中',  
26 successMsg: '登录成功', 24 successMsg: '登录成功',
27 }, 25 },
28 }); 26 });
29 }, 27 },
30 // 发送手机验证码 28 // 发送手机验证码
31 - sendSmsCode: (mobile, scene) => { 29 + sendSmsCode: (data) => {
32 return request({ 30 return request({
33 - url: '/member/auth/send-sms-code', 31 + url: '/app/auth/sendSmsCode',
34 method: 'POST', 32 method: 'POST',
35 - data: {  
36 - mobile,  
37 - scene,  
38 - }, 33 + data,
39 custom: { 34 custom: {
40 - loadingMsg: '发送中',  
41 - showSuccess: true,  
42 - successMsg: '发送成功', 35 + showLoading: false,
43 }, 36 },
44 }); 37 });
45 }, 38 },
@@ -68,6 +61,16 @@ const AuthUtil = { @@ -68,6 +61,16 @@ const AuthUtil = {
68 }, 61 },
69 }); 62 });
70 }, 63 },
  64 + // 重置密码
  65 + setPassword: (data) => {
  66 + return request({
  67 + url: '/app/auth/setPassword',
  68 + method: 'POST',
  69 + data,
  70 +
  71 + });
  72 + },
  73 +
71 // 社交授权的跳转 74 // 社交授权的跳转
72 socialAuthRedirect: (type, redirectUri) => { 75 socialAuthRedirect: (type, redirectUri) => {
73 return request({ 76 return request({
@@ -261,7 +261,7 @@ const UserApi = { @@ -261,7 +261,7 @@ const UserApi = {
261 // 获取用户基本信息 261 // 获取用户基本信息
262 getUserBasicInfo: () => { 262 getUserBasicInfo: () => {
263 return request({ 263 return request({
264 - url: '/app/student/personData', 264 + url: '/app/user/getInfo',
265 method: 'GET', 265 method: 'GET',
266 custom: { 266 custom: {
267 showLoading: false, 267 showLoading: false,