refactor: SchedulesPageRoute.tsx, UserManagementPage.tsx, ProjectManagementPage.tsx
This commit is contained in:
@@ -0,0 +1,476 @@
|
||||
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);
|
||||
}
|
||||
},
|
||||
}));
|
||||
Reference in New Issue
Block a user