工资相关

This commit is contained in:
zhx 2026-06-15 10:43:53 +08:00
parent 1447de8563
commit 64cfa472b8
10 changed files with 491 additions and 132 deletions

View File

@ -34,8 +34,23 @@ test("host org list APIs use generated admin paths and filters", async () => {
), ),
); );
await listBDs({ page: 1, page_size: 10, parent_leader_user_id: 21, region_id: 7, status: "active" }); await listBDs({
await listAgencies({ keyword: "agency", page: 2, page_size: 10, parent_bd_user_id: 31 }); page: 1,
page_size: 10,
parent_leader_user_id: 21,
region_id: 7,
sort_by: "salary_wallet",
sort_direction: "asc",
status: "active",
});
await listAgencies({
keyword: "agency",
page: 2,
page_size: 10,
parent_bd_user_id: 31,
sort_by: "salary_wallet",
sort_direction: "desc",
});
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({ await listCoinSellers({
page: 1, page: 1,
@ -84,10 +99,14 @@ test("host org list APIs use generated admin paths and filters", async () => {
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");
expect(String(bdUrl)).toContain("region_id=7"); expect(String(bdUrl)).toContain("region_id=7");
expect(String(bdUrl)).toContain("sort_by=salary_wallet");
expect(String(bdUrl)).toContain("sort_direction=asc");
expect(String(bdUrl)).toContain("status=active"); expect(String(bdUrl)).toContain("status=active");
expect(bdInit?.method).toBe("GET"); expect(bdInit?.method).toBe("GET");
expect(String(agencyUrl)).toContain("/api/v1/admin/agencies?"); expect(String(agencyUrl)).toContain("/api/v1/admin/agencies?");
expect(String(agencyUrl)).toContain("parent_bd_user_id=31"); expect(String(agencyUrl)).toContain("parent_bd_user_id=31");
expect(String(agencyUrl)).toContain("sort_by=salary_wallet");
expect(String(agencyUrl)).toContain("sort_direction=desc");
expect(String(hostUrl)).toContain("/api/v1/admin/hosts?"); expect(String(hostUrl)).toContain("/api/v1/admin/hosts?");
expect(String(hostUrl)).toContain("agency_id=41"); expect(String(hostUrl)).toContain("agency_id=41");
expect(String(hostUrl)).toContain("sort_by=diamond"); expect(String(hostUrl)).toContain("sort_by=diamond");

View File

@ -0,0 +1,32 @@
import ArrowDownwardOutlined from "@mui/icons-material/ArrowDownwardOutlined";
import ArrowUpwardOutlined from "@mui/icons-material/ArrowUpwardOutlined";
import SwapVertOutlined from "@mui/icons-material/SwapVertOutlined";
export function HostOrgSortHeader({ field, label, onToggle, sortBy, sortDirection }) {
const active = sortBy === field;
const SortIcon =
active && sortDirection === "asc"
? ArrowUpwardOutlined
: active && sortDirection === "desc"
? ArrowDownwardOutlined
: SwapVertOutlined;
const directionLabel = sortDirection === "asc" ? "升序" : "降序";
const nextDirectionLabel = active && sortDirection === "desc" ? "升序" : "降序";
return (
<button
aria-label={`${label}${nextDirectionLabel}排序`}
className={["admin-cell__head-trigger", active ? "admin-cell__head-trigger--active" : ""]
.filter(Boolean)
.join(" ")}
type="button"
onClick={() => onToggle(field)}
>
<SortIcon className="admin-cell__head-icon" fontSize="inherit" />
<span className="admin-cell__head-label">
{label}
{active ? <span className="admin-cell__head-filter">({directionLabel})</span> : null}
</span>
</button>
);
}

View File

@ -28,6 +28,8 @@ export function useHostAgenciesPage() {
const [status, setStatus] = useState(""); const [status, setStatus] = useState("");
const [regionId, setRegionId] = useState(""); const [regionId, setRegionId] = useState("");
const [parentBdUserId, setParentBdUserId] = useState(""); const [parentBdUserId, setParentBdUserId] = useState("");
const [sortBy, setSortBy] = useState("");
const [sortDirection, setSortDirection] = useState("");
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
const [agencyForm, setAgencyForm] = useState(emptyAgencyForm); const [agencyForm, setAgencyForm] = useState(emptyAgencyForm);
const [rowPatches, setRowPatches] = useState({}); const [rowPatches, setRowPatches] = useState({});
@ -39,9 +41,11 @@ export function useHostAgenciesPage() {
keyword: query, keyword: query,
parent_bd_user_id: parentBdUserId, parent_bd_user_id: parentBdUserId,
region_id: regionId, region_id: regionId,
sort_by: sortBy,
sort_direction: sortDirection,
status, status,
}), }),
[parentBdUserId, query, regionId, status], [parentBdUserId, query, regionId, sortBy, sortDirection, status],
); );
const { const {
data = emptyData, data = emptyData,
@ -97,11 +101,19 @@ export function useHostAgenciesPage() {
setPage(1); setPage(1);
}; };
const changeSort = (field) => {
setSortDirection((current) => (sortBy === field ? (current === "asc" ? "desc" : "asc") : "desc"));
setSortBy(field);
setPage(1);
};
const resetFilters = () => { const resetFilters = () => {
setQuery(""); setQuery("");
setStatus(""); setStatus("");
setRegionId(""); setRegionId("");
setParentBdUserId(""); setParentBdUserId("");
setSortBy("");
setSortDirection("");
setPage(1); setPage(1);
}; };
@ -212,8 +224,11 @@ export function useHostAgenciesPage() {
removeAgencyFromList, removeAgencyFromList,
setAgencyForm, setAgencyForm,
setPage, setPage,
sortBy,
sortDirection,
status, status,
submitAgency, submitAgency,
changeSort,
toggleAgencyJoinEnabledFromList, toggleAgencyJoinEnabledFromList,
}; };
} }

