Files
MuMuAINovel/frontend/src/pages/Settings.tsx
T

761 lines
30 KiB
TypeScript
Raw Normal View History

2025-10-30 16:53:50 +08:00
import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
2025-10-31 17:23:25 +08:00
import { Card, Form, Input, Button, Select, Slider, InputNumber, message, Space, Typography, Spin, Modal, Tooltip, Alert, Grid } from 'antd';
import { SettingOutlined, SaveOutlined, DeleteOutlined, ReloadOutlined, ArrowLeftOutlined, InfoCircleOutlined, CheckCircleOutlined, CloseCircleOutlined, ThunderboltOutlined } from '@ant-design/icons';
2025-10-30 16:53:50 +08:00
import { settingsApi } from '../services/api';
import type { SettingsUpdate } from '../types';
const { Title, Paragraph } = Typography;
const { Option } = Select;
2025-10-31 17:23:25 +08:00
const { useBreakpoint } = Grid;
2025-10-30 16:53:50 +08:00
export default function SettingsPage() {
const navigate = useNavigate();
2025-10-31 17:23:25 +08:00
const screens = useBreakpoint();
const isMobile = !screens.md; // md断点是768px
2025-10-30 16:53:50 +08:00
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const [initialLoading, setInitialLoading] = useState(true);
const [hasSettings, setHasSettings] = useState(false);
const [isDefaultSettings, setIsDefaultSettings] = useState(false);
const [modelOptions, setModelOptions] = useState<Array<{ value: string; label: string; description: string }>>([]);
const [fetchingModels, setFetchingModels] = useState(false);
const [modelsFetched, setModelsFetched] = useState(false);
const [testingApi, setTestingApi] = useState(false);
const [testResult, setTestResult] = useState<{
success: boolean;
message: string;
response_time_ms?: number;
response_preview?: string;
error?: string;
error_type?: string;
suggestions?: string[];
} | null>(null);
const [showTestResult, setShowTestResult] = useState(false);
2025-10-30 16:53:50 +08:00
useEffect(() => {
loadSettings();
}, []);
const loadSettings = async () => {
setInitialLoading(true);
try {
const settings = await settingsApi.getSettings();
form.setFieldsValue(settings);
// 判断是否为默认设置(id='0'表示来自.env的默认配置)
if (settings.id === '0' || !settings.id) {
setIsDefaultSettings(true);
setHasSettings(false);
} else {
setIsDefaultSettings(false);
setHasSettings(true);
}
} catch (error: any) {
// 如果404表示还没有设置,使用默认值
if (error?.response?.status === 404) {
setHasSettings(false);
setIsDefaultSettings(true);
form.setFieldsValue({
api_provider: 'openai',
api_base_url: 'https://api.openai.com/v1',
model_name: 'gpt-4',
temperature: 0.7,
max_tokens: 2000,
});
} else {
message.error('加载设置失败');
}
} finally {
setInitialLoading(false);
}
};
const handleSave = async (values: SettingsUpdate) => {
setLoading(true);
try {
await settingsApi.saveSettings(values);
message.success('设置已保存');
setHasSettings(true);
setIsDefaultSettings(false);
} catch (error) {
message.error('保存设置失败');
} finally {
setLoading(false);
}
};
const handleReset = () => {
Modal.confirm({
title: '重置设置',
content: '确定要重置为默认值吗?',
okText: '确定',
cancelText: '取消',
onOk: () => {
form.setFieldsValue({
api_provider: 'openai',
api_key: '',
api_base_url: 'https://api.openai.com/v1',
model_name: 'gpt-4',
temperature: 0.7,
max_tokens: 2000,
});
message.info('已重置为默认值,请点击保存');
},
});
};
const handleDelete = () => {
Modal.confirm({
title: '删除设置',
content: '确定要删除所有设置吗?此操作不可恢复。',
okText: '确定',
cancelText: '取消',
okType: 'danger',
onOk: async () => {
setLoading(true);
try {
await settingsApi.deleteSettings();
message.success('设置已删除');
setHasSettings(false);
form.resetFields();
} catch (error) {
message.error('删除设置失败');
} finally {
setLoading(false);
}
},
});
};
const apiProviders = [
{ value: 'openai', label: 'OpenAI', defaultUrl: 'https://api.openai.com/v1' },
// { value: 'azure', label: 'Azure OpenAI', defaultUrl: 'https://YOUR-RESOURCE.openai.azure.com' },
2025-10-30 16:53:50 +08:00
{ value: 'anthropic', label: 'Anthropic', defaultUrl: 'https://api.anthropic.com' },
// { value: 'custom', label: '自定义', defaultUrl: '' },
2025-10-30 16:53:50 +08:00
];
const handleProviderChange = (value: string) => {
const provider = apiProviders.find(p => p.value === value);
if (provider && provider.defaultUrl) {
form.setFieldValue('api_base_url', provider.defaultUrl);
}
// 清空模型列表,需要重新获取
setModelOptions([]);
setModelsFetched(false);
};
const handleFetchModels = async (silent: boolean = false) => {
const apiKey = form.getFieldValue('api_key');
const apiBaseUrl = form.getFieldValue('api_base_url');
const provider = form.getFieldValue('api_provider');
if (!apiKey || !apiBaseUrl) {
if (!silent) {
message.warning('请先填写 API 密钥和 API 地址');
}
return;
}
setFetchingModels(true);
try {
const response = await settingsApi.getAvailableModels({
api_key: apiKey,
api_base_url: apiBaseUrl,
provider: provider || 'openai'
});
setModelOptions(response.models);
setModelsFetched(true);
if (!silent) {
message.success(`成功获取 ${response.count || response.models.length} 个可用模型`);
}
} catch (error: any) {
const errorMsg = error?.response?.data?.detail || '获取模型列表失败';
if (!silent) {
message.error(errorMsg);
}
setModelOptions([]);
setModelsFetched(true); // 即使失败也标记为已尝试,避免重复请求
} finally {
setFetchingModels(false);
}
};
const handleModelSelectFocus = () => {
// 如果还没有获取过模型列表,自动获取
if (!modelsFetched && !fetchingModels) {
handleFetchModels(true); // silent模式,不显示成功消息
}
};
const handleTestConnection = async () => {
const apiKey = form.getFieldValue('api_key');
const apiBaseUrl = form.getFieldValue('api_base_url');
const provider = form.getFieldValue('api_provider');
const modelName = form.getFieldValue('model_name');
if (!apiKey || !apiBaseUrl || !provider || !modelName) {
message.warning('请先填写完整的配置信息');
return;
}
setTestingApi(true);
setTestResult(null);
try {
const result = await settingsApi.testApiConnection({
api_key: apiKey,
api_base_url: apiBaseUrl,
provider: provider,
model_name: modelName
});
setTestResult(result);
setShowTestResult(true);
if (result.success) {
message.success(`测试成功!响应时间: ${result.response_time_ms}ms`);
} else {
message.error('API 测试失败,请查看详细信息');
}
} catch (error: any) {
const errorMsg = error?.response?.data?.detail || '测试请求失败';
message.error(errorMsg);
setTestResult({
success: false,
message: '测试请求失败',
error: errorMsg,
error_type: 'RequestError',
suggestions: ['请检查网络连接', '请确认后端服务是否正常运行']
});
setShowTestResult(true);
} finally {
setTestingApi(false);
}
};
2025-10-30 16:53:50 +08:00
return (
<div style={{
minHeight: '100vh',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
2025-10-31 17:23:25 +08:00
padding: isMobile ? '16px 12px' : '40px 24px'
2025-10-30 16:53:50 +08:00
}}>
2025-10-31 17:23:25 +08:00
<div style={{
maxWidth: isMobile ? '100%' : 800,
margin: '0 auto'
}}>
2025-10-30 16:53:50 +08:00
<Card
variant="borderless"
style={{
background: 'rgba(255, 255, 255, 0.95)',
2025-10-31 17:23:25 +08:00
borderRadius: isMobile ? 12 : 16,
2025-10-30 16:53:50 +08:00
boxShadow: '0 8px 32px rgba(0, 0, 0, 0.1)',
}}
2025-10-31 17:23:25 +08:00
styles={{
body: {
padding: isMobile ? '16px' : '24px'
}
}}
2025-10-30 16:53:50 +08:00
>
2025-10-31 17:23:25 +08:00
<Space direction="vertical" size={isMobile ? 'middle' : 'large'} style={{ width: '100%' }}>
2025-10-30 16:53:50 +08:00
{/* 标题栏 */}
2025-10-31 17:23:25 +08:00
<div style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
flexWrap: 'wrap',
gap: '8px'
}}>
<Space size={isMobile ? 'small' : 'middle'}>
2025-10-30 16:53:50 +08:00
<Button
icon={<ArrowLeftOutlined />}
onClick={() => navigate('/')}
type="text"
2025-10-31 17:23:25 +08:00
size={isMobile ? 'middle' : 'large'}
2025-10-30 16:53:50 +08:00
/>
2025-10-31 17:23:25 +08:00
<Title
level={isMobile ? 4 : 2}
style={{
margin: 0,
fontSize: isMobile ? '18px' : undefined
}}
>
2025-10-30 16:53:50 +08:00
<SettingOutlined style={{ marginRight: 8, color: '#667eea' }} />
2025-10-31 17:23:25 +08:00
{isMobile ? 'API 设置' : 'AI API 设置'}
2025-10-30 16:53:50 +08:00
</Title>
</Space>
</div>
2025-10-31 17:23:25 +08:00
<Paragraph
type="secondary"
style={{
marginBottom: 0,
fontSize: isMobile ? '13px' : '14px',
lineHeight: isMobile ? '1.5' : '1.6'
}}
>
2025-10-30 16:53:50 +08:00
AI API接口参数AI功能
</Paragraph>
{/* 默认配置提示 */}
{isDefaultSettings && (
<Alert
message="使用 .env 文件中的默认配置"
description={
2025-10-31 17:23:25 +08:00
<div style={{ fontSize: isMobile ? '12px' : '14px' }}>
2025-10-30 16:53:50 +08:00
<p style={{ margin: '8px 0' }}>
<code>.env</code>
</p>
<p style={{ margin: '8px 0 0 0' }}>
"保存设置" <code>.env</code>
</p>
</div>
}
type="info"
showIcon
2025-10-31 17:23:25 +08:00
style={{ marginBottom: isMobile ? 12 : 16 }}
2025-10-30 16:53:50 +08:00
/>
)}
{/* 已保存配置提示 */}
{hasSettings && !isDefaultSettings && (
<Alert
message="使用已保存的个人配置"
type="success"
showIcon
2025-10-31 17:23:25 +08:00
style={{ marginBottom: isMobile ? 12 : 16 }}
2025-10-30 16:53:50 +08:00
/>
)}
{/* 表单 */}
<Spin spinning={initialLoading}>
<Form
form={form}
layout="vertical"
onFinish={handleSave}
autoComplete="off"
>
<Form.Item
label={
2025-10-31 17:23:25 +08:00
<Space size={4}>
2025-10-30 16:53:50 +08:00
<span>API </span>
<Tooltip title="选择你的AI服务提供商">
2025-10-31 17:23:25 +08:00
<InfoCircleOutlined style={{ color: '#8c8c8c', fontSize: isMobile ? '12px' : '14px' }} />
2025-10-30 16:53:50 +08:00
</Tooltip>
</Space>
}
name="api_provider"
rules={[{ required: true, message: '请选择API提供商' }]}
>
2025-10-31 17:23:25 +08:00
<Select size={isMobile ? 'middle' : 'large'} onChange={handleProviderChange}>
2025-10-30 16:53:50 +08:00
{apiProviders.map(provider => (
<Option key={provider.value} value={provider.value}>
{provider.label}
</Option>
))}
</Select>
</Form.Item>
<Form.Item
label={
2025-10-31 17:23:25 +08:00
<Space size={4}>
2025-10-30 16:53:50 +08:00
<span>API </span>
<Tooltip title="你的API密钥,将加密存储">
2025-10-31 17:23:25 +08:00
<InfoCircleOutlined style={{ color: '#8c8c8c', fontSize: isMobile ? '12px' : '14px' }} />
2025-10-30 16:53:50 +08:00
</Tooltip>
</Space>
}
name="api_key"
rules={[{ required: true, message: '请输入API密钥' }]}
>
<Input.Password
2025-10-31 17:23:25 +08:00
size={isMobile ? 'middle' : 'large'}
2025-10-30 16:53:50 +08:00
placeholder="sk-..."
autoComplete="new-password"
/>
</Form.Item>
<Form.Item
label={
2025-10-31 17:23:25 +08:00
<Space size={4}>
2025-10-30 16:53:50 +08:00
<span>API </span>
<Tooltip title="API的基础URL地址">
2025-10-31 17:23:25 +08:00
<InfoCircleOutlined style={{ color: '#8c8c8c', fontSize: isMobile ? '12px' : '14px' }} />
2025-10-30 16:53:50 +08:00
</Tooltip>
</Space>
}
name="api_base_url"
rules={[
{ required: true, message: '请输入API地址' },
{ type: 'url', message: '请输入有效的URL' }
]}
>
<Input
2025-10-31 17:23:25 +08:00
size={isMobile ? 'middle' : 'large'}
2025-10-30 16:53:50 +08:00
placeholder="https://api.openai.com/v1"
/>
</Form.Item>
<Form.Item
label={
2025-10-31 17:23:25 +08:00
<Space size={4}>
2025-10-30 16:53:50 +08:00
<span></span>
<Tooltip title="AI模型的名称,如 gpt-4, gpt-3.5-turbo">
2025-10-31 17:23:25 +08:00
<InfoCircleOutlined style={{ color: '#8c8c8c', fontSize: isMobile ? '12px' : '14px' }} />
2025-10-30 16:53:50 +08:00
</Tooltip>
</Space>
}
name="model_name"
rules={[{ required: true, message: '请输入或选择模型名称' }]}
>
<Select
2025-10-31 17:23:25 +08:00
size={isMobile ? 'middle' : 'large'}
2025-10-30 16:53:50 +08:00
showSearch
2025-10-31 17:23:25 +08:00
placeholder={isMobile ? "选择模型" : "输入模型名称或点击获取"}
2025-10-30 16:53:50 +08:00
optionFilterProp="label"
loading={fetchingModels}
onFocus={handleModelSelectFocus}
filterOption={(input, option) =>
(option?.label ?? '').toLowerCase().includes(input.toLowerCase()) ||
(option?.description ?? '').toLowerCase().includes(input.toLowerCase())
}
dropdownRender={(menu) => (
<>
{menu}
{fetchingModels && (
2025-10-31 17:23:25 +08:00
<div style={{ padding: '8px 12px', color: '#8c8c8c', textAlign: 'center', fontSize: isMobile ? '12px' : '14px' }}>
2025-10-30 16:53:50 +08:00
<Spin size="small" /> ...
</div>
)}
{!fetchingModels && modelOptions.length === 0 && modelsFetched && (
2025-10-31 17:23:25 +08:00
<div style={{ padding: '8px 12px', color: '#ff4d4f', textAlign: 'center', fontSize: isMobile ? '12px' : '14px' }}>
2025-10-30 16:53:50 +08:00
API
</div>
)}
{!fetchingModels && modelOptions.length === 0 && !modelsFetched && (
2025-10-31 17:23:25 +08:00
<div style={{ padding: '8px 12px', color: '#8c8c8c', textAlign: 'center', fontSize: isMobile ? '12px' : '14px' }}>
2025-10-30 16:53:50 +08:00
</div>
)}
</>
)}
notFoundContent={
fetchingModels ? (
2025-10-31 17:23:25 +08:00
<div style={{ padding: '8px 12px', textAlign: 'center', fontSize: isMobile ? '12px' : '14px' }}>
2025-10-30 16:53:50 +08:00
<Spin size="small" /> ...
</div>
) : (
2025-10-31 17:23:25 +08:00
<div style={{ padding: '8px 12px', color: '#8c8c8c', textAlign: 'center', fontSize: isMobile ? '12px' : '14px' }}>
2025-10-30 16:53:50 +08:00
</div>
)
}
suffixIcon={
2025-10-31 17:23:25 +08:00
!isMobile ? (
<div
onClick={(e) => {
e.stopPropagation();
if (!fetchingModels) {
setModelsFetched(false);
handleFetchModels(false);
}
}}
style={{
cursor: fetchingModels ? 'not-allowed' : 'pointer',
display: 'flex',
alignItems: 'center',
padding: '0 4px',
height: '100%',
marginRight: -8
}}
title="重新获取模型列表"
2025-10-30 16:53:50 +08:00
>
2025-10-31 17:23:25 +08:00
<Button
type="text"
size="small"
icon={<ReloadOutlined />}
loading={fetchingModels}
style={{ pointerEvents: 'none' }}
>
</Button>
</div>
) : undefined
2025-10-30 16:53:50 +08:00
}
options={modelOptions.map(model => ({
value: model.value,
label: model.label,
description: model.description
}))}
optionRender={(option) => (
<div>
2025-10-31 17:23:25 +08:00
<div style={{ fontWeight: 500, fontSize: isMobile ? '13px' : '14px' }}>{option.data.label}</div>
2025-10-30 16:53:50 +08:00
{option.data.description && (
2025-10-31 17:23:25 +08:00
<div style={{ fontSize: isMobile ? '11px' : '12px', color: '#8c8c8c', marginTop: '2px' }}>
2025-10-30 16:53:50 +08:00
{option.data.description}
</div>
)}
</div>
)}
/>
</Form.Item>
<Form.Item
label={
2025-10-31 17:23:25 +08:00
<Space size={4}>
2025-10-30 16:53:50 +08:00
<span></span>
<Tooltip title="控制输出的随机性,值越高越随机(0.0-2.0)">
2025-10-31 17:23:25 +08:00
<InfoCircleOutlined style={{ color: '#8c8c8c', fontSize: isMobile ? '12px' : '14px' }} />
2025-10-30 16:53:50 +08:00
</Tooltip>
</Space>
}
name="temperature"
>
<Slider
min={0}
max={2}
step={0.1}
marks={{
2025-10-31 17:23:25 +08:00
0: { style: { fontSize: isMobile ? '11px' : '12px' }, label: '0.0' },
0.7: { style: { fontSize: isMobile ? '11px' : '12px' }, label: '0.7' },
1: { style: { fontSize: isMobile ? '11px' : '12px' }, label: '1.0' },
2: { style: { fontSize: isMobile ? '11px' : '12px' }, label: '2.0' }
2025-10-30 16:53:50 +08:00
}}
/>
</Form.Item>
<Form.Item
label={
2025-10-31 17:23:25 +08:00
<Space size={4}>
2025-10-30 16:53:50 +08:00
<span> Token </span>
<Tooltip title="单次请求的最大token数量">
2025-10-31 17:23:25 +08:00
<InfoCircleOutlined style={{ color: '#8c8c8c', fontSize: isMobile ? '12px' : '14px' }} />
2025-10-30 16:53:50 +08:00
</Tooltip>
</Space>
}
name="max_tokens"
rules={[
{ required: true, message: '请输入最大token数' },
{ type: 'number', min: 1, message: '请输入大于0的数字' }
2025-10-30 16:53:50 +08:00
]}
>
<InputNumber
2025-10-31 17:23:25 +08:00
size={isMobile ? 'middle' : 'large'}
2025-10-30 16:53:50 +08:00
style={{ width: '100%' }}
min={1}
placeholder="2000"
/>
</Form.Item>
{/* 测试结果展示 */}
{showTestResult && testResult && (
<Alert
message={
<Space>
{testResult.success ? (
<CheckCircleOutlined style={{ color: '#52c41a', fontSize: isMobile ? '16px' : '18px' }} />
) : (
<CloseCircleOutlined style={{ color: '#ff4d4f', fontSize: isMobile ? '16px' : '18px' }} />
)}
<span style={{ fontSize: isMobile ? '14px' : '16px', fontWeight: 500 }}>
{testResult.message}
</span>
</Space>
}
description={
<div style={{ marginTop: 8 }}>
{testResult.success ? (
<Space direction="vertical" size="small" style={{ width: '100%' }}>
{testResult.response_time_ms && (
<div style={{ fontSize: isMobile ? '12px' : '14px' }}>
: <strong>{testResult.response_time_ms} ms</strong>
</div>
)}
{testResult.response_preview && (
<div style={{
fontSize: isMobile ? '12px' : '13px',
padding: '8px 12px',
background: '#f6ffed',
borderRadius: '4px',
border: '1px solid #b7eb8f',
marginTop: '8px'
}}>
<div style={{ marginBottom: '4px', fontWeight: 500 }}>AI :</div>
<div style={{ color: '#595959' }}>{testResult.response_preview}</div>
</div>
)}
<div style={{ color: '#52c41a', fontSize: isMobile ? '12px' : '13px', marginTop: '4px' }}>
API 使
</div>
</Space>
) : (
<Space direction="vertical" size="small" style={{ width: '100%' }}>
{testResult.error && (
<div style={{
fontSize: isMobile ? '12px' : '13px',
padding: '8px 12px',
background: '#fff2e8',
borderRadius: '4px',
border: '1px solid #ffbb96',
color: '#d4380d'
}}>
<strong>:</strong> {testResult.error}
</div>
)}
{testResult.error_type && (
<div style={{ fontSize: isMobile ? '11px' : '12px', color: '#8c8c8c' }}>
: {testResult.error_type}
</div>
)}
{testResult.suggestions && testResult.suggestions.length > 0 && (
<div style={{ marginTop: '8px' }}>
<div style={{ fontSize: isMobile ? '12px' : '13px', fontWeight: 500, marginBottom: '4px' }}>
💡 :
</div>
<ul style={{
margin: 0,
paddingLeft: isMobile ? '16px' : '20px',
fontSize: isMobile ? '12px' : '13px',
color: '#595959'
}}>
{testResult.suggestions.map((suggestion, index) => (
<li key={index} style={{ marginBottom: '4px' }}>{suggestion}</li>
))}
</ul>
</div>
)}
</Space>
)}
</div>
}
type={testResult.success ? 'success' : 'error'}
closable
onClose={() => setShowTestResult(false)}
style={{ marginBottom: isMobile ? 16 : 24 }}
/>
)}
2025-10-30 16:53:50 +08:00
{/* 操作按钮 */}
2025-10-31 17:23:25 +08:00
<Form.Item style={{ marginBottom: 0, marginTop: isMobile ? 24 : 32 }}>
{isMobile ? (
// 移动端:垂直堆叠布局
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
2025-10-30 16:53:50 +08:00
<Button
type="primary"
size="large"
icon={<SaveOutlined />}
htmlType="submit"
loading={loading}
2025-10-31 17:23:25 +08:00
block
2025-10-30 16:53:50 +08:00
style={{
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
2025-10-31 17:23:25 +08:00
border: 'none',
height: '44px'
2025-10-30 16:53:50 +08:00
}}
>
</Button>
2025-10-31 17:23:25 +08:00
<Space size="middle" style={{ width: '100%' }}>
<Button
size="large"
icon={<ReloadOutlined />}
onClick={handleReset}
style={{ flex: 1, height: '44px' }}
>
</Button>
{hasSettings && (
<Button
danger
size="large"
icon={<DeleteOutlined />}
onClick={handleDelete}
loading={loading}
style={{ flex: 1, height: '44px' }}
>
</Button>
)}
</Space>
2025-10-30 16:53:50 +08:00
</Space>
2025-10-31 17:23:25 +08:00
) : (
// 桌面端:删除在左边,测试、重置和保存在右边
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
gap: '16px',
flexWrap: 'wrap'
}}>
{/* 左侧:删除按钮 */}
{hasSettings ? (
2025-10-31 17:23:25 +08:00
<Button
danger
2025-10-31 17:23:25 +08:00
size="large"
icon={<DeleteOutlined />}
onClick={handleDelete}
2025-10-31 17:23:25 +08:00
loading={loading}
style={{
minWidth: '100px'
}}
>
</Button>
) : (
<div /> // 占位符,保持右侧按钮位置
)}
{/* 右侧:测试、重置和保存按钮组 */}
<Space size="middle">
<Button
size="large"
icon={<ThunderboltOutlined />}
onClick={handleTestConnection}
loading={testingApi}
style={{
borderColor: '#52c41a',
color: '#52c41a',
fontWeight: 500,
minWidth: '100px'
2025-10-31 17:23:25 +08:00
}}
>
{testingApi ? '测试中...' : '测试'}
2025-10-31 17:23:25 +08:00
</Button>
<Button
size="large"
icon={<ReloadOutlined />}
onClick={handleReset}
style={{
minWidth: '100px'
}}
2025-10-31 17:23:25 +08:00
>
</Button>
<Button
type="primary"
2025-10-31 17:23:25 +08:00
size="large"
icon={<SaveOutlined />}
htmlType="submit"
2025-10-31 17:23:25 +08:00
loading={loading}
style={{
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
border: 'none',
minWidth: '120px',
fontWeight: 500
}}
2025-10-31 17:23:25 +08:00
>
2025-10-31 17:23:25 +08:00
</Button>
</Space>
</div>
2025-10-31 17:23:25 +08:00
)}
2025-10-30 16:53:50 +08:00
</Form.Item>
</Form>
</Spin>
</Space>
</Card>
</div>
</div>
);
}