立项配置页初步优化

This commit is contained in:
Cheng Zhou
2026-02-24 16:53:22 +08:00
parent 0693f09b1d
commit 8f3f717e48
124 changed files with 12519 additions and 2954 deletions
+155
View File
@@ -0,0 +1,155 @@
import type { SetupConfigDraft } from "../types/setupConfig";
export type SetupDiffRow = {
moduleLabel: string;
path: string;
changeType: "新增" | "删除" | "修改";
localValue: string;
serverValue: string;
};
export type SetupModuleLabel = {
key: keyof SetupConfigDraft;
label: string;
};
const setupFieldLabelMap: Record<keyof SetupConfigDraft, Record<string, string>> = {
projectMilestones: {
id: "ID",
name: "里程碑",
planDate: "计划日期",
owner: "负责人",
remark: "备注",
status: "状态",
},
enrollmentPlan: {
totalTarget: "计划总入组例数",
startDate: "计划开始日期",
endDate: "计划结束日期",
monthlyGoalNote: "月度目标说明",
stageBreakdown: "分阶段计划",
},
siteMilestones: {
id: "ID",
milestone: "里程碑",
planDate: "计划日期",
owner: "负责人",
remark: "备注",
},
siteEnrollmentPlans: {
id: "ID",
siteId: "中心ID",
siteName: "中心名称",
target: "计划例数",
startDate: "启动日期",
endDate: "完成日期",
note: "备注",
stageBreakdown: "分阶段计划",
},
monitoringStrategies: {
id: "ID",
strategyType: "监查类型",
detail: "策略详情",
frequency: "监查次数",
updatedAt: "更新时间",
enabled: "是否启用",
},
centerConfirm: {
id: "ID",
siteId: "中心ID",
siteName: "中心名称",
confirmer: "确认人",
confirmStatus: "确认状态",
confirmDate: "确认日期",
note: "备注",
},
};
const formatReadablePath = (moduleKey: keyof SetupConfigDraft, path: string): string => {
const normalized = path.startsWith(`${moduleKey}.`) ? path.slice(moduleKey.length + 1) : path;
if (!normalized || normalized === moduleKey) return "根节点";
const parts = normalized.split(".");
const labels = parts.map((part, idx) => {
const match = part.match(/^([^\[]+)\[(\d+)\]$/);
if (match) {
const fieldKey = match[1];
const rowNo = Number(match[2]) + 1;
const label = setupFieldLabelMap[moduleKey][fieldKey] || fieldKey;
return idx === 0 ? `${label}${rowNo}` : `${rowNo}`;
}
return setupFieldLabelMap[moduleKey][part] || part;
});
return labels.join(" / ");
};
export const serializeDiffValue = (value: unknown): string => {
if (value === null || value === undefined) return "-";
if (typeof value === "string") return value || '""';
if (typeof value === "number" || typeof value === "boolean") return String(value);
return JSON.stringify(value);
};
const isPlainObject = (value: unknown): value is Record<string, unknown> =>
Boolean(value) && typeof value === "object" && !Array.isArray(value);
const collectDiffRows = (
moduleLabel: string,
localValue: unknown,
serverValue: unknown,
path: string,
rows: SetupDiffRow[]
) => {
if (Array.isArray(localValue) || Array.isArray(serverValue)) {
const localArr = Array.isArray(localValue) ? localValue : [];
const serverArr = Array.isArray(serverValue) ? serverValue : [];
const maxLen = Math.max(localArr.length, serverArr.length);
for (let i = 0; i < maxLen; i += 1) {
collectDiffRows(moduleLabel, localArr[i], serverArr[i], `${path}[${i}]`, rows);
}
return;
}
if (isPlainObject(localValue) || isPlainObject(serverValue)) {
const localObj = isPlainObject(localValue) ? localValue : {};
const serverObj = isPlainObject(serverValue) ? serverValue : {};
const keys = Array.from(new Set([...Object.keys(localObj), ...Object.keys(serverObj)]));
keys.forEach((key) => {
const childPath = path ? `${path}.${key}` : key;
collectDiffRows(moduleLabel, localObj[key], serverObj[key], childPath, rows);
});
return;
}
if (localValue === serverValue) return;
const hasLocal = localValue !== undefined;
const hasServer = serverValue !== undefined;
const changeType: SetupDiffRow["changeType"] = hasLocal && hasServer ? "修改" : hasLocal ? "新增" : "删除";
rows.push({
moduleLabel,
path: path || "(root)",
changeType,
localValue: serializeDiffValue(localValue),
serverValue: serializeDiffValue(serverValue),
});
};
export const buildSetupReadableDiffRows = (
localDraft: SetupConfigDraft | null,
serverDraft: SetupConfigDraft | null,
moduleLabels: SetupModuleLabel[],
limit = 300
): SetupDiffRow[] => {
if (!localDraft || !serverDraft) return [];
const rows: SetupDiffRow[] = [];
moduleLabels.forEach((item) => {
collectDiffRows(item.label, localDraft[item.key], serverDraft[item.key], String(item.key), rows);
});
return rows.slice(0, limit).map((row) => {
const moduleKey = moduleLabels.find((item) => item.label === row.moduleLabel)?.key;
if (!moduleKey) return row;
return {
...row,
path: formatReadablePath(moduleKey, row.path),
};
});
};
+33
View File
@@ -0,0 +1,33 @@
export type SetupValidationError = {
field: string;
message: string;
};
export type ParsedFieldPath = {
section: string;
index: number;
field: string;
};
export const parseFieldPath = (path: string): ParsedFieldPath | null => {
const matched = path.match(/^([a-zA-Z0-9_]+)\[(\d+)\]\.([a-zA-Z0-9_]+)$/);
if (!matched) return null;
return {
section: matched[1],
index: Number(matched[2]),
field: matched[3],
};
};
export const groupErrorsBySection = (errors: SetupValidationError[]): Record<string, SetupValidationError[]> => {
const grouped: Record<string, SetupValidationError[]> = {};
errors.forEach((error) => {
const parsed = parseFieldPath(error.field);
const section = parsed?.section || error.field.split(".")[0] || "global";
if (!grouped[section]) {
grouped[section] = [];
}
grouped[section].push(error);
});
return grouped;
};