update:对接个人训练难度页
This commit is contained in:
@@ -6,16 +6,205 @@ import TargetPicker from "@/components/TargetPicker.vue";
|
||||
import TrainingDifficultyBadge from "@/components/training/TrainingDifficultyBadge.vue";
|
||||
import TrainingDifficultyPreviewCard from "@/components/training/TrainingDifficultyPreviewCard.vue";
|
||||
import TrainingDifficultyStartButton from "@/components/training/TrainingDifficultyStartButton.vue";
|
||||
import {
|
||||
getTrainingDifficultyModeConfig,
|
||||
trainingDifficultyStorageKey,
|
||||
} from "@/mock/trainingDifficulty.js";
|
||||
import { getTrainingDifficultyListAPI } from "@/apis";
|
||||
|
||||
// 当前页面的数据源:
|
||||
// 1. 模式配置:src/mock/trainingDifficulty.js -> getTrainingDifficultyModeConfig
|
||||
// 2. 当前进度:页面路由参数或配置中的 activeDifficultyId / progressMap
|
||||
const defaultModeKey = "precision";
|
||||
// 难度页接口数据源:
|
||||
// 1. 接口:GET /training/difficulty/list?type=base/endurance/precision/rhythm
|
||||
// 2. 当前进度:接口 user_levels / list.completed,路由参数可覆盖选中难度
|
||||
const trainingDifficultyStorageKey = "training-selection";
|
||||
const defaultTrainingType = "precision";
|
||||
const defaultUnlockedDifficultyId = "lv1";
|
||||
const trainingTypeMetaMap = {
|
||||
base: {
|
||||
key: "base",
|
||||
title: "基础训练",
|
||||
},
|
||||
endurance: {
|
||||
key: "endurance",
|
||||
title: "耐力训练",
|
||||
},
|
||||
precision: {
|
||||
key: "precision",
|
||||
title: "精准训练",
|
||||
},
|
||||
rhythm: {
|
||||
key: "rhythm",
|
||||
title: "节奏训练",
|
||||
},
|
||||
};
|
||||
const routeModeTypeMap = {
|
||||
basic: "base",
|
||||
base: "base",
|
||||
endurance: "endurance",
|
||||
precision: "precision",
|
||||
rhythm: "rhythm",
|
||||
};
|
||||
|
||||
const resolveTrainingType = (mode) => {
|
||||
const normalizedMode = String(mode || "").toLowerCase();
|
||||
return routeModeTypeMap[normalizedMode] || defaultTrainingType;
|
||||
};
|
||||
|
||||
const createDifficultyId = (level) => `lv${level}`;
|
||||
|
||||
const toNumber = (value, fallback = 0) => {
|
||||
const numberValue = Number(value);
|
||||
return Number.isFinite(numberValue) ? numberValue : fallback;
|
||||
};
|
||||
|
||||
const clampProgress = (value) => {
|
||||
return Math.min(Math.max(value, 0), 100);
|
||||
};
|
||||
|
||||
const getDifficultyProgress = (item = {}) => {
|
||||
const completedCnt = toNumber(item.completed_cnt);
|
||||
const promoteCnt = toNumber(item.promote_cnt);
|
||||
|
||||
if (item.completed) {
|
||||
return 100;
|
||||
}
|
||||
|
||||
if (promoteCnt <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return clampProgress(Math.round((completedCnt / promoteCnt) * 100));
|
||||
};
|
||||
|
||||
const checkDifficultyCompleted = (item = {}) => {
|
||||
const completedCnt = toNumber(item.completed_cnt);
|
||||
const promoteCnt = toNumber(item.promote_cnt);
|
||||
|
||||
return Boolean(item.completed) || (promoteCnt > 0 && completedCnt >= promoteCnt);
|
||||
};
|
||||
|
||||
const getDifficultyModeText = (mode) => {
|
||||
return Number(mode) === 1 ? "随机区域+指定环数" : "随机区域命中";
|
||||
};
|
||||
|
||||
const createEmptyModeConfig = (type = defaultTrainingType) => {
|
||||
const meta = trainingTypeMetaMap[type] || trainingTypeMetaMap[defaultTrainingType];
|
||||
|
||||
return {
|
||||
key: meta.key,
|
||||
title: meta.title,
|
||||
nodes: [],
|
||||
details: {},
|
||||
activeDifficultyId: defaultUnlockedDifficultyId,
|
||||
progressMap: {},
|
||||
};
|
||||
};
|
||||
|
||||
const createDifficultySummary = (item = {}) => {
|
||||
const desc = String(item.desc || "").trim();
|
||||
const type = item.type;
|
||||
const arrows = toNumber(item.arrows);
|
||||
const timeLimit = toNumber(item.time_limit);
|
||||
const hitReq = toNumber(item.hit_req);
|
||||
const totalReq = toNumber(item.total_req);
|
||||
const blocks = toNumber(item.blocks);
|
||||
const promoteCnt = toNumber(item.promote_cnt);
|
||||
const timeText = timeLimit > 0 ? `${timeLimit}秒内完成` : "不限时完成";
|
||||
const promoteText = promoteCnt > 0 ? `完成${promoteCnt}次晋级` : "";
|
||||
|
||||
const summaryMap = {
|
||||
base: [
|
||||
desc || (hitReq > 0 ? `每箭命中${hitReq}环以上` : "上靶即可"),
|
||||
[`${arrows}箭`, promoteText].filter(Boolean).join(" · "),
|
||||
],
|
||||
endurance: [
|
||||
desc || `${timeText}${arrows}箭`,
|
||||
[`累计${totalReq}环`, promoteText].filter(Boolean).join(" · "),
|
||||
],
|
||||
precision: [
|
||||
desc || `命中${blocks}个指定区域`,
|
||||
[
|
||||
`${arrows}箭`,
|
||||
timeText,
|
||||
getDifficultyModeText(item.mode),
|
||||
promoteText,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" · "),
|
||||
],
|
||||
rhythm: [
|
||||
desc || `间隔${timeLimit}秒射击`,
|
||||
[
|
||||
`${arrows}箭`,
|
||||
hitReq > 0 ? `每箭${hitReq}环以上` : "上靶即可",
|
||||
getDifficultyModeText(item.mode),
|
||||
promoteText,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" · "),
|
||||
],
|
||||
};
|
||||
|
||||
return (summaryMap[type] || [desc]).filter(Boolean);
|
||||
};
|
||||
|
||||
const normalizeTrainingDifficultyConfig = (result, type) => {
|
||||
const meta = trainingTypeMetaMap[type] || trainingTypeMetaMap[defaultTrainingType];
|
||||
const list = Array.isArray(result?.list) ? result.list : [];
|
||||
const rawItems = list.filter((item) => !item?.type || item.type === meta.key);
|
||||
const difficultyItems = rawItems
|
||||
.map((item) => {
|
||||
const level = toNumber(item?.difficulty);
|
||||
|
||||
if (level <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const id = createDifficultyId(level);
|
||||
const label = `Lv${level}`;
|
||||
|
||||
return {
|
||||
...item,
|
||||
recordId: item.id,
|
||||
completedCnt: toNumber(item.completed_cnt),
|
||||
promoteCnt: toNumber(item.promote_cnt),
|
||||
id,
|
||||
level,
|
||||
label,
|
||||
title: `${label}难度`,
|
||||
summary: createDifficultySummary(item),
|
||||
startText: "开始",
|
||||
targetPaperType: "20CM全环靶",
|
||||
};
|
||||
})
|
||||
.filter(Boolean)
|
||||
.sort((first, second) => first.level - second.level);
|
||||
const maxLevel = difficultyItems.reduce(
|
||||
(currentMax, item) => Math.max(currentMax, item.level),
|
||||
0
|
||||
);
|
||||
const completedLevelFromList = difficultyItems.reduce((currentMax, item) => {
|
||||
return checkDifficultyCompleted(item)
|
||||
? Math.max(currentMax, item.level)
|
||||
: currentMax;
|
||||
}, 0);
|
||||
const userCompletedLevel = toNumber(result?.user_levels?.[meta.key]);
|
||||
const highestCompletedLevel = userCompletedLevel || completedLevelFromList;
|
||||
const unlockedLevel = maxLevel
|
||||
? Math.min(Math.max(highestCompletedLevel + 1, 1), maxLevel)
|
||||
: 1;
|
||||
|
||||
return {
|
||||
key: meta.key,
|
||||
title: meta.title,
|
||||
nodes: difficultyItems.map((item) => ({
|
||||
id: item.id,
|
||||
label: item.label,
|
||||
})),
|
||||
details: Object.fromEntries(
|
||||
difficultyItems.map((item) => [item.id, item])
|
||||
),
|
||||
activeDifficultyId: createDifficultyId(unlockedLevel),
|
||||
progressMap: Object.fromEntries(
|
||||
difficultyItems.map((item) => [item.id, getDifficultyProgress(item)])
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
// 难度轴布局参数,节点按“等级越低越靠下”的方式排列。
|
||||
const nodesLayout = {
|
||||
@@ -39,18 +228,13 @@ const emptyDifficulty = {
|
||||
};
|
||||
|
||||
// 页面基础状态
|
||||
const modeKey = ref(defaultModeKey);
|
||||
const pageConfig = ref(createEmptyModeConfig(defaultTrainingType));
|
||||
const unlockedDifficultyId = ref(defaultUnlockedDifficultyId);
|
||||
const selectedDifficultyId = ref(defaultUnlockedDifficultyId);
|
||||
const showTargetPicker = ref(false);
|
||||
const nodesScrollTop = ref(0);
|
||||
const nodesScrollWithAnimation = ref(false);
|
||||
|
||||
// 当前训练模式配置
|
||||
const pageConfig = computed(() => {
|
||||
return getTrainingDifficultyModeConfig(modeKey.value);
|
||||
});
|
||||
|
||||
const difficultyProgressMap = computed(() => {
|
||||
return pageConfig.value?.progressMap || {};
|
||||
});
|
||||
@@ -302,15 +486,36 @@ const initScrollPosition = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const initPageState = (options = {}) => {
|
||||
const config = getTrainingDifficultyModeConfig(options.mode);
|
||||
const nodes = createDifficultyNodes(config);
|
||||
const currentUnlockedId = resolveUnlockedDifficultyId(options, config, nodes);
|
||||
const normalizeRouteOptions = (options = {}) => {
|
||||
const difficultyLevel = toNumber(options.difficulty);
|
||||
const completedDifficultyLevel = toNumber(options.completedDifficulty);
|
||||
|
||||
modeKey.value = config.key;
|
||||
return {
|
||||
...options,
|
||||
difficultyId:
|
||||
options.difficultyId ||
|
||||
(difficultyLevel > 0 ? createDifficultyId(difficultyLevel) : ""),
|
||||
completedDifficultyId:
|
||||
options.completedDifficultyId ||
|
||||
(completedDifficultyLevel > 0
|
||||
? createDifficultyId(completedDifficultyLevel)
|
||||
: ""),
|
||||
};
|
||||
};
|
||||
|
||||
const applyPageState = (options = {}, config) => {
|
||||
const safeOptions = normalizeRouteOptions(options);
|
||||
const nodes = createDifficultyNodes(config);
|
||||
const currentUnlockedId = resolveUnlockedDifficultyId(
|
||||
safeOptions,
|
||||
config,
|
||||
nodes
|
||||
);
|
||||
|
||||
pageConfig.value = config;
|
||||
unlockedDifficultyId.value = currentUnlockedId;
|
||||
selectedDifficultyId.value = resolveSelectedDifficultyId(
|
||||
options.difficultyId,
|
||||
safeOptions.difficultyId,
|
||||
nodes,
|
||||
currentUnlockedId
|
||||
);
|
||||
@@ -320,6 +525,28 @@ const initPageState = (options = {}) => {
|
||||
});
|
||||
};
|
||||
|
||||
const initPageState = async (options = {}) => {
|
||||
const trainingType = resolveTrainingType(options.mode);
|
||||
const fallbackConfig = createEmptyModeConfig(trainingType);
|
||||
|
||||
pageConfig.value = fallbackConfig;
|
||||
|
||||
try {
|
||||
const result = await getTrainingDifficultyListAPI(trainingType);
|
||||
applyPageState(
|
||||
options,
|
||||
normalizeTrainingDifficultyConfig(result, trainingType)
|
||||
);
|
||||
} catch (error) {
|
||||
console.log("training difficulty load failed", error);
|
||||
applyPageState(options, fallbackConfig);
|
||||
uni.showToast({
|
||||
title: "训练难度加载失败",
|
||||
icon: "none",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const resolveTargetPaperType = (target) => {
|
||||
return Number(target) === 1 ? "20厘米全环靶" : "40厘米全环靶";
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user