🎨 chore(web): apply ESLint and Prettier auto-fixes (baseline)
- Ran: bun run eslint:fix && bun run lint:fix - Inserted AGPL license header via eslint-plugin-header - Enforced no-multiple-empty-lines and other lint rules - Formatted code using Prettier v3 (@so1ve/prettier-config) - No functional changes; formatting-only baseline across JS/JSX files
This commit is contained in:
@@ -18,13 +18,7 @@ For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import {
|
||||
Banner,
|
||||
Button,
|
||||
Form,
|
||||
Space,
|
||||
Spin,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import { Banner, Button, Form, Space, Spin } from '@douyinfe/semi-ui';
|
||||
import {
|
||||
compareObjects,
|
||||
API,
|
||||
|
||||
@@ -29,19 +29,13 @@ import {
|
||||
Avatar,
|
||||
Modal,
|
||||
Tag,
|
||||
Switch
|
||||
Switch,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import {
|
||||
IllustrationNoResult,
|
||||
IllustrationNoResultDark
|
||||
IllustrationNoResultDark,
|
||||
} from '@douyinfe/semi-illustrations';
|
||||
import {
|
||||
Plus,
|
||||
Edit,
|
||||
Trash2,
|
||||
Save,
|
||||
Settings
|
||||
} from 'lucide-react';
|
||||
import { Plus, Edit, Trash2, Save, Settings } from 'lucide-react';
|
||||
import { API, showError, showSuccess } from '../../../helpers';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
@@ -62,7 +56,7 @@ const SettingsAPIInfo = ({ options, refresh }) => {
|
||||
url: '',
|
||||
description: '',
|
||||
route: '',
|
||||
color: 'blue'
|
||||
color: 'blue',
|
||||
});
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
@@ -87,7 +81,7 @@ const SettingsAPIInfo = ({ options, refresh }) => {
|
||||
{ value: 'light-blue', label: 'light-blue' },
|
||||
{ value: 'indigo', label: 'indigo' },
|
||||
{ value: 'violet', label: 'violet' },
|
||||
{ value: 'grey', label: 'grey' }
|
||||
{ value: 'grey', label: 'grey' },
|
||||
];
|
||||
|
||||
const updateOption = async (key, value) => {
|
||||
@@ -124,7 +118,7 @@ const SettingsAPIInfo = ({ options, refresh }) => {
|
||||
url: '',
|
||||
description: '',
|
||||
route: '',
|
||||
color: 'blue'
|
||||
color: 'blue',
|
||||
});
|
||||
setShowApiModal(true);
|
||||
};
|
||||
@@ -135,7 +129,7 @@ const SettingsAPIInfo = ({ options, refresh }) => {
|
||||
url: api.url,
|
||||
description: api.description,
|
||||
route: api.route,
|
||||
color: api.color
|
||||
color: api.color,
|
||||
});
|
||||
setShowApiModal(true);
|
||||
};
|
||||
@@ -147,7 +141,7 @@ const SettingsAPIInfo = ({ options, refresh }) => {
|
||||
|
||||
const confirmDeleteApi = () => {
|
||||
if (deletingApi) {
|
||||
const newList = apiInfoList.filter(api => api.id !== deletingApi.id);
|
||||
const newList = apiInfoList.filter((api) => api.id !== deletingApi.id);
|
||||
setApiInfoList(newList);
|
||||
setHasChanges(true);
|
||||
showSuccess('API信息已删除,请及时点击“保存设置”进行保存');
|
||||
@@ -167,16 +161,14 @@ const SettingsAPIInfo = ({ options, refresh }) => {
|
||||
|
||||
let newList;
|
||||
if (editingApi) {
|
||||
newList = apiInfoList.map(api =>
|
||||
api.id === editingApi.id
|
||||
? { ...api, ...apiForm }
|
||||
: api
|
||||
newList = apiInfoList.map((api) =>
|
||||
api.id === editingApi.id ? { ...api, ...apiForm } : api,
|
||||
);
|
||||
} else {
|
||||
const newId = Math.max(...apiInfoList.map(api => api.id), 0) + 1;
|
||||
const newId = Math.max(...apiInfoList.map((api) => api.id), 0) + 1;
|
||||
const newApi = {
|
||||
id: newId,
|
||||
...apiForm
|
||||
...apiForm,
|
||||
};
|
||||
newList = [...apiInfoList, newApi];
|
||||
}
|
||||
@@ -184,7 +176,11 @@ const SettingsAPIInfo = ({ options, refresh }) => {
|
||||
setApiInfoList(newList);
|
||||
setHasChanges(true);
|
||||
setShowApiModal(false);
|
||||
showSuccess(editingApi ? 'API信息已更新,请及时点击“保存设置”进行保存' : 'API信息已添加,请及时点击“保存设置”进行保存');
|
||||
showSuccess(
|
||||
editingApi
|
||||
? 'API信息已更新,请及时点击“保存设置”进行保存'
|
||||
: 'API信息已添加,请及时点击“保存设置”进行保存',
|
||||
);
|
||||
} catch (error) {
|
||||
showError('操作失败: ' + error.message);
|
||||
} finally {
|
||||
@@ -216,7 +212,11 @@ const SettingsAPIInfo = ({ options, refresh }) => {
|
||||
|
||||
useEffect(() => {
|
||||
const enabledStr = options['console_setting.api_info_enabled'];
|
||||
setPanelEnabled(enabledStr === undefined ? true : enabledStr === 'true' || enabledStr === true);
|
||||
setPanelEnabled(
|
||||
enabledStr === undefined
|
||||
? true
|
||||
: enabledStr === 'true' || enabledStr === true,
|
||||
);
|
||||
}, [options['console_setting.api_info_enabled']]);
|
||||
|
||||
const handleToggleEnabled = async (checked) => {
|
||||
@@ -247,11 +247,7 @@ const SettingsAPIInfo = ({ options, refresh }) => {
|
||||
title: t('API地址'),
|
||||
dataIndex: 'url',
|
||||
render: (text, record) => (
|
||||
<Tag
|
||||
color={record.color}
|
||||
shape='circle'
|
||||
style={{ maxWidth: '280px' }}
|
||||
>
|
||||
<Tag color={record.color} shape='circle' style={{ maxWidth: '280px' }}>
|
||||
{text}
|
||||
</Tag>
|
||||
),
|
||||
@@ -259,31 +255,18 @@ const SettingsAPIInfo = ({ options, refresh }) => {
|
||||
{
|
||||
title: t('线路描述'),
|
||||
dataIndex: 'route',
|
||||
render: (text, record) => (
|
||||
<Tag shape='circle'>
|
||||
{text}
|
||||
</Tag>
|
||||
),
|
||||
render: (text, record) => <Tag shape='circle'>{text}</Tag>,
|
||||
},
|
||||
{
|
||||
title: t('说明'),
|
||||
dataIndex: 'description',
|
||||
ellipsis: true,
|
||||
render: (text, record) => (
|
||||
<Tag shape='circle'>
|
||||
{text || '-'}
|
||||
</Tag>
|
||||
),
|
||||
render: (text, record) => <Tag shape='circle'>{text || '-'}</Tag>,
|
||||
},
|
||||
{
|
||||
title: t('颜色'),
|
||||
dataIndex: 'color',
|
||||
render: (color) => (
|
||||
<Avatar
|
||||
size="extra-extra-small"
|
||||
color={color}
|
||||
/>
|
||||
),
|
||||
render: (color) => <Avatar size='extra-extra-small' color={color} />,
|
||||
},
|
||||
{
|
||||
title: t('操作'),
|
||||
@@ -320,31 +303,39 @@ const SettingsAPIInfo = ({ options, refresh }) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const newList = apiInfoList.filter(api => !selectedRowKeys.includes(api.id));
|
||||
const newList = apiInfoList.filter(
|
||||
(api) => !selectedRowKeys.includes(api.id),
|
||||
);
|
||||
setApiInfoList(newList);
|
||||
setSelectedRowKeys([]);
|
||||
setHasChanges(true);
|
||||
showSuccess(`已删除 ${selectedRowKeys.length} 个API信息,请及时点击“保存设置”进行保存`);
|
||||
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 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" />
|
||||
<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">
|
||||
<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"
|
||||
className='w-full md:w-auto'
|
||||
onClick={handleAddApi}
|
||||
>
|
||||
{t('添加API')}
|
||||
@@ -355,9 +346,10 @@ const SettingsAPIInfo = ({ options, refresh }) => {
|
||||
theme='light'
|
||||
onClick={handleBatchDelete}
|
||||
disabled={selectedRowKeys.length === 0}
|
||||
className="w-full md:w-auto"
|
||||
className='w-full md:w-auto'
|
||||
>
|
||||
{t('批量删除')} {selectedRowKeys.length > 0 && `(${selectedRowKeys.length})`}
|
||||
{t('批量删除')}{' '}
|
||||
{selectedRowKeys.length > 0 && `(${selectedRowKeys.length})`}
|
||||
</Button>
|
||||
<Button
|
||||
icon={<Save size={14} />}
|
||||
@@ -365,18 +357,15 @@ const SettingsAPIInfo = ({ options, refresh }) => {
|
||||
loading={loading}
|
||||
disabled={!hasChanges}
|
||||
type='secondary'
|
||||
className="w-full md:w-auto"
|
||||
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}
|
||||
/>
|
||||
<div className='order-1 md:order-2 flex items-center gap-2'>
|
||||
<Switch checked={panelEnabled} onChange={handleToggleEnabled} />
|
||||
<Text>{panelEnabled ? t('已启用') : t('已禁用')}</Text>
|
||||
</div>
|
||||
</div>
|
||||
@@ -414,7 +403,7 @@ const SettingsAPIInfo = ({ options, refresh }) => {
|
||||
columns={columns}
|
||||
dataSource={getCurrentPageData()}
|
||||
rowSelection={rowSelection}
|
||||
rowKey="id"
|
||||
rowKey='id'
|
||||
scroll={{ x: 'max-content' }}
|
||||
pagination={{
|
||||
currentPage: currentPage,
|
||||
@@ -430,19 +419,23 @@ const SettingsAPIInfo = ({ options, refresh }) => {
|
||||
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 }} />}
|
||||
image={
|
||||
<IllustrationNoResult style={{ width: 150, height: 150 }} />
|
||||
}
|
||||
darkModeImage={
|
||||
<IllustrationNoResultDark style={{ width: 150, height: 150 }} />
|
||||
}
|
||||
description={t('暂无API信息')}
|
||||
style={{ padding: 30 }}
|
||||
/>
|
||||
}
|
||||
className="overflow-hidden"
|
||||
className='overflow-hidden'
|
||||
/>
|
||||
</Form.Section>
|
||||
|
||||
@@ -455,7 +448,11 @@ const SettingsAPIInfo = ({ options, refresh }) => {
|
||||
cancelText={t('取消')}
|
||||
confirmLoading={modalLoading}
|
||||
>
|
||||
<Form layout='vertical' initValues={apiForm} key={editingApi ? editingApi.id : 'new'}>
|
||||
<Form
|
||||
layout='vertical'
|
||||
initValues={apiForm}
|
||||
key={editingApi ? editingApi.id : 'new'}
|
||||
>
|
||||
<Form.Input
|
||||
field='url'
|
||||
label={t('API地址')}
|
||||
@@ -484,10 +481,7 @@ const SettingsAPIInfo = ({ options, refresh }) => {
|
||||
onChange={(value) => setApiForm({ ...apiForm, color: value })}
|
||||
render={(option) => (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<Avatar
|
||||
size="extra-extra-small"
|
||||
color={option.value}
|
||||
/>
|
||||
<Avatar size='extra-extra-small' color={option.value} />
|
||||
{option.label}
|
||||
</div>
|
||||
)}
|
||||
@@ -505,10 +499,10 @@ const SettingsAPIInfo = ({ options, refresh }) => {
|
||||
}}
|
||||
okText={t('确认删除')}
|
||||
cancelText={t('取消')}
|
||||
type="warning"
|
||||
type='warning'
|
||||
okButtonProps={{
|
||||
type: 'danger',
|
||||
theme: 'solid'
|
||||
theme: 'solid',
|
||||
}}
|
||||
>
|
||||
<Text>{t('确定要删除此API信息吗?')}</Text>
|
||||
@@ -517,4 +511,4 @@ const SettingsAPIInfo = ({ options, refresh }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsAPIInfo;
|
||||
export default SettingsAPIInfo;
|
||||
|
||||
@@ -30,21 +30,20 @@ import {
|
||||
Tag,
|
||||
Switch,
|
||||
TextArea,
|
||||
Tooltip
|
||||
Tooltip,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import {
|
||||
IllustrationNoResult,
|
||||
IllustrationNoResultDark
|
||||
IllustrationNoResultDark,
|
||||
} from '@douyinfe/semi-illustrations';
|
||||
import { Plus, Edit, Trash2, Save, Bell, Maximize2 } from 'lucide-react';
|
||||
import {
|
||||
Plus,
|
||||
Edit,
|
||||
Trash2,
|
||||
Save,
|
||||
Bell,
|
||||
Maximize2
|
||||
} from 'lucide-react';
|
||||
import { API, showError, showSuccess, getRelativeTime, formatDateTimeString } from '../../../helpers';
|
||||
API,
|
||||
showError,
|
||||
showSuccess,
|
||||
getRelativeTime,
|
||||
formatDateTimeString,
|
||||
} from '../../../helpers';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const { Text } = Typography;
|
||||
@@ -65,7 +64,7 @@ const SettingsAnnouncements = ({ options, refresh }) => {
|
||||
content: '',
|
||||
publishDate: new Date(),
|
||||
type: 'default',
|
||||
extra: ''
|
||||
extra: '',
|
||||
});
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
@@ -81,7 +80,7 @@ const SettingsAnnouncements = ({ options, refresh }) => {
|
||||
{ value: 'ongoing', label: t('进行中') },
|
||||
{ value: 'success', label: t('成功') },
|
||||
{ value: 'warning', label: t('警告') },
|
||||
{ value: 'error', label: t('错误') }
|
||||
{ value: 'error', label: t('错误') },
|
||||
];
|
||||
|
||||
const getTypeColor = (type) => {
|
||||
@@ -90,7 +89,7 @@ const SettingsAnnouncements = ({ options, refresh }) => {
|
||||
ongoing: 'blue',
|
||||
success: 'green',
|
||||
warning: 'orange',
|
||||
error: 'red'
|
||||
error: 'red',
|
||||
};
|
||||
return colorMap[type] || 'grey';
|
||||
};
|
||||
@@ -102,16 +101,18 @@ const SettingsAnnouncements = ({ options, refresh }) => {
|
||||
key: 'content',
|
||||
render: (text) => (
|
||||
<Tooltip content={text} position='topLeft' showArrow>
|
||||
<div style={{
|
||||
maxWidth: '300px',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap'
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
maxWidth: '300px',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
</div>
|
||||
</Tooltip>
|
||||
)
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t('发布时间'),
|
||||
@@ -123,15 +124,17 @@ const SettingsAnnouncements = ({ options, refresh }) => {
|
||||
<div style={{ fontWeight: 'bold' }}>
|
||||
{getRelativeTime(publishDate)}
|
||||
</div>
|
||||
<div style={{
|
||||
fontSize: '12px',
|
||||
color: 'var(--semi-color-text-2)',
|
||||
marginTop: '2px'
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
fontSize: '12px',
|
||||
color: 'var(--semi-color-text-2)',
|
||||
marginTop: '2px',
|
||||
}}
|
||||
>
|
||||
{publishDate ? formatDateTimeString(new Date(publishDate)) : '-'}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t('类型'),
|
||||
@@ -140,9 +143,9 @@ const SettingsAnnouncements = ({ options, refresh }) => {
|
||||
width: 100,
|
||||
render: (type) => (
|
||||
<Tag color={getTypeColor(type)} shape='circle'>
|
||||
{typeOptions.find(opt => opt.value === type)?.label || type}
|
||||
{typeOptions.find((opt) => opt.value === type)?.label || type}
|
||||
</Tag>
|
||||
)
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t('说明'),
|
||||
@@ -150,17 +153,19 @@ const SettingsAnnouncements = ({ options, refresh }) => {
|
||||
key: 'extra',
|
||||
render: (text) => (
|
||||
<Tooltip content={text || '-'} showArrow>
|
||||
<div style={{
|
||||
maxWidth: '200px',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
color: 'var(--semi-color-text-2)'
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
maxWidth: '200px',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
color: 'var(--semi-color-text-2)',
|
||||
}}
|
||||
>
|
||||
{text || '-'}
|
||||
</div>
|
||||
</Tooltip>
|
||||
)
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t('操作'),
|
||||
@@ -188,8 +193,8 @@ const SettingsAnnouncements = ({ options, refresh }) => {
|
||||
{t('删除')}
|
||||
</Button>
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const updateOption = async (key, value) => {
|
||||
@@ -226,7 +231,7 @@ const SettingsAnnouncements = ({ options, refresh }) => {
|
||||
content: '',
|
||||
publishDate: new Date(),
|
||||
type: 'default',
|
||||
extra: ''
|
||||
extra: '',
|
||||
});
|
||||
setShowAnnouncementModal(true);
|
||||
};
|
||||
@@ -235,9 +240,11 @@ const SettingsAnnouncements = ({ options, refresh }) => {
|
||||
setEditingAnnouncement(announcement);
|
||||
setAnnouncementForm({
|
||||
content: announcement.content,
|
||||
publishDate: announcement.publishDate ? new Date(announcement.publishDate) : new Date(),
|
||||
publishDate: announcement.publishDate
|
||||
? new Date(announcement.publishDate)
|
||||
: new Date(),
|
||||
type: announcement.type || 'default',
|
||||
extra: announcement.extra || ''
|
||||
extra: announcement.extra || '',
|
||||
});
|
||||
setShowAnnouncementModal(true);
|
||||
};
|
||||
@@ -249,7 +256,9 @@ const SettingsAnnouncements = ({ options, refresh }) => {
|
||||
|
||||
const confirmDeleteAnnouncement = () => {
|
||||
if (deletingAnnouncement) {
|
||||
const newList = announcementsList.filter(item => item.id !== deletingAnnouncement.id);
|
||||
const newList = announcementsList.filter(
|
||||
(item) => item.id !== deletingAnnouncement.id,
|
||||
);
|
||||
setAnnouncementsList(newList);
|
||||
setHasChanges(true);
|
||||
showSuccess('公告已删除,请及时点击“保存设置”进行保存');
|
||||
@@ -270,21 +279,20 @@ const SettingsAnnouncements = ({ options, refresh }) => {
|
||||
// 将publishDate转换为ISO字符串保存
|
||||
const formData = {
|
||||
...announcementForm,
|
||||
publishDate: announcementForm.publishDate.toISOString()
|
||||
publishDate: announcementForm.publishDate.toISOString(),
|
||||
};
|
||||
|
||||
let newList;
|
||||
if (editingAnnouncement) {
|
||||
newList = announcementsList.map(item =>
|
||||
item.id === editingAnnouncement.id
|
||||
? { ...item, ...formData }
|
||||
: item
|
||||
newList = announcementsList.map((item) =>
|
||||
item.id === editingAnnouncement.id ? { ...item, ...formData } : item,
|
||||
);
|
||||
} else {
|
||||
const newId = Math.max(...announcementsList.map(item => item.id), 0) + 1;
|
||||
const newId =
|
||||
Math.max(...announcementsList.map((item) => item.id), 0) + 1;
|
||||
const newAnnouncement = {
|
||||
id: newId,
|
||||
...formData
|
||||
...formData,
|
||||
};
|
||||
newList = [...announcementsList, newAnnouncement];
|
||||
}
|
||||
@@ -292,7 +300,11 @@ const SettingsAnnouncements = ({ options, refresh }) => {
|
||||
setAnnouncementsList(newList);
|
||||
setHasChanges(true);
|
||||
setShowAnnouncementModal(false);
|
||||
showSuccess(editingAnnouncement ? '公告已更新,请及时点击“保存设置”进行保存' : '公告已添加,请及时点击“保存设置”进行保存');
|
||||
showSuccess(
|
||||
editingAnnouncement
|
||||
? '公告已更新,请及时点击“保存设置”进行保存'
|
||||
: '公告已添加,请及时点击“保存设置”进行保存',
|
||||
);
|
||||
} catch (error) {
|
||||
showError('操作失败: ' + error.message);
|
||||
} finally {
|
||||
@@ -312,7 +324,7 @@ const SettingsAnnouncements = ({ options, refresh }) => {
|
||||
// 确保每个项目都有id
|
||||
const listWithIds = list.map((item, index) => ({
|
||||
...item,
|
||||
id: item.id || index + 1
|
||||
id: item.id || index + 1,
|
||||
}));
|
||||
setAnnouncementsList(listWithIds);
|
||||
} catch (error) {
|
||||
@@ -322,7 +334,8 @@ const SettingsAnnouncements = ({ options, refresh }) => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const annStr = options['console_setting.announcements'] ?? options.Announcements;
|
||||
const annStr =
|
||||
options['console_setting.announcements'] ?? options.Announcements;
|
||||
if (annStr !== undefined) {
|
||||
parseAnnouncements(annStr);
|
||||
}
|
||||
@@ -330,7 +343,11 @@ const SettingsAnnouncements = ({ options, refresh }) => {
|
||||
|
||||
useEffect(() => {
|
||||
const enabledStr = options['console_setting.announcements_enabled'];
|
||||
setPanelEnabled(enabledStr === undefined ? true : enabledStr === 'true' || enabledStr === true);
|
||||
setPanelEnabled(
|
||||
enabledStr === undefined
|
||||
? true
|
||||
: enabledStr === 'true' || enabledStr === true,
|
||||
);
|
||||
}, [options['console_setting.announcements_enabled']]);
|
||||
|
||||
const handleToggleEnabled = async (checked) => {
|
||||
@@ -358,31 +375,39 @@ const SettingsAnnouncements = ({ options, refresh }) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const newList = announcementsList.filter(item => !selectedRowKeys.includes(item.id));
|
||||
const newList = announcementsList.filter(
|
||||
(item) => !selectedRowKeys.includes(item.id),
|
||||
);
|
||||
setAnnouncementsList(newList);
|
||||
setSelectedRowKeys([]);
|
||||
setHasChanges(true);
|
||||
showSuccess(`已删除 ${selectedRowKeys.length} 个系统公告,请及时点击“保存设置”进行保存`);
|
||||
showSuccess(
|
||||
`已删除 ${selectedRowKeys.length} 个系统公告,请及时点击“保存设置”进行保存`,
|
||||
);
|
||||
};
|
||||
|
||||
const renderHeader = () => (
|
||||
<div className="flex flex-col w-full">
|
||||
<div className="mb-2">
|
||||
<div className="flex items-center text-blue-500">
|
||||
<Bell size={16} className="mr-2" />
|
||||
<Text>{t('系统公告管理,可以发布系统通知和重要消息(最多100个,前端显示最新20条)')}</Text>
|
||||
<div className='flex flex-col w-full'>
|
||||
<div className='mb-2'>
|
||||
<div className='flex items-center text-blue-500'>
|
||||
<Bell size={16} className='mr-2' />
|
||||
<Text>
|
||||
{t(
|
||||
'系统公告管理,可以发布系统通知和重要消息(最多100个,前端显示最新20条)',
|
||||
)}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Divider margin="12px" />
|
||||
<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">
|
||||
<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"
|
||||
className='w-full md:w-auto'
|
||||
onClick={handleAddAnnouncement}
|
||||
>
|
||||
{t('添加公告')}
|
||||
@@ -393,9 +418,10 @@ const SettingsAnnouncements = ({ options, refresh }) => {
|
||||
theme='light'
|
||||
onClick={handleBatchDelete}
|
||||
disabled={selectedRowKeys.length === 0}
|
||||
className="w-full md:w-auto"
|
||||
className='w-full md:w-auto'
|
||||
>
|
||||
{t('批量删除')} {selectedRowKeys.length > 0 && `(${selectedRowKeys.length})`}
|
||||
{t('批量删除')}{' '}
|
||||
{selectedRowKeys.length > 0 && `(${selectedRowKeys.length})`}
|
||||
</Button>
|
||||
<Button
|
||||
icon={<Save size={14} />}
|
||||
@@ -403,14 +429,14 @@ const SettingsAnnouncements = ({ options, refresh }) => {
|
||||
loading={loading}
|
||||
disabled={!hasChanges}
|
||||
type='secondary'
|
||||
className="w-full md:w-auto"
|
||||
className='w-full md:w-auto'
|
||||
>
|
||||
{t('保存设置')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 启用开关 */}
|
||||
<div className="order-1 md:order-2 flex items-center gap-2">
|
||||
<div className='order-1 md:order-2 flex items-center gap-2'>
|
||||
<Switch checked={panelEnabled} onChange={handleToggleEnabled} />
|
||||
<Text>{panelEnabled ? t('已启用') : t('已禁用')}</Text>
|
||||
</div>
|
||||
@@ -455,7 +481,7 @@ const SettingsAnnouncements = ({ options, refresh }) => {
|
||||
columns={columns}
|
||||
dataSource={getCurrentPageData()}
|
||||
rowSelection={rowSelection}
|
||||
rowKey="id"
|
||||
rowKey='id'
|
||||
scroll={{ x: 'max-content' }}
|
||||
pagination={{
|
||||
currentPage: currentPage,
|
||||
@@ -471,19 +497,23 @@ const SettingsAnnouncements = ({ options, refresh }) => {
|
||||
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 }} />}
|
||||
image={
|
||||
<IllustrationNoResult style={{ width: 150, height: 150 }} />
|
||||
}
|
||||
darkModeImage={
|
||||
<IllustrationNoResultDark style={{ width: 150, height: 150 }} />
|
||||
}
|
||||
description={t('暂无系统公告')}
|
||||
style={{ padding: 30 }}
|
||||
/>
|
||||
}
|
||||
className="overflow-hidden"
|
||||
className='overflow-hidden'
|
||||
/>
|
||||
</Form.Section>
|
||||
|
||||
@@ -509,7 +539,9 @@ const SettingsAnnouncements = ({ options, refresh }) => {
|
||||
maxCount={500}
|
||||
rows={3}
|
||||
rules={[{ required: true, message: t('请输入公告内容') }]}
|
||||
onChange={(value) => setAnnouncementForm({ ...announcementForm, content: value })}
|
||||
onChange={(value) =>
|
||||
setAnnouncementForm({ ...announcementForm, content: value })
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
theme='light'
|
||||
@@ -526,19 +558,25 @@ const SettingsAnnouncements = ({ options, refresh }) => {
|
||||
label={t('发布日期')}
|
||||
type='dateTime'
|
||||
rules={[{ required: true, message: t('请选择发布日期') }]}
|
||||
onChange={(value) => setAnnouncementForm({ ...announcementForm, publishDate: value })}
|
||||
onChange={(value) =>
|
||||
setAnnouncementForm({ ...announcementForm, publishDate: value })
|
||||
}
|
||||
/>
|
||||
<Form.Select
|
||||
field='type'
|
||||
label={t('公告类型')}
|
||||
optionList={typeOptions}
|
||||
onChange={(value) => setAnnouncementForm({ ...announcementForm, type: value })}
|
||||
onChange={(value) =>
|
||||
setAnnouncementForm({ ...announcementForm, type: value })
|
||||
}
|
||||
/>
|
||||
<Form.Input
|
||||
field='extra'
|
||||
label={t('说明信息')}
|
||||
placeholder={t('可选,公告的补充说明')}
|
||||
onChange={(value) => setAnnouncementForm({ ...announcementForm, extra: value })}
|
||||
onChange={(value) =>
|
||||
setAnnouncementForm({ ...announcementForm, extra: value })
|
||||
}
|
||||
/>
|
||||
</Form>
|
||||
</Modal>
|
||||
@@ -553,10 +591,10 @@ const SettingsAnnouncements = ({ options, refresh }) => {
|
||||
}}
|
||||
okText={t('确认删除')}
|
||||
cancelText={t('取消')}
|
||||
type="warning"
|
||||
type='warning'
|
||||
okButtonProps={{
|
||||
type: 'danger',
|
||||
theme: 'solid'
|
||||
theme: 'solid',
|
||||
}}
|
||||
>
|
||||
<Text>{t('确定要删除此公告吗?')}</Text>
|
||||
@@ -584,11 +622,13 @@ const SettingsAnnouncements = ({ options, refresh }) => {
|
||||
maxCount={500}
|
||||
rows={15}
|
||||
style={{ width: '100%' }}
|
||||
onChange={(value) => setAnnouncementForm({ ...announcementForm, content: value })}
|
||||
onChange={(value) =>
|
||||
setAnnouncementForm({ ...announcementForm, content: value })
|
||||
}
|
||||
/>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsAnnouncements;
|
||||
export default SettingsAnnouncements;
|
||||
|
||||
@@ -28,19 +28,13 @@ import {
|
||||
Divider,
|
||||
Modal,
|
||||
Switch,
|
||||
Tooltip
|
||||
Tooltip,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import {
|
||||
IllustrationNoResult,
|
||||
IllustrationNoResultDark
|
||||
IllustrationNoResultDark,
|
||||
} from '@douyinfe/semi-illustrations';
|
||||
import {
|
||||
Plus,
|
||||
Edit,
|
||||
Trash2,
|
||||
Save,
|
||||
HelpCircle
|
||||
} from 'lucide-react';
|
||||
import { Plus, Edit, Trash2, Save, HelpCircle } from 'lucide-react';
|
||||
import { API, showError, showSuccess } from '../../../helpers';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
@@ -59,7 +53,7 @@ const SettingsFAQ = ({ options, refresh }) => {
|
||||
const [hasChanges, setHasChanges] = useState(false);
|
||||
const [faqForm, setFaqForm] = useState({
|
||||
question: '',
|
||||
answer: ''
|
||||
answer: '',
|
||||
});
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
@@ -75,17 +69,19 @@ const SettingsFAQ = ({ options, refresh }) => {
|
||||
key: 'question',
|
||||
render: (text) => (
|
||||
<Tooltip content={text} showArrow>
|
||||
<div style={{
|
||||
maxWidth: '300px',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
fontWeight: 'bold'
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
maxWidth: '300px',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
fontWeight: 'bold',
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
</div>
|
||||
</Tooltip>
|
||||
)
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t('回答内容'),
|
||||
@@ -93,17 +89,19 @@ const SettingsFAQ = ({ options, refresh }) => {
|
||||
key: 'answer',
|
||||
render: (text) => (
|
||||
<Tooltip content={text} showArrow>
|
||||
<div style={{
|
||||
maxWidth: '400px',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
color: 'var(--semi-color-text-1)'
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
maxWidth: '400px',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
color: 'var(--semi-color-text-1)',
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
</div>
|
||||
</Tooltip>
|
||||
)
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t('操作'),
|
||||
@@ -131,8 +129,8 @@ const SettingsFAQ = ({ options, refresh }) => {
|
||||
{t('删除')}
|
||||
</Button>
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const updateOption = async (key, value) => {
|
||||
@@ -167,7 +165,7 @@ const SettingsFAQ = ({ options, refresh }) => {
|
||||
setEditingFaq(null);
|
||||
setFaqForm({
|
||||
question: '',
|
||||
answer: ''
|
||||
answer: '',
|
||||
});
|
||||
setShowFaqModal(true);
|
||||
};
|
||||
@@ -176,7 +174,7 @@ const SettingsFAQ = ({ options, refresh }) => {
|
||||
setEditingFaq(faq);
|
||||
setFaqForm({
|
||||
question: faq.question,
|
||||
answer: faq.answer
|
||||
answer: faq.answer,
|
||||
});
|
||||
setShowFaqModal(true);
|
||||
};
|
||||
@@ -188,7 +186,7 @@ const SettingsFAQ = ({ options, refresh }) => {
|
||||
|
||||
const confirmDeleteFaq = () => {
|
||||
if (deletingFaq) {
|
||||
const newList = faqList.filter(item => item.id !== deletingFaq.id);
|
||||
const newList = faqList.filter((item) => item.id !== deletingFaq.id);
|
||||
setFaqList(newList);
|
||||
setHasChanges(true);
|
||||
showSuccess('问答已删除,请及时点击“保存设置”进行保存');
|
||||
@@ -208,16 +206,14 @@ const SettingsFAQ = ({ options, refresh }) => {
|
||||
|
||||
let newList;
|
||||
if (editingFaq) {
|
||||
newList = faqList.map(item =>
|
||||
item.id === editingFaq.id
|
||||
? { ...item, ...faqForm }
|
||||
: item
|
||||
newList = faqList.map((item) =>
|
||||
item.id === editingFaq.id ? { ...item, ...faqForm } : item,
|
||||
);
|
||||
} else {
|
||||
const newId = Math.max(...faqList.map(item => item.id), 0) + 1;
|
||||
const newId = Math.max(...faqList.map((item) => item.id), 0) + 1;
|
||||
const newFaq = {
|
||||
id: newId,
|
||||
...faqForm
|
||||
...faqForm,
|
||||
};
|
||||
newList = [...faqList, newFaq];
|
||||
}
|
||||
@@ -225,7 +221,11 @@ const SettingsFAQ = ({ options, refresh }) => {
|
||||
setFaqList(newList);
|
||||
setHasChanges(true);
|
||||
setShowFaqModal(false);
|
||||
showSuccess(editingFaq ? '问答已更新,请及时点击“保存设置”进行保存' : '问答已添加,请及时点击“保存设置”进行保存');
|
||||
showSuccess(
|
||||
editingFaq
|
||||
? '问答已更新,请及时点击“保存设置”进行保存'
|
||||
: '问答已添加,请及时点击“保存设置”进行保存',
|
||||
);
|
||||
} catch (error) {
|
||||
showError('操作失败: ' + error.message);
|
||||
} finally {
|
||||
@@ -245,7 +245,7 @@ const SettingsFAQ = ({ options, refresh }) => {
|
||||
// 确保每个项目都有id
|
||||
const listWithIds = list.map((item, index) => ({
|
||||
...item,
|
||||
id: item.id || index + 1
|
||||
id: item.id || index + 1,
|
||||
}));
|
||||
setFaqList(listWithIds);
|
||||
} catch (error) {
|
||||
@@ -262,7 +262,11 @@ const SettingsFAQ = ({ options, refresh }) => {
|
||||
|
||||
useEffect(() => {
|
||||
const enabledStr = options['console_setting.faq_enabled'];
|
||||
setPanelEnabled(enabledStr === undefined ? true : enabledStr === 'true' || enabledStr === true);
|
||||
setPanelEnabled(
|
||||
enabledStr === undefined
|
||||
? true
|
||||
: enabledStr === 'true' || enabledStr === true,
|
||||
);
|
||||
}, [options['console_setting.faq_enabled']]);
|
||||
|
||||
const handleToggleEnabled = async (checked) => {
|
||||
@@ -290,31 +294,39 @@ const SettingsFAQ = ({ options, refresh }) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const newList = faqList.filter(item => !selectedRowKeys.includes(item.id));
|
||||
const newList = faqList.filter(
|
||||
(item) => !selectedRowKeys.includes(item.id),
|
||||
);
|
||||
setFaqList(newList);
|
||||
setSelectedRowKeys([]);
|
||||
setHasChanges(true);
|
||||
showSuccess(`已删除 ${selectedRowKeys.length} 个常见问答,请及时点击“保存设置”进行保存`);
|
||||
showSuccess(
|
||||
`已删除 ${selectedRowKeys.length} 个常见问答,请及时点击“保存设置”进行保存`,
|
||||
);
|
||||
};
|
||||
|
||||
const renderHeader = () => (
|
||||
<div className="flex flex-col w-full">
|
||||
<div className="mb-2">
|
||||
<div className="flex items-center text-blue-500">
|
||||
<HelpCircle size={16} className="mr-2" />
|
||||
<Text>{t('常见问答管理,为用户提供常见问题的答案(最多50个,前端显示最新20条)')}</Text>
|
||||
<div className='flex flex-col w-full'>
|
||||
<div className='mb-2'>
|
||||
<div className='flex items-center text-blue-500'>
|
||||
<HelpCircle size={16} className='mr-2' />
|
||||
<Text>
|
||||
{t(
|
||||
'常见问答管理,为用户提供常见问题的答案(最多50个,前端显示最新20条)',
|
||||
)}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Divider margin="12px" />
|
||||
<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">
|
||||
<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"
|
||||
className='w-full md:w-auto'
|
||||
onClick={handleAddFaq}
|
||||
>
|
||||
{t('添加问答')}
|
||||
@@ -325,9 +337,10 @@ const SettingsFAQ = ({ options, refresh }) => {
|
||||
theme='light'
|
||||
onClick={handleBatchDelete}
|
||||
disabled={selectedRowKeys.length === 0}
|
||||
className="w-full md:w-auto"
|
||||
className='w-full md:w-auto'
|
||||
>
|
||||
{t('批量删除')} {selectedRowKeys.length > 0 && `(${selectedRowKeys.length})`}
|
||||
{t('批量删除')}{' '}
|
||||
{selectedRowKeys.length > 0 && `(${selectedRowKeys.length})`}
|
||||
</Button>
|
||||
<Button
|
||||
icon={<Save size={14} />}
|
||||
@@ -335,14 +348,14 @@ const SettingsFAQ = ({ options, refresh }) => {
|
||||
loading={loading}
|
||||
disabled={!hasChanges}
|
||||
type='secondary'
|
||||
className="w-full md:w-auto"
|
||||
className='w-full md:w-auto'
|
||||
>
|
||||
{t('保存设置')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 启用开关 */}
|
||||
<div className="order-1 md:order-2 flex items-center gap-2">
|
||||
<div className='order-1 md:order-2 flex items-center gap-2'>
|
||||
<Switch checked={panelEnabled} onChange={handleToggleEnabled} />
|
||||
<Text>{panelEnabled ? t('已启用') : t('已禁用')}</Text>
|
||||
</div>
|
||||
@@ -381,7 +394,7 @@ const SettingsFAQ = ({ options, refresh }) => {
|
||||
columns={columns}
|
||||
dataSource={getCurrentPageData()}
|
||||
rowSelection={rowSelection}
|
||||
rowKey="id"
|
||||
rowKey='id'
|
||||
scroll={{ x: 'max-content' }}
|
||||
pagination={{
|
||||
currentPage: currentPage,
|
||||
@@ -397,19 +410,23 @@ const SettingsFAQ = ({ options, refresh }) => {
|
||||
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 }} />}
|
||||
image={
|
||||
<IllustrationNoResult style={{ width: 150, height: 150 }} />
|
||||
}
|
||||
darkModeImage={
|
||||
<IllustrationNoResultDark style={{ width: 150, height: 150 }} />
|
||||
}
|
||||
description={t('暂无常见问答')}
|
||||
style={{ padding: 30 }}
|
||||
/>
|
||||
}
|
||||
className="overflow-hidden"
|
||||
className='overflow-hidden'
|
||||
/>
|
||||
</Form.Section>
|
||||
|
||||
@@ -423,7 +440,11 @@ const SettingsFAQ = ({ options, refresh }) => {
|
||||
confirmLoading={modalLoading}
|
||||
width={800}
|
||||
>
|
||||
<Form layout='vertical' initValues={faqForm} key={editingFaq ? editingFaq.id : 'new'}>
|
||||
<Form
|
||||
layout='vertical'
|
||||
initValues={faqForm}
|
||||
key={editingFaq ? editingFaq.id : 'new'}
|
||||
>
|
||||
<Form.Input
|
||||
field='question'
|
||||
label={t('问题标题')}
|
||||
@@ -454,10 +475,10 @@ const SettingsFAQ = ({ options, refresh }) => {
|
||||
}}
|
||||
okText={t('确认删除')}
|
||||
cancelText={t('取消')}
|
||||
type="warning"
|
||||
type='warning'
|
||||
okButtonProps={{
|
||||
type: 'danger',
|
||||
theme: 'solid'
|
||||
theme: 'solid',
|
||||
}}
|
||||
>
|
||||
<Text>{t('确定要删除此问答吗?')}</Text>
|
||||
@@ -466,4 +487,4 @@ const SettingsFAQ = ({ options, refresh }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsFAQ;
|
||||
export default SettingsFAQ;
|
||||
|
||||
@@ -27,19 +27,13 @@ import {
|
||||
Empty,
|
||||
Divider,
|
||||
Modal,
|
||||
Switch
|
||||
Switch,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import {
|
||||
IllustrationNoResult,
|
||||
IllustrationNoResultDark
|
||||
IllustrationNoResultDark,
|
||||
} from '@douyinfe/semi-illustrations';
|
||||
import {
|
||||
Plus,
|
||||
Edit,
|
||||
Trash2,
|
||||
Save,
|
||||
Activity
|
||||
} from 'lucide-react';
|
||||
import { Plus, Edit, Trash2, Save, Activity } from 'lucide-react';
|
||||
import { API, showError, showSuccess } from '../../../helpers';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
@@ -72,41 +66,47 @@ const SettingsUptimeKuma = ({ options, refresh }) => {
|
||||
dataIndex: 'categoryName',
|
||||
key: 'categoryName',
|
||||
render: (text) => (
|
||||
<div style={{
|
||||
fontWeight: 'bold',
|
||||
color: 'var(--semi-color-text-0)'
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
fontWeight: 'bold',
|
||||
color: 'var(--semi-color-text-0)',
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
</div>
|
||||
)
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t('Uptime Kuma地址'),
|
||||
dataIndex: 'url',
|
||||
key: 'url',
|
||||
render: (text) => (
|
||||
<div style={{
|
||||
maxWidth: '300px',
|
||||
wordBreak: 'break-all',
|
||||
fontFamily: 'monospace',
|
||||
color: 'var(--semi-color-primary)'
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
maxWidth: '300px',
|
||||
wordBreak: 'break-all',
|
||||
fontFamily: 'monospace',
|
||||
color: 'var(--semi-color-primary)',
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
</div>
|
||||
)
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t('状态页面Slug'),
|
||||
dataIndex: 'slug',
|
||||
key: 'slug',
|
||||
render: (text) => (
|
||||
<div style={{
|
||||
fontFamily: 'monospace',
|
||||
color: 'var(--semi-color-text-1)'
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
fontFamily: 'monospace',
|
||||
color: 'var(--semi-color-text-1)',
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
</div>
|
||||
)
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t('操作'),
|
||||
@@ -134,8 +134,8 @@ const SettingsUptimeKuma = ({ options, refresh }) => {
|
||||
{t('删除')}
|
||||
</Button>
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const updateOption = async (key, value) => {
|
||||
@@ -193,7 +193,9 @@ const SettingsUptimeKuma = ({ options, refresh }) => {
|
||||
|
||||
const confirmDeleteGroup = () => {
|
||||
if (deletingGroup) {
|
||||
const newList = uptimeGroupsList.filter(item => item.id !== deletingGroup.id);
|
||||
const newList = uptimeGroupsList.filter(
|
||||
(item) => item.id !== deletingGroup.id,
|
||||
);
|
||||
setUptimeGroupsList(newList);
|
||||
setHasChanges(true);
|
||||
showSuccess('分类已删除,请及时点击“保存设置”进行保存');
|
||||
@@ -225,16 +227,15 @@ const SettingsUptimeKuma = ({ options, refresh }) => {
|
||||
|
||||
let newList;
|
||||
if (editingGroup) {
|
||||
newList = uptimeGroupsList.map(item =>
|
||||
item.id === editingGroup.id
|
||||
? { ...item, ...uptimeForm }
|
||||
: item
|
||||
newList = uptimeGroupsList.map((item) =>
|
||||
item.id === editingGroup.id ? { ...item, ...uptimeForm } : item,
|
||||
);
|
||||
} else {
|
||||
const newId = Math.max(...uptimeGroupsList.map(item => item.id), 0) + 1;
|
||||
const newId =
|
||||
Math.max(...uptimeGroupsList.map((item) => item.id), 0) + 1;
|
||||
const newGroup = {
|
||||
id: newId,
|
||||
...uptimeForm
|
||||
...uptimeForm,
|
||||
};
|
||||
newList = [...uptimeGroupsList, newGroup];
|
||||
}
|
||||
@@ -242,7 +243,11 @@ const SettingsUptimeKuma = ({ options, refresh }) => {
|
||||
setUptimeGroupsList(newList);
|
||||
setHasChanges(true);
|
||||
setShowUptimeModal(false);
|
||||
showSuccess(editingGroup ? '分类已更新,请及时点击“保存设置”进行保存' : '分类已添加,请及时点击“保存设置”进行保存');
|
||||
showSuccess(
|
||||
editingGroup
|
||||
? '分类已更新,请及时点击“保存设置”进行保存'
|
||||
: '分类已添加,请及时点击“保存设置”进行保存',
|
||||
);
|
||||
} catch (error) {
|
||||
showError('操作失败: ' + error.message);
|
||||
} finally {
|
||||
@@ -261,7 +266,7 @@ const SettingsUptimeKuma = ({ options, refresh }) => {
|
||||
const list = Array.isArray(parsed) ? parsed : [];
|
||||
const listWithIds = list.map((item, index) => ({
|
||||
...item,
|
||||
id: item.id || index + 1
|
||||
id: item.id || index + 1,
|
||||
}));
|
||||
setUptimeGroupsList(listWithIds);
|
||||
} catch (error) {
|
||||
@@ -279,7 +284,11 @@ const SettingsUptimeKuma = ({ options, refresh }) => {
|
||||
|
||||
useEffect(() => {
|
||||
const enabledStr = options['console_setting.uptime_kuma_enabled'];
|
||||
setPanelEnabled(enabledStr === undefined ? true : enabledStr === 'true' || enabledStr === true);
|
||||
setPanelEnabled(
|
||||
enabledStr === undefined
|
||||
? true
|
||||
: enabledStr === 'true' || enabledStr === true,
|
||||
);
|
||||
}, [options['console_setting.uptime_kuma_enabled']]);
|
||||
|
||||
const handleToggleEnabled = async (checked) => {
|
||||
@@ -307,31 +316,39 @@ const SettingsUptimeKuma = ({ options, refresh }) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const newList = uptimeGroupsList.filter(item => !selectedRowKeys.includes(item.id));
|
||||
const newList = uptimeGroupsList.filter(
|
||||
(item) => !selectedRowKeys.includes(item.id),
|
||||
);
|
||||
setUptimeGroupsList(newList);
|
||||
setSelectedRowKeys([]);
|
||||
setHasChanges(true);
|
||||
showSuccess(`已删除 ${selectedRowKeys.length} 个分类,请及时点击“保存设置”进行保存`);
|
||||
showSuccess(
|
||||
`已删除 ${selectedRowKeys.length} 个分类,请及时点击“保存设置”进行保存`,
|
||||
);
|
||||
};
|
||||
|
||||
const renderHeader = () => (
|
||||
<div className="flex flex-col w-full">
|
||||
<div className="mb-2">
|
||||
<div className="flex items-center text-blue-500">
|
||||
<Activity size={16} className="mr-2" />
|
||||
<Text>{t('Uptime Kuma监控分类管理,可以配置多个监控分类用于服务状态展示(最多20个)')}</Text>
|
||||
<div className='flex flex-col w-full'>
|
||||
<div className='mb-2'>
|
||||
<div className='flex items-center text-blue-500'>
|
||||
<Activity size={16} className='mr-2' />
|
||||
<Text>
|
||||
{t(
|
||||
'Uptime Kuma监控分类管理,可以配置多个监控分类用于服务状态展示(最多20个)',
|
||||
)}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Divider margin="12px" />
|
||||
<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">
|
||||
<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"
|
||||
className='w-full md:w-auto'
|
||||
onClick={handleAddGroup}
|
||||
>
|
||||
{t('添加分类')}
|
||||
@@ -342,9 +359,10 @@ const SettingsUptimeKuma = ({ options, refresh }) => {
|
||||
theme='light'
|
||||
onClick={handleBatchDelete}
|
||||
disabled={selectedRowKeys.length === 0}
|
||||
className="w-full md:w-auto"
|
||||
className='w-full md:w-auto'
|
||||
>
|
||||
{t('批量删除')} {selectedRowKeys.length > 0 && `(${selectedRowKeys.length})`}
|
||||
{t('批量删除')}{' '}
|
||||
{selectedRowKeys.length > 0 && `(${selectedRowKeys.length})`}
|
||||
</Button>
|
||||
<Button
|
||||
icon={<Save size={14} />}
|
||||
@@ -352,14 +370,14 @@ const SettingsUptimeKuma = ({ options, refresh }) => {
|
||||
loading={loading}
|
||||
disabled={!hasChanges}
|
||||
type='secondary'
|
||||
className="w-full md:w-auto"
|
||||
className='w-full md:w-auto'
|
||||
>
|
||||
{t('保存设置')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 启用开关 */}
|
||||
<div className="order-1 md:order-2 flex items-center gap-2">
|
||||
<div className='order-1 md:order-2 flex items-center gap-2'>
|
||||
<Switch checked={panelEnabled} onChange={handleToggleEnabled} />
|
||||
<Text>{panelEnabled ? t('已启用') : t('已禁用')}</Text>
|
||||
</div>
|
||||
@@ -397,7 +415,7 @@ const SettingsUptimeKuma = ({ options, refresh }) => {
|
||||
columns={columns}
|
||||
dataSource={getCurrentPageData()}
|
||||
rowSelection={rowSelection}
|
||||
rowKey="id"
|
||||
rowKey='id'
|
||||
scroll={{ x: 'max-content' }}
|
||||
pagination={{
|
||||
currentPage: currentPage,
|
||||
@@ -413,19 +431,23 @@ const SettingsUptimeKuma = ({ options, refresh }) => {
|
||||
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 }} />}
|
||||
image={
|
||||
<IllustrationNoResult style={{ width: 150, height: 150 }} />
|
||||
}
|
||||
darkModeImage={
|
||||
<IllustrationNoResultDark style={{ width: 150, height: 150 }} />
|
||||
}
|
||||
description={t('暂无监控数据')}
|
||||
style={{ padding: 30 }}
|
||||
/>
|
||||
}
|
||||
className="overflow-hidden"
|
||||
className='overflow-hidden'
|
||||
/>
|
||||
</Form.Section>
|
||||
|
||||
@@ -439,19 +461,27 @@ const SettingsUptimeKuma = ({ options, refresh }) => {
|
||||
confirmLoading={modalLoading}
|
||||
width={600}
|
||||
>
|
||||
<Form layout='vertical' initValues={uptimeForm} key={editingGroup ? editingGroup.id : 'new'}>
|
||||
<Form
|
||||
layout='vertical'
|
||||
initValues={uptimeForm}
|
||||
key={editingGroup ? editingGroup.id : 'new'}
|
||||
>
|
||||
<Form.Input
|
||||
field='categoryName'
|
||||
label={t('分类名称')}
|
||||
placeholder={t('请输入分类名称,如:OpenAI、Claude等')}
|
||||
maxLength={50}
|
||||
rules={[{ required: true, message: t('请输入分类名称') }]}
|
||||
onChange={(value) => setUptimeForm({ ...uptimeForm, categoryName: value })}
|
||||
onChange={(value) =>
|
||||
setUptimeForm({ ...uptimeForm, categoryName: value })
|
||||
}
|
||||
/>
|
||||
<Form.Input
|
||||
field='url'
|
||||
label={t('Uptime Kuma地址')}
|
||||
placeholder={t('请输入Uptime Kuma服务地址,如:https://status.example.com')}
|
||||
placeholder={t(
|
||||
'请输入Uptime Kuma服务地址,如:https://status.example.com',
|
||||
)}
|
||||
maxLength={500}
|
||||
rules={[{ required: true, message: t('请输入Uptime Kuma地址') }]}
|
||||
onChange={(value) => setUptimeForm({ ...uptimeForm, url: value })}
|
||||
@@ -477,10 +507,10 @@ const SettingsUptimeKuma = ({ options, refresh }) => {
|
||||
}}
|
||||
okText={t('确认删除')}
|
||||
cancelText={t('取消')}
|
||||
type="warning"
|
||||
type='warning'
|
||||
okButtonProps={{
|
||||
type: 'danger',
|
||||
theme: 'solid'
|
||||
theme: 'solid',
|
||||
}}
|
||||
>
|
||||
<Text>{t('确定要删除此分类吗?')}</Text>
|
||||
@@ -489,4 +519,4 @@ const SettingsUptimeKuma = ({ options, refresh }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsUptimeKuma;
|
||||
export default SettingsUptimeKuma;
|
||||
|
||||
@@ -171,8 +171,21 @@ export default function SettingGeminiModel(props) {
|
||||
<Form.TextArea
|
||||
field={'gemini.supported_imagine_models'}
|
||||
label={t('支持的图像模型')}
|
||||
placeholder={t('例如:') + '\n' + JSON.stringify(['gemini-2.0-flash-exp-image-generation'], null, 2)}
|
||||
onChange={(value) => setInputs({ ...inputs, 'gemini.supported_imagine_models': value })}
|
||||
placeholder={
|
||||
t('例如:') +
|
||||
'\n' +
|
||||
JSON.stringify(
|
||||
['gemini-2.0-flash-exp-image-generation'],
|
||||
null,
|
||||
2,
|
||||
)
|
||||
}
|
||||
onChange={(value) =>
|
||||
setInputs({
|
||||
...inputs,
|
||||
'gemini.supported_imagine_models': value,
|
||||
})
|
||||
}
|
||||
trigger='blur'
|
||||
stopValidateWithError
|
||||
rules={[
|
||||
@@ -191,9 +204,9 @@ export default function SettingGeminiModel(props) {
|
||||
<Col span={16}>
|
||||
<Text>
|
||||
{t(
|
||||
"和Claude不同,默认情况下Gemini的思考模型会自动决定要不要思考,就算不开启适配模型也可以正常使用," +
|
||||
"如果您需要计费,推荐设置无后缀模型价格按思考价格设置。" +
|
||||
"支持使用 gemini-2.5-pro-preview-06-05-thinking-128 格式来精确传递思考预算。"
|
||||
'和Claude不同,默认情况下Gemini的思考模型会自动决定要不要思考,就算不开启适配模型也可以正常使用,' +
|
||||
'如果您需要计费,推荐设置无后缀模型价格按思考价格设置。' +
|
||||
'支持使用 gemini-2.5-pro-preview-06-05-thinking-128 格式来精确传递思考预算。',
|
||||
)}
|
||||
</Text>
|
||||
</Col>
|
||||
@@ -203,7 +216,9 @@ export default function SettingGeminiModel(props) {
|
||||
<Form.Switch
|
||||
label={t('启用Gemini思考后缀适配')}
|
||||
field={'gemini.thinking_adapter_enabled'}
|
||||
extraText={t('适配 -thinking、-thinking-预算数字 和 -nothinking 后缀')}
|
||||
extraText={t(
|
||||
'适配 -thinking、-thinking-预算数字 和 -nothinking 后缀',
|
||||
)}
|
||||
onChange={(value) =>
|
||||
setInputs({
|
||||
...inputs,
|
||||
|
||||
@@ -110,22 +110,27 @@ export default function SettingGlobalModel(props) {
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
|
||||
<Form.Section text={t('连接保活设置')}>
|
||||
<Row style={{ marginTop: 10 }}>
|
||||
<Col span={24}>
|
||||
<Banner
|
||||
type="warning"
|
||||
description="警告:启用保活后,如果已经写入保活数据后渠道出错,系统无法重试,如果必须开启,推荐设置尽可能大的Ping间隔"
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{ marginTop: 10 }}>
|
||||
<Col span={24}>
|
||||
<Banner
|
||||
type='warning'
|
||||
description='警告:启用保活后,如果已经写入保活数据后渠道出错,系统无法重试,如果必须开启,推荐设置尽可能大的Ping间隔'
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
|
||||
<Form.Switch
|
||||
label={t('启用Ping间隔')}
|
||||
field={'general_setting.ping_interval_enabled'}
|
||||
onChange={(value) => setInputs({ ...inputs, 'general_setting.ping_interval_enabled': value })}
|
||||
onChange={(value) =>
|
||||
setInputs({
|
||||
...inputs,
|
||||
'general_setting.ping_interval_enabled': value,
|
||||
})
|
||||
}
|
||||
extraText={'开启后,将定期发送ping数据保持连接活跃'}
|
||||
/>
|
||||
</Col>
|
||||
@@ -133,7 +138,12 @@ export default function SettingGlobalModel(props) {
|
||||
<Form.InputNumber
|
||||
label={t('Ping间隔(秒)')}
|
||||
field={'general_setting.ping_interval_seconds'}
|
||||
onChange={(value) => setInputs({ ...inputs, 'general_setting.ping_interval_seconds': value })}
|
||||
onChange={(value) =>
|
||||
setInputs({
|
||||
...inputs,
|
||||
'general_setting.ping_interval_seconds': value,
|
||||
})
|
||||
}
|
||||
min={1}
|
||||
disabled={!inputs['general_setting.ping_interval_enabled']}
|
||||
/>
|
||||
|
||||
@@ -18,15 +18,7 @@ For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import {
|
||||
Banner,
|
||||
Button,
|
||||
Col,
|
||||
Form,
|
||||
Row,
|
||||
Spin,
|
||||
Modal,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import { Banner, Button, Col, Form, Row, Spin, Modal } from '@douyinfe/semi-ui';
|
||||
import {
|
||||
compareObjects,
|
||||
API,
|
||||
|
||||
@@ -18,11 +18,7 @@ For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Form,
|
||||
Spin,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import { Button, Form, Spin } from '@douyinfe/semi-ui';
|
||||
import {
|
||||
API,
|
||||
removeTrailingSlash,
|
||||
@@ -41,7 +37,9 @@ export default function SettingsGeneralPayment(props) {
|
||||
|
||||
useEffect(() => {
|
||||
if (props.options && formApiRef.current) {
|
||||
const currentInputs = { ServerAddress: props.options.ServerAddress || '' };
|
||||
const currentInputs = {
|
||||
ServerAddress: props.options.ServerAddress || '',
|
||||
};
|
||||
setInputs(currentInputs);
|
||||
formApiRef.current.setValues(currentInputs);
|
||||
}
|
||||
@@ -84,11 +82,13 @@ export default function SettingsGeneralPayment(props) {
|
||||
label={t('服务器地址')}
|
||||
placeholder={'https://yourdomain.com'}
|
||||
style={{ width: '100%' }}
|
||||
extraText={t('该服务器地址将影响支付回调地址以及默认首页展示的地址,请确保正确配置')}
|
||||
extraText={t(
|
||||
'该服务器地址将影响支付回调地址以及默认首页展示的地址,请确保正确配置',
|
||||
)}
|
||||
/>
|
||||
<Button onClick={submitServerAddress}>{t('更新服务器地址')}</Button>
|
||||
</Form.Section>
|
||||
</Form>
|
||||
</Spin>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,14 +18,7 @@ For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Form,
|
||||
Row,
|
||||
Col,
|
||||
Typography,
|
||||
Spin,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import { Button, Form, Row, Col, Typography, Spin } from '@douyinfe/semi-ui';
|
||||
const { Text } = Typography;
|
||||
import {
|
||||
API,
|
||||
@@ -58,8 +51,14 @@ export default function SettingsPaymentGateway(props) {
|
||||
PayAddress: props.options.PayAddress || '',
|
||||
EpayId: props.options.EpayId || '',
|
||||
EpayKey: props.options.EpayKey || '',
|
||||
Price: props.options.Price !== undefined ? parseFloat(props.options.Price) : 7.3,
|
||||
MinTopUp: props.options.MinTopUp !== undefined ? parseFloat(props.options.MinTopUp) : 1,
|
||||
Price:
|
||||
props.options.Price !== undefined
|
||||
? parseFloat(props.options.Price)
|
||||
: 7.3,
|
||||
MinTopUp:
|
||||
props.options.MinTopUp !== undefined
|
||||
? parseFloat(props.options.MinTopUp)
|
||||
: 1,
|
||||
TopupGroupRatio: props.options.TopupGroupRatio || '',
|
||||
CustomCallbackAddress: props.options.CustomCallbackAddress || '',
|
||||
PayMethods: props.options.PayMethods || '',
|
||||
@@ -126,19 +125,19 @@ export default function SettingsPaymentGateway(props) {
|
||||
}
|
||||
|
||||
// 发送请求
|
||||
const requestQueue = options.map(opt =>
|
||||
const requestQueue = options.map((opt) =>
|
||||
API.put('/api/option/', {
|
||||
key: opt.key,
|
||||
value: opt.value,
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
const results = await Promise.all(requestQueue);
|
||||
|
||||
// 检查所有请求是否成功
|
||||
const errorResults = results.filter(res => !res.data.success);
|
||||
const errorResults = results.filter((res) => !res.data.success);
|
||||
if (errorResults.length > 0) {
|
||||
errorResults.forEach(res => {
|
||||
errorResults.forEach((res) => {
|
||||
showError(res.data.message);
|
||||
});
|
||||
} else {
|
||||
@@ -162,11 +161,11 @@ export default function SettingsPaymentGateway(props) {
|
||||
>
|
||||
<Form.Section text={t('支付设置')}>
|
||||
<Text>
|
||||
{t('(当前仅支持易支付接口,默认使用上方服务器地址作为回调地址!)')}
|
||||
{t(
|
||||
'(当前仅支持易支付接口,默认使用上方服务器地址作为回调地址!)',
|
||||
)}
|
||||
</Text>
|
||||
<Row
|
||||
gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
|
||||
>
|
||||
<Row gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}>
|
||||
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
|
||||
<Form.Input
|
||||
field='PayAddress'
|
||||
@@ -234,4 +233,4 @@ export default function SettingsPaymentGateway(props) {
|
||||
</Form>
|
||||
</Spin>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,8 +55,14 @@ export default function SettingsPaymentGateway(props) {
|
||||
StripeApiSecret: props.options.StripeApiSecret || '',
|
||||
StripeWebhookSecret: props.options.StripeWebhookSecret || '',
|
||||
StripePriceId: props.options.StripePriceId || '',
|
||||
StripeUnitPrice: props.options.StripeUnitPrice !== undefined ? parseFloat(props.options.StripeUnitPrice) : 8.0,
|
||||
StripeMinTopUp: props.options.StripeMinTopUp !== undefined ? parseFloat(props.options.StripeMinTopUp) : 1,
|
||||
StripeUnitPrice:
|
||||
props.options.StripeUnitPrice !== undefined
|
||||
? parseFloat(props.options.StripeUnitPrice)
|
||||
: 8.0,
|
||||
StripeMinTopUp:
|
||||
props.options.StripeMinTopUp !== undefined
|
||||
? parseFloat(props.options.StripeMinTopUp)
|
||||
: 1,
|
||||
};
|
||||
setInputs(currentInputs);
|
||||
setOriginInputs({ ...currentInputs });
|
||||
@@ -76,38 +82,53 @@ export default function SettingsPaymentGateway(props) {
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const options = []
|
||||
const options = [];
|
||||
|
||||
if (inputs.StripeApiSecret && inputs.StripeApiSecret !== '') {
|
||||
options.push({ key: 'StripeApiSecret', value: inputs.StripeApiSecret });
|
||||
}
|
||||
if (inputs.StripeWebhookSecret && inputs.StripeWebhookSecret !== '') {
|
||||
options.push({ key: 'StripeWebhookSecret', value: inputs.StripeWebhookSecret });
|
||||
options.push({
|
||||
key: 'StripeWebhookSecret',
|
||||
value: inputs.StripeWebhookSecret,
|
||||
});
|
||||
}
|
||||
if (inputs.StripePriceId !== '') {
|
||||
options.push({key: 'StripePriceId', value: inputs.StripePriceId,});
|
||||
options.push({ key: 'StripePriceId', value: inputs.StripePriceId });
|
||||
}
|
||||
if (inputs.StripeUnitPrice !== undefined && inputs.StripeUnitPrice !== null) {
|
||||
options.push({ key: 'StripeUnitPrice', value: inputs.StripeUnitPrice.toString() });
|
||||
if (
|
||||
inputs.StripeUnitPrice !== undefined &&
|
||||
inputs.StripeUnitPrice !== null
|
||||
) {
|
||||
options.push({
|
||||
key: 'StripeUnitPrice',
|
||||
value: inputs.StripeUnitPrice.toString(),
|
||||
});
|
||||
}
|
||||
if (inputs.StripeMinTopUp !== undefined && inputs.StripeMinTopUp !== null) {
|
||||
options.push({ key: 'StripeMinTopUp', value: inputs.StripeMinTopUp.toString() });
|
||||
if (
|
||||
inputs.StripeMinTopUp !== undefined &&
|
||||
inputs.StripeMinTopUp !== null
|
||||
) {
|
||||
options.push({
|
||||
key: 'StripeMinTopUp',
|
||||
value: inputs.StripeMinTopUp.toString(),
|
||||
});
|
||||
}
|
||||
|
||||
// 发送请求
|
||||
const requestQueue = options.map(opt =>
|
||||
const requestQueue = options.map((opt) =>
|
||||
API.put('/api/option/', {
|
||||
key: opt.key,
|
||||
value: opt.value,
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
const results = await Promise.all(requestQueue);
|
||||
|
||||
// 检查所有请求是否成功
|
||||
const errorResults = results.filter(res => !res.data.success);
|
||||
const errorResults = results.filter((res) => !res.data.success);
|
||||
if (errorResults.length > 0) {
|
||||
errorResults.forEach(res => {
|
||||
errorResults.forEach((res) => {
|
||||
showError(res.data.message);
|
||||
});
|
||||
} else {
|
||||
@@ -133,40 +154,39 @@ export default function SettingsPaymentGateway(props) {
|
||||
<Text>
|
||||
Stripe 密钥、Webhook 等设置请
|
||||
<a
|
||||
href='https://dashboard.stripe.com/developers'
|
||||
target='_blank'
|
||||
rel='noreferrer'
|
||||
href='https://dashboard.stripe.com/developers'
|
||||
target='_blank'
|
||||
rel='noreferrer'
|
||||
>
|
||||
点击此处
|
||||
</a>
|
||||
进行设置,最好先在
|
||||
<a
|
||||
href='https://dashboard.stripe.com/test/developers'
|
||||
target='_blank'
|
||||
rel='noreferrer'
|
||||
href='https://dashboard.stripe.com/test/developers'
|
||||
target='_blank'
|
||||
rel='noreferrer'
|
||||
>
|
||||
测试环境
|
||||
</a>
|
||||
进行测试。
|
||||
|
||||
<br />
|
||||
</Text>
|
||||
<Banner
|
||||
type='info'
|
||||
description={`Webhook 填:${props.options.ServerAddress ? removeTrailingSlash(props.options.ServerAddress) : t('网站地址')}/api/stripe/webhook`}
|
||||
type='info'
|
||||
description={`Webhook 填:${props.options.ServerAddress ? removeTrailingSlash(props.options.ServerAddress) : t('网站地址')}/api/stripe/webhook`}
|
||||
/>
|
||||
<Banner
|
||||
type='warning'
|
||||
description={`需要包含事件:checkout.session.completed 和 checkout.session.expired`}
|
||||
type='warning'
|
||||
description={`需要包含事件:checkout.session.completed 和 checkout.session.expired`}
|
||||
/>
|
||||
<Row
|
||||
gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
|
||||
>
|
||||
<Row gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}>
|
||||
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
|
||||
<Form.Input
|
||||
field='StripeApiSecret'
|
||||
label={t('API 密钥')}
|
||||
placeholder={t('sk_xxx 或 rk_xxx 的 Stripe 密钥,敏感信息不显示')}
|
||||
placeholder={t(
|
||||
'sk_xxx 或 rk_xxx 的 Stripe 密钥,敏感信息不显示',
|
||||
)}
|
||||
type='password'
|
||||
/>
|
||||
</Col>
|
||||
@@ -211,4 +231,4 @@ export default function SettingsPaymentGateway(props) {
|
||||
</Form>
|
||||
</Spin>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,11 +68,11 @@ export default function RequestRateLimit(props) {
|
||||
return showError(t('部分保存失败,请重试'));
|
||||
}
|
||||
|
||||
for (let i = 0; i < res.length; i++) {
|
||||
if (!res[i].data.success) {
|
||||
return showError(res[i].data.message);
|
||||
for (let i = 0; i < res.length; i++) {
|
||||
if (!res[i].data.success) {
|
||||
return showError(res[i].data.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
showSuccess(t('保存成功'));
|
||||
props.refresh();
|
||||
@@ -185,23 +185,39 @@ export default function RequestRateLimit(props) {
|
||||
'{\n "default": [200, 100],\n "vip": [0, 1000]\n}',
|
||||
)}
|
||||
field={'ModelRequestRateLimitGroup'}
|
||||
autosize={{ minRows: 5, maxRows: 15 }}
|
||||
trigger='blur'
|
||||
stopValidateWithError
|
||||
rules={[
|
||||
{
|
||||
validator: (rule, value) => verifyJSON(value),
|
||||
message: t('不是合法的 JSON 字符串'),
|
||||
},
|
||||
]}
|
||||
autosize={{ minRows: 5, maxRows: 15 }}
|
||||
trigger='blur'
|
||||
stopValidateWithError
|
||||
rules={[
|
||||
{
|
||||
validator: (rule, value) => verifyJSON(value),
|
||||
message: t('不是合法的 JSON 字符串'),
|
||||
},
|
||||
]}
|
||||
extraText={
|
||||
<div>
|
||||
<p>{t('说明:')}</p>
|
||||
<ul>
|
||||
<li>{t('使用 JSON 对象格式,格式为:{"组名": [最多请求次数, 最多请求完成次数]}')}</li>
|
||||
<li>{t('示例:{"default": [200, 100], "vip": [0, 1000]}。')}</li>
|
||||
<li>{t('[最多请求次数]必须大于等于0,[最多请求完成次数]必须大于等于1。')}</li>
|
||||
<li>{t('[最多请求次数]和[最多请求完成次数]的最大值为2147483647。')}</li>
|
||||
<li>
|
||||
{t(
|
||||
'使用 JSON 对象格式,格式为:{"组名": [最多请求次数, 最多请求完成次数]}',
|
||||
)}
|
||||
</li>
|
||||
<li>
|
||||
{t(
|
||||
'示例:{"default": [200, 100], "vip": [0, 1000]}。',
|
||||
)}
|
||||
</li>
|
||||
<li>
|
||||
{t(
|
||||
'[最多请求次数]必须大于等于0,[最多请求完成次数]必须大于等于1。',
|
||||
)}
|
||||
</li>
|
||||
<li>
|
||||
{t(
|
||||
'[最多请求次数]和[最多请求完成次数]的最大值为2147483647。',
|
||||
)}
|
||||
</li>
|
||||
<li>{t('分组速率配置优先级高于全局速率限制。')}</li>
|
||||
<li>{t('限制周期统一使用上方配置的“限制周期”值。')}</li>
|
||||
</ul>
|
||||
|
||||
@@ -133,9 +133,7 @@ export default function GroupRatioSettings(props) {
|
||||
message: t('不是合法的 JSON 字符串'),
|
||||
},
|
||||
]}
|
||||
onChange={(value) =>
|
||||
setInputs({ ...inputs, GroupRatio: value })
|
||||
}
|
||||
onChange={(value) => setInputs({ ...inputs, GroupRatio: value })}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
@@ -213,7 +211,7 @@ export default function GroupRatioSettings(props) {
|
||||
}
|
||||
|
||||
// Check if every element is a string
|
||||
return parsed.every(item => typeof item === 'string');
|
||||
return parsed.every((item) => typeof item === 'string');
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
@@ -221,9 +219,7 @@ export default function GroupRatioSettings(props) {
|
||||
message: t('必须是有效的 JSON 字符串数组,例如:["g1","g2"]'),
|
||||
},
|
||||
]}
|
||||
onChange={(value) =>
|
||||
setInputs({ ...inputs, AutoGroups: value })
|
||||
}
|
||||
onChange={(value) => setInputs({ ...inputs, AutoGroups: value })}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
@@ -155,9 +155,7 @@ export default function ModelRatioSettings(props) {
|
||||
message: '不是合法的 JSON 字符串',
|
||||
},
|
||||
]}
|
||||
onChange={(value) =>
|
||||
setInputs({ ...inputs, ModelPrice: value })
|
||||
}
|
||||
onChange={(value) => setInputs({ ...inputs, ModelPrice: value })}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
@@ -176,9 +174,7 @@ export default function ModelRatioSettings(props) {
|
||||
message: '不是合法的 JSON 字符串',
|
||||
},
|
||||
]}
|
||||
onChange={(value) =>
|
||||
setInputs({ ...inputs, ModelRatio: value })
|
||||
}
|
||||
onChange={(value) => setInputs({ ...inputs, ModelRatio: value })}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
@@ -197,9 +193,7 @@ export default function ModelRatioSettings(props) {
|
||||
message: '不是合法的 JSON 字符串',
|
||||
},
|
||||
]}
|
||||
onChange={(value) =>
|
||||
setInputs({ ...inputs, CacheRatio: value })
|
||||
}
|
||||
onChange={(value) => setInputs({ ...inputs, CacheRatio: value })}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
@@ -28,7 +28,7 @@ import {
|
||||
RadioGroup,
|
||||
Radio,
|
||||
Checkbox,
|
||||
Tag
|
||||
Tag,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import {
|
||||
IconDelete,
|
||||
@@ -72,7 +72,8 @@ export default function ModelSettingsVisualEditor(props) {
|
||||
const modelData = Array.from(modelNames).map((name) => {
|
||||
const price = modelPrice[name] === undefined ? '' : modelPrice[name];
|
||||
const ratio = modelRatio[name] === undefined ? '' : modelRatio[name];
|
||||
const comp = completionRatio[name] === undefined ? '' : completionRatio[name];
|
||||
const comp =
|
||||
completionRatio[name] === undefined ? '' : completionRatio[name];
|
||||
|
||||
return {
|
||||
name,
|
||||
@@ -263,7 +264,8 @@ export default function ModelSettingsVisualEditor(props) {
|
||||
if (model.name !== name) return model;
|
||||
const updated = { ...model, [field]: value };
|
||||
updated.hasConflict =
|
||||
updated.price !== '' && (updated.ratio !== '' || updated.completionRatio !== '');
|
||||
updated.price !== '' &&
|
||||
(updated.ratio !== '' || updated.completionRatio !== '');
|
||||
return updated;
|
||||
}),
|
||||
);
|
||||
@@ -349,7 +351,8 @@ export default function ModelSettingsVisualEditor(props) {
|
||||
completionRatio: values.completionRatio || '',
|
||||
};
|
||||
updated.hasConflict =
|
||||
updated.price !== '' && (updated.ratio !== '' || updated.completionRatio !== '');
|
||||
updated.price !== '' &&
|
||||
(updated.ratio !== '' || updated.completionRatio !== '');
|
||||
return updated;
|
||||
}),
|
||||
);
|
||||
@@ -371,7 +374,8 @@ export default function ModelSettingsVisualEditor(props) {
|
||||
completionRatio: values.completionRatio || '',
|
||||
};
|
||||
newModel.hasConflict =
|
||||
newModel.price !== '' && (newModel.ratio !== '' || newModel.completionRatio !== '');
|
||||
newModel.price !== '' &&
|
||||
(newModel.ratio !== '' || newModel.completionRatio !== '');
|
||||
return [newModel, ...prev];
|
||||
});
|
||||
setVisible(false);
|
||||
|
||||
@@ -37,13 +37,19 @@ import {
|
||||
AlertTriangle,
|
||||
CheckCircle,
|
||||
} from 'lucide-react';
|
||||
import { API, showError, showSuccess, showWarning, stringToColor } from '../../../helpers';
|
||||
import {
|
||||
API,
|
||||
showError,
|
||||
showSuccess,
|
||||
showWarning,
|
||||
stringToColor,
|
||||
} from '../../../helpers';
|
||||
import { useIsMobile } from '../../../hooks/common/useIsMobile';
|
||||
import { DEFAULT_ENDPOINT } from '../../../constants';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
IllustrationNoResult,
|
||||
IllustrationNoResultDark
|
||||
IllustrationNoResultDark,
|
||||
} from '@douyinfe/semi-illustrations';
|
||||
import ChannelSelectorModal from '../../../components/settings/ChannelSelectorModal';
|
||||
|
||||
@@ -72,7 +78,12 @@ function ConflictConfirmModal({ t, visible, items, onOk, onCancel }) {
|
||||
onOk={onOk}
|
||||
size={isMobile ? 'full-width' : 'large'}
|
||||
>
|
||||
<Table columns={columns} dataSource={items} pagination={false} size="small" />
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={items}
|
||||
pagination={false}
|
||||
size='small'
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -126,7 +137,7 @@ export default function UpstreamRatioSync(props) {
|
||||
if (res.data.success) {
|
||||
const channels = res.data.data || [];
|
||||
|
||||
const transferData = channels.map(channel => ({
|
||||
const transferData = channels.map((channel) => ({
|
||||
key: channel.id,
|
||||
label: channel.name,
|
||||
value: channel.id,
|
||||
@@ -137,9 +148,9 @@ export default function UpstreamRatioSync(props) {
|
||||
setAllChannels(transferData);
|
||||
|
||||
// 合并已有 endpoints,避免每次打开弹窗都重置
|
||||
setChannelEndpoints(prev => {
|
||||
setChannelEndpoints((prev) => {
|
||||
const merged = { ...prev };
|
||||
transferData.forEach(channel => {
|
||||
transferData.forEach((channel) => {
|
||||
if (!merged[channel.key]) {
|
||||
merged[channel.key] = DEFAULT_ENDPOINT;
|
||||
}
|
||||
@@ -158,8 +169,8 @@ export default function UpstreamRatioSync(props) {
|
||||
|
||||
const confirmChannelSelection = () => {
|
||||
const selected = allChannels
|
||||
.filter(ch => selectedChannelIds.includes(ch.value))
|
||||
.map(ch => ch._originalData);
|
||||
.filter((ch) => selectedChannelIds.includes(ch.value))
|
||||
.map((ch) => ch._originalData);
|
||||
|
||||
if (selected.length === 0) {
|
||||
showWarning(t('请至少选择一个渠道'));
|
||||
@@ -173,7 +184,7 @@ export default function UpstreamRatioSync(props) {
|
||||
const fetchRatiosFromChannels = async (channelList) => {
|
||||
setSyncLoading(true);
|
||||
|
||||
const upstreams = channelList.map(ch => ({
|
||||
const upstreams = channelList.map((ch) => ({
|
||||
id: ch.id,
|
||||
name: ch.name,
|
||||
base_url: ch.base_url,
|
||||
@@ -196,9 +207,12 @@ export default function UpstreamRatioSync(props) {
|
||||
|
||||
const { differences = {}, test_results = [] } = res.data.data;
|
||||
|
||||
const errorResults = test_results.filter(r => r.status === 'error');
|
||||
const errorResults = test_results.filter((r) => r.status === 'error');
|
||||
if (errorResults.length > 0) {
|
||||
showWarning(t('部分渠道测试失败:') + errorResults.map(r => `${r.name}: ${r.error}`).join(', '));
|
||||
showWarning(
|
||||
t('部分渠道测试失败:') +
|
||||
errorResults.map((r) => `${r.name}: ${r.error}`).join(', '),
|
||||
);
|
||||
}
|
||||
|
||||
setDifferences(differences);
|
||||
@@ -219,26 +233,29 @@ export default function UpstreamRatioSync(props) {
|
||||
return ratioType === 'model_price' ? 'price' : 'ratio';
|
||||
}
|
||||
|
||||
const selectValue = useCallback((model, ratioType, value) => {
|
||||
const category = getBillingCategory(ratioType);
|
||||
const selectValue = useCallback(
|
||||
(model, ratioType, value) => {
|
||||
const category = getBillingCategory(ratioType);
|
||||
|
||||
setResolutions(prev => {
|
||||
const newModelRes = { ...(prev[model] || {}) };
|
||||
setResolutions((prev) => {
|
||||
const newModelRes = { ...(prev[model] || {}) };
|
||||
|
||||
Object.keys(newModelRes).forEach((rt) => {
|
||||
if (getBillingCategory(rt) !== category) {
|
||||
delete newModelRes[rt];
|
||||
}
|
||||
Object.keys(newModelRes).forEach((rt) => {
|
||||
if (getBillingCategory(rt) !== category) {
|
||||
delete newModelRes[rt];
|
||||
}
|
||||
});
|
||||
|
||||
newModelRes[ratioType] = value;
|
||||
|
||||
return {
|
||||
...prev,
|
||||
[model]: newModelRes,
|
||||
};
|
||||
});
|
||||
|
||||
newModelRes[ratioType] = value;
|
||||
|
||||
return {
|
||||
...prev,
|
||||
[model]: newModelRes,
|
||||
};
|
||||
});
|
||||
}, [setResolutions]);
|
||||
},
|
||||
[setResolutions],
|
||||
);
|
||||
|
||||
const applySync = async () => {
|
||||
const currentRatios = {
|
||||
@@ -252,9 +269,12 @@ export default function UpstreamRatioSync(props) {
|
||||
|
||||
const getLocalBillingCategory = (model) => {
|
||||
if (currentRatios.ModelPrice[model] !== undefined) return 'price';
|
||||
if (currentRatios.ModelRatio[model] !== undefined ||
|
||||
if (
|
||||
currentRatios.ModelRatio[model] !== undefined ||
|
||||
currentRatios.CompletionRatio[model] !== undefined ||
|
||||
currentRatios.CacheRatio[model] !== undefined) return 'ratio';
|
||||
currentRatios.CacheRatio[model] !== undefined
|
||||
)
|
||||
return 'ratio';
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -272,9 +292,10 @@ export default function UpstreamRatioSync(props) {
|
||||
const newCat = 'model_price' in ratios ? 'price' : 'ratio';
|
||||
|
||||
if (localCat && localCat !== newCat) {
|
||||
const currentDesc = localCat === 'price'
|
||||
? `${t('固定价格')} : ${currentRatios.ModelPrice[model]}`
|
||||
: `${t('模型倍率')} : ${currentRatios.ModelRatio[model] ?? '-'}\n${t('补全倍率')} : ${currentRatios.CompletionRatio[model] ?? '-'}`;
|
||||
const currentDesc =
|
||||
localCat === 'price'
|
||||
? `${t('固定价格')} : ${currentRatios.ModelPrice[model]}`
|
||||
: `${t('模型倍率')} : ${currentRatios.ModelRatio[model] ?? '-'}\n${t('补全倍率')} : ${currentRatios.CompletionRatio[model] ?? '-'}`;
|
||||
|
||||
let newDesc = '';
|
||||
if (newCat === 'price') {
|
||||
@@ -308,80 +329,83 @@ export default function UpstreamRatioSync(props) {
|
||||
await performSync(currentRatios);
|
||||
};
|
||||
|
||||
const performSync = useCallback(async (currentRatios) => {
|
||||
const finalRatios = {
|
||||
ModelRatio: { ...currentRatios.ModelRatio },
|
||||
CompletionRatio: { ...currentRatios.CompletionRatio },
|
||||
CacheRatio: { ...currentRatios.CacheRatio },
|
||||
ModelPrice: { ...currentRatios.ModelPrice },
|
||||
};
|
||||
const performSync = useCallback(
|
||||
async (currentRatios) => {
|
||||
const finalRatios = {
|
||||
ModelRatio: { ...currentRatios.ModelRatio },
|
||||
CompletionRatio: { ...currentRatios.CompletionRatio },
|
||||
CacheRatio: { ...currentRatios.CacheRatio },
|
||||
ModelPrice: { ...currentRatios.ModelPrice },
|
||||
};
|
||||
|
||||
Object.entries(resolutions).forEach(([model, ratios]) => {
|
||||
const selectedTypes = Object.keys(ratios);
|
||||
const hasPrice = selectedTypes.includes('model_price');
|
||||
const hasRatio = selectedTypes.some(rt => rt !== 'model_price');
|
||||
Object.entries(resolutions).forEach(([model, ratios]) => {
|
||||
const selectedTypes = Object.keys(ratios);
|
||||
const hasPrice = selectedTypes.includes('model_price');
|
||||
const hasRatio = selectedTypes.some((rt) => rt !== 'model_price');
|
||||
|
||||
if (hasPrice) {
|
||||
delete finalRatios.ModelRatio[model];
|
||||
delete finalRatios.CompletionRatio[model];
|
||||
delete finalRatios.CacheRatio[model];
|
||||
}
|
||||
if (hasRatio) {
|
||||
delete finalRatios.ModelPrice[model];
|
||||
}
|
||||
if (hasPrice) {
|
||||
delete finalRatios.ModelRatio[model];
|
||||
delete finalRatios.CompletionRatio[model];
|
||||
delete finalRatios.CacheRatio[model];
|
||||
}
|
||||
if (hasRatio) {
|
||||
delete finalRatios.ModelPrice[model];
|
||||
}
|
||||
|
||||
Object.entries(ratios).forEach(([ratioType, value]) => {
|
||||
const optionKey = ratioType
|
||||
.split('_')
|
||||
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join('');
|
||||
finalRatios[optionKey][model] = parseFloat(value);
|
||||
Object.entries(ratios).forEach(([ratioType, value]) => {
|
||||
const optionKey = ratioType
|
||||
.split('_')
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join('');
|
||||
finalRatios[optionKey][model] = parseFloat(value);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const updates = Object.entries(finalRatios).map(([key, value]) =>
|
||||
API.put('/api/option/', {
|
||||
key,
|
||||
value: JSON.stringify(value, null, 2),
|
||||
})
|
||||
);
|
||||
setLoading(true);
|
||||
try {
|
||||
const updates = Object.entries(finalRatios).map(([key, value]) =>
|
||||
API.put('/api/option/', {
|
||||
key,
|
||||
value: JSON.stringify(value, null, 2),
|
||||
}),
|
||||
);
|
||||
|
||||
const results = await Promise.all(updates);
|
||||
const results = await Promise.all(updates);
|
||||
|
||||
if (results.every(res => res.data.success)) {
|
||||
showSuccess(t('同步成功'));
|
||||
props.refresh();
|
||||
if (results.every((res) => res.data.success)) {
|
||||
showSuccess(t('同步成功'));
|
||||
props.refresh();
|
||||
|
||||
setDifferences(prevDifferences => {
|
||||
const newDifferences = { ...prevDifferences };
|
||||
setDifferences((prevDifferences) => {
|
||||
const newDifferences = { ...prevDifferences };
|
||||
|
||||
Object.entries(resolutions).forEach(([model, ratios]) => {
|
||||
Object.keys(ratios).forEach(ratioType => {
|
||||
if (newDifferences[model] && newDifferences[model][ratioType]) {
|
||||
delete newDifferences[model][ratioType];
|
||||
Object.entries(resolutions).forEach(([model, ratios]) => {
|
||||
Object.keys(ratios).forEach((ratioType) => {
|
||||
if (newDifferences[model] && newDifferences[model][ratioType]) {
|
||||
delete newDifferences[model][ratioType];
|
||||
|
||||
if (Object.keys(newDifferences[model]).length === 0) {
|
||||
delete newDifferences[model];
|
||||
if (Object.keys(newDifferences[model]).length === 0) {
|
||||
delete newDifferences[model];
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return newDifferences;
|
||||
});
|
||||
|
||||
return newDifferences;
|
||||
});
|
||||
|
||||
setResolutions({});
|
||||
} else {
|
||||
showError(t('部分保存失败'));
|
||||
setResolutions({});
|
||||
} else {
|
||||
showError(t('部分保存失败'));
|
||||
}
|
||||
} catch (error) {
|
||||
showError(t('保存失败'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
} catch (error) {
|
||||
showError(t('保存失败'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [resolutions, props.options, props.refresh]);
|
||||
},
|
||||
[resolutions, props.options, props.refresh],
|
||||
);
|
||||
|
||||
const getCurrentPageData = (dataSource) => {
|
||||
const startIndex = (currentPage - 1) * pageSize;
|
||||
@@ -390,12 +414,12 @@ export default function UpstreamRatioSync(props) {
|
||||
};
|
||||
|
||||
const renderHeader = () => (
|
||||
<div className="flex flex-col w-full">
|
||||
<div className="flex flex-col md:flex-row justify-between items-center gap-4 w-full">
|
||||
<div className="flex flex-col md:flex-row gap-2 w-full md:w-auto order-2 md:order-1">
|
||||
<div className='flex flex-col w-full'>
|
||||
<div className='flex flex-col md:flex-row justify-between items-center gap-4 w-full'>
|
||||
<div className='flex flex-col md:flex-row gap-2 w-full md:w-auto order-2 md:order-1'>
|
||||
<Button
|
||||
icon={<RefreshCcw size={14} />}
|
||||
className="w-full md:w-auto mt-2"
|
||||
className='w-full md:w-auto mt-2'
|
||||
onClick={() => {
|
||||
setModalVisible(true);
|
||||
if (allChannels.length === 0) {
|
||||
@@ -415,20 +439,20 @@ export default function UpstreamRatioSync(props) {
|
||||
type='secondary'
|
||||
onClick={applySync}
|
||||
disabled={!hasSelections}
|
||||
className="w-full md:w-auto mt-2"
|
||||
className='w-full md:w-auto mt-2'
|
||||
>
|
||||
{t('应用同步')}
|
||||
</Button>
|
||||
);
|
||||
})()}
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-2 w-full md:w-auto mt-2">
|
||||
<div className='flex flex-col sm:flex-row gap-2 w-full md:w-auto mt-2'>
|
||||
<Input
|
||||
prefix={<IconSearch size={14} />}
|
||||
placeholder={t('搜索模型名称')}
|
||||
value={searchKeyword}
|
||||
onChange={setSearchKeyword}
|
||||
className="w-full sm:w-64"
|
||||
className='w-full sm:w-64'
|
||||
showClear
|
||||
/>
|
||||
|
||||
@@ -436,14 +460,16 @@ export default function UpstreamRatioSync(props) {
|
||||
placeholder={t('按倍率类型筛选')}
|
||||
value={ratioTypeFilter}
|
||||
onChange={setRatioTypeFilter}
|
||||
className="w-full sm:w-48"
|
||||
className='w-full sm:w-48'
|
||||
showClear
|
||||
onClear={() => setRatioTypeFilter('')}
|
||||
>
|
||||
<Select.Option value="model_ratio">{t('模型倍率')}</Select.Option>
|
||||
<Select.Option value="completion_ratio">{t('补全倍率')}</Select.Option>
|
||||
<Select.Option value="cache_ratio">{t('缓存倍率')}</Select.Option>
|
||||
<Select.Option value="model_price">{t('固定价格')}</Select.Option>
|
||||
<Select.Option value='model_ratio'>{t('模型倍率')}</Select.Option>
|
||||
<Select.Option value='completion_ratio'>
|
||||
{t('补全倍率')}
|
||||
</Select.Option>
|
||||
<Select.Option value='cache_ratio'>{t('缓存倍率')}</Select.Option>
|
||||
<Select.Option value='model_price'>{t('固定价格')}</Select.Option>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
@@ -457,7 +483,11 @@ export default function UpstreamRatioSync(props) {
|
||||
|
||||
Object.entries(differences).forEach(([model, ratioTypes]) => {
|
||||
const hasPrice = 'model_price' in ratioTypes;
|
||||
const hasOtherRatio = ['model_ratio', 'completion_ratio', 'cache_ratio'].some(rt => rt in ratioTypes);
|
||||
const hasOtherRatio = [
|
||||
'model_ratio',
|
||||
'completion_ratio',
|
||||
'cache_ratio',
|
||||
].some((rt) => rt in ratioTypes);
|
||||
const billingConflict = hasPrice && hasOtherRatio;
|
||||
|
||||
Object.entries(ratioTypes).forEach(([ratioType, diff]) => {
|
||||
@@ -481,12 +511,13 @@ export default function UpstreamRatioSync(props) {
|
||||
return dataSource;
|
||||
}
|
||||
|
||||
return dataSource.filter(item => {
|
||||
const matchesKeyword = !searchKeyword.trim() ||
|
||||
return dataSource.filter((item) => {
|
||||
const matchesKeyword =
|
||||
!searchKeyword.trim() ||
|
||||
item.model.toLowerCase().includes(searchKeyword.toLowerCase().trim());
|
||||
|
||||
const matchesRatioType = !ratioTypeFilter ||
|
||||
item.ratioType === ratioTypeFilter;
|
||||
const matchesRatioType =
|
||||
!ratioTypeFilter || item.ratioType === ratioTypeFilter;
|
||||
|
||||
return matchesKeyword && matchesRatioType;
|
||||
});
|
||||
@@ -504,13 +535,17 @@ export default function UpstreamRatioSync(props) {
|
||||
return (
|
||||
<Empty
|
||||
image={<IllustrationNoResult style={{ width: 150, height: 150 }} />}
|
||||
darkModeImage={<IllustrationNoResultDark style={{ width: 150, height: 150 }} />}
|
||||
darkModeImage={
|
||||
<IllustrationNoResultDark style={{ width: 150, height: 150 }} />
|
||||
}
|
||||
description={
|
||||
searchKeyword.trim()
|
||||
? t('未找到匹配的模型')
|
||||
: (Object.keys(differences).length === 0 ?
|
||||
(hasSynced ? t('暂无差异化倍率显示') : t('请先选择同步渠道'))
|
||||
: t('请先选择同步渠道'))
|
||||
: Object.keys(differences).length === 0
|
||||
? hasSynced
|
||||
? t('暂无差异化倍率显示')
|
||||
: t('请先选择同步渠道')
|
||||
: t('请先选择同步渠道')
|
||||
}
|
||||
style={{ padding: 30 }}
|
||||
/>
|
||||
@@ -533,13 +568,22 @@ export default function UpstreamRatioSync(props) {
|
||||
cache_ratio: t('缓存倍率'),
|
||||
model_price: t('固定价格'),
|
||||
};
|
||||
const baseTag = <Tag color={stringToColor(text)} shape="circle">{typeMap[text] || text}</Tag>;
|
||||
const baseTag = (
|
||||
<Tag color={stringToColor(text)} shape='circle'>
|
||||
{typeMap[text] || text}
|
||||
</Tag>
|
||||
);
|
||||
if (record?.billingConflict) {
|
||||
return (
|
||||
<div className="flex items-center gap-1">
|
||||
<div className='flex items-center gap-1'>
|
||||
{baseTag}
|
||||
<Tooltip position="top" content={t('该模型存在固定价格与倍率计费方式冲突,请确认选择')}>
|
||||
<AlertTriangle size={14} className="text-yellow-500" />
|
||||
<Tooltip
|
||||
position='top'
|
||||
content={t(
|
||||
'该模型存在固定价格与倍率计费方式冲突,请确认选择',
|
||||
)}
|
||||
>
|
||||
<AlertTriangle size={14} className='text-yellow-500' />
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
@@ -551,12 +595,19 @@ export default function UpstreamRatioSync(props) {
|
||||
title: t('置信度'),
|
||||
dataIndex: 'confidence',
|
||||
render: (_, record) => {
|
||||
const allConfident = Object.values(record.confidence || {}).every(v => v !== false);
|
||||
const allConfident = Object.values(record.confidence || {}).every(
|
||||
(v) => v !== false,
|
||||
);
|
||||
|
||||
if (allConfident) {
|
||||
return (
|
||||
<Tooltip content={t('所有上游数据均可信')}>
|
||||
<Tag color="green" shape="circle" type="light" prefixIcon={<CheckCircle size={14} />}>
|
||||
<Tag
|
||||
color='green'
|
||||
shape='circle'
|
||||
type='light'
|
||||
prefixIcon={<CheckCircle size={14} />}
|
||||
>
|
||||
{t('可信')}
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
@@ -568,8 +619,15 @@ export default function UpstreamRatioSync(props) {
|
||||
.join(', ');
|
||||
|
||||
return (
|
||||
<Tooltip content={t('以下上游数据可能不可信:') + untrustedSources}>
|
||||
<Tag color="yellow" shape="circle" type="light" prefixIcon={<AlertTriangle size={14} />}>
|
||||
<Tooltip
|
||||
content={t('以下上游数据可能不可信:') + untrustedSources}
|
||||
>
|
||||
<Tag
|
||||
color='yellow'
|
||||
shape='circle'
|
||||
type='light'
|
||||
prefixIcon={<AlertTriangle size={14} />}
|
||||
>
|
||||
{t('谨慎')}
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
@@ -581,7 +639,10 @@ export default function UpstreamRatioSync(props) {
|
||||
title: t('当前值'),
|
||||
dataIndex: 'current',
|
||||
render: (text) => (
|
||||
<Tag color={text !== null && text !== undefined ? 'blue' : 'default'} shape="circle">
|
||||
<Tag
|
||||
color={text !== null && text !== undefined ? 'blue' : 'default'}
|
||||
shape='circle'
|
||||
>
|
||||
{text !== null && text !== undefined ? text : t('未设置')}
|
||||
</Tag>
|
||||
),
|
||||
@@ -593,9 +654,14 @@ export default function UpstreamRatioSync(props) {
|
||||
|
||||
filteredDataSource.forEach((row) => {
|
||||
const upstreamVal = row.upstreams?.[upName];
|
||||
if (upstreamVal !== null && upstreamVal !== undefined && upstreamVal !== 'same') {
|
||||
if (
|
||||
upstreamVal !== null &&
|
||||
upstreamVal !== undefined &&
|
||||
upstreamVal !== 'same'
|
||||
) {
|
||||
selectableCount++;
|
||||
const isSelected = resolutions[row.model]?.[row.ratioType] === upstreamVal;
|
||||
const isSelected =
|
||||
resolutions[row.model]?.[row.ratioType] === upstreamVal;
|
||||
if (isSelected) {
|
||||
selectedCount++;
|
||||
}
|
||||
@@ -605,9 +671,11 @@ export default function UpstreamRatioSync(props) {
|
||||
return {
|
||||
selectableCount,
|
||||
selectedCount,
|
||||
allSelected: selectableCount > 0 && selectedCount === selectableCount,
|
||||
partiallySelected: selectedCount > 0 && selectedCount < selectableCount,
|
||||
hasSelectableItems: selectableCount > 0
|
||||
allSelected:
|
||||
selectableCount > 0 && selectedCount === selectableCount,
|
||||
partiallySelected:
|
||||
selectedCount > 0 && selectedCount < selectableCount,
|
||||
hasSelectableItems: selectableCount > 0,
|
||||
};
|
||||
})();
|
||||
|
||||
@@ -615,7 +683,11 @@ export default function UpstreamRatioSync(props) {
|
||||
if (checked) {
|
||||
filteredDataSource.forEach((row) => {
|
||||
const upstreamVal = row.upstreams?.[upName];
|
||||
if (upstreamVal !== null && upstreamVal !== undefined && upstreamVal !== 'same') {
|
||||
if (
|
||||
upstreamVal !== null &&
|
||||
upstreamVal !== undefined &&
|
||||
upstreamVal !== 'same'
|
||||
) {
|
||||
selectValue(row.model, row.ratioType, upstreamVal);
|
||||
}
|
||||
});
|
||||
@@ -653,17 +725,26 @@ export default function UpstreamRatioSync(props) {
|
||||
const isConfident = record.confidence?.[upName] !== false;
|
||||
|
||||
if (upstreamVal === null || upstreamVal === undefined) {
|
||||
return <Tag color="default" shape="circle">{t('未设置')}</Tag>;
|
||||
return (
|
||||
<Tag color='default' shape='circle'>
|
||||
{t('未设置')}
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
|
||||
if (upstreamVal === 'same') {
|
||||
return <Tag color="blue" shape="circle">{t('与本地相同')}</Tag>;
|
||||
return (
|
||||
<Tag color='blue' shape='circle'>
|
||||
{t('与本地相同')}
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
|
||||
const isSelected = resolutions[record.model]?.[record.ratioType] === upstreamVal;
|
||||
const isSelected =
|
||||
resolutions[record.model]?.[record.ratioType] === upstreamVal;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className='flex items-center gap-2'>
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
onChange={(e) => {
|
||||
@@ -687,8 +768,11 @@ export default function UpstreamRatioSync(props) {
|
||||
{upstreamVal}
|
||||
</Checkbox>
|
||||
{!isConfident && (
|
||||
<Tooltip position='left' content={t('该数据可能不可信,请谨慎使用')}>
|
||||
<AlertTriangle size={16} className="text-yellow-500" />
|
||||
<Tooltip
|
||||
position='left'
|
||||
content={t('该数据可能不可信,请谨慎使用')}
|
||||
>
|
||||
<AlertTriangle size={16} className='text-yellow-500' />
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
@@ -716,7 +800,7 @@ export default function UpstreamRatioSync(props) {
|
||||
onShowSizeChange: (current, size) => {
|
||||
setCurrentPage(1);
|
||||
setPageSize(size);
|
||||
}
|
||||
},
|
||||
}}
|
||||
scroll={{ x: 'max-content' }}
|
||||
size='middle'
|
||||
@@ -726,7 +810,7 @@ export default function UpstreamRatioSync(props) {
|
||||
};
|
||||
|
||||
const updateChannelEndpoint = useCallback((channelId, endpoint) => {
|
||||
setChannelEndpoints(prev => ({ ...prev, [channelId]: endpoint }));
|
||||
setChannelEndpoints((prev) => ({ ...prev, [channelId]: endpoint }));
|
||||
}, []);
|
||||
|
||||
const handleModalClose = () => {
|
||||
@@ -773,4 +857,4 @@ export default function UpstreamRatioSync(props) {
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ import {
|
||||
LayoutDashboard,
|
||||
MessageSquare,
|
||||
Palette,
|
||||
CreditCard
|
||||
CreditCard,
|
||||
} from 'lucide-react';
|
||||
|
||||
import SystemSetting from '../../components/settings/SystemSetting';
|
||||
@@ -169,7 +169,7 @@ const Setting = () => {
|
||||
}
|
||||
}, [location.search]);
|
||||
return (
|
||||
<div className="mt-[60px] px-2">
|
||||
<div className='mt-[60px] px-2'>
|
||||
<Layout>
|
||||
<Layout.Content>
|
||||
<Tabs
|
||||
|
||||
Reference in New Issue
Block a user