Files
SaaS-lib/lib/component/table/table.vue
2024-08-23 15:53:13 +08:00

757 lines
24 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.

<!-- @format -->
<template>
<div class="ns-table" :class="{ 'ns-table-no-search': !(formConfig?.schemas.length > 0) }">
<!-- tabletitle -->
<!-- <div
class="ns-table-title"
@click="
() => {
showBack ? navigateBack() : '';
}
"
v-if="tableTitle">
<ns-icon v-if="showBack" class="backIcon" name="left" />{{ tableTitle }}
</div> -->
<div class="ns-table-container">
<!-- todo drag -->
<div class="ns-part-tree" v-if="!isEmpty(treeConfig)">
<ns-tree-api v-bind="getTreeBindValue" @reload="reload" @select="treeSelect" />
</div>
<div class="ns-part-table">
<a-spin :spinning="tableState.loading">
<div class="ns-table-search" v-if="!isEmpty(formConfig)">
<ns-form
ref="formElRef"
class="ns-table-form"
:showAction="true"
v-bind="formConfig"
:expand="expand"
:showExpand="showExpand"
:expandAll="expandAll"
:title="formConfig.title"
:showExpandAll="showExpandAll"
:model="formModel"
@finish="formFinish" />
</div>
<div class="ns-table-main">
<ns-table-header
v-if="!isEmpty(headerActions) || tableTitle"
:headerActions="headerActions"
:searchData="formModel"
:tableTitle="tableTitle"
:data="tableState.selectedRows">
<template #header="data">
<slot name="header" v-bind="data || {}"></slot>
</template>
</ns-table-header>
<ns-basic-table ref="tableElRef" v-bind="getTableBindValues" :dataSource="tableData">
<template #emptyText>
<template v-if="tableState.loadError">
<div class="ns-table-content">
<div class="fetch-error">
<p>{{ tableState.loadErrorMessage }}</p>
<ns-button type="primary" ghost @click="reload">重新加载</ns-button></div
></div
>
</template>
<template v-else-if="tableState.loading"
><div class="ns-table-content"></div
></template>
<template v-else>
<div class="ns-table-content"> <a-empty /> </div>
</template>
</template>
<template #[item]="data" v-for="item in Object.keys($slots)" :key="item">
<slot :name="item" v-bind="data || {}"></slot>
<template v-if="item === 'bodyCell'">
<template v-if="data?.column?.textEllipsis">
<span
class="tool-tips"
:style="{ width: `${data.column.textWidth || data.column.width}px` }">
<ns-tooltip
placement="top"
v-if="
data.column.customRender
? data.column.customRender(data)
: data.record[data.column.dataIndex]
">
<template #title>
<span>{{
data.column.customRender
? data.column.customRender(data)
: data.record[data.column.dataIndex] || '-'
}}</span>
</template>
<span class="text-ellipsis">{{
data.column.customRender
? data.column.customRender(data)
: data.record[data.column.dataIndex] || '-'
}}</span>
</ns-tooltip>
<span class="text-ellipsis" v-else> - </span>
</span>
</template>
<template v-if="data.column.dataIndex === 'tableAction'">
<ns-table-action
:data="data.record"
:searchData="formModel"
:columnActions="getColumnActions" />
</template>
<template v-if="data.column.edit">
<ns-table-cell
:value="data.text"
:record="data.record"
:column="data.column"
:index="data.index" />
</template>
</template>
<template v-if="item === 'footer'">
<ns-table-footer :footerActions="footerActions" :data="ediRowData" />
</template>
</template>
<template #bodyCell="data" v-if="!Object.keys($slots).includes('bodyCell')">
<template v-if="data.column.textEllipsis">
<span
class="tool-tips"
:style="{ width: `${data.column.textWidth || data.column.width}px` }">
<ns-tooltip
placement="top"
v-if="
data.column.customRender
? data.column.customRender(data)
: data.record[data.column.dataIndex]
">
<template #title>
<span>{{
data.column.customRender
? data.column.customRender(data)
: data.record[data.column.dataIndex] || '-'
}}</span>
</template>
<span class="text-ellipsis">{{
data.column.customRender
? data.column.customRender(data)
: data.record[data.column.dataIndex] || '-'
}}</span>
</ns-tooltip>
<span class="text-ellipsis" v-else> - </span>
</span>
</template>
<template v-if="data.column.dataIndex === 'tableAction'">
<ns-table-action
:data="data.record"
:searchData="formModel"
:columnActions="getColumnActions" />
</template>
<template v-if="data.column.edit">
<ns-table-cell
:value="data.text"
:record="data.record"
:column="data.column"
:index="data.index" />
</template>
</template>
<template
#footer
v-if="!Object.keys($slots).includes('footer') && !isEmpty(footerActions)">
<ns-table-footer :footerActions="footerActions" :data="ediRowData" />
</template>
</ns-basic-table>
</div>
</a-spin>
</div>
</div>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, provide, reactive, ref, unref, watch } from 'vue';
import { RequestParams } from '/nerv-lib/component/table/table';
import {
cloneDeep,
debounce,
throttle,
get,
isArray,
isEmpty,
isEqual,
isFunction,
isObject,
isString,
isUndefined,
} from 'lodash-es';
import NsTableAction from './table-action.vue';
import NsTableHeader from './table-header.vue';
import NsTableFooter from './table-footer.vue';
import NsTableCell from './edit/table-cell.vue';
import { useParams } from '/nerv-lib/use/use-params';
import { transformColumns } from '/nerv-lib/component/table/table-columns';
import NsBasicTable from '/nerv-lib/component/table/basic-table.vue';
import { tableProps } from '/nerv-lib/component/table/props';
import { AxiosRequestConfig } from 'axios';
import { useApi } from '/nerv-lib/use/use-api';
import { useRoute } from 'vue-router';
import { useTableEdit } from '/nerv-lib/component/table/use-table-edit';
import { Form } from 'ant-design-vue';
import { stringUtil } from '/nerv-lib/util/string-util';
import { useTableRefresh } from '/nerv-lib/component/table/use-table-refresh';
import { tableConfig } from '/nerv-base/config/table.config';
import { useTableSession } from '/nerv-lib/component/table/use-table-session';
import { useTableColumn } from '/nerv-lib/component/table/use-table-column';
import { useNavigate } from '/nerv-lib/use/use-navigate';
import { object } from 'vue-types';
export default defineComponent({
name: 'NsTable',
components: {
NsBasicTable,
NsTableAction,
NsTableHeader,
NsTableFooter,
NsTableCell,
},
props: tableProps,
emits: ['cellChange', 'update:value', 'dataSourceChange', 'update:dataSource'],
setup(props, { attrs, emit }) {
const tableElRef = ref(null);
const formElRef = ref(null);
const dataRef = ref([]);
const treeParamsRef = ref({});
const formParamsRef = ref({});
const orderRef = ref({});
const formModel = reactive<Recordable>({});
const tableData = ref<Recordable[]>([]);
const tableState = reactive({
selectedRowKeys: [],
selectedRows: [],
loading: false,
loadError: false,
loadErrorMessage: '',
loadinterval: 0,
});
const clearCheck = () => {
tableState.selectedRowKeys = [];
tableState.selectedRows = [];
};
const route = useRoute();
const { getColumnActionWidth } = useTableColumn({
columnActions: Object.assign({}, tableConfig.columnActions, props.columnActions),
});
const { navigateBack } = useNavigate();
const defaultPageRef = ref(1 - props.pageFieldOffset);
const { setTableSession } = useTableSession(
formModel,
formParamsRef,
defaultPageRef,
treeParamsRef,
);
const { delayFetch } = useTableRefresh({ props, reload });
watch(
[() => props.value, () => props.dataSource],
() => {
tableData.value = props.value || props.dataSource || [];
},
{
immediate: true,
},
);
const formItemContext = Form.useInjectFormItemContext();
const getColumnActions = computed(() => {
const { actions } = props.columnActions as any;
const _tableConfig = cloneDeep(tableConfig);
if (actions) {
_tableConfig.columnActions.width = getColumnActionWidth(actions);
}
return Object.assign(_tableConfig.columnActions, props.columnActions);
});
const getColumns = computed(() => {
const columns = transformColumns(cloneDeep(props.columns || []));
const { title, width, dataIndex, fixed } = getColumnActions.value;
if (props.columnActions) {
columns.push({
title,
width,
dataIndex,
fixed,
});
}
return columns;
});
watch(
() => tableData.value,
(val) => {
// console.log(val, tableData.value);
if (isEqual(val, tableData.value)) return;
const data = cloneDeep(tableData.value);
if (props.editable) {
Object.keys(data).forEach((key) => {
delete data[key][props.rowKey];
});
}
// emit('update:value', data);
emit('dataSourceChange', data);
formItemContext.onFieldChange();
},
{
deep: true,
},
);
const tableEdit = useTableEdit({
dataSource: tableData,
columns: getColumns,
rowKey: props.rowKey,
editable: ref(props.editable),
});
provide('tableEdit', tableEdit);
const { getParams } = useParams();
const rowSelection = computed(() => {
const { rowSelection } = props;
if (rowSelection === false || rowSelection === null) {
return null;
}
return Object.assign(
{
fixed: true,
columnWidth: 48,
preserveSelectedRowKeys: true, // 跨页选中默认不清除选中key
selectedRowKeys: tableState.selectedRowKeys,
onChange: (selectedRowKeys: never[], selectedRows: never[]) => {
tableState.selectedRowKeys = selectedRowKeys;
tableState.selectedRows = selectedRows;
},
},
isFunction(rowSelection) ? rowSelection(tableState) : rowSelection,
);
});
const customizeRenderEmpty = computed(() => {
return () => '暂无数据';
});
const formFinish = debounce((data: object) => {
formParamsRef.value = data;
fetch({
page: 1,
});
}, 300);
function setLoading(loading: boolean) {
tableState.loading = loading;
}
const tableChangeEvent = (pagination: Props, filters: [], sorter: any) => {
if (sorter?.field) {
if (sorter.order) {
orderRef.value = {
[props.paramsOrderField]: stringUtil.toLine(
`${sorter.field} ${sorter.order.replace('end', '')}`,
),
};
} else {
orderRef.value = { [props.paramsOrderField]: '' }; //覆盖默认params
}
fetch({
page: pagination?.current || getPagination.value?.current || 1,
pageSize: pagination?.pageSize,
});
} else if (pagination?.current) {
fetch({
page: pagination?.current,
pageSize: pagination.pageSize,
});
}
};
// pagination
const getPagination: Recordable | Boolean = computed(() => {
const { pagination } = props;
const { getPageParams } = attrs;
if (pagination) {
const current = get(dataRef.value, props.pageField);
function getTotal() {
let total = 0;
if (isFunction(getPageParams)) {
total = getPageParams(dataRef)['total'];
} else {
total = get(dataRef.value, props.totalField);
}
return total;
}
return {
showQuickJumper: true,
showLessItems: true,
showSizeChanger: true,
showTotal: (total: number, range: Array<number>) => {
return `显示第${range[0]}到${range[1]}条记录 ,共 ${
get(dataRef.value, props.totalField) || total
} 条记录`;
},
...(pagination as Props),
total: getTotal(),
current: (current >= 0 ? current : 1) + props.pageFieldOffset, // 后端1 开始
pageSize: get(dataRef.value, props.sizeField),
};
}
return false;
});
const getTableBindValues = computed(() => {
const { params, dynamicParams } = props;
return {
...attrs,
...props,
rowSelection: rowSelection.value,
params: dynamicParams
? getParams({ ...route.params, ...route.query }, dynamicParams, params)
: params || {},
columns: getColumns.value,
pagination: getPagination.value,
onChange: tableChangeEvent,
};
});
watch(
() => getTableBindValues.value.api,
() => {
// console.log(getTableBindValues.value.api);
// fetch(); //路由切换导致api切换 导致发送请求
},
{
immediate: true,
},
);
// watch(
// () => getTableBindValues.value.params,
// () => {
// fetch();
// },
// {
// immediate: true,
// },
// );
/**
* 请求函数
* @param requestParams 主要是传入页面,部分变量闭包处理
* @param clearDelay 是否需要清除刷新时间(页面操作之后,自动刷新重新计算)
*/
function fetch(requestParams: RequestParams = {}, clearDelay = true) {
clearDelay && delayFetch();
if (tableState.loadinterval) {
clearTimeout(tableState.loadinterval);
}
if (tableState.loading) {
tableState.loadinterval = setTimeout(() => {
fetch(requestParams, clearDelay);
}, 500);
return;
}
const { api, pagination } = props;
const { page, pageSize } = requestParams;
if (api) {
let pageParams: Recordable = {};
if (pagination !== false) {
pageParams = {
[props.paramsPageField]: page ? page - props.pageFieldOffset : defaultPageRef.value, // 后端0 开始
[props.paramsPageSizeField]:
pageSize || getPagination.value?.pageSize || props.defaultPageSize,
};
} else {
//判断是否是系统菜单页面过来是的话修改分页总数dyfadd
if (props.tableTitle == '系统菜单') {
pageParams = {};
// [props.paramsPageField]: defaultPageRef.value, // 后端0 开始
// [props.paramsPageSizeField]:999,
} else {
pageParams = {
[props.paramsPageField]: defaultPageRef.value, // 后端0 开始
[props.paramsPageSizeField]:
pageSize || getPagination.value?.pageSize || props.defaultPageSize,
};
}
}
const httpParams = {
...getTableBindValues.value.params,
...pageParams,
...formParamsRef.value,
...treeParamsRef.value,
...orderRef.value,
};
if (!checkrequiredParams(httpParams)) {
console.log('check fail');
return;
}
setTableSession(pageParams[props.paramsPageField]);
clearDelay && setLoading(true);
const requestConfig: AxiosRequestConfig = { method: 'get' };
const { httpRequest } = useApi();
httpRequest({
api,
params: httpParams,
pathParams: { ...route.params, ...route.query },
requestConfig,
})
.then((res: any) => {
tableState.loadError = false;
tableState.loadErrorMessage = '';
dataRef.value = res;
tableData.value = get(unref(dataRef), props.listField);
//saas项目配置
if (attrs['getPageParams']) {
const getPageParams = attrs['getPageParams'];
let realPage = getPageParams(dataRef, pageParams.page)['page'];
if (realPage !== pageParams.page) {
fetch({ page: realPage });
}
}
emit('update:dataSource', tableData.value);
clearDelay && setLoading(false);
})
.catch((error: any) => {
const { response, code, message } = error || {};
let errMessage = response?.data?.msg;
const err: string = error?.toString?.() ?? '';
if (code === 'ECONNABORTED' && message.indexOf('timeout') !== -1) {
errMessage = '接口请求超时,请刷新页面重试!';
}
if (err?.includes('Network Error')) {
errMessage = '网络异常,请检查您的网络连接是否正常!';
}
tableState.loadError = true;
tableState.loadErrorMessage = errMessage;
clearDelay && setLoading(false);
});
}
}
/**
* 检测requiredParams是否全部获得数据
* @param params
*/
function checkrequiredParams(params: Recordable) {
const { params: dynamicParams } = getTableBindValues.value as any;
let { requiredParams } = props;
if (requiredParams) {
if (requiredParams === true) requiredParams = dynamicParams as any;
if (isFunction(requiredParams)) {
console.error(
'Property dynamicParams of props cannot set to Function when using requiredParams',
);
return false;
} else {
if (isString(requiredParams)) {
if (isUndefined(params[requiredParams])) return false;
} else if (isArray(requiredParams)) {
for (let i = 0, l = requiredParams.length; i < l; i++) {
if (isUndefined(params[requiredParams[i]])) return false;
}
} else if (isObject(requiredParams)) {
const keys = Object.keys(requiredParams);
for (let i = 0, l = keys.length; i < l; i++) {
if (isUndefined(params[keys[i]])) return false;
}
}
}
return true;
}
return true;
}
function treeSelect(
selectedKeys: never[],
e: {
selected: boolean;
selectedNodes: { props: { dataRef: any } }[];
node: any;
event: any;
},
) {
console.log(selectedKeys, e);
const { dataRef } = e.node;
treeParamsRef.value = getParams(dataRef, getTreeBindValue.value.dynamicParams);
fetch({
page: 1,
});
}
const getTreeData = computed(() => {
return props?.treeConfig?.treeData || [];
});
const getTreeWidth = computed(() => {
return props?.treeConfig?.width || '300px';
});
const getTreeBindValue = computed(() => ({
...props?.treeConfig,
}));
//todo 异步加载|| 树形接口
function reload(clearDelay = true) {
const pagination = unref(getPagination);
fetch(
{
page: pagination === false ? 1 : pagination.current,
},
clearDelay,
);
}
provide('reload', reload); //提供刷新功能
provide('clearCheck', clearCheck); //提供清空选中功能
return {
navigateBack,
reload,
clearCheck,
formElRef,
tableElRef,
getColumnActions,
getTableBindValues,
formModel,
tableState,
isEmpty,
formFinish,
tableChangeEvent,
treeSelect,
getTreeBindValue,
getTreeWidth,
getTreeData,
customizeRenderEmpty,
tableData,
treeParamsRef,
formParamsRef,
};
},
});
</script>
<style lang="less" scoped>
.backIcon {
cursor: pointer;
margin-right: 6px;
}
.ns-table-title {
text-align: left;
height: 46px;
line-height: 46px;
//font-size: 16px;
font-size: 18px;
font-weight: bold;
user-select: text;
padding-left: 16px;
background: #fff;
width: calc(100% + 32px);
margin-left: -16px;
cursor: pointer;
}
.ns-table-container {
display: flex;
.ns-part-tree {
width: 300px;
// padding: 16px;
overflow-y: auto;
}
.ns-part-table {
flex: 1;
min-width: 0;
}
}
:deep(.ant-spin-container) {
display: flex;
flex-direction: column;
.ns-table-main {
height: 100%;
}
}
.ns-table-content {
// background: #e5ebf0;
margin: 16px;
}
:deep(.ant-spin-nested-loading > div > .ant-spin) {
max-height: none;
}
.ns-table-search {
padding-top: 16px;
}
:deep(.ant-form-item) {
margin-bottom: 16px;
}
.ns-table {
position: relative;
// min-height: 400px;
// background: #e5ebf0;
.ant-spin-nested-loading {
height: 100%;
// min-height: 400px;
}
.ns-table-content {
min-height: 300px;
display: flex;
justify-content: center;
align-items: center;
}
.fetch-error {
p {
line-height: 40px;
padding: 0;
margin: 0;
font-size: 16px;
}
.ant-btn {
width: 88px;
}
}
}
.text-ellipsis {
display: inline-block;
vertical-align: top;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
}
.tool-tips {
display: inline-block;
vertical-align: top;
padding: 0;
word-wrap: break-word;
word-break: break-word;
width: 100%;
}
</style>