From 435c819c7752094795eb980b3261a1c34d58f8af Mon Sep 17 00:00:00 2001 From: Awuqing <3184394176@qq.com> Date: Thu, 2 Apr 2026 13:39:43 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96:=20=E5=AD=98=E5=82=A8?= =?UTF-8?q?=E7=B1=BB=E5=9E=8B=E4=B8=8B=E6=8B=89=E6=A1=86=E5=88=86=E7=B1=BB?= =?UTF-8?q?=E4=B8=AD=E6=96=87=E6=A0=87=E6=B3=A8=20+=20=E5=8E=BB=E9=87=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题:API 返回的 rclone 后端纯英文技术名难辨别,且和内置类型存在重复 (如 rclone 的 drive 和内置的 google_drive)。 修复: - 前端静态定义分类+中文标注(常用/云存储/网盘/文件传输/企业存储/自建存储) - 排除工具类后端(alias/cache/http/archive 等)和重复后端(drive→用google_drive) - Select 使用 OptGroup 按分组渲染,搜索仍支持英文/中文关键词 - 常用类型(S3/阿里云/SFTP 等)置顶,其余按分类排列 --- .../StorageTargetFormDrawer.tsx | 40 +++--- .../storage-targets/field-config.ts | 122 +++++++++++++++--- 2 files changed, 128 insertions(+), 34 deletions(-) diff --git a/web/src/components/storage-targets/StorageTargetFormDrawer.tsx b/web/src/components/storage-targets/StorageTargetFormDrawer.tsx index 9fb65ad..e31d6fb 100644 --- a/web/src/components/storage-targets/StorageTargetFormDrawer.tsx +++ b/web/src/components/storage-targets/StorageTargetFormDrawer.tsx @@ -1,6 +1,6 @@ import { Alert, Button, Divider, Drawer, Input, Select, Space, Switch, Typography } from '@arco-design/web-react' import { useEffect, useMemo, useState } from 'react' -import { getStorageTargetFieldConfigs, getStorageTargetTypeLabel, isBuiltinType, builtinTypeOptions } from './field-config' +import { getStorageTargetFieldConfigs, getStorageTargetTypeLabel, isBuiltinType, buildAllTypeOptions } from './field-config' import type { StorageConnectionTestResult, StorageTargetDetail, StorageTargetPayload, StorageTargetType } from '../../types/storage-targets' import { listRcloneBackends, type RcloneBackendInfo } from '../../services/rclone' @@ -56,17 +56,18 @@ export function StorageTargetFormDrawer({ setTestResult(null) }, [initialValue, visible]) - // 合并类型选项:内置 + 全部 rclone 后端 - const allTypeOptions = useMemo(() => { - const builtinValues = new Set(builtinTypeOptions.map((o) => o.value)) - const rcloneOptions = rcloneBackends - .filter((b) => !builtinValues.has(b.name) && b.name !== 'local' && b.name !== 'rclone') - .map((b) => ({ label: `${b.name.toUpperCase()} — ${b.description}`, value: b.name })) - return [ - ...builtinTypeOptions.map((o) => ({ ...o, label: o.label, value: o.value as string })), - ...rcloneOptions, - ] - }, [rcloneBackends]) + // 构建分类的类型选项(去重、中文标注) + const allTypeOptions = useMemo(() => buildAllTypeOptions(rcloneBackends), [rcloneBackends]) + + // 按分组聚合,用于 Select 的 OptGroup 渲染 + const groupedOptions = useMemo(() => { + const groups: Record = {} + for (const opt of allTypeOptions) { + if (!groups[opt.group]) groups[opt.group] = [] + groups[opt.group].push({ label: opt.label, value: opt.value }) + } + return groups + }, [allTypeOptions]) // 当前类型是否为非内置(rclone 动态后端) const isDynamicType = !isBuiltinType(draft.type) @@ -179,17 +180,24 @@ export function StorageTargetFormDrawer({
diff --git a/web/src/components/storage-targets/field-config.ts b/web/src/components/storage-targets/field-config.ts index f9d6ef5..7fd6a8a 100644 --- a/web/src/components/storage-targets/field-config.ts +++ b/web/src/components/storage-targets/field-config.ts @@ -1,6 +1,9 @@ import type { StorageTargetFieldConfig, StorageTargetType } from '../../types/storage-targets' -// 内置类型的静态字段配置(定制化配置结构) +// --------------------------------------------------------------------------- +// 内置类型的静态字段配置 +// --------------------------------------------------------------------------- + const BUILTIN_FIELD_CONFIG: Record = { local_disk: [ { key: 'basePath', label: '基础目录', type: 'input', required: true, placeholder: '/data/backups', description: 'BackupX 将在该目录下创建和管理备份文件。' }, @@ -55,34 +58,117 @@ const BUILTIN_FIELD_CONFIG: Record = { const BUILTIN_TYPES = new Set(Object.keys(BUILTIN_FIELD_CONFIG)) -/** 是否为内置类型 */ export function isBuiltinType(type: StorageTargetType): boolean { return BUILTIN_TYPES.has(type) } -/** 获取静态字段配置 */ export function getStorageTargetFieldConfigs(type: StorageTargetType): StorageTargetFieldConfig[] { return BUILTIN_FIELD_CONFIG[type] ?? [] } -const BUILTIN_LABELS: Record = { +// --------------------------------------------------------------------------- +// 存储类型完整列表(分类、中文标注、去重) +// --------------------------------------------------------------------------- + +export interface TypeOption { + label: string + value: string + group: string +} + +// rclone 后端中不适合做存储目标的(工具类/代理类/只读类) +const EXCLUDED_BACKENDS = new Set([ + 'alias', 'cache', 'http', 'archive', 'memory', 'tardigrade', // tardigrade = storj 别名 + 'union', 'crypt', 'chunker', 'compress', 'hasher', 'combine', + 'local', // 用内置 local_disk 替代 + 'drive', // 用内置 google_drive 替代(避免和 rclone 的 drive 重复) +]) + +// 内置类型(带中文标签的定制化类型,优先展示) +const BUILTIN_OPTIONS: TypeOption[] = [ + { label: '本地磁盘', value: 'local_disk', group: '常用' }, + { label: 'S3 兼容存储(AWS / MinIO / 阿里云 / 腾讯云等)', value: 's3', group: '常用' }, + { label: '阿里云 OSS', value: 'aliyun_oss', group: '常用' }, + { label: '腾讯云 COS', value: 'tencent_cos', group: '常用' }, + { label: '七牛云 Kodo', value: 'qiniu_kodo', group: '常用' }, + { label: 'Google Drive', value: 'google_drive', group: '常用' }, + { label: 'WebDAV(Nextcloud / 坚果云等)', value: 'webdav', group: '常用' }, + { label: 'FTP / FTPS', value: 'ftp', group: '常用' }, +] + +// rclone 后端的中文标注(仅标注常见的,其余用原始描述) +const RCLONE_LABELS: Record = { + sftp: { label: 'SFTP(SSH 文件传输)', group: '文件传输' }, + smb: { label: 'SMB / CIFS(Windows 共享)', group: '文件传输' }, + azureblob: { label: 'Azure Blob 存储', group: '云存储' }, + azurefiles: { label: 'Azure Files 存储', group: '云存储' }, + 'google cloud storage': { label: 'Google Cloud Storage(GCS)', group: '云存储' }, + b2: { label: 'Backblaze B2', group: '云存储' }, + swift: { label: 'OpenStack Swift', group: '云存储' }, + dropbox: { label: 'Dropbox', group: '网盘' }, + onedrive: { label: 'Microsoft OneDrive', group: '网盘' }, + box: { label: 'Box', group: '网盘' }, + pcloud: { label: 'pCloud', group: '网盘' }, + mega: { label: 'MEGA', group: '网盘' }, + 'google photos': { label: 'Google Photos', group: '网盘' }, + yandex: { label: 'Yandex Disk', group: '网盘' }, + pikpak: { label: 'PikPak', group: '网盘' }, + iclouddrive: { label: 'iCloud Drive', group: '网盘' }, + jottacloud: { label: 'Jottacloud', group: '网盘' }, + hidrive: { label: 'HiDrive', group: '网盘' }, + protondrive: { label: 'Proton Drive', group: '网盘' }, + mailru: { label: 'Mail.ru Cloud', group: '网盘' }, + sugarsync: { label: 'SugarSync', group: '网盘' }, + putio: { label: 'Put.io', group: '网盘' }, + zoho: { label: 'Zoho WorkDrive', group: '网盘' }, + internxt: { label: 'Internxt Drive', group: '网盘' }, + seafile: { label: 'Seafile', group: '自建存储' }, + storj: { label: 'Storj 去中心化存储', group: '云存储' }, + hdfs: { label: 'Hadoop HDFS', group: '企业存储' }, + oracleobjectstorage: { label: 'Oracle 对象存储', group: '云存储' }, + qingstor: { label: '青云 QingStor', group: '云存储' }, + sharefile: { label: 'Citrix ShareFile', group: '企业存储' }, + filefabric: { label: 'Enterprise File Fabric', group: '企业存储' }, + netstorage: { label: 'Akamai NetStorage', group: '企业存储' }, + sia: { label: 'Sia 去中心化存储', group: '云存储' }, + koofr: { label: 'Koofr / Digi Storage', group: '网盘' }, + opendrive: { label: 'OpenDrive', group: '网盘' }, +} + +/** 构建完整类型选项列表(内置 + rclone,去重+分类) */ +export function buildAllTypeOptions(rcloneBackends: { name: string; description: string }[]): TypeOption[] { + const result = [...BUILTIN_OPTIONS] + const existingValues = new Set(BUILTIN_OPTIONS.map((o) => o.value)) + + for (const backend of rcloneBackends) { + if (EXCLUDED_BACKENDS.has(backend.name) || existingValues.has(backend.name)) continue + // 也排除和内置类型实际是同一后端的(如 rclone 的 s3, ftp, webdav 已被内置覆盖) + existingValues.add(backend.name) + + const meta = RCLONE_LABELS[backend.name] + result.push({ + label: meta?.label ?? `${backend.name} — ${backend.description}`, + value: backend.name, + group: meta?.group ?? '其他', + }) + } + + return result +} + +// --------------------------------------------------------------------------- +// 类型标签 +// --------------------------------------------------------------------------- + +const TYPE_LABELS: Record = { local_disk: '本地磁盘', google_drive: 'Google Drive', s3: 'S3 Compatible', webdav: 'WebDAV', aliyun_oss: '阿里云 OSS', tencent_cos: '腾讯云 COS', - qiniu_kodo: '七牛云 Kodo', ftp: 'FTP', rclone: 'Rclone', + qiniu_kodo: '七牛云 Kodo', ftp: 'FTP', + sftp: 'SFTP', smb: 'SMB', azureblob: 'Azure Blob', dropbox: 'Dropbox', + onedrive: 'OneDrive', b2: 'Backblaze B2', mega: 'MEGA', pcloud: 'pCloud', + box: 'Box', swift: 'Swift', pikpak: 'PikPak', } export function getStorageTargetTypeLabel(type: StorageTargetType): string { - return BUILTIN_LABELS[type] || type.toUpperCase() + return TYPE_LABELS[type] || type.toUpperCase() } - -/** 内置类型选项(下拉框"常用"分组) */ -export const builtinTypeOptions = [ - { label: '本地磁盘', value: 'local_disk' }, - { label: '阿里云 OSS', value: 'aliyun_oss' }, - { label: '腾讯云 COS', value: 'tencent_cos' }, - { label: '七牛云 Kodo', value: 'qiniu_kodo' }, - { label: 'S3 Compatible', value: 's3' }, - { label: 'Google Drive', value: 'google_drive' }, - { label: 'WebDAV', value: 'webdav' }, - { label: 'FTP', value: 'ftp' }, -]