mirror of
https://github.com/woodchen-ink/proxy-go.git
synced 2025-07-18 00:21:56 +08:00
505 lines
18 KiB
TypeScript
505 lines
18 KiB
TypeScript
"use client"
|
||
|
||
import { useEffect, useState, useCallback } from "react"
|
||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||
import { Button } from "@/components/ui/button"
|
||
import { useToast } from "@/components/ui/use-toast"
|
||
import { Switch } from "@/components/ui/switch"
|
||
import { Input } from "@/components/ui/input"
|
||
import { Label } from "@/components/ui/label"
|
||
import { useRouter } from "next/navigation"
|
||
|
||
interface CacheStats {
|
||
total_items: number
|
||
total_size: number
|
||
hit_count: number
|
||
miss_count: number
|
||
hit_rate: number
|
||
bytes_saved: number
|
||
enabled: boolean
|
||
format_fallback_hit: number
|
||
image_cache_hit: number
|
||
regular_cache_hit: number
|
||
}
|
||
|
||
interface CacheConfig {
|
||
max_age: number
|
||
cleanup_tick: number
|
||
max_cache_size: number
|
||
}
|
||
|
||
interface CacheData {
|
||
proxy: CacheStats
|
||
mirror: CacheStats
|
||
}
|
||
|
||
interface CacheConfigs {
|
||
proxy: CacheConfig
|
||
mirror: CacheConfig
|
||
}
|
||
|
||
function formatBytes(bytes: number) {
|
||
const units = ['B', 'KB', 'MB', 'GB']
|
||
let size = bytes
|
||
let unitIndex = 0
|
||
|
||
while (size >= 1024 && unitIndex < units.length - 1) {
|
||
size /= 1024
|
||
unitIndex++
|
||
}
|
||
|
||
return `${size.toFixed(2)} ${units[unitIndex]}`
|
||
}
|
||
|
||
export default function CachePage() {
|
||
const [stats, setStats] = useState<CacheData | null>(null)
|
||
const [configs, setConfigs] = useState<CacheConfigs | null>(null)
|
||
const [loading, setLoading] = useState(true)
|
||
const { toast } = useToast()
|
||
const router = useRouter()
|
||
|
||
const fetchStats = useCallback(async () => {
|
||
try {
|
||
const token = localStorage.getItem("token")
|
||
if (!token) {
|
||
router.push("/login")
|
||
return
|
||
}
|
||
|
||
const response = await fetch("/admin/api/cache/stats", {
|
||
headers: {
|
||
'Authorization': `Bearer ${token}`
|
||
}
|
||
})
|
||
|
||
if (response.status === 401) {
|
||
localStorage.removeItem("token")
|
||
router.push("/login")
|
||
return
|
||
}
|
||
|
||
if (!response.ok) throw new Error("获取缓存统计失败")
|
||
const data = await response.json()
|
||
setStats(data)
|
||
} catch (error) {
|
||
toast({
|
||
title: "错误",
|
||
description: error instanceof Error ? error.message : "获取缓存统计失败",
|
||
variant: "destructive",
|
||
})
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}, [toast, router])
|
||
|
||
const fetchConfigs = useCallback(async () => {
|
||
try {
|
||
const token = localStorage.getItem("token")
|
||
if (!token) {
|
||
router.push("/login")
|
||
return
|
||
}
|
||
|
||
const response = await fetch("/admin/api/cache/config", {
|
||
headers: {
|
||
'Authorization': `Bearer ${token}`
|
||
}
|
||
})
|
||
|
||
if (response.status === 401) {
|
||
localStorage.removeItem("token")
|
||
router.push("/login")
|
||
return
|
||
}
|
||
|
||
if (!response.ok) throw new Error("获取缓存配置失败")
|
||
const data = await response.json()
|
||
setConfigs(data)
|
||
} catch (error) {
|
||
toast({
|
||
title: "错误",
|
||
description: error instanceof Error ? error.message : "获取缓存配置失败",
|
||
variant: "destructive",
|
||
})
|
||
}
|
||
}, [toast, router])
|
||
|
||
useEffect(() => {
|
||
// 立即获取一次数据
|
||
fetchStats()
|
||
fetchConfigs()
|
||
|
||
// 设置定时刷新
|
||
const interval = setInterval(fetchStats, 1000)
|
||
return () => clearInterval(interval)
|
||
}, [fetchStats, fetchConfigs])
|
||
|
||
const handleToggleCache = async (type: "proxy" | "mirror", enabled: boolean) => {
|
||
try {
|
||
const token = localStorage.getItem("token")
|
||
if (!token) {
|
||
router.push("/login")
|
||
return
|
||
}
|
||
|
||
const response = await fetch("/admin/api/cache/enable", {
|
||
method: "POST",
|
||
headers: {
|
||
"Content-Type": "application/json",
|
||
'Authorization': `Bearer ${token}`
|
||
},
|
||
body: JSON.stringify({ type, enabled }),
|
||
})
|
||
|
||
if (response.status === 401) {
|
||
localStorage.removeItem("token")
|
||
router.push("/login")
|
||
return
|
||
}
|
||
|
||
if (!response.ok) throw new Error("切换缓存状态失败")
|
||
|
||
toast({
|
||
title: "成功",
|
||
description: `${type === "proxy" ? "代理" : "镜像"}缓存已${enabled ? "启用" : "禁用"}`,
|
||
})
|
||
|
||
fetchStats()
|
||
} catch (error) {
|
||
toast({
|
||
title: "错误",
|
||
description: error instanceof Error ? error.message : "切换缓存状态失败",
|
||
variant: "destructive",
|
||
})
|
||
}
|
||
}
|
||
|
||
const handleUpdateConfig = async (type: "proxy" | "mirror", config: CacheConfig) => {
|
||
try {
|
||
const token = localStorage.getItem("token")
|
||
if (!token) {
|
||
router.push("/login")
|
||
return
|
||
}
|
||
|
||
const response = await fetch("/admin/api/cache/config", {
|
||
method: "POST",
|
||
headers: {
|
||
"Content-Type": "application/json",
|
||
'Authorization': `Bearer ${token}`
|
||
},
|
||
body: JSON.stringify({ type, config }),
|
||
})
|
||
|
||
if (response.status === 401) {
|
||
localStorage.removeItem("token")
|
||
router.push("/login")
|
||
return
|
||
}
|
||
|
||
if (!response.ok) throw new Error("更新缓存配置失败")
|
||
|
||
toast({
|
||
title: "成功",
|
||
description: "缓存配置已更新",
|
||
})
|
||
|
||
fetchConfigs()
|
||
} catch (error) {
|
||
toast({
|
||
title: "错误",
|
||
description: error instanceof Error ? error.message : "更新缓存配置失败",
|
||
variant: "destructive",
|
||
})
|
||
}
|
||
}
|
||
|
||
const handleClearCache = async (type: "proxy" | "mirror" | "all") => {
|
||
try {
|
||
const token = localStorage.getItem("token")
|
||
if (!token) {
|
||
router.push("/login")
|
||
return
|
||
}
|
||
|
||
const response = await fetch("/admin/api/cache/clear", {
|
||
method: "POST",
|
||
headers: {
|
||
"Content-Type": "application/json",
|
||
'Authorization': `Bearer ${token}`
|
||
},
|
||
body: JSON.stringify({ type }),
|
||
})
|
||
|
||
if (response.status === 401) {
|
||
localStorage.removeItem("token")
|
||
router.push("/login")
|
||
return
|
||
}
|
||
|
||
if (!response.ok) throw new Error("清理缓存失败")
|
||
|
||
toast({
|
||
title: "成功",
|
||
description: "缓存已清理",
|
||
})
|
||
|
||
fetchStats()
|
||
} catch (error) {
|
||
toast({
|
||
title: "错误",
|
||
description: error instanceof Error ? error.message : "清理缓存失败",
|
||
variant: "destructive",
|
||
})
|
||
}
|
||
}
|
||
|
||
const renderCacheConfig = (type: "proxy" | "mirror" ) => {
|
||
if (!configs) return null
|
||
|
||
const config = configs[type]
|
||
return (
|
||
<div className="space-y-4 mt-4">
|
||
<h3 className="text-sm font-medium">缓存配置</h3>
|
||
<div className="grid gap-4">
|
||
<div className="grid grid-cols-2 items-center gap-4">
|
||
<Label htmlFor={`${type}-max-age`}>最大缓存时间(分钟)</Label>
|
||
<Input
|
||
id={`${type}-max-age`}
|
||
type="number"
|
||
value={config.max_age}
|
||
onChange={(e) => {
|
||
const newConfigs = { ...configs }
|
||
newConfigs[type].max_age = parseInt(e.target.value)
|
||
setConfigs(newConfigs)
|
||
}}
|
||
onBlur={() => handleUpdateConfig(type, config)}
|
||
/>
|
||
</div>
|
||
<div className="grid grid-cols-2 items-center gap-4">
|
||
<Label htmlFor={`${type}-cleanup-tick`}>清理间隔(分钟)</Label>
|
||
<Input
|
||
id={`${type}-cleanup-tick`}
|
||
type="number"
|
||
value={config.cleanup_tick}
|
||
onChange={(e) => {
|
||
const newConfigs = { ...configs }
|
||
newConfigs[type].cleanup_tick = parseInt(e.target.value)
|
||
setConfigs(newConfigs)
|
||
}}
|
||
onBlur={() => handleUpdateConfig(type, config)}
|
||
/>
|
||
</div>
|
||
<div className="grid grid-cols-2 items-center gap-4">
|
||
<Label htmlFor={`${type}-max-cache-size`}>最大缓存大小(GB)</Label>
|
||
<Input
|
||
id={`${type}-max-cache-size`}
|
||
type="number"
|
||
value={config.max_cache_size}
|
||
onChange={(e) => {
|
||
const newConfigs = { ...configs }
|
||
newConfigs[type].max_cache_size = parseInt(e.target.value)
|
||
setConfigs(newConfigs)
|
||
}}
|
||
onBlur={() => handleUpdateConfig(type, config)}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
if (loading) {
|
||
return (
|
||
<div className="flex h-[calc(100vh-4rem)] items-center justify-center">
|
||
<div className="text-center">
|
||
<div className="text-lg font-medium">加载中...</div>
|
||
<div className="text-sm text-gray-500 mt-1">正在获取缓存统计信息</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
return (
|
||
<div className="space-y-6">
|
||
<div className="flex justify-end space-x-2">
|
||
<Button variant="outline" onClick={() => handleClearCache("all")}>
|
||
清理所有缓存
|
||
</Button>
|
||
</div>
|
||
|
||
{/* 智能缓存汇总 */}
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle>智能缓存汇总</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||
<div className="text-center" title="所有常规文件的精确缓存命中总数">
|
||
<div className="text-2xl font-bold text-blue-600">
|
||
{(stats?.proxy.regular_cache_hit ?? 0) + (stats?.mirror.regular_cache_hit ?? 0)}
|
||
</div>
|
||
<div className="text-sm text-gray-500">常规缓存命中</div>
|
||
</div>
|
||
<div className="text-center" title="所有图片文件的精确格式缓存命中总数">
|
||
<div className="text-2xl font-bold text-green-600">
|
||
{(stats?.proxy.image_cache_hit ?? 0) + (stats?.mirror.image_cache_hit ?? 0)}
|
||
</div>
|
||
<div className="text-sm text-gray-500">图片精确命中</div>
|
||
</div>
|
||
<div className="text-center" title="图片格式回退命中总数,提高了缓存效率">
|
||
<div className="text-2xl font-bold text-orange-600">
|
||
{(stats?.proxy.format_fallback_hit ?? 0) + (stats?.mirror.format_fallback_hit ?? 0)}
|
||
</div>
|
||
<div className="text-sm text-gray-500">格式回退命中</div>
|
||
</div>
|
||
<div className="text-center" title="格式回退在所有图片请求中的占比,显示智能缓存的效果">
|
||
<div className="text-2xl font-bold text-purple-600">
|
||
{(() => {
|
||
const totalImageRequests = (stats?.proxy.image_cache_hit ?? 0) + (stats?.mirror.image_cache_hit ?? 0) + (stats?.proxy.format_fallback_hit ?? 0) + (stats?.mirror.format_fallback_hit ?? 0)
|
||
const fallbackHits = (stats?.proxy.format_fallback_hit ?? 0) + (stats?.mirror.format_fallback_hit ?? 0)
|
||
return totalImageRequests > 0 ? ((fallbackHits / totalImageRequests) * 100).toFixed(1) : '0.0'
|
||
})()}%
|
||
</div>
|
||
<div className="text-sm text-gray-500">格式回退率</div>
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
<div className="grid gap-6 md:grid-cols-2">
|
||
{/* 代理缓存 */}
|
||
<Card>
|
||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||
<CardTitle>代理缓存</CardTitle>
|
||
<div className="flex items-center space-x-2">
|
||
<Switch
|
||
checked={stats?.proxy.enabled ?? false}
|
||
onCheckedChange={(checked) => handleToggleCache("proxy", checked)}
|
||
/>
|
||
<Button
|
||
variant="outline"
|
||
size="sm"
|
||
onClick={() => handleClearCache("proxy")}
|
||
>
|
||
清理
|
||
</Button>
|
||
</div>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<dl className="space-y-2">
|
||
<div className="flex justify-between">
|
||
<dt className="text-sm font-medium text-gray-500">缓存项数量</dt>
|
||
<dd className="text-sm text-gray-900">{stats?.proxy.total_items ?? 0}</dd>
|
||
</div>
|
||
<div className="flex justify-between">
|
||
<dt className="text-sm font-medium text-gray-500">总大小</dt>
|
||
<dd className="text-sm text-gray-900">{formatBytes(stats?.proxy.total_size ?? 0)}</dd>
|
||
</div>
|
||
<div className="flex justify-between">
|
||
<dt className="text-sm font-medium text-gray-500">命中次数</dt>
|
||
<dd className="text-sm text-gray-900">{stats?.proxy.hit_count ?? 0}</dd>
|
||
</div>
|
||
<div className="flex justify-between">
|
||
<dt className="text-sm font-medium text-gray-500">未命中次数</dt>
|
||
<dd className="text-sm text-gray-900">{stats?.proxy.miss_count ?? 0}</dd>
|
||
</div>
|
||
<div className="flex justify-between">
|
||
<dt className="text-sm font-medium text-gray-500">命中率</dt>
|
||
<dd className="text-sm text-gray-900">{(stats?.proxy.hit_rate ?? 0).toFixed(2)}%</dd>
|
||
</div>
|
||
<div className="flex justify-between">
|
||
<dt className="text-sm font-medium text-gray-500">节省带宽</dt>
|
||
<dd className="text-sm text-gray-900">{formatBytes(stats?.proxy.bytes_saved ?? 0)}</dd>
|
||
</div>
|
||
<div className="border-t pt-2 mt-2">
|
||
<div className="text-xs font-medium text-gray-600 mb-1">智能缓存统计</div>
|
||
<div className="grid grid-cols-3 gap-2 text-xs">
|
||
<div className="text-center" title="常规文件的精确缓存命中">
|
||
<div className="text-blue-600 font-medium">{stats?.proxy.regular_cache_hit ?? 0}</div>
|
||
<div className="text-gray-500">常规命中</div>
|
||
</div>
|
||
<div className="text-center" title="图片文件的精确格式缓存命中">
|
||
<div className="text-green-600 font-medium">{stats?.proxy.image_cache_hit ?? 0}</div>
|
||
<div className="text-gray-500">图片命中</div>
|
||
</div>
|
||
<div className="text-center" title="图片格式回退命中(如请求WebP但提供JPEG)">
|
||
<div className="text-orange-600 font-medium">{stats?.proxy.format_fallback_hit ?? 0}</div>
|
||
<div className="text-gray-500">格式回退</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</dl>
|
||
{renderCacheConfig("proxy")}
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* 镜像缓存 */}
|
||
<Card>
|
||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||
<CardTitle>镜像缓存</CardTitle>
|
||
<div className="flex items-center space-x-2">
|
||
<Switch
|
||
checked={stats?.mirror.enabled ?? false}
|
||
onCheckedChange={(checked) => handleToggleCache("mirror", checked)}
|
||
/>
|
||
<Button
|
||
variant="outline"
|
||
size="sm"
|
||
onClick={() => handleClearCache("mirror")}
|
||
>
|
||
清理
|
||
</Button>
|
||
</div>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<dl className="space-y-2">
|
||
<div className="flex justify-between">
|
||
<dt className="text-sm font-medium text-gray-500">缓存项数量</dt>
|
||
<dd className="text-sm text-gray-900">{stats?.mirror.total_items ?? 0}</dd>
|
||
</div>
|
||
<div className="flex justify-between">
|
||
<dt className="text-sm font-medium text-gray-500">总大小</dt>
|
||
<dd className="text-sm text-gray-900">{formatBytes(stats?.mirror.total_size ?? 0)}</dd>
|
||
</div>
|
||
<div className="flex justify-between">
|
||
<dt className="text-sm font-medium text-gray-500">命中次数</dt>
|
||
<dd className="text-sm text-gray-900">{stats?.mirror.hit_count ?? 0}</dd>
|
||
</div>
|
||
<div className="flex justify-between">
|
||
<dt className="text-sm font-medium text-gray-500">未命中次数</dt>
|
||
<dd className="text-sm text-gray-900">{stats?.mirror.miss_count ?? 0}</dd>
|
||
</div>
|
||
<div className="flex justify-between">
|
||
<dt className="text-sm font-medium text-gray-500">命中率</dt>
|
||
<dd className="text-sm text-gray-900">{(stats?.mirror.hit_rate ?? 0).toFixed(2)}%</dd>
|
||
</div>
|
||
<div className="flex justify-between">
|
||
<dt className="text-sm font-medium text-gray-500">节省带宽</dt>
|
||
<dd className="text-sm text-gray-900">{formatBytes(stats?.mirror.bytes_saved ?? 0)}</dd>
|
||
</div>
|
||
<div className="border-t pt-2 mt-2">
|
||
<div className="text-xs font-medium text-gray-600 mb-1">智能缓存统计</div>
|
||
<div className="grid grid-cols-3 gap-2 text-xs">
|
||
<div className="text-center" title="常规文件的精确缓存命中">
|
||
<div className="text-blue-600 font-medium">{stats?.mirror.regular_cache_hit ?? 0}</div>
|
||
<div className="text-gray-500">常规命中</div>
|
||
</div>
|
||
<div className="text-center" title="图片文件的精确格式缓存命中">
|
||
<div className="text-green-600 font-medium">{stats?.mirror.image_cache_hit ?? 0}</div>
|
||
<div className="text-gray-500">图片命中</div>
|
||
</div>
|
||
<div className="text-center" title="图片格式回退命中(如请求WebP但提供JPEG)">
|
||
<div className="text-orange-600 font-medium">{stats?.mirror.format_fallback_hit ?? 0}</div>
|
||
<div className="text-gray-500">格式回退</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</dl>
|
||
{renderCacheConfig("mirror")}
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|