项目里程碑初步优化

This commit is contained in:
Cheng Zhou
2026-02-27 09:06:06 +08:00
parent 8f3f717e48
commit fd7e3fc948
47 changed files with 2029 additions and 783 deletions
+184 -16
View File
@@ -25,27 +25,184 @@ export interface AuditEvent {
timestamp: string;
}
const buildDiffText = (before?: Record<string, any>, after?: Record<string, any>) => {
if (!before || !after) return [];
const lines: string[] = [];
Object.keys({ ...before, ...after }).forEach((key) => {
const prev = before[key];
const next = after[key];
if (prev === next) return;
const entityTypeLabelMap: Record<string, string> = {
study_setup_config: "立项配置",
study: "项目",
subject: "参与者",
visit: "访视",
ae: "不良事件",
finance_contract: "费用合同",
finance_special: "费用特殊费用",
contract_fee: "合同费用条目",
contract_fee_payment: "合同费用回款",
special_expense: "特殊费用条目",
drug_shipment: "药品流向",
startup_feasibility: "立项可行性",
startup_ethics: "立项伦理",
startup_kickoff: "启动会",
training_authorization: "培训授权",
knowledge_note: "注意事项",
subject_history: "参与者历史",
faq_category: "医学咨询分类",
faq_item: "医学咨询问题",
faq_reply: "医学咨询回复",
DOCUMENT: "文档",
DOCUMENT_VERSION: "文档版本",
DISTRIBUTION: "文档分发",
ACKNOWLEDGEMENT: "文档签收",
};
const setupModuleLabelMap: Record<string, string> = {
projectMilestones: "项目里程碑",
enrollmentPlan: "项目入组计划",
siteMilestones: "中心里程碑",
siteEnrollmentPlans: "中心入组计划",
monitoringStrategies: "监查策略",
centerConfirm: "中心确认",
};
const toReadableEntityType = (value?: string): string => {
const key = String(value || "").trim();
if (!key) return TEXT.audit.targetFallback;
return entityTypeLabelMap[key] || key;
};
const normalizeSetupModuleText = (raw: string): string => {
const text = String(raw || "").trim();
if (!text) return "无";
return text
.split(/[,]/)
.map((item) => item.trim())
.filter(Boolean)
.map((item) => setupModuleLabelMap[item] || item)
.join("、") || "无";
};
const normalizeLegacyDetailLine = (line: string): string => {
const text = String(line || "").trim();
if (!text) return "";
const projectionStatusMatch = text.match(/^projection_status=(.+)$/i);
if (projectionStatusMatch) {
const status = projectionStatusMatch[1].trim().toLowerCase();
const statusLabel = status === "success" ? "成功" : status === "failed" ? "失败" : status;
return `发布同步结果:${statusLabel}`;
}
const studyUpdatedMatch = text.match(/^study_updated=(.+)$/i);
if (studyUpdatedMatch) {
const rawValue = studyUpdatedMatch[1].trim().toLowerCase();
return `已同步项目主信息:${rawValue === "true" ? "是" : rawValue === "false" ? "否" : rawValue}`;
}
const siteUpdatedMatch = text.match(/^site_updated=(\d+)$/i);
if (siteUpdatedMatch) return `已同步中心数:${siteUpdatedMatch[1]}`;
const siteSkippedMatch = text.match(/^site_skipped=(\d+)$/i);
if (siteSkippedMatch) return `未同步中心数:${siteSkippedMatch[1]}`;
const warningsMatch = text.match(/^warnings=(.*)$/i);
if (warningsMatch) {
const value = warningsMatch[1].trim();
return `同步告警:${value === "-" ? "无" : value}`;
}
const skippedReasonMatch = text.match(/^skipped_reason_counts=(.*)$/i);
if (skippedReasonMatch) {
const value = skippedReasonMatch[1].trim();
return `未同步原因统计:${value === "-" ? "无" : value}`;
}
const setupModuleMatch = text.match(/^变更模块[:]\s*(.*)$/);
if (setupModuleMatch) {
return `变更模块:${normalizeSetupModuleText(setupModuleMatch[1])}`;
}
const detailMatch = text.match(/^变更明细[:]\s*(.*)$/);
if (detailMatch) {
const detailText = detailMatch[1].trim();
return `变更明细:${detailText || "无"}`;
}
if (text === "立项配置已发布") return "立项配置已发布";
if (text.includes("siteEnrollmentPlans")) return text.replaceAll("siteEnrollmentPlans", "中心入组计划");
if (text.includes("monitoringStrategies")) return text.replaceAll("monitoringStrategies", "监查策略");
return text;
};
const splitAuditDetailSegments = (line: string): string[] => {
const text = String(line || "").trim();
if (!text) return [];
return text
.split(/[;\n]/)
.map((item) => item.trim())
.filter(Boolean);
};
const auditFieldLabelMap: Record<string, string> = {
status: "状态",
is_active: "启用状态",
role: "角色",
role_in_study: "项目角色",
full_name: "姓名",
username: "账号",
department: "部门",
email: "邮箱",
name: "名称",
code: "编号",
site_name: "中心名称",
pi_name: "PI",
city: "城市",
contact: "负责人",
phone: "电话",
plan_start_date: "计划开始日期",
plan_end_date: "计划结束日期",
};
const toReadableFieldLabel = (key: string): string => {
const normalized = String(key || "").trim();
if (!normalized) return "字段";
if (auditFieldLabelMap[normalized]) return auditFieldLabelMap[normalized];
return `字段(${normalized}`;
};
const toReadableValue = (key: string, value: any): string => {
if (value === null || value === undefined || value === "") return "未填写";
if (typeof value === "boolean") {
if (key === "is_active") return value ? "启用" : "停用";
return value ? "是" : "否";
}
if (typeof value === "number") return String(value);
if (typeof value === "string") {
const text = value.trim();
if (!text) return "未填写";
if (key.toLowerCase().includes("status")) {
lines.push(`${TEXT.audit.statusLabel}${getDictLabel(statusDict, prev)}${getDictLabel(statusDict, next)}`);
} else {
lines.push(`${key}${prev ?? TEXT.audit.emptyValue}${next ?? TEXT.audit.emptyValue}`);
return getDictLabel(statusDict, text) || text;
}
return text;
}
if (Array.isArray(value)) return `${value.length}`;
if (typeof value === "object") {
if (typeof value.name === "string" && value.name.trim()) return value.name.trim();
if (typeof value.label === "string" && value.label.trim()) return value.label.trim();
return "已配置";
}
return String(value);
};
const buildDiffText = (before?: Record<string, any>, after?: Record<string, any>) => {
if (!before && !after) return [];
const lines: string[] = [];
const prevObj = before || {};
const nextObj = after || {};
Object.keys({ ...prevObj, ...nextObj }).forEach((key) => {
const prev = prevObj[key];
const next = nextObj[key];
if (prev === next) return;
const label = toReadableFieldLabel(key);
const prevText = toReadableValue(key, prev);
const nextText = toReadableValue(key, next);
lines.push(`${label}${prevText} -> ${nextText}`);
});
return lines;
};
export const normalizeAuditEvent = (raw: any, userMap: Record<string, string>): AuditEvent => {
const dict = auditDict[raw.action] || {
label: TEXT.audit.eventFallback,
actionText: TEXT.audit.actionFallback,
targetLabel: raw.entity_type || TEXT.audit.targetFallback,
label: raw.action ? `${TEXT.audit.eventFallback}${raw.action}` : TEXT.audit.eventFallback,
actionText: raw.action ? `执行了${raw.action}` : TEXT.audit.actionFallback,
targetLabel: toReadableEntityType(raw.entity_type),
};
let detailObj: any = {};
if (raw.detail) {
@@ -57,7 +214,18 @@ export const normalizeAuditEvent = (raw: any, userMap: Record<string, string>):
}
const before = detailObj.before;
const after = detailObj.after;
const diffText = detailObj.diffText || buildDiffText(before, after);
const fallbackDescription = typeof detailObj.description === "string" ? detailObj.description.trim() : "";
let diffText = detailObj.diffText || buildDiffText(before, after);
if ((!Array.isArray(diffText) || diffText.length === 0) && fallbackDescription) {
diffText = fallbackDescription
.split(/[;\n]/)
.map((item: string) => item.trim())
.filter(Boolean);
}
const readableDiffText = (Array.isArray(diffText) ? diffText : diffText ? [String(diffText)] : [])
.flatMap((line) => splitAuditDetailSegments(String(line)))
.map((line) => normalizeLegacyDetailLine(line))
.filter(Boolean);
return {
eventType: raw.action,
eventLabel: dict.label,
@@ -68,11 +236,11 @@ export const normalizeAuditEvent = (raw: any, userMap: Record<string, string>):
targetType: raw.entity_type || "",
targetTypeLabel: dict.targetLabel,
targetId: raw.entity_id || "",
targetName: detailObj.targetName,
targetName: detailObj.targetName || raw.entity_id || "",
actionText: dict.actionText,
before,
after,
diffText: Array.isArray(diffText) ? diffText : diffText ? [diffText] : [],
diffText: readableDiffText,
reason: detailObj.reason,
result: detailObj.result || "SUCCESS",
resultLabel: detailObj.result === "FAIL" ? TEXT.audit.resultFail : TEXT.audit.resultSuccess,