forked from zhongyidian/super_video
feat: 完善模块引导与学习进度体验
统一引导提示的持久化控制,补充学习进度入口与模块资源,避免用户返回页面时重复触发提示。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
fc9633f371
commit
6f8252472f
114 files changed
+2758
-1798
No files matched your search
@@ -0,0 +1,159 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue'
|
||||
import { getStaticImage } from '@/utils'
|
||||
import type { AnchoredProgressBarProps, ProgressAnchor } from './types'
|
||||
|
||||
defineOptions({
|
||||
name: 'AnchoredProgressBar'
|
||||
})
|
||||
|
||||
const props = withDefaults(defineProps<AnchoredProgressBarProps>(), {
|
||||
min: 0,
|
||||
max: 100,
|
||||
showPercent: true,
|
||||
nodeImage: '',
|
||||
trackClass: 'bg-black/20',
|
||||
fillClass: 'bg-[#31effa] shadow-[0_0_26px_rgba(49,239,250,0.95)]',
|
||||
markerClass: 'bg-white shadow-[0_0_12px_rgba(255,255,255,0.9)]',
|
||||
labelClass: 'text-white font-[1000] tracking-wider',
|
||||
childLabelClass: 'text-[#43f3f2] font-[1000] tracking-wider',
|
||||
percentClass: 'text-white font-[1000] italic',
|
||||
labelTop: '0%',
|
||||
childLabelTop: '26%',
|
||||
barTop: '56%',
|
||||
barHeight: '26%',
|
||||
markerTop: '48%',
|
||||
markerHeight: '42%',
|
||||
markerWidth: '0.55%',
|
||||
childMarkerTop: '56%',
|
||||
childMarkerHeight: '26%',
|
||||
childMarkerWidth: '0.22%',
|
||||
nodeTop: '39%',
|
||||
nodeHeight: '58%',
|
||||
percentTop: '76%',
|
||||
labelStyle: () => ({}),
|
||||
childLabelStyle: () => ({}),
|
||||
percentStyle: () => ({})
|
||||
})
|
||||
|
||||
const clamp = (value: number, min: number, max: number) => Math.min(Math.max(value, min), max)
|
||||
|
||||
const toPercent = (value: number) => {
|
||||
const range = props.max - props.min
|
||||
if (range <= 0) return 0
|
||||
return clamp(((value - props.min) / range) * 100, 0, 100)
|
||||
}
|
||||
|
||||
const progressPercent = computed(() => toPercent(props.progress))
|
||||
const nodeSrc = computed(() => (props.nodeImage ? getStaticImage(props.nodeImage) : ''))
|
||||
const markerAnchors = computed(() => {
|
||||
const anchorsByValue = new Map<number, ProgressAnchor & { markerType: 'major' | 'minor' }>()
|
||||
|
||||
props.anchors.forEach(anchor => {
|
||||
anchorsByValue.set(toPercent(anchor.value), { ...anchor, markerType: 'major' })
|
||||
anchor.children?.forEach(child => {
|
||||
const left = toPercent(child.value)
|
||||
if (!anchorsByValue.has(left)) {
|
||||
anchorsByValue.set(left, { ...child, markerType: 'minor' })
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
anchorsByValue.set(0, { id: 'start', label: '', value: props.min, markerType: 'major' })
|
||||
anchorsByValue.set(100, { id: 'end', label: '', value: props.max, markerType: 'major' })
|
||||
|
||||
return [...anchorsByValue.entries()]
|
||||
.sort(([leftA], [leftB]) => leftA - leftB)
|
||||
.map(([left, anchor]) => ({ ...anchor, left }))
|
||||
})
|
||||
|
||||
const anchorWithPercent = computed(() =>
|
||||
props.anchors.map(anchor => ({
|
||||
...anchor,
|
||||
left: toPercent(anchor.value),
|
||||
children: anchor.children?.map(child => ({ ...child, left: toPercent(child.value) }))
|
||||
}))
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relative h-full w-full">
|
||||
<template v-for="anchor in anchorWithPercent" :key="anchor.id">
|
||||
<div
|
||||
v-if="anchor.label"
|
||||
class="absolute whitespace-nowrap font-[1000]"
|
||||
:class="[labelClass, anchor.left >= 98 ? '-translate-x-full' : '-translate-x-0']"
|
||||
:style="{
|
||||
...labelStyle,
|
||||
top: anchor.labelPosition === 'bottom' ? percentTop : labelTop,
|
||||
left: `${anchor.left}%`
|
||||
}"
|
||||
>
|
||||
{{ anchor.label }}
|
||||
</div>
|
||||
<div
|
||||
v-for="child in anchor.children"
|
||||
:key="child.id"
|
||||
class="absolute -translate-x-1/2 whitespace-nowrap font-bold leading-none"
|
||||
:class="childLabelClass"
|
||||
:style="{ ...childLabelStyle, top: childLabelTop, left: `${child.left}%` }"
|
||||
>
|
||||
{{ child.label }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div
|
||||
class="absolute left-0 w-full"
|
||||
:class="trackClass"
|
||||
:style="{ top: barTop, height: barHeight }"
|
||||
></div>
|
||||
<div
|
||||
class="absolute left-0 rounded-full transition-all duration-700 ease-out"
|
||||
:class="fillClass"
|
||||
:style="{ top: barTop, height: barHeight, width: `${progressPercent}%` }"
|
||||
></div>
|
||||
|
||||
<div
|
||||
v-for="anchor in markerAnchors"
|
||||
:key="anchor.id"
|
||||
class="absolute rounded-full"
|
||||
:class="markerClass"
|
||||
:style="{
|
||||
top: anchor.markerType === 'major' ? markerTop : childMarkerTop,
|
||||
height: anchor.markerType === 'major' ? markerHeight : childMarkerHeight,
|
||||
width: anchor.markerType === 'major' ? markerWidth : childMarkerWidth,
|
||||
left: `${anchor.left}%`
|
||||
}"
|
||||
></div>
|
||||
|
||||
<img
|
||||
v-if="nodeSrc"
|
||||
class="absolute w-auto -translate-x-1/2 transition-all duration-700 ease-out"
|
||||
:src="nodeSrc"
|
||||
:style="{ top: nodeTop, height: nodeHeight, left: `${progressPercent}%` }"
|
||||
alt=""
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
class="absolute aspect-square -translate-x-1/2 rounded-full border-[6px] border-[#31effa] bg-white/20 transition-all duration-700 ease-out"
|
||||
:style="{ top: nodeTop, height: nodeHeight, left: `${progressPercent}%` }"
|
||||
></div>
|
||||
|
||||
<div
|
||||
v-if="showPercent"
|
||||
class="absolute -translate-x-1/2 whitespace-nowrap"
|
||||
:class="percentClass"
|
||||
:style="{ ...percentStyle, top: percentTop, left: `${progressPercent}%` }"
|
||||
>
|
||||
{{ Math.round(progressPercent) }}%
|
||||
</div>
|
||||
<div
|
||||
v-if="showPercent"
|
||||
class="absolute right-0"
|
||||
:class="percentClass"
|
||||
:style="{ ...percentStyle, top: percentTop }"
|
||||
>
|
||||
100%
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default as AnchoredProgressBar } from './AnchoredProgressBar.vue'
|
||||
export type * from './types'
|
||||
@@ -0,0 +1,38 @@
|
||||
export type ProgressAnchor = {
|
||||
id: string
|
||||
label: string
|
||||
value: number
|
||||
labelPosition?: 'top' | 'bottom'
|
||||
children?: ProgressAnchor[]
|
||||
}
|
||||
|
||||
export type AnchoredProgressBarProps = {
|
||||
progress: number
|
||||
anchors: ProgressAnchor[]
|
||||
min?: number
|
||||
max?: number
|
||||
showPercent?: boolean
|
||||
nodeImage?: string
|
||||
trackClass?: string
|
||||
fillClass?: string
|
||||
markerClass?: string
|
||||
labelClass?: string
|
||||
childLabelClass?: string
|
||||
percentClass?: string
|
||||
labelTop?: string
|
||||
childLabelTop?: string
|
||||
barTop?: string
|
||||
barHeight?: string
|
||||
markerTop?: string
|
||||
markerHeight?: string
|
||||
markerWidth?: string
|
||||
childMarkerTop?: string
|
||||
childMarkerHeight?: string
|
||||
childMarkerWidth?: string
|
||||
nodeTop?: string
|
||||
nodeHeight?: string
|
||||
percentTop?: string
|
||||
labelStyle?: Record<string, string>
|
||||
childLabelStyle?: Record<string, string>
|
||||
percentStyle?: Record<string, string>
|
||||
}
|
||||
@@ -27,7 +27,7 @@
|
||||
|
||||
<img
|
||||
v-else-if="moduleName === 'interview'"
|
||||
:src="getStaticImage('midterm/中期拍摄')"
|
||||
:src="getStaticImage('interview/制作变焦镜-讲述我的育人故事')"
|
||||
alt="制作变焦镜-我的志愿者手记"
|
||||
class="title-image"
|
||||
/>
|
||||
|
||||
@@ -10,6 +10,7 @@ export * from './Guide'
|
||||
|
||||
// 进度条组件
|
||||
export * from './ProgressBar'
|
||||
export * from './AnchoredProgressBar'
|
||||
|
||||
// 切换组件
|
||||
export * from './Switcher'
|
||||
|
||||
Reference in new issue
Block a user