20260804223152

This commit is contained in:
oneao committed 2026-08-04 22:31:52 +08:00
1 parent 75b1ccaa6d
commit 7c9761a4fc
71 files changed
+5534 -9113

No files matched your search

@@ -1,27 +1,36 @@
<script setup>
import { computed, h, nextTick, onActivated, ref, useSlots, watch } from "vue";
import {
computed,
h,
nextTick,
onActivated,
reactive,
ref,
shallowRef,
useSlots,
watch,
} from "vue";
import { useRoute } from "vue-router";
import { message } from "antdv-next";
import { Columns3, RefreshCw, Save } from "@lucide/vue";
import { saveObjectApi } from "@/services/api";
import { loadDataApi, nextIdApi, pageDataApi, saveObjectApi } from "@/services/api";
import { useAppStore } from "@/stores/app";
import { useAuthStore } from "@/stores/auth";
import { usePermissionStore } from "@/stores/permissions";
import { useModuleRuntime } from "@/module-runtime/useModuleRuntime";
import { useModuleDataSource } from "@/module-runtime/useModuleDataSource";
import { useModuleEditor } from "@/module-runtime/useModuleEditor";
import { createModuleActionRegistry } from "@/module-runtime/actionRegistry";
import {
identifierRe,
buildQueryCondition,
defaultQueryValue,
escapeSqlText,
parseOptions,
requireModuleId,
} from "./moduleListUtils";
import FmsModuleTable from "./FmsModuleTable.vue";
import FmsQueryToolbar from "./FmsQueryToolbar.vue";
const props = defineProps({
moduleId: { type: String, default: "" },
profileId: { type: String, default: "" },
pageNodeId: { type: String, default: "" },
dataCode: { type: String, default: "" },
fixedSearchCondition: { type: String, default: "" },
columnExtensions: { type: Object, default: () => ({}) },
rowActions: { type: Array, default: () => [] },
actionHandlers: { type: Object, default: () => ({}) },
rowSelection: { type: Object, default: null },
rowClassName: { type: Function, default: undefined },
expandable: { type: [Boolean, Object], default: false },
@@ -46,94 +55,39 @@ const emit = defineEmits([
const slots = useSlots();
const route = useRoute();
const appStore = useAppStore();
const authStore = useAuthStore();
const permissionStore = usePermissionStore();
const tableRef = ref(null);
// keep-alive 中的缓存实例必须保留各自的路由上下文,不能跟随全局 route 变化。
const instanceRoutePath = route.path;
const instanceRouteDataCode = route.meta.dataCode;
// ── 内部状态 ──
const dataModule = ref(null);
const activeFields = shallowRef([]);
const activeFieldById = shallowRef(new Map());
const listConfig = shallowRef([]);
const editConfigById = shallowRef(new Map());
const queryItems = shallowRef([]);
const dataSource = shallowRef([]);
const searchConditions = shallowRef([]);
const keyField = ref("");
// 可编辑表格:脏行追踪(rowKey -> Set<已修改字段>),仅在 editable 模式下使用。
const dirtyRows = shallowRef(new Map());
// 可编辑表格:未持久化的新增行 rowKey 集合(新增行保存时作为 inserts 提交)。
const newRowKeys = shallowRef(new Set());
const saving = ref(false);
const configLoading = ref(false);
const dataLoading = ref(false);
const columnSaving = ref(false);
function canModuleAction(actionCode) {
if (!actionCode || !dataModule.value) return false;
return permissionStore.canAction(dataModule.value.b_module_id, actionCode);
}
const moduleRuntime = useModuleRuntime({
orgId: () => authStore.orgId,
moduleId: () => props.moduleId,
profileId: () => props.profileId,
pageNodeId: () => props.pageNodeId,
routePath: instanceRoutePath,
const configError = ref("");
const dataError = ref("");
const pagination = reactive({
current: 1,
pageSize: Number(props.pageSize) || 20,
total: 0,
});
const {
dataModule,
activeFields,
activeFieldById,
listConfig,
formConfigByFieldId: editConfigById,
queryItems,
defaultSearchConditions,
groups,
actions,
keyField,
loading: configLoading,
error: configError,
} = moduleRuntime;
const moduleDataSource = useModuleDataSource({
dataModule,
activeFieldById,
listConfig,
keyField,
runtimeKey: moduleRuntime.runtimeKey,
fixedSearchCondition: () => props.fixedSearchCondition,
pageSize: () => props.pageSize,
});
const {
dataSource,
loading: dataLoading,
error: dataError,
pagination,
fetchData,
search,
resetSearch,
changePage,
reload,
} = moduleDataSource;
const moduleEditor = useModuleEditor({
dataModule,
activeFields,
activeFieldById,
listConfig,
keyField,
dataSource,
editable: () => props.editable,
rowDraggable: () => props.rowDraggable,
orderField: () => props.orderField,
loading: () => configLoading.value || dataLoading.value,
canAction: canModuleAction,
reload: fetchData,
getTable: () => tableRef.value,
onDirtyChange: (count) => emit("dirty-change", { count }),
});
const {
dirtyRows,
saving,
readonlyFields,
resolveRowKey,
handleCellChange,
addRow,
saveChanges,
deleteSelected,
handleRowOrderChange,
} = moduleEditor;
let dataRequestVersion = 0;
const loading = computed(() => configLoading.value || dataLoading.value);
const permittedRowActions = computed(() =>
props.rowActions.filter((action) => canModuleAction(action.permissionCode || action.key)),
);
// ── 外部工具 ──
const moreMenuItems = computed(() => [
@@ -156,38 +110,293 @@ const moreMenuItems = computed(() => [
]);
const cellSlotNames = computed(() => Object.keys(slots).filter((name) => name.startsWith("cell-")));
const actionRegistry = createModuleActionRegistry(() => ({
moduleId: dataModule.value?.b_module_id,
profileId: dataModule.value?.b_profile_id,
}));
actionRegistry.register("create", () => addRow());
actionRegistry.register("update", () => saveChanges());
actionRegistry.register("delete", ({ rowKeys } = {}) => deleteSelected(rowKeys));
actionRegistry.register("refresh", () => reload());
actionRegistry.register("reset-columns", () => resetColumns());
actionRegistry.register("save-column-settings", () => saveColumnSettings());
async function runAction(key, payload) {
const customHandler = props.actionHandlers?.[key];
if (typeof customHandler === "function") {
return customHandler(payload, {
module: dataModule.value,
profileId: dataModule.value?.b_profile_id,
reload,
// ── 模块配置加载 ──
function resolveKeyField(module, fields) {
if (identifierRe.test(module.b_keyfield || "")) return module.b_keyfield;
for (const field of fields) {
if (field.b_field === "b_id") return field.b_field;
}
for (const field of fields) {
if (identifierRe.test(field.b_field || "")) return field.b_field;
}
return "";
}
function getDataCode() {
return String(props.dataCode || instanceRouteDataCode || "").trim();
}
async function loadModuleConfig() {
const code = getDataCode();
configLoading.value = true;
configError.value = "";
try {
let runtimeData = null;
if (code) {
const moduleRes = await loadDataApi(
"s_module",
`b_code = N'${escapeSqlText(code)}' AND b_module_type = 'data' AND b_canuse = 1`,
"b_id ASC",
);
runtimeData = moduleRes.data?.[0];
if (!runtimeData) throw new Error(`未找到编码为 ${code} 的启用数据模块`);
// 向上追溯找到所属的 page(可能经过 data_category 层级)
let parentId = requireModuleId(runtimeData.b_parent_id);
let page = null;
const maxDepth = 10;
for (let i = 0; i < maxDepth; i++) {
const res = await loadDataApi(
"s_module",
`b_id = ${parentId} AND b_canuse = 1`,
"b_id ASC",
);
const node = res.data?.[0];
if (!node) break;
if (node.b_module_type === "page") {
page = node;
break;
}
if (node.b_parent_id == null) break;
parentId = requireModuleId(node.b_parent_id);
}
if (!page) {
throw new Error(`数据编码 ${runtimeData.b_code} 没有对应的启用页面`);
}
} else {
const routePath = escapeSqlText(instanceRoutePath);
const pageRes = await loadDataApi(
"s_module",
`b_route = N'${routePath}' AND b_canuse = 1`,
"b_id ASC",
);
const page = pageRes.data?.[0];
if (!page) throw new Error(`未找到路由 ${instanceRoutePath} 对应的启用模块`);
if (page.b_viewtable) {
runtimeData = page;
} else {
// 递归查找 page 下首个有 b_viewtable 的 data 模块(可能经过 data_category 层级)
const queue = [requireModuleId(page.b_id)];
const visited = new Set();
let found = false;
while (queue.length && !found) {
const curId = queue.shift();
if (visited.has(curId)) continue;
visited.add(curId);
const childrenRes = await loadDataApi(
"s_module",
`b_parent_id = ${curId} AND b_canuse = 1`,
"b_xh ASC, b_id ASC",
);
for (const child of childrenRes.data || []) {
if (child.b_module_type === "data" && child.b_viewtable) {
runtimeData = child;
found = true;
break;
}
if (child.b_module_type === "data_category") {
queue.push(requireModuleId(child.b_id));
}
}
}
if (!runtimeData) throw new Error("当前页面没有配置可用的数据模块");
}
}
const dataId = requireModuleId(runtimeData.b_id);
// 列显示配置:普通模式取列表配置,可编辑模式取编辑配置,二者只取其一。
const listTable = props.editable ? "s_module_field_edit" : "s_module_field_list";
const responses = await Promise.all([
loadDataApi("s_module_field", `b_module_id = ${dataId}`, "b_id ASC"),
loadDataApi(listTable, `b_module_id = ${dataId}`, "b_xh ASC, b_id ASC"),
loadDataApi(
"s_module_field_query",
`b_module_id = ${dataId} AND b_canuse = 1`,
"b_xh ASC, b_id ASC",
),
]);
if (getDataCode() !== code) return false;
const fields = [];
const fieldById = new Map();
for (const field of responses[0].data || []) {
if (field.b_canuse !== 1 || !identifierRe.test(field.b_field || "")) continue;
fields.push(field);
fieldById.set(String(field.b_id), field);
}
// 可编辑模式下:记录编辑配置的 readonly/disabled,用于生成只读编辑器。
const editConfigByIdMap = new Map();
for (const item of props.editable ? responses[1].data || [] : []) {
if (item.b_visible !== 1) continue;
if (!fieldById.has(String(item.b_field_id))) continue;
editConfigByIdMap.set(String(item.b_field_id), item);
}
editConfigById.value = editConfigByIdMap;
// 列表配置:普通模式为 s_module_field_list,可编辑模式为 s_module_field_edit。
const lists = [];
for (const item of responses[1].data || []) {
if (item.b_visible !== 1) continue;
if (!fieldById.has(String(item.b_field_id))) continue;
lists.push(item);
}
const queries = [];
for (const config of responses[2].data || []) {
const field = fieldById.get(String(config.b_field_id));
if (!field) continue;
const optionInfo = parseOptions(field);
queries.push({
config,
field,
optionConfig: optionInfo.optionConfig,
options: optionInfo.options,
});
}
const conditions = [];
for (const item of queries) {
const condition = buildQueryCondition(item, defaultQueryValue(item));
if (condition) conditions.push(condition);
}
dataModule.value = runtimeData;
activeFields.value = fields;
activeFieldById.value = fieldById;
listConfig.value = lists;
queryItems.value = queries;
searchConditions.value = conditions;
keyField.value = resolveKeyField(runtimeData, fields);
dirtyRows.value = new Map();
newRowKeys.value = new Set();
return true;
} catch (error) {
if (getDataCode() === code) {
configError.value = error?.message || String(error);
}
return false;
} finally {
if (getDataCode() === code) {
configLoading.value = false;
}
}
}
// ── 数据查询 ──
function resolveOrderBy() {
if (!dataModule.value) throw new Error("数据模块尚未加载");
const configured = String(dataModule.value.b_orderfield || "").trim();
if (configured) return configured;
if (keyField.value) return `${keyField.value} DESC`;
return "b_id DESC";
}
async function fetchData() {
if (!dataModule.value || !dataModule.value.b_viewtable) return;
const currentModule = dataModule.value;
const viewName = currentModule.b_viewtable;
const currentCode = getDataCode();
const requestVersion = ++dataRequestVersion;
const conditions = [];
const fixedCondition = String(props.fixedSearchCondition || "").trim();
if (fixedCondition) conditions.push(`(${fixedCondition})`);
conditions.push(...searchConditions.value);
dataLoading.value = true;
dataError.value = "";
try {
const res = await pageDataApi({
viewName,
orderBy: resolveOrderBy(),
searchColumns: [],
searchCondition: conditions.join(" AND "),
pageNo: pagination.current,
pageSize: pagination.pageSize,
});
if (requestVersion !== dataRequestVersion || dataModule.value !== currentModule) return;
if (getDataCode() !== currentCode) return;
dataSource.value = res.data?.rows || [];
pagination.total = Number(res.data?.total ?? res.data?.row_count) || 0;
} catch (error) {
if (
requestVersion === dataRequestVersion &&
dataModule.value === currentModule &&
getDataCode() === currentCode
) {
dataSource.value = [];
pagination.total = 0;
dataError.value = error?.message || String(error);
}
} finally {
if (
requestVersion === dataRequestVersion &&
dataModule.value === currentModule &&
getDataCode() === currentCode
) {
dataLoading.value = false;
}
}
return actionRegistry.run(key, payload);
}
// ── 公开方法 ──
async function initialize() {
moduleRuntime.reset();
moduleDataSource.reset();
moduleEditor.reset();
const loaded = await moduleRuntime.load();
if (!loaded) return;
moduleDataSource.reset(defaultSearchConditions.value);
await fetchData();
dataRequestVersion++;
dataModule.value = null;
activeFields.value = [];
activeFieldById.value = new Map();
listConfig.value = [];
editConfigById.value = new Map();
queryItems.value = [];
searchConditions.value = [];
keyField.value = "";
dirtyRows.value = new Map();
newRowKeys.value = new Set();
saving.value = false;
dataLoading.value = false;
dataError.value = "";
pagination.current = 1;
dataSource.value = [];
pagination.total = 0;
const loaded = await loadModuleConfig();
if (loaded) await fetchData();
}
function search({ entries = [] }) {
if (loading.value) return;
const conditions = [];
for (const entry of entries) {
const condition = buildQueryCondition(entry.item, entry.value);
if (condition) conditions.push(condition);
}
searchConditions.value = conditions;
pagination.current = 1;
return fetchData();
}
function resetSearch() {
if (loading.value) return;
searchConditions.value = [];
pagination.current = 1;
return fetchData();
}
function changePage(page, pageSize) {
if (loading.value) return;
pagination.current = pageSize === pagination.pageSize ? page : 1;
pagination.pageSize = pageSize;
return fetchData();
}
function reload() {
if (loading.value) return;
return fetchData();
}
async function resetColumns() {
@@ -216,14 +425,15 @@ async function saveColumnSettings() {
columnSaving.value = true;
try {
// 可编辑表格的列来自编辑配置,栏位设置保存到 s_module_field_edit;普通模式保存到列表配置。
const table = props.editable ? "s_module_field_edit" : "s_module_field_list";
await saveObjectApi([
{
table: "s_module_field_list",
table,
key_field: "b_id",
updates,
},
]);
moduleRuntime.invalidate();
const currentById = new Map();
for (const row of listConfig.value) {
@@ -246,14 +456,269 @@ async function saveColumnSettings() {
}
}
async function handleMoreAction({ key }) {
try {
await runAction(key);
if (key === "reset-columns") {
message.success(appStore.t("common.columnSettingsReset", "已恢复栏位设置"));
function resolveRowKey(row) {
if (keyField.value) return row[keyField.value];
return JSON.stringify(row);
}
// ── 可编辑表格:脏行追踪 / 保存 / 批量删除 ──
// 主键与系统审计字段(由后端维护)在可编辑表格中一律只读。
const SYSTEM_FIELDS = ["b_inputdatetime", "b_inputuser_id", "b_updatedatetime", "b_updateuser_id"];
const readonlyFields = computed(() => {
const fields = new Set(SYSTEM_FIELDS);
if (keyField.value) fields.add(keyField.value);
return [...fields];
});
function handleCellChange({ row, field }) {
if (!props.editable || !keyField.value) return;
const rowKey = String(resolveRowKey(row));
const current = dirtyRows.value.get(rowKey);
if (current) {
current.add(field);
} else {
dirtyRows.value.set(rowKey, new Set([field]));
}
emit("dirty-change", { count: dirtyRows.value.size });
}
function resolveSaveTable() {
if (!dataModule.value) throw new Error("数据模块尚未加载");
return String(dataModule.value.b_savetable || dataModule.value.b_viewtable || "");
}
function dirtyUpdateRows() {
if (!keyField.value) return [];
const updates = [];
const rowByKey = new Map(
dataSource.value.map((row) => [String(resolveRowKey(row)), row]),
);
for (const [rowKey, fields] of dirtyRows.value) {
// 未持久化的新增行走 inserts 分支,避免对不存在的记录执行 UPDATE。
if (newRowKeys.value.has(rowKey)) continue;
const row = rowByKey.get(rowKey);
if (!row) continue;
const update = { [keyField.value]: row[keyField.value] };
for (const field of fields) {
if (field === keyField.value) continue;
update[field] = row[field];
}
updates.push(update);
}
return updates;
}
// 未持久化的新增行:仅收集有实际修改的行,作为 inserts 提交。
function dirtyInsertRows() {
if (!keyField.value) return [];
const rowByKey = new Map(
dataSource.value.map((row) => [String(resolveRowKey(row)), row]),
);
const inserts = [];
for (const rowKey of newRowKeys.value) {
const fields = dirtyRows.value.get(rowKey);
const row = rowByKey.get(rowKey);
if (!fields || !row) continue;
const insert = { [keyField.value]: row[keyField.value] };
for (const field of fields) {
if (field === keyField.value) continue;
insert[field] = row[field];
}
inserts.push(insert);
}
return inserts;
}
// 找到第一个可编辑的列字段(按编辑配置顺序,跳过只读列、主键与 checkbox),
// 用于新增行后定位光标聚焦的单元格。
function firstEditableField() {
const readonly = new Set(readonlyFields.value);
for (const config of listConfig.value) {
const field = activeFieldById.value.get(String(config.b_field_id));
if (!field) continue;
if (readonly.has(field.b_field)) continue;
if (field.b_type === "checkbox") continue;
return field.b_field;
}
return null;
}
// 可编辑模式下新增一行:生成雪花主键,追加到表尾并让第一个可编辑单元格进入编辑态并聚焦。
async function addRow() {
if (!props.editable || saving.value || loading.value) return;
if (!keyField.value) {
message.error("该模块未配置主键,无法新增");
return;
}
const idRes = await nextIdApi();
const newId = String(idRes.data);
const newRow = { [keyField.value]: newId };
// 排序字段取当前最大值 + 10;checkbox 字段默认 0。
if (props.orderField && identifierRe.test(props.orderField)) {
let maxXh = 0;
for (const row of dataSource.value) {
maxXh = Math.max(maxXh, Number(row[props.orderField]) || 0);
}
newRow[props.orderField] = maxXh + 10;
}
for (const field of activeFields.value) {
if (field.b_field === keyField.value || field.b_field === props.orderField) continue;
if (newRow[field.b_field] !== undefined) continue;
if (field.b_type === "checkbox") newRow[field.b_field] = 0;
}
dataSource.value = [...dataSource.value, newRow];
newRowKeys.value = new Set([...newRowKeys.value, newId]);
await nextTick();
// 聚焦第一个可编辑字段,让用户可直接输入。
tableRef.value?.editRow?.(newId, firstEditableField());
}
async function saveChanges() {
if (saving.value) return;
const table = resolveSaveTable();
if (!table) {
message.error("该模块未配置保存表,无法保存");
return;
}
const updates = dirtyUpdateRows();
const inserts = dirtyInsertRows();
if (!updates.length && !inserts.length) {
message.info("没有需要保存的修改");
return;
}
saving.value = true;
try {
const operation = { table, key_field: keyField.value };
if (updates.length) operation.updates = updates;
if (inserts.length) operation.inserts = inserts;
await saveObjectApi([operation]);
// 已提交的新增行不再视为未持久化。
const insertedSet = new Set(inserts.map((row) => String(row[keyField.value])));
newRowKeys.value = new Set(
[...newRowKeys.value].filter((key) => !insertedSet.has(key)),
);
dirtyRows.value = new Map();
// 保存成功后让正在编辑的行退出编辑态。
tableRef.value?.clearEditing?.();
message.success("保存成功");
emit("dirty-change", { count: 0 });
await fetchData();
} catch (error) {
message.error(error?.message || String(error));
message.error("保存失败:" + (error?.message || error));
} finally {
saving.value = false;
}
}
async function deleteSelected(rowKeys) {
if (saving.value || !rowKeys?.length) return;
const table = resolveSaveTable();
if (!table) {
message.error("该模块未配置保存表,无法删除");
return;
}
if (!keyField.value) {
message.error("该模块未配置主键,无法删除");
return;
}
// 未持久化的新增行直接本地移除,不调删除接口。
const newSet = new Set(newRowKeys.value);
const pendingKeys = rowKeys.filter((key) => newSet.has(String(key)));
const persistedKeys = rowKeys.filter((key) => !newSet.has(String(key)));
if (pendingKeys.length) {
const pendingSet = new Set(pendingKeys.map((key) => String(key)));
dataSource.value = dataSource.value.filter(
(row) => !pendingSet.has(String(resolveRowKey(row))),
);
const remaining = new Map();
for (const [rowKey, fields] of dirtyRows.value) {
if (!pendingSet.has(rowKey)) remaining.set(rowKey, fields);
}
dirtyRows.value = remaining;
newRowKeys.value = new Set(
[...newRowKeys.value].filter((key) => !pendingSet.has(key)),
);
tableRef.value?.clearEditing?.();
emit("dirty-change", { count: dirtyRows.value.size });
}
if (!persistedKeys.length) {
message.success(`已删除 ${rowKeys.length} 条记录`);
return;
}
saving.value = true;
try {
await saveObjectApi([
{
table,
key_field: keyField.value,
deletes: persistedKeys.map((key) => ({ [keyField.value]: key })),
},
]);
// 清除被删行的脏记录
const deletedSet = new Set(persistedKeys.map((key) => String(key)));
const remaining = new Map();
for (const [rowKey, fields] of dirtyRows.value) {
if (!deletedSet.has(rowKey)) remaining.set(rowKey, fields);
}
dirtyRows.value = remaining;
// 删除成功后让正在编辑的行退出编辑态。
tableRef.value?.clearEditing?.();
message.success(`已删除 ${rowKeys.length} 条记录`);
await fetchData();
} catch (error) {
message.error("删除失败:" + (error?.message || error));
} finally {
saving.value = false;
}
}
// 行拖拽排序:回灌排序结果,按 orderField 重新编号并标记为脏,随保存一起提交。
function handleRowOrderChange(orderedRows) {
if (!props.rowDraggable || !props.orderField) return;
if (!identifierRe.test(props.orderField)) return;
const rowByOld = new Map(
dataSource.value.map((row) => [String(resolveRowKey(row)), row]),
);
const dirtyFieldsByKey = new Map();
const nextRows = [];
let order = 10;
for (const row of orderedRows) {
const key = String(resolveRowKey(row));
const oldRow = rowByOld.get(key);
const oldValue = oldRow ? Number(oldRow[props.orderField]) || 0 : 0;
const copy = { ...row, [props.orderField]: order };
nextRows.push(copy);
if (oldValue !== order) {
const fields = dirtyFieldsByKey.get(key) || new Set();
fields.add(props.orderField);
dirtyFieldsByKey.set(key, fields);
}
order += 10;
}
dataSource.value = nextRows;
for (const [key, fields] of dirtyFieldsByKey) {
const current = dirtyRows.value.get(key);
if (current) {
fields.forEach((field) => current.add(field));
} else {
dirtyRows.value.set(key, fields);
}
}
emit("dirty-change", { count: dirtyRows.value.size });
}
async function handleMoreAction({ key }) {
if (key === "refresh") {
await reload();
return;
}
if (key === "reset-columns") {
await resetColumns();
message.success(appStore.t("common.columnSettingsReset", "已恢复栏位设置"));
return;
}
if (key === "save-column-settings") {
await saveColumnSettings();
}
}
@@ -270,7 +735,7 @@ onActivated(async () => {
tableRef.value?.refreshLayout?.();
});
watch(() => [props.moduleId, props.profileId, props.pageNodeId], initialize);
watch(() => props.dataCode, initialize);
watch(
() => props.fixedSearchCondition,
@@ -299,9 +764,6 @@ defineExpose({
deleteSelected,
addRow,
dirtyRows,
groups,
actions,
runAction,
});
</script>
@@ -355,7 +817,7 @@ defineExpose({
:data-source="dataSource"
:row-key="resolveRowKey"
:column-extensions="columnExtensions"
:row-actions="permittedRowActions"
:row-actions="rowActions"
:row-selection="rowSelection"
:row-class-name="rowClassName"
:expandable="expandable"
@@ -1,9 +1,9 @@
<script setup>
import { computed, h, ref, shallowRef, useSlots, watch } from "vue";
import { Checkbox } from "antdv-next";
import FmsTable from "@/components/fms-table/FmsTable.vue";
import { useAppStore } from "@/stores/app";
import { createFieldEditor, formatFieldValue } from "@/module-runtime/fieldRegistry";
import { parseOptions } from "./moduleListUtils";
import { formatDateValue, parseOptions } from "./moduleListUtils";
const props = defineProps({
fields: { type: Array, default: () => [] },
@@ -48,24 +48,49 @@ const expansionOptions = computed(() =>
);
const expandConfig = computed(() => ({ height: Number(expansionOptions.value.height) || 180 }));
function formatCellValue(value, field, optionInfo) {
if (value === null || value === undefined || value === "") return "";
if (field.b_type === "date") return formatDateValue(value, false);
if (field.b_type === "datetime") return formatDateValue(value, true);
if (field.b_type === "checkbox") {
return h(Checkbox, { checked: value === true || Number(value) === 1 });
}
if (field.b_type === "select") {
const option = optionInfo.options.find((item) => String(item.value) === String(value));
return option?.label ?? String(value);
}
if (field.b_type === "money") {
const number = Number(value);
if (!Number.isFinite(number)) return String(value);
const precision = Number.isInteger(optionInfo.optionConfig.precision)
? optionInfo.optionConfig.precision
: 2;
return number.toLocaleString("zh-CN", {
minimumFractionDigits: precision,
maximumFractionDigits: precision,
});
}
return String(value);
}
function resolveExtension(fieldName) {
return props.columnExtensions?.[fieldName] || {};
}
// 根据字段类型与编辑配置生成行内编辑器配置。
function buildEditor(field, config) {
const optionInfo = parseOptions(field);
const editor = createFieldEditor(field, config, optionInfo, {
moduleId: field.b_module_id,
profileId: config?.b_profile_id,
});
if (
props.readonlyFields.includes(field.b_field) ||
(config && (Number(config.b_readonly) === 1 || Number(config.b_disabled) === 1))
) {
return { ...editor, disabled: true };
return { type: "input", disabled: true };
}
return editor;
if (field.b_type === "checkbox") return { type: "checkbox" };
if (field.b_type === "select") {
const { options } = parseOptions(field);
return { type: "select", options };
}
return { type: "input" };
}
function createRuntimeColumn(config, field) {
@@ -73,7 +98,9 @@ function createRuntimeColumn(config, field) {
const extension = resolveExtension(field.b_field);
const { formatter, clickable, cellClassName, onClick: _onClick, ...columnProps } = extension;
const slot = slots[`cell-${field.b_field}`];
const editConfig = props.editable ? props.editConfigById.get(String(config.b_field_id)) : null;
const editConfig = props.editable
? props.editConfigById.get(String(config.b_field_id))
: null;
const column = {
title: appStore.t(field.b_i18n, field.b_name || field.b_field),
dataIndex: field.b_field,
@@ -92,10 +119,7 @@ function createRuntimeColumn(config, field) {
column.customCell = ({ row, cellValue, rowIndex, colIndex }) => {
const formattedValue = formatter
? formatter({ value: cellValue, record: row, field, config })
: formatFieldValue(cellValue, field, optionInfo, {
moduleId: field.b_module_id,
profileId: config.b_profile_id,
});
: formatCellValue(cellValue, field, optionInfo);
const cellClass =
typeof cellClassName === "function"
? cellClassName({ value: cellValue, record: row, field, config })
@@ -328,9 +352,7 @@ defineExpose({
@row-click="handleRowClick"
@toggle-row-expand="handleToggleExpand"
@change="handleCellChange"
@row-order-change="
(rows, sourceIndex, targetIndex) => emit('row-order-change', rows, sourceIndex, targetIndex)
"
@row-order-change="(rows, sourceIndex, targetIndex) => emit('row-order-change', rows, sourceIndex, targetIndex)"
>
<template v-if="expansionEnabled" #expand="{ row, col }">
<slot name="expanded-row" :record="row" :column="col" />
@@ -1,5 +1,5 @@
import { h, toRaw } from "vue";
import { Input, InputNumber, Select } from "antdv-next";
import { Input, Select } from "antdv-next";
const stop = (event) => event.stopPropagation();
const inputDrafts = new WeakMap();
@@ -44,15 +44,13 @@ function resolveOptions(editor) {
function displayValue(row, col, editor) {
const value = row[col.dataIndex];
const text = editor.format
? editor.format(value)
: editor.type === "select"
const text =
editor.type === "select"
? (resolveOptions(editor).find((item) => String(item.value) === String(value))?.label ??
value)
: value;
const content = text ?? "";
const title = typeof content === "string" || typeof content === "number" ? String(content) : "";
return h("span", { class: "fms-cell-display", title }, content);
return h("span", { class: "fms-cell-display", title: String(content) }, content);
}
function notifyChange(ctx, editor, row, col, value) {
@@ -88,15 +86,9 @@ function createFocusHandler(ctx, row, col, selectText = false) {
function createInput(row, col, editor, ctx) {
const focus = createFocusHandler(ctx, row, col, true);
let currentValue = getInputDraft(row, col.dataIndex) ?? "";
if (editor.inputType === "datetime-local") {
currentValue = String(currentValue).replace(" ", "T").slice(0, 16);
} else if (editor.inputType === "date") {
currentValue = String(currentValue).slice(0, 10);
}
return h(Input, {
type: editor.inputType || "text",
value: currentValue,
value: getInputDraft(row, col.dataIndex) ?? "",
size: "small",
class: "fms-cell-editor fms-cell-editor-root",
"aria-label": col.title || col.dataIndex,
@@ -104,10 +96,7 @@ function createInput(row, col, editor, ctx) {
setInputDraft(row, col.dataIndex, value);
},
onBlur: (event) => {
const value =
editor.inputType === "datetime-local"
? event.target.value.replace("T", " ")
: event.target.value;
const value = event.target.value;
commitInputDraft(row, col.dataIndex, value);
notifyChange(ctx, editor, row, col, value);
},
@@ -127,48 +116,6 @@ function createInput(row, col, editor, ctx) {
});
}
function createTextarea(row, col, editor, ctx) {
const focus = createFocusHandler(ctx, row, col, true);
return h(Input.TextArea, {
value: getInputDraft(row, col.dataIndex) ?? "",
autoSize: { minRows: 1, maxRows: 4 },
size: "small",
class: "fms-cell-editor fms-cell-editor-root",
"aria-label": col.title || col.dataIndex,
"onUpdate:value": (value) => setInputDraft(row, col.dataIndex, value),
onBlur: (event) => {
const value = event.target.value;
commitInputDraft(row, col.dataIndex, value);
notifyChange(ctx, editor, row, col, value);
},
onClick: stop,
onVnodeMounted: focus,
onVnodeUpdated: focus,
onVnodeBeforeUnmount: () => preserveInputDraft(row, col.dataIndex),
});
}
function createNumber(row, col, editor, ctx) {
const focus = createFocusHandler(ctx, row, col, true);
return h(InputNumber, {
value: row[col.dataIndex] ?? null,
precision: editor.precision,
stringMode: editor.stringMode ?? true,
size: "small",
class: "fms-cell-editor fms-cell-editor-root",
"aria-label": col.title || col.dataIndex,
style: { width: "100%" },
"onUpdate:value": (value) => {
const resolved = value ?? null;
row[col.dataIndex] = resolved;
notifyChange(ctx, editor, row, col, resolved);
},
onClick: stop,
onVnodeMounted: focus,
onVnodeUpdated: focus,
});
}
function createSelect(row, col, editor, ctx) {
const options = resolveOptions(editor);
const focus = createFocusHandler(ctx, row, col);
@@ -193,7 +140,7 @@ function createSelect(row, col, editor, ctx) {
}
function createCheckbox(row, col, editor, ctx) {
const checked = row[col.dataIndex] === true || Number(row[col.dataIndex]) === 1;
const checked = row[col.dataIndex] === 1;
const focus = ctx.isEditing(row) ? createFocusHandler(ctx, row, col) : undefined;
const update = (value) => {
row[col.dataIndex] = value;
@@ -251,8 +198,6 @@ export function createEditableCell(col, ctx) {
return displayValue(row, col, editor);
}
if (editor.type === "select") return createSelect(row, col, editor, ctx);
if (editor.type === "textarea") return createTextarea(row, col, editor, ctx);
if (editor.type === "number") return createNumber(row, col, editor, ctx);
return createInput(row, col, editor, ctx);
};
}