mirror of
https://github.com/LifeArchiveProject/WeChatDataAnalysis.git
synced 2026-02-19 14:20:51 +08:00
feat(wrapped-ui): 年度总结页支持懒加载与复古模式,新增概览/字数卡片
- wrapped 页面改为:先拉 meta/年份列表,再按页请求单张卡片,首屏更快 - 新增 Card#0 全局概览页(含图表) - 新增 Card#2 消息字数页(含键盘敲击统计与图表) - 新增复古模式:像素字体资源 + CRT Overlay,支持一键开关 - 调整 shared 组件、types/useApi,更新前端依赖与 lock
This commit is contained in:
24
frontend/components/wrapped/shared/WrappedCRTOverlay.vue
Normal file
24
frontend/components/wrapped/shared/WrappedCRTOverlay.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<!-- CRT 滤镜叠加层 - 模拟老电视机效果 -->
|
||||
<div class="absolute inset-0 pointer-events-none select-none z-30" aria-hidden="true">
|
||||
<!-- 扫描线层 - 水平条纹带滚动动画 -->
|
||||
<div class="absolute inset-0 crt-scanlines"></div>
|
||||
|
||||
<!-- RGB 子像素层 - 模拟 CRT 像素结构 -->
|
||||
<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>
|
||||
// CRT 滤镜叠加层组件
|
||||
// 通过多层叠加实现复古显像管效果,不修改原始背景
|
||||
</script>
|
||||
@@ -3,13 +3,12 @@
|
||||
<div class="px-6 py-5 border-b border-[#F3F3F3]">
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<div class="text-xs font-semibold tracking-[0.22em] text-[#00000066]">
|
||||
CARD {{ String(cardId).padStart(2, '0') }}
|
||||
</div>
|
||||
<h2 class="mt-2 text-xl font-bold text-[#000000e6]">{{ title }}</h2>
|
||||
<p v-if="narrative" class="mt-2 text-sm text-[#7F7F7F]">
|
||||
{{ narrative }}
|
||||
</p>
|
||||
<h2 class="wrapped-title text-xl text-[#000000e6]">{{ title }}</h2>
|
||||
<slot name="narrative">
|
||||
<p v-if="narrative" class="mt-2 wrapped-body text-sm text-[#7F7F7F] whitespace-pre-wrap">
|
||||
{{ narrative }}
|
||||
</p>
|
||||
</slot>
|
||||
</div>
|
||||
<slot name="badge" />
|
||||
</div>
|
||||
@@ -24,13 +23,12 @@
|
||||
<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>
|
||||
<div class="text-xs font-semibold tracking-[0.22em] text-[#00000066]">
|
||||
CARD {{ String(cardId).padStart(2, '0') }}
|
||||
</div>
|
||||
<h2 class="mt-2 text-2xl sm:text-3xl font-bold text-[#000000e6]">{{ title }}</h2>
|
||||
<p v-if="narrative" class="mt-3 text-sm sm:text-base text-[#7F7F7F] max-w-2xl">
|
||||
{{ narrative }}
|
||||
</p>
|
||||
<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>
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
<div class="flex flex-col sm:flex-row gap-3 sm:items-end sm:justify-between">
|
||||
<div class="flex flex-col sm:flex-row gap-3 sm:items-end">
|
||||
<div v-if="showAccount">
|
||||
<div class="text-xs font-medium text-[#00000099] mb-1">账号</div>
|
||||
<div class="wrapped-label text-xs text-[#00000099] mb-1">Account</div>
|
||||
<select
|
||||
class="w-full sm:w-56 px-3 py-2 rounded-lg border border-[#EDEDED] bg-white text-sm focus:outline-none focus:ring-2 focus:ring-[#07C160]"
|
||||
class="w-full sm:w-56 px-3 py-2 rounded-lg border border-[#EDEDED] bg-white text-sm wrapped-body focus:outline-none focus:ring-2 focus:ring-[#07C160]"
|
||||
:disabled="accountsLoading || accounts.length === 0"
|
||||
:value="modelAccount"
|
||||
@change="$emit('update:account', $event.target.value || '')"
|
||||
@@ -17,9 +17,9 @@
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="text-xs font-medium text-[#00000099] mb-1">年份</div>
|
||||
<div class="wrapped-label text-xs text-[#00000099] mb-1">Year</div>
|
||||
<select
|
||||
class="w-full sm:w-40 px-3 py-2 rounded-lg border border-[#EDEDED] bg-white text-sm focus:outline-none focus:ring-2 focus:ring-[#07C160]"
|
||||
class="w-full sm:w-40 px-3 py-2 rounded-lg border border-[#EDEDED] bg-white text-sm wrapped-body focus:outline-none focus:ring-2 focus:ring-[#07C160]"
|
||||
:value="String(modelYear)"
|
||||
@change="$emit('update:year', Number($event.target.value))"
|
||||
>
|
||||
@@ -34,26 +34,26 @@
|
||||
:checked="modelRefresh"
|
||||
@change="$emit('update:refresh', !!$event.target.checked)"
|
||||
/>
|
||||
<span class="text-sm text-[#7F7F7F]">强制刷新(忽略缓存)</span>
|
||||
<span class="wrapped-body text-sm text-[#7F7F7F]">强制刷新(忽略缓存)</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
class="inline-flex items-center justify-center px-4 py-2 rounded-lg bg-[#07C160] text-white text-sm font-medium hover:bg-[#06AD56] disabled:opacity-60 disabled:cursor-not-allowed transition"
|
||||
class="inline-flex items-center justify-center px-4 py-2 rounded-lg bg-[#07C160] text-white text-sm wrapped-label hover:bg-[#06AD56] disabled:opacity-60 disabled:cursor-not-allowed transition"
|
||||
:disabled="loading"
|
||||
@click="$emit('reload')"
|
||||
>
|
||||
<span v-if="!loading">生成报告</span>
|
||||
<span v-else>生成中...</span>
|
||||
<span v-if="!loading">Generate</span>
|
||||
<span v-else>Loading...</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="accountsLoading" class="text-xs text-[#7F7F7F]">
|
||||
<div v-if="accountsLoading" class="wrapped-body text-xs text-[#7F7F7F]">
|
||||
{{ showAccount ? '正在加载账号列表...' : '正在检查数据...' }}
|
||||
</div>
|
||||
<div v-else-if="accounts.length === 0" class="text-xs text-[#B37800]">
|
||||
<div v-else-if="accounts.length === 0" class="wrapped-body text-xs text-[#B37800]">
|
||||
{{ showAccount ? '未发现已解密账号(请先解密数据库)。' : '未发现可用数据(请先解密数据库)。' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -11,8 +11,8 @@
|
||||
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: adds texture without changing the base color -->
|
||||
<div class="absolute inset-0 wrapped-noise opacity-[0.06]"></div>
|
||||
<!-- Grain/noise: enhanced with dynamic jitter for CRT feel -->
|
||||
<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>
|
||||
|
||||
@@ -11,26 +11,25 @@
|
||||
<template v-if="variant === 'slide'">
|
||||
<div class="h-full flex flex-col justify-between">
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div class="text-xs font-semibold tracking-[0.28em] text-[#00000080]">
|
||||
WECHAT · WRAPPED
|
||||
<div class="wrapped-label text-xs text-[#00000080]">
|
||||
WECHAT WRAPPED
|
||||
</div>
|
||||
<div class="text-xs font-semibold tracking-[0.22em] text-[#00000055]">
|
||||
<div class="wrapped-body text-xs text-[#00000055]">
|
||||
年度回望
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-10 sm:mt-14">
|
||||
<h1 class="text-4xl sm:text-6xl font-black tracking-tight text-[#000000e6] leading-[1.05]">
|
||||
把这一年的聊天
|
||||
<h1 class="wrapped-title text-4xl sm:text-6xl text-[#000000e6] leading-[1.05]">
|
||||
{{ randomTitle.main }}
|
||||
<span class="block mt-3 text-[#07C160]">
|
||||
轻轻翻一翻
|
||||
{{ randomTitle.highlight }}
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
<div class="mt-7 sm:mt-9 max-w-2xl">
|
||||
<p class="text-base sm:text-lg text-[#00000080] leading-relaxed">
|
||||
有些问候写在对话框里,有些陪伴藏在深夜里。
|
||||
我们不读取内容,只把时间整理成几张卡片,让你温柔地回望这一年。
|
||||
<p class="wrapped-body text-base sm:text-lg text-[#00000080]">
|
||||
{{ randomSubtitle }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -45,23 +44,23 @@
|
||||
|
||||
<template v-else>
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div class="text-xs font-semibold tracking-[0.28em] text-[#00000080]">
|
||||
WECHAT · WRAPPED
|
||||
<div class="wrapped-label text-xs text-[#00000080]">
|
||||
WECHAT WRAPPED
|
||||
</div>
|
||||
<!-- 年份放到右上角(分享视图不包含账号信息) -->
|
||||
<span
|
||||
class="inline-flex items-center px-3 py-1 rounded-full text-xs bg-[#00000008] text-[#00000099] border border-[#00000010]"
|
||||
class="wrapped-label inline-flex items-center px-3 py-1 rounded-full text-xs bg-[#00000008] text-[#00000099] border border-[#00000010]"
|
||||
>
|
||||
{{ yearText }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="mt-5 sm:mt-7 flex flex-col gap-2">
|
||||
<h1 class="text-3xl sm:text-4xl font-bold text-[#000000e6] leading-tight">
|
||||
<h1 class="wrapped-title text-3xl sm:text-4xl text-[#000000e6] leading-tight">
|
||||
聊天年度总结
|
||||
</h1>
|
||||
<p class="text-sm sm:text-base text-[#7F7F7F] max-w-2xl">
|
||||
从时间里回看你的聊天节奏。第一张卡:年度赛博作息表(24H × 7Days)。
|
||||
<p class="wrapped-body text-sm sm:text-base text-[#7F7F7F] max-w-2xl">
|
||||
从时间里回看你的聊天节奏。第一张卡:年度赛博作息表(24H x 7Days)。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -72,6 +71,120 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// 50 个主标题(主句 + 高亮句)
|
||||
const TITLES = [
|
||||
{ main: '把这一年的聊天', highlight: '轻轻翻一翻' },
|
||||
{ main: '这一年', highlight: '谁陪你说了最多的话' },
|
||||
{ main: '那些深夜的消息', highlight: '都去哪儿了' },
|
||||
{ main: '一年的对话', highlight: '值得被温柔记住' },
|
||||
{ main: '你的聊天记录里', highlight: '藏着这一年' },
|
||||
{ main: '有些人', highlight: '一直在消息列表里陪着你' },
|
||||
{ main: '翻开这一年的', highlight: '对话框' },
|
||||
{ main: '这一年的问候', highlight: '都在这里了' },
|
||||
{ main: '一年又一年', highlight: '聊天框里的人还在吗' },
|
||||
{ main: '回头看看', highlight: '这一年你和谁聊得最多' },
|
||||
{ main: '你今年说得最多的', highlight: '那个人是谁' },
|
||||
{ main: '这一年', highlight: '你在深夜回复过谁' },
|
||||
{ main: '你的消息', highlight: '都发给了谁' },
|
||||
{ main: '谁在等你的消息', highlight: '你又在等谁的' },
|
||||
{ main: '你有多久', highlight: '没和 TA 聊天了' },
|
||||
{ main: '那个秒回你的人', highlight: '还在吗' },
|
||||
{ main: '你置顶的人', highlight: '这一年变过吗' },
|
||||
{ main: '最后一条消息', highlight: '是你发的还是 TA 发的' },
|
||||
{ main: '你的「在吗」', highlight: '都发给了谁' },
|
||||
{ main: '有没有一个人', highlight: '你想聊却没聊' },
|
||||
{ main: '对话框里的', highlight: '四季' },
|
||||
{ main: '字里行间', highlight: '这一年' },
|
||||
{ main: '消息如潮水', highlight: '来了又退' },
|
||||
{ main: '屏幕那头', highlight: '有人亮着灯' },
|
||||
{ main: '打字的手指', highlight: '记得这一年' },
|
||||
{ main: '时间会走', highlight: '对话会留下来' },
|
||||
{ main: '每一条消息', highlight: '都是一次想起' },
|
||||
{ main: '文字落下的地方', highlight: '有人在等' },
|
||||
{ main: '那些发出去的字', highlight: '都有回响吗' },
|
||||
{ main: '对话框亮起的', highlight: '瞬间' },
|
||||
{ main: '这一年的', highlight: '「在吗」和「晚安」' },
|
||||
{ main: '聊着聊着', highlight: '一年就过去了' },
|
||||
{ main: '发出去的消息', highlight: '收到的回复' },
|
||||
{ main: '那些秒回你的人', highlight: '和你秒回的人' },
|
||||
{ main: '置顶的人', highlight: '还是那几个吗' },
|
||||
{ main: '深夜的消息', highlight: '清晨的问候' },
|
||||
{ main: '群聊里的热闹', highlight: '私聊里的安静' },
|
||||
{ main: '表情包发了多少', highlight: '真心话说了几句' },
|
||||
{ main: '已读不回的', highlight: '和秒回的' },
|
||||
{ main: '消息免打扰的', highlight: '和置顶的' },
|
||||
{ main: '总有人', highlight: '在消息那头' },
|
||||
{ main: '每条消息背后', highlight: '都有一个想你的人' },
|
||||
{ main: '感谢这一年', highlight: '愿意听你说话的人' },
|
||||
{ main: '有人找你聊天', highlight: '是件幸运的事' },
|
||||
{ main: '被回复的感觉', highlight: '叫做被在乎' },
|
||||
{ main: '有些陪伴', highlight: '藏在对话框里' },
|
||||
{ main: '谢谢那些', highlight: '愿意等你回复的人' },
|
||||
{ main: '聊天这件小事', highlight: '其实是大事' },
|
||||
{ main: '能说话的人', highlight: '都是重要的人' },
|
||||
{ main: '这一年', highlight: '谢谢你们陪我聊天' },
|
||||
]
|
||||
|
||||
// 50 个副标题
|
||||
const SUBTITLES = [
|
||||
'有些问候写在对话框里,有些陪伴藏在深夜里。',
|
||||
'有些陪伴不说出口,但聊天记录都记得。',
|
||||
'凌晨三点的消息、周末的闲聊、节日的祝福——都在这里。',
|
||||
'一年的对话,浓缩成几张卡片,轻轻回看。',
|
||||
'有些人聊着聊着就淡了,有些人聊着聊着就近了。',
|
||||
'消息可以删除,但陪伴的时间删不掉。',
|
||||
'那些打出来又删掉的字,也算说过了。',
|
||||
'每一次「在吗」,都是一次想念。',
|
||||
'深夜的对话,往往最真。',
|
||||
'感谢每一个愿意听你说话的人。',
|
||||
'一年的时间,几张卡片,一些数字,一点回忆。',
|
||||
'深夜、清晨、周末、假期——你的聊天节奏,藏着生活的样子。',
|
||||
'数字不会说谎,时间不会忘记。',
|
||||
'365 天的对话,整理成几个瞬间。',
|
||||
'时间知道你和谁聊得最多。',
|
||||
'从时间维度,回看你的聊天节奏。',
|
||||
'把一年的对话,整理成可以回望的样子。',
|
||||
'时间会告诉你,谁一直都在。',
|
||||
'这一年的时间,都花在了谁身上。',
|
||||
'日历翻过去了,对话还留着。',
|
||||
'不读内容,只看时间。让数字告诉你,谁一直都在。',
|
||||
'我们只整理时间,不窥探秘密。这是属于你的一年。',
|
||||
'不翻聊天记录,只看时间留下的痕迹。',
|
||||
'这不是监控,是回望。这不是窥探,是整理。',
|
||||
'我们只看时间,不看内容。你的秘密,依然是秘密。',
|
||||
'不读取内容,只呈现时间的痕迹。',
|
||||
'你的对话内容我们不碰,只帮你数数时间。',
|
||||
'隐私是你的,回忆也是你的。',
|
||||
'内容属于你,我们只借用时间。',
|
||||
'安全地回望,温柔地整理。',
|
||||
'从时间里回看你的聊天节奏。',
|
||||
'一些数字,一点回忆。',
|
||||
'简单整理,安静回看。',
|
||||
'不多说,你自己看。',
|
||||
'数字背后,是你的生活。',
|
||||
'几张卡片,一年时光。',
|
||||
'安静地看看这一年。',
|
||||
'让数据说话。',
|
||||
'你的一年,你的节奏。',
|
||||
'回望,不打扰。',
|
||||
'早安、晚安、在吗、好的——这些小词,撑起了一整年。',
|
||||
'工作日的忙碌,周末的闲聊,都在这里了。',
|
||||
'有些群天天响,有些人很少聊,但都是生活的一部分。',
|
||||
'秒回的、已读不回的、消息免打扰的——都是你的选择。',
|
||||
'置顶的那几个人,大概就是最重要的人吧。',
|
||||
'表情包、语音、文字——你更喜欢哪种聊天方式?',
|
||||
'深夜还在聊的,大概都是真朋友。',
|
||||
'节日的群发祝福,和单独发的那条,不一样。',
|
||||
'有些对话很长,有些只有一个表情包,但都算聊过。',
|
||||
'聊天记录里,藏着你这一年的喜怒哀乐。',
|
||||
]
|
||||
|
||||
// 随机选择 - 使用 useState 确保 SSR/CSR 一致,避免 hydration mismatch
|
||||
const titleIndex = useState('wrapped-title-index', () => Math.floor(Math.random() * TITLES.length))
|
||||
const subtitleIndex = useState('wrapped-subtitle-index', () => Math.floor(Math.random() * SUBTITLES.length))
|
||||
const randomTitle = computed(() => TITLES[titleIndex.value])
|
||||
const randomSubtitle = computed(() => SUBTITLES[subtitleIndex.value])
|
||||
|
||||
const props = defineProps({
|
||||
year: { type: Number, required: true },
|
||||
variant: { type: String, default: 'panel' } // 'panel' | 'slide'
|
||||
|
||||
Reference in New Issue
Block a user