View File

@ -38,6 +38,8 @@ export function useHostBdsPage({ profileType = "bd" } = {}) {
const [status, setStatus] = useState(""); const [status, setStatus] = useState("");
const [regionId, setRegionId] = useState(""); const [regionId, setRegionId] = useState("");
const [parentLeaderUserId, setParentLeaderUserId] = useState(""); const [parentLeaderUserId, setParentLeaderUserId] = useState("");
const [sortBy, setSortBy] = useState("");
const [sortDirection, setSortDirection] = useState("");
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
const [bdLeaderForm, setBDLeaderForm] = useState(emptyBDLeaderForm); const [bdLeaderForm, setBDLeaderForm] = useState(emptyBDLeaderForm);
const [bdForm, setBDForm] = useState(emptyBDForm); const [bdForm, setBDForm] = useState(emptyBDForm);
@ -54,9 +56,11 @@ export function useHostBdsPage({ profileType = "bd" } = {}) {
keyword: query, keyword: query,
parent_leader_user_id: profileType === "bd" ? parentLeaderUserId : "", parent_leader_user_id: profileType === "bd" ? parentLeaderUserId : "",
region_id: regionId, region_id: regionId,
sort_by: sortBy,
sort_direction: sortDirection,
status, status,
}), }),
[parentLeaderUserId, profileType, query, regionId, status], [parentLeaderUserId, profileType, query, regionId, sortBy, sortDirection, status],
); );
const fetcher = profileType === "bd" ? listBDs : listBDLeaders; const fetcher = profileType === "bd" ? listBDs : listBDLeaders;
const { const {
@ -158,11 +162,19 @@ export function useHostBdsPage({ profileType = "bd" } = {}) {
setPage(1); setPage(1);
}; };
const changeSort = (field) => {
setSortDirection((current) => (sortBy === field ? (current === "asc" ? "desc" : "asc") : "desc"));
setSortBy(field);
setPage(1);
};
const resetFilters = () => { const resetFilters = () => {
setQuery(""); setQuery("");
setStatus(""); setStatus("");
setRegionId(""); setRegionId("");
setParentLeaderUserId(""); setParentLeaderUserId("");
setSortBy("");
setSortDirection("");
setPage(1); setPage(1);
}; };
@ -361,10 +373,13 @@ export function useHostBdsPage({ profileType = "bd" } = {}) {
setBDForm, setBDForm,
setBDLeaderForm, setBDLeaderForm,
setPage, setPage,
sortBy,
sortDirection,
status, status,
submitBD, submitBD,
submitBDLeader, submitBDLeader,
submitLeaderBD, submitLeaderBD,
changeSort,
toggleBD, toggleBD,
toggleBDLeader, toggleBDLeader,
}; };

