Files
SPDM/src/components/common/table/baseTable.vue
JiangSheng 466d963c36 Revert "feat: baseTable params"
This reverts commit 31c1cd715c.
2026-03-02 11:03:13 +08:00

720 lines
19 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.

<template>
<div class="comp-content comp-base-table">
<TableSearch
v-show="searchList.length > 0 && !hideSearch"
ref="tableSearchRef"
:searchItems="searchList"
:searchLimitNum="searchLimitNum"
:searchParams="searchParams"
:defaultSearchParams="defaultSearchParams"
:searchAttrs="searchAttrs"
:hideSearchKeys="hideSearchKeys"
@search="searchFun"
@reset="resetSearchFun"
@change="changeFun"
@load="searchLoadFun"
>
<template v-for="name in Object.keys($slots)" :key="name" #[name]="scope">
<slot :name="name" v-bind="scope" />
</template>
</TableSearch>
<div class="options">
<div class="item">
<div v-if="listTitle" class="list-title">{{ listTitle }}</div>
</div>
<div class="item">
<div class="btns">
<slot name="leftOptions" />
<template v-if="$slots['cardTemplate']">
<el-tooltip v-if="viewType === 'list'" content="切换至卡片视图" placement="top">
<div class="icon-btn" @click="viewTypeChangeFun('card')">
<el-icon :size="18">
<List />
</el-icon>
</div>
</el-tooltip>
<el-tooltip v-else content="切换至列表视图" placement="top">
<div class="icon-btn" @click="viewTypeChangeFun('list')">
<el-icon :size="18">
<Menu />
</el-icon>
</div>
</el-tooltip>
</template>
<el-tooltip v-if="exportApi && showExport" :content="$t('表格.导出')" placement="top">
<div v-if="exportApi && showExport" class="icon-btn" @click="exportFun">
<el-icon :size="18">
<Download />
</el-icon>
</div>
</el-tooltip>
<el-tooltip v-if="showImport" :content="$t('表格.导入')" placement="top">
<div v-if="showImport" class="icon-btn" @click="formDiaVisible = true">
<el-icon :size="18">
<Upload />
</el-icon>
</div>
</el-tooltip>
<el-tooltip v-if="showSetting" :content="$t('表格.列表字段设置')" placement="top">
<div class="icon-btn" @click="formDiaVisible = true">
<el-icon :size="18">
<Setting />
</el-icon>
</div>
</el-tooltip>
</div>
</div>
</div>
<div v-if="viewType === 'list'" class="table">
<vxe-table
ref="vxeTableRef"
:loading="loading"
:data="tableData"
v-bind="$attrs"
highlight-hover-row
:seq-config="{ startIndex: (current - 1) * size }"
:column-config="{
drag: true,
resizable: true,
}"
:column-drag-config="{
showIcon: false,
trigger: 'cell',
}"
:height="fullHeight ? '100%' : ''"
>
<vxe-column
v-if="showCheckbox"
type="checkbox"
width="60"
align="left"
header-align="left"
></vxe-column>
<vxe-column v-if="showIndex" type="seq" width="80" align="left" header-align="left" />
<vxe-column
v-for="item in tableHeadVisible"
:key="item.key"
:field="item.key"
:title="item.title"
:min-width="item.width"
align="left"
header-align="left"
:tree-node="item.treeNode"
:sortable="false"
:show-overflow="showOverflow"
>
<!-- TODO 暂时开放以下表头搜索 -->
<template
v-if="
[
'SIMULATION_TASK_ANALYSIS',
'SIMULATION_RUN_ANALYSIS',
'SIMULATION_PERFORMANCE_ANALYSIS',
'RESULT_REPORT',
].includes(tableName)
"
#header
>
<HeadSearch :item="item" :data="headSearchParams" @search="headSearchFun" />
</template>
<template #default="{ row, column }">
<span class="td-text">
<span v-if="item.tableIcon === 'fileIcon' && isString(row[item.key])">
<img :src="fileUploadAllocationIconFun(row[item.key], row.dataType)" class="img" />
</span>
<span v-if="item.tableIcon && item.tableIcon !== 'fileIcon'" class="icon">
<el-icon :size="16">
<component :is="item.tableIcon" />
</el-icon>
</span>
<slot v-if="$slots[item.key]" :name="item.key" :row="row" :column="column"></slot>
<span v-else-if="item.inputMode === 'select' && item.formOptions">
{{ allDictData[item.formOptions]?.O[row[item.key]] || '--' }}
</span>
<span v-else>{{ row[item.key] || '--' }}</span>
</span>
</template>
</vxe-column>
<vxe-column
v-if="actionList.length > 0"
title="操作"
:width="actionAutoWidth"
align="left"
header-align="left"
fixed="right"
>
<template #default="{ row, rowIndex }">
<div class="actions">
<template v-for="(action, aIndex) in actionList" :key="aIndex">
<el-link
v-if="
!(action.hide && action.hide(row)) &&
findVisibleIndex(row, action) <= visibleActionNum
"
class="action-item"
:type="action.type"
@click="actionClickFun(row, action, rowIndex)"
>
{{ action.title }}
</el-link>
</template>
<el-dropdown v-if="visibleNum(row) > visibleActionNum">
<el-icon class="more-icon">
<MoreFilled />
</el-icon>
<template #dropdown>
<el-dropdown-menu>
<template v-for="(action, aIndex) in actionList" :key="aIndex">
<el-dropdown-item
v-if="
!(action.hide && action.hide(row)) &&
findVisibleIndex(row, action) > visibleActionNum
"
@click="actionClickFun(row, action, rowIndex)"
>
<el-link :type="action.type">{{ action.title }}</el-link>
</el-dropdown-item>
</template>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</template>
</vxe-column>
</vxe-table>
</div>
<div v-else class="card">
<slot name="cardTemplate" :tableData="tableData" />
</div>
<div v-if="!hidePagination" class="pagination">
<div>共 {{ pageTotal }} 项数据</div>
<el-pagination
v-model:current-page="current"
v-model:page-size="size"
background
layout="sizes, prev, pager, next"
:total="pageTotal"
@size-change="sizeChangeFun"
@current-change="currentChangeFun"
/>
</div>
<TableFormDia v-model="formDiaVisible" :name="tableName" @update="getHeadDataFun" />
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, watch, nextTick } from 'vue';
import { ElMessageBox } from 'element-plus';
import { Download, Upload, Menu, List, MoreFilled } from '@element-plus/icons-vue';
import type { TableHead, ApiParams, ApiResult } from './types';
import TableSearch from './tableSearch.vue';
import TableFormDia from './tableFormDia.vue';
import HeadSearch from './headSearch.vue';
import { getFormConfigureApi } from '@/api/system/systemData';
import { formOptionsFormat } from './lib';
import { exportFile, fileUploadAllocationIconFun } from '@/utils/file';
import { cloneDeep, isString } from 'lodash-es';
import { CommonStore } from '@/stores/common';
const commonStore = CommonStore();
const allDictData = ref(commonStore.dictData);
const emit = defineEmits([
'searchChange',
'load',
'update:viewType',
'search',
'update:searchParams',
'tableDataLoad',
'tableDataquery',
]);
interface Props {
tableName?: string;
api?: (params: ApiParams) => Promise<any> | undefined;
exportApi?: any;
exportFileName?: string;
exportParams?: any;
render?: (data: any, cb: (cbData: any) => void) => void | undefined;
params?: any;
head?: any;
viewType?: string;
searchItems?: any[];
searchLimitNum?: number;
showCheckbox?: boolean;
showIndex?: boolean;
hidePagination?: boolean;
searchParams?: any;
showExport?: boolean;
showImport?: boolean;
listTitle?: string;
actionList?: any;
showOverflow?: boolean;
searchAttrs?: any;
fullHeight?: boolean;
defaultSearchParams?: any; // 默认搜索项目
showNodeName?: string;
hideSearchKeys?: any;
hideSearch?: boolean; // 隐藏整个搜索栏
showSetting?: boolean; // 是否显示设置按钮
data?: any; // 设置默认表格数据
}
const props = withDefaults(defineProps<Props>(), {
tableName: '',
api: undefined,
exportApi: undefined,
exportFileName: '',
exportParams: {},
render: undefined,
params: () => {},
head: null,
viewType: 'list', // list:列表 card:卡片
searchItems: () => [] as any[],
searchLimitNum: 0,
showCheckbox: false,
showIndex: false,
hidePagination: false,
searchParams: {},
showExport: true,
showImport: false,
listTitle: '',
actionList: [],
showOverflow: true,
searchAttrs: {},
fullHeight: false,
defaultSearchParams: {},
showNodeName: '',
hideSearchKeys: [],
hideSearch: false,
showSetting: true,
data: [],
});
const tableData = ref<any>([]);
const tableHead = ref<TableHead[]>([]);
const tableHeadVisible = ref<TableHead[]>([]);
const searchData = ref<object>({});
const current = ref(1);
const size = ref(20);
const pageTotal = ref(0);
const formDiaVisible = ref(false);
const loading = ref(false);
const vxeTableRef = ref<any>();
const tableSearchRef = ref<any>();
const searchList = ref<any>(props.searchItems);
const actionAutoWidth = ref(95);
const headSearchParams = ref<any>({});
const visibleActionNum = ref(3); // 操作按钮展示个数
const findVisibleIndex = (row: any, action: any) => {
let index = 0;
props.actionList.some((item: any) => {
if (!(item.hide && item.hide(row))) {
index++;
}
if (action.title === item.title) {
return true;
}
});
return index;
};
const visibleNum = (row: any) => {
// 展示按钮数量
return props.actionList.filter((d: any) => !(d.hide && d.hide(row))).length;
};
watch(
() => props.actionList,
(list: any) => {
let width = 20; // cell内边距
list.some((item: any, index: number) => {
if (index < visibleActionNum.value) {
width += item.title.length * 14; // 一个汉字14
width += 8; // 内边距
} else {
return true;
}
});
if (list.length > visibleActionNum.value) {
width += 30; // 更多宽度
}
actionAutoWidth.value = width;
},
{ immediate: true }
);
watch(
() => props.params,
(val: any) => {
resetSearchFun(val);
},
{ deep: true }
);
watch(
() => props.searchItems,
(val) => {
searchList.value = val;
},
{ deep: true, immediate: true }
);
watch(
() => props.data,
(val) => {
setDataFun(val);
},
{ deep: true }
);
onMounted(() => {
initFun();
});
const initFun = () => {
current.value = 1;
searchData.value = cloneDeep(props.defaultSearchParams);
if (!props.head) {
getHeadDataFun();
}
};
// 获取表头数据
const getHeadDataFun = () => {
if (!props.tableName) {
return;
}
const params = {
formName: props.tableName,
};
tableHeadVisible.value = []; // vxe-table动态头部顺序改变时视图不更新置空强制更新
getFormConfigureApi(params).then((res: any) => {
if (res.code === 200) {
const formConfig = JSON.parse(res.data.formConfig);
tableHead.value = formConfig;
tableHeadVisible.value = formConfig.filter((item: any) => item.isShow);
if (props.showNodeName) {
for (let i = 0; i < tableHeadVisible.value.length; i++) {
if (tableHeadVisible.value[i].key === props.showNodeName) {
tableHeadVisible.value[i].treeNode = true;
}
}
}
if (props.searchItems.length === 0) {
// 没有传入搜索配置,则默认搜索配置
const searchBuild: any[] = [];
formConfig.forEach((item: any) => {
if (item.tableSearch) {
searchBuild.push(item);
}
});
searchList.value = searchBuild;
formOptionsFormat(searchList.value);
}
getTableDataFun();
}
});
};
// 获取表单数据
const getTableDataFun = () => {
if (props.data.length > 0) {
setDataFun(props.data);
return;
}
const reqParams: ApiParams = {
current: current.value,
size: size.value,
...props.params,
...searchData.value,
...headSearchParams.value,
};
loading.value = true;
tableData.value = [];
emit('tableDataquery');
if (props.api) {
props
.api(reqParams)
?.then((res: ApiResult) => {
if (res.code === 200) {
if (props.render) {
props.render(res.data, (cbData: any) => {
const { data, total } = cbData;
tableData.value = extrasDataFun(data);
pageTotal.value = total || 0;
});
} else {
const { data, total } = res.data;
tableData.value = extrasDataFun(data);
pageTotal.value = total || 0;
}
}
})
.finally(() => {
loading.value = false;
emit('tableDataLoad');
});
} else {
if (props.render) {
props.render(null, (data: any) => {
tableData.value = extrasDataFun(data);
});
}
loading.value = false;
}
};
// 手动设置表单数据
const setDataFun = (data: any) => {
tableData.value = [];
nextTick(() => {
tableData.value = extrasDataFun(data);
});
};
// 表头搜索
const headSearchFun = (params: any) => {
headSearchParams.value = Object.assign(headSearchParams.value, params);
getTableDataFun();
};
// 搜索
const searchFun = (data: object) => {
current.value = 1;
searchData.value = data;
getTableDataFun();
emit('search');
};
// 重置搜索
const resetSearchFun = (data: object) => {
current.value = 1;
searchData.value = data;
headSearchParams.value = {};
emit('update:searchParams', searchData.value);
getTableDataFun();
};
// 重置表单
const resetFun = (reload?: boolean) => {
if (reload) {
current.value = 1;
}
getTableDataFun();
};
watch(
() => props.head,
(val) => {
if (val) {
tableHead.value = val;
tableHeadVisible.value = val.filter((item: any) => item.isShow);
resetSearchFun({});
}
},
{ deep: true, immediate: true }
);
// 分页变化
const sizeChangeFun = (value: number) => {
size.value = value;
current.value = 1;
getTableDataFun();
};
// 当前页变化
const currentChangeFun = (value: number) => {
current.value = value;
getTableDataFun();
};
// 获取搜索参数
const getSearchParamsFun = () => {
if (tableSearchRef.value) {
return tableSearchRef.value.getSearchParamsFun();
}
};
// 设置搜索参数
const setSearchParamsFun = (data: any) => {
if (tableSearchRef.value) {
return tableSearchRef.value.setSearchParamsFun(data);
}
};
// 通过key获取搜索参数
const getSearchParamByKeyFun = (key: string) => {
if (tableSearchRef.value) {
return tableSearchRef.value.getSearchParamByKeyFun(key);
}
};
// 通过key设置搜索参数
const setSearchParamByKeyFun = (key: string, data: any) => {
if (tableSearchRef.value) {
return tableSearchRef.value.setSearchParamByKeyFun(key, data);
}
};
// 搜索变化
const changeFun = (data: any) => {
emit('searchChange', data);
};
// 视图切换
const viewTypeChangeFun = (type: string) => {
emit('update:viewType', type);
};
const setOptionsFun = (key: string, options: any[]) => {
if (tableSearchRef.value) {
return tableSearchRef.value.setOptionsFun(key, options);
}
};
const searchLoadFun = () => {
emit('load');
};
const actionClickFun = (row: any, action: any, index: number) => {
const { click, needConfirm, confirmTip, confirmTipFun } = action;
if (click) {
if (needConfirm) {
ElMessageBox.confirm(
(confirmTipFun && confirmTipFun(row, index)) || confirmTip || '确定操作吗?',
'提示',
{
type: 'warning',
}
)
.then(() => {
click(row, index);
})
.catch(() => {});
} else {
click(row, index);
}
}
};
const exportFun = () => {
exportFile(props.exportApi, props.tableName, props.exportFileName, {
...searchData.value,
...props.exportParams,
});
};
watch(
() => props.tableName,
() => {
initFun();
}
);
const extrasDataFun = (data = [] as any) => {
data.forEach((item: any) => {
for (const key in item) {
if (key === 'extras' && item.extras) {
item[key].forEach((val: any) => {
item[val.propertyName] = val.propertyValue;
});
}
}
});
return data;
};
defineExpose({
tableData,
tableRef: vxeTableRef,
tableHeadVisible,
resetFun,
setDataFun,
getSearchParamsFun,
getSearchParamByKeyFun,
setSearchParamsFun,
setSearchParamByKeyFun,
setOptionsFun,
});
</script>
<style lang="scss" scoped>
.comp-base-table {
display: flex;
flex-direction: column;
height: 100%;
.td-text {
.icon {
margin-right: 2px;
.el-icon {
transform: translateY(2px);
}
}
.img {
width: 20px;
height: 20px;
margin-right: 8px;
transform: translateY(3px);
}
}
.options {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
.item {
display: flex;
align-items: center;
.list-title {
font-size: 16px;
color: var(--el-text-color-primary);
font-weight: bold;
}
.btns {
display: flex;
align-items: center;
.icon-btn {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
margin: 4px 0 4px 10px;
border-radius: 4px;
color: var(--el-text-color-secondary);
cursor: pointer;
&:hover {
background-color: var(--el-border-color-light);
}
}
}
}
}
.table {
height: 0;
flex: 1;
overflow: hidden;
}
.card {
height: 0;
flex: 1;
overflow-y: auto;
padding-bottom: 10px;
}
.actions {
display: flex;
align-items: center;
justify-content: flex-start;
&::after {
content: '';
position: absolute;
top: 0;
bottom: 0;
left: 0;
width: 1px;
background-color: var(--el-text-color-primary);
filter: blur(5px);
}
.action-item {
padding: 0 4px;
}
.more-icon {
margin-left: 10px;
}
}
.pagination {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
height: 60px;
padding: 30px 20px 10px;
color: var(--el-text-color-secondary);
font-size: 13px;
}
}
</style>