mirror of
https://github.com/woodchen-ink/proxy-go.git
synced 2025-07-18 16:41:54 +08:00
feat(config): Enhance path mapping configuration with size unit and editing support
- Add size unit selection (B, KB, MB, GB) for file size thresholds - Implement path mapping editing functionality - Improve UI with more descriptive placeholders and helper text - Add utility functions for byte conversion and URL truncation - Update table display to show formatted file sizes and truncated URLs
This commit is contained in:
parent
dd57ec2bd5
commit
9de17edcbd
@ -67,6 +67,7 @@ export default function ConfigPage() {
|
|||||||
defaultTarget: "",
|
defaultTarget: "",
|
||||||
extensionMap: {} as Record<string, string>,
|
extensionMap: {} as Record<string, string>,
|
||||||
sizeThreshold: 0,
|
sizeThreshold: 0,
|
||||||
|
sizeUnit: 'MB' as 'B' | 'KB' | 'MB' | 'GB',
|
||||||
})
|
})
|
||||||
const [fixedPathDialogOpen, setFixedPathDialogOpen] = useState(false)
|
const [fixedPathDialogOpen, setFixedPathDialogOpen] = useState(false)
|
||||||
const [editingFixedPath, setEditingFixedPath] = useState<FixedPath | null>(null)
|
const [editingFixedPath, setEditingFixedPath] = useState<FixedPath | null>(null)
|
||||||
@ -79,6 +80,13 @@ export default function ConfigPage() {
|
|||||||
const [editingPath, setEditingPath] = useState<string | null>(null)
|
const [editingPath, setEditingPath] = useState<string | null>(null)
|
||||||
const [newExtension, setNewExtension] = useState({ ext: "", target: "" })
|
const [newExtension, setNewExtension] = useState({ ext: "", target: "" })
|
||||||
|
|
||||||
|
const [editingPathData, setEditingPathData] = useState<{
|
||||||
|
path: string;
|
||||||
|
defaultTarget: string;
|
||||||
|
sizeThreshold: number;
|
||||||
|
sizeUnit: 'B' | 'KB' | 'MB' | 'GB';
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
const fetchConfig = useCallback(async () => {
|
const fetchConfig = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem("token")
|
const token = localStorage.getItem("token")
|
||||||
@ -168,11 +176,32 @@ export default function ConfigPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const addPath = () => {
|
const handleEditPath = (path: string, target: PathMapping | string) => {
|
||||||
if (!config) return
|
setPathDialogOpen(true)
|
||||||
const { path, defaultTarget, sizeThreshold, extensionMap } = newPathData
|
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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!path || !defaultTarget) {
|
const addOrUpdatePath = () => {
|
||||||
|
if (!config) return
|
||||||
|
const data = editingPathData ? editingPathData : newPathData
|
||||||
|
|
||||||
|
if (!data.path || !data.defaultTarget) {
|
||||||
toast({
|
toast({
|
||||||
title: "错误",
|
title: "错误",
|
||||||
description: "路径和默认目标不能为空",
|
description: "路径和默认目标不能为空",
|
||||||
@ -181,20 +210,39 @@ export default function ConfigPage() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const sizeInBytes = convertToBytes(data.sizeThreshold, data.sizeUnit)
|
||||||
const newConfig = { ...config }
|
const newConfig = { ...config }
|
||||||
newConfig.MAP[path] = {
|
const existingMapping = newConfig.MAP[data.path]
|
||||||
DefaultTarget: defaultTarget,
|
|
||||||
...(sizeThreshold ? { SizeThreshold: sizeThreshold } : {}),
|
if (editingPathData) {
|
||||||
...(Object.keys(extensionMap).length > 0 ? { ExtensionMap: extensionMap } : {})
|
if (typeof existingMapping === 'object') {
|
||||||
|
newConfig.MAP[data.path] = {
|
||||||
|
DefaultTarget: data.defaultTarget,
|
||||||
|
SizeThreshold: sizeInBytes,
|
||||||
|
ExtensionMap: existingMapping.ExtensionMap || {}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
newConfig.MAP[data.path] = {
|
||||||
|
DefaultTarget: data.defaultTarget,
|
||||||
|
SizeThreshold: sizeInBytes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
newConfig.MAP[data.path] = {
|
||||||
|
DefaultTarget: data.defaultTarget,
|
||||||
|
SizeThreshold: sizeInBytes
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setConfig(newConfig)
|
setConfig(newConfig)
|
||||||
setPathDialogOpen(false)
|
setPathDialogOpen(false)
|
||||||
|
setEditingPathData(null)
|
||||||
setNewPathData({
|
setNewPathData({
|
||||||
path: "",
|
path: "",
|
||||||
defaultTarget: "",
|
defaultTarget: "",
|
||||||
extensionMap: {},
|
extensionMap: {},
|
||||||
sizeThreshold: 0,
|
sizeThreshold: 0,
|
||||||
|
sizeUnit: 'MB',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -405,34 +453,78 @@ export default function ConfigPage() {
|
|||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>添加路径映射</DialogTitle>
|
<DialogTitle>{editingPathData ? "编辑路径映射" : "添加路径映射"}</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>路径</Label>
|
<Label>路径 (如: /images)</Label>
|
||||||
<Input
|
<Input
|
||||||
value={newPathData.path}
|
value={editingPathData ? editingPathData.path : newPathData.path}
|
||||||
onChange={(e) => setNewPathData({ ...newPathData, path: e.target.value })}
|
onChange={(e) => editingPathData
|
||||||
|
? setEditingPathData({ ...editingPathData, path: e.target.value })
|
||||||
|
: setNewPathData({ ...newPathData, path: e.target.value })
|
||||||
|
}
|
||||||
placeholder="/example"
|
placeholder="/example"
|
||||||
/>
|
/>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
请输入需要代理的路径
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>默认目标</Label>
|
<Label>默认目标</Label>
|
||||||
<Input
|
<Input
|
||||||
value={newPathData.defaultTarget}
|
value={editingPathData ? editingPathData.defaultTarget : newPathData.defaultTarget}
|
||||||
onChange={(e) => setNewPathData({ ...newPathData, defaultTarget: e.target.value })}
|
onChange={(e) => editingPathData
|
||||||
|
? setEditingPathData({ ...editingPathData, defaultTarget: e.target.value })
|
||||||
|
: setNewPathData({ ...newPathData, defaultTarget: e.target.value })
|
||||||
|
}
|
||||||
placeholder="https://example.com"
|
placeholder="https://example.com"
|
||||||
/>
|
/>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
默认的回源地址,所有请求都会转发到这个地址
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>大小阈值 (字节)</Label>
|
<Label>大小阈值</Label>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
value={newPathData.sizeThreshold}
|
value={editingPathData ? editingPathData.sizeThreshold : newPathData.sizeThreshold}
|
||||||
onChange={(e) => setNewPathData({ ...newPathData, sizeThreshold: parseInt(e.target.value) })}
|
onChange={(e) => {
|
||||||
|
const value = parseInt(e.target.value) || 0
|
||||||
|
if (editingPathData) {
|
||||||
|
setEditingPathData({ ...editingPathData, sizeThreshold: value })
|
||||||
|
} else {
|
||||||
|
setNewPathData({ ...newPathData, sizeThreshold: value })
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
min={0}
|
||||||
/>
|
/>
|
||||||
|
<select
|
||||||
|
className="h-10 rounded-md border border-input bg-background px-3"
|
||||||
|
value={editingPathData ? editingPathData.sizeUnit : newPathData.sizeUnit}
|
||||||
|
onChange={(e) => {
|
||||||
|
const unit = e.target.value as 'B' | 'KB' | 'MB' | 'GB'
|
||||||
|
if (editingPathData) {
|
||||||
|
setEditingPathData({ ...editingPathData, sizeUnit: unit })
|
||||||
|
} else {
|
||||||
|
setNewPathData({ ...newPathData, sizeUnit: unit })
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value="B">B</option>
|
||||||
|
<option value="KB">KB</option>
|
||||||
|
<option value="MB">MB</option>
|
||||||
|
<option value="GB">GB</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<Button onClick={addPath}>添加</Button>
|
<p className="text-sm text-muted-foreground">
|
||||||
|
文件大小超过此阈值时,将使用扩展名映射中的目标地址(如果存在)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button onClick={addOrUpdatePath}>
|
||||||
|
{editingPathData ? "保存" : "添加"}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
@ -442,7 +534,7 @@ export default function ConfigPage() {
|
|||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead>路径</TableHead>
|
<TableHead>路径</TableHead>
|
||||||
<TableHead>目标</TableHead>
|
<TableHead>默认目标</TableHead>
|
||||||
<TableHead>大小阈值</TableHead>
|
<TableHead>大小阈值</TableHead>
|
||||||
<TableHead>扩展名映射</TableHead>
|
<TableHead>扩展名映射</TableHead>
|
||||||
<TableHead>操作</TableHead>
|
<TableHead>操作</TableHead>
|
||||||
@ -456,14 +548,18 @@ export default function ConfigPage() {
|
|||||||
{typeof target === 'string' ? target : target.DefaultTarget}
|
{typeof target === 'string' ? target : target.DefaultTarget}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
{typeof target === 'object' && target.SizeThreshold ? target.SizeThreshold : '-'}
|
{typeof target === 'object' && target.SizeThreshold ? (
|
||||||
|
<span title={`${target.SizeThreshold} 字节`}>
|
||||||
|
{formatBytes(target.SizeThreshold)}
|
||||||
|
</span>
|
||||||
|
) : '-'}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
{typeof target === 'object' && target.ExtensionMap ? (
|
{typeof target === 'object' && target.ExtensionMap ? (
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
{Object.entries(target.ExtensionMap).map(([ext, url]) => (
|
{Object.entries(target.ExtensionMap).map(([ext, url]) => (
|
||||||
<div key={ext} className="flex items-center space-x-2">
|
<div key={ext} className="flex items-center space-x-2">
|
||||||
<span className="text-sm">{ext}: {url}</span>
|
<span className="text-sm" title={url}>{ext}: {truncateUrl(url)}</span>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
@ -482,10 +578,17 @@ export default function ConfigPage() {
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={() => handleExtensionMapEdit(path)}
|
onClick={() => handleEditPath(path, target)}
|
||||||
>
|
>
|
||||||
<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"
|
||||||
@ -503,16 +606,19 @@ export default function ConfigPage() {
|
|||||||
<Dialog open={extensionMapDialogOpen} onOpenChange={setExtensionMapDialogOpen}>
|
<Dialog open={extensionMapDialogOpen} onOpenChange={setExtensionMapDialogOpen}>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>编辑扩展名映射</DialogTitle>
|
<DialogTitle>添加扩展名映射</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>扩展名 (如: jpg,png)</Label>
|
<Label>扩展名</Label>
|
||||||
<Input
|
<Input
|
||||||
value={newExtension.ext}
|
value={newExtension.ext}
|
||||||
onChange={(e) => setNewExtension({ ...newExtension, ext: e.target.value })}
|
onChange={(e) => setNewExtension({ ...newExtension, ext: e.target.value })}
|
||||||
placeholder="jpg,png"
|
placeholder="jpg,png,webp"
|
||||||
/>
|
/>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
多个扩展名用逗号分隔,不需要包含点号
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>目标 URL</Label>
|
<Label>目标 URL</Label>
|
||||||
@ -521,6 +627,9 @@ export default function ConfigPage() {
|
|||||||
onChange={(e) => setNewExtension({ ...newExtension, target: e.target.value })}
|
onChange={(e) => setNewExtension({ ...newExtension, target: e.target.value })}
|
||||||
placeholder="https://example.com"
|
placeholder="https://example.com"
|
||||||
/>
|
/>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
当文件大小超过阈值且扩展名匹配时,将使用此地址
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button onClick={addExtensionMap}>添加</Button>
|
<Button onClick={addExtensionMap}>添加</Button>
|
||||||
</div>
|
</div>
|
||||||
@ -671,3 +780,42 @@ export default function ConfigPage() {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 辅助函数:格式化字节大小
|
||||||
|
const formatBytes = (bytes: number) => {
|
||||||
|
if (bytes === 0) return '0 B'
|
||||||
|
const k = 1024
|
||||||
|
const sizes = ['B', 'KB', 'MB', 'GB']
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||||
|
return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 辅助函数:截断 URL
|
||||||
|
const truncateUrl = (url: string) => {
|
||||||
|
if (url.length > 30) {
|
||||||
|
return url.substring(0, 27) + '...'
|
||||||
|
}
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
|
||||||
|
// 辅助函数:单位转换
|
||||||
|
const convertToBytes = (value: number, unit: 'B' | 'KB' | 'MB' | 'GB'): number => {
|
||||||
|
const multipliers = {
|
||||||
|
'B': 1,
|
||||||
|
'KB': 1024,
|
||||||
|
'MB': 1024 * 1024,
|
||||||
|
'GB': 1024 * 1024 * 1024
|
||||||
|
}
|
||||||
|
return value * multipliers[unit]
|
||||||
|
}
|
||||||
|
|
||||||
|
const convertBytesToUnit = (bytes: number): { value: number, unit: 'B' | 'KB' | 'MB' | 'GB' } => {
|
||||||
|
if (bytes === 0) return { value: 0, unit: 'MB' }
|
||||||
|
const k = 1024
|
||||||
|
const sizes: Array<'B' | 'KB' | 'MB' | 'GB'> = ['B', 'KB', 'MB', 'GB']
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||||
|
return {
|
||||||
|
value: Number((bytes / Math.pow(k, i)).toFixed(2)),
|
||||||
|
unit: sizes[i]
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user