View File

@ -14,18 +14,19 @@ export function useHostHostsPage() {
const [status, setStatus] = useState(""); const [status, setStatus] = useState("");
const [regionId, setRegionId] = useState(""); const [regionId, setRegionId] = useState("");
const [agencyId, setAgencyId] = useState(""); const [agencyId, setAgencyId] = useState("");
const [diamondSortDirection, setDiamondSortDirection] = useState(""); const [sortBy, setSortBy] = useState("");
const [sortDirection, setSortDirection] = useState("");
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
const filters = useMemo( const filters = useMemo(
() => ({ () => ({
agency_id: agencyId, agency_id: agencyId,
keyword: query, keyword: query,
region_id: regionId, region_id: regionId,
sort_by: diamondSortDirection ? "diamond" : "", sort_by: sortBy,
sort_direction: diamondSortDirection, sort_direction: sortDirection,
status, status,
}), }),
[agencyId, diamondSortDirection, query, regionId, status], [agencyId, query, regionId, sortBy, sortDirection, status],
); );
const { const {
data = emptyData, data = emptyData,
@ -61,8 +62,9 @@ export function useHostHostsPage() {
setPage(1); setPage(1);
}; };
const toggleDiamondSort = () => { const changeSort = (field) => {
setDiamondSortDirection((current) => (current === "desc" ? "asc" : "desc")); setSortDirection((current) => (sortBy === field ? (current === "asc" ? "desc" : "asc") : "desc"));
setSortBy(field);
setPage(1); setPage(1);
}; };
@ -71,7 +73,8 @@ export function useHostHostsPage() {
setStatus(""); setStatus("");
setRegionId(""); setRegionId("");
setAgencyId(""); setAgencyId("");
setDiamondSortDirection(""); setSortBy("");
setSortDirection("");
setPage(1); setPage(1);
}; };
@ -83,7 +86,7 @@ export function useHostHostsPage() {
changeRegionId, changeRegionId,
changeStatus, changeStatus,
data, data,
diamondSortDirection, diamondSortDirection: sortBy === "diamond" ? sortDirection : "",
error, error,
loading, loading,
loadingRegions, loadingRegions,
@ -94,7 +97,10 @@ export function useHostHostsPage() {
reload, reload,
resetFilters, resetFilters,
setPage, setPage,
sortBy,
sortDirection,
status, status,
toggleDiamondSort, changeSort,
toggleDiamondSort: () => changeSort("diamond"),
}; };
} }

View File

