update:代码备份
This commit is contained in:
327
src/pages/training/components/TrainingDifficultyBadge.vue
Normal file
327
src/pages/training/components/TrainingDifficultyBadge.vue
Normal file
@@ -0,0 +1,327 @@
|
||||
<script setup>
|
||||
import { computed } from "vue";
|
||||
|
||||
const lockedBadgeBackground =
|
||||
"/static/training-difficulty-design/unlock.svg";
|
||||
const unlockedBadgeBackground =
|
||||
"/static/training-difficulty-design/lock.svg";
|
||||
|
||||
const props = defineProps({
|
||||
node: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
active: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
completedProgress: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
locked: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["click"]);
|
||||
|
||||
const badgeStyle = computed(() => {
|
||||
const { left, top } = props.node.style || {};
|
||||
|
||||
return {
|
||||
left,
|
||||
top,
|
||||
};
|
||||
});
|
||||
|
||||
const progressValue = computed(() => {
|
||||
const value = Number(props.completedProgress);
|
||||
|
||||
if (!Number.isFinite(value)) return 0;
|
||||
|
||||
return Math.max(0, Math.min(100, value));
|
||||
});
|
||||
|
||||
const badgeStateStyle = computed(() => {
|
||||
const label = String(props.node?.label || "");
|
||||
const estimatedLabelWidthRpx = Math.max(36, label.length * 14);
|
||||
const labelCircleSizeRpx = Math.max(58, estimatedLabelWidthRpx + 18);
|
||||
const badgeSizeRpx = Math.max(
|
||||
124,
|
||||
Math.round(labelCircleSizeRpx / 0.4727)
|
||||
);
|
||||
|
||||
return {
|
||||
"--badge-progress": progressValue.value,
|
||||
"--badge-size": `${badgeSizeRpx}rpx`,
|
||||
"--badge-label-size": `${labelCircleSizeRpx}rpx`,
|
||||
"--badge-orbit-offset": "12rpx",
|
||||
"--badge-locked-ring-offset": "12rpx",
|
||||
};
|
||||
});
|
||||
|
||||
const showProgress = computed(() => {
|
||||
return !props.locked;
|
||||
});
|
||||
|
||||
const badgeFillSrc = computed(() => {
|
||||
return props.locked ? lockedBadgeBackground : unlockedBadgeBackground;
|
||||
});
|
||||
|
||||
const handleClick = () => {
|
||||
emit("click", props.node);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view
|
||||
class="difficulty-badge"
|
||||
:class="{
|
||||
'difficulty-badge--active': active,
|
||||
'difficulty-badge--progress': showProgress,
|
||||
'difficulty-badge--locked': locked,
|
||||
}"
|
||||
:style="[badgeStyle, badgeStateStyle]"
|
||||
@click="handleClick"
|
||||
>
|
||||
<view class="difficulty-badge__fill">
|
||||
<image class="difficulty-badge__bg" :src="badgeFillSrc" mode="aspectFit" />
|
||||
<view v-if="active" class="difficulty-badge__active-orbit">
|
||||
<view
|
||||
class="difficulty-badge__active-triangle difficulty-badge__active-triangle--top"
|
||||
></view>
|
||||
<view
|
||||
class="difficulty-badge__active-triangle difficulty-badge__active-triangle--right"
|
||||
></view>
|
||||
<view
|
||||
class="difficulty-badge__active-triangle difficulty-badge__active-triangle--bottom"
|
||||
></view>
|
||||
<view
|
||||
class="difficulty-badge__active-triangle difficulty-badge__active-triangle--left"
|
||||
></view>
|
||||
</view>
|
||||
<view class="difficulty-badge__label-wrap">
|
||||
<view class="difficulty-badge__label">{{ node.label }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.difficulty-badge,
|
||||
.difficulty-badge__fill,
|
||||
.difficulty-badge__label-wrap {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.difficulty-badge {
|
||||
position: absolute;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: transform 0.2s ease, opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.difficulty-badge--active {
|
||||
transform: translate(-50%, -50%) scale(1.04);
|
||||
}
|
||||
|
||||
.difficulty-badge--active::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: calc(var(--badge-orbit-offset) * -1);
|
||||
border: 4rpx solid transparent;
|
||||
border-radius: 50%;
|
||||
pointer-events: none;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.difficulty-badge--active::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: -24rpx;
|
||||
border: 4rpx solid rgba(254, 208, 152, 0.96);
|
||||
border-radius: 50%;
|
||||
box-shadow: inset 0 0 10rpx rgba(254, 208, 152, 0.88),
|
||||
inset 0 0 22rpx rgba(254, 208, 152, 0.32),
|
||||
0 0 14rpx rgba(254, 208, 152, 0.92),
|
||||
0 0 32rpx rgba(254, 208, 152, 0.52),
|
||||
0 0 52rpx rgba(254, 208, 152, 0.22);
|
||||
pointer-events: none;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.difficulty-badge__active-orbit {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: calc(100% + var(--badge-orbit-offset) * 2);
|
||||
height: calc(100% + var(--badge-orbit-offset) * 2);
|
||||
border-radius: 50%;
|
||||
z-index: 3;
|
||||
pointer-events: none;
|
||||
transform: translate(-50%, -50%);
|
||||
animation: badge-orbit-spin 5.4s linear infinite;
|
||||
transform-origin: center;
|
||||
}
|
||||
|
||||
.difficulty-badge__active-triangle {
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-style: solid;
|
||||
z-index: 2;
|
||||
pointer-events: none;
|
||||
opacity: 0.92;
|
||||
filter: drop-shadow(0 0 8rpx rgba(255, 255, 255, 0.45));
|
||||
}
|
||||
|
||||
.difficulty-badge__active-triangle--top {
|
||||
top: -3rpx;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
border-width: 11rpx 8rpx 0 8rpx;
|
||||
border-color: #ffffff transparent transparent transparent;
|
||||
}
|
||||
|
||||
.difficulty-badge__active-triangle--right {
|
||||
right: -3rpx;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
border-width: 8rpx 11rpx 8rpx 0;
|
||||
border-color: transparent #ffffff transparent transparent;
|
||||
}
|
||||
|
||||
.difficulty-badge__active-triangle--bottom {
|
||||
bottom: -3rpx;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
border-width: 0 8rpx 11rpx 8rpx;
|
||||
border-color: transparent transparent #ffffff transparent;
|
||||
}
|
||||
|
||||
.difficulty-badge__active-triangle--left {
|
||||
left: -3rpx;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
border-width: 8rpx 0 8rpx 11rpx;
|
||||
border-color: transparent transparent transparent #ffffff;
|
||||
}
|
||||
|
||||
.difficulty-badge__fill {
|
||||
width: var(--badge-size);
|
||||
height: var(--badge-size);
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.difficulty-badge__bg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.difficulty-badge--active .difficulty-badge__fill::before,
|
||||
.difficulty-badge--active .difficulty-badge__fill::after {
|
||||
content: none;
|
||||
}
|
||||
|
||||
.difficulty-badge--progress .difficulty-badge__fill::before,
|
||||
.difficulty-badge--progress .difficulty-badge__fill::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: -12rpx;
|
||||
padding: 6rpx;
|
||||
border-radius: inherit;
|
||||
-webkit-mask: linear-gradient(#fff 0 0) content-box,
|
||||
linear-gradient(#fff 0 0);
|
||||
-webkit-mask-composite: xor;
|
||||
mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
|
||||
mask-composite: exclude;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.difficulty-badge--progress .difficulty-badge__fill::before {
|
||||
background: rgba(255, 255, 255, 0.35);
|
||||
}
|
||||
|
||||
.difficulty-badge--progress .difficulty-badge__fill::after {
|
||||
background: conic-gradient(
|
||||
from -90deg,
|
||||
rgba(254, 208, 152, 1) 0,
|
||||
rgba(255, 229, 198, 1) calc(var(--badge-progress) * 1%),
|
||||
transparent calc(var(--badge-progress) * 1%) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.difficulty-badge--active .difficulty-badge__label,
|
||||
.difficulty-badge--progress .difficulty-badge__label {
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.difficulty-badge--locked {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.difficulty-badge--locked::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: calc(var(--badge-locked-ring-offset) * -1);
|
||||
border: 2rpx solid rgba(160, 160, 160, 0.5);
|
||||
border-radius: 50%;
|
||||
pointer-events: none;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.difficulty-badge--locked .difficulty-badge__label {
|
||||
color: rgba(51, 51, 51, 0.54);
|
||||
}
|
||||
|
||||
.difficulty-badge__label-wrap {
|
||||
width: var(--badge-label-size);
|
||||
height: var(--badge-label-size);
|
||||
border-radius: 50%;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: -5rpx;
|
||||
}
|
||||
|
||||
.difficulty-badge__label {
|
||||
color: rgba(51, 51, 51, 0.7);
|
||||
font-size: 24rpx;
|
||||
max-width: 100%;
|
||||
height: 34rpx;
|
||||
line-height: 34rpx;
|
||||
font-family: "PingFang SC", sans-serif;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@keyframes badge-orbit-spin {
|
||||
from {
|
||||
transform: translate(-50%, -50%) rotate(0deg);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translate(-50%, -50%) rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,86 @@
|
||||
<script setup>
|
||||
import { computed } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
lines: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const previewLines = computed(() => {
|
||||
return props.lines.map((line) => String(line || "").trim()).filter(Boolean);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="difficulty-preview">
|
||||
<image
|
||||
class="difficulty-preview__bg"
|
||||
src="/static/training-difficulty-design/text.png"
|
||||
mode="widthFix"
|
||||
/>
|
||||
<view class="difficulty-preview__content">
|
||||
<text class="difficulty-preview__title">{{ title }}</text>
|
||||
<view class="difficulty-preview__copy">
|
||||
<text
|
||||
v-for="(line, index) in previewLines"
|
||||
:key="`${line}-${index}`"
|
||||
class="difficulty-preview__line"
|
||||
>
|
||||
{{ line }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.difficulty-preview {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.difficulty-preview__bg {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.difficulty-preview__content {
|
||||
position: absolute;
|
||||
top: 28rpx;
|
||||
left: 30rpx;
|
||||
box-sizing: border-box;
|
||||
width: 486rpx;
|
||||
}
|
||||
|
||||
.difficulty-preview__title {
|
||||
display: block;
|
||||
color: #ffd543;
|
||||
font-size: 24rpx;
|
||||
line-height: 34rpx;
|
||||
font-family: "PingFang SC", sans-serif;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.difficulty-preview__copy {
|
||||
width: 80%;
|
||||
margin: 0 auto;
|
||||
display: block;
|
||||
color: #ffffff;
|
||||
font-size: 24rpx;
|
||||
line-height: 34rpx;
|
||||
font-family: "PingFang SC", sans-serif;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.difficulty-preview__line {
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,81 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
text: {
|
||||
type: String,
|
||||
default: "开始",
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["click"]);
|
||||
|
||||
const handleClick = () => {
|
||||
emit("click");
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
class="difficulty-start"
|
||||
hover-class="difficulty-start--hover"
|
||||
@click="handleClick"
|
||||
>
|
||||
<image
|
||||
class="difficulty-start__button"
|
||||
src="/static/training-difficulty-design/btn.png"
|
||||
mode="widthFix"
|
||||
/>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.difficulty-start {
|
||||
position: relative;
|
||||
width: 302rpx;
|
||||
height: 170rpx;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.difficulty-start::after {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.difficulty-start--hover {
|
||||
transform: translateY(2rpx) scale(0.99);
|
||||
}
|
||||
|
||||
.difficulty-start__mascot {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
width: 112rpx;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.difficulty-start__button {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 2;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.difficulty-start__text {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 58rpx;
|
||||
z-index: 3;
|
||||
color: #9f4d00;
|
||||
font-size: 64rpx;
|
||||
line-height: 76rpx;
|
||||
text-align: center;
|
||||
font-family: "AlimamaShuHeiTi-Bold", "PingFang SC", sans-serif;
|
||||
font-weight: 800;
|
||||
text-shadow: 0 3rpx 0 rgba(255, 245, 205, 0.78);
|
||||
}
|
||||
</style>
|
||||
@@ -1,17 +1,18 @@
|
||||
<script setup>
|
||||
import { computed, nextTick, ref } from "vue";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import { onLoad, onShow, onUnload } from "@dcloudio/uni-app";
|
||||
import Container from "@/components/Container.vue";
|
||||
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 TrainingDifficultyBadge from "./components/TrainingDifficultyBadge.vue";
|
||||
import TrainingDifficultyPreviewCard from "./components/TrainingDifficultyPreviewCard.vue";
|
||||
import TrainingDifficultyStartButton from "./components/TrainingDifficultyStartButton.vue";
|
||||
import { getTrainingDifficultyListAPI } from "@/apis";
|
||||
|
||||
// 难度页接口数据源:
|
||||
// 1. 接口:GET /training/difficulty/list?type=base/endurance/precision/rhythm
|
||||
// 2. 当前进度:接口 user_levels / list.completed,路由参数可覆盖选中难度
|
||||
const trainingDifficultyStorageKey = "training-selection";
|
||||
const trainingDifficultyRefreshEvent = "training-difficulty-refresh";
|
||||
const defaultTrainingType = "precision";
|
||||
const defaultUnlockedDifficultyId = "lv1";
|
||||
const trainingTypeMetaMap = {
|
||||
@@ -234,6 +235,8 @@ const selectedDifficultyId = ref(defaultUnlockedDifficultyId);
|
||||
const showTargetPicker = ref(false);
|
||||
const nodesScrollTop = ref(0);
|
||||
const nodesScrollWithAnimation = ref(false);
|
||||
const routeOptions = ref({});
|
||||
const needRefreshProgress = ref(false);
|
||||
|
||||
const difficultyProgressMap = computed(() => {
|
||||
return pageConfig.value?.progressMap || {};
|
||||
@@ -525,11 +528,14 @@ const applyPageState = (options = {}, config) => {
|
||||
});
|
||||
};
|
||||
|
||||
const initPageState = async (options = {}) => {
|
||||
const initPageState = async (options = {}, refreshOptions = {}) => {
|
||||
const { keepCurrent = false } = refreshOptions;
|
||||
const trainingType = resolveTrainingType(options.mode);
|
||||
const fallbackConfig = createEmptyModeConfig(trainingType);
|
||||
|
||||
pageConfig.value = fallbackConfig;
|
||||
if (!keepCurrent) {
|
||||
pageConfig.value = fallbackConfig;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await getTrainingDifficultyListAPI(trainingType);
|
||||
@@ -539,7 +545,9 @@ const initPageState = async (options = {}) => {
|
||||
);
|
||||
} catch (error) {
|
||||
console.log("training difficulty load failed", error);
|
||||
applyPageState(options, fallbackConfig);
|
||||
if (!keepCurrent) {
|
||||
applyPageState(options, fallbackConfig);
|
||||
}
|
||||
uni.showToast({
|
||||
title: "训练难度加载失败",
|
||||
icon: "none",
|
||||
@@ -625,9 +633,30 @@ const handleTargetConfirm = (target) => {
|
||||
});
|
||||
};
|
||||
|
||||
const markProgressRefresh = () => {
|
||||
needRefreshProgress.value = true;
|
||||
};
|
||||
|
||||
onLoad((options = {}) => {
|
||||
routeOptions.value = { ...options };
|
||||
uni.$on(trainingDifficultyRefreshEvent, markProgressRefresh);
|
||||
initPageState(options);
|
||||
});
|
||||
|
||||
onShow(() => {
|
||||
if (!needRefreshProgress.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
needRefreshProgress.value = false;
|
||||
initPageState(routeOptions.value, {
|
||||
keepCurrent: true,
|
||||
});
|
||||
});
|
||||
|
||||
onUnload(() => {
|
||||
uni.$off(trainingDifficultyRefreshEvent, markProgressRefresh);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -36,6 +36,7 @@ const practiseId = ref("");
|
||||
const showGuide = ref(false);
|
||||
const tips = ref("");
|
||||
const targetType = ref(1);
|
||||
const trainingDifficultyRefreshEvent = "training-difficulty-refresh";
|
||||
|
||||
onLoad((options) => {
|
||||
if (options.target) {
|
||||
@@ -63,20 +64,9 @@ async function onReceiveMessage(msg) {
|
||||
}
|
||||
}
|
||||
|
||||
async function onComplete() {
|
||||
const validArrows = (practiseResult.value.details || []).filter(
|
||||
(a) => a.x !== -30 && a.y !== -30
|
||||
);
|
||||
if (validArrows.length === total) {
|
||||
uni.navigateBack();
|
||||
} else {
|
||||
practiseId.value = "";
|
||||
practiseResult.value = {};
|
||||
start.value = false;
|
||||
scores.value = [];
|
||||
const result = await createPractiseAPI(total, 120);
|
||||
if (result) practiseId.value = result.id;
|
||||
}
|
||||
function onComplete() {
|
||||
uni.$emit(trainingDifficultyRefreshEvent);
|
||||
uni.navigateBack();
|
||||
}
|
||||
|
||||
async function onRetry() {
|
||||
|
||||
Reference in New Issue
Block a user