mirror of
https://github.com/LifeArchiveProject/WeChatDataAnalysis.git
synced 2026-06-18 15:54:08 +08:00
Compare commits
2 Commits
@@ -14,6 +14,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Derive version from tag
|
||||
shell: pwsh
|
||||
@@ -26,6 +28,67 @@ jobs:
|
||||
"TAG_NAME=$tag" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
|
||||
"VERSION=$version" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
|
||||
|
||||
- name: Generate release notes from commits
|
||||
shell: pwsh
|
||||
run: |
|
||||
git fetch --force --tags
|
||||
|
||||
$tag = $env:TAG_NAME
|
||||
if ([string]::IsNullOrWhiteSpace($tag)) {
|
||||
throw "TAG_NAME is empty"
|
||||
}
|
||||
|
||||
$repo = "${{ github.repository }}"
|
||||
|
||||
$prev = ""
|
||||
try {
|
||||
$commit = (git rev-list -n 1 $tag).Trim()
|
||||
if (-not [string]::IsNullOrWhiteSpace($commit)) {
|
||||
$prev = (git describe --tags --abbrev=0 "$commit^" 2>$null).Trim()
|
||||
}
|
||||
} catch {}
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($prev)) {
|
||||
# Fallback: best-effort previous version tag by semver-ish sorting.
|
||||
$prev = (git tag --list "v*" --sort=-v:refname | Where-Object { $_ -ne $tag } | Select-Object -First 1)
|
||||
}
|
||||
|
||||
$range = ""
|
||||
if (-not [string]::IsNullOrWhiteSpace($prev)) {
|
||||
$range = "$prev..$tag"
|
||||
}
|
||||
|
||||
$lines = @()
|
||||
if (-not [string]::IsNullOrWhiteSpace($range)) {
|
||||
$lines = @(git log --no-merges --pretty=format:"- %s (%h)" --reverse $range)
|
||||
} else {
|
||||
# First release tag / missing history: include a small recent window.
|
||||
$lines = @(git log --no-merges --pretty=format:"- %s (%h)" --reverse -n 50)
|
||||
}
|
||||
|
||||
if (-not $lines -or $lines.Count -eq 0) {
|
||||
$lines = @("- 修复了一些已知问题,提升了稳定性。")
|
||||
}
|
||||
|
||||
$max = 60
|
||||
if ($lines.Count -gt $max) {
|
||||
$total = $lines.Count
|
||||
$lines = @($lines | Select-Object -First $max)
|
||||
$lines += "- ...(共 $total 条提交,更多请查看完整变更链接)"
|
||||
}
|
||||
|
||||
$body = @()
|
||||
$body += "## 更新内容 ($tag)"
|
||||
$body += ""
|
||||
$body += $lines
|
||||
|
||||
if (-not [string]::IsNullOrWhiteSpace($prev)) {
|
||||
$body += ""
|
||||
$body += "完整变更: https://github.com/$repo/compare/$prev...$tag"
|
||||
}
|
||||
|
||||
($body -join "`n") | Out-File -FilePath release-notes.md -Encoding utf8
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
@@ -71,7 +134,7 @@ jobs:
|
||||
with:
|
||||
tag_name: ${{ env.TAG_NAME }}
|
||||
name: ${{ env.TAG_NAME }}
|
||||
generate_release_notes: true
|
||||
body_path: release-notes.md
|
||||
files: |
|
||||
desktop/dist/*Setup*.exe
|
||||
desktop/dist/*Setup*.exe.blockmap
|
||||
|
||||
@@ -65,26 +65,18 @@
|
||||
|
||||
## 年度总结
|
||||
|
||||
年度总结现在支持 3 种不同风格(style1、style2、style3)。如果你对某个风格有更好的修改建议,或有新风格的点子,欢迎到 Issue 区反馈:https://github.com/LifeArchiveProject/WeChatDataAnalysis/issues
|
||||
年度总结目前只保留「现代(Modern)」风格。如果你对年度总结有更好的修改建议,欢迎到 Issue 区反馈:https://github.com/LifeArchiveProject/WeChatDataAnalysis/issues
|
||||
|
||||
> ⚠️ **提醒**:年度总结目前还不是最终版本,后续还会增加新总结或新风格。
|
||||
> ⚠️ **提醒**:年度总结目前还不是最终版本,后续还会增加新总结或新内容。
|
||||
|
||||
也欢迎加入下方 QQ 群一起讨论。
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td align="center"><b>Style 1</b></td>
|
||||
<td align="center"><b>Style 2</b></td>
|
||||
<td align="center"><b>Modern</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="frontend/public/style1.png" alt="年度总结 Style 1" width="400"/></td>
|
||||
<td><img src="frontend/public/style2.png" alt="年度总结 Style 2" width="400"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" colspan="2"><b>Style 3</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" colspan="2"><img src="frontend/public/style3.png" alt="年度总结 Style 3" width="400"/></td>
|
||||
<td align="center"><img src="frontend/public/style1.png" alt="年度总结 Modern" width="400"/></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
+105
-4
@@ -265,9 +265,108 @@ function setWindowProgressBar(value) {
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function looksLikeHtml(input) {
|
||||
if (!input) return false;
|
||||
const s = String(input);
|
||||
if (!s.includes("<") || !s.includes(">")) return false;
|
||||
// Be conservative: only treat the note as HTML if it contains common tags we expect from GitHub-rendered bodies.
|
||||
return /<(p|div|br|ul|ol|li|a|strong|em|tt|code|pre|h[1-6])\b/i.test(s);
|
||||
}
|
||||
|
||||
function htmlToPlainText(html) {
|
||||
if (!html) return "";
|
||||
|
||||
let text = String(html);
|
||||
|
||||
// Drop script/style blocks entirely.
|
||||
text = text.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "");
|
||||
text = text.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "");
|
||||
|
||||
// Keep links readable after stripping tags.
|
||||
text = text.replace(
|
||||
/<a\s+[^>]*href=(["'])([^"']+)\1[^>]*>([\s\S]*?)<\/a>/gi,
|
||||
(_m, _q, href, inner) => {
|
||||
const innerText = String(inner).replace(/<[^>]*>/g, "").trim();
|
||||
const url = String(href || "").trim();
|
||||
if (!url) return innerText;
|
||||
if (!innerText) return url;
|
||||
return `${innerText} (${url})`;
|
||||
}
|
||||
);
|
||||
|
||||
// Preserve line breaks / list structure before stripping remaining tags.
|
||||
text = text.replace(/<\s*br\s*\/?>/gi, "\n");
|
||||
text = text.replace(/<\/\s*(p|div|h1|h2|h3|h4|h5|h6)\s*>/gi, "\n");
|
||||
text = text.replace(/<\s*li[^>]*>/gi, "- ");
|
||||
text = text.replace(/<\/\s*li\s*>/gi, "\n");
|
||||
text = text.replace(/<\/\s*(ul|ol)\s*>/gi, "\n");
|
||||
|
||||
// Strip remaining tags.
|
||||
text = text.replace(/<[^>]*>/g, "");
|
||||
|
||||
// Decode the handful of entities we commonly see from GitHub-rendered HTML.
|
||||
const named = {
|
||||
nbsp: " ",
|
||||
amp: "&",
|
||||
lt: "<",
|
||||
gt: ">",
|
||||
quot: '"',
|
||||
apos: "'",
|
||||
"#39": "'",
|
||||
};
|
||||
text = text.replace(/&([a-z0-9#]+);/gi, (m, name) => {
|
||||
const key = String(name || "").toLowerCase();
|
||||
if (named[key] != null) return named[key];
|
||||
|
||||
// Numeric entities (decimal / hex).
|
||||
const decMatch = key.match(/^#(\d+)$/);
|
||||
if (decMatch) {
|
||||
const n = Number(decMatch[1]);
|
||||
if (Number.isFinite(n) && n >= 0 && n <= 0x10ffff) {
|
||||
try {
|
||||
return String.fromCodePoint(n);
|
||||
} catch {
|
||||
return m;
|
||||
}
|
||||
}
|
||||
return m;
|
||||
}
|
||||
|
||||
const hexMatch = key.match(/^#x([0-9a-f]+)$/i);
|
||||
if (hexMatch) {
|
||||
const n = Number.parseInt(hexMatch[1], 16);
|
||||
if (Number.isFinite(n) && n >= 0 && n <= 0x10ffff) {
|
||||
try {
|
||||
return String.fromCodePoint(n);
|
||||
} catch {
|
||||
return m;
|
||||
}
|
||||
}
|
||||
return m;
|
||||
}
|
||||
|
||||
return m;
|
||||
});
|
||||
|
||||
// Normalize whitespace/newlines.
|
||||
text = text.replace(/\r\n/g, "\n");
|
||||
text = text.replace(/\n{3,}/g, "\n\n");
|
||||
return text.trim();
|
||||
}
|
||||
|
||||
function normalizeReleaseNotes(releaseNotes) {
|
||||
if (!releaseNotes) return "";
|
||||
if (typeof releaseNotes === "string") return releaseNotes;
|
||||
|
||||
const normalizeText = (value) => {
|
||||
if (value == null) return "";
|
||||
const raw = typeof value === "string" ? value : String(value);
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) return "";
|
||||
if (looksLikeHtml(trimmed)) return htmlToPlainText(trimmed);
|
||||
return trimmed;
|
||||
};
|
||||
|
||||
if (typeof releaseNotes === "string") return normalizeText(releaseNotes);
|
||||
if (Array.isArray(releaseNotes)) {
|
||||
const parts = [];
|
||||
for (const item of releaseNotes) {
|
||||
@@ -275,15 +374,17 @@ function normalizeReleaseNotes(releaseNotes) {
|
||||
const note = item?.note;
|
||||
const noteText =
|
||||
typeof note === "string" ? note : note != null ? JSON.stringify(note, null, 2) : "";
|
||||
const block = [version ? `v${version}` : "", noteText].filter(Boolean).join("\n");
|
||||
const block = [version ? `v${version}` : "", normalizeText(noteText)]
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
if (block) parts.push(block);
|
||||
}
|
||||
return parts.join("\n\n");
|
||||
}
|
||||
try {
|
||||
return JSON.stringify(releaseNotes, null, 2);
|
||||
return normalizeText(JSON.stringify(releaseNotes, null, 2));
|
||||
} catch {
|
||||
return String(releaseNotes);
|
||||
return normalizeText(releaseNotes);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
<span v-if="remainingText">剩余 {{ remainingText }}</span>
|
||||
</div>
|
||||
<div class="mt-2 h-2 w-full rounded bg-gray-200 overflow-hidden">
|
||||
<div class="h-2 bg-emerald-500" :style="{ width: `${percent}%` }" />
|
||||
<div class="h-2 bg-wechat-green" :style="{ width: `${percent}%` }" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
|
||||
<button
|
||||
v-if="readyToInstall"
|
||||
class="px-3 py-1.5 rounded-md bg-emerald-600 text-white text-sm hover:bg-emerald-700"
|
||||
class="px-3 py-1.5 rounded-md bg-wechat-green text-white text-sm hover:bg-wechat-green-hover"
|
||||
type="button"
|
||||
@click="emitInstall"
|
||||
>
|
||||
@@ -86,7 +86,7 @@
|
||||
忽略此版本
|
||||
</button>
|
||||
<button
|
||||
class="px-3 py-1.5 rounded-md bg-emerald-600 text-white text-sm hover:bg-emerald-700"
|
||||
class="px-3 py-1.5 rounded-md bg-wechat-green text-white text-sm hover:bg-wechat-green-hover"
|
||||
type="button"
|
||||
@click="emitUpdate"
|
||||
>
|
||||
|
||||
@@ -115,7 +115,7 @@
|
||||
@error="onShownAvatarError"
|
||||
/>
|
||||
<img
|
||||
v-else-if="isGameboy && phase === 'idle'"
|
||||
v-else-if="(isGameboy || isModern) && phase === 'idle'"
|
||||
src="/assets/images/LuckyBlock.png"
|
||||
class="w-full h-full object-contain"
|
||||
alt="Lucky Block"
|
||||
@@ -258,6 +258,7 @@ const props = defineProps({
|
||||
|
||||
const { theme } = useWrappedTheme()
|
||||
const isGameboy = computed(() => theme.value === 'gameboy')
|
||||
const isModern = computed(() => theme.value === 'off')
|
||||
const isRetro = computed(() => isGameboy.value)
|
||||
|
||||
const nfInt = new Intl.NumberFormat('zh-CN', { maximumFractionDigits: 0 })
|
||||
|
||||
@@ -39,7 +39,6 @@
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2 items-end">
|
||||
<WrappedThemeSwitcher />
|
||||
<button
|
||||
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 controls-btn"
|
||||
:disabled="loading"
|
||||
@@ -83,73 +82,3 @@ const yearOptions = computed(() => {
|
||||
return years
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 复古模式 - 控制面板样式 */
|
||||
.wrapped-retro .controls-panel {
|
||||
background-color: var(--wrapped-card-bg);
|
||||
border-color: var(--wrapped-border);
|
||||
}
|
||||
|
||||
.wrapped-retro .controls-label {
|
||||
color: var(--wrapped-text-secondary);
|
||||
}
|
||||
|
||||
.wrapped-retro .controls-select {
|
||||
background-color: var(--wrapped-bg);
|
||||
border-color: var(--wrapped-border);
|
||||
color: var(--wrapped-text);
|
||||
}
|
||||
|
||||
.wrapped-retro .controls-select:focus {
|
||||
--tw-ring-color: var(--wrapped-accent);
|
||||
}
|
||||
|
||||
.wrapped-retro .controls-checkbox {
|
||||
border-color: var(--wrapped-border);
|
||||
color: var(--wrapped-accent);
|
||||
}
|
||||
|
||||
.wrapped-retro .controls-checkbox:focus {
|
||||
--tw-ring-color: var(--wrapped-accent);
|
||||
}
|
||||
|
||||
.wrapped-retro .controls-hint {
|
||||
color: var(--wrapped-text-secondary);
|
||||
}
|
||||
|
||||
.wrapped-retro .controls-warning {
|
||||
color: var(--wrapped-warning);
|
||||
}
|
||||
|
||||
.wrapped-retro .controls-btn {
|
||||
background-color: var(--wrapped-accent);
|
||||
color: var(--wrapped-bg);
|
||||
}
|
||||
|
||||
.wrapped-retro .controls-btn:hover:not(:disabled) {
|
||||
filter: brightness(1.1);
|
||||
}
|
||||
|
||||
/* 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,109 +1,31 @@
|
||||
/**
|
||||
* 年度总结页面主题管理 composable
|
||||
* 支持三种主题:modern(现代)、gameboy(Game Boy)、win98(Windows 98)
|
||||
* 仅保留 modern(现代)主题
|
||||
*/
|
||||
|
||||
const STORAGE_KEY = 'wrapped-theme'
|
||||
const VALID_THEMES = ['off', 'gameboy', 'win98']
|
||||
const RETRO_THEMES = new Set(['gameboy'])
|
||||
|
||||
// 全局响应式状态(跨组件共享)
|
||||
const theme = ref('off')
|
||||
let initialized = false
|
||||
let keyboardInitialized = false
|
||||
// Note: 历史上曾尝试过 gameboy / win98 等主题,但目前已移除,仅保留 Modern。
|
||||
const theme = ref('off') // off === Modern
|
||||
|
||||
export function useWrappedTheme() {
|
||||
// 初始化:从 localStorage 读取(仅执行一次)
|
||||
const initTheme = () => {
|
||||
if (initialized || !import.meta.client) return
|
||||
const saved = localStorage.getItem(STORAGE_KEY)
|
||||
if (saved && VALID_THEMES.includes(saved)) {
|
||||
theme.value = saved
|
||||
}
|
||||
initialized = true
|
||||
}
|
||||
|
||||
// 设置主题
|
||||
const setTheme = (newTheme) => {
|
||||
if (!VALID_THEMES.includes(newTheme)) {
|
||||
console.warn(`Invalid theme: ${newTheme}`)
|
||||
return
|
||||
}
|
||||
theme.value = newTheme
|
||||
if (import.meta.client) {
|
||||
localStorage.setItem(STORAGE_KEY, newTheme)
|
||||
// Only keep Modern.
|
||||
if (newTheme !== 'off') {
|
||||
console.warn(`Wrapped theme '${newTheme}' has been removed; falling back to Modern.`)
|
||||
}
|
||||
theme.value = 'off'
|
||||
}
|
||||
|
||||
// 切换到下一个主题(循环)
|
||||
const cycleTheme = () => {
|
||||
const currentIndex = VALID_THEMES.indexOf(theme.value)
|
||||
const nextIndex = (currentIndex + 1) % VALID_THEMES.length
|
||||
setTheme(VALID_THEMES[nextIndex])
|
||||
}
|
||||
const cycleTheme = () => setTheme('off')
|
||||
|
||||
// 计算属性:是否为复古模式(非 off)
|
||||
const isRetro = computed(() => theme.value !== 'off')
|
||||
|
||||
// 计算属性:当前主题的 CSS 类名
|
||||
const themeClass = computed(() => {
|
||||
if (theme.value === 'off') return ''
|
||||
// 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}`
|
||||
})
|
||||
|
||||
// 计算属性:主题显示名称
|
||||
const themeName = computed(() => {
|
||||
const names = {
|
||||
off: 'Modern',
|
||||
gameboy: 'Game Boy',
|
||||
win98: 'Windows 98'
|
||||
}
|
||||
return names[theme.value] || 'Modern'
|
||||
})
|
||||
|
||||
// 全局 F1-F3 快捷键切换主题(仅初始化一次)
|
||||
const initKeyboardShortcuts = () => {
|
||||
if (keyboardInitialized || !import.meta.client) return
|
||||
keyboardInitialized = true
|
||||
|
||||
const handleKeydown = (e) => {
|
||||
// 检查是否在可编辑元素中
|
||||
const el = e.target
|
||||
if (el && (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA' || el.tagName === 'SELECT' || el.isContentEditable)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (e.key === 'F1') {
|
||||
e.preventDefault()
|
||||
setTheme('off')
|
||||
} else if (e.key === 'F2') {
|
||||
e.preventDefault()
|
||||
setTheme('gameboy')
|
||||
} else if (e.key === 'F3') {
|
||||
e.preventDefault()
|
||||
setTheme('win98')
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', handleKeydown)
|
||||
}
|
||||
|
||||
// 客户端挂载后再初始化:避免 SSR 与首帧 hydration 不一致
|
||||
onMounted(() => {
|
||||
initTheme()
|
||||
initKeyboardShortcuts()
|
||||
})
|
||||
const isRetro = computed(() => false)
|
||||
const themeClass = computed(() => '')
|
||||
|
||||
return {
|
||||
theme: readonly(theme),
|
||||
setTheme,
|
||||
cycleTheme,
|
||||
isRetro,
|
||||
themeClass,
|
||||
themeName,
|
||||
VALID_THEMES
|
||||
themeClass
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,10 +7,8 @@
|
||||
>
|
||||
<!-- PPT 风格:单张卡片占据全页面,鼠标滚轮切换 -->
|
||||
<WrappedDeckBackground />
|
||||
<!-- CRT 叠加层仅用于“像素屏”类主题,Win98 等桌面 GUI 主题不应开启 -->
|
||||
<WrappedCRTOverlay v-if="theme === 'gameboy'" />
|
||||
|
||||
<!-- 左上角:刷新 + 复古模式开关 -->
|
||||
<!-- 左上角:返回 + 刷新 -->
|
||||
<div class="absolute top-6 left-6 z-20 select-none">
|
||||
<div class="flex items-center gap-3">
|
||||
<button
|
||||
@@ -59,24 +57,6 @@
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="pointer-events-auto inline-flex items-center justify-center w-9 h-9 rounded-full bg-transparent transition disabled:opacity-60 disabled:cursor-not-allowed focus:outline-none focus-visible:ring-2 focus-visible:ring-[#07C160]/30"
|
||||
:class="isRetro ? 'text-[#07C160] hover:bg-[#07C160]/10' : 'text-[#00000055] hover:bg-[#000000]/5'"
|
||||
:aria-pressed="isRetro ? 'true' : 'false'"
|
||||
:aria-label="`复古模式(当前:${theme === 'off' ? 'Modern' : theme.toUpperCase()})`"
|
||||
:title="`复古模式:${theme === 'off' ? 'Modern' : theme.toUpperCase()}(点击切换)`"
|
||||
@click="cycleTheme"
|
||||
>
|
||||
<img
|
||||
src="/assets/images/wechat-audio-dark.png"
|
||||
class="w-4 h-4 transition"
|
||||
:style="{ filter: isRetro ? 'none' : 'grayscale(1)', opacity: isRetro ? '1' : '0.55' }"
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
draggable="false"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="error" class="mt-2 pointer-events-auto bg-white/90 backdrop-blur rounded-xl border border-red-200 px-3 py-2">
|
||||
@@ -205,11 +185,6 @@
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- Win98:底部任务栏 -->
|
||||
<WrappedWin98Taskbar
|
||||
v-if="theme === 'win98'"
|
||||
:title="taskbarTitle"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -229,8 +204,8 @@ const year = ref(Number(route.query?.year) || new Date().getFullYear())
|
||||
// 分享视图不展示账号信息:默认让后端自动选择;需要指定时可用 query ?account=wxid_xxx
|
||||
const account = ref(typeof route.query?.account === 'string' ? route.query.account : '')
|
||||
|
||||
// 主题管理:modern / gameboy / win98
|
||||
const { theme, cycleTheme, isRetro, themeClass } = useWrappedTheme()
|
||||
// 主题:仅保留 Modern
|
||||
const { isRetro, themeClass } = useWrappedTheme()
|
||||
|
||||
const accounts = ref([])
|
||||
const accountsLoading = ref(true)
|
||||
@@ -262,13 +237,6 @@ const wheelAcc = ref(0)
|
||||
let navUnlockTimer = null
|
||||
let deckResizeObserver = null
|
||||
|
||||
// 各主题的背景颜色
|
||||
const THEME_BG = {
|
||||
off: '#F3FFF8', // Modern: 浅绿
|
||||
gameboy: '#9bbc0f', // Game Boy: 亮绿
|
||||
win98: '#008080' // Win98: 经典桌面青色
|
||||
}
|
||||
|
||||
const slides = computed(() => {
|
||||
const cards = Array.isArray(report.value?.cards) ? report.value.cards : []
|
||||
const out = [{ key: 'cover' }]
|
||||
@@ -276,15 +244,7 @@ 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 currentBg = computed(() => '#F3FFF8')
|
||||
const deckTrackClass = computed(() => 'z-10')
|
||||
|
||||
const applyViewportBg = () => {
|
||||
@@ -423,11 +383,8 @@ const onTouchEnd = (e) => {
|
||||
const updateViewport = () => {
|
||||
const h = Math.round(deckEl.value?.getBoundingClientRect?.().height || 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 - effective) > 1) viewportHeight.value = effective
|
||||
if (Math.abs(viewportHeight.value - h) > 1) viewportHeight.value = h
|
||||
}
|
||||
|
||||
const loadAccounts = async () => {
|
||||
@@ -592,12 +549,6 @@ onMounted(async () => {
|
||||
}
|
||||
})
|
||||
|
||||
// Theme switch may change reserved UI space (e.g., Win98 taskbar)
|
||||
watch(theme, () => {
|
||||
applyViewportBg()
|
||||
updateViewport()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (import.meta.client) {
|
||||
document.documentElement.style.backgroundColor = ''
|
||||
|
||||
Reference in New Issue
Block a user