diff --git a/web/berry/.prettierrc b/web/berry/.prettierrc new file mode 100644 index 00000000..d5fba07c --- /dev/null +++ b/web/berry/.prettierrc @@ -0,0 +1,8 @@ +{ + "bracketSpacing": true, + "printWidth": 140, + "singleQuote": true, + "trailingComma": "none", + "tabWidth": 2, + "useTabs": false +} diff --git a/web/berry/public/index.html b/web/berry/public/index.html index 6f232250..abd079e1 100644 --- a/web/berry/public/index.html +++ b/web/berry/public/index.html @@ -11,11 +11,6 @@ name="description" content="OpenAI 接口聚合管理,支持多种渠道包括 Azure,可用于二次分发管理 key,仅单可执行文件,已打包好 Docker 镜像,一键部署,开箱即用" /> - - diff --git a/web/berry/src/App.js b/web/berry/src/App.js index fc54c632..d6422a0f 100644 --- a/web/berry/src/App.js +++ b/web/berry/src/App.js @@ -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 ( diff --git a/web/berry/src/assets/fonts/roboto-500.woff2 b/web/berry/src/assets/fonts/roboto-500.woff2 new file mode 100644 index 00000000..2360b721 Binary files /dev/null and b/web/berry/src/assets/fonts/roboto-500.woff2 differ diff --git a/web/berry/src/assets/fonts/roboto-700.woff2 b/web/berry/src/assets/fonts/roboto-700.woff2 new file mode 100644 index 00000000..4aeda71b Binary files /dev/null and b/web/berry/src/assets/fonts/roboto-700.woff2 differ diff --git a/web/berry/src/assets/fonts/roboto-regular.woff2 b/web/berry/src/assets/fonts/roboto-regular.woff2 new file mode 100644 index 00000000..b65a361a Binary files /dev/null and b/web/berry/src/assets/fonts/roboto-regular.woff2 differ diff --git a/web/berry/src/assets/images/icons/lark.svg b/web/berry/src/assets/images/icons/lark.svg new file mode 100644 index 00000000..239e1bef --- /dev/null +++ b/web/berry/src/assets/images/icons/lark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/berry/src/assets/images/logo-white.svg b/web/berry/src/assets/images/logo-white.svg new file mode 100644 index 00000000..d6289b9a --- /dev/null +++ b/web/berry/src/assets/images/logo-white.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/web/berry/src/assets/scss/_themes-vars.module.scss b/web/berry/src/assets/scss/_themes-vars.module.scss index a470b033..661bb6c6 100644 --- a/web/berry/src/assets/scss/_themes-vars.module.scss +++ b/web/berry/src/assets/scss/_themes-vars.module.scss @@ -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; } diff --git a/web/berry/src/assets/scss/fonts.scss b/web/berry/src/assets/scss/fonts.scss new file mode 100644 index 00000000..c792aab2 --- /dev/null +++ b/web/berry/src/assets/scss/fonts.scss @@ -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; +} + \ No newline at end of file diff --git a/web/berry/src/assets/scss/style.scss b/web/berry/src/assets/scss/style.scss index 17d566e6..5d2d8975 100644 --- a/web/berry/src/assets/scss/style.scss +++ b/web/berry/src/assets/scss/style.scss @@ -1,3 +1,4 @@ +@import 'fonts.scss'; // color variants @import 'themes-vars.module.scss'; diff --git a/web/berry/src/hooks/useLogin.js b/web/berry/src/hooks/useLogin.js index 53626577..39d8b407 100644 --- a/web/berry/src/hooks/useLogin.js +++ b/web/berry/src/hooks/useLogin.js @@ -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; diff --git a/web/berry/src/layout/MainLayout/Header/ProfileSection/index.js b/web/berry/src/layout/MainLayout/Header/ProfileSection/index.js index 3e351254..e1392dc0 100644 --- a/web/berry/src/layout/MainLayout/Header/ProfileSection/index.js +++ b/web/berry/src/layout/MainLayout/Header/ProfileSection/index.js @@ -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`, diff --git a/web/berry/src/layout/MainLayout/Header/index.js b/web/berry/src/layout/MainLayout/Header/index.js index 51d40c75..8fd9c950 100644 --- a/web/berry/src/layout/MainLayout/Header/index.js +++ b/web/berry/src/layout/MainLayout/Header/index.js @@ -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 }) => { - + ); diff --git a/web/berry/src/layout/MainLayout/Sidebar/MenuCard/index.js b/web/berry/src/layout/MainLayout/Sidebar/MenuCard/index.js index 16b13231..dadd3eca 100644 --- a/web/berry/src/layout/MainLayout/Sidebar/MenuCard/index.js +++ b/web/berry/src/layout/MainLayout/Sidebar/MenuCard/index.js @@ -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 = () => { /> - {/* */} ); diff --git a/web/berry/src/layout/MainLayout/Sidebar/index.js b/web/berry/src/layout/MainLayout/Sidebar/index.js index e3c6d12d..10652ba6 100644 --- a/web/berry/src/layout/MainLayout/Sidebar/index.js +++ b/web/berry/src/layout/MainLayout/Sidebar/index.js @@ -39,7 +39,13 @@ const Sidebar = ({ drawerOpen, drawerToggle, window }) => { - + @@ -48,7 +54,13 @@ const Sidebar = ({ drawerOpen, drawerToggle, window }) => { - + diff --git a/web/berry/src/layout/MinimalLayout/Header/index.js b/web/berry/src/layout/MinimalLayout/Header/index.js index 4f61da60..feaeb603 100644 --- a/web/berry/src/layout/MinimalLayout/Header/index.js +++ b/web/berry/src/layout/MinimalLayout/Header/index.js @@ -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 ( <> @@ -31,43 +61,99 @@ const Header = () => { - - - - {account.user ? ( - + + {isMobile ? ( + <> + + + + + ) : ( - + <> + + + + {account.user ? ( + <> + + + + ) : ( + + )} + )} + + + {({ TransitionProps }) => ( + + + + + + + 首页} /> + + + + 关于} /> + + + {account.user ? ( + + 控制台 + + ) : ( + + 登录 + + )} + + + + + + )} + ); }; diff --git a/web/berry/src/layout/MinimalLayout/index.js b/web/berry/src/layout/MinimalLayout/index.js index c2919c6d..81047fd1 100644 --- a/web/berry/src/layout/MinimalLayout/index.js +++ b/web/berry/src/layout/MinimalLayout/index.js @@ -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' }} > - -
- + + +
+ + diff --git a/web/berry/src/routes/OtherRoutes.js b/web/berry/src/routes/OtherRoutes.js index 085c4add..58c0b660 100644 --- a/web/berry/src/routes/OtherRoutes.js +++ b/web/berry/src/routes/OtherRoutes.js @@ -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: }, + { + path: '/oauth/lark', + element: + }, { path: '/404', element: diff --git a/web/berry/src/store/actions.js b/web/berry/src/store/actions.js index 221e8578..f1592d17 100644 --- a/web/berry/src/store/actions.js +++ b/web/berry/src/store/actions.js @@ -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'; diff --git a/web/berry/src/store/customizationReducer.js b/web/berry/src/store/customizationReducer.js index bd8e5f00..0c104025 100644 --- a/web/berry/src/store/customizationReducer.js +++ b/web/berry/src/store/customizationReducer.js @@ -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; } diff --git a/web/berry/src/themes/compStyleOverride.js b/web/berry/src/themes/compStyleOverride.js index b6e87e01..67a3dd14 100644 --- a/web/berry/src/themes/compStyleOverride.js +++ b/web/berry/src/themes/compStyleOverride.js @@ -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; + } + ` } }; } diff --git a/web/berry/src/themes/index.js b/web/berry/src/themes/index.js index 6e694aa6..addd61f7 100644 --- a/web/berry/src/themes/index.js +++ b/web/berry/src/themes/index.js @@ -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 + }; +} diff --git a/web/berry/src/themes/palette.js b/web/berry/src/themes/palette.js index 09768555..70c78782 100644 --- a/web/berry/src/themes/palette.js +++ b/web/berry/src/themes/palette.js @@ -5,7 +5,7 @@ export default function themePalette(theme) { return { - mode: 'light', + mode: theme.mode, common: { black: theme.colors?.darkPaper }, diff --git a/web/berry/src/themes/typography.js b/web/berry/src/themes/typography.js index 24bfabb9..f20d87a5 100644 --- a/web/berry/src/themes/typography.js +++ b/web/berry/src/themes/typography.js @@ -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)' } }; } diff --git a/web/berry/src/ui-component/Logo.js b/web/berry/src/ui-component/Logo.js index a34fe895..52e61f4f 100644 --- a/web/berry/src/ui-component/Logo.js +++ b/web/berry/src/ui-component/Logo.js @@ -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 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 {siteInfo.system_name}; }; diff --git a/web/berry/src/ui-component/ThemeButton.js b/web/berry/src/ui-component/ThemeButton.js new file mode 100644 index 00000000..c907c646 --- /dev/null +++ b/web/berry/src/ui-component/ThemeButton.js @@ -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 ( + + + { + let theme = defaultTheme === 'light' ? 'dark' : 'light'; + dispatch({ type: SET_THEME, theme: theme }); + localStorage.setItem('theme', theme); + }} + color="inherit" + > + {defaultTheme === 'light' ? : } + + + + ); +} diff --git a/web/berry/src/ui-component/cards/MainCard.js b/web/berry/src/ui-component/cards/MainCard.js index 8735282c..32353027 100644 --- a/web/berry/src/ui-component/cards/MainCard.js +++ b/web/berry/src/ui-component/cards/MainCard.js @@ -15,7 +15,7 @@ const headerSX = { const MainCard = forwardRef( ( { - border = true, + border = false, boxShadow, children, content = true, diff --git a/web/berry/src/ui-component/cards/SubCard.js b/web/berry/src/ui-component/cards/SubCard.js index 05f9abb7..a63819a8 100644 --- a/web/berry/src/ui-component/cards/SubCard.js +++ b/web/berry/src/ui-component/cards/SubCard.js @@ -15,8 +15,7 @@ const SubCard = forwardRef( )} @@ -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 = { diff --git a/web/berry/src/utils/chart.js b/web/berry/src/utils/chart.js index 4633fe37..8cf6d847 100644 --- a/web/berry/src/utils/chart.js +++ b/web/berry/src/utils/chart.js @@ -40,7 +40,8 @@ export function generateChartOptions(data, unit) { chart: { sparkline: { enabled: true - } + }, + background: 'transparent' }, dataLabels: { enabled: false diff --git a/web/berry/src/utils/common.js b/web/berry/src/utils/common.js index d8dabac3..947df3bf 100644 --- a/web/berry/src/utils/common.js +++ b/web/berry/src/utils/common.js @@ -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; diff --git a/web/berry/src/views/Authentication/Auth/LarkOAuth.js b/web/berry/src/views/Authentication/Auth/LarkOAuth.js new file mode 100644 index 00000000..88ced5d8 --- /dev/null +++ b/web/berry/src/views/Authentication/Auth/LarkOAuth.js @@ -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 ( + + + + + + + + + + + + + + + + + + 飞书 登录 + + + + + + + + + {prompt} + + + + + + + + + + ); +}; + +export default LarkOAuth; diff --git a/web/berry/src/views/Authentication/AuthForms/AuthLogin.js b/web/berry/src/views/Authentication/AuthForms/AuthLogin.js index 9420b098..bc7a35c0 100644 --- a/web/berry/src/views/Authentication/AuthForms/AuthLogin.js +++ b/web/berry/src/views/Authentication/AuthForms/AuthLogin.js @@ -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 }) => { )} + {siteInfo.lark_client_id && ( + + + + + + )} ({ - backgroundColor: theme.palette.primary.light + backgroundColor: theme.palette.background.default })); // eslint-disable-next-line diff --git a/web/berry/src/views/Channel/component/EditModal.js b/web/berry/src/views/Channel/component/EditModal.js index cbf411b9..03b4df57 100644 --- a/web/berry/src/views/Channel/component/EditModal.js +++ b/web/berry/src/views/Channel/component/EditModal.js @@ -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 = ; const checkedIcon = ; @@ -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 ( - - {option.text} - - ); - })} + {Object.values(CHANNEL_OPTIONS) + .sort((a, b) => { + return a.text.localeCompare(b.text); + }) + .map((option) => { + return ( + + {option.text} + + ); + })} {touched.type && errors.type ? ( @@ -553,7 +558,12 @@ const EditModal = ({ open, channelId, onCancel, onOk }) => { }} renderOption={(props, option, { selected }) => (
  • - + {option.id}
  • )} @@ -599,20 +609,38 @@ const EditModal = ({ open, channelId, onCancel, onOk }) => { error={Boolean(touched.key && errors.key)} sx={{ ...theme.typography.otherInput }} > - - {inputLabel.key} - - + {!batchAdd ? ( + <> + + {inputLabel.key} + + + + ) : ( + + )} + {touched.key && errors.key ? ( {errors.key} @@ -624,6 +652,19 @@ const EditModal = ({ open, channelId, onCancel, onOk }) => { )} + {channelId === 0 && ( + + setBatchAdd(e.target.checked)} + /> + 批量添加 + + )} { - 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} /> - - {renderNumber(item.used_quota)} - + {renderNumber(item.used_quota)} - - 优先级 - setPriority(e.target.value)} - sx={{ textAlign: "center" }} - endAdornment={ - - - - - - } - /> - + diff --git a/web/berry/src/views/Dashboard/component/StatisticalLineChartCard.js b/web/berry/src/views/Dashboard/component/StatisticalLineChartCard.js index 9daa9519..e6b46e25 100644 --- a/web/berry/src/views/Dashboard/component/StatisticalLineChartCard.js +++ b/web/berry/src/views/Dashboard/component/StatisticalLineChartCard.js @@ -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', diff --git a/web/berry/src/views/Profile/index.js b/web/berry/src/views/Profile/index.js index e0683228..483e3141 100644 --- a/web/berry/src/views/Profile/index.js +++ b/web/berry/src/views/Profile/index.js @@ -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() { + @@ -205,6 +210,13 @@ export default function Profile() { )} + {status.lark_client_id && !inputs.lark_id && ( + + + + )} + +
    + { + + 用以推送报警信息, + + 点击此处 + + 了解 Message Pusher + + } + > + + + + Message Pusher 推送地址 + + + + + + Message Pusher 访问凭证 + + + + + + + + ; +const checkedIcon = ; +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 ( - + - {tokenId ? "编辑令牌" : "新建令牌"} + {tokenId ? '编辑令牌' : '新建令牌'} - - 注意,令牌的额度仅用于限制令牌本身的最大额度使用量,实际的使用受到账户的剩余额度限制。 - - - {({ - errors, - handleBlur, - handleChange, - handleSubmit, - touched, - values, - setFieldError, - setFieldValue, - isSubmitting, - }) => ( + 注意,令牌的额度仅用于限制令牌本身的最大额度使用量,实际的使用受到账户的剩余额度限制。 + + {({ errors, handleBlur, handleChange, handleSubmit, touched, values, setFieldError, setFieldValue, isSubmitting }) => (
    - + 名称 { 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 }) => { )} + + { + const event = { + target: { + name: 'models', + value: value + } + }; + handleChange(event); + }} + onBlur={handleBlur} + // filterSelectedOptions + disableCloseOnSelect + renderInput={(params) => } + 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 }) => ( +
  • + + {option} +
  • + )} + /> + {errors.models ? ( + + {errors.models} + + ) : ( + 请选择允许使用的模型,留空则不进行限制 + )} +
    + + IP 限制 + + {touched.subnet && errors.subnet ? ( + + {errors.subnet} + + ) : ( + + 请输入允许访问的网段,例如:192.168.0.0/24,请使用英文逗号分隔多个网段 + + )} + {values.expired_time !== -1 && ( - - + + { 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'] + } }} /> {errors.expired_time && ( - + {errors.expired_time} )} @@ -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); } }} - />{" "} + />{' '} 永不过期 - - - 额度 - + + 额度 - {renderQuotaWithPrompt(values.remain_quota)} - - } + endAdornment={{renderQuotaWithPrompt(values.remain_quota)}} 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 && ( - + {errors.remain_quota} )} @@ -243,19 +291,13 @@ const EditModal = ({ open, tokenId, onCancel, onOk }) => { { - setFieldValue("unlimited_quota", !values.unlimited_quota); + setFieldValue('unlimited_quota', !values.unlimited_quota); }} - />{" "} + />{' '} 无限额度 - @@ -273,5 +315,5 @@ EditModal.propTypes = { open: PropTypes.bool, tokenId: PropTypes.number, onCancel: PropTypes.func, - onOk: PropTypes.func, + onOk: PropTypes.func };