R54 A1-A6:Cutover 控制台与特性开关增强

- Feature Flag 变更历史查看与影响范围预览
- Feature Flag 批量操作(启用/禁用/继承)
- Cutover 控制台页面增强(摘要卡片、时间线、延迟生效)
- 单测覆盖 changelog、batch、权限边界
这个提交包含在:
2026-04-07 21:35:39 +08:00
父节点 7cc6dc3397
当前提交 3e694fa60c
共修改 7 个文件,包含 2143 行新增和 122 行删除
+103 -1
查看文件
@@ -1,8 +1,32 @@
import { apiRequestData } from "./http";
export type FeatureFlagKey = "erp_sync" | "invoice_verification" | "leave_holiday_calendar" | "budget_precheck_v1" | "payment_batch_v1" | "portal_home_v1" | "global_search_v1" | "announcements_v1" | "workflow_definition_admin_v1" | "cutover_console_v1" | "presence_board_v1" | "weekly_report_v1" | "employee_lifecycle_v1" | "todo_board_v1" | "project_domain_v1" | "project_linking_v1" | "project_communication_log_v1" | "project_closure_checklist_v1" | "confidential_doc_refs_v1" | "public_security_hardening_v1";
export type FeatureFlagKey =
| "ai_assist"
| "erp_sync"
| "invoice_verification"
| "leave_holiday_calendar"
| "budget_precheck_v1"
| "payment_batch_v1"
| "portal_home_v1"
| "global_search_v1"
| "announcements_v1"
| "workflow_definition_admin_v1"
| "cutover_console_v1"
| "presence_board_v1"
| "weekly_report_v1"
| "employee_lifecycle_v1"
| "todo_board_v1"
| "wecom_entry_v1"
| "project_domain_v1"
| "project_linking_v1"
| "project_communication_log_v1"
| "project_closure_checklist_v1"
| "confidential_doc_refs_v1"
| "public_security_hardening_v1";
export type FeatureFlagOverrideMode = "inherit" | "force_on" | "force_off";
export type FeatureFlagRolloutType = "global" | "department_allowlist";
export type FeatureFlagSection = "main" | "admin" | "backup";
export type FeatureFlagItem = {
id: string | null;
@@ -23,10 +47,51 @@ export type FeatureFlagItem = {
requiresDepartment: boolean;
};
export type FeatureFlagSnapshot = {
overrideMode: FeatureFlagOverrideMode;
rolloutType: FeatureFlagRolloutType;
departmentIds: string[];
note: string;
version: number;
};
export type FeatureFlagChangelogItem = {
id: string;
actorId: string;
actorName: string;
createdAt: string | null;
from: FeatureFlagSnapshot | null;
to: FeatureFlagSnapshot | null;
};
export type FeatureFlagImpactNavigationItem = {
label: string;
path: string;
section: FeatureFlagSection;
};
export type FeatureFlagImpactBackendModule = {
label: string;
routes: string[];
};
export type FeatureFlagListResponse = {
items: FeatureFlagItem[];
};
export type FeatureFlagChangelogResponse = {
page: number;
pageSize: number;
total: number;
items: FeatureFlagChangelogItem[];
};
export type FeatureFlagImpactResponse = {
flagKey: FeatureFlagKey;
navigationItems: FeatureFlagImpactNavigationItem[];
backendModules: FeatureFlagImpactBackendModule[];
};
export type FeatureFlagUpdatePayload = {
overrideMode: FeatureFlagOverrideMode;
rolloutType: FeatureFlagRolloutType;
@@ -35,13 +100,50 @@ export type FeatureFlagUpdatePayload = {
expectedVersion: number;
};
export type FeatureFlagBatchUpdatePayload = {
items: Array<{
flagKey: FeatureFlagKey;
overrideMode: FeatureFlagOverrideMode;
expectedVersion: number;
}>;
note: string;
};
export async function fetchFeatureFlags() {
return apiRequestData<FeatureFlagListResponse>("/api/system/feature-flags");
}
export async function fetchFeatureFlagChangelog(
flagKey: FeatureFlagKey,
params?: { page?: number; pageSize?: number },
) {
const searchParams = new URLSearchParams();
if (params?.page) {
searchParams.set("page", String(params.page));
}
if (params?.pageSize) {
searchParams.set("pageSize", String(params.pageSize));
}
const suffix = searchParams.toString() ? `?${searchParams.toString()}` : "";
return apiRequestData<FeatureFlagChangelogResponse>(
`/api/system/feature-flags/${flagKey}/changelog${suffix}`,
);
}
export async function fetchFeatureFlagImpact(flagKey: FeatureFlagKey) {
return apiRequestData<FeatureFlagImpactResponse>(`/api/system/feature-flags/${flagKey}/impact`);
}
export async function updateFeatureFlag(flagKey: FeatureFlagKey, payload: FeatureFlagUpdatePayload) {
return apiRequestData<{ item: FeatureFlagItem }>(`/api/system/feature-flags/${flagKey}`, {
method: "PUT",
body: JSON.stringify(payload),
});
}
export async function batchUpdateFeatureFlags(payload: FeatureFlagBatchUpdatePayload) {
return apiRequestData<{ count: number }>("/api/system/feature-flags/batch", {
method: "POST",
body: JSON.stringify(payload),
});
}