njzscloud-dispose-web/src/components/a-page/a-table-page/ATablePage.tsx

663 lines
23 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/* @jsxImportSource vue */
import APage from '@/components/a-page/APage.vue'
import styles from '@/components/a-page/a-table-page/a-table-page.module.styl'
import {
ElButton,
ElDropdown,
ElDropdownItem,
ElDropdownMenu,
ElForm,
ElLoading,
ElMessageBox,
ElPagination,
ElPopconfirm,
ElScrollbar,
ElTable,
ElTableColumn,
ElTooltip,
type TableColumnCtx,
type TableProps,
} from 'element-plus'
import {
defineComponent,
withDirectives,
withModifiers,
} from 'vue'
import type { R } from '@/common/utils/http-util.ts'
import {
elIcons,
type ElIconType,
} from '@/common/element/element.ts'
import type { FormProps } from 'element-plus/es/components/form/src/form'
import Utils, { type ResetAble } from '@/common/utils'
import Strings from '@/common/utils/strings.ts'
import { saveFile } from '@/common/app'
import Colls from '@/common/utils/colls.ts'
import type { SetupContext } from '@vue/runtime-core'
import Types from '@/common/utils/types.ts'
import {
type IconName,
iconNames,
} from '@/components/a-icon/iconfont.ts'
import AIcon from '@/components/a-icon/AIcon.vue'
import type { DefaultRow } from 'element-plus/es/components/table/src/table/defaults'
interface ColumnScopeType<T> {
row: T,
column: TableColumnCtx,
$index: number
}
interface TableActionType<T> {
icon?: ElIconType | IconName
type?: 'text' | 'default' | 'primary' | 'success' | 'warning' | 'info' | 'danger'
tooltip: string | ((data: ColumnScopeType<T>) => string)
loading?: boolean
show?: (data: ColumnScopeType<T>) => boolean
action: (data: ColumnScopeType<T>) => Promise<boolean> | void
confirm?: {
title: string | ((data: ColumnScopeType<T>) => string)
width?: string
confirmButtonText?: string
cancelButtonText?: string
}
}
interface ActionColumnType<T> {
/**
* 列宽默认80px
*/
width: number
foldLimit: number
/**
* 按钮列表
*/
tableActions: TableActionType<T>[]
}
interface ToolType {
icon?: ElIconType
type?: 'text' | 'default' | 'primary' | 'success' | 'warning' | 'info' | 'danger'
label?: string
action: () => void
}
interface ToolBarType {
leftTools: ToolType[]
rightTools: Required<Omit<ToolType, 'type' | 'label'>>[]
}
interface TablePropsType<T extends DefaultRow, F extends object> extends Omit<TableProps<T>, 'data' | 'headerRowClassName' | 'cellClassName' | 'context'> {
treeLoad?: (param: F, row: T, expanded: any, resolve?: (data: T[]) => void) => void
/**
* 操作列配置
*/
actionColumn: ActionColumnType<T>
}
interface FormPropsType<P, T extends DefaultRow> {
/**
* 分页函数
* @param param 查询条件
*/
paging: (param: P) => Promise<R<G.PageResult<T>>>
/**
* 导出函数
* @param param 查询条件
*/
export?: (param: P) => Promise<R<{ content: Blob, filename: string }>>
/**
* 表单默认值
*/
defaultData: P
/**
* 高级查询表单配置
*/
highForm: Partial<Omit<FormProps, 'labelWidth'>> & {
/**
* 标签宽度单位px默认90px
*/
labelWidth: number,
/**
* 输入框宽度单位px默认200px
*/
contentWidth: number,
/**
* 行间隔单位px默认10px
*/
rgap: number,
/**
* 列间隔单位px默认10px
*/
cgap: number
}
/**
* 简单查询表单配置
*/
simpleForm: Partial<FormProps> & {
/**
* 列数默认1
*/
colCount: number,
/**
* 输入框宽度单位px默认200px
*/
contentWidth: number,
/**
* 行间隔单位px默认10px
*/
// rgap: number,
/**
* 列间隔单位px默认10px
*/
// cgap: number
}
}
/**
* 页布局配置
*/
interface PageLayoutType {
/**
* 查询表单高度传入数字时单位fr传入字符串时不会而外添加单位默认1fr
*/
searchFormHeight: number | string
/**
* 数据表格高度传入数字时单位fr传入字符串时不会而外添加单位默认9fr
*/
dataListHeight: number | string
showHighForm: boolean
enableHighForm: boolean
}
interface ATablePageType<P extends object, T extends DefaultRow> {
/**
* 页布局配置
*/
pageLayout: PageLayoutType
/**
* 查询表单配置
*/
searchForm: FormPropsType<P, T>
/**
* 工具栏配置
*/
toolBar: ToolBarType
/**
* 表格配置
*/
table: TablePropsType<T, P>
}
function buildSearchForm<P, T extends DefaultRow>(searchForm: DeepPartial<FormPropsType<P, T>> = {}) {
if (searchForm.defaultData == null) {
searchForm.defaultData = {} as DeepPartial<P>
}
if (searchForm.highForm == null) {
searchForm.highForm = {
labelWidth: 90,
contentWidth: 200,
rgap: 10,
cgap: 10,
}
}
if (searchForm.highForm.labelWidth == null) searchForm.highForm.labelWidth = 90
if (searchForm.highForm.contentWidth == null) searchForm.highForm.contentWidth = 200
if (searchForm.highForm.rgap == null) searchForm.highForm.rgap = 10
if (searchForm.highForm.cgap == null) searchForm.highForm.cgap = 10
if (searchForm.simpleForm == null) {
searchForm.simpleForm = {
contentWidth: 200,
colCount: 1,
// rgap: 10,
// cgap: 10,
}
}
if (searchForm.simpleForm.colCount == null) searchForm.simpleForm.colCount = 1
if (searchForm.simpleForm.contentWidth == null) searchForm.simpleForm.contentWidth = 200
// if (searchForm.simpleForm.rgap == null) searchForm.simpleForm.rgap = 10
// if (searchForm.simpleForm.cgap == null) searchForm.simpleForm.cgap = 10
return searchForm
}
function buildPageLayout(pageLayout: DeepPartial<PageLayoutType> = {}) {
if (pageLayout.searchFormHeight == null) {
pageLayout.searchFormHeight = 1
}
if (pageLayout.dataListHeight == null) {
pageLayout.dataListHeight = 9
}
if (pageLayout.showHighForm == null) {
pageLayout.showHighForm = false
}
if (pageLayout.enableHighForm == null) {
pageLayout.enableHighForm = true
}
return pageLayout
}
function buildTable<P extends object, T extends object>(table: DeepPartial<TablePropsType<T, P>> = {}) {
if (table.actionColumn == null) {
table.actionColumn = {
width: 80,
foldLimit: 3,
tableActions: [],
}
}
if (table.actionColumn.tableActions == null) {
table.actionColumn.tableActions = []
}
for (let tableAction of table.actionColumn.tableActions) {
if (tableAction.confirm != null) {
if (tableAction.confirm.cancelButtonText == null) tableAction.confirm.cancelButtonText = '否'
if (tableAction.confirm.confirmButtonText == null) tableAction.confirm.confirmButtonText = '是'
if (tableAction.confirm.width == null) tableAction.confirm.width = '180'
}
if (tableAction.type == null) {
tableAction.type = 'primary'
}
}
if (table.actionColumn.foldLimit == null) {
table.actionColumn.foldLimit = 3
}
if (table.actionColumn.width == null) {
table.actionColumn.width = Math.min(table.actionColumn.tableActions.length, table.actionColumn.foldLimit) * 50 + 10
}
if (table.emptyText == null) {
table.emptyText = '暂无数据'
}
if (table.rowKey == null) {
table.rowKey = 'id'
}
return table
}
function buildToolBar(toolBar: DeepPartial<ToolBarType> = {}) {
if (toolBar.leftTools == null) toolBar.leftTools = []
for (let leftTool of toolBar.leftTools) {
if (leftTool.type == null) {
leftTool.type = 'primary'
}
}
if (toolBar.rightTools == null) toolBar.rightTools = []
return toolBar
}
export function buildTablePageProps<P extends G.PageParam, T extends DefaultRow>({pageLayout, searchForm, toolBar, table}: DeepPartial<ATablePageType<P, T>>) {
return reactive({
pageLayout: buildPageLayout(pageLayout),
searchForm: buildSearchForm(searchForm),
toolBar: buildToolBar(toolBar),
table: buildTable(table),
})
}
const component = defineComponent(
<P extends G.PageParam, T extends DefaultRow>(props: ATablePageType<P, T>, {slots, expose}: SetupContext) => {
const formData = Utils.resetAble(reactive<P>({
...(props.searchForm.defaultData),
current: 1,
size: 20,
})) as ResetAble<P>
const tableData = Utils.resetAble(reactive<T[]>([])) as ResetAble<T[]>
const totalCount = ref(0)
const loading = ref<boolean>(false)
const showHighForm = ref<boolean>(props.pageLayout.showHighForm)
const doSearch = () => {
loading.value = true
if (props.searchForm.paging != null) {
props.searchForm.paging(formData.$clone() as P)
.then(res => {
totalCount.value = res.data?.total ?? 0
const records = res.data?.records ?? ([] as T[])
tableData.$reset(records)
})
.finally(() => {
loading.value = false
})
return
}
}
expose({doSearch})
const showHighFormHandle = () => {
showHighForm.value = !showHighForm.value
formData.$reset()
}
const doExport = () => {
if (props.searchForm.export == null) {
return
}
loading.value = true
saveFile(props.searchForm.export(formData.$clone() as P))
.finally(() => {
loading.value = false
})
}
const rowAction = (data: ColumnScopeType<T>, action: TableActionType<T>) => {
if (action.loading != null) {
action.loading = true
}
const result = action.action(data)
if (result instanceof Promise) {
result
.then(reload => {
if (reload) {
doSearch()
}
})
.finally(() => {
if (action.loading != null) {
action.loading = false
}
})
}
}
function doReset() {
formData.$reset()
doSearch()
}
const highFormCssParam = computed(() => {
const labelWidth = props.searchForm.highForm.labelWidth
const contentWidth = props.searchForm.highForm.contentWidth
return {
'--item-min-width': (labelWidth + contentWidth) + 'px',
'--rgap': props.searchForm.highForm.rgap + 'px',
'--cgap': props.searchForm.highForm.cgap + 'px',
}
})
const toolBarRightCssParam = computed(() => {
const colCount = props.searchForm.simpleForm.colCount
const contentWidth = props.searchForm.simpleForm.contentWidth
const btnCount = props.toolBar.rightTools.length + (props.searchForm.export == null ? 0 : 1) + (props.pageLayout.enableHighForm ? 1 : 0)
return {
'--form-item-count': colCount,
'--form-item-width': contentWidth + 'px',
// '--rgap': props.searchForm.simpleForm.rgap + 'px',
// '--cgap': props.searchForm.simpleForm.cgap + 'px',
'--btn-count': btnCount,
}
})
const pageCssParam = computed(() => {
const dataListHeight = props.pageLayout.dataListHeight
const searchFormHeight = props.pageLayout.searchFormHeight
return {
'--data-list-height': Types.isString(dataListHeight) ? dataListHeight : dataListHeight + 'fr',
'--search-form-height': Types.isString(searchFormHeight) ? searchFormHeight : searchFormHeight + 'fr',
}
})
const actionColumnBtnRender = (scope: ColumnScopeType<T>, tableAction: TableActionType<T>) => {
let elIcon: any | undefined = undefined
if (iconNames.includes(tableAction.icon as IconName)) {
elIcon = <AIcon name={tableAction.icon as IconName}/>
} else if (Object.keys(elIcons).includes(tableAction.icon as unknown as string)) {
elIcon = elIcons[tableAction.icon as ElIconType]
}
const tooltipTxt = tableAction.tooltip == null ? '' : (typeof tableAction.tooltip === 'function' ? tableAction.tooltip(scope) : tableAction.tooltip)
if (!Strings.isBlank(tooltipTxt)) {
return (<ElTooltip content={tooltipTxt} placement="top">
<ElButton
icon={elIcon}
loading={tableAction.loading}
type={tableAction.type}
class={styles.iconBtn}
onClick={tableAction.confirm == null ? () => rowAction(scope, tableAction) : undefined}
plain/>
</ElTooltip>)
}
return (<ElButton
icon={elIcon}
loading={tableAction.loading}
type={tableAction.type}
class={styles.iconBtn}
onClick={tableAction.confirm == null ? () => rowAction(scope, tableAction) : undefined}
text/>)
}
const actionColumnRender = () => {
const actionColumn = props.table.actionColumn
if (Colls.isEmpty(actionColumn.tableActions)) return (<></>)
return (<ElTableColumn width={actionColumn.width + 'px'} fixed="right" label="操作" prop="tableAction">
{{
default: (scope: ColumnScopeType<T>) => {
const len = actionColumn.tableActions.filter(it => it.show == null || it.show(scope)).length
let btns: any[]
if (len <= actionColumn.foldLimit) {
btns = (actionColumn.tableActions
.filter(it => (it.show == null ? true : it.show(scope)))
.map((tableAction, i) => (tableAction.confirm != null ?
(<ElPopconfirm
key={'action-btn-' + i}
cancel-button-text={tableAction.confirm.cancelButtonText}
confirm-button-text={tableAction.confirm.confirmButtonText}
title={typeof tableAction.confirm.title === 'function' ? tableAction.confirm.title(scope) : tableAction.confirm.title}
width={tableAction.confirm.width}
cancel-button-type="primary"
confirm-button-type="danger"
onConfirm={() => rowAction(scope, tableAction)}>
{{
reference: () => (<div>
{actionColumnBtnRender(scope, tableAction)}
</div>),
}}
</ElPopconfirm>) : actionColumnBtnRender(scope, tableAction)))
)
} else {
btns = (actionColumn.tableActions
.filter(it => (it.show == null ? true : it.show(scope)))
.filter((_, i) => i < actionColumn.foldLimit - 1)
.map((tableAction, i) => (tableAction.confirm != null ?
(<ElPopconfirm
key={'action-btn-' + i}
cancel-button-text={tableAction.confirm.cancelButtonText}
confirm-button-text={tableAction.confirm.confirmButtonText}
title={typeof tableAction.confirm.title === 'function' ? tableAction.confirm.title(scope) : tableAction.confirm.title}
width={tableAction.confirm.width}
cancel-button-type="primary"
confirm-button-type="danger"
onConfirm={() => rowAction(scope, tableAction)}>
{{
reference: () => (<div>
{actionColumnBtnRender(scope, tableAction)}
</div>),
}}
</ElPopconfirm>) : actionColumnBtnRender(scope, tableAction)))
)
btns.push(<ElDropdown onCommand={(cmd) => {
const tableAction = actionColumn.tableActions[cmd]
if (tableAction.confirm != null) {
ElMessageBox.confirm(
typeof tableAction.confirm.title === 'function' ? tableAction.confirm.title(scope) : tableAction.confirm.title,
'操作提示',
{
confirmButtonText: tableAction.confirm.confirmButtonText,
cancelButtonText: tableAction.confirm.cancelButtonText,
confirmButtonType: 'primary',
cancelButtonType: 'danger',
type: 'warning',
},
)
.then(() => {
rowAction(scope, tableAction)
})
.catch(() => {
})
return
}
rowAction(scope, tableAction)
}}>
{{
default: () => (<ElButton icon={elIcons.More} class={[ styles.iconBtn, styles.moreIcon ]} text/>),
dropdown: () => (<ElDropdownMenu>
{
actionColumn.tableActions
.filter(it => (it.show == null ? true : it.show(scope)))
.filter((_, i) => i >= actionColumn.foldLimit - 1)
.map((it, i) => {
let elIcon: any | undefined = undefined
if (iconNames.includes(it.icon as IconName)) {
elIcon = <AIcon name={it.icon as IconName}/>
} else if (Object.keys(elIcons).includes(it.icon as unknown as string)) {
elIcon = elIcons[it.icon as ElIconType]
}
return (
<ElDropdownItem key={'action-btn-' + i} command={i + 2} icon={elIcon}>
{typeof it.tooltip === 'function' ? it.tooltip(scope) : it.tooltip}
</ElDropdownItem>
)
})
}
</ElDropdownMenu>),
}}
</ElDropdown>)
}
return (<div class={styles.actionBtn}>
{btns}
</div>)
},
}}
</ElTableColumn>)
}
const sortChangeHandler = ({prop, order}: { prop: string, order: 'ascending' | 'descending' | null }) => {
formData.orders = order == null ? undefined : (prop + ':' + (order == 'ascending' ? 'asc' : 'desc'))
doSearch()
}
onMounted(doSearch)
return () => (<APage class={[ styles.tablePage, showHighForm.value ? '' : styles.folder ]} style={pageCssParam.value}>
<div class={styles.searchFormWrapper} style={highFormCssParam.value}>
{props.pageLayout.enableHighForm ? (<ElScrollbar>
{/*@ts-ignore*/}
<ElForm class={styles.searchForm} onSubmit={withModifiers(doSearch, [ 'prevent' ])} labelWidth={props.searchForm.highForm.labelWidth + 'px'}>
{
slots.highFormItem?.(formData)
}
<button style="display: none" type="submit"/>
</ElForm>
</ElScrollbar>) : <></>}
</div>
<div class={styles.dataList}>
<div class={styles.toolBar}>
<div class={styles.toolBarLeft}>
{
props.toolBar.leftTools.map((tool, i) => (
<ElButton key={'tool-bar-left-' + i}
icon={Strings.isBlank(tool.icon) ? undefined : elIcons[tool.icon!]}
type={tool.type}
onClick={tool.action}>
{tool.label}
</ElButton>
))
}
</div>
<div class={styles.toolBarRight} style={toolBarRightCssParam.value}>
<ElScrollbar>
{/*@ts-ignore*/}
<ElForm onSubmit={withModifiers(doSearch, [ 'prevent' ])}>
{
slots.simpleFormItem?.(formData)
}
<button style="display: none" type="submit"/>
</ElForm>
</ElScrollbar>
<div>
<ElTooltip content="搜索" placement="top">
<ElDropdown split-button onClick={doSearch} onCommand={(command: string) => {
if (command === 'reset') {
doReset()
}
}}>
{{
default: () => (<ElButton icon={elIcons.Search} loading={loading.value} type="default"/>),
dropdown: () => (<ElDropdownMenu>
<ElDropdownItem icon={elIcons.Refresh} command="reset"></ElDropdownItem>
</ElDropdownMenu>),
}}
</ElDropdown>
</ElTooltip>
{
props.toolBar.rightTools.map((tool, i) => (
<ElButton key={'tool-bar-right-' + i}
icon={elIcons[tool.icon]}
onClick={tool.action}/>
))
}
{
props.searchForm.export == null ? <></> : (<ElTooltip content="导出" placement="top">
<ElButton icon={elIcons.Download} loading={loading.value} type="default" onClick={doExport}/>
</ElTooltip>)
}
{
props.pageLayout.enableHighForm ? (<ElTooltip
content={showHighForm.value ? '关闭高级搜索' : '打开高级搜索'} placement="top">
<ElButton class={showHighForm.value ? styles.filterBtnActive : ''} icon={elIcons.Filter} type="default" onClick={showHighFormHandle}/>
</ElTooltip>) : <></>}
</div>
</div>
</div>
{
withDirectives(<ElTable
data={tableData}
empty-text={props.table.emptyText}
row-key={props.table.rowKey}
ref="dataTable"
lazy={props.table.treeLoad == null ? undefined : true}
load={props.table.treeLoad ? ((row, expanded, resolve) => props.table.treeLoad!(formData as P, row, expanded, resolve)) : undefined}
onExpand-change={props.table.treeLoad ? ((row: any, expandedRows: any) => props.table.treeLoad!(formData as P, row, expandedRows, undefined)) : undefined}
cell-class-name="table-cell"
header-row-class-name="table-header"
class="data-table"
onSort-change={sortChangeHandler}
>
{
slots.columns?.()
}
{
actionColumnRender()
}
</ElTable>, [ [ ElLoading.directive, loading.value ] ])
}
<ElPagination
current-page={(formData as G.PageParam).current}
page-size={(formData as G.PageParam).size}
onUpdate:current-page={(val) => (formData as G.PageParam).current = val}
onUpdate:page-size={(val) => (formData as G.PageParam).size = val}
hide-on-single-page={false}
page-sizes={[ 10, 20, 50, 100, 500 ]}
teleported={false}
total={totalCount.value}
background={true}
layout="total, prev, pager, next, sizes, jumper"
onChange={doSearch}/>
{
slots?.default?.()
}
</div>
</APage>)
},
{
name: 'ATablePage',
props: [ 'pageLayout', 'searchForm', 'toolBar', 'table' ],
},
)
export interface ATablePageInstance extends InstanceType<typeof component> {
doSearch: () => void
}
export default component