From a95330857cf4dd6ce6a79ff3efc712067d987213 Mon Sep 17 00:00:00 2001 From: zhx Date: Mon, 29 Jun 2026 14:26:00 +0800 Subject: [PATCH] =?UTF-8?q?id=E6=90=9C=E7=B4=A2=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- databi/src/DatabiApp.jsx | 138 +++++++++++------- databi/src/DatabiApp.test.jsx | 42 ++++++ databi/src/components/DatabiHeader.jsx | 4 - databi/src/components/ReportSidebar.jsx | 44 ++++++ databi/src/data/createDashboardModel.js | 33 ++++- databi/src/data/createDashboardModel.test.js | 40 ++++- databi/src/styles/layout.css | 85 ++++++++++- databi/src/styles/report.css | 40 ++++- .../app-users/hooks/useAppUsersPage.js | 19 +-- .../app-users/pages/AppUserBanListPage.jsx | 65 ++++++--- .../app-users/pages/AppUserListPage.jsx | 9 +- .../app-users/pages/AppUserLoginLogsPage.jsx | 70 +++++---- .../hooks/useCumulativeRechargeRewardPage.js | 16 +- .../pages/CumulativeRechargeRewardPage.jsx | 9 +- .../hooks/useFirstRechargeRewardPage.js | 13 +- .../pages/FirstRechargeRewardPage.jsx | 9 +- .../pages/HostSalarySettlementPage.jsx | 64 ++++---- .../host-org/hooks/useHostAgenciesPage.js | 35 ++++- src/features/host-org/hooks/useHostBdsPage.js | 37 +++-- .../host-org/hooks/useHostCoinSellersPage.js | 17 ++- .../host-org/hooks/useHostHostsPage.js | 17 ++- .../host-org/hooks/useHostManagersPage.js | 17 ++- .../host-org/pages/HostAgenciesPage.jsx | 22 ++- .../host-org/pages/HostBdLeadersPage.jsx | 9 +- .../host-org/pages/HostBdLeadersPage.test.jsx | 16 +- src/features/host-org/pages/HostBdsPage.jsx | 16 +- .../host-org/pages/HostCoinSellersPage.jsx | 9 +- src/features/host-org/pages/HostHostsPage.jsx | 9 +- .../host-org/pages/HostManagersPage.jsx | 9 +- .../hooks/useInviteActivityRewardPage.js | 15 +- .../pages/InviteActivityRewardPage.jsx | 9 +- .../operations/components/CoinLedgerTable.jsx | 25 ++-- .../components/CoinSellerLedgerTable.jsx | 28 ++-- .../operations/pages/CoinAdjustmentPage.jsx | 20 +-- .../payment/pages/PaymentBillListPage.jsx | 35 +++-- .../hooks/useRegistrationRewardPage.js | 17 ++- .../pages/RegistrationRewardPage.jsx | 9 +- .../resources/hooks/useResourcePages.js | 32 ++-- .../resources/pages/ResourceGrantListPage.jsx | 9 +- .../resources/pages/ResourceShopPage.jsx | 9 +- .../hooks/useRoomTurnoverRewardPage.js | 23 ++- .../pages/RoomTurnoverRewardPage.jsx | 9 +- src/features/rooms/hooks/useRoomsPage.js | 21 ++- src/features/rooms/pages/RoomListPage.jsx | 9 +- .../hooks/useSevenDayCheckInPage.js | 15 +- .../pages/SevenDayCheckInPage.jsx | 9 +- src/shared/ui/DataTable.jsx | 104 ++++++++++++- src/shared/ui/tableFilters.js | 42 ++++++ src/shared/ui/userIdentityFilter.js | 30 ++++ src/styles/shared-ui.css | 2 +- 50 files changed, 984 insertions(+), 401 deletions(-) create mode 100644 databi/src/components/ReportSidebar.jsx create mode 100644 src/shared/ui/userIdentityFilter.js diff --git a/databi/src/DatabiApp.jsx b/databi/src/DatabiApp.jsx index dbfb1e5..d859e0b 100644 --- a/databi/src/DatabiApp.jsx +++ b/databi/src/DatabiApp.jsx @@ -8,6 +8,7 @@ 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"; @@ -28,6 +29,8 @@ 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); @@ -47,6 +50,7 @@ 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)); @@ -170,10 +174,18 @@ export function DatabiApp() { }, [appCode, countryId, gameId, range, regionId, timeZone]); useEffect(() => { + // 非数据报表没有筛选控件,提前停止筛选项加载,避免空报表页产生无意义网络请求。 + if (!isDataReportSection) { + return undefined; + } void loadFilterOptions(); - }, [loadFilterOptions]); + }, [isDataReportSection, loadFilterOptions]); useEffect(() => { + // 当前数据轮询只属于数据报表;切到财务或运营时清理定时器并保留已有数据,方便切回。 + if (!isDataReportSection) { + return undefined; + } if (activeScreen === "selfGames") { void loadSelfGameOverview(); const timer = window.setInterval(() => void loadSelfGameOverview({ preserveData: true }), 60_000); @@ -182,15 +194,18 @@ export function DatabiApp() { void loadOverview(); const timer = window.setInterval(() => void loadOverview({ preserveData: true }), 60_000); return () => window.clearInterval(timer); - }, [activeScreen, loadOverview, loadSelfGameOverview]); + }, [activeScreen, isDataReportSection, loadOverview, loadSelfGameOverview]); const handleRefresh = useCallback(() => { + if (!isDataReportSection) { + return; + } if (activeScreen === "selfGames") { void loadSelfGameOverview({ preserveData: true }); return; } void loadOverview({ preserveData: true }); - }, [activeScreen, loadOverview, loadSelfGameOverview]); + }, [activeScreen, isDataReportSection, loadOverview, loadSelfGameOverview]); const model = useMemo(() => createDashboardModel(overview, { appCode, countryId, range, timeZone }), [appCode, countryId, overview, range, timeZone]); const showSkeleton = loading && !overview; @@ -201,63 +216,74 @@ export function DatabiApp() { return (
-
- + +
+ {isDataReportSection ? ( +
+ - {activeScreen === "selfGames" ? ( - - ) : isReportView ? ( - + {activeScreen === "selfGames" ? ( + + ) : isReportView ? ( + + ) : ( + + )} +
) : ( - + )} -
- {activeScreen === "overview" && !isReportView ? ( - <> - - - ) : null} - {activeScreen === "overview" && poolModalOpen ? setPoolModalOpen(false)} /> : null} - {activeScreen === "overview" && trendModalItem ? setTrendModalItem(null)} /> : null} + {isDataReportSection && activeScreen === "overview" && !isReportView ? ( + <> + + + ) : null} + {isDataReportSection && activeScreen === "overview" && poolModalOpen ? ( + setPoolModalOpen(false)} /> + ) : null} + {isDataReportSection && activeScreen === "overview" && trendModalItem ? ( + setTrendModalItem(null)} /> + ) : null} +
); } diff --git a/databi/src/DatabiApp.test.jsx b/databi/src/DatabiApp.test.jsx index 65858bc..b149bc3 100644 --- a/databi/src/DatabiApp.test.jsx +++ b/databi/src/DatabiApp.test.jsx @@ -99,6 +99,48 @@ 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(); + + 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({ diff --git a/databi/src/components/DatabiHeader.jsx b/databi/src/components/DatabiHeader.jsx index a061c65..ce95d37 100644 --- a/databi/src/components/DatabiHeader.jsx +++ b/databi/src/components/DatabiHeader.jsx @@ -2,7 +2,6 @@ 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 }]; @@ -82,9 +81,6 @@ export function DatabiHeader({
-

数据中心

diff --git a/databi/src/components/ReportSidebar.jsx b/databi/src/components/ReportSidebar.jsx new file mode 100644 index 0000000..516fd8b --- /dev/null +++ b/databi/src/components/ReportSidebar.jsx @@ -0,0 +1,44 @@ +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 ( + + ); +} + +export function EmptyReportSection({ section }) { + const label = reportSections.find((item) => item.key === section)?.label || "报表"; + return
; +} diff --git a/databi/src/data/createDashboardModel.js b/databi/src/data/createDashboardModel.js index f6184b2..d85f5b2 100644 --- a/databi/src/data/createDashboardModel.js +++ b/databi/src/data/createDashboardModel.js @@ -337,6 +337,8 @@ 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", @@ -345,19 +347,27 @@ 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" ]; - return fields.reduce((summary, key) => { + const summary = 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) { @@ -384,12 +394,16 @@ 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: readOptionalNumber(item, "avg_mic_online_ms", "avgMicOnlineMs"), + 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), @@ -412,7 +426,8 @@ 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: readOptionalNumber(item, "mic_online_ms", "micOnlineMs"), + mic_online_ms: micOnlineMS, + mic_online_users: micOnlineUsers, 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, @@ -467,14 +482,21 @@ 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"), @@ -484,13 +506,18 @@ 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, diff --git a/databi/src/data/createDashboardModel.test.js b/databi/src/data/createDashboardModel.test.js index ade9cb2..adf0af8 100644 --- a/databi/src/data/createDashboardModel.test.js +++ b/databi/src/data/createDashboardModel.test.js @@ -138,11 +138,18 @@ 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, @@ -157,7 +164,19 @@ test("builds report rows with range total, daily totals, and daily country rows" } ], daily_series: [ - { active_users: 10, label: "2026-06-05", recharge_usd_minor: 100, stat_day: "2026-06-05" }, + { + 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: 20, label: "2026-06-06", recharge_usd_minor: 200, stat_day: "2026-06-06" } ], recharge_usd_minor: 300 @@ -165,14 +184,31 @@ 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({ country: "总计", date_label: "2026-06-05" })); + 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].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 })); diff --git a/databi/src/styles/layout.css b/databi/src/styles/layout.css index 07759de..6032cd5 100644 --- a/databi/src/styles/layout.css +++ b/databi/src/styles/layout.css @@ -1,8 +1,91 @@ .databi-shell { + display: grid; + grid-template-columns: 80px minmax(0, 1fr); width: 100vw; min-width: 1280px; min-height: 720px; - padding: 0 24px 24px; + 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); } .databi-screen { diff --git a/databi/src/styles/report.css b/databi/src/styles/report.css index 0ce2850..cc56491 100644 --- a/databi/src/styles/report.css +++ b/databi/src/styles/report.css @@ -44,11 +44,45 @@ .databi-shell--report { min-width: 1180px; min-height: 100vh; - padding: 0 24px 28px; + padding: 0 24px 28px 0; 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; @@ -245,6 +279,10 @@ 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)); diff --git a/src/features/app-users/hooks/useAppUsersPage.js b/src/features/app-users/hooks/useAppUsersPage.js index a58ed3f..9cbc8df 100644 --- a/src/features/app-users/hooks/useAppUsersPage.js +++ b/src/features/app-users/hooks/useAppUsersPage.js @@ -4,6 +4,7 @@ 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, @@ -28,7 +29,7 @@ export function useAppUsersPage() { const { countryOptions, loadingCountries } = useCountryOptions(); const { loadingRegions, regionOptions } = useRegionOptions(); const [country, setCountry] = useState(""); - const [query, setQuery] = useState(""); + const [userFilter, setUserFilter] = useState(emptyUserIdentityFilter); const [regionId, setRegionId] = useState(""); const [status, setStatus] = useState(""); const [timeRange, setTimeRange] = useState({ endMs: "", startMs: "" }); @@ -46,15 +47,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, query, regionId, sortBy, sortDirection, status, timeRange.endMs, timeRange.startMs], + [country, regionId, sortBy, sortDirection, status, timeRange.endMs, timeRange.startMs, userFilter], ); const { data: queryData = emptyData, @@ -79,7 +80,7 @@ export function useAppUsersPage() { useEffect(() => { setSelectedUserIds([]); setPatchedUsers({}); - }, [country, page, query, regionId, sortBy, sortDirection, status, timeRange.endMs, timeRange.startMs]); + }, [country, page, regionId, sortBy, sortDirection, status, timeRange.endMs, timeRange.startMs, userFilter]); const locationFilterOptions = useMemo(() => { const countryItems = countryOptions.map((option) => ({ @@ -98,8 +99,8 @@ export function useAppUsersPage() { const locationFilterValue = country ? `country:${country}` : regionId !== "" ? `region:${regionId}` : ""; - const changeQuery = (value) => { - setQuery(value); + const changeUserFilter = (value) => { + setUserFilter(value); setPage(1); }; @@ -136,7 +137,7 @@ export function useAppUsersPage() { const resetFilters = () => { setCountry(""); - setQuery(""); + setUserFilter(emptyUserIdentityFilter); setRegionId(""); setStatus(""); setTimeRange({ endMs: "", startMs: "" }); @@ -299,7 +300,7 @@ export function useAppUsersPage() { activeAction, activeUser, batchBan, - changeQuery, + changeUserFilter, changeStatus, closeAction, changeLocationFilter, @@ -320,7 +321,6 @@ export function useAppUsersPage() { openPassword, page, passwordForm, - query, regionId, regionOptions, reload, @@ -335,6 +335,7 @@ export function useAppUsersPage() { sortDirection, status, timeRange, + userFilter, changeSort, changeTimeRange, submitCountry, diff --git a/src/features/app-users/pages/AppUserBanListPage.jsx b/src/features/app-users/pages/AppUserBanListPage.jsx index 1830cc7..6d1b940 100644 --- a/src/features/app-users/pages/AppUserBanListPage.jsx +++ b/src/features/app-users/pages/AppUserBanListPage.jsx @@ -2,7 +2,12 @@ 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 { createTextColumnFilter } from "@/shared/ui/tableFilters.js"; +import { createTextFieldsColumnFilter, createUserIdentityColumnFilter } from "@/shared/ui/tableFilters.js"; +import { + emptyUserIdentityFilter, + normalizeUserIdentityFilter, + userIdentityFilterParams, +} from "@/shared/ui/userIdentityFilter.js"; import { usePaginatedQuery } from "@/shared/hooks/usePaginatedQuery.js"; import { formatMillis } from "@/shared/utils/time.js"; import { listAppUserBans } from "@/features/app-users/api"; @@ -11,17 +16,27 @@ 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 [targetKeyword, setTargetKeyword] = useState(""); - const [operatorKeyword, setOperatorKeyword] = useState(""); + const [targetFilter, setTargetFilter] = useState(emptyUserIdentityFilter); + const [operatorFilter, setOperatorFilter] = useState(emptyOperatorFilter); const [page, setPage] = useState(1); const filters = useMemo( () => ({ - operator_keyword: operatorKeyword, - target_keyword: targetKeyword, + 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", + }), }), - [operatorKeyword, targetKeyword], + [operatorFilter, targetFilter], ); const { data = emptyData, @@ -37,13 +52,17 @@ export function AppUserBanListPage() { queryKey: ["app-users", "bans", filters, page], }); - const changeTargetKeyword = useCallback((value) => { - setTargetKeyword(value); + const changeTargetFilter = useCallback((value) => { + setTargetFilter(normalizeUserIdentityFilter(value)); setPage(1); }, []); - const changeOperatorKeyword = useCallback((value) => { - setOperatorKeyword(value); + const changeOperatorFilter = useCallback((value) => { + const normalized = normalizeUserIdentityFilter(value); + setOperatorFilter({ + ...normalized, + adminAccount: String(value?.adminAccount ?? "").trim(), + }); setPage(1); }, []); @@ -53,10 +72,9 @@ export function AppUserBanListPage() { key: "target", label: "封禁用户", width: "minmax(280px, 1.4fr)", - filter: createTextColumnFilter({ - placeholder: "搜索昵称、短 ID、长 ID", - value: targetKeyword, - onChange: changeTargetKeyword, + filter: createUserIdentityColumnFilter({ + value: targetFilter, + onChange: changeTargetFilter, }), render: (record) => , }, @@ -64,10 +82,19 @@ export function AppUserBanListPage() { key: "operator", label: "封禁人", width: "minmax(280px, 1.4fr)", - filter: createTextColumnFilter({ - placeholder: "搜索账号、昵称、短 ID、长 ID", - value: operatorKeyword, - onChange: changeOperatorKeyword, + 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, }), render: (record) => , }, @@ -90,7 +117,7 @@ export function AppUserBanListPage() { render: (record) => {formatMillis(record.createdAtMs)}, }, ], - [changeOperatorKeyword, changeTargetKeyword, operatorKeyword, targetKeyword], + [changeOperatorFilter, changeTargetFilter, operatorFilter, targetFilter], ); return ( diff --git a/src/features/app-users/pages/AppUserListPage.jsx b/src/features/app-users/pages/AppUserListPage.jsx index 92e70d8..429d218 100644 --- a/src/features/app-users/pages/AppUserListPage.jsx +++ b/src/features/app-users/pages/AppUserListPage.jsx @@ -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, createTextColumnFilter } from "@/shared/ui/tableFilters.js"; +import { createOptionsColumnFilter, createUserIdentityColumnFilter } 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,10 +83,9 @@ export function AppUserListPage() { key: "identity", label: "用户", width: "minmax(240px, 1.5fr)", - filter: createTextColumnFilter({ - placeholder: "搜索名称、短 ID、用户 ID", - value: page.query, - onChange: page.changeQuery, + filter: createUserIdentityColumnFilter({ + value: page.userFilter, + onChange: page.changeUserFilter, }), render: (user) => , }, diff --git a/src/features/app-users/pages/AppUserLoginLogsPage.jsx b/src/features/app-users/pages/AppUserLoginLogsPage.jsx index 7726eef..9c28f0d 100644 --- a/src/features/app-users/pages/AppUserLoginLogsPage.jsx +++ b/src/features/app-users/pages/AppUserLoginLogsPage.jsx @@ -14,7 +14,13 @@ 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 } from "@/shared/ui/tableFilters.js"; +import { createOptionsColumnFilter, createTextColumnFilter, createUserIdentityColumnFilter } from "@/shared/ui/tableFilters.js"; +import { + emptyUserIdentityFilter, + isUserIdentityFilterEmpty, + normalizeUserIdentityFilter, + userIdentityFilterParams, +} from "@/shared/ui/userIdentityFilter.js"; import { formatMillis } from "@/shared/utils/time.js"; import { listAppUserLoginLogs } from "@/features/app-users/api"; import { addRegionBlockWhitelistIP } from "@/features/region-blocks/api"; @@ -35,27 +41,29 @@ export function AppUserLoginLogsPage() { const queryClient = useQueryClient(); const { showToast } = useToast(); const regionBlockAbilities = useRegionBlockAbilities(); - const [query, setQuery] = useState(""); + const [identityFilter, setIdentityFilter] = useState(emptyUserIdentityFilter); + const [ip, setIp] = 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 userFilter = searchParams.get("user_id") || ""; + const lockedUserId = searchParams.get("user_id") || ""; const regionFilterOptions = useMemo(() => [{ label: "GLOBAL · 0", value: "0" }, ...regionOptions], [regionOptions]); const requestQuery = useMemo( () => toPageQuery({ - keyword: query, + ip, page, pageSize, region_id: regionId, - user_id: userFilter, + ...userIdentityFilterParams(identityFilter), + user_id: lockedUserId || identityFilter.userId, ...(result === "blocked" ? { blocked: "blocked" } : { result }), }), - [page, query, regionId, result, userFilter], + [identityFilter, ip, lockedUserId, page, regionId, result], ); const loadLogs = useCallback(() => listAppUserLoginLogs(requestQuery), [requestQuery]); const { @@ -72,7 +80,7 @@ export function AppUserLoginLogsPage() { useEffect(() => { setMergedData(emptyData); - }, [query, regionId, result, userFilter]); + }, [identityFilter, ip, lockedUserId, regionId, result]); useEffect(() => { const nextData = data || emptyData; @@ -98,8 +106,13 @@ export function AppUserLoginLogsPage() { const loading = queryLoading && (mergedData.items || []).length === 0; - const changeQuery = useCallback((value) => { - setQuery(value); + const changeUserFilter = useCallback((value) => { + setIdentityFilter(normalizeUserIdentityFilter(value)); + setPage(1); + }, []); + + const changeIp = useCallback((value) => { + setIp(value); setPage(1); }, []); @@ -121,7 +134,8 @@ export function AppUserLoginLogsPage() { const nextSearchParams = new URLSearchParams(searchParams); nextSearchParams.set("user_id", log.userId); setSearchParams(nextSearchParams, { replace: true }); - setQuery(""); + setIdentityFilter(emptyUserIdentityFilter); + setIp(""); setResult(""); setRegionId(""); setPage(1); @@ -130,19 +144,19 @@ export function AppUserLoginLogsPage() { ); const resetIdentityFilter = useCallback(() => { - setQuery(""); + setIdentityFilter(emptyUserIdentityFilter); setSearchParams(new URLSearchParams(), { replace: true }); setPage(1); }, [setSearchParams]); const changeIdentityFilter = useCallback( (value) => { - if (userFilter) { + if (lockedUserId) { setSearchParams(new URLSearchParams(), { replace: true }); } - changeQuery(value); + changeUserFilter(value); }, - [changeQuery, setSearchParams, userFilter], + [changeUserFilter, lockedUserId, setSearchParams], ); const addLoginIPToWhitelist = useCallback( @@ -167,12 +181,12 @@ export function AppUserLoginLogsPage() { ); const selectedUserLabel = useMemo(() => { - if (!userFilter) { + if (!lockedUserId) { return ""; } - const matchedLog = (mergedData.items || []).find((log) => String(log.userId || "") === String(userFilter)); - return matchedLog?.displayUserId || matchedLog?.username || userFilter; - }, [mergedData.items, userFilter]); + const matchedLog = (mergedData.items || []).find((log) => String(log.userId || "") === String(lockedUserId)); + return matchedLog?.displayUserId || matchedLog?.username || lockedUserId; + }, [lockedUserId, mergedData.items]); const columns = useMemo( () => [ @@ -180,12 +194,11 @@ export function AppUserLoginLogsPage() { key: "identity", label: "用户", width: "minmax(240px, 1.4fr)", - filter: createTextColumnFilter({ - activeLabel: userFilter ? selectedUserLabel || userFilter : undefined, - activeValue: query || userFilter, + filter: createUserIdentityColumnFilter({ + activeLabel: lockedUserId ? selectedUserLabel || lockedUserId : undefined, + activeValue: isUserIdentityFilterEmpty(identityFilter) ? lockedUserId : "active", onReset: resetIdentityFilter, - placeholder: "搜索昵称、短 ID、用户 ID、IP", - value: query, + value: lockedUserId ? emptyUserIdentityFilter : identityFilter, onChange: changeIdentityFilter, }), render: (log) => , @@ -194,6 +207,11 @@ export function AppUserLoginLogsPage() { key: "ip", label: "IP", width: "minmax(160px, 1fr)", + filter: createTextColumnFilter({ + placeholder: "搜索 IP", + value: ip, + onChange: changeIp, + }), render: (log) => (
{log.loginIp || "-"} @@ -284,18 +302,20 @@ export function AppUserLoginLogsPage() { [ addLoginIPToWhitelist, addingWhitelistIP, + changeIp, changeIdentityFilter, changeRegionId, changeResult, + identityFilter, + ip, + lockedUserId, loadingRegions, - query, regionFilterOptions, regionId, resetIdentityFilter, result, regionBlockAbilities.canUpdate, selectedUserLabel, - userFilter, ], ); diff --git a/src/features/cumulative-recharge-reward/hooks/useCumulativeRechargeRewardPage.js b/src/features/cumulative-recharge-reward/hooks/useCumulativeRechargeRewardPage.js index cd13580..891fb43 100644 --- a/src/features/cumulative-recharge-reward/hooks/useCumulativeRechargeRewardPage.js +++ b/src/features/cumulative-recharge-reward/hooks/useCumulativeRechargeRewardPage.js @@ -8,6 +8,7 @@ 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 }; @@ -26,11 +27,14 @@ export function useCumulativeRechargeRewardPage() { const [configLoading, setConfigLoading] = useState(false); const [configSaving, setConfigSaving] = useState(false); const [resourceGroups, setResourceGroups] = useState([]); - const [query, setQuery] = useState(""); + const [userFilter, setUserFilter] = useState(emptyUserIdentityFilter); const [cycleKey, setCycleKey] = useState(""); const [status, setStatus] = useState(""); const [page, setPage] = useState(1); - const filters = useMemo(() => ({ keyword: query, cycle_key: cycleKey, status }), [cycleKey, query, status]); + const filters = useMemo( + () => ({ cycle_key: cycleKey, status, ...userIdentityFilterParams(userFilter) }), + [cycleKey, status, userFilter], + ); const { data: grants = emptyGrants, error: grantsError, @@ -76,8 +80,8 @@ export function useCumulativeRechargeRewardPage() { setForm(config ? formFromConfig(config) : emptyForm); }, [config]); - const changeQuery = (value) => { - setQuery(value); + const changeUserFilter = (value) => { + setUserFilter(value); setPage(1); }; @@ -114,8 +118,8 @@ export function useCumulativeRechargeRewardPage() { return { abilities, changeCycleKey, - changeQuery, changeStatus, + changeUserFilter, closeConfigDrawer, config, configDrawerOpen, @@ -128,7 +132,6 @@ export function useCumulativeRechargeRewardPage() { grantsLoading, openConfigDrawer, page, - query, reloadConfig, reloadGrants, resourceGroups, @@ -136,6 +139,7 @@ export function useCumulativeRechargeRewardPage() { setPage, status, submitConfig, + userFilter, }; } diff --git a/src/features/cumulative-recharge-reward/pages/CumulativeRechargeRewardPage.jsx b/src/features/cumulative-recharge-reward/pages/CumulativeRechargeRewardPage.jsx index 67820d6..9e15ccc 100644 --- a/src/features/cumulative-recharge-reward/pages/CumulativeRechargeRewardPage.jsx +++ b/src/features/cumulative-recharge-reward/pages/CumulativeRechargeRewardPage.jsx @@ -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 } from "@/shared/ui/tableFilters.js"; +import { createOptionsColumnFilter, createTextColumnFilter, createUserIdentityColumnFilter } from "@/shared/ui/tableFilters.js"; import { formatMillis } from "@/shared/utils/time.js"; const grantStatusOptions = [ @@ -101,10 +101,9 @@ export function CumulativeRechargeRewardPage() { if (column.key === "user") { return { ...column, - filter: createTextColumnFilter({ - placeholder: "搜索用户 ID、短号、名称", - value: page.query, - onChange: page.changeQuery, + filter: createUserIdentityColumnFilter({ + value: page.userFilter, + onChange: page.changeUserFilter, }), }; } diff --git a/src/features/first-recharge-reward/hooks/useFirstRechargeRewardPage.js b/src/features/first-recharge-reward/hooks/useFirstRechargeRewardPage.js index 2f151ab..931b84f 100644 --- a/src/features/first-recharge-reward/hooks/useFirstRechargeRewardPage.js +++ b/src/features/first-recharge-reward/hooks/useFirstRechargeRewardPage.js @@ -8,6 +8,7 @@ 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 }; @@ -26,10 +27,10 @@ export function useFirstRechargeRewardPage() { const [configLoading, setConfigLoading] = useState(false); const [configSaving, setConfigSaving] = useState(false); const [resourceGroups, setResourceGroups] = useState([]); - const [query, setQuery] = useState(""); + const [userFilter, setUserFilter] = useState(emptyUserIdentityFilter); const [status, setStatus] = useState(""); const [page, setPage] = useState(1); - const filters = useMemo(() => ({ keyword: query, status }), [query, status]); + const filters = useMemo(() => ({ status, ...userIdentityFilterParams(userFilter) }), [status, userFilter]); const { data: claims = emptyClaims, error: claimsError, @@ -75,8 +76,8 @@ export function useFirstRechargeRewardPage() { setForm(config ? formFromConfig(config) : emptyForm); }, [config]); - const changeQuery = (value) => { - setQuery(value); + const changeUserFilter = (value) => { + setUserFilter(value); setPage(1); }; @@ -107,7 +108,7 @@ export function useFirstRechargeRewardPage() { return { abilities, - changeQuery, + changeUserFilter, changeStatus, claims, claimsError, @@ -120,7 +121,6 @@ export function useFirstRechargeRewardPage() { form, openConfigDrawer, page, - query, reloadClaims, reloadConfig, resourceGroups, @@ -128,6 +128,7 @@ export function useFirstRechargeRewardPage() { setPage, status, submitConfig, + userFilter, }; } diff --git a/src/features/first-recharge-reward/pages/FirstRechargeRewardPage.jsx b/src/features/first-recharge-reward/pages/FirstRechargeRewardPage.jsx index 51c2e51..7f652eb 100644 --- a/src/features/first-recharge-reward/pages/FirstRechargeRewardPage.jsx +++ b/src/features/first-recharge-reward/pages/FirstRechargeRewardPage.jsx @@ -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 } from "@/shared/ui/tableFilters.js"; +import { createOptionsColumnFilter, createUserIdentityColumnFilter } from "@/shared/ui/tableFilters.js"; import { formatMillis } from "@/shared/utils/time.js"; const claimStatusOptions = [ @@ -84,10 +84,9 @@ export function FirstRechargeRewardPage() { if (column.key === "user") { return { ...column, - filter: createTextColumnFilter({ - placeholder: "搜索用户 ID、短号、名称", - value: page.query, - onChange: page.changeQuery, + filter: createUserIdentityColumnFilter({ + value: page.userFilter, + onChange: page.changeUserFilter, }), }; } diff --git a/src/features/host-agency-policy/pages/HostSalarySettlementPage.jsx b/src/features/host-agency-policy/pages/HostSalarySettlementPage.jsx index e7eadb6..6d24523 100644 --- a/src/features/host-agency-policy/pages/HostSalarySettlementPage.jsx +++ b/src/features/host-agency-policy/pages/HostSalarySettlementPage.jsx @@ -28,7 +28,12 @@ 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 } from "@/shared/ui/tableFilters.js"; +import { createOptionsColumnFilter, createTextColumnFilter, createUserIdentityColumnFilter } from "@/shared/ui/tableFilters.js"; +import { + emptyUserIdentityFilter, + isUserIdentityFilterEmpty, + userIdentityFilterParams, +} from "@/shared/ui/userIdentityFilter.js"; import { formatMillis } from "@/shared/utils/time.js"; import styles from "@/features/host-agency-policy/host-agency-policy.module.css"; @@ -310,8 +315,8 @@ function HostPendingPanel({ onRoleChange, role }) { function HostRecordsPanel() { const [page, setPage] = useState(1); const [cycleKey, setCycleKey] = useState(""); - const [userId, setUserId] = useState(""); - const [agencyOwnerUserId, setAgencyOwnerUserId] = useState(""); + const [userFilter, setUserFilter] = useState(emptyUserIdentityFilter); + const [agencyOwnerFilter, setAgencyOwnerFilter] = useState(emptyUserIdentityFilter); const [settlementType, setSettlementType] = useState(""); const [status, setStatus] = useState(""); const [timeRange, setTimeRange] = useState({ endMs: "", startMs: "" }); @@ -319,15 +324,19 @@ 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, - user_id: userId, + ...userIdentityFilterParams(userFilter), + ...userIdentityFilterParams(agencyOwnerFilter, { + displayUserId: "agency_owner_display_user_id", + userId: "agency_owner_user_id", + username: "agency_owner_username", + }), }), - [agencyOwnerUserId, cycleKey, settlementType, status, timeRange.endMs, timeRange.startMs, userId], + [agencyOwnerFilter, cycleKey, settlementType, status, timeRange.endMs, timeRange.startMs, userFilter], ); const query = usePaginatedQuery({ errorMessage: "获取工资结算记录失败", @@ -351,12 +360,12 @@ function HostRecordsPanel() { setPage(1); }; const resetFilters = () => { - setAgencyOwnerUserId(""); + setAgencyOwnerFilter(emptyUserIdentityFilter); setCycleKey(""); setSettlementType(""); setStatus(""); setTimeRange({ endMs: "", startMs: "" }); - setUserId(""); + setUserFilter(emptyUserIdentityFilter); setPage(1); }; const tableColumns = hostRecordColumns.map((column) => { @@ -364,20 +373,18 @@ function HostRecordsPanel() { if (column.key === "host") { return { ...column, - filter: createTextColumnFilter({ - placeholder: "主播用户 ID", - value: userId, - onChange: changeFilter(setUserId), + filter: createUserIdentityColumnFilter({ + value: userFilter, + onChange: changeFilter(setUserFilter), }), }; } if (column.key === "agency") { return { ...column, - filter: createTextColumnFilter({ - placeholder: "代理用户 ID", - value: agencyOwnerUserId, - onChange: changeFilter(setAgencyOwnerUserId), + filter: createUserIdentityColumnFilter({ + value: agencyOwnerFilter, + onChange: changeFilter(setAgencyOwnerFilter), }), }; } @@ -404,7 +411,13 @@ function HostRecordsPanel() { return column; }); const hasFilters = - agencyOwnerUserId || cycleKey || settlementType || status || timeRange.startMs || timeRange.endMs || userId; + !isUserIdentityFilterEmpty(agencyOwnerFilter) || + cycleKey || + settlementType || + status || + timeRange.startMs || + timeRange.endMs || + !isUserIdentityFilterEmpty(userFilter); return ( <> @@ -633,7 +646,7 @@ function TeamRecordsPanel() { const [triggerMode, setTriggerMode] = useState(""); const [cycleKey, setCycleKey] = useState(""); const [countryCode, setCountryCode] = useState(""); - const [userId, setUserId] = useState(""); + const [userFilter, setUserFilter] = useState(emptyUserIdentityFilter); const [status, setStatus] = useState(""); const [timeRange, setTimeRange] = useState({ endMs: "", startMs: "" }); @@ -647,9 +660,9 @@ function TeamRecordsPanel() { start_at_ms: timeRange.startMs || "", status, trigger_mode: triggerMode, - user_id: userId, + ...userIdentityFilterParams(userFilter), }), - [countryCode, cycleKey, policyType, status, timeRange.endMs, timeRange.startMs, triggerMode, userId], + [countryCode, cycleKey, policyType, status, timeRange.endMs, timeRange.startMs, triggerMode, userFilter], ); const query = usePaginatedQuery({ errorMessage: "获取 BD/Admin 结算记录失败", @@ -675,7 +688,7 @@ function TeamRecordsPanel() { setCountryCode(""); setStatus(""); setTimeRange({ endMs: "", startMs: "" }); - setUserId(""); + setUserFilter(emptyUserIdentityFilter); setPage(1); }; const tableColumns = teamRecordColumns.map((column) => { @@ -683,10 +696,9 @@ function TeamRecordsPanel() { if (column.key === "user") { return { ...column, - filter: createTextColumnFilter({ - placeholder: "用户 ID", - value: userId, - onChange: changeFilter(setUserId), + filter: createUserIdentityColumnFilter({ + value: userFilter, + onChange: changeFilter(setUserFilter), }), }; } @@ -720,7 +732,7 @@ function TeamRecordsPanel() { status || timeRange.startMs || timeRange.endMs || - userId; + !isUserIdentityFilterEmpty(userFilter); return ( <> diff --git a/src/features/host-org/hooks/useHostAgenciesPage.js b/src/features/host-org/hooks/useHostAgenciesPage.js index 57dc744..aa485ac 100644 --- a/src/features/host-org/hooks/useHostAgenciesPage.js +++ b/src/features/host-org/hooks/useHostAgenciesPage.js @@ -5,6 +5,7 @@ import { useConfirm } from "@/shared/ui/ConfirmProvider.jsx"; import { useToast } from "@/shared/ui/ToastProvider.jsx"; 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"; @@ -25,9 +26,10 @@ 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 [parentBdUserId, setParentBdUserId] = useState(""); + const [parentBdFilter, setParentBdFilter] = useState(emptyUserIdentityFilter); const [sortBy, setSortBy] = useState(""); const [sortDirection, setSortDirection] = useState(""); const [page, setPage] = useState(1); @@ -39,13 +41,22 @@ 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", + }), }), - [parentBdUserId, query, regionId, sortBy, sortDirection, status], + [ownerFilter, parentBdFilter, query, regionId, sortBy, sortDirection, status], ); const { data = emptyData, @@ -96,8 +107,13 @@ export function useHostAgenciesPage() { setPage(1); }; - const changeParentBdUserId = (value) => { - setParentBdUserId(value); + const changeParentBdFilter = (value) => { + setParentBdFilter(value); + setPage(1); + }; + + const changeOwnerFilter = (value) => { + setOwnerFilter(value); setPage(1); }; @@ -109,9 +125,10 @@ export function useHostAgenciesPage() { const resetFilters = () => { setQuery(""); + setOwnerFilter(emptyUserIdentityFilter); setStatus(""); setRegionId(""); - setParentBdUserId(""); + setParentBdFilter(emptyUserIdentityFilter); setSortBy(""); setSortDirection(""); setPage(1); @@ -202,7 +219,8 @@ export function useHostAgenciesPage() { abilities, activeAction, agencyForm, - changeParentBdUserId, + changeOwnerFilter, + changeParentBdFilter, changeQuery, changeRegionId, changeStatus, @@ -212,8 +230,9 @@ export function useHostAgenciesPage() { loading, loadingAction, loadingRegions, + ownerFilter, page, - parentBdUserId, + parentBdFilter, query, regionId, regionOptions, diff --git a/src/features/host-org/hooks/useHostBdsPage.js b/src/features/host-org/hooks/useHostBdsPage.js index acf4176..b21419d 100644 --- a/src/features/host-org/hooks/useHostBdsPage.js +++ b/src/features/host-org/hooks/useHostBdsPage.js @@ -15,6 +15,7 @@ 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"; @@ -34,10 +35,10 @@ export function useHostBdsPage({ profileType = "bd" } = {}) { const confirm = useConfirm(); const { showToast } = useToast(); const { loadingRegions, regionOptions } = useRegionOptions(); - const [query, setQuery] = useState(""); + const [userFilter, setUserFilter] = useState(emptyUserIdentityFilter); const [status, setStatus] = useState(""); const [regionId, setRegionId] = useState(""); - const [parentLeaderUserId, setParentLeaderUserId] = useState(""); + const [parentLeaderFilter, setParentLeaderFilter] = useState(emptyUserIdentityFilter); const [sortBy, setSortBy] = useState(""); const [sortDirection, setSortDirection] = useState(""); const [page, setPage] = useState(1); @@ -53,14 +54,20 @@ 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", + }) + : {}), }), - [parentLeaderUserId, profileType, query, regionId, sortBy, sortDirection, status], + [parentLeaderFilter, profileType, regionId, sortBy, sortDirection, status, userFilter], ); const fetcher = profileType === "bd" ? listBDs : listBDLeaders; const { @@ -142,8 +149,8 @@ export function useHostBdsPage({ profileType = "bd" } = {}) { }; }, [activeAction, bdForm.targetUserId]); - const changeQuery = (value) => { - setQuery(value); + const changeUserFilter = (value) => { + setUserFilter(value); setPage(1); }; @@ -157,8 +164,8 @@ export function useHostBdsPage({ profileType = "bd" } = {}) { setPage(1); }; - const changeParentLeaderUserId = (value) => { - setParentLeaderUserId(value); + const changeParentLeaderFilter = (value) => { + setParentLeaderFilter(value); setPage(1); }; @@ -169,10 +176,10 @@ export function useHostBdsPage({ profileType = "bd" } = {}) { }; const resetFilters = () => { - setQuery(""); + setUserFilter(emptyUserIdentityFilter); setStatus(""); setRegionId(""); - setParentLeaderUserId(""); + setParentLeaderFilter(emptyUserIdentityFilter); setSortBy(""); setSortDirection(""); setPage(1); @@ -341,10 +348,10 @@ export function useHostBdsPage({ profileType = "bd" } = {}) { bdForm, bdLeaderForm, bdUserLookup, - changeParentLeaderUserId, - changeQuery, + changeParentLeaderFilter, changeRegionId, changeStatus, + changeUserFilter, data: displayData, error, loading, @@ -354,9 +361,8 @@ export function useHostBdsPage({ profileType = "bd" } = {}) { leaderBDsError, leaderBDsLoading, page, - parentLeaderUserId, + parentLeaderFilter, profileType, - query, regionId, regionOptions, reload, @@ -382,6 +388,7 @@ export function useHostBdsPage({ profileType = "bd" } = {}) { changeSort, toggleBD, toggleBDLeader, + userFilter, }; } diff --git a/src/features/host-org/hooks/useHostCoinSellersPage.js b/src/features/host-org/hooks/useHostCoinSellersPage.js index c11308b..4a02d36 100644 --- a/src/features/host-org/hooks/useHostCoinSellersPage.js +++ b/src/features/host-org/hooks/useHostCoinSellersPage.js @@ -3,6 +3,7 @@ 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, @@ -55,7 +56,7 @@ export function useHostCoinSellersPage() { const abilities = useCoinSellerAbilities(); const { showToast } = useToast(); const { loadingRegions, regionOptions } = useRegionOptions(); - const [query, setQuery] = useState(""); + const [userFilter, setUserFilter] = useState(emptyUserIdentityFilter); const [status, setStatus] = useState(""); const [regionId, setRegionId] = useState(""); const [sortBy, setSortBy] = useState(""); @@ -75,13 +76,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), }), - [query, regionId, sortBy, sortDirection, status], + [regionId, sortBy, sortDirection, status, userFilter], ); const { data: queryData = emptyData, @@ -107,8 +108,8 @@ export function useHostCoinSellersPage() { }; }, [queryData, sellerRowPatches]); - const changeQuery = (value) => { - setQuery(value); + const changeUserFilter = (value) => { + setUserFilter(value); setPage(1); }; @@ -129,7 +130,7 @@ export function useHostCoinSellersPage() { }; const resetFilters = () => { - setQuery(""); + setUserFilter(emptyUserIdentityFilter); setStatus(""); setRegionId(""); setSortBy(""); @@ -396,10 +397,10 @@ export function useHostCoinSellersPage() { return { abilities, activeAction, - changeQuery, changeRegionId, changeSort, changeStatus, + changeUserFilter, closeAction, contactSavingIds, createForm, @@ -417,7 +418,6 @@ export function useHostCoinSellersPage() { openStockCredit, openStockDebit, page, - query, regionId, regionOptions, rateRegionId, @@ -446,6 +446,7 @@ export function useHostCoinSellersPage() { submitStockDebit, toggleSeller, updateRateTier, + userFilter, }; } diff --git a/src/features/host-org/hooks/useHostHostsPage.js b/src/features/host-org/hooks/useHostHostsPage.js index db84c7e..544345e 100644 --- a/src/features/host-org/hooks/useHostHostsPage.js +++ b/src/features/host-org/hooks/useHostHostsPage.js @@ -2,6 +2,7 @@ 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; @@ -10,7 +11,7 @@ const emptyData = { items: [], page: 1, pageSize, total: 0 }; export function useHostHostsPage() { const abilities = useHostAbilities(); const { loadingRegions, regionOptions } = useRegionOptions(); - const [query, setQuery] = useState(""); + const [userFilter, setUserFilter] = useState(emptyUserIdentityFilter); const [status, setStatus] = useState(""); const [regionId, setRegionId] = useState(""); const [agencyId, setAgencyId] = useState(""); @@ -20,13 +21,13 @@ export function useHostHostsPage() { const filters = useMemo( () => ({ agency_id: agencyId, - keyword: query, region_id: regionId, sort_by: sortBy, sort_direction: sortDirection, status, + ...userIdentityFilterParams(userFilter), }), - [agencyId, query, regionId, sortBy, sortDirection, status], + [agencyId, regionId, sortBy, sortDirection, status, userFilter], ); const { data = emptyData, @@ -42,8 +43,8 @@ export function useHostHostsPage() { queryKey: ["host-org", "hosts", filters, page], }); - const changeQuery = (value) => { - setQuery(value); + const changeUserFilter = (value) => { + setUserFilter(value); setPage(1); }; @@ -69,7 +70,7 @@ export function useHostHostsPage() { }; const resetFilters = () => { - setQuery(""); + setUserFilter(emptyUserIdentityFilter); setStatus(""); setRegionId(""); setAgencyId(""); @@ -82,16 +83,15 @@ export function useHostHostsPage() { abilities, agencyId, changeAgencyId, - changeQuery, changeRegionId, changeStatus, + changeUserFilter, data, diamondSortDirection: sortBy === "diamond" ? sortDirection : "", error, loading, loadingRegions, page, - query, regionId, regionOptions, reload, @@ -102,5 +102,6 @@ export function useHostHostsPage() { status, changeSort, toggleDiamondSort: () => changeSort("diamond"), + userFilter, }; } diff --git a/src/features/host-org/hooks/useHostManagersPage.js b/src/features/host-org/hooks/useHostManagersPage.js index 3f4b6c5..69bd94c 100644 --- a/src/features/host-org/hooks/useHostManagersPage.js +++ b/src/features/host-org/hooks/useHostManagersPage.js @@ -6,6 +6,7 @@ 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 }; @@ -48,17 +49,17 @@ export function useHostManagersPage() { const [editingManager, setEditingManager] = useState(null); const [loadingAction, setLoadingAction] = useState(""); const [managerForm, setManagerForm] = useState(emptyManagerForm); - const [query, setQuery] = useState(""); + const [userFilter, setUserFilter] = useState(emptyUserIdentityFilter); const [status, setStatus] = useState(""); const [regionId, setRegionId] = useState(""); const [page, setPage] = useState(1); const filters = useMemo( () => ({ - keyword: query, region_id: regionId, status, + ...userIdentityFilterParams(userFilter), }), - [query, regionId, status], + [regionId, status, userFilter], ); const { data = emptyData, @@ -74,8 +75,8 @@ export function useHostManagersPage() { queryKey: ["host-org", "managers", filters, page], }); - const changeQuery = (value) => { - setQuery(value); + const changeUserFilter = (value) => { + setUserFilter(value); setPage(1); }; @@ -90,7 +91,7 @@ export function useHostManagersPage() { }; const resetFilters = () => { - setQuery(""); + setUserFilter(emptyUserIdentityFilter); setStatus(""); setRegionId(""); setPage(1); @@ -163,9 +164,9 @@ export function useHostManagersPage() { return { abilities, activeAction, - changeQuery, changeRegionId, changeStatus, + changeUserFilter, closeAction, data, editingManager, @@ -177,7 +178,6 @@ export function useHostManagersPage() { openManagerEdit, openManagerForm, page, - query, regionId, regionOptions, reload, @@ -187,5 +187,6 @@ export function useHostManagersPage() { status, submitManager, toggleManager, + userFilter, }; } diff --git a/src/features/host-org/pages/HostAgenciesPage.jsx b/src/features/host-org/pages/HostAgenciesPage.jsx index b1c2047..64390a1 100644 --- a/src/features/host-org/pages/HostAgenciesPage.jsx +++ b/src/features/host-org/pages/HostAgenciesPage.jsx @@ -13,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 } from "@/shared/ui/tableFilters.js"; +import { createOptionsColumnFilter, createTextColumnFilter, createUserIdentityColumnFilter } from "@/shared/ui/tableFilters.js"; import styles from "@/features/host-org/host-org.module.css"; const agencyColumns = [ @@ -97,22 +97,30 @@ export function HostAgenciesPage() { return { ...column, filter: createTextColumnFilter({ - placeholder: "搜索 Agency、Owner、用户", + placeholder: "搜索 Agency", value: page.query, onChange: page.changeQuery, }), }; } - if (column.key === "parentBdUserId") { + if (column.key === "ownerUserId") { return { ...column, - filter: createTextColumnFilter({ - placeholder: "搜索上级 BD 用户 ID", - value: page.parentBdUserId, - onChange: page.changeParentBdUserId, + filter: createUserIdentityColumnFilter({ + value: page.ownerFilter, + onChange: page.changeOwnerFilter, }), }; } + if (column.key === "parentBdUserId") { + return { + ...column, + filter: createUserIdentityColumnFilter({ + value: page.parentBdFilter, + onChange: page.changeParentBdFilter, + }), + }; + } if (column.key === "status") { return { ...column, diff --git a/src/features/host-org/pages/HostBdLeadersPage.jsx b/src/features/host-org/pages/HostBdLeadersPage.jsx index 81114ef..a282e8d 100644 --- a/src/features/host-org/pages/HostBdLeadersPage.jsx +++ b/src/features/host-org/pages/HostBdLeadersPage.jsx @@ -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 { createTextColumnFilter } from "@/shared/ui/tableFilters.js"; +import { createUserIdentityColumnFilter } from "@/shared/ui/tableFilters.js"; import styles from "@/features/host-org/host-org.module.css"; const bdLeaderBaseColumns = [ @@ -107,10 +107,9 @@ export function HostBdLeadersPage() { if (column.key === "user") { return { ...column, - filter: createTextColumnFilter({ - placeholder: "搜索用户 ID、展示 ID、用户名", - value: page.query, - onChange: page.changeQuery, + filter: createUserIdentityColumnFilter({ + value: page.userFilter, + onChange: page.changeUserFilter, }), }; } diff --git a/src/features/host-org/pages/HostBdLeadersPage.test.jsx b/src/features/host-org/pages/HostBdLeadersPage.test.jsx index 73e0329..e0b80b5 100644 --- a/src/features/host-org/pages/HostBdLeadersPage.test.jsx +++ b/src/features/host-org/pages/HostBdLeadersPage.test.jsx @@ -12,21 +12,21 @@ afterEach(() => { }); test("bd leader table exposes user and region column filters", () => { - const changeQuery = vi.fn(); + const changeUserFilter = vi.fn(); const changeRegionId = vi.fn(); - vi.mocked(useHostBdsPage).mockReturnValue(pageFixture({ changeQuery, changeRegionId })); + vi.mocked(useHostBdsPage).mockReturnValue(pageFixture({ changeUserFilter, changeRegionId })); render(); fireEvent.click(screen.getByRole("button", { name: "用户" })); - fireEvent.change(screen.getByPlaceholderText("搜索用户 ID、展示 ID、用户名"), { + fireEvent.change(screen.getByPlaceholderText("输入短 ID"), { target: { value: "164453" }, }); fireEvent.click(screen.getByRole("button", { name: "搜索" })); fireEvent.click(screen.getByRole("button", { name: "区域" })); fireEvent.click(screen.getByRole("menuitem", { name: "南亚区" })); - expect(changeQuery).toHaveBeenCalledWith("164453"); + expect(changeUserFilter).toHaveBeenCalledWith({ displayUserId: "164453", userId: "", username: "" }); 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 }, - changeParentLeaderUserId: vi.fn(), - changeQuery: vi.fn(), + changeParentLeaderFilter: 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,8 +59,7 @@ function pageFixture(patch = {}) { openBDLeaderForm: vi.fn(), openLeaderBDs: vi.fn(), page: 1, - parentLeaderUserId: "", - query: "", + parentLeaderFilter: { displayUserId: "", userId: "", username: "" }, regionId: "", regionOptions: [{ label: "南亚区", value: "1" }], reload: vi.fn(), @@ -75,6 +74,7 @@ function pageFixture(patch = {}) { submitBDLeader: vi.fn(), submitLeaderBD: vi.fn(), toggleBDLeader: vi.fn(), + userFilter: { displayUserId: "", userId: "", username: "" }, ...patch, }; } diff --git a/src/features/host-org/pages/HostBdsPage.jsx b/src/features/host-org/pages/HostBdsPage.jsx index 5e9c36d..5a3ff71 100644 --- a/src/features/host-org/pages/HostBdsPage.jsx +++ b/src/features/host-org/pages/HostBdsPage.jsx @@ -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, createTextColumnFilter } from "@/shared/ui/tableFilters.js"; +import { createOptionsColumnFilter, createUserIdentityColumnFilter } from "@/shared/ui/tableFilters.js"; import styles from "@/features/host-org/host-org.module.css"; const bdColumns = [ @@ -97,20 +97,18 @@ export function HostBdsPage() { if (column.key === "user") { return { ...column, - filter: createTextColumnFilter({ - placeholder: "搜索用户 ID、展示 ID、用户名", - value: page.query, - onChange: page.changeQuery, + filter: createUserIdentityColumnFilter({ + value: page.userFilter, + onChange: page.changeUserFilter, }), }; } if (column.key === "parentLeaderUserId") { return { ...column, - filter: createTextColumnFilter({ - placeholder: "搜索 Leader 用户 ID", - value: page.parentLeaderUserId, - onChange: page.changeParentLeaderUserId, + filter: createUserIdentityColumnFilter({ + value: page.parentLeaderFilter, + onChange: page.changeParentLeaderFilter, }), }; } diff --git a/src/features/host-org/pages/HostCoinSellersPage.jsx b/src/features/host-org/pages/HostCoinSellersPage.jsx index bfec250..eb6f829 100644 --- a/src/features/host-org/pages/HostCoinSellersPage.jsx +++ b/src/features/host-org/pages/HostCoinSellersPage.jsx @@ -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, createTextColumnFilter } from "@/shared/ui/tableFilters.js"; +import { createOptionsColumnFilter, createUserIdentityColumnFilter } from "@/shared/ui/tableFilters.js"; import styles from "@/features/host-org/host-org.module.css"; const baseColumns = [ @@ -106,10 +106,9 @@ export function HostCoinSellersPage() { if (column.key === "seller") { return { ...column, - filter: createTextColumnFilter({ - placeholder: "搜索用户 ID、展示 ID、用户名", - value: page.query, - onChange: page.changeQuery, + filter: createUserIdentityColumnFilter({ + value: page.userFilter, + onChange: page.changeUserFilter, }), }; } diff --git a/src/features/host-org/pages/HostHostsPage.jsx b/src/features/host-org/pages/HostHostsPage.jsx index 153b44f..17a70ac 100644 --- a/src/features/host-org/pages/HostHostsPage.jsx +++ b/src/features/host-org/pages/HostHostsPage.jsx @@ -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 } from "@/shared/ui/tableFilters.js"; +import { createOptionsColumnFilter, createTextColumnFilter, createUserIdentityColumnFilter } from "@/shared/ui/tableFilters.js"; import styles from "@/features/host-org/host-org.module.css"; const hostColumns = [ @@ -99,10 +99,9 @@ export function HostHostsPage() { if (column.key === "user") { return { ...column, - filter: createTextColumnFilter({ - placeholder: "搜索用户 ID、展示 ID、用户名、Agency", - value: page.query, - onChange: page.changeQuery, + filter: createUserIdentityColumnFilter({ + value: page.userFilter, + onChange: page.changeUserFilter, }), }; } diff --git a/src/features/host-org/pages/HostManagersPage.jsx b/src/features/host-org/pages/HostManagersPage.jsx index d428c4a..ac86ada 100644 --- a/src/features/host-org/pages/HostManagersPage.jsx +++ b/src/features/host-org/pages/HostManagersPage.jsx @@ -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, createTextColumnFilter } from "@/shared/ui/tableFilters.js"; +import { createOptionsColumnFilter, createUserIdentityColumnFilter } from "@/shared/ui/tableFilters.js"; import styles from "@/features/host-org/host-org.module.css"; const managerPermissionOptions = [ @@ -100,10 +100,9 @@ export function HostManagersPage() { if (column.key === "userId") { return { ...column, - filter: createTextColumnFilter({ - placeholder: "搜索经理", - value: page.query, - onChange: page.changeQuery, + filter: createUserIdentityColumnFilter({ + value: page.userFilter, + onChange: page.changeUserFilter, }), }; } diff --git a/src/features/invite-activity-reward/hooks/useInviteActivityRewardPage.js b/src/features/invite-activity-reward/hooks/useInviteActivityRewardPage.js index 02b041e..06e47e8 100644 --- a/src/features/invite-activity-reward/hooks/useInviteActivityRewardPage.js +++ b/src/features/invite-activity-reward/hooks/useInviteActivityRewardPage.js @@ -7,6 +7,7 @@ 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 }; @@ -25,12 +26,12 @@ export function useInviteActivityRewardPage() { const [form, setForm] = useState(emptyForm); const [configLoading, setConfigLoading] = useState(false); const [configSaving, setConfigSaving] = useState(false); - const [query, setQuery] = useState(""); + const [userFilter, setUserFilter] = useState(emptyUserIdentityFilter); const [cycleKey, setCycleKey] = useState(""); const [page, setPage] = useState(1); const filters = useMemo( - () => ({ keyword: query, cycle_key: cycleKey }), - [cycleKey, query], + () => ({ cycle_key: cycleKey, ...userIdentityFilterParams(userFilter) }), + [cycleKey, userFilter], ); const { data: summaries = emptySummaries, @@ -73,8 +74,8 @@ export function useInviteActivityRewardPage() { setForm(config ? formFromConfig(config) : emptyForm); }, [config]); - const changeQuery = (value) => { - setQuery(value); + const changeUserFilter = (value) => { + setUserFilter(value); setPage(1); }; const changeCycleKey = (value) => { @@ -104,7 +105,7 @@ export function useInviteActivityRewardPage() { return { abilities, changeCycleKey, - changeQuery, + changeUserFilter, closeConfigDrawer, config, configDrawerOpen, @@ -114,7 +115,6 @@ export function useInviteActivityRewardPage() { form, openConfigDrawer, page, - query, reloadConfig, setForm, setPage, @@ -123,6 +123,7 @@ export function useInviteActivityRewardPage() { summariesError, summariesLoading, submitConfig, + userFilter, }; } diff --git a/src/features/invite-activity-reward/pages/InviteActivityRewardPage.jsx b/src/features/invite-activity-reward/pages/InviteActivityRewardPage.jsx index c78d5a4..ecc9b80 100644 --- a/src/features/invite-activity-reward/pages/InviteActivityRewardPage.jsx +++ b/src/features/invite-activity-reward/pages/InviteActivityRewardPage.jsx @@ -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 } from "@/shared/ui/tableFilters.js"; +import { createTextColumnFilter, createUserIdentityColumnFilter } from "@/shared/ui/tableFilters.js"; import { formatMillis } from "@/shared/utils/time.js"; export function InviteActivityRewardPage() { @@ -25,10 +25,9 @@ export function InviteActivityRewardPage() { label: "用户信息", width: "minmax(280px, 1.1fr)", render: (summary) => , - filter: createTextColumnFilter({ - placeholder: "搜索用户 ID、短号、名称", - value: page.query, - onChange: page.changeQuery, + filter: createUserIdentityColumnFilter({ + value: page.userFilter, + onChange: page.changeUserFilter, }), }, { diff --git a/src/features/operations/components/CoinLedgerTable.jsx b/src/features/operations/components/CoinLedgerTable.jsx index 791bf76..0bc9c73 100644 --- a/src/features/operations/components/CoinLedgerTable.jsx +++ b/src/features/operations/components/CoinLedgerTable.jsx @@ -8,7 +8,8 @@ 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, createTextColumnFilter } from "@/shared/ui/tableFilters.js"; +import { createOptionsColumnFilter, createUserIdentityColumnFilter } from "@/shared/ui/tableFilters.js"; +import { emptyUserIdentityFilter, isUserIdentityFilterEmpty, userIdentityFilterParams } from "@/shared/ui/userIdentityFilter.js"; import styles from "@/features/operations/operations.module.css"; const pageSize = 50; @@ -143,7 +144,7 @@ export function CoinLedgerTable({ lockedUser = null, userId = "" }) { const [page, setPage] = useState(1); const [selectedEntry, setSelectedEntry] = useState(null); const [bizType, setBizType] = useState(""); - const [userKeyword, setUserKeyword] = useState(""); + const [userFilter, setUserFilter] = useState(emptyUserIdentityFilter); const [timeRange, setTimeRange] = useState({ endMs: "", startMs: "" }); const filters = useMemo( @@ -151,9 +152,10 @@ export function CoinLedgerTable({ lockedUser = null, userId = "" }) { biz_type: bizType, end_at_ms: timeRange.endMs || "", start_at_ms: timeRange.startMs || "", - user_keyword: fixedUserId || userKeyword, + ...userIdentityFilterParams(userFilter), + user_id: fixedUserId || userFilter.userId, }), - [bizType, fixedUserId, timeRange.endMs, timeRange.startMs, userKeyword], + [bizType, fixedUserId, timeRange.endMs, timeRange.startMs, userFilter], ); const query = usePaginatedQuery({ errorMessage: "获取金币流水失败", @@ -166,14 +168,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(userKeyword || bizType || timeRange.startMs || timeRange.endMs); + const hasMutableFilters = Boolean(!isUserIdentityFilterEmpty(userFilter) || bizType || timeRange.startMs || timeRange.endMs); const changeBizType = (value) => { setBizType(value); setPage(1); }; - const changeUserKeyword = (value) => { - setUserKeyword(value); + const changeUserFilter = (value) => { + setUserFilter(value); setPage(1); }; const changeTimeRange = (nextRange) => { @@ -182,7 +184,7 @@ export function CoinLedgerTable({ lockedUser = null, userId = "" }) { }; const resetFilters = () => { setBizType(""); - setUserKeyword(""); + setUserFilter(emptyUserIdentityFilter); setTimeRange({ endMs: "", startMs: "" }); setPage(1); }; @@ -196,10 +198,9 @@ export function CoinLedgerTable({ lockedUser = null, userId = "" }) { if (column.key === "user") { return { ...column, - filter: createTextColumnFilter({ - placeholder: "长 ID / 短 ID / 昵称", - value: userKeyword, - onChange: changeUserKeyword, + filter: createUserIdentityColumnFilter({ + value: userFilter, + onChange: changeUserFilter, }), }; } diff --git a/src/features/operations/components/CoinSellerLedgerTable.jsx b/src/features/operations/components/CoinSellerLedgerTable.jsx index be16bd7..a14da69 100644 --- a/src/features/operations/components/CoinSellerLedgerTable.jsx +++ b/src/features/operations/components/CoinSellerLedgerTable.jsx @@ -10,8 +10,9 @@ 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, createTextColumnFilter } from "@/shared/ui/tableFilters.js"; +import { createOptionsColumnFilter, createUserIdentityColumnFilter } 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; @@ -98,7 +99,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 [sellerKeyword, setSellerKeyword] = useState(""); + const [sellerFilter, setSellerFilter] = useState(emptyUserIdentityFilter); const [ledgerType, setLedgerType] = useState(""); const [timeRange, setTimeRange] = useState({ endMs: "", startMs: "" }); const { showToast } = useToast(); @@ -107,11 +108,15 @@ 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, sellerKeyword, timeRange.endMs, timeRange.startMs], + [fixedSellerUserId, ledgerType, sellerFilter, timeRange.endMs, timeRange.startMs], ); const query = usePaginatedQuery({ errorMessage: "获取币商流水失败", @@ -124,10 +129,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(sellerKeyword || ledgerType || timeRange.startMs || timeRange.endMs); + const hasActiveFilters = Boolean(!isUserIdentityFilterEmpty(sellerFilter) || ledgerType || timeRange.startMs || timeRange.endMs); - const changeSellerKeyword = (value) => { - setSellerKeyword(value); + const changeSellerFilter = (value) => { + setSellerFilter(value); setPage(1); }; const changeLedgerType = (value) => { @@ -139,7 +144,7 @@ export function CoinSellerLedgerTable({ lockedSeller = null, sellerUserId = "" } setPage(1); }; const resetFilters = () => { - setSellerKeyword(""); + setSellerFilter(emptyUserIdentityFilter); setLedgerType(""); setTimeRange({ endMs: "", startMs: "" }); setPage(1); @@ -162,10 +167,9 @@ export function CoinSellerLedgerTable({ lockedSeller = null, sellerUserId = "" } if (column.key === "seller" && !fixedSellerUserId) { return { ...column, - filter: createTextColumnFilter({ - placeholder: "长 ID / 短 ID / 名称", - value: sellerKeyword, - onChange: changeSellerKeyword, + filter: createUserIdentityColumnFilter({ + value: sellerFilter, + onChange: changeSellerFilter, }), }; } diff --git a/src/features/operations/pages/CoinAdjustmentPage.jsx b/src/features/operations/pages/CoinAdjustmentPage.jsx index 78f5f87..9022337 100644 --- a/src/features/operations/pages/CoinAdjustmentPage.jsx +++ b/src/features/operations/pages/CoinAdjustmentPage.jsx @@ -26,7 +26,8 @@ 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 { createTextColumnFilter } from "@/shared/ui/tableFilters.js"; +import { createUserIdentityColumnFilter } from "@/shared/ui/tableFilters.js"; +import { emptyUserIdentityFilter, isUserIdentityFilterEmpty, userIdentityFilterParams } from "@/shared/ui/userIdentityFilter.js"; import { formatMillis } from "@/shared/utils/time.js"; const pageSize = 50; @@ -83,7 +84,7 @@ export function CoinAdjustmentPage() { const abilities = useCoinAdjustmentAbilities(); const { showToast } = useToast(); const [page, setPage] = useState(1); - const [userKeyword, setUserKeyword] = useState(""); + const [userFilter, setUserFilter] = useState(emptyUserIdentityFilter); const [timeRange, setTimeRange] = useState({ endMs: "", startMs: "" }); const [dialogOpen, setDialogOpen] = useState(false); const [form, setForm] = useState(emptyForm); @@ -95,9 +96,9 @@ export function CoinAdjustmentPage() { () => ({ end_at_ms: timeRange.endMs || "", start_at_ms: timeRange.startMs || "", - user_keyword: userKeyword, + ...userIdentityFilterParams(userFilter), }), - [timeRange.endMs, timeRange.startMs, userKeyword], + [timeRange.endMs, timeRange.startMs, userFilter], ); const query = usePaginatedQuery({ errorMessage: "获取金币增减列表失败", @@ -115,11 +116,10 @@ export function CoinAdjustmentPage() { column.key === "user" ? { ...column, - filter: createTextColumnFilter({ - placeholder: "长 ID / 短 ID / 昵称", - value: userKeyword, + filter: createUserIdentityColumnFilter({ + value: userFilter, onChange: (value) => { - setUserKeyword(value); + setUserFilter(value); setPage(1); }, }), @@ -128,7 +128,7 @@ export function CoinAdjustmentPage() { ); const resetFilters = () => { - setUserKeyword(""); + setUserFilter(emptyUserIdentityFilter); setTimeRange({ endMs: "", startMs: "" }); setPage(1); }; @@ -210,7 +210,7 @@ export function CoinAdjustmentPage() { <> diff --git a/src/features/payment/pages/PaymentBillListPage.jsx b/src/features/payment/pages/PaymentBillListPage.jsx index 44ae657..0cf3010 100644 --- a/src/features/payment/pages/PaymentBillListPage.jsx +++ b/src/features/payment/pages/PaymentBillListPage.jsx @@ -14,7 +14,8 @@ 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 } from "@/shared/ui/tableFilters.js"; +import { createOptionsColumnFilter, createTextColumnFilter, createUserIdentityColumnFilter } from "@/shared/ui/tableFilters.js"; +import { emptyUserIdentityFilter, userIdentityFilterParams } from "@/shared/ui/userIdentityFilter.js"; import { formatMillis } from "@/shared/utils/time.js"; import styles from "@/features/payment/payment.module.css"; @@ -95,8 +96,8 @@ export function PaymentBillListPage() { const [status, setStatus] = useState(""); const [rechargeType, setRechargeType] = useState(""); const [regionId, setRegionId] = useState(""); - const [userId, setUserId] = useState(""); - const [sellerUserId, setSellerUserId] = useState(""); + const [userFilter, setUserFilter] = useState(emptyUserIdentityFilter); + const [sellerFilter, setSellerFilter] = useState(emptyUserIdentityFilter); const [timeRange, setTimeRange] = useState({ endMs: "", startMs: "" }); const [exporting, setExporting] = useState(false); const { loadingRegions, regionOptions } = useRegionOptions(); @@ -109,12 +110,16 @@ export function PaymentBillListPage() { keyword, recharge_type: rechargeType, region_id: regionId, - seller_user_id: sellerUserId, start_at_ms: timeRange.startMs, status, - user_id: userId, + ...userIdentityFilterParams(userFilter), + ...userIdentityFilterParams(sellerFilter, { + displayUserId: "seller_display_user_id", + userId: "seller_user_id", + username: "seller_username", + }), }), - [keyword, rechargeType, regionId, sellerUserId, status, timeRange.endMs, timeRange.startMs, userId], + [keyword, rechargeType, regionId, sellerFilter, status, timeRange.endMs, timeRange.startMs, userFilter], ); const query = usePaginatedQuery({ errorMessage: "获取账单列表失败", @@ -163,20 +168,18 @@ export function PaymentBillListPage() { if (column.key === "userId") { return { ...column, - filter: createTextColumnFilter({ - placeholder: "搜索用户 ID", - value: userId, - onChange: (value) => resetPage(setUserId)(digitsOnly(value)), + filter: createUserIdentityColumnFilter({ + value: userFilter, + onChange: resetPage(setUserFilter), }), }; } if (column.key === "sellerUserId") { return { ...column, - filter: createTextColumnFilter({ - placeholder: "搜索币商 ID", - value: sellerUserId, - onChange: (value) => resetPage(setSellerUserId)(digitsOnly(value)), + filter: createUserIdentityColumnFilter({ + value: sellerFilter, + onChange: resetPage(setSellerFilter), }), }; } @@ -507,7 +510,3 @@ function filenameTimestamp() { function pad2(value) { return String(value).padStart(2, "0"); } - -function digitsOnly(value) { - return value.replace(/\D/g, ""); -} diff --git a/src/features/registration-reward/hooks/useRegistrationRewardPage.js b/src/features/registration-reward/hooks/useRegistrationRewardPage.js index 7b8c53a..9fa00b1 100644 --- a/src/features/registration-reward/hooks/useRegistrationRewardPage.js +++ b/src/features/registration-reward/hooks/useRegistrationRewardPage.js @@ -1,6 +1,7 @@ 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, @@ -30,7 +31,7 @@ export function useRegistrationRewardPage() { const [configLoading, setConfigLoading] = useState(false); const [configSaving, setConfigSaving] = useState(false); const [resourceGroups, setResourceGroups] = useState([]); - const [query, setQuery] = useState(""); + const [userFilter, setUserFilter] = useState(emptyUserIdentityFilter); const [status, setStatus] = useState(""); const [timeRange, setTimeRange] = useState(emptyTimeRange); const [page, setPage] = useState(1); @@ -38,10 +39,10 @@ export function useRegistrationRewardPage() { () => ({ claimed_end_ms: timeRange.endMs || undefined, claimed_start_ms: timeRange.startMs || undefined, - keyword: query, status, + ...userIdentityFilterParams(userFilter), }), - [query, status, timeRange], + [status, timeRange, userFilter], ); const { data: claims = emptyClaims, @@ -94,8 +95,8 @@ export function useRegistrationRewardPage() { setForm(emptyForm); }, [config]); - const changeQuery = (value) => { - setQuery(value); + const changeUserFilter = (value) => { + setUserFilter(value); setPage(1); }; @@ -110,7 +111,7 @@ export function useRegistrationRewardPage() { }; const resetFilters = () => { - setQuery(""); + setUserFilter(emptyUserIdentityFilter); setStatus(""); setTimeRange(emptyTimeRange); setPage(1); @@ -138,9 +139,9 @@ export function useRegistrationRewardPage() { return { abilities, - changeQuery, changeStatus, changeTimeRange, + changeUserFilter, claims, claimsError, claimsLoading, @@ -152,7 +153,6 @@ export function useRegistrationRewardPage() { form, openConfigDrawer, page, - query, reloadClaims, reloadConfig, resetFilters, @@ -162,6 +162,7 @@ export function useRegistrationRewardPage() { status, submitConfig, timeRange, + userFilter, }; } diff --git a/src/features/registration-reward/pages/RegistrationRewardPage.jsx b/src/features/registration-reward/pages/RegistrationRewardPage.jsx index ec9f2ba..ebe91d7 100644 --- a/src/features/registration-reward/pages/RegistrationRewardPage.jsx +++ b/src/features/registration-reward/pages/RegistrationRewardPage.jsx @@ -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 } from "@/shared/ui/tableFilters.js"; +import { createOptionsColumnFilter, createUserIdentityColumnFilter } 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,10 +61,9 @@ export function RegistrationRewardPage() { if (column.key === "user") { return { ...column, - filter: createTextColumnFilter({ - placeholder: "搜索用户 ID、短号、名称", - value: page.query, - onChange: page.changeQuery, + filter: createUserIdentityColumnFilter({ + value: page.userFilter, + onChange: page.changeUserFilter, }), }; } diff --git a/src/features/resources/hooks/useResourcePages.js b/src/features/resources/hooks/useResourcePages.js index 66b3ed8..94f3ea1 100644 --- a/src/features/resources/hooks/useResourcePages.js +++ b/src/features/resources/hooks/useResourcePages.js @@ -4,6 +4,7 @@ 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, @@ -503,7 +504,7 @@ export function useResourceShopPage() { const [drawerOpen, setDrawerOpen] = useState(false); const [purchaseDrawerOpen, setPurchaseDrawerOpen] = useState(false); const [purchasePage, setPurchasePage] = useState(1); - const [purchaseUserKeyword, setPurchaseUserKeyword] = useState(""); + const [purchaseUserFilter, setPurchaseUserFilter] = useState(emptyUserIdentityFilter); const [purchaseResourceType, setPurchaseResourceType] = useState(""); const [purchaseKeyword, setPurchaseKeyword] = useState(""); const [drawerQuery, setDrawerQuery] = useState(""); @@ -533,9 +534,9 @@ export function useResourceShopPage() { () => ({ keyword: purchaseKeyword, resource_type: purchaseResourceType, - user_keyword: purchaseUserKeyword, + ...userIdentityFilterParams(purchaseUserFilter), }), - [purchaseKeyword, purchaseResourceType, purchaseUserKeyword], + [purchaseKeyword, purchaseResourceType, purchaseUserFilter], ); const fetchPurchaseOrders = useCallback( (requestQuery) => { @@ -765,7 +766,7 @@ export function useResourceShopPage() { purchaseLoading: purchaseResult.loading, purchasePage, purchaseResourceType, - purchaseUserKeyword, + purchaseUserFilter, reloadPurchaseOrders: purchaseResult.reload, setPurchasePage, resourceOptionsLoading, @@ -779,7 +780,7 @@ export function useResourceShopPage() { toggleShopItem, changePurchaseKeyword: resetSetter(setPurchaseKeyword, setPurchasePage), changePurchaseResourceType: resetSetter(setPurchaseResourceType, setPurchasePage), - changePurchaseUserKeyword: resetSetter(setPurchaseUserKeyword, setPurchasePage), + changePurchaseUserFilter: resetSetter(setPurchaseUserFilter, setPurchasePage), updateDraftItem, }; } @@ -1546,7 +1547,7 @@ export function useResourceGrantListPage() { const abilities = useResourceAbilities(); const confirm = useConfirm(); const { showToast } = useToast(); - const [query, setQuery] = useState(""); + const [targetFilter, setTargetFilter] = useState(emptyUserIdentityFilter); const [status, setStatus] = useState(""); const [page, setPage] = useState(1); const [activeAction, setActiveAction] = useState(""); @@ -1560,9 +1561,13 @@ export function useResourceGrantListPage() { const filters = useMemo( () => ({ status, - target_user_id: query.trim(), + ...userIdentityFilterParams(targetFilter, { + displayUserId: "target_display_user_id", + userId: "target_user_id", + username: "target_username", + }), }), - [query, status], + [status, targetFilter], ); const result = usePaginatedQuery({ errorMessage: "加载资源赠送记录失败", @@ -1681,30 +1686,37 @@ export function useResourceGrantListPage() { }; const resetFilters = () => { - setQuery(""); + setTargetFilter(emptyUserIdentityFilter); 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, }; } diff --git a/src/features/resources/pages/ResourceGrantListPage.jsx b/src/features/resources/pages/ResourceGrantListPage.jsx index 5f247c7..c7392f9 100644 --- a/src/features/resources/pages/ResourceGrantListPage.jsx +++ b/src/features/resources/pages/ResourceGrantListPage.jsx @@ -14,7 +14,7 @@ import { AdminListToolbar, AdminRowActions, } from "@/shared/ui/AdminListLayout.jsx"; -import { createOptionsColumnFilter, createTextColumnFilter } from "@/shared/ui/tableFilters.js"; +import { createOptionsColumnFilter, createUserIdentityColumnFilter } from "@/shared/ui/tableFilters.js"; import { formatMillis } from "@/shared/utils/time.js"; import { GiftResourceSelectField } from "@/features/resources/components/GiftResourceSelectDrawer.jsx"; import { @@ -96,10 +96,9 @@ export function ResourceGrantListPage() { if (column.key === "target") { return { ...column, - filter: createTextColumnFilter({ - placeholder: "搜索用户 ID", - value: page.query, - onChange: page.changeQuery, + filter: createUserIdentityColumnFilter({ + value: page.targetFilter, + onChange: page.changeTargetFilter, }), }; } diff --git a/src/features/resources/pages/ResourceShopPage.jsx b/src/features/resources/pages/ResourceShopPage.jsx index 93d1415..b04bfe3 100644 --- a/src/features/resources/pages/ResourceShopPage.jsx +++ b/src/features/resources/pages/ResourceShopPage.jsx @@ -16,7 +16,7 @@ import { AdminListToolbar, } from "@/shared/ui/AdminListLayout.jsx"; import { SideDrawer } from "@/shared/ui/SideDrawer.jsx"; -import { createOptionsColumnFilter, createTextColumnFilter } from "@/shared/ui/tableFilters.js"; +import { createOptionsColumnFilter, createTextColumnFilter, createUserIdentityColumnFilter } from "@/shared/ui/tableFilters.js"; import { formatMillis } from "@/shared/utils/time.js"; import { resourcePriceTypeLabels, @@ -278,10 +278,9 @@ function ResourceShopPurchaseDrawer({ page }) { if (column.key === "user") { return { ...column, - filter: createTextColumnFilter({ - placeholder: "搜索用户ID、短号、靓号、昵称", - value: page.purchaseUserKeyword, - onChange: page.changePurchaseUserKeyword, + filter: createUserIdentityColumnFilter({ + value: page.purchaseUserFilter, + onChange: page.changePurchaseUserFilter, }), }; } diff --git a/src/features/room-turnover-reward/hooks/useRoomTurnoverRewardPage.js b/src/features/room-turnover-reward/hooks/useRoomTurnoverRewardPage.js index 49c1e04..4b49986 100644 --- a/src/features/room-turnover-reward/hooks/useRoomTurnoverRewardPage.js +++ b/src/features/room-turnover-reward/hooks/useRoomTurnoverRewardPage.js @@ -9,6 +9,7 @@ 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 }; @@ -29,12 +30,20 @@ export function useRoomTurnoverRewardPage() { const [configSaving, setConfigSaving] = useState(false); const [retryingSettlementId, setRetryingSettlementId] = useState(""); const [query, setQuery] = useState(""); - const [ownerUserKeyword, setOwnerUserKeyword] = useState(""); + const [ownerFilter, setOwnerFilter] = useState(emptyUserIdentityFilter); const [status, setStatus] = useState(""); const [page, setPage] = useState(1); const filters = useMemo( - () => ({ keyword: query, owner_user_keyword: ownerUserKeyword, status }), - [ownerUserKeyword, query, status], + () => ({ + keyword: query, + status, + ...userIdentityFilterParams(ownerFilter, { + displayUserId: "owner_display_user_id", + userId: "owner_user_id", + username: "owner_username", + }), + }), + [ownerFilter, query, status], ); const { data: settlements = emptySettlements, @@ -87,8 +96,8 @@ export function useRoomTurnoverRewardPage() { setPage(1); }; - const changeOwnerUserKeyword = (value) => { - setOwnerUserKeyword(value); + const changeOwnerFilter = (value) => { + setOwnerFilter(value); setPage(1); }; @@ -138,7 +147,7 @@ export function useRoomTurnoverRewardPage() { return { abilities, - changeOwnerUserKeyword, + changeOwnerFilter, changeQuery, changeStatus, closeConfigDrawer, @@ -148,7 +157,7 @@ export function useRoomTurnoverRewardPage() { configSaving, form, openConfigDrawer, - ownerUserKeyword, + ownerFilter, page, query, reloadConfig, diff --git a/src/features/room-turnover-reward/pages/RoomTurnoverRewardPage.jsx b/src/features/room-turnover-reward/pages/RoomTurnoverRewardPage.jsx index d04f022..a84e93e 100644 --- a/src/features/room-turnover-reward/pages/RoomTurnoverRewardPage.jsx +++ b/src/features/room-turnover-reward/pages/RoomTurnoverRewardPage.jsx @@ -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 } from "@/shared/ui/tableFilters.js"; +import { createOptionsColumnFilter, createTextColumnFilter, createUserIdentityColumnFilter } from "@/shared/ui/tableFilters.js"; import { TimeText } from "@/shared/ui/TimeText.jsx"; const settlementStatusOptions = [ @@ -131,10 +131,9 @@ export function RoomTurnoverRewardPage() { if (column.key === "owner") { return { ...column, - filter: createTextColumnFilter({ - placeholder: "搜索房主 ID、短号、名称", - value: page.ownerUserKeyword, - onChange: page.changeOwnerUserKeyword, + filter: createUserIdentityColumnFilter({ + value: page.ownerFilter, + onChange: page.changeOwnerFilter, }), }; } diff --git a/src/features/rooms/hooks/useRoomsPage.js b/src/features/rooms/hooks/useRoomsPage.js index ff2dd99..99e818f 100644 --- a/src/features/rooms/hooks/useRoomsPage.js +++ b/src/features/rooms/hooks/useRoomsPage.js @@ -4,6 +4,7 @@ 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"; @@ -24,7 +25,7 @@ export function useRoomsPage() { const { showToast } = useToast(); const { loadingRegions, regionOptions } = useRegionOptions(); const [query, setQuery] = useState(""); - const [ownerUserKeyword, setOwnerUserKeyword] = useState(""); + const [ownerFilter, setOwnerFilter] = useState(emptyUserIdentityFilter); const [status, setStatus] = useState(""); const [regionId, setRegionId] = useState(""); const [contributionSortDirection, setContributionSortDirection] = useState(""); @@ -37,13 +38,17 @@ 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, ownerUserKeyword, query, regionId, status], + [contributionSortDirection, ownerFilter, query, regionId, status], ); const { data = emptyData, @@ -64,8 +69,8 @@ export function useRoomsPage() { setPage(1); }; - const changeOwnerUserKeyword = (value) => { - setOwnerUserKeyword(value); + const changeOwnerFilter = (value) => { + setOwnerFilter(value); setPage(1); }; @@ -86,7 +91,7 @@ export function useRoomsPage() { const resetFilters = () => { setQuery(""); - setOwnerUserKeyword(""); + setOwnerFilter(emptyUserIdentityFilter); setStatus(""); setRegionId(""); setPage(1); @@ -191,8 +196,8 @@ export function useRoomsPage() { abilities, activeAction, activeRoom, + changeOwnerFilter, changeQuery, - changeOwnerUserKeyword, changeRegionId, changeStatus, closeAction, @@ -206,7 +211,7 @@ export function useRoomsPage() { openDetail, openEdit, openStatus, - ownerUserKeyword, + ownerFilter, page, query, regionId, diff --git a/src/features/rooms/pages/RoomListPage.jsx b/src/features/rooms/pages/RoomListPage.jsx index 26ff4a0..3b6eb2a 100644 --- a/src/features/rooms/pages/RoomListPage.jsx +++ b/src/features/rooms/pages/RoomListPage.jsx @@ -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 } from "@/shared/ui/tableFilters.js"; +import { createOptionsColumnFilter, createTextColumnFilter, createUserIdentityColumnFilter } 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,10 +93,9 @@ export function RoomListPage() { : column.key === "owner" ? { ...column, - filter: createTextColumnFilter({ - placeholder: "搜索房主 ID、短号、靓号、昵称", - value: page.ownerUserKeyword, - onChange: page.changeOwnerUserKeyword, + filter: createUserIdentityColumnFilter({ + value: page.ownerFilter, + onChange: page.changeOwnerFilter, }), } : column.key === "status" diff --git a/src/features/seven-day-checkin/hooks/useSevenDayCheckInPage.js b/src/features/seven-day-checkin/hooks/useSevenDayCheckInPage.js index 3f782c6..5455fb6 100644 --- a/src/features/seven-day-checkin/hooks/useSevenDayCheckInPage.js +++ b/src/features/seven-day-checkin/hooks/useSevenDayCheckInPage.js @@ -10,6 +10,7 @@ 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 }; @@ -31,14 +32,14 @@ export function useSevenDayCheckInPage() { const [configLoading, setConfigLoading] = useState(false); const [configSaving, setConfigSaving] = useState(false); const [resourceGroups, setResourceGroups] = useState([]); - const [query, setQuery] = useState(""); + const [userFilter, setUserFilter] = useState(emptyUserIdentityFilter); 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, keyword: query, status }), - [checkinDay, dayIndex, query, status], + () => ({ checkin_day: checkinDay, day_index: dayIndex, status, ...userIdentityFilterParams(userFilter) }), + [checkinDay, dayIndex, status, userFilter], ); const { data: claims = emptyClaims, @@ -85,8 +86,8 @@ export function useSevenDayCheckInPage() { setForm(config ? formFromConfig(config) : emptyForm); }, [config]); - const changeQuery = (value) => { - setQuery(value); + const changeUserFilter = (value) => { + setUserFilter(value); setPage(1); }; @@ -130,8 +131,8 @@ export function useSevenDayCheckInPage() { abilities, changeCheckinDay, changeDayIndex, - changeQuery, changeStatus, + changeUserFilter, checkinDay, claims, claimsError, @@ -145,7 +146,6 @@ export function useSevenDayCheckInPage() { form, openConfigDrawer, page, - query, reloadClaims, reloadConfig, resourceGroups, @@ -153,6 +153,7 @@ export function useSevenDayCheckInPage() { setPage, status, submitConfig, + userFilter, }; } diff --git a/src/features/seven-day-checkin/pages/SevenDayCheckInPage.jsx b/src/features/seven-day-checkin/pages/SevenDayCheckInPage.jsx index d5584a2..d961c1f 100644 --- a/src/features/seven-day-checkin/pages/SevenDayCheckInPage.jsx +++ b/src/features/seven-day-checkin/pages/SevenDayCheckInPage.jsx @@ -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 } from "@/shared/ui/tableFilters.js"; +import { createOptionsColumnFilter, createTextColumnFilter, createUserIdentityColumnFilter } 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,10 +98,9 @@ export function SevenDayCheckInPage() { if (column.key === "user") { return { ...column, - filter: createTextColumnFilter({ - placeholder: "搜索用户 ID、短号、名称", - value: page.query, - onChange: page.changeQuery, + filter: createUserIdentityColumnFilter({ + value: page.userFilter, + onChange: page.changeUserFilter, }), }; } diff --git a/src/shared/ui/DataTable.jsx b/src/shared/ui/DataTable.jsx index 457eaba..969c54b 100644 --- a/src/shared/ui/DataTable.jsx +++ b/src/shared/ui/DataTable.jsx @@ -86,10 +86,14 @@ 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, filterQuery), [activeFilter, filterQuery]); + const filterOptions = useMemo( + () => filterVisibleOptions(activeFilter, typeof filterQuery === "string" ? filterQuery : ""), + [activeFilter, filterQuery] + ); useEffect(() => { const scrollNode = scrollRef.current; @@ -217,7 +221,7 @@ export function DataTable({ } event.preventDefault(); setFilterMenu({ anchorEl: event.currentTarget, columnKey: column.key }); - setFilterQuery(column.filter.type === "text" ? String(column.filter.value ?? "") : ""); + setFilterQuery(initialFilterDraft(column.filter)); }; const closeColumnFilter = () => { @@ -236,6 +240,12 @@ export function DataTable({ closeColumnFilter(); }; + const submitTextFieldsFilter = (event) => { + event.preventDefault(); + activeFilter?.onChange?.(normalizeTextFieldsDraft(activeFilter, filterQuery)); + closeColumnFilter(); + }; + const clearTextFilter = () => { if (activeFilter?.onReset) { activeFilter.onReset(); @@ -245,6 +255,15 @@ export function DataTable({ closeColumnFilter(); }; + const clearTextFieldsFilter = () => { + if (activeFilter?.onReset) { + activeFilter.onReset(); + } else { + activeFilter?.onChange?.(emptyTextFieldsDraft(activeFilter)); + } + closeColumnFilter(); + }; + const resetColumnFilters = () => { closeColumnFilter(); filterColumns.forEach((column) => { @@ -391,6 +410,35 @@ export function DataTable({
+ ) : activeFilterIsTextFields ? ( +
+ {normalizeTextFields(activeFilter).map((field, index) => ( + + setFilterQuery((current) => ({ + ...(current && typeof current === "object" ? current : {}), + [field.key]: event.target.value + })) + } + /> + ))} +
+ + +
+ ) : (
{ + 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) !== ""; } @@ -597,6 +654,18 @@ 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 ""; @@ -609,6 +678,37 @@ 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 []; diff --git a/src/shared/ui/tableFilters.js b/src/shared/ui/tableFilters.js index 6e5de79..3c4abc5 100644 --- a/src/shared/ui/tableFilters.js +++ b/src/shared/ui/tableFilters.js @@ -11,6 +11,48 @@ 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, diff --git a/src/shared/ui/userIdentityFilter.js b/src/shared/ui/userIdentityFilter.js new file mode 100644 index 0000000..525aeb8 --- /dev/null +++ b/src/shared/ui/userIdentityFilter.js @@ -0,0 +1,30 @@ +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 + }; +} diff --git a/src/styles/shared-ui.css b/src/styles/shared-ui.css index 705b253..0b2af0f 100644 --- a/src/styles/shared-ui.css +++ b/src/styles/shared-ui.css @@ -1859,7 +1859,7 @@ } .admin-table-filter-popover .MuiPopover-paper { - width: 240px; + width: 280px; overflow: hidden; border: 1px solid var(--border); border-radius: var(--radius-card);