@ithinkdt/naive
Version:
iThinkDT Naive UI
369 lines (329 loc) • 13.4 kB
JSX
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,
})
}
}