diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..472010b
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,35 @@
+# compiled output
+/dist
+/node_modules
+
+# Logs
+logs
+*.log
+npm-debug.log*
+pnpm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+lerna-debug.log*
+
+# OS
+.DS_Store
+
+# Tests
+/coverage
+/.nyc_output
+
+# IDEs and editors
+/.idea
+.project
+.classpath
+.c9/
+*.launch
+.settings/
+*.sublime-workspace
+
+# IDE - VSCode
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
diff --git a/README.md b/README.md
deleted file mode 100644
index 045fdf6..0000000
--- a/README.md
+++ /dev/null
@@ -1 +0,0 @@
-# nestjs-admin
\ No newline at end of file
diff --git a/admin/.env b/admin/.env
new file mode 100644
index 0000000..1f4fd04
--- /dev/null
+++ b/admin/.env
@@ -0,0 +1,2 @@
+VITE_APP_TITLE=nestjs-admin
+VITE_APP_BASE_URL=http://127.0.0.1:3000/api/admin
diff --git a/admin/.eslintignore b/admin/.eslintignore
new file mode 100644
index 0000000..3c3629e
--- /dev/null
+++ b/admin/.eslintignore
@@ -0,0 +1 @@
+node_modules
diff --git a/admin/.eslintrc b/admin/.eslintrc
new file mode 100644
index 0000000..8067077
--- /dev/null
+++ b/admin/.eslintrc
@@ -0,0 +1,34 @@
+{
+ "root": true,
+ "env": {
+ "browser": true,
+ "commonjs": true,
+ "es6": true,
+ "node": true,
+ "jest": true
+ },
+ "parser": "@typescript-eslint/parser",
+ "extends": [
+ "prettier",
+ "plugin:@typescript-eslint/recommended",
+ "plugin:react/recommended",
+ "plugin:react-hooks/recommended"
+ ],
+ "parserOptions": {
+ "ecmaFeatures": {
+ "experimentalObjectRestSpread": true,
+ "jsx": true
+ },
+ "sourceType": "module"
+ },
+ "settings": {
+ "react": {
+ "version": "16.8"
+ }
+ },
+ "plugins": ["react", "babel", "@typescript-eslint/eslint-plugin"],
+ "rules": {
+ "react/display-name": 0,
+ "react/prop-types": 0
+ }
+}
diff --git a/admin/.gitignore b/admin/.gitignore
new file mode 100644
index 0000000..dfb4167
--- /dev/null
+++ b/admin/.gitignore
@@ -0,0 +1,17 @@
+.DS_Store
+node_modules/
+/dist/
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+/test/unit/coverage/
+/test/e2e/reports/
+selenium-debug.log
+
+# Editor directories and files
+.idea
+.vscode
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
diff --git a/admin/.prettierrc b/admin/.prettierrc
new file mode 100644
index 0000000..e8eed13
--- /dev/null
+++ b/admin/.prettierrc
@@ -0,0 +1,7 @@
+{
+ "semi": true,
+ "singleQuote": true,
+ "jsxSingleQuote": false,
+ "useTabs": false,
+ "tabWidth": 2
+}
\ No newline at end of file
diff --git a/admin/.stylelintignore b/admin/.stylelintignore
new file mode 100644
index 0000000..ec719bc
--- /dev/null
+++ b/admin/.stylelintignore
@@ -0,0 +1,4 @@
+**/*.ts
+**/*.tsx
+**/*.jsx
+**/*.js
diff --git a/admin/.stylelintrc b/admin/.stylelintrc
new file mode 100644
index 0000000..c09537c
--- /dev/null
+++ b/admin/.stylelintrc
@@ -0,0 +1,17 @@
+{
+ "extends": ["stylelint-config-standard", "stylelint-config-prettier"],
+ "customSyntax": "postcss-less",
+ "rules": {
+ "selector-class-pattern": null,
+ "no-descending-specificity": null,
+ "no-duplicate-selectors": null,
+ "color-function-notation": null,
+ "font-family-no-missing-generic-family-keyword": null,
+ "selector-pseudo-class-no-unknown": [
+ true,
+ {
+ "ignorePseudoClasses": ["global"]
+ }
+ ]
+ }
+}
diff --git a/admin/README.md b/admin/README.md
new file mode 100644
index 0000000..e69de29
diff --git a/admin/favicon.ico b/admin/favicon.ico
new file mode 100644
index 0000000..d34f764
Binary files /dev/null and b/admin/favicon.ico differ
diff --git a/admin/index.html b/admin/index.html
new file mode 100644
index 0000000..8c66f0d
--- /dev/null
+++ b/admin/index.html
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+ <%- title %>
+
+
+
+
+
+
diff --git a/admin/package.json b/admin/package.json
new file mode 100644
index 0000000..055ecde
--- /dev/null
+++ b/admin/package.json
@@ -0,0 +1,83 @@
+{
+ "name": "nestjs-admin",
+ "version": "1.0.0",
+ "description": "",
+ "scripts": {
+ "start": "vite",
+ "dev": "vite",
+ "preview": "vite preview",
+ "build": "vite build",
+ "eslint": "eslint src/ --ext .ts,.tsx,.js,.jsx --fix --cache",
+ "stylelint": "stylelint 'src/**/*.less' 'src/**/*.css' --fix --cache"
+ },
+ "dependencies": {
+ "@antv/data-set": "^0.11.8",
+ "@arco-design/color": "^0.4.0",
+ "@arco-design/web-react": "^2.32.2",
+ "@arco-plugins/vite-react": "^1.0.5",
+ "@arco-themes/react-arco-pro": "^0.0.7",
+ "@loadable/component": "^5.13.2",
+ "@turf/turf": "^6.5.0",
+ "arco-design-pro": "^2.8.0",
+ "axios": "^0.24.0",
+ "classnames": "^2.3.1",
+ "copy-to-clipboard": "^3.3.1",
+ "lodash": "^4.17.21",
+ "nprogress": "^0.2.0",
+ "query-string": "^6.13.8",
+ "react": "^17.0.2",
+ "react-color": "^2.18.1",
+ "react-dom": "^17.0.2",
+ "react-query": "^3.39.3",
+ "react-router": "^5.2.0",
+ "react-router-dom": "^5.2.0",
+ "zustand": "^4.5.2"
+ },
+ "devDependencies": {
+ "@arco-design/webpack-plugin": "^1.6.0",
+ "@arco-plugins/vite-plugin-svgr": "^0.7.2",
+ "@svgr/webpack": "^5.5.0",
+ "@types/react": "^17.0.0",
+ "@types/react-dom": "^17.0.0",
+ "@typescript-eslint/eslint-plugin": "^5.4.0",
+ "@typescript-eslint/parser": "^5.4.0",
+ "@vitejs/plugin-react": "^1.1.0",
+ "eslint": "^8.10.0",
+ "eslint-config-prettier": "^8.3.0",
+ "eslint-plugin-babel": "^5.3.1",
+ "eslint-plugin-prettier": "^4.0.0",
+ "eslint-plugin-react": "^7.27.1",
+ "eslint-plugin-react-hooks": "^4.3.0",
+ "less": "^4.1.2",
+ "less-loader": "7.3.0",
+ "postcss-less": "4",
+ "prettier": "^2.4.1",
+ "pretty-quick": "^3.1.2",
+ "stylelint": "^14.1.0",
+ "stylelint-config-prettier": "^9.0.3",
+ "stylelint-config-standard": "^24.0.0",
+ "typescript": "^4.5.2",
+ "vite": "^2.6.14",
+ "vite-plugin-html": "^3.2.0"
+ },
+ "browserslist": {
+ "production": [
+ ">0.2%",
+ "not dead",
+ "not op_mini all"
+ ],
+ "development": [
+ "last 1 chrome version",
+ "last 1 firefox version",
+ "last 1 safari version"
+ ]
+ },
+ "lint-staged": {
+ "*.{js,jsx,ts,tsx}": [
+ "eslint --fix --cache"
+ ],
+ "*.{css, less}": [
+ "stylelint --fix"
+ ]
+ }
+}
diff --git a/admin/src/api/auth/index.ts b/admin/src/api/auth/index.ts
new file mode 100644
index 0000000..5e2dbf1
--- /dev/null
+++ b/admin/src/api/auth/index.ts
@@ -0,0 +1,14 @@
+import request from '@/utils/request';
+import { LoginData } from '@/api/auth/type';
+
+/**
+ * 登录
+ * */
+export function login(data: LoginData) {
+ return request.post('/auth/login', data);
+}
+
+
+export function grantUserRoles(userId: number, data) {
+ return request.post('/auth/userRole/' + userId,data);
+}
diff --git a/admin/src/api/auth/type.ts b/admin/src/api/auth/type.ts
new file mode 100644
index 0000000..a979397
--- /dev/null
+++ b/admin/src/api/auth/type.ts
@@ -0,0 +1,4 @@
+export type LoginData = {
+ account: string,
+ password: string
+}
diff --git a/admin/src/api/common/index.ts b/admin/src/api/common/index.ts
new file mode 100644
index 0000000..6f9dcf4
--- /dev/null
+++ b/admin/src/api/common/index.ts
@@ -0,0 +1,7 @@
+import request from '@/utils/request';
+/**
+ * 文件上传
+ * */
+export function uploadFile(data) {
+ return request.post('/files', data);
+}
diff --git a/admin/src/api/common/type.ts b/admin/src/api/common/type.ts
new file mode 100644
index 0000000..e69de29
diff --git a/admin/src/api/config/index.ts b/admin/src/api/config/index.ts
new file mode 100644
index 0000000..f6c43e0
--- /dev/null
+++ b/admin/src/api/config/index.ts
@@ -0,0 +1,21 @@
+import request from '@/utils/request';
+
+export function createConfig(data) {
+ return request.post('/config', data);
+}
+
+export function getConfig(params) {
+ return request.get('/config', params);
+}
+
+export function updateConfig(data) {
+ return request.patch('/config/' + data.key, data);
+}
+
+export function deleteConfig(key: string) {
+ return request.delete('/config/' + key);
+}
+
+export function syncConfig() {
+ return request.post('/config/sync');
+}
diff --git a/admin/src/api/menu/index.ts b/admin/src/api/menu/index.ts
new file mode 100644
index 0000000..b2db098
--- /dev/null
+++ b/admin/src/api/menu/index.ts
@@ -0,0 +1,21 @@
+import request from '@/utils/request';
+
+export function createMenu(data) {
+ return request.post('/menu', data);
+}
+
+export function getMenu() {
+ return request.get('/menu');
+}
+
+export function getTreeMenu() {
+ return request.get('/menu/tree');
+}
+
+export function updateMenu(data) {
+ return request.patch('/menu/' + data.id, data);
+}
+
+export function deleteMenu(id: number) {
+ return request.delete('/menu/' + id);
+}
diff --git a/admin/src/api/permission/index.ts b/admin/src/api/permission/index.ts
new file mode 100644
index 0000000..78fab1d
--- /dev/null
+++ b/admin/src/api/permission/index.ts
@@ -0,0 +1,9 @@
+import request from '@/utils/request';
+
+export function grantPermission(id: number, menuIds: number[]) {
+ return request.patch('/permission/' + id, { menuIds });
+}
+
+export function getRoleMenuIds(id: number) {
+ return request.get('/permission/' + id + '/menu');
+}
diff --git a/admin/src/api/role/index.ts b/admin/src/api/role/index.ts
new file mode 100644
index 0000000..8febc65
--- /dev/null
+++ b/admin/src/api/role/index.ts
@@ -0,0 +1,21 @@
+import request from '@/utils/request';
+
+export function createRole(data) {
+ return request.post('/role', data);
+}
+
+export function getRole(params) {
+ return request.get('/role', params);
+}
+
+export function getAllRole() {
+ return request.get('/role/all');
+}
+
+export function updateRole(data) {
+ return request.patch('/role/' + data.id, data);
+}
+
+export function deleteRole(id: number) {
+ return request.delete('/role/' + id);
+}
diff --git a/admin/src/api/user/index.ts b/admin/src/api/user/index.ts
new file mode 100644
index 0000000..372bb8d
--- /dev/null
+++ b/admin/src/api/user/index.ts
@@ -0,0 +1,36 @@
+import request from '@/utils/request';
+
+/**
+ * 获取用户信息
+ * */
+export function userInfo() {
+ return request.get('/user/info');
+}
+
+/**
+ * 创建新用户
+ * */
+export function createUser(data) {
+ return request.post('/user', data);
+}
+
+/**
+ * 分页查询
+ * */
+export function getUsers(params) {
+ return request.get('/user', params);
+}
+
+/**
+ * 修改用户信息
+ * */
+export function updateUser(data) {
+ return request.patch('/user/' + data.id, data);
+}
+
+/**
+ * 删除用户
+ * */
+export function deleteUser(id: number) {
+ return request.delete('/user/' + id);
+}
diff --git a/admin/src/assets/dark.svg b/admin/src/assets/dark.svg
new file mode 100644
index 0000000..11383fd
--- /dev/null
+++ b/admin/src/assets/dark.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/admin/src/assets/light.svg b/admin/src/assets/light.svg
new file mode 100644
index 0000000..e8af81b
--- /dev/null
+++ b/admin/src/assets/light.svg
@@ -0,0 +1,39 @@
+
+
+
diff --git a/admin/src/assets/logo.png b/admin/src/assets/logo.png
new file mode 100644
index 0000000..e4cbe00
Binary files /dev/null and b/admin/src/assets/logo.png differ
diff --git a/admin/src/components/Footer/index.tsx b/admin/src/components/Footer/index.tsx
new file mode 100644
index 0000000..4036fe4
--- /dev/null
+++ b/admin/src/components/Footer/index.tsx
@@ -0,0 +1,16 @@
+import React from 'react';
+import { Layout } from '@arco-design/web-react';
+import { FooterProps } from '@arco-design/web-react/es/Layout/interface';
+import cs from 'classnames';
+import styles from './style/index.module.less';
+
+function Footer(props: FooterProps = {}) {
+ const { className, ...restProps } = props;
+ return (
+
+ {/*ultimate*/}
+
+ );
+}
+
+export default Footer;
diff --git a/admin/src/components/Footer/style/index.module.less b/admin/src/components/Footer/style/index.module.less
new file mode 100644
index 0000000..892f71c
--- /dev/null
+++ b/admin/src/components/Footer/style/index.module.less
@@ -0,0 +1,8 @@
+.footer {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ height: 40px;
+ text-align: center;
+ color: var(--color-text-2);
+}
diff --git a/admin/src/components/Icon/index.tsx b/admin/src/components/Icon/index.tsx
new file mode 100644
index 0000000..7025395
--- /dev/null
+++ b/admin/src/components/Icon/index.tsx
@@ -0,0 +1,14 @@
+import React from 'react';
+import * as icons from "@arco-design/web-react/icon";
+
+function Icon(props = {name: ''}) {
+ const {name} = props
+ const IconDom = icons[name]
+ return (
+ <>
+ {IconDom && }
+ >
+ )
+}
+
+export default Icon
diff --git a/admin/src/components/NavBar/IconButton.tsx b/admin/src/components/NavBar/IconButton.tsx
new file mode 100644
index 0000000..f37bb07
--- /dev/null
+++ b/admin/src/components/NavBar/IconButton.tsx
@@ -0,0 +1,21 @@
+import React, { forwardRef } from 'react';
+import { Button } from '@arco-design/web-react';
+import styles from './style/icon-button.module.less';
+import cs from 'classnames';
+
+function IconButton(props, ref) {
+ const { icon, className, ...rest } = props;
+
+ return (
+
+ );
+}
+
+export default forwardRef(IconButton);
diff --git a/admin/src/components/NavBar/index.tsx b/admin/src/components/NavBar/index.tsx
new file mode 100644
index 0000000..6fc6dd4
--- /dev/null
+++ b/admin/src/components/NavBar/index.tsx
@@ -0,0 +1,165 @@
+import React, { useContext } from 'react';
+import {
+ Tooltip,
+ Avatar,
+ Select,
+ Dropdown,
+ Menu,
+ Divider,
+ Message,
+ Button,
+} from '@arco-design/web-react';
+import {
+ IconLanguage,
+ IconSunFill,
+ IconMoonFill,
+ IconUser,
+ IconSettings,
+ IconPoweroff,
+ IconTag,
+ IconLoading,
+} from '@arco-design/web-react/icon';
+import { GlobalContext } from '@/context';
+import useLocale from '@/utils/useLocale';
+import IconButton from './IconButton';
+import Settings from '../Settings';
+import styles from './style/index.module.less';
+import defaultLocale from '@/locale';
+import useStorage from '@/utils/useStorage';
+import logoIcon from '@/assets/logo.png'
+import {useStore} from '@/store';
+
+function Navbar({ show }: { show: boolean }) {
+ const t = useLocale();
+ const { userInfo, userLoading } = useStore()
+ const [_, setUserStatus] = useStorage('userStatus');
+ const [role, setRole] = useStorage('userRole', 'admin');
+
+ const { setLang, lang, theme, setTheme } = useContext(GlobalContext);
+
+ function logout() {
+ setUserStatus('logout');
+ window.location.href = '/login';
+ }
+
+ function onMenuItemClick(key) {
+ if (key === 'logout') {
+ logout();
+ } else {
+ Message.info(`You clicked ${key}`);
+ }
+ }
+
+ if (!show) {
+ return (
+
+ } type="primary" size="large" />
+ }
+ />
+
+ );
+ }
+
+ const handleChangeRole = () => {
+ const newRole = role === 'admin' ? 'user' : 'admin';
+ setRole(newRole);
+ };
+
+ const droplist = (
+
+ );
+
+ return (
+
+
+
+

+
{import.meta.env.VITE_APP_TITLE}
+
+
+
+ -
+ } />}
+ options={[
+ { label: '中文', value: 'zh-CN' },
+ { label: 'English', value: 'en-US' },
+ ]}
+ value={lang}
+ triggerProps={{
+ autoAlignPopupWidth: false,
+ autoAlignPopupMinWidth: true,
+ position: 'br',
+ }}
+ trigger="hover"
+ onChange={(value) => {
+ setLang(value);
+ const nextLang = defaultLocale[value];
+ Message.info(`${nextLang['message.lang.tips']}${value}`);
+ }}
+ />
+
+ -
+
+ : }
+ onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}
+ />
+
+
+
+ {userInfo && (
+ -
+
+
+ {userLoading ? (
+
+ ) : (
+
+ )}
+
+
+
+ )}
+
+
+ );
+}
+
+export default Navbar;
diff --git a/admin/src/components/NavBar/style/icon-button.module.less b/admin/src/components/NavBar/style/icon-button.module.less
new file mode 100644
index 0000000..87345dd
--- /dev/null
+++ b/admin/src/components/NavBar/style/icon-button.module.less
@@ -0,0 +1,8 @@
+.icon-button {
+ font-size: 16px;
+ border: 1px solid var(--color-border-2);
+
+ > svg {
+ vertical-align: -3px;
+ }
+}
diff --git a/admin/src/components/NavBar/style/index.module.less b/admin/src/components/NavBar/style/index.module.less
new file mode 100644
index 0000000..f668669
--- /dev/null
+++ b/admin/src/components/NavBar/style/index.module.less
@@ -0,0 +1,83 @@
+.navbar {
+ display: flex;
+ justify-content: space-between;
+ border-bottom: 1px solid var(--color-border);
+ box-sizing: border-box;
+ background-color: var(--color-bg-2);
+ height: 100%;
+}
+
+.left {
+ display: flex;
+ align-items: center;
+}
+
+.logo {
+ display: flex;
+ align-items: center;
+ width: 44px;
+ padding-left: 20px;
+ box-sizing: border-box;
+
+ svg{
+ width: 44px;
+ height: 44px;
+ }
+}
+
+.logo-name {
+ color: var(--color-text-1);
+ font-weight: 500;
+ font-size: 20px;
+ margin-left: 10px;
+ font-family: 'PingFang SC';
+ white-space: nowrap;
+}
+
+.right {
+ display: flex;
+ list-style: none;
+ padding-right: 20px;
+
+ li {
+ padding: 0 8px;
+ display: flex;
+ align-items: center;
+ }
+
+ a {
+ text-decoration: none;
+ color: var(--color-text-1);
+ }
+}
+
+.username {
+ cursor: pointer;
+}
+
+.round {
+ :global(.arco-input-inner-wrapper) {
+ border-radius: 16px;
+ }
+
+ svg {
+ font-size: 16px;
+ }
+}
+
+.dropdown-icon {
+ margin-right: 8px;
+ font-size: 16px;
+ vertical-align: text-bottom;
+}
+
+.fixed-settings {
+ position: fixed;
+ top: 280px;
+ right: 0px;
+
+ svg {
+ font-size: 18px;
+ vertical-align: -4px;
+ }
+}
diff --git a/admin/src/components/PermissionWrapper/index.tsx b/admin/src/components/PermissionWrapper/index.tsx
new file mode 100644
index 0000000..0a5c14d
--- /dev/null
+++ b/admin/src/components/PermissionWrapper/index.tsx
@@ -0,0 +1,40 @@
+import React, { useEffect, useState } from 'react';
+import authentication, { AuthParams } from '@/utils/authentication';
+import {useStore} from '@/store';
+
+type PermissionWrapperProps = AuthParams & {
+ backup?: React.ReactNode;
+};
+
+const PermissionWrapper = (
+ props: React.PropsWithChildren
+) => {
+ const { backup, requiredPermissions, oneOfPerm } = props;
+ const [hasPermission, setHasPermission] = useState(false);
+ const userInfo = useStore().userInfo
+
+ useEffect(() => {
+ const hasPermission = authentication(
+ { requiredPermissions, oneOfPerm },
+ userInfo.permissions
+ );
+ setHasPermission(hasPermission);
+ }, [requiredPermissions, oneOfPerm, userInfo.permissions]);
+
+ if (hasPermission) {
+ return <>{convertReactElement(props.children)}>;
+ }
+ if (backup) {
+ return <>{convertReactElement(backup)}>;
+ }
+ return null;
+};
+
+function convertReactElement(node: React.ReactNode): React.ReactElement {
+ if (!React.isValidElement(node)) {
+ return <>{node}>;
+ }
+ return node;
+}
+
+export default PermissionWrapper;
diff --git a/admin/src/components/Settings/block.tsx b/admin/src/components/Settings/block.tsx
new file mode 100644
index 0000000..8c61770
--- /dev/null
+++ b/admin/src/components/Settings/block.tsx
@@ -0,0 +1,69 @@
+import React, { ReactNode } from 'react';
+import { Switch, Divider, InputNumber } from '@arco-design/web-react';
+import { useStore } from '@/store';
+import useLocale from '../../utils/useLocale';
+import styles from './style/block.module.less';
+
+export interface BlockProps {
+ title?: ReactNode;
+ options?: { name: string; value: string; type?: 'switch' | 'number' }[];
+ children?: ReactNode;
+}
+
+export default function Block(props: BlockProps) {
+ const { title, options, children } = props;
+ const locale = useLocale();
+ const { settings, updateSetting } = useStore();
+
+ return (
+
+
{title}
+ {options &&
+ options.map((option) => {
+ const type = option.type || 'switch';
+
+ return (
+
+ {locale[option.name]}
+ {type === 'switch' && (
+ {
+ const newSetting = {
+ ...settings,
+ [option.value]: checked
+ };
+ updateSetting(newSetting);
+ // set color week
+ if (checked && option.value === 'colorWeek') {
+ document.body.style.filter = 'invert(80%)';
+ }
+ if (!checked && option.value === 'colorWeek') {
+ document.body.style.filter = 'none';
+ }
+ }}
+ />
+ )}
+ {type === 'number' && (
+ {
+ const newSetting = {
+ ...settings,
+ [option.value]: value
+ };
+ updateSetting(newSetting);
+ }}
+ />
+ )}
+
+ );
+ })}
+ {children}
+
+
+ );
+}
diff --git a/admin/src/components/Settings/color.tsx b/admin/src/components/Settings/color.tsx
new file mode 100644
index 0000000..0da8765
--- /dev/null
+++ b/admin/src/components/Settings/color.tsx
@@ -0,0 +1,71 @@
+import React from 'react';
+import { Trigger, Typography } from '@arco-design/web-react';
+import { SketchPicker } from 'react-color';
+import { generate, getRgbStr } from '@arco-design/color';
+import { useStore } from '@/store';
+import useLocale from '@/utils/useLocale';
+import styles from './style/color-panel.module.less';
+
+function ColorPanel() {
+ const theme =
+ document.querySelector('body').getAttribute('arco-theme') || 'light';
+
+ const {settings,updateSetting} = useStore()
+ const locale = useLocale();
+ const themeColor = settings.themeColor;
+ const list = generate(themeColor, { list: true });
+
+ return (
+
+
(
+ {
+ const newColor = color.hex;
+ updateSetting({
+ themeColor: newColor
+ })
+
+ const newList = generate(newColor, {
+ list: true,
+ dark: theme === 'dark',
+ });
+ newList.forEach((l, index) => {
+ const rgbStr = getRgbStr(l);
+ document.body.style.setProperty(
+ `--arcoblue-${index + 1}`,
+ rgbStr
+ );
+ });
+ }}
+ />
+ )}
+ >
+
+
+
+ {list.map((item, index) => (
+
+ ))}
+
+
+ {locale['settings.color.tooltip']}
+
+
+ );
+}
+
+export default ColorPanel;
diff --git a/admin/src/components/Settings/index.tsx b/admin/src/components/Settings/index.tsx
new file mode 100644
index 0000000..41eb5fa
--- /dev/null
+++ b/admin/src/components/Settings/index.tsx
@@ -0,0 +1,71 @@
+import React, { useState } from 'react';
+import { Drawer, Alert, Message } from '@arco-design/web-react';
+import { IconSettings } from '@arco-design/web-react/icon';
+import copy from 'copy-to-clipboard';
+import Block from './block';
+import ColorPanel from './color';
+import IconButton from '../NavBar/IconButton';
+import useLocale from '@/utils/useLocale';
+import { useStore } from '@/store';
+
+interface SettingProps {
+ trigger?: React.ReactElement;
+}
+
+function Setting(props: SettingProps) {
+ const { trigger } = props;
+ const [visible, setVisible] = useState(false);
+ const locale = useLocale();
+ const settings = useStore().settings
+
+ function onCopySettings() {
+ copy(JSON.stringify(settings, null, 2));
+ Message.success(locale['settings.copySettings.message']);
+ }
+
+ return (
+ <>
+ {trigger ? (
+ React.cloneElement(trigger as React.ReactElement, {
+ onClick: () => setVisible(true),
+ })
+ ) : (
+ } onClick={() => setVisible(true)} />
+ )}
+
+
+ {locale['settings.title']}
+ >
+ }
+ visible={visible}
+ okText={locale['settings.copySettings']}
+ cancelText={locale['settings.close']}
+ onOk={onCopySettings}
+ onCancel={() => setVisible(false)}
+ >
+
+
+
+
+
+
+
+ >
+ );
+}
+
+export default Setting;
diff --git a/admin/src/components/Settings/style/block.module.less b/admin/src/components/Settings/style/block.module.less
new file mode 100644
index 0000000..8109bf8
--- /dev/null
+++ b/admin/src/components/Settings/style/block.module.less
@@ -0,0 +1,16 @@
+.block {
+ margin-bottom: 24px;
+}
+
+.title {
+ font-size: 14px;
+ padding: 0;
+ margin: 10px 0;
+}
+
+.switch-wrapper {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ height: 32px;
+}
diff --git a/admin/src/components/Settings/style/color-panel.module.less b/admin/src/components/Settings/style/color-panel.module.less
new file mode 100644
index 0000000..196b752
--- /dev/null
+++ b/admin/src/components/Settings/style/color-panel.module.less
@@ -0,0 +1,25 @@
+.input {
+ display: flex;
+ width: 100%;
+ height: 32px;
+ border: 1px solid var(--color-border);
+ padding: 3px;
+ box-sizing: border-box;
+}
+
+.color {
+ width: 100px;
+ height: 24px;
+ margin-right: 10px;
+}
+
+.ul {
+ list-style: none;
+ display: flex;
+ padding: 0;
+}
+
+.li {
+ width: 10%;
+ height: 26px;
+}
diff --git a/admin/src/components/Upload/index.tsx b/admin/src/components/Upload/index.tsx
new file mode 100644
index 0000000..6e0dc7b
--- /dev/null
+++ b/admin/src/components/Upload/index.tsx
@@ -0,0 +1,33 @@
+import React from 'react';
+import {Upload, UploadProps} from '@arco-design/web-react';
+import { uploadFile } from '@/api/common';
+
+interface Props extends UploadProps {
+ dir?: string,
+ type?: 'image' | 'video'
+}
+
+const UploadFile = (props: Props) => {
+ const {dir = '/', type = 'image', ...rest} = props
+ const customRequest = async (option) => {
+ const {onSuccess, file} = option;
+ const formData = new FormData()
+ formData.append('file', file)
+ formData.append('dir', dir)
+ const {data} = await uploadFile(formData)
+ onSuccess(data.id)
+ }
+
+ return (
+ <>
+
+ >
+ );
+};
+
+export default UploadFile
diff --git a/admin/src/context.tsx b/admin/src/context.tsx
new file mode 100644
index 0000000..2ea1ed2
--- /dev/null
+++ b/admin/src/context.tsx
@@ -0,0 +1,8 @@
+import { createContext } from 'react';
+
+export const GlobalContext = createContext<{
+ lang?: string;
+ setLang?: (value: string) => void;
+ theme?: string;
+ setTheme?: (value: string) => void;
+}>({});
diff --git a/admin/src/declaration.d.ts b/admin/src/declaration.d.ts
new file mode 100644
index 0000000..8eb1c2a
--- /dev/null
+++ b/admin/src/declaration.d.ts
@@ -0,0 +1,27 @@
+declare module '*.svg' {
+ const content: React.FunctionComponent>;
+ export default content;
+}
+
+declare module '*.less' {
+ const classes: { [className: string]: string };
+ export default classes;
+}
+
+declare module '*/settings.json' {
+ const value: {
+ colorWeek: boolean;
+ navbar: boolean;
+ menu: boolean;
+ footer: boolean;
+ themeColor: string;
+ menuWidth: number;
+ };
+
+ export default value;
+}
+
+declare module '*.png' {
+ const value: string;
+ export default value;
+}
diff --git a/admin/src/enum/index.ts b/admin/src/enum/index.ts
new file mode 100644
index 0000000..2dd5976
--- /dev/null
+++ b/admin/src/enum/index.ts
@@ -0,0 +1,7 @@
+export const enum Gender {
+ MALE,
+ FEMALE,
+ UNKNOWN
+}
+
+export const GenderDict = ['女', '男', '未知'];
diff --git a/admin/src/layout.tsx b/admin/src/layout.tsx
new file mode 100644
index 0000000..df28729
--- /dev/null
+++ b/admin/src/layout.tsx
@@ -0,0 +1,267 @@
+import React, { useState, useMemo, useRef, useEffect } from 'react';
+import { Switch, Route, Redirect, useHistory } from 'react-router-dom';
+import { Layout, Menu, Breadcrumb, Spin } from '@arco-design/web-react';
+import cs from 'classnames';
+import {
+ IconMenuFold,
+ IconMenuUnfold,
+ IconSettings,
+ IconList, IconDashboard,
+} from '@arco-design/web-react/icon';
+import qs from 'query-string';
+import NProgress from 'nprogress';
+import Navbar from './components/NavBar';
+import Footer from './components/Footer';
+import useRoute, { IRoute } from '@/routes';
+import { isArray } from './utils/is';
+import useLocale from './utils/useLocale';
+import getUrlParams from './utils/getUrlParams';
+import lazyload from './utils/lazyload';
+import styles from './style/layout.module.less';
+import { useStore } from '@/store';
+
+const MenuItem = Menu.Item;
+const SubMenu = Menu.SubMenu;
+
+const Sider = Layout.Sider;
+const Content = Layout.Content;
+
+function getFlattenRoutes(routes) {
+ const mod = import.meta.glob('./pages/**/[a-z[]*.tsx');
+ const res = [];
+ function travel(_routes) {
+ _routes.forEach((route) => {
+ if (route.key && !route.children) {
+ route.component = lazyload(mod[`./pages/${route.key}/index.tsx`]);
+ res.push(route);
+ } else if (isArray(route.children) && route.children.length) {
+ travel(route.children);
+ }
+ });
+ }
+ travel(routes);
+ return res;
+}
+
+function PageLayout() {
+ const urlParams = getUrlParams();
+ const history = useHistory();
+ const pathname = history.location.pathname;
+ const currentComponent = qs.parseUrl(pathname).url.slice(1);
+ const locale = useLocale();
+ const store = useStore()
+ const {settings,userInfo,userLoading} = store
+ const [routes, defaultRoute] = useRoute(userInfo?.menus);
+
+ const defaultSelectedKeys = [currentComponent || defaultRoute];
+ const paths = (currentComponent || defaultRoute).split('/');
+ const defaultOpenKeys = paths.slice(0, paths.length - 1);
+
+ const [breadcrumb, setBreadCrumb] = useState([]);
+ const [collapsed, setCollapsed] = useState(false);
+ const [selectedKeys, setSelectedKeys] =
+ useState(defaultSelectedKeys);
+ const [openKeys, setOpenKeys] = useState(defaultOpenKeys);
+
+ const routeMap = useRef