720 lines
19 KiB
Vue
720 lines
19 KiB
Vue
<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>
|