cangku/frontend/src/pages/Settings/DatabaseSettings.jsx

440 lines
16 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useCallback, useEffect, useState } from 'react';
import {
Button,
Col,
Form,
Input,
InputNumber,
Modal,
Popconfirm,
Row,
Select,
Space,
Switch,
Table,
Tag,
Typography,
message,
} from 'antd';
import {
ApiOutlined,
DeleteOutlined,
EditOutlined,
PlusOutlined,
ReloadOutlined,
SaveOutlined,
} from '@ant-design/icons';
import {
deleteDatabaseConfig,
getDatabaseConfig,
getDatabaseConfigs,
getSystemDatabaseConfig,
testDatabaseConfig,
testSystemDatabaseConfig,
updateDatabaseConfig,
updateSystemDatabaseConfig,
} from '../../api/admin';
import { SycmCard } from '../../components/Common';
const { Text } = Typography;
const DRIVER_OPTIONS = [
{ value: 'mysql', label: 'MySQL / MariaDB' },
{ value: 'mssql', label: 'SQL Server (MSSQL)' },
{ value: 'postgres', label: 'PostgreSQL' },
];
const DEFAULT_PORTS = {
mysql: 3306,
mssql: 1433,
postgres: 5432,
};
const CORE_CONFIG_NAMES = new Set([
'cloud_database',
'cloud_database_public',
'planning_database',
]);
const CONFIG_LABELS = {
cloud_database: '默认业务数据库',
cloud_database_public: '公网业务数据库',
planning_database: '企划数据库',
jiansun_database: '简尚业务数据库',
product_information_database: '产品资料系统数据库',
};
const DatabaseSettings = () => {
const [systemForm] = Form.useForm();
const [databaseForm] = Form.useForm();
const [systemLoading, setSystemLoading] = useState(false);
const [systemTesting, setSystemTesting] = useState(false);
const [listLoading, setListLoading] = useState(false);
const [saving, setSaving] = useState(false);
const [testing, setTesting] = useState(false);
const [configs, setConfigs] = useState([]);
const [editorOpen, setEditorOpen] = useState(false);
const [editingConfig, setEditingConfig] = useState(null);
const [envConfig, setEnvConfig] = useState(null);
const useEnvConfig = Form.useWatch('useEnvConfig', databaseForm);
const loadSystemConfig = useCallback(async () => {
try {
const config = await getSystemDatabaseConfig();
systemForm.setFieldsValue({
driver: 'mysql',
host: config.host || '',
port: config.port || 3306,
database: config.database || '',
username: config.username || '',
password: '',
});
} catch (error) {
message.error(error.response?.data?.error || '获取系统数据库配置失败');
}
}, [systemForm]);
const loadConfigs = useCallback(async () => {
try {
setListLoading(true);
const result = await getDatabaseConfigs();
setConfigs(Array.isArray(result) ? result : []);
} catch (error) {
message.error(error.response?.data?.error || '获取数据库配置列表失败');
} finally {
setListLoading(false);
}
}, []);
useEffect(() => {
loadSystemConfig();
loadConfigs();
}, [loadConfigs, loadSystemConfig]);
const saveSystemConfig = async () => {
try {
const values = await systemForm.validateFields();
setSystemLoading(true);
const result = await updateSystemDatabaseConfig(values);
message.success(result.message || '系统数据库配置已保存');
await loadSystemConfig();
} catch (error) {
if (!error?.errorFields) {
message.error(error.response?.data?.error || '保存系统数据库配置失败');
}
} finally {
setSystemLoading(false);
}
};
const testSystemConfig = async () => {
try {
const values = await systemForm.validateFields();
if (!values.password) {
message.warning('测试系统数据库前请填写密码');
return;
}
setSystemTesting(true);
const result = await testSystemDatabaseConfig(values);
message.success(result.message || '系统数据库连接成功');
} catch (error) {
if (!error?.errorFields) {
message.error(error.response?.data?.message || error.response?.data?.error || '系统数据库连接失败');
}
} finally {
setSystemTesting(false);
}
};
const openCreate = () => {
setEditingConfig(null);
setEnvConfig(null);
databaseForm.resetFields();
databaseForm.setFieldsValue({ driver: 'mysql', port: 3306, ssl: false, useEnvConfig: false });
setEditorOpen(true);
};
const openEdit = async (record) => {
try {
setEditingConfig(record);
const config = await getDatabaseConfig(record.name);
setEnvConfig(config.envConfig || null);
databaseForm.setFieldsValue({
...config,
password: '',
ssl: Boolean(config.ssl),
useEnvConfig: Boolean(config.useEnvConfig),
});
setEditorOpen(true);
} catch (error) {
message.error(error.response?.data?.error || '获取数据库配置失败');
}
};
const testEditorConfig = async () => {
try {
const values = await databaseForm.validateFields();
setTesting(true);
const result = await testDatabaseConfig(values);
message.success(`${result.message || '连接成功'}${result.responseTime ? `,${result.responseTime}ms` : ''}`);
} catch (error) {
if (!error?.errorFields) {
message.error(error.response?.data?.error || error.message || '数据库连接失败');
}
} finally {
setTesting(false);
}
};
const saveDatabaseConfig = async () => {
try {
const values = await databaseForm.validateFields();
setSaving(true);
const result = await updateDatabaseConfig({ ...values, status: editingConfig?.status || 'active' }, values.name);
message.success(result.message || '数据库配置已保存');
setEditorOpen(false);
await loadConfigs();
} catch (error) {
if (!error?.errorFields) {
message.error(error.response?.data?.error || error.message || '保存数据库配置失败');
}
} finally {
setSaving(false);
}
};
const toggleStatus = async (record, enabled) => {
try {
const current = await getDatabaseConfig(record.name);
await updateDatabaseConfig({ ...current, password: '', status: enabled ? 'active' : 'inactive' }, record.name);
message.success(enabled ? '数据库配置已启用' : '数据库配置已停用');
await loadConfigs();
} catch (error) {
message.error(error.response?.data?.error || '更新数据库状态失败');
}
};
const removeConfig = async (record) => {
try {
const result = await deleteDatabaseConfig(record.id);
message.success(result.message || '数据库配置已删除');
await loadConfigs();
} catch (error) {
message.error(error.response?.data?.error || '删除数据库配置失败');
}
};
const columns = [
{
title: '配置',
dataIndex: 'name',
width: 220,
render: (value) => (
<div className="database-settings-name">
<strong>{CONFIG_LABELS[value] || value}</strong>
{CONFIG_LABELS[value] ? <span>{value}</span> : null}
</div>
),
},
{
title: '类型',
dataIndex: 'driver',
width: 150,
render: (value) => DRIVER_OPTIONS.find((item) => item.value === value)?.label || value,
},
{
title: '连接地址',
key: 'endpoint',
ellipsis: true,
render: (_, record) => `${record.host || '-'}:${record.port || '-'}/${record.database || '-'}`,
},
{ title: '用户名', dataIndex: 'username', width: 150, ellipsis: true },
{
title: '来源',
key: 'source',
width: 100,
render: (_, record) => <Tag>{record.useEnvConfig ? '.env' : '系统配置'}</Tag>,
},
{
title: '状态',
dataIndex: 'status',
width: 90,
render: (value, record) => (
<Switch
size="small"
checked={value !== 'inactive'}
disabled={Boolean(record.is_default)}
onChange={(checked) => toggleStatus(record, checked)}
/>
),
},
{
title: '操作',
key: 'actions',
width: 120,
fixed: 'right',
render: (_, record) => (
<Space size={2}>
<Button type="text" icon={<EditOutlined />} onClick={() => openEdit(record)} aria-label="编辑数据库配置" />
<Popconfirm
title="删除这个数据库配置?"
description="只删除连接配置,不会删除远程数据库中的数据。"
disabled={record.protected || CORE_CONFIG_NAMES.has(record.name)}
onConfirm={() => removeConfig(record)}
>
<Button
type="text"
danger
disabled={record.protected || CORE_CONFIG_NAMES.has(record.name)}
icon={<DeleteOutlined />}
aria-label="删除数据库配置"
/>
</Popconfirm>
</Space>
),
},
];
return (
<div className="settings-tab-page database-settings-page">
<SycmCard bodyStyle={{ padding: 16 }}>
<div className="database-settings-section-head">
<div>
<strong>系统数据库</strong>
<Text type="secondary">保存账号、权限和系统配置,修改后需重启服务。</Text>
</div>
<Button icon={<ReloadOutlined />} onClick={loadSystemConfig}>刷新</Button>
</div>
<Form form={systemForm} layout="vertical">
<Row gutter={[16, 0]}>
<Col xs={24} md={8}>
<Form.Item label="类型" name="driver"><Select options={[{ value: 'mysql', label: 'MySQL / MariaDB' }]} disabled /></Form.Item>
</Col>
<Col xs={24} md={8}>
<Form.Item label="主机地址" name="host" rules={[{ required: true, message: '请输入主机地址' }]}><Input /></Form.Item>
</Col>
<Col xs={24} md={8}>
<Form.Item label="端口" name="port" rules={[{ required: true, message: '请输入端口' }]}><InputNumber min={1} max={65535} style={{ width: '100%' }} /></Form.Item>
</Col>
<Col xs={24} md={8}>
<Form.Item label="数据库名" name="database" rules={[{ required: true, message: '请输入数据库名' }]}><Input /></Form.Item>
</Col>
<Col xs={24} md={8}>
<Form.Item label="用户名" name="username" rules={[{ required: true, message: '请输入用户名' }]}><Input /></Form.Item>
</Col>
<Col xs={24} md={8}>
<Form.Item label="密码" name="password" extra="留空表示沿用已保存密码"><Input.Password /></Form.Item>
</Col>
</Row>
<div className="database-settings-actions">
<Button icon={<ApiOutlined />} loading={systemTesting} onClick={testSystemConfig}>测试连接</Button>
<Button type="primary" icon={<SaveOutlined />} loading={systemLoading} onClick={saveSystemConfig}>保存系统库</Button>
</div>
</Form>
</SycmCard>
<SycmCard bodyStyle={{ padding: 16 }}>
<div className="database-settings-section-head">
<div>
<strong>业务与扩展数据库</strong>
<Text type="secondary">统一管理业务库和管理员自定义的数据源连接。</Text>
</div>
<Space>
<Button icon={<ReloadOutlined />} onClick={loadConfigs} loading={listLoading}>刷新</Button>
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>新增数据库</Button>
</Space>
</div>
<Table
rowKey="id"
columns={columns}
dataSource={configs}
loading={listLoading}
pagination={false}
scroll={{ x: 1050 }}
/>
</SycmCard>
<Modal
title={editingConfig ? '编辑数据库配置' : '新增数据库配置'}
open={editorOpen}
width={720}
onCancel={() => setEditorOpen(false)}
footer={[
<Button key="test" icon={<ApiOutlined />} loading={testing} onClick={testEditorConfig}>测试连接</Button>,
<Button key="cancel" onClick={() => setEditorOpen(false)}>取消</Button>,
<Button key="save" type="primary" loading={saving} onClick={saveDatabaseConfig}>保存</Button>,
]}
destroyOnHidden
>
<Form form={databaseForm} layout="vertical" className="database-settings-editor">
<Row gutter={[16, 0]}>
<Col span={12}>
<Form.Item
label="配置标识"
name="name"
rules={[
{ required: true, message: '请输入配置标识' },
{ pattern: /^[a-zA-Z0-9][a-zA-Z0-9_-]{1,99}$/, message: '使用 2-100 位英文、数字、下划线或短横线' },
{
validator: (_, value) => (
!editingConfig && configs.some((item) => item.name === value)
? Promise.reject(new Error('该配置标识已经存在'))
: Promise.resolve()
),
},
]}
>
<Input disabled={Boolean(editingConfig)} placeholder="例如 market_archive" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item label="数据库类型" name="driver" rules={[{ required: !useEnvConfig }]}>
<Select
options={DRIVER_OPTIONS}
disabled={useEnvConfig}
onChange={(driver) => databaseForm.setFieldValue('port', DEFAULT_PORTS[driver])}
/>
</Form.Item>
</Col>
{editingConfig && ['cloud_database', 'cloud_database_public'].includes(editingConfig.name) ? (
<Col span={24}>
<div className="database-settings-env-row">
<div>
<strong>优先使用 .env 配置</strong>
<Text type="secondary">
{envConfig?.available
? `${envConfig.host}:${envConfig.port}/${envConfig.database}`
: '当前环境变量配置不完整'}
</Text>
</div>
<Form.Item name="useEnvConfig" valuePropName="checked" noStyle><Switch /></Form.Item>
</div>
</Col>
) : null}
<Col span={12}>
<Form.Item label="主机地址" name="host" rules={[{ required: !useEnvConfig, message: '请输入主机地址' }]}><Input disabled={useEnvConfig} /></Form.Item>
</Col>
<Col span={12}>
<Form.Item label="端口" name="port" rules={[{ required: !useEnvConfig, message: '请输入端口' }]}><InputNumber disabled={useEnvConfig} min={1} max={65535} style={{ width: '100%' }} /></Form.Item>
</Col>
<Col span={12}>
<Form.Item label="数据库名" name="database" rules={[{ required: !useEnvConfig, message: '请输入数据库名' }]}><Input disabled={useEnvConfig} /></Form.Item>
</Col>
<Col span={12}>
<Form.Item label="用户名" name="username" rules={[{ required: !useEnvConfig, message: '请输入用户名' }]}><Input disabled={useEnvConfig} /></Form.Item>
</Col>
<Col span={12}>
<Form.Item label="密码" name="password" rules={[{ required: !editingConfig && !useEnvConfig, message: '请输入密码' }]} extra={editingConfig ? '留空表示沿用已保存密码' : null}><Input.Password disabled={useEnvConfig} /></Form.Item>
</Col>
<Col span={12}>
<Form.Item label="SSL 加密" name="ssl" valuePropName="checked"><Switch disabled={useEnvConfig} /></Form.Item>
</Col>
</Row>
</Form>
</Modal>
</div>
);
};
export default DatabaseSettings;