├── .husky ├── commit-msg └── pre-commit ├── src ├── styles │ ├── scss │ │ ├── var.scss │ │ ├── index.scss │ │ ├── global.scss │ │ └── tools.scss │ └── css │ │ └── reset.css ├── utils │ ├── index.ts │ ├── file.ts │ ├── dateUtil.ts │ └── tools.ts ├── store │ ├── index.ts │ └── modules │ │ ├── use-loading-store.ts │ │ └── use-user-store.ts ├── components │ ├── high-light │ │ ├── index.scss │ │ └── index.tsx │ ├── index.ts │ ├── loading │ │ ├── index.scss │ │ └── index.tsx │ ├── not-fount │ │ └── index.tsx │ └── auto-scroll-to-top │ │ └── index.tsx ├── views │ ├── test │ │ ├── auth-test │ │ │ └── index.tsx │ │ ├── error-test │ │ │ └── throw-error-comp.ts │ │ ├── count │ │ │ ├── index.scss │ │ │ └── index.tsx │ │ ├── login │ │ │ └── index.tsx │ │ └── create │ │ │ ├── index.scss │ │ │ └── index.tsx │ └── home │ │ ├── index.tsx │ │ └── index.scss ├── hooks │ ├── index.ts │ ├── create-design │ │ └── index.ts │ ├── use-ref-state │ │ └── index.ts │ ├── use-router │ │ └── index.ts │ └── use-namespace │ │ └── index.ts ├── api │ └── test.ts ├── types │ ├── global.d.ts │ ├── router.d.ts │ └── vite-env.d.ts ├── main.tsx ├── router │ ├── utils │ │ ├── lazy-load.tsx │ │ ├── route-guard.tsx │ │ └── index.ts │ ├── modules │ │ └── test.tsx │ └── index.tsx ├── app.tsx ├── services │ ├── index.ts │ └── service.ts ├── error-boundary.tsx └── assets │ └── react.svg ├── .stylelintignore ├── .prettierignore ├── .npmrc ├── .env.test ├── .env.pro ├── .vscode ├── extensions.json └── settings.json ├── lint-staged.config.js ├── .env ├── .env.dev ├── scripts └── push.sh ├── .gitignore ├── .editorconfig ├── .commitlintrc.js ├── tsconfig.json ├── stylelint.config.js ├── LICENSE ├── .prettierrc.js ├── public └── vite.svg ├── .github └── workflows │ └── deploy.yml ├── eslint.config.js ├── index.html ├── vite.config.ts ├── package.json ├── README.md └── README.en-US.md /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | npx --no -- commitlint --edit -------------------------------------------------------------------------------- /src/styles/scss/var.scss: -------------------------------------------------------------------------------- 1 | /* 全局变量放置处 */ 2 | $color-test: red; 3 | -------------------------------------------------------------------------------- /src/styles/scss/index.scss: -------------------------------------------------------------------------------- 1 | @forward './var'; 2 | @forward './tools'; 3 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | # pnpm run lint:all 全量检查,耗时多 2 | npx lint-staged # 只检查暂存的文件 3 | -------------------------------------------------------------------------------- /.stylelintignore: -------------------------------------------------------------------------------- 1 | /dist/* 2 | /public/* 3 | public/* 4 | /dist* 5 | index.html 6 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /node_modules/** 2 | /dist/ 3 | /dist* 4 | /public/* 5 | /docs/* 6 | CHANGELOG 7 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './tools'; 2 | export * from './file'; 3 | export * from './dateUtil'; 4 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | export * from './modules/use-user-store'; 2 | export * from './modules/use-loading-store'; 3 | -------------------------------------------------------------------------------- /src/components/high-light/index.scss: -------------------------------------------------------------------------------- 1 | .highlight { 2 | font-weight: bold; 3 | background-color: unset; 4 | } 5 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | # 设置最新的淘宝镜像,加快安装依赖速度 2 | registry = https://registry.npmmirror.com 3 | 4 | # 安装依赖时锁定版本号 5 | save-exact = true 6 | -------------------------------------------------------------------------------- /src/views/test/auth-test/index.tsx: -------------------------------------------------------------------------------- 1 | function AuthTest() { 2 | return
AuthTest权限测试403
; 3 | } 4 | 5 | export default AuthTest; 6 | -------------------------------------------------------------------------------- /src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './create-design'; 2 | export * from './use-router'; 3 | export * from './use-namespace'; 4 | export * from './use-ref-state'; 5 | -------------------------------------------------------------------------------- /src/api/test.ts: -------------------------------------------------------------------------------- 1 | import { GET } from '@/services'; 2 | 3 | export function getXXX(params: { test: string }) { 4 | return GET<{ list: any[] }>('/test', params); 5 | } 6 | -------------------------------------------------------------------------------- /src/types/global.d.ts: -------------------------------------------------------------------------------- 1 | declare interface IResponse { 2 | code: number; 3 | message: string; 4 | data: T; 5 | } 6 | 7 | declare type Fn = () => void; 8 | -------------------------------------------------------------------------------- /src/types/router.d.ts: -------------------------------------------------------------------------------- 1 | import { RouteObject as ReactRouteObject } from 'react-router'; 2 | 3 | export type RouteObject = { 4 | meta?: { 5 | title: string; // 页面标题 6 | }; 7 | } & ReactRouteObject; 8 | -------------------------------------------------------------------------------- /src/hooks/create-design/index.ts: -------------------------------------------------------------------------------- 1 | // 或者叫 createNamespace 2 | export function createDesign(scope: string) { 3 | return { 4 | prefixCls: scope, // 前缀示例:'pg'页面、'comp'组件、'pub'公共组件,如 'pg-home' 5 | }; 6 | } 7 | -------------------------------------------------------------------------------- /.env.test: -------------------------------------------------------------------------------- 1 | # 环境 2 | VITE_NODE_ENV = test 3 | 4 | # 接口前缀 5 | VITE_API_BASE_URL = '/test-api' 6 | 7 | # 后端服务地址 8 | VITE_SERVER_URL = 'http://localhost:3000' 9 | 10 | # 输出路径 11 | VITE_OUT_DIR = dist-test 12 | -------------------------------------------------------------------------------- /.env.pro: -------------------------------------------------------------------------------- 1 | # 环境 2 | VITE_NODE_ENV = production 3 | 4 | # 接口前缀 5 | VITE_API_BASE_URL = '/pro-api' 6 | 7 | # 后端服务地址 8 | VITE_SERVER_URL = 'http://localhost:3000' 9 | 10 | # 输出路径 11 | VITE_OUT_DIR = dist-pro 12 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "editorconfig.editorconfig", 4 | "dbaeumer.vscode-eslint", 5 | "esbenp.prettier-vscode", 6 | "stylelint.vscode-stylelint", 7 | "mikestead.dotenv" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /src/components/index.ts: -------------------------------------------------------------------------------- 1 | export { default as AutoScrollToTop } from './auto-scroll-to-top'; 2 | export { default as HighLight } from './high-light'; 3 | export { default as NotFount } from './not-fount'; 4 | export { default as Loading } from './loading'; 5 | -------------------------------------------------------------------------------- /src/views/test/error-test/throw-error-comp.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const ThrowErrorComponent: React.FC = () => { 4 | throw new Error('这是一个故意抛出的错误'); 5 | // return
这个组件会抛出错误
; 6 | }; 7 | 8 | export default ThrowErrorComponent; 9 | -------------------------------------------------------------------------------- /lint-staged.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('lint-staged').Config} */ 2 | export default { 3 | '*.{ts,tsx,js,jsx,cjs,mjs}': 'eslint --fix', 4 | '*.{css,scss}': 'stylelint --fix', 5 | '*.{ts,tsx,js,jsx,cjs,mjs,html,css,scss,json}': 'prettier --write', 6 | }; 7 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | # .env 文件 2 | # 用途: 定义项目的基本环境变量,这些变量在所有环境中都会被加载。 3 | 4 | # VITE_MY_APP_PREFIX = my 5 | # 作用: 6 | # VITE_ 前缀的变量会被 Vite 自动注入到客户端代码中,可以通过 import.meta.env.VITE_MY_APP_PREFIX 访问。 7 | # 这些变量在开发环境、生产环境和测试环境中都会生效。 8 | 9 | # 项目标题 10 | VITE_APP_TITLE = React-Ts-Template 11 | 12 | # 端口号 13 | VITE_APP_PORT = 5173 14 | -------------------------------------------------------------------------------- /.env.dev: -------------------------------------------------------------------------------- 1 | # 为了防止意外地将一些环境变量泄漏到客户端,只有以 VITE_ 为前缀的变量才会暴露给经过 vite 处理的代码。 2 | # js中通过`import.meta.env.VITE_APP_BASE_API`取值 3 | 4 | # 环境 5 | VITE_NODE_ENV = development 6 | 7 | # 接口前缀 8 | VITE_API_BASE_URL = '/dev-api' 9 | 10 | # 后端服务地址 11 | VITE_SERVER_URL = 'http://localhost:3000' 12 | 13 | # 输出路径 14 | VITE_OUT_DIR = dist-dev 15 | -------------------------------------------------------------------------------- /src/hooks/use-ref-state/index.ts: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | 3 | import { useLatest } from 'ahooks'; 4 | 5 | export function useRefState(initialState: T | (() => T)) { 6 | const [state, setState] = useState(initialState); 7 | const stateRef = useLatest(state); 8 | return [state, setState, stateRef] as const; 9 | } 10 | -------------------------------------------------------------------------------- /src/views/test/count/index.scss: -------------------------------------------------------------------------------- 1 | $prefix-cls: 'pg-guild-count'; 2 | 3 | .#{$prefix-cls} { 4 | box-sizing: border-box; 5 | display: flex; 6 | flex-direction: column; 7 | align-items: center; 8 | justify-content: center; 9 | font-size: 17px; 10 | 11 | &__count { 12 | font-weight: bold; 13 | color: skyblue; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /scripts/push.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 确保脚本抛出遇到的错误 4 | set -e 5 | 6 | # 获取提交描述信息,默认为 "feat: update" 7 | commitDescInfo=${1:-"feat: update"} 8 | 9 | git add . 10 | git commit -m "${commitDescInfo}" 11 | git push 12 | 13 | # 快速提交代码脚本: 14 | # 使用示例: 15 | # pnpm push "feat: 添加新功能" 16 | # 或者 pnpm push 走默认提交描述信息 17 | 18 | # 可以删掉此文件,改用git别名以实现快速提交 19 | -------------------------------------------------------------------------------- /src/views/test/login/index.tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from '@/hooks'; 2 | 3 | function Login() { 4 | const router = useRouter(); 5 | return ( 6 |
7 | Login登录页面 8 | 11 |
12 | ); 13 | } 14 | 15 | export default Login; 16 | -------------------------------------------------------------------------------- /src/components/loading/index.scss: -------------------------------------------------------------------------------- 1 | .pub-loading { 2 | position: fixed; 3 | inset: 0; 4 | z-index: 9999999; 5 | display: flex; 6 | align-items: center; 7 | justify-content: center; 8 | width: 100%; 9 | height: 100%; 10 | background-color: rgb(0 0 0 / 20%); 11 | 12 | svg { 13 | width: 100px; 14 | height: 100px; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | /dist* 14 | *.local 15 | 16 | # Editor directories and files 17 | .idea 18 | .DS_Store 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | 25 | package-lock.json 26 | yarn.lock 27 | -------------------------------------------------------------------------------- /src/types/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | interface ImportMetaEnv { 4 | readonly VITE_NODE_ENV: string; 5 | readonly VITE_APP_TITLE: string; 6 | readonly VITE_APP_PORT: string; 7 | readonly VITE_OUT_DIR: string; 8 | readonly VITE_API_BASE_URL: string; 9 | readonly VITE_SERVER_URL: string; 10 | // 更多环境变量... 11 | } 12 | 13 | interface ImportMeta { 14 | readonly env: ImportMetaEnv; 15 | } 16 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import { Inspector } from 'react-dev-inspector'; 2 | import { createRoot } from 'react-dom/client'; 3 | 4 | // 公共样式 5 | import '@/styles/scss/global.scss'; 6 | 7 | import App from './app.tsx'; 8 | 9 | function setupApp() { 10 | createRoot(document.getElementById('root')!).render( 11 | <> 12 | 13 | 14 | , 15 | ); 16 | } 17 | 18 | setupApp(); 19 | -------------------------------------------------------------------------------- /src/router/utils/lazy-load.tsx: -------------------------------------------------------------------------------- 1 | import { FC, ReactNode, Suspense } from 'react'; 2 | 3 | /** 4 | * 组件懒加载,结合Suspense实现 5 | * @param Component 组件对象 6 | * @returns 返回新组件 7 | */ 8 | export const LazyLoad = (Component: FC): ReactNode => { 9 | return ( 10 | // fallback的loading效果可自行修改为ui组件库的loading组件或骨架屏等等 11 | }> 12 | 13 | 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /src/views/test/create/index.scss: -------------------------------------------------------------------------------- 1 | .pg-guild-create { 2 | box-sizing: border-box; 3 | width: 155px; 4 | font-size: 17px; 5 | 6 | &__count { 7 | font-weight: bold; 8 | color: $color-test; 9 | } 10 | 11 | &__avatar { 12 | width: 60px; 13 | height: 60px; 14 | border-radius: 50%; 15 | } 16 | 17 | &__username { 18 | font-size: 18px; 19 | 20 | // 使用全局样式工具函数:文字描边 21 | @include text-stroke(skyblue); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/app.tsx: -------------------------------------------------------------------------------- 1 | import { RouterProvider } from 'react-router/dom'; 2 | 3 | import { useLoadingStore } from '@/store'; 4 | 5 | import { Loading } from '@/components'; 6 | 7 | import router from './router'; 8 | 9 | function App() { 10 | const { isLoading } = useLoadingStore(); 11 | return ( 12 | <> 13 | 14 | {isLoading && } 15 | {/* ...其他需要全局管理的,如Modal弹窗等 */} 16 | 17 | ); 18 | } 19 | 20 | export default App; 21 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # @see: http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] # 表示所有文件适用 6 | charset = utf-8 # 设置文件字符集为 utf-8 7 | end_of_line = lf # 控制换行类型(lf | cr | crlf) 8 | insert_final_newline = true # 始终在文件末尾插入一个新行 9 | indent_style = space # 缩进风格(tab | space) 10 | indent_size = 2 # 缩进大小 11 | max_line_length = 100 # 最大行长度 12 | 13 | [*.md] # 表示仅 md 文件适用以下规则 14 | insert_final_newline = false # 关闭末尾新行插入 15 | max_line_length = off # 关闭最大行长度限制 16 | trim_trailing_whitespace = false # 关闭末尾空格修剪 17 | -------------------------------------------------------------------------------- /src/components/not-fount/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | 3 | import { useRouter } from '@/hooks'; 4 | 5 | function NotFount() { 6 | const router = useRouter(); 7 | 8 | useEffect(() => { 9 | const timerId = setTimeout(() => { 10 | router.push('/'); 11 | }, 1000); 12 | return () => clearTimeout(timerId); 13 | }, [router]); 14 | 15 | return ( 16 |
17 | 404------(1s后将跳转到首页) 18 |
19 | ); 20 | } 21 | 22 | export default NotFount; 23 | -------------------------------------------------------------------------------- /src/hooks/use-router/index.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react'; 2 | import { useNavigate } from 'react-router'; 3 | 4 | export function useRouter() { 5 | const navigate = useNavigate(); 6 | 7 | const router = useMemo( 8 | () => ({ 9 | go: (delta: number) => navigate(delta), 10 | back: () => navigate(-1), 11 | forward: () => navigate(1), 12 | reload: () => window.location.reload(), 13 | push: (href: string) => navigate(href), 14 | replace: (href: string) => navigate(href, { replace: true }), 15 | }), 16 | [navigate], 17 | ); 18 | 19 | return router; 20 | } 21 | -------------------------------------------------------------------------------- /src/store/modules/use-loading-store.ts: -------------------------------------------------------------------------------- 1 | import { create } from 'zustand'; 2 | 3 | interface State { 4 | count: number; 5 | isLoading: boolean; 6 | } 7 | 8 | type Action = { 9 | showLoading: Fn; 10 | hideLoading: Fn; 11 | }; 12 | 13 | /** 14 | * 场景: 15 | * 并发请求,响应有快有慢,第一个请求完成后就给loading设置false结束了,实际我们要最后一个请求完成之后才结束loading,所以需要计数。 16 | */ 17 | export const useLoadingStore = create((set) => ({ 18 | count: 0, // 用于记录并发请求的数量 19 | isLoading: false, 20 | showLoading: () => set((state) => ({ ...state, count: state.count + 1, isLoading: true })), 21 | hideLoading: () => 22 | set((state) => ({ ...state, count: state.count - 1, isLoading: state.count - 1 > 0 })), 23 | })); 24 | -------------------------------------------------------------------------------- /src/utils/file.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 通过url获取文件后缀 3 | * @param url 图片链接 4 | * @example 5 | * getSuffixByUrl('https://example.com/path/to/file.txt'); // 返回 ".txt" 6 | * getSuffixByUrl('https://example.com/path/to/file.txt?version=1.2'); // 返回 ".txt" 7 | * getSuffixByUrl('https://example.com/path/to/file'); // 返回 "" 8 | * getSuffixByUrl('https://example.com/path/to/file.txt#section'); // 返回 ".txt" 9 | * getSuffixByUrl('哈哈哈.jpg'); // 返回 ".jpg" 10 | * getSuffixByUrl(''); // 返回 "" 11 | */ 12 | export function getSuffixByUrl(url = '') { 13 | const temp = url.split('/'); 14 | const filename = temp[temp.length - 1]; 15 | const filenameWithoutSuffix = filename?.split(/#|\?/)[0] || ''; 16 | return (/\.[^./\\]*$/.exec(filenameWithoutSuffix) || [''])[0]; 17 | } 18 | -------------------------------------------------------------------------------- /src/styles/scss/global.scss: -------------------------------------------------------------------------------- 1 | /** 公共样式放置处,main.tsx 中被引入 */ 2 | @import '../css/reset.css'; // 重置样式 3 | 4 | /** v-show */ 5 | .hide { 6 | display: none !important; 7 | } 8 | 9 | /* HTML:
*/ 10 | .route-loading { 11 | position: fixed; 12 | top: 50%; 13 | left: 50%; 14 | width: 120px; 15 | height: 22px; 16 | color: #514b82; 17 | border: 2px solid; 18 | border-radius: 20px; 19 | transform: translate(-50%, -50%); 20 | } 21 | 22 | .route-loading::before { 23 | position: absolute; 24 | inset: 0 100% 0 0; 25 | margin: 2px; 26 | content: ''; 27 | background: currentcolor; 28 | border-radius: inherit; 29 | animation: l6 2s infinite; 30 | 31 | @keyframes l6 { 32 | 100% { 33 | inset: 0; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/components/auto-scroll-to-top/index.tsx: -------------------------------------------------------------------------------- 1 | import { useLayoutEffect } from 'react'; 2 | import { useLocation } from 'react-router'; 3 | 4 | /** 5 | * 路由切换时页面置顶 6 | * (因为react不同于vue,没有方便的scrollBehavior: () => ({ left: 0, top: 0 })的路由配置项,所以封装此组件) 7 | * (将AutoScrollToTop包裹整个App或整个路由即可) 8 | */ 9 | const AutoScrollToTop = ({ children }: { children: any }) => { 10 | const location = useLocation(); 11 | useLayoutEffect(() => { 12 | const notScrollTop = ['']; // 排除不需要置顶的页面,示例'/home' 13 | if (!notScrollTop.includes(location.pathname)) { 14 | if (document?.documentElement || document?.body) { 15 | document.documentElement.scrollTop = document.body.scrollTop = 0; // 切换路由时页面置顶 16 | } 17 | } 18 | }, [location.pathname]); 19 | return children; 20 | }; 21 | 22 | export default AutoScrollToTop; 23 | -------------------------------------------------------------------------------- /src/services/index.ts: -------------------------------------------------------------------------------- 1 | import { AxiosRequestConfig } from 'axios'; 2 | 3 | import axiosInstance from './service'; 4 | 5 | /** 根据 axiosInstance 配置看情况修改 */ 6 | export const GET = >( 7 | url: string, 8 | params?: P, 9 | config?: AxiosRequestConfig, 10 | ): Promise> => { 11 | return axiosInstance>({ 12 | method: 'GET', 13 | url, 14 | params, 15 | ...config, 16 | }) 17 | .then((res) => res?.data) 18 | .catch((e) => e); // async/await就不需要加try/catch了 19 | }; 20 | 21 | export const POST = >( 22 | url: string, 23 | data?: P, 24 | config?: AxiosRequestConfig, 25 | ): Promise> => { 26 | return axiosInstance>({ 27 | method: 'POST', 28 | url, 29 | data, 30 | ...config, 31 | }) 32 | .then((res) => res?.data) 33 | .catch((e) => e); // async/await就不需要加try/catch了 34 | }; 35 | -------------------------------------------------------------------------------- /src/components/high-light/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | 3 | import classnames from 'classnames'; 4 | 5 | import './index.scss'; 6 | 7 | function Highlight({ 8 | children, 9 | keys, 10 | color = '#FFDA00', 11 | className, 12 | }: { 13 | children: React.ReactNode; 14 | keys: string[]; 15 | color?: string; 16 | className?: string; 17 | }) { 18 | // eslint-disable-next-line @eslint-react/no-children-to-array 19 | const string = React.Children.toArray(children).join(''); 20 | const reg = new RegExp(keys.join('|'), 'g'); 21 | const token = string.replace(reg, '#@$&#'); 22 | const elements = token.split('#').map((x, index) => 23 | index % 2 === 0 ? ( 24 | x 25 | ) : ( 26 | 27 | {x[0] === '@' ? x.slice(1) : x} 28 | 29 | ), 30 | ); 31 | 32 | return <>{elements}; 33 | } 34 | 35 | export default memo(Highlight); 36 | -------------------------------------------------------------------------------- /src/store/modules/use-user-store.ts: -------------------------------------------------------------------------------- 1 | // zustand库和Immer中间件:使用示例参考(此store可以随便删除) 2 | import { create } from 'zustand'; 3 | import { immer } from 'zustand/middleware/immer'; 4 | 5 | type UserInfo = { 6 | username: string; 7 | avatar: string; 8 | }; 9 | 10 | interface State { 11 | token: string; 12 | userInfo: UserInfo; 13 | } 14 | 15 | type Action = { 16 | updateToken: (token: string) => void; 17 | updateUserName: (username: string) => void; 18 | updateUserInfo?: (userInfo: UserInfo) => void; 19 | }; 20 | 21 | // 创建带有Immer中间件的zustand存储 22 | export const useUserStore = create()( 23 | immer((set) => ({ 24 | token: '', 25 | userInfo: { username: 'react', avatar: 'https://picsum.photos/200/300' }, 26 | updateToken: (token) => 27 | set((state) => { 28 | state.token = token; 29 | }), 30 | updateUserName: (username) => 31 | set((state) => { 32 | state.userInfo.username = username; 33 | }), 34 | })), 35 | ); 36 | -------------------------------------------------------------------------------- /.commitlintrc.js: -------------------------------------------------------------------------------- 1 | /** @type {import("@commitlint/types").UserConfig} */ 2 | export default { 3 | extends: ['@commitlint/config-conventional'], // 继承使用常规的 Commit 规范。 4 | rules: { 5 | 'type-enum': [ 6 | 2, 7 | 'always', 8 | [ 9 | 'feat', // 新功能(feature) 10 | 'fix', // 修补bug 11 | 'docs', // 文档(documentation) 12 | 'style', // 格式、样式(不影响代码运行的变动) 13 | 'refactor', // 重构(即不是新增功能,也不是修改BUG的代码) 14 | 'perf', // 优化相关,比如提升性能、体验 15 | 'test', // 添加测试 16 | 'ci', // 持续集成修改 17 | 'chore', // 构建过程或辅助工具的变动 18 | 'revert', // 回滚到上一个版本 19 | 'build', // 影响构建系统或外部依赖的更改 20 | /** 以下为自定义,以上(Angular 团队提出的 Conventional Commits 规范,即@commitlint/config-conventional插件配置) */ 21 | 'workflow', // 工作流改进 22 | 'mod', // 不确定分类的修改 23 | 'wip', // 开发中 24 | 'types', // 类型修改 25 | 'release', // 版本发布 26 | ], 27 | ], 28 | 'subject-full-stop': [0, 'never'], 29 | 'subject-case': [0, 'never'], 30 | }, 31 | }; 32 | -------------------------------------------------------------------------------- /src/router/modules/test.tsx: -------------------------------------------------------------------------------- 1 | import { lazy } from 'react'; 2 | import { Outlet } from 'react-router'; 3 | 4 | import { RouteObject } from '@/types/router'; 5 | 6 | import { LazyLoad } from '../utils/lazy-load'; 7 | 8 | export default [ 9 | { 10 | path: '/test', 11 | element: , // 没有元素,呈现空白 12 | children: [ 13 | { 14 | path: '/test/count', 15 | element: LazyLoad(lazy(() => import('@/views/test/count'))), 16 | meta: { 17 | title: '计数标题', 18 | }, 19 | }, 20 | { 21 | path: '/test/create', 22 | element: LazyLoad(lazy(() => import('@/views/test/create'))), 23 | meta: { 24 | title: 'create页面', 25 | }, 26 | }, 27 | { 28 | path: '/test/auth-test', 29 | element: LazyLoad(lazy(() => import('@/views/test/auth-test'))), 30 | }, 31 | { 32 | path: '/test/error-test', 33 | element: LazyLoad(lazy(() => import('@/views/test/error-test/throw-error-comp'))), 34 | }, 35 | ], 36 | }, 37 | ] as RouteObject[]; 38 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["ESNext", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "Bundler", 11 | "allowImportingTsExtensions": true, 12 | "isolatedModules": true, 13 | "moduleDetection": "force", 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true, 22 | "noUncheckedSideEffectImports": true, 23 | "exactOptionalPropertyTypes": true, 24 | "noImplicitReturns": true, 25 | "noPropertyAccessFromIndexSignature": false, 26 | "noUncheckedIndexedAccess": false, 27 | 28 | /** 别名路径提示 */ 29 | "baseUrl": ".", 30 | "paths": { 31 | "@/*": ["./src/*"] 32 | } 33 | }, 34 | "include": ["**/*.ts", "src/**/*.d.ts", "**/*.tsx"], 35 | "exclude": ["node_modules", "dist", "dist-*"] 36 | } 37 | -------------------------------------------------------------------------------- /stylelint.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import("stylelint").Config} */ 2 | export default { 3 | root: true, 4 | // 继承某些已有的规则 5 | extends: [ 6 | 'stylelint-config-standard', // css 标准配置 7 | 'stylelint-config-standard-scss', // scss 标准配置 8 | 'stylelint-config-recess-order', // CSS 属性排序配置 9 | ], 10 | plugins: ['stylelint-order'], 11 | rules: { 12 | 'no-descending-specificity': null, // 禁止在具有较高优先级的选择器后出现被其覆盖的较低优先级的选择器 13 | 'no-empty-source': null, // 关闭禁止空源码 14 | 'selector-class-pattern': null, // 关闭强制选择器类名的格式 15 | 'property-no-unknown': null, // 禁止未知的属性(true 为不允许) 16 | 'value-no-vendor-prefix': null, // 关闭 属性值前缀 --webkit-box 17 | 'property-no-vendor-prefix': null, // 关闭 属性前缀 -webkit-mask 18 | 'selector-pseudo-class-no-unknown': [ 19 | true, 20 | { 21 | ignorePseudoClasses: ['global', 'export'], 22 | }, 23 | ], 24 | }, 25 | ignoreFiles: [ 26 | '**/*.js', 27 | '**/*.jsx', 28 | '**/*.tsx', 29 | '**/*.ts', 30 | '**/*.json', 31 | '**/*.md', 32 | '**/*.yaml', 33 | '**/*.cjs', 34 | ], 35 | }; 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 友人A 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/router/utils/route-guard.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * 第二种实现路由守卫的方式(参考):使用高阶组件(HOC),监听pathname,在useEffect中作路由守卫。 3 | * 不确定的影响: 4 | * 这样实现的话可能会出现页面内容加载完后以及触发接口调用后才跳出页面的情况,闪白屏。 5 | * 6 | * React-Ts-Template 默认使用的是第一种实现方式(loader),如果更喜欢下面这种,可以根据情况修改; 7 | * 如果使用默认的loader实现,【可以将此文件删除】。 8 | * 9 | * 其他拦截组件封装方式参考: 10 | * https://segmentfault.com/a/1190000044439881 11 | */ 12 | import { FC, ReactNode, useEffect } from 'react'; 13 | import { useLocation, useNavigate } from 'react-router'; 14 | 15 | export const RouteGuard: FC<{ children: ReactNode }> = (props) => { 16 | const { pathname } = useLocation(); 17 | 18 | const navigate = useNavigate(); 19 | const token = localStorage.getItem('token') || ''; 20 | 21 | useEffect(() => { 22 | if (!token) { 23 | // message.error("token 过期,请重新登录!"); 24 | navigate('/login'); 25 | } 26 | // 已经登录的状态 27 | if (token) { 28 | if (location.pathname == '/' || location.pathname == '/login') { 29 | navigate('/home'); 30 | } else { 31 | // 如果是其他路由就跳到其他的路由 32 | navigate(location.pathname); 33 | } 34 | } 35 | }, [token, pathname, navigate]); 36 | 37 | return <>{props.children}; 38 | }; 39 | -------------------------------------------------------------------------------- /src/router/index.tsx: -------------------------------------------------------------------------------- 1 | import { lazy } from 'react'; 2 | import { Navigate, RouteObject, createBrowserRouter } from 'react-router'; 3 | 4 | import ErrorBoundary from '../error-boundary'; 5 | import { LazyLoad, routes } from './utils'; 6 | 7 | const router: RouteObject[] = [ 8 | { 9 | path: '/', 10 | /** 11 | * 可以在Root组件(自己新建),用 useLoaderData 接收 loader 返回的数据做一些操作 12 | * @see https://reactrouter.com/en/main/hooks/use-loader-data#useloaderdata 13 | */ 14 | // element: , 15 | errorElement: , 16 | children: [ 17 | { 18 | index: true, 19 | element: , // 重定向 20 | }, 21 | { 22 | path: '/home', 23 | element: LazyLoad(lazy(() => import('@/views/home'))), 24 | }, 25 | { 26 | path: '/login', 27 | element: LazyLoad(lazy(() => import('@/views/test/login'))), 28 | }, 29 | { 30 | path: '/404', 31 | element: LazyLoad(lazy(() => import('@/components/not-fount'))), 32 | }, 33 | ...routes, // modules 路由 34 | ], 35 | }, 36 | { 37 | path: '*', 38 | element: , // 找不到页面 39 | }, 40 | ]; 41 | 42 | export default createBrowserRouter(router); 43 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | /** @type {import("prettier").Config} */ 2 | export default { 3 | printWidth: 100, // 一行的字符数,如果超过会进行换行,默认为80 4 | tabWidth: 2, // 一个tab代表几个空格数,默认为2 5 | useTabs: false, // 是否使用tab进行缩进,默认为false,表示用空格进行缩减 6 | semi: true, // 行尾是否使用分号 7 | singleQuote: true, // 字符串是否使用单引号,默认为false,使用双引号 8 | quoteProps: 'as-needed', // 对象键值对中的字符串是否需要加引号 9 | bracketSpacing: true, // 对象大括号之间是否有空格,默认为true,效果:{ foo: bar } 10 | trailingComma: 'all', // 是否使用尾逗号,有三个可选值:"none", "es5", "all" 11 | jsxSingleQuote: false, // JSX 中的字符串是否使用单引号 12 | arrowParens: 'always', // 箭头函数参数是否总是使用括号 13 | insertPragma: false, // 是否在文件顶部插入@format注释 14 | requirePragma: false, // 是否仅在文件中有@format注释时才格式化 15 | proseWrap: 'never', // 长文本是否自动换行 16 | htmlWhitespaceSensitivity: 'strict', // HTML中的空白字符敏感度 17 | endOfLine: 'auto', // 文件末尾的换行符 18 | rangeStart: 0, // 格式化的起始位置 19 | // import 顺序自动格式化插件: 20 | plugins: ['@trivago/prettier-plugin-sort-imports'], 21 | importOrderSeparation: true, 22 | importOrderSortSpecifiers: true, 23 | importOrder: [ 24 | '^react(.*)$', // React 相关放在最前面 25 | '', // 其他第三方模块 26 | '^@/components/(.*)$', // 全局组件 27 | '^@/(hooks|store)(.*)$', // 自定义 hooks 和 store 统一分组 28 | '^@/(.*)$', // 其他 @/ 开头的模块 29 | '^[./]', // 当前文件夹和父文件夹的相对导入 30 | ], 31 | }; 32 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | // 开启自动修复 3 | "editor.codeActionsOnSave": { 4 | "source.fixAll": "never", 5 | "source.fixAll.eslint": "explicit", 6 | "source.fixAll.stylelint": "explicit" 7 | }, 8 | // 保存的时候自动格式化 9 | "editor.formatOnSave": true, 10 | // 默认格式化工具选择prettier 11 | "editor.defaultFormatter": "esbenp.prettier-vscode", 12 | // 配置该项,新建文件时默认就是space:2 13 | "editor.tabSize": 2, 14 | // stylelint校验的文件格式 15 | "stylelint.validate": ["css", "scss", "html"], 16 | "[typescriptreact]": { 17 | "editor.defaultFormatter": "esbenp.prettier-vscode" 18 | }, 19 | "[typescript]": { 20 | "editor.defaultFormatter": "esbenp.prettier-vscode" 21 | }, 22 | "[javascript]": { 23 | "editor.defaultFormatter": "esbenp.prettier-vscode" 24 | }, 25 | "[javascriptreact]": { 26 | "editor.defaultFormatter": "esbenp.prettier-vscode" 27 | }, 28 | "[json]": { 29 | "editor.defaultFormatter": "esbenp.prettier-vscode" 30 | }, 31 | "[html]": { 32 | "editor.defaultFormatter": "esbenp.prettier-vscode" 33 | }, 34 | "[css]": { 35 | "editor.defaultFormatter": "esbenp.prettier-vscode" 36 | }, 37 | "[scss]": { 38 | "editor.defaultFormatter": "esbenp.prettier-vscode" 39 | }, 40 | "[markdown]": { 41 | "editor.defaultFormatter": "esbenp.prettier-vscode" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/views/home/index.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | 3 | import { useRouter } from '@/hooks'; 4 | 5 | import reactLogo from '@/assets/react.svg'; 6 | 7 | import './index.scss'; 8 | import viteLogo from '/vite.svg'; 9 | 10 | function Home() { 11 | const [count, setCount] = useState(0); 12 | const router = useRouter(); 13 | return ( 14 |
15 | 23 |

Vite + React

24 |
25 | 28 |

29 | Edit src/App.tsx and save to test HMR 30 |

31 |
32 |

Click on the Vite and React logos to learn more

33 |
34 | 37 |
38 | ); 39 | } 40 | 41 | export default Home; 42 | -------------------------------------------------------------------------------- /src/views/test/count/index.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useState } from 'react'; 2 | 3 | import { createDesign, useRouter } from '@/hooks'; 4 | import { useLoadingStore } from '@/store'; 5 | 6 | import './index.scss'; 7 | 8 | const { prefixCls } = createDesign('pg-guild-count'); 9 | 10 | function Count() { 11 | const [count, setCount] = useState(0); 12 | 13 | const router = useRouter(); 14 | 15 | const { showLoading, hideLoading } = useLoadingStore(); 16 | 17 | const handleLoading = useCallback(() => { 18 | showLoading(); 19 | setTimeout(() => { 20 | hideLoading(); 21 | }, 1000); 22 | }, [hideLoading, showLoading]); 23 | 24 | return ( 25 |
26 | {count} 27 | 30 |
31 | 34 | 37 | 40 | 43 |
44 | ); 45 | } 46 | 47 | export default Count; 48 | -------------------------------------------------------------------------------- /src/utils/dateUtil.ts: -------------------------------------------------------------------------------- 1 | import dayjs from 'dayjs'; 2 | 3 | const DATE_TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'; 4 | const DATE_FORMAT = 'YYYY-MM-DD'; 5 | 6 | /** 7 | * 日期时间格式化工具。 8 | */ 9 | export const dateUtil = dayjs; 10 | 11 | /** 12 | * 将日期时间格式化为指定格式的字符串。 13 | * @param date 要格式化的日期时间。如果未提供,则默认为当前时间。 14 | * @param format 要格式化的目标格式,默认为 DATE_TIME_FORMAT。 15 | * @returns 格式化后的日期时间字符串。 16 | */ 17 | export function formatToDateTime(date?: dayjs.ConfigType, format = DATE_TIME_FORMAT): string { 18 | return dayjs(date).format(format); 19 | } 20 | 21 | /** 22 | * 将日期格式化为指定格式的字符串。 23 | * @param date 要格式化的日期。如果未提供,则默认为当前日期。 24 | * @param format 要格式化的目标格式,默认为 DATE_FORMAT。 25 | * @returns 格式化后的日期字符串。 26 | */ 27 | export function formatToDate(date?: dayjs.ConfigType, format = DATE_FORMAT): string { 28 | return dayjs(date).format(format); 29 | } 30 | 31 | /** 32 | * 判断指定时间是否在某个时间之前。 33 | * @param {string | dayjs.ConfigType} [specifiedTime] 要比较的指定时间,默认为当前时间。格式为 'YYYY-MM-DD HH:mm:ss' 或 dayjs 对象。 34 | * @param {string} targetTime 要比较的目标时间,格式为 'YYYY-MM-DD HH:mm:ss'。 35 | * @returns {boolean} 如果指定时间在目标时间之前,则返回 true,否则返回 false。 36 | */ 37 | export function isTimeBefore( 38 | specifiedTime: dayjs.ConfigType = dayjs(), 39 | targetTime: string, 40 | ): boolean { 41 | // 将指定时间转换为 dayjs 对象 42 | const specified = dayjs(specifiedTime); 43 | 44 | // 将目标时间转换为 dayjs 对象 45 | const target = dayjs(targetTime); 46 | 47 | return specified.isBefore(target); 48 | } 49 | -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/views/test/create/index.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | 3 | import { useUserStore } from '@/store'; 4 | 5 | import './index.scss'; 6 | 7 | function Create() { 8 | const [count, _setCount] = useState(99); 9 | 10 | const { userInfo, updateUserName } = useUserStore(); 11 | 12 | return ( 13 |
14 | Guild/Create: 15 | {count} 16 |
17 | {userInfo.username} 18 | 19 | 22 |
23 | ); 24 | } 25 | 26 | export default Create; 27 | 28 | /** 29 | // **** import 自动排序测试 **** 30 | // 复制以下,替换全部上面的import 31 | 32 | import React, { useState } from 'react'; 33 | import { useLocation } from 'react-router'; 34 | import Cmp1 from '@/components/auto-scroll-to-top'; 35 | import Cmp2 from '@/components/not-fount'; 36 | import { useRouter } from '@/hooks'; 37 | import { useUserStore } from '@/store'; 38 | import { uuid } from '@/utils'; 39 | import { initializeApp } from '@core/app'; 40 | import { logger } from '@core/logger'; 41 | import { createConnection } from '@server/database'; 42 | import { createServer } from '@server/node'; 43 | import { Alert } from '@ui/Alert'; 44 | import { Popup } from '@ui/Popup'; 45 | import { Message } from '../Message'; 46 | import { add, filter, repeat } from '../utils'; 47 | import './index.scss'; 48 | 49 | */ 50 | -------------------------------------------------------------------------------- /src/components/loading/index.tsx: -------------------------------------------------------------------------------- 1 | import { Root, createRoot } from 'react-dom/client'; 2 | 3 | import './index.scss'; 4 | 5 | let container: HTMLDivElement | null = null; 6 | let root: Root | null = null; 7 | 8 | /** Loading组件示例,可替换为ui库的loading组件作二次封装 */ 9 | function Loading() { 10 | return ( 11 |
12 | 13 | 22 | 23 | 30 | 31 | 32 |
33 | ); 34 | } 35 | 36 | Loading.show = () => { 37 | if (container || root) return; 38 | container = document.createElement('div'); 39 | container.setAttribute('id', 'pub-loading'); 40 | root = createRoot(container); 41 | root.render(); 42 | document.body.appendChild(container); 43 | }; 44 | 45 | Loading.hide = () => { 46 | if (container && root) { 47 | root.unmount(); 48 | document.body.removeChild(container); 49 | container = root = null; 50 | } 51 | }; 52 | 53 | export default Loading; 54 | -------------------------------------------------------------------------------- /src/utils/tools.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 简单的随机唯一id 3 | * @returns string 4 | */ 5 | export function uuid() { 6 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { 7 | const r = (Math.random() * 16) | 0, 8 | v = c == 'x' ? r : (r & 0x3) | 0x8; 9 | return v.toString(16); 10 | }); 11 | } 12 | 13 | /** 14 | * 复制文本到剪贴板 15 | * @param {string} text 要复制的文本 16 | * @param {string | null} 复制成功时的提示文本 17 | */ 18 | export function copy(text: string, _prompt: string | null = '已成功复制到剪切板!') { 19 | if (navigator.clipboard) { 20 | return navigator.clipboard 21 | .writeText(text) 22 | .then(() => { 23 | // prompt && message.success(prompt); 24 | }) 25 | .catch((error) => { 26 | // message.error('复制失败!' + error.message); 27 | return error; 28 | }); 29 | } 30 | if (Reflect.has(document, 'execCommand')) { 31 | return new Promise((resolve, reject) => { 32 | try { 33 | const textArea = document.createElement('textarea'); 34 | textArea.value = text; 35 | // 在手机 Safari 浏览器中,点击复制按钮,整个页面会跳动一下 36 | textArea.style.width = '0'; 37 | textArea.style.position = 'fixed'; 38 | textArea.style.left = '-999px'; 39 | textArea.style.top = '10px'; 40 | textArea.setAttribute('readonly', 'readonly'); 41 | document.body.appendChild(textArea); 42 | textArea.select(); 43 | document.execCommand('copy'); 44 | document.body.removeChild(textArea); 45 | 46 | // prompt && message.success(prompt); 47 | resolve(); 48 | } catch (error) { 49 | // message.error('复制失败!' + error.message); 50 | reject(error); 51 | } 52 | }); 53 | } 54 | return Promise.reject(`"navigator.clipboard" 或 "document.execCommand" 中存在API错误, 拷贝失败!`); 55 | } 56 | -------------------------------------------------------------------------------- /src/services/service.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosError, AxiosInstance, AxiosResponse, InternalAxiosRequestConfig } from 'axios'; 2 | 3 | // 创建axios实例 4 | const axiosInstance: AxiosInstance = axios.create({ 5 | baseURL: import.meta.env.VITE_API_BASE_URL, // 请求的默认前缀 只要是发出去请求就会 默认带上这个前缀 6 | timeout: 10000, // 请求超时时间:10s 7 | headers: { 'Content-Type': 'application/json' }, // 设置默认请求头 8 | }); 9 | 10 | // 请求拦截器 11 | axiosInstance.interceptors.request.use( 12 | (config: InternalAxiosRequestConfig) => { 13 | // 在请求里加入token认证信息 14 | // const token = getToken() // localStorage.getItem('token') 15 | // if (token) { 16 | // config.headers.Authorization = `Bearer ${token}` 17 | // } 18 | return config; 19 | }, 20 | (err: AxiosError) => { 21 | return Promise.reject(err); 22 | }, 23 | ); 24 | 25 | // 响应拦截器即异常处理 26 | axiosInstance.interceptors.response.use( 27 | (res: AxiosResponse) => { 28 | // const code = res.data.code; 29 | // switch (code) { 30 | // case 200: 31 | // return res.data; 32 | // case 401: 33 | // /** 登录失效逻辑... */ 34 | // return res.data || {}; 35 | // default: 36 | // return res.data || {}; 37 | // } 38 | return res; // res.data 39 | }, 40 | (err: AxiosError) => { 41 | // 如果接口请求报错时,也可以直接返回对象,如return { message: onErrorReason(error.message) },这样使用async/await就不需要加try/catch 42 | // onErrorReason(err.message) // 做一些全局的错误提示,可用ui库的message提示组件 43 | return Promise.resolve(err); 44 | }, 45 | ); 46 | 47 | /** 解析http层面请求异常原因 */ 48 | // function onErrorReason(message: string): string { 49 | // if (message.includes('Network Error')) { 50 | // return '网络异常,请检查网络情况!'; 51 | // } 52 | // if (message.includes('timeout')) { 53 | // return '请求超时,请重试!'; 54 | // } 55 | // return '服务异常,请重试!'; 56 | // } 57 | 58 | // 导出实例 59 | export default axiosInstance; 60 | -------------------------------------------------------------------------------- /src/styles/scss/tools.scss: -------------------------------------------------------------------------------- 1 | /** 全局样式工具函数放置处 */ 2 | @use 'sass:meta'; 3 | @use 'sass:list'; 4 | @use 'sass:map'; 5 | 6 | /** 滚动条 */ 7 | @mixin scrollbar($size: 7px, $color: rgba(0, 0, 0, 0.5)) { 8 | scrollbar-color: $color transparent; 9 | scrollbar-width: thin; 10 | 11 | /* stylelint-disable nesting-selector-no-missing-scoping-root */ 12 | &::-webkit-scrollbar-thumb { 13 | background-color: $color; 14 | border-radius: $size; 15 | } 16 | 17 | &::-webkit-scrollbar-thumb:hover { 18 | background-color: $color; 19 | border-radius: $size; 20 | } 21 | 22 | &::-webkit-scrollbar { 23 | width: $size; 24 | height: $size; 25 | } 26 | 27 | &::-webkit-scrollbar-track-piece { 28 | background-color: rgb(0 0 0 / 0%); 29 | border-radius: 0; 30 | } 31 | } 32 | 33 | /** 文字描边 */ 34 | @mixin text-stroke($color: #fff, $width: 1px) { 35 | text-shadow: 36 | 0 -#{$width} #{$color}, 37 | #{$width} 0 #{$color}, 38 | 0 #{$width} #{$color}, 39 | -#{$width} 0 #{$color}, 40 | -#{$width} -#{$width} #{$color}, 41 | #{$width} #{$width} #{$color}, 42 | #{$width} -#{$width} #{$color}, 43 | -#{$width} #{$width} #{$color}; 44 | } 45 | 46 | /** 媒体查询 */ 47 | // 创建数据集 48 | $arr: ( 49 | 'phone': ( 50 | 320px, 51 | 480px, 52 | ), 53 | 'pad': ( 54 | 481px, 55 | 768px, 56 | ), 57 | 'pc': ( 58 | 769px, 59 | 1024px, 60 | ), 61 | 'desktop': ( 62 | 1025px, 63 | 1200px, 64 | ), 65 | 'tv': 1201px, 66 | ); 67 | 68 | @mixin set-media($key) { 69 | // 根据key来获取数组项 70 | $bp: map.get($arr, $key); // map-get 方法 返回 key的索引值 71 | 72 | @if meta.type-of($bp) == 'list' { 73 | // type-of 类型判断(x,y)结构返回 list 74 | $min: list.nth($bp, 1); // nth(数组,索引) 下标从1开始 返回索引的值 75 | $max: list.nth($bp, 2); 76 | 77 | @media (min-width: $min) and (max-width: $max) { 78 | @content; // @content 可以当做一个插槽 79 | } 80 | } @else { 81 | @media (min-width: $bp) { 82 | @content; 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/views/home/index.scss: -------------------------------------------------------------------------------- 1 | .pg-home { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: center; 5 | height: 100vh; 6 | padding-top: 200px; 7 | text-align: center; 8 | 9 | .logo { 10 | height: 6em; 11 | padding: 1.5em; 12 | transition: filter 300ms; 13 | will-change: filter; 14 | } 15 | 16 | .logo:hover { 17 | filter: drop-shadow(0 0 2em #646cffaa); 18 | } 19 | 20 | .logo.react:hover { 21 | filter: drop-shadow(0 0 2em #61dafbaa); 22 | } 23 | 24 | @keyframes logo-spin { 25 | from { 26 | transform: rotate(0deg); 27 | } 28 | 29 | to { 30 | transform: rotate(360deg); 31 | } 32 | } 33 | 34 | @media (prefers-reduced-motion: no-preference) { 35 | a:nth-of-type(2) .logo { 36 | animation: logo-spin infinite 20s linear; 37 | } 38 | } 39 | 40 | .card { 41 | padding: 2em; 42 | } 43 | 44 | .read-the-docs { 45 | color: #888; 46 | } 47 | 48 | a { 49 | font-weight: 500; 50 | color: #646cff; 51 | text-decoration: inherit; 52 | } 53 | 54 | a:hover { 55 | color: #535bf2; 56 | } 57 | 58 | body { 59 | display: flex; 60 | place-items: center; 61 | min-width: 320px; 62 | min-height: 100vh; 63 | margin: 0; 64 | } 65 | 66 | h1 { 67 | font-size: 3.2em; 68 | line-height: 1.1; 69 | } 70 | 71 | button { 72 | padding: 0.6em 1.2em; 73 | font-family: inherit; 74 | font-size: 1em; 75 | font-weight: 500; 76 | cursor: pointer; 77 | background-color: #1a1a1a; 78 | border: 1px solid transparent; 79 | border-radius: 8px; 80 | transition: border-color 0.25s; 81 | } 82 | 83 | button:hover { 84 | border-color: #646cff; 85 | } 86 | 87 | button:focus, 88 | button:focus-visible { 89 | outline: 4px auto -webkit-focus-ring-color; 90 | } 91 | 92 | @media (prefers-color-scheme: light) { 93 | :root { 94 | color: #213547; 95 | background-color: #fff; 96 | } 97 | 98 | a:hover { 99 | color: #747bff; 100 | } 101 | 102 | button { 103 | background-color: #f9f9f9; 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Pages 2 | 3 | # 触发条件,push到master分支或者pull request到master分支 4 | on: 5 | push: 6 | branches: [master] 7 | pull_request: 8 | branches: [master] 9 | 10 | # 支持手动在工作流上触发 11 | workflow_dispatch: 12 | 13 | # 设置时区 14 | env: 15 | TZ: Asia/Shanghai 16 | 17 | # 权限设置 18 | permissions: 19 | # 允许读取仓库内容的权限。 20 | contents: read 21 | # 允许写入 GitHub Pages 的权限。 22 | pages: write 23 | # 允许写入 id-token 的权限。 24 | id-token: write 25 | 26 | # 并发控制配置 27 | concurrency: 28 | group: pages 29 | cancel-in-progress: false 30 | 31 | # 定义执行任务 32 | jobs: 33 | # 构建任务 34 | build: 35 | runs-on: ubuntu-latest 36 | 37 | # node v20 运行 38 | strategy: 39 | matrix: 40 | node-version: [20] 41 | 42 | steps: 43 | # 拉取代码 44 | - name: Checkout 45 | uses: actions/checkout@v3 46 | with: 47 | # 保留 Git 信息 48 | fetch-depth: 0 49 | 50 | # 设置使用 Node.js 版本 51 | - name: Use Node.js ${{ matrix.node-version }} 52 | uses: actions/setup-node@v3 53 | with: 54 | node-version: ${{ matrix.node-version }} 55 | 56 | # 使用 最新的 PNPM 57 | # 你也可以指定为具体的版本 58 | - uses: pnpm/action-setup@v2 59 | name: Install pnpm 60 | with: 61 | version: latest 62 | # version: 9 63 | run_install: false 64 | 65 | # 安装依赖 66 | - name: Install dependencies 67 | run: pnpm install --frozen-lockfile 68 | 69 | # 构建项目 70 | - name: Build blog project 71 | run: | 72 | echo ${{ github.workspace }} 73 | pnpm run build:dev 74 | 75 | # 资源拷贝 76 | - name: Build with Jekyll 77 | uses: actions/jekyll-build-pages@v1 78 | with: 79 | source: ./dist-dev 80 | destination: ./_site 81 | 82 | # 上传 _site 的资源,用于后续部署 83 | - name: Upload artifact 84 | uses: actions/upload-pages-artifact@v3 85 | 86 | # 部署任务 87 | deploy: 88 | environment: 89 | name: github-pages 90 | url: ${{ steps.deployment.outputs.page_url }} 91 | runs-on: ubuntu-latest 92 | needs: build 93 | steps: 94 | - name: Deploy to GitHub Pages 95 | id: deployment 96 | uses: actions/deploy-pages@v4 97 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import eslintReact from '@eslint-react/eslint-plugin'; 2 | import js from '@eslint/js'; 3 | import reactHooks from 'eslint-plugin-react-hooks'; 4 | import reactRefresh from 'eslint-plugin-react-refresh'; 5 | import globals from 'globals'; 6 | import tseslint from 'typescript-eslint'; 7 | 8 | export default tseslint.config( 9 | { ignores: ['dist', 'dist*', 'node_modules'] }, 10 | { 11 | extends: [ 12 | js.configs.recommended, 13 | tseslint.configs.recommended, 14 | eslintReact.configs['recommended-typescript'], 15 | ], 16 | files: ['**/*.{ts,tsx}'], 17 | languageOptions: { 18 | ecmaVersion: 2020, 19 | globals: globals.browser, 20 | }, 21 | plugins: { 22 | 'react-hooks': reactHooks, 23 | 'react-refresh': reactRefresh, 24 | }, 25 | rules: { 26 | ...reactHooks.configs.recommended.rules, 27 | 28 | // React 相关规则 29 | 'react-refresh/only-export-components': 'off', 30 | '@eslint-react/hooks-extra/no-unnecessary-use-prefix': 'off', 31 | 32 | // TypeScript 相关规则优化 33 | '@typescript-eslint/no-unused-vars': [ 34 | 'warn', 35 | { 36 | argsIgnorePattern: '^_', 37 | varsIgnorePattern: '^_', 38 | caughtErrorsIgnorePattern: '^_', 39 | }, 40 | ], 41 | '@typescript-eslint/no-explicit-any': 'off', 42 | '@typescript-eslint/no-unused-expressions': [ 43 | 'error', 44 | { 45 | allowShortCircuit: true, 46 | allowTernary: true, 47 | allowTaggedTemplates: true, 48 | }, 49 | ], 50 | '@typescript-eslint/prefer-nullish-coalescing': 'off', 51 | '@typescript-eslint/prefer-optional-chain': 'off', 52 | '@typescript-eslint/no-non-null-assertion': 'off', 53 | 54 | // 代码质量规则 55 | 'no-console': ['warn', { allow: ['warn', 'error'] }], 56 | 'no-debugger': 'error', 57 | 'prefer-const': 'error', 58 | 'no-var': 'error', 59 | 60 | // React Hooks 规则 61 | 'react-hooks/exhaustive-deps': 'warn', 62 | 63 | // 导入规则 64 | 'no-duplicate-imports': 'error', 65 | }, 66 | languageOptions: { 67 | parser: tseslint.parser, 68 | parserOptions: { 69 | projectService: true, 70 | }, 71 | }, 72 | }, 73 | ); 74 | -------------------------------------------------------------------------------- /src/router/utils/index.ts: -------------------------------------------------------------------------------- 1 | import { LoaderFunctionArgs } from 'react-router'; 2 | 3 | import { RouteObject } from '@/types/router'; 4 | 5 | export * from './lazy-load'; 6 | 7 | /** 路由列表 */ 8 | export const routes = getRoutesFromModules(); 9 | 10 | /** 路由白名单 */ 11 | export const WHITE_LIST = new Set([ 12 | '/', 13 | '/login', 14 | '/home', 15 | '/404', 16 | '/test/create', 17 | '/test/count', 18 | '/test/error-test', 19 | ]); 20 | 21 | /** 22 | * 基于 router/modules 文件导出的内容动态生成路由 23 | */ 24 | export function getRoutesFromModules() { 25 | const routes: RouteObject[] = []; 26 | 27 | const modules = import.meta.glob('../modules/**/*.tsx', { eager: true }) as Record< 28 | string, 29 | Record<'default', RouteObject[]> 30 | >; 31 | const addConfigurationToRoute = (r: RouteObject) => { 32 | r.loader = (options: LoaderFunctionArgs) => { 33 | // 设置标题 34 | document.title = r.meta?.title ?? import.meta.env.VITE_APP_TITLE; 35 | return loader(options); 36 | }; 37 | if (r.children) { 38 | r.children = r.children.map((child) => addConfigurationToRoute(child)); 39 | } 40 | return r; 41 | }; 42 | Object.keys(modules).forEach((key) => { 43 | const mod = modules[key].default || {}; 44 | const modList = Array.isArray(mod) ? [...mod] : [mod]; 45 | // 为每个路由添加 loader 并递归处理子路由 46 | const processedRoutes = modList.map((route) => { 47 | return addConfigurationToRoute(route); 48 | }); 49 | routes.push(...processedRoutes); 50 | }); 51 | return routes; 52 | } 53 | 54 | /** 55 | * 使用 loader 作路由守卫 56 | * @see https://reactrouter.com/start/data/route-object#loader 57 | */ 58 | export function loader({ request }: LoaderFunctionArgs) { 59 | const pathname = getPathName(request.url); 60 | // 权限校验 61 | const token = localStorage.getItem('token'); 62 | // 未登录且不在白名单中,跳转到登录页 63 | if (!token && !WHITE_LIST.has(pathname)) { 64 | window.location.replace(`/login?callback=${encodeURIComponent(window.location.href)}`); 65 | return false; 66 | } 67 | return true; 68 | } 69 | 70 | /** 71 | * 从给定的 URL 中获取 pathname 72 | */ 73 | export function getPathName(url: string): string { 74 | try { 75 | const parsedUrl = new URL(url); 76 | return parsedUrl.pathname; 77 | } catch { 78 | return window.location.pathname; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | %VITE_APP_TITLE% 9 | 10 | 11 | 12 |
13 | 14 | 71 |
72 |
73 |

%VITE_APP_TITLE%

74 |
75 |
76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /src/styles/css/reset.css: -------------------------------------------------------------------------------- 1 | *, 2 | *::after, 3 | *::before { 4 | box-sizing: border-box; 5 | outline: none; 6 | } 7 | 8 | html, 9 | body, 10 | div, 11 | span, 12 | applet, 13 | object, 14 | iframe, 15 | h1, 16 | h2, 17 | h3, 18 | h4, 19 | h5, 20 | h6, 21 | p, 22 | blockquote, 23 | pre, 24 | a, 25 | abbr, 26 | acronym, 27 | address, 28 | big, 29 | cite, 30 | code, 31 | del, 32 | dfn, 33 | em, 34 | img, 35 | ins, 36 | kbd, 37 | q, 38 | s, 39 | samp, 40 | small, 41 | strike, 42 | strong, 43 | sub, 44 | sup, 45 | tt, 46 | var, 47 | b, 48 | u, 49 | i, 50 | center, 51 | dl, 52 | dt, 53 | dd, 54 | ol, 55 | ul, 56 | li, 57 | fieldset, 58 | form, 59 | label, 60 | legend, 61 | table, 62 | caption, 63 | tbody, 64 | tfoot, 65 | thead, 66 | tr, 67 | th, 68 | td, 69 | article, 70 | aside, 71 | canvas, 72 | details, 73 | embed, 74 | figure, 75 | figcaption, 76 | footer, 77 | header, 78 | hgroup, 79 | menu, 80 | nav, 81 | output, 82 | ruby, 83 | section, 84 | summary, 85 | time, 86 | mark, 87 | audio, 88 | video { 89 | padding: 0; 90 | margin: 0; 91 | font: inherit; 92 | font-size: 100%; 93 | vertical-align: baseline; 94 | border: 0; 95 | } 96 | 97 | article, 98 | aside, 99 | details, 100 | figcaption, 101 | figure, 102 | footer, 103 | header, 104 | hgroup, 105 | menu, 106 | nav, 107 | section { 108 | display: block; 109 | } 110 | 111 | body { 112 | line-height: 1; 113 | } 114 | 115 | ol, 116 | ul { 117 | list-style: none; 118 | } 119 | 120 | blockquote, 121 | q { 122 | quotes: none; 123 | 124 | &::before, 125 | &::after { 126 | content: ''; 127 | content: none; 128 | } 129 | } 130 | 131 | sub, 132 | sup { 133 | position: relative; 134 | font-size: 75%; 135 | line-height: 0; 136 | vertical-align: baseline; 137 | } 138 | 139 | sup { 140 | top: -0.5em; 141 | } 142 | 143 | sub { 144 | bottom: -0.25em; 145 | } 146 | 147 | table { 148 | border-spacing: 0; 149 | border-collapse: collapse; 150 | } 151 | 152 | input, 153 | textarea, 154 | button { 155 | font-family: inherit; 156 | font-size: inherit; 157 | color: inherit; 158 | } 159 | 160 | select { 161 | text-overflow: ''; 162 | text-indent: 0.01px; 163 | appearance: none; 164 | border: 0; 165 | border-radius: 0; 166 | } 167 | 168 | select::-ms-expand { 169 | display: none; 170 | } 171 | 172 | code, 173 | pre { 174 | font-family: monospace; 175 | font-size: 1em; 176 | } 177 | 178 | /* 移动端添加 */ 179 | 180 | /* ::-webkit-scrollbar { 181 | display: none; 182 | } */ 183 | -------------------------------------------------------------------------------- /src/error-boundary.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | import { useRouteError } from 'react-router'; 3 | 4 | const ErrorBoundary: FC = () => { 5 | const routeError = useRouteError() as any; 6 | console.error(routeError); 7 | // 可以在这里根据不同的业务逻辑处理错误或者上报给日志服务 8 | return ( 9 |
19 |

20 | {routeError?.name || 'Error'}: 21 |

22 |

{routeError?.message || routeError?.error?.message}

23 |
31 |

Render Fail:

32 |
33 |           {routeError?.stack || routeError?.error?.stack}
34 |         
35 |
36 | 51 | 67 | 83 |
84 | ); 85 | }; 86 | 87 | export default ErrorBoundary; 88 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { inspectorServer } from '@react-dev-inspector/vite-plugin'; 2 | import react from '@vitejs/plugin-react-swc'; 3 | import { resolve } from 'node:path'; 4 | import process from 'node:process'; 5 | import { ConfigEnv, UserConfig, defineConfig, loadEnv } from 'vite'; 6 | import checker from 'vite-plugin-checker'; 7 | 8 | export default defineConfig(({ mode }: ConfigEnv): UserConfig => { 9 | // 获取`.env`环境配置文件 10 | const env = loadEnv(mode, process.cwd()); 11 | return { 12 | base: env.VITE_NODE_ENV === 'development' ? './' : '/', // 此配置仅为github pages部署用,请自行修改或删除(一般情况下直接移除就行) 13 | plugins: [ 14 | react(), 15 | /** 16 | * 点击页面元素,IDE直接打开对应代码插件(本项目配置的快捷键是:ctrl+alt+q,详见main.tsx) 17 | * @see https://github.com/zthxxx/react-dev-inspector 18 | */ 19 | inspectorServer(), 20 | // 在浏览器中直接看到上报的类型错误(更严格的类型校验) 21 | checker({ 22 | typescript: true, 23 | eslint: { 24 | useFlatConfig: true, 25 | lintCommand: 'eslint "./src/**/*.{ts,tsx}"', 26 | }, 27 | }), 28 | ], 29 | resolve: { 30 | alias: { 31 | '@': resolve(import.meta.dirname, 'src'), 32 | }, 33 | }, 34 | css: { 35 | preprocessorOptions: { 36 | scss: { 37 | // additionalData的内容会在每个scss文件的开头自动注入 38 | additionalData: `@use "@/styles/scss/index.scss" as *;`, // 引入全局scss变量、样式工具函数等 39 | }, 40 | }, 41 | }, 42 | // 反向代理解决跨域问题 43 | server: { 44 | // open: true,// 运行时自动打开浏览器 45 | host: '0.0.0.0', // 局域网别人也可访问 46 | port: Number(env.VITE_APP_PORT), //端口号 47 | proxy: { 48 | [env.VITE_API_BASE_URL]: { 49 | target: env.VITE_SERVER_URL, 50 | changeOrigin: true, 51 | rewrite: (path: string) => path.replace(new RegExp('^' + env.VITE_API_BASE_URL), ''), 52 | }, 53 | }, 54 | }, 55 | build: { 56 | target: 'esnext', // 最低 es2015/es6 57 | outDir: env.VITE_OUT_DIR || 'dist', 58 | chunkSizeWarningLimit: 2000, // 单个 chunk 文件的大小超过 2000kB 时发出警告(默认:超过500kb警告) 59 | rollupOptions: { 60 | // 分包 61 | output: { 62 | chunkFileNames: 'assets/js/[name]-[hash].js', // chunk包输出的文件夹名称 63 | entryFileNames: 'assets/js/[name]-[hash].js', // 入口文件输出的文件夹名称 64 | assetFileNames: 'assets/[ext]/[name]-[hash].[ext]', // 静态文件输出的文件夹名称 65 | // 手动分包,将第三方库拆分到单独的chunk包中(注意这些包名必须存在,否则打包会报错) 66 | manualChunks: { 67 | 'vendor-react': ['react', 'react-dom', 'react-router'], 68 | 'vendor-utils': [ 69 | 'axios', 70 | 'dayjs', 71 | 'immer', 72 | 'zustand', 73 | 'ahooks', 74 | 'classnames', 75 | 'es-toolkit', 76 | ], 77 | // 'vendor-ui':['antd'] 78 | }, 79 | }, 80 | }, 81 | }, 82 | // 预构建的依赖项,优化开发(该优化器仅在开发环境中使用) 83 | optimizeDeps: { 84 | include: [ 85 | 'react', 86 | 'react-dom', 87 | 'react-router', 88 | 'zustand', 89 | 'classnames', 90 | 'es-toolkit', 91 | 'axios', 92 | 'dayjs', 93 | 'immer', 94 | 'ahooks', 95 | ], 96 | }, 97 | }; 98 | }); 99 | -------------------------------------------------------------------------------- /src/hooks/use-namespace/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @see copy https://github.com/element-plus/element-plus/blob/dev/packages/hooks/use-namespace/index.ts 3 | */ 4 | 5 | export const DEFAULT_NAMESPACE = 'my'; 6 | 7 | const statePrefix = 'is-'; 8 | 9 | const _bem = ( 10 | namespace: string, 11 | block: string, 12 | blockSuffix: string, 13 | element: string, 14 | modifier: string, 15 | ) => { 16 | let cls = `${namespace}-${block}`; 17 | if (blockSuffix) { 18 | cls += `-${blockSuffix}`; 19 | } 20 | if (element) { 21 | cls += `__${element}`; 22 | } 23 | if (modifier) { 24 | cls += `--${modifier}`; 25 | } 26 | return cls; 27 | }; 28 | 29 | const is: { 30 | (name: string): string; 31 | (name: string, state: boolean | undefined): string; 32 | } = (name: string, ...args: [] | [boolean | undefined]) => { 33 | const state = args.length > 0 ? args[0] : true; 34 | return name && state ? `${statePrefix}${name}` : ''; 35 | }; 36 | 37 | const useNamespace = (block: string) => { 38 | const namespace = DEFAULT_NAMESPACE; 39 | const b = (blockSuffix = '') => _bem(namespace, block, blockSuffix, '', ''); 40 | const e = (element?: string) => (element ? _bem(namespace, block, '', element, '') : ''); 41 | const m = (modifier?: string) => (modifier ? _bem(namespace, block, '', '', modifier) : ''); 42 | const be = (blockSuffix?: string, element?: string) => 43 | blockSuffix && element ? _bem(namespace, block, blockSuffix, element, '') : ''; 44 | const em = (element?: string, modifier?: string) => 45 | element && modifier ? _bem(namespace, block, '', element, modifier) : ''; 46 | const bm = (blockSuffix?: string, modifier?: string) => 47 | blockSuffix && modifier ? _bem(namespace, block, blockSuffix, '', modifier) : ''; 48 | const bem = (blockSuffix?: string, element?: string, modifier?: string) => 49 | blockSuffix && element && modifier 50 | ? _bem(namespace, block, blockSuffix, element, modifier) 51 | : ''; 52 | 53 | // for css var 54 | // --el-xxx: value; 55 | const cssVar = (object: Record) => { 56 | const styles: Record = {}; 57 | for (const key in object) { 58 | if (object[key]) { 59 | styles[`--${namespace}-${key}`] = object[key]; 60 | } 61 | } 62 | return styles; 63 | }; 64 | // with block 65 | const cssVarBlock = (object: Record) => { 66 | const styles: Record = {}; 67 | for (const key in object) { 68 | if (object[key]) { 69 | styles[`--${namespace}-${block}-${key}`] = object[key]; 70 | } 71 | } 72 | return styles; 73 | }; 74 | 75 | const cssVarName = (name: string) => `--${namespace}-${name}`; 76 | const cssVarBlockName = (name: string) => `--${namespace}-${block}-${name}`; 77 | 78 | return { 79 | b, 80 | be, 81 | bem, 82 | bm, 83 | // css 84 | cssVar, 85 | cssVarBlock, 86 | cssVarBlockName, 87 | cssVarName, 88 | e, 89 | em, 90 | is, 91 | m, 92 | namespace, 93 | }; 94 | }; 95 | 96 | type UseNamespaceReturn = ReturnType; 97 | 98 | export type { UseNamespaceReturn }; 99 | export { useNamespace }; 100 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-ts-template", 3 | "description": "一套基于react19、ts、vite6的项目模板,帮助快速搭建react项目", 4 | "version": "1.0.0", 5 | "type": "module", 6 | "packageManager": "pnpm@9.15.1", 7 | "author": "huangmingfu <212149997@qq.com>", 8 | "license": "MIT", 9 | "homepage": "https://github.com/huangmingfu/react-ts-template", 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/huangmingfu/react-ts-template" 13 | }, 14 | "bugs": "https://github.com/huangmingfu/react-ts-template/issues", 15 | "keywords": [ 16 | "react19 ts template", 17 | "react template", 18 | "react", 19 | "template", 20 | "vite" 21 | ], 22 | "scripts": { 23 | "dev": "vite -m dev", 24 | "dev:test": "vite -m test", 25 | "dev:pro": "vite -m pro", 26 | "build:dev": "pnpm vite build -m dev", 27 | "build:test": "pnpm vite build -m test", 28 | "build:pro": "pnpm vite build -m pro", 29 | "preview:dev": "pnpm vite preview -m dev", 30 | "preview:test": "pnpm vite preview -m test", 31 | "preview:pro": "pnpm vite preview -m pro", 32 | "typecheck": "tsc --noEmit", 33 | "lint:eslint": "eslint --max-warnings 0 \"**/*.{ts,tsx,js,jsx,cjs,mjs}\" --fix", 34 | "lint:format": "prettier --write \"**/*.{ts,tsx,js,jsx,cjs,mjs,html,scss,css,json}\"", 35 | "lint:style": "stylelint \"**/*.{css,scss}\" --fix", 36 | "lint:all": "pnpm run lint:eslint && pnpm run lint:style && pnpm run lint:format", 37 | "push": "sh scripts/push.sh", 38 | "clean": "rm -rf node_modules dist dist-*", 39 | "deps:check": "pnpm outdated -r", 40 | "deps:update": "pnpm update --latest", 41 | "prepare": "husky install", 42 | "preinstall": "npx only-allow pnpm" 43 | }, 44 | "dependencies": { 45 | "ahooks": "^3.9.0", 46 | "axios": "^1.11.0", 47 | "classnames": "^2.5.1", 48 | "dayjs": "^1.11.13", 49 | "es-toolkit": "^1.39.8", 50 | "immer": "^10.1.1", 51 | "react": "^19.1.1", 52 | "react-dom": "^19.1.1", 53 | "react-router": "^7.7.1", 54 | "zustand": "^5.0.7" 55 | }, 56 | "devDependencies": { 57 | "@commitlint/cli": "^19.8.1", 58 | "@commitlint/config-conventional": "^19.8.1", 59 | "@commitlint/prompt-cli": "^19.8.1", 60 | "@eslint-react/eslint-plugin": "1.52.3", 61 | "@eslint/js": "^9.32.0", 62 | "@react-dev-inspector/vite-plugin": "^2.0.1", 63 | "@trivago/prettier-plugin-sort-imports": "^5.2.2", 64 | "@types/node": "^24.2.0", 65 | "@types/react": "^19.1.9", 66 | "@types/react-dom": "^19.1.7", 67 | "@vitejs/plugin-react-swc": "^3.11.0", 68 | "eslint": "^9.32.0", 69 | "eslint-plugin-react-hooks": "^5.2.0", 70 | "eslint-plugin-react-refresh": "^0.4.20", 71 | "globals": "^16.3.0", 72 | "husky": "^9.1.7", 73 | "lint-staged": "^16.1.4", 74 | "prettier": "^3.6.2", 75 | "react-dev-inspector": "^2.0.1", 76 | "sass": "^1.90.0", 77 | "stylelint": "^16.23.0", 78 | "stylelint-config-recess-order": "^7.1.0", 79 | "stylelint-config-standard": "^39.0.0", 80 | "stylelint-config-standard-scss": "^15.0.1", 81 | "stylelint-order": "^7.0.0", 82 | "typescript": "~5.9.2", 83 | "typescript-eslint": "^8.39.0", 84 | "vite": "^7.0.6", 85 | "vite-plugin-checker": "^0.10.2" 86 | }, 87 | "engines": { 88 | "node": "^20.0.0 || >=22.0.0", 89 | "pnpm": ">=9" 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **中文** | [English](./README.en-US.md) 2 | 3 | 6 | 7 |

React-Ts-Template

8 | 9 |

10 | 基于 React 19、TypeScript 和 Vite 7 构建的现代化前端项目模板 11 |

12 | 13 |

14 | 15 | GitHub stars 16 | 17 | 18 | GitHub issues 19 | 20 | 21 | GitHub 22 | 23 | 24 | GitHub forks 25 | 26 |

27 | 28 | > 随着 `create-react-app` 脚手架停止维护,开发者需要一个现代化、高效且开箱即用的 React 项目模板。**React-Ts-Template** 应运而生!这是一个基于最新的 **React 19、TypeScript 和 Vite 7** 打造的项目模板,旨在帮助你极速启动项目,节省大量重复的配置时间。 29 | 30 | ## 🌟 为什么选择 React-Ts-Template? 31 | 32 | - ⚡ **极速开发体验** - 基于 Vite 7 构建,冷启动和热更新速度极快 33 | - 🚀 **前沿技术栈** - React 19、TypeScript、Zustand、React-Router v7 等最新技术 34 | - 📦 **开箱即用** - 集成路由、状态管理、请求封装、代码规范等完整解决方案 35 | - 🛡️ **类型安全** - 完整的 TypeScript 类型定义,保障代码质量 36 | - 🎨 **现代 CSS** - SCSS 预编译 + BEM 命名规范,样式管理更规范 37 | - 🔧 **工程化规范** - ESLint、Prettier、Stylelint、Commitlint 等代码质量保障 38 | 39 | ## 🚀 快速开始 40 | 41 | ```bash 42 | # 克隆项目 43 | git clone https://github.com/huangmingfu/react-ts-template.git 44 | 45 | # 进入项目目录 46 | cd react-ts-template 47 | 48 | # 安装依赖 49 | pnpm install 50 | 51 | # 启动开发服务器 52 | pnpm dev 53 | 54 | # 构建生产环境 55 | pnpm build:pro 56 | ``` 57 | 58 | ## 🧩 核心功能 59 | 60 | - **路由懒加载**:封装实现了路由懒加载,提升页面切换性能,减少初始加载时间。(详见`router`) 61 | - **路由守卫**:封装了灵活的路由守卫管理,确保用户访问权限控制,增强应用的安全性。(详见`router`) 62 | - **全局状态管理**:提供了 Zustand 全局状态管理示例代码,简化跨组件状态共享,提升开发效率。(详见`store`) 63 | - **Axios 请求封装**:对 Axios 进行封装,统一处理 HTTP 请求和响应,简化与后端接口的交互流程。(详见[service](./src/services)) 64 | - **工具函数、hooks**:提供了一些方便实用的工具函数和hooks。(详见[utils](./src/utils)、[hooks](./src/hooks)) 65 | - **react-dev-inspector集成**:点击页面元素,IDE直接打开对应代码插件,方便开发者调试代码,提高开发效率。(详见[vite.config.ts](./vite.config.ts)) 66 | - **import顺序自动美化排序**:集成了 prettier-plugin-sort-imports 插件,可以自动美化 import 顺序,提高代码的可读性和可维护性。 67 | - **其他**:提供一些方便根据环境运行、打包的命令;配置了分包策略;本地反向代理解决跨域;还有详细的`保姆级注释`等等。 68 | 69 | ## 🛠 技术栈选型 70 | 71 | | 类别 | 技术 | 描述 | 72 | | --- | --- | --- | 73 | | **核心框架** | React 19 | 最新版 React,更高性能和更流畅的用户体验 | 74 | | **路由管理** | React-Router v7 | 支持路由懒加载,优化页面切换性能 | 75 | | **状态管理** | Zustand | 轻量级状态管理库,简单易用 | 76 | | **样式方案** | SCSS + BEM | 模块化样式管理,结构清晰(可自行选择使用css module `xxx.module.scss`) | 77 | | **HTTP库** | Axios | 统一处理 HTTP 请求和响应 | 78 | | **工具库** | ahooks + es-toolkit | 丰富的 React Hooks 和 JS 工具函数 | 79 | | **构建工具** | Vite 7 | 极速的构建工具,提升开发体验 | 80 | | **类型检查** | TypeScript | 强大的类型系统,保障代码质量 | 81 | | **代码规范** | ESLint + Prettier + Stylelint | 统一代码风格,提高代码质量 | 82 | 83 | ## 📁 项目结构 84 | 85 | ``` 86 | ├── .vscode # VSCode 配置 87 | ├── .husky # Git Hooks 88 | ├── .github # GitHub 配置 89 | ├── public # 静态资源 90 | ├── src # 源代码 91 | │ ├── assets # 静态资源 92 | │ ├── components # 公共组件 93 | │ ├── hooks # 自定义 Hooks 94 | │ ├── views # 页面组件 95 | │ ├── router # 路由配置 96 | │ ├── services # 接口封装 97 | │ ├── store # 状态管理 98 | │ ├── styles # 样式文件 99 | │ ├── types # 类型定义 100 | │ ├── utils # 工具函数 101 | │ ├── app.tsx # 根组件 102 | │ └── main.tsx # 入口文件 103 | ├── .env # 环境变量 104 | └── ... # 配置文件 105 | ``` 106 | 107 | ## 🎯 特色亮点 108 | 109 | ### 🚀 高性能构建 110 | 111 | - 基于 Vite 7 构建,冷启动时间快至毫秒级 112 | - 支持代码分割和动态导入,优化首屏加载速度 113 | 114 | ### 🛡️ 完善的类型系统 115 | 116 | - 完整的 TypeScript 类型定义 117 | - 严格的 tsconfig 配置,开启所有严格检查选项 118 | - 统一的类型管理,便于维护和协作 119 | 120 | ### 🎨 规范化的代码风格 121 | 122 | - 集成 ESLint、Prettier、Stylelint 三大代码规范工具 123 | - 统一的 commit message 规范(Commitlint + Husky) 124 | - 自动格式化代码,保证团队代码风格一致性 125 | 126 | ### 🔧 强大的开发工具链 127 | 128 | - react-dev-inspector 集成,点击页面元素直接跳转到源码 129 | - import 顺序自动排序,提高代码可读性 130 | - 多环境配置(dev/test/pro),满足不同部署需求 131 | 132 | ## 📦 关于路由缓存 keep-alive 133 | 134 | > React 官方暂时没有实现 vue \ 类似的功能。React 官方出于两点考虑拒绝添加这个功能,具体可以自行搜索查阅。为了达到状态保存的效果,官方推荐以下两种手动保存状态的方式: 135 | 136 | - 将需要保存状态组件的 state 提升至父组件中保存。 137 | - 使用 CSS visible 属性来控制需要保存状态组件的渲染,而不是使用 if/else,以避免 React 将其卸载。 138 | 139 | > 不过也有一些相关库实现了这个功能,如:`react-router-cache-route、react-activation、keepalive-for-react` 等等,如果项目中需要状态缓存处理的数据量较小,那最好还是按照 React 官方的建议,手动解决状态缓存问题。 140 | 141 | ## ⚠️ 注意事项 142 | 143 | > 1. 目前有一些 ui 库或其他第三方库还尚未支持 `react19`,注意甄别安装使用。 144 | > 2. 本项目并未使用 19 版本的相关特性,如需要可以直接使用如下命令降级到 18 版本。 145 | 146 | ```bash 147 | pnpm install react@18.3.1 react-dom@18.3.1 148 | ``` 149 | 150 | ## 🤝 贡献 151 | 152 | 欢迎任何形式的贡献!如果你觉得这个项目有帮助,请给个 Star ⭐ 支持一下! 153 | 154 | 1. Fork 本项目 155 | 2. 创建你的特性分支 (`git checkout -b feature/AmazingFeature`) 156 | 3. 提交你的更改 (`git commit -m 'Add some amazing feature'`) 157 | 4. 推送到分支 (`git push origin feature/AmazingFeature`) 158 | 5. 开启一个 Pull Request 159 | 160 | ## 📄 许可证 161 | 162 | 本项目采用 MIT 许可证,详情请查看 [LICENSE](./LICENSE) 文件。 163 | 164 | --- 165 | 166 |

如果你喜欢这个项目,请不要吝啬你的 Star ⭐

167 | 168 |

169 | 170 | GitHub stars 171 | 172 |

173 | -------------------------------------------------------------------------------- /README.en-US.md: -------------------------------------------------------------------------------- 1 | GitHub Repository: [React-Ts-Template](https://github.com/huangmingfu/react-ts-template) 2 | 3 |

React-Ts-Template

4 | 5 |

6 | Modern frontend project template built with React 19, TypeScript, and Vite 7 7 |

8 | 9 |

10 | 11 | GitHub stars 12 | 13 | 14 | GitHub issues 15 | 16 | 17 | GitHub 18 | 19 | 20 | GitHub forks 21 | 22 |

23 | 24 | > With `create-react-app` scaffold no longer being maintained, developers need a modern, efficient, and out-of-the-box React project template. **React-Ts-Template** was born for this! This is a project template built on the latest **React 19, TypeScript, and Vite 7**, designed to help you rapidly start your project and save considerable configuration time. 25 | 26 | ## 🌟 Why Choose React-Ts-Template? 27 | 28 | - ⚡ **Lightning Fast HMR** - Built on Vite 7 for extremely fast cold start and hot module replacement 29 | - 🚀 **Cutting-edge Tech Stack** - React 19, TypeScript, Zustand, React-Router v7 and more latest technologies 30 | - 📦 **Out-of-the-box** - Integrated routing, state management, request encapsulation, code standards and complete solutions 31 | - 🛡️ **Type Safety** - Complete TypeScript type definitions for code quality assurance 32 | - 🎨 **Modern CSS** - SCSS preprocessing + BEM naming convention for standardized styling 33 | - 🔧 **Engineering Standards** - ESLint, Prettier, Stylelint, Commitlint and other code quality assurance tools 34 | 35 | ## 🚀 Quick Start 36 | 37 | ```bash 38 | # Clone the project 39 | git clone https://github.com/huangmingfu/react-ts-template.git 40 | 41 | # Navigate to project directory 42 | cd react-ts-template 43 | 44 | # Install dependencies 45 | pnpm install 46 | 47 | # Start development server 48 | pnpm dev 49 | 50 | # Build for production 51 | pnpm build:pro 52 | ``` 53 | 54 | ## 🧩 Core Features 55 | 56 | - **Route Lazy Loading**: Implemented route lazy loading to improve page switching performance and reduce initial loading time. (See `router`) 57 | - **Route Guards**: Encapsulated flexible route guard management to ensure user access control and enhance application security. (See `router`) 58 | - **Global State Management**: Provides Zustand global state management example code, simplifying cross-component state sharing and improving development efficiency. (See [store](./src/store)) 59 | - **Axios Request Encapsulation**: Encapsulated Axios to uniformly handle HTTP requests and responses, simplifying interaction with backend interfaces. (See [services](./src/services)) 60 | - **Utility Functions & Hooks**: Provides some convenient and practical utility functions and hooks. (See [utils](./src/utils), [hooks](./src/hooks)) 61 | - **react-dev-inspector Integration**: Click on page elements to open corresponding code in IDE, facilitating code debugging and improving development efficiency. (See [vite.config.ts](./vite.config.ts)) 62 | - **Automatic Import Order Beautification**: Integrated prettier-plugin-sort-imports plugin to automatically beautify import order, enhancing code readability and maintainability. 63 | - **Others**: Provides commands for convenient environment-based running and building; configured code splitting strategy; local reverse proxy for CORS; and detailed `nanny-level comments`, etc. 64 | 65 | ## 🛠 Technology Stack 66 | 67 | | Category | Technology | Description | 68 | | --- | --- | --- | 69 | | **Core Framework** | React 19 | Latest React version for higher performance and smoother user experience | 70 | | **Routing** | React-Router v7 | Supports route lazy loading, optimizes page transition performance | 71 | | **State Management** | Zustand | Lightweight state management library, simple and easy to use | 72 | | **Styling** | SCSS + BEM | Modular style management with clear structure (Optional: Use CSS modules xxx.module.scss) | 73 | | **HTTP Client** | Axios | Unified handling of HTTP requests and responses | 74 | | **Utility Libraries** | ahooks + es-toolkit | Rich React Hooks and JS utility functions | 75 | | **Build Tool** | Vite 7 | Lightning-fast build tool that enhances development experience | 76 | | **Type Checking** | TypeScript | Powerful type system for code quality assurance | 77 | | **Code Standards** | ESLint + Prettier + Stylelint | Unified code style for improved code quality | 78 | 79 | ## 📁 Project Structure 80 | 81 | ``` 82 | ├── .vscode # VSCode configuration 83 | ├── .husky # Git Hooks 84 | ├── .github # GitHub configuration 85 | ├── public # Static assets 86 | ├── src # Source code 87 | │ ├── assets # Static resources 88 | │ ├── components # Reusable components 89 | │ ├── hooks # Custom Hooks 90 | │ ├── views # Page components 91 | │ ├── router # Routing configuration 92 | │ ├── services # API encapsulation 93 | │ ├── store # State management 94 | │ ├── styles # Style files 95 | │ ├── types # Type definitions 96 | │ ├── utils # Utility functions 97 | │ ├── app.tsx # Root component 98 | │ └── main.tsx # Entry file 99 | ├── .env # Environment variables 100 | └── ... # Configuration files 101 | ``` 102 | 103 | ## 🎯 Key Highlights 104 | 105 | ### 🚀 High Performance Building 106 | 107 | - Built on Vite 7 with millisecond-level cold start time 108 | - Supports code splitting and dynamic imports to optimize first screen loading speed 109 | 110 | ### 🛡️ Complete Type System 111 | 112 | - Complete TypeScript type definitions 113 | - Strict tsconfig configuration with all strict checking options enabled 114 | - Unified type management for easy maintenance and collaboration 115 | 116 | ### 🎨 Standardized Code Style 117 | 118 | - Integrated ESLint, Prettier, Stylelint - the three major code standard tools 119 | - Unified commit message standards (Commitlint + Husky) 120 | - Automatic code formatting to ensure team code style consistency 121 | 122 | ### 🔧 Powerful Development Toolchain 123 | 124 | - react-dev-inspector integration - click page elements to jump directly to source code 125 | - Automatic import order sorting for improved code readability 126 | - Multi-environment configuration (dev/test/pro) to meet different deployment requirements 127 | 128 | ## 📦 About Route Caching (keep-alive) 129 | 130 | > React officially hasn't implemented functionality similar to Vue's \. React team rejected adding this feature based on two considerations, which you can search and read about. To achieve state preservation, the official team recommends these two manual state preservation methods: 131 | 132 | - Lift the state of components needing state preservation to their parent components. 133 | - Use CSS visible property to control the rendering of components needing state preservation, instead of using if/else, to prevent React from unmounting them. 134 | 135 | > However, there are some libraries that implement this functionality, such as react-router-cache-route, react-activation, keepalive-for-react, etc. If your project needs to handle a small amount of state caching data, it's better to follow React's official recommendations and solve state caching issues manually. 136 | 137 | ## ⚠️ Notes 138 | 139 | > 1. Currently, some UI libraries or third-party libraries do not yet support React 19. Please verify and choose appropriate versions for installation and usage. 140 | > 2. This project does not use any features specific to version 19. If needed, you can directly downgrade to version 18 using the following command. 141 | 142 | ```bash 143 | pnpm install react@18.3.1 react-dom@18.3.1 144 | ``` 145 | 146 | ## 🤝 Contributing 147 | 148 | Any contributions are welcome! If you find this project helpful, please give it a Star ⭐ for support! 149 | 150 | 1. Fork the project 151 | 2. Create your feature branch (`git checkout -b feature/AmazingFeature`) 152 | 3. Commit your changes (`git commit -m 'Add some amazing feature'`) 153 | 4. Push to the branch (`git push origin feature/AmazingFeature`) 154 | 5. Open a Pull Request 155 | 156 | ## 📄 License 157 | 158 | This project is licensed under the MIT License - see the [LICENSE](./LICENSE) file for details. 159 | 160 | --- 161 | 162 |

If you like this project, don't be stingy with your Star ⭐

163 | 164 |

165 | 166 | GitHub stars 167 | 168 |

169 | --------------------------------------------------------------------------------