diff --git a/src/features/host-org/api.test.ts b/src/features/host-org/api.test.ts index b3066a3..298d2bc 100644 --- a/src/features/host-org/api.test.ts +++ b/src/features/host-org/api.test.ts @@ -1,6 +1,7 @@ import { afterEach, expect, test, vi } from "vitest"; import { setAccessToken } from "@/shared/api/request"; import { + createBDLeader, createCountry, createCoinSeller, createRegion, @@ -34,6 +35,12 @@ test("host org list APIs use generated admin paths and filters", async () => { await listAgencies({ keyword: "agency", page: 2, page_size: 10, parent_bd_user_id: 31 }); await listHosts({ agency_id: 41, page: 1, page_size: 10, sort_by: "diamond", sort_direction: "desc" }); await listCoinSellers({ page: 1, page_size: 10, region_id: 7, status: "active" }); + await createBDLeader({ + commandId: "bd-leader-test", + positionAlias: "Senior Admin", + regionId: 7, + targetUserId: 1002, + }); await createCoinSeller({ commandId: "coin-seller-test", reason: "contact", targetUserId: 1001 }); await setCoinSellerStatus(1001, { commandId: "coin-seller-status-test", reason: "disable", status: "disabled" }); await creditCoinSellerStock(1001, { @@ -47,9 +54,10 @@ test("host org list APIs use generated admin paths and filters", async () => { const [agencyUrl] = vi.mocked(fetch).mock.calls[1]; const [hostUrl] = vi.mocked(fetch).mock.calls[2]; const [coinSellerUrl, coinSellerInit] = vi.mocked(fetch).mock.calls[3]; - const [coinSellerCreateUrl, coinSellerCreateInit] = vi.mocked(fetch).mock.calls[4]; - const [coinSellerStatusUrl, coinSellerStatusInit] = vi.mocked(fetch).mock.calls[5]; - const [coinSellerStockUrl, coinSellerStockInit] = vi.mocked(fetch).mock.calls[6]; + const [bdLeaderCreateUrl, bdLeaderCreateInit] = vi.mocked(fetch).mock.calls[4]; + const [coinSellerCreateUrl, coinSellerCreateInit] = vi.mocked(fetch).mock.calls[5]; + const [coinSellerStatusUrl, coinSellerStatusInit] = vi.mocked(fetch).mock.calls[6]; + const [coinSellerStockUrl, coinSellerStockInit] = vi.mocked(fetch).mock.calls[7]; expect(String(bdUrl)).toContain("/api/v1/admin/bds?"); expect(String(bdUrl)).toContain("parent_leader_user_id=21"); @@ -66,6 +74,9 @@ test("host org list APIs use generated admin paths and filters", async () => { expect(String(coinSellerUrl)).toContain("region_id=7"); expect(String(coinSellerUrl)).toContain("status=active"); expect(coinSellerInit?.method).toBe("GET"); + expect(String(bdLeaderCreateUrl)).toContain("/api/v1/admin/bd-leaders"); + expect(bdLeaderCreateInit?.method).toBe("POST"); + expect(JSON.parse(String(bdLeaderCreateInit?.body))).toMatchObject({ positionAlias: "Senior Admin" }); expect(String(coinSellerCreateUrl)).toContain("/api/v1/admin/coin-sellers"); expect(coinSellerCreateInit?.method).toBe("POST"); expect(String(coinSellerStatusUrl)).toContain("/api/v1/admin/coin-sellers/1001/status"); diff --git a/src/features/host-org/components/HostOrgToolbar.jsx b/src/features/host-org/components/HostOrgToolbar.jsx index a9d5846..8fa6926 100644 --- a/src/features/host-org/components/HostOrgToolbar.jsx +++ b/src/features/host-org/components/HostOrgToolbar.jsx @@ -1,32 +1,51 @@ import Tooltip from "@mui/material/Tooltip"; import styles from "@/features/host-org/host-org.module.css"; +import { Button } from "@/shared/ui/Button.jsx"; import { IconButton } from "@/shared/ui/IconButton.jsx"; -export function HostOrgToolbar({ actions = [] }) { - if (!actions.length) { +export function HostOrgToolbar({ actions = [], leadingActions = [] }) { + if (!actions.length && !leadingActions.length) { return null; } return (
-
- {actions.map((action) => ( - - - - {action.icon} - - - - ))} -
+ {leadingActions.length ? ( +
+ {leadingActions.map((action) => ( + + ))} +
+ ) : null} + {actions.length ? ( +
+ {actions.map((action) => ( + + + + {action.icon} + + + + ))} +
+ ) : null}
); } diff --git a/src/features/host-org/components/HostOrgToolbar.test.jsx b/src/features/host-org/components/HostOrgToolbar.test.jsx new file mode 100644 index 0000000..8e6d150 --- /dev/null +++ b/src/features/host-org/components/HostOrgToolbar.test.jsx @@ -0,0 +1,23 @@ +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { expect, test, vi } from "vitest"; +import { HostOrgToolbar } from "@/features/host-org/components/HostOrgToolbar.jsx"; + +test("renders leading config action separately from right icon actions", async () => { + const user = userEvent.setup(); + const openRates = vi.fn(); + const openCreate = vi.fn(); + + render( + , label: "添加币商", onClick: openCreate, variant: "primary" }]} + leadingActions={[{ icon: , label: "工资兑换比例", onClick: openRates }]} + />, + ); + + await user.click(screen.getByRole("button", { name: "工资兑换比例" })); + await user.click(screen.getByRole("button", { name: "添加币商" })); + + expect(openRates).toHaveBeenCalledTimes(1); + expect(openCreate).toHaveBeenCalledTimes(1); +}); diff --git a/src/features/host-org/hooks/useHostBdsPage.js b/src/features/host-org/hooks/useHostBdsPage.js index ac019c0..b8b800c 100644 --- a/src/features/host-org/hooks/useHostBdsPage.js +++ b/src/features/host-org/hooks/useHostBdsPage.js @@ -17,7 +17,13 @@ import { useRegionOptions } from "@/shared/hooks/useRegionOptions.js"; import { useBDAbilities } from "@/features/host-org/permissions.js"; import { createBDSchema, createBDLeaderSchema } from "@/features/host-org/schema"; -const emptyBDLeaderForm = () => ({ commandId: makeCommandId("bd-leader"), reason: "", regionId: "", targetUserId: "" }); +const emptyBDLeaderForm = () => ({ + commandId: makeCommandId("bd-leader"), + positionAlias: "", + reason: "", + regionId: "", + targetUserId: "", +}); const emptyBDForm = () => ({ commandId: makeCommandId("bd"), parentLeaderUserId: "", reason: "", targetUserId: "" }); const pageSize = 50; const emptyData = { items: [], page: 1, pageSize, total: 0 }; diff --git a/src/features/host-org/host-org.module.css b/src/features/host-org/host-org.module.css index 1a0d3f6..fc5af6c 100644 --- a/src/features/host-org/host-org.module.css +++ b/src/features/host-org/host-org.module.css @@ -66,6 +66,10 @@ margin-left: auto; } +.toolbarTextAction { + flex: 0 0 auto; +} + .filters { display: flex; min-width: 0; diff --git a/src/features/host-org/pages/HostBdLeadersPage.jsx b/src/features/host-org/pages/HostBdLeadersPage.jsx index 02e7866..7edc3a9 100644 --- a/src/features/host-org/pages/HostBdLeadersPage.jsx +++ b/src/features/host-org/pages/HostBdLeadersPage.jsx @@ -28,6 +28,12 @@ const bdLeaderBaseColumns = [ render: (item, _index, context) => regionName(item, context?.regionOptions), width: "minmax(130px, 0.75fr)", }, + { + key: "positionAlias", + label: "职位别名", + render: (item) => item.positionAlias || "-", + width: "minmax(130px, 0.75fr)", + }, { key: "status", label: "状态", @@ -84,9 +90,7 @@ export function HostBdLeadersPage() { return (
- +
@@ -139,6 +143,14 @@ export function HostBdLeadersPage() { value={page.bdLeaderForm.regionId} onChange={(value) => page.setBDLeaderForm({ ...page.bdLeaderForm, regionId: value })} /> + + page.setBDLeaderForm({ ...page.bdLeaderForm, positionAlias: event.target.value }) + } + /> , label: "设置兑换比例", onClick: page.openRateSettings } + ? { icon: , label: "工资兑换比例", onClick: page.openRateSettings } : null, + ].filter(Boolean); + const toolbarActions = [ page.abilities.canCreate ? { icon: , label: "添加币商", onClick: page.openCreateSeller, variant: "primary" } : null, @@ -139,6 +141,7 @@ export function HostCoinSellersPage() {
diff --git a/src/features/host-org/schema.ts b/src/features/host-org/schema.ts index 3360478..f2e6f41 100644 --- a/src/features/host-org/schema.ts +++ b/src/features/host-org/schema.ts @@ -12,15 +12,18 @@ const commandContactSchema = z.object({ }); const userIdSchema = z.coerce.number().int().positive("请输入有效用户短 ID"); -const optionalUserIdSchema = z.preprocess((value) => { - if (value === null || value === undefined) { - return 0; - } - if (typeof value === "string" && value.trim() === "") { - return 0; - } - return value; -}, z.coerce.number().int().min(0, "请输入有效用户短 ID")); +const optionalUserIdSchema = z.preprocess( + (value) => { + if (value === null || value === undefined) { + return 0; + } + if (typeof value === "string" && value.trim() === "") { + return 0; + } + return value; + }, + z.coerce.number().int().min(0, "请输入有效用户短 ID"), +); const regionIdSchema = z.coerce.number().int().positive("请输入有效区域 ID"); const sortOrderSchema = z.coerce.number().int().min(0, "排序不能小于 0").default(0); const optionalTextSchema = (max: number, message: string) => z.string().trim().max(max, message).optional().default(""); @@ -66,6 +69,7 @@ const regionBaseSchema = z }); export const createBDLeaderSchema = commandContactSchema.extend({ + positionAlias: optionalTextSchema(64, "职位别名不能超过 64 个字符"), regionId: regionIdSchema, targetUserId: userIdSchema, }); diff --git a/src/shared/api/types.ts b/src/shared/api/types.ts index e6de788..5cab09c 100644 --- a/src/shared/api/types.ts +++ b/src/shared/api/types.ts @@ -386,6 +386,7 @@ export interface BDProfileDto { parentLeaderDisplayUserId?: string; parentLeaderUsername?: string; parentLeaderAvatar?: string; + positionAlias?: string; regionId?: number; regionName?: string; role?: string; @@ -797,6 +798,7 @@ export interface HostCommandPayload { } export interface CreateBDLeaderPayload extends HostCommandPayload { + positionAlias?: string; regionId: number; targetUserId: number; }