@ -7,6 +7,7 @@ import { DataState } from "@/shared/ui/DataState.jsx";
import { agencyStatusFilters } from "@/features/host-org/constants.js"; import { agencyStatusFilters } from "@/features/host-org/constants.js";
import { HostOrgActionModal } from "@/features/host-org/components/HostOrgActionModal.jsx"; import { HostOrgActionModal } from "@/features/host-org/components/HostOrgActionModal.jsx";
import { HostOrgEntity, HostOrgPerson, hostOrgRegionName } from "@/features/host-org/components/HostOrgIdentity.jsx"; import { HostOrgEntity, HostOrgPerson, hostOrgRegionName } from "@/features/host-org/components/HostOrgIdentity.jsx";
import { HostOrgSortHeader } from "@/features/host-org/components/HostOrgSortHeader.jsx";
import { SalaryWalletAdjustButton, SalaryWalletCell } from "@/features/host-org/components/SalaryWalletControls.jsx"; import { SalaryWalletAdjustButton, SalaryWalletCell } from "@/features/host-org/components/SalaryWalletControls.jsx";
import { HostOrgTable } from "@/features/host-org/components/HostOrgTable.jsx"; import { HostOrgTable } from "@/features/host-org/components/HostOrgTable.jsx";
import { HostOrgToolbar } from "@/features/host-org/components/HostOrgToolbar.jsx"; import { HostOrgToolbar } from "@/features/host-org/components/HostOrgToolbar.jsx";
@ -31,6 +32,8 @@ const agencyColumns = [
{ {
key: "salaryWallet", key: "salaryWallet",
label: "工资钱包", label: "工资钱包",
sortable: true,
sortField: "salary_wallet",
render: (item) => ( render: (item) => (
<SalaryWalletCell <SalaryWalletCell
role="agency" role="agency"
@ -121,15 +124,27 @@ export function HostAgenciesPage() {
}), }),
}; };
} }
if (column.sortable) {
return {
...column,
header: (
<HostOrgSortHeader
field={column.sortField}
label={column.label}
sortBy={page.sortBy}
sortDirection={page.sortDirection}
onToggle={page.changeSort}
/>
),
};
}
return column; return column;
}); });
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}>
@ -191,9 +206,7 @@ export function HostAgenciesPage() {
disabled={createDisabled} disabled={createDisabled}
label="入会状态" label="入会状态"
uncheckedLabel="禁止" uncheckedLabel="禁止"
onChange={(event) => onChange={(event) => page.setAgencyForm({ ...page.agencyForm, joinEnabled: event.target.checked })}
page.setAgencyForm({ ...page.agencyForm, joinEnabled: event.target.checked })
}
/> />
<TextField <TextField
disabled={createDisabled} disabled={createDisabled}

View File

@ -10,6 +10,7 @@ import { AdminActionIconButton, AdminRowActions } from "@/shared/ui/AdminListLay
import { AdminUserIdentity } from "@/shared/ui/AdminUserIdentity.jsx"; import { AdminUserIdentity } from "@/shared/ui/AdminUserIdentity.jsx";
import { SideDrawer } from "@/shared/ui/SideDrawer.jsx"; import { SideDrawer } from "@/shared/ui/SideDrawer.jsx";
import { HostOrgActionModal } from "@/features/host-org/components/HostOrgActionModal.jsx"; import { HostOrgActionModal } from "@/features/host-org/components/HostOrgActionModal.jsx";
import { HostOrgSortHeader } from "@/features/host-org/components/HostOrgSortHeader.jsx";
import { SalaryWalletAdjustButton, SalaryWalletCell } from "@/features/host-org/components/SalaryWalletControls.jsx"; import { SalaryWalletAdjustButton, SalaryWalletCell } from "@/features/host-org/components/SalaryWalletControls.jsx";
import { HostOrgStatus } from "@/features/host-org/components/HostOrgStatus.jsx"; import { HostOrgStatus } from "@/features/host-org/components/HostOrgStatus.jsx";
import { HostOrgTable } from "@/features/host-org/components/HostOrgTable.jsx"; import { HostOrgTable } from "@/features/host-org/components/HostOrgTable.jsx";
@ -35,6 +36,8 @@ const bdLeaderBaseColumns = [
{ {
key: "salaryWallet", key: "salaryWallet",
label: "工资钱包", label: "工资钱包",
sortable: true,
sortField: "salary_wallet",
render: (item) => ( render: (item) => (
<SalaryWalletCell <SalaryWalletCell
role="bd_leader" role="bd_leader"
@ -111,6 +114,20 @@ export function HostBdLeadersPage() {
}), }),
}; };
} }
if (column.sortable) {
return {
...column,
header: (
<HostOrgSortHeader
field={column.sortField}
label={column.label}
sortBy={page.sortBy}
sortDirection={page.sortDirection}
onToggle={page.changeSort}
/>
),
};
}
return column; return column;
}); });
const toolbarActions = page.abilities.canCreate const toolbarActions = page.abilities.canCreate

View File

