enhance list pagenation layout

This commit is contained in:
oiov
2025-06-01 13:49:29 +08:00
parent f2de129ba8
commit cff4579ff1
12 changed files with 522 additions and 73 deletions

View File

@@ -9,6 +9,7 @@ import useSWR, { useSWRConfig } from "swr";
import { DomainFormData } from "@/lib/dto/domains";
import { fetcher, timeAgo } from "@/lib/utils";
import { useMediaQuery } from "@/hooks/use-media-query";
import { Button } from "@/components/ui/button";
import {
Card,
@@ -68,6 +69,7 @@ function TableColumnSekleton() {
}
export default function DomainList({ user, action }: DomainListProps) {
const { isMobile } = useMediaQuery();
const [isShowForm, setShowForm] = useState(false);
const [formType, setFormType] = useState<FormType>("add");
const [currentEditDomain, setCurrentEditDomain] =
@@ -326,6 +328,7 @@ export default function DomainList({ user, action }: DomainListProps) {
</TableBody>
{data && Math.ceil(data.total / pageSize) > 1 && (
<PaginationWrapper
layout={isMobile ? "right" : "split"}
total={data.total}
currentPage={currentPage}
setCurrentPage={setCurrentPage}

View File

@@ -3,6 +3,7 @@
import { ScrapeMeta } from "@prisma/client";
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts";
import { useElementSize } from "@/hooks/use-element-size";
import {
Card,
CardContent,
@@ -54,6 +55,7 @@ export function LineChartMultiple({
type1,
type2,
}: LineChartMultipleProps) {
const { ref: wrapperRef, width: wrapperWidth } = useElementSize();
const processedData = processChartData(chartData, type1, type2);
const chartConfig = {
@@ -75,12 +77,13 @@ export function LineChartMultiple({
{type2 && ` and ${type2}`}.
</CardDescription>
</CardHeader>
<CardContent>
<CardContent ref={wrapperRef}>
<ChartContainer config={chartConfig}>
<AreaChart
className="mt-6"
accessibilityLayer
data={processedData}
width={wrapperWidth}
margin={{
left: 12,
right: 12,

View File

@@ -73,6 +73,7 @@ function TableColumnSekleton({ className }: { className?: string }) {
}
export default function UsersList({ user }: UrlListProps) {
const { isMobile } = useMediaQuery();
const [isShowForm, setShowForm] = useState(false);
const [currentEditUser, setcurrentEditUser] = useState<User | null>(null);
const [currentPage, setCurrentPage] = useState(1);
@@ -282,6 +283,7 @@ export default function UsersList({ user }: UrlListProps) {
</TableBody>
{data && Math.ceil(data.total / pageSize) > 1 && (
<PaginationWrapper
layout={isMobile ? "right" : "split"}
total={data.total}
currentPage={currentPage}
setCurrentPage={setCurrentPage}

View File

@@ -10,6 +10,7 @@ import useSWR, { useSWRConfig } from "swr";
import { UserRecordFormData } from "@/lib/dto/cloudflare-dns-record";
import { TTL_ENUMS } from "@/lib/enums";
import { fetcher, timeAgo } from "@/lib/utils";
import { useMediaQuery } from "@/hooks/use-media-query";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
@@ -79,6 +80,7 @@ function TableColumnSekleton() {
}
export default function UserRecordsList({ user, action }: RecordListProps) {
const { isMobile } = useMediaQuery();
const [isShowForm, setShowForm] = useState(false);
const [formType, setFormType] = useState<FormType>("add");
const [currentEditRecord, setCurrentEditRecord] =
@@ -328,6 +330,7 @@ export default function UserRecordsList({ user, action }: RecordListProps) {
</TableBody>
{data && Math.ceil(data.total / pageSize) > 1 && (
<PaginationWrapper
layout={isMobile ? "right" : "split"}
total={data.total}
currentPage={currentPage}
setCurrentPage={setCurrentPage}

View File

@@ -98,12 +98,12 @@ const LogsTable = ({ userId, target }) => {
onChange={(e) => handleFilterChange("type", e.target.value)}
className="h-8 max-w-xs placeholder:text-xs"
/>
<Input
{/* <Input
placeholder="Filter by IP..."
value={filters.ip}
onChange={(e) => handleFilterChange("ip", e.target.value)}
className="h-8 max-w-xs placeholder:text-xs"
/>
/> */}
{
<>
<Input
@@ -139,16 +139,15 @@ const LogsTable = ({ userId, target }) => {
<div className="rounded-md border">
<Table>
<TableHeader className="bg-muted">
<TableRow className="">
<TableRow className="grid grid-cols-5 items-center sm:grid-cols-6">
<TableHead className="hidden items-center justify-start px-2 sm:flex">
Date
</TableHead>
<TableHead className="px-2">Type</TableHead>
<TableHead className="hidden items-center justify-start px-2 sm:flex">
IP
<TableHead className="flex items-center px-2">Type</TableHead>
<TableHead className="col-span-3 flex items-center px-2">
Link
</TableHead>
<TableHead className="px-2">Link</TableHead>
<TableHead className="px-2">User</TableHead>
<TableHead className="flex items-center px-2">User</TableHead>
</TableRow>
</TableHeader>
<TableBody>
@@ -161,9 +160,6 @@ const LogsTable = ({ userId, target }) => {
<TableCell>
<Skeleton className="h-2 w-[80px]" />
</TableCell>
<TableCell className="hidden sm:inline-block">
<Skeleton className="h-2 w-[120px]" />
</TableCell>
<TableCell>
<Skeleton className="h-2 w-[200px]" />
</TableCell>
@@ -173,15 +169,15 @@ const LogsTable = ({ userId, target }) => {
</TableRow>
))
: logs.map((log) => (
<TableRow className="text-xs hover:bg-muted" key={log.id}>
<TableRow
className="grid grid-cols-5 items-center text-xs hover:bg-muted sm:grid-cols-6"
key={log.id}
>
<TableCell className="hidden truncate p-2 sm:inline-block">
{new Date(log.createdAt).toLocaleString()}
</TableCell>
<TableCell className="p-2">{log.type}</TableCell>
<TableCell className="hidden p-2 sm:inline-block">
{log.ip}
</TableCell>
<TableCell className="max-w-md truncate p-2">
<TableCell className="col-span-3 max-w-full truncate p-2">
{log.link}
</TableCell>
<TableCell className="max-w-md truncate p-2">
@@ -193,12 +189,13 @@ const LogsTable = ({ userId, target }) => {
</Table>
</div>
<div className="flex items-center justify-between gap-2">
<div className="flex items-start justify-between gap-2 sm:items-center">
<p className="ml-auto text-nowrap text-sm">
{nFormatter(data?.total || 0)} logs
</p>
{data && Math.ceil(data.total / pageSize) > 1 && (
<PaginationWrapper
className="m-0"
total={data.total}
currentPage={page}
setCurrentPage={setPage}

View File

@@ -200,19 +200,30 @@ export default function LiveLog({ admin = false }: { admin?: boolean }) {
{error ? (
<div className="text-center text-red-500">{error.message}</div>
) : logs.length === 0 && !newLogs ? (
// <Skeleton className="h-8 w-full" />
<></>
) : (
<div className="scrollbar-hidden h-96 overflow-y-auto bg-primary-foreground">
<Table>
<TableHeader>
<TableRow className="bg-gray-100/50 text-sm dark:bg-primary-foreground">
<TableHead className="h-8 w-1/6 px-1">Time</TableHead>
<TableHead className="h-8 w-1/12 px-1">Slug</TableHead>
<TableHead className="h-8 px-1">Target</TableHead>
<TableHead className="h-8 w-1/12 px-1">IP</TableHead>
<TableHead className="h-8 w-1/6 px-1">Location</TableHead>
<TableHead className="h-8 w-1/12 px-1">Clicks</TableHead>
<TableRow className="grid grid-cols-5 bg-gray-100/50 text-sm dark:bg-primary-foreground sm:grid-cols-9">
<TableHead className="col-span-2 flex h-8 items-center">
Time
</TableHead>
<TableHead className="col-span-1 flex h-8 items-center">
Slug
</TableHead>
<TableHead className="col-span-3 hidden h-8 items-center sm:flex">
Target
</TableHead>
<TableHead className="col-span-1 hidden h-8 items-center sm:flex">
IP
</TableHead>
<TableHead className="col-span-1 flex h-8 items-center">
Location
</TableHead>
<TableHead className="col-span-1 flex h-8 items-center">
Clicks
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
@@ -234,23 +245,24 @@ export default function LiveLog({ admin = false }: { admin?: boolean }) {
ease: "linear",
},
}}
className="font-mono text-xs hover:bg-gray-200 dark:border-gray-800"
className="grid grid-cols-5 font-mono text-xs hover:bg-gray-200 dark:border-gray-800 sm:grid-cols-9"
>
<TableCell className="whitespace-nowrap px-1 py-1.5">
<TableCell className="col-span-2 truncate py-1.5">
{new Date(log.updatedAt).toLocaleString()}
</TableCell>
<TableCell className="font-midium px-1 py-1.5 text-green-700">
<TableCell className="font-midium col-span-1 truncate py-1.5 text-green-700">
{log.slug}
</TableCell>
<TableCell className="max-w-10 truncate px-1 py-1.5 hover:underline">
<TableCell className="col-span-3 hidden max-w-full truncate py-1.5 hover:underline sm:flex">
<a href={log.target} target="_blank" title={log.target}>
{log.target}
</a>
</TableCell>
<TableCell className="px-1 py-1.5">{log.ip}</TableCell>
<TableCell className="col-span-1 hidden truncate py-1.5 sm:flex">
{log.ip}
</TableCell>
<TableCell
className="max-w-6 truncate px-1 py-1.5"
className="col-span-1 truncate py-1.5"
title={getCountryName(log.country || "")}
>
{decodeURIComponent(
@@ -259,7 +271,7 @@ export default function LiveLog({ admin = false }: { admin?: boolean }) {
: "-",
)}
</TableCell>
<TableCell className="px-1 py-1.5 text-green-700">
<TableCell className="col-span-1 py-1.5 text-green-700">
{log.click}
</TableCell>
</motion.tr>

View File

@@ -10,9 +10,17 @@ import { WorldMapTopoJSON } from "@unovis/ts/maps";
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts";
import { TeamPlanQuota } from "@/config/team";
import { getCountryName, getDeviceVendor } from "@/lib/contries";
import {
getBotName,
getCountryName,
getDeviceVendor,
getEngineName,
getLanguageName,
getRegionName,
} from "@/lib/contries";
import { DATE_DIMENSION_ENUMS } from "@/lib/enums";
import { isLink, removeUrlSuffix, timeAgo } from "@/lib/utils";
import { useElementSize } from "@/hooks/use-element-size";
import { Button } from "@/components/ui/button";
import {
Card,
@@ -34,6 +42,7 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Icons } from "@/components/shared/icons";
const chartConfig = {
@@ -117,7 +126,15 @@ function generateStatsList(
? getCountryName(rawValue as string) // 国家代码转为国家名称
: dimension === "device"
? getDeviceVendor(rawValue as string) // 设备型号转为厂商名称
: rawValue; // 其他维度直接使用原始值
: dimension === "engine"
? getEngineName(rawValue as string) // 引擎名称
: dimension === "region"
? getRegionName(rawValue as string) // 区域名称
: dimension === "lang"
? getLanguageName(rawValue as string) // 语言名称
: dimension === "isBot"
? getBotName(rawValue as boolean) // 是否为机器人
: rawValue; // 其他维度直接使用原始值
const click = record.click || 0; // 确保 click 是数字,默认 0 如果未定义
@@ -154,6 +171,7 @@ export function DailyPVUVChart({
setTimeRange: React.Dispatch<React.SetStateAction<string>>;
user: Pick<User, "id" | "name" | "team">;
}) {
const { ref: wrapperRef, width: wrapperWidth } = useElementSize();
const [activeChart, setActiveChart] =
React.useState<keyof typeof chartConfig>("pv");
@@ -162,7 +180,6 @@ export function DailyPVUVChart({
pv: entry.clicks,
uv: new Set(entry.ips).size,
}));
// .sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime());
const dataTotal = calculateUVAndPV(data);
@@ -214,9 +231,15 @@ export function DailyPVUVChart({
const deviceStats = generateStatsList(data, "device");
const browserStats = generateStatsList(data, "browser");
const countryStats = generateStatsList(data, "country");
const osStats = generateStatsList(data, "os");
const cpuStats = generateStatsList(data, "cpu");
const engineStats = generateStatsList(data, "engine");
const languageStats = generateStatsList(data, "lang");
const regionStats = generateStatsList(data, "region");
const isBotStats = generateStatsList(data, "isBot");
return (
<Card className="">
<Card>
<CardHeader className="flex flex-col items-stretch space-y-0 border-b p-0 sm:flex-row">
<div className="flex flex-1 flex-col justify-center gap-1 px-6 py-2 sm:py-3">
<CardTitle>Link Analytics</CardTitle>
@@ -279,7 +302,7 @@ export function DailyPVUVChart({
})}
</div>
</CardHeader>
<CardContent className="px-2 sm:p-6">
<CardContent className="px-2 sm:p-6" ref={wrapperRef}>
<ChartContainer
config={chartConfig}
className="aspect-auto h-[225px] w-full"
@@ -351,9 +374,6 @@ export function DailyPVUVChart({
/>
}
/>
{/* <Bar dataKey="uv" fill={`var(--color-uv)`} stackId="a" />
<Bar dataKey="pv" fill={`var(--color-pv)`} stackId="a" /> */}
<Area
type="monotone"
dataKey="uv"
@@ -371,31 +391,103 @@ export function DailyPVUVChart({
</AreaChart>
</ChartContainer>
<VisSingleContainer data={{ areas: areaData }}>
<VisTopoJSONMap
topojson={WorldMapTopoJSON}
// pointRadius={1.6}
// mapFitToPoints={true}
/>
<VisSingleContainer
data={{ areas: areaData }}
width={wrapperWidth * 0.99}
>
<VisTopoJSONMap topojson={WorldMapTopoJSON} />
<VisTooltip triggers={triggers} />
</VisSingleContainer>
<div className="my-5 grid grid-cols-1 gap-6 sm:grid-cols-2">
{refererStats.length > 0 && (
<StatsList data={refererStats} title="Referrers" />
)}
{countryStats.length > 0 && (
<StatsList data={countryStats} title="Countries" />
)}
{cityStats.length > 0 && (
<StatsList data={cityStats} title="Cities" />
)}
{browserStats.length > 0 && (
<StatsList data={browserStats} title="Browsers" />
)}
{deviceStats.length > 0 && (
<StatsList data={deviceStats} title="Devices" />
)}
{/* Referrers、isBotStats */}
<Tabs defaultValue="referrer">
<TabsList>
<TabsTrigger value="referrer">Referrers</TabsTrigger>
<TabsTrigger value="isBot">Traffic Type</TabsTrigger>
</TabsList>
<TabsContent className="h-[90%]" value="referrer">
{refererStats.length > 0 && (
<StatsList data={refererStats} title="Referrers" />
)}
</TabsContent>
<TabsContent className="h-[90%]" value="isBot">
{isBotStats.length > 0 && (
<StatsList data={isBotStats} title="Is Bot" />
)}
</TabsContent>
</Tabs>
{/* 国家、城市 */}
<Tabs defaultValue="country">
<TabsList>
<TabsTrigger value="country">Country</TabsTrigger>
<TabsTrigger value="city">City</TabsTrigger>
</TabsList>
<TabsContent className="h-[90%]" value="country">
{countryStats.length > 0 && (
<StatsList data={countryStats} title="Countries" />
)}
</TabsContent>
<TabsContent className="h-[90%]" value="city">
{cityStats.length > 0 && (
<StatsList data={cityStats} title="Cities" />
)}
</TabsContent>
</Tabs>
{/* browserStats、engineStats */}
<Tabs defaultValue="browser">
<TabsList>
<TabsTrigger value="browser">Browser</TabsTrigger>
<TabsTrigger value="engine">Browser Engine</TabsTrigger>
</TabsList>
<TabsContent className="h-[90%]" value="browser">
{browserStats.length > 0 && (
<StatsList data={browserStats} title="Browsers" />
)}
</TabsContent>
<TabsContent className="h-[90%]" value="engine">
{engineStats.length > 0 && (
<StatsList data={engineStats} title="Engines" />
)}
</TabsContent>
</Tabs>
{/* Languages、regionStats */}
<Tabs className="h-full" defaultValue="language">
<TabsList>
<TabsTrigger value="language">Language</TabsTrigger>
<TabsTrigger value="region">Region</TabsTrigger>
</TabsList>
<TabsContent className="h-[90%]" value="language">
{languageStats.length > 0 && (
<StatsList data={languageStats} title="Languages" />
)}
</TabsContent>
<TabsContent className="h-[90%]" value="region">
{regionStats.length > 0 && (
<StatsList data={regionStats} title="Regions" />
)}
</TabsContent>
</Tabs>
{/* deviceStats、osStats、cpuStats */}
<Tabs defaultValue="device">
<TabsList>
<TabsTrigger value="device">Device</TabsTrigger>
<TabsTrigger value="os">OS</TabsTrigger>
<TabsTrigger value="cpu">CPU</TabsTrigger>
</TabsList>
<TabsContent className="h-[90%]" value="device">
{deviceStats.length > 0 && (
<StatsList data={deviceStats} title="Devices" />
)}
</TabsContent>
<TabsContent className="h-[90%]" value="os">
{osStats.length > 0 && <StatsList data={osStats} title="OS" />}
</TabsContent>
<TabsContent className="h-[90%]" value="cpu">
{cpuStats.length > 0 && <StatsList data={cpuStats} title="CPU" />}
</TabsContent>
</Tabs>
</div>
</CardContent>
</Card>
@@ -407,10 +499,13 @@ export function StatsList({ data, title }: { data: Stat[]; title: string }) {
const displayedData = showAll ? data.slice(0, 50) : data.slice(0, 8);
return (
<div className="rounded-lg border p-4">
<h1 className="text-lg font-bold">{title}</h1>
<div className="h-full rounded-lg border">
<div className="flex items-center justify-between border-b px-5 py-2 text-xs font-medium text-muted-foreground">
<span></span>
<span className=""></span>
</div>
<div
className={`scrollbar-hidden overflow-hidden overflow-y-auto transition-all duration-500 ease-in-out`}
className={`scrollbar-hidden overflow-hidden overflow-y-auto px-4 pb-4 pt-2 transition-all duration-500 ease-in-out`}
style={{
maxHeight: "18rem", // 动态计算最大高度
}}
@@ -454,7 +549,7 @@ export function StatsList({ data, title }: { data: Stat[]; title: string }) {
</div>
{data.length > 8 && (
<div className="mt-3 text-center">
<div className="mb-3 mt-1 text-center">
<Button
variant={"outline"}
onClick={() => setShowAll(!showAll)}

View File

@@ -18,6 +18,7 @@ import {
removeUrlSuffix,
timeAgo,
} from "@/lib/utils";
import { useMediaQuery } from "@/hooks/use-media-query";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
@@ -99,6 +100,7 @@ function TableColumnSekleton() {
export default function UserUrlsList({ user, action }: UrlListProps) {
const pathname = usePathname();
const { isMobile } = useMediaQuery();
const [currentView, setCurrentView] = useState<string>("List");
const [isShowForm, setShowForm] = useState(false);
const [formType, setFormType] = useState<FormType>("add");
@@ -446,6 +448,7 @@ export default function UserUrlsList({ user, action }: UrlListProps) {
</TableBody>
{data && Math.ceil(data.total / pageSize) > 1 && (
<PaginationWrapper
layout={isMobile ? "right" : "split"}
total={data.total}
currentPage={currentPage}
setCurrentPage={setCurrentPage}
@@ -628,6 +631,7 @@ export default function UserUrlsList({ user, action }: UrlListProps) {
{data && Math.ceil(data.total / pageSize) > 1 && (
<PaginationWrapper
layout={isMobile ? "right" : "split"}
total={data.total}
currentPage={currentPage}
setCurrentPage={setCurrentPage}

View File

@@ -7,6 +7,7 @@ import useSWR from "swr";
import { DATE_DIMENSION_ENUMS } from "@/lib/enums";
import { cn, fetcher, nFormatter } from "@/lib/utils";
import { useElementSize } from "@/hooks/use-element-size";
import {
Card,
CardContent,
@@ -62,6 +63,7 @@ const chartConfig = {
} satisfies ChartConfig;
export function InteractiveBarChart() {
const { ref: wrapperRef, width: wrapperWidth } = useElementSize();
const [timeRange, setTimeRange] = useState<string>("7d");
const [activeChart, setActiveChart] =
React.useState<keyof typeof chartConfig>("users");
@@ -134,7 +136,7 @@ export function InteractiveBarChart() {
</Select>
</div>
<div className="flex">
<div className="grid grid-cols-3 sm:grid-cols-6">
{["users", "records", "urls", "emails", "inbox", "sends"].map(
(key) => {
const chart = key as keyof typeof chartConfig;
@@ -144,13 +146,13 @@ export function InteractiveBarChart() {
<button
key={chart}
data-active={activeChart === chart}
className="relative z-30 flex flex-1 flex-col justify-center gap-1 border-l border-t p-4 text-left data-[active=true]:bg-muted/50 sm:border-l sm:border-t-0 sm:p-6"
className="relative z-30 flex flex-col justify-center gap-1 border-l border-t p-3 text-left transition-colors hover:bg-muted/30 data-[active=true]:bg-muted/50 sm:border-t-0 sm:p-4"
onClick={() => setActiveChart(chart)}
>
<span className="text-xs text-muted-foreground">
{chartConfig[chart].label}
</span>
<span className="text-lg font-bold leading-none sm:text-3xl">
<span className="text-base font-bold leading-none sm:text-lg">
{nFormatter(data.total[key])}
</span>
<span
@@ -170,7 +172,7 @@ export function InteractiveBarChart() {
)}
</div>
</CardHeader>
<CardContent className="px-2 sm:p-6">
<CardContent ref={wrapperRef} className="px-2 sm:p-6">
<ChartContainer
config={chartConfig}
className="aspect-auto h-[200px] w-full"
@@ -178,6 +180,7 @@ export function InteractiveBarChart() {
<BarChart
accessibilityLayer
data={data.list}
width={wrapperWidth}
margin={{
left: 12,
right: 12,

View File

@@ -10,7 +10,7 @@ interface UseElementSizeOptions {
box?: "content-box" | "border-box" | "device-pixel-content-box";
}
export function useElementSize<T extends HTMLElement>(
export function useElementSize<T extends HTMLDivElement>(
initialSize: ElementSize = { width: 0, height: 0 },
options: UseElementSizeOptions = {},
): { ref: React.RefObject<T>; width: number; height: number } {

View File

@@ -263,3 +263,330 @@ export const getDeviceVendor = (model: string) => {
}
return model;
};
export function getRegionName(regionCode: string) {
const regionMap = {
// Vercel/Cloudflare 地区代码
hkg1: "Hong Kong",
sin1: "Singapore",
iad1: "Washington D.C. (US East)",
gru1: "São Paulo (Brazil)",
sfo1: "San Francisco (US West)",
cdg1: "Paris (France)",
cle1: "Cleveland (US Central)",
cpt1: "Cape Town (South Africa)",
// AWS 地区代码
"us-east-1": "N. Virginia (US East)",
"us-west-1": "N. California (US West)",
"us-west-2": "Oregon (US West)",
"eu-west-1": "Ireland (Europe)",
"eu-central-1": "Frankfurt (Europe)",
"ap-southeast-1": "Singapore (Asia Pacific)",
"ap-northeast-1": "Tokyo (Asia Pacific)",
"ap-south-1": "Mumbai (Asia Pacific)",
// Cloudflare 地区代码
lhr: "London (UK)",
fra: "Frankfurt (Germany)",
ams: "Amsterdam (Netherlands)",
nrt: "Tokyo (Japan)",
icn: "Seoul (South Korea)",
syd: "Sydney (Australia)",
yyz: "Toronto (Canada)",
mia: "Miami (US Southeast)",
lax: "Los Angeles (US West)",
ord: "Chicago (US Central)",
atl: "Atlanta (US Southeast)",
dfw: "Dallas (US Central)",
sea: "Seattle (US West)",
bos: "Boston (US Northeast)",
ewr: "Newark (US Northeast)",
jfk: "New York (US Northeast)",
// 其他常见地区代码
pdx1: "Portland (US West)",
bom1: "Mumbai (India)",
syd1: "Sydney (Australia)",
nrt1: "Tokyo (Japan)",
fra1: "Frankfurt (Germany)",
lon1: "London (UK)",
ams1: "Amsterdam (Netherlands)",
tor1: "Toronto (Canada)",
nyc1: "New York (US East)",
dub1: "Dublin (Ireland)",
blr1: "Bangalore (India)",
sgp1: "Singapore",
hnd1: "Tokyo Haneda (Japan)",
kix1: "Osaka (Japan)",
icn1: "Seoul (South Korea)",
bkk1: "Bangkok (Thailand)",
mnl1: "Manila (Philippines)",
jkt1: "Jakarta (Indonesia)",
mel1: "Melbourne (Australia)",
per1: "Perth (Australia)",
akl1: "Auckland (New Zealand)",
mad1: "Madrid (Spain)",
bcn1: "Barcelona (Spain)",
mxp1: "Milan (Italy)",
vie1: "Vienna (Austria)",
zrh1: "Zurich (Switzerland)",
sto1: "Stockholm (Sweden)",
hel1: "Helsinki (Finland)",
cph1: "Copenhagen (Denmark)",
osl1: "Oslo (Norway)",
waw1: "Warsaw (Poland)",
prg1: "Prague (Czech Republic)",
bud1: "Budapest (Hungary)",
buh1: "Bucharest (Romania)",
sof1: "Sofia (Bulgaria)",
ath1: "Athens (Greece)",
ist1: "Istanbul (Turkey)",
tlv1: "Tel Aviv (Israel)",
cai1: "Cairo (Egypt)",
jnb1: "Johannesburg (South Africa)",
lag1: "Lagos (Nigeria)",
nbo1: "Nairobi (Kenya)",
dxb1: "Dubai (UAE)",
bah1: "Bahrain",
khi1: "Karachi (Pakistan)",
del1: "Delhi (India)",
ccj1: "Kolkata (India)",
maa1: "Chennai (India)",
hyd1: "Hyderabad (India)",
pnq1: "Pune (India)",
};
return regionMap[regionCode.toLowerCase()] || regionCode.toUpperCase();
}
export function getLanguageName(langCode: string) {
// 统一转换为小写处理大小写不一致
const normalizedCode = langCode.toLowerCase();
const languageMap = {
// 英语系列
en: "English",
"en-us": "English (United States)",
"en-gb": "English (United Kingdom)",
"en-ca": "English (Canada)",
"en-au": "English (Australia)",
"en-nz": "English (New Zealand)",
"en-ie": "English (Ireland)",
"en-za": "English (South Africa)",
"en-in": "English (India)",
// 中文系列
zh: "Chinese",
"zh-cn": "Chinese (Simplified)",
"zh-tw": "Chinese (Traditional)",
"zh-hk": "Chinese (Hong Kong)",
"zh-sg": "Chinese (Singapore)",
// 法语系列
fr: "French",
"fr-fr": "French (France)",
"fr-ca": "French (Canada)",
"fr-be": "French (Belgium)",
"fr-ch": "French (Switzerland)",
// 德语系列
de: "German",
"de-de": "German (Germany)",
"de-at": "German (Austria)",
"de-ch": "German (Switzerland)",
// 西班牙语系列
es: "Spanish",
"es-es": "Spanish (Spain)",
"es-mx": "Spanish (Mexico)",
"es-ar": "Spanish (Argentina)",
"es-co": "Spanish (Colombia)",
"es-cl": "Spanish (Chile)",
// 葡萄牙语系列
pt: "Portuguese",
"pt-pt": "Portuguese (Portugal)",
"pt-br": "Portuguese (Brazil)",
// 意大利语
it: "Italian",
"it-it": "Italian (Italy)",
// 俄语
ru: "Russian",
"ru-ru": "Russian (Russia)",
// 日语
ja: "Japanese",
"ja-jp": "Japanese (Japan)",
// 韩语
ko: "Korean",
"ko-kr": "Korean (South Korea)",
// 阿拉伯语
ar: "Arabic",
"ar-sa": "Arabic (Saudi Arabia)",
"ar-ae": "Arabic (UAE)",
"ar-eg": "Arabic (Egypt)",
// 荷兰语
nl: "Dutch",
"nl-nl": "Dutch (Netherlands)",
"nl-be": "Dutch (Belgium)",
// 北欧语言
sv: "Swedish",
"sv-se": "Swedish (Sweden)",
da: "Danish",
"da-dk": "Danish (Denmark)",
no: "Norwegian",
"no-no": "Norwegian (Norway)",
"nb-no": "Norwegian Bokmål",
"nn-no": "Norwegian Nynorsk",
fi: "Finnish",
"fi-fi": "Finnish (Finland)",
// 其他常见语言
hi: "Hindi",
"hi-in": "Hindi (India)",
th: "Thai",
"th-th": "Thai (Thailand)",
vi: "Vietnamese",
"vi-vn": "Vietnamese (Vietnam)",
tr: "Turkish",
"tr-tr": "Turkish (Turkey)",
pl: "Polish",
"pl-pl": "Polish (Poland)",
cs: "Czech",
"cs-cz": "Czech (Czech Republic)",
sk: "Slovak",
"sk-sk": "Slovak (Slovakia)",
hu: "Hungarian",
"hu-hu": "Hungarian (Hungary)",
ro: "Romanian",
"ro-ro": "Romanian (Romania)",
bg: "Bulgarian",
"bg-bg": "Bulgarian (Bulgaria)",
hr: "Croatian",
"hr-hr": "Croatian (Croatia)",
sr: "Serbian",
"sr-rs": "Serbian (Serbia)",
sl: "Slovenian",
"sl-si": "Slovenian (Slovenia)",
et: "Estonian",
"et-ee": "Estonian (Estonia)",
lv: "Latvian",
"lv-lv": "Latvian (Latvia)",
lt: "Lithuanian",
"lt-lt": "Lithuanian (Lithuania)",
el: "Greek",
"el-gr": "Greek (Greece)",
he: "Hebrew",
"he-il": "Hebrew (Israel)",
fa: "Persian",
"fa-ir": "Persian (Iran)",
ur: "Urdu",
"ur-pk": "Urdu (Pakistan)",
bn: "Bengali",
"bn-bd": "Bengali (Bangladesh)",
ta: "Tamil",
"ta-in": "Tamil (India)",
te: "Telugu",
"te-in": "Telugu (India)",
ml: "Malayalam",
"ml-in": "Malayalam (India)",
kn: "Kannada",
"kn-in": "Kannada (India)",
gu: "Gujarati",
"gu-in": "Gujarati (India)",
pa: "Punjabi",
"pa-in": "Punjabi (India)",
mr: "Marathi",
"mr-in": "Marathi (India)",
ne: "Nepali",
"ne-np": "Nepali (Nepal)",
si: "Sinhala",
"si-lk": "Sinhala (Sri Lanka)",
my: "Myanmar",
"my-mm": "Myanmar (Myanmar)",
km: "Khmer",
"km-kh": "Khmer (Cambodia)",
lo: "Lao",
"lo-la": "Lao (Laos)",
ka: "Georgian",
"ka-ge": "Georgian (Georgia)",
hy: "Armenian",
"hy-am": "Armenian (Armenia)",
az: "Azerbaijani",
"az-az": "Azerbaijani (Azerbaijan)",
kk: "Kazakh",
"kk-kz": "Kazakh (Kazakhstan)",
ky: "Kyrgyz",
"ky-kg": "Kyrgyz (Kyrgyzstan)",
uz: "Uzbek",
"uz-uz": "Uzbek (Uzbekistan)",
tg: "Tajik",
"tg-tj": "Tajik (Tajikistan)",
mn: "Mongolian",
"mn-mn": "Mongolian (Mongolia)",
bo: "Tibetan",
"bo-cn": "Tibetan (China)",
ug: "Uyghur",
"ug-cn": "Uyghur (China)",
id: "Indonesian",
"id-id": "Indonesian (Indonesia)",
ms: "Malay",
"ms-my": "Malay (Malaysia)",
tl: "Filipino",
"tl-ph": "Filipino (Philippines)",
sw: "Swahili",
"sw-ke": "Swahili (Kenya)",
am: "Amharic",
"am-et": "Amharic (Ethiopia)",
ha: "Hausa",
"ha-ng": "Hausa (Nigeria)",
yo: "Yoruba",
"yo-ng": "Yoruba (Nigeria)",
ig: "Igbo",
"ig-ng": "Igbo (Nigeria)",
zu: "Zulu",
"zu-za": "Zulu (South Africa)",
xh: "Xhosa",
"xh-za": "Xhosa (South Africa)",
af: "Afrikaans",
"af-za": "Afrikaans (South Africa)",
};
// 如果找到精确匹配,返回对应值
if (languageMap[normalizedCode]) {
return languageMap[normalizedCode];
}
// 如果没有精确匹配,尝试匹配语言部分(如 en-xx -> English
const langPart = normalizedCode.split("-")[0];
if (languageMap[langPart]) {
return languageMap[langPart];
}
// 如果都没有匹配,返回原始值(大写)
return langCode.toUpperCase();
}
export function getEngineName(engine: string) {
const engineMap = {
Blink: "Chrome Engine",
WebKit: "Safari Engine",
Gecko: "Firefox Engine",
Trident: "IE Engine",
EdgeHTML: "Edge Engine",
Presto: "Opera Engine",
};
return engineMap[engine] || `${engine} Engine`;
}
export function getBotName(bot: boolean) {
return bot === true ? "Bot" : "Human";
}

File diff suppressed because one or more lines are too long