feat: add useTDesignDesignTokens

pull/355/head^2
金毛88 2026-06-03 13:25:23 +08:00
parent 7fd4b1de04
commit 0eb72ca4fd
10 changed files with 1617 additions and 1857 deletions

View File

@ -3,6 +3,7 @@ import type { GlobalConfigProvider } from 'tdesign-vue-next';
import { watch } from 'vue';
import { useTDesignDesignTokens } from '@vben/hooks';
import { usePreferences } from '@vben/preferences';
import { merge } from 'es-toolkit/compat';
@ -12,10 +13,16 @@ import zhConfig from 'tdesign-vue-next/es/locale/zh_CN';
defineOptions({ name: 'App' });
const { isDark } = usePreferences();
// Vben CSS TDesign
useTDesignDesignTokens();
watch(
() => isDark.value,
(dark) => {
document.documentElement.setAttribute('theme-mode', dark ? 'dark' : '');
document.documentElement.setAttribute(
'theme-mode',
dark ? 'dark' : 'light',
);
},
{ immediate: true },
);

View File

@ -5,8 +5,6 @@ import { registerLoadingDirective } from '@vben/common-ui/es/loading';
import { preferences } from '@vben/preferences';
import { initStores } from '@vben/stores';
import '@vben/styles';
// import '@vben/styles/antd';
// 引入组件库的少量全局样式变量
import { useTitle } from '@vueuse/core';
@ -17,6 +15,7 @@ import { initSetupVbenForm } from './adapter/form';
import App from './app.vue';
import { router } from './router';
// 引入组件库的少量全局样式变量
import 'tdesign-vue-next/es/style/index.css';
async function bootstrap(namespace: string) {

View File

@ -32,7 +32,7 @@ export async function node(): Promise<Linter.Config[]> {
'error',
{
ignores: [],
version: '>=20.12.0',
version: '>=22.18.0',
},
],
'n/prefer-global/buffer': ['error', 'never'],

View File

@ -106,5 +106,5 @@
"node": "^22.18.0 || ^24.0.0",
"pnpm": ">=11.0.0"
},
"packageManager": "pnpm@11.4.0"
"packageManager": "pnpm@11.5.0"
}

View File

@ -28,3 +28,21 @@ it('updateCSSVariables should update CSS variables in :root selector', () => {
updatedStyleContent?.includes('fontSize: 16px;'),
).toBe(true);
});
it('updateCSSVariables should support a custom selector', () => {
document.head.innerHTML = `<style id="tdesign-styles"></style>`;
// 使用自定义选择器(如 TDesign 的 theme-mode 选择器)更新 CSS 变量
updateCSSVariables(
{ '--td-brand-color': 'rgb(0, 82, 217)' },
'tdesign-styles',
":root[theme-mode='dark']",
);
const styleElement = document.querySelector('#tdesign-styles');
const content = styleElement?.textContent ?? '';
// 选择器与变量都应正确写入
expect(content.startsWith(":root[theme-mode='dark'] {")).toBe(true);
expect(content.includes('--td-brand-color: rgb(0, 82, 217);')).toBe(true);
});

View File

