refactor(table): refactor table #150 #148 #146 #130 #76
| | |
| | | ## Wip |
| | | |
| | | ### ✨ 表格破坏性更新 |
| | | |
| | | - 重构了可编辑单元格及可编辑行。具体看示例。写法已改变。针对可编辑表格。 |
| | | |
| | | - 表格编辑支持表单校验 |
| | | |
| | | - 在表格列配置增加了以下配置 |
| | | |
| | | ```bash |
| | | { |
| | | |
| | | # 默认是否显示列。不显示的可以在列配置打开 |
| | | defaultHidden?: boolean; |
| | | # 列头右侧帮助文本 |
| | | helpMessage?: string | string[]; |
| | | # 自定义格式化 单元格内容。 支持时间/枚举自动转化 |
| | | format?: CellFormat; |
| | | |
| | | # Editable |
| | | # 是否是可编辑单元格 |
| | | edit?: boolean; |
| | | # 是否是可编辑行 |
| | | editRow?: boolean; |
| | | # 编辑状态。 |
| | | editable?: boolean; |
| | | # 编辑组件 |
| | | editComponent?: ComponentType; |
| | | # 所对应组件的参数 |
| | | editComponentProps?: Recordable; |
| | | # 校验 |
| | | editRule?: boolean | ((text: string, record: Recordable) => Promise<string>); |
| | | # 值枚举转化 |
| | | editValueMap?: (value: any) => string; |
| | | # 触发编辑正航 |
| | | record.onEditRow?: () => void; |
| | | } |
| | | |
| | | ``` |
| | | |
| | | ### ✨ 表格重构 |
| | | |
| | | - 新增`clickToRowSelect`属性。用于控制点击行是否选中勾选框 |
| | | - 监听行点击事件 |
| | | - 表格列配置按钮增加 列拖拽,列固定功能。 |
| | | - 表格列配置新增`defaultHidden` 属性。用于默认隐藏。可在表格列配置勾选显示 |
| | | - 更强大的列配置 |
| | | - useTable:支持动态改变参数。可以传入`Ref`类型与`Computed`类型进行动态更改 |
| | | - useTable:新增返回 `getForm`函数。可以用于操作表格内的表单 |
| | | - 修复表格已知的问题 |
| | | |
| | | ### ✨ Features |
| | | |
| | | - 新增 `v-ripple`水波纹指令 |
| | |
| | | - form: 新增远程下拉`ApiSelect`及示例 |
| | | - form: 新增`autoFocusFirstItem`配置。用于配置是否聚焦表单第一个输入框 |
| | | - useForm: 支持动态改变参数。可以传入`Ref`类型与`Computed`类型进行动态更改 |
| | | - table: 新增`clickToRowSelect`属性。用于控制点击行是否选中勾选狂 |
| | | - table: 监听行点击事件 |
| | | - table: 表格列配置按钮增加 列拖拽,列固定功能。 |
| | | - table:表格列配置新增`defaultHidden` 属性。用于默认隐藏。可在表格列配置勾选显示 |
| | | |
| | | ### ✨ Refactor |
| | | |
| | | - 重构表单,解决已知 bug |
| | | |
| | | ### ⚡ Performance Improvements |
| | | |
| | |
| | | ### 🎫 Chores |
| | | |
| | | - 升级`ant-design-vue`到`2.0.0-rc.7` |
| | | - 升级`vue`到`3.0.5` |
| | | |
| | | ### 🐛 Bug Fixes |
| | | |
| | |
| | | endTime: '@datetime', |
| | | address: '@city()', |
| | | name: '@cname()', |
| | | name1: '@cname()', |
| | | name2: '@cname()', |
| | | name3: '@cname()', |
| | | name4: '@cname()', |
| | | name5: '@cname()', |
| | | name6: '@cname()', |
| | | name7: '@cname()', |
| | | name8: '@cname()', |
| | | 'no|100000-10000000': 100000, |
| | | 'status|1': ['normal', 'enable', 'disable'], |
| | | }); |
| | |
| | | export { useComponentRegister } from './src/hooks/useComponentRegister'; |
| | | export { useForm } from './src/hooks/useForm'; |
| | | |
| | | export { default as ApiSelect } from './src/components/ApiSelect.vue'; |
| | | export { default as RadioButtonGroup } from './src/components/RadioButtonGroup.vue'; |
| | | |
| | | export { BasicForm }; |
| | |
| | | labelField: propTypes.string.def('label'), |
| | | valueField: propTypes.string.def('value'), |
| | | }, |
| | | setup(props) { |
| | | emits: ['options-change', 'change'], |
| | | setup(props, { emit }) { |
| | | const options = ref<OptionsItem[]>([]); |
| | | const loading = ref(false); |
| | | const attrs = useAttrs(); |
| | |
| | | const res = await api(props.params); |
| | | if (Array.isArray(res)) { |
| | | options.value = res; |
| | | emit('options-change', unref(options)); |
| | | return; |
| | | } |
| | | if (props.resultField) { |
| | | options.value = get(res, props.resultField) || []; |
| | | } |
| | | emit('options-change', unref(options)); |
| | | } catch (error) { |
| | | console.warn(error); |
| | | } finally { |
| | |
| | | mode: Ref<MenuModeEnum>, |
| | | accordion: Ref<boolean> |
| | | ) { |
| | | const { getCollapsed } = useMenuSetting(); |
| | | const { getCollapsed, getIsMixSidebar } = useMenuSetting(); |
| | | |
| | | function setOpenKeys(path: string) { |
| | | if (mode.value === MenuModeEnum.HORIZONTAL) { |
| | |
| | | } |
| | | |
| | | const getOpenKeys = computed(() => { |
| | | return unref(getCollapsed) ? menuState.collapsedOpenKeys : menuState.openKeys; |
| | | const collapse = unref(getIsMixSidebar) ? false : unref(getCollapsed); |
| | | |
| | | return collapse ? menuState.collapsedOpenKeys : menuState.openKeys; |
| | | }); |
| | | |
| | | /** |
| | |
| | | } |
| | | |
| | | function handleOpenChange(openKeys: string[]) { |
| | | if (unref(mode) === MenuModeEnum.HORIZONTAL || !unref(accordion)) { |
| | | if (unref(mode) === MenuModeEnum.HORIZONTAL || !unref(accordion) || unref(getIsMixSidebar)) { |
| | | menuState.openKeys = openKeys; |
| | | } else { |
| | | // const menuList = toRaw(menus.value); |
| | |
| | | export { default as BasicTable } from './src/BasicTable.vue'; |
| | | export { default as TableAction } from './src/components/TableAction.vue'; |
| | | // export { default as TableImg } from './src/components/TableImg.vue'; |
| | | export { renderEditableCell, renderEditableRow } from './src/components/renderEditable'; |
| | | export { default as EditTableHeaderIcon } from './src/components/EditTableHeaderIcon.vue'; |
| | | |
| | | export const TableImg = createAsyncComponent(() => import('./src/components/TableImg.vue')); |
| | |
| | | |
| | | export type { FormSchema, FormProps } from '/@/components/Form/src/types/form'; |
| | | |
| | | export type { EditRecordRow } from './src/components/renderEditable'; |
| | | export type { EditRecordRow } from './src/components/editable'; |
| | |
| | | <template #[item]="data" v-for="item in Object.keys($slots)"> |
| | | <slot :name="item" v-bind="data" /> |
| | | </template> |
| | | <template #[`header-${column.dataIndex}`] v-for="column in columns" :key="column.dataIndex"> |
| | | <HeaderCell :column="column" /> |
| | | </template> |
| | | </Table> |
| | | </div> |
| | | </template> |
| | | <script lang="ts"> |
| | | import type { BasicTableProps, TableActionType, SizeType, SorterResult } from './types/table'; |
| | | import { PaginationProps } from './types/pagination'; |
| | | import type { BasicTableProps, TableActionType, SizeType } from './types/table'; |
| | | |
| | | import { defineComponent, ref, computed, unref } from 'vue'; |
| | | import { Table } from 'ant-design-vue'; |
| | | import { BasicForm, useForm } from '/@/components/Form/index'; |
| | | |
| | | import { isFunction } from '/@/utils/is'; |
| | | |
| | | import { omit } from 'lodash-es'; |
| | | |
| | |
| | | import { createTableContext } from './hooks/useTableContext'; |
| | | import { useTableFooter } from './hooks/useTableFooter'; |
| | | import { useTableForm } from './hooks/useTableForm'; |
| | | import { useExpose } from '/@/hooks/core/useExpose'; |
| | | import { useDesign } from '/@/hooks/web/useDesign'; |
| | | |
| | | import { basicProps } from './props'; |
| | | import { useExpose } from '/@/hooks/core/useExpose'; |
| | | import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent'; |
| | | |
| | | import './style/index.less'; |
| | | import { useDesign } from '/@/hooks/web/useDesign'; |
| | | export default defineComponent({ |
| | | props: basicProps, |
| | | components: { Table, BasicForm }, |
| | | components: { |
| | | Table, |
| | | BasicForm, |
| | | HeaderCell: createAsyncComponent(() => import('./components/HeaderCell.vue')), |
| | | }, |
| | | emits: [ |
| | | 'fetch-success', |
| | | 'fetch-error', |
| | |
| | | 'row-contextmenu', |
| | | 'row-mouseenter', |
| | | 'row-mouseleave', |
| | | 'edit-end', |
| | | 'edit-cancel', |
| | | ], |
| | | setup(props, { attrs, emit, slots }) { |
| | | const tableElRef = ref<ComponentRef>(null); |
| | |
| | | |
| | | const { getLoading, setLoading } = useLoading(getProps); |
| | | const { getPaginationInfo, getPagination, setPagination } = usePagination(getProps); |
| | | const { |
| | | getSortFixedColumns, |
| | | getColumns, |
| | | setColumns, |
| | | getColumnsRef, |
| | | getCacheColumns, |
| | | } = useColumns(getProps, getPaginationInfo); |
| | | |
| | | const { |
| | | getDataSourceRef, |
| | | getDataSource, |
| | | setTableData, |
| | | fetch, |
| | | getRowKey, |
| | | reload, |
| | | getAutoCreateKey, |
| | | } = useDataSource( |
| | | getProps, |
| | | { |
| | | getPaginationInfo, |
| | | setLoading, |
| | | setPagination, |
| | | getFieldsValue: formActions.getFieldsValue, |
| | | }, |
| | | emit |
| | | ); |
| | | |
| | | const { |
| | | getRowSelection, |
| | |
| | | deleteSelectRowByKey, |
| | | setSelectedRowKeys, |
| | | } = useRowSelection(getProps, emit); |
| | | |
| | | const { |
| | | handleTableChange, |
| | | getDataSourceRef, |
| | | getDataSource, |
| | | setTableData, |
| | | fetch, |
| | | getRowKey, |
| | | reload, |
| | | getAutoCreateKey, |
| | | updateTableData, |
| | | } = useDataSource( |
| | | getProps, |
| | | { |
| | | getPaginationInfo, |
| | | setLoading, |
| | | setPagination, |
| | | getFieldsValue: formActions.getFieldsValue, |
| | | clearSelectedRowKeys, |
| | | }, |
| | | emit |
| | | ); |
| | | |
| | | const { getViewColumns, getColumns, setColumns, getColumnsRef, getCacheColumns } = useColumns( |
| | | getProps, |
| | | getPaginationInfo |
| | | ); |
| | | |
| | | const { getScrollRef, redoHeight } = useTableScroll( |
| | | getProps, |
| | |
| | | tableLayout: 'fixed', |
| | | rowSelection: unref(getRowSelectionRef), |
| | | rowKey: unref(getRowKey), |
| | | columns: unref(getSortFixedColumns), |
| | | columns: unref(getViewColumns), |
| | | pagination: unref(getPaginationInfo), |
| | | dataSource: unref(getDataSourceRef), |
| | | footer: unref(getFooterProps), |
| | |
| | | } |
| | | return !!unref(getDataSourceRef).length; |
| | | }); |
| | | |
| | | function handleTableChange( |
| | | pagination: PaginationProps, |
| | | // @ts-ignore |
| | | filters: Partial<Recordable<string[]>>, |
| | | sorter: SorterResult |
| | | ) { |
| | | const { clearSelectOnPageChange, sortFn } = unref(getProps); |
| | | if (clearSelectOnPageChange) { |
| | | clearSelectedRowKeys(); |
| | | } |
| | | setPagination(pagination); |
| | | |
| | | if (sorter && isFunction(sortFn)) { |
| | | const sortInfo = sortFn(sorter); |
| | | fetch({ sortInfo }); |
| | | return; |
| | | } |
| | | fetch(); |
| | | } |
| | | |
| | | function setProps(props: Partial<BasicTableProps>) { |
| | | innerPropsRef.value = { ...unref(innerPropsRef), ...props }; |
| | |
| | | getPaginationRef: getPagination, |
| | | getColumns, |
| | | getCacheColumns, |
| | | emit, |
| | | updateTableData, |
| | | getSize: () => { |
| | | return unref(getBindValues).size as SizeType; |
| | | }, |
| | |
| | | replaceFormSlotKey, |
| | | getFormSlotKeys, |
| | | prefixCls, |
| | | columns: getViewColumns, |
| | | }; |
| | | }, |
| | | }); |
| | |
| | | import { Component } from 'vue'; |
| | | import type { Component } from 'vue'; |
| | | |
| | | import { Input, Select, Checkbox, InputNumber, Switch } from 'ant-design-vue'; |
| | | |
| | | import { ComponentType } from './types/componentType'; |
| | | import type { ComponentType } from './types/componentType'; |
| | | import { ApiSelect } from '/@/components/Form'; |
| | | |
| | | const componentMap = new Map<ComponentType, Component>(); |
| | | |
| | | componentMap.set('Input', Input); |
| | | componentMap.set('InputPassword', Input.Password); |
| | | componentMap.set('InputNumber', InputNumber); |
| | | |
| | | componentMap.set('Select', Select); |
| | | componentMap.set('ApiSelect', ApiSelect); |
| | | componentMap.set('Switch', Switch); |
| | | componentMap.set('Checkbox', Checkbox); |
| | | componentMap.set('CheckboxGroup', Checkbox.Group); |
| | | |
| | | export function add(compName: ComponentType, component: Component) { |
| | | componentMap.set(compName, component); |
| | |
| | | <template> |
| | | <span> |
| | | <slot /> |
| | | {{ title }} |
| | | <FormOutlined class="ml-2" /> |
| | | <FormOutlined /> |
| | | </span> |
| | | </template> |
| | | <script lang="ts"> |
New file |
| | |
| | | <template> |
| | | <EditTableHeaderCell v-if="getIsEdit"> |
| | | {{ getTitle }} |
| | | </EditTableHeaderCell> |
| | | <span v-else>{{ getTitle }}</span> |
| | | <BasicHelp v-if="getHelpMessage" :text="getHelpMessage" :class="`${prefixCls}__help`" /> |
| | | </template> |
| | | <script lang="ts"> |
| | | import type { PropType } from 'vue'; |
| | | import type { BasicColumn } from '../types/table'; |
| | | |
| | | import { defineComponent, computed } from 'vue'; |
| | | |
| | | import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent'; |
| | | import { useDesign } from '/@/hooks/web/useDesign'; |
| | | export default defineComponent({ |
| | | name: 'TableHeaderCell', |
| | | components: { |
| | | EditTableHeaderCell: createAsyncComponent(() => import('./EditTableHeaderIcon.vue')), |
| | | BasicHelp: createAsyncComponent(() => import('/@/components/Basic/src/BasicHelp.vue')), |
| | | }, |
| | | props: { |
| | | column: { |
| | | type: Object as PropType<BasicColumn>, |
| | | default: {}, |
| | | }, |
| | | }, |
| | | setup(props) { |
| | | const { prefixCls } = useDesign('basic-table-header-cell'); |
| | | const getIsEdit = computed(() => { |
| | | return !!props.column?.edit; |
| | | }); |
| | | |
| | | const getTitle = computed(() => { |
| | | return props.column?.customTitle; |
| | | }); |
| | | |
| | | const getHelpMessage = computed(() => { |
| | | return props.column?.helpMessage; |
| | | }); |
| | | |
| | | return { prefixCls, getIsEdit, getTitle, getHelpMessage }; |
| | | }, |
| | | }); |
| | | </script> |
| | | <style lang="less"> |
| | | @prefix-cls: ~'@{namespace}-basic-table-header-cell'; |
| | | |
| | | .@{prefix-cls} { |
| | | &__help { |
| | | margin-left: 8px; |
| | | color: rgba(0, 0, 0, 0.65) !important; |
| | | } |
| | | } |
| | | </style> |
| | |
| | | <template> |
| | | <div :class="[prefixCls, getAlign]"> |
| | | <template v-for="(action, index) in getActions" :key="`${index}`"> |
| | | <template v-for="(action, index) in getActions" :key="`${index}-${action.label}`"> |
| | | <PopConfirmButton v-bind="action"> |
| | | <Icon :icon="action.icon" class="mr-1" v-if="action.icon" /> |
| | | {{ action.label }} |
| | | </PopConfirmButton> |
| | | <Divider type="vertical" v-if="divider && index < getActions.length" /> |
| | | </template> |
| | | |
| | | <Dropdown :trigger="['hover']" :dropMenuList="getDropList"> |
| | | <Dropdown :trigger="['hover']" :dropMenuList="getDropList" v-if="dropDownActions"> |
| | | <slot name="more" /> |
| | | <a-button type="link" size="small" v-if="!$slots.more"> |
| | | <MoreOutlined class="icon-more" /> |
| | |
| | | }); |
| | | |
| | | const getDropList = computed(() => { |
| | | return props.dropDownActions.map((action, index) => { |
| | | return (props.dropDownActions || []).map((action, index) => { |
| | | const { label } = action; |
| | | return { |
| | | ...action, |
New file |
| | |
| | | import type { FunctionalComponent, defineComponent } from 'vue'; |
| | | import type { ComponentType } from '../../types/componentType'; |
| | | import { componentMap } from '/@/components/Table/src/componentMap'; |
| | | |
| | | import { Popover } from 'ant-design-vue'; |
| | | import { h } from 'vue'; |
| | | |
| | | export interface ComponentProps { |
| | | component: ComponentType; |
| | | rule: boolean; |
| | | popoverVisible: boolean; |
| | | ruleMessage: string; |
| | | } |
| | | |
| | | export const CellComponent: FunctionalComponent = ( |
| | | { component = 'Input', rule = true, ruleMessage, popoverVisible }: ComponentProps, |
| | | { attrs } |
| | | ) => { |
| | | const Comp = componentMap.get(component) as typeof defineComponent; |
| | | |
| | | const DefaultComp = h(Comp, attrs); |
| | | if (!rule) { |
| | | return DefaultComp; |
| | | } |
| | | return h( |
| | | Popover, |
| | | { overlayClassName: 'edit-cell-rule-popover', visible: !!popoverVisible }, |
| | | { |
| | | default: () => DefaultComp, |
| | | content: () => ruleMessage, |
| | | } |
| | | ); |
| | | }; |
New file |
| | |
| | | <template> |
| | | <div :class="prefixCls"> |
| | | <div v-show="!isEdit" :class="`${prefixCls}__normal`" @click="handleEdit"> |
| | | {{ value || ' ' }} |
| | | <FormOutlined :class="`${prefixCls}__normal-icon`" v-if="!column.editRow" /> |
| | | </div> |
| | | |
| | | <div v-if="isEdit" :class="`${prefixCls}__wrapper`" v-click-outside="onClickOutside"> |
| | | <CellComponent |
| | | v-bind="getComponentProps" |
| | | :component="getComponent" |
| | | :style="getWrapperStyle" |
| | | :popoverVisible="getRuleVisible" |
| | | :rule="getRule" |
| | | :ruleMessage="ruleMessage" |
| | | size="small" |
| | | ref="elRef" |
| | | @change="handleChange" |
| | | @options-change="handleOptionsChange" |
| | | @pressEnter="handleSubmit" |
| | | > |
| | | </CellComponent> |
| | | <div :class="`${prefixCls}__action`" v-if="!getRowEditable"> |
| | | <CheckOutlined :class="[`${prefixCls}__icon`, 'mx-2']" @click="handleSubmit" /> |
| | | <CloseOutlined :class="`${prefixCls}__icon `" @click="handleCancel" /> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | <script lang="ts"> |
| | | import type { CSSProperties, PropType } from 'vue'; |
| | | import type { BasicColumn } from '../../types/table'; |
| | | |
| | | import { defineComponent, ref, unref, nextTick, computed, watchEffect, toRaw } from 'vue'; |
| | | import { FormOutlined, CloseOutlined, CheckOutlined } from '@ant-design/icons-vue'; |
| | | |
| | | import { useDesign } from '/@/hooks/web/useDesign'; |
| | | import { isString, isBoolean, isFunction, isNumber, isArray } from '/@/utils/is'; |
| | | import clickOutside from '/@/directives/clickOutside'; |
| | | |
| | | import { CellComponent } from './CellComponent'; |
| | | import { useTableContext } from '../../hooks/useTableContext'; |
| | | import { propTypes } from '/@/utils/propTypes'; |
| | | import { createPlaceholderMessage } from './helper'; |
| | | |
| | | import type { EditRecordRow } from './index'; |
| | | |
| | | export default defineComponent({ |
| | | name: 'EditableCell', |
| | | components: { FormOutlined, CloseOutlined, CheckOutlined, CellComponent }, |
| | | props: { |
| | | value: { |
| | | type: [String, Number, Boolean, Object] as PropType<string | number | boolean | Recordable>, |
| | | default: '', |
| | | }, |
| | | record: { |
| | | type: Object as PropType<EditRecordRow>, |
| | | }, |
| | | column: { |
| | | type: Object as PropType<BasicColumn>, |
| | | default: {}, |
| | | }, |
| | | index: propTypes.number, |
| | | }, |
| | | directives: { |
| | | clickOutside, |
| | | }, |
| | | |
| | | setup(props) { |
| | | const table = useTableContext(); |
| | | const isEdit = ref(false); |
| | | const elRef = ref<any>(null); |
| | | const ruleVisible = ref(false); |
| | | const ruleMessage = ref(''); |
| | | const optionsRef = ref<LabelValueOptions>([]); |
| | | const currentValueRef = ref<any>(props.value); |
| | | const defaultValueRef = ref<any>(props.value); |
| | | |
| | | const { prefixCls } = useDesign('editable-cell'); |
| | | |
| | | const getComponent = computed(() => props.column?.editComponent || 'Input'); |
| | | const getRule = computed(() => props.column?.editRule); |
| | | |
| | | const getRuleVisible = computed(() => { |
| | | return unref(ruleMessage) && unref(ruleVisible); |
| | | }); |
| | | |
| | | const getIsCheckComp = computed(() => { |
| | | const component = unref(getComponent); |
| | | return ['Checkbox', 'Switch'].includes(component); |
| | | }); |
| | | |
| | | const getComponentProps = computed(() => { |
| | | const compProps = props.column?.editComponentProps ?? {}; |
| | | const component = unref(getComponent); |
| | | const apiSelectProps: Recordable = {}; |
| | | if (component === 'ApiSelect') { |
| | | apiSelectProps.cache = true; |
| | | } |
| | | |
| | | const isCheckValue = unref(getIsCheckComp); |
| | | |
| | | const valueField = isCheckValue ? 'checked' : 'value'; |
| | | const val = unref(currentValueRef); |
| | | |
| | | const value = isCheckValue ? (isNumber(val) && isBoolean(val) ? val : !!val) : val; |
| | | |
| | | return { |
| | | placeholder: createPlaceholderMessage(unref(getComponent)), |
| | | ...apiSelectProps, |
| | | ...compProps, |
| | | [valueField]: value, |
| | | }; |
| | | }); |
| | | |
| | | const getValues = computed(() => { |
| | | const { editComponentProps, editValueMap } = props.column; |
| | | |
| | | const value = unref(currentValueRef); |
| | | |
| | | if (editValueMap && isFunction(editValueMap)) { |
| | | return editValueMap(value); |
| | | } |
| | | |
| | | const component = unref(getComponent); |
| | | if (!component.includes('Select')) { |
| | | return value; |
| | | } |
| | | const options: LabelValueOptions = editComponentProps?.options ?? (unref(optionsRef) || []); |
| | | const option = options.find((item) => `${item.value}` === `${value}`); |
| | | return option?.label; |
| | | }); |
| | | |
| | | const getWrapperStyle = computed( |
| | | (): CSSProperties => { |
| | | if (unref(getIsCheckComp) || unref(getRowEditable)) { |
| | | return {}; |
| | | } |
| | | return { |
| | | width: 'calc(100% - 48px)', |
| | | }; |
| | | } |
| | | ); |
| | | |
| | | const getRowEditable = computed(() => { |
| | | const { editable } = props.record || {}; |
| | | return !!editable; |
| | | }); |
| | | |
| | | watchEffect(() => { |
| | | defaultValueRef.value = props.value; |
| | | }); |
| | | |
| | | watchEffect(() => { |
| | | const { editable } = props.column; |
| | | if (isBoolean(editable) || isBoolean(unref(getRowEditable))) { |
| | | isEdit.value = !!editable || unref(getRowEditable); |
| | | } |
| | | }); |
| | | |
| | | function handleEdit() { |
| | | if (unref(getRowEditable) || unref(props.column?.editRow)) return; |
| | | ruleMessage.value = ''; |
| | | isEdit.value = true; |
| | | nextTick(() => { |
| | | const el = unref(elRef); |
| | | el?.focus?.(); |
| | | }); |
| | | } |
| | | |
| | | async function handleChange(e: any) { |
| | | const component = unref(getComponent); |
| | | if (e?.target && Reflect.has(e.target, 'value')) { |
| | | currentValueRef.value = (e as ChangeEvent).target.value; |
| | | } |
| | | if (component === 'Checkbox') { |
| | | currentValueRef.value = (e as ChangeEvent).target.checked; |
| | | } else if (isString(e) || isBoolean(e) || isNumber(e)) { |
| | | currentValueRef.value = e; |
| | | } |
| | | handleSubmiRule(); |
| | | } |
| | | |
| | | async function handleSubmiRule() { |
| | | const { column, record } = props; |
| | | const { editRule } = column; |
| | | const currentValue = unref(currentValueRef); |
| | | |
| | | if (editRule) { |
| | | if (isBoolean(editRule) && !currentValue && !isNumber(currentValue)) { |
| | | ruleVisible.value = true; |
| | | const component = unref(getComponent); |
| | | const message = createPlaceholderMessage(component); |
| | | ruleMessage.value = message; |
| | | return false; |
| | | } |
| | | if (isFunction(editRule)) { |
| | | const res = await editRule(currentValue, record as Recordable); |
| | | if (!!res) { |
| | | ruleMessage.value = res; |
| | | ruleVisible.value = true; |
| | | return false; |
| | | } else { |
| | | ruleMessage.value = ''; |
| | | return true; |
| | | } |
| | | } |
| | | } |
| | | ruleMessage.value = ''; |
| | | return true; |
| | | } |
| | | |
| | | async function handleSubmit() { |
| | | const isPass = await handleSubmiRule(); |
| | | if (!isPass) return false; |
| | | const { column, index } = props; |
| | | const { key, dataIndex } = column; |
| | | // const value = unref(currentValueRef); |
| | | if (!key || !dataIndex) return; |
| | | const dataKey = (dataIndex || key) as string; |
| | | |
| | | const record = await table.updateTableData(index, dataKey, unref(getValues)); |
| | | table.emit?.('edit-end', { record, index, key, value: unref(currentValueRef) }); |
| | | isEdit.value = false; |
| | | } |
| | | |
| | | function handleCancel() { |
| | | isEdit.value = false; |
| | | currentValueRef.value = defaultValueRef.value; |
| | | table.emit?.('edit-cancel', unref(currentValueRef)); |
| | | } |
| | | |
| | | function onClickOutside() { |
| | | if (props.column?.editable || unref(getRowEditable)) { |
| | | return; |
| | | } |
| | | const component = unref(getComponent); |
| | | |
| | | if (component.includes('Input')) { |
| | | handleCancel(); |
| | | } |
| | | } |
| | | |
| | | // only ApiSelect |
| | | function handleOptionsChange(options: LabelValueOptions) { |
| | | optionsRef.value = options; |
| | | } |
| | | |
| | | function initCbs(cbs: 'submitCbs' | 'validCbs' | 'cancelCbs', handle: Fn) { |
| | | if (props.record) { |
| | | /* eslint-disable */ |
| | | isArray(props.record[cbs]) |
| | | ? props.record[cbs].push(handle) |
| | | : (props.record[cbs] = [handle]); |
| | | } |
| | | } |
| | | |
| | | if (props.record) { |
| | | initCbs('submitCbs', handleSubmit); |
| | | initCbs('validCbs', handleSubmiRule); |
| | | initCbs('cancelCbs', handleCancel); |
| | | |
| | | /* eslint-disable */ |
| | | props.record.onCancelEdit = () => { |
| | | isArray(props.record?.cancelCbs) && props.record?.cancelCbs.forEach((fn) => fn()); |
| | | }; |
| | | /* eslint-disable */ |
| | | props.record.onSubmitEdit = async () => { |
| | | if (isArray(props.record?.submitCbs)) { |
| | | const validFns = props.record?.validCbs || []; |
| | | |
| | | const res = await Promise.all(validFns.map((fn) => fn())); |
| | | const pass = res.every((item) => !!item); |
| | | |
| | | if (!pass) return; |
| | | const submitFns = props.record?.submitCbs || []; |
| | | submitFns.forEach((fn) => fn()); |
| | | return true; |
| | | } |
| | | // isArray(props.record?.submitCbs) && props.record?.submitCbs.forEach((fn) => fn()); |
| | | }; |
| | | } |
| | | |
| | | return { |
| | | isEdit, |
| | | prefixCls, |
| | | handleEdit, |
| | | currentValueRef, |
| | | handleSubmit, |
| | | handleChange, |
| | | handleCancel, |
| | | elRef, |
| | | getComponent, |
| | | getRule, |
| | | onClickOutside, |
| | | ruleMessage, |
| | | getRuleVisible, |
| | | getComponentProps, |
| | | handleOptionsChange, |
| | | getWrapperStyle, |
| | | getRowEditable, |
| | | }; |
| | | }, |
| | | }); |
| | | </script> |
| | | <style lang="less"> |
| | | @prefix-cls: ~'@{namespace}-editable-cell'; |
| | | |
| | | .edit-cell-rule-popover { |
| | | // .ant-popover-arrow { |
| | | // // border-color: transparent @error-color @error-color transparent !important; |
| | | // } |
| | | |
| | | .ant-popover-inner-content { |
| | | padding: 4px 8px; |
| | | color: @error-color; |
| | | // border: 1px solid @error-color; |
| | | border-radius: 2px; |
| | | } |
| | | } |
| | | .@{prefix-cls} { |
| | | position: relative; |
| | | |
| | | &__wrapper { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | } |
| | | |
| | | &__icon { |
| | | &:hover { |
| | | transform: scale(1.2); |
| | | |
| | | svg { |
| | | color: @primary-color; |
| | | } |
| | | } |
| | | } |
| | | |
| | | &__normal { |
| | | padding-right: 48px; |
| | | |
| | | &-icon { |
| | | position: absolute; |
| | | top: 4px; |
| | | right: 0; |
| | | display: none; |
| | | width: 20px; |
| | | cursor: pointer; |
| | | } |
| | | } |
| | | |
| | | &:hover { |
| | | .@{prefix-cls}__normal-icon { |
| | | display: inline-block; |
| | | } |
| | | } |
| | | } |
| | | </style> |
New file |
| | |
| | | import { ComponentType } from '../../types/componentType'; |
| | | import { useI18n } from '/@/hooks/web/useI18n'; |
| | | |
| | | const { t } = useI18n(); |
| | | |
| | | /** |
| | | * @description: 生成placeholder |
| | | */ |
| | | export function createPlaceholderMessage(component: ComponentType) { |
| | | if (component.includes('Input')) { |
| | | return t('component.form.input'); |
| | | } |
| | | if (component.includes('Picker')) { |
| | | return t('component.form.choose'); |
| | | } |
| | | |
| | | if ( |
| | | component.includes('Select') || |
| | | component.includes('Checkbox') || |
| | | component.includes('Radio') || |
| | | component.includes('Switch') |
| | | ) { |
| | | return t('component.form.choose'); |
| | | } |
| | | return ''; |
| | | } |
New file |
| | |
| | | import type { BasicColumn } from '/@/components/Table/src/types/table'; |
| | | |
| | | import { h } from 'vue'; |
| | | |
| | | import EditableCell from './EditableCell.vue'; |
| | | |
| | | interface Params { |
| | | text: string; |
| | | record: Recordable; |
| | | index: number; |
| | | } |
| | | |
| | | export function renderEditCell(column: BasicColumn) { |
| | | return ({ text: value, record, index }: Params) => { |
| | | record.onEdit = async (edit: boolean, submit = false) => { |
| | | if (!submit) { |
| | | record.editable = edit; |
| | | } |
| | | |
| | | if (!edit && submit) { |
| | | const res = await record.onSubmitEdit?.(); |
| | | if (res) { |
| | | record.editable = false; |
| | | return true; |
| | | } |
| | | return false; |
| | | } |
| | | // cancel |
| | | if (!edit && !submit) { |
| | | record.onCancelEdit?.(); |
| | | } |
| | | return true; |
| | | }; |
| | | |
| | | return h(EditableCell, { |
| | | value, |
| | | record, |
| | | column, |
| | | index, |
| | | }); |
| | | }; |
| | | } |
| | | |
| | | export type EditRecordRow<T = Hash<any>> = { |
| | | onEdit: (editable: boolean, submit?: boolean) => Promise<boolean>; |
| | | editable: boolean; |
| | | onCancel: Fn; |
| | | onSubmit: Fn; |
| | | submitCbs: Fn[]; |
| | | cancelCbs: Fn[]; |
| | | validCbs: Fn[]; |
| | | } & T; |
| | |
| | | const ret: Options[] = []; |
| | | table.getColumns({ ignoreIndex: true, ignoreAction: true }).forEach((item) => { |
| | | ret.push({ |
| | | label: item.title as string, |
| | | label: (item.title as string) || (item.customTitle as string), |
| | | value: (item.dataIndex || item.title) as string, |
| | | ...item, |
| | | }); |
| | |
| | | }; |
| | | } |
| | | |
| | | export function DEFAULT_FILTER_FN(data: Partial<Recordable<string[]>>) { |
| | | return data; |
| | | } |
| | | |
| | | // 表格单元格默认布局 |
| | | export const DEFAULT_ALIGN = 'center'; |
| | | |
| | |
| | | import { BasicColumn, BasicTableProps, GetColumnsParams } from '../types/table'; |
| | | import { PaginationProps } from '../types/pagination'; |
| | | import type { BasicColumn, BasicTableProps, CellFormat, GetColumnsParams } from '../types/table'; |
| | | import type { PaginationProps } from '../types/pagination'; |
| | | import { unref, ComputedRef, Ref, computed, watchEffect, ref, toRaw } from 'vue'; |
| | | import { isBoolean, isArray, isString } from '/@/utils/is'; |
| | | import { isBoolean, isArray, isString, isObject } from '/@/utils/is'; |
| | | import { DEFAULT_ALIGN, PAGE_SIZE, INDEX_COLUMN_FLAG, ACTION_COLUMN_FLAG } from '../const'; |
| | | import { useI18n } from '/@/hooks/web/useI18n'; |
| | | import { isEqual, cloneDeep } from 'lodash-es'; |
| | | import { isFunction } from '/@/utils/is'; |
| | | import { formatToDate } from '/@/utils/dateUtil'; |
| | | import { renderEditCell } from '../components/editable'; |
| | | |
| | | const { t } = useI18n(); |
| | | |
| | |
| | | return columns; |
| | | }); |
| | | |
| | | const getSortFixedColumns = computed(() => { |
| | | return useFixedColumn(unref(getColumnsRef)); |
| | | const getViewColumns = computed(() => { |
| | | const viewColumns = sortFixedColumn(unref(getColumnsRef)); |
| | | |
| | | viewColumns.forEach((column) => { |
| | | const { slots, dataIndex, customRender, format, edit, editRow, flag } = column; |
| | | |
| | | if (!slots || !slots?.title) { |
| | | column.slots = { title: `header-${dataIndex}`, ...(slots || {}) }; |
| | | column.customTitle = column.title; |
| | | Reflect.deleteProperty(column, 'title'); |
| | | } |
| | | const isDefaultAction = [INDEX_COLUMN_FLAG, ACTION_COLUMN_FLAG].includes(flag!); |
| | | if (!customRender && format && !edit && !isDefaultAction) { |
| | | column.customRender = ({ text, record, index }) => { |
| | | return formatCell(text, format, record, index); |
| | | }; |
| | | } |
| | | |
| | | // edit table |
| | | if ((edit || editRow) && !isDefaultAction) { |
| | | column.customRender = renderEditCell(column); |
| | | } |
| | | }); |
| | | return viewColumns; |
| | | }); |
| | | |
| | | watchEffect(() => { |
| | |
| | | } |
| | | |
| | | if (sort) { |
| | | columns = useFixedColumn(columns); |
| | | columns = sortFixedColumn(columns); |
| | | } |
| | | |
| | | return columns; |
| | |
| | | return cacheColumns; |
| | | } |
| | | |
| | | return { getColumnsRef, getCacheColumns, getColumns, setColumns, getSortFixedColumns }; |
| | | return { getColumnsRef, getCacheColumns, getColumns, setColumns, getViewColumns }; |
| | | } |
| | | |
| | | export function useFixedColumn(columns: BasicColumn[]) { |
| | | function sortFixedColumn(columns: BasicColumn[]) { |
| | | const fixedLeftColumns: BasicColumn[] = []; |
| | | const fixedRightColumns: BasicColumn[] = []; |
| | | const defColumns: BasicColumn[] = []; |
| | |
| | | |
| | | return resultColumns; |
| | | } |
| | | |
| | | // format cell |
| | | export function formatCell(text: string, format: CellFormat, record: Recordable, index: number) { |
| | | if (!format) { |
| | | return text; |
| | | } |
| | | |
| | | // custom function |
| | | if (isFunction(format)) { |
| | | return format(text, record, index); |
| | | } |
| | | |
| | | try { |
| | | // date type |
| | | const DATE_FORMAT_PREFIX = 'date|'; |
| | | if (isString(format) && format.startsWith(DATE_FORMAT_PREFIX)) { |
| | | const dateFormat = format.replace(DATE_FORMAT_PREFIX, ''); |
| | | |
| | | if (!dateFormat) { |
| | | return text; |
| | | } |
| | | return formatToDate(text, dateFormat); |
| | | } |
| | | |
| | | // enum |
| | | if (isObject(format) && Reflect.has(format, 'size')) { |
| | | return format.get(text); |
| | | } |
| | | } catch (error) { |
| | | return text; |
| | | } |
| | | } |
| | |
| | | import type { BasicTableProps, FetchParams } from '../types/table'; |
| | | import type { BasicTableProps, FetchParams, SorterResult } from '../types/table'; |
| | | import type { PaginationProps } from '../types/pagination'; |
| | | |
| | | import { ref, unref, ComputedRef, computed, onMounted, watchEffect } from 'vue'; |
| | | import { ref, unref, ComputedRef, computed, onMounted, watchEffect, reactive } from 'vue'; |
| | | |
| | | import { useTimeoutFn } from '/@/hooks/core/useTimeout'; |
| | | |
| | |
| | | setPagination: (info: Partial<PaginationProps>) => void; |
| | | setLoading: (loading: boolean) => void; |
| | | getFieldsValue: () => Recordable; |
| | | clearSelectedRowKeys: () => void; |
| | | } |
| | | |
| | | interface SearchState { |
| | | sortInfo: Recordable; |
| | | filterInfo: Record<string, string[]>; |
| | | } |
| | | export function useDataSource( |
| | | propsRef: ComputedRef<BasicTableProps>, |
| | | { getPaginationInfo, setPagination, setLoading, getFieldsValue }: ActionType, |
| | | { |
| | | getPaginationInfo, |
| | | setPagination, |
| | | setLoading, |
| | | getFieldsValue, |
| | | clearSelectedRowKeys, |
| | | }: ActionType, |
| | | emit: EmitType |
| | | ) { |
| | | const searchState = reactive<SearchState>({ |
| | | sortInfo: {}, |
| | | filterInfo: {}, |
| | | }); |
| | | const dataSourceRef = ref<Recordable[]>([]); |
| | | |
| | | watchEffect(() => { |
| | | const { dataSource, api } = unref(propsRef); |
| | | !api && dataSource && (dataSourceRef.value = dataSource); |
| | | }); |
| | | |
| | | function handleTableChange( |
| | | pagination: PaginationProps, |
| | | filters: Partial<Recordable<string[]>>, |
| | | sorter: SorterResult |
| | | ) { |
| | | const { clearSelectOnPageChange, sortFn, filterFn } = unref(propsRef); |
| | | if (clearSelectOnPageChange) { |
| | | clearSelectedRowKeys(); |
| | | } |
| | | setPagination(pagination); |
| | | |
| | | const params: Recordable = {}; |
| | | if (sorter && isFunction(sortFn)) { |
| | | const sortInfo = sortFn(sorter); |
| | | searchState.sortInfo = sortInfo; |
| | | params.sortInfo = sortInfo; |
| | | } |
| | | |
| | | if (filters && isFunction(filterFn)) { |
| | | const filterInfo = filterFn(filters); |
| | | searchState.filterInfo = filterInfo; |
| | | params.filterInfo = filterInfo; |
| | | } |
| | | fetch(params); |
| | | } |
| | | |
| | | function setTableKey(items: any[]) { |
| | | if (!items || !Array.isArray(items)) return; |
| | |
| | | return unref(dataSourceRef); |
| | | }); |
| | | |
| | | async function updateTableData(index: number, key: string, value: any) { |
| | | const record = dataSourceRef.value[index]; |
| | | if (record) { |
| | | dataSourceRef.value[index][key] = value; |
| | | } |
| | | return dataSourceRef.value[index]; |
| | | } |
| | | |
| | | async function fetch(opt?: FetchParams) { |
| | | const { api, searchInfo, fetchSetting, beforeFetch, afterFetch, useSearchForm } = unref( |
| | | propsRef |
| | |
| | | pageParams[sizeField] = pageSize; |
| | | } |
| | | |
| | | const { sortInfo = {}, filterInfo } = searchState; |
| | | |
| | | let params: Recordable = { |
| | | ...pageParams, |
| | | ...(useSearchForm ? getFieldsValue() : {}), |
| | |
| | | ...(opt ? opt.searchInfo : {}), |
| | | ...(opt ? opt.sortInfo : {}), |
| | | ...(opt ? opt.filterInfo : {}), |
| | | ...sortInfo, |
| | | ...filterInfo, |
| | | }; |
| | | if (beforeFetch && isFunction(beforeFetch)) { |
| | | params = beforeFetch(params) || params; |
| | |
| | | getAutoCreateKey, |
| | | fetch, |
| | | reload, |
| | | updateTableData, |
| | | handleTableChange, |
| | | }; |
| | | } |
| | |
| | | import type { BasicTableProps, TableActionType, FetchParams, BasicColumn } from '../types/table'; |
| | | import type { PaginationProps } from '../types/pagination'; |
| | | import type { DynamicProps } from '/@/types/utils'; |
| | | import { getDynamicProps } from '/@/utils'; |
| | | |
| | | import { ref, onUnmounted, unref } from 'vue'; |
| | | import { isProdMode } from '/@/utils/env'; |
| | | import { isInSetup } from '/@/utils/helper/vueHelper'; |
| | | import { error } from '/@/utils/log'; |
| | | import { watchEffect } from 'vue'; |
| | | import type { FormActionType } from '/@/components/Form'; |
| | | |
| | | type Props = Partial<DynamicProps<BasicTableProps>>; |
| | | |
| | | export function useTable( |
| | | tableProps?: Partial<BasicTableProps> |
| | | ): [(instance: TableActionType) => void, TableActionType] { |
| | | tableProps?: Props |
| | | ): [(instance: TableActionType, formInstance: FormActionType) => void, TableActionType] { |
| | | isInSetup(); |
| | | |
| | | const tableRef = ref<Nullable<TableActionType>>(null); |
| | | const loadedRef = ref<Nullable<boolean>>(false); |
| | | const formRef = ref<Nullable<FormActionType>>(null); |
| | | |
| | | function register(instance: TableActionType) { |
| | | function register(instance: TableActionType, formInstance: FormActionType) { |
| | | isProdMode() && |
| | | onUnmounted(() => { |
| | | tableRef.value = null; |
| | |
| | | return; |
| | | } |
| | | tableRef.value = instance; |
| | | tableProps && instance.setProps(tableProps); |
| | | formRef.value = formInstance; |
| | | // tableProps && instance.setProps(tableProps); |
| | | loadedRef.value = true; |
| | | |
| | | watchEffect(() => { |
| | | tableProps && instance.setProps(getDynamicProps(tableProps)); |
| | | }); |
| | | } |
| | | |
| | | function getTableInstance(): TableActionType { |
| | | const table = unref(tableRef); |
| | | if (!table) { |
| | | throw new Error('table is undefined!'); |
| | | error( |
| | | 'The table instance has not been obtained yet, please make sure the table is presented when performing the table operation!' |
| | | ); |
| | | } |
| | | return table; |
| | | return table as TableActionType; |
| | | } |
| | | |
| | | const methods: TableActionType = { |
| | | reload: (opt?: FetchParams) => { |
| | | const methods: TableActionType & { |
| | | getForm: () => FormActionType; |
| | | } = { |
| | | reload: async (opt?: FetchParams) => { |
| | | getTableInstance().reload(opt); |
| | | }, |
| | | setProps: (props: Partial<BasicTableProps>) => { |
| | |
| | | }, |
| | | getColumns: ({ ignoreIndex = false }: { ignoreIndex?: boolean } = {}) => { |
| | | const columns = getTableInstance().getColumns({ ignoreIndex }) || []; |
| | | |
| | | return columns; |
| | | }, |
| | | setColumns: (columns: BasicColumn[]) => { |
| | |
| | | getSize: () => { |
| | | return getTableInstance().getSize(); |
| | | }, |
| | | } as TableActionType; |
| | | updateTableData: (index: number, key: string, value: any) => { |
| | | return getTableInstance().updateTableData(index, key, value); |
| | | }, |
| | | getRowSelection: () => { |
| | | return getTableInstance().getRowSelection(); |
| | | }, |
| | | getCacheColumns: () => { |
| | | return getTableInstance().getCacheColumns(); |
| | | }, |
| | | getForm: () => { |
| | | return unref(formRef) as FormActionType; |
| | | }, |
| | | }; |
| | | |
| | | return [register, methods]; |
| | | } |
| | |
| | | width += 60; |
| | | } |
| | | |
| | | // TODO props |
| | | // TODO propsdth ?? 0; |
| | | const NORMAL_WIDTH = 150; |
| | | |
| | | const columns = unref(columnsRef); |
| | |
| | | if (len !== 0) { |
| | | width += len * NORMAL_WIDTH; |
| | | } |
| | | return width; |
| | | |
| | | const table = unref(tableElRef); |
| | | const tableWidth = table?.$el?.offsetWidth ?? 0; |
| | | return tableWidth > width ? tableWidth - 24 : width; |
| | | }); |
| | | |
| | | const getScrollRef = computed(() => { |
| | |
| | | TableRowSelection, |
| | | } from './types/table'; |
| | | import type { FormProps } from '/@/components/Form'; |
| | | import { DEFAULT_SORT_FN, FETCH_SETTING } from './const'; |
| | | import { DEFAULT_FILTER_FN, DEFAULT_SORT_FN, FETCH_SETTING } from './const'; |
| | | import { propTypes } from '/@/utils/propTypes'; |
| | | |
| | | // 注释看 types/table |
| | | export const basicProps = { |
| | | clickToRowSelect: propTypes.bool.def(true), |
| | | |
| | | tableSetting: { |
| | | type: Object as PropType<TableSetting>, |
| | | }, |
| | | |
| | | inset: propTypes.bool, |
| | | |
| | | sortFn: { |
| | | type: Function as PropType<(sortInfo: SorterResult) => any>, |
| | | default: DEFAULT_SORT_FN, |
| | | }, |
| | | |
| | | filterFn: { |
| | | type: Function as PropType<(data: Partial<Recordable<string[]>>) => any>, |
| | | default: DEFAULT_FILTER_FN, |
| | | }, |
| | | |
| | | showTableSetting: propTypes.bool, |
| | | autoCreateKey: propTypes.bool.def(true), |
| | | striped: propTypes.bool.def(true), |
| | |
| | | overflow-y: scroll !important; |
| | | } |
| | | |
| | | .ant-table-fixed-right .ant-table-header { |
| | | border-left: 1px solid @border-color !important; |
| | | .ant-table-fixed-right { |
| | | right: -1px; |
| | | |
| | | .ant-table-fixed { |
| | | border-bottom: none; |
| | | .ant-table-header { |
| | | border-left: 1px solid @border-color !important; |
| | | |
| | | .ant-table-thead th { |
| | | background: rgb(241, 243, 244); |
| | | .ant-table-fixed { |
| | | border-bottom: none; |
| | | |
| | | .ant-table-thead th { |
| | | background: rgb(241, 243, 244); |
| | | } |
| | | } |
| | | } |
| | | } |
| | |
| | | export type ComponentType = |
| | | | 'Input' |
| | | | 'InputPassword' |
| | | | 'InputNumber' |
| | | | 'Select' |
| | | | 'ApiSelect' |
| | | | 'Checkbox' |
| | | | 'CheckboxGroup' |
| | | | 'Switch'; |
| | |
| | | TableRowSelection as ITableRowSelection, |
| | | } from 'ant-design-vue/lib/table/interface'; |
| | | import { ComponentType } from './componentType'; |
| | | import { VueNode } from '/@/utils/propTypes'; |
| | | // import { ColumnProps } from './column'; |
| | | export declare type SortOrder = 'ascend' | 'descend'; |
| | | export interface TableCurrentDataSource<T = any> { |
| | | export interface TableCurrentDataSource<T = Recordable> { |
| | | currentDataSource: T[]; |
| | | } |
| | | |
| | |
| | | children?: any; |
| | | } |
| | | |
| | | export interface TableCustomRecord<T = any> { |
| | | export interface TableCustomRecord<T = Recordable> { |
| | | record?: T; |
| | | index?: number; |
| | | } |
| | |
| | | columnKey: string; |
| | | } |
| | | |
| | | export interface RenderEditableCellParams { |
| | | dataIndex: string; |
| | | component?: ComponentType; |
| | | componentProps?: any; |
| | | placeholder?: string; |
| | | } |
| | | |
| | | export interface FetchParams { |
| | | searchInfo?: any; |
| | | searchInfo?: Recordable; |
| | | page?: number; |
| | | sortInfo?: any; |
| | | filterInfo?: any; |
| | | sortInfo?: Recordable; |
| | | filterInfo?: Recordable; |
| | | } |
| | | |
| | | export interface GetColumnsParams { |
| | |
| | | |
| | | export interface TableActionType { |
| | | reload: (opt?: FetchParams) => Promise<void>; |
| | | getSelectRows: <T = any>() => T[]; |
| | | getSelectRows: <T = Recordable>() => T[]; |
| | | clearSelectedRowKeys: () => void; |
| | | getSelectRowKeys: () => string[]; |
| | | deleteSelectRowByKey: (key: string) => void; |
| | |
| | | getSize: () => SizeType; |
| | | getRowSelection: () => TableRowSelection<Recordable>; |
| | | getCacheColumns: () => BasicColumn[]; |
| | | emit?: EmitType; |
| | | updateTableData: (index: number, key: string, value: any) => Recordable; |
| | | } |
| | | |
| | | export interface FetchSetting { |
| | |
| | | clickToRowSelect?: boolean; |
| | | // 自定义排序方法 |
| | | sortFn?: (sortInfo: SorterResult) => any; |
| | | // 排序方法 |
| | | filterFn?: (data: Partial<Recordable<string[]>>) => any; |
| | | // 取消表格的默认padding |
| | | inset?: boolean; |
| | | // 显示表格设置 |
| | |
| | | // 是否自动生成key |
| | | autoCreateKey?: boolean; |
| | | // 计算合计行的方法 |
| | | summaryFunc?: (...arg: any) => any[]; |
| | | summaryFunc?: (...arg: any) => Recordable[]; |
| | | // 是否显示合计行 |
| | | showSummary?: boolean; |
| | | // 是否可拖拽列 |
| | |
| | | onExpandedRowsChange?: (expandedRows: string[] | number[]) => void; |
| | | } |
| | | |
| | | export type CellFormat = |
| | | | string |
| | | | ((text: string, record: Recordable, index: number) => string | number) |
| | | | Map<string | number, any>; |
| | | |
| | | // @ts-ignore |
| | | export interface BasicColumn extends ColumnProps { |
| | | children?: BasicColumn[]; |
| | | filters?: { |
| | | text: string; |
| | | value: string; |
| | | children?: |
| | | | unknown[] |
| | | | (((props: Record<string, unknown>) => unknown[]) & (() => unknown[]) & (() => unknown[])); |
| | | }[]; |
| | | |
| | | // |
| | | flag?: 'INDEX' | 'DEFAULT' | 'CHECKBOX' | 'RADIO' | 'ACTION'; |
| | | customTitle?: VueNode; |
| | | |
| | | slots?: Indexable; |
| | | |
| | | // Whether to hide the column by default, it can be displayed in the column configuration |
| | | defaultHidden?: boolean; |
| | | |
| | | // Help text for table column header |
| | | helpMessage?: string | string[]; |
| | | |
| | | format?: CellFormat; |
| | | |
| | | // Editable |
| | | edit?: boolean; |
| | | editRow?: boolean; |
| | | editable?: boolean; |
| | | editComponent?: ComponentType; |
| | | editComponentProps?: Recordable; |
| | | editRule?: boolean | ((text: string, record: Recordable) => Promise<string>); |
| | | editValueMap?: (value: any) => string; |
| | | onEditRow?: () => void; |
| | | } |
| | |
| | | position: absolute; |
| | | top: 10px; |
| | | right: 30px; |
| | | |
| | | &--dot { |
| | | top: 50%; |
| | | margin-top: -3px; |
| | | } |
| | | } |
| | | |
| | | &__title { |
| | |
| | | { |
| | | path: 'table', |
| | | name: t('routes.demo.table.table'), |
| | | tag: { |
| | | dot: true, |
| | | }, |
| | | children: [ |
| | | { |
| | | path: 'basic', |
| | |
| | | { |
| | | path: 'editCellTable', |
| | | name: t('routes.demo.table.editCellTable'), |
| | | tag: { |
| | | dot: true, |
| | | }, |
| | | }, |
| | | { |
| | | path: 'editRowTable', |
| | | name: t('routes.demo.table.editRowTable'), |
| | | tag: { |
| | | dot: true, |
| | | }, |
| | | }, |
| | | ], |
| | | }, |
| | |
| | | const DATE_TIME_FORMAT = 'YYYY-MM-DD HH:mm'; |
| | | const DATE_FORMAT = 'YYYY-MM-DD '; |
| | | |
| | | export function formatToDateTime(date: moment.MomentInput = null): string { |
| | | return moment(date).format(DATE_TIME_FORMAT); |
| | | export function formatToDateTime( |
| | | date: moment.MomentInput = null, |
| | | format = DATE_TIME_FORMAT |
| | | ): string { |
| | | return moment(date).format(format); |
| | | } |
| | | |
| | | export function formatToDate(date: moment.MomentInput = null): string { |
| | | return moment(date).format(DATE_FORMAT); |
| | | export function formatToDate(date: moment.MomentInput = null, format = DATE_FORMAT): string { |
| | | return moment(date).format(format); |
| | | } |
| | | |
| | | export const formatAgo = (str: string | number) => { |
| | |
| | | <template> |
| | | <div class="p-4"> |
| | | <BasicTable @register="registerTable"> |
| | | <template #customId> |
| | | <EditTableHeaderIcon title="Id" /> |
| | | </template> |
| | | <template #customName> |
| | | <EditTableHeaderIcon title="姓名" /> |
| | | </template> |
| | | <BasicTable @register="registerTable" @edit-end="handleEditEnd" @edit-cancel="handleEditCancel"> |
| | | </BasicTable> |
| | | </div> |
| | | </template> |
| | | <script lang="ts"> |
| | | import { defineComponent } from 'vue'; |
| | | import { |
| | | BasicTable, |
| | | useTable, |
| | | BasicColumn, |
| | | renderEditableCell, |
| | | EditTableHeaderIcon, |
| | | } from '/@/components/Table'; |
| | | import { BasicTable, useTable, BasicColumn, EditTableHeaderIcon } from '/@/components/Table'; |
| | | import { optionsListApi } from '/@/api/demo/select'; |
| | | |
| | | import { demoListApi } from '/@/api/demo/table'; |
| | | const columns: BasicColumn[] = [ |
| | | { |
| | | // title: 'ID', |
| | | dataIndex: 'id', |
| | | slots: { title: 'customId' }, |
| | | customRender: renderEditableCell({ dataIndex: 'id' }), |
| | | }, |
| | | { |
| | | // title: '姓名', |
| | | title: '输入框', |
| | | dataIndex: 'name', |
| | | slots: { title: 'customName' }, |
| | | customRender: renderEditableCell({ |
| | | dataIndex: 'name', |
| | | }), |
| | | edit: true, |
| | | editComponentProps: { |
| | | prefix: '$', |
| | | }, |
| | | width: 200, |
| | | }, |
| | | { |
| | | title: '地址', |
| | | dataIndex: 'address', |
| | | sorter: true, |
| | | title: '默认输入状态', |
| | | dataIndex: 'name7', |
| | | edit: true, |
| | | editable: true, |
| | | width: 200, |
| | | }, |
| | | { |
| | | title: '输入框校验', |
| | | dataIndex: 'name1', |
| | | edit: true, |
| | | // 默认必填校验 |
| | | editRule: true, |
| | | width: 200, |
| | | }, |
| | | { |
| | | title: '输入框函数校验', |
| | | dataIndex: 'name2', |
| | | edit: true, |
| | | editRule: async (text) => { |
| | | if (text === '2') { |
| | | return '不能输入该值'; |
| | | } |
| | | return ''; |
| | | }, |
| | | width: 200, |
| | | }, |
| | | { |
| | | title: '数字输入框', |
| | | dataIndex: 'id', |
| | | edit: true, |
| | | editRule: true, |
| | | editComponent: 'InputNumber', |
| | | width: 200, |
| | | }, |
| | | { |
| | | title: '下拉框', |
| | | dataIndex: 'name3', |
| | | edit: true, |
| | | editComponent: 'Select', |
| | | editComponentProps: { |
| | | options: [ |
| | | { |
| | | label: 'Option1', |
| | | value: '1', |
| | | }, |
| | | { |
| | | label: 'Option2', |
| | | value: '2', |
| | | }, |
| | | ], |
| | | }, |
| | | width: 200, |
| | | }, |
| | | { |
| | | title: '远程下拉', |
| | | dataIndex: 'name4', |
| | | edit: true, |
| | | editComponent: 'ApiSelect', |
| | | editComponentProps: { |
| | | api: optionsListApi, |
| | | }, |
| | | width: 200, |
| | | }, |
| | | { |
| | | title: '勾选框', |
| | | dataIndex: 'name5', |
| | | edit: true, |
| | | editComponent: 'Checkbox', |
| | | editValueMap: (value) => { |
| | | return value ? '是' : '否'; |
| | | }, |
| | | width: 200, |
| | | }, |
| | | { |
| | | title: '开关', |
| | | dataIndex: 'name6', |
| | | edit: true, |
| | | editComponent: 'Switch', |
| | | editValueMap: (value) => { |
| | | return value ? '开' : '关'; |
| | | }, |
| | | width: 200, |
| | | }, |
| | | ]; |
| | | export default defineComponent({ |
| | |
| | | api: demoListApi, |
| | | columns: columns, |
| | | showIndexColumn: false, |
| | | bordered: true, |
| | | }); |
| | | |
| | | function handleEditEnd({ record, index, key, value }: Recordable) { |
| | | console.log(record, index, key, value); |
| | | } |
| | | |
| | | function handleEditCancel() { |
| | | console.log('cancel'); |
| | | } |
| | | |
| | | return { |
| | | registerTable, |
| | | handleEditEnd, |
| | | handleEditCancel, |
| | | }; |
| | | }, |
| | | }); |
| | |
| | | TableAction, |
| | | BasicColumn, |
| | | ActionItem, |
| | | renderEditableRow, |
| | | EditTableHeaderIcon, |
| | | EditRecordRow, |
| | | } from '/@/components/Table'; |
| | | import { optionsListApi } from '/@/api/demo/select'; |
| | | |
| | | import { demoListApi } from '/@/api/demo/table'; |
| | | const columns: BasicColumn[] = [ |
| | | { |
| | | title: 'ID', |
| | | dataIndex: 'id', |
| | | customRender: renderEditableRow({ dataIndex: 'id' }), |
| | | title: '输入框', |
| | | dataIndex: 'name', |
| | | editRow: true, |
| | | editComponentProps: { |
| | | prefix: '$', |
| | | }, |
| | | width: 200, |
| | | }, |
| | | { |
| | | title: '姓名', |
| | | dataIndex: 'name', |
| | | customRender: renderEditableRow({ |
| | | dataIndex: 'name', |
| | | }), |
| | | title: '默认输入状态', |
| | | dataIndex: 'name7', |
| | | editRow: true, |
| | | width: 200, |
| | | }, |
| | | { |
| | | title: '输入框校验', |
| | | dataIndex: 'name1', |
| | | editRow: true, |
| | | // 默认必填校验 |
| | | editRule: true, |
| | | width: 200, |
| | | }, |
| | | { |
| | | title: '输入框函数校验', |
| | | dataIndex: 'name2', |
| | | editRow: true, |
| | | editRule: async (text) => { |
| | | if (text === '2') { |
| | | return '不能输入该值'; |
| | | } |
| | | return ''; |
| | | }, |
| | | width: 200, |
| | | }, |
| | | { |
| | | title: '数字输入框', |
| | | dataIndex: 'id', |
| | | editRow: true, |
| | | editRule: true, |
| | | editComponent: 'InputNumber', |
| | | width: 200, |
| | | }, |
| | | { |
| | | title: '下拉框', |
| | | dataIndex: 'name3', |
| | | editRow: true, |
| | | editComponent: 'Select', |
| | | editComponentProps: { |
| | | options: [ |
| | | { |
| | | label: 'Option1', |
| | | value: '1', |
| | | }, |
| | | { |
| | | label: 'Option2', |
| | | value: '2', |
| | | }, |
| | | ], |
| | | }, |
| | | width: 200, |
| | | }, |
| | | { |
| | | title: '远程下拉', |
| | | dataIndex: 'name4', |
| | | editRow: true, |
| | | editComponent: 'ApiSelect', |
| | | editComponentProps: { |
| | | api: optionsListApi, |
| | | }, |
| | | width: 200, |
| | | }, |
| | | { |
| | | title: '勾选框', |
| | | dataIndex: 'name5', |
| | | editRow: true, |
| | | |
| | | editComponent: 'Checkbox', |
| | | editValueMap: (value) => { |
| | | return value ? '是' : '否'; |
| | | }, |
| | | width: 200, |
| | | }, |
| | | { |
| | | title: '开关', |
| | | dataIndex: 'name6', |
| | | editRow: true, |
| | | editComponent: 'Switch', |
| | | editValueMap: (value) => { |
| | | return value ? '开' : '关'; |
| | | }, |
| | | width: 200, |
| | | }, |
| | | ]; |
| | | export default defineComponent({ |
| | |
| | | |
| | | function handleEdit(record: EditRecordRow) { |
| | | currentEditKeyRef.value = record.key; |
| | | record.editable = true; |
| | | record.onEdit?.(true); |
| | | } |
| | | |
| | | function handleCancel(record: EditRecordRow) { |
| | | currentEditKeyRef.value = ''; |
| | | record.editable = false; |
| | | record.onCancel && record.onCancel(); |
| | | record.onEdit?.(false, true); |
| | | } |
| | | |
| | | function handleSave(record: EditRecordRow) { |
| | | currentEditKeyRef.value = ''; |
| | | record.editable = false; |
| | | record.onSubmit && record.onSubmit(); |
| | | async function handleSave(record: EditRecordRow) { |
| | | const pass = await record.onEdit?.(false, true); |
| | | if (pass) { |
| | | currentEditKeyRef.value = ''; |
| | | } |
| | | } |
| | | |
| | | function createActions(record: EditRecordRow, column: BasicColumn): ActionItem[] { |
| | |
| | | { |
| | | title: '地址', |
| | | dataIndex: 'address', |
| | | width: 260, |
| | | }, |
| | | { |
| | | title: '编号', |
| | |
| | | api: demoListApi, |
| | | columns: columns, |
| | | rowSelection: { type: 'radio' }, |
| | | bordered: true, |
| | | actionColumn: { |
| | | width: 160, |
| | | title: 'Action', |
| | |
| | | title: 'ID', |
| | | dataIndex: 'id', |
| | | fixed: 'left', |
| | | width: 400, |
| | | width: 200, |
| | | }, |
| | | { |
| | | title: '姓名', |
| | | dataIndex: 'name', |
| | | width: 150, |
| | | filters: [ |
| | | { text: 'Male', value: 'male' }, |
| | | { text: 'Female', value: 'female' }, |
| | | ], |
| | | }, |
| | | { |
| | | title: '地址', |
| | |
| | | title: '编号', |
| | | dataIndex: 'no', |
| | | width: 150, |
| | | sorter: true, |
| | | defaultHidden: true, |
| | | }, |
| | | { |
| | | title: '开始时间', |
| | | width: 120, |
| | | sorter: true, |
| | | dataIndex: 'beginTime', |
| | | }, |
| | | { |