mirror of
https://github.com/LifeArchiveProject/WeChatDataAnalysis.git
synced 2026-02-19 14:20:51 +08:00
feat(wrapped-ui): 新增 Win98 主题与桌面化外观
- 主题系统新增 win98(显示名 Windows 98,快捷键扩展到 F1-F4),并区分 retro(pixel/CRT) 与桌面 GUI 主题 - 年度总结页新增 Win98 桌面背景与底部任务栏(背景色/视口高度适配) - 封面与卡片 slide 形态支持 Win98 窗口外观(title bar/icon/controls) - 主题切换器补充 Win98 选项并新增 Win98 专属切换器 - 新增 Win98 图标资源(Start + 桌面图标)
This commit is contained in:
@@ -1287,3 +1287,208 @@
|
||||
color: #000000 !important;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Theme 3: Windows 95/98 - Win98
|
||||
============================================ */
|
||||
.wrapped-theme-win98 {
|
||||
/* System-like colors (approx.) */
|
||||
--win98-face: #c0c0c0; /* ButtonFace */
|
||||
--win98-hi: #ffffff; /* ButtonHighlight */
|
||||
--win98-shadow: #808080; /* ButtonShadow */
|
||||
--win98-dkshadow: #000000; /* Black */
|
||||
--win98-outline: #dedede; /* Extra light line (common in Win95 clones) */
|
||||
--win98-dither: repeating-conic-gradient(#bdbebd 0% 25%, #ffffff 0% 50%) 50% / 2px 2px;
|
||||
|
||||
--win98-title: #000080; /* ActiveCaption */
|
||||
--win98-title2: #1084d0; /* Caption gradient (approx.) */
|
||||
--win98-title-text: #ffffff;
|
||||
--win98-title-inactive: #7b7d7b;
|
||||
--win98-title-inactive-text: #e6e6e6;
|
||||
|
||||
/* Map to Wrapped theme variables */
|
||||
--wrapped-bg: #ffffff; /* fields/content */
|
||||
--wrapped-card-bg: var(--win98-face);
|
||||
--wrapped-text: #000000;
|
||||
--wrapped-text-secondary: #404040;
|
||||
--wrapped-accent: var(--win98-title);
|
||||
--wrapped-border: var(--win98-shadow);
|
||||
--wrapped-warning: #800000;
|
||||
|
||||
font-family: "MS Sans Serif", Tahoma, "Microsoft Sans Serif", "Segoe UI", sans-serif !important;
|
||||
}
|
||||
|
||||
/* Win98: hard edges */
|
||||
.wrapped-theme-win98 [class*="rounded"] {
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
/* Win98: baseline typography (override Tailwind inline colors via !important) */
|
||||
.wrapped-theme-win98 .wrapped-title {
|
||||
color: var(--wrapped-text) !important;
|
||||
}
|
||||
|
||||
.wrapped-theme-win98 .wrapped-body,
|
||||
.wrapped-theme-win98 .wrapped-label {
|
||||
color: var(--wrapped-text-secondary) !important;
|
||||
}
|
||||
|
||||
.wrapped-theme-win98 .wrapped-number {
|
||||
color: var(--wrapped-accent) !important;
|
||||
}
|
||||
|
||||
/* Win98: generic raised button */
|
||||
.wrapped-theme-win98 button {
|
||||
background: var(--win98-face) !important;
|
||||
color: #000000 !important;
|
||||
border-radius: 0 !important;
|
||||
text-shadow: none !important;
|
||||
|
||||
/* Win95-ish bevel: light (top/left) + shadow (bottom/right) + hard drop */
|
||||
border-top: 1px solid var(--win98-hi) !important;
|
||||
border-left: 1px solid var(--win98-hi) !important;
|
||||
border-right: 1px solid var(--win98-shadow) !important;
|
||||
border-bottom: 1px solid var(--win98-shadow) !important;
|
||||
box-shadow: 1px 1px 0 var(--win98-dkshadow) !important;
|
||||
}
|
||||
|
||||
.wrapped-theme-win98 button:active:not(:disabled) {
|
||||
background: var(--win98-face) !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;
|
||||
}
|
||||
|
||||
.wrapped-theme-win98 button:disabled {
|
||||
opacity: 0.65;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Win98: dotted focus rectangle (classic) */
|
||||
.wrapped-theme-win98 button:focus-visible {
|
||||
outline: 1px dotted var(--win98-dkshadow);
|
||||
outline-offset: -4px;
|
||||
}
|
||||
|
||||
/* Win98 helper: checkered/dither fill for "pressed/toggled" UI (taskbar active, start menu pressed, etc.) */
|
||||
.wrapped-theme-win98 .win98-dither {
|
||||
background: var(--win98-dither) !important;
|
||||
}
|
||||
|
||||
/* Win98: text-ish inputs / selects get a sunken look (avoid checkbox/radio etc.) */
|
||||
.wrapped-theme-win98 textarea,
|
||||
.wrapped-theme-win98 select,
|
||||
.wrapped-theme-win98 input:not([type="checkbox"]):not([type="radio"]):not([type="range"]):not([type="color"]) {
|
||||
background: var(--wrapped-bg) !important;
|
||||
color: var(--wrapped-text) !important;
|
||||
border-radius: 0 !important;
|
||||
border: 1px solid var(--win98-shadow) !important;
|
||||
box-shadow:
|
||||
inset 1px 1px 0 var(--win98-dkshadow),
|
||||
inset -1px -1px 0 var(--win98-hi);
|
||||
}
|
||||
|
||||
/* Win98: window primitives (98.css-like semantics) */
|
||||
.wrapped-theme-win98 .window {
|
||||
background: var(--win98-face);
|
||||
|
||||
/* Stronger Win95-ish window frame (inspired by /win95/assets/css/windows/window.css) */
|
||||
border-top: 2px solid var(--win98-hi);
|
||||
border-left: 2px solid var(--win98-hi);
|
||||
border-right: 1.5px solid var(--win98-shadow);
|
||||
border-bottom: 1.5px solid var(--win98-shadow);
|
||||
box-shadow: 1.5px 1.5px 0 var(--win98-dkshadow);
|
||||
outline: 1px solid var(--win98-outline);
|
||||
}
|
||||
|
||||
.wrapped-theme-win98 .title-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 3px 4px;
|
||||
|
||||
/* Win95 is typically solid; Win98 sometimes gradients. Keep solid by default. */
|
||||
background: var(--win98-title);
|
||||
color: var(--win98-title-text);
|
||||
font-weight: 700;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.wrapped-theme-win98 .title-bar-text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
font-size: 11px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.wrapped-theme-win98 .title-bar-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex: none;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
|
||||
.wrapped-theme-win98 .title-bar-text span {
|
||||
min-width: 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.wrapped-theme-win98 .title-bar-controls {
|
||||
display: inline-flex;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.wrapped-theme-win98 .title-bar-controls button {
|
||||
width: 18px;
|
||||
height: 16px;
|
||||
padding: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.wrapped-theme-win98 .title-bar-controls button::before {
|
||||
content: "";
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Win95-ish glyphs (avoid relying on fonts for the line/square). */
|
||||
.wrapped-theme-win98 .title-bar-controls button[aria-label="Minimize"]::before {
|
||||
width: 6px;
|
||||
height: 2px;
|
||||
background: #000000;
|
||||
transform: translateY(4px);
|
||||
}
|
||||
|
||||
.wrapped-theme-win98 .title-bar-controls button[aria-label="Maximize"]::before {
|
||||
width: 9px;
|
||||
height: 8px;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid #000000;
|
||||
border-top-width: 2px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.wrapped-theme-win98 .title-bar-controls button[aria-label="Close"]::before {
|
||||
content: "×";
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.wrapped-theme-win98 .window-body {
|
||||
padding: 8px;
|
||||
background: var(--win98-face);
|
||||
border-top: 1px solid var(--win98-shadow);
|
||||
box-shadow: inset 0 1px 0 var(--win98-hi);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div v-if="variant === 'panel'" class="bg-white rounded-2xl border border-[#EDEDED] overflow-hidden">
|
||||
<div v-if="variant === 'panel'" class="window 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,24 +20,56 @@
|
||||
|
||||
<!-- Slide 模式:单张卡片占据全页面,背景由外层(年度总结)统一控制 -->
|
||||
<section v-else class="relative h-full w-full overflow-hidden">
|
||||
<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-2xl sm:text-3xl text-[#000000e6]">{{ title }}</h2>
|
||||
<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="mt-3 wrapped-body text-sm sm:text-base text-[#7F7F7F] max-w-2xl whitespace-pre-wrap">
|
||||
<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>
|
||||
<slot name="badge" />
|
||||
</div>
|
||||
|
||||
<div class="mt-6 sm:mt-8 flex-1 flex items-center">
|
||||
<div class="w-full">
|
||||
<slot />
|
||||
<!-- 其他主题:保持原样 -->
|
||||
<template v-else>
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<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>
|
||||
|
||||
<div class="mt-6 sm:mt-8 flex-1 flex items-center">
|
||||
<div class="w-full">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
@@ -49,6 +81,15 @@ defineProps({
|
||||
narrative: { type: String, default: '' },
|
||||
variant: { type: String, default: 'panel' } // 'panel' | 'slide'
|
||||
})
|
||||
|
||||
const { theme } = useWrappedTheme()
|
||||
const isWin98 = computed(() => theme.value === 'win98')
|
||||
|
||||
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'
|
||||
: 'relative h-full max-w-5xl mx-auto px-6 py-10 sm:px-8 sm:py-12 flex flex-col'
|
||||
))
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -154,4 +154,26 @@ const yearOptions = computed(() => {
|
||||
.wrapped-theme-dos .controls-btn:hover:not(:disabled) {
|
||||
background-color: #44ff44;
|
||||
}
|
||||
|
||||
/* Win98 特殊样式 */
|
||||
.wrapped-theme-win98 .controls-panel {
|
||||
border-radius: 0;
|
||||
border: 1px solid #808080;
|
||||
background: #c0c0c0;
|
||||
box-shadow:
|
||||
inset 1px 1px 0 #ffffff,
|
||||
inset -1px -1px 0 #000000;
|
||||
}
|
||||
|
||||
.wrapped-theme-win98 .controls-select {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.wrapped-theme-win98 .controls-btn {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.wrapped-theme-win98 .controls-warning {
|
||||
color: #800000;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<!-- Shared backdrop for all "Wrapped" slides (keeps cover + cards visually consistent). -->
|
||||
<div class="absolute inset-0 pointer-events-none select-none z-0" aria-hidden="true">
|
||||
<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>
|
||||
<div class="absolute -top-24 -right-24 w-96 h-96 bg-[#F2AA00] opacity-[0.06] rounded-full blur-3xl"></div>
|
||||
@@ -18,5 +18,61 @@
|
||||
<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>
|
||||
|
||||
@@ -9,7 +9,57 @@
|
||||
|
||||
<div :class="innerClass">
|
||||
<template v-if="variant === 'slide'">
|
||||
<div class="h-full flex flex-col justify-between">
|
||||
<!-- 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-4xl sm:text-6xl 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="flex items-start justify-between gap-4">
|
||||
<div class="wrapped-label text-xs text-[#00000080]">
|
||||
WECHAT WRAPPED
|
||||
@@ -190,6 +240,9 @@ const props = defineProps({
|
||||
variant: { type: String, default: 'panel' } // 'panel' | 'slide'
|
||||
})
|
||||
|
||||
const { theme } = useWrappedTheme()
|
||||
const isWin98 = computed(() => theme.value === 'win98')
|
||||
|
||||
const yearText = computed(() => `${props.year}年`)
|
||||
|
||||
const rootClass = computed(() => {
|
||||
@@ -199,9 +252,19 @@ const rootClass = computed(() => {
|
||||
: `${base} rounded-2xl border border-[#EDEDED] bg-white`
|
||||
})
|
||||
|
||||
const innerClass = computed(() => (
|
||||
props.variant === 'slide'
|
||||
? 'relative h-full max-w-5xl mx-auto px-6 py-10 sm:px-8 sm:py-12'
|
||||
: 'relative px-6 py-7 sm:px-8 sm:py-9'
|
||||
))
|
||||
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;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -10,7 +10,8 @@ const themeSwitcherComponent = computed(() => {
|
||||
const map = {
|
||||
off: resolveComponent('WrappedThemeSwitcherModern'),
|
||||
gameboy: resolveComponent('WrappedThemeSwitcherGameboy'),
|
||||
dos: resolveComponent('WrappedThemeSwitcherDos')
|
||||
dos: resolveComponent('WrappedThemeSwitcherDos'),
|
||||
win98: resolveComponent('WrappedThemeSwitcherWin98')
|
||||
}
|
||||
return map[theme.value] || map.off
|
||||
})
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
</div>
|
||||
<!-- 状态提示 -->
|
||||
<div class="dos-status">
|
||||
Press F1-F3 to switch theme
|
||||
Press F1-F4 to switch theme
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -26,7 +26,8 @@ const { theme, setTheme } = useWrappedTheme()
|
||||
const themes = [
|
||||
{ value: 'off', label: 'Modern' },
|
||||
{ value: 'gameboy', label: 'GameBoy' },
|
||||
{ value: 'dos', label: 'DOS' }
|
||||
{ value: 'dos', label: 'DOS' },
|
||||
{ value: 'win98', label: 'Win98' }
|
||||
]
|
||||
</script>
|
||||
|
||||
|
||||
@@ -25,7 +25,8 @@ const { theme, setTheme } = useWrappedTheme()
|
||||
const themes = [
|
||||
{ value: 'off', label: 'MODERN' },
|
||||
{ value: 'gameboy', label: 'GAME BOY' },
|
||||
{ value: 'dos', label: 'DOS' }
|
||||
{ value: 'dos', label: 'DOS' },
|
||||
{ value: 'win98', label: 'WIN98' }
|
||||
]
|
||||
|
||||
const selectTheme = (value) => {
|
||||
|
||||
@@ -25,6 +25,7 @@ const { theme, setTheme } = useWrappedTheme()
|
||||
const themes = [
|
||||
{ value: 'off', label: 'Modern' },
|
||||
{ value: 'gameboy', label: 'Game Boy' },
|
||||
{ value: 'dos', label: 'DOS' }
|
||||
{ value: 'dos', label: 'DOS' },
|
||||
{ value: 'win98', label: 'Win98' }
|
||||
]
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
<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: 'dos', label: 'DOS' },
|
||||
{ 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>
|
||||
169
frontend/components/wrapped/shared/WrappedWin98Taskbar.vue
Normal file
169
frontend/components/wrapped/shared/WrappedWin98Taskbar.vue
Normal file
@@ -0,0 +1,169 @@
|
||||
<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>
|
||||
@@ -38,6 +38,25 @@
|
||||
>[+]</button>
|
||||
</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="relative inline-flex items-center">
|
||||
@@ -240,4 +259,57 @@ onBeforeUnmount(() => {
|
||||
min-width: 50px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* ========== 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>
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
*/
|
||||
|
||||
const STORAGE_KEY = 'wrapped-theme'
|
||||
const VALID_THEMES = ['off', 'gameboy', 'dos']
|
||||
const VALID_THEMES = ['off', 'gameboy', 'dos', 'win98']
|
||||
const RETRO_THEMES = new Set(['gameboy', 'dos'])
|
||||
|
||||
// 全局响应式状态(跨组件共享)
|
||||
const theme = ref('off')
|
||||
@@ -54,7 +55,10 @@ export function useWrappedTheme() {
|
||||
// 计算属性:当前主题的 CSS 类名
|
||||
const themeClass = computed(() => {
|
||||
if (theme.value === 'off') return ''
|
||||
return `wrapped-retro wrapped-theme-${theme.value}`
|
||||
// Note: not every non-modern theme is "retro pixel/CRT".
|
||||
// Keep wrapped-retro for themes that rely on pixel/CRT shared styles.
|
||||
const base = RETRO_THEMES.has(theme.value) ? 'wrapped-retro ' : ''
|
||||
return `${base}wrapped-theme-${theme.value}`
|
||||
})
|
||||
|
||||
// 计算属性:主题显示名称
|
||||
@@ -62,7 +66,8 @@ export function useWrappedTheme() {
|
||||
const names = {
|
||||
off: 'Modern',
|
||||
gameboy: 'Game Boy',
|
||||
dos: 'DOS Terminal'
|
||||
dos: 'DOS Terminal',
|
||||
win98: 'Windows 98'
|
||||
}
|
||||
return names[theme.value] || 'Modern'
|
||||
})
|
||||
@@ -88,6 +93,9 @@ export function useWrappedTheme() {
|
||||
} else if (e.key === 'F3') {
|
||||
e.preventDefault()
|
||||
setTheme('dos')
|
||||
} else if (e.key === 'F4') {
|
||||
e.preventDefault()
|
||||
setTheme('win98')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
>
|
||||
<!-- PPT 风格:单张卡片占据全页面,鼠标滚轮切换 -->
|
||||
<WrappedDeckBackground />
|
||||
<WrappedCRTOverlay v-if="isRetro" />
|
||||
<!-- CRT 叠加层仅用于“像素屏/终端”类主题,Win98 等桌面 GUI 主题不应开启 -->
|
||||
<WrappedCRTOverlay v-if="theme === 'gameboy' || theme === 'dos'" />
|
||||
|
||||
<!-- 左上角:刷新 + 复古模式开关 -->
|
||||
<div class="absolute top-6 left-6 z-20 select-none">
|
||||
@@ -164,6 +165,12 @@
|
||||
</WrappedCardShell>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- Win98:底部任务栏 -->
|
||||
<WrappedWin98Taskbar
|
||||
v-if="theme === 'win98'"
|
||||
:title="taskbarTitle"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -219,7 +226,8 @@ let navUnlockTimer = null
|
||||
const THEME_BG = {
|
||||
off: '#F3FFF8', // Modern: 浅绿
|
||||
gameboy: '#9bbc0f', // Game Boy: 亮绿
|
||||
dos: '#0a0a0a' // DOS: 黑色
|
||||
dos: '#0a0a0a', // DOS: 黑色
|
||||
win98: '#008080' // Win98: 经典桌面青色
|
||||
}
|
||||
|
||||
const slides = computed(() => {
|
||||
@@ -229,6 +237,14 @@ const slides = computed(() => {
|
||||
return out
|
||||
})
|
||||
|
||||
const taskbarTitle = computed(() => {
|
||||
if (theme.value !== 'win98') return ''
|
||||
if (activeIndex.value === 0) return `${year.value} WeChat Wrapped`
|
||||
const idx = activeIndex.value - 1
|
||||
const c = report.value?.cards?.[idx]
|
||||
return String(c?.title || 'WeChat Wrapped')
|
||||
})
|
||||
|
||||
const currentBg = computed(() => THEME_BG[theme.value] || THEME_BG.off)
|
||||
|
||||
const slideStyle = computed(() => (
|
||||
@@ -356,8 +372,11 @@ const onTouchEnd = (e) => {
|
||||
const updateViewport = () => {
|
||||
const h = deckEl.value?.clientHeight || window.innerHeight || 0
|
||||
if (!h) return
|
||||
// Reserve space for the Win98 taskbar at the bottom.
|
||||
const offset = theme.value === 'win98' ? 40 : 0
|
||||
const effective = Math.max(0, h - offset)
|
||||
// Avoid endless reflows from 1px rounding errors (especially in Electron).
|
||||
if (Math.abs(viewportHeight.value - h) > 1) viewportHeight.value = h
|
||||
if (Math.abs(viewportHeight.value - effective) > 1) viewportHeight.value = effective
|
||||
}
|
||||
|
||||
const loadAccounts = async () => {
|
||||
@@ -515,6 +534,11 @@ onMounted(async () => {
|
||||
}
|
||||
})
|
||||
|
||||
// Theme switch may change reserved UI space (e.g., Win98 taskbar)
|
||||
watch(theme, () => {
|
||||
updateViewport()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', updateViewport)
|
||||
deckEl.value?.removeEventListener('wheel', onWheel)
|
||||
|
||||
BIN
frontend/public/assets/images/win98-icons/folder.png
Normal file
BIN
frontend/public/assets/images/win98-icons/folder.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 571 B |
BIN
frontend/public/assets/images/win98-icons/mail.png
Normal file
BIN
frontend/public/assets/images/win98-icons/mail.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 696 B |
BIN
frontend/public/assets/images/win98-icons/photos.png
Normal file
BIN
frontend/public/assets/images/win98-icons/photos.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 773 B |
BIN
frontend/public/assets/images/win98-icons/recycle.png
Normal file
BIN
frontend/public/assets/images/win98-icons/recycle.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 388 B |
BIN
frontend/public/assets/images/windows-0.png
Normal file
BIN
frontend/public/assets/images/windows-0.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 458 B |
Reference in New Issue
Block a user