feat(dashboard): Enhance configuration management with advanced validation and UX improvements

- Add comprehensive input validation for path mappings, extension maps, and fixed paths
- Implement confirmation dialogs for deletion of paths, extensions, and fixed paths
- Improve error handling with detailed toast messages
- Add dynamic dialog state management for better user experience
- Enhance extension map editing with edit and delete functionality
- Implement more robust configuration import validation
This commit is contained in:
wood chen 2025-02-17 07:12:13 +08:00
parent 4b1c774509
commit ff24191146
2 changed files with 466 additions and 98 deletions

View File

@ -110,7 +110,9 @@ func (c *Collector) RecordRequest(path string, status int, latency time.Duration
now := time.Now() now := time.Now()
if now.Sub(c.lastMinute) >= time.Minute { if now.Sub(c.lastMinute) >= time.Minute {
currentMinute := now.Format("15:04") currentMinute := now.Format("15:04")
c.bandwidthStats.Store(currentMinute, atomic.SwapInt64(&c.minuteBytes, 0)) counter := new(int64)
*counter = atomic.SwapInt64(&c.minuteBytes, 0)
c.bandwidthStats.Store(currentMinute, counter)
c.lastMinute = now c.lastMinute = now
} }
@ -334,7 +336,11 @@ func (c *Collector) GetStats() map[string]interface{} {
// 收集错误类型统计 // 收集错误类型统计
errorTypeStats := make(map[string]int64) errorTypeStats := make(map[string]int64)
c.errorTypes.Range(func(key, value interface{}) bool { c.errorTypes.Range(func(key, value interface{}) bool {
errorTypeStats[key.(string)] = atomic.LoadInt64(value.(*int64)) if counter, ok := value.(*int64); ok {
errorTypeStats[key.(string)] = atomic.LoadInt64(counter)
} else {
errorTypeStats[key.(string)] = value.(int64)
}
return true return true
}) })
@ -351,7 +357,11 @@ func (c *Collector) GetStats() map[string]interface{} {
} }
for _, t := range times { for _, t := range times {
if bytes, ok := c.bandwidthStats.Load(t); ok { if bytes, ok := c.bandwidthStats.Load(t); ok {
bandwidthHistory[t] = utils.FormatBytes(atomic.LoadInt64(bytes.(*int64))) + "/min" if counter, ok := bytes.(*int64); ok {
bandwidthHistory[t] = utils.FormatBytes(atomic.LoadInt64(counter)) + "/min"
} else {
bandwidthHistory[t] = utils.FormatBytes(bytes.(int64)) + "/min"
}
} }
} }

View File

@ -26,6 +26,16 @@ import {
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, Save, Download, Upload } from "lucide-react"
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog"
interface PathMapping { interface PathMapping {
DefaultTarget: string DefaultTarget: string
@ -78,6 +88,7 @@ export default function ConfigPage() {
}) })
const [extensionMapDialogOpen, setExtensionMapDialogOpen] = useState(false) const [extensionMapDialogOpen, setExtensionMapDialogOpen] = useState(false)
const [editingPath, setEditingPath] = useState<string | null>(null) const [editingPath, setEditingPath] = useState<string | null>(null)
const [editingExtension, setEditingExtension] = useState<{ext: string, target: string} | null>(null)
const [newExtension, setNewExtension] = useState({ ext: "", target: "" }) const [newExtension, setNewExtension] = useState({ ext: "", target: "" })
const [editingPathData, setEditingPathData] = useState<{ const [editingPathData, setEditingPathData] = useState<{
@ -87,6 +98,10 @@ export default function ConfigPage() {
sizeUnit: 'B' | 'KB' | 'MB' | 'GB'; sizeUnit: 'B' | 'KB' | 'MB' | 'GB';
} | null>(null); } | null>(null);
const [deletingPath, setDeletingPath] = useState<string | null>(null)
const [deletingFixedPath, setDeletingFixedPath] = useState<FixedPath | null>(null)
const [deletingExtension, setDeletingExtension] = useState<{path: string, ext: string} | null>(null)
const fetchConfig = useCallback(async () => { const fetchConfig = useCallback(async () => {
try { try {
const token = localStorage.getItem("token") const token = localStorage.getItem("token")
@ -176,32 +191,47 @@ export default function ConfigPage() {
} }
} }
const handleEditPath = (path: string, target: PathMapping | string) => { const handlePathDialogOpenChange = (open: boolean) => {
setPathDialogOpen(true) setPathDialogOpen(open)
if (typeof target === 'string') { if (!open) {
setEditingPathData({ setEditingPathData(null)
path, setNewPathData({
defaultTarget: target, path: "",
defaultTarget: "",
extensionMap: {},
sizeThreshold: 0, sizeThreshold: 0,
sizeUnit: 'MB' sizeUnit: 'MB',
}) })
} else { }
const sizeThreshold = target.SizeThreshold || 0 }
const { value, unit } = convertBytesToUnit(sizeThreshold)
setEditingPathData({ const handleFixedPathDialogOpenChange = (open: boolean) => {
path, setFixedPathDialogOpen(open)
defaultTarget: target.DefaultTarget, if (!open) {
sizeThreshold: value, setEditingFixedPath(null)
sizeUnit: unit setNewFixedPath({
Path: "",
TargetHost: "",
TargetURL: "",
}) })
} }
} }
const handleExtensionMapDialogOpenChange = (open: boolean) => {
setExtensionMapDialogOpen(open)
if (!open) {
setEditingPath(null)
setEditingExtension(null)
setNewExtension({ ext: "", target: "" })
}
}
const addOrUpdatePath = () => { const addOrUpdatePath = () => {
if (!config) return if (!config) return
const data = editingPathData ? editingPathData : newPathData const data = editingPathData ? editingPathData : newPathData
if (!data.path || !data.defaultTarget) { // 验证输入
if (!data.path.trim() || !data.defaultTarget.trim()) {
toast({ toast({
title: "错误", title: "错误",
description: "路径和默认目标不能为空", description: "路径和默认目标不能为空",
@ -210,6 +240,28 @@ export default function ConfigPage() {
return return
} }
// 验证路径格式
if (!data.path.startsWith('/')) {
toast({
title: "错误",
description: "路径必须以/开头",
variant: "destructive",
})
return
}
// 验证URL格式
try {
new URL(data.defaultTarget)
} catch {
toast({
title: "错误",
description: "默认目标URL格式不正确",
variant: "destructive",
})
return
}
const sizeInBytes = convertToBytes(data.sizeThreshold, data.sizeUnit) const sizeInBytes = convertToBytes(data.sizeThreshold, data.sizeUnit)
const newConfig = { ...config } const newConfig = { ...config }
const existingMapping = newConfig.MAP[data.path] const existingMapping = newConfig.MAP[data.path]
@ -244,13 +296,27 @@ export default function ConfigPage() {
sizeThreshold: 0, sizeThreshold: 0,
sizeUnit: 'MB', sizeUnit: 'MB',
}) })
toast({
title: "成功",
description: "路径映射已更新",
})
} }
const deletePath = (path: string) => { const deletePath = (path: string) => {
if (!config) return setDeletingPath(path)
}
const confirmDeletePath = () => {
if (!config || !deletingPath) return
const newConfig = { ...config } const newConfig = { ...config }
delete newConfig.MAP[path] delete newConfig.MAP[deletingPath]
setConfig(newConfig) setConfig(newConfig)
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) => {
@ -264,15 +330,24 @@ export default function ConfigPage() {
setConfig(newConfig) setConfig(newConfig)
} }
const handleExtensionMapEdit = (path: string) => { const handleExtensionMapEdit = (path: string, ext?: string, target?: string) => {
setEditingPath(path) setEditingPath(path)
if (ext && target) {
setEditingExtension({ ext, target })
setNewExtension({ ext, target })
} else {
setEditingExtension(null)
setNewExtension({ ext: "", target: "" })
}
setExtensionMapDialogOpen(true) setExtensionMapDialogOpen(true)
} }
const addExtensionMap = () => { const addOrUpdateExtensionMap = () => {
if (!config || !editingPath) return if (!config || !editingPath) return
const { ext, target } = newExtension const { ext, target } = newExtension
if (!ext || !target) {
// 验证输入
if (!ext.trim() || !target.trim()) {
toast({ toast({
title: "错误", title: "错误",
description: "扩展名和目标不能为空", description: "扩展名和目标不能为空",
@ -281,6 +356,29 @@ export default function ConfigPage() {
return return
} }
// 验证扩展名格式
const extensions = ext.split(',').map(e => e.trim())
if (extensions.some(e => !e || e.includes('.'))) {
toast({
title: "错误",
description: "扩展名格式不正确,不需要包含点号",
variant: "destructive",
})
return
}
// 验证URL格式
try {
new URL(target)
} catch {
toast({
title: "错误",
description: "目标URL格式不正确",
variant: "destructive",
})
return
}
const newConfig = { ...config } const newConfig = { ...config }
const mapping = newConfig.MAP[editingPath] const mapping = newConfig.MAP[editingPath]
if (typeof mapping === "string") { if (typeof mapping === "string") {
@ -289,6 +387,13 @@ export default function ConfigPage() {
ExtensionMap: { [ext]: target } ExtensionMap: { [ext]: target }
} }
} else { } else {
// 如果是编辑现有的扩展名映射,先删除旧的
if (editingExtension) {
const newExtMap = { ...mapping.ExtensionMap }
delete newExtMap[editingExtension.ext]
mapping.ExtensionMap = newExtMap
}
// 添加新的映射
mapping.ExtensionMap = { mapping.ExtensionMap = {
...mapping.ExtensionMap, ...mapping.ExtensionMap,
[ext]: target [ext]: target
@ -296,26 +401,43 @@ export default function ConfigPage() {
} }
setConfig(newConfig) setConfig(newConfig)
setExtensionMapDialogOpen(false)
setEditingExtension(null)
setNewExtension({ ext: "", target: "" }) setNewExtension({ ext: "", target: "" })
toast({
title: "成功",
description: "扩展名映射已更新",
})
} }
const deleteExtensionMap = (path: string, ext: string) => { const deleteExtensionMap = (path: string, ext: string) => {
if (!config) return setDeletingExtension({ path, ext })
}
const confirmDeleteExtensionMap = () => {
if (!config || !deletingExtension) return
const newConfig = { ...config } const newConfig = { ...config }
const mapping = newConfig.MAP[path] const mapping = newConfig.MAP[deletingExtension.path]
if (typeof mapping !== "string" && mapping.ExtensionMap) { if (typeof mapping !== "string" && mapping.ExtensionMap) {
const newExtensionMap = { ...mapping.ExtensionMap } const newExtensionMap = { ...mapping.ExtensionMap }
delete newExtensionMap[ext] delete newExtensionMap[deletingExtension.ext]
mapping.ExtensionMap = newExtensionMap mapping.ExtensionMap = newExtensionMap
} }
setConfig(newConfig) setConfig(newConfig)
setDeletingExtension(null)
toast({
title: "成功",
description: "扩展名映射已删除",
})
} }
const addFixedPath = () => { const addFixedPath = () => {
if (!config) return if (!config) return
const { Path, TargetHost, TargetURL } = newFixedPath const { Path, TargetHost, TargetURL } = newFixedPath
if (!Path || !TargetHost || !TargetURL) { // 验证输入
if (!Path.trim() || !TargetHost.trim() || !TargetURL.trim()) {
toast({ toast({
title: "错误", title: "错误",
description: "所有字段都不能为空", description: "所有字段都不能为空",
@ -324,6 +446,38 @@ export default function ConfigPage() {
return return
} }
// 验证路径格式
if (!Path.startsWith('/')) {
toast({
title: "错误",
description: "路径必须以/开头",
variant: "destructive",
})
return
}
// 验证URL格式
try {
new URL(TargetURL)
} catch {
toast({
title: "错误",
description: "目标URL格式不正确",
variant: "destructive",
})
return
}
// 验证主机名格式
if (!/^[a-zA-Z0-9][a-zA-Z0-9-_.]+[a-zA-Z0-9]$/.test(TargetHost)) {
toast({
title: "错误",
description: "目标主机格式不正确",
variant: "destructive",
})
return
}
const newConfig = { ...config } const newConfig = { ...config }
if (editingFixedPath) { if (editingFixedPath) {
const index = newConfig.FixedPaths.findIndex(p => p.Path === editingFixedPath.Path) const index = newConfig.FixedPaths.findIndex(p => p.Path === editingFixedPath.Path)
@ -331,6 +485,15 @@ export default function ConfigPage() {
newConfig.FixedPaths[index] = newFixedPath newConfig.FixedPaths[index] = newFixedPath
} }
} else { } else {
// 检查路径是否已存在
if (newConfig.FixedPaths.some(p => p.Path === Path)) {
toast({
title: "错误",
description: "该路径已存在",
variant: "destructive",
})
return
}
newConfig.FixedPaths.push(newFixedPath) newConfig.FixedPaths.push(newFixedPath)
} }
@ -342,19 +505,59 @@ export default function ConfigPage() {
TargetHost: "", TargetHost: "",
TargetURL: "", TargetURL: "",
}) })
toast({
title: "成功",
description: "固定路径已更新",
})
} }
const editFixedPath = (path: FixedPath) => { const editFixedPath = (path: FixedPath) => {
setEditingFixedPath(path) setEditingFixedPath(path)
setNewFixedPath(path) setNewFixedPath({
Path: path.Path,
TargetHost: path.TargetHost,
TargetURL: path.TargetURL,
})
setFixedPathDialogOpen(true)
}
const openAddPathDialog = () => {
setEditingPathData(null)
setNewPathData({
path: "",
defaultTarget: "",
extensionMap: {},
sizeThreshold: 0,
sizeUnit: 'MB',
})
setPathDialogOpen(true)
}
const openAddFixedPathDialog = () => {
setEditingFixedPath(null)
setNewFixedPath({
Path: "",
TargetHost: "",
TargetURL: "",
})
setFixedPathDialogOpen(true) setFixedPathDialogOpen(true)
} }
const deleteFixedPath = (path: FixedPath) => { const deleteFixedPath = (path: FixedPath) => {
if (!config) return setDeletingFixedPath(path)
}
const confirmDeleteFixedPath = () => {
if (!config || !deletingFixedPath) return
const newConfig = { ...config } const newConfig = { ...config }
newConfig.FixedPaths = newConfig.FixedPaths.filter(p => p.Path !== path.Path) newConfig.FixedPaths = newConfig.FixedPaths.filter(p => p.Path !== deletingFixedPath.Path)
setConfig(newConfig) setConfig(newConfig)
setDeletingFixedPath(null)
toast({
title: "成功",
description: "固定路径已删除",
})
} }
const exportConfig = () => { const exportConfig = () => {
@ -379,15 +582,59 @@ export default function ConfigPage() {
try { try {
const content = e.target?.result as string const content = e.target?.result as string
const newConfig = JSON.parse(content) const newConfig = JSON.parse(content)
// 验证配置结构
if (!newConfig.MAP || typeof newConfig.MAP !== 'object') {
throw new Error('配置文件缺少 MAP 字段或格式不正确')
}
if (!newConfig.Compression ||
typeof newConfig.Compression !== 'object' ||
!newConfig.Compression.Gzip ||
!newConfig.Compression.Brotli) {
throw new Error('配置文件压缩设置格式不正确')
}
if (!Array.isArray(newConfig.FixedPaths)) {
throw new Error('配置文件固定路径格式不正确')
}
// 验证路径映射
for (const [path, target] of Object.entries(newConfig.MAP)) {
if (!path.startsWith('/')) {
throw new Error(`路径 ${path} 必须以/开头`)
}
if (typeof target === 'string') {
try {
new URL(target)
} catch {
throw new Error(`路径 ${path} 的目标URL格式不正确`)
}
} else if (target && typeof target === 'object') {
const mapping = target as PathMapping
if (!mapping.DefaultTarget || typeof mapping.DefaultTarget !== 'string') {
throw new Error(`路径 ${path} 的默认目标格式不正确`)
}
try {
new URL(mapping.DefaultTarget)
} catch {
throw new Error(`路径 ${path} 的默认目标URL格式不正确`)
}
} else {
throw new Error(`路径 ${path} 的目标格式不正确`)
}
}
setConfig(newConfig) setConfig(newConfig)
toast({ toast({
title: "成功", title: "成功",
description: "配置已导入", description: "配置已导入",
}) })
} catch { } catch (error) {
toast({ toast({
title: "错误", title: "错误",
description: "配置文件格式错误", description: error instanceof Error ? error.message : "配置文件格式错误",
variant: "destructive", variant: "destructive",
}) })
} }
@ -395,6 +642,27 @@ export default function ConfigPage() {
reader.readAsText(file) reader.readAsText(file)
} }
const handleEditPath = (path: string, target: PathMapping | string) => {
if (typeof target === 'string') {
setEditingPathData({
path,
defaultTarget: target,
sizeThreshold: 0,
sizeUnit: 'MB'
})
} else {
const sizeThreshold = target.SizeThreshold || 0
const { value, unit } = convertBytesToUnit(sizeThreshold)
setEditingPathData({
path,
defaultTarget: target.DefaultTarget,
sizeThreshold: value,
sizeUnit: unit
})
}
setPathDialogOpen(true)
}
if (loading) { if (loading) {
return ( return (
<div className="flex h-[calc(100vh-4rem)] items-center justify-center"> <div className="flex h-[calc(100vh-4rem)] items-center justify-center">
@ -444,9 +712,9 @@ export default function ConfigPage() {
<TabsContent value="paths" className="space-y-4"> <TabsContent value="paths" className="space-y-4">
<div className="flex justify-end"> <div className="flex justify-end">
<Dialog open={pathDialogOpen} onOpenChange={setPathDialogOpen}> <Dialog open={pathDialogOpen} onOpenChange={handlePathDialogOpenChange}>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button> <Button onClick={openAddPathDialog}>
<Plus className="w-4 h-4 mr-2" /> <Plus className="w-4 h-4 mr-2" />
</Button> </Button>
@ -556,22 +824,69 @@ export default function ConfigPage() {
</TableCell> </TableCell>
<TableCell> <TableCell>
{typeof target === 'object' && target.ExtensionMap ? ( {typeof target === 'object' && target.ExtensionMap ? (
<div className="space-y-1"> <div className="space-y-4">
{Object.entries(target.ExtensionMap).map(([ext, url]) => ( <Table>
<div key={ext} className="flex items-center space-x-2"> <TableHeader>
<span className="text-sm" title={url}>{ext}: {truncateUrl(url)}</span> <TableRow>
<Button <TableHead className="w-1/3"></TableHead>
variant="ghost" <TableHead className="w-1/2"></TableHead>
size="icon" <TableHead className="w-1/6"></TableHead>
className="h-6 w-6" </TableRow>
onClick={() => deleteExtensionMap(path, ext)} </TableHeader>
> <TableBody>
<Trash2 className="h-3 w-3" /> {Object.entries(target.ExtensionMap).map(([ext, url]) => (
</Button> <TableRow key={ext}>
</div> <TableCell>{ext}</TableCell>
))} <TableCell>
<span title={url}>{truncateUrl(url)}</span>
</TableCell>
<TableCell>
<div className="flex space-x-2">
<Button
variant="ghost"
size="icon"
className="h-6 w-6"
onClick={() => handleExtensionMapEdit(path, ext, url)}
>
<Edit className="h-3 w-3" />
</Button>
<Button
variant="ghost"
size="icon"
className="h-6 w-6"
onClick={() => deleteExtensionMap(path, ext)}
>
<Trash2 className="h-3 w-3" />
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
<div className="flex justify-end">
<Button
variant="outline"
size="sm"
onClick={() => handleExtensionMapEdit(path)}
>
<Plus className="w-3 h-3 mr-2" />
</Button>
</div>
</div> </div>
) : '-'} ) : (
<div className="flex justify-end">
<Button
variant="outline"
size="sm"
onClick={() => handleExtensionMapEdit(path)}
>
<Plus className="w-3 h-3 mr-2" />
</Button>
</div>
)}
</TableCell> </TableCell>
<TableCell> <TableCell>
<div className="flex space-x-2"> <div className="flex space-x-2">
@ -582,13 +897,6 @@ export default function ConfigPage() {
> >
<Edit className="w-4 h-4" /> <Edit className="w-4 h-4" />
</Button> </Button>
<Button
variant="ghost"
size="icon"
onClick={() => handleExtensionMapEdit(path)}
>
<Plus className="w-4 h-4" />
</Button>
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
@ -602,39 +910,6 @@ export default function ConfigPage() {
))} ))}
</TableBody> </TableBody>
</Table> </Table>
<Dialog open={extensionMapDialogOpen} onOpenChange={setExtensionMapDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle></DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div className="space-y-2">
<Label></Label>
<Input
value={newExtension.ext}
onChange={(e) => setNewExtension({ ...newExtension, ext: e.target.value })}
placeholder="jpg,png,webp"
/>
<p className="text-sm text-muted-foreground">
</p>
</div>
<div className="space-y-2">
<Label> URL</Label>
<Input
value={newExtension.target}
onChange={(e) => setNewExtension({ ...newExtension, target: e.target.value })}
placeholder="https://example.com"
/>
<p className="text-sm text-muted-foreground">
使
</p>
</div>
<Button onClick={addExtensionMap}></Button>
</div>
</DialogContent>
</Dialog>
</TabsContent> </TabsContent>
<TabsContent value="compression" className="space-y-6"> <TabsContent value="compression" className="space-y-6">
@ -691,7 +966,7 @@ export default function ConfigPage() {
<TabsContent value="fixed-paths"> <TabsContent value="fixed-paths">
<div className="flex justify-end mb-4"> <div className="flex justify-end mb-4">
<Button onClick={() => setFixedPathDialogOpen(true)}> <Button onClick={openAddFixedPathDialog}>
<Plus className="w-4 h-4 mr-2" /> <Plus className="w-4 h-4 mr-2" />
</Button> </Button>
@ -735,7 +1010,7 @@ export default function ConfigPage() {
</TableBody> </TableBody>
</Table> </Table>
<Dialog open={fixedPathDialogOpen} onOpenChange={setFixedPathDialogOpen}> <Dialog open={fixedPathDialogOpen} onOpenChange={handleFixedPathDialogOpenChange}>
<DialogContent> <DialogContent>
<DialogHeader> <DialogHeader>
<DialogTitle> <DialogTitle>
@ -746,7 +1021,7 @@ export default function ConfigPage() {
<div className="space-y-2"> <div className="space-y-2">
<Label></Label> <Label></Label>
<Input <Input
value={newFixedPath.Path} value={editingFixedPath ? editingFixedPath.Path : newFixedPath.Path}
onChange={(e) => setNewFixedPath({ ...newFixedPath, Path: e.target.value })} onChange={(e) => setNewFixedPath({ ...newFixedPath, Path: e.target.value })}
placeholder="/example" placeholder="/example"
/> />
@ -754,7 +1029,7 @@ export default function ConfigPage() {
<div className="space-y-2"> <div className="space-y-2">
<Label></Label> <Label></Label>
<Input <Input
value={newFixedPath.TargetHost} value={editingFixedPath ? editingFixedPath.TargetHost : newFixedPath.TargetHost}
onChange={(e) => setNewFixedPath({ ...newFixedPath, TargetHost: e.target.value })} onChange={(e) => setNewFixedPath({ ...newFixedPath, TargetHost: e.target.value })}
placeholder="example.com" placeholder="example.com"
/> />
@ -762,7 +1037,7 @@ export default function ConfigPage() {
<div className="space-y-2"> <div className="space-y-2">
<Label> URL</Label> <Label> URL</Label>
<Input <Input
value={newFixedPath.TargetURL} value={editingFixedPath ? editingFixedPath.TargetURL : newFixedPath.TargetURL}
onChange={(e) => setNewFixedPath({ ...newFixedPath, TargetURL: e.target.value })} onChange={(e) => setNewFixedPath({ ...newFixedPath, TargetURL: e.target.value })}
placeholder="https://example.com" placeholder="https://example.com"
/> />
@ -777,6 +1052,88 @@ export default function ConfigPage() {
</Tabs> </Tabs>
</CardContent> </CardContent>
</Card> </Card>
<Dialog open={extensionMapDialogOpen} onOpenChange={handleExtensionMapDialogOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>
{editingExtension ? "编辑扩展名映射" : "添加扩展名映射"}
</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div className="space-y-2">
<Label></Label>
<Input
value={newExtension.ext}
onChange={(e) => setNewExtension({ ...newExtension, ext: e.target.value })}
placeholder="jpg,png,webp"
/>
<p className="text-sm text-muted-foreground">
</p>
</div>
<div className="space-y-2">
<Label> URL</Label>
<Input
value={newExtension.target}
onChange={(e) => setNewExtension({ ...newExtension, target: e.target.value })}
placeholder="https://example.com"
/>
<p className="text-sm text-muted-foreground">
使
</p>
</div>
<Button onClick={addOrUpdateExtensionMap}>
{editingExtension ? "保存" : "添加"}
</Button>
</div>
</DialogContent>
</Dialog>
<AlertDialog open={!!deletingPath} onOpenChange={(open) => !open && setDeletingPath(null)}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle></AlertDialogTitle>
<AlertDialogDescription>
&ldquo;{deletingPath}&rdquo;
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel></AlertDialogCancel>
<AlertDialogAction onClick={confirmDeletePath}></AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<AlertDialog open={!!deletingFixedPath} onOpenChange={(open) => !open && setDeletingFixedPath(null)}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle></AlertDialogTitle>
<AlertDialogDescription>
&ldquo;{deletingFixedPath?.Path}&rdquo;
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel></AlertDialogCancel>
<AlertDialogAction onClick={confirmDeleteFixedPath}></AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<AlertDialog open={!!deletingExtension} onOpenChange={(open) => !open && setDeletingExtension(null)}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle></AlertDialogTitle>
<AlertDialogDescription>
&ldquo;{deletingExtension?.ext}&rdquo;
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel></AlertDialogCancel>
<AlertDialogAction onClick={confirmDeleteExtensionMap}></AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div> </div>
) )
} }
@ -800,20 +1157,21 @@ const truncateUrl = (url: string) => {
// 辅助函数:单位转换 // 辅助函数:单位转换
const convertToBytes = (value: number, unit: 'B' | 'KB' | 'MB' | 'GB'): number => { const convertToBytes = (value: number, unit: 'B' | 'KB' | 'MB' | 'GB'): number => {
if (value < 0) return 0
const multipliers = { const multipliers = {
'B': 1, 'B': 1,
'KB': 1024, 'KB': 1024,
'MB': 1024 * 1024, 'MB': 1024 * 1024,
'GB': 1024 * 1024 * 1024 'GB': 1024 * 1024 * 1024
} }
return value * multipliers[unit] return Math.floor(value * multipliers[unit])
} }
const convertBytesToUnit = (bytes: number): { value: number, unit: 'B' | 'KB' | 'MB' | 'GB' } => { const convertBytesToUnit = (bytes: number): { value: number, unit: 'B' | 'KB' | 'MB' | 'GB' } => {
if (bytes === 0) return { value: 0, unit: 'MB' } if (bytes <= 0) return { value: 0, unit: 'MB' }
const k = 1024 const k = 1024
const sizes: Array<'B' | 'KB' | 'MB' | 'GB'> = ['B', 'KB', 'MB', 'GB'] const sizes: Array<'B' | 'KB' | 'MB' | 'GB'> = ['B', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k)) const i = Math.min(Math.floor(Math.log(bytes) / Math.log(k)), sizes.length - 1)
return { return {
value: Number((bytes / Math.pow(k, i)).toFixed(2)), value: Number((bytes / Math.pow(k, i)).toFixed(2)),
unit: sizes[i] unit: sizes[i]