From e915a16cb318afb13b08c1bc263a25c889f766ab Mon Sep 17 00:00:00 2001 From: zhx Date: Fri, 17 Jul 2026 19:39:15 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=BA=A4=E4=BA=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ops-center/src/OpsCenterApp.jsx | 2 +- ops-center/src/OpsCenterApp.test.jsx | 42 +++++++++++++++++++ .../src/components/UserProfilesView.jsx | 13 +++++- 3 files changed, 55 insertions(+), 2 deletions(-) diff --git a/ops-center/src/OpsCenterApp.jsx b/ops-center/src/OpsCenterApp.jsx index 3d21fa8..fc6dc77 100644 --- a/ops-center/src/OpsCenterApp.jsx +++ b/ops-center/src/OpsCenterApp.jsx @@ -520,8 +520,8 @@ export function OpsCenterApp({ can = denyPermission }) { ) : null} diff --git a/ops-center/src/OpsCenterApp.test.jsx b/ops-center/src/OpsCenterApp.test.jsx index 1592755..64182a5 100644 --- a/ops-center/src/OpsCenterApp.test.jsx +++ b/ops-center/src/OpsCenterApp.test.jsx @@ -9,6 +9,8 @@ import { fetchLuckyGiftDraws, fetchLuckyGiftConfigVersions, fetchLuckyGiftExperiments, + fetchLuckyGiftUserProfile, + fetchLuckyGiftUserProfiles, fetchOpsDashboard, rollbackLuckyGiftConfig, saveLuckyGiftConfig, @@ -25,6 +27,8 @@ vi.mock("./api.js", () => ({ fetchLuckyGiftDraws: vi.fn(), fetchLuckyGiftConfigVersions: vi.fn(), fetchLuckyGiftExperiments: vi.fn(), + fetchLuckyGiftUserProfile: vi.fn(), + fetchLuckyGiftUserProfiles: vi.fn(), fetchOpsDashboard: vi.fn(), luckyGiftPoolKey: (item = {}) => `${item.app_code || item.appCode}:${item.pool_id || item.poolId}:${item.strategy_version || item.strategyVersion || "fixed_v2"}`, @@ -83,6 +87,16 @@ beforeEach(() => { pageSize: 20, total: 1, }); + fetchLuckyGiftUserProfile.mockResolvedValue({}); + fetchLuckyGiftUserProfiles.mockResolvedValue({ + hasMore: false, + items: [], + nextCursor: "", + page: 1, + pageSize: 20, + snapshotAtMs: 1767000000000, + total: 0, + }); fetchOpsDashboard.mockResolvedValue({ experiments: [mockRunningExperiment()], giftHostDiamondRatiosByApp: { @@ -297,6 +311,34 @@ test("opens the requested config view from the legacy admin document redirect", expect(screen.getAllByText("lucky").length).toBeGreaterThan(0); }); +test("refreshes user profiles with the active filters instead of resetting the view", async () => { + renderApp(); + await screen.findByText("运营应用"); + fireEvent.click(screen.getByRole("button", { name: /用户画像/ })); + + await waitFor(() => expect(fetchLuckyGiftUserProfiles).toHaveBeenCalledTimes(1)); + fireEvent.mouseDown(screen.getByLabelText("用户来源")); + fireEvent.click(await screen.findByRole("option", { name: "内部用户" })); + const keywordInput = screen.getByPlaceholderText("短号 / 长号 / 靓号"); + fireEvent.change(keywordInput, { target: { value: "163001" } }); + fireEvent.click(screen.getByRole("button", { name: "查询" })); + + await waitFor(() => + expect(fetchLuckyGiftUserProfiles).toHaveBeenLastCalledWith( + expect.objectContaining({ identityType: "internal", keyword: "163001" }), + ), + ); + const requestCountBeforeRefresh = fetchLuckyGiftUserProfiles.mock.calls.length; + fireEvent.click(screen.getByRole("button", { name: "刷新" })); + + await waitFor(() => expect(fetchLuckyGiftUserProfiles).toHaveBeenCalledTimes(requestCountBeforeRefresh + 1)); + expect(fetchLuckyGiftUserProfiles).toHaveBeenLastCalledWith( + expect.objectContaining({ identityType: "internal", keyword: "163001", page: 1 }), + ); + expect(screen.getByLabelText("用户来源")).toHaveTextContent("内部用户"); + expect(keywordInput).toHaveValue("163001"); +}); + test("lists every pool config including published non-default pools", async () => { renderApp(); await screen.findByText("运营应用"); diff --git a/ops-center/src/components/UserProfilesView.jsx b/ops-center/src/components/UserProfilesView.jsx index f5c94c1..3b5dd66 100644 --- a/ops-center/src/components/UserProfilesView.jsx +++ b/ops-center/src/components/UserProfilesView.jsx @@ -44,7 +44,7 @@ const SORT_DIRECTION_OPTIONS = [ ["asc", "从低到高 / 从旧到新"], ]; -export function UserProfilesView({ apps, defaultAppCode, poolIDsByApp }) { +export function UserProfilesView({ apps, defaultAppCode, poolIDsByApp, refreshKey = 0 }) { const [appCode, setAppCode] = useState(defaultAppCode); const [poolId, setPoolId] = useState(""); const [identityType, setIdentityType] = useState("all"); @@ -58,6 +58,7 @@ export function UserProfilesView({ apps, defaultAppCode, poolIDsByApp }) { const [reloadKey, setReloadKey] = useState(0); const [expandedKey, setExpandedKey] = useState(""); const [details, setDetails] = useState({}); + const handledRefreshKey = useRef(refreshKey); // 页码只负责兼容通用 DataTable 的滚动触发;真正翻页使用 owner 返回的不可解释 cursor, // 因此并发开奖插入新画像时不会因为 OFFSET 位移而把同一用户追加两次。 const cursorsByPage = useRef({ 1: "" }); @@ -167,6 +168,16 @@ export function UserProfilesView({ apps, defaultAppCode, poolIDsByApp }) { setReloadKey((current) => current + 1); }, []); + useEffect(() => { + if (handledRefreshKey.current === refreshKey) { + return; + } + handledRefreshKey.current = refreshKey; + // 顶部刷新只重建游标和当前结果,筛选与已提交关键词继续由本组件状态持有, + // 避免通过 React key 重挂载整页后把运营正在排查的条件恢复成默认值。 + reloadProfiles(); + }, [refreshKey, reloadProfiles]); + const changeAppCode = useCallback( (nextAppCode) => { setAppCode(nextAppCode);