You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

807 lines
27 KiB
TypeScript

1 year ago
import type { TablePaginationConfig, TableProps } from 'ant-design-vue'
import { Table } from 'ant-design-vue'
import 'ant-design-vue/es/table/style/index.less'
import 'ant-design-vue/es/pagination/style/index.less'
import ProCard from '#/card'
1 year ago
import type {
TableCurrentDataSource,
SortOrder,
GetRowKey
} from 'ant-design-vue/es/table/interface'
import useFetchData from './useFetchData'
import Toolbar from './components/ToolBar'
import TableAlert from './components/Alert'
import {
genColumnKey,
mergePagination,
useActionType,
isBordered,
parseDefaultColumnConfig
} from './utils'
import { genProColumnToColumn } from './utils/genProColumnToColumn'
import './index.less'
import type {
ActionType,
PageInfo,
RequestData,
TableRowSelection,
UseFetchDataAction
} from './typing'
import { columnSort } from './utils/columnSort'
import { getPrefixCls } from '#/layout/RouteContext'
import type { VueKey, VueText } from '#/types'
import { computed, ref, defineComponent, watchEffect } from 'vue'
import type { CSSProperties, PropType } from 'vue'
import omitUndefined from '../utils/omitUndefined'
import { proTableProps } from './typing'
import { useVModel } from '@vueuse/core'
import { useIntl } from '#/provider'
import { useContainer, useProvideContainer } from '#/table/container'
import { tableProps } from 'ant-design-vue/es/table'
import type { ProSchemaComponentTypes } from '#/utils/typing'
import type { Key } from 'ant-design-vue/es/_util/type'
import { getRender } from '#/layout/utils'
import type { VueNodeOrRender } from '#/types'
const tablePropsInstance = tableProps()
const tablePropKeys = Object.keys(tablePropsInstance) as unknown as [keyof TableProps]
export type ProTableInstanceExpose = {
loading: boolean
actionRef: ActionType
}
// eslint-disable-next-line vue/one-component-per-file
const TableRender = defineComponent({
name: 'TableRender',
props: {
...proTableProps(),
className: { type: String, default: undefined },
action: { type: Object as PropType<UseFetchDataAction>, default: null },
tableColumn: { type: Array as PropType<any[]>, default: () => [] },
isLightFilter: { type: Boolean, default: false },
onSortChange: { type: Function as PropType<(sort: any) => void>, default: undefined },
onFilterChange: { type: Function as PropType<(sort: any) => void>, default: undefined },
editableUtils: { type: Object as PropType<any>, default: undefined },
getRowKey: { type: Function as PropType<GetRowKey<any>>, default: undefined }
},
slots: ['toolbarDom', 'alertDom', 'searchNode'],
setup(props, { attrs, slots }) {
const counter = useContainer()!
/** 需要遍历一下,不然不支持嵌套表格 */
const columns = computed(() => {
const loopFilter = (column: any[]): any[] => {
return column
.map(item => {
// 删掉不应该显示的
const columnKey = genColumnKey(item.key, item.index)
const config = counter.columnsMap.value[columnKey]
if (config && config.show === false) {
return false
}
if (item.children) {
return {
...item,
children: loopFilter(item.children)
}
}
return item
})
.filter(Boolean)
}
return loopFilter(props.tableColumn)
})
/** 如果所有列中的 filters=true| undefined 说明是用的是本地筛选 任何一列配置 filters=false就能绕过这个判断 */
const useLocaleFilter = computed(() =>
columns.value?.every(
column =>
(column.filters === true && column.onFilter === true) ||
(column.filters === undefined && column.onFilter === undefined)
)
)
// 用户传入的 table 属性
const userTableProps = computed(() => {
return Object.fromEntries(tablePropKeys.map(k => [k, props[k]]))
})
const tableProps = computed(() => ({
...userTableProps.value,
size: props.size,
rowSelection: props.rowSelection === false ? undefined : props.rowSelection,
className: props.tableClassName,
style: props.tableStyle,
columns: columns.value.map(item => (item.isExtraColumns ? item.extraColumn : item)),
loading: props.action.loading,
dataSource: props.action.dataSource,
pagination: props.pagination,
onChange: (
changePagination: TablePaginationConfig,
filters: Record<string, (VueKey | boolean)[] | null>,
sorter: any,
extra: TableCurrentDataSource<unknown>
) => {
props.onChange?.(changePagination, filters, sorter, extra)
if (!useLocaleFilter.value) {
props.onFilterChange?.(omitUndefined<any>(filters))
}
// 制造筛选的数据
// 制造一个排序的数据
if (Array.isArray(sorter)) {
const data = sorter.reduce<Record<string, any>>(
(pre, value) => ({
...pre,
[`${value.field}`]: value.order
}),
{}
)
props.onSortChange?.(omitUndefined<any>(data))
} else {
const sorterOfColumn = sorter.column?.sorter
const isSortByField = sorterOfColumn?.toString() === sorterOfColumn
props.onSortChange?.(
omitUndefined({
[`${isSortByField ? sorterOfColumn : sorter.field}`]: sorter.order as SortOrder
}) || {}
)
}
}
}))
return () => {
/** 默认的 table dom如果是编辑模式外面还要包个 form */
const baseTableDom = (
<Table {...tableProps.value} rowKey={props.rowKey}>
{slots}
</Table>
)
/** 自定义的 render */
const tableDom = props.tableViewRender
? props.tableViewRender(
{
...tableProps.value,
rowSelection: props.rowSelection !== false ? props.rowSelection : undefined
},
baseTableDom
)
: baseTableDom
// watchEffect(() => {
// // 如果带了name说明要用自带的 form需要设置一下。
// if (props.name && props.editable) {
// counter.setEditorTableForm(props.editable!.form!)
// }
// })
const tableContentDom = computed(() => {
// if (props.editable && !props.name) {
// return (
// <>
// {toolbarDom}
// {alertDom}
// <ProForm
// onInit={(_, form) => {
// counter.setEditorTableForm(form)
// }}
// // @ts-ignore
// formRef={form => {
// counter.setEditorTableForm(form)
// }}
// {...props.editable?.formProps}
// component={false}
// form={props.editable?.form}
// onValuesChange={editableUtils.onValuesChange}
// key="table"
// submitter={false}
// omitNil={false}
// dateFormatter={props.dateFormatter}
// contentRender={(items: React.ReactNode) => {
// if (counter.editableForm) return items
// if (props.loading === false) return
// const loadingProps = props.loading === true ? {} : props.loading
// return (
// <div style={{ paddingTop: 100, textAlign: 'center' }}>
// <Spin size="large" {...loadingProps} />
// </div>
// )
// }}
// >
// {tableDom}
// </ProForm>
// </>
// )
// }
return (
<>
{slots.toolbarDom?.()}
{slots.alertDom?.()}
{tableDom}
</>
)
})
/** Table 区域的 dom为了方便 render */
const tableAreaDom =
// cardProps 或者 有了name 就不需要这个padding了不然会导致不好对齐
props.cardProps === false ? (
tableContentDom.value
) : (
// @ts-ignore
<ProCard
ghost={props.ghost}
bordered={isBordered('table', props.cardBordered)}
bodyStyle={
slots.toolbarDom
? {
paddingTop: 0
}
: {
padding: 0
}
}
{...props.cardProps}
>
{tableContentDom.value}
</ProCard>
)
const renderTable = () => {
if (props.tableRender) {
return props.tableRender(props, tableAreaDom, {
toolbar: slots.toolbarDom?.(),
alert: slots.alertDom?.(),
table: tableDom || undefined
})
}
return tableAreaDom
}
const proTableDom = (
<div
ref={counter.rootDomRef}
class={[props.className, { [`${props.className}-polling`]: props.action.pollingLoading }]}
style={attrs.style as CSSProperties}
>
{props.isLightFilter ? null : slots.searchNode}
{/* 渲染一个额外的区域,用于一些自定义 */}
{props.tableExtraRender && (
<div class={`${props.className}-extra`}>
{props.tableExtraRender(props, props.action.dataSource || [])}
</div>
)}
{renderTable()}
</div>
)
// TODO: 全屏处理
// 如果不需要的全屏ConfigProvider 没有意义
if (!props.options || !props.options?.fullScreen) {
return proTableDom
}
return proTableDom
}
}
})
// eslint-disable-next-line vue/one-component-per-file
const ProTable = defineComponent({
name: 'ProTable',
props: {
...proTableProps(),
defaultClassName: { type: String, default: undefined },
className: { type: String, default: undefined }
},
setup(props, { slots, expose }) {
const className = [props.defaultClassName, props.className]
const type: ProSchemaComponentTypes = 'table'
/** 通用的来操作子节点的工具类 */
const actionRef = ref<ActionType>()
// const defaultFormRef = ref()
// const formRef = propRef || defaultFormRef
// useImperativeHandle(props.actionRef, () => actionRef.current)
/** 单选多选的相关逻辑 */
const selectedRowKeys = ref<Key[]>()
watch(
() => props.rowSelection,
() => {
if (props.rowSelection === false) {
selectedRowKeys.value = undefined
} else if (props.rowSelection.selectedRowKeys) {
selectedRowKeys.value = [...props.rowSelection.selectedRowKeys]
} else if (props.rowSelection.defaultSelectedRowKeys) {
selectedRowKeys.value = [...props.rowSelection.defaultSelectedRowKeys]
} else {
selectedRowKeys.value = []
}
},
{ deep: true, immediate: true }
)
const setSelectedRowKeys = (keys: Key[]) => {
selectedRowKeys.value = keys
}
const selectedRowsRef = ref<any[]>([])
const setSelectedRowsAndKey = (keys: Key[], rows: unknown[]) => {
setSelectedRowKeys(keys)
if (!props.rowSelection || !props.rowSelection?.selectedRowKeys) {
selectedRowsRef.value = rows
}
}
const formSearch = props.manualRequest ? undefined : {}
// const [formSearch, setFormSearch] = useMountMergeState<Record<string, any> | undefined>(() => {
// // 如果手动模式,或者 search 不存在的时候设置为 undefined
// // undefined 就不会触发首次加载
// if (manualRequest || search !== false) {
// return undefined
// }
// return {}
// })
const proFilter = ref<Record<string, VueText[] | null>>()
const proSort = ref<Record<string, SortOrder>>()
/** 设置默认排序和筛选值 */
watchEffect(() => {
const { sort, filter } = parseDefaultColumnConfig(props.columns)
proFilter.value = filter
proSort.value = sort
})
const intl = useIntl()
/** 需要初始化 不然默认可能报错 这里取了 defaultCurrent 和 current 为了保证不会重复刷新 */
const fetchPagination =
typeof props.pagination === 'object'
? (props.pagination as TablePaginationConfig)
: { defaultCurrent: 1, defaultPageSize: 10, pageSize: 10, current: 1 }
const counter = useContainer()!
// const counter = Container.useContainer()
// ============================ useFetchData ============================
const fetchData = () => {
if (!props.request) return undefined
return async (pageParams?: Record<string, any>) => {
const actionParams = {
...(pageParams || {}),
...formSearch,
...props.params
}
// eslint-disable-next-line no-underscore-dangle
delete (actionParams as any)._timestamp
const response = await props.request?.(actionParams, proSort.value!, proFilter.value!)
6 months ago
// console.log('请求数据:', response)
1 year ago
return response as RequestData<unknown>
}
}
const loading = props.loading === undefined ? ref(false) : useVModel(props, 'loading')
const action = useFetchData(fetchData(), props.defaultData, {
pageInfo: props.pagination === false ? false : fetchPagination,
loading: loading,
dataSource: props.dataSource,
onDataSourceChange: props.onDataSourceChange,
onLoad: props.onLoad,
onLoadingChange: props.onLoadingChange,
onRequestError: props.onRequestError,
postData: props.postData,
revalidateOnFocus: props.revalidateOnFocus ?? false,
manual: formSearch === undefined,
polling: props.polling,
effects: computed(() => [
new URLSearchParams(props.params).toString(),
new URLSearchParams(formSearch).toString(),
new URLSearchParams(proSort.value as any).toString(),
new URLSearchParams(proFilter.value as any).toString()
]),
debounceTime: props.debounceTime,
onPageInfoChange: pageInfo => {
// @ts-ignore
if (type === 'list' || !props.pagination || !fetchData) return
// 总是触发一下 onChange 和 onShowSizeChange
// 目前只有 List 和 Table 支持分页, List 有分页的时候打断 Table 的分页
props.pagination?.onChange?.(pageInfo.current, pageInfo.pageSize)
props.pagination?.onShowSizeChange?.(pageInfo.current, pageInfo.pageSize)
}
})
// ============================ END ============================
/** 默认聚焦的时候重新请求数据,这样可以保证数据都是最新的。 */
// watchEffect(() => {
// // 手动模式和 request 为空都不生效
// if (
// props.manualRequest ||
// !props.request ||
// props.revalidateOnFocus === false
// // || props.form?.ignoreRules
// )
// return
//
// // 聚焦时重新请求事件
// const visibilitychange = () => {
// if (document.visibilityState === 'visible') action.value.reload()
// }
//
// document.addEventListener('visibilitychange', visibilitychange)
// })
// onUnmounted(() => document.removeEventListener('visibilitychange', visibilitychange))
// ============================ RowKey ============================
const getRowKey = computed<GetRowKey<any>>(() => {
if (typeof props.rowKey === 'function') {
return props.rowKey
}
return (record: any, index?: number) => {
if (index === -1) {
return (record as any)?.[props.rowKey as string]
}
// 如果 props 中有name 的话用index 来做行号,这样方便转化为 index
// if (props.name) {
// return index?.toString()
// }
return (record as any)?.[props.rowKey as string] ?? index?.toString()
}
})
/** SelectedRowKeys受控处理selectRows */
const preserveRecordsRef = computed<Map<any, unknown>>(() => {
if (action.value.dataSource?.length) {
const newCache = new Map<any, unknown>()
action.value.dataSource.forEach(data => {
const dataRowKey = getRowKey.value(data, -1)
newCache.set(dataRowKey, data)
})
return newCache
}
return new Map<any, unknown>()
})
watchEffect(() => {
selectedRowsRef.value =
selectedRowKeys.value?.map(key => preserveRecordsRef.value?.get(key)) || []
})
/** 页面编辑的计算 */
const pagination = computed(() => {
const newPropsPagination = props.pagination === false ? false : { ...props.pagination }
const pageConfig = {
...action.value.pageInfo,
setPageInfo: ({ pageSize, current }: PageInfo) => {
const { pageInfo } = action.value
// pageSize 发生改变,并且你不是在第一页,切回到第一页
// 这样可以防止出现 跳转到一个空的数据页的问题
if (pageSize === pageInfo.pageSize || pageInfo.current === 1) {
action.value.setPageInfo({ pageSize, current })
return
}
// 通过request的时候清空数据然后刷新不然可能会导致 pageSize 没有数据多
if (props.request) action.value.setDataSource([])
action.value.setPageInfo({
pageSize,
// 目前只有 List 和 Table 支持分页, List 有分页的时候 还是使用之前的当前页码
current: 1
})
}
}
if (props.request && newPropsPagination) {
delete newPropsPagination.onChange
delete newPropsPagination.onShowSizeChange
}
return mergePagination<any>(newPropsPagination, pageConfig, intl)
})
// useDeepCompareEffect(() => {
// // request 存在且params不为空且已经请求过数据才需要设置。
// if (props.request && props.params && action.value.dataSource && action.value?.pageInfo?.current !== 1) {
// action.value.setPageInfo({
// current: 1
// })
// }
// }, [params])
// 设置 name 到 store 中,里面用了 ref ,所以不用担心直接 set
// counter.setPrefixName(props.name)
/** 清空所有的选中项 */
const onCleanSelected = () => {
if (props.rowSelection && props.rowSelection.onChange) {
props.rowSelection.onChange([], [])
}
setSelectedRowsAndKey([], [])
}
// counter.setaction.value(action.valueRef.current)
// counter.propsRef.current = props
/** 可编辑行的相关配置 */
// const editableUtils = useEditableArray<any>({
// ...props.editable,
// tableName: props.name,
// getRowKey,
// childrenColumnName: props.expandable?.childrenColumnName,
// dataSource: action.value.dataSource || [],
// setDataSource: data => {
// props.editable?.onValuesChange?.(undefined as any, data)
// action.value.setDataSource(data)
// }
// })
/** 绑定 action */
useActionType(actionRef, action.value, {
fullScreen: () => {
if (!counter.rootDomRef.value || !document.fullscreenEnabled) {
return
}
if (document.fullscreenElement) {
document.exitFullscreen()
} else {
counter.rootDomRef.value?.requestFullscreen()
}
},
onCleanSelected: () => {
// 清空选中行
// onCleanSelected()
},
resetAll: () => {
// 清空选中行
// onCleanSelected()
// 清空筛选
proFilter.value = {}
// 清空排序
proSort.value = {}
// 清空 toolbar 搜索
counter.setKeyWords(undefined)
// 重置页码
action.value.setPageInfo({
current: 1
})
// 重置表单
// props.formRef?.current?.resetFields()
// setFormSearch({})
},
editableUtils: undefined
})
// ---------- 列计算相关 start -----------------
const tableColumn = computed(() => {
return genProColumnToColumn({
columns: props.columns,
counter,
columnEmptyText: '-',
type,
editableUtils: null,
rowKey: props.rowKey,
childrenColumnName: props.childrenColumnName
}).sort(columnSort(counter.columnsMap.value))
})
/** Table Column 变化的时候更新一下,这个参数将会用于渲染 */
watchEffect(() => {
if (tableColumn.value && tableColumn.value.length > 0) {
// 重新生成key的字符串用于排序
const columnKeys = tableColumn.value.map(item => genColumnKey(item.key, item.index))
counter.setSortKeyColumns(columnKeys)
}
})
// /** 同步 Pagination支持受控的 页码 和 pageSize */
// useDeepCompareEffect(() => {
// const { pageInfo } = action.value
// const { current = pageInfo?.current, pageSize = pageInfo?.pageSize } = props.pagination || {}
// if (
// props.pagination &&
// (current || pageSize) &&
// (pageSize !== pageInfo?.pageSize || current !== pageInfo?.current)
// ) {
// action.value.setPageInfo({
// pageSize: pageSize || pageInfo.pageSize,
// current: current || pageInfo.current
// })
// }
// }, [
// props.pagination && props.pagination.pageSize,
// props.pagination && props.pagination.current
// ])
/** 行选择相关的问题 */
const rowSelection = computed<TableRowSelection>(() => ({
selectedRowKeys: selectedRowKeys.value,
...props.rowSelection,
onChange: (keys: Key[], rows: unknown[]) => {
if (props.rowSelection && props.rowSelection.onChange) {
props.rowSelection.onChange(keys, rows)
}
setSelectedRowsAndKey(keys, rows)
}
}))
/** 是不是 LightFilter, LightFilter 有一些特殊的处理 */
const isLightFilter: boolean = props.search !== false && props.search?.filterType === 'light'
// const onFormSearchSubmit = <Y extends ParamsType>(values: Y): any => {
// // 判断search.onSearch返回值决定是否更新formSearch
// if (props.options && props.options.search) {
// const { name = 'keyword' } = props.options.search === true ? {} : props.options.search
//
// /** 如果传入的 onSearch 返回值为 false则不要把options.search.name对应的值set到formSearch */
// const success = (props.options.search as OptionSearchProps)?.onSearch?.(counter.keyWords!)
//
// if (success !== false) {
// setFormSearch({
// ...values,
// [name]: counter.keyWords
// })
// return
// }
// }
//
// setFormSearch(values)
// }
const searchNode = null
// const searchNode =
// search === false && type !== 'form' ? null : (
// <FormRender<T, U>
// pagination={pagination}
// beforeSearchSubmit={beforeSearchSubmit}
// action={actionRef}
// columns={propsColumns}
// onFormSearchSubmit={values => {
// onFormSearchSubmit(values)
// }}
// ghost={ghost}
// onReset={props.onReset}
// onSubmit={props.onSubmit}
// loading={!!action.value.loading}
// manualRequest={manualRequest}
// search={search}
// form={props.form}
// formRef={formRef}
// type={props.type || 'table'}
// cardBordered={props.cardBordered}
// dateFormatter={props.dateFormatter}
// />
// )
expose({
loading: loading,
actionRef: actionRef
})
return () => {
const headerTitle = getRender<VueNodeOrRender>(props, slots, 'headerTitle')
/** 内置的工具栏 */
const toolbarDom =
props.toolBarRender === false ? null : (
<Toolbar
headerTitle={headerTitle}
hideToolbar={
props.options === false &&
!props.headerTitle &&
!props.toolBarRender &&
!props.toolbar &&
!isLightFilter
}
selectedRows={selectedRowsRef.value}
selectedRowKeys={selectedRowKeys.value!}
tableColumn={tableColumn.value}
tooltip={props.tooltip}
toolbar={props.toolbar}
onFormSearchSubmit={() => {
// setFormSearch({
// ...formSearch,
// ...newValues
// })
}}
searchNode={isLightFilter ? searchNode : null}
options={props.options}
actionRef={actionRef}
toolBarRender={props.toolBarRender}
>
{slots}
</Toolbar>
)
/** 内置的多选操作栏 */
const alertDom =
props.rowSelection !== false ? (
<TableAlert
selectedRowKeys={selectedRowKeys.value!}
selectedRows={selectedRowsRef.value}
onCleanSelected={onCleanSelected}
alertOptionRender={props.tableAlertOptionRender}
alertInfoRender={props.tableAlertRender}
alwaysShowAlert={props.rowSelection?.alwaysShowAlert}
>
{{
alertOptionRender: slots.tableAlertOptionRender,
alertInfoRender: slots.tableAlertRender
}}
</TableAlert>
) : null
return (
<TableRender
{...props}
// name={false}
size={counter.tableSize.value}
onSizeChange={counter.setTableSize}
pagination={pagination.value}
// searchNode={props.searchNode}
rowSelection={props.rowSelection !== false ? rowSelection.value : undefined}
class={className}
tableColumn={tableColumn.value}
isLightFilter={isLightFilter}
action={action.value}
onSortChange={x => (proSort.value = x)}
onFilterChange={x => (proFilter.value = x)}
editableUtils={null}
getRowKey={getRowKey.value}
>
{{
alertDom: () => alertDom,
toolbarDom: () => toolbarDom,
...slots
}}
</TableRender>
)
}
}
})
/**
* 🏆 Use Ant Design Table like a Pro! 便
*
* @param props
*/
// eslint-disable-next-line vue/one-component-per-file
const ProviderWarp = defineComponent({
name: 'ProviderWarp',
props: proTableProps(),
slots: ['toolBarRender'],
setup(props, { slots, expose }) {
// @ts-ignore
useProvideContainer(props)
const proTableRef = ref()
expose({
loading: computed(() => proTableRef?.value.loading),
actionRef: computed(() => proTableRef?.value.actionRef)
})
return () => (
<ProTable ref={proTableRef} defaultClassName={getPrefixCls('pro-table')} {...props}>
{slots}
</ProTable>
)
}
})
ProviderWarp.Summary = Table.Summary
export default ProviderWarp