@ -6,6 +6,7 @@ import { DataState } from "@/shared/ui/DataState.jsx";
import { bdStatusFilters } from "@/features/host-org/constants.js"; import { bdStatusFilters } from "@/features/host-org/constants.js";
import { HostOrgActionModal } from "@/features/host-org/components/HostOrgActionModal.jsx"; import { HostOrgActionModal } from "@/features/host-org/components/HostOrgActionModal.jsx";
import { HostOrgPerson, hostOrgRegionName } from "@/features/host-org/components/HostOrgIdentity.jsx"; import { HostOrgPerson, hostOrgRegionName } from "@/features/host-org/components/HostOrgIdentity.jsx";
import { HostOrgSortHeader } from "@/features/host-org/components/HostOrgSortHeader.jsx";
import { SalaryWalletAdjustButton, SalaryWalletCell } from "@/features/host-org/components/SalaryWalletControls.jsx"; import { SalaryWalletAdjustButton, SalaryWalletCell } from "@/features/host-org/components/SalaryWalletControls.jsx";
import { HostOrgTable } from "@/features/host-org/components/HostOrgTable.jsx"; import { HostOrgTable } from "@/features/host-org/components/HostOrgTable.jsx";
import { HostOrgToolbar } from "@/features/host-org/components/HostOrgToolbar.jsx"; import { HostOrgToolbar } from "@/features/host-org/components/HostOrgToolbar.jsx";
@ -25,13 +26,10 @@ const bdColumns = [
{ {
key: "salaryWallet", key: "salaryWallet",
label: "工资钱包", label: "工资钱包",
sortable: true,
sortField: "salary_wallet",
render: (item) => ( render: (item) => (
<SalaryWalletCell <SalaryWalletCell role="bd" userId={item.userId} userLabel={bdUserLabel(item)} wallet={item.salaryWallet} />
role="bd"
userId={item.userId}
userLabel={bdUserLabel(item)}
wallet={item.salaryWallet}
/>
), ),
width: "minmax(140px, 0.85fr)", width: "minmax(140px, 0.85fr)",
}, },
@ -93,47 +91,61 @@ export function HostBdsPage() {
width: "minmax(80px, 0.45fr)", width: "minmax(80px, 0.45fr)",
} }
: null, : null,
].filter(Boolean).map((column) => { ]
if (column.key === "user") { .filter(Boolean)
return { .map((column) => {
...column, if (column.key === "user") {
filter: createTextColumnFilter({ return {
placeholder: "搜索用户 ID、展示 ID、用户名", ...column,
value: page.query, filter: createTextColumnFilter({
onChange: page.changeQuery, placeholder: "搜索用户 ID、展示 ID、用户名",
}), value: page.query,
}; onChange: page.changeQuery,
} }),
if (column.key === "parentLeaderUserId") { };
return { }
...column, if (column.key === "parentLeaderUserId") {
filter: createTextColumnFilter({ return {
placeholder: "搜索 Leader 用户 ID", ...column,
value: page.parentLeaderUserId, filter: createTextColumnFilter({
onChange: page.changeParentLeaderUserId, placeholder: "搜索 Leader 用户 ID",
}), value: page.parentLeaderUserId,
}; onChange: page.changeParentLeaderUserId,
} }),
if (column.key === "status") { };
return { }
...column, if (column.key === "status") {
filter: createOptionsColumnFilter({ return {
options: bdStatusFilters, ...column,
placeholder: "搜索状态", filter: createOptionsColumnFilter({
value: page.status, options: bdStatusFilters,
onChange: page.changeStatus, placeholder: "搜索状态",
}), value: page.status,
}; onChange: page.changeStatus,
} }),
return column; };
}); }
if (column.sortable) {
return {
...column,
header: (
<HostOrgSortHeader
field={column.sortField}
label={column.label}
sortBy={page.sortBy}
sortDirection={page.sortDirection}
onToggle={page.changeSort}
/>
),
};
}
return column;
});
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}>

View File

