Commit 9c2f3f30bbd8abcccc4f256183ed7794da7fcda2
1 parent
f3a70eed
refactor(table): refactor table #150 #148 #146 #130 #76
Showing
35 changed files
with
1099 additions
and
454 deletions
CHANGELOG.zh_CN.md
1 | 1 | ## Wip |
2 | 2 | |
3 | +### ✨ 表格破坏性更新 | |
4 | + | |
5 | +- 重构了可编辑单元格及可编辑行。具体看示例。写法已改变。针对可编辑表格。 | |
6 | + | |
7 | +- 表格编辑支持表单校验 | |
8 | + | |
9 | +- 在表格列配置增加了以下配置 | |
10 | + | |
11 | +```bash | |
12 | +{ | |
13 | + | |
14 | + # 默认是否显示列。不显示的可以在列配置打开 | |
15 | + defaultHidden?: boolean; | |
16 | + # 列头右侧帮助文本 | |
17 | + helpMessage?: string | string[]; | |
18 | + # 自定义格式化 单元格内容。 支持时间/枚举自动转化 | |
19 | + format?: CellFormat; | |
20 | + | |
21 | + # Editable | |
22 | + # 是否是可编辑单元格 | |
23 | + edit?: boolean; | |
24 | + # 是否是可编辑行 | |
25 | + editRow?: boolean; | |
26 | + # 编辑状态。 | |
27 | + editable?: boolean; | |
28 | + # 编辑组件 | |
29 | + editComponent?: ComponentType; | |
30 | + # 所对应组件的参数 | |
31 | + editComponentProps?: Recordable; | |
32 | + # 校验 | |
33 | + editRule?: boolean | ((text: string, record: Recordable) => Promise<string>); | |
34 | + # 值枚举转化 | |
35 | + editValueMap?: (value: any) => string; | |
36 | + # 触发编辑正航 | |
37 | + record.onEditRow?: () => void; | |
38 | +} | |
39 | + | |
40 | +``` | |
41 | + | |
42 | +### ✨ 表格重构 | |
43 | + | |
44 | +- 新增`clickToRowSelect`属性。用于控制点击行是否选中勾选框 | |
45 | +- 监听行点击事件 | |
46 | +- 表格列配置按钮增加 列拖拽,列固定功能。 | |
47 | +- 表格列配置新增`defaultHidden` 属性。用于默认隐藏。可在表格列配置勾选显示 | |
48 | +- 更强大的列配置 | |
49 | +- useTable:支持动态改变参数。可以传入`Ref`类型与`Computed`类型进行动态更改 | |
50 | +- useTable:新增返回 `getForm`函数。可以用于操作表格内的表单 | |
51 | +- 修复表格已知的问题 | |
52 | + | |
3 | 53 | ### ✨ Features |
4 | 54 | |
5 | 55 | - 新增 `v-ripple`水波纹指令 |
... | ... | @@ -12,14 +62,6 @@ |
12 | 62 | - form: 新增远程下拉`ApiSelect`及示例 |
13 | 63 | - form: 新增`autoFocusFirstItem`配置。用于配置是否聚焦表单第一个输入框 |
14 | 64 | - useForm: 支持动态改变参数。可以传入`Ref`类型与`Computed`类型进行动态更改 |
15 | -- table: 新增`clickToRowSelect`属性。用于控制点击行是否选中勾选狂 | |
16 | -- table: 监听行点击事件 | |
17 | -- table: 表格列配置按钮增加 列拖拽,列固定功能。 | |
18 | -- table:表格列配置新增`defaultHidden` 属性。用于默认隐藏。可在表格列配置勾选显示 | |
19 | - | |
20 | -### ✨ Refactor | |
21 | - | |
22 | -- 重构表单,解决已知 bug | |
23 | 65 | |
24 | 66 | ### ⚡ Performance Improvements |
25 | 67 | |
... | ... | @@ -30,6 +72,7 @@ |
30 | 72 | ### 🎫 Chores |
31 | 73 | |
32 | 74 | - 升级`ant-design-vue`到`2.0.0-rc.7` |
75 | +- 升级`vue`到`3.0.5` | |
33 | 76 | |
34 | 77 | ### 🐛 Bug Fixes |
35 | 78 | ... | ... |
mock/demo/table-demo.ts
... | ... | @@ -10,6 +10,14 @@ const demoList = (() => { |
10 | 10 | endTime: '@datetime', |
11 | 11 | address: '@city()', |
12 | 12 | name: '@cname()', |
13 | + name1: '@cname()', | |
14 | + name2: '@cname()', | |
15 | + name3: '@cname()', | |
16 | + name4: '@cname()', | |
17 | + name5: '@cname()', | |
18 | + name6: '@cname()', | |
19 | + name7: '@cname()', | |
20 | + name8: '@cname()', | |
13 | 21 | 'no|100000-10000000': 100000, |
14 | 22 | 'status|1': ['normal', 'enable', 'disable'], |
15 | 23 | }); | ... | ... |
src/components/Form/index.ts
... | ... | @@ -9,4 +9,7 @@ export * from './src/types/formItem'; |
9 | 9 | export { useComponentRegister } from './src/hooks/useComponentRegister'; |
10 | 10 | export { useForm } from './src/hooks/useForm'; |
11 | 11 | |
12 | +export { default as ApiSelect } from './src/components/ApiSelect.vue'; | |
13 | +export { default as RadioButtonGroup } from './src/components/RadioButtonGroup.vue'; | |
14 | + | |
12 | 15 | export { BasicForm }; | ... | ... |
src/components/Form/src/components/ApiSelect.vue
... | ... | @@ -50,7 +50,8 @@ |
50 | 50 | labelField: propTypes.string.def('label'), |
51 | 51 | valueField: propTypes.string.def('value'), |
52 | 52 | }, |
53 | - setup(props) { | |
53 | + emits: ['options-change', 'change'], | |
54 | + setup(props, { emit }) { | |
54 | 55 | const options = ref<OptionsItem[]>([]); |
55 | 56 | const loading = ref(false); |
56 | 57 | const attrs = useAttrs(); |
... | ... | @@ -86,11 +87,13 @@ |
86 | 87 | const res = await api(props.params); |
87 | 88 | if (Array.isArray(res)) { |
88 | 89 | options.value = res; |
90 | + emit('options-change', unref(options)); | |
89 | 91 | return; |
90 | 92 | } |
91 | 93 | if (props.resultField) { |
92 | 94 | options.value = get(res, props.resultField) || []; |
93 | 95 | } |
96 | + emit('options-change', unref(options)); | |
94 | 97 | } catch (error) { |
95 | 98 | console.warn(error); |
96 | 99 | } finally { | ... | ... |
src/components/Menu/src/useOpenKeys.ts
... | ... | @@ -15,7 +15,7 @@ export function useOpenKeys( |
15 | 15 | mode: Ref<MenuModeEnum>, |
16 | 16 | accordion: Ref<boolean> |
17 | 17 | ) { |
18 | - const { getCollapsed } = useMenuSetting(); | |
18 | + const { getCollapsed, getIsMixSidebar } = useMenuSetting(); | |
19 | 19 | |
20 | 20 | function setOpenKeys(path: string) { |
21 | 21 | if (mode.value === MenuModeEnum.HORIZONTAL) { |
... | ... | @@ -30,7 +30,9 @@ export function useOpenKeys( |
30 | 30 | } |
31 | 31 | |
32 | 32 | const getOpenKeys = computed(() => { |
33 | - return unref(getCollapsed) ? menuState.collapsedOpenKeys : menuState.openKeys; | |
33 | + const collapse = unref(getIsMixSidebar) ? false : unref(getCollapsed); | |
34 | + | |
35 | + return collapse ? menuState.collapsedOpenKeys : menuState.openKeys; | |
34 | 36 | }); |
35 | 37 | |
36 | 38 | /** |
... | ... | @@ -42,7 +44,7 @@ export function useOpenKeys( |
42 | 44 | } |
43 | 45 | |
44 | 46 | function handleOpenChange(openKeys: string[]) { |
45 | - if (unref(mode) === MenuModeEnum.HORIZONTAL || !unref(accordion)) { | |
47 | + if (unref(mode) === MenuModeEnum.HORIZONTAL || !unref(accordion) || unref(getIsMixSidebar)) { | |
46 | 48 | menuState.openKeys = openKeys; |
47 | 49 | } else { |
48 | 50 | // const menuList = toRaw(menus.value); | ... | ... |
src/components/Table/index.ts
... | ... | @@ -3,7 +3,6 @@ import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent'; |
3 | 3 | export { default as BasicTable } from './src/BasicTable.vue'; |
4 | 4 | export { default as TableAction } from './src/components/TableAction.vue'; |
5 | 5 | // export { default as TableImg } from './src/components/TableImg.vue'; |
6 | -export { renderEditableCell, renderEditableRow } from './src/components/renderEditable'; | |
7 | 6 | export { default as EditTableHeaderIcon } from './src/components/EditTableHeaderIcon.vue'; |
8 | 7 | |
9 | 8 | export const TableImg = createAsyncComponent(() => import('./src/components/TableImg.vue')); |
... | ... | @@ -17,4 +16,4 @@ export { useTable } from './src/hooks/useTable'; |
17 | 16 | |
18 | 17 | export type { FormSchema, FormProps } from '/@/components/Form/src/types/form'; |
19 | 18 | |
20 | -export type { EditRecordRow } from './src/components/renderEditable'; | |
19 | +export type { EditRecordRow } from './src/components/editable'; | ... | ... |
src/components/Table/src/BasicTable.vue
... | ... | @@ -34,19 +34,19 @@ |
34 | 34 | <template #[item]="data" v-for="item in Object.keys($slots)"> |
35 | 35 | <slot :name="item" v-bind="data" /> |
36 | 36 | </template> |
37 | + <template #[`header-${column.dataIndex}`] v-for="column in columns" :key="column.dataIndex"> | |
38 | + <HeaderCell :column="column" /> | |
39 | + </template> | |
37 | 40 | </Table> |
38 | 41 | </div> |
39 | 42 | </template> |
40 | 43 | <script lang="ts"> |
41 | - import type { BasicTableProps, TableActionType, SizeType, SorterResult } from './types/table'; | |
42 | - import { PaginationProps } from './types/pagination'; | |
44 | + import type { BasicTableProps, TableActionType, SizeType } from './types/table'; | |
43 | 45 | |
44 | 46 | import { defineComponent, ref, computed, unref } from 'vue'; |
45 | 47 | import { Table } from 'ant-design-vue'; |
46 | 48 | import { BasicForm, useForm } from '/@/components/Form/index'; |
47 | 49 | |
48 | - import { isFunction } from '/@/utils/is'; | |
49 | - | |
50 | 50 | import { omit } from 'lodash-es'; |
51 | 51 | |
52 | 52 | import { usePagination } from './hooks/usePagination'; |
... | ... | @@ -61,15 +61,20 @@ |
61 | 61 | import { createTableContext } from './hooks/useTableContext'; |
62 | 62 | import { useTableFooter } from './hooks/useTableFooter'; |
63 | 63 | import { useTableForm } from './hooks/useTableForm'; |
64 | + import { useExpose } from '/@/hooks/core/useExpose'; | |
65 | + import { useDesign } from '/@/hooks/web/useDesign'; | |
64 | 66 | |
65 | 67 | import { basicProps } from './props'; |
66 | - import { useExpose } from '/@/hooks/core/useExpose'; | |
68 | + import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent'; | |
67 | 69 | |
68 | 70 | import './style/index.less'; |
69 | - import { useDesign } from '/@/hooks/web/useDesign'; | |
70 | 71 | export default defineComponent({ |
71 | 72 | props: basicProps, |
72 | - components: { Table, BasicForm }, | |
73 | + components: { | |
74 | + Table, | |
75 | + BasicForm, | |
76 | + HeaderCell: createAsyncComponent(() => import('./components/HeaderCell.vue')), | |
77 | + }, | |
73 | 78 | emits: [ |
74 | 79 | 'fetch-success', |
75 | 80 | 'fetch-error', |
... | ... | @@ -80,6 +85,8 @@ |
80 | 85 | 'row-contextmenu', |
81 | 86 | 'row-mouseenter', |
82 | 87 | 'row-mouseleave', |
88 | + 'edit-end', | |
89 | + 'edit-cancel', | |
83 | 90 | ], |
84 | 91 | setup(props, { attrs, emit, slots }) { |
85 | 92 | const tableElRef = ref<ComponentRef>(null); |
... | ... | @@ -96,15 +103,19 @@ |
96 | 103 | |
97 | 104 | const { getLoading, setLoading } = useLoading(getProps); |
98 | 105 | const { getPaginationInfo, getPagination, setPagination } = usePagination(getProps); |
106 | + | |
99 | 107 | const { |
100 | - getSortFixedColumns, | |
101 | - getColumns, | |
102 | - setColumns, | |
103 | - getColumnsRef, | |
104 | - getCacheColumns, | |
105 | - } = useColumns(getProps, getPaginationInfo); | |
108 | + getRowSelection, | |
109 | + getRowSelectionRef, | |
110 | + getSelectRows, | |
111 | + clearSelectedRowKeys, | |
112 | + getSelectRowKeys, | |
113 | + deleteSelectRowByKey, | |
114 | + setSelectedRowKeys, | |
115 | + } = useRowSelection(getProps, emit); | |
106 | 116 | |
107 | 117 | const { |
118 | + handleTableChange, | |
108 | 119 | getDataSourceRef, |
109 | 120 | getDataSource, |
110 | 121 | setTableData, |
... | ... | @@ -112,6 +123,7 @@ |
112 | 123 | getRowKey, |
113 | 124 | reload, |
114 | 125 | getAutoCreateKey, |
126 | + updateTableData, | |
115 | 127 | } = useDataSource( |
116 | 128 | getProps, |
117 | 129 | { |
... | ... | @@ -119,19 +131,15 @@ |
119 | 131 | setLoading, |
120 | 132 | setPagination, |
121 | 133 | getFieldsValue: formActions.getFieldsValue, |
134 | + clearSelectedRowKeys, | |
122 | 135 | }, |
123 | 136 | emit |
124 | 137 | ); |
125 | 138 | |
126 | - const { | |
127 | - getRowSelection, | |
128 | - getRowSelectionRef, | |
129 | - getSelectRows, | |
130 | - clearSelectedRowKeys, | |
131 | - getSelectRowKeys, | |
132 | - deleteSelectRowByKey, | |
133 | - setSelectedRowKeys, | |
134 | - } = useRowSelection(getProps, emit); | |
139 | + const { getViewColumns, getColumns, setColumns, getColumnsRef, getCacheColumns } = useColumns( | |
140 | + getProps, | |
141 | + getPaginationInfo | |
142 | + ); | |
135 | 143 | |
136 | 144 | const { getScrollRef, redoHeight } = useTableScroll( |
137 | 145 | getProps, |
... | ... | @@ -178,7 +186,7 @@ |
178 | 186 | tableLayout: 'fixed', |
179 | 187 | rowSelection: unref(getRowSelectionRef), |
180 | 188 | rowKey: unref(getRowKey), |
181 | - columns: unref(getSortFixedColumns), | |
189 | + columns: unref(getViewColumns), | |
182 | 190 | pagination: unref(getPaginationInfo), |
183 | 191 | dataSource: unref(getDataSourceRef), |
184 | 192 | footer: unref(getFooterProps), |
... | ... | @@ -197,26 +205,6 @@ |
197 | 205 | return !!unref(getDataSourceRef).length; |
198 | 206 | }); |
199 | 207 | |
200 | - function handleTableChange( | |
201 | - pagination: PaginationProps, | |
202 | - // @ts-ignore | |
203 | - filters: Partial<Recordable<string[]>>, | |
204 | - sorter: SorterResult | |
205 | - ) { | |
206 | - const { clearSelectOnPageChange, sortFn } = unref(getProps); | |
207 | - if (clearSelectOnPageChange) { | |
208 | - clearSelectedRowKeys(); | |
209 | - } | |
210 | - setPagination(pagination); | |
211 | - | |
212 | - if (sorter && isFunction(sortFn)) { | |
213 | - const sortInfo = sortFn(sorter); | |
214 | - fetch({ sortInfo }); | |
215 | - return; | |
216 | - } | |
217 | - fetch(); | |
218 | - } | |
219 | - | |
220 | 208 | function setProps(props: Partial<BasicTableProps>) { |
221 | 209 | innerPropsRef.value = { ...unref(innerPropsRef), ...props }; |
222 | 210 | } |
... | ... | @@ -239,6 +227,8 @@ |
239 | 227 | getPaginationRef: getPagination, |
240 | 228 | getColumns, |
241 | 229 | getCacheColumns, |
230 | + emit, | |
231 | + updateTableData, | |
242 | 232 | getSize: () => { |
243 | 233 | return unref(getBindValues).size as SizeType; |
244 | 234 | }, |
... | ... | @@ -265,6 +255,7 @@ |
265 | 255 | replaceFormSlotKey, |
266 | 256 | getFormSlotKeys, |
267 | 257 | prefixCls, |
258 | + columns: getViewColumns, | |
268 | 259 | }; |
269 | 260 | }, |
270 | 261 | }); | ... | ... |
src/components/Table/src/componentMap.ts
1 | -import { Component } from 'vue'; | |
1 | +import type { Component } from 'vue'; | |
2 | 2 | |
3 | 3 | import { Input, Select, Checkbox, InputNumber, Switch } from 'ant-design-vue'; |
4 | 4 | |
5 | -import { ComponentType } from './types/componentType'; | |
5 | +import type { ComponentType } from './types/componentType'; | |
6 | +import { ApiSelect } from '/@/components/Form'; | |
6 | 7 | |
7 | 8 | const componentMap = new Map<ComponentType, Component>(); |
8 | 9 | |
9 | 10 | componentMap.set('Input', Input); |
10 | -componentMap.set('InputPassword', Input.Password); | |
11 | 11 | componentMap.set('InputNumber', InputNumber); |
12 | 12 | |
13 | 13 | componentMap.set('Select', Select); |
14 | +componentMap.set('ApiSelect', ApiSelect); | |
14 | 15 | componentMap.set('Switch', Switch); |
15 | 16 | componentMap.set('Checkbox', Checkbox); |
16 | -componentMap.set('CheckboxGroup', Checkbox.Group); | |
17 | 17 | |
18 | 18 | export function add(compName: ComponentType, component: Component) { |
19 | 19 | componentMap.set(compName, component); | ... | ... |
src/components/Table/src/components/EditTableHeaderIcon.vue
src/components/Table/src/components/HeaderCell.vue
0 → 100644
1 | +<template> | |
2 | + <EditTableHeaderCell v-if="getIsEdit"> | |
3 | + {{ getTitle }} | |
4 | + </EditTableHeaderCell> | |
5 | + <span v-else>{{ getTitle }}</span> | |
6 | + <BasicHelp v-if="getHelpMessage" :text="getHelpMessage" :class="`${prefixCls}__help`" /> | |
7 | +</template> | |
8 | +<script lang="ts"> | |
9 | + import type { PropType } from 'vue'; | |
10 | + import type { BasicColumn } from '../types/table'; | |
11 | + | |
12 | + import { defineComponent, computed } from 'vue'; | |
13 | + | |
14 | + import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent'; | |
15 | + import { useDesign } from '/@/hooks/web/useDesign'; | |
16 | + export default defineComponent({ | |
17 | + name: 'TableHeaderCell', | |
18 | + components: { | |
19 | + EditTableHeaderCell: createAsyncComponent(() => import('./EditTableHeaderIcon.vue')), | |
20 | + BasicHelp: createAsyncComponent(() => import('/@/components/Basic/src/BasicHelp.vue')), | |
21 | + }, | |
22 | + props: { | |
23 | + column: { | |
24 | + type: Object as PropType<BasicColumn>, | |
25 | + default: {}, | |
26 | + }, | |
27 | + }, | |
28 | + setup(props) { | |
29 | + const { prefixCls } = useDesign('basic-table-header-cell'); | |
30 | + const getIsEdit = computed(() => { | |
31 | + return !!props.column?.edit; | |
32 | + }); | |
33 | + | |
34 | + const getTitle = computed(() => { | |
35 | + return props.column?.customTitle; | |
36 | + }); | |
37 | + | |
38 | + const getHelpMessage = computed(() => { | |
39 | + return props.column?.helpMessage; | |
40 | + }); | |
41 | + | |
42 | + return { prefixCls, getIsEdit, getTitle, getHelpMessage }; | |
43 | + }, | |
44 | + }); | |
45 | +</script> | |
46 | +<style lang="less"> | |
47 | + @prefix-cls: ~'@{namespace}-basic-table-header-cell'; | |
48 | + | |
49 | + .@{prefix-cls} { | |
50 | + &__help { | |
51 | + margin-left: 8px; | |
52 | + color: rgba(0, 0, 0, 0.65) !important; | |
53 | + } | |
54 | + } | |
55 | +</style> | ... | ... |
src/components/Table/src/components/TableAction.vue
1 | 1 | <template> |
2 | 2 | <div :class="[prefixCls, getAlign]"> |
3 | - <template v-for="(action, index) in getActions" :key="`${index}`"> | |
3 | + <template v-for="(action, index) in getActions" :key="`${index}-${action.label}`"> | |
4 | 4 | <PopConfirmButton v-bind="action"> |
5 | 5 | <Icon :icon="action.icon" class="mr-1" v-if="action.icon" /> |
6 | 6 | {{ action.label }} |
7 | 7 | </PopConfirmButton> |
8 | 8 | <Divider type="vertical" v-if="divider && index < getActions.length" /> |
9 | 9 | </template> |
10 | - | |
11 | - <Dropdown :trigger="['hover']" :dropMenuList="getDropList"> | |
10 | + <Dropdown :trigger="['hover']" :dropMenuList="getDropList" v-if="dropDownActions"> | |
12 | 11 | <slot name="more" /> |
13 | 12 | <a-button type="link" size="small" v-if="!$slots.more"> |
14 | 13 | <MoreOutlined class="icon-more" /> |
... | ... | @@ -61,7 +60,7 @@ |
61 | 60 | }); |
62 | 61 | |
63 | 62 | const getDropList = computed(() => { |
64 | - return props.dropDownActions.map((action, index) => { | |
63 | + return (props.dropDownActions || []).map((action, index) => { | |
65 | 64 | const { label } = action; |
66 | 65 | return { |
67 | 66 | ...action, | ... | ... |
src/components/Table/src/components/editable/CellComponent.ts
0 → 100644
1 | +import type { FunctionalComponent, defineComponent } from 'vue'; | |
2 | +import type { ComponentType } from '../../types/componentType'; | |
3 | +import { componentMap } from '/@/components/Table/src/componentMap'; | |
4 | + | |
5 | +import { Popover } from 'ant-design-vue'; | |
6 | +import { h } from 'vue'; | |
7 | + | |
8 | +export interface ComponentProps { | |
9 | + component: ComponentType; | |
10 | + rule: boolean; | |
11 | + popoverVisible: boolean; | |
12 | + ruleMessage: string; | |
13 | +} | |
14 | + | |
15 | +export const CellComponent: FunctionalComponent = ( | |
16 | + { component = 'Input', rule = true, ruleMessage, popoverVisible }: ComponentProps, | |
17 | + { attrs } | |
18 | +) => { | |
19 | + const Comp = componentMap.get(component) as typeof defineComponent; | |
20 | + | |
21 | + const DefaultComp = h(Comp, attrs); | |
22 | + if (!rule) { | |
23 | + return DefaultComp; | |
24 | + } | |
25 | + return h( | |
26 | + Popover, | |
27 | + { overlayClassName: 'edit-cell-rule-popover', visible: !!popoverVisible }, | |
28 | + { | |
29 | + default: () => DefaultComp, | |
30 | + content: () => ruleMessage, | |
31 | + } | |
32 | + ); | |
33 | +}; | ... | ... |
src/components/Table/src/components/editable/EditableCell.vue
0 → 100644
1 | +<template> | |
2 | + <div :class="prefixCls"> | |
3 | + <div v-show="!isEdit" :class="`${prefixCls}__normal`" @click="handleEdit"> | |
4 | + {{ value || ' ' }} | |
5 | + <FormOutlined :class="`${prefixCls}__normal-icon`" v-if="!column.editRow" /> | |
6 | + </div> | |
7 | + | |
8 | + <div v-if="isEdit" :class="`${prefixCls}__wrapper`" v-click-outside="onClickOutside"> | |
9 | + <CellComponent | |
10 | + v-bind="getComponentProps" | |
11 | + :component="getComponent" | |
12 | + :style="getWrapperStyle" | |
13 | + :popoverVisible="getRuleVisible" | |
14 | + :rule="getRule" | |
15 | + :ruleMessage="ruleMessage" | |
16 | + size="small" | |
17 | + ref="elRef" | |
18 | + @change="handleChange" | |
19 | + @options-change="handleOptionsChange" | |
20 | + @pressEnter="handleSubmit" | |
21 | + > | |
22 | + </CellComponent> | |
23 | + <div :class="`${prefixCls}__action`" v-if="!getRowEditable"> | |
24 | + <CheckOutlined :class="[`${prefixCls}__icon`, 'mx-2']" @click="handleSubmit" /> | |
25 | + <CloseOutlined :class="`${prefixCls}__icon `" @click="handleCancel" /> | |
26 | + </div> | |
27 | + </div> | |
28 | + </div> | |
29 | +</template> | |
30 | +<script lang="ts"> | |
31 | + import type { CSSProperties, PropType } from 'vue'; | |
32 | + import type { BasicColumn } from '../../types/table'; | |
33 | + | |
34 | + import { defineComponent, ref, unref, nextTick, computed, watchEffect, toRaw } from 'vue'; | |
35 | + import { FormOutlined, CloseOutlined, CheckOutlined } from '@ant-design/icons-vue'; | |
36 | + | |
37 | + import { useDesign } from '/@/hooks/web/useDesign'; | |
38 | + import { isString, isBoolean, isFunction, isNumber, isArray } from '/@/utils/is'; | |
39 | + import clickOutside from '/@/directives/clickOutside'; | |
40 | + | |
41 | + import { CellComponent } from './CellComponent'; | |
42 | + import { useTableContext } from '../../hooks/useTableContext'; | |
43 | + import { propTypes } from '/@/utils/propTypes'; | |
44 | + import { createPlaceholderMessage } from './helper'; | |
45 | + | |
46 | + import type { EditRecordRow } from './index'; | |
47 | + | |
48 | + export default defineComponent({ | |
49 | + name: 'EditableCell', | |
50 | + components: { FormOutlined, CloseOutlined, CheckOutlined, CellComponent }, | |
51 | + props: { | |
52 | + value: { | |
53 | + type: [String, Number, Boolean, Object] as PropType<string | number | boolean | Recordable>, | |
54 | + default: '', | |
55 | + }, | |
56 | + record: { | |
57 | + type: Object as PropType<EditRecordRow>, | |
58 | + }, | |
59 | + column: { | |
60 | + type: Object as PropType<BasicColumn>, | |
61 | + default: {}, | |
62 | + }, | |
63 | + index: propTypes.number, | |
64 | + }, | |
65 | + directives: { | |
66 | + clickOutside, | |
67 | + }, | |
68 | + | |
69 | + setup(props) { | |
70 | + const table = useTableContext(); | |
71 | + const isEdit = ref(false); | |
72 | + const elRef = ref<any>(null); | |
73 | + const ruleVisible = ref(false); | |
74 | + const ruleMessage = ref(''); | |
75 | + const optionsRef = ref<LabelValueOptions>([]); | |
76 | + const currentValueRef = ref<any>(props.value); | |
77 | + const defaultValueRef = ref<any>(props.value); | |
78 | + | |
79 | + const { prefixCls } = useDesign('editable-cell'); | |
80 | + | |
81 | + const getComponent = computed(() => props.column?.editComponent || 'Input'); | |
82 | + const getRule = computed(() => props.column?.editRule); | |
83 | + | |
84 | + const getRuleVisible = computed(() => { | |
85 | + return unref(ruleMessage) && unref(ruleVisible); | |
86 | + }); | |
87 | + | |
88 | + const getIsCheckComp = computed(() => { | |
89 | + const component = unref(getComponent); | |
90 | + return ['Checkbox', 'Switch'].includes(component); | |
91 | + }); | |
92 | + | |
93 | + const getComponentProps = computed(() => { | |
94 | + const compProps = props.column?.editComponentProps ?? {}; | |
95 | + const component = unref(getComponent); | |
96 | + const apiSelectProps: Recordable = {}; | |
97 | + if (component === 'ApiSelect') { | |
98 | + apiSelectProps.cache = true; | |
99 | + } | |
100 | + | |
101 | + const isCheckValue = unref(getIsCheckComp); | |
102 | + | |
103 | + const valueField = isCheckValue ? 'checked' : 'value'; | |
104 | + const val = unref(currentValueRef); | |
105 | + | |
106 | + const value = isCheckValue ? (isNumber(val) && isBoolean(val) ? val : !!val) : val; | |
107 | + | |
108 | + return { | |
109 | + placeholder: createPlaceholderMessage(unref(getComponent)), | |
110 | + ...apiSelectProps, | |
111 | + ...compProps, | |
112 | + [valueField]: value, | |
113 | + }; | |
114 | + }); | |
115 | + | |
116 | + const getValues = computed(() => { | |
117 | + const { editComponentProps, editValueMap } = props.column; | |
118 | + | |
119 | + const value = unref(currentValueRef); | |
120 | + | |
121 | + if (editValueMap && isFunction(editValueMap)) { | |
122 | + return editValueMap(value); | |
123 | + } | |
124 | + | |
125 | + const component = unref(getComponent); | |
126 | + if (!component.includes('Select')) { | |
127 | + return value; | |
128 | + } | |
129 | + const options: LabelValueOptions = editComponentProps?.options ?? (unref(optionsRef) || []); | |
130 | + const option = options.find((item) => `${item.value}` === `${value}`); | |
131 | + return option?.label; | |
132 | + }); | |
133 | + | |
134 | + const getWrapperStyle = computed( | |
135 | + (): CSSProperties => { | |
136 | + if (unref(getIsCheckComp) || unref(getRowEditable)) { | |
137 | + return {}; | |
138 | + } | |
139 | + return { | |
140 | + width: 'calc(100% - 48px)', | |
141 | + }; | |
142 | + } | |
143 | + ); | |
144 | + | |
145 | + const getRowEditable = computed(() => { | |
146 | + const { editable } = props.record || {}; | |
147 | + return !!editable; | |
148 | + }); | |
149 | + | |
150 | + watchEffect(() => { | |
151 | + defaultValueRef.value = props.value; | |
152 | + }); | |
153 | + | |
154 | + watchEffect(() => { | |
155 | + const { editable } = props.column; | |
156 | + if (isBoolean(editable) || isBoolean(unref(getRowEditable))) { | |
157 | + isEdit.value = !!editable || unref(getRowEditable); | |
158 | + } | |
159 | + }); | |
160 | + | |
161 | + function handleEdit() { | |
162 | + if (unref(getRowEditable) || unref(props.column?.editRow)) return; | |
163 | + ruleMessage.value = ''; | |
164 | + isEdit.value = true; | |
165 | + nextTick(() => { | |
166 | + const el = unref(elRef); | |
167 | + el?.focus?.(); | |
168 | + }); | |
169 | + } | |
170 | + | |
171 | + async function handleChange(e: any) { | |
172 | + const component = unref(getComponent); | |
173 | + if (e?.target && Reflect.has(e.target, 'value')) { | |
174 | + currentValueRef.value = (e as ChangeEvent).target.value; | |
175 | + } | |
176 | + if (component === 'Checkbox') { | |
177 | + currentValueRef.value = (e as ChangeEvent).target.checked; | |
178 | + } else if (isString(e) || isBoolean(e) || isNumber(e)) { | |
179 | + currentValueRef.value = e; | |
180 | + } | |
181 | + handleSubmiRule(); | |
182 | + } | |
183 | + | |
184 | + async function handleSubmiRule() { | |
185 | + const { column, record } = props; | |
186 | + const { editRule } = column; | |
187 | + const currentValue = unref(currentValueRef); | |
188 | + | |
189 | + if (editRule) { | |
190 | + if (isBoolean(editRule) && !currentValue && !isNumber(currentValue)) { | |
191 | + ruleVisible.value = true; | |
192 | + const component = unref(getComponent); | |
193 | + const message = createPlaceholderMessage(component); | |
194 | + ruleMessage.value = message; | |
195 | + return false; | |
196 | + } | |
197 | + if (isFunction(editRule)) { | |
198 | + const res = await editRule(currentValue, record as Recordable); | |
199 | + if (!!res) { | |
200 | + ruleMessage.value = res; | |
201 | + ruleVisible.value = true; | |
202 | + return false; | |
203 | + } else { | |
204 | + ruleMessage.value = ''; | |
205 | + return true; | |
206 | + } | |
207 | + } | |
208 | + } | |
209 | + ruleMessage.value = ''; | |
210 | + return true; | |
211 | + } | |
212 | + | |
213 | + async function handleSubmit() { | |
214 | + const isPass = await handleSubmiRule(); | |
215 | + if (!isPass) return false; | |
216 | + const { column, index } = props; | |
217 | + const { key, dataIndex } = column; | |
218 | + // const value = unref(currentValueRef); | |
219 | + if (!key || !dataIndex) return; | |
220 | + const dataKey = (dataIndex || key) as string; | |
221 | + | |
222 | + const record = await table.updateTableData(index, dataKey, unref(getValues)); | |
223 | + table.emit?.('edit-end', { record, index, key, value: unref(currentValueRef) }); | |
224 | + isEdit.value = false; | |
225 | + } | |
226 | + | |
227 | + function handleCancel() { | |
228 | + isEdit.value = false; | |
229 | + currentValueRef.value = defaultValueRef.value; | |
230 | + table.emit?.('edit-cancel', unref(currentValueRef)); | |
231 | + } | |
232 | + | |
233 | + function onClickOutside() { | |
234 | + if (props.column?.editable || unref(getRowEditable)) { | |
235 | + return; | |
236 | + } | |
237 | + const component = unref(getComponent); | |
238 | + | |
239 | + if (component.includes('Input')) { | |
240 | + handleCancel(); | |
241 | + } | |
242 | + } | |
243 | + | |
244 | + // only ApiSelect | |
245 | + function handleOptionsChange(options: LabelValueOptions) { | |
246 | + optionsRef.value = options; | |
247 | + } | |
248 | + | |
249 | + function initCbs(cbs: 'submitCbs' | 'validCbs' | 'cancelCbs', handle: Fn) { | |
250 | + if (props.record) { | |
251 | + /* eslint-disable */ | |
252 | + isArray(props.record[cbs]) | |
253 | + ? props.record[cbs].push(handle) | |
254 | + : (props.record[cbs] = [handle]); | |
255 | + } | |
256 | + } | |
257 | + | |
258 | + if (props.record) { | |
259 | + initCbs('submitCbs', handleSubmit); | |
260 | + initCbs('validCbs', handleSubmiRule); | |
261 | + initCbs('cancelCbs', handleCancel); | |
262 | + | |
263 | + /* eslint-disable */ | |
264 | + props.record.onCancelEdit = () => { | |
265 | + isArray(props.record?.cancelCbs) && props.record?.cancelCbs.forEach((fn) => fn()); | |
266 | + }; | |
267 | + /* eslint-disable */ | |
268 | + props.record.onSubmitEdit = async () => { | |
269 | + if (isArray(props.record?.submitCbs)) { | |
270 | + const validFns = props.record?.validCbs || []; | |
271 | + | |
272 | + const res = await Promise.all(validFns.map((fn) => fn())); | |
273 | + const pass = res.every((item) => !!item); | |
274 | + | |
275 | + if (!pass) return; | |
276 | + const submitFns = props.record?.submitCbs || []; | |
277 | + submitFns.forEach((fn) => fn()); | |
278 | + return true; | |
279 | + } | |
280 | + // isArray(props.record?.submitCbs) && props.record?.submitCbs.forEach((fn) => fn()); | |
281 | + }; | |
282 | + } | |
283 | + | |
284 | + return { | |
285 | + isEdit, | |
286 | + prefixCls, | |
287 | + handleEdit, | |
288 | + currentValueRef, | |
289 | + handleSubmit, | |
290 | + handleChange, | |
291 | + handleCancel, | |
292 | + elRef, | |
293 | + getComponent, | |
294 | + getRule, | |
295 | + onClickOutside, | |
296 | + ruleMessage, | |
297 | + getRuleVisible, | |
298 | + getComponentProps, | |
299 | + handleOptionsChange, | |
300 | + getWrapperStyle, | |
301 | + getRowEditable, | |
302 | + }; | |
303 | + }, | |
304 | + }); | |
305 | +</script> | |
306 | +<style lang="less"> | |
307 | + @prefix-cls: ~'@{namespace}-editable-cell'; | |
308 | + | |
309 | + .edit-cell-rule-popover { | |
310 | + // .ant-popover-arrow { | |
311 | + // // border-color: transparent @error-color @error-color transparent !important; | |
312 | + // } | |
313 | + | |
314 | + .ant-popover-inner-content { | |
315 | + padding: 4px 8px; | |
316 | + color: @error-color; | |
317 | + // border: 1px solid @error-color; | |
318 | + border-radius: 2px; | |
319 | + } | |
320 | + } | |
321 | + .@{prefix-cls} { | |
322 | + position: relative; | |
323 | + | |
324 | + &__wrapper { | |
325 | + display: flex; | |
326 | + align-items: center; | |
327 | + justify-content: center; | |
328 | + } | |
329 | + | |
330 | + &__icon { | |
331 | + &:hover { | |
332 | + transform: scale(1.2); | |
333 | + | |
334 | + svg { | |
335 | + color: @primary-color; | |
336 | + } | |
337 | + } | |
338 | + } | |
339 | + | |
340 | + &__normal { | |
341 | + padding-right: 48px; | |
342 | + | |
343 | + &-icon { | |
344 | + position: absolute; | |
345 | + top: 4px; | |
346 | + right: 0; | |
347 | + display: none; | |
348 | + width: 20px; | |
349 | + cursor: pointer; | |
350 | + } | |
351 | + } | |
352 | + | |
353 | + &:hover { | |
354 | + .@{prefix-cls}__normal-icon { | |
355 | + display: inline-block; | |
356 | + } | |
357 | + } | |
358 | + } | |
359 | +</style> | ... | ... |
src/components/Table/src/components/editable/helper.ts
0 → 100644
1 | +import { ComponentType } from '../../types/componentType'; | |
2 | +import { useI18n } from '/@/hooks/web/useI18n'; | |
3 | + | |
4 | +const { t } = useI18n(); | |
5 | + | |
6 | +/** | |
7 | + * @description: 生成placeholder | |
8 | + */ | |
9 | +export function createPlaceholderMessage(component: ComponentType) { | |
10 | + if (component.includes('Input')) { | |
11 | + return t('component.form.input'); | |
12 | + } | |
13 | + if (component.includes('Picker')) { | |
14 | + return t('component.form.choose'); | |
15 | + } | |
16 | + | |
17 | + if ( | |
18 | + component.includes('Select') || | |
19 | + component.includes('Checkbox') || | |
20 | + component.includes('Radio') || | |
21 | + component.includes('Switch') | |
22 | + ) { | |
23 | + return t('component.form.choose'); | |
24 | + } | |
25 | + return ''; | |
26 | +} | ... | ... |
src/components/Table/src/components/editable/index.ts
0 → 100644
1 | +import type { BasicColumn } from '/@/components/Table/src/types/table'; | |
2 | + | |
3 | +import { h } from 'vue'; | |
4 | + | |
5 | +import EditableCell from './EditableCell.vue'; | |
6 | + | |
7 | +interface Params { | |
8 | + text: string; | |
9 | + record: Recordable; | |
10 | + index: number; | |
11 | +} | |
12 | + | |
13 | +export function renderEditCell(column: BasicColumn) { | |
14 | + return ({ text: value, record, index }: Params) => { | |
15 | + record.onEdit = async (edit: boolean, submit = false) => { | |
16 | + if (!submit) { | |
17 | + record.editable = edit; | |
18 | + } | |
19 | + | |
20 | + if (!edit && submit) { | |
21 | + const res = await record.onSubmitEdit?.(); | |
22 | + if (res) { | |
23 | + record.editable = false; | |
24 | + return true; | |
25 | + } | |
26 | + return false; | |
27 | + } | |
28 | + // cancel | |
29 | + if (!edit && !submit) { | |
30 | + record.onCancelEdit?.(); | |
31 | + } | |
32 | + return true; | |
33 | + }; | |
34 | + | |
35 | + return h(EditableCell, { | |
36 | + value, | |
37 | + record, | |
38 | + column, | |
39 | + index, | |
40 | + }); | |
41 | + }; | |
42 | +} | |
43 | + | |
44 | +export type EditRecordRow<T = Hash<any>> = { | |
45 | + onEdit: (editable: boolean, submit?: boolean) => Promise<boolean>; | |
46 | + editable: boolean; | |
47 | + onCancel: Fn; | |
48 | + onSubmit: Fn; | |
49 | + submitCbs: Fn[]; | |
50 | + cancelCbs: Fn[]; | |
51 | + validCbs: Fn[]; | |
52 | +} & T; | ... | ... |
src/components/Table/src/components/renderEditable.tsx deleted
100644 → 0
1 | -import '../style/editable-cell.less'; | |
2 | - | |
3 | -import { defineComponent, PropType, ref, unref, nextTick, watchEffect } from 'vue'; | |
4 | -import { ClickOutSide } from '/@/components/ClickOutSide'; | |
5 | - | |
6 | -import { RenderEditableCellParams } from '../types/table'; | |
7 | -import { ComponentType } from '../types/componentType'; | |
8 | - | |
9 | -import { componentMap } from '../componentMap'; | |
10 | -import { isString, isBoolean, isArray } from '/@/utils/is'; | |
11 | -import { FormOutlined, CloseOutlined, CheckOutlined } from '@ant-design/icons-vue'; | |
12 | - | |
13 | -const prefixCls = 'editable-cell'; | |
14 | -const EditableCell = defineComponent({ | |
15 | - name: 'EditableCell', | |
16 | - props: { | |
17 | - value: { | |
18 | - type: String as PropType<string>, | |
19 | - default: '', | |
20 | - }, | |
21 | - componentProps: { | |
22 | - type: Object as PropType<any>, | |
23 | - default: null, | |
24 | - }, | |
25 | - | |
26 | - dataKey: { | |
27 | - type: String as PropType<string>, | |
28 | - default: '', | |
29 | - }, | |
30 | - | |
31 | - dataIndex: { | |
32 | - type: String as PropType<string>, | |
33 | - default: '', | |
34 | - }, | |
35 | - | |
36 | - component: { | |
37 | - type: String as PropType<ComponentType>, | |
38 | - default: 'Input', | |
39 | - }, | |
40 | - editable: { | |
41 | - type: Boolean as PropType<boolean>, | |
42 | - default: false, | |
43 | - }, | |
44 | - editRow: { | |
45 | - type: Boolean as PropType<boolean>, | |
46 | - default: false, | |
47 | - }, | |
48 | - record: { | |
49 | - type: Object as PropType<EditRecordRow>, | |
50 | - }, | |
51 | - placeholder: { | |
52 | - type: String as PropType<string>, | |
53 | - default: '', | |
54 | - }, | |
55 | - }, | |
56 | - emits: ['submit', 'cancel'], | |
57 | - setup(props, { attrs, emit }) { | |
58 | - const elRef = ref<any>(null); | |
59 | - | |
60 | - const isEditRef = ref(false); | |
61 | - const currentValueRef = ref<string | boolean>(props.value); | |
62 | - const defaultValueRef = ref<string | boolean>(props.value); | |
63 | - | |
64 | - watchEffect(() => { | |
65 | - defaultValueRef.value = props.value; | |
66 | - if (isBoolean(props.editable)) { | |
67 | - isEditRef.value = props.editable; | |
68 | - } | |
69 | - }); | |
70 | - | |
71 | - function handleChange(e: any) { | |
72 | - if (e && e.target && Reflect.has(e.target, 'value')) { | |
73 | - currentValueRef.value = (e as ChangeEvent).target.value; | |
74 | - } | |
75 | - if (isString(e) || isBoolean(e)) { | |
76 | - currentValueRef.value = e; | |
77 | - } | |
78 | - } | |
79 | - | |
80 | - function handleEdit() { | |
81 | - isEditRef.value = true; | |
82 | - nextTick(() => { | |
83 | - const el = unref(elRef); | |
84 | - el && el.focus(); | |
85 | - }); | |
86 | - } | |
87 | - | |
88 | - function handleCancel() { | |
89 | - isEditRef.value = false; | |
90 | - currentValueRef.value = defaultValueRef.value; | |
91 | - emit('cancel'); | |
92 | - } | |
93 | - | |
94 | - if (props.record) { | |
95 | - /* eslint-disable */ | |
96 | - isArray(props.record.submitCbs) | |
97 | - ? props.record.submitCbs.push(handleSubmit) | |
98 | - : (props.record.submitCbs = [handleSubmit]); | |
99 | - /* eslint-disable */ | |
100 | - isArray(props.record.cancelCbs) | |
101 | - ? props.record.cancelCbs.push(handleCancel) | |
102 | - : (props.record.cancelCbs = [handleCancel]); | |
103 | - | |
104 | - /* eslint-disable */ | |
105 | - props.record.onCancel = () => { | |
106 | - isArray(props.record?.cancelCbs) && props.record?.cancelCbs.forEach((fn) => fn()); | |
107 | - }; | |
108 | - /* eslint-disable */ | |
109 | - props.record.onSubmit = () => { | |
110 | - isArray(props.record?.submitCbs) && props.record?.submitCbs.forEach((fn) => fn()); | |
111 | - }; | |
112 | - } | |
113 | - | |
114 | - function handleSubmit() { | |
115 | - const { dataKey, dataIndex } = props; | |
116 | - if (!dataKey || !dataIndex) return; | |
117 | - | |
118 | - if (props.record) { | |
119 | - /* eslint-disable */ | |
120 | - props.record[dataIndex] = unref(currentValueRef) as string; | |
121 | - } | |
122 | - isEditRef.value = false; | |
123 | - } | |
124 | - | |
125 | - function onClickOutside() { | |
126 | - if (props.editRow) return; | |
127 | - const { component } = props; | |
128 | - | |
129 | - if (component && component.includes('Input')) { | |
130 | - handleCancel(); | |
131 | - } | |
132 | - } | |
133 | - | |
134 | - function renderValue() { | |
135 | - const { value } = props; | |
136 | - if (props.editRow) { | |
137 | - return !unref(isEditRef) ? value : null; | |
138 | - } | |
139 | - return ( | |
140 | - !unref(isEditRef) && ( | |
141 | - <div class={`${prefixCls}__normal`} onClick={handleEdit}> | |
142 | - {value} | |
143 | - <FormOutlined class={`${prefixCls}__normal-icon`} /> | |
144 | - </div> | |
145 | - ) | |
146 | - ); | |
147 | - } | |
148 | - return () => { | |
149 | - const { component, componentProps = {} } = props; | |
150 | - | |
151 | - const Comp = componentMap.get(component!) as any; | |
152 | - return ( | |
153 | - <div class={prefixCls}> | |
154 | - {unref(isEditRef) && ( | |
155 | - <ClickOutSide onClickOutside={onClickOutside}> | |
156 | - {() => ( | |
157 | - <div class={`${prefixCls}__wrapper`}> | |
158 | - <Comp | |
159 | - placeholder={props.placeholder} | |
160 | - {...{ | |
161 | - ...attrs, | |
162 | - ...componentProps, | |
163 | - }} | |
164 | - style={{ width: 'calc(100% - 48px)' }} | |
165 | - ref={elRef} | |
166 | - value={unref(currentValueRef)} | |
167 | - size="small" | |
168 | - onChange={handleChange} | |
169 | - onPressEnter={handleSubmit} | |
170 | - /> | |
171 | - {!props.editRow && ( | |
172 | - <div class={`${prefixCls}__action`}> | |
173 | - <CheckOutlined | |
174 | - class={[`${prefixCls}__icon`, 'mx-2']} | |
175 | - onClick={handleSubmit} | |
176 | - /> | |
177 | - <CloseOutlined class={[`${prefixCls}__icon `]} onClick={handleCancel} /> | |
178 | - </div> | |
179 | - )} | |
180 | - </div> | |
181 | - )} | |
182 | - </ClickOutSide> | |
183 | - )} | |
184 | - {renderValue()} | |
185 | - </div> | |
186 | - ); | |
187 | - }; | |
188 | - }, | |
189 | -}); | |
190 | - | |
191 | -export function renderEditableCell({ | |
192 | - dataIndex, | |
193 | - component, | |
194 | - componentProps = {}, | |
195 | - placeholder, | |
196 | -}: RenderEditableCellParams) { | |
197 | - return ({ text, record }: { text: string; record: EditRecordRow }) => { | |
198 | - return ( | |
199 | - <EditableCell | |
200 | - {...componentProps} | |
201 | - placeholder={placeholder} | |
202 | - value={text} | |
203 | - record={record} | |
204 | - dataKey={record.key} | |
205 | - dataIndex={dataIndex} | |
206 | - component={component} | |
207 | - /> | |
208 | - ); | |
209 | - }; | |
210 | -} | |
211 | - | |
212 | -export function renderEditableRow({ | |
213 | - dataIndex, | |
214 | - component, | |
215 | - componentProps = {}, | |
216 | - placeholder, | |
217 | -}: RenderEditableCellParams) { | |
218 | - return ({ text, record }: { text: string; record: EditRecordRow }) => { | |
219 | - return ( | |
220 | - <EditableCell | |
221 | - {...componentProps} | |
222 | - value={text} | |
223 | - placeholder={placeholder} | |
224 | - editRow={true} | |
225 | - editable={record.editable} | |
226 | - dataKey={record.key} | |
227 | - record={record} | |
228 | - dataIndex={dataIndex} | |
229 | - component={component} | |
230 | - /> | |
231 | - ); | |
232 | - }; | |
233 | -} | |
234 | - | |
235 | -export type EditRecordRow<T = Hash<any>> = { | |
236 | - editable: boolean; | |
237 | - onCancel: Fn; | |
238 | - onSubmit: Fn; | |
239 | - submitCbs: Fn[]; | |
240 | - cancelCbs: Fn[]; | |
241 | -} & T; |
src/components/Table/src/components/renderExpandIcon.tsx deleted
100644 → 0
src/components/Table/src/components/settings/ColumnSetting.vue
... | ... | @@ -184,7 +184,7 @@ |
184 | 184 | const ret: Options[] = []; |
185 | 185 | table.getColumns({ ignoreIndex: true, ignoreAction: true }).forEach((item) => { |
186 | 186 | ret.push({ |
187 | - label: item.title as string, | |
187 | + label: (item.title as string) || (item.customTitle as string), | |
188 | 188 | value: (item.dataIndex || item.title) as string, |
189 | 189 | ...item, |
190 | 190 | }); | ... | ... |
src/components/Table/src/const.ts
src/components/Table/src/hooks/useColumns.ts
1 | -import { BasicColumn, BasicTableProps, GetColumnsParams } from '../types/table'; | |
2 | -import { PaginationProps } from '../types/pagination'; | |
1 | +import type { BasicColumn, BasicTableProps, CellFormat, GetColumnsParams } from '../types/table'; | |
2 | +import type { PaginationProps } from '../types/pagination'; | |
3 | 3 | import { unref, ComputedRef, Ref, computed, watchEffect, ref, toRaw } from 'vue'; |
4 | -import { isBoolean, isArray, isString } from '/@/utils/is'; | |
4 | +import { isBoolean, isArray, isString, isObject } from '/@/utils/is'; | |
5 | 5 | import { DEFAULT_ALIGN, PAGE_SIZE, INDEX_COLUMN_FLAG, ACTION_COLUMN_FLAG } from '../const'; |
6 | 6 | import { useI18n } from '/@/hooks/web/useI18n'; |
7 | 7 | import { isEqual, cloneDeep } from 'lodash-es'; |
8 | +import { isFunction } from '/@/utils/is'; | |
9 | +import { formatToDate } from '/@/utils/dateUtil'; | |
10 | +import { renderEditCell } from '../components/editable'; | |
8 | 11 | |
9 | 12 | const { t } = useI18n(); |
10 | 13 | |
... | ... | @@ -127,8 +130,30 @@ export function useColumns( |
127 | 130 | return columns; |
128 | 131 | }); |
129 | 132 | |
130 | - const getSortFixedColumns = computed(() => { | |
131 | - return useFixedColumn(unref(getColumnsRef)); | |
133 | + const getViewColumns = computed(() => { | |
134 | + const viewColumns = sortFixedColumn(unref(getColumnsRef)); | |
135 | + | |
136 | + viewColumns.forEach((column) => { | |
137 | + const { slots, dataIndex, customRender, format, edit, editRow, flag } = column; | |
138 | + | |
139 | + if (!slots || !slots?.title) { | |
140 | + column.slots = { title: `header-${dataIndex}`, ...(slots || {}) }; | |
141 | + column.customTitle = column.title; | |
142 | + Reflect.deleteProperty(column, 'title'); | |
143 | + } | |
144 | + const isDefaultAction = [INDEX_COLUMN_FLAG, ACTION_COLUMN_FLAG].includes(flag!); | |
145 | + if (!customRender && format && !edit && !isDefaultAction) { | |
146 | + column.customRender = ({ text, record, index }) => { | |
147 | + return formatCell(text, format, record, index); | |
148 | + }; | |
149 | + } | |
150 | + | |
151 | + // edit table | |
152 | + if ((edit || editRow) && !isDefaultAction) { | |
153 | + column.customRender = renderEditCell(column); | |
154 | + } | |
155 | + }); | |
156 | + return viewColumns; | |
132 | 157 | }); |
133 | 158 | |
134 | 159 | watchEffect(() => { |
... | ... | @@ -191,7 +216,7 @@ export function useColumns( |
191 | 216 | } |
192 | 217 | |
193 | 218 | if (sort) { |
194 | - columns = useFixedColumn(columns); | |
219 | + columns = sortFixedColumn(columns); | |
195 | 220 | } |
196 | 221 | |
197 | 222 | return columns; |
... | ... | @@ -200,10 +225,10 @@ export function useColumns( |
200 | 225 | return cacheColumns; |
201 | 226 | } |
202 | 227 | |
203 | - return { getColumnsRef, getCacheColumns, getColumns, setColumns, getSortFixedColumns }; | |
228 | + return { getColumnsRef, getCacheColumns, getColumns, setColumns, getViewColumns }; | |
204 | 229 | } |
205 | 230 | |
206 | -export function useFixedColumn(columns: BasicColumn[]) { | |
231 | +function sortFixedColumn(columns: BasicColumn[]) { | |
207 | 232 | const fixedLeftColumns: BasicColumn[] = []; |
208 | 233 | const fixedRightColumns: BasicColumn[] = []; |
209 | 234 | const defColumns: BasicColumn[] = []; |
... | ... | @@ -224,3 +249,35 @@ export function useFixedColumn(columns: BasicColumn[]) { |
224 | 249 | |
225 | 250 | return resultColumns; |
226 | 251 | } |
252 | + | |
253 | +// format cell | |
254 | +export function formatCell(text: string, format: CellFormat, record: Recordable, index: number) { | |
255 | + if (!format) { | |
256 | + return text; | |
257 | + } | |
258 | + | |
259 | + // custom function | |
260 | + if (isFunction(format)) { | |
261 | + return format(text, record, index); | |
262 | + } | |
263 | + | |
264 | + try { | |
265 | + // date type | |
266 | + const DATE_FORMAT_PREFIX = 'date|'; | |
267 | + if (isString(format) && format.startsWith(DATE_FORMAT_PREFIX)) { | |
268 | + const dateFormat = format.replace(DATE_FORMAT_PREFIX, ''); | |
269 | + | |
270 | + if (!dateFormat) { | |
271 | + return text; | |
272 | + } | |
273 | + return formatToDate(text, dateFormat); | |
274 | + } | |
275 | + | |
276 | + // enum | |
277 | + if (isObject(format) && Reflect.has(format, 'size')) { | |
278 | + return format.get(text); | |
279 | + } | |
280 | + } catch (error) { | |
281 | + return text; | |
282 | + } | |
283 | +} | ... | ... |
src/components/Table/src/hooks/useDataSource.ts
1 | -import type { BasicTableProps, FetchParams } from '../types/table'; | |
1 | +import type { BasicTableProps, FetchParams, SorterResult } from '../types/table'; | |
2 | 2 | import type { PaginationProps } from '../types/pagination'; |
3 | 3 | |
4 | -import { ref, unref, ComputedRef, computed, onMounted, watchEffect } from 'vue'; | |
4 | +import { ref, unref, ComputedRef, computed, onMounted, watchEffect, reactive } from 'vue'; | |
5 | 5 | |
6 | 6 | import { useTimeoutFn } from '/@/hooks/core/useTimeout'; |
7 | 7 | |
... | ... | @@ -16,12 +16,28 @@ interface ActionType { |
16 | 16 | setPagination: (info: Partial<PaginationProps>) => void; |
17 | 17 | setLoading: (loading: boolean) => void; |
18 | 18 | getFieldsValue: () => Recordable; |
19 | + clearSelectedRowKeys: () => void; | |
20 | +} | |
21 | + | |
22 | +interface SearchState { | |
23 | + sortInfo: Recordable; | |
24 | + filterInfo: Record<string, string[]>; | |
19 | 25 | } |
20 | 26 | export function useDataSource( |
21 | 27 | propsRef: ComputedRef<BasicTableProps>, |
22 | - { getPaginationInfo, setPagination, setLoading, getFieldsValue }: ActionType, | |
28 | + { | |
29 | + getPaginationInfo, | |
30 | + setPagination, | |
31 | + setLoading, | |
32 | + getFieldsValue, | |
33 | + clearSelectedRowKeys, | |
34 | + }: ActionType, | |
23 | 35 | emit: EmitType |
24 | 36 | ) { |
37 | + const searchState = reactive<SearchState>({ | |
38 | + sortInfo: {}, | |
39 | + filterInfo: {}, | |
40 | + }); | |
25 | 41 | const dataSourceRef = ref<Recordable[]>([]); |
26 | 42 | |
27 | 43 | watchEffect(() => { |
... | ... | @@ -29,6 +45,32 @@ export function useDataSource( |
29 | 45 | !api && dataSource && (dataSourceRef.value = dataSource); |
30 | 46 | }); |
31 | 47 | |
48 | + function handleTableChange( | |
49 | + pagination: PaginationProps, | |
50 | + filters: Partial<Recordable<string[]>>, | |
51 | + sorter: SorterResult | |
52 | + ) { | |
53 | + const { clearSelectOnPageChange, sortFn, filterFn } = unref(propsRef); | |
54 | + if (clearSelectOnPageChange) { | |
55 | + clearSelectedRowKeys(); | |
56 | + } | |
57 | + setPagination(pagination); | |
58 | + | |
59 | + const params: Recordable = {}; | |
60 | + if (sorter && isFunction(sortFn)) { | |
61 | + const sortInfo = sortFn(sorter); | |
62 | + searchState.sortInfo = sortInfo; | |
63 | + params.sortInfo = sortInfo; | |
64 | + } | |
65 | + | |
66 | + if (filters && isFunction(filterFn)) { | |
67 | + const filterInfo = filterFn(filters); | |
68 | + searchState.filterInfo = filterInfo; | |
69 | + params.filterInfo = filterInfo; | |
70 | + } | |
71 | + fetch(params); | |
72 | + } | |
73 | + | |
32 | 74 | function setTableKey(items: any[]) { |
33 | 75 | if (!items || !Array.isArray(items)) return; |
34 | 76 | items.forEach((item) => { |
... | ... | @@ -75,6 +117,14 @@ export function useDataSource( |
75 | 117 | return unref(dataSourceRef); |
76 | 118 | }); |
77 | 119 | |
120 | + async function updateTableData(index: number, key: string, value: any) { | |
121 | + const record = dataSourceRef.value[index]; | |
122 | + if (record) { | |
123 | + dataSourceRef.value[index][key] = value; | |
124 | + } | |
125 | + return dataSourceRef.value[index]; | |
126 | + } | |
127 | + | |
78 | 128 | async function fetch(opt?: FetchParams) { |
79 | 129 | const { api, searchInfo, fetchSetting, beforeFetch, afterFetch, useSearchForm } = unref( |
80 | 130 | propsRef |
... | ... | @@ -94,6 +144,8 @@ export function useDataSource( |
94 | 144 | pageParams[sizeField] = pageSize; |
95 | 145 | } |
96 | 146 | |
147 | + const { sortInfo = {}, filterInfo } = searchState; | |
148 | + | |
97 | 149 | let params: Recordable = { |
98 | 150 | ...pageParams, |
99 | 151 | ...(useSearchForm ? getFieldsValue() : {}), |
... | ... | @@ -101,6 +153,8 @@ export function useDataSource( |
101 | 153 | ...(opt ? opt.searchInfo : {}), |
102 | 154 | ...(opt ? opt.sortInfo : {}), |
103 | 155 | ...(opt ? opt.filterInfo : {}), |
156 | + ...sortInfo, | |
157 | + ...filterInfo, | |
104 | 158 | }; |
105 | 159 | if (beforeFetch && isFunction(beforeFetch)) { |
106 | 160 | params = beforeFetch(params) || params; |
... | ... | @@ -175,5 +229,7 @@ export function useDataSource( |
175 | 229 | getAutoCreateKey, |
176 | 230 | fetch, |
177 | 231 | reload, |
232 | + updateTableData, | |
233 | + handleTableChange, | |
178 | 234 | }; |
179 | 235 | } | ... | ... |
src/components/Table/src/hooks/useTable.ts
1 | 1 | import type { BasicTableProps, TableActionType, FetchParams, BasicColumn } from '../types/table'; |
2 | 2 | import type { PaginationProps } from '../types/pagination'; |
3 | +import type { DynamicProps } from '/@/types/utils'; | |
4 | +import { getDynamicProps } from '/@/utils'; | |
3 | 5 | |
4 | 6 | import { ref, onUnmounted, unref } from 'vue'; |
5 | 7 | import { isProdMode } from '/@/utils/env'; |
6 | 8 | import { isInSetup } from '/@/utils/helper/vueHelper'; |
9 | +import { error } from '/@/utils/log'; | |
10 | +import { watchEffect } from 'vue'; | |
11 | +import type { FormActionType } from '/@/components/Form'; | |
12 | + | |
13 | +type Props = Partial<DynamicProps<BasicTableProps>>; | |
7 | 14 | |
8 | 15 | export function useTable( |
9 | - tableProps?: Partial<BasicTableProps> | |
10 | -): [(instance: TableActionType) => void, TableActionType] { | |
16 | + tableProps?: Props | |
17 | +): [(instance: TableActionType, formInstance: FormActionType) => void, TableActionType] { | |
11 | 18 | isInSetup(); |
12 | 19 | |
13 | 20 | const tableRef = ref<Nullable<TableActionType>>(null); |
14 | 21 | const loadedRef = ref<Nullable<boolean>>(false); |
22 | + const formRef = ref<Nullable<FormActionType>>(null); | |
15 | 23 | |
16 | - function register(instance: TableActionType) { | |
24 | + function register(instance: TableActionType, formInstance: FormActionType) { | |
17 | 25 | isProdMode() && |
18 | 26 | onUnmounted(() => { |
19 | 27 | tableRef.value = null; |
... | ... | @@ -24,20 +32,29 @@ export function useTable( |
24 | 32 | return; |
25 | 33 | } |
26 | 34 | tableRef.value = instance; |
27 | - tableProps && instance.setProps(tableProps); | |
35 | + formRef.value = formInstance; | |
36 | + // tableProps && instance.setProps(tableProps); | |
28 | 37 | loadedRef.value = true; |
38 | + | |
39 | + watchEffect(() => { | |
40 | + tableProps && instance.setProps(getDynamicProps(tableProps)); | |
41 | + }); | |
29 | 42 | } |
30 | 43 | |
31 | 44 | function getTableInstance(): TableActionType { |
32 | 45 | const table = unref(tableRef); |
33 | 46 | if (!table) { |
34 | - throw new Error('table is undefined!'); | |
47 | + error( | |
48 | + 'The table instance has not been obtained yet, please make sure the table is presented when performing the table operation!' | |
49 | + ); | |
35 | 50 | } |
36 | - return table; | |
51 | + return table as TableActionType; | |
37 | 52 | } |
38 | 53 | |
39 | - const methods: TableActionType = { | |
40 | - reload: (opt?: FetchParams) => { | |
54 | + const methods: TableActionType & { | |
55 | + getForm: () => FormActionType; | |
56 | + } = { | |
57 | + reload: async (opt?: FetchParams) => { | |
41 | 58 | getTableInstance().reload(opt); |
42 | 59 | }, |
43 | 60 | setProps: (props: Partial<BasicTableProps>) => { |
... | ... | @@ -54,7 +71,6 @@ export function useTable( |
54 | 71 | }, |
55 | 72 | getColumns: ({ ignoreIndex = false }: { ignoreIndex?: boolean } = {}) => { |
56 | 73 | const columns = getTableInstance().getColumns({ ignoreIndex }) || []; |
57 | - | |
58 | 74 | return columns; |
59 | 75 | }, |
60 | 76 | setColumns: (columns: BasicColumn[]) => { |
... | ... | @@ -87,7 +103,19 @@ export function useTable( |
87 | 103 | getSize: () => { |
88 | 104 | return getTableInstance().getSize(); |
89 | 105 | }, |
90 | - } as TableActionType; | |
106 | + updateTableData: (index: number, key: string, value: any) => { | |
107 | + return getTableInstance().updateTableData(index, key, value); | |
108 | + }, | |
109 | + getRowSelection: () => { | |
110 | + return getTableInstance().getRowSelection(); | |
111 | + }, | |
112 | + getCacheColumns: () => { | |
113 | + return getTableInstance().getCacheColumns(); | |
114 | + }, | |
115 | + getForm: () => { | |
116 | + return unref(formRef) as FormActionType; | |
117 | + }, | |
118 | + }; | |
91 | 119 | |
92 | 120 | return [register, methods]; |
93 | 121 | } | ... | ... |
src/components/Table/src/hooks/useTableScroll.ts
... | ... | @@ -121,7 +121,7 @@ export function useTableScroll( |
121 | 121 | width += 60; |
122 | 122 | } |
123 | 123 | |
124 | - // TODO props | |
124 | + // TODO propsdth ?? 0; | |
125 | 125 | const NORMAL_WIDTH = 150; |
126 | 126 | |
127 | 127 | const columns = unref(columnsRef); |
... | ... | @@ -135,7 +135,10 @@ export function useTableScroll( |
135 | 135 | if (len !== 0) { |
136 | 136 | width += len * NORMAL_WIDTH; |
137 | 137 | } |
138 | - return width; | |
138 | + | |
139 | + const table = unref(tableElRef); | |
140 | + const tableWidth = table?.$el?.offsetWidth ?? 0; | |
141 | + return tableWidth > width ? tableWidth - 24 : width; | |
139 | 142 | }); |
140 | 143 | |
141 | 144 | const getScrollRef = computed(() => { | ... | ... |
src/components/Table/src/props.ts
... | ... | @@ -9,21 +9,29 @@ import type { |
9 | 9 | TableRowSelection, |
10 | 10 | } from './types/table'; |
11 | 11 | import type { FormProps } from '/@/components/Form'; |
12 | -import { DEFAULT_SORT_FN, FETCH_SETTING } from './const'; | |
12 | +import { DEFAULT_FILTER_FN, DEFAULT_SORT_FN, FETCH_SETTING } from './const'; | |
13 | 13 | import { propTypes } from '/@/utils/propTypes'; |
14 | 14 | |
15 | 15 | // 注释看 types/table |
16 | 16 | export const basicProps = { |
17 | 17 | clickToRowSelect: propTypes.bool.def(true), |
18 | + | |
18 | 19 | tableSetting: { |
19 | 20 | type: Object as PropType<TableSetting>, |
20 | 21 | }, |
22 | + | |
21 | 23 | inset: propTypes.bool, |
24 | + | |
22 | 25 | sortFn: { |
23 | 26 | type: Function as PropType<(sortInfo: SorterResult) => any>, |
24 | 27 | default: DEFAULT_SORT_FN, |
25 | 28 | }, |
26 | 29 | |
30 | + filterFn: { | |
31 | + type: Function as PropType<(data: Partial<Recordable<string[]>>) => any>, | |
32 | + default: DEFAULT_FILTER_FN, | |
33 | + }, | |
34 | + | |
27 | 35 | showTableSetting: propTypes.bool, |
28 | 36 | autoCreateKey: propTypes.bool.def(true), |
29 | 37 | striped: propTypes.bool.def(true), | ... | ... |
src/components/Table/src/style/editable-cell.less deleted
100644 → 0
1 | -@prefix-cls: ~'editable-cell'; | |
2 | - | |
3 | -.@{prefix-cls} { | |
4 | - position: relative; | |
5 | - | |
6 | - &__wrapper { | |
7 | - display: flex; | |
8 | - align-items: center; | |
9 | - } | |
10 | - | |
11 | - &__icon { | |
12 | - &:hover { | |
13 | - transform: scale(1.2); | |
14 | - | |
15 | - svg { | |
16 | - color: @primary-color; | |
17 | - } | |
18 | - } | |
19 | - } | |
20 | - | |
21 | - &__normal { | |
22 | - padding-right: 48px; | |
23 | - | |
24 | - &-icon { | |
25 | - position: absolute; | |
26 | - top: 4px; | |
27 | - right: 0; | |
28 | - display: none; | |
29 | - width: 20px; | |
30 | - cursor: pointer; | |
31 | - } | |
32 | - } | |
33 | - | |
34 | - &:hover { | |
35 | - .@{prefix-cls}__normal-icon { | |
36 | - display: inline-block; | |
37 | - } | |
38 | - } | |
39 | -} |
src/components/Table/src/style/index.less
... | ... | @@ -133,14 +133,18 @@ |
133 | 133 | overflow-y: scroll !important; |
134 | 134 | } |
135 | 135 | |
136 | - .ant-table-fixed-right .ant-table-header { | |
137 | - border-left: 1px solid @border-color !important; | |
136 | + .ant-table-fixed-right { | |
137 | + right: -1px; | |
138 | 138 | |
139 | - .ant-table-fixed { | |
140 | - border-bottom: none; | |
139 | + .ant-table-header { | |
140 | + border-left: 1px solid @border-color !important; | |
141 | + | |
142 | + .ant-table-fixed { | |
143 | + border-bottom: none; | |
141 | 144 | |
142 | - .ant-table-thead th { | |
143 | - background: rgb(241, 243, 244); | |
145 | + .ant-table-thead th { | |
146 | + background: rgb(241, 243, 244); | |
147 | + } | |
144 | 148 | } |
145 | 149 | } |
146 | 150 | } | ... | ... |
src/components/Table/src/types/componentType.ts
src/components/Table/src/types/table.ts
... | ... | @@ -6,9 +6,10 @@ import type { |
6 | 6 | TableRowSelection as ITableRowSelection, |
7 | 7 | } from 'ant-design-vue/lib/table/interface'; |
8 | 8 | import { ComponentType } from './componentType'; |
9 | +import { VueNode } from '/@/utils/propTypes'; | |
9 | 10 | // import { ColumnProps } from './column'; |
10 | 11 | export declare type SortOrder = 'ascend' | 'descend'; |
11 | -export interface TableCurrentDataSource<T = any> { | |
12 | +export interface TableCurrentDataSource<T = Recordable> { | |
12 | 13 | currentDataSource: T[]; |
13 | 14 | } |
14 | 15 | |
... | ... | @@ -53,7 +54,7 @@ export interface ColumnFilterItem { |
53 | 54 | children?: any; |
54 | 55 | } |
55 | 56 | |
56 | -export interface TableCustomRecord<T = any> { | |
57 | +export interface TableCustomRecord<T = Recordable> { | |
57 | 58 | record?: T; |
58 | 59 | index?: number; |
59 | 60 | } |
... | ... | @@ -65,18 +66,11 @@ export interface SorterResult { |
65 | 66 | columnKey: string; |
66 | 67 | } |
67 | 68 | |
68 | -export interface RenderEditableCellParams { | |
69 | - dataIndex: string; | |
70 | - component?: ComponentType; | |
71 | - componentProps?: any; | |
72 | - placeholder?: string; | |
73 | -} | |
74 | - | |
75 | 69 | export interface FetchParams { |
76 | - searchInfo?: any; | |
70 | + searchInfo?: Recordable; | |
77 | 71 | page?: number; |
78 | - sortInfo?: any; | |
79 | - filterInfo?: any; | |
72 | + sortInfo?: Recordable; | |
73 | + filterInfo?: Recordable; | |
80 | 74 | } |
81 | 75 | |
82 | 76 | export interface GetColumnsParams { |
... | ... | @@ -89,7 +83,7 @@ export type SizeType = 'default' | 'middle' | 'small' | 'large'; |
89 | 83 | |
90 | 84 | export interface TableActionType { |
91 | 85 | reload: (opt?: FetchParams) => Promise<void>; |
92 | - getSelectRows: <T = any>() => T[]; | |
86 | + getSelectRows: <T = Recordable>() => T[]; | |
93 | 87 | clearSelectedRowKeys: () => void; |
94 | 88 | getSelectRowKeys: () => string[]; |
95 | 89 | deleteSelectRowByKey: (key: string) => void; |
... | ... | @@ -106,6 +100,8 @@ export interface TableActionType { |
106 | 100 | getSize: () => SizeType; |
107 | 101 | getRowSelection: () => TableRowSelection<Recordable>; |
108 | 102 | getCacheColumns: () => BasicColumn[]; |
103 | + emit?: EmitType; | |
104 | + updateTableData: (index: number, key: string, value: any) => Recordable; | |
109 | 105 | } |
110 | 106 | |
111 | 107 | export interface FetchSetting { |
... | ... | @@ -131,6 +127,8 @@ export interface BasicTableProps<T = any> { |
131 | 127 | clickToRowSelect?: boolean; |
132 | 128 | // 自定义排序方法 |
133 | 129 | sortFn?: (sortInfo: SorterResult) => any; |
130 | + // 排序方法 | |
131 | + filterFn?: (data: Partial<Recordable<string[]>>) => any; | |
134 | 132 | // 取消表格的默认padding |
135 | 133 | inset?: boolean; |
136 | 134 | // 显示表格设置 |
... | ... | @@ -141,7 +139,7 @@ export interface BasicTableProps<T = any> { |
141 | 139 | // 是否自动生成key |
142 | 140 | autoCreateKey?: boolean; |
143 | 141 | // 计算合计行的方法 |
144 | - summaryFunc?: (...arg: any) => any[]; | |
142 | + summaryFunc?: (...arg: any) => Recordable[]; | |
145 | 143 | // 是否显示合计行 |
146 | 144 | showSummary?: boolean; |
147 | 145 | // 是否可拖拽列 |
... | ... | @@ -374,13 +372,43 @@ export interface BasicTableProps<T = any> { |
374 | 372 | onExpandedRowsChange?: (expandedRows: string[] | number[]) => void; |
375 | 373 | } |
376 | 374 | |
375 | +export type CellFormat = | |
376 | + | string | |
377 | + | ((text: string, record: Recordable, index: number) => string | number) | |
378 | + | Map<string | number, any>; | |
379 | + | |
380 | +// @ts-ignore | |
377 | 381 | export interface BasicColumn extends ColumnProps { |
378 | 382 | children?: BasicColumn[]; |
383 | + filters?: { | |
384 | + text: string; | |
385 | + value: string; | |
386 | + children?: | |
387 | + | unknown[] | |
388 | + | (((props: Record<string, unknown>) => unknown[]) & (() => unknown[]) & (() => unknown[])); | |
389 | + }[]; | |
379 | 390 | |
380 | 391 | // |
381 | 392 | flag?: 'INDEX' | 'DEFAULT' | 'CHECKBOX' | 'RADIO' | 'ACTION'; |
393 | + customTitle?: VueNode; | |
382 | 394 | |
383 | 395 | slots?: Indexable; |
384 | 396 | |
397 | + // Whether to hide the column by default, it can be displayed in the column configuration | |
385 | 398 | defaultHidden?: boolean; |
399 | + | |
400 | + // Help text for table column header | |
401 | + helpMessage?: string | string[]; | |
402 | + | |
403 | + format?: CellFormat; | |
404 | + | |
405 | + // Editable | |
406 | + edit?: boolean; | |
407 | + editRow?: boolean; | |
408 | + editable?: boolean; | |
409 | + editComponent?: ComponentType; | |
410 | + editComponentProps?: Recordable; | |
411 | + editRule?: boolean | ((text: string, record: Recordable) => Promise<string>); | |
412 | + editValueMap?: (value: any) => string; | |
413 | + onEditRow?: () => void; | |
386 | 414 | } | ... | ... |
src/layouts/default/sider/MixSider.vue
src/router/menus/modules/demo/comp.ts
... | ... | @@ -52,6 +52,9 @@ const menu: MenuModule = { |
52 | 52 | { |
53 | 53 | path: 'table', |
54 | 54 | name: t('routes.demo.table.table'), |
55 | + tag: { | |
56 | + dot: true, | |
57 | + }, | |
55 | 58 | children: [ |
56 | 59 | { |
57 | 60 | path: 'basic', |
... | ... | @@ -108,10 +111,16 @@ const menu: MenuModule = { |
108 | 111 | { |
109 | 112 | path: 'editCellTable', |
110 | 113 | name: t('routes.demo.table.editCellTable'), |
114 | + tag: { | |
115 | + dot: true, | |
116 | + }, | |
111 | 117 | }, |
112 | 118 | { |
113 | 119 | path: 'editRowTable', |
114 | 120 | name: t('routes.demo.table.editRowTable'), |
121 | + tag: { | |
122 | + dot: true, | |
123 | + }, | |
115 | 124 | }, |
116 | 125 | ], |
117 | 126 | }, | ... | ... |
src/utils/dateUtil.ts
... | ... | @@ -3,12 +3,15 @@ import moment from 'moment'; |
3 | 3 | const DATE_TIME_FORMAT = 'YYYY-MM-DD HH:mm'; |
4 | 4 | const DATE_FORMAT = 'YYYY-MM-DD '; |
5 | 5 | |
6 | -export function formatToDateTime(date: moment.MomentInput = null): string { | |
7 | - return moment(date).format(DATE_TIME_FORMAT); | |
6 | +export function formatToDateTime( | |
7 | + date: moment.MomentInput = null, | |
8 | + format = DATE_TIME_FORMAT | |
9 | +): string { | |
10 | + return moment(date).format(format); | |
8 | 11 | } |
9 | 12 | |
10 | -export function formatToDate(date: moment.MomentInput = null): string { | |
11 | - return moment(date).format(DATE_FORMAT); | |
13 | +export function formatToDate(date: moment.MomentInput = null, format = DATE_FORMAT): string { | |
14 | + return moment(date).format(format); | |
12 | 15 | } |
13 | 16 | |
14 | 17 | export const formatAgo = (str: string | number) => { | ... | ... |
src/views/demo/table/EditCellTable.vue
1 | 1 | <template> |
2 | 2 | <div class="p-4"> |
3 | - <BasicTable @register="registerTable"> | |
4 | - <template #customId> | |
5 | - <EditTableHeaderIcon title="Id" /> | |
6 | - </template> | |
7 | - <template #customName> | |
8 | - <EditTableHeaderIcon title="姓名" /> | |
9 | - </template> | |
3 | + <BasicTable @register="registerTable" @edit-end="handleEditEnd" @edit-cancel="handleEditCancel"> | |
10 | 4 | </BasicTable> |
11 | 5 | </div> |
12 | 6 | </template> |
13 | 7 | <script lang="ts"> |
14 | 8 | import { defineComponent } from 'vue'; |
15 | - import { | |
16 | - BasicTable, | |
17 | - useTable, | |
18 | - BasicColumn, | |
19 | - renderEditableCell, | |
20 | - EditTableHeaderIcon, | |
21 | - } from '/@/components/Table'; | |
9 | + import { BasicTable, useTable, BasicColumn, EditTableHeaderIcon } from '/@/components/Table'; | |
10 | + import { optionsListApi } from '/@/api/demo/select'; | |
22 | 11 | |
23 | 12 | import { demoListApi } from '/@/api/demo/table'; |
24 | 13 | const columns: BasicColumn[] = [ |
25 | 14 | { |
26 | - // title: 'ID', | |
15 | + title: '输入框', | |
16 | + dataIndex: 'name', | |
17 | + edit: true, | |
18 | + editComponentProps: { | |
19 | + prefix: '$', | |
20 | + }, | |
21 | + width: 200, | |
22 | + }, | |
23 | + { | |
24 | + title: '默认输入状态', | |
25 | + dataIndex: 'name7', | |
26 | + edit: true, | |
27 | + editable: true, | |
28 | + width: 200, | |
29 | + }, | |
30 | + { | |
31 | + title: '输入框校验', | |
32 | + dataIndex: 'name1', | |
33 | + edit: true, | |
34 | + // 默认必填校验 | |
35 | + editRule: true, | |
36 | + width: 200, | |
37 | + }, | |
38 | + { | |
39 | + title: '输入框函数校验', | |
40 | + dataIndex: 'name2', | |
41 | + edit: true, | |
42 | + editRule: async (text) => { | |
43 | + if (text === '2') { | |
44 | + return '不能输入该值'; | |
45 | + } | |
46 | + return ''; | |
47 | + }, | |
48 | + width: 200, | |
49 | + }, | |
50 | + { | |
51 | + title: '数字输入框', | |
27 | 52 | dataIndex: 'id', |
28 | - slots: { title: 'customId' }, | |
29 | - customRender: renderEditableCell({ dataIndex: 'id' }), | |
53 | + edit: true, | |
54 | + editRule: true, | |
55 | + editComponent: 'InputNumber', | |
56 | + width: 200, | |
30 | 57 | }, |
31 | 58 | { |
32 | - // title: '姓名', | |
33 | - dataIndex: 'name', | |
34 | - slots: { title: 'customName' }, | |
35 | - customRender: renderEditableCell({ | |
36 | - dataIndex: 'name', | |
37 | - }), | |
59 | + title: '下拉框', | |
60 | + dataIndex: 'name3', | |
61 | + edit: true, | |
62 | + editComponent: 'Select', | |
63 | + editComponentProps: { | |
64 | + options: [ | |
65 | + { | |
66 | + label: 'Option1', | |
67 | + value: '1', | |
68 | + }, | |
69 | + { | |
70 | + label: 'Option2', | |
71 | + value: '2', | |
72 | + }, | |
73 | + ], | |
74 | + }, | |
75 | + width: 200, | |
76 | + }, | |
77 | + { | |
78 | + title: '远程下拉', | |
79 | + dataIndex: 'name4', | |
80 | + edit: true, | |
81 | + editComponent: 'ApiSelect', | |
82 | + editComponentProps: { | |
83 | + api: optionsListApi, | |
84 | + }, | |
85 | + width: 200, | |
38 | 86 | }, |
39 | 87 | { |
40 | - title: '地址', | |
41 | - dataIndex: 'address', | |
42 | - sorter: true, | |
88 | + title: '勾选框', | |
89 | + dataIndex: 'name5', | |
90 | + edit: true, | |
91 | + editComponent: 'Checkbox', | |
92 | + editValueMap: (value) => { | |
93 | + return value ? '是' : '否'; | |
94 | + }, | |
95 | + width: 200, | |
96 | + }, | |
97 | + { | |
98 | + title: '开关', | |
99 | + dataIndex: 'name6', | |
100 | + edit: true, | |
101 | + editComponent: 'Switch', | |
102 | + editValueMap: (value) => { | |
103 | + return value ? '开' : '关'; | |
104 | + }, | |
105 | + width: 200, | |
43 | 106 | }, |
44 | 107 | ]; |
45 | 108 | export default defineComponent({ |
... | ... | @@ -50,10 +113,21 @@ |
50 | 113 | api: demoListApi, |
51 | 114 | columns: columns, |
52 | 115 | showIndexColumn: false, |
116 | + bordered: true, | |
53 | 117 | }); |
54 | 118 | |
119 | + function handleEditEnd({ record, index, key, value }: Recordable) { | |
120 | + console.log(record, index, key, value); | |
121 | + } | |
122 | + | |
123 | + function handleEditCancel() { | |
124 | + console.log('cancel'); | |
125 | + } | |
126 | + | |
55 | 127 | return { |
56 | 128 | registerTable, |
129 | + handleEditEnd, | |
130 | + handleEditCancel, | |
57 | 131 | }; |
58 | 132 | }, |
59 | 133 | }); | ... | ... |
src/views/demo/table/EditRowTable.vue
... | ... | @@ -15,24 +15,105 @@ |
15 | 15 | TableAction, |
16 | 16 | BasicColumn, |
17 | 17 | ActionItem, |
18 | - renderEditableRow, | |
19 | 18 | EditTableHeaderIcon, |
20 | 19 | EditRecordRow, |
21 | 20 | } from '/@/components/Table'; |
21 | + import { optionsListApi } from '/@/api/demo/select'; | |
22 | 22 | |
23 | 23 | import { demoListApi } from '/@/api/demo/table'; |
24 | 24 | const columns: BasicColumn[] = [ |
25 | 25 | { |
26 | - title: 'ID', | |
26 | + title: '输入框', | |
27 | + dataIndex: 'name', | |
28 | + editRow: true, | |
29 | + editComponentProps: { | |
30 | + prefix: '$', | |
31 | + }, | |
32 | + width: 200, | |
33 | + }, | |
34 | + { | |
35 | + title: '默认输入状态', | |
36 | + dataIndex: 'name7', | |
37 | + editRow: true, | |
38 | + width: 200, | |
39 | + }, | |
40 | + { | |
41 | + title: '输入框校验', | |
42 | + dataIndex: 'name1', | |
43 | + editRow: true, | |
44 | + // 默认必填校验 | |
45 | + editRule: true, | |
46 | + width: 200, | |
47 | + }, | |
48 | + { | |
49 | + title: '输入框函数校验', | |
50 | + dataIndex: 'name2', | |
51 | + editRow: true, | |
52 | + editRule: async (text) => { | |
53 | + if (text === '2') { | |
54 | + return '不能输入该值'; | |
55 | + } | |
56 | + return ''; | |
57 | + }, | |
58 | + width: 200, | |
59 | + }, | |
60 | + { | |
61 | + title: '数字输入框', | |
27 | 62 | dataIndex: 'id', |
28 | - customRender: renderEditableRow({ dataIndex: 'id' }), | |
63 | + editRow: true, | |
64 | + editRule: true, | |
65 | + editComponent: 'InputNumber', | |
66 | + width: 200, | |
29 | 67 | }, |
30 | 68 | { |
31 | - title: '姓名', | |
32 | - dataIndex: 'name', | |
33 | - customRender: renderEditableRow({ | |
34 | - dataIndex: 'name', | |
35 | - }), | |
69 | + title: '下拉框', | |
70 | + dataIndex: 'name3', | |
71 | + editRow: true, | |
72 | + editComponent: 'Select', | |
73 | + editComponentProps: { | |
74 | + options: [ | |
75 | + { | |
76 | + label: 'Option1', | |
77 | + value: '1', | |
78 | + }, | |
79 | + { | |
80 | + label: 'Option2', | |
81 | + value: '2', | |
82 | + }, | |
83 | + ], | |
84 | + }, | |
85 | + width: 200, | |
86 | + }, | |
87 | + { | |
88 | + title: '远程下拉', | |
89 | + dataIndex: 'name4', | |
90 | + editRow: true, | |
91 | + editComponent: 'ApiSelect', | |
92 | + editComponentProps: { | |
93 | + api: optionsListApi, | |
94 | + }, | |
95 | + width: 200, | |
96 | + }, | |
97 | + { | |
98 | + title: '勾选框', | |
99 | + dataIndex: 'name5', | |
100 | + editRow: true, | |
101 | + | |
102 | + editComponent: 'Checkbox', | |
103 | + editValueMap: (value) => { | |
104 | + return value ? '是' : '否'; | |
105 | + }, | |
106 | + width: 200, | |
107 | + }, | |
108 | + { | |
109 | + title: '开关', | |
110 | + dataIndex: 'name6', | |
111 | + editRow: true, | |
112 | + editComponent: 'Switch', | |
113 | + editValueMap: (value) => { | |
114 | + return value ? '开' : '关'; | |
115 | + }, | |
116 | + width: 200, | |
36 | 117 | }, |
37 | 118 | ]; |
38 | 119 | export default defineComponent({ |
... | ... | @@ -55,19 +136,19 @@ |
55 | 136 | |
56 | 137 | function handleEdit(record: EditRecordRow) { |
57 | 138 | currentEditKeyRef.value = record.key; |
58 | - record.editable = true; | |
139 | + record.onEdit?.(true); | |
59 | 140 | } |
60 | 141 | |
61 | 142 | function handleCancel(record: EditRecordRow) { |
62 | 143 | currentEditKeyRef.value = ''; |
63 | - record.editable = false; | |
64 | - record.onCancel && record.onCancel(); | |
144 | + record.onEdit?.(false, true); | |
65 | 145 | } |
66 | 146 | |
67 | - function handleSave(record: EditRecordRow) { | |
68 | - currentEditKeyRef.value = ''; | |
69 | - record.editable = false; | |
70 | - record.onSubmit && record.onSubmit(); | |
147 | + async function handleSave(record: EditRecordRow) { | |
148 | + const pass = await record.onEdit?.(false, true); | |
149 | + if (pass) { | |
150 | + currentEditKeyRef.value = ''; | |
151 | + } | |
71 | 152 | } |
72 | 153 | |
73 | 154 | function createActions(record: EditRecordRow, column: BasicColumn): ActionItem[] { | ... | ... |
src/views/demo/table/FixedColumn.vue
... | ... | @@ -41,7 +41,6 @@ |
41 | 41 | { |
42 | 42 | title: '地址', |
43 | 43 | dataIndex: 'address', |
44 | - width: 260, | |
45 | 44 | }, |
46 | 45 | { |
47 | 46 | title: '编号', |
... | ... | @@ -67,6 +66,7 @@ |
67 | 66 | api: demoListApi, |
68 | 67 | columns: columns, |
69 | 68 | rowSelection: { type: 'radio' }, |
69 | + bordered: true, | |
70 | 70 | actionColumn: { |
71 | 71 | width: 160, |
72 | 72 | title: 'Action', | ... | ... |
src/views/demo/table/tableData.tsx
... | ... | @@ -7,12 +7,16 @@ export function getBasicColumns(): BasicColumn[] { |
7 | 7 | title: 'ID', |
8 | 8 | dataIndex: 'id', |
9 | 9 | fixed: 'left', |
10 | - width: 400, | |
10 | + width: 200, | |
11 | 11 | }, |
12 | 12 | { |
13 | 13 | title: '姓名', |
14 | 14 | dataIndex: 'name', |
15 | 15 | width: 150, |
16 | + filters: [ | |
17 | + { text: 'Male', value: 'male' }, | |
18 | + { text: 'Female', value: 'female' }, | |
19 | + ], | |
16 | 20 | }, |
17 | 21 | { |
18 | 22 | title: '地址', |
... | ... | @@ -22,11 +26,13 @@ export function getBasicColumns(): BasicColumn[] { |
22 | 26 | title: '编号', |
23 | 27 | dataIndex: 'no', |
24 | 28 | width: 150, |
29 | + sorter: true, | |
25 | 30 | defaultHidden: true, |
26 | 31 | }, |
27 | 32 | { |
28 | 33 | title: '开始时间', |
29 | 34 | width: 120, |
35 | + sorter: true, | |
30 | 36 | dataIndex: 'beginTime', |
31 | 37 | }, |
32 | 38 | { | ... | ... |