This commit is contained in:
zhx 2026-06-24 15:50:08 +08:00
parent dae91e892e
commit b6d8af0fa3
5 changed files with 137 additions and 7 deletions

View File

@ -67,6 +67,7 @@ test("host org list APIs use generated admin paths and filters", async () => {
canGrantVehicle: true,
canTransferUserCountry: false,
contact: "+63",
status: "disabled",
});
await listHosts({ agency_id: 41, page: 1, page_size: 10, sort_by: "diamond", sort_direction: "desc" });
await listCoinSellers({
@ -149,6 +150,7 @@ test("host org list APIs use generated admin paths and filters", async () => {
canGrantVehicle: true,
canTransferUserCountry: false,
contact: "+63",
status: "disabled",
});
expect(String(hostUrl)).toContain("/api/v1/admin/hosts?");
expect(String(hostUrl)).toContain("agency_id=41");

View File

@ -28,6 +28,12 @@ const emptyManagerForm = () => ({ contact: "", targetUserId: "", ...emptyPermiss
const managerPermissionPayload = (form) =>
Object.fromEntries(managerPermissionFields.map((field) => [field, form[field] !== false]));
const managerStatusPayload = (item, status) => ({
contact: item?.contact || "",
status,
...Object.fromEntries(managerPermissionFields.map((field) => [field, item?.[field] !== false])),
});
const managerFormFromItem = (item) => ({
contact: item?.contact || "",
targetUserId: String(item?.userId || ""),
@ -139,6 +145,21 @@ export function useHostManagersPage() {
setActiveAction("manager-edit");
};
const toggleManager = async (item, enabled) => {
const nextStatus = enabled ? "active" : "disabled";
setLoadingAction(`manager-status-${item.userId}`);
try {
// 状态接口复用经理更新入口;提交当前行的权限快照,避免只改状态时把已有权限开关覆盖成后端默认值。
await updateManager(item.userId, managerStatusPayload(item, nextStatus));
showToast(enabled ? "经理已启用" : "经理已停用", "success");
await reload();
} catch (err) {
showToast(err.message || (enabled ? "启用经理失败" : "停用经理失败"), "error");
} finally {
setLoadingAction("");
}
};
return {
abilities,
activeAction,
@ -165,5 +186,6 @@ export function useHostManagersPage() {
setPage,
status,
submitManager,
toggleManager,
};
}

View File

@ -6,7 +6,7 @@ import { formatMillis } from "@/shared/utils/time.js";
import { AdminSwitch } from "@/shared/ui/AdminSwitch.jsx";
import { DataState } from "@/shared/ui/DataState.jsx";
import { IconButton } from "@/shared/ui/IconButton.jsx";
import { managerStatusFilters, statusLabels } from "@/features/host-org/constants.js";
import { managerStatusFilters } from "@/features/host-org/constants.js";
import { HostOrgActionModal } from "@/features/host-org/components/HostOrgActionModal.jsx";
import { HostOrgPerson, hostOrgRegionName } from "@/features/host-org/components/HostOrgIdentity.jsx";
import { HostOrgTable } from "@/features/host-org/components/HostOrgTable.jsx";
@ -55,7 +55,7 @@ const managerColumns = [
{
key: "status",
label: "状态",
render: (item) => <ManagerStatus item={item} />,
render: (item, _index, context) => <ManagerStatusSwitch item={item} page={context?.page} />,
width: "minmax(90px, 0.7fr)",
},
{
@ -187,12 +187,14 @@ function ManagerUser({ item }) {
);
}
function ManagerStatus({ item }) {
const active = item.status === "active";
function ManagerStatusSwitch({ item, page }) {
return (
<span className={active ? styles.positive : styles.negative}>
{statusLabels[item.status] || item.status || "-"}
</span>
<AdminSwitch
checked={item.status === "active"}
disabled={!page?.abilities.canUpdate || page.loadingAction === `manager-status-${item.userId}`}
inputProps={{ "aria-label": item.status === "active" ? "停用经理" : "启用经理" }}
onChange={(event) => page.toggleManager(item, event.target.checked)}
/>
);
}

View File

@ -0,0 +1,103 @@
import { fireEvent, render, screen } from "@testing-library/react";
import { afterEach, expect, test, vi } from "vitest";
import { HostManagersPage } from "./HostManagersPage.jsx";
import { useHostManagersPage } from "@/features/host-org/hooks/useHostManagersPage.js";
const mocks = vi.hoisted(() => ({
toggleManager: vi.fn(),
}));
vi.mock("@/features/host-org/hooks/useHostManagersPage.js", () => ({
useHostManagersPage: vi.fn(),
}));
afterEach(() => {
vi.clearAllMocks();
});
test("manager status column uses a switch and toggles through the page hook", () => {
const manager = managerFixture();
vi.mocked(useHostManagersPage).mockReturnValue(pageFixture({ data: { items: [manager], page: 1, pageSize: 50, total: 1 } }));
render(<HostManagersPage />);
const statusSwitch = screen.getByRole("switch", { name: "停用经理" });
expect(statusSwitch).toBeChecked();
fireEvent.click(statusSwitch);
expect(mocks.toggleManager).toHaveBeenCalledWith(manager, false);
});
function pageFixture(patch = {}) {
return {
abilities: {
canCreate: true,
canUpdate: true,
canView: true,
},
activeAction: "",
changeQuery: vi.fn(),
changeRegionId: vi.fn(),
changeStatus: vi.fn(),
closeAction: vi.fn(),
data: { items: [], page: 1, pageSize: 50, total: 0 },
editingManager: null,
error: null,
loading: false,
loadingAction: "",
loadingRegions: false,
managerForm: {
canAddAdmin: true,
canAddBdLeader: true,
canAddSuperadmin: true,
canBlockUser: true,
canGrantAvatarFrame: true,
canGrantBadge: true,
canGrantVehicle: true,
canTransferUserCountry: true,
canUpdateUserLevel: true,
contact: "",
targetUserId: "",
},
openManagerEdit: vi.fn(),
openManagerForm: vi.fn(),
page: 1,
query: "",
regionId: "",
regionOptions: [],
reload: vi.fn(),
resetFilters: vi.fn(),
setManagerForm: vi.fn(),
setPage: vi.fn(),
status: "",
submitManager: vi.fn(),
toggleManager: mocks.toggleManager,
...patch,
};
}
function managerFixture() {
return {
avatar: "",
bdLeaderCount: 3,
canAddAdmin: true,
canAddBdLeader: false,
canAddSuperadmin: true,
canBlockUser: true,
canGrantAvatarFrame: true,
canGrantBadge: true,
canGrantVehicle: true,
canTransferUserCountry: true,
canUpdateUserLevel: true,
contact: "+63 910 323 3670",
displayUserId: "167579",
lastInvitedAtMs: 1782198308000,
regionId: 1,
regionName: "欧亚美区",
status: "active",
updatedAtMs: 1782284840000,
userId: "10001",
username: "PH-MANAGER",
};
}

View File

@ -607,6 +607,7 @@ export interface UpdateManagerPayload {
canTransferUserCountry?: boolean;
canUpdateUserLevel?: boolean;
contact?: string;
status?: "active" | "disabled";
}
export interface HostProfileDto {