update:对接个人训练改版

This commit is contained in:
2026-07-23 09:44:58 +08:00
parent a78ab1daeb
commit 3c5754b3fd
64 changed files with 1769 additions and 488 deletions
+240 -73
View File
@@ -27,6 +27,14 @@ const props = defineProps({
type: Number,
default: 0,
},
trainingType: {
type: String,
default: "",
},
difficultyLevel: {
type: Number,
default: 0,
},
result: {
type: Object,
default: () => ({}),
@@ -60,12 +68,6 @@ function onClickShare() {
uni.$emit("share-image");
}
onMounted(() => {
if (props.result.lvl > user.value.lvl) {
showUpgrade.value = true;
}
});
const details = computed(() => props.result.details || []);
const arrows = computed(() => {
@@ -81,25 +83,89 @@ const totalRing = computed(() =>
details.value.reduce((last, next) => last + (Number(next.ring) || 0), 0)
);
const gainedExp = computed(
() => props.result.exp || props.result.experience || validArrows.value
const hasResultValue = (...keys) =>
keys.some((key) => {
const value = props.result[key];
return value !== undefined && value !== null && value !== "";
});
const readResultNumber = (keys, fallback = 0) => {
for (const key of keys) {
const value = props.result[key];
if (value === undefined || value === null || value === "") continue;
const numberValue = Number(value);
if (Number.isFinite(numberValue)) return numberValue;
}
return fallback;
};
const beforeExp = computed(() =>
readResultNumber(["beforeExp", "before_exp"])
);
const currentLevel = computed(
() => props.result.lvl || user.value.lvl || user.value.rankLvl || 1
);
const currentExp = computed(() => {
const userScores = Number(user.value.scores);
return readResultNumber(
["currentExp", "current_exp", "score"],
Number.isFinite(userScores) ? userScores : 0
);
});
const currentExp = computed(
() => props.result.currentExp || props.result.score || user.value.scores || 0
);
// 新版练习结算返回练习前后累计经验,本局经验由两者相减得到。
const gainedExp = computed(() => {
if (
hasResultValue("beforeExp", "before_exp") &&
hasResultValue("currentExp", "current_exp")
) {
return Math.max(0, currentExp.value - beforeExp.value);
}
return Math.max(0, readResultNumber(["exp", "experience"]));
});
const nextExp = computed(
() => props.result.nextExp || props.result.upgradeScore || 100
const beforeLevel = computed(() => {
const currentUserLevel = Number(user.value.lvl);
return readResultNumber(
["beforeLevel", "before_level"],
Number.isFinite(currentUserLevel) ? currentUserLevel : 0
);
});
const userLevel = computed(() => {
const fallbackLevel = Number(user.value.lvl ?? user.value.rankLvl ?? 1);
const level = readResultNumber(
["level", "lvl"],
Number.isFinite(fallbackLevel) ? fallbackLevel : 1
);
return Math.max(1, Math.trunc(level));
});
const resultDifficultyLevel = computed(() => {
const level = Number(props.difficultyLevel);
return Number.isInteger(level) && level > 0 ? level : "--";
});
const upgradeExp = computed(() =>
Math.max(
0,
readResultNumber(
["upgradeExp", "upgrade_exp", "nextExp", "upgradeScore"],
100
)
)
);
const expPercent = computed(() => {
if (!nextExp.value) return 0;
return Math.min(100, Math.max(0, (currentExp.value / nextExp.value) * 100));
if (!upgradeExp.value) return 0;
return Math.min(
100,
Math.max(0, (currentExp.value / upgradeExp.value) * 100)
);
});
onMounted(() => {
if (userLevel.value > beforeLevel.value) {
showUpgrade.value = true;
}
});
const findValue = (...keys) => {
@@ -108,83 +174,184 @@ const findValue = (...keys) => {
};
const formatDuration = (value) => {
const seconds = Number(value || 0);
if (!seconds) return "--";
const valueNumber = Number(value);
const seconds = Number.isFinite(valueNumber)
? Math.max(0, Math.round(valueNumber))
: 0;
const minutes = Math.floor(seconds / 60);
const rest = seconds % 60;
return minutes ? `${minutes}${rest}` : `${rest}`;
};
const usedTime = computed(() =>
findValue("duration", "usedTime", "shootTime", "time")
const formatMetricNumber = (value) => {
const valueNumber = Number(value);
if (!Number.isFinite(valueNumber)) return "0";
return String(Number(valueNumber.toFixed(2)));
};
const readMetricNumber = (keys, fallback = 0) => {
const value = findValue(...keys);
const valueNumber = Number(value);
return Number.isFinite(valueNumber) ? valueNumber : fallback;
};
const resultTrainingType = computed(
() => props.result.trainingType || props.trainingType || "precision"
);
const hitCompare = computed(
() => Number(findValue("hitCompare", "hitDiff", "hitDelta") || 0)
const metricConfigs = {
base: [
{
label: "平均环数",
valueKeys: ["averageRing", "average_ring"],
unit: "环",
deltaKeys: ["deltaAverageRing", "delta_average_ring"],
deltaUnit: "环",
},
{
label: "稳定性",
valueKeys: ["stability"],
unit: "",
deltaKeys: ["deltaStability", "delta_stability"],
deltaUnit: "",
},
],
rhythm: [
{
label: "最高连击次数",
valueKeys: ["maxCombo", "max_combo"],
unit: "连",
deltaKeys: ["deltaMaxCombo", "delta_max_combo"],
deltaUnit: "连",
},
{
label: "共命中环数",
valueKeys: ["currentRings", "current_rings"],
unit: "环",
deltaKeys: ["deltaTotalRings", "delta_total_rings"],
deltaUnit: "环",
},
],
endurance: [
{
label: "完成箭数",
valueKeys: ["totalArrows", "total_arrows"],
unit: "支",
deltaKeys: ["deltaTotalArrows", "delta_total_arrows"],
deltaUnit: "支",
},
{
label: "命中环数",
valueKeys: ["currentRings", "current_rings"],
unit: "环",
deltaKeys: ["deltaTotalRings", "delta_total_rings"],
deltaUnit: "环",
},
],
precision: [
{
label: "共命中目标",
valueKeys: ["totalHits", "total_hits"],
fallback: () => 0,
unit: "次",
deltaKeys: [
"deltaTotalHits",
"delta_total_hits",
"hitCompare",
"hitDiff",
"hitDelta",
],
deltaUnit: "次",
},
{
label: "用时",
valueKeys: ["duration", "usedTime", "shootTime", "time"],
unit: "",
deltaKeys: [
"deltaDuration",
"delta_duration",
"timeCompare",
"timeDiff",
"durationDiff",
],
deltaUnit: "",
duration: true,
},
],
};
const resultRows = computed(() => {
const configs = metricConfigs[resultTrainingType.value] || metricConfigs.precision;
return configs.map((config) => {
const fallback = config.fallback ? config.fallback() : 0;
const value = readMetricNumber(config.valueKeys, fallback);
const delta = readMetricNumber(config.deltaKeys);
const formatter = config.duration ? formatDuration : formatMetricNumber;
return {
...config,
valueText: formatter(value),
delta,
deltaText: formatter(Math.abs(delta)),
};
});
});
const advancesDifficulty = computed(() =>
["base", "endurance"].includes(resultTrainingType.value)
);
const primaryText = computed(() =>
advancesDifficulty.value ? "下一难度" : "再来一次"
);
const timeCompare = computed(
() => Number(findValue("timeCompare", "timeDiff", "durationDiff") || 0)
);
const handlePrimary = () => {
if (advancesDifficulty.value) {
closePanel();
return;
}
retryPractice();
};
const calories = computed(
() => Number(findValue("calories", "calorie", "kcal") || 0)
() => formatMetricNumber(readMetricNumber(["calories", "calorie", "kcal"]))
);
</script>
<template>
<view :class="['result-mask', showPanel ? 'result-mask--show' : 'result-mask--hide']">
<image class="hero-glow" src="/static/training-difficulty-design/result-bg.png" mode="widthFix" />
<image class="hero-glow" src="../static/training-difficulty-design/result-bg.png" mode="widthFix" />
<view class="result-title">
<image class="result-title-bg" src="/static/training-difficulty-design/result-t-bg.png" mode="widthFix" />
<view class="result-title-text">Lv{{ currentLevel }}</view>
<image class="result-title-bg" src="../static/training-difficulty-design/result-t-bg.png" mode="widthFix" />
<view class="result-title-text">Lv{{ resultDifficultyLevel }}</view>
</view>
<view class="result-panel">
<view class="line-top"></view>
<view class="line-bottom"></view>
<view class="stats">
<view class="stat-row">
<image class="stat-bg" src="/static/training-difficulty-design/result-c-bg.png" mode="scaleToFill" />
<view v-for="row in resultRows" :key="row.label" class="stat-row">
<image class="stat-bg" src="../static/training-difficulty-design/result-c-bg.png" mode="scaleToFill" />
<view class="stat-cell">
<text class="stat-label">共命中目标</text>
<text class="stat-label">{{ row.label }}</text>
<view class="stat-value">
<text>{{ validArrows }}</text>
<text class="stat-unit"></text>
<text>{{ row.valueText }}</text>
<text v-if="row.unit" class="stat-unit">{{ row.unit }}</text>
</view>
</view>
<view class="stat-divider"></view>
<view class="stat-cell stat-cell--compare">
<text class="stat-label">对比上次</text>
<view class="stat-value">
<text>{{ Math.abs(hitCompare) }}</text>
<text class="stat-unit"></text>
<image class="trend-icon" :class="{ 'trend-icon--down': hitCompare < 0 }"
src="/static/training-difficulty-design/result-up.png" mode="widthFix" />
</view>
</view>
</view>
<view class="stat-row">
<image class="stat-bg" src="/static/training-difficulty-design/result-c-bg.png" mode="scaleToFill" />
<view class="stat-cell">
<text class="stat-label">用时</text>
<view class="stat-value">
<text>{{ formatDuration(usedTime) }}</text>
</view>
</view>
<view class="stat-divider"></view>
<view class="stat-cell stat-cell--compare">
<text class="stat-label">对比上次</text>
<view class="stat-value">
<text>{{ formatDuration(Math.abs(timeCompare)) }}</text>
<image class="trend-icon" :class="{ 'trend-icon--down': timeCompare <= 0 }"
src="/static/training-difficulty-design/result-up.png" mode="widthFix" />
<view v-if="row.delta !== 0" class="stat-value">
<text>{{ row.delta > 0 ? "+" : "-" }}{{ row.deltaText }}</text>
<text v-if="row.deltaUnit" class="stat-unit">{{ row.deltaUnit }}</text>
<image class="trend-icon" :class="{ 'trend-icon--down': row.delta < 0 }"
src="../static/training-difficulty-design/result-up.png" mode="widthFix" />
</view>
<view v-else class="stat-value">--</view>
</view>
</view>
<view class="stat-row">
<image class="stat-bg" src="/static/training-difficulty-design/result-c-bg.png" mode="scaleToFill" />
<image class="stat-bg" src="../static/training-difficulty-design/result-c-bg.png" mode="scaleToFill" />
<view class="stat-cell">
<text class="stat-label">消耗卡路里</text>
<view class="stat-value">
@@ -196,7 +363,7 @@ const calories = computed(
<view class="stat-cell stat-cell--compare">
<view class="stat-value">
<image v-for="index in 3" :key="index" class="rice-icon"
src="/static/training-difficulty-design/result-rice.png" mode="widthFix" />
src="../static/training-difficulty-design/result-rice.png" mode="widthFix" />
</view>
</view>
</view>
@@ -204,15 +371,15 @@ const calories = computed(
<view class="actions">
<view class="action-item" @click="() => (showBowData = true)">
<image class="action-icon" src="/static/training-difficulty-design/result-icon-1.png" mode="widthFix" />
<image class="action-icon" src="../static/training-difficulty-design/result-icon-1.png" mode="widthFix" />
<text>查看靶纸</text>
</view>
<view v-if="validArrows === total" class="action-item" @click="() => (showComment = true)">
<image class="action-icon" src="/static/training-difficulty-design/result-icon-2.png" mode="widthFix" />
<view class="action-item" @click="() => (showComment = true)">
<image class="action-icon" src="../static/training-difficulty-design/result-icon-2.png" mode="widthFix" />
<text>教练点评</text>
</view>
<view v-if="validArrows === total" class="action-item" @click="onClickShare">
<image class="action-icon" src="/static/training-difficulty-design/result-icon-3.png" mode="widthFix" />
<view class="action-item" @click="onClickShare">
<image class="action-icon" src="../static/training-difficulty-design/result-icon-3.png" mode="widthFix" />
<text>分享成绩</text>
</view>
</view>
@@ -222,20 +389,20 @@ const calories = computed(
<view class="exp-area">
<text class="exp-gain">+{{ gainedExp }}经验</text>
<view class="level-progress">
<text class="level-text">LV.{{ currentLevel }}</text>
<text class="level-text">LV.{{ userLevel }}</text>
<view class="progress-track">
<view class="progress-fill" :style="{ width: `${expPercent}%` }"></view>
</view>
<text class="progress-text">{{ currentExp }} / {{ nextExp }}</text>
<text class="progress-text">{{ currentExp }} / {{ upgradeExp }}</text>
</view>
</view>
<view class="footer-actions">
<view class="result-btn result-btn--muted" @click="closePanel">
<text>{{ validArrows === total ? "完成" : "返回" }}</text>
<text>完成</text>
</view>
<view class="result-btn result-btn--primary" @click="retryPractice">
<text>再来一次</text>
<view class="result-btn result-btn--primary" @click="handlePrimary">
<text>{{ primaryText }}</text>
</view>
</view>
</view>
@@ -268,7 +435,7 @@ const calories = computed(
</ScreenHint>
<BowData :total="arrows.length" :arrows="result.details" :show="showBowData"
:onClose="() => (showBowData = false)" />
<UserUpgrade :show="showUpgrade" :onClose="() => (showUpgrade = false)" :lvl="result.lvl" />
<UserUpgrade :show="showUpgrade" :onClose="() => (showUpgrade = false)" :lvl="userLevel" />
</view>
</template>