style(web): format code

This commit is contained in:
QuentinHsu
2025-04-04 12:00:38 +08:00
parent 424424c160
commit 6b79b89dc0
74 changed files with 6413 additions and 3548 deletions

View File

@@ -1 +1 @@
module.exports = require("@so1ve/prettier-config"); module.exports = require('@so1ve/prettier-config');

2633
web/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -21,9 +21,9 @@ import Chat2Link from './pages/Chat2Link';
import { Layout } from '@douyinfe/semi-ui'; import { Layout } from '@douyinfe/semi-ui';
import Midjourney from './pages/Midjourney'; import Midjourney from './pages/Midjourney';
import Pricing from './pages/Pricing/index.js'; import Pricing from './pages/Pricing/index.js';
import Task from "./pages/Task/index.js"; import Task from './pages/Task/index.js';
import Playground from './pages/Playground/Playground.js'; import Playground from './pages/Playground/Playground.js';
import OAuth2Callback from "./components/OAuth2Callback.js"; import OAuth2Callback from './components/OAuth2Callback.js';
import PersonalSetting from './components/PersonalSetting.js'; import PersonalSetting from './components/PersonalSetting.js';
import Setup from './pages/Setup/index.js'; import Setup from './pages/Setup/index.js';
@@ -166,18 +166,18 @@ function App() {
} }
/> />
<Route <Route
path='/oauth/oidc' path='/oauth/oidc'
element={ element={
<Suspense fallback={<Loading></Loading>}> <Suspense fallback={<Loading></Loading>}>
<OAuth2Callback type='oidc'></OAuth2Callback> <OAuth2Callback type='oidc'></OAuth2Callback>
</Suspense> </Suspense>
} }
/> />
<Route <Route
path='/oauth/linuxdo' path='/oauth/linuxdo'
element={ element={
<Suspense fallback={<Loading></Loading>} key={location.pathname}> <Suspense fallback={<Loading></Loading>} key={location.pathname}>
<OAuth2Callback type='linuxdo'></OAuth2Callback> <OAuth2Callback type='linuxdo'></OAuth2Callback>
</Suspense> </Suspense>
} }
/> />
@@ -274,19 +274,19 @@ function App() {
} }
/> />
{/* 方便使用chat2link直接跳转聊天... */} {/* 方便使用chat2link直接跳转聊天... */}
<Route <Route
path='/chat2link' path='/chat2link'
element={ element={
<PrivateRoute> <PrivateRoute>
<Suspense fallback={<Loading></Loading>} key={location.pathname}> <Suspense fallback={<Loading></Loading>} key={location.pathname}>
<Chat2Link /> <Chat2Link />
</Suspense> </Suspense>
</PrivateRoute> </PrivateRoute>
} }
/> />
<Route path='*' element={<NotFound />} /> <Route path='*' element={<NotFound />} />
</Routes> </Routes>
</> </>
); );
} }

File diff suppressed because it is too large Load Diff

View File

@@ -28,11 +28,7 @@ const FooterBar = () => {
New API {import.meta.env.VITE_REACT_APP_VERSION}{' '} New API {import.meta.env.VITE_REACT_APP_VERSION}{' '}
</a> </a>
{t('由')}{' '} {t('由')}{' '}
<a <a href='https://github.com/Calcium-Ion' target='_blank' rel='noreferrer'>
href='https://github.com/Calcium-Ion'
target='_blank'
rel='noreferrer'
>
Calcium-Ion Calcium-Ion
</a>{' '} </a>{' '}
{t('开发,基于')}{' '} {t('开发,基于')}{' '}
@@ -59,10 +55,12 @@ const FooterBar = () => {
}, []); }, []);
return ( return (
<div style={{ <div
textAlign: 'center', style={{
paddingBottom: '5px', textAlign: 'center',
}}> paddingBottom: '5px',
}}
>
{footer ? ( {footer ? (
<div <div
className='custom-footer' className='custom-footer'

View File

@@ -13,18 +13,28 @@ import {
IconClose, IconClose,
IconHelpCircle, IconHelpCircle,
IconHome, IconHome,
IconHomeStroked, IconIndentLeft, IconHomeStroked,
IconIndentLeft,
IconComment, IconComment,
IconKey, IconMenu, IconKey,
IconMenu,
IconNoteMoneyStroked, IconNoteMoneyStroked,
IconPriceTag, IconPriceTag,
IconUser, IconUser,
IconLanguage, IconLanguage,
IconInfoCircle, IconInfoCircle,
IconCreditCard, IconCreditCard,
IconTerminal IconTerminal,
} from '@douyinfe/semi-icons'; } from '@douyinfe/semi-icons';
import { Avatar, Button, Dropdown, Layout, Nav, Switch, Tag } from '@douyinfe/semi-ui'; import {
Avatar,
Button,
Dropdown,
Layout,
Nav,
Switch,
Tag,
} from '@douyinfe/semi-ui';
import { stringToColor } from '../helpers/render'; import { stringToColor } from '../helpers/render';
import Text from '@douyinfe/semi-ui/lib/es/typography/text'; import Text from '@douyinfe/semi-ui/lib/es/typography/text';
import { StyleContext } from '../context/Style/index.js'; import { StyleContext } from '../context/Style/index.js';
@@ -36,20 +46,20 @@ const headerStyle = {
borderBottom: '1px solid var(--semi-color-border)', borderBottom: '1px solid var(--semi-color-border)',
background: 'var(--semi-color-bg-0)', background: 'var(--semi-color-bg-0)',
transition: 'all 0.3s ease', transition: 'all 0.3s ease',
width: '100%' width: '100%',
}; };
// 自定义顶部栏按钮样式 // 自定义顶部栏按钮样式
const headerItemStyle = { const headerItemStyle = {
borderRadius: '4px', borderRadius: '4px',
margin: '0 4px', margin: '0 4px',
transition: 'all 0.3s ease' transition: 'all 0.3s ease',
}; };
// 自定义顶部栏按钮悬停样式 // 自定义顶部栏按钮悬停样式
const headerItemHoverStyle = { const headerItemHoverStyle = {
backgroundColor: 'var(--semi-color-primary-light-default)', backgroundColor: 'var(--semi-color-primary-light-default)',
color: 'var(--semi-color-primary)' color: 'var(--semi-color-primary)',
}; };
// 自定义顶部栏Logo样式 // 自定义顶部栏Logo样式
@@ -58,23 +68,24 @@ const logoStyle = {
alignItems: 'center', alignItems: 'center',
gap: '10px', gap: '10px',
padding: '0 10px', padding: '0 10px',
height: '100%' height: '100%',
}; };
// 自定义顶部栏系统名称样式 // 自定义顶部栏系统名称样式
const systemNameStyle = { const systemNameStyle = {
fontWeight: 'bold', fontWeight: 'bold',
fontSize: '18px', fontSize: '18px',
background: 'linear-gradient(45deg, var(--semi-color-primary), var(--semi-color-secondary))', background:
'linear-gradient(45deg, var(--semi-color-primary), var(--semi-color-secondary))',
WebkitBackgroundClip: 'text', WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent', WebkitTextFillColor: 'transparent',
padding: '0 5px' padding: '0 5px',
}; };
// 自定义顶部栏按钮图标样式 // 自定义顶部栏按钮图标样式
const headerIconStyle = { const headerIconStyle = {
fontSize: '18px', fontSize: '18px',
transition: 'all 0.3s ease' transition: 'all 0.3s ease',
}; };
// 自定义头像样式 // 自定义头像样式
@@ -82,19 +93,19 @@ const avatarStyle = {
margin: '4px', margin: '4px',
cursor: 'pointer', cursor: 'pointer',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)', boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
transition: 'all 0.3s ease' transition: 'all 0.3s ease',
}; };
// 自定义下拉菜单样式 // 自定义下拉菜单样式
const dropdownStyle = { const dropdownStyle = {
borderRadius: '8px', borderRadius: '8px',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)', boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
overflow: 'hidden' overflow: 'hidden',
}; };
// 自定义主题切换开关样式 // 自定义主题切换开关样式
const switchStyle = { const switchStyle = {
margin: '0 8px' margin: '0 8px',
}; };
const HeaderBar = () => { const HeaderBar = () => {
@@ -109,8 +120,7 @@ const HeaderBar = () => {
const logo = getLogo(); const logo = getLogo();
const currentDate = new Date(); const currentDate = new Date();
// enable fireworks on new year(1.1 and 2.9-2.24) // enable fireworks on new year(1.1 and 2.9-2.24)
const isNewYear = const isNewYear = currentDate.getMonth() === 0 && currentDate.getDate() === 1;
(currentDate.getMonth() === 0 && currentDate.getDate() === 1);
// Check if self-use mode is enabled // Check if self-use mode is enabled
const isSelfUseMode = statusState?.status?.self_use_mode_enabled || false; const isSelfUseMode = statusState?.status?.self_use_mode_enabled || false;
@@ -137,13 +147,17 @@ const HeaderBar = () => {
icon: <IconPriceTag style={headerIconStyle} />, icon: <IconPriceTag style={headerIconStyle} />,
}, },
// Only include the docs button if docsLink exists // Only include the docs button if docsLink exists
...(docsLink ? [{ ...(docsLink
text: t('文档'), ? [
itemKey: 'docs', {
isExternal: true, text: t('文档'),
externalLink: docsLink, itemKey: 'docs',
icon: <IconHelpCircle style={headerIconStyle} />, isExternal: true,
}] : []), externalLink: docsLink,
icon: <IconHelpCircle style={headerIconStyle} />,
},
]
: []),
{ {
text: t('关于'), text: t('关于'),
itemKey: 'about', itemKey: 'about',
@@ -232,30 +246,38 @@ const HeaderBar = () => {
chat: '/chat', chat: '/chat',
}; };
return ( return (
<div onClick={(e) => { <div
if (props.itemKey === 'home') { onClick={(e) => {
styleDispatch({ type: 'SET_INNER_PADDING', payload: false }); if (props.itemKey === 'home') {
styleDispatch({ type: 'SET_SIDER', payload: false }); styleDispatch({
} else { type: 'SET_INNER_PADDING',
styleDispatch({ type: 'SET_INNER_PADDING', payload: true }); payload: false,
if (!styleState.isMobile) { });
styleDispatch({ type: 'SET_SIDER', payload: true }); styleDispatch({ type: 'SET_SIDER', payload: false });
} else {
styleDispatch({
type: 'SET_INNER_PADDING',
payload: true,
});
if (!styleState.isMobile) {
styleDispatch({ type: 'SET_SIDER', payload: true });
}
} }
} }}
}}> >
{props.isExternal ? ( {props.isExternal ? (
<a <a
className="header-bar-text" className='header-bar-text'
style={{ textDecoration: 'none' }} style={{ textDecoration: 'none' }}
href={props.externalLink} href={props.externalLink}
target="_blank" target='_blank'
rel="noopener noreferrer" rel='noopener noreferrer'
> >
{itemElement} {itemElement}
</a> </a>
) : ( ) : (
<Link <Link
className="header-bar-text" className='header-bar-text'
style={{ textDecoration: 'none' }} style={{ textDecoration: 'none' }}
to={routerMap[props.itemKey]} to={routerMap[props.itemKey]}
> >
@@ -268,67 +290,98 @@ const HeaderBar = () => {
selectedKeys={[]} selectedKeys={[]}
// items={headerButtons} // items={headerButtons}
onSelect={(key) => {}} onSelect={(key) => {}}
header={styleState.isMobile?{ header={
logo: ( styleState.isMobile
<div style={{ display: 'flex', alignItems: 'center', position: 'relative' }}> ? {
{ logo: (
!styleState.showSider ? <div
<Button icon={<IconMenu />} theme="light" aria-label={t('展开侧边栏')} onClick={ style={{
() => styleDispatch({ type: 'SET_SIDER', payload: true }) display: 'flex',
} />: alignItems: 'center',
<Button icon={<IconIndentLeft />} theme="light" aria-label={t('闭侧边栏')} onClick={ position: 'relative',
() => styleDispatch({ type: 'SET_SIDER', payload: false }) }}
} /> >
{!styleState.showSider ? (
<Button
icon={<IconMenu />}
theme='light'
aria-label={t('展开侧边栏')}
onClick={() =>
styleDispatch({
type: 'SET_SIDER',
payload: true,
})
}
/>
) : (
<Button
icon={<IconIndentLeft />}
theme='light'
aria-label={t('闭侧边栏')}
onClick={() =>
styleDispatch({
type: 'SET_SIDER',
payload: false,
})
}
/>
)}
{(isSelfUseMode || isDemoSiteMode) && (
<Tag
color={isSelfUseMode ? 'purple' : 'blue'}
style={{
position: 'absolute',
top: '-8px',
right: '-15px',
fontSize: '0.7rem',
padding: '0 4px',
height: 'auto',
lineHeight: '1.2',
zIndex: 1,
pointerEvents: 'none',
}}
>
{isSelfUseMode ? t('自用模式') : t('演示站点')}
</Tag>
)}
</div>
),
} }
{(isSelfUseMode || isDemoSiteMode) && ( : {
<Tag logo: (
color={isSelfUseMode ? 'purple' : 'blue'} <div style={logoStyle}>
style={{ <img src={logo} alt='logo' style={{ height: '28px' }} />
position: 'absolute', </div>
top: '-8px', ),
right: '-15px', text: (
fontSize: '0.7rem', <div
padding: '0 4px', style={{
height: 'auto', position: 'relative',
lineHeight: '1.2', display: 'inline-block',
zIndex: 1, }}
pointerEvents: 'none' >
}} <span style={systemNameStyle}>{systemName}</span>
> {(isSelfUseMode || isDemoSiteMode) && (
{isSelfUseMode ? t('自用模式') : t('演示站点')} <Tag
</Tag> color={isSelfUseMode ? 'purple' : 'blue'}
)} style={{
</div> position: 'absolute',
), top: '-10px',
}:{ right: '-25px',
logo: ( fontSize: '0.7rem',
<div style={logoStyle}> padding: '0 4px',
<img src={logo} alt='logo' style={{ height: '28px' }} /> whiteSpace: 'nowrap',
</div> zIndex: 1,
), boxShadow: '0 0 3px rgba(255, 255, 255, 0.7)',
text: ( }}
<div style={{ position: 'relative', display: 'inline-block' }}> >
<span style={systemNameStyle}>{systemName}</span> {isSelfUseMode ? t('自用模式') : t('演示站点')}
{(isSelfUseMode || isDemoSiteMode) && ( </Tag>
<Tag )}
color={isSelfUseMode ? 'purple' : 'blue'} </div>
style={{ ),
position: 'absolute', }
top: '-10px', }
right: '-25px',
fontSize: '0.7rem',
padding: '0 4px',
whiteSpace: 'nowrap',
zIndex: 1,
boxShadow: '0 0 3px rgba(255, 255, 255, 0.7)'
}}
>
{isSelfUseMode ? t('自用模式') : t('演示站点')}
</Tag>
)}
</div>
),
}}
items={buttons} items={buttons}
footer={ footer={
<> <>
@@ -351,7 +404,7 @@ const HeaderBar = () => {
<> <>
<Switch <Switch
checkedText='🌞' checkedText='🌞'
size={styleState.isMobile?'default':'large'} size={styleState.isMobile ? 'default' : 'large'}
checked={theme === 'dark'} checked={theme === 'dark'}
uncheckedText='🌙' uncheckedText='🌙'
style={switchStyle} style={switchStyle}
@@ -390,7 +443,9 @@ const HeaderBar = () => {
position='bottomRight' position='bottomRight'
render={ render={
<Dropdown.Menu style={dropdownStyle}> <Dropdown.Menu style={dropdownStyle}>
<Dropdown.Item onClick={logout}>{t('退出')}</Dropdown.Item> <Dropdown.Item onClick={logout}>
{t('退出')}
</Dropdown.Item>
</Dropdown.Menu> </Dropdown.Menu>
} }
> >
@@ -401,14 +456,18 @@ const HeaderBar = () => {
> >
{userState.user.username[0]} {userState.user.username[0]}
</Avatar> </Avatar>
{styleState.isMobile?null:<Text style={{ marginLeft: '4px', fontWeight: '500' }}>{userState.user.username}</Text>} {styleState.isMobile ? null : (
<Text style={{ marginLeft: '4px', fontWeight: '500' }}>
{userState.user.username}
</Text>
)}
</Dropdown> </Dropdown>
</> </>
) : ( ) : (
<> <>
<Nav.Item <Nav.Item
itemKey={'login'} itemKey={'login'}
text={!styleState.isMobile?t('登录'):null} text={!styleState.isMobile ? t('登录') : null}
icon={<IconUser style={headerIconStyle} />} icon={<IconUser style={headerIconStyle} />}
/> />
{ {

View File

@@ -9,7 +9,11 @@ import {
showSuccess, showSuccess,
updateAPI, updateAPI,
} from '../helpers'; } from '../helpers';
import {onGitHubOAuthClicked, onOIDCClicked, onLinuxDOOAuthClicked} from './utils'; import {
onGitHubOAuthClicked,
onOIDCClicked,
onLinuxDOOAuthClicked,
} from './utils';
import Turnstile from 'react-turnstile'; import Turnstile from 'react-turnstile';
import { import {
Button, Button,
@@ -71,7 +75,6 @@ const LoginForm = () => {
} }
}, []); }, []);
const onWeChatLoginClicked = () => { const onWeChatLoginClicked = () => {
setShowWeChatLoginModal(true); setShowWeChatLoginModal(true);
}; };
@@ -223,7 +226,8 @@ const LoginForm = () => {
}} }}
> >
<Text> <Text>
{t('没有账户?')} <Link to='/register'>{t('点击注册')}</Link> {t('没有账户?')}{' '}
<Link to='/register'>{t('点击注册')}</Link>
</Text> </Text>
<Text> <Text>
{t('忘记密码?')} <Link to='/reset'>{t('点击重置')}</Link> {t('忘记密码?')} <Link to='/reset'>{t('点击重置')}</Link>
@@ -257,15 +261,18 @@ const LoginForm = () => {
<></> <></>
)} )}
{status.oidc_enabled ? ( {status.oidc_enabled ? (
<Button <Button
type='primary' type='primary'
icon={<OIDCIcon />} icon={<OIDCIcon />}
onClick={() => onClick={() =>
onOIDCClicked(status.oidc_authorization_endpoint, status.oidc_client_id) onOIDCClicked(
} status.oidc_authorization_endpoint,
/> status.oidc_client_id,
)
}
/>
) : ( ) : (
<></> <></>
)} )}
{status.linuxdo_oauth ? ( {status.linuxdo_oauth ? (
<Button <Button
@@ -331,7 +338,9 @@ const LoginForm = () => {
</div> </div>
<div style={{ textAlign: 'center' }}> <div style={{ textAlign: 'center' }}>
<p> <p>
{t('微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)')} {t(
'微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)',
)}
</p> </p>
</div> </div>
<Form size='large'> <Form size='large'>

View File

@@ -12,17 +12,19 @@ import {
import { import {
Avatar, Avatar,
Button, Descriptions, Button,
Descriptions,
Form, Form,
Layout, Layout,
Modal, Popover, Modal,
Popover,
Select, Select,
Space, Space,
Spin, Spin,
Table, Table,
Tag, Tag,
Tooltip, Tooltip,
Checkbox Checkbox,
} from '@douyinfe/semi-ui'; } from '@douyinfe/semi-ui';
import { ITEMS_PER_PAGE } from '../constants'; import { ITEMS_PER_PAGE } from '../constants';
import { import {
@@ -36,7 +38,7 @@ import {
renderModelPriceSimple, renderModelPriceSimple,
renderNumber, renderNumber,
renderQuota, renderQuota,
stringToColor stringToColor,
} from '../helpers/render'; } from '../helpers/render';
import Paragraph from '@douyinfe/semi-ui/lib/es/typography/paragraph'; import Paragraph from '@douyinfe/semi-ui/lib/es/typography/paragraph';
import { getLogOther } from '../helpers/other.js'; import { getLogOther } from '../helpers/other.js';
@@ -78,23 +80,51 @@ const LogsTable = () => {
function renderType(type) { function renderType(type) {
switch (type) { switch (type) {
case 1: case 1:
return <Tag color='cyan' size='large'>{t('充值')}</Tag>; return (
<Tag color='cyan' size='large'>
{t('充值')}
</Tag>
);
case 2: case 2:
return <Tag color='lime' size='large'>{t('消费')}</Tag>; return (
<Tag color='lime' size='large'>
{t('消费')}
</Tag>
);
case 3: case 3:
return <Tag color='orange' size='large'>{t('管理')}</Tag>; return (
<Tag color='orange' size='large'>
{t('管理')}
</Tag>
);
case 4: case 4:
return <Tag color='purple' size='large'>{t('系统')}</Tag>; return (
<Tag color='purple' size='large'>
{t('系统')}
</Tag>
);
default: default:
return <Tag color='black' size='large'>{t('未知')}</Tag>; return (
<Tag color='black' size='large'>
{t('未知')}
</Tag>
);
} }
} }
function renderIsStream(bool) { function renderIsStream(bool) {
if (bool) { if (bool) {
return <Tag color='blue' size='large'>{t('流')}</Tag>; return (
<Tag color='blue' size='large'>
{t('流')}
</Tag>
);
} else { } else {
return <Tag color='purple' size='large'>{t('非流')}</Tag>; return (
<Tag color='purple' size='large'>
{t('非流')}
</Tag>
);
} }
} }
@@ -152,56 +182,70 @@ const LogsTable = () => {
} }
function renderModelName(record) { function renderModelName(record) {
let other = getLogOther(record.other); let other = getLogOther(record.other);
let modelMapped = other?.is_model_mapped && other?.upstream_model_name && other?.upstream_model_name !== ''; let modelMapped =
other?.is_model_mapped &&
other?.upstream_model_name &&
other?.upstream_model_name !== '';
if (!modelMapped) { if (!modelMapped) {
return <Tag return (
color={stringToColor(record.model_name)} <Tag
size='large' color={stringToColor(record.model_name)}
onClick={(event) => { size='large'
copyText(event, record.model_name).then(r => {}); onClick={(event) => {
}} copyText(event, record.model_name).then((r) => {});
> }}
{' '}{record.model_name}{' '} >
</Tag>; {' '}
{record.model_name}{' '}
</Tag>
);
} else { } else {
return ( return (
<> <>
<Space vertical align={'start'}> <Space vertical align={'start'}>
<Popover content={ <Popover
<div style={{padding: 10}}> content={
<Space vertical align={'start'}> <div style={{ padding: 10 }}>
<Tag <Space vertical align={'start'}>
color={stringToColor(record.model_name)} <Tag
size='large' color={stringToColor(record.model_name)}
onClick={(event) => { size='large'
copyText(event, record.model_name).then(r => {}); onClick={(event) => {
}} copyText(event, record.model_name).then((r) => {});
> }}
{t('请求并计费模型')}{' '}{record.model_name}{' '} >
</Tag> {t('请求并计费模型')} {record.model_name}{' '}
<Tag </Tag>
color={stringToColor(other.upstream_model_name)} <Tag
size='large' color={stringToColor(other.upstream_model_name)}
onClick={(event) => { size='large'
copyText(event, other.upstream_model_name).then(r => {}); onClick={(event) => {
}} copyText(event, other.upstream_model_name).then(
> (r) => {},
{t('实际模型')}{' '}{other.upstream_model_name}{' '} );
</Tag> }}
</Space> >
</div> {t('实际模型')} {other.upstream_model_name}{' '}
}> </Tag>
</Space>
</div>
}
>
<Tag <Tag
color={stringToColor(record.model_name)} color={stringToColor(record.model_name)}
size='large' size='large'
onClick={(event) => { onClick={(event) => {
copyText(event, record.model_name).then(r => {}); copyText(event, record.model_name).then((r) => {});
}} }}
suffixIcon={<IconRefresh style={{width: '0.8em', height: '0.8em', opacity: 0.6}} />} suffixIcon={
<IconRefresh
style={{ width: '0.8em', height: '0.8em', opacity: 0.6 }}
/>
}
> >
{' '}{record.model_name}{' '} {' '}
{record.model_name}{' '}
</Tag> </Tag>
</Popover> </Popover>
{/*<Tooltip content={t('实际模型')}>*/} {/*<Tooltip content={t('实际模型')}>*/}
@@ -219,7 +263,6 @@ const LogsTable = () => {
</> </>
); );
} }
} }
// Define column keys for selection // Define column keys for selection
@@ -236,7 +279,7 @@ const LogsTable = () => {
COMPLETION: 'completion', COMPLETION: 'completion',
COST: 'cost', COST: 'cost',
RETRY: 'retry', RETRY: 'retry',
DETAILS: 'details' DETAILS: 'details',
}; };
// State for column visibility // State for column visibility
@@ -277,7 +320,7 @@ const LogsTable = () => {
[COLUMN_KEYS.COMPLETION]: true, [COLUMN_KEYS.COMPLETION]: true,
[COLUMN_KEYS.COST]: true, [COLUMN_KEYS.COST]: true,
[COLUMN_KEYS.RETRY]: isAdminUser, [COLUMN_KEYS.RETRY]: isAdminUser,
[COLUMN_KEYS.DETAILS]: true [COLUMN_KEYS.DETAILS]: true,
}; };
}; };
@@ -296,12 +339,17 @@ const LogsTable = () => {
// Handle "Select All" checkbox // Handle "Select All" checkbox
const handleSelectAll = (checked) => { const handleSelectAll = (checked) => {
const allKeys = Object.keys(COLUMN_KEYS).map(key => COLUMN_KEYS[key]); const allKeys = Object.keys(COLUMN_KEYS).map((key) => COLUMN_KEYS[key]);
const updatedColumns = {}; const updatedColumns = {};
allKeys.forEach(key => { allKeys.forEach((key) => {
// For admin-only columns, only enable them if user is admin // For admin-only columns, only enable them if user is admin
if ((key === COLUMN_KEYS.CHANNEL || key === COLUMN_KEYS.USERNAME || key === COLUMN_KEYS.RETRY) && !isAdminUser) { if (
(key === COLUMN_KEYS.CHANNEL ||
key === COLUMN_KEYS.USERNAME ||
key === COLUMN_KEYS.RETRY) &&
!isAdminUser
) {
updatedColumns[key] = false; updatedColumns[key] = false;
} else { } else {
updatedColumns[key] = checked; updatedColumns[key] = checked;
@@ -361,7 +409,7 @@ const LogsTable = () => {
style={{ marginRight: 4 }} style={{ marginRight: 4 }}
onClick={(event) => { onClick={(event) => {
event.stopPropagation(); event.stopPropagation();
showUserInfo(record.user_id) showUserInfo(record.user_id);
}} }}
> >
{typeof text === 'string' && text.slice(0, 1)} {typeof text === 'string' && text.slice(0, 1)}
@@ -403,32 +451,27 @@ const LogsTable = () => {
dataIndex: 'group', dataIndex: 'group',
render: (text, record, index) => { render: (text, record, index) => {
if (record.type === 0 || record.type === 2) { if (record.type === 0 || record.type === 2) {
if (record.group) { if (record.group) {
return ( return <>{renderGroup(record.group)}</>;
<> } else {
{renderGroup(record.group)} let other = null;
</> try {
); other = JSON.parse(record.other);
} else { } catch (e) {
let other = null; console.error(
try { `Failed to parse record.other: "${record.other}".`,
other = JSON.parse(record.other); e,
} catch (e) { );
console.error(`Failed to parse record.other: "${record.other}".`, e); }
} if (other === null) {
if (other === null) { return <></>;
return <></>; }
} if (other.group !== undefined) {
if (other.group !== undefined) { return <>{renderGroup(other.group)}</>;
return ( } else {
<> return <></>;
{renderGroup(other.group)} }
</> }
);
} else {
return <></>;
}
}
} else { } else {
return <></>; return <></>;
} }
@@ -572,30 +615,30 @@ const LogsTable = () => {
let content = other?.claude let content = other?.claude
? renderClaudeModelPriceSimple( ? renderClaudeModelPriceSimple(
other.model_ratio, other.model_ratio,
other.model_price, other.model_price,
other.group_ratio, other.group_ratio,
other.cache_tokens || 0, other.cache_tokens || 0,
other.cache_ratio || 1.0, other.cache_ratio || 1.0,
other.cache_creation_tokens || 0, other.cache_creation_tokens || 0,
other.cache_creation_ratio || 1.0, other.cache_creation_ratio || 1.0,
) )
: renderModelPriceSimple( : renderModelPriceSimple(
other.model_ratio, other.model_ratio,
other.model_price, other.model_price,
other.group_ratio, other.group_ratio,
other.cache_tokens || 0, other.cache_tokens || 0,
other.cache_ratio || 1.0, other.cache_ratio || 1.0,
); );
return ( return (
<Paragraph <Paragraph
ellipsis={{ ellipsis={{
rows: 2, rows: 2,
}} }}
style={{ maxWidth: 240 }} style={{ maxWidth: 240 }}
> >
{content} {content}
</Paragraph> </Paragraph>
); );
}, },
}, },
@@ -605,13 +648,16 @@ const LogsTable = () => {
useEffect(() => { useEffect(() => {
if (Object.keys(visibleColumns).length > 0) { if (Object.keys(visibleColumns).length > 0) {
// Save to localStorage // Save to localStorage
localStorage.setItem('logs-table-columns', JSON.stringify(visibleColumns)); localStorage.setItem(
'logs-table-columns',
JSON.stringify(visibleColumns),
);
} }
}, [visibleColumns]); }, [visibleColumns]);
// Filter columns based on visibility settings // Filter columns based on visibility settings
const getVisibleColumns = () => { const getVisibleColumns = () => {
return allColumns.filter(column => visibleColumns[column.key]); return allColumns.filter((column) => visibleColumns[column.key]);
}; };
// Column selector modal // Column selector modal
@@ -624,42 +670,59 @@ const LogsTable = () => {
footer={ footer={
<> <>
<Button onClick={() => initDefaultColumns()}>{t('重置')}</Button> <Button onClick={() => initDefaultColumns()}>{t('重置')}</Button>
<Button onClick={() => setShowColumnSelector(false)}>{t('取消')}</Button> <Button onClick={() => setShowColumnSelector(false)}>
<Button type="primary" onClick={() => setShowColumnSelector(false)}>{t('确定')}</Button> {t('取消')}
</Button>
<Button type='primary' onClick={() => setShowColumnSelector(false)}>
{t('确定')}
</Button>
</> </>
} }
> >
<div style={{ marginBottom: 20 }}> <div style={{ marginBottom: 20 }}>
<Checkbox <Checkbox
checked={Object.values(visibleColumns).every(v => v === true)} checked={Object.values(visibleColumns).every((v) => v === true)}
indeterminate={Object.values(visibleColumns).some(v => v === true) && !Object.values(visibleColumns).every(v => v === true)} indeterminate={
onChange={e => handleSelectAll(e.target.checked)} Object.values(visibleColumns).some((v) => v === true) &&
!Object.values(visibleColumns).every((v) => v === true)
}
onChange={(e) => handleSelectAll(e.target.checked)}
> >
{t('全选')} {t('全选')}
</Checkbox> </Checkbox>
</div> </div>
<div style={{ <div
display: 'flex', style={{
flexWrap: 'wrap', display: 'flex',
maxHeight: '400px', flexWrap: 'wrap',
overflowY: 'auto', maxHeight: '400px',
border: '1px solid var(--semi-color-border)', overflowY: 'auto',
borderRadius: '6px', border: '1px solid var(--semi-color-border)',
padding: '16px' borderRadius: '6px',
}}> padding: '16px',
{allColumns.map(column => { }}
>
{allColumns.map((column) => {
// Skip admin-only columns for non-admin users // Skip admin-only columns for non-admin users
if (!isAdminUser && (column.key === COLUMN_KEYS.CHANNEL || if (
column.key === COLUMN_KEYS.USERNAME || !isAdminUser &&
column.key === COLUMN_KEYS.RETRY)) { (column.key === COLUMN_KEYS.CHANNEL ||
column.key === COLUMN_KEYS.USERNAME ||
column.key === COLUMN_KEYS.RETRY)
) {
return null; return null;
} }
return ( return (
<div key={column.key} style={{ width: '50%', marginBottom: 16, paddingRight: 8 }}> <div
key={column.key}
style={{ width: '50%', marginBottom: 16, paddingRight: 8 }}
>
<Checkbox <Checkbox
checked={!!visibleColumns[column.key]} checked={!!visibleColumns[column.key]}
onChange={e => handleColumnVisibilityChange(column.key, e.target.checked)} onChange={(e) =>
handleColumnVisibilityChange(column.key, e.target.checked)
}
> >
{column.title} {column.title}
</Checkbox> </Checkbox>
@@ -709,7 +772,7 @@ const LogsTable = () => {
}); });
const handleInputChange = (value, name) => { const handleInputChange = (value, name) => {
setInputs(inputs => ({ ...inputs, [name]: value })); setInputs((inputs) => ({ ...inputs, [name]: value }));
}; };
const getLogSelfStat = async () => { const getLogSelfStat = async () => {
@@ -765,10 +828,18 @@ const LogsTable = () => {
title: t('用户信息'), title: t('用户信息'),
content: ( content: (
<div style={{ padding: 12 }}> <div style={{ padding: 12 }}>
<p>{t('用户名')}: {data.username}</p> <p>
<p>{t('余额')}: {renderQuota(data.quota)}</p> {t('用户名')}: {data.username}
<p>{t('已用额度')}{renderQuota(data.used_quota)}</p> </p>
<p>{t('请求次数')}{renderNumber(data.request_count)}</p> <p>
{t('余额')}: {renderQuota(data.quota)}
</p>
<p>
{t('已用额度')}{renderQuota(data.used_quota)}
</p>
<p>
{t('请求次数')}{renderNumber(data.request_count)}
</p>
</div> </div>
), ),
centered: true, centered: true,
@@ -807,7 +878,7 @@ const LogsTable = () => {
if (isAdminUser && (logs[i].type === 0 || logs[i].type === 2)) { if (isAdminUser && (logs[i].type === 0 || logs[i].type === 2)) {
expandDataLocal.push({ expandDataLocal.push({
key: t('渠道信息'), key: t('渠道信息'),
value: `${logs[i].channel} - ${logs[i].channel_name || '[未知]'}` value: `${logs[i].channel} - ${logs[i].channel_name || '[未知]'}`,
}); });
} }
if (other?.ws || other?.audio) { if (other?.ws || other?.audio) {
@@ -845,25 +916,28 @@ const LogsTable = () => {
key: t('日志详情'), key: t('日志详情'),
value: other?.claude value: other?.claude
? renderClaudeLogContent( ? renderClaudeLogContent(
other?.model_ratio, other?.model_ratio,
other.completion_ratio, other.completion_ratio,
other.model_price, other.model_price,
other.group_ratio, other.group_ratio,
other.user_group_ratio, other.user_group_ratio,
other.cache_ratio || 1.0, other.cache_ratio || 1.0,
other.cache_creation_ratio || 1.0 other.cache_creation_ratio || 1.0,
) )
: renderLogContent( : renderLogContent(
other?.model_ratio, other?.model_ratio,
other.completion_ratio, other.completion_ratio,
other.model_price, other.model_price,
other.group_ratio, other.group_ratio,
other.user_group_ratio other.user_group_ratio,
), ),
}); });
} }
if (logs[i].type === 2) { if (logs[i].type === 2) {
let modelMapped = other?.is_model_mapped && other?.upstream_model_name && other?.upstream_model_name !== ''; let modelMapped =
other?.is_model_mapped &&
other?.upstream_model_name &&
other?.upstream_model_name !== '';
if (modelMapped) { if (modelMapped) {
expandDataLocal.push({ expandDataLocal.push({
key: t('请求并计费模型'), key: t('请求并计费模型'),
@@ -1014,29 +1088,41 @@ const LogsTable = () => {
<Header> <Header>
<Spin spinning={loadingStat}> <Spin spinning={loadingStat}>
<Space> <Space>
<Tag color='blue' size='large' style={{ <Tag
padding: 15, color='blue'
borderRadius: '8px', size='large'
fontWeight: 500, style={{
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)' padding: 15,
}}> borderRadius: '8px',
fontWeight: 500,
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
}}
>
{t('消耗额度')}: {renderQuota(stat.quota)} {t('消耗额度')}: {renderQuota(stat.quota)}
</Tag> </Tag>
<Tag color='pink' size='large' style={{ <Tag
padding: 15, color='pink'
borderRadius: '8px', size='large'
fontWeight: 500, style={{
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)' padding: 15,
}}> borderRadius: '8px',
fontWeight: 500,
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
}}
>
RPM: {stat.rpm} RPM: {stat.rpm}
</Tag> </Tag>
<Tag color='white' size='large' style={{ <Tag
padding: 15, color='white'
border: 'none', size='large'
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)', style={{
borderRadius: '8px', padding: 15,
fontWeight: 500, border: 'none',
}}> boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
borderRadius: '8px',
fontWeight: 500,
}}
>
TPM: {stat.tpm} TPM: {stat.tpm}
</Tag> </Tag>
</Space> </Space>
@@ -1046,46 +1132,46 @@ const LogsTable = () => {
<> <>
<Form.Section> <Form.Section>
<div style={{ marginBottom: 10 }}> <div style={{ marginBottom: 10 }}>
{ {styleState.isMobile ? (
styleState.isMobile ? ( <div>
<div>
<Form.DatePicker
field='start_timestamp'
label={t('起始时间')}
style={{ width: 272 }}
initValue={start_timestamp}
type='dateTime'
onChange={(value) => {
console.log(value);
handleInputChange(value, 'start_timestamp')
}}
/>
<Form.DatePicker
field='end_timestamp'
fluid
label={t('结束时间')}
style={{ width: 272 }}
initValue={end_timestamp}
type='dateTime'
onChange={(value) => handleInputChange(value, 'end_timestamp')}
/>
</div>
) : (
<Form.DatePicker <Form.DatePicker
field="range_timestamp" field='start_timestamp'
label={t('时间范围')} label={t('起始时间')}
initValue={[start_timestamp, end_timestamp]} style={{ width: 272 }}
type="dateTimeRange" initValue={start_timestamp}
name="range_timestamp" type='dateTime'
onChange={(value) => { onChange={(value) => {
if (Array.isArray(value) && value.length === 2) { console.log(value);
handleInputChange(value[0], 'start_timestamp'); handleInputChange(value, 'start_timestamp');
handleInputChange(value[1], 'end_timestamp');
}
}} }}
/> />
) <Form.DatePicker
} field='end_timestamp'
fluid
label={t('结束时间')}
style={{ width: 272 }}
initValue={end_timestamp}
type='dateTime'
onChange={(value) =>
handleInputChange(value, 'end_timestamp')
}
/>
</div>
) : (
<Form.DatePicker
field='range_timestamp'
label={t('时间范围')}
initValue={[start_timestamp, end_timestamp]}
type='dateTimeRange'
name='range_timestamp'
onChange={(value) => {
if (Array.isArray(value) && value.length === 2) {
handleInputChange(value[0], 'start_timestamp');
handleInputChange(value[1], 'end_timestamp');
}
}}
/>
)}
</div> </div>
</Form.Section> </Form.Section>
<Form.Input <Form.Input
@@ -1146,14 +1232,14 @@ const LogsTable = () => {
<Form.Section></Form.Section> <Form.Section></Form.Section>
</> </>
</Form> </Form>
<div style={{marginTop:10}}> <div style={{ marginTop: 10 }}>
<Select <Select
defaultValue='0' defaultValue='0'
style={{ width: 120 }} style={{ width: 120 }}
onChange={(value) => { onChange={(value) => {
setLogType(parseInt(value)); setLogType(parseInt(value));
loadLogs(0, pageSize, parseInt(value)); loadLogs(0, pageSize, parseInt(value));
}} }}
> >
<Select.Option value='0'>{t('全部')}</Select.Option> <Select.Option value='0'>{t('全部')}</Select.Option>
<Select.Option value='1'>{t('充值')}</Select.Option> <Select.Option value='1'>{t('充值')}</Select.Option>
@@ -1177,13 +1263,13 @@ const LogsTable = () => {
expandedRowRender={expandRowRender} expandedRowRender={expandRowRender}
expandRowByClick={true} expandRowByClick={true}
dataSource={logs} dataSource={logs}
rowKey="key" rowKey='key'
pagination={{ pagination={{
formatPageText: (page) => formatPageText: (page) =>
t('第 {{start}} - {{end}} 条,共 {{total}} 条', { t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
start: page.currentStart, start: page.currentStart,
end: page.currentEnd, end: page.currentEnd,
total: logCount total: logCount,
}), }),
currentPage: activePage, currentPage: activePage,
pageSize: pageSize, pageSize: pageSize,

View File

@@ -46,7 +46,6 @@ const LogsTable = () => {
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
const [modalContent, setModalContent] = useState(''); const [modalContent, setModalContent] = useState('');
function renderType(type) { function renderType(type) {
switch (type) { switch (type) {
case 'IMAGINE': case 'IMAGINE':
return ( return (
@@ -98,9 +97,9 @@ const LogsTable = () => {
); );
case 'UPLOAD': case 'UPLOAD':
return ( return (
<Tag color='blue' size='large'> <Tag color='blue' size='large'>
上传文件 上传文件
</Tag> </Tag>
); );
case 'SHORTEN': case 'SHORTEN':
return ( return (
@@ -154,7 +153,6 @@ const LogsTable = () => {
} }
function renderCode(code) { function renderCode(code) {
switch (code) { switch (code) {
case 1: case 1:
return ( return (
@@ -190,7 +188,6 @@ const LogsTable = () => {
} }
function renderStatus(type) { function renderStatus(type) {
switch (type) { switch (type) {
case 'SUCCESS': case 'SUCCESS':
return ( return (
@@ -251,7 +248,6 @@ const LogsTable = () => {
}; };
// 修改renderDuration函数以包含颜色逻辑 // 修改renderDuration函数以包含颜色逻辑
function renderDuration(submit_time, finishTime) { function renderDuration(submit_time, finishTime) {
if (!submit_time || !finishTime) return 'N/A'; if (!submit_time || !finishTime) return 'N/A';
const start = new Date(submit_time); const start = new Date(submit_time);
@@ -261,7 +257,7 @@ const LogsTable = () => {
const color = durationSec > 60 ? 'red' : 'green'; const color = durationSec > 60 ? 'red' : 'green';
return ( return (
<Tag color={color} size="large"> <Tag color={color} size='large'>
{durationSec} {t('秒')} {durationSec} {t('秒')}
</Tag> </Tag>
); );
@@ -560,7 +556,9 @@ const LogsTable = () => {
{isAdminUser && showBanner ? ( {isAdminUser && showBanner ? (
<Banner <Banner
type='info' type='info'
description={t('当前未开启Midjourney回调部分项目可能无法获得绘图结果可在运营设置中开启。')} description={t(
'当前未开启Midjourney回调部分项目可能无法获得绘图结果可在运营设置中开启。',
)}
/> />
) : ( ) : (
<></> <></>
@@ -634,7 +632,7 @@ const LogsTable = () => {
t('第 {{start}} - {{end}} 条,共 {{total}} 条', { t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
start: page.currentStart, start: page.currentStart,
end: page.currentEnd, end: page.currentEnd,
total: logCount total: logCount,
}), }),
}} }}
loading={loading} loading={loading}

View File

@@ -34,12 +34,12 @@ const ModelPricing = () => {
const [selectedGroup, setSelectedGroup] = useState('default'); const [selectedGroup, setSelectedGroup] = useState('default');
const rowSelection = useMemo( const rowSelection = useMemo(
() => ({ () => ({
onChange: (selectedRowKeys, selectedRows) => { onChange: (selectedRowKeys, selectedRows) => {
setSelectedRowKeys(selectedRowKeys); setSelectedRowKeys(selectedRowKeys);
}, },
}), }),
[] [],
); );
const handleChange = (value) => { const handleChange = (value) => {
@@ -96,9 +96,9 @@ const ModelPricing = () => {
borderStyle: 'solid', borderStyle: 'solid',
}} }}
> >
<IconVerify style={{ color: 'green' }} size="large" /> <IconVerify style={{ color: 'green' }} size='large' />
</Popover> </Popover>
) );
} }
const columns = [ const columns = [
@@ -106,7 +106,7 @@ const ModelPricing = () => {
title: t('可用性'), title: t('可用性'),
dataIndex: 'available', dataIndex: 'available',
render: (text, record, index) => { render: (text, record, index) => {
// if record.enable_groups contains selectedGroup, then available is true // if record.enable_groups contains selectedGroup, then available is true
return renderAvailable(record.enable_groups.includes(selectedGroup)); return renderAvailable(record.enable_groups.includes(selectedGroup));
}, },
sorter: (a, b) => a.available - b.available, sorter: (a, b) => a.available - b.available,
@@ -145,7 +145,6 @@ const ModelPricing = () => {
title: t('可用分组'), title: t('可用分组'),
dataIndex: 'enable_groups', dataIndex: 'enable_groups',
render: (text, record, index) => { render: (text, record, index) => {
// enable_groups is a string array // enable_groups is a string array
return ( return (
<Space> <Space>
@@ -153,11 +152,7 @@ const ModelPricing = () => {
if (usableGroup[group]) { if (usableGroup[group]) {
if (group === selectedGroup) { if (group === selectedGroup) {
return ( return (
<Tag <Tag color='blue' size='large' prefixIcon={<IconVerify />}>
color='blue'
size='large'
prefixIcon={<IconVerify />}
>
{group} {group}
</Tag> </Tag>
); );
@@ -168,10 +163,12 @@ const ModelPricing = () => {
size='large' size='large'
onClick={() => { onClick={() => {
setSelectedGroup(group); setSelectedGroup(group);
showInfo(t('当前查看的分组为:{{group}},倍率为:{{ratio}}', { showInfo(
group: group, t('当前查看的分组为:{{group}},倍率为:{{ratio}}', {
ratio: groupRatio[group] group: group,
})); ratio: groupRatio[group],
}),
);
}} }}
> >
{group} {group}
@@ -186,22 +183,23 @@ const ModelPricing = () => {
}, },
{ {
title: () => ( title: () => (
<span style={{'display':'flex','alignItems':'center'}}> <span style={{ display: 'flex', alignItems: 'center' }}>
{t('倍率')} {t('倍率')}
<Popover <Popover
content={ content={
<div style={{ padding: 8 }}> <div style={{ padding: 8 }}>
{t('倍率是为了方便换算不同价格的模型')}<br/> {t('倍率是为了方便换算不同价格的模型')}
<br />
{t('点击查看倍率说明')} {t('点击查看倍率说明')}
</div> </div>
} }
position='top' position='top'
style={{ style={{
backgroundColor: 'rgba(var(--semi-blue-4),1)', backgroundColor: 'rgba(var(--semi-blue-4),1)',
borderColor: 'rgba(var(--semi-blue-4),1)', borderColor: 'rgba(var(--semi-blue-4),1)',
color: 'var(--semi-color-white)', color: 'var(--semi-color-white)',
borderWidth: 1, borderWidth: 1,
borderStyle: 'solid', borderStyle: 'solid',
}} }}
> >
<IconHelpCircle <IconHelpCircle
@@ -219,11 +217,18 @@ const ModelPricing = () => {
let completionRatio = parseFloat(record.completion_ratio.toFixed(3)); let completionRatio = parseFloat(record.completion_ratio.toFixed(3));
content = ( content = (
<> <>
<Text>{t('模型倍率')}{record.quota_type === 0 ? text : t('无')}</Text> <Text>
{t('模型倍率')}{record.quota_type === 0 ? text : t('无')}
</Text>
<br /> <br />
<Text>{t('补全倍率')}{record.quota_type === 0 ? completionRatio : t('无')}</Text> <Text>
{t('补全倍率')}
{record.quota_type === 0 ? completionRatio : t('无')}
</Text>
<br /> <br />
<Text>{t('分组倍率')}{groupRatio[selectedGroup]}</Text> <Text>
{t('分组倍率')}{groupRatio[selectedGroup]}
</Text>
</> </>
); );
return <div>{content}</div>; return <div>{content}</div>;
@@ -236,21 +241,31 @@ const ModelPricing = () => {
let content = text; let content = text;
if (record.quota_type === 0) { if (record.quota_type === 0) {
// 这里的 *2 是因为 1倍率=0.002刀,请勿删除 // 这里的 *2 是因为 1倍率=0.002刀,请勿删除
let inputRatioPrice = record.model_ratio * 2 * groupRatio[selectedGroup]; let inputRatioPrice =
record.model_ratio * 2 * groupRatio[selectedGroup];
let completionRatioPrice = let completionRatioPrice =
record.model_ratio * record.model_ratio *
record.completion_ratio * 2 * record.completion_ratio *
2 *
groupRatio[selectedGroup]; groupRatio[selectedGroup];
content = ( content = (
<> <>
<Text>{t('提示')} ${inputRatioPrice} / 1M tokens</Text> <Text>
{t('提示')} ${inputRatioPrice} / 1M tokens
</Text>
<br /> <br />
<Text>{t('补全')} ${completionRatioPrice} / 1M tokens</Text> <Text>
{t('补全')} ${completionRatioPrice} / 1M tokens
</Text>
</> </>
); );
} else { } else {
let price = parseFloat(text) * groupRatio[selectedGroup]; let price = parseFloat(text) * groupRatio[selectedGroup];
content = <>${t('模型价格')}${price}</>; content = (
<>
${t('模型价格')}${price}
</>
);
} }
return <div>{content}</div>; return <div>{content}</div>;
}, },
@@ -300,7 +315,7 @@ const ModelPricing = () => {
if (success) { if (success) {
setGroupRatio(group_ratio); setGroupRatio(group_ratio);
setUsableGroup(usable_group); setUsableGroup(usable_group);
setSelectedGroup(userState.user ? userState.user.group : 'default') setSelectedGroup(userState.user ? userState.user.group : 'default');
setModelsFormat(data, group_ratio); setModelsFormat(data, group_ratio);
} else { } else {
showError(message); showError(message);
@@ -330,32 +345,38 @@ const ModelPricing = () => {
<Layout> <Layout>
{userState.user ? ( {userState.user ? (
<Banner <Banner
type="success" type='success'
fullMode={false} fullMode={false}
closeIcon="null" closeIcon='null'
description={t('您的默认分组为:{{group}},分组倍率为:{{ratio}}', { description={t('您的默认分组为:{{group}},分组倍率为:{{ratio}}', {
group: userState.user.group, group: userState.user.group,
ratio: groupRatio[userState.user.group] ratio: groupRatio[userState.user.group],
})} })}
/> />
) : ( ) : (
<Banner <Banner
type='warning' type='warning'
fullMode={false} fullMode={false}
closeIcon="null" closeIcon='null'
description={t('您还未登陆,显示的价格为默认分组倍率: {{ratio}}', { description={t('您还未登陆,显示的价格为默认分组倍率: {{ratio}}', {
ratio: groupRatio['default'] ratio: groupRatio['default'],
})} })}
/> />
)} )}
<br/> <br />
<Banner <Banner
type="info" type='info'
fullMode={false} fullMode={false}
description={<div>{t('按量计费费用 = 分组倍率 × 模型倍率 × 提示token数 + 补全token数 × 补全倍率)/ 500000 (单位:美元)')}</div>} description={
closeIcon="null" <div>
{t(
'按量计费费用 = 分组倍率 × 模型倍率 × 提示token数 + 补全token数 × 补全倍率)/ 500000 (单位:美元)',
)}
</div>
}
closeIcon='null'
/> />
<br/> <br />
<Space style={{ marginBottom: 16 }}> <Space style={{ marginBottom: 16 }}>
<Input <Input
placeholder={t('模糊搜索模型名称')} placeholder={t('模糊搜索模型名称')}
@@ -368,11 +389,11 @@ const ModelPricing = () => {
<Button <Button
theme='light' theme='light'
type='tertiary' type='tertiary'
style={{width: 150}} style={{ width: 150 }}
onClick={() => { onClick={() => {
copyText(selectedRowKeys); copyText(selectedRowKeys);
}} }}
disabled={selectedRowKeys == ""} disabled={selectedRowKeys == ''}
> >
{t('复制选中模型')} {t('复制选中模型')}
</Button> </Button>
@@ -387,7 +408,7 @@ const ModelPricing = () => {
t('第 {{start}} - {{end}} 条,共 {{total}} 条', { t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
start: page.currentStart, start: page.currentStart,
end: page.currentEnd, end: page.currentEnd,
total: models.length total: models.length,
}), }),
pageSize: models.length, pageSize: models.length,
showSizeChanger: false, showSizeChanger: false,

View File

@@ -1,7 +1,6 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Card, Spin, Tabs } from '@douyinfe/semi-ui'; import { Card, Spin, Tabs } from '@douyinfe/semi-ui';
import { API, showError, showSuccess } from '../helpers'; import { API, showError, showSuccess } from '../helpers';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import SettingGeminiModel from '../pages/Setting/Model/SettingGeminiModel.js'; import SettingGeminiModel from '../pages/Setting/Model/SettingGeminiModel.js';
@@ -31,14 +30,12 @@ const ModelSetting = () => {
if ( if (
item.key === 'gemini.safety_settings' || item.key === 'gemini.safety_settings' ||
item.key === 'gemini.version_settings' || item.key === 'gemini.version_settings' ||
item.key === 'claude.model_headers_settings'|| item.key === 'claude.model_headers_settings' ||
item.key === 'claude.default_max_tokens' item.key === 'claude.default_max_tokens'
) { ) {
item.value = JSON.stringify(JSON.parse(item.value), null, 2); item.value = JSON.stringify(JSON.parse(item.value), null, 2);
} }
if ( if (item.key.endsWith('Enabled') || item.key.endsWith('enabled')) {
item.key.endsWith('Enabled') || item.key.endsWith('enabled')
) {
newInputs[item.key] = item.value === 'true' ? true : false; newInputs[item.key] = item.value === 'true' ? true : false;
} else { } else {
newInputs[item.key] = item.value; newInputs[item.key] = item.value;

View File

@@ -6,56 +6,58 @@ import { UserContext } from '../context/User';
import { setUserData } from '../helpers/data.js'; import { setUserData } from '../helpers/data.js';
const OAuth2Callback = (props) => { const OAuth2Callback = (props) => {
const [searchParams, setSearchParams] = useSearchParams(); const [searchParams, setSearchParams] = useSearchParams();
const [userState, userDispatch] = useContext(UserContext); const [userState, userDispatch] = useContext(UserContext);
const [prompt, setPrompt] = useState('处理中...'); const [prompt, setPrompt] = useState('处理中...');
const [processing, setProcessing] = useState(true); const [processing, setProcessing] = useState(true);
let navigate = useNavigate(); let navigate = useNavigate();
const sendCode = async (code, state, count) => { const sendCode = async (code, state, count) => {
const res = await API.get(`/api/oauth/${props.type}?code=${code}&state=${state}`); const res = await API.get(
const { success, message, data } = res.data; `/api/oauth/${props.type}?code=${code}&state=${state}`,
if (success) {
if (message === 'bind') {
showSuccess('绑定成功!');
navigate('/setting');
} else {
userDispatch({ type: 'login', payload: data });
localStorage.setItem('user', JSON.stringify(data));
setUserData(data);
updateAPI()
showSuccess('登录成功!');
navigate('/token');
}
} else {
showError(message);
if (count === 0) {
setPrompt(`操作失败,重定向至登录界面中...`);
navigate('/setting'); // in case this is failed to bind GitHub
return;
}
count++;
setPrompt(`出现错误,第 ${count} 次重试中...`);
await new Promise((resolve) => setTimeout(resolve, count * 2000));
await sendCode(code, state, count);
}
};
useEffect(() => {
let code = searchParams.get('code');
let state = searchParams.get('state');
sendCode(code, state, 0).then();
}, []);
return (
<Segment style={{ minHeight: '300px' }}>
<Dimmer active inverted>
<Loader size='large'>{prompt}</Loader>
</Dimmer>
</Segment>
); );
const { success, message, data } = res.data;
if (success) {
if (message === 'bind') {
showSuccess('绑定成功!');
navigate('/setting');
} else {
userDispatch({ type: 'login', payload: data });
localStorage.setItem('user', JSON.stringify(data));
setUserData(data);
updateAPI();
showSuccess('登录成功!');
navigate('/token');
}
} else {
showError(message);
if (count === 0) {
setPrompt(`操作失败,重定向至登录界面中...`);
navigate('/setting'); // in case this is failed to bind GitHub
return;
}
count++;
setPrompt(`出现错误,第 ${count} 次重试中...`);
await new Promise((resolve) => setTimeout(resolve, count * 2000));
await sendCode(code, state, count);
}
};
useEffect(() => {
let code = searchParams.get('code');
let state = searchParams.get('state');
sendCode(code, state, 0).then();
}, []);
return (
<Segment style={{ minHeight: '300px' }}>
<Dimmer active inverted>
<Loader size='large'>{prompt}</Loader>
</Dimmer>
</Segment>
);
}; };
export default OAuth2Callback; export default OAuth2Callback;

View File

@@ -2,21 +2,37 @@ import React from 'react';
import { Icon } from '@douyinfe/semi-ui'; import { Icon } from '@douyinfe/semi-ui';
const OIDCIcon = (props) => { const OIDCIcon = (props) => {
function CustomIcon() { function CustomIcon() {
return ( return (
<svg t="1723135116886" className="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" <svg
p-id="10969" width="1em" height="1em"> t='1723135116886'
<path className='icon'
d="M512 960C265 960 64 759 64 512S265 64 512 64s448 201 448 448-201 448-448 448z m0-882.6c-239.7 0-434.6 195-434.6 434.6s195 434.6 434.6 434.6 434.6-195 434.6-434.6S751.7 77.4 512 77.4z" viewBox='0 0 1024 1024'
p-id="10970" fill="#2c2c2c" stroke="#2c2c2c" stroke-width="60"></path> version='1.1'
<path xmlns='http://www.w3.org/2000/svg'
d="M197.7 512c0-78.3 31.6-98.8 87.2-98.8 56.2 0 87.2 20.5 87.2 98.8s-31 98.8-87.2 98.8c-55.7 0-87.2-20.5-87.2-98.8z m130.4 0c0-46.8-7.8-64.5-43.2-64.5-35.2 0-42.9 17.7-42.9 64.5 0 47.1 7.8 63.7 42.9 63.7 35.4 0 43.2-16.6 43.2-63.7zM409.7 415.9h42.1V608h-42.1V415.9zM653.9 512c0 74.2-37.1 96.1-93.6 96.1h-65.9V415.9h65.9c56.5 0 93.6 16.1 93.6 96.1z m-43.5 0c0-49.3-17.7-60.6-52.3-60.6h-21.6v120.7h21.6c35.4 0 52.3-13.3 52.3-60.1zM686.5 512c0-74.2 36.3-98.8 92.7-98.8 18.3 0 33.2 2.2 44.8 6.4v36.3c-11.9-4.2-26-6.6-42.1-6.6-34.6 0-49.8 15.5-49.8 62.6 0 50.1 15.2 62.6 49.3 62.6 15.8 0 30.2-2.2 44.8-7.5v36c-11.3 4.7-28.5 8-46.8 8-56.1-0.2-92.9-18.7-92.9-99z" p-id='10969'
p-id="10971" fill="#2c2c2c" stroke="#2c2c2c" stroke-width="20"></path> width='1em'
</svg> height='1em'
); >
} <path
d='M512 960C265 960 64 759 64 512S265 64 512 64s448 201 448 448-201 448-448 448z m0-882.6c-239.7 0-434.6 195-434.6 434.6s195 434.6 434.6 434.6 434.6-195 434.6-434.6S751.7 77.4 512 77.4z'
p-id='10970'
fill='#2c2c2c'
stroke='#2c2c2c'
stroke-width='60'
></path>
<path
d='M197.7 512c0-78.3 31.6-98.8 87.2-98.8 56.2 0 87.2 20.5 87.2 98.8s-31 98.8-87.2 98.8c-55.7 0-87.2-20.5-87.2-98.8z m130.4 0c0-46.8-7.8-64.5-43.2-64.5-35.2 0-42.9 17.7-42.9 64.5 0 47.1 7.8 63.7 42.9 63.7 35.4 0 43.2-16.6 43.2-63.7zM409.7 415.9h42.1V608h-42.1V415.9zM653.9 512c0 74.2-37.1 96.1-93.6 96.1h-65.9V415.9h65.9c56.5 0 93.6 16.1 93.6 96.1z m-43.5 0c0-49.3-17.7-60.6-52.3-60.6h-21.6v120.7h21.6c35.4 0 52.3-13.3 52.3-60.1zM686.5 512c0-74.2 36.3-98.8 92.7-98.8 18.3 0 33.2 2.2 44.8 6.4v36.3c-11.9-4.2-26-6.6-42.1-6.6-34.6 0-49.8 15.5-49.8 62.6 0 50.1 15.2 62.6 49.3 62.6 15.8 0 30.2-2.2 44.8-7.5v36c-11.3 4.7-28.5 8-46.8 8-56.1-0.2-92.9-18.7-92.9-99z'
p-id='10971'
fill='#2c2c2c'
stroke='#2c2c2c'
stroke-width='20'
></path>
</svg>
);
}
return <Icon svg={<CustomIcon />} />; return <Icon svg={<CustomIcon />} />;
}; };
export default OIDCIcon; export default OIDCIcon;

View File

@@ -11,7 +11,6 @@ import ModelSettingsVisualEditor from '../pages/Setting/Operation/ModelSettingsV
import GroupRatioSettings from '../pages/Setting/Operation/GroupRatioSettings.js'; import GroupRatioSettings from '../pages/Setting/Operation/GroupRatioSettings.js';
import ModelRatioSettings from '../pages/Setting/Operation/ModelRatioSettings.js'; import ModelRatioSettings from '../pages/Setting/Operation/ModelRatioSettings.js';
import { API, showError, showSuccess } from '../helpers'; import { API, showError, showSuccess } from '../helpers';
import SettingsChats from '../pages/Setting/Operation/SettingsChats.js'; import SettingsChats from '../pages/Setting/Operation/SettingsChats.js';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@@ -58,7 +57,7 @@ const OperationSetting = () => {
DataExportInterval: 5, DataExportInterval: 5,
DefaultCollapseSidebar: false, // 默认折叠侧边栏 DefaultCollapseSidebar: false, // 默认折叠侧边栏
RetryTimes: 0, RetryTimes: 0,
Chats: "[]", Chats: '[]',
DemoSiteEnabled: false, DemoSiteEnabled: false,
SelfUseModeEnabled: false, SelfUseModeEnabled: false,
AutomaticDisableKeywords: '', AutomaticDisableKeywords: '',
@@ -154,14 +153,14 @@ const OperationSetting = () => {
</Card> </Card>
{/* 合并模型倍率设置和可视化倍率设置 */} {/* 合并模型倍率设置和可视化倍率设置 */}
<Card style={{ marginTop: '10px' }}> <Card style={{ marginTop: '10px' }}>
<Tabs type="line"> <Tabs type='line'>
<Tabs.TabPane tab={t('模型倍率设置')} itemKey="model"> <Tabs.TabPane tab={t('模型倍率设置')} itemKey='model'>
<ModelRatioSettings options={inputs} refresh={onRefresh} /> <ModelRatioSettings options={inputs} refresh={onRefresh} />
</Tabs.TabPane> </Tabs.TabPane>
<Tabs.TabPane tab={t('可视化倍率设置')} itemKey="visual"> <Tabs.TabPane tab={t('可视化倍率设置')} itemKey='visual'>
<ModelSettingsVisualEditor options={inputs} refresh={onRefresh} /> <ModelSettingsVisualEditor options={inputs} refresh={onRefresh} />
</Tabs.TabPane> </Tabs.TabPane>
<Tabs.TabPane tab={t('未设置倍率模型')} itemKey="unset_models"> <Tabs.TabPane tab={t('未设置倍率模型')} itemKey='unset_models'>
<ModelRatioNotSetEditor options={inputs} refresh={onRefresh} /> <ModelRatioNotSetEditor options={inputs} refresh={onRefresh} />
</Tabs.TabPane> </Tabs.TabPane>
</Tabs> </Tabs>

View File

@@ -1,5 +1,13 @@
import React, { useContext, useEffect, useRef, useState } from 'react'; import React, { useContext, useEffect, useRef, useState } from 'react';
import { Banner, Button, Col, Form, Row, Modal, Space } from '@douyinfe/semi-ui'; import {
Banner,
Button,
Col,
Form,
Row,
Modal,
Space,
} from '@douyinfe/semi-ui';
import { API, showError, showSuccess, timestamp2string } from '../helpers'; import { API, showError, showSuccess, timestamp2string } from '../helpers';
import { marked } from 'marked'; import { marked } from 'marked';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@@ -46,7 +54,7 @@ const OtherSetting = () => {
HomePageContent: false, HomePageContent: false,
About: false, About: false,
Footer: false, Footer: false,
CheckUpdate: false CheckUpdate: false,
}); });
const handleInputChange = async (value, e) => { const handleInputChange = async (value, e) => {
const name = e.target.id; const name = e.target.id;
@@ -151,7 +159,10 @@ const OtherSetting = () => {
const checkUpdate = async () => { const checkUpdate = async () => {
try { try {
setLoadingInput((loadingInput) => ({ ...loadingInput, CheckUpdate: true })); setLoadingInput((loadingInput) => ({
...loadingInput,
CheckUpdate: true,
}));
// Use a CORS proxy to avoid direct cross-origin requests to GitHub API // Use a CORS proxy to avoid direct cross-origin requests to GitHub API
// Option 1: Use a public CORS proxy service // Option 1: Use a public CORS proxy service
// const proxyUrl = 'https://cors-anywhere.herokuapp.com/'; // const proxyUrl = 'https://cors-anywhere.herokuapp.com/';
@@ -164,13 +175,13 @@ const OtherSetting = () => {
'https://api.github.com/repos/Calcium-Ion/new-api/releases/latest', 'https://api.github.com/repos/Calcium-Ion/new-api/releases/latest',
{ {
headers: { headers: {
'Accept': 'application/json', Accept: 'application/json',
'Content-Type': 'application/json', 'Content-Type': 'application/json',
// Adding User-Agent which is often required by GitHub API // Adding User-Agent which is often required by GitHub API
'User-Agent': 'new-api-update-checker' 'User-Agent': 'new-api-update-checker',
} },
} },
).then(response => response.json()); ).then((response) => response.json());
// Option 3: Use a local proxy endpoint // Option 3: Use a local proxy endpoint
// Create a cached version of the response to avoid frequent GitHub API calls // Create a cached version of the response to avoid frequent GitHub API calls
@@ -190,7 +201,10 @@ const OtherSetting = () => {
console.error('Failed to check for updates:', error); console.error('Failed to check for updates:', error);
showError('检查更新失败,请稍后再试'); showError('检查更新失败,请稍后再试');
} finally { } finally {
setLoadingInput((loadingInput) => ({ ...loadingInput, CheckUpdate: false })); setLoadingInput((loadingInput) => ({
...loadingInput,
CheckUpdate: false,
}));
} }
}; };
const getOptions = async () => { const getOptions = async () => {
@@ -217,7 +231,10 @@ const OtherSetting = () => {
// Function to open GitHub release page // Function to open GitHub release page
const openGitHubRelease = () => { const openGitHubRelease = () => {
window.open(`https://github.com/Calcium-Ion/new-api/releases/tag/${updateData.tag_name}`, '_blank'); window.open(
`https://github.com/Calcium-Ion/new-api/releases/tag/${updateData.tag_name}`,
'_blank',
);
}; };
const getStartTimeString = () => { const getStartTimeString = () => {
@@ -237,7 +254,11 @@ const OtherSetting = () => {
<Text> <Text>
{t('当前版本')}{statusState?.status?.version || t('未知')} {t('当前版本')}{statusState?.status?.version || t('未知')}
</Text> </Text>
<Button type="primary" onClick={checkUpdate} loading={loadingInput['CheckUpdate']}> <Button
type='primary'
onClick={checkUpdate}
loading={loadingInput['CheckUpdate']}
>
{t('检查更新')} {t('检查更新')}
</Button> </Button>
</Space> </Space>
@@ -245,7 +266,9 @@ const OtherSetting = () => {
</Row> </Row>
<Row> <Row>
<Col span={16}> <Col span={16}>
<Text>{t('启动时间')}{getStartTimeString()}</Text> <Text>
{t('启动时间')}{getStartTimeString()}
</Text>
</Col> </Col>
</Row> </Row>
</Form.Section> </Form.Section>
@@ -300,7 +323,9 @@ const OtherSetting = () => {
</Button> </Button>
<Form.TextArea <Form.TextArea
label={t('首页内容')} label={t('首页内容')}
placeholder={t('在此输入首页内容,支持 Markdown & HTML 代码,设置后首页的状态信息将不再显示。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为首页')} placeholder={t(
'在此输入首页内容,支持 Markdown & HTML 代码,设置后首页的状态信息将不再显示。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为首页',
)}
field={'HomePageContent'} field={'HomePageContent'}
onChange={handleInputChange} onChange={handleInputChange}
style={{ fontFamily: 'JetBrains Mono, Consolas' }} style={{ fontFamily: 'JetBrains Mono, Consolas' }}
@@ -314,7 +339,9 @@ const OtherSetting = () => {
</Button> </Button>
<Form.TextArea <Form.TextArea
label={t('关于')} label={t('关于')}
placeholder={t('在此输入新的关于内容,支持 Markdown & HTML 代码。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为关于页面')} placeholder={t(
'在此输入新的关于内容,支持 Markdown & HTML 代码。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为关于页面',
)}
field={'About'} field={'About'}
onChange={handleInputChange} onChange={handleInputChange}
style={{ fontFamily: 'JetBrains Mono, Consolas' }} style={{ fontFamily: 'JetBrains Mono, Consolas' }}
@@ -327,13 +354,17 @@ const OtherSetting = () => {
<Banner <Banner
fullMode={false} fullMode={false}
type='info' type='info'
description={t('移除 One API 的版权标识必须首先获得授权,项目维护需要花费大量精力,如果本项目对你有意义,请主动支持本项目')} description={t(
'移除 One API 的版权标识必须首先获得授权,项目维护需要花费大量精力,如果本项目对你有意义,请主动支持本项目',
)}
closeIcon={null} closeIcon={null}
style={{ marginTop: 15 }} style={{ marginTop: 15 }}
/> />
<Form.Input <Form.Input
label={t('页脚')} label={t('页脚')}
placeholder={t('在此输入新的页脚,留空则使用默认页脚,支持 HTML 代码')} placeholder={t(
'在此输入新的页脚,留空则使用默认页脚,支持 HTML 代码',
)}
field={'Footer'} field={'Footer'}
onChange={handleInputChange} onChange={handleInputChange}
/> />
@@ -349,15 +380,15 @@ const OtherSetting = () => {
onCancel={() => setShowUpdateModal(false)} onCancel={() => setShowUpdateModal(false)}
footer={[ footer={[
<Button <Button
key="details" key='details'
type="primary" type='primary'
onClick={() => { onClick={() => {
setShowUpdateModal(false); setShowUpdateModal(false);
openGitHubRelease(); openGitHubRelease();
}} }}
> >
{t('详情')} {t('详情')}
</Button> </Button>,
]} ]}
> >
<div dangerouslySetInnerHTML={{ __html: updateData.content }}></div> <div dangerouslySetInnerHTML={{ __html: updateData.content }}></div>

View File

@@ -13,7 +13,6 @@ import { UserContext } from '../context/User/index.js';
import { StatusContext } from '../context/Status/index.js'; import { StatusContext } from '../context/Status/index.js';
const { Sider, Content, Header, Footer } = Layout; const { Sider, Content, Header, Footer } = Layout;
const PageLayout = () => { const PageLayout = () => {
const [userState, userDispatch] = useContext(UserContext); const [userState, userDispatch] = useContext(UserContext);
const [statusState, statusDispatch] = useContext(StatusContext); const [statusState, statusDispatch] = useContext(StatusContext);
@@ -68,79 +67,98 @@ const PageLayout = () => {
}, [i18n]); }, [i18n]);
// 获取侧边栏折叠状态 // 获取侧边栏折叠状态
const isSidebarCollapsed = localStorage.getItem('default_collapse_sidebar') === 'true'; const isSidebarCollapsed =
localStorage.getItem('default_collapse_sidebar') === 'true';
return ( return (
<Layout style={{ <Layout
height: '100vh', style={{
display: 'flex', height: '100vh',
flexDirection: 'column', display: 'flex',
overflow: styleState.isMobile ? 'visible' : 'hidden' flexDirection: 'column',
}}> overflow: styleState.isMobile ? 'visible' : 'hidden',
<Header style={{ }}
padding: 0, >
height: 'auto', <Header
lineHeight: 'normal', style={{
position: styleState.isMobile ? 'sticky' : 'fixed', padding: 0,
width: '100%', height: 'auto',
top: 0, lineHeight: 'normal',
zIndex: 100, position: styleState.isMobile ? 'sticky' : 'fixed',
boxShadow: '0 1px 6px rgba(0, 0, 0, 0.08)' width: '100%',
}}> top: 0,
zIndex: 100,
boxShadow: '0 1px 6px rgba(0, 0, 0, 0.08)',
}}
>
<HeaderBar /> <HeaderBar />
</Header> </Header>
<Layout style={{ <Layout
marginTop: styleState.isMobile ? '0' : '56px', style={{
height: styleState.isMobile ? 'auto' : 'calc(100vh - 56px)', marginTop: styleState.isMobile ? '0' : '56px',
overflow: styleState.isMobile ? 'visible' : 'auto', height: styleState.isMobile ? 'auto' : 'calc(100vh - 56px)',
display: 'flex', overflow: styleState.isMobile ? 'visible' : 'auto',
flexDirection: 'column' display: 'flex',
}}> flexDirection: 'column',
}}
>
{styleState.showSider && ( {styleState.showSider && (
<Sider style={{ <Sider
position: 'fixed', style={{
left: 0, position: 'fixed',
top: '56px', left: 0,
zIndex: 99, top: '56px',
background: 'var(--semi-color-bg-1)', zIndex: 99,
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)', background: 'var(--semi-color-bg-1)',
border: 'none', boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)',
paddingRight: '0', border: 'none',
height: 'calc(100vh - 56px)', paddingRight: '0',
}}> height: 'calc(100vh - 56px)',
}}
>
<SiderBar /> <SiderBar />
</Sider> </Sider>
)} )}
<Layout style={{ <Layout
marginLeft: styleState.isMobile ? '0' : (styleState.showSider ? (styleState.siderCollapsed ? '60px' : '200px') : '0'), style={{
transition: 'margin-left 0.3s ease', marginLeft: styleState.isMobile
flex: '1 1 auto', ? '0'
display: 'flex', : styleState.showSider
flexDirection: 'column' ? styleState.siderCollapsed
}}> ? '60px'
: '200px'
: '0',
transition: 'margin-left 0.3s ease',
flex: '1 1 auto',
display: 'flex',
flexDirection: 'column',
}}
>
<Content <Content
style={{ style={{
flex: '1 0 auto', flex: '1 0 auto',
overflowY: styleState.isMobile ? 'visible' : 'auto', overflowY: styleState.isMobile ? 'visible' : 'auto',
WebkitOverflowScrolling: 'touch', WebkitOverflowScrolling: 'touch',
padding: styleState.shouldInnerPadding? '24px': '0', padding: styleState.shouldInnerPadding ? '24px' : '0',
position: 'relative', position: 'relative',
marginTop: styleState.isMobile ? '2px' : '0', marginTop: styleState.isMobile ? '2px' : '0',
}} }}
> >
<App /> <App />
</Content> </Content>
<Layout.Footer style={{ <Layout.Footer
flex: '0 0 auto', style={{
width: '100%' flex: '0 0 auto',
}}> width: '100%',
}}
>
<FooterBar /> <FooterBar />
</Layout.Footer> </Layout.Footer>
</Layout> </Layout>
</Layout> </Layout>
<ToastContainer /> <ToastContainer />
</Layout> </Layout>
) );
} };
export default PageLayout; export default PageLayout;

View File

@@ -6,11 +6,15 @@ import {
isRoot, isRoot,
showError, showError,
showInfo, showInfo,
showSuccess showSuccess,
} from '../helpers'; } from '../helpers';
import Turnstile from 'react-turnstile'; import Turnstile from 'react-turnstile';
import { UserContext } from '../context/User'; import { UserContext } from '../context/User';
import { onGitHubOAuthClicked, onOIDCClicked, onLinuxDOOAuthClicked } from './utils'; import {
onGitHubOAuthClicked,
onOIDCClicked,
onLinuxDOOAuthClicked,
} from './utils';
import { import {
Avatar, Avatar,
Banner, Banner,
@@ -32,13 +36,13 @@ import {
AutoComplete, AutoComplete,
Checkbox, Checkbox,
Tabs, Tabs,
TabPane TabPane,
} from '@douyinfe/semi-ui'; } from '@douyinfe/semi-ui';
import { import {
getQuotaPerUnit, getQuotaPerUnit,
renderQuota, renderQuota,
renderQuotaWithPrompt, renderQuotaWithPrompt,
stringToColor stringToColor,
} from '../helpers/render'; } from '../helpers/render';
import TelegramLoginButton from 'react-telegram-login'; import TelegramLoginButton from 'react-telegram-login';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@@ -54,7 +58,7 @@ const PersonalSetting = () => {
email: '', email: '',
self_account_deletion_confirmation: '', self_account_deletion_confirmation: '',
set_new_password: '', set_new_password: '',
set_new_password_confirmation: '' set_new_password_confirmation: '',
}); });
const [status, setStatus] = useState({}); const [status, setStatus] = useState({});
const [showChangePasswordModal, setShowChangePasswordModal] = useState(false); const [showChangePasswordModal, setShowChangePasswordModal] = useState(false);
@@ -77,14 +81,14 @@ const PersonalSetting = () => {
const savedState = localStorage.getItem('modelsExpanded'); const savedState = localStorage.getItem('modelsExpanded');
return savedState ? JSON.parse(savedState) : false; return savedState ? JSON.parse(savedState) : false;
}); });
const MODELS_DISPLAY_COUNT = 10; // 默认显示的模型数量 const MODELS_DISPLAY_COUNT = 10; // 默认显示的模型数量
const [notificationSettings, setNotificationSettings] = useState({ const [notificationSettings, setNotificationSettings] = useState({
warningType: 'email', warningType: 'email',
warningThreshold: 100000, warningThreshold: 100000,
webhookUrl: '', webhookUrl: '',
webhookSecret: '', webhookSecret: '',
notificationEmail: '', notificationEmail: '',
acceptUnsetModelRatioModel: false acceptUnsetModelRatioModel: false,
}); });
const [showWebhookDocs, setShowWebhookDocs] = useState(false); const [showWebhookDocs, setShowWebhookDocs] = useState(false);
@@ -128,7 +132,8 @@ const PersonalSetting = () => {
webhookUrl: settings.webhook_url || '', webhookUrl: settings.webhook_url || '',
webhookSecret: settings.webhook_secret || '', webhookSecret: settings.webhook_secret || '',
notificationEmail: settings.notification_email || '', notificationEmail: settings.notification_email || '',
acceptUnsetModelRatioModel: settings.accept_unset_model_ratio_model || false acceptUnsetModelRatioModel:
settings.accept_unset_model_ratio_model || false,
}); });
} }
}, [userState?.user?.setting]); }, [userState?.user?.setting]);
@@ -222,7 +227,7 @@ const PersonalSetting = () => {
const bindWeChat = async () => { const bindWeChat = async () => {
if (inputs.wechat_verification_code === '') return; if (inputs.wechat_verification_code === '') return;
const res = await API.get( const res = await API.get(
`/api/oauth/wechat/bind?code=${inputs.wechat_verification_code}` `/api/oauth/wechat/bind?code=${inputs.wechat_verification_code}`,
); );
const { success, message } = res.data; const { success, message } = res.data;
if (success) { if (success) {
@@ -239,7 +244,7 @@ const PersonalSetting = () => {
return; return;
} }
const res = await API.put(`/api/user/self`, { const res = await API.put(`/api/user/self`, {
password: inputs.set_new_password password: inputs.set_new_password,
}); });
const { success, message } = res.data; const { success, message } = res.data;
if (success) { if (success) {
@@ -257,7 +262,7 @@ const PersonalSetting = () => {
return; return;
} }
const res = await API.post(`/api/user/aff_transfer`, { const res = await API.post(`/api/user/aff_transfer`, {
quota: transferAmount quota: transferAmount,
}); });
const { success, message } = res.data; const { success, message } = res.data;
if (success) { if (success) {
@@ -281,7 +286,7 @@ const PersonalSetting = () => {
} }
setLoading(true); setLoading(true);
const res = await API.get( const res = await API.get(
`/api/verification?email=${inputs.email}&turnstile=${turnstileToken}` `/api/verification?email=${inputs.email}&turnstile=${turnstileToken}`,
); );
const { success, message } = res.data; const { success, message } = res.data;
if (success) { if (success) {
@@ -299,7 +304,7 @@ const PersonalSetting = () => {
} }
setLoading(true); setLoading(true);
const res = await API.get( const res = await API.get(
`/api/oauth/email/bind?email=${inputs.email}&code=${inputs.email_verification_code}` `/api/oauth/email/bind?email=${inputs.email}&code=${inputs.email_verification_code}`,
); );
const { success, message } = res.data; const { success, message } = res.data;
if (success) { if (success) {
@@ -334,9 +339,9 @@ const PersonalSetting = () => {
}; };
const handleNotificationSettingChange = (type, value) => { const handleNotificationSettingChange = (type, value) => {
setNotificationSettings(prev => ({ setNotificationSettings((prev) => ({
...prev, ...prev,
[type]: value.target ? value.target.value : value // 处理 Radio 事件对象 [type]: value.target ? value.target.value : value, // 处理 Radio 事件对象
})); }));
}; };
@@ -344,11 +349,14 @@ const PersonalSetting = () => {
try { try {
const res = await API.put('/api/user/setting', { const res = await API.put('/api/user/setting', {
notify_type: notificationSettings.warningType, notify_type: notificationSettings.warningType,
quota_warning_threshold: parseFloat(notificationSettings.warningThreshold), quota_warning_threshold: parseFloat(
notificationSettings.warningThreshold,
),
webhook_url: notificationSettings.webhookUrl, webhook_url: notificationSettings.webhookUrl,
webhook_secret: notificationSettings.webhookSecret, webhook_secret: notificationSettings.webhookSecret,
notification_email: notificationSettings.notificationEmail, notification_email: notificationSettings.notificationEmail,
accept_unset_model_ratio_model: notificationSettings.acceptUnsetModelRatioModel accept_unset_model_ratio_model:
notificationSettings.acceptUnsetModelRatioModel,
}); });
if (res.data.success) { if (res.data.success) {
@@ -363,7 +371,6 @@ const PersonalSetting = () => {
}; };
return ( return (
<div> <div>
<Layout> <Layout>
<Layout.Content> <Layout.Content>
@@ -377,7 +384,10 @@ const PersonalSetting = () => {
centered={true} centered={true}
> >
<div style={{ marginTop: 20 }}> <div style={{ marginTop: 20 }}>
<Typography.Text>{t('可用额度')}{renderQuotaWithPrompt(userState?.user?.aff_quota)}</Typography.Text> <Typography.Text>
{t('可用额度')}
{renderQuotaWithPrompt(userState?.user?.aff_quota)}
</Typography.Text>
<Input <Input
style={{ marginTop: 5 }} style={{ marginTop: 5 }}
value={userState?.user?.aff_quota} value={userState?.user?.aff_quota}
@@ -386,7 +396,9 @@ const PersonalSetting = () => {
</div> </div>
<div style={{ marginTop: 20 }}> <div style={{ marginTop: 20 }}>
<Typography.Text> <Typography.Text>
{t('划转额度')}{renderQuotaWithPrompt(transferAmount)} {t('最低') + renderQuota(getQuotaPerUnit())} {t('划转额度')}
{renderQuotaWithPrompt(transferAmount)}{' '}
{t('最低') + renderQuota(getQuotaPerUnit())}
</Typography.Text> </Typography.Text>
<div> <div>
<InputNumber <InputNumber
@@ -405,7 +417,7 @@ const PersonalSetting = () => {
<Card.Meta <Card.Meta
avatar={ avatar={
<Avatar <Avatar
size="default" size='default'
color={stringToColor(getUsername())} color={stringToColor(getUsername())}
style={{ marginRight: 4 }} style={{ marginRight: 4 }}
> >
@@ -416,25 +428,29 @@ const PersonalSetting = () => {
title={<Typography.Text>{getUsername()}</Typography.Text>} title={<Typography.Text>{getUsername()}</Typography.Text>}
description={ description={
isRoot() ? ( isRoot() ? (
<Tag color="red">{t('管理员')}</Tag> <Tag color='red'>{t('管理员')}</Tag>
) : ( ) : (
<Tag color="blue">{t('普通用户')}</Tag> <Tag color='blue'>{t('普通用户')}</Tag>
) )
} }
></Card.Meta> ></Card.Meta>
} }
headerExtraContent={ headerExtraContent={
<> <>
<Space vertical align="start"> <Space vertical align='start'>
<Tag color="green">{'ID: ' + userState?.user?.id}</Tag> <Tag color='green'>{'ID: ' + userState?.user?.id}</Tag>
<Tag color="blue">{userState?.user?.group}</Tag> <Tag color='blue'>{userState?.user?.group}</Tag>
</Space> </Space>
</> </>
} }
footer={ footer={
<> <>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}> <div
<Typography.Title heading={6}>{t('可用模型')}</Typography.Title> style={{ display: 'flex', alignItems: 'center', gap: 8 }}
>
<Typography.Title heading={6}>
{t('可用模型')}
</Typography.Title>
</div> </div>
<div style={{ marginTop: 10 }}> <div style={{ marginTop: 10 }}>
{models.length <= MODELS_DISPLAY_COUNT ? ( {models.length <= MODELS_DISPLAY_COUNT ? (
@@ -442,7 +458,7 @@ const PersonalSetting = () => {
{models.map((model) => ( {models.map((model) => (
<Tag <Tag
key={model} key={model}
color="cyan" color='cyan'
onClick={() => { onClick={() => {
copyText(model); copyText(model);
}} }}
@@ -458,7 +474,7 @@ const PersonalSetting = () => {
{models.map((model) => ( {models.map((model) => (
<Tag <Tag
key={model} key={model}
color="cyan" color='cyan'
onClick={() => { onClick={() => {
copyText(model); copyText(model);
}} }}
@@ -467,8 +483,8 @@ const PersonalSetting = () => {
</Tag> </Tag>
))} ))}
<Tag <Tag
color="blue" color='blue'
type="light" type='light'
style={{ cursor: 'pointer' }} style={{ cursor: 'pointer' }}
onClick={() => setIsModelsExpanded(false)} onClick={() => setIsModelsExpanded(false)}
> >
@@ -478,24 +494,27 @@ const PersonalSetting = () => {
</Collapsible> </Collapsible>
{!isModelsExpanded && ( {!isModelsExpanded && (
<Space wrap> <Space wrap>
{models.slice(0, MODELS_DISPLAY_COUNT).map((model) => ( {models
<Tag .slice(0, MODELS_DISPLAY_COUNT)
key={model} .map((model) => (
color="cyan" <Tag
onClick={() => { key={model}
copyText(model); color='cyan'
}} onClick={() => {
> copyText(model);
{model} }}
</Tag> >
))} {model}
</Tag>
))}
<Tag <Tag
color="blue" color='blue'
type="light" type='light'
style={{ cursor: 'pointer' }} style={{ cursor: 'pointer' }}
onClick={() => setIsModelsExpanded(true)} onClick={() => setIsModelsExpanded(true)}
> >
{t('更多')} {models.length - MODELS_DISPLAY_COUNT} {t('个模型')} {t('更多')} {models.length - MODELS_DISPLAY_COUNT}{' '}
{t('个模型')}
</Tag> </Tag>
</Space> </Space>
)} )}
@@ -503,7 +522,6 @@ const PersonalSetting = () => {
)} )}
</div> </div>
</> </>
} }
> >
<Descriptions row> <Descriptions row>
@@ -536,9 +554,9 @@ const PersonalSetting = () => {
<div style={{ marginTop: 10 }}> <div style={{ marginTop: 10 }}>
<Descriptions row> <Descriptions row>
<Descriptions.Item itemKey={t('待使用收益')}> <Descriptions.Item itemKey={t('待使用收益')}>
<span style={{ color: 'rgba(var(--semi-red-5), 1)' }}> <span style={{ color: 'rgba(var(--semi-red-5), 1)' }}>
{renderQuota(userState?.user?.aff_quota)} {renderQuota(userState?.user?.aff_quota)}
</span> </span>
<Button <Button
type={'secondary'} type={'secondary'}
onClick={() => setOpenTransfer(true)} onClick={() => setOpenTransfer(true)}
@@ -589,7 +607,9 @@ const PersonalSetting = () => {
</div> </div>
<div style={{ marginTop: 10 }}> <div style={{ marginTop: 10 }}>
<Typography.Text strong>{t('微信')}</Typography.Text> <Typography.Text strong>{t('微信')}</Typography.Text>
<div style={{ display: 'flex', justifyContent: 'space-between' }}> <div
style={{ display: 'flex', justifyContent: 'space-between' }}
>
<div> <div>
<Input <Input
value={ value={
@@ -664,7 +684,10 @@ const PersonalSetting = () => {
<div> <div>
<Button <Button
onClick={() => { onClick={() => {
onOIDCClicked(status.oidc_authorization_endpoint, status.oidc_client_id); onOIDCClicked(
status.oidc_authorization_endpoint,
status.oidc_client_id,
);
}} }}
disabled={ disabled={
(userState.user && userState.user.oidc_id !== '') || (userState.user && userState.user.oidc_id !== '') ||
@@ -697,7 +720,7 @@ const PersonalSetting = () => {
<Button disabled={true}>{t('已绑定')}</Button> <Button disabled={true}>{t('已绑定')}</Button>
) : ( ) : (
<TelegramLoginButton <TelegramLoginButton
dataAuthUrl="/api/oauth/telegram/bind" dataAuthUrl='/api/oauth/telegram/bind'
botName={status.telegram_bot_name} botName={status.telegram_bot_name}
/> />
) )
@@ -779,75 +802,113 @@ const PersonalSetting = () => {
</p> </p>
</div> </div>
<Input <Input
placeholder="验证码" placeholder='验证码'
name="wechat_verification_code" name='wechat_verification_code'
value={inputs.wechat_verification_code} value={inputs.wechat_verification_code}
onChange={(v) => onChange={(v) =>
handleInputChange('wechat_verification_code', v) handleInputChange('wechat_verification_code', v)
} }
/> />
<Button color="" fluid size="large" onClick={bindWeChat}> <Button color='' fluid size='large' onClick={bindWeChat}>
{t('绑定')} {t('绑定')}
</Button> </Button>
</Modal> </Modal>
</div> </div>
</Card> </Card>
<Card style={{ marginTop: 10 }}> <Card style={{ marginTop: 10 }}>
<Tabs type="line" defaultActiveKey="price"> <Tabs type='line' defaultActiveKey='price'>
<TabPane tab={t('价格设置')} itemKey="price"> <TabPane tab={t('价格设置')} itemKey='price'>
<div style={{ marginTop: 20 }}> <div style={{ marginTop: 20 }}>
<Typography.Text strong>{t('接受未设置价格模型')}</Typography.Text> <Typography.Text strong>
{t('接受未设置价格模型')}
</Typography.Text>
<div style={{ marginTop: 10 }}> <div style={{ marginTop: 10 }}>
<Checkbox <Checkbox
checked={notificationSettings.acceptUnsetModelRatioModel} checked={
onChange={e => handleNotificationSettingChange('acceptUnsetModelRatioModel', e.target.checked)} notificationSettings.acceptUnsetModelRatioModel
}
onChange={(e) =>
handleNotificationSettingChange(
'acceptUnsetModelRatioModel',
e.target.checked,
)
}
> >
{t('接受未设置价格模型')} {t('接受未设置价格模型')}
</Checkbox> </Checkbox>
<Typography.Text type="secondary" style={{ marginTop: 8, display: 'block' }}> <Typography.Text
{t('当模型没有设置价格时仍接受调用,仅当您信任该网站时使用,可能会产生高额费用')} type='secondary'
style={{ marginTop: 8, display: 'block' }}
>
{t(
'当模型没有设置价格时仍接受调用,仅当您信任该网站时使用,可能会产生高额费用',
)}
</Typography.Text> </Typography.Text>
</div> </div>
</div> </div>
</TabPane> </TabPane>
<TabPane tab={t('通知设置')} itemKey="notification"> <TabPane tab={t('通知设置')} itemKey='notification'>
<div style={{ marginTop: 20 }}> <div style={{ marginTop: 20 }}>
<Typography.Text strong>{t('通知方式')}</Typography.Text> <Typography.Text strong>{t('通知方式')}</Typography.Text>
<div style={{ marginTop: 10 }}> <div style={{ marginTop: 10 }}>
<RadioGroup <RadioGroup
value={notificationSettings.warningType} value={notificationSettings.warningType}
onChange={value => handleNotificationSettingChange('warningType', value)} onChange={(value) =>
handleNotificationSettingChange('warningType', value)
}
> >
<Radio value="email">{t('邮件通知')}</Radio> <Radio value='email'>{t('邮件通知')}</Radio>
<Radio value="webhook">{t('Webhook通知')}</Radio> <Radio value='webhook'>{t('Webhook通知')}</Radio>
</RadioGroup> </RadioGroup>
</div> </div>
</div> </div>
{notificationSettings.warningType === 'webhook' && ( {notificationSettings.warningType === 'webhook' && (
<> <>
<div style={{ marginTop: 20 }}> <div style={{ marginTop: 20 }}>
<Typography.Text strong>{t('Webhook地址')}</Typography.Text> <Typography.Text strong>
{t('Webhook地址')}
</Typography.Text>
<div style={{ marginTop: 10 }}> <div style={{ marginTop: 10 }}>
<Input <Input
value={notificationSettings.webhookUrl} value={notificationSettings.webhookUrl}
onChange={val => handleNotificationSettingChange('webhookUrl', val)} onChange={(val) =>
placeholder={t('请输入Webhook地址,例如: https://example.com/webhook')} handleNotificationSettingChange('webhookUrl', val)
}
placeholder={t(
'请输入Webhook地址例如: https://example.com/webhook',
)}
/> />
<Typography.Text type="secondary" style={{ marginTop: 8, display: 'block' }}> <Typography.Text
{t('只支持https系统将以 POST 方式发送通知,请确保地址可以接收 POST 请求')} type='secondary'
style={{ marginTop: 8, display: 'block' }}
>
{t(
'只支持https系统将以 POST 方式发送通知,请确保地址可以接收 POST 请求',
)}
</Typography.Text> </Typography.Text>
<Typography.Text type="secondary" style={{ marginTop: 8, display: 'block' }}> <Typography.Text
<div style={{ cursor: 'pointer' }} onClick={() => setShowWebhookDocs(!showWebhookDocs)}> type='secondary'
{t('Webhook请求结构')} {showWebhookDocs ? '▼' : '▶'} style={{ marginTop: 8, display: 'block' }}
>
<div
style={{ cursor: 'pointer' }}
onClick={() =>
setShowWebhookDocs(!showWebhookDocs)
}
>
{t('Webhook请求结构')}{' '}
{showWebhookDocs ? '▼' : '▶'}
</div> </div>
<Collapsible isOpen={showWebhookDocs}> <Collapsible isOpen={showWebhookDocs}>
<pre style={{ <pre
marginTop: 4, style={{
background: 'var(--semi-color-fill-0)', marginTop: 4,
padding: 8, background: 'var(--semi-color-fill-0)',
borderRadius: 4 padding: 8,
}}> borderRadius: 4,
{`{ }}
>
{`{
"type": "quota_exceed", // 通知类型 "type": "quota_exceed", // 通知类型
"title": "标题", // 通知标题 "title": "标题", // 通知标题
"content": "通知内容", // 通知内容,支持 {{value}} 变量占位符 "content": "通知内容", // 通知内容,支持 {{value}} 变量占位符
@@ -863,23 +924,38 @@ const PersonalSetting = () => {
"values": ["$0.99"], "values": ["$0.99"],
"timestamp": 1739950503 "timestamp": 1739950503
}`} }`}
</pre> </pre>
</Collapsible> </Collapsible>
</Typography.Text> </Typography.Text>
</div> </div>
</div> </div>
<div style={{ marginTop: 20 }}> <div style={{ marginTop: 20 }}>
<Typography.Text strong>{t('接口凭证(可选)')}</Typography.Text> <Typography.Text strong>
{t('接口凭证(可选)')}
</Typography.Text>
<div style={{ marginTop: 10 }}> <div style={{ marginTop: 10 }}>
<Input <Input
value={notificationSettings.webhookSecret} value={notificationSettings.webhookSecret}
onChange={val => handleNotificationSettingChange('webhookSecret', val)} onChange={(val) =>
handleNotificationSettingChange(
'webhookSecret',
val,
)
}
placeholder={t('请输入密钥')} placeholder={t('请输入密钥')}
/> />
<Typography.Text type="secondary" style={{ marginTop: 8, display: 'block' }}> <Typography.Text
{t('密钥将以 Bearer 方式添加到请求头中用于验证webhook请求的合法性')} type='secondary'
style={{ marginTop: 8, display: 'block' }}
>
{t(
'密钥将以 Bearer 方式添加到请求头中用于验证webhook请求的合法性',
)}
</Typography.Text> </Typography.Text>
<Typography.Text type="secondary" style={{ marginTop: 4, display: 'block' }}> <Typography.Text
type='secondary'
style={{ marginTop: 4, display: 'block' }}
>
{t('Authorization: Bearer your-secret-key')} {t('Authorization: Bearer your-secret-key')}
</Typography.Text> </Typography.Text>
</div> </div>
@@ -892,40 +968,64 @@ const PersonalSetting = () => {
<div style={{ marginTop: 10 }}> <div style={{ marginTop: 10 }}>
<Input <Input
value={notificationSettings.notificationEmail} value={notificationSettings.notificationEmail}
onChange={val => handleNotificationSettingChange('notificationEmail', val)} onChange={(val) =>
handleNotificationSettingChange(
'notificationEmail',
val,
)
}
placeholder={t('留空则使用账号绑定的邮箱')} placeholder={t('留空则使用账号绑定的邮箱')}
/> />
<Typography.Text type="secondary" style={{ marginTop: 8, display: 'block' }}> <Typography.Text
{t('设置用于接收额度预警的邮箱地址,不填则使用账号绑定的邮箱')} type='secondary'
style={{ marginTop: 8, display: 'block' }}
>
{t(
'设置用于接收额度预警的邮箱地址,不填则使用账号绑定的邮箱',
)}
</Typography.Text> </Typography.Text>
</div> </div>
</div> </div>
)} )}
<div style={{ marginTop: 20 }}> <div style={{ marginTop: 20 }}>
<Typography.Text <Typography.Text strong>
strong>{t('额度预警阈值')} {renderQuotaWithPrompt(notificationSettings.warningThreshold)}</Typography.Text> {t('额度预警阈值')}{' '}
{renderQuotaWithPrompt(
notificationSettings.warningThreshold,
)}
</Typography.Text>
<div style={{ marginTop: 10 }}> <div style={{ marginTop: 10 }}>
<AutoComplete <AutoComplete
value={notificationSettings.warningThreshold} value={notificationSettings.warningThreshold}
onChange={val => handleNotificationSettingChange('warningThreshold', val)} onChange={(val) =>
handleNotificationSettingChange(
'warningThreshold',
val,
)
}
style={{ width: 200 }} style={{ width: 200 }}
placeholder={t('请输入预警额度')} placeholder={t('请输入预警额度')}
data={[ data={[
{ value: 100000, label: '0.2$' }, { value: 100000, label: '0.2$' },
{ value: 500000, label: '1$' }, { value: 500000, label: '1$' },
{ value: 1000000, label: '5$' }, { value: 1000000, label: '5$' },
{ value: 5000000, label: '10$' } { value: 5000000, label: '10$' },
]} ]}
/> />
</div> </div>
<Typography.Text type="secondary" style={{ marginTop: 10, display: 'block' }}> <Typography.Text
{t('当剩余额度低于此数值时,系统将通过选择的方式发送通知')} type='secondary'
style={{ marginTop: 10, display: 'block' }}
>
{t(
'当剩余额度低于此数值时,系统将通过选择的方式发送通知',
)}
</Typography.Text> </Typography.Text>
</div> </div>
</TabPane> </TabPane>
</Tabs> </Tabs>
<div style={{ marginTop: 20 }}> <div style={{ marginTop: 20 }}>
<Button type="primary" onClick={saveNotificationSettings}> <Button type='primary' onClick={saveNotificationSettings}>
{t('保存设置')} {t('保存设置')}
</Button> </Button>
</div> </div>
@@ -938,20 +1038,22 @@ const PersonalSetting = () => {
centered={true} centered={true}
maskClosable={false} maskClosable={false}
> >
<Typography.Title heading={6}>{t('绑定邮箱地址')}</Typography.Title> <Typography.Title heading={6}>
{t('绑定邮箱地址')}
</Typography.Title>
<div <div
style={{ style={{
marginTop: 20, marginTop: 20,
display: 'flex', display: 'flex',
justifyContent: 'space-between' justifyContent: 'space-between',
}} }}
> >
<Input <Input
fluid fluid
placeholder="输入邮箱地址" placeholder='输入邮箱地址'
onChange={(value) => handleInputChange('email', value)} onChange={(value) => handleInputChange('email', value)}
name="email" name='email'
type="email" type='email'
/> />
<Button <Button
onClick={sendVerificationCode} onClick={sendVerificationCode}
@@ -963,8 +1065,8 @@ const PersonalSetting = () => {
<div style={{ marginTop: 10 }}> <div style={{ marginTop: 10 }}>
<Input <Input
fluid fluid
placeholder="验证码" placeholder='验证码'
name="email_verification_code" name='email_verification_code'
value={inputs.email_verification_code} value={inputs.email_verification_code}
onChange={(value) => onChange={(value) =>
handleInputChange('email_verification_code', value) handleInputChange('email_verification_code', value)
@@ -991,20 +1093,20 @@ const PersonalSetting = () => {
> >
<div style={{ marginTop: 20 }}> <div style={{ marginTop: 20 }}>
<Banner <Banner
type="danger" type='danger'
description="您正在删除自己的帐户,将清空所有数据且不可恢复" description='您正在删除自己的帐户,将清空所有数据且不可恢复'
closeIcon={null} closeIcon={null}
/> />
</div> </div>
<div style={{ marginTop: 20 }}> <div style={{ marginTop: 20 }}>
<Input <Input
placeholder={`输入你的账户名 ${userState?.user?.username} 以确认删除`} placeholder={`输入你的账户名 ${userState?.user?.username} 以确认删除`}
name="self_account_deletion_confirmation" name='self_account_deletion_confirmation'
value={inputs.self_account_deletion_confirmation} value={inputs.self_account_deletion_confirmation}
onChange={(value) => onChange={(value) =>
handleInputChange( handleInputChange(
'self_account_deletion_confirmation', 'self_account_deletion_confirmation',
value value,
) )
} }
/> />
@@ -1029,7 +1131,7 @@ const PersonalSetting = () => {
> >
<div style={{ marginTop: 20 }}> <div style={{ marginTop: 20 }}>
<Input <Input
name="set_new_password" name='set_new_password'
placeholder={t('新密码')} placeholder={t('新密码')}
value={inputs.set_new_password} value={inputs.set_new_password}
onChange={(value) => onChange={(value) =>
@@ -1038,7 +1140,7 @@ const PersonalSetting = () => {
/> />
<Input <Input
style={{ marginTop: 20 }} style={{ marginTop: 20 }}
name="set_new_password_confirmation" name='set_new_password_confirmation'
placeholder={t('确认新密码')} placeholder={t('确认新密码')}
value={inputs.set_new_password_confirmation} value={inputs.set_new_password_confirmation}
onChange={(value) => onChange={(value) =>

View File

@@ -1,7 +1,6 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Card, Spin, Tabs } from '@douyinfe/semi-ui'; import { Card, Spin, Tabs } from '@douyinfe/semi-ui';
import { API, showError, showSuccess } from '../helpers'; import { API, showError, showSuccess } from '../helpers';
import SettingsChats from '../pages/Setting/Operation/SettingsChats.js'; import SettingsChats from '../pages/Setting/Operation/SettingsChats.js';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@@ -24,9 +23,7 @@ const RateLimitSetting = () => {
if (success) { if (success) {
let newInputs = {}; let newInputs = {};
data.forEach((item) => { data.forEach((item) => {
if ( if (item.key.endsWith('Enabled')) {
item.key.endsWith('Enabled')
) {
newInputs[item.key] = item.value === 'true' ? true : false; newInputs[item.key] = item.value === 'true' ? true : false;
} else { } else {
newInputs[item.key] = item.value; newInputs[item.key] = item.value;

View File

@@ -10,7 +10,8 @@ import {
import { ITEMS_PER_PAGE } from '../constants'; import { ITEMS_PER_PAGE } from '../constants';
import { renderQuota } from '../helpers/render'; import { renderQuota } from '../helpers/render';
import { import {
Button, Divider, Button,
Divider,
Form, Form,
Modal, Modal,
Popconfirm, Popconfirm,
@@ -193,15 +194,17 @@ const RedemptionsTable = () => {
}; };
const loadRedemptions = async (startIdx, pageSize) => { const loadRedemptions = async (startIdx, pageSize) => {
const res = await API.get(`/api/redemption/?p=${startIdx}&page_size=${pageSize}`); const res = await API.get(
`/api/redemption/?p=${startIdx}&page_size=${pageSize}`,
);
const { success, message, data } = res.data; const { success, message, data } = res.data;
if (success) { if (success) {
const newPageData = data.items; const newPageData = data.items;
setActivePage(data.page); setActivePage(data.page);
setTokenCount(data.total); setTokenCount(data.total);
setRedemptionFormat(newPageData); setRedemptionFormat(newPageData);
} else { } else {
showError(message); showError(message);
} }
setLoading(false); setLoading(false);
}; };
@@ -282,19 +285,21 @@ const RedemptionsTable = () => {
const searchRedemptions = async (keyword, page, pageSize) => { const searchRedemptions = async (keyword, page, pageSize) => {
if (searchKeyword === '') { if (searchKeyword === '') {
await loadRedemptions(page, pageSize); await loadRedemptions(page, pageSize);
return; return;
} }
setSearching(true); setSearching(true);
const res = await API.get(`/api/redemption/search?keyword=${keyword}&p=${page}&page_size=${pageSize}`); const res = await API.get(
`/api/redemption/search?keyword=${keyword}&p=${page}&page_size=${pageSize}`,
);
const { success, message, data } = res.data; const { success, message, data } = res.data;
if (success) { if (success) {
const newPageData = data.items; const newPageData = data.items;
setActivePage(data.page); setActivePage(data.page);
setTokenCount(data.total); setTokenCount(data.total);
setRedemptionFormat(newPageData); setRedemptionFormat(newPageData);
} else { } else {
showError(message); showError(message);
} }
setSearching(false); setSearching(false);
}; };
@@ -355,9 +360,11 @@ const RedemptionsTable = () => {
visiable={showEdit} visiable={showEdit}
handleClose={closeEdit} handleClose={closeEdit}
></EditRedemption> ></EditRedemption>
<Form onSubmit={()=> { <Form
searchRedemptions(searchKeyword, activePage, pageSize).then(); onSubmit={() => {
}}> searchRedemptions(searchKeyword, activePage, pageSize).then();
}}
>
<Form.Input <Form.Input
label={t('搜索关键字')} label={t('搜索关键字')}
field='keyword' field='keyword'
@@ -369,35 +376,36 @@ const RedemptionsTable = () => {
onChange={handleKeywordChange} onChange={handleKeywordChange}
/> />
</Form> </Form>
<Divider style={{margin:'5px 0 15px 0'}}/> <Divider style={{ margin: '5px 0 15px 0' }} />
<div> <div>
<Button <Button
theme='light' theme='light'
type='primary' type='primary'
style={{ marginRight: 8 }} style={{ marginRight: 8 }}
onClick={() => { onClick={() => {
setEditingRedemption({ setEditingRedemption({
id: undefined, id: undefined,
}); });
setShowEdit(true); setShowEdit(true);
}} }}
> >
{t('添加兑换码')} {t('添加兑换码')}
</Button> </Button>
<Button <Button
label={t('复制所选兑换码')} label={t('复制所选兑换码')}
type='warning' type='warning'
onClick={async () => { onClick={async () => {
if (selectedKeys.length === 0) { if (selectedKeys.length === 0) {
showError(t('请至少选择一个兑换码!')); showError(t('请至少选择一个兑换码!'));
return; return;
} }
let keys = ''; let keys = '';
for (let i = 0; i < selectedKeys.length; i++) { for (let i = 0; i < selectedKeys.length; i++) {
keys += selectedKeys[i].name + ' ' + selectedKeys[i].key + '\n'; keys +=
} selectedKeys[i].name + ' ' + selectedKeys[i].key + '\n';
await copyText(keys); }
}} await copyText(keys);
}}
> >
{t('复制所选兑换码到剪贴板')} {t('复制所选兑换码到剪贴板')}
</Button> </Button>
@@ -417,7 +425,7 @@ const RedemptionsTable = () => {
t('第 {{start}} - {{end}} 条,共 {{total}} 条', { t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
start: page.currentStart, start: page.currentStart,
end: page.currentEnd, end: page.currentEnd,
total: tokenCount total: tokenCount,
}), }),
onPageSizeChange: (size) => { onPageSizeChange: (size) => {
setPageSize(size); setPageSize(size);

View File

@@ -1,13 +1,32 @@
import React, { useContext, useEffect, useState } from 'react'; import React, { useContext, useEffect, useState } from 'react';
import { Link, useNavigate } from 'react-router-dom'; import { Link, useNavigate } from 'react-router-dom';
import { API, getLogo, showError, showInfo, showSuccess, updateAPI } from '../helpers'; import {
API,
getLogo,
showError,
showInfo,
showSuccess,
updateAPI,
} from '../helpers';
import Turnstile from 'react-turnstile'; import Turnstile from 'react-turnstile';
import { Button, Card, Divider, Form, Icon, Layout, Modal } from '@douyinfe/semi-ui'; import {
Button,
Card,
Divider,
Form,
Icon,
Layout,
Modal,
} from '@douyinfe/semi-ui';
import Title from '@douyinfe/semi-ui/lib/es/typography/title'; import Title from '@douyinfe/semi-ui/lib/es/typography/title';
import Text from '@douyinfe/semi-ui/lib/es/typography/text'; import Text from '@douyinfe/semi-ui/lib/es/typography/text';
import { IconGithubLogo } from '@douyinfe/semi-icons'; import { IconGithubLogo } from '@douyinfe/semi-icons';
import {onGitHubOAuthClicked, onLinuxDOOAuthClicked, onOIDCClicked} from './utils.js'; import {
import OIDCIcon from "./OIDCIcon.js"; onGitHubOAuthClicked,
onLinuxDOOAuthClicked,
onOIDCClicked,
} from './utils.js';
import OIDCIcon from './OIDCIcon.js';
import LinuxDoIcon from './LinuxDoIcon.js'; import LinuxDoIcon from './LinuxDoIcon.js';
import WeChatIcon from './WeChatIcon.js'; import WeChatIcon from './WeChatIcon.js';
import TelegramLoginButton from 'react-telegram-login/src'; import TelegramLoginButton from 'react-telegram-login/src';
@@ -22,7 +41,7 @@ const RegisterForm = () => {
password: '', password: '',
password2: '', password2: '',
email: '', email: '',
verification_code: '' verification_code: '',
}); });
const { username, password, password2 } = inputs; const { username, password, password2 } = inputs;
const [showEmailVerification, setShowEmailVerification] = useState(false); const [showEmailVerification, setShowEmailVerification] = useState(false);
@@ -54,7 +73,6 @@ const RegisterForm = () => {
} }
}); });
const onWeChatLoginClicked = () => { const onWeChatLoginClicked = () => {
setShowWeChatLoginModal(true); setShowWeChatLoginModal(true);
}; };
@@ -106,7 +124,7 @@ const RegisterForm = () => {
inputs.aff_code = affCode; inputs.aff_code = affCode;
const res = await API.post( const res = await API.post(
`/api/user/register?turnstile=${turnstileToken}`, `/api/user/register?turnstile=${turnstileToken}`,
inputs inputs,
); );
const { success, message } = res.data; const { success, message } = res.data;
if (success) { if (success) {
@@ -127,7 +145,7 @@ const RegisterForm = () => {
} }
setLoading(true); setLoading(true);
const res = await API.get( const res = await API.get(
`/api/verification?email=${inputs.email}&turnstile=${turnstileToken}` `/api/verification?email=${inputs.email}&turnstile=${turnstileToken}`,
); );
const { success, message } = res.data; const { success, message } = res.data;
if (success) { if (success) {
@@ -169,7 +187,6 @@ const RegisterForm = () => {
} }
}; };
return ( return (
<div> <div>
<Layout> <Layout>
@@ -179,7 +196,7 @@ const RegisterForm = () => {
style={{ style={{
justifyContent: 'center', justifyContent: 'center',
display: 'flex', display: 'flex',
marginTop: 120 marginTop: 120,
}} }}
> >
<div style={{ width: 500 }}> <div style={{ width: 500 }}>
@@ -187,28 +204,28 @@ const RegisterForm = () => {
<Title heading={2} style={{ textAlign: 'center' }}> <Title heading={2} style={{ textAlign: 'center' }}>
{t('新用户注册')} {t('新用户注册')}
</Title> </Title>
<Form size="large"> <Form size='large'>
<Form.Input <Form.Input
field={'username'} field={'username'}
label={t('用户名')} label={t('用户名')}
placeholder={t('用户名')} placeholder={t('用户名')}
name="username" name='username'
onChange={(value) => handleChange('username', value)} onChange={(value) => handleChange('username', value)}
/> />
<Form.Input <Form.Input
field={'password'} field={'password'}
label={t('密码')} label={t('密码')}
placeholder={t('输入密码,最短 8 位,最长 20 位')} placeholder={t('输入密码,最短 8 位,最长 20 位')}
name="password" name='password'
type="password" type='password'
onChange={(value) => handleChange('password', value)} onChange={(value) => handleChange('password', value)}
/> />
<Form.Input <Form.Input
field={'password2'} field={'password2'}
label={t('确认密码')} label={t('确认密码')}
placeholder={t('确认密码')} placeholder={t('确认密码')}
name="password2" name='password2'
type="password" type='password'
onChange={(value) => handleChange('password2', value)} onChange={(value) => handleChange('password2', value)}
/> />
{showEmailVerification ? ( {showEmailVerification ? (
@@ -218,10 +235,13 @@ const RegisterForm = () => {
label={t('邮箱')} label={t('邮箱')}
placeholder={t('输入邮箱地址')} placeholder={t('输入邮箱地址')}
onChange={(value) => handleChange('email', value)} onChange={(value) => handleChange('email', value)}
name="email" name='email'
type="email" type='email'
suffix={ suffix={
<Button onClick={sendVerificationCode} disabled={loading}> <Button
onClick={sendVerificationCode}
disabled={loading}
>
{t('获取验证码')} {t('获取验证码')}
</Button> </Button>
} }
@@ -230,8 +250,10 @@ const RegisterForm = () => {
field={'verification_code'} field={'verification_code'}
label={t('验证码')} label={t('验证码')}
placeholder={t('输入验证码')} placeholder={t('输入验证码')}
onChange={(value) => handleChange('verification_code', value)} onChange={(value) =>
name="verification_code" handleChange('verification_code', value)
}
name='verification_code'
/> />
</> </>
) : ( ) : (
@@ -252,14 +274,12 @@ const RegisterForm = () => {
style={{ style={{
display: 'flex', display: 'flex',
justifyContent: 'space-between', justifyContent: 'space-between',
marginTop: 20 marginTop: 20,
}} }}
> >
<Text> <Text>
{t('已有账户?')} {t('已有账户?')}
<Link to="/login"> <Link to='/login'>{t('点击登录')}</Link>
{t('点击登录')}
</Link>
</Text> </Text>
</div> </div>
{status.github_oauth || {status.github_oauth ||
@@ -290,15 +310,18 @@ const RegisterForm = () => {
<></> <></>
)} )}
{status.oidc_enabled ? ( {status.oidc_enabled ? (
<Button <Button
type='primary' type='primary'
icon={<OIDCIcon />} icon={<OIDCIcon />}
onClick={() => onClick={() =>
onOIDCClicked(status.oidc_authorization_endpoint, status.oidc_client_id) onOIDCClicked(
} status.oidc_authorization_endpoint,
/> status.oidc_client_id,
)
}
/>
) : ( ) : (
<></> <></>
)} )}
{status.linuxdo_oauth ? ( {status.linuxdo_oauth ? (
<Button <Button
@@ -365,7 +388,9 @@ const RegisterForm = () => {
</div> </div>
<div style={{ textAlign: 'center' }}> <div style={{ textAlign: 'center' }}>
<p> <p>
{t('微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)')} {t(
'微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)',
)}
</p> </p>
</div> </div>
<Form size='large'> <Form size='large'>

View File

@@ -15,10 +15,13 @@ import {
import '../index.css'; import '../index.css';
import { import {
IconCalendarClock, IconChecklistStroked, IconCalendarClock,
IconComment, IconCommentStroked, IconChecklistStroked,
IconComment,
IconCommentStroked,
IconCreditCard, IconCreditCard,
IconGift, IconHelpCircle, IconGift,
IconHelpCircle,
IconHistogram, IconHistogram,
IconHome, IconHome,
IconImage, IconImage,
@@ -26,9 +29,16 @@ import {
IconLayers, IconLayers,
IconPriceTag, IconPriceTag,
IconSetting, IconSetting,
IconUser IconUser,
} from '@douyinfe/semi-icons'; } from '@douyinfe/semi-icons';
import { Avatar, Dropdown, Layout, Nav, Switch, Divider } from '@douyinfe/semi-ui'; import {
Avatar,
Dropdown,
Layout,
Nav,
Switch,
Divider,
} from '@douyinfe/semi-ui';
import { setStatusData } from '../helpers/data.js'; import { setStatusData } from '../helpers/data.js';
import { stringToColor } from '../helpers/render.js'; import { stringToColor } from '../helpers/render.js';
import { useSetTheme, useTheme } from '../context/Theme/index.js'; import { useSetTheme, useTheme } from '../context/Theme/index.js';
@@ -44,21 +54,23 @@ const navItemStyle = {
// 自定义侧边栏按钮悬停样式 // 自定义侧边栏按钮悬停样式
const navItemHoverStyle = { const navItemHoverStyle = {
backgroundColor: 'var(--semi-color-primary-light-default)', backgroundColor: 'var(--semi-color-primary-light-default)',
color: 'var(--semi-color-primary)' color: 'var(--semi-color-primary)',
}; };
// 自定义侧边栏按钮选中样式 // 自定义侧边栏按钮选中样式
const navItemSelectedStyle = { const navItemSelectedStyle = {
backgroundColor: 'var(--semi-color-primary-light-default)', backgroundColor: 'var(--semi-color-primary-light-default)',
color: 'var(--semi-color-primary)', color: 'var(--semi-color-primary)',
fontWeight: '600' fontWeight: '600',
}; };
// 自定义图标样式 // 自定义图标样式
const iconStyle = (itemKey, selectedKeys) => { const iconStyle = (itemKey, selectedKeys) => {
return { return {
fontSize: '18px', fontSize: '18px',
color: selectedKeys.includes(itemKey) ? 'var(--semi-color-primary)' : 'var(--semi-color-text-2)', color: selectedKeys.includes(itemKey)
? 'var(--semi-color-primary)'
: 'var(--semi-color-text-2)',
}; };
}; };
@@ -99,8 +111,24 @@ const SiderBar = () => {
// 预先计算所有可能的图标样式 // 预先计算所有可能的图标样式
const allItemKeys = useMemo(() => { const allItemKeys = useMemo(() => {
const keys = ['home', 'channel', 'token', 'redemption', 'topup', 'user', 'log', 'midjourney', const keys = [
'setting', 'about', 'chat', 'detail', 'pricing', 'task', 'playground', 'personal']; 'home',
'channel',
'token',
'redemption',
'topup',
'user',
'log',
'midjourney',
'setting',
'about',
'chat',
'detail',
'pricing',
'task',
'playground',
'personal',
];
// 添加聊天项的keys // 添加聊天项的keys
for (let i = 0; i < chatItems.length; i++) { for (let i = 0; i < chatItems.length; i++) {
keys.push('chat' + i); keys.push('chat' + i);
@@ -111,7 +139,7 @@ const SiderBar = () => {
// 使用useMemo一次性计算所有图标样式 // 使用useMemo一次性计算所有图标样式
const iconStyles = useMemo(() => { const iconStyles = useMemo(() => {
const styles = {}; const styles = {};
allItemKeys.forEach(key => { allItemKeys.forEach((key) => {
styles[key] = iconStyle(key, selectedKeys); styles[key] = iconStyle(key, selectedKeys);
}); });
return styles; return styles;
@@ -157,10 +185,8 @@ const SiderBar = () => {
to: '/task', to: '/task',
icon: <IconChecklistStroked />, icon: <IconChecklistStroked />,
className: className:
localStorage.getItem('enable_task') === 'true' localStorage.getItem('enable_task') === 'true' ? '' : 'tableHiddle',
? '' },
: 'tableHiddle',
}
], ],
[ [
localStorage.getItem('enable_data_export'), localStorage.getItem('enable_data_export'),
@@ -276,7 +302,7 @@ const SiderBar = () => {
} }
} catch (e) { } catch (e) {
console.error(e); console.error(e);
showError('聊天数据解析失败') showError('聊天数据解析失败');
} }
} }
}, []); }, []);
@@ -284,7 +310,9 @@ const SiderBar = () => {
// Update the useEffect for route selection // Update the useEffect for route selection
useEffect(() => { useEffect(() => {
const currentPath = location.pathname; const currentPath = location.pathname;
let matchingKey = Object.keys(routerMapState).find(key => routerMapState[key] === currentPath); let matchingKey = Object.keys(routerMapState).find(
(key) => routerMapState[key] === currentPath,
);
// Handle chat routes // Handle chat routes
if (!matchingKey && currentPath.startsWith('/chat/')) { if (!matchingKey && currentPath.startsWith('/chat/')) {
@@ -325,7 +353,7 @@ const SiderBar = () => {
return ( return (
<> <>
<Nav <Nav
className="custom-sidebar-nav" className='custom-sidebar-nav'
style={{ style={{
width: isCollapsed ? '60px' : '200px', width: isCollapsed ? '60px' : '200px',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)', boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)',
@@ -351,7 +379,9 @@ const SiderBar = () => {
// 确保在收起侧边栏时有选中的项目,避免不必要的计算 // 确保在收起侧边栏时有选中的项目,避免不必要的计算
if (selectedKeys.length === 0) { if (selectedKeys.length === 0) {
const currentPath = location.pathname; const currentPath = location.pathname;
const matchingKey = Object.keys(routerMapState).find(key => routerMapState[key] === currentPath); const matchingKey = Object.keys(routerMapState).find(
(key) => routerMapState[key] === currentPath,
);
if (matchingKey) { if (matchingKey) {
setSelectedKeys([matchingKey]); setSelectedKeys([matchingKey]);
@@ -385,7 +415,7 @@ const SiderBar = () => {
// 如果点击的是已经展开的子菜单的父项,则收起子菜单 // 如果点击的是已经展开的子菜单的父项,则收起子菜单
if (openedKeys.includes(key.itemKey)) { if (openedKeys.includes(key.itemKey)) {
setOpenedKeys(openedKeys.filter(k => k !== key.itemKey)); setOpenedKeys(openedKeys.filter((k) => k !== key.itemKey));
} }
setSelectedKeys([key.itemKey]); setSelectedKeys([key.itemKey]);
@@ -403,7 +433,9 @@ const SiderBar = () => {
key={item.itemKey} key={item.itemKey}
itemKey={item.itemKey} itemKey={item.itemKey}
text={item.text} text={item.text}
icon={React.cloneElement(item.icon, { style: iconStyles[item.itemKey] })} icon={React.cloneElement(item.icon, {
style: iconStyles[item.itemKey],
})}
> >
{item.items.map((subItem) => ( {item.items.map((subItem) => (
<Nav.Item <Nav.Item
@@ -420,7 +452,9 @@ const SiderBar = () => {
key={item.itemKey} key={item.itemKey}
itemKey={item.itemKey} itemKey={item.itemKey}
text={item.text} text={item.text}
icon={React.cloneElement(item.icon, { style: iconStyles[item.itemKey] })} icon={React.cloneElement(item.icon, {
style: iconStyles[item.itemKey],
})}
/> />
); );
} }
@@ -436,7 +470,9 @@ const SiderBar = () => {
key={item.itemKey} key={item.itemKey}
itemKey={item.itemKey} itemKey={item.itemKey}
text={item.text} text={item.text}
icon={React.cloneElement(item.icon, { style: iconStyles[item.itemKey] })} icon={React.cloneElement(item.icon, {
style: iconStyles[item.itemKey],
})}
className={item.className} className={item.className}
/> />
))} ))}
@@ -453,7 +489,9 @@ const SiderBar = () => {
key={item.itemKey} key={item.itemKey}
itemKey={item.itemKey} itemKey={item.itemKey}
text={item.text} text={item.text}
icon={React.cloneElement(item.icon, { style: iconStyles[item.itemKey] })} icon={React.cloneElement(item.icon, {
style: iconStyles[item.itemKey],
})}
className={item.className} className={item.className}
/> />
))} ))}
@@ -470,7 +508,9 @@ const SiderBar = () => {
key={item.itemKey} key={item.itemKey}
itemKey={item.itemKey} itemKey={item.itemKey}
text={item.text} text={item.text}
icon={React.cloneElement(item.icon, { style: iconStyles[item.itemKey] })} icon={React.cloneElement(item.icon, {
style: iconStyles[item.itemKey],
})}
className={item.className} className={item.className}
/> />
))} ))}
@@ -480,14 +520,12 @@ const SiderBar = () => {
paddingBottom: styleState?.isMobile ? '112px' : '', paddingBottom: styleState?.isMobile ? '112px' : '',
}} }}
collapseButton={true} collapseButton={true}
collapseText={(collapsed)=> collapseText={(collapsed) => {
{ if (collapsed) {
if(collapsed){ return t('展开侧边栏');
return t('展开侧边栏')
}
return t('收起侧边栏')
} }
} return t('收起侧边栏');
}}
/> />
</Nav> </Nav>
</> </>

View File

@@ -77,7 +77,8 @@ const SystemSetting = () => {
const [isLoaded, setIsLoaded] = useState(false); const [isLoaded, setIsLoaded] = useState(false);
const formApiRef = useRef(null); const formApiRef = useRef(null);
const [emailDomainWhitelist, setEmailDomainWhitelist] = useState([]); const [emailDomainWhitelist, setEmailDomainWhitelist] = useState([]);
const [showPasswordLoginConfirmModal, setShowPasswordLoginConfirmModal] = useState(false); const [showPasswordLoginConfirmModal, setShowPasswordLoginConfirmModal] =
useState(false);
const [linuxDOOAuthEnabled, setLinuxDOOAuthEnabled] = useState(false); const [linuxDOOAuthEnabled, setLinuxDOOAuthEnabled] = useState(false);
const getOptions = async () => { const getOptions = async () => {
@@ -138,18 +139,18 @@ const SystemSetting = () => {
setLoading(true); setLoading(true);
try { try {
// 分离 checkbox 类型的选项和其他选项 // 分离 checkbox 类型的选项和其他选项
const checkboxOptions = options.filter(opt => const checkboxOptions = options.filter((opt) =>
opt.key.toLowerCase().endsWith('enabled') opt.key.toLowerCase().endsWith('enabled'),
); );
const otherOptions = options.filter(opt => const otherOptions = options.filter(
!opt.key.toLowerCase().endsWith('enabled') (opt) => !opt.key.toLowerCase().endsWith('enabled'),
); );
// 处理 checkbox 类型的选项 // 处理 checkbox 类型的选项
for (const opt of checkboxOptions) { for (const opt of checkboxOptions) {
const res = await API.put('/api/option/', { const res = await API.put('/api/option/', {
key: opt.key, key: opt.key,
value: opt.value.toString() value: opt.value.toString(),
}); });
if (!res.data.success) { if (!res.data.success) {
showError(res.data.message); showError(res.data.message);
@@ -159,18 +160,19 @@ const SystemSetting = () => {
// 处理其他选项 // 处理其他选项
if (otherOptions.length > 0) { if (otherOptions.length > 0) {
const requestQueue = otherOptions.map(opt => const requestQueue = otherOptions.map((opt) =>
API.put('/api/option/', { API.put('/api/option/', {
key: opt.key, key: opt.key,
value: typeof opt.value === 'boolean' ? opt.value.toString() : opt.value value:
}) typeof opt.value === 'boolean' ? opt.value.toString() : opt.value,
}),
); );
const results = await Promise.all(requestQueue); const results = await Promise.all(requestQueue);
// 检查所有请求是否成功 // 检查所有请求是否成功
const errorResults = results.filter(res => !res.data.success); const errorResults = results.filter((res) => !res.data.success);
errorResults.forEach(res => { errorResults.forEach((res) => {
showError(res.data.message); showError(res.data.message);
}); });
} }
@@ -178,7 +180,7 @@ const SystemSetting = () => {
showSuccess('更新成功'); showSuccess('更新成功');
// 更新本地状态 // 更新本地状态
const newInputs = { ...inputs }; const newInputs = { ...inputs };
options.forEach(opt => { options.forEach((opt) => {
newInputs[opt.key] = opt.value; newInputs[opt.key] = opt.value;
}); });
setInputs(newInputs); setInputs(newInputs);
@@ -201,7 +203,7 @@ const SystemSetting = () => {
let WorkerUrl = removeTrailingSlash(inputs.WorkerUrl); let WorkerUrl = removeTrailingSlash(inputs.WorkerUrl);
await updateOptions([ await updateOptions([
{ key: 'WorkerUrl', value: WorkerUrl }, { key: 'WorkerUrl', value: WorkerUrl },
{ key: 'WorkerValidKey', value: inputs.WorkerValidKey } { key: 'WorkerValidKey', value: inputs.WorkerValidKey },
]); ]);
}; };
@@ -218,7 +220,7 @@ const SystemSetting = () => {
} }
const options = [ const options = [
{ key: 'PayAddress', value: removeTrailingSlash(inputs.PayAddress) } { key: 'PayAddress', value: removeTrailingSlash(inputs.PayAddress) },
]; ];
if (inputs.EpayId !== '') { if (inputs.EpayId !== '') {
@@ -234,7 +236,10 @@ const SystemSetting = () => {
options.push({ key: 'MinTopUp', value: inputs.MinTopUp.toString() }); options.push({ key: 'MinTopUp', value: inputs.MinTopUp.toString() });
} }
if (inputs.CustomCallbackAddress !== '') { if (inputs.CustomCallbackAddress !== '') {
options.push({ key: 'CustomCallbackAddress', value: inputs.CustomCallbackAddress }); options.push({
key: 'CustomCallbackAddress',
value: inputs.CustomCallbackAddress,
});
} }
if (originInputs['TopupGroupRatio'] !== inputs.TopupGroupRatio) { if (originInputs['TopupGroupRatio'] !== inputs.TopupGroupRatio) {
options.push({ key: 'TopupGroupRatio', value: inputs.TopupGroupRatio }); options.push({ key: 'TopupGroupRatio', value: inputs.TopupGroupRatio });
@@ -255,10 +260,16 @@ const SystemSetting = () => {
if (originInputs['SMTPFrom'] !== inputs.SMTPFrom) { if (originInputs['SMTPFrom'] !== inputs.SMTPFrom) {
options.push({ key: 'SMTPFrom', value: inputs.SMTPFrom }); options.push({ key: 'SMTPFrom', value: inputs.SMTPFrom });
} }
if (originInputs['SMTPPort'] !== inputs.SMTPPort && inputs.SMTPPort !== '') { if (
originInputs['SMTPPort'] !== inputs.SMTPPort &&
inputs.SMTPPort !== ''
) {
options.push({ key: 'SMTPPort', value: inputs.SMTPPort }); options.push({ key: 'SMTPPort', value: inputs.SMTPPort });
} }
if (originInputs['SMTPToken'] !== inputs.SMTPToken && inputs.SMTPToken !== '') { if (
originInputs['SMTPToken'] !== inputs.SMTPToken &&
inputs.SMTPToken !== ''
) {
options.push({ key: 'SMTPToken', value: inputs.SMTPToken }); options.push({ key: 'SMTPToken', value: inputs.SMTPToken });
} }
@@ -269,10 +280,12 @@ const SystemSetting = () => {
const submitEmailDomainWhitelist = async () => { const submitEmailDomainWhitelist = async () => {
if (Array.isArray(emailDomainWhitelist)) { if (Array.isArray(emailDomainWhitelist)) {
await updateOptions([{ await updateOptions([
key: 'EmailDomainWhitelist', {
value: emailDomainWhitelist.join(',') key: 'EmailDomainWhitelist',
}]); value: emailDomainWhitelist.join(','),
},
]);
} else { } else {
showError('邮箱域名白名单格式不正确'); showError('邮箱域名白名单格式不正确');
} }
@@ -284,17 +297,26 @@ const SystemSetting = () => {
if (originInputs['WeChatServerAddress'] !== inputs.WeChatServerAddress) { if (originInputs['WeChatServerAddress'] !== inputs.WeChatServerAddress) {
options.push({ options.push({
key: 'WeChatServerAddress', key: 'WeChatServerAddress',
value: removeTrailingSlash(inputs.WeChatServerAddress) value: removeTrailingSlash(inputs.WeChatServerAddress),
}); });
} }
if (originInputs['WeChatAccountQRCodeImageURL'] !== inputs.WeChatAccountQRCodeImageURL) { if (
originInputs['WeChatAccountQRCodeImageURL'] !==
inputs.WeChatAccountQRCodeImageURL
) {
options.push({ options.push({
key: 'WeChatAccountQRCodeImageURL', key: 'WeChatAccountQRCodeImageURL',
value: inputs.WeChatAccountQRCodeImageURL value: inputs.WeChatAccountQRCodeImageURL,
}); });
} }
if (originInputs['WeChatServerToken'] !== inputs.WeChatServerToken && inputs.WeChatServerToken !== '') { if (
options.push({ key: 'WeChatServerToken', value: inputs.WeChatServerToken }); originInputs['WeChatServerToken'] !== inputs.WeChatServerToken &&
inputs.WeChatServerToken !== ''
) {
options.push({
key: 'WeChatServerToken',
value: inputs.WeChatServerToken,
});
} }
if (options.length > 0) { if (options.length > 0) {
@@ -308,8 +330,14 @@ const SystemSetting = () => {
if (originInputs['GitHubClientId'] !== inputs.GitHubClientId) { if (originInputs['GitHubClientId'] !== inputs.GitHubClientId) {
options.push({ key: 'GitHubClientId', value: inputs.GitHubClientId }); options.push({ key: 'GitHubClientId', value: inputs.GitHubClientId });
} }
if (originInputs['GitHubClientSecret'] !== inputs.GitHubClientSecret && inputs.GitHubClientSecret !== '') { if (
options.push({ key: 'GitHubClientSecret', value: inputs.GitHubClientSecret }); originInputs['GitHubClientSecret'] !== inputs.GitHubClientSecret &&
inputs.GitHubClientSecret !== ''
) {
options.push({
key: 'GitHubClientSecret',
value: inputs.GitHubClientSecret,
});
} }
if (options.length > 0) { if (options.length > 0) {
@@ -319,19 +347,25 @@ const SystemSetting = () => {
const submitOIDCSettings = async () => { const submitOIDCSettings = async () => {
if (inputs['oidc.well_known'] !== '') { if (inputs['oidc.well_known'] !== '') {
if (!inputs['oidc.well_known'].startsWith('http://') && !inputs['oidc.well_known'].startsWith('https://')) { if (
!inputs['oidc.well_known'].startsWith('http://') &&
!inputs['oidc.well_known'].startsWith('https://')
) {
showError('Well-Known URL 必须以 http:// 或 https:// 开头'); showError('Well-Known URL 必须以 http:// 或 https:// 开头');
return; return;
} }
try { try {
const res = await API.get(inputs['oidc.well_known']); const res = await API.get(inputs['oidc.well_known']);
inputs['oidc.authorization_endpoint'] = res.data['authorization_endpoint']; inputs['oidc.authorization_endpoint'] =
res.data['authorization_endpoint'];
inputs['oidc.token_endpoint'] = res.data['token_endpoint']; inputs['oidc.token_endpoint'] = res.data['token_endpoint'];
inputs['oidc.user_info_endpoint'] = res.data['userinfo_endpoint']; inputs['oidc.user_info_endpoint'] = res.data['userinfo_endpoint'];
showSuccess('获取 OIDC 配置成功!'); showSuccess('获取 OIDC 配置成功!');
} catch (err) { } catch (err) {
console.error(err); console.error(err);
showError("获取 OIDC 配置失败,请检查网络状况和 Well-Known URL 是否正确"); showError(
'获取 OIDC 配置失败,请检查网络状况和 Well-Known URL 是否正确',
);
return; return;
} }
} }
@@ -339,22 +373,46 @@ const SystemSetting = () => {
const options = []; const options = [];
if (originInputs['oidc.well_known'] !== inputs['oidc.well_known']) { if (originInputs['oidc.well_known'] !== inputs['oidc.well_known']) {
options.push({ key: 'oidc.well_known', value: inputs['oidc.well_known'] }); options.push({
key: 'oidc.well_known',
value: inputs['oidc.well_known'],
});
} }
if (originInputs['oidc.client_id'] !== inputs['oidc.client_id']) { if (originInputs['oidc.client_id'] !== inputs['oidc.client_id']) {
options.push({ key: 'oidc.client_id', value: inputs['oidc.client_id'] }); options.push({ key: 'oidc.client_id', value: inputs['oidc.client_id'] });
} }
if (originInputs['oidc.client_secret'] !== inputs['oidc.client_secret'] && inputs['oidc.client_secret'] !== '') { if (
options.push({ key: 'oidc.client_secret', value: inputs['oidc.client_secret'] }); originInputs['oidc.client_secret'] !== inputs['oidc.client_secret'] &&
inputs['oidc.client_secret'] !== ''
) {
options.push({
key: 'oidc.client_secret',
value: inputs['oidc.client_secret'],
});
} }
if (originInputs['oidc.authorization_endpoint'] !== inputs['oidc.authorization_endpoint']) { if (
options.push({ key: 'oidc.authorization_endpoint', value: inputs['oidc.authorization_endpoint'] }); originInputs['oidc.authorization_endpoint'] !==
inputs['oidc.authorization_endpoint']
) {
options.push({
key: 'oidc.authorization_endpoint',
value: inputs['oidc.authorization_endpoint'],
});
} }
if (originInputs['oidc.token_endpoint'] !== inputs['oidc.token_endpoint']) { if (originInputs['oidc.token_endpoint'] !== inputs['oidc.token_endpoint']) {
options.push({ key: 'oidc.token_endpoint', value: inputs['oidc.token_endpoint'] }); options.push({
key: 'oidc.token_endpoint',
value: inputs['oidc.token_endpoint'],
});
} }
if (originInputs['oidc.user_info_endpoint'] !== inputs['oidc.user_info_endpoint']) { if (
options.push({ key: 'oidc.user_info_endpoint', value: inputs['oidc.user_info_endpoint'] }); originInputs['oidc.user_info_endpoint'] !==
inputs['oidc.user_info_endpoint']
) {
options.push({
key: 'oidc.user_info_endpoint',
value: inputs['oidc.user_info_endpoint'],
});
} }
if (options.length > 0) { if (options.length > 0) {
@@ -365,7 +423,7 @@ const SystemSetting = () => {
const submitTelegramSettings = async () => { const submitTelegramSettings = async () => {
const options = [ const options = [
{ key: 'TelegramBotToken', value: inputs.TelegramBotToken }, { key: 'TelegramBotToken', value: inputs.TelegramBotToken },
{ key: 'TelegramBotName', value: inputs.TelegramBotName } { key: 'TelegramBotName', value: inputs.TelegramBotName },
]; ];
await updateOptions(options); await updateOptions(options);
}; };
@@ -376,8 +434,14 @@ const SystemSetting = () => {
if (originInputs['TurnstileSiteKey'] !== inputs.TurnstileSiteKey) { if (originInputs['TurnstileSiteKey'] !== inputs.TurnstileSiteKey) {
options.push({ key: 'TurnstileSiteKey', value: inputs.TurnstileSiteKey }); options.push({ key: 'TurnstileSiteKey', value: inputs.TurnstileSiteKey });
} }
if (originInputs['TurnstileSecretKey'] !== inputs.TurnstileSecretKey && inputs.TurnstileSecretKey !== '') { if (
options.push({ key: 'TurnstileSecretKey', value: inputs.TurnstileSecretKey }); originInputs['TurnstileSecretKey'] !== inputs.TurnstileSecretKey &&
inputs.TurnstileSecretKey !== ''
) {
options.push({
key: 'TurnstileSecretKey',
value: inputs.TurnstileSecretKey,
});
} }
if (options.length > 0) { if (options.length > 0) {
@@ -391,8 +455,14 @@ const SystemSetting = () => {
if (originInputs['LinuxDOClientId'] !== inputs.LinuxDOClientId) { if (originInputs['LinuxDOClientId'] !== inputs.LinuxDOClientId) {
options.push({ key: 'LinuxDOClientId', value: inputs.LinuxDOClientId }); options.push({ key: 'LinuxDOClientId', value: inputs.LinuxDOClientId });
} }
if (originInputs['LinuxDOClientSecret'] !== inputs.LinuxDOClientSecret && inputs.LinuxDOClientSecret !== '') { if (
options.push({ key: 'LinuxDOClientSecret', value: inputs.LinuxDOClientSecret }); originInputs['LinuxDOClientSecret'] !== inputs.LinuxDOClientSecret &&
inputs.LinuxDOClientSecret !== ''
) {
options.push({
key: 'LinuxDOClientSecret',
value: inputs.LinuxDOClientSecret,
});
} }
if (options.length > 0) { if (options.length > 0) {
@@ -450,7 +520,9 @@ const SystemSetting = () => {
</a> </a>
</Text> </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={12} lg={12} xl={12}> <Col xs={24} sm={24} md={12} lg={12} xl={12}>
<Form.Input <Form.Input
field='WorkerUrl' field='WorkerUrl'
@@ -474,7 +546,9 @@ const SystemSetting = () => {
<Text> <Text>
当前仅支持易支付接口默认使用上方服务器地址作为回调地址 当前仅支持易支付接口默认使用上方服务器地址作为回调地址
</Text> </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}> <Col xs={24} sm={24} md={8} lg={8} xl={8}>
<Form.Input <Form.Input
field='PayAddress' field='PayAddress'
@@ -535,7 +609,9 @@ const SystemSetting = () => {
</Form.Section> </Form.Section>
<Form.Section text='配置登录注册'> <Form.Section 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={12} lg={12} xl={12}> <Col xs={24} sm={24} md={12} lg={12} xl={12}>
<Form.Checkbox <Form.Checkbox
field='PasswordLoginEnabled' field='PasswordLoginEnabled'
@@ -567,7 +643,9 @@ const SystemSetting = () => {
<Form.Checkbox <Form.Checkbox
field='RegisterEnabled' field='RegisterEnabled'
noLabel noLabel
onChange={(e) => handleCheckboxChange('RegisterEnabled', e)} onChange={(e) =>
handleCheckboxChange('RegisterEnabled', e)
}
> >
允许新用户注册 允许新用户注册
</Form.Checkbox> </Form.Checkbox>
@@ -631,7 +709,9 @@ const SystemSetting = () => {
<Form.Section text='配置邮箱域名白名单'> <Form.Section text='配置邮箱域名白名单'>
<Text>用以防止恶意用户利用临时邮箱批量注册</Text> <Text>用以防止恶意用户利用临时邮箱批量注册</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={12} lg={12} xl={12}> <Col xs={24} sm={24} md={12} lg={12} xl={12}>
<Form.Checkbox <Form.Checkbox
field='EmailDomainRestrictionEnabled' field='EmailDomainRestrictionEnabled'
@@ -671,7 +751,9 @@ const SystemSetting = () => {
<Form.Section text='配置 SMTP'> <Form.Section text='配置 SMTP'>
<Text>用以支持系统的邮件发送</Text> <Text>用以支持系统的邮件发送</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}> <Col xs={24} sm={24} md={8} lg={8} xl={8}>
<Form.Input field='SMTPServer' label='SMTP 服务器地址' /> <Form.Input field='SMTPServer' label='SMTP 服务器地址' />
</Col> </Col>
@@ -701,7 +783,9 @@ const SystemSetting = () => {
<Form.Checkbox <Form.Checkbox
field='SMTPSSLEnabled' field='SMTPSSLEnabled'
noLabel noLabel
onChange={(e) => handleCheckboxChange('SMTPSSLEnabled', e)} onChange={(e) =>
handleCheckboxChange('SMTPSSLEnabled', e)
}
> >
启用SMTP SSL 启用SMTP SSL
</Form.Checkbox> </Form.Checkbox>
@@ -711,14 +795,22 @@ const SystemSetting = () => {
</Form.Section> </Form.Section>
<Form.Section text='配置 OIDC'> <Form.Section text='配置 OIDC'>
<Text>用以支持通过 OIDC 登录例如 OktaAuth0 等兼容 OIDC 协议的 IdP</Text> <Text>
用以支持通过 OIDC 登录例如 OktaAuth0 等兼容 OIDC 协议的
IdP
</Text>
<Banner <Banner
type='info' type='info'
description={`主页链接填 ${inputs.ServerAddress ? inputs.ServerAddress : '网站地址'},重定向 URL 填 ${inputs.ServerAddress ? inputs.ServerAddress : '网站地址'}/oauth/oidc`} description={`主页链接填 ${inputs.ServerAddress ? inputs.ServerAddress : '网站地址'},重定向 URL 填 ${inputs.ServerAddress ? inputs.ServerAddress : '网站地址'}/oauth/oidc`}
style={{ marginBottom: 20, marginTop: 16 }} style={{ marginBottom: 20, marginTop: 16 }}
/> />
<Text>若你的 OIDC Provider 支持 Discovery Endpoint你可以仅填写 OIDC Well-Known URL系统会自动获取 OIDC 配置</Text> <Text>
<Row gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}> 若你的 OIDC Provider 支持 Discovery Endpoint你可以仅填写
OIDC Well-Known URL系统会自动获取 OIDC 配置
</Text>
<Row
gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
>
<Col xs={24} sm={24} md={12} lg={12} xl={12}> <Col xs={24} sm={24} md={12} lg={12} xl={12}>
<Form.Input <Form.Input
field='oidc.well_known' field='oidc.well_known'
@@ -734,7 +826,9 @@ const SystemSetting = () => {
/> />
</Col> </Col>
</Row> </Row>
<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={12} lg={12} xl={12}> <Col xs={24} sm={24} md={12} lg={12} xl={12}>
<Form.Input <Form.Input
field='oidc.client_secret' field='oidc.client_secret'
@@ -751,7 +845,9 @@ const SystemSetting = () => {
/> />
</Col> </Col>
</Row> </Row>
<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={12} lg={12} xl={12}> <Col xs={24} sm={24} md={12} lg={12} xl={12}>
<Form.Input <Form.Input
field='oidc.token_endpoint' field='oidc.token_endpoint'
@@ -777,9 +873,14 @@ const SystemSetting = () => {
description={`Homepage URL 填 ${inputs.ServerAddress ? inputs.ServerAddress : '网站地址'}Authorization callback URL 填 ${inputs.ServerAddress ? inputs.ServerAddress : '网站地址'}/oauth/github`} description={`Homepage URL 填 ${inputs.ServerAddress ? inputs.ServerAddress : '网站地址'}Authorization callback URL 填 ${inputs.ServerAddress ? inputs.ServerAddress : '网站地址'}/oauth/github`}
style={{ marginBottom: 20, marginTop: 16 }} style={{ marginBottom: 20, marginTop: 16 }}
/> />
<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={12} lg={12} xl={12}> <Col xs={24} sm={24} md={12} lg={12} xl={12}>
<Form.Input field='GitHubClientId' label='GitHub Client ID' /> <Form.Input
field='GitHubClientId'
label='GitHub Client ID'
/>
</Col> </Col>
<Col xs={24} sm={24} md={12} lg={12} xl={12}> <Col xs={24} sm={24} md={12} lg={12} xl={12}>
<Form.Input <Form.Input
@@ -801,7 +902,11 @@ const SystemSetting = () => {
href='https://connect.linux.do/' href='https://connect.linux.do/'
target='_blank' target='_blank'
rel='noreferrer' rel='noreferrer'
style={{ display: 'inline-block', marginLeft: 4, marginRight: 4 }} style={{
display: 'inline-block',
marginLeft: 4,
marginRight: 4,
}}
> >
点击此处 点击此处
</a> </a>
@@ -812,7 +917,9 @@ const SystemSetting = () => {
description={`回调 URL 填 ${inputs.ServerAddress ? inputs.ServerAddress : '网站地址'}/oauth/linuxdo`} description={`回调 URL 填 ${inputs.ServerAddress ? inputs.ServerAddress : '网站地址'}/oauth/linuxdo`}
style={{ marginBottom: 20, marginTop: 16 }} style={{ marginBottom: 20, marginTop: 16 }}
/> />
<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={12} lg={12} xl={12}> <Col xs={24} sm={24} md={12} lg={12} xl={12}>
<Form.Input <Form.Input
field='LinuxDOClientId' field='LinuxDOClientId'
@@ -835,7 +942,9 @@ const SystemSetting = () => {
</Form.Section> </Form.Section>
<Form.Section text='配置 WeChat Server'> <Form.Section text='配置 WeChat Server'>
<Text>用以支持通过微信进行登录注册</Text> <Text>用以支持通过微信进行登录注册</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}> <Col xs={24} sm={24} md={8} lg={8} xl={8}>
<Form.Input <Form.Input
field='WeChatServerAddress' field='WeChatServerAddress'
@@ -861,7 +970,9 @@ const SystemSetting = () => {
</Form.Section> </Form.Section>
<Form.Section text='配置 Telegram 登录'> <Form.Section text='配置 Telegram 登录'>
<Text>用以支持通过 Telegram 进行登录注册</Text> <Text>用以支持通过 Telegram 进行登录注册</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={12} lg={12} xl={12}> <Col xs={24} sm={24} md={12} lg={12} xl={12}>
<Form.Input <Form.Input
field='TelegramBotToken' field='TelegramBotToken'
@@ -883,7 +994,9 @@ const SystemSetting = () => {
</Form.Section> </Form.Section>
<Form.Section text='配置 Turnstile'> <Form.Section text='配置 Turnstile'>
<Text>用以支持用户校验</Text> <Text>用以支持用户校验</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={12} lg={12} xl={12}> <Col xs={24} sm={24} md={12} lg={12} xl={12}>
<Form.Input <Form.Input
field='TurnstileSiteKey' field='TurnstileSiteKey'
@@ -903,15 +1016,15 @@ const SystemSetting = () => {
</Form.Section> </Form.Section>
<Modal <Modal
title="确认取消密码登录" title='确认取消密码登录'
visible={showPasswordLoginConfirmModal} visible={showPasswordLoginConfirmModal}
onOk={handlePasswordLoginConfirm} onOk={handlePasswordLoginConfirm}
onCancel={() => { onCancel={() => {
setShowPasswordLoginConfirmModal(false); setShowPasswordLoginConfirmModal(false);
formApiRef.current.setValue('PasswordLoginEnabled', true); formApiRef.current.setValue('PasswordLoginEnabled', true);
}} }}
okText="确认" okText='确认'
cancelText="取消" cancelText='取消'
> >
<p>您确定要取消密码登录功能吗这可能会影响用户的登录方式</p> <p>您确定要取消密码登录功能吗这可能会影响用户的登录方式</p>
</Modal> </Modal>
@@ -919,8 +1032,15 @@ const SystemSetting = () => {
)} )}
</Form> </Form>
) : ( ) : (
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100vh' }}> <div
<Spin size="large" /> style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '100vh',
}}
>
<Spin size='large' />
</div> </div>
)} )}
</div> </div>

View File

@@ -1,400 +1,512 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Label } from 'semantic-ui-react'; import { Label } from 'semantic-ui-react';
import { API, copy, isAdmin, showError, showSuccess, timestamp2string } from '../helpers'; import {
API,
copy,
isAdmin,
showError,
showSuccess,
timestamp2string,
} from '../helpers';
import { import {
Table, Table,
Tag, Tag,
Form, Form,
Button, Button,
Layout, Layout,
Modal, Modal,
Typography, Progress, Card Typography,
Progress,
Card,
} from '@douyinfe/semi-ui'; } from '@douyinfe/semi-ui';
import { ITEMS_PER_PAGE } from '../constants'; import { ITEMS_PER_PAGE } from '../constants';
const colors = ['amber', 'blue', 'cyan', 'green', 'grey', 'indigo', const colors = [
'light-blue', 'lime', 'orange', 'pink', 'amber',
'purple', 'red', 'teal', 'violet', 'yellow' 'blue',
] 'cyan',
'green',
'grey',
'indigo',
'light-blue',
'lime',
'orange',
'pink',
'purple',
'red',
'teal',
'violet',
'yellow',
];
const renderTimestamp = (timestampInSeconds) => { const renderTimestamp = (timestampInSeconds) => {
const date = new Date(timestampInSeconds * 1000); // 从秒转换为毫秒 const date = new Date(timestampInSeconds * 1000); // 从秒转换为毫秒
const year = date.getFullYear(); // 获取年份 const year = date.getFullYear(); // 获取年份
const month = ('0' + (date.getMonth() + 1)).slice(-2); // 获取月份从0开始需要+1并保证两位数 const month = ('0' + (date.getMonth() + 1)).slice(-2); // 获取月份从0开始需要+1并保证两位数
const day = ('0' + date.getDate()).slice(-2); // 获取日期,并保证两位数 const day = ('0' + date.getDate()).slice(-2); // 获取日期,并保证两位数
const hours = ('0' + date.getHours()).slice(-2); // 获取小时,并保证两位数 const hours = ('0' + date.getHours()).slice(-2); // 获取小时,并保证两位数
const minutes = ('0' + date.getMinutes()).slice(-2); // 获取分钟,并保证两位数 const minutes = ('0' + date.getMinutes()).slice(-2); // 获取分钟,并保证两位数
const seconds = ('0' + date.getSeconds()).slice(-2); // 获取秒钟,并保证两位数 const seconds = ('0' + date.getSeconds()).slice(-2); // 获取秒钟,并保证两位数
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; // 格式化输出 return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; // 格式化输出
}; };
function renderDuration(submit_time, finishTime) { function renderDuration(submit_time, finishTime) {
// 确保startTime和finishTime都是有效的时间戳 // 确保startTime和finishTime都是有效的时间戳
if (!submit_time || !finishTime) return 'N/A'; if (!submit_time || !finishTime) return 'N/A';
// 将时间戳转换为Date对象 // 将时间戳转换为Date对象
const start = new Date(submit_time); const start = new Date(submit_time);
const finish = new Date(finishTime); const finish = new Date(finishTime);
// 计算时间差(毫秒) // 计算时间差(毫秒)
const durationMs = finish - start; const durationMs = finish - start;
// 将时间差转换为秒,并保留一位小数 // 将时间差转换为秒,并保留一位小数
const durationSec = (durationMs / 1000).toFixed(1); const durationSec = (durationMs / 1000).toFixed(1);
// 设置颜色大于60秒则为红色小于等于60秒则为绿色 // 设置颜色大于60秒则为红色小于等于60秒则为绿色
const color = durationSec > 60 ? 'red' : 'green'; const color = durationSec > 60 ? 'red' : 'green';
// 返回带有样式的颜色标签 // 返回带有样式的颜色标签
return ( return (
<Tag color={color} size="large"> <Tag color={color} size='large'>
{durationSec} {durationSec}
</Tag> </Tag>
); );
} }
const LogsTable = () => { const LogsTable = () => {
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
const [modalContent, setModalContent] = useState(''); const [modalContent, setModalContent] = useState('');
const isAdminUser = isAdmin(); const isAdminUser = isAdmin();
const columns = [ const columns = [
{ {
title: "提交时间", title: '提交时间',
dataIndex: 'submit_time', dataIndex: 'submit_time',
render: (text, record, index) => { render: (text, record, index) => {
return ( return <div>{text ? renderTimestamp(text) : '-'}</div>;
<div> },
{text ? renderTimestamp(text) : "-"} },
</div> {
); title: '结束时间',
}, dataIndex: 'finish_time',
}, render: (text, record, index) => {
{ return <div>{text ? renderTimestamp(text) : '-'}</div>;
title: "结束时间", },
dataIndex: 'finish_time', },
render: (text, record, index) => { {
return ( title: '进度',
<div> dataIndex: 'progress',
{text ? renderTimestamp(text) : "-"} width: 50,
</div> render: (text, record, index) => {
); return (
}, <div>
}, {
{ // 转换例如100%为数字100如果text未定义返回0
title: '进度', isNaN(text.replace('%', '')) ? (
dataIndex: 'progress', text
width: 50, ) : (
render: (text, record, index) => { <Progress
return ( width={42}
<div> type='circle'
{ showInfo={true}
// 转换例如100%为数字100如果text未定义返回0 percent={Number(text.replace('%', '') || 0)}
isNaN(text.replace('%', '')) ? text : <Progress width={42} type="circle" showInfo={true} percent={Number(text.replace('%', '') || 0)} aria-label="drawing progress" /> aria-label='drawing progress'
} />
</div> )
);
},
},
{
title: '花费时间',
dataIndex: 'finish_time', // 以finish_time作为dataIndex
key: 'finish_time',
render: (finish, record) => {
// 假设record.start_time是存在的并且finish是完成时间的时间戳
return <>
{
finish ? renderDuration(record.submit_time, finish) : "-"
}
</>
},
},
{
title: "渠道",
dataIndex: 'channel_id',
className: isAdminUser ? 'tableShow' : 'tableHiddle',
render: (text, record, index) => {
return (
<div>
<Tag
color={colors[parseInt(text) % colors.length]}
size='large'
onClick={() => {
copyText(text); // 假设copyText是用于文本复制的函数
}}
>
{' '}
{text}{' '}
</Tag>
</div>
);
},
},
{
title: "平台",
dataIndex: 'platform',
render: (text, record, index) => {
return (
<div>
{renderPlatform(text)}
</div>
);
},
},
{
title: '类型',
dataIndex: 'action',
render: (text, record, index) => {
return (
<div>
{renderType(text)}
</div>
);
},
},
{
title: '任务ID点击查看详情',
dataIndex: 'task_id',
render: (text, record, index) => {
return (<Typography.Text
ellipsis={{ showTooltip: true }}
//style={{width: 100}}
onClick={() => {
setModalContent(JSON.stringify(record, null, 2));
setIsModalOpen(true);
}}
>
<div>
{text}
</div>
</Typography.Text>);
},
},
{
title: '任务状态',
dataIndex: 'status',
render: (text, record, index) => {
return (
<div>
{renderStatus(text)}
</div>
);
},
},
{
title: '失败原因',
dataIndex: 'fail_reason',
render: (text, record, index) => {
// 如果text未定义返回替代文本例如空字符串''或其他
if (!text) {
return '无';
}
return (
<Typography.Text
ellipsis={{ showTooltip: true }}
style={{ width: 100 }}
onClick={() => {
setModalContent(text);
setIsModalOpen(true);
}}
>
{text}
</Typography.Text>
);
} }
</div>
);
},
},
{
title: '花费时间',
dataIndex: 'finish_time', // 以finish_time作为dataIndex
key: 'finish_time',
render: (finish, record) => {
// 假设record.start_time是存在的并且finish是完成时间的时间戳
return <>{finish ? renderDuration(record.submit_time, finish) : '-'}</>;
},
},
{
title: '渠道',
dataIndex: 'channel_id',
className: isAdminUser ? 'tableShow' : 'tableHiddle',
render: (text, record, index) => {
return (
<div>
<Tag
color={colors[parseInt(text) % colors.length]}
size='large'
onClick={() => {
copyText(text); // 假设copyText是用于文本复制的函数
}}
>
{' '}
{text}{' '}
</Tag>
</div>
);
},
},
{
title: '平台',
dataIndex: 'platform',
render: (text, record, index) => {
return <div>{renderPlatform(text)}</div>;
},
},
{
title: '类型',
dataIndex: 'action',
render: (text, record, index) => {
return <div>{renderType(text)}</div>;
},
},
{
title: '任务ID点击查看详情',
dataIndex: 'task_id',
render: (text, record, index) => {
return (
<Typography.Text
ellipsis={{ showTooltip: true }}
//style={{width: 100}}
onClick={() => {
setModalContent(JSON.stringify(record, null, 2));
setIsModalOpen(true);
}}
>
<div>{text}</div>
</Typography.Text>
);
},
},
{
title: '任务状态',
dataIndex: 'status',
render: (text, record, index) => {
return <div>{renderStatus(text)}</div>;
},
},
{
title: '失败原因',
dataIndex: 'fail_reason',
render: (text, record, index) => {
// 如果text未定义返回替代文本例如空字符串''或其他
if (!text) {
return '无';
} }
];
const [logs, setLogs] = useState([]); return (
const [loading, setLoading] = useState(true); <Typography.Text
const [activePage, setActivePage] = useState(1); ellipsis={{ showTooltip: true }}
const [logCount, setLogCount] = useState(ITEMS_PER_PAGE); style={{ width: 100 }}
const [logType] = useState(0); onClick={() => {
setModalContent(text);
setIsModalOpen(true);
}}
>
{text}
</Typography.Text>
);
},
},
];
let now = new Date(); const [logs, setLogs] = useState([]);
// 初始化start_timestamp为前一天 const [loading, setLoading] = useState(true);
let zeroNow = new Date(now.getFullYear(), now.getMonth(), now.getDate()); const [activePage, setActivePage] = useState(1);
const [inputs, setInputs] = useState({ const [logCount, setLogCount] = useState(ITEMS_PER_PAGE);
channel_id: '', const [logType] = useState(0);
task_id: '',
start_timestamp: timestamp2string(zeroNow.getTime() /1000),
end_timestamp: '',
});
const { channel_id, task_id, start_timestamp, end_timestamp } = inputs;
const handleInputChange = (value, name) => { let now = new Date();
setInputs((inputs) => ({ ...inputs, [name]: value })); // 初始化start_timestamp为前一天
}; let zeroNow = new Date(now.getFullYear(), now.getMonth(), now.getDate());
const [inputs, setInputs] = useState({
channel_id: '',
task_id: '',
start_timestamp: timestamp2string(zeroNow.getTime() / 1000),
end_timestamp: '',
});
const { channel_id, task_id, start_timestamp, end_timestamp } = inputs;
const handleInputChange = (value, name) => {
setInputs((inputs) => ({ ...inputs, [name]: value }));
};
const setLogsFormat = (logs) => { const setLogsFormat = (logs) => {
for (let i = 0; i < logs.length; i++) { for (let i = 0; i < logs.length; i++) {
logs[i].timestamp2string = timestamp2string(logs[i].created_at); logs[i].timestamp2string = timestamp2string(logs[i].created_at);
logs[i].key = '' + logs[i].id; logs[i].key = '' + logs[i].id;
}
// data.key = '' + data.id
setLogs(logs);
setLogCount(logs.length + ITEMS_PER_PAGE);
// console.log(logCount);
} }
// data.key = '' + data.id
setLogs(logs);
setLogCount(logs.length + ITEMS_PER_PAGE);
// console.log(logCount);
};
const loadLogs = async (startIdx) => { const loadLogs = async (startIdx) => {
setLoading(true); setLoading(true);
let url = ''; let url = '';
let localStartTimestamp = parseInt(Date.parse(start_timestamp) / 1000); let localStartTimestamp = parseInt(Date.parse(start_timestamp) / 1000);
let localEndTimestamp = parseInt(Date.parse(end_timestamp) / 1000 ); let localEndTimestamp = parseInt(Date.parse(end_timestamp) / 1000);
if (isAdminUser) { if (isAdminUser) {
url = `/api/task/?p=${startIdx}&channel_id=${channel_id}&task_id=${task_id}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`; url = `/api/task/?p=${startIdx}&channel_id=${channel_id}&task_id=${task_id}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`;
} else { } else {
url = `/api/task/self?p=${startIdx}&task_id=${task_id}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`; url = `/api/task/self?p=${startIdx}&task_id=${task_id}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`;
}
const res = await API.get(url);
let { success, message, data } = res.data;
if (success) {
if (startIdx === 0) {
setLogsFormat(data);
} else {
let newLogs = [...logs];
newLogs.splice(startIdx * ITEMS_PER_PAGE, data.length, ...data);
setLogsFormat(newLogs);
}
} else {
showError(message);
}
setLoading(false);
};
const pageData = logs.slice((activePage - 1) * ITEMS_PER_PAGE, activePage * ITEMS_PER_PAGE);
const handlePageChange = page => {
setActivePage(page);
if (page === Math.ceil(logs.length / ITEMS_PER_PAGE) + 1) {
// In this case we have to load more data and then append them.
loadLogs(page - 1).then(r => {
});
}
};
const refresh = async () => {
// setLoading(true);
setActivePage(1);
await loadLogs(0);
};
const copyText = async (text) => {
if (await copy(text)) {
showSuccess('已复制:' + text);
} else {
// setSearchKeyword(text);
Modal.error({ title: "无法复制到剪贴板,请手动复制", content: text });
}
} }
const res = await API.get(url);
useEffect(() => { let { success, message, data } = res.data;
refresh().then(); if (success) {
}, [logType]); if (startIdx === 0) {
setLogsFormat(data);
const renderType = (type) => { } else {
switch (type) { let newLogs = [...logs];
case 'MUSIC': newLogs.splice(startIdx * ITEMS_PER_PAGE, data.length, ...data);
return <Label basic color='grey'> 生成音乐 </Label>; setLogsFormat(newLogs);
case 'LYRICS': }
return <Label basic color='pink'> 生成歌词 </Label>; } else {
showError(message);
default:
return <Label basic color='black'> 未知 </Label>;
}
} }
setLoading(false);
};
const renderPlatform = (type) => { const pageData = logs.slice(
switch (type) { (activePage - 1) * ITEMS_PER_PAGE,
case "suno": activePage * ITEMS_PER_PAGE,
return <Label basic color='green'> Suno </Label>; );
default:
return <Label basic color='black'> 未知 </Label>; const handlePageChange = (page) => {
} setActivePage(page);
if (page === Math.ceil(logs.length / ITEMS_PER_PAGE) + 1) {
// In this case we have to load more data and then append them.
loadLogs(page - 1).then((r) => {});
} }
};
const renderStatus = (type) => { const refresh = async () => {
switch (type) { // setLoading(true);
case 'SUCCESS': setActivePage(1);
return <Label basic color='green'> 成功 </Label>; await loadLogs(0);
case 'NOT_START': };
return <Label basic color='black'> 未启动 </Label>;
case 'SUBMITTED': const copyText = async (text) => {
return <Label basic color='yellow'> 队列中 </Label>; if (await copy(text)) {
case 'IN_PROGRESS': showSuccess('已复制:' + text);
return <Label basic color='blue'> 执行中 </Label>; } else {
case 'FAILURE': // setSearchKeyword(text);
return <Label basic color='red'> 失败 </Label>; Modal.error({ title: '无法复制到剪贴板,请手动复制', content: text });
case 'QUEUED':
return <Label basic color='red'> 排队中 </Label>;
case 'UNKNOWN':
return <Label basic color='red'> 未知 </Label>;
case '':
return <Label basic color='black'> 正在提交 </Label>;
default:
return <Label basic color='black'> 未知 </Label>;
}
} }
};
return ( useEffect(() => {
<> refresh().then();
}, [logType]);
<Layout> const renderType = (type) => {
<Form layout='horizontal' labelPosition='inset'> switch (type) {
<> case 'MUSIC':
{isAdminUser && <Form.Input field="channel_id" label='渠道 ID' style={{ width: '236px', marginBottom: '10px' }} value={channel_id} return (
placeholder={'可选值'} name='channel_id' <Label basic color='grey'>
onChange={value => handleInputChange(value, 'channel_id')} /> {' '}
} 生成音乐{' '}
<Form.Input field="task_id" label={"任务 ID"} style={{ width: '236px', marginBottom: '10px' }} value={task_id} </Label>
placeholder={"可选值"} );
name='task_id' case 'LYRICS':
onChange={value => handleInputChange(value, 'task_id')} /> return (
<Label basic color='pink'>
{' '}
生成歌词{' '}
</Label>
);
<Form.DatePicker field="start_timestamp" label={"起始时间"} style={{ width: '236px', marginBottom: '10px' }} default:
initValue={start_timestamp} return (
value={start_timestamp} type='dateTime' <Label basic color='black'>
name='start_timestamp' {' '}
onChange={value => handleInputChange(value, 'start_timestamp')} /> 未知{' '}
<Form.DatePicker field="end_timestamp" fluid label={"结束时间"} style={{ width: '236px', marginBottom: '10px' }} </Label>
initValue={end_timestamp} );
value={end_timestamp} type='dateTime' }
name='end_timestamp' };
onChange={value => handleInputChange(value, 'end_timestamp')} />
<Button label={"查询"} type="primary" htmlType="submit" className="btn-margin-right" const renderPlatform = (type) => {
onClick={refresh}>查询</Button> switch (type) {
</> case 'suno':
</Form> return (
<Card> <Label basic color='green'>
<Table columns={columns} dataSource={pageData} pagination={{ {' '}
currentPage: activePage, Suno{' '}
pageSize: ITEMS_PER_PAGE, </Label>
total: logCount, );
pageSizeOpts: [10, 20, 50, 100], default:
onPageChange: handlePageChange, return (
}} loading={loading} /> <Label basic color='black'>
</Card> {' '}
<Modal 未知{' '}
visible={isModalOpen} </Label>
onOk={() => setIsModalOpen(false)} );
onCancel={() => setIsModalOpen(false)} }
closable={null} };
bodyStyle={{ height: '400px', overflow: 'auto' }} // 设置模态框内容区域样式
width={800} // 设置模态框宽度 const renderStatus = (type) => {
> switch (type) {
<p style={{ whiteSpace: 'pre-line' }}>{modalContent}</p> case 'SUCCESS':
</Modal> return (
</Layout> <Label basic color='green'>
</> {' '}
); 成功{' '}
</Label>
);
case 'NOT_START':
return (
<Label basic color='black'>
{' '}
未启动{' '}
</Label>
);
case 'SUBMITTED':
return (
<Label basic color='yellow'>
{' '}
队列中{' '}
</Label>
);
case 'IN_PROGRESS':
return (
<Label basic color='blue'>
{' '}
执行中{' '}
</Label>
);
case 'FAILURE':
return (
<Label basic color='red'>
{' '}
失败{' '}
</Label>
);
case 'QUEUED':
return (
<Label basic color='red'>
{' '}
排队中{' '}
</Label>
);
case 'UNKNOWN':
return (
<Label basic color='red'>
{' '}
未知{' '}
</Label>
);
case '':
return (
<Label basic color='black'>
{' '}
正在提交{' '}
</Label>
);
default:
return (
<Label basic color='black'>
{' '}
未知{' '}
</Label>
);
}
};
return (
<>
<Layout>
<Form layout='horizontal' labelPosition='inset'>
<>
{isAdminUser && (
<Form.Input
field='channel_id'
label='渠道 ID'
style={{ width: '236px', marginBottom: '10px' }}
value={channel_id}
placeholder={'可选值'}
name='channel_id'
onChange={(value) => handleInputChange(value, 'channel_id')}
/>
)}
<Form.Input
field='task_id'
label={'任务 ID'}
style={{ width: '236px', marginBottom: '10px' }}
value={task_id}
placeholder={'可选值'}
name='task_id'
onChange={(value) => handleInputChange(value, 'task_id')}
/>
<Form.DatePicker
field='start_timestamp'
label={'起始时间'}
style={{ width: '236px', marginBottom: '10px' }}
initValue={start_timestamp}
value={start_timestamp}
type='dateTime'
name='start_timestamp'
onChange={(value) => handleInputChange(value, 'start_timestamp')}
/>
<Form.DatePicker
field='end_timestamp'
fluid
label={'结束时间'}
style={{ width: '236px', marginBottom: '10px' }}
initValue={end_timestamp}
value={end_timestamp}
type='dateTime'
name='end_timestamp'
onChange={(value) => handleInputChange(value, 'end_timestamp')}
/>
<Button
label={'查询'}
type='primary'
htmlType='submit'
className='btn-margin-right'
onClick={refresh}
>
查询
</Button>
</>
</Form>
<Card>
<Table
columns={columns}
dataSource={pageData}
pagination={{
currentPage: activePage,
pageSize: ITEMS_PER_PAGE,
total: logCount,
pageSizeOpts: [10, 20, 50, 100],
onPageChange: handlePageChange,
}}
loading={loading}
/>
</Card>
<Modal
visible={isModalOpen}
onOk={() => setIsModalOpen(false)}
onCancel={() => setIsModalOpen(false)}
closable={null}
bodyStyle={{ height: '400px', overflow: 'auto' }} // 设置模态框内容区域样式
width={800} // 设置模态框宽度
>
<p style={{ whiteSpace: 'pre-line' }}>{modalContent}</p>
</Modal>
</Layout>
</>
);
}; };
export default LogsTable; export default LogsTable;

View File

@@ -8,14 +8,16 @@ import {
} from '../helpers'; } from '../helpers';
import { ITEMS_PER_PAGE } from '../constants'; import { ITEMS_PER_PAGE } from '../constants';
import {renderGroup, renderQuota} from '../helpers/render'; import { renderGroup, renderQuota } from '../helpers/render';
import { import {
Button, Divider, Button,
Divider,
Dropdown, Dropdown,
Form, Form,
Modal, Modal,
Popconfirm, Popconfirm,
Popover, Space, Popover,
Space,
SplitButtonGroup, SplitButtonGroup,
Table, Table,
Tag, Tag,
@@ -30,7 +32,6 @@ function renderTimestamp(timestamp) {
} }
const TokensTable = () => { const TokensTable = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const renderStatus = (status, model_limits_enabled = false) => { const renderStatus = (status, model_limits_enabled = false) => {
@@ -86,12 +87,14 @@ const TokensTable = () => {
dataIndex: 'status', dataIndex: 'status',
key: 'status', key: 'status',
render: (text, record, index) => { render: (text, record, index) => {
return <div> return (
<Space> <div>
{renderStatus(text, record.model_limits_enabled)} <Space>
{renderGroup(record.group)} {renderStatus(text, record.model_limits_enabled)}
</Space> {renderGroup(record.group)}
</div>; </Space>
</div>
);
}, },
}, },
{ {
@@ -143,7 +146,7 @@ const TokensTable = () => {
dataIndex: 'operate', dataIndex: 'operate',
render: (text, record, index) => { render: (text, record, index) => {
let chats = localStorage.getItem('chats'); let chats = localStorage.getItem('chats');
let chatsArray = [] let chatsArray = [];
let shouldUseCustom = true; let shouldUseCustom = true;
if (shouldUseCustom) { if (shouldUseCustom) {
@@ -153,7 +156,7 @@ const TokensTable = () => {
// check chats is array // check chats is array
if (Array.isArray(chats)) { if (Array.isArray(chats)) {
for (let i = 0; i < chats.length; i++) { for (let i = 0; i < chats.length; i++) {
let chat = {} let chat = {};
chat.node = 'item'; chat.node = 'item';
// c is a map // c is a map
// chat.key = chats[i].name; // chat.key = chats[i].name;
@@ -164,13 +167,12 @@ const TokensTable = () => {
chat.name = key; chat.name = key;
chat.onClick = () => { chat.onClick = () => {
onOpenLink(key, chats[i][key], record); onOpenLink(key, chats[i][key], record);
} };
} }
} }
chatsArray.push(chat); chatsArray.push(chat);
} }
} }
} catch (e) { } catch (e) {
console.log(e); console.log(e);
showError(t('聊天链接配置错误,请联系管理员')); showError(t('聊天链接配置错误,请联系管理员'));
@@ -208,7 +210,11 @@ const TokensTable = () => {
if (chatsArray.length === 0) { if (chatsArray.length === 0) {
showError(t('请联系管理员配置聊天链接')); showError(t('请联系管理员配置聊天链接'));
} else { } else {
onOpenLink('default', chats[0][Object.keys(chats[0])[0]], record); onOpenLink(
'default',
chats[0][Object.keys(chats[0])[0]],
record,
);
} }
}} }}
> >
@@ -539,36 +545,36 @@ const TokensTable = () => {
{t('查询')} {t('查询')}
</Button> </Button>
</Form> </Form>
<Divider style={{margin:'15px 0'}}/> <Divider style={{ margin: '15px 0' }} />
<div> <div>
<Button <Button
theme='light' theme='light'
type='primary' type='primary'
style={{ marginRight: 8 }} style={{ marginRight: 8 }}
onClick={() => { onClick={() => {
setEditingToken({ setEditingToken({
id: undefined, id: undefined,
}); });
setShowEdit(true); setShowEdit(true);
}} }}
> >
{t('添加令牌')} {t('添加令牌')}
</Button> </Button>
<Button <Button
label={t('复制所选令牌')} label={t('复制所选令牌')}
type='warning' type='warning'
onClick={async () => { onClick={async () => {
if (selectedKeys.length === 0) { if (selectedKeys.length === 0) {
showError(t('请至少选择一个令牌!')); showError(t('请至少选择一个令牌!'));
return; return;
} }
let keys = ''; let keys = '';
for (let i = 0; i < selectedKeys.length; i++) { for (let i = 0; i < selectedKeys.length; i++) {
keys += keys +=
selectedKeys[i].name + ' sk-' + selectedKeys[i].key + '\n'; selectedKeys[i].name + ' sk-' + selectedKeys[i].key + '\n';
} }
await copyText(keys); await copyText(keys);
}} }}
> >
{t('复制所选令牌到剪贴板')} {t('复制所选令牌到剪贴板')}
</Button> </Button>
@@ -588,7 +594,7 @@ const TokensTable = () => {
t('第 {{start}} - {{end}} 条,共 {{total}} 条', { t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
start: page.currentStart, start: page.currentStart,
end: page.currentEnd, end: page.currentEnd,
total: tokens.length total: tokens.length,
}), }),
onPageSizeChange: (size) => { onPageSizeChange: (size) => {
setPageSize(size); setPageSize(size);

View File

@@ -167,7 +167,11 @@ const UsersTable = () => {
manageUser(record.id, 'demote', record); manageUser(record.id, 'demote', record);
}} }}
> >
<Button theme='light' type='secondary' style={{ marginRight: 1 }}> <Button
theme='light'
type='secondary'
style={{ marginRight: 1 }}
>
{t('降级')} {t('降级')}
</Button> </Button>
</Popconfirm> </Popconfirm>
@@ -261,7 +265,7 @@ const UsersTable = () => {
users[i].key = users[i].id; users[i].key = users[i].id;
} }
setUsers(users); setUsers(users);
} };
const loadUsers = async (startIdx, pageSize) => { const loadUsers = async (startIdx, pageSize) => {
const res = await API.get(`/api/user/?p=${startIdx}&page_size=${pageSize}`); const res = await API.get(`/api/user/?p=${startIdx}&page_size=${pageSize}`);
@@ -277,7 +281,6 @@ const UsersTable = () => {
setLoading(false); setLoading(false);
}; };
useEffect(() => { useEffect(() => {
loadUsers(0, pageSize) loadUsers(0, pageSize)
.then() .then()
@@ -327,22 +330,29 @@ const UsersTable = () => {
} }
}; };
const searchUsers = async (startIdx, pageSize, searchKeyword, searchGroup) => { const searchUsers = async (
startIdx,
pageSize,
searchKeyword,
searchGroup,
) => {
if (searchKeyword === '' && searchGroup === '') { if (searchKeyword === '' && searchGroup === '') {
// if keyword is blank, load files instead. // if keyword is blank, load files instead.
await loadUsers(startIdx, pageSize); await loadUsers(startIdx, pageSize);
return; return;
} }
setSearching(true); setSearching(true);
const res = await API.get(`/api/user/search?keyword=${searchKeyword}&group=${searchGroup}&p=${startIdx}&page_size=${pageSize}`); const res = await API.get(
`/api/user/search?keyword=${searchKeyword}&group=${searchGroup}&p=${startIdx}&page_size=${pageSize}`,
);
const { success, message, data } = res.data; const { success, message, data } = res.data;
if (success) { if (success) {
const newPageData = data.items; const newPageData = data.items;
setActivePage(data.page); setActivePage(data.page);
setUserCount(data.total); setUserCount(data.total);
setUserFormat(newPageData); setUserFormat(newPageData);
} else { } else {
showError(message); showError(message);
} }
setSearching(false); setSearching(false);
}; };
@@ -354,9 +364,9 @@ const UsersTable = () => {
const handlePageChange = (page) => { const handlePageChange = (page) => {
setActivePage(page); setActivePage(page);
if (searchKeyword === '' && searchGroup === '') { if (searchKeyword === '' && searchGroup === '') {
loadUsers(page, pageSize).then(); loadUsers(page, pageSize).then();
} else { } else {
searchUsers(page, pageSize, searchKeyword, searchGroup).then(); searchUsers(page, pageSize, searchKeyword, searchGroup).then();
} }
}; };
@@ -372,7 +382,7 @@ const UsersTable = () => {
}; };
const refresh = async () => { const refresh = async () => {
setActivePage(1) setActivePage(1);
if (searchKeyword === '') { if (searchKeyword === '') {
await loadUsers(activePage, pageSize); await loadUsers(activePage, pageSize);
} else { } else {
@@ -431,7 +441,9 @@ const UsersTable = () => {
> >
<div style={{ display: 'flex' }}> <div style={{ display: 'flex' }}>
<Space> <Space>
<Tooltip content={t('支持搜索用户的 ID、用户名、显示名称和邮箱地址')}> <Tooltip
content={t('支持搜索用户的 ID、用户名、显示名称和邮箱地址')}
>
<Form.Input <Form.Input
label={t('搜索关键字')} label={t('搜索关键字')}
icon='search' icon='search'
@@ -482,7 +494,7 @@ const UsersTable = () => {
t('第 {{start}} - {{end}} 条,共 {{total}} 条', { t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
start: page.currentStart, start: page.currentStart,
end: page.currentEnd, end: page.currentEnd,
total: users.length total: users.length,
}), }),
currentPage: activePage, currentPage: activePage,
pageSize: pageSize, pageSize: pageSize,

View File

@@ -1,7 +1,14 @@
import { Input, Typography } from '@douyinfe/semi-ui'; import { Input, Typography } from '@douyinfe/semi-ui';
import React from 'react'; import React from 'react';
const TextInput = ({ label, name, value, onChange, placeholder, type = 'text' }) => { const TextInput = ({
label,
name,
value,
onChange,
placeholder,
type = 'text',
}) => {
return ( return (
<> <>
<div style={{ marginTop: 10 }}> <div style={{ marginTop: 10 }}>
@@ -12,10 +19,10 @@ const TextInput = ({ label, name, value, onChange, placeholder, type = 'text' })
placeholder={placeholder} placeholder={placeholder}
onChange={(value) => onChange(value)} onChange={(value) => onChange(value)}
value={value} value={value}
autoComplete="new-password" autoComplete='new-password'
/> />
</> </>
); );
} };
export default TextInput; export default TextInput;

View File

@@ -12,10 +12,10 @@ const TextNumberInput = ({ label, name, value, onChange, placeholder }) => {
placeholder={placeholder} placeholder={placeholder}
onChange={(value) => onChange(value)} onChange={(value) => onChange(value)}
value={value} value={value}
autoComplete="new-password" autoComplete='new-password'
/> />
</> </>
); );
} };
export default TextNumberInput; export default TextNumberInput;

View File

@@ -13,7 +13,7 @@ async function fetchTokenKeys() {
throw new Error('Failed to fetch token keys'); throw new Error('Failed to fetch token keys');
} }
} catch (error) { } catch (error) {
console.error("Error fetching token keys:", error); console.error('Error fetching token keys:', error);
return []; return [];
} }
} }
@@ -27,7 +27,7 @@ function getServerAddress() {
status = JSON.parse(status); status = JSON.parse(status);
serverAddress = status.server_address || ''; serverAddress = status.server_address || '';
} catch (error) { } catch (error) {
console.error("Failed to parse status from localStorage:", error); console.error('Failed to parse status from localStorage:', error);
} }
} }

View File

@@ -20,13 +20,12 @@ export async function onOIDCClicked(auth_url, client_id, openInNewTab = false) {
const state = await getOAuthState(); const state = await getOAuthState();
if (!state) return; if (!state) return;
const redirect_uri = `${window.location.origin}/oauth/oidc`; const redirect_uri = `${window.location.origin}/oauth/oidc`;
const response_type = "code"; const response_type = 'code';
const scope = "openid profile email"; const scope = 'openid profile email';
const url = `${auth_url}?client_id=${client_id}&redirect_uri=${redirect_uri}&response_type=${response_type}&scope=${scope}&state=${state}`; const url = `${auth_url}?client_id=${client_id}&redirect_uri=${redirect_uri}&response_type=${response_type}&scope=${scope}&state=${state}`;
if (openInNewTab) { if (openInNewTab) {
window.open(url); window.open(url);
} else } else {
{
window.location.href = url; window.location.href = url;
} }
} }

View File

@@ -3,86 +3,86 @@ export const CHANNEL_OPTIONS = [
{ {
value: 2, value: 2,
color: 'light-blue', color: 'light-blue',
label: 'Midjourney Proxy' label: 'Midjourney Proxy',
}, },
{ {
value: 5, value: 5,
color: 'blue', color: 'blue',
label: 'Midjourney Proxy Plus' label: 'Midjourney Proxy Plus',
}, },
{ {
value: 36, value: 36,
color: 'purple', color: 'purple',
label: 'Suno API' label: 'Suno API',
}, },
{ value: 4, color: 'grey', label: 'Ollama' }, { value: 4, color: 'grey', label: 'Ollama' },
{ {
value: 14, value: 14,
color: 'indigo', color: 'indigo',
label: 'Anthropic Claude' label: 'Anthropic Claude',
}, },
{ {
value: 33, value: 33,
color: 'indigo', color: 'indigo',
label: 'AWS Claude' label: 'AWS Claude',
}, },
{ value: 41, color: 'blue', label: 'Vertex AI' }, { value: 41, color: 'blue', label: 'Vertex AI' },
{ {
value: 3, value: 3,
color: 'teal', color: 'teal',
label: 'Azure OpenAI' label: 'Azure OpenAI',
}, },
{ {
value: 34, value: 34,
color: 'purple', color: 'purple',
label: 'Cohere' label: 'Cohere',
}, },
{ value: 39, color: 'grey', label: 'Cloudflare' }, { value: 39, color: 'grey', label: 'Cloudflare' },
{ value: 43, color: 'blue', label: 'DeepSeek' }, { value: 43, color: 'blue', label: 'DeepSeek' },
{ {
value: 15, value: 15,
color: 'blue', color: 'blue',
label: '百度文心千帆' label: '百度文心千帆',
}, },
{ {
value: 46, value: 46,
color: 'blue', color: 'blue',
label: '百度文心千帆V2' label: '百度文心千帆V2',
}, },
{ {
value: 17, value: 17,
color: 'orange', color: 'orange',
label: '阿里通义千问' label: '阿里通义千问',
}, },
{ {
value: 18, value: 18,
color: 'blue', color: 'blue',
label: '讯飞星火认知' label: '讯飞星火认知',
}, },
{ {
value: 16, value: 16,
color: 'violet', color: 'violet',
label: '智谱 ChatGLM' label: '智谱 ChatGLM',
}, },
{ {
value: 26, value: 26,
color: 'purple', color: 'purple',
label: '智谱 GLM-4V' label: '智谱 GLM-4V',
}, },
{ {
value: 24, value: 24,
color: 'orange', color: 'orange',
label: 'Google Gemini' label: 'Google Gemini',
}, },
{ {
value: 11, value: 11,
color: 'orange', color: 'orange',
label: 'Google PaLM2' label: 'Google PaLM2',
}, },
{ {
value: 47, value: 47,
color: 'blue', color: 'blue',
label: 'Xinference' label: 'Xinference',
}, },
{ value: 25, color: 'green', label: 'Moonshot' }, { value: 25, color: 'green', label: 'Moonshot' },
{ value: 20, color: 'green', label: 'OpenRouter' }, { value: 20, color: 'green', label: 'OpenRouter' },
@@ -98,21 +98,21 @@ export const CHANNEL_OPTIONS = [
{ {
value: 22, value: 22,
color: 'blue', color: 'blue',
label: '知识库FastGPT' label: '知识库FastGPT',
}, },
{ {
value: 21, value: 21,
color: 'purple', color: 'purple',
label: '知识库AI Proxy' label: '知识库AI Proxy',
}, },
{ {
value: 44, value: 44,
color: 'purple', color: 'purple',
label: '嵌入模型MokaAI M3E' label: '嵌入模型MokaAI M3E',
}, },
{ {
value: 45, value: 45,
color: 'blue', color: 'blue',
label: '字节火山方舟、豆包、DeepSeek通用' label: '字节火山方舟、豆包、DeepSeek通用',
}, },
]; ];

View File

@@ -19,25 +19,25 @@ export const StyleProvider = ({ children }) => {
if ('type' in action) { if ('type' in action) {
switch (action.type) { switch (action.type) {
case 'TOGGLE_SIDER': case 'TOGGLE_SIDER':
setState(prev => ({ ...prev, showSider: !prev.showSider })); setState((prev) => ({ ...prev, showSider: !prev.showSider }));
break; break;
case 'SET_SIDER': case 'SET_SIDER':
setState(prev => ({ ...prev, showSider: action.payload })); setState((prev) => ({ ...prev, showSider: action.payload }));
break; break;
case 'SET_MOBILE': case 'SET_MOBILE':
setState(prev => ({ ...prev, isMobile: action.payload })); setState((prev) => ({ ...prev, isMobile: action.payload }));
break; break;
case 'SET_SIDER_COLLAPSED': case 'SET_SIDER_COLLAPSED':
setState(prev => ({ ...prev, siderCollapsed: action.payload })); setState((prev) => ({ ...prev, siderCollapsed: action.payload }));
break break;
case 'SET_INNER_PADDING': case 'SET_INNER_PADDING':
setState(prev => ({ ...prev, shouldInnerPadding: action.payload })); setState((prev) => ({ ...prev, shouldInnerPadding: action.payload }));
break; break;
default: default:
setState(prev => ({ ...prev, ...action })); setState((prev) => ({ ...prev, ...action }));
} }
} else { } else {
setState(prev => ({ ...prev, ...action })); setState((prev) => ({ ...prev, ...action }));
} }
}; };
@@ -57,7 +57,12 @@ export const StyleProvider = ({ children }) => {
const updateShowSider = () => { const updateShowSider = () => {
// check pathname // check pathname
const pathname = window.location.pathname; const pathname = window.location.pathname;
if (pathname === '' || pathname === '/' || pathname.includes('/home') || pathname.includes('/chat')) { if (
pathname === '' ||
pathname === '/' ||
pathname.includes('/home') ||
pathname.includes('/chat')
) {
dispatch({ type: 'SET_SIDER', payload: false }); dispatch({ type: 'SET_SIDER', payload: false });
dispatch({ type: 'SET_INNER_PADDING', payload: false }); dispatch({ type: 'SET_INNER_PADDING', payload: false });
} else if (pathname === '/setup') { } else if (pathname === '/setup') {
@@ -73,7 +78,8 @@ export const StyleProvider = ({ children }) => {
updateShowSider(); updateShowSider();
const updateSiderCollapsed = () => { const updateSiderCollapsed = () => {
const isCollapsed = localStorage.getItem('default_collapse_sidebar') === 'true'; const isCollapsed =
localStorage.getItem('default_collapse_sidebar') === 'true';
dispatch({ type: 'SET_SIDER_COLLAPSED', payload: isCollapsed }); dispatch({ type: 'SET_SIDER_COLLAPSED', payload: isCollapsed });
}; };

View File

@@ -7,8 +7,8 @@ export let API = axios.create({
: '', : '',
headers: { headers: {
'New-API-User': getUserIdFromLocalStorage(), 'New-API-User': getUserIdFromLocalStorage(),
'Cache-Control': 'no-store' 'Cache-Control': 'no-store',
} },
}); });
export function updateAPI() { export function updateAPI() {
@@ -18,8 +18,8 @@ export function updateAPI() {
: '', : '',
headers: { headers: {
'New-API-User': getUserIdFromLocalStorage(), 'New-API-User': getUserIdFromLocalStorage(),
'Cache-Control': 'no-store' 'Cache-Control': 'no-store',
} },
}); });
} }

View File

@@ -1,7 +1,7 @@
export function getLogOther(otherStr) { export function getLogOther(otherStr) {
if (otherStr === undefined || otherStr === '') { if (otherStr === undefined || otherStr === '') {
otherStr = '{}' otherStr = '{}';
} }
let other = JSON.parse(otherStr) let other = JSON.parse(otherStr);
return other return other;
} }

View File

@@ -44,7 +44,10 @@ export function renderGroup(group) {
if (await copy(group)) { if (await copy(group)) {
showSuccess(i18next.t('已复制:') + group); showSuccess(i18next.t('已复制:') + group);
} else { } else {
Modal.error({ title: t('无法复制到剪贴板,请手动复制'), content: group }); Modal.error({
title: t('无法复制到剪贴板,请手动复制'),
content: group,
});
} }
}} }}
> >
@@ -64,13 +67,22 @@ export function renderRatio(ratio) {
} else if (ratio > 1) { } else if (ratio > 1) {
color = 'blue'; color = 'blue';
} }
return <Tag color={color}>{ratio}x {i18next.t('倍率')}</Tag>; return (
<Tag color={color}>
{ratio}x {i18next.t('倍率')}
</Tag>
);
} }
const measureTextWidth = (text, style = { const measureTextWidth = (
fontSize: '14px', text,
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif' style = {
}, containerWidth) => { fontSize: '14px',
fontFamily:
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
},
containerWidth,
) => {
const span = document.createElement('span'); const span = document.createElement('span');
span.style.visibility = 'hidden'; span.style.visibility = 'hidden';
@@ -126,7 +138,10 @@ export function truncateText(text, maxWidth = 200) {
return result; return result;
} catch (error) { } catch (error) {
console.warn('Text measurement failed, falling back to character count', error); console.warn(
'Text measurement failed, falling back to character count',
error,
);
if (text.length > 20) { if (text.length > 20) {
return text.slice(0, 17) + '...'; return text.slice(0, 17) + '...';
} }
@@ -162,8 +177,8 @@ export const renderGroupOption = (item) => {
backgroundColor: 'var(--semi-color-primary-light-default)', backgroundColor: 'var(--semi-color-primary-light-default)',
}), }),
'&:hover': { '&:hover': {
backgroundColor: !disabled && 'var(--semi-color-fill-1)' backgroundColor: !disabled && 'var(--semi-color-fill-1)',
} },
}; };
const handleClick = () => { const handleClick = () => {
@@ -188,7 +203,7 @@ export const renderGroupOption = (item) => {
<Typography.Text strong type={disabled ? 'tertiary' : undefined}> <Typography.Text strong type={disabled ? 'tertiary' : undefined}>
{value} {value}
</Typography.Text> </Typography.Text>
<Typography.Text type="secondary" size="small"> <Typography.Text type='secondary' size='small'>
{label} {label}
</Typography.Text> </Typography.Text>
</div> </div>
@@ -222,8 +237,7 @@ export function renderQuotaNumberWithDigit(num, digits = 2) {
} }
export function renderNumberWithPoint(num) { export function renderNumberWithPoint(num) {
if (num === undefined) if (num === undefined) return '';
return '';
num = num.toFixed(2); num = num.toFixed(2);
if (num >= 100000) { if (num >= 100000) {
// Convert number to string to manipulate it // Convert number to string to manipulate it
@@ -302,11 +316,14 @@ export function renderModelPrice(
cacheRatio = 1.0, cacheRatio = 1.0,
) { ) {
if (modelPrice !== -1) { if (modelPrice !== -1) {
return i18next.t('模型价格:${{price}} * 分组倍率:{{ratio}} = ${{total}}', { return i18next.t(
price: modelPrice, '模型价格:${{price}} * 分组倍率:{{ratio}} = ${{total}}',
ratio: groupRatio, {
total: modelPrice * groupRatio price: modelPrice,
}); ratio: groupRatio,
total: modelPrice * groupRatio,
},
);
} else { } else {
if (completionRatio === undefined) { if (completionRatio === undefined) {
completionRatio = 0; completionRatio = 0;
@@ -316,7 +333,8 @@ export function renderModelPrice(
let cacheRatioPrice = modelRatio * 2.0 * cacheRatio; let cacheRatioPrice = modelRatio * 2.0 * cacheRatio;
// Calculate effective input tokens (non-cached + cached with ratio applied) // Calculate effective input tokens (non-cached + cached with ratio applied)
const effectiveInputTokens = (inputTokens - cacheTokens) + (cacheTokens * cacheRatio); const effectiveInputTokens =
inputTokens - cacheTokens + cacheTokens * cacheRatio;
let price = let price =
(effectiveInputTokens / 1000000) * inputRatioPrice * groupRatio + (effectiveInputTokens / 1000000) * inputRatioPrice * groupRatio +
@@ -325,43 +343,60 @@ export function renderModelPrice(
return ( return (
<> <>
<article> <article>
<p>{i18next.t('提示价格:${{price}} / 1M tokens', { <p>
price: inputRatioPrice, {i18next.t('提示价格:${{price}} / 1M tokens', {
})}</p>
<p>{i18next.t('补全价格:${{price}} * {{completionRatio}} = ${{total}} / 1M tokens (补全倍率: {{completionRatio}})', {
price: inputRatioPrice,
total: completionRatioPrice,
completionRatio: completionRatio
})}</p>
{cacheTokens > 0 && (
<p>{i18next.t('缓存价格:${{price}} * {{cacheRatio}} = ${{total}} / 1M tokens (缓存倍率: {{cacheRatio}})', {
price: inputRatioPrice, price: inputRatioPrice,
total: inputRatioPrice * cacheRatio, })}
cacheRatio: cacheRatio </p>
})}</p> <p>
{i18next.t(
'补全价格:${{price}} * {{completionRatio}} = ${{total}} / 1M tokens (补全倍率: {{completionRatio}})',
{
price: inputRatioPrice,
total: completionRatioPrice,
completionRatio: completionRatio,
},
)}
</p>
{cacheTokens > 0 && (
<p>
{i18next.t(
'缓存价格:${{price}} * {{cacheRatio}} = ${{total}} / 1M tokens (缓存倍率: {{cacheRatio}})',
{
price: inputRatioPrice,
total: inputRatioPrice * cacheRatio,
cacheRatio: cacheRatio,
},
)}
</p>
)} )}
<p></p> <p></p>
<p> <p>
{cacheTokens > 0 ? {cacheTokens > 0
i18next.t('提示 {{nonCacheInput}} tokens / 1M tokens * ${{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * ${{cachePrice}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}', { ? i18next.t(
nonCacheInput: inputTokens - cacheTokens, '提示 {{nonCacheInput}} tokens / 1M tokens * ${{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * ${{cachePrice}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}',
cacheInput: cacheTokens, {
cachePrice: inputRatioPrice * cacheRatio, nonCacheInput: inputTokens - cacheTokens,
price: inputRatioPrice, cacheInput: cacheTokens,
completion: completionTokens, cachePrice: inputRatioPrice * cacheRatio,
compPrice: completionRatioPrice, price: inputRatioPrice,
ratio: groupRatio, completion: completionTokens,
total: price.toFixed(6) compPrice: completionRatioPrice,
}) : ratio: groupRatio,
i18next.t('提示 {{input}} tokens / 1M tokens * ${{price}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}', { total: price.toFixed(6),
input: inputTokens, },
price: inputRatioPrice, )
completion: completionTokens, : i18next.t(
compPrice: completionRatioPrice, '提示 {{input}} tokens / 1M tokens * ${{price}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}',
ratio: groupRatio, {
total: price.toFixed(6) input: inputTokens,
}) price: inputRatioPrice,
} completion: completionTokens,
compPrice: completionRatioPrice,
ratio: groupRatio,
total: price.toFixed(6),
},
)}
</p> </p>
<p>{i18next.t('仅供参考,以实际扣费为准')}</p> <p>{i18next.t('仅供参考,以实际扣费为准')}</p>
</article> </article>
@@ -380,19 +415,22 @@ export function renderModelPriceSimple(
if (modelPrice !== -1) { if (modelPrice !== -1) {
return i18next.t('价格:${{price}} * 分组:{{ratio}}', { return i18next.t('价格:${{price}} * 分组:{{ratio}}', {
price: modelPrice, price: modelPrice,
ratio: groupRatio ratio: groupRatio,
}); });
} else { } else {
if (cacheTokens !== 0) { if (cacheTokens !== 0) {
return i18next.t('模型: {{ratio}} * 分组: {{groupRatio}} * 缓存: {{cacheRatio}}', { return i18next.t(
ratio: modelRatio, '模型: {{ratio}} * 分组: {{groupRatio}} * 缓存: {{cacheRatio}}',
groupRatio: groupRatio, {
cacheRatio: cacheRatio ratio: modelRatio,
}); groupRatio: groupRatio,
cacheRatio: cacheRatio,
},
);
} else { } else {
return i18next.t('模型: {{ratio}} * 分组: {{groupRatio}}', { return i18next.t('模型: {{ratio}} * 分组: {{groupRatio}}', {
ratio: modelRatio, ratio: modelRatio,
groupRatio: groupRatio groupRatio: groupRatio,
}); });
} }
} }
@@ -414,11 +452,14 @@ export function renderAudioModelPrice(
) { ) {
// 1 ratio = $0.002 / 1K tokens // 1 ratio = $0.002 / 1K tokens
if (modelPrice !== -1) { if (modelPrice !== -1) {
return i18next.t('模型价格:${{price}} * 分组倍率:{{ratio}} = ${{total}}', { return i18next.t(
price: modelPrice, '模型价格:${{price}} * 分组倍率:{{ratio}} = ${{total}}',
ratio: groupRatio, {
total: modelPrice * groupRatio price: modelPrice,
}); ratio: groupRatio,
total: modelPrice * groupRatio,
},
);
} else { } else {
if (completionRatio === undefined) { if (completionRatio === undefined) {
completionRatio = 0; completionRatio = 0;
@@ -432,79 +473,118 @@ export function renderAudioModelPrice(
let cacheRatioPrice = modelRatio * 2.0 * cacheRatio; let cacheRatioPrice = modelRatio * 2.0 * cacheRatio;
// Calculate effective input tokens (non-cached + cached with ratio applied) // Calculate effective input tokens (non-cached + cached with ratio applied)
const effectiveInputTokens = (inputTokens - cacheTokens) + (cacheTokens * cacheRatio); const effectiveInputTokens =
inputTokens - cacheTokens + cacheTokens * cacheRatio;
let textPrice = let textPrice =
(effectiveInputTokens / 1000000) * inputRatioPrice * groupRatio + (effectiveInputTokens / 1000000) * inputRatioPrice * groupRatio +
(completionTokens / 1000000) * completionRatioPrice * groupRatio (completionTokens / 1000000) * completionRatioPrice * groupRatio;
let audioPrice = let audioPrice =
(audioInputTokens / 1000000) * inputRatioPrice * audioRatio * groupRatio + (audioInputTokens / 1000000) * inputRatioPrice * audioRatio * groupRatio +
(audioCompletionTokens / 1000000) * inputRatioPrice * audioRatio * audioCompletionRatio * groupRatio; (audioCompletionTokens / 1000000) *
inputRatioPrice *
audioRatio *
audioCompletionRatio *
groupRatio;
let price = textPrice + audioPrice; let price = textPrice + audioPrice;
return ( return (
<> <>
<article> <article>
<p>{i18next.t('提示价格:${{price}} / 1M tokens', { <p>
price: inputRatioPrice, {i18next.t('提示价格:${{price}} / 1M tokens', {
})}</p>
<p>{i18next.t('补全价格:${{price}} * {{completionRatio}} = ${{total}} / 1M tokens (补全倍率: {{completionRatio}})', {
price: inputRatioPrice,
total: completionRatioPrice,
completionRatio: completionRatio
})}</p>
{cacheTokens > 0 && (
<p>{i18next.t('缓存价格:${{price}} * {{cacheRatio}} = ${{total}} / 1M tokens (缓存倍率: {{cacheRatio}})', {
price: inputRatioPrice, price: inputRatioPrice,
total: inputRatioPrice * cacheRatio, })}
cacheRatio: cacheRatio </p>
})}</p> <p>
{i18next.t(
'补全价格:${{price}} * {{completionRatio}} = ${{total}} / 1M tokens (补全倍率: {{completionRatio}})',
{
price: inputRatioPrice,
total: completionRatioPrice,
completionRatio: completionRatio,
},
)}
</p>
{cacheTokens > 0 && (
<p>
{i18next.t(
'缓存价格:${{price}} * {{cacheRatio}} = ${{total}} / 1M tokens (缓存倍率: {{cacheRatio}})',
{
price: inputRatioPrice,
total: inputRatioPrice * cacheRatio,
cacheRatio: cacheRatio,
},
)}
</p>
)} )}
<p>{i18next.t('音频提示价格:${{price}} * {{audioRatio}} = ${{total}} / 1M tokens (音频倍率: {{audioRatio}})', {
price: inputRatioPrice,
total: inputRatioPrice * audioRatio,
audioRatio: audioRatio
})}</p>
<p>{i18next.t('音频补全价格:${{price}} * {{audioRatio}} * {{audioCompRatio}} = ${{total}} / 1M tokens (音频补全倍率: {{audioCompRatio}})', {
price: inputRatioPrice,
total: inputRatioPrice * audioRatio * audioCompletionRatio,
audioRatio: audioRatio,
audioCompRatio: audioCompletionRatio
})}</p>
<p> <p>
{cacheTokens > 0 ? {i18next.t(
i18next.t('文字提示 {{nonCacheInput}} tokens / 1M tokens * ${{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * ${{cachePrice}} + 文字补全 {{completion}} tokens / 1M tokens * ${{compPrice}} = ${{total}}', { '音频提示价格:${{price}} * {{audioRatio}} = ${{total}} / 1M tokens (音频倍率: {{audioRatio}})',
nonCacheInput: inputTokens - cacheTokens, {
cacheInput: cacheTokens,
cachePrice: inputRatioPrice * cacheRatio,
price: inputRatioPrice, price: inputRatioPrice,
completion: completionTokens, total: inputRatioPrice * audioRatio,
compPrice: completionRatioPrice, audioRatio: audioRatio,
total: textPrice.toFixed(6) },
}) : )}
i18next.t('文字提示 {{input}} tokens / 1M tokens * ${{price}} + 文字补全 {{completion}} tokens / 1M tokens * ${{compPrice}} = ${{total}}', {
input: inputTokens,
price: inputRatioPrice,
completion: completionTokens,
compPrice: completionRatioPrice,
total: textPrice.toFixed(6)
})
}
</p> </p>
<p> <p>
{i18next.t('音频提示 {{input}} tokens / 1M tokens * ${{audioInputPrice}} + 音频补全 {{completion}} tokens / 1M tokens * ${{audioCompPrice}} = ${{total}}', { {i18next.t(
input: audioInputTokens, '音频补全价格:${{price}} * {{audioRatio}} * {{audioCompRatio}} = ${{total}} / 1M tokens (音频补全倍率: {{audioCompRatio}})',
completion: audioCompletionTokens, {
audioInputPrice: audioRatio * inputRatioPrice, price: inputRatioPrice,
audioCompPrice: audioRatio * audioCompletionRatio * inputRatioPrice, total: inputRatioPrice * audioRatio * audioCompletionRatio,
total: audioPrice.toFixed(6) audioRatio: audioRatio,
})} audioCompRatio: audioCompletionRatio,
},
)}
</p> </p>
<p> <p>
{i18next.t('总价:文字价格 {{textPrice}} + 音频价格 {{audioPrice}} = ${{total}}', { {cacheTokens > 0
total: price.toFixed(6), ? i18next.t(
textPrice: textPrice.toFixed(6), '文字提示 {{nonCacheInput}} tokens / 1M tokens * ${{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * ${{cachePrice}} + 文字补全 {{completion}} tokens / 1M tokens * ${{compPrice}} = ${{total}}',
audioPrice: audioPrice.toFixed(6) {
})} nonCacheInput: inputTokens - cacheTokens,
cacheInput: cacheTokens,
cachePrice: inputRatioPrice * cacheRatio,
price: inputRatioPrice,
completion: completionTokens,
compPrice: completionRatioPrice,
total: textPrice.toFixed(6),
},
)
: i18next.t(
'文字提示 {{input}} tokens / 1M tokens * ${{price}} + 文字补全 {{completion}} tokens / 1M tokens * ${{compPrice}} = ${{total}}',
{
input: inputTokens,
price: inputRatioPrice,
completion: completionTokens,
compPrice: completionRatioPrice,
total: textPrice.toFixed(6),
},
)}
</p>
<p>
{i18next.t(
'音频提示 {{input}} tokens / 1M tokens * ${{audioInputPrice}} + 音频补全 {{completion}} tokens / 1M tokens * ${{audioCompPrice}} = ${{total}}',
{
input: audioInputTokens,
completion: audioCompletionTokens,
audioInputPrice: audioRatio * inputRatioPrice,
audioCompPrice:
audioRatio * audioCompletionRatio * inputRatioPrice,
total: audioPrice.toFixed(6),
},
)}
</p>
<p>
{i18next.t(
'总价:文字价格 {{textPrice}} + 音频价格 {{audioPrice}} = ${{total}}',
{
total: price.toFixed(6),
textPrice: textPrice.toFixed(6),
audioPrice: audioPrice.toFixed(6),
},
)}
</p> </p>
<p>{i18next.t('仅供参考,以实际扣费为准')}</p> <p>{i18next.t('仅供参考,以实际扣费为准')}</p>
</article> </article>
@@ -517,7 +597,9 @@ export function renderQuotaWithPrompt(quota, digits) {
let displayInCurrency = localStorage.getItem('display_in_currency'); let displayInCurrency = localStorage.getItem('display_in_currency');
displayInCurrency = displayInCurrency === 'true'; displayInCurrency = displayInCurrency === 'true';
if (displayInCurrency) { if (displayInCurrency) {
return ' | ' + i18next.t('等价金额') + ': ' + renderQuota(quota, digits) + ''; return (
' | ' + i18next.t('等价金额') + ': ' + renderQuota(quota, digits) + ''
);
} }
return ''; return '';
} }
@@ -537,7 +619,7 @@ const colors = [
'red', 'red',
'teal', 'teal',
'violet', 'violet',
'yellow' 'yellow',
]; ];
// 基础10色色板 (N ≤ 10) // 基础10色色板 (N ≤ 10)
@@ -551,7 +633,7 @@ const baseColors = [
'#304D77', '#304D77',
'#B48DEB', '#B48DEB',
'#009488', '#009488',
'#FF7DDA' '#FF7DDA',
]; ];
// 扩展20色色板 (10 < N ≤ 20) // 扩展20色色板 (10 < N ≤ 20)
@@ -575,7 +657,7 @@ const extendedColors = [
'#009488', '#009488',
'#59BAA8', '#59BAA8',
'#FF7DDA', '#FF7DDA',
'#FFCFEE' '#FFCFEE',
]; ];
export const modelColorMap = { export const modelColorMap = {
@@ -631,7 +713,7 @@ export function modelToColor(modelName) {
// 2. 生成一个稳定的数字作为索引 // 2. 生成一个稳定的数字作为索引
let hash = 0; let hash = 0;
for (let i = 0; i < modelName.length; i++) { for (let i = 0; i < modelName.length; i++) {
hash = ((hash << 5) - hash) + modelName.charCodeAt(i); hash = (hash << 5) - hash + modelName.charCodeAt(i);
hash = hash & hash; // Convert to 32-bit integer hash = hash & hash; // Convert to 32-bit integer
} }
hash = Math.abs(hash); hash = Math.abs(hash);
@@ -668,12 +750,15 @@ export function renderClaudeModelPrice(
const ratioLabel = false ? i18next.t('专属倍率') : i18next.t('分组倍率'); const ratioLabel = false ? i18next.t('专属倍率') : i18next.t('分组倍率');
if (modelPrice !== -1) { if (modelPrice !== -1) {
return i18next.t('模型价格:${{price}} * {{ratioType}}{{ratio}} = ${{total}}', { return i18next.t(
price: modelPrice, '模型价格:${{price}} * {{ratioType}}{{ratio}} = ${{total}}',
ratioType: ratioLabel, {
ratio: groupRatio, price: modelPrice,
total: modelPrice * groupRatio ratioType: ratioLabel,
}); ratio: groupRatio,
total: modelPrice * groupRatio,
},
);
} else { } else {
if (completionRatio === undefined) { if (completionRatio === undefined) {
completionRatio = 0; completionRatio = 0;
@@ -687,9 +772,10 @@ export function renderClaudeModelPrice(
// Calculate effective input tokens (non-cached + cached with ratio applied + cache creation with ratio applied) // Calculate effective input tokens (non-cached + cached with ratio applied + cache creation with ratio applied)
const nonCachedTokens = inputTokens; const nonCachedTokens = inputTokens;
const effectiveInputTokens = nonCachedTokens + const effectiveInputTokens =
(cacheTokens * cacheRatio) + nonCachedTokens +
(cacheCreationTokens * cacheCreationRatio); cacheTokens * cacheRatio +
cacheCreationTokens * cacheCreationRatio;
let price = let price =
(effectiveInputTokens / 1000000) * inputRatioPrice * groupRatio + (effectiveInputTokens / 1000000) * inputRatioPrice * groupRatio +
@@ -698,56 +784,78 @@ export function renderClaudeModelPrice(
return ( return (
<> <>
<article> <article>
<p>{i18next.t('提示价格:${{price}} / 1M tokens', { <p>
price: inputRatioPrice, {i18next.t('提示价格:${{price}} / 1M tokens', {
})}</p>
<p>{i18next.t('补全价格:${{price}} * {{ratio}} = ${{total}} / 1M tokens', {
price: inputRatioPrice,
ratio: completionRatio,
total: completionRatioPrice
})}</p>
{cacheTokens > 0 && (
<p>{i18next.t('缓存价格:${{price}} * {{ratio}} = ${{total}} / 1M tokens (缓存倍率: {{cacheRatio}})', {
price: inputRatioPrice, price: inputRatioPrice,
ratio: cacheRatio, })}
total: cacheRatioPrice, </p>
cacheRatio: cacheRatio <p>
})}</p> {i18next.t(
'补全价格:${{price}} * {{ratio}} = ${{total}} / 1M tokens',
{
price: inputRatioPrice,
ratio: completionRatio,
total: completionRatioPrice,
},
)}
</p>
{cacheTokens > 0 && (
<p>
{i18next.t(
'缓存价格:${{price}} * {{ratio}} = ${{total}} / 1M tokens (缓存倍率: {{cacheRatio}})',
{
price: inputRatioPrice,
ratio: cacheRatio,
total: cacheRatioPrice,
cacheRatio: cacheRatio,
},
)}
</p>
)} )}
{cacheCreationTokens > 0 && ( {cacheCreationTokens > 0 && (
<p>{i18next.t('缓存创建价格:${{price}} * {{ratio}} = ${{total}} / 1M tokens (缓存创建倍率: {{cacheCreationRatio}})', { <p>
price: inputRatioPrice, {i18next.t(
ratio: cacheCreationRatio, '缓存创建价格:${{price}} * {{ratio}} = ${{total}} / 1M tokens (缓存创建倍率: {{cacheCreationRatio}})',
total: cacheCreationRatioPrice, {
cacheCreationRatio: cacheCreationRatio price: inputRatioPrice,
})}</p> ratio: cacheCreationRatio,
total: cacheCreationRatioPrice,
cacheCreationRatio: cacheCreationRatio,
},
)}
</p>
)} )}
<p></p> <p></p>
<p> <p>
{(cacheTokens > 0 || cacheCreationTokens > 0) ? {cacheTokens > 0 || cacheCreationTokens > 0
i18next.t('提示 {{nonCacheInput}} tokens / 1M tokens * ${{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * ${{cachePrice}} + 缓存创建 {{cacheCreationInput}} tokens / 1M tokens * ${{cacheCreationPrice}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}', { ? i18next.t(
nonCacheInput: nonCachedTokens, '提示 {{nonCacheInput}} tokens / 1M tokens * ${{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * ${{cachePrice}} + 缓存创建 {{cacheCreationInput}} tokens / 1M tokens * ${{cacheCreationPrice}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}',
cacheInput: cacheTokens, {
cacheRatio: cacheRatio, nonCacheInput: nonCachedTokens,
cacheCreationInput: cacheCreationTokens, cacheInput: cacheTokens,
cacheCreationRatio: cacheCreationRatio, cacheRatio: cacheRatio,
cachePrice: cacheRatioPrice, cacheCreationInput: cacheCreationTokens,
cacheCreationPrice: cacheCreationRatioPrice, cacheCreationRatio: cacheCreationRatio,
price: inputRatioPrice, cachePrice: cacheRatioPrice,
completion: completionTokens, cacheCreationPrice: cacheCreationRatioPrice,
compPrice: completionRatioPrice, price: inputRatioPrice,
ratio: groupRatio, completion: completionTokens,
total: price.toFixed(6) compPrice: completionRatioPrice,
}) : ratio: groupRatio,
i18next.t('提示 {{input}} tokens / 1M tokens * ${{price}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}', { total: price.toFixed(6),
input: inputTokens, },
price: inputRatioPrice, )
completion: completionTokens, : i18next.t(
compPrice: completionRatioPrice, '提示 {{input}} tokens / 1M tokens * ${{price}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}',
ratio: groupRatio, {
total: price.toFixed(6) input: inputTokens,
}) price: inputRatioPrice,
} completion: completionTokens,
compPrice: completionRatioPrice,
ratio: groupRatio,
total: price.toFixed(6),
},
)}
</p> </p>
<p>{i18next.t('仅供参考,以实际扣费为准')}</p> <p>{i18next.t('仅供参考,以实际扣费为准')}</p>
</article> </article>
@@ -770,17 +878,20 @@ export function renderClaudeLogContent(
return i18next.t('模型价格 ${{price}}{{ratioType}} {{ratio}}', { return i18next.t('模型价格 ${{price}}{{ratioType}} {{ratio}}', {
price: modelPrice, price: modelPrice,
ratioType: ratioLabel, ratioType: ratioLabel,
ratio: groupRatio ratio: groupRatio,
}); });
} else { } else {
return i18next.t('模型倍率 {{modelRatio}},补全倍率 {{completionRatio}},缓存倍率 {{cacheRatio}},缓存创建倍率 {{cacheCreationRatio}}{{ratioType}} {{ratio}}', { return i18next.t(
modelRatio: modelRatio, '模型倍率 {{modelRatio}},补全倍率 {{completionRatio}},缓存倍率 {{cacheRatio}},缓存创建倍率 {{cacheCreationRatio}}{{ratioType}} {{ratio}}',
completionRatio: completionRatio, {
cacheRatio: cacheRatio, modelRatio: modelRatio,
cacheCreationRatio: cacheCreationRatio, completionRatio: completionRatio,
ratioType: ratioLabel, cacheRatio: cacheRatio,
ratio: groupRatio cacheCreationRatio: cacheCreationRatio,
}); ratioType: ratioLabel,
ratio: groupRatio,
},
);
} }
} }
@@ -799,22 +910,25 @@ export function renderClaudeModelPriceSimple(
return i18next.t('价格:${{price}} * {{ratioType}}{{ratio}}', { return i18next.t('价格:${{price}} * {{ratioType}}{{ratio}}', {
price: modelPrice, price: modelPrice,
ratioType: ratioLabel, ratioType: ratioLabel,
ratio: groupRatio ratio: groupRatio,
}); });
} else { } else {
if (cacheTokens !== 0 || cacheCreationTokens !== 0) { if (cacheTokens !== 0 || cacheCreationTokens !== 0) {
return i18next.t('模型: {{ratio}} * {{ratioType}}: {{groupRatio}} * 缓存: {{cacheRatio}}', { return i18next.t(
ratio: modelRatio, '模型: {{ratio}} * {{ratioType}}: {{groupRatio}} * 缓存: {{cacheRatio}}',
ratioType: ratioLabel, {
groupRatio: groupRatio, ratio: modelRatio,
cacheRatio: cacheRatio, ratioType: ratioLabel,
cacheCreationRatio: cacheCreationRatio groupRatio: groupRatio,
}); cacheRatio: cacheRatio,
cacheCreationRatio: cacheCreationRatio,
},
);
} else { } else {
return i18next.t('模型: {{ratio}} * {{ratioType}}: {{groupRatio}}', { return i18next.t('模型: {{ratio}} * {{ratioType}}: {{groupRatio}}', {
ratio: modelRatio, ratio: modelRatio,
ratioType: ratioLabel, ratioType: ratioLabel,
groupRatio: groupRatio groupRatio: groupRatio,
}); });
} }
} }
@@ -824,7 +938,7 @@ export function renderLogContent(
modelRatio, modelRatio,
completionRatio, completionRatio,
modelPrice = -1, modelPrice = -1,
groupRatio groupRatio,
) { ) {
const ratioLabel = false ? i18next.t('专属倍率') : i18next.t('分组倍率'); const ratioLabel = false ? i18next.t('专属倍率') : i18next.t('分组倍率');
@@ -832,14 +946,17 @@ export function renderLogContent(
return i18next.t('模型价格 ${{price}}{{ratioType}} {{ratio}}', { return i18next.t('模型价格 ${{price}}{{ratioType}} {{ratio}}', {
price: modelPrice, price: modelPrice,
ratioType: ratioLabel, ratioType: ratioLabel,
ratio: groupRatio ratio: groupRatio,
}); });
} else { } else {
return i18next.t('模型倍率 {{modelRatio}},补全倍率 {{completionRatio}}{{ratioType}} {{ratio}}', { return i18next.t(
modelRatio: modelRatio, '模型倍率 {{modelRatio}},补全倍率 {{completionRatio}}{{ratioType}} {{ratio}}',
completionRatio: completionRatio, {
ratioType: ratioLabel, modelRatio: modelRatio,
ratio: groupRatio completionRatio: completionRatio,
}); ratioType: ratioLabel,
ratio: groupRatio,
},
);
} }
} }

View File

@@ -51,11 +51,11 @@ export async function copy(text) {
} catch (e) { } catch (e) {
try { try {
// 构建input 执行 复制命令 // 构建input 执行 复制命令
var _input = window.document.createElement("input"); var _input = window.document.createElement('input');
_input.value = text; _input.value = text;
window.document.body.appendChild(_input); window.document.body.appendChild(_input);
_input.select(); _input.select();
window.document.execCommand("Copy"); window.document.execCommand('Copy');
window.document.body.removeChild(_input); window.document.body.removeChild(_input);
} catch (e) { } catch (e) {
okay = false; okay = false;
@@ -191,7 +191,7 @@ export function timestamp2string1(timestamp, dataExportDefaultTime = 'hour') {
let day = date.getDate().toString(); let day = date.getDate().toString();
let hour = date.getHours().toString(); let hour = date.getHours().toString();
if (day === '24') { if (day === '24') {
console.log("timestamp", timestamp); console.log('timestamp', timestamp);
} }
if (month.length === 1) { if (month.length === 1) {
month = '0' + month; month = '0' + month;
@@ -247,7 +247,6 @@ export function verifyJSONPromise(value) {
} }
} }
export function shouldShowPrompt(id) { export function shouldShowPrompt(id) {
let prompt = localStorage.getItem(`prompt-${id}`); let prompt = localStorage.getItem(`prompt-${id}`);
return !prompt; return !prompt;

View File

@@ -11,16 +11,16 @@ i18n
.init({ .init({
resources: { resources: {
en: { en: {
translation: enTranslation translation: enTranslation,
}, },
zh: { zh: {
translation: zhTranslation translation: zhTranslation,
} },
}, },
fallbackLng: 'zh', fallbackLng: 'zh',
interpolation: { interpolation: {
escapeValue: false escapeValue: false,
} },
}); });
export default i18n; export default i18n;

View File

@@ -1,8 +1,8 @@
body { body {
margin: 0; margin: 0;
padding-top: 0; padding-top: 0;
font-family: Lato, 'Helvetica Neue', Arial, Helvetica, 'Microsoft YaHei', font-family:
sans-serif; Lato, 'Helvetica Neue', Arial, Helvetica, 'Microsoft YaHei', sans-serif;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
scrollbar-width: none; scrollbar-width: none;
@@ -18,7 +18,20 @@ body {
overflow: hidden; overflow: hidden;
} }
#root > section > header > section > div > div > div > div.semi-navigation-header-list-outer > div.semi-navigation-list-wrapper > ul > div > a > li > span{ #root
> section
> header
> section
> div
> div
> div
> div.semi-navigation-header-list-outer
> div.semi-navigation-list-wrapper
> ul
> div
> a
> li
> span {
font-weight: 600 !important; font-weight: 600 !important;
} }
@@ -44,13 +57,45 @@ body {
overflow-x: auto; overflow-x: auto;
scrollbar-width: none; scrollbar-width: none;
} }
#root > section > header > section > div > div > div > div.semi-navigation-footer > div > a > li { #root
> section
> header
> section
> div
> div
> div
> div.semi-navigation-footer
> div
> a
> li {
padding: 0 0; padding: 0 0;
} }
#root > section > header > section > div > div > div > div.semi-navigation-header-list-outer > div.semi-navigation-list-wrapper > ul > div > a > li { #root
> section
> header
> section
> div
> div
> div
> div.semi-navigation-header-list-outer
> div.semi-navigation-list-wrapper
> ul
> div
> a
> li {
padding: 0 5px; padding: 0 5px;
} }
#root > section > header > section > div > div > div > div.semi-navigation-footer > div:nth-child(1) > a > li { #root
> section
> header
> section
> div
> div
> div
> div.semi-navigation-footer
> div:nth-child(1)
> a
> li {
padding: 0 5px; padding: 0 5px;
} }
.semi-navigation-footer { .semi-navigation-footer {
@@ -147,8 +192,8 @@ body::-webkit-scrollbar {
} }
code { code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', font-family:
monospace; source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
} }
.semi-navigation-item { .semi-navigation-item {

View File

@@ -28,7 +28,7 @@ root.render(
<BrowserRouter> <BrowserRouter>
<ThemeProvider> <ThemeProvider>
<StyleProvider> <StyleProvider>
<PageLayout/> <PageLayout />
</StyleProvider> </StyleProvider>
</ThemeProvider> </ThemeProvider>
</BrowserRouter> </BrowserRouter>

View File

@@ -6,8 +6,9 @@ import {
isMobile, isMobile,
showError, showError,
showInfo, showInfo,
showSuccess, showWarning, showSuccess,
verifyJSON showWarning,
verifyJSON,
} from '../../helpers'; } from '../../helpers';
import { CHANNEL_OPTIONS } from '../../constants'; import { CHANNEL_OPTIONS } from '../../constants';
import Title from '@douyinfe/semi-ui/lib/es/typography/title'; import Title from '@douyinfe/semi-ui/lib/es/typography/title';
@@ -22,21 +23,22 @@ import {
Select, Select,
TextArea, TextArea,
Checkbox, Checkbox,
Banner, Modal Banner,
Modal,
} from '@douyinfe/semi-ui'; } from '@douyinfe/semi-ui';
import { getChannelModels, loadChannelModels } from '../../components/utils.js'; import { getChannelModels, loadChannelModels } from '../../components/utils.js';
const MODEL_MAPPING_EXAMPLE = { const MODEL_MAPPING_EXAMPLE = {
'gpt-3.5-turbo': 'gpt-3.5-turbo-0125' 'gpt-3.5-turbo': 'gpt-3.5-turbo-0125',
}; };
const STATUS_CODE_MAPPING_EXAMPLE = { const STATUS_CODE_MAPPING_EXAMPLE = {
400: '500' 400: '500',
}; };
const REGION_EXAMPLE = { const REGION_EXAMPLE = {
'default': 'us-central1', default: 'us-central1',
'claude-3-5-sonnet-20240620': 'europe-west1' 'claude-3-5-sonnet-20240620': 'europe-west1',
}; };
function type2secretPrompt(type) { function type2secretPrompt(type) {
@@ -82,7 +84,7 @@ const EditChannel = (props) => {
groups: ['default'], groups: ['default'],
priority: 0, priority: 0,
weight: 0, weight: 0,
tag: '' tag: '',
}; };
const [batch, setBatch] = useState(false); const [batch, setBatch] = useState(false);
const [autoBan, setAutoBan] = useState(true); const [autoBan, setAutoBan] = useState(true);
@@ -98,12 +100,13 @@ const EditChannel = (props) => {
if (name === 'base_url' && value.endsWith('/v1')) { if (name === 'base_url' && value.endsWith('/v1')) {
Modal.confirm({ Modal.confirm({
title: '警告', title: '警告',
content: '不需要在末尾加/v1New API会自动处理添加后可能导致请求失败是否继续', content:
'不需要在末尾加/v1New API会自动处理添加后可能导致请求失败是否继续',
onOk: () => { onOk: () => {
setInputs((inputs) => ({ ...inputs, [name]: value })); setInputs((inputs) => ({ ...inputs, [name]: value }));
} },
}) });
return return;
} }
setInputs((inputs) => ({ ...inputs, [name]: value })); setInputs((inputs) => ({ ...inputs, [name]: value }));
if (name === 'type') { if (name === 'type') {
@@ -117,7 +120,7 @@ const EditChannel = (props) => {
'mj_blend', 'mj_blend',
'mj_upscale', 'mj_upscale',
'mj_describe', 'mj_describe',
'mj_uploads' 'mj_uploads',
]; ];
break; break;
case 5: case 5:
@@ -137,14 +140,11 @@ const EditChannel = (props) => {
'mj_high_variation', 'mj_high_variation',
'mj_low_variation', 'mj_low_variation',
'mj_pan', 'mj_pan',
'mj_uploads' 'mj_uploads',
]; ];
break; break;
case 36: case 36:
localModels = [ localModels = ['suno_music', 'suno_lyrics'];
'suno_music',
'suno_lyrics'
];
break; break;
default: default:
localModels = getChannelModels(value); localModels = getChannelModels(value);
@@ -180,7 +180,7 @@ const EditChannel = (props) => {
data.model_mapping = JSON.stringify( data.model_mapping = JSON.stringify(
JSON.parse(data.model_mapping), JSON.parse(data.model_mapping),
null, null,
2 2,
); );
} }
setInputs(data); setInputs(data);
@@ -197,7 +197,6 @@ const EditChannel = (props) => {
setLoading(false); setLoading(false);
}; };
const fetchUpstreamModelList = async (name) => { const fetchUpstreamModelList = async (name) => {
// if (inputs['type'] !== 1) { // if (inputs['type'] !== 1) {
// showError(t('仅支持 OpenAI 接口格式')); // showError(t('仅支持 OpenAI 接口格式'));
@@ -225,7 +224,7 @@ const EditChannel = (props) => {
const res = await API.post('/api/channel/fetch_models', { const res = await API.post('/api/channel/fetch_models', {
base_url: inputs['base_url'], base_url: inputs['base_url'],
type: inputs['type'], type: inputs['type'],
key: inputs['key'] key: inputs['key'],
}); });
if (res.data && res.data.success) { if (res.data && res.data.success) {
@@ -254,7 +253,7 @@ const EditChannel = (props) => {
let res = await API.get(`/api/channel/models`); let res = await API.get(`/api/channel/models`);
let localModelOptions = res.data.data.map((model) => ({ let localModelOptions = res.data.data.map((model) => ({
label: model.id, label: model.id,
value: model.id value: model.id,
})); }));
setOriginModelOptions(localModelOptions); setOriginModelOptions(localModelOptions);
setFullModels(res.data.data.map((model) => model.id)); setFullModels(res.data.data.map((model) => model.id));
@@ -263,7 +262,7 @@ const EditChannel = (props) => {
.filter((model) => { .filter((model) => {
return model.id.startsWith('gpt-') || model.id.startsWith('text-'); return model.id.startsWith('gpt-') || model.id.startsWith('text-');
}) })
.map((model) => model.id) .map((model) => model.id),
); );
} catch (error) { } catch (error) {
showError(error.message); showError(error.message);
@@ -279,8 +278,8 @@ const EditChannel = (props) => {
setGroupOptions( setGroupOptions(
res.data.data.map((group) => ({ res.data.data.map((group) => ({
label: group, label: group,
value: group value: group,
})) })),
); );
} catch (error) { } catch (error) {
showError(error.message); showError(error.message);
@@ -293,7 +292,7 @@ const EditChannel = (props) => {
if (!localModelOptions.find((option) => option.label === model)) { if (!localModelOptions.find((option) => option.label === model)) {
localModelOptions.push({ localModelOptions.push({
label: model, label: model,
value: model value: model,
}); });
} }
}); });
@@ -330,7 +329,7 @@ const EditChannel = (props) => {
if (localInputs.base_url && localInputs.base_url.endsWith('/')) { if (localInputs.base_url && localInputs.base_url.endsWith('/')) {
localInputs.base_url = localInputs.base_url.slice( localInputs.base_url = localInputs.base_url.slice(
0, 0,
localInputs.base_url.length - 1 localInputs.base_url.length - 1,
); );
} }
if (localInputs.type === 18 && localInputs.other === '') { if (localInputs.type === 18 && localInputs.other === '') {
@@ -348,7 +347,7 @@ const EditChannel = (props) => {
if (isEdit) { if (isEdit) {
res = await API.put(`/api/channel/`, { res = await API.put(`/api/channel/`, {
...localInputs, ...localInputs,
id: parseInt(channelId) id: parseInt(channelId),
}); });
} else { } else {
res = await API.post(`/api/channel/`, localInputs); res = await API.post(`/api/channel/`, localInputs);
@@ -382,7 +381,7 @@ const EditChannel = (props) => {
localModelOptions.push({ localModelOptions.push({
key: model, key: model,
text: model, text: model,
value: model value: model,
}); });
} else if (model) { } else if (model) {
showError(t('某些模型已存在!')); showError(t('某些模型已存在!'));
@@ -397,14 +396,15 @@ const EditChannel = (props) => {
handleInputChange('models', localModels); handleInputChange('models', localModels);
}; };
return ( return (
<> <>
<SideSheet <SideSheet
maskClosable={false} maskClosable={false}
placement={isEdit ? 'right' : 'left'} placement={isEdit ? 'right' : 'left'}
title={ title={
<Title level={3}>{isEdit ? t('更新渠道信息') : t('创建新的渠道')}</Title> <Title level={3}>
{isEdit ? t('更新渠道信息') : t('创建新的渠道')}
</Title>
} }
headerStyle={{ borderBottom: '1px solid var(--semi-color-border)' }} headerStyle={{ borderBottom: '1px solid var(--semi-color-border)' }}
bodyStyle={{ borderBottom: '1px solid var(--semi-color-border)' }} bodyStyle={{ borderBottom: '1px solid var(--semi-color-border)' }}
@@ -412,11 +412,11 @@ const EditChannel = (props) => {
footer={ footer={
<div style={{ display: 'flex', justifyContent: 'flex-end' }}> <div style={{ display: 'flex', justifyContent: 'flex-end' }}>
<Space> <Space>
<Button theme="solid" size={'large'} onClick={submit}> <Button theme='solid' size={'large'} onClick={submit}>
{t('提交')} {t('提交')}
</Button> </Button>
<Button <Button
theme="solid" theme='solid'
size={'large'} size={'large'}
type={'tertiary'} type={'tertiary'}
onClick={handleCancel} onClick={handleCancel}
@@ -432,11 +432,10 @@ const EditChannel = (props) => {
> >
<Spin spinning={loading}> <Spin spinning={loading}>
<div style={{ marginTop: 10 }}> <div style={{ marginTop: 10 }}>
<Typography.Text strong>{t('类型')}</Typography.Text> <Typography.Text strong>{t('类型')}</Typography.Text>
</div> </div>
<Select <Select
name="type" name='type'
required required
optionList={CHANNEL_OPTIONS} optionList={CHANNEL_OPTIONS}
value={inputs.type} value={inputs.type}
@@ -449,17 +448,17 @@ const EditChannel = (props) => {
{inputs.type === 40 && ( {inputs.type === 40 && (
<div style={{ marginTop: 10 }}> <div style={{ marginTop: 10 }}>
<Banner <Banner
type="info" type='info'
description={ description={
<div> <div>
<Typography.Text strong> <Typography.Text strong>{t('邀请链接')}:</Typography.Text>
{t('邀请链接')}:
</Typography.Text>
<Typography.Text <Typography.Text
link link
underline underline
style={{marginLeft: 8}} style={{ marginLeft: 8 }}
onClick={() => window.open('https://cloud.siliconflow.cn/i/hij0YNTZ')} onClick={() =>
window.open('https://cloud.siliconflow.cn/i/hij0YNTZ')
}
> >
https://cloud.siliconflow.cn/i/hij0YNTZ https://cloud.siliconflow.cn/i/hij0YNTZ
</Typography.Text> </Typography.Text>
@@ -482,27 +481,29 @@ const EditChannel = (props) => {
</Typography.Text> </Typography.Text>
</div> </div>
<Input <Input
label="AZURE_OPENAI_ENDPOINT" label='AZURE_OPENAI_ENDPOINT'
name="azure_base_url" name='azure_base_url'
placeholder={t('请输入 AZURE_OPENAI_ENDPOINT例如https://docs-test-001.openai.azure.com')} placeholder={t(
'请输入 AZURE_OPENAI_ENDPOINT例如https://docs-test-001.openai.azure.com',
)}
onChange={(value) => { onChange={(value) => {
handleInputChange('base_url', value); handleInputChange('base_url', value);
}} }}
value={inputs.base_url} value={inputs.base_url}
autoComplete="new-password" autoComplete='new-password'
/> />
<div style={{ marginTop: 10 }}> <div style={{ marginTop: 10 }}>
<Typography.Text strong>{t('默认 API 版本')}</Typography.Text> <Typography.Text strong>{t('默认 API 版本')}</Typography.Text>
</div> </div>
<Input <Input
label={t('默认 API 版本')} label={t('默认 API 版本')}
name="azure_other" name='azure_other'
placeholder={t('请输入默认 API 版本例如2024-12-01-preview')} placeholder={t('请输入默认 API 版本例如2024-12-01-preview')}
onChange={(value) => { onChange={(value) => {
handleInputChange('other', value); handleInputChange('other', value);
}} }}
value={inputs.other} value={inputs.other}
autoComplete="new-password" autoComplete='new-password'
/> />
</> </>
)} )}
@@ -511,7 +512,9 @@ const EditChannel = (props) => {
<div style={{ marginTop: 10 }}> <div style={{ marginTop: 10 }}>
<Banner <Banner
type={'warning'} type={'warning'}
description={t('如果你对接的是上游One API或者New API等转发项目请使用OpenAI类型不要使用此类型除非你知道你在做什么。')} description={t(
'如果你对接的是上游One API或者New API等转发项目请使用OpenAI类型不要使用此类型除非你知道你在做什么。',
)}
></Banner> ></Banner>
</div> </div>
<div style={{ marginTop: 10 }}> <div style={{ marginTop: 10 }}>
@@ -520,13 +523,15 @@ const EditChannel = (props) => {
</Typography.Text> </Typography.Text>
</div> </div>
<Input <Input
name="base_url" name='base_url'
placeholder={t('请输入完整的URL例如https://api.openai.com/v1/chat/completions')} placeholder={t(
'请输入完整的URL例如https://api.openai.com/v1/chat/completions',
)}
onChange={(value) => { onChange={(value) => {
handleInputChange('base_url', value); handleInputChange('base_url', value);
}} }}
value={inputs.base_url} value={inputs.base_url}
autoComplete="new-password" autoComplete='new-password'
/> />
</> </>
)} )}
@@ -535,7 +540,9 @@ const EditChannel = (props) => {
<div style={{ marginTop: 10 }}> <div style={{ marginTop: 10 }}>
<Banner <Banner
type={'warning'} type={'warning'}
description={t('Dify渠道只适配chatflow和agent并且agent不支持图片')} description={t(
'Dify渠道只适配chatflow和agent并且agent不支持图片',
)}
></Banner> ></Banner>
</div> </div>
</> </>
@@ -545,40 +552,50 @@ const EditChannel = (props) => {
</div> </div>
<Input <Input
required required
name="name" name='name'
placeholder={t('请为渠道命名')} placeholder={t('请为渠道命名')}
onChange={(value) => { onChange={(value) => {
handleInputChange('name', value); handleInputChange('name', value);
}} }}
value={inputs.name} value={inputs.name}
autoComplete="new-password" autoComplete='new-password'
/> />
{inputs.type !== 3 && inputs.type !== 8 && inputs.type !== 22 && inputs.type !== 36 && inputs.type !== 45 && ( {inputs.type !== 3 &&
<> inputs.type !== 8 &&
<div style={{ marginTop: 10 }}> inputs.type !== 22 &&
<Typography.Text strong>{t('代理站地址')}</Typography.Text> inputs.type !== 36 &&
</div> inputs.type !== 45 && (
<Tooltip content={t('对于官方渠道new-api已经内置地址除非是第三方代理站点或者Azure的特殊接入地址否则不需要填写')}> <>
<Input <div style={{ marginTop: 10 }}>
label={t('代理站地址')} <Typography.Text strong>{t('代理站地址')}</Typography.Text>
name="base_url" </div>
placeholder={t('此项可选,用于通过代理站来进行 API 调用,末尾不要带/v1和/')} <Tooltip
onChange={(value) => { content={t(
handleInputChange('base_url', value); '对于官方渠道new-api已经内置地址除非是第三方代理站点或者Azure的特殊接入地址否则不需要填写',
}} )}
value={inputs.base_url} >
autoComplete="new-password" <Input
/> label={t('代理站地址')}
</Tooltip> name='base_url'
</> placeholder={t(
)} '此项可选,用于通过代理站来进行 API 调用,末尾不要带/v1和/',
)}
onChange={(value) => {
handleInputChange('base_url', value);
}}
value={inputs.base_url}
autoComplete='new-password'
/>
</Tooltip>
</>
)}
<div style={{ marginTop: 10 }}> <div style={{ marginTop: 10 }}>
<Typography.Text strong>{t('密钥')}</Typography.Text> <Typography.Text strong>{t('密钥')}</Typography.Text>
</div> </div>
{batch ? ( {batch ? (
<TextArea <TextArea
label={t('密钥')} label={t('密钥')}
name="key" name='key'
required required
placeholder={t('请输入密钥,一行一个')} placeholder={t('请输入密钥,一行一个')}
onChange={(value) => { onChange={(value) => {
@@ -586,16 +603,17 @@ const EditChannel = (props) => {
}} }}
value={inputs.key} value={inputs.key}
style={{ minHeight: 150, fontFamily: 'JetBrains Mono, Consolas' }} style={{ minHeight: 150, fontFamily: 'JetBrains Mono, Consolas' }}
autoComplete="new-password" autoComplete='new-password'
/> />
) : ( ) : (
<> <>
{inputs.type === 41 ? ( {inputs.type === 41 ? (
<TextArea <TextArea
label={t('鉴权json')} label={t('鉴权json')}
name="key" name='key'
required required
placeholder={'{\n' + placeholder={
'{\n' +
' "type": "service_account",\n' + ' "type": "service_account",\n' +
' "project_id": "abc-bcd-123-456",\n' + ' "project_id": "abc-bcd-123-456",\n' +
' "private_key_id": "123xxxxx456",\n' + ' "private_key_id": "123xxxxx456",\n' +
@@ -607,25 +625,26 @@ const EditChannel = (props) => {
' "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",\n' + ' "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",\n' +
' "client_x509_cert_url": "https://xxxxx.gserviceaccount.com",\n' + ' "client_x509_cert_url": "https://xxxxx.gserviceaccount.com",\n' +
' "universe_domain": "googleapis.com"\n' + ' "universe_domain": "googleapis.com"\n' +
'}'} '}'
}
onChange={(value) => { onChange={(value) => {
handleInputChange('key', value); handleInputChange('key', value);
}} }}
autosize={{ minRows: 10 }} autosize={{ minRows: 10 }}
value={inputs.key} value={inputs.key}
autoComplete="new-password" autoComplete='new-password'
/> />
) : ( ) : (
<Input <Input
label={t('密钥')} label={t('密钥')}
name="key" name='key'
required required
placeholder={t(type2secretPrompt(inputs.type))} placeholder={t(type2secretPrompt(inputs.type))}
onChange={(value) => { onChange={(value) => {
handleInputChange('key', value); handleInputChange('key', value);
}} }}
value={inputs.key} value={inputs.key}
autoComplete="new-password" autoComplete='new-password'
/> />
)} )}
</> </>
@@ -636,7 +655,7 @@ const EditChannel = (props) => {
<Checkbox <Checkbox
checked={batch} checked={batch}
label={t('批量创建')} label={t('批量创建')}
name="batch" name='batch'
onChange={() => setBatch(!batch)} onChange={() => setBatch(!batch)}
/> />
<Typography.Text strong>{t('批量创建')}</Typography.Text> <Typography.Text strong>{t('批量创建')}</Typography.Text>
@@ -649,13 +668,15 @@ const EditChannel = (props) => {
<Typography.Text strong>{t('私有部署地址')}</Typography.Text> <Typography.Text strong>{t('私有部署地址')}</Typography.Text>
</div> </div>
<Input <Input
name="base_url" name='base_url'
placeholder={t('请输入私有部署地址格式为https://fastgpt.run/api/openapi')} placeholder={t(
'请输入私有部署地址格式为https://fastgpt.run/api/openapi',
)}
onChange={(value) => { onChange={(value) => {
handleInputChange('base_url', value); handleInputChange('base_url', value);
}} }}
value={inputs.base_url} value={inputs.base_url}
autoComplete="new-password" autoComplete='new-password'
/> />
</> </>
)} )}
@@ -663,17 +684,21 @@ const EditChannel = (props) => {
<> <>
<div style={{ marginTop: 10 }}> <div style={{ marginTop: 10 }}>
<Typography.Text strong> <Typography.Text strong>
{t('注意非Chat API请务必填写正确的API地址否则可能导致无法使用')} {t(
'注意非Chat API请务必填写正确的API地址否则可能导致无法使用',
)}
</Typography.Text> </Typography.Text>
</div> </div>
<Input <Input
name="base_url" name='base_url'
placeholder={t('请输入到 /suno 前的路径通常就是域名例如https://api.example.com')} placeholder={t(
'请输入到 /suno 前的路径通常就是域名例如https://api.example.com',
)}
onChange={(value) => { onChange={(value) => {
handleInputChange('base_url', value); handleInputChange('base_url', value);
}} }}
value={inputs.base_url} value={inputs.base_url}
autoComplete="new-password" autoComplete='new-password'
/> />
</> </>
)} )}
@@ -682,7 +707,7 @@ const EditChannel = (props) => {
</div> </div>
<Select <Select
placeholder={t('请选择可以使用该渠道的分组')} placeholder={t('请选择可以使用该渠道的分组')}
name="groups" name='groups'
required required
multiple multiple
selection selection
@@ -692,7 +717,7 @@ const EditChannel = (props) => {
handleInputChange('groups', value); handleInputChange('groups', value);
}} }}
value={inputs.groups} value={inputs.groups}
autoComplete="new-password" autoComplete='new-password'
optionList={groupOptions} optionList={groupOptions}
/> />
{inputs.type === 18 && ( {inputs.type === 18 && (
@@ -701,7 +726,7 @@ const EditChannel = (props) => {
<Typography.Text strong>模型版本</Typography.Text> <Typography.Text strong>模型版本</Typography.Text>
</div> </div>
<Input <Input
name="other" name='other'
placeholder={ placeholder={
'请输入星火大模型版本注意是接口地址中的版本号例如v2.1' '请输入星火大模型版本注意是接口地址中的版本号例如v2.1'
} }
@@ -709,7 +734,7 @@ const EditChannel = (props) => {
handleInputChange('other', value); handleInputChange('other', value);
}} }}
value={inputs.other} value={inputs.other}
autoComplete="new-password" autoComplete='new-password'
/> />
</> </>
)} )}
@@ -719,29 +744,31 @@ const EditChannel = (props) => {
<Typography.Text strong>{t('部署地区')}</Typography.Text> <Typography.Text strong>{t('部署地区')}</Typography.Text>
</div> </div>
<TextArea <TextArea
name="other" name='other'
placeholder={t('请输入部署地区例如us-central1\n支持使用模型映射格式\n' + placeholder={t(
'{\n' + '请输入部署地区例如us-central1\n支持使用模型映射格式\n' +
' "default": "us-central1",\n' + '{\n' +
' "claude-3-5-sonnet-20240620": "europe-west1"\n' + ' "default": "us-central1",\n' +
'}')} ' "claude-3-5-sonnet-20240620": "europe-west1"\n' +
'}',
)}
autosize={{ minRows: 2 }} autosize={{ minRows: 2 }}
onChange={(value) => { onChange={(value) => {
handleInputChange('other', value); handleInputChange('other', value);
}} }}
value={inputs.other} value={inputs.other}
autoComplete="new-password" autoComplete='new-password'
/> />
<Typography.Text <Typography.Text
style={{ style={{
color: 'rgba(var(--semi-blue-5), 1)', color: 'rgba(var(--semi-blue-5), 1)',
userSelect: 'none', userSelect: 'none',
cursor: 'pointer' cursor: 'pointer',
}} }}
onClick={() => { onClick={() => {
handleInputChange( handleInputChange(
'other', 'other',
JSON.stringify(REGION_EXAMPLE, null, 2) JSON.stringify(REGION_EXAMPLE, null, 2),
); );
}} }}
> >
@@ -755,14 +782,14 @@ const EditChannel = (props) => {
<Typography.Text strong>知识库 ID</Typography.Text> <Typography.Text strong>知识库 ID</Typography.Text>
</div> </div>
<Input <Input
label="知识库 ID" label='知识库 ID'
name="other" name='other'
placeholder={'请输入知识库 ID例如123456'} placeholder={'请输入知识库 ID例如123456'}
onChange={(value) => { onChange={(value) => {
handleInputChange('other', value); handleInputChange('other', value);
}} }}
value={inputs.other} value={inputs.other}
autoComplete="new-password" autoComplete='new-password'
/> />
</> </>
)} )}
@@ -772,7 +799,7 @@ const EditChannel = (props) => {
<Typography.Text strong>Account ID</Typography.Text> <Typography.Text strong>Account ID</Typography.Text>
</div> </div>
<Input <Input
name="other" name='other'
placeholder={ placeholder={
'请输入Account ID例如d6b5da8hk1awo8nap34ube6gh' '请输入Account ID例如d6b5da8hk1awo8nap34ube6gh'
} }
@@ -780,7 +807,7 @@ const EditChannel = (props) => {
handleInputChange('other', value); handleInputChange('other', value);
}} }}
value={inputs.other} value={inputs.other}
autoComplete="new-password" autoComplete='new-password'
/> />
</> </>
)} )}
@@ -789,7 +816,7 @@ const EditChannel = (props) => {
</div> </div>
<Select <Select
placeholder={'请选择该渠道所支持的模型'} placeholder={'请选择该渠道所支持的模型'}
name="models" name='models'
required required
multiple multiple
selection selection
@@ -799,13 +826,13 @@ const EditChannel = (props) => {
handleInputChange('models', value); handleInputChange('models', value);
}} }}
value={inputs.models} value={inputs.models}
autoComplete="new-password" autoComplete='new-password'
optionList={modelOptions} optionList={modelOptions}
/> />
<div style={{ lineHeight: '40px', marginBottom: '12px' }}> <div style={{ lineHeight: '40px', marginBottom: '12px' }}>
<Space> <Space>
<Button <Button
type="primary" type='primary'
onClick={() => { onClick={() => {
handleInputChange('models', basicModels); handleInputChange('models', basicModels);
}} }}
@@ -813,16 +840,20 @@ const EditChannel = (props) => {
{t('填入相关模型')} {t('填入相关模型')}
</Button> </Button>
<Button <Button
type="secondary" type='secondary'
onClick={() => { onClick={() => {
handleInputChange('models', fullModels); handleInputChange('models', fullModels);
}} }}
> >
{t('填入所有模型')} {t('填入所有模型')}
</Button> </Button>
<Tooltip content={t('新建渠道时,请求通过当前浏览器发出;编辑已有渠道,请求通过后端服务器发出')}> <Tooltip
content={t(
'新建渠道时,请求通过当前浏览器发出;编辑已有渠道,请求通过后端服务器发出',
)}
>
<Button <Button
type="tertiary" type='tertiary'
onClick={() => { onClick={() => {
fetchUpstreamModelList('models'); fetchUpstreamModelList('models');
}} }}
@@ -831,7 +862,7 @@ const EditChannel = (props) => {
</Button> </Button>
</Tooltip> </Tooltip>
<Button <Button
type="warning" type='warning'
onClick={() => { onClick={() => {
handleInputChange('models', []); handleInputChange('models', []);
}} }}
@@ -841,7 +872,7 @@ const EditChannel = (props) => {
</Space> </Space>
<Input <Input
addonAfter={ addonAfter={
<Button type="primary" onClick={addCustomModels}> <Button type='primary' onClick={addCustomModels}>
{t('填入')} {t('填入')}
</Button> </Button>
} }
@@ -856,53 +887,53 @@ const EditChannel = (props) => {
<Typography.Text strong>{t('模型重定向')}</Typography.Text> <Typography.Text strong>{t('模型重定向')}</Typography.Text>
</div> </div>
<TextArea <TextArea
placeholder={t('此项可选,用于修改请求体中的模型名称,为一个 JSON 字符串,键为请求中模型名称,值为要替换的模型名称,例如:') + `\n${JSON.stringify(MODEL_MAPPING_EXAMPLE, null, 2)}`} placeholder={
name="model_mapping" t(
'此项可选,用于修改请求体中的模型名称,为一个 JSON 字符串,键为请求中模型名称,值为要替换的模型名称,例如:',
) + `\n${JSON.stringify(MODEL_MAPPING_EXAMPLE, null, 2)}`
}
name='model_mapping'
onChange={(value) => { onChange={(value) => {
handleInputChange('model_mapping', value); handleInputChange('model_mapping', value);
}} }}
autosize autosize
value={inputs.model_mapping} value={inputs.model_mapping}
autoComplete="new-password" autoComplete='new-password'
/> />
<Typography.Text <Typography.Text
style={{ style={{
color: 'rgba(var(--semi-blue-5), 1)', color: 'rgba(var(--semi-blue-5), 1)',
userSelect: 'none', userSelect: 'none',
cursor: 'pointer' cursor: 'pointer',
}} }}
onClick={() => { onClick={() => {
handleInputChange( handleInputChange(
'model_mapping', 'model_mapping',
JSON.stringify(MODEL_MAPPING_EXAMPLE, null, 2) JSON.stringify(MODEL_MAPPING_EXAMPLE, null, 2),
); );
}} }}
> >
{t('填入模板')} {t('填入模板')}
</Typography.Text> </Typography.Text>
<div style={{ marginTop: 10 }}> <div style={{ marginTop: 10 }}>
<Typography.Text strong> <Typography.Text strong>{t('渠道标签')}</Typography.Text>
{t('渠道标签')}
</Typography.Text>
</div> </div>
<Input <Input
label={t('渠道标签')} label={t('渠道标签')}
name="tag" name='tag'
placeholder={t('渠道标签')} placeholder={t('渠道标签')}
onChange={(value) => { onChange={(value) => {
handleInputChange('tag', value); handleInputChange('tag', value);
}} }}
value={inputs.tag} value={inputs.tag}
autoComplete="new-password" autoComplete='new-password'
/> />
<div style={{ marginTop: 10 }}> <div style={{ marginTop: 10 }}>
<Typography.Text strong> <Typography.Text strong>{t('渠道优先级')}</Typography.Text>
{t('渠道优先级')}
</Typography.Text>
</div> </div>
<Input <Input
label={t('渠道优先级')} label={t('渠道优先级')}
name="priority" name='priority'
placeholder={t('渠道优先级')} placeholder={t('渠道优先级')}
onChange={(value) => { onChange={(value) => {
const number = parseInt(value); const number = parseInt(value);
@@ -913,16 +944,14 @@ const EditChannel = (props) => {
} }
}} }}
value={inputs.priority} value={inputs.priority}
autoComplete="new-password" autoComplete='new-password'
/> />
<div style={{ marginTop: 10 }}> <div style={{ marginTop: 10 }}>
<Typography.Text strong> <Typography.Text strong>{t('渠道权重')}</Typography.Text>
{t('渠道权重')}
</Typography.Text>
</div> </div>
<Input <Input
label={t('渠道权重')} label={t('渠道权重')}
name="weight" name='weight'
placeholder={t('渠道权重')} placeholder={t('渠道权重')}
onChange={(value) => { onChange={(value) => {
const number = parseInt(value); const number = parseInt(value);
@@ -933,37 +962,43 @@ const EditChannel = (props) => {
} }
}} }}
value={inputs.weight} value={inputs.weight}
autoComplete="new-password" autoComplete='new-password'
/> />
<> <>
<div style={{ marginTop: 10 }}> <div style={{ marginTop: 10 }}>
<Typography.Text strong> <Typography.Text strong>{t('渠道额外设置')}</Typography.Text>
{t('渠道额外设置')}
</Typography.Text>
</div> </div>
<TextArea <TextArea
placeholder={t('此项可选,用于配置渠道特定设置,为一个 JSON 字符串,例如:') + '\n{\n "force_format": true\n}'} placeholder={
name="setting" t(
'此项可选,用于配置渠道特定设置,为一个 JSON 字符串,例如:',
) + '\n{\n "force_format": true\n}'
}
name='setting'
onChange={(value) => { onChange={(value) => {
handleInputChange('setting', value); handleInputChange('setting', value);
}} }}
autosize autosize
value={inputs.setting} value={inputs.setting}
autoComplete="new-password" autoComplete='new-password'
/> />
<Space> <Space>
<Typography.Text <Typography.Text
style={{ style={{
color: 'rgba(var(--semi-blue-5), 1)', color: 'rgba(var(--semi-blue-5), 1)',
userSelect: 'none', userSelect: 'none',
cursor: 'pointer' cursor: 'pointer',
}} }}
onClick={() => { onClick={() => {
handleInputChange( handleInputChange(
'setting', 'setting',
JSON.stringify({ JSON.stringify(
force_format: true {
}, null, 2) force_format: true,
},
null,
2,
),
); );
}} }}
> >
@@ -973,10 +1008,12 @@ const EditChannel = (props) => {
style={{ style={{
color: 'rgba(var(--semi-blue-5), 1)', color: 'rgba(var(--semi-blue-5), 1)',
userSelect: 'none', userSelect: 'none',
cursor: 'pointer' cursor: 'pointer',
}} }}
onClick={() => { onClick={() => {
window.open('https://github.com/Calcium-Ion/new-api/blob/main/docs/channel/other_setting.md'); window.open(
'https://github.com/Calcium-Ion/new-api/blob/main/docs/channel/other_setting.md',
);
}} }}
> >
{t('设置说明')} {t('设置说明')}
@@ -985,19 +1022,21 @@ const EditChannel = (props) => {
</> </>
<> <>
<div style={{ marginTop: 10 }}> <div style={{ marginTop: 10 }}>
<Typography.Text strong> <Typography.Text strong>{t('参数覆盖')}</Typography.Text>
{t('参数覆盖')}
</Typography.Text>
</div> </div>
<TextArea <TextArea
placeholder={t('此项可选,用于覆盖请求参数。不支持覆盖 stream 参数。为一个 JSON 字符串,例如:') + '\n{\n "temperature": 0\n}'} placeholder={
name="setting" t(
'此项可选,用于覆盖请求参数。不支持覆盖 stream 参数。为一个 JSON 字符串,例如:',
) + '\n{\n "temperature": 0\n}'
}
name='setting'
onChange={(value) => { onChange={(value) => {
handleInputChange('param_override', value); handleInputChange('param_override', value);
}} }}
autosize autosize
value={inputs.param_override} value={inputs.param_override}
autoComplete="new-password" autoComplete='new-password'
/> />
</> </>
{inputs.type === 1 && ( {inputs.type === 1 && (
@@ -1007,7 +1046,7 @@ const EditChannel = (props) => {
</div> </div>
<Input <Input
label={t('组织,可选,不填则为默认组织')} label={t('组织,可选,不填则为默认组织')}
name="openai_organization" name='openai_organization'
placeholder={t('请输入组织org-xxx')} placeholder={t('请输入组织org-xxx')}
onChange={(value) => { onChange={(value) => {
handleInputChange('openai_organization', value); handleInputChange('openai_organization', value);
@@ -1020,7 +1059,7 @@ const EditChannel = (props) => {
<Typography.Text strong>{t('默认测试模型')}</Typography.Text> <Typography.Text strong>{t('默认测试模型')}</Typography.Text>
</div> </div>
<Input <Input
name="test_model" name='test_model'
placeholder={t('不填则为模型列表第一个')} placeholder={t('不填则为模型列表第一个')}
onChange={(value) => { onChange={(value) => {
handleInputChange('test_model', value); handleInputChange('test_model', value);
@@ -1030,14 +1069,16 @@ const EditChannel = (props) => {
<div style={{ marginTop: 10, display: 'flex' }}> <div style={{ marginTop: 10, display: 'flex' }}>
<Space> <Space>
<Checkbox <Checkbox
name="auto_ban" name='auto_ban'
checked={autoBan} checked={autoBan}
onChange={() => { onChange={() => {
setAutoBan(!autoBan); setAutoBan(!autoBan);
}} }}
/> />
<Typography.Text strong> <Typography.Text strong>
{t('是否自动禁用(仅当自动禁用开启时有效),关闭后不会自动禁用该渠道:')} {t(
'是否自动禁用(仅当自动禁用开启时有效),关闭后不会自动禁用该渠道:',
)}
</Typography.Text> </Typography.Text>
</Space> </Space>
</div> </div>
@@ -1047,26 +1088,31 @@ const EditChannel = (props) => {
</Typography.Text> </Typography.Text>
</div> </div>
<TextArea <TextArea
placeholder={t('此项可选用于复写返回的状态码比如将claude渠道的400错误复写为500用于重试请勿滥用该功能例如') + placeholder={
'\n' + JSON.stringify(STATUS_CODE_MAPPING_EXAMPLE, null, 2)} t(
name="status_code_mapping" '此项可选用于复写返回的状态码比如将claude渠道的400错误复写为500用于重试请勿滥用该功能例如',
) +
'\n' +
JSON.stringify(STATUS_CODE_MAPPING_EXAMPLE, null, 2)
}
name='status_code_mapping'
onChange={(value) => { onChange={(value) => {
handleInputChange('status_code_mapping', value); handleInputChange('status_code_mapping', value);
}} }}
autosize autosize
value={inputs.status_code_mapping} value={inputs.status_code_mapping}
autoComplete="new-password" autoComplete='new-password'
/> />
<Typography.Text <Typography.Text
style={{ style={{
color: 'rgba(var(--semi-blue-5), 1)', color: 'rgba(var(--semi-blue-5), 1)',
userSelect: 'none', userSelect: 'none',
cursor: 'pointer' cursor: 'pointer',
}} }}
onClick={() => { onClick={() => {
handleInputChange( handleInputChange(
'status_code_mapping', 'status_code_mapping',
JSON.stringify(STATUS_CODE_MAPPING_EXAMPLE, null, 2) JSON.stringify(STATUS_CODE_MAPPING_EXAMPLE, null, 2),
); );
}} }}
> >

View File

@@ -1,11 +1,29 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { API, showError, showInfo, showSuccess, showWarning, verifyJSON } from '../../helpers'; import {
import { SideSheet, Space, Button, Input, Typography, Spin, Modal, Select, Banner, TextArea } from '@douyinfe/semi-ui'; API,
showError,
showInfo,
showSuccess,
showWarning,
verifyJSON,
} from '../../helpers';
import {
SideSheet,
Space,
Button,
Input,
Typography,
Spin,
Modal,
Select,
Banner,
TextArea,
} from '@douyinfe/semi-ui';
import TextInput from '../../components/custom/TextInput.js'; import TextInput from '../../components/custom/TextInput.js';
import { getChannelModels } from '../../components/utils.js'; import { getChannelModels } from '../../components/utils.js';
const MODEL_MAPPING_EXAMPLE = { const MODEL_MAPPING_EXAMPLE = {
'gpt-3.5-turbo': 'gpt-3.5-turbo-0125' 'gpt-3.5-turbo': 'gpt-3.5-turbo-0125',
}; };
const EditTagModal = (props) => { const EditTagModal = (props) => {
@@ -23,7 +41,7 @@ const EditTagModal = (props) => {
model_mapping: null, model_mapping: null,
groups: [], groups: [],
models: [], models: [],
} };
const [inputs, setInputs] = useState(originInputs); const [inputs, setInputs] = useState(originInputs);
const handleInputChange = (name, value) => { const handleInputChange = (name, value) => {
@@ -39,7 +57,7 @@ const EditTagModal = (props) => {
'mj_blend', 'mj_blend',
'mj_upscale', 'mj_upscale',
'mj_describe', 'mj_describe',
'mj_uploads' 'mj_uploads',
]; ];
break; break;
case 5: case 5:
@@ -59,14 +77,11 @@ const EditTagModal = (props) => {
'mj_high_variation', 'mj_high_variation',
'mj_low_variation', 'mj_low_variation',
'mj_pan', 'mj_pan',
'mj_uploads' 'mj_uploads',
]; ];
break; break;
case 36: case 36:
localModels = [ localModels = ['suno_music', 'suno_lyrics'];
'suno_music',
'suno_lyrics'
];
break; break;
default: default:
localModels = getChannelModels(value); localModels = getChannelModels(value);
@@ -84,7 +99,7 @@ const EditTagModal = (props) => {
let res = await API.get(`/api/channel/models`); let res = await API.get(`/api/channel/models`);
let localModelOptions = res.data.data.map((model) => ({ let localModelOptions = res.data.data.map((model) => ({
label: model.id, label: model.id,
value: model.id value: model.id,
})); }));
setOriginModelOptions(localModelOptions); setOriginModelOptions(localModelOptions);
setFullModels(res.data.data.map((model) => model.id)); setFullModels(res.data.data.map((model) => model.id));
@@ -93,7 +108,7 @@ const EditTagModal = (props) => {
.filter((model) => { .filter((model) => {
return model.id.startsWith('gpt-') || model.id.startsWith('text-'); return model.id.startsWith('gpt-') || model.id.startsWith('text-');
}) })
.map((model) => model.id) .map((model) => model.id),
); );
} catch (error) { } catch (error) {
showError(error.message); showError(error.message);
@@ -109,27 +124,26 @@ const EditTagModal = (props) => {
setGroupOptions( setGroupOptions(
res.data.data.map((group) => ({ res.data.data.map((group) => ({
label: group, label: group,
value: group value: group,
})) })),
); );
} catch (error) { } catch (error) {
showError(error.message); showError(error.message);
} }
}; };
const handleSave = async () => { const handleSave = async () => {
setLoading(true); setLoading(true);
let data = { let data = {
tag: tag, tag: tag,
} };
if (inputs.model_mapping !== null && inputs.model_mapping !== '') { if (inputs.model_mapping !== null && inputs.model_mapping !== '') {
if (inputs.model_mapping !== '' && !verifyJSON(inputs.model_mapping)) { if (inputs.model_mapping !== '' && !verifyJSON(inputs.model_mapping)) {
showInfo('模型映射必须是合法的 JSON 格式!'); showInfo('模型映射必须是合法的 JSON 格式!');
setLoading(false); setLoading(false);
return; return;
} }
data.model_mapping = inputs.model_mapping data.model_mapping = inputs.model_mapping;
} }
if (inputs.groups.length > 0) { if (inputs.groups.length > 0) {
data.groups = inputs.groups.join(','); data.groups = inputs.groups.join(',');
@@ -139,7 +153,12 @@ const EditTagModal = (props) => {
} }
data.new_tag = inputs.new_tag; data.new_tag = inputs.new_tag;
// check have any change // check have any change
if (data.model_mapping === undefined && data.groups === undefined && data.models === undefined && data.new_tag === undefined) { if (
data.model_mapping === undefined &&
data.groups === undefined &&
data.models === undefined &&
data.new_tag === undefined
) {
showWarning('没有任何修改!'); showWarning('没有任何修改!');
setLoading(false); setLoading(false);
return; return;
@@ -159,7 +178,7 @@ const EditTagModal = (props) => {
} catch (error) { } catch (error) {
showError(error); showError(error);
} }
} };
useEffect(() => { useEffect(() => {
let localModelOptions = [...originModelOptions]; let localModelOptions = [...originModelOptions];
@@ -167,7 +186,7 @@ const EditTagModal = (props) => {
if (!localModelOptions.find((option) => option.label === model)) { if (!localModelOptions.find((option) => option.label === model)) {
localModelOptions.push({ localModelOptions.push({
label: model, label: model,
value: model value: model,
}); });
} }
}); });
@@ -179,7 +198,7 @@ const EditTagModal = (props) => {
...originInputs, ...originInputs,
tag: tag, tag: tag,
new_tag: tag, new_tag: tag,
}) });
fetchModels().then(); fetchModels().then();
fetchGroups().then(); fetchGroups().then();
}, [visible]); }, [visible]);
@@ -201,7 +220,7 @@ const EditTagModal = (props) => {
// 添加到下拉选项 // 添加到下拉选项
key: model, key: model,
text: model, text: model,
value: model value: model,
}); });
} else if (model) { } else if (model) {
showError('某些模型已存在!'); showError('某些模型已存在!');
@@ -217,17 +236,18 @@ const EditTagModal = (props) => {
handleInputChange('models', localModels); handleInputChange('models', localModels);
}; };
return ( return (
<SideSheet <SideSheet
title="编辑标签" title='编辑标签'
visible={visible} visible={visible}
onCancel={handleClose} onCancel={handleClose}
footer={ footer={
<div style={{ display: 'flex', justifyContent: 'flex-end' }}> <div style={{ display: 'flex', justifyContent: 'flex-end' }}>
<Space> <Space>
<Button onClick={handleClose}>取消</Button> <Button onClick={handleClose}>取消</Button>
<Button type="primary" onClick={handleSave} loading={loading}>保存</Button> <Button type='primary' onClick={handleSave} loading={loading}>
保存
</Button>
</Space> </Space>
</div> </div>
} }
@@ -235,27 +255,23 @@ const EditTagModal = (props) => {
<div style={{ marginTop: 10 }}> <div style={{ marginTop: 10 }}>
<Banner <Banner
type={'warning'} type={'warning'}
description={ description={<>所有编辑均为覆盖操作留空则不更改</>}
<>
所有编辑均为覆盖操作留空则不更改
</>
}
></Banner> ></Banner>
</div> </div>
<Spin spinning={loading}> <Spin spinning={loading}>
<TextInput <TextInput
label="标签名,留空则解散标签" label='标签名,留空则解散标签'
name="newTag" name='newTag'
value={inputs.new_tag} value={inputs.new_tag}
onChange={(value) => setInputs({ ...inputs, new_tag: value })} onChange={(value) => setInputs({ ...inputs, new_tag: value })}
placeholder="请输入新标签" placeholder='请输入新标签'
/> />
<div style={{ marginTop: 10 }}> <div style={{ marginTop: 10 }}>
<Typography.Text strong>模型留空则不更改</Typography.Text> <Typography.Text strong>模型留空则不更改</Typography.Text>
</div> </div>
<Select <Select
placeholder={'请选择该渠道所支持的模型,留空则不更改'} placeholder={'请选择该渠道所支持的模型,留空则不更改'}
name="models" name='models'
required required
multiple multiple
selection selection
@@ -265,16 +281,16 @@ const EditTagModal = (props) => {
handleInputChange('models', value); handleInputChange('models', value);
}} }}
value={inputs.models} value={inputs.models}
autoComplete="new-password" autoComplete='new-password'
optionList={modelOptions} optionList={modelOptions}
/> />
<Input <Input
addonAfter={ addonAfter={
<Button type="primary" onClick={addCustomModels}> <Button type='primary' onClick={addCustomModels}>
填入 填入
</Button> </Button>
} }
placeholder="输入自定义模型名称" placeholder='输入自定义模型名称'
value={customModel} value={customModel}
onChange={(value) => { onChange={(value) => {
setCustomModel(value.trim()); setCustomModel(value.trim());
@@ -285,7 +301,7 @@ const EditTagModal = (props) => {
</div> </div>
<Select <Select
placeholder={'请选择可以使用该渠道的分组,留空则不更改'} placeholder={'请选择可以使用该渠道的分组,留空则不更改'}
name="groups" name='groups'
required required
multiple multiple
selection selection
@@ -295,7 +311,7 @@ const EditTagModal = (props) => {
handleInputChange('groups', value); handleInputChange('groups', value);
}} }}
value={inputs.groups} value={inputs.groups}
autoComplete="new-password" autoComplete='new-password'
optionList={groupOptions} optionList={groupOptions}
/> />
<div style={{ marginTop: 10 }}> <div style={{ marginTop: 10 }}>
@@ -303,25 +319,25 @@ const EditTagModal = (props) => {
</div> </div>
<TextArea <TextArea
placeholder={`此项可选,用于修改请求体中的模型名称,为一个 JSON 字符串,键为请求中模型名称,值为要替换的模型名称,留空则不更改`} placeholder={`此项可选,用于修改请求体中的模型名称,为一个 JSON 字符串,键为请求中模型名称,值为要替换的模型名称,留空则不更改`}
name="model_mapping" name='model_mapping'
onChange={(value) => { onChange={(value) => {
handleInputChange('model_mapping', value); handleInputChange('model_mapping', value);
}} }}
autosize autosize
value={inputs.model_mapping} value={inputs.model_mapping}
autoComplete="new-password" autoComplete='new-password'
/> />
<Space> <Space>
<Typography.Text <Typography.Text
style={{ style={{
color: 'rgba(var(--semi-blue-5), 1)', color: 'rgba(var(--semi-blue-5), 1)',
userSelect: 'none', userSelect: 'none',
cursor: 'pointer' cursor: 'pointer',
}} }}
onClick={() => { onClick={() => {
handleInputChange( handleInputChange(
'model_mapping', 'model_mapping',
JSON.stringify(MODEL_MAPPING_EXAMPLE, null, 2) JSON.stringify(MODEL_MAPPING_EXAMPLE, null, 2),
); );
}} }}
> >
@@ -331,13 +347,10 @@ const EditTagModal = (props) => {
style={{ style={{
color: 'rgba(var(--semi-blue-5), 1)', color: 'rgba(var(--semi-blue-5), 1)',
userSelect: 'none', userSelect: 'none',
cursor: 'pointer' cursor: 'pointer',
}} }}
onClick={() => { onClick={() => {
handleInputChange( handleInputChange('model_mapping', JSON.stringify({}, null, 2));
'model_mapping',
JSON.stringify({}, null, 2)
);
}} }}
> >
清空重定向 清空重定向
@@ -346,13 +359,10 @@ const EditTagModal = (props) => {
style={{ style={{
color: 'rgba(var(--semi-blue-5), 1)', color: 'rgba(var(--semi-blue-5), 1)',
userSelect: 'none', userSelect: 'none',
cursor: 'pointer' cursor: 'pointer',
}} }}
onClick={() => { onClick={() => {
handleInputChange( handleInputChange('model_mapping', '');
'model_mapping',
""
);
}} }}
> >
不更改 不更改

View File

@@ -9,10 +9,10 @@ const File = () => {
<> <>
<Layout> <Layout>
<Layout.Header> <Layout.Header>
<h3>{t('管理渠道')}</h3> <h3>{t('管理渠道')}</h3>
</Layout.Header> </Layout.Header>
<Layout.Content> <Layout.Content>
<ChannelsTable /> <ChannelsTable />
</Layout.Content> </Layout.Content>
</Layout> </Layout>
</> </>

View File

@@ -1,6 +1,6 @@
import React, {useEffect} from 'react'; import React, { useEffect } from 'react';
import { useTokenKeys } from '../../components/fetchTokenKeys'; import { useTokenKeys } from '../../components/fetchTokenKeys';
import {Banner, Layout} from '@douyinfe/semi-ui'; import { Banner, Layout } from '@douyinfe/semi-ui';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
const ChatPage = () => { const ChatPage = () => {
@@ -10,21 +10,24 @@ const ChatPage = () => {
const comLink = (key) => { const comLink = (key) => {
// console.log('chatLink:', chatLink); // console.log('chatLink:', chatLink);
if (!serverAddress || !key) return ''; if (!serverAddress || !key) return '';
let link = ""; let link = '';
if (id) { if (id) {
let chats = localStorage.getItem('chats'); let chats = localStorage.getItem('chats');
if (chats) { if (chats) {
chats = JSON.parse(chats); chats = JSON.parse(chats);
if (Array.isArray(chats) && chats.length > 0) { if (Array.isArray(chats) && chats.length > 0) {
for (let k in chats[id]) { for (let k in chats[id]) {
link = chats[id][k]; link = chats[id][k];
link = link.replaceAll('{address}', encodeURIComponent(serverAddress)); link = link.replaceAll(
link = link.replaceAll('{key}', 'sk-' + key); '{address}',
} encodeURIComponent(serverAddress),
} );
link = link.replaceAll('{key}', 'sk-' + key);
} }
}
} }
return link; }
return link;
}; };
const iframeSrc = keys.length > 0 ? comLink(keys[0]) : ''; const iframeSrc = keys.length > 0 ? comLink(keys[0]) : '';
@@ -33,17 +36,14 @@ const ChatPage = () => {
<iframe <iframe
src={iframeSrc} src={iframeSrc}
style={{ width: '100%', height: '100%', border: 'none' }} style={{ width: '100%', height: '100%', border: 'none' }}
title="Token Frame" title='Token Frame'
allow="camera;microphone" allow='camera;microphone'
/> />
) : ( ) : (
<div> <div>
<Layout> <Layout>
<Layout.Header> <Layout.Header>
<Banner <Banner description={'正在跳转......'} type={'warning'} />
description={"正在跳转......"}
type={"warning"}
/>
</Layout.Header> </Layout.Header>
</Layout> </Layout>
</div> </div>

View File

@@ -18,7 +18,7 @@ const chat2page = () => {
return ( return (
<div> <div>
<h3>正在加载请稍候...</h3> <h3>正在加载请稍候...</h3>
</div> </div>
); );
}; };

View File

@@ -1,8 +1,18 @@
import React, { useContext, useEffect, useRef, useState } from 'react'; import React, { useContext, useEffect, useRef, useState } from 'react';
import { initVChartSemiTheme } from '@visactor/vchart-semi-theme'; import { initVChartSemiTheme } from '@visactor/vchart-semi-theme';
import { Button, Card, Col, Descriptions, Form, Layout, Row, Spin, Tabs } from '@douyinfe/semi-ui'; import {
import { VChart } from "@visactor/react-vchart"; Button,
Card,
Col,
Descriptions,
Form,
Layout,
Row,
Spin,
Tabs,
} from '@douyinfe/semi-ui';
import { VChart } from '@visactor/react-vchart';
import { import {
API, API,
isAdmin, isAdmin,
@@ -59,10 +69,12 @@ const Detail = (props) => {
const [lineData, setLineData] = useState([]); const [lineData, setLineData] = useState([]);
const [spec_pie, setSpecPie] = useState({ const [spec_pie, setSpecPie] = useState({
type: 'pie', type: 'pie',
data: [{ data: [
id: 'id0', {
values: pieData id: 'id0',
}], values: pieData,
},
],
outerRadius: 0.8, outerRadius: 0.8,
innerRadius: 0.5, innerRadius: 0.5,
padAngle: 0.6, padAngle: 0.6,
@@ -113,10 +125,12 @@ const Detail = (props) => {
}); });
const [spec_line, setSpecLine] = useState({ const [spec_line, setSpecLine] = useState({
type: 'bar', type: 'bar',
data: [{ data: [
id: 'barData', {
values: lineData id: 'barData',
}], values: lineData,
},
],
xField: 'Time', xField: 'Time',
yField: 'Usage', yField: 'Usage',
seriesField: 'Model', seriesField: 'Model',
@@ -158,7 +172,7 @@ const Detail = (props) => {
array.sort((a, b) => b.value - a.value); array.sort((a, b) => b.value - a.value);
let sum = 0; let sum = 0;
for (let i = 0; i < array.length; i++) { for (let i = 0; i < array.length; i++) {
if (array[i].key == "其他") { if (array[i].key == '其他') {
continue; continue;
} }
let value = parseFloat(array[i].value); let value = parseFloat(array[i].value);
@@ -245,7 +259,7 @@ const Detail = (props) => {
let totalTokens = 0; let totalTokens = 0;
// 收集所有唯一的模型名称 // 收集所有唯一的模型名称
data.forEach(item => { data.forEach((item) => {
uniqueModels.add(item.model_name); uniqueModels.add(item.model_name);
totalTokens += item.token_used; totalTokens += item.token_used;
totalQuota += item.quota; totalQuota += item.quota;
@@ -255,7 +269,8 @@ const Detail = (props) => {
// 处理颜色映射 // 处理颜色映射
const newModelColors = {}; const newModelColors = {};
Array.from(uniqueModels).forEach((modelName) => { Array.from(uniqueModels).forEach((modelName) => {
newModelColors[modelName] = modelColorMap[modelName] || newModelColors[modelName] =
modelColorMap[modelName] ||
modelColors[modelName] || modelColors[modelName] ||
modelToColor(modelName); modelToColor(modelName);
}); });
@@ -263,7 +278,7 @@ const Detail = (props) => {
// 按时间和模型聚合数据 // 按时间和模型聚合数据
let aggregatedData = new Map(); let aggregatedData = new Map();
data.forEach(item => { data.forEach((item) => {
const timeKey = timestamp2string1(item.created_at, dataExportDefaultTime); const timeKey = timestamp2string1(item.created_at, dataExportDefaultTime);
const modelKey = item.model_name; const modelKey = item.model_name;
const key = `${timeKey}-${modelKey}`; const key = `${timeKey}-${modelKey}`;
@@ -273,7 +288,7 @@ const Detail = (props) => {
time: timeKey, time: timeKey,
model: modelKey, model: modelKey,
quota: 0, quota: 0,
count: 0 count: 0,
}); });
} }
@@ -293,33 +308,38 @@ const Detail = (props) => {
newPieData = Array.from(modelTotals).map(([model, count]) => ({ newPieData = Array.from(modelTotals).map(([model, count]) => ({
type: model, type: model,
value: count value: count,
})); }));
// 生成时间点序列 // 生成时间点序列
let timePoints = Array.from(new Set([...aggregatedData.values()].map(d => d.time))); let timePoints = Array.from(
new Set([...aggregatedData.values()].map((d) => d.time)),
);
if (timePoints.length < 7) { if (timePoints.length < 7) {
const lastTime = Math.max(...data.map(item => item.created_at)); const lastTime = Math.max(...data.map((item) => item.created_at));
const interval = dataExportDefaultTime === 'hour' ? 3600 const interval =
: dataExportDefaultTime === 'day' ? 86400 dataExportDefaultTime === 'hour'
: 604800; ? 3600
: dataExportDefaultTime === 'day'
? 86400
: 604800;
timePoints = Array.from({length: 7}, (_, i) => timePoints = Array.from({ length: 7 }, (_, i) =>
timestamp2string1(lastTime - (6-i) * interval, dataExportDefaultTime) timestamp2string1(lastTime - (6 - i) * interval, dataExportDefaultTime),
); );
} }
// 生成柱状图数据 // 生成柱状图数据
timePoints.forEach(time => { timePoints.forEach((time) => {
// 为每个时间点收集所有模型的数据 // 为每个时间点收集所有模型的数据
let timeData = Array.from(uniqueModels).map(model => { let timeData = Array.from(uniqueModels).map((model) => {
const key = `${time}-${model}`; const key = `${time}-${model}`;
const aggregated = aggregatedData.get(key); const aggregated = aggregatedData.get(key);
return { return {
Time: time, Time: time,
Model: model, Model: model,
rawQuota: aggregated?.quota || 0, rawQuota: aggregated?.quota || 0,
Usage: aggregated?.quota ? getQuotaWithUnit(aggregated.quota, 4) : 0 Usage: aggregated?.quota ? getQuotaWithUnit(aggregated.quota, 4) : 0,
}; };
}); });
@@ -330,9 +350,9 @@ const Detail = (props) => {
timeData.sort((a, b) => b.rawQuota - a.rawQuota); timeData.sort((a, b) => b.rawQuota - a.rawQuota);
// 为每个数据点添加该时间的总计 // 为每个数据点添加该时间的总计
timeData = timeData.map(item => ({ timeData = timeData.map((item) => ({
...item, ...item,
TimeSum: timeSum TimeSum: timeSum,
})); }));
// 将排序后的数据添加到 newLineData // 将排序后的数据添加到 newLineData
@@ -344,28 +364,28 @@ const Detail = (props) => {
newLineData.sort((a, b) => a.Time.localeCompare(b.Time)); newLineData.sort((a, b) => a.Time.localeCompare(b.Time));
// 更新图表配置和数据 // 更新图表配置和数据
setSpecPie(prev => ({ setSpecPie((prev) => ({
...prev, ...prev,
data: [{ id: 'id0', values: newPieData }], data: [{ id: 'id0', values: newPieData }],
title: { title: {
...prev.title, ...prev.title,
subtext: `${t('总计')}${renderNumber(totalTimes)}` subtext: `${t('总计')}${renderNumber(totalTimes)}`,
}, },
color: { color: {
specified: newModelColors specified: newModelColors,
} },
})); }));
setSpecLine(prev => ({ setSpecLine((prev) => ({
...prev, ...prev,
data: [{ id: 'barData', values: newLineData }], data: [{ id: 'barData', values: newLineData }],
title: { title: {
...prev.title, ...prev.title,
subtext: `${t('总计')}${renderQuota(totalQuota, 2)}` subtext: `${t('总计')}${renderQuota(totalQuota, 2)}`,
}, },
color: { color: {
specified: newModelColors specified: newModelColors,
} },
})); }));
setPieData(newPieData); setPieData(newPieData);
@@ -377,16 +397,16 @@ const Detail = (props) => {
const getUserData = async () => { const getUserData = async () => {
let res = await API.get(`/api/user/self`); let res = await API.get(`/api/user/self`);
const {success, message, data} = res.data; const { success, message, data } = res.data;
if (success) { if (success) {
userDispatch({type: 'login', payload: data}); userDispatch({ type: 'login', payload: data });
} else { } else {
showError(message); showError(message);
} }
}; };
useEffect(() => { useEffect(() => {
getUserData() getUserData();
if (!initialized.current) { if (!initialized.current) {
initVChartSemiTheme({ initVChartSemiTheme({
isWatchingThemeSwitch: true, isWatchingThemeSwitch: true,
@@ -468,15 +488,19 @@ const Detail = (props) => {
> >
{t('查询')} {t('查询')}
</Button> </Button>
<Form.Section> <Form.Section></Form.Section>
</Form.Section>
</> </>
</Form> </Form>
<Spin spinning={loading}> <Spin spinning={loading}>
<Row gutter={{ xs: 16, sm: 16, md: 16, lg: 24, xl: 24, xxl: 24 }} style={{marginTop: 20}} type="flex" justify="space-between"> <Row
<Col span={styleState.isMobile?24:8}> gutter={{ xs: 16, sm: 16, md: 16, lg: 24, xl: 24, xxl: 24 }}
style={{ marginTop: 20 }}
type='flex'
justify='space-between'
>
<Col span={styleState.isMobile ? 24 : 8}>
<Card className='panel-desc-card'> <Card className='panel-desc-card'>
<Descriptions row size="small"> <Descriptions row size='small'>
<Descriptions.Item itemKey={t('当前余额')}> <Descriptions.Item itemKey={t('当前余额')}>
{renderQuota(userState?.user?.quota)} {renderQuota(userState?.user?.quota)}
</Descriptions.Item> </Descriptions.Item>
@@ -489,9 +513,9 @@ const Detail = (props) => {
</Descriptions> </Descriptions>
</Card> </Card>
</Col> </Col>
<Col span={styleState.isMobile?24:8}> <Col span={styleState.isMobile ? 24 : 8}>
<Card> <Card>
<Descriptions row size="small"> <Descriptions row size='small'>
<Descriptions.Item itemKey={t('统计额度')}> <Descriptions.Item itemKey={t('统计额度')}>
{renderQuota(consumeQuota)} {renderQuota(consumeQuota)}
</Descriptions.Item> </Descriptions.Item>
@@ -508,40 +532,43 @@ const Detail = (props) => {
<Card> <Card>
<Descriptions row size='small'> <Descriptions row size='small'>
<Descriptions.Item itemKey={t('平均RPM')}> <Descriptions.Item itemKey={t('平均RPM')}>
{(times / {(
times /
((Date.parse(end_timestamp) - ((Date.parse(end_timestamp) -
Date.parse(start_timestamp)) / Date.parse(start_timestamp)) /
60000)).toFixed(3)} 60000)
).toFixed(3)}
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item itemKey={t('平均TPM')}> <Descriptions.Item itemKey={t('平均TPM')}>
{(consumeTokens / {(
consumeTokens /
((Date.parse(end_timestamp) - ((Date.parse(end_timestamp) -
Date.parse(start_timestamp)) / Date.parse(start_timestamp)) /
60000)).toFixed(3)} 60000)
).toFixed(3)}
</Descriptions.Item> </Descriptions.Item>
</Descriptions> </Descriptions>
</Card> </Card>
</Col> </Col>
</Row> </Row>
<Card style={{marginTop: 20}}> <Card style={{ marginTop: 20 }}>
<Tabs type="line" defaultActiveKey="1"> <Tabs type='line' defaultActiveKey='1'>
<Tabs.TabPane tab={t('消耗分布')} itemKey="1"> <Tabs.TabPane tab={t('消耗分布')} itemKey='1'>
<div style={{ height: 500 }}> <div style={{ height: 500 }}>
<VChart <VChart
spec={spec_line} spec={spec_line}
option={{ mode: "desktop-browser" }} option={{ mode: 'desktop-browser' }}
/> />
</div> </div>
</Tabs.TabPane> </Tabs.TabPane>
<Tabs.TabPane tab={t('调用次数分布')} itemKey="2"> <Tabs.TabPane tab={t('调用次数分布')} itemKey='2'>
<div style={{ height: 500 }}> <div style={{ height: 500 }}>
<VChart <VChart
spec={spec_pie} spec={spec_pie}
option={{ mode: "desktop-browser" }} option={{ mode: 'desktop-browser' }}
/> />
</div> </div>
</Tabs.TabPane> </Tabs.TabPane>
</Tabs> </Tabs>
</Card> </Card>
</Spin> </Spin>

View File

@@ -40,19 +40,19 @@ const Home = () => {
setHomePageContent(content); setHomePageContent(content);
localStorage.setItem('home_page_content', content); localStorage.setItem('home_page_content', content);
// 如果内容是 URL则发送主题模式 // 如果内容是 URL则发送主题模式
if (data.startsWith('https://')) { if (data.startsWith('https://')) {
const iframe = document.querySelector('iframe'); const iframe = document.querySelector('iframe');
if (iframe) { if (iframe) {
const theme = localStorage.getItem('theme-mode') || 'light'; const theme = localStorage.getItem('theme-mode') || 'light';
// 测试是否正确传递theme-mode给iframe // 测试是否正确传递theme-mode给iframe
// console.log('Sending theme-mode to iframe:', theme); // console.log('Sending theme-mode to iframe:', theme);
iframe.onload = () => { iframe.onload = () => {
iframe.contentWindow.postMessage({ themeMode: theme }, '*'); iframe.contentWindow.postMessage({ themeMode: theme }, '*');
iframe.contentWindow.postMessage({ lang: i18n.language }, '*'); iframe.contentWindow.postMessage({ lang: i18n.language }, '*');
}; };
}
} }
}
} else { } else {
showError(message); showError(message);
setHomePageContent('加载首页内容失败...'); setHomePageContent('加载首页内容失败...');
@@ -99,7 +99,9 @@ const Home = () => {
</span> </span>
} }
> >
<p>{t('名称')}{statusState?.status?.system_name}</p> <p>
{t('名称')}{statusState?.status?.system_name}
</p>
<p> <p>
{t('版本')} {t('版本')}
{statusState?.status?.version {statusState?.status?.version
@@ -126,7 +128,9 @@ const Home = () => {
Apache-2.0 License Apache-2.0 License
</a> </a>
</p> </p>
<p>{t('启动时间')}{getStartTimeString()}</p> <p>
{t('启动时间')}{getStartTimeString()}
</p>
</Card> </Card>
</Col> </Col>
<Col span={12}> <Col span={12}>
@@ -158,8 +162,8 @@ const Home = () => {
<p> <p>
{t('OIDC 身份验证')} {t('OIDC 身份验证')}
{statusState?.status?.oidc === true {statusState?.status?.oidc === true
? t('已启用') ? t('已启用')
: t('未启用')} : t('未启用')}
</p> </p>
<p> <p>
{t('微信身份验证')} {t('微信身份验证')}

View File

@@ -1,8 +1,23 @@
import React, { useCallback, useContext, useEffect, useState } from 'react'; import React, { useCallback, useContext, useEffect, useState } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom'; import { useNavigate, useSearchParams } from 'react-router-dom';
import { UserContext } from '../../context/User/index.js'; import { UserContext } from '../../context/User/index.js';
import { API, getUserIdFromLocalStorage, showError } from '../../helpers/index.js'; import {
import { Card, Chat, Input, Layout, Select, Slider, TextArea, Typography, Button, Highlight } from '@douyinfe/semi-ui'; API,
getUserIdFromLocalStorage,
showError,
} from '../../helpers/index.js';
import {
Card,
Chat,
Input,
Layout,
Select,
Slider,
TextArea,
Typography,
Button,
Highlight,
} from '@douyinfe/semi-ui';
import { SSE } from 'sse'; import { SSE } from 'sse';
import { IconSetting } from '@douyinfe/semi-icons'; import { IconSetting } from '@douyinfe/semi-icons';
import { StyleContext } from '../../context/Style/index.js'; import { StyleContext } from '../../context/Style/index.js';
@@ -12,21 +27,23 @@ import { renderGroupOption, truncateText } from '../../helpers/render.js';
const roleInfo = { const roleInfo = {
user: { user: {
name: 'User', name: 'User',
avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png' avatar:
'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png',
}, },
assistant: { assistant: {
name: 'Assistant', name: 'Assistant',
avatar: 'logo.png' avatar: 'logo.png',
}, },
system: { system: {
name: 'System', name: 'System',
avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png' avatar:
} 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png',
} },
};
let id = 4; let id = 4;
function getId() { function getId() {
return `${id++}` return `${id++}`;
} }
const Playground = () => { const Playground = () => {
@@ -44,7 +61,7 @@ const Playground = () => {
id: '3', id: '3',
createAt: 1715676751919, createAt: 1715676751919,
content: t('你好,请问有什么可以帮助您的吗?'), content: t('你好,请问有什么可以帮助您的吗?'),
} },
]; ];
const [inputs, setInputs] = useState({ const [inputs, setInputs] = useState({
@@ -56,7 +73,9 @@ const Playground = () => {
const [searchParams, setSearchParams] = useSearchParams(); const [searchParams, setSearchParams] = useSearchParams();
const [userState, userDispatch] = useContext(UserContext); const [userState, userDispatch] = useContext(UserContext);
const [status, setStatus] = useState({}); const [status, setStatus] = useState({});
const [systemPrompt, setSystemPrompt] = useState('You are a helpful assistant. You can help me by answering my questions. You can also ask me questions.'); const [systemPrompt, setSystemPrompt] = useState(
'You are a helpful assistant. You can help me by answering my questions. You can also ask me questions.',
);
const [message, setMessage] = useState(defaultMessage); const [message, setMessage] = useState(defaultMessage);
const [models, setModels] = useState([]); const [models, setModels] = useState([]);
const [groups, setGroups] = useState([]); const [groups, setGroups] = useState([]);
@@ -99,26 +118,35 @@ const Playground = () => {
const { success, message, data } = res.data; const { success, message, data } = res.data;
if (success) { if (success) {
let localGroupOptions = Object.entries(data).map(([group, info]) => ({ let localGroupOptions = Object.entries(data).map(([group, info]) => ({
label: truncateText(info.desc, "50%"), label: truncateText(info.desc, '50%'),
value: group, value: group,
ratio: info.ratio, ratio: info.ratio,
fullLabel: info.desc // 保存完整文本用于tooltip fullLabel: info.desc, // 保存完整文本用于tooltip
})); }));
if (localGroupOptions.length === 0) { if (localGroupOptions.length === 0) {
localGroupOptions = [{ localGroupOptions = [
label: t('用户分组'), {
value: '', label: t('用户分组'),
ratio: 1 value: '',
}]; ratio: 1,
},
];
} else { } else {
const localUser = JSON.parse(localStorage.getItem('user')); const localUser = JSON.parse(localStorage.getItem('user'));
const userGroup = (userState.user && userState.user.group) || (localUser && localUser.group); const userGroup =
(userState.user && userState.user.group) ||
(localUser && localUser.group);
if (userGroup) { if (userGroup) {
const userGroupIndex = localGroupOptions.findIndex(g => g.value === userGroup); const userGroupIndex = localGroupOptions.findIndex(
(g) => g.value === userGroup,
);
if (userGroupIndex > -1) { if (userGroupIndex > -1) {
const userGroupOption = localGroupOptions.splice(userGroupIndex, 1)[0]; const userGroupOption = localGroupOptions.splice(
userGroupIndex,
1,
)[0];
localGroupOptions.unshift(userGroupOption); localGroupOptions.unshift(userGroupOption);
} }
} }
@@ -135,7 +163,7 @@ const Playground = () => {
border: '1px solid var(--semi-color-border)', border: '1px solid var(--semi-color-border)',
borderRadius: '16px', borderRadius: '16px',
margin: '0px 8px', margin: '0px 8px',
} };
const getSystemMessage = () => { const getSystemMessage = () => {
if (systemPrompt !== '') { if (systemPrompt !== '') {
@@ -144,22 +172,22 @@ const Playground = () => {
id: '1', id: '1',
createAt: 1715676751919, createAt: 1715676751919,
content: systemPrompt, content: systemPrompt,
} };
} }
} };
let handleSSE = (payload) => { let handleSSE = (payload) => {
let source = new SSE('/pg/chat/completions', { let source = new SSE('/pg/chat/completions', {
headers: { headers: {
"Content-Type": "application/json", 'Content-Type': 'application/json',
"New-Api-User": getUserIdFromLocalStorage(), 'New-Api-User': getUserIdFromLocalStorage(),
}, },
method: "POST", method: 'POST',
payload: JSON.stringify(payload), payload: JSON.stringify(payload),
}); });
source.addEventListener("message", (e) => { source.addEventListener('message', (e) => {
// 只有收到 [DONE] 时才结束 // 只有收到 [DONE] 时才结束
if (e.data === "[DONE]") { if (e.data === '[DONE]') {
source.close(); source.close();
completeMessage(); completeMessage();
return; return;
@@ -172,12 +200,12 @@ const Playground = () => {
} }
}); });
source.addEventListener("error", (e) => { source.addEventListener('error', (e) => {
generateMockResponse(e.data) generateMockResponse(e.data);
completeMessage('error') completeMessage('error');
}); });
source.addEventListener("readystatechange", (e) => { source.addEventListener('readystatechange', (e) => {
if (e.readyState >= 2) { if (e.readyState >= 2) {
if (source.status === undefined) { if (source.status === undefined) {
source.close(); source.close();
@@ -186,55 +214,58 @@ const Playground = () => {
} }
}); });
source.stream(); source.stream();
} };
const onMessageSend = useCallback((content, attachment) => { const onMessageSend = useCallback(
console.log("attachment: ", attachment); (content, attachment) => {
setMessage((prevMessage) => { console.log('attachment: ', attachment);
const newMessage = [ setMessage((prevMessage) => {
...prevMessage, const newMessage = [
{ ...prevMessage,
role: 'user', {
content: content, role: 'user',
createAt: Date.now(), content: content,
id: getId() createAt: Date.now(),
} id: getId(),
]; },
];
// 将 getPayload 移到这里 // 将 getPayload 移到这里
const getPayload = () => { const getPayload = () => {
let systemMessage = getSystemMessage(); let systemMessage = getSystemMessage();
let messages = newMessage.map((item) => { let messages = newMessage.map((item) => {
return { return {
role: item.role, role: item.role,
content: item.content, content: item.content,
};
});
if (systemMessage) {
messages.unshift(systemMessage);
} }
}); return {
if (systemMessage) { messages: messages,
messages.unshift(systemMessage); stream: true,
} model: inputs.model,
return { group: inputs.group,
messages: messages, max_tokens: parseInt(inputs.max_tokens),
stream: true, temperature: inputs.temperature,
model: inputs.model, };
group: inputs.group,
max_tokens: parseInt(inputs.max_tokens),
temperature: inputs.temperature,
}; };
};
// 使用更新后的消息状态调用 handleSSE // 使用更新后的消息状态调用 handleSSE
handleSSE(getPayload()); handleSSE(getPayload());
newMessage.push({ newMessage.push({
role: 'assistant', role: 'assistant',
content: '', content: '',
createAt: Date.now(), createAt: Date.now(),
id: getId(), id: getId(),
status: 'loading' status: 'loading',
});
return newMessage;
}); });
return newMessage; },
}); [getSystemMessage],
}, [getSystemMessage]); );
const completeMessage = useCallback((status = 'complete') => { const completeMessage = useCallback((status = 'complete') => {
// console.log("Complete Message: ", status) // console.log("Complete Message: ", status)
@@ -244,27 +275,27 @@ const Playground = () => {
if (lastMessage.status === 'complete' || lastMessage.status === 'error') { if (lastMessage.status === 'complete' || lastMessage.status === 'error') {
return prevMessage; return prevMessage;
} }
return [ return [...prevMessage.slice(0, -1), { ...lastMessage, status: status }];
...prevMessage.slice(0, -1),
{ ...lastMessage, status: status }
];
}); });
}, []) }, []);
const generateMockResponse = useCallback((content) => { const generateMockResponse = useCallback((content) => {
// console.log("Generate Mock Response: ", content); // console.log("Generate Mock Response: ", content);
setMessage((message) => { setMessage((message) => {
const lastMessage = message[message.length - 1]; const lastMessage = message[message.length - 1];
let newMessage = {...lastMessage}; let newMessage = { ...lastMessage };
if (lastMessage.status === 'loading' || lastMessage.status === 'incomplete') { if (
lastMessage.status === 'loading' ||
lastMessage.status === 'incomplete'
) {
newMessage = { newMessage = {
...newMessage, ...newMessage,
content: (lastMessage.content || '') + content, content: (lastMessage.content || '') + content,
status: 'incomplete' status: 'incomplete',
} };
} }
return [ ...message.slice(0, -1), newMessage ] return [...message.slice(0, -1), newMessage];
}) });
}, []); }, []);
const SettingsToggle = () => { const SettingsToggle = () => {
@@ -285,34 +316,47 @@ const Playground = () => {
boxShadow: '2px 0 8px rgba(0, 0, 0, 0.15)', boxShadow: '2px 0 8px rgba(0, 0, 0, 0.15)',
}} }}
onClick={() => setShowSettings(!showSettings)} onClick={() => setShowSettings(!showSettings)}
theme="solid" theme='solid'
type="primary" type='primary'
/> />
); );
}; };
function CustomInputRender(props) { function CustomInputRender(props) {
const { detailProps } = props; const { detailProps } = props;
const { clearContextNode, uploadNode, inputNode, sendNode, onClick } = detailProps; const { clearContextNode, uploadNode, inputNode, sendNode, onClick } =
detailProps;
return <div style={{margin: '8px 16px', display: 'flex', flexDirection:'row', return (
alignItems: 'flex-end', borderRadius: 16,padding: 10, border: '1px solid var(--semi-color-border)'}} <div
onClick={onClick} style={{
> margin: '8px 16px',
{/*{uploadNode}*/} display: 'flex',
{inputNode} flexDirection: 'row',
{sendNode} alignItems: 'flex-end',
</div> borderRadius: 16,
padding: 10,
border: '1px solid var(--semi-color-border)',
}}
onClick={onClick}
>
{/*{uploadNode}*/}
{inputNode}
{sendNode}
</div>
);
} }
const renderInputArea = useCallback((props) => { const renderInputArea = useCallback((props) => {
return (<CustomInputRender {...props} />) return <CustomInputRender {...props} />;
}, []); }, []);
return ( return (
<Layout style={{height: '100%'}}> <Layout style={{ height: '100%' }}>
{(showSettings || !styleState.isMobile) && ( {(showSettings || !styleState.isMobile) && (
<Layout.Sider style={{ display: styleState.isMobile ? 'block' : 'initial' }}> <Layout.Sider
style={{ display: styleState.isMobile ? 'block' : 'initial' }}
>
<Card style={commonOuterStyle}> <Card style={commonOuterStyle}>
<div style={{ marginTop: 10 }}> <div style={{ marginTop: 10 }}>
<Typography.Text strong>{t('分组')}</Typography.Text> <Typography.Text strong>{t('分组')}</Typography.Text>
@@ -390,18 +434,17 @@ const Playground = () => {
setSystemPrompt(value); setSystemPrompt(value);
}} }}
/> />
</Card> </Card>
</Layout.Sider> </Layout.Sider>
)} )}
<Layout.Content> <Layout.Content>
<div style={{height: '100%', position: 'relative'}}> <div style={{ height: '100%', position: 'relative' }}>
<SettingsToggle /> <SettingsToggle />
<Chat <Chat
chatBoxRenderConfig={{ chatBoxRenderConfig={{
renderChatBoxAction: () => { renderChatBoxAction: () => {
return <div></div> return <div></div>;
} },
}} }}
renderInputArea={renderInputArea} renderInputArea={renderInputArea}
roleConfig={roleInfo} roleConfig={roleInfo}

View File

@@ -8,7 +8,11 @@ import {
showError, showError,
showSuccess, showSuccess,
} from '../../helpers'; } from '../../helpers';
import { getQuotaPerUnit, renderQuota, renderQuotaWithPrompt } from '../../helpers/render'; import {
getQuotaPerUnit,
renderQuota,
renderQuotaWithPrompt,
} from '../../helpers/render';
import { import {
AutoComplete, AutoComplete,
Button, Button,
@@ -171,7 +175,9 @@ const EditRedemption = (props) => {
/> />
<Divider /> <Divider />
<div style={{ marginTop: 20 }}> <div style={{ marginTop: 20 }}>
<Typography.Text>{t('额度') + renderQuotaWithPrompt(quota)}</Typography.Text> <Typography.Text>
{t('额度') + renderQuotaWithPrompt(quota)}
</Typography.Text>
</div> </div>
<AutoComplete <AutoComplete
style={{ marginTop: 8 }} style={{ marginTop: 8 }}

View File

@@ -9,14 +9,14 @@ const Redemption = () => {
<> <>
<Layout> <Layout>
<Layout.Header> <Layout.Header>
<h3>{t('管理兑换码')}</h3> <h3>{t('管理兑换码')}</h3>
</Layout.Header> </Layout.Header>
<Layout.Content> <Layout.Content>
<RedemptionsTable /> <RedemptionsTable />
</Layout.Content> </Layout.Content>
</Layout> </Layout>
</> </>
); );
} };
export default Redemption; export default Redemption;

View File

@@ -5,23 +5,27 @@ import {
API, API,
showError, showError,
showSuccess, showSuccess,
showWarning, verifyJSON showWarning,
verifyJSON,
} from '../../../helpers'; } from '../../../helpers';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import Text from '@douyinfe/semi-ui/lib/es/typography/text'; import Text from '@douyinfe/semi-ui/lib/es/typography/text';
const CLAUDE_HEADER = { const CLAUDE_HEADER = {
'claude-3-7-sonnet-20250219-thinking': { 'claude-3-7-sonnet-20250219-thinking': {
'anthropic-beta': ['output-128k-2025-02-19', 'token-efficient-tools-2025-02-19'], 'anthropic-beta': [
} 'output-128k-2025-02-19',
'token-efficient-tools-2025-02-19',
],
},
}; };
const CLAUDE_DEFAULT_MAX_TOKENS = { const CLAUDE_DEFAULT_MAX_TOKENS = {
'default': 8192, default: 8192,
"claude-3-haiku-20240307": 4096, 'claude-3-haiku-20240307': 4096,
"claude-3-opus-20240229": 4096, 'claude-3-opus-20240229': 4096,
'claude-3-7-sonnet-20250219-thinking': 8192, 'claude-3-7-sonnet-20250219-thinking': 8192,
} };
export default function SettingClaudeModel(props) { export default function SettingClaudeModel(props) {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -53,7 +57,8 @@ export default function SettingClaudeModel(props) {
if (requestQueue.length === 1) { if (requestQueue.length === 1) {
if (res.includes(undefined)) return; if (res.includes(undefined)) return;
} else if (requestQueue.length > 1) { } else if (requestQueue.length > 1) {
if (res.includes(undefined)) return showError(t('部分保存失败,请重试')); if (res.includes(undefined))
return showError(t('部分保存失败,请重试'));
} }
showSuccess(t('保存成功')); showSuccess(t('保存成功'));
props.refresh(); props.refresh();
@@ -92,18 +97,29 @@ export default function SettingClaudeModel(props) {
<Form.TextArea <Form.TextArea
label={t('Claude请求头覆盖')} label={t('Claude请求头覆盖')}
field={'claude.model_headers_settings'} field={'claude.model_headers_settings'}
placeholder={t('为一个 JSON 文本,例如:') + '\n' + JSON.stringify(CLAUDE_HEADER, null, 2)} placeholder={
extraText={t('示例') + '\n' + JSON.stringify(CLAUDE_HEADER, null, 2)} t('为一个 JSON 文本,例如:') +
'\n' +
JSON.stringify(CLAUDE_HEADER, null, 2)
}
extraText={
t('示例') + '\n' + JSON.stringify(CLAUDE_HEADER, null, 2)
}
autosize={{ minRows: 6, maxRows: 12 }} autosize={{ minRows: 6, maxRows: 12 }}
trigger='blur' trigger='blur'
stopValidateWithError stopValidateWithError
rules={[ rules={[
{ {
validator: (rule, value) => verifyJSON(value), validator: (rule, value) => verifyJSON(value),
message: t('不是合法的 JSON 字符串') message: t('不是合法的 JSON 字符串'),
} },
]} ]}
onChange={(value) => setInputs({ ...inputs, 'claude.model_headers_settings': value })} onChange={(value) =>
setInputs({
...inputs,
'claude.model_headers_settings': value,
})
}
/> />
</Col> </Col>
</Row> </Row>
@@ -112,18 +128,28 @@ export default function SettingClaudeModel(props) {
<Form.TextArea <Form.TextArea
label={t('缺省 MaxTokens')} label={t('缺省 MaxTokens')}
field={'claude.default_max_tokens'} field={'claude.default_max_tokens'}
placeholder={t('为一个 JSON 文本,例如:') + '\n' + JSON.stringify(CLAUDE_DEFAULT_MAX_TOKENS, null, 2)} placeholder={
extraText={t('示例') + '\n' + JSON.stringify(CLAUDE_DEFAULT_MAX_TOKENS, null, 2)} t('为一个 JSON 文本,例如:') +
'\n' +
JSON.stringify(CLAUDE_DEFAULT_MAX_TOKENS, null, 2)
}
extraText={
t('示例') +
'\n' +
JSON.stringify(CLAUDE_DEFAULT_MAX_TOKENS, null, 2)
}
autosize={{ minRows: 6, maxRows: 12 }} autosize={{ minRows: 6, maxRows: 12 }}
trigger='blur' trigger='blur'
stopValidateWithError stopValidateWithError
rules={[ rules={[
{ {
validator: (rule, value) => verifyJSON(value), validator: (rule, value) => verifyJSON(value),
message: t('不是合法的 JSON 字符串') message: t('不是合法的 JSON 字符串'),
} },
]} ]}
onChange={(value) => setInputs({ ...inputs, 'claude.default_max_tokens': value })} onChange={(value) =>
setInputs({ ...inputs, 'claude.default_max_tokens': value })
}
/> />
</Col> </Col>
</Row> </Row>
@@ -132,7 +158,12 @@ export default function SettingClaudeModel(props) {
<Form.Switch <Form.Switch
label={t('启用Claude思考适配-thinking后缀')} label={t('启用Claude思考适配-thinking后缀')}
field={'claude.thinking_adapter_enabled'} field={'claude.thinking_adapter_enabled'}
onChange={(value) => setInputs({ ...inputs, 'claude.thinking_adapter_enabled': value })} onChange={(value) =>
setInputs({
...inputs,
'claude.thinking_adapter_enabled': value,
})
}
/> />
</Col> </Col>
</Row> </Row>
@@ -140,7 +171,9 @@ export default function SettingClaudeModel(props) {
<Col span={16}> <Col span={16}>
{/*//展示MaxTokens和BudgetTokens的计算公式, 并展示实际数字*/} {/*//展示MaxTokens和BudgetTokens的计算公式, 并展示实际数字*/}
<Text> <Text>
{t('Claude思考适配 BudgetTokens = MaxTokens * BudgetTokens 百分比')} {t(
'Claude思考适配 BudgetTokens = MaxTokens * BudgetTokens 百分比',
)}
</Text> </Text>
</Col> </Col>
</Row> </Row>
@@ -153,7 +186,12 @@ export default function SettingClaudeModel(props) {
extraText={t('0.1-1之间的小数')} extraText={t('0.1-1之间的小数')}
min={0.1} min={0.1}
max={1} max={1}
onChange={(value) => setInputs({ ...inputs, 'claude.thinking_adapter_budget_tokens_percentage': value })} onChange={(value) =>
setInputs({
...inputs,
'claude.thinking_adapter_budget_tokens_percentage': value,
})
}
/> />
</Col> </Col>
</Row> </Row>

View File

@@ -5,20 +5,20 @@ import {
API, API,
showError, showError,
showSuccess, showSuccess,
showWarning, verifyJSON showWarning,
verifyJSON,
} from '../../../helpers'; } from '../../../helpers';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
const GEMINI_SETTING_EXAMPLE = { const GEMINI_SETTING_EXAMPLE = {
'default': 'OFF', default: 'OFF',
'HARM_CATEGORY_CIVIC_INTEGRITY': 'BLOCK_NONE', HARM_CATEGORY_CIVIC_INTEGRITY: 'BLOCK_NONE',
}; };
const GEMINI_VERSION_EXAMPLE = { const GEMINI_VERSION_EXAMPLE = {
'default': 'v1beta', default: 'v1beta',
}; };
export default function SettingGeminiModel(props) { export default function SettingGeminiModel(props) {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -51,7 +51,8 @@ export default function SettingGeminiModel(props) {
if (requestQueue.length === 1) { if (requestQueue.length === 1) {
if (res.includes(undefined)) return; if (res.includes(undefined)) return;
} else if (requestQueue.length > 1) { } else if (requestQueue.length > 1) {
if (res.includes(undefined)) return showError(t('部分保存失败,请重试')); if (res.includes(undefined))
return showError(t('部分保存失败,请重试'));
} }
showSuccess(t('保存成功')); showSuccess(t('保存成功'));
props.refresh(); props.refresh();
@@ -89,19 +90,27 @@ export default function SettingGeminiModel(props) {
<Col xs={24} sm={12} md={8} lg={8} xl={8}> <Col xs={24} sm={12} md={8} lg={8} xl={8}>
<Form.TextArea <Form.TextArea
label={t('Gemini安全设置')} label={t('Gemini安全设置')}
placeholder={t('为一个 JSON 文本,例如:') + '\n' + JSON.stringify(GEMINI_SETTING_EXAMPLE, null, 2)} placeholder={
t('为一个 JSON 文本,例如:') +
'\n' +
JSON.stringify(GEMINI_SETTING_EXAMPLE, null, 2)
}
field={'gemini.safety_settings'} field={'gemini.safety_settings'}
extraText={t('default为默认设置可单独设置每个分类的安全等级')} extraText={t(
'default为默认设置可单独设置每个分类的安全等级',
)}
autosize={{ minRows: 6, maxRows: 12 }} autosize={{ minRows: 6, maxRows: 12 }}
trigger='blur' trigger='blur'
stopValidateWithError stopValidateWithError
rules={[ rules={[
{ {
validator: (rule, value) => verifyJSON(value), validator: (rule, value) => verifyJSON(value),
message: t('不是合法的 JSON 字符串') message: t('不是合法的 JSON 字符串'),
} },
]} ]}
onChange={(value) => setInputs({ ...inputs, 'gemini.safety_settings': value })} onChange={(value) =>
setInputs({ ...inputs, 'gemini.safety_settings': value })
}
/> />
</Col> </Col>
</Row> </Row>
@@ -109,7 +118,11 @@ export default function SettingGeminiModel(props) {
<Col xs={24} sm={12} md={8} lg={8} xl={8}> <Col xs={24} sm={12} md={8} lg={8} xl={8}>
<Form.TextArea <Form.TextArea
label={t('Gemini版本设置')} label={t('Gemini版本设置')}
placeholder={t('为一个 JSON 文本,例如:') + '\n' + JSON.stringify(GEMINI_VERSION_EXAMPLE, null, 2)} placeholder={
t('为一个 JSON 文本,例如:') +
'\n' +
JSON.stringify(GEMINI_VERSION_EXAMPLE, null, 2)
}
field={'gemini.version_settings'} field={'gemini.version_settings'}
extraText={t('default为默认设置可单独设置每个模型的版本')} extraText={t('default为默认设置可单独设置每个模型的版本')}
autosize={{ minRows: 6, maxRows: 12 }} autosize={{ minRows: 6, maxRows: 12 }}
@@ -118,10 +131,12 @@ export default function SettingGeminiModel(props) {
rules={[ rules={[
{ {
validator: (rule, value) => verifyJSON(value), validator: (rule, value) => verifyJSON(value),
message: t('不是合法的 JSON 字符串') message: t('不是合法的 JSON 字符串'),
} },
]} ]}
onChange={(value) => setInputs({ ...inputs, 'gemini.version_settings': value })} onChange={(value) =>
setInputs({ ...inputs, 'gemini.version_settings': value })
}
/> />
</Col> </Col>
</Row> </Row>

View File

@@ -5,7 +5,8 @@ import {
API, API,
showError, showError,
showSuccess, showSuccess,
showWarning, verifyJSON showWarning,
verifyJSON,
} from '../../../helpers'; } from '../../../helpers';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@@ -40,7 +41,8 @@ export default function SettingGlobalModel(props) {
if (requestQueue.length === 1) { if (requestQueue.length === 1) {
if (res.includes(undefined)) return; if (res.includes(undefined)) return;
} else if (requestQueue.length > 1) { } else if (requestQueue.length > 1) {
if (res.includes(undefined)) return showError(t('部分保存失败,请重试')); if (res.includes(undefined))
return showError(t('部分保存失败,请重试'));
} }
showSuccess(t('保存成功')); showSuccess(t('保存成功'));
props.refresh(); props.refresh();
@@ -79,8 +81,15 @@ export default function SettingGlobalModel(props) {
<Form.Switch <Form.Switch
label={t('启用请求透传')} label={t('启用请求透传')}
field={'global.pass_through_request_enabled'} field={'global.pass_through_request_enabled'}
onChange={(value) => setInputs({ ...inputs, 'global.pass_through_request_enabled': value })} onChange={(value) =>
extraText={'开启后,所有请求将直接透传给上游,不会进行任何处理(重定向和渠道适配也将失效),请谨慎开启'} setInputs({
...inputs,
'global.pass_through_request_enabled': value,
})
}
extraText={
'开启后,所有请求将直接透传给上游,不会进行任何处理(重定向和渠道适配也将失效),请谨慎开启'
}
/> />
</Col> </Col>
</Row> </Row>

View File

@@ -15,50 +15,59 @@ export default function GroupRatioSettings(props) {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [inputs, setInputs] = useState({ const [inputs, setInputs] = useState({
GroupRatio: '', GroupRatio: '',
UserUsableGroups: '' UserUsableGroups: '',
}); });
const refForm = useRef(); const refForm = useRef();
const [inputsRow, setInputsRow] = useState(inputs); const [inputsRow, setInputsRow] = useState(inputs);
async function onSubmit() { async function onSubmit() {
try { try {
await refForm.current.validate().then(() => { await refForm.current
const updateArray = compareObjects(inputs, inputsRow); .validate()
if (!updateArray.length) return showWarning(t('你似乎并没有修改什么')); .then(() => {
const updateArray = compareObjects(inputs, inputsRow);
if (!updateArray.length)
return showWarning(t('你似乎并没有修改什么'));
const requestQueue = updateArray.map((item) => { const requestQueue = updateArray.map((item) => {
const value = typeof inputs[item.key] === 'boolean' const value =
? String(inputs[item.key]) typeof inputs[item.key] === 'boolean'
: inputs[item.key]; ? String(inputs[item.key])
return API.put('/api/option/', { key: item.key, value }); : inputs[item.key];
}); return API.put('/api/option/', { key: item.key, value });
setLoading(true);
Promise.all(requestQueue)
.then((res) => {
if (res.includes(undefined)) {
return showError(requestQueue.length > 1 ? t('部分保存失败,请重试') : t('保存失败'));
}
for (let i = 0; i < res.length; i++) {
if (!res[i].data.success) {
return showError(res[i].data.message);
}
}
showSuccess(t('保存成功'));
props.refresh();
})
.catch(error => {
console.error('Unexpected error:', error);
showError(t('保存失败,请重试'));
})
.finally(() => {
setLoading(false);
}); });
}).catch(() => {
showError(t('请检查输入')); setLoading(true);
}); Promise.all(requestQueue)
.then((res) => {
if (res.includes(undefined)) {
return showError(
requestQueue.length > 1
? t('部分保存失败,请重试')
: t('保存失败'),
);
}
for (let i = 0; i < res.length; i++) {
if (!res[i].data.success) {
return showError(res[i].data.message);
}
}
showSuccess(t('保存成功'));
props.refresh();
})
.catch((error) => {
console.error('Unexpected error:', error);
showError(t('保存失败,请重试'));
})
.finally(() => {
setLoading(false);
});
})
.catch(() => {
showError(t('请检查输入'));
});
} catch (error) { } catch (error) {
showError(t('请检查输入')); showError(t('请检查输入'));
console.error(error); console.error(error);
@@ -97,10 +106,12 @@ export default function GroupRatioSettings(props) {
rules={[ rules={[
{ {
validator: (rule, value) => verifyJSON(value), validator: (rule, value) => verifyJSON(value),
message: t('不是合法的 JSON 字符串') message: t('不是合法的 JSON 字符串'),
} },
]} ]}
onChange={(value) => setInputs({ ...inputs, GroupRatio: value })} onChange={(value) =>
setInputs({ ...inputs, GroupRatio: value })
}
/> />
</Col> </Col>
</Row> </Row>
@@ -116,10 +127,12 @@ export default function GroupRatioSettings(props) {
rules={[ rules={[
{ {
validator: (rule, value) => verifyJSON(value), validator: (rule, value) => verifyJSON(value),
message: t('不是合法的 JSON 字符串') message: t('不是合法的 JSON 字符串'),
} },
]} ]}
onChange={(value) => setInputs({ ...inputs, UserUsableGroups: value })} onChange={(value) =>
setInputs({ ...inputs, UserUsableGroups: value })
}
/> />
</Col> </Col>
</Row> </Row>

View File

@@ -1,5 +1,13 @@
import React, { useEffect, useState, useRef } from 'react'; import React, { useEffect, useState, useRef } from 'react';
import { Button, Col, Form, Popconfirm, Row, Space, Spin } from '@douyinfe/semi-ui'; import {
Button,
Col,
Form,
Popconfirm,
Row,
Space,
Spin,
} from '@douyinfe/semi-ui';
import { import {
compareObjects, compareObjects,
API, API,
@@ -24,43 +32,52 @@ export default function ModelRatioSettings(props) {
async function onSubmit() { async function onSubmit() {
try { try {
await refForm.current.validate().then(() => { await refForm.current
const updateArray = compareObjects(inputs, inputsRow); .validate()
if (!updateArray.length) return showWarning(t('你似乎并没有修改什么')); .then(() => {
const updateArray = compareObjects(inputs, inputsRow);
if (!updateArray.length)
return showWarning(t('你似乎并没有修改什么'));
const requestQueue = updateArray.map((item) => { const requestQueue = updateArray.map((item) => {
const value = typeof inputs[item.key] === 'boolean' const value =
? String(inputs[item.key]) typeof inputs[item.key] === 'boolean'
: inputs[item.key]; ? String(inputs[item.key])
return API.put('/api/option/', { key: item.key, value }); : inputs[item.key];
}); return API.put('/api/option/', { key: item.key, value });
setLoading(true);
Promise.all(requestQueue)
.then((res) => {
if (res.includes(undefined)) {
return showError(requestQueue.length > 1 ? t('部分保存失败,请重试') : t('保存失败'));
}
for (let i = 0; i < res.length; i++) {
if (!res[i].data.success) {
return showError(res[i].data.message);
}
}
showSuccess(t('保存成功'));
props.refresh();
})
.catch(error => {
console.error('Unexpected error:', error);
showError(t('保存失败,请重试'));
})
.finally(() => {
setLoading(false);
}); });
}).catch(() => {
showError(t('请检查输入')); setLoading(true);
}); Promise.all(requestQueue)
.then((res) => {
if (res.includes(undefined)) {
return showError(
requestQueue.length > 1
? t('部分保存失败,请重试')
: t('保存失败'),
);
}
for (let i = 0; i < res.length; i++) {
if (!res[i].data.success) {
return showError(res[i].data.message);
}
}
showSuccess(t('保存成功'));
props.refresh();
})
.catch((error) => {
console.error('Unexpected error:', error);
showError(t('保存失败,请重试'));
})
.finally(() => {
setLoading(false);
});
})
.catch(() => {
showError(t('请检查输入'));
});
} catch (error) { } catch (error) {
showError(t('请检查输入')); showError(t('请检查输入'));
console.error(error); console.error(error);
@@ -106,7 +123,9 @@ export default function ModelRatioSettings(props) {
<Form.TextArea <Form.TextArea
label={t('模型固定价格')} label={t('模型固定价格')}
extraText={t('一次调用消耗多少刀,优先级大于模型倍率')} extraText={t('一次调用消耗多少刀,优先级大于模型倍率')}
placeholder={t('为一个 JSON 文本,键为模型名称,值为一次调用消耗多少刀,比如 "gpt-4-gizmo-*": 0.1一次消耗0.1刀')} placeholder={t(
'为一个 JSON 文本,键为模型名称,值为一次调用消耗多少刀,比如 "gpt-4-gizmo-*": 0.1一次消耗0.1刀',
)}
field={'ModelPrice'} field={'ModelPrice'}
autosize={{ minRows: 6, maxRows: 12 }} autosize={{ minRows: 6, maxRows: 12 }}
trigger='blur' trigger='blur'
@@ -114,10 +133,12 @@ export default function ModelRatioSettings(props) {
rules={[ rules={[
{ {
validator: (rule, value) => verifyJSON(value), validator: (rule, value) => verifyJSON(value),
message: '不是合法的 JSON 字符串' message: '不是合法的 JSON 字符串',
} },
]} ]}
onChange={(value) => setInputs({ ...inputs, ModelPrice: value })} onChange={(value) =>
setInputs({ ...inputs, ModelPrice: value })
}
/> />
</Col> </Col>
</Row> </Row>
@@ -133,10 +154,12 @@ export default function ModelRatioSettings(props) {
rules={[ rules={[
{ {
validator: (rule, value) => verifyJSON(value), validator: (rule, value) => verifyJSON(value),
message: '不是合法的 JSON 字符串' message: '不是合法的 JSON 字符串',
} },
]} ]}
onChange={(value) => setInputs({ ...inputs, ModelRatio: value })} onChange={(value) =>
setInputs({ ...inputs, ModelRatio: value })
}
/> />
</Col> </Col>
</Row> </Row>
@@ -152,10 +175,12 @@ export default function ModelRatioSettings(props) {
rules={[ rules={[
{ {
validator: (rule, value) => verifyJSON(value), validator: (rule, value) => verifyJSON(value),
message: '不是合法的 JSON 字符串' message: '不是合法的 JSON 字符串',
} },
]} ]}
onChange={(value) => setInputs({ ...inputs, CacheRatio: value })} onChange={(value) =>
setInputs({ ...inputs, CacheRatio: value })
}
/> />
</Col> </Col>
</Row> </Row>
@@ -172,10 +197,12 @@ export default function ModelRatioSettings(props) {
rules={[ rules={[
{ {
validator: (rule, value) => verifyJSON(value), validator: (rule, value) => verifyJSON(value),
message: '不是合法的 JSON 字符串' message: '不是合法的 JSON 字符串',
} },
]} ]}
onChange={(value) => setInputs({ ...inputs, CompletionRatio: value })} onChange={(value) =>
setInputs({ ...inputs, CompletionRatio: value })
}
/> />
</Col> </Col>
</Row> </Row>

View File

@@ -1,6 +1,22 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Table, Button, Input, Modal, Form, Space, Typography, Radio, Notification } from '@douyinfe/semi-ui'; import {
import { IconDelete, IconPlus, IconSearch, IconSave, IconBolt } from '@douyinfe/semi-icons'; Table,
Button,
Input,
Modal,
Form,
Space,
Typography,
Radio,
Notification,
} from '@douyinfe/semi-ui';
import {
IconDelete,
IconPlus,
IconSearch,
IconSave,
IconBolt,
} from '@douyinfe/semi-icons';
import { showError, showSuccess } from '../../../helpers'; import { showError, showSuccess } from '../../../helpers';
import { API } from '../../../helpers'; import { API } from '../../../helpers';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@@ -20,7 +36,8 @@ export default function ModelRatioNotSetEditor(props) {
const [batchFillType, setBatchFillType] = useState('ratio'); const [batchFillType, setBatchFillType] = useState('ratio');
const [batchFillValue, setBatchFillValue] = useState(''); const [batchFillValue, setBatchFillValue] = useState('');
const [batchRatioValue, setBatchRatioValue] = useState(''); const [batchRatioValue, setBatchRatioValue] = useState('');
const [batchCompletionRatioValue, setBatchCompletionRatioValue] = useState(''); const [batchCompletionRatioValue, setBatchCompletionRatioValue] =
useState('');
const { Text } = Typography; const { Text } = Typography;
// 定义可选的每页显示条数 // 定义可选的每页显示条数
const pageSizeOptions = [10, 20, 50, 100]; const pageSizeOptions = [10, 20, 50, 100];
@@ -38,7 +55,7 @@ export default function ModelRatioNotSetEditor(props) {
console.error(t('获取启用模型失败:'), error); console.error(t('获取启用模型失败:'), error);
showError(t('获取启用模型失败')); showError(t('获取启用模型失败'));
} }
} };
useEffect(() => { useEffect(() => {
// 获取所有启用的模型 // 获取所有启用的模型
@@ -52,7 +69,7 @@ export default function ModelRatioNotSetEditor(props) {
const completionRatio = JSON.parse(props.options.CompletionRatio || '{}'); const completionRatio = JSON.parse(props.options.CompletionRatio || '{}');
// 找出所有未设置价格和倍率的模型 // 找出所有未设置价格和倍率的模型
const unsetModels = enabledModels.filter(modelName => { const unsetModels = enabledModels.filter((modelName) => {
const hasPrice = modelPrice[modelName] !== undefined; const hasPrice = modelPrice[modelName] !== undefined;
const hasRatio = modelRatio[modelName] !== undefined; const hasRatio = modelRatio[modelName] !== undefined;
@@ -61,11 +78,11 @@ export default function ModelRatioNotSetEditor(props) {
}); });
// 创建模型数据 // 创建模型数据
const modelData = unsetModels.map(name => ({ const modelData = unsetModels.map((name) => ({
name, name,
price: modelPrice[name] || '', price: modelPrice[name] || '',
ratio: modelRatio[name] || '', ratio: modelRatio[name] || '',
completionRatio: completionRatio[name] || '' completionRatio: completionRatio[name] || '',
})); }));
setModels(modelData); setModels(modelData);
@@ -94,8 +111,10 @@ export default function ModelRatioNotSetEditor(props) {
}; };
// 在 return 语句之前,先处理过滤和分页逻辑 // 在 return 语句之前,先处理过滤和分页逻辑
const filteredModels = models.filter(model => const filteredModels = models.filter((model) =>
searchText ? model.name.toLowerCase().includes(searchText.toLowerCase()) : true searchText
? model.name.toLowerCase().includes(searchText.toLowerCase())
: true,
); );
// 然后基于过滤后的数据计算分页数据 // 然后基于过滤后的数据计算分页数据
@@ -106,19 +125,23 @@ export default function ModelRatioNotSetEditor(props) {
const output = { const output = {
ModelPrice: JSON.parse(props.options.ModelPrice || '{}'), ModelPrice: JSON.parse(props.options.ModelPrice || '{}'),
ModelRatio: JSON.parse(props.options.ModelRatio || '{}'), ModelRatio: JSON.parse(props.options.ModelRatio || '{}'),
CompletionRatio: JSON.parse(props.options.CompletionRatio || '{}') CompletionRatio: JSON.parse(props.options.CompletionRatio || '{}'),
}; };
try { try {
// 数据转换 - 只处理已修改的模型 // 数据转换 - 只处理已修改的模型
models.forEach(model => { models.forEach((model) => {
// 只有当用户设置了值时才更新 // 只有当用户设置了值时才更新
if (model.price !== '') { if (model.price !== '') {
// 如果价格不为空,则转换为浮点数,忽略倍率参数 // 如果价格不为空,则转换为浮点数,忽略倍率参数
output.ModelPrice[model.name] = parseFloat(model.price); output.ModelPrice[model.name] = parseFloat(model.price);
} else { } else {
if (model.ratio !== '') output.ModelRatio[model.name] = parseFloat(model.ratio); if (model.ratio !== '')
if (model.completionRatio !== '') output.CompletionRatio[model.name] = parseFloat(model.completionRatio); output.ModelRatio[model.name] = parseFloat(model.ratio);
if (model.completionRatio !== '')
output.CompletionRatio[model.name] = parseFloat(
model.completionRatio,
);
} }
}); });
@@ -126,13 +149,13 @@ export default function ModelRatioNotSetEditor(props) {
const finalOutput = { const finalOutput = {
ModelPrice: JSON.stringify(output.ModelPrice, null, 2), ModelPrice: JSON.stringify(output.ModelPrice, null, 2),
ModelRatio: JSON.stringify(output.ModelRatio, null, 2), ModelRatio: JSON.stringify(output.ModelRatio, null, 2),
CompletionRatio: JSON.stringify(output.CompletionRatio, null, 2) CompletionRatio: JSON.stringify(output.CompletionRatio, null, 2),
}; };
const requestQueue = Object.entries(finalOutput).map(([key, value]) => { const requestQueue = Object.entries(finalOutput).map(([key, value]) => {
return API.put('/api/option/', { return API.put('/api/option/', {
key, key,
value value,
}); });
}); });
@@ -159,7 +182,6 @@ export default function ModelRatioNotSetEditor(props) {
props.refresh(); props.refresh();
// 重新获取未设置的模型 // 重新获取未设置的模型
getAllEnabledModels(); getAllEnabledModels();
} catch (error) { } catch (error) {
console.error(t('保存失败:'), error); console.error(t('保存失败:'), error);
showError(t('保存失败,请重试')); showError(t('保存失败,请重试'));
@@ -182,9 +204,9 @@ export default function ModelRatioNotSetEditor(props) {
<Input <Input
value={text} value={text}
placeholder={t('按量计费')} placeholder={t('按量计费')}
onChange={value => updateModel(record.name, 'price', value)} onChange={(value) => updateModel(record.name, 'price', value)}
/> />
) ),
}, },
{ {
title: t('模型倍率'), title: t('模型倍率'),
@@ -195,9 +217,9 @@ export default function ModelRatioNotSetEditor(props) {
value={text} value={text}
placeholder={record.price !== '' ? t('模型倍率') : t('输入模型倍率')} placeholder={record.price !== '' ? t('模型倍率') : t('输入模型倍率')}
disabled={record.price !== ''} disabled={record.price !== ''}
onChange={value => updateModel(record.name, 'ratio', value)} onChange={(value) => updateModel(record.name, 'ratio', value)}
/> />
) ),
}, },
{ {
title: t('补全倍率'), title: t('补全倍率'),
@@ -208,10 +230,12 @@ export default function ModelRatioNotSetEditor(props) {
value={text} value={text}
placeholder={record.price !== '' ? t('补全倍率') : t('输入补全倍率')} placeholder={record.price !== '' ? t('补全倍率') : t('输入补全倍率')}
disabled={record.price !== ''} disabled={record.price !== ''}
onChange={value => updateModel(record.name, 'completionRatio', value)} onChange={(value) =>
updateModel(record.name, 'completionRatio', value)
}
/> />
) ),
} },
]; ];
const updateModel = (name, field, value) => { const updateModel = (name, field, value) => {
@@ -219,27 +243,28 @@ export default function ModelRatioNotSetEditor(props) {
showError(t('请输入数字')); showError(t('请输入数字'));
return; return;
} }
setModels(prev => setModels((prev) =>
prev.map(model => prev.map((model) =>
model.name === name model.name === name ? { ...model, [field]: value } : model,
? { ...model, [field]: value } ),
: model
)
); );
}; };
const addModel = (values) => { const addModel = (values) => {
// 检查模型名称是否存在, 如果存在则拒绝添加 // 检查模型名称是否存在, 如果存在则拒绝添加
if (models.some(model => model.name === values.name)) { if (models.some((model) => model.name === values.name)) {
showError(t('模型名称已存在')); showError(t('模型名称已存在'));
return; return;
} }
setModels(prev => [{ setModels((prev) => [
name: values.name, {
price: values.price || '', name: values.name,
ratio: values.ratio || '', price: values.price || '',
completionRatio: values.completionRatio || '' ratio: values.ratio || '',
}, ...prev]); completionRatio: values.completionRatio || '',
},
...prev,
]);
setVisible(false); setVisible(false);
showSuccess(t('添加成功')); showSuccess(t('添加成功'));
}; };
@@ -272,39 +297,39 @@ export default function ModelRatioNotSetEditor(props) {
} }
// 根据选择的类型批量更新模型 // 根据选择的类型批量更新模型
setModels(prev => setModels((prev) =>
prev.map(model => { prev.map((model) => {
if (selectedRowKeys.includes(model.name)) { if (selectedRowKeys.includes(model.name)) {
if (batchFillType === 'price') { if (batchFillType === 'price') {
return { return {
...model, ...model,
price: batchFillValue, price: batchFillValue,
ratio: '', ratio: '',
completionRatio: '' completionRatio: '',
}; };
} else if (batchFillType === 'ratio') { } else if (batchFillType === 'ratio') {
return { return {
...model, ...model,
price: '', price: '',
ratio: batchFillValue ratio: batchFillValue,
}; };
} else if (batchFillType === 'completionRatio') { } else if (batchFillType === 'completionRatio') {
return { return {
...model, ...model,
price: '', price: '',
completionRatio: batchFillValue completionRatio: batchFillValue,
}; };
} else if (batchFillType === 'bothRatio') { } else if (batchFillType === 'bothRatio') {
return { return {
...model, ...model,
price: '', price: '',
ratio: batchRatioValue, ratio: batchRatioValue,
completionRatio: batchCompletionRatioValue completionRatio: batchCompletionRatioValue,
}; };
} }
} }
return model; return model;
}) }),
); );
setBatchVisible(false); setBatchVisible(false);
@@ -312,9 +337,14 @@ export default function ModelRatioNotSetEditor(props) {
title: t('批量设置成功'), title: t('批量设置成功'),
content: t('已为 {{count}} 个模型设置{{type}}', { content: t('已为 {{count}} 个模型设置{{type}}', {
count: selectedRowKeys.length, count: selectedRowKeys.length,
type: batchFillType === 'price' ? t('固定价格') : type:
batchFillType === 'ratio' ? t('模型倍率') : batchFillType === 'price'
batchFillType === 'completionRatio' ? t('补全倍率') : t('模型倍率和补全倍率') ? t('固定价格')
: batchFillType === 'ratio'
? t('模型倍率')
: batchFillType === 'completionRatio'
? t('补全倍率')
: t('模型倍率和补全倍率'),
}), }),
duration: 3, duration: 3,
}); });
@@ -342,56 +372,63 @@ export default function ModelRatioNotSetEditor(props) {
return ( return (
<> <>
<Space vertical align="start" style={{ width: '100%' }}> <Space vertical align='start' style={{ width: '100%' }}>
<Space> <Space>
<Button icon={<IconPlus />} onClick={() => setVisible(true)}> <Button icon={<IconPlus />} onClick={() => setVisible(true)}>
{t('添加模型')} {t('添加模型')}
</Button> </Button>
<Button <Button
icon={<IconBolt />} icon={<IconBolt />}
type="secondary" type='secondary'
onClick={() => setBatchVisible(true)} onClick={() => setBatchVisible(true)}
disabled={selectedRowKeys.length === 0} disabled={selectedRowKeys.length === 0}
> >
{t('批量设置')} ({selectedRowKeys.length}) {t('批量设置')} ({selectedRowKeys.length})
</Button> </Button>
<Button type="primary" icon={<IconSave />} onClick={SubmitData} loading={loading}> <Button
type='primary'
icon={<IconSave />}
onClick={SubmitData}
loading={loading}
>
{t('应用更改')} {t('应用更改')}
</Button> </Button>
<Input <Input
prefix={<IconSearch />} prefix={<IconSearch />}
placeholder={t('搜索模型名称')} placeholder={t('搜索模型名称')}
value={searchText} value={searchText}
onChange={value => { onChange={(value) => {
setSearchText(value) setSearchText(value);
setCurrentPage(1); setCurrentPage(1);
}} }}
style={{ width: 200 }} style={{ width: 200 }}
/> />
</Space> </Space>
<Text>{t('此页面仅显示未设置价格或倍率的模型,设置后将自动从列表中移除')}</Text> <Text>
{t('此页面仅显示未设置价格或倍率的模型,设置后将自动从列表中移除')}
</Text>
<Table <Table
columns={columns} columns={columns}
dataSource={pagedData} dataSource={pagedData}
rowSelection={rowSelection} rowSelection={rowSelection}
rowKey="name" rowKey='name'
pagination={{ pagination={{
currentPage: currentPage, currentPage: currentPage,
pageSize: pageSize, pageSize: pageSize,
total: filteredModels.length, total: filteredModels.length,
onPageChange: page => setCurrentPage(page), onPageChange: (page) => setCurrentPage(page),
onPageSizeChange: handlePageSizeChange, onPageSizeChange: handlePageSizeChange,
pageSizeOptions: pageSizeOptions, pageSizeOptions: pageSizeOptions,
formatPageText: (page) => formatPageText: (page) =>
t('第 {{start}} - {{end}} 条,共 {{total}} 条', { t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
start: page.currentStart, start: page.currentStart,
end: page.currentEnd, end: page.currentEnd,
total: filteredModels.length total: filteredModels.length,
}), }),
showTotal: true, showTotal: true,
showSizeChanger: true showSizeChanger: true,
}} }}
empty={ empty={
<div style={{ textAlign: 'center', padding: '20px' }}> <div style={{ textAlign: 'center', padding: '20px' }}>
@@ -412,45 +449,61 @@ export default function ModelRatioNotSetEditor(props) {
> >
<Form> <Form>
<Form.Input <Form.Input
field="name" field='name'
label={t('模型名称')} label={t('模型名称')}
placeholder="strawberry" placeholder='strawberry'
required required
onChange={value => setCurrentModel(prev => ({ ...prev, name: value }))} onChange={(value) =>
setCurrentModel((prev) => ({ ...prev, name: value }))
}
/> />
<Form.Switch <Form.Switch
field="priceMode" field='priceMode'
label={<>{t('定价模式')}{currentModel?.priceMode ? t("固定价格") : t("倍率模式")}</>} label={
onChange={checked => { <>
setCurrentModel(prev => ({ {t('定价模式')}
{currentModel?.priceMode ? t('固定价格') : t('倍率模式')}
</>
}
onChange={(checked) => {
setCurrentModel((prev) => ({
...prev, ...prev,
price: '', price: '',
ratio: '', ratio: '',
completionRatio: '', completionRatio: '',
priceMode: checked priceMode: checked,
})); }));
}} }}
/> />
{currentModel?.priceMode ? ( {currentModel?.priceMode ? (
<Form.Input <Form.Input
field="price" field='price'
label={t('固定价格(每次)')} label={t('固定价格(每次)')}
placeholder={t('输入每次价格')} placeholder={t('输入每次价格')}
onChange={value => setCurrentModel(prev => ({ ...prev, price: value }))} onChange={(value) =>
setCurrentModel((prev) => ({ ...prev, price: value }))
}
/> />
) : ( ) : (
<> <>
<Form.Input <Form.Input
field="ratio" field='ratio'
label={t('模型倍率')} label={t('模型倍率')}
placeholder={t('输入模型倍率')} placeholder={t('输入模型倍率')}
onChange={value => setCurrentModel(prev => ({ ...prev, ratio: value }))} onChange={(value) =>
setCurrentModel((prev) => ({ ...prev, ratio: value }))
}
/> />
<Form.Input <Form.Input
field="completionRatio" field='completionRatio'
label={t('补全倍率')} label={t('补全倍率')}
placeholder={t('输入补全价格')} placeholder={t('输入补全价格')}
onChange={value => setCurrentModel(prev => ({ ...prev, completionRatio: value }))} onChange={(value) =>
setCurrentModel((prev) => ({
...prev,
completionRatio: value,
}))
}
/> />
</> </>
)} )}
@@ -500,23 +553,23 @@ export default function ModelRatioNotSetEditor(props) {
{batchFillType === 'bothRatio' ? ( {batchFillType === 'bothRatio' ? (
<> <>
<Form.Input <Form.Input
field="batchRatioValue" field='batchRatioValue'
label={t('模型倍率值')} label={t('模型倍率值')}
placeholder={t('请输入模型倍率')} placeholder={t('请输入模型倍率')}
value={batchRatioValue} value={batchRatioValue}
onChange={value => setBatchRatioValue(value)} onChange={(value) => setBatchRatioValue(value)}
/> />
<Form.Input <Form.Input
field="batchCompletionRatioValue" field='batchCompletionRatioValue'
label={t('补全倍率值')} label={t('补全倍率值')}
placeholder={t('请输入补全倍率')} placeholder={t('请输入补全倍率')}
value={batchCompletionRatioValue} value={batchCompletionRatioValue}
onChange={value => setBatchCompletionRatioValue(value)} onChange={(value) => setBatchCompletionRatioValue(value)}
/> />
</> </>
) : ( ) : (
<Form.Input <Form.Input
field="batchFillValue" field='batchFillValue'
label={ label={
batchFillType === 'price' batchFillType === 'price'
? t('固定价格值') ? t('固定价格值')
@@ -526,20 +579,26 @@ export default function ModelRatioNotSetEditor(props) {
} }
placeholder={t('请输入数值')} placeholder={t('请输入数值')}
value={batchFillValue} value={batchFillValue}
onChange={value => setBatchFillValue(value)} onChange={(value) => setBatchFillValue(value)}
/> />
)} )}
<Text type="tertiary"> <Text type='tertiary'>
{t('将为选中的 ')} <Text strong>{selectedRowKeys.length}</Text> {t(' ')} {t('将为选中的 ')} <Text strong>{selectedRowKeys.length}</Text>{' '}
{t(' 个模型设置相同的值')}
</Text> </Text>
<div style={{ marginTop: '8px' }}> <div style={{ marginTop: '8px' }}>
<Text type="tertiary"> <Text type='tertiary'>
{t('当前设置类型: ')} <Text strong>{ {t('当前设置类型: ')}{' '}
batchFillType === 'price' ? t('固定价格') : <Text strong>
batchFillType === 'ratio' ? t('模型倍率') : {batchFillType === 'price'
batchFillType === 'completionRatio' ? t('补全倍率') : t('模型倍率和补全倍率') ? t('固定价格')
}</Text> : batchFillType === 'ratio'
? t('模型倍率')
: batchFillType === 'completionRatio'
? t('补全倍率')
: t('模型倍率和补全倍率')}
</Text>
</Text> </Text>
</div> </div>
</Form> </Form>

View File

@@ -1,7 +1,24 @@
// ModelSettingsVisualEditor.js // ModelSettingsVisualEditor.js
import React, { useContext, useEffect, useState, useRef } from 'react'; import React, { useContext, useEffect, useState, useRef } from 'react';
import { Table, Button, Input, Modal, Form, Space, RadioGroup, Radio, Tabs, TabPane } from '@douyinfe/semi-ui'; import {
import { IconDelete, IconPlus, IconSearch, IconSave, IconEdit } from '@douyinfe/semi-icons'; Table,
Button,
Input,
Modal,
Form,
Space,
RadioGroup,
Radio,
Tabs,
TabPane,
} from '@douyinfe/semi-ui';
import {
IconDelete,
IconPlus,
IconSearch,
IconSave,
IconEdit,
} from '@douyinfe/semi-icons';
import { showError, showSuccess } from '../../../helpers'; import { showError, showSuccess } from '../../../helpers';
import { API } from '../../../helpers'; import { API } from '../../../helpers';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@@ -20,7 +37,7 @@ export default function ModelSettingsVisualEditor(props) {
const [pricingSubMode, setPricingSubMode] = useState('ratio'); // 'ratio' or 'token-price' const [pricingSubMode, setPricingSubMode] = useState('ratio'); // 'ratio' or 'token-price'
const formRef = useRef(null); const formRef = useRef(null);
const pageSize = 10; const pageSize = 10;
const quotaPerUnit = getQuotaPerUnit() const quotaPerUnit = getQuotaPerUnit();
useEffect(() => { useEffect(() => {
try { try {
@@ -32,14 +49,15 @@ export default function ModelSettingsVisualEditor(props) {
const modelNames = new Set([ const modelNames = new Set([
...Object.keys(modelPrice), ...Object.keys(modelPrice),
...Object.keys(modelRatio), ...Object.keys(modelRatio),
...Object.keys(completionRatio) ...Object.keys(completionRatio),
]); ]);
const modelData = Array.from(modelNames).map(name => ({ const modelData = Array.from(modelNames).map((name) => ({
name, name,
price: modelPrice[name] === undefined ? '' : modelPrice[name], price: modelPrice[name] === undefined ? '' : modelPrice[name],
ratio: modelRatio[name] === undefined ? '' : modelRatio[name], ratio: modelRatio[name] === undefined ? '' : modelRatio[name],
completionRatio: completionRatio[name] === undefined ? '' : completionRatio[name] completionRatio:
completionRatio[name] === undefined ? '' : completionRatio[name],
})); }));
setModels(modelData); setModels(modelData);
@@ -56,8 +74,10 @@ export default function ModelSettingsVisualEditor(props) {
}; };
// 在 return 语句之前,先处理过滤和分页逻辑 // 在 return 语句之前,先处理过滤和分页逻辑
const filteredModels = models.filter(model => const filteredModels = models.filter((model) =>
searchText ? model.name.toLowerCase().includes(searchText.toLowerCase()) : true searchText
? model.name.toLowerCase().includes(searchText.toLowerCase())
: true,
); );
// 然后基于过滤后的数据计算分页数据 // 然后基于过滤后的数据计算分页数据
@@ -68,20 +88,24 @@ export default function ModelSettingsVisualEditor(props) {
const output = { const output = {
ModelPrice: {}, ModelPrice: {},
ModelRatio: {}, ModelRatio: {},
CompletionRatio: {} CompletionRatio: {},
}; };
let currentConvertModelName = ''; let currentConvertModelName = '';
try { try {
// 数据转换 // 数据转换
models.forEach(model => { models.forEach((model) => {
currentConvertModelName = model.name; currentConvertModelName = model.name;
if (model.price !== '') { if (model.price !== '') {
// 如果价格不为空,则转换为浮点数,忽略倍率参数 // 如果价格不为空,则转换为浮点数,忽略倍率参数
output.ModelPrice[model.name] = parseFloat(model.price) output.ModelPrice[model.name] = parseFloat(model.price);
} else { } else {
if (model.ratio !== '') output.ModelRatio[model.name] = parseFloat(model.ratio); if (model.ratio !== '')
if (model.completionRatio !== '') output.CompletionRatio[model.name] = parseFloat(model.completionRatio); output.ModelRatio[model.name] = parseFloat(model.ratio);
if (model.completionRatio !== '')
output.CompletionRatio[model.name] = parseFloat(
model.completionRatio,
);
} }
}); });
@@ -89,13 +113,13 @@ export default function ModelSettingsVisualEditor(props) {
const finalOutput = { const finalOutput = {
ModelPrice: JSON.stringify(output.ModelPrice, null, 2), ModelPrice: JSON.stringify(output.ModelPrice, null, 2),
ModelRatio: JSON.stringify(output.ModelRatio, null, 2), ModelRatio: JSON.stringify(output.ModelRatio, null, 2),
CompletionRatio: JSON.stringify(output.CompletionRatio, null, 2) CompletionRatio: JSON.stringify(output.CompletionRatio, null, 2),
}; };
const requestQueue = Object.entries(finalOutput).map(([key, value]) => { const requestQueue = Object.entries(finalOutput).map(([key, value]) => {
return API.put('/api/option/', { return API.put('/api/option/', {
key, key,
value value,
}); });
}); });
@@ -120,7 +144,6 @@ export default function ModelSettingsVisualEditor(props) {
showSuccess('保存成功'); showSuccess('保存成功');
props.refresh(); props.refresh();
} catch (error) { } catch (error) {
console.error('保存失败:', error); console.error('保存失败:', error);
showError('保存失败,请重试'); showError('保存失败,请重试');
@@ -143,9 +166,9 @@ export default function ModelSettingsVisualEditor(props) {
<Input <Input
value={text} value={text}
placeholder={t('按量计费')} placeholder={t('按量计费')}
onChange={value => updateModel(record.name, 'price', value)} onChange={(value) => updateModel(record.name, 'price', value)}
/> />
) ),
}, },
{ {
title: t('模型倍率'), title: t('模型倍率'),
@@ -156,9 +179,9 @@ export default function ModelSettingsVisualEditor(props) {
value={text} value={text}
placeholder={record.price !== '' ? t('模型倍率') : t('默认补全倍率')} placeholder={record.price !== '' ? t('模型倍率') : t('默认补全倍率')}
disabled={record.price !== ''} disabled={record.price !== ''}
onChange={value => updateModel(record.name, 'ratio', value)} onChange={(value) => updateModel(record.name, 'ratio', value)}
/> />
) ),
}, },
{ {
title: t('补全倍率'), title: t('补全倍率'),
@@ -169,9 +192,11 @@ export default function ModelSettingsVisualEditor(props) {
value={text} value={text}
placeholder={record.price !== '' ? t('补全倍率') : t('默认补全倍率')} placeholder={record.price !== '' ? t('补全倍率') : t('默认补全倍率')}
disabled={record.price !== ''} disabled={record.price !== ''}
onChange={value => updateModel(record.name, 'completionRatio', value)} onChange={(value) =>
updateModel(record.name, 'completionRatio', value)
}
/> />
) ),
}, },
{ {
title: t('操作'), title: t('操作'),
@@ -179,19 +204,18 @@ export default function ModelSettingsVisualEditor(props) {
render: (_, record) => ( render: (_, record) => (
<Space> <Space>
<Button <Button
type="primary" type='primary'
icon={<IconEdit />} icon={<IconEdit />}
onClick={() => editModel(record)} onClick={() => editModel(record)}
> ></Button>
</Button>
<Button <Button
icon={<IconDelete />} icon={<IconDelete />}
type="danger" type='danger'
onClick={() => deleteModel(record.name)} onClick={() => deleteModel(record.name)}
/> />
</Space> </Space>
) ),
} },
]; ];
const updateModel = (name, field, value) => { const updateModel = (name, field, value) => {
@@ -199,24 +223,25 @@ export default function ModelSettingsVisualEditor(props) {
showError('请输入数字'); showError('请输入数字');
return; return;
} }
setModels(prev => setModels((prev) =>
prev.map(model => prev.map((model) =>
model.name === name model.name === name ? { ...model, [field]: value } : model,
? { ...model, [field]: value } ),
: model
)
); );
}; };
const deleteModel = (name) => { const deleteModel = (name) => {
setModels(prev => prev.filter(model => model.name !== name)); setModels((prev) => prev.filter((model) => model.name !== name));
}; };
const calculateRatioFromTokenPrice = (tokenPrice) => { const calculateRatioFromTokenPrice = (tokenPrice) => {
return tokenPrice / 2; return tokenPrice / 2;
}; };
const calculateCompletionRatioFromPrices = (modelTokenPrice, completionTokenPrice) => { const calculateCompletionRatioFromPrices = (
modelTokenPrice,
completionTokenPrice,
) => {
if (!modelTokenPrice || modelTokenPrice === '0') { if (!modelTokenPrice || modelTokenPrice === '0') {
showError('模型价格不能为0'); showError('模型价格不能为0');
return ''; return '';
@@ -225,12 +250,11 @@ export default function ModelSettingsVisualEditor(props) {
}; };
const handleTokenPriceChange = (value) => { const handleTokenPriceChange = (value) => {
// Use a temporary variable to hold the new state // Use a temporary variable to hold the new state
let newState = { let newState = {
...(currentModel || {}), ...(currentModel || {}),
tokenPrice: value, tokenPrice: value,
ratio: 0 ratio: 0,
}; };
if (!isNaN(value) && value !== '') { if (!isNaN(value) && value !== '') {
@@ -244,12 +268,11 @@ export default function ModelSettingsVisualEditor(props) {
}; };
const handleCompletionTokenPriceChange = (value) => { const handleCompletionTokenPriceChange = (value) => {
// Use a temporary variable to hold the new state // Use a temporary variable to hold the new state
let newState = { let newState = {
...(currentModel || {}), ...(currentModel || {}),
completionTokenPrice: value, completionTokenPrice: value,
completionRatio: 0 completionRatio: 0,
}; };
if (!isNaN(value) && value !== '' && currentModel?.tokenPrice) { if (!isNaN(value) && value !== '' && currentModel?.tokenPrice) {
@@ -257,7 +280,10 @@ export default function ModelSettingsVisualEditor(props) {
const modelTokenPrice = parseFloat(currentModel.tokenPrice); const modelTokenPrice = parseFloat(currentModel.tokenPrice);
if (modelTokenPrice > 0) { if (modelTokenPrice > 0) {
const completionRatio = calculateCompletionRatioFromPrices(modelTokenPrice, completionTokenPrice); const completionRatio = calculateCompletionRatioFromPrices(
modelTokenPrice,
completionTokenPrice,
);
newState.completionRatio = completionRatio; newState.completionRatio = completionRatio;
} }
} }
@@ -268,34 +294,43 @@ export default function ModelSettingsVisualEditor(props) {
const addOrUpdateModel = (values) => { const addOrUpdateModel = (values) => {
// Check if we're editing an existing model or adding a new one // Check if we're editing an existing model or adding a new one
const existingModelIndex = models.findIndex(model => model.name === values.name); const existingModelIndex = models.findIndex(
(model) => model.name === values.name,
);
if (existingModelIndex >= 0) { if (existingModelIndex >= 0) {
// Update existing model // Update existing model
setModels(prev => prev.map((model, index) => setModels((prev) =>
index === existingModelIndex ? { prev.map((model, index) =>
name: values.name, index === existingModelIndex
price: values.price || '', ? {
ratio: values.ratio || '', name: values.name,
completionRatio: values.completionRatio || '' price: values.price || '',
} : model ratio: values.ratio || '',
)); completionRatio: values.completionRatio || '',
}
: model,
),
);
setVisible(false); setVisible(false);
showSuccess(t('更新成功')); showSuccess(t('更新成功'));
} else { } else {
// Add new model // Add new model
// Check if model name already exists // Check if model name already exists
if (models.some(model => model.name === values.name)) { if (models.some((model) => model.name === values.name)) {
showError(t('模型名称已存在')); showError(t('模型名称已存在'));
return; return;
} }
setModels(prev => [{ setModels((prev) => [
name: values.name, {
price: values.price || '', name: values.name,
ratio: values.ratio || '', price: values.price || '',
completionRatio: values.completionRatio || '' ratio: values.ratio || '',
}, ...prev]); completionRatio: values.completionRatio || '',
},
...prev,
]);
setVisible(false); setVisible(false);
showSuccess(t('添加成功')); showSuccess(t('添加成功'));
} }
@@ -312,7 +347,6 @@ export default function ModelSettingsVisualEditor(props) {
}; };
const editModel = (record) => { const editModel = (record) => {
// Determine which pricing mode to use based on the model's current configuration // Determine which pricing mode to use based on the model's current configuration
let initialPricingMode = 'per-token'; let initialPricingMode = 'per-token';
let initialPricingSubMode = 'ratio'; let initialPricingSubMode = 'ratio';
@@ -333,10 +367,14 @@ export default function ModelSettingsVisualEditor(props) {
// If the model has ratio data and we want to populate token price fields // If the model has ratio data and we want to populate token price fields
if (record.ratio) { if (record.ratio) {
modelCopy.tokenPrice = calculateTokenPriceFromRatio(parseFloat(record.ratio)).toString(); modelCopy.tokenPrice = calculateTokenPriceFromRatio(
parseFloat(record.ratio),
).toString();
if (record.completionRatio) { if (record.completionRatio) {
modelCopy.completionTokenPrice = (parseFloat(modelCopy.tokenPrice) * parseFloat(record.completionRatio)).toString(); modelCopy.completionTokenPrice = (
parseFloat(modelCopy.tokenPrice) * parseFloat(record.completionRatio)
).toString();
} }
} }
@@ -370,23 +408,26 @@ export default function ModelSettingsVisualEditor(props) {
return ( return (
<> <>
<Space vertical align="start" style={{ width: '100%' }}> <Space vertical align='start' style={{ width: '100%' }}>
<Space> <Space>
<Button icon={<IconPlus />} onClick={() => { <Button
resetModalState(); icon={<IconPlus />}
setVisible(true); onClick={() => {
}}> resetModalState();
setVisible(true);
}}
>
{t('添加模型')} {t('添加模型')}
</Button> </Button>
<Button type="primary" icon={<IconSave />} onClick={SubmitData}> <Button type='primary' icon={<IconSave />} onClick={SubmitData}>
{t('应用更改')} {t('应用更改')}
</Button> </Button>
<Input <Input
prefix={<IconSearch />} prefix={<IconSearch />}
placeholder={t('搜索模型名称')} placeholder={t('搜索模型名称')}
value={searchText} value={searchText}
onChange={value => { onChange={(value) => {
setSearchText(value) setSearchText(value);
setCurrentPage(1); setCurrentPage(1);
}} }}
style={{ width: 200 }} style={{ width: 200 }}
@@ -399,21 +440,27 @@ export default function ModelSettingsVisualEditor(props) {
currentPage: currentPage, currentPage: currentPage,
pageSize: pageSize, pageSize: pageSize,
total: filteredModels.length, total: filteredModels.length,
onPageChange: page => setCurrentPage(page), onPageChange: (page) => setCurrentPage(page),
formatPageText: (page) => formatPageText: (page) =>
t('第 {{start}} - {{end}} 条,共 {{total}} 条', { t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
start: page.currentStart, start: page.currentStart,
end: page.currentEnd, end: page.currentEnd,
total: filteredModels.length total: filteredModels.length,
}), }),
showTotal: true, showTotal: true,
showSizeChanger: false showSizeChanger: false,
}} }}
/> />
</Space> </Space>
<Modal <Modal
title={currentModel && currentModel.name && models.some(model => model.name === currentModel.name) ? t('编辑模型') : t('添加模型')} title={
currentModel &&
currentModel.name &&
models.some((model) => model.name === currentModel.name)
? t('编辑模型')
: t('添加模型')
}
visible={visible} visible={visible}
onCancel={() => { onCancel={() => {
resetModalState(); resetModalState();
@@ -424,17 +471,28 @@ export default function ModelSettingsVisualEditor(props) {
// If we're in token price mode, make sure ratio values are properly set // If we're in token price mode, make sure ratio values are properly set
const valuesToSave = { ...currentModel }; const valuesToSave = { ...currentModel };
if (pricingMode === 'per-token' && pricingSubMode === 'token-price' && currentModel.tokenPrice) { if (
pricingMode === 'per-token' &&
pricingSubMode === 'token-price' &&
currentModel.tokenPrice
) {
// Calculate and set ratio from token price // Calculate and set ratio from token price
const tokenPrice = parseFloat(currentModel.tokenPrice); const tokenPrice = parseFloat(currentModel.tokenPrice);
valuesToSave.ratio = (tokenPrice / 2).toString(); valuesToSave.ratio = (tokenPrice / 2).toString();
// Calculate and set completion ratio if both token prices are available // Calculate and set completion ratio if both token prices are available
if (currentModel.completionTokenPrice && currentModel.tokenPrice) { if (
const completionPrice = parseFloat(currentModel.completionTokenPrice); currentModel.completionTokenPrice &&
currentModel.tokenPrice
) {
const completionPrice = parseFloat(
currentModel.completionTokenPrice,
);
const modelPrice = parseFloat(currentModel.tokenPrice); const modelPrice = parseFloat(currentModel.tokenPrice);
if (modelPrice > 0) { if (modelPrice > 0) {
valuesToSave.completionRatio = (completionPrice / modelPrice).toString(); valuesToSave.completionRatio = (
completionPrice / modelPrice
).toString();
} }
} }
} }
@@ -452,51 +510,64 @@ export default function ModelSettingsVisualEditor(props) {
} }
}} }}
> >
<Form getFormApi={api => formRef.current = api}> <Form getFormApi={(api) => (formRef.current = api)}>
<Form.Input <Form.Input
field="name" field='name'
label={t('模型名称')} label={t('模型名称')}
placeholder="strawberry" placeholder='strawberry'
required required
disabled={currentModel && currentModel.name && models.some(model => model.name === currentModel.name)} disabled={
onChange={value => setCurrentModel(prev => ({ ...prev, name: value }))} currentModel &&
currentModel.name &&
models.some((model) => model.name === currentModel.name)
}
onChange={(value) =>
setCurrentModel((prev) => ({ ...prev, name: value }))
}
/> />
<Form.Section text={t('定价模式')}> <Form.Section text={t('定价模式')}>
<div style={{ marginBottom: '16px' }}> <div style={{ marginBottom: '16px' }}>
<RadioGroup type="button" value={pricingMode} onChange={(e) => { <RadioGroup
const newMode = e.target.value; type='button'
const oldMode = pricingMode; value={pricingMode}
setPricingMode(newMode); onChange={(e) => {
const newMode = e.target.value;
const oldMode = pricingMode;
setPricingMode(newMode);
// Instead of resetting all values, convert between modes // Instead of resetting all values, convert between modes
if (currentModel) { if (currentModel) {
const updatedModel = { ...currentModel }; const updatedModel = { ...currentModel };
// Update formRef with converted values // Update formRef with converted values
if (formRef.current) { if (formRef.current) {
const formValues = { const formValues = {
name: updatedModel.name name: updatedModel.name,
}; };
if (newMode === 'per-request') { if (newMode === 'per-request') {
formValues.priceInput = updatedModel.price || ''; formValues.priceInput = updatedModel.price || '';
} else if (newMode === 'per-token') { } else if (newMode === 'per-token') {
formValues.ratioInput = updatedModel.ratio || ''; formValues.ratioInput = updatedModel.ratio || '';
formValues.completionRatioInput = updatedModel.completionRatio || ''; formValues.completionRatioInput =
formValues.modelTokenPrice = updatedModel.tokenPrice || ''; updatedModel.completionRatio || '';
formValues.completionTokenPrice = updatedModel.completionTokenPrice || ''; formValues.modelTokenPrice =
updatedModel.tokenPrice || '';
formValues.completionTokenPrice =
updatedModel.completionTokenPrice || '';
}
formRef.current.setValues(formValues);
} }
formRef.current.setValues(formValues); // Update the model state
setCurrentModel(updatedModel);
} }
}}
// Update the model state >
setCurrentModel(updatedModel); <Radio value='per-token'>{t('按量计费')}</Radio>
} <Radio value='per-request'>{t('按次计费')}</Radio>
}}>
<Radio value="per-token">{t('按量计费')}</Radio>
<Radio value="per-request">{t('按次计费')}</Radio>
</RadioGroup> </RadioGroup>
</div> </div>
</Form.Section> </Form.Section>
@@ -505,48 +576,67 @@ export default function ModelSettingsVisualEditor(props) {
<> <>
<Form.Section text={t('价格设置方式')}> <Form.Section text={t('价格设置方式')}>
<div style={{ marginBottom: '16px' }}> <div style={{ marginBottom: '16px' }}>
<RadioGroup type="button" value={pricingSubMode} onChange={(e) => { <RadioGroup
const newSubMode = e.target.value; type='button'
const oldSubMode = pricingSubMode; value={pricingSubMode}
setPricingSubMode(newSubMode); onChange={(e) => {
const newSubMode = e.target.value;
const oldSubMode = pricingSubMode;
setPricingSubMode(newSubMode);
// Handle conversion between submodes // Handle conversion between submodes
if (currentModel) { if (currentModel) {
const updatedModel = { ...currentModel }; const updatedModel = { ...currentModel };
// Convert between ratio and token price // Convert between ratio and token price
if (oldSubMode === 'ratio' && newSubMode === 'token-price') { if (
if (updatedModel.ratio) { oldSubMode === 'ratio' &&
updatedModel.tokenPrice = calculateTokenPriceFromRatio(parseFloat(updatedModel.ratio)).toString(); newSubMode === 'token-price'
) {
if (updatedModel.ratio) {
updatedModel.tokenPrice =
calculateTokenPriceFromRatio(
parseFloat(updatedModel.ratio),
).toString();
if (updatedModel.completionRatio) { if (updatedModel.completionRatio) {
updatedModel.completionTokenPrice = (parseFloat(updatedModel.tokenPrice) * parseFloat(updatedModel.completionRatio)).toString(); updatedModel.completionTokenPrice = (
parseFloat(updatedModel.tokenPrice) *
parseFloat(updatedModel.completionRatio)
).toString();
}
} }
} } else if (
} else if (oldSubMode === 'token-price' && newSubMode === 'ratio') { oldSubMode === 'token-price' &&
// Ratio values should already be calculated by the handlers newSubMode === 'ratio'
} ) {
// Ratio values should already be calculated by the handlers
// Update the form values
if (formRef.current) {
const formValues = {};
if (newSubMode === 'ratio') {
formValues.ratioInput = updatedModel.ratio || '';
formValues.completionRatioInput = updatedModel.completionRatio || '';
} else if (newSubMode === 'token-price') {
formValues.modelTokenPrice = updatedModel.tokenPrice || '';
formValues.completionTokenPrice = updatedModel.completionTokenPrice || '';
} }
formRef.current.setValues(formValues); // Update the form values
} if (formRef.current) {
const formValues = {};
setCurrentModel(updatedModel); if (newSubMode === 'ratio') {
} formValues.ratioInput = updatedModel.ratio || '';
}}> formValues.completionRatioInput =
<Radio value="ratio">{t('按倍率设置')}</Radio> updatedModel.completionRatio || '';
<Radio value="token-price">{t('按价格设置')}</Radio> } else if (newSubMode === 'token-price') {
formValues.modelTokenPrice =
updatedModel.tokenPrice || '';
formValues.completionTokenPrice =
updatedModel.completionTokenPrice || '';
}
formRef.current.setValues(formValues);
}
setCurrentModel(updatedModel);
}
}}
>
<Radio value='ratio'>{t('按倍率设置')}</Radio>
<Radio value='token-price'>{t('按价格设置')}</Radio>
</RadioGroup> </RadioGroup>
</div> </div>
</Form.Section> </Form.Section>
@@ -554,23 +644,27 @@ export default function ModelSettingsVisualEditor(props) {
{pricingSubMode === 'ratio' && ( {pricingSubMode === 'ratio' && (
<> <>
<Form.Input <Form.Input
field="ratioInput" field='ratioInput'
label={t('模型倍率')} label={t('模型倍率')}
placeholder={t('输入模型倍率')} placeholder={t('输入模型倍率')}
onChange={value => setCurrentModel(prev => ({ onChange={(value) =>
...prev || {}, setCurrentModel((prev) => ({
ratio: value ...(prev || {}),
}))} ratio: value,
}))
}
initValue={currentModel?.ratio || ''} initValue={currentModel?.ratio || ''}
/> />
<Form.Input <Form.Input
field="completionRatioInput" field='completionRatioInput'
label={t('补全倍率')} label={t('补全倍率')}
placeholder={t('输入补全倍率')} placeholder={t('输入补全倍率')}
onChange={value => setCurrentModel(prev => ({ onChange={(value) =>
...prev || {}, setCurrentModel((prev) => ({
completionRatio: value ...(prev || {}),
}))} completionRatio: value,
}))
}
initValue={currentModel?.completionRatio || ''} initValue={currentModel?.completionRatio || ''}
/> />
</> </>
@@ -579,7 +673,7 @@ export default function ModelSettingsVisualEditor(props) {
{pricingSubMode === 'token-price' && ( {pricingSubMode === 'token-price' && (
<> <>
<Form.Input <Form.Input
field="modelTokenPrice" field='modelTokenPrice'
label={t('输入价格')} label={t('输入价格')}
onChange={(value) => { onChange={(value) => {
handleTokenPriceChange(value); handleTokenPriceChange(value);
@@ -588,7 +682,7 @@ export default function ModelSettingsVisualEditor(props) {
suffix={t('$/1M tokens')} suffix={t('$/1M tokens')}
/> />
<Form.Input <Form.Input
field="completionTokenPrice" field='completionTokenPrice'
label={t('输出价格')} label={t('输出价格')}
onChange={(value) => { onChange={(value) => {
handleCompletionTokenPriceChange(value); handleCompletionTokenPriceChange(value);
@@ -603,13 +697,15 @@ export default function ModelSettingsVisualEditor(props) {
{pricingMode === 'per-request' && ( {pricingMode === 'per-request' && (
<Form.Input <Form.Input
field="priceInput" field='priceInput'
label={t('固定价格(每次)')} label={t('固定价格(每次)')}
placeholder={t('输入每次价格')} placeholder={t('输入每次价格')}
onChange={value => setCurrentModel(prev => ({ onChange={(value) =>
...prev || {}, setCurrentModel((prev) => ({
price: value ...(prev || {}),
}))} price: value,
}))
}
initValue={currentModel?.price || ''} initValue={currentModel?.price || ''}
/> />
)} )}

View File

@@ -1,5 +1,14 @@
import React, { useEffect, useState, useRef } from 'react'; import React, { useEffect, useState, useRef } from 'react';
import { Banner, Button, Col, Form, Popconfirm, Row, Space, Spin } from '@douyinfe/semi-ui'; import {
Banner,
Button,
Col,
Form,
Popconfirm,
Row,
Space,
Spin,
} from '@douyinfe/semi-ui';
import { import {
compareObjects, compareObjects,
API, API,
@@ -7,7 +16,7 @@ import {
showSuccess, showSuccess,
showWarning, showWarning,
verifyJSON, verifyJSON,
verifyJSONPromise verifyJSONPromise,
} from '../../../helpers'; } from '../../../helpers';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@@ -15,7 +24,7 @@ export default function SettingsChats(props) {
const { t } = useTranslation(); const { t } = useTranslation();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [inputs, setInputs] = useState({ const [inputs, setInputs] = useState({
Chats: "[]", Chats: '[]',
}); });
const refForm = useRef(); const refForm = useRef();
const [inputsRow, setInputsRow] = useState(inputs); const [inputsRow, setInputsRow] = useState(inputs);
@@ -23,44 +32,48 @@ export default function SettingsChats(props) {
async function onSubmit() { async function onSubmit() {
try { try {
console.log('Starting validation...'); console.log('Starting validation...');
await refForm.current.validate().then(() => { await refForm.current
console.log('Validation passed'); .validate()
const updateArray = compareObjects(inputs, inputsRow); .then(() => {
if (!updateArray.length) return showWarning(t('你似乎并没有修改什么')); console.log('Validation passed');
const requestQueue = updateArray.map((item) => { const updateArray = compareObjects(inputs, inputsRow);
let value = ''; if (!updateArray.length)
if (typeof inputs[item.key] === 'boolean') { return showWarning(t('你似乎并没有修改什么'));
value = String(inputs[item.key]); const requestQueue = updateArray.map((item) => {
} else { let value = '';
value = inputs[item.key]; if (typeof inputs[item.key] === 'boolean') {
} value = String(inputs[item.key]);
return API.put('/api/option/', { } else {
key: item.key, value = inputs[item.key];
value
});
});
setLoading(true);
Promise.all(requestQueue)
.then((res) => {
if (requestQueue.length === 1) {
if (res.includes(undefined)) return;
} else if (requestQueue.length > 1) {
if (res.includes(undefined))
return showError(t('部分保存失败,请重试'));
} }
showSuccess(t('保存成功')); return API.put('/api/option/', {
props.refresh(); key: item.key,
}) value,
.catch(() => { });
showError(t('保存失败,请重试'));
})
.finally(() => {
setLoading(false);
}); });
}).catch((error) => { setLoading(true);
console.error('Validation failed:', error); Promise.all(requestQueue)
showError(t('请检查输入')); .then((res) => {
}); if (requestQueue.length === 1) {
if (res.includes(undefined)) return;
} else if (requestQueue.length > 1) {
if (res.includes(undefined))
return showError(t('部分保存失败,请重试'));
}
showSuccess(t('保存成功'));
props.refresh();
})
.catch(() => {
showError(t('保存失败,请重试'));
})
.finally(() => {
setLoading(false);
});
})
.catch((error) => {
console.error('Validation failed:', error);
showError(t('请检查输入'));
});
} catch (error) { } catch (error) {
showError(t('请检查输入')); showError(t('请检查输入'));
console.error(error); console.error(error);
@@ -109,11 +122,15 @@ export default function SettingsChats(props) {
<Form.Section text={t('令牌聊天设置')}> <Form.Section text={t('令牌聊天设置')}>
<Banner <Banner
type='warning' type='warning'
description={t('必须将上方聊天链接全部设置为空,才能使用下方聊天设置功能')} description={t(
'必须将上方聊天链接全部设置为空,才能使用下方聊天设置功能',
)}
/> />
<Banner <Banner
type='info' type='info'
description={t('链接中的{key}将自动替换为sk-xxxx{address}将自动替换为系统设置的服务器地址,末尾不带/和/v1')} description={t(
'链接中的{key}将自动替换为sk-xxxx{address}将自动替换为系统设置的服务器地址,末尾不带/和/v1',
)}
/> />
<Form.TextArea <Form.TextArea
label={t('聊天配置')} label={t('聊天配置')}
@@ -128,22 +145,20 @@ export default function SettingsChats(props) {
validator: (rule, value) => { validator: (rule, value) => {
return verifyJSON(value); return verifyJSON(value);
}, },
message: t('不是合法的 JSON 字符串') message: t('不是合法的 JSON 字符串'),
} },
]} ]}
onChange={(value) => onChange={(value) =>
setInputs({ setInputs({
...inputs, ...inputs,
Chats: value Chats: value,
}) })
} }
/> />
</Form.Section> </Form.Section>
</Form> </Form>
<Space> <Space>
<Button onClick={onSubmit}> <Button onClick={onSubmit}>{t('保存聊天设置')}</Button>
{t('保存聊天设置')}
</Button>
</Space> </Space>
</Spin> </Spin>
); );

View File

@@ -42,7 +42,8 @@ export default function SettingsCreditLimit(props) {
if (requestQueue.length === 1) { if (requestQueue.length === 1) {
if (res.includes(undefined)) return; if (res.includes(undefined)) return;
} else if (requestQueue.length > 1) { } else if (requestQueue.length > 1) {
if (res.includes(undefined)) return showError(t('部分保存失败,请重试')); if (res.includes(undefined))
return showError(t('部分保存失败,请重试'));
} }
showSuccess(t('保存成功')); showSuccess(t('保存成功'));
props.refresh(); props.refresh();

View File

@@ -47,7 +47,8 @@ export default function DataDashboard(props) {
if (requestQueue.length === 1) { if (requestQueue.length === 1) {
if (res.includes(undefined)) return; if (res.includes(undefined)) return;
} else if (requestQueue.length > 1) { } else if (requestQueue.length > 1) {
if (res.includes(undefined)) return showError(t('部分保存失败,请重试')); if (res.includes(undefined))
return showError(t('部分保存失败,请重试'));
} }
showSuccess(t('保存成功')); showSuccess(t('保存成功'));
props.refresh(); props.refresh();

View File

@@ -44,7 +44,8 @@ export default function SettingsDrawing(props) {
if (requestQueue.length === 1) { if (requestQueue.length === 1) {
if (res.includes(undefined)) return; if (res.includes(undefined)) return;
} else if (requestQueue.length > 1) { } else if (requestQueue.length > 1) {
if (res.includes(undefined)) return showError(t('部分保存失败,请重试')); if (res.includes(undefined))
return showError(t('部分保存失败,请重试'));
} }
showSuccess(t('保存成功')); showSuccess(t('保存成功'));
props.refresh(); props.refresh();
@@ -146,7 +147,8 @@ export default function SettingsDrawing(props) {
label={ label={
<> <>
{t('开启之后会清除用户提示词中的')} <Tag>--fast</Tag> {t('开启之后会清除用户提示词中的')} <Tag>--fast</Tag>
<Tag>--relax</Tag> {t('')} <Tag>--turbo</Tag> {t('')} <Tag>--relax</Tag> {t('')} <Tag>--turbo</Tag>{' '}
{t('参数')}
</> </>
} }
size='default' size='default'

View File

@@ -1,5 +1,14 @@
import React, { useEffect, useState, useRef } from 'react'; import React, { useEffect, useState, useRef } from 'react';
import { Banner, Button, Col, Form, Row, Spin, Collapse, Modal } from '@douyinfe/semi-ui'; import {
Banner,
Button,
Col,
Form,
Row,
Spin,
Collapse,
Modal,
} from '@douyinfe/semi-ui';
import { import {
compareObjects, compareObjects,
API, API,
@@ -54,7 +63,8 @@ export default function GeneralSettings(props) {
if (requestQueue.length === 1) { if (requestQueue.length === 1) {
if (res.includes(undefined)) return; if (res.includes(undefined)) return;
} else if (requestQueue.length > 1) { } else if (requestQueue.length > 1) {
if (res.includes(undefined)) return showError(t('部分保存失败,请重试')); if (res.includes(undefined))
return showError(t('部分保存失败,请重试'));
} }
showSuccess(t('保存成功')); showSuccess(t('保存成功'));
props.refresh(); props.refresh();
@@ -209,7 +219,9 @@ export default function GeneralSettings(props) {
> >
<Banner <Banner
type='warning' type='warning'
description={t('此设置用于系统内部计算默认值500000是为了精确到6位小数点设计不推荐修改。')} description={t(
'此设置用于系统内部计算默认值500000是为了精确到6位小数点设计不推荐修改。',
)}
bordered bordered
fullMode={false} fullMode={false}
closeIcon={null} closeIcon={null}

View File

@@ -45,7 +45,8 @@ export default function SettingsLog(props) {
if (requestQueue.length === 1) { if (requestQueue.length === 1) {
if (res.includes(undefined)) return; if (res.includes(undefined)) return;
} else if (requestQueue.length > 1) { } else if (requestQueue.length > 1) {
if (res.includes(undefined)) return showError(t('部分保存失败,请重试')); if (res.includes(undefined))
return showError(t('部分保存失败,请重试'));
} }
showSuccess(t('保存成功')); showSuccess(t('保存成功'));
props.refresh(); props.refresh();

View File

@@ -5,7 +5,8 @@ import {
API, API,
showError, showError,
showSuccess, showSuccess,
showWarning, verifyJSON showWarning,
verifyJSON,
} from '../../../helpers'; } from '../../../helpers';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@@ -43,7 +44,8 @@ export default function SettingsMonitoring(props) {
if (requestQueue.length === 1) { if (requestQueue.length === 1) {
if (res.includes(undefined)) return; if (res.includes(undefined)) return;
} else if (requestQueue.length > 1) { } else if (requestQueue.length > 1) {
if (res.includes(undefined)) return showError(t('部分保存失败,请重试')); if (res.includes(undefined))
return showError(t('部分保存失败,请重试'));
} }
showSuccess(t('保存成功')); showSuccess(t('保存成功'));
props.refresh(); props.refresh();
@@ -84,7 +86,9 @@ export default function SettingsMonitoring(props) {
step={1} step={1}
min={0} min={0}
suffix={t('秒')} suffix={t('秒')}
extraText={t('当运行通道全部测试时,超过此时间将自动禁用通道')} extraText={t(
'当运行通道全部测试时,超过此时间将自动禁用通道',
)}
placeholder={''} placeholder={''}
field={'ChannelDisableThreshold'} field={'ChannelDisableThreshold'}
onChange={(value) => onChange={(value) =>
@@ -150,10 +154,14 @@ export default function SettingsMonitoring(props) {
<Form.TextArea <Form.TextArea
label={t('自动禁用关键词')} label={t('自动禁用关键词')}
placeholder={t('一行一个,不区分大小写')} placeholder={t('一行一个,不区分大小写')}
extraText={t('当上游通道返回错误中包含这些关键词时(不区分大小写),自动禁用通道')} extraText={t(
'当上游通道返回错误中包含这些关键词时(不区分大小写),自动禁用通道',
)}
field={'AutomaticDisableKeywords'} field={'AutomaticDisableKeywords'}
autosize={{ minRows: 6, maxRows: 12 }} autosize={{ minRows: 6, maxRows: 12 }}
onChange={(value) => setInputs({ ...inputs, AutomaticDisableKeywords: value })} onChange={(value) =>
setInputs({ ...inputs, AutomaticDisableKeywords: value })
}
/> />
</Col> </Col>
</Row> </Row>

View File

@@ -41,7 +41,8 @@ export default function SettingsSensitiveWords(props) {
if (requestQueue.length === 1) { if (requestQueue.length === 1) {
if (res.includes(undefined)) return; if (res.includes(undefined)) return;
} else if (requestQueue.length > 1) { } else if (requestQueue.length > 1) {
if (res.includes(undefined)) return showError(t('部分保存失败,请重试')); if (res.includes(undefined))
return showError(t('部分保存失败,请重试'));
} }
showSuccess(t('保存成功')); showSuccess(t('保存成功'));
props.refresh(); props.refresh();

View File

@@ -17,7 +17,7 @@ export default function RequestRateLimit(props) {
ModelRequestRateLimitEnabled: false, ModelRequestRateLimitEnabled: false,
ModelRequestRateLimitCount: -1, ModelRequestRateLimitCount: -1,
ModelRequestRateLimitSuccessCount: 1000, ModelRequestRateLimitSuccessCount: 1000,
ModelRequestRateLimitDurationMinutes: 1 ModelRequestRateLimitDurationMinutes: 1,
}); });
const refForm = useRef(); const refForm = useRef();
const [inputsRow, setInputsRow] = useState(inputs); const [inputsRow, setInputsRow] = useState(inputs);
@@ -43,7 +43,8 @@ export default function RequestRateLimit(props) {
if (requestQueue.length === 1) { if (requestQueue.length === 1) {
if (res.includes(undefined)) return; if (res.includes(undefined)) return;
} else if (requestQueue.length > 1) { } else if (requestQueue.length > 1) {
if (res.includes(undefined)) return showError(t('部分保存失败,请重试')); if (res.includes(undefined))
return showError(t('部分保存失败,请重试'));
} }
showSuccess(t('保存成功')); showSuccess(t('保存成功'));
props.refresh(); props.refresh();

View File

@@ -1,11 +1,27 @@
import React, { useContext, useEffect, useState, useRef } from 'react'; import React, { useContext, useEffect, useState, useRef } from 'react';
import { Card, Col, Row, Form, Button, Typography, Space, RadioGroup, Radio, Modal, Banner } from '@douyinfe/semi-ui'; import {
Card,
Col,
Row,
Form,
Button,
Typography,
Space,
RadioGroup,
Radio,
Modal,
Banner,
} from '@douyinfe/semi-ui';
import { API, showError, showNotice, timestamp2string } from '../../helpers'; import { API, showError, showNotice, timestamp2string } from '../../helpers';
import { StatusContext } from '../../context/Status'; import { StatusContext } from '../../context/Status';
import { marked } from 'marked'; import { marked } from 'marked';
import { StyleContext } from '../../context/Style/index.js'; import { StyleContext } from '../../context/Style/index.js';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { IconHelpCircle, IconInfoCircle, IconAlertTriangle } from '@douyinfe/semi-icons'; import {
IconHelpCircle,
IconInfoCircle,
IconAlertTriangle,
} from '@douyinfe/semi-icons';
const Setup = () => { const Setup = () => {
const { t, i18n } = useTranslation(); const { t, i18n } = useTranslation();
@@ -16,7 +32,7 @@ const Setup = () => {
const [setupStatus, setSetupStatus] = useState({ const [setupStatus, setSetupStatus] = useState({
status: false, status: false,
root_init: false, root_init: false,
database_type: '' database_type: '',
}); });
const { Text, Title } = Typography; const { Text, Title } = Typography;
const formRef = useRef(null); const formRef = useRef(null);
@@ -25,7 +41,7 @@ const Setup = () => {
username: '', username: '',
password: '', password: '',
confirmPassword: '', confirmPassword: '',
usageMode: 'external' usageMode: 'external',
}); });
useEffect(() => { useEffect(() => {
@@ -53,18 +69,18 @@ const Setup = () => {
}; };
const handleUsageModeChange = (val) => { const handleUsageModeChange = (val) => {
setFormData({...formData, usageMode: val}); setFormData({ ...formData, usageMode: val });
}; };
const onSubmit = () => { const onSubmit = () => {
if (!formRef.current) { if (!formRef.current) {
console.error("Form reference is null"); console.error('Form reference is null');
showError(t('表单引用错误,请刷新页面重试')); showError(t('表单引用错误,请刷新页面重试'));
return; return;
} }
const values = formRef.current.getValues(); const values = formRef.current.getValues();
console.log("Form values:", values); console.log('Form values:', values);
// For root_init=false, validate admin username and password // For root_init=false, validate admin username and password
if (!setupStatus.root_init) { if (!setupStatus.root_init) {
@@ -85,21 +101,21 @@ const Setup = () => {
} }
// Prepare submission data // Prepare submission data
const formValues = {...values}; const formValues = { ...values };
formValues.SelfUseModeEnabled = values.usageMode === 'self'; formValues.SelfUseModeEnabled = values.usageMode === 'self';
formValues.DemoSiteEnabled = values.usageMode === 'demo'; formValues.DemoSiteEnabled = values.usageMode === 'demo';
// Remove usageMode as it's not needed by the backend // Remove usageMode as it's not needed by the backend
delete formValues.usageMode; delete formValues.usageMode;
console.log("Submitting data to backend:", formValues); console.log('Submitting data to backend:', formValues);
setLoading(true); setLoading(true);
// Submit to backend // Submit to backend
API.post('/api/setup', formValues) API.post('/api/setup', formValues)
.then(res => { .then((res) => {
const { success, message } = res.data; const { success, message } = res.data;
console.log("API response:", res.data); console.log('API response:', res.data);
if (success) { if (success) {
showNotice(t('系统初始化成功,正在跳转...')); showNotice(t('系统初始化成功,正在跳转...'));
@@ -110,7 +126,7 @@ const Setup = () => {
showError(message || t('初始化失败,请重试')); showError(message || t('初始化失败,请重试'));
} }
}) })
.catch(error => { .catch((error) => {
console.error('API error:', error); console.error('API error:', error);
showError(t('系统初始化失败,请重试')); showError(t('系统初始化失败,请重试'));
setLoading(false); setLoading(false);
@@ -124,18 +140,28 @@ const Setup = () => {
<> <>
<div style={{ maxWidth: '800px', margin: '0 auto', padding: '20px' }}> <div style={{ maxWidth: '800px', margin: '0 auto', padding: '20px' }}>
<Card> <Card>
<Title heading={2} style={{ marginBottom: '24px' }}>{t('系统初始化')}</Title> <Title heading={2} style={{ marginBottom: '24px' }}>
{t('系统初始化')}
</Title>
{setupStatus.database_type === 'sqlite' && ( {setupStatus.database_type === 'sqlite' && (
<Banner <Banner
type="warning" type='warning'
icon={<IconAlertTriangle size="large" />} icon={<IconAlertTriangle size='large' />}
closeIcon={null} closeIcon={null}
title={t('数据库警告')} title={t('数据库警告')}
description={ description={
<div> <div>
<p>{t('您正在使用 SQLite 数据库。如果您在容器环境中运行,请确保已正确设置数据库文件的持久化映射,否则容器重启后所有数据将丢失!')}</p> <p>
<p>{t('建议在生产环境中使用 MySQL 或 PostgreSQL 数据库,或确保 SQLite 数据库文件已映射到宿主机的持久化存储。')}</p> {t(
'您正在使用 SQLite 数据库。如果您在容器环境中运行,请确保已正确设置数据库文件的持久化映射,否则容器重启后所有数据将丢失!',
)}
</p>
<p>
{t(
'建议在生产环境中使用 MySQL 或 PostgreSQL 数据库,或确保 SQLite 数据库文件已映射到宿主机的持久化存储。',
)}
</p>
</div> </div>
} }
style={{ marginBottom: '24px' }} style={{ marginBottom: '24px' }}
@@ -143,12 +169,15 @@ const Setup = () => {
)} )}
<Form <Form
getFormApi={(formApi) => { formRef.current = formApi; console.log("Form API set:", formApi); }} getFormApi={(formApi) => {
formRef.current = formApi;
console.log('Form API set:', formApi);
}}
initValues={formData} initValues={formData}
> >
{setupStatus.root_init ? ( {setupStatus.root_init ? (
<Banner <Banner
type="info" type='info'
icon={<IconInfoCircle />} icon={<IconInfoCircle />}
closeIcon={null} closeIcon={null}
description={t('管理员账号已经初始化过,请继续设置系统参数')} description={t('管理员账号已经初始化过,请继续设置系统参数')}
@@ -157,43 +186,56 @@ const Setup = () => {
) : ( ) : (
<Form.Section text={t('管理员账号')}> <Form.Section text={t('管理员账号')}>
<Form.Input <Form.Input
field="username" field='username'
label={t('用户名')} label={t('用户名')}
placeholder={t('请输入管理员用户名')} placeholder={t('请输入管理员用户名')}
showClear showClear
onChange={(value) => setFormData({...formData, username: value})} onChange={(value) =>
setFormData({ ...formData, username: value })
}
/> />
<Form.Input <Form.Input
field="password" field='password'
label={t('密码')} label={t('密码')}
placeholder={t('请输入管理员密码')} placeholder={t('请输入管理员密码')}
type="password" type='password'
showClear showClear
onChange={(value) => setFormData({...formData, password: value})} onChange={(value) =>
setFormData({ ...formData, password: value })
}
/> />
<Form.Input <Form.Input
field="confirmPassword" field='confirmPassword'
label={t('确认密码')} label={t('确认密码')}
placeholder={t('请确认管理员密码')} placeholder={t('请确认管理员密码')}
type="password" type='password'
showClear showClear
onChange={(value) => setFormData({...formData, confirmPassword: value})} onChange={(value) =>
setFormData({ ...formData, confirmPassword: value })
}
/> />
</Form.Section> </Form.Section>
)} )}
<Form.Section text={ <Form.Section
<div style={{ display: 'flex', alignItems: 'center' }}> text={
{t('系统设置')} <div style={{ display: 'flex', alignItems: 'center' }}>
</div> {t('系统设置')}
}> </div>
}
>
<Form.RadioGroup <Form.RadioGroup
field="usageMode" field='usageMode'
label={ label={
<div style={{ display: 'flex', alignItems: 'center' }}> <div style={{ display: 'flex', alignItems: 'center' }}>
{t('使用模式')} {t('使用模式')}
<IconHelpCircle <IconHelpCircle
style={{ marginLeft: '4px', color: 'var(--semi-color-primary)', verticalAlign: 'middle', cursor: 'pointer' }} style={{
marginLeft: '4px',
color: 'var(--semi-color-primary)',
verticalAlign: 'middle',
cursor: 'pointer',
}}
onClick={(e) => { onClick={(e) => {
// e.preventDefault(); // e.preventDefault();
// e.stopPropagation(); // e.stopPropagation();
@@ -203,18 +245,18 @@ const Setup = () => {
</div> </div>
} }
extraText={t('可在初始化后修改')} extraText={t('可在初始化后修改')}
initValue="external" initValue='external'
onChange={handleUsageModeChange} onChange={handleUsageModeChange}
> >
<Form.Radio value="external">{t('对外运营模式')}</Form.Radio> <Form.Radio value='external'>{t('对外运营模式')}</Form.Radio>
<Form.Radio value="self">{t('自用模式')}</Form.Radio> <Form.Radio value='self'>{t('自用模式')}</Form.Radio>
<Form.Radio value="demo">{t('演示站点模式')}</Form.Radio> <Form.Radio value='demo'>{t('演示站点模式')}</Form.Radio>
</Form.RadioGroup> </Form.RadioGroup>
</Form.Section> </Form.Section>
</Form> </Form>
<div style={{ marginTop: '24px', textAlign: 'right' }}> <div style={{ marginTop: '24px', textAlign: 'right' }}>
<Button type="primary" onClick={onSubmit} loading={loading}> <Button type='primary' onClick={onSubmit} loading={loading}>
{t('初始化系统')} {t('初始化系统')}
</Button> </Button>
</div> </div>
@@ -233,12 +275,18 @@ const Setup = () => {
<div style={{ padding: '8px 0' }}> <div style={{ padding: '8px 0' }}>
<Title heading={6}>{t('对外运营模式')}</Title> <Title heading={6}>{t('对外运营模式')}</Title>
<p>{t('默认模式,适用于为多个用户提供服务的场景。')}</p> <p>{t('默认模式,适用于为多个用户提供服务的场景。')}</p>
<p>{t('此模式下,系统将计算每次调用的用量,您需要对每个模型都设置价格,如果没有设置价格,用户将无法使用该模型。')}</p> <p>
{t(
'此模式下,系统将计算每次调用的用量,您需要对每个模型都设置价格,如果没有设置价格,用户将无法使用该模型。',
)}
</p>
</div> </div>
<div style={{ padding: '8px 0' }}> <div style={{ padding: '8px 0' }}>
<Title heading={6}>{t('自用模式')}</Title> <Title heading={6}>{t('自用模式')}</Title>
<p>{t('适用于个人使用的场景。')}</p> <p>{t('适用于个人使用的场景。')}</p>
<p>{t('不需要设置模型价格,系统将弱化用量计算,您可专注于使用模型。')}</p> <p>
{t('不需要设置模型价格,系统将弱化用量计算,您可专注于使用模型。')}
</p>
</div> </div>
<div style={{ padding: '8px 0' }}> <div style={{ padding: '8px 0' }}>
<Title heading={6}>{t('演示站点模式')}</Title> <Title heading={6}>{t('演示站点模式')}</Title>

View File

@@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import TaskLogsTable from "../../components/TaskLogsTable.js"; import TaskLogsTable from '../../components/TaskLogsTable.js';
const Task = () => ( const Task = () => (
<> <>

View File

@@ -18,8 +18,9 @@ import {
Select, Select,
SideSheet, SideSheet,
Space, Space,
Spin, TextArea, Spin,
Typography TextArea,
Typography,
} from '@douyinfe/semi-ui'; } from '@douyinfe/semi-ui';
import Title from '@douyinfe/semi-ui/lib/es/typography/title'; import Title from '@douyinfe/semi-ui/lib/es/typography/title';
import { Divider } from 'semantic-ui-react'; import { Divider } from 'semantic-ui-react';
@@ -47,7 +48,7 @@ const EditToken = (props) => {
model_limits_enabled, model_limits_enabled,
model_limits, model_limits,
allow_ips, allow_ips,
group group,
} = inputs; } = inputs;
// const [visible, setVisible] = useState(false); // const [visible, setVisible] = useState(false);
const [models, setModels] = useState([]); const [models, setModels] = useState([]);
@@ -100,7 +101,7 @@ const EditToken = (props) => {
let localGroupOptions = Object.entries(data).map(([group, info]) => ({ let localGroupOptions = Object.entries(data).map(([group, info]) => ({
label: info.desc, label: info.desc,
value: group, value: group,
ratio: info.ratio ratio: info.ratio,
})); }));
setGroups(localGroupOptions); setGroups(localGroupOptions);
} else { } else {
@@ -229,9 +230,7 @@ const EditToken = (props) => {
} }
if (successCount > 0) { if (successCount > 0) {
showSuccess( showSuccess(t('令牌创建成功,请在列表页面点击复制获取令牌!'));
t('令牌创建成功,请在列表页面点击复制获取令牌!')
);
props.refresh(); props.refresh();
props.handleClose(); props.handleClose();
} }
@@ -246,7 +245,9 @@ const EditToken = (props) => {
<SideSheet <SideSheet
placement={isEdit ? 'right' : 'left'} placement={isEdit ? 'right' : 'left'}
title={ title={
<Title level={3}>{isEdit ? t('更新令牌信息') : t('创建新的令牌')}</Title> <Title level={3}>
{isEdit ? t('更新令牌信息') : t('创建新的令牌')}
</Title>
} }
headerStyle={{ borderBottom: '1px solid var(--semi-color-border)' }} headerStyle={{ borderBottom: '1px solid var(--semi-color-border)' }}
bodyStyle={{ borderBottom: '1px solid var(--semi-color-border)' }} bodyStyle={{ borderBottom: '1px solid var(--semi-color-border)' }}
@@ -333,7 +334,9 @@ const EditToken = (props) => {
<Divider /> <Divider />
<Banner <Banner
type={'warning'} type={'warning'}
description={t('注意,令牌的额度仅用于限制令牌本身的最大额度使用量,实际的使用受到账户的剩余额度限制。')} description={t(
'注意,令牌的额度仅用于限制令牌本身的最大额度使用量,实际的使用受到账户的剩余额度限制。',
)}
></Banner> ></Banner>
<div style={{ marginTop: 20 }}> <div style={{ marginTop: 20 }}>
<Typography.Text>{`${t('额度')}${renderQuotaWithPrompt(remain_quota)}`}</Typography.Text> <Typography.Text>{`${t('额度')}${renderQuotaWithPrompt(remain_quota)}`}</Typography.Text>
@@ -396,7 +399,9 @@ const EditToken = (props) => {
</div> </div>
<Divider /> <Divider />
<div style={{ marginTop: 10 }}> <div style={{ marginTop: 10 }}>
<Typography.Text>{t('IP白名单请勿过度信任此功能')}</Typography.Text> <Typography.Text>
{t('IP白名单请勿过度信任此功能')}
</Typography.Text>
</div> </div>
<TextArea <TextArea
label={t('IP白名单')} label={t('IP白名单')}
@@ -440,7 +445,7 @@ const EditToken = (props) => {
<div style={{ marginTop: 10 }}> <div style={{ marginTop: 10 }}>
<Typography.Text>{t('令牌分组,默认为用户的分组')}</Typography.Text> <Typography.Text>{t('令牌分组,默认为用户的分组')}</Typography.Text>
</div> </div>
{groups.length > 0 ? {groups.length > 0 ? (
<Select <Select
style={{ marginTop: 8 }} style={{ marginTop: 8 }}
placeholder={t('令牌分组,默认为用户的分组')} placeholder={t('令牌分组,默认为用户的分组')}
@@ -455,14 +460,15 @@ const EditToken = (props) => {
value={inputs.group} value={inputs.group}
autoComplete='new-password' autoComplete='new-password'
optionList={groups} optionList={groups}
/>: />
) : (
<Select <Select
style={{ marginTop: 8 }} style={{ marginTop: 8 }}
placeholder={t('管理员未设置用户可选分组')} placeholder={t('管理员未设置用户可选分组')}
name='gruop' name='gruop'
disabled={true} disabled={true}
/> />
} )}
</Spin> </Spin>
</SideSheet> </SideSheet>
</> </>

View File

@@ -8,13 +8,15 @@ const Token = () => {
<> <>
<Layout> <Layout>
<Layout.Header> <Layout.Header>
<Banner <Banner
type='warning' type='warning'
description={t('令牌无法精确控制使用额度,只允许自用,请勿直接将令牌分发给他人。')} description={t(
/> '令牌无法精确控制使用额度,只允许自用,请勿直接将令牌分发给他人。',
</Layout.Header> )}
<Layout.Content> />
<TokensTable /> </Layout.Header>
<Layout.Content>
<TokensTable />
</Layout.Content> </Layout.Content>
</Layout> </Layout>
</> </>

View File

@@ -228,8 +228,12 @@ const TopUp = () => {
size={'small'} size={'small'}
centered={true} centered={true}
> >
<p>{t('充值数量')}{topUpCount}</p> <p>
<p>{t('实付金额')}{renderAmount()}</p> {t('充值数量')}{topUpCount}
</p>
<p>
{t('实付金额')}{renderAmount()}
</p>
<p>{t('是否确认充值?')}</p> <p>{t('是否确认充值?')}</p>
</Modal> </Modal>
<div <div
@@ -280,7 +284,9 @@ const TopUp = () => {
disabled={!enableOnlineTopUp} disabled={!enableOnlineTopUp}
field={'redemptionCount'} field={'redemptionCount'}
label={t('实付金额:') + ' ' + renderAmount()} label={t('实付金额:') + ' ' + renderAmount()}
placeholder={t('充值数量,最低 ') + renderQuotaWithAmount(minTopUp)} placeholder={
t('充值数量,最低 ') + renderQuotaWithAmount(minTopUp)
}
name='redemptionCount' name='redemptionCount'
type={'number'} type={'number'}
value={topUpCount} value={topUpCount}

View File

@@ -201,7 +201,9 @@ const EditUser = (props) => {
search search
selection selection
allowAdditions allowAdditions
additionLabel={t('请在系统设置页面编辑分组倍率以添加新的分组:')} additionLabel={t(
'请在系统设置页面编辑分组倍率以添加新的分组:',
)}
onChange={(value) => handleInputChange('group', value)} onChange={(value) => handleInputChange('group', value)}
value={inputs.group} value={inputs.group}
autoComplete='new-password' autoComplete='new-password'
@@ -231,17 +233,21 @@ const EditUser = (props) => {
name='github_id' name='github_id'
value={github_id} value={github_id}
autoComplete='new-password' autoComplete='new-password'
placeholder={t('此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改')} placeholder={t(
'此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改',
)}
readonly readonly
/> />
<div style={{ marginTop: 20 }}> <div style={{ marginTop: 20 }}>
<Typography.Text>{t('`已绑定的 OIDC 账户')}</Typography.Text> <Typography.Text>{t('`已绑定的 OIDC 账户')}</Typography.Text>
</div> </div>
<Input <Input
name='oidc_id' name='oidc_id'
value={oidc_id} value={oidc_id}
placeholder={t('此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改')} placeholder={t(
readonly '此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改',
)}
readonly
/> />
<div style={{ marginTop: 20 }}> <div style={{ marginTop: 20 }}>
<Typography.Text>{t('已绑定的微信账户')}</Typography.Text> <Typography.Text>{t('已绑定的微信账户')}</Typography.Text>
@@ -250,7 +256,9 @@ const EditUser = (props) => {
name='wechat_id' name='wechat_id'
value={wechat_id} value={wechat_id}
autoComplete='new-password' autoComplete='new-password'
placeholder={t('此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改')} placeholder={t(
'此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改',
)}
readonly readonly
/> />
<div style={{ marginTop: 20 }}> <div style={{ marginTop: 20 }}>
@@ -260,7 +268,9 @@ const EditUser = (props) => {
name='email' name='email'
value={email} value={email}
autoComplete='new-password' autoComplete='new-password'
placeholder={t('此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改')} placeholder={t(
'此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改',
)}
readonly readonly
/> />
<div style={{ marginTop: 20 }}> <div style={{ marginTop: 20 }}>
@@ -270,7 +280,9 @@ const EditUser = (props) => {
name='telegram_id' name='telegram_id'
value={telegram_id} value={telegram_id}
autoComplete='new-password' autoComplete='new-password'
placeholder={t('此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改')} placeholder={t(
'此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改',
)}
readonly readonly
/> />
</Spin> </Spin>

View File

@@ -9,10 +9,10 @@ const User = () => {
<> <>
<Layout> <Layout>
<Layout.Header> <Layout.Header>
<h3>{t('管理用户')}</h3> <h3>{t('管理用户')}</h3>
</Layout.Header> </Layout.Header>
<Layout.Content> <Layout.Content>
<UsersTable /> <UsersTable />
</Layout.Content> </Layout.Content>
</Layout> </Layout>
</> </>

View File

@@ -46,7 +46,11 @@ export default defineConfig({
'react-toastify', 'react-toastify',
'react-turnstile', 'react-turnstile',
], ],
'i18n': ['i18next', 'react-i18next', 'i18next-browser-languagedetector'], i18n: [
'i18next',
'react-i18next',
'i18next-browser-languagedetector',
],
}, },
}, },
}, },