Authored by liangjingyao

1:修复登录没有客户端问题

2:修复取消模板确认框在明细后面问题
3:修复开始训练打勾问题
@@ -8,7 +8,7 @@ SHOPRO_VERSION=v2.4.1 @@ -8,7 +8,7 @@ SHOPRO_VERSION=v2.4.1
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://xunji.geaktec.com 11 + #SHOPRO_DEV_BASE_URL=https://xunji.geaktec.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
@@ -29,15 +29,21 @@ @@ -29,15 +29,21 @@
29 </view> 29 </view>
30 30
31 <template v-if="resdailyData.length > 0"> 31 <template v-if="resdailyData.length > 0">
  32 + <!-- Tab 按钮:如���有多个训记,用来源名作为标签 -->
32 <scroll-view class="plan-tabs" scroll-x v-if="resdailyData.length > 1"> 33 <scroll-view class="plan-tabs" scroll-x v-if="resdailyData.length > 1">
33 <button class="tab-btn" :class="{ active: currentPlanIndex === index }" v-for="(item, index) in resdailyData" 34 <button class="tab-btn" :class="{ active: currentPlanIndex === index }" v-for="(item, index) in resdailyData"
34 :key="index" @click.stop="switchPlan(index)"> 35 :key="index" @click.stop="switchPlan(index)">
35 - 训记{{ index + 1 }} 36 + {{ item.sourceType === 1 || item.sourceType === 2 ? (item.sourceName || item.name) : (item.name || '训记' + (index + 1)) }}
36 </button> 37 </button>
37 </scroll-view> 38 </scroll-view>
38 - <!-- 接口获取当天的多个训练计划 → 放进 resdailyData,currentPlan 自动变成 resdailyData[0] 一个模板-->  
39 <!-- 训练内容区域 --> 39 <!-- 训练内容区域 -->
40 <view v-if="resdailyData.length > 0 && currentPlan" class="training-container"> 40 <view v-if="resdailyData.length > 0 && currentPlan" class="training-container">
  41 + <!-- 训练来源标签 -->
  42 + <view class="source-tag-row" :class="getSourceClass(currentPlan.sourceType || 4)">
  43 + <text class="source-text">
  44 + {{ currentPlan.sourceType === 1 || currentPlan.sourceType === 2 ? (currentPlan.sourceName || currentPlan.name) : getSourceLabel(currentPlan.sourceType || 4) }}
  45 + </text>
  46 + </view>
41 <view class="unitCart"> 47 <view class="unitCart">
42 <!-- 训练计划头部卡片 --> 48 <!-- 训练计划头部卡片 -->
43 <view class="plan-header-card"> 49 <view class="plan-header-card">
@@ -416,6 +422,67 @@ const currentPlan = computed(() => { @@ -416,6 +422,67 @@ const currentPlan = computed(() => {
416 return resdailyData.value[currentPlanIndex.value] || null; 422 return resdailyData.value[currentPlanIndex.value] || null;
417 }); 423 });
418 424
  425 +// ========= 训练来源分组展示 =========
  426 +
  427 +/** sourceType → 中文标签 */
  428 +const getSourceLabel = (type) => {
  429 + const labels = {
  430 + 1: '来自计划',
  431 + 2: '来自模板',
  432 + 3: '自定义训练',
  433 + 4: '自由训练',
  434 + };
  435 + return labels[type] || '训练';
  436 +};
  437 +
  438 +/** sourceType → 风格 class */
  439 +const getSourceClass = (type) => {
  440 + const classes = {
  441 + 1: 'source-plan', // 计划
  442 + 2: 'source-tpl', // 模板
  443 + 3: 'source-custom', // 自定义
  444 + 4: 'source-free', // 自由
  445 + };
  446 + return classes[type] || 'source-free';
  447 +};
  448 +
  449 +/** 按 sourceType 分组后的模板列表,供 tab 切换使用 */
  450 +const sourceGrouped = computed(() => {
  451 + const list = resdailyData.value || [];
  452 + const map = new Map();
  453 + list.forEach((item) => {
  454 + const key = item.sourceType ? `s${item.sourceType}_${item.sourceName || ''}` : `t_${item.dailyTemplateId}`;
  455 + if (!map.has(key)) {
  456 + const st = item.sourceType || 4;
  457 + map.set(key, {
  458 + sourceType: st,
  459 + sourceName: item.sourceName || item.name,
  460 + sourceLabel: item.sourceType === 1 || item.sourceType === 2 ? item.sourceName : getSourceLabel(st),
  461 + items: [],
  462 + });
  463 + }
  464 + map.get(key).items.push(item);
  465 + });
  466 + return Array.from(map.values());
  467 +});
  468 +
  469 +/** 扁平化的模板列表(带 source 信息),用于直接渲染 */
  470 +const flatPlanList = computed(() => {
  471 + const result = [];
  472 + sourceGrouped.value.forEach((group) => {
  473 + group.items.forEach((item, idx) => {
  474 + result.push({
  475 + ...item,
  476 + sourceType: group.sourceType,
  477 + sourceName: group.sourceName,
  478 + sourceLabel: group.sourceLabel,
  479 + isFirstInGroup: idx === 0,
  480 + });
  481 + });
  482 + });
  483 + return result;
  484 +});
  485 +
