From 79da96b2d32663da78a9282b6cafa42e2456e9f4 Mon Sep 17 00:00:00 2001 From: 2977094657 <2977094657@qq.com> Date: Fri, 30 Jan 2026 16:26:52 +0800 Subject: [PATCH] =?UTF-8?q?feat(wrapped-ui):=20=E6=96=B0=E5=A2=9E=E5=B9=B4?= =?UTF-8?q?=E5=BA=A6=E6=80=BB=E7=BB=93=E9=A1=B5=E9=9D=A2=E4=B8=8E=E7=83=AD?= =?UTF-8?q?=E5=8A=9B=E5=9B=BE=E5=8D=A1=E7=89=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 /wrapped PPT 风格滑动浏览(封面 + 卡片页) - 新增 Card#1 组件与 24×7 周-小时热力图可视化 - 首页新增年度总结入口;useApi 增加 getWrappedAnnual;补充 wrapped 背景纹理 --- frontend/assets/css/tailwind.css | 8 + .../wrapped/cards/Card01CyberSchedule.vue | 23 ++ .../wrapped/shared/WrappedCardShell.vue | 54 +++ .../wrapped/shared/WrappedControls.vue | 84 +++++ .../wrapped/shared/WrappedDeckBackground.vue | 22 ++ .../components/wrapped/shared/WrappedHero.vue | 94 +++++ .../visualizations/WeekdayHourHeatmap.vue | 105 ++++++ frontend/composables/useApi.js | 13 +- frontend/pages/index.vue | 8 + frontend/pages/wrapped/index.vue | 336 ++++++++++++++++++ frontend/utils/wrapped/heatmap.js | 47 +++ frontend/utils/wrapped/types.js | 27 ++ 12 files changed, 820 insertions(+), 1 deletion(-) create mode 100644 frontend/components/wrapped/cards/Card01CyberSchedule.vue create mode 100644 frontend/components/wrapped/shared/WrappedCardShell.vue create mode 100644 frontend/components/wrapped/shared/WrappedControls.vue create mode 100644 frontend/components/wrapped/shared/WrappedDeckBackground.vue create mode 100644 frontend/components/wrapped/shared/WrappedHero.vue create mode 100644 frontend/components/wrapped/visualizations/WeekdayHourHeatmap.vue create mode 100644 frontend/pages/wrapped/index.vue create mode 100644 frontend/utils/wrapped/heatmap.js create mode 100644 frontend/utils/wrapped/types.js diff --git a/frontend/assets/css/tailwind.css b/frontend/assets/css/tailwind.css index 6fb94bf..cb6674f 100644 --- a/frontend/assets/css/tailwind.css +++ b/frontend/assets/css/tailwind.css @@ -110,6 +110,14 @@ @apply hover:transform hover:scale-[1.02] transition-all duration-300; } + /* Wrapped (年度总结) 背景纹理 */ + .wrapped-noise { + background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNjAiIGhlaWdodD0iMTYwIj4KICA8ZmlsdGVyIGlkPSJuIj4KICAgIDxmZVR1cmJ1bGVuY2UgdHlwZT0iZnJhY3RhbE5vaXNlIiBiYXNlRnJlcXVlbmN5PSIwLjgiIG51bU9jdGF2ZXM9IjQiIHN0aXRjaFRpbGVzPSJzdGl0Y2giLz4KICAgIDxmZUNvbG9yTWF0cml4IHR5cGU9InNhdHVyYXRlIiB2YWx1ZXM9IjAiLz4KICA8L2ZpbHRlcj4KICA8cmVjdCB3aWR0aD0iMTYwIiBoZWlnaHQ9IjE2MCIgZmlsdGVyPSJ1cmwoI24pIiBvcGFjaXR5PSIwLjQ1Ii8+Cjwvc3ZnPg=="); + background-repeat: repeat; + background-size: 320px 320px; + mix-blend-mode: multiply; + } + /* 输入框样式 */ .input { @apply w-full px-4 py-3 bg-[#f7f8fa] border border-transparent rounded-xl focus:outline-none focus:ring-2 focus:ring-[#07c160] focus:bg-white focus:border-[#07c160] transition-all duration-200; diff --git a/frontend/components/wrapped/cards/Card01CyberSchedule.vue b/frontend/components/wrapped/cards/Card01CyberSchedule.vue new file mode 100644 index 0000000..a45435f --- /dev/null +++ b/frontend/components/wrapped/cards/Card01CyberSchedule.vue @@ -0,0 +1,23 @@ + + + + + 作息规律 + + + + + + + + diff --git a/frontend/components/wrapped/shared/WrappedCardShell.vue b/frontend/components/wrapped/shared/WrappedCardShell.vue new file mode 100644 index 0000000..4be81de --- /dev/null +++ b/frontend/components/wrapped/shared/WrappedCardShell.vue @@ -0,0 +1,54 @@ + + + + + + + CARD {{ String(cardId).padStart(2, '0') }} + + {{ title }} + + {{ narrative }} + + + + + + + + + + + + + + + + + CARD {{ String(cardId).padStart(2, '0') }} + + {{ title }} + + {{ narrative }} + + + + + + + + + + + + + + + diff --git a/frontend/components/wrapped/shared/WrappedControls.vue b/frontend/components/wrapped/shared/WrappedControls.vue new file mode 100644 index 0000000..4de951f --- /dev/null +++ b/frontend/components/wrapped/shared/WrappedControls.vue @@ -0,0 +1,84 @@ + + + + + + + 账号 + + 默认(自动选择) + {{ a }} + + + + + 年份 + + {{ y }}年 + + + + + + 强制刷新(忽略缓存) + + + + + + 生成报告 + 生成中... + + + + + + {{ showAccount ? '正在加载账号列表...' : '正在检查数据...' }} + + + {{ showAccount ? '未发现已解密账号(请先解密数据库)。' : '未发现可用数据(请先解密数据库)。' }} + + + + + + diff --git a/frontend/components/wrapped/shared/WrappedDeckBackground.vue b/frontend/components/wrapped/shared/WrappedDeckBackground.vue new file mode 100644 index 0000000..14172d5 --- /dev/null +++ b/frontend/components/wrapped/shared/WrappedDeckBackground.vue @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/components/wrapped/shared/WrappedHero.vue b/frontend/components/wrapped/shared/WrappedHero.vue new file mode 100644 index 0000000..a2a90ea --- /dev/null +++ b/frontend/components/wrapped/shared/WrappedHero.vue @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + WECHAT · WRAPPED + + + 年度回望 + + + + + + 把这一年的聊天 + + 轻轻翻一翻 + + + + + + 有些问候写在对话框里,有些陪伴藏在深夜里。 + 我们不读取内容,只把时间整理成几张卡片,让你温柔地回望这一年。 + + + + + + + + + + + + + + + + WECHAT · WRAPPED + + + + {{ yearText }} + + + + + + 聊天年度总结 + + + 从时间里回看你的聊天节奏。第一张卡:年度赛博作息表(24H × 7Days)。 + + + + + + + + + + diff --git a/frontend/components/wrapped/visualizations/WeekdayHourHeatmap.vue b/frontend/components/wrapped/visualizations/WeekdayHourHeatmap.vue new file mode 100644 index 0000000..6ae106e --- /dev/null +++ b/frontend/components/wrapped/visualizations/WeekdayHourHeatmap.vue @@ -0,0 +1,105 @@ + + + + + 共 {{ totalMessages }} 条消息 + + 24H × 7Days + + + + + + + + + {{ s }} + + + + + + + + {{ w }} + + + + + + + + + + + + + + + 低 + + + + 高 + + 最大 {{ maxValue }} + + + + + diff --git a/frontend/composables/useApi.js b/frontend/composables/useApi.js index d2bf197..5977948 100644 --- a/frontend/composables/useApi.js +++ b/frontend/composables/useApi.js @@ -311,6 +311,16 @@ export const useApi = () => { if (!exportId) throw new Error('Missing exportId') return await request(`/chat/exports/${encodeURIComponent(String(exportId))}`, { method: 'DELETE' }) } + + // WeChat Wrapped(年度总结) + const getWrappedAnnual = async (params = {}) => { + const query = new URLSearchParams() + if (params && params.year != null) query.set('year', String(params.year)) + if (params && params.account) query.set('account', String(params.account)) + if (params && params.refresh != null) query.set('refresh', String(!!params.refresh)) + const url = '/wrapped/annual' + (query.toString() ? `?${query.toString()}` : '') + return await request(url) + } return { detectWechat, @@ -339,6 +349,7 @@ export const useApi = () => { createChatExport, getChatExport, listChatExports, - cancelChatExport + cancelChatExport, + getWrappedAnnual } } diff --git a/frontend/pages/index.vue b/frontend/pages/index.vue index cf06df5..ea89a4c 100644 --- a/frontend/pages/index.vue +++ b/frontend/pages/index.vue @@ -49,6 +49,14 @@ 聊天预览 + + + + + + 年度总结 + diff --git a/frontend/pages/wrapped/index.vue b/frontend/pages/wrapped/index.vue new file mode 100644 index 0000000..03ef0fe --- /dev/null +++ b/frontend/pages/wrapped/index.vue @@ -0,0 +1,336 @@ + + + + + + + + + + + {{ year }}年 + + + + + + + + + + + + + + + + 生成失败 + {{ error }} + + 提示:请确认已完成解密,并且后端服务正在运行(默认 http://127.0.0.1:8000)。 + + + + { year.value = v }" + @update:account="(v) => { account.value = v }" + @update:refresh="(v) => { refresh.value = v }" + @reload="reload" + /> + + + + + + + + + + + 该卡片暂未实现,后续会逐步补齐。 + + + + + + + + diff --git a/frontend/utils/wrapped/heatmap.js b/frontend/utils/wrapped/heatmap.js new file mode 100644 index 0000000..2d38ade --- /dev/null +++ b/frontend/utils/wrapped/heatmap.js @@ -0,0 +1,47 @@ +// Utilities for Wrapped heatmap rendering. + +export const clamp01 = (v) => { + const n = Number(v) + if (!Number.isFinite(n)) return 0 + if (n < 0) return 0 + if (n > 1) return 1 + return n +} + +export const maxInMatrix = (matrix) => { + if (!Array.isArray(matrix)) return 0 + let m = 0 + for (const row of matrix) { + if (!Array.isArray(row)) continue + for (const v of row) { + const n = Number(v) + if (Number.isFinite(n) && n > m) m = n + } + } + return m +} + +// Color inspired by WeChat green, with a slight "gold" shift on high intensity +// (EchoTrace-style accent) while keeping the overall WeChat vibe. +export const heatColor = (value, max) => { + const v = Number(value) || 0 + const m = Number(max) || 0 + if (!(v > 0) || !(m > 0)) return 'rgba(0,0,0,0.05)' + + // Use sqrt scaling to make low values still visible. + const t = clamp01(Math.sqrt(v / m)) + + // Hue from green (~145) -> yellow-green (~95) + const hue = 145 - 50 * t + const sat = 70 + const light = 92 - 42 * t + return `hsl(${hue.toFixed(1)} ${sat}% ${light.toFixed(1)}%)` +} + +export const formatHourRange = (hour) => { + const h = Number(hour) + if (!Number.isFinite(h)) return '' + const hh = String(h).padStart(2, '0') + return `${hh}:00-${hh}:59` +} + diff --git a/frontend/utils/wrapped/types.js b/frontend/utils/wrapped/types.js new file mode 100644 index 0000000..04c8581 --- /dev/null +++ b/frontend/utils/wrapped/types.js @@ -0,0 +1,27 @@ +// JSDoc types for the Wrapped API (kept in JS to match the current codebase). + +/** + * @typedef {Object} WrappedCardBase + * @property {number} id + * @property {string} title + * @property {'global'} scope + * @property {'A'|'B'|'C'|'D'|'E'} category + * @property {'ok'|'error'} status + * @property {string} kind + * @property {string} narrative + * @property {Record} data + */ + +/** + * @typedef {Object} WrappedAnnualResponse + * @property {string} account + * @property {number} year + * @property {'global'} scope + * @property {string|null} username + * @property {number} generated_at + * @property {boolean} cached + * @property {WrappedCardBase[]} cards + */ + +export {} +
+ {{ narrative }} +
+ 有些问候写在对话框里,有些陪伴藏在深夜里。 + 我们不读取内容,只把时间整理成几张卡片,让你温柔地回望这一年。 +
+ 从时间里回看你的聊天节奏。第一张卡:年度赛博作息表(24H × 7Days)。 +