修复靓号
This commit is contained in:
parent
810ee6721c
commit
b0b1a26ae8
@ -1,5 +1,5 @@
|
|||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { fetchFilterOptions, fetchSelfGameStatisticsOverview, fetchStatisticsOverview, getCurrentAppCode, setCurrentAppCode } from "./api.js";
|
import { fetchFilterOptions, fetchSelfGameStatisticsOverview, fetchStatisticsOverview, getCurrentAppCode, getDefaultAppOptions, setCurrentAppCode } from "./api.js";
|
||||||
import { CountryPerformanceTable } from "./components/CountryPerformanceTable.jsx";
|
import { CountryPerformanceTable } from "./components/CountryPerformanceTable.jsx";
|
||||||
import { DatabiHeader } from "./components/DatabiHeader.jsx";
|
import { DatabiHeader } from "./components/DatabiHeader.jsx";
|
||||||
import { FunnelPanel } from "./components/FunnelPanel.jsx";
|
import { FunnelPanel } from "./components/FunnelPanel.jsx";
|
||||||
@ -15,7 +15,7 @@ import { createDashboardModel } from "./data/createDashboardModel.js";
|
|||||||
import { lastDaysRange, rangeEndMs, rangeStartMs, readStoredTimeZone, sameRange, thisMonthRange, TIME_ZONE_OPTIONS, todayRange, writeStoredTimeZone } from "./utils/time.js";
|
import { lastDaysRange, rangeEndMs, rangeStartMs, readStoredTimeZone, sameRange, thisMonthRange, TIME_ZONE_OPTIONS, todayRange, writeStoredTimeZone } from "./utils/time.js";
|
||||||
|
|
||||||
const initialFilterOptions = {
|
const initialFilterOptions = {
|
||||||
appOptions: [],
|
appOptions: getDefaultAppOptions(),
|
||||||
countryOptions: [{ countryCode: "", id: 0, label: "全部国家", regionIds: [], searchText: "全部国家 all", value: 0 }],
|
countryOptions: [{ countryCode: "", id: 0, label: "全部国家", regionIds: [], searchText: "全部国家 all", value: 0 }],
|
||||||
regionOptions: [{ countryCodes: [], id: "all", label: "全部区域", value: "all" }]
|
regionOptions: [{ countryCodes: [], id: "all", label: "全部区域", value: "all" }]
|
||||||
};
|
};
|
||||||
@ -333,10 +333,17 @@ function BigscreenOverview({ loading, model, onMetricTrend, onOpenLuckyGiftPools
|
|||||||
|
|
||||||
function resolveSelectedAppCode(options, current) {
|
function resolveSelectedAppCode(options, current) {
|
||||||
const normalized = String(current || "").trim().toLowerCase();
|
const normalized = String(current || "").trim().toLowerCase();
|
||||||
if (options.some((item) => String(item.code || item.value || "").trim().toLowerCase() === normalized)) {
|
if (options.some((item) => optionAppCode(item) === normalized)) {
|
||||||
return normalized;
|
return normalized;
|
||||||
}
|
}
|
||||||
return String(options[0]?.code || options[0]?.value || normalized || "lalu").trim().toLowerCase();
|
if (options.length) {
|
||||||
|
return optionAppCode(options[0]);
|
||||||
|
}
|
||||||
|
return normalized || "lalu";
|
||||||
|
}
|
||||||
|
|
||||||
|
function optionAppCode(option) {
|
||||||
|
return String(option?.code ?? option?.value ?? "").trim().toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveRangePresetValue(range, timeZone) {
|
function resolveRangePresetValue(range, timeZone) {
|
||||||
|
|||||||
@ -8,6 +8,12 @@ vi.mock("./api.js", () => ({
|
|||||||
fetchSelfGameStatisticsOverview: vi.fn(),
|
fetchSelfGameStatisticsOverview: vi.fn(),
|
||||||
fetchStatisticsOverview: vi.fn(),
|
fetchStatisticsOverview: vi.fn(),
|
||||||
getCurrentAppCode: vi.fn(() => "lalu"),
|
getCurrentAppCode: vi.fn(() => "lalu"),
|
||||||
|
getDefaultAppOptions: vi.fn(() => [
|
||||||
|
{ code: "", label: "全部", value: "" },
|
||||||
|
{ code: "lalu", label: "Lalu", value: "lalu" },
|
||||||
|
{ code: "aslan", label: "Aslan", value: "aslan" },
|
||||||
|
{ code: "yumi", label: "Yumi", value: "yumi" }
|
||||||
|
]),
|
||||||
setCurrentAppCode: vi.fn((value) => String(value || "lalu").toLowerCase())
|
setCurrentAppCode: vi.fn((value) => String(value || "lalu").toLowerCase())
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@ -5,19 +5,31 @@ const API_BASE_URL = import.meta.env?.VITE_API_BASE_URL || "/api";
|
|||||||
const TOKEN_KEY = "hyapp-admin.access-token";
|
const TOKEN_KEY = "hyapp-admin.access-token";
|
||||||
const APP_CODE_KEY = "hyapp-admin.app-code";
|
const APP_CODE_KEY = "hyapp-admin.app-code";
|
||||||
const APP_CODE_HEADER = "X-App-Code";
|
const APP_CODE_HEADER = "X-App-Code";
|
||||||
|
const DEFAULT_APP_CODE = "lalu";
|
||||||
|
const ALL_APP_OPTION = { code: "", label: "全部", value: "" };
|
||||||
|
const DEFAULT_APP_OPTION = { code: DEFAULT_APP_CODE, label: "Lalu", value: DEFAULT_APP_CODE };
|
||||||
|
const FIXED_APP_OPTIONS = [
|
||||||
|
{ code: "aslan", label: "Aslan", value: "aslan" },
|
||||||
|
{ code: "yumi", label: "Yumi", value: "yumi" }
|
||||||
|
];
|
||||||
|
|
||||||
|
export function getDefaultAppOptions() {
|
||||||
|
return mergeAppOptions([ALL_APP_OPTION, DEFAULT_APP_OPTION, ...FIXED_APP_OPTIONS]);
|
||||||
|
}
|
||||||
|
|
||||||
export function getCurrentAppCode() {
|
export function getCurrentAppCode() {
|
||||||
return normalizeAppCode(window.localStorage.getItem(APP_CODE_KEY) || "lalu");
|
const stored = window.localStorage.getItem(APP_CODE_KEY);
|
||||||
|
return stored === null ? DEFAULT_APP_CODE : normalizeAppCode(stored);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setCurrentAppCode(value) {
|
export function setCurrentAppCode(value) {
|
||||||
const appCode = normalizeAppCode(value) || "lalu";
|
const appCode = normalizeAppCode(value);
|
||||||
window.localStorage.setItem(APP_CODE_KEY, appCode);
|
window.localStorage.setItem(APP_CODE_KEY, appCode);
|
||||||
return appCode;
|
return appCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchStatisticsOverview({ appCode, countryId, endMs, regionId, seriesEndMs, seriesStartMs, startMs, statTz }) {
|
export async function fetchStatisticsOverview({ appCode, countryId, endMs, regionId, seriesEndMs, seriesStartMs, startMs, statTz }) {
|
||||||
const query = { app_code: normalizeAppCode(appCode) || "lalu" };
|
const query = appScopedQuery(appCode);
|
||||||
if (statTz) {
|
if (statTz) {
|
||||||
query.stat_tz = statTz;
|
query.stat_tz = statTz;
|
||||||
}
|
}
|
||||||
@ -58,7 +70,7 @@ export async function fetchPlatformGrantRecords({ appCode, countryId, endMs, pag
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchSelfGameStatisticsOverview({ appCode, countryId, endMs, gameId, regionId, startMs, statTz }) {
|
export async function fetchSelfGameStatisticsOverview({ appCode, countryId, endMs, gameId, regionId, startMs, statTz }) {
|
||||||
const query = { app_code: normalizeAppCode(appCode) || "lalu" };
|
const query = appScopedQuery(appCode);
|
||||||
if (statTz) {
|
if (statTz) {
|
||||||
query.stat_tz = statTz;
|
query.stat_tz = statTz;
|
||||||
}
|
}
|
||||||
@ -82,7 +94,7 @@ export async function fetchSelfGameStatisticsOverview({ appCode, countryId, endM
|
|||||||
}
|
}
|
||||||
|
|
||||||
function platformGrantQuery({ appCode, countryId, endMs, page, pageSize, regionId, source, startMs, statDay, statTz, userId }) {
|
function platformGrantQuery({ appCode, countryId, endMs, page, pageSize, regionId, source, startMs, statDay, statTz, userId }) {
|
||||||
const query = { app_code: normalizeAppCode(appCode) || "lalu" };
|
const query = appScopedQuery(appCode);
|
||||||
if (statTz) {
|
if (statTz) {
|
||||||
query.stat_tz = statTz;
|
query.stat_tz = statTz;
|
||||||
}
|
}
|
||||||
@ -194,6 +206,12 @@ function normalizeAppCode(value) {
|
|||||||
return String(value || "").trim().toLowerCase();
|
return String(value || "").trim().toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function appScopedQuery(appCode) {
|
||||||
|
const normalized = normalizeAppCode(appCode);
|
||||||
|
// “全部”使用空 appCode,让请求层省略 X-App-Code 和 app_code,避免把 all 当成真实 App 传给后端。
|
||||||
|
return normalized ? { app_code: normalized } : {};
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeFilterOptions({ apps, countries, regions }) {
|
function normalizeFilterOptions({ apps, countries, regions }) {
|
||||||
const regionItems = normalizeList(regions);
|
const regionItems = normalizeList(regions);
|
||||||
const regionOptions = [
|
const regionOptions = [
|
||||||
@ -218,7 +236,7 @@ function normalizeFilterOptions({ apps, countries, regions }) {
|
|||||||
.map((country) => normalizeCountryOption(country, countryRegionIds))
|
.map((country) => normalizeCountryOption(country, countryRegionIds))
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
];
|
];
|
||||||
const appOptions = normalizeList(apps)
|
const apiAppOptions = normalizeList(apps)
|
||||||
.map((app) => {
|
.map((app) => {
|
||||||
const code = normalizeAppCode(app.appCode ?? app.app_code ?? app.code);
|
const code = normalizeAppCode(app.appCode ?? app.app_code ?? app.code);
|
||||||
return {
|
return {
|
||||||
@ -228,6 +246,7 @@ function normalizeFilterOptions({ apps, countries, regions }) {
|
|||||||
};
|
};
|
||||||
})
|
})
|
||||||
.filter((app) => app.code);
|
.filter((app) => app.code);
|
||||||
|
const appOptions = mergeAppOptions([ALL_APP_OPTION, ...apiAppOptions, DEFAULT_APP_OPTION, ...FIXED_APP_OPTIONS]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
appOptions,
|
appOptions,
|
||||||
@ -236,6 +255,18 @@ function normalizeFilterOptions({ apps, countries, regions }) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function mergeAppOptions(options) {
|
||||||
|
const seen = new Set();
|
||||||
|
return options.filter((option) => {
|
||||||
|
const code = normalizeAppCode(option.code ?? option.value);
|
||||||
|
if (seen.has(code)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
seen.add(code);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeList(value) {
|
function normalizeList(value) {
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
return value;
|
return value;
|
||||||
|
|||||||
84
databi/src/api.test.js
Normal file
84
databi/src/api.test.js
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import { afterEach, expect, test, vi } from "vitest";
|
||||||
|
import { fetchFilterOptions, fetchStatisticsOverview, getCurrentAppCode, getDefaultAppOptions, setCurrentAppCode } from "./api.js";
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
window.localStorage.clear();
|
||||||
|
vi.unstubAllGlobals();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("stores the last selected databi app including all apps", () => {
|
||||||
|
expect(getCurrentAppCode()).toBe("lalu");
|
||||||
|
|
||||||
|
expect(setCurrentAppCode("YUMI")).toBe("yumi");
|
||||||
|
expect(window.localStorage.getItem("hyapp-admin.app-code")).toBe("yumi");
|
||||||
|
expect(getCurrentAppCode()).toBe("yumi");
|
||||||
|
|
||||||
|
expect(setCurrentAppCode("")).toBe("");
|
||||||
|
expect(window.localStorage.getItem("hyapp-admin.app-code")).toBe("");
|
||||||
|
expect(getCurrentAppCode()).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("adds fixed databi app options without losing API apps", async () => {
|
||||||
|
vi.stubGlobal(
|
||||||
|
"fetch",
|
||||||
|
vi.fn(async (input) => {
|
||||||
|
const url = String(input);
|
||||||
|
if (url.includes("/v1/admin/apps")) {
|
||||||
|
return jsonResponse({ items: [{ appCode: "huwaa", appName: "Huwaa" }, { app_code: "lalu", name: "Lalu" }] });
|
||||||
|
}
|
||||||
|
if (url.includes("/v1/admin/regions")) {
|
||||||
|
return jsonResponse({ items: [] });
|
||||||
|
}
|
||||||
|
if (url.includes("/v1/admin/countries")) {
|
||||||
|
return jsonResponse({ items: [] });
|
||||||
|
}
|
||||||
|
return jsonResponse(null, 404);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const options = await fetchFilterOptions({ appCode: "lalu" });
|
||||||
|
|
||||||
|
expect(options.appOptions).toEqual([
|
||||||
|
{ code: "", label: "全部", value: "" },
|
||||||
|
{ code: "huwaa", label: "Huwaa", value: "huwaa" },
|
||||||
|
{ code: "lalu", label: "Lalu", value: "lalu" },
|
||||||
|
{ code: "aslan", label: "Aslan", value: "aslan" },
|
||||||
|
{ code: "yumi", label: "Yumi", value: "yumi" }
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("provides static databi app options before filter API loads", () => {
|
||||||
|
expect(getDefaultAppOptions()).toEqual([
|
||||||
|
{ code: "", label: "全部", value: "" },
|
||||||
|
{ code: "lalu", label: "Lalu", value: "lalu" },
|
||||||
|
{ code: "aslan", label: "Aslan", value: "aslan" },
|
||||||
|
{ code: "yumi", label: "Yumi", value: "yumi" }
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("omits app scope headers and query params for all databi apps", async () => {
|
||||||
|
vi.stubGlobal("fetch", vi.fn(async () => jsonResponse({ updated_at_ms: 1 })));
|
||||||
|
|
||||||
|
await fetchStatisticsOverview({
|
||||||
|
appCode: "",
|
||||||
|
countryId: 0,
|
||||||
|
endMs: 2,
|
||||||
|
regionId: "all",
|
||||||
|
seriesEndMs: 2,
|
||||||
|
seriesStartMs: 1,
|
||||||
|
startMs: 1,
|
||||||
|
statTz: "Asia/Shanghai"
|
||||||
|
});
|
||||||
|
|
||||||
|
const [url, init] = fetch.mock.calls[0];
|
||||||
|
const requestURL = new URL(String(url));
|
||||||
|
expect(requestURL.searchParams.has("app_code")).toBe(false);
|
||||||
|
expect(init.headers["X-App-Code"]).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
function jsonResponse(data, status = 200) {
|
||||||
|
return new Response(JSON.stringify({ code: status === 200 ? 0 : status, data, message: status === 200 ? "" : "error" }), {
|
||||||
|
headers: { "content-type": "application/json" },
|
||||||
|
status
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -34,3 +34,34 @@ test("switches to end time after selecting a start date", async () => {
|
|||||||
|
|
||||||
expect(within(dialog).getByRole("tab", { name: /结束时间/ })).toHaveAttribute("aria-selected", "true");
|
expect(within(dialog).getByRole("tab", { name: /结束时间/ })).toHaveAttribute("aria-selected", "true");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("selects all apps from app filter", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
const onAppChange = vi.fn();
|
||||||
|
|
||||||
|
render(
|
||||||
|
<DatabiHeader
|
||||||
|
appCode="lalu"
|
||||||
|
appOptions={[
|
||||||
|
{ code: "", label: "全部", value: "" },
|
||||||
|
{ code: "lalu", label: "Lalu", value: "lalu" },
|
||||||
|
{ code: "aslan", label: "Aslan", value: "aslan" },
|
||||||
|
{ code: "yumi", label: "Yumi", value: "yumi" }
|
||||||
|
]}
|
||||||
|
countryId={0}
|
||||||
|
onAppChange={onAppChange}
|
||||||
|
onCountryChange={noop}
|
||||||
|
onRangeChange={noop}
|
||||||
|
onRegionChange={noop}
|
||||||
|
onTimeZoneChange={noop}
|
||||||
|
range={{ end: "2026-06-08", endTime: "23:59:59", start: "2026-06-08", startTime: "00:00:00" }}
|
||||||
|
regionId="all"
|
||||||
|
timeZone="UTC"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
await user.click(screen.getByRole("button", { name: /App\s*Lalu/ }));
|
||||||
|
await user.click(within(screen.getByRole("listbox")).getByRole("option", { name: "全部" }));
|
||||||
|
|
||||||
|
expect(onAppChange).toHaveBeenCalledWith("");
|
||||||
|
});
|
||||||
|
|||||||
@ -156,6 +156,42 @@ test("recyclePrettyDisplayID uses generated recycle endpoint", async () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("listPrettyDisplayIDs normalizes zero assigned user as empty", async () => {
|
||||||
|
vi.stubGlobal(
|
||||||
|
"fetch",
|
||||||
|
vi.fn(
|
||||||
|
async () =>
|
||||||
|
new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
code: 0,
|
||||||
|
data: {
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
assigned_user_id: "0",
|
||||||
|
display_user_id: "00002",
|
||||||
|
pretty_id: "pretty-0",
|
||||||
|
source: "pool",
|
||||||
|
status: "available",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
page: 1,
|
||||||
|
page_size: 20,
|
||||||
|
total: 1,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await listPrettyDisplayIDs({ page: 1, page_size: 20 });
|
||||||
|
|
||||||
|
expect(result.items[0]).toMatchObject({
|
||||||
|
assignedUser: undefined,
|
||||||
|
assignedUserId: "",
|
||||||
|
displayUserId: "00002",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test("listAppUsers sends country region and time filters", async () => {
|
test("listAppUsers sends country region and time filters", async () => {
|
||||||
vi.stubGlobal(
|
vi.stubGlobal(
|
||||||
"fetch",
|
"fetch",
|
||||||
|
|||||||
@ -630,7 +630,7 @@ function normalizeOptionalAppUserBrief(item?: RawAppUserBrief | null) {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
const user = normalizeAppUserBrief(item);
|
const user = normalizeAppUserBrief(item);
|
||||||
return user.userId || user.displayUserId || user.username ? user : undefined;
|
return nonZeroStringValue(user.userId) || nonZeroStringValue(user.displayUserId) || user.username ? user : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeAppUserBanRecord(item: RawAppUserBanRecord = {}): AppUserBanRecordDto {
|
function normalizeAppUserBanRecord(item: RawAppUserBanRecord = {}): AppUserBanRecordDto {
|
||||||
@ -708,7 +708,7 @@ function normalizePrettyDisplayID(item: RawPrettyDisplayID): PrettyDisplayIDDto
|
|||||||
assignedAtMs: item.assignedAtMs ?? item.assigned_at_ms ?? 0,
|
assignedAtMs: item.assignedAtMs ?? item.assigned_at_ms ?? 0,
|
||||||
assignedLeaseId: item.assignedLeaseId ?? item.assigned_lease_id ?? "",
|
assignedLeaseId: item.assignedLeaseId ?? item.assigned_lease_id ?? "",
|
||||||
assignedUser: normalizeOptionalAppUserBrief(item.assignedUser ?? item.assigned_user),
|
assignedUser: normalizeOptionalAppUserBrief(item.assignedUser ?? item.assigned_user),
|
||||||
assignedUserId: item.assignedUserId ?? item.assigned_user_id ?? "",
|
assignedUserId: optionalEntityID(item.assignedUserId ?? item.assigned_user_id),
|
||||||
createdAtMs: item.createdAtMs ?? item.created_at_ms ?? 0,
|
createdAtMs: item.createdAtMs ?? item.created_at_ms ?? 0,
|
||||||
createdByAdminId,
|
createdByAdminId,
|
||||||
displayUserId: item.displayUserId ?? item.display_user_id ?? "",
|
displayUserId: item.displayUserId ?? item.display_user_id ?? "",
|
||||||
@ -764,3 +764,12 @@ function normalizeAdminGrantPrettyDisplayIDResult(
|
|||||||
prettyId: item.prettyId ?? item.pretty_id ?? "",
|
prettyId: item.prettyId ?? item.pretty_id ?? "",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function optionalEntityID(value: unknown): string {
|
||||||
|
return nonZeroStringValue(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function nonZeroStringValue(value: unknown): string {
|
||||||
|
const normalized = stringValue(value);
|
||||||
|
return normalized === "0" ? "" : normalized;
|
||||||
|
}
|
||||||
|
|||||||
@ -397,12 +397,12 @@ function idColumns(page) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function AssignedUserCell({ item }) {
|
function AssignedUserCell({ item }) {
|
||||||
const assignedUserId = formatEntityID(item.assignedUserId);
|
const user = assignedUser(item);
|
||||||
if (!item.assignedUserId) {
|
if (!user) {
|
||||||
return <span className={styles.meta}>-</span>;
|
return <span className={styles.meta}>-</span>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <AdminUserIdentity openInAppUserDetail rows={[assignedUserId]} user={assignedUser(item)} />;
|
return <AdminUserIdentity openInAppUserDetail user={user} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function OperatorCell({ item }) {
|
function OperatorCell({ item }) {
|
||||||
@ -433,12 +433,16 @@ function PrettyIDTimeCell({ item }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function assignedUser(item) {
|
function assignedUser(item) {
|
||||||
|
const user = item.assignedUser || null;
|
||||||
|
if (!user) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
...(item.assignedUser || {}),
|
...user,
|
||||||
displayUserId: item.assignedUser?.displayUserId || item.assignedUser?.defaultDisplayUserId || item.assignedUserId,
|
displayUserId: user.displayUserId || user.defaultDisplayUserId,
|
||||||
prettyDisplayUserId: item.displayUserId,
|
prettyDisplayUserId: item.displayUserId,
|
||||||
prettyId: item.prettyId,
|
prettyId: item.prettyId,
|
||||||
userId: item.assignedUser?.userId || item.assignedUserId,
|
userId: user.userId,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -713,13 +717,6 @@ function ruleLabel(ruleType) {
|
|||||||
return prettyRuleOptions.find(([value]) => value === ruleType)?.[1] || ruleType || "-";
|
return prettyRuleOptions.find(([value]) => value === ruleType)?.[1] || ruleType || "-";
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatEntityID(value) {
|
|
||||||
if (value === 0 || value === "0" || value === "" || value === undefined || value === null) {
|
|
||||||
return "-";
|
|
||||||
}
|
|
||||||
return String(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
function canChangePrettyStatus(item) {
|
function canChangePrettyStatus(item) {
|
||||||
const assigned = item.assignedUserId;
|
const assigned = item.assignedUserId;
|
||||||
const unassigned =
|
const unassigned =
|
||||||
|
|||||||
@ -19,8 +19,10 @@ test("pretty id page uses tabs and hides internal pretty record ids", () => {
|
|||||||
expect(screen.getByRole("tab", { name: "靓号列表 1" })).toHaveAttribute("aria-selected", "true");
|
expect(screen.getByRole("tab", { name: "靓号列表 1" })).toHaveAttribute("aria-selected", "true");
|
||||||
expect(screen.getByRole("tab", { name: "靓号池 1" })).toBeInTheDocument();
|
expect(screen.getByRole("tab", { name: "靓号池 1" })).toBeInTheDocument();
|
||||||
expect(screen.getAllByText("777777").length).toBeGreaterThan(0);
|
expect(screen.getAllByText("777777").length).toBeGreaterThan(0);
|
||||||
|
expect(screen.getByText("10001")).toBeInTheDocument();
|
||||||
expect(screen.queryByText("pretty_1782492525506_0b70bb6c6dbc63cf")).not.toBeInTheDocument();
|
expect(screen.queryByText("pretty_1782492525506_0b70bb6c6dbc63cf")).not.toBeInTheDocument();
|
||||||
expect(screen.getByText("占用人 A")).toBeInTheDocument();
|
expect(screen.getByText("占用人 A")).toBeInTheDocument();
|
||||||
|
expect(document.querySelector('img[src="https://cdn.example/owner-a.png"]')).toBeInTheDocument();
|
||||||
expect(screen.getByText("Admin ID 8")).toBeInTheDocument();
|
expect(screen.getByText("Admin ID 8")).toBeInTheDocument();
|
||||||
expect(screen.getByText("释放 2026-06-27 01:00:00")).toBeInTheDocument();
|
expect(screen.getByText("释放 2026-06-27 01:00:00")).toBeInTheDocument();
|
||||||
expect(screen.getAllByLabelText("回收").length).toBeGreaterThan(0);
|
expect(screen.getAllByLabelText("回收").length).toBeGreaterThan(0);
|
||||||
@ -143,7 +145,7 @@ function prettyIdFixture() {
|
|||||||
return {
|
return {
|
||||||
assignedAtMs: 1782499200000,
|
assignedAtMs: 1782499200000,
|
||||||
assignedUser: {
|
assignedUser: {
|
||||||
avatar: "",
|
avatar: "https://cdn.example/owner-a.png",
|
||||||
defaultDisplayUserId: "10001",
|
defaultDisplayUserId: "10001",
|
||||||
displayUserId: "777777",
|
displayUserId: "777777",
|
||||||
userId: "9001",
|
userId: "9001",
|
||||||
|
|||||||
@ -28,6 +28,12 @@ export function AdminUserIdentity({
|
|||||||
user,
|
user,
|
||||||
userId,
|
userId,
|
||||||
});
|
});
|
||||||
|
const visibleRows = Array.isArray(rows)
|
||||||
|
? resolveVisibleRows(rows, normalized)
|
||||||
|
: [normalized.shortId].filter(Boolean);
|
||||||
|
if (!normalized.hasIdentity) {
|
||||||
|
return <span className={[styles.empty, className].filter(Boolean).join(" ")}>-</span>;
|
||||||
|
}
|
||||||
const shouldOpenAppUserDetail = openInAppUserDetail && normalized.userId && openAppUserDetail;
|
const shouldOpenAppUserDetail = openInAppUserDetail && normalized.userId && openAppUserDetail;
|
||||||
const Root = onClick || shouldOpenAppUserDetail ? "button" : "div";
|
const Root = onClick || shouldOpenAppUserDetail ? "button" : "div";
|
||||||
const rootProps = resolveRootProps({
|
const rootProps = resolveRootProps({
|
||||||
@ -37,9 +43,6 @@ export function AdminUserIdentity({
|
|||||||
shouldOpenAppUserDetail,
|
shouldOpenAppUserDetail,
|
||||||
user: normalized.detailUser,
|
user: normalized.detailUser,
|
||||||
});
|
});
|
||||||
const visibleRows = Array.isArray(rows)
|
|
||||||
? resolveVisibleRows(rows, normalized)
|
|
||||||
: [normalized.shortId].filter(Boolean);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Root
|
<Root
|
||||||
@ -120,8 +123,8 @@ function normalizeUserIdentity({
|
|||||||
}) {
|
}) {
|
||||||
const source = user || {};
|
const source = user || {};
|
||||||
// 后台各模块的数据来源不统一:新接口是 camelCase,部分老接口仍透传 snake_case。
|
// 后台各模块的数据来源不统一:新接口是 camelCase,部分老接口仍透传 snake_case。
|
||||||
// 这里在公共入口一次性归一化,后续页面只关心“能否展示当前有效靓号”和“点击能否定位到用户”。
|
// 这里在公共入口一次性归一化,0 是后台未绑定用户的哨兵值,不能再被当成真实用户渲染和点击。
|
||||||
const resolvedUserId = firstNonEmpty(
|
const resolvedUserId = firstNonEmptyIdentityValue(
|
||||||
userId,
|
userId,
|
||||||
source.userId,
|
source.userId,
|
||||||
source.user_id,
|
source.user_id,
|
||||||
@ -130,12 +133,12 @@ function normalizeUserIdentity({
|
|||||||
source.targetUserId,
|
source.targetUserId,
|
||||||
source.target_user_id,
|
source.target_user_id,
|
||||||
);
|
);
|
||||||
const resolvedDefaultDisplayUserId = firstNonEmpty(
|
const resolvedDefaultDisplayUserId = firstNonEmptyIdentityValue(
|
||||||
defaultDisplayUserId,
|
defaultDisplayUserId,
|
||||||
source.defaultDisplayUserId,
|
source.defaultDisplayUserId,
|
||||||
source.default_display_user_id,
|
source.default_display_user_id,
|
||||||
);
|
);
|
||||||
const resolvedDisplayUserId = firstNonEmpty(
|
const resolvedDisplayUserId = firstNonEmptyIdentityValue(
|
||||||
displayUserId,
|
displayUserId,
|
||||||
source.displayUserId,
|
source.displayUserId,
|
||||||
source.display_user_id,
|
source.display_user_id,
|
||||||
@ -153,31 +156,39 @@ function normalizeUserIdentity({
|
|||||||
const resolvedShortId = hasPretty
|
const resolvedShortId = hasPretty
|
||||||
? firstNonEmpty(resolvedDefaultDisplayUserId, resolvedUserId, resolvedDisplayUserId)
|
? firstNonEmpty(resolvedDefaultDisplayUserId, resolvedUserId, resolvedDisplayUserId)
|
||||||
: firstNonEmpty(resolvedDisplayUserId, resolvedDefaultDisplayUserId, resolvedUserId);
|
: firstNonEmpty(resolvedDisplayUserId, resolvedDefaultDisplayUserId, resolvedUserId);
|
||||||
const displayName = firstNonEmpty(
|
const resolvedAvatar = firstNonEmpty(avatar, source.avatar);
|
||||||
|
const explicitName = firstNonEmpty(
|
||||||
name,
|
name,
|
||||||
source.username,
|
source.username,
|
||||||
source.name,
|
source.name,
|
||||||
source.account,
|
source.account,
|
||||||
|
);
|
||||||
|
const displayName = firstNonEmpty(
|
||||||
|
explicitName,
|
||||||
resolvedDisplayUserId,
|
resolvedDisplayUserId,
|
||||||
resolvedUserId,
|
resolvedUserId,
|
||||||
"-",
|
"-",
|
||||||
);
|
);
|
||||||
|
const hasIdentity = Boolean(
|
||||||
|
resolvedAvatar || explicitName || resolvedDisplayUserId || resolvedDefaultDisplayUserId || resolvedUserId,
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
avatar: firstNonEmpty(avatar, source.avatar),
|
avatar: resolvedAvatar,
|
||||||
detailUser: {
|
detailUser: {
|
||||||
...source,
|
...source,
|
||||||
avatar: firstNonEmpty(avatar, source.avatar),
|
avatar: resolvedAvatar,
|
||||||
defaultDisplayUserId: resolvedDefaultDisplayUserId,
|
defaultDisplayUserId: resolvedDefaultDisplayUserId,
|
||||||
displayUserId: resolvedDisplayUserId,
|
displayUserId: resolvedDisplayUserId,
|
||||||
prettyDisplayUserId: resolvedPrettyDisplayUserId,
|
prettyDisplayUserId: resolvedPrettyDisplayUserId,
|
||||||
prettyId: resolvedPrettyId,
|
prettyId: resolvedPrettyId,
|
||||||
userId: resolvedUserId,
|
userId: resolvedUserId,
|
||||||
username: firstNonEmpty(name, source.username, source.name, source.account),
|
username: explicitName,
|
||||||
},
|
},
|
||||||
displayUserId: resolvedDisplayUserId,
|
displayUserId: resolvedDisplayUserId,
|
||||||
displayName,
|
displayName,
|
||||||
hasPretty,
|
hasPretty,
|
||||||
|
hasIdentity,
|
||||||
prettyDisplayUserId: resolvedPrettyDisplayUserId,
|
prettyDisplayUserId: resolvedPrettyDisplayUserId,
|
||||||
prettyId: resolvedPrettyId,
|
prettyId: resolvedPrettyId,
|
||||||
shortId: resolvedShortId || "-",
|
shortId: resolvedShortId || "-",
|
||||||
@ -232,3 +243,13 @@ function firstNonEmpty(...values) {
|
|||||||
}
|
}
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function firstNonEmptyIdentityValue(...values) {
|
||||||
|
for (const value of values) {
|
||||||
|
const normalized = String(value ?? "").trim();
|
||||||
|
if (normalized && normalized !== "0") {
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|||||||
@ -8,6 +8,10 @@
|
|||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.empty {
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
.button {
|
.button {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|||||||
@ -44,6 +44,15 @@ test("clickable identity opens detail handler", async () => {
|
|||||||
expect(onClick).toHaveBeenCalledTimes(1);
|
expect(onClick).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("empty identity renders dash without avatar or click target", () => {
|
||||||
|
const onClick = vi.fn();
|
||||||
|
render(<AdminUserIdentity onClick={onClick} openInAppUserDetail user={{ displayUserId: "0", userId: "0" }} />);
|
||||||
|
|
||||||
|
expect(screen.getByText("-")).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText("0")).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByRole("button")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
test("uses original id before pretty id when rows pass current display id", () => {
|
test("uses original id before pretty id when rows pass current display id", () => {
|
||||||
render(
|
render(
|
||||||
<AdminUserIdentity
|
<AdminUserIdentity
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user