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:
wood chen 2025-02-17 06:57:31 +08:00
parent dd57ec2bd5
commit 9de17edcbd

View File

@ -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) => {
setPathDialogOpen(true)
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
})
}
}
const addOrUpdatePath = () => {
if (!config) return if (!config) return
const { path, defaultTarget, sizeThreshold, extensionMap } = newPathData const data = editingPathData ? editingPathData : newPathData
if (!path || !defaultTarget) { 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>
<Input <div className="flex items-center space-x-2">
type="number" <Input
value={newPathData.sizeThreshold} type="number"
onChange={(e) => setNewPathData({ ...newPathData, sizeThreshold: parseInt(e.target.value) })} value={editingPathData ? editingPathData.sizeThreshold : newPathData.sizeThreshold}
/> 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>
<p className="text-sm text-muted-foreground">
使
</p>
</div> </div>
<Button onClick={addPath}></Button> <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>
@ -670,4 +779,43 @@ export default function ConfigPage() {
</Card> </Card>
</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]
}
} }