mirror of
https://github.com/songquanpeng/one-api.git
synced 2025-11-14 12:23:41 +08:00
feat: add new theme berry (#860)
* feat: add theme berry * docs: add development notes * fix: fix blank page * chore: update implementation * fix: fix package.json * chore: update ui copy --------- Co-authored-by: JustSong <songquanpeng@foxmail.com>
This commit is contained in:
275
web/berry/src/views/Token/component/EditModal.js
Normal file
275
web/berry/src/views/Token/component/EditModal.js
Normal file
@@ -0,0 +1,275 @@
|
||||
import PropTypes from "prop-types";
|
||||
import * as Yup from "yup";
|
||||
import { Formik } from "formik";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useState, useEffect } from "react";
|
||||
import dayjs from "dayjs";
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
Button,
|
||||
Divider,
|
||||
Alert,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
OutlinedInput,
|
||||
InputAdornment,
|
||||
Switch,
|
||||
FormHelperText,
|
||||
} from "@mui/material";
|
||||
|
||||
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
|
||||
import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
|
||||
import { DateTimePicker } from "@mui/x-date-pickers/DateTimePicker";
|
||||
import { renderQuotaWithPrompt, showSuccess, showError } from "utils/common";
|
||||
import { API } from "utils/api";
|
||||
require("dayjs/locale/zh-cn");
|
||||
|
||||
const validationSchema = Yup.object().shape({
|
||||
is_edit: Yup.boolean(),
|
||||
name: Yup.string().required("名称 不能为空"),
|
||||
remain_quota: Yup.number().min(0, "必须大于等于0"),
|
||||
expired_time: Yup.number(),
|
||||
unlimited_quota: Yup.boolean(),
|
||||
});
|
||||
|
||||
const originInputs = {
|
||||
is_edit: false,
|
||||
name: "",
|
||||
remain_quota: 0,
|
||||
expired_time: -1,
|
||||
unlimited_quota: false,
|
||||
};
|
||||
|
||||
const EditModal = ({ open, tokenId, onCancel, onOk }) => {
|
||||
const theme = useTheme();
|
||||
const [inputs, setInputs] = useState(originInputs);
|
||||
|
||||
const submit = async (values, { setErrors, setStatus, setSubmitting }) => {
|
||||
setSubmitting(true);
|
||||
|
||||
values.remain_quota = parseInt(values.remain_quota);
|
||||
let res;
|
||||
if (values.is_edit) {
|
||||
res = await API.put(`/api/token/`, { ...values, id: parseInt(tokenId) });
|
||||
} else {
|
||||
res = await API.post(`/api/token/`, values);
|
||||
}
|
||||
const { success, message } = res.data;
|
||||
if (success) {
|
||||
if (values.is_edit) {
|
||||
showSuccess("令牌更新成功!");
|
||||
} else {
|
||||
showSuccess("令牌创建成功,请在列表页面点击复制获取令牌!");
|
||||
}
|
||||
setSubmitting(false);
|
||||
setStatus({ success: true });
|
||||
onOk(true);
|
||||
} else {
|
||||
showError(message);
|
||||
setErrors({ submit: message });
|
||||
}
|
||||
};
|
||||
|
||||
const loadToken = async () => {
|
||||
let res = await API.get(`/api/token/${tokenId}`);
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
data.is_edit = true;
|
||||
setInputs(data);
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (tokenId) {
|
||||
loadToken().then();
|
||||
}
|
||||
}, [tokenId]);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={onCancel} fullWidth maxWidth={"md"}>
|
||||
<DialogTitle
|
||||
sx={{
|
||||
margin: "0px",
|
||||
fontWeight: 700,
|
||||
lineHeight: "1.55556",
|
||||
padding: "24px",
|
||||
fontSize: "1.125rem",
|
||||
}}
|
||||
>
|
||||
{tokenId ? "编辑Token" : "新建Token"}
|
||||
</DialogTitle>
|
||||
<Divider />
|
||||
<DialogContent>
|
||||
<Alert severity="info">
|
||||
注意,令牌的额度仅用于限制令牌本身的最大额度使用量,实际的使用受到账户的剩余额度限制。
|
||||
</Alert>
|
||||
<Formik
|
||||
initialValues={inputs}
|
||||
enableReinitialize
|
||||
validationSchema={validationSchema}
|
||||
onSubmit={submit}
|
||||
>
|
||||
{({
|
||||
errors,
|
||||
handleBlur,
|
||||
handleChange,
|
||||
handleSubmit,
|
||||
touched,
|
||||
values,
|
||||
setFieldError,
|
||||
setFieldValue,
|
||||
isSubmitting,
|
||||
}) => (
|
||||
<form noValidate onSubmit={handleSubmit}>
|
||||
<FormControl
|
||||
fullWidth
|
||||
error={Boolean(touched.name && errors.name)}
|
||||
sx={{ ...theme.typography.otherInput }}
|
||||
>
|
||||
<InputLabel htmlFor="channel-name-label">名称</InputLabel>
|
||||
<OutlinedInput
|
||||
id="channel-name-label"
|
||||
label="名称"
|
||||
type="text"
|
||||
value={values.name}
|
||||
name="name"
|
||||
onBlur={handleBlur}
|
||||
onChange={handleChange}
|
||||
inputProps={{ autoComplete: "name" }}
|
||||
aria-describedby="helper-text-channel-name-label"
|
||||
/>
|
||||
{touched.name && errors.name && (
|
||||
<FormHelperText error id="helper-tex-channel-name-label">
|
||||
{errors.name}
|
||||
</FormHelperText>
|
||||
)}
|
||||
</FormControl>
|
||||
{values.expired_time !== -1 && (
|
||||
<FormControl
|
||||
fullWidth
|
||||
error={Boolean(touched.expired_time && errors.expired_time)}
|
||||
sx={{ ...theme.typography.otherInput }}
|
||||
>
|
||||
<LocalizationProvider
|
||||
dateAdapter={AdapterDayjs}
|
||||
adapterLocale={"zh-cn"}
|
||||
>
|
||||
<DateTimePicker
|
||||
label="过期时间"
|
||||
ampm={false}
|
||||
value={dayjs.unix(values.expired_time)}
|
||||
onError={(newError) => {
|
||||
if (newError === null) {
|
||||
setFieldError("expired_time", null);
|
||||
} else {
|
||||
setFieldError("expired_time", "无效的日期");
|
||||
}
|
||||
}}
|
||||
onChange={(newValue) => {
|
||||
setFieldValue("expired_time", newValue.unix());
|
||||
}}
|
||||
slotProps={{
|
||||
actionBar: {
|
||||
actions: ["today", "accept"],
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</LocalizationProvider>
|
||||
{errors.expired_time && (
|
||||
<FormHelperText
|
||||
error
|
||||
id="helper-tex-channel-expired_time-label"
|
||||
>
|
||||
{errors.expired_time}
|
||||
</FormHelperText>
|
||||
)}
|
||||
</FormControl>
|
||||
)}
|
||||
<Switch
|
||||
checked={values.expired_time === -1}
|
||||
onClick={() => {
|
||||
if (values.expired_time === -1) {
|
||||
setFieldValue(
|
||||
"expired_time",
|
||||
Math.floor(Date.now() / 1000)
|
||||
);
|
||||
} else {
|
||||
setFieldValue("expired_time", -1);
|
||||
}
|
||||
}}
|
||||
/>{" "}
|
||||
永不过期
|
||||
<FormControl
|
||||
fullWidth
|
||||
error={Boolean(touched.remain_quota && errors.remain_quota)}
|
||||
sx={{ ...theme.typography.otherInput }}
|
||||
>
|
||||
<InputLabel htmlFor="channel-remain_quota-label">
|
||||
额度
|
||||
</InputLabel>
|
||||
<OutlinedInput
|
||||
id="channel-remain_quota-label"
|
||||
label="额度"
|
||||
type="number"
|
||||
value={values.remain_quota}
|
||||
name="remain_quota"
|
||||
endAdornment={
|
||||
<InputAdornment position="end">
|
||||
{renderQuotaWithPrompt(values.remain_quota)}
|
||||
</InputAdornment>
|
||||
}
|
||||
onBlur={handleBlur}
|
||||
onChange={handleChange}
|
||||
aria-describedby="helper-text-channel-remain_quota-label"
|
||||
disabled={values.unlimited_quota}
|
||||
/>
|
||||
|
||||
{touched.remain_quota && errors.remain_quota && (
|
||||
<FormHelperText
|
||||
error
|
||||
id="helper-tex-channel-remain_quota-label"
|
||||
>
|
||||
{errors.remain_quota}
|
||||
</FormHelperText>
|
||||
)}
|
||||
</FormControl>
|
||||
<Switch
|
||||
checked={values.unlimited_quota === true}
|
||||
onClick={() => {
|
||||
setFieldValue("unlimited_quota", !values.unlimited_quota);
|
||||
}}
|
||||
/>{" "}
|
||||
无限额度
|
||||
<DialogActions>
|
||||
<Button onClick={onCancel}>取消</Button>
|
||||
<Button
|
||||
disableElevation
|
||||
disabled={isSubmitting}
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
>
|
||||
提交
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</form>
|
||||
)}
|
||||
</Formik>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditModal;
|
||||
|
||||
EditModal.propTypes = {
|
||||
open: PropTypes.bool,
|
||||
tokenId: PropTypes.number,
|
||||
onCancel: PropTypes.func,
|
||||
onOk: PropTypes.func,
|
||||
};
|
||||
19
web/berry/src/views/Token/component/TableHead.js
Normal file
19
web/berry/src/views/Token/component/TableHead.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import { TableCell, TableHead, TableRow } from '@mui/material';
|
||||
|
||||
const TokenTableHead = () => {
|
||||
return (
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>名称</TableCell>
|
||||
<TableCell>状态</TableCell>
|
||||
<TableCell>已用额度</TableCell>
|
||||
<TableCell>剩余额度</TableCell>
|
||||
<TableCell>创建时间</TableCell>
|
||||
<TableCell>过期时间</TableCell>
|
||||
<TableCell>操作</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
);
|
||||
};
|
||||
|
||||
export default TokenTableHead;
|
||||
270
web/berry/src/views/Token/component/TableRow.js
Normal file
270
web/berry/src/views/Token/component/TableRow.js
Normal file
@@ -0,0 +1,270 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import {
|
||||
Popover,
|
||||
TableRow,
|
||||
MenuItem,
|
||||
TableCell,
|
||||
IconButton,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogContentText,
|
||||
DialogTitle,
|
||||
Button,
|
||||
Tooltip,
|
||||
Stack,
|
||||
ButtonGroup
|
||||
} from '@mui/material';
|
||||
|
||||
import TableSwitch from 'ui-component/Switch';
|
||||
import { renderQuota, showSuccess, timestamp2string } from 'utils/common';
|
||||
|
||||
import { IconDotsVertical, IconEdit, IconTrash, IconCaretDownFilled } from '@tabler/icons-react';
|
||||
|
||||
const COPY_OPTIONS = [
|
||||
{
|
||||
key: 'next',
|
||||
text: 'ChatGPT Next',
|
||||
url: 'https://chat.oneapi.pro/#/?settings={"key":"sk-{key}","url":"{serverAddress}"}',
|
||||
encode: false
|
||||
},
|
||||
{ key: 'ama', text: 'AMA 问天', url: 'ama://set-api-key?server={serverAddress}&key=sk-{key}', encode: true },
|
||||
{ key: 'opencat', text: 'OpenCat', url: 'opencat://team/join?domain={serverAddress}&token=sk-{key}', encode: true }
|
||||
];
|
||||
|
||||
function replacePlaceholders(text, key, serverAddress) {
|
||||
return text.replace('{key}', key).replace('{serverAddress}', serverAddress);
|
||||
}
|
||||
|
||||
function createMenu(menuItems) {
|
||||
return (
|
||||
<>
|
||||
{menuItems.map((menuItem, index) => (
|
||||
<MenuItem key={index} onClick={menuItem.onClick} sx={{ color: menuItem.color }}>
|
||||
{menuItem.icon}
|
||||
{menuItem.text}
|
||||
</MenuItem>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function TokensTableRow({ item, manageToken, handleOpenModal, setModalTokenId }) {
|
||||
const [open, setOpen] = useState(null);
|
||||
const [menuItems, setMenuItems] = useState(null);
|
||||
const [openDelete, setOpenDelete] = useState(false);
|
||||
const [statusSwitch, setStatusSwitch] = useState(item.status);
|
||||
const siteInfo = useSelector((state) => state.siteInfo);
|
||||
|
||||
const handleDeleteOpen = () => {
|
||||
handleCloseMenu();
|
||||
setOpenDelete(true);
|
||||
};
|
||||
|
||||
const handleDeleteClose = () => {
|
||||
setOpenDelete(false);
|
||||
};
|
||||
|
||||
const handleOpenMenu = (event, type) => {
|
||||
switch (type) {
|
||||
case 'copy':
|
||||
setMenuItems(copyItems);
|
||||
break;
|
||||
case 'link':
|
||||
setMenuItems(linkItems);
|
||||
break;
|
||||
default:
|
||||
setMenuItems(actionItems);
|
||||
}
|
||||
setOpen(event.currentTarget);
|
||||
};
|
||||
|
||||
const handleCloseMenu = () => {
|
||||
setOpen(null);
|
||||
};
|
||||
|
||||
const handleStatus = async () => {
|
||||
const switchVlue = statusSwitch === 1 ? 2 : 1;
|
||||
const { success } = await manageToken(item.id, 'status', switchVlue);
|
||||
if (success) {
|
||||
setStatusSwitch(switchVlue);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
handleCloseMenu();
|
||||
await manageToken(item.id, 'delete', '');
|
||||
};
|
||||
|
||||
const actionItems = createMenu([
|
||||
{
|
||||
text: '编辑',
|
||||
icon: <IconEdit style={{ marginRight: '16px' }} />,
|
||||
onClick: () => {
|
||||
handleCloseMenu();
|
||||
handleOpenModal();
|
||||
setModalTokenId(item.id);
|
||||
},
|
||||
color: undefined
|
||||
},
|
||||
{
|
||||
text: '删除',
|
||||
icon: <IconTrash style={{ marginRight: '16px' }} />,
|
||||
onClick: handleDeleteOpen,
|
||||
color: 'error.main'
|
||||
}
|
||||
]);
|
||||
|
||||
const handleCopy = (option, type) => {
|
||||
let serverAddress = '';
|
||||
if (siteInfo?.server_address) {
|
||||
serverAddress = siteInfo.server_address;
|
||||
} else {
|
||||
serverAddress = window.location.host;
|
||||
}
|
||||
|
||||
if (option.encode) {
|
||||
serverAddress = encodeURIComponent(serverAddress);
|
||||
}
|
||||
|
||||
let url = option.url;
|
||||
|
||||
if (option.key === 'next' && siteInfo?.chat_link) {
|
||||
url = siteInfo.chat_link + `/#/?settings={"key":"sk-{key}","url":"{serverAddress}"}`;
|
||||
}
|
||||
|
||||
const key = item.key;
|
||||
const text = replacePlaceholders(url, key, serverAddress);
|
||||
if (type === 'link') {
|
||||
window.open(text);
|
||||
} else {
|
||||
navigator.clipboard.writeText(text);
|
||||
showSuccess('已复制到剪贴板!');
|
||||
}
|
||||
handleCloseMenu();
|
||||
};
|
||||
|
||||
const copyItems = createMenu(
|
||||
COPY_OPTIONS.map((option) => ({
|
||||
text: option.text,
|
||||
icon: undefined,
|
||||
onClick: () => handleCopy(option, 'copy'),
|
||||
color: undefined
|
||||
}))
|
||||
);
|
||||
|
||||
const linkItems = createMenu(
|
||||
COPY_OPTIONS.map((option) => ({
|
||||
text: option.text,
|
||||
icon: undefined,
|
||||
onClick: () => handleCopy(option, 'link'),
|
||||
color: undefined
|
||||
}))
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<TableRow tabIndex={item.id}>
|
||||
<TableCell>{item.name}</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<Tooltip
|
||||
title={(() => {
|
||||
switch (statusSwitch) {
|
||||
case 1:
|
||||
return '已启用';
|
||||
case 2:
|
||||
return '已禁用';
|
||||
case 3:
|
||||
return '已过期';
|
||||
case 4:
|
||||
return '已耗尽';
|
||||
default:
|
||||
return '未知';
|
||||
}
|
||||
})()}
|
||||
placement="top"
|
||||
>
|
||||
<TableSwitch
|
||||
id={`switch-${item.id}`}
|
||||
checked={statusSwitch === 1}
|
||||
onChange={handleStatus}
|
||||
disabled={statusSwitch !== 1 && statusSwitch !== 2}
|
||||
/>
|
||||
</Tooltip>
|
||||
</TableCell>
|
||||
|
||||
<TableCell>{renderQuota(item.used_quota)}</TableCell>
|
||||
|
||||
<TableCell>{item.unlimited_quota ? '无限制' : renderQuota(item.remain_quota, 2)}</TableCell>
|
||||
|
||||
<TableCell>{timestamp2string(item.created_time)}</TableCell>
|
||||
|
||||
<TableCell>{item.expired_time === -1 ? '永不过期' : timestamp2string(item.expired_time)}</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<Stack direction="row" spacing={1}>
|
||||
<ButtonGroup size="small" aria-label="split button">
|
||||
<Button
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(`sk-${item.key}`);
|
||||
showSuccess('已复制到剪贴板!');
|
||||
}}
|
||||
>
|
||||
复制
|
||||
</Button>
|
||||
<Button size="small" onClick={(e) => handleOpenMenu(e, 'copy')}>
|
||||
<IconCaretDownFilled size={'16px'} />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup size="small" aria-label="split button">
|
||||
<Button color="primary">聊天</Button>
|
||||
<Button size="small" onClick={(e) => handleOpenMenu(e, 'link')}>
|
||||
<IconCaretDownFilled size={'16px'} />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<IconButton onClick={(e) => handleOpenMenu(e, 'action')} sx={{ color: 'rgb(99, 115, 129)' }}>
|
||||
<IconDotsVertical />
|
||||
</IconButton>
|
||||
</Stack>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<Popover
|
||||
open={!!open}
|
||||
anchorEl={open}
|
||||
onClose={handleCloseMenu}
|
||||
anchorOrigin={{ vertical: 'top', horizontal: 'left' }}
|
||||
transformOrigin={{ vertical: 'top', horizontal: 'right' }}
|
||||
PaperProps={{
|
||||
sx: { width: 140 }
|
||||
}}
|
||||
>
|
||||
{menuItems}
|
||||
</Popover>
|
||||
|
||||
<Dialog open={openDelete} onClose={handleDeleteClose}>
|
||||
<DialogTitle>删除Token</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>是否删除Token {item.name}?</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleDeleteClose}>关闭</Button>
|
||||
<Button onClick={handleDelete} sx={{ color: 'error.main' }} autoFocus>
|
||||
删除
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
TokensTableRow.propTypes = {
|
||||
item: PropTypes.object,
|
||||
manageToken: PropTypes.func,
|
||||
handleOpenModal: PropTypes.func,
|
||||
setModalTokenId: PropTypes.func
|
||||
};
|
||||
215
web/berry/src/views/Token/index.js
Normal file
215
web/berry/src/views/Token/index.js
Normal file
@@ -0,0 +1,215 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { showError, showSuccess } from 'utils/common';
|
||||
|
||||
import Table from '@mui/material/Table';
|
||||
import TableBody from '@mui/material/TableBody';
|
||||
import TableContainer from '@mui/material/TableContainer';
|
||||
import PerfectScrollbar from 'react-perfect-scrollbar';
|
||||
import TablePagination from '@mui/material/TablePagination';
|
||||
import LinearProgress from '@mui/material/LinearProgress';
|
||||
import Alert from '@mui/material/Alert';
|
||||
import ButtonGroup from '@mui/material/ButtonGroup';
|
||||
import Toolbar from '@mui/material/Toolbar';
|
||||
|
||||
import { Button, Card, Box, Stack, Container, Typography } from '@mui/material';
|
||||
import TokensTableRow from './component/TableRow';
|
||||
import TokenTableHead from './component/TableHead';
|
||||
import TableToolBar from 'ui-component/TableToolBar';
|
||||
import { API } from 'utils/api';
|
||||
import { ITEMS_PER_PAGE } from 'constants';
|
||||
import { IconRefresh, IconPlus } from '@tabler/icons-react';
|
||||
import EditeModal from './component/EditModal';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
export default function Token() {
|
||||
const [tokens, setTokens] = useState([]);
|
||||
const [activePage, setActivePage] = useState(0);
|
||||
const [searching, setSearching] = useState(false);
|
||||
const [searchKeyword, setSearchKeyword] = useState('');
|
||||
const [openModal, setOpenModal] = useState(false);
|
||||
const [editTokenId, setEditTokenId] = useState(0);
|
||||
const siteInfo = useSelector((state) => state.siteInfo);
|
||||
|
||||
const loadTokens = async (startIdx) => {
|
||||
setSearching(true);
|
||||
const res = await API.get(`/api/token/?p=${startIdx}`);
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
if (startIdx === 0) {
|
||||
setTokens(data);
|
||||
} else {
|
||||
let newTokens = [...tokens];
|
||||
newTokens.splice(startIdx * ITEMS_PER_PAGE, data.length, ...data);
|
||||
setTokens(newTokens);
|
||||
}
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
setSearching(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadTokens(0)
|
||||
.then()
|
||||
.catch((reason) => {
|
||||
showError(reason);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const onPaginationChange = (event, activePage) => {
|
||||
(async () => {
|
||||
if (activePage === Math.ceil(tokens.length / ITEMS_PER_PAGE)) {
|
||||
// In this case we have to load more data and then append them.
|
||||
await loadTokens(activePage);
|
||||
}
|
||||
setActivePage(activePage);
|
||||
})();
|
||||
};
|
||||
|
||||
const searchTokens = async (event) => {
|
||||
event.preventDefault();
|
||||
if (searchKeyword === '') {
|
||||
await loadTokens(0);
|
||||
setActivePage(0);
|
||||
return;
|
||||
}
|
||||
setSearching(true);
|
||||
const res = await API.get(`/api/token/search?keyword=${searchKeyword}`);
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
setTokens(data);
|
||||
setActivePage(0);
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
setSearching(false);
|
||||
};
|
||||
|
||||
const handleSearchKeyword = (event) => {
|
||||
setSearchKeyword(event.target.value);
|
||||
};
|
||||
|
||||
const manageToken = async (id, action, value) => {
|
||||
const url = '/api/token/';
|
||||
let data = { id };
|
||||
let res;
|
||||
switch (action) {
|
||||
case 'delete':
|
||||
res = await API.delete(url + id);
|
||||
break;
|
||||
case 'status':
|
||||
res = await API.put(url + `?status_only=true`, {
|
||||
...data,
|
||||
status: value
|
||||
});
|
||||
break;
|
||||
}
|
||||
const { success, message } = res.data;
|
||||
if (success) {
|
||||
showSuccess('操作成功完成!');
|
||||
if (action === 'delete') {
|
||||
await handleRefresh();
|
||||
}
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
|
||||
return res.data;
|
||||
};
|
||||
|
||||
// 处理刷新
|
||||
const handleRefresh = async () => {
|
||||
await loadTokens(activePage);
|
||||
};
|
||||
|
||||
const handleOpenModal = (tokenId) => {
|
||||
setEditTokenId(tokenId);
|
||||
setOpenModal(true);
|
||||
};
|
||||
|
||||
const handleCloseModal = () => {
|
||||
setOpenModal(false);
|
||||
setEditTokenId(0);
|
||||
};
|
||||
|
||||
const handleOkModal = (status) => {
|
||||
if (status === true) {
|
||||
handleCloseModal();
|
||||
handleRefresh();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack direction="row" alignItems="center" justifyContent="space-between" mb={5}>
|
||||
<Typography variant="h4">令牌</Typography>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
handleOpenModal(0);
|
||||
}}
|
||||
startIcon={<IconPlus />}
|
||||
>
|
||||
新建令牌
|
||||
</Button>
|
||||
</Stack>
|
||||
<Stack mb={5}>
|
||||
<Alert severity="info">
|
||||
将 OpenAI API 基础地址 https://api.openai.com 替换为 <b>{siteInfo.server_address}</b>,复制下面的密钥即可使用
|
||||
</Alert>
|
||||
</Stack>
|
||||
<Card>
|
||||
<Box component="form" onSubmit={searchTokens} noValidate>
|
||||
<TableToolBar filterName={searchKeyword} handleFilterName={handleSearchKeyword} placeholder={'搜索令牌的名称...'} />
|
||||
</Box>
|
||||
<Toolbar
|
||||
sx={{
|
||||
textAlign: 'right',
|
||||
height: 50,
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
p: (theme) => theme.spacing(0, 1, 0, 3)
|
||||
}}
|
||||
>
|
||||
<Container>
|
||||
<ButtonGroup variant="outlined" aria-label="outlined small primary button group">
|
||||
<Button onClick={handleRefresh} startIcon={<IconRefresh width={'18px'} />}>
|
||||
刷新
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Container>
|
||||
</Toolbar>
|
||||
{searching && <LinearProgress />}
|
||||
<PerfectScrollbar component="div">
|
||||
<TableContainer sx={{ overflow: 'unset' }}>
|
||||
<Table sx={{ minWidth: 800 }}>
|
||||
<TokenTableHead />
|
||||
<TableBody>
|
||||
{tokens.slice(activePage * ITEMS_PER_PAGE, (activePage + 1) * ITEMS_PER_PAGE).map((row) => (
|
||||
<TokensTableRow
|
||||
item={row}
|
||||
manageToken={manageToken}
|
||||
key={row.id}
|
||||
handleOpenModal={handleOpenModal}
|
||||
setModalTokenId={setEditTokenId}
|
||||
/>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</PerfectScrollbar>
|
||||
<TablePagination
|
||||
page={activePage}
|
||||
component="div"
|
||||
count={tokens.length + (tokens.length % ITEMS_PER_PAGE === 0 ? 1 : 0)}
|
||||
rowsPerPage={ITEMS_PER_PAGE}
|
||||
onPageChange={onPaginationChange}
|
||||
rowsPerPageOptions={[ITEMS_PER_PAGE]}
|
||||
/>
|
||||
</Card>
|
||||
<EditeModal open={openModal} onCancel={handleCloseModal} onOk={handleOkModal} tokenId={editTokenId} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user