Authored by liangjingyao

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

2:修复取消模板确认框在明细后面问题
3:修复开始训练打勾问题
... ... @@ -8,7 +8,7 @@ SHOPRO_VERSION=v2.4.1
# 后端接口 - 测试环境(通过 process.env.NODE_ENV = development)
SHOPRO_DEV_BASE_URL=http://192.168.1.200:48081
# SHOPRO_DEV_BASE_URL=http://192.168.1.85:48080
SHOPRO_DEV_BASE_URL=https://xunji.geaktec.com
#SHOPRO_DEV_BASE_URL=https://xunji.geaktec.com
# SHOPRO_DEV_BASE_URL=http://api-dashboard.yudao.iocoder.cn/
### SHOPRO_DEV_BASE_URL=http://10.171.1.188:48080
### SHOPRO_DEV_BASE_URL = http://yunai.natapp1.cc
... ...
... ... @@ -29,15 +29,21 @@
</view>
<template v-if="resdailyData.length > 0">
<!-- Tab 按钮:如���有多个训记,用来源名作为标签 -->
<scroll-view class="plan-tabs" scroll-x v-if="resdailyData.length > 1">
<button class="tab-btn" :class="{ active: currentPlanIndex === index }" v-for="(item, index) in resdailyData"
:key="index" @click.stop="switchPlan(index)">
训记{{ index + 1 }}
{{ item.sourceType === 1 || item.sourceType === 2 ? (item.sourceName || item.name) : (item.name || '训记' + (index + 1)) }}
</button>
</scroll-view>
<!-- 接口获取当天的多个训练计划 → 放进 resdailyData,currentPlan 自动变成 resdailyData[0] 一个模板-->
<!-- 训练内容区域 -->
<view v-if="resdailyData.length > 0 && currentPlan" class="training-container">
<!-- 训练来源标签 -->
<view class="source-tag-row" :class="getSourceClass(currentPlan.sourceType || 4)">
<text class="source-text">
{{ currentPlan.sourceType === 1 || currentPlan.sourceType === 2 ? (currentPlan.sourceName || currentPlan.name) : getSourceLabel(currentPlan.sourceType || 4) }}
</text>
</view>
<view class="unitCart">
<!-- 训练计划头部卡片 -->
<view class="plan-header-card">
... ... @@ -416,6 +422,67 @@ const currentPlan = computed(() => {
return resdailyData.value[currentPlanIndex.value] || null;
});
// ========= 训练来源分组展示 =========
/** sourceType → 中文标签 */
const getSourceLabel = (type) => {
const labels = {
1: '来自计划',
2: '来自模板',
3: '自定义训练',
4: '自由训练',
};
return labels[type] || '训练';
};
/** sourceType → 风格 class */
const getSourceClass = (type) => {
const classes = {
1: 'source-plan', // 计划
2: 'source-tpl', // 模板
3: 'source-custom', // 自定义
4: 'source-free', // 自由
};
return classes[type] || 'source-free';
};
/** 按 sourceType 分组后的模板列表,供 tab 切换使用 */
const sourceGrouped = computed(() => {
const list = resdailyData.value || [];
const map = new Map();
list.forEach((item) => {
const key = item.sourceType ? `s${item.sourceType}_${item.sourceName || ''}` : `t_${item.dailyTemplateId}`;
if (!map.has(key)) {
const st = item.sourceType || 4;
map.set(key, {
sourceType: st,
sourceName: item.sourceName || item.name,
sourceLabel: item.sourceType === 1 || item.sourceType === 2 ? item.sourceName : getSourceLabel(st),
items: [],
});
}
map.get(key).items.push(item);
});
return Array.from(map.values());
});
/** 扁平化的模板列表(带 source 信息),用于直接渲染 */
const flatPlanList = computed(() => {
const result = [];
sourceGrouped.value.forEach((group) => {
group.items.forEach((item, idx) => {
result.push({
...item,
sourceType: group.sourceType,
sourceName: group.sourceName,
sourceLabel: group.sourceLabel,
isFirstInGroup: idx === 0,
});
});
});
return result;
});
const loaddailytemplate = async () => {
if (!selectedDate.value) return;
console.log('【子组件】开始加载数据...');
... ... @@ -980,6 +1047,34 @@ onMounted(() => {
font-weight: bold;
}
/* 训练来源标签 */
.source-tag-row {
display: flex;
align-items: center;
padding: 16rpx 24rpx;
margin: 0rpx 20rpx;
border-radius: 12rpx;
font-size: 24rpx;
font-weight: 500;
.source-text {
color: #fff;
}
&.source-plan {
background: #3b82f6; /* 蓝色 - 计划 */
}
&.source-tpl {
background: #8b5cf6; /* 紫色 - 模板 */
}
&.source-custom {
background: #f59e0b; /* 橙色 - 自定义 */
}
&.source-free {
background: #10b981; /* 绿色 - 自由 */
}
}
/* 训练计划头部卡片 */
.plan-header-card {
display: flex;
... ...
... ... @@ -247,17 +247,32 @@ const loadPlans = async (monthStr) => {
planMap[dateStr] = planMap[dateStr] || [];
if (Array.isArray(plan.templates)) {
let totalWeightForDay = 0;
// 按 sourceType + sourceName 分组
const sourceMap = new Map();
plan.templates.forEach((template) => {
if (template && template.name) {
planMap[dateStr].push({
name: template.name,
if (!template || !template.name) return;
const sourceKey = template.sourceType
? `src_${template.sourceType}_${template.sourceName || ''}`
: `tpl_${template.dailyTemplateId}`;
if (!sourceMap.has(sourceKey)) {
sourceMap.set(sourceKey, {
name: template.sourceName || template.name,
bg: template.backgroundColor || '#fef08a',
sourceType: template.sourceType || 4,
sourceName: template.sourceName || template.name,
dailyTemplateId: template.dailyTemplateId,
totalWeight: 0,
});
}
const group = sourceMap.get(sourceKey);
if (template && typeof template.totalWeight === 'number') {
group.totalWeight += template.totalWeight;
totalWeightForDay += template.totalWeight;
}
});
sourceMap.forEach((group) => {
planMap[dateStr].push(group);
});
if (totalWeightForDay > 0) {
weightMap[dateStr] = totalWeightForDay;
}
... ...
... ... @@ -346,14 +346,14 @@ const chartSeries = computed(() => {
const dataKey = TREND_DATA_KEYS[NumberTabIndex.value] || 'volume';
// 当前周期数据
const currData = fullDateKeys.map((dateStr) => currDataMap[dateStr]?.[dataKey] ?? 0);
const currData = fullDateKeys.map((dateStr) => convertUnit(currDataMap[dateStr]?.[dataKey], dataKey));
// 上一周期数据(按日期偏移匹配)
const lastData = fullDateKeys.map((dateStr) => {
const targetKey = isWeekly
? dayjs(dateStr).subtract(7, 'day').format('YYYY-MM-DD')
: dayjs(dateStr).subtract(1, 'month').format('YYYY-MM-DD');
return lastDataMap[targetKey]?.[dataKey] ?? 0;
return convertUnit(lastDataMap[targetKey]?.[dataKey], dataKey);
});
const currName = isWeekly ? '本周' : '本月';
... ... @@ -373,25 +373,39 @@ const listData = computed(() => {
const d = weeklyData.value;
const fields = [
{ label: '容量', last: d.lastWeekTotalVolume, curr: d.totalVolume },
{ label: '组数', last: d.lastWeekTotalSets, curr: d.totalSets },
{ label: '时长', last: d.lastWeekTotalDuration, curr: d.totalDuration },
{ label: '距离', last: d.lastWeekTotalDistance, curr: d.totalDistance },
{ label: '次数', last: d.lastWeekTotalCount, curr: d.totalCount },
{ label: '容量', key: 'volume', last: d.lastWeekTotalVolume, curr: d.totalVolume },
{ label: '组数', key: 'setCount', last: d.lastWeekTotalSets, curr: d.totalSets },
{ label: '时长', key: 'duration', last: d.lastWeekTotalDuration, curr: d.totalDuration },
{ label: '距离', key: 'distance', last: d.lastWeekTotalDistance, curr: d.totalDistance },
{ label: '次数', key: 'count', last: d.lastWeekTotalCount, curr: d.totalCount },
];
return fields.map(({ label, last, curr }) => {
const lastVal = last ?? 0;
const currVal = curr ?? 0;
return fields.map(({ label, key, last, curr }) => {
const lastVal = convertUnit(last, key);
const currVal = convertUnit(curr, key);
return {
label,
lastPeriod: lastVal,
currPeriod: currVal,
diff: currVal - lastVal,
diff: Number((currVal - lastVal).toFixed(1)),
};
});
});
/**
* 字段单位换算:API 返回秒/米等,UI 展示为分/km 等
* duration(秒)→ 分钟
* 其它字段(volume/setCount/distance/count)保持原值
*/
const convertUnit = (val, key) => {
const v = Number(val) || 0;
if (key === 'duration') {
// 秒 → 分钟,保留 1 位小数(如 90s → 1.5)
return Math.round((v / 60) * 10) / 10;
}
return v;
};
/** 肌肉弹窗图表 X 轴 */
const muscleChartCategories = computed(() => {
if (MainCurrentDateType.value === 'week') {
... ...
... ... @@ -233,29 +233,37 @@ const handleDeleteTemplate = async () => {
return;
}
uni.showModal({
title: '确认删除',
content: '确定要删除这个模板吗?删除后无法恢复',
confirmColor: '#ff4444',
success: async (res) => {
if (res.confirm) {
try {
uni.showLoading({ title: '删除中...' });
await TemplatesApi.deleteCustTemplate(currentTemplate.value.id);
uni.hideLoading();
uni.showToast({ title: '删除成功', icon: 'success' });
closeTemplateMenu();
// 通知父组件刷新列表
emit('refresh');
} catch (err) {
uni.hideLoading();
console.error('删除失败:', err);
uni.showToast({ title: '删除失败', icon: 'none' });
// 在 showModal 前把 id 存到局部变量,防止异步回调执行时 currentTemplate.value 已被清空
const templateId = currentTemplate.value.id;
// 必须先关闭 WebView 弹窗(up-popup),否则它会盖在 uni.showModal 原生弹窗上面
closeTemplateMenu();
// 延时一帧再弹原生确认框,避免与 closeTemplateMenu 的卸载动画在视觉上打架
setTimeout(() => {
uni.showModal({
title: '确认删除',
content: '确定要删除这个模板吗?删除后无法恢复',
confirmColor: '#ff4444',
success: async (res) => {
if (res.confirm) {
try {
uni.showLoading({ title: '删除中...' });
await TemplatesApi.deleteCustTemplate(templateId);
uni.hideLoading();
uni.showToast({ title: '删除成功', icon: 'success' });
// 通知父组件刷新列表
emit('refresh');
} catch (err) {
uni.hideLoading();
console.error('删除失败:', err);
uni.showToast({ title: '删除失败', icon: 'none' });
}
}
}
}
});
});
}, 200);
};
... ...
... ... @@ -8,7 +8,7 @@
<text class="time">{{ formattedTime.substring(3) }}</text>
<view class="triangle-icon"></view>
</view>
<view class="finish-btn" @click="save">完成</view>
<view class="finish-btn" :class="{ disabled: !allUnitsChecked }" @click="save">完成</view>
</view>
<view class="action-list">
... ... @@ -431,6 +431,14 @@
unitActiveStates.value = [...unitActiveStates.value]
}
// 全部 unit 已勾选后才能提交
const allUnitsChecked = computed(() => {
const list = actionDetail.value?.units || []
if (list.length === 0) return false
if (unitActiveStates.value.length !== list.length) return false
return unitActiveStates.value.every((v) => v === true)
})
const openSummaryPopup = () => {
summaryShow.value = true;
summaryContent.value = '';
... ... @@ -507,6 +515,17 @@
});
const save = async () => {
// 前置校验:所有 unit 必须已勾选(顶部 ✓)才能提交
if (!allUnitsChecked.value) {
uni.showModal({
title: '提示',
content: '还有训练没有完成,请先勾选所有动作后再提交',
showCancel: false,
confirmText: '知道了',
});
return;
}
await nextTick();
const units = [];
const unitList = actionDetail.value?.units || [];
... ... @@ -552,16 +571,17 @@
try {
if (trainingStore.isTraining) {
await TrainingApi.createTrainHistory({ units });
await TrainingApi.createTrainHistory({ units, allCompleted: allUnitsChecked.value });
uni.showToast({ title: '训练提交成功', icon: 'success' });
} else if (trainingStore.type === 3 && trainingStore.dailyTemplateId) {
await dailytemplateApi.updateDailyTemplate({
dailyTemplateId: trainingStore.dailyTemplateId,
units,
allCompleted: allUnitsChecked.value,
});
uni.showToast({ title: '模板修改成功', icon: 'success' });
} else {
await TrainingApi.createTrainHistory({ units });
await TrainingApi.createTrainHistory({ units, allCompleted: allUnitsChecked.value });
uni.showToast({ title: '训练提交成功', icon: 'success' });
}
setTimeout(() => {
... ... @@ -681,6 +701,14 @@
border-radius: 30rpx;
font-weight: bold;
font-size: 28rpx;
transition: opacity 0.2s;
&.disabled {
background-color: #555;
color: #999;
opacity: 0.6;
pointer-events: none;
}
}
}
... ...
... ... @@ -8,6 +8,7 @@ const AuthUtil = {
method: 'POST',
data,
custom: {
isToken: false,
showSuccess: true,
successMsg: '登录成功',
},
... ... @@ -20,6 +21,7 @@ const AuthUtil = {
method: 'POST',
data,
custom: {
isToken: false,
showSuccess: true,
successMsg: '登录成功',
},
... ... @@ -32,6 +34,7 @@ const AuthUtil = {
method: 'POST',
data,
custom: {
isToken: false,
showLoading: false,
},
});
... ... @@ -67,7 +70,9 @@ const AuthUtil = {
url: '/app/auth/setPassword',
method: 'POST',
data,
custom: {
isToken: false,
},
});
},
... ... @@ -113,6 +118,7 @@ const AuthUtil = {
role: 1,
},
custom: {
isToken: false,
showSuccess: true,
loadingMsg: '登录中',
successMsg: '登录成功',
... ... @@ -128,6 +134,7 @@ const AuthUtil = {
code,
},
custom: {
isToken: false,
showSuccess: true,
loadingMsg: '登录中',
successMsg: '登录成功',
... ...
... ... @@ -422,6 +422,14 @@ export const useTrainingStore = defineStore('training', {
this.defaultTimeIndex = [0, 0, 0];
this.showPicker = false;
this.isTraining = false;
// 绕过 persist 插件的异步 watch,直接同步写入 storage
// 防止 navigateBack 后 persist flush 赶不及,导致 isTraining 残留为 true
try {
uni.setStorageSync('training', JSON.stringify(this.$state));
} catch (e) {
// 静默处理,不影响主流程
}
},
},
... ...