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) =>
,
},
],
- [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({