From 9d6478dcaa3c3942e6c07695495fac5d328b2178 Mon Sep 17 00:00:00 2001 From: kilisamemarisaaa <1798456934@qq.com> Date: Mon, 31 Aug 2026 13:35:19 +0800 Subject: [PATCH] fix(@vben/plugins): stabilize vxe toolbar slot options --- .changeset/steady-grids-measure.md | 5 + .../__tests__/use-vxe-grid.reactivity.test.ts | 162 ++++++++++++++++++ .../plugins/src/vxe-table/use-vxe-grid.vue | 14 +- 3 files changed, 174 insertions(+), 7 deletions(-) create mode 100644 .changeset/steady-grids-measure.md create mode 100644 packages/effects/plugins/src/vxe-table/__tests__/use-vxe-grid.reactivity.test.ts diff --git a/.changeset/steady-grids-measure.md b/.changeset/steady-grids-measure.md new file mode 100644 index 000000000..b928b16d0 --- /dev/null +++ b/.changeset/steady-grids-measure.md @@ -0,0 +1,5 @@ +--- +'@vben/plugins': patch +--- + +fix(@vben/plugins): keep vxe toolbar options stable when slot content updates diff --git a/packages/effects/plugins/src/vxe-table/__tests__/use-vxe-grid.reactivity.test.ts b/packages/effects/plugins/src/vxe-table/__tests__/use-vxe-grid.reactivity.test.ts new file mode 100644 index 000000000..a165ee069 --- /dev/null +++ b/packages/effects/plugins/src/vxe-table/__tests__/use-vxe-grid.reactivity.test.ts @@ -0,0 +1,162 @@ +import type { App } from 'vue'; + +import { createApp, defineComponent, h, nextTick, ref } from 'vue'; + +import { describe, expect, it, vi } from 'vitest'; + +import VbenVxeGrid from '../use-vxe-grid.vue'; + +const observedToolbarConfigs = vi.hoisted(() => [] as unknown[]); + +vi.mock('@vben/hooks', async () => { + const { ref } = await import('vue'); + + return { + usePriorityValues: () => ({ + class: ref(undefined), + formOptions: ref(undefined), + gridClass: ref(undefined), + gridEvents: ref(undefined), + gridOptions: ref({ + columns: [ + { + field: 'operation', + fixed: 'right', + width: 'auto', + }, + ], + }), + separator: ref(undefined), + showSearchForm: ref(true), + tableData: ref(undefined), + tableTitle: ref(undefined), + tableTitleHelp: ref(undefined), + viewedRowOptions: ref(undefined), + }), + }; +}); + +vi.mock('@vben/preferences', async () => { + const { ref } = await import('vue'); + return { usePreferences: () => ({ isMobile: ref(false) }) }; +}); + +vi.mock('@vben/icons', async () => { + const { defineComponent, h } = await import('vue'); + return { + EmptyIcon: defineComponent(() => () => h('span')), + }; +}); + +vi.mock('@vben/locales', () => ({ $t: (key: string) => key })); + +vi.mock('@vben-core/shadcn-ui', async () => { + const { defineComponent, h } = await import('vue'); + const Stub = defineComponent((_, { slots }) => () => h('div', slots.default?.())); + return { VbenHelpTooltip: Stub, VbenLoading: Stub }; +}); + +vi.mock('vxe-pc-ui', async () => { + const { defineComponent, h } = await import('vue'); + return { VxeButton: defineComponent(() => () => h('button')) }; +}); + +vi.mock('vxe-table', async () => { + const { defineComponent, h } = await import('vue'); + + return { + VxeGrid: defineComponent({ + inheritAttrs: false, + setup(_, { attrs, slots }) { + return () => { + observedToolbarConfigs.push(attrs.toolbarConfig); + return h('div', [ + slots['toolbar-actions']?.({}), + slots['toolbar-tools']?.({}), + ]); + }; + }, + }), + VxeUI: { getConfig: () => ({ grid: {} }) }, + }; +}); + +vi.mock('../extends', () => ({ extendProxyOptions: vi.fn() })); + +vi.mock('../init', async () => { + const { defineComponent, h } = await import('vue'); + return { + useTableForm: () => [ + defineComponent(() => () => h('form')), + { + getLatestSubmissionValues: vi.fn(), + getState: vi.fn(() => ({ compact: true })), + getValues: vi.fn(async () => ({})), + reset: vi.fn(), + setLatestSubmissionValues: vi.fn(), + setState: vi.fn(), + unmount: vi.fn(), + }, + ], + }; +}); + +vi.mock('../viewed-row', () => ({ + applyViewedRowOptions: vi.fn(), + useViewedRow: vi.fn(), +})); + +describe('vben vxe grid toolbar slots', () => { + it.each(['table-title', 'toolbar-actions', 'toolbar-tools'])( + 'keeps toolbar options stable when the %s slot updates', + async (slotName) => { + observedToolbarConfigs.length = 0; + const loading = ref(false); + const api = { + grid: { commitProxy: vi.fn() }, + mount: vi.fn(), + reload: vi.fn(), + setState: vi.fn(), + toggleSearchForm: vi.fn(), + unmount: vi.fn(), + useStore: vi.fn(() => ({})), + }; + const Consumer = defineComponent(() => () => + h(VbenVxeGrid, { api } as any, { + [slotName]: () => + h( + 'span', + { 'data-slot-state': '' }, + loading.value ? 'loading' : 'idle', + ), + }), + ); + const host = document.createElement('div'); + document.body.append(host); + const app: App = createApp(Consumer); + + try { + app.mount(host); + await nextTick(); + await nextTick(); + + expect(host.querySelector('[data-slot-state]')?.textContent).toBe( + 'idle', + ); + const initialToolbarConfig = observedToolbarConfigs.at(-1); + + loading.value = true; + await nextTick(); + await nextTick(); + + expect(host.querySelector('[data-slot-state]')?.textContent).toBe( + 'loading', + ); + expect(observedToolbarConfigs.at(-1)).toBe(initialToolbarConfig); + } finally { + app.unmount(); + host.remove(); + } + }, + ); +}); diff --git a/packages/effects/plugins/src/vxe-table/use-vxe-grid.vue b/packages/effects/plugins/src/vxe-table/use-vxe-grid.vue index ae671258f..f45912d3e 100644 --- a/packages/effects/plugins/src/vxe-table/use-vxe-grid.vue +++ b/packages/effects/plugins/src/vxe-table/use-vxe-grid.vue @@ -152,20 +152,20 @@ const [Form, formApi] = useTableForm({ }); const showTableTitle = computed(() => { - return !!slots[TABLE_TITLE]?.() || tableTitle.value; + return !!slots[TABLE_TITLE] || tableTitle.value; }); const showToolbar = computed(() => { return ( - !!slots[TOOLBAR_ACTIONS]?.() || - !!slots[TOOLBAR_TOOLS]?.() || + !!slots[TOOLBAR_ACTIONS] || + !!slots[TOOLBAR_TOOLS] || showTableTitle.value ); }); const toolbarOptions = computed(() => { - const slotActions = slots[TOOLBAR_ACTIONS]?.(); - const slotTools = slots[TOOLBAR_TOOLS]?.(); + const hasSlotActions = !!slots[TOOLBAR_ACTIONS]; + const hasSlotTools = !!slots[TOOLBAR_TOOLS]; const searchBtn: VxeToolbarPropTypes.ToolConfig = { code: 'search', icon: 'vxe-icon-search', @@ -194,10 +194,10 @@ const toolbarOptions = computed(() => { // 强制使用固定的toolbar配置,不允许用户自定义 // 减少配置的复杂度,以及后续维护的成本 toolbarConfig.slots = { - ...(slotActions || showTableTitle.value + ...(hasSlotActions || showTableTitle.value ? { buttons: TOOLBAR_ACTIONS } : {}), - ...(slotTools ? { tools: TOOLBAR_TOOLS } : {}), + ...(hasSlotTools ? { tools: TOOLBAR_TOOLS } : {}), }; return { toolbarConfig }; });