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;
}