feat: berry theme update & bug fix (#1282)
* ⚡️ improve: delete google fonts * ⚡️ improve: Optimized priority input handling in TableRow component. * 🔖 chore: channel batch add * ✨ feat: add dark mod * ✨ feat: support token limit ip range and models * ✨ feat: add MessagePusher * ✨ feat: add lark login
This commit is contained in:
parent
acf8cb6248
commit
3fe2863ff7
8
web/berry/.prettierrc
Normal file
8
web/berry/.prettierrc
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"bracketSpacing": true,
|
||||
"printWidth": 140,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"tabWidth": 2,
|
||||
"useTabs": false
|
||||
}
|
@ -11,11 +11,6 @@
|
||||
name="description"
|
||||
content="OpenAI 接口聚合管理,支持多种渠道包括 Azure,可用于二次分发管理 key,仅单可执行文件,已打包好 Docker 镜像,一键部署,开箱即用"
|
||||
/>
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Poppins:wght@400;500;600;700&family=Roboto:wght@400;500;700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
@ -1,8 +1,9 @@
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useEffect } from 'react';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
|
||||
import { ThemeProvider } from '@mui/material/styles';
|
||||
import { CssBaseline, StyledEngineProvider } from '@mui/material';
|
||||
|
||||
import { SET_THEME } from 'store/actions';
|
||||
// routing
|
||||
import Routes from 'routes';
|
||||
|
||||
@ -20,8 +21,16 @@ import { SnackbarProvider } from 'notistack';
|
||||
// ==============================|| APP ||============================== //
|
||||
|
||||
const App = () => {
|
||||
const dispatch = useDispatch();
|
||||
const customization = useSelector((state) => state.customization);
|
||||
|
||||
useEffect(() => {
|
||||
const storedTheme = localStorage.getItem('theme');
|
||||
if (storedTheme) {
|
||||
dispatch({ type: SET_THEME, theme: storedTheme });
|
||||
}
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<StyledEngineProvider injectFirst>
|
||||
<ThemeProvider theme={themes(customization)}>
|
||||
|
BIN
web/berry/src/assets/fonts/roboto-500.woff2
Normal file
BIN
web/berry/src/assets/fonts/roboto-500.woff2
Normal file
Binary file not shown.
BIN
web/berry/src/assets/fonts/roboto-700.woff2
Normal file
BIN
web/berry/src/assets/fonts/roboto-700.woff2
Normal file
Binary file not shown.
BIN
web/berry/src/assets/fonts/roboto-regular.woff2
Normal file
BIN
web/berry/src/assets/fonts/roboto-regular.woff2
Normal file
Binary file not shown.
1
web/berry/src/assets/images/icons/lark.svg
Normal file
1
web/berry/src/assets/images/icons/lark.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 5.4 KiB |
13
web/berry/src/assets/images/logo-white.svg
Normal file
13
web/berry/src/assets/images/logo-white.svg
Normal file
@ -0,0 +1,13 @@
|
||||
<svg viewBox="0 0 590 360" xmlns="http://www.w3.org/2000/svg" version="1.1">
|
||||
<g>
|
||||
<ellipse transform="rotate(103.6 287.6 32.87)" id="svg_1" ry="28.14" rx="28.47" cy="32.87" cx="287.60001" fill="#fff"/>
|
||||
<path id="svg_2" d="m232.92,128.89c3.78,27.29 -1.81,55.44 -17.71,78.09a2.62,2.62 0 0 0 -0.06,2.92c1.24,1.92 2.96,5.05 5.56,4.94q5.25,-0.22 10.79,0.11a1.26,1.26 0 0 1 1.19,1.27l-0.4,42.53a1.31,1.31 0 0 1 -1.31,1.3q-16.77,-0.09 -36.53,0.01q-2.25,0.02 -3.71,-1.56q-16.02,-17.28 -31.98,-35.32c-5.13,-5.8 -10.18,-11.16 -14.86,-17.59a1.35,1.34 -31.1 0 1 0.5,-2q12.88,-6.32 22.13,-17.12q18.18,-21.23 15.08,-48.84q-2.66,-23.7 -22.4,-40.46q-23.43,-19.9 -54.88,-13.86c-4.1,0.79 -7.83,2.5 -11.72,4.12q-11.86,4.94 -20.59,14.64c-14.25,15.81 -20.07,36.4 -15.05,57.16q4.99,20.63 22.86,35.71c10.45,8.81 23.7,13.12 37.26,14.18q1.47,0.11 3.6,2.65c11.68,13.89 24.48,27.72 35.94,41.96a0.43,0.43 0 0 1 -0.21,0.68q-22.51,7.27 -47.37,5.37q-19.4,-1.47 -39.74,-11.22q-18.27,-8.75 -30.59,-21.28q-18.66,-18.98 -28.02,-43.57q-10.8,-28.4 -4.93,-58.67c1.59,-8.17 4.03,-17 7.42,-24.61q5.08,-11.38 11.61,-20.64q25.41,-36.03 68.45,-46.13q32.42,-7.61 64.23,3.92q25.31,9.17 43.2,27.31c16.85,17.09 28.91,40.01 32.24,64z" fill="#fff"/>
|
||||
<path id="svg_3" d="m499.47,180.61c6.45,13.53 16.44,21.75 31.96,22q11.94,0.19 22.17,-5.36q2.21,-1.2 3.93,0.69q12.56,13.78 24.89,28.47q1.21,1.44 1.44,3.13a0.95,0.95 0 0 1 -0.36,0.89c-1.62,1.23 -3.33,2.71 -5.03,3.69q-29.37,17.01 -62.47,11.31c-20.61,-3.55 -39.05,-15.24 -51.47,-32.51q-6.4,-8.89 -9.91,-17.08c-2.62,-6.12 -4.73,-13.3 -5.41,-20.08q-3.96,-39.88 22.94,-67.74c9.48,-9.81 21.15,-16.67 34.39,-19.49c16.54,-3.53 34.64,-1.83 48.77,7.1q13.92,8.79 21.13,20.4q11.07,17.84 10.48,38.92c-0.02,0.94 -0.21,1.81 -0.85,2.54q-7.73,8.77 -18.71,20.16c-1.28,1.32 -2.61,2.26 -4.51,2.23q-24.45,-0.37 -51.64,-0.41q-5.03,0 -10.84,-0.22a0.96,0.95 -11.7 0 0 -0.9,1.36zm1.12,-37.17q-0.55,1.19 -0.63,2.34q-0.08,1.01 0.94,1.03q19.01,0.25 36.98,0.01q0.5,0 0.94,-0.22q0.57,-0.28 0.44,-0.9q-2.34,-11.6 -14.11,-15.25q-3.59,-1.11 -6.44,-0.57q-13.07,2.5 -18.12,13.56z" fill="#fff"/>
|
||||
<path id="svg_4" d="m312.3,100.22a0.5,0.49 -22.1 0 0 0.84,0.35q2.76,-2.64 5.82,-4.31q8.45,-4.62 16.71,-6.57c15.81,-3.72 33.58,-3.2 48.2,3.95q24.49,11.98 35.05,35.76c4.66,10.5 5.44,22.96 5.5,35.35q0.21,49.99 -0.12,88q-0.03,3.06 -0.08,6.16a1.32,1.32 0 0 1 -1.33,1.3q-20.22,-0.18 -40.18,-0.23q-3.64,-0.01 -8.13,-0.44a1.06,1.05 -87.3 0 1 -0.95,-1.05q0.02,-45.49 -0.22,-92.99c-0.03,-6.25 -1.21,-13.88 -5.05,-18.95q-5.33,-7.03 -12.32,-10.18c-10.99,-4.93 -24.52,-1.84 -33.13,6.37q-10.01,9.53 -10.07,23.76q-0.11,25.46 -0.1,48.98c0,3.52 -0.06,8.31 -1.1,11.68c-4.37,14.04 -17.31,19.5 -31.04,16.77c-8.22,-1.64 -15.07,-7.75 -17.62,-15.62q-1.45,-4.49 -1.42,-10.2q0.3,-64.69 0.1,-129.86a0.47,0.47 0 0 1 0.47,-0.47l48.46,-0.35a1.56,1.55 89.4 0 1 1.56,1.54l0.15,11.25z" fill="#fff"/>
|
||||
<path id="svg_5" d="m265.63,344.43a2.02,2.01 76.7 0 0 -1.85,-1.15l-17.03,0.24a2.25,2.22 9.3 0 0 -2.06,1.46l-2.86,7.84a2.47,2.46 -79.1 0 1 -2.38,1.62l-6.23,-0.19q-1.19,-0.04 -0.88,-1.19q1.38,-5.23 2.81,-8.7c3.41,-8.3 6.48,-16.83 10.12,-25.35q2.96,-6.93 5.21,-14.24c0.46,-1.52 1.69,-2.64 3.37,-2.63c2.02,0 4.68,-0.78 5.7,1.58q7.68,17.74 18.16,44.75q0.96,2.46 1.48,5a0.67,0.66 84.3 0 1 -0.65,0.8l-6.05,-0.02q-2.16,-0.01 -3.1,-1.96l-3.76,-7.86zm-16.73,-10.31a0.34,0.34 0 0 0 0.32,0.47l12.85,-0.36a0.34,0.34 0 0 0 0.3,-0.48l-6.84,-14.7a0.34,0.34 0 0 0 -0.62,0.02l-6.01,15.05z" fill="#fff"/>
|
||||
<rect id="svg_6" rx="2.17" height="52.28" width="9.84" y="302.19" x="345.67" fill="#fff"/>
|
||||
<path id="svg_7" d="m303.07,338.46l-0.15,14.42q-0.01,1.55 -1.56,1.52l-5.84,-0.12q-1.79,-0.04 -1.81,-1.83c-0.24,-15.33 -0.25,-30.89 -0.27,-47.22q-0.01,-2.99 2.55,-3.06q12.47,-0.33 20.15,0.8q8.61,1.25 12.86,9.17c2.95,5.49 2.53,13.5 -1.5,18.65c-5.57,7.14 -14.88,6.62 -23.24,6.51a1.17,1.17 0 0 0 -1.19,1.16zm-0.15,-24.81l0.16,12.72a1.72,1.72 0 0 0 1.74,1.7l6.07,-0.08a10.01,7.98 -0.7 0 0 9.91,-8.1l0,-0.2a10.01,7.98 -0.7 0 0 -10.11,-7.86l-6.07,0.08a1.72,1.72 0 0 0 -1.7,1.74z" fill="#fff"/>
|
||||
<rect id="svg_8" rx="3.58" height="7.26" width="79.2" y="322.99" x="107" fill="#fff"/>
|
||||
<rect id="svg_9" rx="3.81" height="7.72" width="79.1" y="322.78" x="417.27" fill="#fff"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 4.1 KiB |
@ -46,11 +46,16 @@ $grey600: #4b5565;
|
||||
$grey700: #364152;
|
||||
$grey900: #121926;
|
||||
|
||||
$tableBackground: #f4f6f8;
|
||||
$tableBorderBottom: #f1f3f4;
|
||||
|
||||
// ==============================|| DARK THEME VARIANTS ||============================== //
|
||||
|
||||
// paper & background
|
||||
$darkBackground: #1a223f; // level 3
|
||||
$darkPaper: #111936; // level 4
|
||||
$darkDivider: rgba(227, 232, 239, 0.2);
|
||||
$darkSelectedBack : rgba(124, 77, 255, 0.15);
|
||||
|
||||
// dark 800 & 900
|
||||
$darkLevel1: #29314f; // level 1
|
||||
@ -154,4 +159,9 @@ $darkTextSecondary: #8492c4;
|
||||
darkSecondaryDark: $darkSecondaryDark;
|
||||
darkSecondary200: $darkSecondary200;
|
||||
darkSecondary800: $darkSecondary800;
|
||||
|
||||
darkDivider: $darkDivider;
|
||||
darkSelectedBack: $darkSelectedBack;
|
||||
tableBackground: $tableBackground;
|
||||
tableBorderBottom: $tableBorderBottom;
|
||||
}
|
||||
|
32
web/berry/src/assets/scss/fonts.scss
Normal file
32
web/berry/src/assets/scss/fonts.scss
Normal file
@ -0,0 +1,32 @@
|
||||
|
||||
/* roboto-regular */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: local('Roboto'), url('../fonts/roboto-regular.woff2') format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
|
||||
/* roboto-500 */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: local('Roboto'), url('../fonts/roboto-500.woff2') format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
|
||||
|
||||
/* roboto-700 */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: local('Roboto'), url('../fonts/roboto-700.woff2') format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
@import 'fonts.scss';
|
||||
// color variants
|
||||
@import 'themes-vars.module.scss';
|
||||
|
||||
|
@ -48,6 +48,28 @@ const useLogin = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const larkLogin = async (code, state) => {
|
||||
try {
|
||||
const res = await API.get(`/api/oauth/lark?code=${code}&state=${state}`);
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
if (message === 'bind') {
|
||||
showSuccess('绑定成功!');
|
||||
navigate('/panel');
|
||||
} else {
|
||||
dispatch({ type: LOGIN, payload: data });
|
||||
localStorage.setItem('user', JSON.stringify(data));
|
||||
showSuccess('登录成功!');
|
||||
navigate('/panel');
|
||||
}
|
||||
}
|
||||
return { success, message };
|
||||
} catch (err) {
|
||||
// 请求失败,设置错误信息
|
||||
return { success: false, message: '' };
|
||||
}
|
||||
};
|
||||
|
||||
const wechatLogin = async (code) => {
|
||||
try {
|
||||
const res = await API.get(`/api/oauth/wechat?code=${code}`);
|
||||
@ -72,7 +94,7 @@ const useLogin = () => {
|
||||
navigate('/');
|
||||
};
|
||||
|
||||
return { login, logout, githubLogin, wechatLogin };
|
||||
return { login, logout, githubLogin, wechatLogin, larkLogin };
|
||||
};
|
||||
|
||||
export default useLogin;
|
||||
|
@ -71,8 +71,8 @@ const ProfileSection = () => {
|
||||
alignItems: 'center',
|
||||
borderRadius: '27px',
|
||||
transition: 'all .2s ease-in-out',
|
||||
borderColor: theme.palette.primary.light,
|
||||
backgroundColor: theme.palette.primary.light,
|
||||
borderColor: theme.typography.menuChip.background,
|
||||
backgroundColor: theme.typography.menuChip.background,
|
||||
'&[aria-controls="menu-list-grow"], &:hover': {
|
||||
borderColor: theme.palette.primary.main,
|
||||
background: `${theme.palette.primary.main}!important`,
|
||||
|
@ -7,6 +7,7 @@ import { Avatar, Box, ButtonBase } from '@mui/material';
|
||||
// project imports
|
||||
import LogoSection from '../LogoSection';
|
||||
import ProfileSection from './ProfileSection';
|
||||
import ThemeButton from 'ui-component/ThemeButton';
|
||||
|
||||
// assets
|
||||
import { IconMenu2 } from '@tabler/icons-react';
|
||||
@ -37,9 +38,8 @@ const Header = ({ handleLeftDrawerToggle }) => {
|
||||
sx={{
|
||||
...theme.typography.commonAvatar,
|
||||
...theme.typography.mediumAvatar,
|
||||
...theme.typography.menuButton,
|
||||
transition: 'all .2s ease-in-out',
|
||||
background: theme.palette.secondary.light,
|
||||
color: theme.palette.secondary.dark,
|
||||
'&:hover': {
|
||||
background: theme.palette.secondary.dark,
|
||||
color: theme.palette.secondary.light
|
||||
@ -55,7 +55,7 @@ const Header = ({ handleLeftDrawerToggle }) => {
|
||||
|
||||
<Box sx={{ flexGrow: 1 }} />
|
||||
<Box sx={{ flexGrow: 1 }} />
|
||||
|
||||
<ThemeButton />
|
||||
<ProfileSection />
|
||||
</>
|
||||
);
|
||||
|
@ -36,7 +36,7 @@ import { useNavigate } from 'react-router-dom';
|
||||
// }));
|
||||
|
||||
const CardStyle = styled(Card)(({ theme }) => ({
|
||||
background: theme.palette.primary.light,
|
||||
background: theme.typography.menuChip.background,
|
||||
marginBottom: '22px',
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
@ -121,7 +121,6 @@ const MenuCard = () => {
|
||||
/>
|
||||
</ListItem>
|
||||
</List>
|
||||
{/* <LinearProgressWithLabel value={80} /> */}
|
||||
</CardContent>
|
||||
</CardStyle>
|
||||
);
|
||||
|
@ -39,7 +39,13 @@ const Sidebar = ({ drawerOpen, drawerToggle, window }) => {
|
||||
<MenuList />
|
||||
<MenuCard />
|
||||
<Stack direction="row" justifyContent="center" sx={{ mb: 2 }}>
|
||||
<Chip label={process.env.REACT_APP_VERSION} disabled chipcolor="secondary" size="small" sx={{ cursor: 'pointer' }} />
|
||||
<Chip
|
||||
label={process.env.REACT_APP_VERSION || '未知版本号'}
|
||||
disabled
|
||||
chipcolor="secondary"
|
||||
size="small"
|
||||
sx={{ cursor: 'pointer' }}
|
||||
/>
|
||||
</Stack>
|
||||
</PerfectScrollbar>
|
||||
</BrowserView>
|
||||
@ -48,7 +54,13 @@ const Sidebar = ({ drawerOpen, drawerToggle, window }) => {
|
||||
<MenuList />
|
||||
<MenuCard />
|
||||
<Stack direction="row" justifyContent="center" sx={{ mb: 2 }}>
|
||||
<Chip label={process.env.REACT_APP_VERSION} disabled chipcolor="secondary" size="small" sx={{ cursor: 'pointer' }} />
|
||||
<Chip
|
||||
label={process.env.REACT_APP_VERSION || '未知版本号'}
|
||||
disabled
|
||||
chipcolor="secondary"
|
||||
size="small"
|
||||
sx={{ cursor: 'pointer' }}
|
||||
/>
|
||||
</Stack>
|
||||
</Box>
|
||||
</MobileView>
|
||||
|
@ -1,10 +1,30 @@
|
||||
// material-ui
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { Box, Button, Stack } from "@mui/material";
|
||||
import LogoSection from "layout/MainLayout/LogoSection";
|
||||
import { Link } from "react-router-dom";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useState } from 'react';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Stack,
|
||||
Popper,
|
||||
IconButton,
|
||||
List,
|
||||
ListItemButton,
|
||||
Paper,
|
||||
ListItemText,
|
||||
Typography,
|
||||
Divider,
|
||||
ClickAwayListener
|
||||
} from '@mui/material';
|
||||
import LogoSection from 'layout/MainLayout/LogoSection';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useSelector } from 'react-redux';
|
||||
import ThemeButton from 'ui-component/ThemeButton';
|
||||
import ProfileSection from 'layout/MainLayout/Header/ProfileSection';
|
||||
import { IconMenu2 } from '@tabler/icons-react';
|
||||
import Transitions from 'ui-component/extended/Transitions';
|
||||
import MainCard from 'ui-component/cards/MainCard';
|
||||
import { useMediaQuery } from '@mui/material';
|
||||
|
||||
// ==============================|| MAIN NAVBAR / HEADER ||============================== //
|
||||
|
||||
@ -12,16 +32,26 @@ const Header = () => {
|
||||
const theme = useTheme();
|
||||
const { pathname } = useLocation();
|
||||
const account = useSelector((state) => state.account);
|
||||
const [open, setOpen] = useState(null);
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
|
||||
const handleOpenMenu = (event) => {
|
||||
setOpen(open ? null : event.currentTarget);
|
||||
};
|
||||
|
||||
const handleCloseMenu = () => {
|
||||
setOpen(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
sx={{
|
||||
width: 228,
|
||||
display: "flex",
|
||||
[theme.breakpoints.down("md")]: {
|
||||
width: "auto",
|
||||
},
|
||||
display: 'flex',
|
||||
[theme.breakpoints.down('md')]: {
|
||||
width: 'auto'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Box component="span" sx={{ flexGrow: 1 }}>
|
||||
@ -31,43 +61,99 @@ const Header = () => {
|
||||
|
||||
<Box sx={{ flexGrow: 1 }} />
|
||||
<Box sx={{ flexGrow: 1 }} />
|
||||
<Stack spacing={2} direction="row">
|
||||
<Button
|
||||
component={Link}
|
||||
variant="text"
|
||||
to="/"
|
||||
color={pathname === "/" ? "primary" : "inherit"}
|
||||
>
|
||||
首页
|
||||
</Button>
|
||||
<Button
|
||||
component={Link}
|
||||
variant="text"
|
||||
to="/about"
|
||||
color={pathname === "/about" ? "primary" : "inherit"}
|
||||
>
|
||||
关于
|
||||
</Button>
|
||||
{account.user ? (
|
||||
<Button
|
||||
component={Link}
|
||||
variant="contained"
|
||||
to="/panel"
|
||||
color="primary"
|
||||
>
|
||||
控制台
|
||||
</Button>
|
||||
<Stack spacing={2} direction="row" justifyContent="center" alignItems="center">
|
||||
{isMobile ? (
|
||||
<>
|
||||
<ThemeButton />
|
||||
<IconButton onClick={handleOpenMenu}>
|
||||
<IconMenu2 />
|
||||
</IconButton>
|
||||
</>
|
||||
) : (
|
||||
<Button
|
||||
component={Link}
|
||||
variant="contained"
|
||||
to="/login"
|
||||
color="primary"
|
||||
>
|
||||
登入
|
||||
</Button>
|
||||
<>
|
||||
<Button component={Link} variant="text" to="/" color={pathname === '/' ? 'primary' : 'inherit'}>
|
||||
首页
|
||||
</Button>
|
||||
<Button component={Link} variant="text" to="/about" color={pathname === '/about' ? 'primary' : 'inherit'}>
|
||||
关于
|
||||
</Button>
|
||||
<ThemeButton />
|
||||
{account.user ? (
|
||||
<>
|
||||
<Button component={Link} variant="contained" to="/panel" color="primary">
|
||||
控制台
|
||||
</Button>
|
||||
<ProfileSection />
|
||||
</>
|
||||
) : (
|
||||
<Button component={Link} variant="contained" to="/login" color="primary">
|
||||
登录
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
<Popper
|
||||
open={!!open}
|
||||
anchorEl={open}
|
||||
transition
|
||||
disablePortal
|
||||
popperOptions={{
|
||||
modifiers: [
|
||||
{
|
||||
name: 'offset',
|
||||
options: {
|
||||
offset: [0, 14]
|
||||
}
|
||||
}
|
||||
]
|
||||
}}
|
||||
style={{ width: '100vw' }}
|
||||
>
|
||||
{({ TransitionProps }) => (
|
||||
<Transitions in={open} {...TransitionProps}>
|
||||
<ClickAwayListener onClickAway={handleCloseMenu}>
|
||||
<Paper style={{ width: '100%' }}>
|
||||
<MainCard border={false} elevation={16} content={false} boxShadow shadow={theme.shadows[16]}>
|
||||
<List
|
||||
component="nav"
|
||||
sx={{
|
||||
width: '100%',
|
||||
maxWidth: '100%',
|
||||
minWidth: '100%',
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
|
||||
'& .MuiListItemButton-root': {
|
||||
mt: 0.5
|
||||
}
|
||||
}}
|
||||
onClick={handleCloseMenu}
|
||||
>
|
||||
<ListItemButton component={Link} variant="text" to="/">
|
||||
<ListItemText primary={<Typography variant="body2">首页</Typography>} />
|
||||
</ListItemButton>
|
||||
|
||||
<ListItemButton component={Link} variant="text" to="/about">
|
||||
<ListItemText primary={<Typography variant="body2">关于</Typography>} />
|
||||
</ListItemButton>
|
||||
<Divider />
|
||||
{account.user ? (
|
||||
<ListItemButton component={Link} variant="contained" to="/panel" color="primary">
|
||||
控制台
|
||||
</ListItemButton>
|
||||
) : (
|
||||
<ListItemButton component={Link} variant="contained" to="/login" color="primary">
|
||||
登录
|
||||
</ListItemButton>
|
||||
)}
|
||||
</List>
|
||||
</MainCard>
|
||||
</Paper>
|
||||
</ClickAwayListener>
|
||||
</Transitions>
|
||||
)}
|
||||
</Popper>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Outlet } from 'react-router-dom';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { AppBar, Box, CssBaseline, Toolbar } from '@mui/material';
|
||||
import { AppBar, Box, CssBaseline, Toolbar, Container } from '@mui/material';
|
||||
import Header from './Header';
|
||||
import Footer from 'ui-component/Footer';
|
||||
|
||||
@ -22,9 +22,11 @@ const MinimalLayout = () => {
|
||||
flex: 'none'
|
||||
}}
|
||||
>
|
||||
<Toolbar>
|
||||
<Header />
|
||||
</Toolbar>
|
||||
<Container>
|
||||
<Toolbar>
|
||||
<Header />
|
||||
</Toolbar>
|
||||
</Container>
|
||||
</AppBar>
|
||||
<Box sx={{ flex: '1 1 auto', overflow: 'auto' }} marginTop={'80px'}>
|
||||
<Outlet />
|
||||
|
@ -8,6 +8,7 @@ import MinimalLayout from 'layout/MinimalLayout';
|
||||
const AuthLogin = Loadable(lazy(() => import('views/Authentication/Auth/Login')));
|
||||
const AuthRegister = Loadable(lazy(() => import('views/Authentication/Auth/Register')));
|
||||
const GitHubOAuth = Loadable(lazy(() => import('views/Authentication/Auth/GitHubOAuth')));
|
||||
const LarkOAuth = Loadable(lazy(() => import('views/Authentication/Auth/LarkOAuth')));
|
||||
const ForgetPassword = Loadable(lazy(() => import('views/Authentication/Auth/ForgetPassword')));
|
||||
const ResetPassword = Loadable(lazy(() => import('views/Authentication/Auth/ResetPassword')));
|
||||
const Home = Loadable(lazy(() => import('views/Home')));
|
||||
@ -48,6 +49,10 @@ const OtherRoutes = {
|
||||
path: '/oauth/github',
|
||||
element: <GitHubOAuth />
|
||||
},
|
||||
{
|
||||
path: '/oauth/lark',
|
||||
element: <LarkOAuth />
|
||||
},
|
||||
{
|
||||
path: '/404',
|
||||
element: <NotFoundView />
|
||||
|
@ -7,3 +7,4 @@ export const SET_BORDER_RADIUS = '@customization/SET_BORDER_RADIUS';
|
||||
export const SET_SITE_INFO = '@siteInfo/SET_SITE_INFO';
|
||||
export const LOGIN = '@account/LOGIN';
|
||||
export const LOGOUT = '@account/LOGOUT';
|
||||
export const SET_THEME = '@customization/SET_THEME';
|
||||
|
@ -9,7 +9,8 @@ export const initialState = {
|
||||
defaultId: 'default',
|
||||
fontFamily: config.fontFamily,
|
||||
borderRadius: config.borderRadius,
|
||||
opened: true
|
||||
opened: true,
|
||||
theme: 'light'
|
||||
};
|
||||
|
||||
// ==============================|| CUSTOMIZATION REDUCER ||============================== //
|
||||
@ -38,6 +39,11 @@ const customizationReducer = (state = initialState, action) => {
|
||||
...state,
|
||||
borderRadius: action.borderRadius
|
||||
};
|
||||
case actionTypes.SET_THEME:
|
||||
return {
|
||||
...state,
|
||||
theme: action.theme
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
export default function componentStyleOverrides(theme) {
|
||||
const bgColor = theme.colors?.grey50;
|
||||
const bgColor = theme.mode === 'dark' ? theme.backgroundDefault : theme.colors?.grey50;
|
||||
return {
|
||||
MuiButton: {
|
||||
styleOverrides: {
|
||||
@ -12,15 +12,7 @@ export default function componentStyleOverrides(theme) {
|
||||
}
|
||||
}
|
||||
},
|
||||
MuiMenuItem: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
'&:hover': {
|
||||
backgroundColor: theme.colors?.grey100
|
||||
}
|
||||
}
|
||||
}
|
||||
}, //MuiAutocomplete-popper MuiPopover-root
|
||||
//MuiAutocomplete-popper MuiPopover-root
|
||||
MuiAutocomplete: {
|
||||
styleOverrides: {
|
||||
popper: {
|
||||
@ -226,12 +218,12 @@ export default function componentStyleOverrides(theme) {
|
||||
MuiTableCell: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
borderBottom: '1px solid rgb(241, 243, 244)',
|
||||
borderBottom: '1px solid ' + theme.tableBorderBottom,
|
||||
textAlign: 'center'
|
||||
},
|
||||
head: {
|
||||
color: theme.darkTextSecondary,
|
||||
backgroundColor: 'rgb(244, 246, 248)'
|
||||
backgroundColor: theme.headBackgroundColor
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -239,7 +231,7 @@ export default function componentStyleOverrides(theme) {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgb(244, 246, 248)'
|
||||
backgroundColor: theme.headBackgroundColor
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -247,10 +239,29 @@ export default function componentStyleOverrides(theme) {
|
||||
MuiTooltip: {
|
||||
styleOverrides: {
|
||||
tooltip: {
|
||||
color: theme.paper,
|
||||
color: theme.colors.paper,
|
||||
background: theme.colors?.grey700
|
||||
}
|
||||
}
|
||||
},
|
||||
MuiCssBaseline: {
|
||||
styleOverrides: `
|
||||
.apexcharts-title-text {
|
||||
fill: ${theme.textDark} !important
|
||||
}
|
||||
.apexcharts-text {
|
||||
fill: ${theme.textDark} !important
|
||||
}
|
||||
.apexcharts-legend-text {
|
||||
color: ${theme.textDark} !important
|
||||
}
|
||||
.apexcharts-menu {
|
||||
background: ${theme.backgroundDefault} !important
|
||||
}
|
||||
.apexcharts-gridline, .apexcharts-xaxistooltip-background, .apexcharts-yaxistooltip-background {
|
||||
stroke: ${theme.divider} !important;
|
||||
}
|
||||
`
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -15,19 +15,10 @@ import themeTypography from './typography';
|
||||
|
||||
export const theme = (customization) => {
|
||||
const color = colors;
|
||||
|
||||
const options = customization.theme === 'light' ? GetLightOption() : GetDarkOption();
|
||||
const themeOption = {
|
||||
colors: color,
|
||||
heading: color.grey900,
|
||||
paper: color.paper,
|
||||
backgroundDefault: color.paper,
|
||||
background: color.primaryLight,
|
||||
darkTextPrimary: color.grey700,
|
||||
darkTextSecondary: color.grey500,
|
||||
textDark: color.grey900,
|
||||
menuSelected: color.secondaryDark,
|
||||
menuSelectedBack: color.secondaryLight,
|
||||
divider: color.grey200,
|
||||
...options,
|
||||
customization
|
||||
};
|
||||
|
||||
@ -53,3 +44,49 @@ export const theme = (customization) => {
|
||||
};
|
||||
|
||||
export default theme;
|
||||
|
||||
function GetDarkOption() {
|
||||
const color = colors;
|
||||
return {
|
||||
mode: 'dark',
|
||||
heading: color.darkTextTitle,
|
||||
paper: color.darkLevel2,
|
||||
backgroundDefault: color.darkPaper,
|
||||
background: color.darkBackground,
|
||||
darkTextPrimary: color.darkTextPrimary,
|
||||
darkTextSecondary: color.darkTextSecondary,
|
||||
textDark: color.darkTextTitle,
|
||||
menuSelected: color.darkSecondaryMain,
|
||||
menuSelectedBack: color.darkSelectedBack,
|
||||
divider: color.darkDivider,
|
||||
borderColor: color.darkBorderColor,
|
||||
menuButton: color.darkLevel1,
|
||||
menuButtonColor: color.darkSecondaryMain,
|
||||
menuChip: color.darkLevel1,
|
||||
headBackgroundColor: color.darkBackground,
|
||||
tableBorderBottom: color.darkDivider
|
||||
};
|
||||
}
|
||||
|
||||
function GetLightOption() {
|
||||
const color = colors;
|
||||
return {
|
||||
mode: 'light',
|
||||
heading: color.grey900,
|
||||
paper: color.paper,
|
||||
backgroundDefault: color.paper,
|
||||
background: color.primaryLight,
|
||||
darkTextPrimary: color.grey700,
|
||||
darkTextSecondary: color.grey500,
|
||||
textDark: color.grey900,
|
||||
menuSelected: color.secondaryDark,
|
||||
menuSelectedBack: color.secondaryLight,
|
||||
divider: color.grey200,
|
||||
borderColor: color.grey300,
|
||||
menuButton: color.secondaryLight,
|
||||
menuButtonColor: color.secondaryDark,
|
||||
menuChip: color.primaryLight,
|
||||
headBackgroundColor: color.tableBackground,
|
||||
tableBorderBottom: color.tableBorderBottom
|
||||
};
|
||||
}
|
||||
|
@ -5,7 +5,7 @@
|
||||
|
||||
export default function themePalette(theme) {
|
||||
return {
|
||||
mode: 'light',
|
||||
mode: theme.mode,
|
||||
common: {
|
||||
black: theme.colors?.darkPaper
|
||||
},
|
||||
|
@ -132,6 +132,19 @@ export default function themeTypography(theme) {
|
||||
width: '44px',
|
||||
height: '44px',
|
||||
fontSize: '1.5rem'
|
||||
},
|
||||
menuButton: {
|
||||
color: theme.menuButtonColor,
|
||||
background: theme.menuButton
|
||||
},
|
||||
menuChip: {
|
||||
background: theme.menuChip
|
||||
},
|
||||
CardWrapper: {
|
||||
backgroundColor: theme.mode === 'dark' ? theme.colors.darkLevel2 : theme.colors.primaryDark
|
||||
},
|
||||
SubCard: {
|
||||
border: theme.mode === 'dark' ? '1px solid rgba(227, 232, 239, 0.2)' : '1px solid rgb(227, 232, 239)'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
// material-ui
|
||||
import logo from 'assets/images/logo.svg';
|
||||
import logoLight from 'assets/images/logo.svg';
|
||||
import logoDark from 'assets/images/logo-white.svg';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
|
||||
/**
|
||||
* if you want to use image instead of <svg> uncomment following.
|
||||
@ -14,6 +16,8 @@ import { useSelector } from 'react-redux';
|
||||
|
||||
const Logo = () => {
|
||||
const siteInfo = useSelector((state) => state.siteInfo);
|
||||
const theme = useTheme();
|
||||
const logo = theme.palette.mode === 'light' ? logoLight : logoDark;
|
||||
|
||||
return <img src={siteInfo.logo || logo} alt={siteInfo.system_name} height="50" />;
|
||||
};
|
||||
|
50
web/berry/src/ui-component/ThemeButton.js
Normal file
50
web/berry/src/ui-component/ThemeButton.js
Normal file
@ -0,0 +1,50 @@
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { SET_THEME } from 'store/actions';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { Avatar, Box, ButtonBase } from '@mui/material';
|
||||
import { IconSun, IconMoon } from '@tabler/icons-react';
|
||||
|
||||
export default function ThemeButton() {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const defaultTheme = useSelector((state) => state.customization.theme);
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
ml: 2,
|
||||
mr: 3,
|
||||
[theme.breakpoints.down('md')]: {
|
||||
mr: 2
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ButtonBase sx={{ borderRadius: '12px' }}>
|
||||
<Avatar
|
||||
variant="rounded"
|
||||
sx={{
|
||||
...theme.typography.commonAvatar,
|
||||
...theme.typography.mediumAvatar,
|
||||
transition: 'all .2s ease-in-out',
|
||||
borderColor: theme.typography.menuChip.background,
|
||||
backgroundColor: theme.typography.menuChip.background,
|
||||
'&[aria-controls="menu-list-grow"],&:hover': {
|
||||
background: theme.palette.secondary.dark,
|
||||
color: theme.palette.secondary.light
|
||||
}
|
||||
}}
|
||||
onClick={() => {
|
||||
let theme = defaultTheme === 'light' ? 'dark' : 'light';
|
||||
dispatch({ type: SET_THEME, theme: theme });
|
||||
localStorage.setItem('theme', theme);
|
||||
}}
|
||||
color="inherit"
|
||||
>
|
||||
{defaultTheme === 'light' ? <IconSun stroke={1.5} size="1.3rem" /> : <IconMoon stroke={1.5} size="1.3rem" />}
|
||||
</Avatar>
|
||||
</ButtonBase>
|
||||
</Box>
|
||||
);
|
||||
}
|
@ -15,7 +15,7 @@ const headerSX = {
|
||||
const MainCard = forwardRef(
|
||||
(
|
||||
{
|
||||
border = true,
|
||||
border = false,
|
||||
boxShadow,
|
||||
children,
|
||||
content = true,
|
||||
|
@ -15,8 +15,7 @@ const SubCard = forwardRef(
|
||||
<Card
|
||||
ref={ref}
|
||||
sx={{
|
||||
border: '1px solid',
|
||||
borderColor: theme.palette.primary.light,
|
||||
border: theme.typography.SubCard.border,
|
||||
':hover': {
|
||||
boxShadow: '0 2px 14px 0 rgb(32 40 45 / 8%)'
|
||||
},
|
||||
@ -36,8 +35,8 @@ const SubCard = forwardRef(
|
||||
{title && (
|
||||
<Divider
|
||||
sx={{
|
||||
opacity: 1,
|
||||
borderColor: theme.palette.primary.light
|
||||
opacity: 1
|
||||
// borderColor: theme.palette.primary.light
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
@ -62,7 +61,8 @@ SubCard.propTypes = {
|
||||
secondary: PropTypes.oneOfType([PropTypes.node, PropTypes.string, PropTypes.object]),
|
||||
sx: PropTypes.object,
|
||||
contentSX: PropTypes.object,
|
||||
title: PropTypes.oneOfType([PropTypes.node, PropTypes.string, PropTypes.object])
|
||||
title: PropTypes.oneOfType([PropTypes.node, PropTypes.string, PropTypes.object]),
|
||||
subTitle: PropTypes.oneOfType([PropTypes.node, PropTypes.string, PropTypes.object])
|
||||
};
|
||||
|
||||
SubCard.defaultProps = {
|
||||
|
@ -40,7 +40,8 @@ export function generateChartOptions(data, unit) {
|
||||
chart: {
|
||||
sparkline: {
|
||||
enabled: true
|
||||
}
|
||||
},
|
||||
background: 'transparent'
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: false
|
||||
|
@ -91,6 +91,13 @@ export async function onGitHubOAuthClicked(github_client_id, openInNewTab = fals
|
||||
}
|
||||
}
|
||||
|
||||
export async function onLarkOAuthClicked(lark_client_id) {
|
||||
const state = await getOAuthState();
|
||||
if (!state) return;
|
||||
let redirect_uri = `${window.location.origin}/oauth/lark`;
|
||||
window.open(`https://open.feishu.cn/open-apis/authen/v1/index?redirect_uri=${redirect_uri}&app_id=${lark_client_id}&state=${state}`);
|
||||
}
|
||||
|
||||
export function isAdmin() {
|
||||
let user = localStorage.getItem('user');
|
||||
if (!user) return false;
|
||||
|
94
web/berry/src/views/Authentication/Auth/LarkOAuth.js
Normal file
94
web/berry/src/views/Authentication/Auth/LarkOAuth.js
Normal 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;
|
@ -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={{
|
||||
|
@ -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
|
||||
|
@ -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)}
|
||||
|
@ -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>
|
||||
|
@ -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',
|
||||
|
@ -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"
|
||||
|
@ -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={
|
||||
|
@ -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
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user