+
+ {opt.key}{opt.required ? ' *' : ''}
+ {opt.isPassword ? (
+ updateConfig(opt.key, v)} />
+ ) : (
+ updateConfig(opt.key, v)} />
+ )}
+ {opt.label && (
+ {opt.label}
+ )}
+
+ )
+ }
+
+ // 渲染动态字段(rclone 后端)— 必填优先,可选折叠
function renderDynamicFields() {
+ const requiredOptions = dynamicBackend?.options.filter((opt) => opt.required) ?? []
+ const optionalOptions = dynamicBackend?.options.filter((opt) => !opt.required) ?? []
+
return (
<>
@@ -147,19 +167,19 @@ export function StorageTargetFormDrawer({
updateConfig('root', v)} />
远端根路径、桶名或挂载点,留空使用根目录
- {dynamicBackend && dynamicBackend.options.length > 0 && dynamicBackend.options.map((opt) => (
-
- {opt.key}{opt.required ? ' *' : ''}
- {opt.isPassword ? (
- updateConfig(opt.key, v)} />
- ) : (
- updateConfig(opt.key, v)} />
- )}
- {opt.label && (
- {opt.label}
- )}
-
- ))}
+ {requiredOptions.map(renderDynamicOption)}
+ {optionalOptions.length > 0 && (
+
+ 高级配置({optionalOptions.length} 个可选项)}
+ name="advanced"
+ >
+
+ {optionalOptions.map(renderDynamicOption)}
+
+
+
+ )}
>
)
}
diff --git a/web/src/pages/nodes/NodesPage.tsx b/web/src/pages/nodes/NodesPage.tsx
index 75fe2d5..1fbe828 100644
--- a/web/src/pages/nodes/NodesPage.tsx
+++ b/web/src/pages/nodes/NodesPage.tsx
@@ -3,10 +3,10 @@ import {
Table, Button, Space, Tag, Typography, PageHeader, Modal, Input, Message, Badge, Popconfirm, Card, Descriptions, Empty
} from '@arco-design/web-react'
import {
- IconPlus, IconDelete, IconDesktop, IconCloudDownload
+ IconPlus, IconDelete, IconDesktop, IconCloudDownload, IconEdit
} from '@arco-design/web-react/icon'
import type { NodeSummary } from '../../types/nodes'
-import { listNodes, createNode, deleteNode } from '../../services/nodes'
+import { listNodes, createNode, deleteNode, updateNode } from '../../services/nodes'
const { Title, Text } = Typography
@@ -17,6 +17,11 @@ export default function NodesPage() {
const [newNodeName, setNewNodeName] = useState('')
const [newToken, setNewToken] = useState('')
+ // 编辑状态
+ const [editVisible, setEditVisible] = useState(false)
+ const [editNode, setEditNode] = useState
(null)
+ const [editName, setEditName] = useState('')
+
const fetchNodes = useCallback(async () => {
setLoading(true)
try {
@@ -56,6 +61,21 @@ export default function NodesPage() {
}
}
+ const handleEdit = async () => {
+ if (!editNode || !editName.trim()) {
+ Message.warning('请输入节点名称')
+ return
+ }
+ try {
+ await updateNode(editNode.id, { name: editName.trim() })
+ Message.success('节点更新成功')
+ setEditVisible(false)
+ fetchNodes()
+ } catch {
+ Message.error('更新节点失败')
+ }
+ }
+
const columns = [
{
title: '节点名称',
@@ -110,15 +130,22 @@ export default function NodesPage() {
},
{
title: '操作',
- width: 80,
- render: (_: unknown, record: NodeSummary) => {
- if (record.isLocal) return -
- return (
- handleDelete(record.id)}>
- } size="small" />
-
- )
- },
+ width: 120,
+ render: (_: unknown, record: NodeSummary) => (
+
+ }
+ size="small"
+ onClick={() => { setEditNode(record); setEditName(record.name); setEditVisible(true) }}
+ />
+ {!record.isLocal && (
+ handleDelete(record.id)}>
+ } size="small" />
+
+ )}
+
+ ),
},
]
@@ -145,6 +172,7 @@ export default function NodesPage() {
/>
+ {/* 添加节点弹窗 */}
)}
+
+ {/* 编辑节点弹窗 */}
+ setEditVisible(false)}
+ onOk={handleEdit}
+ okText="保存"
+ cancelText="取消"
+ >
+
+ 节点名称
+
+
+
)
}
diff --git a/web/src/pages/settings/SettingsPage.tsx b/web/src/pages/settings/SettingsPage.tsx
index 0ae8665..244a8c6 100644
--- a/web/src/pages/settings/SettingsPage.tsx
+++ b/web/src/pages/settings/SettingsPage.tsx
@@ -1,6 +1,6 @@
import { Badge, Button, Card, Descriptions, Grid, Link, Message, PageHeader, Space, Tag, Typography } from '@arco-design/web-react'
import { useEffect, useState } from 'react'
-import { fetchSystemInfo, checkUpdate, applyUpdate, type SystemInfo, type UpdateCheckResult } from '../../services/system'
+import { fetchSystemInfo, checkUpdate, type SystemInfo, type UpdateCheckResult } from '../../services/system'
import { resolveErrorMessage } from '../../utils/error'
import { formatDuration } from '../../utils/format'
@@ -24,7 +24,6 @@ export function SettingsPage() {
const [error, setError] = useState('')
const [updateResult, setUpdateResult] = useState