20260811221831
This commit is contained in:
1 parent
ce78653207
commit
1d7f7c2bba
3 files changed
+82
-52
No files matched your search
@@ -12,7 +12,7 @@ const props = defineProps({
|
|||||||
virtual: { type: Boolean, default: true },
|
virtual: { type: Boolean, default: true },
|
||||||
virtualX: { type: Boolean, default: true },
|
virtualX: { type: Boolean, default: true },
|
||||||
colResizable: { type: Boolean, default: true },
|
colResizable: { type: Boolean, default: true },
|
||||||
bordered: { type: [Boolean, String], default: true },
|
bordered: { type: [Boolean, String], default: false },
|
||||||
noDataFull: { type: Boolean, default: true },
|
noDataFull: { type: Boolean, default: true },
|
||||||
showOverflow: { type: Boolean, default: true },
|
showOverflow: { type: Boolean, default: true },
|
||||||
columns: { type: Array, default: () => [] },
|
columns: { type: Array, default: () => [] },
|
||||||
@@ -44,7 +44,6 @@ const tableRef = ref(null);
|
|||||||
const slots = useSlots();
|
const slots = useSlots();
|
||||||
const appStore = useAppStore();
|
const appStore = useAppStore();
|
||||||
const tableTheme = computed(() => props.theme ?? (appStore.isDark ? "dark" : "light"));
|
const tableTheme = computed(() => props.theme ?? (appStore.isDark ? "dark" : "light"));
|
||||||
const containerHeight = ref(0);
|
|
||||||
|
|
||||||
// Tab 面板从隐藏恢复时,等到容器拥有有效尺寸再刷新虚拟滚动布局。
|
// Tab 面板从隐藏恢复时,等到容器拥有有效尺寸再刷新虚拟滚动布局。
|
||||||
// RAF 将同一帧内的多次 ResizeObserver 通知合并,避免重复重排。
|
// RAF 将同一帧内的多次 ResizeObserver 通知合并,避免重复重排。
|
||||||
@@ -59,7 +58,6 @@ const { resume: refreshLayout } = useRafFn(
|
|||||||
|
|
||||||
useResizeObserver(rootRef, ([entry]) => {
|
useResizeObserver(rootRef, ([entry]) => {
|
||||||
const { width, height } = entry.contentRect;
|
const { width, height } = entry.contentRect;
|
||||||
containerHeight.value = Math.floor(height);
|
|
||||||
if (width > 0 && height > 0) refreshLayout();
|
if (width > 0 && height > 0) refreshLayout();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -250,7 +248,10 @@ function onColumnsUpdate(columns) {
|
|||||||
const real = (columns || [])
|
const real = (columns || [])
|
||||||
.filter(
|
.filter(
|
||||||
(c) =>
|
(c) =>
|
||||||
!isSelectionColumn(c) && c.type !== "seq" && c.type !== "dragRow" && c.dataIndex !== "",
|
!isSelectionColumn(c) &&
|
||||||
|
c.type !== "seq" &&
|
||||||
|
c.type !== "dragRow" &&
|
||||||
|
c.dataIndex !== "",
|
||||||
)
|
)
|
||||||
.map((col) => {
|
.map((col) => {
|
||||||
const original = originalByKey.get(String(col.key ?? col.dataIndex ?? ""));
|
const original = originalByKey.get(String(col.key ?? col.dataIndex ?? ""));
|
||||||
@@ -266,19 +267,14 @@ function handleRowOrderChange(sourceIndex, targetIndex) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 行高同时作为 CSS 变量与 StkTable 的 row-height / header-row-height
|
// 行高同时作为 CSS 变量与 StkTable 的 row-height / header-row-height
|
||||||
const rowHeight = 34;
|
const rowHeight = 28;
|
||||||
const headerRowHeight = 40;
|
const headerRowHeight = 30;
|
||||||
const hasBottomGap = computed(() => {
|
|
||||||
if (!props.dataSource.length || containerHeight.value <= 0) return false;
|
|
||||||
const bodyHeight = Math.max(containerHeight.value - headerRowHeight, 0);
|
|
||||||
return props.dataSource.length * rowHeight < bodyHeight - 1;
|
|
||||||
});
|
|
||||||
const customVars = computed(() => {
|
const customVars = computed(() => {
|
||||||
return {
|
return {
|
||||||
"--header-row-height": `${headerRowHeight}px`,
|
"--header-row-height": `${headerRowHeight}px`,
|
||||||
"--row-height": `${rowHeight}px`,
|
"--row-height": `${rowHeight}px`,
|
||||||
"--footer-row-height": "36px",
|
"--footer-row-height": "36px",
|
||||||
"--th-bgc": "var(--background)",
|
"--th-bgc": "#f5f5f8",
|
||||||
"--tf-bgc": "var(--background)",
|
"--tf-bgc": "var(--background)",
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -526,16 +522,10 @@ defineExpose({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div ref="rootRef" class="fms-table" @keydown.capture="handleKeydownCapture">
|
||||||
ref="rootRef"
|
|
||||||
class="fms-table"
|
|
||||||
:class="{ 'fms-table--bordered': bordered }"
|
|
||||||
@keydown.capture="handleKeydownCapture"
|
|
||||||
>
|
|
||||||
<StkTable
|
<StkTable
|
||||||
ref="tableRef"
|
ref="tableRef"
|
||||||
class="fms-table__inner"
|
class="fms-table__inner"
|
||||||
:class="{ 'fms-table--bottom-gap': hasBottomGap }"
|
|
||||||
v-bind="$attrs"
|
v-bind="$attrs"
|
||||||
:style="customVars"
|
:style="customVars"
|
||||||
:theme="tableTheme"
|
:theme="tableTheme"
|
||||||
@@ -554,7 +544,7 @@ defineExpose({
|
|||||||
:no-data-full="noDataFull"
|
:no-data-full="noDataFull"
|
||||||
:show-overflow="showOverflow"
|
:show-overflow="showOverflow"
|
||||||
:area-selection="editable"
|
:area-selection="editable"
|
||||||
:bordered="false"
|
:bordered="bordered"
|
||||||
@area-selection-change="handleAreaSelectionChange"
|
@area-selection-change="handleAreaSelectionChange"
|
||||||
@row-order-change="handleRowOrderChange"
|
@row-order-change="handleRowOrderChange"
|
||||||
>
|
>
|
||||||
@@ -576,19 +566,32 @@ defineExpose({
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.fms-table {
|
.fms-table {
|
||||||
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
border-radius: 8px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
.fms-table--bordered {
|
|
||||||
|
/*
|
||||||
|
* 用伪元素画外框放到最上层,避免与 td 底边重叠。
|
||||||
|
* 不管数据多少,外框始终完整,td 行线在伪元素之下。
|
||||||
|
*/
|
||||||
|
.fms-table::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
pointer-events: none;
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: 8px;
|
border-radius: inherit;
|
||||||
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fms-table__inner {
|
.fms-table__inner {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<!-- 非 scoped 全局样式:原生 checkbox 保持低组件开销,外观使用 shadcn 语义变量。 -->
|
<!-- 非 scoped 全局样式:原生 checkbox 保持低组件开销,外观使用 shadcn 语义变量。 -->
|
||||||
@@ -600,6 +603,11 @@ defineExpose({
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 行间分割线 */
|
||||||
|
.stk-tbody-main td {
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
th.fms-selection-header {
|
th.fms-selection-header {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
|
||||||
@@ -613,29 +621,6 @@ defineExpose({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
* 外层绘制表格边框,表体只保留横向分割线。
|
|
||||||
*/
|
|
||||||
.fms-table__inner.stk-table {
|
|
||||||
th,
|
|
||||||
td {
|
|
||||||
border-bottom: 1px solid var(--border);
|
|
||||||
}
|
|
||||||
|
|
||||||
thead th {
|
|
||||||
border-bottom-color: var(--border);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 数据铺满视口时隐藏最后一行底线,避免与外框底线叠加;数据较少时保留底线标记数据区域结束。
|
|
||||||
&:not(.fms-table--bottom-gap) {
|
|
||||||
.stk-tbody-main tr:last-child {
|
|
||||||
td {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.fms-checkbox {
|
.fms-checkbox {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -782,9 +767,6 @@ defineExpose({
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fms-table__inner td.fms-editor-cell {
|
|
||||||
padding-inline: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
// actions 列按钮容器
|
// actions 列按钮容器
|
||||||
.fms-actions-cell {
|
.fms-actions-cell {
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
|||||||
<AlertDialogPortal>
|
<AlertDialogPortal>
|
||||||
<AlertDialogOverlay
|
<AlertDialogOverlay
|
||||||
data-slot="alert-dialog-overlay"
|
data-slot="alert-dialog-overlay"
|
||||||
class="data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80"
|
class="data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50"
|
||||||
/>
|
/>
|
||||||
<AlertDialogContent
|
<AlertDialogContent
|
||||||
data-slot="alert-dialog-content"
|
data-slot="alert-dialog-content"
|
||||||
|
|||||||
@@ -22,9 +22,10 @@ type DemoRow = {
|
|||||||
status: number;
|
status: number;
|
||||||
enabled: number;
|
enabled: number;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
|
[key: string]: any;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ROW_COUNT = 5000;
|
const ROW_COUNT = 20;
|
||||||
const DEMO_NAMES = ["客户资料", "订单状态", "库存预警", "合同归档", "发票登记"];
|
const DEMO_NAMES = ["客户资料", "订单状态", "库存预警", "合同归档", "发票登记"];
|
||||||
const OWNER_OPTIONS = [
|
const OWNER_OPTIONS = [
|
||||||
{ label: "林晓", value: "lin-xiao" },
|
{ label: "林晓", value: "lin-xiao" },
|
||||||
@@ -56,8 +57,12 @@ const statusFilter = ref("all");
|
|||||||
const selectedRowKeys = ref<string[]>([]);
|
const selectedRowKeys = ref<string[]>([]);
|
||||||
const activeRowKey = ref<string | null>(null);
|
const activeRowKey = ref<string | null>(null);
|
||||||
const nextRowNumber = ref(ROW_COUNT + 1);
|
const nextRowNumber = ref(ROW_COUNT + 1);
|
||||||
|
const nextColumnNumber = ref(1);
|
||||||
|
const extraColumns = shallowRef<
|
||||||
|
{ dataIndex: string; title: string; width: number; editor: { type: string } }[]
|
||||||
|
>([]);
|
||||||
|
|
||||||
const columns = shallowRef([
|
const baseColumns = [
|
||||||
{ dataIndex: "code", title: "记录编号", width: 145, fixed: "left" },
|
{ dataIndex: "code", title: "记录编号", width: 145, fixed: "left" },
|
||||||
{ dataIndex: "name", title: "项目名称", width: 220, editor: { type: "input" } },
|
{ dataIndex: "name", title: "项目名称", width: 220, editor: { type: "input" } },
|
||||||
{
|
{
|
||||||
@@ -108,7 +113,9 @@ const columns = shallowRef([
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]);
|
];
|
||||||
|
|
||||||
|
const columns = shallowRef([...baseColumns]);
|
||||||
|
|
||||||
const filteredRows = computed(() => {
|
const filteredRows = computed(() => {
|
||||||
const keyword = searchQuery.value.trim().toLowerCase();
|
const keyword = searchQuery.value.trim().toLowerCase();
|
||||||
@@ -133,6 +140,37 @@ function syncRows() {
|
|||||||
dataSource.value = [...dataSource.value];
|
dataSource.value = [...dataSource.value];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function syncColumns() {
|
||||||
|
columns.value = [...baseColumns, ...extraColumns.value];
|
||||||
|
}
|
||||||
|
|
||||||
|
function addColumn() {
|
||||||
|
const colNum = nextColumnNumber.value++;
|
||||||
|
const dataIndex = `field_${colNum}`;
|
||||||
|
const title = `自定义${colNum}`;
|
||||||
|
// 为已有行补上该字段的默认值
|
||||||
|
dataSource.value = dataSource.value.map((row) => ({
|
||||||
|
...row,
|
||||||
|
[dataIndex]: row[dataIndex] ?? "",
|
||||||
|
}));
|
||||||
|
extraColumns.value = [
|
||||||
|
...extraColumns.value,
|
||||||
|
{ dataIndex, title, width: 160, editor: { type: "input" } },
|
||||||
|
];
|
||||||
|
syncColumns();
|
||||||
|
toast.success(`已新增列: ${title}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeColumn(dataIndex: string) {
|
||||||
|
extraColumns.value = extraColumns.value.filter((col) => col.dataIndex !== dataIndex);
|
||||||
|
dataSource.value = dataSource.value.map((row) => {
|
||||||
|
const { [dataIndex]: _, ...rest } = row;
|
||||||
|
return rest as DemoRow;
|
||||||
|
});
|
||||||
|
syncColumns();
|
||||||
|
toast.success(`已移除列`);
|
||||||
|
}
|
||||||
|
|
||||||
function onTableChange(
|
function onTableChange(
|
||||||
row: DemoRow,
|
row: DemoRow,
|
||||||
column: { title?: string; dataIndex?: string },
|
column: { title?: string; dataIndex?: string },
|
||||||
@@ -152,9 +190,12 @@ function onRowOrderChange(rows: DemoRow[]) {
|
|||||||
|
|
||||||
function regenerate() {
|
function regenerate() {
|
||||||
dataSource.value = buildRows();
|
dataSource.value = buildRows();
|
||||||
|
extraColumns.value = [];
|
||||||
|
syncColumns();
|
||||||
selectedRowKeys.value = [];
|
selectedRowKeys.value = [];
|
||||||
activeRowKey.value = null;
|
activeRowKey.value = null;
|
||||||
nextRowNumber.value = ROW_COUNT + 1;
|
nextRowNumber.value = ROW_COUNT + 1;
|
||||||
|
nextColumnNumber.value = 1;
|
||||||
toast.success(`已生成 ${ROW_COUNT.toLocaleString()} 条演示数据`);
|
toast.success(`已生成 ${ROW_COUNT.toLocaleString()} 条演示数据`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,6 +210,9 @@ async function addRow() {
|
|||||||
enabled: 1,
|
enabled: 1,
|
||||||
updatedAt: "2026-08-10 20:00",
|
updatedAt: "2026-08-10 20:00",
|
||||||
};
|
};
|
||||||
|
for (const col of extraColumns.value) {
|
||||||
|
row[col.dataIndex] = "";
|
||||||
|
}
|
||||||
|
|
||||||
dataSource.value = [...dataSource.value, row];
|
dataSource.value = [...dataSource.value, row];
|
||||||
searchQuery.value = "";
|
searchQuery.value = "";
|
||||||
@@ -234,6 +278,10 @@ function clearSelection() {
|
|||||||
<RefreshCw data-icon="inline-start" />
|
<RefreshCw data-icon="inline-start" />
|
||||||
重新生成
|
重新生成
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button variant="outline" size="sm" @click="addColumn">
|
||||||
|
<Plus data-icon="inline-start" />
|
||||||
|
新增列
|
||||||
|
</Button>
|
||||||
<Button variant="outline" size="sm" :disabled="!selectedCount" @click="editFirstSelected">
|
<Button variant="outline" size="sm" :disabled="!selectedCount" @click="editFirstSelected">
|
||||||
<Pencil data-icon="inline-start" />
|
<Pencil data-icon="inline-start" />
|
||||||
编辑选中
|
编辑选中
|
||||||
|
|||||||
Reference in new issue
Block a user