mirror of
https://github.com/LifeArchiveProject/WeChatDataAnalysis.git
synced 2026-02-20 23:00:50 +08:00
improvement(wrapped): 清理主题系统,Wrapped 固定 Modern
- 移除 useWrappedTheme 与主题切换入口(仅保留 Modern 视觉) - 封面/卡片/年份选择器/热力图等组件去掉 Win98/Gameboy/DOS 分支与样式 - 删除 themedHeatColor、CRT/像素字体相关样式,降低维护成本
This commit is contained in:
@@ -1,27 +0,0 @@
|
||||
<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>
|
||||
<div class="absolute inset-0 crt-flicker"></div>
|
||||
<div class="absolute inset-0 crt-vignette"></div>
|
||||
<div class="absolute inset-0 crt-curvature"></div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const { theme } = useWrappedTheme()
|
||||
</script>
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div v-if="variant === 'panel'" class="window bg-white rounded-2xl border border-[#EDEDED] overflow-hidden">
|
||||
<div v-if="variant === 'panel'" class="bg-white rounded-2xl border border-[#EDEDED] overflow-hidden">
|
||||
<div class="px-6 py-5 border-b border-[#F3F3F3]">
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
@@ -20,58 +20,24 @@
|
||||
|
||||
<!-- Slide 模式:单张卡片占据全页面,背景由外层(年度总结)统一控制 -->
|
||||
<section v-else class="relative h-full w-full overflow-hidden">
|
||||
<div :class="slideContainerClass">
|
||||
<!-- Win98:把整页内容包进一个“窗口” -->
|
||||
<div v-if="isWin98" class="window w-full flex-1 flex flex-col overflow-hidden">
|
||||
<div class="title-bar">
|
||||
<div class="title-bar-text">
|
||||
<img class="title-bar-icon" src="/assets/images/windows-0.png" alt="" aria-hidden="true" />
|
||||
<span>{{ title }}</span>
|
||||
</div>
|
||||
<div class="title-bar-controls" aria-hidden="true">
|
||||
<button type="button" aria-label="Minimize" tabindex="-1"></button>
|
||||
<button type="button" aria-label="Maximize" tabindex="-1"></button>
|
||||
<button type="button" aria-label="Close" tabindex="-1"></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="window-body flex-1 flex flex-col min-h-0">
|
||||
<slot name="narrative">
|
||||
<p v-if="narrative" class="wrapped-body text-sm sm:text-base whitespace-pre-wrap">
|
||||
{{ narrative }}
|
||||
</p>
|
||||
</slot>
|
||||
|
||||
<div class="mt-4 flex-1 min-h-0 overflow-auto">
|
||||
<div class="w-full">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 其他主题:保持原样 -->
|
||||
<template v-else>
|
||||
<div class="relative h-full max-w-5xl mx-auto px-6 py-10 sm:px-8 sm:py-12 flex flex-col">
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<h2 class="wrapped-title text-[#000000e6]" :class="slideTitleClass">{{ title }}</h2>
|
||||
<div :class="slideNarrativeWrapClass">
|
||||
<slot name="narrative">
|
||||
<p v-if="narrative" class="mt-3 wrapped-body text-sm sm:text-base text-[#7F7F7F] max-w-2xl whitespace-pre-wrap">
|
||||
{{ narrative }}
|
||||
</p>
|
||||
</slot>
|
||||
</div>
|
||||
<h2 class="wrapped-title text-2xl sm:text-3xl text-[#000000e6]">{{ title }}</h2>
|
||||
<slot name="narrative">
|
||||
<p v-if="narrative" class="mt-3 wrapped-body text-sm sm:text-base text-[#7F7F7F] max-w-2xl whitespace-pre-wrap">
|
||||
{{ narrative }}
|
||||
</p>
|
||||
</slot>
|
||||
</div>
|
||||
<slot name="badge" />
|
||||
</div>
|
||||
|
||||
<div class="flex-1 flex items-center" :class="slideContentWrapClass">
|
||||
<div class="flex-1 flex items-center mt-6 sm:mt-8">
|
||||
<div class="w-full">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
@@ -83,62 +49,4 @@ defineProps({
|
||||
narrative: { type: String, default: '' },
|
||||
variant: { type: String, default: 'panel' } // 'panel' | 'slide'
|
||||
})
|
||||
|
||||
const { theme } = useWrappedTheme()
|
||||
const isWin98 = computed(() => theme.value === 'win98')
|
||||
const isGameboy = computed(() => theme.value === 'gameboy')
|
||||
const isCompactSlide = computed(() => isGameboy.value)
|
||||
|
||||
const slideTitleClass = computed(() => (
|
||||
isCompactSlide.value ? 'text-xl sm:text-2xl' : 'text-2xl sm:text-3xl'
|
||||
))
|
||||
|
||||
// Keep as a computed so we can tune per-theme spacing later without touching template.
|
||||
const slideNarrativeWrapClass = computed(() => '')
|
||||
|
||||
const slideContentWrapClass = computed(() => (
|
||||
isCompactSlide.value ? 'mt-4 sm:mt-5' : 'mt-6 sm:mt-8'
|
||||
))
|
||||
|
||||
const slideContainerClass = computed(() => (
|
||||
isWin98.value
|
||||
? 'relative h-full max-w-5xl mx-auto px-6 pt-2 pb-4 sm:px-8 sm:pt-3 sm:pb-6 flex flex-col'
|
||||
: (isCompactSlide.value
|
||||
? 'relative h-full max-w-5xl mx-auto px-6 pt-5 pb-6 sm:px-8 sm:pt-6 sm:pb-7 flex flex-col'
|
||||
: 'relative h-full max-w-5xl mx-auto px-6 py-10 sm:px-8 sm:py-12 flex flex-col')
|
||||
))
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* ========== Game Boy 主题 ========== */
|
||||
|
||||
/* 卡片背景 */
|
||||
.wrapped-theme-gameboy .bg-white {
|
||||
background: #9bbc0f !important;
|
||||
border-color: #306230 !important;
|
||||
}
|
||||
|
||||
/* 标题 */
|
||||
.wrapped-theme-gameboy .wrapped-title {
|
||||
color: #0f380f !important;
|
||||
font-family: var(--font-pixel-10), 'Courier New', monospace;
|
||||
}
|
||||
|
||||
/* 描述文字 */
|
||||
.wrapped-theme-gameboy .wrapped-body {
|
||||
color: #306230 !important;
|
||||
}
|
||||
|
||||
/* 数字高亮 */
|
||||
.wrapped-theme-gameboy .wrapped-number {
|
||||
color: #0f380f !important;
|
||||
font-family: var(--font-pixel-10), 'Courier New', monospace;
|
||||
}
|
||||
|
||||
/* 边框 */
|
||||
.wrapped-theme-gameboy .border-\[\#EDEDED\],
|
||||
.wrapped-theme-gameboy .border-\[\#F3F3F3\] {
|
||||
border-color: #306230 !important;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<!-- 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">
|
||||
<!-- Shared backdrop for modern "Wrapped" slides (keeps cover + cards visually consistent). -->
|
||||
<div 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>
|
||||
<div class="absolute -top-24 -right-24 w-96 h-96 bg-[#F2AA00] opacity-[0.06] rounded-full blur-3xl"></div>
|
||||
@@ -11,77 +11,11 @@
|
||||
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: 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>
|
||||
<!-- Grain/noise -->
|
||||
<div 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>
|
||||
<div class="absolute inset-x-0 bottom-0 h-44 bg-gradient-to-t from-white/40 to-transparent"></div>
|
||||
</div>
|
||||
|
||||
<!-- Win98: classic desktop icons (purely decorative) -->
|
||||
<div v-else class="absolute inset-0 pointer-events-none select-none z-0" aria-hidden="true">
|
||||
<div class="win98-desktop-icons">
|
||||
<div v-for="it in desktopIcons" :key="it.label" class="win98-desktop-icon">
|
||||
<img class="win98-desktop-icon__img" :src="it.src" :alt="it.label" />
|
||||
<div class="win98-desktop-icon__label">{{ it.label }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const { theme } = useWrappedTheme()
|
||||
|
||||
const desktopIcons = [
|
||||
{ label: '我的文档', src: '/assets/images/win98-icons/folder.png' },
|
||||
{ label: '图片', src: '/assets/images/win98-icons/photos.png' },
|
||||
{ label: '收件箱', src: '/assets/images/win98-icons/mail.png' },
|
||||
{ label: '回收站', src: '/assets/images/win98-icons/recycle.png' }
|
||||
]
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.win98-desktop-icons {
|
||||
position: absolute;
|
||||
top: 84px; /* leave space for top-left controls */
|
||||
left: 14px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.win98-desktop-icon {
|
||||
width: 74px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.win98-desktop-icon__img {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
|
||||
.win98-desktop-icon__label {
|
||||
max-width: 74px;
|
||||
padding: 0 2px;
|
||||
font-size: 12px;
|
||||
line-height: 1.1;
|
||||
color: #ffffff;
|
||||
text-align: center;
|
||||
text-shadow: 1px 1px 0 #000000;
|
||||
word-break: break-word;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
<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>
|
||||
@@ -9,57 +9,7 @@
|
||||
|
||||
<div :class="innerClass">
|
||||
<template v-if="variant === 'slide'">
|
||||
<!-- Win98:封面也做成一个“窗口” -->
|
||||
<div v-if="isWin98" class="window h-full w-full flex flex-col overflow-hidden">
|
||||
<div class="title-bar">
|
||||
<div class="title-bar-text">
|
||||
<img class="title-bar-icon" src="/assets/images/windows-0.png" alt="" aria-hidden="true" />
|
||||
<span>WECHAT WRAPPED</span>
|
||||
</div>
|
||||
<div class="title-bar-controls" aria-hidden="true">
|
||||
<button type="button" aria-label="Minimize" tabindex="-1"></button>
|
||||
<button type="button" aria-label="Maximize" tabindex="-1"></button>
|
||||
<button type="button" aria-label="Close" tabindex="-1"></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="window-body flex-1 overflow-hidden">
|
||||
<div class="h-full flex flex-col justify-between">
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div class="wrapped-label text-xs text-[#00000080]">
|
||||
WECHAT WRAPPED
|
||||
</div>
|
||||
<div class="wrapped-body text-xs text-[#00000055]">
|
||||
年度回望
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-10 sm:mt-14">
|
||||
<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 }}
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
<div class="mt-7 sm:mt-9 max-w-2xl">
|
||||
<p class="wrapped-body text-base sm:text-lg text-[#00000080]">
|
||||
{{ randomSubtitle }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pb-1">
|
||||
<div class="flex flex-wrap items-center gap-x-4 gap-y-2 text-xs text-[#00000066]">
|
||||
<!-- Intentionally left blank (avoid "feature bullet list" tone on the cover). -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 其他主题:保持原样 -->
|
||||
<div v-else class="h-full flex flex-col justify-between">
|
||||
<div class="h-full flex flex-col justify-between">
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div class="wrapped-label text-xs text-[#00000080]">
|
||||
WECHAT WRAPPED
|
||||
@@ -92,79 +42,41 @@
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="previewQuestions.length > 0 && (isGameboy || isModern)"
|
||||
v-if="previewQuestions.length > 0"
|
||||
class="pointer-events-none absolute bottom-0 right-0 hidden xl:flex items-end"
|
||||
>
|
||||
<div class="pointer-events-auto relative" :class="previewStageClass">
|
||||
<div class="relative" :class="previewViewportClass">
|
||||
<template v-if="isGameboy">
|
||||
<BitsCardSwap
|
||||
:width="previewCardWidth"
|
||||
:height="previewCardHeight"
|
||||
:delay="previewSwapDelay"
|
||||
:card-count="previewQuestions.length"
|
||||
:card-distance="previewCardDistance"
|
||||
:vertical-distance="previewVerticalDistance"
|
||||
:skew-amount="4"
|
||||
easing="elastic"
|
||||
:pause-on-hover="true"
|
||||
<BitsGridMotion
|
||||
:items="modernPreviewItems"
|
||||
gradient-color="rgba(7, 193, 96, 0.24)"
|
||||
:row-count="7"
|
||||
:column-count="8"
|
||||
:scroll-speed="42"
|
||||
:base-offset-x="46"
|
||||
>
|
||||
<template
|
||||
v-for="(previewItem, previewIndex) in previewQuestions"
|
||||
:key="`preview-${previewItem.order}-${previewIndex}`"
|
||||
v-slot:[`card-${previewIndex}`]
|
||||
>
|
||||
<template #item="{ item }">
|
||||
<WrappedCardShell
|
||||
:card-id="previewItem.order"
|
||||
:title="previewItem.title"
|
||||
:card-id="Number(item?.order || 0)"
|
||||
:title="String(item?.title || '年度卡片')"
|
||||
variant="panel"
|
||||
class="h-full w-full"
|
||||
class="h-full w-full preview-grid-shell"
|
||||
>
|
||||
<div
|
||||
class="flex h-[168px] items-center justify-center rounded-xl border border-dashed px-5"
|
||||
:class="previewQuestionPanelClass"
|
||||
>
|
||||
<p class="wrapped-body text-lg leading-relaxed text-center" :class="previewQuestionClass">
|
||||
{{ previewItem.question }}
|
||||
<div class="preview-grid-body">
|
||||
<div class="preview-grid-summary">
|
||||
{{ String(item?.summary || '年度线索') }}
|
||||
</div>
|
||||
<p class="preview-grid-question">
|
||||
{{ String(item?.question || '这一页会揭晓你的聊天答案。') }}
|
||||
</p>
|
||||
<div class="preview-grid-lines" aria-hidden="true">
|
||||
<span></span>
|
||||
<span></span>
|
||||
</div>
|
||||
</div>
|
||||
</WrappedCardShell>
|
||||
</template>
|
||||
</BitsCardSwap>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<BitsGridMotion
|
||||
:items="modernPreviewItems"
|
||||
gradient-color="rgba(7, 193, 96, 0.24)"
|
||||
:row-count="7"
|
||||
:column-count="8"
|
||||
:scroll-speed="42"
|
||||
:base-offset-x="46"
|
||||
>
|
||||
<template #item="{ item }">
|
||||
<WrappedCardShell
|
||||
:card-id="Number(item?.order || 0)"
|
||||
:title="String(item?.title || '年度卡片')"
|
||||
variant="panel"
|
||||
class="h-full w-full preview-grid-shell"
|
||||
>
|
||||
<div class="preview-grid-body">
|
||||
<div class="preview-grid-summary">
|
||||
{{ String(item?.summary || '年度线索') }}
|
||||
</div>
|
||||
<p class="preview-grid-question">
|
||||
{{ String(item?.question || '这一页会揭晓你的聊天答案。') }}
|
||||
</p>
|
||||
<div class="preview-grid-lines" aria-hidden="true">
|
||||
<span></span>
|
||||
<span></span>
|
||||
</div>
|
||||
</div>
|
||||
</WrappedCardShell>
|
||||
</template>
|
||||
</BitsGridMotion>
|
||||
</template>
|
||||
</BitsGridMotion>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -362,11 +274,6 @@ const props = defineProps({
|
||||
cardManifests: { type: Array, default: () => [] }
|
||||
})
|
||||
|
||||
const { theme } = useWrappedTheme()
|
||||
const isWin98 = computed(() => theme.value === 'win98')
|
||||
const isGameboy = computed(() => theme.value === 'gameboy')
|
||||
const isModern = computed(() => theme.value === 'off')
|
||||
|
||||
const previewQuestions = computed(() => {
|
||||
const manifests = Array.isArray(props.cardManifests) ? props.cardManifests : []
|
||||
if (!manifests.length) {
|
||||
@@ -392,10 +299,6 @@ const previewQuestions = computed(() => {
|
||||
})
|
||||
})
|
||||
|
||||
const previewSwapDelay = 4200
|
||||
const previewCardWidth = 420
|
||||
const previewCardHeight = 280
|
||||
|
||||
const modernPreviewItems = computed(() => {
|
||||
if (!previewQuestions.value.length) return []
|
||||
return previewQuestions.value.map((item) => ({
|
||||
@@ -407,15 +310,11 @@ const modernPreviewItems = computed(() => {
|
||||
})
|
||||
|
||||
const previewStageClass = computed(() => (
|
||||
isGameboy.value
|
||||
? 'w-[500px] h-[360px] translate-x-24 -translate-y-8'
|
||||
: 'w-[620px] h-[420px] translate-x-32 -translate-y-10'
|
||||
'w-[620px] h-[420px] translate-x-32 -translate-y-10'
|
||||
))
|
||||
|
||||
const previewViewportClass = computed(() => (
|
||||
isGameboy.value
|
||||
? 'h-[340px] w-[460px]'
|
||||
: 'h-[390px] w-[580px]'
|
||||
'h-[390px] w-[580px]'
|
||||
))
|
||||
|
||||
const previewCardDistance = computed(() => {
|
||||
@@ -428,16 +327,6 @@ const previewVerticalDistance = computed(() => {
|
||||
return total >= 9 ? 10 : total >= 7 ? 11 : total >= 5 ? 14 : 18
|
||||
})
|
||||
|
||||
const previewQuestionClass = computed(() => {
|
||||
if (isWin98.value) return 'text-[#111111]'
|
||||
return 'text-[#1F2937]'
|
||||
})
|
||||
|
||||
const previewQuestionPanelClass = computed(() => {
|
||||
if (isWin98.value) return 'border-[#B7B7B7] bg-[#FFFFFF]'
|
||||
return 'border-[#07C160]/30 bg-[#F7FFFB]'
|
||||
})
|
||||
|
||||
const yearText = computed(() => `${props.year}年`)
|
||||
|
||||
const rootClass = computed(() => {
|
||||
@@ -449,20 +338,11 @@ const rootClass = computed(() => {
|
||||
|
||||
const innerClass = computed(() => {
|
||||
if (props.variant !== 'slide') return 'relative px-6 py-7 sm:px-8 sm:py-9'
|
||||
if (isWin98.value) return 'relative h-full max-w-5xl mx-auto px-6 pt-2 pb-4 sm:px-8 sm:pt-3 sm:pb-6'
|
||||
return 'relative h-full max-w-5xl mx-auto px-6 py-10 sm:px-8 sm:py-12'
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Win98:封面标题的高亮句做成“选中/标题栏”感觉 */
|
||||
.win98-hero-highlight {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
background: #000080;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.preview-grid-shell {
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 10px 24px rgba(7, 193, 96, 0.14);
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
<template>
|
||||
<component :is="themeSwitcherComponent" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const { theme } = useWrappedTheme()
|
||||
|
||||
// 根据当前主题动态选择对应的切换器组件
|
||||
const themeSwitcherComponent = computed(() => {
|
||||
const map = {
|
||||
off: resolveComponent('WrappedThemeSwitcherModern'),
|
||||
gameboy: resolveComponent('WrappedThemeSwitcherGameboy'),
|
||||
win98: resolveComponent('WrappedThemeSwitcherWin98')
|
||||
}
|
||||
return map[theme.value] || map.off
|
||||
})
|
||||
</script>
|
||||
@@ -1,97 +0,0 @@
|
||||
<template>
|
||||
<div class="gameboy-menu select-none">
|
||||
<!-- 像素风格菜单框 -->
|
||||
<div class="gameboy-menu-box">
|
||||
<div class="gameboy-menu-title">SELECT THEME</div>
|
||||
<div class="gameboy-menu-items">
|
||||
<button
|
||||
v-for="t in themes"
|
||||
:key="t.value"
|
||||
class="gameboy-menu-item"
|
||||
:class="{ 'is-active': theme === t.value }"
|
||||
@click="selectTheme(t.value)"
|
||||
>
|
||||
<span class="gameboy-cursor">{{ theme === t.value ? '▶' : ' ' }}</span>
|
||||
<span class="gameboy-label">{{ t.label }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const { theme, setTheme } = useWrappedTheme()
|
||||
|
||||
const themes = [
|
||||
{ value: 'off', label: 'MODERN' },
|
||||
{ value: 'gameboy', label: 'GAME BOY' },
|
||||
{ value: 'win98', label: 'WIN98' }
|
||||
]
|
||||
|
||||
const selectTheme = (value) => {
|
||||
setTheme(value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.gameboy-menu {
|
||||
font-family: 'Press Start 2P', 'Courier New', monospace;
|
||||
font-size: 8px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.gameboy-menu-box {
|
||||
background: #0f380f;
|
||||
border: 3px solid #306230;
|
||||
padding: 8px;
|
||||
box-shadow:
|
||||
inset 2px 2px 0 #9bbc0f,
|
||||
inset -2px -2px 0 #0f380f;
|
||||
}
|
||||
|
||||
.gameboy-menu-title {
|
||||
color: #9bbc0f;
|
||||
text-align: center;
|
||||
margin-bottom: 8px;
|
||||
padding-bottom: 4px;
|
||||
border-bottom: 2px dashed #306230;
|
||||
}
|
||||
|
||||
.gameboy-menu-items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.gameboy-menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 4px 6px;
|
||||
color: #9bbc0f;
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
transition: background-color 0.1s;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
.gameboy-menu-item:hover {
|
||||
background: #306230;
|
||||
}
|
||||
|
||||
.gameboy-menu-item.is-active {
|
||||
color: #8bac0f;
|
||||
}
|
||||
|
||||
.gameboy-cursor {
|
||||
width: 10px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.gameboy-label {
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,30 +0,0 @@
|
||||
<template>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-xs text-[#00000099]">Theme</span>
|
||||
<div class="inline-flex rounded-lg border border-[#EDEDED] overflow-hidden">
|
||||
<button
|
||||
v-for="t in themes"
|
||||
:key="t.value"
|
||||
class="px-3 py-1.5 text-xs wrapped-label transition-colors"
|
||||
:class="[
|
||||
theme === t.value
|
||||
? 'bg-[#07C160] text-white'
|
||||
: 'bg-white text-[#333] hover:bg-[#F5F5F5]'
|
||||
]"
|
||||
@click="setTheme(t.value)"
|
||||
>
|
||||
{{ t.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const { theme, setTheme } = useWrappedTheme()
|
||||
|
||||
const themes = [
|
||||
{ value: 'off', label: 'Modern' },
|
||||
{ value: 'gameboy', label: 'Game Boy' },
|
||||
{ value: 'win98', label: 'Win98' }
|
||||
]
|
||||
</script>
|
||||
@@ -1,73 +0,0 @@
|
||||
<template>
|
||||
<div class="win98-switcher select-none">
|
||||
<span class="win98-switcher__label">Theme</span>
|
||||
|
||||
<div class="win98-switcher__group" role="group" aria-label="Theme switcher">
|
||||
<button
|
||||
v-for="t in themes"
|
||||
:key="t.value"
|
||||
type="button"
|
||||
class="win98-switcher__btn"
|
||||
:class="{ 'is-active': theme === t.value }"
|
||||
@click="setTheme(t.value)"
|
||||
>
|
||||
{{ t.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const { theme, setTheme } = useWrappedTheme()
|
||||
|
||||
const themes = [
|
||||
{ value: 'off', label: 'Modern' },
|
||||
{ value: 'gameboy', label: 'Game Boy' },
|
||||
{ value: 'win98', label: 'Win98' }
|
||||
]
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.win98-switcher {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.win98-switcher__label {
|
||||
color: rgba(0, 0, 0, 0.75);
|
||||
}
|
||||
|
||||
/* Bevel group container */
|
||||
.win98-switcher__group {
|
||||
display: inline-flex;
|
||||
align-items: stretch;
|
||||
padding: 2px;
|
||||
background: #c0c0c0;
|
||||
border: 1px solid #808080;
|
||||
box-shadow:
|
||||
inset 1px 1px 0 #ffffff,
|
||||
inset -1px -1px 0 #000000;
|
||||
}
|
||||
|
||||
.win98-switcher__btn {
|
||||
padding: 4px 10px;
|
||||
background: #c0c0c0;
|
||||
color: #000000;
|
||||
border: 1px solid transparent;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.win98-switcher__btn:hover {
|
||||
filter: brightness(1.03);
|
||||
}
|
||||
|
||||
.win98-switcher__btn.is-active {
|
||||
background: #000080 !important;
|
||||
color: #ffffff !important;
|
||||
border-color: #000080 !important;
|
||||
}
|
||||
</style>
|
||||
@@ -1,169 +0,0 @@
|
||||
<template>
|
||||
<div class="win98-taskbar" @wheel.stop.prevent>
|
||||
<button
|
||||
type="button"
|
||||
class="win98-start"
|
||||
aria-label="Start"
|
||||
:aria-pressed="startPressed ? 'true' : 'false'"
|
||||
@mousedown="startPressed = true"
|
||||
@mouseup="startPressed = false"
|
||||
@mouseleave="startPressed = false"
|
||||
>
|
||||
<img class="win98-start__icon" src="/assets/images/windows-0.png" alt="" aria-hidden="true" />
|
||||
<span class="win98-start__text">Start</span>
|
||||
</button>
|
||||
|
||||
<div class="win98-taskbar__divider" aria-hidden="true"></div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="win98-task"
|
||||
:title="title"
|
||||
tabindex="-1"
|
||||
aria-label="Active window"
|
||||
>
|
||||
{{ title }}
|
||||
</button>
|
||||
|
||||
<div class="win98-taskbar__spacer" aria-hidden="true"></div>
|
||||
|
||||
<div class="win98-tray" aria-label="System tray">
|
||||
<div class="win98-tray__clock" :title="timeText">
|
||||
{{ timeText }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
|
||||
defineProps({
|
||||
title: { type: String, default: 'WeChat Wrapped' }
|
||||
})
|
||||
|
||||
const startPressed = ref(false)
|
||||
const timeText = ref('--:--')
|
||||
let timer = null
|
||||
|
||||
const formatWin98Time = (d) => {
|
||||
try {
|
||||
// Win98 screenshot style: 12-hour + AM/PM
|
||||
return new Intl.DateTimeFormat('en-US', { hour: 'numeric', minute: '2-digit' }).format(d)
|
||||
} catch {
|
||||
const hh = String(d.getHours()).padStart(2, '0')
|
||||
const mm = String(d.getMinutes()).padStart(2, '0')
|
||||
return `${hh}:${mm}`
|
||||
}
|
||||
}
|
||||
|
||||
const updateClock = () => { timeText.value = formatWin98Time(new Date()) }
|
||||
|
||||
onMounted(() => {
|
||||
updateClock()
|
||||
timer = setInterval(updateClock, 30_000)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (timer) clearInterval(timer)
|
||||
timer = null
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.win98-taskbar {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px;
|
||||
background: #c0c0c0;
|
||||
border-top: 2px solid #ffffff;
|
||||
z-index: 40;
|
||||
}
|
||||
|
||||
.win98-start {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
height: 30px;
|
||||
padding: 0 10px 0 8px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.win98-start__icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
|
||||
.win98-start__text {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.win98-taskbar__divider {
|
||||
width: 2px;
|
||||
height: 28px;
|
||||
background: #808080;
|
||||
box-shadow: 1px 0 0 #ffffff;
|
||||
}
|
||||
|
||||
.win98-task {
|
||||
height: 30px;
|
||||
min-width: 160px;
|
||||
max-width: 56vw;
|
||||
padding: 0 10px;
|
||||
font-weight: 400;
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.win98-task {
|
||||
/* Active window task button: depressed + dither fill (Win95-ish) */
|
||||
background: var(--win98-dither) !important;
|
||||
box-shadow: none !important;
|
||||
border-top: 1px solid var(--win98-dkshadow) !important;
|
||||
border-left: 1px solid var(--win98-dkshadow) !important;
|
||||
border-right: 1px solid var(--win98-hi) !important;
|
||||
border-bottom: 1px solid var(--win98-hi) !important;
|
||||
}
|
||||
|
||||
.win98-start[aria-pressed="true"] {
|
||||
/* Start button pressed: depressed + dither */
|
||||
background: var(--win98-dither) !important;
|
||||
box-shadow: none !important;
|
||||
border-top: 1px solid var(--win98-dkshadow) !important;
|
||||
border-left: 1px solid var(--win98-dkshadow) !important;
|
||||
border-right: 1px solid var(--win98-hi) !important;
|
||||
border-bottom: 1px solid var(--win98-hi) !important;
|
||||
}
|
||||
|
||||
.win98-taskbar__spacer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.win98-tray {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
height: 30px;
|
||||
padding: 0 8px;
|
||||
background: #c0c0c0;
|
||||
border-top: 1px solid var(--win98-shadow);
|
||||
border-left: 1px solid var(--win98-shadow);
|
||||
border-right: 1px solid var(--win98-hi);
|
||||
border-bottom: 1px solid var(--win98-hi);
|
||||
}
|
||||
|
||||
.win98-tray__clock {
|
||||
font-size: 11px;
|
||||
color: #000000;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
@@ -1,45 +1,6 @@
|
||||
<template>
|
||||
<div class="year-selector" :class="selectorClass">
|
||||
<!-- Game Boy 风格 -->
|
||||
<div v-if="theme === 'gameboy'" class="year-gameboy">
|
||||
<div class="gameboy-year-box">
|
||||
<button
|
||||
class="gameboy-arrow"
|
||||
:disabled="!canGoPrev"
|
||||
@click="prevYear"
|
||||
aria-label="Previous year"
|
||||
>◀</button>
|
||||
<span class="gameboy-year-value">{{ modelValue }}</span>
|
||||
<button
|
||||
class="gameboy-arrow"
|
||||
:disabled="!canGoNext"
|
||||
@click="nextYear"
|
||||
aria-label="Next year"
|
||||
>▶</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Win98 风格 -->
|
||||
<div v-else-if="theme === 'win98'" class="year-win98">
|
||||
<div class="win98-year-box">
|
||||
<button
|
||||
class="win98-arrow"
|
||||
:disabled="!canGoPrev"
|
||||
@click="prevYear"
|
||||
aria-label="Previous year"
|
||||
>◄</button>
|
||||
<span class="win98-year-value">{{ modelValue }}年</span>
|
||||
<button
|
||||
class="win98-arrow"
|
||||
:disabled="!canGoNext"
|
||||
@click="nextYear"
|
||||
aria-label="Next year"
|
||||
>►</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modern 风格:下拉菜单(默认) -->
|
||||
<div v-else class="year-modern">
|
||||
<div class="year-selector">
|
||||
<div class="year-modern">
|
||||
<div class="relative inline-flex items-center">
|
||||
<select
|
||||
class="appearance-none bg-transparent pr-5 pl-0 py-0.5 rounded-md wrapped-label text-xs text-[#00000066] text-right focus:outline-none focus-visible:ring-2 focus-visible:ring-[#07C160]/30 hover:bg-[#000000]/5 transition disabled:opacity-70 disabled:cursor-default"
|
||||
@@ -81,8 +42,6 @@ const props = defineProps({
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const { theme } = useWrappedTheme()
|
||||
|
||||
const currentIndex = computed(() => props.years.indexOf(props.modelValue))
|
||||
const canGoPrev = computed(() => currentIndex.value > 0)
|
||||
const canGoNext = computed(() => currentIndex.value < props.years.length - 1)
|
||||
@@ -106,10 +65,6 @@ const onSelectChange = (e) => {
|
||||
}
|
||||
}
|
||||
|
||||
const selectorClass = computed(() => {
|
||||
return `year-selector-${theme.value}`
|
||||
})
|
||||
|
||||
// 全局左右键切换年份(所有主题)
|
||||
const handleKeydown = (e) => {
|
||||
if (props.years.length <= 1) return
|
||||
@@ -144,102 +99,4 @@ onBeforeUnmount(() => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* ========== Game Boy 风格 ========== */
|
||||
.year-gameboy {
|
||||
font-family: 'Press Start 2P', 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.gameboy-year-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
background: #0f380f;
|
||||
border: 3px solid #306230;
|
||||
padding: 6px 8px;
|
||||
box-shadow:
|
||||
inset 2px 2px 0 #9bbc0f,
|
||||
inset -2px -2px 0 #0f380f;
|
||||
}
|
||||
|
||||
.gameboy-arrow {
|
||||
background: #306230;
|
||||
border: none;
|
||||
color: #9bbc0f;
|
||||
font-size: 8px;
|
||||
padding: 4px 6px;
|
||||
cursor: pointer;
|
||||
transition: background 0.1s;
|
||||
}
|
||||
|
||||
.gameboy-arrow:hover:not(:disabled) {
|
||||
background: #8bac0f;
|
||||
color: #0f380f;
|
||||
}
|
||||
|
||||
.gameboy-arrow:disabled {
|
||||
opacity: 0.3;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.gameboy-year-value {
|
||||
color: #9bbc0f;
|
||||
font-size: 10px;
|
||||
min-width: 40px;
|
||||
text-align: center;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
/* ========== Win98 风格 ========== */
|
||||
.year-win98 {
|
||||
font-family: "MS Sans Serif", Tahoma, "Microsoft Sans Serif", "Segoe UI", sans-serif;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.win98-year-box {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
background: #c0c0c0;
|
||||
padding: 2px;
|
||||
border: 1px solid #808080;
|
||||
box-shadow:
|
||||
inset 1px 1px 0 #ffffff,
|
||||
inset -1px -1px 0 #000000;
|
||||
}
|
||||
|
||||
.win98-year-value {
|
||||
min-width: 62px;
|
||||
text-align: center;
|
||||
color: #000000;
|
||||
padding: 2px 8px;
|
||||
background: #ffffff;
|
||||
border: 1px solid #808080;
|
||||
box-shadow:
|
||||
inset 1px 1px 0 #000000,
|
||||
inset -1px -1px 0 #ffffff;
|
||||
}
|
||||
|
||||
.win98-arrow {
|
||||
width: 24px;
|
||||
height: 22px;
|
||||
background: #c0c0c0;
|
||||
border: 1px solid #808080;
|
||||
box-shadow:
|
||||
inset 1px 1px 0 #ffffff,
|
||||
inset -1px -1px 0 #000000;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.win98-arrow:active:not(:disabled) {
|
||||
box-shadow:
|
||||
inset 1px 1px 0 #000000,
|
||||
inset -1px -1px 0 #ffffff;
|
||||
}
|
||||
|
||||
.win98-arrow:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user