├── src ├── index.css ├── env.d.ts ├── components │ ├── stateful │ │ ├── index.tsx │ │ └── ErrorBoundary │ │ │ ├── index.module.less │ │ │ └── index.tsx │ └── stateless │ │ ├── FixLayout │ │ └── index.tsx │ │ ├── AlignCenter │ │ └── index.tsx │ │ ├── FixTabPanel │ │ └── index.tsx │ │ ├── Exception │ │ ├── exception500.tsx │ │ ├── exception404.tsx │ │ ├── exception401.tsx │ │ └── exception403.tsx │ │ ├── ContainerLoading │ │ ├── index.md │ │ └── index.tsx │ │ ├── Loading │ │ └── index.tsx │ │ ├── ScrollToTop │ │ └── index.tsx │ │ ├── MultiColorBorder │ │ ├── index.tsx │ │ └── index.module.less │ │ ├── NoMatch │ │ └── index.tsx │ │ ├── TypedText │ │ └── index.tsx │ │ ├── LanguageSwitcher │ │ └── index.tsx │ │ └── UserIP │ │ └── index.tsx ├── utils │ ├── token │ │ └── index.ts │ ├── index.ts │ ├── sleep │ │ └── index.ts │ ├── style │ │ └── index.ts │ ├── suffix │ │ └── index.ts │ ├── tryCatch │ │ ├── index.js │ │ └── runPromise.js │ ├── encrypt │ │ └── index.ts │ ├── sentry │ │ └── index.js │ ├── menu │ │ └── index.ts │ ├── publicFn │ │ └── index.tsx │ └── aidFn.js ├── layout │ ├── proContent │ │ ├── index.module.less │ │ └── index.tsx │ ├── proHeader │ │ ├── components │ │ │ ├── Breadcrumb │ │ │ │ ├── index.module.less │ │ │ │ ├── util.ts │ │ │ │ └── index.tsx │ │ │ ├── UserInfo │ │ │ │ ├── ChangeAvatar │ │ │ │ │ ├── index.less │ │ │ │ │ └── index.tsx │ │ │ │ ├── index.less │ │ │ │ ├── index.tsx │ │ │ │ └── ChangePassword │ │ │ │ │ └── index.tsx │ │ │ ├── SwitchWorkspace │ │ │ │ ├── index.module.less │ │ │ │ └── index.tsx │ │ │ ├── HeaderSearch │ │ │ │ ├── index.module.less │ │ │ │ └── index.tsx │ │ │ ├── HeaderRight │ │ │ │ ├── index.module.less │ │ │ │ └── index.tsx │ │ │ └── TabsHistory │ │ │ │ ├── index.module.less │ │ │ │ └── inex.tsx │ │ ├── index.module.less │ │ └── index.tsx │ ├── index.module.less │ ├── proSecNav │ │ ├── index.module.less │ │ └── index.tsx │ ├── index.tsx │ ├── proSider │ │ └── index.tsx │ ├── fullscreen │ │ └── index.tsx │ └── proTabs │ │ └── index.tsx ├── theme │ ├── dark.ts │ ├── light.ts │ ├── index.ts │ └── hooks.tsx ├── assets │ ├── images │ │ └── signIn │ │ │ └── bg.jpg │ └── svg │ │ ├── v8-outline.svg │ │ └── antd.svg ├── locales │ ├── zh │ │ └── translation.ts │ └── en │ │ └── translation.ts ├── pages │ ├── signIn │ │ ├── LoginCard │ │ │ ├── index.tsx │ │ │ └── index.less │ │ └── index.tsx │ ├── coupons │ │ ├── home │ │ │ └── index.jsx │ │ ├── edit │ │ │ └── index.jsx │ │ ├── detail │ │ │ └── index.jsx │ │ ├── add │ │ │ └── index.jsx │ │ └── index.jsx │ ├── demo │ │ └── index.tsx │ ├── error │ │ └── index.jsx │ ├── home │ │ └── index.tsx │ ├── echarts │ │ └── index.jsx │ └── crypto │ │ └── index.jsx ├── store │ ├── index.ts │ ├── userInfoSlice │ │ └── index.ts │ └── tabsSlice │ │ └── index.tsx ├── index.tsx ├── i18n │ └── i18n.ts ├── App.tsx ├── routers │ ├── authRouter.tsx │ └── index.tsx ├── ThemeIndex.tsx ├── styles │ └── reset.css └── hooks │ └── usePagination │ └── index.ts ├── .env ├── .gitignore ├── prettier.config.mjs ├── uno.config.ts ├── .vscode ├── settings.json └── launch.json ├── typings └── i18next.d.ts ├── README.md ├── tsconfig.json ├── rsbuild.config.ts ├── .github └── workflows │ └── deploy.yml ├── eslint.config.mjs └── package.json /src/index.css: -------------------------------------------------------------------------------- 1 | @unocss preflights; 2 | @unocss default; 3 | -------------------------------------------------------------------------------- /src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/components/stateful/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './ErrorBoundary'; 2 | -------------------------------------------------------------------------------- /src/utils/token/index.ts: -------------------------------------------------------------------------------- 1 | const getToken = () => 'token' 2 | 3 | export default getToken 4 | -------------------------------------------------------------------------------- /src/layout/proContent/index.module.less: -------------------------------------------------------------------------------- 1 | .layout { 2 | height: 100%; 3 | overflow: hidden; 4 | } 5 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | APP_BASE_URL= 2 | DEPLOYED_ENV=Dev 3 | VITE_GREETINGS=Dev 4 | AUTH_USER=cl1107 5 | AUTH_PASSWORD=cl1107 -------------------------------------------------------------------------------- /src/theme/dark.ts: -------------------------------------------------------------------------------- 1 | const darkTheme = { 2 | colorPrimary: '#1677ff', 3 | }; 4 | 5 | export default darkTheme; 6 | -------------------------------------------------------------------------------- /src/theme/light.ts: -------------------------------------------------------------------------------- 1 | const lightTheme = { 2 | colorPrimary: '#1677ff', 3 | } 4 | 5 | export default lightTheme 6 | -------------------------------------------------------------------------------- /src/assets/images/signIn/bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cl1107/react-antd-admin-pro/HEAD/src/assets/images/signIn/bg.jpg -------------------------------------------------------------------------------- /src/locales/zh/translation.ts: -------------------------------------------------------------------------------- 1 | const zh = { 2 | demo: '演示', 3 | lang: 'ZH', 4 | home: '首页', 5 | } 6 | 7 | export default zh 8 | -------------------------------------------------------------------------------- /src/locales/en/translation.ts: -------------------------------------------------------------------------------- 1 | const en = { 2 | demo: 'Demo', 3 | lang: 'En', 4 | home: 'Home', 5 | } 6 | 7 | export default en 8 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import tryCatch from './tryCatch' 2 | import sentryInit from './sentry' 3 | 4 | export { tryCatch, sentryInit } 5 | -------------------------------------------------------------------------------- /src/layout/proHeader/components/Breadcrumb/index.module.less: -------------------------------------------------------------------------------- 1 | .breadcrumb { 2 | cursor: pointer; 3 | line-height: 56px !important; 4 | } 5 | -------------------------------------------------------------------------------- /src/layout/index.module.less: -------------------------------------------------------------------------------- 1 | .layout { 2 | height: 100vh; 3 | width: 100vw; 4 | overflow: hidden; 5 | // flex-shrink: 0; 6 | } 7 | -------------------------------------------------------------------------------- /src/components/stateful/ErrorBoundary/index.module.less: -------------------------------------------------------------------------------- 1 | .pre { 2 | white-space: pre-wrap; 3 | word-wrap: break-word; 4 | color: #f00; 5 | } 6 | -------------------------------------------------------------------------------- /src/layout/proHeader/components/UserInfo/ChangeAvatar/index.less: -------------------------------------------------------------------------------- 1 | .change-avatar { 2 | .cl-ant-upload-list { 3 | display: none; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/layout/proSecNav/index.module.less: -------------------------------------------------------------------------------- 1 | .menu { 2 | border-right: 0; 3 | height: calc(100% - 40px); 4 | overflow-y: auto; 5 | overflow-x: hidden; 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Local 2 | .DS_Store 3 | *.local 4 | *.log* 5 | 6 | # Dist 7 | node_modules 8 | dist/ 9 | 10 | # IDE 11 | !.vscode/extensions.json 12 | .idea 13 | -------------------------------------------------------------------------------- /src/theme/index.ts: -------------------------------------------------------------------------------- 1 | import darkTheme from './dark' 2 | import lightTheme from './light' 3 | 4 | const myThemes = { darkTheme, lightTheme } 5 | export default myThemes 6 | -------------------------------------------------------------------------------- /src/layout/proHeader/index.module.less: -------------------------------------------------------------------------------- 1 | .header { 2 | padding: 0 12px !important; 3 | display: flex; 4 | align-items: center; 5 | justify-content: space-between; 6 | } 7 | -------------------------------------------------------------------------------- /src/utils/sleep/index.ts: -------------------------------------------------------------------------------- 1 | const sleep = (time = 100) => 2 | new Promise((resolve) => { 3 | setTimeout(() => { 4 | resolve(true); 5 | }, time); 6 | }); 7 | 8 | export default sleep; 9 | -------------------------------------------------------------------------------- /src/utils/style/index.ts: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import { twMerge } from 'tailwind-merge'; 3 | 4 | export function cn(...inputs: clsx.ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | -------------------------------------------------------------------------------- /src/utils/suffix/index.ts: -------------------------------------------------------------------------------- 1 | const suffix = (map) => { 2 | const timestamp = Math.round(new Date().getTime()) 3 | return { 4 | ...map, 5 | timestamp, 6 | } 7 | } 8 | export default suffix 9 | -------------------------------------------------------------------------------- /prettier.config.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | tabWidth: 2, 3 | semi: true, 4 | trailingComma: 'all', 5 | printWidth: 100, 6 | proseWrap: 'never', 7 | endOfLine: 'lf', 8 | singleQuote: true, 9 | }; 10 | -------------------------------------------------------------------------------- /uno.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, presetUno } from 'unocss'; 2 | 3 | export default defineConfig({ 4 | content: { 5 | filesystem: ['./src/**/*.{html,js,ts,jsx,tsx}'], 6 | }, 7 | presets: [presetUno()], 8 | }); 9 | -------------------------------------------------------------------------------- /src/components/stateless/FixLayout/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const FixLayout = ({ children }) =>
{children}
4 | 5 | export default FixLayout 6 | -------------------------------------------------------------------------------- /src/utils/tryCatch/index.js: -------------------------------------------------------------------------------- 1 | const tryCatch = (tryer) => { 2 | try { 3 | const result = tryer() 4 | return [result, null] 5 | } catch (error) { 6 | return [null, error] 7 | } 8 | } 9 | 10 | export default tryCatch 11 | -------------------------------------------------------------------------------- /src/components/stateless/AlignCenter/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const AlignCenter = ({ children }: { children: React.ReactNode }) => ( 4 |
{children}
5 | ); 6 | 7 | export default AlignCenter; 8 | -------------------------------------------------------------------------------- /src/components/stateless/FixTabPanel/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const FixTabPanel = ({ children }: { children: React.ReactNode }) => ( 4 |
{children}
5 | ); 6 | 7 | export default FixTabPanel; 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "javascript.suggest.paths": true, 3 | "typescript.suggest.paths": true, 4 | "editor.codeActionsOnSave": { 5 | "source.organizeImports": "explicit", 6 | "source.fixAll": "explicit" 7 | }, 8 | "cSpell.words": ["partialize", "zustand"] 9 | } 10 | -------------------------------------------------------------------------------- /src/components/stateless/Exception/exception500.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Result } from 'antd' 3 | 4 | const Exception500 = () => ( 5 | <> 6 | 7 | 8 | ) 9 | 10 | export default Exception500 11 | -------------------------------------------------------------------------------- /typings/i18next.d.ts: -------------------------------------------------------------------------------- 1 | import 'i18next'; 2 | import translation from '../src/locales/zh/translation'; 3 | 4 | declare module 'i18next' { 5 | interface CustomTypeOptions { 6 | defaultNS: 'translation'; 7 | resources: { 8 | translation: typeof translation; 9 | }; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/components/stateless/Exception/exception404.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Result } from 'antd' 3 | 4 | const Exception404 = () => ( 5 | <> 6 | 7 | 8 | ) 9 | 10 | export default Exception404 11 | -------------------------------------------------------------------------------- /src/components/stateless/Exception/exception401.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { Result } from 'antd' 4 | 5 | const Exception401 = () => ( 6 | <> 7 | 8 | 9 | ) 10 | 11 | export default Exception401 12 | -------------------------------------------------------------------------------- /src/pages/signIn/LoginCard/index.tsx: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from 'react'; 2 | import './index.less'; 3 | 4 | export const LoginCard = ({ children }: { children: ReactNode }) => { 5 | return ( 6 |
7 |
{children}
8 |
9 | ); 10 | }; 11 | -------------------------------------------------------------------------------- /src/layout/proHeader/components/UserInfo/index.less: -------------------------------------------------------------------------------- 1 | .user-info { 2 | height: 100%; 3 | 4 | .cl-ant-popover-inner-content &__card { 5 | margin: -12px -16px; 6 | 7 | .cl-ant-card-body { 8 | padding: 24px 12px 16px 12px; 9 | } 10 | 11 | .cl-ant-card-actions > li { 12 | margin: 8px 0; 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.1", 3 | "configurations": [ 4 | { 5 | "name": "Run Application", 6 | "type": "node", 7 | "request": "launch", 8 | "runtimeExecutable": "npm", 9 | "runtimeArgs": [ 10 | "run", 11 | "dev" 12 | ], 13 | "cwd": "${workspaceFolder}" 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /src/components/stateless/ContainerLoading/index.md: -------------------------------------------------------------------------------- 1 | ## 整个页面 loading 2 | 3 | 若页面父容器不是 Card 等自带 loading 的组件,在接口 loading 时可以使用该组件 4 | 5 | ### Demo: 6 | 7 | ```tsx 8 | import { useState } from 'react'; 9 | import * as React from 'react'; 10 | import { ContainerLoading } from 'r5-ui'; 11 | 12 | export default () => { 13 | return ; 14 | }; 15 | ``` 16 | -------------------------------------------------------------------------------- /src/pages/coupons/home/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Alert } from 'antd' 3 | 4 | const HomeCoupons = () => ( 5 | <> 6 | 12 | 13 | ) 14 | 15 | export default HomeCoupons 16 | -------------------------------------------------------------------------------- /src/components/stateless/Loading/index.tsx: -------------------------------------------------------------------------------- 1 | import { LoadingOutlined } from '@ant-design/icons'; 2 | import { Spin } from 'antd'; 3 | 4 | const antIcon = ; 5 | const Loading = () => ( 6 |
7 | 8 |
9 | ); 10 | 11 | export default Loading; 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rsbuild Project 2 | 3 | ## Setup 4 | 5 | Install the dependencies: 6 | 7 | ```bash 8 | pnpm install 9 | ``` 10 | 11 | ## Get Started 12 | 13 | Start the dev server: 14 | 15 | ```bash 16 | pnpm dev 17 | ``` 18 | 19 | Build the app for production: 20 | 21 | ```bash 22 | pnpm build 23 | ``` 24 | 25 | Preview the production build locally: 26 | 27 | ```bash 28 | pnpm preview 29 | ``` 30 | -------------------------------------------------------------------------------- /src/components/stateless/ContainerLoading/index.tsx: -------------------------------------------------------------------------------- 1 | import { Card } from 'antd'; 2 | import type { CSSProperties, FC } from 'react'; 3 | 4 | const ContainerLoading: FC<{ style?: CSSProperties; bordered?: boolean }> = ({ 5 | style, 6 | bordered = true, 7 | }) => { 8 | return ; 9 | }; 10 | export default ContainerLoading; 11 | -------------------------------------------------------------------------------- /src/pages/demo/index.tsx: -------------------------------------------------------------------------------- 1 | import FixTabPanel from '@/components/stateless/FixTabPanel'; 2 | import { Input } from 'antd'; 3 | 4 | const ProDemo = () => { 5 | return ( 6 | 7 |

8 | 项目文档待完善 9 |

10 | 11 |
12 | ); 13 | }; 14 | 15 | export default ProDemo; 16 | -------------------------------------------------------------------------------- /src/components/stateless/ScrollToTop/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react' 2 | import { useLocation } from 'react-router-dom' 3 | 4 | const ScrollToTop = (props) => { 5 | const { pathname } = useLocation() 6 | useEffect(() => { 7 | window.scrollTo({ 8 | top: 0, 9 | left: 0, 10 | behavior: 'smooth', 11 | }) 12 | }, [pathname]) 13 | return props.children 14 | } 15 | 16 | export default ScrollToTop 17 | -------------------------------------------------------------------------------- /src/pages/error/index.jsx: -------------------------------------------------------------------------------- 1 | import FixTabPanel from '@/components/stateless/FixTabPanel'; 2 | import React from 'react'; 3 | 4 | const MyError = () => { 5 | const error = { error: 'error' }; 6 | return ( 7 | 8 |

Cool! Hi, React && Ant Design

9 | {error.map((item) => ( 10 | {item} 11 | ))} 12 |
13 | ); 14 | }; 15 | 16 | export default MyError; 17 | -------------------------------------------------------------------------------- /src/layout/proHeader/components/SwitchWorkspace/index.module.less: -------------------------------------------------------------------------------- 1 | .switch-workspace { 2 | margin: 0 12px; 3 | 4 | :global { 5 | .cl-ant-dropdown-menu-item, 6 | .cl-ant-dropdown-menu-submenu-title { 7 | padding: 5px 20px; 8 | } 9 | 10 | .cl-ant-dropdown-menu-item-selected, 11 | .cl-ant-dropdown-menu-submenu-title-selected { 12 | color: #fff; 13 | background: var(--theme-color); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/pages/home/index.tsx: -------------------------------------------------------------------------------- 1 | import FixTabPanel from '@/components/stateless/FixTabPanel'; 2 | import TypedText from '@/components/stateless/TypedText'; 3 | import { Input } from 'antd'; 4 | import { version } from 'react'; 5 | 6 | const Home = () => { 7 | return ( 8 | 9 |

10 | Cool! Hi, React & Ant Design! 11 |

12 |

React version: {version}

13 | 14 |
15 | ); 16 | }; 17 | 18 | export default Home; 19 | -------------------------------------------------------------------------------- /src/utils/encrypt/index.ts: -------------------------------------------------------------------------------- 1 | import JsEncrypt from 'jsencrypt'; 2 | 3 | /** 4 | * rsa加密密码 5 | * @param {string} password 6 | * @returns 7 | */ 8 | export const encryptPassword = (password: string) => { 9 | const encrypt = new JsEncrypt({}); 10 | encrypt.setPublicKey( 11 | 'MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCRzv+ez8OvPfzncOQrS4bz2bSTRecaxjKp9fV7iukfLeU9kpOX0+ZNneahtSq5/HJWXBE4P4D5j+STlHIF7rNbue620yRLteYOpYqI3m9QBF9yH08b6yAn1nsTcRy4TeMPA6xDCYzI961E/1e89HGRZ4JlS+L1I4df3RK6lUHYjQIDAQAB', 12 | ); 13 | return encrypt.encrypt(password); 14 | }; 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "lib": ["DOM", "ES2020"], 5 | "module": "ESNext", 6 | "jsx": "react-jsx", 7 | "strict": true, 8 | "skipLibCheck": true, 9 | "isolatedModules": true, 10 | "resolveJsonModule": true, 11 | "moduleResolution": "bundler", 12 | "useDefineForClassFields": true, 13 | "baseUrl": ".", 14 | "paths": { 15 | "@/*": ["src/*"] 16 | } /* Specify a set of entries that re-map imports to additional lookup locations. */ 17 | }, 18 | "include": ["src", "typings"] 19 | } 20 | -------------------------------------------------------------------------------- /src/components/stateless/MultiColorBorder/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import styles from './index.module.less' 4 | 5 | const MultiColorBorder = ({ text, wrapperStyles = { color: '#fff' }, contentStyles = { color: '#fff' } }) => ( 6 | <> 7 |
13 |
14 | {text} 15 |
16 |
17 | 18 | ) 19 | 20 | export default MultiColorBorder 21 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { create } from 'zustand'; 2 | import { devtools, persist } from 'zustand/middleware'; 3 | import { TabsState, createTabsSlice } from './tabsSlice'; 4 | import { UserInfoState, createUserInfoSlice } from './userInfoSlice'; 5 | 6 | export const useGlobalStore = create()( 7 | devtools( 8 | persist( 9 | (...a) => ({ 10 | ...createUserInfoSlice(...a), 11 | ...createTabsSlice(...a), 12 | }), 13 | { name: 'userInfo', partialize: (state) => ({ userInfo: state.userInfo }) }, // 挑选需要持久化的数据 14 | ), 15 | ), 16 | ); 17 | -------------------------------------------------------------------------------- /src/components/stateless/NoMatch/index.tsx: -------------------------------------------------------------------------------- 1 | import { useLocation, useNavigate } from 'react-router-dom'; 2 | import { Button } from 'antd'; 3 | 4 | const NoMatch = () => { 5 | const location = useLocation(); 6 | const navigate = useNavigate(); 7 | 8 | return ( 9 |
10 |

11 | No match for {location.pathname} 12 |

13 | 14 | 17 |
18 | ); 19 | }; 20 | 21 | export default NoMatch; 22 | -------------------------------------------------------------------------------- /src/store/userInfoSlice/index.ts: -------------------------------------------------------------------------------- 1 | import { StateCreator } from 'zustand'; 2 | import { TabsState } from '../tabsSlice'; 3 | 4 | type UserInfo = { 5 | username: string; 6 | lastLogin: string; 7 | roleName: string; 8 | }; 9 | 10 | export interface UserInfoState { 11 | userInfo: UserInfo; 12 | updateUserInfo: (userInfo: UserInfo) => void; 13 | } 14 | export const createUserInfoSlice: StateCreator = ( 15 | set, 16 | ) => ({ 17 | userInfo: { username: '', lastLogin: '', roleName: '' }, 18 | updateUserInfo: (userInfo: UserInfo) => set({ userInfo }), 19 | }); 20 | -------------------------------------------------------------------------------- /src/components/stateless/TypedText/index.tsx: -------------------------------------------------------------------------------- 1 | import { memo, useEffect, useState } from 'react'; 2 | 3 | const TypedText = ({ children, delay = 110 }: { children: string; delay?: number }) => { 4 | const [revealedLetters, setRevealedLetters] = useState(0); 5 | const interval = setInterval(() => setRevealedLetters((l) => l + 1), delay); 6 | 7 | useEffect(() => { 8 | if (revealedLetters === children.length) clearInterval(interval); 9 | }, [children, interval, revealedLetters]); 10 | 11 | useEffect(() => () => clearInterval(interval), [interval]); 12 | 13 | return <>{children.substring(0, revealedLetters)}; 14 | }; 15 | 16 | export default memo(TypedText); 17 | -------------------------------------------------------------------------------- /src/utils/sentry/index.js: -------------------------------------------------------------------------------- 1 | import * as Sentry from '@sentry/react' 2 | import packageJson from '../../../package.json' 3 | 4 | const sentryInit = () => { 5 | // const nodeEnv = process.env.NODE_ENV 6 | // if (nodeEnv !== 'production') return 7 | Sentry.init({ 8 | dsn: 'https://39892504629549fa9c0b040d98e87d03@o64827.ingest.sentry.io/5791911', 9 | integrations: [new Sentry.BrowserTracing()], 10 | tracesSampleRate: 1.0, 11 | release: packageJson.version, 12 | // environment: nodeEnv, 13 | // autoSessionTracking: nodeEnv === 'production', 14 | }) 15 | 16 | Sentry.setExtra('projectOwner', '150****5870') 17 | } 18 | 19 | export default sentryInit 20 | -------------------------------------------------------------------------------- /src/components/stateless/Exception/exception403.tsx: -------------------------------------------------------------------------------- 1 | import FixTabPanel from '@/components/stateless/FixTabPanel'; 2 | import { Button, Result } from 'antd'; 3 | import { useNavigate } from 'react-router-dom'; 4 | 5 | const Exception403 = () => { 6 | const navigate = useNavigate(); 7 | return ( 8 | 9 | navigate('/')}> 13 | 去首页 14 | 15 | } 16 | subTitle="Sorry, you are not authorized to access this page." 17 | /> 18 | 19 | ); 20 | }; 21 | 22 | export default Exception403; 23 | -------------------------------------------------------------------------------- /src/layout/index.tsx: -------------------------------------------------------------------------------- 1 | import { Layout, Watermark } from 'antd'; 2 | import ProContent from './proContent'; 3 | import ProHeader from './proHeader'; 4 | import ProSecNav from './proSecNav'; 5 | import ProSider from './proSider'; 6 | 7 | import styles from './index.module.less'; 8 | 9 | const ProLayout = () => ( 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | ); 22 | 23 | export default ProLayout; 24 | -------------------------------------------------------------------------------- /src/pages/coupons/edit/index.jsx: -------------------------------------------------------------------------------- 1 | import FixTabPanel from '@/components/stateless/FixTabPanel'; 2 | import { Alert } from 'antd'; 3 | import React from 'react'; 4 | import { useSearchParams } from 'react-router-dom'; 5 | 6 | const EditCoupons = () => { 7 | const [searchParams] = useSearchParams(); 8 | const term = searchParams.get('id'); 9 | return ( 10 | 11 | 17 | Search Id: {term} 18 | 19 | ); 20 | }; 21 | 22 | export default EditCoupons; 23 | -------------------------------------------------------------------------------- /src/components/stateless/LanguageSwitcher/index.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Space } from 'antd'; 2 | import { useTranslation } from 'react-i18next'; 3 | 4 | const LanguageSwitcher = () => { 5 | const { i18n } = useTranslation(); 6 | 7 | const handleLanguageChange = (language: string) => { 8 | i18n.changeLanguage(language); 9 | // window.location.reload() 10 | }; 11 | 12 | return ( 13 | 14 | {(i18n.language === 'zh-CN' || i18n.language === 'zh') && ( 15 | 16 | )} 17 | {i18n.language === 'en' && } 18 | 19 | ); 20 | }; 21 | 22 | export default LanguageSwitcher; 23 | -------------------------------------------------------------------------------- /src/components/stateful/ErrorBoundary/index.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from 'antd'; 2 | import { ErrorBoundary } from 'react-error-boundary'; 3 | import styles from './index.module.less'; 4 | 5 | const ErrorFallback = ({ error, resetErrorBoundary }) => ( 6 |
7 |

Something went wrong:

8 |
{error.message}
9 | 12 |
13 | ); 14 | 15 | export const MyErrorBoundary = (props) => ( 16 | { 19 | if (props.fixError) { 20 | props.fixError(); 21 | } 22 | }} 23 | > 24 | {props.children} 25 | 26 | ); 27 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import { I18nextProvider } from 'react-i18next'; 4 | import { BrowserRouter } from 'react-router-dom'; 5 | import ThemeIndex from './ThemeIndex'; 6 | import i18n from './i18n/i18n'; 7 | import './index.css'; 8 | import { ProThemeProvider } from './theme/hooks'; 9 | 10 | const root = ReactDOM.createRoot(document.getElementById('root')!); 11 | root.render( 12 | 13 | 14 | 15 | 18 | 19 | 20 | 21 | 22 | , 23 | ); 24 | -------------------------------------------------------------------------------- /src/pages/coupons/detail/index.jsx: -------------------------------------------------------------------------------- 1 | import FixTabPanel from '@/components/stateless/FixTabPanel'; 2 | import { Alert } from 'antd'; 3 | import React from 'react'; 4 | import { useParams, useSearchParams } from 'react-router-dom'; 5 | 6 | const DetailCoupons = () => { 7 | const [searchParams] = useSearchParams(); 8 | 9 | const id = searchParams.get('id'); 10 | 11 | const params = useParams(); 12 | 13 | return ( 14 | 15 | 21 |
useParams: {JSON.stringify(params, null, 2)}
22 | Search Id: {id} 23 |
24 | ); 25 | }; 26 | 27 | export default DetailCoupons; 28 | -------------------------------------------------------------------------------- /src/components/stateless/MultiColorBorder/index.module.less: -------------------------------------------------------------------------------- 1 | .multiWrapper { 2 | border-radius: 3px; 3 | box-sizing: border-box; 4 | display: block; 5 | overflow: hidden; 6 | padding: 2px; 7 | position: relative; 8 | } 9 | 10 | .multiWrapper::before { 11 | background: linear-gradient(115deg, #4fcf70, #fad648, #a767e5, #12bcfe, #44ce7b); 12 | background-size: 50% 100%; 13 | content: ''; 14 | height: 100%; 15 | width: 200%; 16 | left: 0; 17 | position: absolute; 18 | top: 0; 19 | } 20 | 21 | .multiWrapper:hover::before { 22 | animation: wrapper-animation 0.75s linear infinite; 23 | } 24 | 25 | .multiWrapper .multiContent { 26 | background: #000; 27 | position: relative; 28 | border-radius: 3px; 29 | color: #fff; 30 | } 31 | 32 | @keyframes wrapper-animation { 33 | to { 34 | transform: translateX(-50%); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/i18n/i18n.ts: -------------------------------------------------------------------------------- 1 | import i18n from 'i18next' 2 | import { initReactI18next } from 'react-i18next' 3 | import LanguageDetector from 'i18next-browser-languagedetector' 4 | import translationInZh from '../locales/zh/translation' 5 | import translationInEn from '../locales/en/translation' 6 | 7 | i18n 8 | .use(LanguageDetector) 9 | .use(initReactI18next) 10 | .init({ 11 | resources: { 12 | en: { 13 | translation: translationInEn, 14 | }, 15 | zh: { 16 | translation: translationInZh, 17 | }, 18 | }, 19 | lng: 'zh', 20 | fallbackLng: 'en', // 默认语言 21 | debug: process.env.NODE_ENV !== 'production', // 开启调试模式 22 | interpolation: { 23 | escapeValue: false, // 不转义特殊字符 24 | }, 25 | detection: { 26 | order: ['localStorage', 'navigator'], 27 | }, 28 | }) 29 | 30 | export default i18n 31 | -------------------------------------------------------------------------------- /src/components/stateless/UserIP/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react' 2 | // import UserAgent from 'user-agents' 3 | 4 | const UserIP = () => { 5 | const [userIp, setUserIp] = useState('') 6 | // const userAgent = new UserAgent() 7 | useEffect(() => { 8 | getUserIp() 9 | }, []) 10 | 11 | const getUserIp = () => { 12 | fetch('https://api.ipify.org?format=json') 13 | .then((response) => response.json()) 14 | .then((data) => { 15 | const ipAddress = data.ip 16 | setUserIp(ipAddress ?? '0.0.0.0') 17 | }) 18 | .catch(() => { 19 | setUserIp('0.0.0.0') 20 | }) 21 | } 22 | 23 | return ( 24 | <> 25 |

欢迎您,来自远方的朋友!

26 |

您的IP: {userIp}

27 | {/*

{userAgent.toString()}

*/} 28 | 29 | ) 30 | } 31 | 32 | export default UserIP 33 | -------------------------------------------------------------------------------- /src/theme/hooks.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, useContext, useMemo, useState } from 'react'; 2 | 3 | const defaultTheme = 'light'; 4 | type ThemeContextType = { 5 | myTheme: string; 6 | setMyTheme: React.Dispatch>; 7 | }; 8 | 9 | const ProThemeContext = createContext({} as ThemeContextType); 10 | const useProThemeContext = () => useContext(ProThemeContext); 11 | 12 | const ProThemeProvider = ({ children }: { children: React.ReactNode }) => { 13 | const [myTheme, setMyTheme] = useState(defaultTheme); 14 | 15 | const themeProvider = useMemo( 16 | () => ({ 17 | myTheme, 18 | setMyTheme, 19 | }), 20 | [myTheme, setMyTheme], 21 | ); 22 | return {children}; 23 | }; 24 | 25 | export { ProThemeProvider, useProThemeContext }; 26 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { useRoutes } from 'react-router-dom'; 3 | import Loading from './components/stateless/Loading'; 4 | import rootRouter from './routers'; 5 | import AuthRouter from './routers/authRouter'; 6 | 7 | // import { sentryInit } from './utils'; 8 | 9 | const App = () => { 10 | // const { i18n } = useTranslation() 11 | const [loading, setLoading] = useState(true); 12 | const asyncCall = () => new Promise((resolve) => setTimeout(() => resolve(), 500)); 13 | useEffect(() => { 14 | // sentryInit(); 15 | asyncCall() 16 | .then(() => setLoading(false)) 17 | .catch(() => setLoading(false)); 18 | }, []); 19 | 20 | const element = useRoutes(rootRouter); 21 | 22 | if (loading) { 23 | return ; 24 | } 25 | 26 | return {element}; 27 | }; 28 | 29 | export default App; 30 | -------------------------------------------------------------------------------- /src/layout/proHeader/components/HeaderSearch/index.module.less: -------------------------------------------------------------------------------- 1 | // @import '~antd/es/style/themes/default.less'; 2 | /* stylelint-disable selector-class-pattern */ 3 | .headerSearch { 4 | // & > .cl-ant-select { 5 | // border: none; 6 | // } 7 | // & > .cl-ant-select > .cl-ant-select-selector { 8 | // border: none; 9 | // } 10 | .input { 11 | width: 0; 12 | min-width: 0; 13 | overflow: hidden; 14 | background: transparent; 15 | border-radius: 0; 16 | transition: 17 | width 0.3s, 18 | margin-left 0.3s; 19 | 20 | input { 21 | padding-right: 0; 22 | padding-left: 0; 23 | border: 0; 24 | box-shadow: none !important; 25 | } 26 | // &, 27 | // &:hover, 28 | // &:focus { 29 | // // border-bottom: 1px solid @border-color-base; 30 | // } 31 | &.show { 32 | width: 210px; 33 | margin-left: 8px; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/layout/proHeader/components/HeaderRight/index.module.less: -------------------------------------------------------------------------------- 1 | @pro-header-hover-bg: rgba(0, 0, 0, 0.025); 2 | 3 | .header { 4 | &__right { 5 | display: flex; 6 | align-items: center; 7 | justify-content: flex-end; 8 | height: 52px; 9 | transition: all 0.3s; 10 | 11 | .cl-ant-badge-count-sm { 12 | padding: 0 2px; 13 | } 14 | } 15 | 16 | &__action-icons { 17 | display: flex; 18 | align-items: center; 19 | height: 100%; 20 | } 21 | 22 | &__action-icon { 23 | margin-right: 20px; 24 | 25 | &:last-child { 26 | margin-right: 0; 27 | } 28 | } 29 | } 30 | 31 | .action { 32 | display: flex; 33 | align-items: center; 34 | height: 100%; 35 | cursor: pointer; 36 | transition: all 0.3s; 37 | 38 | > span { 39 | vertical-align: middle; 40 | } 41 | 42 | &:hover { 43 | background: transparent; 44 | } 45 | 46 | &:global(.opened) { 47 | background: @pro-header-hover-bg; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/routers/authRouter.tsx: -------------------------------------------------------------------------------- 1 | import { getKeyName, getLocalStorage } from '@/utils/publicFn'; 2 | import { Navigate, useLocation } from 'react-router-dom'; 3 | 4 | const AuthRouter = (props) => { 5 | const { pathname } = useLocation(); 6 | const route = getKeyName(pathname); 7 | 8 | if (!route?.auth) return props.children; 9 | 10 | const { token } = getLocalStorage('token') || { token: null }; 11 | if (!token) return ; 12 | 13 | // * 后端返回有权限路由列表 暂时硬编码 需要结合 proSecNav组件中的menuItems 14 | //TODO:初始化activeKey和panes,目前是写死初始化为/,home组件 15 | const routerList = [ 16 | '/', 17 | '/home', 18 | '/demo', 19 | '/parallax', 20 | '/dashboard', 21 | '/tilt', 22 | '/prism', 23 | '/three', 24 | '/echarts', 25 | '/video', 26 | '/crypto', 27 | ]; 28 | if (routerList.indexOf(pathname) === -1) return ; 29 | 30 | return props.children; 31 | }; 32 | 33 | export default AuthRouter; 34 | -------------------------------------------------------------------------------- /src/pages/coupons/add/index.jsx: -------------------------------------------------------------------------------- 1 | import FixTabPanel from '@/components/stateless/FixTabPanel'; 2 | import { useTabsStore } from '@/store/proTabsContext'; 3 | import { Alert, Button } from 'antd'; 4 | import React from 'react'; 5 | import { useNavigate } from 'react-router-dom'; 6 | 7 | const AddCoupons = () => { 8 | const { activeKey, removeTab } = useTabsStore(); 9 | const navigate = useNavigate(); 10 | const closeActiveOpenAngular = () => { 11 | removeTab(activeKey, () => { 12 | navigate('coupons/edit', { replace: true }); 13 | }); 14 | }; 15 | return ( 16 | 17 | 20 | 26 | 27 | ); 28 | }; 29 | 30 | export default AddCoupons; 31 | -------------------------------------------------------------------------------- /src/layout/proHeader/components/TabsHistory/index.module.less: -------------------------------------------------------------------------------- 1 | .section-layout-tabs { 2 | // height: 100%; 3 | 4 | :global { 5 | .cl-ant-tabs-nav > .cl-ant-tabs-nav-wrap > .cl-ant-tabs-nav-list > .cl-ant-tabs-tab { 6 | margin-left: 0; 7 | border-top: none; 8 | border-bottom: none; 9 | border-left: none; 10 | border-radius: 0; 11 | } 12 | 13 | .cl-ant-tabs-nav 14 | > .cl-ant-tabs-nav-wrap 15 | > .cl-ant-tabs-nav-list 16 | > .cl-ant-tabs-tab 17 | .cl-ant-tabs-tab-remove { 18 | margin-left: 0; 19 | } 20 | 21 | .cl-ant-tabs-nav { 22 | margin-bottom: 0 !important; 23 | background: var(--bg-color); 24 | // box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08); 25 | 26 | &::before { 27 | border: none !important; 28 | } 29 | } 30 | 31 | .cl-ant-tabs-nav .cl-ant-tabs-nav-wrap, 32 | div > .cl-ant-tabs-nav .cl-ant-tabs-nav-wrap { 33 | flex: 0 1 auto !important; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /rsbuild.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from '@rsbuild/core'; 2 | import { pluginReact } from '@rsbuild/plugin-react'; 3 | import UnoCSS from '@unocss/postcss'; 4 | import { pluginLess } from '@rsbuild/plugin-less'; 5 | 6 | export default defineConfig({ 7 | plugins: [pluginReact(),pluginLess()], 8 | tools: { 9 | postcss: { 10 | postcssOptions: { 11 | plugins: [UnoCSS()], 12 | }, 13 | }, 14 | }, 15 | server: { 16 | port: 9999, 17 | }, 18 | output: { 19 | sourceMap: { 20 | js: process.env.NODE_ENV === 'production' ? false : 'source-map', 21 | css: false, 22 | }, 23 | assetPrefix: '/react-antd-admin-pro', 24 | }, 25 | source: { 26 | define: { 27 | 'process.env.AUTH_USER': JSON.stringify(process.env.AUTH_USER), 28 | 'process.env.AUTH_PASSWORD': JSON.stringify(process.env.AUTH_PASSWORD), 29 | 'process.env.APP_BASE_URL': JSON.stringify(process.env.APP_BASE_URL), 30 | }, 31 | alias: { 32 | '@': './src', 33 | }, 34 | }, 35 | }); 36 | -------------------------------------------------------------------------------- /src/pages/coupons/index.jsx: -------------------------------------------------------------------------------- 1 | import FixTabPanel from '@/components/stateless/FixTabPanel'; 2 | import { Button } from 'antd'; 3 | import React from 'react'; 4 | import { useNavigate } from 'react-router-dom'; 5 | 6 | const Coupons = () => { 7 | const navigate = useNavigate(); 8 | const redirectTo = (path) => { 9 | navigate(path); 10 | }; 11 | 12 | return ( 13 | 14 | 17 | 20 | 23 | 30 | 31 | ); 32 | }; 33 | 34 | export default Coupons; 35 | -------------------------------------------------------------------------------- /src/layout/proSider/index.tsx: -------------------------------------------------------------------------------- 1 | import { MenuFoldOutlined, MenuUnfoldOutlined } from '@ant-design/icons'; 2 | import { Layout } from 'antd'; 3 | import React, { useState } from 'react'; 4 | 5 | const ProSider = ({ children }: { children: React.ReactNode }) => { 6 | const [collapsed, setCollapsed] = useState(false); 7 | 8 | const onCollapse = () => { 9 | setCollapsed(!collapsed); 10 | }; 11 | 12 | return ( 13 | 22 | {children} 23 |
24 | {collapsed ? ( 25 | 26 | ) : ( 27 | 28 | )} 29 |
30 |
31 | ); 32 | }; 33 | 34 | export default ProSider; 35 | -------------------------------------------------------------------------------- /src/utils/tryCatch/runPromise.js: -------------------------------------------------------------------------------- 1 | // https://github.com/rahuulmiishra/trendyAwait/blob/main/runPromise.js 2 | 3 | const runPromise = async (fn) => { 4 | try { 5 | const d = await fn() 6 | return [d, null] 7 | } catch (e) { 8 | return [null, e] 9 | } 10 | } 11 | 12 | const promise = () => 13 | new Promise((res) => { 14 | setTimeout(() => { 15 | console.log('1') 16 | res('promise') 17 | }, 3000) 18 | }) 19 | 20 | async function test() { 21 | const [res] = await runPromise(promise) 22 | console.log('23', res) 23 | } 24 | 25 | test() 26 | 27 | // Demonstration of using params. 28 | const fetchDataFromServer = (params) => () => 29 | new Promise((res) => { 30 | setTimeout(() => { 31 | res(`Received Data' ${params}`) 32 | }, 3000) 33 | }) 34 | 35 | async function main() { 36 | const [res1, error1] = await runPromise(fetchDataFromServer('123')) 37 | const [res2, error2] = await runPromise(fetchDataFromServer()) 38 | console.log('res1', res1) 39 | console.log('error1', error1) 40 | console.log('res2', res2) 41 | console.log('error2', error2) 42 | } 43 | 44 | main() 45 | -------------------------------------------------------------------------------- /src/ThemeIndex.tsx: -------------------------------------------------------------------------------- 1 | import { StyleProvider } from '@ant-design/cssinjs'; 2 | import { ConfigProvider, theme } from 'antd'; 3 | import 'antd/dist/reset.css'; 4 | import dayjs from 'dayjs'; 5 | import 'dayjs/locale/zh-cn'; 6 | import './styles/reset.css'; 7 | 8 | import App from './App'; 9 | import { useProThemeContext } from './theme/hooks'; 10 | import myThemes from './theme/index'; 11 | 12 | dayjs.locale('zh-cn'); 13 | 14 | const ThemeIndex = () => { 15 | const { myTheme } = useProThemeContext(); 16 | console.log(myTheme); 17 | ConfigProvider.config({ 18 | prefixCls: 'cl-ant', 19 | iconPrefixCls: 'cl-icon', 20 | }); 21 | return ( 22 | 23 | 31 | 32 | 33 | 34 | ); 35 | }; 36 | 37 | export default ThemeIndex; 38 | -------------------------------------------------------------------------------- /src/layout/proHeader/components/Breadcrumb/util.ts: -------------------------------------------------------------------------------- 1 | export const getRouteItem = (arrList, queryItem) => { 2 | let result 3 | if (Array.isArray(arrList)) { 4 | result = arrList.find((item) => item.key === queryItem || getRouteItem(item.children, queryItem)) 5 | } 6 | return result 7 | } 8 | 9 | export const getRouteList = (result, arrList, queryItem) => { 10 | if (Array.isArray(arrList)) { 11 | arrList.forEach((item) => { 12 | if (item.key === queryItem) { 13 | result.push({ 14 | path: item.path, 15 | key: item.key, 16 | name: item.name, 17 | isSubMenu: item.isSubMenu, 18 | i18nKey: item.i18nKey, 19 | }) 20 | } else { 21 | result.push({ 22 | path: item.path, 23 | key: item.key, 24 | name: item.name, 25 | isSubMenu: item.isSubMenu, 26 | i18nKey: item.i18nKey, 27 | }) 28 | getRouteList( 29 | result, 30 | getRouteItem(item.children, queryItem) ? [getRouteItem(item.children, queryItem)] : [], 31 | queryItem 32 | ) 33 | } 34 | }) 35 | } 36 | return result 37 | } 38 | -------------------------------------------------------------------------------- /src/layout/proHeader/index.tsx: -------------------------------------------------------------------------------- 1 | import antd from '@/assets/svg/antd.svg'; 2 | import { Layout, theme } from 'antd'; 3 | import { useNavigate } from 'react-router-dom'; 4 | import ProBreadcrumb from './components/Breadcrumb'; 5 | import { HeaderRight } from './components/HeaderRight'; 6 | import styles from './index.module.less'; 7 | 8 | const ProHeader = () => { 9 | const navigate = useNavigate(); 10 | const redirectTo = (path: string) => { 11 | navigate(path); 12 | }; 13 | 14 | const { 15 | token: { colorBgContainer, colorBorder }, 16 | } = theme.useToken(); 17 | 18 | return ( 19 | 23 |
24 | 28 | 31 |
32 | 33 |
34 | ); 35 | }; 36 | 37 | export default ProHeader; 38 | -------------------------------------------------------------------------------- /src/layout/fullscreen/index.tsx: -------------------------------------------------------------------------------- 1 | import { FullscreenExitOutlined, FullscreenOutlined } from '@ant-design/icons'; 2 | import { Space, Tooltip, message } from 'antd'; 3 | import { useEffect, useState } from 'react'; 4 | import screenfull from 'screenfull'; 5 | 6 | const FullScreen = ({ ele, tips = '全屏', placement = 'bottom' }: any) => { 7 | const [fullScreen, setFullScreen] = useState(false); 8 | 9 | useEffect(() => { 10 | screenfull.on('change', () => { 11 | if (screenfull.isFullscreen) setFullScreen(true); 12 | else setFullScreen(false); 13 | return () => screenfull.off('change', () => {}); 14 | }); 15 | }, []); 16 | 17 | const handleFullScreen = () => { 18 | if (!screenfull.isEnabled) message.warning('当前您的浏览器不支持全屏'); 19 | const dom = document.querySelector(ele) || undefined; 20 | screenfull.toggle(dom); 21 | }; 22 | return ( 23 | 24 | 25 | {fullScreen ? ( 26 | 27 | ) : ( 28 | 29 | )} 30 | 31 | 32 | ); 33 | }; 34 | export default FullScreen; 35 | -------------------------------------------------------------------------------- /src/pages/echarts/index.jsx: -------------------------------------------------------------------------------- 1 | import FixTabPanel from '@/components/stateless/FixTabPanel'; 2 | import ReactEcharts from 'echarts-for-react'; 3 | import React from 'react'; 4 | 5 | const Echarts = () => { 6 | const option = { 7 | title: { 8 | text: '某站点用户访问来源', 9 | subtext: '纯属虚构', 10 | x: 'center', 11 | }, 12 | tooltip: { 13 | trigger: 'item', 14 | formatter: '{a}
{b} : {c} ({d}%)', 15 | }, 16 | legend: { 17 | orient: 'vertical', 18 | left: 'left', 19 | data: ['直接访问', '邮件营销', '联盟广告', '视频广告', '搜索引擎'], 20 | }, 21 | series: [ 22 | { 23 | name: '访问来源', 24 | type: 'pie', 25 | radius: '55%', 26 | center: ['50%', '60%'], 27 | data: [ 28 | { value: 335, name: '直接访问' }, 29 | { value: 310, name: '邮件营销' }, 30 | { value: 234, name: '联盟广告' }, 31 | { value: 135, name: '视频广告' }, 32 | { value: 1548, name: '搜索引擎' }, 33 | ], 34 | itemStyle: { 35 | emphasis: { 36 | shadowBlur: 10, 37 | shadowOffsetX: 0, 38 | shadowColor: 'rgba(0, 0, 0, 0.5)', 39 | }, 40 | }, 41 | }, 42 | ], 43 | }; 44 | return ( 45 | 46 |

Welcome to echarts!

47 | 48 |
49 | ); 50 | }; 51 | 52 | export default Echarts; 53 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Rsbuild Deployment 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | 7 | workflow_dispatch: 8 | 9 | permissions: 10 | contents: read 11 | pages: write 12 | id-token: write 13 | 14 | concurrency: 15 | group: pages 16 | cancel-in-progress: false 17 | 18 | jobs: 19 | # Build job 20 | build: 21 | runs-on: ubuntu-latest 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@v4 25 | with: 26 | fetch-depth: 0 # Not needed if lastUpdated is not enabled 27 | - uses: pnpm/action-setup@v3 # pnpm is optional but recommended, you can also use npm / yarn 28 | with: 29 | version: 8 30 | - name: Setup Node 31 | uses: actions/setup-node@v4 32 | with: 33 | node-version: 20 34 | cache: pnpm 35 | - name: Setup Pages 36 | uses: actions/configure-pages@v5 37 | - name: Install dependencies 38 | run: pnpm install 39 | - name: Build with Rsbuild 40 | run: | 41 | pnpm run build 42 | - name: Upload artifact 43 | uses: actions/upload-pages-artifact@v3 44 | with: 45 | path: dist 46 | 47 | # Deployment job 48 | deploy: 49 | environment: 50 | name: github-pages 51 | url: ${{ steps.deployment.outputs.page_url }} 52 | needs: build 53 | runs-on: ubuntu-latest 54 | name: Deploy 55 | steps: 56 | - name: Deploy to GitHub Pages 57 | id: deployment 58 | uses: actions/deploy-pages@v4 59 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import pluginJs from '@eslint/js'; 2 | import reactPlugin from 'eslint-plugin-react'; 3 | import reactHooksPlugin from 'eslint-plugin-react-hooks'; 4 | import pluginReactConfig from 'eslint-plugin-react/configs/recommended.js'; 5 | import globals from 'globals'; 6 | import tseslint from 'typescript-eslint'; 7 | 8 | export default [ 9 | { languageOptions: { globals: globals.browser } }, 10 | pluginJs.configs.recommended, 11 | ...tseslint.configs.recommended, 12 | { 13 | languageOptions: pluginReactConfig.languageOptions, 14 | plugins: { 15 | react: reactPlugin, 16 | }, 17 | rules: { 18 | ...pluginReactConfig.rules, 19 | 'react/no-string-refs': 0, 20 | 'react/display-name': 0, 21 | 'react/no-direct-mutation-state': 0, 22 | 'react/prop-types': 0, 23 | 'react/jsx-no-undef': 0, 24 | 'react/jsx-uses-react': 0, 25 | 'react/jsx-uses-vars': 0, 26 | 'react/no-danger-with-children': 0, 27 | 'react/require-render-return': 0, 28 | 'react/react-in-jsx-scope': 0, 29 | }, 30 | }, 31 | { 32 | files: ['**/*.{js,jsx,mjs,cjs,ts,tsx}'], 33 | languageOptions: { 34 | parserOptions: { 35 | ecmaFeatures: { 36 | jsx: true, 37 | }, 38 | }, 39 | }, 40 | plugins: { 41 | 'react-hooks': reactHooksPlugin, 42 | }, 43 | rules: { 44 | 'react-hooks/rules-of-hooks': 'warn', 45 | 'react-hooks/exhaustive-deps': 'warn', 46 | }, 47 | settings: { 48 | react: { 49 | version: 'detect', 50 | }, 51 | }, 52 | }, 53 | ]; 54 | -------------------------------------------------------------------------------- /src/layout/proSecNav/index.tsx: -------------------------------------------------------------------------------- 1 | import { Menu } from 'antd'; 2 | import { useLocation, useNavigate } from 'react-router-dom'; 3 | 4 | import { useGlobalStore } from '@/store'; 5 | import { getKeyName } from '@/utils/publicFn'; 6 | import { useShallow } from 'zustand/shallow'; 7 | import styles from './index.module.less'; 8 | const ProSecNav = () => { 9 | const { menus, addTab, openKeys, selectedKeys, setOpenKeys, setSelectedKeys } = useGlobalStore( 10 | useShallow((state) => ({ 11 | menus: state.menus, 12 | addTab: state.addTab, 13 | openKeys: state.openKeys, 14 | setOpenKeys: state.setOpenKeys, 15 | selectedKeys: state.selectedKeys, 16 | setSelectedKeys: state.setSelectedKeys, 17 | })), 18 | ); 19 | const navigate = useNavigate(); 20 | const { pathname } = useLocation(); 21 | 22 | return ( 23 | String(item))} 30 | onOpenChange={(value) => setOpenKeys(value)} 31 | onSelect={async ({ key, selectedKeys, keyPath, item, domEvent }) => { 32 | if (pathname !== key) { 33 | const { tabKey, title, element, i18nKey } = getKeyName(key); 34 | addTab({ 35 | label: title, 36 | content: element, 37 | key: tabKey, 38 | closable: tabKey !== '/', 39 | path: key, 40 | i18nKey, 41 | }); 42 | navigate(key); 43 | setSelectedKeys([key]); 44 | } 45 | }} 46 | items={menus} 47 | /> 48 | ); 49 | }; 50 | 51 | export default ProSecNav; 52 | -------------------------------------------------------------------------------- /src/utils/menu/index.ts: -------------------------------------------------------------------------------- 1 | import { MenusItem } from '@/store/tabsSlice'; 2 | 3 | /** 4 | * 菜单搜索 5 | * @param {string} name 搜索值 6 | * @param {Array} menu 菜单 7 | * @returns 8 | */ 9 | export const findMenuItem = (name: string, menu: MenusItem[]) => { 10 | function findItemNameAndReturnNamePath( 11 | tree: MenusItem, 12 | treeName: string, 13 | ): { name: string; path: string }[] { 14 | let result = []; 15 | // 即可以通过key来查找也可以通过name查找 16 | if (tree.label.indexOf(treeName) > -1 || tree.key.indexOf(treeName) > -1) { 17 | if (tree.key) { 18 | result.push({ name: tree.label, path: tree.key }); 19 | } 20 | } 21 | 22 | let res; 23 | if (tree.children) { 24 | for (let i = 0; i < tree.children.length; i++) { 25 | res = findItemNameAndReturnNamePath(tree.children[i], treeName); 26 | if (res) { 27 | result = result.concat(res); 28 | // return result; 29 | } 30 | } 31 | } 32 | return result; 33 | } 34 | const result = []; 35 | for (let i = 0; i < menu.length; i++) { 36 | const item = menu[i]; 37 | const res = findItemNameAndReturnNamePath(item, name); 38 | if (res.length > 0) { 39 | result.push(...res); 40 | } 41 | } 42 | return result; 43 | }; 44 | 45 | /** 46 | * 寻找子菜单第一个path和name 47 | * @param {Array} menu 48 | * @returns 49 | */ 50 | export const findNestedChildrenFirstKeyAndLabel = ( 51 | menu: MenusItem | undefined, 52 | ): { label: string; key: string } => { 53 | if (!menu) { 54 | return { label: '', key: '' }; 55 | } 56 | if (menu.key) { 57 | return { 58 | label: menu.label, 59 | key: menu.key, 60 | }; 61 | } 62 | return findNestedChildrenFirstKeyAndLabel(menu?.children?.[0]); 63 | }; 64 | -------------------------------------------------------------------------------- /src/pages/signIn/LoginCard/index.less: -------------------------------------------------------------------------------- 1 | @property --rotate { 2 | syntax: ''; 3 | initial-value: 132deg; 4 | inherits: false; 5 | } 6 | 7 | @keyframes spin { 8 | 0% { 9 | --rotate: 0deg; 10 | } 11 | 12 | 100% { 13 | --rotate: 360deg; 14 | } 15 | } 16 | 17 | .login-card { 18 | min-width: 400px; 19 | min-height: 281px; 20 | background: rgba(233, 231, 230, 0.31); 21 | border: 1px solid rgba(163, 128, 21, 0.16); 22 | border-radius: 8px; 23 | 24 | &__inner { 25 | position: relative; 26 | margin: 12px; 27 | border-radius: 8px; 28 | 29 | &::before { 30 | position: absolute; 31 | top: -1%; 32 | left: -1%; 33 | z-index: 1; 34 | width: 102%; 35 | height: 102%; 36 | background-image: linear-gradient(var(--rotate), #eae2f7, transparent, #eae2f7); 37 | border-radius: 8px; 38 | animation: spin 4.5s linear infinite; 39 | content: ''; 40 | } 41 | 42 | &::after { 43 | position: absolute; 44 | top: 30px; 45 | right: 0; 46 | left: 0; 47 | z-index: 1; 48 | width: 106%; 49 | height: 100%; 50 | margin: 0 auto; 51 | background-image: linear-gradient(var(--rotate), #d9effc, #ed7ead, #ebf190); 52 | transform: scale(0.8); 53 | opacity: 1; 54 | filter: blur(calc(281px / 6)); 55 | transition: opacity 0.5s; 56 | animation: spin 2.5s linear infinite; 57 | content: ''; 58 | } 59 | 60 | & .login-form-title { 61 | margin-bottom: 24px; 62 | color: #000; 63 | font-weight: bold; 64 | font-size: 26px; 65 | text-align: center; 66 | background: linear-gradient(180deg, #84cffb 0%, #4096ff 100%); 67 | background-clip: text; 68 | -webkit-text-fill-color: transparent; 69 | } 70 | 71 | & .login-content { 72 | z-index: 2; 73 | position: relative; 74 | background: #ece9e9ee; 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/layout/proContent/index.tsx: -------------------------------------------------------------------------------- 1 | import { VerticalAlignTopOutlined } from '@ant-design/icons'; 2 | import { FloatButton, Layout, Space, theme } from 'antd'; 3 | import ProTabs from '../proTabs'; 4 | import styles from './index.module.less'; 5 | 6 | const { Content, Footer } = Layout; 7 | 8 | const ProContent = () => { 9 | // const [tabActiveKey, setTabActiveKey] = useState('home'); 10 | // const { activeKey, panes } = useGlobalStore(); 11 | // const [panesItem, setPanesItem] = useState({ 12 | // title: '', 13 | // content: null, 14 | // key: '', 15 | // closable: false, 16 | // path: '', 17 | // i18nKey: '', 18 | // }); 19 | 20 | // const pathRef = useRef(''); 21 | // const { pathname, search } = useLocation(); 22 | const { 23 | token: { colorBgContainer }, 24 | } = theme.useToken(); 25 | // useEffect(() => { 26 | // const { tabKey, title, element, i18nKey } = getKeyName(pathname); 27 | // const newPath = search ? pathname + search : pathname; 28 | // pathRef.current = newPath; 29 | 30 | // setPanesItem({ 31 | // title, 32 | // content: element, 33 | // key: tabKey, 34 | // closable: tabKey !== '/', 35 | // path: newPath, 36 | // i18nKey, 37 | // }); 38 | // setTabActiveKey(tabKey); 39 | // }, [pathname, search, panes, activeKey]); 40 | 41 | return ( 42 | 43 | 44 | 45 | 46 |
47 | document.querySelector('#container') ?? document.body} 49 | > 50 | 51 | 52 | © {new Date().getFullYear()} React Antd Admin Pro 53 |
54 |
55 | ); 56 | }; 57 | 58 | export default ProContent; 59 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rspress-rsbuild-react-admin-pro", 3 | "private": true, 4 | "version": "1.0.0", 5 | "scripts": { 6 | "start": "pnpm run dev", 7 | "dev": "rsbuild dev --open", 8 | "build": "rsbuild build", 9 | "preview": "rsbuild preview" 10 | }, 11 | "dependencies": { 12 | "@ant-design/cssinjs": "^1.22.1", 13 | "@ant-design/icons": "^5.5.2", 14 | "@tanstack/react-query": "^5.64.1", 15 | "ahooks": "^3.8.4", 16 | "antd": "^5.23.1", 17 | "axios": "^1.7.9", 18 | "clsx": "^2.1.1", 19 | "dayjs": "^1.11.13", 20 | "echarts": "^5.6.0", 21 | "echarts-for-react": "^3.0.2", 22 | "html2canvas": "^1.4.1", 23 | "i18next": "^24.2.1", 24 | "i18next-browser-languagedetector": "^8.0.2", 25 | "jsencrypt": "^3.3.2", 26 | "lodash-es": "^4.17.21", 27 | "nanoid": "^5.0.9", 28 | "paj-md5.js": "^1.0.1", 29 | "prettier": "^3.4.2", 30 | "qs": "^6.13.1", 31 | "radash": "^12.1.0", 32 | "rc-util": "^5.44.3", 33 | "react": "^18.3.1", 34 | "react-avatar-editor": "^13.0.2", 35 | "react-dom": "^18.3.1", 36 | "react-error-boundary": "^5.0.0", 37 | "react-i18next": "^15.4.0", 38 | "react-router-dom": "^7.1.1", 39 | "react-sticky": "^6.0.3", 40 | "react-svg": "^16.2.0", 41 | "screenfull": "^6.0.2", 42 | "tailwind-merge": "^2.6.0", 43 | "zustand": "^5.0.3" 44 | }, 45 | "devDependencies": { 46 | "@eslint/js": "^9.18.0", 47 | "@rsbuild/core": "1.1.13", 48 | "@rsbuild/plugin-less": "1.1.0", 49 | "@rsbuild/plugin-react": "1.1.0", 50 | "@types/lodash-es": "^4.17.12", 51 | "@types/node": "^22.10.6", 52 | "@types/react": "^18.3.18", 53 | "@types/react-dom": "^18.3.5", 54 | "@unocss/postcss": "^65.4.0", 55 | "eslint": "^9.18.0", 56 | "eslint-plugin-react": "^7.37.4", 57 | "eslint-plugin-react-hooks": "5.1.0", 58 | "globals": "^15.14.0", 59 | "typescript": "^5.7.3", 60 | "typescript-eslint": "^8.20.0", 61 | "unocss": "^65.4.0" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/routers/index.tsx: -------------------------------------------------------------------------------- 1 | import Loading from '@/components/stateless/Loading'; 2 | import { Suspense, lazy } from 'react'; 3 | 4 | /** 5 | * 带有回退到加载组件的组件延迟加载器 6 | */ 7 | const lazyLoad = (Component: React.LazyExoticComponent<() => JSX.Element>): React.ReactElement => ( 8 | }> 9 | 10 | 11 | ); 12 | 13 | // 以下路由可根据需求另分成不同的文件维护 14 | // 结合 proSecNav组件中的menuItems 15 | 16 | const SignIn = lazy(() => import('@/pages/signIn')); 17 | const Layout = lazy(() => import('@/layout')); 18 | const Home = lazy(() => import('@/pages/home')); 19 | const Demo = lazy(() => import('@/pages/demo')); 20 | const Exception403 = lazy(() => import('@/components/stateless/Exception/exception403')); 21 | const NoMatch = lazy(() => import('@/components/stateless/NoMatch')); 22 | 23 | const rootRouter = [ 24 | { 25 | path: '/', 26 | name: '首页', 27 | i18nKey: 'home', 28 | key: '/', 29 | auth: true, 30 | element: lazyLoad(Layout), 31 | children: [ 32 | { 33 | index: true, 34 | name: '首页', 35 | key: '/', 36 | i18nKey: 'home', 37 | auth: true, 38 | element: lazyLoad(Home), 39 | }, 40 | { 41 | index: false, 42 | path: 'demo', 43 | name: 'Demo', 44 | i18nKey: 'demo', 45 | key: '/demo', 46 | auth: true, 47 | element: lazyLoad(Demo), 48 | }, 49 | { 50 | path: '*', 51 | name: 'No Match', 52 | key: '*', 53 | element: lazyLoad(NoMatch), 54 | }, 55 | ], 56 | }, 57 | { 58 | index: false, 59 | path: 'signIn', 60 | name: '登录', 61 | key: '/signIn', 62 | auth: false, 63 | element: lazyLoad(SignIn), 64 | }, 65 | { 66 | index: false, 67 | path: '/403', 68 | name: '403', 69 | key: '/403', 70 | auth: false, 71 | element: lazyLoad(Exception403), 72 | }, 73 | { 74 | path: '*', 75 | name: 'No Match', 76 | key: '*', 77 | element: lazyLoad(NoMatch), 78 | }, 79 | ]; 80 | 81 | export default rootRouter; 82 | -------------------------------------------------------------------------------- /src/utils/publicFn/index.tsx: -------------------------------------------------------------------------------- 1 | import Exception404 from '@/components/stateless/Exception/exception404'; 2 | import routes from '@/routers/index'; 3 | 4 | export const flattenRoutes = (arr) => 5 | arr.reduce((prev, item) => { 6 | if (Array.isArray(item.children)) { 7 | prev.push(item); 8 | } 9 | return prev.concat(Array.isArray(item.children) ? flattenRoutes(item.children) : item); 10 | }, []); 11 | 12 | export const getKeyName = (pathName = '/404') => { 13 | const thePath = pathName.split('?')[0]; 14 | const curRoute = flattenRoutes(routes) 15 | .filter((item) => !item.index) 16 | .filter((item) => item.key?.indexOf(thePath) !== -1); 17 | if (!curRoute[0]) { 18 | return { 19 | title: 'Not Found', 20 | tabKey: '/404', 21 | element: , 22 | i18nKey: 'notFound', 23 | }; 24 | } 25 | 26 | const { name, key, element, index, path, auth, i18nKey } = curRoute[0]; 27 | return { index: index ?? false, path, auth, title: name, tabKey: key, element, i18nKey }; 28 | }; 29 | 30 | export const getLocalStorage = (key: string) => { 31 | const value = window.localStorage.getItem(key); 32 | try { 33 | if (value) { 34 | return JSON.parse(value); 35 | } else { 36 | return value; 37 | } 38 | } catch (error) { 39 | return value; 40 | } 41 | }; 42 | 43 | /** 44 | * Sets the value of a key in local storage. 45 | * 46 | * @param {string} key - The key to set in local storage. 47 | * @param {any} value - The value to set for the key. 48 | * @return {void} This function does not return a value. 49 | */ 50 | export const setLocalStorage = (key: string, value: any): void => { 51 | window.localStorage.setItem(key, JSON.stringify(value)); 52 | }; 53 | 54 | /** 55 | * Removes a key from local storage. 56 | * 57 | * @param {string} key - The key to remove from local storage. 58 | * @return {void} This function does not return a value. 59 | */ 60 | export const removeLocalStorage = (key: string): void => { 61 | window.localStorage.removeItem(key); 62 | }; 63 | 64 | export const clearLocalStorage = () => { 65 | window.localStorage.clear(); 66 | }; 67 | -------------------------------------------------------------------------------- /src/layout/proHeader/components/Breadcrumb/index.tsx: -------------------------------------------------------------------------------- 1 | import rootRouter from '@/routers'; 2 | import { Breadcrumb, Button } from 'antd'; 3 | import { useEffect, useState } from 'react'; 4 | import { useTranslation } from 'react-i18next'; 5 | import { useLocation, useNavigate } from 'react-router-dom'; 6 | import styles from './index.module.less'; 7 | import { getRouteItem, getRouteList } from './util'; 8 | 9 | const ProBreadcrumb = () => { 10 | const { pathname } = useLocation(); 11 | const navigate = useNavigate(); 12 | const [breadcrumbList, setBreadcrumbList] = useState([]); 13 | 14 | const { t } = useTranslation(); 15 | 16 | useEffect(() => { 17 | const routeList = getRouteList( 18 | [], 19 | getRouteItem(rootRouter, pathname) ? [getRouteItem(rootRouter, pathname)] : [], 20 | pathname, 21 | ); 22 | 23 | if (routeList.length === 0) { 24 | setBreadcrumbList([ 25 | { path: '/', name: '首页', key: '/', i18nKey: 'home', isSubMenu: false }, 26 | { path: '404', name: 'Not Found', key: '/404', i18nKey: 'notFound', isSubMenu: false }, 27 | ]); 28 | } else { 29 | setBreadcrumbList([...routeList]); 30 | } 31 | }, [pathname]); 32 | 33 | const linkTo = (path) => { 34 | navigate(path); 35 | }; 36 | 37 | const breadcrumbItem = () => 38 | breadcrumbList.map((item, index) => ({ 39 | title: 40 | index !== breadcrumbList.length - 1 ? ( 41 | 42 | {item.isSubMenu ? ( 43 | 46 | ) : ( 47 | 50 | )} 51 | 52 | ) : ( 53 | 54 | {item.i18nKey ? t(item.i18nKey) : item.name} 55 | 56 | ), 57 | key: item.key, 58 | })); 59 | 60 | return ; 61 | }; 62 | 63 | export default ProBreadcrumb; 64 | -------------------------------------------------------------------------------- /src/layout/proHeader/components/TabsHistory/inex.tsx: -------------------------------------------------------------------------------- 1 | import TabTools from '@/layouts/basicLayout/components/TabTools'; 2 | import { useGlobalStore } from '@/store'; 3 | import { CloseCircleFilled } from '@ant-design/icons'; 4 | import { useSize } from 'ahooks'; 5 | import { Tabs } from 'antd'; 6 | import { useNavigate } from 'react-router-dom'; 7 | import styles from './index.module.less'; 8 | 9 | // const { TabPane } = panes; 10 | 11 | const OperationsSlot = { 12 | right: , 13 | }; 14 | export const TabsHistory = () => { 15 | const navigate = useNavigate(); 16 | const { activeKey, setActiveKey, panes, setPanes, removeTab } = useGlobalStore(); 17 | 18 | /** 19 | * tabs编辑 20 | * @param {string} targetKey 被编辑的tab‘s key 21 | * @param {string} action 删除或者添加 22 | */ 23 | const handleTabsEdit = ( 24 | targetKey: string | React.MouseEvent | React.KeyboardEvent, 25 | action: string, 26 | ) => { 27 | if (action === 'remove' && typeof targetKey === 'string') removeTab(targetKey); 28 | }; 29 | 30 | /** 31 | * tab切换时,路由要变化 32 | * @param {string} TabActiveKey 33 | */ 34 | const handleTabsChange = (TabActiveKey: string) => { 35 | const { path } = panes.filter((item) => item.key === TabActiveKey)[0]; 36 | navigate(path); 37 | setActiveKey(TabActiveKey); 38 | }; 39 | 40 | const { width: headerRightWidth } = useSize(document.querySelector('#RootHeaderRight')) ?? { 41 | width: 390, 42 | }; 43 | 44 | return ( 45 |
51 | { 60 | // const comp = getRouteCom(item.path); 61 | return { 62 | label: item.name, 63 | key: item.path, 64 | closeIcon: , 65 | }; 66 | })} 67 | /> 68 |
69 | ); 70 | }; 71 | -------------------------------------------------------------------------------- /src/styles/reset.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | height: 100%; 4 | overflow: hidden; 5 | text-size-adjust: 100%; 6 | box-sizing: border-box; 7 | scrollbar-width: thin; 8 | font-size: 16px; 9 | } 10 | 11 | *, 12 | *::before, 13 | *::after { 14 | box-sizing: inherit; 15 | scrollbar-width: inherit; 16 | } 17 | 18 | *::-webkit-scrollbar { 19 | width: 6px; 20 | height: 6px; 21 | } 22 | 23 | *::-webkit-scrollbar-thumb { 24 | background: #7e7e7e; 25 | border-radius: 3px; 26 | } 27 | 28 | input:-webkit-autofill, 29 | input:-webkit-autofill:hover, 30 | input:-webkit-autofill:focus, 31 | input:-webkit-autofill:active, 32 | textarea:-webkit-autofill, 33 | textarea:-webkit-autofill:hover, 34 | textarea:-webkit-autofill:focus, 35 | textarea:-webkit-autofill:active, 36 | select:-webkit-autofill, 37 | select:-webkit-autofill:hover, 38 | select:-webkit-autofill:focus, 39 | select:-webkit-autofill:active { 40 | transition: 41 | background-color 600000s ease-in-out 0s, 42 | color 600000s ease-in-out 0s; 43 | } 44 | 45 | .text-nowrap { 46 | white-space: nowrap; 47 | text-overflow: ellipsis; 48 | overflow: hidden; 49 | } 50 | 51 | .text-more-line-nowrap { 52 | overflow: hidden; 53 | text-overflow: ellipsis; 54 | 55 | /* display: -webkit-box; */ 56 | -webkit-line-clamp: 2; 57 | -webkit-box-orient: vertical; 58 | } 59 | 60 | #root { 61 | height: 100%; 62 | width: 100%; 63 | } 64 | 65 | .layout-footer { 66 | text-align: center; 67 | font-size: 14px; 68 | height: 40px; 69 | line-height: 40px; 70 | padding: 0 !important; 71 | } 72 | 73 | .cl-ant-layout-header { 74 | padding-left: 0 !important; 75 | padding-right: 0 !important; 76 | } 77 | 78 | .cl-ant-breadcrumb-separator { 79 | line-height: 56px; 80 | } 81 | 82 | .layout-header { 83 | padding-left: 20px !important; 84 | } 85 | 86 | .layout-content { 87 | height: 100%; 88 | width: 100%; 89 | overflow: hidden; 90 | } 91 | 92 | .layout-container { 93 | overflow: auto; 94 | height: 100%; 95 | width: 100%; 96 | } 97 | 98 | .layout-container .cl-ant-tabs-content > .cl-ant-tabs-tabpane { 99 | padding: 16px; 100 | } 101 | 102 | .layout-container .pro-tabs { 103 | z-index: 10; 104 | } 105 | 106 | .layout-container .pro-tabs .cl-ant-tabs-nav-wrap { 107 | overflow: visible !important; 108 | } 109 | 110 | .layout-container .layout-tabpanel { 111 | width: 100%; 112 | min-height: calc(100vh - 232px); 113 | display: flex; 114 | justify-content: center; 115 | align-items: center; 116 | } 117 | -------------------------------------------------------------------------------- /src/assets/svg/v8-outline.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/layout/proHeader/components/UserInfo/ChangeAvatar/index.tsx: -------------------------------------------------------------------------------- 1 | import { UploadOutlined } from '@ant-design/icons'; 2 | 3 | import { Button, Modal, Slider, Upload } from 'antd'; 4 | import type { RcFile } from 'antd/lib/upload'; 5 | 6 | import { useRef, useState } from 'react'; 7 | import AvatarEditor from 'react-avatar-editor'; 8 | import './index.less'; 9 | 10 | export const ChangeAvatar: React.FC<{ 11 | isChangeAvatarVisible: boolean; 12 | onCancel: () => void; 13 | }> = ({ isChangeAvatarVisible = false, onCancel }) => { 14 | const [zoom, setZoom] = useState(1); 15 | const [selectedFile, setSelectedFile] = useState(() => ``); 16 | // const [previewFile, setPreviewFile] = useState(''); 17 | const editor = useRef(null); 18 | const props = { 19 | name: 'file', 20 | action: '#', 21 | beforeUpload: async (file: RcFile) => { 22 | console.log('🚀 ~ file: index.tsx ~ line 30 ~ beforeUpload: ~ file', file); 23 | setSelectedFile(file); 24 | return false; 25 | }, 26 | }; 27 | const handleOk = () => { 28 | const canvas = editor.current.getImageScaledToCanvas(); 29 | canvas.toBlob(async (blob: Blob) => { 30 | // const formData = new FormData(); 31 | // formData.append('userId', getUserInfoFromLocalStorageByKey('userId')); 32 | // formData.append('file', blob); 33 | // try { 34 | // await uploadUserAvatarApi(formData); 35 | // // await uploadLogo(formData); 36 | // onCancel(); 37 | // setAvatarRandom(Date.now().toString()); 38 | // } catch (error) { 39 | // console.log('%c [ error ]', 'font-size:13px; background:pink; color:#bf2c9f;', error); 40 | // } 41 | }); 42 | }; 43 | 44 | return ( 45 | 56 |
57 | 68 |
69 |
70 |
71 | 72 | 75 | 76 | 77 | setZoom(value)} 85 | /> 86 |
87 |
88 |
89 | ); 90 | }; 91 | -------------------------------------------------------------------------------- /src/hooks/usePagination/index.ts: -------------------------------------------------------------------------------- 1 | import type { QueryKey } from '@tanstack/react-query'; 2 | import { useQuery, UseQueryOptions, UseQueryResult } from '@tanstack/react-query'; 3 | import { useMemoizedFn } from 'ahooks'; 4 | import { isEqual } from 'lodash-es'; 5 | import { useEffect, useMemo, useRef, useState } from 'react'; 6 | 7 | // import useMemoizedFn from '../useMemoizedFn'; 8 | // import useRequest from '../useRequest'; 9 | export declare type QueryFunction = (param: { 10 | pageSize: number; 11 | current: number; 12 | }) => T | Promise; 13 | 14 | const usePagination = < 15 | TQueryFnData = unknown, 16 | TError = unknown, 17 | TData = TQueryFnData, 18 | TQueryKey extends QueryKey = QueryKey, 19 | >( 20 | options: Omit< 21 | UseQueryOptions, 22 | 'initialData' | 'queryFn' 23 | > & { 24 | initialData?: () => undefined; 25 | defaultPageSize?: number; 26 | defaultCurrent?: number; 27 | queryFn: QueryFunction; 28 | }, 29 | ) => { 30 | const { defaultPageSize = 10, defaultCurrent = 1, queryKey, queryFn, ...rest } = options; 31 | const [current, setCurrent] = useState(defaultCurrent); 32 | const [pageSize, setPageSize] = useState(defaultPageSize); 33 | 34 | const result = useQuery<{ list: unknown[]; total: number }>({ 35 | queryKey: [...queryKey, current, pageSize], 36 | queryFn: () => queryFn({ current, pageSize }), 37 | keepPreviousData: true, 38 | ...rest, 39 | }); 40 | // const result = useQuery( 41 | // [...queryKey, current, pageSize], 42 | // () => queryFn({ current, pageSize }), 43 | // { keepPreviousData: true, staleTime: 5 * 1e3 }, 44 | // // ...rest, 45 | // ); 46 | //当queryKey改变后要将current重置到第一页 47 | const previousKeyRef = useRef(queryKey); 48 | useEffect(() => { 49 | console.log(queryKey, previousKeyRef.current); 50 | if (!isEqual(queryKey, previousKeyRef.current)) { 51 | previousKeyRef.current = queryKey; 52 | setCurrent(1); 53 | } 54 | }, [queryKey]); 55 | 56 | const total = result.data?.total || 0; 57 | const totalPage = useMemo(() => Math.ceil(total / pageSize), [pageSize, total]); 58 | 59 | const onChange = (c: number, p: number) => { 60 | let toCurrent = c <= 0 ? 1 : c; 61 | const toPageSize = p <= 0 ? 1 : p; 62 | const tempTotalPage = Math.ceil(total / toPageSize); 63 | if (toCurrent > tempTotalPage) { 64 | toCurrent = Math.max(1, tempTotalPage); 65 | } 66 | setCurrent(c); 67 | setPageSize(p); 68 | }; 69 | 70 | const changeCurrent = (c: number) => { 71 | onChange(c, pageSize); 72 | }; 73 | 74 | const changePageSize = (p: number) => { 75 | onChange(current, p); 76 | }; 77 | 78 | return { 79 | ...result, 80 | pagination: { 81 | current, 82 | pageSize, 83 | total, 84 | totalPage, 85 | onChange: useMemoizedFn(onChange), 86 | changeCurrent: useMemoizedFn(changeCurrent), 87 | changePageSize: useMemoizedFn(changePageSize), 88 | }, 89 | } as UseQueryResult & { pagination: any }; 90 | }; 91 | 92 | export default usePagination; 93 | -------------------------------------------------------------------------------- /src/layout/proHeader/components/HeaderSearch/index.tsx: -------------------------------------------------------------------------------- 1 | import { SearchOutlined } from '@ant-design/icons'; 2 | import type { AutoCompleteProps } from 'antd'; 3 | import { AutoComplete } from 'antd'; 4 | import type { DefaultOptionType } from 'antd/es/select'; 5 | import clsx from 'clsx'; 6 | import useMergedState from 'rc-util/es/hooks/useMergedState'; 7 | import { useRef, useState } from 'react'; 8 | import styles from './index.module.less'; 9 | 10 | export type HeaderSearchProps = { 11 | onSearch?: (value?: string) => void; 12 | onChange?: (value?: string) => void; 13 | onVisibleChange?: (b: boolean) => void; 14 | onSelect?: (value: DefaultOptionType) => void; 15 | onBlur?: () => void; 16 | onFocus?: () => void; 17 | className?: string; 18 | placeholder?: string; 19 | options: AutoCompleteProps['options']; 20 | defaultOpen?: boolean; 21 | open?: boolean; 22 | value?: string; 23 | }; 24 | 25 | const HeaderSearch: React.FC = (props) => { 26 | const { className, onVisibleChange, placeholder, open, defaultOpen, ...restProps } = props; 27 | 28 | const inputRef = useRef(null); 29 | 30 | const [value, setValue] = useState(''); 31 | 32 | const [searchMode, setSearchMode] = useMergedState(defaultOpen ?? false, { 33 | value: open, 34 | onChange: onVisibleChange, 35 | }); 36 | 37 | const inputClass = clsx(styles.input, { 38 | [styles.show]: searchMode, 39 | }); 40 | 41 | return ( 42 |
{ 45 | setSearchMode(true); 46 | if (inputRef.current) { 47 | inputRef.current.focus(); 48 | } 49 | }} 50 | onTransitionEnd={({ propertyName }) => { 51 | if (propertyName === 'width' && !searchMode) { 52 | if (onVisibleChange) { 53 | onVisibleChange(searchMode); 54 | } 55 | } 56 | }} 57 | > 58 | 65 | 66 | { 76 | if (restProps.onSelect) { 77 | restProps.onSelect(options); 78 | } 79 | }} 80 | onChange={(valueParams) => { 81 | setValue(valueParams); 82 | if (restProps.onSearch) { 83 | restProps.onSearch(valueParams); 84 | } 85 | }} 86 | onBlur={() => { 87 | setSearchMode(false); 88 | setValue(''); 89 | if (restProps.onBlur) { 90 | restProps.onBlur(); 91 | } 92 | }} 93 | onFocus={restProps.onFocus} 94 | /> 95 |
96 | ); 97 | }; 98 | 99 | export default HeaderSearch; 100 | -------------------------------------------------------------------------------- /src/pages/signIn/index.tsx: -------------------------------------------------------------------------------- 1 | import bgImage from '@/assets/images/signIn/bg.jpg'; 2 | import AlignCenter from '@/components/stateless/AlignCenter'; 3 | import { useGlobalStore } from '@/store'; 4 | import { setLocalStorage } from '@/utils/publicFn'; 5 | import { LockOutlined, UserOutlined } from '@ant-design/icons'; 6 | import { Button, Checkbox, Form, Input, Layout, theme } from 'antd'; 7 | import dayjs from 'dayjs'; 8 | import { useNavigate } from 'react-router-dom'; 9 | import { LoginCard } from './LoginCard'; 10 | 11 | const { Content } = Layout; 12 | 13 | const layout = { 14 | wrapperCol: { span: 24 }, 15 | }; 16 | const tailLayout = { 17 | wrapperCol: { offset: 0, span: 16 }, 18 | }; 19 | 20 | const SignIn = () => { 21 | const navigate = useNavigate(); 22 | const updateUserInfo = useGlobalStore((state) => state.updateUserInfo); 23 | 24 | const { 25 | token: { colorBgContainer }, 26 | } = theme.useToken(); 27 | 28 | const onFinish = (values: { username: string; password: string; remember: boolean }) => { 29 | // 模拟后端登录 30 | const { username } = values; 31 | setLocalStorage('token', { token: username }); 32 | navigate('/'); 33 | updateUserInfo({ 34 | username: username, 35 | lastLogin: dayjs().format('YYYY-MM-DD HH:mm:ss'), 36 | roleName: '管理员', 37 | }); 38 | }; 39 | 40 | return ( 41 | 42 |
49 | 50 | 51 | 52 |
53 |
React Antd Admin Pro
54 |
用户登录
55 |
65 | 66 | } placeholder="请输入用户名" /> 67 | 68 | 69 | 70 | } /> 71 | 72 | 73 | 74 | 记住用户名 75 | 76 | 77 | 78 | 81 | 82 |
83 |
84 |
85 |
86 |
87 |
88 | ); 89 | }; 90 | 91 | export default SignIn; 92 | -------------------------------------------------------------------------------- /src/layout/proHeader/components/UserInfo/index.tsx: -------------------------------------------------------------------------------- 1 | import { useGlobalStore } from '@/store'; 2 | import { removeLocalStorage } from '@/utils/publicFn'; 3 | import { DownOutlined, LogoutOutlined, TeamOutlined, UserOutlined } from '@ant-design/icons'; 4 | import { Avatar, Button, Card, Popover, Tooltip } from 'antd'; 5 | import dayjs from 'dayjs'; 6 | import { useState } from 'react'; 7 | import { useNavigate } from 'react-router-dom'; 8 | import { ChangeAvatar } from './ChangeAvatar'; 9 | import { ChangePassword } from './ChangePassword'; 10 | import './index.less'; 11 | 12 | const { Meta } = Card; 13 | export const UserInfo = () => { 14 | const navigate = useNavigate(); 15 | const userInfo = useGlobalStore((state) => state.userInfo); 16 | const [isChangeAvatarVisible, setIsChangeAvatarVisible] = useState(false); 17 | const [isChangePasswordVisible, setIsChangePasswordVisible] = useState(false); 18 | 19 | const handleCancel = () => { 20 | setIsChangeAvatarVisible(false); 21 | }; 22 | const handleCancel2 = () => { 23 | setIsChangePasswordVisible(false); 24 | }; 25 | 26 | const handleLogout = () => { 27 | removeLocalStorage('token'); 28 | useGlobalStore.setState({ userInfo: { username: '', lastLogin: '', roleName: '' } }); 29 | navigate('/signIn', { replace: true }); 30 | }; 31 | 32 | const CardContain = ( 33 | 38 | 39 | 退出 40 | , 41 | ]} 42 | > 43 | setIsChangeAvatarVisible(true)} className="cursor-pointer"> 47 | } /> 48 | 49 | } 50 | title={ 51 |
52 | {userInfo?.username} 53 | 54 | 57 |
58 | } 59 | description={ 60 |
61 | {userInfo?.lastLogin 62 | ? `上次登录时间:${dayjs(userInfo?.lastLogin).format('YYYY-MM-DD HH:mm:ss')}` 63 | : null} 64 |
65 | } 66 | /> 67 | 68 |
69 | 70 | 71 | 72 |
{userInfo.roleName}
73 |
74 |
75 | ); 76 | return ( 77 |
78 | 79 |
80 | } /> 81 | {userInfo?.username || '管理员'} 82 | 83 |
84 |
85 | {isChangeAvatarVisible && ( 86 | 87 | )} 88 | {isChangePasswordVisible && ( 89 | 93 | )} 94 |
95 | ); 96 | }; 97 | -------------------------------------------------------------------------------- /src/layout/proHeader/components/SwitchWorkspace/index.tsx: -------------------------------------------------------------------------------- 1 | // import { findNestedChildrenFirstPathAndName } from '@r5/shared/utils'; 2 | import { DownOutlined } from '@ant-design/icons'; 3 | import { Dropdown, Menu, Spin } from 'antd'; 4 | import { useReducer } from 'react'; 5 | // import './index.less'; 6 | // import clsx from 'clsx'; 7 | import { useGlobalStore } from '@/store'; 8 | import { defaultMenus, testMenus } from '@/store/tabsSlice'; 9 | import { findNestedChildrenFirstKeyAndLabel } from '@/utils/menu'; 10 | import { useLocation, useNavigate } from 'react-router-dom'; 11 | import { useShallow } from 'zustand/shallow'; 12 | import styles from './index.module.less'; 13 | // function loop(menuParam: PortalApi.MenuItem, parent?: PortalApi.MenuItem[0]) { 14 | // for (let i = 0; i < menuParam.length; i++) { 15 | // const menuItem = menuParam[i]; 16 | // menuItem.parent = parent; 17 | // if (menuItem.children) { 18 | // loop(menuItem.children, menuItem); 19 | // } 20 | // } 21 | // return menuParam; 22 | // } 23 | export const SwitchWorkspace = () => { 24 | const { pathname } = useLocation(); 25 | const navigate = useNavigate(); 26 | const { 27 | workspaces: workspaceList, 28 | activeWorkspace: workspaceKey, 29 | panes, 30 | setActiveWorkspace, 31 | setMenus, 32 | setActiveKey, 33 | setPanes, 34 | } = useGlobalStore( 35 | useShallow((state) => ({ 36 | workspaces: state.workspaces, 37 | activeWorkspace: state.activeWorkspace, 38 | panes: state.panes, 39 | setActiveWorkspace: state.setActiveWorkspace, 40 | setMenus: state.setMenus, 41 | setActiveKey: state.setActiveKey, 42 | setPanes: state.setPanes, 43 | })), 44 | ); 45 | const [spinning, toggleSpinning] = useReducer((x) => !x, false); 46 | 47 | const workspaceName = workspaceList.find((item) => item.code === workspaceKey)?.label; 48 | const handleMenuClick = async (e: any) => { 49 | toggleSpinning(); 50 | // const menu = await getMenuApi({ 51 | // projectName: e.key, 52 | // id: initialData.userInfo?.id ?? '', 53 | // }); 54 | // const res = loop(menu, undefined); 55 | setActiveWorkspace(e.key); 56 | let tmp; 57 | //get menu from workspace then set menu 58 | if (e.key === 'default') { 59 | setMenus(defaultMenus); 60 | tmp = defaultMenus; 61 | } else { 62 | setMenus(testMenus); 63 | tmp = testMenus; 64 | } 65 | const { key } = findNestedChildrenFirstKeyAndLabel(tmp[0]); 66 | setActiveKey(key); 67 | if (key != pathname) { 68 | navigate(key); 69 | } 70 | 71 | toggleSpinning(); 72 | }; 73 | 74 | const dropDownList = ( 75 | { 80 | return { label: item.label, key: item.code }; 81 | })} 82 | > 83 | {/* {workspaceList?.map((item) => ( 84 | {item.describe} 85 | ))} */} 86 | 87 | ); 88 | return ( 89 |
90 |
91 | dropDownList} placement="bottom" arrow trigger={['click']}> 92 |
93 |
{workspaceName}
94 |
95 |
96 |
97 | {spinning && ( 98 |
99 | 100 |
101 | )} 102 |
103 | ); 104 | }; 105 | -------------------------------------------------------------------------------- /src/assets/svg/antd.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Group 28 Copy 5 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 42 | 43 | -------------------------------------------------------------------------------- /src/pages/crypto/index.jsx: -------------------------------------------------------------------------------- 1 | import FixTabPanel from '@/components/stateless/FixTabPanel'; 2 | import CryptoJS, { AES, enc } from 'crypto-js'; 3 | import JSEncrypt from 'jsencrypt'; 4 | import React, { useEffect, useState } from 'react'; 5 | 6 | const MyCrypto = () => { 7 | const [aesData, setAesData] = useState(); 8 | const [aesKey, setAesKey] = useState(''); 9 | const [aesEncryptData, setAesEncrypt] = useState(''); 10 | const [aesDecryptData, setAesDecryptData] = useState(''); 11 | 12 | const [reaData, setReaData] = useState(''); 13 | const [reaEncryptData, setReaEncryptData] = useState(''); 14 | const [reaDecryptData, setReaDecryptData] = useState(''); 15 | 16 | // 导入RSA密钥对 17 | const publicKey = ` 18 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0BDRgoeZCRRvH/QLbGhe 19 | M6ecmHUzm4ofqRgBPl1yThEryOQ8gGjmr16Xlj7cAedZz0vqvUsWnZh5KMZ5b5vQ 20 | Y4HGhPfPL3CzlI+iL0JyfFN9DsIe7uSDsStBfbLQas+IYIu47RMW9YNAmS8QFmqn 21 | 4Gpw6S1t3H+1AfwQpAGxXHm3+2mTClkautPOAqmTkAzM5eLIisOI/RE4YZiHRl49 22 | l+yUAmpAqRw0WnvqRlw76ES6naSBxHM7iQeAlo8R5YqheD2kNzJbEcJ7Owd4Rcfo 23 | kKZxSh7Qy/Pre8QFvIKdsCu4hpIGkws86s1IHvFLCXsXUxPR5z3E69VuW6K6rkXT 24 | lwIDAQAB 25 | `; 26 | const privateKey = ` 27 | MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDQENGCh5kJFG8f 28 | 9AtsaF4zp5yYdTObih+pGAE+XXJOESvI5DyAaOavXpeWPtwB51nPS+q9SxadmHko 29 | xnlvm9BjgcaE988vcLOUj6IvQnJ8U30Owh7u5IOxK0F9stBqz4hgi7jtExb1g0CZ 30 | LxAWaqfganDpLW3cf7UB/BCkAbFcebf7aZMKWRq6084CqZOQDMzl4siKw4j9EThh 31 | mIdGXj2X7JQCakCpHDRae+pGXDvoRLqdpIHEczuJB4CWjxHliqF4PaQ3MlsRwns7 32 | B3hFx+iQpnFKHtDL8+t7xAW8gp2wK7iGkgaTCzzqzUge8UsJexdTE9HnPcTr1W5b 33 | orquRdOXAgMBAAECggEANFGeVSvCrBlSzh6gRrzBv0xs4JtMBFcJmgv6uBNoXEAO 34 | GgBmREXciAmJpZKd4O6rUyh8WOgKQkumX730qD9uea8W27Wyh/PXfEpX1nlnb2LR 35 | BgaDH8Afa0v8tl5h8RHJcbmAoUCVG9xKwJooefKMzy4EwWWWhAUq31piVtNEJYPT 36 | zEXXMrbDJJ9bCRJXgvJ+oeedcIMz1OktU/DKjPdHLdU71MR0NUCPPMmD9RmSCjk2 37 | TkswP3Mm6EpY85kG9xT5tNvr6p5Rsw828TAtYuybzVeJxnSvPY4IXr5Zqx+D5/B3 38 | GoZYfDBV+nA1RYddwwnl58W5BpbFbVvLhFJIsnr4lQKBgQDrNwONQMmLG38pq+TY 39 | 0/JKcO0N1iFCIolOj7wyB8vOGOzAIqATEp1Tra5tRa0eVAlVl19rFrils0YNokgP 40 | ITv+K9Y79JzlSOH2bpMBCy5kB0IoYTDwNs0Kc2IMoV2NekAvFMVAS8Ldh2S39O4v 41 | a2JMGjHVoQCCcNDnq9QMAiMmzQKBgQDic6P5dbzMsKrHLRNu9gQRn7rrFe6FtErv 42 | 2mMiIdAI/4BtUs9n4q4gNprZlhIXVGGpzZ2mXMXJsJvcE6JOSS6Z8wRHE/AfOoVI 43 | 7i2qk5LFslvLHKrV1jqtyb6PTSG9Di9YF3u/JckSrMhUbDXG2PhN7Sp08O9cp3FY 44 | zc62jVH78wKBgFwdxXRSRRwHfruRKCSKjL7+jrf2fjvqTp/HxspJJ1XliQKODOar 45 | SZX11PPSb8QK4UT17VaBJXsvRGYegd37BAW8oUAFwlRBQM1D7Kph+J8QAKbTuVi5 46 | /X70RRfxMjQwBmbp2X4erYgYeCda8tT7Vxm6wH2LeimbiRTRxE+XnrCZAoGBAKyf 47 | 6OUWyqjjGByjkQfqRKnGsO/alSyZhvKW8TEow3TIiPdNxEv2MjTeS2cJDpt4OMb/ 48 | tmkGmcQpfHblBLpW8U5sQduJKGg17TruTiOVQbKxR2ZrYROHrs2iWEDXVJvQ/2hQ 49 | 5oWNYV16F3C72LbP2WFWJSJmNKHWBwLiSO1Ch7ffAoGBAOEGk+98m4l1jtDkIr/w 50 | EYOns+p9wj3be5YfARMRHxFjJCyXyaZstuk4RsmHhOlnO999nVX7eCAU36HfEahQ 51 | l5BNkobjNZF/xd9XTWywJFTGJNg6ejF991ucWnfSwnlRbJN8sGYRrr/IYyd6a/YL 52 | v4U73TKOI+a1xxr6ZMQ4vzwt 53 | `; 54 | useEffect(() => { 55 | const encryptData = () => { 56 | setAesKey(CryptoJS.lib.WordArray.random(32).toString()); 57 | // const aseParameter = CryptoJS.lib.WordArray.random(80000 / 4).toString() 58 | const aseParameter = `大型语言模型(LLM)是基于大量数据进行预训练的超大型深度学习模型。底层转换器是一组神经网络,这些神经网络由具有自注意力功能的编码器和解码器组成。编码器和解码器从一系列文本中提取含义,并理解其中的单词和短语之间的关系。 59 | 转换器 LLM 能够进行无监督的训练,但更精确的解释是转换器可以执行自主学习。通过此过程,转换器可学会理解基本的语法、语言和知识。 60 | 与早期按顺序处理输入的循环神经网络(RNN)不同,转换器并行处理整个序列。这可让数据科学家使用 GPU 训练基于转换器的 LLM,从而大幅度缩短训练时间。 61 | 借助转换器神经网络架构,您可使用非常大规模的模型,其中通常具有数千亿个参数。这种大规模模型可以摄取通常来自互联网的大量数据,但也可以从包含 500 多亿个网页的 Common Crawl 和拥有约 5700 万个页面的 Wikipedia 等来源摄取数据。`; 62 | const aesEncrypt = AES.encrypt(aseParameter, aesKey).toString(); 63 | setAesData(aseParameter); 64 | setAesEncrypt(aesEncrypt); 65 | const aesDecrypted = AES.decrypt(aesEncrypt, aesKey).toString(enc.Utf8); 66 | setAesDecryptData(aesDecrypted); 67 | 68 | const encrypt = new JSEncrypt(); 69 | encrypt.setPublicKey(publicKey); 70 | 71 | // const reaParameter = CryptoJS.lib.WordArray.random(80 / 4).toString() 72 | const reaParameter = 73 | '基于转换器的大型神经网络可以有数十亿个参数。模型的大小通常由模型大小、参数数量和训练数据规模之间的经验关系决定。'; 74 | 75 | const reaEncrypt = encrypt.encrypt(reaParameter); 76 | setReaData(reaParameter); 77 | 78 | setReaEncryptData(reaEncrypt); 79 | 80 | const decrypt = new JSEncrypt(); 81 | decrypt.setPrivateKey(privateKey); 82 | const uncrypted = decrypt.decrypt(reaEncrypt); 83 | setReaDecryptData(uncrypted); 84 | }; 85 | 86 | encryptData(); 87 | 88 | // eslint-disable-next-line react-hooks/exhaustive-deps 89 | }, []); 90 | 91 | return ( 92 | 93 |
94 |

AES加密明文: {aesData}

95 |

AES密钥:{aesKey}

96 |

AES加密密文:{aesEncryptData}

97 |

AES解密后文件:{aesDecryptData}

98 |

REA加密明文:{reaData}

99 |

REA加密密文:{reaEncryptData}

100 |

REA解密后:{reaDecryptData}

101 |
102 |
103 | ); 104 | }; 105 | 106 | export default MyCrypto; 107 | -------------------------------------------------------------------------------- /src/layout/proHeader/components/HeaderRight/index.tsx: -------------------------------------------------------------------------------- 1 | import { Space, Switch } from 'antd'; 2 | import { ReactElement, useState } from 'react'; 3 | import HeaderSearch from '../HeaderSearch'; 4 | import { SwitchWorkspace } from '../SwitchWorkspace'; 5 | import { UserInfo } from '../UserInfo'; 6 | // import { SwitchThemeButton } from '@/components/SwitchThemeButton'; 7 | import LanguageSwitcher from '@/components/stateless/LanguageSwitcher'; 8 | import Fullscreen from '@/layout/fullscreen'; 9 | import { useGlobalStore } from '@/store'; 10 | import { useProThemeContext } from '@/theme/hooks'; 11 | import { findMenuItem } from '@/utils/menu'; 12 | import { getKeyName } from '@/utils/publicFn'; 13 | import { GithubOutlined, MoonOutlined, SunOutlined } from '@ant-design/icons'; 14 | import { Link, useNavigate } from 'react-router-dom'; 15 | import { useShallow } from 'zustand/shallow'; 16 | import styles from './index.module.less'; 17 | const highlightText = (text: string, searchText: string) => { 18 | const highlightStr = `${searchText}`; 19 | // new 出来一个正则表达式reg>根据动态数据变量来创建 20 | // 参数一 将 searchText的值解析成字符串,并根据这个创建正则表达式, 21 | // 参数二 匹配模式 "gi" 22 | const reg = new RegExp(searchText, 'gi'); 23 | // 返回替换后的心字符串 24 | return text.replace(reg, highlightStr); 25 | }; 26 | 27 | export const HeaderRight = () => { 28 | const navigate = useNavigate(); 29 | 30 | const { menus, addTab } = useGlobalStore( 31 | useShallow((state) => ({ 32 | menus: state.menus, 33 | addTab: state.addTab, 34 | })), 35 | ); 36 | const [options, setOptions] = useState<{ label: JSX.Element; value: string }[]>([]); 37 | const [isFocus, setIsFocus] = useState(false); 38 | const handleSearch = (value: string | undefined) => { 39 | if (value) { 40 | const res = findMenuItem(value, menus); 41 | console.log('🚀 ~ file: index.js ~ line 70 ~ handleSearch ~ res', res); 42 | if (Array.isArray(res)) { 43 | const option = res.map((item) => { 44 | if (item.path.startsWith('https://') || item.path.startsWith('http://')) { 45 | return { 46 | label: ( 47 | 56 | ), 57 | value: item.name, 58 | }; 59 | } 60 | return { 61 | label: ( 62 | 69 | ), 70 | value: item.name, 71 | }; 72 | }); 73 | setOptions(option); 74 | } 75 | } else { 76 | setOptions([]); 77 | } 78 | }; 79 | // 按下enter选中后跳转路由 80 | const handleSelect = (optionsParams: { label: ReactElement }) => { 81 | if (optionsParams && optionsParams.label) { 82 | if (optionsParams.label.props.to) { 83 | navigate(optionsParams.label.props.to); 84 | const { tabKey, title, element, i18nKey } = getKeyName(optionsParams.label.props.to); 85 | addTab({ 86 | label: title, 87 | content: element, 88 | key: tabKey, 89 | closable: tabKey !== '/', 90 | path: optionsParams.label.props.to, 91 | i18nKey, 92 | }); 93 | } else if (optionsParams.label.props.href) { 94 | window.open(optionsParams.label.props.href, '_blank'); 95 | } 96 | } 97 | }; 98 | 99 | const { myTheme, setMyTheme } = useProThemeContext(); 100 | 101 | const setAntdTheme = () => { 102 | setMyTheme(myTheme === 'light' ? 'dark' : 'light'); 103 | }; 104 | 105 | const redirectGithub = () => { 106 | window.open('https://github.com/wkylin/promotion-web', '_blank'); 107 | }; 108 | 109 | return ( 110 |
111 | 112 | { 120 | setOptions([]); 121 | setTimeout(() => { 122 | setIsFocus(false); 123 | }, 300); 124 | }} 125 | onFocus={() => { 126 | setIsFocus(true); 127 | }} 128 | /> 129 | } 132 | unCheckedChildren={} 133 | onClick={setAntdTheme} 134 | /> 135 | 136 | 137 | 138 | 139 | 140 | 141 |
142 | ); 143 | }; 144 | -------------------------------------------------------------------------------- /src/store/tabsSlice/index.tsx: -------------------------------------------------------------------------------- 1 | import Home from '@/pages/home'; 2 | import React, { ReactNode } from 'react'; 3 | import { useNavigate } from 'react-router-dom'; 4 | import { StateCreator } from 'zustand'; 5 | 6 | import { 7 | ApartmentOutlined, 8 | DeploymentUnitOutlined, 9 | FireOutlined, 10 | GlobalOutlined, 11 | HeatMapOutlined, 12 | HomeOutlined, 13 | QrcodeOutlined, 14 | QuestionCircleOutlined, 15 | } from '@ant-design/icons'; 16 | import { uniqBy } from 'lodash-es'; 17 | import { UserInfoState } from '../userInfoSlice'; 18 | 19 | export interface PanesItem { 20 | label: string; 21 | i18nKey: string; 22 | key: string; 23 | content: React.ReactNode; 24 | closable: boolean; 25 | path: string; 26 | } 27 | export interface MenusItem { 28 | label: string; 29 | key: string; 30 | icon?: ReactNode; 31 | children?: MenusItem[]; 32 | } 33 | export interface TabsState { 34 | activeKey: string; 35 | setActiveKey: (key: string) => void; 36 | panes: PanesItem[]; 37 | setPanes: (panes: PanesItem[]) => void; 38 | menus: MenusItem[]; 39 | setMenus: (menus: MenusItem[]) => void; 40 | workspaces: { label: string; code: string }[]; 41 | setWorkspaces: (workspaces: { label: string; code: string }[]) => void; 42 | activeWorkspace: string; 43 | setActiveWorkspace: (workspace: string) => void; 44 | removeTab: (targetKey: string, callbackFun?: () => void) => void; 45 | addTab: (pane: PanesItem) => void; 46 | openKeys: React.Key[]; 47 | setOpenKeys: (openKeys: React.Key[]) => void; 48 | selectedKeys: string[]; 49 | setSelectedKeys: (selectedKeys: string[]) => void; 50 | } 51 | 52 | export const defaultMenus = [ 53 | { label: 'home', key: '/', icon: }, 54 | { label: 'demo', key: '/demo', icon: }, 55 | { label: 'Parallax', key: '/parallax', icon: }, 56 | { label: 'QrGenerate', key: '/qrcode', icon: }, 57 | { label: 'PrismRender', key: '/prism', icon: }, 58 | { label: 'ReactTilt', key: '/tilt', icon: }, 59 | { label: 'Music', key: '/music', icon: }, 60 | { label: 'Crypto', key: '/crypto', icon: }, 61 | { label: 'Video', key: '/video', icon: }, 62 | { label: 'Three', key: '/three', icon: }, 63 | { label: 'Echarts', key: '/echarts', icon: }, 64 | { label: 'ChatGPT', key: '/markmap', icon: }, 65 | { label: 'Mermaid', key: '/mermaid', icon: }, 66 | { 67 | label: '技术栈', 68 | key: '/sub-act', 69 | icon: , 70 | children: [ 71 | { 72 | label: '前端技术栈', 73 | key: '/sub-coupons', 74 | icon: , 75 | children: [ 76 | { label: 'Vue', key: '/coupons/add' }, 77 | { label: 'Angular', key: '/coupons/edit' }, 78 | ], 79 | }, 80 | { label: '后端技术栈', key: '/product', icon: }, 81 | ], 82 | }, 83 | { 84 | label: '构建工具', 85 | key: '/sub-list', 86 | icon: , 87 | children: [ 88 | { label: 'Webpack', key: '/coupons/list' }, 89 | { label: 'Vite', key: '/order/list' }, 90 | ], 91 | }, 92 | { 93 | label: 'Error', 94 | key: '/sub-error', 95 | icon: , 96 | children: [{ label: 'ErrorBoundary', key: '/error' }], 97 | }, 98 | ]; 99 | 100 | export const testMenus = [ 101 | { label: 'home', key: '/', icon: }, 102 | { label: 'demo', key: '/demo', icon: }, 103 | ]; 104 | 105 | export const createTabsSlice: StateCreator = ( 106 | set, 107 | ) => ({ 108 | activeKey: '/', 109 | setActiveKey: (key) => set({ activeKey: key }), 110 | panes: [ 111 | { 112 | label: '首页', 113 | i18nKey: 'home', 114 | key: '/', 115 | content: , 116 | closable: false, 117 | path: '/', 118 | }, 119 | ], 120 | setPanes: (panes) => set({ panes }), 121 | menus: defaultMenus, 122 | setMenus: (menus) => set({ menus }), 123 | workspaces: [ 124 | { label: '默认工作区', code: 'default' }, 125 | { label: '测试工作区', code: 'test' }, 126 | ], 127 | setWorkspaces: (workspaces) => set({ workspaces }), 128 | activeWorkspace: 'default', 129 | setActiveWorkspace: (workspace) => set({ activeWorkspace: workspace }), 130 | removeTab: (targetKey, callbackFun) => { 131 | set((state) => { 132 | const delIndex = state.panes.findIndex((item) => item.key === targetKey); 133 | const filterPanes = state.panes.filter((pane) => pane.key !== targetKey); 134 | // 删除非当前/当前tab 135 | if (targetKey !== state.activeKey) { 136 | return { panes: filterPanes }; 137 | } 138 | const nextPath = filterPanes[delIndex - 1]?.key || '/'; 139 | const navigate = useNavigate(); 140 | navigate(nextPath); 141 | return { activeKey: nextPath, panes: filterPanes }; 142 | }); 143 | callbackFun?.(); 144 | }, 145 | addTab: (pane) => { 146 | set((state) => { 147 | const tmp = [...state.panes, pane]; 148 | const newPanes = uniqBy(tmp, 'key'); 149 | return { panes: newPanes, activeKey: pane.key }; 150 | }); 151 | }, 152 | openKeys: [], 153 | setOpenKeys: (openKeys) => set({ openKeys }), 154 | selectedKeys: ['/'], 155 | setSelectedKeys: (selectedKeys) => set({ selectedKeys }), 156 | }); 157 | -------------------------------------------------------------------------------- /src/layout/proTabs/index.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Dropdown, Tabs, TabsProps, theme } from 'antd'; 2 | import { useEffect, useState } from 'react'; 3 | import { useLocation, useNavigate } from 'react-router-dom'; 4 | // import { StickyContainer, Sticky } from 'react-sticky-ts' 5 | import { MyErrorBoundary } from '@/components/stateful'; 6 | import Loading from '@/components/stateless/Loading'; 7 | import { useGlobalStore } from '@/store'; 8 | import { AlignRightOutlined, SyncOutlined } from '@ant-design/icons'; 9 | import { nanoid } from 'nanoid'; 10 | import { useTranslation } from 'react-i18next'; 11 | import { Sticky, StickyContainer } from 'react-sticky'; 12 | 13 | const ProTabs = () => { 14 | const { activeKey, setActiveKey, panes, setPanes, removeTab } = useGlobalStore(); 15 | console.log(panes, 'panes'); 16 | const [isReload, setIsReload] = useState(false); 17 | // const pathRef = useRef(''); 18 | 19 | const navigate = useNavigate(); 20 | const { t } = useTranslation(); 21 | // const { panesItem, tabActiveKey } = props; 22 | const { pathname, search } = useLocation(); 23 | const fullPath = pathname + search; 24 | 25 | const { 26 | token: { colorBgContainer }, 27 | } = theme.useToken(); 28 | 29 | const renderTabBar = (_props: TabsProps, DefaultTabBar: React.ComponentType) => ( 30 | 31 | {({ style }: { style: React.CSSProperties }) => ( 32 | 38 | )} 39 | 40 | ); 41 | 42 | useEffect(() => { 43 | document.querySelector('#container')?.scrollTo({ 44 | top: 0, 45 | left: 0, 46 | behavior: 'smooth', 47 | }); 48 | }, [pathname]); 49 | 50 | const onChange = (key: string) => { 51 | setActiveKey(key); 52 | }; 53 | 54 | // tab 点击 55 | const onTabClick = (targetKey: string) => { 56 | const { path } = panes.filter((item) => item.key === targetKey)[0]; 57 | navigate(path); 58 | }; 59 | 60 | // const onTabScroll = ({ direction }) => { 61 | // console.log('direction', direction); 62 | // }; 63 | 64 | const onEdit = (targetKey: string, action: string) => { 65 | if (action === 'remove') removeTab(targetKey); 66 | }; 67 | 68 | // 刷新当前 tab 69 | const refreshTab = () => { 70 | setIsReload(true); 71 | setTimeout(() => { 72 | setIsReload(false); 73 | }, 1000); 74 | }; 75 | 76 | const onTabContextMenu = (rightMenuKey: string) => { 77 | if (rightMenuKey === 'all') { 78 | const filterPanes = panes.filter((pane) => pane.key === '/'); 79 | setPanes(filterPanes); 80 | navigate('/'); 81 | setActiveKey('/'); 82 | } 83 | if (rightMenuKey === 'other') { 84 | const filterPanes = panes.filter((pane) => pane.key === '/' || pane.key === activeKey); 85 | setPanes(filterPanes); 86 | } 87 | }; 88 | 89 | // tab 右键菜单 90 | const tabRightMenu = [ 91 | { 92 | label: '关闭其他', 93 | key: 'other', 94 | }, 95 | { 96 | label: '全部关闭', 97 | key: 'all', 98 | }, 99 | ]; 100 | 101 | const fixError = () => { 102 | refreshTab(); 103 | }; 104 | 105 | return ( 106 | 107 | 123 | // 124 | // 125 | // ), 126 | right: ( 127 |
128 | { 132 | onTabContextMenu(key); 133 | }, 134 | }} 135 | trigger={['hover', 'click']} 136 | > 137 | 138 | 139 |
140 | ), 141 | }} 142 | items={panes.map((pane) => ({ 143 | label: ( 144 |
145 | {pane.key === fullPath && pane.key !== '/404' && ( 146 | 147 | )} 148 | {pane.i18nKey ? t(pane.i18nKey) : pane.label} 149 |
150 | ), 151 | key: pane.key, 152 | closable: pane.closable, 153 | forceRender: true, 154 | children: ( 155 | 156 |
157 | {isReload && pane.key === fullPath && pane.key !== '/404' ? ( 158 | 159 | ) : ( 160 | <>{pane.content} 161 | )} 162 |
163 |
164 | ), 165 | }))} 166 | /> 167 |
168 | ); 169 | }; 170 | 171 | export default ProTabs; 172 | -------------------------------------------------------------------------------- /src/layout/proHeader/components/UserInfo/ChangePassword/index.tsx: -------------------------------------------------------------------------------- 1 | import { LockOutlined } from '@ant-design/icons'; 2 | import { Col, Form, Input, message, Modal, Progress, Row } from 'antd'; 3 | import type { RuleObject } from 'antd/lib/form'; 4 | import type { ChangeEvent } from 'react'; 5 | import { useEffect, useState } from 'react'; 6 | 7 | const passwordStrengthTestMap = new Map([ 8 | ['LOW', /^[a-zA-Z0-9\W_!@#$%^&*`~()-+=]{6,}$/], 9 | [ 10 | 'MEDIUM', 11 | /^(?![0-9]+$)(?![a-z]+$)(?![A-Z]+$)(?![\W_!@#$%^&*`~()-+=]+$)[a-zA-Z0-9\W_!@#$%^&*`~()-+=]{6,}$/, 12 | ], 13 | [ 14 | 'SAFE', 15 | /(?![a-zA-Z]+$)(?![A-Z0-9]+$)(?![A-Z\W_!@#$%^&*`~()-+=]+$)(?![a-z0-9]+$)(?![a-z\W_!@#$%^&*`~()-+=]+$)(?![0-9\W_!@#$%^&*`~()-+=]+$)[a-zA-Z0-9\W_!@#$%^&*`~()-+=]{8,}$/, 16 | ], 17 | [ 18 | 'HIGH', 19 | /^(?=.*?[a-z])(?=.*?[A-Z])(?=.*?\d)(?=.*?[\W_!@#$%^&*`~()-+=])[a-zA-Z\d\W_!@#$%^&*`~()-+=]{10,}$/, 20 | ], 21 | ]); 22 | 23 | export const ChangePassword: React.FC<{ 24 | isChangePasswordVisible: boolean; 25 | onCancel?: () => void; 26 | isPwdExpired?: boolean; 27 | }> = ({ isChangePasswordVisible, onCancel, isPwdExpired }) => { 28 | const [strokeColor, setStrokeColor] = useState('rgb(170, 0, 51)'); 29 | const [progress, setProgress] = useState(0); 30 | const [strengthPolicy, setStrengthPolicy] = useState('MEDIUM'); 31 | const [tip, setTip] = useState( 32 | '数字、小写字母,大写字母或特殊字符中的任意两种组成,长度至少 6 位', 33 | ); 34 | 35 | useEffect(() => { 36 | if (isChangePasswordVisible) { 37 | // getPasswordStrengthPolicyApi().then((res) => { 38 | // if (res) { 39 | // setTip(res.desc); 40 | // setStrengthPolicy(res.name); 41 | // } 42 | // }); 43 | } 44 | }, [isChangePasswordVisible]); 45 | const [form] = Form.useForm(); 46 | const handleOk = () => { 47 | form.validateFields().then(() => { 48 | form.submit(); 49 | }); 50 | }; 51 | const onFinish = async (values: { newPwd: string; oldPwd: string }) => { 52 | if (values.oldPwd === values.newPwd) { 53 | message.info('新密码不能与旧密码相同'); 54 | return; 55 | } 56 | // 调用密码修改接口然后回到登录页面重新登录 57 | }; 58 | const handleChange = (e: ChangeEvent) => { 59 | const { value: targetValue } = e.target; 60 | setProgress(targetValue.length); 61 | passwordStrengthTestMap.forEach((value, key) => { 62 | if (value.test(targetValue)) { 63 | switch (key) { 64 | case 'LOW': 65 | setProgress(25); 66 | // setStrong('exception'); 67 | setStrokeColor('rgb(170, 0, 51)'); 68 | break; 69 | case 'MEDIUM': 70 | setProgress(50); 71 | // setStrong('exception'); 72 | setStrokeColor('rgb(255, 204, 51)'); 73 | break; 74 | case 'SAFE': 75 | setProgress(75); 76 | // setStrong('success'); 77 | setStrokeColor('rgb(102, 153, 204)'); 78 | break; 79 | case 'HIGH': 80 | setProgress(100); 81 | // setStrong('exception'); 82 | setStrokeColor('rgb(0, 128, 0)'); 83 | break; 84 | default: 85 | // setStrong('exception'); 86 | setStrokeColor('rgb(170, 0, 51)'); 87 | break; 88 | } 89 | } 90 | }); 91 | }; 92 | 93 | const formatProgress = (percent?: number) => { 94 | switch (percent) { 95 | case 0: 96 | return ''; 97 | case 25: 98 | return '低'; 99 | case 50: 100 | return '中等'; 101 | case 75: 102 | return '安全'; 103 | case 100: 104 | return '高'; 105 | default: 106 | return '不符合要求'; 107 | } 108 | }; 109 | 110 | const validateNewPassword = (rule: RuleObject, value: string) => { 111 | const reg = passwordStrengthTestMap.get(strengthPolicy); 112 | if (reg && reg.test(value)) { 113 | return Promise.resolve(); 114 | } 115 | // return Promise.reject(passwordTipMap.get(strengthPolicy)); 116 | return Promise.reject(); 117 | }; 118 | 119 | return ( 120 | 131 |
138 | 144 | } name="oldPwd" /> 145 | 146 | 157 | } 160 | name="newPwd" 161 | onChange={handleChange} 162 | /> 163 | 164 | 165 | 166 |
{tip}
167 | 168 | 密码强度: 169 | 170 | 176 | 177 | 178 | 179 |
180 | 181 | { 189 | if (value === form.getFieldsValue().newPwd) { 190 | return Promise.resolve(); 191 | } 192 | return Promise.reject(new Error('两次密码输入不相同')); 193 | }, 194 | }, 195 | ]} 196 | > 197 | } name="confirmNewPwd" /> 198 | 199 |
200 |
201 | ); 202 | }; 203 | -------------------------------------------------------------------------------- /src/utils/aidFn.js: -------------------------------------------------------------------------------- 1 | import { parse, stringify } from 'qs' 2 | import html2canvas from 'html2canvas' 3 | 4 | export const getEnv = () => { 5 | let env 6 | if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') { 7 | env = 'NODE' 8 | } 9 | if (typeof XMLHttpRequest !== 'undefined') { 10 | env = 'BROWSER' 11 | } 12 | return env 13 | } 14 | 15 | export const isArray = (val) => typeof val === 'object' && Object.prototype.toString.call(val) === '[object Array]' 16 | 17 | export const isURLSearchParams = (val) => typeof URLSearchParams !== 'undefined' && val instanceof URLSearchParams 18 | 19 | export const isDate = (val) => typeof val === 'object' && Object.prototype.toString.call(val) === '[object Date]' 20 | 21 | export const isObject = (val) => val !== null && typeof val === 'object' 22 | 23 | export const getParamObject = (val) => { 24 | if (isURLSearchParams(val)) { 25 | return parse(val.toString(), { strictNullHandling: true }) 26 | } 27 | if (typeof val === 'string') { 28 | return [val] 29 | } 30 | return val 31 | } 32 | 33 | export const reqStringify = (val) => stringify(val, { arrayFormat: 'repeat', strictNullHandling: true }) 34 | 35 | export const getType = (obj) => { 36 | const type = typeof obj 37 | if (type !== 'object') { 38 | return type 39 | } 40 | return Object.prototype.toString.call(obj).replace(/^$/, '$1') 41 | } 42 | 43 | export const hidePhone = (phone) => phone?.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2') 44 | 45 | // asyncAction(action)(callback) 46 | export const asyncAction = (action) => { 47 | const wait = Promise.resolve(action) 48 | return (cb) => { 49 | wait.then(() => setTimeout(() => cb())) 50 | } 51 | } 52 | 53 | export const getImgsUrl = (html) => { 54 | const imgReg = /|\/>)/gi 55 | const srcReg = /src=['"]?([^'"]*)['"]?/i 56 | const arr = html.match(imgReg) 57 | if (!arr) return null 58 | const urlArr = arr.reduce((prev, next) => { 59 | const src = next.match(srcReg) 60 | return src[1] ? [...prev, src[1]] : prev 61 | }, []) 62 | return urlArr 63 | } 64 | 65 | export const customizeTimer = { 66 | intervalTimer: null, 67 | timeoutTimer: null, 68 | setTimeout(cb, interval) { 69 | const { now } = Date 70 | const stime = now() 71 | let etime = stime 72 | const loop = () => { 73 | this.timeoutTimer = requestAnimationFrame(loop) 74 | etime = now() 75 | if (etime - stime >= interval) { 76 | cb() 77 | cancelAnimationFrame(this.timeoutTimer) 78 | } 79 | } 80 | this.timeoutTimer = requestAnimationFrame(loop) 81 | return this.timeoutTimer 82 | }, 83 | clearTimeout() { 84 | cancelAnimationFrame(this.timeoutTimer) 85 | }, 86 | setInterval(cb, interval) { 87 | const { now } = Date 88 | let stime = now() 89 | let etime = stime 90 | const loop = () => { 91 | this.intervalTimer = requestAnimationFrame(loop) 92 | etime = now() 93 | if (etime - stime >= interval) { 94 | stime = now() 95 | etime = stime 96 | cb() 97 | } 98 | } 99 | this.intervalTimer = requestAnimationFrame(loop) 100 | return this.intervalTimer 101 | }, 102 | clearInterval() { 103 | cancelAnimationFrame(this.intervalTimer) 104 | }, 105 | } 106 | 107 | export const isDecimal = (value) => { 108 | const reg = /(?:^[1-9](\d+)?(?:\.\d{1,2})?$)|(?:^(?:0)$)|(?:^\d\.\d(?:\d)?$)/ 109 | return reg.test(value) 110 | } 111 | 112 | export const limitDecimal = (val) => val.replace(/^(-)*(\d+)\.(\d\d).*$/, '$1$2.$3') 113 | 114 | /* 115 | ** 判断用户是否离开当前页面 116 | */ 117 | export const checkIsLocalPage = () => { 118 | document.addEventListener('visibilitychange', () => { 119 | if (document.visibilityState === 'hidden') { 120 | return false 121 | } 122 | if (document.visibilityState === 'visible') { 123 | return true 124 | } 125 | window.addEventListener( 126 | 'pagehide', 127 | (event) => { 128 | if (event.persisted) { 129 | /* the page isn't being discarded, so it can be reused later */ 130 | } 131 | }, 132 | false 133 | ) 134 | }) 135 | } 136 | 137 | // Generate Random Hex 138 | export const randomHex = () => 139 | `#${Math.floor(Math.random() * 0xffffff) 140 | .toString(16) 141 | .padEnd(6, '0')}` 142 | 143 | // Clear All Cookies 144 | export const clearCookies = document.cookie 145 | .split(';') 146 | .forEach( 147 | (cookie) => 148 | (document.cookie = cookie.replace(/^ +/, '').replace(/[=].*/, `=;expires=${new Date(0).toUTCString()};path=/`)) 149 | ) 150 | 151 | // Find the number of days between two days 152 | export const dayDif = (date1, date2) => Math.ceil(Math.abs(date1.getTime() - date2.getTime()) / 86400000) 153 | 154 | // Capitalize a String 155 | export const capitalize = (str) => str.charAt(0).toUpperCase() + str.slice(1) 156 | 157 | // Check if the array is empty 158 | export const isNotEmpty = (arr) => Array.isArray(arr) && arr.length > 0 159 | 160 | // Detect Dark Mode 161 | export const isDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches 162 | 163 | export const fetchSomething = () => 164 | new Promise((resolve) => { 165 | setTimeout(() => { 166 | resolve('') 167 | }, 1000) 168 | }) 169 | 170 | export const toFixed = (number, m) => { 171 | if (typeof number !== 'number') { 172 | throw new Error('number不是数字') 173 | } 174 | let result = Math.round(10 ** m * number) / 10 ** m 175 | result = String(result) 176 | if (result.indexOf('.') === -1) { 177 | if (m !== 0) { 178 | result += '.' 179 | result += new Array(m + 1).join('0') 180 | } 181 | } else { 182 | const arr = result.split('.') 183 | if (arr[1].length < m) { 184 | arr[1] += new Array(m - arr[1].length + 1).join('0') 185 | } 186 | result = arr.join('.') 187 | } 188 | return result 189 | } 190 | export const toFixedBug = (n, fixed) => ~~(10 ** fixed * n) / 10 ** fixed 191 | 192 | export const promiseWithTimeout = (promise, timeout) => { 193 | const timeoutPromise = new Promise((resolve) => setTimeout(() => resolve('Time Out!'), timeout)) 194 | return Promise.race([timeoutPromise, promise]) 195 | } 196 | 197 | export const shuffleArr = (arr) => arr.sort(() => 0.5 - Math.random()) 198 | export const sleep = (time) => new Promise((resolve) => setTimeout(() => resolve(), time)) 199 | export const ThousandNum = (num) => num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',') 200 | export const RandomId = (len) => Math.random().toString(36).substring(3, len) 201 | export const RoundNum = (num, decimal) => Math.round(num * 10 ** decimal) / 10 ** decimal 202 | export const randomNum = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min 203 | 204 | export const isEmptyArray = (arr) => Array.isArray(arr) && !arr.length 205 | export const randomItem = (arr) => arr[Math.floor(Math.random() * arr.length)] 206 | export const asyncTo = (promise) => promise.then((data) => [null, data]).catch((err) => [err]) 207 | export const hasFocus = (element) => element === document.activeElement 208 | export const isEqual = (a, b) => JSON.stringify(a) === JSON.stringify(b) 209 | export const randomString = () => Math.random().toString(36).slice(2) 210 | export const toCamelCase = (str) => str.trim().replace(/[-_\s]+(.)?/g, (_, c) => (c ? c.toUpperCase() : '')) 211 | export const random = (min, max) => Math.floor(Math.random() * (max - min + 1) + min) 212 | export const randomColor = () => `#${Math.random().toString(16).slice(2, 8).padEnd(6, '0')}` 213 | export const pause = (millions) => new Promise((resolve) => setTimeout(resolve, millions)) 214 | export const camelizeCamelCase = (str) => 215 | str 216 | .replace(/(?:^\w|[A-Z]|\b\w)/g, (letter, index) => (index === 0 ? letter.toLowerCase() : letter.toUpperCase())) 217 | .replace(/\s+/g, '') 218 | 219 | export const copyTextToClipboard = async (textToCopy) => { 220 | try { 221 | if (navigator?.clipboard?.writeText) { 222 | await navigator.clipboard.writeText(textToCopy) 223 | console.log('已成功复制到剪贴板') 224 | } 225 | } catch (err) { 226 | console.error(`复制到剪贴板失败:${err.message}`) 227 | } 228 | } 229 | 230 | export const getRandomId = () => { 231 | let text = '' 232 | const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' 233 | // eslint-disable-next-line no-plusplus 234 | for (let i = 0; i < 32; i++) { 235 | text += possible.charAt(Math.floor(Math.random() * possible.length)) 236 | } 237 | return text 238 | } 239 | 240 | // https://github.com/Azure/fetch-event-source 241 | // https://github.com/mpetazzoni/sse.js 242 | // https://nodejs.org/api/http.html#httprequesturl-options-callback 243 | export const oneApiChat = (chatList, token, signal) => 244 | fetch('https://api.openai.com/v1/chat/completions', { 245 | method: 'POST', 246 | signal, 247 | headers: { 248 | Authorization: token, 249 | 'Content-Type': 'application/json', 250 | }, 251 | body: JSON.stringify({ 252 | model: 'gpt-3.5-turbo', 253 | messages: chatList, 254 | stream: true, 255 | }), 256 | }) 257 | 258 | export const getCurrentDate = () => { 259 | const date = new Date() 260 | const day = date.getDate() 261 | const month = date.getMonth() + 1 262 | const year = date.getFullYear() 263 | return `${year}-${month}-${day}` 264 | } 265 | 266 | export const exportJsonData = (data) => { 267 | const date = getCurrentDate() 268 | const jsonString = JSON.stringify(JSON.parse(data), null, 2) 269 | const blob = new Blob([jsonString], { type: 'application/json' }) 270 | const url = URL.createObjectURL(blob) 271 | const link = document.createElement('a') 272 | link.href = url 273 | link.download = `chat-store_${date}.json` 274 | document.body.appendChild(link) 275 | link.click() 276 | document.body.removeChild(link) 277 | } 278 | 279 | export const saveHtmlToPng = async (eleHtml, successFun, errorFun) => { 280 | try { 281 | const ele = eleHtml ?? document.getElementById('image-wrapper') 282 | const canvas = await html2canvas(ele, { 283 | useCORS: true, 284 | }) 285 | const imgUrl = canvas.toDataURL('image/png') 286 | const tempLink = document.createElement('a') 287 | tempLink.style.display = 'none' 288 | tempLink.href = imgUrl 289 | tempLink.setAttribute('download', 'chat-shot.png') 290 | if (typeof tempLink.download === 'undefined') tempLink.setAttribute('target', '_blank') 291 | 292 | document.body.appendChild(tempLink) 293 | tempLink.click() 294 | document.body.removeChild(tempLink) 295 | window.URL.revokeObjectURL(imgUrl) 296 | if (successFun) successFun() 297 | Promise.resolve() 298 | } catch (error) { 299 | if (errorFun) errorFun(error.message) 300 | } 301 | } 302 | 303 | export const trimTopic = (topic) => topic.replace(/[,。!?”“"、,.!?]*$/, '') 304 | 305 | // onClick={() => importFromFile()} 306 | // readFromFile().then((content) => { JSON.parse(content)}) 307 | 308 | export const readFromFile = () => 309 | new Promise((res, rej) => { 310 | const fileInput = document.createElement('input') 311 | fileInput.type = 'file' 312 | fileInput.accept = 'application/json' 313 | 314 | fileInput.onchange = (event) => { 315 | const file = event.target.files[0] 316 | const fileReader = new FileReader() 317 | fileReader.onload = (e) => { 318 | res(e.target.result) 319 | } 320 | fileReader.onerror = (e) => rej(e) 321 | fileReader.readAsText(file) 322 | } 323 | 324 | fileInput.click() 325 | }) 326 | 327 | export const prettyObject = (msg) => { 328 | let obj = '' 329 | if (typeof msg !== 'string') { 330 | obj = JSON.stringify(msg, null, ' ') 331 | } 332 | if (obj === '{}') { 333 | return obj.toString() 334 | } 335 | if (obj.startsWith('```json')) { 336 | return obj 337 | } 338 | return ['```json', obj, '```'].join('\n') 339 | } 340 | --------------------------------------------------------------------------------