bd leader 别名
This commit is contained in:
parent
dcf5ea2045
commit
cf83d334e7
@ -1,6 +1,7 @@
|
|||||||
import { afterEach, expect, test, vi } from "vitest";
|
import { afterEach, expect, test, vi } from "vitest";
|
||||||
import { setAccessToken } from "@/shared/api/request";
|
import { setAccessToken } from "@/shared/api/request";
|
||||||
import {
|
import {
|
||||||
|
createBDLeader,
|
||||||
createCountry,
|
createCountry,
|
||||||
createCoinSeller,
|
createCoinSeller,
|
||||||
createRegion,
|
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 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 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 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 createCoinSeller({ commandId: "coin-seller-test", reason: "contact", targetUserId: 1001 });
|
||||||
await setCoinSellerStatus(1001, { commandId: "coin-seller-status-test", reason: "disable", status: "disabled" });
|
await setCoinSellerStatus(1001, { commandId: "coin-seller-status-test", reason: "disable", status: "disabled" });
|
||||||
await creditCoinSellerStock(1001, {
|
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 [agencyUrl] = vi.mocked(fetch).mock.calls[1];
|
||||||
const [hostUrl] = vi.mocked(fetch).mock.calls[2];
|
const [hostUrl] = vi.mocked(fetch).mock.calls[2];
|
||||||
const [coinSellerUrl, coinSellerInit] = vi.mocked(fetch).mock.calls[3];
|
const [coinSellerUrl, coinSellerInit] = vi.mocked(fetch).mock.calls[3];
|
||||||
const [coinSellerCreateUrl, coinSellerCreateInit] = vi.mocked(fetch).mock.calls[4];
|
const [bdLeaderCreateUrl, bdLeaderCreateInit] = vi.mocked(fetch).mock.calls[4];
|
||||||
const [coinSellerStatusUrl, coinSellerStatusInit] = vi.mocked(fetch).mock.calls[5];
|
const [coinSellerCreateUrl, coinSellerCreateInit] = vi.mocked(fetch).mock.calls[5];
|
||||||
const [coinSellerStockUrl, coinSellerStockInit] = vi.mocked(fetch).mock.calls[6];
|
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("/api/v1/admin/bds?");
|
||||||
expect(String(bdUrl)).toContain("parent_leader_user_id=21");
|
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("region_id=7");
|
||||||
expect(String(coinSellerUrl)).toContain("status=active");
|
expect(String(coinSellerUrl)).toContain("status=active");
|
||||||
expect(coinSellerInit?.method).toBe("GET");
|
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(String(coinSellerCreateUrl)).toContain("/api/v1/admin/coin-sellers");
|
||||||
expect(coinSellerCreateInit?.method).toBe("POST");
|
expect(coinSellerCreateInit?.method).toBe("POST");
|
||||||
expect(String(coinSellerStatusUrl)).toContain("/api/v1/admin/coin-sellers/1001/status");
|
expect(String(coinSellerStatusUrl)).toContain("/api/v1/admin/coin-sellers/1001/status");
|
||||||
|
|||||||
@ -1,32 +1,51 @@
|
|||||||
import Tooltip from "@mui/material/Tooltip";
|
import Tooltip from "@mui/material/Tooltip";
|
||||||
import styles from "@/features/host-org/host-org.module.css";
|
import styles from "@/features/host-org/host-org.module.css";
|
||||||
|
import { Button } from "@/shared/ui/Button.jsx";
|
||||||
import { IconButton } from "@/shared/ui/IconButton.jsx";
|
import { IconButton } from "@/shared/ui/IconButton.jsx";
|
||||||
|
|
||||||
export function HostOrgToolbar({ actions = [] }) {
|
export function HostOrgToolbar({ actions = [], leadingActions = [] }) {
|
||||||
if (!actions.length) {
|
if (!actions.length && !leadingActions.length) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.toolbar}>
|
<div className={styles.toolbar}>
|
||||||
<div className={styles.toolbarActions}>
|
{leadingActions.length ? (
|
||||||
{actions.map((action) => (
|
<div className={styles.toolbarLeft}>
|
||||||
<Tooltip arrow key={action.label} title={action.label}>
|
{leadingActions.map((action) => (
|
||||||
<span>
|
<Button
|
||||||
<IconButton
|
className={styles.toolbarTextAction}
|
||||||
className={styles.toolbarAction}
|
disabled={action.disabled}
|
||||||
disabled={action.disabled}
|
key={action.label}
|
||||||
label={action.label}
|
startIcon={action.icon}
|
||||||
tone={action.tone}
|
variant={action.variant}
|
||||||
onClick={action.onClick}
|
onClick={action.onClick}
|
||||||
sx={action.variant === "primary" ? primaryActionSx : undefined}
|
>
|
||||||
>
|
{action.label}
|
||||||
{action.icon}
|
</Button>
|
||||||
</IconButton>
|
))}
|
||||||
</span>
|
</div>
|
||||||
</Tooltip>
|
) : null}
|
||||||
))}
|
{actions.length ? (
|
||||||
</div>
|
<div className={styles.toolbarActions}>
|
||||||
|
{actions.map((action) => (
|
||||||
|
<Tooltip arrow key={action.label} title={action.label}>
|
||||||
|
<span>
|
||||||
|
<IconButton
|
||||||
|
className={styles.toolbarAction}
|
||||||
|
disabled={action.disabled}
|
||||||
|
label={action.label}
|
||||||
|
tone={action.tone}
|
||||||
|
onClick={action.onClick}
|
||||||
|
sx={action.variant === "primary" ? primaryActionSx : undefined}
|
||||||
|
>
|
||||||
|
{action.icon}
|
||||||
|
</IconButton>
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
23
src/features/host-org/components/HostOrgToolbar.test.jsx
Normal file
23
src/features/host-org/components/HostOrgToolbar.test.jsx
Normal file
@ -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(
|
||||||
|
<HostOrgToolbar
|
||||||
|
actions={[{ icon: <span />, label: "添加币商", onClick: openCreate, variant: "primary" }]}
|
||||||
|
leadingActions={[{ icon: <span />, 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);
|
||||||
|
});
|
||||||
@ -17,7 +17,13 @@ import { useRegionOptions } from "@/shared/hooks/useRegionOptions.js";
|
|||||||
import { useBDAbilities } from "@/features/host-org/permissions.js";
|
import { useBDAbilities } from "@/features/host-org/permissions.js";
|
||||||
import { createBDSchema, createBDLeaderSchema } from "@/features/host-org/schema";
|
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 emptyBDForm = () => ({ commandId: makeCommandId("bd"), parentLeaderUserId: "", reason: "", targetUserId: "" });
|
||||||
const pageSize = 50;
|
const pageSize = 50;
|
||||||
const emptyData = { items: [], page: 1, pageSize, total: 0 };
|
const emptyData = { items: [], page: 1, pageSize, total: 0 };
|
||||||
|
|||||||
@ -66,6 +66,10 @@
|
|||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.toolbarTextAction {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
.filters {
|
.filters {
|
||||||
display: flex;
|
display: flex;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
|||||||
@ -28,6 +28,12 @@ const bdLeaderBaseColumns = [
|
|||||||
render: (item, _index, context) => regionName(item, context?.regionOptions),
|
render: (item, _index, context) => regionName(item, context?.regionOptions),
|
||||||
width: "minmax(130px, 0.75fr)",
|
width: "minmax(130px, 0.75fr)",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: "positionAlias",
|
||||||
|
label: "职位别名",
|
||||||
|
render: (item) => item.positionAlias || "-",
|
||||||
|
width: "minmax(130px, 0.75fr)",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: "status",
|
key: "status",
|
||||||
label: "状态",
|
label: "状态",
|
||||||
@ -84,9 +90,7 @@ export function HostBdLeadersPage() {
|
|||||||
return (
|
return (
|
||||||
<section className={styles.root}>
|
<section className={styles.root}>
|
||||||
<div className={styles.contentPanel}>
|
<div className={styles.contentPanel}>
|
||||||
<HostOrgToolbar
|
<HostOrgToolbar actions={toolbarActions} />
|
||||||
actions={toolbarActions}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DataState error={page.error} loading={page.loading} onRetry={page.reload}>
|
<DataState error={page.error} loading={page.loading} onRetry={page.reload}>
|
||||||
<div className={styles.listBlock}>
|
<div className={styles.listBlock}>
|
||||||
@ -139,6 +143,14 @@ export function HostBdLeadersPage() {
|
|||||||
value={page.bdLeaderForm.regionId}
|
value={page.bdLeaderForm.regionId}
|
||||||
onChange={(value) => page.setBDLeaderForm({ ...page.bdLeaderForm, regionId: value })}
|
onChange={(value) => page.setBDLeaderForm({ ...page.bdLeaderForm, regionId: value })}
|
||||||
/>
|
/>
|
||||||
|
<TextField
|
||||||
|
disabled={createDisabled}
|
||||||
|
label="职位别名"
|
||||||
|
value={page.bdLeaderForm.positionAlias}
|
||||||
|
onChange={(event) =>
|
||||||
|
page.setBDLeaderForm({ ...page.bdLeaderForm, positionAlias: event.target.value })
|
||||||
|
}
|
||||||
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
disabled={createDisabled}
|
disabled={createDisabled}
|
||||||
label="联系方式"
|
label="联系方式"
|
||||||
|
|||||||
@ -125,10 +125,12 @@ export function HostCoinSellersPage() {
|
|||||||
value: page.regionId,
|
value: page.regionId,
|
||||||
onChange: page.changeRegionId,
|
onChange: page.changeRegionId,
|
||||||
});
|
});
|
||||||
const toolbarActions = [
|
const leadingActions = [
|
||||||
page.abilities.canExchangeRate
|
page.abilities.canExchangeRate
|
||||||
? { icon: <SettingsOutlined fontSize="small" />, label: "设置兑换比例", onClick: page.openRateSettings }
|
? { icon: <SettingsOutlined fontSize="small" />, label: "工资兑换比例", onClick: page.openRateSettings }
|
||||||
: null,
|
: null,
|
||||||
|
].filter(Boolean);
|
||||||
|
const toolbarActions = [
|
||||||
page.abilities.canCreate
|
page.abilities.canCreate
|
||||||
? { icon: <Add fontSize="small" />, label: "添加币商", onClick: page.openCreateSeller, variant: "primary" }
|
? { icon: <Add fontSize="small" />, label: "添加币商", onClick: page.openCreateSeller, variant: "primary" }
|
||||||
: null,
|
: null,
|
||||||
@ -139,6 +141,7 @@ export function HostCoinSellersPage() {
|
|||||||
<div className={styles.contentPanel}>
|
<div className={styles.contentPanel}>
|
||||||
<HostOrgToolbar
|
<HostOrgToolbar
|
||||||
actions={toolbarActions}
|
actions={toolbarActions}
|
||||||
|
leadingActions={leadingActions}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DataState error={page.error} loading={page.loading} onRetry={page.reload}>
|
<DataState error={page.error} loading={page.loading} onRetry={page.reload}>
|
||||||
|
|||||||
@ -12,15 +12,18 @@ const commandContactSchema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const userIdSchema = z.coerce.number().int().positive("请输入有效用户短 ID");
|
const userIdSchema = z.coerce.number().int().positive("请输入有效用户短 ID");
|
||||||
const optionalUserIdSchema = z.preprocess((value) => {
|
const optionalUserIdSchema = z.preprocess(
|
||||||
if (value === null || value === undefined) {
|
(value) => {
|
||||||
return 0;
|
if (value === null || value === undefined) {
|
||||||
}
|
return 0;
|
||||||
if (typeof value === "string" && value.trim() === "") {
|
}
|
||||||
return 0;
|
if (typeof value === "string" && value.trim() === "") {
|
||||||
}
|
return 0;
|
||||||
return value;
|
}
|
||||||
}, z.coerce.number().int().min(0, "请输入有效用户短 ID"));
|
return value;
|
||||||
|
},
|
||||||
|
z.coerce.number().int().min(0, "请输入有效用户短 ID"),
|
||||||
|
);
|
||||||
const regionIdSchema = z.coerce.number().int().positive("请输入有效区域 ID");
|
const regionIdSchema = z.coerce.number().int().positive("请输入有效区域 ID");
|
||||||
const sortOrderSchema = z.coerce.number().int().min(0, "排序不能小于 0").default(0);
|
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("");
|
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({
|
export const createBDLeaderSchema = commandContactSchema.extend({
|
||||||
|
positionAlias: optionalTextSchema(64, "职位别名不能超过 64 个字符"),
|
||||||
regionId: regionIdSchema,
|
regionId: regionIdSchema,
|
||||||
targetUserId: userIdSchema,
|
targetUserId: userIdSchema,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -386,6 +386,7 @@ export interface BDProfileDto {
|
|||||||
parentLeaderDisplayUserId?: string;
|
parentLeaderDisplayUserId?: string;
|
||||||
parentLeaderUsername?: string;
|
parentLeaderUsername?: string;
|
||||||
parentLeaderAvatar?: string;
|
parentLeaderAvatar?: string;
|
||||||
|
positionAlias?: string;
|
||||||
regionId?: number;
|
regionId?: number;
|
||||||
regionName?: string;
|
regionName?: string;
|
||||||
role?: string;
|
role?: string;
|
||||||
@ -797,6 +798,7 @@ export interface HostCommandPayload {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface CreateBDLeaderPayload extends HostCommandPayload {
|
export interface CreateBDLeaderPayload extends HostCommandPayload {
|
||||||
|
positionAlias?: string;
|
||||||
regionId: number;
|
regionId: number;
|
||||||
targetUserId: number;
|
targetUserId: number;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user