616 lines
18 KiB
Vue
616 lines
18 KiB
Vue
<script lang="tsx">
|
||
import {
|
||
ElTable,
|
||
ElTableColumn,
|
||
ElPagination,
|
||
ComponentSize,
|
||
ElTooltipProps,
|
||
ElImage,
|
||
ElEmpty,
|
||
ElCard
|
||
} from 'element-plus'
|
||
import { defineComponent, PropType, ref, computed, unref, watch, onMounted } from 'vue'
|
||
import { propTypes } from '@/utils/propTypes'
|
||
import { setIndex } from './helper'
|
||
import type { TableProps, TableColumn, Pagination, TableSetProps } from './types'
|
||
import { set, get } from 'lodash-es'
|
||
import { CSSProperties } from 'vue'
|
||
import { getSlot } from '@/utils/tsxHelper'
|
||
import TableActions from './components/TableActions.vue'
|
||
import { useAppStore } from '@/store/modules/app'
|
||
import { createVideoViewer } from '@/components/VideoPlayer'
|
||
import { Icon } from '@/components/Icon'
|
||
import { BaseButton } from '@/components/Button'
|
||
|
||
const appStore = useAppStore()
|
||
|
||
export default defineComponent({
|
||
name: 'Table',
|
||
props: {
|
||
pageSize: propTypes.number.def(10),
|
||
currentPage: propTypes.number.def(1),
|
||
// 是否展示表格的工具栏
|
||
showAction: propTypes.bool.def(false),
|
||
// 是否所有的超出隐藏,优先级低于schema中的showOverflowTooltip,
|
||
showOverflowTooltip: propTypes.bool.def(true),
|
||
// 表头
|
||
columns: {
|
||
type: Array as PropType<TableColumn[]>,
|
||
default: () => []
|
||
},
|
||
// 展开行
|
||
// expand: propTypes.bool.def(false),
|
||
// 是否展示分页
|
||
pagination: {
|
||
type: Object as PropType<Pagination>,
|
||
default: (): Pagination | undefined => undefined
|
||
},
|
||
// 仅对 type=selection 的列有效,类型为 Boolean,为 true 则会在数据更新之后保留之前选中的数据(需指定 row-key)
|
||
reserveSelection: propTypes.bool.def(false),
|
||
// 加载状态
|
||
loading: propTypes.bool.def(false),
|
||
// 是否叠加索引
|
||
reserveIndex: propTypes.bool.def(false),
|
||
// 对齐方式
|
||
align: propTypes.string
|
||
.validate((v: string) => ['left', 'center', 'right'].includes(v))
|
||
.def('left'),
|
||
// 表头对齐方式
|
||
headerAlign: propTypes.string
|
||
.validate((v: string) => ['left', 'center', 'right'].includes(v))
|
||
.def('left'),
|
||
data: {
|
||
type: Array as PropType<Recordable[]>,
|
||
default: () => []
|
||
},
|
||
// 图片自动预览字段数组
|
||
imagePreview: {
|
||
type: Array as PropType<string[]>,
|
||
default: () => []
|
||
},
|
||
// 视频自动预览字段数组
|
||
videoPreview: {
|
||
type: Array as PropType<string[]>,
|
||
default: () => []
|
||
},
|
||
// sortable: propTypes.bool.def(false),
|
||
height: propTypes.oneOfType([Number, String]),
|
||
maxHeight: propTypes.oneOfType([Number, String]),
|
||
stripe: propTypes.bool.def(false),
|
||
border: propTypes.bool.def(true),
|
||
size: {
|
||
type: String as PropType<ComponentSize>,
|
||
validator: (v: ComponentSize) => ['medium', 'small', 'mini'].includes(v)
|
||
},
|
||
fit: propTypes.bool.def(true),
|
||
showHeader: propTypes.bool.def(true),
|
||
highlightCurrentRow: propTypes.bool.def(false),
|
||
currentRowKey: propTypes.oneOfType([Number, String]),
|
||
// row-class-name, 类型为 (row: Recordable, rowIndex: number) => string | string
|
||
rowClassName: {
|
||
type: [Function, String] as PropType<(row: Recordable, rowIndex: number) => string | string>,
|
||
default: ''
|
||
},
|
||
rowStyle: {
|
||
type: [Function, Object] as PropType<
|
||
(row: Recordable, rowIndex: number) => Recordable | CSSProperties
|
||
>,
|
||
default: () => undefined
|
||
},
|
||
cellClassName: {
|
||
type: [Function, String] as PropType<
|
||
(row: Recordable, column: any, rowIndex: number) => string | string
|
||
>,
|
||
default: ''
|
||
},
|
||
cellStyle: {
|
||
type: [Function, Object] as PropType<
|
||
(row: Recordable, column: any, rowIndex: number) => Recordable | CSSProperties
|
||
>,
|
||
default: () => undefined
|
||
},
|
||
headerRowClassName: {
|
||
type: [Function, String] as PropType<(row: Recordable, rowIndex: number) => string | string>,
|
||
default: ''
|
||
},
|
||
headerRowStyle: {
|
||
type: [Function, Object] as PropType<
|
||
(row: Recordable, rowIndex: number) => Recordable | CSSProperties
|
||
>,
|
||
default: () => undefined
|
||
},
|
||
headerCellClassName: {
|
||
type: [Function, String] as PropType<
|
||
(row: Recordable, column: any, rowIndex: number) => string | string
|
||
>,
|
||
default: ''
|
||
},
|
||
headerCellStyle: {
|
||
type: [Function, Object] as PropType<
|
||
(row: Recordable, column: any, rowIndex: number) => Recordable | CSSProperties
|
||
>,
|
||
default: () => undefined
|
||
},
|
||
rowKey: propTypes.string.def('id'),
|
||
emptyText: propTypes.string.def('暂无数据'),
|
||
// 表格工具栏缓存唯一标识符
|
||
activeUID: propTypes.string.def(''),
|
||
defaultExpandAll: propTypes.bool.def(false),
|
||
expandRowKeys: {
|
||
type: Array as PropType<string[]>,
|
||
default: () => []
|
||
},
|
||
defaultSort: {
|
||
type: Object as PropType<{ prop: string; order: string }>,
|
||
default: () => ({})
|
||
},
|
||
tooltipEffect: {
|
||
type: String as PropType<'dark' | 'light'>,
|
||
default: 'dark'
|
||
},
|
||
tooltipOptions: {
|
||
type: Object as PropType<
|
||
Pick<
|
||
ElTooltipProps,
|
||
| 'effect'
|
||
| 'enterable'
|
||
| 'hideAfter'
|
||
| 'offset'
|
||
| 'placement'
|
||
| 'popperClass'
|
||
| 'popperOptions'
|
||
| 'showAfter'
|
||
| 'showArrow'
|
||
>
|
||
>,
|
||
default: () => ({
|
||
enterable: true,
|
||
placement: 'top',
|
||
showArrow: true,
|
||
hideAfter: 200,
|
||
popperOptions: { strategy: 'fixed' }
|
||
})
|
||
},
|
||
showSummary: propTypes.bool.def(false),
|
||
sumText: propTypes.string.def('Sum'),
|
||
summaryMethod: {
|
||
type: Function as PropType<(param: { columns: any[]; data: any[] }) => any[]>,
|
||
default: () => undefined
|
||
},
|
||
spanMethod: {
|
||
type: Function as PropType<
|
||
(param: { row: any; column: any; rowIndex: number; columnIndex: number }) => any[]
|
||
>,
|
||
default: () => undefined
|
||
},
|
||
selectOnIndeterminate: propTypes.bool.def(true),
|
||
indent: propTypes.number.def(16),
|
||
lazy: propTypes.bool.def(false),
|
||
load: {
|
||
type: Function as PropType<(row: Recordable, treeNode: any, resolve: Function) => void>,
|
||
default: () => undefined
|
||
},
|
||
treeProps: {
|
||
type: Object as PropType<{ hasChildren?: string; children?: string; label?: string }>,
|
||
default: () => ({ hasChildren: 'hasChildren', children: 'children', label: 'label' })
|
||
},
|
||
tableLayout: {
|
||
type: String as PropType<'auto' | 'fixed'>,
|
||
default: 'fixed'
|
||
},
|
||
scrollbarAlwaysOn: propTypes.bool.def(false),
|
||
flexible: propTypes.bool.def(false),
|
||
// 自定义内容
|
||
customContent: propTypes.bool.def(false),
|
||
cardBodyStyle: {
|
||
type: Object as PropType<CSSProperties>,
|
||
default: () => ({})
|
||
},
|
||
cardBodyClass: {
|
||
type: String as PropType<string>,
|
||
default: ''
|
||
},
|
||
cardWrapStyle: {
|
||
type: Object as PropType<CSSProperties>,
|
||
default: () => ({})
|
||
},
|
||
cardWrapClass: {
|
||
type: String as PropType<string>,
|
||
default: ''
|
||
}
|
||
},
|
||
emits: ['update:pageSize', 'update:currentPage', 'register', 'refresh'],
|
||
setup(props, { attrs, emit, slots, expose }) {
|
||
const elTableRef = ref<ComponentRef<typeof ElTable>>()
|
||
|
||
// 注册
|
||
onMounted(() => {
|
||
const tableRef = unref(elTableRef)
|
||
emit('register', tableRef?.$parent, elTableRef)
|
||
})
|
||
|
||
const pageSizeRef = ref(props.pageSize)
|
||
|
||
const currentPageRef = ref(props.currentPage)
|
||
|
||
// useTable传入的props
|
||
const outsideProps = ref<TableProps>({})
|
||
|
||
const mergeProps = ref<TableProps>({})
|
||
|
||
const getProps = computed(() => {
|
||
const propsObj = { ...props }
|
||
Object.assign(propsObj, unref(mergeProps))
|
||
return propsObj
|
||
})
|
||
|
||
const setProps = (props: TableProps = {}) => {
|
||
mergeProps.value = Object.assign(unref(mergeProps), props)
|
||
outsideProps.value = { ...props } as any
|
||
}
|
||
|
||
const setColumn = (columnProps: TableSetProps[], columnsChildren?: TableColumn[]) => {
|
||
const { columns } = unref(getProps)
|
||
for (const v of columnsChildren || columns) {
|
||
for (const item of columnProps) {
|
||
if (v.field === item.field) {
|
||
set(v, item.path, item.value)
|
||
} else if (v.children?.length) {
|
||
setColumn(columnProps, v.children)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
const addColumn = (column: TableColumn, index?: number) => {
|
||
const { columns } = unref(getProps)
|
||
if (index !== void 0) {
|
||
columns.splice(index, 0, column)
|
||
} else {
|
||
columns.push(column)
|
||
}
|
||
}
|
||
|
||
const getColumn = () => {
|
||
const { columns } = unref(getProps)
|
||
return columns
|
||
}
|
||
|
||
const delColumn = (field: string) => {
|
||
const { columns } = unref(getProps)
|
||
const index = columns.findIndex((item) => item.field === field)
|
||
if (index > -1) {
|
||
columns.splice(index, 1)
|
||
}
|
||
}
|
||
|
||
const refresh = () => {
|
||
emit('refresh')
|
||
}
|
||
|
||
const changSize = (size: ComponentSize) => {
|
||
setProps({ size })
|
||
}
|
||
|
||
expose({
|
||
setProps,
|
||
setColumn,
|
||
delColumn,
|
||
addColumn,
|
||
getColumn,
|
||
elTableRef
|
||
})
|
||
|
||
const pagination = computed(() => {
|
||
return Object.assign(
|
||
{
|
||
small: false,
|
||
background: false,
|
||
pagerCount: 7,
|
||
layout: 'sizes, prev, pager, next, jumper, ->, total',
|
||
pageSizes: [10, 20, 30, 40, 50, 100],
|
||
disabled: false,
|
||
hideOnSinglePage: false,
|
||
total: 10
|
||
},
|
||
unref(getProps).pagination
|
||
)
|
||
})
|
||
|
||
watch(
|
||
() => unref(getProps).pageSize,
|
||
(val: number) => {
|
||
pageSizeRef.value = val
|
||
}
|
||
)
|
||
|
||
watch(
|
||
() => unref(getProps).currentPage,
|
||
(val: number) => {
|
||
currentPageRef.value = val
|
||
}
|
||
)
|
||
|
||
watch(
|
||
() => pageSizeRef.value,
|
||
(val: number) => {
|
||
emit('update:pageSize', val)
|
||
}
|
||
)
|
||
|
||
watch(
|
||
() => currentPageRef.value,
|
||
(val: number) => {
|
||
emit('update:currentPage', val)
|
||
}
|
||
)
|
||
|
||
const getBindValue = computed(() => {
|
||
const bindValue: Recordable = { ...attrs, ...unref(getProps) }
|
||
delete bindValue.columns
|
||
delete bindValue.data
|
||
return bindValue
|
||
})
|
||
|
||
const renderTreeTableColumn = (columnsChildren: TableColumn[]) => {
|
||
const { align, headerAlign, showOverflowTooltip, imagePreview, videoPreview } =
|
||
unref(getProps)
|
||
return columnsChildren.map((v) => {
|
||
if (v.show === false) return null
|
||
const props = { ...v } as any
|
||
if (props.children) delete props.children
|
||
|
||
const children = v.children
|
||
|
||
const slots = {
|
||
default: (...args: any[]) => {
|
||
const data = args[0]
|
||
let isPreview = false
|
||
isPreview =
|
||
imagePreview.some((item) => (item as string) === v.field) ||
|
||
videoPreview.some((item) => (item as string) === v.field)
|
||
|
||
return children && children.length
|
||
? renderTreeTableColumn(children)
|
||
: props?.slots?.default
|
||
? props.slots.default(...args)
|
||
: v?.formatter
|
||
? v?.formatter?.(data.row, data.column, get(data.row, v.field), data.$index)
|
||
: isPreview
|
||
? renderPreview(get(data.row, v.field), v.field)
|
||
: get(data.row, v.field)
|
||
}
|
||
}
|
||
if (props?.slots?.header) {
|
||
slots['header'] = (...args: any[]) => props.slots.header(...args)
|
||
}
|
||
|
||
return (
|
||
<ElTableColumn
|
||
showOverflowTooltip={showOverflowTooltip}
|
||
align={align}
|
||
headerAlign={headerAlign}
|
||
{...props}
|
||
prop={v.field}
|
||
>
|
||
{slots}
|
||
</ElTableColumn>
|
||
)
|
||
})
|
||
}
|
||
|
||
const renderPreview = (url: string, field: string) => {
|
||
const { imagePreview, videoPreview } = unref(getProps)
|
||
return (
|
||
<div class="flex items-center">
|
||
{imagePreview.includes(field) ? (
|
||
<ElImage
|
||
src={url}
|
||
fit="cover"
|
||
class="w-[100%]"
|
||
lazy
|
||
preview-src-list={[url]}
|
||
preview-teleported
|
||
/>
|
||
) : videoPreview.includes(field) ? (
|
||
<BaseButton
|
||
type="primary"
|
||
icon={<Icon icon="ep:video-play" />}
|
||
onClick={() => {
|
||
createVideoViewer({
|
||
url
|
||
})
|
||
}}
|
||
>
|
||
预览
|
||
</BaseButton>
|
||
) : null}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
const renderTableColumn = (columnsChildren?: TableColumn[]) => {
|
||
const {
|
||
columns,
|
||
reserveIndex,
|
||
pageSize,
|
||
currentPage,
|
||
align,
|
||
headerAlign,
|
||
showOverflowTooltip,
|
||
reserveSelection,
|
||
imagePreview,
|
||
videoPreview
|
||
} = unref(getProps)
|
||
|
||
return (columnsChildren || columns).map((v) => {
|
||
if (v.show === false) return null
|
||
if (v.type === 'index') {
|
||
return (
|
||
<ElTableColumn
|
||
type="index"
|
||
index={
|
||
v.index ? v.index : (index) => setIndex(reserveIndex, index, pageSize, currentPage)
|
||
}
|
||
align={v.align || align}
|
||
headerAlign={v.headerAlign || headerAlign}
|
||
label={v.label}
|
||
width="65px"
|
||
fixed="left"
|
||
></ElTableColumn>
|
||
)
|
||
} else if (v.type === 'selection') {
|
||
return (
|
||
<ElTableColumn
|
||
type="selection"
|
||
reserveSelection={reserveSelection}
|
||
align="center"
|
||
headerAlign="center"
|
||
selectable={v.selectable}
|
||
width="50px"
|
||
fixed="left"
|
||
></ElTableColumn>
|
||
)
|
||
} else {
|
||
const props = { ...v } as any
|
||
if (props.children) delete props.children
|
||
|
||
const children = v.children
|
||
|
||
const slots = {
|
||
default: (...args: any[]) => {
|
||
const data = args[0]
|
||
|
||
let isPreview = false
|
||
isPreview =
|
||
imagePreview.some((item) => (item as string) === v.field) ||
|
||
videoPreview.some((item) => (item as string) === v.field)
|
||
|
||
return children && children.length
|
||
? renderTreeTableColumn(children)
|
||
: props?.slots?.default
|
||
? props.slots.default(...args)
|
||
: v?.formatter
|
||
? v?.formatter?.(data.row, data.column, get(data.row, v.field), data.$index)
|
||
: isPreview
|
||
? renderPreview(get(data.row, v.field), v.field)
|
||
: get(data.row, v.field)
|
||
}
|
||
}
|
||
if (props?.slots?.header) {
|
||
slots['header'] = (...args: any[]) => props.slots.header(...args)
|
||
}
|
||
|
||
return (
|
||
<ElTableColumn
|
||
showOverflowTooltip={showOverflowTooltip}
|
||
align={align}
|
||
headerAlign={headerAlign}
|
||
{...props}
|
||
prop={v.field}
|
||
>
|
||
{slots}
|
||
</ElTableColumn>
|
||
)
|
||
}
|
||
})
|
||
}
|
||
|
||
return () => {
|
||
const tableSlots = {}
|
||
if (getSlot(slots, 'empty')) {
|
||
tableSlots['empty'] = (...args: any[]) => getSlot(slots, 'empty', args)
|
||
}
|
||
if (getSlot(slots, 'append')) {
|
||
tableSlots['append'] = (...args: any[]) => getSlot(slots, 'append', args)
|
||
}
|
||
const toolbar = getSlot(slots, 'toolbar')
|
||
|
||
return (
|
||
<div v-loading={unref(getProps).loading}>
|
||
{unref(getProps).customContent ? (
|
||
<div class="flex flex-wrap">
|
||
{unref(getProps)?.data?.length ? (
|
||
unref(getProps)?.data.map((item) => {
|
||
const cardSlots = {
|
||
default: () => {
|
||
return getSlot(slots, 'content', item)
|
||
}
|
||
}
|
||
if (getSlot(slots, 'content-header')) {
|
||
cardSlots['header'] = () => {
|
||
return getSlot(slots, 'content-header', item)
|
||
}
|
||
}
|
||
if (getSlot(slots, 'content-footer')) {
|
||
cardSlots['footer'] = () => {
|
||
return getSlot(slots, 'content-footer', item)
|
||
}
|
||
}
|
||
return (
|
||
<ElCard
|
||
shadow="hover"
|
||
class={unref(getProps).cardWrapClass}
|
||
style={unref(getProps).cardWrapStyle}
|
||
bodyClass={unref(getProps).cardBodyClass}
|
||
bodyStyle={unref(getProps).cardBodyStyle}
|
||
>
|
||
{cardSlots}
|
||
</ElCard>
|
||
)
|
||
})
|
||
) : (
|
||
<div class="flex flex-1 justify-center">
|
||
<ElEmpty description="暂无数据" />
|
||
</div>
|
||
)}
|
||
</div>
|
||
) : (
|
||
<>
|
||
<div class="flex justify-between mb-1">
|
||
<div>{toolbar}</div>
|
||
<div class="pt-2">
|
||
{unref(getProps).showAction ? (
|
||
<TableActions
|
||
activeUID={unref(getProps).activeUID}
|
||
columns={unref(getProps).columns}
|
||
el-table-ref={elTableRef}
|
||
onChangSize={changSize}
|
||
onRefresh={refresh}
|
||
/>
|
||
) : null}
|
||
</div>
|
||
</div>
|
||
<ElTable
|
||
ref={elTableRef}
|
||
data={unref(getProps).data}
|
||
{...unref(getBindValue)}
|
||
header-cell-style={
|
||
appStore.getIsDark
|
||
? { color: '#CFD3DC', 'background-color': '#000' }
|
||
: { color: '#000', 'background-color': '#f5f7fa' }
|
||
}
|
||
>
|
||
{{
|
||
default: () => renderTableColumn(),
|
||
...tableSlots
|
||
}}
|
||
</ElTable>
|
||
</>
|
||
)}
|
||
|
||
{unref(getProps).pagination ? (
|
||
<ElPagination
|
||
v-model:pageSize={pageSizeRef.value}
|
||
v-model:currentPage={currentPageRef.value}
|
||
class="mt-10px"
|
||
{...unref(pagination)}
|
||
></ElPagination>
|
||
) : undefined}
|
||
</div>
|
||
)
|
||
}
|
||
}
|
||
})
|
||
</script>
|