419 const loaddailytemplate = async () => { 486 const loaddailytemplate = async () => {
420 if (!selectedDate.value) return; 487 if (!selectedDate.value) return;
421 console.log('【子组件】开始加载数据...'); 488 console.log('【子组件】开始加载数据...');
@@ -980,6 +1047,34 @@ onMounted(() => { @@ -980,6 +1047,34 @@ onMounted(() => {
980 font-weight: bold; 1047 font-weight: bold;
981 } 1048 }
982 1049
  1050 +/* 训练来源标签 */
  1051 +.source-tag-row {
  1052 + display: flex;
  1053 + align-items: center;
  1054 + padding: 16rpx 24rpx;
  1055 + margin: 0rpx 20rpx;
  1056 + border-radius: 12rpx;
  1057 + font-size: 24rpx;
  1058 + font-weight: 500;
  1059 +
  1060 + .source-text {
  1061 + color: #fff;
  1062 + }
  1063 +
  1064 + &.source-plan {
  1065 + background: #3b82f6; /* 蓝色 - 计划 */
  1066 + }
  1067 + &.source-tpl {
  1068 + background: #8b5cf6; /* 紫色 - 模板 */
  1069 + }
  1070 + &.source-custom {
  1071 + background: #f59e0b; /* 橙色 - 自定义 */
  1072 + }
  1073 + &.source-free {
  1074 + background: #10b981; /* 绿色 - 自由 */
  1075 + }
  1076 +}
  1077 +
983 /* 训练计划头部卡片 */ 1078 /* 训练计划头部卡片 */
984 .plan-header-card { 1079 .plan-header-card {
985 display: flex; 1080 display: flex;
@@ -247,17 +247,32 @@ const loadPlans = async (monthStr) => { @@ -247,17 +247,32 @@ const loadPlans = async (monthStr) => {
247 planMap[dateStr] = planMap[dateStr] || []; 247 planMap[dateStr] = planMap[dateStr] || [];
248 if (Array.isArray(plan.templates)) { 248 if (Array.isArray(plan.templates)) {
249 let totalWeightForDay = 0; 249 let totalWeightForDay = 0;
  250 + // 按 sourceType + sourceName 分组
  251 + const sourceMap = new Map();
250 plan.templates.forEach((template) => { 252 plan.templates.forEach((template) => {
251 - if (template && template.name) {  
252 - planMap[dateStr].push({  
253 - name: template.name, 253 + if (!template || !template.name) return;
  254 + const sourceKey = template.sourceType
  255 + ? `src_${template.sourceType}_${template.sourceName || ''}`
  256 + : `tpl_${template.dailyTemplateId}`;
  257 + if (!sourceMap.has(sourceKey)) {
  258 + sourceMap.set(sourceKey, {
  259 + name: template.sourceName || template.name,
254 bg: template.backgroundColor || '#fef08a', 260 bg: template.backgroundColor || '#fef08a',
  261 + sourceType: template.sourceType || 4,
  262 + sourceName: template.sourceName || template.name,
  263 + dailyTemplateId: template.dailyTemplateId,
  264 + totalWeight: 0,
255 }); 265 });
256 } 266 }
  267 + const group = sourceMap.get(sourceKey);
257 if (template && typeof template.totalWeight === 'number') { 268 if (template && typeof template.totalWeight === 'number') {
  269 + group.totalWeight += template.totalWeight;
258 totalWeightForDay += template.totalWeight; 270 totalWeightForDay += template.totalWeight;
259 } 271 }
260 }); 272 });
  273 + sourceMap.forEach((group) => {
  274 + planMap[dateStr].push(group);
  275 + });
261 if (totalWeightForDay > 0) { 276 if (totalWeightForDay > 0) {
262 weightMap[dateStr] = totalWeightForDay; 277 weightMap[dateStr] = totalWeightForDay;
263 } 278 }
@@ -346,14 +346,14 @@ const chartSeries = computed(() => { @@ -346,14 +346,14 @@ const chartSeries = computed(() => {
346 const dataKey = TREND_DATA_KEYS[NumberTabIndex.value] || 'volume'; 346 const dataKey = TREND_DATA_KEYS[NumberTabIndex.value] || 'volume';
347 347
348 // 当前周期数据 348 // 当前周期数据
349 - const currData = fullDateKeys.map((dateStr) => currDataMap[dateStr]?.[dataKey] ?? 0); 349 + const currData = fullDateKeys.map((dateStr) => convertUnit(currDataMap[dateStr]?.[dataKey], dataKey));
350 350
351 // 上一周期数据(按日期偏移匹配) 351 // 上一周期数据(按日期偏移匹配)
352 const lastData = fullDateKeys.map((dateStr) => { 352 const lastData = fullDateKeys.map((dateStr) => {
353 const targetKey = isWeekly 353 const targetKey = isWeekly
354 ? dayjs(dateStr).subtract(7, 'day').format('YYYY-MM-DD') 354 ? dayjs(dateStr).subtract(7, 'day').format('YYYY-MM-DD')
355 : dayjs(dateStr).subtract(1, 'month').format('YYYY-MM-DD'); 355 : dayjs(dateStr).subtract(1, 'month').format('YYYY-MM-DD');
356 - return lastDataMap[targetKey]?.[dataKey] ?? 0; 356 + return convertUnit(lastDataMap[targetKey]?.[dataKey], dataKey);
357 }); 357 });
358 358
359 const currName = isWeekly ? '本周' : '本月'; 359 const currName = isWeekly ? '本周' : '本月';
@@ -373,25 +373,39 @@ const listData = computed(() => { @@ -373,25 +373,39 @@ const listData = computed(() => {
373 373
374 const d = weeklyData.value; 374 const d = weeklyData.value;
375 const fields = [ 375 const fields = [
376 - { label: '容量', last: d.lastWeekTotalVolume, curr: d.totalVolume },  
377 - { label: '组数', last: d.lastWeekTotalSets, curr: d.totalSets },  
378 - { label: '时长', last: d.lastWeekTotalDuration, curr: d.totalDuration },  
379 - { label: '距离', last: d.lastWeekTotalDistance, curr: d.totalDistance },  
380 - { label: '次数', last: d.lastWeekTotalCount, curr: d.totalCount }, 376 + { label: '容量', key: 'volume', last: d.lastWeekTotalVolume, curr: d.totalVolume },
  377 + { label: '组数', key: 'setCount', last: d.lastWeekTotalSets, curr: d.totalSets },
  378 + { label: '时长', key: 'duration', last: d.lastWeekTotalDuration, curr: d.totalDuration },
  379 + { label: '距离', key: 'distance', last: d.lastWeekTotalDistance, curr: d.totalDistance },
  380 + { label: '次数', key: 'count', last: d.lastWeekTotalCount, curr: d.totalCount },
381 ]; 381 ];
382 382
383 - return fields.map(({ label, last, curr }) => {  
384 - const lastVal = last ?? 0;  
385 - const currVal = curr ?? 0; 383 + return fields.map(({ label, key, last, curr }) => {
  384 + const lastVal = convertUnit(last, key);
  385 + const currVal = convertUnit(curr, key);
386 return { 386 return {
387 label, 387 label,
388 lastPeriod: lastVal, 388 lastPeriod: lastVal,
389 currPeriod: currVal, 389 currPeriod: currVal,
390 - diff: currVal - lastVal, 390 + diff: Number((currVal - lastVal).toFixed(1)),
391 }; 391 };
392 }); 392 });
393 }); 393 });
394 394
  395 +/**
  396 + * 字段单位换算:API 返回秒/米等,UI 展示为分/km 等
  397 + * duration(秒)→ 分钟
  398 + * 其它字段(volume/setCount/distance/count)保持原值
  399 + */
  400 +const convertUnit = (val, key) => {
  401 + const v = Number(val) || 0;
  402 + if (key === 'duration') {
  403 + // 秒 → 分钟,保留 1 位小数(如 90s → 1.5)
  404 + return Math.round((v / 60) * 10) / 10;
  405 + }
  406 + return v;
  407 +};
  408 +
395 /** 肌肉弹窗图表 X 轴 */ 409 /** 肌肉弹窗图表 X 轴 */
396 const muscleChartCategories = computed(() => { 410 const muscleChartCategories = computed(() => {
397 if (MainCurrentDateType.value === 'week') { 411 if (MainCurrentDateType.value === 'week') {
@@ -233,29 +233,37 @@ const handleDeleteTemplate = async () => { @@ -233,29 +233,37 @@ const handleDeleteTemplate = async () => {
233 return; 233 return;
234 } 234 }
235 235
236 - uni.showModal({  
237 - title: '确认删除',  
238 - content: '确定要删除这个模板吗?删除后无法恢复',  
239 - confirmColor: '#ff4444',  
240 - success: async (res) => {  
241 - if (res.confirm) {  
242 - try {  
243 - uni.showLoading({ title: '删除中...' });  
244 - await TemplatesApi.deleteCustTemplate(currentTemplate.value.id);  
245 - uni.hideLoading();  
246 - uni.showToast({ title: '删除成功', icon: 'success' });  
247 - closeTemplateMenu();  
248 - // 通知父组件刷新列表  
249 - emit('refresh');  
250 -  
251 - } catch (err) {  
252 - uni.hideLoading();  
253 - console.error('删除失败:', err);  
254 - uni.showToast({ title: '删除失败', icon: 'none' }); 236 + // 在 showModal 前把 id 存到局部变量,防止异步回调执行时 currentTemplate.value 已被清空
  237 + const templateId = currentTemplate.value.id;
  238 +
  239 + // 必须先关闭 WebView 弹窗(up-popup),否则它会盖在 uni.showModal 原生弹窗上面
  240 + closeTemplateMenu();
  241 +
  242 + // 延时一帧再弹原生确认框,避免与 closeTemplateMenu 的卸载动画在视觉上打架
  243 + setTimeout(() => {
  244 + uni.showModal({
  245 + title: '确认删除',
  246 + content: '确定要删除这个模板吗?删除后无法恢复',
  247 + confirmColor: '#ff4444',
  248 + success: async (res) => {
  249 + if (res.confirm) {
  250 + try {
  251 + uni.showLoading({ title: '删除中...' });
  252 + await TemplatesApi.deleteCustTemplate(templateId);
  253 + uni.hideLoading();
  254 + uni.showToast({ title: '删除成功', icon: 'success' });
  255 + // 通知父组件刷新列表
  256 + emit('refresh');
  257 +
  258 + } catch (err) {
  259 + uni.hideLoading();
  260 + console.error('删除失败:', err);
  261 + uni.showToast({ title: '删除失败', icon: 'none' });
  262 + }
255 } 263 }
256 } 264 }
257 - }  
258 - }); 265 + });
  266 + }, 200);
259 }; 267 };
260 268
261 269
@@ -8,7 +8,7 @@ @@ -8,7 +8,7 @@
8 <text class="time">{{ formattedTime.substring(3) }}</text> 8 <text class="time">{{ formattedTime.substring(3) }}</text>
9 <view class="triangle-icon"></view> 9 <view class="triangle-icon"></view>
10 </view> 10 </view>
11 - <view class="finish-btn" @click="save">完成</view> 11 + <view class="finish-btn" :class="{ disabled: !allUnitsChecked }" @click="save">完成</view>
12 </view> 12 </view>
13 13
14 <view class="action-list"> 14 <view class="action-list">
@@ -431,6 +431,14 @@ @@ -431,6 +431,14 @@
431 unitActiveStates.value = [...unitActiveStates.value] 431 unitActiveStates.value = [...unitActiveStates.value]
432 } 432 }
433 433
  434 + // 全部 unit 已勾选后才能提交
  435 + const allUnitsChecked = computed(() => {
  436 + const list = actionDetail.value?.units || []
  437 + if (list.length === 0) return false
  438 + if (unitActiveStates.value.length !== list.length) return false
  439 + return unitActiveStates.value.every((v) => v === true)
  440 + })
  441 +
434 const openSummaryPopup = () => { 442 const openSummaryPopup = () => {
435 summaryShow.value = true; 443 summaryShow.value = true;
436 summaryContent.value = ''; 444 summaryContent.value = '';
@@ -507,6 +515,17 @@ @@ -507,6 +515,17 @@
507 }); 515 });
508 516
509 const save = async () => { 517 const save = async () => {
  518 + // 前置校验:所有 unit 必须已勾选(顶部 ✓)才能提交
  519 + if (!allUnitsChecked.value) {
  520 + uni.showModal({
  521 + title: '提示',
  522 + content: '还有训练没有完成,请先勾选所有动作后再提交',
  523 + showCancel: false,
  524 + confirmText: '知道了',
  525 + });
  526 + return;
  527 + }
  528 +
510 await nextTick(); 529 await nextTick();
511 const units = []; 530 const units = [];
512 const unitList = actionDetail.value?.units || []; 531 const unitList = actionDetail.value?.units || [];
@@ -552,16 +571,17 @@ @@ -552,16 +571,17 @@
552 571
553 try { 572 try {
554 if (trainingStore.isTraining) { 573 if (trainingStore.isTraining) {
555 - await TrainingApi.createTrainHistory({ units }); 574 + await TrainingApi.createTrainHistory({ units, allCompleted: allUnitsChecked.value });
556 uni.showToast({ title: '训练提交成功', icon: 'success' }); 575 uni.showToast({ title: '训练提交成功', icon: 'success' });
557 } else if (trainingStore.type === 3 && trainingStore.dailyTemplateId) { 576 } else if (trainingStore.type === 3 && trainingStore.dailyTemplateId) {
558 await dailytemplateApi.updateDailyTemplate({ 577 await dailytemplateApi.updateDailyTemplate({
559 dailyTemplateId: trainingStore.dailyTemplateId, 578 dailyTemplateId: trainingStore.dailyTemplateId,
560 units, 579 units,
  580 + allCompleted: allUnitsChecked.value,
561 }); 581 });
562 uni.showToast({ title: '模板修改成功', icon: 'success' }); 582 uni.showToast({ title: '模板修改成功', icon: 'success' });
563 } else { 583 } else {
564 - await TrainingApi.createTrainHistory({ units }); 584 + await TrainingApi.createTrainHistory({ units, allCompleted: allUnitsChecked.value });
565 uni.showToast({ title: '训练提交成功', icon: 'success' }); 585 uni.showToast({ title: '训练提交成功', icon: 'success' });
566 } 586 }
567 setTimeout(() => { 587 setTimeout(() => {
@@ -681,6 +701,14 @@ @@ -681,6 +701,14 @@
681 border-radius: 30rpx; 701 border-radius: 30rpx;
682 font-weight: bold; 702 font-weight: bold;
683 font-size: 28rpx; 703 font-size: 28rpx;
  704 + transition: opacity 0.2s;
  705 +
  706 + &.disabled {
  707 + background-color: #555;
  708 + color: #999;
  709 + opacity: 0.6;
  710 + pointer-events: none;
  711 + }
684 } 712 }
685 } 713 }
686 714
@@ -8,6 +8,7 @@ const AuthUtil = { @@ -8,6 +8,7 @@ const AuthUtil = {
8 method: 'POST', 8 method: 'POST',
9 data, 9 data,
10 custom: { 10 custom: {
  11 + isToken: false,
11 showSuccess: true, 12 showSuccess: true,
12 successMsg: '登录成功', 13 successMsg: '登录成功',
13 }, 14 },
@@ -20,6 +21,7 @@ const AuthUtil = { @@ -20,6 +21,7 @@ const AuthUtil = {
20 method: 'POST', 21 method: 'POST',
21 data, 22 data,
22 custom: { 23 custom: {
  24 + isToken: false,
23 showSuccess: true, 25 showSuccess: true,
24 successMsg: '登录成功', 26 successMsg: '登录成功',
25 }, 27 },
@@ -32,6 +34,7 @@ const AuthUtil = { @@ -32,6 +34,7 @@ const AuthUtil = {
32 method: 'POST', 34 method: 'POST',
33 data, 35 data,
34 custom: { 36 custom: {
  37 + isToken: false,
35 showLoading: false, 38 showLoading: false,
36 }, 39 },
37 }); 40 });
@@ -67,7 +70,9 @@ const AuthUtil = { @@ -67,7 +70,9 @@ const AuthUtil = {
67 url: '/app/auth/setPassword', 70 url: '/app/auth/setPassword',
68 method: 'POST', 71 method: 'POST',
69 data, 72 data,
70 - 73 + custom: {
  74 + isToken: false,
  75 + },
71 }); 76 });
72 }, 77 },
73 78
@@ -113,6 +118,7 @@ const AuthUtil = { @@ -113,6 +118,7 @@ const AuthUtil = {
113 role: 1, 118 role: 1,
114 }, 119 },
115 custom: { 120 custom: {
  121 + isToken: false,
116 showSuccess: true, 122 showSuccess: true,
117 loadingMsg: '登录中', 123 loadingMsg: '登录中',
118 successMsg: '登录成功', 124 successMsg: '登录成功',
@@ -128,6 +134,7 @@ const AuthUtil = { @@ -128,6 +134,7 @@ const AuthUtil = {
128 code, 134 code,
129 }, 135 },
130 custom: { 136 custom: {
  137 + isToken: false,
131 showSuccess: true, 138 showSuccess: true,
132 loadingMsg: '登录中', 139 loadingMsg: '登录中',
133 successMsg: '登录成功', 140 successMsg: '登录成功',
@@ -422,6 +422,14 @@ export const useTrainingStore = defineStore('training', { @@ -422,6 +422,14 @@ export const useTrainingStore = defineStore('training', {
422 this.defaultTimeIndex = [0, 0, 0]; 422 this.defaultTimeIndex = [0, 0, 0];
423 this.showPicker = false; 423 this.showPicker = false;
424 this.isTraining = false; 424 this.isTraining = false;
  425 +
  426 + // 绕过 persist 插件的异步 watch,直接同步写入 storage
  427 + // 防止 navigateBack 后 persist flush 赶不及,导致 isTraining 残留为 true
  428 + try {
  429 + uni.setStorageSync('training', JSON.stringify(this.$state));
  430 + } catch (e) {
  431 + // 静默处理,不影响主流程
  432 + }
425 }, 433 },
426 }, 434 },
427 435