@ -1,10 +1,15 @@
/**
* CSS
* @param variables CSS
* @param id id便
* @param selector CSS `:root`
* TDesign `:root[theme-mode='dark']`
*
*/
function updateCSSVariables(
variables: { [key: string]: string },
id = '__vben-styles__',
selector = ':root',
): void {
// 获取或创建内联样式表元素
const styleElement =
@ -13,7 +18,7 @@ function updateCSSVariables(
styleElement.id = id;
// 构建要更新的 CSS 变量的样式文本
let cssText = ':root {';
let cssText = `${selector} {`;
for (const key in variables) {
if (Object.prototype.hasOwnProperty.call(variables, key)) {
cssText += `${key}: ${variables[key]};`;

View File

@ -37,7 +37,7 @@ import SubMenu from './sub-menu.vue';
interface Props extends MenuProps {}
defineOptions({ name: 'Menu' });
defineOptions({ name: 'MenuUI' });
const props = withDefaults(defineProps<Props>(), {
accordion: true,

View File

@ -319,3 +319,103 @@ export function useElementPlusDesignTokens() {
{ immediate: true },
);
}
export function useTDesignDesignTokens() {
const { isDark } = usePreferences();
const rootStyles = getComputedStyle(document.documentElement);
const getCssVariableValue = (variable: string, isColor: boolean = true) => {
const value = rootStyles.getPropertyValue(variable);
return isColor ? convertToRgb(`hsl(${value})`) : value;
};
/**
* TDesign + 1~10
* TDesign 1 10
* Vben 50~900
* @param tdName TDesign `brand``error`
* @param vbenName Vben `primary``destructive`
*/
const getColorTokens = (tdName: string, vbenName: string) => {
const dark = isDark.value;
const getColor = (level?: number) =>
getCssVariableValue(
level === undefined ? `--${vbenName}` : `--${vbenName}-${level}`,
);
// 亮色模式 1 最浅、10 最深;暗色模式相反
const lightScale = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900];
const scaleLevels = dark ? [...lightScale].toReversed() : lightScale;
const scale: Record<string, string> = {};
scaleLevels.forEach((level, index) => {
scale[`--td-${tdName}-color-${index + 1}`] = getColor(level);
});
return {
[`--td-${tdName}-color`]: getColor(),
[`--td-${tdName}-color-active`]: getColor(dark ? 300 : 700),
[`--td-${tdName}-color-disabled`]: getColor(dark ? 800 : 200),
[`--td-${tdName}-color-focus`]: getColor(dark ? 900 : 100),
[`--td-${tdName}-color-hover`]: getColor(dark ? 400 : 600),
[`--td-${tdName}-color-light`]: getColor(dark ? 950 : 50),
[`--td-${tdName}-color-light-hover`]: getColor(dark ? 900 : 100),
...scale,
};
};
watch(
() => preferences.theme,
() => {
const variables: Record<string, string> = {
// 品牌色(主题色)、功能色
...getColorTokens('brand', 'primary'),
...getColorTokens('error', 'destructive'),
...getColorTokens('warning', 'warning'),
...getColorTokens('success', 'success'),
// 文字颜色
'--td-text-color-anti': getCssVariableValue('--primary-foreground'),
'--td-text-color-brand': getCssVariableValue('--primary'),
'--td-text-color-disabled': getCssVariableValue('--muted-foreground'),
'--td-text-color-link': getCssVariableValue('--primary'),
'--td-text-color-placeholder': getCssVariableValue(
'--input-placeholder',
),
'--td-text-color-primary': getCssVariableValue('--foreground'),
'--td-text-color-secondary': getCssVariableValue('--muted-foreground'),
// 背景颜色
'--td-bg-color-component': getCssVariableValue('--accent'),
'--td-bg-color-component-active':
getCssVariableValue('--accent-darker'),
'--td-bg-color-component-disabled': getCssVariableValue('--muted'),
'--td-bg-color-component-hover': getCssVariableValue('--accent-hover'),
'--td-bg-color-container': getCssVariableValue('--background'),
'--td-bg-color-container-active':
getCssVariableValue('--accent-darker'),
'--td-bg-color-container-hover': getCssVariableValue('--accent'),
'--td-bg-color-container-select': getCssVariableValue('--accent'),
'--td-bg-color-page': getCssVariableValue('--background-deep'),
'--td-bg-color-secondarycontainer': getCssVariableValue('--card'),
// 边框颜色
'--td-border-level-1-color': getCssVariableValue('--border'),
'--td-border-level-2-color': getCssVariableValue('--border'),
'--td-component-border': getCssVariableValue('--border'),
'--td-component-stroke': getCssVariableValue('--border'),
// 圆角
'--td-radius-default': getCssVariableValue('--radius', false),
};
// TDesign 将亮/暗变量定义在 `:root[theme-mode='light'|'dark']` 下,
// 需要写入相同优先级的选择器才能覆盖默认值
const selector = isDark.value
? `:root[theme-mode='dark']`
: `:root[theme-mode='light']`;
updateCSSVariables(variables, `__vben_design_tdesign_styles__`, selector);
},
{ immediate: true },
);
}

File diff suppressed because it is too large Load Diff

View File

@ -41,20 +41,20 @@ catalog:
'@changesets/changelog-github': ^0.7.0
'@changesets/cli': ^2.31.0
'@changesets/git': ^3.0.4
'@clack/prompts': ^1.4.0
'@commitlint/cli': ^21.0.1
'@commitlint/config-conventional': ^21.0.1
'@clack/prompts': ^1.5.0
'@commitlint/cli': ^21.0.2
'@commitlint/config-conventional': ^21.0.2
'@ctrl/tinycolor': ^4.2.0
'@eslint-community/eslint-plugin-eslint-comments': ^4.7.2
'@eslint/js': ^10.0.1
'@faker-js/faker': ^10.4.0
'@iconify/json': ^2.2.479
'@iconify/json': ^2.2.481
'@iconify/tailwind4': ^1.2.3
'@iconify/vue': ^5.0.1
'@intlify/core-base': ^11.4.4
'@intlify/unplugin-vue-i18n': ^11.2.3
'@jspm/generator': ^2.16.1
'@lucide/vue': ^1.16.0
'@lucide/vue': ^1.17.0
'@manypkg/get-packages': ^3.1.0
'@nolebase/vitepress-plugin-git-changelog': ^2.18.2
'@playwright/test': ^1.60.0
@ -64,19 +64,19 @@ catalog:
'@tailwindcss/vite': ^4.3.0
'@tanstack/vue-query': ^5.100.14
'@tanstack/vue-store': ^0.11.0
'@tiptap/core': ^3.23.6
'@tiptap/extension-document': ^3.23.6
'@tiptap/extension-highlight': ^3.23.6
'@tiptap/extension-image': ^3.23.6
'@tiptap/extension-link': ^3.23.6
'@tiptap/extension-placeholder': ^3.23.6
'@tiptap/extension-text-align': ^3.23.6
'@tiptap/extension-text-style': ^3.23.6
'@tiptap/extension-underline': ^3.23.6
'@tiptap/pm': ^3.23.6
'@tiptap/starter-kit': ^3.23.6
'@tiptap/vue-3': ^3.23.6
'@tsdown/css': ^0.22.0
'@tiptap/core': ^3.24.0
'@tiptap/extension-document': ^3.24.0
'@tiptap/extension-highlight': ^3.24.0
'@tiptap/extension-image': ^3.24.0
'@tiptap/extension-link': ^3.24.0
'@tiptap/extension-placeholder': ^3.24.0
'@tiptap/extension-text-align': ^3.24.0
'@tiptap/extension-text-style': ^3.24.0
'@tiptap/extension-underline': ^3.24.0
'@tiptap/pm': ^3.24.0
'@tiptap/starter-kit': ^3.24.0
'@tiptap/vue-3': ^3.24.0
'@tsdown/css': ^0.22.1
'@types/archiver': ^7.0.0
'@types/html-minifier-terser': ^7.0.2
'@types/json-bigint': ^1.0.4
@ -87,8 +87,8 @@ catalog:
'@types/qrcode': ^1.5.6
'@types/qs': ^6.15.1
'@types/sortablejs': ^1.15.9
'@typescript-eslint/eslint-plugin': ^8.60.0
'@typescript-eslint/parser': ^8.60.0
'@typescript-eslint/eslint-plugin': ^8.60.1
'@typescript-eslint/parser': ^8.60.1
'@vee-validate/zod': ^4.15.1
'@vite-pwa/vitepress': ^1.1.0
'@vitejs/plugin-vue': ^6.0.7
@ -112,26 +112,26 @@ catalog:
commitlint-plugin-function-rules: ^5.0.1
consola: ^3.4.2
cross-env: ^10.1.0
cspell: ^10.0.0
cspell: ^10.0.1
cz-git: ^1.13.1
czg: ^1.13.1
dayjs: ^1.11.21
defu: ^6.1.7
dotenv: ^17.4.2
echarts: ^6.1.0
element-plus: ^2.14.0
element-plus: ^2.14.1
es-toolkit: ^1.47.0
eslint: ^10.4.0
eslint: ^10.4.1
eslint-plugin-better-tailwindcss: ^4.5.0
eslint-plugin-command: ^3.5.2
eslint-plugin-jsonc: ^3.1.2
eslint-plugin-jsonc: ^3.2.0
eslint-plugin-n: ^18.0.1
eslint-plugin-perfectionist: ^5.9.0
eslint-plugin-pnpm: ^1.6.1
eslint-plugin-unicorn: ^64.0.0
eslint-plugin-unused-imports: ^4.4.1
eslint-plugin-vue: ^10.9.1
eslint-plugin-yml: ^3.3.2
eslint-plugin-yml: ^3.4.0
execa: ^9.6.1
find-up: ^8.0.0
get-port: ^7.2.0
@ -142,16 +142,16 @@ catalog:
is-ci: ^4.1.0
json-bigint: ^1.0.0
jsonwebtoken: ^9.0.3
knip: ^6.14.2
lefthook: ^2.1.8
knip: ^6.15.0
lefthook: ^2.1.9
lodash.clonedeep: ^4.5.0
medium-zoom: ^1.1.0
naive-ui: ^2.44.1
nitropack: ^2.13.4
nprogress: ^0.2.0
ora: ^9.4.0
oxfmt: ^0.52.0
oxlint: ^1.67.0
oxfmt: ^0.53.0
oxlint: ^1.68.0
oxlint-tsgolint: ^0.23.0
pinia: ^3.0.4
pinia-plugin-persistedstate: ^4.7.1
@ -184,31 +184,31 @@ catalog:
tdesign-vue-next: ^1.20.0
theme-colors: ^0.1.0
tippy.js: ^6.3.7
tsdown: ^0.22.0
turbo: ^2.9.15
tsdown: ^0.22.1
turbo: ^2.9.16
tw-animate-css: ^1.4.0
typescript: ^6.0.3
unplugin-dts: ^1.0.1
unplugin-dts: ^1.0.2
unplugin-element-plus: ^0.11.2
unplugin-vue: ^7.2.0
vee-validate: ^4.15.1
vite: 8.0.10
vite: ^8.0.16
vite-plugin-compression: ^0.5.1
vite-plugin-lazy-import: ^1.0.7
vite-plugin-pwa: ^1.3.0
vite-plugin-vue-devtools: ^8.1.2
vitepress: ^2.0.0-alpha.17
vitepress-plugin-group-icons: ^1.7.5
vitest: ^4.1.7
vitest: ^4.1.8
vue: ^3.5.35
vue-eslint-parser: ^10.4.0
vue-i18n: ^11.4.4
vue-json-pretty: ^2.6.0
vue-router: ^5.0.7
vue-router: ^5.1.0
vue-tippy: ^6.7.1
vue-tsc: ^3.3.2
vxe-pc-ui: ^4.14.24
vxe-table: ^4.19.3
vue-tsc: ^3.3.3
vxe-pc-ui: ^4.14.26
vxe-table: ^4.19.6
watermark-js-plus: ^1.6.3
yaml-eslint-parser: ^2.0.0
zod: ^3.25.76