mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2026-09-09 21:49:03 +00:00
feat(table): 为操作列新增 Popconfirm 二次确认能力
支持 popconfirm、confirmText、popconfirmWidth 配置,并扩展 Popconfirm 组件的 width 与 teleported 属性。
This commit is contained in:
parent
9aa251ce57
commit
9e8272b521
@ -234,6 +234,9 @@ export interface PaginationProps {
|
|||||||
|
|
||||||
export interface PopconfirmProps {
|
export interface PopconfirmProps {
|
||||||
title?: string;
|
title?: string;
|
||||||
|
width?: string | number;
|
||||||
|
/** 浮层是否插入到 body,默认 true。设为 false 时浮层内联渲染,便于嵌套在 hover 浮层中避免父级因 mouseleave 收起。 */
|
||||||
|
teleported?: boolean;
|
||||||
placement?:
|
placement?:
|
||||||
| 'top'
|
| 'top'
|
||||||
| 'left'
|
| 'left'
|
||||||
|
|||||||
@ -1,23 +1,46 @@
|
|||||||
<template>
|
<template>
|
||||||
<TMagicTooltip
|
<template v-for="(action, actionIndex) in config.actions" :key="actionIndex">
|
||||||
v-for="(action, actionIndex) in config.actions"
|
<TMagicPopconfirm
|
||||||
:placement="action.tooltipPlacement || 'top'"
|
v-if="action.popconfirm"
|
||||||
:key="actionIndex"
|
placement="top"
|
||||||
:disabled="!Boolean(action.tooltip)"
|
:width="action.popconfirmWidth"
|
||||||
:content="action.tooltip"
|
:title="formatter(action.confirmText, row) || '确定执行此操作?'"
|
||||||
>
|
@confirm="actionHandler(action, row, index)"
|
||||||
<TMagicButton
|
>
|
||||||
v-show="display(action.display, row) && !editState[index]"
|
<template #reference>
|
||||||
class="action-btn"
|
<TMagicButton
|
||||||
link
|
v-show="display(action.display, row) && !editState[index]"
|
||||||
size="small"
|
class="action-btn"
|
||||||
:type="action.buttonType || 'primary'"
|
link
|
||||||
:icon="action.icon"
|
size="small"
|
||||||
:disabled="disabled(action.disabled, row)"
|
:type="action.buttonType || 'primary'"
|
||||||
@click="actionHandler(action, row, index)"
|
:icon="action.icon"
|
||||||
><span v-html="formatter(action.text, row)"></span
|
:disabled="disabled(action.disabled, row)"
|
||||||
></TMagicButton>
|
>
|
||||||
</TMagicTooltip>
|
<span v-html="formatter(action.text, row)"></span>
|
||||||
|
</TMagicButton>
|
||||||
|
</template>
|
||||||
|
</TMagicPopconfirm>
|
||||||
|
|
||||||
|
<TMagicTooltip
|
||||||
|
v-else
|
||||||
|
:placement="action.tooltipPlacement || 'top'"
|
||||||
|
:disabled="!Boolean(action.tooltip)"
|
||||||
|
:content="action.tooltip"
|
||||||
|
>
|
||||||
|
<TMagicButton
|
||||||
|
v-show="display(action.display, row) && !editState[index]"
|
||||||
|
class="action-btn"
|
||||||
|
link
|
||||||
|
size="small"
|
||||||
|
:type="action.buttonType || 'primary'"
|
||||||
|
:icon="action.icon"
|
||||||
|
:disabled="disabled(action.disabled, row)"
|
||||||
|
@click="actionHandler(action, row, index)"
|
||||||
|
><span v-html="formatter(action.text, row)"></span
|
||||||
|
></TMagicButton>
|
||||||
|
</TMagicTooltip>
|
||||||
|
</template>
|
||||||
|
|
||||||
<TMagicButton
|
<TMagicButton
|
||||||
class="action-btn"
|
class="action-btn"
|
||||||
@ -42,7 +65,7 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { cloneDeep } from 'lodash-es';
|
import { cloneDeep } from 'lodash-es';
|
||||||
|
|
||||||
import { TMagicButton, tMagicMessage, TMagicTooltip } from '@tmagic/design';
|
import { TMagicButton, tMagicMessage, TMagicPopconfirm, TMagicTooltip } from '@tmagic/design';
|
||||||
|
|
||||||
import { ColumnActionConfig, ColumnConfig } from './schema';
|
import { ColumnActionConfig, ColumnConfig } from './schema';
|
||||||
|
|
||||||
|
|||||||
@ -28,6 +28,12 @@ export interface ColumnActionConfig {
|
|||||||
tooltip?: string;
|
tooltip?: string;
|
||||||
tooltipPlacement?: string;
|
tooltipPlacement?: string;
|
||||||
icon?: any;
|
icon?: any;
|
||||||
|
/** 为 true 时用 Popconfirm 包裹按钮,点击后需二次确认才会触发 handler */
|
||||||
|
popconfirm?: boolean;
|
||||||
|
/** Popconfirm 的确认提示文案,支持函数动态生成 */
|
||||||
|
confirmText?: string | ((row: any) => string);
|
||||||
|
/** Popconfirm 浮层宽度,数字按 px 处理 */
|
||||||
|
popconfirmWidth?: string | number;
|
||||||
handler?: (row: any, index: number) => Promise<any> | any;
|
handler?: (row: any, index: number) => Promise<any> | any;
|
||||||
before?: (row: any, index: number) => Promise<void> | void;
|
before?: (row: any, index: number) => Promise<void> | void;
|
||||||
after?: (row: any, index: number) => Promise<void> | void;
|
after?: (row: any, index: number) => Promise<void> | void;
|
||||||
|
|||||||
@ -86,6 +86,14 @@ export const createDesignMock = () => ({
|
|||||||
return () => h('div', { class: 'tmagic-popover-stub' }, [slots.reference?.(), slots.default?.()]);
|
return () => h('div', { class: 'tmagic-popover-stub' }, [slots.reference?.(), slots.default?.()]);
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
TMagicPopconfirm: defineComponent({
|
||||||
|
name: 'TMagicPopconfirm',
|
||||||
|
props: ['title', 'placement', 'width'],
|
||||||
|
emits: ['confirm', 'cancel'],
|
||||||
|
setup(props, { slots }) {
|
||||||
|
return () => h('div', { class: 'tmagic-popconfirm-stub' }, [props.title, slots.reference?.()]);
|
||||||
|
},
|
||||||
|
}),
|
||||||
tMagicMessage,
|
tMagicMessage,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import ActionsColumn from '../src/ActionsColumn.vue';
|
|||||||
import ComponentColumn from '../src/ComponentColumn.vue';
|
import ComponentColumn from '../src/ComponentColumn.vue';
|
||||||
import ExpandColumn from '../src/ExpandColumn.vue';
|
import ExpandColumn from '../src/ExpandColumn.vue';
|
||||||
import PopoverColumn from '../src/PopoverColumn.vue';
|
import PopoverColumn from '../src/PopoverColumn.vue';
|
||||||
|
import { ColumnActionConfig } from '../src/schema';
|
||||||
import TextColumn from '../src/TextColumn.vue';
|
import TextColumn from '../src/TextColumn.vue';
|
||||||
import { tMagicMessage } from '../test-support/design.mock';
|
import { tMagicMessage } from '../test-support/design.mock';
|
||||||
|
|
||||||
@ -126,7 +127,7 @@ describe('ActionsColumn.vue', () => {
|
|||||||
after: vi.fn(),
|
after: vi.fn(),
|
||||||
before: vi.fn(),
|
before: vi.fn(),
|
||||||
},
|
},
|
||||||
],
|
] as ColumnActionConfig[],
|
||||||
},
|
},
|
||||||
row: { id: 1, visible: true, locked: false },
|
row: { id: 1, visible: true, locked: false },
|
||||||
index: 0,
|
index: 0,
|
||||||
@ -180,6 +181,34 @@ describe('ActionsColumn.vue', () => {
|
|||||||
expect(deleteAction.handler).toHaveBeenCalledWith(props.row, 0);
|
expect(deleteAction.handler).toHaveBeenCalledWith(props.row, 0);
|
||||||
expect(deleteAction.after).toHaveBeenCalled();
|
expect(deleteAction.after).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('popconfirm action 用 Popconfirm 包裹并在确认时触发 handler', async () => {
|
||||||
|
const props = baseProps();
|
||||||
|
props.config.actions[1].popconfirm = true;
|
||||||
|
props.config.actions[1].confirmText = (row: any) => `确定删除${row.id}?`;
|
||||||
|
const wrapper = mount(ActionsColumn, { props });
|
||||||
|
const deleteAction = props.config.actions[1];
|
||||||
|
|
||||||
|
expect(wrapper.find('.tmagic-popconfirm-stub').exists()).toBe(true);
|
||||||
|
expect(wrapper.text()).toContain('确定删除1?');
|
||||||
|
|
||||||
|
// 普通点击按钮不应立即触发 handler,需等待 Popconfirm 确认
|
||||||
|
const btn = wrapper.findAll('.action-btn').find((b) => b.text().includes('删除'));
|
||||||
|
await btn?.trigger('click');
|
||||||
|
expect(deleteAction.handler).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
await wrapper.findComponent({ name: 'TMagicPopconfirm' }).vm.$emit('confirm');
|
||||||
|
expect(deleteAction.handler).toHaveBeenCalledWith(props.row, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('popconfirm 透传 popconfirmWidth 到 TMagicPopconfirm', () => {
|
||||||
|
const props = baseProps();
|
||||||
|
props.config.actions[1].popconfirm = true;
|
||||||
|
props.config.actions[1].popconfirmWidth = 240;
|
||||||
|
const wrapper = mount(ActionsColumn, { props });
|
||||||
|
const popconfirm = wrapper.findComponent({ name: 'TMagicPopconfirm' });
|
||||||
|
expect(popconfirm.props('width')).toBe(240);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('ComponentColumn.vue', () => {
|
describe('ComponentColumn.vue', () => {
|
||||||
|
|||||||
108
packages/table/tests/popconfirm.spec.ts
Normal file
108
packages/table/tests/popconfirm.spec.ts
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
/*
|
||||||
|
* Tencent is pleased to support the open source community by making TMagicEditor available.
|
||||||
|
*
|
||||||
|
* Copyright (C) 2025 Tencent. All rights reserved.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
import { describe, expect, test, vi } from 'vitest';
|
||||||
|
import { mount } from '@vue/test-utils';
|
||||||
|
|
||||||
|
import { setDesignConfig } from '@tmagic/design';
|
||||||
|
|
||||||
|
import elementPlusAdapter from '../../element-plus-adapter/src/index';
|
||||||
|
import ActionsColumn from '../src/ActionsColumn.vue';
|
||||||
|
|
||||||
|
setDesignConfig(elementPlusAdapter);
|
||||||
|
|
||||||
|
describe('ActionsColumn popconfirm (real element-plus)', () => {
|
||||||
|
test('点击按钮弹出 Popconfirm,确认后触发 handler', async () => {
|
||||||
|
const handler = vi.fn();
|
||||||
|
const wrapper = mount(ActionsColumn, {
|
||||||
|
props: {
|
||||||
|
columns: [],
|
||||||
|
config: {
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
text: '删除',
|
||||||
|
buttonType: 'danger',
|
||||||
|
popconfirm: true,
|
||||||
|
confirmText: (row: any) => `确定删除${row.title}?`,
|
||||||
|
handler,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
row: { title: 't1' },
|
||||||
|
index: 0,
|
||||||
|
editState: [],
|
||||||
|
} as any,
|
||||||
|
attachTo: document.body,
|
||||||
|
});
|
||||||
|
|
||||||
|
const btn = wrapper.findAll('.action-btn').find((b) => b.text().includes('删除'));
|
||||||
|
await btn?.trigger('click');
|
||||||
|
|
||||||
|
await vi.waitFor(() => {
|
||||||
|
expect(document.body.textContent).toContain('确定删除t1?');
|
||||||
|
});
|
||||||
|
|
||||||
|
const confirmBtn = Array.from(document.querySelectorAll('button')).find(
|
||||||
|
(b) => b.textContent?.trim().toLowerCase() === 'yes',
|
||||||
|
);
|
||||||
|
confirmBtn?.click();
|
||||||
|
await vi.waitFor(() => {
|
||||||
|
expect(handler).toHaveBeenCalledWith({ title: 't1' }, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
wrapper.unmount();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('popconfirmWidth 透传到浮层宽度', async () => {
|
||||||
|
const handler = vi.fn();
|
||||||
|
const wrapper = mount(ActionsColumn, {
|
||||||
|
props: {
|
||||||
|
columns: [],
|
||||||
|
config: {
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
text: '删除',
|
||||||
|
popconfirm: true,
|
||||||
|
popconfirmWidth: 240,
|
||||||
|
confirmText: '确定删除?',
|
||||||
|
handler,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
row: {},
|
||||||
|
index: 0,
|
||||||
|
editState: [],
|
||||||
|
} as any,
|
||||||
|
attachTo: document.body,
|
||||||
|
});
|
||||||
|
|
||||||
|
const btn = wrapper.findAll('.action-btn').find((b) => b.text().includes('删除'));
|
||||||
|
await btn?.trigger('click');
|
||||||
|
|
||||||
|
await vi.waitFor(() => {
|
||||||
|
expect(document.body.textContent).toContain('确定删除?');
|
||||||
|
});
|
||||||
|
|
||||||
|
const popper = Array.from(document.querySelectorAll('.el-popper')).find((e) =>
|
||||||
|
(e.textContent || '').includes('确定删除?'),
|
||||||
|
);
|
||||||
|
expect(popper).toBeTruthy();
|
||||||
|
expect(popper.style.width).toBe('240px');
|
||||||
|
|
||||||
|
wrapper.unmount();
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -1,5 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<TPopconfirm :content="title" :placement="placement" @confirm="confirmHandler" @cancel="cancelHandler">
|
<TPopconfirm
|
||||||
|
:content="title"
|
||||||
|
:placement="placement"
|
||||||
|
:popup-props="popupProps"
|
||||||
|
@confirm="confirmHandler"
|
||||||
|
@cancel="cancelHandler"
|
||||||
|
>
|
||||||
<template #default>
|
<template #default>
|
||||||
<slot name="reference"></slot>
|
<slot name="reference"></slot>
|
||||||
</template>
|
</template>
|
||||||
@ -7,6 +13,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
import { Popconfirm as TPopconfirm } from 'tdesign-vue-next';
|
import { Popconfirm as TPopconfirm } from 'tdesign-vue-next';
|
||||||
|
|
||||||
import type { PopconfirmProps } from '@tmagic/design';
|
import type { PopconfirmProps } from '@tmagic/design';
|
||||||
@ -15,10 +22,16 @@ defineOptions({
|
|||||||
name: 'TTDesignAdapterPopconfirm',
|
name: 'TTDesignAdapterPopconfirm',
|
||||||
});
|
});
|
||||||
|
|
||||||
defineProps<PopconfirmProps>();
|
const props = defineProps<PopconfirmProps>();
|
||||||
|
|
||||||
const emit = defineEmits(['confirm', 'cancel']);
|
const emit = defineEmits(['confirm', 'cancel']);
|
||||||
|
|
||||||
|
const popupProps = computed(() => {
|
||||||
|
if (!props.width) return undefined;
|
||||||
|
const width = typeof props.width === 'number' ? `${props.width}px` : props.width;
|
||||||
|
return { overlayInnerStyle: { width } };
|
||||||
|
});
|
||||||
|
|
||||||
const confirmHandler = (...args: any[]) => {
|
const confirmHandler = (...args: any[]) => {
|
||||||
emit('confirm', ...args);
|
emit('confirm', ...args);
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user