20260722173146

This commit is contained in:
oneao committed 2026-07-22 17:31:48 +08:00
1 parent e9f099f044
commit d04fad24ed
24 files changed
+1309 -422

No files matched your search

@@ -0,0 +1,164 @@
<script setup>
import { computed, nextTick, ref, useSlots } from "vue";
import { useRafFn, useResizeObserver } from "@vueuse/core";
import { StkTable } from "stk-table-vue";
import { createEditableCellRenderer } from "./utils/createEditableCellRenderer";
import { createSelectionCellRenderers } from "./utils/createSelectionCellRenderers";
defineOptions({ inheritAttrs: false });
const props = defineProps({
virtual: { type: Boolean, default: true },
virtualX: { type: Boolean, default: true },
colResizable: { type: Boolean, default: true },
bordered: { type: [Boolean, String], default: true },
noDataFull: { type: Boolean, default: true },
columns: { type: Array, default: () => [] },
dataSource: { type: Array, default: () => [] },
rowKey: { type: [String, Function], default: "id" },
rowSelection: { type: Object, default: null },
// 当前处于编辑状态的行 key;为 null 时所有行均以纯文本展示(单行编辑模式)。
activeRowKey: { type: [String, Number], default: null },
});
const emit = defineEmits(["update:columns"]);
function getRowKey(row) {
return typeof props.rowKey === "function" ? props.rowKey(row) : row?.[props.rowKey];
}
const selectedRowKeySet = computed(() => new Set(props.rowSelection?.selectedRowKeys || []));
const EditableCell = createEditableCellRenderer({
getRowKey,
getActiveRowKey: () => props.activeRowKey,
});
const { SelectionCell, SelectionHeaderCell } = createSelectionCellRenderers({
getDataSource: () => props.dataSource,
getRowKey,
getSelectedRowKeySet: () => selectedRowKeySet.value,
onChange: (keys, rows) => props.rowSelection?.onChange?.(keys, rows),
});
// StkTable 的编辑单元格和多选列统一在封装层生成,业务页面只声明 editor / rowSelection。
const tableColumnsModel = computed({
get: () => {
const columns = props.columns.map((column) =>
column.editor && !column.customCell
? { ...column, customCell: EditableCell, __fmsEditor: true }
: column,
);
if (!props.rowSelection) return columns;
return [
{
dataIndex: "__fmsSelection",
key: "__fmsSelection",
title: "",
width: 44,
fixed: "left",
align: "center",
headerAlign: "center",
customCell: SelectionCell,
customHeaderCell: SelectionHeaderCell,
__fmsSelection: true,
},
...columns,
];
},
set: (value) =>
emit(
"update:columns",
value
.filter((column) => !column.__fmsSelection)
.map((column) => {
if (!column.__fmsEditor) return column;
const rest = { ...column };
delete rest.customCell;
delete rest.__fmsEditor;
return rest;
}),
),
});
const slots = useSlots();
const tableRef = ref(null);
// 表格样式
const customVars = ref({
"--header-row-height": "40px",
"--row-height": "34px",
"--footer-row-height": "36px",
});
const { resume: refreshLayout } = useRafFn(
async () => {
await nextTick();
tableRef.value?.initVirtualScroll?.();
await nextTick();
},
{ immediate: false, once: true },
);
useResizeObserver(
() => tableRef.value?.$el,
([entry]) => {
const { width, height } = entry.contentRect;
if (width > 0 && height > 0) refreshLayout();
},
);
// Forward every slot except `empty`; the `empty` slot is handled below so the
// default Result fallback only renders when the parent does not provide one.
const forwardSlots = computed(() => {
const result = {};
for (const name in slots) {
if (name !== "empty") result[name] = slots[name];
}
return result;
});
</script>
<template>
<StkTable
ref="tableRef"
class="fms-table"
v-bind="$attrs"
:style="customVars"
:auto-resize="false"
:virtual="virtual"
:virtual-x="virtualX"
:col-resizable="colResizable"
v-model:columns="tableColumnsModel"
:data-source="dataSource"
:row-key="rowKey"
:bordered="bordered"
:no-data-full="noDataFull"
>
<template v-for="(slot, name) in forwardSlots" :key="name" #[name]="slotProps">
<slot :name="name" v-bind="slotProps || {}" />
</template>
<template v-if="slots.empty" #empty>
<slot name="empty" />
</template>
<template v-else #empty>
<a-result status="info" title="暂无数据" sub-title="当前没有可显示的内容" />
</template>
</StkTable>
</template>
<style scoped>
.fms-table {
width: 100%;
height: 100%;
}
:deep(.fms-cell-text) {
display: inline-block;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
vertical-align: middle;
}
</style>