🎨 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:
t0ng7u
2025-08-30 21:15:10 +08:00
parent 41cf516ec5
commit 0d57b1acd4
274 changed files with 11025 additions and 7659 deletions

View File

@@ -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,

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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,

View File

@@ -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']}
/>

View File

@@ -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,

View File

@@ -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>
);
}
}

View File

@@ -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>
);
}
}

View File

@@ -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>
);
}
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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);

View File

@@ -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) {
/>
</>
);
}
}

View File

@@ -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