2026-06-04 19:29:35 +08:00

258 lines
7.8 KiB
JavaScript

import { API_OPERATIONS, apiEndpointPath } from "../../src/shared/api/generated/endpoints.ts";
import { countryFlag, stripCountryFlagPrefix } from "./data/countryMeta.js";
const API_BASE_URL = import.meta.env?.VITE_API_BASE_URL || "/api";
const TOKEN_KEY = "hyapp-admin.access-token";
const APP_CODE_KEY = "hyapp-admin.app-code";
const APP_CODE_HEADER = "X-App-Code";
export function getCurrentAppCode() {
return normalizeAppCode(window.localStorage.getItem(APP_CODE_KEY) || "lalu");
}
export function setCurrentAppCode(value) {
const appCode = normalizeAppCode(value) || "lalu";
window.localStorage.setItem(APP_CODE_KEY, appCode);
return appCode;
}
export async function fetchStatisticsOverview({ appCode, countryId, endMs, regionId, startMs }) {
const query = { app_code: normalizeAppCode(appCode) || "lalu" };
if (startMs) {
query.start_ms = String(startMs);
}
if (endMs) {
query.end_ms = String(endMs);
}
if (countryId) {
query.country_id = String(countryId);
}
if (regionId && regionId !== "all") {
query.region_id = String(regionId);
}
return fetchDatabiData("/v1/statistics/overview", { appCode, query });
}
export async function fetchCountryStatisticsBreakdown({ appCode, countries = [], endMs, regionId, startMs }) {
return mapWithConcurrency(
countries.filter((country) => Number(country.id) > 0),
8,
async (country) => {
const data = await fetchStatisticsOverview({
appCode,
countryId: country.id,
endMs,
regionId,
startMs
});
return normalizeCountryStatisticsRow(data, country);
}
);
}
export async function fetchFilterOptions({ appCode } = {}) {
const [apps, regions, countries] = await Promise.all([
fetchDatabiData(apiEndpointPath(API_OPERATIONS.listApps), { appCode }),
fetchDatabiData(apiEndpointPath(API_OPERATIONS.listRegions), { appCode, query: { status: "active" } }),
fetchDatabiData(apiEndpointPath(API_OPERATIONS.listCountries), { appCode, query: { enabled: true } })
]);
return normalizeFilterOptions({ apps, countries, regions });
}
async function fetchDatabiData(path, { appCode, query } = {}) {
const response = await fetch(buildURL(path, query), {
credentials: "include",
headers: requestHeaders(appCode)
});
const payload = await readJSON(response);
if (isAuthExpired(response, payload)) {
redirectToLogin(window);
throw new Error(payload.message || "访问凭证已失效");
}
if (!response.ok || payload.code !== 0) {
throw new Error(payload.message || response.statusText || "请求失败");
}
return payload.data;
}
function buildURL(path, query) {
const url = new URL(`${API_BASE_URL}${path}`, window.location.origin);
if (query) {
Object.entries(query).forEach(([key, value]) => {
if (value !== undefined && value !== null && value !== "") {
url.searchParams.set(key, String(value));
}
});
}
return url.toString();
}
function requestHeaders(appCode) {
const headers = {};
const token = window.localStorage.getItem(TOKEN_KEY);
if (token) {
headers.Authorization = `Bearer ${token}`;
}
const normalized = normalizeAppCode(appCode);
if (normalized) {
headers[APP_CODE_HEADER] = normalized;
}
return headers;
}
async function readJSON(response) {
const text = await response.text();
if (!text) {
return {};
}
try {
return JSON.parse(text);
} catch {
return { code: response.ok ? 0 : response.status, message: text };
}
}
export function isAuthExpired(response, payload) {
return response.status === 401 || Number(payload?.code) === 40100;
}
export function redirectToLogin(targetWindow = window) {
targetWindow.localStorage.removeItem(TOKEN_KEY);
if (targetWindow.location.pathname !== "/login") {
targetWindow.location.replace("/login");
}
}
function normalizeAppCode(value) {
return String(value || "").trim().toLowerCase();
}
function normalizeFilterOptions({ apps, countries, regions }) {
const regionItems = normalizeList(regions);
const regionOptions = [
{ countryCodes: [], id: "all", label: "全部区域", value: "all" },
...regionItems
.filter((region) => String(region.regionCode || "").toUpperCase() !== "GLOBAL")
.map((region) => {
const id = String(region.regionId ?? region.id ?? region.regionCode ?? "");
return {
countryCodes: normalizeCodes(region.countries),
id,
label: [region.name, region.regionCode].filter(Boolean).join(" · ") || `区域 ${id}`,
value: id
};
})
.filter((region) => region.id)
];
const countryRegionIds = buildCountryRegionIndex(regionOptions);
const countryOptions = [
{ countryCode: "", id: 0, label: "全部国家", regionIds: [], searchText: "全部国家 all", value: 0 },
...normalizeList(countries)
.map((country) => normalizeCountryOption(country, countryRegionIds))
.filter(Boolean)
];
const appOptions = normalizeList(apps)
.map((app) => {
const code = normalizeAppCode(app.appCode ?? app.app_code ?? app.code);
return {
code,
label: app.appName || app.app_name || app.name || code,
value: code
};
})
.filter((app) => app.code);
return {
appOptions,
countryOptions,
regionOptions
};
}
function normalizeList(value) {
if (Array.isArray(value)) {
return value;
}
if (Array.isArray(value?.items)) {
return value.items;
}
if (Array.isArray(value?.list)) {
return value.list;
}
return [];
}
function normalizeCodes(value) {
return Array.isArray(value) ? value.map((item) => String(item || "").trim().toUpperCase()).filter(Boolean) : [];
}
function buildCountryRegionIndex(regions) {
const index = new Map();
regions.forEach((region) => {
if (region.id === "all") {
return;
}
region.countryCodes.forEach((code) => {
const ids = index.get(code) || [];
ids.push(region.id);
index.set(code, ids);
});
});
return index;
}
function normalizeCountryOption(country, countryRegionIds) {
const countryCode = String(country.countryCode ?? country.country_code ?? country.code ?? "").trim().toUpperCase();
const id = Number(country.countryId ?? country.country_id ?? country.id ?? 0);
if (!countryCode && !id) {
return null;
}
const rawName = country.countryDisplayName || country.country_display_name || country.countryName || country.country_name || country.name || countryCode;
const name = stripCountryFlagPrefix(rawName);
const flag = country.flag || countryFlag(countryCode);
const label = [flag, name].filter(Boolean).join(" ");
const searchText = [flag, rawName, name, countryCode, country.isoAlpha3, country.iso_alpha3, country.phoneCountryCode, country.phone_country_code]
.filter(Boolean)
.join(" ")
.toLowerCase();
return {
countryCode,
flag,
id,
label,
name,
regionIds: countryRegionIds.get(countryCode) || [],
searchText,
value: id
};
}
function normalizeCountryStatisticsRow(data, country) {
const source = data || {};
const row = Array.isArray(source.country_breakdown) && source.country_breakdown.length ? source.country_breakdown[0] : source;
return {
...row,
country: stripCountryFlagPrefix(row.country || row.country_name || country.name || country.label),
country_code: row.country_code || row.countryCode || country.countryCode,
country_id: row.country_id || row.countryId || country.id
};
}
async function mapWithConcurrency(items, limit, callback) {
const results = [];
let index = 0;
async function worker() {
while (index < items.length) {
const currentIndex = index;
index += 1;
results[currentIndex] = await callback(items[currentIndex], currentIndex);
}
}
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, () => worker()));
return results.filter(Boolean);
}