style(no-data): 重做 404 空状态视觉

- 移除 44KB 外链插画 SVG,改为纯内联 SVG + CSS 绘制,消除加载延迟
- 中间 "0" 设计为对称电影胶卷盘,呼应影视主题
- 数字采用主题色渐变,明暗主题自动适配
- 加入入场动画、柔光呼吸晕与漂浮胶片帧装饰,适配 prefers-reduced-motion
- 删除无引用的 404.svg / 404.png 死资源
This commit is contained in:
jxxghp
2026-08-15 12:07:28 +08:00
parent 8ecb4a6203
commit 17f26408eb
2 changed files with 187 additions and 11 deletions
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 41 KiB

+187 -10
View File
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import page404 from '@images/pages/404.svg'
// 国际化
const { t } = useI18n()
@@ -14,13 +14,45 @@ interface Props {
icon?: string
iconColor?: string
}
// 逐字符渲染错误码,"0" 替换为旋转的胶卷盘
const codeChars = computed(() => (props.errorCode || '404').split(''))
</script>
<template>
<div class="no-data-container">
<!-- 图标容器 -->
<div class="icon-wrapper">
<img :src="page404" alt="404" />
<!-- 错误码 -->
<div class="code-wrapper" role="img" :aria-label="props.errorCode || '404'">
<span class="code-glow" aria-hidden="true" />
<template v-for="(char, index) in codeChars" :key="index">
<!-- 胶卷盘替代 "0" -->
<span v-if="char === '0'" class="reel-holder" aria-hidden="true">
<svg class="reel" viewBox="0 0 200 200" fill="none">
<!-- 盘缘 -->
<circle cx="100" cy="100" r="88" class="reel-rim" />
<!-- 盘面 6 个镂孔围绕中心均匀分布 -->
<g class="reel-holes">
<circle cx="148" cy="100" r="18" />
<circle cx="124" cy="141.6" r="18" />
<circle cx="76" cy="141.6" r="18" />
<circle cx="52" cy="100" r="18" />
<circle cx="76" cy="58.4" r="18" />
<circle cx="124" cy="58.4" r="18" />
</g>
<!-- 中心轴孔 -->
<circle cx="100" cy="100" r="11" class="reel-hub" />
</svg>
</span>
<!-- 普通数字 -->
<span v-else class="code-digit" aria-hidden="true">{{ char }}</span>
</template>
<!-- 漂浮的胶片帧装饰 -->
<span class="film-chip chip-a" aria-hidden="true" />
<span class="film-chip chip-b" aria-hidden="true" />
<span class="film-chip chip-c" aria-hidden="true" />
</div>
<!-- 标题 -->
@@ -51,20 +83,110 @@ interface Props {
min-block-size: 300px;
padding-block-start: 3rem;
text-align: center;
animation: rise-in 0.6s ease both;
}
/* 图标样式 */
.icon-wrapper {
/* ---------- 错误码 ---------- */
.code-wrapper {
position: relative;
display: flex;
align-items: center;
justify-content: center;
inline-size: 15rem;
margin-block: 0 1rem;
margin-inline: auto;
gap: 0.06em;
margin-block: 0 1.5rem;
font-size: clamp(5rem, 16vw, 9rem);
font-weight: 800;
line-height: 1;
letter-spacing: 0.02em;
}
/* 文字样式 */
.code-digit {
background: linear-gradient(175deg, rgba(var(--v-theme-primary), 1) 12%, rgba(var(--v-theme-primary), 0.45) 100%);
background-clip: text;
-webkit-background-clip: text;
color: transparent;
text-shadow: none;
}
/* 背景柔光 */
.code-glow {
position: absolute;
inset: -30% -10%;
z-index: -1;
background: radial-gradient(closest-side, rgba(var(--v-theme-primary), 0.16), transparent 70%);
animation: breathe 5s ease-in-out infinite;
}
/* ---------- 胶卷盘 ---------- */
.reel-holder {
display: inline-flex;
align-items: center;
justify-content: center;
block-size: 0.92em;
inline-size: 0.92em;
margin-inline: 0.06em;
}
.reel {
block-size: 100%;
inline-size: 100%;
animation: reel-spin 24s linear infinite;
}
.reel-rim {
fill: rgba(var(--v-theme-primary), 0.08);
stroke: rgba(var(--v-theme-primary), 0.9);
stroke-width: 9;
}
/* 盘面 6 个镂孔:仅用描边,孔内透出背景色 */
.reel-holes circle {
stroke: rgba(var(--v-theme-on-surface), 0.5);
stroke-width: 4;
}
.reel-hub {
fill: rgba(var(--v-theme-primary), 0.95);
}
/* ---------- 漂浮胶片帧 ---------- */
.film-chip {
position: absolute;
border: 1.5px solid rgba(var(--v-theme-on-surface), 0.18);
border-radius: 4px;
/* 上下齿孔用重复渐变模拟 */
background-image:
repeating-linear-gradient(90deg, transparent 0 6px, rgba(var(--v-theme-on-surface), 0.22) 6px 10px);
background-position: center top 3px;
background-repeat: repeat-x;
background-size: 100% 3px;
block-size: 14px;
inline-size: 34px;
opacity: 0.7;
}
.chip-a {
inset-block-start: -8%;
inset-inline-start: 6%;
rotate: -18deg;
animation: drift 6s ease-in-out infinite;
}
.chip-b {
inset-block-end: -6%;
inset-inline-end: 4%;
rotate: 14deg;
animation: drift 7s ease-in-out 1.2s infinite;
}
.chip-c {
inset-block-start: 12%;
inset-inline-end: -6%;
rotate: 26deg;
animation: drift 8s ease-in-out 2.4s infinite;
}
/* ---------- 文字 ---------- */
.error-title {
position: relative;
color: rgba(var(--v-theme-on-surface), 0.95);
@@ -81,6 +203,7 @@ interface Props {
block-size: 3px;
content: '';
inline-size: 60px;
margin-block-start: 0.5rem;
margin-inline: auto;
}
@@ -91,4 +214,58 @@ interface Props {
margin-inline: auto;
max-inline-size: 80%;
}
/* ---------- 动画 ---------- */
@keyframes rise-in {
from {
opacity: 0;
transform: translateY(14px);
}
to {
opacity: 1;
transform: none;
}
}
@keyframes breathe {
0%,
100% {
opacity: 0.7;
transform: scale(1);
}
50% {
opacity: 1;
transform: scale(1.08);
}
}
@keyframes reel-spin {
to {
rotate: 360deg;
}
}
@keyframes drift {
0%,
100% {
transform: translateY(0);
}
50% {
transform: translateY(-8px);
}
}
@media (prefers-reduced-motion: reduce) {
.no-data-container,
.code-glow,
.reel,
.film-chip {
animation: none;
}
}
</style>