476 lines
14 KiB
TypeScript
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);
|
|
}
|
|
},
|
|
})); |