<!-- @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" :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'; 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) => { 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; }); 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; 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); //提供刷新功能 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>