✨ feat: support token limit ip range and models
This commit is contained in:
parent
eb482990c1
commit
df247bbfb6
@ -1,9 +1,9 @@
|
|||||||
import PropTypes from "prop-types";
|
import PropTypes from 'prop-types';
|
||||||
import * as Yup from "yup";
|
import * as Yup from 'yup';
|
||||||
import { Formik } from "formik";
|
import { Formik } from 'formik';
|
||||||
import { useTheme } from "@mui/material/styles";
|
import { useTheme } from '@mui/material/styles';
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from 'react';
|
||||||
import dayjs from "dayjs";
|
import dayjs from 'dayjs';
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
@ -16,53 +16,66 @@ import {
|
|||||||
InputLabel,
|
InputLabel,
|
||||||
OutlinedInput,
|
OutlinedInput,
|
||||||
InputAdornment,
|
InputAdornment,
|
||||||
|
Autocomplete,
|
||||||
|
Checkbox,
|
||||||
|
TextField,
|
||||||
Switch,
|
Switch,
|
||||||
FormHelperText,
|
FormHelperText
|
||||||
} from "@mui/material";
|
} from '@mui/material';
|
||||||
|
|
||||||
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
|
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
|
||||||
import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
|
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
|
||||||
import { DateTimePicker } from "@mui/x-date-pickers/DateTimePicker";
|
import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker';
|
||||||
import { renderQuotaWithPrompt, showSuccess, showError } from "utils/common";
|
import { renderQuotaWithPrompt, showSuccess, showError } from 'utils/common';
|
||||||
import { API } from "utils/api";
|
import { API } from 'utils/api';
|
||||||
require("dayjs/locale/zh-cn");
|
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({
|
const validationSchema = Yup.object().shape({
|
||||||
is_edit: Yup.boolean(),
|
is_edit: Yup.boolean(),
|
||||||
name: Yup.string().required("名称 不能为空"),
|
name: Yup.string().required('名称 不能为空'),
|
||||||
remain_quota: Yup.number().min(0, "必须大于等于0"),
|
remain_quota: Yup.number().min(0, '必须大于等于0'),
|
||||||
expired_time: Yup.number(),
|
expired_time: Yup.number(),
|
||||||
unlimited_quota: Yup.boolean(),
|
unlimited_quota: Yup.boolean()
|
||||||
});
|
});
|
||||||
|
|
||||||
const originInputs = {
|
const originInputs = {
|
||||||
is_edit: false,
|
is_edit: false,
|
||||||
name: "",
|
name: '',
|
||||||
remain_quota: 0,
|
remain_quota: 0,
|
||||||
expired_time: -1,
|
expired_time: -1,
|
||||||
unlimited_quota: false,
|
unlimited_quota: false,
|
||||||
|
subnet: '',
|
||||||
|
models: []
|
||||||
};
|
};
|
||||||
|
|
||||||
const EditModal = ({ open, tokenId, onCancel, onOk }) => {
|
const EditModal = ({ open, tokenId, onCancel, onOk }) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const [inputs, setInputs] = useState(originInputs);
|
const [inputs, setInputs] = useState(originInputs);
|
||||||
|
const [modelOptions, setModelOptions] = useState([]);
|
||||||
|
|
||||||
const submit = async (values, { setErrors, setStatus, setSubmitting }) => {
|
const submit = async (values, { setErrors, setStatus, setSubmitting }) => {
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
|
|
||||||
values.remain_quota = parseInt(values.remain_quota);
|
values.remain_quota = parseInt(values.remain_quota);
|
||||||
let res;
|
let res;
|
||||||
|
let models = values.models.join(',');
|
||||||
if (values.is_edit) {
|
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 {
|
} else {
|
||||||
res = await API.post(`/api/token/`, values);
|
res = await API.post(`/api/token/`, { ...values, models: models });
|
||||||
}
|
}
|
||||||
const { success, message } = res.data;
|
const { success, message } = res.data;
|
||||||
if (success) {
|
if (success) {
|
||||||
if (values.is_edit) {
|
if (values.is_edit) {
|
||||||
showSuccess("令牌更新成功!");
|
showSuccess('令牌更新成功!');
|
||||||
} else {
|
} else {
|
||||||
showSuccess("令牌创建成功,请在列表页面点击复制获取令牌!");
|
showSuccess('令牌创建成功,请在列表页面点击复制获取令牌!');
|
||||||
}
|
}
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
setStatus({ success: true });
|
setStatus({ success: true });
|
||||||
@ -78,61 +91,55 @@ const EditModal = ({ open, tokenId, onCancel, onOk }) => {
|
|||||||
const { success, message, data } = res.data;
|
const { success, message, data } = res.data;
|
||||||
if (success) {
|
if (success) {
|
||||||
data.is_edit = true;
|
data.is_edit = true;
|
||||||
|
if (data.models === '') {
|
||||||
|
data.models = [];
|
||||||
|
} else {
|
||||||
|
data.models = data.models.split(',');
|
||||||
|
}
|
||||||
setInputs(data);
|
setInputs(data);
|
||||||
} else {
|
} else {
|
||||||
showError(message);
|
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(() => {
|
useEffect(() => {
|
||||||
if (tokenId) {
|
if (tokenId) {
|
||||||
loadToken().then();
|
loadToken().then();
|
||||||
} else {
|
} else {
|
||||||
setInputs({...originInputs});
|
setInputs({ ...originInputs });
|
||||||
}
|
}
|
||||||
|
loadAvailableModels().then();
|
||||||
}, [tokenId]);
|
}, [tokenId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onClose={onCancel} fullWidth maxWidth={"md"}>
|
<Dialog open={open} onClose={onCancel} fullWidth maxWidth={'md'}>
|
||||||
<DialogTitle
|
<DialogTitle
|
||||||
sx={{
|
sx={{
|
||||||
margin: "0px",
|
margin: '0px',
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
lineHeight: "1.55556",
|
lineHeight: '1.55556',
|
||||||
padding: "24px",
|
padding: '24px',
|
||||||
fontSize: "1.125rem",
|
fontSize: '1.125rem'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{tokenId ? "编辑令牌" : "新建令牌"}
|
{tokenId ? '编辑令牌' : '新建令牌'}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<Divider />
|
<Divider />
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<Alert severity="info">
|
<Alert severity="info">注意,令牌的额度仅用于限制令牌本身的最大额度使用量,实际的使用受到账户的剩余额度限制。</Alert>
|
||||||
注意,令牌的额度仅用于限制令牌本身的最大额度使用量,实际的使用受到账户的剩余额度限制。
|
<Formik initialValues={inputs} enableReinitialize validationSchema={validationSchema} onSubmit={submit}>
|
||||||
</Alert>
|
{({ errors, handleBlur, handleChange, handleSubmit, touched, values, setFieldError, setFieldValue, isSubmitting }) => (
|
||||||
<Formik
|
|
||||||
initialValues={inputs}
|
|
||||||
enableReinitialize
|
|
||||||
validationSchema={validationSchema}
|
|
||||||
onSubmit={submit}
|
|
||||||
>
|
|
||||||
{({
|
|
||||||
errors,
|
|
||||||
handleBlur,
|
|
||||||
handleChange,
|
|
||||||
handleSubmit,
|
|
||||||
touched,
|
|
||||||
values,
|
|
||||||
setFieldError,
|
|
||||||
setFieldValue,
|
|
||||||
isSubmitting,
|
|
||||||
}) => (
|
|
||||||
<form noValidate onSubmit={handleSubmit}>
|
<form noValidate onSubmit={handleSubmit}>
|
||||||
<FormControl
|
<FormControl fullWidth error={Boolean(touched.name && errors.name)} sx={{ ...theme.typography.otherInput }}>
|
||||||
fullWidth
|
|
||||||
error={Boolean(touched.name && errors.name)}
|
|
||||||
sx={{ ...theme.typography.otherInput }}
|
|
||||||
>
|
|
||||||
<InputLabel htmlFor="channel-name-label">名称</InputLabel>
|
<InputLabel htmlFor="channel-name-label">名称</InputLabel>
|
||||||
<OutlinedInput
|
<OutlinedInput
|
||||||
id="channel-name-label"
|
id="channel-name-label"
|
||||||
@ -142,7 +149,7 @@ const EditModal = ({ open, tokenId, onCancel, onOk }) => {
|
|||||||
name="name"
|
name="name"
|
||||||
onBlur={handleBlur}
|
onBlur={handleBlur}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
inputProps={{ autoComplete: "name" }}
|
inputProps={{ autoComplete: 'name' }}
|
||||||
aria-describedby="helper-text-channel-name-label"
|
aria-describedby="helper-text-channel-name-label"
|
||||||
/>
|
/>
|
||||||
{touched.name && errors.name && (
|
{touched.name && errors.name && (
|
||||||
@ -151,42 +158,99 @@ const EditModal = ({ open, tokenId, onCancel, onOk }) => {
|
|||||||
</FormHelperText>
|
</FormHelperText>
|
||||||
)}
|
)}
|
||||||
</FormControl>
|
</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 && (
|
{values.expired_time !== -1 && (
|
||||||
<FormControl
|
<FormControl fullWidth error={Boolean(touched.expired_time && errors.expired_time)} sx={{ ...theme.typography.otherInput }}>
|
||||||
fullWidth
|
<LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale={'zh-cn'}>
|
||||||
error={Boolean(touched.expired_time && errors.expired_time)}
|
|
||||||
sx={{ ...theme.typography.otherInput }}
|
|
||||||
>
|
|
||||||
<LocalizationProvider
|
|
||||||
dateAdapter={AdapterDayjs}
|
|
||||||
adapterLocale={"zh-cn"}
|
|
||||||
>
|
|
||||||
<DateTimePicker
|
<DateTimePicker
|
||||||
label="过期时间"
|
label="过期时间"
|
||||||
ampm={false}
|
ampm={false}
|
||||||
value={dayjs.unix(values.expired_time)}
|
value={dayjs.unix(values.expired_time)}
|
||||||
onError={(newError) => {
|
onError={(newError) => {
|
||||||
if (newError === null) {
|
if (newError === null) {
|
||||||
setFieldError("expired_time", null);
|
setFieldError('expired_time', null);
|
||||||
} else {
|
} else {
|
||||||
setFieldError("expired_time", "无效的日期");
|
setFieldError('expired_time', '无效的日期');
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onChange={(newValue) => {
|
onChange={(newValue) => {
|
||||||
setFieldValue("expired_time", newValue.unix());
|
setFieldValue('expired_time', newValue.unix());
|
||||||
}}
|
}}
|
||||||
slotProps={{
|
slotProps={{
|
||||||
actionBar: {
|
actionBar: {
|
||||||
actions: ["today", "accept"],
|
actions: ['today', 'accept']
|
||||||
},
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</LocalizationProvider>
|
</LocalizationProvider>
|
||||||
{errors.expired_time && (
|
{errors.expired_time && (
|
||||||
<FormHelperText
|
<FormHelperText error id="helper-tex-channel-expired_time-label">
|
||||||
error
|
|
||||||
id="helper-tex-channel-expired_time-label"
|
|
||||||
>
|
|
||||||
{errors.expired_time}
|
{errors.expired_time}
|
||||||
</FormHelperText>
|
</FormHelperText>
|
||||||
)}
|
)}
|
||||||
@ -196,35 +260,22 @@ const EditModal = ({ open, tokenId, onCancel, onOk }) => {
|
|||||||
checked={values.expired_time === -1}
|
checked={values.expired_time === -1}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (values.expired_time === -1) {
|
if (values.expired_time === -1) {
|
||||||
setFieldValue(
|
setFieldValue('expired_time', Math.floor(Date.now() / 1000));
|
||||||
"expired_time",
|
|
||||||
Math.floor(Date.now() / 1000)
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
setFieldValue("expired_time", -1);
|
setFieldValue('expired_time', -1);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>{" "}
|
/>{' '}
|
||||||
永不过期
|
永不过期
|
||||||
<FormControl
|
<FormControl fullWidth error={Boolean(touched.remain_quota && errors.remain_quota)} sx={{ ...theme.typography.otherInput }}>
|
||||||
fullWidth
|
<InputLabel htmlFor="channel-remain_quota-label">额度</InputLabel>
|
||||||
error={Boolean(touched.remain_quota && errors.remain_quota)}
|
|
||||||
sx={{ ...theme.typography.otherInput }}
|
|
||||||
>
|
|
||||||
<InputLabel htmlFor="channel-remain_quota-label">
|
|
||||||
额度
|
|
||||||
</InputLabel>
|
|
||||||
<OutlinedInput
|
<OutlinedInput
|
||||||
id="channel-remain_quota-label"
|
id="channel-remain_quota-label"
|
||||||
label="额度"
|
label="额度"
|
||||||
type="number"
|
type="number"
|
||||||
value={values.remain_quota}
|
value={values.remain_quota}
|
||||||
name="remain_quota"
|
name="remain_quota"
|
||||||
endAdornment={
|
endAdornment={<InputAdornment position="end">{renderQuotaWithPrompt(values.remain_quota)}</InputAdornment>}
|
||||||
<InputAdornment position="end">
|
|
||||||
{renderQuotaWithPrompt(values.remain_quota)}
|
|
||||||
</InputAdornment>
|
|
||||||
}
|
|
||||||
onBlur={handleBlur}
|
onBlur={handleBlur}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
aria-describedby="helper-text-channel-remain_quota-label"
|
aria-describedby="helper-text-channel-remain_quota-label"
|
||||||
@ -232,10 +283,7 @@ const EditModal = ({ open, tokenId, onCancel, onOk }) => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{touched.remain_quota && errors.remain_quota && (
|
{touched.remain_quota && errors.remain_quota && (
|
||||||
<FormHelperText
|
<FormHelperText error id="helper-tex-channel-remain_quota-label">
|
||||||
error
|
|
||||||
id="helper-tex-channel-remain_quota-label"
|
|
||||||
>
|
|
||||||
{errors.remain_quota}
|
{errors.remain_quota}
|
||||||
</FormHelperText>
|
</FormHelperText>
|
||||||
)}
|
)}
|
||||||
@ -243,19 +291,13 @@ const EditModal = ({ open, tokenId, onCancel, onOk }) => {
|
|||||||
<Switch
|
<Switch
|
||||||
checked={values.unlimited_quota === true}
|
checked={values.unlimited_quota === true}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setFieldValue("unlimited_quota", !values.unlimited_quota);
|
setFieldValue('unlimited_quota', !values.unlimited_quota);
|
||||||
}}
|
}}
|
||||||
/>{" "}
|
/>{' '}
|
||||||
无限额度
|
无限额度
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={onCancel}>取消</Button>
|
<Button onClick={onCancel}>取消</Button>
|
||||||
<Button
|
<Button disableElevation disabled={isSubmitting} type="submit" variant="contained" color="primary">
|
||||||
disableElevation
|
|
||||||
disabled={isSubmitting}
|
|
||||||
type="submit"
|
|
||||||
variant="contained"
|
|
||||||
color="primary"
|
|
||||||
>
|
|
||||||
提交
|
提交
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
@ -273,5 +315,5 @@ EditModal.propTypes = {
|
|||||||
open: PropTypes.bool,
|
open: PropTypes.bool,
|
||||||
tokenId: PropTypes.number,
|
tokenId: PropTypes.number,
|
||||||
onCancel: PropTypes.func,
|
onCancel: PropTypes.func,
|
||||||
onOk: PropTypes.func,
|
onOk: PropTypes.func
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user