packages/element/src/array-table/index.ts (548 lines of code) (raw):
import {
ArrayField,
FieldDisplayTypes,
GeneralField,
IVoidFieldFactoryProps,
} from '@formily/core'
import type { Schema } from '@formily/json-schema'
import { observer } from '@formily/reactive-vue'
import { isArr, isBool, isFn } from '@formily/shared'
import {
Fragment,
h,
RecursionField as _RecursionField,
useField,
useFieldSchema,
} from '@formily/vue'
import type {
Pagination as PaginationProps,
Table as TableProps,
TableColumn as ElColumnProps,
} from 'element-ui'
import {
Badge,
Option,
Pagination,
Select,
Table as ElTable,
TableColumn as ElTableColumn,
} from 'element-ui'
import type { Component, VNode } from 'vue'
import { computed, defineComponent, inject, provide, ref, Ref } from 'vue-demi'
import { ArrayBase } from '../array-base'
import { Space } from '../space'
import { stylePrefix } from '../__builtins__/configs'
import { composeExport } from '../__builtins__/shared'
const RecursionField = _RecursionField as unknown as Component
interface IArrayTableProps extends TableProps {
pagination?: PaginationProps | boolean
}
interface IArrayTablePaginationProps extends PaginationProps {
dataSource?: any[]
}
interface ObservableColumnSource {
field: GeneralField
fieldProps: IVoidFieldFactoryProps<any, any>
columnProps: ElColumnProps & { title: string; asterisk: boolean }
schema: Schema
display: FieldDisplayTypes
required: boolean
name: string
}
type ColumnProps = ElColumnProps & {
key: string | number
asterisk: boolean
render?: (
startIndex?: Ref<number>
) => (props: {
row: Record<string, any>
column: ElColumnProps
$index: number
}) => VNode
}
interface PaginationAction {
totalPage?: number
pageSize?: number
changePage?: (page: number) => void
}
const PaginationSymbol = Symbol('pagination')
const isColumnComponent = (schema: Schema) => {
return schema['x-component']?.indexOf('Column') > -1
}
const isOperationsComponent = (schema: Schema) => {
return schema['x-component']?.indexOf('Operations') > -1
}
const isAdditionComponent = (schema: Schema) => {
return schema['x-component']?.indexOf('Addition') > -1
}
const getArrayTableSources = (
arrayFieldRef: Ref<ArrayField>,
schemaRef: Ref<Schema>
) => {
const arrayField = arrayFieldRef.value
const parseSources = (schema: Schema): ObservableColumnSource[] => {
if (
isColumnComponent(schema) ||
isOperationsComponent(schema) ||
isAdditionComponent(schema)
) {
if (!schema['x-component-props']?.['prop'] && !schema['name']) return []
const name = schema['x-component-props']?.['prop'] || schema['name']
const field = arrayField.query(arrayField.address.concat(name)).take()
const fieldProps = field?.props || schema.toFieldProps()
const columnProps =
(field?.component as any[])?.[1] || schema['x-component-props'] || {}
const display = field?.display || schema['x-display']
const required = schema.reduceProperties((required, property) => {
if (required) {
return required
}
return !!property.required
}, false)
return [
{
name,
display,
required,
field,
fieldProps,
schema,
columnProps,
},
]
} else if (schema.properties) {
return schema.reduceProperties((buf: any[], schema) => {
return buf.concat(parseSources(schema))
}, [])
} else {
return []
}
}
const parseArrayTable = (schema: Schema['items']) => {
if (!schema) return []
const sources: ObservableColumnSource[] = []
const items = isArr(schema) ? schema : ([schema] as Schema[])
return items.reduce((columns, schema) => {
const item = parseSources(schema)
if (item) {
return columns.concat(item)
}
return columns
}, sources)
}
if (!schemaRef.value) throw new Error('can not found schema object')
return parseArrayTable(schemaRef.value.items)
}
const getArrayTableColumns = (
sources: ObservableColumnSource[]
): ColumnProps[] => {
return sources.reduce(
(
buf: ColumnProps[],
{ name, columnProps, schema, display, required },
key
) => {
const { title, asterisk, ...props } = columnProps
if (display !== 'visible') return buf
if (!isColumnComponent(schema)) return buf
const render = (startIndex?: Ref<number>) => {
return columnProps?.type && columnProps?.type !== 'default'
? undefined
: (props: {
row: Record<string, any>
column: ElColumnProps
$index: number
}): VNode => {
let index = (startIndex?.value ?? 0) + props.$index
// const index = reactiveDataSource.value.indexOf(props.row)
const children = h(
ArrayBase.Item,
{ props: { index, record: props.row }, key: `${key}${index}` },
{
default: () =>
h(
RecursionField,
{
props: {
schema,
name: index,
onlyRenderProperties: true,
},
},
{}
),
}
)
return children
}
}
return buf.concat({
label: title,
...props,
key,
prop: name,
asterisk: asterisk ?? required,
render,
})
},
[]
)
}
const renderAddition = () => {
const schema = useFieldSchema()
return schema.value.reduceProperties((addition, schema) => {
if (isAdditionComponent(schema)) {
return h(
RecursionField,
{
props: {
schema,
name: 'addition',
},
},
{}
)
}
return addition
}, null)
}
const schedulerRequest = {
request: null,
}
const StatusSelect = observer(
defineComponent({
props: {
value: Number,
onChange: Function,
options: Array,
pageSize: Number,
},
setup(props) {
const fieldRef = useField<ArrayField>()
const prefixCls = `${stylePrefix}-array-table`
return () => {
const field = fieldRef.value
const width = String(props.options?.length).length * 15
const errors = field.errors
const parseIndex = (address: string) => {
return Number(
address
.slice(address.indexOf(field.address.toString()) + 1)
.match(/(\d+)/)?.[1]
)
}
return h(
Select,
{
style: {
width: `${width < 60 ? 60 : width}px`,
},
class: [
`${prefixCls}-status-select`,
{
'has-error': errors?.length,
},
],
props: {
value: props.value,
popperClass: `${prefixCls}-status-select-dropdown`,
},
on: {
input: props.onChange,
},
},
{
default: () => {
return props.options?.map(({ label, value }) => {
const hasError = errors.some(({ address }) => {
const currentIndex = parseIndex(address)
const startIndex = (value - 1) * props.pageSize
const endIndex = value * props.pageSize
return currentIndex >= startIndex && currentIndex <= endIndex
})
return h(
Option,
{
key: value,
props: {
label,
value,
},
},
{
default: () => {
if (hasError) {
return h(
Badge,
{
props: {
isDot: true,
},
},
{ default: () => label }
)
}
return label
},
}
)
})
},
}
)
}
},
}),
{
scheduler: (update) => {
clearTimeout(schedulerRequest.request)
schedulerRequest.request = setTimeout(() => {
update()
}, 100)
},
}
)
const usePagination = () => {
return inject<Ref<PaginationAction>>(PaginationSymbol, ref({}))
}
const ArrayTablePagination = defineComponent<IArrayTablePaginationProps>({
inheritAttrs: false,
props: ['pageSize', 'dataSource'],
setup(props, { attrs, slots }) {
const prefixCls = `${stylePrefix}-array-table`
const current = ref(1)
const pageSize = computed(() => props.pageSize || 10)
const dataSource = computed(() => props.dataSource || [])
const startIndex = computed(() => (current.value - 1) * pageSize.value)
const endIndex = computed(() => startIndex.value + pageSize.value - 1)
const total = computed(() => dataSource.value?.length || 0)
const totalPage = computed(() => Math.ceil(total.value / pageSize.value))
const pages = computed(() => {
return Array.from(new Array(totalPage.value)).map((_, index) => {
const page = index + 1
return {
label: page,
value: page,
}
})
})
const renderPagination = function () {
if (totalPage.value <= 1) return
return h(
'div',
{
class: [`${prefixCls}-pagination`],
},
{
default: () =>
h(
Space,
{},
{
default: () => [
h(
StatusSelect,
{
props: {
value: current.value,
onChange: (val: number) => {
current.value = val
},
pageSize: pageSize.value,
options: pages.value,
},
},
{}
),
h(
Pagination,
{
props: {
background: true,
layout: 'prev, pager, next',
...attrs,
pageSize: pageSize.value,
pageCount: totalPage.value,
currentPage: current.value,
},
on: {
'current-change': (val: number) => {
current.value = val
},
},
},
{}
),
],
}
),
}
)
}
const paginationContext = computed<PaginationAction>(() => {
return {
totalPage: totalPage.value,
pageSize: pageSize.value,
changePage: (page: number) => (current.value = page),
}
})
provide(PaginationSymbol, paginationContext)
return () => {
return h(
Fragment,
{},
{
default: () =>
slots?.default?.(
dataSource.value?.slice(startIndex.value, endIndex.value + 1),
renderPagination,
startIndex
),
}
)
}
},
})
const ArrayTableInner = observer(
defineComponent<IArrayTableProps>({
name: 'FArrayTable',
inheritAttrs: false,
setup(props, { attrs, listeners, slots }) {
const fieldRef = useField<ArrayField>()
const schemaRef = useFieldSchema()
const prefixCls = `${stylePrefix}-array-table`
const { getKey, keyMap } = ArrayBase.useKey(schemaRef.value)
const defaultRowKey = (record: any) => {
return getKey(record)
}
return () => {
const props = attrs as unknown as IArrayTableProps
const field = fieldRef.value
const dataSource = Array.isArray(field.value) ? field.value.slice() : []
const pagination = props.pagination
const sources = getArrayTableSources(fieldRef, schemaRef)
const columns = getArrayTableColumns(sources)
const renderColumns = (startIndex?: Ref<number>) => {
return columns.map(({ key, render, asterisk, ...props }) => {
const children = {} as Record<string, any>
if (render) {
children.default = render(startIndex)
}
if (asterisk) {
children.header = ({ column }: { column: ElColumnProps }) =>
h(
'span',
{},
{
default: () => [
h(
'span',
{ class: `${prefixCls}-asterisk` },
{ default: () => ['*'] }
),
column.label,
],
}
)
}
return h(
ElTableColumn,
{
key,
props,
},
children
)
})
}
const renderStateManager = () =>
sources.map((column, key) => {
//专门用来承接对Column的状态管理
if (!isColumnComponent(column.schema)) return
return h(
RecursionField,
{
props: {
name: column.name,
schema: column.schema,
onlyRenderSelf: true,
},
key,
},
{}
)
})
const renderTable = (
dataSource?: any[],
pager?: () => VNode,
startIndex?: Ref<number>
) => {
return h(
'div',
{ class: prefixCls },
{
default: () =>
h(
ArrayBase,
{
props: {
keyMap,
},
},
{
default: () => [
h(
ElTable,
{
props: {
rowKey: defaultRowKey,
...attrs,
data: dataSource,
},
on: listeners,
},
{
...slots,
default: () => renderColumns(startIndex),
}
),
pager?.(),
renderStateManager(),
renderAddition(),
],
}
),
}
)
}
if (!pagination) {
return renderTable(dataSource, null)
}
return h(
ArrayTablePagination,
{
attrs: {
...(isBool(pagination) ? {} : pagination),
dataSource,
},
},
{ default: renderTable }
)
}
},
})
)
const ArrayTableColumn: Component = {
name: 'FArrayTableColumn',
render(h) {
return h()
},
}
const ArrayAddition = defineComponent({
name: 'ArrayAddition',
setup(props, { attrs, listeners, slots }) {
const array = ArrayBase.useArray()
const paginationRef = usePagination()
const onClick = listeners['click']
listeners['click'] = (e) => {
const { totalPage = 0, pageSize = 10, changePage } = paginationRef.value
// 如果添加数据后超过当前页,则自动切换到下一页
const total = array?.field?.value?.value.length || 0
if (total === (totalPage - 1) * pageSize + 1 && isFn(changePage)) {
changePage(totalPage)
}
if (onClick) onClick(e)
}
return () => {
return h(
ArrayBase.Addition,
{
props,
attrs,
on: listeners,
},
slots
)
}
},
})
export const ArrayTable = composeExport(ArrayTableInner, {
Column: ArrayTableColumn,
Index: ArrayBase.Index,
SortHandle: ArrayBase.SortHandle,
Addition: ArrayAddition,
Remove: ArrayBase.Remove,
MoveDown: ArrayBase.MoveDown,
MoveUp: ArrayBase.MoveUp,
useArray: ArrayBase.useArray,
useIndex: ArrayBase.useIndex,
useRecord: ArrayBase.useRecord,
})
export default ArrayTable