diff --git a/Dockerfile b/Dockerfile index ffb8c21..5ca85d8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,7 +5,7 @@ COPY web/package.json . RUN npm install COPY ./web . COPY ./VERSION . -RUN DISABLE_ESLINT_PLUGIN='true' REACT_APP_VERSION=$(cat VERSION) npm run build +RUN DISABLE_ESLINT_PLUGIN='true' VITE_REACT_APP_VERSION=$(cat VERSION) npm run build FROM golang AS builder2 diff --git a/main.go b/main.go index 234bca7..37c6a0a 100644 --- a/main.go +++ b/main.go @@ -20,10 +20,10 @@ import ( _ "net/http/pprof" ) -//go:embed web/build +//go:embed web/dist var buildFS embed.FS -//go:embed web/build/index.html +//go:embed web/dist/index.html var indexPage []byte func main() { diff --git a/router/api-router.go b/router/api-router.go index 592e8ed..8547454 100644 --- a/router/api-router.go +++ b/router/api-router.go @@ -17,7 +17,7 @@ func SetApiRouter(router *gin.Engine) { apiRouter.GET("/status/test", middleware.AdminAuth(), controller.TestStatus) apiRouter.GET("/notice", controller.GetNotice) apiRouter.GET("/about", controller.GetAbout) - apiRouter.GET("/midjourney", controller.GetMidjourney) + //apiRouter.GET("/midjourney", controller.GetMidjourney) apiRouter.GET("/home_page_content", controller.GetHomePageContent) apiRouter.GET("/verification", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.SendEmailVerification) apiRouter.GET("/reset_password", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.SendPasswordResetEmail) diff --git a/router/web-router.go b/router/web-router.go index 8f9c18a..57cd61a 100644 --- a/router/web-router.go +++ b/router/web-router.go @@ -16,9 +16,9 @@ func SetWebRouter(router *gin.Engine, buildFS embed.FS, indexPage []byte) { router.Use(gzip.Gzip(gzip.DefaultCompression)) router.Use(middleware.GlobalWebRateLimit()) router.Use(middleware.Cache()) - router.Use(static.Serve("/", common.EmbedFolder(buildFS, "web/build"))) + router.Use(static.Serve("/", common.EmbedFolder(buildFS, "web/dist"))) router.NoRoute(func(c *gin.Context) { - if strings.HasPrefix(c.Request.RequestURI, "/v1") || strings.HasPrefix(c.Request.RequestURI, "/api") { + if strings.HasPrefix(c.Request.RequestURI, "/v1") || strings.HasPrefix(c.Request.RequestURI, "/api") || strings.HasPrefix(c.Request.RequestURI, "/assets") { controller.RelayNotFound(c) return } diff --git a/web/.prettierrc.mjs b/web/.prettierrc.mjs new file mode 100644 index 0000000..7890fda --- /dev/null +++ b/web/.prettierrc.mjs @@ -0,0 +1 @@ +module.exports = require("@so1ve/prettier-config"); \ No newline at end of file diff --git a/web/README.md b/web/README.md index 1b1031a..07a1fd2 100644 --- a/web/README.md +++ b/web/README.md @@ -18,4 +18,4 @@ Before you start editing, make sure your `Actions on Save` options have `Optimiz ## Reference 1. https://github.com/OIerDb-ng/OIerDb -2. https://github.com/cornflourblue/react-hooks-redux-registration-login-example \ No newline at end of file +2. https://github.com/cornflourblue/react-hooks-redux-registration-login-example diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..adc8605 --- /dev/null +++ b/web/index.html @@ -0,0 +1,19 @@ + + + + + + + + + New API + + + +
+ + + diff --git a/web/package.json b/web/package.json index dc6ca10..4f9eced 100644 --- a/web/package.json +++ b/web/package.json @@ -2,6 +2,7 @@ "name": "react-template", "version": "0.1.0", "private": true, + "type": "module", "dependencies": { "@douyinfe/semi-icons": "^2.46.1", "@douyinfe/semi-ui": "^2.46.1", @@ -16,19 +17,18 @@ "react-dropzone": "^14.2.3", "react-fireworks": "^1.0.4", "react-router-dom": "^6.3.0", - "react-scripts": "5.0.1", "react-telegram-login": "^1.1.2", "react-toastify": "^9.0.8", "react-turnstile": "^1.0.5", - "semantic-ui-css": "^2.5.0", - "semantic-ui-react": "^2.1.3", - "usehooks-ts": "^2.9.1" + "semantic-ui-offline": "^2.5.0", + "semantic-ui-react": "^2.1.3" }, "scripts": { - "start": "react-scripts start", - "build": "react-scripts build", - "test": "react-scripts test", - "eject": "react-scripts eject" + "dev": "vite", + "build": "vite build", + "lint": "prettier . --check", + "lint:fix": "prettier . --write", + "preview": "vite preview" }, "eslintConfig": { "extends": [ @@ -49,8 +49,11 @@ ] }, "devDependencies": { - "prettier": "2.8.8", - "typescript": "4.4.2" + "@so1ve/prettier-config": "^2.0.0", + "@vitejs/plugin-react": "^4.2.1", + "prettier": "^3.0.0", + "typescript": "4.4.2", + "vite": "^5.2.0" }, "prettier": { "singleQuote": true, diff --git a/web/public/index.html b/web/public/index.html deleted file mode 100644 index e9697b9..0000000 --- a/web/public/index.html +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - New API - - - -
- - diff --git a/web/src/App.js b/web/src/App.js index 5a67318..a3b0660 100644 --- a/web/src/App.js +++ b/web/src/App.js @@ -22,9 +22,10 @@ import Log from './pages/Log'; import Chat from './pages/Chat'; import { Layout } from '@douyinfe/semi-ui'; import Midjourney from './pages/Midjourney'; -import Detail from './pages/Detail'; +// import Detail from './pages/Detail'; const Home = lazy(() => import('./pages/Home')); +const Detail = lazy(() => import('./pages/Detail')); const About = lazy(() => import('./pages/About')); function App() { @@ -47,7 +48,7 @@ function App() { } let logo = getLogo(); if (logo) { - let linkElement = document.querySelector('link[rel~=\'icon\']'); + let linkElement = document.querySelector("link[rel~='icon']"); if (linkElement) { linkElement.href = logo; } @@ -59,7 +60,7 @@ function App() { }> @@ -67,7 +68,7 @@ function App() { } /> @@ -75,7 +76,7 @@ function App() { } /> }> @@ -83,7 +84,7 @@ function App() { } /> }> @@ -91,7 +92,7 @@ function App() { } /> @@ -99,7 +100,7 @@ function App() { } /> @@ -107,7 +108,7 @@ function App() { } /> @@ -115,7 +116,7 @@ function App() { } /> }> @@ -123,7 +124,7 @@ function App() { } /> }> @@ -131,7 +132,7 @@ function App() { } /> }> @@ -139,7 +140,7 @@ function App() { } /> }> @@ -147,7 +148,7 @@ function App() { } /> }> @@ -155,7 +156,7 @@ function App() { } /> }> @@ -163,7 +164,7 @@ function App() { } /> }> @@ -171,7 +172,7 @@ function App() { } /> }> @@ -181,7 +182,7 @@ function App() { } /> }> @@ -191,7 +192,7 @@ function App() { } /> @@ -199,23 +200,27 @@ function App() { } /> - + }> + + } /> - + }> + + } /> }> @@ -223,16 +228,14 @@ function App() { } /> }> } /> - - } /> + } /> diff --git a/web/src/components/ChannelsTable.js b/web/src/components/ChannelsTable.js index 144842e..3f61ffc 100644 --- a/web/src/components/ChannelsTable.js +++ b/web/src/components/ChannelsTable.js @@ -1,31 +1,39 @@ import React, { useEffect, useState } from 'react'; -import { API, isMobile, shouldShowPrompt, showError, showInfo, showSuccess, timestamp2string } from '../helpers'; +import { + API, + isMobile, + shouldShowPrompt, + showError, + showInfo, + showSuccess, + timestamp2string, +} from '../helpers'; import { CHANNEL_OPTIONS, ITEMS_PER_PAGE } from '../constants'; -import { renderGroup, renderNumberWithPoint, renderQuota } from '../helpers/render'; import { - Button, - Dropdown, - Form, - InputNumber, - Popconfirm, - Space, - SplitButtonGroup, - Switch, - Table, - Tag, - Tooltip, - Typography + renderGroup, + renderNumberWithPoint, + renderQuota, +} from '../helpers/render'; +import { + Button, + Dropdown, + Form, + InputNumber, + Popconfirm, + Space, + SplitButtonGroup, + Switch, + Table, + Tag, + Tooltip, + Typography, } from '@douyinfe/semi-ui'; import EditChannel from '../pages/Channel/EditChannel'; import { IconTreeTriangleDown } from '@douyinfe/semi-icons'; function renderTimestamp(timestamp) { - return ( - <> - {timestamp2string(timestamp)} - - ); + return <>{timestamp2string(timestamp)}; } let type2label = undefined; @@ -38,7 +46,11 @@ function renderType(type) { } type2label[0] = { value: 0, text: '未知类型', color: 'grey' }; } - return {type2label[type]?.text}; + return ( + + {type2label[type]?.text} + + ); } const ChannelsTable = () => { @@ -50,11 +62,11 @@ const ChannelsTable = () => { // }, { title: 'ID', - dataIndex: 'id' + dataIndex: 'id', }, { title: '名称', - dataIndex: 'name' + dataIndex: 'name', }, { title: '分组', @@ -63,48 +75,34 @@ const ChannelsTable = () => { return (
- { - text.split(',').map((item, index) => { - return (renderGroup(item)); - }) - } + {text.split(',').map((item, index) => { + return renderGroup(item); + })}
); - } + }, }, { title: '类型', dataIndex: 'type', render: (text, record, index) => { - return ( -
- {renderType(text)} -
- ); - } + return
{renderType(text)}
; + }, }, { title: '状态', dataIndex: 'status', render: (text, record, index) => { - return ( -
- {renderStatus(text)} -
- ); - } + return
{renderStatus(text)}
; + }, }, { title: '响应时间', dataIndex: 'response_time', render: (text, record, index) => { - return ( -
- {renderResponseTime(text)} -
- ); - } + return
{renderResponseTime(text)}
; + }, }, { title: '已用/剩余', @@ -114,17 +112,26 @@ const ChannelsTable = () => {
- {renderQuota(record.used_quota)} + + {renderQuota(record.used_quota)} + - { - updateChannelBalance(record); - }}>${renderNumberWithPoint(record.balance)} + { + updateChannelBalance(record); + }} + > + ${renderNumberWithPoint(record.balance)} +
); - } + }, }, { title: '优先级', @@ -134,8 +141,8 @@ const ChannelsTable = () => {
{ + name='priority' + onBlur={(e) => { manageChannel(record.id, 'priority', record, e.target.value); }} keepFocus={true} @@ -145,7 +152,7 @@ const ChannelsTable = () => { />
); - } + }, }, { title: '权重', @@ -155,8 +162,8 @@ const ChannelsTable = () => {
{ + name='weight' + onBlur={(e) => { manageChannel(record.id, 'weight', record, e.target.value); }} keepFocus={true} @@ -166,68 +173,90 @@ const ChannelsTable = () => { />
); - } + }, }, { title: '', dataIndex: 'operate', render: (text, record, index) => (
- - - + + 测试 + + + {/**/} { - manageChannel(record.id, 'delete', record).then( - () => { - removeRecord(record.id); - } - ); + manageChannel(record.id, 'delete', record).then(() => { + removeRecord(record.id); + }); }} > - + - { - record.status === 1 ? - : - - } - + ) : ( + + )} + + }} + > + 编辑 +
- ) - } + ), + }, ]; const [channels, setChannels] = useState([]); @@ -240,20 +269,22 @@ const ChannelsTable = () => { const [searching, setSearching] = useState(false); const [updatingBalance, setUpdatingBalance] = useState(false); const [pageSize, setPageSize] = useState(ITEMS_PER_PAGE); - const [showPrompt, setShowPrompt] = useState(shouldShowPrompt('channel-test')); + const [showPrompt, setShowPrompt] = useState( + shouldShowPrompt('channel-test'), + ); const [channelCount, setChannelCount] = useState(pageSize); const [groupOptions, setGroupOptions] = useState([]); const [showEdit, setShowEdit] = useState(false); const [enableBatchDelete, setEnableBatchDelete] = useState(false); const [editingChannel, setEditingChannel] = useState({ - id: undefined + id: undefined, }); const [selectedChannels, setSelectedChannels] = useState([]); - const removeRecord = id => { + const removeRecord = (id) => { let newDataSource = [...channels]; if (id != null) { - let idx = newDataSource.findIndex(data => data.id === id); + let idx = newDataSource.findIndex((data) => data.id === id); if (idx > -1) { newDataSource.splice(idx, 1); @@ -272,7 +303,7 @@ const ChannelsTable = () => { name: item, onClick: () => { testChannel(channels[i], item); - } + }, }); }); channels[i].test_models = test_models; @@ -288,7 +319,9 @@ const ChannelsTable = () => { const loadChannels = async (startIdx, pageSize, idSort) => { setLoading(true); - const res = await API.get(`/api/channel/?p=${startIdx}&page_size=${pageSize}&id_sort=${idSort}`); + const res = await API.get( + `/api/channel/?p=${startIdx}&page_size=${pageSize}&id_sort=${idSort}`, + ); const { success, message, data } = res.data; if (success) { if (startIdx === 0) { @@ -311,7 +344,8 @@ const ChannelsTable = () => { useEffect(() => { // console.log('default effect') const localIdSort = localStorage.getItem('id-sort') === 'true'; - const localPageSize = parseInt(localStorage.getItem('page-size')) || ITEMS_PER_PAGE; + const localPageSize = + parseInt(localStorage.getItem('page-size')) || ITEMS_PER_PAGE; setIdSort(localIdSort); setPageSize(localPageSize); loadChannels(0, localPageSize, localIdSort) @@ -361,7 +395,6 @@ const ChannelsTable = () => { let channel = res.data.data; let newChannels = [...channels]; if (action === 'delete') { - } else { record.status = channel.status; } @@ -374,22 +407,26 @@ const ChannelsTable = () => { const renderStatus = (status) => { switch (status) { case 1: - return 已启用; + return ( + + 已启用 + + ); case 2: return ( - + 已禁用 ); case 3: return ( - + 自动禁用 ); default: return ( - + 未知状态 ); @@ -400,15 +437,35 @@ const ChannelsTable = () => { let time = responseTime / 1000; time = time.toFixed(2) + ' 秒'; if (responseTime === 0) { - return 未测试; + return ( + + 未测试 + + ); } else if (responseTime <= 1000) { - return {time}; + return ( + + {time} + + ); } else if (responseTime <= 3000) { - return {time}; + return ( + + {time} + + ); } else if (responseTime <= 5000) { - return {time}; + return ( + + {time} + + ); } else { - return {time}; + return ( + + {time} + + ); } }; @@ -420,7 +477,9 @@ const ChannelsTable = () => { return; } setSearching(true); - const res = await API.get(`/api/channel/search?keyword=${searchKeyword}&group=${searchGroup}&model=${searchModel}`); + const res = await API.get( + `/api/channel/search?keyword=${searchKeyword}&group=${searchGroup}&model=${searchModel}`, + ); const { success, message, data } = res.data; if (success) { setChannels(data); @@ -520,14 +579,16 @@ const ChannelsTable = () => { } }; - let pageData = channels.slice((activePage - 1) * pageSize, activePage * pageSize); + let pageData = channels.slice( + (activePage - 1) * pageSize, + activePage * pageSize, + ); - const handlePageChange = page => { + const handlePageChange = (page) => { setActivePage(page); if (page === Math.ceil(channels.length / pageSize) + 1) { // In this case we have to load more data and then append them. - loadChannels(page - 1, pageSize, idSort).then(r => { - }); + loadChannels(page - 1, pageSize, idSort).then((r) => {}); } }; @@ -547,10 +608,12 @@ const ChannelsTable = () => { let res = await API.get(`/api/group/`); // add 'all' option // res.data.data.unshift('all'); - setGroupOptions(res.data.data.map((group) => ({ - label: group, - value: group - }))); + setGroupOptions( + res.data.data.map((group) => ({ + label: group, + value: group, + })), + ); } catch (error) { showError(error.message); } @@ -564,27 +627,34 @@ const ChannelsTable = () => { if (record.status !== 1) { return { style: { - background: 'var(--semi-color-disabled-border)' - } + background: 'var(--semi-color-disabled-border)', + }, }; } else { return {}; } }; - return ( <> - -
{ - searchChannels(searchKeyword, searchGroup, searchModel); - }} labelPosition="left"> + + { + searchChannels(searchKeyword, searchGroup, searchModel); + }} + labelPosition='left' + >
{ @@ -592,21 +662,33 @@ const ChannelsTable = () => { }} /> { setSearchModel(v.trim()); }} /> - { - setSearchGroup(v); - searchChannels(searchKeyword, v, searchModel); - }} /> - + { + setSearchGroup(v); + searchChannels(searchKeyword, v, searchModel); + }} + /> +
@@ -614,80 +696,118 @@ const ChannelsTable = () => { 使用ID排序 - { - localStorage.setItem('id-sort', v + ''); - setIdSort(v); - loadChannels(0, pageSize, v) - .then() - .catch((reason) => { - showError(reason); - }); - }}> + { + localStorage.setItem('id-sort', v + ''); + setIdSort(v); + loadChannels(0, pageSize, v) + .then() + .catch((reason) => { + showError(reason); + }); + }} + > - '', - onPageSizeChange: (size) => { - handlePageSizeChange(size).then(); - }, - onPageChange: handlePageChange - }} loading={loading} onRow={handleRow} rowSelection={ - enableBatchDelete ? - { - onChange: (selectedRowKeys, selectedRows) => { - // console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows); - setSelectedChannels(selectedRows); - } - } : null - } /> -
- -
'', + onPageSizeChange: (size) => { + handlePageSizeChange(size).then(); + }, + onPageChange: handlePageChange, + }} + loading={loading} + onRow={handleRow} + rowSelection={ + enableBatchDelete + ? { + onChange: (selectedRowKeys, selectedRows) => { + // console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows); + setSelectedChannels(selectedRows); + }, + } + : null + } + /> +
+ + + }} + > + 添加渠道 + - + - + - + - + {/*
*/} @@ -696,28 +816,41 @@ const ChannelsTable = () => {
开启批量删除 - { - setEnableBatchDelete(v); - }}> + { + setEnableBatchDelete(v); + }} + > - + - +
diff --git a/web/src/components/Footer.js b/web/src/components/Footer.js index 9e29426..694004a 100644 --- a/web/src/components/Footer.js +++ b/web/src/components/Footer.js @@ -32,27 +32,36 @@ const Footer = () => { {footer ? (
) : ( -
+
- New API {process.env.REACT_APP_VERSION}{' '} + New API {import.meta.env.VITE_REACT_APP_VERSION}{' '} 由{' '} - + Calcium-Ion {' '} 开发,基于{' '} - + One API v0.5.4 {' '} ,本项目根据{' '} - + MIT 许可证 {' '} 授权 diff --git a/web/src/components/GitHubOAuth.js b/web/src/components/GitHubOAuth.js index 4e3b93b..c43ed2a 100644 --- a/web/src/components/GitHubOAuth.js +++ b/web/src/components/GitHubOAuth.js @@ -49,7 +49,7 @@ const GitHubOAuth = () => { return ( - {prompt} + {prompt} ); diff --git a/web/src/components/HeaderBar.js b/web/src/components/HeaderBar.js index eaf36c4..a5da4df 100644 --- a/web/src/components/HeaderBar.js +++ b/web/src/components/HeaderBar.js @@ -17,15 +17,15 @@ let headerButtons = [ text: '关于', itemKey: 'about', to: '/about', - icon: - } + icon: , + }, ]; if (localStorage.getItem('chat_link')) { headerButtons.splice(1, 0, { name: '聊天', to: '/chat', - icon: 'comments' + icon: 'comments', }); } @@ -40,7 +40,11 @@ const HeaderBar = () => { var themeMode = localStorage.getItem('theme-mode'); const currentDate = new Date(); // enable fireworks on new year(1.1 and 2.9-2.24) - const isNewYear = (currentDate.getMonth() === 0 && currentDate.getDate() === 1) || (currentDate.getMonth() === 1 && currentDate.getDate() >= 9 && currentDate.getDate() <= 24); + const isNewYear = + (currentDate.getMonth() === 0 && currentDate.getDate() === 1) || + (currentDate.getMonth() === 1 && + currentDate.getDate() >= 9 && + currentDate.getDate() <= 24); async function logout() { setShowSidebar(false); @@ -93,7 +97,7 @@ const HeaderBar = () => { const routerMap = { about: '/about', login: '/login', - register: '/register' + register: '/register', }; return ( { }} selectedKeys={[]} // items={headerButtons} - onSelect={key => { - - }} + onSelect={(key) => {}} footer={ <> - {isNewYear && + {isNewYear && ( // happy new year - Happy New Year!!! + + Happy New Year!!! + } > - } + )} } /> - - {userState.user ? + + {userState.user ? ( <> 退出 } > - + {userState.user.username[0]} {userState.user.username} - : + ) : ( <> - } /> - } /> + } + /> + } + /> - } + )} } - > - + >
diff --git a/web/src/components/Loading.js b/web/src/components/Loading.js index bacb53b..14242e4 100644 --- a/web/src/components/Loading.js +++ b/web/src/components/Loading.js @@ -1,13 +1,11 @@ import React from 'react'; -import { Dimmer, Loader, Segment } from 'semantic-ui-react'; +import { Spin } from '@douyinfe/semi-ui'; const Loading = ({ prompt: name = 'page' }) => { return ( - - - 加载{name}中... - - + + 加载{name}中... + ); }; diff --git a/web/src/components/LoginForm.js b/web/src/components/LoginForm.js index 3cbeb52..e75b296 100644 --- a/web/src/components/LoginForm.js +++ b/web/src/components/LoginForm.js @@ -4,7 +4,15 @@ import { UserContext } from '../context/User'; import { API, getLogo, showError, showInfo, showSuccess } from '../helpers'; import { onGitHubOAuthClicked } from './utils'; 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 Text from '@douyinfe/semi-ui/lib/es/typography/text'; import TelegramLoginButton from 'react-telegram-login'; @@ -16,7 +24,7 @@ const LoginForm = () => { const [inputs, setInputs] = useState({ username: '', password: '', - wechat_verification_code: '' + wechat_verification_code: '', }); const [searchParams, setSearchParams] = useSearchParams(); const [submitted, setSubmitted] = useState(false); @@ -56,7 +64,7 @@ const LoginForm = () => { return; } 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; if (success) { @@ -81,17 +89,24 @@ const LoginForm = () => { } setSubmitted(true); if (username && password) { - const res = await API.post(`/api/user/login?turnstile=${turnstileToken}`, { - username, - password - }); + const res = await API.post( + `/api/user/login?turnstile=${turnstileToken}`, + { + username, + password, + }, + ); const { success, message, data } = res.data; if (success) { userDispatch({ type: 'login', payload: data }); localStorage.setItem('user', JSON.stringify(data)); showSuccess('登录成功!'); if (username === 'root' && password === '123456') { - Modal.error({ title: '您正在使用默认密码!', content: '请立刻修改默认密码!', centered: true }); + Modal.error({ + title: '您正在使用默认密码!', + content: '请立刻修改默认密码!', + centered: true, + }); } navigate('/token'); } else { @@ -104,7 +119,16 @@ const LoginForm = () => { // 添加Telegram登录处理函数 const onTelegramLoginClicked = async (response) => { - const fields = ['id', 'first_name', 'last_name', 'username', 'photo_url', 'auth_date', 'hash', 'lang']; + const fields = [ + 'id', + 'first_name', + 'last_name', + 'username', + 'photo_url', + 'auth_date', + 'hash', + 'lang', + ]; const params = {}; fields.forEach((field) => { if (response[field]) { @@ -126,10 +150,15 @@ const LoginForm = () => { return (
- - + -
+
@@ -139,50 +168,72 @@ const LoginForm = () => { <Form.Input field={'username'} label={'用户名'} - placeholder="用户名" - name="username" + placeholder='用户名' + name='username' onChange={(value) => handleChange('username', value)} /> <Form.Input field={'password'} label={'密码'} - placeholder="密码" - name="password" - type="password" + placeholder='密码' + name='password' + type='password' onChange={(value) => handleChange('password', value)} /> - <Button theme="solid" style={{ width: '100%' }} type={'primary'} size="large" - htmlType={'submit'} onClick={handleSubmit}> + <Button + theme='solid' + style={{ width: '100%' }} + type={'primary'} + size='large' + htmlType={'submit'} + onClick={handleSubmit} + > 登录 </Button> </Form> - <div style={{ display: 'flex', justifyContent: 'space-between', marginTop: 20 }}> + <div + style={{ + display: 'flex', + justifyContent: 'space-between', + marginTop: 20, + }} + > <Text> - 没有账号请先 <Link to="/register">注册账号</Link> + 没有账号请先 <Link to='/register'>注册账号</Link> </Text> <Text> - 忘记密码 <Link to="/reset">点击重置</Link> + 忘记密码 <Link to='/reset'>点击重置</Link> </Text> </div> - {status.github_oauth || status.wechat_login || status.telegram_oauth ? ( + {status.github_oauth || + status.wechat_login || + status.telegram_oauth ? ( <> - <Divider margin="12px" align="center"> + <Divider margin='12px' align='center'> 第三方登录 </Divider> - <div style={{ display: 'flex', justifyContent: 'center', marginTop: 20 }}> + <div + style={{ + display: 'flex', + justifyContent: 'center', + marginTop: 20, + }} + > {status.github_oauth ? ( <Button - type="primary" + type='primary' icon={<IconGithubLogo />} - onClick={() => onGitHubOAuthClicked(status.github_client_id)} + onClick={() => + onGitHubOAuthClicked(status.github_client_id) + } /> ) : ( <></> )} {status.wechat_login ? ( <Button - type="primary" + type='primary' style={{ color: 'rgba(var(--semi-green-5), 1)' }} icon={<Icon svg={<WeChatIcon />} />} onClick={onWeChatLoginClicked} @@ -192,7 +243,10 @@ const LoginForm = () => { )} {status.telegram_oauth ? ( - <TelegramLoginButton dataOnauth={onTelegramLoginClicked} botName={status.telegram_bot_name} /> + <TelegramLoginButton + dataOnauth={onTelegramLoginClicked} + botName={status.telegram_bot_name} + /> ) : ( <></> )} @@ -202,7 +256,7 @@ const LoginForm = () => { <></> )} <Modal - title="微信扫码登录" + title='微信扫码登录' visible={showWeChatLoginModal} maskClosable={true} onOk={onSubmitWeChatVerificationCode} @@ -211,7 +265,13 @@ const LoginForm = () => { size={'small'} centered={true} > - <div style={{ display: 'flex', alignItem: 'center', flexDirection: 'column' }}> + <div + style={{ + display: 'flex', + alignItem: 'center', + flexDirection: 'column', + }} + > <img src={status.wechat_qrcode} /> </div> <div style={{ textAlign: 'center' }}> @@ -219,19 +279,27 @@ const LoginForm = () => { 微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效) </p> </div> - <Form size="large"> + <Form size='large'> <Form.Input field={'wechat_verification_code'} - placeholder="验证码" + placeholder='验证码' label={'验证码'} value={inputs.wechat_verification_code} - onChange={(value) => handleChange('wechat_verification_code', value)} + onChange={(value) => + handleChange('wechat_verification_code', value) + } /> </Form> </Modal> </Card> {turnstileEnabled ? ( - <div style={{ display: 'flex', justifyContent: 'center', marginTop: 20 }}> + <div + style={{ + display: 'flex', + justifyContent: 'center', + marginTop: 20, + }} + > <Turnstile sitekey={turnstileSiteKey} onVerify={(token) => { @@ -244,7 +312,6 @@ const LoginForm = () => { )} </div> </div> - </Layout.Content> </Layout> </div> diff --git a/web/src/components/LogsTable.js b/web/src/components/LogsTable.js index b07682b..57003e0 100644 --- a/web/src/components/LogsTable.js +++ b/web/src/components/LogsTable.js @@ -1,7 +1,25 @@ import React, { useEffect, useState } from 'react'; -import { API, copy, isAdmin, showError, showSuccess, timestamp2string } from '../helpers'; +import { + API, + copy, + isAdmin, + showError, + showSuccess, + timestamp2string, +} from '../helpers'; -import { Avatar, Button, Form, Layout, Modal, Select, Space, Spin, Table, Tag } from '@douyinfe/semi-ui'; +import { + Avatar, + Button, + Form, + Layout, + Modal, + Select, + Space, + Spin, + Table, + Tag, +} from '@douyinfe/semi-ui'; import { ITEMS_PER_PAGE } from '../constants'; import { renderNumber, renderQuota, stringToColor } from '../helpers/render'; import Paragraph from '@douyinfe/semi-ui/lib/es/typography/paragraph'; @@ -9,131 +27,285 @@ import Paragraph from '@douyinfe/semi-ui/lib/es/typography/paragraph'; const { Header } = Layout; function renderTimestamp(timestamp) { - return (<> - {timestamp2string(timestamp)} - </>); + return <>{timestamp2string(timestamp)}</>; } -const MODE_OPTIONS = [{ key: 'all', text: '全部用户', value: 'all' }, { key: 'self', text: '当前用户', value: 'self' }]; +const MODE_OPTIONS = [ + { key: 'all', text: '全部用户', value: 'all' }, + { key: 'self', text: '当前用户', value: 'self' }, +]; -const colors = ['amber', 'blue', 'cyan', 'green', 'grey', 'indigo', 'light-blue', 'lime', 'orange', 'pink', 'purple', 'red', 'teal', 'violet', 'yellow']; +const colors = [ + 'amber', + 'blue', + 'cyan', + 'green', + 'grey', + 'indigo', + 'light-blue', + 'lime', + 'orange', + 'pink', + 'purple', + 'red', + 'teal', + 'violet', + 'yellow', +]; function renderType(type) { switch (type) { case 1: - return <Tag color="cyan" size="large"> 充值 </Tag>; + return ( + <Tag color='cyan' size='large'> + {' '} + 充值{' '} + </Tag> + ); case 2: - return <Tag color="lime" size="large"> 消费 </Tag>; + return ( + <Tag color='lime' size='large'> + {' '} + 消费{' '} + </Tag> + ); case 3: - return <Tag color="orange" size="large"> 管理 </Tag>; + return ( + <Tag color='orange' size='large'> + {' '} + 管理{' '} + </Tag> + ); case 4: - return <Tag color="purple" size="large"> 系统 </Tag>; + return ( + <Tag color='purple' size='large'> + {' '} + 系统{' '} + </Tag> + ); default: - return <Tag color="black" size="large"> 未知 </Tag>; + return ( + <Tag color='black' size='large'> + {' '} + 未知{' '} + </Tag> + ); } } function renderIsStream(bool) { if (bool) { - return <Tag color="blue" size="large">流</Tag>; + return ( + <Tag color='blue' size='large'> + 流 + </Tag> + ); } else { - return <Tag color="purple" size="large">非流</Tag>; + return ( + <Tag color='purple' size='large'> + 非流 + </Tag> + ); } } function renderUseTime(type) { const time = parseInt(type); if (time < 101) { - return <Tag color="green" size="large"> {time} s </Tag>; + return ( + <Tag color='green' size='large'> + {' '} + {time} s{' '} + </Tag> + ); } else if (time < 300) { - return <Tag color="orange" size="large"> {time} s </Tag>; + return ( + <Tag color='orange' size='large'> + {' '} + {time} s{' '} + </Tag> + ); } else { - return <Tag color="red" size="large"> {time} s </Tag>; + return ( + <Tag color='red' size='large'> + {' '} + {time} s{' '} + </Tag> + ); } } const LogsTable = () => { - const columns = [{ - title: '时间', dataIndex: 'timestamp2string' - }, { - title: '渠道', - dataIndex: 'channel', - className: isAdmin() ? 'tableShow' : 'tableHiddle', - render: (text, record, index) => { - return (isAdminUser ? record.type === 0 || record.type === 2 ? <div> - {<Tag color={colors[parseInt(text) % colors.length]} size="large"> {text} </Tag>} - </div> : <></> : <></>); - } - }, { - title: '用户', - dataIndex: 'username', - className: isAdmin() ? 'tableShow' : 'tableHiddle', - render: (text, record, index) => { - return (isAdminUser ? <div> - <Avatar size="small" color={stringToColor(text)} style={{ marginRight: 4 }} - onClick={() => showUserInfo(record.user_id)}> - {typeof text === 'string' && text.slice(0, 1)} - </Avatar> - {text} - </div> : <></>); - } - }, { - title: '令牌', dataIndex: 'token_name', render: (text, record, index) => { - return (record.type === 0 || record.type === 2 ? <div> - <Tag color="grey" size="large" onClick={() => { - copyText(text); - }}> {text} </Tag> - </div> : <></>); - } - }, { - title: '类型', dataIndex: 'type', render: (text, record, index) => { - return (<div> - {renderType(text)} - </div>); - } - }, { - title: '模型', dataIndex: 'model_name', render: (text, record, index) => { - return (record.type === 0 || record.type === 2 ? <div> - <Tag color={stringToColor(text)} size="large" onClick={() => { - copyText(text); - }}> {text} </Tag> - </div> : <></>); - } - }, { - title: '用时', dataIndex: 'use_time', render: (text, record, index) => { - return (<div> - <Space> - {renderUseTime(text)} - {renderIsStream(record.is_stream)} - </Space> - </div>); - } - }, { - title: '提示', dataIndex: 'prompt_tokens', render: (text, record, index) => { - return (record.type === 0 || record.type === 2 ? <div> - {<span> {text} </span>} - </div> : <></>); - } - }, { - title: '补全', dataIndex: 'completion_tokens', render: (text, record, index) => { - return (parseInt(text) > 0 && (record.type === 0 || record.type === 2) ? <div> - {<span> {text} </span>} - </div> : <></>); - } - }, { - title: '花费', dataIndex: 'quota', render: (text, record, index) => { - return (record.type === 0 || record.type === 2 ? <div> - {renderQuota(text, 6)} - </div> : <></>); - } - }, { - title: '详情', dataIndex: 'content', render: (text, record, index) => { - return <Paragraph ellipsis={{ rows: 2, showTooltip: { type: 'popover', opts: { style: { width: 240 } } } }} - style={{ maxWidth: 240 }}> - {text} - </Paragraph>; - } - }]; + const columns = [ + { + title: '时间', + dataIndex: 'timestamp2string', + }, + { + title: '渠道', + dataIndex: 'channel', + className: isAdmin() ? 'tableShow' : 'tableHiddle', + render: (text, record, index) => { + return isAdminUser ? ( + record.type === 0 || record.type === 2 ? ( + <div> + { + <Tag + color={colors[parseInt(text) % colors.length]} + size='large' + > + {' '} + {text}{' '} + </Tag> + } + </div> + ) : ( + <></> + ) + ) : ( + <></> + ); + }, + }, + { + title: '用户', + dataIndex: 'username', + className: isAdmin() ? 'tableShow' : 'tableHiddle', + render: (text, record, index) => { + return isAdminUser ? ( + <div> + <Avatar + size='small' + color={stringToColor(text)} + style={{ marginRight: 4 }} + onClick={() => showUserInfo(record.user_id)} + > + {typeof text === 'string' && text.slice(0, 1)} + </Avatar> + {text} + </div> + ) : ( + <></> + ); + }, + }, + { + title: '令牌', + dataIndex: 'token_name', + render: (text, record, index) => { + return record.type === 0 || record.type === 2 ? ( + <div> + <Tag + color='grey' + size='large' + onClick={() => { + copyText(text); + }} + > + {' '} + {text}{' '} + </Tag> + </div> + ) : ( + <></> + ); + }, + }, + { + title: '类型', + dataIndex: 'type', + render: (text, record, index) => { + return <div>{renderType(text)}</div>; + }, + }, + { + title: '模型', + dataIndex: 'model_name', + render: (text, record, index) => { + return record.type === 0 || record.type === 2 ? ( + <div> + <Tag + color={stringToColor(text)} + size='large' + onClick={() => { + copyText(text); + }} + > + {' '} + {text}{' '} + </Tag> + </div> + ) : ( + <></> + ); + }, + }, + { + title: '用时', + dataIndex: 'use_time', + render: (text, record, index) => { + return ( + <div> + <Space> + {renderUseTime(text)} + {renderIsStream(record.is_stream)} + </Space> + </div> + ); + }, + }, + { + title: '提示', + dataIndex: 'prompt_tokens', + render: (text, record, index) => { + return record.type === 0 || record.type === 2 ? ( + <div>{<span> {text} </span>}</div> + ) : ( + <></> + ); + }, + }, + { + title: '补全', + dataIndex: 'completion_tokens', + render: (text, record, index) => { + return parseInt(text) > 0 && + (record.type === 0 || record.type === 2) ? ( + <div>{<span> {text} </span>}</div> + ) : ( + <></> + ); + }, + }, + { + title: '花费', + dataIndex: 'quota', + render: (text, record, index) => { + return record.type === 0 || record.type === 2 ? ( + <div>{renderQuota(text, 6)}</div> + ) : ( + <></> + ); + }, + }, + { + title: '详情', + dataIndex: 'content', + render: (text, record, index) => { + return ( + <Paragraph + ellipsis={{ + rows: 2, + showTooltip: { type: 'popover', opts: { style: { width: 240 } } }, + }} + style={{ maxWidth: 240 }} + > + {text} + </Paragraph> + ); + }, + }, + ]; const [logs, setLogs] = useState([]); const [showStat, setShowStat] = useState(false); @@ -154,12 +326,20 @@ const LogsTable = () => { model_name: '', start_timestamp: timestamp2string(now.getTime() / 1000 - 86400), end_timestamp: timestamp2string(now.getTime() / 1000 + 3600), - channel: '' + channel: '', }); - const { username, token_name, model_name, start_timestamp, end_timestamp, channel } = inputs; + const { + username, + token_name, + model_name, + start_timestamp, + end_timestamp, + channel, + } = inputs; const [stat, setStat] = useState({ - quota: 0, token: 0 + quota: 0, + token: 0, }); const handleInputChange = (value, name) => { @@ -169,7 +349,9 @@ const LogsTable = () => { const getLogSelfStat = async () => { let localStartTimestamp = Date.parse(start_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; if (success) { setStat(data); @@ -181,7 +363,9 @@ const LogsTable = () => { const getLogStat = async () => { let localStartTimestamp = Date.parse(start_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}&channel=${channel}`); + 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}&channel=${channel}`, + ); const { success, message, data } = res.data; if (success) { setStat(data); @@ -209,12 +393,16 @@ const LogsTable = () => { const { success, message, data } = res.data; if (success) { Modal.info({ - title: '用户信息', content: <div style={{ padding: 12 }}> - <p>用户名: {data.username}</p> - <p>余额: {renderQuota(data.quota)}</p> - <p>已用额度:{renderQuota(data.used_quota)}</p> - <p>请求次数:{renderNumber(data.request_count)}</p> - </div>, centered: true + title: '用户信息', + content: ( + <div style={{ padding: 12 }}> + <p>用户名: {data.username}</p> + <p>余额: {renderQuota(data.quota)}</p> + <p>已用额度:{renderQuota(data.used_quota)}</p> + <p>请求次数:{renderNumber(data.request_count)}</p> + </div> + ), + centered: true, }); } else { showError(message); @@ -259,14 +447,16 @@ const LogsTable = () => { setLoading(false); }; - const pageData = logs.slice((activePage - 1) * pageSize, activePage * pageSize); + const pageData = logs.slice( + (activePage - 1) * pageSize, + activePage * pageSize, + ); - const handlePageChange = page => { + const handlePageChange = (page) => { setActivePage(page); if (page === Math.ceil(logs.length / pageSize) + 1) { // In this case we have to load more data and then append them. - loadLogs(page - 1, pageSize, logType).then(r => { - }); + loadLogs(page - 1, pageSize, logType).then((r) => {}); } }; @@ -298,7 +488,8 @@ const LogsTable = () => { useEffect(() => { // console.log('default effect') - const localPageSize = parseInt(localStorage.getItem('page-size')) || ITEMS_PER_PAGE; + const localPageSize = + parseInt(localStorage.getItem('page-size')) || ITEMS_PER_PAGE; setPageSize(localPageSize); loadLogs(0, localPageSize) .then() @@ -326,74 +517,136 @@ const LogsTable = () => { setSearching(false); }; - return (<> - <Layout> - <Header> - <Spin spinning={loadingStat}> - <h3>使用明细(总消耗额度: - <span onClick={handleEyeClick} style={{ - cursor: 'pointer', color: 'gray' - }}>{showStat ? renderQuota(stat.quota) : '点击查看'}</span> - ) - </h3> - </Spin> - </Header> - <Form layout="horizontal" style={{ marginTop: 10 }}> - <> - <Form.Input field="token_name" label="令牌名称" style={{ width: 176 }} value={token_name} - placeholder={'可选值'} name="token_name" - onChange={value => handleInputChange(value, 'token_name')} /> - <Form.Input field="model_name" label="模型名称" style={{ width: 176 }} value={model_name} - placeholder="可选值" - name="model_name" - onChange={value => handleInputChange(value, 'model_name')} /> - <Form.DatePicker field="start_timestamp" label="起始时间" style={{ width: 272 }} - 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: 272 }} - initValue={end_timestamp} - value={end_timestamp} type="dateTime" - name="end_timestamp" - onChange={value => handleInputChange(value, 'end_timestamp')} /> - {isAdminUser && <> - <Form.Input field="channel" label="渠道 ID" style={{ width: 176 }} value={channel} - placeholder="可选值" name="channel" - onChange={value => handleInputChange(value, 'channel')} /> - <Form.Input field="username" label="用户名称" style={{ width: 176 }} value={username} - placeholder={'可选值'} name="username" - onChange={value => handleInputChange(value, 'username')} /> - </>} - <Form.Section> - <Button label="查询" type="primary" htmlType="submit" className="btn-margin-right" - onClick={refresh} loading={loading}>查询</Button> - </Form.Section> - </> - </Form> - <Table style={{ marginTop: 5 }} columns={columns} dataSource={pageData} pagination={{ - currentPage: activePage, - pageSize: pageSize, - total: logCount, - pageSizeOpts: [10, 20, 50, 100], - showSizeChanger: true, - onPageSizeChange: (size) => { - handlePageSizeChange(size).then(); - }, - onPageChange: handlePageChange - }} /> - <Select defaultValue="0" style={{ width: 120 }} onChange={(value) => { - setLogType(parseInt(value)); - refresh(parseInt(value)).then(); - }}> - <Select.Option value="0">全部</Select.Option> - <Select.Option value="1">充值</Select.Option> - <Select.Option value="2">消费</Select.Option> - <Select.Option value="3">管理</Select.Option> - <Select.Option value="4">系统</Select.Option> - </Select> - </Layout> - </>); + return ( + <> + <Layout> + <Header> + <Spin spinning={loadingStat}> + <h3> + 使用明细(总消耗额度: + <span + onClick={handleEyeClick} + style={{ + cursor: 'pointer', + color: 'gray', + }} + > + {showStat ? renderQuota(stat.quota) : '点击查看'} + </span> + ) + </h3> + </Spin> + </Header> + <Form layout='horizontal' style={{ marginTop: 10 }}> + <> + <Form.Input + field='token_name' + label='令牌名称' + style={{ width: 176 }} + value={token_name} + placeholder={'可选值'} + name='token_name' + onChange={(value) => handleInputChange(value, 'token_name')} + /> + <Form.Input + field='model_name' + label='模型名称' + style={{ width: 176 }} + value={model_name} + placeholder='可选值' + name='model_name' + onChange={(value) => handleInputChange(value, 'model_name')} + /> + <Form.DatePicker + field='start_timestamp' + label='起始时间' + style={{ width: 272 }} + 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: 272 }} + initValue={end_timestamp} + value={end_timestamp} + type='dateTime' + name='end_timestamp' + onChange={(value) => handleInputChange(value, 'end_timestamp')} + /> + {isAdminUser && ( + <> + <Form.Input + field='channel' + label='渠道 ID' + style={{ width: 176 }} + value={channel} + placeholder='可选值' + name='channel' + onChange={(value) => handleInputChange(value, 'channel')} + /> + <Form.Input + field='username' + label='用户名称' + style={{ width: 176 }} + value={username} + placeholder={'可选值'} + name='username' + onChange={(value) => handleInputChange(value, 'username')} + /> + </> + )} + <Form.Section> + <Button + label='查询' + type='primary' + htmlType='submit' + className='btn-margin-right' + onClick={refresh} + loading={loading} + > + 查询 + </Button> + </Form.Section> + </> + </Form> + <Table + style={{ marginTop: 5 }} + columns={columns} + dataSource={pageData} + pagination={{ + currentPage: activePage, + pageSize: pageSize, + total: logCount, + pageSizeOpts: [10, 20, 50, 100], + showSizeChanger: true, + onPageSizeChange: (size) => { + handlePageSizeChange(size).then(); + }, + onPageChange: handlePageChange, + }} + /> + <Select + defaultValue='0' + style={{ width: 120 }} + onChange={(value) => { + setLogType(parseInt(value)); + refresh(parseInt(value)).then(); + }} + > + <Select.Option value='0'>全部</Select.Option> + <Select.Option value='1'>充值</Select.Option> + <Select.Option value='2'>消费</Select.Option> + <Select.Option value='3'>管理</Select.Option> + <Select.Option value='4'>系统</Select.Option> + </Select> + </Layout> + </> + ); }; export default LogsTable; diff --git a/web/src/components/MjLogsTable.js b/web/src/components/MjLogsTable.js index 6a6fbd9..f22ddae 100644 --- a/web/src/components/MjLogsTable.js +++ b/web/src/components/MjLogsTable.js @@ -1,86 +1,226 @@ import React, { useEffect, useState } from 'react'; -import { API, copy, isAdmin, showError, showSuccess, timestamp2string } from '../helpers'; +import { + API, + copy, + isAdmin, + showError, + showSuccess, + timestamp2string, +} from '../helpers'; -import { Banner, Button, Form, ImagePreview, Layout, Modal, Progress, Table, Tag, Typography } from '@douyinfe/semi-ui'; +import { + Banner, + Button, + Form, + ImagePreview, + Layout, + Modal, + Progress, + Table, + Tag, + Typography, +} from '@douyinfe/semi-ui'; import { ITEMS_PER_PAGE } from '../constants'; - -const colors = ['amber', 'blue', 'cyan', 'green', 'grey', 'indigo', - 'light-blue', 'lime', 'orange', 'pink', - 'purple', 'red', 'teal', 'violet', 'yellow' +const colors = [ + 'amber', + 'blue', + 'cyan', + 'green', + 'grey', + 'indigo', + 'light-blue', + 'lime', + 'orange', + 'pink', + 'purple', + 'red', + 'teal', + 'violet', + 'yellow', ]; function renderType(type) { switch (type) { case 'IMAGINE': - return <Tag color="blue" size="large">绘图</Tag>; + return ( + <Tag color='blue' size='large'> + 绘图 + </Tag> + ); case 'UPSCALE': - return <Tag color="orange" size="large">放大</Tag>; + return ( + <Tag color='orange' size='large'> + 放大 + </Tag> + ); case 'VARIATION': - return <Tag color="purple" size="large">变换</Tag>; + return ( + <Tag color='purple' size='large'> + 变换 + </Tag> + ); case 'HIGH_VARIATION': - return <Tag color="purple" size="large">强变换</Tag>; + return ( + <Tag color='purple' size='large'> + 强变换 + </Tag> + ); case 'LOW_VARIATION': - return <Tag color="purple" size="large">弱变换</Tag>; + return ( + <Tag color='purple' size='large'> + 弱变换 + </Tag> + ); case 'PAN': - return <Tag color="cyan" size="large">平移</Tag>; + return ( + <Tag color='cyan' size='large'> + 平移 + </Tag> + ); case 'DESCRIBE': - return <Tag color="yellow" size="large">图生文</Tag>; + return ( + <Tag color='yellow' size='large'> + 图生文 + </Tag> + ); case 'BLEND': - return <Tag color="lime" size="large">图混合</Tag>; + return ( + <Tag color='lime' size='large'> + 图混合 + </Tag> + ); case 'SHORTEN': - return <Tag color="pink" size="large">缩词</Tag>; + return ( + <Tag color='pink' size='large'> + 缩词 + </Tag> + ); case 'REROLL': - return <Tag color="indigo" size="large">重绘</Tag>; + return ( + <Tag color='indigo' size='large'> + 重绘 + </Tag> + ); case 'INPAINT': - return <Tag color="violet" size="large">局部重绘-提交</Tag>; + return ( + <Tag color='violet' size='large'> + 局部重绘-提交 + </Tag> + ); case 'ZOOM': - return <Tag color="teal" size="large">变焦</Tag>; + return ( + <Tag color='teal' size='large'> + 变焦 + </Tag> + ); case 'CUSTOM_ZOOM': - return <Tag color="teal" size="large">自定义变焦-提交</Tag>; + return ( + <Tag color='teal' size='large'> + 自定义变焦-提交 + </Tag> + ); case 'MODAL': - return <Tag color="green" size="large">窗口处理</Tag>; + return ( + <Tag color='green' size='large'> + 窗口处理 + </Tag> + ); case 'SWAP_FACE': - return <Tag color="light-green" size="large">换脸</Tag>; + return ( + <Tag color='light-green' size='large'> + 换脸 + </Tag> + ); default: - return <Tag color="white" size="large">未知</Tag>; + return ( + <Tag color='white' size='large'> + 未知 + </Tag> + ); } } - function renderCode(code) { switch (code) { case 1: - return <Tag color="green" size="large">已提交</Tag>; + return ( + <Tag color='green' size='large'> + 已提交 + </Tag> + ); case 21: - return <Tag color="lime" size="large">等待中</Tag>; + return ( + <Tag color='lime' size='large'> + 等待中 + </Tag> + ); case 22: - return <Tag color="orange" size="large">重复提交</Tag>; + return ( + <Tag color='orange' size='large'> + 重复提交 + </Tag> + ); case 0: - return <Tag color="yellow" size="large">未提交</Tag>; + return ( + <Tag color='yellow' size='large'> + 未提交 + </Tag> + ); default: - return <Tag color="white" size="large">未知</Tag>; + return ( + <Tag color='white' size='large'> + 未知 + </Tag> + ); } } - function renderStatus(type) { // Ensure all cases are string literals by adding quotes. switch (type) { case 'SUCCESS': - return <Tag color="green" size="large">成功</Tag>; + return ( + <Tag color='green' size='large'> + 成功 + </Tag> + ); case 'NOT_START': - return <Tag color="grey" size="large">未启动</Tag>; + return ( + <Tag color='grey' size='large'> + 未启动 + </Tag> + ); case 'SUBMITTED': - return <Tag color="yellow" size="large">队列中</Tag>; + return ( + <Tag color='yellow' size='large'> + 队列中 + </Tag> + ); case 'IN_PROGRESS': - return <Tag color="blue" size="large">执行中</Tag>; + return ( + <Tag color='blue' size='large'> + 执行中 + </Tag> + ); case 'FAILURE': - return <Tag color="red" size="large">失败</Tag>; + return ( + <Tag color='red' size='large'> + 失败 + </Tag> + ); case 'MODAL': - return <Tag color="yellow" size="large">窗口等待</Tag>; + return ( + <Tag color='yellow' size='large'> + 窗口等待 + </Tag> + ); default: - return <Tag color="white" size="large">未知</Tag>; + return ( + <Tag color='white' size='large'> + 未知 + </Tag> + ); } } @@ -97,7 +237,6 @@ const renderTimestamp = (timestampInSeconds) => { return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; // 格式化输出 }; - const LogsTable = () => { const [isModalOpen, setIsModalOpen] = useState(false); const [modalContent, setModalContent] = useState(''); @@ -106,12 +245,8 @@ const LogsTable = () => { title: '提交时间', dataIndex: 'submit_time', render: (text, record, index) => { - return ( - <div> - {renderTimestamp(text / 1000)} - </div> - ); - } + return <div>{renderTimestamp(text / 1000)}</div>; + }, }, { title: '渠道', @@ -119,61 +254,50 @@ const LogsTable = () => { className: isAdmin() ? 'tableShow' : 'tableHiddle', render: (text, record, index) => { return ( - <div> - <Tag color={colors[parseInt(text) % colors.length]} size="large" onClick={() => { - copyText(text); // 假设copyText是用于文本复制的函数 - }}> {text} </Tag> + <Tag + color={colors[parseInt(text) % colors.length]} + size='large' + onClick={() => { + copyText(text); // 假设copyText是用于文本复制的函数 + }} + > + {' '} + {text}{' '} + </Tag> </div> - ); - } + }, }, { title: '类型', dataIndex: 'action', render: (text, record, index) => { - return ( - <div> - {renderType(text)} - </div> - ); - } + return <div>{renderType(text)}</div>; + }, }, { title: '任务ID', dataIndex: 'mj_id', render: (text, record, index) => { - return ( - <div> - {text} - </div> - ); - } + return <div>{text}</div>; + }, }, { title: '提交结果', dataIndex: 'code', className: isAdmin() ? 'tableShow' : 'tableHiddle', render: (text, record, index) => { - return ( - <div> - {renderCode(text)} - </div> - ); - } + return <div>{renderCode(text)}</div>; + }, }, { title: '任务状态', dataIndex: 'status', className: isAdmin() ? 'tableShow' : 'tableHiddle', render: (text, record, index) => { - return ( - <div> - {renderStatus(text)} - </div> - ); - } + return <div>{renderStatus(text)}</div>; + }, }, { title: '进度', @@ -183,13 +307,20 @@ const LogsTable = () => { <div> { // 转换例如100%为数字100,如果text未定义,返回0 - <Progress stroke={record.status === 'FAILURE' ? 'var(--semi-color-warning)' : null} - percent={text ? parseInt(text.replace('%', '')) : 0} showInfo={true} - aria-label="drawing progress" /> + <Progress + stroke={ + record.status === 'FAILURE' + ? 'var(--semi-color-warning)' + : null + } + percent={text ? parseInt(text.replace('%', '')) : 0} + showInfo={true} + aria-label='drawing progress' + /> } </div> ); - } + }, }, { title: '结果图片', @@ -201,14 +332,14 @@ const LogsTable = () => { return ( <Button onClick={() => { - setModalImageUrl(text); // 更新图片URL状态 - setIsModalOpenurl(true); // 打开模态框 + setModalImageUrl(text); // 更新图片URL状态 + setIsModalOpenurl(true); // 打开模态框 }} > 查看图片 </Button> ); - } + }, }, { title: 'Prompt', @@ -231,7 +362,7 @@ const LogsTable = () => { {text} </Typography.Text> ); - } + }, }, { title: 'PromptEn', @@ -254,7 +385,7 @@ const LogsTable = () => { {text} </Typography.Text> ); - } + }, }, { title: '失败原因', @@ -277,9 +408,8 @@ const LogsTable = () => { {text} </Typography.Text> ); - } - } - + }, + }, ]; const [logs, setLogs] = useState([]); @@ -299,20 +429,19 @@ const LogsTable = () => { channel_id: '', mj_id: '', start_timestamp: timestamp2string(now.getTime() / 1000 - 2592000), - end_timestamp: timestamp2string(now.getTime() / 1000 + 3600) + end_timestamp: timestamp2string(now.getTime() / 1000 + 3600), }); const { channel_id, mj_id, start_timestamp, end_timestamp } = inputs; const [stat, setStat] = useState({ quota: 0, - token: 0 + token: 0, }); const handleInputChange = (value, name) => { setInputs((inputs) => ({ ...inputs, [name]: value })); }; - const setLogsFormat = (logs) => { for (let i = 0; i < logs.length; i++) { logs[i].timestamp2string = timestamp2string(logs[i].created_at); @@ -351,14 +480,16 @@ const LogsTable = () => { setLoading(false); }; - const pageData = logs.slice((activePage - 1) * ITEMS_PER_PAGE, activePage * ITEMS_PER_PAGE); + const pageData = logs.slice( + (activePage - 1) * ITEMS_PER_PAGE, + activePage * ITEMS_PER_PAGE, + ); - const handlePageChange = 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 => { - }); + loadLogs(page - 1).then((r) => {}); } }; @@ -390,46 +521,83 @@ const LogsTable = () => { return ( <> - <Layout> - {isAdminUser && showBanner ? <Banner - type="info" - description="当前未开启Midjourney回调,部分项目可能无法获得绘图结果,可在运营设置中开启。" - /> : <></> - } - <Form layout="horizontal" style={{ marginTop: 10 }}> + {isAdminUser && showBanner ? ( + <Banner + type='info' + description='当前未开启Midjourney回调,部分项目可能无法获得绘图结果,可在运营设置中开启。' + /> + ) : ( + <></> + )} + <Form layout='horizontal' style={{ marginTop: 10 }}> <> - <Form.Input field="channel_id" label="渠道 ID" style={{ width: 176 }} value={channel_id} - placeholder={'可选值'} name="channel_id" - onChange={value => handleInputChange(value, 'channel_id')} /> - <Form.Input field="mj_id" label="任务 ID" style={{ width: 176 }} value={mj_id} - placeholder="可选值" - name="mj_id" - onChange={value => handleInputChange(value, 'mj_id')} /> - <Form.DatePicker field="start_timestamp" label="起始时间" style={{ width: 272 }} - 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: 272 }} - initValue={end_timestamp} - value={end_timestamp} type="dateTime" - name="end_timestamp" - onChange={value => handleInputChange(value, 'end_timestamp')} /> + <Form.Input + field='channel_id' + label='渠道 ID' + style={{ width: 176 }} + value={channel_id} + placeholder={'可选值'} + name='channel_id' + onChange={(value) => handleInputChange(value, 'channel_id')} + /> + <Form.Input + field='mj_id' + label='任务 ID' + style={{ width: 176 }} + value={mj_id} + placeholder='可选值' + name='mj_id' + onChange={(value) => handleInputChange(value, 'mj_id')} + /> + <Form.DatePicker + field='start_timestamp' + label='起始时间' + style={{ width: 272 }} + 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: 272 }} + initValue={end_timestamp} + value={end_timestamp} + type='dateTime' + name='end_timestamp' + onChange={(value) => handleInputChange(value, 'end_timestamp')} + /> <Form.Section> - <Button label="查询" type="primary" htmlType="submit" className="btn-margin-right" - onClick={refresh}>查询</Button> + <Button + label='查询' + type='primary' + htmlType='submit' + className='btn-margin-right' + onClick={refresh} + > + 查询 + </Button> </Form.Section> </> </Form> - <Table style={{ marginTop: 5 }} columns={columns} dataSource={pageData} pagination={{ - currentPage: activePage, - pageSize: ITEMS_PER_PAGE, - total: logCount, - pageSizeOpts: [10, 20, 50, 100], - onPageChange: handlePageChange - }} loading={loading} /> + <Table + style={{ marginTop: 5 }} + columns={columns} + dataSource={pageData} + pagination={{ + currentPage: activePage, + pageSize: ITEMS_PER_PAGE, + total: logCount, + pageSizeOpts: [10, 20, 50, 100], + onPageChange: handlePageChange, + }} + loading={loading} + /> <Modal visible={isModalOpen} onOk={() => setIsModalOpen(false)} @@ -445,7 +613,6 @@ const LogsTable = () => { visible={isModalOpenurl} onVisibleChange={(visible) => setIsModalOpenurl(visible)} /> - </Layout> </> ); diff --git a/web/src/components/OperationSetting.js b/web/src/components/OperationSetting.js index 728eab0..3019906 100644 --- a/web/src/components/OperationSetting.js +++ b/web/src/components/OperationSetting.js @@ -1,6 +1,12 @@ import React, { useEffect, useState } from 'react'; import { Divider, Form, Grid, Header } from 'semantic-ui-react'; -import { API, showError, showSuccess, timestamp2string, verifyJSON } from '../helpers'; +import { + API, + showError, + showSuccess, + timestamp2string, + verifyJSON, +} from '../helpers'; const OperationSetting = () => { let now = new Date(); @@ -35,16 +41,18 @@ const OperationSetting = () => { DataExportDefaultTime: 'hour', DataExportInterval: 5, DefaultCollapseSidebar: '', // 默认折叠侧边栏 - RetryTimes: 0 + RetryTimes: 0, }); const [originInputs, setOriginInputs] = useState({}); let [loading, setLoading] = useState(false); - let [historyTimestamp, setHistoryTimestamp] = useState(timestamp2string(now.getTime() / 1000 - 30 * 24 * 3600)); // a month ago + let [historyTimestamp, setHistoryTimestamp] = useState( + timestamp2string(now.getTime() / 1000 - 30 * 24 * 3600), + ); // a month ago // 精确时间选项(小时,天,周) const timeOptions = [ { key: 'hour', text: '小时', value: 'hour' }, { key: 'day', text: '天', value: 'day' }, - { key: 'week', text: '周', value: 'week' } + { key: 'week', text: '周', value: 'week' }, ]; const getOptions = async () => { const res = await API.get('/api/option/'); @@ -52,7 +60,11 @@ const OperationSetting = () => { if (success) { let newInputs = {}; data.forEach((item) => { - if (item.key === 'ModelRatio' || item.key === 'GroupRatio' || item.key === 'ModelPrice') { + if ( + item.key === 'ModelRatio' || + item.key === 'GroupRatio' || + item.key === 'ModelPrice' + ) { item.value = JSON.stringify(JSON.parse(item.value), null, 2); } newInputs[item.key] = item.value; @@ -79,7 +91,7 @@ const OperationSetting = () => { console.log(key, value); const res = await API.put('/api/option/', { key, - value + value, }); const { success, message } = res.data; if (success) { @@ -91,7 +103,12 @@ const OperationSetting = () => { }; const handleInputChange = async (e, { name, value }) => { - if (name.endsWith('Enabled') || name === 'DataExportInterval' || name === 'DataExportDefaultTime' || name === 'DefaultCollapseSidebar') { + if ( + name.endsWith('Enabled') || + name === 'DataExportInterval' || + name === 'DataExportDefaultTime' || + name === 'DefaultCollapseSidebar' + ) { if (name === 'DataExportDefaultTime') { localStorage.setItem('data_export_default_time', value); } else if (name === 'MjNotifyEnabled') { @@ -106,11 +123,22 @@ const OperationSetting = () => { const submitConfig = async (group) => { switch (group) { case 'monitor': - if (originInputs['ChannelDisableThreshold'] !== inputs.ChannelDisableThreshold) { - await updateOption('ChannelDisableThreshold', inputs.ChannelDisableThreshold); + if ( + originInputs['ChannelDisableThreshold'] !== + inputs.ChannelDisableThreshold + ) { + await updateOption( + 'ChannelDisableThreshold', + inputs.ChannelDisableThreshold, + ); } - if (originInputs['QuotaRemindThreshold'] !== inputs.QuotaRemindThreshold) { - await updateOption('QuotaRemindThreshold', inputs.QuotaRemindThreshold); + if ( + originInputs['QuotaRemindThreshold'] !== inputs.QuotaRemindThreshold + ) { + await updateOption( + 'QuotaRemindThreshold', + inputs.QuotaRemindThreshold, + ); } break; case 'ratio': @@ -177,7 +205,9 @@ const OperationSetting = () => { const deleteHistoryLogs = async () => { console.log(inputs); - const res = await API.delete(`/api/log/?target_timestamp=${Date.parse(historyTimestamp) / 1000}`); + const res = await API.delete( + `/api/log/?target_timestamp=${Date.parse(historyTimestamp) / 1000}`, + ); const { success, message, data } = res.data; if (success) { showSuccess(`${data} 条日志已清理!`); @@ -189,131 +219,129 @@ const OperationSetting = () => { <Grid columns={1}> <Grid.Column> <Form loading={loading}> - <Header as="h3"> - 通用设置 - </Header> + <Header as='h3'>通用设置</Header> <Form.Group widths={4}> <Form.Input - label="充值链接" - name="TopUpLink" + label='充值链接' + name='TopUpLink' onChange={handleInputChange} - autoComplete="new-password" + autoComplete='new-password' value={inputs.TopUpLink} - type="link" - placeholder="例如发卡网站的购买链接" + type='link' + placeholder='例如发卡网站的购买链接' /> <Form.Input - label="默认聊天页面链接" - name="ChatLink" + label='默认聊天页面链接' + name='ChatLink' onChange={handleInputChange} - autoComplete="new-password" + autoComplete='new-password' value={inputs.ChatLink} - type="link" - placeholder="例如 ChatGPT Next Web 的部署地址" + type='link' + placeholder='例如 ChatGPT Next Web 的部署地址' /> <Form.Input - label="聊天页面2链接" - name="ChatLink2" + label='聊天页面2链接' + name='ChatLink2' onChange={handleInputChange} - autoComplete="new-password" + autoComplete='new-password' value={inputs.ChatLink2} - type="link" - placeholder="例如 ChatGPT Web & Midjourney 的部署地址" + type='link' + placeholder='例如 ChatGPT Web & Midjourney 的部署地址' /> <Form.Input - label="单位美元额度" - name="QuotaPerUnit" + label='单位美元额度' + name='QuotaPerUnit' onChange={handleInputChange} - autoComplete="new-password" + autoComplete='new-password' value={inputs.QuotaPerUnit} - type="number" - step="0.01" - placeholder="一单位货币能兑换的额度" + type='number' + step='0.01' + placeholder='一单位货币能兑换的额度' /> <Form.Input - label="失败重试次数" - name="RetryTimes" + label='失败重试次数' + name='RetryTimes' type={'number'} - step="1" - min="0" + step='1' + min='0' onChange={handleInputChange} - autoComplete="new-password" + autoComplete='new-password' value={inputs.RetryTimes} - placeholder="失败重试次数" + placeholder='失败重试次数' /> </Form.Group> <Form.Group inline> <Form.Checkbox checked={inputs.DisplayInCurrencyEnabled === 'true'} - label="以货币形式显示额度" - name="DisplayInCurrencyEnabled" + label='以货币形式显示额度' + name='DisplayInCurrencyEnabled' onChange={handleInputChange} /> <Form.Checkbox checked={inputs.DisplayTokenStatEnabled === 'true'} - label="Billing 相关 API 显示令牌额度而非用户额度" - name="DisplayTokenStatEnabled" + label='Billing 相关 API 显示令牌额度而非用户额度' + name='DisplayTokenStatEnabled' onChange={handleInputChange} /> <Form.Checkbox checked={inputs.DefaultCollapseSidebar === 'true'} - label="默认折叠侧边栏" - name="DefaultCollapseSidebar" + label='默认折叠侧边栏' + name='DefaultCollapseSidebar' onChange={handleInputChange} /> </Form.Group> - <Form.Button onClick={() => { - submitConfig('general').then(); - }}>保存通用设置</Form.Button> + <Form.Button + onClick={() => { + submitConfig('general').then(); + }} + > + 保存通用设置 + </Form.Button> <Divider /> - <Header as="h3"> - 绘图设置 - </Header> + <Header as='h3'>绘图设置</Header> <Form.Group inline> <Form.Checkbox checked={inputs.DrawingEnabled === 'true'} - label="启用绘图功能" - name="DrawingEnabled" + label='启用绘图功能' + name='DrawingEnabled' onChange={handleInputChange} /> <Form.Checkbox checked={inputs.MjNotifyEnabled === 'true'} - label="允许回调(会泄露服务器ip地址)" - name="MjNotifyEnabled" + label='允许回调(会泄露服务器ip地址)' + name='MjNotifyEnabled' onChange={handleInputChange} /> </Form.Group> <Divider /> - <Header as="h3"> - 屏蔽词过滤设置 - </Header> + <Header as='h3'>屏蔽词过滤设置</Header> <Form.Group inline> <Form.Checkbox checked={inputs.CheckSensitiveEnabled === 'true'} - label="启用屏蔽词过滤功能" - name="CheckSensitiveEnabled" + label='启用屏蔽词过滤功能' + name='CheckSensitiveEnabled' onChange={handleInputChange} /> </Form.Group> <Form.Group inline> <Form.Checkbox checked={inputs.CheckSensitiveOnPromptEnabled === 'true'} - label="启用prompt检查" - name="CheckSensitiveOnPromptEnabled" + label='启用prompt检查' + name='CheckSensitiveOnPromptEnabled' onChange={handleInputChange} /> <Form.Checkbox checked={inputs.CheckSensitiveOnCompletionEnabled === 'true'} - label="启用生成内容检查" - name="CheckSensitiveOnCompletionEnabled" + label='启用生成内容检查' + name='CheckSensitiveOnCompletionEnabled' onChange={handleInputChange} /> </Form.Group> <Form.Group inline> <Form.Checkbox checked={inputs.StopOnSensitiveEnabled === 'true'} - label="在检测到屏蔽词时,立刻停止生成,否则替换屏蔽词" - name="StopOnSensitiveEnabled" + label='在检测到屏蔽词时,立刻停止生成,否则替换屏蔽词' + name='StopOnSensitiveEnabled' onChange={handleInputChange} /> </Form.Group> @@ -328,210 +356,223 @@ const OperationSetting = () => { {/* placeholder="例如:10"*/} {/* />*/} {/*</Form.Group>*/} - <Form.Group widths="equal"> + <Form.Group widths='equal'> <Form.TextArea - label="屏蔽词列表,一行一个屏蔽词,不需要符号分割" - name="SensitiveWords" + label='屏蔽词列表,一行一个屏蔽词,不需要符号分割' + name='SensitiveWords' onChange={handleInputChange} style={{ minHeight: 250, fontFamily: 'JetBrains Mono, Consolas' }} value={inputs.SensitiveWords} - placeholder="一行一个屏蔽词" + placeholder='一行一个屏蔽词' /> </Form.Group> - <Form.Button onClick={() => { - submitConfig('words').then(); - }}>保存屏蔽词设置</Form.Button> + <Form.Button + onClick={() => { + submitConfig('words').then(); + }} + > + 保存屏蔽词设置 + </Form.Button> <Divider /> - <Header as="h3"> - 日志设置 - </Header> + <Header as='h3'>日志设置</Header> <Form.Group inline> <Form.Checkbox checked={inputs.LogConsumeEnabled === 'true'} - label="启用额度消费日志记录" - name="LogConsumeEnabled" + label='启用额度消费日志记录' + name='LogConsumeEnabled' onChange={handleInputChange} /> </Form.Group> <Form.Group widths={4}> - <Form.Input label="目标时间" value={historyTimestamp} type="datetime-local" - name="history_timestamp" - onChange={(e, { name, value }) => { - setHistoryTimestamp(value); - }} /> + <Form.Input + label='目标时间' + value={historyTimestamp} + type='datetime-local' + name='history_timestamp' + onChange={(e, { name, value }) => { + setHistoryTimestamp(value); + }} + /> </Form.Group> - <Form.Button onClick={() => { - deleteHistoryLogs().then(); - }}>清理历史日志</Form.Button> + <Form.Button + onClick={() => { + deleteHistoryLogs().then(); + }} + > + 清理历史日志 + </Form.Button> <Divider /> - <Header as="h3"> - 数据看板 - </Header> + <Header as='h3'>数据看板</Header> <Form.Checkbox checked={inputs.DataExportEnabled === 'true'} - label="启用数据看板(实验性)" - name="DataExportEnabled" + label='启用数据看板(实验性)' + name='DataExportEnabled' onChange={handleInputChange} /> <Form.Group> <Form.Input - label="数据看板更新间隔(分钟,设置过短会影响数据库性能)" - name="DataExportInterval" + label='数据看板更新间隔(分钟,设置过短会影响数据库性能)' + name='DataExportInterval' type={'number'} - step="1" - min="1" + step='1' + min='1' onChange={handleInputChange} - autoComplete="new-password" + autoComplete='new-password' value={inputs.DataExportInterval} - placeholder="数据看板更新间隔(分钟,设置过短会影响数据库性能)" + placeholder='数据看板更新间隔(分钟,设置过短会影响数据库性能)' /> <Form.Select - label="数据看板默认时间粒度(仅修改展示粒度,统计精确到小时)" + label='数据看板默认时间粒度(仅修改展示粒度,统计精确到小时)' options={timeOptions} - name="DataExportDefaultTime" + name='DataExportDefaultTime' onChange={handleInputChange} - autoComplete="new-password" + autoComplete='new-password' value={inputs.DataExportDefaultTime} - placeholder="数据看板默认时间粒度" + placeholder='数据看板默认时间粒度' /> </Form.Group> <Divider /> - <Header as="h3"> - 监控设置 - </Header> + <Header as='h3'>监控设置</Header> <Form.Group widths={3}> <Form.Input - label="最长响应时间" - name="ChannelDisableThreshold" + label='最长响应时间' + name='ChannelDisableThreshold' onChange={handleInputChange} - autoComplete="new-password" + autoComplete='new-password' value={inputs.ChannelDisableThreshold} - type="number" - min="0" - placeholder="单位秒,当运行通道全部测试时,超过此时间将自动禁用通道" + type='number' + min='0' + placeholder='单位秒,当运行通道全部测试时,超过此时间将自动禁用通道' /> <Form.Input - label="额度提醒阈值" - name="QuotaRemindThreshold" + label='额度提醒阈值' + name='QuotaRemindThreshold' onChange={handleInputChange} - autoComplete="new-password" + autoComplete='new-password' value={inputs.QuotaRemindThreshold} - type="number" - min="0" - placeholder="低于此额度时将发送邮件提醒用户" + type='number' + min='0' + placeholder='低于此额度时将发送邮件提醒用户' /> </Form.Group> <Form.Group inline> <Form.Checkbox checked={inputs.AutomaticDisableChannelEnabled === 'true'} - label="失败时自动禁用通道" - name="AutomaticDisableChannelEnabled" + label='失败时自动禁用通道' + name='AutomaticDisableChannelEnabled' onChange={handleInputChange} /> <Form.Checkbox checked={inputs.AutomaticEnableChannelEnabled === 'true'} - label="成功时自动启用通道" - name="AutomaticEnableChannelEnabled" + label='成功时自动启用通道' + name='AutomaticEnableChannelEnabled' onChange={handleInputChange} /> </Form.Group> - <Form.Button onClick={() => { - submitConfig('monitor').then(); - }}>保存监控设置</Form.Button> + <Form.Button + onClick={() => { + submitConfig('monitor').then(); + }} + > + 保存监控设置 + </Form.Button> <Divider /> - <Header as="h3"> - 额度设置 - </Header> + <Header as='h3'>额度设置</Header> <Form.Group widths={4}> <Form.Input - label="新用户初始额度" - name="QuotaForNewUser" + label='新用户初始额度' + name='QuotaForNewUser' onChange={handleInputChange} - autoComplete="new-password" + autoComplete='new-password' value={inputs.QuotaForNewUser} - type="number" - min="0" - placeholder="例如:100" + type='number' + min='0' + placeholder='例如:100' /> <Form.Input - label="请求预扣费额度" - name="PreConsumedQuota" + label='请求预扣费额度' + name='PreConsumedQuota' onChange={handleInputChange} - autoComplete="new-password" + autoComplete='new-password' value={inputs.PreConsumedQuota} - type="number" - min="0" - placeholder="请求结束后多退少补" + type='number' + min='0' + placeholder='请求结束后多退少补' /> <Form.Input - label="邀请新用户奖励额度" - name="QuotaForInviter" + label='邀请新用户奖励额度' + name='QuotaForInviter' onChange={handleInputChange} - autoComplete="new-password" + autoComplete='new-password' value={inputs.QuotaForInviter} - type="number" - min="0" - placeholder="例如:2000" + type='number' + min='0' + placeholder='例如:2000' /> <Form.Input - label="新用户使用邀请码奖励额度" - name="QuotaForInvitee" + label='新用户使用邀请码奖励额度' + name='QuotaForInvitee' onChange={handleInputChange} - autoComplete="new-password" + autoComplete='new-password' value={inputs.QuotaForInvitee} - type="number" - min="0" - placeholder="例如:1000" + type='number' + min='0' + placeholder='例如:1000' /> </Form.Group> - <Form.Button onClick={() => { - submitConfig('quota').then(); - }}>保存额度设置</Form.Button> + <Form.Button + onClick={() => { + submitConfig('quota').then(); + }} + > + 保存额度设置 + </Form.Button> <Divider /> - <Header as="h3"> - 倍率设置 - </Header> - <Form.Group widths="equal"> + <Header as='h3'>倍率设置</Header> + <Form.Group widths='equal'> <Form.TextArea - label="模型固定价格(一次调用消耗多少刀,优先级大于模型倍率)" - name="ModelPrice" + label='模型固定价格(一次调用消耗多少刀,优先级大于模型倍率)' + name='ModelPrice' onChange={handleInputChange} style={{ minHeight: 250, fontFamily: 'JetBrains Mono, Consolas' }} - autoComplete="new-password" + autoComplete='new-password' value={inputs.ModelPrice} placeholder='为一个 JSON 文本,键为模型名称,值为一次调用消耗多少刀,比如 "gpt-4-gizmo-*": 0.1,一次消耗0.1刀' /> </Form.Group> - <Form.Group widths="equal"> + <Form.Group widths='equal'> <Form.TextArea - label="模型倍率" - name="ModelRatio" + label='模型倍率' + name='ModelRatio' onChange={handleInputChange} style={{ minHeight: 250, fontFamily: 'JetBrains Mono, Consolas' }} - autoComplete="new-password" + autoComplete='new-password' value={inputs.ModelRatio} - placeholder="为一个 JSON 文本,键为模型名称,值为倍率" + placeholder='为一个 JSON 文本,键为模型名称,值为倍率' /> </Form.Group> - <Form.Group widths="equal"> + <Form.Group widths='equal'> <Form.TextArea - label="分组倍率" - name="GroupRatio" + label='分组倍率' + name='GroupRatio' onChange={handleInputChange} style={{ minHeight: 250, fontFamily: 'JetBrains Mono, Consolas' }} - autoComplete="new-password" + autoComplete='new-password' value={inputs.GroupRatio} - placeholder="为一个 JSON 文本,键为分组名称,值为倍率" + placeholder='为一个 JSON 文本,键为分组名称,值为倍率' /> </Form.Group> - <Form.Button onClick={() => { - submitConfig('ratio').then(); - }}>保存倍率设置</Form.Button> + <Form.Button + onClick={() => { + submitConfig('ratio').then(); + }} + > + 保存倍率设置 + </Form.Button> </Form> </Grid.Column> </Grid> - ) - ; + ); }; export default OperationSetting; diff --git a/web/src/components/OtherSetting.js b/web/src/components/OtherSetting.js index 014492d..8ab4d7c 100644 --- a/web/src/components/OtherSetting.js +++ b/web/src/components/OtherSetting.js @@ -10,21 +10,20 @@ const OtherSetting = () => { Logo: '', Footer: '', About: '', - HomePageContent: '' + HomePageContent: '', }); let [loading, setLoading] = useState(false); const [showUpdateModal, setShowUpdateModal] = useState(false); const [updateData, setUpdateData] = useState({ tag_name: '', - content: '' + content: '', }); - const updateOption = async (key, value) => { setLoading(true); const res = await API.put('/api/option/', { key, - value + value, }); const { success, message } = res.data; if (success) { @@ -41,7 +40,7 @@ const OtherSetting = () => { Logo: false, HomePageContent: false, About: false, - Footer: false + Footer: false, }); const handleInputChange = async (value, e) => { const name = e.target.id; @@ -68,14 +67,20 @@ const OtherSetting = () => { // 个性化设置 - SystemName const submitSystemName = async () => { try { - setLoadingInput((loadingInput) => ({ ...loadingInput, SystemName: true })); + setLoadingInput((loadingInput) => ({ + ...loadingInput, + SystemName: true, + })); await updateOption('SystemName', inputs.SystemName); showSuccess('系统名称已更新'); } catch (error) { console.error('系统名称更新失败', error); showError('系统名称更新失败'); } finally { - setLoadingInput((loadingInput) => ({ ...loadingInput, SystemName: false })); + setLoadingInput((loadingInput) => ({ + ...loadingInput, + SystemName: false, + })); } }; @@ -95,14 +100,20 @@ const OtherSetting = () => { // 个性化设置 - 首页内容 const submitOption = async (key) => { try { - setLoadingInput((loadingInput) => ({ ...loadingInput, HomePageContent: true })); + setLoadingInput((loadingInput) => ({ + ...loadingInput, + HomePageContent: true, + })); await updateOption(key, inputs[key]); showSuccess('首页内容已更新'); } catch (error) { console.error('首页内容更新失败', error); showError('首页内容更新失败'); } finally { - setLoadingInput((loadingInput) => ({ ...loadingInput, HomePageContent: false })); + setLoadingInput((loadingInput) => ({ + ...loadingInput, + HomePageContent: false, + })); } }; // 个性化设置 - 关于 @@ -132,15 +143,13 @@ const OtherSetting = () => { } }; - const openGitHubRelease = () => { - window.location = - 'https://github.com/songquanpeng/one-api/releases/latest'; + window.location = 'https://github.com/songquanpeng/one-api/releases/latest'; }; const checkUpdate = async () => { 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; if (tag_name === process.env.REACT_APP_VERSION) { @@ -148,7 +157,7 @@ const OtherSetting = () => { } else { setUpdateData({ tag_name: tag_name, - content: marked.parse(body) + content: marked.parse(body), }); setShowUpdateModal(true); } @@ -175,13 +184,15 @@ const OtherSetting = () => { getOptions(); }, []); - return ( <Row> <Col span={24}> {/* 通用设置 */} - <Form values={inputs} getFormApi={formAPI => formAPISettingGeneral.current = formAPI} - style={{ marginBottom: 15 }}> + <Form + values={inputs} + getFormApi={(formAPI) => (formAPISettingGeneral.current = formAPI)} + style={{ marginBottom: 15 }} + > <Form.Section text={'通用设置'}> <Form.TextArea label={'公告'} @@ -191,12 +202,17 @@ const OtherSetting = () => { style={{ fontFamily: 'JetBrains Mono, Consolas' }} autosize={{ minRows: 6, maxRows: 12 }} /> - <Button onClick={submitNotice} loading={loadingInput['Notice']}>设置公告</Button> + <Button onClick={submitNotice} loading={loadingInput['Notice']}> + 设置公告 + </Button> </Form.Section> </Form> {/* 个性化设置 */} - <Form values={inputs} getFormApi={formAPI => formAPIPersonalization.current = formAPI} - style={{ marginBottom: 15 }}> + <Form + values={inputs} + getFormApi={(formAPI) => (formAPIPersonalization.current = formAPI)} + style={{ marginBottom: 15 }} + > <Form.Section text={'个性化设置'}> <Form.Input label={'系统名称'} @@ -204,48 +220,69 @@ const OtherSetting = () => { field={'SystemName'} onChange={handleInputChange} /> - <Button onClick={submitSystemName} loading={loadingInput['SystemName']}>设置系统名称</Button> + <Button + onClick={submitSystemName} + loading={loadingInput['SystemName']} + > + 设置系统名称 + </Button> <Form.Input label={'Logo 图片地址'} placeholder={'在此输入 Logo 图片地址'} field={'Logo'} onChange={handleInputChange} /> - <Button onClick={submitLogo} loading={loadingInput['Logo']}>设置 Logo</Button> + <Button onClick={submitLogo} loading={loadingInput['Logo']}> + 设置 Logo + </Button> <Form.TextArea label={'首页内容'} - placeholder={'在此输入首页内容,支持 Markdown & HTML 代码,设置后首页的状态信息将不再显示。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为首页。'} + placeholder={ + '在此输入首页内容,支持 Markdown & HTML 代码,设置后首页的状态信息将不再显示。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为首页。' + } field={'HomePageContent'} onChange={handleInputChange} style={{ fontFamily: 'JetBrains Mono, Consolas' }} autosize={{ minRows: 6, maxRows: 12 }} /> - <Button onClick={() => submitOption('HomePageContent')} - loading={loadingInput['HomePageContent']}>设置首页内容</Button> + <Button + onClick={() => submitOption('HomePageContent')} + loading={loadingInput['HomePageContent']} + > + 设置首页内容 + </Button> <Form.TextArea label={'关于'} - placeholder={'在此输入新的关于内容,支持 Markdown & HTML 代码。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为关于页面。'} + placeholder={ + '在此输入新的关于内容,支持 Markdown & HTML 代码。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为关于页面。' + } field={'About'} onChange={handleInputChange} style={{ fontFamily: 'JetBrains Mono, Consolas' }} autosize={{ minRows: 6, maxRows: 12 }} /> - <Button onClick={submitAbout} loading={loadingInput['About']}>设置关于</Button> + <Button onClick={submitAbout} loading={loadingInput['About']}> + 设置关于 + </Button> {/* */} <Banner fullMode={false} - type="info" - description="移除 One API 的版权标识必须首先获得授权,项目维护需要花费大量精力,如果本项目对你有意义,请主动支持本项目。" + type='info' + description='移除 One API 的版权标识必须首先获得授权,项目维护需要花费大量精力,如果本项目对你有意义,请主动支持本项目。' closeIcon={null} style={{ marginTop: 15 }} /> <Form.Input label={'页脚'} - placeholder={'在此输入新的页脚,留空则使用默认页脚,支持 HTML 代码'} + placeholder={ + '在此输入新的页脚,留空则使用默认页脚,支持 HTML 代码' + } field={'Footer'} onChange={handleInputChange} /> - <Button onClick={submitFooter} loading={loadingInput['Footer']}>设置页脚</Button> + <Button onClick={submitFooter} loading={loadingInput['Footer']}> + 设置页脚 + </Button> </Form.Section> </Form> </Col> diff --git a/web/src/components/PasswordResetConfirm.js b/web/src/components/PasswordResetConfirm.js index 071837a..222c8ad 100644 --- a/web/src/components/PasswordResetConfirm.js +++ b/web/src/components/PasswordResetConfirm.js @@ -6,7 +6,7 @@ import { useSearchParams } from 'react-router-dom'; const PasswordResetConfirm = () => { const [inputs, setInputs] = useState({ email: '', - token: '' + token: '', }); const { email, token } = inputs; @@ -23,7 +23,7 @@ const PasswordResetConfirm = () => { let email = searchParams.get('email'); setInputs({ token, - email + email, }); }, []); @@ -46,7 +46,7 @@ const PasswordResetConfirm = () => { setLoading(true); const res = await API.post(`/api/user/reset`, { email, - token + token, }); const { success, message } = res.data; if (success) { @@ -61,29 +61,29 @@ const PasswordResetConfirm = () => { } return ( - <Grid textAlign="center" style={{ marginTop: '48px' }}> + <Grid textAlign='center' style={{ marginTop: '48px' }}> <Grid.Column style={{ maxWidth: 450 }}> - <Header as="h2" color="" textAlign="center"> - <Image src="/logo.png" /> 密码重置确认 + <Header as='h2' color='' textAlign='center'> + <Image src='/logo.png' /> 密码重置确认 </Header> - <Form size="large"> + <Form size='large'> <Segment> <Form.Input fluid - icon="mail" - iconPosition="left" - placeholder="邮箱地址" - name="email" + icon='mail' + iconPosition='left' + placeholder='邮箱地址' + name='email' value={email} readOnly /> {newPassword && ( <Form.Input fluid - icon="lock" - iconPosition="left" - placeholder="新密码" - name="newPassword" + icon='lock' + iconPosition='left' + placeholder='新密码' + name='newPassword' value={newPassword} readOnly onClick={(e) => { @@ -94,9 +94,9 @@ const PasswordResetConfirm = () => { /> )} <Button - color="green" + color='green' fluid - size="large" + size='large' onClick={handleSubmit} loading={loading} disabled={disableButton} diff --git a/web/src/components/PasswordResetForm.js b/web/src/components/PasswordResetForm.js index ff3eaad..631d83b 100644 --- a/web/src/components/PasswordResetForm.js +++ b/web/src/components/PasswordResetForm.js @@ -5,7 +5,7 @@ import Turnstile from 'react-turnstile'; const PasswordResetForm = () => { const [inputs, setInputs] = useState({ - email: '' + email: '', }); const { email } = inputs; @@ -31,7 +31,7 @@ const PasswordResetForm = () => { function handleChange(e) { const { name, value } = e.target; - setInputs(inputs => ({ ...inputs, [name]: value })); + setInputs((inputs) => ({ ...inputs, [name]: value })); } async function handleSubmit(e) { @@ -43,7 +43,7 @@ const PasswordResetForm = () => { } setLoading(true); const res = await API.get( - `/api/reset_password?email=${email}&turnstile=${turnstileToken}` + `/api/reset_password?email=${email}&turnstile=${turnstileToken}`, ); const { success, message } = res.data; if (success) { @@ -56,19 +56,19 @@ const PasswordResetForm = () => { } return ( - <Grid textAlign="center" style={{ marginTop: '48px' }}> + <Grid textAlign='center' style={{ marginTop: '48px' }}> <Grid.Column style={{ maxWidth: 450 }}> - <Header as="h2" color="" textAlign="center"> - <Image src="/logo.png" /> 密码重置 + <Header as='h2' color='' textAlign='center'> + <Image src='/logo.png' /> 密码重置 </Header> - <Form size="large"> + <Form size='large'> <Segment> <Form.Input fluid - icon="mail" - iconPosition="left" - placeholder="邮箱地址" - name="email" + icon='mail' + iconPosition='left' + placeholder='邮箱地址' + name='email' value={email} onChange={handleChange} /> @@ -83,9 +83,9 @@ const PasswordResetForm = () => { <></> )} <Button - color="green" + color='green' fluid - size="large" + size='large' onClick={handleSubmit} loading={loading} disabled={disableButton} diff --git a/web/src/components/PersonalSetting.js b/web/src/components/PersonalSetting.js index 85e21b6..ded9ad0 100644 --- a/web/src/components/PersonalSetting.js +++ b/web/src/components/PersonalSetting.js @@ -1,6 +1,13 @@ import React, { useContext, useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import { API, copy, isRoot, showError, showInfo, showSuccess } from '../helpers'; +import { + API, + copy, + isRoot, + showError, + showInfo, + showSuccess, +} from '../helpers'; import Turnstile from 'react-turnstile'; import { UserContext } from '../context/User'; import { onGitHubOAuthClicked } from './utils'; @@ -17,9 +24,14 @@ import { Modal, Space, Tag, - Typography + Typography, } from '@douyinfe/semi-ui'; -import { getQuotaPerUnit, renderQuota, renderQuotaWithPrompt, stringToColor } from '../helpers/render'; +import { + getQuotaPerUnit, + renderQuota, + renderQuotaWithPrompt, + stringToColor, +} from '../helpers/render'; import TelegramLoginButton from 'react-telegram-login'; const PersonalSetting = () => { @@ -32,7 +44,7 @@ const PersonalSetting = () => { email: '', self_account_deletion_confirmation: '', set_new_password: '', - set_new_password_confirmation: '' + set_new_password_confirmation: '', }); const [status, setStatus] = useState({}); const [showChangePasswordModal, setShowChangePasswordModal] = useState(false); @@ -67,11 +79,9 @@ const PersonalSetting = () => { setTurnstileSiteKey(status.turnstile_site_key); } } - getUserData().then( - (res) => { - console.log(userState); - } - ); + getUserData().then((res) => { + console.log(userState); + }); loadModels().then(); getAffLink().then(); setTransferAmount(getQuotaPerUnit()); @@ -173,7 +183,7 @@ const PersonalSetting = () => { const bindWeChat = async () => { if (inputs.wechat_verification_code === '') return; 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; if (success) { @@ -189,12 +199,9 @@ const PersonalSetting = () => { showError('两次输入的密码不一致!'); return; } - const res = await API.put( - `/api/user/self`, - { - password: inputs.set_new_password - } - ); + const res = await API.put(`/api/user/self`, { + password: inputs.set_new_password, + }); const { success, message } = res.data; if (success) { showSuccess('密码修改成功!'); @@ -210,12 +217,9 @@ const PersonalSetting = () => { showError('划转金额最低为' + renderQuota(getQuotaPerUnit())); return; } - const res = await API.post( - `/api/user/aff_transfer`, - { - quota: transferAmount - } - ); + const res = await API.post(`/api/user/aff_transfer`, { + quota: transferAmount, + }); const { success, message } = res.data; if (success) { showSuccess(message); @@ -238,7 +242,7 @@ const PersonalSetting = () => { } setLoading(true); const res = await API.get( - `/api/verification?email=${inputs.email}&turnstile=${turnstileToken}` + `/api/verification?email=${inputs.email}&turnstile=${turnstileToken}`, ); const { success, message } = res.data; if (success) { @@ -256,7 +260,7 @@ const PersonalSetting = () => { } setLoading(true); 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; if (success) { @@ -295,7 +299,7 @@ const PersonalSetting = () => { <Layout> <Layout.Content> <Modal - title="请输入要划转的数量" + title='请输入要划转的数量' visible={openTransfer} onOk={transfer} onCancel={handleCancel} @@ -305,13 +309,25 @@ const PersonalSetting = () => { > <div style={{ marginTop: 20 }}> <Typography.Text>{`可用额度${renderQuotaWithPrompt(userState?.user?.aff_quota)}`}</Typography.Text> - <Input style={{ marginTop: 5 }} value={userState?.user?.aff_quota} disabled={true}></Input> + <Input + style={{ marginTop: 5 }} + value={userState?.user?.aff_quota} + disabled={true} + ></Input> </div> <div style={{ marginTop: 20 }}> - <Typography.Text>{`划转额度${renderQuotaWithPrompt(transferAmount)} 最低` + renderQuota(getQuotaPerUnit())}</Typography.Text> + <Typography.Text> + {`划转额度${renderQuotaWithPrompt(transferAmount)} 最低` + + renderQuota(getQuotaPerUnit())} + </Typography.Text> <div> - <InputNumber min={0} style={{ marginTop: 5 }} value={transferAmount} - onChange={(value) => setTransferAmount(value)} disabled={false}></InputNumber> + <InputNumber + min={0} + style={{ marginTop: 5 }} + value={transferAmount} + onChange={(value) => setTransferAmount(value)} + disabled={false} + ></InputNumber> </div> </div> </Modal> @@ -319,27 +335,45 @@ const PersonalSetting = () => { <Card title={ <Card.Meta - avatar={<Avatar size="default" color={stringToColor(getUsername())} - style={{ marginRight: 4 }}> - {typeof getUsername() === 'string' && getUsername().slice(0, 1)} - </Avatar>} + avatar={ + <Avatar + size='default' + color={stringToColor(getUsername())} + style={{ marginRight: 4 }} + > + {typeof getUsername() === 'string' && + getUsername().slice(0, 1)} + </Avatar> + } title={<Typography.Text>{getUsername()}</Typography.Text>} - description={isRoot() ? <Tag color="red">管理员</Tag> : <Tag color="blue">普通用户</Tag>} + description={ + isRoot() ? ( + <Tag color='red'>管理员</Tag> + ) : ( + <Tag color='blue'>普通用户</Tag> + ) + } ></Card.Meta> } headerExtraContent={ <> - <Space vertical align="start"> - <Tag color="green">{'ID: ' + userState?.user?.id}</Tag> - <Tag color="blue">{userState?.user?.group}</Tag> + <Space vertical align='start'> + <Tag color='green'>{'ID: ' + userState?.user?.id}</Tag> + <Tag color='blue'>{userState?.user?.group}</Tag> </Space> </> } footer={ <Descriptions row> - <Descriptions.Item itemKey="当前余额">{renderQuota(userState?.user?.quota)}</Descriptions.Item> - <Descriptions.Item itemKey="历史消耗">{renderQuota(userState?.user?.used_quota)}</Descriptions.Item> - <Descriptions.Item itemKey="请求次数">{userState.user?.request_count}</Descriptions.Item> + <Descriptions.Item itemKey='当前余额'> + {renderQuota(userState?.user?.quota)} + </Descriptions.Item> + <Descriptions.Item itemKey='历史消耗'> + {renderQuota(userState?.user?.used_quota)} + </Descriptions.Item> + <Descriptions.Item itemKey='请求次数'> + {userState.user?.request_count} + </Descriptions.Item> </Descriptions> } > @@ -347,15 +381,18 @@ const PersonalSetting = () => { <div style={{ marginTop: 10 }}> <Space wrap> {models.map((model) => ( - <Tag key={model} color="cyan" onClick={() => { - copyText(model); - }}> + <Tag + key={model} + color='cyan' + onClick={() => { + copyText(model); + }} + > {model} </Tag> ))} </Space> </div> - </Card> <Card footer={ @@ -373,18 +410,25 @@ const PersonalSetting = () => { <Typography.Title heading={6}>邀请信息</Typography.Title> <div style={{ marginTop: 10 }}> <Descriptions row> - <Descriptions.Item itemKey="待使用收益"> - <span style={{ color: 'rgba(var(--semi-red-5), 1)' }}> - { - renderQuota(userState?.user?.aff_quota) - } - </span> - <Button type={'secondary'} onClick={() => setOpenTransfer(true)} size={'small'} - style={{ marginLeft: 10 }}>划转</Button> + <Descriptions.Item itemKey='待使用收益'> + <span style={{ color: 'rgba(var(--semi-red-5), 1)' }}> + {renderQuota(userState?.user?.aff_quota)} + </span> + <Button + type={'secondary'} + onClick={() => setOpenTransfer(true)} + size={'small'} + style={{ marginLeft: 10 }} + > + 划转 + </Button> + </Descriptions.Item> + <Descriptions.Item itemKey='总收益'> + {renderQuota(userState?.user?.aff_history_quota)} + </Descriptions.Item> + <Descriptions.Item itemKey='邀请人数'> + {userState?.user?.aff_count} </Descriptions.Item> - <Descriptions.Item - itemKey="总收益">{renderQuota(userState?.user?.aff_history_quota)}</Descriptions.Item> - <Descriptions.Item itemKey="邀请人数">{userState?.user?.aff_count}</Descriptions.Item> </Descriptions> </div> </Card> @@ -392,46 +436,71 @@ const PersonalSetting = () => { <Typography.Title heading={6}>个人信息</Typography.Title> <div style={{ marginTop: 20 }}> <Typography.Text strong>邮箱</Typography.Text> - <div style={{ display: 'flex', justifyContent: 'space-between' }}> + <div + style={{ display: 'flex', justifyContent: 'space-between' }} + > <div> <Input - value={userState.user && userState.user.email !== '' ? userState.user.email : '未绑定'} + value={ + userState.user && userState.user.email !== '' + ? userState.user.email + : '未绑定' + } readonly={true} ></Input> </div> <div> - <Button onClick={() => { - setShowEmailBindModal(true); - }}>{ - userState.user && userState.user.email !== '' ? '修改绑定' : '绑定邮箱' - }</Button> + <Button + onClick={() => { + setShowEmailBindModal(true); + }} + > + {userState.user && userState.user.email !== '' + ? '修改绑定' + : '绑定邮箱'} + </Button> </div> </div> </div> <div style={{ marginTop: 10 }}> <Typography.Text strong>微信</Typography.Text> - <div style={{ display: 'flex', justifyContent: 'space-between' }}> + <div + style={{ display: 'flex', justifyContent: 'space-between' }} + > <div> <Input - value={userState.user && userState.user.wechat_id !== '' ? '已绑定' : '未绑定'} + value={ + userState.user && userState.user.wechat_id !== '' + ? '已绑定' + : '未绑定' + } readonly={true} ></Input> </div> <div> - <Button disabled={(userState.user && userState.user.wechat_id !== '') || !status.wechat_login}> - { - status.wechat_login ? '绑定' : '未启用' + <Button + disabled={ + (userState.user && userState.user.wechat_id !== '') || + !status.wechat_login } + > + {status.wechat_login ? '绑定' : '未启用'} </Button> </div> </div> </div> <div style={{ marginTop: 10 }}> <Typography.Text strong>GitHub</Typography.Text> - <div style={{ display: 'flex', justifyContent: 'space-between' }}> + <div + style={{ display: 'flex', justifyContent: 'space-between' }} + > <div> <Input - value={userState.user && userState.user.github_id !== '' ? userState.user.github_id : '未绑定'} + value={ + userState.user && userState.user.github_id !== '' + ? userState.user.github_id + : '未绑定' + } readonly={true} ></Input> </div> @@ -440,11 +509,12 @@ const PersonalSetting = () => { onClick={() => { onGitHubOAuthClicked(status.github_client_id); }} - disabled={(userState.user && userState.user.github_id !== '') || !status.github_oauth} - > - { - status.github_oauth ? '绑定' : '未启用' + disabled={ + (userState.user && userState.user.github_id !== '') || + !status.github_oauth } + > + {status.github_oauth ? '绑定' : '未启用'} </Button> </div> </div> @@ -452,33 +522,56 @@ const PersonalSetting = () => { <div style={{ marginTop: 10 }}> <Typography.Text strong>Telegram</Typography.Text> - <div style={{ display: 'flex', justifyContent: 'space-between' }}> + <div + style={{ display: 'flex', justifyContent: 'space-between' }} + > <div> <Input - value={userState.user && userState.user.telegram_id !== '' ? userState.user.telegram_id : '未绑定'} + value={ + userState.user && userState.user.telegram_id !== '' + ? userState.user.telegram_id + : '未绑定' + } readonly={true} ></Input> </div> <div> - {status.telegram_oauth ? - userState.user.telegram_id !== '' ? <Button disabled={true}>已绑定</Button> - : <TelegramLoginButton dataAuthUrl="/api/oauth/telegram/bind" - botName={status.telegram_bot_name} /> - : <Button disabled={true}>未启用</Button> - } + {status.telegram_oauth ? ( + userState.user.telegram_id !== '' ? ( + <Button disabled={true}>已绑定</Button> + ) : ( + <TelegramLoginButton + dataAuthUrl='/api/oauth/telegram/bind' + botName={status.telegram_bot_name} + /> + ) + ) : ( + <Button disabled={true}>未启用</Button> + )} </div> </div> </div> <div style={{ marginTop: 10 }}> <Space> - <Button onClick={generateAccessToken}>生成系统访问令牌</Button> - <Button onClick={() => { - setShowChangePasswordModal(true); - }}>修改密码</Button> - <Button type={'danger'} onClick={() => { - setShowAccountDeleteModal(true); - }}>删除个人账户</Button> + <Button onClick={generateAccessToken}> + 生成系统访问令牌 + </Button> + <Button + onClick={() => { + setShowChangePasswordModal(true); + }} + > + 修改密码 + </Button> + <Button + type={'danger'} + onClick={() => { + setShowAccountDeleteModal(true); + }} + > + 删除个人账户 + </Button> </Space> {systemToken && ( @@ -489,17 +582,15 @@ const PersonalSetting = () => { style={{ marginTop: '10px' }} /> )} - { - status.wechat_login && ( - <Button - onClick={() => { - setShowWeChatBindModal(true); - }} - > - 绑定微信账号 - </Button> - ) - } + {status.wechat_login && ( + <Button + onClick={() => { + setShowWeChatBindModal(true); + }} + > + 绑定微信账号 + </Button> + )} <Modal onCancel={() => setShowWeChatBindModal(false)} // onOpen={() => setShowWeChatBindModal(true)} @@ -513,12 +604,14 @@ const PersonalSetting = () => { </p> </div> <Input - placeholder="验证码" - name="wechat_verification_code" + placeholder='验证码' + name='wechat_verification_code' value={inputs.wechat_verification_code} - onChange={(v) => handleInputChange('wechat_verification_code', v)} + onChange={(v) => + handleInputChange('wechat_verification_code', v) + } /> - <Button color="" fluid size="large" onClick={bindWeChat}> + <Button color='' fluid size='large' onClick={bindWeChat}> 绑定 </Button> </Modal> @@ -534,26 +627,36 @@ const PersonalSetting = () => { maskClosable={false} > <Typography.Title heading={6}>绑定邮箱地址</Typography.Title> - <div style={{ marginTop: 20, display: 'flex', justifyContent: 'space-between' }}> + <div + style={{ + marginTop: 20, + display: 'flex', + justifyContent: 'space-between', + }} + > <Input fluid - placeholder="输入邮箱地址" + placeholder='输入邮箱地址' onChange={(value) => handleInputChange('email', value)} - name="email" - type="email" + name='email' + type='email' /> - <Button onClick={sendVerificationCode} - disabled={disableButton || loading}> + <Button + onClick={sendVerificationCode} + disabled={disableButton || loading} + > {disableButton ? `重新发送(${countdown})` : '获取验证码'} </Button> </div> <div style={{ marginTop: 10 }}> <Input fluid - placeholder="验证码" - name="email_verification_code" + placeholder='验证码' + name='email_verification_code' value={inputs.email_verification_code} - onChange={(value) => handleInputChange('email_verification_code', value)} + onChange={(value) => + handleInputChange('email_verification_code', value) + } /> </div> {turnstileEnabled ? ( @@ -576,17 +679,22 @@ const PersonalSetting = () => { > <div style={{ marginTop: 20 }}> <Banner - type="danger" - description="您正在删除自己的帐户,将清空所有数据且不可恢复" + type='danger' + description='您正在删除自己的帐户,将清空所有数据且不可恢复' closeIcon={null} /> </div> <div style={{ marginTop: 20 }}> <Input placeholder={`输入你的账户名 ${userState?.user?.username} 以确认删除`} - name="self_account_deletion_confirmation" + name='self_account_deletion_confirmation' value={inputs.self_account_deletion_confirmation} - onChange={(value) => handleInputChange('self_account_deletion_confirmation', value)} + onChange={(value) => + handleInputChange( + 'self_account_deletion_confirmation', + value, + ) + } /> {turnstileEnabled ? ( <Turnstile @@ -609,17 +717,21 @@ const PersonalSetting = () => { > <div style={{ marginTop: 20 }}> <Input - name="set_new_password" - placeholder="新密码" + name='set_new_password' + placeholder='新密码' value={inputs.set_new_password} - onChange={(value) => handleInputChange('set_new_password', value)} + onChange={(value) => + handleInputChange('set_new_password', value) + } /> <Input style={{ marginTop: 20 }} - name="set_new_password_confirmation" - placeholder="确认新密码" + name='set_new_password_confirmation' + placeholder='确认新密码' value={inputs.set_new_password_confirmation} - onChange={(value) => handleInputChange('set_new_password_confirmation', value)} + onChange={(value) => + handleInputChange('set_new_password_confirmation', value) + } /> {turnstileEnabled ? ( <Turnstile @@ -634,7 +746,6 @@ const PersonalSetting = () => { </div> </Modal> </div> - </Layout.Content> </Layout> </div> diff --git a/web/src/components/PrivateRoute.js b/web/src/components/PrivateRoute.js index 9ef826c..ca938c4 100644 --- a/web/src/components/PrivateRoute.js +++ b/web/src/components/PrivateRoute.js @@ -2,12 +2,11 @@ import { Navigate } from 'react-router-dom'; import { history } from '../helpers'; - function PrivateRoute({ children }) { if (!localStorage.getItem('user')) { - return <Navigate to="/login" state={{ from: history.location }} />; + return <Navigate to='/login' state={{ from: history.location }} />; } return children; } -export { PrivateRoute }; \ No newline at end of file +export { PrivateRoute }; diff --git a/web/src/components/RedemptionsTable.js b/web/src/components/RedemptionsTable.js index 8c9c96a..09d812b 100644 --- a/web/src/components/RedemptionsTable.js +++ b/web/src/components/RedemptionsTable.js @@ -1,29 +1,58 @@ import React, { useEffect, useState } from 'react'; -import { API, copy, showError, showSuccess, timestamp2string } from '../helpers'; +import { + API, + copy, + showError, + showSuccess, + timestamp2string, +} from '../helpers'; import { ITEMS_PER_PAGE } from '../constants'; import { renderQuota } from '../helpers/render'; -import { Button, Form, Modal, Popconfirm, Popover, Table, Tag } from '@douyinfe/semi-ui'; +import { + Button, + Form, + Modal, + Popconfirm, + Popover, + Table, + Tag, +} from '@douyinfe/semi-ui'; import EditRedemption from '../pages/Redemption/EditRedemption'; function renderTimestamp(timestamp) { - return ( - <> - {timestamp2string(timestamp)} - </> - ); + return <>{timestamp2string(timestamp)}</>; } function renderStatus(status) { switch (status) { case 1: - return <Tag color="green" size="large">未使用</Tag>; + return ( + <Tag color='green' size='large'> + 未使用 + </Tag> + ); case 2: - return <Tag color="red" size="large"> 已禁用 </Tag>; + return ( + <Tag color='red' size='large'> + {' '} + 已禁用{' '} + </Tag> + ); case 3: - return <Tag color="grey" size="large"> 已使用 </Tag>; + return ( + <Tag color='grey' size='large'> + {' '} + 已使用{' '} + </Tag> + ); default: - return <Tag color="black" size="large"> 未知状态 </Tag>; + return ( + <Tag color='black' size='large'> + {' '} + 未知状态{' '} + </Tag> + ); } } @@ -31,121 +60,115 @@ const RedemptionsTable = () => { const columns = [ { title: 'ID', - dataIndex: 'id' + dataIndex: 'id', }, { title: '名称', - dataIndex: 'name' + dataIndex: 'name', }, { title: '状态', dataIndex: 'status', key: 'status', render: (text, record, index) => { - return ( - <div> - {renderStatus(text)} - </div> - ); - } + return <div>{renderStatus(text)}</div>; + }, }, { title: '额度', dataIndex: 'quota', render: (text, record, index) => { - return ( - <div> - {renderQuota(parseInt(text))} - </div> - ); - } + return <div>{renderQuota(parseInt(text))}</div>; + }, }, { title: '创建时间', dataIndex: 'created_time', render: (text, record, index) => { - return ( - <div> - {renderTimestamp(text)} - </div> - ); - } + return <div>{renderTimestamp(text)}</div>; + }, }, { title: '兑换人ID', dataIndex: 'used_user_id', render: (text, record, index) => { - return ( - <div> - {text === 0 ? '无' : text} - </div> - ); - } + return <div>{text === 0 ? '无' : text}</div>; + }, }, { title: '', dataIndex: 'operate', render: (text, record, index) => ( <div> - <Popover - content={ - record.key - } - style={{ padding: 20 }} - position="top" - > - <Button theme="light" type="tertiary" style={{ marginRight: 1 }}>查看</Button> + <Popover content={record.key} style={{ padding: 20 }} position='top'> + <Button theme='light' type='tertiary' style={{ marginRight: 1 }}> + 查看 + </Button> </Popover> - <Button theme="light" type="secondary" style={{ marginRight: 1 }} - onClick={async (text) => { - await copyText(record.key); - }} - >复制</Button> + <Button + theme='light' + type='secondary' + style={{ marginRight: 1 }} + onClick={async (text) => { + await copyText(record.key); + }} + > + 复制 + </Button> <Popconfirm - title="确定是否要删除此兑换码?" - content="此修改将不可逆" + title='确定是否要删除此兑换码?' + content='此修改将不可逆' okType={'danger'} position={'left'} onConfirm={() => { - manageRedemption(record.id, 'delete', record).then( - () => { - removeRecord(record.key); - } - ); + manageRedemption(record.id, 'delete', record).then(() => { + removeRecord(record.key); + }); }} > - <Button theme="light" type="danger" style={{ marginRight: 1 }}>删除</Button> + <Button theme='light' type='danger' style={{ marginRight: 1 }}> + 删除 + </Button> </Popconfirm> - { - record.status === 1 ? - <Button theme="light" type="warning" style={{ marginRight: 1 }} onClick={ - async () => { - manageRedemption( - record.id, - 'disable', - record - ); - } - }>禁用</Button> : - <Button theme="light" type="secondary" style={{ marginRight: 1 }} onClick={ - async () => { - manageRedemption( - record.id, - 'enable', - record - ); - } - } disabled={record.status === 3}>启用</Button> - } - <Button theme="light" type="tertiary" style={{ marginRight: 1 }} onClick={ - () => { + {record.status === 1 ? ( + <Button + theme='light' + type='warning' + style={{ marginRight: 1 }} + onClick={async () => { + manageRedemption(record.id, 'disable', record); + }} + > + 禁用 + </Button> + ) : ( + <Button + theme='light' + type='secondary' + style={{ marginRight: 1 }} + onClick={async () => { + manageRedemption(record.id, 'enable', record); + }} + disabled={record.status === 3} + > + 启用 + </Button> + )} + <Button + theme='light' + type='tertiary' + style={{ marginRight: 1 }} + onClick={() => { setEditingRedemption(record); setShowEdit(true); - } - } disabled={record.status !== 1}>编辑</Button> + }} + disabled={record.status !== 1} + > + 编辑 + </Button> </div> - ) - } + ), + }, ]; const [redemptions, setRedemptions] = useState([]); @@ -156,7 +179,7 @@ const RedemptionsTable = () => { const [tokenCount, setTokenCount] = useState(ITEMS_PER_PAGE); const [selectedKeys, setSelectedKeys] = useState([]); const [editingRedemption, setEditingRedemption] = useState({ - id: undefined + id: undefined, }); const [showEdit, setShowEdit] = useState(false); @@ -178,7 +201,7 @@ const RedemptionsTable = () => { // } // data.key = '' + data.id setRedemptions(redeptions); - if (redeptions.length >= (activePage) * ITEMS_PER_PAGE) { + if (redeptions.length >= activePage * ITEMS_PER_PAGE) { setTokenCount(redeptions.length + 1); } else { setTokenCount(redeptions.length); @@ -202,10 +225,10 @@ const RedemptionsTable = () => { setLoading(false); }; - const removeRecord = key => { + const removeRecord = (key) => { let newDataSource = [...redemptions]; if (key != null) { - let idx = newDataSource.findIndex(data => data.key === key); + let idx = newDataSource.findIndex((data) => data.key === key); if (idx > -1) { newDataSource.splice(idx, 1); @@ -268,7 +291,6 @@ const RedemptionsTable = () => { let newRedemptions = [...redemptions]; // let realIdx = (activePage - 1) * ITEMS_PER_PAGE + idx; if (action === 'delete') { - } else { record.status = redemption.status; } @@ -286,7 +308,9 @@ const RedemptionsTable = () => { return; } 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; if (success) { setRedemptions(data); @@ -315,32 +339,32 @@ const RedemptionsTable = () => { setLoading(false); }; - const handlePageChange = page => { + const handlePageChange = (page) => { setActivePage(page); if (page === Math.ceil(redemptions.length / ITEMS_PER_PAGE) + 1) { // In this case we have to load more data and then append them. - loadRedemptions(page - 1).then(r => { - }); + loadRedemptions(page - 1).then((r) => {}); } }; - let pageData = redemptions.slice((activePage - 1) * ITEMS_PER_PAGE, activePage * ITEMS_PER_PAGE); + let pageData = redemptions.slice( + (activePage - 1) * ITEMS_PER_PAGE, + activePage * ITEMS_PER_PAGE, + ); const rowSelection = { - onSelect: (record, selected) => { - }, - onSelectAll: (selected, selectedRows) => { - }, + onSelect: (record, selected) => {}, + onSelectAll: (selected, selectedRows) => {}, onChange: (selectedRowKeys, selectedRows) => { setSelectedKeys(selectedRows); - } + }, }; const handleRow = (record, index) => { if (record.status !== 1) { return { style: { - background: 'var(--semi-color-disabled-border)' - } + background: 'var(--semi-color-disabled-border)', + }, }; } else { return {}; @@ -349,45 +373,64 @@ const RedemptionsTable = () => { return ( <> - <EditRedemption refresh={refresh} editingRedemption={editingRedemption} visiable={showEdit} - handleClose={closeEdit}></EditRedemption> + <EditRedemption + refresh={refresh} + editingRedemption={editingRedemption} + visiable={showEdit} + handleClose={closeEdit} + ></EditRedemption> <Form onSubmit={searchRedemptions}> <Form.Input - label="搜索关键字" - field="keyword" - icon="search" - iconPosition="left" - placeholder="关键字(id或者名称)" + label='搜索关键字' + field='keyword' + icon='search' + iconPosition='left' + placeholder='关键字(id或者名称)' value={searchKeyword} loading={searching} onChange={handleKeywordChange} /> </Form> - <Table style={{ marginTop: 20 }} columns={columns} dataSource={pageData} pagination={{ - currentPage: activePage, - pageSize: ITEMS_PER_PAGE, - total: tokenCount, - // showSizeChanger: true, - // pageSizeOptions: [10, 20, 50, 100], - formatPageText: (page) => `第 ${page.currentStart} - ${page.currentEnd} 条,共 ${redemptions.length} 条`, - // onPageSizeChange: (size) => { - // setPageSize(size); - // setActivePage(1); - // }, - onPageChange: handlePageChange - }} loading={loading} rowSelection={rowSelection} onRow={handleRow}> - </Table> - <Button theme="light" type="primary" style={{ marginRight: 8 }} onClick={ - () => { + <Table + style={{ marginTop: 20 }} + columns={columns} + dataSource={pageData} + pagination={{ + currentPage: activePage, + pageSize: ITEMS_PER_PAGE, + total: tokenCount, + // showSizeChanger: true, + // pageSizeOptions: [10, 20, 50, 100], + formatPageText: (page) => + `第 ${page.currentStart} - ${page.currentEnd} 条,共 ${redemptions.length} 条`, + // onPageSizeChange: (size) => { + // setPageSize(size); + // setActivePage(1); + // }, + onPageChange: handlePageChange, + }} + loading={loading} + rowSelection={rowSelection} + onRow={handleRow} + ></Table> + <Button + theme='light' + type='primary' + style={{ marginRight: 8 }} + onClick={() => { setEditingRedemption({ - id: undefined + id: undefined, }); setShowEdit(true); - } - }>添加兑换码</Button> - <Button label="复制所选兑换码" type="warning" onClick={ - async () => { + }} + > + 添加兑换码 + </Button> + <Button + label='复制所选兑换码' + type='warning' + onClick={async () => { if (selectedKeys.length === 0) { showError('请至少选择一个兑换码!'); return; @@ -397,8 +440,10 @@ const RedemptionsTable = () => { keys += selectedKeys[i].name + ' ' + selectedKeys[i].key + '\n'; } await copyText(keys); - } - }>复制所选兑换码到剪贴板</Button> + }} + > + 复制所选兑换码到剪贴板 + </Button> </> ); }; diff --git a/web/src/components/RegisterForm.js b/web/src/components/RegisterForm.js index 1f26b63..fcd2638 100644 --- a/web/src/components/RegisterForm.js +++ b/web/src/components/RegisterForm.js @@ -1,5 +1,13 @@ import React, { useEffect, useState } from 'react'; -import { Button, Form, Grid, Header, Image, Message, Segment } from 'semantic-ui-react'; +import { + Button, + Form, + Grid, + Header, + Image, + Message, + Segment, +} from 'semantic-ui-react'; import { Link, useNavigate } from 'react-router-dom'; import { API, getLogo, showError, showInfo, showSuccess } from '../helpers'; import Turnstile from 'react-turnstile'; @@ -10,7 +18,7 @@ const RegisterForm = () => { password: '', password2: '', email: '', - verification_code: '' + verification_code: '', }); const { username, password, password2 } = inputs; const [showEmailVerification, setShowEmailVerification] = useState(false); @@ -65,7 +73,7 @@ const RegisterForm = () => { inputs.aff_code = affCode; const res = await API.post( `/api/user/register?turnstile=${turnstileToken}`, - inputs + inputs, ); const { success, message } = res.data; if (success) { @@ -86,7 +94,7 @@ const RegisterForm = () => { } setLoading(true); const res = await API.get( - `/api/verification?email=${inputs.email}&turnstile=${turnstileToken}` + `/api/verification?email=${inputs.email}&turnstile=${turnstileToken}`, ); const { success, message } = res.data; if (success) { @@ -98,49 +106,49 @@ const RegisterForm = () => { }; return ( - <Grid textAlign="center" style={{ marginTop: '48px' }}> + <Grid textAlign='center' style={{ marginTop: '48px' }}> <Grid.Column style={{ maxWidth: 450 }}> - <Header as="h2" color="" textAlign="center"> + <Header as='h2' color='' textAlign='center'> <Image src={logo} /> 新用户注册 </Header> - <Form size="large"> + <Form size='large'> <Segment> <Form.Input fluid - icon="user" - iconPosition="left" - placeholder="输入用户名,最长 12 位" + icon='user' + iconPosition='left' + placeholder='输入用户名,最长 12 位' onChange={handleChange} - name="username" + name='username' /> <Form.Input fluid - icon="lock" - iconPosition="left" - placeholder="输入密码,最短 8 位,最长 20 位" + icon='lock' + iconPosition='left' + placeholder='输入密码,最短 8 位,最长 20 位' onChange={handleChange} - name="password" - type="password" + name='password' + type='password' /> <Form.Input fluid - icon="lock" - iconPosition="left" - placeholder="输入密码,最短 8 位,最长 20 位" + icon='lock' + iconPosition='left' + placeholder='输入密码,最短 8 位,最长 20 位' onChange={handleChange} - name="password2" - type="password" + name='password2' + type='password' /> {showEmailVerification ? ( <> <Form.Input fluid - icon="mail" - iconPosition="left" - placeholder="输入邮箱地址" + icon='mail' + iconPosition='left' + placeholder='输入邮箱地址' onChange={handleChange} - name="email" - type="email" + name='email' + type='email' action={ <Button onClick={sendVerificationCode} disabled={loading}> 获取验证码 @@ -149,11 +157,11 @@ const RegisterForm = () => { /> <Form.Input fluid - icon="lock" - iconPosition="left" - placeholder="输入验证码" + icon='lock' + iconPosition='left' + placeholder='输入验证码' onChange={handleChange} - name="verification_code" + name='verification_code' /> </> ) : ( @@ -170,9 +178,9 @@ const RegisterForm = () => { <></> )} <Button - color="green" + color='green' fluid - size="large" + size='large' onClick={handleSubmit} loading={loading} > @@ -182,7 +190,7 @@ const RegisterForm = () => { </Form> <Message> 已有账户? - <Link to="/login" className="btn btn-link"> + <Link to='/login' className='btn btn-link'> 点击登录 </Link> </Message> diff --git a/web/src/components/SiderBar.js b/web/src/components/SiderBar.js index 78120aa..e0b2ae3 100644 --- a/web/src/components/SiderBar.js +++ b/web/src/components/SiderBar.js @@ -3,7 +3,14 @@ import { Link, useNavigate } from 'react-router-dom'; import { UserContext } from '../context/User'; import { StatusContext } from '../context/Status'; -import { API, getLogo, getSystemName, isAdmin, isMobile, showError } from '../helpers'; +import { + API, + getLogo, + getSystemName, + isAdmin, + isMobile, + showError, +} from '../helpers'; import '../index.css'; import { @@ -17,7 +24,7 @@ import { IconKey, IconLayers, IconSetting, - IconUser + IconUser, } from '@douyinfe/semi-icons'; import { Layout, Nav } from '@douyinfe/semi-ui'; @@ -26,7 +33,8 @@ import { Layout, Nav } from '@douyinfe/semi-ui'; const SiderBar = () => { const [userState, userDispatch] = useContext(UserContext); const [statusState, statusDispatch] = useContext(StatusContext); - const defaultIsCollapsed = isMobile() || localStorage.getItem('default_collapse_sidebar') === 'true'; + const defaultIsCollapsed = + isMobile() || localStorage.getItem('default_collapse_sidebar') === 'true'; let navigate = useNavigate(); const [selectedKeys, setSelectedKeys] = useState(['home']); @@ -46,89 +54,105 @@ const SiderBar = () => { setting: '/setting', about: '/about', chat: '/chat', - detail: '/detail' + detail: '/detail', }; - const headerButtons = useMemo(() => [ - { - text: '首页', - itemKey: 'home', - to: '/', - icon: <IconHome /> - }, - { - text: '渠道', - itemKey: 'channel', - to: '/channel', - icon: <IconLayers />, - className: isAdmin() ? 'semi-navigation-item-normal' : 'tableHiddle' - }, - { - text: '聊天', - itemKey: 'chat', - to: '/chat', - icon: <IconComment />, - className: localStorage.getItem('chat_link') ? 'semi-navigation-item-normal' : 'tableHiddle' - }, - { - text: '令牌', - itemKey: 'token', - to: '/token', - icon: <IconKey /> - }, - { - text: '兑换码', - itemKey: 'redemption', - to: '/redemption', - icon: <IconGift />, - className: isAdmin() ? 'semi-navigation-item-normal' : 'tableHiddle' - }, - { - text: '钱包', - itemKey: 'topup', - to: '/topup', - icon: <IconCreditCard /> - }, - { - text: '用户管理', - itemKey: 'user', - to: '/user', - icon: <IconUser />, - className: isAdmin() ? 'semi-navigation-item-normal' : 'tableHiddle' - }, - { - text: '日志', - itemKey: 'log', - to: '/log', - icon: <IconHistogram /> - }, - { - text: '数据看板', - itemKey: 'detail', - to: '/detail', - icon: <IconCalendarClock />, - className: localStorage.getItem('enable_data_export') === 'true' ? 'semi-navigation-item-normal' : 'tableHiddle' - }, - { - text: '绘图', - itemKey: 'midjourney', - to: '/midjourney', - icon: <IconImage />, - className: localStorage.getItem('enable_drawing') === 'true' ? 'semi-navigation-item-normal' : 'tableHiddle' - }, - { - text: '设置', - itemKey: 'setting', - to: '/setting', - icon: <IconSetting /> - } - // { - // text: '关于', - // itemKey: 'about', - // to: '/about', - // icon: <IconAt/> - // } - ], [localStorage.getItem('enable_data_export'), localStorage.getItem('enable_drawing'), localStorage.getItem('chat_link'), isAdmin()]); + const headerButtons = useMemo( + () => [ + { + text: '首页', + itemKey: 'home', + to: '/', + icon: <IconHome />, + }, + { + text: '渠道', + itemKey: 'channel', + to: '/channel', + icon: <IconLayers />, + className: isAdmin() ? 'semi-navigation-item-normal' : 'tableHiddle', + }, + { + text: '聊天', + itemKey: 'chat', + to: '/chat', + icon: <IconComment />, + className: localStorage.getItem('chat_link') + ? 'semi-navigation-item-normal' + : 'tableHiddle', + }, + { + text: '令牌', + itemKey: 'token', + to: '/token', + icon: <IconKey />, + }, + { + text: '兑换码', + itemKey: 'redemption', + to: '/redemption', + icon: <IconGift />, + className: isAdmin() ? 'semi-navigation-item-normal' : 'tableHiddle', + }, + { + text: '钱包', + itemKey: 'topup', + to: '/topup', + icon: <IconCreditCard />, + }, + { + text: '用户管理', + itemKey: 'user', + to: '/user', + icon: <IconUser />, + className: isAdmin() ? 'semi-navigation-item-normal' : 'tableHiddle', + }, + { + text: '日志', + itemKey: 'log', + to: '/log', + icon: <IconHistogram />, + }, + { + text: '数据看板', + itemKey: 'detail', + to: '/detail', + icon: <IconCalendarClock />, + className: + localStorage.getItem('enable_data_export') === 'true' + ? 'semi-navigation-item-normal' + : 'tableHiddle', + }, + { + text: '绘图', + itemKey: 'midjourney', + to: '/midjourney', + icon: <IconImage />, + className: + localStorage.getItem('enable_drawing') === 'true' + ? 'semi-navigation-item-normal' + : 'tableHiddle', + }, + { + text: '设置', + itemKey: 'setting', + to: '/setting', + icon: <IconSetting />, + }, + // { + // text: '关于', + // itemKey: 'about', + // to: '/about', + // icon: <IconAt/> + // } + ], + [ + localStorage.getItem('enable_data_export'), + localStorage.getItem('enable_drawing'), + localStorage.getItem('chat_link'), + isAdmin(), + ], + ); const loadStatus = async () => { const res = await API.get('/api/status'); @@ -143,8 +167,14 @@ const SiderBar = () => { localStorage.setItem('display_in_currency', data.display_in_currency); localStorage.setItem('enable_drawing', data.enable_drawing); localStorage.setItem('enable_data_export', data.enable_data_export); - localStorage.setItem('data_export_default_time', data.data_export_default_time); - localStorage.setItem('default_collapse_sidebar', data.default_collapse_sidebar); + localStorage.setItem( + 'data_export_default_time', + data.data_export_default_time, + ); + localStorage.setItem( + 'default_collapse_sidebar', + data.default_collapse_sidebar, + ); localStorage.setItem('mj_notify_enabled', data.mj_notify_enabled); if (data.chat_link) { localStorage.setItem('chat_link', data.chat_link); @@ -163,11 +193,14 @@ const SiderBar = () => { useEffect(() => { loadStatus().then(() => { - setIsCollapsed(isMobile() || localStorage.getItem('default_collapse_sidebar') === 'true'); + setIsCollapsed( + isMobile() || + localStorage.getItem('default_collapse_sidebar') === 'true', + ); }); - let localKey = window.location.pathname.split('/')[1] + let localKey = window.location.pathname.split('/')[1]; if (localKey === '') { - localKey = 'home' + localKey = 'home'; } setSelectedKeys([localKey]); }, []); @@ -179,9 +212,12 @@ const SiderBar = () => { <Nav // bodyStyle={{ maxWidth: 200 }} style={{ maxWidth: 200 }} - defaultIsCollapsed={isMobile() || localStorage.getItem('default_collapse_sidebar') === 'true'} + defaultIsCollapsed={ + isMobile() || + localStorage.getItem('default_collapse_sidebar') === 'true' + } isCollapsed={isCollapsed} - onCollapseChange={collapsed => { + onCollapseChange={(collapsed) => { setIsCollapsed(collapsed); }} selectedKeys={selectedKeys} @@ -196,20 +232,20 @@ const SiderBar = () => { ); }} items={headerButtons} - onSelect={key => { + onSelect={(key) => { setSelectedKeys([key.itemKey]); }} header={{ - logo: <img src={logo} alt="logo" style={{ marginRight: '0.75em' }} />, - text: systemName + logo: ( + <img src={logo} alt='logo' style={{ marginRight: '0.75em' }} /> + ), + text: systemName, }} // footer={{ // text: '© 2021 NekoAPI', // }} > - - <Nav.Footer collapseButton={true}> - </Nav.Footer> + <Nav.Footer collapseButton={true}></Nav.Footer> </Nav> </div> </Layout> diff --git a/web/src/components/SystemSetting.js b/web/src/components/SystemSetting.js index 213685b..3716a00 100644 --- a/web/src/components/SystemSetting.js +++ b/web/src/components/SystemSetting.js @@ -1,5 +1,13 @@ 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, removeTrailingSlash, showError, verifyJSON } from '../helpers'; const SystemSetting = () => { @@ -38,13 +46,14 @@ const SystemSetting = () => { // telegram login TelegramOAuthEnabled: '', TelegramBotToken: '', - TelegramBotName: '' + TelegramBotName: '', }); const [originInputs, setOriginInputs] = useState({}); let [loading, setLoading] = useState(false); const [EmailDomainWhitelist, setEmailDomainWhitelist] = useState([]); const [restrictedDomainInput, setRestrictedDomainInput] = useState(''); - const [showPasswordWarningModal, setShowPasswordWarningModal] = useState(false); + const [showPasswordWarningModal, setShowPasswordWarningModal] = + useState(false); const getOptions = async () => { const res = await API.get('/api/option/'); @@ -59,13 +68,15 @@ const SystemSetting = () => { }); setInputs({ ...newInputs, - EmailDomainWhitelist: newInputs.EmailDomainWhitelist.split(',') + EmailDomainWhitelist: newInputs.EmailDomainWhitelist.split(','), }); setOriginInputs(newInputs); - setEmailDomainWhitelist(newInputs.EmailDomainWhitelist.split(',').map((item) => { - return { key: item, text: item, value: item }; - })); + setEmailDomainWhitelist( + newInputs.EmailDomainWhitelist.split(',').map((item) => { + return { key: item, text: item, value: item }; + }), + ); } else { showError(message); } @@ -94,7 +105,7 @@ const SystemSetting = () => { } const res = await API.put('/api/option/', { key, - value + value, }); const { success, message } = res.data; if (success) { @@ -105,7 +116,8 @@ const SystemSetting = () => { value = parseFloat(value); } setInputs((inputs) => ({ - ...inputs, [key]: value + ...inputs, + [key]: value, })); } else { showError(message); @@ -197,13 +209,16 @@ const SystemSetting = () => { } }; - const submitEmailDomainWhitelist = async () => { if ( - originInputs['EmailDomainWhitelist'] !== inputs.EmailDomainWhitelist.join(',') && + originInputs['EmailDomainWhitelist'] !== + inputs.EmailDomainWhitelist.join(',') && inputs.SMTPToken !== '' ) { - await updateOption('EmailDomainWhitelist', inputs.EmailDomainWhitelist.join(',')); + await updateOption( + 'EmailDomainWhitelist', + inputs.EmailDomainWhitelist.join(','), + ); } }; @@ -211,7 +226,7 @@ const SystemSetting = () => { if (originInputs['WeChatServerAddress'] !== inputs.WeChatServerAddress) { await updateOption( 'WeChatServerAddress', - removeTrailingSlash(inputs.WeChatServerAddress) + removeTrailingSlash(inputs.WeChatServerAddress), ); } if ( @@ -220,7 +235,7 @@ const SystemSetting = () => { ) { await updateOption( 'WeChatAccountQRCodeImageURL', - inputs.WeChatAccountQRCodeImageURL + inputs.WeChatAccountQRCodeImageURL, ); } if ( @@ -263,17 +278,23 @@ const SystemSetting = () => { const submitNewRestrictedDomain = () => { const localDomainList = inputs.EmailDomainWhitelist; - if (restrictedDomainInput !== '' && !localDomainList.includes(restrictedDomainInput)) { + if ( + restrictedDomainInput !== '' && + !localDomainList.includes(restrictedDomainInput) + ) { setRestrictedDomainInput(''); setInputs({ ...inputs, - EmailDomainWhitelist: [...localDomainList, restrictedDomainInput] + EmailDomainWhitelist: [...localDomainList, restrictedDomainInput], }); - setEmailDomainWhitelist([...EmailDomainWhitelist, { - key: restrictedDomainInput, - text: restrictedDomainInput, - value: restrictedDomainInput - }]); + setEmailDomainWhitelist([ + ...EmailDomainWhitelist, + { + key: restrictedDomainInput, + text: restrictedDomainInput, + value: restrictedDomainInput, + }, + ]); } }; @@ -281,13 +302,13 @@ const SystemSetting = () => { <Grid columns={1}> <Grid.Column> <Form loading={loading}> - <Header as="h3">通用设置</Header> - <Form.Group widths="equal"> + <Header as='h3'>通用设置</Header> + <Form.Group widths='equal'> <Form.Input - label="服务器地址" - placeholder="例如:https://yourdomain.com" + label='服务器地址' + placeholder='例如:https://yourdomain.com' value={inputs.ServerAddress} - name="ServerAddress" + name='ServerAddress' onChange={handleInputChange} /> </Form.Group> @@ -295,81 +316,79 @@ const SystemSetting = () => { 更新服务器地址 </Form.Button> <Divider /> - <Header as="h3">支付设置(当前仅支持易支付接口,默认使用上方服务器地址作为回调地址!)</Header> - <Form.Group widths="equal"> + <Header as='h3'> + 支付设置(当前仅支持易支付接口,默认使用上方服务器地址作为回调地址!) + </Header> + <Form.Group widths='equal'> <Form.Input - label="支付地址,不填写则不启用在线支付" - placeholder="例如:https://yourdomain.com" + label='支付地址,不填写则不启用在线支付' + placeholder='例如:https://yourdomain.com' value={inputs.PayAddress} - name="PayAddress" + name='PayAddress' onChange={handleInputChange} /> <Form.Input - label="易支付商户ID" - placeholder="例如:0001" + label='易支付商户ID' + placeholder='例如:0001' value={inputs.EpayId} - name="EpayId" + name='EpayId' onChange={handleInputChange} /> <Form.Input - label="易支付商户密钥" - placeholder="例如:dejhfueqhujasjmndbjkqaw" + label='易支付商户密钥' + placeholder='例如:dejhfueqhujasjmndbjkqaw' value={inputs.EpayKey} - name="EpayKey" + name='EpayKey' onChange={handleInputChange} /> - </Form.Group> - <Form.Group widths="equal"> + <Form.Group widths='equal'> <Form.Input - label="回调地址,不填写则使用上方服务器地址作为回调地址" - placeholder="例如:https://yourdomain.com" + label='回调地址,不填写则使用上方服务器地址作为回调地址' + placeholder='例如:https://yourdomain.com' value={inputs.CustomCallbackAddress} - name="CustomCallbackAddress" + name='CustomCallbackAddress' onChange={handleInputChange} /> <Form.Input - label="充值价格(x元/美金)" - placeholder="例如:7,就是7元/美金" + label='充值价格(x元/美金)' + placeholder='例如:7,就是7元/美金' value={inputs.Price} - name="Price" + name='Price' min={0} onChange={handleInputChange} /> <Form.Input - label="最低充值数量" - placeholder="例如:2,就是最低充值2$" + label='最低充值数量' + placeholder='例如:2,就是最低充值2$' value={inputs.MinTopUp} - name="MinTopUp" + name='MinTopUp' min={1} onChange={handleInputChange} /> </Form.Group> - <Form.Group widths="equal"> + <Form.Group widths='equal'> <Form.TextArea - label="充值分组倍率" - name="TopupGroupRatio" + label='充值分组倍率' + name='TopupGroupRatio' onChange={handleInputChange} style={{ minHeight: 250, fontFamily: 'JetBrains Mono, Consolas' }} - autoComplete="new-password" + autoComplete='new-password' value={inputs.TopupGroupRatio} - placeholder="为一个 JSON 文本,键为组名称,值为倍率" + placeholder='为一个 JSON 文本,键为组名称,值为倍率' /> </Form.Group> - <Form.Button onClick={submitPayAddress}> - 更新支付设置 - </Form.Button> + <Form.Button onClick={submitPayAddress}>更新支付设置</Form.Button> <Divider /> - <Header as="h3">配置登录注册</Header> + <Header as='h3'>配置登录注册</Header> <Form.Group inline> <Form.Checkbox checked={inputs.PasswordLoginEnabled === 'true'} - label="允许通过密码进行登录" - name="PasswordLoginEnabled" + label='允许通过密码进行登录' + name='PasswordLoginEnabled' onChange={handleInputChange} /> - { - showPasswordWarningModal && + {showPasswordWarningModal && ( <Modal open={showPasswordWarningModal} onClose={() => setShowPasswordWarningModal(false)} @@ -378,12 +397,16 @@ const SystemSetting = () => { > <Modal.Header>警告</Modal.Header> <Modal.Content> - <p>取消密码登录将导致所有未绑定其他登录方式的用户(包括管理员)无法通过密码登录,确认取消?</p> + <p> + 取消密码登录将导致所有未绑定其他登录方式的用户(包括管理员)无法通过密码登录,确认取消? + </p> </Modal.Content> <Modal.Actions> - <Button onClick={() => setShowPasswordWarningModal(false)}>取消</Button> + <Button onClick={() => setShowPasswordWarningModal(false)}> + 取消 + </Button> <Button - color="yellow" + color='yellow' onClick={async () => { setShowPasswordWarningModal(false); await updateOption('PasswordLoginEnabled', 'false'); @@ -393,157 +416,170 @@ const SystemSetting = () => { </Button> </Modal.Actions> </Modal> - } + )} <Form.Checkbox checked={inputs.PasswordRegisterEnabled === 'true'} - label="允许通过密码进行注册" - name="PasswordRegisterEnabled" + label='允许通过密码进行注册' + name='PasswordRegisterEnabled' onChange={handleInputChange} /> <Form.Checkbox checked={inputs.EmailVerificationEnabled === 'true'} - label="通过密码注册时需要进行邮箱验证" - name="EmailVerificationEnabled" + label='通过密码注册时需要进行邮箱验证' + name='EmailVerificationEnabled' onChange={handleInputChange} /> <Form.Checkbox checked={inputs.GitHubOAuthEnabled === 'true'} - label="允许通过 GitHub 账户登录 & 注册" - name="GitHubOAuthEnabled" + label='允许通过 GitHub 账户登录 & 注册' + name='GitHubOAuthEnabled' onChange={handleInputChange} /> <Form.Checkbox checked={inputs.WeChatAuthEnabled === 'true'} - label="允许通过微信登录 & 注册" - name="WeChatAuthEnabled" + label='允许通过微信登录 & 注册' + name='WeChatAuthEnabled' onChange={handleInputChange} /> <Form.Checkbox checked={inputs.TelegramOAuthEnabled === 'true'} - label="允许通过 Telegram 进行登录" - name="TelegramOAuthEnabled" + label='允许通过 Telegram 进行登录' + name='TelegramOAuthEnabled' onChange={handleInputChange} /> </Form.Group> <Form.Group inline> <Form.Checkbox checked={inputs.RegisterEnabled === 'true'} - label="允许新用户注册(此项为否时,新用户将无法以任何方式进行注册)" - name="RegisterEnabled" + label='允许新用户注册(此项为否时,新用户将无法以任何方式进行注册)' + name='RegisterEnabled' onChange={handleInputChange} /> <Form.Checkbox checked={inputs.TurnstileCheckEnabled === 'true'} - label="启用 Turnstile 用户校验" - name="TurnstileCheckEnabled" + label='启用 Turnstile 用户校验' + name='TurnstileCheckEnabled' onChange={handleInputChange} /> </Form.Group> <Divider /> - <Header as="h3"> + <Header as='h3'> 配置邮箱域名白名单 - <Header.Subheader>用以防止恶意用户利用临时邮箱批量注册</Header.Subheader> + <Header.Subheader> + 用以防止恶意用户利用临时邮箱批量注册 + </Header.Subheader> </Header> <Form.Group widths={3}> <Form.Checkbox - label="启用邮箱域名白名单" - name="EmailDomainRestrictionEnabled" + label='启用邮箱域名白名单' + name='EmailDomainRestrictionEnabled' onChange={handleInputChange} checked={inputs.EmailDomainRestrictionEnabled === 'true'} /> </Form.Group> <Form.Group widths={2}> <Form.Dropdown - label="允许的邮箱域名" - placeholder="允许的邮箱域名" - name="EmailDomainWhitelist" + label='允许的邮箱域名' + placeholder='允许的邮箱域名' + name='EmailDomainWhitelist' required fluid multiple selection onChange={handleInputChange} value={inputs.EmailDomainWhitelist} - autoComplete="new-password" + autoComplete='new-password' options={EmailDomainWhitelist} /> <Form.Input - label="添加新的允许的邮箱域名" + label='添加新的允许的邮箱域名' action={ - <Button type="button" onClick={() => { - submitNewRestrictedDomain(); - }}>填入</Button> + <Button + type='button' + onClick={() => { + submitNewRestrictedDomain(); + }} + > + 填入 + </Button> } onKeyDown={(e) => { if (e.key === 'Enter') { submitNewRestrictedDomain(); } }} - autoComplete="new-password" - placeholder="输入新的允许的邮箱域名" + autoComplete='new-password' + placeholder='输入新的允许的邮箱域名' value={restrictedDomainInput} onChange={(e, { value }) => { setRestrictedDomainInput(value); }} /> </Form.Group> - <Form.Button onClick={submitEmailDomainWhitelist}>保存邮箱域名白名单设置</Form.Button> + <Form.Button onClick={submitEmailDomainWhitelist}> + 保存邮箱域名白名单设置 + </Form.Button> <Divider /> - <Header as="h3"> + <Header as='h3'> 配置 SMTP <Header.Subheader>用以支持系统的邮件发送</Header.Subheader> </Header> <Form.Group widths={3}> <Form.Input - label="SMTP 服务器地址" - name="SMTPServer" + label='SMTP 服务器地址' + name='SMTPServer' onChange={handleInputChange} - autoComplete="new-password" + autoComplete='new-password' value={inputs.SMTPServer} - placeholder="例如:smtp.qq.com" + placeholder='例如:smtp.qq.com' /> <Form.Input - label="SMTP 端口" - name="SMTPPort" + label='SMTP 端口' + name='SMTPPort' onChange={handleInputChange} - autoComplete="new-password" + autoComplete='new-password' value={inputs.SMTPPort} - placeholder="默认: 587" + placeholder='默认: 587' /> <Form.Input - label="SMTP 账户" - name="SMTPAccount" + label='SMTP 账户' + name='SMTPAccount' onChange={handleInputChange} - autoComplete="new-password" + autoComplete='new-password' value={inputs.SMTPAccount} - placeholder="通常是邮箱地址" + placeholder='通常是邮箱地址' /> </Form.Group> <Form.Group widths={3}> <Form.Input - label="SMTP 发送者邮箱" - name="SMTPFrom" + label='SMTP 发送者邮箱' + name='SMTPFrom' onChange={handleInputChange} - autoComplete="new-password" + autoComplete='new-password' value={inputs.SMTPFrom} - placeholder="通常和邮箱地址保持一致" + placeholder='通常和邮箱地址保持一致' /> <Form.Input - label="SMTP 访问凭证" - name="SMTPToken" + label='SMTP 访问凭证' + name='SMTPToken' onChange={handleInputChange} - type="password" - autoComplete="new-password" + type='password' + autoComplete='new-password' checked={inputs.RegisterEnabled === 'true'} - placeholder="敏感信息不会发送到前端显示" + placeholder='敏感信息不会发送到前端显示' /> </Form.Group> <Form.Button onClick={submitSMTP}>保存 SMTP 设置</Form.Button> <Divider /> - <Header as="h3"> + <Header as='h3'> 配置 GitHub OAuth App <Header.Subheader> 用以支持通过 GitHub 进行登录注册, - <a href="https://github.com/settings/developers" target="_blank" rel="noreferrer"> + <a + href='https://github.com/settings/developers' + target='_blank' + rel='noreferrer' + > 点击此处 </a> 管理你的 GitHub OAuth App @@ -556,34 +592,35 @@ const SystemSetting = () => { </Message> <Form.Group widths={3}> <Form.Input - label="GitHub Client ID" - name="GitHubClientId" + label='GitHub Client ID' + name='GitHubClientId' onChange={handleInputChange} - autoComplete="new-password" + autoComplete='new-password' value={inputs.GitHubClientId} - placeholder="输入你注册的 GitHub OAuth APP 的 ID" + placeholder='输入你注册的 GitHub OAuth APP 的 ID' /> <Form.Input - label="GitHub Client Secret" - name="GitHubClientSecret" + label='GitHub Client Secret' + name='GitHubClientSecret' onChange={handleInputChange} - type="password" - autoComplete="new-password" + type='password' + autoComplete='new-password' value={inputs.GitHubClientSecret} - placeholder="敏感信息不会发送到前端显示" + placeholder='敏感信息不会发送到前端显示' /> </Form.Group> <Form.Button onClick={submitGitHubOAuth}> 保存 GitHub OAuth 设置 </Form.Button> <Divider /> - <Header as="h3"> + <Header as='h3'> 配置 WeChat Server <Header.Subheader> 用以支持通过微信进行登录注册, <a - href="https://github.com/songquanpeng/wechat-server" - target="_blank" rel="noreferrer" + href='https://github.com/songquanpeng/wechat-server' + target='_blank' + rel='noreferrer' > 点击此处 </a> @@ -592,61 +629,65 @@ const SystemSetting = () => { </Header> <Form.Group widths={3}> <Form.Input - label="WeChat Server 服务器地址" - name="WeChatServerAddress" - placeholder="例如:https://yourdomain.com" + label='WeChat Server 服务器地址' + name='WeChatServerAddress' + placeholder='例如:https://yourdomain.com' onChange={handleInputChange} - autoComplete="new-password" + autoComplete='new-password' value={inputs.WeChatServerAddress} /> <Form.Input - label="WeChat Server 访问凭证" - name="WeChatServerToken" - type="password" + label='WeChat Server 访问凭证' + name='WeChatServerToken' + type='password' onChange={handleInputChange} - autoComplete="new-password" + autoComplete='new-password' value={inputs.WeChatServerToken} - placeholder="敏感信息不会发送到前端显示" + placeholder='敏感信息不会发送到前端显示' /> <Form.Input - label="微信公众号二维码图片链接" - name="WeChatAccountQRCodeImageURL" + label='微信公众号二维码图片链接' + name='WeChatAccountQRCodeImageURL' onChange={handleInputChange} - autoComplete="new-password" + autoComplete='new-password' value={inputs.WeChatAccountQRCodeImageURL} - placeholder="输入一个图片链接" + placeholder='输入一个图片链接' /> </Form.Group> <Form.Button onClick={submitWeChat}> 保存 WeChat Server 设置 </Form.Button> <Divider /> - <Header as="h3">配置 Telegram 登录</Header> + <Header as='h3'>配置 Telegram 登录</Header> <Form.Group inline> <Form.Input - label="Telegram Bot Token" - name="TelegramBotToken" + label='Telegram Bot Token' + name='TelegramBotToken' onChange={handleInputChange} value={inputs.TelegramBotToken} - placeholder="输入你的 Telegram Bot Token" + placeholder='输入你的 Telegram Bot Token' /> <Form.Input - label="Telegram Bot 名称" - name="TelegramBotName" + label='Telegram Bot 名称' + name='TelegramBotName' onChange={handleInputChange} value={inputs.TelegramBotName} - placeholder="输入你的 Telegram Bot 名称" + placeholder='输入你的 Telegram Bot 名称' /> </Form.Group> <Form.Button onClick={submitTelegramSettings}> 保存 Telegram 登录设置 </Form.Button> <Divider /> - <Header as="h3"> + <Header as='h3'> 配置 Turnstile <Header.Subheader> 用以支持用户校验, - <a href="https://dash.cloudflare.com/" target="_blank" rel="noreferrer"> + <a + href='https://dash.cloudflare.com/' + target='_blank' + rel='noreferrer' + > 点击此处 </a> 管理你的 Turnstile Sites,推荐选择 Invisible Widget Type @@ -654,21 +695,21 @@ const SystemSetting = () => { </Header> <Form.Group widths={3}> <Form.Input - label="Turnstile Site Key" - name="TurnstileSiteKey" + label='Turnstile Site Key' + name='TurnstileSiteKey' onChange={handleInputChange} - autoComplete="new-password" + autoComplete='new-password' value={inputs.TurnstileSiteKey} - placeholder="输入你注册的 Turnstile Site Key" + placeholder='输入你注册的 Turnstile Site Key' /> <Form.Input - label="Turnstile Secret Key" - name="TurnstileSecretKey" + label='Turnstile Secret Key' + name='TurnstileSecretKey' onChange={handleInputChange} - type="password" - autoComplete="new-password" + type='password' + autoComplete='new-password' value={inputs.TurnstileSecretKey} - placeholder="敏感信息不会发送到前端显示" + placeholder='敏感信息不会发送到前端显示' /> </Form.Group> <Form.Button onClick={submitTurnstile}> diff --git a/web/src/components/TokensTable.js b/web/src/components/TokensTable.js index 5901bfd..4687ce9 100644 --- a/web/src/components/TokensTable.js +++ b/web/src/components/TokensTable.js @@ -1,9 +1,25 @@ import React, { useEffect, useState } from 'react'; -import { API, copy, showError, showSuccess, timestamp2string } from '../helpers'; +import { + API, + copy, + showError, + showSuccess, + timestamp2string, +} from '../helpers'; import { ITEMS_PER_PAGE } from '../constants'; import { renderQuota } from '../helpers/render'; -import { Button, Dropdown, Form, Modal, Popconfirm, Popover, SplitButtonGroup, Table, Tag } from '@douyinfe/semi-ui'; +import { + Button, + Dropdown, + Form, + Modal, + Popconfirm, + Popover, + SplitButtonGroup, + Table, + Tag, +} from '@douyinfe/semi-ui'; import { IconTreeTriangleDown } from '@douyinfe/semi-icons'; import EditToken from '../pages/Token/EditToken'; @@ -11,85 +27,107 @@ import EditToken from '../pages/Token/EditToken'; const COPY_OPTIONS = [ { key: 'next', text: 'ChatGPT Next Web', value: 'next' }, { key: 'ama', text: 'ChatGPT Web & Midjourney', value: 'ama' }, - { key: 'opencat', text: 'OpenCat', value: 'opencat' } + { key: 'opencat', text: 'OpenCat', value: 'opencat' }, ]; const OPEN_LINK_OPTIONS = [ { key: 'ama', text: 'ChatGPT Web & Midjourney', value: 'ama' }, - { key: 'opencat', text: 'OpenCat', value: 'opencat' } + { key: 'opencat', text: 'OpenCat', value: 'opencat' }, ]; function renderTimestamp(timestamp) { - return ( - <> - {timestamp2string(timestamp)} - </> - ); + return <>{timestamp2string(timestamp)}</>; } function renderStatus(status, model_limits_enabled = false) { switch (status) { case 1: if (model_limits_enabled) { - return <Tag color="green" size="large">已启用:限制模型</Tag>; + return ( + <Tag color='green' size='large'> + 已启用:限制模型 + </Tag> + ); } else { - return <Tag color="green" size="large">已启用</Tag>; + return ( + <Tag color='green' size='large'> + 已启用 + </Tag> + ); } case 2: - return <Tag color="red" size="large"> 已禁用 </Tag>; + return ( + <Tag color='red' size='large'> + {' '} + 已禁用{' '} + </Tag> + ); case 3: - return <Tag color="yellow" size="large"> 已过期 </Tag>; + return ( + <Tag color='yellow' size='large'> + {' '} + 已过期{' '} + </Tag> + ); case 4: - return <Tag color="grey" size="large"> 已耗尽 </Tag>; + return ( + <Tag color='grey' size='large'> + {' '} + 已耗尽{' '} + </Tag> + ); default: - return <Tag color="black" size="large"> 未知状态 </Tag>; + return ( + <Tag color='black' size='large'> + {' '} + 未知状态{' '} + </Tag> + ); } } const TokensTable = () => { - const link_menu = [ { - node: 'item', key: 'next', name: 'ChatGPT Next Web', onClick: () => { + node: 'item', + key: 'next', + name: 'ChatGPT Next Web', + onClick: () => { onOpenLink('next'); - } + }, }, { node: 'item', key: 'ama', name: 'AMA 问天', value: 'ama' }, { - node: 'item', key: 'next-mj', name: 'ChatGPT Web & Midjourney', value: 'next-mj', onClick: () => { + node: 'item', + key: 'next-mj', + name: 'ChatGPT Web & Midjourney', + value: 'next-mj', + onClick: () => { onOpenLink('next-mj'); - } + }, }, - { node: 'item', key: 'opencat', name: 'OpenCat', value: 'opencat' } + { node: 'item', key: 'opencat', name: 'OpenCat', value: 'opencat' }, ]; const columns = [ { title: '名称', - dataIndex: 'name' + dataIndex: 'name', }, { title: '状态', dataIndex: 'status', key: 'status', render: (text, record, index) => { - return ( - <div> - {renderStatus(text, record.model_limits_enabled)} - </div> - ); - } + return <div>{renderStatus(text, record.model_limits_enabled)}</div>; + }, }, { title: '已用额度', dataIndex: 'used_quota', render: (text, record, index) => { - return ( - <div> - {renderQuota(parseInt(text))} - </div> - ); - } + return <div>{renderQuota(parseInt(text))}</div>; + }, }, { title: '剩余额度', @@ -97,22 +135,25 @@ const TokensTable = () => { render: (text, record, index) => { return ( <div> - {record.unlimited_quota ? <Tag size={'large'} color={'white'}>无限制</Tag> : - <Tag size={'large'} color={'light-blue'}>{renderQuota(parseInt(text))}</Tag>} + {record.unlimited_quota ? ( + <Tag size={'large'} color={'white'}> + 无限制 + </Tag> + ) : ( + <Tag size={'large'} color={'light-blue'}> + {renderQuota(parseInt(text))} + </Tag> + )} </div> ); - } + }, }, { title: '创建时间', dataIndex: 'created_time', render: (text, record, index) => { - return ( - <div> - {renderTimestamp(text)} - </div> - ); - } + return <div>{renderTimestamp(text)}</div>; + }, }, { title: '过期时间', @@ -123,7 +164,7 @@ const TokensTable = () => { {record.expired_time === -1 ? '永不过期' : renderTimestamp(text)} </div> ); - } + }, }, { title: '', @@ -131,25 +172,41 @@ const TokensTable = () => { render: (text, record, index) => ( <div> <Popover - content={ - 'sk-' + record.key - } + content={'sk-' + record.key} style={{ padding: 20 }} - position="top" + position='top' > - <Button theme="light" type="tertiary" style={{ marginRight: 1 }}>查看</Button> + <Button theme='light' type='tertiary' style={{ marginRight: 1 }}> + 查看 + </Button> </Popover> - <Button theme="light" type="secondary" style={{ marginRight: 1 }} - onClick={async (text) => { - await copyText('sk-' + record.key); - }} - >复制</Button> - <SplitButtonGroup style={{ marginRight: 1 }} aria-label="项目操作按钮组"> - <Button theme="light" style={{ color: 'rgba(var(--semi-teal-7), 1)' }} onClick={() => { - onOpenLink('next', record.key); - }}>聊天</Button> - <Dropdown trigger="click" position="bottomRight" menu={ - [ + <Button + theme='light' + type='secondary' + style={{ marginRight: 1 }} + onClick={async (text) => { + await copyText('sk-' + record.key); + }} + > + 复制 + </Button> + <SplitButtonGroup + style={{ marginRight: 1 }} + aria-label='项目操作按钮组' + > + <Button + theme='light' + style={{ color: 'rgba(var(--semi-teal-7), 1)' }} + onClick={() => { + onOpenLink('next', record.key); + }} + > + 聊天 + </Button> + <Dropdown + trigger='click' + position='bottomRight' + menu={[ { node: 'item', key: 'next', @@ -157,7 +214,7 @@ const TokensTable = () => { name: 'ChatGPT Next Web', onClick: () => { onOpenLink('next', record.key); - } + }, }, { node: 'item', @@ -166,70 +223,88 @@ const TokensTable = () => { name: 'ChatGPT Web & Midjourney', onClick: () => { onOpenLink('next-mj', record.key); - } + }, }, { - node: 'item', key: 'ama', name: 'AMA 问天(BotGem)', onClick: () => { + node: 'item', + key: 'ama', + name: 'AMA 问天(BotGem)', + onClick: () => { onOpenLink('ama', record.key); - } + }, }, { - node: 'item', key: 'opencat', name: 'OpenCat', onClick: () => { + node: 'item', + key: 'opencat', + name: 'OpenCat', + onClick: () => { onOpenLink('opencat', record.key); - } - } - ] - } + }, + }, + ]} > - <Button style={{ padding: '8px 4px', color: 'rgba(var(--semi-teal-7), 1)' }} type="primary" - icon={<IconTreeTriangleDown />}></Button> + <Button + style={{ + padding: '8px 4px', + color: 'rgba(var(--semi-teal-7), 1)', + }} + type='primary' + icon={<IconTreeTriangleDown />} + ></Button> </Dropdown> </SplitButtonGroup> <Popconfirm - title="确定是否要删除此令牌?" - content="此修改将不可逆" + title='确定是否要删除此令牌?' + content='此修改将不可逆' okType={'danger'} position={'left'} onConfirm={() => { - manageToken(record.id, 'delete', record).then( - () => { - removeRecord(record.key); - } - ); + manageToken(record.id, 'delete', record).then(() => { + removeRecord(record.key); + }); }} > - <Button theme="light" type="danger" style={{ marginRight: 1 }}>删除</Button> + <Button theme='light' type='danger' style={{ marginRight: 1 }}> + 删除 + </Button> </Popconfirm> - { - record.status === 1 ? - <Button theme="light" type="warning" style={{ marginRight: 1 }} onClick={ - async () => { - manageToken( - record.id, - 'disable', - record - ); - } - }>禁用</Button> : - <Button theme="light" type="secondary" style={{ marginRight: 1 }} onClick={ - async () => { - manageToken( - record.id, - 'enable', - record - ); - } - }>启用</Button> - } - <Button theme="light" type="tertiary" style={{ marginRight: 1 }} onClick={ - () => { + {record.status === 1 ? ( + <Button + theme='light' + type='warning' + style={{ marginRight: 1 }} + onClick={async () => { + manageToken(record.id, 'disable', record); + }} + > + 禁用 + </Button> + ) : ( + <Button + theme='light' + type='secondary' + style={{ marginRight: 1 }} + onClick={async () => { + manageToken(record.id, 'enable', record); + }} + > + 启用 + </Button> + )} + <Button + theme='light' + type='tertiary' + style={{ marginRight: 1 }} + onClick={() => { setEditingToken(record); setShowEdit(true); - } - }>编辑</Button> + }} + > + 编辑 + </Button> </div> - ) - } + ), + }, ]; const [pageSize, setPageSize] = useState(ITEMS_PER_PAGE); @@ -245,14 +320,14 @@ const TokensTable = () => { const [showTopUpModal, setShowTopUpModal] = useState(false); const [targetTokenIdx, setTargetTokenIdx] = useState(0); const [editingToken, setEditingToken] = useState({ - id: undefined + id: undefined, }); const closeEdit = () => { setShowEdit(false); setTimeout(() => { setEditingToken({ - id: undefined + id: undefined, }); }, 500); }; @@ -266,7 +341,10 @@ const TokensTable = () => { } }; - let pageData = tokens.slice((activePage - 1) * pageSize, activePage * pageSize); + let pageData = tokens.slice( + (activePage - 1) * pageSize, + activePage * pageSize, + ); const loadTokens = async (startIdx) => { setLoading(true); const res = await API.get(`/api/token/?p=${startIdx}&size=${pageSize}`); @@ -315,7 +393,8 @@ const TokensTable = () => { let nextUrl; if (nextLink) { - nextUrl = nextLink + `/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`; + nextUrl = + nextLink + `/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`; } else { nextUrl = `https://chat.oneapi.pro/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`; } @@ -323,7 +402,8 @@ const TokensTable = () => { let url; switch (type) { case 'ama': - url = mjLink + `/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`; + url = + mjLink + `/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`; break; case 'opencat': url = `opencat://team/join?domain=${encodedServerAddress}&token=sk-${key}`; @@ -367,7 +447,8 @@ const TokensTable = () => { let defaultUrl; if (chatLink) { - defaultUrl = chatLink + `/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`; + defaultUrl = + chatLink + `/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`; } let url; switch (type) { @@ -378,7 +459,8 @@ const TokensTable = () => { url = `opencat://team/join?domain=${encodedServerAddress}&token=sk-${key}`; break; case 'next-mj': - url = mjLink + `/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`; + url = + mjLink + `/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`; break; default: if (!chatLink) { @@ -399,10 +481,10 @@ const TokensTable = () => { }); }, [pageSize]); - const removeRecord = key => { + const removeRecord = (key) => { let newDataSource = [...tokens]; if (key != null) { - let idx = newDataSource.findIndex(data => data.key === key); + let idx = newDataSource.findIndex((data) => data.key === key); if (idx > -1) { newDataSource.splice(idx, 1); @@ -435,7 +517,6 @@ const TokensTable = () => { let newTokens = [...tokens]; // let realIdx = (activePage - 1) * ITEMS_PER_PAGE + idx; if (action === 'delete') { - } else { record.status = token.status; // newTokens[realIdx].status = token.status; @@ -455,7 +536,9 @@ const TokensTable = () => { return; } setSearching(true); - const res = await API.get(`/api/token/search?keyword=${searchKeyword}&token=${searchToken}`); + const res = await API.get( + `/api/token/search?keyword=${searchKeyword}&token=${searchToken}`, + ); const { success, message, data } = res.data; if (success) { setTokensFormat(data); @@ -488,32 +571,28 @@ const TokensTable = () => { setLoading(false); }; - - const handlePageChange = page => { + const handlePageChange = (page) => { setActivePage(page); if (page === Math.ceil(tokens.length / pageSize) + 1) { // In this case we have to load more data and then append them. - loadTokens(page - 1).then(r => { - }); + loadTokens(page - 1).then((r) => {}); } }; const rowSelection = { - onSelect: (record, selected) => { - }, - onSelectAll: (selected, selectedRows) => { - }, + onSelect: (record, selected) => {}, + onSelectAll: (selected, selectedRows) => {}, onChange: (selectedRowKeys, selectedRows) => { setSelectedKeys(selectedRows); - } + }, }; const handleRow = (record, index) => { if (record.status !== 1) { return { style: { - background: 'var(--semi-color-disabled-border)' - } + background: 'var(--semi-color-disabled-border)', + }, }; } else { return {}; @@ -522,63 +601,98 @@ const TokensTable = () => { return ( <> - <EditToken refresh={refresh} editingToken={editingToken} visiable={showEdit} handleClose={closeEdit}></EditToken> - <Form layout="horizontal" style={{ marginTop: 10 }} labelPosition={'left'}> + <EditToken + refresh={refresh} + editingToken={editingToken} + visiable={showEdit} + handleClose={closeEdit} + ></EditToken> + <Form + layout='horizontal' + style={{ marginTop: 10 }} + labelPosition={'left'} + > <Form.Input - field="keyword" - label="搜索关键字" - placeholder="令牌名称" + field='keyword' + label='搜索关键字' + placeholder='令牌名称' value={searchKeyword} loading={searching} onChange={handleKeywordChange} /> <Form.Input - field="token" - label="Key" - placeholder="密钥" + field='token' + label='Key' + placeholder='密钥' value={searchToken} loading={searching} onChange={handleSearchTokenChange} /> - <Button label="查询" type="primary" htmlType="submit" className="btn-margin-right" - onClick={searchTokens} style={{ marginRight: 8 }}>查询</Button> + <Button + label='查询' + type='primary' + htmlType='submit' + className='btn-margin-right' + onClick={searchTokens} + style={{ marginRight: 8 }} + > + 查询 + </Button> </Form> - <Table style={{ marginTop: 20 }} columns={columns} dataSource={pageData} pagination={{ - currentPage: activePage, - pageSize: pageSize, - total: tokenCount, - showSizeChanger: true, - pageSizeOptions: [10, 20, 50, 100], - formatPageText: (page) => `第 ${page.currentStart} - ${page.currentEnd} 条,共 ${tokens.length} 条`, - onPageSizeChange: (size) => { - setPageSize(size); - setActivePage(1); - }, - onPageChange: handlePageChange - }} loading={loading} rowSelection={rowSelection} onRow={handleRow}> - </Table> - <Button theme="light" type="primary" style={{ marginRight: 8 }} onClick={ - () => { + <Table + style={{ marginTop: 20 }} + columns={columns} + dataSource={pageData} + pagination={{ + currentPage: activePage, + pageSize: pageSize, + total: tokenCount, + showSizeChanger: true, + pageSizeOptions: [10, 20, 50, 100], + formatPageText: (page) => + `第 ${page.currentStart} - ${page.currentEnd} 条,共 ${tokens.length} 条`, + onPageSizeChange: (size) => { + setPageSize(size); + setActivePage(1); + }, + onPageChange: handlePageChange, + }} + loading={loading} + rowSelection={rowSelection} + onRow={handleRow} + ></Table> + <Button + theme='light' + type='primary' + style={{ marginRight: 8 }} + onClick={() => { setEditingToken({ - id: undefined + id: undefined, }); setShowEdit(true); - } - }>添加令牌</Button> - <Button label="复制所选令牌" type="warning" onClick={ - async () => { + }} + > + 添加令牌 + </Button> + <Button + label='复制所选令牌' + type='warning' + onClick={async () => { if (selectedKeys.length === 0) { showError('请至少选择一个令牌!'); return; } let keys = ''; for (let i = 0; i < selectedKeys.length; i++) { - keys += selectedKeys[i].name + ' sk-' + selectedKeys[i].key + '\n'; + keys += + selectedKeys[i].name + ' sk-' + selectedKeys[i].key + '\n'; } await copyText(keys); - } - }>复制所选令牌到剪贴板</Button> + }} + > + 复制所选令牌到剪贴板 + </Button> </> ); }; diff --git a/web/src/components/UsersTable.js b/web/src/components/UsersTable.js index 7757afa..50fe7a0 100644 --- a/web/src/components/UsersTable.js +++ b/web/src/components/UsersTable.js @@ -1,6 +1,14 @@ import React, { useEffect, useState } from 'react'; import { API, showError, showSuccess } from '../helpers'; -import { Button, Form, Popconfirm, Space, Table, Tag, Tooltip } from '@douyinfe/semi-ui'; +import { + Button, + Form, + Popconfirm, + Space, + Table, + Tag, + Tooltip, +} from '@douyinfe/semi-ui'; import { ITEMS_PER_PAGE } from '../constants'; import { renderGroup, renderNumber, renderQuota } from '../helpers/render'; import AddUser from '../pages/User/AddUser'; @@ -9,124 +17,218 @@ import EditUser from '../pages/User/EditUser'; function renderRole(role) { switch (role) { case 1: - return <Tag size="large">普通用户</Tag>; + return <Tag size='large'>普通用户</Tag>; case 10: - return <Tag color="yellow" size="large">管理员</Tag>; + return ( + <Tag color='yellow' size='large'> + 管理员 + </Tag> + ); case 100: - return <Tag color="orange" size="large">超级管理员</Tag>; + return ( + <Tag color='orange' size='large'> + 超级管理员 + </Tag> + ); default: - return <Tag color="red" size="large">未知身份</Tag>; + return ( + <Tag color='red' size='large'> + 未知身份 + </Tag> + ); } } const UsersTable = () => { - const columns = [{ - title: 'ID', dataIndex: 'id' - }, { - title: '用户名', dataIndex: 'username' - }, { - title: '分组', dataIndex: 'group', render: (text, record, index) => { - return (<div> - {renderGroup(text)} - </div>); - } - }, { - title: '统计信息', dataIndex: 'info', render: (text, record, index) => { - return (<div> - <Space spacing={1}> - <Tooltip content={'剩余额度'}> - <Tag color="white" size="large">{renderQuota(record.quota)}</Tag> - </Tooltip> - <Tooltip content={'已用额度'}> - <Tag color="white" size="large">{renderQuota(record.used_quota)}</Tag> - </Tooltip> - <Tooltip content={'调用次数'}> - <Tag color="white" size="large">{renderNumber(record.request_count)}</Tag> - </Tooltip> - </Space> - </div>); - } - }, { - title: '邀请信息', dataIndex: 'invite', render: (text, record, index) => { - return (<div> - <Space spacing={1}> - <Tooltip content={'邀请人数'}> - <Tag color="white" size="large">{renderNumber(record.aff_count)}</Tag> - </Tooltip> - <Tooltip content={'邀请总收益'}> - <Tag color="white" size="large">{renderQuota(record.aff_history_quota)}</Tag> - </Tooltip> - <Tooltip content={'邀请人ID'}> - {record.inviter_id === 0 ? <Tag color="white" size="large">无</Tag> : - <Tag color="white" size="large">{record.inviter_id}</Tag>} - </Tooltip> - </Space> - </div>); - } - }, { - title: '角色', dataIndex: 'role', render: (text, record, index) => { - return (<div> - {renderRole(text)} - </div>); - } - }, { - title: '状态', dataIndex: 'status', render: (text, record, index) => { - return (<div> - {record.DeletedAt !== null ? <Tag color="red">已注销</Tag> : renderStatus(text)} - </div>); - } - }, { - title: '', dataIndex: 'operate', render: (text, record, index) => (<div> - { - record.DeletedAt !== null ? <></> : - <> - <Popconfirm - title="确定?" - okType={'warning'} - onConfirm={() => { - manageUser(record.username, 'promote', record); - }} - > - <Button theme="light" type="warning" style={{ marginRight: 1 }}>提升</Button> - </Popconfirm> - <Popconfirm - title="确定?" - okType={'warning'} - onConfirm={() => { - manageUser(record.username, 'demote', record); - }} - > - <Button theme="light" type="secondary" style={{ marginRight: 1 }}>降级</Button> - </Popconfirm> - {record.status === 1 ? - <Button theme="light" type="warning" style={{ marginRight: 1 }} onClick={async () => { - manageUser(record.username, 'disable', record); - }}>禁用</Button> : - <Button theme="light" type="secondary" style={{ marginRight: 1 }} onClick={async () => { - manageUser(record.username, 'enable', record); - }} disabled={record.status === 3}>启用</Button>} - <Button theme="light" type="tertiary" style={{ marginRight: 1 }} onClick={() => { - setEditingUser(record); - setShowEditUser(true); - }}>编辑</Button> - </> - - } - <Popconfirm - title="确定是否要删除此用户?" - content="硬删除,此修改将不可逆" - okType={'danger'} - position={'left'} - onConfirm={() => { - manageUser(record.username, 'delete', record).then(() => { - removeRecord(record.id); - }); - }} - > - <Button theme="light" type="danger" style={{ marginRight: 1 }}>删除</Button> - </Popconfirm> - </div>) - }]; + const columns = [ + { + title: 'ID', + dataIndex: 'id', + }, + { + title: '用户名', + dataIndex: 'username', + }, + { + title: '分组', + dataIndex: 'group', + render: (text, record, index) => { + return <div>{renderGroup(text)}</div>; + }, + }, + { + title: '统计信息', + dataIndex: 'info', + render: (text, record, index) => { + return ( + <div> + <Space spacing={1}> + <Tooltip content={'剩余额度'}> + <Tag color='white' size='large'> + {renderQuota(record.quota)} + </Tag> + </Tooltip> + <Tooltip content={'已用额度'}> + <Tag color='white' size='large'> + {renderQuota(record.used_quota)} + </Tag> + </Tooltip> + <Tooltip content={'调用次数'}> + <Tag color='white' size='large'> + {renderNumber(record.request_count)} + </Tag> + </Tooltip> + </Space> + </div> + ); + }, + }, + { + title: '邀请信息', + dataIndex: 'invite', + render: (text, record, index) => { + return ( + <div> + <Space spacing={1}> + <Tooltip content={'邀请人数'}> + <Tag color='white' size='large'> + {renderNumber(record.aff_count)} + </Tag> + </Tooltip> + <Tooltip content={'邀请总收益'}> + <Tag color='white' size='large'> + {renderQuota(record.aff_history_quota)} + </Tag> + </Tooltip> + <Tooltip content={'邀请人ID'}> + {record.inviter_id === 0 ? ( + <Tag color='white' size='large'> + 无 + </Tag> + ) : ( + <Tag color='white' size='large'> + {record.inviter_id} + </Tag> + )} + </Tooltip> + </Space> + </div> + ); + }, + }, + { + title: '角色', + dataIndex: 'role', + render: (text, record, index) => { + return <div>{renderRole(text)}</div>; + }, + }, + { + title: '状态', + dataIndex: 'status', + render: (text, record, index) => { + return ( + <div> + {record.DeletedAt !== null ? ( + <Tag color='red'>已注销</Tag> + ) : ( + renderStatus(text) + )} + </div> + ); + }, + }, + { + title: '', + dataIndex: 'operate', + render: (text, record, index) => ( + <div> + {record.DeletedAt !== null ? ( + <></> + ) : ( + <> + <Popconfirm + title='确定?' + okType={'warning'} + onConfirm={() => { + manageUser(record.username, 'promote', record); + }} + > + <Button theme='light' type='warning' style={{ marginRight: 1 }}> + 提升 + </Button> + </Popconfirm> + <Popconfirm + title='确定?' + okType={'warning'} + onConfirm={() => { + manageUser(record.username, 'demote', record); + }} + > + <Button + theme='light' + type='secondary' + style={{ marginRight: 1 }} + > + 降级 + </Button> + </Popconfirm> + {record.status === 1 ? ( + <Button + theme='light' + type='warning' + style={{ marginRight: 1 }} + onClick={async () => { + manageUser(record.username, 'disable', record); + }} + > + 禁用 + </Button> + ) : ( + <Button + theme='light' + type='secondary' + style={{ marginRight: 1 }} + onClick={async () => { + manageUser(record.username, 'enable', record); + }} + disabled={record.status === 3} + > + 启用 + </Button> + )} + <Button + theme='light' + type='tertiary' + style={{ marginRight: 1 }} + onClick={() => { + setEditingUser(record); + setShowEditUser(true); + }} + > + 编辑 + </Button> + </> + )} + <Popconfirm + title='确定是否要删除此用户?' + content='硬删除,此修改将不可逆' + okType={'danger'} + position={'left'} + onConfirm={() => { + manageUser(record.username, 'delete', record).then(() => { + removeRecord(record.id); + }); + }} + > + <Button theme='light' type='danger' style={{ marginRight: 1 }}> + 删除 + </Button> + </Popconfirm> + </div> + ), + }, + ]; const [users, setUsers] = useState([]); const [loading, setLoading] = useState(true); @@ -137,22 +239,22 @@ const UsersTable = () => { const [showAddUser, setShowAddUser] = useState(false); const [showEditUser, setShowEditUser] = useState(false); const [editingUser, setEditingUser] = useState({ - id: undefined + id: undefined, }); const setCount = (data) => { - if (data.length >= (activePage) * ITEMS_PER_PAGE) { + if (data.length >= activePage * ITEMS_PER_PAGE) { setUserCount(data.length + 1); } else { setUserCount(data.length); } }; - const removeRecord = key => { + const removeRecord = (key) => { console.log(key); let newDataSource = [...users]; if (key != null) { - let idx = newDataSource.findIndex(data => data.id === key); + let idx = newDataSource.findIndex((data) => data.id === key); if (idx > -1) { newDataSource.splice(idx, 1); @@ -200,7 +302,8 @@ const UsersTable = () => { const manageUser = async (username, action, record) => { const res = await API.post('/api/user/manage', { - username, action + username, + action, }); const { success, message } = res.data; if (success) { @@ -208,7 +311,6 @@ const UsersTable = () => { let user = res.data.data; let newUsers = [...users]; if (action === 'delete') { - } else { record.status = user.status; record.role = user.role; @@ -222,15 +324,19 @@ const UsersTable = () => { const renderStatus = (status) => { switch (status) { case 1: - return <Tag size="large">已激活</Tag>; + return <Tag size='large'>已激活</Tag>; case 2: - return (<Tag size="large" color="red"> - 已封禁 - </Tag>); + return ( + <Tag size='large' color='red'> + 已封禁 + </Tag> + ); default: - return (<Tag size="large" color="grey"> - 未知状态 - </Tag>); + return ( + <Tag size='large' color='grey'> + 未知状态 + </Tag> + ); } }; @@ -271,16 +377,18 @@ const UsersTable = () => { setLoading(false); }; - const handlePageChange = page => { + const handlePageChange = (page) => { setActivePage(page); if (page === Math.ceil(users.length / ITEMS_PER_PAGE) + 1) { // In this case we have to load more data and then append them. - loadUsers(page - 1).then(r => { - }); + loadUsers(page - 1).then((r) => {}); } }; - const pageData = users.slice((activePage - 1) * ITEMS_PER_PAGE, activePage * ITEMS_PER_PAGE); + const pageData = users.slice( + (activePage - 1) * ITEMS_PER_PAGE, + activePage * ITEMS_PER_PAGE, + ); const closeAddUser = () => { setShowAddUser(false); @@ -289,7 +397,7 @@ const UsersTable = () => { const closeEditUser = () => { setShowEditUser(false); setEditingUser({ - id: undefined + id: undefined, }); }; @@ -303,34 +411,52 @@ const UsersTable = () => { return ( <> - <AddUser refresh={refresh} visible={showAddUser} handleClose={closeAddUser}></AddUser> - <EditUser refresh={refresh} visible={showEditUser} handleClose={closeEditUser} - editingUser={editingUser}></EditUser> + <AddUser + refresh={refresh} + visible={showAddUser} + handleClose={closeAddUser} + ></AddUser> + <EditUser + refresh={refresh} + visible={showEditUser} + handleClose={closeEditUser} + editingUser={editingUser} + ></EditUser> <Form onSubmit={searchUsers}> <Form.Input - label="搜索关键字" - icon="search" - field="keyword" - iconPosition="left" - placeholder="搜索用户的 ID,用户名,显示名称,以及邮箱地址 ..." + label='搜索关键字' + icon='search' + field='keyword' + iconPosition='left' + placeholder='搜索用户的 ID,用户名,显示名称,以及邮箱地址 ...' value={searchKeyword} loading={searching} - onChange={value => handleKeywordChange(value)} + onChange={(value) => handleKeywordChange(value)} /> </Form> - <Table columns={columns} dataSource={pageData} pagination={{ - currentPage: activePage, - pageSize: ITEMS_PER_PAGE, - total: userCount, - pageSizeOpts: [10, 20, 50, 100], - onPageChange: handlePageChange - }} loading={loading} /> - <Button theme="light" type="primary" style={{ marginRight: 8 }} onClick={ - () => { + <Table + columns={columns} + dataSource={pageData} + pagination={{ + currentPage: activePage, + pageSize: ITEMS_PER_PAGE, + total: userCount, + pageSizeOpts: [10, 20, 50, 100], + onPageChange: handlePageChange, + }} + loading={loading} + /> + <Button + theme='light' + type='primary' + style={{ marginRight: 8 }} + onClick={() => { setShowAddUser(true); - } - }>添加用户</Button> + }} + > + 添加用户 + </Button> </> ); }; diff --git a/web/src/components/WeChatIcon.js b/web/src/components/WeChatIcon.js index 22210d9..d3f5742 100644 --- a/web/src/components/WeChatIcon.js +++ b/web/src/components/WeChatIcon.js @@ -3,15 +3,27 @@ import { Icon } from '@douyinfe/semi-ui'; const WeChatIcon = () => { function CustomIcon() { - return <svg t="1709714447384" className="icon" viewBox="0 0 1024 1024" version="1.1" - xmlns="http://www.w3.org/2000/svg" p-id="5091" width="16" height="16"> - <path - d="M690.1 377.4c5.9 0 11.8 0.2 17.6 0.5-24.4-128.7-158.3-227.1-319.9-227.1C209 150.8 64 271.4 64 420.2c0 81.1 43.6 154.2 111.9 203.6 5.5 3.9 9.1 10.3 9.1 17.6 0 2.4-0.5 4.6-1.1 6.9-5.5 20.3-14.2 52.8-14.6 54.3-0.7 2.6-1.7 5.2-1.7 7.9 0 5.9 4.8 10.8 10.8 10.8 2.3 0 4.2-0.9 6.2-2l70.9-40.9c5.3-3.1 11-5 17.2-5 3.2 0 6.4 0.5 9.5 1.4 33.1 9.5 68.8 14.8 105.7 14.8 6 0 11.9-0.1 17.8-0.4-7.1-21-10.9-43.1-10.9-66 0-135.8 132.2-245.8 295.3-245.8z m-194.3-86.5c23.8 0 43.2 19.3 43.2 43.1s-19.3 43.1-43.2 43.1c-23.8 0-43.2-19.3-43.2-43.1s19.4-43.1 43.2-43.1z m-215.9 86.2c-23.8 0-43.2-19.3-43.2-43.1s19.3-43.1 43.2-43.1 43.2 19.3 43.2 43.1-19.4 43.1-43.2 43.1z" - p-id="5092"></path> - <path - d="M866.7 792.7c56.9-41.2 93.2-102 93.2-169.7 0-124-120.8-224.5-269.9-224.5-149 0-269.9 100.5-269.9 224.5S540.9 847.5 690 847.5c30.8 0 60.6-4.4 88.1-12.3 2.6-0.8 5.2-1.2 7.9-1.2 5.2 0 9.9 1.6 14.3 4.1l59.1 34c1.7 1 3.3 1.7 5.2 1.7 2.4 0 4.7-0.9 6.4-2.6 1.7-1.7 2.6-4 2.6-6.4 0-2.2-0.9-4.4-1.4-6.6-0.3-1.2-7.6-28.3-12.2-45.3-0.5-1.9-0.9-3.8-0.9-5.7 0.1-5.9 3.1-11.2 7.6-14.5zM600.2 587.2c-19.9 0-36-16.1-36-35.9 0-19.8 16.1-35.9 36-35.9s36 16.1 36 35.9c0 19.8-16.2 35.9-36 35.9z m179.9 0c-19.9 0-36-16.1-36-35.9 0-19.8 16.1-35.9 36-35.9s36 16.1 36 35.9c-0.1 19.8-16.2 35.9-36 35.9z" - p-id="5093"></path> - </svg>; + return ( + <svg + t='1709714447384' + className='icon' + viewBox='0 0 1024 1024' + version='1.1' + xmlns='http://www.w3.org/2000/svg' + p-id='5091' + width='16' + height='16' + > + <path + d='M690.1 377.4c5.9 0 11.8 0.2 17.6 0.5-24.4-128.7-158.3-227.1-319.9-227.1C209 150.8 64 271.4 64 420.2c0 81.1 43.6 154.2 111.9 203.6 5.5 3.9 9.1 10.3 9.1 17.6 0 2.4-0.5 4.6-1.1 6.9-5.5 20.3-14.2 52.8-14.6 54.3-0.7 2.6-1.7 5.2-1.7 7.9 0 5.9 4.8 10.8 10.8 10.8 2.3 0 4.2-0.9 6.2-2l70.9-40.9c5.3-3.1 11-5 17.2-5 3.2 0 6.4 0.5 9.5 1.4 33.1 9.5 68.8 14.8 105.7 14.8 6 0 11.9-0.1 17.8-0.4-7.1-21-10.9-43.1-10.9-66 0-135.8 132.2-245.8 295.3-245.8z m-194.3-86.5c23.8 0 43.2 19.3 43.2 43.1s-19.3 43.1-43.2 43.1c-23.8 0-43.2-19.3-43.2-43.1s19.4-43.1 43.2-43.1z m-215.9 86.2c-23.8 0-43.2-19.3-43.2-43.1s19.3-43.1 43.2-43.1 43.2 19.3 43.2 43.1-19.4 43.1-43.2 43.1z' + p-id='5092' + ></path> + <path + d='M866.7 792.7c56.9-41.2 93.2-102 93.2-169.7 0-124-120.8-224.5-269.9-224.5-149 0-269.9 100.5-269.9 224.5S540.9 847.5 690 847.5c30.8 0 60.6-4.4 88.1-12.3 2.6-0.8 5.2-1.2 7.9-1.2 5.2 0 9.9 1.6 14.3 4.1l59.1 34c1.7 1 3.3 1.7 5.2 1.7 2.4 0 4.7-0.9 6.4-2.6 1.7-1.7 2.6-4 2.6-6.4 0-2.2-0.9-4.4-1.4-6.6-0.3-1.2-7.6-28.3-12.2-45.3-0.5-1.9-0.9-3.8-0.9-5.7 0.1-5.9 3.1-11.2 7.6-14.5zM600.2 587.2c-19.9 0-36-16.1-36-35.9 0-19.8 16.1-35.9 36-35.9s36 16.1 36 35.9c0 19.8-16.2 35.9-36 35.9z m179.9 0c-19.9 0-36-16.1-36-35.9 0-19.8 16.1-35.9 36-35.9s36 16.1 36 35.9c-0.1 19.8-16.2 35.9-36 35.9z' + p-id='5093' + ></path> + </svg> + ); } return ( diff --git a/web/src/components/utils.js b/web/src/components/utils.js index 5363ba5..59e3a01 100644 --- a/web/src/components/utils.js +++ b/web/src/components/utils.js @@ -15,6 +15,6 @@ export async function onGitHubOAuthClicked(github_client_id) { const state = await getOAuthState(); if (!state) return; window.open( - `https://github.com/login/oauth/authorize?client_id=${github_client_id}&state=${state}&scope=user:email` + `https://github.com/login/oauth/authorize?client_id=${github_client_id}&state=${state}&scope=user:email`, ); -} \ No newline at end of file +} diff --git a/web/src/constants/channel.constants.js b/web/src/constants/channel.constants.js index 9c4165d..8fdfd1b 100644 --- a/web/src/constants/channel.constants.js +++ b/web/src/constants/channel.constants.js @@ -1,22 +1,100 @@ export const CHANNEL_OPTIONS = [ - {key: 1, text: 'OpenAI', value: 1, color: 'green', label: 'OpenAI'}, - {key: 2, text: 'Midjourney Proxy', value: 2, color: 'light-blue', label: 'Midjourney Proxy'}, - {key: 5, text: 'Midjourney Proxy Plus', value: 5, color: 'blue', label: 'Midjourney Proxy Plus'}, - {key: 4, text: 'Ollama', value: 4, color: 'grey', label: 'Ollama'}, - {key: 14, text: 'Anthropic Claude', value: 14, color: 'indigo', label: 'Anthropic Claude'}, - {key: 3, text: 'Azure OpenAI', value: 3, color: 'teal', label: 'Azure OpenAI'}, - {key: 11, text: 'Google PaLM2', value: 11, color: 'orange', label: 'Google PaLM2'}, - {key: 24, text: 'Google Gemini', value: 24, color: 'orange', label: 'Google Gemini'}, - {key: 15, text: '百度文心千帆', value: 15, color: 'blue', label: '百度文心千帆'}, - {key: 17, text: '阿里通义千问', value: 17, color: 'orange', label: '阿里通义千问'}, - {key: 18, text: '讯飞星火认知', value: 18, color: 'blue', label: '讯飞星火认知'}, - {key: 16, text: '智谱 ChatGLM', value: 16, color: 'violet', label: '智谱 ChatGLM'}, - {key: 16, text: '智谱 GLM-4V', value: 26, color: 'purple', label: '智谱 GLM-4V'}, - {key: 16, text: 'Moonshot', value: 25, color: 'green', label: 'Moonshot'}, - {key: 19, text: '360 智脑', value: 19, color: 'blue', label: '360 智脑'}, - {key: 23, text: '腾讯混元', value: 23, color: 'teal', label: '腾讯混元'}, - {key: 31, text: '零一万物', value: 31, color: 'green', label: '零一万物'}, - {key: 8, text: '自定义渠道', value: 8, color: 'pink', label: '自定义渠道'}, - {key: 22, text: '知识库:FastGPT', value: 22, color: 'blue', label: '知识库:FastGPT'}, - {key: 21, text: '知识库:AI Proxy', value: 21, color: 'purple', label: '知识库:AI Proxy'}, + { key: 1, text: 'OpenAI', value: 1, color: 'green', label: 'OpenAI' }, + { + key: 2, + text: 'Midjourney Proxy', + value: 2, + color: 'light-blue', + label: 'Midjourney Proxy', + }, + { + key: 5, + text: 'Midjourney Proxy Plus', + value: 5, + color: 'blue', + label: 'Midjourney Proxy Plus', + }, + { key: 4, text: 'Ollama', value: 4, color: 'grey', label: 'Ollama' }, + { + key: 14, + text: 'Anthropic Claude', + value: 14, + color: 'indigo', + label: 'Anthropic Claude', + }, + { + key: 3, + text: 'Azure OpenAI', + value: 3, + color: 'teal', + label: 'Azure OpenAI', + }, + { + key: 11, + text: 'Google PaLM2', + value: 11, + color: 'orange', + label: 'Google PaLM2', + }, + { + key: 24, + text: 'Google Gemini', + value: 24, + color: 'orange', + label: 'Google Gemini', + }, + { + key: 15, + text: '百度文心千帆', + value: 15, + color: 'blue', + label: '百度文心千帆', + }, + { + key: 17, + text: '阿里通义千问', + value: 17, + color: 'orange', + label: '阿里通义千问', + }, + { + key: 18, + text: '讯飞星火认知', + value: 18, + color: 'blue', + label: '讯飞星火认知', + }, + { + key: 16, + text: '智谱 ChatGLM', + value: 16, + color: 'violet', + label: '智谱 ChatGLM', + }, + { + key: 16, + text: '智谱 GLM-4V', + value: 26, + color: 'purple', + label: '智谱 GLM-4V', + }, + { key: 16, text: 'Moonshot', value: 25, color: 'green', label: 'Moonshot' }, + { key: 19, text: '360 智脑', value: 19, color: 'blue', label: '360 智脑' }, + { key: 23, text: '腾讯混元', value: 23, color: 'teal', label: '腾讯混元' }, + { key: 31, text: '零一万物', value: 31, color: 'green', label: '零一万物' }, + { key: 8, text: '自定义渠道', value: 8, color: 'pink', label: '自定义渠道' }, + { + key: 22, + text: '知识库:FastGPT', + value: 22, + color: 'blue', + label: '知识库:FastGPT', + }, + { + key: 21, + text: '知识库:AI Proxy', + value: 21, + color: 'purple', + label: '知识库:AI Proxy', + }, ]; diff --git a/web/src/constants/index.js b/web/src/constants/index.js index e83152b..1321207 100644 --- a/web/src/constants/index.js +++ b/web/src/constants/index.js @@ -1,4 +1,4 @@ export * from './toast.constants'; export * from './user.constants'; export * from './common.constant'; -export * from './channel.constants'; \ No newline at end of file +export * from './channel.constants'; diff --git a/web/src/constants/toast.constants.js b/web/src/constants/toast.constants.js index 5068472..f8853df 100644 --- a/web/src/constants/toast.constants.js +++ b/web/src/constants/toast.constants.js @@ -3,5 +3,5 @@ export const toastConstants = { INFO_TIMEOUT: 3000, ERROR_TIMEOUT: 5000, WARNING_TIMEOUT: 10000, - NOTICE_TIMEOUT: 20000 + NOTICE_TIMEOUT: 20000, }; diff --git a/web/src/constants/user.constants.js b/web/src/constants/user.constants.js index 2680d8e..cde70df 100644 --- a/web/src/constants/user.constants.js +++ b/web/src/constants/user.constants.js @@ -1,19 +1,19 @@ export const userConstants = { - REGISTER_REQUEST: 'USERS_REGISTER_REQUEST', - REGISTER_SUCCESS: 'USERS_REGISTER_SUCCESS', - REGISTER_FAILURE: 'USERS_REGISTER_FAILURE', + REGISTER_REQUEST: 'USERS_REGISTER_REQUEST', + REGISTER_SUCCESS: 'USERS_REGISTER_SUCCESS', + REGISTER_FAILURE: 'USERS_REGISTER_FAILURE', - LOGIN_REQUEST: 'USERS_LOGIN_REQUEST', - LOGIN_SUCCESS: 'USERS_LOGIN_SUCCESS', - LOGIN_FAILURE: 'USERS_LOGIN_FAILURE', - - LOGOUT: 'USERS_LOGOUT', + LOGIN_REQUEST: 'USERS_LOGIN_REQUEST', + LOGIN_SUCCESS: 'USERS_LOGIN_SUCCESS', + LOGIN_FAILURE: 'USERS_LOGIN_FAILURE', - GETALL_REQUEST: 'USERS_GETALL_REQUEST', - GETALL_SUCCESS: 'USERS_GETALL_SUCCESS', - GETALL_FAILURE: 'USERS_GETALL_FAILURE', + LOGOUT: 'USERS_LOGOUT', - DELETE_REQUEST: 'USERS_DELETE_REQUEST', - DELETE_SUCCESS: 'USERS_DELETE_SUCCESS', - DELETE_FAILURE: 'USERS_DELETE_FAILURE' + GETALL_REQUEST: 'USERS_GETALL_REQUEST', + GETALL_SUCCESS: 'USERS_GETALL_SUCCESS', + GETALL_FAILURE: 'USERS_GETALL_FAILURE', + + DELETE_REQUEST: 'USERS_DELETE_REQUEST', + DELETE_SUCCESS: 'USERS_DELETE_SUCCESS', + DELETE_FAILURE: 'USERS_DELETE_FAILURE', }; diff --git a/web/src/context/Status/index.js b/web/src/context/Status/index.js index 71f0682..5a5319e 100644 --- a/web/src/context/Status/index.js +++ b/web/src/context/Status/index.js @@ -16,4 +16,4 @@ export const StatusProvider = ({ children }) => { {children} </StatusContext.Provider> ); -}; \ No newline at end of file +}; diff --git a/web/src/context/User/index.js b/web/src/context/User/index.js index c667159..033b361 100644 --- a/web/src/context/User/index.js +++ b/web/src/context/User/index.js @@ -1,19 +1,19 @@ // contexts/User/index.jsx -import React from "react" -import { reducer, initialState } from "./reducer" +import React from 'react'; +import { reducer, initialState } from './reducer'; export const UserContext = React.createContext({ state: initialState, - dispatch: () => null -}) + dispatch: () => null, +}); export const UserProvider = ({ children }) => { - const [state, dispatch] = React.useReducer(reducer, initialState) + const [state, dispatch] = React.useReducer(reducer, initialState); return ( - <UserContext.Provider value={[ state, dispatch ]}> - { children } + <UserContext.Provider value={[state, dispatch]}> + {children} </UserContext.Provider> - ) -} \ No newline at end of file + ); +}; diff --git a/web/src/context/User/reducer.js b/web/src/context/User/reducer.js index 9ed1d80..d44cffc 100644 --- a/web/src/context/User/reducer.js +++ b/web/src/context/User/reducer.js @@ -3,12 +3,12 @@ export const reducer = (state, action) => { case 'login': return { ...state, - user: action.payload + user: action.payload, }; case 'logout': return { ...state, - user: undefined + user: undefined, }; default: @@ -17,5 +17,5 @@ export const reducer = (state, action) => { }; export const initialState = { - user: undefined -}; \ No newline at end of file + user: undefined, +}; diff --git a/web/src/helpers/api.js b/web/src/helpers/api.js index 35fdb1e..31a8c14 100644 --- a/web/src/helpers/api.js +++ b/web/src/helpers/api.js @@ -2,12 +2,14 @@ import { showError } from './utils'; import axios from 'axios'; export const API = axios.create({ - baseURL: process.env.REACT_APP_SERVER ? process.env.REACT_APP_SERVER : '', + baseURL: import.meta.env.VITE_REACT_APP_SERVER_URL + ? import.meta.env.VITE_REACT_APP_SERVER_URL + : '', }); API.interceptors.response.use( (response) => response, (error) => { showError(error); - } + }, ); diff --git a/web/src/helpers/auth-header.js b/web/src/helpers/auth-header.js index a8fe5f5..f094dd1 100644 --- a/web/src/helpers/auth-header.js +++ b/web/src/helpers/auth-header.js @@ -1,10 +1,10 @@ export function authHeader() { - // return authorization header with jwt token - let user = JSON.parse(localStorage.getItem('user')); + // return authorization header with jwt token + let user = JSON.parse(localStorage.getItem('user')); - if (user && user.token) { - return { 'Authorization': 'Bearer ' + user.token }; - } else { - return {}; - } -} \ No newline at end of file + if (user && user.token) { + return { Authorization: 'Bearer ' + user.token }; + } else { + return {}; + } +} diff --git a/web/src/helpers/history.js b/web/src/helpers/history.js index 629039e..f529e5d 100644 --- a/web/src/helpers/history.js +++ b/web/src/helpers/history.js @@ -1,3 +1,3 @@ import { createBrowserHistory } from 'history'; -export const history = createBrowserHistory(); \ No newline at end of file +export const history = createBrowserHistory(); diff --git a/web/src/helpers/index.js b/web/src/helpers/index.js index 505a8cf..ac16496 100644 --- a/web/src/helpers/index.js +++ b/web/src/helpers/index.js @@ -1,4 +1,4 @@ export * from './history'; export * from './auth-header'; export * from './utils'; -export * from './api'; \ No newline at end of file +export * from './api'; diff --git a/web/src/helpers/render.js b/web/src/helpers/render.js index 62fb0dc..a71215e 100644 --- a/web/src/helpers/render.js +++ b/web/src/helpers/render.js @@ -1,170 +1,197 @@ -import {Label} from 'semantic-ui-react'; -import {Tag} from "@douyinfe/semi-ui"; +import { Label } from 'semantic-ui-react'; +import { Tag } from '@douyinfe/semi-ui'; export function renderText(text, limit) { - if (text.length > limit) { - return text.slice(0, limit - 3) + '...'; - } - return text; + if (text.length > limit) { + return text.slice(0, limit - 3) + '...'; + } + return text; } export function renderGroup(group) { - if (group === '') { - return <Tag size='large'>default</Tag>; - } - let groups = group.split(','); - groups.sort(); - return <> - {groups.map((group) => { - if (group === 'vip' || group === 'pro') { - return <Tag size='large' color='yellow'>{group}</Tag>; - } else if (group === 'svip' || group === 'premium') { - return <Tag size='large' color='red'>{group}</Tag>; - } - if (group === 'default') { - return <Tag size='large'>{group}</Tag>; - } else { - return <Tag size='large' color={stringToColor(group)}>{group}</Tag>; - } - })} - </>; + if (group === '') { + return <Tag size='large'>default</Tag>; + } + let groups = group.split(','); + groups.sort(); + return ( + <> + {groups.map((group) => { + if (group === 'vip' || group === 'pro') { + return ( + <Tag size='large' color='yellow'> + {group} + </Tag> + ); + } else if (group === 'svip' || group === 'premium') { + return ( + <Tag size='large' color='red'> + {group} + </Tag> + ); + } + if (group === 'default') { + return <Tag size='large'>{group}</Tag>; + } else { + return ( + <Tag size='large' color={stringToColor(group)}> + {group} + </Tag> + ); + } + })} + </> + ); } export function renderNumber(num) { - if (num >= 1000000000) { - return (num / 1000000000).toFixed(1) + 'B'; - } else if (num >= 1000000) { - return (num / 1000000).toFixed(1) + 'M'; - } else if (num >= 10000) { - return (num / 1000).toFixed(1) + 'k'; - } else { - return num; - } + if (num >= 1000000000) { + return (num / 1000000000).toFixed(1) + 'B'; + } else if (num >= 1000000) { + return (num / 1000000).toFixed(1) + 'M'; + } else if (num >= 10000) { + return (num / 1000).toFixed(1) + 'k'; + } else { + return num; + } } export function renderQuotaNumberWithDigit(num, digits = 2) { - let displayInCurrency = localStorage.getItem('display_in_currency'); - num = num.toFixed(digits); - if (displayInCurrency) { - return '$' + num; - } - return num; + let displayInCurrency = localStorage.getItem('display_in_currency'); + num = num.toFixed(digits); + if (displayInCurrency) { + return '$' + num; + } + return num; } export function renderNumberWithPoint(num) { - num = num.toFixed(2); - if (num >= 100000) { - // Convert number to string to manipulate it - let numStr = num.toString(); - // Find the position of the decimal point - let decimalPointIndex = numStr.indexOf('.'); + num = num.toFixed(2); + if (num >= 100000) { + // Convert number to string to manipulate it + let numStr = num.toString(); + // Find the position of the decimal point + let decimalPointIndex = numStr.indexOf('.'); - let wholePart = numStr; - let decimalPart = ''; + let wholePart = numStr; + let decimalPart = ''; - // If there is a decimal point, split the number into whole and decimal parts - if (decimalPointIndex !== -1) { - wholePart = numStr.slice(0, decimalPointIndex); - decimalPart = numStr.slice(decimalPointIndex); - } - - // Take the first two and last two digits of the whole number part - let shortenedWholePart = wholePart.slice(0, 2) + '..' + wholePart.slice(-2); - - // Return the formatted number - return shortenedWholePart + decimalPart; + // If there is a decimal point, split the number into whole and decimal parts + if (decimalPointIndex !== -1) { + wholePart = numStr.slice(0, decimalPointIndex); + decimalPart = numStr.slice(decimalPointIndex); } - // If the number is less than 100,000, return it unmodified - return num; + // Take the first two and last two digits of the whole number part + let shortenedWholePart = wholePart.slice(0, 2) + '..' + wholePart.slice(-2); + + // Return the formatted number + return shortenedWholePart + decimalPart; + } + + // If the number is less than 100,000, return it unmodified + return num; } export function getQuotaPerUnit() { - let quotaPerUnit = localStorage.getItem('quota_per_unit'); - quotaPerUnit = parseFloat(quotaPerUnit); - return quotaPerUnit; + let quotaPerUnit = localStorage.getItem('quota_per_unit'); + quotaPerUnit = parseFloat(quotaPerUnit); + return quotaPerUnit; } export function getQuotaWithUnit(quota, digits = 6) { - let quotaPerUnit = localStorage.getItem('quota_per_unit'); - quotaPerUnit = parseFloat(quotaPerUnit); - return (quota / quotaPerUnit).toFixed(digits); + let quotaPerUnit = localStorage.getItem('quota_per_unit'); + quotaPerUnit = parseFloat(quotaPerUnit); + return (quota / quotaPerUnit).toFixed(digits); } export function renderQuota(quota, digits = 2) { - let quotaPerUnit = localStorage.getItem('quota_per_unit'); - let displayInCurrency = localStorage.getItem('display_in_currency'); - quotaPerUnit = parseFloat(quotaPerUnit); - displayInCurrency = displayInCurrency === 'true'; - if (displayInCurrency) { - return '$' + (quota / quotaPerUnit).toFixed(digits); - } - return renderNumber(quota); + let quotaPerUnit = localStorage.getItem('quota_per_unit'); + let displayInCurrency = localStorage.getItem('display_in_currency'); + quotaPerUnit = parseFloat(quotaPerUnit); + displayInCurrency = displayInCurrency === 'true'; + if (displayInCurrency) { + return '$' + (quota / quotaPerUnit).toFixed(digits); + } + return renderNumber(quota); } export function renderQuotaWithPrompt(quota, digits) { - let displayInCurrency = localStorage.getItem('display_in_currency'); - displayInCurrency = displayInCurrency === 'true'; - if (displayInCurrency) { - return `(等价金额:${renderQuota(quota, digits)})`; - } - return ''; + let displayInCurrency = localStorage.getItem('display_in_currency'); + displayInCurrency = displayInCurrency === 'true'; + if (displayInCurrency) { + return `(等价金额:${renderQuota(quota, digits)})`; + } + return ''; } -const colors = ['amber', 'blue', 'cyan', 'green', 'grey', 'indigo', - 'light-blue', 'lime', 'orange', 'pink', - 'purple', 'red', 'teal', 'violet', 'yellow' -] +const colors = [ + 'amber', + 'blue', + 'cyan', + 'green', + 'grey', + 'indigo', + 'light-blue', + 'lime', + 'orange', + 'pink', + 'purple', + 'red', + 'teal', + 'violet', + 'yellow', +]; export const modelColorMap = { - 'dall-e': 'rgb(147,112,219)', // 深紫色 - 'dall-e-2': 'rgb(147,112,219)', // 介于紫色和蓝色之间的色调 - 'dall-e-3': 'rgb(153,50,204)', // 介于紫罗兰和洋红之间的色调 - 'midjourney': 'rgb(136,43,180)', // 介于紫罗兰和洋红之间的色调 - 'gpt-3.5-turbo': 'rgb(184,227,167)', // 浅绿色 - 'gpt-3.5-turbo-0301': 'rgb(131,220,131)', // 亮绿色 - 'gpt-3.5-turbo-0613': 'rgb(60,179,113)', // 海洋绿 - 'gpt-3.5-turbo-1106': 'rgb(32,178,170)', // 浅海洋绿 - 'gpt-3.5-turbo-16k': 'rgb(252,200,149)', // 淡橙色 - 'gpt-3.5-turbo-16k-0613': 'rgb(255,181,119)', // 淡桃色 - 'gpt-3.5-turbo-instruct': 'rgb(175,238,238)', // 粉蓝色 - 'gpt-4': 'rgb(135,206,235)', // 天蓝色 - 'gpt-4-0314': 'rgb(70,130,180)', // 钢蓝色 - 'gpt-4-0613': 'rgb(100,149,237)', // 矢车菊蓝 - 'gpt-4-1106-preview': 'rgb(30,144,255)', // 道奇蓝 - 'gpt-4-0125-preview': 'rgb(2,177,236)', // 深天蓝 - 'gpt-4-turbo-preview': 'rgb(2,177,255)', // 深天蓝 - 'gpt-4-32k': 'rgb(104,111,238)', // 中紫色 - 'gpt-4-32k-0314': 'rgb(90,105,205)', // 暗灰蓝色 - 'gpt-4-32k-0613': 'rgb(61,71,139)', // 暗蓝灰色 - 'gpt-4-all': 'rgb(65,105,225)', // 皇家蓝 - 'gpt-4-gizmo-*': 'rgb(0,0,255)', // 纯蓝色 - 'gpt-4-vision-preview': 'rgb(25,25,112)', // 午夜蓝 - 'text-ada-001': 'rgb(255,192,203)', // 粉红色 - 'text-babbage-001': 'rgb(255,160,122)', // 浅珊瑚色 - 'text-curie-001': 'rgb(219,112,147)', // 苍紫罗兰色 - 'text-davinci-002': 'rgb(199,21,133)', // 中紫罗兰红色 - 'text-davinci-003': 'rgb(219,112,147)', // 苍紫罗兰色(与Curie相同,表示同一个系列) - 'text-davinci-edit-001': 'rgb(255,105,180)', // 热粉色 - 'text-embedding-ada-002': 'rgb(255,182,193)', // 浅粉红 - 'text-embedding-v1': 'rgb(255,174,185)', // 浅粉红色(略有区别) - 'text-moderation-latest': 'rgb(255,130,171)', // 强粉色 - 'text-moderation-stable': 'rgb(255,160,122)', // 浅珊瑚色(与Babbage相同,表示同一类功能) - 'tts-1': 'rgb(255,140,0)', // 深橙色 - 'tts-1-1106': 'rgb(255,165,0)', // 橙色 - 'tts-1-hd': 'rgb(255,215,0)', // 金色 - 'tts-1-hd-1106': 'rgb(255,223,0)', // 金黄色(略有区别) - 'whisper-1': 'rgb(245,245,220)' // 米色 -} + 'dall-e': 'rgb(147,112,219)', // 深紫色 + 'dall-e-2': 'rgb(147,112,219)', // 介于紫色和蓝色之间的色调 + 'dall-e-3': 'rgb(153,50,204)', // 介于紫罗兰和洋红之间的色调 + midjourney: 'rgb(136,43,180)', // 介于紫罗兰和洋红之间的色调 + 'gpt-3.5-turbo': 'rgb(184,227,167)', // 浅绿色 + 'gpt-3.5-turbo-0301': 'rgb(131,220,131)', // 亮绿色 + 'gpt-3.5-turbo-0613': 'rgb(60,179,113)', // 海洋绿 + 'gpt-3.5-turbo-1106': 'rgb(32,178,170)', // 浅海洋绿 + 'gpt-3.5-turbo-16k': 'rgb(252,200,149)', // 淡橙色 + 'gpt-3.5-turbo-16k-0613': 'rgb(255,181,119)', // 淡桃色 + 'gpt-3.5-turbo-instruct': 'rgb(175,238,238)', // 粉蓝色 + 'gpt-4': 'rgb(135,206,235)', // 天蓝色 + 'gpt-4-0314': 'rgb(70,130,180)', // 钢蓝色 + 'gpt-4-0613': 'rgb(100,149,237)', // 矢车菊蓝 + 'gpt-4-1106-preview': 'rgb(30,144,255)', // 道奇蓝 + 'gpt-4-0125-preview': 'rgb(2,177,236)', // 深天蓝 + 'gpt-4-turbo-preview': 'rgb(2,177,255)', // 深天蓝 + 'gpt-4-32k': 'rgb(104,111,238)', // 中紫色 + 'gpt-4-32k-0314': 'rgb(90,105,205)', // 暗灰蓝色 + 'gpt-4-32k-0613': 'rgb(61,71,139)', // 暗蓝灰色 + 'gpt-4-all': 'rgb(65,105,225)', // 皇家蓝 + 'gpt-4-gizmo-*': 'rgb(0,0,255)', // 纯蓝色 + 'gpt-4-vision-preview': 'rgb(25,25,112)', // 午夜蓝 + 'text-ada-001': 'rgb(255,192,203)', // 粉红色 + 'text-babbage-001': 'rgb(255,160,122)', // 浅珊瑚色 + 'text-curie-001': 'rgb(219,112,147)', // 苍紫罗兰色 + 'text-davinci-002': 'rgb(199,21,133)', // 中紫罗兰红色 + 'text-davinci-003': 'rgb(219,112,147)', // 苍紫罗兰色(与Curie相同,表示同一个系列) + 'text-davinci-edit-001': 'rgb(255,105,180)', // 热粉色 + 'text-embedding-ada-002': 'rgb(255,182,193)', // 浅粉红 + 'text-embedding-v1': 'rgb(255,174,185)', // 浅粉红色(略有区别) + 'text-moderation-latest': 'rgb(255,130,171)', // 强粉色 + 'text-moderation-stable': 'rgb(255,160,122)', // 浅珊瑚色(与Babbage相同,表示同一类功能) + 'tts-1': 'rgb(255,140,0)', // 深橙色 + 'tts-1-1106': 'rgb(255,165,0)', // 橙色 + 'tts-1-hd': 'rgb(255,215,0)', // 金色 + 'tts-1-hd-1106': 'rgb(255,223,0)', // 金黄色(略有区别) + 'whisper-1': 'rgb(245,245,220)', // 米色 +}; export function stringToColor(str) { - let sum = 0; - // 对字符串中的每个字符进行操作 - for (let i = 0; i < str.length; i++) { - // 将字符的ASCII值加到sum中 - sum += str.charCodeAt(i); - } - // 使用模运算得到个位数 - let i = sum % colors.length; - return colors[i]; -} \ No newline at end of file + let sum = 0; + // 对字符串中的每个字符进行操作 + for (let i = 0; i < str.length; i++) { + // 将字符的ASCII值加到sum中 + sum += str.charCodeAt(i); + } + // 使用模运算得到个位数 + let i = sum % colors.length; + return colors[i]; +} diff --git a/web/src/helpers/utils.js b/web/src/helpers/utils.js index 3e1fdb2..78fa3d6 100644 --- a/web/src/helpers/utils.js +++ b/web/src/helpers/utils.js @@ -1,7 +1,7 @@ import { Toast } from '@douyinfe/semi-ui'; import { toastConstants } from '../constants'; import React from 'react'; -import {toast} from "react-toastify"; +import { toast } from 'react-toastify'; const HTMLToastContent = ({ htmlContent }) => { return <div dangerouslySetInnerHTML={{ __html: htmlContent }} />; @@ -30,7 +30,7 @@ export function getSystemName() { export function getLogo() { let logo = localStorage.getItem('logo'); if (!logo) return '/logo.png'; - return logo + return logo; } export function getFooterHTML() { @@ -157,17 +157,7 @@ export function timestamp2string(timestamp) { second = '0' + second; } return ( - year + - '-' + - month + - '-' + - day + - ' ' + - hour + - ':' + - minute + - ':' + - second + year + '-' + month + '-' + day + ' ' + hour + ':' + minute + ':' + second ); } @@ -186,20 +176,20 @@ export function timestamp2string1(timestamp, dataExportDefaultTime = 'hour') { if (hour.length === 1) { hour = '0' + hour; } - let str = month + '-' + day + let str = month + '-' + day; if (dataExportDefaultTime === 'hour') { - str += ' ' + hour + ":00" + str += ' ' + hour + ':00'; } else if (dataExportDefaultTime === 'week') { let nextWeek = new Date(timestamp * 1000 + 6 * 24 * 60 * 60 * 1000); let nextMonth = (nextWeek.getMonth() + 1).toString(); let nextDay = nextWeek.getDate().toString(); if (nextMonth.length === 1) { - nextMonth = '0' + nextMonth; + nextMonth = '0' + nextMonth; } if (nextDay.length === 1) { - nextDay = '0' + nextDay; + nextDay = '0' + nextDay; } - str += ' - ' + nextMonth + '-' + nextDay + str += ' - ' + nextMonth + '-' + nextDay; } return str; } @@ -225,9 +215,8 @@ export const verifyJSON = (str) => { export function shouldShowPrompt(id) { let prompt = localStorage.getItem(`prompt-${id}`); return !prompt; - } export function setPromptShown(id) { localStorage.setItem(`prompt-${id}`, 'true'); -} \ No newline at end of file +} diff --git a/web/src/index.css b/web/src/index.css index 8e53624..9c77d18 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -1,105 +1,109 @@ body { - margin: 0; - padding-top: 55px; - overflow-y: scroll; - font-family: Lato, 'Helvetica Neue', Arial, Helvetica, "Microsoft YaHei", sans-serif; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - scrollbar-width: none; - color: var(--semi-color-text-0) !important; - background-color: var( --semi-color-bg-0) !important; - height: 100%; + margin: 0; + padding-top: 55px; + overflow-y: scroll; + font-family: Lato, 'Helvetica Neue', Arial, Helvetica, 'Microsoft YaHei', + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + scrollbar-width: none; + color: var(--semi-color-text-0) !important; + background-color: var(--semi-color-bg-0) !important; + height: 100%; } #root { - height: 100%; + height: 100%; } @media only screen and (max-width: 767px) { - .semi-table-tbody, .semi-table-row, .semi-table-row-cell { - display: block!important; - width: auto!important; - padding: 2px!important; - } - .semi-table-row-cell { - border-bottom: 0!important; - } - .semi-table-tbody>.semi-table-row { - border-bottom: 1px solid rgba(0,0,0,.1); - } - .semi-space { - /*display: block!important;*/ - display: flex; - flex-direction: row; - flex-wrap: wrap; - row-gap: 3px; - column-gap: 10px; - } + .semi-table-tbody, + .semi-table-row, + .semi-table-row-cell { + display: block !important; + width: auto !important; + padding: 2px !important; + } + .semi-table-row-cell { + border-bottom: 0 !important; + } + .semi-table-tbody > .semi-table-row { + border-bottom: 1px solid rgba(0, 0, 0, 0.1); + } + .semi-space { + /*display: block!important;*/ + display: flex; + flex-direction: row; + flex-wrap: wrap; + row-gap: 3px; + column-gap: 10px; + } } .semi-table-tbody > .semi-table-row > .semi-table-row-cell { - padding: 16px 14px; + padding: 16px 14px; } .channel-table { - .semi-table-tbody > .semi-table-row > .semi-table-row-cell { - padding: 16px 8px; - } + .semi-table-tbody > .semi-table-row > .semi-table-row-cell { + padding: 16px 8px; + } } .semi-layout { - height: 100%; + height: 100%; } .tableShow { - display: revert; + display: revert; } .tableHiddle { - display: none !important; + display: none !important; } body::-webkit-scrollbar { - display: none; + display: none; } code { - font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace; + font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', + monospace; } .semi-navigation-vertical { - /*display: flex;*/ - /*flex-direction: column;*/ + /*display: flex;*/ + /*flex-direction: column;*/ } .semi-navigation-item { - margin-bottom: 0; + margin-bottom: 0; } .semi-navigation-vertical { - /*flex: 0 0 auto;*/ - /*display: flex;*/ - /*flex-direction: column;*/ - /*width: 100%;*/ - height: 100%; - overflow: hidden; + /*flex: 0 0 auto;*/ + /*display: flex;*/ + /*flex-direction: column;*/ + /*width: 100%;*/ + height: 100%; + overflow: hidden; } .main-content { - padding: 4px; - height: 100%; + padding: 4px; + height: 100%; } .small-icon .icon { - font-size: 1em !important; + font-size: 1em !important; } .custom-footer { - font-size: 1.1em; + font-size: 1.1em; } @media only screen and (max-width: 600px) { - .hide-on-mobile { - display: none !important; - } + .hide-on-mobile { + display: none !important; + } } diff --git a/web/src/index.js b/web/src/index.js index 25b1d39..c73daef 100644 --- a/web/src/index.js +++ b/web/src/index.js @@ -1,54 +1,50 @@ -import { initVChartSemiTheme } from '@visactor/vchart-semi-theme'; import React from 'react'; import ReactDOM from 'react-dom/client'; -import {BrowserRouter} from 'react-router-dom'; +import { BrowserRouter } from 'react-router-dom'; import App from './App'; import HeaderBar from './components/HeaderBar'; import Footer from './components/Footer'; -import 'semantic-ui-css/semantic.min.css'; +import 'semantic-ui-offline/semantic.min.css'; import './index.css'; -import {UserProvider} from './context/User'; -import {ToastContainer} from 'react-toastify'; +import { UserProvider } from './context/User'; +import { ToastContainer } from 'react-toastify'; import 'react-toastify/dist/ReactToastify.css'; -import {StatusProvider} from './context/Status'; -import {Layout} from "@douyinfe/semi-ui"; -import SiderBar from "./components/SiderBar"; +import { StatusProvider } from './context/Status'; +import { Layout } from '@douyinfe/semi-ui'; +import SiderBar from './components/SiderBar'; // initialization -initVChartSemiTheme({ - isWatchingThemeSwitch: true, -}); const root = ReactDOM.createRoot(document.getElementById('root')); -const {Sider, Content, Header} = Layout; +const { Sider, Content, Header } = Layout; root.render( - <React.StrictMode> - <StatusProvider> - <UserProvider> - <BrowserRouter> - <Layout> - <Sider> - <SiderBar/> - </Sider> - <Layout> - <Header> - <HeaderBar/> - </Header> - <Content - style={{ - padding: '24px', - }} - > - <App/> - </Content> - <Layout.Footer> - <Footer></Footer> - </Layout.Footer> - </Layout> - <ToastContainer/> - </Layout> - </BrowserRouter> - </UserProvider> - </StatusProvider> - </React.StrictMode> + <React.StrictMode> + <StatusProvider> + <UserProvider> + <BrowserRouter> + <Layout> + <Sider> + <SiderBar /> + </Sider> + <Layout> + <Header> + <HeaderBar /> + </Header> + <Content + style={{ + padding: '24px', + }} + > + <App /> + </Content> + <Layout.Footer> + <Footer></Footer> + </Layout.Footer> + </Layout> + <ToastContainer /> + </Layout> + </BrowserRouter> + </UserProvider> + </StatusProvider> + </React.StrictMode>, ); diff --git a/web/src/pages/About/index.js b/web/src/pages/About/index.js index c9c2fbf..35cc676 100644 --- a/web/src/pages/About/index.js +++ b/web/src/pages/About/index.js @@ -1,7 +1,7 @@ import React, { useEffect, useState } from 'react'; import { API, showError } from '../../helpers'; import { marked } from 'marked'; -import {Layout} from "@douyinfe/semi-ui"; +import { Layout } from '@douyinfe/semi-ui'; const About = () => { const [about, setAbout] = useState(''); @@ -31,37 +31,42 @@ const About = () => { return ( <> - { - aboutLoaded && about === '' ? <> + {aboutLoaded && about === '' ? ( + <> <Layout> <Layout.Header> <h3>关于</h3> </Layout.Header> <Layout.Content> - <p> - 可在设置页面设置关于内容,支持 HTML & Markdown - </p> + <p>可在设置页面设置关于内容,支持 HTML & Markdown</p> new-api项目仓库地址: <a href='https://github.com/Calcium-Ion/new-api'> https://github.com/Calcium-Ion/new-api </a> <p> - NewAPI © 2023 CalciumIon | 基于 One API v0.5.4 © 2023 JustSong。本项目根据MIT许可证授权。 + NewAPI © 2023 CalciumIon | 基于 One API v0.5.4 © 2023 + JustSong。本项目根据MIT许可证授权。 </p> </Layout.Content> </Layout> - </> : <> - { - about.startsWith('https://') ? <iframe + </> + ) : ( + <> + {about.startsWith('https://') ? ( + <iframe src={about} style={{ width: '100%', height: '100vh', border: 'none' }} - /> : <div style={{ fontSize: 'larger' }} dangerouslySetInnerHTML={{ __html: about }}></div> - } + /> + ) : ( + <div + style={{ fontSize: 'larger' }} + dangerouslySetInnerHTML={{ __html: about }} + ></div> + )} </> - } + )} </> ); }; - export default About; diff --git a/web/src/pages/Channel/EditChannel.js b/web/src/pages/Channel/EditChannel.js index fac136d..ab0a373 100644 --- a/web/src/pages/Channel/EditChannel.js +++ b/web/src/pages/Channel/EditChannel.js @@ -1,631 +1,733 @@ -import React, {useEffect, useRef, useState} from 'react'; -import {useNavigate, useParams} from 'react-router-dom'; -import {API, isMobile, showError, showInfo, showSuccess, verifyJSON} from '../../helpers'; -import {CHANNEL_OPTIONS} from '../../constants'; -import Title from "@douyinfe/semi-ui/lib/es/typography/title"; -import {SideSheet, Space, Spin, Button, Input, Typography, Select, TextArea, Checkbox, Banner} from "@douyinfe/semi-ui"; +import React, { useEffect, useRef, useState } from 'react'; +import { useNavigate, useParams } from 'react-router-dom'; +import { + API, + isMobile, + showError, + showInfo, + showSuccess, + verifyJSON, +} from '../../helpers'; +import { CHANNEL_OPTIONS } from '../../constants'; +import Title from '@douyinfe/semi-ui/lib/es/typography/title'; +import { + SideSheet, + Space, + Spin, + Button, + Input, + Typography, + Select, + TextArea, + Checkbox, + Banner, +} from '@douyinfe/semi-ui'; const MODEL_MAPPING_EXAMPLE = { - 'gpt-3.5-turbo-0301': 'gpt-3.5-turbo', - 'gpt-4-0314': 'gpt-4', - 'gpt-4-32k-0314': 'gpt-4-32k' + 'gpt-3.5-turbo-0301': 'gpt-3.5-turbo', + 'gpt-4-0314': 'gpt-4', + 'gpt-4-32k-0314': 'gpt-4-32k', }; function type2secretPrompt(type) { - // inputs.type === 15 ? '按照如下格式输入:APIKey|SecretKey' : (inputs.type === 18 ? '按照如下格式输入:APPID|APISecret|APIKey' : '请输入渠道对应的鉴权密钥') - switch (type) { - case 15: - return '按照如下格式输入:APIKey|SecretKey'; - case 18: - return '按照如下格式输入:APPID|APISecret|APIKey'; - case 22: - return '按照如下格式输入:APIKey-AppId,例如:fastgpt-0sp2gtvfdgyi4k30jwlgwf1i-64f335d84283f05518e9e041'; - case 23: - return '按照如下格式输入:AppId|SecretId|SecretKey'; - default: - return '请输入渠道对应的鉴权密钥'; - } + // inputs.type === 15 ? '按照如下格式输入:APIKey|SecretKey' : (inputs.type === 18 ? '按照如下格式输入:APPID|APISecret|APIKey' : '请输入渠道对应的鉴权密钥') + switch (type) { + case 15: + return '按照如下格式输入:APIKey|SecretKey'; + case 18: + return '按照如下格式输入:APPID|APISecret|APIKey'; + case 22: + return '按照如下格式输入:APIKey-AppId,例如:fastgpt-0sp2gtvfdgyi4k30jwlgwf1i-64f335d84283f05518e9e041'; + case 23: + return '按照如下格式输入:AppId|SecretId|SecretKey'; + default: + return '请输入渠道对应的鉴权密钥'; + } } const EditChannel = (props) => { - const navigate = useNavigate(); - const channelId = props.editingChannel.id; - const isEdit = channelId !== undefined; - const [loading, setLoading] = useState(isEdit); - const handleCancel = () => { - props.handleClose() - }; - const originInputs = { - name: '', - type: 1, - key: '', - openai_organization: '', - base_url: '', - other: '', - model_mapping: '', - models: [], - auto_ban: 1, - groups: ['default'] - }; - const [batch, setBatch] = useState(false); - const [autoBan, setAutoBan] = useState(true); - // const [autoBan, setAutoBan] = useState(true); - const [inputs, setInputs] = useState(originInputs); - const [originModelOptions, setOriginModelOptions] = useState([]); - const [modelOptions, setModelOptions] = useState([]); - const [groupOptions, setGroupOptions] = useState([]); - const [basicModels, setBasicModels] = useState([]); - const [fullModels, setFullModels] = useState([]); - const [customModel, setCustomModel] = useState(''); - const handleInputChange = (name, value) => { - setInputs((inputs) => ({...inputs, [name]: value})); - if (name === 'type' && inputs.models.length === 0) { - let localModels = []; - switch (value) { - case 14: - localModels = ["claude-instant-1.2", "claude-2", "claude-2.0", "claude-2.1", "claude-3-opus-20240229", "claude-3-sonnet-20240229", "claude-3-haiku-20240307"]; - break; - case 11: - localModels = ['PaLM-2']; - break; - case 15: - localModels = ['ERNIE-Bot', 'ERNIE-Bot-turbo', 'ERNIE-Bot-4', 'Embedding-V1']; - break; - case 17: - localModels = ["qwen-turbo", "qwen-plus", "qwen-max", "qwen-max-longcontext", 'text-embedding-v1']; - break; - case 16: - localModels = ['chatglm_pro', 'chatglm_std', 'chatglm_lite']; - break; - case 18: - localModels = ['SparkDesk', 'SparkDesk-v1.1', 'SparkDesk-v2.1', 'SparkDesk-v3.1', 'SparkDesk-v3.5']; - break; - case 19: - localModels = ['360GPT_S2_V9', 'embedding-bert-512-v1', 'embedding_s1_v1', 'semantic_similarity_s1_v1']; - break; - case 23: - localModels = ['hunyuan']; - break; - case 24: - localModels = ['gemini-pro', 'gemini-pro-vision']; - break; - case 25: - localModels = ['moonshot-v1-8k', 'moonshot-v1-32k', 'moonshot-v1-128k']; - break; - case 26: - localModels = ['glm-4', 'glm-4v', 'glm-3-turbo']; - break; - case 31: - localModels = ['yi-34b-chat-0205', 'yi-34b-chat-200k', 'yi-vl-plus']; - break; - case 2: - localModels = ['mj_imagine', 'mj_variation', 'mj_reroll', 'mj_blend', 'mj_upscale', 'mj_describe']; - break; - case 5: - localModels = [ - 'swap_face', - 'mj_imagine', - 'mj_variation', - 'mj_reroll', - 'mj_blend', - 'mj_upscale', - 'mj_describe', - 'mj_zoom', - 'mj_shorten', - 'mj_modal', - 'mj_inpaint', - 'mj_custom_zoom', - 'mj_high_variation', - 'mj_low_variation', - 'mj_pan', - ]; - break; - } - setInputs((inputs) => ({...inputs, models: localModels})); - } - //setAutoBan - }; + const navigate = useNavigate(); + const channelId = props.editingChannel.id; + const isEdit = channelId !== undefined; + const [loading, setLoading] = useState(isEdit); + const handleCancel = () => { + props.handleClose(); + }; + const originInputs = { + name: '', + type: 1, + key: '', + openai_organization: '', + base_url: '', + other: '', + model_mapping: '', + models: [], + auto_ban: 1, + groups: ['default'], + }; + const [batch, setBatch] = useState(false); + const [autoBan, setAutoBan] = useState(true); + // const [autoBan, setAutoBan] = useState(true); + const [inputs, setInputs] = useState(originInputs); + const [originModelOptions, setOriginModelOptions] = useState([]); + const [modelOptions, setModelOptions] = useState([]); + const [groupOptions, setGroupOptions] = useState([]); + const [basicModels, setBasicModels] = useState([]); + const [fullModels, setFullModels] = useState([]); + const [customModel, setCustomModel] = useState(''); + const handleInputChange = (name, value) => { + setInputs((inputs) => ({ ...inputs, [name]: value })); + if (name === 'type' && inputs.models.length === 0) { + let localModels = []; + switch (value) { + case 14: + localModels = [ + 'claude-instant-1.2', + 'claude-2', + 'claude-2.0', + 'claude-2.1', + 'claude-3-opus-20240229', + 'claude-3-sonnet-20240229', + 'claude-3-haiku-20240307', + ]; + break; + case 11: + localModels = ['PaLM-2']; + break; + case 15: + localModels = [ + 'ERNIE-Bot', + 'ERNIE-Bot-turbo', + 'ERNIE-Bot-4', + 'Embedding-V1', + ]; + break; + case 17: + localModels = [ + 'qwen-turbo', + 'qwen-plus', + 'qwen-max', + 'qwen-max-longcontext', + 'text-embedding-v1', + ]; + break; + case 16: + localModels = ['chatglm_pro', 'chatglm_std', 'chatglm_lite']; + break; + case 18: + localModels = [ + 'SparkDesk', + 'SparkDesk-v1.1', + 'SparkDesk-v2.1', + 'SparkDesk-v3.1', + 'SparkDesk-v3.5', + ]; + break; + case 19: + localModels = [ + '360GPT_S2_V9', + 'embedding-bert-512-v1', + 'embedding_s1_v1', + 'semantic_similarity_s1_v1', + ]; + break; + case 23: + localModels = ['hunyuan']; + break; + case 24: + localModels = ['gemini-pro', 'gemini-pro-vision']; + break; + case 25: + localModels = [ + 'moonshot-v1-8k', + 'moonshot-v1-32k', + 'moonshot-v1-128k', + ]; + break; + case 26: + localModels = ['glm-4', 'glm-4v', 'glm-3-turbo']; + break; + case 31: + localModels = ['yi-34b-chat-0205', 'yi-34b-chat-200k', 'yi-vl-plus']; + break; + case 2: + localModels = [ + 'mj_imagine', + 'mj_variation', + 'mj_reroll', + 'mj_blend', + 'mj_upscale', + 'mj_describe', + ]; + break; + case 5: + localModels = [ + 'swap_face', + 'mj_imagine', + 'mj_variation', + 'mj_reroll', + 'mj_blend', + 'mj_upscale', + 'mj_describe', + 'mj_zoom', + 'mj_shorten', + 'mj_modal', + 'mj_inpaint', + 'mj_custom_zoom', + 'mj_high_variation', + 'mj_low_variation', + 'mj_pan', + ]; + break; + } + setInputs((inputs) => ({ ...inputs, models: localModels })); + } + //setAutoBan + }; + const loadChannel = async () => { + setLoading(true); + let res = await API.get(`/api/channel/${channelId}`); + const { success, message, data } = res.data; + if (success) { + if (data.models === '') { + data.models = []; + } else { + data.models = data.models.split(','); + } + if (data.group === '') { + data.groups = []; + } else { + data.groups = data.group.split(','); + } + if (data.model_mapping !== '') { + data.model_mapping = JSON.stringify( + JSON.parse(data.model_mapping), + null, + 2, + ); + } + setInputs(data); + if (data.auto_ban === 0) { + setAutoBan(false); + } else { + setAutoBan(true); + } + // console.log(data); + } else { + showError(message); + } + setLoading(false); + }; - const loadChannel = async () => { - setLoading(true) - let res = await API.get(`/api/channel/${channelId}`); - const {success, message, data} = res.data; - if (success) { - if (data.models === '') { - data.models = []; - } else { - data.models = data.models.split(','); - } - if (data.group === '') { - data.groups = []; - } else { - data.groups = data.group.split(','); - } - if (data.model_mapping !== '') { - data.model_mapping = JSON.stringify(JSON.parse(data.model_mapping), null, 2); - } - setInputs(data); - if (data.auto_ban === 0) { - setAutoBan(false); - } else { - setAutoBan(true); - } - // console.log(data); - } else { - showError(message); - } - setLoading(false); - }; + const fetchModels = async () => { + try { + let res = await API.get(`/api/channel/models`); + let localModelOptions = res.data.data.map((model) => ({ + label: model.id, + value: model.id, + })); + setOriginModelOptions(localModelOptions); + setFullModels(res.data.data.map((model) => model.id)); + setBasicModels( + res.data.data + .filter((model) => { + return model.id.startsWith('gpt-3') || model.id.startsWith('text-'); + }) + .map((model) => model.id), + ); + } catch (error) { + showError(error.message); + } + }; - const fetchModels = async () => { - try { - let res = await API.get(`/api/channel/models`); - let localModelOptions = res.data.data.map((model) => ({ - label: model.id, - value: model.id - })); - setOriginModelOptions(localModelOptions); - setFullModels(res.data.data.map((model) => model.id)); - setBasicModels(res.data.data.filter((model) => { - return model.id.startsWith('gpt-3') || model.id.startsWith('text-'); - }).map((model) => model.id)); - } catch (error) { - showError(error.message); - } - }; + const fetchGroups = async () => { + try { + let res = await API.get(`/api/group/`); + setGroupOptions( + res.data.data.map((group) => ({ + label: group, + value: group, + })), + ); + } catch (error) { + showError(error.message); + } + }; - const fetchGroups = async () => { - try { - let res = await API.get(`/api/group/`); - setGroupOptions(res.data.data.map((group) => ({ - label: group, - value: group - }))); - } catch (error) { - showError(error.message); - } - }; - - useEffect(() => { - let localModelOptions = [...originModelOptions]; - inputs.models.forEach((model) => { - if (!localModelOptions.find((option) => option.key === model)) { - localModelOptions.push({ - label: model, - value: model - }); - } - }); - setModelOptions(localModelOptions); - }, [originModelOptions, inputs.models]); - - useEffect(() => { - fetchModels().then(); - fetchGroups().then(); - if (isEdit) { - loadChannel().then( - () => { - - } - ); - } else { - setInputs(originInputs) - } - }, [props.editingChannel.id]); - - - const submit = async () => { - if (!isEdit && (inputs.name === '' || inputs.key === '')) { - showInfo('请填写渠道名称和渠道密钥!'); - return; - } - if (inputs.models.length === 0) { - showInfo('请至少选择一个模型!'); - return; - } - if (inputs.model_mapping !== '' && !verifyJSON(inputs.model_mapping)) { - showInfo('模型映射必须是合法的 JSON 格式!'); - return; - } - let localInputs = {...inputs}; - if (localInputs.base_url && localInputs.base_url.endsWith('/')) { - localInputs.base_url = localInputs.base_url.slice(0, localInputs.base_url.length - 1); - } - if (localInputs.type === 3 && localInputs.other === '') { - localInputs.other = '2023-06-01-preview'; - } - if (localInputs.type === 18 && localInputs.other === '') { - localInputs.other = 'v2.1'; - } - let res; - if (!Array.isArray(localInputs.models)) { - showError('提交失败,请勿重复提交!'); - handleCancel(); - return; - } - localInputs.auto_ban = autoBan ? 1 : 0; - localInputs.models = localInputs.models.join(','); - localInputs.group = localInputs.groups.join(','); - if (isEdit) { - res = await API.put(`/api/channel/`, {...localInputs, id: parseInt(channelId)}); - } else { - res = await API.post(`/api/channel/`, localInputs); - } - const {success, message} = res.data; - if (success) { - if (isEdit) { - showSuccess('渠道更新成功!'); - } else { - showSuccess('渠道创建成功!'); - setInputs(originInputs); - } - props.refresh(); - props.handleClose(); - } else { - showError(message); - } - }; - - const addCustomModel = () => { - if (customModel.trim() === '') return; - if (inputs.models.includes(customModel)) return showError("该模型已存在!"); - let localModels = [...inputs.models]; - localModels.push(customModel); - let localModelOptions = []; + useEffect(() => { + let localModelOptions = [...originModelOptions]; + inputs.models.forEach((model) => { + if (!localModelOptions.find((option) => option.key === model)) { localModelOptions.push({ - key: customModel, - text: customModel, - value: customModel + label: model, + value: model, }); - setModelOptions(modelOptions => { - return [...modelOptions, ...localModelOptions]; - }); - setCustomModel(''); - handleInputChange('models', localModels); - }; + } + }); + setModelOptions(localModelOptions); + }, [originModelOptions, inputs.models]); - return ( - <> - <SideSheet - maskClosable={false} - placement={isEdit ? 'right' : 'left'} - title={<Title level={3}>{isEdit ? '更新渠道信息' : '创建新的渠道'}} - headerStyle={{borderBottom: '1px solid var(--semi-color-border)'}} - bodyStyle={{borderBottom: '1px solid var(--semi-color-border)'}} - visible={props.visible} - footer={ -
- - - - -
+ useEffect(() => { + fetchModels().then(); + fetchGroups().then(); + if (isEdit) { + loadChannel().then(() => {}); + } else { + setInputs(originInputs); + } + }, [props.editingChannel.id]); + + const submit = async () => { + if (!isEdit && (inputs.name === '' || inputs.key === '')) { + showInfo('请填写渠道名称和渠道密钥!'); + return; + } + if (inputs.models.length === 0) { + showInfo('请至少选择一个模型!'); + return; + } + if (inputs.model_mapping !== '' && !verifyJSON(inputs.model_mapping)) { + showInfo('模型映射必须是合法的 JSON 格式!'); + return; + } + let localInputs = { ...inputs }; + if (localInputs.base_url && localInputs.base_url.endsWith('/')) { + localInputs.base_url = localInputs.base_url.slice( + 0, + localInputs.base_url.length - 1, + ); + } + if (localInputs.type === 3 && localInputs.other === '') { + localInputs.other = '2023-06-01-preview'; + } + if (localInputs.type === 18 && localInputs.other === '') { + localInputs.other = 'v2.1'; + } + let res; + if (!Array.isArray(localInputs.models)) { + showError('提交失败,请勿重复提交!'); + handleCancel(); + return; + } + localInputs.auto_ban = autoBan ? 1 : 0; + localInputs.models = localInputs.models.join(','); + localInputs.group = localInputs.groups.join(','); + if (isEdit) { + res = await API.put(`/api/channel/`, { + ...localInputs, + id: parseInt(channelId), + }); + } else { + res = await API.post(`/api/channel/`, localInputs); + } + const { success, message } = res.data; + if (success) { + if (isEdit) { + showSuccess('渠道更新成功!'); + } else { + showSuccess('渠道创建成功!'); + setInputs(originInputs); + } + props.refresh(); + props.handleClose(); + } else { + showError(message); + } + }; + + const addCustomModel = () => { + if (customModel.trim() === '') return; + if (inputs.models.includes(customModel)) return showError('该模型已存在!'); + let localModels = [...inputs.models]; + localModels.push(customModel); + let localModelOptions = []; + localModelOptions.push({ + key: customModel, + text: customModel, + value: customModel, + }); + setModelOptions((modelOptions) => { + return [...modelOptions, ...localModelOptions]; + }); + setCustomModel(''); + handleInputChange('models', localModels); + }; + + return ( + <> + {isEdit ? '更新渠道信息' : '创建新的渠道'} + } + headerStyle={{ borderBottom: '1px solid var(--semi-color-border)' }} + bodyStyle={{ borderBottom: '1px solid var(--semi-color-border)' }} + visible={props.visible} + footer={ +
+ + + + +
+ } + closeIcon={null} + onCancel={() => handleCancel()} + width={isMobile() ? '100%' : 600} + > + +
+ 类型: +
+ handleCancel()} - width={isMobile() ? '100%' : 600} - > - -
- 类型: -
- { - handleInputChange('base_url', value) - }} - value={inputs.base_url} - autoComplete='new-password' - /> -
- 默认 API 版本: -
- { - handleInputChange('other', value) - }} - value={inputs.other} - autoComplete='new-password' - /> - - ) - } - { - inputs.type === 8 && ( - <> -
- Base URL: -
- { - handleInputChange('base_url', value) - }} - value={inputs.base_url} - autoComplete='new-password' - /> - - ) - } -
- 名称: -
- { - handleInputChange('name', value) - }} - value={inputs.name} - autoComplete='new-password' - /> -
- 分组: -
- { - handleInputChange('other', value) - }} - value={inputs.other} - autoComplete='new-password' - /> - - ) - } - { - inputs.type === 21 && ( - <> -
- 知识库 ID: -
- { - handleInputChange('other', value) - }} - value={inputs.other} - autoComplete='new-password' - /> - - ) - } -
- 模型: -
- 填入 - } - placeholder='输入自定义模型名称' - value={customModel} - onChange={(value) => { - setCustomModel(value.trim()); - }} - /> -
-
- 模型重定向: -
-