616 lines
18 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>