mirror of
https://github.com/LifeArchiveProject/WeChatDataAnalysis.git
synced 2026-02-19 06:10:52 +08:00
improvement(wrapped-ui): 下线 DOS 主题并优化 Wrapped 多主题体验
- 移除 DOS 主题入口、切换器组件与相关样式逻辑,统一主题为 Modern / GameBoy / Win98。 - 新增 WrappedGameboyDither 组件,并在背景与 CRT 叠加层中引入 GameBoy 噪点效果。 - 优化 wrapped 页面视口高度与背景同步逻辑(含 ResizeObserver 与 100dvh 适配),提升桌面容器显示稳定性。 - 调整封面标题与预览位移、回复速度卡片滚动行为等细节,提升主题下视觉与交互一致性。
This commit is contained in:
@@ -186,7 +186,7 @@
|
||||
暂无可展示的排行榜数据。
|
||||
</div>
|
||||
|
||||
<div v-else class="race-scroll mt-4 max-h-[26rem] overflow-auto pr-1">
|
||||
<div v-else class="race-scroll mt-4 max-h-[26rem] overflow-y-auto overflow-x-hidden pr-1">
|
||||
<TransitionGroup
|
||||
name="race"
|
||||
tag="div"
|
||||
@@ -258,8 +258,7 @@ const props = defineProps({
|
||||
|
||||
const { theme } = useWrappedTheme()
|
||||
const isGameboy = computed(() => theme.value === 'gameboy')
|
||||
const isDos = computed(() => theme.value === 'dos')
|
||||
const isRetro = computed(() => isGameboy.value || isDos.value)
|
||||
const isRetro = computed(() => isGameboy.value)
|
||||
|
||||
const nfInt = new Intl.NumberFormat('zh-CN', { maximumFractionDigits: 0 })
|
||||
const formatInt = (n) => nfInt.format(Math.round(Number(n) || 0))
|
||||
@@ -756,6 +755,16 @@ onBeforeUnmount(() => {
|
||||
transition: transform 500ms cubic-bezier(0.22, 1, 0.36, 1) !important;
|
||||
}
|
||||
|
||||
.race-scroll {
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
|
||||
.race-scroll::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.race-move {
|
||||
transition: transform 350ms cubic-bezier(0.22, 1, 0.36, 1) !important;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
<template>
|
||||
<!-- CRT 滤镜叠加层 - 复古主题使用 -->
|
||||
<div class="absolute inset-0 pointer-events-none select-none z-30" aria-hidden="true">
|
||||
<!-- Game Boy: noise 作为最前景层统一覆盖整个画面 -->
|
||||
<WrappedGameboyDither
|
||||
v-if="theme === 'gameboy'"
|
||||
class="opacity-[0.3]"
|
||||
style="filter: contrast(1.16)"
|
||||
:pattern-refresh-interval="1"
|
||||
:pattern-alpha="56"
|
||||
mix-blend-mode="overlay"
|
||||
:pattern-size="256"
|
||||
/>
|
||||
|
||||
<!-- 扫描线 / RGB 子像素 / 闪烁 / 暗角 / 曲率 -->
|
||||
<div class="absolute inset-0 crt-scanlines"></div>
|
||||
<div class="absolute inset-0 crt-rgb-pixels"></div>
|
||||
@@ -8,31 +19,9 @@
|
||||
<div class="absolute inset-0 crt-vignette"></div>
|
||||
<div class="absolute inset-0 crt-curvature"></div>
|
||||
|
||||
<!-- DOS: 语义化光标 -->
|
||||
<div v-if="theme === 'dos'" class="dos-cursor">█</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const { theme } = useWrappedTheme()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* DOS 语义化光标 */
|
||||
.dos-cursor {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
color: #33ff33;
|
||||
font-size: 1.5rem;
|
||||
font-family: var(--font-pixel-10), 'Courier New', monospace;
|
||||
text-shadow: 0 0 8px rgba(51, 255, 51, 0.6);
|
||||
animation: dos-cursor-blink 530ms steps(1) infinite;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
@keyframes dos-cursor-blink {
|
||||
0%, 50% { opacity: 1; }
|
||||
51%, 100% { opacity: 0; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -87,8 +87,7 @@ defineProps({
|
||||
const { theme } = useWrappedTheme()
|
||||
const isWin98 = computed(() => theme.value === 'win98')
|
||||
const isGameboy = computed(() => theme.value === 'gameboy')
|
||||
const isDos = computed(() => theme.value === 'dos')
|
||||
const isCompactSlide = computed(() => isGameboy.value || isDos.value)
|
||||
const isCompactSlide = computed(() => isGameboy.value)
|
||||
|
||||
const slideTitleClass = computed(() => (
|
||||
isCompactSlide.value ? 'text-xl sm:text-2xl' : 'text-2xl sm:text-3xl'
|
||||
@@ -142,39 +141,4 @@ const slideContainerClass = computed(() => (
|
||||
border-color: #306230 !important;
|
||||
}
|
||||
|
||||
/* ========== DOS 主题 ========== */
|
||||
|
||||
/* 卡片背景 */
|
||||
.wrapped-theme-dos .bg-white {
|
||||
background: #0a0a0a !important;
|
||||
border-color: #33ff33 !important;
|
||||
box-shadow: 0 0 10px rgba(51, 255, 51, 0.3);
|
||||
}
|
||||
|
||||
/* 标题 */
|
||||
.wrapped-theme-dos .wrapped-title {
|
||||
color: #33ff33 !important;
|
||||
text-shadow: 0 0 5px #33ff33;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
/* 描述文字 */
|
||||
.wrapped-theme-dos .wrapped-body {
|
||||
color: #22aa22 !important;
|
||||
text-shadow: 0 0 3px #22aa22;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
/* 数字高亮 */
|
||||
.wrapped-theme-dos .wrapped-number {
|
||||
color: #33ff33 !important;
|
||||
text-shadow: 0 0 8px #33ff33;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
/* 边框 */
|
||||
.wrapped-theme-dos .border-\[\#EDEDED\],
|
||||
.wrapped-theme-dos .border-\[\#F3F3F3\] {
|
||||
border-color: #33ff33 !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -131,30 +131,6 @@ const yearOptions = computed(() => {
|
||||
filter: brightness(1.1);
|
||||
}
|
||||
|
||||
/* DOS 特殊样式 */
|
||||
.wrapped-theme-dos .controls-panel {
|
||||
border-radius: 0;
|
||||
border: 2px solid #33ff33;
|
||||
box-shadow: 0 0 10px rgba(51, 255, 51, 0.3);
|
||||
}
|
||||
|
||||
.wrapped-theme-dos .controls-select {
|
||||
border-radius: 0;
|
||||
border: 1px solid #33ff33;
|
||||
text-shadow: 0 0 5px #33ff33;
|
||||
}
|
||||
|
||||
.wrapped-theme-dos .controls-btn {
|
||||
border-radius: 0;
|
||||
background-color: #33ff33;
|
||||
color: #000000;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
.wrapped-theme-dos .controls-btn:hover:not(:disabled) {
|
||||
background-color: #44ff44;
|
||||
}
|
||||
|
||||
/* Win98 特殊样式 */
|
||||
.wrapped-theme-win98 .controls-panel {
|
||||
border-radius: 0;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<!-- Shared backdrop for all "Wrapped" slides (keeps cover + cards visually consistent). -->
|
||||
<!-- Shared backdrop for modern/gameboy "Wrapped" slides (keeps cover + cards visually consistent). -->
|
||||
<div v-if="theme !== 'win98'" class="absolute inset-0 pointer-events-none select-none z-0" aria-hidden="true">
|
||||
<!-- Soft color blobs (brand + warm highlights) -->
|
||||
<div class="absolute -top-24 -left-24 w-80 h-80 bg-[#07C160] opacity-[0.08] rounded-full blur-3xl"></div>
|
||||
@@ -11,8 +11,17 @@
|
||||
class="absolute inset-0 bg-[linear-gradient(rgba(7,193,96,0.05)_1px,transparent_1px),linear-gradient(90deg,rgba(7,193,96,0.05)_1px,transparent_1px)] bg-[size:52px_52px] opacity-[0.28]"
|
||||
></div>
|
||||
|
||||
<!-- Grain/noise: enhanced with dynamic jitter for CRT feel -->
|
||||
<div class="absolute inset-0 wrapped-noise-enhanced opacity-[0.08]"></div>
|
||||
<!-- Grain/noise: gameboy 使用动态 canvas 噪点,其它主题沿用现有纹理 -->
|
||||
<WrappedGameboyDither
|
||||
v-if="theme === 'gameboy'"
|
||||
class="opacity-[0.3]"
|
||||
style="filter: contrast(1.16)"
|
||||
:pattern-refresh-interval="1"
|
||||
:pattern-alpha="56"
|
||||
mix-blend-mode="overlay"
|
||||
:pattern-size="256"
|
||||
/>
|
||||
<div v-else class="absolute inset-0 wrapped-noise-enhanced opacity-[0.08]"></div>
|
||||
|
||||
<!-- Gentle vignette so typography stays readable on textured bg -->
|
||||
<div class="absolute inset-x-0 top-0 h-40 bg-gradient-to-b from-white/50 to-transparent"></div>
|
||||
|
||||
98
frontend/components/wrapped/shared/WrappedGameboyDither.vue
Normal file
98
frontend/components/wrapped/shared/WrappedGameboyDither.vue
Normal file
@@ -0,0 +1,98 @@
|
||||
<template>
|
||||
<canvas
|
||||
ref="grainRef"
|
||||
class="pointer-events-none absolute inset-0 h-full w-full"
|
||||
:style="canvasStyle"
|
||||
aria-hidden="true"
|
||||
></canvas>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
patternRefreshInterval: {
|
||||
type: Number,
|
||||
default: 2
|
||||
},
|
||||
patternAlpha: {
|
||||
type: Number,
|
||||
default: 20
|
||||
},
|
||||
mixBlendMode: {
|
||||
type: String,
|
||||
default: 'multiply'
|
||||
},
|
||||
patternSize: {
|
||||
type: Number,
|
||||
default: 512
|
||||
}
|
||||
})
|
||||
|
||||
const grainRef = ref(null)
|
||||
|
||||
const canvasStyle = computed(() => `image-rendering: pixelated; mix-blend-mode: ${props.mixBlendMode};`)
|
||||
|
||||
let animationId = 0
|
||||
let frame = 0
|
||||
let noiseData
|
||||
let noise32
|
||||
|
||||
const clamp = (value, min, max) => Math.min(max, Math.max(min, value))
|
||||
|
||||
const resize = () => {
|
||||
const canvas = grainRef.value
|
||||
if (!canvas) return
|
||||
const size = Math.max(64, Math.round(props.patternSize))
|
||||
canvas.width = size
|
||||
canvas.height = size
|
||||
}
|
||||
|
||||
const initImageData = (ctx) => {
|
||||
const canvas = grainRef.value
|
||||
if (!canvas) return
|
||||
noiseData = ctx.createImageData(canvas.width, canvas.height)
|
||||
noise32 = new Uint32Array(noiseData.data.buffer)
|
||||
}
|
||||
|
||||
const drawGrain = () => {
|
||||
if (!noise32) return
|
||||
const alpha = clamp(Math.round(props.patternAlpha), 0, 255) << 24
|
||||
for (let i = 0; i < noise32.length; i++) {
|
||||
const value = (Math.random() * 255) | 0
|
||||
noise32[i] = alpha | (value << 16) | (value << 8) | value
|
||||
}
|
||||
}
|
||||
|
||||
const loop = (ctx) => {
|
||||
const refreshEvery = Math.max(1, Math.round(props.patternRefreshInterval))
|
||||
if (frame % refreshEvery === 0) {
|
||||
drawGrain()
|
||||
ctx.putImageData(noiseData, 0, 0)
|
||||
}
|
||||
|
||||
frame++
|
||||
animationId = window.requestAnimationFrame(() => loop(ctx))
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const canvas = grainRef.value
|
||||
if (!canvas) return
|
||||
|
||||
const ctx = canvas.getContext('2d', { alpha: true })
|
||||
if (!ctx) return
|
||||
|
||||
resize()
|
||||
initImageData(ctx)
|
||||
drawGrain()
|
||||
ctx.putImageData(noiseData, 0, 0)
|
||||
loop(ctx)
|
||||
|
||||
window.addEventListener('resize', resize)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', resize)
|
||||
window.cancelAnimationFrame(animationId)
|
||||
})
|
||||
</script>
|
||||
@@ -35,7 +35,7 @@
|
||||
</div>
|
||||
|
||||
<div class="mt-10 sm:mt-14">
|
||||
<h1 class="wrapped-title text-4xl sm:text-6xl text-[#000000e6] leading-[1.05]">
|
||||
<h1 class="wrapped-title text-3xl sm:text-5xl text-[#000000e6] leading-[1.05]">
|
||||
{{ randomTitle.main }}
|
||||
<span class="block mt-3 win98-hero-highlight">
|
||||
{{ randomTitle.highlight }}
|
||||
@@ -70,7 +70,7 @@
|
||||
</div>
|
||||
|
||||
<div class="mt-10 sm:mt-14">
|
||||
<h1 class="wrapped-title text-4xl sm:text-6xl text-[#000000e6] leading-[1.05]">
|
||||
<h1 class="wrapped-title text-3xl sm:text-5xl text-[#000000e6] leading-[1.05]">
|
||||
{{ randomTitle.main }}
|
||||
<span class="block mt-3 text-[#07C160]">
|
||||
{{ randomTitle.highlight }}
|
||||
@@ -404,8 +404,8 @@ const modernPreviewItems = computed(() => {
|
||||
|
||||
const previewStageClass = computed(() => (
|
||||
isGameboy.value
|
||||
? 'w-[500px] h-[360px] translate-x-12 -translate-y-8'
|
||||
: 'w-[620px] h-[420px] translate-x-20 -translate-y-10'
|
||||
? 'w-[500px] h-[360px] translate-x-24 -translate-y-8'
|
||||
: 'w-[620px] h-[420px] translate-x-32 -translate-y-10'
|
||||
))
|
||||
|
||||
const previewViewportClass = computed(() => (
|
||||
|
||||
@@ -10,7 +10,6 @@ const themeSwitcherComponent = computed(() => {
|
||||
const map = {
|
||||
off: resolveComponent('WrappedThemeSwitcherModern'),
|
||||
gameboy: resolveComponent('WrappedThemeSwitcherGameboy'),
|
||||
dos: resolveComponent('WrappedThemeSwitcherDos'),
|
||||
win98: resolveComponent('WrappedThemeSwitcherWin98')
|
||||
}
|
||||
return map[theme.value] || map.off
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
<template>
|
||||
<div class="dos-menu select-none">
|
||||
<!-- DOS 风格功能键菜单栏 -->
|
||||
<div class="dos-menu-bar">
|
||||
<button
|
||||
v-for="(t, idx) in themes"
|
||||
:key="t.value"
|
||||
class="dos-menu-item"
|
||||
:class="{ 'is-active': theme === t.value }"
|
||||
@click="setTheme(t.value)"
|
||||
>
|
||||
<span class="dos-fkey">F{{ idx + 1 }}</span>
|
||||
<span class="dos-label">{{ t.label }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<!-- 状态提示 -->
|
||||
<div class="dos-status">
|
||||
Press F1-F4 to switch theme
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const { theme, setTheme } = useWrappedTheme()
|
||||
|
||||
const themes = [
|
||||
{ value: 'off', label: 'Modern' },
|
||||
{ value: 'gameboy', label: 'GameBoy' },
|
||||
{ value: 'dos', label: 'DOS' },
|
||||
{ value: 'win98', label: 'Win98' }
|
||||
]
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dos-menu {
|
||||
font-family: 'Courier New', 'Consolas', monospace;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.dos-menu-bar {
|
||||
display: flex;
|
||||
background: #000000;
|
||||
border: 1px solid #33ff33;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dos-menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
padding: 4px 8px;
|
||||
background: #000000;
|
||||
color: #33ff33;
|
||||
border: none;
|
||||
border-right: 1px solid #1a5c1a;
|
||||
cursor: pointer;
|
||||
text-shadow: 0 0 5px #33ff33;
|
||||
transition: all 0.1s;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
.dos-menu-item:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.dos-menu-item:hover {
|
||||
background: #0a1a0a;
|
||||
}
|
||||
|
||||
.dos-menu-item.is-active {
|
||||
background: #33ff33;
|
||||
color: #000000;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
.dos-fkey {
|
||||
font-weight: bold;
|
||||
padding: 1px 3px;
|
||||
background: #1a5c1a;
|
||||
color: #33ff33;
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
.dos-menu-item.is-active .dos-fkey {
|
||||
background: #000000;
|
||||
color: #33ff33;
|
||||
}
|
||||
|
||||
.dos-label {
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.dos-status {
|
||||
margin-top: 4px;
|
||||
color: #1a5c1a;
|
||||
font-size: 9px;
|
||||
text-shadow: 0 0 3px #1a5c1a;
|
||||
}
|
||||
</style>
|
||||
@@ -25,7 +25,6 @@ const { theme, setTheme } = useWrappedTheme()
|
||||
const themes = [
|
||||
{ value: 'off', label: 'MODERN' },
|
||||
{ value: 'gameboy', label: 'GAME BOY' },
|
||||
{ value: 'dos', label: 'DOS' },
|
||||
{ value: 'win98', label: 'WIN98' }
|
||||
]
|
||||
|
||||
|
||||
@@ -25,7 +25,6 @@ const { theme, setTheme } = useWrappedTheme()
|
||||
const themes = [
|
||||
{ value: 'off', label: 'Modern' },
|
||||
{ value: 'gameboy', label: 'Game Boy' },
|
||||
{ value: 'dos', label: 'DOS' },
|
||||
{ value: 'win98', label: 'Win98' }
|
||||
]
|
||||
</script>
|
||||
|
||||
@@ -23,7 +23,6 @@ const { theme, setTheme } = useWrappedTheme()
|
||||
const themes = [
|
||||
{ value: 'off', label: 'Modern' },
|
||||
{ value: 'gameboy', label: 'Game Boy' },
|
||||
{ value: 'dos', label: 'DOS' },
|
||||
{ value: 'win98', label: 'Win98' }
|
||||
]
|
||||
</script>
|
||||
|
||||
@@ -19,25 +19,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- DOS 风格 -->
|
||||
<div v-else-if="theme === 'dos'" class="year-dos">
|
||||
<span class="dos-prompt">C:\WRAPPED></span>
|
||||
<span class="dos-label">YEAR:</span>
|
||||
<button
|
||||
class="dos-arrow"
|
||||
:disabled="!canGoPrev"
|
||||
@click="prevYear"
|
||||
aria-label="Previous year"
|
||||
>[-]</button>
|
||||
<span class="dos-value">{{ modelValue }}</span>
|
||||
<button
|
||||
class="dos-arrow"
|
||||
:disabled="!canGoNext"
|
||||
@click="nextYear"
|
||||
aria-label="Next year"
|
||||
>[+]</button>
|
||||
</div>
|
||||
|
||||
<!-- Win98 风格 -->
|
||||
<div v-else-if="theme === 'win98'" class="year-win98">
|
||||
<div class="win98-year-box">
|
||||
@@ -209,57 +190,6 @@ onBeforeUnmount(() => {
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
/* ========== DOS 风格 ========== */
|
||||
.year-dos {
|
||||
font-family: 'Courier New', 'Consolas', monospace;
|
||||
font-size: 11px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
color: #33ff33;
|
||||
text-shadow: 0 0 5px #33ff33;
|
||||
}
|
||||
|
||||
.dos-prompt {
|
||||
color: #1a5c1a;
|
||||
}
|
||||
|
||||
.dos-label {
|
||||
color: #33ff33;
|
||||
}
|
||||
|
||||
.dos-arrow {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #33ff33;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
cursor: pointer;
|
||||
padding: 0 2px;
|
||||
text-shadow: 0 0 5px #33ff33;
|
||||
transition: color 0.1s;
|
||||
}
|
||||
|
||||
.dos-arrow:hover:not(:disabled) {
|
||||
color: #66ff66;
|
||||
text-shadow: 0 0 8px #66ff66;
|
||||
}
|
||||
|
||||
.dos-arrow:disabled {
|
||||
color: #1a5c1a;
|
||||
cursor: not-allowed;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
.dos-value {
|
||||
background: #0a1a0a;
|
||||
padding: 2px 6px;
|
||||
border: 1px solid #1a5c1a;
|
||||
letter-spacing: 1px;
|
||||
min-width: 50px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* ========== Win98 风格 ========== */
|
||||
.year-win98 {
|
||||
font-family: "MS Sans Serif", Tahoma, "Microsoft Sans Serif", "Segoe UI", sans-serif;
|
||||
|
||||
Reference in New Issue
Block a user