diff --git a/controller/model.go b/controller/model.go
index 6cb530db..c12ccf34 100644
--- a/controller/model.go
+++ b/controller/model.go
@@ -436,7 +436,7 @@ func init() {
Id: "PaLM-2",
Object: "model",
Created: 1677649963,
- OwnedBy: "google",
+ OwnedBy: "google palm",
Permission: permission,
Root: "PaLM-2",
Parent: nil,
@@ -445,7 +445,7 @@ func init() {
Id: "gemini-pro",
Object: "model",
Created: 1677649963,
- OwnedBy: "google",
+ OwnedBy: "google gemini",
Permission: permission,
Root: "gemini-pro",
Parent: nil,
@@ -454,7 +454,7 @@ func init() {
Id: "gemini-pro-vision",
Object: "model",
Created: 1677649963,
- OwnedBy: "google",
+ OwnedBy: "google gemini",
Permission: permission,
Root: "gemini-pro-vision",
Parent: nil,
diff --git a/controller/user.go b/controller/user.go
index 8fd10b82..174300ed 100644
--- a/controller/user.go
+++ b/controller/user.go
@@ -7,6 +7,7 @@ import (
"one-api/common"
"one-api/model"
"strconv"
+ "time"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
@@ -248,6 +249,29 @@ func GetUser(c *gin.Context) {
return
}
+func GetUserDashboard(c *gin.Context) {
+ id := c.GetInt("id")
+ now := time.Now()
+ startOfDay := now.Truncate(24*time.Hour).AddDate(0, 0, -6).Unix()
+ endOfDay := now.Truncate(24 * time.Hour).Add(24*time.Hour - time.Second).Unix()
+
+ dashboards, err := model.SearchLogsByDayAndModel(id, int(startOfDay), int(endOfDay))
+ if err != nil {
+ c.JSON(http.StatusOK, gin.H{
+ "success": false,
+ "message": "无法获取统计信息",
+ "data": nil,
+ })
+ return
+ }
+ c.JSON(http.StatusOK, gin.H{
+ "success": true,
+ "message": "",
+ "data": dashboards,
+ })
+ return
+}
+
func GenerateAccessToken(c *gin.Context) {
id := c.GetInt("id")
user, err := model.GetUserById(id, true)
diff --git a/model/log.go b/model/log.go
index 3d3ffae3..aa4be60d 100644
--- a/model/log.go
+++ b/model/log.go
@@ -3,8 +3,9 @@ package model
import (
"context"
"fmt"
- "gorm.io/gorm"
"one-api/common"
+
+ "gorm.io/gorm"
)
type Log struct {
@@ -182,3 +183,42 @@ func DeleteOldLog(targetTimestamp int64) (int64, error) {
result := DB.Where("created_at < ?", targetTimestamp).Delete(&Log{})
return result.RowsAffected, result.Error
}
+
+type LogStatistic struct {
+ Day string `gorm:"column:day"`
+ ModelName string `gorm:"column:model_name"`
+ RequestCount int `gorm:"column:request_count"`
+ Quota int `gorm:"column:quota"`
+ PromptTokens int `gorm:"column:prompt_tokens"`
+ CompletionTokens int `gorm:"column:completion_tokens"`
+}
+
+func SearchLogsByDayAndModel(userId, start, end int) (LogStatistics []*LogStatistic, err error) {
+ groupSelect := "DATE_FORMAT(FROM_UNIXTIME(created_at), '%Y-%m-%d') as day"
+
+ if common.UsingPostgreSQL {
+ groupSelect = "TO_CHAR(date_trunc('day', to_timestamp(created_at)), 'YYYY-MM-DD') as day"
+ }
+
+ if common.UsingSQLite {
+ groupSelect = "strftime('%Y-%m-%d', datetime(created_at, 'unixepoch')) as day"
+ }
+
+ err = DB.Raw(`
+ SELECT `+groupSelect+`,
+ model_name, count(1) as request_count,
+ sum(quota) as quota,
+ sum(prompt_tokens) as prompt_tokens,
+ sum(completion_tokens) as completion_tokens
+ FROM logs
+ WHERE type=2
+ AND userId= ?
+ AND created_at BETWEEN ? AND ?
+ GROUP BY day, model_name
+ ORDER BY day, model_name
+ `, userId, start, end).Scan(&LogStatistics).Error
+
+ fmt.Println(userId, start, end)
+
+ return LogStatistics, err
+}
diff --git a/router/api-router.go b/router/api-router.go
index da3f9e61..162675ce 100644
--- a/router/api-router.go
+++ b/router/api-router.go
@@ -35,6 +35,7 @@ func SetApiRouter(router *gin.Engine) {
selfRoute := userRoute.Group("/")
selfRoute.Use(middleware.UserAuth())
{
+ selfRoute.GET("/dashboard", controller.GetUserDashboard)
selfRoute.GET("/self", controller.GetSelf)
selfRoute.PUT("/self", controller.UpdateSelf)
selfRoute.DELETE("/self", controller.DeleteSelf)
diff --git a/web/README.md b/web/README.md
index ca73b298..8e6827c9 100644
--- a/web/README.md
+++ b/web/README.md
@@ -1,17 +1,36 @@
# One API 的前端界面
+
> 每个文件夹代表一个主题,欢迎提交你的主题
## 提交新的主题
+
> 欢迎在页面底部保留你和 One API 的版权信息以及指向链接
+
1. 在 `web` 文件夹下新建一个文件夹,文件夹名为主题名。
2. 把你的主题文件放到这个文件夹下。
3. 修改 `package.json` 文件,把 `build` 命令改为:`"build": "react-scripts build && mv -f build ../build/default"`,其中 `default` 为你的主题名。
## 主题列表
+
### 主题:default
+
默认主题,由 [JustSong](https://github.com/songquanpeng) 开发。
预览:
|||
|:---:|:---:|
+### 主题:berry
+
+由 [MartialBE](https://github.com/MartialBE) 开发。
+
+预览:
+|||
+|:---:|:---:|
+|||
+|||
+|||
+
+#### 开发说明
+
+请查看 [web/berry/README.md](https://github.com/songquanpeng/one-api/tree/main/web/berry/README.md)
diff --git a/web/THEMES b/web/THEMES
index 331d858c..b6597eeb 100644
--- a/web/THEMES
+++ b/web/THEMES
@@ -1 +1,2 @@
-default
\ No newline at end of file
+default
+berry
\ No newline at end of file
diff --git a/web/berry/.gitignore b/web/berry/.gitignore
new file mode 100644
index 00000000..2b5bba76
--- /dev/null
+++ b/web/berry/.gitignore
@@ -0,0 +1,26 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+
+# testing
+/coverage
+
+# production
+/build
+
+# misc
+.DS_Store
+.env.local
+.env.development.local
+.env.test.local
+.env.production.local
+
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+.idea
+package-lock.json
+yarn.lock
\ No newline at end of file
diff --git a/web/berry/README.md b/web/berry/README.md
new file mode 100644
index 00000000..170feedc
--- /dev/null
+++ b/web/berry/README.md
@@ -0,0 +1,61 @@
+# One API 前端界面
+
+这个项目是 One API 的前端界面,它基于 [Berry Free React Admin Template](https://github.com/codedthemes/berry-free-react-admin-template) 进行开发。
+
+## 使用的开源项目
+
+使用了以下开源项目作为我们项目的一部分:
+
+- [Berry Free React Admin Template](https://github.com/codedthemes/berry-free-react-admin-template)
+- [minimal-ui-kit](minimal-ui-kit)
+
+## 开发说明
+
+当添加新的渠道时,需要修改以下地方:
+
+1. `web/berry/src/constants/ChannelConstants.js`
+
+在该文件中的 `CHANNEL_OPTIONS` 添加新的渠道
+
+```js
+export const CHANNEL_OPTIONS = {
+ //key 为渠道ID
+ 1: {
+ key: 1, // 渠道ID
+ text: "OpenAI", // 渠道名称
+ value: 1, // 渠道ID
+ color: "primary", // 渠道列表显示的颜色
+ },
+};
+```
+
+2. `web/berry/src/views/Channel/type/Config.js`
+
+在该文件中的`typeConfig`添加新的渠道配置, 如果无需配置,可以不添加
+
+```js
+const typeConfig = {
+ // key 为渠道ID
+ 3: {
+ inputLabel: {
+ // 输入框名称 配置
+ // 对应的字段名称
+ base_url: "AZURE_OPENAI_ENDPOINT",
+ other: "默认 API 版本",
+ },
+ prompt: {
+ // 输入框提示 配置
+ // 对应的字段名称
+ base_url: "请填写AZURE_OPENAI_ENDPOINT",
+
+ // 注意:通过判断 `other` 是否有值来判断是否需要显示 `other` 输入框, 默认是没有值的
+ other: "请输入默认API版本,例如:2023-06-01-preview",
+ },
+ modelGroup: "openai", // 模型组名称,这个值是给 填入渠道支持模型 按钮使用的。 填入渠道支持模型 按钮会根据这个值来获取模型组,如果填写默认是 openai
+ },
+};
+```
+
+## 许可证
+
+本项目中使用的代码遵循 MIT 许可证。
diff --git a/web/berry/jsconfig.json b/web/berry/jsconfig.json
new file mode 100644
index 00000000..35332c70
--- /dev/null
+++ b/web/berry/jsconfig.json
@@ -0,0 +1,9 @@
+{
+ "compilerOptions": {
+ "target": "esnext",
+ "module": "commonjs",
+ "baseUrl": "src"
+ },
+ "include": ["src/**/*"],
+ "exclude": ["node_modules"]
+}
diff --git a/web/berry/package.json b/web/berry/package.json
new file mode 100644
index 00000000..f428fd9c
--- /dev/null
+++ b/web/berry/package.json
@@ -0,0 +1,84 @@
+{
+ "name": "one_api_web",
+ "version": "1.0.0",
+ "proxy": "http://127.0.0.1:3000",
+ "private": true,
+ "homepage": "",
+ "dependencies": {
+ "@emotion/cache": "^11.9.3",
+ "@emotion/react": "^11.9.3",
+ "@emotion/styled": "^11.9.3",
+ "@mui/icons-material": "^5.8.4",
+ "@mui/lab": "^5.0.0-alpha.88",
+ "@mui/material": "^5.8.6",
+ "@mui/system": "^5.8.6",
+ "@mui/utils": "^5.8.6",
+ "@mui/x-date-pickers": "^6.18.5",
+ "@tabler/icons-react": "^2.44.0",
+ "apexcharts": "^3.35.3",
+ "axios": "^0.27.2",
+ "dayjs": "^1.11.10",
+ "formik": "^2.2.9",
+ "framer-motion": "^6.3.16",
+ "history": "^5.3.0",
+ "marked": "^4.1.1",
+ "material-ui-popup-state": "^4.0.1",
+ "notistack": "^3.0.1",
+ "prop-types": "^15.8.1",
+ "react": "^18.2.0",
+ "react-apexcharts": "^1.4.0",
+ "react-device-detect": "^2.2.2",
+ "react-dom": "^18.2.0",
+ "react-perfect-scrollbar": "^1.5.8",
+ "react-redux": "^8.0.2",
+ "react-router": "6.3.0",
+ "react-router-dom": "6.3.0",
+ "react-scripts": "^5.0.1",
+ "react-turnstile": "^1.1.2",
+ "redux": "^4.2.0",
+ "yup": "^0.32.11"
+ },
+ "scripts": {
+ "start": "react-scripts start",
+ "build": "react-scripts build && mv -f build ../build/berry",
+ "test": "react-scripts test",
+ "eject": "react-scripts eject"
+ },
+ "eslintConfig": {
+ "extends": [
+ "react-app"
+ ]
+ },
+ "babel": {
+ "presets": [
+ "@babel/preset-react"
+ ]
+ },
+ "browserslist": {
+ "production": [
+ "defaults",
+ "not IE 11"
+ ],
+ "development": [
+ "last 1 chrome version",
+ "last 1 firefox version",
+ "last 1 safari version"
+ ]
+ },
+ "devDependencies": {
+ "@babel/core": "^7.21.4",
+ "@babel/eslint-parser": "^7.21.3",
+ "eslint": "^8.38.0",
+ "eslint-config-prettier": "^8.8.0",
+ "eslint-config-react-app": "^7.0.1",
+ "eslint-plugin-flowtype": "^8.0.3",
+ "eslint-plugin-import": "^2.27.5",
+ "eslint-plugin-jsx-a11y": "^6.7.1",
+ "eslint-plugin-prettier": "^4.2.1",
+ "eslint-plugin-react": "^7.32.2",
+ "eslint-plugin-react-hooks": "^4.6.0",
+ "immutable": "^4.3.0",
+ "prettier": "^2.8.7",
+ "sass": "^1.53.0"
+ }
+}
diff --git a/web/berry/public/favicon.ico b/web/berry/public/favicon.ico
new file mode 100644
index 00000000..fbcfb14a
Binary files /dev/null and b/web/berry/public/favicon.ico differ
diff --git a/web/berry/public/index.html b/web/berry/public/index.html
new file mode 100644
index 00000000..6f232250
--- /dev/null
+++ b/web/berry/public/index.html
@@ -0,0 +1,26 @@
+
+
+
+ One API
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/berry/src/App.js b/web/berry/src/App.js
new file mode 100644
index 00000000..fc54c632
--- /dev/null
+++ b/web/berry/src/App.js
@@ -0,0 +1,43 @@
+import { useSelector } from 'react-redux';
+
+import { ThemeProvider } from '@mui/material/styles';
+import { CssBaseline, StyledEngineProvider } from '@mui/material';
+
+// routing
+import Routes from 'routes';
+
+// defaultTheme
+import themes from 'themes';
+
+// project imports
+import NavigationScroll from 'layout/NavigationScroll';
+
+// auth
+import UserProvider from 'contexts/UserContext';
+import StatusProvider from 'contexts/StatusContext';
+import { SnackbarProvider } from 'notistack';
+
+// ==============================|| APP ||============================== //
+
+const App = () => {
+ const customization = useSelector((state) => state.customization);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default App;
diff --git a/web/berry/src/assets/images/404.svg b/web/berry/src/assets/images/404.svg
new file mode 100644
index 00000000..352a14ad
--- /dev/null
+++ b/web/berry/src/assets/images/404.svg
@@ -0,0 +1,40 @@
+
\ No newline at end of file
diff --git a/web/berry/src/assets/images/auth/auth-blue-card.svg b/web/berry/src/assets/images/auth/auth-blue-card.svg
new file mode 100644
index 00000000..6c9fe3e7
--- /dev/null
+++ b/web/berry/src/assets/images/auth/auth-blue-card.svg
@@ -0,0 +1,65 @@
+
diff --git a/web/berry/src/assets/images/auth/auth-pattern-dark.svg b/web/berry/src/assets/images/auth/auth-pattern-dark.svg
new file mode 100644
index 00000000..aa0e4ab2
--- /dev/null
+++ b/web/berry/src/assets/images/auth/auth-pattern-dark.svg
@@ -0,0 +1,39 @@
+
diff --git a/web/berry/src/assets/images/auth/auth-pattern.svg b/web/berry/src/assets/images/auth/auth-pattern.svg
new file mode 100644
index 00000000..b7ac8e27
--- /dev/null
+++ b/web/berry/src/assets/images/auth/auth-pattern.svg
@@ -0,0 +1,39 @@
+
diff --git a/web/berry/src/assets/images/auth/auth-purple-card.svg b/web/berry/src/assets/images/auth/auth-purple-card.svg
new file mode 100644
index 00000000..c724e0a3
--- /dev/null
+++ b/web/berry/src/assets/images/auth/auth-purple-card.svg
@@ -0,0 +1,69 @@
+
diff --git a/web/berry/src/assets/images/auth/auth-signup-blue-card.svg b/web/berry/src/assets/images/auth/auth-signup-blue-card.svg
new file mode 100644
index 00000000..ebb8e85f
--- /dev/null
+++ b/web/berry/src/assets/images/auth/auth-signup-blue-card.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/web/berry/src/assets/images/auth/auth-signup-white-card.svg b/web/berry/src/assets/images/auth/auth-signup-white-card.svg
new file mode 100644
index 00000000..56b97e20
--- /dev/null
+++ b/web/berry/src/assets/images/auth/auth-signup-white-card.svg
@@ -0,0 +1,40 @@
+
diff --git a/web/berry/src/assets/images/icons/earning.svg b/web/berry/src/assets/images/icons/earning.svg
new file mode 100644
index 00000000..e877b599
--- /dev/null
+++ b/web/berry/src/assets/images/icons/earning.svg
@@ -0,0 +1,5 @@
+
diff --git a/web/berry/src/assets/images/icons/github.svg b/web/berry/src/assets/images/icons/github.svg
new file mode 100644
index 00000000..e5b1b82a
--- /dev/null
+++ b/web/berry/src/assets/images/icons/github.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/web/berry/src/assets/images/icons/shape-avatar.svg b/web/berry/src/assets/images/icons/shape-avatar.svg
new file mode 100644
index 00000000..38aac7e2
--- /dev/null
+++ b/web/berry/src/assets/images/icons/shape-avatar.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/web/berry/src/assets/images/icons/social-google.svg b/web/berry/src/assets/images/icons/social-google.svg
new file mode 100644
index 00000000..2231ce98
--- /dev/null
+++ b/web/berry/src/assets/images/icons/social-google.svg
@@ -0,0 +1,6 @@
+
diff --git a/web/berry/src/assets/images/icons/wechat.svg b/web/berry/src/assets/images/icons/wechat.svg
new file mode 100644
index 00000000..a0b2e36c
--- /dev/null
+++ b/web/berry/src/assets/images/icons/wechat.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/web/berry/src/assets/images/invite/cover.jpg b/web/berry/src/assets/images/invite/cover.jpg
new file mode 100644
index 00000000..93be1a40
Binary files /dev/null and b/web/berry/src/assets/images/invite/cover.jpg differ
diff --git a/web/berry/src/assets/images/invite/cwok_casual_19.webp b/web/berry/src/assets/images/invite/cwok_casual_19.webp
new file mode 100644
index 00000000..1cf2c376
Binary files /dev/null and b/web/berry/src/assets/images/invite/cwok_casual_19.webp differ
diff --git a/web/berry/src/assets/images/logo-2.svg b/web/berry/src/assets/images/logo-2.svg
new file mode 100644
index 00000000..2e674a7e
--- /dev/null
+++ b/web/berry/src/assets/images/logo-2.svg
@@ -0,0 +1,15 @@
+
\ No newline at end of file
diff --git a/web/berry/src/assets/images/logo.svg b/web/berry/src/assets/images/logo.svg
new file mode 100644
index 00000000..348c7e5a
--- /dev/null
+++ b/web/berry/src/assets/images/logo.svg
@@ -0,0 +1,13 @@
+
\ No newline at end of file
diff --git a/web/berry/src/assets/images/users/user-round.svg b/web/berry/src/assets/images/users/user-round.svg
new file mode 100644
index 00000000..eaef7ed9
--- /dev/null
+++ b/web/berry/src/assets/images/users/user-round.svg
@@ -0,0 +1 @@
+
\ 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
new file mode 100644
index 00000000..a470b033
--- /dev/null
+++ b/web/berry/src/assets/scss/_themes-vars.module.scss
@@ -0,0 +1,157 @@
+// paper & background
+$paper: #ffffff;
+
+// primary
+$primaryLight: #eef2f6;
+$primaryMain: #2196f3;
+$primaryDark: #1e88e5;
+$primary200: #90caf9;
+$primary800: #1565c0;
+
+// secondary
+$secondaryLight: #ede7f6;
+$secondaryMain: #673ab7;
+$secondaryDark: #5e35b1;
+$secondary200: #b39ddb;
+$secondary800: #4527a0;
+
+// success Colors
+$successLight: #b9f6ca;
+$success200: #69f0ae;
+$successMain: #00e676;
+$successDark: #00c853;
+
+// error
+$errorLight: #ef9a9a;
+$errorMain: #f44336;
+$errorDark: #c62828;
+
+// orange
+$orangeLight: #fbe9e7;
+$orangeMain: #ffab91;
+$orangeDark: #d84315;
+
+// warning
+$warningLight: #fff8e1;
+$warningMain: #ffe57f;
+$warningDark: #ffc107;
+
+// grey
+$grey50: #f8fafc;
+$grey100: #eef2f6;
+$grey200: #e3e8ef;
+$grey300: #cdd5df;
+$grey500: #697586;
+$grey600: #4b5565;
+$grey700: #364152;
+$grey900: #121926;
+
+// ==============================|| DARK THEME VARIANTS ||============================== //
+
+// paper & background
+$darkBackground: #1a223f; // level 3
+$darkPaper: #111936; // level 4
+
+// dark 800 & 900
+$darkLevel1: #29314f; // level 1
+$darkLevel2: #212946; // level 2
+
+// primary dark
+$darkPrimaryLight: #eef2f6;
+$darkPrimaryMain: #2196f3;
+$darkPrimaryDark: #1e88e5;
+$darkPrimary200: #90caf9;
+$darkPrimary800: #1565c0;
+
+// secondary dark
+$darkSecondaryLight: #d1c4e9;
+$darkSecondaryMain: #7c4dff;
+$darkSecondaryDark: #651fff;
+$darkSecondary200: #b39ddb;
+$darkSecondary800: #6200ea;
+
+// text variants
+$darkTextTitle: #d7dcec;
+$darkTextPrimary: #bdc8f0;
+$darkTextSecondary: #8492c4;
+
+// ==============================|| JAVASCRIPT ||============================== //
+
+:export {
+ // paper & background
+ paper: $paper;
+
+ // primary
+ primaryLight: $primaryLight;
+ primary200: $primary200;
+ primaryMain: $primaryMain;
+ primaryDark: $primaryDark;
+ primary800: $primary800;
+
+ // secondary
+ secondaryLight: $secondaryLight;
+ secondary200: $secondary200;
+ secondaryMain: $secondaryMain;
+ secondaryDark: $secondaryDark;
+ secondary800: $secondary800;
+
+ // success
+ successLight: $successLight;
+ success200: $success200;
+ successMain: $successMain;
+ successDark: $successDark;
+
+ // error
+ errorLight: $errorLight;
+ errorMain: $errorMain;
+ errorDark: $errorDark;
+
+ // orange
+ orangeLight: $orangeLight;
+ orangeMain: $orangeMain;
+ orangeDark: $orangeDark;
+
+ // warning
+ warningLight: $warningLight;
+ warningMain: $warningMain;
+ warningDark: $warningDark;
+
+ // grey
+ grey50: $grey50;
+ grey100: $grey100;
+ grey200: $grey200;
+ grey300: $grey300;
+ grey500: $grey500;
+ grey600: $grey600;
+ grey700: $grey700;
+ grey900: $grey900;
+
+ // ==============================|| DARK THEME VARIANTS ||============================== //
+
+ // paper & background
+ darkPaper: $darkPaper;
+ darkBackground: $darkBackground;
+
+ // dark 800 & 900
+ darkLevel1: $darkLevel1;
+ darkLevel2: $darkLevel2;
+
+ // text variants
+ darkTextTitle: $darkTextTitle;
+ darkTextPrimary: $darkTextPrimary;
+ darkTextSecondary: $darkTextSecondary;
+
+ // primary dark
+ darkPrimaryLight: $darkPrimaryLight;
+ darkPrimaryMain: $darkPrimaryMain;
+ darkPrimaryDark: $darkPrimaryDark;
+ darkPrimary200: $darkPrimary200;
+ darkPrimary800: $darkPrimary800;
+
+ // secondary dark
+ darkSecondaryLight: $darkSecondaryLight;
+ darkSecondaryMain: $darkSecondaryMain;
+ darkSecondaryDark: $darkSecondaryDark;
+ darkSecondary200: $darkSecondary200;
+ darkSecondary800: $darkSecondary800;
+}
diff --git a/web/berry/src/assets/scss/style.scss b/web/berry/src/assets/scss/style.scss
new file mode 100644
index 00000000..17d566e6
--- /dev/null
+++ b/web/berry/src/assets/scss/style.scss
@@ -0,0 +1,128 @@
+// color variants
+@import 'themes-vars.module.scss';
+
+// third-party
+@import '~react-perfect-scrollbar/dist/css/styles.css';
+
+// ==============================|| LIGHT BOX ||============================== //
+.fullscreen .react-images__blanket {
+ z-index: 1200;
+}
+
+// ==============================|| APEXCHART ||============================== //
+
+.apexcharts-legend-series .apexcharts-legend-marker {
+ margin-right: 8px;
+}
+
+// ==============================|| PERFECT SCROLLBAR ||============================== //
+
+.scrollbar-container {
+ .ps__rail-y {
+ &:hover > .ps__thumb-y,
+ &:focus > .ps__thumb-y,
+ &.ps--clicking .ps__thumb-y {
+ background-color: $grey500;
+ width: 5px;
+ }
+ }
+ .ps__thumb-y {
+ background-color: $grey500;
+ border-radius: 6px;
+ width: 5px;
+ right: 0;
+ }
+}
+
+.scrollbar-container.ps,
+.scrollbar-container > .ps {
+ &.ps--active-y > .ps__rail-y {
+ width: 5px;
+ background-color: transparent !important;
+ z-index: 999;
+ &:hover,
+ &.ps--clicking {
+ width: 5px;
+ background-color: transparent;
+ }
+ }
+ &.ps--scrolling-y > .ps__rail-y,
+ &.ps--scrolling-x > .ps__rail-x {
+ opacity: 0.4;
+ background-color: transparent;
+ }
+}
+
+// ==============================|| ANIMATION KEYFRAMES ||============================== //
+
+@keyframes wings {
+ 50% {
+ transform: translateY(-40px);
+ }
+ 100% {
+ transform: translateY(0px);
+ }
+}
+
+@keyframes blink {
+ 50% {
+ opacity: 0;
+ }
+ 100% {
+ opacity: 1;
+ }
+}
+
+@keyframes bounce {
+ 0%,
+ 20%,
+ 53%,
+ to {
+ animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
+ transform: translateZ(0);
+ }
+ 40%,
+ 43% {
+ animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06);
+ transform: translate3d(0, -5px, 0);
+ }
+ 70% {
+ animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06);
+ transform: translate3d(0, -7px, 0);
+ }
+ 80% {
+ transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
+ transform: translateZ(0);
+ }
+ 90% {
+ transform: translate3d(0, -2px, 0);
+ }
+}
+
+@keyframes slideY {
+ 0%,
+ 50%,
+ 100% {
+ transform: translateY(0px);
+ }
+ 25% {
+ transform: translateY(-10px);
+ }
+ 75% {
+ transform: translateY(10px);
+ }
+}
+
+@keyframes slideX {
+ 0%,
+ 50%,
+ 100% {
+ transform: translateX(0px);
+ }
+ 25% {
+ transform: translateX(-10px);
+ }
+ 75% {
+ transform: translateX(10px);
+ }
+}
diff --git a/web/berry/src/config.js b/web/berry/src/config.js
new file mode 100644
index 00000000..eeeda99a
--- /dev/null
+++ b/web/berry/src/config.js
@@ -0,0 +1,29 @@
+const config = {
+ // basename: only at build time to set, and Don't add '/' at end off BASENAME for breadcrumbs, also Don't put only '/' use blank('') instead,
+ // like '/berry-material-react/react/default'
+ basename: '/',
+ defaultPath: '/panel/dashboard',
+ fontFamily: `'Roboto', sans-serif, Helvetica, Arial, sans-serif`,
+ borderRadius: 12,
+ siteInfo: {
+ chat_link: '',
+ display_in_currency: true,
+ email_verification: false,
+ footer_html: '',
+ github_client_id: '',
+ github_oauth: false,
+ logo: '',
+ quota_per_unit: 500000,
+ server_address: '',
+ start_time: 0,
+ system_name: 'One API',
+ top_up_link: '',
+ turnstile_check: false,
+ turnstile_site_key: '',
+ version: '',
+ wechat_login: false,
+ wechat_qrcode: ''
+ }
+};
+
+export default config;
diff --git a/web/berry/src/constants/ChannelConstants.js b/web/berry/src/constants/ChannelConstants.js
new file mode 100644
index 00000000..3ce27838
--- /dev/null
+++ b/web/berry/src/constants/ChannelConstants.js
@@ -0,0 +1,146 @@
+export const CHANNEL_OPTIONS = {
+ 1: {
+ key: 1,
+ text: 'OpenAI',
+ value: 1,
+ color: 'primary'
+ },
+ 14: {
+ key: 14,
+ text: 'Anthropic Claude',
+ value: 14,
+ color: 'info'
+ },
+ 3: {
+ key: 3,
+ text: 'Azure OpenAI',
+ value: 3,
+ color: 'orange'
+ },
+ 11: {
+ key: 11,
+ text: 'Google PaLM2',
+ value: 11,
+ color: 'orange'
+ },
+ 24: {
+ key: 24,
+ text: 'Google Gemini',
+ value: 24,
+ color: 'orange'
+ },
+ 15: {
+ key: 15,
+ text: '百度文心千帆',
+ value: 15,
+ color: 'default'
+ },
+ 17: {
+ key: 17,
+ text: '阿里通义千问',
+ value: 17,
+ color: 'default'
+ },
+ 18: {
+ key: 18,
+ text: '讯飞星火认知',
+ value: 18,
+ color: 'default'
+ },
+ 16: {
+ key: 16,
+ text: '智谱 ChatGLM',
+ value: 16,
+ color: 'default'
+ },
+ 19: {
+ key: 19,
+ text: '360 智脑',
+ value: 19,
+ color: 'default'
+ },
+ 23: {
+ key: 23,
+ text: '腾讯混元',
+ value: 23,
+ color: 'default'
+ },
+ 8: {
+ key: 8,
+ text: '自定义渠道',
+ value: 8,
+ color: 'primary'
+ },
+ 22: {
+ key: 22,
+ text: '知识库:FastGPT',
+ value: 22,
+ color: 'default'
+ },
+ 21: {
+ key: 21,
+ text: '知识库:AI Proxy',
+ value: 21,
+ color: 'purple'
+ },
+ 20: {
+ key: 20,
+ text: '代理:OpenRouter',
+ value: 20,
+ color: 'primary'
+ },
+ 2: {
+ key: 2,
+ text: '代理:API2D',
+ value: 2,
+ color: 'primary'
+ },
+ 5: {
+ key: 5,
+ text: '代理:OpenAI-SB',
+ value: 5,
+ color: 'primary'
+ },
+ 7: {
+ key: 7,
+ text: '代理:OhMyGPT',
+ value: 7,
+ color: 'primary'
+ },
+ 10: {
+ key: 10,
+ text: '代理:AI Proxy',
+ value: 10,
+ color: 'primary'
+ },
+ 4: {
+ key: 4,
+ text: '代理:CloseAI',
+ value: 4,
+ color: 'primary'
+ },
+ 6: {
+ key: 6,
+ text: '代理:OpenAI Max',
+ value: 6,
+ color: 'primary'
+ },
+ 9: {
+ key: 9,
+ text: '代理:AI.LS',
+ value: 9,
+ color: 'primary'
+ },
+ 12: {
+ key: 12,
+ text: '代理:API2GPT',
+ value: 12,
+ color: 'primary'
+ },
+ 13: {
+ key: 13,
+ text: '代理:AIGC2D',
+ value: 13,
+ color: 'primary'
+ }
+};
diff --git a/web/berry/src/constants/CommonConstants.js b/web/berry/src/constants/CommonConstants.js
new file mode 100644
index 00000000..1a37d5f6
--- /dev/null
+++ b/web/berry/src/constants/CommonConstants.js
@@ -0,0 +1 @@
+export const ITEMS_PER_PAGE = 10; // this value must keep same as the one defined in backend!
diff --git a/web/berry/src/constants/SnackbarConstants.js b/web/berry/src/constants/SnackbarConstants.js
new file mode 100644
index 00000000..a05c6652
--- /dev/null
+++ b/web/berry/src/constants/SnackbarConstants.js
@@ -0,0 +1,27 @@
+export const snackbarConstants = {
+ Common: {
+ ERROR: {
+ variant: 'error',
+ autoHideDuration: 5000
+ },
+ WARNING: {
+ variant: 'warning',
+ autoHideDuration: 10000
+ },
+ SUCCESS: {
+ variant: 'success',
+ autoHideDuration: 1500
+ },
+ INFO: {
+ variant: 'info',
+ autoHideDuration: 3000
+ },
+ NOTICE: {
+ variant: 'info',
+ autoHideDuration: 20000
+ }
+ },
+ Mobile: {
+ anchorOrigin: { vertical: 'bottom', horizontal: 'center' }
+ }
+};
diff --git a/web/berry/src/constants/index.js b/web/berry/src/constants/index.js
new file mode 100644
index 00000000..716ef6aa
--- /dev/null
+++ b/web/berry/src/constants/index.js
@@ -0,0 +1,3 @@
+export * from './SnackbarConstants';
+export * from './CommonConstants';
+export * from './ChannelConstants';
diff --git a/web/berry/src/contexts/StatusContext.js b/web/berry/src/contexts/StatusContext.js
new file mode 100644
index 00000000..ed9f1621
--- /dev/null
+++ b/web/berry/src/contexts/StatusContext.js
@@ -0,0 +1,70 @@
+import { useEffect, useCallback, createContext } from "react";
+import { API } from "utils/api";
+import { showNotice, showError } from "utils/common";
+import { SET_SITE_INFO } from "store/actions";
+import { useDispatch } from "react-redux";
+
+export const LoadStatusContext = createContext();
+
+// eslint-disable-next-line
+const StatusProvider = ({ children }) => {
+ const dispatch = useDispatch();
+
+ const loadStatus = useCallback(async () => {
+ const res = await API.get("/api/status");
+ const { success, data } = res.data;
+ let system_name = "";
+ if (success) {
+ if (!data.chat_link) {
+ delete data.chat_link;
+ }
+ localStorage.setItem("siteInfo", JSON.stringify(data));
+ localStorage.setItem("quota_per_unit", data.quota_per_unit);
+ localStorage.setItem("display_in_currency", data.display_in_currency);
+ dispatch({ type: SET_SITE_INFO, payload: data });
+ if (
+ data.version !== process.env.REACT_APP_VERSION &&
+ data.version !== "v0.0.0" &&
+ data.version !== "" &&
+ process.env.REACT_APP_VERSION !== ""
+ ) {
+ showNotice(
+ `新版本可用:${data.version},请使用快捷键 Shift + F5 刷新页面`
+ );
+ }
+ if (data.system_name) {
+ system_name = data.system_name;
+ }
+ } else {
+ const backupSiteInfo = localStorage.getItem("siteInfo");
+ if (backupSiteInfo) {
+ const data = JSON.parse(backupSiteInfo);
+ if (data.system_name) {
+ system_name = data.system_name;
+ }
+ dispatch({
+ type: SET_SITE_INFO,
+ payload: data,
+ });
+ }
+ showError("无法正常连接至服务器!");
+ }
+
+ if (system_name) {
+ document.title = system_name;
+ }
+ }, [dispatch]);
+
+ useEffect(() => {
+ loadStatus().then();
+ }, [loadStatus]);
+
+ return (
+
+ {" "}
+ {children}{" "}
+
+ );
+};
+
+export default StatusProvider;
diff --git a/web/berry/src/contexts/UserContext.js b/web/berry/src/contexts/UserContext.js
new file mode 100644
index 00000000..491da9d9
--- /dev/null
+++ b/web/berry/src/contexts/UserContext.js
@@ -0,0 +1,29 @@
+// contexts/User/index.jsx
+import React, { useEffect, useCallback, createContext, useState } from 'react';
+import { LOGIN } from 'store/actions';
+import { useDispatch } from 'react-redux';
+
+export const UserContext = createContext();
+
+// eslint-disable-next-line
+const UserProvider = ({ children }) => {
+ const dispatch = useDispatch();
+ const [isUserLoaded, setIsUserLoaded] = useState(false);
+
+ const loadUser = useCallback(() => {
+ let user = localStorage.getItem('user');
+ if (user) {
+ let data = JSON.parse(user);
+ dispatch({ type: LOGIN, payload: data });
+ }
+ setIsUserLoaded(true);
+ }, [dispatch]);
+
+ useEffect(() => {
+ loadUser();
+ }, [loadUser]);
+
+ return {children} ;
+};
+
+export default UserProvider;
diff --git a/web/berry/src/hooks/useAuth.js b/web/berry/src/hooks/useAuth.js
new file mode 100644
index 00000000..fa7cb934
--- /dev/null
+++ b/web/berry/src/hooks/useAuth.js
@@ -0,0 +1,13 @@
+import { isAdmin } from 'utils/common';
+import { useNavigate } from 'react-router-dom';
+const navigate = useNavigate();
+
+const useAuth = () => {
+ const userIsAdmin = isAdmin();
+
+ if (!userIsAdmin) {
+ navigate('/panel/404');
+ }
+};
+
+export default useAuth;
diff --git a/web/berry/src/hooks/useLogin.js b/web/berry/src/hooks/useLogin.js
new file mode 100644
index 00000000..53626577
--- /dev/null
+++ b/web/berry/src/hooks/useLogin.js
@@ -0,0 +1,78 @@
+import { API } from 'utils/api';
+import { useDispatch } from 'react-redux';
+import { LOGIN } from 'store/actions';
+import { useNavigate } from 'react-router';
+import { showSuccess } from 'utils/common';
+
+const useLogin = () => {
+ const dispatch = useDispatch();
+ const navigate = useNavigate();
+ const login = async (username, password) => {
+ try {
+ const res = await API.post(`/api/user/login`, {
+ username,
+ password
+ });
+ const { success, message, data } = res.data;
+ if (success) {
+ localStorage.setItem('user', JSON.stringify(data));
+ dispatch({ type: LOGIN, payload: data });
+ navigate('/panel');
+ }
+ return { success, message };
+ } catch (err) {
+ // 请求失败,设置错误信息
+ return { success: false, message: '' };
+ }
+ };
+
+ const githubLogin = async (code, state) => {
+ try {
+ const res = await API.get(`/api/oauth/github?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}`);
+ const { success, message, data } = res.data;
+ if (success) {
+ dispatch({ type: LOGIN, payload: data });
+ localStorage.setItem('user', JSON.stringify(data));
+ showSuccess('登录成功!');
+ navigate('/panel');
+ }
+ return { success, message };
+ } catch (err) {
+ // 请求失败,设置错误信息
+ return { success: false, message: '' };
+ }
+ };
+
+ const logout = async () => {
+ await API.get('/api/user/logout');
+ localStorage.removeItem('user');
+ dispatch({ type: LOGIN, payload: null });
+ navigate('/');
+ };
+
+ return { login, logout, githubLogin, wechatLogin };
+};
+
+export default useLogin;
diff --git a/web/berry/src/hooks/useRegister.js b/web/berry/src/hooks/useRegister.js
new file mode 100644
index 00000000..d07dc43a
--- /dev/null
+++ b/web/berry/src/hooks/useRegister.js
@@ -0,0 +1,39 @@
+import { API } from 'utils/api';
+import { useNavigate } from 'react-router';
+import { showSuccess } from 'utils/common';
+
+const useRegister = () => {
+ const navigate = useNavigate();
+ const register = async (input, turnstile) => {
+ try {
+ const res = await API.post(`/api/user/register?turnstile=${turnstile}`, input);
+ const { success, message } = res.data;
+ if (success) {
+ showSuccess('注册成功!');
+ navigate('/login');
+ }
+ return { success, message };
+ } catch (err) {
+ // 请求失败,设置错误信息
+ return { success: false, message: '' };
+ }
+ };
+
+ const sendVerificationCode = async (email, turnstile) => {
+ try {
+ const res = await API.get(`/api/verification?email=${email}&turnstile=${turnstile}`);
+ const { success, message } = res.data;
+ if (success) {
+ showSuccess('验证码发送成功,请检查你的邮箱!');
+ }
+ return { success, message };
+ } catch (err) {
+ // 请求失败,设置错误信息
+ return { success: false, message: '' };
+ }
+ };
+
+ return { register, sendVerificationCode };
+};
+
+export default useRegister;
diff --git a/web/berry/src/hooks/useScriptRef.js b/web/berry/src/hooks/useScriptRef.js
new file mode 100644
index 00000000..bd300cbb
--- /dev/null
+++ b/web/berry/src/hooks/useScriptRef.js
@@ -0,0 +1,18 @@
+import { useEffect, useRef } from 'react';
+
+// ==============================|| ELEMENT REFERENCE HOOKS ||============================== //
+
+const useScriptRef = () => {
+ const scripted = useRef(true);
+
+ useEffect(
+ () => () => {
+ scripted.current = true;
+ },
+ []
+ );
+
+ return scripted;
+};
+
+export default useScriptRef;
diff --git a/web/berry/src/index.js b/web/berry/src/index.js
new file mode 100644
index 00000000..d1411be3
--- /dev/null
+++ b/web/berry/src/index.js
@@ -0,0 +1,31 @@
+import { createRoot } from 'react-dom/client';
+
+// third party
+import { BrowserRouter } from 'react-router-dom';
+import { Provider } from 'react-redux';
+
+// project imports
+import * as serviceWorker from 'serviceWorker';
+import App from 'App';
+import { store } from 'store';
+
+// style + assets
+import 'assets/scss/style.scss';
+import config from './config';
+
+// ==============================|| REACT DOM RENDER ||============================== //
+
+const container = document.getElementById('root');
+const root = createRoot(container); // createRoot(container!) if you use TypeScript
+root.render(
+
+
+
+
+
+);
+
+// If you want your app to work offline and load faster, you can change
+// unregister() to register() below. Note this comes with some pitfalls.
+// Learn more about service workers: https://bit.ly/CRA-PWA
+serviceWorker.register();
diff --git a/web/berry/src/layout/MainLayout/Header/ProfileSection/index.js b/web/berry/src/layout/MainLayout/Header/ProfileSection/index.js
new file mode 100644
index 00000000..3e351254
--- /dev/null
+++ b/web/berry/src/layout/MainLayout/Header/ProfileSection/index.js
@@ -0,0 +1,173 @@
+import { useState, useRef, useEffect } from 'react';
+
+import { useSelector } from 'react-redux';
+import { useNavigate } from 'react-router-dom';
+// material-ui
+import { useTheme } from '@mui/material/styles';
+import {
+ Avatar,
+ Chip,
+ ClickAwayListener,
+ List,
+ ListItemButton,
+ ListItemIcon,
+ ListItemText,
+ Paper,
+ Popper,
+ Typography
+} from '@mui/material';
+
+// project imports
+import MainCard from 'ui-component/cards/MainCard';
+import Transitions from 'ui-component/extended/Transitions';
+import User1 from 'assets/images/users/user-round.svg';
+import useLogin from 'hooks/useLogin';
+
+// assets
+import { IconLogout, IconSettings, IconUserScan } from '@tabler/icons-react';
+
+// ==============================|| PROFILE MENU ||============================== //
+
+const ProfileSection = () => {
+ const theme = useTheme();
+ const navigate = useNavigate();
+ const customization = useSelector((state) => state.customization);
+ const { logout } = useLogin();
+
+ const [open, setOpen] = useState(false);
+ /**
+ * anchorRef is used on different componets and specifying one type leads to other components throwing an error
+ * */
+ const anchorRef = useRef(null);
+ const handleLogout = async () => {
+ logout();
+ };
+
+ const handleClose = (event) => {
+ if (anchorRef.current && anchorRef.current.contains(event.target)) {
+ return;
+ }
+ setOpen(false);
+ };
+
+ const handleToggle = () => {
+ setOpen((prevOpen) => !prevOpen);
+ };
+
+ const prevOpen = useRef(open);
+ useEffect(() => {
+ if (prevOpen.current === true && open === false) {
+ anchorRef.current.focus();
+ }
+
+ prevOpen.current = open;
+ }, [open]);
+
+ return (
+ <>
+
+ }
+ label={}
+ variant="outlined"
+ ref={anchorRef}
+ aria-controls={open ? 'menu-list-grow' : undefined}
+ aria-haspopup="true"
+ onClick={handleToggle}
+ color="primary"
+ />
+
+ {({ TransitionProps }) => (
+
+
+
+
+
+ navigate('/panel/profile')}>
+
+
+
+ 设置} />
+
+
+
+
+
+
+ 登出} />
+
+
+
+
+
+
+ )}
+
+ >
+ );
+};
+
+export default ProfileSection;
diff --git a/web/berry/src/layout/MainLayout/Header/index.js b/web/berry/src/layout/MainLayout/Header/index.js
new file mode 100644
index 00000000..51d40c75
--- /dev/null
+++ b/web/berry/src/layout/MainLayout/Header/index.js
@@ -0,0 +1,68 @@
+import PropTypes from 'prop-types';
+
+// material-ui
+import { useTheme } from '@mui/material/styles';
+import { Avatar, Box, ButtonBase } from '@mui/material';
+
+// project imports
+import LogoSection from '../LogoSection';
+import ProfileSection from './ProfileSection';
+
+// assets
+import { IconMenu2 } from '@tabler/icons-react';
+
+// ==============================|| MAIN NAVBAR / HEADER ||============================== //
+
+const Header = ({ handleLeftDrawerToggle }) => {
+ const theme = useTheme();
+
+ return (
+ <>
+ {/* logo & toggler button */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
+
+Header.propTypes = {
+ handleLeftDrawerToggle: PropTypes.func
+};
+
+export default Header;
diff --git a/web/berry/src/layout/MainLayout/LogoSection/index.js b/web/berry/src/layout/MainLayout/LogoSection/index.js
new file mode 100644
index 00000000..1d70e48c
--- /dev/null
+++ b/web/berry/src/layout/MainLayout/LogoSection/index.js
@@ -0,0 +1,23 @@
+import { Link } from 'react-router-dom';
+import { useDispatch, useSelector } from 'react-redux';
+
+// material-ui
+import { ButtonBase } from '@mui/material';
+
+// project imports
+import Logo from 'ui-component/Logo';
+import { MENU_OPEN } from 'store/actions';
+
+// ==============================|| MAIN LOGO ||============================== //
+
+const LogoSection = () => {
+ const defaultId = useSelector((state) => state.customization.defaultId);
+ const dispatch = useDispatch();
+ return (
+ dispatch({ type: MENU_OPEN, id: defaultId })} component={Link} to="/">
+
+
+ );
+};
+
+export default LogoSection;
diff --git a/web/berry/src/layout/MainLayout/Sidebar/MenuCard/index.js b/web/berry/src/layout/MainLayout/Sidebar/MenuCard/index.js
new file mode 100644
index 00000000..16b13231
--- /dev/null
+++ b/web/berry/src/layout/MainLayout/Sidebar/MenuCard/index.js
@@ -0,0 +1,130 @@
+// import PropTypes from 'prop-types';
+import { useSelector } from 'react-redux';
+
+// material-ui
+import { styled, useTheme } from '@mui/material/styles';
+import {
+ Avatar,
+ Card,
+ CardContent,
+ // Grid,
+ // LinearProgress,
+ List,
+ ListItem,
+ ListItemAvatar,
+ ListItemText,
+ Typography
+ // linearProgressClasses
+} from '@mui/material';
+import User1 from 'assets/images/users/user-round.svg';
+import { useNavigate } from 'react-router-dom';
+
+// assets
+// import TableChartOutlinedIcon from '@mui/icons-material/TableChartOutlined';
+
+// styles
+// const BorderLinearProgress = styled(LinearProgress)(({ theme }) => ({
+// height: 10,
+// borderRadius: 30,
+// [`&.${linearProgressClasses.colorPrimary}`]: {
+// backgroundColor: '#fff'
+// },
+// [`& .${linearProgressClasses.bar}`]: {
+// borderRadius: 5,
+// backgroundColor: theme.palette.primary.main
+// }
+// }));
+
+const CardStyle = styled(Card)(({ theme }) => ({
+ background: theme.palette.primary.light,
+ marginBottom: '22px',
+ overflow: 'hidden',
+ position: 'relative',
+ '&:after': {
+ content: '""',
+ position: 'absolute',
+ width: '157px',
+ height: '157px',
+ background: theme.palette.primary[200],
+ borderRadius: '50%',
+ top: '-105px',
+ right: '-96px'
+ }
+}));
+
+// ==============================|| PROGRESS BAR WITH LABEL ||============================== //
+
+// function LinearProgressWithLabel({ value, ...others }) {
+// const theme = useTheme();
+
+// return (
+//
+//
+//
+//
+//
+// Progress
+//
+//
+//
+// {`${Math.round(value)}%`}
+//
+//
+//
+//
+//
+//
+//
+// );
+// }
+
+// LinearProgressWithLabel.propTypes = {
+// value: PropTypes.number
+// };
+
+// ==============================|| SIDEBAR MENU Card ||============================== //
+
+const MenuCard = () => {
+ const theme = useTheme();
+ const account = useSelector((state) => state.account);
+ const navigate = useNavigate();
+
+ return (
+
+
+
+
+
+ navigate('/panel/profile')}
+ >
+
+
+ {account.user?.username}
+
+ }
+ secondary={ 欢迎回来 }
+ />
+
+
+ {/* */}
+
+
+ );
+};
+
+export default MenuCard;
diff --git a/web/berry/src/layout/MainLayout/Sidebar/MenuList/NavCollapse/index.js b/web/berry/src/layout/MainLayout/Sidebar/MenuList/NavCollapse/index.js
new file mode 100644
index 00000000..0632d56f
--- /dev/null
+++ b/web/berry/src/layout/MainLayout/Sidebar/MenuList/NavCollapse/index.js
@@ -0,0 +1,158 @@
+import PropTypes from 'prop-types';
+import { useEffect, useState } from 'react';
+import { useSelector } from 'react-redux';
+import { useLocation, useNavigate } from 'react-router';
+
+// material-ui
+import { useTheme } from '@mui/material/styles';
+import { Collapse, List, ListItemButton, ListItemIcon, ListItemText, Typography } from '@mui/material';
+
+// project imports
+import NavItem from '../NavItem';
+
+// assets
+import FiberManualRecordIcon from '@mui/icons-material/FiberManualRecord';
+import { IconChevronDown, IconChevronUp } from '@tabler/icons-react';
+
+// ==============================|| SIDEBAR MENU LIST COLLAPSE ITEMS ||============================== //
+
+const NavCollapse = ({ menu, level }) => {
+ const theme = useTheme();
+ const customization = useSelector((state) => state.customization);
+ const navigate = useNavigate();
+
+ const [open, setOpen] = useState(false);
+ const [selected, setSelected] = useState(null);
+
+ const handleClick = () => {
+ setOpen(!open);
+ setSelected(!selected ? menu.id : null);
+ if (menu?.id !== 'authentication') {
+ navigate(menu.children[0]?.url);
+ }
+ };
+
+ const { pathname } = useLocation();
+ const checkOpenForParent = (child, id) => {
+ child.forEach((item) => {
+ if (item.url === pathname) {
+ setOpen(true);
+ setSelected(id);
+ }
+ });
+ };
+
+ // menu collapse for sub-levels
+ useEffect(() => {
+ setOpen(false);
+ setSelected(null);
+ if (menu.children) {
+ menu.children.forEach((item) => {
+ if (item.children?.length) {
+ checkOpenForParent(item.children, menu.id);
+ }
+ if (item.url === pathname) {
+ setSelected(menu.id);
+ setOpen(true);
+ }
+ });
+ }
+
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [pathname, menu.children]);
+
+ // menu collapse & item
+ const menus = menu.children?.map((item) => {
+ switch (item.type) {
+ case 'collapse':
+ return ;
+ case 'item':
+ return ;
+ default:
+ return (
+
+ Menu Items Error
+
+ );
+ }
+ });
+
+ const Icon = menu.icon;
+ const menuIcon = menu.icon ? (
+
+ ) : (
+ 0 ? 'inherit' : 'medium'}
+ />
+ );
+
+ return (
+ <>
+ 1 ? 'transparent !important' : 'inherit',
+ py: level > 1 ? 1 : 1.25,
+ pl: `${level * 24}px`
+ }}
+ selected={selected === menu.id}
+ onClick={handleClick}
+ >
+ {menuIcon}
+
+ {menu.title}
+
+ }
+ secondary={
+ menu.caption && (
+
+ {menu.caption}
+
+ )
+ }
+ />
+ {open ? (
+
+ ) : (
+
+ )}
+
+
+
+ {menus}
+
+
+ >
+ );
+};
+
+NavCollapse.propTypes = {
+ menu: PropTypes.object,
+ level: PropTypes.number
+};
+
+export default NavCollapse;
diff --git a/web/berry/src/layout/MainLayout/Sidebar/MenuList/NavGroup/index.js b/web/berry/src/layout/MainLayout/Sidebar/MenuList/NavGroup/index.js
new file mode 100644
index 00000000..b6479bc2
--- /dev/null
+++ b/web/berry/src/layout/MainLayout/Sidebar/MenuList/NavGroup/index.js
@@ -0,0 +1,61 @@
+import PropTypes from 'prop-types';
+
+// material-ui
+import { useTheme } from '@mui/material/styles';
+import { Divider, List, Typography } from '@mui/material';
+
+// project imports
+import NavItem from '../NavItem';
+import NavCollapse from '../NavCollapse';
+
+// ==============================|| SIDEBAR MENU LIST GROUP ||============================== //
+
+const NavGroup = ({ item }) => {
+ const theme = useTheme();
+
+ // menu list collapse & items
+ const items = item.children?.map((menu) => {
+ switch (menu.type) {
+ case 'collapse':
+ return ;
+ case 'item':
+ return ;
+ default:
+ return (
+
+ Menu Items Error
+
+ );
+ }
+ });
+
+ return (
+ <>
+
+ {item.title}
+ {item.caption && (
+
+ {item.caption}
+
+ )}
+
+ )
+ }
+ >
+ {items}
+
+
+ {/* group divider */}
+
+ >
+ );
+};
+
+NavGroup.propTypes = {
+ item: PropTypes.object
+};
+
+export default NavGroup;
diff --git a/web/berry/src/layout/MainLayout/Sidebar/MenuList/NavItem/index.js b/web/berry/src/layout/MainLayout/Sidebar/MenuList/NavItem/index.js
new file mode 100644
index 00000000..ddce9cf4
--- /dev/null
+++ b/web/berry/src/layout/MainLayout/Sidebar/MenuList/NavItem/index.js
@@ -0,0 +1,115 @@
+import PropTypes from 'prop-types';
+import { forwardRef, useEffect } from 'react';
+import { Link, useLocation } from 'react-router-dom';
+import { useDispatch, useSelector } from 'react-redux';
+
+// material-ui
+import { useTheme } from '@mui/material/styles';
+import { Avatar, Chip, ListItemButton, ListItemIcon, ListItemText, Typography, useMediaQuery } from '@mui/material';
+
+// project imports
+import { MENU_OPEN, SET_MENU } from 'store/actions';
+
+// assets
+import FiberManualRecordIcon from '@mui/icons-material/FiberManualRecord';
+
+// ==============================|| SIDEBAR MENU LIST ITEMS ||============================== //
+
+const NavItem = ({ item, level }) => {
+ const theme = useTheme();
+ const dispatch = useDispatch();
+ const { pathname } = useLocation();
+ const customization = useSelector((state) => state.customization);
+ const matchesSM = useMediaQuery(theme.breakpoints.down('lg'));
+
+ const Icon = item.icon;
+ const itemIcon = item?.icon ? (
+
+ ) : (
+ id === item?.id) > -1 ? 8 : 6,
+ height: customization.isOpen.findIndex((id) => id === item?.id) > -1 ? 8 : 6
+ }}
+ fontSize={level > 0 ? 'inherit' : 'medium'}
+ />
+ );
+
+ let itemTarget = '_self';
+ if (item.target) {
+ itemTarget = '_blank';
+ }
+
+ let listItemProps = {
+ component: forwardRef((props, ref) => )
+ };
+ if (item?.external) {
+ listItemProps = { component: 'a', href: item.url, target: itemTarget };
+ }
+
+ const itemHandler = (id) => {
+ dispatch({ type: MENU_OPEN, id });
+ if (matchesSM) dispatch({ type: SET_MENU, opened: false });
+ };
+
+ // active menu item on page load
+ useEffect(() => {
+ const currentIndex = document.location.pathname
+ .toString()
+ .split('/')
+ .findIndex((id) => id === item.id);
+ if (currentIndex > -1) {
+ dispatch({ type: MENU_OPEN, id: item.id });
+ }
+ // eslint-disable-next-line
+ }, [pathname]);
+
+ return (
+ 1 ? 'transparent !important' : 'inherit',
+ py: level > 1 ? 1 : 1.25,
+ pl: `${level * 24}px`
+ }}
+ selected={customization.isOpen.findIndex((id) => id === item.id) > -1}
+ onClick={() => itemHandler(item.id)}
+ >
+ {itemIcon}
+ id === item.id) > -1 ? 'h5' : 'body1'} color="inherit">
+ {item.title}
+
+ }
+ secondary={
+ item.caption && (
+
+ {item.caption}
+
+ )
+ }
+ />
+ {item.chip && (
+ {item.chip.avatar}}
+ />
+ )}
+
+ );
+};
+
+NavItem.propTypes = {
+ item: PropTypes.object,
+ level: PropTypes.number
+};
+
+export default NavItem;
diff --git a/web/berry/src/layout/MainLayout/Sidebar/MenuList/index.js b/web/berry/src/layout/MainLayout/Sidebar/MenuList/index.js
new file mode 100644
index 00000000..4872057a
--- /dev/null
+++ b/web/berry/src/layout/MainLayout/Sidebar/MenuList/index.js
@@ -0,0 +1,36 @@
+// material-ui
+import { Typography } from '@mui/material';
+
+// project imports
+import NavGroup from './NavGroup';
+import menuItem from 'menu-items';
+import { isAdmin } from 'utils/common';
+
+// ==============================|| SIDEBAR MENU LIST ||============================== //
+const MenuList = () => {
+ const userIsAdmin = isAdmin();
+
+ return (
+ <>
+ {menuItem.items.map((item) => {
+ if (item.type !== 'group') {
+ return (
+
+ Menu Items Error
+
+ );
+ }
+
+ const filteredChildren = item.children.filter((child) => !child.isAdmin || userIsAdmin);
+
+ if (filteredChildren.length === 0) {
+ return null;
+ }
+
+ return ;
+ })}
+ >
+ );
+};
+
+export default MenuList;
diff --git a/web/berry/src/layout/MainLayout/Sidebar/index.js b/web/berry/src/layout/MainLayout/Sidebar/index.js
new file mode 100644
index 00000000..e3c6d12d
--- /dev/null
+++ b/web/berry/src/layout/MainLayout/Sidebar/index.js
@@ -0,0 +1,94 @@
+import PropTypes from 'prop-types';
+
+// material-ui
+import { useTheme } from '@mui/material/styles';
+import { Box, Chip, Drawer, Stack, useMediaQuery } from '@mui/material';
+
+// third-party
+import PerfectScrollbar from 'react-perfect-scrollbar';
+import { BrowserView, MobileView } from 'react-device-detect';
+
+// project imports
+import MenuList from './MenuList';
+import LogoSection from '../LogoSection';
+import MenuCard from './MenuCard';
+import { drawerWidth } from 'store/constant';
+
+// ==============================|| SIDEBAR DRAWER ||============================== //
+
+const Sidebar = ({ drawerOpen, drawerToggle, window }) => {
+ const theme = useTheme();
+ const matchUpMd = useMediaQuery(theme.breakpoints.up('md'));
+
+ const drawer = (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+
+ const container = window !== undefined ? () => window.document.body : undefined;
+
+ return (
+
+
+ {drawer}
+
+
+ );
+};
+
+Sidebar.propTypes = {
+ drawerOpen: PropTypes.bool,
+ drawerToggle: PropTypes.func,
+ window: PropTypes.object
+};
+
+export default Sidebar;
diff --git a/web/berry/src/layout/MainLayout/index.js b/web/berry/src/layout/MainLayout/index.js
new file mode 100644
index 00000000..973a167b
--- /dev/null
+++ b/web/berry/src/layout/MainLayout/index.js
@@ -0,0 +1,103 @@
+import { useDispatch, useSelector } from 'react-redux';
+import { Outlet } from 'react-router-dom';
+import AuthGuard from 'utils/route-guard/AuthGuard';
+
+// material-ui
+import { styled, useTheme } from '@mui/material/styles';
+import { AppBar, Box, CssBaseline, Toolbar, useMediaQuery } from '@mui/material';
+import AdminContainer from 'ui-component/AdminContainer';
+
+// project imports
+import Breadcrumbs from 'ui-component/extended/Breadcrumbs';
+import Header from './Header';
+import Sidebar from './Sidebar';
+import navigation from 'menu-items';
+import { drawerWidth } from 'store/constant';
+import { SET_MENU } from 'store/actions';
+
+// assets
+import { IconChevronRight } from '@tabler/icons-react';
+
+// styles
+const Main = styled('main', { shouldForwardProp: (prop) => prop !== 'open' })(({ theme, open }) => ({
+ ...theme.typography.mainContent,
+ borderBottomLeftRadius: 0,
+ borderBottomRightRadius: 0,
+ transition: theme.transitions.create(
+ 'margin',
+ open
+ ? {
+ easing: theme.transitions.easing.easeOut,
+ duration: theme.transitions.duration.enteringScreen
+ }
+ : {
+ easing: theme.transitions.easing.sharp,
+ duration: theme.transitions.duration.leavingScreen
+ }
+ ),
+ [theme.breakpoints.up('md')]: {
+ marginLeft: open ? 0 : -(drawerWidth - 20),
+ width: `calc(100% - ${drawerWidth}px)`
+ },
+ [theme.breakpoints.down('md')]: {
+ marginLeft: '20px',
+ width: `calc(100% - ${drawerWidth}px)`,
+ padding: '16px'
+ },
+ [theme.breakpoints.down('sm')]: {
+ marginLeft: '10px',
+ width: `calc(100% - ${drawerWidth}px)`,
+ padding: '16px',
+ marginRight: '10px'
+ }
+}));
+
+// ==============================|| MAIN LAYOUT ||============================== //
+
+const MainLayout = () => {
+ const theme = useTheme();
+ const matchDownMd = useMediaQuery(theme.breakpoints.down('md'));
+ // Handle left drawer
+ const leftDrawerOpened = useSelector((state) => state.customization.opened);
+ const dispatch = useDispatch();
+ const handleLeftDrawerToggle = () => {
+ dispatch({ type: SET_MENU, opened: !leftDrawerOpened });
+ };
+
+ return (
+
+
+ {/* header */}
+
+
+
+
+
+
+ {/* drawer */}
+
+
+ {/* main content */}
+
+ {/* breadcrumb */}
+
+
+
+
+
+
+
+
+ );
+};
+
+export default MainLayout;
diff --git a/web/berry/src/layout/MinimalLayout/Header/index.js b/web/berry/src/layout/MinimalLayout/Header/index.js
new file mode 100644
index 00000000..4f61da60
--- /dev/null
+++ b/web/berry/src/layout/MinimalLayout/Header/index.js
@@ -0,0 +1,75 @@
+// 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";
+
+// ==============================|| MAIN NAVBAR / HEADER ||============================== //
+
+const Header = () => {
+ const theme = useTheme();
+ const { pathname } = useLocation();
+ const account = useSelector((state) => state.account);
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+ {account.user ? (
+
+ ) : (
+
+ )}
+
+ >
+ );
+};
+
+export default Header;
diff --git a/web/berry/src/layout/MinimalLayout/index.js b/web/berry/src/layout/MinimalLayout/index.js
new file mode 100644
index 00000000..084ee6ac
--- /dev/null
+++ b/web/berry/src/layout/MinimalLayout/index.js
@@ -0,0 +1,39 @@
+import { Outlet } from 'react-router-dom';
+import { useTheme } from '@mui/material/styles';
+import { AppBar, Box, CssBaseline, Toolbar } from '@mui/material';
+import Header from './Header';
+import Footer from 'ui-component/Footer';
+
+// ==============================|| MINIMAL LAYOUT ||============================== //
+
+const MinimalLayout = () => {
+ const theme = useTheme();
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default MinimalLayout;
diff --git a/web/berry/src/layout/NavMotion.js b/web/berry/src/layout/NavMotion.js
new file mode 100644
index 00000000..d82f7e4f
--- /dev/null
+++ b/web/berry/src/layout/NavMotion.js
@@ -0,0 +1,39 @@
+import PropTypes from 'prop-types';
+import { motion } from 'framer-motion';
+
+// ==============================|| ANIMATION FOR CONTENT ||============================== //
+
+const NavMotion = ({ children }) => {
+ const motionVariants = {
+ initial: {
+ opacity: 0,
+ scale: 0.99
+ },
+ in: {
+ opacity: 1,
+ scale: 1
+ },
+ out: {
+ opacity: 0,
+ scale: 1.01
+ }
+ };
+
+ const motionTransition = {
+ type: 'tween',
+ ease: 'anticipate',
+ duration: 0.4
+ };
+
+ return (
+
+ {children}
+
+ );
+};
+
+NavMotion.propTypes = {
+ children: PropTypes.node
+};
+
+export default NavMotion;
diff --git a/web/berry/src/layout/NavigationScroll.js b/web/berry/src/layout/NavigationScroll.js
new file mode 100644
index 00000000..89b22e65
--- /dev/null
+++ b/web/berry/src/layout/NavigationScroll.js
@@ -0,0 +1,26 @@
+import PropTypes from 'prop-types';
+import { useEffect } from 'react';
+import { useLocation } from 'react-router-dom';
+
+// ==============================|| NAVIGATION SCROLL TO TOP ||============================== //
+
+const NavigationScroll = ({ children }) => {
+ const location = useLocation();
+ const { pathname } = location;
+
+ useEffect(() => {
+ window.scrollTo({
+ top: 0,
+ left: 0,
+ behavior: 'smooth'
+ });
+ }, [pathname]);
+
+ return children || null;
+};
+
+NavigationScroll.propTypes = {
+ children: PropTypes.node
+};
+
+export default NavigationScroll;
diff --git a/web/berry/src/menu-items/index.js b/web/berry/src/menu-items/index.js
new file mode 100644
index 00000000..e732f8af
--- /dev/null
+++ b/web/berry/src/menu-items/index.js
@@ -0,0 +1,18 @@
+import panel from './panel';
+
+// ==============================|| MENU ITEMS ||============================== //
+
+const menuItems = {
+ items: [panel],
+ urlMap: {}
+};
+
+// Initialize urlMap
+menuItems.urlMap = menuItems.items.reduce((map, item) => {
+ item.children.forEach((child) => {
+ map[child.url] = child;
+ });
+ return map;
+}, {});
+
+export default menuItems;
diff --git a/web/berry/src/menu-items/panel.js b/web/berry/src/menu-items/panel.js
new file mode 100644
index 00000000..8d7738ad
--- /dev/null
+++ b/web/berry/src/menu-items/panel.js
@@ -0,0 +1,104 @@
+// assets
+import {
+ IconDashboard,
+ IconSitemap,
+ IconArticle,
+ IconCoin,
+ IconAdjustments,
+ IconKey,
+ IconGardenCart,
+ IconUser,
+ IconUserScan
+} from '@tabler/icons-react';
+
+// constant
+const icons = { IconDashboard, IconSitemap, IconArticle, IconCoin, IconAdjustments, IconKey, IconGardenCart, IconUser, IconUserScan };
+
+// ==============================|| DASHBOARD MENU ITEMS ||============================== //
+
+const panel = {
+ id: 'panel',
+ type: 'group',
+ children: [
+ {
+ id: 'dashboard',
+ title: '总览',
+ type: 'item',
+ url: '/panel/dashboard',
+ icon: icons.IconDashboard,
+ breadcrumbs: false,
+ isAdmin: false
+ },
+ {
+ id: 'channel',
+ title: '渠道',
+ type: 'item',
+ url: '/panel/channel',
+ icon: icons.IconSitemap,
+ breadcrumbs: false,
+ isAdmin: true
+ },
+ {
+ id: 'token',
+ title: '令牌',
+ type: 'item',
+ url: '/panel/token',
+ icon: icons.IconKey,
+ breadcrumbs: false
+ },
+ {
+ id: 'log',
+ title: '日志',
+ type: 'item',
+ url: '/panel/log',
+ icon: icons.IconArticle,
+ breadcrumbs: false
+ },
+ {
+ id: 'redemption',
+ title: '兑换',
+ type: 'item',
+ url: '/panel/redemption',
+ icon: icons.IconCoin,
+ breadcrumbs: false,
+ isAdmin: true
+ },
+ {
+ id: 'topup',
+ title: '充值',
+ type: 'item',
+ url: '/panel/topup',
+ icon: icons.IconGardenCart,
+ breadcrumbs: false
+ },
+ {
+ id: 'user',
+ title: '用户',
+ type: 'item',
+ url: '/panel/user',
+ icon: icons.IconUser,
+ breadcrumbs: false,
+ isAdmin: true
+ },
+ {
+ id: 'profile',
+ title: '我的',
+ type: 'item',
+ url: '/panel/profile',
+ icon: icons.IconUserScan,
+ breadcrumbs: false,
+ isAdmin: true
+ },
+ {
+ id: 'setting',
+ title: '设置',
+ type: 'item',
+ url: '/panel/setting',
+ icon: icons.IconAdjustments,
+ breadcrumbs: false,
+ isAdmin: true
+ }
+ ]
+};
+
+export default panel;
diff --git a/web/berry/src/routes/MainRoutes.js b/web/berry/src/routes/MainRoutes.js
new file mode 100644
index 00000000..74f7e4c2
--- /dev/null
+++ b/web/berry/src/routes/MainRoutes.js
@@ -0,0 +1,73 @@
+import { lazy } from 'react';
+
+// project imports
+import MainLayout from 'layout/MainLayout';
+import Loadable from 'ui-component/Loadable';
+
+const Channel = Loadable(lazy(() => import('views/Channel')));
+const Log = Loadable(lazy(() => import('views/Log')));
+const Redemption = Loadable(lazy(() => import('views/Redemption')));
+const Setting = Loadable(lazy(() => import('views/Setting')));
+const Token = Loadable(lazy(() => import('views/Token')));
+const Topup = Loadable(lazy(() => import('views/Topup')));
+const User = Loadable(lazy(() => import('views/User')));
+const Profile = Loadable(lazy(() => import('views/Profile')));
+const NotFoundView = Loadable(lazy(() => import('views/Error')));
+
+// dashboard routing
+const Dashboard = Loadable(lazy(() => import('views/Dashboard')));
+
+// ==============================|| MAIN ROUTING ||============================== //
+
+const MainRoutes = {
+ path: '/panel',
+ element: ,
+ children: [
+ {
+ path: '',
+ element:
+ },
+ {
+ path: 'dashboard',
+ element:
+ },
+ {
+ path: 'channel',
+ element:
+ },
+ {
+ path: 'log',
+ element:
+ },
+ {
+ path: 'redemption',
+ element:
+ },
+ {
+ path: 'setting',
+ element:
+ },
+ {
+ path: 'token',
+ element:
+ },
+ {
+ path: 'topup',
+ element:
+ },
+ {
+ path: 'user',
+ element:
+ },
+ {
+ path: 'profile',
+ element:
+ },
+ {
+ path: '404',
+ element:
+ }
+ ]
+};
+
+export default MainRoutes;
diff --git a/web/berry/src/routes/OtherRoutes.js b/web/berry/src/routes/OtherRoutes.js
new file mode 100644
index 00000000..085c4add
--- /dev/null
+++ b/web/berry/src/routes/OtherRoutes.js
@@ -0,0 +1,58 @@
+import { lazy } from 'react';
+
+// project imports
+import Loadable from 'ui-component/Loadable';
+import MinimalLayout from 'layout/MinimalLayout';
+
+// login option 3 routing
+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 ForgetPassword = Loadable(lazy(() => import('views/Authentication/Auth/ForgetPassword')));
+const ResetPassword = Loadable(lazy(() => import('views/Authentication/Auth/ResetPassword')));
+const Home = Loadable(lazy(() => import('views/Home')));
+const About = Loadable(lazy(() => import('views/About')));
+const NotFoundView = Loadable(lazy(() => import('views/Error')));
+
+// ==============================|| AUTHENTICATION ROUTING ||============================== //
+
+const OtherRoutes = {
+ path: '/',
+ element: ,
+ children: [
+ {
+ path: '',
+ element:
+ },
+ {
+ path: '/about',
+ element:
+ },
+ {
+ path: '/login',
+ element:
+ },
+ {
+ path: '/register',
+ element:
+ },
+ {
+ path: '/reset',
+ element:
+ },
+ {
+ path: '/user/reset',
+ element:
+ },
+ {
+ path: '/oauth/github',
+ element:
+ },
+ {
+ path: '/404',
+ element:
+ }
+ ]
+};
+
+export default OtherRoutes;
diff --git a/web/berry/src/routes/index.js b/web/berry/src/routes/index.js
new file mode 100644
index 00000000..e77c610a
--- /dev/null
+++ b/web/berry/src/routes/index.js
@@ -0,0 +1,11 @@
+import { useRoutes } from 'react-router-dom';
+
+// routes
+import MainRoutes from './MainRoutes';
+import OtherRoutes from './OtherRoutes';
+
+// ==============================|| ROUTING RENDER ||============================== //
+
+export default function ThemeRoutes() {
+ return useRoutes([MainRoutes, OtherRoutes]);
+}
diff --git a/web/berry/src/serviceWorker.js b/web/berry/src/serviceWorker.js
new file mode 100644
index 00000000..02320234
--- /dev/null
+++ b/web/berry/src/serviceWorker.js
@@ -0,0 +1,128 @@
+// This optional code is used to register a service worker.
+// register() is not called by default.
+
+// This lets the app load faster on subsequent visits in production, and gives
+// it offline capabilities. However, it also means that developers (and users)
+// will only see deployed updates on subsequent visits to a page, after all the
+// existing tabs open on the page have been closed, since previously cached
+// resources are updated in the background.
+
+// To learn more about the benefits of this model and instructions on how to
+// opt-in, read https://bit.ly/CRA-PWA
+
+const isLocalhost = Boolean(
+ window.location.hostname === 'localhost' ||
+ // [::1] is the IPv6 localhost address.
+ window.location.hostname === '[::1]' ||
+ // 127.0.0.0/8 are considered localhost for IPv4.
+ window.location.hostname.match(/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/)
+);
+
+function registerValidSW(swUrl, config) {
+ navigator.serviceWorker
+ .register(swUrl)
+ .then((registration) => {
+ registration.onupdatefound = () => {
+ const installingWorker = registration.installing;
+ if (installingWorker == null) {
+ return;
+ }
+ installingWorker.onstatechange = () => {
+ if (installingWorker.state === 'installed') {
+ if (navigator.serviceWorker.controller) {
+ // At this point, the updated precached content has been fetched,
+ // but the previous service worker will still serve the older
+ // content until all client tabs are closed.
+ console.log('New content is available and will be used when all tabs for this page are closed. See https://bit.ly/CRA-PWA.');
+
+ // Execute callback
+ if (config && config.onUpdate) {
+ config.onUpdate(registration);
+ }
+ } else {
+ // At this point, everything has been precached.
+ // It's the perfect time to display a
+ // "Content is cached for offline use." message.
+ console.log('Content is cached for offline use.');
+
+ // Execute callback
+ if (config && config.onSuccess) {
+ config.onSuccess(registration);
+ }
+ }
+ }
+ };
+ };
+ })
+ .catch((error) => {
+ console.error('Error during service worker registration:', error);
+ });
+}
+
+function checkValidServiceWorker(swUrl, config) {
+ // Check if the service worker can be found. If it can't reload the page.
+ fetch(swUrl, {
+ headers: { 'Service-Worker': 'script' }
+ })
+ .then((response) => {
+ // Ensure service worker exists, and that we really are getting a JS file.
+ const contentType = response.headers.get('content-type');
+ if (response.status === 404 || (contentType != null && contentType.indexOf('javascript') === -1)) {
+ // No service worker found. Probably a different app. Reload the page.
+ navigator.serviceWorker.ready.then((registration) => {
+ registration.unregister().then(() => {
+ window.location.reload();
+ });
+ });
+ } else {
+ // Service worker found. Proceed as normal.
+ registerValidSW(swUrl, config);
+ }
+ })
+ .catch(() => {
+ console.log('No internet connection found. App is running in offline mode.');
+ });
+}
+
+export function register(config) {
+ if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
+ // The URL constructor is available in all browsers that support SW.
+ const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
+ if (publicUrl.origin !== window.location.origin) {
+ // Our service worker won't work if PUBLIC_URL is on a different origin
+ // from what our page is served on. This might happen if a CDN is used to
+ // serve assets; see https://github.com/facebook/create-react-app/issues/2374
+ return;
+ }
+
+ window.addEventListener('load', () => {
+ const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
+
+ if (isLocalhost) {
+ // This is running on localhost. Let's check if a service worker still exists or not.
+ checkValidServiceWorker(swUrl, config);
+
+ // Add some additional logging to localhost, pointing developers to the
+ // service worker/PWA documentation.
+ navigator.serviceWorker.ready.then(() => {
+ console.log('This web app is being served cache-first by a service worker. To learn more, visit https://bit.ly/CRA-PWA');
+ });
+ } else {
+ // Is not localhost. Just register service worker
+ registerValidSW(swUrl, config);
+ }
+ });
+ }
+}
+
+export function unregister() {
+ if ('serviceWorker' in navigator) {
+ navigator.serviceWorker.ready
+ .then((registration) => {
+ registration.unregister();
+ })
+ .catch((error) => {
+ console.error(error.message);
+ });
+ }
+}
diff --git a/web/berry/src/store/accountReducer.js b/web/berry/src/store/accountReducer.js
new file mode 100644
index 00000000..5414bb97
--- /dev/null
+++ b/web/berry/src/store/accountReducer.js
@@ -0,0 +1,24 @@
+import * as actionTypes from './actions';
+
+export const initialState = {
+ user: undefined
+};
+
+const accountReducer = (state = initialState, action) => {
+ switch (action.type) {
+ case actionTypes.LOGIN:
+ return {
+ ...state,
+ user: action.payload
+ };
+ case actionTypes.LOGOUT:
+ return {
+ ...state,
+ user: undefined
+ };
+ default:
+ return state;
+ }
+};
+
+export default accountReducer;
diff --git a/web/berry/src/store/actions.js b/web/berry/src/store/actions.js
new file mode 100644
index 00000000..221e8578
--- /dev/null
+++ b/web/berry/src/store/actions.js
@@ -0,0 +1,9 @@
+// action - customization reducer
+export const SET_MENU = '@customization/SET_MENU';
+export const MENU_TOGGLE = '@customization/MENU_TOGGLE';
+export const MENU_OPEN = '@customization/MENU_OPEN';
+export const SET_FONT_FAMILY = '@customization/SET_FONT_FAMILY';
+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';
diff --git a/web/berry/src/store/constant.js b/web/berry/src/store/constant.js
new file mode 100644
index 00000000..75da000c
--- /dev/null
+++ b/web/berry/src/store/constant.js
@@ -0,0 +1,4 @@
+// theme constant
+export const gridSpacing = 3;
+export const drawerWidth = 260;
+export const appDrawerWidth = 320;
diff --git a/web/berry/src/store/customizationReducer.js b/web/berry/src/store/customizationReducer.js
new file mode 100644
index 00000000..bd8e5f00
--- /dev/null
+++ b/web/berry/src/store/customizationReducer.js
@@ -0,0 +1,46 @@
+// project imports
+import config from 'config';
+
+// action - state management
+import * as actionTypes from './actions';
+
+export const initialState = {
+ isOpen: [], // for active default menu
+ defaultId: 'default',
+ fontFamily: config.fontFamily,
+ borderRadius: config.borderRadius,
+ opened: true
+};
+
+// ==============================|| CUSTOMIZATION REDUCER ||============================== //
+
+const customizationReducer = (state = initialState, action) => {
+ let id;
+ switch (action.type) {
+ case actionTypes.MENU_OPEN:
+ id = action.id;
+ return {
+ ...state,
+ isOpen: [id]
+ };
+ case actionTypes.SET_MENU:
+ return {
+ ...state,
+ opened: action.opened
+ };
+ case actionTypes.SET_FONT_FAMILY:
+ return {
+ ...state,
+ fontFamily: action.fontFamily
+ };
+ case actionTypes.SET_BORDER_RADIUS:
+ return {
+ ...state,
+ borderRadius: action.borderRadius
+ };
+ default:
+ return state;
+ }
+};
+
+export default customizationReducer;
diff --git a/web/berry/src/store/index.js b/web/berry/src/store/index.js
new file mode 100644
index 00000000..b9ec2a68
--- /dev/null
+++ b/web/berry/src/store/index.js
@@ -0,0 +1,9 @@
+import { createStore } from 'redux';
+import reducer from './reducer';
+
+// ==============================|| REDUX - MAIN STORE ||============================== //
+
+const store = createStore(reducer);
+const persister = 'Free';
+
+export { store, persister };
diff --git a/web/berry/src/store/reducer.js b/web/berry/src/store/reducer.js
new file mode 100644
index 00000000..220f585f
--- /dev/null
+++ b/web/berry/src/store/reducer.js
@@ -0,0 +1,16 @@
+import { combineReducers } from 'redux';
+
+// reducer import
+import customizationReducer from './customizationReducer';
+import accountReducer from './accountReducer';
+import siteInfoReducer from './siteInfoReducer';
+
+// ==============================|| COMBINE REDUCER ||============================== //
+
+const reducer = combineReducers({
+ customization: customizationReducer,
+ account: accountReducer,
+ siteInfo: siteInfoReducer
+});
+
+export default reducer;
diff --git a/web/berry/src/store/siteInfoReducer.js b/web/berry/src/store/siteInfoReducer.js
new file mode 100644
index 00000000..e14bc245
--- /dev/null
+++ b/web/berry/src/store/siteInfoReducer.js
@@ -0,0 +1,18 @@
+import config from 'config';
+import * as actionTypes from './actions';
+
+export const initialState = config.siteInfo;
+
+const siteInfoReducer = (state = initialState, action) => {
+ switch (action.type) {
+ case actionTypes.SET_SITE_INFO:
+ return {
+ ...state,
+ ...action.payload
+ };
+ default:
+ return state;
+ }
+};
+
+export default siteInfoReducer;
diff --git a/web/berry/src/themes/compStyleOverride.js b/web/berry/src/themes/compStyleOverride.js
new file mode 100644
index 00000000..b6e87e01
--- /dev/null
+++ b/web/berry/src/themes/compStyleOverride.js
@@ -0,0 +1,256 @@
+export default function componentStyleOverrides(theme) {
+ const bgColor = theme.colors?.grey50;
+ return {
+ MuiButton: {
+ styleOverrides: {
+ root: {
+ fontWeight: 500,
+ borderRadius: '4px',
+ '&.Mui-disabled': {
+ color: theme.colors?.grey600
+ }
+ }
+ }
+ },
+ MuiMenuItem: {
+ styleOverrides: {
+ root: {
+ '&:hover': {
+ backgroundColor: theme.colors?.grey100
+ }
+ }
+ }
+ }, //MuiAutocomplete-popper MuiPopover-root
+ MuiAutocomplete: {
+ styleOverrides: {
+ popper: {
+ // 继承 MuiPopover-root
+ boxShadow: '0px 5px 5px -3px rgba(0,0,0,0.2),0px 8px 10px 1px rgba(0,0,0,0.14),0px 3px 14px 2px rgba(0,0,0,0.12)',
+ borderRadius: '12px',
+ color: '#364152'
+ },
+ listbox: {
+ // 继承 MuiPopover-root
+ padding: '0px',
+ paddingTop: '8px',
+ paddingBottom: '8px'
+ },
+ option: {
+ fontSize: '16px',
+ fontWeight: '400',
+ lineHeight: '1.334em',
+ alignItems: 'center',
+ paddingTop: '6px',
+ paddingBottom: '6px',
+ paddingLeft: '16px',
+ paddingRight: '16px'
+ }
+ }
+ },
+ MuiIconButton: {
+ styleOverrides: {
+ root: {
+ color: theme.darkTextPrimary,
+ '&:hover': {
+ backgroundColor: theme.colors?.grey200
+ }
+ }
+ }
+ },
+ MuiPaper: {
+ defaultProps: {
+ elevation: 0
+ },
+ styleOverrides: {
+ root: {
+ backgroundImage: 'none'
+ },
+ rounded: {
+ borderRadius: `${theme?.customization?.borderRadius}px`
+ }
+ }
+ },
+ MuiCardHeader: {
+ styleOverrides: {
+ root: {
+ color: theme.colors?.textDark,
+ padding: '24px'
+ },
+ title: {
+ fontSize: '1.125rem'
+ }
+ }
+ },
+ MuiCardContent: {
+ styleOverrides: {
+ root: {
+ padding: '24px'
+ }
+ }
+ },
+ MuiCardActions: {
+ styleOverrides: {
+ root: {
+ padding: '24px'
+ }
+ }
+ },
+ MuiListItemButton: {
+ styleOverrides: {
+ root: {
+ color: theme.darkTextPrimary,
+ paddingTop: '10px',
+ paddingBottom: '10px',
+ '&.Mui-selected': {
+ color: theme.menuSelected,
+ backgroundColor: theme.menuSelectedBack,
+ '&:hover': {
+ backgroundColor: theme.menuSelectedBack
+ },
+ '& .MuiListItemIcon-root': {
+ color: theme.menuSelected
+ }
+ },
+ '&:hover': {
+ backgroundColor: theme.menuSelectedBack,
+ color: theme.menuSelected,
+ '& .MuiListItemIcon-root': {
+ color: theme.menuSelected
+ }
+ }
+ }
+ }
+ },
+ MuiListItemIcon: {
+ styleOverrides: {
+ root: {
+ color: theme.darkTextPrimary,
+ minWidth: '36px'
+ }
+ }
+ },
+ MuiListItemText: {
+ styleOverrides: {
+ primary: {
+ color: theme.textDark
+ }
+ }
+ },
+ MuiInputBase: {
+ styleOverrides: {
+ input: {
+ color: theme.textDark,
+ '&::placeholder': {
+ color: theme.darkTextSecondary,
+ fontSize: '0.875rem'
+ }
+ }
+ }
+ },
+ MuiOutlinedInput: {
+ styleOverrides: {
+ root: {
+ background: bgColor,
+ borderRadius: `${theme?.customization?.borderRadius}px`,
+ '& .MuiOutlinedInput-notchedOutline': {
+ borderColor: theme.colors?.grey400
+ },
+ '&:hover $notchedOutline': {
+ borderColor: theme.colors?.primaryLight
+ },
+ '&.MuiInputBase-multiline': {
+ padding: 1
+ }
+ },
+ input: {
+ fontWeight: 500,
+ background: bgColor,
+ padding: '15.5px 14px',
+ borderRadius: `${theme?.customization?.borderRadius}px`,
+ '&.MuiInputBase-inputSizeSmall': {
+ padding: '10px 14px',
+ '&.MuiInputBase-inputAdornedStart': {
+ paddingLeft: 0
+ }
+ }
+ },
+ inputAdornedStart: {
+ paddingLeft: 4
+ },
+ notchedOutline: {
+ borderRadius: `${theme?.customization?.borderRadius}px`
+ }
+ }
+ },
+ MuiSlider: {
+ styleOverrides: {
+ root: {
+ '&.Mui-disabled': {
+ color: theme.colors?.grey300
+ }
+ },
+ mark: {
+ backgroundColor: theme.paper,
+ width: '4px'
+ },
+ valueLabel: {
+ color: theme?.colors?.primaryLight
+ }
+ }
+ },
+ MuiDivider: {
+ styleOverrides: {
+ root: {
+ borderColor: theme.divider,
+ opacity: 1
+ }
+ }
+ },
+ MuiAvatar: {
+ styleOverrides: {
+ root: {
+ color: theme.colors?.primaryDark,
+ background: theme.colors?.primary200
+ }
+ }
+ },
+ MuiChip: {
+ styleOverrides: {
+ root: {
+ '&.MuiChip-deletable .MuiChip-deleteIcon': {
+ color: 'inherit'
+ }
+ }
+ }
+ },
+ MuiTableCell: {
+ styleOverrides: {
+ root: {
+ borderBottom: '1px solid rgb(241, 243, 244)',
+ textAlign: 'center'
+ },
+ head: {
+ color: theme.darkTextSecondary,
+ backgroundColor: 'rgb(244, 246, 248)'
+ }
+ }
+ },
+ MuiTableRow: {
+ styleOverrides: {
+ root: {
+ '&:hover': {
+ backgroundColor: 'rgb(244, 246, 248)'
+ }
+ }
+ }
+ },
+ MuiTooltip: {
+ styleOverrides: {
+ tooltip: {
+ color: theme.paper,
+ background: theme.colors?.grey700
+ }
+ }
+ }
+ };
+}
diff --git a/web/berry/src/themes/index.js b/web/berry/src/themes/index.js
new file mode 100644
index 00000000..6e694aa6
--- /dev/null
+++ b/web/berry/src/themes/index.js
@@ -0,0 +1,55 @@
+import { createTheme } from '@mui/material/styles';
+
+// assets
+import colors from 'assets/scss/_themes-vars.module.scss';
+
+// project imports
+import componentStyleOverrides from './compStyleOverride';
+import themePalette from './palette';
+import themeTypography from './typography';
+
+/**
+ * Represent theme style and structure as per Material-UI
+ * @param {JsonObject} customization customization parameter object
+ */
+
+export const theme = (customization) => {
+ const color = colors;
+
+ 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,
+ customization
+ };
+
+ const themeOptions = {
+ direction: 'ltr',
+ palette: themePalette(themeOption),
+ mixins: {
+ toolbar: {
+ minHeight: '48px',
+ padding: '16px',
+ '@media (min-width: 600px)': {
+ minHeight: '48px'
+ }
+ }
+ },
+ typography: themeTypography(themeOption)
+ };
+
+ const themes = createTheme(themeOptions);
+ themes.components = componentStyleOverrides(themeOption);
+
+ return themes;
+};
+
+export default theme;
diff --git a/web/berry/src/themes/palette.js b/web/berry/src/themes/palette.js
new file mode 100644
index 00000000..09768555
--- /dev/null
+++ b/web/berry/src/themes/palette.js
@@ -0,0 +1,73 @@
+/**
+ * Color intention that you want to used in your theme
+ * @param {JsonObject} theme Theme customization object
+ */
+
+export default function themePalette(theme) {
+ return {
+ mode: 'light',
+ common: {
+ black: theme.colors?.darkPaper
+ },
+ primary: {
+ light: theme.colors?.primaryLight,
+ main: theme.colors?.primaryMain,
+ dark: theme.colors?.primaryDark,
+ 200: theme.colors?.primary200,
+ 800: theme.colors?.primary800
+ },
+ secondary: {
+ light: theme.colors?.secondaryLight,
+ main: theme.colors?.secondaryMain,
+ dark: theme.colors?.secondaryDark,
+ 200: theme.colors?.secondary200,
+ 800: theme.colors?.secondary800
+ },
+ error: {
+ light: theme.colors?.errorLight,
+ main: theme.colors?.errorMain,
+ dark: theme.colors?.errorDark
+ },
+ orange: {
+ light: theme.colors?.orangeLight,
+ main: theme.colors?.orangeMain,
+ dark: theme.colors?.orangeDark
+ },
+ warning: {
+ light: theme.colors?.warningLight,
+ main: theme.colors?.warningMain,
+ dark: theme.colors?.warningDark
+ },
+ success: {
+ light: theme.colors?.successLight,
+ 200: theme.colors?.success200,
+ main: theme.colors?.successMain,
+ dark: theme.colors?.successDark
+ },
+ grey: {
+ 50: theme.colors?.grey50,
+ 100: theme.colors?.grey100,
+ 500: theme.darkTextSecondary,
+ 600: theme.heading,
+ 700: theme.darkTextPrimary,
+ 900: theme.textDark
+ },
+ dark: {
+ light: theme.colors?.darkTextPrimary,
+ main: theme.colors?.darkLevel1,
+ dark: theme.colors?.darkLevel2,
+ 800: theme.colors?.darkBackground,
+ 900: theme.colors?.darkPaper
+ },
+ text: {
+ primary: theme.darkTextPrimary,
+ secondary: theme.darkTextSecondary,
+ dark: theme.textDark,
+ hint: theme.colors?.grey100
+ },
+ background: {
+ paper: theme.paper,
+ default: theme.backgroundDefault
+ }
+ };
+}
diff --git a/web/berry/src/themes/typography.js b/web/berry/src/themes/typography.js
new file mode 100644
index 00000000..24bfabb9
--- /dev/null
+++ b/web/berry/src/themes/typography.js
@@ -0,0 +1,137 @@
+/**
+ * Typography used in theme
+ * @param {JsonObject} theme theme customization object
+ */
+
+export default function themeTypography(theme) {
+ return {
+ fontFamily: theme?.customization?.fontFamily,
+ h6: {
+ fontWeight: 500,
+ color: theme.heading,
+ fontSize: '0.75rem'
+ },
+ h5: {
+ fontSize: '0.875rem',
+ color: theme.heading,
+ fontWeight: 500
+ },
+ h4: {
+ fontSize: '1rem',
+ color: theme.heading,
+ fontWeight: 600
+ },
+ h3: {
+ fontSize: '1.25rem',
+ color: theme.heading,
+ fontWeight: 600
+ },
+ h2: {
+ fontSize: '1.5rem',
+ color: theme.heading,
+ fontWeight: 700
+ },
+ h1: {
+ fontSize: '2.125rem',
+ color: theme.heading,
+ fontWeight: 700
+ },
+ subtitle1: {
+ fontSize: '0.875rem',
+ fontWeight: 500,
+ color: theme.textDark
+ },
+ subtitle2: {
+ fontSize: '0.75rem',
+ fontWeight: 400,
+ color: theme.darkTextSecondary
+ },
+ caption: {
+ fontSize: '0.75rem',
+ color: theme.darkTextSecondary,
+ fontWeight: 400
+ },
+ body1: {
+ fontSize: '0.875rem',
+ fontWeight: 400,
+ lineHeight: '1.334em'
+ },
+ body2: {
+ letterSpacing: '0em',
+ fontWeight: 400,
+ lineHeight: '1.5em',
+ color: theme.darkTextPrimary
+ },
+ button: {
+ textTransform: 'capitalize'
+ },
+ customInput: {
+ marginTop: 1,
+ marginBottom: 1,
+ '& > label': {
+ top: 23,
+ left: 0,
+ color: theme.grey500,
+ '&[data-shrink="false"]': {
+ top: 5
+ }
+ },
+ '& > div > input': {
+ padding: '30.5px 14px 11.5px !important'
+ },
+ '& legend': {
+ display: 'none'
+ },
+ '& fieldset': {
+ top: 0
+ }
+ },
+ otherInput: {
+ marginTop: 1,
+ marginBottom: 1
+ },
+ mainContent: {
+ backgroundColor: theme.background,
+ width: '100%',
+ minHeight: 'calc(100vh - 88px)',
+ flexGrow: 1,
+ padding: '20px',
+ marginTop: '88px',
+ marginRight: '20px',
+ borderRadius: `${theme?.customization?.borderRadius}px`
+ },
+ menuCaption: {
+ fontSize: '0.875rem',
+ fontWeight: 500,
+ color: theme.heading,
+ padding: '6px',
+ textTransform: 'capitalize',
+ marginTop: '10px'
+ },
+ subMenuCaption: {
+ fontSize: '0.6875rem',
+ fontWeight: 500,
+ color: theme.darkTextSecondary,
+ textTransform: 'capitalize'
+ },
+ commonAvatar: {
+ cursor: 'pointer',
+ borderRadius: '8px'
+ },
+ smallAvatar: {
+ width: '22px',
+ height: '22px',
+ fontSize: '1rem'
+ },
+ mediumAvatar: {
+ width: '34px',
+ height: '34px',
+ fontSize: '1.2rem'
+ },
+ largeAvatar: {
+ width: '44px',
+ height: '44px',
+ fontSize: '1.5rem'
+ }
+ };
+}
diff --git a/web/berry/src/ui-component/AdminContainer.js b/web/berry/src/ui-component/AdminContainer.js
new file mode 100644
index 00000000..eff42a22
--- /dev/null
+++ b/web/berry/src/ui-component/AdminContainer.js
@@ -0,0 +1,11 @@
+import { styled } from '@mui/material/styles';
+import { Container } from '@mui/material';
+
+const AdminContainer = styled(Container)(({ theme }) => ({
+ [theme.breakpoints.down('md')]: {
+ paddingLeft: '0px',
+ paddingRight: '0px'
+ }
+}));
+
+export default AdminContainer;
diff --git a/web/berry/src/ui-component/Footer.js b/web/berry/src/ui-component/Footer.js
new file mode 100644
index 00000000..38f61993
--- /dev/null
+++ b/web/berry/src/ui-component/Footer.js
@@ -0,0 +1,37 @@
+// material-ui
+import { Link, Container, Box } from '@mui/material';
+import React from 'react';
+import { useSelector } from 'react-redux';
+
+// ==============================|| FOOTER - AUTHENTICATION 2 & 3 ||============================== //
+
+const Footer = () => {
+ const siteInfo = useSelector((state) => state.siteInfo);
+
+ return (
+
+
+ {siteInfo.footer_html ? (
+
+ ) : (
+ <>
+
+ {siteInfo.system_name} {process.env.REACT_APP_VERSION}{' '}
+
+ 由{' '}
+
+ JustSong
+ {' '}
+ 构建,主题 berry 来自{' '}
+
+ MartialBE
+ {' '},源代码遵循
+ MIT 协议
+ >
+ )}
+
+
+ );
+};
+
+export default Footer;
diff --git a/web/berry/src/ui-component/Label.js b/web/berry/src/ui-component/Label.js
new file mode 100644
index 00000000..715c6248
--- /dev/null
+++ b/web/berry/src/ui-component/Label.js
@@ -0,0 +1,158 @@
+/*
+ * Label.js
+ *
+ * This file uses code from the Minimal UI project, available at
+ * https://github.com/minimal-ui-kit/material-kit-react/blob/main/src/components/label/label.jsx
+ *
+ * Minimal UI is licensed under the MIT License. A copy of the license is included below:
+ *
+ * MIT License
+ *
+ * Copyright (c) 2021 Minimal UI (https://minimals.cc/)
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+import PropTypes from 'prop-types';
+import { forwardRef } from 'react';
+
+import Box from '@mui/material/Box';
+import { useTheme } from '@mui/material/styles';
+import { alpha, styled } from '@mui/material/styles';
+
+// ----------------------------------------------------------------------
+
+const Label = forwardRef(({ children, color = 'default', variant = 'soft', startIcon, endIcon, sx, ...other }, ref) => {
+ const theme = useTheme();
+
+ const iconStyles = {
+ width: 16,
+ height: 16,
+ '& svg, img': { width: 1, height: 1, objectFit: 'cover' }
+ };
+
+ return (
+
+ {startIcon && {startIcon} }
+
+ {children}
+
+ {endIcon && {endIcon} }
+
+ );
+});
+
+Label.propTypes = {
+ children: PropTypes.node,
+ endIcon: PropTypes.object,
+ startIcon: PropTypes.object,
+ sx: PropTypes.object,
+ variant: PropTypes.oneOf(['filled', 'outlined', 'ghost', 'soft']),
+ color: PropTypes.oneOf(['default', 'primary', 'secondary', 'info', 'success', 'warning', 'orange', 'error'])
+};
+
+export default Label;
+
+const StyledLabel = styled(Box)(({ theme, ownerState }) => {
+ // const lightMode = theme.palette.mode === 'light';
+
+ const filledVariant = ownerState.variant === 'filled';
+
+ const outlinedVariant = ownerState.variant === 'outlined';
+
+ const softVariant = ownerState.variant === 'soft';
+
+ const ghostVariant = ownerState.variant === 'ghost';
+
+ const defaultStyle = {
+ ...(ownerState.color === 'default' && {
+ // FILLED
+ ...(filledVariant && {
+ color: theme.palette.grey[300],
+ backgroundColor: theme.palette.text.primary
+ }),
+ // OUTLINED
+ ...(outlinedVariant && {
+ color: theme.palette.grey[500],
+ border: `2px solid ${theme.palette.grey[500]}`
+ }),
+ // SOFT
+ ...(softVariant && {
+ color: theme.palette.text.secondary,
+ backgroundColor: alpha(theme.palette.grey[500], 0.16)
+ })
+ })
+ };
+
+ const colorStyle = {
+ ...(ownerState.color !== 'default' && {
+ // FILLED
+ ...(filledVariant && {
+ color: theme.palette.background.paper,
+ backgroundColor: theme.palette[ownerState.color]?.main
+ }),
+ // OUTLINED
+ ...(outlinedVariant && {
+ backgroundColor: 'transparent',
+ color: theme.palette[ownerState.color]?.main,
+ border: `2px solid ${theme.palette[ownerState.color]?.main}`
+ }),
+ // SOFT
+ ...(softVariant && {
+ color: theme.palette[ownerState.color]['dark'],
+ backgroundColor: alpha(theme.palette[ownerState.color]?.main, 0.16)
+ }),
+ // GHOST
+ ...(ghostVariant && {
+ color: theme.palette[ownerState.color]?.main
+ })
+ })
+ };
+
+ return {
+ height: 24,
+ minWidth: 24,
+ lineHeight: 0,
+ borderRadius: 6,
+ cursor: 'default',
+ alignItems: 'center',
+ whiteSpace: 'nowrap',
+ display: 'inline-flex',
+ justifyContent: 'center',
+ // textTransform: 'capitalize',
+ padding: theme.spacing(0, 0.75),
+ fontSize: theme.typography.pxToRem(12),
+ fontWeight: theme.typography.fontWeightBold,
+ transition: theme.transitions.create('all', {
+ duration: theme.transitions.duration.shorter
+ }),
+ ...defaultStyle,
+ ...colorStyle
+ };
+});
diff --git a/web/berry/src/ui-component/Loadable.js b/web/berry/src/ui-component/Loadable.js
new file mode 100644
index 00000000..01de3f90
--- /dev/null
+++ b/web/berry/src/ui-component/Loadable.js
@@ -0,0 +1,15 @@
+import { Suspense } from 'react';
+
+// project imports
+import Loader from './Loader';
+
+// ==============================|| LOADABLE - LAZY LOADING ||============================== //
+
+const Loadable = (Component) => (props) =>
+ (
+ }>
+
+
+ );
+
+export default Loadable;
diff --git a/web/berry/src/ui-component/Loader.js b/web/berry/src/ui-component/Loader.js
new file mode 100644
index 00000000..9072dcdb
--- /dev/null
+++ b/web/berry/src/ui-component/Loader.js
@@ -0,0 +1,21 @@
+// material-ui
+import LinearProgress from '@mui/material/LinearProgress';
+import { styled } from '@mui/material/styles';
+
+// styles
+const LoaderWrapper = styled('div')({
+ position: 'fixed',
+ top: 0,
+ left: 0,
+ zIndex: 1301,
+ width: '100%'
+});
+
+// ==============================|| LOADER ||============================== //
+const Loader = () => (
+
+
+
+);
+
+export default Loader;
diff --git a/web/berry/src/ui-component/Logo.js b/web/berry/src/ui-component/Logo.js
new file mode 100644
index 00000000..9c65825b
--- /dev/null
+++ b/web/berry/src/ui-component/Logo.js
@@ -0,0 +1,21 @@
+// material-ui
+import logo from 'assets/images/logo.svg';
+import { useSelector } from 'react-redux';
+
+/**
+ * if you want to use image instead of