项目里程碑初步优化
This commit is contained in:
@@ -37,11 +37,7 @@ export const displayUser = (
|
||||
opts?: { users?: Record<string, string>; members?: Record<string, string> }
|
||||
) => {
|
||||
if (!userId) return displayFallback;
|
||||
const name =
|
||||
opts?.users?.[userId] ||
|
||||
opts?.members?.[userId] ||
|
||||
// 兼容成员数据的 user_id 为 key 的情况
|
||||
(opts?.members && Object.values(opts.members).find((_, k) => k === userId));
|
||||
const name = opts?.users?.[userId] || opts?.members?.[userId];
|
||||
return (name as string) || displayFallback;
|
||||
};
|
||||
|
||||
|
||||
@@ -13,11 +13,32 @@ export type SetupModuleLabel = {
|
||||
label: string;
|
||||
};
|
||||
|
||||
type ParsedEnrollmentPayload = {
|
||||
cycle?: string;
|
||||
valuesByCycle?: {
|
||||
month?: Record<string, unknown>;
|
||||
quarter?: Record<string, unknown>;
|
||||
};
|
||||
};
|
||||
|
||||
const statusLabelMap: Record<string, string> = {
|
||||
ACTIVE: "进行中",
|
||||
CLOSED: "已关闭",
|
||||
DRAFT: "草稿",
|
||||
DONE: "已完成",
|
||||
BLOCKED: "阻塞/延期",
|
||||
TODO: "未开始",
|
||||
PENDING: "待处理",
|
||||
};
|
||||
|
||||
const setupFieldLabelMap: Record<keyof SetupConfigDraft, Record<string, string>> = {
|
||||
projectMilestones: {
|
||||
id: "ID",
|
||||
name: "里程碑",
|
||||
planDate: "计划日期",
|
||||
startDate: "开始日期",
|
||||
endDate: "结束日期",
|
||||
durationDays: "耗时(天)",
|
||||
owner: "负责人",
|
||||
remark: "备注",
|
||||
status: "状态",
|
||||
@@ -35,6 +56,7 @@ const setupFieldLabelMap: Record<keyof SetupConfigDraft, Record<string, string>>
|
||||
planDate: "计划日期",
|
||||
owner: "负责人",
|
||||
remark: "备注",
|
||||
status: "状态",
|
||||
},
|
||||
siteEnrollmentPlans: {
|
||||
id: "ID",
|
||||
@@ -65,46 +87,171 @@ const setupFieldLabelMap: Record<keyof SetupConfigDraft, Record<string, string>>
|
||||
},
|
||||
};
|
||||
|
||||
const globalReadableFieldMap: Record<string, string> = {
|
||||
type: "配置类型",
|
||||
valuesByCycle: "分阶段分配明细",
|
||||
month: "按月分配",
|
||||
quarter: "按季度分配",
|
||||
cycle: "统计口径",
|
||||
value: "配置值",
|
||||
};
|
||||
|
||||
const getArrayRowIdentity = (moduleKey: keyof SetupConfigDraft, value: unknown): string => {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) return "";
|
||||
const row = value as Record<string, unknown>;
|
||||
if (moduleKey === "projectMilestones") return String(row.name || "").trim();
|
||||
if (moduleKey === "siteMilestones") return String(row.milestone || "").trim();
|
||||
if (moduleKey === "siteEnrollmentPlans") return String(row.siteName || row.siteId || "").trim();
|
||||
if (moduleKey === "monitoringStrategies") return String(row.strategyType || "").trim();
|
||||
if (moduleKey === "centerConfirm") return String(row.siteName || row.siteId || "").trim();
|
||||
return "";
|
||||
};
|
||||
|
||||
const formatReadablePath = (moduleKey: keyof SetupConfigDraft, path: string): string => {
|
||||
const normalized = path.startsWith(`${moduleKey}.`) ? path.slice(moduleKey.length + 1) : path;
|
||||
let normalized = path;
|
||||
if (path === moduleKey) {
|
||||
normalized = "";
|
||||
} else if (path.startsWith(`${moduleKey}.`)) {
|
||||
normalized = path.slice(moduleKey.length + 1);
|
||||
} else if (path.startsWith(`${moduleKey}[`)) {
|
||||
normalized = path.slice(moduleKey.length);
|
||||
if (normalized.startsWith("[")) {
|
||||
normalized = `items${normalized}`;
|
||||
}
|
||||
}
|
||||
if (!normalized || normalized === moduleKey) return "根节点";
|
||||
const parts = normalized.split(".");
|
||||
const labels = parts.map((part, idx) => {
|
||||
const match = part.match(/^([^\[]+)\[(\d+)\]$/);
|
||||
const match = part.match(/^([^\[]+)\[(\d+)(?:\|([^\]]+))?\]$/);
|
||||
if (match) {
|
||||
const fieldKey = match[1];
|
||||
const rowNo = Number(match[2]) + 1;
|
||||
const rowIdentity = String(match[3] || "").trim();
|
||||
if (idx === 0 && (fieldKey === String(moduleKey) || fieldKey === "items")) {
|
||||
return rowIdentity || `第${rowNo}行`;
|
||||
}
|
||||
const label = setupFieldLabelMap[moduleKey][fieldKey] || fieldKey;
|
||||
return idx === 0 ? `${label} 第${rowNo}行` : `第${rowNo}行`;
|
||||
if (idx === 0) {
|
||||
return rowIdentity ? `${label} / ${rowIdentity}` : `${label} 第${rowNo}行`;
|
||||
}
|
||||
return rowIdentity ? rowIdentity : `第${rowNo}行`;
|
||||
}
|
||||
return setupFieldLabelMap[moduleKey][part] || part;
|
||||
return setupFieldLabelMap[moduleKey][part] || globalReadableFieldMap[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 summarizeEnrollmentValues = (raw: Record<string, unknown> | undefined): { count: number; total: number } => {
|
||||
if (!raw || typeof raw !== "object") return { count: 0, total: 0 };
|
||||
let count = 0;
|
||||
let total = 0;
|
||||
Object.values(raw).forEach((v) => {
|
||||
const n = Number(v);
|
||||
if (!Number.isFinite(n) || n <= 0) return;
|
||||
count += 1;
|
||||
total += Math.floor(n);
|
||||
});
|
||||
return { count, total };
|
||||
};
|
||||
|
||||
const summarizeStructuredPayload = (raw: unknown): string | null => {
|
||||
try {
|
||||
const parsed =
|
||||
typeof raw === "string"
|
||||
? (JSON.parse(raw) as Record<string, unknown>)
|
||||
: ((raw as Record<string, unknown>) || {});
|
||||
const payloadType = String(parsed?.type || "").trim().toLowerCase();
|
||||
const enrollmentTypes = new Set(["enrollment_plan_v2", "site_enrollment_plan_v2"]);
|
||||
if (!enrollmentTypes.has(payloadType)) return null;
|
||||
|
||||
const p = parsed as ParsedEnrollmentPayload;
|
||||
const month = summarizeEnrollmentValues(p.valuesByCycle?.month);
|
||||
const quarter = summarizeEnrollmentValues(p.valuesByCycle?.quarter);
|
||||
const parts = [
|
||||
`按月:${month.count}期,合计${month.total}人`,
|
||||
`按季度:${quarter.count}期,合计${quarter.total}人`,
|
||||
];
|
||||
if (payloadType === "enrollment_plan_v2") {
|
||||
const cycle = String(p.cycle || "").trim();
|
||||
const cycleLabel = cycle === "quarter" ? "当前口径:季度" : "当前口径:月";
|
||||
return `分阶段计划(${cycleLabel})${parts.join(";")}`;
|
||||
}
|
||||
return `中心分阶段计划:${parts.join(";")}`;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
if (value === null || value === undefined) return "未填写";
|
||||
if (typeof value === "boolean") return value ? "是" : "否";
|
||||
if (typeof value === "number") return String(value);
|
||||
if (typeof value === "string") {
|
||||
const text = value.trim();
|
||||
if (!text) return "未填写";
|
||||
const statusLabel = statusLabelMap[text.toUpperCase()];
|
||||
if (statusLabel) return statusLabel;
|
||||
if ((text.startsWith("{") && text.endsWith("}")) || (text.startsWith("[") && text.endsWith("]"))) {
|
||||
const summary = summarizeStructuredPayload(text);
|
||||
if (summary) return summary;
|
||||
try {
|
||||
const parsed = JSON.parse(text);
|
||||
if (Array.isArray(parsed)) return `已配置列表(${parsed.length}项)`;
|
||||
if (parsed && typeof parsed === "object") return "已配置内容";
|
||||
} catch {
|
||||
// fall through to raw text
|
||||
}
|
||||
}
|
||||
const normalizedText = text.toLowerCase();
|
||||
if (normalizedText.includes("site_enrollment_plan_v2")) return "中心分阶段计划(已配置)";
|
||||
if (normalizedText.includes("enrollment_plan_v2")) return "分阶段计划(已配置)";
|
||||
return text.length > 80 ? `${text.slice(0, 80)}...` : text;
|
||||
}
|
||||
try {
|
||||
const structuredSummary = isPlainObject(value) ? summarizeStructuredPayload(value) : null;
|
||||
if (structuredSummary) return structuredSummary;
|
||||
if (Array.isArray(value)) return `已配置列表(${value.length}项)`;
|
||||
if (value && typeof value === "object") return "已配置内容";
|
||||
return JSON.stringify(value);
|
||||
} catch {
|
||||
return String(value);
|
||||
}
|
||||
};
|
||||
|
||||
const isPlainObject = (value: unknown): value is Record<string, unknown> =>
|
||||
Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
||||
|
||||
const collectDiffRows = (
|
||||
moduleKey: keyof SetupConfigDraft,
|
||||
moduleLabel: string,
|
||||
localValue: unknown,
|
||||
serverValue: unknown,
|
||||
path: string,
|
||||
rows: SetupDiffRow[]
|
||||
) => {
|
||||
const isStructuredBusinessLeaf = path.endsWith(".stageBreakdown") || path.endsWith(".owner");
|
||||
if (isStructuredBusinessLeaf) {
|
||||
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),
|
||||
});
|
||||
return;
|
||||
}
|
||||
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);
|
||||
const rowIdentity = getArrayRowIdentity(moduleKey, localArr[i] ?? serverArr[i]);
|
||||
const indexLabel = rowIdentity ? `${i}|${rowIdentity}` : `${i}`;
|
||||
collectDiffRows(moduleKey, moduleLabel, localArr[i], serverArr[i], `${path}[${indexLabel}]`, rows);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -115,7 +262,7 @@ const collectDiffRows = (
|
||||
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);
|
||||
collectDiffRows(moduleKey, moduleLabel, localObj[key], serverObj[key], childPath, rows);
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -142,14 +289,24 @@ export const buildSetupReadableDiffRows = (
|
||||
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),
|
||||
};
|
||||
collectDiffRows(item.key, 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),
|
||||
};
|
||||
})
|
||||
.filter((row) => {
|
||||
// 屏蔽技术噪音字段,优先展示业务可理解差异
|
||||
const p = row.path.trim();
|
||||
if (p === "ID" || p.endsWith(" / ID")) return false;
|
||||
if (p === "中心ID" || p.endsWith(" / 中心ID")) return false;
|
||||
if (p.includes("配置类型") || p.includes("分阶段分配明细")) return false;
|
||||
return true;
|
||||
});
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user