立项配置版本管理优化(回滚逻辑、UI、下载功能)

This commit is contained in:
Cheng Zhou
2026-03-04 14:49:59 +08:00
parent 6c5e5770c6
commit 0cd03eefa8
16 changed files with 3528 additions and 418 deletions
+17
View File
@@ -1,7 +1,10 @@
import { apiGet, apiPatch, apiPost, apiDelete, apiPut } from "./axios";
import type { ApiListResponse, Study } from "../types/api";
import type {
StudySetupConfigCheckoutBranchPayload,
StudySetupConfigDraftActionPayload,
StudySetupConfigPublishPayload,
StudySetupConfigMergeMainPayload,
StudySetupConfigResponse,
StudySetupConfigRollbackPayload,
StudySetupConfigUpsertPayload,
@@ -42,3 +45,17 @@ export const deleteStudySetupConfigVersion = (studyId: string, targetVersion: nu
export const rollbackStudySetupConfig = (studyId: string, payload: StudySetupConfigRollbackPayload) =>
apiPost<StudySetupConfigResponse>(`/api/v1/studies/${studyId}/setup-config/rollback`, payload, { suppressErrorMessage: true });
export const checkoutStudySetupConfigBranchDraft = (studyId: string, payload: StudySetupConfigCheckoutBranchPayload) =>
apiPost<StudySetupConfigResponse>(`/api/v1/studies/${studyId}/setup-config/draft/checkout-branch`, payload, {
suppressErrorMessage: true,
});
export const clearStudySetupConfigDraft = (studyId: string, payload: StudySetupConfigDraftActionPayload) =>
apiPost<StudySetupConfigResponse>(`/api/v1/studies/${studyId}/setup-config/draft/clear`, payload, { suppressErrorMessage: true });
export const refillStudySetupConfigDraft = (studyId: string, payload: StudySetupConfigDraftActionPayload) =>
apiPost<StudySetupConfigResponse>(`/api/v1/studies/${studyId}/setup-config/draft/refill`, payload, { suppressErrorMessage: true });
export const mergeStudySetupConfigToMain = (studyId: string, payload: StudySetupConfigMergeMainPayload) =>
apiPost<StudySetupConfigResponse>(`/api/v1/studies/${studyId}/setup-config/merge-main`, payload, { suppressErrorMessage: true });
@@ -1,12 +1,19 @@
import {
checkoutStudySetupConfigBranchDraft,
clearStudySetupConfigDraft,
deleteStudySetupConfigVersion,
fetchStudySetupConfig,
fetchStudySetupConfigVersions,
mergeStudySetupConfigToMain,
publishStudySetupConfig,
refillStudySetupConfigDraft,
rollbackStudySetupConfig,
saveStudySetupConfig,
} from "../api/studies";
import type {
StudySetupConfigCheckoutBranchPayload,
StudySetupConfigDraftActionPayload,
StudySetupConfigMergeMainPayload,
StudySetupConfigPublishPayload,
StudySetupConfigRollbackPayload,
StudySetupConfigUpsertPayload,
@@ -16,16 +23,25 @@ export const useSetupConfig = () => {
const getConfig = (studyId: string) => fetchStudySetupConfig(studyId);
const saveConfig = (studyId: string, payload: StudySetupConfigUpsertPayload) => saveStudySetupConfig(studyId, payload);
const publishConfig = (studyId: string, payload: StudySetupConfigPublishPayload) => publishStudySetupConfig(studyId, payload);
const mergeToMain = (studyId: string, payload: StudySetupConfigMergeMainPayload) => mergeStudySetupConfigToMain(studyId, payload);
const listVersions = (studyId: string) => fetchStudySetupConfigVersions(studyId);
const rollbackConfig = (studyId: string, payload: StudySetupConfigRollbackPayload) => rollbackStudySetupConfig(studyId, payload);
const checkoutBranchDraft = (studyId: string, payload: StudySetupConfigCheckoutBranchPayload) =>
checkoutStudySetupConfigBranchDraft(studyId, payload);
const clearDraft = (studyId: string, payload: StudySetupConfigDraftActionPayload) => clearStudySetupConfigDraft(studyId, payload);
const refillDraft = (studyId: string, payload: StudySetupConfigDraftActionPayload) => refillStudySetupConfigDraft(studyId, payload);
const deleteVersion = (studyId: string, targetVersion: number) => deleteStudySetupConfigVersion(studyId, targetVersion);
return {
getConfig,
saveConfig,
publishConfig,
mergeToMain,
listVersions,
rollbackConfig,
checkoutBranchDraft,
clearDraft,
refillDraft,
deleteVersion,
};
};
+27
View File
@@ -98,6 +98,11 @@ export interface StudySetupConfigResponse {
id: string;
study_id: string;
version: number;
current_branch_name?: string;
current_published_version_id?: string | null;
current_published_version_label?: string | null;
active_branch_base_version_id?: string | null;
active_branch_base_version_label?: string | null;
data: SetupConfigDraft;
publish_status: "DRAFT" | "PUBLISHED" | string;
published_data?: SetupConfigDraft | null;
@@ -134,14 +139,36 @@ export interface StudySetupConfigRollbackPayload {
target_version: number;
}
export interface StudySetupConfigCheckoutBranchPayload {
expected_version?: number | null;
target_version: number;
}
export interface StudySetupConfigMergeMainPayload {
expected_version?: number | null;
source_version: number;
}
export interface StudySetupConfigDraftActionPayload {
expected_version?: number | null;
}
export interface StudySetupConfigVersionItem {
id: string;
study_id: string;
study_setup_config_id: string;
version: number;
display_version: number;
branch_name?: string;
branch_seq?: number;
version_label?: string;
source_version?: number | null;
parent_version_id?: string | null;
merged_from_version_id?: string | null;
config: SetupConfigDraft;
published_project_snapshot?: ProjectPublishSnapshot | null;
is_current_published?: boolean;
is_active_draft_base?: boolean;
published_by?: string | null;
published_by_name?: string | null;
published_at: string;
+73 -5
View File
@@ -107,6 +107,38 @@ const getArrayRowIdentity = (moduleKey: keyof SetupConfigDraft, value: unknown):
return "";
};
type ArrayRowEntry = {
key: string;
value: unknown;
rowIdentity: string;
};
const buildArrayRowEntry = (moduleKey: keyof SetupConfigDraft, value: unknown, index: number): ArrayRowEntry => {
const rowIdentity = getArrayRowIdentity(moduleKey, value);
if (value && typeof value === "object" && !Array.isArray(value)) {
const id = String((value as Record<string, unknown>).id || "").trim();
if (id) {
return {
key: `id:${id}`,
value,
rowIdentity,
};
}
}
if (rowIdentity) {
return {
key: `identity:${rowIdentity}`,
value,
rowIdentity,
};
}
return {
key: `index:${index}`,
value,
rowIdentity: "",
};
};
const formatReadablePath = (moduleKey: keyof SetupConfigDraft, path: string): string => {
let normalized = path;
if (path === moduleKey) {
@@ -247,11 +279,47 @@ const collectDiffRows = (
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) {
const rowIdentity = getArrayRowIdentity(moduleKey, localArr[i] ?? serverArr[i]);
const indexLabel = rowIdentity ? `${i}|${rowIdentity}` : `${i}`;
collectDiffRows(moduleKey, moduleLabel, localArr[i], serverArr[i], `${path}[${indexLabel}]`, rows);
const localEntryMap = new Map<string, ArrayRowEntry[]>();
localArr.forEach((item, index) => {
const entry = buildArrayRowEntry(moduleKey, item, index);
const list = localEntryMap.get(entry.key);
if (list) {
list.push(entry);
} else {
localEntryMap.set(entry.key, [entry]);
}
});
const consumeLocalEntry = (key: string): ArrayRowEntry | null => {
const list = localEntryMap.get(key);
if (!list || list.length === 0) return null;
const entry = list.shift() || null;
if (list.length === 0) localEntryMap.delete(key);
return entry;
};
let pairIndex = 0;
serverArr.forEach((item, index) => {
const serverEntry = buildArrayRowEntry(moduleKey, item, index);
const localEntry = consumeLocalEntry(serverEntry.key);
const rowIdentity = (localEntry?.rowIdentity || serverEntry.rowIdentity || "").trim();
const indexLabel = rowIdentity ? `${pairIndex}|${rowIdentity}` : `${pairIndex}`;
collectDiffRows(moduleKey, moduleLabel, localEntry?.value, serverEntry.value, `${path}[${indexLabel}]`, rows);
pairIndex += 1;
});
localEntryMap.forEach((list) => {
list.forEach((localEntry) => {
const rowIdentity = localEntry.rowIdentity.trim();
const indexLabel = rowIdentity ? `${pairIndex}|${rowIdentity}` : `${pairIndex}`;
collectDiffRows(moduleKey, moduleLabel, localEntry.value, undefined, `${path}[${indexLabel}]`, rows);
pairIndex += 1;
});
});
if (localArr.length === 0 && serverArr.length === 0) {
// Keep behavior explicit for empty array pair to avoid extra recursion.
return;
}
return;
}
+2 -2
View File
@@ -6,8 +6,8 @@ export type SetupWorkflowStatus = "UNSAVED_DRAFT" | "SAVED_DRAFT" | "PUBLISHED";
export type SetupWorkflowTagMeta = { label: string; type: "success" | "warning" | "info" };
export const SETUP_WORKFLOW_TAG_META: Record<SetupWorkflowStatus, SetupWorkflowTagMeta> = {
UNSAVED_DRAFT: { label: "草稿未保存", type: "warning" },
SAVED_DRAFT: { label: "草稿已保存(未发布)", type: "info" },
UNSAVED_DRAFT: { label: "未保存", type: "warning" },
SAVED_DRAFT: { label: "已保存", type: "info" },
PUBLISHED: { label: "已发布", type: "success" },
};
File diff suppressed because it is too large Load Diff