440 lines
16 KiB
JavaScript
440 lines
16 KiB
JavaScript
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;
|