diff --git a/shared/backups.test.ts b/shared/backups.test.ts new file mode 100644 index 0000000..9e93d74 --- /dev/null +++ b/shared/backups.test.ts @@ -0,0 +1,16 @@ +import { expect, test } from 'bun:test' +import { backupTime } from './backups.ts' + +test('标准备份名排成可读时间', () => { + expect(backupTime('config.yaml.bak-20260811-031000')).toBe('2026-08-11 03:10:00') +}) + +test('配置文件名不同也能解析,只看结尾的时间戳', () => { + expect(backupTime('mihomo.yml.bak-20260101-235959')).toBe('2026-01-01 23:59:59') +}) + +test('不合预期的名字返回 null', () => { + expect(backupTime('config.yaml.bak')).toBeNull() + expect(backupTime('config.yaml.bak-2026081-031000')).toBeNull() + expect(backupTime('notes.txt')).toBeNull() +}) diff --git a/shared/backups.ts b/shared/backups.ts new file mode 100644 index 0000000..3573da1 --- /dev/null +++ b/shared/backups.ts @@ -0,0 +1,15 @@ +/** + * 备份文件名里的时间戳。 + * + * 名字由服务端生成(server/config-store.ts 的 backupName),形如 + * `config.yaml.bak-20260811-031000`,时间戳是服务端的本地时间。 + */ + +const STAMP = /\.bak-(\d{4})(\d{2})(\d{2})-(\d{2})(\d{2})(\d{2})$/ + +/** 把备份文件名排成可读时间。名字不合预期时返回 null,调用方退回显示原名。 */ +export function backupTime(name: string): string | null { + const m = STAMP.exec(name) + if (!m) return null + return `${m[1]}-${m[2]}-${m[3]} ${m[4]}:${m[5]}:${m[6]}` +} diff --git a/web/prefs.ts b/web/prefs.ts index 22acfa7..918ddaa 100644 --- a/web/prefs.ts +++ b/web/prefs.ts @@ -1,9 +1,11 @@ /** * 界面偏好,存在 localStorage 里。 * - * 目前只有一项:工具栏和导航是只显示图标,还是图标加文字。 + * 密度:工具栏和导航是只显示图标,还是图标加文字。 * 默认是「完整」——这个工具是隔一阵子才动一次的,纯图标对偶尔来一次的人不友好; * 想要紧凑的人自己去设置里切。 + * + * 主题:跟随系统、浅色、深色。默认跟随系统。 */ import { ref, watch } from 'vue' @@ -21,3 +23,25 @@ watch(density, (value) => { export function toggleDensity(): void { density.value = density.value === 'full' ? 'compact' : 'full' } + +export type Theme = 'system' | 'light' | 'dark' + +const THEME_KEY = 'patchbay.theme' + +const storedTheme = localStorage.getItem(THEME_KEY) +export const theme = ref( + storedTheme === 'light' || storedTheme === 'dark' ? storedTheme : 'system', +) + +// 显式选了浅色或深色就在 上标出来:tokens.css 里 :root[data-theme=…] 的权重 +// 高于 :root 和媒体查询里的 :root,压过系统偏好。跟随系统则不加标记,交回给媒体查询, +// 这样系统主题切换时界面仍然跟着变。 +watch( + theme, + (value) => { + localStorage.setItem(THEME_KEY, value) + if (value === 'system') document.documentElement.removeAttribute('data-theme') + else document.documentElement.setAttribute('data-theme', value) + }, + { immediate: true }, +) diff --git a/web/styles/tokens.css b/web/styles/tokens.css index e0b4b0f..546d6bc 100644 --- a/web/styles/tokens.css +++ b/web/styles/tokens.css @@ -71,6 +71,66 @@ --line: 1.7; } +/* + * 显式选择的主题(web/prefs.ts 在 上写 data-theme)。两个方向都要写: + * 深色系统上选浅色也要生效。选择器权重高于 :root 和上面媒体查询里的 :root, + * 显式选择压过系统偏好;不标 data-theme 时这两块不命中,媒体查询说了算。 + * 只覆盖颜色变量,间距、字号与主题无关。 + */ +:root[data-theme='light'] { + --c-bg: #fbfbfc; + --c-surface: #ffffff; + --c-surface-sunken: #f4f5f7; + --c-border: #e4e6ea; + --c-border-strong: #d0d3d9; + + --c-text: #1c1e21; + --c-text-soft: #5c6169; + --c-text-faint: #8b9098; + + --c-accent: #2b7a78; + --c-accent-soft: #e6f2f1; + --c-accent-text: #1f5c5a; + + --c-chain: #5145cd; + --c-chain-soft: #eeedfb; + --c-chain-text: #4038a8; + + --c-danger: #b3261e; + --c-danger-soft: #fdedec; + --c-warn: #8a6100; + --c-warn-soft: #fdf5e3; + --c-ok: #1f6b3a; + --c-ok-soft: #e9f5ed; +} + +:root[data-theme='dark'] { + --c-bg: #16181c; + --c-surface: #1d2025; + --c-surface-sunken: #121417; + --c-border: #2b2f36; + --c-border-strong: #3a3f48; + + --c-text: #e6e8eb; + --c-text-soft: #9ba1a9; + --c-text-faint: #6c727a; + + --c-accent: #4db6ac; + --c-accent-soft: #1b3330; + --c-accent-text: #7fd4cb; + + --c-chain: #8b83f0; + --c-chain-soft: #241f4a; + --c-chain-text: #a9a2f7; + + --c-danger: #f2b8b5; + --c-danger-soft: #3a1f1e; + --c-warn: #e8c37a; + --c-warn-soft: #33290f; + --c-ok: #86d6a0; + --c-ok-soft: #16301f; +} + @media (prefers-color-scheme: dark) { :root { --c-bg: #16181c; diff --git a/web/views/InboundForm.vue b/web/views/InboundForm.vue index b7b57ef..b1ed253 100644 --- a/web/views/InboundForm.vue +++ b/web/views/InboundForm.vue @@ -310,7 +310,7 @@ select { .btn-go { background: var(--c-accent); border-color: transparent; - color: #fff; + color: var(--c-bg); font-weight: 500; } diff --git a/web/views/SettingsPage.vue b/web/views/SettingsPage.vue new file mode 100644 index 0000000..77348d6 --- /dev/null +++ b/web/views/SettingsPage.vue @@ -0,0 +1,309 @@ + + + + + diff --git a/web/views/TokenGate.vue b/web/views/TokenGate.vue index 1df866c..a13121b 100644 --- a/web/views/TokenGate.vue +++ b/web/views/TokenGate.vue @@ -67,7 +67,7 @@ h1 { border: 1px solid transparent; border-radius: var(--r-sm); background: var(--c-accent); - color: #fff; + color: var(--c-bg); font-weight: 500; cursor: pointer; }