Merge commit 'af543ab8ecb6827cbbc151c2cff181cdc3286274'

This commit is contained in:
Laisky.Cai
2024-04-08 01:13:00 +00:00
49 changed files with 1024 additions and 335 deletions

View File

@@ -0,0 +1,94 @@
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
import React, { useEffect, useState } from 'react';
import { showError } from 'utils/common';
import useLogin from 'hooks/useLogin';
// material-ui
import { useTheme } from '@mui/material/styles';
import { Grid, Stack, Typography, useMediaQuery, CircularProgress } from '@mui/material';
// project imports
import AuthWrapper from '../AuthWrapper';
import AuthCardWrapper from '../AuthCardWrapper';
import Logo from 'ui-component/Logo';
// assets
// ================================|| AUTH3 - LOGIN ||================================ //
const LarkOAuth = () => {
const theme = useTheme();
const matchDownSM = useMediaQuery(theme.breakpoints.down('md'));
const [searchParams] = useSearchParams();
const [prompt, setPrompt] = useState('处理中...');
const { larkLogin } = useLogin();
let navigate = useNavigate();
const sendCode = async (code, state, count) => {
const { success, message } = await larkLogin(code, state);
if (!success) {
if (message) {
showError(message);
}
if (count === 0) {
setPrompt(`操作失败,重定向至登录界面中...`);
await new Promise((resolve) => setTimeout(resolve, 2000));
navigate('/login');
return;
}
count++;
setPrompt(`出现错误,第 ${count} 次重试中...`);
await new Promise((resolve) => setTimeout(resolve, 2000));
await sendCode(code, state, count);
}
};
useEffect(() => {
let code = searchParams.get('code');
let state = searchParams.get('state');
sendCode(code, state, 0).then();
}, []);
return (
<AuthWrapper>
<Grid container direction="column" justifyContent="flex-end">
<Grid item xs={12}>
<Grid container justifyContent="center" alignItems="center" sx={{ minHeight: 'calc(100vh - 136px)' }}>
<Grid item sx={{ m: { xs: 1, sm: 3 }, mb: 0 }}>
<AuthCardWrapper>
<Grid container spacing={2} alignItems="center" justifyContent="center">
<Grid item sx={{ mb: 3 }}>
<Link to="#">
<Logo />
</Link>
</Grid>
<Grid item xs={12}>
<Grid container direction={matchDownSM ? 'column-reverse' : 'row'} alignItems="center" justifyContent="center">
<Grid item>
<Stack alignItems="center" justifyContent="center" spacing={1}>
<Typography color={theme.palette.primary.main} gutterBottom variant={matchDownSM ? 'h3' : 'h2'}>
飞书 登录
</Typography>
</Stack>
</Grid>
</Grid>
</Grid>
<Grid item xs={12} container direction="column" justifyContent="center" alignItems="center" style={{ height: '200px' }}>
<CircularProgress />
<Typography variant="h3" paddingTop={'20px'}>
{prompt}
</Typography>
</Grid>
</Grid>
</AuthCardWrapper>
</Grid>
</Grid>
</Grid>
</Grid>
</AuthWrapper>
);
};
export default LarkOAuth;

View File

