diff --git a/frontend/assets/css/tailwind.css b/frontend/assets/css/tailwind.css index 84a4645..96d1bc2 100644 --- a/frontend/assets/css/tailwind.css +++ b/frontend/assets/css/tailwind.css @@ -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); +} diff --git a/frontend/components/wrapped/shared/WrappedCardShell.vue b/frontend/components/wrapped/shared/WrappedCardShell.vue index d86256a..837edaf 100644 --- a/frontend/components/wrapped/shared/WrappedCardShell.vue +++ b/frontend/components/wrapped/shared/WrappedCardShell.vue @@ -1,5 +1,5 @@ @@ -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' +)) diff --git a/frontend/components/wrapped/shared/WrappedDeckBackground.vue b/frontend/components/wrapped/shared/WrappedDeckBackground.vue index d30c074..81bc08d 100644 --- a/frontend/components/wrapped/shared/WrappedDeckBackground.vue +++ b/frontend/components/wrapped/shared/WrappedDeckBackground.vue @@ -1,6 +1,6 @@ + + + diff --git a/frontend/components/wrapped/shared/WrappedHero.vue b/frontend/components/wrapped/shared/WrappedHero.vue index f2b4d39..93669ad 100644 --- a/frontend/components/wrapped/shared/WrappedHero.vue +++ b/frontend/components/wrapped/shared/WrappedHero.vue @@ -9,7 +9,57 @@
@@ -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' } ] diff --git a/frontend/components/wrapped/shared/WrappedThemeSwitcherGameboy.vue b/frontend/components/wrapped/shared/WrappedThemeSwitcherGameboy.vue index 904f6c8..de24aa6 100644 --- a/frontend/components/wrapped/shared/WrappedThemeSwitcherGameboy.vue +++ b/frontend/components/wrapped/shared/WrappedThemeSwitcherGameboy.vue @@ -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) => { diff --git a/frontend/components/wrapped/shared/WrappedThemeSwitcherModern.vue b/frontend/components/wrapped/shared/WrappedThemeSwitcherModern.vue index c65b74d..cd2ebcc 100644 --- a/frontend/components/wrapped/shared/WrappedThemeSwitcherModern.vue +++ b/frontend/components/wrapped/shared/WrappedThemeSwitcherModern.vue @@ -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' } ] diff --git a/frontend/components/wrapped/shared/WrappedThemeSwitcherWin98.vue b/frontend/components/wrapped/shared/WrappedThemeSwitcherWin98.vue new file mode 100644 index 0000000..b3e2e88 --- /dev/null +++ b/frontend/components/wrapped/shared/WrappedThemeSwitcherWin98.vue @@ -0,0 +1,74 @@ + + + + + diff --git a/frontend/components/wrapped/shared/WrappedWin98Taskbar.vue b/frontend/components/wrapped/shared/WrappedWin98Taskbar.vue new file mode 100644 index 0000000..cb8c659 --- /dev/null +++ b/frontend/components/wrapped/shared/WrappedWin98Taskbar.vue @@ -0,0 +1,169 @@ + + + + + diff --git a/frontend/components/wrapped/shared/WrappedYearSelector.vue b/frontend/components/wrapped/shared/WrappedYearSelector.vue index 00e3873..d5425bd 100644 --- a/frontend/components/wrapped/shared/WrappedYearSelector.vue +++ b/frontend/components/wrapped/shared/WrappedYearSelector.vue @@ -38,6 +38,25 @@ >[+]
+ +
+
+ + {{ modelValue }}年 + +
+
+
@@ -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; +} diff --git a/frontend/composables/useWrappedTheme.js b/frontend/composables/useWrappedTheme.js index ed54aeb..e56be6b 100644 --- a/frontend/composables/useWrappedTheme.js +++ b/frontend/composables/useWrappedTheme.js @@ -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') } } diff --git a/frontend/pages/wrapped/index.vue b/frontend/pages/wrapped/index.vue index 25f6880..1f2b2bc 100644 --- a/frontend/pages/wrapped/index.vue +++ b/frontend/pages/wrapped/index.vue @@ -7,7 +7,8 @@ > - + +
@@ -164,6 +165,12 @@
+ + +
@@ -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) diff --git a/frontend/public/assets/images/win98-icons/folder.png b/frontend/public/assets/images/win98-icons/folder.png new file mode 100644 index 0000000..136be6a Binary files /dev/null and b/frontend/public/assets/images/win98-icons/folder.png differ diff --git a/frontend/public/assets/images/win98-icons/mail.png b/frontend/public/assets/images/win98-icons/mail.png new file mode 100644 index 0000000..d5cb3ec Binary files /dev/null and b/frontend/public/assets/images/win98-icons/mail.png differ diff --git a/frontend/public/assets/images/win98-icons/photos.png b/frontend/public/assets/images/win98-icons/photos.png new file mode 100644 index 0000000..8ba4cf6 Binary files /dev/null and b/frontend/public/assets/images/win98-icons/photos.png differ diff --git a/frontend/public/assets/images/win98-icons/recycle.png b/frontend/public/assets/images/win98-icons/recycle.png new file mode 100644 index 0000000..74d32be Binary files /dev/null and b/frontend/public/assets/images/win98-icons/recycle.png differ diff --git a/frontend/public/assets/images/windows-0.png b/frontend/public/assets/images/windows-0.png new file mode 100644 index 0000000..572511d Binary files /dev/null and b/frontend/public/assets/images/windows-0.png differ