fix: run prettier

This commit is contained in:
ckt1031 2023-07-15 21:14:40 +08:00
parent 0424baef6a
commit caabdd1e21
49 changed files with 1087 additions and 582 deletions

12
web/package-lock.json generated
View File

@ -22,7 +22,7 @@
}, },
"devDependencies": { "devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"prettier": "^2.7.1", "prettier": "3.0.0",
"react-scripts": "^5.0.1" "react-scripts": "^5.0.1"
} }
}, },
@ -14575,15 +14575,15 @@
} }
}, },
"node_modules/prettier": { "node_modules/prettier": {
"version": "2.8.8", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.0.tgz",
"integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", "integrity": "sha512-zBf5eHpwHOGPC47h0zrPyNn+eAEIdEzfywMoYn2XPi0P44Zp0tSq64rq0xAREh4auw2cJZHo9QUob+NqCQky4g==",
"dev": true, "dev": true,
"bin": { "bin": {
"prettier": "bin-prettier.js" "prettier": "bin/prettier.cjs"
}, },
"engines": { "engines": {
"node": ">=10.13.0" "node": ">=14"
}, },
"funding": { "funding": {
"url": "https://github.com/prettier/prettier?sponsor=1" "url": "https://github.com/prettier/prettier?sponsor=1"

View File

@ -41,7 +41,7 @@
}, },
"devDependencies": { "devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"prettier": "^2.7.1", "prettier": "3.0.0",
"react-scripts": "^5.0.1" "react-scripts": "^5.0.1"
}, },
"prettier": { "prettier": {

View File

@ -1,4 +1,4 @@
<!DOCTYPE html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />

View File

@ -1,30 +1,43 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Button, Form, Label, Pagination, Popup, Table } from 'semantic-ui-react'; import {
Button,
Form,
Label,
Pagination,
Popup,
Table,
} from 'semantic-ui-react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { API, showError, showInfo, showSuccess, timestamp2string } from '../helpers'; import {
API,
showError,
showInfo,
showSuccess,
timestamp2string,
} from '../helpers';
import { CHANNEL_OPTIONS, ITEMS_PER_PAGE } from '../constants'; import { CHANNEL_OPTIONS, ITEMS_PER_PAGE } from '../constants';
import { renderGroup, renderNumber } from '../helpers/render'; import { renderGroup, renderNumber } from '../helpers/render';
function renderTimestamp(timestamp) { function renderTimestamp(timestamp) {
return ( return <>{timestamp2string(timestamp)}</>;
<>
{timestamp2string(timestamp)}
</>
);
} }
let type2label = undefined; let type2label = undefined;
function renderType(type) { function renderType(type) {
if (!type2label) { if (!type2label) {
type2label = new Map; type2label = new Map();
for (let i = 0; i < CHANNEL_OPTIONS.length; i++) { for (let i = 0; i < CHANNEL_OPTIONS.length; i++) {
type2label[CHANNEL_OPTIONS[i].value] = CHANNEL_OPTIONS[i]; type2label[CHANNEL_OPTIONS[i].value] = CHANNEL_OPTIONS[i];
} }
type2label[0] = { value: 0, text: '未知类型', color: 'grey' }; type2label[0] = { value: 0, text: '未知类型', color: 'grey' };
} }
return <Label basic color={type2label[type].color}>{type2label[type].text}</Label>; return (
<Label basic color={type2label[type].color}>
{type2label[type].text}
</Label>
);
} }
function renderBalance(type, balance) { function renderBalance(type, balance) {
@ -132,7 +145,11 @@ const ChannelsTable = () => {
const renderStatus = (status) => { const renderStatus = (status) => {
switch (status) { switch (status) {
case 1: case 1:
return <Label basic color='green'>已启用</Label>; return (
<Label basic color='green'>
已启用
</Label>
);
case 2: case 2:
return ( return (
<Label basic color='red'> <Label basic color='red'>
@ -152,15 +169,35 @@ const ChannelsTable = () => {
let time = responseTime / 1000; let time = responseTime / 1000;
time = time.toFixed(2) + ' 秒'; time = time.toFixed(2) + ' 秒';
if (responseTime === 0) { if (responseTime === 0) {
return <Label basic color='grey'>未测试</Label>; return (
<Label basic color='grey'>
未测试
</Label>
);
} else if (responseTime <= 1000) { } else if (responseTime <= 1000) {
return <Label basic color='green'>{time}</Label>; return (
<Label basic color='green'>
{time}
</Label>
);
} else if (responseTime <= 3000) { } else if (responseTime <= 3000) {
return <Label basic color='olive'>{time}</Label>; return (
<Label basic color='olive'>
{time}
</Label>
);
} else if (responseTime <= 5000) { } else if (responseTime <= 5000) {
return <Label basic color='yellow'>{time}</Label>; return (
<Label basic color='yellow'>
{time}
</Label>
);
} else { } else {
return <Label basic color='red'>{time}</Label>; return (
<Label basic color='red'>
{time}
</Label>
);
} }
}; };
@ -342,7 +379,7 @@ const ChannelsTable = () => {
{channels {channels
.slice( .slice(
(activePage - 1) * ITEMS_PER_PAGE, (activePage - 1) * ITEMS_PER_PAGE,
activePage * ITEMS_PER_PAGE activePage * ITEMS_PER_PAGE,
) )
.map((channel, idx) => { .map((channel, idx) => {
if (channel.deleted) return <></>; if (channel.deleted) return <></>;
@ -355,7 +392,11 @@ const ChannelsTable = () => {
<Table.Cell>{renderStatus(channel.status)}</Table.Cell> <Table.Cell>{renderStatus(channel.status)}</Table.Cell>
<Table.Cell> <Table.Cell>
<Popup <Popup
content={channel.test_time ? renderTimestamp(channel.test_time) : '未测试'} content={
channel.test_time
? renderTimestamp(channel.test_time)
: '未测试'
}
key={channel.id} key={channel.id}
trigger={renderResponseTime(channel.response_time)} trigger={renderResponseTime(channel.response_time)}
basic basic
@ -363,7 +404,11 @@ const ChannelsTable = () => {
</Table.Cell> </Table.Cell>
<Table.Cell> <Table.Cell>
<Popup <Popup
content={channel.balance_updated_time ? renderTimestamp(channel.balance_updated_time) : '未更新'} content={
channel.balance_updated_time
? renderTimestamp(channel.balance_updated_time)
: '未更新'
}
key={channel.id} key={channel.id}
trigger={renderBalance(channel.type, channel.balance)} trigger={renderBalance(channel.type, channel.balance)}
basic basic
@ -415,7 +460,7 @@ const ChannelsTable = () => {
manageChannel( manageChannel(
channel.id, channel.id,
channel.status === 1 ? 'disable' : 'enable', channel.status === 1 ? 'disable' : 'enable',
idx idx,
); );
}} }}
> >
@ -438,14 +483,24 @@ const ChannelsTable = () => {
<Table.Footer> <Table.Footer>
<Table.Row> <Table.Row>
<Table.HeaderCell colSpan='8'> <Table.HeaderCell colSpan='8'>
<Button size='small' as={Link} to='/channel/add' loading={loading}> <Button
size='small'
as={Link}
to='/channel/add'
loading={loading}
>
添加新的渠道 添加新的渠道
</Button> </Button>
<Button size='small' loading={loading} onClick={testAllChannels}> <Button size='small' loading={loading} onClick={testAllChannels}>
测试所有已启用通道 测试所有已启用通道
</Button> </Button>
<Button size='small' onClick={updateAllChannelsBalance} <Button
loading={loading || updatingBalance}>更新所有已启用通道余额</Button> size='small'
onClick={updateAllChannelsBalance}
loading={loading || updatingBalance}
>
更新所有已启用通道余额
</Button>
<Pagination <Pagination
floated='right' floated='right'
activePage={activePage} activePage={activePage}
@ -457,7 +512,9 @@ const ChannelsTable = () => {
(channels.length % ITEMS_PER_PAGE === 0 ? 1 : 0) (channels.length % ITEMS_PER_PAGE === 0 ? 1 : 0)
} }
/> />
<Button size='small' onClick={refresh} loading={loading}>刷新</Button> <Button size='small' onClick={refresh} loading={loading}>
刷新
</Button>
</Table.HeaderCell> </Table.HeaderCell>
</Table.Row> </Table.Row>
</Table.Footer> </Table.Footer>

View File

@ -37,10 +37,7 @@ const Footer = () => {
></div> ></div>
) : ( ) : (
<div className='custom-footer'> <div className='custom-footer'>
<a <a href='https://github.com/songquanpeng/one-api' target='_blank'>
href='https://github.com/songquanpeng/one-api'
target='_blank'
>
{systemName} {process.env.REACT_APP_VERSION}{' '} {systemName} {process.env.REACT_APP_VERSION}{' '}
</a> </a>
{' '} {' '}

View File

@ -2,8 +2,22 @@ import React, { useContext, useState } from 'react';
import { Link, useNavigate } from 'react-router-dom'; import { Link, useNavigate } from 'react-router-dom';
import { UserContext } from '../context/User'; import { UserContext } from '../context/User';
import { Button, Container, Dropdown, Icon, Menu, Segment } from 'semantic-ui-react'; import {
import { API, getLogo, getSystemName, isAdmin, isMobile, showSuccess } from '../helpers'; Button,
Container,
Dropdown,
Icon,
Menu,
Segment,
} from 'semantic-ui-react';
import {
API,
getLogo,
getSystemName,
isAdmin,
isMobile,
showSuccess,
} from '../helpers';
import '../index.css'; import '../index.css';
// Header Buttons // Header Buttons
@ -11,58 +25,58 @@ let headerButtons = [
{ {
name: '首页', name: '首页',
to: '/', to: '/',
icon: 'home' icon: 'home',
}, },
{ {
name: '渠道', name: '渠道',
to: '/channel', to: '/channel',
icon: 'sitemap', icon: 'sitemap',
admin: true admin: true,
}, },
{ {
name: '令牌', name: '令牌',
to: '/token', to: '/token',
icon: 'key' icon: 'key',
}, },
{ {
name: '兑换', name: '兑换',
to: '/redemption', to: '/redemption',
icon: 'dollar sign', icon: 'dollar sign',
admin: true admin: true,
}, },
{ {
name: '充值', name: '充值',
to: '/topup', to: '/topup',
icon: 'cart' icon: 'cart',
}, },
{ {
name: '用户', name: '用户',
to: '/user', to: '/user',
icon: 'user', icon: 'user',
admin: true admin: true,
}, },
{ {
name: '日志', name: '日志',
to: '/log', to: '/log',
icon: 'book' icon: 'book',
}, },
{ {
name: '设置', name: '设置',
to: '/setting', to: '/setting',
icon: 'setting' icon: 'setting',
}, },
{ {
name: '关于', name: '关于',
to: '/about', to: '/about',
icon: 'info circle' icon: 'info circle',
} },
]; ];
if (localStorage.getItem('chat_link')) { if (localStorage.getItem('chat_link')) {
headerButtons.splice(1, 0, { headerButtons.splice(1, 0, {
name: '聊天', name: '聊天',
to: '/chat', to: '/chat',
icon: 'comments' icon: 'comments',
}); });
} }
@ -120,21 +134,17 @@ const Header = () => {
style={ style={
showSidebar showSidebar
? { ? {
borderBottom: 'none', borderBottom: 'none',
marginBottom: '0', marginBottom: '0',
borderTop: 'none', borderTop: 'none',
height: '51px' height: '51px',
} }
: { borderTop: 'none', height: '52px' } : { borderTop: 'none', height: '52px' }
} }
> >
<Container> <Container>
<Menu.Item as={Link} to='/'> <Menu.Item as={Link} to='/'>
<img <img src={logo} alt='logo' style={{ marginRight: '0.75em' }} />
src={logo}
alt='logo'
style={{ marginRight: '0.75em' }}
/>
<div style={{ fontSize: '20px' }}> <div style={{ fontSize: '20px' }}>
<b>{systemName}</b> <b>{systemName}</b>
</div> </div>

View File

@ -34,7 +34,7 @@ const LoginForm = () => {
const logo = getLogo(); const logo = getLogo();
useEffect(() => { useEffect(() => {
if (searchParams.get("expired")) { if (searchParams.get('expired')) {
showError('未登录或登录已过期,请重新登录!'); showError('未登录或登录已过期,请重新登录!');
} }
let status = localStorage.getItem('status'); let status = localStorage.getItem('status');
@ -53,13 +53,13 @@ const LoginForm = () => {
const onGitHubOAuthClicked = () => { const onGitHubOAuthClicked = () => {
window.open( window.open(
`https://github.com/login/oauth/authorize?client_id=${status.github_client_id}&scope=user:email` `https://github.com/login/oauth/authorize?client_id=${status.github_client_id}&scope=user:email`,
); );
}; };
const onDiscordOAuthClicked = () => { const onDiscordOAuthClicked = () => {
window.open( window.open(
`https://discord.com/oauth2/authorize?response_type=code&client_id=${status.discord_client_id}&redirect_uri=${window.location.origin}/oauth/discord&scope=identify` `https://discord.com/oauth2/authorize?response_type=code&client_id=${status.discord_client_id}&redirect_uri=${window.location.origin}/oauth/discord&scope=identify`,
); );
}; };
@ -69,7 +69,7 @@ const LoginForm = () => {
const onSubmitWeChatVerificationCode = async () => { const onSubmitWeChatVerificationCode = async () => {
const res = await API.get( const res = await API.get(
`/api/oauth/wechat?code=${inputs.wechat_verification_code}` `/api/oauth/wechat?code=${inputs.wechat_verification_code}`,
); );
const { success, message, data } = res.data; const { success, message, data } = res.data;
if (success) { if (success) {
@ -96,10 +96,13 @@ const LoginForm = () => {
return; return;
} }
const res = await API.post(`/api/user/login?turnstile=${turnstileToken}`, { const res = await API.post(
username, `/api/user/login?turnstile=${turnstileToken}`,
password, {
}); username,
password,
},
);
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 });
@ -113,29 +116,29 @@ const LoginForm = () => {
} }
return ( return (
<Grid textAlign="center" style={{ marginTop: '48px' }}> <Grid textAlign='center' style={{ marginTop: '48px' }}>
<Grid.Column style={{ maxWidth: 450 }}> <Grid.Column style={{ maxWidth: 450 }}>
<Header as="h2" color="" textAlign="center"> <Header as='h2' color='' textAlign='center'>
<Image src={logo} /> 用户登录 <Image src={logo} /> 用户登录
</Header> </Header>
<Form size="large"> <Form size='large'>
<Segment> <Segment>
<Form.Input <Form.Input
fluid fluid
icon="user" icon='user'
iconPosition="left" iconPosition='left'
placeholder="用户名" placeholder='用户名'
name="username" name='username'
value={username} value={username}
onChange={handleChange} onChange={handleChange}
/> />
<Form.Input <Form.Input
fluid fluid
icon="lock" icon='lock'
iconPosition="left" iconPosition='left'
placeholder="密码" placeholder='密码'
name="password" name='password'
type="password" type='password'
value={password} value={password}
onChange={handleChange} onChange={handleChange}
/> />
@ -149,18 +152,18 @@ const LoginForm = () => {
) : ( ) : (
<></> <></>
)} )}
<Button color="" fluid size="large" onClick={handleSubmit}> <Button color='' fluid size='large' onClick={handleSubmit}>
登录 登录
</Button> </Button>
</Segment> </Segment>
</Form> </Form>
<Message> <Message>
忘记密码 忘记密码
<Link to="/reset" className="btn btn-link"> <Link to='/reset' className='btn btn-link'>
点击重置 点击重置
</Link> </Link>
没有账户 没有账户
<Link to="/register" className="btn btn-link"> <Link to='/register' className='btn btn-link'>
点击注册 点击注册
</Link> </Link>
</Message> </Message>
@ -170,24 +173,24 @@ const LoginForm = () => {
{status.discord_oauth && ( {status.discord_oauth && (
<Button <Button
circular circular
color="blue" color='blue'
icon="discord" icon='discord'
onClick={onDiscordOAuthClicked} onClick={onDiscordOAuthClicked}
/> />
)} )}
{status.github_oauth && ( {status.github_oauth && (
<Button <Button
circular circular
color="black" color='black'
icon="github" icon='github'
onClick={onGitHubOAuthClicked} onClick={onGitHubOAuthClicked}
/> />
)} )}
{status.wechat_login && ( {status.wechat_login && (
<Button <Button
circular circular
color="green" color='green'
icon="wechat" icon='wechat'
onClick={onWeChatLoginClicked} onClick={onWeChatLoginClicked}
/> />
)} )}
@ -209,18 +212,18 @@ const LoginForm = () => {
微信扫码关注公众号输入验证码获取验证码三分钟内有效 微信扫码关注公众号输入验证码获取验证码三分钟内有效
</p> </p>
</div> </div>
<Form size="large"> <Form size='large'>
<Form.Input <Form.Input
fluid fluid
placeholder="验证码" placeholder='验证码'
name="wechat_verification_code" name='wechat_verification_code'
value={inputs.wechat_verification_code} value={inputs.wechat_verification_code}
onChange={handleChange} onChange={handleChange}
/> />
<Button <Button
color="" color=''
fluid fluid
size="large" size='large'
onClick={onSubmitWeChatVerificationCode} onClick={onSubmitWeChatVerificationCode}
> >
登录 登录

View File

@ -1,21 +1,26 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Button, Form, Header, Label, Pagination, Segment, Select, Table } from 'semantic-ui-react'; import {
Button,
Form,
Header,
Label,
Pagination,
Segment,
Select,
Table,
} from 'semantic-ui-react';
import { API, isAdmin, showError, timestamp2string } from '../helpers'; import { API, isAdmin, showError, timestamp2string } from '../helpers';
import { ITEMS_PER_PAGE } from '../constants'; import { ITEMS_PER_PAGE } from '../constants';
import { renderQuota } from '../helpers/render'; import { renderQuota } from '../helpers/render';
function renderTimestamp(timestamp) { function renderTimestamp(timestamp) {
return ( return <>{timestamp2string(timestamp)}</>;
<>
{timestamp2string(timestamp)}
</>
);
} }
const MODE_OPTIONS = [ const MODE_OPTIONS = [
{ key: 'all', text: '全部用户', value: 'all' }, { key: 'all', text: '全部用户', value: 'all' },
{ key: 'self', text: '当前用户', value: 'self' } { key: 'self', text: '当前用户', value: 'self' },
]; ];
const LOG_OPTIONS = [ const LOG_OPTIONS = [
@ -23,21 +28,46 @@ const LOG_OPTIONS = [
{ key: '1', text: '充值', value: 1 }, { key: '1', text: '充值', value: 1 },
{ key: '2', text: '消费', value: 2 }, { key: '2', text: '消费', value: 2 },
{ key: '3', text: '管理', value: 3 }, { key: '3', text: '管理', value: 3 },
{ key: '4', text: '系统', value: 4 } { key: '4', text: '系统', value: 4 },
]; ];
function renderType(type) { function renderType(type) {
switch (type) { switch (type) {
case 1: case 1:
return <Label basic color='green'> 充值 </Label>; return (
<Label basic color='green'>
{' '}
充值{' '}
</Label>
);
case 2: case 2:
return <Label basic color='olive'> 消费 </Label>; return (
<Label basic color='olive'>
{' '}
消费{' '}
</Label>
);
case 3: case 3:
return <Label basic color='orange'> 管理 </Label>; return (
<Label basic color='orange'>
{' '}
管理{' '}
</Label>
);
case 4: case 4:
return <Label basic color='purple'> 系统 </Label>; return (
<Label basic color='purple'>
{' '}
系统{' '}
</Label>
);
default: default:
return <Label basic color='black'> 未知 </Label>; return (
<Label basic color='black'>
{' '}
未知{' '}
</Label>
);
} }
} }
@ -55,13 +85,14 @@ const LogsTable = () => {
token_name: '', token_name: '',
model_name: '', model_name: '',
start_timestamp: timestamp2string(0), start_timestamp: timestamp2string(0),
end_timestamp: timestamp2string(now.getTime() / 1000 + 3600) end_timestamp: timestamp2string(now.getTime() / 1000 + 3600),
}); });
const { username, token_name, model_name, start_timestamp, end_timestamp } = inputs; const { username, token_name, model_name, start_timestamp, end_timestamp } =
inputs;
const [stat, setStat] = useState({ const [stat, setStat] = useState({
quota: 0, quota: 0,
token: 0 token: 0,
}); });
const handleInputChange = (e, { name, value }) => { const handleInputChange = (e, { name, value }) => {
@ -71,7 +102,9 @@ const LogsTable = () => {
const getLogSelfStat = async () => { const getLogSelfStat = async () => {
let localStartTimestamp = Date.parse(start_timestamp) / 1000; let localStartTimestamp = Date.parse(start_timestamp) / 1000;
let localEndTimestamp = Date.parse(end_timestamp) / 1000; let localEndTimestamp = Date.parse(end_timestamp) / 1000;
let res = await API.get(`/api/log/self/stat?type=${logType}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`); let res = await API.get(
`/api/log/self/stat?type=${logType}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`,
);
const { success, message, data } = res.data; const { success, message, data } = res.data;
if (success) { if (success) {
setStat(data); setStat(data);
@ -83,7 +116,9 @@ const LogsTable = () => {
const getLogStat = async () => { const getLogStat = async () => {
let localStartTimestamp = Date.parse(start_timestamp) / 1000; let localStartTimestamp = Date.parse(start_timestamp) / 1000;
let localEndTimestamp = Date.parse(end_timestamp) / 1000; let localEndTimestamp = Date.parse(end_timestamp) / 1000;
let res = await API.get(`/api/log/stat?type=${logType}&username=${username}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`); let res = await API.get(
`/api/log/stat?type=${logType}&username=${username}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`,
);
const { success, message, data } = res.data; const { success, message, data } = res.data;
if (success) { if (success) {
setStat(data); setStat(data);
@ -129,7 +164,7 @@ const LogsTable = () => {
const refresh = async () => { const refresh = async () => {
setLoading(true); setLoading(true);
setActivePage(1) setActivePage(1);
await loadLogs(0); await loadLogs(0);
if (isAdminUser) { if (isAdminUser) {
getLogStat().then(); getLogStat().then();
@ -169,7 +204,7 @@ const LogsTable = () => {
if (logs.length === 0) return; if (logs.length === 0) return;
setLoading(true); setLoading(true);
let sortedLogs = [...logs]; let sortedLogs = [...logs];
if (typeof sortedLogs[0][key] === 'string'){ if (typeof sortedLogs[0][key] === 'string') {
sortedLogs.sort((a, b) => { sortedLogs.sort((a, b) => {
return ('' + a[key]).localeCompare(b[key]); return ('' + a[key]).localeCompare(b[key]);
}); });
@ -190,28 +225,61 @@ const LogsTable = () => {
return ( return (
<> <>
<Segment> <Segment>
<Header as='h3'>使用明细总消耗额度{renderQuota(stat.quota)}</Header> <Header as='h3'>
使用明细总消耗额度{renderQuota(stat.quota)}
</Header>
<Form> <Form>
<Form.Group> <Form.Group>
{ {isAdminUser && (
isAdminUser && ( <Form.Input
<Form.Input fluid label={'用户名称'} width={2} value={username} fluid
placeholder={'可选值'} name='username' label={'用户名称'}
onChange={handleInputChange} /> width={2}
) value={username}
} placeholder={'可选值'}
<Form.Input fluid label={'令牌名称'} width={isAdminUser ? 2 : 3} value={token_name} name='username'
placeholder={'可选值'} name='token_name' onChange={handleInputChange} /> onChange={handleInputChange}
<Form.Input fluid label='模型名称' width={isAdminUser ? 2 : 3} value={model_name} placeholder='可选值' />
name='model_name' )}
onChange={handleInputChange} /> <Form.Input
<Form.Input fluid label='起始时间' width={4} value={start_timestamp} type='datetime-local' fluid
name='start_timestamp' label={'令牌名称'}
onChange={handleInputChange} /> width={isAdminUser ? 2 : 3}
<Form.Input fluid label='结束时间' width={4} value={end_timestamp} type='datetime-local' value={token_name}
name='end_timestamp' placeholder={'可选值'}
onChange={handleInputChange} /> name='token_name'
<Form.Button fluid label='操作' width={2} onClick={refresh}>查询</Form.Button> onChange={handleInputChange}
/>
<Form.Input
fluid
label='模型名称'
width={isAdminUser ? 2 : 3}
value={model_name}
placeholder='可选值'
name='model_name'
onChange={handleInputChange}
/>
<Form.Input
fluid
label='起始时间'
width={4}
value={start_timestamp}
type='datetime-local'
name='start_timestamp'
onChange={handleInputChange}
/>
<Form.Input
fluid
label='结束时间'
width={4}
value={end_timestamp}
type='datetime-local'
name='end_timestamp'
onChange={handleInputChange}
/>
<Form.Button fluid label='操作' width={2} onClick={refresh}>
查询
</Form.Button>
</Form.Group> </Form.Group>
</Form> </Form>
<Table basic compact size='small'> <Table basic compact size='small'>
@ -226,8 +294,8 @@ const LogsTable = () => {
> >
时间 时间
</Table.HeaderCell> </Table.HeaderCell>
{ {isAdminUser && (
isAdminUser && <Table.HeaderCell <Table.HeaderCell
style={{ cursor: 'pointer' }} style={{ cursor: 'pointer' }}
onClick={() => { onClick={() => {
sortLog('username'); sortLog('username');
@ -236,7 +304,7 @@ const LogsTable = () => {
> >
用户 用户
</Table.HeaderCell> </Table.HeaderCell>
} )}
<Table.HeaderCell <Table.HeaderCell
style={{ cursor: 'pointer' }} style={{ cursor: 'pointer' }}
onClick={() => { onClick={() => {
@ -307,24 +375,42 @@ const LogsTable = () => {
{logs {logs
.slice( .slice(
(activePage - 1) * ITEMS_PER_PAGE, (activePage - 1) * ITEMS_PER_PAGE,
activePage * ITEMS_PER_PAGE activePage * ITEMS_PER_PAGE,
) )
.map((log, idx) => { .map((log, idx) => {
if (log.deleted) return <></>; if (log.deleted) return <></>;
return ( return (
<Table.Row key={log.created_at}> <Table.Row key={log.created_at}>
<Table.Cell>{renderTimestamp(log.created_at)}</Table.Cell> <Table.Cell>{renderTimestamp(log.created_at)}</Table.Cell>
{ {isAdminUser && (
isAdminUser && ( <Table.Cell>
<Table.Cell>{log.username ? <Label>{log.username}</Label> : ''}</Table.Cell> {log.username ? <Label>{log.username}</Label> : ''}
) </Table.Cell>
} )}
<Table.Cell>{log.token_name ? <Label basic>{log.token_name}</Label> : ''}</Table.Cell> <Table.Cell>
{log.token_name ? (
<Label basic>{log.token_name}</Label>
) : (
''
)}
</Table.Cell>
<Table.Cell>{renderType(log.type)}</Table.Cell> <Table.Cell>{renderType(log.type)}</Table.Cell>
<Table.Cell>{log.model_name ? <Label basic>{log.model_name}</Label> : ''}</Table.Cell> <Table.Cell>
<Table.Cell>{log.prompt_tokens ? log.prompt_tokens : ''}</Table.Cell> {log.model_name ? (
<Table.Cell>{log.completion_tokens ? log.completion_tokens : ''}</Table.Cell> <Label basic>{log.model_name}</Label>
<Table.Cell>{log.quota ? renderQuota(log.quota, 6) : ''}</Table.Cell> ) : (
''
)}
</Table.Cell>
<Table.Cell>
{log.prompt_tokens ? log.prompt_tokens : ''}
</Table.Cell>
<Table.Cell>
{log.completion_tokens ? log.completion_tokens : ''}
</Table.Cell>
<Table.Cell>
{log.quota ? renderQuota(log.quota, 6) : ''}
</Table.Cell>
<Table.Cell>{log.content}</Table.Cell> <Table.Cell>{log.content}</Table.Cell>
</Table.Row> </Table.Row>
); );
@ -344,7 +430,9 @@ const LogsTable = () => {
setLogType(value); setLogType(value);
}} }}
/> />
<Button size='small' onClick={refresh} loading={loading}>刷新</Button> <Button size='small' onClick={refresh} loading={loading}>
刷新
</Button>
<Pagination <Pagination
floated='right' floated='right'
activePage={activePage} activePage={activePage}

View File

@ -54,7 +54,7 @@ const OperationSetting = () => {
} }
const res = await API.put('/api/option/', { const res = await API.put('/api/option/', {
key, key,
value value,
}); });
const { success, message } = res.data; const { success, message } = res.data;
if (success) { if (success) {
@ -76,11 +76,22 @@ const OperationSetting = () => {
const submitConfig = async (group) => { const submitConfig = async (group) => {
switch (group) { switch (group) {
case 'monitor': case 'monitor':
if (originInputs['ChannelDisableThreshold'] !== inputs.ChannelDisableThreshold) { if (
await updateOption('ChannelDisableThreshold', inputs.ChannelDisableThreshold); originInputs['ChannelDisableThreshold'] !==
inputs.ChannelDisableThreshold
) {
await updateOption(
'ChannelDisableThreshold',
inputs.ChannelDisableThreshold,
);
} }
if (originInputs['QuotaRemindThreshold'] !== inputs.QuotaRemindThreshold) { if (
await updateOption('QuotaRemindThreshold', inputs.QuotaRemindThreshold); originInputs['QuotaRemindThreshold'] !== inputs.QuotaRemindThreshold
) {
await updateOption(
'QuotaRemindThreshold',
inputs.QuotaRemindThreshold,
);
} }
break; break;
case 'ratio': case 'ratio':
@ -134,9 +145,7 @@ const OperationSetting = () => {
<Grid columns={1}> <Grid columns={1}>
<Grid.Column> <Grid.Column>
<Form loading={loading}> <Form loading={loading}>
<Header as='h3'> <Header as='h3'>通用设置</Header>
通用设置
</Header>
<Form.Group widths={4}> <Form.Group widths={4}>
<Form.Input <Form.Input
label='充值链接' label='充值链接'
@ -204,13 +213,15 @@ const OperationSetting = () => {
onChange={handleInputChange} onChange={handleInputChange}
/> />
</Form.Group> </Form.Group>
<Form.Button onClick={() => { <Form.Button
submitConfig('general').then(); onClick={() => {
}}>保存通用设置</Form.Button> submitConfig('general').then();
}}
>
保存通用设置
</Form.Button>
<Divider /> <Divider />
<Header as='h3'> <Header as='h3'>监控设置</Header>
监控设置
</Header>
<Form.Group widths={3}> <Form.Group widths={3}>
<Form.Input <Form.Input
label='最长响应时间' label='最长响应时间'
@ -241,13 +252,15 @@ const OperationSetting = () => {
onChange={handleInputChange} onChange={handleInputChange}
/> />
</Form.Group> </Form.Group>
<Form.Button onClick={() => { <Form.Button
submitConfig('monitor').then(); onClick={() => {
}}>保存监控设置</Form.Button> submitConfig('monitor').then();
}}
>
保存监控设置
</Form.Button>
<Divider /> <Divider />
<Header as='h3'> <Header as='h3'>额度设置</Header>
额度设置
</Header>
<Form.Group widths={4}> <Form.Group widths={4}>
<Form.Input <Form.Input
label='新用户初始额度' label='新用户初始额度'
@ -290,13 +303,15 @@ const OperationSetting = () => {
placeholder='例如1000' placeholder='例如1000'
/> />
</Form.Group> </Form.Group>
<Form.Button onClick={() => { <Form.Button
submitConfig('quota').then(); onClick={() => {
}}>保存额度设置</Form.Button> submitConfig('quota').then();
}}
>
保存额度设置
</Form.Button>
<Divider /> <Divider />
<Header as='h3'> <Header as='h3'>倍率设置</Header>
倍率设置
</Header>
<Form.Group widths='equal'> <Form.Group widths='equal'>
<Form.TextArea <Form.TextArea
label='模型倍率' label='模型倍率'
@ -319,9 +334,13 @@ const OperationSetting = () => {
placeholder='为一个 JSON 文本,键为分组名称,值为倍率' placeholder='为一个 JSON 文本,键为分组名称,值为倍率'
/> />
</Form.Group> </Form.Group>
<Form.Button onClick={() => { <Form.Button
submitConfig('ratio').then(); onClick={() => {
}}>保存倍率设置</Form.Button> submitConfig('ratio').then();
}}
>
保存倍率设置
</Form.Button>
</Form> </Form>
</Grid.Column> </Grid.Column>
</Grid> </Grid>

View File

@ -1,5 +1,13 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Button, Divider, Form, Grid, Header, Message, Modal } from 'semantic-ui-react'; import {
Button,
Divider,
Form,
Grid,
Header,
Message,
Modal,
} from 'semantic-ui-react';
import { API, showError, showSuccess } from '../helpers'; import { API, showError, showSuccess } from '../helpers';
import { marked } from 'marked'; import { marked } from 'marked';
@ -10,13 +18,13 @@ const OtherSetting = () => {
About: '', About: '',
SystemName: '', SystemName: '',
Logo: '', Logo: '',
HomePageContent: '' HomePageContent: '',
}); });
let [loading, setLoading] = useState(false); let [loading, setLoading] = useState(false);
const [showUpdateModal, setShowUpdateModal] = useState(false); const [showUpdateModal, setShowUpdateModal] = useState(false);
const [updateData, setUpdateData] = useState({ const [updateData, setUpdateData] = useState({
tag_name: '', tag_name: '',
content: '' content: '',
}); });
const getOptions = async () => { const getOptions = async () => {
@ -43,7 +51,7 @@ const OtherSetting = () => {
setLoading(true); setLoading(true);
const res = await API.put('/api/option/', { const res = await API.put('/api/option/', {
key, key,
value value,
}); });
const { success, message } = res.data; const { success, message } = res.data;
if (success) { if (success) {
@ -83,13 +91,12 @@ const OtherSetting = () => {
}; };
const openGitHubRelease = () => { const openGitHubRelease = () => {
window.location = window.location = 'https://github.com/songquanpeng/one-api/releases/latest';
'https://github.com/songquanpeng/one-api/releases/latest';
}; };
const checkUpdate = async () => { const checkUpdate = async () => {
const res = await API.get( const res = await API.get(
'https://api.github.com/repos/songquanpeng/one-api/releases/latest' 'https://api.github.com/repos/songquanpeng/one-api/releases/latest',
); );
const { tag_name, body } = res.data; const { tag_name, body } = res.data;
if (tag_name === process.env.REACT_APP_VERSION) { if (tag_name === process.env.REACT_APP_VERSION) {
@ -97,7 +104,7 @@ const OtherSetting = () => {
} else { } else {
setUpdateData({ setUpdateData({
tag_name: tag_name, tag_name: tag_name,
content: marked.parse(body) content: marked.parse(body),
}); });
setShowUpdateModal(true); setShowUpdateModal(true);
} }
@ -153,7 +160,9 @@ const OtherSetting = () => {
style={{ minHeight: 150, fontFamily: 'JetBrains Mono, Consolas' }} style={{ minHeight: 150, fontFamily: 'JetBrains Mono, Consolas' }}
/> />
</Form.Group> </Form.Group>
<Form.Button onClick={() => submitOption('HomePageContent')}>保存首页内容</Form.Button> <Form.Button onClick={() => submitOption('HomePageContent')}>
保存首页内容
</Form.Button>
<Form.Group widths='equal'> <Form.Group widths='equal'>
<Form.TextArea <Form.TextArea
label='关于' label='关于'
@ -165,7 +174,10 @@ const OtherSetting = () => {
/> />
</Form.Group> </Form.Group>
<Form.Button onClick={submitAbout}>保存关于</Form.Button> <Form.Button onClick={submitAbout}>保存关于</Form.Button>
<Message>移除 One API 的版权标识必须首先获得授权项目维护需要花费大量精力如果本项目对你有意义请主动支持本项目</Message> <Message>
移除 One API
的版权标识必须首先获得授权项目维护需要花费大量精力如果本项目对你有意义请主动支持本项目
</Message>
<Form.Group widths='equal'> <Form.Group widths='equal'>
<Form.Input <Form.Input
label='页脚' label='页脚'

View File

@ -1,6 +1,13 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Button, Form, Grid, Header, Image, Segment } from 'semantic-ui-react'; import { Button, Form, Grid, Header, Image, Segment } from 'semantic-ui-react';
import { API, copy, showError, showInfo, showNotice, showSuccess } from '../helpers'; import {
API,
copy,
showError,
showInfo,
showNotice,
showSuccess,
} from '../helpers';
import { useSearchParams } from 'react-router-dom'; import { useSearchParams } from 'react-router-dom';
const PasswordResetConfirm = () => { const PasswordResetConfirm = () => {

View File

@ -38,7 +38,7 @@ const PasswordResetForm = () => {
} }
setLoading(true); setLoading(true);
const res = await API.get( const res = await API.get(
`/api/reset_password?email=${email}&turnstile=${turnstileToken}` `/api/reset_password?email=${email}&turnstile=${turnstileToken}`,
); );
const { success, message } = res.data; const { success, message } = res.data;
if (success) { if (success) {

View File

@ -1,7 +1,23 @@
import React, { useEffect, useState, useContext } from 'react'; import React, { useEffect, useState, useContext } from 'react';
import { Button, Divider, Form, Header, Image, Message, Modal, Label } from 'semantic-ui-react'; import {
Button,
Divider,
Form,
Header,
Image,
Message,
Modal,
Label,
} from 'semantic-ui-react';
import { Link, useNavigate } from 'react-router-dom'; import { Link, useNavigate } from 'react-router-dom';
import { API, copy, showError, showInfo, showNotice, showSuccess } from '../helpers'; import {
API,
copy,
showError,
showInfo,
showNotice,
showSuccess,
} from '../helpers';
import Turnstile from 'react-turnstile'; import Turnstile from 'react-turnstile';
import { UserContext } from '../context/User'; import { UserContext } from '../context/User';
@ -81,12 +97,12 @@ const PersonalSetting = () => {
} else { } else {
showError(message); showError(message);
} }
} };
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) {
@ -99,15 +115,15 @@ const PersonalSetting = () => {
const openGitHubOAuth = () => { const openGitHubOAuth = () => {
window.open( window.open(
`https://github.com/login/oauth/authorize?client_id=${status.github_client_id}&scope=user:email` `https://github.com/login/oauth/authorize?client_id=${status.github_client_id}&scope=user:email`,
); );
}; };
const openDiscordOAuth = () => { const openDiscordOAuth = () => {
window.open( window.open(
`https://discord.com/api/oauth2/authorize?client_id=${status.discord_client_id}&scope=identify%20email&response_type=code&redirect_uri=${window.location.origin}/oauth/discord` `https://discord.com/api/oauth2/authorize?client_id=${status.discord_client_id}&scope=identify%20email&response_type=code&redirect_uri=${window.location.origin}/oauth/discord`,
); );
} };
const sendVerificationCode = async () => { const sendVerificationCode = async () => {
if (inputs.email === '') return; if (inputs.email === '') return;
@ -117,7 +133,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) {
@ -132,7 +148,7 @@ const PersonalSetting = () => {
if (inputs.email_verification_code === '') return; if (inputs.email_verification_code === '') return;
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) {
@ -148,29 +164,33 @@ const PersonalSetting = () => {
<div style={{ lineHeight: '40px' }}> <div style={{ lineHeight: '40px' }}>
<Header as='h3'>通用设置</Header> <Header as='h3'>通用设置</Header>
<Message> <Message>
注意此处生成的令牌用于系统管理而非用于请求 OpenAI 相关的服务请知悉 注意此处生成的令牌用于系统管理而非用于请求 OpenAI
相关的服务请知悉
</Message> </Message>
<Button as={Link} to={`/user/edit/`}> <Button as={Link} to={`/user/edit/`}>
更新个人信息 更新个人信息
</Button> </Button>
<Button onClick={generateAccessToken}>生成系统访问令牌</Button> <Button onClick={generateAccessToken}>生成系统访问令牌</Button>
<Button onClick={getAffLink}>复制邀请链接</Button> <Button onClick={getAffLink}>复制邀请链接</Button>
<Button onClick={() => { <Button
setShowAccountDeleteModal(true); onClick={() => {
}} color='red'>删除个人账户</Button> setShowAccountDeleteModal(true);
}}
color='red'
>
删除个人账户
</Button>
<Divider /> <Divider />
<Header as='h3'>账号绑定</Header> <Header as='h3'>账号绑定</Header>
{ {status.wechat_login && (
status.wechat_login && ( <Button
<Button onClick={() => {
onClick={() => { setShowWeChatBindModal(true);
setShowWeChatBindModal(true); }}
}} >
> 绑定微信账号
绑定微信账号 </Button>
</Button> )}
)
}
<Modal <Modal
onClose={() => setShowWeChatBindModal(false)} onClose={() => setShowWeChatBindModal(false)}
onOpen={() => setShowWeChatBindModal(true)} onOpen={() => setShowWeChatBindModal(true)}
@ -200,16 +220,12 @@ const PersonalSetting = () => {
</Modal.Description> </Modal.Description>
</Modal.Content> </Modal.Content>
</Modal> </Modal>
{ {status.github_oauth && (
status.github_oauth && ( <Button onClick={openGitHubOAuth}>绑定 GitHub 账号</Button>
<Button onClick={openGitHubOAuth}>绑定 GitHub 账号</Button> )}
) {status.discord_oauth && (
} <Button onClick={openDiscordOAuth}>绑定 Discord 账号</Button>
{ )}
status.discord_oauth && (
<Button onClick={openDiscordOAuth}>绑定 Discord 账号</Button>
)
}
<Button <Button
onClick={() => { onClick={() => {
setShowEmailBindModal(true); setShowEmailBindModal(true);

View File

@ -2,7 +2,6 @@ import { Navigate } from 'react-router-dom';
import { history } from '../helpers'; import { history } from '../helpers';
function PrivateRoute({ children }) { function PrivateRoute({ children }) {
if (!localStorage.getItem('user')) { if (!localStorage.getItem('user')) {
return <Navigate to='/login' state={{ from: history.location }} />; return <Navigate to='/login' state={{ from: history.location }} />;

View File

@ -1,29 +1,59 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Button, Form, Label, Message, Pagination, Table } from 'semantic-ui-react'; import {
Button,
Form,
Label,
Message,
Pagination,
Table,
} from 'semantic-ui-react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { API, copy, showError, showInfo, showSuccess, showWarning, timestamp2string } from '../helpers'; import {
API,
copy,
showError,
showInfo,
showSuccess,
showWarning,
timestamp2string,
} from '../helpers';
import { ITEMS_PER_PAGE } from '../constants'; import { ITEMS_PER_PAGE } from '../constants';
import { renderQuota } from '../helpers/render'; import { renderQuota } from '../helpers/render';
function renderTimestamp(timestamp) { function renderTimestamp(timestamp) {
return ( return <>{timestamp2string(timestamp)}</>;
<>
{timestamp2string(timestamp)}
</>
);
} }
function renderStatus(status) { function renderStatus(status) {
switch (status) { switch (status) {
case 1: case 1:
return <Label basic color='green'>未使用</Label>; return (
<Label basic color='green'>
未使用
</Label>
);
case 2: case 2:
return <Label basic color='red'> 已禁用 </Label>; return (
<Label basic color='red'>
{' '}
已禁用{' '}
</Label>
);
case 3: case 3:
return <Label basic color='grey'> 已使用 </Label>; return (
<Label basic color='grey'>
{' '}
已使用{' '}
</Label>
);
default: default:
return <Label basic color='black'> 未知状态 </Label>; return (
<Label basic color='black'>
{' '}
未知状态{' '}
</Label>
);
} }
} }
@ -110,7 +140,9 @@ const RedemptionsTable = () => {
return; return;
} }
setSearching(true); setSearching(true);
const res = await API.get(`/api/redemption/search?keyword=${searchKeyword}`); const res = await API.get(
`/api/redemption/search?keyword=${searchKeyword}`,
);
const { success, message, data } = res.data; const { success, message, data } = res.data;
if (success) { if (success) {
setRedemptions(data); setRedemptions(data);
@ -212,18 +244,26 @@ const RedemptionsTable = () => {
{redemptions {redemptions
.slice( .slice(
(activePage - 1) * ITEMS_PER_PAGE, (activePage - 1) * ITEMS_PER_PAGE,
activePage * ITEMS_PER_PAGE activePage * ITEMS_PER_PAGE,
) )
.map((redemption, idx) => { .map((redemption, idx) => {
if (redemption.deleted) return <></>; if (redemption.deleted) return <></>;
return ( return (
<Table.Row key={redemption.id}> <Table.Row key={redemption.id}>
<Table.Cell>{redemption.id}</Table.Cell> <Table.Cell>{redemption.id}</Table.Cell>
<Table.Cell>{redemption.name ? redemption.name : '无'}</Table.Cell> <Table.Cell>
{redemption.name ? redemption.name : '无'}
</Table.Cell>
<Table.Cell>{renderStatus(redemption.status)}</Table.Cell> <Table.Cell>{renderStatus(redemption.status)}</Table.Cell>
<Table.Cell>{renderQuota(redemption.quota)}</Table.Cell> <Table.Cell>{renderQuota(redemption.quota)}</Table.Cell>
<Table.Cell>{renderTimestamp(redemption.created_time)}</Table.Cell> <Table.Cell>
<Table.Cell>{redemption.redeemed_time ? renderTimestamp(redemption.redeemed_time) : "尚未兑换"} </Table.Cell> {renderTimestamp(redemption.created_time)}
</Table.Cell>
<Table.Cell>
{redemption.redeemed_time
? renderTimestamp(redemption.redeemed_time)
: '尚未兑换'}{' '}
</Table.Cell>
<Table.Cell> <Table.Cell>
<div> <div>
<Button <Button
@ -233,7 +273,9 @@ const RedemptionsTable = () => {
if (await copy(redemption.key)) { if (await copy(redemption.key)) {
showSuccess('已复制到剪贴板!'); showSuccess('已复制到剪贴板!');
} else { } else {
showWarning('无法复制到剪贴板,请手动复制,已将兑换码填入搜索框。') showWarning(
'无法复制到剪贴板,请手动复制,已将兑换码填入搜索框。',
);
setSearchKeyword(redemption.key); setSearchKeyword(redemption.key);
} }
}} }}
@ -251,12 +293,12 @@ const RedemptionsTable = () => {
</Button> </Button>
<Button <Button
size={'small'} size={'small'}
disabled={redemption.status === 3} // used disabled={redemption.status === 3} // used
onClick={() => { onClick={() => {
manageRedemption( manageRedemption(
redemption.id, redemption.id,
redemption.status === 1 ? 'disable' : 'enable', redemption.status === 1 ? 'disable' : 'enable',
idx idx,
); );
}} }}
> >
@ -279,7 +321,12 @@ const RedemptionsTable = () => {
<Table.Footer> <Table.Footer>
<Table.Row> <Table.Row>
<Table.HeaderCell colSpan='8'> <Table.HeaderCell colSpan='8'>
<Button size='small' as={Link} to='/redemption/add' loading={loading}> <Button
size='small'
as={Link}
to='/redemption/add'
loading={loading}
>
添加新的兑换码 添加新的兑换码
</Button> </Button>
<Pagination <Pagination

View File

@ -73,7 +73,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) {
@ -94,7 +94,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) {

View File

@ -70,7 +70,7 @@ const SystemSetting = () => {
} }
const res = await API.put('/api/option/', { const res = await API.put('/api/option/', {
key, key,
value value,
}); });
const { success, message } = res.data; const { success, message } = res.data;
if (success) { if (success) {
@ -135,7 +135,7 @@ const SystemSetting = () => {
if (originInputs['WeChatServerAddress'] !== inputs.WeChatServerAddress) { if (originInputs['WeChatServerAddress'] !== inputs.WeChatServerAddress) {
await updateOption( await updateOption(
'WeChatServerAddress', 'WeChatServerAddress',
removeTrailingSlash(inputs.WeChatServerAddress) removeTrailingSlash(inputs.WeChatServerAddress),
); );
} }
if ( if (
@ -144,7 +144,7 @@ const SystemSetting = () => {
) { ) {
await updateOption( await updateOption(
'WeChatAccountQRCodeImageURL', 'WeChatAccountQRCodeImageURL',
inputs.WeChatAccountQRCodeImageURL inputs.WeChatAccountQRCodeImageURL,
); );
} }
if ( if (
@ -265,7 +265,9 @@ const SystemSetting = () => {
<Divider /> <Divider />
<Header as='h3'> <Header as='h3'>
Configure SMTP Configure SMTP
<Header.Subheader>To support the system email sending</Header.Subheader> <Header.Subheader>
To support the system email sending
</Header.Subheader>
</Header> </Header>
<Form.Group widths={3}> <Form.Group widths={3}>
<Form.Input <Form.Input
@ -318,7 +320,10 @@ const SystemSetting = () => {
Configure Discord OAuth App Configure Discord OAuth App
<Header.Subheader> <Header.Subheader>
To support login & registration via GitHub To support login & registration via GitHub
<a href='https://discord.com/developers/applications' target='_blank'> <a
href='https://discord.com/developers/applications'
target='_blank'
>
Click here Click here
</a> </a>
Manage your Discord OAuth App Manage your Discord OAuth App
@ -441,7 +446,8 @@ const SystemSetting = () => {
<a href='https://dash.cloudflare.com/' target='_blank'> <a href='https://dash.cloudflare.com/' target='_blank'>
Click here Click here
</a> </a>
Manage your Turnstile Sites, recommend selecting Invisible Widget Type Manage your Turnstile Sites, recommend selecting Invisible Widget
Type
</Header.Subheader> </Header.Subheader>
</Header> </Header>
<Form.Group widths={3}> <Form.Group widths={3}>

View File

@ -1,31 +1,66 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Button, Form, Label, Modal, Pagination, Popup, Table } from 'semantic-ui-react'; import {
Button,
Form,
Label,
Modal,
Pagination,
Popup,
Table,
} from 'semantic-ui-react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { API, copy, showError, showSuccess, showWarning, timestamp2string } from '../helpers'; import {
API,
copy,
showError,
showSuccess,
showWarning,
timestamp2string,
} from '../helpers';
import { ITEMS_PER_PAGE } from '../constants'; import { ITEMS_PER_PAGE } from '../constants';
import { renderQuota } from '../helpers/render'; import { renderQuota } from '../helpers/render';
function renderTimestamp(timestamp) { function renderTimestamp(timestamp) {
return ( return <>{timestamp2string(timestamp)}</>;
<>
{timestamp2string(timestamp)}
</>
);
} }
function renderStatus(status) { function renderStatus(status) {
switch (status) { switch (status) {
case 1: case 1:
return <Label basic color='green'>已启用</Label>; return (
<Label basic color='green'>
已启用
</Label>
);
case 2: case 2:
return <Label basic color='red'> 已禁用 </Label>; return (
<Label basic color='red'>
{' '}
已禁用{' '}
</Label>
);
case 3: case 3:
return <Label basic color='yellow'> 已过期 </Label>; return (
<Label basic color='yellow'>
{' '}
已过期{' '}
</Label>
);
case 4: case 4:
return <Label basic color='grey'> 已耗尽 </Label>; return (
<Label basic color='grey'>
{' '}
已耗尽{' '}
</Label>
);
default: default:
return <Label basic color='black'> 未知状态 </Label>; return (
<Label basic color='black'>
{' '}
未知状态{' '}
</Label>
);
} }
} }
@ -68,7 +103,7 @@ const TokensTable = () => {
const refresh = async () => { const refresh = async () => {
setLoading(true); setLoading(true);
await loadTokens(activePage - 1); await loadTokens(activePage - 1);
} };
useEffect(() => { useEffect(() => {
loadTokens(0) loadTokens(0)
@ -221,7 +256,7 @@ const TokensTable = () => {
{tokens {tokens
.slice( .slice(
(activePage - 1) * ITEMS_PER_PAGE, (activePage - 1) * ITEMS_PER_PAGE,
activePage * ITEMS_PER_PAGE activePage * ITEMS_PER_PAGE,
) )
.map((token, idx) => { .map((token, idx) => {
if (token.deleted) return <></>; if (token.deleted) return <></>;
@ -230,20 +265,30 @@ const TokensTable = () => {
<Table.Cell>{token.name ? token.name : '无'}</Table.Cell> <Table.Cell>{token.name ? token.name : '无'}</Table.Cell>
<Table.Cell>{renderStatus(token.status)}</Table.Cell> <Table.Cell>{renderStatus(token.status)}</Table.Cell>
<Table.Cell>{renderQuota(token.used_quota)}</Table.Cell> <Table.Cell>{renderQuota(token.used_quota)}</Table.Cell>
<Table.Cell>{token.unlimited_quota ? '无限制' : renderQuota(token.remain_quota, 2)}</Table.Cell> <Table.Cell>
{token.unlimited_quota
? '无限制'
: renderQuota(token.remain_quota, 2)}
</Table.Cell>
<Table.Cell>{renderTimestamp(token.created_time)}</Table.Cell> <Table.Cell>{renderTimestamp(token.created_time)}</Table.Cell>
<Table.Cell>{token.expired_time === -1 ? '永不过期' : renderTimestamp(token.expired_time)}</Table.Cell> <Table.Cell>
{token.expired_time === -1
? '永不过期'
: renderTimestamp(token.expired_time)}
</Table.Cell>
<Table.Cell> <Table.Cell>
<div> <div>
<Button <Button
size={'small'} size={'small'}
positive positive
onClick={async () => { onClick={async () => {
let key = "sk-" + token.key; let key = 'sk-' + token.key;
if (await copy(key)) { if (await copy(key)) {
showSuccess('已复制到剪贴板!'); showSuccess('已复制到剪贴板!');
} else { } else {
showWarning('无法复制到剪贴板,请手动复制,已将令牌填入搜索框。'); showWarning(
'无法复制到剪贴板,请手动复制,已将令牌填入搜索框。',
);
setSearchKeyword(key); setSearchKeyword(key);
} }
}} }}
@ -275,7 +320,7 @@ const TokensTable = () => {
manageToken( manageToken(
token.id, token.id,
token.status === 1 ? 'disable' : 'enable', token.status === 1 ? 'disable' : 'enable',
idx idx,
); );
}} }}
> >
@ -301,7 +346,9 @@ const TokensTable = () => {
<Button size='small' as={Link} to='/token/add' loading={loading}> <Button size='small' as={Link} to='/token/add' loading={loading}>
添加新的令牌 添加新的令牌
</Button> </Button>
<Button size='small' onClick={refresh} loading={loading}>刷新</Button> <Button size='small' onClick={refresh} loading={loading}>
刷新
</Button>
<Pagination <Pagination
floated='right' floated='right'
activePage={activePage} activePage={activePage}

View File

@ -1,10 +1,22 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Button, Form, Label, Pagination, Popup, Table } from 'semantic-ui-react'; import {
Button,
Form,
Label,
Pagination,
Popup,
Table,
} from 'semantic-ui-react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { API, showError, showSuccess } from '../helpers'; import { API, showError, showSuccess } from '../helpers';
import { ITEMS_PER_PAGE } from '../constants'; import { ITEMS_PER_PAGE } from '../constants';
import { renderGroup, renderNumber, renderQuota, renderText } from '../helpers/render'; import {
renderGroup,
renderNumber,
renderQuota,
renderText,
} from '../helpers/render';
function renderRole(role) { function renderRole(role) {
switch (role) { switch (role) {
@ -65,7 +77,7 @@ const UsersTable = () => {
(async () => { (async () => {
const res = await API.post('/api/user/manage', { const res = await API.post('/api/user/manage', {
username, username,
action action,
}); });
const { success, message } = res.data; const { success, message } = res.data;
if (success) { if (success) {
@ -215,7 +227,7 @@ const UsersTable = () => {
{users {users
.slice( .slice(
(activePage - 1) * ITEMS_PER_PAGE, (activePage - 1) * ITEMS_PER_PAGE,
activePage * ITEMS_PER_PAGE activePage * ITEMS_PER_PAGE,
) )
.map((user, idx) => { .map((user, idx) => {
if (user.deleted) return <></>; if (user.deleted) return <></>;
@ -226,7 +238,9 @@ const UsersTable = () => {
<Popup <Popup
content={user.email ? user.email : '未绑定邮箱地址'} content={user.email ? user.email : '未绑定邮箱地址'}
key={user.username} key={user.username}
header={user.display_name ? user.display_name : user.username} header={
user.display_name ? user.display_name : user.username
}
trigger={<span>{renderText(user.username, 10)}</span>} trigger={<span>{renderText(user.username, 10)}</span>}
hoverable hoverable
/> />
@ -236,9 +250,22 @@ const UsersTable = () => {
{/* {user.email ? <Popup hoverable content={user.email} trigger={<span>{renderText(user.email, 24)}</span>} /> : '无'}*/} {/* {user.email ? <Popup hoverable content={user.email} trigger={<span>{renderText(user.email, 24)}</span>} /> : '无'}*/}
{/*</Table.Cell>*/} {/*</Table.Cell>*/}
<Table.Cell> <Table.Cell>
<Popup content='剩余额度' trigger={<Label basic>{renderQuota(user.quota)}</Label>} /> <Popup
<Popup content='已用额度' trigger={<Label basic>{renderQuota(user.used_quota)}</Label>} /> content='剩余额度'
<Popup content='请求次数' trigger={<Label basic>{renderNumber(user.request_count)}</Label>} /> trigger={<Label basic>{renderQuota(user.quota)}</Label>}
/>
<Popup
content='已用额度'
trigger={
<Label basic>{renderQuota(user.used_quota)}</Label>
}
/>
<Popup
content='请求次数'
trigger={
<Label basic>{renderNumber(user.request_count)}</Label>
}
/>
</Table.Cell> </Table.Cell>
<Table.Cell>{renderRole(user.role)}</Table.Cell> <Table.Cell>{renderRole(user.role)}</Table.Cell>
<Table.Cell>{renderStatus(user.status)}</Table.Cell> <Table.Cell>{renderStatus(user.status)}</Table.Cell>
@ -266,7 +293,11 @@ const UsersTable = () => {
</Button> </Button>
<Popup <Popup
trigger={ trigger={
<Button size='small' negative disabled={user.role === 100}> <Button
size='small'
negative
disabled={user.role === 100}
>
删除 删除
</Button> </Button>
} }
@ -289,7 +320,7 @@ const UsersTable = () => {
manageUser( manageUser(
user.username, user.username,
user.status === 1 ? 'disable' : 'enable', user.status === 1 ? 'disable' : 'enable',
idx idx,
); );
}} }}
disabled={user.role === 100} disabled={user.role === 100}

View File

@ -3,5 +3,5 @@ export const toastConstants = {
INFO_TIMEOUT: 3000, INFO_TIMEOUT: 3000,
ERROR_TIMEOUT: 5000, ERROR_TIMEOUT: 5000,
WARNING_TIMEOUT: 10000, WARNING_TIMEOUT: 10000,
NOTICE_TIMEOUT: 20000 NOTICE_TIMEOUT: 20000,
}; };

View File

@ -1,19 +1,19 @@
export const userConstants = { export const userConstants = {
REGISTER_REQUEST: 'USERS_REGISTER_REQUEST', REGISTER_REQUEST: 'USERS_REGISTER_REQUEST',
REGISTER_SUCCESS: 'USERS_REGISTER_SUCCESS', REGISTER_SUCCESS: 'USERS_REGISTER_SUCCESS',
REGISTER_FAILURE: 'USERS_REGISTER_FAILURE', REGISTER_FAILURE: 'USERS_REGISTER_FAILURE',
LOGIN_REQUEST: 'USERS_LOGIN_REQUEST', LOGIN_REQUEST: 'USERS_LOGIN_REQUEST',
LOGIN_SUCCESS: 'USERS_LOGIN_SUCCESS', LOGIN_SUCCESS: 'USERS_LOGIN_SUCCESS',
LOGIN_FAILURE: 'USERS_LOGIN_FAILURE', LOGIN_FAILURE: 'USERS_LOGIN_FAILURE',
LOGOUT: 'USERS_LOGOUT', LOGOUT: 'USERS_LOGOUT',
GETALL_REQUEST: 'USERS_GETALL_REQUEST', GETALL_REQUEST: 'USERS_GETALL_REQUEST',
GETALL_SUCCESS: 'USERS_GETALL_SUCCESS', GETALL_SUCCESS: 'USERS_GETALL_SUCCESS',
GETALL_FAILURE: 'USERS_GETALL_FAILURE', GETALL_FAILURE: 'USERS_GETALL_FAILURE',
DELETE_REQUEST: 'USERS_DELETE_REQUEST', DELETE_REQUEST: 'USERS_DELETE_REQUEST',
DELETE_SUCCESS: 'USERS_DELETE_SUCCESS', DELETE_SUCCESS: 'USERS_DELETE_SUCCESS',
DELETE_FAILURE: 'USERS_DELETE_FAILURE' DELETE_FAILURE: 'USERS_DELETE_FAILURE',
}; };

View File

@ -1,19 +1,19 @@
// contexts/User/index.jsx // contexts/User/index.jsx
import React from "react" import React from 'react';
import { reducer, initialState } from "./reducer" import { reducer, initialState } from './reducer';
export const UserContext = React.createContext({ export const UserContext = React.createContext({
state: initialState, state: initialState,
dispatch: () => null dispatch: () => null,
}) });
export const UserProvider = ({ children }) => { export const UserProvider = ({ children }) => {
const [state, dispatch] = React.useReducer(reducer, initialState) const [state, dispatch] = React.useReducer(reducer, initialState);
return ( return (
<UserContext.Provider value={[ state, dispatch ]}> <UserContext.Provider value={[state, dispatch]}>
{ children } {children}
</UserContext.Provider> </UserContext.Provider>
) );
} };

View File

@ -3,12 +3,12 @@ export const reducer = (state, action) => {
case 'login': case 'login':
return { return {
...state, ...state,
user: action.payload user: action.payload,
}; };
case 'logout': case 'logout':
return { return {
...state, ...state,
user: undefined user: undefined,
}; };
default: default:
@ -17,5 +17,5 @@ export const reducer = (state, action) => {
}; };
export const initialState = { export const initialState = {
user: undefined user: undefined,
}; };

View File

@ -9,5 +9,5 @@ API.interceptors.response.use(
(response) => response, (response) => response,
(error) => { (error) => {
showError(error); showError(error);
} },
); );

View File

@ -1,10 +1,10 @@
export function authHeader() { export function authHeader() {
// return authorization header with jwt token // return authorization header with jwt token
let user = JSON.parse(localStorage.getItem('user')); let user = JSON.parse(localStorage.getItem('user'));
if (user && user.token) { if (user && user.token) {
return { 'Authorization': 'Bearer ' + user.token }; return { Authorization: 'Bearer ' + user.token };
} else { } else {
return {}; return {};
} }
} }

View File

@ -13,16 +13,18 @@ export function renderGroup(group) {
} }
let groups = group.split(','); let groups = group.split(',');
groups.sort(); groups.sort();
return <> return (
{groups.map((group) => { <>
if (group === 'vip' || group === 'pro') { {groups.map((group) => {
return <Label color='yellow'>{group}</Label>; if (group === 'vip' || group === 'pro') {
} else if (group === 'svip' || group === 'premium') { return <Label color='yellow'>{group}</Label>;
return <Label color='red'>{group}</Label>; } else if (group === 'svip' || group === 'premium') {
} return <Label color='red'>{group}</Label>;
return <Label>{group}</Label>; }
})} return <Label>{group}</Label>;
</>; })}
</>
);
} }
export function renderNumber(num) { export function renderNumber(num) {

View File

@ -24,7 +24,7 @@ export function getSystemName() {
export function getLogo() { export function getLogo() {
let logo = localStorage.getItem('logo'); let logo = localStorage.getItem('logo');
if (!logo) return '/logo.png'; if (!logo) return '/logo.png';
return logo return logo;
} }
export function getFooterHTML() { export function getFooterHTML() {
@ -147,17 +147,7 @@ export function timestamp2string(timestamp) {
second = '0' + second; second = '0' + second;
} }
return ( return (
year + year + '-' + month + '-' + day + ' ' + hour + ':' + minute + ':' + second
'-' +
month +
'-' +
day +
' ' +
hour +
':' +
minute +
':' +
second
); );
} }

View File

@ -1,35 +1,37 @@
body { body {
margin: 0; margin: 0;
padding-top: 55px; padding-top: 55px;
overflow-y: scroll; overflow-y: scroll;
font-family: Lato, 'Helvetica Neue', Arial, Helvetica, "Microsoft YaHei", sans-serif; font-family: Lato, 'Helvetica Neue', Arial, Helvetica, 'Microsoft YaHei',
-webkit-font-smoothing: antialiased; sans-serif;
-moz-osx-font-smoothing: grayscale; -webkit-font-smoothing: antialiased;
scrollbar-width: none; -moz-osx-font-smoothing: grayscale;
scrollbar-width: none;
} }
body::-webkit-scrollbar { body::-webkit-scrollbar {
display: none; display: none;
} }
code { code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace; font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
} }
.main-content { .main-content {
padding: 4px; padding: 4px;
} }
.small-icon .icon { .small-icon .icon {
font-size: 1em !important; font-size: 1em !important;
} }
.custom-footer { .custom-footer {
font-size: 1.1em; font-size: 1.1em;
} }
@media only screen and (max-width: 600px) { @media only screen and (max-width: 600px) {
.hide-on-mobile { .hide-on-mobile {
display: none !important; display: none !important;
} }
} }

View File

@ -27,5 +27,5 @@ root.render(
</BrowserRouter> </BrowserRouter>
</UserProvider> </UserProvider>
</StatusProvider> </StatusProvider>
</React.StrictMode> </React.StrictMode>,
); );

View File

@ -31,8 +31,8 @@ const About = () => {
return ( return (
<> <>
{ {aboutLoaded && about === '' ? (
aboutLoaded && about === '' ? <> <>
<Segment> <Segment>
<Header as='h3'>关于</Header> <Header as='h3'>关于</Header>
<p>可在设置页面设置关于内容支持 HTML & Markdown</p> <p>可在设置页面设置关于内容支持 HTML & Markdown</p>
@ -41,20 +41,26 @@ const About = () => {
https://github.com/songquanpeng/one-api https://github.com/songquanpeng/one-api
</a> </a>
</Segment> </Segment>
</> : <> </>
{ ) : (
about.startsWith('https://') ? <iframe <>
{about.startsWith('https://') ? (
<iframe
src={about} src={about}
style={{ width: '100%', height: '100vh', border: 'none' }} style={{ width: '100%', height: '100vh', border: 'none' }}
/> : <Segment> />
<div style={{ fontSize: 'larger' }} dangerouslySetInnerHTML={{ __html: about }}></div> ) : (
<Segment>
<div
style={{ fontSize: 'larger' }}
dangerouslySetInnerHTML={{ __html: about }}
></div>
</Segment> </Segment>
} )}
</> </>
} )}
</> </>
); );
}; };
export default About; export default About;

View File

@ -1,13 +1,26 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Button, Form, Header, Input, Message, Segment } from 'semantic-ui-react'; import {
Button,
Form,
Header,
Input,
Message,
Segment,
} from 'semantic-ui-react';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { API, showError, showInfo, showSuccess, verifyJSON } from '../../helpers'; import {
API,
showError,
showInfo,
showSuccess,
verifyJSON,
} from '../../helpers';
import { CHANNEL_OPTIONS } from '../../constants'; import { CHANNEL_OPTIONS } from '../../constants';
const MODEL_MAPPING_EXAMPLE = { const MODEL_MAPPING_EXAMPLE = {
'gpt-3.5-turbo-0301': 'gpt-3.5-turbo', 'gpt-3.5-turbo-0301': 'gpt-3.5-turbo',
'gpt-4-0314': 'gpt-4', 'gpt-4-0314': 'gpt-4',
'gpt-4-32k-0314': 'gpt-4-32k' 'gpt-4-32k-0314': 'gpt-4-32k',
}; };
const EditChannel = () => { const EditChannel = () => {
@ -34,7 +47,7 @@ const EditChannel = () => {
const [fullModels, setFullModels] = useState([]); const [fullModels, setFullModels] = useState([]);
const [customModel, setCustomModel] = useState(''); const [customModel, setCustomModel] = useState('');
const handleInputChange = (e, { name, value }) => { const handleInputChange = (e, { name, value }) => {
console.log(name, value) console.log(name, value);
setInputs((inputs) => ({ ...inputs, [name]: value })); setInputs((inputs) => ({ ...inputs, [name]: value }));
}; };
@ -53,7 +66,7 @@ const EditChannel = () => {
localModelOptions.push({ localModelOptions.push({
key: model, key: model,
text: model, text: model,
value: model value: model,
}); });
} }
}); });
@ -66,7 +79,11 @@ const EditChannel = () => {
data.groups = data.group.split(','); data.groups = data.group.split(',');
} }
if (data.model_mapping !== '') { if (data.model_mapping !== '') {
data.model_mapping = JSON.stringify(JSON.parse(data.model_mapping), null, 2); data.model_mapping = JSON.stringify(
JSON.parse(data.model_mapping),
null,
2,
);
} }
setInputs(data); setInputs(data);
} else { } else {
@ -78,13 +95,19 @@ const EditChannel = () => {
const fetchModels = async () => { const fetchModels = async () => {
try { try {
let res = await API.get(`/api/channel/models`); let res = await API.get(`/api/channel/models`);
setModelOptions(res.data.data.map((model) => ({ setModelOptions(
key: model.id, res.data.data.map((model) => ({
text: model.id, key: model.id,
value: model.id text: model.id,
}))); value: model.id,
})),
);
setFullModels(res.data.data.map((model) => model.id)); setFullModels(res.data.data.map((model) => model.id));
setBasicModels(res.data.data.filter((model) => !model.id.startsWith('gpt-4')).map((model) => model.id)); setBasicModels(
res.data.data
.filter((model) => !model.id.startsWith('gpt-4'))
.map((model) => model.id),
);
} catch (error) { } catch (error) {
showError(error.message); showError(error.message);
} }
@ -93,11 +116,13 @@ const EditChannel = () => {
const fetchGroups = async () => { const fetchGroups = async () => {
try { try {
let res = await API.get(`/api/group/`); let res = await API.get(`/api/group/`);
setGroupOptions(res.data.data.map((group) => ({ setGroupOptions(
key: group, res.data.data.map((group) => ({
text: group, key: group,
value: group text: group,
}))); value: group,
})),
);
} catch (error) { } catch (error) {
showError(error.message); showError(error.message);
} }
@ -126,7 +151,10 @@ const EditChannel = () => {
} }
let localInputs = inputs; let localInputs = inputs;
if (localInputs.base_url.endsWith('/')) { if (localInputs.base_url.endsWith('/')) {
localInputs.base_url = localInputs.base_url.slice(0, localInputs.base_url.length - 1); localInputs.base_url = localInputs.base_url.slice(
0,
localInputs.base_url.length - 1,
);
} }
if (localInputs.type === 3 && localInputs.other === '') { if (localInputs.type === 3 && localInputs.other === '') {
localInputs.other = '2023-03-15-preview'; localInputs.other = '2023-03-15-preview';
@ -135,7 +163,10 @@ const EditChannel = () => {
localInputs.models = localInputs.models.join(','); localInputs.models = localInputs.models.join(',');
localInputs.group = localInputs.groups.join(','); localInputs.group = localInputs.groups.join(',');
if (isEdit) { if (isEdit) {
res = await API.put(`/api/channel/`, { ...localInputs, id: parseInt(channelId) }); res = await API.put(`/api/channel/`, {
...localInputs,
id: parseInt(channelId),
});
} else { } else {
res = await API.post(`/api/channel/`, localInputs); res = await API.post(`/api/channel/`, localInputs);
} }
@ -167,65 +198,74 @@ const EditChannel = () => {
onChange={handleInputChange} onChange={handleInputChange}
/> />
</Form.Field> </Form.Field>
{ {inputs.type === 3 && (
inputs.type === 3 && ( <>
<> <Message>
<Message> 注意<strong>模型部署名称必须和模型名称保持一致</strong>
注意<strong>模型部署名称必须和模型名称保持一致</strong> One API model One API 会把请求体中的 model
参数替换为你的部署名称模型名称中的点会被剔除<a target='_blank' 参数替换为你的部署名称模型名称中的点会被剔除
href='https://github.com/songquanpeng/one-api/issues/133?notification_referrer_id=NT_kwDOAmJSYrM2NjIwMzI3NDgyOjM5OTk4MDUw#issuecomment-1571602271'>图片演示</a> <a
</Message> target='_blank'
<Form.Field> href='https://github.com/songquanpeng/one-api/issues/133?notification_referrer_id=NT_kwDOAmJSYrM2NjIwMzI3NDgyOjM5OTk4MDUw#issuecomment-1571602271'
<Form.Input >
label='AZURE_OPENAI_ENDPOINT' 图片演示
name='base_url' </a>
placeholder={'请输入 AZURE_OPENAI_ENDPOINT例如https://docs-test-001.openai.azure.com'}
onChange={handleInputChange} </Message>
value={inputs.base_url}
autoComplete='new-password'
/>
</Form.Field>
<Form.Field>
<Form.Input
label='默认 API 版本'
name='other'
placeholder={'请输入默认 API 版本例如2023-03-15-preview该配置可以被实际的请求查询参数所覆盖'}
onChange={handleInputChange}
value={inputs.other}
autoComplete='new-password'
/>
</Form.Field>
</>
)
}
{
inputs.type === 8 && (
<Form.Field> <Form.Field>
<Form.Input <Form.Input
label='Base URL' label='AZURE_OPENAI_ENDPOINT'
name='base_url' name='base_url'
placeholder={'请输入自定义渠道的 Base URL例如https://openai.justsong.cn'} placeholder={
'请输入 AZURE_OPENAI_ENDPOINT例如https://docs-test-001.openai.azure.com'
}
onChange={handleInputChange} onChange={handleInputChange}
value={inputs.base_url} value={inputs.base_url}
autoComplete='new-password' autoComplete='new-password'
/> />
</Form.Field> </Form.Field>
)
}
{
inputs.type !== 3 && inputs.type !== 8 && (
<Form.Field> <Form.Field>
<Form.Input <Form.Input
label='镜像' label='默认 API 版本'
name='base_url' name='other'
placeholder={'此项可选输入镜像站地址格式为https://domain.com'} placeholder={
'请输入默认 API 版本例如2023-03-15-preview该配置可以被实际的请求查询参数所覆盖'
}
onChange={handleInputChange} onChange={handleInputChange}
value={inputs.base_url} value={inputs.other}
autoComplete='new-password' autoComplete='new-password'
/> />
</Form.Field> </Form.Field>
) </>
} )}
{inputs.type === 8 && (
<Form.Field>
<Form.Input
label='Base URL'
name='base_url'
placeholder={
'请输入自定义渠道的 Base URL例如https://openai.justsong.cn'
}
onChange={handleInputChange}
value={inputs.base_url}
autoComplete='new-password'
/>
</Form.Field>
)}
{inputs.type !== 3 && inputs.type !== 8 && (
<Form.Field>
<Form.Input
label='镜像'
name='base_url'
placeholder={
'此项可选输入镜像站地址格式为https://domain.com'
}
onChange={handleInputChange}
value={inputs.base_url}
autoComplete='new-password'
/>
</Form.Field>
)}
<Form.Field> <Form.Field>
<Form.Input <Form.Input
label='名称' label='名称'
@ -270,29 +310,52 @@ const EditChannel = () => {
/> />
</Form.Field> </Form.Field>
<div style={{ lineHeight: '40px', marginBottom: '12px' }}> <div style={{ lineHeight: '40px', marginBottom: '12px' }}>
<Button type={'button'} onClick={() => { <Button
handleInputChange(null, { name: 'models', value: basicModels }); type={'button'}
}}>填入基础模型</Button> onClick={() => {
<Button type={'button'} onClick={() => { handleInputChange(null, { name: 'models', value: basicModels });
handleInputChange(null, { name: 'models', value: fullModels }); }}
}}>填入所有模型</Button> >
<Button type={'button'} onClick={() => { 填入基础模型
handleInputChange(null, { name: 'models', value: [] }); </Button>
}}>清除所有模型</Button> <Button
type={'button'}
onClick={() => {
handleInputChange(null, { name: 'models', value: fullModels });
}}
>
填入所有模型
</Button>
<Button
type={'button'}
onClick={() => {
handleInputChange(null, { name: 'models', value: [] });
}}
>
清除所有模型
</Button>
<Input <Input
action={ action={
<Button type={'button'} onClick={()=>{ <Button
let localModels = [...inputs.models]; type={'button'}
localModels.push(customModel); onClick={() => {
let localModelOptions = [...modelOptions]; let localModels = [...inputs.models];
localModelOptions.push({ localModels.push(customModel);
key: customModel, let localModelOptions = [...modelOptions];
text: customModel, localModelOptions.push({
value: customModel, key: customModel,
}); text: customModel,
setModelOptions(localModelOptions); value: customModel,
handleInputChange(null, { name: 'models', value: localModels }); });
}}>填入</Button> setModelOptions(localModelOptions);
handleInputChange(null, {
name: 'models',
value: localModels,
});
}}
>
填入
</Button>
} }
placeholder='输入自定义模型名称' placeholder='输入自定义模型名称'
value={customModel} value={customModel}
@ -315,7 +378,11 @@ const EditChannel = () => {
<Form.Field> <Form.Field>
<Form.TextArea <Form.TextArea
label='模型映射' label='模型映射'
placeholder={`此项可选,为一个 JSON 文本,键为用户请求的模型名称,值为要替换的模型名称,例如:\n${JSON.stringify(MODEL_MAPPING_EXAMPLE, null, 2)}`} placeholder={`此项可选,为一个 JSON 文本,键为用户请求的模型名称,值为要替换的模型名称,例如:\n${JSON.stringify(
MODEL_MAPPING_EXAMPLE,
null,
2,
)}`}
name='model_mapping' name='model_mapping'
onChange={handleInputChange} onChange={handleInputChange}
value={inputs.model_mapping} value={inputs.model_mapping}
@ -323,18 +390,23 @@ const EditChannel = () => {
autoComplete='new-password' autoComplete='new-password'
/> />
</Form.Field> </Form.Field>
{ {batch ? (
batch ? <Form.Field> <Form.Field>
<Form.TextArea <Form.TextArea
label='密钥' label='密钥'
name='key' name='key'
placeholder={'请输入密钥,一行一个'} placeholder={'请输入密钥,一行一个'}
onChange={handleInputChange} onChange={handleInputChange}
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'
/> />
</Form.Field> : <Form.Field> </Form.Field>
) : (
<Form.Field>
<Form.Input <Form.Input
label='密钥' label='密钥'
name='key' name='key'
@ -345,18 +417,18 @@ const EditChannel = () => {
autoComplete='new-password' autoComplete='new-password'
/> />
</Form.Field> </Form.Field>
} )}
{ {!isEdit && (
!isEdit && ( <Form.Checkbox
<Form.Checkbox checked={batch}
checked={batch} label='批量创建'
label='批量创建' name='batch'
name='batch' onChange={() => setBatch(!batch)}
onChange={() => setBatch(!batch)} />
/> )}
) <Button type={isEdit ? 'button' : 'submit'} positive onClick={submit}>
} 提交
<Button type={isEdit ? "button" : "submit"} positive onClick={submit}>提交</Button> </Button>
</Form> </Form>
</Segment> </Segment>
</> </>

View File

@ -11,5 +11,4 @@ const Chat = () => {
); );
}; };
export default Chat; export default Chat;

View File

@ -52,8 +52,8 @@ const Home = () => {
}, []); }, []);
return ( return (
<> <>
{ {homePageContentLoaded && homePageContent === '' ? (
homePageContentLoaded && homePageContent === '' ? <> <>
<Segment> <Segment>
<Header as='h3'>系统状况</Header> <Header as='h3'>系统状况</Header>
<Grid columns={2} stackable> <Grid columns={2} stackable>
@ -121,16 +121,22 @@ const Home = () => {
</Grid.Column> </Grid.Column>
</Grid> </Grid>
</Segment> </Segment>
</> : <> </>
{ ) : (
homePageContent.startsWith('https://') ? <iframe <>
{homePageContent.startsWith('https://') ? (
<iframe
src={homePageContent} src={homePageContent}
style={{ width: '100%', height: '100vh', border: 'none' }} style={{ width: '100%', height: '100vh', border: 'none' }}
/> : <div style={{ fontSize: 'larger' }} dangerouslySetInnerHTML={{ __html: homePageContent }}></div> />
} ) : (
<div
style={{ fontSize: 'larger' }}
dangerouslySetInnerHTML={{ __html: homePageContent }}
></div>
)}
</> </>
} )}
</> </>
); );
}; };

View File

@ -5,15 +5,13 @@ const NotFound = () => (
<> <>
<Header <Header
block block
as="h4" as='h4'
content="404" content='404'
attached="top" attached='top'
icon="info" icon='info'
className="small-icon" className='small-icon'
/> />
<Segment attached="bottom"> <Segment attached='bottom'>未找到所请求的页面</Segment>
未找到所请求的页面
</Segment>
</> </>
); );

View File

@ -12,7 +12,7 @@ const EditRedemption = () => {
const originInputs = { const originInputs = {
name: '', name: '',
quota: 100000, quota: 100000,
count: 1 count: 1,
}; };
const [inputs, setInputs] = useState(originInputs); const [inputs, setInputs] = useState(originInputs);
const { name, quota, count } = inputs; const { name, quota, count } = inputs;
@ -44,10 +44,13 @@ const EditRedemption = () => {
localInputs.quota = parseInt(localInputs.quota); localInputs.quota = parseInt(localInputs.quota);
let res; let res;
if (isEdit) { if (isEdit) {
res = await API.put(`/api/redemption/`, { ...localInputs, id: parseInt(redemptionId) }); res = await API.put(`/api/redemption/`, {
...localInputs,
id: parseInt(redemptionId),
});
} else { } else {
res = await API.post(`/api/redemption/`, { res = await API.post(`/api/redemption/`, {
...localInputs ...localInputs,
}); });
} }
const { success, message, data } = res.data; const { success, message, data } = res.data;
@ -62,9 +65,9 @@ const EditRedemption = () => {
showError(message); showError(message);
} }
if (!isEdit && data) { if (!isEdit && data) {
let text = ""; let text = '';
for (let i = 0; i < data.length; i++) { for (let i = 0; i < data.length; i++) {
text += data[i] + "\n"; text += data[i] + '\n';
} }
downloadTextAsFile(text, `${inputs.name}.txt`); downloadTextAsFile(text, `${inputs.name}.txt`);
} }
@ -97,8 +100,8 @@ const EditRedemption = () => {
type='number' type='number'
/> />
</Form.Field> </Form.Field>
{ {!isEdit && (
!isEdit && <> <>
<Form.Field> <Form.Field>
<Form.Input <Form.Input
label='生成数量' label='生成数量'
@ -111,8 +114,10 @@ const EditRedemption = () => {
/> />
</Form.Field> </Form.Field>
</> </>
} )}
<Button positive onClick={submit}>提交</Button> <Button positive onClick={submit}>
提交
</Button>
</Form> </Form>
</Segment> </Segment>
</> </>

View File

@ -6,7 +6,7 @@ const Redemption = () => (
<> <>
<Segment> <Segment>
<Header as='h3'>管理兑换码</Header> <Header as='h3'>管理兑换码</Header>
<RedemptionsTable/> <RedemptionsTable />
</Segment> </Segment>
</> </>
); );

View File

@ -14,8 +14,8 @@ const Setting = () => {
<Tab.Pane attached={false}> <Tab.Pane attached={false}>
<PersonalSetting /> <PersonalSetting />
</Tab.Pane> </Tab.Pane>
) ),
} },
]; ];
if (isRoot()) { if (isRoot()) {
@ -25,7 +25,7 @@ const Setting = () => {
<Tab.Pane attached={false}> <Tab.Pane attached={false}>
<OperationSetting /> <OperationSetting />
</Tab.Pane> </Tab.Pane>
) ),
}); });
panes.push({ panes.push({
menuItem: '系统设置', menuItem: '系统设置',
@ -33,7 +33,7 @@ const Setting = () => {
<Tab.Pane attached={false}> <Tab.Pane attached={false}>
<SystemSetting /> <SystemSetting />
</Tab.Pane> </Tab.Pane>
) ),
}); });
panes.push({ panes.push({
menuItem: '其他设置', menuItem: '其他设置',
@ -41,7 +41,7 @@ const Setting = () => {
<Tab.Pane attached={false}> <Tab.Pane attached={false}>
<OtherSetting /> <OtherSetting />
</Tab.Pane> </Tab.Pane>
) ),
}); });
} }

View File

@ -14,7 +14,15 @@ const EditToken = () => {
remain_quota: isEdit ? 0 : 500000, remain_quota: isEdit ? 0 : 500000,
expired_time: -1, expired_time: -1,
unlimited_quota: false, unlimited_quota: false,
models: isEdit ? [] : ['gpt-3.5-turbo', 'gpt-3.5-turbo-0301', 'gpt-3.5-turbo-0613', 'gpt-3.5-turbo-16k', 'gpt-3.5-turbo-16k-0613'] models: isEdit
? []
: [
'gpt-3.5-turbo',
'gpt-3.5-turbo-0301',
'gpt-3.5-turbo-0613',
'gpt-3.5-turbo-16k',
'gpt-3.5-turbo-16k-0613',
],
}; };
const [modelOptions, setModelOptions] = useState([]); const [modelOptions, setModelOptions] = useState([]);
const [basicModels, setBasicModels] = useState([]); const [basicModels, setBasicModels] = useState([]);
@ -48,13 +56,19 @@ const EditToken = () => {
const fetchModels = async () => { const fetchModels = async () => {
try { try {
let res = await API.get(`/api/channel/models`); let res = await API.get(`/api/channel/models`);
setModelOptions(res.data.data.map((model) => ({ setModelOptions(
key: model.id, res.data.data.map((model) => ({
text: model.id, key: model.id,
value: model.id text: model.id,
}))); value: model.id,
})),
);
setFullModels(res.data.data.map((model) => model.id)); setFullModels(res.data.data.map((model) => model.id));
setBasicModels(res.data.data.filter((model) => !model.id.startsWith('gpt-4')).map((model) => model.id)); setBasicModels(
res.data.data
.filter((model) => !model.id.startsWith('gpt-4'))
.map((model) => model.id),
);
} catch (error) { } catch (error) {
showError(error.message); showError(error.message);
} }
@ -103,7 +117,10 @@ const EditToken = () => {
localInputs.models = localInputs.models.join(','); localInputs.models = localInputs.models.join(',');
let res; let res;
if (isEdit) { if (isEdit) {
res = await API.put(`/api/token/`, { ...localInputs, id: parseInt(tokenId) }); res = await API.put(`/api/token/`, {
...localInputs,
id: parseInt(tokenId),
});
} else { } else {
res = await API.post(`/api/token/`, localInputs); res = await API.post(`/api/token/`, localInputs);
} }
@ -144,7 +161,9 @@ const EditToken = () => {
<Form.Input <Form.Input
label='过期时间' label='过期时间'
name='expired_time' name='expired_time'
placeholder={'请输入过期时间,格式为 yyyy-MM-dd HH:mm:ss-1 表示无限制'} placeholder={
'请输入过期时间,格式为 yyyy-MM-dd HH:mm:ss-1 表示无限制'
}
onChange={handleInputChange} onChange={handleInputChange}
value={expired_time} value={expired_time}
autoComplete='new-password' autoComplete='new-password'
@ -152,23 +171,50 @@ const EditToken = () => {
/> />
</Form.Field> </Form.Field>
<div style={{ lineHeight: '40px' }}> <div style={{ lineHeight: '40px' }}>
<Button type={'button'} onClick={() => { <Button
setExpiredTime(0, 0, 0, 0); type={'button'}
}}>永不过期</Button> onClick={() => {
<Button type={'button'} onClick={() => { setExpiredTime(0, 0, 0, 0);
setExpiredTime(1, 0, 0, 0); }}
}}>一个月后过期</Button> >
<Button type={'button'} onClick={() => { 永不过期
setExpiredTime(0, 1, 0, 0); </Button>
}}>一天后过期</Button> <Button
<Button type={'button'} onClick={() => { type={'button'}
setExpiredTime(0, 0, 1, 0); onClick={() => {
}}>一小时后过期</Button> setExpiredTime(1, 0, 0, 0);
<Button type={'button'} onClick={() => { }}
setExpiredTime(0, 0, 0, 1); >
}}>一分钟后过期</Button> 一个月后过期
</Button>
<Button
type={'button'}
onClick={() => {
setExpiredTime(0, 1, 0, 0);
}}
>
一天后过期
</Button>
<Button
type={'button'}
onClick={() => {
setExpiredTime(0, 0, 1, 0);
}}
>
一小时后过期
</Button>
<Button
type={'button'}
onClick={() => {
setExpiredTime(0, 0, 0, 1);
}}
>
一分钟后过期
</Button>
</div> </div>
<Message>注意令牌的额度仅用于限制令牌本身的最大额度使用量实际的使用受到账户的剩余额度限制</Message> <Message>
注意令牌的额度仅用于限制令牌本身的最大额度使用量实际的使用受到账户的剩余额度限制
</Message>
<Form.Field> <Form.Field>
<Form.Input <Form.Input
label={`额度${renderQuotaWithPrompt(remain_quota)}`} label={`额度${renderQuotaWithPrompt(remain_quota)}`}
@ -181,9 +227,14 @@ const EditToken = () => {
disabled={unlimited_quota} disabled={unlimited_quota}
/> />
</Form.Field> </Form.Field>
<Button type={'button'} onClick={() => { <Button
setUnlimitedQuota(); type={'button'}
}}>{unlimited_quota ? '取消无限额度' : '设置为无限额度'}</Button> onClick={() => {
setUnlimitedQuota();
}}
>
{unlimited_quota ? '取消无限额度' : '设置为无限额度'}
</Button>
<Form.Field style={{ marginTop: '12px' }}> <Form.Field style={{ marginTop: '12px' }}>
<Form.Dropdown <Form.Dropdown
label='模型' label='模型'
@ -200,17 +251,34 @@ const EditToken = () => {
/> />
</Form.Field> </Form.Field>
<div style={{ lineHeight: '40px', marginBottom: '12px' }}> <div style={{ lineHeight: '40px', marginBottom: '12px' }}>
<Button type={'button'} onClick={() => { <Button
handleInputChange(null, { name: 'models', value: basicModels }); type={'button'}
}}>填入基础模型</Button> onClick={() => {
<Button type={'button'} onClick={() => { handleInputChange(null, { name: 'models', value: basicModels });
handleInputChange(null, { name: 'models', value: fullModels }); }}
}}>填入所有模型</Button> >
<Button type={'button'} onClick={() => { 填入基础模型
handleInputChange(null, { name: 'models', value: [] }); </Button>
}}>清除所有模型</Button> <Button
type={'button'}
onClick={() => {
handleInputChange(null, { name: 'models', value: fullModels });
}}
>
填入所有模型
</Button>
<Button
type={'button'}
onClick={() => {
handleInputChange(null, { name: 'models', value: [] });
}}
>
清除所有模型
</Button>
</div> </div>
<Button positive onClick={submit}>提交</Button> <Button positive onClick={submit}>
提交
</Button>
</Form> </Form>
</Segment> </Segment>
</> </>

View File

@ -6,7 +6,7 @@ const Token = () => (
<> <>
<Segment> <Segment>
<Header as='h3'>我的令牌</Header> <Header as='h3'>我的令牌</Header>
<TokensTable/> <TokensTable />
</Segment> </Segment>
</> </>
); );

View File

@ -1,5 +1,12 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Button, Form, Grid, Header, Segment, Statistic } from 'semantic-ui-react'; import {
Button,
Form,
Grid,
Header,
Segment,
Statistic,
} from 'semantic-ui-react';
import { API, showError, showInfo, showSuccess } from '../../helpers'; import { API, showError, showInfo, showSuccess } from '../../helpers';
import { renderQuota } from '../../helpers/render'; import { renderQuota } from '../../helpers/render';
@ -10,11 +17,11 @@ const TopUp = () => {
const topUp = async () => { const topUp = async () => {
if (redemptionCode === '') { if (redemptionCode === '') {
showInfo('请输入充值码!') showInfo('请输入充值码!');
return; return;
} }
const res = await API.post('/api/user/topup', { const res = await API.post('/api/user/topup', {
key: redemptionCode key: redemptionCode,
}); });
const { success, message, data } = res.data; const { success, message, data } = res.data;
if (success) { if (success) {
@ -36,15 +43,15 @@ const TopUp = () => {
window.open(topUpLink, '_blank'); window.open(topUpLink, '_blank');
}; };
const getUserQuota = async ()=>{ const getUserQuota = 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) {
setUserQuota(data.quota); setUserQuota(data.quota);
} else { } else {
showError(message); showError(message);
} }
} };
useEffect(() => { useEffect(() => {
let status = localStorage.getItem('status'); let status = localStorage.getItem('status');
@ -92,5 +99,4 @@ const TopUp = () => {
); );
}; };
export default TopUp; export default TopUp;

View File

@ -30,38 +30,38 @@ const AddUser = () => {
return ( return (
<> <>
<Segment> <Segment>
<Header as="h3">创建新用户账户</Header> <Header as='h3'>创建新用户账户</Header>
<Form autoComplete="off"> <Form autoComplete='off'>
<Form.Field> <Form.Field>
<Form.Input <Form.Input
label="用户名" label='用户名'
name="username" name='username'
placeholder={'请输入用户名'} placeholder={'请输入用户名'}
onChange={handleInputChange} onChange={handleInputChange}
value={username} value={username}
autoComplete="off" autoComplete='off'
required required
/> />
</Form.Field> </Form.Field>
<Form.Field> <Form.Field>
<Form.Input <Form.Input
label="显示名称" label='显示名称'
name="display_name" name='display_name'
placeholder={'请输入显示名称'} placeholder={'请输入显示名称'}
onChange={handleInputChange} onChange={handleInputChange}
value={display_name} value={display_name}
autoComplete="off" autoComplete='off'
/> />
</Form.Field> </Form.Field>
<Form.Field> <Form.Field>
<Form.Input <Form.Input
label="密码" label='密码'
name="password" name='password'
type={'password'} type={'password'}
placeholder={'请输入密码'} placeholder={'请输入密码'}
onChange={handleInputChange} onChange={handleInputChange}
value={password} value={password}
autoComplete="off" autoComplete='off'
required required
/> />
</Form.Field> </Form.Field>

View File

@ -17,22 +17,32 @@ const EditUser = () => {
wechat_id: '', wechat_id: '',
email: '', email: '',
quota: 0, quota: 0,
group: 'default' group: 'default',
}); });
const [groupOptions, setGroupOptions] = useState([]); const [groupOptions, setGroupOptions] = useState([]);
const { username, display_name, password, github_id, wechat_id, email, quota, discord_id } = const {
inputs; username,
display_name,
password,
github_id,
wechat_id,
email,
quota,
discord_id,
} = inputs;
const handleInputChange = (e, { name, value }) => { const handleInputChange = (e, { name, value }) => {
setInputs((inputs) => ({ ...inputs, [name]: value })); setInputs((inputs) => ({ ...inputs, [name]: value }));
}; };
const fetchGroups = async () => { const fetchGroups = async () => {
try { try {
let res = await API.get(`/api/group/`); let res = await API.get(`/api/group/`);
setGroupOptions(res.data.data.map((group) => ({ setGroupOptions(
key: group, res.data.data.map((group) => ({
text: group, key: group,
value: group, text: group,
}))); value: group,
})),
);
} catch (error) { } catch (error) {
showError(error.message); showError(error.message);
} }
@ -116,8 +126,8 @@ const EditUser = () => {
autoComplete='new-password' autoComplete='new-password'
/> />
</Form.Field> </Form.Field>
{ {userId && (
userId && <> <>
<Form.Field> <Form.Field>
<Form.Dropdown <Form.Dropdown
label='分组' label='分组'
@ -146,7 +156,7 @@ const EditUser = () => {
/> />
</Form.Field> </Form.Field>
</> </>
} )}
<Form.Field> <Form.Field>
<Form.Input <Form.Input
label='已绑定的 GitHub 账户' label='已绑定的 GitHub 账户'
@ -187,7 +197,9 @@ const EditUser = () => {
readOnly readOnly
/> />
</Form.Field> </Form.Field>
<Button positive onClick={submit}>提交</Button> <Button positive onClick={submit}>
提交
</Button>
</Form> </Form>
</Segment> </Segment>
</> </>

View File

@ -6,7 +6,7 @@ const User = () => (
<> <>
<Segment> <Segment>
<Header as='h3'>管理用户</Header> <Header as='h3'>管理用户</Header>
<UsersTable/> <UsersTable />
</Segment> </Segment>
</> </>
); );