feat(design,editor,element-plus-adapter,form,table,tdesign-vue-next-adapter): 重构table组件,适配tdesign

This commit is contained in:
roymondchen 2025-10-21 18:59:26 +08:00
parent 1cb2d57ade
commit 08b476e04f
51 changed files with 1733 additions and 1206 deletions

View File

@ -30,3 +30,11 @@ const clickHandler = (...args: any[]) => {
emit('click', ...args);
};
</script>
<style lang="scss">
.tmagic-design-button {
.t-button__text {
align-items: center;
}
}
</style>

View File

@ -19,3 +19,16 @@ const uiComponent = ui?.component || 'el-icon';
const props = defineProps<IconProps>();
const uiProps = computed<IconProps>(() => ui?.props(props) || props);
</script>
<style lang="scss">
.t-t-design-adapter-icon {
justify-content: center;
align-items: center;
display: flex;
svg {
width: 1em;
height: 1em;
}
}
</style>

View File

@ -16,7 +16,7 @@
</template>
<script setup lang="ts">
import { computed, ref, watchEffect } from 'vue';
import { computed, useTemplateRef } from 'vue';
import { getDesignConfig } from './config';
import type { TableProps } from './types';
@ -37,7 +37,7 @@ const uiProps = computed<TableProps>(() => ui?.props(props) || props);
const emit = defineEmits(['select', 'sort-change', 'expand-change', 'cell-click']);
const table = ref<any>();
const tableRef = useTemplateRef('table');
const selectHandler = (...args: any[]) => {
emit('select', ...args);
@ -55,27 +55,21 @@ const cellClickHandler = (...args: any[]) => {
emit('cell-click', ...args);
};
let $el: HTMLDivElement | undefined;
watchEffect(() => {
$el = table.value?.$el;
});
defineExpose({
instance: table,
getEl: () => tableRef.value?.getTableRef().$el,
$el,
getTableRef: () => tableRef.value.getTableRef(),
clearSelection(...args: any[]) {
return table.value?.clearSelection(...args);
return tableRef.value?.clearSelection(...args);
},
toggleRowSelection(...args: any[]) {
return table.value?.toggleRowSelection(...args);
return tableRef.value?.toggleRowSelection(...args);
},
toggleRowExpansion(...args: any[]) {
return table.value?.toggleRowExpansion(...args);
return tableRef.value?.toggleRowExpansion(...args);
},
});
</script>

View File

@ -1,27 +0,0 @@
<template>
<component :is="uiComponent" v-bind="uiProps">
<template #default="{ $index, row }">
<!-- eslint-disable-next-line vue/valid-attribute-name -->
<slot :$index="$index" :row="row"></slot>
</template>
</component>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import { getDesignConfig } from './config';
import type { TableColumnProps } from './types';
defineOptions({
name: 'TMTableColumn',
});
const props = defineProps<TableColumnProps>();
const ui = getDesignConfig('components')?.tableColumn;
const uiComponent = ui?.component || 'el-table-column';
const uiProps = computed<TableColumnProps>(() => ui?.props(props) || props);
</script>

View File

@ -1,110 +0,0 @@
<template>
<component
class="tmagic-design-tree"
ref="tree"
:is="uiComponent"
v-bind="uiProps"
@node-click="nodeClickHandler"
@node-contextmenu="contextmenu"
@node-drag-end="handleDragEnd"
@node-collapse="handleCollapse"
@node-expand="handleExpand"
@check="checkHandler"
@mousedown="mousedownHandler"
@mouseup="mouseupHandler"
>
<template #default="{ data, node }">
<slot :data="data" :node="node"></slot>
</template>
</component>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue';
import { getDesignConfig } from './config';
import type { TreeProps } from './types';
defineOptions({
name: 'TMTree',
});
const props = defineProps<TreeProps>();
const ui = getDesignConfig('components')?.tree;
const uiComponent = ui?.component || 'el-tree';
const uiProps = computed<TreeProps>(() => ui?.props(props) || props);
const emit = defineEmits([
'node-click',
'node-contextmenu',
'node-drag-end',
'node-collapse',
'node-expand',
'check',
'mousedown',
'mouseup',
]);
const nodeClickHandler = (...args: any[]) => {
emit('node-click', ...args);
};
const contextmenu = (...args: any[]) => {
emit('node-contextmenu', ...args);
};
const handleDragEnd = (...args: any[]) => {
emit('node-drag-end', ...args);
};
const handleCollapse = (...args: any[]) => {
emit('node-collapse', ...args);
};
const handleExpand = (...args: any[]) => {
emit('node-expand', ...args);
};
const checkHandler = (...args: any[]) => {
emit('check', ...args);
};
const mousedownHandler = (...args: any[]) => {
emit('mousedown', ...args);
};
const mouseupHandler = (...args: any[]) => {
emit('mouseup', ...args);
};
const tree = ref<any>();
defineExpose({
getData() {
return tree.value?.data;
},
getStore() {
return tree.value?.store;
},
filter(...args: any[]) {
return tree.value?.filter(...args);
},
getNode(...args: any[]) {
return tree.value?.getNode(...args);
},
setCheckedKeys(...args: any[]) {
return tree.value?.setCheckedKeys(...args);
},
setCurrentKey(...args: any[]) {
return tree.value?.setCurrentKey(...args);
},
});
</script>

View File

@ -46,13 +46,11 @@ export { default as TMagicStep } from './Step.vue';
export { default as TMagicSteps } from './Steps.vue';
export { default as TMagicSwitch } from './Switch.vue';
export { default as TMagicTable } from './Table.vue';
export { default as TMagicTableColumn } from './TableColumn.vue';
export { default as TMagicTabPane } from './TabPane.vue';
export { default as TMagicTabs } from './Tabs.vue';
export { default as TMagicTag } from './Tag.vue';
export { default as TMagicTimePicker } from './TimePicker.vue';
export { default as TMagicTooltip } from './Tooltip.vue';
export { default as TMagicTree } from './Tree.vue';
export { default as TMagicUpload } from './Upload.vue';
export const tMagicMessage = {

View File

@ -182,6 +182,7 @@ export interface FormItemProps {
prop?: string;
labelWidth?: string | number;
rules?: any;
extra?: string;
}
export interface InputProps {
@ -295,17 +296,36 @@ export interface SwitchProps {
}
export interface TableProps {
columns?: TableColumnOptions[];
data?: any[];
border?: boolean;
maxHeight?: number | string;
defaultExpandAll?: boolean;
showHeader?: boolean;
rowKey?: string;
treeProps?: Record<string, any>;
emptyText?: string;
tooltipEffect?: string;
tooltipOptions?: any;
showOverflowTooltip?: boolean;
spanMethod?: (data: any) => any;
}
export interface TableColumnProps {
label?: string;
align?: string;
fixed?: string | boolean;
width?: string | number;
export interface TableColumnOptions<T = any> {
props: {
class?: string;
label?: string;
fixed?: 'left' | 'right' | boolean;
width?: number | string;
type?: 'default' | 'selection' | 'index' | 'expand';
prop?: string;
align?: string;
headerAlign?: string;
sortable?: boolean;
sortOrders?: Array<'ascending' | 'descending'>;
selectable?: (row: T, index: number) => boolean;
};
cell?: (scope: { row: T; $index: number }) => any;
}
export interface TabPaneProps {
@ -342,33 +362,6 @@ export interface TooltipProps {
offset?: number;
}
export interface TreeProps {
data?: any[];
emptyText?: string;
nodeKey?: string;
props?: any;
renderAfterExpand?: boolean;
load?: any;
renderContent?: any;
highlightCurrent?: boolean;
defaultExpandAll?: boolean;
checkOnClickNode?: boolean;
autoExpandParent?: boolean;
defaultExpandedKeys?: any[];
showCheckbox?: boolean;
checkStrictly?: boolean;
defaultCheckedKeys?: any[];
currentNodeKey?: string | number;
filterNodeMethod?: (value: any, data: any, node: any) => boolean;
accordion?: boolean;
indent?: number;
icon?: any;
lazy?: boolean;
draggable?: boolean;
allowDrag?: (node: any) => boolean;
allowDrop?: any;
}
export interface UploadProps {
action?: string;
autoUpload?: boolean;
@ -635,8 +628,8 @@ export interface Components {
| DefineComponent<
TableProps,
{
instance: any;
$el: HTMLDivElement | undefined;
getEl: () => HTMLElement | undefined;
getTableRef: () => any;
clearSelection: (...args: any[]) => void;
toggleRowSelection: (...args: any[]) => void;
toggleRowExpansion: (...args: any[]) => void;
@ -647,11 +640,6 @@ export interface Components {
props: (props: TableProps) => TableProps;
};
tableColumn: {
component: DefineComponent<TableColumnProps, {}, any> | string;
props: (props: TableColumnProps) => TableColumnProps;
};
tabPane: {
component: DefineComponent<TabPaneProps, {}, any> | string;
props: (props: TabPaneProps) => TabPaneProps;
@ -677,24 +665,6 @@ export interface Components {
props: (props: TooltipProps) => TooltipProps;
};
tree: {
component:
| DefineComponent<
TreeProps,
{
getData: () => TreeProps['data'];
getStore: () => any;
filter: (...args: any[]) => any;
getNode: (...args: any[]) => any;
setCheckedKeys: (...args: any[]) => any;
setCurrentKey: (...args: any[]) => any;
},
any
>
| string;
props: (props: TreeProps) => TreeProps;
};
upload: {
component:
| DefineComponent<

View File

@ -1,5 +1,5 @@
<template>
<TMagicCollapse class="m-fields-style-setter" :model-value="collapseValue">
<TMagicCollapse class="m-fields-style-setter" v-model="collapseValue">
<template v-for="(item, index) in list" :key="index">
<TMagicCollapseItem :name="`${index}`">
<template #title><MIcon :icon="Grid"></MIcon>{{ item.title }}</template>

View File

@ -5,7 +5,7 @@
<SearchInput @search="filterTextChangeHandler"></SearchInput>
<slot name="component-list" :component-group-list="list">
<TMagicCollapse class="ui-component-panel" :model-value="collapseValue">
<TMagicCollapse class="ui-component-panel" v-model="collapseValue">
<template v-for="(group, index) in list">
<TMagicCollapseItem v-if="group.items && group.items.length" :key="index" :name="`${index}`">
<template #title><MIcon :icon="Grid"></MIcon>{{ group.title }}</template>
@ -34,7 +34,7 @@
</template>
<script lang="ts" setup>
import { computed, inject, ref } from 'vue';
import { computed, inject, ref, watch } from 'vue';
import { Grid } from '@element-plus/icons-vue';
import serialize from 'serialize-javascript';
@ -74,10 +74,19 @@ const list = computed<ComponentGroup[]>(() =>
items: group.items.filter((item: ComponentItem) => item.text.includes(searchText.value)),
})),
);
const collapseValue = computed(() =>
Array(list.value?.length)
.fill(1)
.map((x, i) => `${i}`),
const collapseValue = ref();
watch(
list,
() => {
collapseValue.value = Array(list.value?.length)
.fill(1)
.map((x, i) => `${i}`);
},
{
immediate: true,
},
);
let timeout: ReturnType<typeof setTimeout> | undefined;

View File

@ -1,3 +1,5 @@
@use "../common/var" as *;
.background-position-container {
display: flex;
width: 100%;
@ -6,8 +8,8 @@
flex-wrap: wrap;
width: 80px;
height: auto;
.el-button {
& + .el-button {
.tmagic-design-button {
& + .tmagic-design-button {
margin-left: 2px;
}
&:nth-child(3n + 1) {
@ -15,13 +17,18 @@
}
}
.t-button--variant-text {
padding-left: 2px;
padding-right: 2px;
}
.position-icon {
position: relative;
width: 14px;
height: 14px;
border: 1px solid #1d1f24;
&.active {
background-color: var(--el-color-primary);
background-color: var($theme-color);
&::after {
border: 1px solid #fff;
}

View File

@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/naming-convention */
/*
* Tencent is pleased to support the open source community by making TMagicEditor available.
*

View File

@ -0,0 +1,26 @@
<template>
<ElFormItem v-bind="itemProps">
<template #label>
<slot name="label"></slot>
</template>
<slot></slot>
<div v-if="extra" v-html="extra" class="m-form-tip"></div>
</ElFormItem>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import { FormItemProps } from '@tmagic/design';
defineOptions({
name: 'TElAdapterFormItem',
});
const props = defineProps<FormItemProps>();
const itemProps = computed(() => {
const { extra, ...rest } = props;
return rest;
});
</script>

View File

@ -0,0 +1,92 @@
<template>
<ElTable
ref="table"
:data="data"
:border="border"
:max-height="maxHeight"
:default-expand-all="defaultExpandAll"
:show-header="showHeader"
:row-key="rowKey"
:tree-props="treeProps"
:empty-text="emptyText"
:show-overflow-tooltip="showOverflowTooltip"
:tooltip-effect="tooltipEffect"
:tooltip-options="tooltipOptions"
:span-method="spanMethod"
@sort-change="sortChange"
@select="selectHandler"
@select-all="selectAllHandler"
@selection-change="selectionChangeHandler"
@cell-click="cellClickHandler"
@expand-change="expandChange"
>
<template v-for="(item, columnIndex) in columns" :key="columnIndex">
<ElTableColumn v-bind="item.props || {}">
<template #default="scope" v-if="item.cell">
<component :is="item.cell(scope)"></component>
</template>
</ElTableColumn>
</template>
</ElTable>
</template>
<script setup lang="ts">
import { useTemplateRef } from 'vue';
import { ElTable, ElTableColumn } from 'element-plus';
import type { TableProps } from '@tmagic/design';
defineOptions({
name: 'TElAdapterTable',
});
const emit = defineEmits(['sort-change', 'select', 'select-all', 'selection-change', 'expand-change', 'cell-click']);
defineProps<TableProps>();
const tableRef = useTemplateRef('table');
const sortChange = (data: any) => {
emit('sort-change', data);
};
const selectHandler = (...args: any[]) => {
emit('select', ...args);
};
const selectAllHandler = (...args: any[]) => {
emit('select-all', ...args);
};
const selectionChangeHandler = (...args: any[]) => {
emit('selection-change', ...args);
};
const cellClickHandler = (...args: any[]) => {
emit('cell-click', ...args);
};
const expandChange = (...args: any[]) => {
emit('expand-change', ...args);
};
const toggleRowSelection = (row: any, selected: boolean) => {
tableRef.value?.toggleRowSelection(row, selected);
};
const toggleRowExpansion = (row: any, expanded: boolean) => {
tableRef.value?.toggleRowExpansion(row, expanded);
};
const clearSelection = () => {
tableRef.value?.clearSelection();
};
defineExpose({
getEl: () => tableRef.value?.$el,
getTableRef: () => tableRef.value,
clearSelection,
toggleRowSelection,
toggleRowExpansion,
});
</script>

View File

@ -18,7 +18,6 @@ import {
ElDropdownItem,
ElDropdownMenu,
ElForm,
ElFormItem,
ElIcon,
ElInput,
ElInputNumber,
@ -37,14 +36,11 @@ import {
ElStep,
ElSteps,
ElSwitch,
ElTable,
ElTableColumn,
ElTabPane,
ElTabs,
ElTag,
ElTimePicker,
ElTooltip,
ElTree,
ElUpload,
useZIndex,
} from 'element-plus';
@ -83,17 +79,18 @@ import type {
StepProps,
StepsProps,
SwitchProps,
TableColumnProps,
TableProps,
TabPaneProps,
TabsProps,
TagProps,
TimePickerProps,
TooltipProps,
TreeProps,
UploadProps,
} from '@tmagic/design';
import FormItem from './FormItem.vue';
import Table from './Table.vue';
const adapter: DesignPluginOptions = {
useZIndex,
message: ElMessage,
@ -195,7 +192,7 @@ const adapter: DesignPluginOptions = {
},
formItem: {
component: ElFormItem as any,
component: FormItem as any,
props: (props: FormItemProps) => props,
},
@ -275,15 +272,10 @@ const adapter: DesignPluginOptions = {
},
table: {
component: ElTable as any,
component: Table as any,
props: (props: TableProps) => props,
},
tableColumn: {
component: ElTableColumn as any,
props: (props: TableColumnProps) => props,
},
tabPane: {
component: ElTabPane as any,
props: (props: TabPaneProps) => props,
@ -309,11 +301,6 @@ const adapter: DesignPluginOptions = {
props: (props: TooltipProps) => props,
},
tree: {
component: ElTree as any,
props: (props: TreeProps) => props,
},
upload: {
component: ElUpload as any,
props: (props: UploadProps) => props,

View File

@ -700,6 +700,7 @@ export interface TableConfig extends FormItem {
onSelect?: (mForm: FormState | undefined, data: any) => any;
defautSort?: SortProp;
defaultSort?: SortProp;
/** 是否支持拖拽排序 */
dropSort?: boolean;
/** 是否显示全屏按钮 */
enableFullscreen?: boolean;

View File

@ -46,8 +46,6 @@
@change="onChangeHandler"
@addDiffCount="onAddDiffCount"
></component>
<div v-if="extra && type !== 'table'" v-html="extra" class="m-form-tip"></div>
</TMagicFormItem>
<TMagicTooltip v-if="config.tip" placement="left">
@ -74,8 +72,6 @@
</TMagicTooltip>
<component v-else v-bind="fieldsProps" :is="tagName" :model="lastValues" @change="onChangeHandler"></component>
<div v-if="extra" v-html="extra" class="m-form-tip"></div>
</TMagicFormItem>
<TMagicTooltip v-if="config.tip" placement="left">
@ -100,8 +96,6 @@
</TMagicTooltip>
<component v-else v-bind="fieldsProps" :is="tagName" :model="model" @change="onChangeHandler"></component>
<div v-if="extra" v-html="extra" class="m-form-tip"></div>
</TMagicFormItem>
<TMagicTooltip v-if="config.tip" placement="left">
@ -231,8 +225,6 @@ const text = computed(() => filterFunction(mForm, props.config.text, props));
const tooltip = computed(() => filterFunction(mForm, props.config.tooltip, props));
const extra = computed(() => filterFunction(mForm, props.config.extra, props));
const rule = computed(() => getRules(mForm, props.config.rules, props));
const type = computed((): string => {
@ -266,6 +258,7 @@ const formItemProps = computed(() => ({
labelWidth: itemLabelWidth.value,
labelPosition: props.config.labelPosition,
rules: rule.value,
extra: filterFunction(mForm, props.config.extra, props),
}));
const itemLabelWidth = computed(() => props.config.labelWidth ?? props.labelWidth);

View File

@ -1,681 +0,0 @@
<template>
<div class="m-fields-table-wrap">
<teleport to="body" :disabled="!isFullscreen">
<div ref="mTable" class="m-fields-table" :class="{ 'm-fields-table-item-extra': config.itemExtra }">
<span v-if="config.extra" style="color: rgba(0, 0, 0, 0.45)" v-html="config.extra"></span>
<TMagicTooltip content="拖拽可排序" placement="left-start" :disabled="config.dropSort !== true">
<TMagicTable
v-if="model[modelName]"
ref="tMagicTable"
style="width: 100%"
:row-key="config.rowKey || 'id'"
:data="data"
:lastData="lastData"
:border="config.border"
:max-height="config.maxHeight"
:default-expand-all="true"
:key="updateKey"
@select="selectHandle"
@sort-change="sortChange"
>
<TMagicTableColumn v-if="config.itemExtra && !config.dropSort" :fixed="'left'" width="30" type="expand">
<template v-slot="scope">
<span v-html="itemExtra(config.itemExtra, scope.$index)" class="m-form-tip"></span>
</template>
</TMagicTableColumn>
<TMagicTableColumn
label="操作"
:width="config.operateColWidth || 100"
align="center"
:fixed="config.fixed === false ? undefined : 'left'"
>
<template v-slot="scope">
<slot name="operateCol" :scope="scope"></slot>
<TMagicButton
v-show="showDelete(scope.$index + 1 + pagecontext * pagesize - 1)"
size="small"
type="danger"
link
title="删除"
:icon="Delete"
@click="removeHandler(scope.$index + 1 + pagecontext * pagesize - 1)"
></TMagicButton>
<TMagicButton
v-if="copyable(scope.$index + 1 + pagecontext * pagesize - 1)"
link
size="small"
type="primary"
title="复制"
:icon="DocumentCopy"
:disabled="disabled"
@click="copyHandler(scope.$index + 1 + pagecontext * pagesize - 1)"
></TMagicButton>
</template>
</TMagicTableColumn>
<TMagicTableColumn v-if="sort && model[modelName] && model[modelName].length > 1" label="排序" width="60">
<template v-slot="scope">
<TMagicTooltip
v-if="scope.$index + 1 + pagecontext * pagesize - 1 !== 0"
content="点击上移,双击置顶"
placement="top"
>
<TMagicButton
plain
size="small"
type="primary"
:icon="ArrowUp"
:disabled="disabled"
link
@click="upHandler(scope.$index + 1 + pagecontext * pagesize - 1)"
@dblclick="topHandler(scope.$index + 1 + pagecontext * pagesize - 1)"
></TMagicButton>
</TMagicTooltip>
<TMagicTooltip
v-if="scope.$index + 1 + pagecontext * pagesize - 1 !== model[modelName].length - 1"
content="点击下移,双击置底"
placement="top"
>
<TMagicButton
plain
size="small"
type="primary"
:icon="ArrowDown"
:disabled="disabled"
link
@click="downHandler(scope.$index + 1 + pagecontext * pagesize - 1)"
@dblclick="bottomHandler(scope.$index + 1 + pagecontext * pagesize - 1)"
></TMagicButton>
</TMagicTooltip>
</template>
</TMagicTableColumn>
<TMagicTableColumn
v-if="selection"
align="center"
header-align="center"
type="selection"
width="45"
></TMagicTableColumn>
<TMagicTableColumn width="60" label="序号" v-if="showIndex && config.showIndex">
<template v-slot="scope">{{ scope.$index + 1 + pagecontext * pagesize }}</template>
</TMagicTableColumn>
<template v-for="(column, index) in config.items">
<TMagicTableColumn
v-if="column.type !== 'hidden' && display(column.display)"
:prop="column.name"
:width="column.width"
:label="column.label"
:sortable="column.sortable"
:sort-orders="['ascending', 'descending']"
:key="column[mForm?.keyProp || '__key'] ?? index"
:class-name="config.dropSort === true ? 'el-table__column--dropable' : ''"
>
<template #default="scope">
<Container
v-if="scope.$index > -1"
labelWidth="0"
:disabled="disabled"
:prop="getProp(scope.$index)"
:rules="column.rules"
:config="makeConfig(column, scope.row)"
:model="scope.row"
:lastValues="lastData[scope.$index]"
:is-compare="isCompare"
:size="size"
@change="changeHandler"
@addDiffCount="onAddDiffCount()"
></Container>
</template>
</TMagicTableColumn>
</template>
</TMagicTable>
</TMagicTooltip>
<slot></slot>
<div style="display: flex; justify-content: space-between; margin: 10px 0">
<TMagicButton v-if="addable" size="small" type="primary" :disabled="disabled" plain @click="newHandler()"
>新增一行</TMagicButton
>
<div style="display: flex">
<TMagicButton
:icon="Grid"
size="small"
type="primary"
@click="toggleMode"
v-if="enableToggleMode && config.enableToggleMode !== false && !isFullscreen"
>展开配置</TMagicButton
>
<TMagicButton
:icon="FullScreen"
size="small"
type="primary"
@click="toggleFullscreen"
v-if="config.enableFullscreen !== false"
>
{{ isFullscreen ? '退出全屏' : '全屏编辑' }}
</TMagicButton>
<TMagicUpload
v-if="importable"
style="display: inline-block"
ref="excelBtn"
action="/noop"
:disabled="disabled"
:on-change="excelHandler"
:auto-upload="false"
>
<TMagicButton size="small" type="success" :disabled="disabled" plain>导入EXCEL</TMagicButton>
</TMagicUpload>
<TMagicButton
v-if="importable"
size="small"
type="warning"
:disabled="disabled"
plain
@click="clearHandler()"
>清空</TMagicButton
>
</div>
</div>
<div class="bottom" style="text-align: right" v-if="config.pagination">
<TMagicPagination
layout="total, sizes, prev, pager, next, jumper"
:hide-on-single-page="model[modelName].length < pagesize"
:current-page="pagecontext + 1"
:page-sizes="[pagesize, 60, 120, 300]"
:page-size="pagesize"
:total="model[modelName].length"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
>
</TMagicPagination>
</div>
</div>
</teleport>
</div>
</template>
<script setup lang="ts">
import { computed, inject, onMounted, ref, toRefs, watchEffect } from 'vue';
import { ArrowDown, ArrowUp, Delete, DocumentCopy, FullScreen, Grid } from '@element-plus/icons-vue';
import { cloneDeep } from 'lodash-es';
import Sortable, { SortableEvent } from 'sortablejs';
import {
TMagicButton,
tMagicMessage,
TMagicPagination,
TMagicTable,
TMagicTableColumn,
TMagicTooltip,
TMagicUpload,
useZIndex,
} from '@tmagic/design';
import { asyncLoadJs } from '@tmagic/utils';
import type { ContainerChangeEventData, FormState, SortProp, TableColumnConfig, TableConfig } from '../schema';
import { display as displayFunc, initValue } from '../utils/form';
import Container from './Container.vue';
defineOptions({
name: 'MFormTable',
});
const props = withDefaults(
defineProps<{
model: any;
lastValues?: any;
isCompare?: boolean;
config: TableConfig;
name: string;
prop?: string;
labelWidth?: string;
sort?: boolean;
disabled?: boolean;
sortKey?: string;
text?: string;
size?: string;
enableToggleMode?: boolean;
showIndex?: boolean;
}>(),
{
prop: '',
sortKey: '',
enableToggleMode: true,
showIndex: true,
lastValues: () => ({}),
isCompare: false,
},
);
const emit = defineEmits(['change', 'select', 'addDiffCount']);
let timer: any | null = null;
const mForm = inject<FormState | undefined>('mForm');
const { nextZIndex } = useZIndex();
const tMagicTable = ref<InstanceType<typeof TMagicTable>>();
const excelBtn = ref<InstanceType<typeof TMagicUpload>>();
const mTable = ref<HTMLDivElement>();
const pagesize = ref(10);
const pagecontext = ref(0);
const updateKey = ref(1);
const isFullscreen = ref(false);
const modelName = computed(() => props.name || props.config.name || '');
const getDataByPage = (data: any[] = []) =>
data.filter(
(item: any, index: number) =>
index >= pagecontext.value * pagesize.value && index + 1 <= (pagecontext.value + 1) * pagesize.value,
);
const pageinationData = computed(() => getDataByPage(props.model[modelName.value]));
const data = computed(() => (props.config.pagination ? pageinationData.value : props.model[modelName.value]));
const lastData = computed(() =>
props.config.pagination ? getDataByPage(props.lastValues[modelName.value]) : props.lastValues[modelName.value] || [],
);
const sortChange = ({ prop, order }: SortProp) => {
if (order === 'ascending') {
props.model[modelName.value] = props.model[modelName.value].sort((a: any, b: any) => a[prop] - b[prop]);
} else if (order === 'descending') {
props.model[modelName.value] = props.model[modelName.value].sort((a: any, b: any) => b[prop] - a[prop]);
}
};
const swapArray = (index1: number, index2: number) => {
props.model[modelName.value].splice(index1, 0, props.model[modelName.value].splice(index2, 1)[0]);
if (props.sortKey) {
for (let i = props.model[modelName.value].length - 1, v = 0; i >= 0; i--, v++) {
props.model[modelName.value][v][props.sortKey] = i;
}
}
mForm?.$emit('field-change', props.prop, props.model[modelName.value]);
};
let sortable: Sortable | undefined;
const rowDrop = () => {
sortable?.destroy();
const tableEl = tMagicTable.value?.instance.$el;
const tBodyEl = tableEl?.querySelector('.el-table__body > tbody');
if (!tBodyEl) {
return;
}
sortable = Sortable.create(tBodyEl, {
draggable: '.tmagic-design-table-row',
filter: 'input', //
preventOnFilter: false, //
direction: 'vertical',
onEnd: ({ newIndex, oldIndex }: SortableEvent) => {
if (typeof newIndex === 'undefined') return;
if (typeof oldIndex === 'undefined') return;
swapArray(newIndex, oldIndex);
emit('change', props.model[modelName.value]);
mForm?.$emit('field-change', props.prop, props.model[modelName.value]);
},
});
};
const newHandler = async (row?: any) => {
if (props.config.max && props.model[modelName.value].length >= props.config.max) {
tMagicMessage.error(`最多新增配置不能超过${props.config.max}`);
return;
}
if (typeof props.config.beforeAddRow === 'function') {
const beforeCheckRes = props.config.beforeAddRow(mForm, {
model: props.model[modelName.value],
formValue: mForm?.values,
prop: props.prop,
});
if (!beforeCheckRes) return;
}
const columns = props.config.items;
const enumValues = props.config.enum || [];
let enumV = [];
const { length } = props.model[modelName.value];
const key = props.config.key || 'id';
let inputs: any = {};
if (enumValues.length) {
if (length >= enumValues.length) {
return;
}
enumV = enumValues.filter((item) => {
let i = 0;
for (; i < length; i++) {
if (item[key] === props.model[modelName.value][i][key]) {
break;
}
}
return i === length;
});
if (enumV.length > 0) {
// eslint-disable-next-line prefer-destructuring
inputs = enumV[0];
}
} else if (Array.isArray(row)) {
columns.forEach((column, index) => {
column.name && (inputs[column.name] = row[index]);
});
} else {
if (typeof props.config.defaultAdd === 'function') {
inputs = await props.config.defaultAdd(mForm, {
model: props.model[modelName.value],
formValue: mForm?.values,
});
} else if (props.config.defaultAdd) {
inputs = props.config.defaultAdd;
}
inputs = await initValue(mForm, {
config: columns,
initValues: inputs,
});
}
if (props.sortKey && length) {
inputs[props.sortKey] = props.model[modelName.value][length - 1][props.sortKey] - 1;
}
props.model[modelName.value].push(inputs);
emit('change', props.model[modelName.value], {
changeRecords: [
{
propPath: `${props.prop}.${props.model[modelName.value].length - 1}`,
value: inputs,
},
],
});
};
onMounted(() => {
if (props.config.defautSort) {
sortChange(props.config.defautSort);
} else if (props.config.defaultSort) {
sortChange(props.config.defaultSort);
}
if (props.sort && props.sortKey) {
props.model[modelName.value].sort((a: any, b: any) => b[props.sortKey] - a[props.sortKey]);
}
});
watchEffect(() => {
if (props.config.dropSort) {
rowDrop();
}
});
const addable = computed(() => {
if (!props.model[modelName.value].length) {
return true;
}
if (typeof props.config.addable === 'function') {
return props.config.addable(mForm, {
model: props.model[modelName.value],
formValue: mForm?.values,
prop: props.prop,
});
}
return typeof props.config.addable === 'undefined' ? true : props.config.addable;
});
const selection = computed(() => {
if (typeof props.config.selection === 'function') {
return props.config.selection(mForm, { model: props.model[modelName.value] });
}
return props.config.selection;
});
const importable = computed(() => {
if (typeof props.config.importable === 'function') {
return props.config.importable(mForm, {
formValue: mForm?.values,
model: props.model[modelName.value],
});
}
return typeof props.config.importable === 'undefined' ? false : props.config.importable;
});
const display = (fuc: any) => displayFunc(mForm, fuc, props);
const itemExtra = (fuc: any, index: number) => {
if (typeof fuc === 'function') {
return fuc(mForm, {
values: mForm?.initValues,
model: props.model,
formValue: mForm ? mForm.values : props.model,
prop: props.prop,
index,
});
}
return fuc;
};
const removeHandler = (index: number) => {
if (props.disabled) return;
props.model[modelName.value].splice(index, 1);
emit('change', props.model[modelName.value]);
};
const selectHandle = (selection: any, row: any) => {
if (typeof props.config.selection === 'string' && props.config.selection === 'single') {
tMagicTable.value?.clearSelection();
tMagicTable.value?.toggleRowSelection(row, true);
}
emit('select', selection, row);
if (typeof props.config.onSelect === 'function') {
props.config.onSelect(mForm, { selection, row, config: props.config });
}
};
const toggleRowSelection = (row: any, selected: boolean) => {
tMagicTable.value?.toggleRowSelection.call(tMagicTable.value, row, selected);
};
const makeConfig = (config: TableColumnConfig, row: any) => {
const newConfig = cloneDeep(config);
if (typeof config.itemsFunction === 'function') {
newConfig.items = config.itemsFunction(row);
}
delete newConfig.display;
return newConfig;
};
const upHandler = (index: number) => {
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
swapArray(index, index - 1);
timer = undefined;
}, 300);
};
const topHandler = (index: number) => {
if (timer) {
clearTimeout(timer);
}
// ,
const moveNum = index;
//
for (let i = 0; i < moveNum; i++) {
swapArray(index, index - 1);
index -= 1;
}
};
const downHandler = (index: number) => {
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
swapArray(index, index + 1);
timer = undefined;
}, 300);
};
const bottomHandler = (index: number) => {
if (timer) {
clearTimeout(timer);
}
// ,
const moveNum = props.model[modelName.value].length - 1 - index;
//
for (let i = 0; i < moveNum; i++) {
swapArray(index, index + 1);
index += 1;
}
};
//
const showDelete = (index: number) => {
const deleteFunc = props.config.delete;
if (deleteFunc && typeof deleteFunc === 'function') {
return deleteFunc(props.model[modelName.value], index, mForm?.values);
}
return true;
};
const copyable = (index: number) => {
const copyableFunc = props.config.copyable;
if (copyableFunc && typeof copyableFunc === 'function') {
return copyableFunc(mForm, {
values: mForm?.initValues || {},
model: props.model,
parent: mForm?.parentValues || {},
formValue: mForm?.values || props.model,
prop: props.prop,
config: props.config,
index,
});
}
return true;
};
const clearHandler = () => {
const len = props.model[modelName.value].length;
props.model[modelName.value].splice(0, len);
mForm?.$emit('field-change', props.prop, props.model[modelName.value]);
};
const excelHandler = async (file: any) => {
if (!file?.raw) {
return false;
}
if (!(globalThis as any).XLSX) {
await asyncLoadJs('https://cdn.bootcdn.net/ajax/libs/xlsx/0.17.0/xlsx.full.min.js');
}
const reader = new FileReader();
reader.onload = () => {
const data = reader.result;
const pdata = (globalThis as any).XLSX.read(data, { type: 'array' });
pdata.SheetNames.forEach((sheetName: string) => {
const arr = (globalThis as any).XLSX.utils.sheet_to_json(pdata.Sheets[sheetName], { header: 1 });
if (arr?.[0]) {
arr.forEach((row: any) => {
newHandler(row);
});
}
setTimeout(() => {
excelBtn.value?.clearFiles();
}, 300);
});
};
reader.readAsArrayBuffer(file.raw);
return false;
};
const handleSizeChange = (val: number) => {
pagesize.value = val;
};
const handleCurrentChange = (val: number) => {
pagecontext.value = val - 1;
};
const copyHandler = (index: number) => {
props.model[modelName.value].push(cloneDeep(props.model[modelName.value][index]));
};
const toggleMode = () => {
const calcLabelWidth = (label: string) => {
if (!label) return '0px';
const zhLength = label.match(/[^\x00-\xff]/g)?.length || 0;
const chLength = label.length - zhLength;
return `${Math.max(chLength * 8 + zhLength * 20, 80)}px`;
};
// groupList
props.config.type = 'groupList';
props.config.enableToggleMode = true;
props.config.tableItems = props.config.items;
props.config.items =
props.config.groupItems ||
props.config.items.map((item: any) => {
const text = item.text || item.label;
const labelWidth = calcLabelWidth(text);
return {
...item,
text,
labelWidth,
span: item.span || 12,
};
});
};
const toggleFullscreen = () => {
if (!mTable.value) return;
if (isFullscreen.value) {
mTable.value.classList.remove('fixed');
isFullscreen.value = false;
} else {
mTable.value.classList.add('fixed');
mTable.value.style.zIndex = `${nextZIndex()}`;
isFullscreen.value = true;
}
};
const getProp = (index: number) => {
const { prop } = toRefs(props);
return `${prop.value}${prop.value ? '.' : ''}${index + 1 + pagecontext.value * pagesize.value - 1}`;
};
const onAddDiffCount = () => emit('addDiffCount');
const changeHandler = (v: any, eventData: ContainerChangeEventData) => {
emit('change', props.model, eventData);
};
defineExpose({
toggleRowSelection,
});
</script>

View File

@ -5,7 +5,7 @@
:is="itemComponent"
:value="option.value"
:key="`${option.value}`"
@click.prevent="clickHandler(option.value)"
@click="clickHandler(option.value)"
>
<TMagicTooltip :disabled="!Boolean(option.tooltip)" placement="top-start" :content="option.tooltip">
<div>

View File

@ -24,7 +24,6 @@ import GroupList from './containers/GroupList.vue';
import Panel from './containers/Panel.vue';
import Row from './containers/Row.vue';
import MStep from './containers/Step.vue';
import Table from './containers/Table.vue';
import Tabs from './containers/Tabs.vue';
import Cascader from './fields/Cascader.vue';
import Checkbox from './fields/Checkbox.vue';
@ -46,6 +45,7 @@ import Text from './fields/Text.vue';
import Textarea from './fields/Textarea.vue';
import Time from './fields/Time.vue';
import Timerange from './fields/Timerange.vue';
import Table from './table/Table.vue';
import { setConfig } from './utils/config';
import Form from './Form.vue';
import FormDialog from './FormDialog.vue';
@ -66,7 +66,7 @@ export { default as MFieldset } from './containers/Fieldset.vue';
export { default as MPanel } from './containers/Panel.vue';
export { default as MRow } from './containers/Row.vue';
export { default as MTabs } from './containers/Tabs.vue';
export { default as MTable } from './containers/Table.vue';
export { default as MTable } from './table/Table.vue';
export { default as MGroupList } from './containers/GroupList.vue';
export { default as MText } from './fields/Text.vue';
export { default as MNumber } from './fields/Number.vue';

View File

@ -0,0 +1,84 @@
<template>
<slot name="operateCol" :scope="{ $index: index, row: row }"></slot>
<TMagicButton
v-show="showDelete(index + 1 + currentPage * pageSize - 1)"
size="small"
type="danger"
link
title="删除"
:icon="Delete"
@click="removeHandler(index + 1 + currentPage * pageSize - 1)"
></TMagicButton>
<TMagicButton
v-if="copyable(index + 1 + currentPage * pageSize - 1)"
link
size="small"
type="primary"
title="复制"
:icon="DocumentCopy"
:disabled="disabled"
@click="copyHandler(index + 1 + currentPage * pageSize - 1)"
></TMagicButton>
</template>
<script setup lang="ts">
import { inject } from 'vue';
import { Delete, DocumentCopy } from '@element-plus/icons-vue';
import { cloneDeep } from 'lodash-es';
import { TMagicButton } from '@tmagic/design';
import type { FormState, TableConfig } from '../schema';
const emit = defineEmits(['change']);
const props = defineProps<{
config: TableConfig;
model: any;
name: string | number;
disabled?: boolean;
currentPage: number;
pageSize: number;
index: number;
row: any;
prop?: string;
}>();
const mForm = inject<FormState | undefined>('mForm');
const removeHandler = (index: number) => {
if (props.disabled) return;
props.model[props.name].splice(index, 1);
emit('change', props.model[props.name]);
};
const copyHandler = (index: number) => {
props.model[props.name].push(cloneDeep(props.model[props.name][index]));
};
//
const showDelete = (index: number) => {
const deleteFunc = props.config.delete;
if (deleteFunc && typeof deleteFunc === 'function') {
return deleteFunc(props.model[props.name], index, mForm?.values);
}
return true;
};
const copyable = (index: number) => {
const copyableFunc = props.config.copyable;
if (copyableFunc && typeof copyableFunc === 'function') {
return copyableFunc(mForm, {
values: mForm?.initValues || {},
model: props.model,
parent: mForm?.parentValues || {},
formValue: mForm?.values || props.model,
prop: props.prop,
config: props.config,
index,
});
}
return true;
};
</script>

View File

@ -0,0 +1,101 @@
<template>
<TMagicTooltip v-if="index + 1 + currentPage * pageSize - 1 !== 0" content="点击上移,双击置顶" placement="top">
<TMagicButton
plain
size="small"
type="primary"
:icon="ArrowUp"
:disabled="disabled"
link
@click="upHandler(index + 1 + currentPage * pageSize - 1)"
@dblclick="topHandler(index + 1 + currentPage * pageSize - 1)"
></TMagicButton>
</TMagicTooltip>
<TMagicTooltip
v-if="index + 1 + currentPage * pageSize - 1 !== model[name].length - 1"
content="点击下移,双击置底"
placement="top"
>
<TMagicButton
plain
size="small"
type="primary"
:icon="ArrowDown"
:disabled="disabled"
link
@click="downHandler(index + 1 + currentPage * pageSize - 1)"
@dblclick="bottomHandler(index + 1 + currentPage * pageSize - 1)"
></TMagicButton>
</TMagicTooltip>
</template>
<script setup lang="ts">
import { ArrowDown, ArrowUp } from '@element-plus/icons-vue';
import { TMagicButton, TMagicTooltip } from '@tmagic/design';
const props = defineProps<{
index: number;
disabled?: boolean;
currentPage: number;
pageSize: number;
name: string | number;
model: any;
}>();
const emit = defineEmits(['swap']);
let timer: ReturnType<typeof setTimeout> | null = null;
const upHandler = (index: number) => {
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
emit('swap', index, index - 1);
timer = null;
}, 300);
};
const topHandler = (index: number) => {
if (timer) {
clearTimeout(timer);
}
// ,
const moveNum = index;
//
for (let i = 0; i < moveNum; i++) {
emit('swap', index, index - 1);
index -= 1;
}
};
const downHandler = (index: number) => {
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
emit('swap', index, index + 1);
timer = null;
}, 300);
};
const bottomHandler = (index: number) => {
if (timer) {
clearTimeout(timer);
}
// ,
const moveNum = props.model[props.name].length - 1 - index;
//
for (let i = 0; i < moveNum; i++) {
emit('swap', index, index + 1);
index += 1;
}
};
</script>

View File

@ -0,0 +1,170 @@
<template>
<div class="m-fields-table-wrap">
<teleport to="body" :disabled="!isFullscreen">
<div ref="mTable" class="m-fields-table" :class="{ 'm-fields-table-item-extra': config.itemExtra }">
<span v-if="config.extra" style="color: rgba(0, 0, 0, 0.45)" v-html="config.extra"></span>
<TMagicTooltip content="拖拽可排序" placement="left-start" :disabled="config.dropSort !== true">
<TMagicTable
v-if="model[modelName]"
ref="tMagicTable"
style="width: 100%"
show-header
:row-key="config.rowKey || 'id'"
:columns="columns"
:data="data"
:border="config.border"
:max-height="config.maxHeight"
:default-expand-all="true"
:key="updateKey"
@select="selectHandle"
@sort-change="sortChangeHandler"
></TMagicTable>
</TMagicTooltip>
<slot></slot>
<div style="display: flex; justify-content: space-between; margin: 10px 0">
<TMagicButton v-if="addable" size="small" type="primary" :disabled="disabled" plain @click="newHandler()"
>新增一行</TMagicButton
>
<div style="display: flex">
<TMagicButton
:icon="Grid"
size="small"
type="primary"
@click="toggleMode"
v-if="enableToggleMode && config.enableToggleMode !== false && !isFullscreen"
>展开配置</TMagicButton
>
<TMagicButton
:icon="FullScreen"
size="small"
type="primary"
@click="toggleFullscreen"
v-if="config.enableFullscreen !== false"
>
{{ isFullscreen ? '退出全屏' : '全屏编辑' }}
</TMagicButton>
<TMagicUpload
v-if="importable"
style="display: inline-block"
ref="excelBtn"
action="/noop"
:disabled="disabled"
:on-change="excelHandler"
:auto-upload="false"
>
<TMagicButton size="small" type="success" :disabled="disabled" plain>导入EXCEL</TMagicButton>
</TMagicUpload>
<TMagicButton v-if="importable" size="small" type="warning" :disabled="disabled" plain @click="clearHandler"
>清空</TMagicButton
>
</div>
</div>
<div class="bottom" style="text-align: right" v-if="config.pagination">
<TMagicPagination
layout="total, sizes, prev, pager, next, jumper"
:hide-on-single-page="model[modelName].length < pageSize"
:current-page="currentPage + 1"
:page-sizes="[pageSize, 60, 120, 300]"
:page-size="pageSize"
:total="model[modelName].length"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
>
</TMagicPagination>
</div>
</div>
</teleport>
</div>
</template>
<script setup lang="ts">
import { computed, ref, useTemplateRef } from 'vue';
import { FullScreen, Grid } from '@element-plus/icons-vue';
import { TMagicButton, TMagicPagination, TMagicTable, TMagicTooltip, TMagicUpload } from '@tmagic/design';
import type { SortProp } from '../schema';
import { sortChange } from '../utils/form';
import type { TableProps } from './type';
import { useAdd } from './useAdd';
import { useFullscreen } from './useFullscreen';
import { useImport } from './useImport';
import { usePagination } from './usePagination';
import { useSelection } from './useSelection';
import { useSortable } from './useSortable';
import { useTableColumns } from './useTableColumns';
defineOptions({
name: 'MFormTable',
});
const props = withDefaults(defineProps<TableProps>(), {
prop: '',
sortKey: '',
enableToggleMode: true,
showIndex: true,
lastValues: () => ({}),
isCompare: false,
});
const emit = defineEmits(['change', 'select', 'addDiffCount']);
const modelName = computed(() => props.name || props.config.name || '');
const tMagicTableRef = useTemplateRef<InstanceType<typeof TMagicTable>>('tMagicTable');
const { pageSize, currentPage, paginationData, handleSizeChange, handleCurrentChange } = usePagination(
props,
modelName,
);
const { addable, newHandler } = useAdd(props, emit);
const { columns } = useTableColumns(props, emit, currentPage, pageSize, modelName);
useSortable(props, emit, tMagicTableRef, modelName);
const { isFullscreen, toggleFullscreen } = useFullscreen();
const { importable, excelHandler, clearHandler } = useImport(props, emit, newHandler);
const { selectHandle, toggleRowSelection } = useSelection(props, emit, tMagicTableRef);
const updateKey = ref(1);
const data = computed(() => (props.config.pagination ? paginationData.value : props.model[modelName.value]));
const toggleMode = () => {
const calcLabelWidth = (label: string) => {
if (!label) return '0px';
const zhLength = label.match(/[^\x00-\xff]/g)?.length || 0;
const chLength = label.length - zhLength;
return `${Math.max(chLength * 8 + zhLength * 20, 80)}px`;
};
// groupList
props.config.type = 'groupList';
props.config.enableToggleMode = true;
props.config.tableItems = props.config.items;
props.config.items =
props.config.groupItems ||
props.config.items.map((item: any) => {
const text = item.text || item.label;
const labelWidth = calcLabelWidth(text);
return {
...item,
text,
labelWidth,
span: item.span || 12,
};
});
};
const sortChangeHandler = (sortOptions: SortProp) => {
const modelName = props.name || props.config.name || '';
sortChange(props.model[modelName], sortOptions);
};
defineExpose({
toggleRowSelection,
});
</script>

View File

@ -0,0 +1,18 @@
import type { TableConfig } from '@tmagic/form-schema';
export interface TableProps {
model: any;
lastValues?: any;
isCompare?: boolean;
config: TableConfig;
name: string;
prop?: string;
labelWidth?: string;
sort?: boolean;
disabled?: boolean;
sortKey?: string;
text?: string;
size?: string;
enableToggleMode?: boolean;
showIndex?: boolean;
}

View File

@ -0,0 +1,111 @@
import { computed, inject } from 'vue';
import { tMagicMessage } from '@tmagic/design';
import type { FormState } from '@tmagic/form-schema';
import { initValue } from '../utils/form';
import type { TableProps } from './type';
export const useAdd = (
props: TableProps,
emit: (event: 'select' | 'change' | 'addDiffCount', ...args: any[]) => void,
) => {
const mForm = inject<FormState | undefined>('mForm');
const addable = computed(() => {
const modelName = props.name || props.config.name || '';
if (!props.model[modelName].length) {
return true;
}
if (typeof props.config.addable === 'function') {
return props.config.addable(mForm, {
model: props.model[modelName],
formValue: mForm?.values,
prop: props.prop,
});
}
return typeof props.config.addable === 'undefined' ? true : props.config.addable;
});
const newHandler = async (row?: any) => {
const modelName = props.name || props.config.name || '';
if (props.config.max && props.model[modelName].length >= props.config.max) {
tMagicMessage.error(`最多新增配置不能超过${props.config.max}`);
return;
}
if (typeof props.config.beforeAddRow === 'function') {
const beforeCheckRes = props.config.beforeAddRow(mForm, {
model: props.model[modelName],
formValue: mForm?.values,
prop: props.prop,
});
if (!beforeCheckRes) return;
}
const columns = props.config.items;
const enumValues = props.config.enum || [];
let enumV = [];
const { length } = props.model[modelName];
const key = props.config.key || 'id';
let inputs: any = {};
if (enumValues.length) {
if (length >= enumValues.length) {
return;
}
enumV = enumValues.filter((item) => {
let i = 0;
for (; i < length; i++) {
if (item[key] === props.model[modelName][i][key]) {
break;
}
}
return i === length;
});
if (enumV.length > 0) {
// eslint-disable-next-line prefer-destructuring
inputs = enumV[0];
}
} else if (Array.isArray(row)) {
columns.forEach((column, index) => {
column.name && (inputs[column.name] = row[index]);
});
} else {
if (typeof props.config.defaultAdd === 'function') {
inputs = await props.config.defaultAdd(mForm, {
model: props.model[modelName],
formValue: mForm?.values,
});
} else if (props.config.defaultAdd) {
inputs = props.config.defaultAdd;
}
inputs = await initValue(mForm, {
config: columns,
initValues: inputs,
});
}
if (props.sortKey && length) {
inputs[props.sortKey] = props.model[modelName][length - 1][props.sortKey] - 1;
}
emit('change', [...props.model[modelName], inputs], {
changeRecords: [
{
propPath: `${props.prop}.${props.model[modelName].length}`,
value: inputs,
},
],
});
};
return {
addable,
newHandler,
};
};

View File

@ -0,0 +1,28 @@
import { ref, useTemplateRef } from 'vue';
import { useZIndex } from '@tmagic/design';
export const useFullscreen = () => {
const isFullscreen = ref(false);
const mTableEl = useTemplateRef<HTMLDivElement>('mTable');
const { nextZIndex } = useZIndex();
const toggleFullscreen = () => {
if (!mTableEl.value) return;
if (isFullscreen.value) {
mTableEl.value.classList.remove('fixed');
isFullscreen.value = false;
} else {
mTableEl.value.classList.add('fixed');
mTableEl.value.style.zIndex = `${nextZIndex()}`;
isFullscreen.value = true;
}
};
return {
isFullscreen,
toggleFullscreen,
};
};

View File

@ -0,0 +1,68 @@
import { computed, inject, useTemplateRef } from 'vue';
import type { TMagicUpload } from '@tmagic/design';
import type { FormState } from '@tmagic/form-schema';
import { asyncLoadJs } from '@tmagic/utils';
import type { TableProps } from './type';
export const useImport = (
props: TableProps,
emit: (event: 'select' | 'change' | 'addDiffCount', ...args: any[]) => void,
newHandler: (row: any) => void,
) => {
const mForm = inject<FormState | undefined>('mForm');
const modelName = computed(() => props.name || props.config.name || '');
const importable = computed(() => {
if (typeof props.config.importable === 'function') {
return props.config.importable(mForm, {
formValue: mForm?.values,
model: props.model[modelName.value],
});
}
return typeof props.config.importable === 'undefined' ? false : props.config.importable;
});
const excelBtn = useTemplateRef<InstanceType<typeof TMagicUpload>>('excelBtn');
const excelHandler = async (file: any) => {
if (!file?.raw) {
return false;
}
if (!(globalThis as any).XLSX) {
await asyncLoadJs('https://cdn.bootcdn.net/ajax/libs/xlsx/0.17.0/xlsx.full.min.js');
}
const reader = new FileReader();
reader.onload = () => {
const data = reader.result;
const pdata = (globalThis as any).XLSX.read(data, { type: 'array' });
pdata.SheetNames.forEach((sheetName: string) => {
const arr = (globalThis as any).XLSX.utils.sheet_to_json(pdata.Sheets[sheetName], { header: 1 });
if (arr?.[0]) {
arr.forEach((row: any) => {
newHandler(row);
});
}
setTimeout(() => {
excelBtn.value?.clearFiles();
}, 300);
});
};
reader.readAsArrayBuffer(file.raw);
return false;
};
const clearHandler = () => {
emit('change', []);
mForm?.$emit('field-change', props.prop, props.model[modelName.value]);
};
return {
importable,
excelHandler,
clearHandler,
};
};

View File

@ -0,0 +1,30 @@
import { computed, type Ref, ref } from 'vue';
import { getDataByPage } from '../utils/form';
import type { TableProps } from './type';
export const usePagination = (props: TableProps, modelName: Ref<string | number>) => {
const pageSize = ref(10);
/**
*
*/
const currentPage = ref(0);
const paginationData = computed(() => getDataByPage(props.model[modelName.value], currentPage.value, pageSize.value));
const handleSizeChange = (val: number) => {
pageSize.value = val;
};
const handleCurrentChange = (val: number) => {
currentPage.value = val - 1;
};
return {
pageSize,
currentPage,
paginationData,
handleSizeChange,
handleCurrentChange,
};
};

View File

@ -0,0 +1,34 @@
import { inject, type ShallowRef } from 'vue';
import type { TMagicTable } from '@tmagic/design';
import type { FormState } from '@tmagic/form-schema';
import type { TableProps } from './type';
export const useSelection = (
props: TableProps,
emit: (event: 'select' | 'change' | 'addDiffCount', ...args: any[]) => void,
tMagicTableRef: ShallowRef<InstanceType<typeof TMagicTable> | null>,
) => {
const mForm = inject<FormState | undefined>('mForm');
const selectHandle = (selection: any, row: any) => {
if (typeof props.config.selection === 'string' && props.config.selection === 'single') {
tMagicTableRef.value?.clearSelection();
tMagicTableRef.value?.toggleRowSelection(row, true);
}
emit('select', selection, row);
if (typeof props.config.onSelect === 'function') {
props.config.onSelect(mForm, { selection, row, config: props.config });
}
};
const toggleRowSelection = (row: any, selected: boolean) => {
tMagicTableRef.value?.toggleRowSelection.call(tMagicTableRef.value?.getTableRef(), row, selected);
};
return {
selectHandle,
toggleRowSelection,
};
};

View File

@ -0,0 +1,48 @@
import { inject, type Ref, type ShallowRef, watchEffect } from 'vue';
import Sortable, { type SortableEvent } from 'sortablejs';
import { type TMagicTable } from '@tmagic/design';
import type { FormState } from '@tmagic/form-schema';
import { sortArray } from '../utils/form';
import type { TableProps } from './type';
export const useSortable = (
props: TableProps,
emit: (event: 'select' | 'change' | 'addDiffCount', ...args: any[]) => void,
tMagicTableRef: ShallowRef<InstanceType<typeof TMagicTable> | null>,
modelName: Ref<string | number>,
) => {
const mForm = inject<FormState | undefined>('mForm');
let sortable: Sortable | undefined;
const rowDrop = () => {
sortable?.destroy();
const tableEl = tMagicTableRef.value?.getEl();
const tBodyEl = tableEl?.querySelector('.el-table__body > tbody');
if (!tBodyEl) {
return;
}
sortable = Sortable.create(tBodyEl, {
draggable: '.tmagic-design-table-row',
filter: 'input', // 表单组件选字操作和触发拖拽会冲突,优先保证选字操作
preventOnFilter: false, // 允许选字
direction: 'vertical',
onEnd: ({ newIndex, oldIndex }: SortableEvent) => {
if (typeof newIndex === 'undefined') return;
if (typeof oldIndex === 'undefined') return;
const newData = sortArray(props.model[modelName.value], newIndex, oldIndex, props.sortKey);
emit('change', newData);
mForm?.$emit('field-change', newData);
},
});
};
watchEffect(() => {
if (props.config.dropSort) {
rowDrop();
}
});
};

View File

@ -0,0 +1,193 @@
import { computed, h, inject, type Ref } from 'vue';
import { cloneDeep } from 'lodash-es';
import type { TableColumnOptions } from '@tmagic/design';
import type { FormState, TableColumnConfig } from '@tmagic/form-schema';
import Container from '../containers/Container.vue';
import type { ContainerChangeEventData } from '../schema';
import { display as displayFunc, getDataByPage, sortArray } from '../utils/form';
import ActionsColumn from './ActionsColumn.vue';
import SortColumn from './SortColumn.vue';
import type { TableProps } from './type';
export const useTableColumns = (
props: TableProps,
emit: (event: 'select' | 'change' | 'addDiffCount', ...args: any[]) => void,
currentPage: Ref<number>,
pageSize: Ref<number>,
modelName: Ref<string | number>,
) => {
const mForm = inject<FormState | undefined>('mForm');
const display = (fuc: any) => displayFunc(mForm, fuc, props);
const lastData = computed(() =>
props.config.pagination
? getDataByPage(props.lastValues[modelName.value], currentPage.value, pageSize.value)
: props.lastValues[modelName.value] || [],
);
const itemExtra = (fuc: any, index: number) => {
if (typeof fuc === 'function') {
return fuc(mForm, {
values: mForm?.initValues,
model: props.model,
formValue: mForm ? mForm.values : props.model,
prop: props.prop,
index,
});
}
return fuc;
};
const selection = computed(() => {
if (typeof props.config.selection === 'function') {
return props.config.selection(mForm, { model: props.model[modelName.value] });
}
return props.config.selection;
});
const getProp = (index: number) => {
return `${props.prop}${props.prop ? '.' : ''}${index + 1 + currentPage.value * pageSize.value - 1}`;
};
const makeConfig = (config: TableColumnConfig, row: any) => {
const newConfig = cloneDeep(config);
if (typeof config.itemsFunction === 'function') {
newConfig.items = config.itemsFunction(row);
}
delete newConfig.display;
return newConfig;
};
const changeHandler = (v: any, eventData: ContainerChangeEventData) => {
emit('change', props.model, eventData);
};
const onAddDiffCount = () => emit('addDiffCount');
const columns = computed<TableColumnOptions[]>(() => {
const columns: TableColumnOptions[] = [];
if (props.config.itemExtra && !props.config.dropSort) {
columns.push({
props: {
fixed: 'left',
width: 30,
type: 'expand',
},
cell: ({ $index }: any) =>
h('span', {
innerHTML: itemExtra(props.config.itemExtra, $index),
class: 'm-form-tip',
}),
});
}
columns.push({
props: {
label: '操作',
fixed: props.config.fixed === false ? undefined : 'left',
width: props.config.operateColWidth || 100,
align: 'center',
},
cell: ({ row, $index }: any) =>
h(ActionsColumn, {
row,
index: $index,
model: props.model,
config: props.config,
prop: props.prop,
disabled: props.disabled,
name: modelName.value,
currentPage: currentPage.value,
pageSize: pageSize.value,
onChange: (v: any) => {
emit('change', v);
},
}),
});
if (props.sort && props.model[modelName.value] && props.model[modelName.value].length > 1) {
columns.push({
props: {
label: '排序',
width: 80,
},
cell: ({ $index }: any) =>
h(SortColumn, {
index: $index,
model: props.model,
disabled: props.disabled,
name: modelName.value,
currentPage: currentPage.value,
pageSize: pageSize.value,
onSwap: (index1: number, index2: number) => {
const newData = sortArray(props.model[modelName.value], index1, index2, props.sortKey);
emit('change', newData);
mForm?.$emit('field-change', newData);
},
}),
});
}
if (selection.value) {
columns.push({
props: {
align: 'center',
headerAlign: 'center',
type: 'selection',
width: 45,
},
});
}
if (props.showIndex && props.config.showIndex) {
columns.push({
props: {
label: '序号',
width: 60,
},
cell: ({ $index }: any) => h('span', $index + 1 + currentPage.value * pageSize.value),
});
}
for (const column of props.config.items) {
if (column.type !== 'hidden' && display(column.display)) {
columns.push({
props: {
prop: column.name,
label: column.label,
width: column.width,
sortable: column.sortable,
sortOrders: ['ascending', 'descending'],
class: props.config.dropSort === true ? 'el-table__column--dropable' : '',
},
cell: ({ row, $index }: any) =>
h(Container, {
labelWidth: '0',
disabled: props.disabled,
prop: getProp($index),
rules: column.rules,
config: makeConfig(column, row),
model: row,
lastValues: lastData.value[$index],
isCompare: props.isCompare,
size: props.size,
onChange: changeHandler,
onAddDiffCount,
}),
});
}
}
return columns;
});
return {
columns,
};
};

View File

@ -19,17 +19,21 @@
height: 100%;
}
.el-table {
.tmagic-design-table {
.cell > div.m-form-container {
display: block;
&.has-tip {
display: flex;
}
}
}
.el-tabs {
.tmagic-design-tabs {
margin-bottom: 10px;
}
.el-form-item.tmagic-form-hidden {
.tmagic-design-form-item.tmagic-form-hidden {
> .el-form-item__label {
display: none;
}
@ -39,5 +43,9 @@
> .t-form__label {
display: none;
}
> .t-form__controls {
margin-left: 0 !important;
}
}
}

View File

@ -31,6 +31,7 @@ import {
FormValue,
HtmlField,
Rule,
SortProp,
TabPaneConfig,
TypeFunction,
} from '../schema';
@ -136,6 +137,18 @@ const initValueItem = function (
setValue(mForm, value, initValue, item);
if (type === 'table') {
if (item.defautSort) {
sortChange(value[name], item.defautSort);
} else if (item.defaultSort) {
sortChange(value[name], item.defaultSort);
}
if (item.sort && item.sortKey) {
value[name].sort((a: any, b: any) => b[item.sortKey] - a[item.sortKey]);
}
}
return value;
};
@ -297,3 +310,36 @@ export const datetimeFormatter = (
}
return defaultValue;
};
export const getDataByPage = (data: any[] = [], pagecontext: number, pagesize: number) =>
data.filter(
(item: any, index: number) => index >= pagecontext * pagesize && index + 1 <= (pagecontext + 1) * pagesize,
);
export const sortArray = (data: any[], newIndex: number, oldIndex: number, sortKey?: string) => {
if (newIndex === oldIndex) {
return data;
}
if (newIndex < 0 || newIndex >= data.length || oldIndex < 0 || oldIndex >= data.length) {
return data;
}
const newData = data.toSpliced(newIndex, 0, ...data.splice(oldIndex, 1));
if (sortKey) {
for (let i = newData.length - 1, v = 0; i >= 0; i--, v++) {
newData[v][sortKey] = i;
}
}
return newData;
};
export const sortChange = (data: any[], { prop, order }: SortProp) => {
if (order === 'ascending') {
data = data.sort((a: any, b: any) => a[prop] - b[prop]);
} else if (order === 'descending') {
data = data.sort((a: any, b: any) => b[prop] - a[prop]);
}
};

View File

@ -18,7 +18,7 @@
import { describe, expect, test } from 'vitest';
import type { FormState } from '@form/index';
import { datetimeFormatter, display, filterFunction, getRules, initValue } from '@form/utils/form';
import { datetimeFormatter, display, filterFunction, getRules, initValue, sortArray } from '@form/utils/form';
// form state mock 数据
const mForm: FormState = {
@ -32,6 +32,19 @@ const mForm: FormState = {
setField: (prop: string, field: any) => field,
getField: (prop: string) => prop,
deleteField: (prop: string) => prop,
$messageBox: {
alert: () => Promise.resolve(),
confirm: () => Promise.resolve(),
prompt: () => Promise.resolve(),
close: () => undefined,
},
$message: {
success: () => undefined,
warning: () => undefined,
info: () => undefined,
error: () => undefined,
closeAll: () => undefined,
},
};
describe('filterFunction', () => {
@ -339,3 +352,71 @@ describe('datetimeFormatter', () => {
expect(datetimeFormatter(date.toISOString(), defaultValue, 'timestamp')).toBe(date.getTime());
});
});
describe('sortArray', () => {
test('索引相同时不执行任何操作', () => {
const data = [1, 2, 3, 4, 5];
expect(sortArray(data, 2, 2)).toEqual(data);
});
test('正常交换两个元素的位置', () => {
const data = [1, 2, 3, 4, 5];
expect(sortArray(data, 0, 3)).toEqual([4, 1, 2, 3, 5]);
});
test('从后往前移动元素', () => {
const data = [1, 2, 3, 4, 5];
expect(sortArray(data, 3, 1)).toEqual([1, 3, 4, 2, 5]);
});
test('使用sortKey参数重新排序', () => {
const data = [
{ id: 1, order: 0 },
{ id: 2, order: 1 },
{ id: 3, order: 2 },
{ id: 4, order: 3 },
];
expect(sortArray(data, 0, 2, 'order')).toEqual([
{ id: 3, order: 3 },
{ id: 1, order: 2 },
{ id: 2, order: 1 },
{ id: 4, order: 0 },
]);
});
test('移动第一个元素到最后', () => {
const data = [1, 2, 3, 4, 5];
expect(sortArray(data, 4, 0)).toEqual([2, 3, 4, 5, 1]);
});
test('移动最后一个元素到第一个', () => {
const data = [1, 2, 3, 4, 5];
expect(sortArray(data, 0, 4)).toEqual([5, 1, 2, 3, 4]);
});
test('空数组不执行任何操作', () => {
const data: any[] = [];
expect(sortArray(data, 0, 1)).toEqual([]);
});
test('只有一个元素的数组不执行任何操作', () => {
const data = [1];
expect(sortArray(data, 0, 0)).toEqual([1]);
});
test('索引超出范围时正常处理', () => {
const data = [1, 2, 3];
// 索引超出范围应该由调用方处理,这里测试函数的行为
expect(sortArray(data, 5, 1)).toEqual(data);
expect(sortArray(data, 1, 5)).toEqual(data);
});
});

View File

@ -1,49 +1,46 @@
<template>
<TMagicTableColumn :label="config.label" :width="config.width" :fixed="config.fixed">
<template v-slot="scope">
<TMagicTooltip
v-for="(action, actionIndex) in config.actions"
:placement="action.tooltipPlacement || 'top'"
:key="actionIndex"
:disabled="!Boolean(action.tooltip)"
:content="action.tooltip"
>
<TMagicButton
v-show="display(action.display, scope.row) && !editState[scope.$index]"
class="action-btn"
link
size="small"
:type="action.buttonType || 'primary'"
:icon="action.icon"
:disabled="disabled(action.disabled, scope.row)"
@click="actionHandler(action, scope.row, scope.$index)"
><span v-html="formatter(action.text, scope.row)"></span
></TMagicButton>
</TMagicTooltip>
<TMagicButton
class="action-btn"
v-show="editState[scope.$index]"
link
type="primary"
size="small"
@click="save(scope.$index, config)"
>保存</TMagicButton
>
<TMagicButton
class="action-btn"
v-show="editState[scope.$index]"
link
type="primary"
size="small"
@click="editState[scope.$index] = undefined"
>取消</TMagicButton
>
</template>
</TMagicTableColumn>
<TMagicTooltip
v-for="(action, actionIndex) in config.actions"
:placement="action.tooltipPlacement || 'top'"
:key="actionIndex"
: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>
<TMagicButton
class="action-btn"
v-show="editState[index]"
link
type="primary"
size="small"
@click="save(index, config)"
>保存</TMagicButton
>
<TMagicButton
class="action-btn"
v-show="editState[index]"
link
type="primary"
size="small"
@click="editState[index] = undefined"
>取消</TMagicButton
>
</template>
<script lang="ts" setup>
import { TMagicButton, tMagicMessage, TMagicTableColumn, TMagicTooltip } from '@tmagic/design';
import { TMagicButton, tMagicMessage, TMagicTooltip } from '@tmagic/design';
import { ColumnActionConfig, ColumnConfig } from './schema';
@ -53,10 +50,12 @@ defineOptions({
const props = withDefaults(
defineProps<{
columns: any[];
columns: ColumnConfig[];
config: ColumnConfig;
rowkeyName?: string;
editState?: any;
row: any;
index: number;
}>(),
{
columns: () => [],
@ -114,7 +113,9 @@ const save = async (index: number, config: ColumnConfig) => {
props.columns
.filter((item) => item.type)
.forEach((column) => {
data[column.prop] = row[column.prop];
if (column.prop) {
data[column.prop] = row[column.prop];
}
});
const res: any = await action({

View File

@ -1,25 +1,12 @@
<template>
<TMagicTableColumn
show-overflow-tooltip
:label="config.label"
:width="config.width"
:fixed="config.fixed"
:sortable="config.sortable"
:prop="config.prop"
>
<template v-slot="scope">
<component
:is="config.component"
v-bind="componentProps(scope.row, scope.$index)"
v-on="componentListeners(scope.row, scope.$index)"
></component>
</template>
</TMagicTableColumn>
<component
:is="config.component"
v-bind="componentProps(row, index)"
v-on="componentListeners(row, index)"
></component>
</template>
<script lang="ts" setup>
import { TMagicTableColumn } from '@tmagic/design';
import { ColumnConfig } from './schema';
defineOptions({
@ -29,6 +16,8 @@ defineOptions({
const props = withDefaults(
defineProps<{
config: ColumnConfig;
row: any;
index: number;
}>(),
{
config: () => ({}),

View File

@ -1,26 +1,20 @@
<template>
<!-- @ts-nocheck -->
<TMagicTableColumn type="expand" :width="config.width" :fixed="config.fixed">
<template #default="scope">
<MTable
v-if="config.table"
:show-header="false"
:columns="config.table"
:data="(config.prop && scope.row[config.prop]) || []"
></MTable>
<MForm
v-if="config.form"
:config="config.form"
:init-values="config.values || (config.prop && scope.row[config.prop]) || {}"
></MForm>
<div v-if="config.expandContent" v-html="config.expandContent(scope.row, config.prop)"></div>
<component v-if="config.component" :is="config.component" v-bind="componentProps(scope.row)"></component>
</template>
</TMagicTableColumn>
<MTable
v-if="config.table"
:show-header="false"
:columns="config.table"
:data="(config.prop && row[config.prop]) || []"
></MTable>
<MForm
v-if="config.form"
:config="config.form"
:init-values="config.values || (config.prop && row[config.prop]) || {}"
></MForm>
<div v-if="config.expandContent" v-html="config.expandContent(row, config.prop)"></div>
<component v-if="config.component" :is="config.component" v-bind="componentProps(row)"></component>
</template>
<script lang="ts" setup>
import { TMagicTableColumn } from '@tmagic/design';
import { MForm } from '@tmagic/form';
import { ColumnConfig } from './schema';
@ -33,6 +27,7 @@ defineOptions({
const props = withDefaults(
defineProps<{
config: ColumnConfig;
row: any;
}>(),
{
config: () => ({}),

View File

@ -1,32 +1,25 @@
<template>
<!-- @ts-nocheck -->
<TMagicTableColumn :label="config.label" :width="config.width" :fixed="config.fixed">
<template v-slot="scope">
<TMagicPopover
v-if="config.popover"
:placement="config.popover.placement"
:width="config.popover.width"
:trigger="config.popover.trigger"
:destroy-on-close="config.popover.destroyOnClose ?? true"
>
<MTable
v-if="config.popover.tableEmbed"
:show-header="config.showHeader"
:columns="config.table"
:data="(config.prop && scope.row[config.prop]) || []"
></MTable>
<template #reference>
<TMagicButton link type="primary">
{{ config.text || formatter(config, scope.row, { index: scope.$index }) }}</TMagicButton
>
</template>
</TMagicPopover>
<TMagicPopover
v-if="config.popover"
:placement="config.popover.placement"
:width="config.popover.width"
:trigger="config.popover.trigger"
:destroy-on-close="config.popover.destroyOnClose ?? true"
>
<MTable
v-if="config.popover.tableEmbed"
:show-header="config.showHeader"
:columns="config.table"
:data="(config.prop && row[config.prop]) || []"
></MTable>
<template #reference>
<TMagicButton link type="primary">{{ config.text || formatter(config, row, { index: index }) }}</TMagicButton>
</template>
</TMagicTableColumn>
</TMagicPopover>
</template>
<script lang="ts" setup>
import { TMagicButton, TMagicPopover, TMagicTableColumn } from '@tmagic/design';
import { TMagicButton, TMagicPopover } from '@tmagic/design';
import { ColumnConfig } from './schema';
import MTable from './Table.vue';
@ -39,6 +32,8 @@ defineOptions({
withDefaults(
defineProps<{
config: ColumnConfig;
row: any;
index: number;
}>(),
{
config: () => ({}),

View File

@ -1,10 +1,11 @@
<template>
<TMagicTable
tooltip-effect="dark"
:tooltip-options="{ popperOptions: { strategy: 'absolute' } }"
v-loading="loading"
class="m-table"
ref="tMagicTable"
v-loading="loading"
:show-overflow-tooltip="true"
tooltip-effect="dark"
:tooltip-options="{ popperOptions: { strategy: 'absolute' } }"
:data="tableData"
:show-header="showHeader"
:max-height="bodyHeight"
@ -14,59 +15,21 @@
:tree-props="{ children: 'children' }"
:empty-text="emptyText || '暂无数据'"
:span-method="objectSpanMethod"
:columns="tableColumns"
@sort-change="sortChange"
@select="selectHandler"
@select-all="selectAllHandler"
@selection-change="selectionChangeHandler"
@cell-click="cellClickHandler"
@expand-change="expandChange"
>
<template v-for="(item, columnIndex) in columns">
<template v-if="item.type === 'expand'">
<ExpandColumn :config="item" :key="columnIndex"></ExpandColumn>
</template>
<template v-else-if="item.type === 'component'">
<ComponentColumn :config="item" :key="columnIndex"></ComponentColumn>
</template>
<template v-else-if="item.selection">
<component
width="40"
type="selection"
:is="tableColumnComponent?.component || 'el-table-column'"
:key="columnIndex"
:selectable="item.selectable"
></component>
</template>
<template v-else-if="item.actions">
<ActionsColumn
:columns="columns"
:config="item"
:rowkey-name="rowkeyName"
:edit-state="editState"
:key="columnIndex"
@after-action="$emit('after-action')"
></ActionsColumn>
</template>
<template v-else-if="item.type === 'popover'">
<PopoverColumn :key="columnIndex" :config="item"></PopoverColumn>
</template>
<template v-else>
<TextColumn :key="columnIndex" :config="item" :edit-state="editState"></TextColumn>
</template>
</template>
</TMagicTable>
></TMagicTable>
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue';
import { computed, h, ref, useTemplateRef } from 'vue';
import { cloneDeep } from 'lodash-es';
import { getDesignConfig, TMagicTable } from '@tmagic/design';
import { TMagicTable } from '@tmagic/design';
import ActionsColumn from './ActionsColumn.vue';
import ComponentColumn from './ComponentColumn.vue';
@ -117,11 +80,72 @@ const emit = defineEmits([
'cell-click',
]);
const tMagicTable = ref<InstanceType<typeof TMagicTable>>();
const cellRender = (config: ColumnConfig, { row = {}, $index }: any) => {
if (config.type === 'expand') {
return h(ExpandColumn, {
config,
row,
});
}
if (config.type === 'component') {
return h(ComponentColumn, {
config,
row,
index: $index,
});
}
if (config.actions) {
return h(ActionsColumn, {
config,
row,
index: $index,
rowkeyName: props.rowkeyName,
editState: editState.value,
columns: props.columns,
});
}
if (config.type === 'popover') {
return h(PopoverColumn, {
config,
row,
index: $index,
});
}
return h(TextColumn, {
config,
row,
index: $index,
editState: editState.value,
});
};
const tableColumns = computed(() =>
props.columns.map((item) => {
let type: 'default' | 'selection' | 'index' | 'expand' = 'default';
if (item.type === 'expand') {
type = 'expand';
} else if (item.selection) {
type = 'selection';
}
return {
props: {
label: item.label,
fixed: item.fixed,
width: item.width ?? (item.selection ? 40 : undefined),
prop: item.prop,
type,
selectable: item.selectable,
},
cell: type === 'selection' ? undefined : ({ row, $index }: any) => cellRender(item, { row, $index }),
};
}),
);
const tMagicTableRef = useTemplateRef('tMagicTable');
const editState = ref([]);
const tableColumnComponent = getDesignConfig('components')?.tableColumn;
const selectionColumn = computed(() => {
const column = props.columns.filter((item) => item.selection);
return column.length ? column[0] : null;
@ -171,15 +195,15 @@ const expandChange = (...args: any[]) => {
};
const toggleRowSelection = (row: any, selected: boolean) => {
tMagicTable.value?.toggleRowSelection(row, selected);
tMagicTableRef.value?.toggleRowSelection(row, selected);
};
const toggleRowExpansion = (row: any, expanded: boolean) => {
tMagicTable.value?.toggleRowExpansion(row, expanded);
tMagicTableRef.value?.toggleRowExpansion(row, expanded);
};
const clearSelection = () => {
tMagicTable.value?.clearSelection();
tMagicTableRef.value?.clearSelection();
};
const objectSpanMethod = (data: any) => {

View File

@ -1,69 +1,52 @@
<template>
<TMagicTableColumn
show-overflow-tooltip
:label="config.label"
:width="config.width"
:fixed="config.fixed"
:sortable="config.sortable"
:prop="config.prop"
<div v-if="config.type === 'index'">
{{ config.pageIndex && config.pageSize ? config.pageIndex * config.pageSize + index + 1 : index + 1 }}
</div>
<TMagicForm v-else-if="config.type && editState[index]" label-width="0" :model="editState[index]">
<m-form-container
:prop="config.prop"
:rules="config.rules"
:config="config"
:name="config.prop"
:model="editState[index]"
></m-form-container>
</TMagicForm>
<TMagicButton
v-else-if="config.action === 'actionLink' && config.prop"
link
type="primary"
@click="config.handler?.(row)"
>
<template v-slot="scope">
<div v-if="config.type === 'index'">
{{
config.pageIndex && config.pageSize ? config.pageIndex * config.pageSize + scope.$index + 1 : scope.$index + 1
}}
</div>
<TMagicForm v-else-if="config.type && editState[scope.$index]" label-width="0" :model="editState[scope.$index]">
<m-form-container
:prop="config.prop"
:rules="config.rules"
:config="config"
:name="config.prop"
:model="editState[scope.$index]"
></m-form-container>
</TMagicForm>
<span v-html="formatter(config, row, { index: index })"></span>
</TMagicButton>
<TMagicButton
v-else-if="config.action === 'actionLink' && config.prop"
link
type="primary"
@click="config.handler?.(scope.row)"
>
<span v-html="formatter(config, scope.row, { index: scope.$index })"></span>
</TMagicButton>
<a v-else-if="config.action === 'img' && config.prop" target="_blank" :href="row[config.prop]"
><img :src="row[config.prop]" height="50"
/></a>
<a v-else-if="config.action === 'img' && config.prop" target="_blank" :href="scope.row[config.prop]"
><img :src="scope.row[config.prop]" height="50"
/></a>
<a v-else-if="config.action === 'link' && config.prop" target="_blank" :href="row[config.prop]" class="keep-all">{{
row[config.prop]
}}</a>
<a
v-else-if="config.action === 'link' && config.prop"
target="_blank"
:href="scope.row[config.prop]"
class="keep-all"
>{{ scope.row[config.prop] }}</a
>
<el-tooltip v-else-if="config.action === 'tip'" placement="left">
<template #content>
<div>{{ formatter(config, scope.row, { index: scope.$index }) }}</div>
</template>
<TMagicButton link type="primary">{{ config.buttonText || '扩展配置' }}</TMagicButton>
</el-tooltip>
<TMagicTag
v-else-if="config.action === 'tag' && config.prop"
:type="typeof config.type === 'function' ? config.type(scope.row[config.prop], scope.row) : config.type"
close-transition
>{{ formatter(config, scope.row, { index: scope.$index }) }}</TMagicTag
>
<div v-else v-html="formatter(config, scope.row, { index: scope.$index })"></div>
<el-tooltip v-else-if="config.action === 'tip'" placement="left">
<template #content>
<div>{{ formatter(config, row, { index: index }) }}</div>
</template>
</TMagicTableColumn>
<TMagicButton link type="primary">{{ config.buttonText || '扩展配置' }}</TMagicButton>
</el-tooltip>
<TMagicTag
v-else-if="config.action === 'tag' && config.prop"
:type="typeof config.type === 'function' ? config.type(row[config.prop], row) : config.type"
close-transition
>{{ formatter(config, row, { index: index }) }}</TMagicTag
>
<div v-else v-html="formatter(config, row, { index: index })"></div>
</template>
<script lang="ts" setup>
import { TMagicButton, TMagicForm, TMagicTableColumn, TMagicTag } from '@tmagic/design';
import { TMagicButton, TMagicForm, TMagicTag } from '@tmagic/design';
import { ColumnConfig } from './schema';
import { formatter } from './utils';
@ -76,6 +59,8 @@ withDefaults(
defineProps<{
config: ColumnConfig;
editState?: any;
row: any;
index: number;
}>(),
{
config: () => ({}),

View File

@ -39,7 +39,7 @@
],
"peerDependencies": {
"@tmagic/design": "workspace:*",
"tdesign-vue-next": "^1.9.8",
"tdesign-vue-next": "^1.17.1",
"vue": "catalog:",
"typescript": "catalog:"
},

View File

@ -0,0 +1,47 @@
<template>
<TCheckbox v-model="checked" :disabled="disabled" :value="value" @change="changeHandler">
<template #default v-if="$slots.default"> <slot></slot> </template>
</TCheckbox>
</template>
<script lang="ts" setup>
import { ref, watch } from 'vue';
import { Checkbox as TCheckbox } from 'tdesign-vue-next';
import type { CheckboxProps } from '@tmagic/design';
defineOptions({
name: 'TTDesignAdapterCheckbox',
});
const props = defineProps<CheckboxProps>();
const emit = defineEmits(['change', 'update:modelValue']);
const checked = ref(false);
watch(
() => props.modelValue,
(v) => {
if (typeof props.trueValue !== 'undefined') {
checked.value = v === props.trueValue;
} else if (typeof props.falseValue !== 'undefined') {
checked.value = v !== props.falseValue;
} else {
checked.value = Boolean(v);
}
},
{
immediate: true,
},
);
const changeHandler = (v: boolean) => {
updateModelValue(v);
emit('change', v ? (props.trueValue ?? true) : (props.falseValue ?? false));
};
const updateModelValue = (v: boolean) => {
emit('update:modelValue', v ? (props.trueValue ?? true) : (props.falseValue ?? false));
};
</script>

View File

@ -8,7 +8,7 @@
:size="size === 'default' ? 'medium' : size"
:separator="rangeSeparator"
:format="format"
:valueType="valueFormat === 's' ? 'time-stamp' : valueFormat"
:valueType="valueType"
@change="changeHandler"
@update:modelValue="updateModelValue"
/>
@ -21,7 +21,7 @@
:size="size === 'default' ? 'medium' : size"
:format="format"
:enableTimePicker="type.includes('time')"
:valueType="valueFormat === 's' ? 'time-stamp' : valueFormat"
:valueType="valueType"
@change="changeHandler"
@update:modelValue="updateModelValue"
/>
@ -54,6 +54,8 @@ const mode = computed(() => {
return map[props.type] || props.type;
});
const valueType = computed(() => (props.valueFormat === 's' ? 'time-stamp' : props.valueFormat.replace(/\//g, '-')));
const emit = defineEmits(['change', 'update:modelValue']);
const changeHandler = (v: any) => {

View File

@ -0,0 +1,43 @@
<template>
<TDialog
:visible="modelValue"
:attach="appendToBody ? 'body' : undefined"
:header="title"
:width="width"
:mode="fullscreen ? 'full-screen' : 'modal'"
:close-on-overlay-click="closeOnClickModal"
:close-on-esc-keydown="closeOnPressEscape"
:destroy-on-close="destroyOnClose"
@before-open="beforeClose"
@close="closeHandler"
@update:visible="updateModelValue"
>
<slot></slot>
<template #footer>
<slot name="footer"></slot>
</template>
</TDialog>
</template>
<script setup lang="ts">
import { Dialog as TDialog } from 'tdesign-vue-next';
import type { DialogProps } from '@tmagic/design';
defineOptions({
name: 'TTDesignAdapterDialog',
});
defineProps<DialogProps>();
const emit = defineEmits(['close', 'update:modelValue']);
const closeHandler = (...args: any[]) => {
emit('close', ...args);
};
const updateModelValue = (v: any) => {
emit('update:modelValue', v);
};
</script>

View File

@ -1,7 +1,11 @@
<template>
<i>
<i class="t-t-design-adapter-icon">
<slot></slot>
</i>
</template>
<script setup lang="ts"></script>
<script setup lang="ts">
defineOptions({
name: 'TTDesignAdapterIcon',
});
</script>

View File

@ -0,0 +1,23 @@
<template>
<TRadio :value="value" @click="clickHandler">
<template #default v-if="$slots.default"> <slot></slot> </template>
</TRadio>
</template>
<script lang="ts" setup>
import { Radio as TRadio } from 'tdesign-vue-next';
import type { RadioProps } from '@tmagic/design';
defineOptions({
name: 'TTDesignAdapterRadio',
});
defineProps<RadioProps>();
const emit = defineEmits(['click']);
const clickHandler = () => {
emit('click');
};
</script>

View File

@ -0,0 +1,23 @@
<template>
<TRadioButton :value="value" :disabled="disabled" @click="clickHandler">
<template #default v-if="$slots.default"> <slot></slot> </template>
</TRadioButton>
</template>
<script lang="ts" setup>
import { RadioButton as TRadioButton } from 'tdesign-vue-next';
import type { RadioButtonProps } from '@tmagic/design';
defineOptions({
name: 'TTDesignAdapterRadioButton',
});
defineProps<RadioButtonProps>();
const emit = defineEmits(['click']);
const clickHandler = () => {
emit('click');
};
</script>

View File

@ -0,0 +1,124 @@
<template>
<TTable
ref="table"
:data="data"
:bordered="border"
:max-height="maxHeight"
:default-expand-all="defaultExpandAll"
:show-header="showHeader"
:row-key="rowKey"
:tree="treeProps"
:empty="emptyText"
:columns="tableColumns"
@sort-change="sortChange"
@select-change="selectHandler"
@cell-click="cellClickHandler"
@expand-change="expandChange"
/>
</template>
<script setup lang="ts">
import { computed, useTemplateRef } from 'vue';
import { Table as TTable } from 'tdesign-vue-next';
import type { TableProps } from '@tmagic/design';
defineOptions({
name: 'TTDesignAdapterTable',
});
const emit = defineEmits(['sort-change', 'select', 'select-all', 'selection-change', 'expand-change', 'cell-click']);
const props = defineProps<TableProps>();
const tableRef = useTemplateRef('table');
// TDesign
const tableColumns = computed(() => {
if (!props.columns) return [];
const columns = [];
for (const item of props.columns) {
if (item.props.type === 'expand') {
continue;
}
let colKey = item.props?.prop || item.props?.type;
if (!colKey) {
colKey = 'tmagic_table_operation';
}
const column: any = {
thClassName: item.props?.class,
colKey,
title: item.props?.label,
width: item.props?.width,
fixed: item.props?.fixed === true ? 'left' : item.props?.fixed || undefined,
ellipsis: props.showOverflowTooltip,
sorter: item.props?.sortable,
align: item.props?.align,
};
//
if (item.cell) {
column.cell = (h: any, { row, rowIndex }: any) => {
return item.cell?.({ row, $index: rowIndex });
};
}
columns.push(column);
}
return columns;
});
const sortChange = (data: any) => {
emit('sort-change', data);
};
const selectHandler = (selectedRowKeys: any[], options: any) => {
const { selectedRowData, type } = options;
if (type === 'check') {
emit('select', selectedRowData);
} else if (type === 'uncheck') {
emit('select', selectedRowData);
}
emit('selection-change', selectedRowData);
};
const cellClickHandler = (context: any) => {
const { row, col, e } = context;
emit('cell-click', row, col, undefined, e);
};
const expandChange = (expandedRowKeys: any[], options: any) => {
emit('expand-change', options.expandedRowData, options.currentRowData);
};
const toggleRowSelection = (_row: any, _selected: boolean) => {
// TDesign selectedRowKeys
//
console.warn('toggleRowSelection needs to be implemented based on TDesign API');
};
const toggleRowExpansion = (_row: any, _expanded: boolean) => {
// TDesign expandedRowKeys
console.warn('toggleRowExpansion needs to be implemented based on TDesign API');
};
const clearSelection = () => {
// TDesign selectedRowKeys
console.warn('clearSelection needs to be implemented based on TDesign API');
};
defineExpose({
getEl: () => tableRef.value?.$el,
getTableRef: () => tableRef.value,
clearSelection,
toggleRowSelection,
toggleRowExpansion,
});
</script>

View File

@ -1,5 +0,0 @@
<template>
<div></div>
</template>
<script setup lang="ts"></script>

View File

@ -4,13 +4,11 @@ import {
Button as TButton,
Card as TCard,
Cascader as TCascader,
Checkbox as TCheckbox,
CheckboxGroup as TCheckboxGroup,
Col as TCol,
Collapse as TCollapse,
CollapsePanel as TCollapsePanel,
ColorPicker as TColorPicker,
Dialog as TDialog,
DialogPlugin,
Divider as TDivider,
Drawer as TDrawer,
@ -23,15 +21,12 @@ import {
Option as TOption,
OptionGroup as TOptionGroup,
Pagination as TPagination,
Radio as TRadio,
RadioButton as TRadioButton,
RadioGroup as TRadioGroup,
Row as TRow,
Select as TSelect,
StepItem as TStepItem,
Steps as TSteps,
Switch as TSwitch,
Table as TTable,
TabPanel as TTabPanel,
Tabs as TTabs,
Tag as TTag,
@ -71,7 +66,6 @@ import type {
StepProps,
StepsProps,
SwitchProps,
TableColumnProps,
TableProps,
TabPaneProps,
TabsProps,
@ -81,11 +75,15 @@ import type {
UploadProps,
} from '@tmagic/design';
import Checkbox from './Checkbox.vue';
import DatePicker from './DatePicker.vue';
import Dialog from './Dialog.vue';
import Icon from './Icon.vue';
import Input from './Input.vue';
import Radio from './Radio.vue';
import RadioButton from './RadioButton.vue';
import Scrollbar from './Scrollbar.vue';
import TableColumn from './TableColumn.vue';
import Table from './Table.vue';
const adapter: any = {
message: MessagePlugin,
@ -119,7 +117,7 @@ const adapter: any = {
props: (props: ButtonProps) => ({
theme: props.type,
size: props.size === 'default' ? 'medium' : props.size,
icon: () => (props.icon ? h(props.icon) : null),
icon: () => (props.icon ? h(Icon, null, { default: () => h(props.icon) }) : null),
variant: props.link || props.text ? 'text' : 'base',
shape: props.circle ? 'circle' : 'rectangle',
}),
@ -153,13 +151,8 @@ const adapter: any = {
},
checkbox: {
component: TCheckbox,
props: (props: CheckboxProps) => ({
modelValue: props.modelValue,
label: props.label,
value: props.value,
disabled: props.disabled,
}),
component: Checkbox,
props: (props: CheckboxProps) => props,
},
checkboxGroup: {
@ -174,14 +167,14 @@ const adapter: any = {
col: {
component: TCol,
props: (props: ColProps) => ({
span: props.span,
span: props.span ? props.span / 2 : 12,
}),
},
collapse: {
component: TCollapse,
props: (props: CollapseProps) => ({
value: props.modelValue,
modelValue: props.modelValue,
expandIconPlacement: 'right',
}),
},
@ -212,15 +205,8 @@ const adapter: any = {
},
dialog: {
component: TDialog,
props: (props: DialogProps) => ({
visible: props.modelValue,
attach: props.appendToBody ? 'body' : '',
header: props.title,
width: props.width,
mode: props.fullscreen ? 'full-screen' : 'modal',
closeOnOverlayClick: props.closeOnClickModal,
}),
component: Dialog,
props: (props: DialogProps) => props,
},
divider: {
@ -295,6 +281,7 @@ const adapter: any = {
labelWidth: props.labelWidth,
name: props.prop,
rules: props.rules,
help: props.extra,
}),
},
@ -347,18 +334,13 @@ const adapter: any = {
},
radio: {
component: TRadio,
props: (props: RadioProps) => ({
label: props.label,
value: props.value,
}),
component: Radio,
props: (props: RadioProps) => props,
},
radioButton: {
component: TRadioButton,
props: (props: RadioButtonProps) => ({
label: props.label,
}),
component: RadioButton,
props: (props: RadioButtonProps) => props,
},
radioGroup: {
@ -424,15 +406,10 @@ const adapter: any = {
},
table: {
component: TTable,
component: Table,
props: (props: TableProps) => props,
},
tableColumn: {
component: TableColumn,
props: (props: TableColumnProps) => props,
},
tabPane: {
component: TTabPanel,
props: (props: TabPaneProps) => ({

View File

@ -1,15 +1,10 @@
<template>
<div class="m-editor-nav-menu">
<TMagicButton
v-for="(item, index) in data"
class="menu-item button"
:key="index"
size="small"
link
@click="item.handler"
>
<TMagicIcon><component :is="item.icon"></component></TMagicIcon><span>{{ item.text }}</span>
</TMagicButton>
<div v-for="(item, index) in data" :key="index" class="menu-item button">
<TMagicButton size="small" link @click="item.handler">
<TMagicIcon><component :is="item.icon"></component></TMagicIcon><span>{{ item.text }}</span>
</TMagicButton>
</div>
</div>
</template>

View File

@ -85,6 +85,10 @@ export default defineConfig({
find: /^@tmagic\/element-plus-adapter/,
replacement: path.join(__dirname, '../packages/element-plus-adapter/src/index.ts'),
},
{
find: /^@tmagic\/tdesign-vue-next-adapter/,
replacement: path.join(__dirname, '../packages/tdesign-vue-next-adapter/src/index.ts'),
},
] : [],
},