import { HolderOutlined } from '@ant-design/icons'
import type { DragEndEvent } from '@dnd-kit/core'
import { DndContext } from '@dnd-kit/core'
import type { SyntheticListenerMap } from '@dnd-kit/core/dist/hooks/utilities'
import { restrictToVerticalAxis } from '@dnd-kit/modifiers'
import { SortableContext, useSortable, verticalListSortingStrategy } from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities'
import { Button } from 'antd'
import React, { createContext, PropsWithChildren, useContext, useMemo } from 'react'
interface RowContextProps {
setActivatorNodeRef?: (element: HTMLElement | null) => void
listeners?: SyntheticListenerMap
}
const RowContext = createContext<RowContextProps>({})
export const DragHandle = () => {
const { setActivatorNodeRef, listeners } = useContext(RowContext)
return (
<Button type='text' icon={<HolderOutlined />} style={{ cursor: 'move' }} ref={setActivatorNodeRef} {...listeners} />
)
}
interface RowProps extends React.HTMLAttributes<HTMLTableRowElement> {
'data-row-key': string
}
export const CustomRow = (props: RowProps) => {
const { attributes, listeners, setNodeRef, setActivatorNodeRef, transform, transition, isDragging } = useSortable({
id: props['data-row-key'],
})
const style: React.CSSProperties = {
...props.style,
transform: CSS.Translate.toString(transform),
transition,
...(isDragging ? { position: 'relative', zIndex: 9999 } : {}),
}
const contextValue = useMemo<RowContextProps>(
() => ({ setActivatorNodeRef, listeners }),
[setActivatorNodeRef, listeners],
)
return (
<RowContext.Provider value={contextValue}>
<tr {...props} ref={setNodeRef} style={style} {...attributes} />
</RowContext.Provider>
)
}
export function SortWrapper(
props: PropsWithChildren<{ dataSource: { id: string }[]; onDragEnd: (e: DragEndEvent) => void }>,
) {
return (
<DndContext modifiers={[restrictToVerticalAxis]} onDragEnd={props.onDragEnd}>
<SortableContext items={props.dataSource.map((i) => i.id)} strategy={verticalListSortingStrategy}>
{props.children}
</SortableContext>
</DndContext>
)
}
export const uid = () => Math.random().toString(36).slice(2, 8)
export const toFixed = (num: number, precision = 2) => Number(num.toFixed(precision))
export const cateOptions = [
{ value: '1', label: '分类1' },
{ value: '2', label: '分类2' },
{ value: '3', label: '分类3' },
]
export const subCateOptions = [
{ value: '1', label: '子分类1' },
{ value: '2', label: '子分类2' },
{ value: '3', label: '子分类3' },
]
export type RowData = {
id: string
category?: string
subCategory?: string
price?: number
c
ount?: number
hours?: number
total?: number
}
import { DragEndEvent } from '@dnd-kit/core'
import { arrayMove } from '@dnd-kit/sortable'
import type { TableProps } from 'antd'
import { Button, Form, InputNumber, Select, Space, Table } from 'antd'
import { useState } from 'react'
import { CustomRow, DragHandle, SortWrapper } from './sort'
import { cateOptions, RowData, subCateOptions, toFixed, uid } from './utils'
export default function DataTable() {
const [datatSource, setDataSource] = useState<RowData[]>([])
const [editingId, setEditingId] = useState<string>()
const [form] = Form.useForm<RowData>()
const category = Form.useWatch('category', form)
const subCategory = Form.useWatch('subCategory', form)
const price = category && subCategory ? Number(`${category}.${subCategory}`) : 0
const count = Form.useWatch('count', form)
const hours = Form.useWatch('hours', form)
const total = price && count && hours ? toFixed(price * count * hours) : 0
const onAdd = () => {
const id = uid()
setDataSource((prev) => prev.concat({ id }))
setEditingId(id)
}
const onEdit = (record: RowData) => {
form.setFieldsValue(record)
setEditingId(record.id)
}
const onSave = async () => {
await form.validateFields()
const values = form.getFieldsValue()
setDataSource((arr) =>
arr.map((v) => {
if (v.id === editingId) {
const price = values.category && values.subCategory ? Number(`${values.category}.${values.subCategory}`) : 0
const total = price && values.count && values.hours ? toFixed(price * values.count * values.hours) : 0
return { ...v, ...values, price, total }
}
return v
}),
)
form.resetFields()
setEditingId(undefined)
}
const onCancel = () => {
form.resetFields()
setEditingId(undefined)
}
const onRemove = (id: string) => {
form.resetFields()
setDataSource((prev) => prev.filter((item) => item.id !== id))
}
const columns: TableProps<RowData>['columns'] = [
{
title: '分类',
dataIndex: 'category',
render(value, record) {
if (editingId === record.id) {
return (
<Form.Item name='category' rules={[{ required: true }]}>
<Select options={cateOptions} />
</Form.Item>
)
} else {
return value
}
},
},
{
title: '子分类',
dataIndex: 'subCategory',
render(value, record) {
if (editingId === record.id) {
return (
<Form.Item name='subCategory' rules={[{ required: true }]}>
<Select options={subCateOptions} />
</Form.Item>
)
} else {
return value
}
},
},
{
title: '价格',
dataIndex: 'price',
render(value, record) {
return `¥ ${editingId === record.id ? price : value || ''}`
},
},
{
title: '数量',
dataIndex: 'count',
render(value, record) {
if (editingId === record.id) {
return (
<Form.Item name='count' rules={[{ required: true }]}>
<InputNumber />
</Form.Item>
)
} else {
return value
}
},
},
{
title: '时长',
dataIndex: 'hours',
render(value, record) {
if (editingId === record.id) {
return (
<Form.Item name='hours' rules={[{ required: true }]}>
<InputNumber />
</Form.Item>
)
} else {
return value
}
},
},
{
title: '总价',
dataIndex: 'total',
render(value, record) {
return `¥ ${editingId === record.id ? total : value || ''}`
},
},
{
title: '操作',
fixed: 'right',
render: (record: RowData) => {
if (editingId === record.id) {
return (
<Space>
<Button onClick={() => onSave()}>保存</Button>
<Button onClick={() => onCancel()}>取消</Button>
<Button onClick={() => onRemove(record.id)}>删除</Button>
</Space>
)
} else {
return (
<Space>
<Button onClick={() => onEdit(record)}>编辑</Button>
<DragHandle />
</Space>
)
}
},
},
]
const onDragEnd = ({ active, over }: DragEndEvent) => {
if (active.id !== over?.id) {
setDataSource((prevState) => {
const activeIndex = prevState.findIndex((record) => record.id === active?.id)
const overIndex = prevState.findIndex((record) => record.id === over?.id)
return arrayMove(prevState, activeIndex, overIndex)
})
}
}
return (
<>
<Space>
<Button onClick={onAdd}>添加</Button>
</Space>
<SortWrapper dataSource={datatSource} onDragEnd={onDragEnd}>
<Form component={false} form={form}>
<Table
rowKey={(row) => row.id}
components={{ body: { row: CustomRow } }}
columns={columns}
dataSource={datatSource}
pagination={{ pageSize: 1000, hideOnSinglePage: true }}
/>
</Form>
</SortWrapper>
</>
)
}