@@ -35,7 +35,8 @@ import VisibilityOff from '@mui/icons-material/VisibilityOff';
import Github from 'assets/images/icons/github.svg';
import Wechat from 'assets/images/icons/wechat.svg';
import { onGitHubOAuthClicked } from 'utils/common';
import Lark from 'assets/images/icons/lark.svg';
import { onGitHubOAuthClicked, onLarkOAuthClicked } from 'utils/common';
// ============================|| FIREBASE - LOGIN ||============================ //
@@ -49,7 +50,7 @@ const LoginForm = ({ ...others }) => {
// const [checked, setChecked] = useState(true);
let tripartiteLogin = false;
if (siteInfo.github_oauth || siteInfo.wechat_login) {
if (siteInfo.github_oauth || siteInfo.wechat_login || siteInfo.lark_client_id) {
tripartiteLogin = true;
}
@@ -121,6 +122,29 @@ const LoginForm = ({ ...others }) => {
<WechatModal open={openWechat} handleClose={handleWechatClose} wechatLogin={wechatLogin} qrCode={siteInfo.wechat_qrcode} />
</Grid>
)}
{siteInfo.lark_client_id && (
<Grid item xs={12}>
<AnimateButton>
<Button
disableElevation
fullWidth
onClick={() => onLarkOAuthClicked(siteInfo.lark_client_id)}
size="large"
variant="outlined"
sx={{
color: 'grey.700',
backgroundColor: theme.palette.grey[50],
borderColor: theme.palette.grey[100]
}}
>
<Box sx={{ mr: { xs: 1, sm: 2, width: 20 }, display: 'flex', alignItems: 'center' }}>
<img src={Lark} alt="Lark" width={25} height={25} style={{ marginRight: matchDownSM ? 8 : 16 }} />
</Box>
使用飞书登录
</Button>
</AnimateButton>
</Grid>
)}
<Grid item xs={12}>
<Box
sx={{

View File

@@ -8,7 +8,7 @@ import { UserContext } from 'contexts/UserContext';
// ==============================|| AUTHENTICATION 1 WRAPPER ||============================== //
const AuthStyle = styled('div')(({ theme }) => ({
backgroundColor: theme.palette.primary.light
backgroundColor: theme.palette.background.default
}));
// eslint-disable-next-line

View File

@@ -21,15 +21,16 @@ import {
Container,
Autocomplete,
FormHelperText,
Checkbox
Switch,
Checkbox,
} from "@mui/material";
import { Formik } from "formik";
import * as Yup from "yup";
import { defaultConfig, typeConfig } from "../type/Config"; //typeConfig
import { createFilterOptions } from "@mui/material/Autocomplete";
import CheckBoxOutlineBlankIcon from '@mui/icons-material/CheckBoxOutlineBlank';
import CheckBoxIcon from '@mui/icons-material/CheckBox';
import CheckBoxOutlineBlankIcon from "@mui/icons-material/CheckBoxOutlineBlank";
import CheckBoxIcon from "@mui/icons-material/CheckBox";
const icon = <CheckBoxOutlineBlankIcon fontSize="small" />;
const checkedIcon = <CheckBoxIcon fontSize="small" />;
@@ -79,6 +80,7 @@ const EditModal = ({ open, channelId, onCancel, onOk }) => {
const [inputPrompt, setInputPrompt] = useState(defaultConfig.prompt);
const [groupOptions, setGroupOptions] = useState([]);
const [modelOptions, setModelOptions] = useState([]);
const [batchAdd, setBatchAdd] = useState(false);
const initChannel = (typeValue) => {
if (typeConfig[typeValue]?.inputLabel) {
@@ -151,7 +153,7 @@ const EditModal = ({ open, channelId, onCancel, onOk }) => {
try {
let res = await API.get(`/api/channel/models`);
const { data } = res.data;
data.forEach(item => {
data.forEach((item) => {
if (!item.owned_by) {
item.owned_by = "未知";
}
@@ -166,7 +168,7 @@ const EditModal = ({ open, channelId, onCancel, onOk }) => {
});
setModelOptions(
data.map((model) => {
data.map((model) => {
return {
id: model.id,
group: model.owned_by,
@@ -258,7 +260,7 @@ const EditModal = ({ open, channelId, onCancel, onOk }) => {
2
);
}
data.base_url = data.base_url ?? '';
data.base_url = data.base_url ?? "";
data.is_edit = true;
initChannel(data.type);
setInitialInput(data);
@@ -273,6 +275,7 @@ const EditModal = ({ open, channelId, onCancel, onOk }) => {
}, []);
useEffect(() => {
setBatchAdd(false);
if (channelId) {
loadChannel().then();
} else {
@@ -340,15 +343,17 @@ const EditModal = ({ open, channelId, onCancel, onOk }) => {
},
}}
>
{Object.values(CHANNEL_OPTIONS).sort((a, b) => {
return a.text.localeCompare(b.text)
}).map((option) => {
return (
<MenuItem key={option.value} value={option.value}>
{option.text}
</MenuItem>
);
})}
{Object.values(CHANNEL_OPTIONS)
.sort((a, b) => {
return a.text.localeCompare(b.text);
})
.map((option) => {
return (
<MenuItem key={option.value} value={option.value}>
{option.text}
</MenuItem>
);
})}
</Select>
{touched.type && errors.type ? (
<FormHelperText error id="helper-tex-channel-type-label">
@@ -553,7 +558,12 @@ const EditModal = ({ open, channelId, onCancel, onOk }) => {
}}
renderOption={(props, option, { selected }) => (
<li {...props}>
<Checkbox icon={icon} checkedIcon={checkedIcon} style={{ marginRight: 8 }} checked={selected} />
<Checkbox
icon={icon}
checkedIcon={checkedIcon}
style={{ marginRight: 8 }}
checked={selected}
/>
{option.id}
</li>
)}
@@ -599,20 +609,38 @@ const EditModal = ({ open, channelId, onCancel, onOk }) => {
error={Boolean(touched.key && errors.key)}
sx={{ ...theme.typography.otherInput }}
>
<InputLabel htmlFor="channel-key-label">
{inputLabel.key}
</InputLabel>
<OutlinedInput
id="channel-key-label"
label={inputLabel.key}
type="text"
value={values.key}
name="key"
onBlur={handleBlur}
onChange={handleChange}
inputProps={{}}
aria-describedby="helper-text-channel-key-label"
/>
{!batchAdd ? (
<>
<InputLabel htmlFor="channel-key-label">
{inputLabel.key}
</InputLabel>
<OutlinedInput
id="channel-key-label"
label={inputLabel.key}
type="text"
value={values.key}
name="key"
onBlur={handleBlur}
onChange={handleChange}
inputProps={{}}
aria-describedby="helper-text-channel-key-label"
/>
</>
) : (
<TextField
multiline
id="channel-key-label"
label={inputLabel.key}
value={values.key}
name="key"
onBlur={handleBlur}
onChange={handleChange}
aria-describedby="helper-text-channel-key-label"
minRows={5}
placeholder={inputPrompt.key + ",一行一个密钥"}
/>
)}
{touched.key && errors.key ? (
<FormHelperText error id="helper-tex-channel-key-label">
{errors.key}
@@ -624,6 +652,19 @@ const EditModal = ({ open, channelId, onCancel, onOk }) => {
</FormHelperText>
)}
</FormControl>
{channelId === 0 && (
<Container
sx={{
textAlign: "right",
}}
>
<Switch
checked={batchAdd}
onChange={(e) => setBatchAdd(e.target.checked)}
/>
批量添加
</Container>
)}
<FormControl
fullWidth
error={Boolean(touched.model_mapping && errors.model_mapping)}

View File

@@ -11,10 +11,7 @@ import {
MenuItem,
TableCell,
IconButton,
FormControl,
InputLabel,
InputAdornment,
Input,
TextField,
Dialog,
DialogActions,
DialogContent,
@@ -31,12 +28,7 @@ import ResponseTimeLabel from "./ResponseTimeLabel";
import GroupLabel from "./GroupLabel";
import NameLabel from "./NameLabel";
import {
IconDotsVertical,
IconEdit,
IconTrash,
IconPencil,
} from "@tabler/icons-react";
import { IconDotsVertical, IconEdit, IconTrash } from "@tabler/icons-react";
export default function ChannelTableRow({
item,
@@ -79,11 +71,19 @@ export default function ChannelTableRow({
}
};
const handlePriority = async () => {
if (priorityValve === "" || priorityValve === item.priority) {
const handlePriority = async (event) => {
const currentValue = parseInt(event.target.value);
if (isNaN(currentValue) || currentValue === priorityValve) {
return;
}
await manageChannel(item.id, "priority", priorityValve);
if (currentValue < 0) {
showError("优先级不能小于 0");
return;
}
await manageChannel(item.id, "priority", currentValue);
setPriority(currentValue);
};
const handleResponseTime = async () => {
@@ -170,9 +170,7 @@ export default function ChannelTableRow({
handle_action={handleResponseTime}
/>
</TableCell>
<TableCell>
{renderNumber(item.used_quota)}
</TableCell>
<TableCell>{renderNumber(item.used_quota)}</TableCell>
<TableCell>
<Tooltip
title={"点击更新余额"}
@@ -183,27 +181,16 @@ export default function ChannelTableRow({
</Tooltip>
</TableCell>
<TableCell>
<FormControl sx={{ m: 1, width: "70px" }} variant="standard">
<InputLabel htmlFor={`priority-${item.id}`}>优先级</InputLabel>
<Input
id={`priority-${item.id}`}
type="text"
value={priorityValve}
onChange={(e) => setPriority(e.target.value)}
sx={{ textAlign: "center" }}
endAdornment={
<InputAdornment position="end">
<IconButton
onClick={handlePriority}
sx={{ color: "rgb(99, 115, 129)" }}
size="small"
>
<IconPencil />
</IconButton>
</InputAdornment>
}
/>
</FormControl>
<TextField
id={`priority-${item.id}`}
onBlur={handlePriority}
type="number"
label="优先级"
variant="standard"
defaultValue={item.priority}
inputProps={{ min: "0" }}
sx={{ width: 80 }}
/>
</TableCell>
<TableCell>

View File

@@ -12,7 +12,7 @@ import MainCard from 'ui-component/cards/MainCard';
import SkeletonTotalOrderCard from 'ui-component/cards/Skeleton/EarningCard';
const CardWrapper = styled(MainCard)(({ theme }) => ({
backgroundColor: theme.palette.primary.dark,
...theme.typography.CardWrapper,
color: '#fff',
overflow: 'hidden',
position: 'relative',

View File

@@ -12,7 +12,8 @@ import {
DialogTitle,
DialogContent,
DialogActions,
Divider
Divider,
SvgIcon
} from '@mui/material';
import Grid from '@mui/material/Unstable_Grid2';
import SubCard from 'ui-component/cards/SubCard';
@@ -20,12 +21,13 @@ import { IconBrandWechat, IconBrandGithub, IconMail } from '@tabler/icons-react'
import Label from 'ui-component/Label';
import { API } from 'utils/api';
import { showError, showSuccess } from 'utils/common';
import { onGitHubOAuthClicked } from 'utils/common';
import { onGitHubOAuthClicked, onLarkOAuthClicked } from 'utils/common';
import * as Yup from 'yup';
import WechatModal from 'views/Authentication/AuthForms/WechatModal';
import { useSelector } from 'react-redux';
import EmailModal from './component/EmailModal';
import Turnstile from 'react-turnstile';
import { ReactComponent as Lark } from 'assets/images/icons/lark.svg';
const validationSchema = Yup.object().shape({
username: Yup.string().required('用户名 不能为空').min(3, '用户名 不能小于 3 个字符'),
@@ -137,6 +139,9 @@ export default function Profile() {
<Label variant="ghost" color={inputs.email ? 'primary' : 'default'}>
<IconMail /> {inputs.email || '未绑定'}
</Label>
<Label variant="ghost" color={inputs.lark_id ? 'primary' : 'default'}>
<SvgIcon component={Lark} inheritViewBox="0 0 24 24" /> {inputs.lark_id || '未绑定'}
</Label>
</Stack>
<SubCard title="个人信息">
<Grid container spacing={2}>
@@ -205,6 +210,13 @@ export default function Profile() {
</Button>
</Grid>
)}
{status.lark_client_id && !inputs.lark_id && (
<Grid xs={12} md={4}>
<Button variant="contained" onClick={() => onLarkOAuthClicked(status.lark_client_id)}>
绑定 飞书 账号
</Button>
</Grid>
)}
<Grid xs={12} md={4}>
<Button
variant="contained"

View File

@@ -31,6 +31,8 @@ const SystemSetting = () => {
GitHubOAuthEnabled: '',
GitHubClientId: '',
GitHubClientSecret: '',
LarkClientId: '',
LarkClientSecret: '',
Notice: '',
SMTPServer: '',
SMTPPort: '',
@@ -48,7 +50,9 @@ const SystemSetting = () => {
TurnstileSecretKey: '',
RegisterEnabled: '',
EmailDomainRestrictionEnabled: '',
EmailDomainWhitelist: []
EmailDomainWhitelist: [],
MessagePusherAddress: '',
MessagePusherToken: ''
});
const [originInputs, setOriginInputs] = useState({});
let [loading, setLoading] = useState(false);
@@ -134,7 +138,11 @@ const SystemSetting = () => {
name === 'WeChatAccountQRCodeImageURL' ||
name === 'TurnstileSiteKey' ||
name === 'TurnstileSecretKey' ||
name === 'EmailDomainWhitelist'
name === 'EmailDomainWhitelist' ||
name === 'MessagePusherAddress' ||
name === 'MessagePusherToken' ||
name === 'LarkClientId' ||
name === 'LarkClientSecret'
) {
setInputs((inputs) => ({ ...inputs, [name]: value }));
} else {
@@ -199,6 +207,24 @@ const SystemSetting = () => {
}
};
const submitMessagePusher = async () => {
if (originInputs['MessagePusherAddress'] !== inputs.MessagePusherAddress) {
await updateOption('MessagePusherAddress', removeTrailingSlash(inputs.MessagePusherAddress));
}
if (originInputs['MessagePusherToken'] !== inputs.MessagePusherToken && inputs.MessagePusherToken !== '') {
await updateOption('MessagePusherToken', inputs.MessagePusherToken);
}
};
const submitLarkOAuth = async () => {
if (originInputs['LarkClientId'] !== inputs.LarkClientId) {
await updateOption('LarkClientId', inputs.LarkClientId);
}
if (originInputs['LarkClientSecret'] !== inputs.LarkClientSecret && inputs.LarkClientSecret !== '') {
await updateOption('LarkClientSecret', inputs.LarkClientSecret);
}
};
return (
<>
<Stack spacing={2}>
@@ -473,6 +499,61 @@ const SystemSetting = () => {
</Grid>
</Grid>
</SubCard>
<SubCard
title="配置飞书授权登录"
subTitle={
<span>
{' '}
用以支持通过飞书进行登录注册
<a href="https://open.feishu.cn/app" target="_blank" rel="noreferrer">
点击此处
</a>
管理你的飞书应用
</span>
}
>
<Grid container spacing={{ xs: 3, sm: 2, md: 4 }}>
<Grid xs={12}>
<Alert severity="info" sx={{ wordWrap: 'break-word' }}>
主页链接填 <code>{inputs.ServerAddress}</code>
重定向 URL <code>{`${inputs.ServerAddress}/oauth/lark`}</code>
</Alert>
</Grid>
<Grid xs={12} md={6}>
<FormControl fullWidth>
<InputLabel htmlFor="LarkClientId">App ID</InputLabel>
<OutlinedInput
id="LarkClientId"
name="LarkClientId"
value={inputs.LarkClientId || ''}
onChange={handleInputChange}
label="App ID"
placeholder="输入 App ID"
disabled={loading}
/>
</FormControl>
</Grid>
<Grid xs={12} md={6}>
<FormControl fullWidth>
<InputLabel htmlFor="LarkClientSecret">App Secret</InputLabel>
<OutlinedInput
id="LarkClientSecret"
name="LarkClientSecret"
value={inputs.LarkClientSecret || ''}
onChange={handleInputChange}
label="App Secret"
placeholder="敏感信息不会发送到前端显示"
disabled={loading}
/>
</FormControl>
</Grid>
<Grid xs={12}>
<Button variant="contained" onClick={submitLarkOAuth}>
保存飞书 OAuth 设置
</Button>
</Grid>
</Grid>
</SubCard>
<SubCard
title="配置 WeChat Server"
subTitle={
@@ -535,6 +616,55 @@ const SystemSetting = () => {
</Grid>
</Grid>
</SubCard>
<SubCard
title="配置 Message Pusher"
subTitle={
<span>
用以推送报警信息
<a href="https://github.com/songquanpeng/message-pusher" target="_blank" rel="noreferrer">
点击此处
</a>
了解 Message Pusher
</span>
}
>
<Grid container spacing={{ xs: 3, sm: 2, md: 4 }}>
<Grid xs={12} md={6}>
<FormControl fullWidth>
<InputLabel htmlFor="MessagePusherAddress">Message Pusher 推送地址</InputLabel>
<OutlinedInput
id="MessagePusherAddress"
name="MessagePusherAddress"
value={inputs.MessagePusherAddress || ''}
onChange={handleInputChange}
label="Message Pusher 推送地址"
placeholder="例如https://msgpusher.com/push/your_username"
disabled={loading}
/>
</FormControl>
</Grid>
<Grid xs={12} md={6}>
<FormControl fullWidth>
<InputLabel htmlFor="MessagePusherToken">Message Pusher 访问凭证</InputLabel>
<OutlinedInput
id="MessagePusherToken"
name="MessagePusherToken"
type="password"
value={inputs.MessagePusherToken || ''}
onChange={handleInputChange}
label="Message Pusher 访问凭证"
placeholder="敏感信息不会发送到前端显示"
disabled={loading}
/>
</FormControl>
</Grid>
<Grid xs={12}>
<Button variant="contained" onClick={submitMessagePusher}>
保存 Message Pusher 设置
</Button>
</Grid>
</Grid>
</SubCard>
<SubCard
title="配置 Turnstile"
subTitle={

View File

@@ -1,9 +1,9 @@
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 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,
@@ -16,53 +16,66 @@ import {
InputLabel,
OutlinedInput,
InputAdornment,
Autocomplete,
Checkbox,
TextField,
Switch,
FormHelperText,
} from "@mui/material";
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");
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';
import CheckBoxOutlineBlankIcon from '@mui/icons-material/CheckBoxOutlineBlank';
import CheckBoxIcon from '@mui/icons-material/CheckBox';
import { createFilterOptions } from '@mui/material/Autocomplete';
require('dayjs/locale/zh-cn');
const icon = <CheckBoxOutlineBlankIcon fontSize="small" />;
const checkedIcon = <CheckBoxIcon fontSize="small" />;
const filter = createFilterOptions();
const validationSchema = Yup.object().shape({
is_edit: Yup.boolean(),
name: Yup.string().required("名称 不能为空"),
remain_quota: Yup.number().min(0, "必须大于等于0"),
name: Yup.string().required('名称 不能为空'),
remain_quota: Yup.number().min(0, '必须大于等于0'),
expired_time: Yup.number(),
unlimited_quota: Yup.boolean(),
unlimited_quota: Yup.boolean()
});
const originInputs = {
is_edit: false,
name: "",
name: '',
remain_quota: 0,
expired_time: -1,
unlimited_quota: false,
subnet: '',
models: []
};
const EditModal = ({ open, tokenId, onCancel, onOk }) => {
const theme = useTheme();
const [inputs, setInputs] = useState(originInputs);
const [modelOptions, setModelOptions] = useState([]);
const submit = async (values, { setErrors, setStatus, setSubmitting }) => {
setSubmitting(true);
values.remain_quota = parseInt(values.remain_quota);
let res;
let models = values.models.join(',');
if (values.is_edit) {
res = await API.put(`/api/token/`, { ...values, id: parseInt(tokenId) });
res = await API.put(`/api/token/`, { ...values, id: parseInt(tokenId), models: models });
} else {
res = await API.post(`/api/token/`, values);
res = await API.post(`/api/token/`, { ...values, models: models });
}
const { success, message } = res.data;
if (success) {
if (values.is_edit) {
showSuccess("令牌更新成功!");
showSuccess('令牌更新成功!');
} else {
showSuccess("令牌创建成功,请在列表页面点击复制获取令牌!");
showSuccess('令牌创建成功,请在列表页面点击复制获取令牌!');
}
setSubmitting(false);
setStatus({ success: true });
@@ -78,61 +91,55 @@ const EditModal = ({ open, tokenId, onCancel, onOk }) => {
const { success, message, data } = res.data;
if (success) {
data.is_edit = true;
if (data.models === '') {
data.models = [];
} else {
data.models = data.models.split(',');
}
setInputs(data);
} else {
showError(message);
}
};
const loadAvailableModels = async () => {
let res = await API.get(`/api/user/available_models`);
const { success, message, data } = res.data;
if (success) {
setModelOptions(data);
} else {
showError(message);
}
};
useEffect(() => {
if (tokenId) {
loadToken().then();
} else {
setInputs({...originInputs});
setInputs({ ...originInputs });
}
loadAvailableModels().then();
}, [tokenId]);
return (
<Dialog open={open} onClose={onCancel} fullWidth maxWidth={"md"}>
<Dialog open={open} onClose={onCancel} fullWidth maxWidth={'md'}>
<DialogTitle
sx={{
margin: "0px",
margin: '0px',
fontWeight: 700,
lineHeight: "1.55556",
padding: "24px",
fontSize: "1.125rem",
lineHeight: '1.55556',
padding: '24px',
fontSize: '1.125rem'
}}
>
{tokenId ? "编辑令牌" : "新建令牌"}
{tokenId ? '编辑令牌' : '新建令牌'}
</DialogTitle>
<Divider />
<DialogContent>
<Alert severity="info">
注意令牌的额度仅用于限制令牌本身的最大额度使用量实际的使用受到账户的剩余额度限制
</Alert>
<Formik
initialValues={inputs}
enableReinitialize
validationSchema={validationSchema}
onSubmit={submit}
>
{({
errors,
handleBlur,
handleChange,
handleSubmit,
touched,
values,
setFieldError,
setFieldValue,
isSubmitting,
}) => (
<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 }}
>
<FormControl fullWidth error={Boolean(touched.name && errors.name)} sx={{ ...theme.typography.otherInput }}>
<InputLabel htmlFor="channel-name-label">名称</InputLabel>
<OutlinedInput
id="channel-name-label"
@@ -142,7 +149,7 @@ const EditModal = ({ open, tokenId, onCancel, onOk }) => {
name="name"
onBlur={handleBlur}
onChange={handleChange}
inputProps={{ autoComplete: "name" }}
inputProps={{ autoComplete: 'name' }}
aria-describedby="helper-text-channel-name-label"
/>
{touched.name && errors.name && (
@@ -151,42 +158,99 @@ const EditModal = ({ open, tokenId, onCancel, onOk }) => {
</FormHelperText>
)}
</FormControl>
<FormControl fullWidth sx={{ ...theme.typography.otherInput }}>
<Autocomplete
multiple
freeSolo
id="channel-models-label"
options={modelOptions}
value={values.models}
onChange={(e, value) => {
const event = {
target: {
name: 'models',
value: value
}
};
handleChange(event);
}}
onBlur={handleBlur}
// filterSelectedOptions
disableCloseOnSelect
renderInput={(params) => <TextField {...params} name="models" error={Boolean(errors.models)} label="模型范围" />}
filterOptions={(options, params) => {
const filtered = filter(options, params);
const { inputValue } = params;
const isExisting = options.some((option) => inputValue === option);
if (inputValue !== '' && !isExisting) {
filtered.push(inputValue);
}
return filtered;
}}
renderOption={(props, option, { selected }) => (
<li {...props}>
<Checkbox icon={icon} checkedIcon={checkedIcon} style={{ marginRight: 8 }} checked={selected} />
{option}
</li>
)}
/>
{errors.models ? (
<FormHelperText error id="helper-tex-channel-models-label">
{errors.models}
</FormHelperText>
) : (
<FormHelperText id="helper-tex-channel-models-label">请选择允许使用的模型留空则不进行限制</FormHelperText>
)}
</FormControl>
<FormControl fullWidth error={Boolean(touched.subnet && errors.subnet)} sx={{ ...theme.typography.otherInput }}>
<InputLabel htmlFor="channel-subnet-label">IP 限制</InputLabel>
<OutlinedInput
id="channel-subnet-label"
label="IP 限制"
type="text"
value={values.subnet}
name="subnet"
onBlur={handleBlur}
onChange={handleChange}
inputProps={{ autoComplete: 'subnet' }}
aria-describedby="helper-text-channel-subnet-label"
/>
{touched.subnet && errors.subnet ? (
<FormHelperText error id="helper-tex-channel-subnet-label">
{errors.subnet}
</FormHelperText>
) : (
<FormHelperText id="helper-tex-channel-subnet-label">
请输入允许访问的网段例如192.168.0.0/24请使用英文逗号分隔多个网段
</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"}
>
<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);
setFieldError('expired_time', null);
} else {
setFieldError("expired_time", "无效的日期");
setFieldError('expired_time', '无效的日期');
}
}}
onChange={(newValue) => {
setFieldValue("expired_time", newValue.unix());
setFieldValue('expired_time', newValue.unix());
}}
slotProps={{
actionBar: {
actions: ["today", "accept"],
},
actions: ['today', 'accept']
}
}}
/>
</LocalizationProvider>
{errors.expired_time && (
<FormHelperText
error
id="helper-tex-channel-expired_time-label"
>
<FormHelperText error id="helper-tex-channel-expired_time-label">
{errors.expired_time}
</FormHelperText>
)}
@@ -196,35 +260,22 @@ const EditModal = ({ open, tokenId, onCancel, onOk }) => {
checked={values.expired_time === -1}
onClick={() => {
if (values.expired_time === -1) {
setFieldValue(
"expired_time",
Math.floor(Date.now() / 1000)
);
setFieldValue('expired_time', Math.floor(Date.now() / 1000));
} else {
setFieldValue("expired_time", -1);
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>
<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>
}
endAdornment={<InputAdornment position="end">{renderQuotaWithPrompt(values.remain_quota)}</InputAdornment>}
onBlur={handleBlur}
onChange={handleChange}
aria-describedby="helper-text-channel-remain_quota-label"
@@ -232,10 +283,7 @@ const EditModal = ({ open, tokenId, onCancel, onOk }) => {
/>
{touched.remain_quota && errors.remain_quota && (
<FormHelperText
error
id="helper-tex-channel-remain_quota-label"
>
<FormHelperText error id="helper-tex-channel-remain_quota-label">
{errors.remain_quota}
</FormHelperText>
)}
@@ -243,19 +291,13 @@ const EditModal = ({ open, tokenId, onCancel, onOk }) => {
<Switch
checked={values.unlimited_quota === true}
onClick={() => {
setFieldValue("unlimited_quota", !values.unlimited_quota);
setFieldValue('unlimited_quota', !values.unlimited_quota);
}}
/>{" "}
/>{' '}
无限额度
<DialogActions>
<Button onClick={onCancel}>取消</Button>
<Button
disableElevation
disabled={isSubmitting}
type="submit"
variant="contained"
color="primary"
>
<Button disableElevation disabled={isSubmitting} type="submit" variant="contained" color="primary">
提交
</Button>
</DialogActions>
@@ -273,5 +315,5 @@ EditModal.propTypes = {
open: PropTypes.bool,
tokenId: PropTypes.number,
onCancel: PropTypes.func,
onOk: PropTypes.func,
onOk: PropTypes.func
};

View File

@@ -28,11 +28,11 @@ const COPY_OPTIONS = [
{
key: 'next',
text: 'ChatGPT Next',
url: 'https://chat.oneapi.pro/#/?settings={"key":"sk-{key}","url":"{serverAddress}"}',
url: 'https://app.nextchat.dev/#/?settings={"key":"laisky-{key}","url":"{serverAddress}"}',
encode: false
},
{ key: 'ama', text: 'BotGem', 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 }
{ key: 'ama', text: 'BotGem', url: 'ama://set-api-key?server={serverAddress}&key=laisky-{key}', encode: true },
{ key: 'opencat', text: 'OpenCat', url: 'opencat://team/join?domain={serverAddress}&token=laisky-{key}', encode: true }
];
function replacePlaceholders(text, key, serverAddress) {
@@ -133,7 +133,7 @@ export default function TokensTableRow({ item, manageToken, handleOpenModal, set
let url = option.url;
if (option.key === 'next' && siteInfo?.chat_link) {
url = siteInfo.chat_link + `/#/?settings={"key":"sk-{key}","url":"{serverAddress}"}`;
url = siteInfo.chat_link + `/#/?settings={"key":"laisky-{key}","url":"{serverAddress}"}`;
}
const key = item.key;
@@ -211,7 +211,7 @@ export default function TokensTableRow({ item, manageToken, handleOpenModal, set
<Button
color="primary"
onClick={() => {
navigator.clipboard.writeText(`sk-${item.key}`);
navigator.clipboard.writeText(`laisky-${item.key}`);
showSuccess('已复制到剪贴板!');
}}
>