├── .env ├── .env.development ├── .env.production ├── .eslintrc.json ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .lintstagedrc.js ├── .npmrc ├── .prettierignore ├── .prettierrc ├── CHANGELOG.md ├── Dockerfile ├── README.md ├── commitlint.config.js ├── next.config.js ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── public ├── 2k.svg ├── bg.jpg └── favicon.ico ├── src ├── apis │ └── index.ts ├── app │ ├── [locale] │ │ ├── (empty-layout) │ │ │ ├── layout.tsx │ │ │ ├── login │ │ │ │ └── page.tsx │ │ │ ├── not-auth │ │ │ │ └── page.tsx │ │ │ └── not-server │ │ │ │ └── page.tsx │ │ └── (main-layout) │ │ │ ├── dashboard │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ ├── list │ │ │ ├── ahook-table │ │ │ │ └── page.tsx │ │ │ ├── page.tsx │ │ │ ├── pro-form │ │ │ │ └── page.tsx │ │ │ └── pro-table │ │ │ │ ├── (components) │ │ │ │ └── ModalForm.tsx │ │ │ │ └── page.tsx │ │ │ └── welcome │ │ │ └── page.tsx │ ├── globals.css │ ├── layout.tsx │ ├── loading.tsx │ ├── not-found.tsx │ └── page.tsx ├── components │ ├── ChangeLanguage.tsx │ ├── Link.tsx │ ├── MockComponent.tsx │ ├── Navigation.tsx │ ├── NoSSR.tsx │ ├── index.ts │ └── layouts │ │ ├── EmptyLayout.tsx │ │ ├── MainLayout.tsx │ │ ├── RootLayout.tsx │ │ └── useMainLayoutProps.tsx ├── hooks │ └── useEvent.ts ├── i18n │ ├── en.ts │ ├── locales │ │ ├── en │ │ │ ├── login.ts │ │ │ └── menu.ts │ │ └── zh │ │ │ ├── login.ts │ │ │ └── menu.ts │ └── zh.ts ├── lib │ ├── antd-registry.tsx │ ├── create-ctx.tsx │ ├── events.ts │ ├── language.ts │ └── request.ts ├── mock │ ├── index.ts │ └── userInfo.ts ├── services │ └── userService.ts ├── static │ ├── emits.ts │ ├── encrypt.ts │ ├── locales.ts │ └── staticRouter.ts ├── store │ ├── useAccessStore.ts │ ├── useAuthStore.ts │ └── useSettingStore.ts └── types │ ├── Layout.ts │ └── index.ts ├── tailwind.config.ts └── tsconfig.json /.env: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_PORT=8000 2 | NEXT_PUBLIC_BASE_URL='/' 3 | NEXT_PUBLIC_MOCK=true 4 | -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | PROXY='https://randomuser.me/api/:slug*' 2 | -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_BASE_URL='https://randomuser.me/' 2 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | 38 | .idea 39 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx --no -- commitlint --edit "$1" 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npm exec lint-staged 5 | git add . 6 | -------------------------------------------------------------------------------- /.lintstagedrc.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | 4 | //如果你想使用 next lint 和 lint-staged 在暂存的 git 文件上运行 linter, 5 | // 则必须将以下内容添加到项目根目录中的 .lintstagedrc.js 文件中,以便指定 --file 标志的使用 6 | const buildEslintCommand = (filenames) => 7 | `next lint --fix --file ${filenames 8 | .map((f) => path.relative(process.cwd(), f)) 9 | .join(' --file ')}`; 10 | 11 | module.exports = { 12 | '*.{js,jsx,ts,tsx}': [buildEslintCommand], 13 | }; 14 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | # pnpm 配置 2 | shamefully-hoist=true 3 | auto-install-peers=true 4 | strict-peer-dependencies=false 5 | 6 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .yarn 2 | .next 3 | dist 4 | node_modules 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true, 6 | "extends": [ 7 | "next", 8 | "prettier" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.1.0 (2024-01-25) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * 菜单选择不高亮问题修改 ([6e0e02d](https://github.com/zhaoth/React-Next-Admin/commit/6e0e02db4c9f481a3b85ba10e006f7608b067a2e)) 7 | 8 | 9 | ### Features 10 | 11 | * add Lint Staged ([2796c63](https://github.com/zhaoth/React-Next-Admin/commit/2796c63a9d72cb9d96f33a308f119b5d2de0774b)) 12 | * ant design全局国家化添加 ([e8ae1fb](https://github.com/zhaoth/React-Next-Admin/commit/e8ae1fb0d9779569522e71bc4b550d76a58e9551)) 13 | * 修改登录背景图片 ([ff00b4e](https://github.com/zhaoth/React-Next-Admin/commit/ff00b4ed07badd544997910146f0f93f932fc44b)) 14 | * 修改语言包json 文件为 ts 文件 ([a0c0fa1](https://github.com/zhaoth/React-Next-Admin/commit/a0c0fa18f1026207c442391a089ee2af3b701fd5)) 15 | * 基于 next-intl export模式下静态路由国际化 ([1eaca1b](https://github.com/zhaoth/React-Next-Admin/commit/1eaca1b6d3cb95badc71959d223f57f2b5209480)) 16 | * 增加ahooks插件 ([5fbceb7](https://github.com/zhaoth/React-Next-Admin/commit/5fbceb7490ae70a813e6477907af25f66586ac53)) 17 | * 增加procomponent 组件 ([6c99db0](https://github.com/zhaoth/React-Next-Admin/commit/6c99db08ff16f78fb2fde7a2ab4570040be1a179)) 18 | * 增加ProComponent组件 ([bf2d3b5](https://github.com/zhaoth/React-Next-Admin/commit/bf2d3b595c0d1c71020902ddd546de8ceefe8d59)) 19 | * 增加proTable demo ([c660898](https://github.com/zhaoth/React-Next-Admin/commit/c660898288e3c893444c5be07ef503b2dd974f3c)) 20 | * 增加异步请求封装 ([aaf8224](https://github.com/zhaoth/React-Next-Admin/commit/aaf822452ca812aa813177ddb62dd75a433908d2)) 21 | * 增加菜单和权限控制 ([5ddbf12](https://github.com/zhaoth/React-Next-Admin/commit/5ddbf128c35be9108e7f646d12639073493bcc65)) 22 | * 工程初始化 ([469b627](https://github.com/zhaoth/React-Next-Admin/commit/469b627ce51feef9e80839ab7ce8ff47a6a60f6a)) 23 | * 添加 zustand 状态管理组件 ([6024bf2](https://github.com/zhaoth/React-Next-Admin/commit/6024bf2abc6bbb4e9d82d9261494d002fd5c3a74)) 24 | * 添加语言切换 ([2f03b25](https://github.com/zhaoth/React-Next-Admin/commit/2f03b25d00bc68ce50b41c42ce8fb49e11bffe16)) 25 | * 添加遗漏文件 ([996955a](https://github.com/zhaoth/React-Next-Admin/commit/996955aa3a1758cf79f82b8b8b88d31dd886401a)) 26 | * 添加静态路由声明文件 ([5b12c69](https://github.com/zhaoth/React-Next-Admin/commit/5b12c6934aca7c856d4ac9cf37df464fce3dda40)) 27 | * 菜单选择添加高亮 ([665758d](https://github.com/zhaoth/React-Next-Admin/commit/665758d0abc931efca5c1d58182df4a9d2ce2b2e)) 28 | * 设置异常处理了页 ([b4cb211](https://github.com/zhaoth/React-Next-Admin/commit/b4cb211d4b4ebb5fcf859efed385d29676e3fd0d)) 29 | * 设置重定向地址 和 修改线上 api 地址 ([3e8fc21](https://github.com/zhaoth/React-Next-Admin/commit/3e8fc21ad50054de7e2999f6ce4bda29dab54e3b)) 30 | * 设置静态部署模式 ([22924fa](https://github.com/zhaoth/React-Next-Admin/commit/22924fa0ce4753e8067dd9a0667564d253c9312a)) 31 | * 静态部署模式下 需要用navigation进行跳转 ([a524c6b](https://github.com/zhaoth/React-Next-Admin/commit/a524c6bf77504ed0b8aca6bb0158f8af5c0ee10e)) 32 | 33 | 34 | ### Performance Improvements 35 | 36 | * typescript 类型修改 ([afd0f16](https://github.com/zhaoth/React-Next-Admin/commit/afd0f16ab03b2dbc222cd34e144f5a068ee44607)) 37 | * 添加layout 布局组件 ([70229eb](https://github.com/zhaoth/React-Next-Admin/commit/70229eb2d443ba33c93ee4302b31fc12fcd7ac76)) 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Install dependencies only when needed 2 | FROM node:alpine AS deps 3 | # Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. 4 | RUN apk add --no-cache libc6-compat 5 | WORKDIR /app 6 | COPY package.json package-lock.json ./ 7 | RUN yarn install --frozen-lockfile --registry=https://registry.npm.taobao.org 8 | 9 | # Rebuild the source code only when needed 10 | FROM node:alpine AS builder 11 | WORKDIR /app 12 | COPY . . 13 | COPY --from=deps /app/node_modules ./node_modules 14 | RUN yarn build && yarn install --production --ignore-scripts --prefer-offline --registry=https://registry.npm.taobao.org 15 | 16 | # Production image, copy all the files and run next 17 | FROM node:alpine AS runner 18 | WORKDIR /app 19 | 20 | ENV NODE_ENV production 21 | 22 | RUN addgroup -g 1001 -S nodejs 23 | RUN adduser -S nextjs -u 1001 24 | 25 | # You only need to copy next.config.js if you are NOT using the default configuration 26 | # COPY --from=builder /app/next.config.js ./ 27 | COPY --from=builder /app/public ./public 28 | COPY --from=builder --chown=nextjs:nodejs /app/.next ./.next 29 | COPY --from=builder /app/node_modules ./node_modules 30 | COPY --from=builder /app/package.json ./package.json 31 | 32 | USER nextjs 33 | 34 | EXPOSE 4000 35 | 36 | ENV PORT 4000 37 | 38 | # Next.js collects completely anonymous telemetry data about general usage. 39 | # Learn more here: https://nextjs.org/telemetry 40 | # Uncomment the following line in case you want to disable telemetry. 41 | # ENV NEXT_TELEMETRY_DISABLED 1 42 | 43 | CMD ["node_modules/.bin/next", "start"] 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 这是一个基于 React18、NextJS14、 Ant-Design、TypeScript 的前台解决方案,目标是为开发中大型项目提供开箱即用的解决方案 2 | 3 | ## 项目 4 | 5 | ### 安装包 6 | 7 | ```bash 8 | pnpm install 9 | ``` 10 | 11 | ### 运行 12 | 13 | ```bash 14 | npm run dev 15 | # or 16 | yarn dev 17 | ``` 18 | 19 | 输入 [http://localhost:3000](http://localhost:3000) 可在浏览器中显示结果 20 | 21 | ## 环境要求 22 | 23 | ```bash 24 | "node": ">=18.17.0" 25 | ``` 26 | 27 | ## 基础组件 28 | 29 | - [NextJS](https://nextjs.org/docs)是一个用于构建全栈 Web 应用程序的 React 框架 30 | - [Ant Design](https://ant-design.antgroup.com/components/overview-cn?from=msidevs.net) UI组件库 31 | - [Tailwindcss](https://www.tailwindcss.cn/docs/installation) 原子化 css 组件库(可选择使用,但是强烈推荐使用) 32 | - [ZUSTAND](https://awesomedevin.github.io/zustand-vue/docs/introduce/start/zustand) 状态管理 33 | - [ahooks](https://ahooks.js.org/zh-CN)一套高质量可靠的 React Hooks 库 34 | 35 | ## 线上地址 36 | 37 | https://react-next-admin.pages.dev 38 | 39 | ## 项目搭建 40 | 41 | https://juejin.cn/post/7327107254603268123 42 | 43 | ## svg 图标 44 | 45 | next/image 组件直接支持 svg 直接引用不用二次封装 46 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | // build: 影响构建系统或外部依赖项的更改(示例范围:gulp、broccoli、npm) 2 | // ci: 更改我们的 CI 配置文件和脚本(示例范围:Travis、Circle、BrowserStack、SauceLabs) 3 | // docs: 文档修改 4 | // feat: 一个新的功能 5 | // fix: 一个 bug 修复 6 | // perf: 提升性能的代码修改 7 | // refactor: 既不修复错误也不添加功能的代码更改 8 | // style: 不影响代码含义的更改(空格、格式、缺少分号等) 9 | // test: 添加缺失的测试或更正现有测试 10 | 11 | module.exports = { 12 | extends: ['@commitlint/config-conventional'], 13 | rules: { 14 | 'body-leading-blank': [1, 'always'], 15 | 'body-max-line-length': [2, 'always', 100], 16 | 'footer-leading-blank': [1, 'always'], 17 | 'footer-max-line-length': [2, 'always', 100], 18 | 'header-max-length': [2, 'always', 100], 19 | 'scope-case': [2, 'always', 'lower-case'], 20 | 'subject-case': [ 21 | 2, 22 | 'never', 23 | ['sentence-case', 'start-case', 'pascal-case', 'upper-case'], 24 | ], 25 | 'subject-empty': [2, 'never'], 26 | 'subject-full-stop': [2, 'never', '.'], 27 | 'type-case': [2, 'always', 'lower-case'], 28 | 'type-empty': [2, 'never'], 29 | 'type-enum': [ 30 | 2, 31 | 'always', 32 | [ 33 | 'build', 34 | 'chore', 35 | 'ci', 36 | 'docs', 37 | 'feat', 38 | 'fix', 39 | 'perf', 40 | 'refactor', 41 | 'revert', 42 | 'style', 43 | 'test', 44 | 'translation', 45 | 'security', 46 | 'changeset', 47 | ], 48 | ], 49 | }, 50 | }; 51 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | // 判断环境 3 | const isProd = ['production'].includes(process.env.NODE_ENV); 4 | // 转发 5 | const rewrites = () => { 6 | if (!isProd) { 7 | return [ 8 | { 9 | source: '/api/:slug*', 10 | destination: process.env.PROXY, 11 | }, 12 | ]; 13 | } else { 14 | return []; 15 | } 16 | }; 17 | const nextConfig = { 18 | rewrites, 19 | output: 'export', 20 | reactStrictMode: false, 21 | }; 22 | 23 | module.exports = nextConfig; 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-next-admin", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev -p 3000", 7 | "build": "next build", 8 | "start": "next start -p 3100", 9 | "lint": "next lint", 10 | "prettier": "prettier --write .", 11 | "prepare": "husky install", 12 | "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0", 13 | "preinstall": "npx only-allow pnpm" 14 | }, 15 | "dependencies": { 16 | "-": "^0.0.1", 17 | "@ant-design/nextjs-registry": "^1.0.0", 18 | "@ant-design/pro-components": "^2.6.49", 19 | "@siyuan0215/easier-axios-dsl": "^1.1.11", 20 | "@types/lodash": "^4.14.202", 21 | "ahooks": "^3.7.10", 22 | "antd": "^5.14.0", 23 | "axios": "^0.24.0", 24 | "crypto-js": "^4.2.0", 25 | "lodash": "^4.17.21", 26 | "mitt": "^3.0.1", 27 | "mockjs": "^1.1.0", 28 | "next": "^14.1.0", 29 | "next-intl": "^3.5.4", 30 | "react": "^18.2.0", 31 | "react-dom": "^18.2.0", 32 | "zod": "^3.22.4", 33 | "zustand": "^4.5.0" 34 | }, 35 | "devDependencies": { 36 | "@commitlint/cli": "^18.6.0", 37 | "@commitlint/config-conventional": "^18.6.0", 38 | "@types/crypto-js": "^4.2.2", 39 | "@types/mockjs": "^1.0.10", 40 | "@types/node": "^20.11.16", 41 | "@types/react": "^18.2.52", 42 | "@types/react-dom": "^18.2.18", 43 | "autoprefixer": "^10.4.17", 44 | "conventional-changelog": "^5.1.0", 45 | "conventional-changelog-cli": "^4.1.0", 46 | "eslint": "^8.56.0", 47 | "eslint-config-next": "14.1.0", 48 | "eslint-config-prettier": "^9.1.0", 49 | "husky": "^8.0.3", 50 | "lint-staged": "^15.2.1", 51 | "postcss": "^8.4.33", 52 | "prettier": "^3.2.4", 53 | "tailwindcss": "^3.4.1", 54 | "typescript": "^5.3.3" 55 | }, 56 | "engines": { 57 | "node": ">=18.17.0" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /public/2k.svg: -------------------------------------------------------------------------------- 1 | 2 | 2K 3 | 4 | 5 | -------------------------------------------------------------------------------- /public/bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhaoth/React-Next-Admin/c5e40cf3147b415fc7bc969478ca5067c43939fb/public/bg.jpg -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhaoth/React-Next-Admin/c5e40cf3147b415fc7bc969478ca5067c43939fb/public/favicon.ico -------------------------------------------------------------------------------- /src/apis/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | import { generatorAPIS } from '@/lib/request'; 3 | 4 | /** 5 | * '[POST|GET|PUT|DELETE|...] [url] [d(data)(.(f|formData)):|q(query):|path:][keys with `,`]' 6 | * 7 | * d|data => data for POST and PUT 8 | * - if data in request is array, you can use `[(d|data)]`; 9 | * - if you want to pass all params to backend, you can use `(d|data):*`; 10 | * - if you want to pass FormData to backend, you can use `(d|data).(f|formData):`; 11 | * 12 | * q|query => query for GET and DELETE; 13 | * 14 | * path => dynamic parameters in url, like: vehicle/tips/vehicleBaseInfo/{vin}; 15 | * 16 | * eg.: 17 | * 18 | * import APIS from '@/api/XXX'; 19 | * 20 | * APIS.testRequestUrlMethod(majorParams: Record, otherParams?: Record) 21 | * 22 | * If `majorParams` is array, and at the same time, you have to pass other params, you should use second param `otherParams`. 23 | * 24 | * POST: 25 | * - `POST tipscase/save d:*`; 26 | * equal: (params: unknown) => api.post({ url: baseUrl + 'tipscase/save', params }, true) 27 | * 28 | * - `POST static-files d:sourceType,systemType,fileName,file,remark`; 29 | * equal: (types: string[]) => api.post>({ url: baseUrl + 'static-files', params: types }) 30 | * 31 | * - `POST tipscase/save q:a,b,c`; => POST case-dict/search-types?a=[value of otherParams[a]] 32 | * equal: (params: unknown) => api.post({ url: baseUrl + 'tipscase/save', params }) 33 | * 34 | * - `POST case-dict/search-types [d] q:a` => POST case-dict/search-types?a=[value of otherParams[a]] and taking majorParams as data 35 | * equal: (types: string[]) => api.post>({ url: baseUrl + 'case-dict/search-types' + '?=languageType=CN', params: types }) 36 | * 37 | * ! What final `data` is depends on the keys of `d:[keys]` 38 | * 39 | * GET: 40 | * - `GET tipscase/getInitInfo q:symptomId` => GET tipscase/getInitInfo?symptomId=[value of majorParams[symptomId]] 41 | * equal: (symptomId: string) => api.get({ url: baseUrl + 'tipscase/getInitInfo' + symptomId }) 42 | * 43 | * - `GET tipscase/get/{id} path:id` => GET tipscase/get/[value of majorParams[id]] 44 | * equal: (id: string) => api.get({ url: baseUrl + 'tipscase/get/' + id }) 45 | * */ 46 | 47 | enum apis { 48 | getTableList = 'GET api query:results,page,size', 49 | getUsers = 'POST user/login' 50 | } 51 | 52 | export default generatorAPIS(apis); 53 | -------------------------------------------------------------------------------- /src/app/[locale]/(empty-layout)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { EmptyLayout } from '@/components'; 2 | import React from 'react'; 3 | import { Props } from '@/types/Layout'; 4 | import { locales } from '@/static/locales'; 5 | 6 | //function to generate the routes for all the locales 7 | export function generateStaticParams() { 8 | return locales.map((locale) => ({ locale })); 9 | } 10 | 11 | export default function Layout({ children, params: { locale } }: Props) { 12 | return ( 13 | <> 14 | {children} 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/app/[locale]/(empty-layout)/login/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Tabs, theme } from 'antd'; 4 | import { LockOutlined, UserOutlined } from '@ant-design/icons'; 5 | import { LoginForm, ProConfigProvider, ProFormCheckbox, ProFormText } from '@ant-design/pro-components'; 6 | import React, { useEffect, useState } from 'react'; 7 | import { useRouter } from '@/lib/language'; 8 | import useAccessStore from '@/store/useAccessStore'; 9 | import { sleep } from 'ahooks/es/utils/testingHelpers'; 10 | import { useTranslations } from 'next-intl'; 11 | import { Props } from '@/types/Layout'; 12 | import { staticRouter } from '@/static/staticRouter'; 13 | import { ChangeLanguage } from '@/components'; 14 | import { login } from '@/static/emits'; 15 | import { useEvent } from '@/hooks/useEvent'; 16 | import Apis from '@/apis'; 17 | 18 | type LoginType = 'phone' | 'account'; 19 | 20 | export default function Login() { 21 | const { token } = theme.useToken(); 22 | const router = useRouter(); 23 | const [loginType, setLoginType] = useState('account'); 24 | const setAccess = useAccessStore((state) => state.setAccess); 25 | const t = useTranslations('login'); 26 | const tabItems = [ 27 | { label: '账户密码登录', key: 'account' }, 28 | ]; 29 | const event = useEvent(); 30 | const onSubmit = async () => { 31 | await sleep(1000); 32 | setAccess('canAccessSystem'); 33 | event.emitEvent(login, { login: '我登录了' }); 34 | router.push(`${staticRouter.welcome}`); 35 | return true; 36 | }; 37 | 38 | useEffect(() => { 39 | Apis.getUsers().then(res => { 40 | console.log(res); 41 | }); 42 | }); 43 | 44 | return ( 45 | 46 |
48 | 54 | } 55 | > 56 | setLoginType(activeKey as LoginType)} 61 | > 62 | 63 | {loginType === 'account' && ( 64 | <> 65 | , 70 | }} 71 | placeholder={'用户名: admin or user'} 72 | rules={[ 73 | { 74 | required: true, 75 | message: '请输入用户名!', 76 | }, 77 | ]} 78 | /> 79 | , 84 | strengthText: 85 | 'Password should contain numbers, letters and special characters, at least 8 characters long.', 86 | 87 | statusRender: (value) => { 88 | const getStatus = () => { 89 | if (value && value.length > 12) { 90 | return 'ok'; 91 | } 92 | if (value && value.length > 6) { 93 | return 'pass'; 94 | } 95 | return 'poor'; 96 | }; 97 | const status = getStatus(); 98 | if (status === 'pass') { 99 | return ( 100 |
101 | 强度:中 102 |
103 | ); 104 | } 105 | if (status === 'ok') { 106 | return ( 107 |
108 | 强度:强 109 |
110 | ); 111 | } 112 | return ( 113 |
强度:弱
114 | ); 115 | }, 116 | }} 117 | placeholder={'密码: admin'} 118 | rules={[ 119 | { 120 | required: true, 121 | message: '请输入密码!', 122 | }, 123 | ]} 124 | /> 125 | 126 | )} 127 |
132 | 133 | {t('auto')} 134 | 135 | 140 | 忘记密码 141 | 142 |
143 |
144 |
145 |
146 | ); 147 | } 148 | -------------------------------------------------------------------------------- /src/app/[locale]/(empty-layout)/not-auth/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import Link from 'next/link'; 3 | import { staticRouter } from '@/static/staticRouter'; 4 | import { Result } from 'antd'; 5 | 6 | export default function NotAuthorized() { 7 | return ( 8 | Return Home} 13 | /> 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /src/app/[locale]/(empty-layout)/not-server/page.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import { staticRouter } from '@/static/staticRouter'; 3 | import { Result } from 'antd'; 4 | 5 | export default function NotServer() { 6 | return ( 7 | Return Home} 12 | /> 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /src/app/[locale]/(main-layout)/dashboard/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | export default function Dashboard() { 4 | return ( 5 |
dashboard
6 | ); 7 | } 8 | -------------------------------------------------------------------------------- /src/app/[locale]/(main-layout)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { MainLayout } from '@/components'; 2 | import React from 'react'; 3 | import { Props } from '@/types/Layout'; 4 | import { locales } from '@/static/locales'; 5 | 6 | //function to generate the routes for all the locales 7 | export function generateStaticParams() { 8 | return locales.map((locale) => ({ locale })); 9 | } 10 | 11 | export default function Layout({ children, params: { locale } }: Props) { 12 | return ( 13 | <> 14 | {children} 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/app/[locale]/(main-layout)/list/ahook-table/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { Table } from 'antd'; 3 | import React from 'react'; 4 | import { useAntdTable } from 'ahooks'; 5 | import Apis from '@/apis'; 6 | 7 | interface Item { 8 | name: { 9 | last: string; 10 | }; 11 | email: string; 12 | phone: string; 13 | gender: 'male' | 'female'; 14 | } 15 | 16 | interface Result { 17 | total: number; 18 | list: Item[]; 19 | } 20 | 21 | const getTableData = async ({ current, pageSize }: { 22 | current: number, pageSize: number 23 | }): Promise => { 24 | const res = await Apis.getTableList({ 25 | results: 55, 26 | page: current, 27 | size: pageSize, 28 | }); 29 | return ({ 30 | total: res.info.results, 31 | list: res.results, 32 | }); 33 | }; 34 | 35 | export default function DemoTable() { 36 | const { tableProps } = useAntdTable(getTableData); 37 | 38 | const columns = [ 39 | { 40 | title: 'name', 41 | dataIndex: ['name', 'last'], 42 | }, 43 | { 44 | title: 'email', 45 | dataIndex: 'email', 46 | }, 47 | { 48 | title: 'phone', 49 | dataIndex: 'phone', 50 | }, 51 | { 52 | title: 'gender', 53 | dataIndex: 'gender', 54 | }, 55 | ]; 56 | 57 | return ; 58 | }; 59 | -------------------------------------------------------------------------------- /src/app/[locale]/(main-layout)/list/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { redirect } from '@/lib/language'; 3 | import useSettingStore from '@/store/useSettingStore'; 4 | import { staticRouter } from '@/static/staticRouter'; 5 | 6 | export default function List() { 7 | const defaultLocale = useSettingStore((state) => state.defaultLocale); 8 | // 静态 build 模式下 不能用 next/router 需要用next/navigation 9 | redirect(`/${staticRouter.ahookTable}`); 10 | } 11 | -------------------------------------------------------------------------------- /src/app/[locale]/(main-layout)/list/pro-form/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { 3 | ProForm, 4 | ProFormDatePicker, 5 | ProFormDateRangePicker, 6 | ProFormDigit, 7 | ProFormRadio, 8 | ProFormSelect, 9 | ProFormSwitch, 10 | ProFormText, 11 | ProFormTextArea, 12 | } from '@ant-design/pro-components'; 13 | import { Col, message, Row, Space } from 'antd'; 14 | import type { FormLayout } from 'antd/lib/form/Form'; 15 | import { useState } from 'react'; 16 | 17 | const LAYOUT_TYPE_HORIZONTAL = 'horizontal'; 18 | 19 | const waitTime = (time: number = 100) => { 20 | return new Promise((resolve) => { 21 | setTimeout(() => { 22 | resolve(true); 23 | }, time); 24 | }); 25 | }; 26 | 27 | export default function ProFormDemo() { 28 | const [formLayoutType, setFormLayoutType] = useState( 29 | LAYOUT_TYPE_HORIZONTAL, 30 | ); 31 | 32 | const [grid, setGrid] = useState(true); 33 | 34 | return ( 35 | 40 | layout={formLayoutType} 41 | grid={grid} 42 | rowProps={{ 43 | gutter: [16, formLayoutType === 'inline' ? 16 : 0], 44 | }} 45 | submitter={{ 46 | render: (props, doms) => { 47 | return formLayoutType === LAYOUT_TYPE_HORIZONTAL ? ( 48 | 49 | 50 | {doms} 51 | 52 | 53 | ) : ( 54 | doms 55 | ); 56 | }, 57 | }} 58 | onFinish={async (values) => { 59 | await waitTime(2000); 60 | console.log(values); 61 | message.success('提交成功'); 62 | }} 63 | params={{}} 64 | request={async () => { 65 | await waitTime(100); 66 | return { 67 | name: '蚂蚁设计有限公司', 68 | useMode: 'chapter', 69 | }; 70 | }} 71 | > 72 | setFormLayoutType(e.target.value), 78 | }} 79 | colProps={{ 80 | span: 20, 81 | }} 82 | options={['horizontal', 'vertical', 'inline']} 83 | /> 84 | 95 | 101 | 102 | 103 | 104 | 109 | 114 | 119 | 129 | 130 | ); 131 | }; 132 | -------------------------------------------------------------------------------- /src/app/[locale]/(main-layout)/list/pro-table/(components)/ModalForm.tsx: -------------------------------------------------------------------------------- 1 | import { PlusOutlined } from '@ant-design/icons'; 2 | import { ModalForm, ProForm, ProFormDateRangePicker, ProFormSelect, ProFormText } from '@ant-design/pro-components'; 3 | import { Button, Form, message } from 'antd'; 4 | 5 | const waitTime = (time: number = 100) => { 6 | return new Promise((resolve) => { 7 | setTimeout(() => { 8 | resolve(true); 9 | }, time); 10 | }); 11 | }; 12 | 13 | export default function ModalFormDemo() { 14 | const [form] = Form.useForm<{ name: string; company: string }>(); 15 | return ( 16 | 20 | title="新建表单" 21 | trigger={ 22 | 26 | } 27 | form={form} 28 | autoFocusFirstInput 29 | modalProps={{ 30 | destroyOnClose: true, 31 | onCancel: () => console.log('run'), 32 | }} 33 | submitTimeout={2000} 34 | onFinish={async (values) => { 35 | await waitTime(2000); 36 | console.log(values.name); 37 | message.success('提交成功'); 38 | return true; 39 | }} 40 | > 41 | 42 | 49 | 50 | 56 | 57 | 58 | 64 | 65 | 66 | 67 | [ 69 | { 70 | value: 'chapter', 71 | label: '盖章后生效', 72 | }, 73 | ]} 74 | width="xs" 75 | name="useMode" 76 | label="合同约定生效方式" 77 | /> 78 | 89 | 90 | 91 | 97 | 104 | 105 | ); 106 | }; 107 | -------------------------------------------------------------------------------- /src/app/[locale]/(main-layout)/list/pro-table/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { DownOutlined } from '@ant-design/icons'; 3 | import { ProColumns, ProTable, RequestData } from '@ant-design/pro-components'; 4 | import { Button } from 'antd'; 5 | import Apis from '@/apis'; 6 | import ModalFormDemo from '@/app/[locale]/(main-layout)/list/pro-table/(components)/ModalForm'; 7 | 8 | export type TableListItem = { 9 | name: string; 10 | email: number; 11 | phone: string; 12 | gender: string; 13 | }; 14 | 15 | const columns: ProColumns[] = [ 16 | { 17 | title: 'name', 18 | width: 80, 19 | dataIndex: ['name', 'last'], 20 | }, 21 | { 22 | title: 'email', 23 | dataIndex: 'email', 24 | }, 25 | { 26 | title: 'phone', 27 | dataIndex: 'phone', 28 | }, 29 | { 30 | title: 'gender', 31 | dataIndex: 'gender', 32 | }, 33 | ]; 34 | 35 | const getTableData = async (params: { 36 | pageSize?: number; 37 | current?: number; 38 | }): Promise> => { 39 | const res = await Apis.getTableList({ 40 | results: 55, 41 | page: params.current, 42 | size: params.pageSize, 43 | }); 44 | return ({ 45 | success: true, 46 | total: res.info.results || 0, 47 | data: res.results || [], 48 | }); 49 | }; 50 | 51 | export default function ProTaleDemo() { 52 | return ( 53 | <> 54 | 55 | columns={columns} 56 | request={getTableData} 57 | rowKey="phone" 58 | pagination={{ 59 | pageSize: 10, 60 | showQuickJumper: true, 61 | }} 62 | search={{ 63 | optionRender: false, 64 | collapsed: false, 65 | }} 66 | dateFormatter="string" 67 | headerTitle="表格标题" 68 | toolBarRender={() => [ 69 | , 70 | , 74 | , 75 | ]} 76 | /> 77 | 78 | ); 79 | }; 80 | -------------------------------------------------------------------------------- /src/app/[locale]/(main-layout)/welcome/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { DatePicker } from 'antd'; 4 | import { login } from '@/static/emits'; 5 | import { useEvent } from '@/hooks/useEvent'; 6 | import { useMount } from 'ahooks'; 7 | import Image from 'next/image'; 8 | import { useEffect } from 'react'; 9 | import Apis from '@/apis'; 10 | 11 | export default function Welcome() { 12 | const event = useEvent(); 13 | useMount(() => event.onEvent(login, e => console.log(e))); 14 | useEffect(() => { 15 | Apis.getUsers().then(res => { 16 | console.log(res); 17 | }); 18 | }); 19 | return ( 20 | <> 21 |
welcome
22 | {/* image 组件直接支持 svg 不用二次封装*/} 23 | 24 | 25 | 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --foreground-rgb: 0, 0, 0; 7 | --background-start-rgb: 214, 219, 220; 8 | --background-end-rgb: 255, 255, 255; 9 | } 10 | 11 | @media (prefers-color-scheme: dark) { 12 | :root { 13 | --foreground-rgb: 255, 255, 255; 14 | --background-start-rgb: 0, 0, 0; 15 | --background-end-rgb: 0, 0, 0; 16 | } 17 | } 18 | 19 | body { 20 | color: rgb(var(--foreground-rgb)); 21 | background: linear-gradient( 22 | to bottom, 23 | transparent, 24 | rgb(var(--background-end-rgb)) 25 | ) rgb(var(--background-start-rgb)); 26 | } 27 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Props } from '@/types/Layout'; 3 | 4 | export default function Layout({ children }: Props) { 5 | return ( 6 | 7 | 8 | {children} 9 | 10 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /src/app/loading.tsx: -------------------------------------------------------------------------------- 1 | export default function Loading() { 2 | return

Loading...

; 3 | } 4 | -------------------------------------------------------------------------------- /src/app/not-found.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import { staticRouter } from '@/static/staticRouter'; 3 | import { Result } from 'antd'; 4 | 5 | export default function NotFound() { 6 | return ( 7 | Return Home} 13 | /> 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { redirect } from 'next/navigation'; 3 | import useSettingStore from '@/store/useSettingStore'; 4 | import { staticRouter } from '@/static/staticRouter'; 5 | 6 | export default function Home() { 7 | const defaultLocale = useSettingStore((state) => state.defaultLocale); 8 | // 静态 build 模式下 不能用 next/router 需要用next/navigation 9 | redirect(`/${defaultLocale}/${staticRouter.login}`); 10 | } 11 | -------------------------------------------------------------------------------- /src/components/ChangeLanguage.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Group } from 'antd/es/radio'; 3 | import { usePathname as useIntlPathname, useRouter as useIntlRouter } from '@/lib/language'; 4 | import useSettingStore from '@/store/useSettingStore'; 5 | import { RadioChangeEvent } from 'antd'; 6 | 7 | export default function ChangeLanguage() { 8 | const options = [ 9 | { label: 'EN', value: 'en' }, 10 | { label: '中', value: 'zh' }, 11 | ]; 12 | const intlPathname = useIntlPathname(); 13 | const intlRouter = useIntlRouter(); 14 | const setDefaultLocale = useSettingStore((state) => state.setDefaultLocale); 15 | const defaultLocale = useSettingStore((state) => state.defaultLocale); 16 | const [value, setValue] = useState(defaultLocale); 17 | 18 | const onLanguageChange = ({ target: { value } }: RadioChangeEvent) => { 19 | setValue(value); 20 | setDefaultLocale(value); 21 | intlRouter.replace(intlPathname, { locale: value }); 22 | }; 23 | return ( 24 | <> 25 | 26 | 27 | 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /src/components/Link.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Link from 'next/link'; 3 | import { useRouter } from 'next/router'; 4 | 5 | // @ts-ignore 6 | const LinkComponent = ({ children, skipLocaleHandling, ...rest }) => { 7 | const router = useRouter(); 8 | const locale = rest.locale || router.query.locale || ''; 9 | 10 | let href = rest.href || router.asPath; 11 | if (href.indexOf('http') === 0) skipLocaleHandling = true; 12 | if (locale && !skipLocaleHandling) { 13 | href = href 14 | ? `/${locale}${href}` 15 | : router.pathname.replace('[locale]', locale); 16 | } 17 | 18 | return ( 19 | <> 20 | 21 | {children} 22 | 23 | 24 | ); 25 | }; 26 | 27 | export default LinkComponent; 28 | -------------------------------------------------------------------------------- /src/components/MockComponent.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { useEffect } from 'react'; 3 | 4 | export default function MockComponent() { 5 | useEffect(() => { 6 | if (typeof window !== 'undefined') { 7 | if (process.env.NEXT_PUBLIC_MOCK === 'true') { 8 | require('@/mock/index'); 9 | } 10 | } 11 | }, []); 12 | 13 | return null; 14 | }; 15 | -------------------------------------------------------------------------------- /src/components/Navigation.tsx: -------------------------------------------------------------------------------- 1 | import { useTranslations } from 'next-intl'; 2 | 3 | export default function Navigation({ onNavCLick, name }: { 4 | onNavCLick?: () => void, 5 | name?: string 6 | }) { 7 | const t = useTranslations('menu'); 8 | return ( 9 | <> 10 |
{t(`${name}`)}
11 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /src/components/NoSSR.tsx: -------------------------------------------------------------------------------- 1 | import dynamic from 'next/dynamic'; 2 | import React from 'react'; 3 | import { Props } from '@/types/Layout'; 4 | 5 | const NoSSR = (props: Partial) => {props.children}; 6 | 7 | export default dynamic(() => Promise.resolve(NoSSR), { 8 | ssr: false, 9 | }); 10 | -------------------------------------------------------------------------------- /src/components/index.ts: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | //* LAYOUT COMPONENTS 4 | export { default as MainLayout } from './layouts/MainLayout'; 5 | export { default as EmptyLayout } from './layouts/EmptyLayout'; 6 | export { default as RootLayout } from './layouts/RootLayout'; 7 | 8 | // OTHERS 9 | export { default as NoSSR } from './NoSSR'; 10 | export { default as Link } from './Link'; 11 | export { default as Navigation } from './Navigation'; 12 | export { default as ChangeLanguage } from './ChangeLanguage'; 13 | export { default as MockComponent } from './MockComponent'; 14 | -------------------------------------------------------------------------------- /src/components/layouts/EmptyLayout.tsx: -------------------------------------------------------------------------------- 1 | import '@/app/globals.css'; 2 | import AntdStyledComponentsRegistry from '@/lib/antd-registry'; 3 | import React from 'react'; 4 | import { NextIntlClientProvider } from 'next-intl'; 5 | import { Props } from '@/types/Layout'; 6 | import { timeZone } from '@/static/locales'; 7 | import { MockComponent } from '@/components'; 8 | import { en } from '@/i18n/en'; 9 | import { zh } from '@/i18n/zh'; 10 | 11 | export const metadata: { title: string, description: string } = { 12 | title: 'React Next Admin', 13 | description: '', 14 | }; 15 | 16 | export default function EmptyLayout({ children, params: { locale } }: Props) { 17 | const messages = locale === 'en' ? en : zh; 18 | return ( 19 | 20 | 21 | 22 | {children} 23 | 24 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /src/components/layouts/MainLayout.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import '@/app/globals.css'; 3 | import React, { Suspense, useEffect, useState } from 'react'; 4 | import AntdStyledComponentsRegistry from '@/lib/antd-registry'; 5 | import { PageContainer, ProCard, ProLayout } from '@ant-design/pro-components'; 6 | import { LogoutOutlined } from '@ant-design/icons'; 7 | import { Dropdown, MenuProps, Spin } from 'antd'; 8 | import { usePathname, useRouter } from '@/lib/language'; 9 | import { Props } from '@/types/Layout'; 10 | import { NextIntlClientProvider } from 'next-intl'; 11 | import useMainLayoutProps from '@/components/layouts/useMainLayoutProps'; 12 | import { staticRouter } from '@/static/staticRouter'; 13 | import { timeZone } from '@/static/locales'; 14 | import { ChangeLanguage, MockComponent, Navigation, NoSSR } from '@/components'; 15 | import { en } from '@/i18n/en'; 16 | import { zh } from '@/i18n/zh'; 17 | 18 | export default function MainLayout({ children, params: { locale } }: Props) { 19 | useEffect(() => { 20 | }, []); 21 | 22 | const router = useRouter(); 23 | const currentRoute = usePathname() as string; 24 | const [pathname, setPathname] = useState(currentRoute || `${staticRouter.welcome}`); 25 | const onClick: MenuProps['onClick'] = (event: any) => { 26 | if (event.key === 'logout') router.push(`${staticRouter.login}`); 27 | }; 28 | 29 | const [props, settings] = useMainLayoutProps(); 30 | 31 | // 右侧菜单 32 | const items: MenuProps['items'] = [ 33 | { 34 | key: 'logout', 35 | icon: , 36 | label: '退出登录', 37 | }, 38 | ]; 39 | const messages = locale === 'en' ? en : zh; 40 | 41 | return ( 42 | 43 | 44 | 45 | 46 |
50 | { 62 | return ( 63 | 69 | {dom} 70 | 71 | ); 72 | }, 73 | }} 74 | itemRender={(route: any) => { 75 | return ; 76 | }} 77 | actionsRender={(props) => { 78 | if (props.isMobile) return []; 79 | return [ 80 | , 81 | ]; 82 | }} 83 | menuDataRender={(menuData) => { 84 | return menuData.map((item: any) => { 85 | return item.access && item; 86 | }); 87 | }} 88 | menuItemRender={(item: any) => { 89 | return ( 90 | { 91 | setPathname(`${item.path || staticRouter.welcome}`); 92 | router.push(`${item.path}`); 93 | }}> 94 | ); 95 | }} 96 | subMenuItemRender={(item) => { 97 | return ( 98 | { 99 | if (!item.children) { 100 | setPathname(`${item.path || staticRouter.welcome}`); 101 | router.push(`${item.path}`); 102 | } 103 | }}> 104 | ); 105 | }} 106 | > 107 | 113 | 119 | }> 120 | {children} 121 | 122 | 123 | 124 | 125 | 126 |
127 |
128 |
129 |
130 | ) 131 | ; 132 | } 133 | -------------------------------------------------------------------------------- /src/components/layouts/RootLayout.tsx: -------------------------------------------------------------------------------- 1 | import '@/app/globals.css'; 2 | import AntdStyledComponentsRegistry from '@/lib/antd-registry'; 3 | import React from 'react'; 4 | import { Props } from '@/types/Layout'; 5 | 6 | export const metadata: { title: string, description: string } = { 7 | title: 'React Next Admin', 8 | description: '', 9 | }; 10 | 11 | export default async function RootLayout({ children }: Partial) { 12 | return ( 13 | 14 | 15 | 16 | {children} 17 | 18 | 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /src/components/layouts/useMainLayoutProps.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { CrownFilled, SmileFilled } from '@ant-design/icons'; 3 | import useAccessStore from '@/store/useAccessStore'; 4 | import type { ProSettings } from '@ant-design/pro-components'; 5 | import React from 'react'; 6 | import { staticRouter } from '@/static/staticRouter'; 7 | 8 | const useDefaultLayoutProps = () => { 9 | // eslint-disable-next-line react-hooks/rules-of-hooks 10 | const canAccessSystem = useAccessStore((state) => state.canAccessSystem); 11 | // 设置 12 | const settings: ProSettings | undefined = { 13 | fixSiderbar: true, 14 | layout: 'mix', 15 | // title: false, 16 | }; 17 | const props = { 18 | title: 'React Next Admin', 19 | siderWidth: 216, 20 | logo: false, 21 | location: { 22 | pathname: staticRouter.root, 23 | }, 24 | route: { 25 | path: staticRouter.root, 26 | routes: [ 27 | { 28 | path: staticRouter.welcome, 29 | name: 'welcome', 30 | icon: , 31 | access: canAccessSystem, 32 | }, 33 | { 34 | path: staticRouter.dashboard, 35 | name: 'dashboard', 36 | icon: , 37 | access: canAccessSystem, 38 | }, 39 | { 40 | name: 'demo', 41 | path: staticRouter.list, 42 | access: canAccessSystem, 43 | routes: [ 44 | { 45 | path: staticRouter.ahookTable, 46 | name: 'ahookList', 47 | icon: , 48 | }, 49 | { 50 | path: staticRouter.proTable, 51 | name: 'proList', 52 | icon: , 53 | }, 54 | { 55 | path: staticRouter.proForm, 56 | name: 'proForm', 57 | icon: , 58 | }, 59 | ], 60 | }, 61 | ], 62 | }, 63 | }; 64 | return [props, settings]; 65 | }; 66 | 67 | 68 | export default useDefaultLayoutProps; 69 | -------------------------------------------------------------------------------- /src/hooks/useEvent.ts: -------------------------------------------------------------------------------- 1 | import mitt from 'mitt'; 2 | 3 | type IUseEventbus = { 4 | emitEvent: (eventName: symbol, val: any) => void; 5 | onEvent: (eventName: symbol, callback: (val: any) => void) => void; 6 | }; 7 | 8 | const emitter = mitt(); 9 | 10 | const emitEvent = (eventName: symbol, val: any) => { 11 | emitter.emit(eventName, val); 12 | }; 13 | 14 | const onEvent = (eventName: symbol, callback: (val: any) => void) => { 15 | emitter.on(eventName, callback); 16 | }; 17 | 18 | 19 | export const useEvent = (): IUseEventbus => { 20 | 21 | return { 22 | emitEvent, 23 | onEvent, 24 | }; 25 | }; 26 | -------------------------------------------------------------------------------- /src/i18n/en.ts: -------------------------------------------------------------------------------- 1 | import { login } from '@/i18n/locales/en/login'; 2 | import { menu } from '@/i18n/locales/en/menu'; 3 | 4 | 5 | export const en = { 6 | login, 7 | menu, 8 | }; 9 | -------------------------------------------------------------------------------- /src/i18n/locales/en/login.ts: -------------------------------------------------------------------------------- 1 | export const login = { 2 | auto: 'Automatic login', 3 | }; 4 | -------------------------------------------------------------------------------- /src/i18n/locales/en/menu.ts: -------------------------------------------------------------------------------- 1 | export const menu = { 2 | welcome: 'Welcome', 3 | dashboard: 'Dashboard', 4 | demo: 'Demo', 5 | ahookList: 'ahookList', 6 | proList: 'proList', 7 | proForm: 'proFrom', 8 | }; 9 | -------------------------------------------------------------------------------- /src/i18n/locales/zh/login.ts: -------------------------------------------------------------------------------- 1 | export const login = { 2 | auto: '自动登录', 3 | }; 4 | -------------------------------------------------------------------------------- /src/i18n/locales/zh/menu.ts: -------------------------------------------------------------------------------- 1 | export const menu = { 2 | welcome: '欢迎', 3 | dashboard: '看板', 4 | demo: '演示', 5 | ahookList: '基于 ahook 的 table', 6 | proList: '基于proTable的 table', 7 | proForm: '基于proFrom的 表单', 8 | }; 9 | -------------------------------------------------------------------------------- /src/i18n/zh.ts: -------------------------------------------------------------------------------- 1 | import { login } from '@/i18n/locales/zh/login'; 2 | import { menu } from '@/i18n/locales/zh/menu'; 3 | 4 | export const zh = { 5 | login, 6 | menu, 7 | }; 8 | -------------------------------------------------------------------------------- /src/lib/antd-registry.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import React, { useEffect, useState } from 'react'; 3 | import { App, ConfigProvider, ConfigProviderProps } from 'antd'; 4 | import 'antd/dist/reset.css'; 5 | import { AntdRegistry } from '@ant-design/nextjs-registry'; 6 | import enUS from 'antd/locale/en_US'; 7 | import zhCN from 'antd/locale/zh_CN'; 8 | import dayjs from 'dayjs'; 9 | import useSettingStore from '@/store/useSettingStore'; 10 | import { locales } from '@/static/locales'; 11 | 12 | type Locale = ConfigProviderProps['locale']; 13 | const AntdConfigProvider: React.FC = ({ children }) => { 14 | const defaultLocale = useSettingStore((state) => state.defaultLocale); 15 | const [locale, setLocal] = useState(enUS); 16 | useEffect(() => { 17 | dayjs.locale('en'); 18 | if (defaultLocale === locales[0]) { 19 | setLocal(enUS); 20 | dayjs.locale('en'); 21 | } else { 22 | setLocal(zhCN); 23 | dayjs.locale('zh-cn'); 24 | } 25 | }, [defaultLocale]); 26 | return ( 27 | 31 |
{children}
32 |
33 | ); 34 | }; 35 | 36 | const AntdStyleRegistry: React.FC = ({ children }) => { 37 | return ( 38 | 39 | 40 | {children} 41 | 42 | 43 | ); 44 | }; 45 | 46 | export default AntdStyleRegistry; 47 | -------------------------------------------------------------------------------- /src/lib/create-ctx.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import React, { createContext, Dispatch, SetStateAction, useContext, useState } from 'react'; 3 | 4 | const createCtx = (initial_value?: T) => { 5 | const storeContext = createContext(null!); 6 | const dispatchContext = createContext>>(null!); 7 | 8 | const useStore = () => { 9 | const context = useContext(storeContext); 10 | if (context === undefined) { 11 | throw new Error('useStore:context undefined'); 12 | } 13 | return context; 14 | }; 15 | 16 | const useDispatch = () => { 17 | const context = useContext(dispatchContext); 18 | if (context === undefined) { 19 | throw new Error('useDispatch:context undefined'); 20 | } 21 | return context; 22 | }; 23 | 24 | const ContextProvider: React.FC> = ({ value, children }) => { 25 | const [state, dispatch] = useState(initial_value || value); 26 | 27 | return ( 28 | 29 | {children} 30 | 31 | ); 32 | }; 33 | 34 | return { ContextProvider, useStore, useDispatch }; 35 | }; 36 | export default createCtx; 37 | -------------------------------------------------------------------------------- /src/lib/events.ts: -------------------------------------------------------------------------------- 1 | import mitt from 'mitt'; 2 | 3 | const emitter = mitt(); 4 | 5 | export default emitter; 6 | -------------------------------------------------------------------------------- /src/lib/language.ts: -------------------------------------------------------------------------------- 1 | import { createLocalizedPathnamesNavigation } from 'next-intl/navigation'; 2 | import { locales } from '@/static/locales'; 3 | import { staticRouter } from '@/static/staticRouter'; 4 | 5 | export function generateStaticParams() { 6 | return ['en', 'zh'].map((locale) => ({ locale })); 7 | } 8 | 9 | export const localePrefix = undefined; 10 | export const pathnames = {} as any; 11 | 12 | export const { Link, redirect, usePathname, useRouter, getPathname } = 13 | createLocalizedPathnamesNavigation({ 14 | locales, 15 | pathnames, 16 | localePrefix, 17 | }); 18 | 19 | export type AppPathnames = keyof typeof staticRouter; 20 | 21 | -------------------------------------------------------------------------------- /src/lib/request.ts: -------------------------------------------------------------------------------- 1 | import { StatusCodes } from 'http-status-codes'; 2 | import { G, requestCreator } from '@siyuan0215/easier-axios-dsl'; 3 | 4 | const TIMEOUT = { 5 | DEFAULT: 3 * 60000, 6 | UPLOADING: 5 * 60000, 7 | }; 8 | export const request = requestCreator({ 9 | baseURL: process.env.NEXT_PUBLIC_BASE_URL, 10 | timeout: TIMEOUT.DEFAULT, 11 | withCredentials: true, 12 | requestInterceptors: [ 13 | (config) => { 14 | return { 15 | ...config, 16 | timeout: TIMEOUT.UPLOADING, 17 | headers: { 18 | ...config.headers, 19 | authorization: '1', 20 | }, 21 | }; 22 | }, 23 | (error: any) => Promise.reject(error), 24 | ], 25 | responseInterceptors: [ 26 | (response) => { 27 | const { status } = response; 28 | 29 | if (status === StatusCodes.OK) { 30 | return response; 31 | } 32 | return Promise.reject(response); 33 | }, 34 | (error: string) => { 35 | return Promise.reject(error); 36 | }, 37 | ], 38 | }); 39 | 40 | export const generatorAPIS = (apiConfig: T) => G(request, apiConfig); 41 | -------------------------------------------------------------------------------- /src/mock/index.ts: -------------------------------------------------------------------------------- 1 | export { userInfo } from './userInfo'; 2 | -------------------------------------------------------------------------------- /src/mock/userInfo.ts: -------------------------------------------------------------------------------- 1 | import Mock from 'mockjs'; 2 | 3 | const Random = Mock.Random; 4 | const userInfo = Mock.mock('/user/login', 'post', () => { 5 | const ret = Mock.mock({ username: '@cname', age: Random.integer(60, 100), ID: Random.id() }); 6 | return { 7 | status: 200, 8 | data: ret, 9 | }; 10 | }); 11 | 12 | export { userInfo }; 13 | -------------------------------------------------------------------------------- /src/services/userService.ts: -------------------------------------------------------------------------------- 1 | import Apis from '@/apis'; 2 | 3 | export function getUsers() { 4 | return Apis.getUsers(); 5 | } 6 | -------------------------------------------------------------------------------- /src/static/emits.ts: -------------------------------------------------------------------------------- 1 | export const login = Symbol('LOGIN'); 2 | -------------------------------------------------------------------------------- /src/static/encrypt.ts: -------------------------------------------------------------------------------- 1 | // 使用对称加密方式 2 | import CryptoJS from 'crypto-js'; 3 | // 替换为你的密钥 4 | const secretKey = 'your-secret-key'; 5 | 6 | export const encryptData = (data: Record) => { 7 | const dataString = JSON.stringify(data); 8 | const encrypted = CryptoJS.AES.encrypt(dataString, secretKey); 9 | return encrypted.toString(); 10 | }; 11 | 12 | export const decryptData = (encryptedData: string) => { 13 | const bytes = CryptoJS.AES.decrypt(encryptedData, secretKey); 14 | const decryptedDataString = bytes.toString(CryptoJS.enc.Utf8); 15 | return JSON.parse(decryptedDataString); 16 | }; 17 | -------------------------------------------------------------------------------- /src/static/locales.ts: -------------------------------------------------------------------------------- 1 | export const defaultLocale: string = 'zh'; 2 | export const locales: string[] = ['en', 'zh']; 3 | export const timeZone = 'UTC'; 4 | -------------------------------------------------------------------------------- /src/static/staticRouter.ts: -------------------------------------------------------------------------------- 1 | export const staticRouter = { 2 | root: '/', 3 | login: '/login', 4 | welcome: '/welcome', 5 | notFound: '/not-found', 6 | dashboard: '/dashboard', 7 | dictionary: '/dictionary', 8 | list: '/list', 9 | ahookTable: '/list/ahook-table', 10 | proTable: '/list/pro-table', 11 | proForm: '/list/pro-form', 12 | }; 13 | -------------------------------------------------------------------------------- /src/store/useAccessStore.ts: -------------------------------------------------------------------------------- 1 | import { create } from 'zustand'; 2 | import { createJSONStorage, persist } from 'zustand/middleware'; 3 | 4 | type AccessesState = { 5 | canAccessSystem: boolean 6 | setAccess: (newVal: string) => void; 7 | } 8 | 9 | export function containsAny(arr1: string[], arr2: string[]): boolean { 10 | return arr1.some((r) => arr2.includes(r)); 11 | } 12 | 13 | const useAccessStore = create()( 14 | persist( 15 | (set) => ({ 16 | canAccessSystem: true, 17 | setAccess: (newVal: any) => set((state: any) => ({ 18 | canAccessSystem: state.canAccessSystem = containsAny([newVal], ['canAccessSystem']), 19 | })), 20 | }), 21 | { 22 | name: 'access', 23 | storage: createJSONStorage(() => sessionStorage), // default localstorage 24 | }, 25 | ), 26 | ); 27 | 28 | export default useAccessStore; 29 | -------------------------------------------------------------------------------- /src/store/useAuthStore.ts: -------------------------------------------------------------------------------- 1 | import { create } from 'zustand'; 2 | import { createJSONStorage, persist } from 'zustand/middleware'; 3 | 4 | 5 | // Custom types for theme 6 | 7 | interface AuthState { 8 | isAuthenticated: boolean; 9 | user: any; 10 | token: null | string; 11 | login: (email: string, password: string) => Promise; 12 | register: (userInfo: FormData) => Promise; 13 | logout: () => void; 14 | setToken: (newVal: string) => void; 15 | } 16 | 17 | const useAuthStore = create()( 18 | persist( 19 | (set) => ({ 20 | isAuthenticated: false, 21 | user: null, 22 | token: null, 23 | setToken: (newVal) => set((state: any) => ({ 24 | token: state.token = newVal, 25 | })), 26 | login: async () => { 27 | // Login user code 28 | }, 29 | register: async () => { 30 | // Registering user code 31 | }, 32 | logout: () => { 33 | // Logout user code 34 | }, 35 | }), 36 | { 37 | name: 'auth', 38 | storage: createJSONStorage(() => sessionStorage), // default localstorage 39 | }, 40 | ), 41 | ); 42 | 43 | export default useAuthStore; 44 | -------------------------------------------------------------------------------- /src/store/useSettingStore.ts: -------------------------------------------------------------------------------- 1 | import { create } from 'zustand'; 2 | import { createJSONStorage, persist } from 'zustand/middleware'; 3 | import { defaultLocale, locales } from '@/static/locales'; 4 | 5 | interface SettingState { 6 | defaultLocale: string; 7 | locales: string[]; 8 | setDefaultLocale: (newVal: string) => void; 9 | } 10 | 11 | const useSettingStore = create()( 12 | persist( 13 | (set, get) => ({ 14 | defaultLocale: get()?.defaultLocale ? get()?.defaultLocale : defaultLocale, 15 | locales: locales, 16 | setDefaultLocale: (newVal) => set((state: any) => ({ 17 | defaultLocale: state.defaultLocale = newVal, 18 | })), 19 | }), 20 | { 21 | name: 'setting', 22 | storage: createJSONStorage(() => sessionStorage), // default localstorage 23 | }, 24 | ), 25 | ); 26 | 27 | export default useSettingStore; 28 | -------------------------------------------------------------------------------- /src/types/Layout.ts: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | 3 | interface PropsParams { 4 | locale:string 5 | } 6 | 7 | export interface Props { 8 | children: ReactNode 9 | params: Partial 10 | } 11 | 12 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | export type StringOrNumber = string | number 2 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'tailwindcss'; 2 | 3 | const config: Config = { 4 | content: [ 5 | './src/**/*.{js,ts,jsx,tsx,mdx}', 6 | ], 7 | theme: { 8 | extend: { 9 | backgroundImage: { 10 | 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', 11 | 'gradient-conic': 12 | 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))', 13 | }, 14 | }, 15 | }, 16 | plugins: [], 17 | }; 18 | export default config; 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": true, 12 | "noEmit": true, 13 | "esModuleInterop": true, 14 | "module": "esnext", 15 | "moduleResolution": "bundler", 16 | "resolveJsonModule": true, 17 | "isolatedModules": true, 18 | "jsx": "preserve", 19 | "incremental": true, 20 | "plugins": [ 21 | { 22 | "name": "next" 23 | } 24 | ], 25 | "paths": { 26 | "@/*": [ 27 | "./src/*" 28 | ] 29 | } 30 | }, 31 | "include": [ 32 | "next-env.d.ts", 33 | "**/*.ts", 34 | "**/*.tsx", 35 | ".next/types/**/*.ts" 36 | ], 37 | "exclude": [ 38 | "node_modules" 39 | ] 40 | } 41 | --------------------------------------------------------------------------------