Compare commits
No commits in common. "c08f9e7a839cfbe9d0d1a73df2c819bfdc2e2839" and "0e87bb24afd1df0c1ed8940876b73aa0e45a39be" have entirely different histories.
c08f9e7a83
...
0e87bb24af
@ -881,28 +881,6 @@
|
||||
"x-permissions": ["agency:status"]
|
||||
}
|
||||
},
|
||||
"/admin/agencies/{agency_id}/status": {
|
||||
"patch": {
|
||||
"operationId": "setAgencyStatus",
|
||||
"responses": {
|
||||
"200": {
|
||||
"$ref": "#/components/responses/EmptyResponse"
|
||||
}
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "agency_id",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
],
|
||||
"x-permission": "agency:status",
|
||||
"x-permissions": ["agency:status"]
|
||||
}
|
||||
},
|
||||
"/admin/agencies/{agency_id}/delete": {
|
||||
"post": {
|
||||
"operationId": "deleteAgency",
|
||||
@ -5779,9 +5757,6 @@
|
||||
"agencyId": {
|
||||
"type": "integer"
|
||||
},
|
||||
"activeHostCount": {
|
||||
"type": "integer"
|
||||
},
|
||||
"createdAtMs": {
|
||||
"type": "integer"
|
||||
},
|
||||
@ -5874,9 +5849,6 @@
|
||||
"type": "object",
|
||||
"required": ["userId"],
|
||||
"properties": {
|
||||
"agencyCount": {
|
||||
"type": "integer"
|
||||
},
|
||||
"createdAtMs": {
|
||||
"type": "integer"
|
||||
},
|
||||
|
||||
@ -8,7 +8,6 @@ import { LuckyGiftPoolModal } from "./components/LuckyGiftPoolModal.jsx";
|
||||
import { MetricCard } from "./components/MetricCard.jsx";
|
||||
import { MetricTrendModal } from "./components/MetricTrendModal.jsx";
|
||||
import { ReportOverview } from "./components/ReportOverview.jsx";
|
||||
import { EmptyReportSection, ReportSidebar } from "./components/ReportSidebar.jsx";
|
||||
import { RevenueTrendPanel } from "./components/RevenueTrendPanel.jsx";
|
||||
import { SelfGameScreen } from "./components/SelfGameScreen.jsx";
|
||||
import { createDashboardModel } from "./data/createDashboardModel.js";
|
||||
@ -29,8 +28,6 @@ export function DatabiApp() {
|
||||
const [regionId, setRegionId] = useState("all");
|
||||
const [appCode, setAppCode] = useState(() => getCurrentAppCode());
|
||||
const [viewMode, setViewMode] = useState("report");
|
||||
// 侧边栏目前只有数据报表接入真实业务流,财务和运营保持空组件,避免误触发数据报表请求。
|
||||
const [activeReportSection, setActiveReportSection] = useState("data");
|
||||
const [activeScreen, setActiveScreen] = useState("overview");
|
||||
const [gameId, setGameId] = useState("all");
|
||||
const [filterOptions, setFilterOptions] = useState(initialFilterOptions);
|
||||
@ -50,7 +47,6 @@ export function DatabiApp() {
|
||||
const scopedCountries = useMemo(() => {
|
||||
return filterOptions.countryOptions.filter((country) => Number(country.id) > 0 && countryBelongsToRegion(country, regionId));
|
||||
}, [filterOptions.countryOptions, regionId]);
|
||||
const isDataReportSection = activeReportSection === "data";
|
||||
|
||||
const handleAppChange = useCallback((value) => {
|
||||
setAppCode(setCurrentAppCode(value));
|
||||
@ -174,18 +170,10 @@ export function DatabiApp() {
|
||||
}, [appCode, countryId, gameId, range, regionId, timeZone]);
|
||||
|
||||
useEffect(() => {
|
||||
// 非数据报表没有筛选控件,提前停止筛选项加载,避免空报表页产生无意义网络请求。
|
||||
if (!isDataReportSection) {
|
||||
return undefined;
|
||||
}
|
||||
void loadFilterOptions();
|
||||
}, [isDataReportSection, loadFilterOptions]);
|
||||
}, [loadFilterOptions]);
|
||||
|
||||
useEffect(() => {
|
||||
// 当前数据轮询只属于数据报表;切到财务或运营时清理定时器并保留已有数据,方便切回。
|
||||
if (!isDataReportSection) {
|
||||
return undefined;
|
||||
}
|
||||
if (activeScreen === "selfGames") {
|
||||
void loadSelfGameOverview();
|
||||
const timer = window.setInterval(() => void loadSelfGameOverview({ preserveData: true }), 60_000);
|
||||
@ -194,18 +182,15 @@ export function DatabiApp() {
|
||||
void loadOverview();
|
||||
const timer = window.setInterval(() => void loadOverview({ preserveData: true }), 60_000);
|
||||
return () => window.clearInterval(timer);
|
||||
}, [activeScreen, isDataReportSection, loadOverview, loadSelfGameOverview]);
|
||||
}, [activeScreen, loadOverview, loadSelfGameOverview]);
|
||||
|
||||
const handleRefresh = useCallback(() => {
|
||||
if (!isDataReportSection) {
|
||||
return;
|
||||
}
|
||||
if (activeScreen === "selfGames") {
|
||||
void loadSelfGameOverview({ preserveData: true });
|
||||
return;
|
||||
}
|
||||
void loadOverview({ preserveData: true });
|
||||
}, [activeScreen, isDataReportSection, loadOverview, loadSelfGameOverview]);
|
||||
}, [activeScreen, loadOverview, loadSelfGameOverview]);
|
||||
|
||||
const model = useMemo(() => createDashboardModel(overview, { appCode, countryId, range, timeZone }), [appCode, countryId, overview, range, timeZone]);
|
||||
const showSkeleton = loading && !overview;
|
||||
@ -216,74 +201,63 @@ export function DatabiApp() {
|
||||
|
||||
return (
|
||||
<main className={`databi-shell databi-shell--${viewMode}`}>
|
||||
<ReportSidebar activeSection={activeReportSection} onSectionChange={setActiveReportSection} />
|
||||
<div className="databi-shell-content">
|
||||
{isDataReportSection ? (
|
||||
<section className={[
|
||||
"databi-screen",
|
||||
isReportView ? "databi-screen--report" : "databi-screen--bigscreen",
|
||||
activeScreen === "selfGames" ? "databi-screen--self-game" : ""
|
||||
].filter(Boolean).join(" ")}>
|
||||
<DatabiHeader
|
||||
activeScreen={activeScreen}
|
||||
appCode={appCode}
|
||||
appOptions={filterOptions.appOptions}
|
||||
countryOptions={filterOptions.countryOptions}
|
||||
countryId={countryId}
|
||||
error={activeScreen === "selfGames" ? selfGameError : error}
|
||||
filtersLoading={filtersLoading}
|
||||
loading={activeScreen === "selfGames" ? selfGameLoading : loading}
|
||||
onAppChange={handleAppChange}
|
||||
onCountryChange={setCountryId}
|
||||
onRefresh={handleRefresh}
|
||||
onRangeChange={setRange}
|
||||
onRegionChange={handleRegionChange}
|
||||
onScreenChange={setActiveScreen}
|
||||
onTimeZoneChange={handleTimeZoneChange}
|
||||
range={range}
|
||||
regionId={regionId}
|
||||
regionOptions={filterOptions.regionOptions}
|
||||
timeZone={timeZone}
|
||||
timeZoneOptions={TIME_ZONE_OPTIONS}
|
||||
updatedAt={model.updatedAt}
|
||||
viewMode={viewMode}
|
||||
onViewModeChange={setViewMode}
|
||||
/>
|
||||
<section className={[
|
||||
"databi-screen",
|
||||
isReportView ? "databi-screen--report" : "databi-screen--bigscreen",
|
||||
activeScreen === "selfGames" ? "databi-screen--self-game" : ""
|
||||
].filter(Boolean).join(" ")}>
|
||||
<DatabiHeader
|
||||
activeScreen={activeScreen}
|
||||
appCode={appCode}
|
||||
appOptions={filterOptions.appOptions}
|
||||
countryOptions={filterOptions.countryOptions}
|
||||
countryId={countryId}
|
||||
error={activeScreen === "selfGames" ? selfGameError : error}
|
||||
filtersLoading={filtersLoading}
|
||||
loading={activeScreen === "selfGames" ? selfGameLoading : loading}
|
||||
onAppChange={handleAppChange}
|
||||
onCountryChange={setCountryId}
|
||||
onRefresh={handleRefresh}
|
||||
onRangeChange={setRange}
|
||||
onRegionChange={handleRegionChange}
|
||||
onScreenChange={setActiveScreen}
|
||||
onTimeZoneChange={handleTimeZoneChange}
|
||||
range={range}
|
||||
regionId={regionId}
|
||||
regionOptions={filterOptions.regionOptions}
|
||||
timeZone={timeZone}
|
||||
timeZoneOptions={TIME_ZONE_OPTIONS}
|
||||
updatedAt={model.updatedAt}
|
||||
viewMode={viewMode}
|
||||
onViewModeChange={setViewMode}
|
||||
/>
|
||||
|
||||
{activeScreen === "selfGames" ? (
|
||||
<SelfGameScreen gameId={gameId} loading={selfGameLoading && !selfGameOverview} onGameChange={setGameId} overview={selfGameOverview} />
|
||||
) : isReportView ? (
|
||||
<ReportOverview
|
||||
loading={showSkeleton}
|
||||
model={model}
|
||||
onMetricTrend={setTrendModalItem}
|
||||
onOpenLuckyGiftPools={openLuckyGiftPools}
|
||||
/>
|
||||
) : (
|
||||
<BigscreenOverview
|
||||
loading={showSkeleton}
|
||||
model={model}
|
||||
onMetricTrend={setTrendModalItem}
|
||||
onOpenLuckyGiftPools={openLuckyGiftPools}
|
||||
/>
|
||||
)}
|
||||
</section>
|
||||
{activeScreen === "selfGames" ? (
|
||||
<SelfGameScreen gameId={gameId} loading={selfGameLoading && !selfGameOverview} onGameChange={setGameId} overview={selfGameOverview} />
|
||||
) : isReportView ? (
|
||||
<ReportOverview
|
||||
loading={showSkeleton}
|
||||
model={model}
|
||||
onMetricTrend={setTrendModalItem}
|
||||
onOpenLuckyGiftPools={openLuckyGiftPools}
|
||||
/>
|
||||
) : (
|
||||
<EmptyReportSection section={activeReportSection} />
|
||||
<BigscreenOverview
|
||||
loading={showSkeleton}
|
||||
model={model}
|
||||
onMetricTrend={setTrendModalItem}
|
||||
onOpenLuckyGiftPools={openLuckyGiftPools}
|
||||
/>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{isDataReportSection && activeScreen === "overview" && !isReportView ? (
|
||||
<>
|
||||
<CountryPerformanceTable gameRanking={model.gameRanking} giftRanking={model.giftRanking} items={model.countryBreakdown} loading={showSkeleton} />
|
||||
</>
|
||||
) : null}
|
||||
{isDataReportSection && activeScreen === "overview" && poolModalOpen ? (
|
||||
<LuckyGiftPoolModal items={model.luckyGiftPools} onClose={() => setPoolModalOpen(false)} />
|
||||
) : null}
|
||||
{isDataReportSection && activeScreen === "overview" && trendModalItem ? (
|
||||
<MetricTrendModal item={trendModalItem} onClose={() => setTrendModalItem(null)} />
|
||||
) : null}
|
||||
</div>
|
||||
{activeScreen === "overview" && !isReportView ? (
|
||||
<>
|
||||
<CountryPerformanceTable gameRanking={model.gameRanking} giftRanking={model.giftRanking} items={model.countryBreakdown} loading={showSkeleton} />
|
||||
</>
|
||||
) : null}
|
||||
{activeScreen === "overview" && poolModalOpen ? <LuckyGiftPoolModal items={model.luckyGiftPools} onClose={() => setPoolModalOpen(false)} /> : null}
|
||||
{activeScreen === "overview" && trendModalItem ? <MetricTrendModal item={trendModalItem} onClose={() => setTrendModalItem(null)} /> : null}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@ -99,48 +99,6 @@ test("renders coin seller stock and outbound coin columns in report view", async
|
||||
expect(screen.getAllByText("88,000").length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("switches report sidebar sections and stops data report polling on empty sections", async () => {
|
||||
fetchStatisticsOverview.mockResolvedValue({ active_users: 10, paid_users: 2, recharge_usd_minor: 100, updated_at_ms: 1 });
|
||||
|
||||
render(<DatabiApp />);
|
||||
|
||||
await flushEffects();
|
||||
|
||||
const reportTypeTabs = screen.getByRole("tablist", { name: "报表类型" });
|
||||
expect(within(reportTypeTabs).getByRole("tab", { name: "数据报表" })).toHaveAttribute("aria-selected", "true");
|
||||
expect(screen.getByRole("region", { name: "数据报表" })).toBeTruthy();
|
||||
|
||||
await act(async () => {
|
||||
within(reportTypeTabs).getByRole("tab", { name: "财务报表" }).click();
|
||||
});
|
||||
|
||||
expect(within(reportTypeTabs).getByRole("tab", { name: "财务报表" })).toHaveAttribute("aria-selected", "true");
|
||||
expect(screen.getByRole("region", { name: "财务报表" })).toBeTruthy();
|
||||
expect(screen.queryByRole("region", { name: "数据报表" })).toBeNull();
|
||||
const requestCountAfterFinanceSwitch = fetchStatisticsOverview.mock.calls.length;
|
||||
|
||||
await act(async () => {
|
||||
vi.advanceTimersByTime(60_000);
|
||||
});
|
||||
|
||||
expect(fetchStatisticsOverview).toHaveBeenCalledTimes(requestCountAfterFinanceSwitch);
|
||||
|
||||
await act(async () => {
|
||||
within(reportTypeTabs).getByRole("tab", { name: "运营报表" }).click();
|
||||
});
|
||||
|
||||
expect(within(reportTypeTabs).getByRole("tab", { name: "运营报表" })).toHaveAttribute("aria-selected", "true");
|
||||
expect(screen.getByRole("region", { name: "运营报表" })).toBeTruthy();
|
||||
|
||||
await act(async () => {
|
||||
within(reportTypeTabs).getByRole("tab", { name: "数据报表" }).click();
|
||||
});
|
||||
await flushEffects();
|
||||
|
||||
expect(screen.getByRole("region", { name: "数据报表" })).toBeTruthy();
|
||||
expect(fetchStatisticsOverview.mock.calls.length).toBeGreaterThan(requestCountAfterFinanceSwitch);
|
||||
});
|
||||
|
||||
test("loads self game statistics from the big screen switch", async () => {
|
||||
fetchStatisticsOverview.mockResolvedValue({ active_users: 10, paid_users: 2, recharge_usd_minor: 100, updated_at_ms: 1 });
|
||||
fetchSelfGameStatisticsOverview.mockResolvedValue({
|
||||
|
||||
@ -2,6 +2,7 @@ import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import CalendarMonthOutlined from "@mui/icons-material/CalendarMonthOutlined";
|
||||
import KeyboardArrowDownOutlined from "@mui/icons-material/KeyboardArrowDownOutlined";
|
||||
import PublicOutlined from "@mui/icons-material/PublicOutlined";
|
||||
import StorageOutlined from "@mui/icons-material/StorageOutlined";
|
||||
import { getTimeZoneLabel, lastDaysRange, rangeEndMs, rangeStartMs, sameRange, thisMonthRange, TIME_ZONE_OPTIONS, todayRange } from "../utils/time.js";
|
||||
|
||||
const defaultCountryOptions = [{ countryCode: "", id: 0, label: "全部国家", regionIds: [], searchText: "全部国家 all", value: 0 }];
|
||||
@ -81,6 +82,9 @@ export function DatabiHeader({
|
||||
<header className="databi-header" ref={headerRef}>
|
||||
<div className="databi-header-left">
|
||||
<div className="brand-lockup">
|
||||
<div className="brand-emblem" aria-hidden="true">
|
||||
<StorageOutlined fontSize="small" />
|
||||
</div>
|
||||
<div>
|
||||
<h1>数据中心</h1>
|
||||
</div>
|
||||
|
||||
@ -15,7 +15,7 @@ const reportTabs = [
|
||||
];
|
||||
|
||||
const countryColumns = [
|
||||
{ align: "left", key: "date_label", label: "日期", render: (row) => <DateCell row={row} />, sortable: false, stickyLeft: true },
|
||||
{ align: "left", key: "date_label", label: "日期", render: (row) => <DateCell row={row} />, sortable: false },
|
||||
{ align: "left", key: "country", label: "国家", render: (row) => <CountryCell row={row} />, sortable: false },
|
||||
{ key: "registrations", label: "注册", render: (row) => formatOptionalCoin(row.registrations), sortable: false },
|
||||
{ key: "active_users", label: "活跃用户", render: (row) => formatOptionalCoin(row.active_users), sortable: false },
|
||||
@ -315,12 +315,8 @@ function ReportTable({ columns, defaultSort, loading, preserveOrder = false, ren
|
||||
{columns.map((column) => {
|
||||
const isSortable = !preserveOrder && column.sortable !== false;
|
||||
const isActive = sortState?.key === column.key;
|
||||
const headerClassName = [
|
||||
isSortable ? "is-sortable" : "",
|
||||
column.stickyLeft ? "is-sticky-left" : ""
|
||||
].filter(Boolean).join(" ");
|
||||
return (
|
||||
<th className={headerClassName} key={column.key}>
|
||||
<th className={isSortable ? "is-sortable" : ""} key={column.key}>
|
||||
{isSortable ? (
|
||||
<button
|
||||
className={["report-sort-button", column.align === "left" ? "report-sort-button--left" : "", isActive ? "is-active" : ""].filter(Boolean).join(" ")}
|
||||
@ -353,15 +349,7 @@ function ReportTable({ columns, defaultSort, loading, preserveOrder = false, ren
|
||||
>
|
||||
<td>{reportRowIndex(row, index)}</td>
|
||||
{columns.map((column) => (
|
||||
<td
|
||||
className={[
|
||||
column.align === "left" ? "is-left" : "",
|
||||
column.stickyLeft ? "is-sticky-left" : ""
|
||||
].filter(Boolean).join(" ")}
|
||||
key={column.key}
|
||||
>
|
||||
{column.render ? column.render(row) : row[column.key]}
|
||||
</td>
|
||||
<td className={column.align === "left" ? "is-left" : ""} key={column.key}>{column.render ? column.render(row) : row[column.key]}</td>
|
||||
))}
|
||||
</tr>
|
||||
{row.expanded && renderExpandedRow ? (
|
||||
|
||||
@ -41,15 +41,11 @@ test("shows daily totals first and expands daily country details on demand", asy
|
||||
recharge_usd_minor: 35000
|
||||
}, { appCode: "lalu", countryId: 0, range: { end: "2026-06-06", start: "2026-06-05" } });
|
||||
|
||||
const { container } = render(<ReportOverview model={model} />);
|
||||
render(<ReportOverview model={model} />);
|
||||
|
||||
expect(screen.queryByText("巴西")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("沙特")).not.toBeInTheDocument();
|
||||
|
||||
const stickyDateHeader = container.querySelector(".report-table--wide th.is-sticky-left");
|
||||
expect(stickyDateHeader).toHaveTextContent("日期");
|
||||
expect(container.querySelector(".report-table--wide td.is-sticky-left .report-date-cell")).toBeTruthy();
|
||||
|
||||
const june5Row = screen.getByRole("button", { name: "展开 2026-06-05 国家明细" });
|
||||
expect(june5Row).toHaveAttribute("aria-expanded", "false");
|
||||
|
||||
@ -58,7 +54,6 @@ test("shows daily totals first and expands daily country details on demand", asy
|
||||
expect(june5Row).toHaveAttribute("aria-expanded", "true");
|
||||
const detailRows = within(screen.getByRole("table", { name: "2026-06-05 国家明细" })).getAllByRole("row");
|
||||
expect(detailRows).toHaveLength(3);
|
||||
expect(container.querySelector(".report-subtable .is-sticky-left")).toBeNull();
|
||||
expect(screen.getByText("巴西")).toBeInTheDocument();
|
||||
expect(screen.getByText("沙特")).toBeInTheDocument();
|
||||
|
||||
|
||||
@ -1,44 +0,0 @@
|
||||
import AccountBalanceWalletOutlined from "@mui/icons-material/AccountBalanceWalletOutlined";
|
||||
import AssessmentOutlined from "@mui/icons-material/AssessmentOutlined";
|
||||
import CampaignOutlined from "@mui/icons-material/CampaignOutlined";
|
||||
import StorageOutlined from "@mui/icons-material/StorageOutlined";
|
||||
|
||||
const reportSections = [
|
||||
{ icon: AssessmentOutlined, key: "data", label: "数据报表" },
|
||||
{ icon: AccountBalanceWalletOutlined, key: "finance", label: "财务报表" },
|
||||
{ icon: CampaignOutlined, key: "operations", label: "运营报表" }
|
||||
];
|
||||
|
||||
export function ReportSidebar({ activeSection = "data", onSectionChange }) {
|
||||
return (
|
||||
<aside className="databi-report-sidebar" aria-label="报表侧边栏">
|
||||
<div className="report-sidebar-brand" aria-hidden="true">
|
||||
<StorageOutlined fontSize="small" />
|
||||
</div>
|
||||
<nav className="report-sidebar-nav" role="tablist" aria-label="报表类型">
|
||||
{reportSections.map((section) => {
|
||||
const Icon = section.icon;
|
||||
const isActive = activeSection === section.key;
|
||||
return (
|
||||
<button
|
||||
aria-selected={isActive}
|
||||
className={isActive ? "report-sidebar-item is-active" : "report-sidebar-item"}
|
||||
key={section.key}
|
||||
onClick={() => onSectionChange?.(section.key)}
|
||||
role="tab"
|
||||
type="button"
|
||||
>
|
||||
<Icon fontSize="small" />
|
||||
<span>{section.label}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
export function EmptyReportSection({ section }) {
|
||||
const label = reportSections.find((item) => item.key === section)?.label || "报表";
|
||||
return <section className="databi-empty-report-section" aria-label={label} />;
|
||||
}
|
||||
@ -337,8 +337,6 @@ function summarizeReportRows(rows) {
|
||||
"coin_seller_recharge_usd_minor",
|
||||
"coin_seller_stock_coin",
|
||||
"coin_seller_transfer_coin",
|
||||
"coin_total",
|
||||
"consumed_coin",
|
||||
"game_payout",
|
||||
"game_refund",
|
||||
"game_turnover",
|
||||
@ -347,27 +345,19 @@ function summarizeReportRows(rows) {
|
||||
"lucky_gift_payout",
|
||||
"lucky_gift_turnover",
|
||||
"mifapay_recharge_usd_minor",
|
||||
"mic_online_ms",
|
||||
"mic_online_users",
|
||||
"new_user_recharge_usd_minor",
|
||||
"paid_users",
|
||||
"platform_grant_coin",
|
||||
"recharge_usd_minor",
|
||||
"recharge_users",
|
||||
"registrations",
|
||||
"manual_grant_coin",
|
||||
"salary_usd_minor",
|
||||
"salary_transfer_coin",
|
||||
"super_lucky_gift_payout",
|
||||
"super_lucky_gift_turnover"
|
||||
];
|
||||
const summary = fields.reduce((summary, key) => {
|
||||
return fields.reduce((summary, key) => {
|
||||
summary[key] = rows.reduce((sum, row) => sum + numberValue(row[key]), 0);
|
||||
return summary;
|
||||
}, {});
|
||||
// 日期父行是多个国家行的合计,平均麦上时间必须按合计后的总时长/人数重算,不能把国家平均值相加。
|
||||
summary.avg_mic_online_ms = Math.round(ratio(summary.mic_online_ms, summary.mic_online_users));
|
||||
return summary;
|
||||
}
|
||||
|
||||
function isMultiDayRange(range) {
|
||||
@ -394,16 +384,12 @@ function normalizeCountryRow(item, index, totalRecharge) {
|
||||
const superLuckyGiftPayout = readOptionalNumber(item, "super_lucky_gift_payout", "superLuckyGiftPayout");
|
||||
const superLuckyGiftProfit = readOptionalNumber(item, "super_lucky_gift_profit", "superLuckyGiftProfit") ??
|
||||
(superLuckyGiftTurnover === null || superLuckyGiftPayout === null ? null : superLuckyGiftTurnover - superLuckyGiftPayout);
|
||||
const micOnlineMS = readOptionalNumber(item, "mic_online_ms", "micOnlineMs");
|
||||
const micOnlineUsers = readOptionalNumber(item, "mic_online_users", "micOnlineUsers");
|
||||
const avgMicOnlineMS = readOptionalNumber(item, "avg_mic_online_ms", "avgMicOnlineMs") ??
|
||||
(micOnlineMS === null || micOnlineUsers === null ? null : Math.round(ratio(micOnlineMS, micOnlineUsers)));
|
||||
return {
|
||||
active_users: activeUsers,
|
||||
arpu_usd_minor: numberValue(item.arpu_usd_minor ?? item.arpuUsdMinor),
|
||||
arppu_usd_minor: numberValue(item.arppu_usd_minor),
|
||||
avg_recharge_usd_minor: numberValue(item.avg_recharge_usd_minor ?? item.avgRechargeUsdMinor),
|
||||
avg_mic_online_ms: avgMicOnlineMS,
|
||||
avg_mic_online_ms: readOptionalNumber(item, "avg_mic_online_ms", "avgMicOnlineMs"),
|
||||
coin_seller_recharge_usd_minor: readNumber(item, "coin_seller_recharge_usd_minor", "coinSellerRechargeUsdMinor"),
|
||||
coin_seller_stock_coin: readOptionalNumber(item, "coin_seller_stock_coin", "coinSellerStockCoin"),
|
||||
coin_seller_transfer_coin: numberValue(item.coin_seller_transfer_coin ?? item.coinSellerTransferCoin),
|
||||
@ -426,8 +412,7 @@ function normalizeCountryRow(item, index, totalRecharge) {
|
||||
lucky_gift_profit_rate: readOptionalNumber(item, "lucky_gift_profit_rate", "luckyGiftProfitRate") ?? ratio(luckyGiftProfit, luckyGiftTurnover),
|
||||
lucky_gift_turnover: luckyGiftTurnover,
|
||||
manual_grant_coin: readOptionalNumber(item, "manual_grant_coin", "manualGrantCoin"),
|
||||
mic_online_ms: micOnlineMS,
|
||||
mic_online_users: micOnlineUsers,
|
||||
mic_online_ms: readOptionalNumber(item, "mic_online_ms", "micOnlineMs"),
|
||||
mifapay_recharge_usd_minor: readNumber(item, "mifapay_recharge_usd_minor", "mifapayRechargeUsdMinor", "mifa_pay_recharge_usd_minor", "mifaPayRechargeUsdMinor"),
|
||||
new_user_recharge_usd_minor: numberValue(item.new_user_recharge_usd_minor ?? item.newUserRechargeUsdMinor),
|
||||
paid_users: paidUsers,
|
||||
@ -482,21 +467,14 @@ function normalizeDailySeries(source) {
|
||||
const gameProfit = readNumber(item, "game_profit", "gameProfit");
|
||||
const luckyGiftTurnover = readNumber(item, "lucky_gift_turnover", "luckyGiftTurnover", "room_lucky_gift_turnover", "roomLuckyGiftTurnover");
|
||||
const luckyGiftPayout = readNumber(item, "lucky_gift_payout", "luckyGiftPayout");
|
||||
const micOnlineMS = readOptionalNumber(item, "mic_online_ms", "micOnlineMs");
|
||||
const micOnlineUsers = readOptionalNumber(item, "mic_online_users", "micOnlineUsers");
|
||||
const avgMicOnlineMS = readOptionalNumber(item, "avg_mic_online_ms", "avgMicOnlineMs") ??
|
||||
(micOnlineMS === null || micOnlineUsers === null ? null : Math.round(ratio(micOnlineMS, micOnlineUsers)));
|
||||
return {
|
||||
active_users: readNumber(item, "active_users", "activeUsers"),
|
||||
arppu_usd_minor: readNumber(item, "arppu_usd_minor", "arppuUsdMinor"),
|
||||
arpu_usd_minor: readNumber(item, "arpu_usd_minor", "arpuUsdMinor"),
|
||||
avg_mic_online_ms: avgMicOnlineMS,
|
||||
coin_seller: coinSeller,
|
||||
coin_seller_recharge_usd_minor: coinSeller,
|
||||
coin_seller_stock_coin: readOptionalNumber(item, "coin_seller_stock_coin", "coinSellerStockCoin"),
|
||||
coin_seller_transfer_coin: readNumber(item, "coin_seller_transfer_coin", "coinSellerTransferCoin"),
|
||||
coin_total: readOptionalNumber(item, "coin_total", "coinTotal"),
|
||||
consumed_coin: readOptionalNumber(item, "consumed_coin", "consumedCoin"),
|
||||
game_profit: gameProfit,
|
||||
game_turnover: gameTurnover,
|
||||
gift_coin_spent: readNumber(item, "gift_coin_spent", "giftCoinSpent"),
|
||||
@ -506,18 +484,13 @@ function normalizeDailySeries(source) {
|
||||
lucky_gift_payout: luckyGiftPayout,
|
||||
lucky_gift_profit: readNumber(item, "lucky_gift_profit", "luckyGiftProfit") || (luckyGiftTurnover - luckyGiftPayout),
|
||||
lucky_gift_turnover: luckyGiftTurnover,
|
||||
manual_grant_coin: readOptionalNumber(item, "manual_grant_coin", "manualGrantCoin"),
|
||||
mic_online_ms: micOnlineMS,
|
||||
mic_online_users: micOnlineUsers,
|
||||
mifapay,
|
||||
new_paid_users: readNumber(item, "new_paid_users", "newPaidUsers"),
|
||||
new_user_recharge_usd_minor: readNumber(item, "new_user_recharge_usd_minor", "newUserRechargeUsdMinor"),
|
||||
new_users: readNumber(item, "new_users", "newUsers"),
|
||||
paid_users: readNumber(item, "paid_users", "paidUsers"),
|
||||
platform_grant_coin: readOptionalNumber(item, "platform_grant_coin", "platformGrantCoin"),
|
||||
recharge_usd_minor: recharge,
|
||||
recharge_users: readNumber(item, "recharge_users", "rechargeUsers", "paid_users", "paidUsers"),
|
||||
salary_usd_minor: readOptionalNumber(item, "salary_usd_minor", "salaryUsdMinor"),
|
||||
salary_transfer_coin: readOptionalNumber(item, "salary_transfer_coin", "salaryTransferCoin"),
|
||||
stat_day: item.stat_day || item.statDay || item.day || item.label || "",
|
||||
total: recharge,
|
||||
|
||||
@ -138,18 +138,11 @@ test("builds report rows with range total, daily totals, and daily country rows"
|
||||
active_users: 10,
|
||||
coin_seller_stock_coin: 800,
|
||||
coin_seller_transfer_coin: 500,
|
||||
coin_total: 1_200,
|
||||
consumed_coin: 300,
|
||||
country: "巴西",
|
||||
country_id: 86,
|
||||
game_turnover: 300,
|
||||
lucky_gift_turnover: 200,
|
||||
manual_grant_coin: 20,
|
||||
mic_online_ms: 6_000,
|
||||
mic_online_users: 3,
|
||||
platform_grant_coin: 150,
|
||||
recharge_usd_minor: 100,
|
||||
salary_usd_minor: 900,
|
||||
salary_transfer_coin: 700,
|
||||
stat_day: "2026-06-05",
|
||||
super_lucky_gift_turnover: 80,
|
||||
@ -164,19 +157,7 @@ test("builds report rows with range total, daily totals, and daily country rows"
|
||||
}
|
||||
],
|
||||
daily_series: [
|
||||
{
|
||||
active_users: 10,
|
||||
coin_total: 1_200,
|
||||
consumed_coin: 300,
|
||||
label: "2026-06-05",
|
||||
manual_grant_coin: 20,
|
||||
mic_online_ms: 6_000,
|
||||
mic_online_users: 3,
|
||||
platform_grant_coin: 150,
|
||||
recharge_usd_minor: 100,
|
||||
salary_usd_minor: 900,
|
||||
stat_day: "2026-06-05"
|
||||
},
|
||||
{ active_users: 10, label: "2026-06-05", recharge_usd_minor: 100, stat_day: "2026-06-05" },
|
||||
{ active_users: 20, label: "2026-06-06", recharge_usd_minor: 200, stat_day: "2026-06-06" }
|
||||
],
|
||||
recharge_usd_minor: 300
|
||||
@ -184,31 +165,14 @@ test("builds report rows with range total, daily totals, and daily country rows"
|
||||
|
||||
expect(model.reportCountryRows.map((row) => row.row_type)).toEqual(["total", "date_total", "date_total"]);
|
||||
expect(model.reportCountryRows[0]).toEqual(expect.objectContaining({ country: "总计", date_label: "全部" }));
|
||||
expect(model.reportCountryRows[1]).toEqual(expect.objectContaining({
|
||||
avg_mic_online_ms: 2_000,
|
||||
coin_total: 1_200,
|
||||
consumed_coin: 300,
|
||||
country: "总计",
|
||||
date_label: "2026-06-05",
|
||||
manual_grant_coin: 20,
|
||||
mic_online_ms: 6_000,
|
||||
platform_grant_coin: 150,
|
||||
salary_usd_minor: 900
|
||||
}));
|
||||
expect(model.reportCountryRows[1]).toEqual(expect.objectContaining({ country: "总计", date_label: "2026-06-05" }));
|
||||
expect(model.reportCountryRows[1].children).toHaveLength(1);
|
||||
expect(model.reportCountryRows[1].children[0]).toEqual(expect.objectContaining({
|
||||
avg_mic_online_ms: 2_000,
|
||||
coin_seller_stock_coin: 800,
|
||||
coin_seller_transfer_coin: 500,
|
||||
coin_total: 1_200,
|
||||
consumed_coin: 300,
|
||||
country: "巴西",
|
||||
game_turnover: 300,
|
||||
lucky_gift_turnover: 200,
|
||||
manual_grant_coin: 20,
|
||||
mic_online_ms: 6_000,
|
||||
platform_grant_coin: 150,
|
||||
salary_usd_minor: 900,
|
||||
salary_transfer_coin: 700,
|
||||
super_lucky_gift_profit: 60
|
||||
}));
|
||||
|
||||
@ -1,91 +1,8 @@
|
||||
.databi-shell {
|
||||
display: grid;
|
||||
grid-template-columns: 80px minmax(0, 1fr);
|
||||
width: 100vw;
|
||||
min-width: 1280px;
|
||||
min-height: 720px;
|
||||
padding: 0 24px 24px 0;
|
||||
}
|
||||
|
||||
.databi-shell-content {
|
||||
min-width: 0;
|
||||
padding-left: 24px;
|
||||
}
|
||||
|
||||
.databi-report-sidebar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 8;
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
min-height: 720px;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 18px;
|
||||
padding: 18px 8px;
|
||||
border-right: 1px solid rgba(34, 121, 170, 0.46);
|
||||
background: rgba(2, 15, 31, 0.84);
|
||||
}
|
||||
|
||||
.report-sidebar-brand {
|
||||
display: grid;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
flex: 0 0 auto;
|
||||
place-items: center;
|
||||
border: 1px solid rgba(39, 228, 245, 0.7);
|
||||
border-radius: 50%;
|
||||
background: rgba(11, 45, 70, 0.82);
|
||||
box-shadow: inset 0 0 12px rgba(39, 228, 245, 0.16);
|
||||
}
|
||||
|
||||
.report-sidebar-brand svg {
|
||||
width: 23px;
|
||||
height: 23px;
|
||||
color: #27e4f5;
|
||||
filter: drop-shadow(0 0 8px rgba(39, 228, 245, 0.38));
|
||||
}
|
||||
|
||||
.report-sidebar-nav {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.report-sidebar-item {
|
||||
display: grid;
|
||||
min-height: 66px;
|
||||
place-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 4px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
color: #9eb7ce;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
font-size: 12px;
|
||||
font-weight: 760;
|
||||
letter-spacing: 0;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.report-sidebar-item svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.report-sidebar-item:hover,
|
||||
.report-sidebar-item.is-active {
|
||||
border-color: rgba(37, 216, 245, 0.38);
|
||||
background: rgba(37, 216, 245, 0.18);
|
||||
color: #e9f6ff;
|
||||
box-shadow: inset 0 0 0 1px rgba(37, 216, 245, 0.18);
|
||||
}
|
||||
|
||||
.report-sidebar-item.is-active:hover {
|
||||
border-color: rgba(37, 216, 245, 0.56);
|
||||
background: rgba(37, 216, 245, 0.22);
|
||||
padding: 0 24px 24px;
|
||||
}
|
||||
|
||||
.databi-screen {
|
||||
|
||||
@ -44,45 +44,11 @@
|
||||
.databi-shell--report {
|
||||
min-width: 1180px;
|
||||
min-height: 100vh;
|
||||
padding: 0 24px 28px 0;
|
||||
padding: 0 24px 28px;
|
||||
background: #f5f7fb;
|
||||
color: #172033;
|
||||
}
|
||||
|
||||
.databi-shell--report .databi-report-sidebar {
|
||||
border-right-color: #dfe8f2;
|
||||
background: #ffffff;
|
||||
box-shadow: 8px 0 26px rgba(15, 23, 42, 0.04);
|
||||
}
|
||||
|
||||
.databi-shell--report .report-sidebar-brand {
|
||||
border-color: #b9dcf4;
|
||||
background: #eef8ff;
|
||||
box-shadow: inset 0 0 0 1px rgba(22, 136, 217, 0.04);
|
||||
}
|
||||
|
||||
.databi-shell--report .report-sidebar-brand svg {
|
||||
color: #1688d9;
|
||||
filter: none;
|
||||
}
|
||||
|
||||
.databi-shell--report .report-sidebar-item {
|
||||
color: #5f6f86;
|
||||
}
|
||||
|
||||
.databi-shell--report .report-sidebar-item:hover,
|
||||
.databi-shell--report .report-sidebar-item.is-active {
|
||||
border-color: #d8e5f0;
|
||||
background: #eef7ff;
|
||||
color: #126fb2;
|
||||
box-shadow: 0 1px 3px rgba(15, 23, 42, 0.08);
|
||||
}
|
||||
|
||||
.databi-shell--report .report-sidebar-item.is-active:hover {
|
||||
border-color: #1688d9;
|
||||
background: #e9f5ff;
|
||||
}
|
||||
|
||||
.databi-shell--report .databi-header {
|
||||
height: auto;
|
||||
min-height: 72px;
|
||||
@ -279,10 +245,6 @@
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.databi-empty-report-section {
|
||||
min-height: calc(100vh - 28px);
|
||||
}
|
||||
|
||||
.report-metric-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
@ -516,51 +478,22 @@
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.report-table th.is-sticky-left,
|
||||
.report-table td.is-sticky-left {
|
||||
position: sticky;
|
||||
left: 0;
|
||||
z-index: 2;
|
||||
box-shadow: 1px 0 0 #dbe7f2, 16px 0 18px -20px rgba(27, 39, 56, 0.48);
|
||||
}
|
||||
|
||||
.report-table th.is-sticky-left {
|
||||
z-index: 4;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.report-table td.is-sticky-left {
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.report-table tbody tr:hover td {
|
||||
background: #f7fbff;
|
||||
}
|
||||
|
||||
.report-table tbody tr:hover td.is-sticky-left {
|
||||
background: #f7fbff;
|
||||
}
|
||||
|
||||
.report-row-total td {
|
||||
background: #eef7ff;
|
||||
color: #172033;
|
||||
font-weight: 780;
|
||||
}
|
||||
|
||||
.report-row-total td.is-sticky-left {
|
||||
background: #eef7ff;
|
||||
}
|
||||
|
||||
.report-row-date-total td {
|
||||
background: #f7fafc;
|
||||
color: #243044;
|
||||
font-weight: 760;
|
||||
}
|
||||
|
||||
.report-row-date-total td.is-sticky-left {
|
||||
background: #f7fafc;
|
||||
}
|
||||
|
||||
.report-row-expandable {
|
||||
cursor: pointer;
|
||||
}
|
||||
@ -576,11 +509,6 @@
|
||||
background: #e9f5ff;
|
||||
}
|
||||
|
||||
.report-row-total:hover td.is-sticky-left,
|
||||
.report-row-date-total:hover td.is-sticky-left {
|
||||
background: #e9f5ff;
|
||||
}
|
||||
|
||||
.report-sort-button {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
|
||||
@ -4,7 +4,6 @@ import { usePaginatedQuery } from "@/shared/hooks/usePaginatedQuery.js";
|
||||
import { useRegionOptions } from "@/shared/hooks/useRegionOptions.js";
|
||||
import { useConfirm } from "@/shared/ui/ConfirmProvider.jsx";
|
||||
import { useToast } from "@/shared/ui/ToastProvider.jsx";
|
||||
import { emptyUserIdentityFilter, userIdentityFilterParams } from "@/shared/ui/userIdentityFilter.js";
|
||||
import {
|
||||
banAppUser,
|
||||
listAppUsers,
|
||||
@ -29,7 +28,7 @@ export function useAppUsersPage() {
|
||||
const { countryOptions, loadingCountries } = useCountryOptions();
|
||||
const { loadingRegions, regionOptions } = useRegionOptions();
|
||||
const [country, setCountry] = useState("");
|
||||
const [userFilter, setUserFilter] = useState(emptyUserIdentityFilter);
|
||||
const [query, setQuery] = useState("");
|
||||
const [regionId, setRegionId] = useState("");
|
||||
const [status, setStatus] = useState("");
|
||||
const [timeRange, setTimeRange] = useState({ endMs: "", startMs: "" });
|
||||
@ -47,15 +46,15 @@ export function useAppUsersPage() {
|
||||
const filters = useMemo(
|
||||
() => ({
|
||||
country,
|
||||
keyword: query,
|
||||
region_id: regionId,
|
||||
sort_by: sortBy,
|
||||
sort_direction: sortDirection,
|
||||
end_ms: timeRange.endMs,
|
||||
status,
|
||||
start_ms: timeRange.startMs,
|
||||
...userIdentityFilterParams(userFilter),
|
||||
}),
|
||||
[country, regionId, sortBy, sortDirection, status, timeRange.endMs, timeRange.startMs, userFilter],
|
||||
[country, query, regionId, sortBy, sortDirection, status, timeRange.endMs, timeRange.startMs],
|
||||
);
|
||||
const {
|
||||
data: queryData = emptyData,
|
||||
@ -80,7 +79,7 @@ export function useAppUsersPage() {
|
||||
useEffect(() => {
|
||||
setSelectedUserIds([]);
|
||||
setPatchedUsers({});
|
||||
}, [country, page, regionId, sortBy, sortDirection, status, timeRange.endMs, timeRange.startMs, userFilter]);
|
||||
}, [country, page, query, regionId, sortBy, sortDirection, status, timeRange.endMs, timeRange.startMs]);
|
||||
|
||||
const locationFilterOptions = useMemo(() => {
|
||||
const countryItems = countryOptions.map((option) => ({
|
||||
@ -99,8 +98,8 @@ export function useAppUsersPage() {
|
||||
|
||||
const locationFilterValue = country ? `country:${country}` : regionId !== "" ? `region:${regionId}` : "";
|
||||
|
||||
const changeUserFilter = (value) => {
|
||||
setUserFilter(value);
|
||||
const changeQuery = (value) => {
|
||||
setQuery(value);
|
||||
setPage(1);
|
||||
};
|
||||
|
||||
@ -137,7 +136,7 @@ export function useAppUsersPage() {
|
||||
|
||||
const resetFilters = () => {
|
||||
setCountry("");
|
||||
setUserFilter(emptyUserIdentityFilter);
|
||||
setQuery("");
|
||||
setRegionId("");
|
||||
setStatus("");
|
||||
setTimeRange({ endMs: "", startMs: "" });
|
||||
@ -300,7 +299,7 @@ export function useAppUsersPage() {
|
||||
activeAction,
|
||||
activeUser,
|
||||
batchBan,
|
||||
changeUserFilter,
|
||||
changeQuery,
|
||||
changeStatus,
|
||||
closeAction,
|
||||
changeLocationFilter,
|
||||
@ -321,6 +320,7 @@ export function useAppUsersPage() {
|
||||
openPassword,
|
||||
page,
|
||||
passwordForm,
|
||||
query,
|
||||
regionId,
|
||||
regionOptions,
|
||||
reload,
|
||||
@ -335,7 +335,6 @@ export function useAppUsersPage() {
|
||||
sortDirection,
|
||||
status,
|
||||
timeRange,
|
||||
userFilter,
|
||||
changeSort,
|
||||
changeTimeRange,
|
||||
submitCountry,
|
||||
|
||||
@ -2,12 +2,7 @@ import { useCallback, useMemo, useState } from "react";
|
||||
import { AdminUserIdentity } from "@/shared/ui/AdminUserIdentity.jsx";
|
||||
import { DataState } from "@/shared/ui/DataState.jsx";
|
||||
import { DataTable } from "@/shared/ui/DataTable.jsx";
|
||||
import { createTextFieldsColumnFilter, createUserIdentityColumnFilter } from "@/shared/ui/tableFilters.js";
|
||||
import {
|
||||
emptyUserIdentityFilter,
|
||||
normalizeUserIdentityFilter,
|
||||
userIdentityFilterParams,
|
||||
} from "@/shared/ui/userIdentityFilter.js";
|
||||
import { createTextColumnFilter } from "@/shared/ui/tableFilters.js";
|
||||
import { usePaginatedQuery } from "@/shared/hooks/usePaginatedQuery.js";
|
||||
import { formatMillis } from "@/shared/utils/time.js";
|
||||
import { listAppUserBans } from "@/features/app-users/api";
|
||||
@ -16,27 +11,17 @@ import styles from "@/features/app-users/app-users.module.css";
|
||||
|
||||
const pageSize = 50;
|
||||
const emptyData = { items: [], page: 1, pageSize, total: 0 };
|
||||
const emptyOperatorFilter = Object.freeze({ ...emptyUserIdentityFilter, adminAccount: "" });
|
||||
|
||||
export function AppUserBanListPage() {
|
||||
const [targetFilter, setTargetFilter] = useState(emptyUserIdentityFilter);
|
||||
const [operatorFilter, setOperatorFilter] = useState(emptyOperatorFilter);
|
||||
const [targetKeyword, setTargetKeyword] = useState("");
|
||||
const [operatorKeyword, setOperatorKeyword] = useState("");
|
||||
const [page, setPage] = useState(1);
|
||||
const filters = useMemo(
|
||||
() => ({
|
||||
operator_admin_keyword: String(operatorFilter.adminAccount || "").trim(),
|
||||
...userIdentityFilterParams(targetFilter, {
|
||||
displayUserId: "target_display_user_id",
|
||||
userId: "target_user_id",
|
||||
username: "target_username",
|
||||
}),
|
||||
...userIdentityFilterParams(operatorFilter, {
|
||||
displayUserId: "operator_display_user_id",
|
||||
userId: "operator_user_id",
|
||||
username: "operator_username",
|
||||
}),
|
||||
operator_keyword: operatorKeyword,
|
||||
target_keyword: targetKeyword,
|
||||
}),
|
||||
[operatorFilter, targetFilter],
|
||||
[operatorKeyword, targetKeyword],
|
||||
);
|
||||
const {
|
||||
data = emptyData,
|
||||
@ -52,17 +37,13 @@ export function AppUserBanListPage() {
|
||||
queryKey: ["app-users", "bans", filters, page],
|
||||
});
|
||||
|
||||
const changeTargetFilter = useCallback((value) => {
|
||||
setTargetFilter(normalizeUserIdentityFilter(value));
|
||||
const changeTargetKeyword = useCallback((value) => {
|
||||
setTargetKeyword(value);
|
||||
setPage(1);
|
||||
}, []);
|
||||
|
||||
const changeOperatorFilter = useCallback((value) => {
|
||||
const normalized = normalizeUserIdentityFilter(value);
|
||||
setOperatorFilter({
|
||||
...normalized,
|
||||
adminAccount: String(value?.adminAccount ?? "").trim(),
|
||||
});
|
||||
const changeOperatorKeyword = useCallback((value) => {
|
||||
setOperatorKeyword(value);
|
||||
setPage(1);
|
||||
}, []);
|
||||
|
||||
@ -72,9 +53,10 @@ export function AppUserBanListPage() {
|
||||
key: "target",
|
||||
label: "封禁用户",
|
||||
width: "minmax(280px, 1.4fr)",
|
||||
filter: createUserIdentityColumnFilter({
|
||||
value: targetFilter,
|
||||
onChange: changeTargetFilter,
|
||||
filter: createTextColumnFilter({
|
||||
placeholder: "搜索昵称、短 ID、长 ID",
|
||||
value: targetKeyword,
|
||||
onChange: changeTargetKeyword,
|
||||
}),
|
||||
render: (record) => <TargetIdentity target={record.target} />,
|
||||
},
|
||||
@ -82,19 +64,10 @@ export function AppUserBanListPage() {
|
||||
key: "operator",
|
||||
label: "封禁人",
|
||||
width: "minmax(280px, 1.4fr)",
|
||||
filter: createTextFieldsColumnFilter({
|
||||
fields: [
|
||||
{ key: "displayUserId", label: "短 ID", placeholder: "输入短 ID", value: operatorFilter.displayUserId },
|
||||
{ key: "userId", label: "长 ID", placeholder: "输入长 ID", value: operatorFilter.userId },
|
||||
{ key: "username", label: "昵称", placeholder: "输入昵称", value: operatorFilter.username },
|
||||
{
|
||||
key: "adminAccount",
|
||||
label: "后台账号",
|
||||
placeholder: "输入后台账号",
|
||||
value: operatorFilter.adminAccount,
|
||||
},
|
||||
],
|
||||
onChange: changeOperatorFilter,
|
||||
filter: createTextColumnFilter({
|
||||
placeholder: "搜索账号、昵称、短 ID、长 ID",
|
||||
value: operatorKeyword,
|
||||
onChange: changeOperatorKeyword,
|
||||
}),
|
||||
render: (record) => <OperatorIdentity operator={record.operator} />,
|
||||
},
|
||||
@ -117,7 +90,7 @@ export function AppUserBanListPage() {
|
||||
render: (record) => <span className="muted">{formatMillis(record.createdAtMs)}</span>,
|
||||
},
|
||||
],
|
||||
[changeOperatorFilter, changeTargetFilter, operatorFilter, targetFilter],
|
||||
[changeOperatorKeyword, changeTargetKeyword, operatorKeyword, targetKeyword],
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@ -17,7 +17,7 @@ import { DataState } from "@/shared/ui/DataState.jsx";
|
||||
import { DataTable } from "@/shared/ui/DataTable.jsx";
|
||||
import { IconButton } from "@/shared/ui/IconButton.jsx";
|
||||
import { SideDrawer } from "@/shared/ui/SideDrawer.jsx";
|
||||
import { createOptionsColumnFilter, createUserIdentityColumnFilter } from "@/shared/ui/tableFilters.js";
|
||||
import { createOptionsColumnFilter, createTextColumnFilter } from "@/shared/ui/tableFilters.js";
|
||||
import { TimeRangeFilter } from "@/shared/ui/TimeRangeFilter.jsx";
|
||||
import { UploadField } from "@/shared/ui/UploadField.jsx";
|
||||
import { formatMillis } from "@/shared/utils/time.js";
|
||||
@ -83,9 +83,10 @@ export function AppUserListPage() {
|
||||
key: "identity",
|
||||
label: "用户",
|
||||
width: "minmax(240px, 1.5fr)",
|
||||
filter: createUserIdentityColumnFilter({
|
||||
value: page.userFilter,
|
||||
onChange: page.changeUserFilter,
|
||||
filter: createTextColumnFilter({
|
||||
placeholder: "搜索名称、短 ID、用户 ID",
|
||||
value: page.query,
|
||||
onChange: page.changeQuery,
|
||||
}),
|
||||
render: (user) => <AdminUserIdentity openInAppUserDetail user={user} />,
|
||||
},
|
||||
|
||||
@ -14,13 +14,7 @@ import { mergePaginatedItems } from "@/shared/hooks/usePaginatedQuery.js";
|
||||
import { useRegionOptions } from "@/shared/hooks/useRegionOptions.js";
|
||||
import { useToast } from "@/shared/ui/ToastProvider.jsx";
|
||||
import { createRegionColumnFilter } from "@/shared/ui/RegionFilterControl.jsx";
|
||||
import { createOptionsColumnFilter, createTextColumnFilter, createUserIdentityColumnFilter } from "@/shared/ui/tableFilters.js";
|
||||
import {
|
||||
emptyUserIdentityFilter,
|
||||
isUserIdentityFilterEmpty,
|
||||
normalizeUserIdentityFilter,
|
||||
userIdentityFilterParams,
|
||||
} from "@/shared/ui/userIdentityFilter.js";
|
||||
import { createOptionsColumnFilter, createTextColumnFilter } from "@/shared/ui/tableFilters.js";
|
||||
import { formatMillis } from "@/shared/utils/time.js";
|
||||
import { listAppUserLoginLogs } from "@/features/app-users/api";
|
||||
import { addRegionBlockWhitelistIP } from "@/features/region-blocks/api";
|
||||
@ -41,29 +35,27 @@ export function AppUserLoginLogsPage() {
|
||||
const queryClient = useQueryClient();
|
||||
const { showToast } = useToast();
|
||||
const regionBlockAbilities = useRegionBlockAbilities();
|
||||
const [identityFilter, setIdentityFilter] = useState(emptyUserIdentityFilter);
|
||||
const [ip, setIp] = useState("");
|
||||
const [query, setQuery] = useState("");
|
||||
const [result, setResult] = useState("");
|
||||
const [regionId, setRegionId] = useState("");
|
||||
const [page, setPage] = useState(1);
|
||||
const [addingWhitelistIP, setAddingWhitelistIP] = useState("");
|
||||
const [mergedData, setMergedData] = useState(emptyData);
|
||||
const { loadingRegions, regionOptions } = useRegionOptions();
|
||||
const lockedUserId = searchParams.get("user_id") || "";
|
||||
const userFilter = searchParams.get("user_id") || "";
|
||||
const regionFilterOptions = useMemo(() => [{ label: "GLOBAL · 0", value: "0" }, ...regionOptions], [regionOptions]);
|
||||
|
||||
const requestQuery = useMemo(
|
||||
() =>
|
||||
toPageQuery({
|
||||
ip,
|
||||
keyword: query,
|
||||
page,
|
||||
pageSize,
|
||||
region_id: regionId,
|
||||
...userIdentityFilterParams(identityFilter),
|
||||
user_id: lockedUserId || identityFilter.userId,
|
||||
user_id: userFilter,
|
||||
...(result === "blocked" ? { blocked: "blocked" } : { result }),
|
||||
}),
|
||||
[identityFilter, ip, lockedUserId, page, regionId, result],
|
||||
[page, query, regionId, result, userFilter],
|
||||
);
|
||||
const loadLogs = useCallback(() => listAppUserLoginLogs(requestQuery), [requestQuery]);
|
||||
const {
|
||||
@ -80,7 +72,7 @@ export function AppUserLoginLogsPage() {
|
||||
|
||||
useEffect(() => {
|
||||
setMergedData(emptyData);
|
||||
}, [identityFilter, ip, lockedUserId, regionId, result]);
|
||||
}, [query, regionId, result, userFilter]);
|
||||
|
||||
useEffect(() => {
|
||||
const nextData = data || emptyData;
|
||||
@ -106,13 +98,8 @@ export function AppUserLoginLogsPage() {
|
||||
|
||||
const loading = queryLoading && (mergedData.items || []).length === 0;
|
||||
|
||||
const changeUserFilter = useCallback((value) => {
|
||||
setIdentityFilter(normalizeUserIdentityFilter(value));
|
||||
setPage(1);
|
||||
}, []);
|
||||
|
||||
const changeIp = useCallback((value) => {
|
||||
setIp(value);
|
||||
const changeQuery = useCallback((value) => {
|
||||
setQuery(value);
|
||||
setPage(1);
|
||||
}, []);
|
||||
|
||||
@ -134,8 +121,7 @@ export function AppUserLoginLogsPage() {
|
||||
const nextSearchParams = new URLSearchParams(searchParams);
|
||||
nextSearchParams.set("user_id", log.userId);
|
||||
setSearchParams(nextSearchParams, { replace: true });
|
||||
setIdentityFilter(emptyUserIdentityFilter);
|
||||
setIp("");
|
||||
setQuery("");
|
||||
setResult("");
|
||||
setRegionId("");
|
||||
setPage(1);
|
||||
@ -144,19 +130,19 @@ export function AppUserLoginLogsPage() {
|
||||
);
|
||||
|
||||
const resetIdentityFilter = useCallback(() => {
|
||||
setIdentityFilter(emptyUserIdentityFilter);
|
||||
setQuery("");
|
||||
setSearchParams(new URLSearchParams(), { replace: true });
|
||||
setPage(1);
|
||||
}, [setSearchParams]);
|
||||
|
||||
const changeIdentityFilter = useCallback(
|
||||
(value) => {
|
||||
if (lockedUserId) {
|
||||
if (userFilter) {
|
||||
setSearchParams(new URLSearchParams(), { replace: true });
|
||||
}
|
||||
changeUserFilter(value);
|
||||
changeQuery(value);
|
||||
},
|
||||
[changeUserFilter, lockedUserId, setSearchParams],
|
||||
[changeQuery, setSearchParams, userFilter],
|
||||
);
|
||||
|
||||
const addLoginIPToWhitelist = useCallback(
|
||||
@ -181,12 +167,12 @@ export function AppUserLoginLogsPage() {
|
||||
);
|
||||
|
||||
const selectedUserLabel = useMemo(() => {
|
||||
if (!lockedUserId) {
|
||||
if (!userFilter) {
|
||||
return "";
|
||||
}
|
||||
const matchedLog = (mergedData.items || []).find((log) => String(log.userId || "") === String(lockedUserId));
|
||||
return matchedLog?.displayUserId || matchedLog?.username || lockedUserId;
|
||||
}, [lockedUserId, mergedData.items]);
|
||||
const matchedLog = (mergedData.items || []).find((log) => String(log.userId || "") === String(userFilter));
|
||||
return matchedLog?.displayUserId || matchedLog?.username || userFilter;
|
||||
}, [mergedData.items, userFilter]);
|
||||
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
@ -194,11 +180,12 @@ export function AppUserLoginLogsPage() {
|
||||
key: "identity",
|
||||
label: "用户",
|
||||
width: "minmax(240px, 1.4fr)",
|
||||
filter: createUserIdentityColumnFilter({
|
||||
activeLabel: lockedUserId ? selectedUserLabel || lockedUserId : undefined,
|
||||
activeValue: isUserIdentityFilterEmpty(identityFilter) ? lockedUserId : "active",
|
||||
filter: createTextColumnFilter({
|
||||
activeLabel: userFilter ? selectedUserLabel || userFilter : undefined,
|
||||
activeValue: query || userFilter,
|
||||
onReset: resetIdentityFilter,
|
||||
value: lockedUserId ? emptyUserIdentityFilter : identityFilter,
|
||||
placeholder: "搜索昵称、短 ID、用户 ID、IP",
|
||||
value: query,
|
||||
onChange: changeIdentityFilter,
|
||||
}),
|
||||
render: (log) => <AdminUserIdentity openInAppUserDetail user={log} />,
|
||||
@ -207,11 +194,6 @@ export function AppUserLoginLogsPage() {
|
||||
key: "ip",
|
||||
label: "IP",
|
||||
width: "minmax(160px, 1fr)",
|
||||
filter: createTextColumnFilter({
|
||||
placeholder: "搜索 IP",
|
||||
value: ip,
|
||||
onChange: changeIp,
|
||||
}),
|
||||
render: (log) => (
|
||||
<div className={styles.stack}>
|
||||
<span>{log.loginIp || "-"}</span>
|
||||
@ -302,20 +284,18 @@ export function AppUserLoginLogsPage() {
|
||||
[
|
||||
addLoginIPToWhitelist,
|
||||
addingWhitelistIP,
|
||||
changeIp,
|
||||
changeIdentityFilter,
|
||||
changeRegionId,
|
||||
changeResult,
|
||||
identityFilter,
|
||||
ip,
|
||||
lockedUserId,
|
||||
loadingRegions,
|
||||
query,
|
||||
regionFilterOptions,
|
||||
regionId,
|
||||
resetIdentityFilter,
|
||||
result,
|
||||
regionBlockAbilities.canUpdate,
|
||||
selectedUserLabel,
|
||||
userFilter,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@ -8,7 +8,6 @@ import {
|
||||
import { useCumulativeRechargeRewardAbilities } from "@/features/cumulative-recharge-reward/permissions.js";
|
||||
import { usePaginatedQuery } from "@/shared/hooks/usePaginatedQuery.js";
|
||||
import { useToast } from "@/shared/ui/ToastProvider.jsx";
|
||||
import { emptyUserIdentityFilter, userIdentityFilterParams } from "@/shared/ui/userIdentityFilter.js";
|
||||
|
||||
const pageSize = 50;
|
||||
const emptyGrants = { items: [], page: 1, pageSize, total: 0 };
|
||||
@ -27,14 +26,11 @@ export function useCumulativeRechargeRewardPage() {
|
||||
const [configLoading, setConfigLoading] = useState(false);
|
||||
const [configSaving, setConfigSaving] = useState(false);
|
||||
const [resourceGroups, setResourceGroups] = useState([]);
|
||||
const [userFilter, setUserFilter] = useState(emptyUserIdentityFilter);
|
||||
const [query, setQuery] = useState("");
|
||||
const [cycleKey, setCycleKey] = useState("");
|
||||
const [status, setStatus] = useState("");
|
||||
const [page, setPage] = useState(1);
|
||||
const filters = useMemo(
|
||||
() => ({ cycle_key: cycleKey, status, ...userIdentityFilterParams(userFilter) }),
|
||||
[cycleKey, status, userFilter],
|
||||
);
|
||||
const filters = useMemo(() => ({ keyword: query, cycle_key: cycleKey, status }), [cycleKey, query, status]);
|
||||
const {
|
||||
data: grants = emptyGrants,
|
||||
error: grantsError,
|
||||
@ -80,8 +76,8 @@ export function useCumulativeRechargeRewardPage() {
|
||||
setForm(config ? formFromConfig(config) : emptyForm);
|
||||
}, [config]);
|
||||
|
||||
const changeUserFilter = (value) => {
|
||||
setUserFilter(value);
|
||||
const changeQuery = (value) => {
|
||||
setQuery(value);
|
||||
setPage(1);
|
||||
};
|
||||
|
||||
@ -118,8 +114,8 @@ export function useCumulativeRechargeRewardPage() {
|
||||
return {
|
||||
abilities,
|
||||
changeCycleKey,
|
||||
changeQuery,
|
||||
changeStatus,
|
||||
changeUserFilter,
|
||||
closeConfigDrawer,
|
||||
config,
|
||||
configDrawerOpen,
|
||||
@ -132,6 +128,7 @@ export function useCumulativeRechargeRewardPage() {
|
||||
grantsLoading,
|
||||
openConfigDrawer,
|
||||
page,
|
||||
query,
|
||||
reloadConfig,
|
||||
reloadGrants,
|
||||
resourceGroups,
|
||||
@ -139,7 +136,6 @@ export function useCumulativeRechargeRewardPage() {
|
||||
setPage,
|
||||
status,
|
||||
submitConfig,
|
||||
userFilter,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -6,7 +6,7 @@ import { AdminListBody, AdminListPage } from "@/shared/ui/AdminListLayout.jsx";
|
||||
import { AdminUserIdentity } from "@/shared/ui/AdminUserIdentity.jsx";
|
||||
import { DataState } from "@/shared/ui/DataState.jsx";
|
||||
import { DataTable } from "@/shared/ui/DataTable.jsx";
|
||||
import { createOptionsColumnFilter, createTextColumnFilter, createUserIdentityColumnFilter } from "@/shared/ui/tableFilters.js";
|
||||
import { createOptionsColumnFilter, createTextColumnFilter } from "@/shared/ui/tableFilters.js";
|
||||
import { formatMillis } from "@/shared/utils/time.js";
|
||||
|
||||
const grantStatusOptions = [
|
||||
@ -101,9 +101,10 @@ export function CumulativeRechargeRewardPage() {
|
||||
if (column.key === "user") {
|
||||
return {
|
||||
...column,
|
||||
filter: createUserIdentityColumnFilter({
|
||||
value: page.userFilter,
|
||||
onChange: page.changeUserFilter,
|
||||
filter: createTextColumnFilter({
|
||||
placeholder: "搜索用户 ID、短号、名称",
|
||||
value: page.query,
|
||||
onChange: page.changeQuery,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@ -8,7 +8,6 @@ import {
|
||||
import { useFirstRechargeRewardAbilities } from "@/features/first-recharge-reward/permissions.js";
|
||||
import { usePaginatedQuery } from "@/shared/hooks/usePaginatedQuery.js";
|
||||
import { useToast } from "@/shared/ui/ToastProvider.jsx";
|
||||
import { emptyUserIdentityFilter, userIdentityFilterParams } from "@/shared/ui/userIdentityFilter.js";
|
||||
|
||||
const pageSize = 50;
|
||||
const emptyClaims = { items: [], page: 1, pageSize, total: 0 };
|
||||
@ -27,10 +26,10 @@ export function useFirstRechargeRewardPage() {
|
||||
const [configLoading, setConfigLoading] = useState(false);
|
||||
const [configSaving, setConfigSaving] = useState(false);
|
||||
const [resourceGroups, setResourceGroups] = useState([]);
|
||||
const [userFilter, setUserFilter] = useState(emptyUserIdentityFilter);
|
||||
const [query, setQuery] = useState("");
|
||||
const [status, setStatus] = useState("");
|
||||
const [page, setPage] = useState(1);
|
||||
const filters = useMemo(() => ({ status, ...userIdentityFilterParams(userFilter) }), [status, userFilter]);
|
||||
const filters = useMemo(() => ({ keyword: query, status }), [query, status]);
|
||||
const {
|
||||
data: claims = emptyClaims,
|
||||
error: claimsError,
|
||||
@ -76,8 +75,8 @@ export function useFirstRechargeRewardPage() {
|
||||
setForm(config ? formFromConfig(config) : emptyForm);
|
||||
}, [config]);
|
||||
|
||||
const changeUserFilter = (value) => {
|
||||
setUserFilter(value);
|
||||
const changeQuery = (value) => {
|
||||
setQuery(value);
|
||||
setPage(1);
|
||||
};
|
||||
|
||||
@ -108,7 +107,7 @@ export function useFirstRechargeRewardPage() {
|
||||
|
||||
return {
|
||||
abilities,
|
||||
changeUserFilter,
|
||||
changeQuery,
|
||||
changeStatus,
|
||||
claims,
|
||||
claimsError,
|
||||
@ -121,6 +120,7 @@ export function useFirstRechargeRewardPage() {
|
||||
form,
|
||||
openConfigDrawer,
|
||||
page,
|
||||
query,
|
||||
reloadClaims,
|
||||
reloadConfig,
|
||||
resourceGroups,
|
||||
@ -128,7 +128,6 @@ export function useFirstRechargeRewardPage() {
|
||||
setPage,
|
||||
status,
|
||||
submitConfig,
|
||||
userFilter,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -6,7 +6,7 @@ import { AdminListBody, AdminListPage } from "@/shared/ui/AdminListLayout.jsx";
|
||||
import { AdminUserIdentity } from "@/shared/ui/AdminUserIdentity.jsx";
|
||||
import { DataState } from "@/shared/ui/DataState.jsx";
|
||||
import { DataTable } from "@/shared/ui/DataTable.jsx";
|
||||
import { createOptionsColumnFilter, createUserIdentityColumnFilter } from "@/shared/ui/tableFilters.js";
|
||||
import { createOptionsColumnFilter, createTextColumnFilter } from "@/shared/ui/tableFilters.js";
|
||||
import { formatMillis } from "@/shared/utils/time.js";
|
||||
|
||||
const claimStatusOptions = [
|
||||
@ -84,9 +84,10 @@ export function FirstRechargeRewardPage() {
|
||||
if (column.key === "user") {
|
||||
return {
|
||||
...column,
|
||||
filter: createUserIdentityColumnFilter({
|
||||
value: page.userFilter,
|
||||
onChange: page.changeUserFilter,
|
||||
filter: createTextColumnFilter({
|
||||
placeholder: "搜索用户 ID、短号、名称",
|
||||
value: page.query,
|
||||
onChange: page.changeQuery,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@ -60,7 +60,6 @@ const writableBridgeAdapterTypes = new Set([
|
||||
"baishun_v1",
|
||||
"vivagames_v1",
|
||||
"reyou_v1",
|
||||
"zgame_v1",
|
||||
]);
|
||||
|
||||
const emptyCatalog = { items: [], pageSize: 50 };
|
||||
|
||||
@ -51,7 +51,6 @@ const adapterTypeOptions = [
|
||||
["baishun_v1", "百顺 BAISHUN V1"],
|
||||
["vivagames_v1", "VIVAGAMES V1"],
|
||||
["reyou_v1", "热游 Reyou V1"],
|
||||
["zgame_v1", "ZGame V1"],
|
||||
];
|
||||
|
||||
const baseColumns = [
|
||||
|
||||
@ -28,12 +28,7 @@ import { DataTable } from "@/shared/ui/DataTable.jsx";
|
||||
import { usePaginatedQuery } from "@/shared/hooks/usePaginatedQuery.js";
|
||||
import { TimeRangeFilter } from "@/shared/ui/TimeRangeFilter.jsx";
|
||||
import { useToast } from "@/shared/ui/ToastProvider.jsx";
|
||||
import { createOptionsColumnFilter, createTextColumnFilter, createUserIdentityColumnFilter } from "@/shared/ui/tableFilters.js";
|
||||
import {
|
||||
emptyUserIdentityFilter,
|
||||
isUserIdentityFilterEmpty,
|
||||
userIdentityFilterParams,
|
||||
} from "@/shared/ui/userIdentityFilter.js";
|
||||
import { createOptionsColumnFilter, createTextColumnFilter } from "@/shared/ui/tableFilters.js";
|
||||
import { formatMillis } from "@/shared/utils/time.js";
|
||||
import styles from "@/features/host-agency-policy/host-agency-policy.module.css";
|
||||
|
||||
@ -315,8 +310,8 @@ function HostPendingPanel({ onRoleChange, role }) {
|
||||
function HostRecordsPanel() {
|
||||
const [page, setPage] = useState(1);
|
||||
const [cycleKey, setCycleKey] = useState("");
|
||||
const [userFilter, setUserFilter] = useState(emptyUserIdentityFilter);
|
||||
const [agencyOwnerFilter, setAgencyOwnerFilter] = useState(emptyUserIdentityFilter);
|
||||
const [userId, setUserId] = useState("");
|
||||
const [agencyOwnerUserId, setAgencyOwnerUserId] = useState("");
|
||||
const [settlementType, setSettlementType] = useState("");
|
||||
const [status, setStatus] = useState("");
|
||||
const [timeRange, setTimeRange] = useState({ endMs: "", startMs: "" });
|
||||
@ -324,19 +319,15 @@ function HostRecordsPanel() {
|
||||
// filters 使用后端 snake_case 字段,直接进入 apiRequest query,减少额外字段映射层。
|
||||
const filters = useMemo(
|
||||
() => ({
|
||||
agency_owner_user_id: agencyOwnerUserId,
|
||||
cycle_key: cycleKey,
|
||||
end_at_ms: timeRange.endMs || "",
|
||||
settlement_type: settlementType,
|
||||
start_at_ms: timeRange.startMs || "",
|
||||
status,
|
||||
...userIdentityFilterParams(userFilter),
|
||||
...userIdentityFilterParams(agencyOwnerFilter, {
|
||||
displayUserId: "agency_owner_display_user_id",
|
||||
userId: "agency_owner_user_id",
|
||||
username: "agency_owner_username",
|
||||
}),
|
||||
user_id: userId,
|
||||
}),
|
||||
[agencyOwnerFilter, cycleKey, settlementType, status, timeRange.endMs, timeRange.startMs, userFilter],
|
||||
[agencyOwnerUserId, cycleKey, settlementType, status, timeRange.endMs, timeRange.startMs, userId],
|
||||
);
|
||||
const query = usePaginatedQuery({
|
||||
errorMessage: "获取工资结算记录失败",
|
||||
@ -360,12 +351,12 @@ function HostRecordsPanel() {
|
||||
setPage(1);
|
||||
};
|
||||
const resetFilters = () => {
|
||||
setAgencyOwnerFilter(emptyUserIdentityFilter);
|
||||
setAgencyOwnerUserId("");
|
||||
setCycleKey("");
|
||||
setSettlementType("");
|
||||
setStatus("");
|
||||
setTimeRange({ endMs: "", startMs: "" });
|
||||
setUserFilter(emptyUserIdentityFilter);
|
||||
setUserId("");
|
||||
setPage(1);
|
||||
};
|
||||
const tableColumns = hostRecordColumns.map((column) => {
|
||||
@ -373,18 +364,20 @@ function HostRecordsPanel() {
|
||||
if (column.key === "host") {
|
||||
return {
|
||||
...column,
|
||||
filter: createUserIdentityColumnFilter({
|
||||
value: userFilter,
|
||||
onChange: changeFilter(setUserFilter),
|
||||
filter: createTextColumnFilter({
|
||||
placeholder: "主播用户 ID",
|
||||
value: userId,
|
||||
onChange: changeFilter(setUserId),
|
||||
}),
|
||||
};
|
||||
}
|
||||
if (column.key === "agency") {
|
||||
return {
|
||||
...column,
|
||||
filter: createUserIdentityColumnFilter({
|
||||
value: agencyOwnerFilter,
|
||||
onChange: changeFilter(setAgencyOwnerFilter),
|
||||
filter: createTextColumnFilter({
|
||||
placeholder: "代理用户 ID",
|
||||
value: agencyOwnerUserId,
|
||||
onChange: changeFilter(setAgencyOwnerUserId),
|
||||
}),
|
||||
};
|
||||
}
|
||||
@ -411,13 +404,7 @@ function HostRecordsPanel() {
|
||||
return column;
|
||||
});
|
||||
const hasFilters =
|
||||
!isUserIdentityFilterEmpty(agencyOwnerFilter) ||
|
||||
cycleKey ||
|
||||
settlementType ||
|
||||
status ||
|
||||
timeRange.startMs ||
|
||||
timeRange.endMs ||
|
||||
!isUserIdentityFilterEmpty(userFilter);
|
||||
agencyOwnerUserId || cycleKey || settlementType || status || timeRange.startMs || timeRange.endMs || userId;
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -646,7 +633,7 @@ function TeamRecordsPanel() {
|
||||
const [triggerMode, setTriggerMode] = useState("");
|
||||
const [cycleKey, setCycleKey] = useState("");
|
||||
const [countryCode, setCountryCode] = useState("");
|
||||
const [userFilter, setUserFilter] = useState(emptyUserIdentityFilter);
|
||||
const [userId, setUserId] = useState("");
|
||||
const [status, setStatus] = useState("");
|
||||
const [timeRange, setTimeRange] = useState({ endMs: "", startMs: "" });
|
||||
|
||||
@ -660,9 +647,9 @@ function TeamRecordsPanel() {
|
||||
start_at_ms: timeRange.startMs || "",
|
||||
status,
|
||||
trigger_mode: triggerMode,
|
||||
...userIdentityFilterParams(userFilter),
|
||||
user_id: userId,
|
||||
}),
|
||||
[countryCode, cycleKey, policyType, status, timeRange.endMs, timeRange.startMs, triggerMode, userFilter],
|
||||
[countryCode, cycleKey, policyType, status, timeRange.endMs, timeRange.startMs, triggerMode, userId],
|
||||
);
|
||||
const query = usePaginatedQuery({
|
||||
errorMessage: "获取 BD/Admin 结算记录失败",
|
||||
@ -688,7 +675,7 @@ function TeamRecordsPanel() {
|
||||
setCountryCode("");
|
||||
setStatus("");
|
||||
setTimeRange({ endMs: "", startMs: "" });
|
||||
setUserFilter(emptyUserIdentityFilter);
|
||||
setUserId("");
|
||||
setPage(1);
|
||||
};
|
||||
const tableColumns = teamRecordColumns.map((column) => {
|
||||
@ -696,9 +683,10 @@ function TeamRecordsPanel() {
|
||||
if (column.key === "user") {
|
||||
return {
|
||||
...column,
|
||||
filter: createUserIdentityColumnFilter({
|
||||
value: userFilter,
|
||||
onChange: changeFilter(setUserFilter),
|
||||
filter: createTextColumnFilter({
|
||||
placeholder: "用户 ID",
|
||||
value: userId,
|
||||
onChange: changeFilter(setUserId),
|
||||
}),
|
||||
};
|
||||
}
|
||||
@ -732,7 +720,7 @@ function TeamRecordsPanel() {
|
||||
status ||
|
||||
timeRange.startMs ||
|
||||
timeRange.endMs ||
|
||||
!isUserIdentityFilterEmpty(userFilter);
|
||||
userId;
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@ -4,7 +4,6 @@ export { listRegions } from "@/shared/api/regions";
|
||||
import type {
|
||||
AgencyDto,
|
||||
AgencyJoinEnabledPayload,
|
||||
AgencyStatusPayload,
|
||||
ApiList,
|
||||
ApiPage,
|
||||
BDLeaderPositionAliasPayload,
|
||||
@ -383,17 +382,6 @@ export function deleteAgency(agencyId: EntityId, payload: HostCommandPayload): P
|
||||
);
|
||||
}
|
||||
|
||||
export function setAgencyStatus(agencyId: EntityId, payload: AgencyStatusPayload): Promise<AgencyDto> {
|
||||
const endpoint = API_ENDPOINTS.setAgencyStatus;
|
||||
return apiRequest<AgencyDto, AgencyStatusPayload>(
|
||||
apiEndpointPath(API_OPERATIONS.setAgencyStatus, { agency_id: agencyId }),
|
||||
{
|
||||
body: payload,
|
||||
method: endpoint.method,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export function setAgencyJoinEnabled(agencyId: EntityId, payload: AgencyJoinEnabledPayload): Promise<AgencyDto> {
|
||||
const endpoint = API_ENDPOINTS.setAgencyJoinEnabled;
|
||||
return apiRequest<AgencyDto, AgencyJoinEnabledPayload>(
|
||||
|
||||
@ -3,9 +3,8 @@ import { parseForm } from "@/shared/forms/validation";
|
||||
import { usePaginatedQuery } from "@/shared/hooks/usePaginatedQuery.js";
|
||||
import { useConfirm } from "@/shared/ui/ConfirmProvider.jsx";
|
||||
import { useToast } from "@/shared/ui/ToastProvider.jsx";
|
||||
import { createAgency, deleteAgency, listAgencies, setAgencyJoinEnabled, setAgencyStatus } from "@/features/host-org/api";
|
||||
import { closeAgency, createAgency, deleteAgency, listAgencies, setAgencyJoinEnabled } from "@/features/host-org/api";
|
||||
import { useRegionOptions } from "@/shared/hooks/useRegionOptions.js";
|
||||
import { emptyUserIdentityFilter, userIdentityFilterParams } from "@/shared/ui/userIdentityFilter.js";
|
||||
import { useAgencyAbilities } from "@/features/host-org/permissions.js";
|
||||
import { createAgencySchema } from "@/features/host-org/schema";
|
||||
|
||||
@ -26,10 +25,9 @@ export function useHostAgenciesPage() {
|
||||
const { showToast } = useToast();
|
||||
const { loadingRegions, regionOptions } = useRegionOptions();
|
||||
const [query, setQuery] = useState("");
|
||||
const [ownerFilter, setOwnerFilter] = useState(emptyUserIdentityFilter);
|
||||
const [status, setStatus] = useState("");
|
||||
const [regionId, setRegionId] = useState("");
|
||||
const [parentBdFilter, setParentBdFilter] = useState(emptyUserIdentityFilter);
|
||||
const [parentBdUserId, setParentBdUserId] = useState("");
|
||||
const [sortBy, setSortBy] = useState("");
|
||||
const [sortDirection, setSortDirection] = useState("");
|
||||
const [page, setPage] = useState(1);
|
||||
@ -41,22 +39,13 @@ export function useHostAgenciesPage() {
|
||||
const filters = useMemo(
|
||||
() => ({
|
||||
keyword: query,
|
||||
parent_bd_user_id: parentBdUserId,
|
||||
region_id: regionId,
|
||||
sort_by: sortBy,
|
||||
sort_direction: sortDirection,
|
||||
status,
|
||||
...userIdentityFilterParams(ownerFilter, {
|
||||
displayUserId: "owner_display_user_id",
|
||||
userId: "owner_user_id",
|
||||
username: "owner_username",
|
||||
}),
|
||||
...userIdentityFilterParams(parentBdFilter, {
|
||||
displayUserId: "parent_bd_display_user_id",
|
||||
userId: "parent_bd_user_id",
|
||||
username: "parent_bd_username",
|
||||
}),
|
||||
}),
|
||||
[ownerFilter, parentBdFilter, query, regionId, sortBy, sortDirection, status],
|
||||
[parentBdUserId, query, regionId, sortBy, sortDirection, status],
|
||||
);
|
||||
const {
|
||||
data = emptyData,
|
||||
@ -107,13 +96,8 @@ export function useHostAgenciesPage() {
|
||||
setPage(1);
|
||||
};
|
||||
|
||||
const changeParentBdFilter = (value) => {
|
||||
setParentBdFilter(value);
|
||||
setPage(1);
|
||||
};
|
||||
|
||||
const changeOwnerFilter = (value) => {
|
||||
setOwnerFilter(value);
|
||||
const changeParentBdUserId = (value) => {
|
||||
setParentBdUserId(value);
|
||||
setPage(1);
|
||||
};
|
||||
|
||||
@ -125,10 +109,9 @@ export function useHostAgenciesPage() {
|
||||
|
||||
const resetFilters = () => {
|
||||
setQuery("");
|
||||
setOwnerFilter(emptyUserIdentityFilter);
|
||||
setStatus("");
|
||||
setRegionId("");
|
||||
setParentBdFilter(emptyUserIdentityFilter);
|
||||
setParentBdUserId("");
|
||||
setSortBy("");
|
||||
setSortDirection("");
|
||||
setPage(1);
|
||||
@ -146,32 +129,20 @@ export function useHostAgenciesPage() {
|
||||
});
|
||||
};
|
||||
|
||||
const toggleAgencyStatusFromList = async (agency, enabled) => {
|
||||
if (!abilities.canStatus || !agency?.agencyId) {
|
||||
const closeAgencyFromList = async (agency) => {
|
||||
if (!abilities.canStatus || !agency?.agencyId || agency.status !== "active") {
|
||||
return;
|
||||
}
|
||||
const nextStatus = enabled ? "active" : "closed";
|
||||
if (agency.status === nextStatus || agency.status === "deleted") {
|
||||
return;
|
||||
}
|
||||
await runAction(`agency-status-${agency.agencyId}`, enabled ? "Agency 已启用" : "Agency 已关闭", async () => {
|
||||
const data = await setAgencyStatus(agency.agencyId, {
|
||||
commandId: makeCommandId("agency-status"),
|
||||
reason: enabled ? "open agency from list switch" : "close agency from list switch",
|
||||
status: nextStatus,
|
||||
await runAction(`agency-status-${agency.agencyId}`, "Agency 已关闭", async () => {
|
||||
const data = await closeAgency(agency.agencyId, {
|
||||
commandId: makeCommandId("agency-close"),
|
||||
reason: "close agency from list switch",
|
||||
});
|
||||
patchAgencyRow(
|
||||
agency.agencyId,
|
||||
agencyActionPatch(data, enabled ? { status: nextStatus } : { status: nextStatus, joinEnabled: false }),
|
||||
);
|
||||
patchAgencyRow(agency.agencyId, agencyActionPatch(data, { status: "closed", joinEnabled: false }));
|
||||
return data;
|
||||
});
|
||||
};
|
||||
|
||||
const closeAgencyFromList = async (agency) => {
|
||||
await toggleAgencyStatusFromList(agency, false);
|
||||
};
|
||||
|
||||
const toggleAgencyJoinEnabledFromList = async (agency, joinEnabled) => {
|
||||
if (!abilities.canStatus || !agency?.agencyId || agency.status !== "active") {
|
||||
return;
|
||||
@ -231,8 +202,7 @@ export function useHostAgenciesPage() {
|
||||
abilities,
|
||||
activeAction,
|
||||
agencyForm,
|
||||
changeOwnerFilter,
|
||||
changeParentBdFilter,
|
||||
changeParentBdUserId,
|
||||
changeQuery,
|
||||
changeRegionId,
|
||||
changeStatus,
|
||||
@ -242,9 +212,8 @@ export function useHostAgenciesPage() {
|
||||
loading,
|
||||
loadingAction,
|
||||
loadingRegions,
|
||||
ownerFilter,
|
||||
page,
|
||||
parentBdFilter,
|
||||
parentBdUserId,
|
||||
query,
|
||||
regionId,
|
||||
regionOptions,
|
||||
@ -260,7 +229,6 @@ export function useHostAgenciesPage() {
|
||||
status,
|
||||
submitAgency,
|
||||
changeSort,
|
||||
toggleAgencyStatusFromList,
|
||||
toggleAgencyJoinEnabledFromList,
|
||||
};
|
||||
}
|
||||
@ -271,7 +239,7 @@ function makeCommandId(prefix) {
|
||||
|
||||
function agencyActionPatch(data, fallback = {}) {
|
||||
const patch = { ...fallback };
|
||||
for (const key of ["activeHostCount", "status", "joinEnabled", "updatedAtMs"]) {
|
||||
for (const key of ["status", "joinEnabled", "updatedAtMs"]) {
|
||||
if (data?.[key] !== undefined) {
|
||||
patch[key] = data[key];
|
||||
}
|
||||
|
||||
@ -15,7 +15,6 @@ import {
|
||||
} from "@/features/host-org/api";
|
||||
import { listAppUsers } from "@/features/app-users/api";
|
||||
import { useRegionOptions } from "@/shared/hooks/useRegionOptions.js";
|
||||
import { emptyUserIdentityFilter, userIdentityFilterParams } from "@/shared/ui/userIdentityFilter.js";
|
||||
import { useBDAbilities } from "@/features/host-org/permissions.js";
|
||||
import { createBDSchema, createBDLeaderSchema } from "@/features/host-org/schema";
|
||||
|
||||
@ -35,10 +34,10 @@ export function useHostBdsPage({ profileType = "bd" } = {}) {
|
||||
const confirm = useConfirm();
|
||||
const { showToast } = useToast();
|
||||
const { loadingRegions, regionOptions } = useRegionOptions();
|
||||
const [userFilter, setUserFilter] = useState(emptyUserIdentityFilter);
|
||||
const [query, setQuery] = useState("");
|
||||
const [status, setStatus] = useState("");
|
||||
const [regionId, setRegionId] = useState("");
|
||||
const [parentLeaderFilter, setParentLeaderFilter] = useState(emptyUserIdentityFilter);
|
||||
const [parentLeaderUserId, setParentLeaderUserId] = useState("");
|
||||
const [sortBy, setSortBy] = useState("");
|
||||
const [sortDirection, setSortDirection] = useState("");
|
||||
const [page, setPage] = useState(1);
|
||||
@ -54,20 +53,14 @@ export function useHostBdsPage({ profileType = "bd" } = {}) {
|
||||
const [profileOverrides, setProfileOverrides] = useState({});
|
||||
const filters = useMemo(
|
||||
() => ({
|
||||
keyword: query,
|
||||
parent_leader_user_id: profileType === "bd" ? parentLeaderUserId : "",
|
||||
region_id: regionId,
|
||||
sort_by: sortBy,
|
||||
sort_direction: sortDirection,
|
||||
status,
|
||||
...userIdentityFilterParams(userFilter),
|
||||
...(profileType === "bd"
|
||||
? userIdentityFilterParams(parentLeaderFilter, {
|
||||
displayUserId: "parent_leader_display_user_id",
|
||||
userId: "parent_leader_user_id",
|
||||
username: "parent_leader_username",
|
||||
})
|
||||
: {}),
|
||||
}),
|
||||
[parentLeaderFilter, profileType, regionId, sortBy, sortDirection, status, userFilter],
|
||||
[parentLeaderUserId, profileType, query, regionId, sortBy, sortDirection, status],
|
||||
);
|
||||
const fetcher = profileType === "bd" ? listBDs : listBDLeaders;
|
||||
const {
|
||||
@ -149,8 +142,8 @@ export function useHostBdsPage({ profileType = "bd" } = {}) {
|
||||
};
|
||||
}, [activeAction, bdForm.targetUserId]);
|
||||
|
||||
const changeUserFilter = (value) => {
|
||||
setUserFilter(value);
|
||||
const changeQuery = (value) => {
|
||||
setQuery(value);
|
||||
setPage(1);
|
||||
};
|
||||
|
||||
@ -164,8 +157,8 @@ export function useHostBdsPage({ profileType = "bd" } = {}) {
|
||||
setPage(1);
|
||||
};
|
||||
|
||||
const changeParentLeaderFilter = (value) => {
|
||||
setParentLeaderFilter(value);
|
||||
const changeParentLeaderUserId = (value) => {
|
||||
setParentLeaderUserId(value);
|
||||
setPage(1);
|
||||
};
|
||||
|
||||
@ -176,10 +169,10 @@ export function useHostBdsPage({ profileType = "bd" } = {}) {
|
||||
};
|
||||
|
||||
const resetFilters = () => {
|
||||
setUserFilter(emptyUserIdentityFilter);
|
||||
setQuery("");
|
||||
setStatus("");
|
||||
setRegionId("");
|
||||
setParentLeaderFilter(emptyUserIdentityFilter);
|
||||
setParentLeaderUserId("");
|
||||
setSortBy("");
|
||||
setSortDirection("");
|
||||
setPage(1);
|
||||
@ -348,10 +341,10 @@ export function useHostBdsPage({ profileType = "bd" } = {}) {
|
||||
bdForm,
|
||||
bdLeaderForm,
|
||||
bdUserLookup,
|
||||
changeParentLeaderFilter,
|
||||
changeParentLeaderUserId,
|
||||
changeQuery,
|
||||
changeRegionId,
|
||||
changeStatus,
|
||||
changeUserFilter,
|
||||
data: displayData,
|
||||
error,
|
||||
loading,
|
||||
@ -361,8 +354,9 @@ export function useHostBdsPage({ profileType = "bd" } = {}) {
|
||||
leaderBDsError,
|
||||
leaderBDsLoading,
|
||||
page,
|
||||
parentLeaderFilter,
|
||||
parentLeaderUserId,
|
||||
profileType,
|
||||
query,
|
||||
regionId,
|
||||
regionOptions,
|
||||
reload,
|
||||
@ -388,7 +382,6 @@ export function useHostBdsPage({ profileType = "bd" } = {}) {
|
||||
changeSort,
|
||||
toggleBD,
|
||||
toggleBDLeader,
|
||||
userFilter,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -3,7 +3,6 @@ import { parseForm } from "@/shared/forms/validation";
|
||||
import { usePaginatedQuery } from "@/shared/hooks/usePaginatedQuery.js";
|
||||
import { useRegionOptions } from "@/shared/hooks/useRegionOptions.js";
|
||||
import { useToast } from "@/shared/ui/ToastProvider.jsx";
|
||||
import { emptyUserIdentityFilter, userIdentityFilterParams } from "@/shared/ui/userIdentityFilter.js";
|
||||
import {
|
||||
createCoinSeller,
|
||||
creditCoinSellerStock,
|
||||
@ -56,7 +55,7 @@ export function useHostCoinSellersPage() {
|
||||
const abilities = useCoinSellerAbilities();
|
||||
const { showToast } = useToast();
|
||||
const { loadingRegions, regionOptions } = useRegionOptions();
|
||||
const [userFilter, setUserFilter] = useState(emptyUserIdentityFilter);
|
||||
const [query, setQuery] = useState("");
|
||||
const [status, setStatus] = useState("");
|
||||
const [regionId, setRegionId] = useState("");
|
||||
const [sortBy, setSortBy] = useState("");
|
||||
@ -76,13 +75,13 @@ export function useHostCoinSellersPage() {
|
||||
const [sellerRowPatches, setSellerRowPatches] = useState({});
|
||||
const filters = useMemo(
|
||||
() => ({
|
||||
keyword: query,
|
||||
region_id: regionId,
|
||||
sort_by: sortBy,
|
||||
sort_direction: sortDirection,
|
||||
status,
|
||||
...userIdentityFilterParams(userFilter),
|
||||
}),
|
||||
[regionId, sortBy, sortDirection, status, userFilter],
|
||||
[query, regionId, sortBy, sortDirection, status],
|
||||
);
|
||||
const {
|
||||
data: queryData = emptyData,
|
||||
@ -108,8 +107,8 @@ export function useHostCoinSellersPage() {
|
||||
};
|
||||
}, [queryData, sellerRowPatches]);
|
||||
|
||||
const changeUserFilter = (value) => {
|
||||
setUserFilter(value);
|
||||
const changeQuery = (value) => {
|
||||
setQuery(value);
|
||||
setPage(1);
|
||||
};
|
||||
|
||||
@ -130,7 +129,7 @@ export function useHostCoinSellersPage() {
|
||||
};
|
||||
|
||||
const resetFilters = () => {
|
||||
setUserFilter(emptyUserIdentityFilter);
|
||||
setQuery("");
|
||||
setStatus("");
|
||||
setRegionId("");
|
||||
setSortBy("");
|
||||
@ -397,10 +396,10 @@ export function useHostCoinSellersPage() {
|
||||
return {
|
||||
abilities,
|
||||
activeAction,
|
||||
changeQuery,
|
||||
changeRegionId,
|
||||
changeSort,
|
||||
changeStatus,
|
||||
changeUserFilter,
|
||||
closeAction,
|
||||
contactSavingIds,
|
||||
createForm,
|
||||
@ -418,6 +417,7 @@ export function useHostCoinSellersPage() {
|
||||
openStockCredit,
|
||||
openStockDebit,
|
||||
page,
|
||||
query,
|
||||
regionId,
|
||||
regionOptions,
|
||||
rateRegionId,
|
||||
@ -446,7 +446,6 @@ export function useHostCoinSellersPage() {
|
||||
submitStockDebit,
|
||||
toggleSeller,
|
||||
updateRateTier,
|
||||
userFilter,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -1,9 +1,7 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { useMemo, useState } from "react";
|
||||
import { usePaginatedQuery } from "@/shared/hooks/usePaginatedQuery.js";
|
||||
import { listHosts } from "@/features/host-org/api";
|
||||
import { useRegionOptions } from "@/shared/hooks/useRegionOptions.js";
|
||||
import { emptyUserIdentityFilter, userIdentityFilterParams } from "@/shared/ui/userIdentityFilter.js";
|
||||
import { useHostAbilities } from "@/features/host-org/permissions.js";
|
||||
|
||||
const pageSize = 50;
|
||||
@ -12,25 +10,23 @@ const emptyData = { items: [], page: 1, pageSize, total: 0 };
|
||||
export function useHostHostsPage() {
|
||||
const abilities = useHostAbilities();
|
||||
const { loadingRegions, regionOptions } = useRegionOptions();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const agencyIdParam = searchParams.get("agency_id") || searchParams.get("agencyId") || "";
|
||||
const [userFilter, setUserFilter] = useState(emptyUserIdentityFilter);
|
||||
const [query, setQuery] = useState("");
|
||||
const [status, setStatus] = useState("");
|
||||
const [regionId, setRegionId] = useState("");
|
||||
const [agencyId, setAgencyId] = useState(() => agencyIdParam);
|
||||
const [agencyId, setAgencyId] = useState("");
|
||||
const [sortBy, setSortBy] = useState("");
|
||||
const [sortDirection, setSortDirection] = useState("");
|
||||
const [page, setPage] = useState(1);
|
||||
const filters = useMemo(
|
||||
() => ({
|
||||
agency_id: agencyId,
|
||||
keyword: query,
|
||||
region_id: regionId,
|
||||
sort_by: sortBy,
|
||||
sort_direction: sortDirection,
|
||||
status,
|
||||
...userIdentityFilterParams(userFilter),
|
||||
}),
|
||||
[agencyId, regionId, sortBy, sortDirection, status, userFilter],
|
||||
[agencyId, query, regionId, sortBy, sortDirection, status],
|
||||
);
|
||||
const {
|
||||
data = emptyData,
|
||||
@ -46,16 +42,8 @@ export function useHostHostsPage() {
|
||||
queryKey: ["host-org", "hosts", filters, page],
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (agencyId === agencyIdParam) {
|
||||
return;
|
||||
}
|
||||
setAgencyId(agencyIdParam);
|
||||
setPage(1);
|
||||
}, [agencyId, agencyIdParam]);
|
||||
|
||||
const changeUserFilter = (value) => {
|
||||
setUserFilter(value);
|
||||
const changeQuery = (value) => {
|
||||
setQuery(value);
|
||||
setPage(1);
|
||||
};
|
||||
|
||||
@ -69,23 +57,9 @@ export function useHostHostsPage() {
|
||||
setPage(1);
|
||||
};
|
||||
|
||||
const syncAgencyIdSearchParam = (value) => {
|
||||
const nextSearchParams = new URLSearchParams(searchParams);
|
||||
const normalizedValue = String(value || "").trim();
|
||||
if (normalizedValue) {
|
||||
nextSearchParams.set("agency_id", normalizedValue);
|
||||
nextSearchParams.delete("agencyId");
|
||||
} else {
|
||||
nextSearchParams.delete("agency_id");
|
||||
nextSearchParams.delete("agencyId");
|
||||
}
|
||||
setSearchParams(nextSearchParams, { replace: true });
|
||||
};
|
||||
|
||||
const changeAgencyId = (value) => {
|
||||
setAgencyId(value);
|
||||
setPage(1);
|
||||
syncAgencyIdSearchParam(value);
|
||||
};
|
||||
|
||||
const changeSort = (field) => {
|
||||
@ -95,29 +69,29 @@ export function useHostHostsPage() {
|
||||
};
|
||||
|
||||
const resetFilters = () => {
|
||||
setUserFilter(emptyUserIdentityFilter);
|
||||
setQuery("");
|
||||
setStatus("");
|
||||
setRegionId("");
|
||||
setAgencyId("");
|
||||
setSortBy("");
|
||||
setSortDirection("");
|
||||
setPage(1);
|
||||
syncAgencyIdSearchParam("");
|
||||
};
|
||||
|
||||
return {
|
||||
abilities,
|
||||
agencyId,
|
||||
changeAgencyId,
|
||||
changeQuery,
|
||||
changeRegionId,
|
||||
changeStatus,
|
||||
changeUserFilter,
|
||||
data,
|
||||
diamondSortDirection: sortBy === "diamond" ? sortDirection : "",
|
||||
error,
|
||||
loading,
|
||||
loadingRegions,
|
||||
page,
|
||||
query,
|
||||
regionId,
|
||||
regionOptions,
|
||||
reload,
|
||||
@ -128,6 +102,5 @@ export function useHostHostsPage() {
|
||||
status,
|
||||
changeSort,
|
||||
toggleDiamondSort: () => changeSort("diamond"),
|
||||
userFilter,
|
||||
};
|
||||
}
|
||||
|
||||
@ -6,7 +6,6 @@ import { createManager, listManagers, updateManager } from "@/features/host-org/
|
||||
import { useBDAbilities } from "@/features/host-org/permissions.js";
|
||||
import { createManagerSchema } from "@/features/host-org/schema";
|
||||
import { useToast } from "@/shared/ui/ToastProvider.jsx";
|
||||
import { emptyUserIdentityFilter, userIdentityFilterParams } from "@/shared/ui/userIdentityFilter.js";
|
||||
|
||||
const pageSize = 50;
|
||||
const emptyData = { items: [], page: 1, pageSize, total: 0 };
|
||||
@ -49,17 +48,17 @@ export function useHostManagersPage() {
|
||||
const [editingManager, setEditingManager] = useState(null);
|
||||
const [loadingAction, setLoadingAction] = useState("");
|
||||
const [managerForm, setManagerForm] = useState(emptyManagerForm);
|
||||
const [userFilter, setUserFilter] = useState(emptyUserIdentityFilter);
|
||||
const [query, setQuery] = useState("");
|
||||
const [status, setStatus] = useState("");
|
||||
const [regionId, setRegionId] = useState("");
|
||||
const [page, setPage] = useState(1);
|
||||
const filters = useMemo(
|
||||
() => ({
|
||||
keyword: query,
|
||||
region_id: regionId,
|
||||
status,
|
||||
...userIdentityFilterParams(userFilter),
|
||||
}),
|
||||
[regionId, status, userFilter],
|
||||
[query, regionId, status],
|
||||
);
|
||||
const {
|
||||
data = emptyData,
|
||||
@ -75,8 +74,8 @@ export function useHostManagersPage() {
|
||||
queryKey: ["host-org", "managers", filters, page],
|
||||
});
|
||||
|
||||
const changeUserFilter = (value) => {
|
||||
setUserFilter(value);
|
||||
const changeQuery = (value) => {
|
||||
setQuery(value);
|
||||
setPage(1);
|
||||
};
|
||||
|
||||
@ -91,7 +90,7 @@ export function useHostManagersPage() {
|
||||
};
|
||||
|
||||
const resetFilters = () => {
|
||||
setUserFilter(emptyUserIdentityFilter);
|
||||
setQuery("");
|
||||
setStatus("");
|
||||
setRegionId("");
|
||||
setPage(1);
|
||||
@ -164,9 +163,9 @@ export function useHostManagersPage() {
|
||||
return {
|
||||
abilities,
|
||||
activeAction,
|
||||
changeQuery,
|
||||
changeRegionId,
|
||||
changeStatus,
|
||||
changeUserFilter,
|
||||
closeAction,
|
||||
data,
|
||||
editingManager,
|
||||
@ -178,6 +177,7 @@ export function useHostManagersPage() {
|
||||
openManagerEdit,
|
||||
openManagerForm,
|
||||
page,
|
||||
query,
|
||||
regionId,
|
||||
regionOptions,
|
||||
reload,
|
||||
@ -187,6 +187,5 @@ export function useHostManagersPage() {
|
||||
status,
|
||||
submitManager,
|
||||
toggleManager,
|
||||
userFilter,
|
||||
};
|
||||
}
|
||||
|
||||
@ -234,29 +234,6 @@
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.hostCountButton {
|
||||
display: inline-flex;
|
||||
min-width: 44px;
|
||||
min-height: 28px;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--primary);
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
font-weight: 760;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.hostCountButton:hover,
|
||||
.hostCountButton:focus-visible {
|
||||
color: var(--primary-strong);
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 3px;
|
||||
}
|
||||
|
||||
.inlineAliasText {
|
||||
color: var(--text-secondary);
|
||||
font-weight: 650;
|
||||
|
||||
@ -2,7 +2,6 @@ import Add from "@mui/icons-material/Add";
|
||||
import DeleteOutlineOutlined from "@mui/icons-material/DeleteOutlineOutlined";
|
||||
import { AdminSwitch } from "@/shared/ui/AdminSwitch.jsx";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { formatMillis } from "@/shared/utils/time.js";
|
||||
import { DataState } from "@/shared/ui/DataState.jsx";
|
||||
import { agencyStatusFilters } from "@/features/host-org/constants.js";
|
||||
@ -14,7 +13,7 @@ import { HostOrgTable } from "@/features/host-org/components/HostOrgTable.jsx";
|
||||
import { HostOrgToolbar } from "@/features/host-org/components/HostOrgToolbar.jsx";
|
||||
import { useHostAgenciesPage } from "@/features/host-org/hooks/useHostAgenciesPage.js";
|
||||
import { AdminActionIconButton, AdminRowActions } from "@/shared/ui/AdminListLayout.jsx";
|
||||
import { createOptionsColumnFilter, createTextColumnFilter, createUserIdentityColumnFilter } from "@/shared/ui/tableFilters.js";
|
||||
import { createOptionsColumnFilter, createTextColumnFilter } from "@/shared/ui/tableFilters.js";
|
||||
import styles from "@/features/host-org/host-org.module.css";
|
||||
|
||||
const agencyColumns = [
|
||||
@ -30,12 +29,6 @@ const agencyColumns = [
|
||||
render: (item) => <AgencyOwner item={item} />,
|
||||
width: "minmax(220px, 1.2fr)",
|
||||
},
|
||||
{
|
||||
key: "activeHostCount",
|
||||
label: "子 Host",
|
||||
render: (item, _index, context) => <AgencyHostCount item={item} onOpen={context?.openAgencyHosts} />,
|
||||
width: "minmax(110px, 0.65fr)",
|
||||
},
|
||||
{
|
||||
key: "salaryWallet",
|
||||
label: "工资钱包",
|
||||
@ -91,16 +84,9 @@ const agencyColumns = [
|
||||
|
||||
export function HostAgenciesPage() {
|
||||
const page = useHostAgenciesPage();
|
||||
const navigate = useNavigate();
|
||||
const createDisabled = !page.abilities.canCreate;
|
||||
const items = page.data.items || [];
|
||||
const total = page.data.total || 0;
|
||||
const openAgencyHosts = (agency) => {
|
||||
if (!agency?.agencyId) {
|
||||
return;
|
||||
}
|
||||
navigate(`/host/hosts?agency_id=${encodeURIComponent(String(agency.agencyId))}`);
|
||||
};
|
||||
const toolbarActions = [
|
||||
page.abilities.canCreate
|
||||
? { icon: <Add fontSize="small" />, label: "创建 Agency", onClick: page.openAgencyForm, variant: "primary" }
|
||||
@ -111,30 +97,22 @@ export function HostAgenciesPage() {
|
||||
return {
|
||||
...column,
|
||||
filter: createTextColumnFilter({
|
||||
placeholder: "搜索 Agency",
|
||||
placeholder: "搜索 Agency、Owner、用户",
|
||||
value: page.query,
|
||||
onChange: page.changeQuery,
|
||||
}),
|
||||
};
|
||||
}
|
||||
if (column.key === "ownerUserId") {
|
||||
if (column.key === "parentBdUserId") {
|
||||
return {
|
||||
...column,
|
||||
filter: createUserIdentityColumnFilter({
|
||||
value: page.ownerFilter,
|
||||
onChange: page.changeOwnerFilter,
|
||||
filter: createTextColumnFilter({
|
||||
placeholder: "搜索上级 BD 用户 ID",
|
||||
value: page.parentBdUserId,
|
||||
onChange: page.changeParentBdUserId,
|
||||
}),
|
||||
};
|
||||
}
|
||||
if (column.key === "parentBdUserId") {
|
||||
return {
|
||||
...column,
|
||||
filter: createUserIdentityColumnFilter({
|
||||
value: page.parentBdFilter,
|
||||
onChange: page.changeParentBdFilter,
|
||||
}),
|
||||
};
|
||||
}
|
||||
if (column.key === "status") {
|
||||
return {
|
||||
...column,
|
||||
@ -172,9 +150,9 @@ export function HostAgenciesPage() {
|
||||
<div className={styles.listBlock}>
|
||||
<HostOrgTable
|
||||
columns={columns}
|
||||
context={{ openAgencyHosts, page, regionOptions: page.regionOptions }}
|
||||
context={{ page, regionOptions: page.regionOptions }}
|
||||
items={items}
|
||||
minWidth="1450px"
|
||||
minWidth="1340px"
|
||||
pagination={
|
||||
total > 0
|
||||
? {
|
||||
@ -265,28 +243,18 @@ function ParentBD({ item }) {
|
||||
);
|
||||
}
|
||||
|
||||
function AgencyHostCount({ item, onOpen }) {
|
||||
const count = Number(item.activeHostCount ?? item.hostCount ?? 0);
|
||||
return (
|
||||
<button
|
||||
aria-label={`查看 Agency ${item.agencyId} 的 Host 列表`}
|
||||
className={styles.hostCountButton}
|
||||
type="button"
|
||||
onClick={() => onOpen?.(item)}
|
||||
>
|
||||
{count.toLocaleString("zh-CN")}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function AgencyStatusSwitch({ item, page }) {
|
||||
const active = item.status === "active";
|
||||
return (
|
||||
<AdminSwitch
|
||||
checked={active}
|
||||
disabled={!page?.abilities.canStatus || page.loadingAction === `agency-status-${item.agencyId}`}
|
||||
inputProps={{ "aria-label": active ? "关闭 Agency" : "启用 Agency" }}
|
||||
onChange={(event) => page.toggleAgencyStatusFromList(item, event.target.checked)}
|
||||
disabled={!active || !page?.abilities.canStatus || page.loadingAction === `agency-status-${item.agencyId}`}
|
||||
inputProps={{ "aria-label": active ? "关闭 Agency" : "Agency 已关闭" }}
|
||||
onChange={(event) => {
|
||||
if (!event.target.checked) {
|
||||
page.closeAgencyFromList(item);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -17,7 +17,7 @@ import { HostOrgTable } from "@/features/host-org/components/HostOrgTable.jsx";
|
||||
import { HostOrgToolbar } from "@/features/host-org/components/HostOrgToolbar.jsx";
|
||||
import { useHostBdsPage } from "@/features/host-org/hooks/useHostBdsPage.js";
|
||||
import { createRegionColumnFilter } from "@/shared/ui/RegionFilterControl.jsx";
|
||||
import { createUserIdentityColumnFilter } from "@/shared/ui/tableFilters.js";
|
||||
import { createTextColumnFilter } from "@/shared/ui/tableFilters.js";
|
||||
import styles from "@/features/host-org/host-org.module.css";
|
||||
|
||||
const bdLeaderBaseColumns = [
|
||||
@ -107,9 +107,10 @@ export function HostBdLeadersPage() {
|
||||
if (column.key === "user") {
|
||||
return {
|
||||
...column,
|
||||
filter: createUserIdentityColumnFilter({
|
||||
value: page.userFilter,
|
||||
onChange: page.changeUserFilter,
|
||||
filter: createTextColumnFilter({
|
||||
placeholder: "搜索用户 ID、展示 ID、用户名",
|
||||
value: page.query,
|
||||
onChange: page.changeQuery,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@ -12,21 +12,21 @@ afterEach(() => {
|
||||
});
|
||||
|
||||
test("bd leader table exposes user and region column filters", () => {
|
||||
const changeUserFilter = vi.fn();
|
||||
const changeQuery = vi.fn();
|
||||
const changeRegionId = vi.fn();
|
||||
vi.mocked(useHostBdsPage).mockReturnValue(pageFixture({ changeUserFilter, changeRegionId }));
|
||||
vi.mocked(useHostBdsPage).mockReturnValue(pageFixture({ changeQuery, changeRegionId }));
|
||||
|
||||
render(<HostBdLeadersPage />);
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "用户" }));
|
||||
fireEvent.change(screen.getByPlaceholderText("输入短 ID"), {
|
||||
fireEvent.change(screen.getByPlaceholderText("搜索用户 ID、展示 ID、用户名"), {
|
||||
target: { value: "164453" },
|
||||
});
|
||||
fireEvent.click(screen.getByRole("button", { name: "搜索" }));
|
||||
fireEvent.click(screen.getByRole("button", { name: "区域" }));
|
||||
fireEvent.click(screen.getByRole("menuitem", { name: "南亚区" }));
|
||||
|
||||
expect(changeUserFilter).toHaveBeenCalledWith({ displayUserId: "164453", userId: "", username: "" });
|
||||
expect(changeQuery).toHaveBeenCalledWith("164453");
|
||||
expect(changeRegionId).toHaveBeenCalledWith("1");
|
||||
});
|
||||
|
||||
@ -41,10 +41,10 @@ function pageFixture(patch = {}) {
|
||||
bdForm: { parentLeaderUserId: "", reason: "", targetUserId: "" },
|
||||
bdLeaderForm: { positionAlias: "", reason: "", targetUserId: "" },
|
||||
bdUserLookup: { error: "", loading: false, user: null },
|
||||
changeParentLeaderFilter: vi.fn(),
|
||||
changeParentLeaderUserId: vi.fn(),
|
||||
changeQuery: vi.fn(),
|
||||
changeRegionId: vi.fn(),
|
||||
changeStatus: vi.fn(),
|
||||
changeUserFilter: vi.fn(),
|
||||
closeAction: vi.fn(),
|
||||
data: { items: [leaderFixture()], page: 1, pageSize: 50, total: 1 },
|
||||
error: null,
|
||||
@ -59,7 +59,8 @@ function pageFixture(patch = {}) {
|
||||
openBDLeaderForm: vi.fn(),
|
||||
openLeaderBDs: vi.fn(),
|
||||
page: 1,
|
||||
parentLeaderFilter: { displayUserId: "", userId: "", username: "" },
|
||||
parentLeaderUserId: "",
|
||||
query: "",
|
||||
regionId: "",
|
||||
regionOptions: [{ label: "南亚区", value: "1" }],
|
||||
reload: vi.fn(),
|
||||
@ -74,7 +75,6 @@ function pageFixture(patch = {}) {
|
||||
submitBDLeader: vi.fn(),
|
||||
submitLeaderBD: vi.fn(),
|
||||
toggleBDLeader: vi.fn(),
|
||||
userFilter: { displayUserId: "", userId: "", username: "" },
|
||||
...patch,
|
||||
};
|
||||
}
|
||||
|
||||
@ -12,7 +12,7 @@ import { HostOrgTable } from "@/features/host-org/components/HostOrgTable.jsx";
|
||||
import { HostOrgToolbar } from "@/features/host-org/components/HostOrgToolbar.jsx";
|
||||
import { useHostBdsPage } from "@/features/host-org/hooks/useHostBdsPage.js";
|
||||
import { AdminRowActions } from "@/shared/ui/AdminListLayout.jsx";
|
||||
import { createOptionsColumnFilter, createUserIdentityColumnFilter } from "@/shared/ui/tableFilters.js";
|
||||
import { createOptionsColumnFilter, createTextColumnFilter } from "@/shared/ui/tableFilters.js";
|
||||
import styles from "@/features/host-org/host-org.module.css";
|
||||
|
||||
const bdColumns = [
|
||||
@ -22,12 +22,6 @@ const bdColumns = [
|
||||
render: (item) => <BDUser item={item} />,
|
||||
width: "minmax(220px, 1.25fr)",
|
||||
},
|
||||
{
|
||||
key: "agencyCount",
|
||||
label: "子 Agency",
|
||||
render: (item) => <span className={styles.inlineValue}>{formatCount(item.agencyCount)}</span>,
|
||||
width: "minmax(110px, 0.65fr)",
|
||||
},
|
||||
{ key: "role", label: "角色", width: "minmax(100px, 0.65fr)" },
|
||||
{
|
||||
key: "salaryWallet",
|
||||
@ -103,18 +97,20 @@ export function HostBdsPage() {
|
||||
if (column.key === "user") {
|
||||
return {
|
||||
...column,
|
||||
filter: createUserIdentityColumnFilter({
|
||||
value: page.userFilter,
|
||||
onChange: page.changeUserFilter,
|
||||
filter: createTextColumnFilter({
|
||||
placeholder: "搜索用户 ID、展示 ID、用户名",
|
||||
value: page.query,
|
||||
onChange: page.changeQuery,
|
||||
}),
|
||||
};
|
||||
}
|
||||
if (column.key === "parentLeaderUserId") {
|
||||
return {
|
||||
...column,
|
||||
filter: createUserIdentityColumnFilter({
|
||||
value: page.parentLeaderFilter,
|
||||
onChange: page.changeParentLeaderFilter,
|
||||
filter: createTextColumnFilter({
|
||||
placeholder: "搜索 Leader 用户 ID",
|
||||
value: page.parentLeaderUserId,
|
||||
onChange: page.changeParentLeaderUserId,
|
||||
}),
|
||||
};
|
||||
}
|
||||
@ -157,7 +153,7 @@ export function HostBdsPage() {
|
||||
columns={columns}
|
||||
context={{ page, regionOptions: page.regionOptions }}
|
||||
items={items}
|
||||
minWidth="1550px"
|
||||
minWidth="1440px"
|
||||
pagination={
|
||||
total > 0
|
||||
? {
|
||||
@ -277,7 +273,3 @@ function BDActions({ item, page }) {
|
||||
function bdUserLabel(item) {
|
||||
return item.displayUserId || item.username || item.userId;
|
||||
}
|
||||
|
||||
function formatCount(value) {
|
||||
return Number(value || 0).toLocaleString("zh-CN");
|
||||
}
|
||||
|
||||
@ -30,7 +30,7 @@ import { HostOrgToolbar } from "@/features/host-org/components/HostOrgToolbar.js
|
||||
import { useHostCoinSellersPage } from "@/features/host-org/hooks/useHostCoinSellersPage.js";
|
||||
import { CoinSellerLedgerTable } from "@/features/operations/components/CoinSellerLedgerTable.jsx";
|
||||
import { createRegionColumnFilter } from "@/shared/ui/RegionFilterControl.jsx";
|
||||
import { createOptionsColumnFilter, createUserIdentityColumnFilter } from "@/shared/ui/tableFilters.js";
|
||||
import { createOptionsColumnFilter, createTextColumnFilter } from "@/shared/ui/tableFilters.js";
|
||||
import styles from "@/features/host-org/host-org.module.css";
|
||||
|
||||
const baseColumns = [
|
||||
@ -106,9 +106,10 @@ export function HostCoinSellersPage() {
|
||||
if (column.key === "seller") {
|
||||
return {
|
||||
...column,
|
||||
filter: createUserIdentityColumnFilter({
|
||||
value: page.userFilter,
|
||||
onChange: page.changeUserFilter,
|
||||
filter: createTextColumnFilter({
|
||||
placeholder: "搜索用户 ID、展示 ID、用户名",
|
||||
value: page.query,
|
||||
onChange: page.changeQuery,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@ -8,7 +8,7 @@ import { SalaryWalletAdjustButton, SalaryWalletCell } from "@/features/host-org/
|
||||
import { HostOrgTable } from "@/features/host-org/components/HostOrgTable.jsx";
|
||||
import { useHostHostsPage } from "@/features/host-org/hooks/useHostHostsPage.js";
|
||||
import { AdminRowActions } from "@/shared/ui/AdminListLayout.jsx";
|
||||
import { createOptionsColumnFilter, createTextColumnFilter, createUserIdentityColumnFilter } from "@/shared/ui/tableFilters.js";
|
||||
import { createOptionsColumnFilter, createTextColumnFilter } from "@/shared/ui/tableFilters.js";
|
||||
import styles from "@/features/host-org/host-org.module.css";
|
||||
|
||||
const hostColumns = [
|
||||
@ -99,9 +99,10 @@ export function HostHostsPage() {
|
||||
if (column.key === "user") {
|
||||
return {
|
||||
...column,
|
||||
filter: createUserIdentityColumnFilter({
|
||||
value: page.userFilter,
|
||||
onChange: page.changeUserFilter,
|
||||
filter: createTextColumnFilter({
|
||||
placeholder: "搜索用户 ID、展示 ID、用户名、Agency",
|
||||
value: page.query,
|
||||
onChange: page.changeQuery,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@ -12,7 +12,7 @@ import { HostOrgPerson, hostOrgRegionName } from "@/features/host-org/components
|
||||
import { HostOrgTable } from "@/features/host-org/components/HostOrgTable.jsx";
|
||||
import { HostOrgToolbar } from "@/features/host-org/components/HostOrgToolbar.jsx";
|
||||
import { useHostManagersPage } from "@/features/host-org/hooks/useHostManagersPage.js";
|
||||
import { createOptionsColumnFilter, createUserIdentityColumnFilter } from "@/shared/ui/tableFilters.js";
|
||||
import { createOptionsColumnFilter, createTextColumnFilter } from "@/shared/ui/tableFilters.js";
|
||||
import styles from "@/features/host-org/host-org.module.css";
|
||||
|
||||
const managerPermissionOptions = [
|
||||
@ -100,9 +100,10 @@ export function HostManagersPage() {
|
||||
if (column.key === "userId") {
|
||||
return {
|
||||
...column,
|
||||
filter: createUserIdentityColumnFilter({
|
||||
value: page.userFilter,
|
||||
onChange: page.changeUserFilter,
|
||||
filter: createTextColumnFilter({
|
||||
placeholder: "搜索经理",
|
||||
value: page.query,
|
||||
onChange: page.changeQuery,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import { afterEach, expect, test, vi } from "vitest";
|
||||
import { MemoryRouter, useLocation } from "react-router-dom";
|
||||
import { HostAgenciesPage } from "./HostAgenciesPage.jsx";
|
||||
import { HostBdLeadersPage } from "./HostBdLeadersPage.jsx";
|
||||
import { HostBdsPage } from "./HostBdsPage.jsx";
|
||||
@ -29,37 +28,12 @@ test("agency list salary wallet header requests salary wallet sorting", () => {
|
||||
const changeSort = vi.fn();
|
||||
vi.mocked(useHostAgenciesPage).mockReturnValue(agencyPageFixture({ changeSort }));
|
||||
|
||||
renderWithRouter(<HostAgenciesPage />);
|
||||
render(<HostAgenciesPage />);
|
||||
fireEvent.click(screen.getByLabelText("按工资钱包降序排序"));
|
||||
|
||||
expect(changeSort).toHaveBeenCalledWith("salary_wallet");
|
||||
});
|
||||
|
||||
test("agency host count opens filtered host list", () => {
|
||||
vi.mocked(useHostAgenciesPage).mockReturnValue(agencyPageFixture());
|
||||
|
||||
const location = renderWithRouter(<HostAgenciesPage />);
|
||||
fireEvent.click(screen.getByRole("button", { name: "查看 Agency 9001 的 Host 列表" }));
|
||||
|
||||
expect(location.current()).toBe("/host/hosts?agency_id=9001");
|
||||
});
|
||||
|
||||
test("closed agency status switch can reopen agency", () => {
|
||||
const toggleAgencyStatusFromList = vi.fn();
|
||||
vi.mocked(useHostAgenciesPage).mockReturnValue(
|
||||
agencyPageFixture({
|
||||
abilities: { canCreate: false, canDelete: false, canSalaryAdjust: false, canStatus: true },
|
||||
data: { items: [{ ...agencyFixture(), joinEnabled: false, status: "closed" }], page: 1, pageSize: 50, total: 1 },
|
||||
toggleAgencyStatusFromList,
|
||||
}),
|
||||
);
|
||||
|
||||
renderWithRouter(<HostAgenciesPage />);
|
||||
fireEvent.click(screen.getByRole("switch", { name: "启用 Agency" }));
|
||||
|
||||
expect(toggleAgencyStatusFromList).toHaveBeenCalledWith(expect.objectContaining({ agencyId: "9001" }), true);
|
||||
});
|
||||
|
||||
test("bd leader list salary wallet header requests salary wallet sorting", () => {
|
||||
const changeSort = vi.fn();
|
||||
vi.mocked(useHostBdsPage).mockReturnValue(bdPageFixture({ changeSort, profileType: "leader" }));
|
||||
@ -80,20 +54,6 @@ test("bd list salary wallet header requests salary wallet sorting", () => {
|
||||
expect(changeSort).toHaveBeenCalledWith("salary_wallet");
|
||||
});
|
||||
|
||||
test("bd list renders child agency count", () => {
|
||||
vi.mocked(useHostBdsPage).mockReturnValue(
|
||||
bdPageFixture({
|
||||
data: { items: [{ ...bdFixture(), agencyCount: 7 }], page: 1, pageSize: 50, total: 1 },
|
||||
profileType: "bd",
|
||||
}),
|
||||
);
|
||||
|
||||
render(<HostBdsPage />);
|
||||
|
||||
expect(screen.getByText("子 Agency")).toBeInTheDocument();
|
||||
expect(screen.getByText("7")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("host list salary wallet header requests salary wallet sorting", () => {
|
||||
const changeSort = vi.fn();
|
||||
vi.mocked(useHostHostsPage).mockReturnValue(hostPageFixture({ changeSort }));
|
||||
@ -136,7 +96,6 @@ function agencyPageFixture(patch = {}) {
|
||||
sortDirection: "",
|
||||
status: "",
|
||||
submitAgency: vi.fn(),
|
||||
toggleAgencyStatusFromList: vi.fn(),
|
||||
toggleAgencyJoinEnabledFromList: vi.fn(),
|
||||
...patch,
|
||||
};
|
||||
@ -225,7 +184,6 @@ function hostPageFixture(patch = {}) {
|
||||
|
||||
function agencyFixture() {
|
||||
return {
|
||||
activeHostCount: 12,
|
||||
agencyId: "9001",
|
||||
joinEnabled: true,
|
||||
name: "Agency One",
|
||||
@ -242,7 +200,6 @@ function agencyFixture() {
|
||||
|
||||
function bdFixture() {
|
||||
return {
|
||||
agencyCount: 3,
|
||||
createdAtMs: 1760000000000,
|
||||
displayUserId: "160002",
|
||||
parentLeaderUserId: "3001",
|
||||
@ -272,19 +229,3 @@ function hostFixture() {
|
||||
username: "host_user",
|
||||
};
|
||||
}
|
||||
|
||||
function renderWithRouter(ui) {
|
||||
const location = { value: "" };
|
||||
function LocationProbe() {
|
||||
const currentLocation = useLocation();
|
||||
location.value = `${currentLocation.pathname}${currentLocation.search}`;
|
||||
return null;
|
||||
}
|
||||
render(
|
||||
<MemoryRouter initialEntries={["/host/agencies"]}>
|
||||
{ui}
|
||||
<LocationProbe />
|
||||
</MemoryRouter>,
|
||||
);
|
||||
return { current: () => location.value };
|
||||
}
|
||||
|
||||
@ -7,7 +7,6 @@ import {
|
||||
import { useInviteActivityRewardAbilities } from "@/features/invite-activity-reward/permissions.js";
|
||||
import { usePaginatedQuery } from "@/shared/hooks/usePaginatedQuery.js";
|
||||
import { useToast } from "@/shared/ui/ToastProvider.jsx";
|
||||
import { emptyUserIdentityFilter, userIdentityFilterParams } from "@/shared/ui/userIdentityFilter.js";
|
||||
|
||||
const pageSize = 50;
|
||||
const emptySummaries = { items: [], page: 1, pageSize, total: 0 };
|
||||
@ -26,12 +25,12 @@ export function useInviteActivityRewardPage() {
|
||||
const [form, setForm] = useState(emptyForm);
|
||||
const [configLoading, setConfigLoading] = useState(false);
|
||||
const [configSaving, setConfigSaving] = useState(false);
|
||||
const [userFilter, setUserFilter] = useState(emptyUserIdentityFilter);
|
||||
const [query, setQuery] = useState("");
|
||||
const [cycleKey, setCycleKey] = useState("");
|
||||
const [page, setPage] = useState(1);
|
||||
const filters = useMemo(
|
||||
() => ({ cycle_key: cycleKey, ...userIdentityFilterParams(userFilter) }),
|
||||
[cycleKey, userFilter],
|
||||
() => ({ keyword: query, cycle_key: cycleKey }),
|
||||
[cycleKey, query],
|
||||
);
|
||||
const {
|
||||
data: summaries = emptySummaries,
|
||||
@ -74,8 +73,8 @@ export function useInviteActivityRewardPage() {
|
||||
setForm(config ? formFromConfig(config) : emptyForm);
|
||||
}, [config]);
|
||||
|
||||
const changeUserFilter = (value) => {
|
||||
setUserFilter(value);
|
||||
const changeQuery = (value) => {
|
||||
setQuery(value);
|
||||
setPage(1);
|
||||
};
|
||||
const changeCycleKey = (value) => {
|
||||
@ -105,7 +104,7 @@ export function useInviteActivityRewardPage() {
|
||||
return {
|
||||
abilities,
|
||||
changeCycleKey,
|
||||
changeUserFilter,
|
||||
changeQuery,
|
||||
closeConfigDrawer,
|
||||
config,
|
||||
configDrawerOpen,
|
||||
@ -115,6 +114,7 @@ export function useInviteActivityRewardPage() {
|
||||
form,
|
||||
openConfigDrawer,
|
||||
page,
|
||||
query,
|
||||
reloadConfig,
|
||||
setForm,
|
||||
setPage,
|
||||
@ -123,7 +123,6 @@ export function useInviteActivityRewardPage() {
|
||||
summariesError,
|
||||
summariesLoading,
|
||||
submitConfig,
|
||||
userFilter,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -9,7 +9,7 @@ import { AdminListBody, AdminListPage } from "@/shared/ui/AdminListLayout.jsx";
|
||||
import { AdminUserIdentity } from "@/shared/ui/AdminUserIdentity.jsx";
|
||||
import { DataState } from "@/shared/ui/DataState.jsx";
|
||||
import { DataTable } from "@/shared/ui/DataTable.jsx";
|
||||
import { createTextColumnFilter, createUserIdentityColumnFilter } from "@/shared/ui/tableFilters.js";
|
||||
import { createTextColumnFilter } from "@/shared/ui/tableFilters.js";
|
||||
import { formatMillis } from "@/shared/utils/time.js";
|
||||
|
||||
export function InviteActivityRewardPage() {
|
||||
@ -25,9 +25,10 @@ export function InviteActivityRewardPage() {
|
||||
label: "用户信息",
|
||||
width: "minmax(280px, 1.1fr)",
|
||||
render: (summary) => <SummaryUser expanded={expandedKey === summary.key} summary={summary} />,
|
||||
filter: createUserIdentityColumnFilter({
|
||||
value: page.userFilter,
|
||||
onChange: page.changeUserFilter,
|
||||
filter: createTextColumnFilter({
|
||||
placeholder: "搜索用户 ID、短号、名称",
|
||||
value: page.query,
|
||||
onChange: page.changeQuery,
|
||||
}),
|
||||
},
|
||||
{
|
||||
|
||||
@ -8,8 +8,7 @@ import { SideDrawer } from "@/shared/ui/SideDrawer.jsx";
|
||||
import { usePaginatedQuery } from "@/shared/hooks/usePaginatedQuery.js";
|
||||
import { TimeRangeFilter } from "@/shared/ui/TimeRangeFilter.jsx";
|
||||
import { TimeText } from "@/shared/ui/TimeText.jsx";
|
||||
import { createOptionsColumnFilter, createUserIdentityColumnFilter } from "@/shared/ui/tableFilters.js";
|
||||
import { emptyUserIdentityFilter, isUserIdentityFilterEmpty, userIdentityFilterParams } from "@/shared/ui/userIdentityFilter.js";
|
||||
import { createOptionsColumnFilter, createTextColumnFilter } from "@/shared/ui/tableFilters.js";
|
||||
import styles from "@/features/operations/operations.module.css";
|
||||
|
||||
const pageSize = 50;
|
||||
@ -144,7 +143,7 @@ export function CoinLedgerTable({ lockedUser = null, userId = "" }) {
|
||||
const [page, setPage] = useState(1);
|
||||
const [selectedEntry, setSelectedEntry] = useState(null);
|
||||
const [bizType, setBizType] = useState("");
|
||||
const [userFilter, setUserFilter] = useState(emptyUserIdentityFilter);
|
||||
const [userKeyword, setUserKeyword] = useState("");
|
||||
const [timeRange, setTimeRange] = useState({ endMs: "", startMs: "" });
|
||||
|
||||
const filters = useMemo(
|
||||
@ -152,10 +151,9 @@ export function CoinLedgerTable({ lockedUser = null, userId = "" }) {
|
||||
biz_type: bizType,
|
||||
end_at_ms: timeRange.endMs || "",
|
||||
start_at_ms: timeRange.startMs || "",
|
||||
...userIdentityFilterParams(userFilter),
|
||||
user_id: fixedUserId || userFilter.userId,
|
||||
user_keyword: fixedUserId || userKeyword,
|
||||
}),
|
||||
[bizType, fixedUserId, timeRange.endMs, timeRange.startMs, userFilter],
|
||||
[bizType, fixedUserId, timeRange.endMs, timeRange.startMs, userKeyword],
|
||||
);
|
||||
const query = usePaginatedQuery({
|
||||
errorMessage: "获取金币流水失败",
|
||||
@ -168,14 +166,14 @@ export function CoinLedgerTable({ lockedUser = null, userId = "" }) {
|
||||
const data = query.data || { items: [], total: 0, page, pageSize };
|
||||
const items = data.items || [];
|
||||
const total = data.total || 0;
|
||||
const hasMutableFilters = Boolean(!isUserIdentityFilterEmpty(userFilter) || bizType || timeRange.startMs || timeRange.endMs);
|
||||
const hasMutableFilters = Boolean(userKeyword || bizType || timeRange.startMs || timeRange.endMs);
|
||||
|
||||
const changeBizType = (value) => {
|
||||
setBizType(value);
|
||||
setPage(1);
|
||||
};
|
||||
const changeUserFilter = (value) => {
|
||||
setUserFilter(value);
|
||||
const changeUserKeyword = (value) => {
|
||||
setUserKeyword(value);
|
||||
setPage(1);
|
||||
};
|
||||
const changeTimeRange = (nextRange) => {
|
||||
@ -184,7 +182,7 @@ export function CoinLedgerTable({ lockedUser = null, userId = "" }) {
|
||||
};
|
||||
const resetFilters = () => {
|
||||
setBizType("");
|
||||
setUserFilter(emptyUserIdentityFilter);
|
||||
setUserKeyword("");
|
||||
setTimeRange({ endMs: "", startMs: "" });
|
||||
setPage(1);
|
||||
};
|
||||
@ -198,9 +196,10 @@ export function CoinLedgerTable({ lockedUser = null, userId = "" }) {
|
||||
if (column.key === "user") {
|
||||
return {
|
||||
...column,
|
||||
filter: createUserIdentityColumnFilter({
|
||||
value: userFilter,
|
||||
onChange: changeUserFilter,
|
||||
filter: createTextColumnFilter({
|
||||
placeholder: "长 ID / 短 ID / 昵称",
|
||||
value: userKeyword,
|
||||
onChange: changeUserKeyword,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@ -10,9 +10,8 @@ import { DataState } from "@/shared/ui/DataState.jsx";
|
||||
import { DataTable } from "@/shared/ui/DataTable.jsx";
|
||||
import { AdminUserIdentity } from "@/shared/ui/AdminUserIdentity.jsx";
|
||||
import { TimeRangeFilter } from "@/shared/ui/TimeRangeFilter.jsx";
|
||||
import { createOptionsColumnFilter, createUserIdentityColumnFilter } from "@/shared/ui/tableFilters.js";
|
||||
import { createOptionsColumnFilter, createTextColumnFilter } from "@/shared/ui/tableFilters.js";
|
||||
import { useToast } from "@/shared/ui/ToastProvider.jsx";
|
||||
import { emptyUserIdentityFilter, isUserIdentityFilterEmpty, userIdentityFilterParams } from "@/shared/ui/userIdentityFilter.js";
|
||||
import { formatMillis } from "@/shared/utils/time.js";
|
||||
|
||||
const pageSize = 50;
|
||||
@ -99,7 +98,7 @@ export function CoinSellerLedgerTable({ lockedSeller = null, sellerUserId = "" }
|
||||
const fixedSellerUserId = String(sellerUserId || lockedSeller?.userId || "").trim();
|
||||
const [page, setPage] = useState(1);
|
||||
const [exporting, setExporting] = useState(false);
|
||||
const [sellerFilter, setSellerFilter] = useState(emptyUserIdentityFilter);
|
||||
const [sellerKeyword, setSellerKeyword] = useState("");
|
||||
const [ledgerType, setLedgerType] = useState("");
|
||||
const [timeRange, setTimeRange] = useState({ endMs: "", startMs: "" });
|
||||
const { showToast } = useToast();
|
||||
@ -108,15 +107,11 @@ export function CoinSellerLedgerTable({ lockedSeller = null, sellerUserId = "" }
|
||||
() => ({
|
||||
end_at_ms: timeRange.endMs || "",
|
||||
ledger_type: ledgerType,
|
||||
seller_keyword: fixedSellerUserId ? "" : sellerKeyword,
|
||||
seller_user_id: fixedSellerUserId,
|
||||
start_at_ms: timeRange.startMs || "",
|
||||
...userIdentityFilterParams(sellerFilter, {
|
||||
displayUserId: "seller_display_user_id",
|
||||
userId: "seller_filter_user_id",
|
||||
username: "seller_username",
|
||||
}),
|
||||
}),
|
||||
[fixedSellerUserId, ledgerType, sellerFilter, timeRange.endMs, timeRange.startMs],
|
||||
[fixedSellerUserId, ledgerType, sellerKeyword, timeRange.endMs, timeRange.startMs],
|
||||
);
|
||||
const query = usePaginatedQuery({
|
||||
errorMessage: "获取币商流水失败",
|
||||
@ -129,10 +124,10 @@ export function CoinSellerLedgerTable({ lockedSeller = null, sellerUserId = "" }
|
||||
const data = query.data || { items: [], total: 0, page, pageSize };
|
||||
const items = data.items || [];
|
||||
const total = data.total || 0;
|
||||
const hasActiveFilters = Boolean(!isUserIdentityFilterEmpty(sellerFilter) || ledgerType || timeRange.startMs || timeRange.endMs);
|
||||
const hasActiveFilters = Boolean(sellerKeyword || ledgerType || timeRange.startMs || timeRange.endMs);
|
||||
|
||||
const changeSellerFilter = (value) => {
|
||||
setSellerFilter(value);
|
||||
const changeSellerKeyword = (value) => {
|
||||
setSellerKeyword(value);
|
||||
setPage(1);
|
||||
};
|
||||
const changeLedgerType = (value) => {
|
||||
@ -144,7 +139,7 @@ export function CoinSellerLedgerTable({ lockedSeller = null, sellerUserId = "" }
|
||||
setPage(1);
|
||||
};
|
||||
const resetFilters = () => {
|
||||
setSellerFilter(emptyUserIdentityFilter);
|
||||
setSellerKeyword("");
|
||||
setLedgerType("");
|
||||
setTimeRange({ endMs: "", startMs: "" });
|
||||
setPage(1);
|
||||
@ -167,9 +162,10 @@ export function CoinSellerLedgerTable({ lockedSeller = null, sellerUserId = "" }
|
||||
if (column.key === "seller" && !fixedSellerUserId) {
|
||||
return {
|
||||
...column,
|
||||
filter: createUserIdentityColumnFilter({
|
||||
value: sellerFilter,
|
||||
onChange: changeSellerFilter,
|
||||
filter: createTextColumnFilter({
|
||||
placeholder: "长 ID / 短 ID / 名称",
|
||||
value: sellerKeyword,
|
||||
onChange: changeSellerKeyword,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@ -26,8 +26,7 @@ import { DataTable } from "@/shared/ui/DataTable.jsx";
|
||||
import { AdminUserIdentity } from "@/shared/ui/AdminUserIdentity.jsx";
|
||||
import { TimeRangeFilter } from "@/shared/ui/TimeRangeFilter.jsx";
|
||||
import { useToast } from "@/shared/ui/ToastProvider.jsx";
|
||||
import { createUserIdentityColumnFilter } from "@/shared/ui/tableFilters.js";
|
||||
import { emptyUserIdentityFilter, isUserIdentityFilterEmpty, userIdentityFilterParams } from "@/shared/ui/userIdentityFilter.js";
|
||||
import { createTextColumnFilter } from "@/shared/ui/tableFilters.js";
|
||||
import { formatMillis } from "@/shared/utils/time.js";
|
||||
|
||||
const pageSize = 50;
|
||||
@ -84,7 +83,7 @@ export function CoinAdjustmentPage() {
|
||||
const abilities = useCoinAdjustmentAbilities();
|
||||
const { showToast } = useToast();
|
||||
const [page, setPage] = useState(1);
|
||||
const [userFilter, setUserFilter] = useState(emptyUserIdentityFilter);
|
||||
const [userKeyword, setUserKeyword] = useState("");
|
||||
const [timeRange, setTimeRange] = useState({ endMs: "", startMs: "" });
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [form, setForm] = useState(emptyForm);
|
||||
@ -96,9 +95,9 @@ export function CoinAdjustmentPage() {
|
||||
() => ({
|
||||
end_at_ms: timeRange.endMs || "",
|
||||
start_at_ms: timeRange.startMs || "",
|
||||
...userIdentityFilterParams(userFilter),
|
||||
user_keyword: userKeyword,
|
||||
}),
|
||||
[timeRange.endMs, timeRange.startMs, userFilter],
|
||||
[timeRange.endMs, timeRange.startMs, userKeyword],
|
||||
);
|
||||
const query = usePaginatedQuery({
|
||||
errorMessage: "获取金币增减列表失败",
|
||||
@ -116,10 +115,11 @@ export function CoinAdjustmentPage() {
|
||||
column.key === "user"
|
||||
? {
|
||||
...column,
|
||||
filter: createUserIdentityColumnFilter({
|
||||
value: userFilter,
|
||||
filter: createTextColumnFilter({
|
||||
placeholder: "长 ID / 短 ID / 昵称",
|
||||
value: userKeyword,
|
||||
onChange: (value) => {
|
||||
setUserFilter(value);
|
||||
setUserKeyword(value);
|
||||
setPage(1);
|
||||
},
|
||||
}),
|
||||
@ -128,7 +128,7 @@ export function CoinAdjustmentPage() {
|
||||
);
|
||||
|
||||
const resetFilters = () => {
|
||||
setUserFilter(emptyUserIdentityFilter);
|
||||
setUserKeyword("");
|
||||
setTimeRange({ endMs: "", startMs: "" });
|
||||
setPage(1);
|
||||
};
|
||||
@ -210,7 +210,7 @@ export function CoinAdjustmentPage() {
|
||||
<>
|
||||
<TimeRangeFilter value={timeRange} onChange={changeTimeRange} />
|
||||
<AdminFilterResetButton
|
||||
disabled={isUserIdentityFilterEmpty(userFilter) && !timeRange.startMs && !timeRange.endMs}
|
||||
disabled={!userKeyword && !timeRange.startMs && !timeRange.endMs}
|
||||
onClick={resetFilters}
|
||||
/>
|
||||
</>
|
||||
|
||||
@ -14,8 +14,7 @@ import { TimeRangeFilter } from "@/shared/ui/TimeRangeFilter.jsx";
|
||||
import { useRegionOptions } from "@/shared/hooks/useRegionOptions.js";
|
||||
import { usePaginatedQuery } from "@/shared/hooks/usePaginatedQuery.js";
|
||||
import { useToast } from "@/shared/ui/ToastProvider.jsx";
|
||||
import { createOptionsColumnFilter, createTextColumnFilter, createUserIdentityColumnFilter } from "@/shared/ui/tableFilters.js";
|
||||
import { emptyUserIdentityFilter, userIdentityFilterParams } from "@/shared/ui/userIdentityFilter.js";
|
||||
import { createOptionsColumnFilter, createTextColumnFilter } from "@/shared/ui/tableFilters.js";
|
||||
import { formatMillis } from "@/shared/utils/time.js";
|
||||
import styles from "@/features/payment/payment.module.css";
|
||||
|
||||
@ -96,8 +95,8 @@ export function PaymentBillListPage() {
|
||||
const [status, setStatus] = useState("");
|
||||
const [rechargeType, setRechargeType] = useState("");
|
||||
const [regionId, setRegionId] = useState("");
|
||||
const [userFilter, setUserFilter] = useState(emptyUserIdentityFilter);
|
||||
const [sellerFilter, setSellerFilter] = useState(emptyUserIdentityFilter);
|
||||
const [userId, setUserId] = useState("");
|
||||
const [sellerUserId, setSellerUserId] = useState("");
|
||||
const [timeRange, setTimeRange] = useState({ endMs: "", startMs: "" });
|
||||
const [exporting, setExporting] = useState(false);
|
||||
const { loadingRegions, regionOptions } = useRegionOptions();
|
||||
@ -110,16 +109,12 @@ export function PaymentBillListPage() {
|
||||
keyword,
|
||||
recharge_type: rechargeType,
|
||||
region_id: regionId,
|
||||
seller_user_id: sellerUserId,
|
||||
start_at_ms: timeRange.startMs,
|
||||
status,
|
||||
...userIdentityFilterParams(userFilter),
|
||||
...userIdentityFilterParams(sellerFilter, {
|
||||
displayUserId: "seller_display_user_id",
|
||||
userId: "seller_user_id",
|
||||
username: "seller_username",
|
||||
}),
|
||||
user_id: userId,
|
||||
}),
|
||||
[keyword, rechargeType, regionId, sellerFilter, status, timeRange.endMs, timeRange.startMs, userFilter],
|
||||
[keyword, rechargeType, regionId, sellerUserId, status, timeRange.endMs, timeRange.startMs, userId],
|
||||
);
|
||||
const query = usePaginatedQuery({
|
||||
errorMessage: "获取账单列表失败",
|
||||
@ -168,18 +163,20 @@ export function PaymentBillListPage() {
|
||||
if (column.key === "userId") {
|
||||
return {
|
||||
...column,
|
||||
filter: createUserIdentityColumnFilter({
|
||||
value: userFilter,
|
||||
onChange: resetPage(setUserFilter),
|
||||
filter: createTextColumnFilter({
|
||||
placeholder: "搜索用户 ID",
|
||||
value: userId,
|
||||
onChange: (value) => resetPage(setUserId)(digitsOnly(value)),
|
||||
}),
|
||||
};
|
||||
}
|
||||
if (column.key === "sellerUserId") {
|
||||
return {
|
||||
...column,
|
||||
filter: createUserIdentityColumnFilter({
|
||||
value: sellerFilter,
|
||||
onChange: resetPage(setSellerFilter),
|
||||
filter: createTextColumnFilter({
|
||||
placeholder: "搜索币商 ID",
|
||||
value: sellerUserId,
|
||||
onChange: (value) => resetPage(setSellerUserId)(digitsOnly(value)),
|
||||
}),
|
||||
};
|
||||
}
|
||||
@ -510,3 +507,7 @@ function filenameTimestamp() {
|
||||
function pad2(value) {
|
||||
return String(value).padStart(2, "0");
|
||||
}
|
||||
|
||||
function digitsOnly(value) {
|
||||
return value.replace(/\D/g, "");
|
||||
}
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { usePaginatedQuery } from "@/shared/hooks/usePaginatedQuery.js";
|
||||
import { useToast } from "@/shared/ui/ToastProvider.jsx";
|
||||
import { emptyUserIdentityFilter, userIdentityFilterParams } from "@/shared/ui/userIdentityFilter.js";
|
||||
import { listResourceGroups } from "@/features/resources/api";
|
||||
import {
|
||||
getRegistrationRewardConfig,
|
||||
@ -31,7 +30,7 @@ export function useRegistrationRewardPage() {
|
||||
const [configLoading, setConfigLoading] = useState(false);
|
||||
const [configSaving, setConfigSaving] = useState(false);
|
||||
const [resourceGroups, setResourceGroups] = useState([]);
|
||||
const [userFilter, setUserFilter] = useState(emptyUserIdentityFilter);
|
||||
const [query, setQuery] = useState("");
|
||||
const [status, setStatus] = useState("");
|
||||
const [timeRange, setTimeRange] = useState(emptyTimeRange);
|
||||
const [page, setPage] = useState(1);
|
||||
@ -39,10 +38,10 @@ export function useRegistrationRewardPage() {
|
||||
() => ({
|
||||
claimed_end_ms: timeRange.endMs || undefined,
|
||||
claimed_start_ms: timeRange.startMs || undefined,
|
||||
keyword: query,
|
||||
status,
|
||||
...userIdentityFilterParams(userFilter),
|
||||
}),
|
||||
[status, timeRange, userFilter],
|
||||
[query, status, timeRange],
|
||||
);
|
||||
const {
|
||||
data: claims = emptyClaims,
|
||||
@ -95,8 +94,8 @@ export function useRegistrationRewardPage() {
|
||||
setForm(emptyForm);
|
||||
}, [config]);
|
||||
|
||||
const changeUserFilter = (value) => {
|
||||
setUserFilter(value);
|
||||
const changeQuery = (value) => {
|
||||
setQuery(value);
|
||||
setPage(1);
|
||||
};
|
||||
|
||||
@ -111,7 +110,7 @@ export function useRegistrationRewardPage() {
|
||||
};
|
||||
|
||||
const resetFilters = () => {
|
||||
setUserFilter(emptyUserIdentityFilter);
|
||||
setQuery("");
|
||||
setStatus("");
|
||||
setTimeRange(emptyTimeRange);
|
||||
setPage(1);
|
||||
@ -139,9 +138,9 @@ export function useRegistrationRewardPage() {
|
||||
|
||||
return {
|
||||
abilities,
|
||||
changeQuery,
|
||||
changeStatus,
|
||||
changeTimeRange,
|
||||
changeUserFilter,
|
||||
claims,
|
||||
claimsError,
|
||||
claimsLoading,
|
||||
@ -153,6 +152,7 @@ export function useRegistrationRewardPage() {
|
||||
form,
|
||||
openConfigDrawer,
|
||||
page,
|
||||
query,
|
||||
reloadClaims,
|
||||
reloadConfig,
|
||||
resetFilters,
|
||||
@ -162,7 +162,6 @@ export function useRegistrationRewardPage() {
|
||||
status,
|
||||
submitConfig,
|
||||
timeRange,
|
||||
userFilter,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@ import { DataState } from "@/shared/ui/DataState.jsx";
|
||||
import { DataTable } from "@/shared/ui/DataTable.jsx";
|
||||
import { AdminListBody, AdminListPage } from "@/shared/ui/AdminListLayout.jsx";
|
||||
import { AdminUserIdentity } from "@/shared/ui/AdminUserIdentity.jsx";
|
||||
import { createOptionsColumnFilter, createUserIdentityColumnFilter } from "@/shared/ui/tableFilters.js";
|
||||
import { createOptionsColumnFilter, createTextColumnFilter } from "@/shared/ui/tableFilters.js";
|
||||
import { formatMillis } from "@/shared/utils/time.js";
|
||||
import { RegistrationRewardConfigDrawer } from "@/features/registration-reward/components/RegistrationRewardConfigDrawer.jsx";
|
||||
import { RegistrationRewardConfigSummary } from "@/features/registration-reward/components/RegistrationRewardConfigSummary.jsx";
|
||||
@ -61,9 +61,10 @@ export function RegistrationRewardPage() {
|
||||
if (column.key === "user") {
|
||||
return {
|
||||
...column,
|
||||
filter: createUserIdentityColumnFilter({
|
||||
value: page.userFilter,
|
||||
onChange: page.changeUserFilter,
|
||||
filter: createTextColumnFilter({
|
||||
placeholder: "搜索用户 ID、短号、名称",
|
||||
value: page.query,
|
||||
onChange: page.changeQuery,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@ -4,7 +4,6 @@ import { useRegionOptions } from "@/shared/hooks/useRegionOptions.js";
|
||||
import { usePaginatedQuery } from "@/shared/hooks/usePaginatedQuery.js";
|
||||
import { useConfirm } from "@/shared/ui/ConfirmProvider.jsx";
|
||||
import { useToast } from "@/shared/ui/ToastProvider.jsx";
|
||||
import { emptyUserIdentityFilter, userIdentityFilterParams } from "@/shared/ui/userIdentityFilter.js";
|
||||
import {
|
||||
createEmojiPack,
|
||||
createGift,
|
||||
@ -504,7 +503,7 @@ export function useResourceShopPage() {
|
||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||
const [purchaseDrawerOpen, setPurchaseDrawerOpen] = useState(false);
|
||||
const [purchasePage, setPurchasePage] = useState(1);
|
||||
const [purchaseUserFilter, setPurchaseUserFilter] = useState(emptyUserIdentityFilter);
|
||||
const [purchaseUserKeyword, setPurchaseUserKeyword] = useState("");
|
||||
const [purchaseResourceType, setPurchaseResourceType] = useState("");
|
||||
const [purchaseKeyword, setPurchaseKeyword] = useState("");
|
||||
const [drawerQuery, setDrawerQuery] = useState("");
|
||||
@ -534,9 +533,9 @@ export function useResourceShopPage() {
|
||||
() => ({
|
||||
keyword: purchaseKeyword,
|
||||
resource_type: purchaseResourceType,
|
||||
...userIdentityFilterParams(purchaseUserFilter),
|
||||
user_keyword: purchaseUserKeyword,
|
||||
}),
|
||||
[purchaseKeyword, purchaseResourceType, purchaseUserFilter],
|
||||
[purchaseKeyword, purchaseResourceType, purchaseUserKeyword],
|
||||
);
|
||||
const fetchPurchaseOrders = useCallback(
|
||||
(requestQuery) => {
|
||||
@ -766,7 +765,7 @@ export function useResourceShopPage() {
|
||||
purchaseLoading: purchaseResult.loading,
|
||||
purchasePage,
|
||||
purchaseResourceType,
|
||||
purchaseUserFilter,
|
||||
purchaseUserKeyword,
|
||||
reloadPurchaseOrders: purchaseResult.reload,
|
||||
setPurchasePage,
|
||||
resourceOptionsLoading,
|
||||
@ -780,7 +779,7 @@ export function useResourceShopPage() {
|
||||
toggleShopItem,
|
||||
changePurchaseKeyword: resetSetter(setPurchaseKeyword, setPurchasePage),
|
||||
changePurchaseResourceType: resetSetter(setPurchaseResourceType, setPurchasePage),
|
||||
changePurchaseUserFilter: resetSetter(setPurchaseUserFilter, setPurchasePage),
|
||||
changePurchaseUserKeyword: resetSetter(setPurchaseUserKeyword, setPurchasePage),
|
||||
updateDraftItem,
|
||||
};
|
||||
}
|
||||
@ -1547,7 +1546,7 @@ export function useResourceGrantListPage() {
|
||||
const abilities = useResourceAbilities();
|
||||
const confirm = useConfirm();
|
||||
const { showToast } = useToast();
|
||||
const [targetFilter, setTargetFilter] = useState(emptyUserIdentityFilter);
|
||||
const [query, setQuery] = useState("");
|
||||
const [status, setStatus] = useState("");
|
||||
const [page, setPage] = useState(1);
|
||||
const [activeAction, setActiveAction] = useState("");
|
||||
@ -1561,13 +1560,9 @@ export function useResourceGrantListPage() {
|
||||
const filters = useMemo(
|
||||
() => ({
|
||||
status,
|
||||
...userIdentityFilterParams(targetFilter, {
|
||||
displayUserId: "target_display_user_id",
|
||||
userId: "target_user_id",
|
||||
username: "target_username",
|
||||
}),
|
||||
target_user_id: query.trim(),
|
||||
}),
|
||||
[status, targetFilter],
|
||||
[query, status],
|
||||
);
|
||||
const result = usePaginatedQuery({
|
||||
errorMessage: "加载资源赠送记录失败",
|
||||
@ -1686,37 +1681,30 @@ export function useResourceGrantListPage() {
|
||||
};
|
||||
|
||||
const resetFilters = () => {
|
||||
setTargetFilter(emptyUserIdentityFilter);
|
||||
setQuery("");
|
||||
setStatus("");
|
||||
setPage(1);
|
||||
};
|
||||
|
||||
return {
|
||||
...sharedPageState({ page, query, result, setPage, setQuery }),
|
||||
abilities,
|
||||
activeAction,
|
||||
changeTargetFilter: resetSetter(setTargetFilter, setPage),
|
||||
changeStatus: resetSetter(setStatus, setPage),
|
||||
closeAction,
|
||||
data: result.data || emptyData,
|
||||
error: result.error,
|
||||
form,
|
||||
giftOptions,
|
||||
giftTypeOptions,
|
||||
groupOptions,
|
||||
loading: result.loading,
|
||||
loadingAction,
|
||||
openCreateGrant,
|
||||
optionsLoading,
|
||||
page,
|
||||
reload: result.reload,
|
||||
resourceOptions,
|
||||
resetFilters,
|
||||
revokeGrant,
|
||||
setForm,
|
||||
setPage,
|
||||
status,
|
||||
submitGrant,
|
||||
targetFilter,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -14,7 +14,7 @@ import {
|
||||
AdminListToolbar,
|
||||
AdminRowActions,
|
||||
} from "@/shared/ui/AdminListLayout.jsx";
|
||||
import { createOptionsColumnFilter, createUserIdentityColumnFilter } from "@/shared/ui/tableFilters.js";
|
||||
import { createOptionsColumnFilter, createTextColumnFilter } from "@/shared/ui/tableFilters.js";
|
||||
import { formatMillis } from "@/shared/utils/time.js";
|
||||
import { GiftResourceSelectField } from "@/features/resources/components/GiftResourceSelectDrawer.jsx";
|
||||
import {
|
||||
@ -96,9 +96,10 @@ export function ResourceGrantListPage() {
|
||||
if (column.key === "target") {
|
||||
return {
|
||||
...column,
|
||||
filter: createUserIdentityColumnFilter({
|
||||
value: page.targetFilter,
|
||||
onChange: page.changeTargetFilter,
|
||||
filter: createTextColumnFilter({
|
||||
placeholder: "搜索用户 ID",
|
||||
value: page.query,
|
||||
onChange: page.changeQuery,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@ -16,7 +16,7 @@ import {
|
||||
AdminListToolbar,
|
||||
} from "@/shared/ui/AdminListLayout.jsx";
|
||||
import { SideDrawer } from "@/shared/ui/SideDrawer.jsx";
|
||||
import { createOptionsColumnFilter, createTextColumnFilter, createUserIdentityColumnFilter } from "@/shared/ui/tableFilters.js";
|
||||
import { createOptionsColumnFilter, createTextColumnFilter } from "@/shared/ui/tableFilters.js";
|
||||
import { formatMillis } from "@/shared/utils/time.js";
|
||||
import {
|
||||
resourcePriceTypeLabels,
|
||||
@ -278,9 +278,10 @@ function ResourceShopPurchaseDrawer({ page }) {
|
||||
if (column.key === "user") {
|
||||
return {
|
||||
...column,
|
||||
filter: createUserIdentityColumnFilter({
|
||||
value: page.purchaseUserFilter,
|
||||
onChange: page.changePurchaseUserFilter,
|
||||
filter: createTextColumnFilter({
|
||||
placeholder: "搜索用户ID、短号、靓号、昵称",
|
||||
value: page.purchaseUserKeyword,
|
||||
onChange: page.changePurchaseUserKeyword,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@ -9,7 +9,6 @@ import { useRoomTurnoverRewardAbilities } from "@/features/room-turnover-reward/
|
||||
import { useConfirm } from "@/shared/ui/ConfirmProvider.jsx";
|
||||
import { usePaginatedQuery } from "@/shared/hooks/usePaginatedQuery.js";
|
||||
import { useToast } from "@/shared/ui/ToastProvider.jsx";
|
||||
import { emptyUserIdentityFilter, userIdentityFilterParams } from "@/shared/ui/userIdentityFilter.js";
|
||||
|
||||
const pageSize = 50;
|
||||
const emptySettlements = { items: [], page: 1, pageSize, total: 0 };
|
||||
@ -30,20 +29,12 @@ export function useRoomTurnoverRewardPage() {
|
||||
const [configSaving, setConfigSaving] = useState(false);
|
||||
const [retryingSettlementId, setRetryingSettlementId] = useState("");
|
||||
const [query, setQuery] = useState("");
|
||||
const [ownerFilter, setOwnerFilter] = useState(emptyUserIdentityFilter);
|
||||
const [ownerUserKeyword, setOwnerUserKeyword] = useState("");
|
||||
const [status, setStatus] = useState("");
|
||||
const [page, setPage] = useState(1);
|
||||
const filters = useMemo(
|
||||
() => ({
|
||||
keyword: query,
|
||||
status,
|
||||
...userIdentityFilterParams(ownerFilter, {
|
||||
displayUserId: "owner_display_user_id",
|
||||
userId: "owner_user_id",
|
||||
username: "owner_username",
|
||||
}),
|
||||
}),
|
||||
[ownerFilter, query, status],
|
||||
() => ({ keyword: query, owner_user_keyword: ownerUserKeyword, status }),
|
||||
[ownerUserKeyword, query, status],
|
||||
);
|
||||
const {
|
||||
data: settlements = emptySettlements,
|
||||
@ -96,8 +87,8 @@ export function useRoomTurnoverRewardPage() {
|
||||
setPage(1);
|
||||
};
|
||||
|
||||
const changeOwnerFilter = (value) => {
|
||||
setOwnerFilter(value);
|
||||
const changeOwnerUserKeyword = (value) => {
|
||||
setOwnerUserKeyword(value);
|
||||
setPage(1);
|
||||
};
|
||||
|
||||
@ -147,7 +138,7 @@ export function useRoomTurnoverRewardPage() {
|
||||
|
||||
return {
|
||||
abilities,
|
||||
changeOwnerFilter,
|
||||
changeOwnerUserKeyword,
|
||||
changeQuery,
|
||||
changeStatus,
|
||||
closeConfigDrawer,
|
||||
@ -157,7 +148,7 @@ export function useRoomTurnoverRewardPage() {
|
||||
configSaving,
|
||||
form,
|
||||
openConfigDrawer,
|
||||
ownerFilter,
|
||||
ownerUserKeyword,
|
||||
page,
|
||||
query,
|
||||
reloadConfig,
|
||||
|
||||
@ -7,7 +7,7 @@ import { AdminActionIconButton, AdminListBody, AdminListPage } from "@/shared/ui
|
||||
import { AdminUserIdentity } from "@/shared/ui/AdminUserIdentity.jsx";
|
||||
import { DataState } from "@/shared/ui/DataState.jsx";
|
||||
import { DataTable } from "@/shared/ui/DataTable.jsx";
|
||||
import { createOptionsColumnFilter, createTextColumnFilter, createUserIdentityColumnFilter } from "@/shared/ui/tableFilters.js";
|
||||
import { createOptionsColumnFilter, createTextColumnFilter } from "@/shared/ui/tableFilters.js";
|
||||
import { TimeText } from "@/shared/ui/TimeText.jsx";
|
||||
|
||||
const settlementStatusOptions = [
|
||||
@ -131,9 +131,10 @@ export function RoomTurnoverRewardPage() {
|
||||
if (column.key === "owner") {
|
||||
return {
|
||||
...column,
|
||||
filter: createUserIdentityColumnFilter({
|
||||
value: page.ownerFilter,
|
||||
onChange: page.changeOwnerFilter,
|
||||
filter: createTextColumnFilter({
|
||||
placeholder: "搜索房主 ID、短号、名称",
|
||||
value: page.ownerUserKeyword,
|
||||
onChange: page.changeOwnerUserKeyword,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@ -4,7 +4,6 @@ import { useRegionOptions } from "@/shared/hooks/useRegionOptions.js";
|
||||
import { usePaginatedQuery } from "@/shared/hooks/usePaginatedQuery.js";
|
||||
import { useConfirm } from "@/shared/ui/ConfirmProvider.jsx";
|
||||
import { useToast } from "@/shared/ui/ToastProvider.jsx";
|
||||
import { emptyUserIdentityFilter, userIdentityFilterParams } from "@/shared/ui/userIdentityFilter.js";
|
||||
import { deleteRoom, listRooms, updateRoom } from "@/features/rooms/api";
|
||||
import { useRoomAbilities } from "@/features/rooms/permissions.js";
|
||||
import { roomStatusSchema, roomUpdateSchema } from "@/features/rooms/schema";
|
||||
@ -25,7 +24,7 @@ export function useRoomsPage() {
|
||||
const { showToast } = useToast();
|
||||
const { loadingRegions, regionOptions } = useRegionOptions();
|
||||
const [query, setQuery] = useState("");
|
||||
const [ownerFilter, setOwnerFilter] = useState(emptyUserIdentityFilter);
|
||||
const [ownerUserKeyword, setOwnerUserKeyword] = useState("");
|
||||
const [status, setStatus] = useState("");
|
||||
const [regionId, setRegionId] = useState("");
|
||||
const [contributionSortDirection, setContributionSortDirection] = useState("");
|
||||
@ -38,17 +37,13 @@ export function useRoomsPage() {
|
||||
const filters = useMemo(
|
||||
() => ({
|
||||
keyword: query,
|
||||
owner_user_keyword: ownerUserKeyword,
|
||||
region_id: regionId,
|
||||
sort_by: contributionSortDirection ? "room_contribution" : "",
|
||||
sort_direction: contributionSortDirection,
|
||||
status,
|
||||
...userIdentityFilterParams(ownerFilter, {
|
||||
displayUserId: "owner_display_user_id",
|
||||
userId: "owner_user_id",
|
||||
username: "owner_username",
|
||||
}),
|
||||
}),
|
||||
[contributionSortDirection, ownerFilter, query, regionId, status],
|
||||
[contributionSortDirection, ownerUserKeyword, query, regionId, status],
|
||||
);
|
||||
const {
|
||||
data = emptyData,
|
||||
@ -69,8 +64,8 @@ export function useRoomsPage() {
|
||||
setPage(1);
|
||||
};
|
||||
|
||||
const changeOwnerFilter = (value) => {
|
||||
setOwnerFilter(value);
|
||||
const changeOwnerUserKeyword = (value) => {
|
||||
setOwnerUserKeyword(value);
|
||||
setPage(1);
|
||||
};
|
||||
|
||||
@ -91,7 +86,7 @@ export function useRoomsPage() {
|
||||
|
||||
const resetFilters = () => {
|
||||
setQuery("");
|
||||
setOwnerFilter(emptyUserIdentityFilter);
|
||||
setOwnerUserKeyword("");
|
||||
setStatus("");
|
||||
setRegionId("");
|
||||
setPage(1);
|
||||
@ -196,8 +191,8 @@ export function useRoomsPage() {
|
||||
abilities,
|
||||
activeAction,
|
||||
activeRoom,
|
||||
changeOwnerFilter,
|
||||
changeQuery,
|
||||
changeOwnerUserKeyword,
|
||||
changeRegionId,
|
||||
changeStatus,
|
||||
closeAction,
|
||||
@ -211,7 +206,7 @@ export function useRoomsPage() {
|
||||
openDetail,
|
||||
openEdit,
|
||||
openStatus,
|
||||
ownerFilter,
|
||||
ownerUserKeyword,
|
||||
page,
|
||||
query,
|
||||
regionId,
|
||||
|
||||
@ -11,7 +11,7 @@ import { DataTable } from "@/shared/ui/DataTable.jsx";
|
||||
import { IconButton } from "@/shared/ui/IconButton.jsx";
|
||||
import { RegionSelect } from "@/shared/ui/RegionSelect.jsx";
|
||||
import { createRegionColumnFilter } from "@/shared/ui/RegionFilterControl.jsx";
|
||||
import { createOptionsColumnFilter, createTextColumnFilter, createUserIdentityColumnFilter } from "@/shared/ui/tableFilters.js";
|
||||
import { createOptionsColumnFilter, createTextColumnFilter } from "@/shared/ui/tableFilters.js";
|
||||
import { UploadField } from "@/shared/ui/UploadField.jsx";
|
||||
import { formatMillis } from "@/shared/utils/time.js";
|
||||
import { RoomDetailDrawer } from "@/features/rooms/components/RoomDetailDrawer.jsx";
|
||||
@ -93,9 +93,10 @@ export function RoomListPage() {
|
||||
: column.key === "owner"
|
||||
? {
|
||||
...column,
|
||||
filter: createUserIdentityColumnFilter({
|
||||
value: page.ownerFilter,
|
||||
onChange: page.changeOwnerFilter,
|
||||
filter: createTextColumnFilter({
|
||||
placeholder: "搜索房主 ID、短号、靓号、昵称",
|
||||
value: page.ownerUserKeyword,
|
||||
onChange: page.changeOwnerUserKeyword,
|
||||
}),
|
||||
}
|
||||
: column.key === "status"
|
||||
|
||||
@ -10,7 +10,6 @@ import {
|
||||
} from "@/features/seven-day-checkin/api";
|
||||
import { useSevenDayCheckInAbilities } from "@/features/seven-day-checkin/permissions.js";
|
||||
import { sevenDayCheckInConfigFormSchema } from "@/features/seven-day-checkin/schema";
|
||||
import { emptyUserIdentityFilter, userIdentityFilterParams } from "@/shared/ui/userIdentityFilter.js";
|
||||
|
||||
const pageSize = 50;
|
||||
const emptyClaims = { items: [], page: 1, pageSize, total: 0 };
|
||||
@ -32,14 +31,14 @@ export function useSevenDayCheckInPage() {
|
||||
const [configLoading, setConfigLoading] = useState(false);
|
||||
const [configSaving, setConfigSaving] = useState(false);
|
||||
const [resourceGroups, setResourceGroups] = useState([]);
|
||||
const [userFilter, setUserFilter] = useState(emptyUserIdentityFilter);
|
||||
const [query, setQuery] = useState("");
|
||||
const [status, setStatus] = useState("");
|
||||
const [checkinDay, setCheckinDay] = useState("");
|
||||
const [dayIndex, setDayIndex] = useState("");
|
||||
const [page, setPage] = useState(1);
|
||||
const filters = useMemo(
|
||||
() => ({ checkin_day: checkinDay, day_index: dayIndex, status, ...userIdentityFilterParams(userFilter) }),
|
||||
[checkinDay, dayIndex, status, userFilter],
|
||||
() => ({ checkin_day: checkinDay, day_index: dayIndex, keyword: query, status }),
|
||||
[checkinDay, dayIndex, query, status],
|
||||
);
|
||||
const {
|
||||
data: claims = emptyClaims,
|
||||
@ -86,8 +85,8 @@ export function useSevenDayCheckInPage() {
|
||||
setForm(config ? formFromConfig(config) : emptyForm);
|
||||
}, [config]);
|
||||
|
||||
const changeUserFilter = (value) => {
|
||||
setUserFilter(value);
|
||||
const changeQuery = (value) => {
|
||||
setQuery(value);
|
||||
setPage(1);
|
||||
};
|
||||
|
||||
@ -131,8 +130,8 @@ export function useSevenDayCheckInPage() {
|
||||
abilities,
|
||||
changeCheckinDay,
|
||||
changeDayIndex,
|
||||
changeQuery,
|
||||
changeStatus,
|
||||
changeUserFilter,
|
||||
checkinDay,
|
||||
claims,
|
||||
claimsError,
|
||||
@ -146,6 +145,7 @@ export function useSevenDayCheckInPage() {
|
||||
form,
|
||||
openConfigDrawer,
|
||||
page,
|
||||
query,
|
||||
reloadClaims,
|
||||
reloadConfig,
|
||||
resourceGroups,
|
||||
@ -153,7 +153,6 @@ export function useSevenDayCheckInPage() {
|
||||
setPage,
|
||||
status,
|
||||
submitConfig,
|
||||
userFilter,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@ import { DataState } from "@/shared/ui/DataState.jsx";
|
||||
import { DataTable } from "@/shared/ui/DataTable.jsx";
|
||||
import { AdminListBody, AdminListPage } from "@/shared/ui/AdminListLayout.jsx";
|
||||
import { AdminUserIdentity } from "@/shared/ui/AdminUserIdentity.jsx";
|
||||
import { createOptionsColumnFilter, createTextColumnFilter, createUserIdentityColumnFilter } from "@/shared/ui/tableFilters.js";
|
||||
import { createOptionsColumnFilter, createTextColumnFilter } from "@/shared/ui/tableFilters.js";
|
||||
import { formatMillis } from "@/shared/utils/time.js";
|
||||
import { SevenDayCheckInConfigDrawer } from "@/features/seven-day-checkin/components/SevenDayCheckInConfigDrawer.jsx";
|
||||
import { SevenDayCheckInConfigSummary } from "@/features/seven-day-checkin/components/SevenDayCheckInConfigSummary.jsx";
|
||||
@ -98,9 +98,10 @@ export function SevenDayCheckInPage() {
|
||||
if (column.key === "user") {
|
||||
return {
|
||||
...column,
|
||||
filter: createUserIdentityColumnFilter({
|
||||
value: page.userFilter,
|
||||
onChange: page.changeUserFilter,
|
||||
filter: createTextColumnFilter({
|
||||
placeholder: "搜索用户 ID、短号、名称",
|
||||
value: page.query,
|
||||
onChange: page.changeQuery,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@ -235,7 +235,6 @@ export const API_OPERATIONS = {
|
||||
revokeResourceGrant: "revokeResourceGrant",
|
||||
search: "search",
|
||||
setAgencyJoinEnabled: "setAgencyJoinEnabled",
|
||||
setAgencyStatus: "setAgencyStatus",
|
||||
setBDLeaderStatus: "setBDLeaderStatus",
|
||||
setBDStatus: "setBDStatus",
|
||||
setCoinSellerStatus: "setCoinSellerStatus",
|
||||
@ -1851,13 +1850,6 @@ export const API_ENDPOINTS: Record<ApiOperationId, ApiEndpoint> = {
|
||||
permission: "agency:status",
|
||||
permissions: ["agency:status"]
|
||||
},
|
||||
setAgencyStatus: {
|
||||
method: "PATCH",
|
||||
operationId: API_OPERATIONS.setAgencyStatus,
|
||||
path: "/v1/admin/agencies/{agency_id}/status",
|
||||
permission: "agency:status",
|
||||
permissions: ["agency:status"]
|
||||
},
|
||||
setBDLeaderStatus: {
|
||||
method: "PATCH",
|
||||
operationId: API_OPERATIONS.setBDLeaderStatus,
|
||||
|
||||
32
src/shared/api/generated/schema.d.ts
vendored
32
src/shared/api/generated/schema.d.ts
vendored
@ -708,22 +708,6 @@ export interface paths {
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/admin/agencies/{agency_id}/status": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch: operations["setAgencyStatus"];
|
||||
trace?: never;
|
||||
};
|
||||
"/admin/agencies/{agency_id}/delete": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
@ -3495,7 +3479,6 @@ export interface components {
|
||||
};
|
||||
Agency: {
|
||||
agencyId: number;
|
||||
activeHostCount?: number;
|
||||
createdAtMs?: number;
|
||||
createdByUserId?: number;
|
||||
joinEnabled?: boolean;
|
||||
@ -3527,7 +3510,6 @@ export interface components {
|
||||
targetUserId: string;
|
||||
};
|
||||
BDProfile: {
|
||||
agencyCount?: number;
|
||||
createdAtMs?: number;
|
||||
createdByUserId?: number;
|
||||
parentLeaderUserId?: string;
|
||||
@ -4724,20 +4706,6 @@ export interface operations {
|
||||
200: components["responses"]["EmptyResponse"];
|
||||
};
|
||||
};
|
||||
setAgencyStatus: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
agency_id: number;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
200: components["responses"]["EmptyResponse"];
|
||||
};
|
||||
};
|
||||
deleteAgency: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
|
||||
@ -568,7 +568,6 @@ export interface AppVersionPayload {
|
||||
}
|
||||
|
||||
export interface BDProfileDto {
|
||||
agencyCount?: number;
|
||||
avatar?: string;
|
||||
createdAtMs?: number;
|
||||
createdByAvatar?: string;
|
||||
@ -595,7 +594,6 @@ export interface BDProfileDto {
|
||||
}
|
||||
|
||||
export interface AgencyDto {
|
||||
activeHostCount?: number;
|
||||
agencyId: number;
|
||||
createdAtMs?: number;
|
||||
createdByUserId?: number;
|
||||
@ -1490,10 +1488,6 @@ export interface AgencyJoinEnabledPayload extends HostCommandPayload {
|
||||
joinEnabled: boolean;
|
||||
}
|
||||
|
||||
export interface AgencyStatusPayload extends HostCommandPayload {
|
||||
status: string;
|
||||
}
|
||||
|
||||
export interface CountryDto {
|
||||
countryCode: string;
|
||||
countryDisplayName?: string;
|
||||
|
||||
@ -86,14 +86,10 @@ export function DataTable({
|
||||
const activeFilterColumn = preparedColumns.find((column) => column.key === filterMenu.columnKey && column.filter) || null;
|
||||
const activeFilter = activeFilterColumn?.filter || null;
|
||||
const activeFilterIsText = activeFilter?.type === "text";
|
||||
const activeFilterIsTextFields = activeFilter?.type === "textFields";
|
||||
const filterColumns = preparedColumns.filter((column) => column.filter);
|
||||
const hasColumnFilters = filterColumns.length > 0;
|
||||
const hasActiveColumnFilters = filterColumns.some((column) => isFilterActive(column.filter));
|
||||
const filterOptions = useMemo(
|
||||
() => filterVisibleOptions(activeFilter, typeof filterQuery === "string" ? filterQuery : ""),
|
||||
[activeFilter, filterQuery]
|
||||
);
|
||||
const filterOptions = useMemo(() => filterVisibleOptions(activeFilter, filterQuery), [activeFilter, filterQuery]);
|
||||
|
||||
useEffect(() => {
|
||||
const scrollNode = scrollRef.current;
|
||||
@ -221,7 +217,7 @@ export function DataTable({
|
||||
}
|
||||
event.preventDefault();
|
||||
setFilterMenu({ anchorEl: event.currentTarget, columnKey: column.key });
|
||||
setFilterQuery(initialFilterDraft(column.filter));
|
||||
setFilterQuery(column.filter.type === "text" ? String(column.filter.value ?? "") : "");
|
||||
};
|
||||
|
||||
const closeColumnFilter = () => {
|
||||
@ -240,12 +236,6 @@ export function DataTable({
|
||||
closeColumnFilter();
|
||||
};
|
||||
|
||||
const submitTextFieldsFilter = (event) => {
|
||||
event.preventDefault();
|
||||
activeFilter?.onChange?.(normalizeTextFieldsDraft(activeFilter, filterQuery));
|
||||
closeColumnFilter();
|
||||
};
|
||||
|
||||
const clearTextFilter = () => {
|
||||
if (activeFilter?.onReset) {
|
||||
activeFilter.onReset();
|
||||
@ -255,15 +245,6 @@ export function DataTable({
|
||||
closeColumnFilter();
|
||||
};
|
||||
|
||||
const clearTextFieldsFilter = () => {
|
||||
if (activeFilter?.onReset) {
|
||||
activeFilter.onReset();
|
||||
} else {
|
||||
activeFilter?.onChange?.(emptyTextFieldsDraft(activeFilter));
|
||||
}
|
||||
closeColumnFilter();
|
||||
};
|
||||
|
||||
const resetColumnFilters = () => {
|
||||
closeColumnFilter();
|
||||
filterColumns.forEach((column) => {
|
||||
@ -410,35 +391,6 @@ export function DataTable({
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
) : activeFilterIsTextFields ? (
|
||||
<form className="admin-table-filter admin-table-filter--fields" onSubmit={submitTextFieldsFilter}>
|
||||
{normalizeTextFields(activeFilter).map((field, index) => (
|
||||
<TextField
|
||||
key={field.key}
|
||||
autoFocus={index === 0}
|
||||
className="admin-table-filter__search"
|
||||
disabled={activeFilter.loading}
|
||||
label={field.label}
|
||||
placeholder={field.placeholder || field.label}
|
||||
size="small"
|
||||
value={filterQuery?.[field.key] ?? ""}
|
||||
onChange={(event) =>
|
||||
setFilterQuery((current) => ({
|
||||
...(current && typeof current === "object" ? current : {}),
|
||||
[field.key]: event.target.value
|
||||
}))
|
||||
}
|
||||
/>
|
||||
))}
|
||||
<div className="admin-table-filter__actions">
|
||||
<Button disabled={activeFilter.loading || !isFilterActive(activeFilter)} onClick={clearTextFieldsFilter}>
|
||||
清空
|
||||
</Button>
|
||||
<Button disabled={activeFilter.loading} type="submit" variant="primary">
|
||||
搜索
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
) : (
|
||||
<div className="admin-table-filter">
|
||||
<TextField
|
||||
@ -637,15 +589,6 @@ function clamp(value, min, max) {
|
||||
}
|
||||
|
||||
function isFilterActive(filter) {
|
||||
if (filter?.activeLabel) {
|
||||
return true;
|
||||
}
|
||||
if (filter?.type === "textFields") {
|
||||
return normalizeTextFields(filter).some((field) => {
|
||||
const value = field.activeValue !== undefined ? field.activeValue : field.value;
|
||||
return value !== undefined && value !== null && String(value).trim() !== "";
|
||||
});
|
||||
}
|
||||
const value = filter?.activeValue !== undefined ? filter.activeValue : filter?.value;
|
||||
return value !== undefined && value !== null && String(value) !== "";
|
||||
}
|
||||
@ -654,18 +597,6 @@ function filterDisplayValue(filter) {
|
||||
if (filter?.activeLabel) {
|
||||
return filter.activeLabel;
|
||||
}
|
||||
if (filter?.type === "textFields") {
|
||||
const activeFields = normalizeTextFields(filter)
|
||||
.map((field) => {
|
||||
const value = String((field.activeValue !== undefined ? field.activeValue : field.value) ?? "").trim();
|
||||
return value ? `${field.label || field.key}: ${value}` : "";
|
||||
})
|
||||
.filter(Boolean);
|
||||
if (activeFields.length <= 2) {
|
||||
return activeFields.join(" / ");
|
||||
}
|
||||
return `${activeFields.length} 项`;
|
||||
}
|
||||
const value = String((filter?.activeValue !== undefined ? filter.activeValue : filter?.value) ?? "");
|
||||
if (!value) {
|
||||
return "";
|
||||
@ -678,37 +609,6 @@ function filterDisplayValue(filter) {
|
||||
return matchedOption?.label || value;
|
||||
}
|
||||
|
||||
function initialFilterDraft(filter) {
|
||||
if (filter?.type === "text") {
|
||||
return String(filter.value ?? "");
|
||||
}
|
||||
if (filter?.type === "textFields") {
|
||||
return normalizeTextFieldsDraft(filter);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function normalizeTextFields(filter) {
|
||||
return (filter?.fields || []).map((field) => ({
|
||||
...field,
|
||||
key: String(field.key || "")
|
||||
})).filter((field) => field.key);
|
||||
}
|
||||
|
||||
function normalizeTextFieldsDraft(filter, draft) {
|
||||
const source = draft && typeof draft === "object" ? draft : undefined;
|
||||
return Object.fromEntries(
|
||||
normalizeTextFields(filter).map((field) => {
|
||||
const rawValue = source ? source[field.key] : field.value;
|
||||
return [field.key, String(rawValue ?? "").trim()];
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function emptyTextFieldsDraft(filter) {
|
||||
return Object.fromEntries(normalizeTextFields(filter).map((field) => [field.key, ""]));
|
||||
}
|
||||
|
||||
function filterVisibleOptions(filter, query) {
|
||||
if (!filter) {
|
||||
return [];
|
||||
|
||||
@ -11,48 +11,6 @@ export function createTextColumnFilter({ activeLabel, activeValue, loading = fal
|
||||
};
|
||||
}
|
||||
|
||||
export function createTextFieldsColumnFilter({ activeLabel, activeValue, fields = [], loading = false, onChange, onReset }) {
|
||||
return {
|
||||
activeLabel,
|
||||
activeValue,
|
||||
fields,
|
||||
loading,
|
||||
onChange,
|
||||
onReset,
|
||||
type: "textFields"
|
||||
};
|
||||
}
|
||||
|
||||
export function createUserIdentityColumnFilter({ activeLabel, activeValue, loading = false, onChange, onReset, value = {} }) {
|
||||
return createTextFieldsColumnFilter({
|
||||
activeLabel,
|
||||
activeValue,
|
||||
loading,
|
||||
onChange,
|
||||
onReset,
|
||||
fields: [
|
||||
{
|
||||
key: "displayUserId",
|
||||
label: "短 ID",
|
||||
placeholder: "输入短 ID",
|
||||
value: value.displayUserId
|
||||
},
|
||||
{
|
||||
key: "userId",
|
||||
label: "长 ID",
|
||||
placeholder: "输入长 ID",
|
||||
value: value.userId
|
||||
},
|
||||
{
|
||||
key: "username",
|
||||
label: "昵称",
|
||||
placeholder: "输入昵称",
|
||||
value: value.username
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
export function createOptionsColumnFilter({
|
||||
activeLabel,
|
||||
activeValue,
|
||||
|
||||
@ -1,30 +0,0 @@
|
||||
export const emptyUserIdentityFilter = Object.freeze({
|
||||
displayUserId: "",
|
||||
userId: "",
|
||||
username: ""
|
||||
});
|
||||
|
||||
export function normalizeUserIdentityFilter(value = {}) {
|
||||
return {
|
||||
displayUserId: String(value.displayUserId ?? "").trim(),
|
||||
userId: String(value.userId ?? "").trim(),
|
||||
username: String(value.username ?? "").trim()
|
||||
};
|
||||
}
|
||||
|
||||
export function isUserIdentityFilterEmpty(value = {}) {
|
||||
const normalized = normalizeUserIdentityFilter(value);
|
||||
return !normalized.displayUserId && !normalized.userId && !normalized.username;
|
||||
}
|
||||
|
||||
export function userIdentityFilterParams(value = {}, names = {}) {
|
||||
const normalized = normalizeUserIdentityFilter(value);
|
||||
const displayUserIdKey = names.displayUserId || "display_user_id";
|
||||
const userIdKey = names.userId || "user_id";
|
||||
const usernameKey = names.username || "username";
|
||||
return {
|
||||
[displayUserIdKey]: normalized.displayUserId,
|
||||
[userIdKey]: normalized.userId,
|
||||
[usernameKey]: normalized.username
|
||||
};
|
||||
}
|
||||
@ -1859,7 +1859,7 @@
|
||||
}
|
||||
|
||||
.admin-table-filter-popover .MuiPopover-paper {
|
||||
width: 280px;
|
||||
width: 240px;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-card);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user