|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
import React, { useEffect, useState } from 'react'; |
|
|
import { |
|
|
Button, |
|
|
Space, |
|
|
Table, |
|
|
Form, |
|
|
Typography, |
|
|
Empty, |
|
|
Divider, |
|
|
Avatar, |
|
|
Modal, |
|
|
Tag, |
|
|
Switch, |
|
|
} from '@douyinfe/semi-ui'; |
|
|
import { |
|
|
IllustrationNoResult, |
|
|
IllustrationNoResultDark, |
|
|
} from '@douyinfe/semi-illustrations'; |
|
|
import { Plus, Edit, Trash2, Save, Settings } from 'lucide-react'; |
|
|
import { API, showError, showSuccess } from '../../../helpers'; |
|
|
import { useTranslation } from 'react-i18next'; |
|
|
|
|
|
const { Text } = Typography; |
|
|
|
|
|
const SettingsAPIInfo = ({ options, refresh }) => { |
|
|
const { t } = useTranslation(); |
|
|
|
|
|
const [apiInfoList, setApiInfoList] = useState([]); |
|
|
const [showApiModal, setShowApiModal] = useState(false); |
|
|
const [showDeleteModal, setShowDeleteModal] = useState(false); |
|
|
const [deletingApi, setDeletingApi] = useState(null); |
|
|
const [editingApi, setEditingApi] = useState(null); |
|
|
const [modalLoading, setModalLoading] = useState(false); |
|
|
const [loading, setLoading] = useState(false); |
|
|
const [hasChanges, setHasChanges] = useState(false); |
|
|
const [apiForm, setApiForm] = useState({ |
|
|
url: '', |
|
|
description: '', |
|
|
route: '', |
|
|
color: 'blue', |
|
|
}); |
|
|
const [currentPage, setCurrentPage] = useState(1); |
|
|
const [pageSize, setPageSize] = useState(10); |
|
|
const [selectedRowKeys, setSelectedRowKeys] = useState([]); |
|
|
|
|
|
|
|
|
const [panelEnabled, setPanelEnabled] = useState(true); |
|
|
|
|
|
const colorOptions = [ |
|
|
{ value: 'blue', label: 'blue' }, |
|
|
{ value: 'green', label: 'green' }, |
|
|
{ value: 'cyan', label: 'cyan' }, |
|
|
{ value: 'purple', label: 'purple' }, |
|
|
{ value: 'pink', label: 'pink' }, |
|
|
{ value: 'red', label: 'red' }, |
|
|
{ value: 'orange', label: 'orange' }, |
|
|
{ value: 'amber', label: 'amber' }, |
|
|
{ value: 'yellow', label: 'yellow' }, |
|
|
{ value: 'lime', label: 'lime' }, |
|
|
{ value: 'light-green', label: 'light-green' }, |
|
|
{ value: 'teal', label: 'teal' }, |
|
|
{ value: 'light-blue', label: 'light-blue' }, |
|
|
{ value: 'indigo', label: 'indigo' }, |
|
|
{ value: 'violet', label: 'violet' }, |
|
|
{ value: 'grey', label: 'grey' }, |
|
|
]; |
|
|
|
|
|
const updateOption = async (key, value) => { |
|
|
const res = await API.put('/api/option/', { |
|
|
key, |
|
|
value, |
|
|
}); |
|
|
const { success, message } = res.data; |
|
|
if (success) { |
|
|
showSuccess('API信息已更新'); |
|
|
if (refresh) refresh(); |
|
|
} else { |
|
|
showError(message); |
|
|
} |
|
|
}; |
|
|
|
|
|
const submitApiInfo = async () => { |
|
|
try { |
|
|
setLoading(true); |
|
|
const apiInfoJson = JSON.stringify(apiInfoList); |
|
|
await updateOption('console_setting.api_info', apiInfoJson); |
|
|
setHasChanges(false); |
|
|
} catch (error) { |
|
|
console.error('API信息更新失败', error); |
|
|
showError('API信息更新失败'); |
|
|
} finally { |
|
|
setLoading(false); |
|
|
} |
|
|
}; |
|
|
|
|
|
const handleAddApi = () => { |
|
|
setEditingApi(null); |
|
|
setApiForm({ |
|
|
url: '', |
|
|
description: '', |
|
|
route: '', |
|
|
color: 'blue', |
|
|
}); |
|
|
setShowApiModal(true); |
|
|
}; |
|
|
|
|
|
const handleEditApi = (api) => { |
|
|
setEditingApi(api); |
|
|
setApiForm({ |
|
|
url: api.url, |
|
|
description: api.description, |
|
|
route: api.route, |
|
|
color: api.color, |
|
|
}); |
|
|
setShowApiModal(true); |
|
|
}; |
|
|
|
|
|
const handleDeleteApi = (api) => { |
|
|
setDeletingApi(api); |
|
|
setShowDeleteModal(true); |
|
|
}; |
|
|
|
|
|
const confirmDeleteApi = () => { |
|
|
if (deletingApi) { |
|
|
const newList = apiInfoList.filter((api) => api.id !== deletingApi.id); |
|
|
setApiInfoList(newList); |
|
|
setHasChanges(true); |
|
|
showSuccess('API信息已删除,请及时点击“保存设置”进行保存'); |
|
|
} |
|
|
setShowDeleteModal(false); |
|
|
setDeletingApi(null); |
|
|
}; |
|
|
|
|
|
const handleSaveApi = async () => { |
|
|
if (!apiForm.url || !apiForm.route || !apiForm.description) { |
|
|
showError('请填写完整的API信息'); |
|
|
return; |
|
|
} |
|
|
|
|
|
try { |
|
|
setModalLoading(true); |
|
|
|
|
|
let newList; |
|
|
if (editingApi) { |
|
|
newList = apiInfoList.map((api) => |
|
|
api.id === editingApi.id ? { ...api, ...apiForm } : api, |
|
|
); |
|
|
} else { |
|
|
const newId = Math.max(...apiInfoList.map((api) => api.id), 0) + 1; |
|
|
const newApi = { |
|
|
id: newId, |
|
|
...apiForm, |
|
|
}; |
|
|
newList = [...apiInfoList, newApi]; |
|
|
} |
|
|
|
|
|
setApiInfoList(newList); |
|
|
setHasChanges(true); |
|
|
setShowApiModal(false); |
|
|
showSuccess( |
|
|
editingApi |
|
|
? 'API信息已更新,请及时点击“保存设置”进行保存' |
|
|
: 'API信息已添加,请及时点击“保存设置”进行保存', |
|
|
); |
|
|
} catch (error) { |
|
|
showError('操作失败: ' + error.message); |
|
|
} finally { |
|
|
setModalLoading(false); |
|
|
} |
|
|
}; |
|
|
|
|
|
const parseApiInfo = (apiInfoStr) => { |
|
|
if (!apiInfoStr) { |
|
|
setApiInfoList([]); |
|
|
return; |
|
|
} |
|
|
|
|
|
try { |
|
|
const parsed = JSON.parse(apiInfoStr); |
|
|
setApiInfoList(Array.isArray(parsed) ? parsed : []); |
|
|
} catch (error) { |
|
|
console.error('解析API信息失败:', error); |
|
|
setApiInfoList([]); |
|
|
} |
|
|
}; |
|
|
|
|
|
useEffect(() => { |
|
|
const apiInfoStr = options['console_setting.api_info'] ?? options.ApiInfo; |
|
|
if (apiInfoStr !== undefined) { |
|
|
parseApiInfo(apiInfoStr); |
|
|
} |
|
|
}, [options['console_setting.api_info'], options.ApiInfo]); |
|
|
|
|
|
useEffect(() => { |
|
|
const enabledStr = options['console_setting.api_info_enabled']; |
|
|
setPanelEnabled( |
|
|
enabledStr === undefined |
|
|
? true |
|
|
: enabledStr === 'true' || enabledStr === true, |
|
|
); |
|
|
}, [options['console_setting.api_info_enabled']]); |
|
|
|
|
|
const handleToggleEnabled = async (checked) => { |
|
|
const newValue = checked ? 'true' : 'false'; |
|
|
try { |
|
|
const res = await API.put('/api/option/', { |
|
|
key: 'console_setting.api_info_enabled', |
|
|
value: newValue, |
|
|
}); |
|
|
if (res.data.success) { |
|
|
setPanelEnabled(checked); |
|
|
showSuccess(t('设置已保存')); |
|
|
refresh?.(); |
|
|
} else { |
|
|
showError(res.data.message); |
|
|
} |
|
|
} catch (err) { |
|
|
showError(err.message); |
|
|
} |
|
|
}; |
|
|
|
|
|
const columns = [ |
|
|
{ |
|
|
title: 'ID', |
|
|
dataIndex: 'id', |
|
|
}, |
|
|
{ |
|
|
title: t('API地址'), |
|
|
dataIndex: 'url', |
|
|
render: (text, record) => ( |
|
|
<Tag color={record.color} shape='circle' style={{ maxWidth: '280px' }}> |
|
|
{text} |
|
|
</Tag> |
|
|
), |
|
|
}, |
|
|
{ |
|
|
title: t('线路描述'), |
|
|
dataIndex: 'route', |
|
|
render: (text, record) => <Tag shape='circle'>{text}</Tag>, |
|
|
}, |
|
|
{ |
|
|
title: t('说明'), |
|
|
dataIndex: 'description', |
|
|
ellipsis: true, |
|
|
render: (text, record) => <Tag shape='circle'>{text || '-'}</Tag>, |
|
|
}, |
|
|
{ |
|
|
title: t('颜色'), |
|
|
dataIndex: 'color', |
|
|
render: (color) => <Avatar size='extra-extra-small' color={color} />, |
|
|
}, |
|
|
{ |
|
|
title: t('操作'), |
|
|
fixed: 'right', |
|
|
width: 150, |
|
|
render: (_, record) => ( |
|
|
<Space> |
|
|
<Button |
|
|
icon={<Edit size={14} />} |
|
|
theme='light' |
|
|
type='tertiary' |
|
|
size='small' |
|
|
onClick={() => handleEditApi(record)} |
|
|
> |
|
|
{t('编辑')} |
|
|
</Button> |
|
|
<Button |
|
|
icon={<Trash2 size={14} />} |
|
|
type='danger' |
|
|
theme='light' |
|
|
size='small' |
|
|
onClick={() => handleDeleteApi(record)} |
|
|
> |
|
|
{t('删除')} |
|
|
</Button> |
|
|
</Space> |
|
|
), |
|
|
}, |
|
|
]; |
|
|
|
|
|
const handleBatchDelete = () => { |
|
|
if (selectedRowKeys.length === 0) { |
|
|
showError('请先选择要删除的API信息'); |
|
|
return; |
|
|
} |
|
|
|
|
|
const newList = apiInfoList.filter( |
|
|
(api) => !selectedRowKeys.includes(api.id), |
|
|
); |
|
|
setApiInfoList(newList); |
|
|
setSelectedRowKeys([]); |
|
|
setHasChanges(true); |
|
|
showSuccess( |
|
|
`已删除 ${selectedRowKeys.length} 个API信息,请及时点击“保存设置”进行保存`, |
|
|
); |
|
|
}; |
|
|
|
|
|
const renderHeader = () => ( |
|
|
<div className='flex flex-col w-full'> |
|
|
<div className='mb-2'> |
|
|
<div className='flex items-center text-blue-500'> |
|
|
<Settings size={16} className='mr-2' /> |
|
|
<Text> |
|
|
{t( |
|
|
'API信息管理,可以配置多个API地址用于状态展示和负载均衡(最多50个)', |
|
|
)} |
|
|
</Text> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<Divider margin='12px' /> |
|
|
|
|
|
<div className='flex flex-col md:flex-row justify-between items-center gap-4 w-full'> |
|
|
<div className='flex gap-2 w-full md:w-auto order-2 md:order-1'> |
|
|
<Button |
|
|
theme='light' |
|
|
type='primary' |
|
|
icon={<Plus size={14} />} |
|
|
className='w-full md:w-auto' |
|
|
onClick={handleAddApi} |
|
|
> |
|
|
{t('添加API')} |
|
|
</Button> |
|
|
<Button |
|
|
icon={<Trash2 size={14} />} |
|
|
type='danger' |
|
|
theme='light' |
|
|
onClick={handleBatchDelete} |
|
|
disabled={selectedRowKeys.length === 0} |
|
|
className='w-full md:w-auto' |
|
|
> |
|
|
{t('批量删除')}{' '} |
|
|
{selectedRowKeys.length > 0 && `(${selectedRowKeys.length})`} |
|
|
</Button> |
|
|
<Button |
|
|
icon={<Save size={14} />} |
|
|
onClick={submitApiInfo} |
|
|
loading={loading} |
|
|
disabled={!hasChanges} |
|
|
type='secondary' |
|
|
className='w-full md:w-auto' |
|
|
> |
|
|
{t('保存设置')} |
|
|
</Button> |
|
|
</div> |
|
|
|
|
|
{/* 启用开关 */} |
|
|
<div className='order-1 md:order-2 flex items-center gap-2'> |
|
|
<Switch checked={panelEnabled} onChange={handleToggleEnabled} /> |
|
|
<Text>{panelEnabled ? t('已启用') : t('已禁用')}</Text> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
); |
|
|
|
|
|
|
|
|
const getCurrentPageData = () => { |
|
|
const startIndex = (currentPage - 1) * pageSize; |
|
|
const endIndex = startIndex + pageSize; |
|
|
return apiInfoList.slice(startIndex, endIndex); |
|
|
}; |
|
|
|
|
|
const rowSelection = { |
|
|
selectedRowKeys, |
|
|
onChange: (selectedRowKeys, selectedRows) => { |
|
|
setSelectedRowKeys(selectedRowKeys); |
|
|
}, |
|
|
onSelect: (record, selected, selectedRows) => { |
|
|
console.log(`选择行: ${selected}`, record); |
|
|
}, |
|
|
onSelectAll: (selected, selectedRows) => { |
|
|
console.log(`全选: ${selected}`, selectedRows); |
|
|
}, |
|
|
getCheckboxProps: (record) => ({ |
|
|
disabled: false, |
|
|
name: record.id, |
|
|
}), |
|
|
}; |
|
|
|
|
|
return ( |
|
|
<> |
|
|
<Form.Section text={renderHeader()}> |
|
|
<Table |
|
|
columns={columns} |
|
|
dataSource={getCurrentPageData()} |
|
|
rowSelection={rowSelection} |
|
|
rowKey='id' |
|
|
scroll={{ x: 'max-content' }} |
|
|
pagination={{ |
|
|
currentPage: currentPage, |
|
|
pageSize: pageSize, |
|
|
total: apiInfoList.length, |
|
|
showSizeChanger: true, |
|
|
showQuickJumper: true, |
|
|
pageSizeOptions: ['5', '10', '20', '50'], |
|
|
onChange: (page, size) => { |
|
|
setCurrentPage(page); |
|
|
setPageSize(size); |
|
|
}, |
|
|
onShowSizeChange: (current, size) => { |
|
|
setCurrentPage(1); |
|
|
setPageSize(size); |
|
|
}, |
|
|
}} |
|
|
size='middle' |
|
|
loading={loading} |
|
|
empty={ |
|
|
<Empty |
|
|
image={ |
|
|
<IllustrationNoResult style={{ width: 150, height: 150 }} /> |
|
|
} |
|
|
darkModeImage={ |
|
|
<IllustrationNoResultDark style={{ width: 150, height: 150 }} /> |
|
|
} |
|
|
description={t('暂无API信息')} |
|
|
style={{ padding: 30 }} |
|
|
/> |
|
|
} |
|
|
className='overflow-hidden' |
|
|
/> |
|
|
</Form.Section> |
|
|
|
|
|
<Modal |
|
|
title={editingApi ? t('编辑API') : t('添加API')} |
|
|
visible={showApiModal} |
|
|
onOk={handleSaveApi} |
|
|
onCancel={() => setShowApiModal(false)} |
|
|
okText={t('保存')} |
|
|
cancelText={t('取消')} |
|
|
confirmLoading={modalLoading} |
|
|
> |
|
|
<Form |
|
|
layout='vertical' |
|
|
initValues={apiForm} |
|
|
key={editingApi ? editingApi.id : 'new'} |
|
|
> |
|
|
<Form.Input |
|
|
field='url' |
|
|
label={t('API地址')} |
|
|
placeholder='https://api.example.com' |
|
|
rules={[{ required: true, message: t('请输入API地址') }]} |
|
|
onChange={(value) => setApiForm({ ...apiForm, url: value })} |
|
|
/> |
|
|
<Form.Input |
|
|
field='route' |
|
|
label={t('线路描述')} |
|
|
placeholder={t('如:香港线路')} |
|
|
rules={[{ required: true, message: t('请输入线路描述') }]} |
|
|
onChange={(value) => setApiForm({ ...apiForm, route: value })} |
|
|
/> |
|
|
<Form.Input |
|
|
field='description' |
|
|
label={t('说明')} |
|
|
placeholder={t('如:大带宽批量分析图片推荐')} |
|
|
rules={[{ required: true, message: t('请输入说明') }]} |
|
|
onChange={(value) => setApiForm({ ...apiForm, description: value })} |
|
|
/> |
|
|
<Form.Select |
|
|
field='color' |
|
|
label={t('标识颜色')} |
|
|
optionList={colorOptions} |
|
|
onChange={(value) => setApiForm({ ...apiForm, color: value })} |
|
|
render={(option) => ( |
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}> |
|
|
<Avatar size='extra-extra-small' color={option.value} /> |
|
|
{option.label} |
|
|
</div> |
|
|
)} |
|
|
/> |
|
|
</Form> |
|
|
</Modal> |
|
|
|
|
|
<Modal |
|
|
title={t('确认删除')} |
|
|
visible={showDeleteModal} |
|
|
onOk={confirmDeleteApi} |
|
|
onCancel={() => { |
|
|
setShowDeleteModal(false); |
|
|
setDeletingApi(null); |
|
|
}} |
|
|
okText={t('确认删除')} |
|
|
cancelText={t('取消')} |
|
|
type='warning' |
|
|
okButtonProps={{ |
|
|
type: 'danger', |
|
|
theme: 'solid', |
|
|
}} |
|
|
> |
|
|
<Text>{t('确定要删除此API信息吗?')}</Text> |
|
|
</Modal> |
|
|
</> |
|
|
); |
|
|
}; |
|
|
|
|
|
export default SettingsAPIInfo; |
|
|
|