2026-04-22 17:53:20 +08:00
|
|
|
|
<template>
|
2026-05-15 17:38:03 +08:00
|
|
|
|
<div class="table-container" ref="tableContainerRef">
|
2026-06-04 11:42:07 +08:00
|
|
|
|
<a-table size="small" :loading="loading" :row-selection="enableRowSelection ? rowSelection : undefined"
|
|
|
|
|
|
:data-source="tableData" :columns="enhancedColumns" :pagination="paginationConfig" :scroll="scrollConfig"
|
|
|
|
|
|
:row-key="rowKey" @change="handleTableChange">
|
2026-05-15 17:38:03 +08:00
|
|
|
|
<!-- 透传插槽,支持自定义列内容 -->
|
|
|
|
|
|
<template v-for="slot in Object.keys($slots)" #[slot]="scope" :key="slot">
|
|
|
|
|
|
<slot :name="slot" v-bind="scope"></slot>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</a-table>
|
|
|
|
|
|
</div>
|
2026-04-22 17:53:20 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
2026-06-04 11:42:07 +08:00
|
|
|
|
import { ref, computed, onMounted, watch, nextTick, h } from "vue";
|
|
|
|
|
|
import { Tooltip } from "ant-design-vue";
|
2026-05-20 17:55:48 +08:00
|
|
|
|
import { calcTableScrollY } from "@/utils/index";
|
2026-04-22 17:53:20 +08:00
|
|
|
|
|
|
|
|
|
|
// --- Types ---
|
|
|
|
|
|
interface Props {
|
|
|
|
|
|
columns: any[];
|
2026-05-15 17:38:03 +08:00
|
|
|
|
scrollY?: string | number; // 优先使用传入的固定值
|
2026-04-22 17:53:20 +08:00
|
|
|
|
listUrl: (params: any) => Promise<any>;
|
2026-05-08 19:11:10 +08:00
|
|
|
|
data?: any[];
|
2026-04-22 17:53:20 +08:00
|
|
|
|
enableRowSelection?: boolean;
|
|
|
|
|
|
rowKey?: string;
|
|
|
|
|
|
searchParams?: Record<string, any>;
|
|
|
|
|
|
defaultPageSize?: number;
|
2026-05-20 17:55:48 +08:00
|
|
|
|
isPage?: boolean;
|
2026-04-27 10:35:06 +08:00
|
|
|
|
getCheckboxProps?: (record: any) => any;
|
2026-04-27 19:11:22 +08:00
|
|
|
|
transformData?: (res: any) => { records: any[]; total: number };
|
2026-06-04 11:42:07 +08:00
|
|
|
|
emptyPlaceholder?: string; // 新增:空值占位符
|
|
|
|
|
|
processEmptyValues?: boolean; // 新增:是否开启空值处理
|
|
|
|
|
|
enableEllipsis?: boolean; // 新增:是否开启超出隐藏
|
|
|
|
|
|
enableSort?: boolean; // 新增:是否启用排序功能
|
2026-04-22 17:53:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const props = withDefaults(defineProps<Props>(), {
|
|
|
|
|
|
enableRowSelection: false,
|
2026-05-20 17:55:48 +08:00
|
|
|
|
rowKey: "id",
|
|
|
|
|
|
isPage: true,
|
2026-05-15 17:38:03 +08:00
|
|
|
|
data: () => [],
|
2026-04-22 17:53:20 +08:00
|
|
|
|
searchParams: () => ({}),
|
|
|
|
|
|
defaultPageSize: 20,
|
2026-04-27 19:11:22 +08:00
|
|
|
|
getCheckboxProps: undefined,
|
|
|
|
|
|
transformData: undefined,
|
2026-05-20 17:55:48 +08:00
|
|
|
|
scrollY: undefined,
|
2026-06-04 11:42:07 +08:00
|
|
|
|
emptyPlaceholder: "-",
|
|
|
|
|
|
processEmptyValues: true, // 默认开启
|
|
|
|
|
|
enableEllipsis: true, // 默认开启
|
|
|
|
|
|
enableSort: false, // 默认关闭,确保旧代码不受影响
|
2026-04-22 17:53:20 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const emit = defineEmits<{
|
2026-05-20 17:55:48 +08:00
|
|
|
|
(e: "data-loaded", params: any, data: any): void;
|
|
|
|
|
|
(e: "selection-change", selectedRowKeys: string[], selectedRows: any[]): void;
|
2026-06-04 11:42:07 +08:00
|
|
|
|
(e: "sort-change", sortInfo: { field: string; order: 'ascend' | 'descend' | null }): void;
|
2026-04-22 17:53:20 +08:00
|
|
|
|
}>();
|
|
|
|
|
|
|
|
|
|
|
|
// --- State ---
|
|
|
|
|
|
const loading = ref(false);
|
|
|
|
|
|
const tableData = ref<any[]>([]);
|
|
|
|
|
|
const total = ref(0);
|
|
|
|
|
|
const page = ref(1);
|
|
|
|
|
|
const size = ref(props.defaultPageSize);
|
|
|
|
|
|
const selectedRowKeys = ref<string[]>([]);
|
|
|
|
|
|
const selectedRows = ref<any[]>([]);
|
2026-05-05 22:38:24 +08:00
|
|
|
|
const lastFilter = ref<Record<string, any> | undefined>(undefined);
|
2026-05-15 17:38:03 +08:00
|
|
|
|
const tableContainerRef = ref<any>(null);
|
|
|
|
|
|
const tableScrollY = ref<number>(0); // ✅ 新增:存储容器高度
|
|
|
|
|
|
|
|
|
|
|
|
// --- Computed Scroll Config ---
|
|
|
|
|
|
const scrollConfig = computed(() => {
|
|
|
|
|
|
const config: any = {
|
2026-05-20 17:55:48 +08:00
|
|
|
|
x: "100%",
|
|
|
|
|
|
y: 0,
|
2026-05-15 17:38:03 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-05-19 18:56:33 +08:00
|
|
|
|
// // 1. 如果父组件传入了固定的 scrollY,优先使用
|
2026-05-15 17:38:03 +08:00
|
|
|
|
if (props.scrollY !== undefined && props.scrollY !== null) {
|
|
|
|
|
|
config.y = props.scrollY;
|
|
|
|
|
|
}
|
|
|
|
|
|
// 2. 否则,如果检测到容器有高度,则使用容器高度作为滚动高度
|
|
|
|
|
|
else if (tableScrollY.value > 0) {
|
|
|
|
|
|
config.y = tableScrollY.value;
|
|
|
|
|
|
}
|
2026-05-26 19:30:22 +08:00
|
|
|
|
console.log(config);
|
2026-05-05 22:38:24 +08:00
|
|
|
|
|
2026-05-15 17:38:03 +08:00
|
|
|
|
return config;
|
|
|
|
|
|
});
|
2026-04-29 16:26:15 +08:00
|
|
|
|
|
2026-04-22 17:53:20 +08:00
|
|
|
|
// --- Row Selection ---
|
|
|
|
|
|
const rowSelection = computed(() => ({
|
|
|
|
|
|
selectedRowKeys: selectedRowKeys.value,
|
|
|
|
|
|
onChange: (keys: string[], rows: any[]) => {
|
|
|
|
|
|
selectedRowKeys.value = keys;
|
|
|
|
|
|
selectedRows.value = rows;
|
2026-05-20 17:55:48 +08:00
|
|
|
|
emit("selection-change", keys, rows);
|
2026-04-22 17:53:20 +08:00
|
|
|
|
},
|
2026-05-15 17:38:03 +08:00
|
|
|
|
getCheckboxProps: props.getCheckboxProps
|
|
|
|
|
|
? props.getCheckboxProps
|
2026-05-20 17:55:48 +08:00
|
|
|
|
: (record: any) => ({}),
|
2026-04-22 17:53:20 +08:00
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
|
|
// --- Pagination Config ---
|
|
|
|
|
|
const paginationConfig = computed(() => ({
|
|
|
|
|
|
total: total.value,
|
|
|
|
|
|
current: page.value,
|
|
|
|
|
|
pageSize: size.value,
|
|
|
|
|
|
showSizeChanger: true,
|
|
|
|
|
|
showQuickJumper: true,
|
|
|
|
|
|
showTotal: (total: number) => `共 ${total} 条`,
|
2026-05-20 17:55:48 +08:00
|
|
|
|
pageSizeOptions: ["20", "50", "100"],
|
2026-04-22 17:53:20 +08:00
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
|
|
// --- Methods ---
|
2026-04-24 15:31:32 +08:00
|
|
|
|
const getList = async (filter?: Record<string, any>) => {
|
2026-05-15 17:38:03 +08:00
|
|
|
|
if (props.data.length > 0) return;
|
2026-04-22 17:53:20 +08:00
|
|
|
|
loading.value = true;
|
2026-05-07 15:40:18 +08:00
|
|
|
|
tableData.value = [];
|
|
|
|
|
|
total.value = 0;
|
2026-05-05 22:38:24 +08:00
|
|
|
|
if (filter !== undefined) {
|
|
|
|
|
|
lastFilter.value = filter;
|
|
|
|
|
|
}
|
2026-05-20 17:55:48 +08:00
|
|
|
|
console.log(props.searchParams);
|
2026-04-22 17:53:20 +08:00
|
|
|
|
try {
|
|
|
|
|
|
const params = {
|
2026-04-30 17:09:07 +08:00
|
|
|
|
...props.searchParams,
|
2026-04-22 17:53:20 +08:00
|
|
|
|
skip: page.value,
|
|
|
|
|
|
take: size.value,
|
2026-05-20 17:55:48 +08:00
|
|
|
|
filter: lastFilter.value,
|
2026-04-22 17:53:20 +08:00
|
|
|
|
};
|
2026-05-20 17:55:48 +08:00
|
|
|
|
if (!props.isPage) {
|
|
|
|
|
|
delete params.take;
|
|
|
|
|
|
delete params.skip;
|
|
|
|
|
|
}
|
2026-04-22 17:53:20 +08:00
|
|
|
|
|
|
|
|
|
|
const res = await props.listUrl(params);
|
2026-04-27 19:11:22 +08:00
|
|
|
|
|
2026-05-15 17:38:03 +08:00
|
|
|
|
let finalRecords: any[] = [];
|
2026-06-01 08:42:06 +08:00
|
|
|
|
let finalTotal = 0;
|
2026-05-15 17:38:03 +08:00
|
|
|
|
|
2026-04-27 19:11:22 +08:00
|
|
|
|
if (props.transformData) {
|
|
|
|
|
|
const result = props.transformData(res);
|
2026-05-15 17:38:03 +08:00
|
|
|
|
finalRecords = result.records || [];
|
|
|
|
|
|
finalTotal = result.total || 0;
|
2026-04-27 19:11:22 +08:00
|
|
|
|
} else {
|
2026-05-20 17:55:48 +08:00
|
|
|
|
finalRecords = res?.data?.records || res?.data?.data || res?.data || [];
|
|
|
|
|
|
finalTotal = res.data?.data?.total || res?.data?.total || res?.total || 0;
|
2026-04-27 19:11:22 +08:00
|
|
|
|
}
|
2026-04-24 15:31:32 +08:00
|
|
|
|
|
2026-06-04 11:42:07 +08:00
|
|
|
|
tableData.value = processData(finalRecords);
|
2026-05-15 17:38:03 +08:00
|
|
|
|
total.value = finalTotal;
|
2026-05-20 17:55:48 +08:00
|
|
|
|
emit("data-loaded", params, { records: finalRecords, total: finalTotal });
|
2026-04-22 17:53:20 +08:00
|
|
|
|
} catch (error) {
|
2026-05-20 17:55:48 +08:00
|
|
|
|
console.error("Fetch table data error:", error);
|
2026-04-22 17:53:20 +08:00
|
|
|
|
tableData.value = [];
|
|
|
|
|
|
total.value = 0;
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
loading.value = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-06-04 11:42:07 +08:00
|
|
|
|
const handleTableChange = (pag: any, filters: any, sorter: any) => {
|
2026-04-22 17:53:20 +08:00
|
|
|
|
page.value = pag.current;
|
|
|
|
|
|
size.value = pag.pageSize;
|
2026-06-04 11:42:07 +08:00
|
|
|
|
if (props.enableSort) {
|
|
|
|
|
|
if (sorter && sorter.field) {
|
|
|
|
|
|
emit("sort-change", { field: sorter.field, order: sorter.order });
|
|
|
|
|
|
} else {
|
|
|
|
|
|
emit("sort-change", { field: '', order: null });
|
|
|
|
|
|
}
|
|
|
|
|
|
}else {
|
|
|
|
|
|
getList();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-22 17:53:20 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const reset = () => {
|
|
|
|
|
|
page.value = 1;
|
|
|
|
|
|
size.value = props.defaultPageSize;
|
|
|
|
|
|
selectedRowKeys.value = [];
|
|
|
|
|
|
selectedRows.value = [];
|
|
|
|
|
|
getList();
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const refresh = () => {
|
|
|
|
|
|
getList();
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-05-07 15:40:18 +08:00
|
|
|
|
const clearSelection = () => {
|
|
|
|
|
|
selectedRowKeys.value = [];
|
|
|
|
|
|
selectedRows.value = [];
|
2026-05-15 17:38:03 +08:00
|
|
|
|
};
|
2026-04-22 17:53:20 +08:00
|
|
|
|
defineExpose({
|
2026-05-15 17:38:03 +08:00
|
|
|
|
loading,
|
2026-04-22 17:53:20 +08:00
|
|
|
|
getList,
|
|
|
|
|
|
reset,
|
|
|
|
|
|
refresh,
|
2026-05-07 15:40:18 +08:00
|
|
|
|
tableData,
|
|
|
|
|
|
clearSelection,
|
2026-04-22 17:53:20 +08:00
|
|
|
|
getSelected: () => ({
|
|
|
|
|
|
keys: selectedRowKeys.value,
|
2026-05-20 17:55:48 +08:00
|
|
|
|
rows: selectedRows.value,
|
|
|
|
|
|
}),
|
2026-04-22 17:53:20 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-05-15 17:38:03 +08:00
|
|
|
|
const observer = new ResizeObserver(() => {
|
|
|
|
|
|
tableScrollY.value = calcTableScrollY(tableContainerRef.value);
|
|
|
|
|
|
});
|
2026-06-04 11:42:07 +08:00
|
|
|
|
|
|
|
|
|
|
// --- 空值处理工具函数 ---
|
|
|
|
|
|
const processData = (records: any[]) => {
|
|
|
|
|
|
if (!props.processEmptyValues || !Array.isArray(records)) return records;
|
|
|
|
|
|
|
|
|
|
|
|
return records.map(record => {
|
|
|
|
|
|
const processedRecord: any = { ...record };
|
|
|
|
|
|
for (const key in processedRecord) {
|
|
|
|
|
|
if (Object.prototype.hasOwnProperty.call(processedRecord, key)) {
|
|
|
|
|
|
const val = processedRecord[key];
|
|
|
|
|
|
if (val === null || val === undefined || val === "") {
|
|
|
|
|
|
processedRecord[key] = props.emptyPlaceholder;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return processedRecord;
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// --- 列配置增强(超出隐藏 + Tooltip)---
|
|
|
|
|
|
const enhancedColumns = computed(() => {
|
|
|
|
|
|
if (!props.enableEllipsis) return props.columns;
|
|
|
|
|
|
|
|
|
|
|
|
return props.columns.map(col => {
|
|
|
|
|
|
if (col.customRender) {
|
|
|
|
|
|
return col; // 如果已有自定义渲染,则不覆盖
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
...col,
|
|
|
|
|
|
ellipsis: true, // 启用 Ant Design Vue 原生的 ellipsis 支持
|
|
|
|
|
|
customRender: ({ text }: any) => {
|
|
|
|
|
|
const content = String(text ?? props.emptyPlaceholder);
|
|
|
|
|
|
return h(
|
|
|
|
|
|
Tooltip,
|
|
|
|
|
|
{ title: content },
|
|
|
|
|
|
() => h('div', {
|
|
|
|
|
|
class: 'table-cell-ellipsis',
|
|
|
|
|
|
style: { overflow: 'hidden', whiteSpace: 'nowrap', textOverflow: 'ellipsis' }
|
|
|
|
|
|
}, content)
|
|
|
|
|
|
);
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-04-22 17:53:20 +08:00
|
|
|
|
onMounted(() => {
|
2026-05-15 17:38:03 +08:00
|
|
|
|
nextTick(() => {
|
|
|
|
|
|
tableScrollY.value = calcTableScrollY(tableContainerRef.value);
|
|
|
|
|
|
if (tableContainerRef.value) {
|
|
|
|
|
|
observer.observe(tableContainerRef.value);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
if (props.data && props.data.length > 0) {
|
2026-06-04 11:42:07 +08:00
|
|
|
|
tableData.value = processData(props.data);
|
2026-05-15 17:38:03 +08:00
|
|
|
|
}
|
2026-04-22 17:53:20 +08:00
|
|
|
|
});
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
2026-05-15 17:38:03 +08:00
|
|
|
|
<style scoped lang="scss"></style>
|