mirror of
https://github.com/songquanpeng/one-api.git
synced 2025-11-13 03:43:44 +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:
718
web/berry/src/views/Channel/component/EditModal.js
Normal file
718
web/berry/src/views/Channel/component/EditModal.js
Normal file
@@ -0,0 +1,718 @@
|
||||
import PropTypes from "prop-types";
|
||||
import { useState, useEffect } from "react";
|
||||
import { CHANNEL_OPTIONS } from "constants/ChannelConstants";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { API } from "utils/api";
|
||||
import { showError, showSuccess } from "utils/common";
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
TextField,
|
||||
Button,
|
||||
Divider,
|
||||
Select,
|
||||
MenuItem,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
OutlinedInput,
|
||||
ButtonGroup,
|
||||
Container,
|
||||
Autocomplete,
|
||||
FormHelperText,
|
||||
} 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";
|
||||
|
||||
const filter = createFilterOptions();
|
||||
const validationSchema = Yup.object().shape({
|
||||
is_edit: Yup.boolean(),
|
||||
name: Yup.string().required("名称 不能为空"),
|
||||
type: Yup.number().required("渠道 不能为空"),
|
||||
key: Yup.string().when("is_edit", {
|
||||
is: false,
|
||||
then: Yup.string().required("密钥 不能为空"),
|
||||
}),
|
||||
other: Yup.string(),
|
||||
proxy: Yup.string(),
|
||||
test_model: Yup.string(),
|
||||
models: Yup.array().min(1, "模型 不能为空"),
|
||||
groups: Yup.array().min(1, "用户组 不能为空"),
|
||||
base_url: Yup.string().when("type", {
|
||||
is: (value) => [3, 24, 8].includes(value),
|
||||
then: Yup.string().required("渠道API地址 不能为空"), // base_url 是必需的
|
||||
otherwise: Yup.string(), // 在其他情况下,base_url 可以是任意字符串
|
||||
}),
|
||||
model_mapping: Yup.string().test(
|
||||
"is-json",
|
||||
"必须是有效的JSON字符串",
|
||||
function (value) {
|
||||
try {
|
||||
if (value === "" || value === null || value === undefined) {
|
||||
return true;
|
||||
}
|
||||
const parsedValue = JSON.parse(value);
|
||||
if (typeof parsedValue === "object") {
|
||||
return true;
|
||||
}
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
),
|
||||
});
|
||||
|
||||
const EditModal = ({ open, channelId, onCancel, onOk }) => {
|
||||
const theme = useTheme();
|
||||
// const [loading, setLoading] = useState(false);
|
||||
const [initialInput, setInitialInput] = useState(defaultConfig.input);
|
||||
const [inputLabel, setInputLabel] = useState(defaultConfig.inputLabel); //
|
||||
const [inputPrompt, setInputPrompt] = useState(defaultConfig.prompt);
|
||||
const [groupOptions, setGroupOptions] = useState([]);
|
||||
const [modelOptions, setModelOptions] = useState([]);
|
||||
|
||||
const initChannel = (typeValue) => {
|
||||
if (typeConfig[typeValue]?.inputLabel) {
|
||||
setInputLabel({
|
||||
...defaultConfig.inputLabel,
|
||||
...typeConfig[typeValue].inputLabel,
|
||||
});
|
||||
} else {
|
||||
setInputLabel(defaultConfig.inputLabel);
|
||||
}
|
||||
|
||||
if (typeConfig[typeValue]?.prompt) {
|
||||
setInputPrompt({
|
||||
...defaultConfig.prompt,
|
||||
...typeConfig[typeValue].prompt,
|
||||
});
|
||||
} else {
|
||||
setInputPrompt(defaultConfig.prompt);
|
||||
}
|
||||
|
||||
return typeConfig[typeValue]?.input;
|
||||
};
|
||||
const handleTypeChange = (setFieldValue, typeValue, values) => {
|
||||
const newInput = initChannel(typeValue);
|
||||
|
||||
if (newInput) {
|
||||
Object.keys(newInput).forEach((key) => {
|
||||
if (
|
||||
(!Array.isArray(values[key]) &&
|
||||
values[key] !== null &&
|
||||
values[key] !== undefined &&
|
||||
values[key] !== "") ||
|
||||
(Array.isArray(values[key]) && values[key].length > 0)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (key === "models") {
|
||||
setFieldValue(key, initialModel(newInput[key]));
|
||||
return;
|
||||
}
|
||||
setFieldValue(key, newInput[key]);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const basicModels = (channelType) => {
|
||||
let modelGroup =
|
||||
typeConfig[channelType]?.modelGroup || defaultConfig.modelGroup;
|
||||
// 循环 modelOptions,找到 modelGroup 对应的模型
|
||||
let modelList = [];
|
||||
modelOptions.forEach((model) => {
|
||||
if (model.group === modelGroup) {
|
||||
modelList.push(model);
|
||||
}
|
||||
});
|
||||
return modelList;
|
||||
};
|
||||
|
||||
const fetchGroups = async () => {
|
||||
try {
|
||||
let res = await API.get(`/api/group/`);
|
||||
setGroupOptions(res.data.data);
|
||||
} catch (error) {
|
||||
showError(error.message);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchModels = async () => {
|
||||
try {
|
||||
let res = await API.get(`/api/channel/models`);
|
||||
setModelOptions(
|
||||
res.data.data.map((model) => {
|
||||
return {
|
||||
id: model.id,
|
||||
group: model.owned_by,
|
||||
};
|
||||
})
|
||||
);
|
||||
} catch (error) {
|
||||
showError(error.message);
|
||||
}
|
||||
};
|
||||
|
||||
const submit = async (values, { setErrors, setStatus, setSubmitting }) => {
|
||||
setSubmitting(true);
|
||||
if (values.base_url && values.base_url.endsWith("/")) {
|
||||
values.base_url = values.base_url.slice(0, values.base_url.length - 1);
|
||||
}
|
||||
if (values.type === 3 && values.other === "") {
|
||||
values.other = "2023-09-01-preview";
|
||||
}
|
||||
if (values.type === 18 && values.other === "") {
|
||||
values.other = "v2.1";
|
||||
}
|
||||
let res;
|
||||
const modelsStr = values.models.map((model) => model.id).join(",");
|
||||
values.group = values.groups.join(",");
|
||||
if (channelId) {
|
||||
res = await API.put(`/api/channel/`, {
|
||||
...values,
|
||||
id: parseInt(channelId),
|
||||
models: modelsStr,
|
||||
});
|
||||
} else {
|
||||
res = await API.post(`/api/channel/`, { ...values, models: modelsStr });
|
||||
}
|
||||
const { success, message } = res.data;
|
||||
if (success) {
|
||||
if (channelId) {
|
||||
showSuccess("渠道更新成功!");
|
||||
} else {
|
||||
showSuccess("渠道创建成功!");
|
||||
}
|
||||
setSubmitting(false);
|
||||
setStatus({ success: true });
|
||||
onOk(true);
|
||||
} else {
|
||||
setStatus({ success: false });
|
||||
showError(message);
|
||||
setErrors({ submit: message });
|
||||
}
|
||||
};
|
||||
|
||||
function initialModel(channelModel) {
|
||||
if (!channelModel) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 如果 channelModel 是一个字符串
|
||||
if (typeof channelModel === "string") {
|
||||
channelModel = channelModel.split(",");
|
||||
}
|
||||
let modelList = channelModel.map((model) => {
|
||||
const modelOption = modelOptions.find((option) => option.id === model);
|
||||
if (modelOption) {
|
||||
return modelOption;
|
||||
}
|
||||
return { id: model, group: "自定义:点击或回车输入" };
|
||||
});
|
||||
return modelList;
|
||||
}
|
||||
|
||||
const loadChannel = async () => {
|
||||
let res = await API.get(`/api/channel/${channelId}`);
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
if (data.models === "") {
|
||||
data.models = [];
|
||||
} else {
|
||||
data.models = initialModel(data.models);
|
||||
}
|
||||
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
|
||||
);
|
||||
}
|
||||
data.is_edit = true;
|
||||
initChannel(data.type);
|
||||
setInitialInput(data);
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchGroups().then();
|
||||
fetchModels().then();
|
||||
if (channelId) {
|
||||
loadChannel().then();
|
||||
} else {
|
||||
initChannel(1);
|
||||
setInitialInput({ ...defaultConfig.input, is_edit: false });
|
||||
}
|
||||
}, [channelId]);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={onCancel} fullWidth maxWidth={"md"}>
|
||||
<DialogTitle
|
||||
sx={{
|
||||
margin: "0px",
|
||||
fontWeight: 700,
|
||||
lineHeight: "1.55556",
|
||||
padding: "24px",
|
||||
fontSize: "1.125rem",
|
||||
}}
|
||||
>
|
||||
{channelId ? "编辑渠道" : "新建渠道"}
|
||||
</DialogTitle>
|
||||
<Divider />
|
||||
<DialogContent>
|
||||
<Formik
|
||||
initialValues={initialInput}
|
||||
enableReinitialize
|
||||
validationSchema={validationSchema}
|
||||
onSubmit={submit}
|
||||
>
|
||||
{({
|
||||
errors,
|
||||
handleBlur,
|
||||
handleChange,
|
||||
handleSubmit,
|
||||
isSubmitting,
|
||||
touched,
|
||||
values,
|
||||
setFieldValue,
|
||||
}) => (
|
||||
<form noValidate onSubmit={handleSubmit}>
|
||||
<FormControl
|
||||
fullWidth
|
||||
error={Boolean(touched.type && errors.type)}
|
||||
sx={{ ...theme.typography.otherInput }}
|
||||
>
|
||||
<InputLabel htmlFor="channel-type-label">
|
||||
{inputLabel.type}
|
||||
</InputLabel>
|
||||
<Select
|
||||
id="channel-type-label"
|
||||
label={inputLabel.type}
|
||||
value={values.type}
|
||||
name="type"
|
||||
onBlur={handleBlur}
|
||||
onChange={(e) => {
|
||||
handleChange(e);
|
||||
handleTypeChange(setFieldValue, e.target.value, values);
|
||||
}}
|
||||
MenuProps={{
|
||||
PaperProps: {
|
||||
style: {
|
||||
maxHeight: 200,
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
{Object.values(CHANNEL_OPTIONS).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">
|
||||
{errors.type}
|
||||
</FormHelperText>
|
||||
) : (
|
||||
<FormHelperText id="helper-tex-channel-type-label">
|
||||
{" "}
|
||||
{inputPrompt.type}{" "}
|
||||
</FormHelperText>
|
||||
)}
|
||||
</FormControl>
|
||||
|
||||
<FormControl
|
||||
fullWidth
|
||||
error={Boolean(touched.name && errors.name)}
|
||||
sx={{ ...theme.typography.otherInput }}
|
||||
>
|
||||
<InputLabel htmlFor="channel-name-label">
|
||||
{inputLabel.name}
|
||||
</InputLabel>
|
||||
<OutlinedInput
|
||||
id="channel-name-label"
|
||||
label={inputLabel.name}
|
||||
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>
|
||||
) : (
|
||||
<FormHelperText id="helper-tex-channel-name-label">
|
||||
{" "}
|
||||
{inputPrompt.name}{" "}
|
||||
</FormHelperText>
|
||||
)}
|
||||
</FormControl>
|
||||
|
||||
<FormControl
|
||||
fullWidth
|
||||
error={Boolean(touched.base_url && errors.base_url)}
|
||||
sx={{ ...theme.typography.otherInput }}
|
||||
>
|
||||
<InputLabel htmlFor="channel-base_url-label">
|
||||
{inputLabel.base_url}
|
||||
</InputLabel>
|
||||
<OutlinedInput
|
||||
id="channel-base_url-label"
|
||||
label={inputLabel.base_url}
|
||||
type="text"
|
||||
value={values.base_url}
|
||||
name="base_url"
|
||||
onBlur={handleBlur}
|
||||
onChange={handleChange}
|
||||
inputProps={{}}
|
||||
aria-describedby="helper-text-channel-base_url-label"
|
||||
/>
|
||||
{touched.base_url && errors.base_url ? (
|
||||
<FormHelperText error id="helper-tex-channel-base_url-label">
|
||||
{errors.base_url}
|
||||
</FormHelperText>
|
||||
) : (
|
||||
<FormHelperText id="helper-tex-channel-base_url-label">
|
||||
{" "}
|
||||
{inputPrompt.base_url}{" "}
|
||||
</FormHelperText>
|
||||
)}
|
||||
</FormControl>
|
||||
|
||||
{inputPrompt.other && (
|
||||
<FormControl
|
||||
fullWidth
|
||||
error={Boolean(touched.other && errors.other)}
|
||||
sx={{ ...theme.typography.otherInput }}
|
||||
>
|
||||
<InputLabel htmlFor="channel-other-label">
|
||||
{inputLabel.other}
|
||||
</InputLabel>
|
||||
<OutlinedInput
|
||||
id="channel-other-label"
|
||||
label={inputLabel.other}
|
||||
type="text"
|
||||
value={values.other}
|
||||
name="other"
|
||||
onBlur={handleBlur}
|
||||
onChange={handleChange}
|
||||
inputProps={{}}
|
||||
aria-describedby="helper-text-channel-other-label"
|
||||
/>
|
||||
{touched.other && errors.other ? (
|
||||
<FormHelperText error id="helper-tex-channel-other-label">
|
||||
{errors.other}
|
||||
</FormHelperText>
|
||||
) : (
|
||||
<FormHelperText id="helper-tex-channel-other-label">
|
||||
{" "}
|
||||
{inputPrompt.other}{" "}
|
||||
</FormHelperText>
|
||||
)}
|
||||
</FormControl>
|
||||
)}
|
||||
|
||||
<FormControl fullWidth sx={{ ...theme.typography.otherInput }}>
|
||||
<Autocomplete
|
||||
multiple
|
||||
id="channel-groups-label"
|
||||
options={groupOptions}
|
||||
value={values.groups}
|
||||
onChange={(e, value) => {
|
||||
const event = {
|
||||
target: {
|
||||
name: "groups",
|
||||
value: value,
|
||||
},
|
||||
};
|
||||
handleChange(event);
|
||||
}}
|
||||
onBlur={handleBlur}
|
||||
filterSelectedOptions
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
name="groups"
|
||||
error={Boolean(errors.groups)}
|
||||
label={inputLabel.groups}
|
||||
/>
|
||||
)}
|
||||
aria-describedby="helper-text-channel-groups-label"
|
||||
/>
|
||||
{errors.groups ? (
|
||||
<FormHelperText error id="helper-tex-channel-groups-label">
|
||||
{errors.groups}
|
||||
</FormHelperText>
|
||||
) : (
|
||||
<FormHelperText id="helper-tex-channel-groups-label">
|
||||
{" "}
|
||||
{inputPrompt.groups}{" "}
|
||||
</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.map((item) =>
|
||||
typeof item === "string"
|
||||
? { id: item, group: "自定义:点击或回车输入" }
|
||||
: item
|
||||
),
|
||||
},
|
||||
};
|
||||
handleChange(event);
|
||||
}}
|
||||
onBlur={handleBlur}
|
||||
filterSelectedOptions
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
name="models"
|
||||
error={Boolean(errors.models)}
|
||||
label={inputLabel.models}
|
||||
/>
|
||||
)}
|
||||
groupBy={(option) => option.group}
|
||||
getOptionLabel={(option) => {
|
||||
if (typeof option === "string") {
|
||||
return option;
|
||||
}
|
||||
if (option.inputValue) {
|
||||
return option.inputValue;
|
||||
}
|
||||
return option.id;
|
||||
}}
|
||||
filterOptions={(options, params) => {
|
||||
const filtered = filter(options, params);
|
||||
const { inputValue } = params;
|
||||
const isExisting = options.some(
|
||||
(option) => inputValue === option.id
|
||||
);
|
||||
if (inputValue !== "" && !isExisting) {
|
||||
filtered.push({
|
||||
id: inputValue,
|
||||
group: "自定义:点击或回车输入",
|
||||
});
|
||||
}
|
||||
return filtered;
|
||||
}}
|
||||
/>
|
||||
{errors.models ? (
|
||||
<FormHelperText error id="helper-tex-channel-models-label">
|
||||
{errors.models}
|
||||
</FormHelperText>
|
||||
) : (
|
||||
<FormHelperText id="helper-tex-channel-models-label">
|
||||
{" "}
|
||||
{inputPrompt.models}{" "}
|
||||
</FormHelperText>
|
||||
)}
|
||||
</FormControl>
|
||||
<Container
|
||||
sx={{
|
||||
textAlign: "right",
|
||||
}}
|
||||
>
|
||||
<ButtonGroup
|
||||
variant="outlined"
|
||||
aria-label="small outlined primary button group"
|
||||
>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setFieldValue("models", basicModels(values.type));
|
||||
}}
|
||||
>
|
||||
填入渠道支持模型
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setFieldValue("models", modelOptions);
|
||||
}}
|
||||
>
|
||||
填入所有模型
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Container>
|
||||
<FormControl
|
||||
fullWidth
|
||||
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"
|
||||
/>
|
||||
{touched.key && errors.key ? (
|
||||
<FormHelperText error id="helper-tex-channel-key-label">
|
||||
{errors.key}
|
||||
</FormHelperText>
|
||||
) : (
|
||||
<FormHelperText id="helper-tex-channel-key-label">
|
||||
{" "}
|
||||
{inputPrompt.key}{" "}
|
||||
</FormHelperText>
|
||||
)}
|
||||
</FormControl>
|
||||
<FormControl
|
||||
fullWidth
|
||||
error={Boolean(touched.model_mapping && errors.model_mapping)}
|
||||
sx={{ ...theme.typography.otherInput }}
|
||||
>
|
||||
{/* <InputLabel htmlFor="channel-model_mapping-label">{inputLabel.model_mapping}</InputLabel> */}
|
||||
<TextField
|
||||
multiline
|
||||
id="channel-model_mapping-label"
|
||||
label={inputLabel.model_mapping}
|
||||
value={values.model_mapping}
|
||||
name="model_mapping"
|
||||
onBlur={handleBlur}
|
||||
onChange={handleChange}
|
||||
aria-describedby="helper-text-channel-model_mapping-label"
|
||||
minRows={5}
|
||||
placeholder={inputPrompt.model_mapping}
|
||||
/>
|
||||
{touched.model_mapping && errors.model_mapping ? (
|
||||
<FormHelperText
|
||||
error
|
||||
id="helper-tex-channel-model_mapping-label"
|
||||
>
|
||||
{errors.model_mapping}
|
||||
</FormHelperText>
|
||||
) : (
|
||||
<FormHelperText id="helper-tex-channel-model_mapping-label">
|
||||
{" "}
|
||||
{inputPrompt.model_mapping}{" "}
|
||||
</FormHelperText>
|
||||
)}
|
||||
</FormControl>
|
||||
<FormControl
|
||||
fullWidth
|
||||
error={Boolean(touched.proxy && errors.proxy)}
|
||||
sx={{ ...theme.typography.otherInput }}
|
||||
>
|
||||
<InputLabel htmlFor="channel-proxy-label">
|
||||
{inputLabel.proxy}
|
||||
</InputLabel>
|
||||
<OutlinedInput
|
||||
id="channel-proxy-label"
|
||||
label={inputLabel.proxy}
|
||||
type="text"
|
||||
value={values.proxy}
|
||||
name="proxy"
|
||||
onBlur={handleBlur}
|
||||
onChange={handleChange}
|
||||
inputProps={{}}
|
||||
aria-describedby="helper-text-channel-proxy-label"
|
||||
/>
|
||||
{touched.proxy && errors.proxy ? (
|
||||
<FormHelperText error id="helper-tex-channel-proxy-label">
|
||||
{errors.proxy}
|
||||
</FormHelperText>
|
||||
) : (
|
||||
<FormHelperText id="helper-tex-channel-proxy-label">
|
||||
{" "}
|
||||
{inputPrompt.proxy}{" "}
|
||||
</FormHelperText>
|
||||
)}
|
||||
</FormControl>
|
||||
{inputPrompt.test_model && (
|
||||
<FormControl
|
||||
fullWidth
|
||||
error={Boolean(touched.test_model && errors.test_model)}
|
||||
sx={{ ...theme.typography.otherInput }}
|
||||
>
|
||||
<InputLabel htmlFor="channel-test_model-label">
|
||||
{inputLabel.test_model}
|
||||
</InputLabel>
|
||||
<OutlinedInput
|
||||
id="channel-test_model-label"
|
||||
label={inputLabel.test_model}
|
||||
type="text"
|
||||
value={values.test_model}
|
||||
name="test_model"
|
||||
onBlur={handleBlur}
|
||||
onChange={handleChange}
|
||||
inputProps={{}}
|
||||
aria-describedby="helper-text-channel-test_model-label"
|
||||
/>
|
||||
{touched.test_model && errors.test_model ? (
|
||||
<FormHelperText
|
||||
error
|
||||
id="helper-tex-channel-test_model-label"
|
||||
>
|
||||
{errors.test_model}
|
||||
</FormHelperText>
|
||||
) : (
|
||||
<FormHelperText id="helper-tex-channel-test_model-label">
|
||||
{" "}
|
||||
{inputPrompt.test_model}{" "}
|
||||
</FormHelperText>
|
||||
)}
|
||||
</FormControl>
|
||||
)}
|
||||
<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,
|
||||
channelId: PropTypes.number,
|
||||
onCancel: PropTypes.func,
|
||||
onOk: PropTypes.func,
|
||||
};
|
||||
27
web/berry/src/views/Channel/component/GroupLabel.js
Normal file
27
web/berry/src/views/Channel/component/GroupLabel.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import PropTypes from "prop-types";
|
||||
import Label from "ui-component/Label";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Divider from "@mui/material/Divider";
|
||||
|
||||
const GroupLabel = ({ group }) => {
|
||||
let groups = [];
|
||||
if (group === "") {
|
||||
groups = ["default"];
|
||||
} else {
|
||||
groups = group.split(",");
|
||||
groups.sort();
|
||||
}
|
||||
return (
|
||||
<Stack divider={<Divider orientation="vertical" flexItem />} spacing={0.5}>
|
||||
{groups.map((group, index) => {
|
||||
return <Label key={index}>{group}</Label>;
|
||||
})}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
GroupLabel.propTypes = {
|
||||
group: PropTypes.string,
|
||||
};
|
||||
|
||||
export default GroupLabel;
|
||||
54
web/berry/src/views/Channel/component/NameLabel.js
Normal file
54
web/berry/src/views/Channel/component/NameLabel.js
Normal file
@@ -0,0 +1,54 @@
|
||||
import PropTypes from "prop-types";
|
||||
import { Tooltip, Stack, Container } from "@mui/material";
|
||||
import Label from "ui-component/Label";
|
||||
import { styled } from "@mui/material/styles";
|
||||
import { showSuccess } from "utils/common";
|
||||
|
||||
const TooltipContainer = styled(Container)({
|
||||
maxHeight: "250px",
|
||||
overflow: "auto",
|
||||
"&::-webkit-scrollbar": {
|
||||
width: "0px", // Set the width to 0 to hide the scrollbar
|
||||
},
|
||||
});
|
||||
|
||||
const NameLabel = ({ name, models }) => {
|
||||
let modelMap = [];
|
||||
modelMap = models.split(",");
|
||||
modelMap.sort();
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
title={
|
||||
<TooltipContainer>
|
||||
<Stack spacing={1}>
|
||||
{modelMap.map((item, index) => {
|
||||
return (
|
||||
<Label
|
||||
variant="ghost"
|
||||
key={index}
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(item);
|
||||
showSuccess("复制模型名称成功!");
|
||||
}}
|
||||
>
|
||||
{item}
|
||||
</Label>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
</TooltipContainer>
|
||||
}
|
||||
placement="top"
|
||||
>
|
||||
<span>{name}</span>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
NameLabel.propTypes = {
|
||||
name: PropTypes.string,
|
||||
models: PropTypes.string,
|
||||
};
|
||||
|
||||
export default NameLabel;
|
||||
43
web/berry/src/views/Channel/component/ResponseTimeLabel.js
Normal file
43
web/berry/src/views/Channel/component/ResponseTimeLabel.js
Normal file
@@ -0,0 +1,43 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import Label from 'ui-component/Label';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
import { timestamp2string } from 'utils/common';
|
||||
|
||||
const ResponseTimeLabel = ({ test_time, response_time, handle_action }) => {
|
||||
let color = 'default';
|
||||
let time = response_time / 1000;
|
||||
time = time.toFixed(2) + ' 秒';
|
||||
|
||||
if (response_time === 0) {
|
||||
color = 'default';
|
||||
} else if (response_time <= 1000) {
|
||||
color = 'success';
|
||||
} else if (response_time <= 3000) {
|
||||
color = 'primary';
|
||||
} else if (response_time <= 5000) {
|
||||
color = 'secondary';
|
||||
} else {
|
||||
color = 'error';
|
||||
}
|
||||
let title = (
|
||||
<>
|
||||
点击测速
|
||||
<br />
|
||||
{test_time != 0 ? '上次测速时间:' + timestamp2string(test_time) : '未测试'}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<Tooltip title={title} placement="top" onClick={handle_action}>
|
||||
<Label color={color}> {response_time == 0 ? '未测试' : time} </Label>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
ResponseTimeLabel.propTypes = {
|
||||
test_time: PropTypes.number,
|
||||
response_time: PropTypes.number,
|
||||
handle_action: PropTypes.func
|
||||
};
|
||||
|
||||
export default ResponseTimeLabel;
|
||||
21
web/berry/src/views/Channel/component/TableHead.js
Normal file
21
web/berry/src/views/Channel/component/TableHead.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import { TableCell, TableHead, TableRow } from '@mui/material';
|
||||
|
||||
const ChannelTableHead = () => {
|
||||
return (
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>ID</TableCell>
|
||||
<TableCell>名称</TableCell>
|
||||
<TableCell>分组</TableCell>
|
||||
<TableCell>类型</TableCell>
|
||||
<TableCell>状态</TableCell>
|
||||
<TableCell>响应时间</TableCell>
|
||||
<TableCell>余额</TableCell>
|
||||
<TableCell>优先级</TableCell>
|
||||
<TableCell>操作</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChannelTableHead;
|
||||
284
web/berry/src/views/Channel/component/TableRow.js
Normal file
284
web/berry/src/views/Channel/component/TableRow.js
Normal file
@@ -0,0 +1,284 @@
|
||||
import PropTypes from "prop-types";
|
||||
import { useState } from "react";
|
||||
|
||||
import { showInfo, showError, renderNumber } from "utils/common";
|
||||
import { API } from "utils/api";
|
||||
import { CHANNEL_OPTIONS } from "constants/ChannelConstants";
|
||||
|
||||
import {
|
||||
Popover,
|
||||
TableRow,
|
||||
MenuItem,
|
||||
TableCell,
|
||||
IconButton,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
InputAdornment,
|
||||
Input,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogContentText,
|
||||
DialogTitle,
|
||||
Tooltip,
|
||||
Button,
|
||||
} from "@mui/material";
|
||||
|
||||
import Label from "ui-component/Label";
|
||||
import TableSwitch from "ui-component/Switch";
|
||||
|
||||
import ResponseTimeLabel from "./ResponseTimeLabel";
|
||||
import GroupLabel from "./GroupLabel";
|
||||
import NameLabel from "./NameLabel";
|
||||
|
||||
import {
|
||||
IconDotsVertical,
|
||||
IconEdit,
|
||||
IconTrash,
|
||||
IconPencil,
|
||||
} from "@tabler/icons-react";
|
||||
|
||||
export default function ChannelTableRow({
|
||||
item,
|
||||
manageChannel,
|
||||
handleOpenModal,
|
||||
setModalChannelId,
|
||||
}) {
|
||||
const [open, setOpen] = useState(null);
|
||||
const [openDelete, setOpenDelete] = useState(false);
|
||||
const [statusSwitch, setStatusSwitch] = useState(item.status);
|
||||
const [priorityValve, setPriority] = useState(item.priority);
|
||||
const [responseTimeData, setResponseTimeData] = useState({
|
||||
test_time: item.test_time,
|
||||
response_time: item.response_time,
|
||||
});
|
||||
const [itemBalance, setItemBalance] = useState(item.balance);
|
||||
|
||||
const handleDeleteOpen = () => {
|
||||
handleCloseMenu();
|
||||
setOpenDelete(true);
|
||||
};
|
||||
|
||||
const handleDeleteClose = () => {
|
||||
setOpenDelete(false);
|
||||
};
|
||||
|
||||
const handleOpenMenu = (event) => {
|
||||
setOpen(event.currentTarget);
|
||||
};
|
||||
|
||||
const handleCloseMenu = () => {
|
||||
setOpen(null);
|
||||
};
|
||||
|
||||
const handleStatus = async () => {
|
||||
const switchVlue = statusSwitch === 1 ? 2 : 1;
|
||||
const { success } = await manageChannel(item.id, "status", switchVlue);
|
||||
if (success) {
|
||||
setStatusSwitch(switchVlue);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePriority = async () => {
|
||||
if (priorityValve === "" || priorityValve === item.priority) {
|
||||
return;
|
||||
}
|
||||
await manageChannel(item.id, "priority", priorityValve);
|
||||
};
|
||||
|
||||
const handleResponseTime = async () => {
|
||||
const { success, time } = await manageChannel(item.id, "test", "");
|
||||
if (success) {
|
||||
setResponseTimeData({
|
||||
test_time: Date.now() / 1000,
|
||||
response_time: time * 1000,
|
||||
});
|
||||
showInfo(`通道 ${item.name} 测试成功,耗时 ${time.toFixed(2)} 秒。`);
|
||||
}
|
||||
};
|
||||
|
||||
const updateChannelBalance = async () => {
|
||||
const res = await API.get(`/api/channel/update_balance/${item.id}`);
|
||||
const { success, message, balance } = res.data;
|
||||
if (success) {
|
||||
setItemBalance(balance);
|
||||
|
||||
showInfo(`余额更新成功!`);
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
handleCloseMenu();
|
||||
await manageChannel(item.id, "delete", "");
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<TableRow tabIndex={item.id}>
|
||||
<TableCell>{item.id}</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<NameLabel name={item.name} models={item.models} />
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<GroupLabel group={item.group} />
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
{!CHANNEL_OPTIONS[item.type] ? (
|
||||
<Label color="error" variant="outlined">
|
||||
未知
|
||||
</Label>
|
||||
) : (
|
||||
<Label color={CHANNEL_OPTIONS[item.type].color} variant="outlined">
|
||||
{CHANNEL_OPTIONS[item.type].text}
|
||||
</Label>
|
||||
)}
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<Tooltip
|
||||
title={(() => {
|
||||
switch (statusSwitch) {
|
||||
case 1:
|
||||
return "已启用";
|
||||
case 2:
|
||||
return "本渠道被手动禁用";
|
||||
case 3:
|
||||
return "本渠道被程序自动禁用";
|
||||
default:
|
||||
return "未知";
|
||||
}
|
||||
})()}
|
||||
placement="top"
|
||||
>
|
||||
<TableSwitch
|
||||
id={`switch-${item.id}`}
|
||||
checked={statusSwitch === 1}
|
||||
onChange={handleStatus}
|
||||
/>
|
||||
</Tooltip>
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<ResponseTimeLabel
|
||||
test_time={responseTimeData.test_time}
|
||||
response_time={responseTimeData.response_time}
|
||||
handle_action={handleResponseTime}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Tooltip
|
||||
title={"点击更新余额"}
|
||||
placement="top"
|
||||
onClick={updateChannelBalance}
|
||||
>
|
||||
{renderBalance(item.type, itemBalance)}
|
||||
</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>
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<IconButton
|
||||
onClick={handleOpenMenu}
|
||||
sx={{ color: "rgb(99, 115, 129)" }}
|
||||
>
|
||||
<IconDotsVertical />
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
<Popover
|
||||
open={!!open}
|
||||
anchorEl={open}
|
||||
onClose={handleCloseMenu}
|
||||
anchorOrigin={{ vertical: "top", horizontal: "left" }}
|
||||
transformOrigin={{ vertical: "top", horizontal: "right" }}
|
||||
PaperProps={{
|
||||
sx: { width: 140 },
|
||||
}}
|
||||
>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
handleCloseMenu();
|
||||
handleOpenModal();
|
||||
setModalChannelId(item.id);
|
||||
}}
|
||||
>
|
||||
<IconEdit style={{ marginRight: "16px" }} />
|
||||
编辑
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleDeleteOpen} sx={{ color: "error.main" }}>
|
||||
<IconTrash style={{ marginRight: "16px" }} />
|
||||
删除
|
||||
</MenuItem>
|
||||
</Popover>
|
||||
|
||||
<Dialog open={openDelete} onClose={handleDeleteClose}>
|
||||
<DialogTitle>删除通道</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>是否删除通道 {item.name}?</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleDeleteClose}>关闭</Button>
|
||||
<Button onClick={handleDelete} sx={{ color: "error.main" }} autoFocus>
|
||||
删除
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
ChannelTableRow.propTypes = {
|
||||
item: PropTypes.object,
|
||||
manageChannel: PropTypes.func,
|
||||
handleOpenModal: PropTypes.func,
|
||||
setModalChannelId: PropTypes.func,
|
||||
};
|
||||
|
||||
function renderBalance(type, balance) {
|
||||
switch (type) {
|
||||
case 1: // OpenAI
|
||||
return <span>${balance.toFixed(2)}</span>;
|
||||
case 4: // CloseAI
|
||||
return <span>¥{balance.toFixed(2)}</span>;
|
||||
case 8: // 自定义
|
||||
return <span>${balance.toFixed(2)}</span>;
|
||||
case 5: // OpenAI-SB
|
||||
return <span>¥{(balance / 10000).toFixed(2)}</span>;
|
||||
case 10: // AI Proxy
|
||||
return <span>{renderNumber(balance)}</span>;
|
||||
case 12: // API2GPT
|
||||
return <span>¥{balance.toFixed(2)}</span>;
|
||||
case 13: // AIGC2D
|
||||
return <span>{renderNumber(balance)}</span>;
|
||||
default:
|
||||
return <span>不支持</span>;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user