lzq 2026-01-27 23:58:17 +08:00
parent 9d4b5956ad
commit 771c2dff8f
6 changed files with 249 additions and 141 deletions

View File

@ -1,5 +1,5 @@
import { TypeTag } from '@/common/utils/index.ts'
import { isString } from '@/common/utils/types.ts'
import Types from '@/common/utils/types.ts'
/**
*
@ -36,7 +36,7 @@ export function isArrayLike(obj: any) {
* @param obj
*/
export function isNumStr(obj: any) {
return isString(obj) && !isNaN(Number(obj))
return Types.isString(obj) && !isNaN(Number(obj))
}
type tags = 'error' | 'info' | 'warning' | 'risk' | 'error' | 'fatal' | 'success' | undefined;

View File

@ -1,8 +1,4 @@
import {
isDate,
isNumber,
isString,
} from '@/common/utils/types'
import Types from '@/common/utils/types'
import { isNumStr } from '@/common/utils/objects.ts'
import {
type DateObjectUnits,
@ -44,11 +40,11 @@ function now() {
* @return luxon
*/
function parse(date: Date | number | string | DateObjectUnits, fmt: string = FMT.date_time_sec) {
if (isDate(date)) {
if (Types.isDate(date)) {
return DateTime.fromJSDate(date as Date)
} else if (isNumber(date) || isNumStr(date)) {
} else if (Types.isNumber(date) || isNumStr(date)) {
return DateTime.fromMillis(Number(date))
} else if (isString(date)) {
} else if (Types.isString(date)) {
return DateTime.fromFormat(date as string, fmt)
} else {
return DateTime.fromObject(date as DateObjectUnits)
@ -63,7 +59,7 @@ function parse(date: Date | number | string | DateObjectUnits, fmt: string = FMT
*/
function format(date?: DateTime<true | false> | Date, fmt: string = FMT.date_time_sec) {
if (date == null) return ''
if (isDate(date)) {
if (Types.isDate(date)) {
return DateTime.fromJSDate(date as Date).toFormat(fmt)
} else {
return (date as DateTime<true | false>).toFormat(fmt)

View File

@ -4,7 +4,7 @@ import { TypeTag } from '@/common/utils/index.ts'
*
* @param obj
*/
export function isString(obj: any) {
function isString(obj: any) {
return obj != null &&
(typeof obj === 'string' ||
(typeof obj === 'object' &&
@ -16,7 +16,7 @@ export function isString(obj: any) {
* JS Date
* @param obj
*/
export function isDate(obj: any) {
function isDate(obj: any) {
return obj != null &&
(typeof obj === 'object' &&
Object.prototype.toString.call(obj) === TypeTag.DATE)
@ -26,7 +26,7 @@ export function isDate(obj: any) {
*
* @param obj
*/
export function isNumber(obj: any) {
function isNumber(obj: any) {
return obj != null &&
(typeof obj === 'number' ||
(typeof obj === 'object' &&

View File

@ -34,6 +34,15 @@ 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'
type DeepPartial<T> = T extends Function
? T
: T extends Array<infer U>
? Array<DeepPartial<U>>
: T extends object
? { [K in keyof T]?: DeepPartial<T[K]> }
: T;
interface ColumnScopeType<T> {
row: T,
@ -41,13 +50,12 @@ interface ColumnScopeType<T> {
$index: number
}
export interface TableActionType<T> {
interface TableActionType<T> {
icon?: ElIconType
type?: 'text' | 'default' | 'primary' | 'success' | 'warning' | 'info' | 'danger'
label?: string | ((data: ColumnScopeType<T>) => string)
tooltip?: string | ((data: ColumnScopeType<T>) => string)
loading?: boolean
textBtn?: boolean
show?: (data: ColumnScopeType<T>) => boolean
action: (data: ColumnScopeType<T>) => Promise<boolean> | void
confirm?: {
@ -58,57 +66,130 @@ export interface TableActionType<T> {
}
}
export interface ActionColumnType<T> {
width?: string
interface ActionColumnType<T> {
/**
* 80px
*/
width: string
/**
*
*/
tableActions: TableActionType<T>[]
}
export interface ToolType {
interface ToolType {
icon?: ElIconType
type?: 'text' | 'default' | 'primary' | 'success' | 'warning' | 'info' | 'danger'
label?: string
action: () => void
}
export interface ToolBarType {
interface ToolBarType {
leftTools: ToolType[]
rightTools: Required<Omit<ToolType, 'type' | 'label'>>[]
}
export type TablePropsType<T extends DefaultRow, F extends object> = Omit<TableProps<T>, 'data' | 'headerRowClassName' | 'cellClassName' | 'context'> & {
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>
}
export type FormPropsType<P, T> = {
paging?: (param: P) => Promise<R<G.PageResult<T>>>
interface FormPropsType<P, T> {
/**
*
* @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<FormProps> & { colCount?: number, vgap?: string, hgap?: string }
simpleForm?: Partial<FormProps>
/**
*
*/
highForm: Partial<Omit<FormProps, 'labelWidth'>> & {
/**
* px90px
*/
labelWidth: number,
/**
* px190px
*/
contentWidth: number,
/**
* px10px
*/
rgap: number,
/**
* px10px
*/
cgap: number
}
/**
*
*/
simpleForm: Partial<FormProps>
}
/**
*
*/
interface PageLayoutType {
/* columns: string[]
rows: string[] */
dataListHeight: string
/**
* fr9fr
*/
dataListHeight: number | string
}
export interface ATablePageType<P extends object, T extends DefaultRow> {
interface ATablePageType<P extends object, T extends DefaultRow> {
/**
*
*/
pageLayout: PageLayoutType
/**
*
*/
searchForm: FormPropsType<P, T>
/**
*
*/
toolBar: ToolBarType
/**
*
*/
table: TablePropsType<T, P>
}
function buildSearchForm<P, T>(searchForm: Partial<FormPropsType<P, T>> = {}) {
function buildSearchForm<P, T>(searchForm: DeepPartial<FormPropsType<P, T>> = {}) {
if (searchForm.defaultData == null) {
searchForm.defaultData = {} as P
searchForm.defaultData = {} as DeepPartial<P>
}
if (searchForm.highForm == null) {
searchForm.highForm = {
labelWidth: 90,
contentWidth: 190,
rgap: 10,
cgap: 10,
}
}
if (searchForm.highForm.labelWidth == null) searchForm.highForm.labelWidth = 90
if (searchForm.highForm.contentWidth == null) searchForm.highForm.contentWidth = 90
if (searchForm.highForm.rgap == null) searchForm.highForm.rgap = 10
if (searchForm.highForm.cgap == null) searchForm.highForm.cgap = 10
return searchForm
}
function buildPageLayout(pageLayout: Partial<PageLayoutType> = {}) {
function buildPageLayout(pageLayout: DeepPartial<PageLayoutType> = {}) {
/* if (pageLayout.columns == null) {
pageLayout.columns = [ '1fr' ]
}
@ -117,40 +198,55 @@ function buildPageLayout(pageLayout: Partial<PageLayoutType> = {}) {
pageLayout.rows = [ '1fr', '9fr' ]
} */
if (pageLayout.dataListHeight == null) {
pageLayout.dataListHeight = '9fr'
pageLayout.dataListHeight = 9
}
return pageLayout
}
function buildTable<P extends object, T extends DefaultRow>(table: Partial<TablePropsType<T, P>> = {}) {
function buildTable<P extends object, T extends DefaultRow>(table: DeepPartial<TablePropsType<T, P>> = {}) {
if (table.actionColumn == null) {
table.actionColumn = {
width: '80px',
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 (table.actionColumn.width == null) {
table.actionColumn.width = '80px'
}
if (table.emptyText == null) {
table.emptyText = '暂无数据'
}
if (table.rowKey == null) {
table.emptyText = 'id'
}
return table
}
function buildToolBar(toolBar: Partial<ToolBarType> = {}) {
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 interface PropsTplType<P extends G.PageParam, T extends DefaultRow> {
pageLayout?: Partial<PageLayoutType>
searchForm?: Partial<FormPropsType<P, T>>
toolBar?: Partial<ToolBarType>
table?: Partial<TablePropsType<T, P>>
}
export function buildProps<P extends G.PageParam, T extends DefaultRow>({pageLayout, searchForm, toolBar, table}: PropsTplType<P, T>) {
export function buildProps<P extends G.PageParam, T extends DefaultRow>({pageLayout, searchForm, toolBar, table}: DeepPartial<ATablePageType<P, T>>) {
return reactive({
pageLayout: buildPageLayout(pageLayout),
searchForm: buildSearchForm(searchForm),
@ -159,10 +255,6 @@ export function buildProps<P extends G.PageParam, T extends DefaultRow>({pageLay
})
}
interface Exposed extends Record<string, any> {
doSearch: () => void
}
const component = defineComponent(
<P extends G.PageParam, T extends DefaultRow>(props: ATablePageType<P, T>, {slots, expose}: SetupContext) => {
const formData = Utils.resetAble(reactive<P>({
@ -197,11 +289,11 @@ const component = defineComponent(
formData.$reset()
}
const doExport = () => {
if (props?.searchForm?.export == null) {
if (props.searchForm.export == null) {
return
}
loading.value = true
saveFile(props?.searchForm?.export(formData.$clone() as P))
saveFile(props.searchForm.export(formData.$clone() as P))
.finally(() => {
loading.value = false
})
@ -232,16 +324,19 @@ const component = defineComponent(
}
const highFormCssParam = computed(() => {
const labelWidth = props.searchForm.highForm.labelWidth
const contentWidth = props.searchForm.highForm.contentWidth
return {
'--col-count': props.searchForm.highForm?.colCount ?? 3,
'--vgap': props.searchForm.highForm?.vgap ?? '0',
'--hgap': props.searchForm.highForm?.hgap ?? '0',
'--item-min-width': (labelWidth + contentWidth) + 'px',
'--rgap': props.searchForm.highForm.rgap + 'px',
'--cgap': props.searchForm.highForm.cgap + 'px',
}
})
const pageCssParam = computed(() => {
const dataListHeight = props.pageLayout.dataListHeight
return {
'--data-list-height': props.pageLayout.dataListHeight,
'--data-list-height': Types.isString(dataListHeight) ? dataListHeight : dataListHeight + 'fr',
}
})
const actionColumnBtnRender = (scope: ColumnScopeType<T>, tableAction: TableActionType<T>) => {
@ -249,7 +344,7 @@ const component = defineComponent(
icon={tableAction.icon == null ? undefined : elIcons[tableAction.icon]}
loading={tableAction.loading}
type={tableAction.type}
class={tableAction.textBtn ? 'text-btn' : 'icon-btn'}
class={styles.iconBtn}
text>
{tableAction.label}
</ElButton>
@ -264,20 +359,20 @@ const component = defineComponent(
return Btn
}
const actionColumnRender = () => {
const actionColumn = props.table?.actionColumn
return Colls.isEmpty(actionColumn?.tableActions) ? <></>
: (<ElTableColumn width={actionColumn?.width ?? '180'} fixed="right" label="操作">
const actionColumn = props.table.actionColumn
return Colls.isEmpty(actionColumn.tableActions) ? <></>
: (<ElTableColumn width={actionColumn.width} fixed="right" label="操作">
<div class="action-btn">
{{
default: (scope: ColumnScopeType<T>) => (actionColumn?.tableActions!
default: (scope: ColumnScopeType<T>) => (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 ?? '是'}
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 ?? '180'}
width={tableAction.confirm.width}
cancel-button-type="primary"
confirm-button-type="danger"
onConfirm={() => rowAction(scope, tableAction)}>
@ -292,11 +387,10 @@ const component = defineComponent(
}
onMounted(doSearch)
return () => (<APage class={[ styles.tablePage, showHighForm.value ? '' : styles.folder ]} style={pageCssParam.value}>
{/*@ts-ignore*/}
<div class={styles.searchFormWrapper} style={highFormCssParam.value}>
<ElScrollbar>
{/*@ts-ignore*/}
<ElForm class={styles.searchForm} onSubmit={withModifiers(doSearch, [ 'prevent' ])} labelWidth={props.searchForm.highForm?.labelWidth ?? '90px'}>
<ElForm class={styles.searchForm} onSubmit={withModifiers(doSearch, [ 'prevent' ])} labelWidth={props.searchForm.highForm.labelWidth + 'px'}>
{
slots.highFormItem?.(formData)
}
@ -308,10 +402,10 @@ const component = defineComponent(
<div class={styles.toolBar}>
<div class={styles.toolBarLeft}>
{
props.toolBar?.leftTools?.map((tool, i) => (
props.toolBar.leftTools.map((tool, i) => (
<ElButton key={'tool-bar-left-' + i}
icon={Strings.isBlank(tool.icon) ? undefined : elIcons[tool.icon!]}
type={tool.type ?? 'primary'}
type={tool.type}
onClick={tool.action}>
{tool.label}
</ElButton>
@ -319,6 +413,7 @@ const component = defineComponent(
}
</div>
<div class={styles.toolBarRight}>
<ElScrollbar>
{/*@ts-ignore*/}
<ElForm onSubmit={withModifiers(doSearch, [ 'prevent' ])}>
{
@ -326,6 +421,8 @@ const component = defineComponent(
}
<button style="display: none" type="submit"/>
</ElForm>
</ElScrollbar>
<div>
<ElTooltip content="搜索" placement="top">
<ElDropdown split-button onClick={doSearch} onCommand={(command: string) => {
if (command === 'reset') {
@ -341,29 +438,32 @@ const component = defineComponent(
</ElDropdown>
</ElTooltip>
{
props.toolBar?.rightTools?.map((tool, i) => (
props.toolBar.rightTools.map((tool, i) => (
<ElButton key={'tool-bar-right-' + i}
icon={elIcons[tool.icon]}
onClick={tool.action}/>
))
}
<ElTooltip content="导出" placement="top">
{
props.searchForm.export == null ? <></> : (<ElTooltip content="导出" placement="top">
<ElButton icon={elIcons.Download} loading={loading.value} type="default" onClick={doExport}/>
</ElTooltip>
</ElTooltip>)
}
<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 ?? 'id'}
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}
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"
>
@ -399,7 +499,8 @@ const component = defineComponent(
},
)
export interface ATablePageInstance extends InstanceType<typeof component>, Exposed {
export interface ATablePageInstance extends InstanceType<typeof component> {
doSearch: () => void
}
export default component

View File

@ -1,6 +1,6 @@
.table-page {
grid-template-columns 1fr
grid-template-rows: minmax(0, 1fr) minmax(0, var(--data-list-height));
grid-template-rows: minmax(74px, 1fr) minmax(277px, var(--data-list-height));
grid-auto-rows: minmax(0, auto);
gap 10px
@ -35,9 +35,9 @@
.search-form {
display grid
grid-template-columns repeat(var(--col-count), 1fr)
row-gap var(--vgap);
column-gap var(--hgap);
grid-template-columns repeat(auto-fit, minmax(var(--item-min-width), 1fr))
row-gap var(--rgap);
column-gap var(--cgap);
:global(.el-form-item) {
margin 0
@ -67,30 +67,36 @@
gap 20px
.tool-bar {
display flex
justify-content space-between
display: grid;
grid-template-columns: minmax(260px, 1fr) minmax(428px, 1fr);
grid-template-rows: 50px;
box-sizing: border-box;
.tool-bar-left {
flex 1
//flex 1
}
.tool-bar-right {
flex 1
display flex
display grid
grid-template-columns: minmax(200px, 1fr) 150px;
grid-auto-rows: 32px;
gap 10px
justify-content end
& > div:first-child {
:global(.el-form) {
display flex
display: grid;
grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
gap 10px
flex-shrink 0
.el-form-item {
:global(.el-form-item) {
margin: 0;
flex 1
}
}
}
& > div:last-child {
display: flex;
justify-content: space-evenly;
:global(.el-button--default) {
width 32px
@ -106,7 +112,6 @@
}
}
:global(.el-dropdown) {
flex-shrink 0
}
@ -116,6 +121,7 @@
}
}
}
}
:global(.el-table) {
width 100%

View File

@ -72,6 +72,7 @@
<ElTableColumn label="出场时间" prop="outTime" width="140"/>
<!-- <ElTableColumn label="完结时间" prop="finishTime"/> -->
<ElTableColumn fixed="right" label="支付状态" prop="paymentStatusTxt"/>
y
<ElTableColumn fixed="right" label="订单状态" prop="transStatusTxt"/>
<ElTableColumn fixed="right" label="勘料状态" prop="checkStatusTxt"/>
@ -98,6 +99,9 @@ function research() {
}
const tablePageProps = buildProps({
pageLayout: {
dataListHeight: 4,
},
table: {
actionColumn: {
tableActions: [
@ -111,10 +115,13 @@ const tablePageProps = buildProps({
},
},
searchForm: {
highForm: {
contentWidth: 342,
},
paging(param: OrderTypes.SearchOrderParam) {
return OrderApi.paging(param).then(res => {
res.data.records = []
for (let i = 0; i < 5; i++) {
for (let i = 0; i < 20; i++) {
res.data.records.push({})
}
return res
@ -122,6 +129,4 @@ const tablePageProps = buildProps({
},
},
})
</script>