You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

741 lines
24 KiB

<!-- @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" @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"
:model="formModel"
@finish="formFinish" />
</div>
<a-row type="flex" class="ns-table-main">
<!-- <a-col :flex="getTreeWidth" v-if="!isEmpty(treeConfig)">
<ns-tree v-if="getTreeData.length" v-bind="getTreeBindValue" @select="treeSelect" />
</a-col> -->
<a-col flex="auto">
<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>
</a-col>
</a-row>
</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';
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 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) => {
console.log('params', pagination, filters, sorter);
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;
});
console.log(getPagination.value);
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 {
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;
console.log(props.listField);
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 = '网络异常,请检查您的网络连接是否正常!';
}
// console.log(getPagination.value);
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); //提供刷新功能
return {
navigateBack,
reload,
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;
}
}
.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>