mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-11 17:56:37 +08:00
fix(frontend): keep browser API on same-origin rewrites in dev
Document that NEXT_PUBLIC_WAVELET_BACKEND_URL must stay unset for local next dev so axios hits /api/* and Next rewrites proxy to the backend.
This commit is contained in:
@@ -28,6 +28,7 @@ import {
|
||||
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from 'recharts';
|
||||
|
||||
import services from '@/lib/services';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { ErrorInline } from '@/components/layout/error';
|
||||
import { LoadingStateWithBorder } from '@/components/layout/loading';
|
||||
import { EmptyStateWithBorder } from '@/components/layout/empty';
|
||||
@@ -83,6 +84,7 @@ function formatDateLabel(dateStr: string) {
|
||||
}
|
||||
|
||||
export function AccessAnalytics() {
|
||||
const t = useTranslations('admin.logs.analytics');
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
const [clickhouseDisabled, setClickhouseDisabled] = useState(false);
|
||||
@@ -135,11 +137,9 @@ export function AccessAnalytics() {
|
||||
return (
|
||||
<div className='flex flex-col items-center justify-center p-8 border border-dashed rounded-lg bg-card text-center my-6 min-h-[300px]'>
|
||||
<XCircle className='size-10 text-muted-foreground mb-3' />
|
||||
<h3 className='text-base font-semibold'>ClickHouse 未启用</h3>
|
||||
<h3 className='text-base font-semibold'>{t('clickhouseDisabled')}</h3>
|
||||
<p className='text-sm text-muted-foreground mt-1 max-w-[400px] mb-4'>
|
||||
当前系统配置未启用 ClickHouse
|
||||
存储,系统不会收集用户访问日志。如需使用此功能,请在后端 `config.yaml`
|
||||
配置文件中配置并启用 ClickHouse。
|
||||
{t('clickhouseDisabledDesc')}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
@@ -153,7 +153,7 @@ export function AccessAnalytics() {
|
||||
return (
|
||||
<LoadingStateWithBorder
|
||||
icon={BarChart3}
|
||||
description='加载访问统计指标中...'
|
||||
description={t('loadingAnalytics')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -165,7 +165,7 @@ export function AccessAnalytics() {
|
||||
<Card className='bg-card/25 border-border/40'>
|
||||
<CardHeader className='flex flex-row items-center justify-between pb-2 space-y-0'>
|
||||
<CardTitle className='text-sm font-semibold tracking-tight'>
|
||||
近 7 天总请求数
|
||||
{t('totalRequests7d')}
|
||||
</CardTitle>
|
||||
<TrendingUp className='size-4 text-muted-foreground' />
|
||||
</CardHeader>
|
||||
@@ -174,14 +174,14 @@ export function AccessAnalytics() {
|
||||
{totalTrendRequests.toLocaleString()}
|
||||
</div>
|
||||
<p className='text-[10px] text-muted-foreground mt-1'>
|
||||
系统记录的所有成功认证的访问总频次
|
||||
{t('totalRequests7dDesc')}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className='bg-card/25 border-border/40'>
|
||||
<CardHeader className='flex flex-row items-center justify-between pb-2 space-y-0'>
|
||||
<CardTitle className='text-sm font-semibold tracking-tight'>
|
||||
活跃终端分类数
|
||||
{t('activeBrowserTypes')}
|
||||
</CardTitle>
|
||||
<Globe className='size-4 text-muted-foreground' />
|
||||
</CardHeader>
|
||||
@@ -190,14 +190,14 @@ export function AccessAnalytics() {
|
||||
{browsers.length}
|
||||
</div>
|
||||
<p className='text-[10px] text-muted-foreground mt-1'>
|
||||
在一周内发起请求的浏览器代理大类统计
|
||||
{t('activeBrowserTypesDesc')}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className='bg-card/25 border-border/40 sm:col-span-2 lg:col-span-1'>
|
||||
<CardHeader className='flex flex-row items-center justify-between pb-2 space-y-0'>
|
||||
<CardTitle className='text-sm font-semibold tracking-tight'>
|
||||
活跃独立用户数
|
||||
{t('activeUsers')}
|
||||
</CardTitle>
|
||||
<Users className='size-4 text-muted-foreground' />
|
||||
</CardHeader>
|
||||
@@ -206,7 +206,7 @@ export function AccessAnalytics() {
|
||||
{topUsers.length}
|
||||
</div>
|
||||
<p className='text-[10px] text-muted-foreground mt-1'>
|
||||
一周内累计发起高频请求的注册账户总量
|
||||
{t('activeUsersDesc')}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -217,10 +217,10 @@ export function AccessAnalytics() {
|
||||
<CardHeader className='flex flex-row items-center justify-between'>
|
||||
<div>
|
||||
<CardTitle className='text-base font-bold'>
|
||||
一周访问量趋势
|
||||
{t('weeklyAccessTrend')}
|
||||
</CardTitle>
|
||||
<CardDescription className='text-xs'>
|
||||
展现系统最近 7 天内的每日 API 请求曲线
|
||||
{t('weeklyAccessTrendDesc')}
|
||||
</CardDescription>
|
||||
</div>
|
||||
<Button
|
||||
@@ -241,7 +241,7 @@ export function AccessAnalytics() {
|
||||
{trend.length === 0 ? (
|
||||
<EmptyStateWithBorder
|
||||
icon={TrendingUp}
|
||||
description='暂无趋势数据'
|
||||
description={t('noTrendData')}
|
||||
/>
|
||||
) : (
|
||||
<div className='h-[280px] w-full'>
|
||||
@@ -305,17 +305,17 @@ export function AccessAnalytics() {
|
||||
<CardHeader>
|
||||
<CardTitle className='text-base font-bold flex items-center gap-1.5'>
|
||||
<Globe className='size-4.5 text-muted-foreground' />
|
||||
使用的浏览器排行
|
||||
{t('browserRanking')}
|
||||
</CardTitle>
|
||||
<CardDescription className='text-xs'>
|
||||
基于请求头 User-Agent 智能分类的一周占比统计
|
||||
{t('browserRankingDesc')}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className='flex-1'>
|
||||
{browsers.length === 0 ? (
|
||||
<EmptyStateWithBorder
|
||||
icon={Globe}
|
||||
description='暂无浏览器排行数据'
|
||||
description={t('noBrowserData')}
|
||||
/>
|
||||
) : (
|
||||
<div className='space-y-4'>
|
||||
@@ -360,17 +360,17 @@ export function AccessAnalytics() {
|
||||
<CardHeader>
|
||||
<CardTitle className='text-base font-bold flex items-center gap-1.5'>
|
||||
<Users className='size-4.5 text-muted-foreground' />
|
||||
最活跃的用户排行 (Top 10)
|
||||
{t('topActiveUsers')}
|
||||
</CardTitle>
|
||||
<CardDescription className='text-xs'>
|
||||
统计最近一周发起接口访问请求数量最多的账户
|
||||
{t('topActiveUsersDesc')}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className='flex-1'>
|
||||
{topUsers.length === 0 ? (
|
||||
<EmptyStateWithBorder
|
||||
icon={Users}
|
||||
description='暂无活跃用户数据'
|
||||
description={t('noActiveUsersData')}
|
||||
/>
|
||||
) : (
|
||||
<div className='divide-y divide-border/40'>
|
||||
@@ -385,11 +385,13 @@ export function AccessAnalytics() {
|
||||
</div>
|
||||
<div className='truncate max-w-[180px] sm:max-w-xs'>
|
||||
<div className='text-xs font-bold leading-tight truncate'>
|
||||
{user.username || '未知'}
|
||||
{user.username || t('unknown')}
|
||||
</div>
|
||||
<div className='text-[10px] text-muted-foreground leading-normal truncate'>
|
||||
{user.nickname ? `(${user.nickname})` : '(无昵称)'} |
|
||||
ID: {user.user_id}
|
||||
{user.nickname
|
||||
? `(${user.nickname})`
|
||||
: t('noNickname')}{' '}
|
||||
| ID: {user.user_id}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -398,7 +400,7 @@ export function AccessAnalytics() {
|
||||
{user.count.toLocaleString()}
|
||||
</div>
|
||||
<div className='text-[9px] text-muted-foreground uppercase font-bold tracking-wider'>
|
||||
次请求
|
||||
{t('requests')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
} from 'lucide-react';
|
||||
|
||||
import services from '@/lib/services';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { ErrorInline } from '@/components/layout/error';
|
||||
import { LoadingStateWithBorder } from '@/components/layout/loading';
|
||||
import { EmptyStateWithBorder } from '@/components/layout/empty';
|
||||
@@ -97,6 +98,7 @@ function isClickhouseDisabledError(error: Error) {
|
||||
}
|
||||
|
||||
export function AccessLogs() {
|
||||
const t = useTranslations('admin.logs.accessLogs');
|
||||
const [page, setPage] = useState(1);
|
||||
|
||||
// 搜索过滤条件
|
||||
@@ -202,11 +204,9 @@ export function AccessLogs() {
|
||||
return (
|
||||
<div className='flex flex-col items-center justify-center p-8 border border-dashed rounded-lg bg-card text-center my-6 min-h-[300px]'>
|
||||
<XCircle className='size-10 text-muted-foreground mb-3' />
|
||||
<h3 className='text-base font-semibold'>ClickHouse 未启用</h3>
|
||||
<h3 className='text-base font-semibold'>{t('clickhouseDisabled')}</h3>
|
||||
<p className='text-sm text-muted-foreground mt-1 max-w-[400px] mb-4'>
|
||||
当前系统配置未启用 ClickHouse
|
||||
存储,系统不会收集用户访问日志。如需使用此功能,请在后端 `config.yaml`
|
||||
配置文件中配置并启用 ClickHouse。
|
||||
{t('clickhouseDisabledDesc')}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
@@ -227,11 +227,11 @@ export function AccessLogs() {
|
||||
>
|
||||
<div className='grid gap-1.5'>
|
||||
<Label htmlFor='search-user' className='text-xs'>
|
||||
用户名
|
||||
{t('username')}
|
||||
</Label>
|
||||
<Input
|
||||
id='search-user'
|
||||
placeholder='输入用户名搜索...'
|
||||
placeholder={t('usernamePlaceholder')}
|
||||
value={usernameFilter}
|
||||
onChange={(e) => setUsernameFilter(e.target.value)}
|
||||
className='h-8 text-xs'
|
||||
@@ -239,11 +239,11 @@ export function AccessLogs() {
|
||||
</div>
|
||||
<div className='grid gap-1.5'>
|
||||
<Label htmlFor='search-path' className='text-xs'>
|
||||
接口路径
|
||||
{t('apiPath')}
|
||||
</Label>
|
||||
<Input
|
||||
id='search-path'
|
||||
placeholder='模糊匹配路径...'
|
||||
placeholder={t('pathPlaceholder')}
|
||||
value={pathFilter}
|
||||
onChange={(e) => setPathFilter(e.target.value)}
|
||||
className='h-8 text-xs'
|
||||
@@ -251,7 +251,7 @@ export function AccessLogs() {
|
||||
</div>
|
||||
<div className='grid gap-1.5'>
|
||||
<Label htmlFor='search-start' className='text-xs'>
|
||||
起始时间
|
||||
{t('startTime')}
|
||||
</Label>
|
||||
<Input
|
||||
id='search-start'
|
||||
@@ -263,7 +263,7 @@ export function AccessLogs() {
|
||||
</div>
|
||||
<div className='grid gap-1.5'>
|
||||
<Label htmlFor='search-end' className='text-xs'>
|
||||
结束时间
|
||||
{t('endTime')}
|
||||
</Label>
|
||||
<Input
|
||||
id='search-end'
|
||||
@@ -276,7 +276,7 @@ export function AccessLogs() {
|
||||
<div className='flex gap-2'>
|
||||
<Button type='submit' size='sm' className='h-8 flex-1'>
|
||||
<Search className='size-3.5 mr-1' />
|
||||
搜索
|
||||
{t('search')}
|
||||
</Button>
|
||||
<Button
|
||||
type='button'
|
||||
@@ -286,7 +286,7 @@ export function AccessLogs() {
|
||||
className='h-8 flex-1'
|
||||
>
|
||||
<RotateCcw className='size-3.5 mr-1' />
|
||||
重置
|
||||
{t('reset')}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -295,26 +295,27 @@ export function AccessLogs() {
|
||||
{loading && logs.length === 0 ? (
|
||||
<LoadingStateWithBorder
|
||||
icon={Activity}
|
||||
description='加载访问日志中...'
|
||||
description={t('loadingAccessLogs')}
|
||||
/>
|
||||
) : logs.length === 0 ? (
|
||||
<EmptyStateWithBorder
|
||||
icon={Activity}
|
||||
description='未查询到任何访问日志数据'
|
||||
/>
|
||||
<EmptyStateWithBorder icon={Activity} description={t('noAccessLogs')} />
|
||||
) : (
|
||||
<div className='rounded-lg border bg-card overflow-hidden'>
|
||||
<Table className='min-w-[900px]'>
|
||||
<TableHeader>
|
||||
<TableRow className='hover:bg-transparent'>
|
||||
<TableHead className='w-[100px]'>请求方法</TableHead>
|
||||
<TableHead className='min-w-[200px]'>路径</TableHead>
|
||||
<TableHead className='w-[150px]'>用户</TableHead>
|
||||
<TableHead className='w-[120px]'>IP</TableHead>
|
||||
<TableHead className='w-[90px]'>状态</TableHead>
|
||||
<TableHead className='w-[100px]'>耗时</TableHead>
|
||||
<TableHead className='w-[170px]'>请求时间</TableHead>
|
||||
<TableHead className='w-[80px] text-center'>详情</TableHead>
|
||||
<TableHead className='w-[100px]'>{t('colMethod')}</TableHead>
|
||||
<TableHead className='min-w-[200px]'>{t('colPath')}</TableHead>
|
||||
<TableHead className='w-[150px]'>{t('colUser')}</TableHead>
|
||||
<TableHead className='w-[120px]'>{t('colIp')}</TableHead>
|
||||
<TableHead className='w-[90px]'>{t('colStatus')}</TableHead>
|
||||
<TableHead className='w-[100px]'>{t('colLatency')}</TableHead>
|
||||
<TableHead className='w-[170px]'>
|
||||
{t('colRequestTime')}
|
||||
</TableHead>
|
||||
<TableHead className='w-[80px] text-center'>
|
||||
{t('colDetail')}
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
@@ -334,7 +335,7 @@ export function AccessLogs() {
|
||||
<TableCell>
|
||||
<div className='flex flex-col'>
|
||||
<span className='text-xs font-medium'>
|
||||
{log.username || '未知用户'}
|
||||
{log.username || t('unknownUser')}
|
||||
</span>
|
||||
{log.nickname && (
|
||||
<span className='text-[10px] text-muted-foreground'>
|
||||
@@ -381,7 +382,7 @@ export function AccessLogs() {
|
||||
{logs.length > 0 && (
|
||||
<div className='flex items-center justify-between py-1'>
|
||||
<div className='text-xs text-muted-foreground'>
|
||||
共 {total} 条记录,当前第 {page}/{totalPages} 页
|
||||
{t('paginationInfo', { total, page, totalPages })}
|
||||
</div>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Button
|
||||
@@ -392,7 +393,7 @@ export function AccessLogs() {
|
||||
className='h-8 text-xs'
|
||||
>
|
||||
<ChevronLeft className='size-3.5 mr-1' />
|
||||
上一页
|
||||
{t('prevPage')}
|
||||
</Button>
|
||||
<Button
|
||||
variant='outline'
|
||||
@@ -401,7 +402,7 @@ export function AccessLogs() {
|
||||
disabled={page >= totalPages || loading}
|
||||
className='h-8 text-xs'
|
||||
>
|
||||
下一页
|
||||
{t('nextPage')}
|
||||
<ChevronRight className='size-3.5 ml-1' />
|
||||
</Button>
|
||||
</div>
|
||||
@@ -412,7 +413,9 @@ export function AccessLogs() {
|
||||
<Sheet open={detailOpen} onOpenChange={setDetailOpen}>
|
||||
<SheetContent className='w-full p-0 sm:max-w-[640px] flex flex-col h-full bg-background border-l'>
|
||||
<SheetHeader className='border-b px-5 py-4 shrink-0'>
|
||||
<SheetTitle className='text-lg font-bold'>访问日志详情</SheetTitle>
|
||||
<SheetTitle className='text-lg font-bold'>
|
||||
{t('accessLogDetail')}
|
||||
</SheetTitle>
|
||||
<SheetDescription className='text-xs'>
|
||||
访问 ID: {selectedLog?.id}
|
||||
</SheetDescription>
|
||||
@@ -424,7 +427,7 @@ export function AccessLogs() {
|
||||
<div className='grid grid-cols-2 gap-3 sm:grid-cols-3'>
|
||||
<div className='rounded-lg border p-3 bg-muted/10'>
|
||||
<div className='text-[10px] text-muted-foreground uppercase font-bold tracking-wider'>
|
||||
请求方法
|
||||
{t('method')}
|
||||
</div>
|
||||
<div className='mt-1 font-mono text-sm font-bold uppercase'>
|
||||
{selectedLog.method}
|
||||
@@ -432,7 +435,7 @@ export function AccessLogs() {
|
||||
</div>
|
||||
<div className='rounded-lg border p-3 bg-muted/10'>
|
||||
<div className='text-[10px] text-muted-foreground uppercase font-bold tracking-wider'>
|
||||
响应状态
|
||||
{t('responseStatus')}
|
||||
</div>
|
||||
<div className='mt-1'>
|
||||
<Badge variant={statusVariant(selectedLog.status)}>
|
||||
@@ -442,7 +445,7 @@ export function AccessLogs() {
|
||||
</div>
|
||||
<div className='rounded-lg border p-3 bg-muted/10'>
|
||||
<div className='text-[10px] text-muted-foreground uppercase font-bold tracking-wider'>
|
||||
耗时
|
||||
{t('latency')}
|
||||
</div>
|
||||
<div className='mt-1 font-mono text-sm'>
|
||||
{formatLatency(selectedLog.latency)}
|
||||
@@ -450,18 +453,18 @@ export function AccessLogs() {
|
||||
</div>
|
||||
<div className='rounded-lg border p-3 bg-muted/10 col-span-2 sm:col-span-1'>
|
||||
<div className='text-[10px] text-muted-foreground uppercase font-bold tracking-wider'>
|
||||
IP 地址
|
||||
{t('ipAddress')}
|
||||
</div>
|
||||
<div className='mt-1 font-mono text-sm'>{selectedLog.ip}</div>
|
||||
</div>
|
||||
<div className='rounded-lg border p-3 bg-muted/10 col-span-2'>
|
||||
<div className='text-[10px] text-muted-foreground uppercase font-bold tracking-wider'>
|
||||
用户
|
||||
{t('user')}
|
||||
</div>
|
||||
<div className='mt-1 text-sm font-medium'>
|
||||
{selectedLog.username
|
||||
? `${selectedLog.username} (${selectedLog.nickname || '无昵称'})`
|
||||
: '未知/游客'}
|
||||
? `${selectedLog.username} (${selectedLog.nickname || t('noNickname')})`
|
||||
: t('unknownGuest')}
|
||||
<span className='block font-mono text-[10px] text-muted-foreground mt-0.5'>
|
||||
ID: {selectedLog.user_id}
|
||||
</span>
|
||||
@@ -472,7 +475,7 @@ export function AccessLogs() {
|
||||
{/* Path */}
|
||||
<div className='grid gap-2'>
|
||||
<Label className='text-xs font-bold text-muted-foreground'>
|
||||
请求路径
|
||||
{t('requestPath')}
|
||||
</Label>
|
||||
<div className='rounded-md border bg-muted/30 px-3 py-2 font-mono text-xs break-all'>
|
||||
{selectedLog.path}
|
||||
@@ -482,7 +485,7 @@ export function AccessLogs() {
|
||||
{/* Request Time */}
|
||||
<div className='grid gap-2'>
|
||||
<Label className='text-xs font-bold text-muted-foreground'>
|
||||
请求时间
|
||||
{t('requestTime')}
|
||||
</Label>
|
||||
<div className='font-mono text-xs'>
|
||||
{formatDateTime(selectedLog.created_at)}
|
||||
@@ -542,7 +545,7 @@ export function AccessLogs() {
|
||||
<div className='flex-grow flex items-center justify-center'>
|
||||
<EmptyStateWithBorder
|
||||
icon={Activity}
|
||||
description='未选中日志明细'
|
||||
description={t('noLogSelected')}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -22,6 +22,7 @@ import { toast } from 'sonner';
|
||||
import { ArrowDown, ChevronUp, Loader2 } from 'lucide-react';
|
||||
|
||||
import services from '@/lib/services';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { ErrorInline } from '@/components/layout/error';
|
||||
import { LoadingStateWithBorder } from '@/components/layout/loading';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -92,6 +93,7 @@ function parseLogLevel(
|
||||
}
|
||||
|
||||
export function AppLogs() {
|
||||
const t = useTranslations('admin.logs.appLogs');
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
@@ -300,12 +302,12 @@ export function AppLogs() {
|
||||
{loadingMore ? (
|
||||
<>
|
||||
<Loader2 className='size-3 mr-1.5 animate-spin' />
|
||||
加载中...
|
||||
{t('loading')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ChevronUp className='size-3 mr-1.5' />
|
||||
加载更早日志
|
||||
{t('loadOlderLogs')}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
@@ -313,7 +315,9 @@ export function AppLogs() {
|
||||
)}
|
||||
|
||||
{logs.length === 0 ? (
|
||||
<div className='px-4 py-8 text-center text-gray-500'>暂无日志</div>
|
||||
<div className='px-4 py-8 text-center text-gray-500'>
|
||||
{t('noLogs')}
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className='relative px-3 py-3'
|
||||
@@ -349,7 +353,7 @@ export function AppLogs() {
|
||||
className='shadow-lg bg-background/80 backdrop-blur-sm border-border/50'
|
||||
>
|
||||
<ArrowDown className='size-3.5 mr-1.5' />
|
||||
回到最新
|
||||
{t('backToLatest')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user