All checks were successful
Build and Deploy (tool.xpcool.com) / build-and-deploy (push) Successful in 1m6s
视觉体系(核心是「令牌 → 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
103 lines
3.6 KiB
TypeScript
103 lines
3.6 KiB
TypeScript
import { computed, ref } from 'vue'
|
||
import { defineStore } from 'pinia'
|
||
|
||
/** 用户在界面上选择的主题偏好 */
|
||
export type ThemePreference = 'light' | 'dark' | 'auto'
|
||
|
||
/** 实际生效的主题(auto 会被解析成二者之一) */
|
||
export type ThemeMode = 'light' | 'dark'
|
||
|
||
/** 主题偏好本地存储 key(index.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 }
|
||
})
|