Files
model-platform/frontend/app/features/admin/state/adminStore.ts
T

476 lines
14 KiB
TypeScript

import { create } from "zustand";
import {
ApiRequestError,
type Employee,
type Workspace,
type WorkspaceBoundApi,
} from "../../../services/api";
import { useScriptWorkspaceStore } from "../../platform/state/scriptWorkspaceStore";
import { useUiStore } from "../../platform/state/uiStore";
type Notice = {
tone: "success" | "error" | "info";
message: string;
};
// ---- Users domain ----
export type UserForm = {
username: string;
display_name: string;
email: string;
role_code: "admin" | "developer";
password: string;
status: "active" | "disabled" | "locked";
};
export const EMPTY_USER_FORM: UserForm = {
username: "",
display_name: "",
email: "",
role_code: "developer",
password: "",
status: "active",
};
// ---- Projects domain ----
export type ProjectForm = {
workspace_code: string;
workspace_name: string;
quota_bytes: number;
description: string;
};
export const EMPTY_PROJECT_FORM: ProjectForm = {
workspace_code: "",
workspace_name: "",
quota_bytes: 0,
description: "",
};
// ---- Module-level non-reactive holder ----
let _api: WorkspaceBoundApi | null = null;
export const bindAdminApi = (api: WorkspaceBoundApi | null) => {
_api = api;
};
function requireApi(): WorkspaceBoundApi {
if (!_api) throw new Error("admin API 未绑定");
return _api;
}
function pushToast(notice: Notice) {
useUiStore.getState().pushToast(notice);
}
function setApiOnline(online: boolean) {
useScriptWorkspaceStore.getState().setApiOnline(online);
}
// ---- Store ----
type State = {
// users
employees: Employee[];
usersLoading: boolean;
usersSaving: boolean;
editingUser: Employee | null;
userDialogOpen: boolean;
userForm: UserForm;
userSearchTerm: string;
// projects
projects: Workspace[];
projectsLoading: boolean;
projectsSaving: boolean;
editingProject: Workspace | null;
projectDialogOpen: boolean;
projectForm: ProjectForm;
selectedProject: Workspace | null;
availableUsers: Employee[];
selectedUserIds: string[];
selectedRoleCode: "admin" | "developer";
projectSearchTerm: string;
importMemberDialogOpen: boolean;
};
type Actions = {
bindApi: (api: WorkspaceBoundApi | null) => void;
reset: () => void;
// users setters
setEmployees: (updater: (current: Employee[]) => Employee[]) => void;
setUsersLoading: (loading: boolean) => void;
setUsersSaving: (saving: boolean) => void;
setEditingUser: (e: Employee | null) => void;
setUserDialogOpen: (open: boolean) => void;
setUserForm: (form: UserForm) => void;
setUserSearchTerm: (k: string) => void;
// projects setters
setProjects: (updater: (current: Workspace[]) => Workspace[]) => void;
setProjectsLoading: (loading: boolean) => void;
setProjectsSaving: (saving: boolean) => void;
setEditingProject: (p: Workspace | null) => void;
setProjectDialogOpen: (open: boolean) => void;
setProjectForm: (form: ProjectForm) => void;
setSelectedProject: (p: Workspace | null) => void;
setAvailableUsers: (users: Employee[]) => void;
setSelectedUserIds: (ids: string[]) => void;
setSelectedRoleCode: (r: "admin" | "developer") => void;
setProjectSearchTerm: (k: string) => void;
setImportMemberDialogOpen: (open: boolean) => void;
// users actions
loadEmployees: () => Promise<void>;
createEmployee: (
input: {
username: string;
display_name: string;
email?: string | null;
role_code: "admin" | "developer";
password: string;
status?: "active" | "disabled" | "locked";
},
onNotify: (n: Notice) => void,
onConnectionChange: (online: boolean) => void,
) => Promise<void>;
updateEmployee: (
employee: Employee,
input: {
display_name: string;
email?: string | null;
role_code: "admin" | "developer";
status: "active" | "disabled" | "locked";
},
onNotify: (n: Notice) => void,
onConnectionChange: (online: boolean) => void,
) => Promise<void>;
deleteEmployee: (
employee: Employee,
onNotify: (n: Notice) => void,
onConnectionChange: (online: boolean) => void,
) => Promise<void>;
// projects actions
loadProjects: (
onNotify: (n: Notice) => void,
onConnectionChange: (online: boolean) => void,
) => Promise<void>;
createProject: (
input: {
workspace_code: string;
workspace_name: string;
quota_bytes: number;
description?: string;
},
onNotify: (n: Notice) => void,
onConnectionChange: (online: boolean) => void,
) => Promise<void>;
updateProject: (
workspace: Workspace,
input: {
workspace_name: string;
quota_bytes: number;
description?: string;
},
onNotify: (n: Notice) => void,
onConnectionChange: (online: boolean) => void,
) => Promise<void>;
deleteProject: (
workspace: Workspace,
onNotify: (n: Notice) => void,
onConnectionChange: (online: boolean) => void,
) => Promise<void>;
openImportMembers: (
workspace: Workspace,
onNotify: (n: Notice) => void,
) => Promise<void>;
importMembers: (
workspace: Workspace,
user_ids: string[],
role_code: "admin" | "developer",
onNotify: (n: Notice) => void,
) => Promise<void>;
};
const initial: State = {
employees: [],
usersLoading: true,
usersSaving: false,
editingUser: null,
userDialogOpen: false,
userForm: EMPTY_USER_FORM,
userSearchTerm: "",
projects: [],
projectsLoading: true,
projectsSaving: false,
editingProject: null,
projectDialogOpen: false,
projectForm: EMPTY_PROJECT_FORM,
selectedProject: null,
availableUsers: [],
selectedUserIds: [],
selectedRoleCode: "developer",
projectSearchTerm: "",
importMemberDialogOpen: false,
};
async function handleError(
error: unknown,
fallback: string,
onNotify: (n: Notice) => void,
): Promise<void> {
if (error instanceof ApiRequestError && error.status === 401) {
onNotify({ tone: "error", message: "登录已失效,请重新登录" });
return;
}
if (error instanceof ApiRequestError && error.status === 403) {
onNotify({ tone: "error", message: "没有权限执行该操作" });
return;
}
if (error instanceof ApiRequestError && error.status === 409) {
onNotify({
tone: "error",
message:
error.message
|| "操作冲突,目标资源可能已存在或状态不一致",
});
return;
}
onNotify({
tone: "error",
message: error instanceof Error ? error.message : fallback,
});
}
export const useAdminStore = create<State & Actions>((set, get) => ({
...initial,
bindApi: bindAdminApi,
reset: () => set({ ...initial, usersLoading: true, projectsLoading: true }),
// ---- users setters ----
setEmployees: (updater) =>
set((state) => ({ employees: updater(state.employees) })),
setUsersLoading: (loading) => set({ usersLoading: loading }),
setUsersSaving: (saving) => set({ usersSaving: saving }),
setEditingUser: (e) => set({ editingUser: e }),
setUserDialogOpen: (open) => set({ userDialogOpen: open }),
setUserForm: (form) => set({ userForm: form }),
setUserSearchTerm: (k) => set({ userSearchTerm: k }),
// ---- projects setters ----
setProjects: (updater) =>
set((state) => ({ projects: updater(state.projects) })),
setProjectsLoading: (loading) => set({ projectsLoading: loading }),
setProjectsSaving: (saving) => set({ projectsSaving: saving }),
setEditingProject: (p) => set({ editingProject: p }),
setProjectDialogOpen: (open) => set({ projectDialogOpen: open }),
setProjectForm: (form) => set({ projectForm: form }),
setSelectedProject: (p) => set({ selectedProject: p }),
setAvailableUsers: (users) => set({ availableUsers: users }),
setSelectedUserIds: (ids) => set({ selectedUserIds: ids }),
setSelectedRoleCode: (r) => set({ selectedRoleCode: r }),
setProjectSearchTerm: (k) => set({ projectSearchTerm: k }),
setImportMemberDialogOpen: (open) => set({ importMemberDialogOpen: open }),
// ---- users actions ----
loadEmployees: async () => {
const api = requireApi();
set({ usersLoading: true });
try {
const list = await api.listEmployees();
set({ employees: list });
setApiOnline(true);
} catch (error) {
setApiOnline(false);
pushToast({
tone: "error",
message: error instanceof Error ? error.message : "用户列表加载失败",
});
} finally {
set({ usersLoading: false });
}
},
createEmployee: async (form, onNotify, onConnectionChange) => {
const api = requireApi();
if (get().usersSaving) return;
set({ usersSaving: true });
try {
const created = await api.createEmployee(form);
set((state) => ({ employees: [created, ...state.employees] }));
set({ userDialogOpen: false, editingUser: null });
onConnectionChange(true);
onNotify({ tone: "success", message: "用户已创建" });
} catch (error) {
await handleError(error, "创建用户失败", onNotify);
} finally {
set({ usersSaving: false });
}
},
updateEmployee: async (employee, form, onNotify, onConnectionChange) => {
const api = requireApi();
if (get().usersSaving) return;
set({ usersSaving: true });
try {
const updated = await api.updateEmployee(employee.user_id, form);
set((state) => ({
employees: state.employees.map((e) =>
e.user_id === updated.user_id ? updated : e
),
}));
set({ userDialogOpen: false, editingUser: null });
onConnectionChange(true);
onNotify({ tone: "success", message: "用户已更新" });
} catch (error) {
await handleError(error, "更新用户失败", onNotify);
} finally {
set({ usersSaving: false });
}
},
deleteEmployee: async (employee, onNotify, onConnectionChange) => {
const api = requireApi();
if (get().usersSaving) return;
set({ usersSaving: true });
try {
await api.deleteEmployee(employee.user_id);
set((state) => ({
employees: state.employees.filter((e) => e.user_id !== employee.user_id),
}));
onConnectionChange(true);
onNotify({ tone: "success", message: "用户已删除" });
} catch (error) {
await handleError(error, "删除用户失败", onNotify);
} finally {
set({ usersSaving: false });
}
},
// ---- projects actions ----
loadProjects: async (onNotify, onConnectionChange) => {
const api = requireApi();
set({ projectsLoading: true });
try {
const list = await api.listWorkspaces();
set({ projects: list });
onConnectionChange(true);
} catch (error) {
onConnectionChange(false);
await handleError(error, "项目列表加载失败", onNotify);
} finally {
set({ projectsLoading: false });
}
},
createProject: async (form, onNotify, onConnectionChange) => {
const api = requireApi();
if (get().projectsSaving) return;
set({ projectsSaving: true });
try {
const created = await api.createWorkspace(form);
set((state) => ({ projects: [created, ...state.projects] }));
set({ projectDialogOpen: false, editingProject: null });
onConnectionChange(true);
onNotify({ tone: "success", message: "项目已创建" });
} catch (error) {
await handleError(error, "创建项目失败", onNotify);
} finally {
set({ projectsSaving: false });
}
},
updateProject: async (workspace, form, onNotify, onConnectionChange) => {
const api = requireApi();
if (get().projectsSaving) return;
set({ projectsSaving: true });
try {
const updated = await api.updateWorkspace(workspace.workspace_id, form);
set((state) => ({
projects: state.projects.map((p) =>
p.workspace_id === updated.workspace_id ? updated : p
),
}));
set({ projectDialogOpen: false, editingProject: null });
onConnectionChange(true);
onNotify({ tone: "success", message: "项目已更新" });
} catch (error) {
await handleError(error, "更新项目失败", onNotify);
} finally {
set({ projectsSaving: false });
}
},
deleteProject: async (workspace, onNotify, onConnectionChange) => {
const api = requireApi();
if (get().projectsSaving) return;
set({ projectsSaving: true });
try {
await api.deleteWorkspace(workspace.workspace_id);
set((state) => ({
projects: state.projects.filter(
(p) => p.workspace_id !== workspace.workspace_id,
),
}));
onConnectionChange(true);
onNotify({ tone: "success", message: "项目已删除" });
} catch (error) {
await handleError(error, "删除项目失败", onNotify);
} finally {
set({ projectsSaving: false });
}
},
openImportMembers: async (workspace, onNotify) => {
const api = requireApi();
set({ selectedProject: workspace, importMemberDialogOpen: true });
try {
const all = await api.listEmployees();
const existing = await api.listWorkspaceMembers(workspace.workspace_id);
const taken = new Set(existing.map((m) => m.user_id));
set({
availableUsers: all.filter((u) => !taken.has(u.user_id)),
selectedUserIds: [],
selectedRoleCode: "developer",
});
} catch (error) {
await handleError(error, "加载成员候选失败", onNotify);
set({ importMemberDialogOpen: false });
}
},
importMembers: async (workspace, user_ids, role_code, onNotify) => {
const api = requireApi();
try {
await Promise.all(
user_ids.map((uid) =>
api.addWorkspaceMember(workspace.workspace_id, {
user_id: uid,
role_code,
}),
),
);
set({ importMemberDialogOpen: false });
onNotify({
tone: "success",
message: `已添加 ${user_ids.length} 名成员`,
});
} catch (error) {
await handleError(error, "导入成员失败", onNotify);
}
},
}));