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
99 lines
2.7 KiB
Vue
99 lines
2.7 KiB
Vue
<template>
|
||
<t-dropdown
|
||
trigger="click"
|
||
:options="menuOptions"
|
||
:max-height="240"
|
||
@click="onSelect"
|
||
>
|
||
<t-button
|
||
variant="text"
|
||
class="switch-btn"
|
||
:title="`${t('locale.label')}:${currentLabel}`"
|
||
:aria-label="t('locale.label')"
|
||
>
|
||
<template #icon><TranslateIcon /></template>
|
||
<span class="switch-text">{{ currentCode }}</span>
|
||
<ChevronDownIcon class="switch-caret" />
|
||
</t-button>
|
||
</t-dropdown>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { computed } from 'vue'
|
||
import { useI18n } from 'vue-i18n'
|
||
import { TranslateIcon, ChevronDownIcon } from 'tdesign-icons-vue-next'
|
||
import { useLocaleStore } from '@/stores/locale'
|
||
import { syncDocumentTitle } from '@/router'
|
||
import type { AppLocale } from '@/i18n'
|
||
|
||
/**
|
||
* 语言切换器(简体中文 / English / 日本語 / 한국어)。
|
||
*
|
||
* 交互取舍:用下拉菜单而不是逐次点击循环切换。
|
||
* 语言有 4 种且用户能明确知道自己要哪种,直接选择比「点三次才轮到」更省事,
|
||
* 也和右侧主题切换器保持一致的交互模式。
|
||
*
|
||
* 按钮上显示语言代码(ZH / EN / JA / KO)而不是国旗:
|
||
* 语言不等于国家,用国旗会让繁体中文、多语种地区的用户产生歧义。
|
||
*/
|
||
const { t } = useI18n()
|
||
const localeStore = useLocaleStore()
|
||
|
||
/** 当前语言的按钮短码(中文用 ZH 而非国旗,避免语言与国家的绑定歧义) */
|
||
const currentCode = computed(() => localeStore.locale.split('-')[0].toUpperCase())
|
||
|
||
const currentLabel = computed(() => t(`locale.${localeStore.locale}`))
|
||
|
||
const menuOptions = computed(() =>
|
||
localeStore.options.map((opt) => ({
|
||
content: opt.label,
|
||
value: opt.value,
|
||
// 当前语言在菜单里打勾,方便确认
|
||
active: opt.value === localeStore.locale,
|
||
})),
|
||
)
|
||
|
||
function onSelect(item: { value?: string | number }) {
|
||
const next = item.value
|
||
if (typeof next !== 'string') return
|
||
localeStore.set(next as AppLocale)
|
||
// 语言变了,浏览器标题也要跟着换,否则标签页会停留在旧语言
|
||
syncDocumentTitle()
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.switch-btn {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
padding: 0 8px;
|
||
height: 34px;
|
||
color: var(--tk-text-secondary);
|
||
border-radius: var(--tk-radius-md);
|
||
}
|
||
.switch-btn:hover {
|
||
color: var(--tk-text);
|
||
background: var(--tk-surface-hover);
|
||
}
|
||
.switch-text {
|
||
font-size: var(--tk-text-sm);
|
||
font-weight: 600;
|
||
letter-spacing: 0.02em;
|
||
/* 语言代码固定宽度,切换时按钮不会左右跳动 */
|
||
min-width: 22px;
|
||
text-align: center;
|
||
}
|
||
.switch-caret {
|
||
font-size: 14px;
|
||
opacity: 0.6;
|
||
}
|
||
/* 窄屏只留图标,避免顶栏拥挤 */
|
||
@media (max-width: 767px) {
|
||
.switch-text,
|
||
.switch-caret {
|
||
display: none;
|
||
}
|
||
}
|
||
</style>
|