@ -1,11 +1,9 @@
import ArrowDownwardOutlined from "@mui/icons-material/ArrowDownwardOutlined";
import ArrowUpwardOutlined from "@mui/icons-material/ArrowUpwardOutlined";
import SwapVertOutlined from "@mui/icons-material/SwapVertOutlined";
import { AdminSwitch } from "@/shared/ui/AdminSwitch.jsx"; import { AdminSwitch } from "@/shared/ui/AdminSwitch.jsx";
import { formatMillis } from "@/shared/utils/time.js"; import { formatMillis } from "@/shared/utils/time.js";
import { DataState } from "@/shared/ui/DataState.jsx"; import { DataState } from "@/shared/ui/DataState.jsx";
import { hostSourceLabels, hostStatusFilters } from "@/features/host-org/constants.js"; import { hostSourceLabels, hostStatusFilters } from "@/features/host-org/constants.js";
import { HostOrgPerson, hostOrgRegionName } from "@/features/host-org/components/HostOrgIdentity.jsx"; import { HostOrgPerson, hostOrgRegionName } from "@/features/host-org/components/HostOrgIdentity.jsx";
import { HostOrgSortHeader } from "@/features/host-org/components/HostOrgSortHeader.jsx";
import { SalaryWalletAdjustButton, SalaryWalletCell } from "@/features/host-org/components/SalaryWalletControls.jsx"; import { SalaryWalletAdjustButton, SalaryWalletCell } from "@/features/host-org/components/SalaryWalletControls.jsx";
import { HostOrgTable } from "@/features/host-org/components/HostOrgTable.jsx"; import { HostOrgTable } from "@/features/host-org/components/HostOrgTable.jsx";
import { useHostHostsPage } from "@/features/host-org/hooks/useHostHostsPage.js"; import { useHostHostsPage } from "@/features/host-org/hooks/useHostHostsPage.js";
@ -29,12 +27,16 @@ const hostColumns = [
{ {
key: "diamond", key: "diamond",
label: "钻石", label: "钻石",
sortable: true,
sortField: "diamond",
render: (item) => formatNumber(item.diamond), render: (item) => formatNumber(item.diamond),
width: "minmax(110px, 0.7fr)", width: "minmax(110px, 0.7fr)",
}, },
{ {
key: "salaryWallet", key: "salaryWallet",
label: "工资钱包", label: "工资钱包",
sortable: true,
sortField: "salary_wallet",
render: (item) => ( render: (item) => (
<SalaryWalletCell <SalaryWalletCell
role="host" role="host"
@ -91,51 +93,70 @@ export function HostHostsPage() {
width: "minmax(80px, 0.45fr)", width: "minmax(80px, 0.45fr)",
} }
: null, : null,
].filter(Boolean).map((column) => { ]
if (column.key === "user") { .filter(Boolean)
return { .map((column) => {
...column, if (column.key === "user") {
filter: createTextColumnFilter({ return {
placeholder: "搜索用户 ID、展示 ID、用户名、Agency", ...column,
value: page.query, filter: createTextColumnFilter({
onChange: page.changeQuery, placeholder: "搜索用户 ID、展示 ID、用户名、Agency",
}), value: page.query,
}; onChange: page.changeQuery,
} }),
if (column.key === "currentAgencyId") { };
return { }
...column, if (column.key === "currentAgencyId") {
filter: createTextColumnFilter({ return {
placeholder: "搜索 Agency ID", ...column,
value: page.agencyId, filter: createTextColumnFilter({
onChange: page.changeAgencyId, placeholder: "搜索 Agency ID",
}), value: page.agencyId,
}; onChange: page.changeAgencyId,
} }),
if (column.key === "status") { };
return { }
...column, if (column.key === "status") {
filter: createOptionsColumnFilter({ return {
options: hostStatusFilters, ...column,
placeholder: "搜索状态", filter: createOptionsColumnFilter({
value: page.status, options: hostStatusFilters,
onChange: page.changeStatus, placeholder: "搜索状态",
}), value: page.status,
}; onChange: page.changeStatus,
} }),
if (column.key === "diamond") { };
return { }
...column, if (column.key === "diamond") {
header: ( return {
<DiamondSortHeader ...column,
direction={page.diamondSortDirection} header: (
onToggle={page.toggleDiamondSort} <HostOrgSortHeader
/> field={column.sortField}
), label={column.label}
}; sortBy={page.sortBy}
} sortDirection={page.sortDirection}
return column; onToggle={page.changeSort}
}); />
),
};
}
if (column.sortable) {
return {
...column,
header: (
<HostOrgSortHeader
field={column.sortField}
label={column.label}
sortBy={page.sortBy}
sortDirection={page.sortDirection}
onToggle={page.changeSort}
/>
),
};
}
return column;
});
return ( return (
<section className={styles.root}> <section className={styles.root}>
@ -170,28 +191,6 @@ function formatNumber(value) {
return Number(value || 0).toLocaleString("zh-CN"); return Number(value || 0).toLocaleString("zh-CN");
} }
function DiamondSortHeader({ direction, onToggle }) {
const SortIcon =
direction === "asc" ? ArrowUpwardOutlined : direction === "desc" ? ArrowDownwardOutlined : SwapVertOutlined;
const directionLabel = direction === "asc" ? "升序" : "降序";
const nextDirectionLabel = direction === "desc" ? "升序" : "降序";
return (
<button
aria-label={`按钻石${nextDirectionLabel}排序`}
className={["admin-cell__head-trigger", direction ? "admin-cell__head-trigger--active" : ""]
.filter(Boolean)
.join(" ")}
type="button"
onClick={onToggle}
>
<SortIcon className="admin-cell__head-icon" fontSize="inherit" />
<span className="admin-cell__head-label">
钻石{direction ? <span className="admin-cell__head-filter">({directionLabel})</span> : null}
</span>
</button>
);
}
function HostUser({ item }) { function HostUser({ item }) {
return ( return (
<HostOrgPerson <HostOrgPerson

View File

@ -0,0 +1,231 @@
import { fireEvent, render, screen } from "@testing-library/react";
import { afterEach, expect, test, vi } from "vitest";
import { HostAgenciesPage } from "./HostAgenciesPage.jsx";
import { HostBdLeadersPage } from "./HostBdLeadersPage.jsx";
import { HostBdsPage } from "./HostBdsPage.jsx";
import { HostHostsPage } from "./HostHostsPage.jsx";
import { useHostAgenciesPage } from "@/features/host-org/hooks/useHostAgenciesPage.js";
import { useHostBdsPage } from "@/features/host-org/hooks/useHostBdsPage.js";
import { useHostHostsPage } from "@/features/host-org/hooks/useHostHostsPage.js";
vi.mock("@/features/host-org/hooks/useHostAgenciesPage.js", () => ({
useHostAgenciesPage: vi.fn(),
}));
vi.mock("@/features/host-org/hooks/useHostBdsPage.js", () => ({
useHostBdsPage: vi.fn(),
}));
vi.mock("@/features/host-org/hooks/useHostHostsPage.js", () => ({
useHostHostsPage: vi.fn(),
}));
afterEach(() => {
vi.clearAllMocks();
});
test("agency list salary wallet header requests salary wallet sorting", () => {
const changeSort = vi.fn();
vi.mocked(useHostAgenciesPage).mockReturnValue(agencyPageFixture({ changeSort }));
render(<HostAgenciesPage />);
fireEvent.click(screen.getByLabelText("按工资钱包降序排序"));
expect(changeSort).toHaveBeenCalledWith("salary_wallet");
});
test("bd leader list salary wallet header requests salary wallet sorting", () => {
const changeSort = vi.fn();
vi.mocked(useHostBdsPage).mockReturnValue(bdPageFixture({ changeSort, profileType: "leader" }));
render(<HostBdLeadersPage />);
fireEvent.click(screen.getByLabelText("按工资钱包降序排序"));
expect(changeSort).toHaveBeenCalledWith("salary_wallet");
});
test("bd list salary wallet header requests salary wallet sorting", () => {
const changeSort = vi.fn();
vi.mocked(useHostBdsPage).mockReturnValue(bdPageFixture({ changeSort, profileType: "bd" }));
render(<HostBdsPage />);
fireEvent.click(screen.getByLabelText("按工资钱包降序排序"));
expect(changeSort).toHaveBeenCalledWith("salary_wallet");
});
test("host list salary wallet header requests salary wallet sorting", () => {
const changeSort = vi.fn();
vi.mocked(useHostHostsPage).mockReturnValue(hostPageFixture({ changeSort }));
render(<HostHostsPage />);
fireEvent.click(screen.getByLabelText("按工资钱包降序排序"));
expect(changeSort).toHaveBeenCalledWith("salary_wallet");
});
function agencyPageFixture(patch = {}) {
return {
abilities: { canCreate: false, canDelete: false, canSalaryAdjust: false, canStatus: false },
activeAction: "",
agencyForm: { joinEnabled: true, name: "", ownerUserId: "", parentBdUserId: "", reason: "" },
changeParentBdUserId: vi.fn(),
changeQuery: vi.fn(),
changeRegionId: vi.fn(),
changeSort: vi.fn(),
changeStatus: vi.fn(),
closeAction: vi.fn(),
closeAgencyFromList: vi.fn(),
data: { items: [agencyFixture()], page: 1, pageSize: 50, total: 1 },
error: null,
loading: false,
loadingAction: "",
loadingRegions: false,
openAgencyForm: vi.fn(),
page: 1,
parentBdUserId: "",
query: "",
regionId: "",
regionOptions: [],
reload: vi.fn(),
removeAgencyFromList: vi.fn(),
resetFilters: vi.fn(),
setAgencyForm: vi.fn(),
setPage: vi.fn(),
sortBy: "",
sortDirection: "",
status: "",
submitAgency: vi.fn(),
toggleAgencyJoinEnabledFromList: vi.fn(),
...patch,
};
}
function bdPageFixture(patch = {}) {
return {
abilities: { canCreate: false, canSalaryAdjust: false, canUpdate: false, canView: true },
activeAction: "",
bdForm: { parentLeaderUserId: "", reason: "", targetUserId: "" },
bdLeaderForm: { positionAlias: "", reason: "", targetUserId: "" },
bdUserLookup: { error: "", loading: false, user: null },
changeParentLeaderUserId: vi.fn(),
changeQuery: vi.fn(),
changeRegionId: vi.fn(),
changeSort: vi.fn(),
changeStatus: vi.fn(),
closeAction: vi.fn(),
data: { items: [bdFixture()], page: 1, pageSize: 50, total: 1 },
error: null,
leaderBDs: { items: [], page: 1, pageSize: 50, total: 0 },
leaderBDsError: "",
leaderBDsLoading: false,
loadLeaderBDs: vi.fn(),
loading: false,
loadingAction: "",
loadingRegions: false,
openAddLeaderBD: vi.fn(),
openBDForm: vi.fn(),
openBDLeaderForm: vi.fn(),
openLeaderBDs: vi.fn(),
page: 1,
parentLeaderUserId: "",
profileType: "bd",
query: "",
regionId: "",
regionOptions: [],
reload: vi.fn(),
removeBDLeader: vi.fn(),
resetFilters: vi.fn(),
saveBDLeaderPositionAlias: vi.fn(),
selectedLeader: null,
setBDForm: vi.fn(),
setBDLeaderForm: vi.fn(),
setPage: vi.fn(),
sortBy: "",
sortDirection: "",
status: "",
submitBD: vi.fn(),
submitBDLeader: vi.fn(),
submitLeaderBD: vi.fn(),
toggleBD: vi.fn(),
toggleBDLeader: vi.fn(),
...patch,
};
}
function hostPageFixture(patch = {}) {
return {
abilities: { canSalaryAdjust: false, canView: true },
agencyId: "",
changeAgencyId: vi.fn(),
changeQuery: vi.fn(),
changeRegionId: vi.fn(),
changeSort: vi.fn(),
changeStatus: vi.fn(),
data: { items: [hostFixture()], page: 1, pageSize: 50, total: 1 },
diamondSortDirection: "",
error: null,
loading: false,
loadingRegions: false,
page: 1,
query: "",
regionId: "",
regionOptions: [],
reload: vi.fn(),
resetFilters: vi.fn(),
setPage: vi.fn(),
sortBy: "",
sortDirection: "",
status: "",
toggleDiamondSort: vi.fn(),
...patch,
};
}
function agencyFixture() {
return {
agencyId: "9001",
joinEnabled: true,
name: "Agency One",
ownerDisplayUserId: "160001",
ownerUserId: "1001",
ownerUsername: "agency_owner",
parentBdUserId: "2001",
regionName: "中东区",
salaryWallet: { availableAmount: 1200 },
status: "active",
updatedAtMs: 1760000000000,
};
}
function bdFixture() {
return {
createdAtMs: 1760000000000,
displayUserId: "160002",
parentLeaderUserId: "3001",
regionName: "中东区",
role: "bd",
salaryWallet: { availableAmount: 1200 },
status: "active",
subBdCount: 0,
updatedAtMs: 1760000000000,
userId: "2001",
username: "bd_user",
};
}
function hostFixture() {
return {
currentAgencyId: "9001",
diamond: 100,
displayUserId: "160003",
firstBecameHostAtMs: 1760000000000,
regionName: "中东区",
salaryWallet: { availableAmount: 1200 },
source: "agency_invite",
status: "active",
updatedAtMs: 1760000000000,
userId: "3001",
username: "host_user",
};
}