diff --git a/apps/web-antdv-next/src/adapter/component/__tests__/theme-aware-button.test.ts b/apps/web-antdv-next/src/adapter/component/__tests__/theme-aware-button.test.ts new file mode 100644 index 000000000..bbcf93a74 --- /dev/null +++ b/apps/web-antdv-next/src/adapter/component/__tests__/theme-aware-button.test.ts @@ -0,0 +1,130 @@ +/* eslint-disable vue/one-component-per-file, vue/require-default-prop */ + +import type { App, Component } from 'vue'; + +import { createApp, defineComponent, h } from 'vue'; + +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import { createThemeAwareButton } from '../theme-aware-button'; + +const mocks = vi.hoisted(() => ({ + compact: false, + contextTheme: undefined as object | undefined, + isDark: false, + providerThemes: [] as any[], + tokens: { colorPrimary: '#e11d48' }, +})); + +vi.mock('@vben/hooks', () => ({ + useAntdDesignTokens: () => ({ tokens: mocks.tokens }), +})); + +vi.mock('@vben/preferences', () => ({ + preferences: { + app: { + get compact() { + return mocks.compact; + }, + }, + }, + usePreferences: () => ({ + isDark: { + get value() { + return mocks.isDark; + }, + }, + }), +})); + +vi.mock('antdv-next/config-provider/context', () => ({ + useConfig: () => ({ value: { theme: mocks.contextTheme } }), +})); + +vi.mock('antdv-next', async () => { + const { defineComponent, h } = + await vi.importActual('vue'); + return { + ConfigProvider: defineComponent({ + props: { theme: Object }, + setup(props, { slots }) { + mocks.providerThemes.push(props.theme); + return () => + h('section', { 'data-theme-provider': '' }, slots.default?.()); + }, + }), + theme: { + compactAlgorithm: 'compact', + darkAlgorithm: 'dark', + defaultAlgorithm: 'default', + }, + }; +}); + +const Button = defineComponent({ + inheritAttrs: false, + props: { type: String }, + setup(props, { attrs, slots }) { + return () => + h( + 'button', + { ...attrs, 'data-button-type': props.type }, + slots.default?.(), + ); + }, +}); + +let activeApp: App | undefined; + +function mountButton(component: Component) { + const container = document.createElement('div'); + document.body.append(container); + activeApp = createApp(() => + h(component, { 'data-probe': 'button' }, () => 'Submit'), + ); + activeApp.mount(container); + return container; +} + +beforeEach(() => { + mocks.compact = false; + mocks.contextTheme = undefined; + mocks.isDark = false; + mocks.providerThemes.length = 0; +}); + +afterEach(() => { + activeApp?.unmount(); + activeApp = undefined; + document.body.innerHTML = ''; +}); + +describe('createThemeAwareButton', () => { + it('uses the existing ConfigProvider context without adding a wrapper', () => { + mocks.contextTheme = { token: mocks.tokens }; + const container = mountButton(createThemeAwareButton(Button, 'primary')); + + expect(container.querySelector('[data-theme-provider]')).toBeNull(); + expect(container.querySelector('button')?.dataset.buttonType).toBe( + 'primary', + ); + expect(container.querySelector('button')?.dataset.probe).toBe('button'); + }); + + it('provides the current Vben theme when no ConfigProvider is present', () => { + mocks.compact = true; + mocks.isDark = true; + const container = mountButton(createThemeAwareButton(Button, 'default')); + + expect(container.querySelector('[data-theme-provider]')).not.toBeNull(); + expect(mocks.providerThemes).toEqual([ + { + algorithm: ['dark', 'compact'], + token: mocks.tokens, + }, + ]); + expect(container.querySelector('button')?.dataset.buttonType).toBe( + 'default', + ); + }); +}); diff --git a/apps/web-antdv-next/src/adapter/component/index.ts b/apps/web-antdv-next/src/adapter/component/index.ts index 6f40a6a43..f51694bc6 100644 --- a/apps/web-antdv-next/src/adapter/component/index.ts +++ b/apps/web-antdv-next/src/adapter/component/index.ts @@ -73,6 +73,8 @@ import { isEmpty } from '@vben/utils'; import { message, Modal, notification } from 'antdv-next'; import { upload_file } from '#/api'; + +import { createThemeAwareButton } from './theme-aware-button'; type AdapterUploadProps = UploadProps & { aspectRatio?: string; crop?: boolean; @@ -89,6 +91,7 @@ const AutoComplete = defineAsyncComponent( const Button = defineAsyncComponent( () => import('antdv-next/dist/button/index'), ); + const Checkbox = defineAsyncComponent( () => import('antdv-next/dist/checkbox/index'), ); @@ -716,9 +719,7 @@ async function initComponentAdapter() { CheckboxGroup, DatePicker, // 自定义默认按钮 - DefaultButton: (props, { attrs, slots }) => { - return h(Button, { ...props, attrs, type: 'default' }, slots); - }, + DefaultButton: createThemeAwareButton(Button, 'default'), Divider, IconPicker: withDefaultPlaceholder(IconPicker, 'select', { iconSlot: 'addonAfter', @@ -730,9 +731,7 @@ async function initComponentAdapter() { InputPassword: withDefaultPlaceholder(InputPassword, 'input'), Mentions: withDefaultPlaceholder(Mentions, 'input'), // 自定义主要按钮 - PrimaryButton: (props, { attrs, slots }) => { - return h(Button, { ...props, attrs, type: 'primary' }, slots); - }, + PrimaryButton: createThemeAwareButton(Button, 'primary'), Radio, RadioGroup, RangePicker, diff --git a/apps/web-antdv-next/src/adapter/component/theme-aware-button.ts b/apps/web-antdv-next/src/adapter/component/theme-aware-button.ts new file mode 100644 index 000000000..b3f233ce3 --- /dev/null +++ b/apps/web-antdv-next/src/adapter/component/theme-aware-button.ts @@ -0,0 +1,47 @@ +import type { Component } from 'vue'; + +import { computed, defineComponent, h } from 'vue'; + +import { useAntdDesignTokens } from '@vben/hooks'; +import { preferences, usePreferences } from '@vben/preferences'; + +import { ConfigProvider, theme } from 'antdv-next'; +import { useConfig } from 'antdv-next/config-provider/context'; + +function createThemeAwareButton( + Button: Component, + type: 'default' | 'primary', +) { + return defineComponent({ + inheritAttrs: false, + setup(props, { attrs, slots }) { + const config = useConfig(); + if (config.value?.theme) { + return () => h(Button, { ...attrs, ...props, type }, slots); + } + + const { isDark } = usePreferences(); + const { tokens } = useAntdDesignTokens(); + const buttonTheme = computed(() => { + const algorithm = [ + isDark.value ? theme.darkAlgorithm : theme.defaultAlgorithm, + ]; + if (preferences.app.compact) { + algorithm.push(theme.compactAlgorithm); + } + return { algorithm, token: tokens }; + }); + + return () => + h( + ConfigProvider, + { theme: buttonTheme.value }, + { + default: () => h(Button, { ...attrs, ...props, type }, slots), + }, + ); + }, + }); +} + +export { createThemeAwareButton }; diff --git a/apps/web-antdv-next/src/app.vue b/apps/web-antdv-next/src/app.vue index 46366e915..cc6508750 100644 --- a/apps/web-antdv-next/src/app.vue +++ b/apps/web-antdv-next/src/app.vue @@ -30,9 +30,9 @@ const tokenTheme = computed(() => { }); watch( - tokenTheme, - (themeConfig) => { - ConfigProvider.config({ theme: themeConfig }); + [tokenTheme, antdLocale], + ([themeConfig, locale]) => { + ConfigProvider.config({ theme: themeConfig, locale }); }, { immediate: true }, );