feat(storage): implement storage management API and enhance storage mode handling

This commit is contained in:
shiyu
2025-06-09 12:12:15 +08:00
parent 4ef4b2056b
commit 0a6fe70537
43 changed files with 2449 additions and 907 deletions

View File

@@ -20,6 +20,13 @@ export interface PaginatedResult<T> {
code: number;
}
// 通用批量删除结果
export interface BatchDeleteResult {
successCount: number;
failedCount: number;
failedIds?: number[];
}
export const BASE_URL = import.meta.env.PROD ? '/api' : 'http://localhost:5153/api';
export async function fetchApi<T = any>(

View File

@@ -8,4 +8,5 @@ export * from './pictureApi';
export * from './pictureManagementApi';
export * from './tagApi';
export * from './userManagementApi';
export * from './vectorDbApi';
export * from './vectorDbApi';
export * from './storageManagementApi';

View File

@@ -1,5 +1,4 @@
import { fetchApi, type BaseResult, type PaginatedResult } from './fetchClient';
import { type BatchDeleteResult } from './userManagementApi';
import { fetchApi, type BaseResult, type BatchDeleteResult, type PaginatedResult } from './fetchClient';
// 日志级别枚举

View File

@@ -40,6 +40,7 @@ export interface PictureResponse {
permission: number;
albumId?: number;
albumName?: string;
storageModeName:string;
}
// 收藏请求

View File

@@ -1,6 +1,5 @@
import { fetchApi, type BaseResult, type PaginatedResult } from './fetchClient';
import { fetchApi, type BaseResult, type BatchDeleteResult, type PaginatedResult } from './fetchClient';
import { type PictureResponse } from './pictureApi';
import { type BatchDeleteResult } from './userManagementApi';
// 获取图片列表

View File

@@ -0,0 +1,156 @@
import { fetchApi, type BaseResult, type BatchDeleteResult, type PaginatedResult } from './fetchClient';
export enum StorageTypeEnum {
Local = 0,
Telegram = 1,
S3 = 2,
Cos = 3,
WebDAV = 4,
}
export const StorageTypeLabels: Record<StorageTypeEnum, string> = {
[StorageTypeEnum.Local]: "本地存储",
[StorageTypeEnum.Telegram]: "Telegram",
[StorageTypeEnum.S3]: "S3 对象存储",
[StorageTypeEnum.Cos]: "腾讯云 COS",
[StorageTypeEnum.WebDAV]: "WebDAV",
};
export interface StorageModeResponse {
id: number;
name: string;
storageType: StorageTypeEnum;
storageTypeName: string;
configurationJson?: string;
isEnabled: boolean;
createdAt: Date;
updatedAt: Date;
}
export interface CreateStorageModeRequest {
name: string;
storageType: StorageTypeEnum;
configurationJson?: string;
isEnabled: boolean;
}
export interface UpdateStorageModeRequest {
id: number;
name: string;
storageType: StorageTypeEnum;
configurationJson?: string;
isEnabled: boolean;
}
export interface StorageTypeResponse {
value: StorageTypeEnum;
name: string;
}
export interface StorageModeFilterRequest {
page?: number;
pageSize?: number;
searchQuery?: string;
storageType?: StorageTypeEnum;
isEnabled?: boolean;
}
// 获取存储模式列表
export const getStorageModes = async (
filters: StorageModeFilterRequest = {}
): Promise<PaginatedResult<StorageModeResponse>> => {
const { page = 1, pageSize = 10, searchQuery, storageType, isEnabled } = filters;
const params = new URLSearchParams({
page: page.toString(),
pageSize: pageSize.toString(),
});
if (searchQuery) params.append('searchQuery', searchQuery);
if (storageType !== undefined) params.append('storageType', storageType.toString());
if (isEnabled !== undefined) params.append('isEnabled', isEnabled.toString());
const response = await fetchApi(`/management/storage/get_modes?${params.toString()}`);
return response as PaginatedResult<StorageModeResponse>;
};
// 根据ID获取单个存储模式
export const getStorageModeById = async (id: number): Promise<BaseResult<StorageModeResponse>> => {
return fetchApi<StorageModeResponse>(
`/management/storage/get_mode/${id}`,
{ method: 'GET' }
);
};
// 创建存储模式
export const createStorageMode = async (
data: CreateStorageModeRequest
): Promise<BaseResult<StorageModeResponse>> => {
return fetchApi<StorageModeResponse>(
'/management/storage/create_mode',
{
method: 'POST',
body: JSON.stringify(data)
}
);
};
// 更新存储模式
export const updateStorageMode = async (
data: UpdateStorageModeRequest
): Promise<BaseResult<StorageModeResponse>> => {
return fetchApi<StorageModeResponse>(
'/management/storage/update_mode',
{
method: 'POST',
body: JSON.stringify(data)
}
);
};
// 删除存储模式
export const deleteStorageMode = async (id: number): Promise<BaseResult<boolean>> => {
return fetchApi<boolean>(
'/management/storage/delete_mode',
{
method: 'POST',
body: JSON.stringify(id) // 后端期望body中直接是id
}
);
};
// 批量删除存储模式
export const batchDeleteStorageModes = async (
ids: number[]
): Promise<BaseResult<BatchDeleteResult>> => {
return fetchApi<BatchDeleteResult>(
'/management/storage/batch_delete_modes',
{
method: 'POST',
body: JSON.stringify(ids)
}
);
};
// 获取所有可用的存储类型
export const getStorageTypes = async (): Promise<BaseResult<StorageTypeResponse[]>> => {
return fetchApi<StorageTypeResponse[]>(
'/management/storage/get_storage_types',
{ method: 'GET' }
);
};
// 获取默认存储模式ID
export const getDefaultStorageModeId = async (): Promise<BaseResult<number | null>> => {
return fetchApi<number | null>(
'/management/storage/get_default_mode_id',
{ method: 'GET' }
);
};
// 设置默认存储模式
export const setDefaultStorageMode = async (id: number): Promise<BaseResult<boolean>> => {
return fetchApi<boolean>(
`/management/storage/set_default_mode/${id}`,
{ method: 'POST' }
);
};

View File

@@ -1,4 +1,4 @@
import { fetchApi, type BaseResult, type PaginatedResult } from './fetchClient';
import { fetchApi, type BaseResult, type BatchDeleteResult, type PaginatedResult } from './fetchClient';
export type UserRole = "Administrator" | "User" | "";
@@ -34,13 +34,6 @@ export interface AdminUpdateUserRequest {
role?: string;
}
// 批量删除结果
export interface BatchDeleteResult {
successCount: number;
failedCount: number;
failedIds?: number[];
}
// 用户筛选请求参数
export interface UserFilterRequest {
page?: number;

View File

@@ -91,10 +91,17 @@ const ImageCard: React.FC<ImageCardProps> = ({
{!selectable && (
<>
<div className="custom-card-indicators">
<div className="custom-card-permission" style={{
backgroundColor: permissionTypeMap[image.permission]?.color || 'rgba(0, 0, 0, 0.6)'
}}>
{permissionTypeMap[image.permission]?.icon} {permissionTypeMap[image.permission]?.label || '公开'}
<div className="custom-card-left-indicators">
<div className="custom-card-permission" style={{
backgroundColor: permissionTypeMap[image.permission]?.color || 'rgba(0, 0, 0, 0.6)'
}}>
{permissionTypeMap[image.permission]?.icon} {permissionTypeMap[image.permission]?.label || '公开'}
</div>
{image.storageModeName && (
<div className="custom-card-storage-mode">
{image.storageModeName}
</div>
)}
</div>
<div className="custom-card-metadata">

View File

@@ -121,12 +121,19 @@
right: 0;
display: flex;
justify-content: space-between;
align-items: center; /* 确保垂直对齐 */
padding: 0 8px;
opacity: 0;
transition: opacity 0.35s ease;
z-index: 2;
}
.custom-card-left-indicators { /* 新增样式 */
display: flex;
align-items: center;
gap: 6px; /* 指示器之间的间距 */
}
.custom-card:hover .custom-card-indicators {
opacity: 1;
}
@@ -145,6 +152,20 @@
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}
.custom-card-storage-mode { /* 新增样式 */
background-color: rgba(0, 0, 0, 0.6);
color: white;
border-radius: 12px;
padding: 3px 8px;
font-size: 10px;
font-weight: 500;
display: flex;
align-items: center;
gap: 3px;
backdrop-filter: blur(4px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}
.custom-card-metadata {
background-color: rgba(0, 0, 0, 0.6);
color: white;

View File

@@ -0,0 +1,571 @@
import React, { useState, useEffect, useCallback } from 'react';
import {
Table, Button, Card, Input, Space, Modal, Form, message, Tag, Typography, Popconfirm, Row, Col, Select, Switch, Tooltip, Alert} from 'antd';
import {
DeleteOutlined, EditOutlined, SearchOutlined, ExclamationCircleOutlined, ReloadOutlined,
PlusOutlined, DatabaseOutlined, FilterOutlined, ClearOutlined, StarOutlined, StarFilled
} from '@ant-design/icons';
import {
getStorageModes, deleteStorageMode, createStorageMode, updateStorageMode, batchDeleteStorageModes, getStorageTypes,
getDefaultStorageModeId, setDefaultStorageMode,
type StorageModeResponse, type CreateStorageModeRequest, type UpdateStorageModeRequest, type StorageModeFilterRequest,
type StorageTypeResponse, StorageTypeEnum, StorageTypeLabels
} from '../../../api';
import { useOutletContext } from 'react-router';
import type { Breakpoint } from 'antd';
const { Title, Text } = Typography;
const { Option } = Select;
const { confirm } = Modal;
const StorageManagementPage: React.FC = () => {
const { isMobile } = useOutletContext<{ isMobile: boolean }>();
const [storageModes, setStorageModes] = useState<StorageModeResponse[]>([]);
const [availableStorageTypes, setAvailableStorageTypes] = useState<StorageTypeResponse[]>([]);
const [loading, setLoading] = useState(false);
const [total, setTotal] = useState(0);
const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
const [defaultStorageModeId, setDefaultStorageModeId] = useState<number | null>(null);
const [, setIsLoadingDefault] = useState(false);
const [settingDefaultModeId, setSettingDefaultModeId] = useState<number | null>(null);
const [filters, setFilters] = useState<StorageModeFilterRequest>({});
const [showFilters, setShowFilters] = useState(false);
const [filterForm] = Form.useForm();
const [isModalVisible, setIsModalVisible] = useState(false);
const [modalTitle, setModalTitle] = useState('');
const [editingMode, setEditingMode] = useState<StorageModeResponse | null>(null);
const [form] = Form.useForm();
const [currentStorageTypeForHelp, setCurrentStorageTypeForHelp] = useState<StorageTypeEnum | null>(null);
const fetchStorageTypes = useCallback(async () => {
try {
const response = await getStorageTypes();
if (response.success && response.data) {
setAvailableStorageTypes(response.data);
} else {
message.error(response.message || '获取存储类型失败');
}
} catch (error) {
message.error('获取存储类型失败,请检查网络');
}
}, []);
const fetchDefaultStorageModeId = useCallback(async () => {
try {
setIsLoadingDefault(true);
const response = await getDefaultStorageModeId();
if (response.success) {
setDefaultStorageModeId(response.data ?? null);
} else {
message.error(response.message || '获取默认存储模式失败');
}
} catch (error) {
message.error('获取默认存储模式失败,请检查网络');
} finally {
setIsLoadingDefault(false);
}
}, []);
const fetchStorageModes = useCallback(async (page = currentPage, size = pageSize, filterParams = filters) => {
setLoading(true);
try {
const response = await getStorageModes({
page,
pageSize: size,
...filterParams
});
if (response.success && response.data) {
setStorageModes(response.data.map(m => ({...m, createdAt: new Date(m.createdAt), updatedAt: new Date(m.updatedAt) })));
setTotal(response.totalCount || 0);
} else {
message.error(response.message || '获取存储模式列表失败');
}
} catch (error) {
message.error('获取存储模式列表失败,请检查网络连接');
} finally {
setLoading(false);
}
}, [currentPage, pageSize, filters]);
useEffect(() => {
fetchStorageTypes();
fetchStorageModes();
fetchDefaultStorageModeId();
}, [fetchStorageModes, fetchStorageTypes, fetchDefaultStorageModeId]);
const handlePageChange = (page: number, size?: number) => {
setCurrentPage(page);
if (size) setPageSize(size);
fetchStorageModes(page, size || pageSize, filters);
};
const handleFilter = async () => {
const values = await filterForm.validateFields();
const newFilters: StorageModeFilterRequest = {
searchQuery: values.searchQuery,
storageType: values.storageType,
isEnabled: typeof values.isEnabled === 'boolean' ? values.isEnabled : undefined,
};
setFilters(newFilters);
setCurrentPage(1);
fetchStorageModes(1, pageSize, newFilters);
};
const handleClearFilters = () => {
filterForm.resetFields();
setFilters({});
setCurrentPage(1);
fetchStorageModes(1, pageSize, {});
};
const handleQuickSearch = (searchQuery: string) => {
const newFilters = { ...filters, searchQuery };
setFilters(newFilters);
setCurrentPage(1);
fetchStorageModes(1, pageSize, newFilters);
};
const showCreateModal = () => {
setModalTitle('创建新存储模式');
setEditingMode(null);
setCurrentStorageTypeForHelp(null);
form.resetFields(); // This will clear all fields, including any 'configuration' fields
form.setFieldsValue({ isEnabled: true });
setIsModalVisible(true);
};
const showEditModal = (mode: StorageModeResponse) => {
setModalTitle('编辑存储模式');
setEditingMode(mode);
setCurrentStorageTypeForHelp(mode.storageType);
let parsedConfig = {};
if (mode.configurationJson) {
try {
parsedConfig = JSON.parse(mode.configurationJson);
} catch (e) {
message.error('解析现有配置JSON失败请检查数据格式。');
parsedConfig = {};
}
}
form.setFieldsValue({
name: mode.name,
storageType: mode.storageType,
isEnabled: mode.isEnabled,
configuration: parsedConfig,
});
setIsModalVisible(true);
};
const handleModalOk = async () => {
try {
const values = await form.validateFields();
const { name, storageType, isEnabled, configuration } = values;
const configToSave = configuration || {};
const commonData = {
name,
storageType,
configurationJson: JSON.stringify(configToSave),
isEnabled,
};
let response;
if (editingMode) {
const updateData: UpdateStorageModeRequest = { id: editingMode.id, ...commonData };
response = await updateStorageMode(updateData);
} else {
const createData: CreateStorageModeRequest = commonData;
response = await createStorageMode(createData);
}
if (response.success) {
message.success(editingMode ? '存储模式更新成功' : '存储模式创建成功');
fetchStorageModes(editingMode ? currentPage : 1);
setIsModalVisible(false);
} else {
message.error(response.message || (editingMode ? '更新失败' : '创建失败'));
}
} catch (errorInfo) {
console.error('Form validation failed:', errorInfo);
message.error('请检查表单输入。');
}
};
const handleDelete = async (id: number) => {
try {
const response = await deleteStorageMode(id);
if (response.success) {
message.success('存储模式删除成功');
fetchStorageModes(); // Refresh
} else {
message.error(response.message || '删除失败');
}
} catch (error) {
message.error('删除失败,请检查网络');
}
};
const handleBatchDelete = async () => {
if (selectedRowKeys.length === 0) {
message.warning('请选择要删除的存储模式');
return;
}
confirm({
title: `确定要删除 ${selectedRowKeys.length} 个存储模式吗?`,
icon: <ExclamationCircleOutlined />,
content: '此操作不可逆。如果存储模式仍被图片使用,则无法删除。',
okText: '确认',
okType: 'danger',
cancelText: '取消',
async onOk() {
try {
const response = await batchDeleteStorageModes(selectedRowKeys as number[]);
if (response.success && response.data) {
message.success(`成功删除 ${response.data.successCount} 个存储模式`);
if (response.data.failedCount > 0) {
message.warning(`${response.data.failedCount} 个存储模式删除失败 (可能仍在使用中)。失败ID: ${response.data.failedIds?.join(', ')}`);
}
setSelectedRowKeys([]);
fetchStorageModes(); // Refresh
} else {
message.error(response.message || '批量删除失败');
}
} catch (error) {
message.error('批量删除失败,请检查网络');
}
}
});
};
const handleSetDefault = async (id: number) => {
try {
setSettingDefaultModeId(id); // 开始为此特定项目设置默认
const response = await setDefaultStorageMode(id);
if (response.success) {
message.success('默认存储模式设置成功');
setDefaultStorageModeId(id);
// 可选: 如果默认状态会影响列表显示方式(除了星星图标),则重新获取列表
// fetchStorageModes(currentPage, pageSize, filters);
} else {
message.error(response.message || '设置默认存储模式失败');
}
} catch (error) {
message.error('设置默认存储模式失败,请检查网络');
} finally {
setSettingDefaultModeId(null); // 清除此特定项目的加载状态
}
};
const renderDynamicConfigFields = (storageType: StorageTypeEnum | null) => {
if (storageType === null || storageType === undefined) return null;
switch (storageType) {
case StorageTypeEnum.Local:
return (
<>
<Form.Item name={['configuration', 'BasePath']} label="基础路径 (BasePath)" rules={[{ required: true, message: '请输入基础路径' }]}>
<Input placeholder="例如: /path/to/your/uploads" />
</Form.Item>
<Form.Item name={['configuration', 'ServerUrl']} label="服务器URL (ServerUrl)" rules={[{ required: true, message: '请输入服务器URL' }]}>
<Input placeholder="例如: http://localhost:5000" />
</Form.Item>
<Form.Item name={['configuration', 'PublicBasePath']} label="公共基础路径 (PublicBasePath)" rules={[{ required: true, message: '请输入公共基础路径'}]}>
<Input placeholder="例如: /Uploads (用于拼接图片URL)" />
</Form.Item>
</>
);
case StorageTypeEnum.Telegram:
return (
<>
<Form.Item name={['configuration', 'BotToken']} label="机器人Token (BotToken)" rules={[{ required: true, message: '请输入机器人Token' }]}>
<Input.Password placeholder="您的Telegram机器人Token" />
</Form.Item>
<Form.Item name={['configuration', 'ChatId']} label="聊天ID (ChatId)" rules={[{ required: true, message: '请输入聊天ID' }]}>
<Input placeholder="目标聊天或频道的ID" />
</Form.Item>
<Form.Item name={['configuration', 'ProxyAddress']} label="代理地址 (ProxyAddress) (可选)">
<Input placeholder="例如: 127.0.0.1" />
</Form.Item>
<Form.Item name={['configuration', 'ProxyPort']} label="代理端口 (ProxyPort) (可选)">
<Input placeholder="例如: 1080" type="number" />
</Form.Item>
</>
);
case StorageTypeEnum.S3:
return (
<>
<Form.Item name={['configuration', 'AccessKey']} label="AccessKey" rules={[{ required: true, message: '请输入AccessKey' }]}>
<Input placeholder="您的S3 AccessKey" />
</Form.Item>
<Form.Item name={['configuration', 'SecretKey']} label="SecretKey" rules={[{ required: true, message: '请输入SecretKey' }]}>
<Input.Password placeholder="您的S3 SecretKey" />
</Form.Item>
<Form.Item name={['configuration', 'Endpoint']} label="Endpoint" rules={[{ required: true, message: '请输入Endpoint' }]}>
<Input placeholder="例如: s3.us-west-2.amazonaws.com" />
</Form.Item>
<Form.Item name={['configuration', 'Region']} label="区域 (Region)" rules={[{ required: true, message: '请输入区域' }]}>
<Input placeholder="例如: us-west-2" />
</Form.Item>
<Form.Item name={['configuration', 'BucketName']} label="存储桶名称 (BucketName)" rules={[{ required: true, message: '请输入存储桶名称' }]}>
<Input placeholder="您的S3存储桶名称" />
</Form.Item>
<Form.Item name={['configuration', 'UsePathStyleUrls']} label="使用路径样式URL (UsePathStyleUrls)" valuePropName="checked">
<Switch checkedChildren="是" unCheckedChildren="否" />
</Form.Item>
<Form.Item name={['configuration', 'CdnUrl']} label="CDN URL (可选)">
<Input placeholder="例如: https://cdn.example.com" />
</Form.Item>
</>
);
case StorageTypeEnum.Cos:
return (
<>
<Form.Item name={['configuration', 'Region']} label="区域 (Region)" rules={[{ required: true, message: '请输入区域' }]}>
<Input placeholder="例如: ap-guangzhou" />
</Form.Item>
<Form.Item name={['configuration', 'SecretId']} label="SecretId" rules={[{ required: true, message: '请输入SecretId' }]}>
<Input placeholder="您的腾讯云SecretId" />
</Form.Item>
<Form.Item name={['configuration', 'SecretKey']} label="SecretKey" rules={[{ required: true, message: '请输入SecretKey' }]}>
<Input.Password placeholder="您的腾讯云SecretKey" />
</Form.Item>
<Form.Item name={['configuration', 'BucketName']} label="存储桶名称 (BucketName)" rules={[{ required: true, message: '请输入存储桶名称' }]}>
<Input placeholder="格式: your-bucket-appid" />
</Form.Item>
<Form.Item name={['configuration', 'CdnUrl']} label="CDN URL (可选)">
<Input placeholder="例如: https://cdn.example.com" />
</Form.Item>
<Form.Item name={['configuration', 'PublicRead']} label="公共读 (PublicRead)" valuePropName="checked">
<Switch checkedChildren="是" unCheckedChildren="否" />
</Form.Item>
</>
);
case StorageTypeEnum.WebDAV:
return (
<>
<Form.Item name={['configuration', 'ServerUrl']} label="服务器URL (ServerUrl)" rules={[{ required: true, message: '请输入WebDAV服务器URL' }]}>
<Input placeholder="例如: https://dav.example.com" />
</Form.Item>
<Form.Item name={['configuration', 'BasePath']} label="基础路径 (BasePath)">
<Input placeholder="例如: uploads (在服务器上的相对路径)" />
</Form.Item>
<Form.Item name={['configuration', 'UserName']} label="用户名 (UserName)" rules={[{ required: true, message: '请输入用户名' }]}>
<Input placeholder="WebDAV用户名" />
</Form.Item>
<Form.Item name={['configuration', 'Password']} label="密码 (Password)" rules={[{ required: true, message: '请输入密码' }]}>
<Input.Password placeholder="WebDAV密码" />
</Form.Item>
<Form.Item name={['configuration', 'PublicUrl']} label="公共URL (PublicUrl) (可选)">
<Input placeholder="例如: https://public.example.com/dav (如果WebDAV内容可通过不同URL公开访问)" />
</Form.Item>
</>
);
default:
return <Alert message="此存储类型可能不需要额外配置,或配置界面暂未实现。" type="info" showIcon />;
}
};
const columns = [
{ title: 'ID', dataIndex: 'id', key: 'id', responsive: ['md'] as Breakpoint[] },
{
title: '名称',
dataIndex: 'name',
key: 'name',
render: (name: string, record: StorageModeResponse) => (
<Space>
{record.id === defaultStorageModeId && (
<Tooltip title="默认存储模式">
<StarFilled style={{ color: '#faad14' }} />
</Tooltip>
)}
{name}
</Space>
)
},
{
title: '类型', dataIndex: 'storageType', key: 'storageType',
render: (type: StorageTypeEnum) => <Tag color="blue">{StorageTypeLabels[type] || type}</Tag>,
},
{
title: '配置 (JSON)', dataIndex: 'configurationJson', key: 'configurationJson', responsive: ['lg'] as Breakpoint[],
render: (json?: string) => json ? <Tooltip title={json}><Text style={{ maxWidth: 200 }} ellipsis>{json}</Text></Tooltip> : <Text type="secondary"></Text>,
},
{
title: '启用状态', dataIndex: 'isEnabled', key: 'isEnabled',
render: (enabled: boolean) => <Tag color={enabled ? 'green' : 'red'}>{enabled ? '已启用' : '已禁用'}</Tag>,
},
{ title: '更新时间', dataIndex: 'updatedAt', key: 'updatedAt', responsive: ['lg'] as Breakpoint[], render: (date: Date) => date.toLocaleString() },
{
title: '操作', key: 'action',
render: (_: any, record: StorageModeResponse) => (
<Space size="small">
{record.isEnabled && record.id !== defaultStorageModeId && (
<Button
type="text"
icon={<StarOutlined />}
onClick={() => handleSetDefault(record.id)}
loading={settingDefaultModeId === record.id} // 使用新的行特定加载状态
title="设为默认"
>
{isMobile ? '' : '设为默认'}
</Button>
)}
<Button type="text" icon={<EditOutlined />} onClick={() => showEditModal(record)}>{isMobile ? '' : '编辑'}</Button>
<Popconfirm
title="确定删除此存储模式吗?"
onConfirm={() => handleDelete(record.id)}
okText="确定"
cancelText="取消"
disabled={record.id === defaultStorageModeId}
>
<Button
type="text"
danger
icon={<DeleteOutlined />}
disabled={record.id === defaultStorageModeId}
>
{isMobile ? '' : '删除'}
</Button>
</Popconfirm>
</Space>
),
},
];
return (
<div>
<Row gutter={[16, 16]} align="middle" justify="space-between">
<Col>
<Space align="center">
<DatabaseOutlined style={{ fontSize: 24 }} />
<Title level={2} style={{ margin: 0 }}></Title>
</Space>
<Text type="secondary" style={{ marginTop: 8, display: 'block' }}>
</Text>
</Col>
<Col>
{defaultStorageModeId && (
<Alert
type="info"
message={
<Space>
<StarFilled style={{ color: '#faad14' }} />
<span>ID: {defaultStorageModeId}</span>
</Space>
}
style={{ padding: '4px 12px' }}
/>
)}
{!defaultStorageModeId && (
<Alert
type="warning"
message="未设置默认存储模式"
description="请选择一个启用的存储模式设为默认"
showIcon
/>
)}
</Col>
</Row>
<Card style={{ marginTop: 16 }}>
<Row gutter={[16, 16]} justify="space-between" style={{ marginBottom: 16 }}>
<Col xs={24} sm={14} md={16}>
<Space wrap>
<Button type="primary" icon={<PlusOutlined />} onClick={showCreateModal}></Button>
<Button danger icon={<DeleteOutlined />} onClick={handleBatchDelete} disabled={selectedRowKeys.length === 0}></Button>
<Button icon={<ReloadOutlined />} onClick={() => fetchStorageModes(currentPage, pageSize, filters)}></Button>
<Button icon={<FilterOutlined />} onClick={() => setShowFilters(!showFilters)} type={showFilters ? 'primary' : 'default'}></Button>
</Space>
</Col>
<Col xs={24} sm={10} md={8}>
<Input.Search placeholder="搜索模式名称" allowClear enterButton={<SearchOutlined />} onSearch={handleQuickSearch} />
</Col>
</Row>
{showFilters && (
<Card size="small" style={{ marginBottom: 16, backgroundColor: '#fafafa' }}>
<Form form={filterForm} layout="inline" onFinish={handleFilter}>
<Form.Item name="searchQuery" label="名称">
<Input placeholder="模式名称" style={{ width: 150 }} />
</Form.Item>
<Form.Item name="storageType" label="类型">
<Select placeholder="选择类型" style={{ width: 150 }} allowClear>
{availableStorageTypes.map(st => <Option key={st.value} value={st.value}>{st.name}</Option>)}
</Select>
</Form.Item>
<Form.Item name="isEnabled" label="状态">
<Select placeholder="选择状态" style={{ width: 120 }} allowClear>
<Option value={true}></Option>
<Option value={false}></Option>
</Select>
</Form.Item>
<Form.Item>
<Space>
<Button type="primary" htmlType="submit" icon={<SearchOutlined />}></Button>
<Button icon={<ClearOutlined />} onClick={handleClearFilters}></Button>
</Space>
</Form.Item>
</Form>
</Card>
)}
<Table
rowKey="id"
columns={columns}
dataSource={storageModes}
loading={loading}
pagination={{
current: currentPage, pageSize: pageSize, total: total,
showSizeChanger: true, showQuickJumper: true, onChange: handlePageChange,
showTotal: (t) => `${t} 条记录`,
}}
rowSelection={{ selectedRowKeys, onChange: (keys) => setSelectedRowKeys(keys) }}
size={isMobile ? "small" : "middle"}
scroll={{ x: 'max-content' }}
/>
</Card>
<Modal
title={modalTitle}
open={isModalVisible}
onOk={handleModalOk}
onCancel={() => setIsModalVisible(false)}
okText={editingMode ? "更新" : "创建"}
cancelText="取消"
width={isMobile ? '90%' : 700}
>
<Form form={form} layout="vertical" initialValues={{ isEnabled: true, configuration: {} }}>
<Form.Item name="name" label="模式名称" rules={[{ required: true, message: '请输入模式名称' }]}>
<Input placeholder="例如:主图片存储、备份存储等" />
</Form.Item>
<Form.Item name="storageType" label="存储类型" rules={[{ required: true, message: '请选择存储类型' }]}>
<Select
placeholder="选择一个存储类型"
onChange={(value) => {
setCurrentStorageTypeForHelp(value as StorageTypeEnum);
form.setFieldsValue({ configuration: {} });
}}
>
{availableStorageTypes.map(st => <Option key={st.value} value={st.value}>{StorageTypeLabels[st.value as StorageTypeEnum] || st.name}</Option>)}
</Select>
</Form.Item>
{renderDynamicConfigFields(currentStorageTypeForHelp)}
<Form.Item name="isEnabled" label="启用状态" valuePropName="checked">
<Switch checkedChildren="已启用" unCheckedChildren="已禁用" />
</Form.Item>
</Form>
</Modal>
</div>
);
};
export default StorageManagementPage;

View File

@@ -1,16 +1,16 @@
import React from 'react';
import { Tabs, Form, Input, Button, Select, Space, Divider, Typography } from 'antd';
import { Tabs, Form, Input, Button, Space, Divider, Slider, InputNumber } from 'antd'; // InputNumber added
import {
ApiOutlined, RocketOutlined, PictureOutlined, SaveOutlined,
SafetyCertificateOutlined, LockOutlined, GlobalOutlined, SettingOutlined,
CloudServerOutlined, DatabaseOutlined, UploadOutlined} from '@ant-design/icons';
DatabaseOutlined, UploadOutlined
} from '@ant-design/icons';
import ConfigFormItem from './ConfigFormItem';
import ConfigSection from './ConfigSection';
import VectorDbConfig from './VectorDbConfig';
import VectorDbConfig from './VectorDbConfig';
const { TabPane } = Tabs;
const { Option } = Select;
const { Title, Paragraph } = Typography;
// const { Option } = Select; // Removed
interface ConfigStructure {
[key: string]: {
@@ -24,17 +24,13 @@ interface ConfigTabsProps {
isMobile: boolean;
activeKey: string;
onTabChange: (key: string) => void;
storageType: string;
onStorageTypeChange: (type: string) => void;
formsMap: Record<string, any>;
allDescriptions: Record<string, Record<string, string>>;
onSaveSingleConfig: (formInstance: any, groupName: string, key: string) => Promise<void>;
onSaveAllForGroup: (formInstance: any, groupName: string, itemKeys: string[]) => Promise<void>;
onBaseSaveConfig: (group: string, key: string, value: string) => Promise<boolean>;
setConfigs: React.Dispatch<React.SetStateAction<ConfigStructure>>;
storageOptions: Array<{ value: string; label: string; icon: React.ReactNode; }>;
imageFormatOptions: Array<{ value: string; label: string; description: string; }>;
imageQualityOptions: Array<{ value: string; label: string; description: string; }>;
// imageQualityOptions: Array<{ value: string; label: string; description: string; }>; // Removed
}
const ConfigTabs: React.FC<ConfigTabsProps> = ({
@@ -43,17 +39,13 @@ const ConfigTabs: React.FC<ConfigTabsProps> = ({
isMobile,
activeKey,
onTabChange,
storageType,
onStorageTypeChange,
formsMap,
allDescriptions,
onSaveSingleConfig,
onSaveAllForGroup,
onBaseSaveConfig,
setConfigs,
storageOptions,
imageFormatOptions,
imageQualityOptions,
// imageQualityOptions, // Removed
}) => {
const renderConfigFormItems = (formInstance: any, groupName: string, itemKeys: string[]) => {
@@ -306,245 +298,93 @@ const ConfigTabs: React.FC<ConfigTabsProps> = ({
)
},
{
key: 'Storage',
label: '存储设置',
icon: <CloudServerOutlined />,
key: 'Upload',
label: '上传设置',
icon: <UploadOutlined />,
children: (
<>
<ConfigSection
title="存储类型配置"
icon={<DatabaseOutlined />}
description="配置系统默认使用的文件存储方式"
isMobile={isMobile}
>
<div style={{
display: 'grid',
gridTemplateColumns: isMobile ? '1fr' : 'repeat(auto-fit, minmax(300px, 1fr))',
gap: isMobile ? 12 : 16,
marginBottom: 0
}}>
<div>
<div style={{ marginBottom: 8, fontSize: 14, fontWeight: 500, color: '#666' }}>
</div>
<Select
value={configs.Storage?.DefaultStorage || 'Local'}
onChange={(value) => onBaseSaveConfig('Storage', 'DefaultStorage', value)}
style={{ width: '100%' }}
size="large"
placeholder="选择登录用户的默认存储方式"
optionLabelProp="label"
>
{storageOptions.map(option => (
<Option key={option.value} value={option.value} label={option.label}>
<div style={{ display: 'flex', alignItems: 'center' }}>
{option.icon}
<span style={{ marginLeft: 8 }}>{option.label}</span>
</div>
</Option>
))}
</Select>
<div style={{ fontSize: 12, color: '#999', marginTop: 4 }}>
{allDescriptions.Storage?.DefaultStorage}
</div>
</div>
<div>
<div style={{ marginBottom: 8, fontSize: 14, fontWeight: 500, color: '#666' }}>
</div>
<Select
value={configs.Storage?.AnonymousDefaultStorage || 'Local'}
onChange={(value) => onBaseSaveConfig('Storage', 'AnonymousDefaultStorage', value)}
style={{ width: '100%' }}
size="large"
placeholder="选择匿名用户的默认存储方式"
optionLabelProp="label"
>
{storageOptions.map(option => (
<Option key={option.value} value={option.value} label={option.label}>
<div style={{ display: 'flex', alignItems: 'center' }}>
{option.icon}
<span style={{ marginLeft: 8 }}>{option.label}</span>
</div>
</Option>
))}
</Select>
<div style={{ fontSize: 12, color: '#999', marginTop: 4 }}>
{allDescriptions.Storage?.AnonymousDefaultStorage}
</div>
</div>
</div>
</ConfigSection>
<ConfigSection
title="上传设置配置"
title="上传参数配置"
icon={<UploadOutlined />}
description="配置文件上传处理方式和图片转换参数"
isMobile={isMobile}
>
<div style={{
display: 'grid',
gridTemplateColumns: isMobile ? '1fr' : 'repeat(auto-fit, minmax(300px, 1fr))',
gap: isMobile ? 12 : 16,
gridTemplateColumns: '1fr',
gap: isMobile ? 20 : 24,
marginBottom: 0
}}>
<div>
<div style={{ marginBottom: 8, fontSize: 14, fontWeight: 500, color: '#666' }}>
(px)
</div>
<Select
value={configs.Upload?.DefaultImageFormat || 'Original'}
onChange={(value) => onBaseSaveConfig('Upload', 'DefaultImageFormat', value)}
<InputNumber
min={100}
max={1000}
step={50}
value={parseInt(configs.Upload?.ThumbnailMaxWidth || '400', 10)}
onChange={(value) => {
if (value !== null) {
onBaseSaveConfig('Upload', 'ThumbnailMaxWidth', value.toString())
}
}}
style={{ width: '100%' }}
size="large"
placeholder="选择上传图片的默认处理格式"
optionLabelProp="label"
>
{imageFormatOptions.map(option => (
<Option key={option.value} value={option.value} label={option.label}>
<div>
<div>{option.label}</div>
<div style={{ fontSize: '12px', color: '#999' }}>{option.description}</div>
</div>
</Option>
))}
</Select>
/>
<div style={{ fontSize: 12, color: '#999', marginTop: 4 }}>
{allDescriptions.Upload?.DefaultImageFormat}
{allDescriptions.Upload?.ThumbnailMaxWidth}
</div>
</div>
<div>
<div style={{ marginBottom: 8, fontSize: 14, fontWeight: 500, color: '#666' }}>
</div>
<Select
value={configs.Upload?.DefaultImageQuality || '95'}
onChange={(value) => onBaseSaveConfig('Upload', 'DefaultImageQuality', value)}
style={{ width: '100%' }}
size="large"
placeholder="选择图片压缩质量"
optionLabelProp="label"
>
{imageQualityOptions.map(option => (
<Option key={option.value} value={option.value} label={option.label}>
<div>
<div>{option.label}</div>
<div style={{ fontSize: '12px', color: '#999' }}>{option.description}</div>
</div>
</Option>
))}
</Select>
<div style={{ fontSize: 12, color: '#999', marginTop: 4 }}>
{allDescriptions.Upload?.DefaultImageQuality}
<Slider
min={30}
max={90}
step={5}
value={parseInt(configs.Upload?.ThumbnailCompressionQuality || '75', 10)}
onChange={(value) => onBaseSaveConfig('Upload', 'ThumbnailCompressionQuality', value.toString())}
style={{ margin: isMobile ? '0 5px' : '0 10px' }}
tooltip={{
formatter: value => `${value}%`
}}
marks={{
30: '30%',
60: '60%',
90: '90%'
}}
/>
<div style={{ fontSize: 12, color: '#999', marginTop: 16, textAlign: 'center' }}>
{allDescriptions.Upload?.ThumbnailCompressionQuality}
</div>
</div>
</div>
</ConfigSection>
<ConfigSection
title="存储服务配置"
icon={<CloudServerOutlined />}
description="配置各种外部存储服务的连接参数"
isMobile={isMobile}
>
<div style={{ marginBottom: 16 }}>
<div style={{ marginBottom: 8, fontSize: 14, fontWeight: 500, color: '#666' }}>
</div>
<Select
value={storageType}
onChange={onStorageTypeChange}
style={{ width: isMobile ? '100%' : '300px' }}
size="large"
placeholder="选择需要配置的存储服务类型"
optionLabelProp="label"
>
{storageOptions.map(option => (
<Option key={option.value} value={option.value} label={option.label}>
<div style={{ display: 'flex', alignItems: 'center' }}>
{option.icon}
<span style={{ marginLeft: 8 }}>{option.label}</span>
</div>
</Option>
))}
</Select>
<div style={{ fontSize: 12, color: '#999', marginTop: 4, marginBottom: 16 }}>
</div>
</div>
<div style={{ border: '1px solid #f0f0f0', borderRadius: 6, padding: isMobile ? 12 : 16, backgroundColor: '#fafafa' }}>
{storageType === 'Local' && (
<div style={{ textAlign: 'center', color: '#999', padding: '30px 0' }}>
<DatabaseOutlined style={{ fontSize: 32, color: '#52c41a', marginBottom: 16 }} />
<Title level={5}></Title>
<Paragraph type="secondary"></Paragraph>
<div>
<div style={{ marginBottom: 8, fontSize: 14, fontWeight: 500, color: '#666' }}>
</div>
)}
{storageType === 'Telegram' && (
<Form form={formsMap.TelegramStorage} layout="vertical" size={isMobile ? "middle" : "large"}>
{renderConfigFormItems(formsMap.TelegramStorage, "Storage", ["TelegramStorageBotToken", "TelegramStorageChatId", "TelegramProxyAddress", "TelegramProxyPort", "TelegramProxyUsername", "TelegramProxyPassword"])}
<Divider style={{ margin: '12px 0 20px' }} />
<Form.Item style={{ marginBottom: 0, textAlign: 'center' }}>
<Button
type="primary"
icon={<SaveOutlined />}
onClick={() => onSaveAllForGroup(formsMap.TelegramStorage, "Storage", ["TelegramStorageBotToken", "TelegramStorageChatId", "TelegramProxyAddress", "TelegramProxyPort", "TelegramProxyUsername", "TelegramProxyPassword"])}
style={{ width: isMobile ? '100%' : '240px' }}
>
Telegram
</Button>
</Form.Item>
</Form>
)}
{storageType === 'S3' && (
<Form form={formsMap.S3Storage} layout="vertical" size={isMobile ? "middle" : "large"}>
{renderConfigFormItems(formsMap.S3Storage, "Storage", ["S3StorageAccessKey", "S3StorageSecretKey", "S3StorageBucketName", "S3StorageRegion", "S3StorageEndpoint", "S3StorageCdnUrl", "S3StorageUsePathStyleUrls"])}
<Divider style={{ margin: '12px 0 20px' }} />
<Form.Item style={{ marginBottom: 0, textAlign: 'center' }}>
<Button
type="primary"
icon={<SaveOutlined />}
onClick={() => onSaveAllForGroup(formsMap.S3Storage, "Storage", ["S3StorageAccessKey", "S3StorageSecretKey", "S3StorageBucketName", "S3StorageRegion", "S3StorageEndpoint", "S3StorageCdnUrl", "S3StorageUsePathStyleUrls"])}
style={{ width: isMobile ? '100%' : '240px' }}
>
S3
</Button>
</Form.Item>
</Form>
)}
{storageType === 'Cos' && (
<Form form={formsMap.CosStorage} layout="vertical" size={isMobile ? "middle" : "large"}>
{renderConfigFormItems(formsMap.CosStorage, "Storage", ["CosStorageSecretId", "CosStorageSecretKey", "CosStorageToken", "CosStorageBucketName", "CosStorageRegion", "CosStorageCdnUrl"])}
<Divider style={{ margin: '12px 0 20px' }} />
<Form.Item style={{ marginBottom: 0, textAlign: 'center' }}>
<Button
type="primary"
icon={<SaveOutlined />}
onClick={() => onSaveAllForGroup(formsMap.CosStorage, "Storage", ["CosStorageSecretId", "CosStorageSecretKey", "CosStorageToken", "CosStorageBucketName", "CosStorageRegion", "CosStorageCdnUrl"])}
style={{ width: isMobile ? '100%' : '240px' }}
>
COS
</Button>
</Form.Item>
</Form>
)}
{storageType === 'WebDAV' && (
<Form form={formsMap.WebDAVStorage} layout="vertical" size={isMobile ? "middle" : "large"}>
{renderConfigFormItems(formsMap.WebDAVStorage, "Storage", ["WebDAVServerUrl", "WebDAVUserName", "WebDAVPassword", "WebDAVBasePath", "WebDAVPublicUrl"])}
<Divider style={{ margin: '12px 0 20px' }} />
<Form.Item style={{ marginBottom: 0, textAlign: 'center' }}>
<Button
type="primary"
icon={<SaveOutlined />}
onClick={() => onSaveAllForGroup(formsMap.WebDAVStorage, "Storage", ["WebDAVServerUrl", "WebDAVUserName", "WebDAVPassword", "WebDAVBasePath", "WebDAVPublicUrl"])}
style={{ width: isMobile ? '100%' : '240px' }}
>
WebDAV
</Button>
</Form.Item>
</Form>
)}
<Slider
min={50}
max={100}
step={5}
value={parseInt(configs.Upload?.HighQualityImageCompressionQuality || '95', 10)}
onChange={(value) => onBaseSaveConfig('Upload', 'HighQualityImageCompressionQuality', value.toString())}
style={{ margin: isMobile ? '0 5px' : '0 10px' }}
tooltip={{
formatter: value => `${value}%`
}}
marks={{
50: '50%',
75: '75%',
100: '100%'
}}
/>
<div style={{ fontSize: 12, color: '#999', marginTop: 16, textAlign: 'center' }}>
{allDescriptions.Upload?.HighQualityImageCompressionQuality}
</div>
</div>
</div>
</ConfigSection>
</>

View File

@@ -1,7 +1,6 @@
import React, { useEffect, useState, useRef, useCallback } from 'react';
import { Card, message, Spin, Button, Upload, Modal, Space, Tooltip, Form, Typography, notification } from 'antd';
import {
CloudOutlined, DatabaseOutlined, CloudServerOutlined, GlobalOutlined,
DownloadOutlined, UploadOutlined, QuestionCircleOutlined,
SettingOutlined,
CheckCircleOutlined
@@ -46,37 +45,10 @@ const allDescriptions: Record<string, Record<string, string>> = {
ServerUrl: '服务器URL',
MaxConcurrentTasks: '后台任务最大并发处理数量 (例如: 图像分析、标签生成等)'
},
Storage: {
DefaultStorage: '已登录用户上传文件时的默认存储位置',
AnonymousDefaultStorage: '未登录用户上传文件时的默认存储位置',
TelegramStorageBotToken: 'Telegram 机器人令牌',
TelegramStorageChatId: 'Telegram 聊天ID',
TelegramProxyAddress: '代理服务器地址 (例如: 127.0.0.1)',
TelegramProxyPort: '代理服务器端口 (例如: 1080)',
TelegramProxyUsername: '代理用户名 (可选)',
TelegramProxyPassword: '代理密码 (可选)',
S3StorageAccessKey: 'S3访问密钥',
S3StorageSecretKey: 'S3私有密钥',
S3StorageBucketName: 'S3存储桶名称',
S3StorageRegion: 'S3区域 (例如:us-east-1)',
S3StorageEndpoint: 'S3端点URL (可选,默认为AWS S3)',
S3StorageCdnUrl: 'CDN URL (可选,用于加速文件访问)',
S3StorageUsePathStyleUrls: '使用路径形式URLs (true/false,兼容非AWS服务)',
CosStorageSecretId: '腾讯云COS密钥ID',
CosStorageSecretKey: '腾讯云COS私有密钥',
CosStorageToken: '腾讯云COS临时令牌(可选)',
CosStorageBucketName: 'COS存储桶名称',
CosStorageRegion: 'COS区域 (例如:ap-shanghai)',
CosStorageCdnUrl: 'CDN URL (可选,用于加速文件访问)',
WebDAVServerUrl: 'WebDAV 服务器 URL (例如: https://dav.example.com)',
WebDAVUserName: 'WebDAV 用户名',
WebDAVPassword: 'WebDAV 密码',
WebDAVBasePath: 'WebDAV 基础路径 (例如: files/upload)',
WebDAVPublicUrl: 'WebDAV 公共访问 URL (可选,用于文件访问)',
},
Upload: {
DefaultImageFormat: '上传图片时的默认处理格式,选择合适的格式可以优化存储和显示',
DefaultImageQuality: '适用于JPEG和WebP格式的图片质量设置越高图片质量越好但文件越大'
HighQualityImageCompressionQuality: '高清图片的压缩质量,越高图片质量越好但文件越大。范围 50-100。',
ThumbnailMaxWidth: '缩略图的最大宽度(像素),例如设置为 400。',
ThumbnailCompressionQuality: '缩略图的压缩质量,用于平衡文件大小和清晰度。范围 30-90。'
}
};
@@ -86,7 +58,6 @@ const System: React.FC = () => {
const [loading, setLoading] = useState(true);
const [configs, setConfigs] = useState<ConfigStructure>({});
const [activeKey, setActiveKey] = useState('AI');
const [storageType, setStorageType] = useState('Telegram');
const [backupLoading, setBackupLoading] = useState(false);
const [restoreLoading, setRestoreLoading] = useState(false);
const [restoreModalVisible, setRestoreModalVisible] = useState(false);
@@ -100,10 +71,6 @@ const System: React.FC = () => {
const [jwtForm] = Form.useForm();
const [authForm] = Form.useForm();
const [appSettingsForm] = Form.useForm();
const [telegramForm] = Form.useForm();
const [s3Form] = Form.useForm();
const [cosForm] = Form.useForm();
const [webDAVForm] = Form.useForm();
const [uploadForm] = Form.useForm();
const formsMap: Record<string, any> = {
@@ -111,10 +78,6 @@ const System: React.FC = () => {
Jwt: jwtForm,
Authentication: authForm,
AppSettings: appSettingsForm,
TelegramStorage: telegramForm,
S3Storage: s3Form,
CosStorage: cosForm,
WebDAVStorage: webDAVForm,
Upload: uploadForm,
};
@@ -145,20 +108,11 @@ const System: React.FC = () => {
setConfigs(configGroups);
setSecretFields(secretFieldsMap);
if (configGroups.Storage?.DefaultStorage) {
setStorageType(configGroups.Storage.DefaultStorage);
}
// 更高效地更新表单值
Object.keys(configGroups).forEach(group => {
let formInstanceKey = group;
if (group === "Storage") {
} else if (group.endsWith("Storage") && formsMap[group]) {
formInstanceKey = group;
}
const formInstance = formsMap[formInstanceKey] || (group === "Storage" ? formsMap[`${configGroups.Storage.DefaultStorage}Storage`] : null);
const formInstance = formsMap[formInstanceKey];
if (formInstance) {
const initialGroupValues: Record<string, string> = {};
@@ -456,34 +410,6 @@ const System: React.FC = () => {
}
};
// 存储类型选项
const storageOptions = [
{ value: 'Local', label: '本地存储', icon: <DatabaseOutlined style={{ color: '#52c41a' }} /> },
{ value: 'Telegram', label: 'Telegram 频道', icon: <CloudOutlined style={{ color: '#0088cc' }} /> },
{ value: 'S3', label: '亚马逊 S3', icon: <CloudServerOutlined style={{ color: '#ff9900' }} /> },
{ value: 'Cos', label: '腾讯云 COS', icon: <CloudServerOutlined style={{ color: '#00a4ff' }} /> },
{ value: 'WebDAV', label: 'WebDAV 存储', icon: <GlobalOutlined style={{ color: '#1890ff' }} /> },
];
// 上传格式选项
const imageFormatOptions = [
{ value: 'Original', label: '保持原始格式', description: '不改变原始图片格式' },
{ value: 'Jpeg', label: '转换为JPEG', description: '适合照片,文件较小但有损压缩' },
{ value: 'Png', label: '转换为PNG', description: '适合图形,无损但文件较大' },
{ value: 'Webp', label: '转换为WebP', description: '现代格式,体积小且质量好' },
];
// 图片质量选项
const imageQualityOptions = [
{ value: '100', label: '100% - 最高质量', description: '无损压缩,文件较大' },
{ value: '95', label: '95% - 高质量', description: '几乎无损,推荐用于高质量需求' },
{ value: '90', label: '90% - 优质', description: '良好平衡,推荐一般用途' },
{ value: '85', label: '85% - 良好', description: '适合网页展示,节省空间' },
{ value: '80', label: '80% - 节省空间', description: '明显压缩但质量可接受' },
{ value: '75', label: '75% - 平衡', description: '显著减小文件大小' },
{ value: '70', label: '70% - 压缩', description: '最大压缩,质量较低' },
];
useEffect(() => {
fetchConfigs();
}, []);
@@ -546,17 +472,12 @@ const System: React.FC = () => {
isMobile={isMobile}
activeKey={activeKey}
onTabChange={setActiveKey}
storageType={storageType}
onStorageTypeChange={setStorageType}
formsMap={formsMap}
allDescriptions={allDescriptions}
onSaveSingleConfig={handleSaveSingleConfig}
onSaveAllForGroup={handleSaveAllForGroup}
onBaseSaveConfig={baseSaveConfig}
setConfigs={setConfigs}
storageOptions={storageOptions}
imageFormatOptions={imageFormatOptions}
imageQualityOptions={imageQualityOptions}
/>
)}

View File

@@ -24,6 +24,7 @@ import UserManagement from '../pages/admin/users/Index';
import PictureManagement from '../pages/admin/pictures/Index';
import UserDetail from '../pages/admin/users/UserDetail';
import AdminLogManagement from '../pages/admin/log/Index';
import StorageManagementPage from '../pages/admin/storage/StorageManagement';
export interface RouteConfig {
path: string;
@@ -182,6 +183,17 @@ const routes: RouteConfig[] = [
breadcrumb: {
title: '日志中心'
}
},
{
path: 'storage',
key: 'admin-storage',
icon: <FileTextOutlined />,
label: '存储配置',
element: <StorageManagementPage />,
area: 'admin',
breadcrumb: {
title: '存储配置'
}
},
{
path: 'system',