立项配置页初步优化
This commit is contained in:
@@ -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),
|
||||
};
|
||||
});
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
Reference in New Issue
Block a user