feat(xunji): 优化训练来源展示与提交流程
Showing
7 changed files
with
119 additions
and
13 deletions
| @@ -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 |
| @@ -27,15 +27,21 @@ | @@ -27,15 +27,21 @@ | ||
| 27 | </view> | 27 | </view> |
| 28 | 28 | ||
| 29 | <template v-if="resdailyData.length > 0"> | 29 | <template v-if="resdailyData.length > 0"> |
| 30 | + <!-- Tab 按钮:如���有多个训记,用来源名作为标签 --> | ||
| 30 | <scroll-view class="plan-tabs" scroll-x v-if="resdailyData.length > 1"> | 31 | <scroll-view class="plan-tabs" scroll-x v-if="resdailyData.length > 1"> |
| 31 | <button class="tab-btn" :class="{ active: currentPlanIndex === index }" v-for="(item, index) in resdailyData" | 32 | <button class="tab-btn" :class="{ active: currentPlanIndex === index }" v-for="(item, index) in resdailyData" |
| 32 | :key="index" @click.stop="switchPlan(index)"> | 33 | :key="index" @click.stop="switchPlan(index)"> |
| 33 | - 训记{{ index + 1 }} | 34 | + {{ item.sourceType === 1 || item.sourceType === 2 ? (item.sourceName || item.name) : (item.name || '训记' + (index + 1)) }} |
| 34 | </button> | 35 | </button> |
| 35 | </scroll-view> | 36 | </scroll-view> |
| 36 | - <!-- 接口获取当天的多个训练计划 → 放进 resdailyData,currentPlan 自动变成 resdailyData[0] 一个模板--> | ||
| 37 | <!-- 训练内容区域 --> | 37 | <!-- 训练内容区域 --> |
| 38 | <view v-if="resdailyData.length > 0 && currentPlan" class="training-container"> | 38 | <view v-if="resdailyData.length > 0 && currentPlan" class="training-container"> |
| 39 | + <!-- 训练来源标签 --> | ||
| 40 | + <view class="source-tag-row" :class="getSourceClass(currentPlan.sourceType || 4)"> | ||
| 41 | + <text class="source-text"> | ||
| 42 | + {{ currentPlan.sourceType === 1 || currentPlan.sourceType === 2 ? (currentPlan.sourceName || currentPlan.name) : getSourceLabel(currentPlan.sourceType || 4) }} | ||
| 43 | + </text> | ||
| 44 | + </view> | ||
| 39 | <view class="unitCart"> | 45 | <view class="unitCart"> |
| 40 | <!-- 训练计划头部卡片 --> | 46 | <!-- 训练计划头部卡片 --> |
| 41 | <view class="plan-header-card"> | 47 | <view class="plan-header-card"> |
| @@ -959,6 +965,34 @@ $radius-round: 40rpx; | @@ -959,6 +965,34 @@ $radius-round: 40rpx; | ||
| 959 | font-weight: bold; | 965 | font-weight: bold; |
| 960 | } | 966 | } |
| 961 | 967 | ||
| 968 | +/* 训练来源标签 */ | ||
| 969 | +.source-tag-row { | ||
| 970 | + display: flex; | ||
| 971 | + align-items: center; | ||
| 972 | + padding: 16rpx 24rpx; | ||
| 973 | + margin: 0rpx 20rpx; | ||
| 974 | + border-radius: 12rpx; | ||
| 975 | + font-size: 24rpx; | ||
| 976 | + font-weight: 500; | ||
| 977 | + | ||
| 978 | + .source-text { | ||
| 979 | + color: #fff; | ||
| 980 | + } | ||
| 981 | + | ||
| 982 | + &.source-plan { | ||
| 983 | + background: #3b82f6; /* 蓝色 - 计划 */ | ||
| 984 | + } | ||
| 985 | + &.source-tpl { | ||
| 986 | + background: #8b5cf6; /* 紫色 - 模板 */ | ||
| 987 | + } | ||
| 988 | + &.source-custom { | ||
| 989 | + background: #f59e0b; /* 橙色 - 自定义 */ | ||
| 990 | + } | ||
| 991 | + &.source-free { | ||
| 992 | + background: #10b981; /* 绿色 - 自由 */ | ||
| 993 | + } | ||
| 994 | +} | ||
| 995 | + | ||
| 962 | /* 训练计划头部卡片 */ | 996 | /* 训练计划头部卡片 */ |
| 963 | .plan-header-card { | 997 | .plan-header-card { |
| 964 | display: flex; | 998 | 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 | } |
| @@ -392,9 +392,9 @@ const listData = computed(() => { | @@ -392,9 +392,9 @@ const listData = computed(() => { | ||
| 392 | { label: '次数', last: d.lastWeekTotalCount, curr: d.totalCount }, | 392 | { label: '次数', last: d.lastWeekTotalCount, curr: d.totalCount }, |
| 393 | ]; | 393 | ]; |
| 394 | 394 | ||
| 395 | - return fields.map(({ label, last, curr }) => { | ||
| 396 | - const lastVal = last ?? 0; | ||
| 397 | - const currVal = curr ?? 0; | 395 | + return fields.map(({ label, key, last, curr }) => { |
| 396 | + const lastVal = convertUnit(last, key); | ||
| 397 | + const currVal = convertUnit(curr, key); | ||
| 398 | return { | 398 | return { |
| 399 | label, | 399 | label, |
| 400 | lastPeriod: lastVal, | 400 | lastPeriod: lastVal, |
| @@ -404,6 +404,20 @@ const listData = computed(() => { | @@ -404,6 +404,20 @@ const listData = computed(() => { | ||
| 404 | }); | 404 | }); |
| 405 | }); | 405 | }); |
| 406 | 406 | ||
| 407 | +/** | ||
| 408 | + * 字段单位换算:API 返回秒/米等,UI 展示为分/km 等 | ||
| 409 | + * duration(秒)→ 分钟 | ||
| 410 | + * 其它字段(volume/setCount/distance/count)保持原值 | ||
| 411 | + */ | ||
| 412 | +const convertUnit = (val, key) => { | ||
| 413 | + const v = Number(val) || 0; | ||
| 414 | + if (key === 'duration') { | ||
| 415 | + // 秒 → 分钟,保留 1 位小数(如 90s → 1.5) | ||
| 416 | + return Math.round((v / 60) * 10) / 10; | ||
| 417 | + } | ||
| 418 | + return v; | ||
| 419 | +}; | ||
| 420 | + | ||
| 407 | /** 肌肉弹窗图表 X 轴 */ | 421 | /** 肌肉弹窗图表 X 轴 */ |
| 408 | const muscleChartCategories = computed(() => { | 422 | const muscleChartCategories = computed(() => { |
| 409 | if (MainCurrentDateType.value === 'week') { | 423 | if (MainCurrentDateType.value === 'week') { |
| @@ -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"> |
| @@ -415,6 +415,14 @@ | @@ -415,6 +415,14 @@ | ||
| 415 | unitActiveStates.value = [...unitActiveStates.value] | 415 | unitActiveStates.value = [...unitActiveStates.value] |
| 416 | } | 416 | } |
| 417 | 417 | ||
| 418 | + // 全部 unit 已勾选后才能提交 | ||
| 419 | + const allUnitsChecked = computed(() => { | ||
| 420 | + const list = actionDetail.value?.units || [] | ||
| 421 | + if (list.length === 0) return false | ||
| 422 | + if (unitActiveStates.value.length !== list.length) return false | ||
| 423 | + return unitActiveStates.value.every((v) => v === true) | ||
| 424 | + }) | ||
| 425 | + | ||
| 418 | const openSummaryPopup = () => { | 426 | const openSummaryPopup = () => { |
| 419 | summaryShow.value = true; | 427 | summaryShow.value = true; |
| 420 | summaryContent.value = ''; | 428 | summaryContent.value = ''; |
| @@ -491,6 +499,17 @@ | @@ -491,6 +499,17 @@ | ||
| 491 | }); | 499 | }); |
| 492 | 500 | ||
| 493 | const save = async () => { | 501 | const save = async () => { |
| 502 | + // 前置校验:所有 unit 必须已勾选(顶部 ✓)才能提交 | ||
| 503 | + if (!allUnitsChecked.value) { | ||
| 504 | + uni.showModal({ | ||
| 505 | + title: '提示', | ||
| 506 | + content: '还有训练没有完成,请先勾选所有动作后再提交', | ||
| 507 | + showCancel: false, | ||
| 508 | + confirmText: '知道了', | ||
| 509 | + }); | ||
| 510 | + return; | ||
| 511 | + } | ||
| 512 | + | ||
| 494 | await nextTick(); | 513 | await nextTick(); |
| 495 | const units = []; | 514 | const units = []; |
| 496 | const unitList = actionDetail.value?.units || []; | 515 | const unitList = actionDetail.value?.units || []; |
| @@ -553,16 +572,17 @@ | @@ -553,16 +572,17 @@ | ||
| 553 | 572 | ||
| 554 | try { | 573 | try { |
| 555 | if (trainingStore.isTraining) { | 574 | if (trainingStore.isTraining) { |
| 556 | - await TrainingApi.createTrainHistory({ units }); | 575 | + await TrainingApi.createTrainHistory({ units, allCompleted: allUnitsChecked.value }); |
| 557 | uni.showToast({ title: '训练提交成功', icon: 'success' }); | 576 | uni.showToast({ title: '训练提交成功', icon: 'success' }); |
| 558 | } else if (trainingStore.type === 3 && trainingStore.dailyTemplateId) { | 577 | } else if (trainingStore.type === 3 && trainingStore.dailyTemplateId) { |
| 559 | await dailytemplateApi.updateDailyTemplate({ | 578 | await dailytemplateApi.updateDailyTemplate({ |
| 560 | dailyTemplateId: trainingStore.dailyTemplateId, | 579 | dailyTemplateId: trainingStore.dailyTemplateId, |
| 561 | units, | 580 | units, |
| 581 | + allCompleted: allUnitsChecked.value, | ||
| 562 | }); | 582 | }); |
| 563 | uni.showToast({ title: '模板修改成功', icon: 'success' }); | 583 | uni.showToast({ title: '模板修改成功', icon: 'success' }); |
| 564 | } else { | 584 | } else { |
| 565 | - await TrainingApi.createTrainHistory({ units }); | 585 | + await TrainingApi.createTrainHistory({ units, allCompleted: allUnitsChecked.value }); |
| 566 | uni.showToast({ title: '训练提交成功', icon: 'success' }); | 586 | uni.showToast({ title: '训练提交成功', icon: 'success' }); |
| 567 | } | 587 | } |
| 568 | setTimeout(() => { | 588 | setTimeout(() => { |
| @@ -682,6 +702,14 @@ | @@ -682,6 +702,14 @@ | ||
| 682 | border-radius: 30rpx; | 702 | border-radius: 30rpx; |
| 683 | font-weight: bold; | 703 | font-weight: bold; |
| 684 | font-size: 28rpx; | 704 | font-size: 28rpx; |
| 705 | + transition: opacity 0.2s; | ||
| 706 | + | ||
| 707 | + &.disabled { | ||
| 708 | + background-color: #555; | ||
| 709 | + color: #999; | ||
| 710 | + opacity: 0.6; | ||
| 711 | + pointer-events: none; | ||
| 712 | + } | ||
| 685 | } | 713 | } |
| 686 | } | 714 | } |
| 687 | 715 |
| @@ -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: '登录成功', |
| @@ -400,6 +400,14 @@ export const useTrainingStore = defineStore('training', { | @@ -400,6 +400,14 @@ export const useTrainingStore = defineStore('training', { | ||
| 400 | this.defaultTimeIndex = [0, 0, 0]; | 400 | this.defaultTimeIndex = [0, 0, 0]; |
| 401 | this.showPicker = false; | 401 | this.showPicker = false; |
| 402 | this.isTraining = false; | 402 | this.isTraining = false; |
| 403 | + | ||
| 404 | + // 绕过 persist 插件的异步 watch,直接同步写入 storage | ||
| 405 | + // 防止 navigateBack 后 persist flush 赶不及,导致 isTraining 残留为 true | ||
| 406 | + try { | ||
| 407 | + uni.setStorageSync('training', JSON.stringify(this.$state)); | ||
| 408 | + } catch (e) { | ||
| 409 | + // 静默处理,不影响主流程 | ||
| 410 | + } | ||
| 403 | }, | 411 | }, |
| 404 | }, | 412 | }, |
| 405 | 413 |
-
Please register or login to post a comment