优化指标处理逻辑,移除错误统计部分并更新前端展示。更新.gitignore以忽略.cursor文件。

This commit is contained in:
wood chen 2025-03-13 00:54:29 +08:00
parent 64423b00e2
commit de2209d177
4 changed files with 63 additions and 147 deletions

1
.gitignore vendored
View File

@ -26,3 +26,4 @@ web/dist/
data/config.json data/config.json
data/config.json data/config.json
kaifa.md kaifa.md
.cursor

View File

@ -51,13 +51,6 @@ type Metrics struct {
Distribution map[string]int64 `json:"distribution"` Distribution map[string]int64 `json:"distribution"`
} `json:"latency_stats"` } `json:"latency_stats"`
// 错误统计
ErrorStats struct {
ClientErrors int64 `json:"client_errors"`
ServerErrors int64 `json:"server_errors"`
Types map[string]int64 `json:"types"`
} `json:"error_stats"`
// 带宽统计 // 带宽统计
BandwidthHistory map[string]string `json:"bandwidth_history"` BandwidthHistory map[string]string `json:"bandwidth_history"`
CurrentBandwidth string `json:"current_bandwidth"` CurrentBandwidth string `json:"current_bandwidth"`
@ -115,26 +108,8 @@ func (h *ProxyHandler) MetricsHandler(w http.ResponseWriter, r *http.Request) {
} }
} }
// 计算客户端错误和服务器错误数量 // 处理状态码统计数据
var clientErrors, serverErrors int64
statusCodeStats := models.SafeStatusCodeStats(stats["status_code_stats"]) statusCodeStats := models.SafeStatusCodeStats(stats["status_code_stats"])
for code, count := range statusCodeStats {
codeInt := utils.ParseInt(code, 0)
if codeInt >= 400 && codeInt < 500 {
clientErrors += count
} else if codeInt >= 500 {
serverErrors += count
}
}
// 创建错误类型统计
errorTypes := make(map[string]int64)
if clientErrors > 0 {
errorTypes["客户端错误"] = clientErrors
}
if serverErrors > 0 {
errorTypes["服务器错误"] = serverErrors
}
metrics := Metrics{ metrics := Metrics{
Uptime: metrics.FormatUptime(uptime), Uptime: metrics.FormatUptime(uptime),
@ -197,11 +172,6 @@ func (h *ProxyHandler) MetricsHandler(w http.ResponseWriter, r *http.Request) {
metrics.LatencyStats.Distribution["gt1s"] = 0 metrics.LatencyStats.Distribution["gt1s"] = 0
} }
// 填充错误统计数据
metrics.ErrorStats.ClientErrors = clientErrors
metrics.ErrorStats.ServerErrors = serverErrors
metrics.ErrorStats.Types = errorTypes
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(metrics); err != nil { if err := json.NewEncoder(w).Encode(metrics); err != nil {
log.Printf("Error encoding metrics: %v", err) log.Printf("Error encoding metrics: %v", err)

View File

@ -25,7 +25,7 @@ import {
} from "@/components/ui/dialog" } from "@/components/ui/dialog"
import { Switch } from "@/components/ui/switch" import { Switch } from "@/components/ui/switch"
import { Slider } from "@/components/ui/slider" import { Slider } from "@/components/ui/slider"
import { Plus, Trash2, Edit, Save, Download, Upload } from "lucide-react" import { Plus, Trash2, Edit, Download, Upload } from "lucide-react"
import { import {
AlertDialog, AlertDialog,
AlertDialogAction, AlertDialogAction,
@ -66,6 +66,10 @@ export default function ConfigPage() {
// 使用 ref 来保存滚动位置 // 使用 ref 来保存滚动位置
const scrollPositionRef = useRef(0) const scrollPositionRef = useRef(0)
// 添加一个ref来跟踪是否是初始加载
const isInitialLoadRef = useRef(true)
// 添加一个防抖定时器ref
const saveTimeoutRef = useRef<NodeJS.Timeout | null>(null)
// 对话框状态 // 对话框状态
const [pathDialogOpen, setPathDialogOpen] = useState(false) const [pathDialogOpen, setPathDialogOpen] = useState(false)
@ -139,7 +143,8 @@ export default function ConfigPage() {
fetchConfig() fetchConfig()
}, [fetchConfig]) }, [fetchConfig])
const handleSave = async () => {
const handleSave = useCallback(async () => {
if (!config) return if (!config) return
setSaving(true) setSaving(true)
@ -172,7 +177,7 @@ export default function ConfigPage() {
toast({ toast({
title: "成功", title: "成功",
description: "配置已保存", description: "配置已自动保存",
}) })
} catch (error) { } catch (error) {
toast({ toast({
@ -183,7 +188,35 @@ export default function ConfigPage() {
} finally { } finally {
setSaving(false) setSaving(false)
} }
} }, [config, router, toast])
// 添加自动保存的useEffect
useEffect(() => {
// 如果是初始加载或者配置为空,不触发保存
if (isInitialLoadRef.current || !config) {
isInitialLoadRef.current = false
return
}
// 清除之前的定时器
if (saveTimeoutRef.current) {
clearTimeout(saveTimeoutRef.current)
}
// 设置新的定时器延迟1秒后保存
saveTimeoutRef.current = setTimeout(() => {
handleSave()
}, 1000)
// 组件卸载时清除定时器
return () => {
if (saveTimeoutRef.current) {
clearTimeout(saveTimeoutRef.current)
}
}
}, [config, handleSave]) // 监听config变化
// 处理对话框打开和关闭时的滚动位置 // 处理对话框打开和关闭时的滚动位置
const handleDialogOpenChange = useCallback((open: boolean, handler: (open: boolean) => void) => { const handleDialogOpenChange = useCallback((open: boolean, handler: (open: boolean) => void) => {
@ -289,11 +322,6 @@ export default function ConfigPage() {
} }
setPathDialogOpen(false) setPathDialogOpen(false)
toast({
title: "成功",
description: `${editingPathData ? '更新' : '添加'}路径配置成功`,
})
} }
const deletePath = (path: string) => { const deletePath = (path: string) => {
@ -306,10 +334,6 @@ export default function ConfigPage() {
delete newConfig.MAP[deletingPath] delete newConfig.MAP[deletingPath]
setConfig(newConfig) setConfig(newConfig)
setDeletingPath(null) setDeletingPath(null)
toast({
title: "成功",
description: "路径映射已删除",
})
} }
const updateCompression = (type: 'Gzip' | 'Brotli', field: 'Enabled' | 'Level', value: boolean | number) => { const updateCompression = (type: 'Gzip' | 'Brotli', field: 'Enabled' | 'Level', value: boolean | number) => {
@ -397,11 +421,6 @@ export default function ConfigPage() {
setExtensionMapDialogOpen(false) setExtensionMapDialogOpen(false)
setEditingExtension(null) setEditingExtension(null)
setNewExtension({ ext: "", target: "" }) setNewExtension({ ext: "", target: "" })
toast({
title: "成功",
description: "扩展名映射已更新",
})
} }
const deleteExtensionMap = (path: string, ext: string) => { const deleteExtensionMap = (path: string, ext: string) => {
@ -419,15 +438,8 @@ export default function ConfigPage() {
} }
setConfig(newConfig) setConfig(newConfig)
setDeletingExtension(null) setDeletingExtension(null)
toast({
title: "成功",
description: "扩展名映射已删除",
})
} }
const openAddPathDialog = () => { const openAddPathDialog = () => {
setEditingPathData(null) setEditingPathData(null)
setNewPathData({ setNewPathData({
@ -571,34 +583,31 @@ export default function ConfigPage() {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<Card> <Card>
<CardHeader> <CardHeader className="flex flex-row items-center justify-between">
<div className="flex flex-row items-center justify-between"> <CardTitle>Proxy Go配置</CardTitle>
<div> <div className="flex space-x-2">
<CardTitle>Proxy Go配置</CardTitle> <Button onClick={exportConfig} variant="outline">
<p className="text-sm text-muted-foreground mt-1"></p> <Download className="w-4 h-4 mr-2" />
</div>
<div className="flex space-x-2"> </Button>
<Button onClick={exportConfig} variant="outline"> <label>
<Download className="w-4 h-4 mr-2" /> <Button variant="outline" className="cursor-pointer">
<Upload className="w-4 h-4 mr-2" />
</Button> </Button>
<label> <input
<Button variant="outline" className="cursor-pointer"> type="file"
<Upload className="w-4 h-4 mr-2" /> className="hidden"
accept=".json"
</Button> onChange={importConfig}
<input />
type="file" </label>
className="hidden" {saving && (
accept=".json" <div className="flex items-center text-sm text-muted-foreground">
onChange={importConfig} <span className="animate-pulse mr-2"></span>
/> ...
</label> </div>
<Button onClick={handleSave} disabled={saving}> )}
<Save className="w-4 h-4 mr-2" />
{saving ? "保存中..." : "保存配置"}
</Button>
</div>
</div> </div>
</CardHeader> </CardHeader>
<CardContent> <CardContent>

View File

@ -37,11 +37,6 @@ interface Metrics {
max: string max: string
distribution: Record<string, number> distribution: Record<string, number>
} }
error_stats: {
client_errors: number
server_errors: number
types: Record<string, number>
}
bandwidth_history: Record<string, string> bandwidth_history: Record<string, string>
current_bandwidth: string current_bandwidth: string
total_bytes: number total_bytes: number
@ -325,65 +320,6 @@ export default function DashboardPage() {
</Card> </Card>
</div> </div>
{/* 错误统计卡片 */}
<Card>
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="space-y-2">
<div className="text-sm font-medium text-gray-500"> (4xx)</div>
<div className="text-2xl font-semibold text-yellow-600">
{metrics.error_stats?.client_errors || 0}
</div>
<div className="text-sm text-gray-500">
{metrics.total_requests ?
((metrics.error_stats?.client_errors || 0) / metrics.total_requests * 100).toFixed(2) : 0}%
</div>
</div>
<div className="space-y-2">
<div className="text-sm font-medium text-gray-500"> (5xx)</div>
<div className="text-2xl font-semibold text-red-600">
{metrics.error_stats?.server_errors || 0}
</div>
<div className="text-sm text-gray-500">
{metrics.total_requests ?
((metrics.error_stats?.server_errors || 0) / metrics.total_requests * 100).toFixed(2) : 0}%
</div>
</div>
<div className="space-y-2">
<div className="text-sm font-medium text-gray-500"></div>
<div className="text-2xl font-semibold">
{(metrics.error_rate * 100).toFixed(2)}%
</div>
<div className="text-sm text-gray-500">
: {metrics.total_errors || 0}
</div>
</div>
</div>
{metrics.error_stats?.types && Object.keys(metrics.error_stats.types).length > 0 && (
<div className="mt-6">
<div className="text-sm font-medium text-gray-500 mb-2"></div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{Object.entries(metrics.error_stats.types).map(([type, count]) => (
<div key={type} className="p-3 rounded-lg border bg-card text-card-foreground shadow-sm">
<div className="text-sm font-medium text-gray-500">{type}</div>
<div className="text-lg font-semibold">{count}</div>
<div className="text-xs text-gray-500 mt-1">
{metrics.total_errors ? ((count / metrics.total_errors) * 100).toFixed(1) : 0}%
</div>
</div>
))}
</div>
</div>
)}
</CardContent>
</Card>
{/* 引用来源统计卡片 */} {/* 引用来源统计卡片 */}
{metrics.top_referers && metrics.top_referers.length > 0 && ( {metrics.top_referers && metrics.top_referers.length > 0 && (
<Card> <Card>