update:对接个人训练难度页
This commit is contained in:
@@ -411,6 +411,11 @@ export const getPersonalTrainingAPI = async () => {
|
||||
return request("GET", "/personal/training");
|
||||
};
|
||||
|
||||
export const getTrainingDifficultyListAPI = async (type) => {
|
||||
const query = type ? `?type=${encodeURIComponent(type)}` : "";
|
||||
return request("GET", `/training/difficulty/list${query}`);
|
||||
};
|
||||
|
||||
export const getBattleDataAPI = async () => {
|
||||
return request("GET", "/user/fight/statistics");
|
||||
};
|
||||
|
||||
@@ -62,8 +62,8 @@ const badgeStateStyle = computed(() => {
|
||||
};
|
||||
});
|
||||
|
||||
const isCompleted = computed(() => {
|
||||
return !props.active && !props.locked && progressValue.value > 0;
|
||||
const showProgress = computed(() => {
|
||||
return !props.locked;
|
||||
});
|
||||
|
||||
const badgeFillSrc = computed(() => {
|
||||
@@ -80,7 +80,7 @@ const handleClick = () => {
|
||||
class="difficulty-badge"
|
||||
:class="{
|
||||
'difficulty-badge--active': active,
|
||||
'difficulty-badge--completed': isCompleted,
|
||||
'difficulty-badge--progress': showProgress,
|
||||
'difficulty-badge--locked': locked,
|
||||
}"
|
||||
:style="[badgeStyle, badgeStateStyle]"
|
||||
@@ -134,7 +134,7 @@ const handleClick = () => {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: calc(var(--badge-orbit-offset) * -1);
|
||||
border: 4rpx solid #ffffff;
|
||||
border: 4rpx solid transparent;
|
||||
border-radius: 50%;
|
||||
pointer-events: none;
|
||||
box-sizing: border-box;
|
||||
@@ -236,8 +236,8 @@ const handleClick = () => {
|
||||
content: none;
|
||||
}
|
||||
|
||||
.difficulty-badge--completed .difficulty-badge__fill::before,
|
||||
.difficulty-badge--completed .difficulty-badge__fill::after {
|
||||
.difficulty-badge--progress .difficulty-badge__fill::before,
|
||||
.difficulty-badge--progress .difficulty-badge__fill::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: -12rpx;
|
||||
@@ -251,11 +251,11 @@ const handleClick = () => {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.difficulty-badge--completed .difficulty-badge__fill::before {
|
||||
.difficulty-badge--progress .difficulty-badge__fill::before {
|
||||
background: rgba(255, 255, 255, 0.35);
|
||||
}
|
||||
|
||||
.difficulty-badge--completed .difficulty-badge__fill::after {
|
||||
.difficulty-badge--progress .difficulty-badge__fill::after {
|
||||
background: conic-gradient(
|
||||
from -90deg,
|
||||
rgba(254, 208, 152, 1) 0,
|
||||
@@ -265,7 +265,7 @@ const handleClick = () => {
|
||||
}
|
||||
|
||||
.difficulty-badge--active .difficulty-badge__label,
|
||||
.difficulty-badge--completed .difficulty-badge__label {
|
||||
.difficulty-badge--progress .difficulty-badge__label {
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
import { computed } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
default: "",
|
||||
@@ -9,6 +11,10 @@ defineProps({
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const previewLines = computed(() => {
|
||||
return props.lines.map((line) => String(line || "").trim()).filter(Boolean);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -21,8 +27,13 @@ defineProps({
|
||||
<view class="difficulty-preview__content">
|
||||
<text class="difficulty-preview__title">{{ title }}</text>
|
||||
<view class="difficulty-preview__copy">
|
||||
箭靶划分为3个区域、需5次命中目标
|
||||
100秒内完成所有射击
|
||||
<text
|
||||
v-for="(line, index) in previewLines"
|
||||
:key="`${line}-${index}`"
|
||||
class="difficulty-preview__line"
|
||||
>
|
||||
{{ line }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -67,5 +78,9 @@ defineProps({
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.difficulty-preview__line {
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
|
||||
@@ -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