20260722173146
This commit is contained in:
1 parent
e9f099f044
commit
d04fad24ed
24 files changed
+1309
-422
No files matched your search
@@ -1,261 +0,0 @@
|
||||
<script setup>
|
||||
import { computed, defineComponent, h, nextTick, ref, useSlots } from "vue";
|
||||
import { useRafFn, useResizeObserver } from "@vueuse/core";
|
||||
import { Checkbox, Input, InputNumber, Select } from "antdv-next";
|
||||
import { StkTable } from "stk-table-vue";
|
||||
|
||||
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 },
|
||||
});
|
||||
|
||||
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 || []),
|
||||
);
|
||||
|
||||
function changeSelection(row, checked) {
|
||||
const key = getRowKey(row);
|
||||
const nextKeys = new Set(selectedRowKeySet.value);
|
||||
if (checked) nextKeys.add(key);
|
||||
else nextKeys.delete(key);
|
||||
const keys = Array.from(nextKeys);
|
||||
const rows = props.dataSource.filter((item) => nextKeys.has(getRowKey(item)));
|
||||
props.rowSelection?.onChange?.(keys, rows);
|
||||
}
|
||||
|
||||
function changeAllSelection(checked) {
|
||||
const visibleKeys = props.dataSource.map(getRowKey);
|
||||
const nextKeys = new Set(selectedRowKeySet.value);
|
||||
visibleKeys.forEach((key) => {
|
||||
if (checked) nextKeys.add(key);
|
||||
else nextKeys.delete(key);
|
||||
});
|
||||
const keys = Array.from(nextKeys);
|
||||
const rows = props.dataSource.filter((item) => nextKeys.has(getRowKey(item)));
|
||||
props.rowSelection?.onChange?.(keys, rows);
|
||||
}
|
||||
|
||||
const SelectionCell = defineComponent({
|
||||
props: ["row"],
|
||||
setup(cellProps) {
|
||||
return () =>
|
||||
h(Checkbox, {
|
||||
checked: selectedRowKeySet.value.has(getRowKey(cellProps.row)),
|
||||
"onUpdate:checked": (checked) => changeSelection(cellProps.row, checked),
|
||||
onClick: (event) => event.stopPropagation(),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const SelectionHeaderCell = defineComponent({
|
||||
setup() {
|
||||
return () => {
|
||||
const visibleKeys = props.dataSource.map(getRowKey);
|
||||
const selectedCount = visibleKeys.filter((key) => selectedRowKeySet.value.has(key)).length;
|
||||
return h(Checkbox, {
|
||||
checked: visibleKeys.length > 0 && selectedCount === visibleKeys.length,
|
||||
indeterminate: selectedCount > 0 && selectedCount < visibleKeys.length,
|
||||
"onUpdate:checked": changeAllSelection,
|
||||
onClick: (event) => event.stopPropagation(),
|
||||
});
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
const EditableCell = defineComponent({
|
||||
props: ["row", "col", "cellValue"],
|
||||
setup(cellProps) {
|
||||
function editorOptions() {
|
||||
return typeof cellProps.col.editor === "string"
|
||||
? { type: cellProps.col.editor }
|
||||
: cellProps.col.editor || {};
|
||||
}
|
||||
|
||||
function updateValue(value) {
|
||||
cellProps.row[cellProps.col.dataIndex] = value;
|
||||
editorOptions().onChange?.(value, cellProps.row, cellProps.col);
|
||||
}
|
||||
|
||||
return () => {
|
||||
const editor = editorOptions();
|
||||
const disabled =
|
||||
typeof editor.disabled === "function" ? editor.disabled(cellProps.row) : editor.disabled;
|
||||
const commonProps = {
|
||||
size: "small",
|
||||
disabled,
|
||||
style: { width: "100%" },
|
||||
};
|
||||
|
||||
if (editor.type === "number") {
|
||||
return h(InputNumber, {
|
||||
...commonProps,
|
||||
value: cellProps.row[cellProps.col.dataIndex],
|
||||
min: editor.min,
|
||||
max: editor.max,
|
||||
precision: editor.precision,
|
||||
"onUpdate:value": updateValue,
|
||||
});
|
||||
}
|
||||
if (editor.type === "select") {
|
||||
return h(Select, {
|
||||
...commonProps,
|
||||
value: cellProps.row[cellProps.col.dataIndex],
|
||||
options: editor.options || [],
|
||||
"onUpdate:value": updateValue,
|
||||
});
|
||||
}
|
||||
if (editor.type === "checkbox") {
|
||||
const checkedValue = editor.checkedValue ?? 1;
|
||||
const uncheckedValue = editor.uncheckedValue ?? 0;
|
||||
return h(Checkbox, {
|
||||
disabled,
|
||||
checked: cellProps.row[cellProps.col.dataIndex] === checkedValue,
|
||||
"onUpdate:checked": (checked) => updateValue(checked ? checkedValue : uncheckedValue),
|
||||
onClick: (event) => event.stopPropagation(),
|
||||
});
|
||||
}
|
||||
return h(Input, {
|
||||
...commonProps,
|
||||
value: cellProps.row[cellProps.col.dataIndex],
|
||||
allowClear: editor.allowClear ?? true,
|
||||
maxlength: editor.maxlength,
|
||||
placeholder:
|
||||
typeof editor.placeholder === "function"
|
||||
? editor.placeholder(cellProps.row)
|
||||
: editor.placeholder,
|
||||
status: typeof editor.status === "function" ? editor.status(cellProps.row) : editor.status,
|
||||
"onUpdate:value": updateValue,
|
||||
});
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
// 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%;
|
||||
}
|
||||
</style>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,80 @@
|
||||
import { h } from "vue";
|
||||
import { Checkbox, Input, InputNumber, Select } from "antdv-next";
|
||||
|
||||
function resolveOption(option, row) {
|
||||
return typeof option === "function" ? option(row) : option;
|
||||
}
|
||||
|
||||
export function createEditableCellRenderer({ getRowKey, getActiveRowKey }) {
|
||||
return function EditableCell({ row, col, cellValue }) {
|
||||
const editor = typeof col.editor === "string" ? { type: col.editor } : col.editor || {};
|
||||
const activeRowKey = getActiveRowKey();
|
||||
const isEditing = activeRowKey != null && String(getRowKey(row)) === String(activeRowKey);
|
||||
|
||||
if (!isEditing) {
|
||||
if (editor.type === "checkbox") {
|
||||
return h(Checkbox, {
|
||||
checked: cellValue === (editor.checkedValue ?? 1),
|
||||
style: { pointerEvents: "none" },
|
||||
});
|
||||
}
|
||||
if (editor.type === "select") {
|
||||
const options = resolveOption(editor.options, row) || [];
|
||||
const option = options.find((item) => item.value === cellValue);
|
||||
return h("span", { class: "fms-cell-text" }, option?.label ?? cellValue ?? "");
|
||||
}
|
||||
return h("span", { class: "fms-cell-text" }, cellValue ?? "");
|
||||
}
|
||||
|
||||
const update = (value) => {
|
||||
row[col.dataIndex] = value;
|
||||
editor.onChange?.(value, row, col);
|
||||
};
|
||||
const common = {
|
||||
size: "small",
|
||||
style: { width: "100%" },
|
||||
onClick: (event) => event.stopPropagation(),
|
||||
};
|
||||
|
||||
if (editor.type === "number") {
|
||||
return h(InputNumber, {
|
||||
...common,
|
||||
value: cellValue,
|
||||
min: resolveOption(editor.min, row),
|
||||
max: resolveOption(editor.max, row),
|
||||
precision: resolveOption(editor.precision, row),
|
||||
disabled: resolveOption(editor.disabled, row),
|
||||
"onUpdate:value": update,
|
||||
});
|
||||
}
|
||||
if (editor.type === "select") {
|
||||
return h(Select, {
|
||||
...common,
|
||||
value: cellValue,
|
||||
options: resolveOption(editor.options, row) || [],
|
||||
disabled: resolveOption(editor.disabled, row),
|
||||
"onUpdate:value": update,
|
||||
});
|
||||
}
|
||||
if (editor.type === "checkbox") {
|
||||
const checkedValue = editor.checkedValue ?? 1;
|
||||
const uncheckedValue = editor.uncheckedValue ?? 0;
|
||||
return h(Checkbox, {
|
||||
checked: cellValue === checkedValue,
|
||||
disabled: resolveOption(editor.disabled, row),
|
||||
onClick: (event) => event.stopPropagation(),
|
||||
"onUpdate:checked": (checked) => update(checked ? checkedValue : uncheckedValue),
|
||||
});
|
||||
}
|
||||
return h(Input, {
|
||||
...common,
|
||||
value: cellValue,
|
||||
disabled: resolveOption(editor.disabled, row),
|
||||
maxlength: resolveOption(editor.maxlength, row),
|
||||
placeholder: resolveOption(editor.placeholder, row),
|
||||
status: resolveOption(editor.status, row),
|
||||
allowClear: editor.allowClear ?? true,
|
||||
"onUpdate:value": update,
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { h } from "vue";
|
||||
import { Checkbox } from "antdv-next";
|
||||
|
||||
export function createSelectionCellRenderers({
|
||||
getDataSource,
|
||||
getRowKey,
|
||||
getSelectedRowKeySet,
|
||||
onChange,
|
||||
}) {
|
||||
function updateSelection(nextKeys) {
|
||||
const keySet = new Set(nextKeys);
|
||||
const rows = getDataSource().filter((row) => keySet.has(getRowKey(row)));
|
||||
onChange(nextKeys, rows);
|
||||
}
|
||||
|
||||
function changeRow(row, checked) {
|
||||
const nextKeys = new Set(getSelectedRowKeySet());
|
||||
const key = getRowKey(row);
|
||||
if (checked) nextKeys.add(key);
|
||||
else nextKeys.delete(key);
|
||||
updateSelection(Array.from(nextKeys));
|
||||
}
|
||||
|
||||
function changeAll(checked) {
|
||||
const nextKeys = new Set(getSelectedRowKeySet());
|
||||
getDataSource().forEach((row) => {
|
||||
const key = getRowKey(row);
|
||||
if (checked) nextKeys.add(key);
|
||||
else nextKeys.delete(key);
|
||||
});
|
||||
updateSelection(Array.from(nextKeys));
|
||||
}
|
||||
|
||||
const SelectionCell = ({ row }) =>
|
||||
h(Checkbox, {
|
||||
checked: getSelectedRowKeySet().has(getRowKey(row)),
|
||||
"onUpdate:checked": (checked) => changeRow(row, checked),
|
||||
onClick: (event) => event.stopPropagation(),
|
||||
});
|
||||
|
||||
const SelectionHeaderCell = () => {
|
||||
const rows = getDataSource();
|
||||
const selectedRowKeySet = getSelectedRowKeySet();
|
||||
const selectedCount = rows.filter((row) => selectedRowKeySet.has(getRowKey(row))).length;
|
||||
return h(Checkbox, {
|
||||
checked: rows.length > 0 && selectedCount === rows.length,
|
||||
indeterminate: selectedCount > 0 && selectedCount < rows.length,
|
||||
"onUpdate:checked": changeAll,
|
||||
onClick: (event) => event.stopPropagation(),
|
||||
});
|
||||
};
|
||||
|
||||
return { SelectionCell, SelectionHeaderCell };
|
||||
}
|
||||
Reference in new issue
Block a user