Compare commits

...

20 Commits

Author SHA1 Message Date
roymondchen
611628de78 chore: update lockfile v1.7.0-beta.1 2025-10-24 18:19:46 +08:00
roymondchen
df497579c9 chore: release v1.7.0-beta.1 2025-10-24 18:18:17 +08:00
roymondchen
c7174726b3 feat(design, form, form-schema, tdesign-vue-next-adapter): textarea支持rows配置 2025-10-24 18:15:26 +08:00
roymondchen
b2474909cf feat(form): 表单校验后的错误信息将name转换成text 2025-10-24 18:13:14 +08:00
roymondchen
318433aafe build(editor): css变量使用错误导致lightingcss构建失败 2025-10-23 20:48:25 +08:00
roymondchen
b85cf6257d chore(playgournd): 添加@tmagic/design依赖 2025-10-23 20:37:48 +08:00
roymondchen
dca25e4b8f chore: update lockfile v1.7.0-beta.0 2025-10-23 20:00:55 +08:00
roymondchen
184d931dbe chore: release v1.7.0-beta.0 2025-10-23 19:59:29 +08:00
roymondchen
8ce5f71aa2 feat(design, form, tdesign-vue-next-adapter): 完善tdesign适配 2025-10-23 19:56:57 +08:00
roymondchen
6a2436fb99 fix(form): tabel复制行不生效 2025-10-23 19:55:52 +08:00
roymondchen
1eeabc8220 feat(form): 新增style,fieldStyle配置;tooltip支持配置placement;配置中的函数新增getFormValue方法 2025-10-23 19:54:14 +08:00
roymondchen
cea6569020 feat(form): text新增prepend, append不默认使用button 2025-10-23 19:49:42 +08:00
roymondchen
3435661348 feat(form): fieldset中checkbox新增name,trueValue,falseValue配置 2025-10-23 19:37:37 +08:00
roymondchen
3e76d34f59 feat(form): 新增flex-layout组件 2025-10-23 19:27:43 +08:00
roymondchen
3181f32b38 feat(design, element-plus-adapter, tdesign-vue-next-adapter): 新增popconfirm组件 2025-10-23 19:23:14 +08:00
roymondchen
dd3e901a3d feat(playground): 支持UI组件库切换 2025-10-22 14:45:33 +08:00
roymondchen
17b52aeaa0 style(design, editor, tdesign-vue-next-adapter): 编辑器顶部导航按钮tdesign下样式优化 2025-10-22 14:44:38 +08:00
roymondchen
5b16ec00e1 feat(design,editor,element-plus-adapter,form,table,tdesign-vue-next-adapter): 重构table组件,适配tdesign 2025-10-21 18:59:26 +08:00
roymondchen
566b754887 refactor(editor): 调整表单配置 2025-10-17 15:02:15 +08:00
roymondchen
d1da5fed55 refactor(form): 保持单向数据流,表单内部的组件不去修改表单的值,统一通过chang事件通知表单修改 2025-10-16 21:03:57 +08:00
107 changed files with 2977 additions and 1659 deletions

View File

