tool.xpcool.com/src/stores/theme.ts
夏犀麟 af233e1628
All checks were successful
Build and Deploy (tool.xpcool.com) / build-and-deploy (push) Successful in 1m6s
feat: 视觉重设计 + 中英日韩四语言 + 三态昼夜主题 + 服务端接口对接
视觉体系(核心是「令牌 → TDesign 桥接」)
- 新增 tokens.scss:--tk-* 设计令牌(青玉 Jade 单一强调色、冷调中性色阶、
  圆角/间距/字号/z-index/动效规范、按语言切换字体栈)
- 新增 tdesign-theme.scss:--tk-* → --td-* 桥接(含完整冷调灰阶 1-14)
  → 60+ 个未改动的存量工具页零成本继承新配色与圆角
- index.scss 重写:只过渡颜色、统一 focus-visible、reduced-motion 降级
- 新增 v-reveal 滚动进入指令

布局与交互
- MainLayout 重写:毛玻璃 sticky 顶栏 + 三区布局 + 页脚(含备案号)
- 新增 CommandPalette(⌘K/Ctrl+K)、LocaleSwitch(显示语言代码不用国旗)、
  ThemeSwitch(三态;图标显示生效 mode、菜单勾选用户偏好 preference)
- SideNav 重写:内联筛选、可折叠分组持久化、活动态指示条、需联网标记
- HomeView 重写:左对齐 Hero + 真实可用搜索 + 概览数字条 + 服务端工具专区

国际化与主题
- vue-i18n 11 + zh-CN/en-US/ja-JP/ko-KR 完整语言包(58 个工具名与描述全译)
- helpers 取词带中文兜底;index.html 防闪烁内联脚本与 store 三处同步
- 主题三态 light/dark/auto,preference 与 mode 分离

服务端对接(service.xpcool.com /api/service/open/tools/*)
- 新增 openTools.ts:接口元数据与调用函数共用同一份路径定义
- 改造/新建 6 页:ip-info、uuid、hash(服务端 MD5 两端口径对比)、
  timestamp(服务端时钟 + 时钟偏移)、random-string、api-status
- 均为「本机 / 服务端」双模式,默认本机、不自动打网络请求
- 后端未实现的 5 个接口用 BackendPending 如实标注,不伪装可用

修复
- 空分类渲染缺陷(health/study/travel 无工具却渲染空区块),分类 9 → 6
- UnoCSS panel 快捷类补内边距;IpInfoView 漏 onBeforeUnmount;
  SideNav 末尾 import;HomeView 错误导入 SUPPORTED_LOCALES

其他
- 新增 src/config.ts 集中站点常量,页脚展示备案号并链工信部核验
- docs/api-contract.md 按线上实际重写;PROJECT_STATE.md 全面更新
- 构建通过:vue-tsc --noEmit + vite build
2026-09-14 02:05:38 +08:00

103 lines
3.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { computed, ref } from 'vue'
import { defineStore } from 'pinia'
/** 用户在界面上选择的主题偏好 */
export type ThemePreference = 'light' | 'dark' | 'auto'
/** 实际生效的主题auto 会被解析成二者之一) */
export type ThemeMode = 'light' | 'dark'
/** 主题偏好本地存储 keyindex.html 内联脚本会读取它做首屏防闪烁) */
export const THEME_STORAGE_KEY = 'tool-theme'
const MEDIA_QUERY = '(prefers-color-scheme: dark)'
/** 查询系统当前是否为深色(旧浏览器不支持 matchMedia 时按浅色处理) */
function systemPrefersDark(): boolean {
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return false
return window.matchMedia(MEDIA_QUERY).matches
}
/** 读取初始偏好:本地存储优先,其次跟随系统 */
function resolveInitialPreference(): ThemePreference {
try {
const saved = localStorage.getItem(THEME_STORAGE_KEY)
if (saved === 'light' || saved === 'dark' || saved === 'auto') return saved
} catch {
// 忽略存储不可用的场景
}
return 'auto'
}
/**
* 主题状态管理:浅色 / 深色 / 跟随系统 三态切换。
*
* 之所以要有「跟随系统」这一态:用户手动选过一次之后,
* 系统在日落时自动切深色时页面不会跟着变,体验会断裂。
* 三态让用户既能保留手动控制,也能交回给系统。
*
* 实现上把「偏好」与「实际生效主题」分开:
* preference → 用户选择,写 localStorage
* mode → 真实生效值,写到 html[theme-mode]TDesign 依赖该属性)
*/
export const useThemeStore = defineStore('theme', () => {
const preference = ref<ThemePreference>(resolveInitialPreference())
const systemDark = ref(systemPrefersDark())
/** 实际生效的主题 */
const mode = computed<ThemeMode>(() => {
if (preference.value === 'auto') return systemDark.value ? 'dark' : 'light'
return preference.value
})
/** 把主题写到 DOM 并持久化偏好 */
function apply(next: ThemePreference) {
preference.value = next
// TDesign 通过 html[theme-mode] 切换整套设计变量,必须是 light / dark 具值
document.documentElement.setAttribute('theme-mode', mode.value)
try {
localStorage.setItem(THEME_STORAGE_KEY, next)
} catch {
// 忽略存储失败
}
}
/**
* 三态循环切换:浅色 → 深色 → 跟随系统 → 浅色。
* 单按钮即可覆盖三种偏好,避免在顶栏塞三个按钮。
*/
function cycle() {
const order: ThemePreference[] = ['light', 'dark', 'auto']
const next = order[(order.indexOf(preference.value) + 1) % order.length]
apply(next)
}
/** 直接指定偏好 */
function set(next: ThemePreference) {
apply(next)
}
/**
* 初始化:应用一次主题,并在「跟随系统」时监听系统主题变化。
* @returns 取消监听的清理函数(应用卸载时调用,避免监听器泄漏)
*/
function init(): () => void {
apply(preference.value)
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') {
return () => {}
}
const mql = window.matchMedia(MEDIA_QUERY)
const onChange = (e: MediaQueryListEvent) => {
systemDark.value = e.matches
// 仅在跟随系统时同步 DOM用户手动指定时不受系统影响
if (preference.value === 'auto') {
document.documentElement.setAttribute('theme-mode', e.matches ? 'dark' : 'light')
}
}
mql.addEventListener('change', onChange)
return () => mql.removeEventListener('change', onChange)
}
return { preference, mode, apply, set, cycle, init }
})