|
@@ -0,0 +1,841 @@
|
|
|
|
|
+<script lang="ts" setup generic="T">
|
|
|
|
|
+import type { TableInstance, TableProps } from 'element-plus'
|
|
|
|
|
+import {
|
|
|
|
|
+ Comment,
|
|
|
|
|
+ Fragment,
|
|
|
|
|
+ Text,
|
|
|
|
|
+ cloneVNode,
|
|
|
|
|
+ computed,
|
|
|
|
|
+ nextTick,
|
|
|
|
|
+ provide,
|
|
|
|
|
+ ref,
|
|
|
|
|
+ useAttrs,
|
|
|
|
|
+ useSlots,
|
|
|
|
|
+ watch
|
|
|
|
|
+} from 'vue'
|
|
|
|
|
+import { useRoute } from 'vue-router'
|
|
|
|
|
+import { TableContextKey } from './token'
|
|
|
|
|
+import type { ColumnAlign, ColumnFixed, ColumnSettingItem } from './token'
|
|
|
|
|
+import type { DefaultRow } from 'element-plus/es/components/table/src/table/defaults'
|
|
|
|
|
+import type { VNode } from 'vue'
|
|
|
|
|
+
|
|
|
|
|
+interface ColumnMeta {
|
|
|
|
|
+ key: string
|
|
|
|
|
+ label: string
|
|
|
|
|
+ visible?: boolean
|
|
|
|
|
+ action: boolean
|
|
|
|
|
+ configurable: boolean
|
|
|
|
|
+ fixed: ColumnFixed
|
|
|
|
|
+ children: ColumnMeta[]
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+interface Props
|
|
|
|
|
+ extends /* @vue-ignore */ Partial<
|
|
|
|
|
+ Omit<TableProps<T extends DefaultRow ? T : DefaultRow>, 'data'>
|
|
|
|
|
+ > {
|
|
|
|
|
+ data: T[]
|
|
|
|
|
+ loading: boolean
|
|
|
|
|
+ customClass?: boolean
|
|
|
|
|
+ showBorder?: boolean
|
|
|
|
|
+ hoverHighlight?: boolean
|
|
|
|
|
+ align?: ColumnAlign
|
|
|
|
|
+ columnMaxWidth?: number
|
|
|
|
|
+ settingsCache?: boolean
|
|
|
|
|
+ settingsCacheKey?: string
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const props = defineProps<Props>()
|
|
|
|
|
+const attrs = useAttrs()
|
|
|
|
|
+const slots = useSlots()
|
|
|
|
|
+const route = useRoute()
|
|
|
|
|
+const tableRef = ref<TableInstance>()
|
|
|
|
|
+const columnSettings = ref<ColumnSettingItem[]>([])
|
|
|
|
|
+const columnDefaultSettings = ref<ColumnSettingItem[]>([])
|
|
|
|
|
+const slotColumnSignature = ref('')
|
|
|
|
|
+const columnSettingsStorageKey = ref('')
|
|
|
|
|
+const isSyncingColumnSettings = ref(false)
|
|
|
|
|
+const columnSettingsStoragePrefix = 'zm-table:column-settings:'
|
|
|
|
|
+
|
|
|
|
|
+const defaultOptions: Partial<Props> = {
|
|
|
|
|
+ size: 'default',
|
|
|
|
|
+ stripe: true,
|
|
|
|
|
+ border: true,
|
|
|
|
|
+ highlightCurrentRow: true,
|
|
|
|
|
+ showOverflowTooltip: true,
|
|
|
|
|
+ scrollbarAlwaysOn: true,
|
|
|
|
|
+ showBorder: false,
|
|
|
|
|
+ hoverHighlight: true,
|
|
|
|
|
+ customClass: false,
|
|
|
|
|
+ tooltipOptions: {
|
|
|
|
|
+ popperClass: 'max-w-120'
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const bindProps = computed(() => {
|
|
|
|
|
+ const {
|
|
|
|
|
+ data,
|
|
|
|
|
+ customClass: _customClass,
|
|
|
|
|
+ showBorder: _showBorder,
|
|
|
|
|
+ hoverHighlight: _hoverHighlight,
|
|
|
|
|
+ align: _align,
|
|
|
|
|
+ columnMaxWidth,
|
|
|
|
|
+ settingsCache: _settingsCache,
|
|
|
|
|
+ settingsCacheKey: _settingsCacheKey,
|
|
|
|
|
+ ...otherProps
|
|
|
|
|
+ } = props
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ ...defaultOptions,
|
|
|
|
|
+ ...attrs,
|
|
|
|
|
+ ...otherProps,
|
|
|
|
|
+ data: data || []
|
|
|
|
|
+ }
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+const safeData = computed(() => props.data || [])
|
|
|
|
|
+const safeLoading = computed(() => props.loading)
|
|
|
|
|
+const safeColumnAlign = computed<ColumnAlign>(() => {
|
|
|
|
|
+ return props.align === 'left' || props.align === 'right' || props.align === 'center'
|
|
|
|
|
+ ? props.align
|
|
|
|
|
+ : 'center'
|
|
|
|
|
+})
|
|
|
|
|
+const safeColumnMaxWidth = computed(() => {
|
|
|
|
|
+ const maxWidth = Number(props.columnMaxWidth)
|
|
|
|
|
+ return Number.isFinite(maxWidth) && maxWidth > 0 ? maxWidth : 360
|
|
|
|
|
+})
|
|
|
|
|
+const isColumnSettingsCacheEnabled = computed(() => props.settingsCache !== false)
|
|
|
|
|
+
|
|
|
|
|
+const forwardedSlots = computed(() => {
|
|
|
|
|
+ const { default: _default, ...restSlots } = slots
|
|
|
|
|
+ return restSlots
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+const getPropValue = (props: Record<string, any> | null | undefined, ...keys: string[]) => {
|
|
|
|
|
+ if (!props) return undefined
|
|
|
|
|
+ for (const key of keys) {
|
|
|
|
|
+ if (props[key] !== undefined) return props[key]
|
|
|
|
|
+ }
|
|
|
|
|
+ return undefined
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const toBoolean = (value: unknown) => value === true || value === ''
|
|
|
|
|
+
|
|
|
|
|
+const normalizeFixed = (fixed: unknown): ColumnFixed => {
|
|
|
|
|
+ if (fixed === true || fixed === 'left') return 'left'
|
|
|
|
|
+ if (fixed === 'right') return 'right'
|
|
|
|
|
+ return false
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const isColumnNode = (node: VNode) => {
|
|
|
|
|
+ if (!node.type && typeof node.type !== 'object') return false
|
|
|
|
|
+ const nodeType = node.type as { __name?: string }
|
|
|
|
|
+ return nodeType.__name === 'ZmTableColumn'
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const canResolveColumnChildren = (node: VNode) => {
|
|
|
|
|
+ const nodeProps = node.props
|
|
|
|
|
+ return toBoolean(getPropValue(nodeProps, 'isParent', 'is-parent'))
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const getColumnKey = (node: VNode, index: number, parentKey?: string) => {
|
|
|
|
|
+ const nodeProps = node.props
|
|
|
|
|
+ const key = getPropValue(nodeProps, 'columnKey', 'column-key')
|
|
|
|
|
+ const prop = getPropValue(nodeProps, 'prop')
|
|
|
|
|
+ const type = getPropValue(nodeProps, 'type')
|
|
|
|
|
+ const label = getPropValue(nodeProps, 'label')
|
|
|
|
|
+ const fallbackKey = parentKey ? `${parentKey}.column-${index}` : `column-${index}`
|
|
|
|
|
+ return String(key ?? prop ?? type ?? label ?? fallbackKey)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const getColumnLabel = (node: VNode, index: number, parentKey?: string) => {
|
|
|
|
|
+ const label = getPropValue(node.props, 'label')
|
|
|
|
|
+ return String(label ?? getColumnKey(node, index, parentKey))
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const getColumnChildren = (node: VNode) => {
|
|
|
|
|
+ if (!canResolveColumnChildren(node)) return []
|
|
|
|
|
+ const children = node.children
|
|
|
|
|
+ if (children && typeof children === 'object' && 'default' in children) {
|
|
|
|
|
+ const defaultSlot = (children as { default?: unknown }).default
|
|
|
|
|
+ if (typeof defaultSlot !== 'function') return []
|
|
|
|
|
+ try {
|
|
|
|
|
+ const slotNodes = defaultSlot()
|
|
|
|
|
+ const normalizedNodes = Array.isArray(slotNodes) ? slotNodes : slotNodes ? [slotNodes] : []
|
|
|
|
|
+ return flattenSlotNodes(normalizedNodes as VNode[]).filter(isColumnNode)
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ return []
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return []
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const getColumnMeta = (node: VNode, index: number, parentKey?: string): ColumnMeta => {
|
|
|
|
|
+ const key = getColumnKey(node, index, parentKey)
|
|
|
|
|
+ const action = toBoolean(getPropValue(node.props, 'action'))
|
|
|
|
|
+ const hideInColumnSettings = toBoolean(
|
|
|
|
|
+ getPropValue(node.props, 'hideInColumnSettings', 'hide-in-column-settings')
|
|
|
|
|
+ )
|
|
|
|
|
+ return {
|
|
|
|
|
+ key,
|
|
|
|
|
+ label: getColumnLabel(node, index, parentKey),
|
|
|
|
|
+ visible: getPropValue(node.props, 'visible'),
|
|
|
|
|
+ action,
|
|
|
|
|
+ configurable: !hideInColumnSettings,
|
|
|
|
|
+ fixed: normalizeFixed(getPropValue(node.props, 'fixed')),
|
|
|
|
|
+ children: getColumnChildren(node)
|
|
|
|
|
+ .map((child, childIndex) => getColumnMeta(child, childIndex, key))
|
|
|
|
|
+ .filter((item) => item.configurable)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const flattenSlotNodes = (nodes: Array<VNode | null | undefined>): VNode[] => {
|
|
|
|
|
+ return nodes.flatMap((node) => {
|
|
|
|
|
+ if (!node) return []
|
|
|
|
|
+ if (node.type === Fragment && Array.isArray(node.children)) {
|
|
|
|
|
+ return flattenSlotNodes(node.children as VNode[])
|
|
|
|
|
+ }
|
|
|
|
|
+ if (node.type === Comment || node.type === Text) return []
|
|
|
|
|
+ return [node]
|
|
|
|
|
+ })
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const syncColumnSettings = (nodes: VNode[]) => {
|
|
|
|
|
+ const metas = nodes
|
|
|
|
|
+ .map((node, index) => getColumnMeta(node, index))
|
|
|
|
|
+ .filter((item) => item.configurable)
|
|
|
|
|
+ const signature = JSON.stringify(metas)
|
|
|
|
|
+ if (signature === slotColumnSignature.value) return
|
|
|
|
|
+ slotColumnSignature.value = signature
|
|
|
|
|
+ nextTick(() => {
|
|
|
|
|
+ const defaults = metas.map(createDefaultColumnSetting)
|
|
|
|
|
+ const storageKey = resolveColumnSettingsStorageKey(defaults)
|
|
|
|
|
+ const cachedSettings = loadColumnSettings(storageKey)
|
|
|
|
|
+ columnDefaultSettings.value = defaults
|
|
|
|
|
+ columnSettingsStorageKey.value = storageKey
|
|
|
|
|
+ setColumnSettings(mergeColumnSettings(defaults, cachedSettings || columnSettings.value))
|
|
|
|
|
+ })
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const createDefaultColumnSetting = (meta: ColumnMeta): ColumnSettingItem => ({
|
|
|
|
|
+ key: meta.key,
|
|
|
|
|
+ label: meta.label,
|
|
|
|
|
+ visible: true,
|
|
|
|
|
+ fixed: meta.fixed,
|
|
|
|
|
+ children: meta.children.length ? meta.children.map(createDefaultColumnSetting) : undefined
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+const mergeColumnSettings = (
|
|
|
|
|
+ defaults: ColumnSettingItem[],
|
|
|
|
|
+ current: ColumnSettingItem[]
|
|
|
|
|
+): ColumnSettingItem[] => {
|
|
|
|
|
+ const defaultMap = new Map(defaults.map((item) => [item.key, item]))
|
|
|
|
|
+ const currentMap = new Map(current.map((item) => [item.key, item]))
|
|
|
|
|
+ const existingItems = current
|
|
|
|
|
+ .filter((item) => defaultMap.has(item.key))
|
|
|
|
|
+ .map((item) => {
|
|
|
|
|
+ const defaultItem = defaultMap.get(item.key)!
|
|
|
|
|
+ return {
|
|
|
|
|
+ ...defaultItem,
|
|
|
|
|
+ visible: item.visible,
|
|
|
|
|
+ fixed: item.fixed,
|
|
|
|
|
+ children: defaultItem.children
|
|
|
|
|
+ ? mergeColumnSettings(defaultItem.children, item.children || [])
|
|
|
|
|
+ : undefined
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ const addedItems = defaults.filter((item) => !currentMap.has(item.key))
|
|
|
|
|
+ return [...existingItems, ...addedItems]
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const mapColumnSettings = (
|
|
|
|
|
+ items: ColumnSettingItem[],
|
|
|
|
|
+ mapper: (item: ColumnSettingItem) => ColumnSettingItem
|
|
|
|
|
+): ColumnSettingItem[] => {
|
|
|
|
|
+ return items.map((item) => {
|
|
|
|
|
+ const mappedItem = mapper(item)
|
|
|
|
|
+ return {
|
|
|
|
|
+ ...mappedItem,
|
|
|
|
|
+ children: mappedItem.children ? mapColumnSettings(mappedItem.children, mapper) : undefined
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const updateColumnVisible = (key: string, visible: boolean) => {
|
|
|
|
|
+ columnSettings.value = mapColumnSettings(columnSettings.value, (item) =>
|
|
|
|
|
+ item.key === key ? { ...item, visible } : item
|
|
|
|
|
+ )
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const updateColumnFixed = (key: string, fixed: ColumnFixed) => {
|
|
|
|
|
+ columnSettings.value = mapColumnSettings(columnSettings.value, (item) =>
|
|
|
|
|
+ item.key === key ? { ...item, fixed } : item
|
|
|
|
|
+ )
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const sortSettingsByKeys = (items: ColumnSettingItem[], keys: string[]) => {
|
|
|
|
|
+ const orderMap = new Map(keys.map((key, index) => [key, index]))
|
|
|
|
|
+ return [...items].sort((a, b) => {
|
|
|
|
|
+ const orderA = orderMap.get(a.key) ?? Number.MAX_SAFE_INTEGER
|
|
|
|
|
+ const orderB = orderMap.get(b.key) ?? Number.MAX_SAFE_INTEGER
|
|
|
|
|
+ return orderA - orderB
|
|
|
|
|
+ })
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const updateColumnOrder = (keys: string[], parentKey?: string) => {
|
|
|
|
|
+ if (!parentKey) {
|
|
|
|
|
+ columnSettings.value = sortSettingsByKeys(columnSettings.value, keys)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ columnSettings.value = mapColumnSettings(columnSettings.value, (item) => {
|
|
|
|
|
+ if (item.key !== parentKey) return item
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ ...item,
|
|
|
|
|
+ children: item.children ? sortSettingsByKeys(item.children, keys) : item.children
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const cloneColumnSettings = (items: ColumnSettingItem[]): ColumnSettingItem[] => {
|
|
|
|
|
+ return items.map((item) => ({
|
|
|
|
|
+ ...item,
|
|
|
|
|
+ children: item.children ? cloneColumnSettings(item.children) : undefined
|
|
|
|
|
+ }))
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const resetColumnSettings = () => {
|
|
|
|
|
+ setColumnSettings(cloneColumnSettings(columnDefaultSettings.value))
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const flattenColumnSettingKeys = (items: ColumnSettingItem[]): string[] => {
|
|
|
|
|
+ return items.flatMap((item) => [
|
|
|
|
|
+ item.key,
|
|
|
|
|
+ ...(item.children ? flattenColumnSettingKeys(item.children) : [])
|
|
|
|
|
+ ])
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const encodeStorageKeyPart = (value: string) => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ return encodeURIComponent(value)
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ return value
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const resolveColumnSettingsStorageKey = (defaults: ColumnSettingItem[]) => {
|
|
|
|
|
+ if (!isColumnSettingsCacheEnabled.value) return ''
|
|
|
|
|
+
|
|
|
|
|
+ const userKey = props.settingsCacheKey?.trim()
|
|
|
|
|
+ if (userKey) return `${columnSettingsStoragePrefix}${encodeStorageKeyPart(userKey)}`
|
|
|
|
|
+
|
|
|
|
|
+ const routeKey = String(route.name || route.path || 'global')
|
|
|
|
|
+ const columnKey = flattenColumnSettingKeys(defaults).sort().join('|')
|
|
|
|
|
+ return `${columnSettingsStoragePrefix}${encodeStorageKeyPart(`${routeKey}:${columnKey}`)}`
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const isColumnSettingItem = (item: unknown): item is ColumnSettingItem => {
|
|
|
|
|
+ if (!item || typeof item !== 'object') return false
|
|
|
|
|
+ const setting = item as ColumnSettingItem
|
|
|
|
|
+ const validFixed =
|
|
|
|
|
+ setting.fixed === false || setting.fixed === 'left' || setting.fixed === 'right'
|
|
|
|
|
+ return typeof setting.key === 'string' && typeof setting.visible === 'boolean' && validFixed
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const normalizeCachedColumnSettings = (items: unknown): ColumnSettingItem[] => {
|
|
|
|
|
+ if (!Array.isArray(items)) return []
|
|
|
|
|
+
|
|
|
|
|
+ return items.filter(isColumnSettingItem).map((item) => ({
|
|
|
|
|
+ key: item.key,
|
|
|
|
|
+ label: typeof item.label === 'string' ? item.label : item.key,
|
|
|
|
|
+ visible: item.visible,
|
|
|
|
|
+ fixed: item.fixed,
|
|
|
|
|
+ children: item.children ? normalizeCachedColumnSettings(item.children) : undefined
|
|
|
|
|
+ }))
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const loadColumnSettings = (storageKey: string) => {
|
|
|
|
|
+ if (!storageKey || typeof window === 'undefined') return null
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const rawValue = window.localStorage.getItem(storageKey)
|
|
|
|
|
+ if (!rawValue) return null
|
|
|
|
|
+
|
|
|
|
|
+ const cachedValue = JSON.parse(rawValue)
|
|
|
|
|
+ const settings = normalizeCachedColumnSettings(cachedValue?.settings)
|
|
|
|
|
+ return settings.length ? settings : null
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ return null
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const saveColumnSettings = (storageKey: string, settings: ColumnSettingItem[]) => {
|
|
|
|
|
+ if (!storageKey || typeof window === 'undefined') return
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ window.localStorage.setItem(
|
|
|
|
|
+ storageKey,
|
|
|
|
|
+ JSON.stringify({
|
|
|
|
|
+ version: 1,
|
|
|
|
|
+ settings
|
|
|
|
|
+ })
|
|
|
|
|
+ )
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ // localStorage 可能被浏览器策略禁用,失败时不影响表格正常使用。
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const setColumnSettings = (settings: ColumnSettingItem[]) => {
|
|
|
|
|
+ isSyncingColumnSettings.value = true
|
|
|
|
|
+ columnSettings.value = settings
|
|
|
|
|
+ nextTick(() => {
|
|
|
|
|
+ isSyncingColumnSettings.value = false
|
|
|
|
|
+ saveColumnSettings(columnSettingsStorageKey.value, columnSettings.value)
|
|
|
|
|
+ })
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+watch(
|
|
|
|
|
+ columnSettings,
|
|
|
|
|
+ (settings) => {
|
|
|
|
|
+ if (isSyncingColumnSettings.value) return
|
|
|
|
|
+ saveColumnSettings(columnSettingsStorageKey.value, settings)
|
|
|
|
|
+ },
|
|
|
|
|
+ { deep: true }
|
|
|
|
|
+)
|
|
|
|
|
+
|
|
|
|
|
+watch(
|
|
|
|
|
+ [() => props.settingsCache, () => props.settingsCacheKey, () => route.name, () => route.path],
|
|
|
|
|
+ () => {
|
|
|
|
|
+ slotColumnSignature.value = ''
|
|
|
|
|
+ }
|
|
|
|
|
+)
|
|
|
|
|
+
|
|
|
|
|
+const applyColumnSetting = (node: VNode, setting: ColumnSettingItem) => {
|
|
|
|
|
+ const clonedNode = cloneVNode(
|
|
|
|
|
+ node,
|
|
|
|
|
+ {
|
|
|
|
|
+ columnKey: setting.key,
|
|
|
|
|
+ fixed: setting.fixed || undefined,
|
|
|
|
|
+ key: `${setting.key}-${setting.visible}-${setting.fixed || 'none'}`
|
|
|
|
|
+ },
|
|
|
|
|
+ true
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ const childNodes = getColumnChildren(node)
|
|
|
|
|
+ if (!childNodes.length || !setting.children?.length) return clonedNode
|
|
|
|
|
+
|
|
|
|
|
+ const originalChildren =
|
|
|
|
|
+ clonedNode.children && typeof clonedNode.children === 'object' ? clonedNode.children : {}
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ ...clonedNode,
|
|
|
|
|
+ children: {
|
|
|
|
|
+ ...(originalChildren as Record<string, unknown>),
|
|
|
|
|
+ default: () => renderColumnNodes(childNodes, setting.children || [], setting.key)
|
|
|
|
|
+ }
|
|
|
|
|
+ } as VNode
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const getSettingVisible = (meta: ColumnMeta, setting?: ColumnSettingItem) => {
|
|
|
|
|
+ return meta.visible ?? setting?.visible ?? true
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const isColumnVisible = (meta: ColumnMeta, setting?: ColumnSettingItem) => {
|
|
|
|
|
+ if (!getSettingVisible(meta, setting)) return false
|
|
|
|
|
+ if (!meta.children.length) return true
|
|
|
|
|
+
|
|
|
|
|
+ const childrenSettings = setting?.children || []
|
|
|
|
|
+ const childSettingMap = new Map(childrenSettings.map((item) => [item.key, item]))
|
|
|
|
|
+ return meta.children.some((child) => isColumnVisible(child, childSettingMap.get(child.key)))
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const renderColumnNodes = (nodes: VNode[], settings: ColumnSettingItem[], parentKey?: string) => {
|
|
|
|
|
+ const settingMap = new Map(settings.map((item) => [item.key, item]))
|
|
|
|
|
+ const orderMap = new Map(settings.map((item, index) => [item.key, index]))
|
|
|
|
|
+ const sortedConfigurableNodes = nodes
|
|
|
|
|
+ .map((node, index) => ({ node, meta: getColumnMeta(node, index, parentKey) }))
|
|
|
|
|
+ .filter(({ meta }) => {
|
|
|
|
|
+ const setting = settingMap.get(meta.key)
|
|
|
|
|
+ return meta.configurable && setting && isColumnVisible(meta, setting)
|
|
|
|
|
+ })
|
|
|
|
|
+ .sort((a, b) => {
|
|
|
|
|
+ const orderA = orderMap.get(a.meta.key) ?? Number.MAX_SAFE_INTEGER
|
|
|
|
|
+ const orderB = orderMap.get(b.meta.key) ?? Number.MAX_SAFE_INTEGER
|
|
|
|
|
+ return orderA - orderB
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ return nodes.flatMap((node, index) => {
|
|
|
|
|
+ const meta = getColumnMeta(node, index, parentKey)
|
|
|
|
|
+
|
|
|
|
|
+ const setting = settingMap.get(meta.key)
|
|
|
|
|
+
|
|
|
|
|
+ if (!isColumnVisible(meta, setting)) return []
|
|
|
|
|
+
|
|
|
|
|
+ if (!meta.configurable) return [node]
|
|
|
|
|
+
|
|
|
|
|
+ const nextColumn = sortedConfigurableNodes.shift()
|
|
|
|
|
+ if (!nextColumn) return []
|
|
|
|
|
+
|
|
|
|
|
+ const nextColumnSetting = settingMap.get(nextColumn.meta.key)
|
|
|
|
|
+ return nextColumnSetting
|
|
|
|
|
+ ? [applyColumnSetting(nextColumn.node, nextColumnSetting)]
|
|
|
|
|
+ : [nextColumn.node]
|
|
|
|
|
+ })
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const renderDefaultSlot = () => {
|
|
|
|
|
+ const nodes = flattenSlotNodes(slots.default?.() || [])
|
|
|
|
|
+ syncColumnSettings(nodes)
|
|
|
|
|
+ return renderColumnNodes(nodes, columnSettings.value)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const TableDefaultSlot = () => renderDefaultSlot()
|
|
|
|
|
+
|
|
|
|
|
+provide(TableContextKey, {
|
|
|
|
|
+ data: safeData,
|
|
|
|
|
+ loading: safeLoading,
|
|
|
|
|
+ columnAlign: safeColumnAlign,
|
|
|
|
|
+ columnMaxWidth: safeColumnMaxWidth,
|
|
|
|
|
+ columnSettings,
|
|
|
|
|
+ updateColumnVisible,
|
|
|
|
|
+ updateColumnFixed,
|
|
|
|
|
+ updateColumnOrder,
|
|
|
|
|
+ resetColumnSettings
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+defineExpose({
|
|
|
|
|
+ elTableRef: tableRef
|
|
|
|
|
+})
|
|
|
|
|
+</script>
|
|
|
|
|
+
|
|
|
|
|
+<template>
|
|
|
|
|
+ <el-table
|
|
|
|
|
+ ref="tableRef"
|
|
|
|
|
+ v-loading="loading"
|
|
|
|
|
+ :class="{
|
|
|
|
|
+ 'zm-table': !customClass,
|
|
|
|
|
+ 'show-border': showBorder,
|
|
|
|
|
+ 'is-hover-highlight-disabled': hoverHighlight === false
|
|
|
|
|
+ }"
|
|
|
|
|
+ v-bind="bindProps"
|
|
|
|
|
+ :data="data">
|
|
|
|
|
+ <template v-for="(_, name) in forwardedSlots" #[name]="slotData">
|
|
|
|
|
+ <slot :name="name" v-bind="slotData || {}"></slot>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ <TableDefaultSlot />
|
|
|
|
|
+ </el-table>
|
|
|
|
|
+</template>
|
|
|
|
|
+
|
|
|
|
|
+<style lang="scss">
|
|
|
|
|
+.zm-table {
|
|
|
|
|
+ --zm-table-font-family: inherit;
|
|
|
|
|
+ --zm-table-font-size: 12px;
|
|
|
|
|
+ --zm-table-text-color: #40546d;
|
|
|
|
|
+ --zm-table-strong-text-color: #24364d;
|
|
|
|
|
+ --zm-table-row-font-weight: 500;
|
|
|
|
|
+ --zm-table-bg: var(--el-bg-color);
|
|
|
|
|
+ --zm-table-border-color: #e7edf4;
|
|
|
|
|
+ --zm-table-radius: 10px;
|
|
|
|
|
+ --zm-table-header-bg: var(--el-fill-color-extra-light, #f7f9fc);
|
|
|
|
|
+ --zm-table-header-text-color: var(--el-text-color-secondary, #6b7f99);
|
|
|
|
|
+ --zm-table-header-border-color: var(--zm-table-border-color);
|
|
|
|
|
+ --zm-table-header-cell-height: 36px;
|
|
|
|
|
+ --zm-table-header-group-cell-height: 42px;
|
|
|
|
|
+ --zm-table-header-font-size: var(--zm-table-font-size);
|
|
|
|
|
+ --zm-table-header-font-weight: 600;
|
|
|
|
|
+ --zm-table-header-line-height: 16px;
|
|
|
|
|
+ --zm-table-header-icon-btn-size: 18px;
|
|
|
|
|
+ --zm-table-header-icon-btn-color: #8aa0b8;
|
|
|
|
|
+ --zm-table-header-icon-btn-radius: 4px;
|
|
|
|
|
+ --zm-table-header-icon-btn-hover-color: var(--el-color-primary);
|
|
|
|
|
+ --zm-table-header-icon-btn-hover-bg: var(--el-color-primary-light-9);
|
|
|
|
|
+ --zm-table-header-icon-btn-active-color: var(--zm-table-header-icon-btn-hover-color);
|
|
|
|
|
+ --zm-table-header-icon-btn-active-bg: var(--zm-table-header-icon-btn-hover-bg);
|
|
|
|
|
+ --zm-table-header-icon-size: 16px;
|
|
|
|
|
+ --zm-table-row-border-color: #edf2f7;
|
|
|
|
|
+ --zm-table-stripe-bg: #fcfdff;
|
|
|
|
|
+ --zm-table-hover-bg: #f5f9ff;
|
|
|
|
|
+ --zm-table-current-bg: #eef6ff;
|
|
|
|
|
+ --zm-table-summary-bg: #f7f9fc;
|
|
|
|
|
+ --zm-table-summary-text-color: var(--zm-table-strong-text-color);
|
|
|
|
|
+ --zm-table-summary-font-weight: 600;
|
|
|
|
|
+ --zm-table-summary-border-color: var(--zm-table-border-color);
|
|
|
|
|
+ --zm-table-cell-height: 38px;
|
|
|
|
|
+ --zm-table-cell-padding-x: 8px;
|
|
|
|
|
+ --zm-table-cell-first-padding-left: 0px;
|
|
|
|
|
+ --zm-table-cell-last-padding-right: 0px;
|
|
|
|
|
+ --zm-table-cell-line-height: 18px;
|
|
|
|
|
+ --zm-table-empty-min-height: 148px;
|
|
|
|
|
+ --zm-table-empty-bg: var(--zm-table-bg);
|
|
|
|
|
+ --zm-table-empty-text-font-size: var(--zm-table-font-size);
|
|
|
|
|
+ --zm-table-empty-text-color: var(--el-text-color-secondary);
|
|
|
|
|
+ --zm-table-scrollbar-size: 7px;
|
|
|
|
|
+ --zm-table-scrollbar-thumb-bg: #b8c5d6;
|
|
|
|
|
+
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ font-family: var(--zm-table-font-family);
|
|
|
|
|
+ font-size: var(--zm-table-font-size);
|
|
|
|
|
+ color: var(--zm-table-text-color);
|
|
|
|
|
+ background: var(--zm-table-bg);
|
|
|
|
|
+ border: 1px solid var(--zm-table-border-color);
|
|
|
|
|
+ border-radius: var(--zm-table-radius);
|
|
|
|
|
+ box-shadow: none;
|
|
|
|
|
+
|
|
|
|
|
+ &::before,
|
|
|
|
|
+ &::after {
|
|
|
|
|
+ display: none;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .el-table__inner-wrapper {
|
|
|
|
|
+ &::before,
|
|
|
|
|
+ &::after {
|
|
|
|
|
+ display: none;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .el-table__border-left-patch {
|
|
|
|
|
+ display: none;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .el-table__inner-wrapper,
|
|
|
|
|
+ .el-table__header-wrapper,
|
|
|
|
|
+ .el-table__body-wrapper,
|
|
|
|
|
+ .el-scrollbar__wrap {
|
|
|
|
|
+ background: transparent;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .el-table__inner-wrapper {
|
|
|
|
|
+ border-radius: var(--zm-table-radius);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .el-table__cell {
|
|
|
|
|
+ height: var(--zm-table-cell-height);
|
|
|
|
|
+ padding: 0;
|
|
|
|
|
+ color: var(--zm-table-text-color);
|
|
|
|
|
+ background: var(--zm-table-bg);
|
|
|
|
|
+ border-right: 1px solid var(--zm-table-row-border-color) !important;
|
|
|
|
|
+ border-bottom: 1px solid var(--zm-table-row-border-color) !important;
|
|
|
|
|
+ transition:
|
|
|
|
|
+ background-color 0.16s ease,
|
|
|
|
|
+ color 0.16s ease;
|
|
|
|
|
+
|
|
|
|
|
+ &:last-child {
|
|
|
|
|
+ border-right: none !important;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .cell {
|
|
|
|
|
+ padding-right: var(--zm-table-cell-padding-x);
|
|
|
|
|
+ padding-left: var(--zm-table-cell-padding-x);
|
|
|
|
|
+ line-height: var(--zm-table-cell-line-height);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .el-table__header {
|
|
|
|
|
+ color: var(--zm-table-header-text-color);
|
|
|
|
|
+
|
|
|
|
|
+ .el-table__cell {
|
|
|
|
|
+ height: var(--zm-table-header-cell-height);
|
|
|
|
|
+ font-size: var(--zm-table-header-font-size);
|
|
|
|
|
+ font-weight: var(--zm-table-header-font-weight);
|
|
|
|
|
+ color: var(--zm-table-header-text-color);
|
|
|
|
|
+ background: var(--zm-table-header-bg) !important;
|
|
|
|
|
+ border-right: 1px solid var(--zm-table-header-border-color) !important;
|
|
|
|
|
+ border-bottom: 1px solid var(--zm-table-header-border-color) !important;
|
|
|
|
|
+
|
|
|
|
|
+ .cell {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ min-height: 100%;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ padding-top: 0;
|
|
|
|
|
+ padding-bottom: 0;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &:last-child {
|
|
|
|
|
+ .cell {
|
|
|
|
|
+ border-right: none;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ tr:first-child {
|
|
|
|
|
+ .el-table__cell {
|
|
|
|
|
+ &:first-child {
|
|
|
|
|
+ border-top-left-radius: var(--zm-table-radius);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &:last-child {
|
|
|
|
|
+ border-top-right-radius: var(--zm-table-radius);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ tr:not(:last-child) {
|
|
|
|
|
+ .el-table__cell {
|
|
|
|
|
+ height: var(--zm-table-header-group-cell-height);
|
|
|
|
|
+ border-bottom-color: var(--zm-table-header-border-color) !important;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .el-table__body {
|
|
|
|
|
+ tr.el-table__row--striped {
|
|
|
|
|
+ .el-table__cell {
|
|
|
|
|
+ background: var(--zm-table-stripe-bg);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ tr.current-row {
|
|
|
|
|
+ .el-table__cell {
|
|
|
|
|
+ // color: var(--el-color-primary);
|
|
|
|
|
+ background: var(--zm-table-current-bg) !important;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &:not(.is-hover-highlight-disabled) {
|
|
|
|
|
+ .el-table__body {
|
|
|
|
|
+ tr:hover,
|
|
|
|
|
+ tr.hover-row {
|
|
|
|
|
+ .el-table__cell {
|
|
|
|
|
+ background: var(--zm-table-hover-bg) !important;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .el-table__row {
|
|
|
|
|
+ .el-table__cell {
|
|
|
|
|
+ font-weight: var(--zm-table-row-font-weight);
|
|
|
|
|
+ color: var(--zm-table-strong-text-color);
|
|
|
|
|
+
|
|
|
|
|
+ &:first-child {
|
|
|
|
|
+ // .cell {
|
|
|
|
|
+ // padding-left: var(--zm-table-cell-first-padding-left);
|
|
|
|
|
+ // }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &:last-child {
|
|
|
|
|
+ // .cell {
|
|
|
|
|
+ // padding-right: var(--zm-table-cell-last-padding-right);
|
|
|
|
|
+ // }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .el-table__empty-block {
|
|
|
|
|
+ width: 100% !important;
|
|
|
|
|
+ min-width: 100%;
|
|
|
|
|
+ min-height: var(--zm-table-empty-min-height);
|
|
|
|
|
+ background: var(--zm-table-empty-bg);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .el-table__empty-text {
|
|
|
|
|
+ font-size: var(--zm-table-empty-text-font-size);
|
|
|
|
|
+ color: var(--zm-table-empty-text-color);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .el-table__footer-wrapper {
|
|
|
|
|
+ background: var(--zm-table-summary-bg);
|
|
|
|
|
+ border-top: 1px solid var(--zm-table-summary-border-color);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .el-table__footer {
|
|
|
|
|
+ color: var(--zm-table-summary-text-color);
|
|
|
|
|
+
|
|
|
|
|
+ .el-table__cell {
|
|
|
|
|
+ height: var(--zm-table-cell-height);
|
|
|
|
|
+ font-weight: var(--zm-table-summary-font-weight);
|
|
|
|
|
+ color: var(--zm-table-summary-text-color);
|
|
|
|
|
+ background: var(--zm-table-summary-bg) !important;
|
|
|
|
|
+ border-right: 1px solid var(--zm-table-row-border-color) !important;
|
|
|
|
|
+ border-bottom: none !important;
|
|
|
|
|
+
|
|
|
|
|
+ .cell {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ min-height: 100%;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &:last-child {
|
|
|
|
|
+ border-right: none !important;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ tr:last-child {
|
|
|
|
|
+ .el-table__cell {
|
|
|
|
|
+ &:first-child {
|
|
|
|
|
+ border-bottom-left-radius: var(--zm-table-radius);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &:last-child {
|
|
|
|
|
+ border-bottom-right-radius: var(--zm-table-radius);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .el-table__cell.el-table-fixed-column--left.is-last-column,
|
|
|
|
|
+ .el-table__cell.el-table-fixed-column--right.is-first-column {
|
|
|
|
|
+ box-shadow: none;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .el-table__cell.el-table-fixed-column--left,
|
|
|
|
|
+ .el-table__cell.el-table-fixed-column--right {
|
|
|
|
|
+ background: inherit;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .el-table__cell.el-table-fixed-column--left.is-last-column {
|
|
|
|
|
+ box-shadow: 6px 0 12px -10px rgb(15 23 42 / 22%);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .el-table__cell.el-table-fixed-column--right.is-first-column {
|
|
|
|
|
+ box-shadow: -6px 0 12px -10px rgb(15 23 42 / 22%);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .el-table__fixed-right-patch {
|
|
|
|
|
+ background: var(--zm-table-header-bg);
|
|
|
|
|
+ border-bottom: 1px solid var(--zm-table-header-border-color);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .el-scrollbar__bar {
|
|
|
|
|
+ &.is-horizontal {
|
|
|
|
|
+ height: var(--zm-table-scrollbar-size);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &.is-vertical {
|
|
|
|
|
+ width: var(--zm-table-scrollbar-size);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .el-scrollbar__thumb {
|
|
|
|
|
+ background: var(--zm-table-scrollbar-thumb-bg);
|
|
|
|
|
+ border-radius: 999px;
|
|
|
|
|
+ opacity: 0.55;
|
|
|
|
|
+
|
|
|
|
|
+ &:hover {
|
|
|
|
|
+ opacity: 0.85;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.zm-table:not(.show-border) {
|
|
|
|
|
+ .el-table__header {
|
|
|
|
|
+ .el-table__cell {
|
|
|
|
|
+ border-right-color: var(--zm-table-header-border-color) !important;
|
|
|
|
|
+
|
|
|
|
|
+ .cell {
|
|
|
|
|
+ border-right: none;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &:last-child {
|
|
|
|
|
+ .cell {
|
|
|
|
|
+ border-right: none;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+</style>
|