diff --git a/packages/table/src/ActionButton.vue b/packages/table/src/ActionButton.vue new file mode 100644 index 00000000..195af554 --- /dev/null +++ b/packages/table/src/ActionButton.vue @@ -0,0 +1,45 @@ + + + diff --git a/packages/table/src/ActionsColumn.vue b/packages/table/src/ActionsColumn.vue index ff25030c..166e9857 100644 --- a/packages/table/src/ActionsColumn.vue +++ b/packages/table/src/ActionsColumn.vue @@ -8,37 +8,83 @@ @confirm="actionHandler(action, row, index)" > + + +
+ +
+
+ - + @@ -65,8 +111,10 @@ + + diff --git a/packages/table/src/ComponentColumn.vue b/packages/table/src/ComponentColumn.vue index 00414b71..d09e5c67 100644 --- a/packages/table/src/ComponentColumn.vue +++ b/packages/table/src/ComponentColumn.vue @@ -1,19 +1,20 @@ diff --git a/packages/table/src/TextColumn.vue b/packages/table/src/TextColumn.vue index 1308f345..bfb45a88 100644 --- a/packages/table/src/TextColumn.vue +++ b/packages/table/src/TextColumn.vue @@ -47,8 +47,8 @@ import { TMagicButton, TMagicTag, TMagicTooltip } from '@tmagic/design'; import { type ContainerChangeEventData, MForm } from '@tmagic/form'; import type { FormItemConfig, FormValue } from '@tmagic/form-schema'; -import { setValueByKeyPath } from '@tmagic/utils'; +import { applyInlineEditChange } from './formHelpers'; import { ColumnConfig } from './schema'; import { formatter } from './utils'; @@ -70,12 +70,6 @@ const props = withDefaults( ); const formChangeHandler = (v: FormValue, eventData: ContainerChangeEventData) => { - if (eventData.changeRecords?.length) { - for (const record of eventData.changeRecords) { - if (record.propPath) { - setValueByKeyPath(record.propPath, record.value, props.editState[props.index]); - } - } - } + applyInlineEditChange(props.editState[props.index], eventData); }; diff --git a/packages/table/src/actionHelpers.ts b/packages/table/src/actionHelpers.ts new file mode 100644 index 00000000..01be098a --- /dev/null +++ b/packages/table/src/actionHelpers.ts @@ -0,0 +1,26 @@ +export const display = (fuc: boolean | Function | undefined, row: any) => { + if (typeof fuc === 'function') { + return fuc(row); + } + if (typeof fuc === 'boolean') { + return fuc; + } + return true; +}; + +export const disabled = (fuc: boolean | Function | undefined, row: any) => { + if (typeof fuc === 'function') { + return fuc(row); + } + if (typeof fuc === 'boolean') { + return fuc; + } + return false; +}; + +export const formatActionText = (fuc: string | Function | undefined, row: any) => { + if (typeof fuc === 'function') { + return fuc(row); + } + return fuc; +}; diff --git a/packages/table/src/componentHelpers.ts b/packages/table/src/componentHelpers.ts new file mode 100644 index 00000000..fafe7115 --- /dev/null +++ b/packages/table/src/componentHelpers.ts @@ -0,0 +1,15 @@ +import type { ColumnConfig } from './schema'; + +export const resolveComponentProps = (config: ColumnConfig, row: any, index: number) => { + if (typeof config.props === 'function') { + return config.props(row, index) || {}; + } + return config.props || {}; +}; + +export const resolveComponentListeners = (config: ColumnConfig, row: any, index: number) => { + if (typeof config.listeners === 'function') { + return config.listeners(row, index) || {}; + } + return config.listeners || {}; +}; diff --git a/packages/table/src/formHelpers.ts b/packages/table/src/formHelpers.ts new file mode 100644 index 00000000..34f5f58c --- /dev/null +++ b/packages/table/src/formHelpers.ts @@ -0,0 +1,12 @@ +import type { ContainerChangeEventData } from '@tmagic/form'; +import { setValueByKeyPath } from '@tmagic/utils'; + +export const applyInlineEditChange = (target: any, eventData: ContainerChangeEventData) => { + if (eventData.changeRecords?.length) { + for (const record of eventData.changeRecords) { + if (record.propPath) { + setValueByKeyPath(record.propPath, record.value, target); + } + } + } +}; diff --git a/packages/table/src/schema.ts b/packages/table/src/schema.ts index 05d71ca8..aa93c81f 100644 --- a/packages/table/src/schema.ts +++ b/packages/table/src/schema.ts @@ -18,8 +18,44 @@ import { FormConfig, FormValue } from '@tmagic/form'; +export type ColumnActionPlacement = + | 'auto' + | 'auto-start' + | 'auto-end' + | 'top' + | 'bottom' + | 'left' + | 'right' + | 'top-start' + | 'top-end' + | 'bottom-start' + | 'bottom-end' + | 'left-start' + | 'left-end' + | 'right-start' + | 'right-end'; + +/** 当 type 为 sub-actions 时,更多菜单(Popover)的配置 */ +export interface ColumnSubActionConfig { + /** Popover 的弹出位置,默认 bottom */ + placement?: ColumnActionPlacement; + /** Popover 浮层宽度,数字按 px 处理 */ + popoverWidth?: string | number; + /** 附加到 Popover 浮层上的自定义 class */ + popoverClass?: string; + /** Popover 关闭后是否销毁内容,默认 false */ + popoverDestroyOnClose?: boolean; + /** 更多菜单中的子动作配置 */ + items?: ColumnActionConfig[]; +} + export interface ColumnActionConfig { - type?: 'delete' | 'copy' | 'edit' | string; + /** + * 动作类型: + * - `delete` / `copy` / `edit`:内置语义,`edit` 会进入行内编辑态。 + * - `sub-actions`:点击后以 Popover 形式展开更多菜单,菜单配置通过 `subActionConfig` 提供。 + */ + type?: 'delete' | 'copy' | 'edit' | 'sub-actions' | string; buttonType?: string; display?: boolean | ((row: any) => boolean); disabled?: boolean | ((row: any) => boolean); @@ -34,10 +70,15 @@ export interface ColumnActionConfig { confirmText?: string | ((row: any) => string); /** Popconfirm 浮层宽度,数字按 px 处理 */ popconfirmWidth?: string | number; + /** 当 type 为 sub-actions 时,更多菜单(Popover)的配置 */ + subActionConfig?: ColumnSubActionConfig; handler?: (row: any, index: number) => Promise | any; before?: (row: any, index: number) => Promise | void; after?: (row: any, index: number) => Promise | void; - action?: (data: { data: any; index: number }) => Promise | void; + action?: (data: { + data: any; + index: number; + }) => Promise<{ ret: number; msg?: string } | void> | { ret: number; msg?: string } | void; cancel?: (data: { index: number }) => Promise | void; } diff --git a/packages/table/tests/columns.spec.ts b/packages/table/tests/columns.spec.ts index dc9c4c9b..e322a90d 100644 --- a/packages/table/tests/columns.spec.ts +++ b/packages/table/tests/columns.spec.ts @@ -7,9 +7,13 @@ import { describe, expect, test, vi } from 'vitest'; import { defineComponent, h } from 'vue'; import { mount } from '@vue/test-utils'; +import ActionButton from '../src/ActionButton.vue'; +import { disabled, display, formatActionText } from '../src/actionHelpers'; import ActionsColumn from '../src/ActionsColumn.vue'; import ComponentColumn from '../src/ComponentColumn.vue'; +import { resolveComponentListeners, resolveComponentProps } from '../src/componentHelpers'; import ExpandColumn from '../src/ExpandColumn.vue'; +import { applyInlineEditChange } from '../src/formHelpers'; import PopoverColumn from '../src/PopoverColumn.vue'; import { ColumnActionConfig } from '../src/schema'; import TextColumn from '../src/TextColumn.vue'; @@ -96,7 +100,7 @@ describe('TextColumn.vue', () => { }, }); expect(wrapper.find('.mform-stub').exists()).toBe(true); - wrapper.vm.formChangeHandler({}, { changeRecords: [{ propPath: 'name', value: 'new' }] }); + applyInlineEditChange(editState[0], { changeRecords: [{ propPath: 'name', value: 'new' }] }); expect(editState[0].name).toBe('new'); }); @@ -108,6 +112,31 @@ describe('TextColumn.vue', () => { }); }); +describe('actionHelpers', () => { + test('display / disabled / formatActionText 辅助函数', () => { + expect(display(() => false, {})).toBe(false); + expect(display(true, {})).toBe(true); + expect(display(undefined, {})).toBe(true); + expect(disabled(() => true, {})).toBe(true); + expect(disabled(false, {})).toBe(false); + expect(formatActionText((row: any) => row.id, { id: 2 })).toBe(2); + expect(formatActionText('静态', {})).toBe('静态'); + }); +}); + +describe('ActionButton.vue', () => { + test('渲染操作按钮', () => { + const wrapper = mount(ActionButton, { + props: { + action: { text: '操作' }, + row: { id: 1 }, + index: 0, + }, + }); + expect(wrapper.text()).toContain('操作'); + }); +}); + describe('ActionsColumn.vue', () => { const baseProps = () => ({ columns: [], @@ -134,17 +163,6 @@ describe('ActionsColumn.vue', () => { editState: [] as any[], }); - test('display / disabled / formatter 辅助函数', () => { - const wrapper = mount(ActionsColumn, { props: baseProps() }); - expect(wrapper.vm.display(() => false, {})).toBe(false); - expect(wrapper.vm.display(true, {})).toBe(true); - expect(wrapper.vm.display(undefined, {})).toBe(true); - expect(wrapper.vm.disabled(() => true, {})).toBe(true); - expect(wrapper.vm.disabled(false, {})).toBe(false); - expect(wrapper.vm.formatter((row: any) => row.id, { id: 2 })).toBe(2); - expect(wrapper.vm.formatter('静态', {})).toBe('静态'); - }); - test('编辑 / 保存 / 取消流程', async () => { const props = baseProps(); const wrapper = mount(ActionsColumn, { props }); @@ -233,18 +251,12 @@ describe('ComponentColumn.vue', () => { }, }); expect(wrapper.find('.inner').text()).toBe('Cell'); - wrapper.vm.componentListeners({ name: 'Cell' }, 3).click(); + resolveComponentListeners(wrapper.props('config'), { name: 'Cell' }, 3).click(); expect(onClick).toHaveBeenCalledWith(3); - const wrapper2 = mount(ComponentColumn, { - props: { - config: { component: innerComp, props: { label: '静态' }, listeners: {} }, - row: {}, - index: 0, - }, - }); - expect(wrapper2.vm.componentProps({}, 0)).toEqual({ label: '静态' }); - expect(wrapper2.vm.componentListeners({}, 0)).toEqual({}); + const staticConfig = { component: innerComp, props: { label: '静态' }, listeners: {} }; + expect(resolveComponentProps(staticConfig, {}, 0)).toEqual({ label: '静态' }); + expect(resolveComponentListeners(staticConfig, {}, 0)).toEqual({}); }); });