@ -1,3 +1,35 @@
# [1.7.0-beta.1](https://github.com/Tencent/tmagic-editor/compare/v1.7.0-beta.0...v1.7.0-beta.1) (2025-10-24)
### Features
* **design, form, form-schema, tdesign-vue-next-adapter:** textarea支持rows配置 ([c717472](https://github.com/Tencent/tmagic-editor/commit/c7174726b3f017afb5666f9ccc343a0c98d8d1fa))
* **form:** 表单校验后的错误信息将name转换成text ([b247490](https://github.com/Tencent/tmagic-editor/commit/b2474909cff049b0800da5be3293ad35309c1b1e))
# [1.7.0-beta.0](https://github.com/Tencent/tmagic-editor/compare/v1.6.1...v1.7.0-beta.0) (2025-10-23)
### Bug Fixes
* **form:** tabel复制行不生效 ([6a2436f](https://github.com/Tencent/tmagic-editor/commit/6a2436fb99f15cdf662f39fd86952955fb78a9d2))
### Features
* **design, element-plus-adapter, tdesign-vue-next-adapter:** 新增popconfirm组件 ([3181f32](https://github.com/Tencent/tmagic-editor/commit/3181f32b389325ca1524c42af25279ceab0a9a09))
* **design, form, tdesign-vue-next-adapter:** 完善tdesign适配 ([8ce5f71](https://github.com/Tencent/tmagic-editor/commit/8ce5f71aa26ea3fb1516aa020fa65fefdaa9cc55))
* **design,editor,element-plus-adapter,form,table,tdesign-vue-next-adapter:** 重构table组件,适配tdesign ([5b16ec0](https://github.com/Tencent/tmagic-editor/commit/5b16ec00e1de45ed50f1c206de10ad48f9d71275))
* **form:** fieldset中checkbox新增name,trueValue,falseValue配置 ([3435661](https://github.com/Tencent/tmagic-editor/commit/3435661348efce00390d0a79361073e8fea8af33))
* **form:** text新增prepend, append不默认使用button ([cea6569](https://github.com/Tencent/tmagic-editor/commit/cea65690204dbe8ccf178f92fa7c47ad6fd24a8c))
* **form:** 新增flex-layout组件 ([3e76d34](https://github.com/Tencent/tmagic-editor/commit/3e76d34f59343b5c3e833d32a35fad152a5bf7fd))
* **form:** 新增style,fieldStyle配置;tooltip支持配置placement;配置中的函数新增getFormValue方法 ([1eeabc8](https://github.com/Tencent/tmagic-editor/commit/1eeabc8220649405746558d09bd648d2b650854d))
* **playground:** 支持UI组件库切换 ([dd3e901](https://github.com/Tencent/tmagic-editor/commit/dd3e901a3d4122e6f4043521ff776fc0df478f98))
* **table:** action支持配置disabled ([8809351](https://github.com/Tencent/tmagic-editor/commit/88093515373e1b431c83c0da9c28a2688421d151))
## [1.6.1](https://github.com/Tencent/tmagic-editor/compare/v1.6.0...v1.6.1) (2025-10-14)

View File

@ -1,5 +1,5 @@
{
"version": "1.6.1",
"version": "1.7.0-beta.1",
"name": "tmagic",
"private": true,
"type": "module",

View File

@ -1,5 +1,5 @@
{
"version": "1.6.1",
"version": "1.7.0-beta.1",
"name": "@tmagic/cli",
"main": "lib/index.js",
"types": "lib/index.d.ts",

View File

@ -1,5 +1,5 @@
{
"version": "1.6.1",
"version": "1.7.0-beta.1",
"name": "@tmagic/core",
"type": "module",
"main": "dist/tmagic-core.umd.cjs",

View File

@ -1,5 +1,5 @@
{
"version": "1.6.1",
"version": "1.7.0-beta.1",
"name": "@tmagic/data-source",
"type": "module",
"main": "dist/tmagic-data-source.umd.cjs",

View File

@ -1,5 +1,5 @@
{
"version": "1.6.1",
"version": "1.7.0-beta.1",
"name": "@tmagic/dep",
"type": "module",
"main": "dist/tmagic-dep.umd.cjs",

View File

@ -1,5 +1,5 @@
{
"version": "1.6.1",
"version": "1.7.0-beta.1",
"name": "@tmagic/design",
"type": "module",
"sideEffects": [

View File

@ -1,5 +1,8 @@
<template>
<component class="tmagic-design-button" :is="uiComponent" v-bind="uiProps" @click="clickHandler">
<template #icon v-if="$slots.icon">
<slot name="icon"></slot>
</template>
<template #default v-if="$slots.default">
<slot></slot>
</template>
@ -30,3 +33,15 @@ const clickHandler = (...args: any[]) => {
emit('click', ...args);
};
</script>
<style lang="scss">
.tmagic-design-button {
.t-button__text {
align-items: center;
}
+ .tmagic-design-button {
margin-left: 12px;
}
}
</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

@ -7,6 +7,8 @@
@change="changeHandler"
@input="inputHandler"
@update:modelValue="updateModelValue"
@blur="blurHandler"
@focus="focusHandler"
>
<template #prepend v-if="$slots.prepend">
<slot name="prepend"></slot>
@ -41,7 +43,7 @@ const uiComponent = ui?.component || 'el-input';
const uiProps = computed<InputProps>(() => ui?.props(props) || props);
const emit = defineEmits(['change', 'input', 'update:modelValue']);
const emit = defineEmits(['change', 'input', 'blur', 'focus', 'update:modelValue']);
const instance = ref<any>();
@ -57,13 +59,56 @@ const updateModelValue = (...args: any[]) => {
emit('update:modelValue', ...args);
};
const blurHandler = (...args: any[]) => {
emit('blur', ...args);
};
const focusHandler = (...args: any[]) => {
emit('focus', ...args);
};
defineExpose({
instance,
getInput() {
return instance.value.input;
if (instance.value.input) {
return instance.value.input;
}
return instance.value?.$el?.querySelector('input');
},
getTextarea() {
return instance.value.textarea;
if (instance.value.textarea) {
return instance.value.textarea;
}
return instance.value?.$el?.querySelector('textarea');
},
});
</script>
<style lang="scss">
.tmagic-design-input {
&.t-input-adornment {
.t-input-adornment__prepend {
> span {
border-radius: var(--td-radius-default) 0 0 var(--td-radius-default);
}
}
.t-input-adornment__append {
> span {
border-radius: 0 var(--td-radius-default) var(--td-radius-default) 0;
}
}
.t-input-adornment__prepend,
.t-input-adornment__append {
> span {
display: inline-flex;
height: 100%;
align-items: center;
box-sizing: border-box;
white-space: nowrap;
padding: 0 var(--td-comp-paddingLR-s);
border: 1px solid var(--td-border-level-2-color);
}
}
}
}
</style>

View File

@ -0,0 +1,42 @@
<template>
<component
class="tmagic-design-popconfirm"
:is="uiComponent"
v-bind="uiProps"
@confirm="confirmHandler"
@cancel="cancelHandler"
>
<template #reference>
<slot name="reference"></slot>
</template>
</component>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import { getDesignConfig } from './config';
import type { PopconfirmProps } from './types';
defineOptions({
name: 'TMPopconfirm',
});
const emit = defineEmits(['confirm', 'cancel']);
const props = defineProps<PopconfirmProps>();
const ui = getDesignConfig('components')?.popconfirm;
const uiComponent = ui?.component || 'el-popconfirm';
const uiProps = computed<PopconfirmProps>(() => ui?.props(props) || props);
const confirmHandler = (...args: any[]) => {
emit('confirm', ...args);
};
const cancelHandler = (...args: any[]) => {
emit('cancel', ...args);
};
</script>

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

@ -8,6 +8,9 @@
@tab-remove="onTabRemove"
@update:model-value="updateModelName"
>
<template #add-icon v-if="$slots['add-icon']">
<slot name="add-icon"></slot>
</template>
<template #default>
<slot></slot>
</template>

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,14 +46,13 @@ 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 { default as TMagicPopconfirm } from './Popconfirm.vue';
export const tMagicMessage = {
error: (msg: string) => {

View File

@ -32,6 +32,7 @@ export interface ButtonProps {
text?: boolean;
circle?: boolean;
icon?: any;
variant?: string;
}
export interface CardProps {
@ -182,6 +183,7 @@ export interface FormItemProps {
prop?: string;
labelWidth?: string | number;
rules?: any;
extra?: string;
}
export interface InputProps {
@ -226,6 +228,23 @@ export interface PaginationProps {
total?: number;
}
export interface PopconfirmProps {
title?: string;
placement?:
| 'top'
| 'left'
| 'right'
| 'bottom'
| 'top-left'
| 'top-right'
| 'bottom-left'
| 'bottom-right'
| 'left-top'
| 'left-bottom'
| 'right-top'
| 'right-bottom';
}
export interface PopoverProps {
placement?: Placement;
width?: string | number;
@ -295,17 +314,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 {
@ -317,7 +355,7 @@ export interface TabPaneProps {
export interface TabsProps {
type?: string;
editable?: boolean;
tabPosition?: string;
tabPosition?: 'left' | 'right' | 'top' | 'bottom';
modelValue?: string | number;
}
@ -342,33 +380,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 +646,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 +658,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 +683,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<
@ -707,6 +695,11 @@ export interface Components {
| string;
props: (props: UploadProps) => UploadProps;
};
popconfirm: {
component: DefineComponent<PopconfirmProps, {}, any> | string;
props: (props: PopconfirmProps) => PopconfirmProps;
};
}
export interface DesignPluginOptions {

View File

@ -1,5 +1,5 @@
{
"version": "1.6.1",
"version": "1.7.0-beta.1",
"name": "@tmagic/editor",
"type": "module",
"sideEffects": [

View File

@ -12,13 +12,20 @@
<template v-else-if="data.type === 'button'">
<TMagicTooltip v-if="data.tooltip" effect="dark" placement="bottom-start" :content="data.tooltip">
<TMagicButton size="small" link :disabled="disabled"
><MIcon v-if="data.icon" :icon="data.icon"></MIcon><span>{{ data.text }}</span></TMagicButton
>
<TMagicButton size="small" link :disabled="disabled">
<template #icon v-if="data.icon">
<MIcon :icon="data.icon"></MIcon>
</template>
<template #default v-if="data.text">{{ data.text }}</template>
</TMagicButton>
</TMagicTooltip>
<TMagicButton v-else size="small" link :disabled="disabled" :title="data.text"
><MIcon v-if="data.icon" :icon="data.icon"></MIcon><span>{{ data.text }}</span></TMagicButton
>
<TMagicButton v-else size="small" link :disabled="disabled" :title="data.text">
<template #icon v-if="data.icon">
<MIcon :icon="data.icon"></MIcon>
</template>
<template #default v-if="data.text">{{ data.text }}</template>
</TMagicButton>
</template>
<TMagicDropdown

View File

@ -55,7 +55,7 @@ const config = computed<GroupListConfig>(() => ({
{
type: 'table',
name: 'cond',
operateColWidth: 100,
operateColWidth: 80,
enableToggleMode: false,
items: [
parentFields.value.length
@ -93,7 +93,7 @@ const config = computed<GroupListConfig>(() => ({
type: 'cond-op-select',
parentFields: parentFields.value,
label: '条件',
width: 160,
width: 140,
name: 'op',
},
{

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

@ -4,7 +4,7 @@
<span class="help-txt" v-if="item.text">{{ item.text }}</span>
<span class="next-input">
<input
v-model="model[item.name]"
:model-value="model[item.name]"
placeholder="0"
:title="model[item.name]"
:disabled="disabled"

View File

@ -3,7 +3,7 @@
<div v-for="(item, index) in list" :key="index" :class="item.class">
<span class="next-input">
<input
v-model="model[item.name]"
:model-value="model[item.name]"
placeholder="0"
:title="model[item.name]"
:disabled="disabled"

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

@ -574,8 +574,6 @@ class Editor extends BaseService {
nodes.splice(targetIndex, 1, newConfig);
this.set('nodes', [...nodes]);
// update后会触发依赖收集收集完后会掉stage.update方法
if (isPage(newConfig) || isPageFragment(newConfig)) {
this.set('page', newConfig as MPage | MPageFragment);
}
@ -591,6 +589,7 @@ class Editor extends BaseService {
/**
*
* update后会触发依赖收集stage.update方法
* @param config id信息
* @returns
*/

View File

@ -18,6 +18,7 @@
> div {
display: flex;
gap: 3px;
height: 100%;
z-index: 1;
align-items: center;
@ -76,5 +77,10 @@
transform: rotate(-90deg);
}
}
.t-button {
padding-left: 1px;
padding-right: 1px;
}
}
}

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: $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

@ -1,5 +1,5 @@
{
"version": "1.6.1",
"version": "1.7.0-beta.1",
"name": "@tmagic/element-plus-adapter",
"type": "module",
"main": "dist/tmagic-element-plus-adapter.umd.cjs",

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,
@ -28,6 +27,7 @@ import {
ElOption,
ElOptionGroup,
ElPagination,
ElPopconfirm,
ElRadio,
ElRadioButton,
ElRadioGroup,
@ -37,14 +37,11 @@ import {
ElStep,
ElSteps,
ElSwitch,
ElTable,
ElTableColumn,
ElTabPane,
ElTabs,
ElTag,
ElTimePicker,
ElTooltip,
ElTree,
ElUpload,
useZIndex,
} from 'element-plus';
@ -76,6 +73,7 @@ import type {
OptionGroupProps,
OptionProps,
PaginationProps,
PopconfirmProps,
RadioButtonProps,
RadioGroupProps,
RadioProps,
@ -83,17 +81,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 +194,7 @@ const adapter: DesignPluginOptions = {
},
formItem: {
component: ElFormItem as any,
component: FormItem as any,
props: (props: FormItemProps) => props,
},
@ -275,15 +274,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,15 +303,15 @@ 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,
},
popconfirm: {
component: ElPopconfirm as any,
props: (props: PopconfirmProps) => props,
},
},
loading: ElLoading.directive,
};

View File

@ -1,5 +1,5 @@
{
"version": "1.6.1",
"version": "1.7.0-beta.1",
"name": "@tmagic/form-schema",
"type": "module",
"main": "dist/tmagic-form-schema.umd.cjs",

View File

@ -15,13 +15,14 @@ export interface ChangeRecord {
export interface OnChangeHandlerData {
model: FormValue;
values?: FormValue;
values?: Readonly<FormValue>;
parent?: FormValue;
formValue?: FormValue;
config: any;
config: Readonly<any>;
prop: string;
changeRecords: ChangeRecord[];
setModel: (prop: string, value: any) => void;
setFromValue: (prop: string, value: any) => void;
}
export type FormValue = Record<string | number, any>;
@ -83,6 +84,8 @@ export interface SortProp {
order: 'ascending' | 'descending';
}
export type ToolTipConfigType = string | { text?: string; placement?: string };
export interface FormItem {
/** vnode的key值默认是遍历数组时的index */
__key?: string | number;
@ -98,7 +101,7 @@ export interface FormItem {
/** 额外的提示信息,和 help 类似,当提示文案同时出现时,可以使用这个。 */
extra?: string | FilterFunction<string>;
/** 配置提示信息 */
tooltip?: string | FilterFunction<string>;
tooltip?: ToolTipConfigType | FilterFunction<ToolTipConfigType>;
/** 是否置灰 */
disabled?: boolean | FilterFunction;
/** 使用表单中的值作为key例如配置了text则使用model.text作为key */
@ -123,6 +126,8 @@ export interface FormItem {
dynamicKey?: string;
/** 是否需要显示`展开更多配置` */
expand?: boolean;
style?: Record<string, any>;
fieldStyle?: Record<string, any>;
[key: string]: any;
}
@ -193,6 +198,7 @@ export type FilterFunction<T = boolean> = (
prop: string;
config: any;
index?: number;
getFormValue: (prop: string) => any;
},
) => T;
@ -343,6 +349,7 @@ export interface DisplayConfig extends FormItem {
export interface TextConfig extends FormItem, Input {
type?: 'text';
tooltip?: string;
prepend?: string;
/** 后置元素,一般为标签或按钮 */
append?:
| string
@ -366,6 +373,7 @@ export interface TextConfig extends FormItem, Input {
export interface TextareaConfig extends FormItem {
type: 'textarea';
placeholder?: string;
rows?: number;
}
/**
@ -385,6 +393,7 @@ export interface NumberConfig extends FormItem {
*/
export interface NumberRangeConfig extends FormItem {
type?: 'number-range';
clearable?: boolean;
}
/**
@ -429,6 +438,7 @@ export interface CheckboxConfig extends FormItem {
type: 'checkbox';
activeValue?: number | string;
inactiveValue?: number | string;
useLabel?: boolean;
}
/**
@ -640,7 +650,13 @@ export interface TabConfig extends FormItem, ContainerCommonConfig {
*/
export interface FieldsetConfig extends FormItem, ContainerCommonConfig {
type: 'fieldset';
checkbox?: boolean;
checkbox?:
| boolean
| {
name: string;
trueValue?: string | number;
falseValue?: string | number;
};
expand?: boolean;
legend?: string;
schematic?: string;
@ -698,6 +714,7 @@ export interface TableConfig extends FormItem {
onSelect?: (mForm: FormState | undefined, data: any) => any;
defautSort?: SortProp;
defaultSort?: SortProp;
/** 是否支持拖拽排序 */
dropSort?: boolean;
/** 是否显示全屏按钮 */
enableFullscreen?: boolean;
@ -758,6 +775,10 @@ export interface ComponentConfig extends FormItem {
display: any;
}
export interface FlexLayoutConfig extends FormItem, ContainerCommonConfig {
type: 'flex-layout';
}
export type ChildConfig =
| FormItem
| TabConfig

View File

@ -1,5 +1,5 @@
{
"version": "1.6.1",
"version": "1.7.0-beta.1",
"name": "@tmagic/form",
"type": "module",
"sideEffects": [

View File

@ -28,10 +28,11 @@
</template>
<script setup lang="ts">
import { provide, reactive, ref, shallowRef, toRaw, watch, watchEffect } from 'vue';
import { provide, reactive, ref, shallowRef, toRaw, useTemplateRef, watch, watchEffect } from 'vue';
import { cloneDeep, isEqual } from 'lodash-es';
import { TMagicForm, tMagicMessage, tMagicMessageBox } from '@tmagic/design';
import { setValueByKeyPath } from '@tmagic/utils';
import Container from './containers/Container.vue';
import { getConfig } from './utils/config';
@ -83,7 +84,7 @@ const props = withDefaults(
const emit = defineEmits(['change', 'error', 'field-input', 'field-change', 'update:stepActive']);
const tMagicForm = ref<InstanceType<typeof TMagicForm>>();
const tMagicFormRef = useTemplateRef('tMagicForm');
const initialized = ref(false);
const values = ref<FormValue>({});
const lastValuesProcessed = ref<FormValue>({});
@ -173,7 +174,18 @@ watch(
const changeHandler = (v: FormValue, eventData: ContainerChangeEventData) => {
if (eventData.changeRecords?.length) {
changeRecords.value.push(...eventData.changeRecords);
for (const record of eventData.changeRecords) {
if (record.propPath) {
const index = changeRecords.value.findIndex((item) => item.propPath === record.propPath);
if (index > -1) {
changeRecords.value[index] = record;
} else {
changeRecords.value.push(record);
}
setValueByKeyPath(record.propPath, record.value, values.value);
}
}
}
emit('change', values.value, eventData);
};
@ -184,6 +196,46 @@ const submitHandler = (e: SubmitEvent) => {
}
};
/**
* 通过 name config 中查找对应的 text
* @param name - 字段名支持点分隔的路径格式 'a.b.c'
* @param config - 表单配置数组
* @returns 找到的 text 如果未找到则返回 undefined
*/
const getTextByName = (name: string, config: FormConfig = props.config): string | undefined => {
if (!name || !Array.isArray(config)) return undefined;
const nameParts = name.split('.');
const findInConfig = (configs: FormConfig, parts: string[]): string | undefined => {
if (parts.length === 0) return undefined;
const [currentPart, ...remainingParts] = parts;
for (const item of configs) {
if (item.name === currentPart) {
if (remainingParts.length === 0) {
return typeof item.text === 'string' ? item.text : undefined;
}
if (item.items && Array.isArray(item.items)) {
const result = findInConfig(item.items, remainingParts);
if (result !== undefined) return result;
}
}
if (item.items && Array.isArray(item.items)) {
const result = findInConfig(item.items, parts);
if (result !== undefined) return result;
}
}
return undefined;
};
return findInConfig(config, nameParts);
};
defineExpose({
values,
lastValuesProcessed,
@ -194,29 +246,38 @@ defineExpose({
changeHandler,
resetForm: () => {
tMagicForm.value?.resetFields();
tMagicFormRef.value?.resetFields();
changeRecords.value = [];
},
submitForm: async (native?: boolean): Promise<any> => {
try {
await tMagicForm.value?.validate();
const result = await tMagicFormRef.value?.validate();
// tdesign
// element-plus throw error
if (result !== true) {
throw result;
}
changeRecords.value = [];
return native ? values.value : cloneDeep(toRaw(values.value));
} catch (invalidFields: any) {
emit('error', invalidFields);
const error: string[] = [];
Object.entries(invalidFields).forEach(([, ValidateError]) => {
Object.entries(invalidFields).forEach(([prop, ValidateError]) => {
(ValidateError as ValidateError[]).forEach(({ field, message }) => {
if (field && message) error.push(`${field} -> ${message}`);
if (field && !message) error.push(`${field} -> 出现错误`);
if (!field && message) error.push(`${message}`);
const name = field || prop;
const text = getTextByName(name, props.config) || name;
error.push(`${text} -> ${message}`);
});
});
throw new Error(error.join('<br>'));
}
},
getTextByName,
});
</script>

View File

@ -1,84 +1,55 @@
<template>
<div
v-if="config"
:data-tmagic-id="config.id"
:data-tmagic-form-item-prop="itemProp"
:style="config.tip ? 'display: flex;align-items: baseline;' : ''"
:class="`m-form-container m-container-${type || ''} ${config.className || ''}`"
:class="`m-form-container m-container-${type || ''} ${config.className || ''}${config.tip ? ' has-tip' : ''}`"
:style="config.style"
>
<m-fields-hidden
v-if="type === 'hidden'"
:model="model"
:config="config"
:name="config.name"
:disabled="disabled"
:prop="itemProp"
></m-fields-hidden>
<m-fields-hidden v-if="type === 'hidden'" v-bind="fieldsProps" :model="model"></m-fields-hidden>
<component
v-else-if="items && !text && type && display"
:key="key(config)"
:size="size"
v-bind="fieldsProps"
:is="tagName"
:model="model"
:last-values="lastValues"
:is-compare="isCompare"
:config="config"
:disabled="disabled"
:name="name"
:prop="itemProp"
:step-active="stepActive"
:expand-more="expand"
:label-width="itemLabelWidth"
:style="config.fieldStyle"
@change="onChangeHandler"
@addDiffCount="onAddDiffCount"
></component>
<template v-else-if="type && display && !showDiff">
<TMagicFormItem
:style="config.tip ? 'flex: 1' : ''"
:class="{ 'tmagic-form-hidden': `${itemLabelWidth}` === '0' || !text }"
:prop="itemProp"
:label-width="itemLabelWidth"
:label-position="config.labelPosition"
:rules="rule"
>
<template #label><span v-html="type === 'checkbox' ? '' : text" :title="config.labelTitle"></span></template>
<TMagicTooltip v-if="tooltip">
<TMagicFormItem v-bind="formItemProps" :class="{ 'tmagic-form-hidden': `${itemLabelWidth}` === '0' || !text }">
<template #label
><span v-html="type === 'checkbox' && !config.useLabel ? '' : text" :title="config.labelTitle"></span
></template>
<TMagicTooltip v-if="tooltip.text" :placement="tooltip.placement">
<component
:key="key(config)"
:size="size"
v-bind="fieldsProps"
:is="tagName"
:model="model"
:last-values="lastValues"
:config="config"
:name="name"
:disabled="disabled"
:prop="itemProp"
@change="onChangeHandler"
@addDiffCount="onAddDiffCount"
></component>
<template #content>
<div v-html="tooltip"></div>
<div v-html="tooltip.text"></div>
</template>
</TMagicTooltip>
<component
v-else
:key="key(config)"
:size="size"
v-bind="fieldsProps"
:is="tagName"
:model="model"
:last-values="lastValues"
:config="config"
:name="name"
:disabled="disabled"
:prop="itemProp"
@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">
@ -93,46 +64,18 @@
<template v-else-if="type && display && showDiff">
<!-- 上次内容 -->
<TMagicFormItem
:style="config.tip ? 'flex: 1' : ''"
:class="{ 'tmagic-form-hidden': `${itemLabelWidth}` === '0' || !text }"
:prop="itemProp"
:label-width="itemLabelWidth"
:label-position="config.labelPosition"
:rules="rule"
style="background: #f7dadd"
v-bind="formItemProps"
:class="{ 'tmagic-form-hidden': `${itemLabelWidth}` === '0' || !text, 'show-diff': true }"
>
<template #label><span v-html="type === 'checkbox' ? '' : text" :title="config.labelTitle"></span></template>
<TMagicTooltip v-if="tooltip">
<component
:key="key(config)"
:size="size"
:is="tagName"
:model="lastValues"
:config="config"
:name="name"
:disabled="disabled"
:prop="itemProp"
@change="onChangeHandler"
></component>
<TMagicTooltip v-if="tooltip.text" :placement="tooltip.placement">
<component v-bind="fieldsProps" :is="tagName" :model="lastValues" @change="onChangeHandler"></component>
<template #content>
<div v-html="tooltip"></div>
<div v-html="tooltip.text"></div>
</template>
</TMagicTooltip>
<component
v-else
:key="key(config)"
:size="size"
:is="tagName"
:model="lastValues"
:config="config"
:name="name"
:disabled="disabled"
:prop="itemProp"
@change="onChangeHandler"
></component>
<div v-if="extra" v-html="extra" class="m-form-tip"></div>
<component v-else v-bind="fieldsProps" :is="tagName" :model="lastValues" @change="onChangeHandler"></component>
</TMagicFormItem>
<TMagicTooltip v-if="config.tip" placement="left">
@ -141,48 +84,22 @@
<div v-html="config.tip"></div>
</template>
</TMagicTooltip>
<!-- 当前内容 -->
<TMagicFormItem
v-bind="formItemProps"
:style="config.tip ? 'flex: 1' : ''"
:class="{ 'tmagic-form-hidden': `${itemLabelWidth}` === '0' || !text }"
:prop="itemProp"
:label-width="itemLabelWidth"
:label-position="config.labelPosition"
:rules="rule"
style="background: #def7da"
:class="{ 'tmagic-form-hidden': `${itemLabelWidth}` === '0' || !text, 'show-diff': true }"
>
<template #label><span v-html="type === 'checkbox' ? '' : text" :title="config.labelTitle"></span></template>
<TMagicTooltip v-if="tooltip">
<component
:key="key(config)"
:size="size"
:is="tagName"
:model="model"
:config="config"
:name="name"
:disabled="disabled"
:prop="itemProp"
@change="onChangeHandler"
></component>
<TMagicTooltip v-if="tooltip.text" :placement="tooltip.placement">
<component v-bind="fieldsProps" :is="tagName" :model="model" @change="onChangeHandler"></component>
<template #content>
<div v-html="tooltip"></div>
<div v-html="tooltip.text"></div>
</template>
</TMagicTooltip>
<component
v-else
:key="key(config)"
:size="size"
:is="tagName"
:model="model"
:config="config"
:name="name"
:disabled="disabled"
:prop="itemProp"
@change="onChangeHandler"
></component>
<div v-if="extra" v-html="extra" class="m-form-tip"></div>
<component v-else v-bind="fieldsProps" :is="tagName" :model="model" @change="onChangeHandler"></component>
</TMagicFormItem>
<TMagicTooltip v-if="config.tip" placement="left">
@ -223,14 +140,21 @@
</template>
<script setup lang="ts">
import { computed, inject, ref, toRaw, watch, watchEffect } from 'vue';
import { computed, inject, readonly, ref, toRaw, watch, watchEffect } from 'vue';
import { WarningFilled } from '@element-plus/icons-vue';
import { isEqual } from 'lodash-es';
import { TMagicButton, TMagicFormItem, TMagicIcon, TMagicTooltip } from '@tmagic/design';
import { setValueByKeyPath } from '@tmagic/utils';
import { getValueByKeyPath } from '@tmagic/utils';
import type { ChildConfig, ContainerChangeEventData, ContainerCommonConfig, FormState, FormValue } from '../schema';
import type {
ChildConfig,
ContainerChangeEventData,
ContainerCommonConfig,
FormState,
FormValue,
ToolTipConfigType,
} from '../schema';
import { display as displayFunction, filterFunction, getRules } from '../utils/form';
defineOptions({
@ -311,9 +235,20 @@ const disabled = computed(() => props.disabled || filterFunction(mForm, props.co
const text = computed(() => filterFunction(mForm, props.config.text, props));
const tooltip = computed(() => filterFunction(mForm, props.config.tooltip, props));
const tooltip = computed(() => {
const config = filterFunction<ToolTipConfigType>(mForm, props.config.tooltip, props);
if (typeof config === 'string') {
return {
text: config,
placement: 'top',
};
}
const extra = computed(() => filterFunction(mForm, props.config.extra, props));
return {
text: config?.text,
placement: config?.placement || 'top',
};
});
const rule = computed(() => getRules(mForm, props.config.rules, props));
@ -334,6 +269,24 @@ const display = computed((): boolean => {
return value;
});
const fieldsProps = computed(() => ({
size: props.size,
config: props.config,
name: name.value,
disabled: disabled.value,
prop: itemProp.value,
key: props.config[mForm?.keyProps],
style: props.config.fieldStyle,
}));
const formItemProps = computed(() => ({
prop: itemProp.value,
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);
watchEffect(() => {
@ -367,6 +320,7 @@ const filterHandler = (filter: any, value: FormValue | number | string) => {
formValue: mForm?.values,
prop: itemProp.value,
config: props.config,
getFormValue: (prop: string) => getValueByKeyPath(prop, mForm?.values || props.model),
});
}
@ -406,8 +360,29 @@ const isValidName = () => {
return true;
};
const createModelProxy = (
target: any,
setModelFn: (_key: string, _value: any) => void,
pathPrefix: string = '',
): any => {
return new Proxy(target, {
get: (obj, key: string) => {
const value = obj[key];
if (value && typeof value === 'object') {
const newPath = pathPrefix ? `${pathPrefix}.${key}` : key;
return createModelProxy(value, setModelFn, newPath);
}
return value;
},
set: (obj, key: string, value) => {
setModelFn(pathPrefix ? `${pathPrefix}.${key}` : key, value);
return true;
},
});
};
const onChangeHandler = async function (v: any, eventData: ContainerChangeEventData = {}) {
const { filter, onChange, trim, dynamicKey } = props.config as any;
const { filter, onChange, trim } = props.config as any;
let value: FormValue | number | string | any[] = toRaw(v);
const changeRecords = eventData.changeRecords || [];
const newChangeRecords = [...changeRecords];
@ -416,20 +391,29 @@ const onChangeHandler = async function (v: any, eventData: ContainerChangeEventD
value = filterHandler(filter, v);
if (typeof onChange === 'function') {
const setModel = (key: string, value: any) => {
if (props.config.name) {
newChangeRecords.push({ propPath: itemProp.value.replace(`${props.config.name}`, key), value });
} else {
newChangeRecords.push({ propPath: itemProp.value, value });
}
};
const setFormValue = (key: string, value: any) => {
newChangeRecords.push({ propPath: key, value });
};
value =
(await onChange(mForm, value, {
model: props.model,
values: mForm?.initValues,
formValue: mForm?.values,
model: createModelProxy(props.model, setModel),
values: mForm ? readonly(mForm.initValues) : null,
formValue: createModelProxy(mForm?.values || {}, setFormValue),
prop: itemProp.value,
config: props.config,
changeRecords: newChangeRecords,
setModel: (key: string, value: any) => {
setValueByKeyPath(key, value, props.model);
if (props.config.name) {
newChangeRecords.push({ propPath: itemProp.value.replace(`${props.config.name}`, key), value });
}
},
setModel,
setFormValue,
getFormValue: (prop: string) => getValueByKeyPath(prop, mForm?.values || props.model),
})) ?? value;
}
value = trimHandler(trim, value) ?? value;
@ -440,19 +424,10 @@ const onChangeHandler = async function (v: any, eventData: ContainerChangeEventD
let valueProp = itemProp.value;
if (hasModifyKey(eventData)) {
if (dynamicKey) {
props.model[eventData.modifyKey!] = value;
} else if (isValidName()) {
props.model[name.value][eventData.modifyKey!] = value;
}
valueProp = valueProp ? `${valueProp}.${eventData.modifyKey}` : eventData.modifyKey!;
// modifyKey
delete eventData.modifyKey;
} else if (isValidName() && props.model !== value && (v !== value || props.model[name.value] !== value)) {
// fieldfield-linkmodel===value,
props.model[name.value] = value;
}
if (changeRecords.length === 0) {

View File

@ -2,10 +2,10 @@
<fieldset v-if="name ? model[name] : model" class="m-fieldset" :style="show ? 'padding: 15px' : 'border: 0'">
<component v-if="name && config.checkbox" :is="!show ? 'div' : 'legend'">
<TMagicCheckbox
v-model="model[name].value"
:prop="`${prop}${prop ? '.' : ''}${config.name}.value`"
:true-value="1"
:false-value="0"
:model-value="(name ? model[name] : model)[checkboxName]"
:prop="`${prop}${prop ? '.' : ''}${config.name}.${checkboxName}`"
:true-value="checkboxTrueValue"
:false-value="checkboxFalseValue"
@update:modelValue="valueChangeHandler"
><span v-html="config.legend"></span><span v-if="config.extra" v-html="config.extra" class="m-form-tip"></span
></TMagicCheckbox>
@ -99,9 +99,33 @@ const mForm = inject<FormState | undefined>('mForm');
const name = computed(() => props.config.name || '');
const checkboxName = computed(() => {
if (typeof props.config.checkbox === 'object' && typeof props.config.checkbox.name === 'string') {
return props.config.checkbox.name;
}
return 'value';
});
const checkboxTrueValue = computed(() => {
if (typeof props.config.checkbox === 'object' && typeof props.config.checkbox.trueValue !== 'undefined') {
return props.config.checkbox.trueValue;
}
return 1;
});
const checkboxFalseValue = computed(() => {
if (typeof props.config.checkbox === 'object' && typeof props.config.checkbox.falseValue !== 'undefined') {
return props.config.checkbox.falseValue;
}
return 0;
});
const show = computed(() => {
if (props.config.expand && name.value) {
return props.model[name.value]?.value;
if (props.config.expand && checkboxName.value) {
return (name.value ? props.model[name.value] : props.model)?.[checkboxName.value] === checkboxTrueValue.value;
}
return true;
});
@ -114,7 +138,7 @@ const lWidth = computed(() => {
});
const valueChangeHandler = (value: number | boolean) => {
emit('change', value, { modifyKey: 'value' });
emit('change', value, { modifyKey: checkboxName.value });
};
const changeHandler = (v: any, eventData: ContainerChangeEventData) => emit('change', v, eventData);

View File

@ -0,0 +1,59 @@
<template>
<div class="m-form-flex-layout" :style="{ display: 'flex', flexWrap: 'wrap', gap }">
<Container
v-for="(item, index) in config.items"
:key="(item as Record<string, any>)[mForm?.keyProp || '__key'] ?? index"
:config="item"
:model="name ? model[name] : model"
:lastValues="name ? lastValues[name] : lastValues"
:is-compare="isCompare"
:prop="prop"
:size="size"
:disabled="disabled"
:label-width="config.labelWidth || labelWidth"
@change="changeHandler"
@addDiffCount="onAddDiffCount()"
/>
</div>
</template>
<script setup lang="ts">
import { computed, inject } from 'vue';
import type { FlexLayoutConfig } from '@tmagic/form-schema';
import type { ContainerChangeEventData, FormState } from '../schema';
import Container from './Container.vue';
defineOptions({
name: 'MFormFlexLayout',
});
const props = defineProps<{
model: any;
lastValues?: any;
isCompare?: boolean;
config: FlexLayoutConfig;
name?: string;
labelWidth?: string;
prop?: string;
size?: string;
disabled?: boolean;
}>();
const emit = defineEmits<{
change: [v: any, eventData: ContainerChangeEventData];
addDiffCount: [];
}>();
const mForm = inject<FormState | undefined>('mForm');
const gap = computed(() => props.config.gap || '16px');
const changeHandler = (v: any, eventData: ContainerChangeEventData) => {
emit('change', props.model, eventData);
};
const onAddDiffCount = () => emit('addDiffCount');
</script>

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

@ -167,7 +167,17 @@ watchEffect(() => {
const tabItems = (tab: TabPaneConfig) => (props.config.dynamic ? props.config.items : tab.items);
const tabClickHandler = (tab: any) => tabClick(mForm, tab, props);
const tabClickHandler = (tab: any) => {
if (typeof tab === 'object') {
tabClick(mForm, tab, props);
} else {
let item = tabs.value.find((tab: any) => tab.status === tab);
if (!item) {
item = tabs.value[tab];
}
tabClick(mForm, item, props);
}
};
const onTabAdd = async () => {
if (!props.name) throw new Error('dynamic tab 必须配置name');

View File

@ -1,6 +1,6 @@
<template>
<TMagicCascader
v-model="value"
:model-value="value"
ref="tMagicCascader"
style="width: 100%"
clearable
@ -15,6 +15,7 @@
emitPath: config.emitPath ?? true,
checkStrictly: checkStrictly ?? false,
}"
@update:model-value="updateModelValueHandler"
@change="changeHandler"
></TMagicCascader>
</template>
@ -51,22 +52,31 @@ const remoteData = ref<any>(null);
const checkStrictly = computed(() => filterFunction(mForm, props.config.checkStrictly, props));
const valueSeparator = computed(() => filterFunction<string>(mForm, props.config.valueSeparator, props));
const value = computed({
get() {
if (typeof props.model[props.name] === 'string' && valueSeparator.value) {
return props.model[props.name].split(valueSeparator.value);
}
return props.model[props.name];
},
set(value) {
let result = value;
if (valueSeparator.value) {
result = value.join(valueSeparator.value);
}
props.model[props.name] = result;
},
const value = computed(() => {
if (typeof props.model[props.name] === 'string' && valueSeparator.value) {
return props.model[props.name].split(valueSeparator.value);
}
return props.model[props.name];
});
const updateModelValueHandler = (value: string[] | number[] | any) => {
let result = value;
if (valueSeparator.value) {
result = value.join(valueSeparator.value);
}
if (typeof result === 'undefined') {
if (Array.isArray(props.model[props.name])) {
emit('change', []);
} else if (typeof props.model[props.name] === 'string') {
emit('change', '');
} else if (typeof props.model[props.name] === 'object') {
emit('change', null);
}
}
emit('change', result);
};
const setRemoteOptions = async function () {
const { config } = props;
const { option } = config;
@ -126,6 +136,5 @@ const changeHandler = () => {
if (!tMagicCascader.value) return;
tMagicCascader.value.setQuery('');
tMagicCascader.value.setPreviousQuery(null);
emit('change', props.model[props.name]);
};
</script>

View File

@ -1,12 +1,12 @@
<template>
<TMagicCheckbox
v-model="model[name]"
:model-value="model[name]"
:size="size"
:trueValue="activeValue"
:falseValue="inactiveValue"
:disabled="disabled"
@change="changeHandler"
>{{ config.text }}</TMagicCheckbox
@update:model-value="changeHandler"
><template #default v-if="!config.useLabel">{{ config.text }}</template></TMagicCheckbox
>
</template>

View File

@ -1,5 +1,5 @@
<template>
<TMagicCheckboxGroup v-model="model[name]" :size="size" :disabled="disabled" @change="changeHandler">
<TMagicCheckboxGroup :model-value="model[name]" :size="size" :disabled="disabled" @update:model-value="changeHandler">
<TMagicCheckbox v-for="option in options" :value="option.value" :key="option.value" :disabled="option.disabled"
>{{ option.text }}
</TMagicCheckbox>

View File

@ -1,10 +1,10 @@
<template>
<TMagicColorPicker
v-model="model[name]"
:model-value="model[name]"
:size="size"
:disabled="disabled"
:showAlpha="true"
@change="changeHandler"
@update:model-value="changeHandler"
></TMagicColorPicker>
</template>

View File

@ -1,13 +1,13 @@
<template>
<TMagicDatePicker
v-model="model[name]"
:model-value="model[name]"
type="date"
:size="size"
:placeholder="config.placeholder"
:disabled="disabled"
:format="config.format || 'YYYY/MM/DD'"
:value-format="config.valueFormat || 'YYYY/MM/DD'"
@change="changeHandler"
@update:model-value="changeHandler"
></TMagicDatePicker>
</template>

View File

@ -1,6 +1,6 @@
<template>
<TMagicDatePicker
v-model="model[name]"
:model-value="model[name]"
popper-class="magic-datetime-picker-popper"
type="datetime"
:size="size"
@ -9,7 +9,7 @@
:format="config.format || 'YYYY/MM/DD HH:mm:ss'"
:value-format="config.valueFormat || 'YYYY/MM/DD HH:mm:ss'"
:default-time="config.defaultTime"
@change="changeHandler"
@update:model-value="changeHandler"
></TMagicDatePicker>
</template>

View File

@ -1,6 +1,6 @@
<template>
<TMagicDatePicker
v-model="value"
:model-value="value"
type="datetimerange"
range-separator="-"
start-placeholder="开始日期"
@ -12,7 +12,7 @@
:value-format="config.valueFormat || 'YYYY/MM/DD HH:mm:ss'"
:date-format="config.dateFormat || 'YYYY/MM/DD'"
:time-format="config.timeFormat || 'HH:mm:ss'"
@change="changeHandler"
@update:model-value="changeHandler"
></TMagicDatePicker>
</template>

View File

@ -1,7 +1,7 @@
<template>
<TMagicInputNumber
v-if="model"
v-model="model[name]"
:model-value="model[name]"
clearable
controls-position="right"
:size="size"
@ -10,7 +10,7 @@
:step="config.step"
:placeholder="config.placeholder"
:disabled="disabled"
@change="changeHandler"
@update:model-value="changeHandler"
@input="inputHandler"
></TMagicInputNumber>
</template>

View File

@ -1,19 +1,19 @@
<template>
<div class="m-fields-number-range">
<TMagicInput
v-model="model[name][0]"
clearable
:model-value="model[name][0]"
:clearable="config.clearable ?? true"
:size="size"
:disabled="disabled"
@change="minChangeHandler"
@update:model-value="minChangeHandler"
></TMagicInput>
<span class="split-tag">-</span>
<TMagicInput
v-model="model[name][1]"
clearable
:model-value="model[name][1]"
:clearable="config.clearable ?? true"
:size="size"
:disabled="disabled"
@change="maxChangeHandler"
@update:model-value="maxChangeHandler"
></TMagicInput>
</div>
</template>

View File

@ -1,22 +1,18 @@
<template>
<TMagicRadioGroup v-if="model" v-model="model[name]" :size="size" :disabled="disabled">
<TMagicRadioGroup v-if="model" :model-value="model[name]" :size="size" :disabled="disabled">
<component
:is="itemComponent"
v-for="option in config.options"
:is="itemComponent"
:value="option.value"
:key="`${option.value}`"
@click.prevent="clickHandler(option.value)"
@click="clickHandler(option.value)"
>
<TMagicTooltip v-if="option.tooltip" placement="top-start" :content="option.tooltip">
<TMagicTooltip :disabled="!Boolean(option.tooltip)" placement="top-start" :content="option.tooltip">
<div>
<TMagicIcon v-if="option.icon" :size="iconSize"><component :is="option.icon"></component></TMagicIcon>
<span>{{ option.text }}</span>
</div>
</TMagicTooltip>
<div v-else>
<TMagicIcon v-if="option.icon" :size="iconSize"><component :is="option.icon"></component></TMagicIcon>
<span>{{ option.text }}</span>
</div>
</component>
</TMagicRadioGroup>
</template>
@ -39,13 +35,9 @@ const itemComponent = computed(() => (props.config.childType === 'button' ? TMag
const emit = defineEmits(['change']);
const changeHandler = (value: number) => {
emit('change', value);
};
const clickHandler = (item: any) => {
props.model[props.name] = props.model[props.name] === item ? '' : item;
changeHandler(props.model[props.name]);
const clickHandler = (item: string | number | boolean) => {
//
emit('change', props.model[props.name] === item ? '' : item);
};
useAddField(props.prop);

View File

@ -1,7 +1,7 @@
<template>
<TMagicSelect
v-if="model"
v-model="model[name]"
:model-value="model[name]"
v-loading="loading"
class="m-select"
ref="tMagicSelect"
@ -16,7 +16,7 @@
:allow-create="config.allowCreate"
:disabled="disabled"
:remote-method="config.remote && remoteMethod"
@change="changeHandler"
@update:model-value="changeHandler"
@visible-change="visibleHandler"
>
<template v-if="config.group">

View File

@ -1,11 +1,11 @@
<template>
<TMagicSwitch
v-model="model[name]"
:model-value="model[name]"
:size="size"
:activeValue="activeValue"
:inactiveValue="inactiveValue"
:disabled="disabled"
@change="changeHandler"
@update:model-value="changeHandler"
></TMagicSwitch>
</template>

View File

@ -1,16 +1,19 @@
<template>
<div class="m-fields-text">
<TMagicInput
v-model="model[name]"
:model-value="model[name]"
ref="input"
clearable
:size="size"
:placeholder="config.placeholder"
:disabled="disabled"
@change="changeHandler"
@update:model-value="changeHandler"
@input="inputHandler"
@keyup="keyUpHandler($event)"
>
<template #prepend v-if="config.prepend">
<span>{{ config.prepend }}</span>
</template>
<template #append v-if="appendConfig">
<TMagicButton
v-if="appendConfig.type === 'button'"
@ -20,6 +23,7 @@
>
{{ appendConfig.text }}
</TMagicButton>
<span v-else>{{ appendConfig.text }}</span>
</template>
</TMagicInput>
@ -66,18 +70,26 @@ const mForm = inject<FormState | undefined>('mForm');
const appendConfig = computed(() => {
if (typeof props.config.append === 'string') {
return {
type: 'text',
text: props.config.append,
type: 'button',
handler: undefined,
};
}
if (props.config.append && typeof props.config.append === 'object') {
if (props.config.append.value === 0) {
return false;
if (typeof props.config.append === 'object') {
if (typeof props.config.append?.handler === 'function') {
return {
type: 'button',
text: props.config.append.text,
handler: props.config.append.handler,
};
}
if (props.config.append) {
if (props.config.append.value === 0) {
return false;
}
return props.config.append;
return props.config.append;
}
}
return false;

View File

@ -1,12 +1,13 @@
<template>
<TMagicInput
v-model="model[name]"
:model-value="model[name]"
type="textarea"
:size="size"
clearable
:placeholder="config.placeholder"
:disabled="disabled"
@change="changeHandler"
:rows="config.rows"
@update:model-value="changeHandler"
@input="inputHandler"
>
</TMagicInput>

View File

@ -1,12 +1,12 @@
<template>
<TMagicTimePicker
v-model="model[name]"
:model-value="model[name]"
:value-format="config.valueFormat || 'HH:mm:ss'"
:format="config.format || 'HH:mm:ss'"
:size="size"
:placeholder="config.placeholder"
:disabled="disabled"
@change="changeHandler"
@update:model-value="changeHandler"
></TMagicTimePicker>
</template>

View File

@ -1,6 +1,6 @@
<template>
<TMagicTimePicker
v-model="value"
:model-value="value"
is-range
range-separator="-"
start-placeholder="开始时间"
@ -9,7 +9,7 @@
:unlink-panels="true"
:disabled="disabled"
:default-time="config.defaultTime"
@change="changeHandler"
@update:model-value="changeHandler"
></TMagicTimePicker>
</template>

View File

@ -20,11 +20,11 @@ import type { App } from 'vue';
import Container from './containers/Container.vue';
import Fieldset from './containers/Fieldset.vue';
import FlexLayout from './containers/FlexLayout.vue';
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 +46,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';
@ -63,10 +64,11 @@ export { default as MFormDrawer } from './FormDrawer.vue';
export { default as MFormBox } from './FormBox.vue';
export { default as MContainer } from './containers/Container.vue';
export { default as MFieldset } from './containers/Fieldset.vue';
export { default as MFlexLayout } from './containers/FlexLayout.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';
@ -114,6 +116,7 @@ export default {
app.component('m-form-step', MStep);
app.component('m-form-table', Table);
app.component('m-form-tab', Tabs);
app.component('m-form-flex-layout', FlexLayout);
app.component('m-fields-text', Text);
app.component('m-fields-img-upload', Text);
app.component('m-fields-number', Number);

View File

@ -0,0 +1,97 @@
<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;
sortKey?: string;
}>();
const mForm = inject<FormState | undefined>('mForm');
const removeHandler = (index: number) => {
if (props.disabled) return;
emit('change', props.model[props.name].toSpliced(index, 1));
};
const copyHandler = (index: number) => {
const inputs = cloneDeep(props.model[props.name][index]);
const { length } = props.model[props.name];
if (props.sortKey && length) {
inputs[props.sortKey] = props.model[props.name][length - 1][props.sortKey] - 1;
}
emit('change', [...props.model[props.name], inputs], {
changeRecords: [
{
propPath: `${props.prop}.${props.model[props.name].length}`,
value: inputs,
},
],
});
};
//
const showDelete = (index: number) => {
const deleteFunc = props.config.delete;
if (deleteFunc && typeof deleteFunc === 'function') {
return deleteFunc(props.model[props.name], index, mForm?.values);
}
return props.config.delete ?? 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 props.config.copyable ?? 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,194 @@
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 || 112,
align: 'center',
},
cell: ({ row, $index }: any) =>
h(ActionsColumn, {
row,
index: $index,
model: props.model,
config: props.config,
prop: props.prop,
disabled: props.disabled,
sortKey: props.sortKey,
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

@ -3,3 +3,20 @@
display: inline-flex;
}
}
.m-form-container {
&.has-tip {
display: flex;
align-items: baseline;
.tmagic-design-form-item {
flex: 1;
}
}
.tmagic-design-form-item {
&.show-diff {
background: #f7dadd;
}
}
}

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,13 @@
> .t-form__label {
display: none;
}
> .t-form__controls {
margin-left: 0 !important;
}
}
&.t-form:not(.t-form-inline) .t-form__item:last-of-type {
margin-bottom: var(--td-comp-margin-xxl);
}
}

View File

@ -21,6 +21,8 @@ import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';
import { cloneDeep } from 'lodash-es';
import { getValueByKeyPath } from '@tmagic/utils';
import {
ChildConfig,
ContainerCommonConfig,
@ -31,6 +33,7 @@ import {
FormValue,
HtmlField,
Rule,
SortProp,
TabPaneConfig,
TypeFunction,
} from '../schema';
@ -89,8 +92,13 @@ const setValue = (mForm: FormState | undefined, value: FormValue, initValue: For
// 如果fieldset配置checkboxcheckbox的值保存在value中
if (type === 'fieldset' && checkbox) {
if (typeof value[name] === 'object') {
value[name].value = typeof initValue[name] === 'object' ? initValue[name].value || 0 : 0;
const checkboxName = typeof checkbox === 'object' && typeof checkbox.name === 'string' ? checkbox.name : 'value';
const checkboxFalseValue =
typeof checkbox === 'object' && typeof checkbox.falseValue !== 'undefined' ? checkbox.falseValue : 0;
if (name && typeof value[name] === 'object') {
value[name][checkboxName] =
typeof initValue[name] === 'object' ? initValue[name][checkboxName] || checkboxFalseValue : checkboxFalseValue;
}
}
};
@ -136,6 +144,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;
};
@ -197,6 +217,7 @@ export const filterFunction = <T = any>(
prop: props.prop,
config: props.config,
index: props.index,
getFormValue: (prop: string) => getValueByKeyPath(prop, mForm?.values || props.model),
});
}
@ -297,3 +318,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

@ -17,7 +17,7 @@
*/
import { describe, expect, test } from 'vitest';
import { nextTick } from 'vue';
import MagicForm, { MForm } from '@form/index';
import MagicForm, { createForm, MForm } from '@form/index';
import { mount } from '@vue/test-utils';
import ElementPlus from 'element-plus';
@ -45,4 +45,192 @@ describe('表单', () => {
expect(wrapper.text()).toBe('text');
});
test('changeRecords', async () => {
const initValues = {};
const config = [
{
text: 'text',
name: 'text',
},
{
name: 'object',
items: [
{
text: 'text',
name: 'objectText',
},
],
},
{
items: [
{
text: 'text',
name: 'text1',
},
],
},
{
items: [
{
name: 'object',
items: [
{
text: 'text',
name: 'text1',
},
],
},
],
},
];
const wrapper = mount(MForm, {
global: {
plugins: [ElementPlus as any, MagicForm as any],
},
props: {
initValues,
config,
},
});
await nextTick();
const inputs = wrapper.findAll('input');
for (let i = 0; i < inputs.length; i++) {
const input = inputs[i];
input.setValue(i);
}
expect(wrapper.vm.changeRecords).toEqual([
{ propPath: 'text', value: '0' },
{ propPath: 'object.objectText', value: '1' },
{ propPath: 'text1', value: '2' },
{ propPath: 'object.text1', value: '3' },
]);
expect(wrapper.vm.values).toEqual({
text: '0',
object: {
objectText: '1',
text1: '3',
},
text1: '2',
});
});
test('onChange setFormValue', async () => {
const initValues = {};
const config = createForm([
{
text: 'text',
name: 'text',
onChange: (vm, value: string, { formValue, setFormValue }: any) => {
setFormValue('object.objectText', value);
formValue!.object.objectText2 = value;
},
},
{
name: 'object',
items: [
{
text: 'text',
name: 'objectText',
},
{
text: 'text',
name: 'objectText2',
},
],
},
]);
const wrapper = mount(MForm, {
global: {
plugins: [ElementPlus as any, MagicForm as any],
},
props: {
initValues,
config,
},
});
await nextTick();
const input = wrapper.find('input');
input.setValue('a');
await nextTick();
expect(wrapper.vm.changeRecords).toEqual([
{ propPath: 'object.objectText', value: 'a' },
{ propPath: 'object.objectText2', value: 'a' },
{ propPath: 'text', value: 'a' },
]);
expect(wrapper.vm.values).toEqual({
text: 'a',
object: {
objectText: 'a',
objectText2: 'a',
},
});
});
test('onChange setModel', async () => {
const initValues = {};
const config = createForm([
{
name: 'object',
items: [
{
text: 'text',
name: 'objectText',
onChange: (vm: any, value: string, { model, setModel }: any) => {
model.objectText2 = value;
setModel('objectText3', value);
},
},
{
text: 'text',
name: 'objectText2',
},
{
text: 'text',
name: 'objectText3',
},
],
},
]);
const wrapper = mount(MForm, {
global: {
plugins: [ElementPlus as any, MagicForm as any],
},
props: {
initValues,
config,
},
});
await nextTick();
const input = wrapper.find('input');
input.setValue('a');
await nextTick();
expect(wrapper.vm.changeRecords).toEqual([
{ propPath: 'object.objectText2', value: 'a' },
{ propPath: 'object.objectText3', value: 'a' },
{ propPath: 'object.objectText', value: 'a' },
]);
expect(wrapper.vm.values).toEqual({
object: {
objectText: 'a',
objectText2: 'a',
objectText3: 'a',
},
});
});
});

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,5 +1,5 @@
{
"version": "1.6.1",
"version": "1.7.0-beta.1",
"name": "@tmagic/schema",
"type": "module",
"main": "dist/tmagic-schema.umd.cjs",

View File

@ -1,5 +1,5 @@
{
"version": "1.6.1",
"version": "1.7.0-beta.1",
"name": "@tmagic/stage",
"type": "module",
"main": "dist/tmagic-stage.umd.cjs",

View File

@ -1,5 +1,5 @@
{
"version": "1.6.1",
"version": "1.7.0-beta.1",
"name": "@tmagic/table",
"type": "module",
"sideEffects": [

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

@ -1,5 +1,5 @@
{
"version": "1.6.1",
"version": "1.7.0-beta.1",
"name": "@tmagic/tdesign-vue-next-adapter",
"type": "module",
"main": "dist/tmagic-tdesign-vue-next-adapter.umd.cjs",
@ -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 t-icon">
<slot></slot>
</i>
</template>
<script setup lang="ts"></script>
<script setup lang="ts">
defineOptions({
name: 'TTDesignAdapterIcon',
});
</script>

View File

@ -1,45 +1,79 @@
<template>
<TTextarea
v-if="type === 'textarea'"
ref="textarea"
:modelValue="modelValue"
:size="size === 'default' ? 'medium' : size"
:disabled="disabled"
:placeholder="placeholder"
:rows="rows"
@keypress="inputHandler"
@change="changeHandler"
></TTextarea>
<TInput
v-else
:modelValue="modelValue"
:size="size === 'default' ? 'medium' : size"
:clearable="clearable"
:disabled="disabled"
:placeholder="placeholder"
@keypress="inputHandler"
@change="changeHandler"
@blur="blurHandler"
@focus="focusHandler"
@update:modelValue="updateModelValue"
>
<template #prefix-icon v-if="$slots.prefix">
<slot name="prefix"></slot>
></TTextarea>
<TInputAdornment v-else>
<template #prepend v-if="$slots.prepend">
<slot name="prepend"></slot>
</template>
<template #suffix v-if="$slots.suffix">
<slot name="suffix"></slot>
<template #append v-if="$slots.append">
<slot name="append"></slot>
</template>
</TInput>
<TInput
:modelValue="modelValue"
:size="size === 'default' ? 'medium' : size"
:clearable="clearable"
:disabled="disabled"
:placeholder="placeholder"
@keypress="inputHandler"
@change="changeHandler"
@blur="blurHandler"
@focus="focusHandler"
@update:modelValue="updateModelValue"
>
<template #prefix-icon v-if="$slots.prefix">
<slot name="prefix"></slot>
</template>
<template #suffix v-if="$slots.suffix">
<slot name="suffix"></slot>
</template>
</TInput>
</TInputAdornment>
</template>
<script lang="ts" setup>
import { Input as TInput, Textarea as TTextarea } from 'tdesign-vue-next';
import { useTemplateRef, watch } from 'vue';
import { Input as TInput, InputAdornment as TInputAdornment, Textarea as TTextarea } from 'tdesign-vue-next';
import type { InputProps } from '@tmagic/design';
defineProps<
defineOptions({
name: 'TTDesignAdapterInput',
});
const props = defineProps<
InputProps & {
modelValue: string;
}
>();
const emit = defineEmits(['change', 'input', 'update:modelValue']);
const emit = defineEmits(['change', 'input', 'blur', 'focus', 'update:modelValue']);
const textareaRef = useTemplateRef('textarea');
watch(
[textareaRef, () => props.rows],
([val, rows]) => {
if (val && rows) {
const el = val.$el.querySelector('textarea');
if (el) {
el.rows = rows;
}
}
},
{ immediate: true },
);
const changeHandler = (...args: any[]) => {
emit('change', ...args);
@ -49,6 +83,14 @@ const inputHandler = (...args: any[]) => {
emit('input', ...args);
};
const blurHandler = (...args: any[]) => {
emit('blur', ...args);
};
const focusHandler = (...args: any[]) => {
emit('focus', ...args);
};
const updateModelValue = (...args: any[]) => {
emit('update:modelValue', ...args);
};

View File

@ -0,0 +1,29 @@
<template>
<TPopconfirm :content="title" :placement="placement" @confirm="confirmHandler" @cancel="cancelHandler">
<template #default>
<slot name="reference"></slot>
</template>
</TPopconfirm>
</template>
<script setup lang="ts">
import { Popconfirm as TPopconfirm } from 'tdesign-vue-next';
import type { PopconfirmProps } from '@tmagic/design';
defineOptions({
name: 'TTDesignAdapterPopconfirm',
});
defineProps<PopconfirmProps>();
const emit = defineEmits(['confirm', 'cancel']);
const confirmHandler = (...args: any[]) => {
emit('confirm', ...args);
};
const cancelHandler = (...args: any[]) => {
emit('cancel', ...args);
};
</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

@ -0,0 +1,49 @@
<template>
<TTabs
:model-value="modelValue"
:addable="editable"
:theme="type === 'card' ? 'card' : 'normal'"
:placement="tabPosition"
@add="onTabAdd"
@change="tabClickHandler"
@remove="onTabRemove"
@update:model-value="updateModelName"
>
<template #action v-if="$slots['add-icon']">
<slot name="add-icon"></slot>
</template>
<template #default>
<slot></slot>
</template>
</TTabs>
</template>
<script setup lang="ts">
import { Tabs as TTabs } from 'tdesign-vue-next';
import type { TabsProps } from '@tmagic/design';
defineOptions({
name: 'TTDesignAdapterTabs',
});
defineProps<TabsProps>();
const emit = defineEmits(['tab-click', 'tab-add', 'tab-remove', 'update:model-value']);
const tabClickHandler = (...args: any[]) => {
emit('tab-click', ...args);
};
const onTabAdd = (...args: any[]) => {
emit('tab-add', ...args);
};
const onTabRemove = (...args: any[]) => {
emit('tab-remove', ...args);
};
const updateModelName = (...args: any[]) => {
emit('update:model-value', ...args);
};
</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,
@ -19,21 +17,18 @@ import {
Form as TForm,
FormItem as TFormItem,
InputNumber as TInputNumber,
LoadingDirective,
MessagePlugin,
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,
TimePicker as TTimePicker,
Tooltip as TTooltip,
@ -64,6 +59,7 @@ import type {
OptionGroupProps,
OptionProps,
PaginationProps,
PopconfirmProps,
RadioButtonProps,
RadioGroupProps,
RadioProps,
@ -71,7 +67,6 @@ import type {
StepProps,
StepsProps,
SwitchProps,
TableColumnProps,
TableProps,
TabPaneProps,
TabsProps,
@ -81,23 +76,51 @@ 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 Popconfirm from './Popconfirm.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';
import Tabs from './Tabs.vue';
const adapter: any = {
message: MessagePlugin,
messageBox: {
alert: (msg: string) => {
DialogPlugin.alert({
body: msg,
alert: (msg: string, title?: string) => {
return new Promise((resolve, reject) => {
const dia = DialogPlugin.alert({
header: title,
body: msg,
onConfirm: (e) => {
dia.hide();
resolve(e);
},
onClose: (e) => {
dia.hide();
reject(e);
},
});
});
},
confirm: (msg: string) => {
DialogPlugin.confirm({
body: msg,
confirm: (msg: string, title?: string) => {
return new Promise((resolve, reject) => {
const dia = DialogPlugin.confirm({
header: title,
body: msg,
onConfirm: (e) => {
dia.hide();
resolve(e);
},
onClose: (e) => {
dia.hide();
reject(e);
},
});
});
},
close: (msg: string) => {
@ -119,8 +142,8 @@ const adapter: any = {
props: (props: ButtonProps) => ({
theme: props.type,
size: props.size === 'default' ? 'medium' : props.size,
icon: () => (props.icon ? h(props.icon) : null),
variant: props.link || props.text ? 'text' : 'base',
icon: props.icon ? () => h(Icon, null, { default: () => h(props.icon) }) : undefined,
variant: props.link || props.text ? 'text' : props.variant || 'base',
shape: props.circle ? 'circle' : 'rectangle',
}),
},
@ -131,6 +154,8 @@ const adapter: any = {
shadow: props.shadow !== 'never',
hoverShadow: props.shadow === 'hover',
header: props.header,
bodyStyle: props.bodyStyle,
headerBordered: true,
}),
},
@ -153,13 +178,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 +194,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 +232,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 +308,7 @@ const adapter: any = {
labelWidth: props.labelWidth,
name: props.prop,
rules: props.rules,
help: props.extra,
}),
},
@ -347,18 +361,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 +433,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) => ({
@ -442,13 +446,8 @@ const adapter: any = {
},
tabs: {
component: TTabs,
props: (props: TabsProps) => ({
addable: props.editable,
theme: props.type === 'card' ? 'card' : 'normal',
placement: props.tabPosition,
value: props.modelValue,
}),
component: Tabs,
props: (props: TabsProps) => props,
},
tag: {
@ -472,7 +471,7 @@ const adapter: any = {
component: TTooltip,
props: (props: TooltipProps) => ({
...props,
placement: props.placement,
placement: props.placement?.replace(/\B([A-Z])/g, '-$1').toLowerCase(),
content: props.content,
}),
},
@ -485,7 +484,13 @@ const adapter: any = {
autoUpload: props.autoUpload,
}),
},
popconfirm: {
component: Popconfirm,
props: (props: PopconfirmProps) => props,
},
},
loading: LoadingDirective,
};
export default adapter;

View File

@ -1,5 +1,5 @@
{
"version": "1.6.1",
"version": "1.7.0-beta.1",
"name": "@tmagic/utils",
"type": "module",
"main": "dist/tmagic-utils.umd.cjs",

View File

@ -1,6 +1,6 @@
{
"name": "tmagic-playground",
"version": "1.6.1",
"version": "1.7.0-beta.1",
"type": "module",
"private": true,
"scripts": {
@ -12,14 +12,17 @@
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.2",
"@tmagic/core": "1.6.1",
"@tmagic/editor": "1.6.1",
"@tmagic/element-plus-adapter": "1.6.1",
"@tmagic/core": "1.7.0-beta.1",
"@tmagic/design": "1.7.0-beta.1",
"@tmagic/editor": "1.7.0-beta.1",
"@tmagic/element-plus-adapter": "1.7.0-beta.1",
"@tmagic/tdesign-vue-next-adapter": "1.7.0-beta.1",
"@tmagic/tmagic-form-runtime": "1.1.3",
"element-plus": "^2.11.4",
"lodash-es": "^4.17.21",
"monaco-editor": "^0.52.2",
"serialize-javascript": "^6.0.2",
"tdesign-vue-next": "^1.17.1",
"vue": "catalog:",
"vue-router": "^4.5.1"
},

View File

@ -0,0 +1,31 @@
<template>
<TMagicForm size="small" label-position="right" style="margin-left: 10px">
<TMagicFormItem label="UI组件库">
<TMagicSelect v-model="adapter" size="small" @change="adapterChange" style="width: 150px">
<TMagicOption value="element-plus">element-plus</TMagicOption>
<TMagicOption value="tdesign-vue-next">tdesign-vue-next</TMagicOption>
</TMagicSelect>
</TMagicFormItem>
</TMagicForm>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { TMagicForm, TMagicFormItem, TMagicOption, TMagicSelect } from '@tmagic/design';
const adapter = ref(sessionStorage.getItem('tmagic-playground-ui-adapter') || 'element-plus');
const adapterChange = (adapter: string) => {
sessionStorage.setItem('tmagic-playground-ui-adapter', adapter);
globalThis.location.reload();
};
</script>
<style lang="scss">
.m-editor-nav-menu {
.tmagic-design-form-item {
margin-bottom: 0;
}
}
</style>

View File

@ -1,33 +1,22 @@
<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>
<AdapterSelect></AdapterSelect>
<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>
<script lang="ts">
import { defineComponent, PropType } from 'vue';
<script lang="ts" setup>
import { MenuButton, TMagicButton, TMagicIcon } from '@tmagic/editor';
export default defineComponent({
name: 'nav-menu',
props: {
data: {
type: Array as PropType<MenuButton[]>,
default: () => [],
},
},
components: { TMagicIcon, TMagicButton },
});
import AdapterSelect from './AdapterSelect.vue';
defineProps<{
data: MenuButton[];
}>();
</script>
<style lang="scss" scoped>

Some files were not shown because too many files have changed in this diff Show More