UNPKG

@ithinkdt/naive

Version:

iThinkDT Naive UI

369 lines (329 loc) 13.4 kB
import { defineComponent, useSlots, shallowRef, computed, watch, ref, nextTick, withDirectives, resolveDirective, mergeProps, onUnmounted, onMounted, isVNode, } from 'vue' import { NFlex, NTooltip, NButton, NIcon, NDataTable } from 'ithinkdt-ui' import { toReactive, until, useElementSize, useDebounceFn } from '@vueuse/core' import { c, cB, CSS_MOUNT_ANCHOR_META_NAME, CSS_STYLE_PREFIX as p } from '@ithinkdt/core/cssr' import { IHelp } from './assets' export const DtTable = defineComponent({ name: 'DtTable', props: { data: { type: Array, default: () => [], }, columns: { type: Array, default: () => [], }, rowKey: { type: [String, Function], default: 'id', }, rowClassName: { type: [Function, String], default: undefined, }, sorts: { type: Object, default: () => ({}), }, optimization: { type: Boolean, default: true, }, }, emits: { sort: () => true, select: () => true, }, setup(props, { emit, expose }) { const cls = `${p}-table` createStyle(cls) const tableRef = ref({}) const handleSort = ({ name, order } = {}, oSort) => { if (name == oSort?.name && order == oSort?.order) return tableRef.value?.sort(name, order ? order + 'end' : false) } watch(() => ({ ...props.sorts }), handleSort, { deep: true }) const exposed = shallowRef({}) until(tableRef) .changed() .then(() => { handleSort(props.sorts) exposed.value = tableRef.value exposed.value.scrollToRow = (index) => { const row = tableRef.value.$el.querySelector(`.${cls}-row-marker:nth-child(${index + 1})`) tableRef.value.scrollTo({ top: row.offsetTop, behavior: 'smooth', }) } }) expose(toReactive(exposed)) const slots = useSlots() let selectable const vTooltip = resolveDirective('tooltip') function _map(columns) { return (columns ?? []) .map((col) => { let column = { csvTitle: col.label, ...col, title: () => { const _label = typeof col.label === 'function' ? col.label() : col.label const label = _label ? withDirectives(<span>{_label}</span>, [[vTooltip, {}, '', { auto: true }]]) : _label if (!col.tip) return label return ( <NFlex size={3} wrap={false} inline style="max-width: 100%"> <span style="flex: 0 1 auto; overflow: hidden; white-space: nowrap; text-overflow: ellipsis"> {label} </span> <NTooltip> {{ default: typeof col.tip === 'function' ? col.tip : () => col.tip, trigger: () => ( <NButton text style="font-size: 1.25em; opacity: 0.8"> <NIcon> <IHelp /> </NIcon> </NButton> ), }} </NTooltip> </NFlex> ) }, } if (col.children?.length) { column.children = _map(col.children) } else { const ellipsis = col.ellipsis ? { expandTrigger: 'click', lineClamp: 1, style: { verticalAlign: 'bottom', }, ...(col.ellipsis === true ? {} : col.ellipsis), tooltip: false, } : col.ellipsis const _render = col.render ? (model, index) => col.render?.(model[col.key], { index, model, col }) : undefined let render = _render if (ellipsis && col.ellipsis?.tooltip !== false) { let tipProps = typeof col.ellipsis?.tooltip === 'object' ? tipProps : {} tipProps = mergeProps( { style: { maxWidth: '48vw', wordBreak: 'break-all', }, }, tipProps, ) render = (...params) => { const content = _render(...params) return ( <span> {withDirectives( !isVNode(content) || content.type === 'string' ? ( <span>{content}</span> ) : ( content ), [ [ vTooltip, { tip: () => _render(...params), ...tipProps, }, '', { auto: true }, ], ], )} </span> ) } } if (col.format === 'selection') { selectable = col.selectable } Object.assign(column, { resizable: col.resizable ?? true, sorter: col.sortable, width: col.width ?? (col.minWidth ? undefined : 100), ellipsis, type: col.format === 'selection' ? 'selection' : col.format === 'expand' ? 'expand' : undefined, className: col.format?.startsWith('tag') ? col.className ?? '' + ' td-format-multiple' : col.className, disabled: col.selectable ? (model, i) => col.selectable(model, i) === false : undefined, render, }) } return column }) .filter((it) => it?.hidden !== true) } const columns = computed(() => _map(props.columns)) const rowKey = computed(() => { const rowKey = props.rowKey return typeof rowKey === 'string' ? (row) => row[rowKey] : rowKey }) const $el = computed(() => { return tableRef.value?.$el }) const width = ref() const { width: tw } = useElementSize($el) const { width: thw } = useElementSize( computed(() => { return $el.value?.querySelector('.n-data-table-base-table-header .n-data-table-thead') }), ) const update = useDebounceFn(async () => { await nextTick() width.value = Math.floor(thw.value) }, 1000 / 60) watch( [tw, thw], async ([w, thw], [ow, othw]) => { if (Math.abs(w - ow) >= 1 || Math.abs(thw - othw) >= 1) { width.value = undefined await nextTick() update() } }, { immediate: true, }, ) const _data = shallowRef([]) const setData = (v) => { _data.value = [..._data.value, ...v] } let unmounted = false let timer onUnmounted(() => { unmounted = true if (timer) { cancelAnimationFrame(timer) timer = undefined } }) onMounted(() => { watch( [() => props.optimization, () => props.data], async ([optimization, data]) => { if (!optimization) { _data.value = [...data] return } data ||= [] if (timer) { cancelAnimationFrame(timer) timer = undefined } if (unmounted) return _data.value = data.slice(0, 20) if (data.length <= 20) return let batch = 2 let i = 0 const set = () => { timer = requestAnimationFrame(() => { if (unmounted) return setData(data.slice(20 + i * batch, 20 + (i + 1) * batch)) i++ if (i < (data.length - 20) / batch) { set() } else { timer = undefined } }) } set() }, { immediate: true }, ) }) return () => { return ( <NDataTable class={cls} data={_data.value} columns={columns?.value} rowKey={rowKey.value} ref={tableRef} scrollX={width.value} rowClassName={(row, i) => { const p = props.rowClassName?.(row, i) || '' return p + ` ${cls}-row-marker` }} onUpdateSorter={(e) => { if (!e) return if (props.sorts) { const { name, order } = props.sorts if (e?.columnKey == name && (e?.order ? e.order === order + 'end' : !order)) return } emit('sort', { name: e?.order ? e?.columnKey : '', order: e?.order ? e.order.replace('end', '') : undefined, }) }} onUpdateCheckedRowKeys={(_keys, _rows, { row, action }) => { const key = row ? rowKey.value(row) : undefined const selected = ['check', 'checkAll'].includes(action) if (!key && selectable) { for (const [i, model] of props.data.entries()) { if (selectable(model, i) !== false) { emit('select', { key: rowKey.value(model), selected }) } } } else { emit('select', key ? { key, selected } : selected) } }} > {slots} </NDataTable> ) } }, }) let style function createStyle(cls) { if (!style) { style = cB('table', [ c('.n-data-table-td > *', { verticalAlign: 'middle', }), ]) style.mount({ id: cls, anchorMetaName: CSS_MOUNT_ANCHOR_META_NAME, }) } }