feat: 优化移动端学习与证书体验
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
dd233e102b
commit
680e8b97d7
55 files changed
+513
-230
No files matched your search
@@ -1,4 +1,5 @@
|
||||
<script lang="ts" setup>
|
||||
import { onMounted } from 'vue'
|
||||
import { getStaticImage } from '@/utils'
|
||||
import { ImgButton } from '@/components'
|
||||
|
||||
@@ -18,6 +19,13 @@ const emit = defineEmits<{
|
||||
close: []
|
||||
}>()
|
||||
|
||||
const certificateDetailSrc = getStaticImage('components/medal/证书详情')
|
||||
|
||||
onMounted(() => {
|
||||
const img = new Image()
|
||||
img.src = certificateDetailSrc
|
||||
})
|
||||
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
emit('close')
|
||||
@@ -25,14 +33,7 @@ const handleClose = () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Transition
|
||||
enter-active-class="transition-all duration-500 ease-out"
|
||||
enter-from-class="opacity-0 scale-95 blur-[6px]"
|
||||
enter-to-class="opacity-100 scale-100 blur-none"
|
||||
leave-active-class="transition-all duration-300 ease-in"
|
||||
leave-from-class="opacity-100 scale-100 blur-none"
|
||||
leave-to-class="opacity-0 scale-95 blur-[6px]"
|
||||
>
|
||||
<Transition name="certificate-fade">
|
||||
<div
|
||||
v-if="visible"
|
||||
class="absolute inset-0 z-[120] flex items-center justify-center bg-black/45"
|
||||
@@ -41,12 +42,12 @@ const handleClose = () => {
|
||||
<div class="certificate-detail relative text-2xl">
|
||||
<img
|
||||
draggable="false"
|
||||
:src="getStaticImage('components/medal/证书详情')"
|
||||
:src="certificateDetailSrc"
|
||||
class="certificate-detail-img object-contain"
|
||||
alt=""
|
||||
/>
|
||||
<ImgButton
|
||||
class="absolute right-2 top-0 z-10"
|
||||
class="absolute right-0 top-0 z-10"
|
||||
:image="'components/close'"
|
||||
@click="handleClose"
|
||||
/>
|
||||
@@ -58,22 +59,57 @@ const handleClose = () => {
|
||||
<!-- 颁发日期 -->
|
||||
<span class="absolute bottom-[122px] left-[390px]">{{ date }}</span>
|
||||
<!-- 多少位学员 -->
|
||||
<span class="absolute bottom-[468px] left-[948px] text-3xl">
|
||||
<!-- <span class="absolute bottom-[468px] left-[948px] text-3xl">
|
||||
{{ studentNumber }}
|
||||
</span>
|
||||
<span class="absolute bottom-[380px] left-[1070px]">
|
||||
{{ studentNumber }}
|
||||
</span>
|
||||
</span> -->
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.certificate-fade-enter-active {
|
||||
transition: opacity 220ms ease-out;
|
||||
}
|
||||
|
||||
.certificate-fade-leave-active {
|
||||
transition: opacity 160ms ease-in;
|
||||
}
|
||||
|
||||
.certificate-fade-enter-from,
|
||||
.certificate-fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.certificate-fade-enter-active .certificate-detail,
|
||||
.certificate-fade-leave-active .certificate-detail {
|
||||
transition:
|
||||
opacity 220ms ease,
|
||||
transform 220ms cubic-bezier(0.2, 0.8, 0.2, 1);
|
||||
}
|
||||
|
||||
.certificate-fade-enter-from .certificate-detail {
|
||||
opacity: 0;
|
||||
transform: translate3d(0, 36px, 0) scale(0.96);
|
||||
}
|
||||
|
||||
.certificate-fade-enter-to .certificate-detail,
|
||||
.certificate-fade-leave-from .certificate-detail {
|
||||
opacity: 1;
|
||||
transform: translate3d(0, 0, 0) scale(1);
|
||||
}
|
||||
|
||||
.certificate-fade-leave-to .certificate-detail {
|
||||
opacity: 0;
|
||||
transform: translate3d(0, 24px, 0) scale(0.98);
|
||||
}
|
||||
|
||||
.certificate-detail {
|
||||
animation: certificate-detail-pop 650ms cubic-bezier(0.18, 0.9, 0.24, 1.12)
|
||||
both;
|
||||
filter: drop-shadow(0 0 38px rgba(133, 236, 255, 0.85));
|
||||
transform: translateZ(0);
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
|
||||
.certificate-detail::before {
|
||||
@@ -84,42 +120,14 @@ const handleClose = () => {
|
||||
border-radius: 48px;
|
||||
background: radial-gradient(
|
||||
circle,
|
||||
rgba(255, 255, 255, 0.55) 0%,
|
||||
rgba(117, 235, 255, 0.32) 38%,
|
||||
rgba(255, 255, 255, 0.35) 0%,
|
||||
rgba(117, 235, 255, 0.2) 38%,
|
||||
rgba(117, 235, 255, 0) 72%
|
||||
);
|
||||
animation: certificate-glow 1600ms ease-in-out infinite;
|
||||
}
|
||||
|
||||
.certificate-detail-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@keyframes certificate-detail-pop {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(28px) scale(0.9);
|
||||
}
|
||||
58% {
|
||||
opacity: 1;
|
||||
transform: translateY(-8px) scale(1.025);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes certificate-glow {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.45;
|
||||
transform: scale(0.98);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.85;
|
||||
transform: scale(1.04);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -125,6 +125,22 @@ const totalProgress = computed(() =>
|
||||
filmProgress.value,
|
||||
]),
|
||||
)
|
||||
const isRefreshingProgress = ref(false)
|
||||
|
||||
const refreshStudyProgress = async () => {
|
||||
if (isRefreshingProgress.value) return
|
||||
isRefreshingProgress.value = true
|
||||
try {
|
||||
await Promise.allSettled([
|
||||
basicElements.loadBasicElementsStore(),
|
||||
vlog.loadVlogStore(),
|
||||
interview.loadInterviewStore(),
|
||||
film.loadFilmStore(),
|
||||
])
|
||||
} finally {
|
||||
isRefreshingProgress.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const CERTIFICATE_SHOWN_KEY = 'study-progress-certificate-shown'
|
||||
const certificateVisible = ref(false)
|
||||
@@ -140,10 +156,8 @@ const hasAllBadges = computed(() =>
|
||||
const hasCertificate = computed(
|
||||
() => basicElements.guideShown[CERTIFICATE_SHOWN_KEY] === true,
|
||||
)
|
||||
const certificateSrc = computed(() =>
|
||||
getStaticImage(
|
||||
`home/stuy/通关证书-${hasCertificate.value ? '已获取' : '暂未获取'}`,
|
||||
),
|
||||
const certificateSrc = computed(
|
||||
() => `home/stuy/通关证书-${hasCertificate.value ? '已获取' : '暂未获取'}`,
|
||||
)
|
||||
|
||||
const buildItem = (
|
||||
@@ -222,6 +236,12 @@ const closeCertificate = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const openCertificate = () => {
|
||||
if (!hasCertificate.value) return
|
||||
basicElements.ensureCertificate()
|
||||
certificateVisible.value = true
|
||||
}
|
||||
|
||||
watch([visible, hasAllBadges, hasCertificate], scheduleCertificatePopup)
|
||||
|
||||
onBeforeUnmount(clearCertificateTimer)
|
||||
@@ -267,20 +287,39 @@ const certificateInfo = computed(() => basicElements.certificate)
|
||||
@click="handleClose"
|
||||
/>
|
||||
|
||||
<img
|
||||
<ImgButton
|
||||
draggable="false"
|
||||
:src="certificateSrc"
|
||||
:image="certificateSrc"
|
||||
class="absolute -bottom-12 -right-10 object-contain"
|
||||
:class="hasCertificate ? 'cursor-pointer' : 'cursor-default'"
|
||||
alt=""
|
||||
@click="openCertificate"
|
||||
/>
|
||||
|
||||
<div class="absolute left-0 right-0 top-[150px] flex justify-center">
|
||||
<div
|
||||
class="flex items-end pr-10 text-white font-[1000] tracking-widest"
|
||||
:style="{ textShadow: totalTextShadow }"
|
||||
>
|
||||
<span class="text-[56px] leading-none">总进度</span>
|
||||
<span class="text-[56px] leading-none">{{ totalProgress }}%</span>
|
||||
<div class="flex items-center gap-5">
|
||||
<div
|
||||
class="flex items-end text-white font-[1000] tracking-widest"
|
||||
:style="{ textShadow: totalTextShadow }"
|
||||
>
|
||||
<span class="text-[56px] leading-none">总进度</span>
|
||||
<span class="text-[56px] leading-none">{{ totalProgress }}%</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="group flex items-center gap-3 rounded-full border border-[#7df8ff]/80 bg-gradient-to-r from-[#0b48c8]/70 via-[#1678ff]/70 to-[#00bfe8]/70 px-5 py-2 text-[24px] font-[1000] leading-none tracking-widest text-white shadow-[0_0_16px_rgba(67,243,242,0.45),inset_0_0_12px_rgba(255,255,255,0.18)] backdrop-blur-sm transition hover:scale-105 hover:shadow-[0_0_24px_rgba(67,243,242,0.7),inset_0_0_14px_rgba(255,255,255,0.22)] active:scale-95 disabled:cursor-wait disabled:opacity-70"
|
||||
:disabled="isRefreshingProgress"
|
||||
:style="{ textShadow: totalTextShadow }"
|
||||
@click.stop="refreshStudyProgress"
|
||||
>
|
||||
<span
|
||||
class="inline-flex h-7 w-7 items-center justify-center rounded-full border border-white/70 bg-white/15 text-[20px] transition group-hover:rotate-180"
|
||||
:class="isRefreshingProgress ? 'animate-spin' : ''"
|
||||
>
|
||||
↻
|
||||
</span>
|
||||
<span>{{ isRefreshingProgress ? '更新中' : '更新进度' }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in new issue
Block a user