├── .prettierignore
├── .stylelintignore
├── pnpm-workspace.yaml
├── .env
├── .husky
└── commit-msg
├── src
├── vite-env.d.ts
├── assets
│ ├── fonts
│ │ ├── DIN.otf
│ │ ├── MetroDF.ttf
│ │ ├── YouSheBiaoTiHei.ttf
│ │ └── font.less
│ ├── images
│ │ ├── avatar.png
│ │ └── logo.svg
│ └── css
│ │ ├── theme-color.less
│ │ ├── scrollbar.less
│ │ ├── default.less
│ │ ├── theme.less
│ │ ├── antd.less
│ │ ├── public.less
│ │ └── reset.less
├── components
│ ├── Theme
│ │ ├── index.module.less
│ │ └── index.tsx
│ ├── GlobalSearch
│ │ ├── index.module.less
│ │ ├── index.tsx
│ │ └── components
│ │ │ └── SearchFooter.tsx
│ ├── Buttons
│ │ ├── index.ts
│ │ └── components
│ │ │ ├── BaseBtn.tsx
│ │ │ ├── UpdateBtn.tsx
│ │ │ └── DeleteBtn.tsx
│ ├── Selects
│ │ ├── components
│ │ │ └── Loading.tsx
│ │ ├── index.ts
│ │ ├── types.ts
│ │ ├── BaseTreeSelect.tsx
│ │ ├── BaseSelect.tsx
│ │ ├── ApiSelect.tsx
│ │ └── ApiTreeSelect.tsx
│ ├── Dates
│ │ ├── index.ts
│ │ └── components
│ │ │ ├── BaseTimePicker.tsx
│ │ │ ├── BaseDatePicker.tsx
│ │ │ ├── BaseTimeRangePicker.tsx
│ │ │ └── BaseRangePicker.tsx
│ ├── Table
│ │ ├── index.less
│ │ ├── utils
│ │ │ ├── state.ts
│ │ │ ├── reducer.ts
│ │ │ └── helper.ts
│ │ ├── components
│ │ │ ├── ResizableTitle.tsx
│ │ │ ├── VirtualWrapper.tsx
│ │ │ ├── EllipsisText.tsx
│ │ │ └── DragContent.tsx
│ │ └── hooks
│ │ │ └── useFiler.ts
│ ├── Business
│ │ ├── index.tsx
│ │ └── Selects
│ │ │ ├── PartnerSelect.tsx
│ │ │ └── GameSelect.tsx
│ ├── Upload
│ │ └── BaseUpload.tsx
│ ├── Card
│ │ └── BaseCard.tsx
│ ├── Github
│ │ └── index.tsx
│ ├── Content
│ │ └── BaseContent.tsx
│ ├── Transfer
│ │ └── BaseTransfer.tsx
│ ├── PasswordStrength
│ │ ├── components
│ │ │ └── StrengthBar.tsx
│ │ └── index.tsx
│ ├── Pagination
│ │ ├── BasePagination.tsx
│ │ └── index.less
│ ├── Fullscreen
│ │ └── index.tsx
│ ├── Bottom
│ │ └── SubmitBottom.tsx
│ ├── Form
│ │ └── components
│ │ │ └── LoadingComponent.tsx
│ ├── Modal
│ │ └── index.less
│ ├── Copy
│ │ ├── CopyBtn.tsx
│ │ └── CopyInput.tsx
│ ├── Count
│ │ └── index.tsx
│ ├── Ellipsis
│ │ └── index.tsx
│ ├── I18n
│ │ └── index.tsx
│ └── WangEditor
│ │ └── index.tsx
├── router
│ ├── utils
│ │ ├── config.ts
│ │ └── helper.tsx
│ ├── components
│ │ ├── Router.tsx
│ │ └── Guards.tsx
│ └── index.tsx
├── servers
│ ├── dashboard
│ │ └── index.ts
│ ├── platform
│ │ ├── game.ts
│ │ └── partner.ts
│ ├── login
│ │ └── index.ts
│ ├── content
│ │ └── article.ts
│ └── system
│ │ ├── menu.ts
│ │ ├── role.ts
│ │ └── user.ts
├── stores
│ ├── index.ts
│ ├── user.ts
│ ├── public.ts
│ └── menu.ts
├── utils
│ ├── permissions.ts
│ ├── request.ts
│ ├── is.ts
│ ├── config.ts
│ └── constants.ts
├── locales
│ ├── zh
│ │ ├── dashboard.ts
│ │ ├── systems
│ │ │ └── menu.ts
│ │ ├── system.ts
│ │ ├── content.ts
│ │ ├── login.ts
│ │ └── public.ts
│ ├── en
│ │ ├── dashboard.ts
│ │ ├── systems
│ │ │ └── menu.ts
│ │ ├── content.ts
│ │ ├── system.ts
│ │ ├── login.ts
│ │ └── public.ts
│ ├── config.ts
│ └── utils
│ │ └── helper.ts
├── pages
│ ├── content
│ │ └── article
│ │ │ ├── components
│ │ │ └── CustomizeInput.tsx
│ │ │ └── model.ts
│ ├── login
│ │ └── model.ts
│ ├── all.module.less
│ ├── demo
│ │ ├── level1
│ │ │ └── level2
│ │ │ │ └── level3.tsx
│ │ ├── editor
│ │ │ └── index.tsx
│ │ ├── [id]
│ │ │ └── dynamic
│ │ │ │ └── index.tsx
│ │ ├── virtualScroll
│ │ │ ├── components
│ │ │ │ ├── VirtualList.tsx
│ │ │ │ └── VirtualTable.tsx
│ │ │ └── index.tsx
│ │ ├── copy
│ │ │ └── index.tsx
│ │ └── watermark
│ │ │ └── index.tsx
│ ├── index.tsx
│ ├── dashboard
│ │ ├── model.ts
│ │ ├── components
│ │ │ ├── Bar.tsx
│ │ │ ├── Line.tsx
│ │ │ └── Block.tsx
│ │ └── index.tsx
│ ├── 403.tsx
│ ├── 404.tsx
│ └── system
│ │ ├── menu
│ │ └── components
│ │ │ ├── IconInput.tsx
│ │ │ └── StateSwitch.tsx
│ │ ├── role
│ │ ├── components
│ │ │ └── AuthorizeSelect.tsx
│ │ └── model.tsx
│ │ └── user
│ │ └── components
│ │ └── PermissionDrawer.tsx
├── menus
│ ├── index.ts
│ ├── README.md
│ └── demo.ts
├── hooks
│ ├── useToken.ts
│ ├── useTime.ts
│ ├── useLogout.ts
│ ├── useFullscreen.ts
│ ├── useSearchUrlParams.ts
│ ├── useKeyStroke.ts
│ ├── useClipboard.ts
│ ├── useCommonStore.ts
│ ├── useEcharts.ts
│ └── useWatermark.ts
├── layouts
│ ├── components
│ │ ├── TabRefresh.tsx
│ │ ├── TabMaximize.tsx
│ │ ├── DraggableTabNode.tsx
│ │ ├── TabOptions.tsx
│ │ ├── Nav.tsx
│ │ └── ErrorBoundary.tsx
│ ├── index.module.less
│ └── utils
│ │ └── helper.ts
└── main.tsx
├── packages
├── utils
│ ├── src
│ │ ├── index.ts
│ │ ├── crypto.ts
│ │ └── local.ts
│ ├── package.json
│ └── tsconfig.json
├── message
│ ├── package.json
│ ├── tsconfig.json
│ └── src
│ │ └── index.ts
├── request
│ ├── package.json
│ ├── tsconfig.json
│ └── src
│ │ ├── types.ts
│ │ └── index.ts
└── stylelintConfig
│ ├── README.md
│ ├── package.json
│ └── index.mjs
├── stylelint.config.mjs
├── .env.production
├── .env.test
├── tsconfig.node.json
├── public
├── upgrade.css
├── logo.svg
└── loading.css
├── .vscode
├── extensions.json
└── settings.json
├── .env.development
├── .prettierrc
├── .gitignore
├── .commitlintrc.json
├── tsconfig.json
├── index.html
├── vite.config.ts
├── LICENSE
└── types
└── public.ts
/.prettierignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | *.md
3 | autoImports.d.ts
--------------------------------------------------------------------------------
/.stylelintignore:
--------------------------------------------------------------------------------
1 | /dist/*
2 | /public/*
3 | public/*
4 |
--------------------------------------------------------------------------------
/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
1 | packages:
2 | - 'packages/*'
3 |
--------------------------------------------------------------------------------
/.env:
--------------------------------------------------------------------------------
1 | # 加密密钥
2 | VITE_SECRET_KEY = "__Vite_Admin_Secret__"
3 |
--------------------------------------------------------------------------------
/.husky/commit-msg:
--------------------------------------------------------------------------------
1 | npx --no-install commitlint --edit $1
2 |
--------------------------------------------------------------------------------
/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/packages/utils/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './local';
2 | export * from './crypto';
3 |
--------------------------------------------------------------------------------
/stylelint.config.mjs:
--------------------------------------------------------------------------------
1 | export default {
2 | extends: ['@south/stylelint'],
3 | root: true,
4 | };
5 |
--------------------------------------------------------------------------------
/src/assets/fonts/DIN.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/southliu/react-admin/HEAD/src/assets/fonts/DIN.otf
--------------------------------------------------------------------------------
/src/assets/fonts/MetroDF.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/southliu/react-admin/HEAD/src/assets/fonts/MetroDF.ttf
--------------------------------------------------------------------------------
/src/assets/images/avatar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/southliu/react-admin/HEAD/src/assets/images/avatar.png
--------------------------------------------------------------------------------
/.env.production:
--------------------------------------------------------------------------------
1 | VITE_ENV = "production"
2 |
3 | VITE_BASE_URL = "https://mock.mengxuegu.com/mock/63f830b1c5a76a117cab185e/v1"
--------------------------------------------------------------------------------
/.env.test:
--------------------------------------------------------------------------------
1 | VITE_ENV = "test"
2 |
3 | # 测试接口
4 | VITE_BASE_URL = "https://mock.mengxuegu.com/mock/63f830b1c5a76a117cab185e/v1"
--------------------------------------------------------------------------------
/src/assets/fonts/YouSheBiaoTiHei.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/southliu/react-admin/HEAD/src/assets/fonts/YouSheBiaoTiHei.ttf
--------------------------------------------------------------------------------
/src/components/Theme/index.module.less:
--------------------------------------------------------------------------------
1 | ::view-transition-new(root),
2 | ::view-transition-old(root) {
3 | /*关闭默认动画 */
4 | animation: none;
5 | }
6 |
--------------------------------------------------------------------------------
/src/components/GlobalSearch/index.module.less:
--------------------------------------------------------------------------------
1 | .icon {
2 | box-shadow:
3 | inset 0 -2px #cdcde6,
4 | inset 0 0 1px 1px #fff,
5 | 0 1px 2px 1px #1e235a66;
6 | }
7 |
--------------------------------------------------------------------------------
/src/components/Buttons/index.ts:
--------------------------------------------------------------------------------
1 | export { default as BaseBtn } from './components/BaseBtn';
2 | export { default as UpdateBtn } from './components/UpdateBtn';
3 | export { default as DeleteBtn } from './components/DeleteBtn';
4 |
--------------------------------------------------------------------------------
/src/router/utils/config.ts:
--------------------------------------------------------------------------------
1 | // 生成路由排除内容,不带后缀名转换成“/文件名/”格式
2 | export const ROUTER_EXCLUDE = [
3 | 'login',
4 | 'forget',
5 | 'components',
6 | 'utils',
7 | 'lib',
8 | 'hooks',
9 | 'model.tsx',
10 | '404.tsx',
11 | ];
12 |
--------------------------------------------------------------------------------
/src/servers/dashboard/index.ts:
--------------------------------------------------------------------------------
1 | import { request } from '@/utils/request';
2 |
3 | /**
4 | * 获取数据总览数据
5 | * @param data - 请求数据
6 | */
7 | export function getDataTrends(data: object) {
8 | return request.get('/dashboard', { params: data });
9 | }
10 |
--------------------------------------------------------------------------------
/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "module": "ESNext",
5 | "moduleResolution": "Bundler",
6 | "allowSyntheticDefaultImports": true
7 | },
8 | "include": ["vite.config.ts", "build/**/*.ts"]
9 | }
10 |
--------------------------------------------------------------------------------
/public/upgrade.css:
--------------------------------------------------------------------------------
1 | .upgrade h1 {
2 | font-size: 48px;
3 | margin-bottom: 20px;
4 | }
5 | .upgrade p {
6 | font-size: 24px;
7 | margin-bottom: 40px;
8 | }
9 | .upgrade a {
10 | color: #0077cc;
11 | text-decoration: none;
12 | font-weight: bold;
13 | }
14 |
--------------------------------------------------------------------------------
/packages/message/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@south/message",
3 | "version": "0.0.1",
4 | "exports": {
5 | ".": "./src/index.ts"
6 | },
7 | "typesVersions": {
8 | "*": {
9 | "*": [
10 | "./src/*"
11 | ]
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/packages/request/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@south/request",
3 | "version": "0.0.1",
4 | "exports": {
5 | ".": "./src/index.ts"
6 | },
7 | "typesVersions": {
8 | "*": {
9 | "*": [
10 | "./src/*"
11 | ]
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/stores/index.ts:
--------------------------------------------------------------------------------
1 | import { useTabsStore } from '@/stores/tabs';
2 | import { useUserStore } from '@/stores/user';
3 | import { usePublicStore } from './public';
4 | import { useMenuStore } from './menu';
5 |
6 | export { useTabsStore, useUserStore, usePublicStore, useMenuStore };
7 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "antfu.iconify",
4 | "voorjaar.windicss-intellisense",
5 | "streetsidesoftware.code-spell-checker",
6 | "lokalise.i18n-ally",
7 | "esbenp.prettier-vscode",
8 | "usernamehw.errorlens"
9 | ]
10 | }
11 |
--------------------------------------------------------------------------------
/.env.development:
--------------------------------------------------------------------------------
1 | VITE_ENV = "development"
2 |
3 | # 端口号
4 | VITE_SERVER_PORT = 7000
5 |
6 | # 跨域
7 | VITE_PROXY = [["/api", "https://mock.mengxuegu.com/mock/63f830b1c5a76a117cab185e/v1"], ["/test", "https://www.baidu.com"]]
8 |
9 | # VITE_PROXY = [["/api", "http://127.0.0.1:8000/"]]
10 |
--------------------------------------------------------------------------------
/packages/stylelintConfig/README.md:
--------------------------------------------------------------------------------
1 | ## 💻 安装使用
2 |
3 | - 安装依赖
4 | ```bash
5 | pnpm install @south/stylelint stylelint -w
6 | ```
7 |
8 | - 配置文件
9 | 根目录创建`stylelint.config.mjs`文件:
10 | ```ts
11 | export default {
12 | extends: ['@south/stylelint'],
13 | root: true,
14 | };
15 | ```
16 |
--------------------------------------------------------------------------------
/src/assets/fonts/font.less:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: YouSheBiaoTiHei;
3 | src: url('./YouSheBiaoTiHei.ttf');
4 | }
5 |
6 | @font-face {
7 | font-family: MetroDF;
8 | src: url('./MetroDF.ttf');
9 | }
10 |
11 | @font-face {
12 | font-family: DIN;
13 | src: url('./DIN.Otf');
14 | }
15 |
--------------------------------------------------------------------------------
/src/utils/permissions.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 检测是否有权限
3 | * @param value - 检测值
4 | * @param permissions - 权限
5 | */
6 | export const checkPermission = (value: string, permissions: string[]): boolean => {
7 | if (!permissions || permissions.length === 0) return false;
8 | return permissions.includes(value);
9 | };
10 |
--------------------------------------------------------------------------------
/src/components/Selects/components/Loading.tsx:
--------------------------------------------------------------------------------
1 | import { Spin } from 'antd';
2 |
3 | function Loading() {
4 | return (
5 |
6 |
7 |
8 | );
9 | }
10 |
11 | export default Loading;
12 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "semi": true,
4 | "bracketSpacing": true,
5 | "trailingComma": "all",
6 | "printWidth": 100,
7 | "tabWidth": 2,
8 | "useTabs": false,
9 | "endOfLine": "lf",
10 | "jsxBracketSameLine": false,
11 | "jsxSingleAttributePerLine": true,
12 | "arrowParens": "always"
13 | }
14 |
--------------------------------------------------------------------------------
/src/locales/zh/dashboard.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | title: '数据展览',
3 | rechargeRankingDay: '当日充值排行',
4 | rechargeAmount: '充值数',
5 | usersNumber: '用户数',
6 | orderNumber: '订单数',
7 | gameNumber: '游戏数',
8 | gameID: '游戏ID',
9 | effectiveRechargeRatio: '有效充值占比',
10 | cooperativeCompany: '合作公司',
11 | fullServerRecharge: '全服充值',
12 | };
13 |
--------------------------------------------------------------------------------
/src/components/Dates/index.ts:
--------------------------------------------------------------------------------
1 | import BaseDatePicker from './components/BaseDatePicker';
2 | import BaseRangePicker from './components/BaseRangePicker';
3 | import BaseTimePicker from './components/BaseTimePicker';
4 | import BaseTimeRangePicker from './components/BaseTimeRangePicker';
5 |
6 | export * from './utils/helper';
7 | export { BaseDatePicker, BaseRangePicker, BaseTimePicker, BaseTimeRangePicker };
8 |
--------------------------------------------------------------------------------
/src/locales/zh/systems/menu.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | label: '中文菜单',
3 | labelEn: '英文菜单',
4 | icon: '图标',
5 | router: '路由',
6 | sort: '排序',
7 | rule: '权限标识',
8 | catalog: '目录',
9 | menu: '菜单',
10 | button: '按钮',
11 | parentMenu: '上级菜单',
12 | addChildMenu: '新增下级',
13 | helpIcon: '点击问号可跳转查询icon,查询完将icon name值传入输入框中',
14 | changeState: '切换状态',
15 | changeStateMsg: '是否将{{name}}改为【{{state}}】状态?',
16 | };
17 |
--------------------------------------------------------------------------------
/src/servers/platform/game.ts:
--------------------------------------------------------------------------------
1 | import { request } from '@/utils/request';
2 |
3 | enum API {
4 | COMMON_URL = '/authority/common',
5 | }
6 |
7 | interface Result {
8 | id: string;
9 | name: string;
10 | children?: Result[];
11 | }
12 |
13 | /**
14 | * 获取游戏数据
15 | * @param data - 请求数据
16 | */
17 | export function getGames(data?: unknown) {
18 | return request.get(`${API.COMMON_URL}/games`, { params: data });
19 | }
20 |
--------------------------------------------------------------------------------
/packages/utils/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@south/utils",
3 | "version": "0.0.1",
4 | "exports": {
5 | ".": "./src/index.ts"
6 | },
7 | "typesVersions": {
8 | "*": {
9 | "*": [
10 | "./src/*"
11 | ]
12 | }
13 | },
14 | "dependencies": {
15 | "@south/message": "workspace:^",
16 | "crypto-js": "^4.2.0"
17 | },
18 | "devDependencies": {
19 | "@types/crypto-js": "^4.2.2"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/components/Selects/index.ts:
--------------------------------------------------------------------------------
1 | import BaseSelect from './BaseSelect';
2 | import BaseTreeSelect from './BaseTreeSelect';
3 | import ApiSelect from './ApiSelect';
4 | import ApiTreeSelect from './ApiTreeSelect';
5 | import ApiPageSelect from './ApiPageSelect';
6 |
7 | export const MAX_TAG_COUNT = 'responsive'; // 最多显示多少个标签,responsive:自适应
8 |
9 | export { BaseSelect, BaseTreeSelect, ApiSelect, ApiTreeSelect, ApiPageSelect };
10 | export type * from './types';
11 |
--------------------------------------------------------------------------------
/src/components/Table/index.less:
--------------------------------------------------------------------------------
1 | .react-resizable {
2 | position: relative;
3 | background-clip: padding-box;
4 | }
5 |
6 | .react-resizable-handle {
7 | position: absolute;
8 | right: 0;
9 | bottom: 0;
10 | z-index: 1;
11 | width: 10px;
12 | height: 100%;
13 | cursor: col-resize;
14 | }
15 |
16 | .ant-table-body,
17 | .ant-table-container {
18 | overflow: auto !important;
19 | scrollbar-color: auto;
20 | scrollbar-width: auto;
21 | }
22 |
--------------------------------------------------------------------------------
/src/components/Business/index.tsx:
--------------------------------------------------------------------------------
1 | import { addComponent } from '../Form/utils/componentMap';
2 |
3 | // 自定义组件名
4 | export type BusinessComponents = 'GameSelect' | 'PartnerSelect';
5 |
6 | /** 组件注入 */
7 | export function CreateBusiness() {
8 | addComponent(
9 | 'GameSelect',
10 | lazy(() => import('./Selects/GameSelect')),
11 | );
12 | addComponent(
13 | 'PartnerSelect',
14 | lazy(() => import('./Selects/PartnerSelect')),
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/src/locales/en/dashboard.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | title: 'Dashboard',
3 | rechargeRankingDay: 'Recharge ranking of the day',
4 | rechargeAmount: 'Recharge amount',
5 | usersNumber: 'Number of users',
6 | orderNumber: 'Number of order',
7 | gameNumber: 'Number of games',
8 | gameID: 'game ID',
9 | effectiveRechargeRatio: 'Effective recharge ratio',
10 | cooperativeCompany: 'Cooperative company',
11 | fullServerRecharge: 'Full server recharge',
12 | };
13 |
--------------------------------------------------------------------------------
/src/assets/css/theme-color.less:
--------------------------------------------------------------------------------
1 | @import url('./default.less');
2 | @import url('./theme.less');
3 |
4 | // 默认
5 | .theme-primary {
6 | .changeTheme(
7 | @primary-color,
8 | @primary-bg,
9 | @layout-content-bg,
10 | @content-bg,
11 | @svg-color
12 | );
13 | }
14 |
15 | // 暗黑主题
16 | .theme-dark {
17 | .changeTheme(
18 | @dark-color,
19 | @dark-bg,
20 | @dark-layout-content-bg,
21 | @dark-content-bg,
22 | @dark-svg-color
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/src/components/Dates/components/BaseTimePicker.tsx:
--------------------------------------------------------------------------------
1 | import type { TimePickerProps } from 'antd';
2 | import { TimePicker } from 'antd';
3 | import { string2Dayjs } from '../utils/helper';
4 |
5 | function BaseTimePicker(props: TimePickerProps) {
6 | const { value } = props;
7 | const params = { ...props };
8 |
9 | // 如果值不是dayjs类型则进行转换
10 | if (value) params.value = string2Dayjs(value);
11 |
12 | return ;
13 | }
14 |
15 | export default BaseTimePicker;
16 |
--------------------------------------------------------------------------------
/src/components/Upload/BaseUpload.tsx:
--------------------------------------------------------------------------------
1 | import { Button, Upload, type UploadProps } from 'antd';
2 |
3 | function BaseUpload(props: UploadProps) {
4 | const { t } = useTranslation();
5 | const [getToken] = useToken();
6 | const token = getToken();
7 |
8 | return (
9 |
10 |
11 |
12 | );
13 | }
14 |
15 | export default BaseUpload;
16 |
--------------------------------------------------------------------------------
/src/pages/content/article/components/CustomizeInput.tsx:
--------------------------------------------------------------------------------
1 | import type { InputProps } from 'antd';
2 | import { Input } from 'antd';
3 |
4 | /**
5 | * 自定义输入
6 | */
7 | function CustomizeInput(props: InputProps) {
8 | const { t } = useTranslation();
9 |
10 | return (
11 | <>
12 |
13 | {t('content.sensitiveInfo')}
14 | >
15 | );
16 | }
17 |
18 | export default CustomizeInput;
19 |
--------------------------------------------------------------------------------
/src/pages/login/model.ts:
--------------------------------------------------------------------------------
1 | // 接口传入数据
2 | export interface LoginData {
3 | username: string;
4 | password: string;
5 | }
6 |
7 | // 用户数据
8 | interface User {
9 | id: number;
10 | username: string;
11 | phone: string;
12 | email: string;
13 | roles: number[];
14 | }
15 |
16 | // 用户权限数据
17 | interface Roles {
18 | id: string;
19 | }
20 |
21 | // 接口返回数据
22 | export interface LoginResult {
23 | token: string;
24 | user: User;
25 | permissions: string[];
26 | roles: Roles[];
27 | }
28 |
--------------------------------------------------------------------------------
/src/locales/zh/system.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | menuTitle: '菜单管理',
3 | userTitle: '用户管理',
4 | permissions: '权限',
5 | authorizationSuccessful: '授权成功',
6 | state: '状态',
7 | module: '模块',
8 | permissionButton: '权限按钮',
9 | age: '年龄',
10 | role: '角色',
11 | phone: '手机',
12 | email: '邮箱',
13 | rightsProfile: '权限配置',
14 | create: '创建',
15 | update: '更新',
16 | delete: '删除',
17 | detail: '详情',
18 | export: '导出',
19 | status: '状态',
20 | description: '描述',
21 | authorize: '授权',
22 | };
23 |
--------------------------------------------------------------------------------
/src/assets/css/scrollbar.less:
--------------------------------------------------------------------------------
1 | /* 修改滚动条样式 */
2 | ::-webkit-scrollbar {
3 | width: 8px;
4 | height: 8px;
5 | background: hsl(0deg 0% 70% / 10%);
6 | }
7 |
8 | ::-webkit-scrollbar-thumb {
9 | background: transparent;
10 | border-radius: 4px;
11 | }
12 |
13 | :hover::-webkit-scrollbar-thumb {
14 | background: hsl(0deg 0% 53% / 40%);
15 | }
16 |
17 | :hover::-webkit-scrollbar-track {
18 | background: hsl(0deg 0% 53% / 10%);
19 | }
20 |
21 | ::-webkit-scrollbar-corner {
22 | background: rgb(0 0 0 / 0%);
23 | }
24 |
--------------------------------------------------------------------------------
/src/assets/css/default.less:
--------------------------------------------------------------------------------
1 | @layout-top: 4.8rem;
2 |
3 | @layout-left: 15rem;
4 |
5 | @layout-left-close: 5rem;
6 |
7 | @bg: #f6f9f8;
8 |
9 | // 默认颜色
10 | @primary-bg: #fff;
11 | @content-bg: #fff;
12 |
13 | @layout-content-bg: #f6f9f8;
14 |
15 | @primary-color: rgba(0, 0, 0, 0.85);
16 |
17 | @svg-color: #00000073;
18 |
19 | // 黑暗主题
20 | @dark-bg: #18181c;
21 | @dark-content-bg: #18181c;
22 |
23 | @dark-layout-content-bg: #000;
24 |
25 | @dark-color: rgb(153, 153, 153);
26 |
27 | @dark-svg-color: rgb(153, 153, 153);
28 |
--------------------------------------------------------------------------------
/.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 | package-lock.json
10 | pnpm-lock.yaml
11 | yarn.lock
12 |
13 | node_modules
14 | dist
15 | dist-ssr
16 | *.local
17 |
18 | # Editor directories and files
19 | .idea
20 | .DS_Store
21 | *.suo
22 | *.ntvs*
23 | *.njsproj
24 | *.sln
25 | *.sw?
26 |
27 | vite.config.ts.*.mjs
28 | __unconfig_vite.config.ts
29 |
30 | # 测试覆盖率
31 | coverage/
32 |
33 | stats.html
34 | .vite
35 | types/autoImports.d.ts
--------------------------------------------------------------------------------
/src/components/Business/Selects/PartnerSelect.tsx:
--------------------------------------------------------------------------------
1 | import type { SelectProps } from 'antd';
2 | import { getPartner } from '@/servers/platform/partner';
3 | import { ApiSelect } from '@/components/Selects';
4 |
5 | /**
6 | * @description: 合作公司下拉组件
7 | */
8 | function PartnerSelect(props: SelectProps) {
9 | return (
10 |
16 | );
17 | }
18 |
19 | export default PartnerSelect;
20 |
--------------------------------------------------------------------------------
/src/components/Dates/components/BaseDatePicker.tsx:
--------------------------------------------------------------------------------
1 | import type { Dayjs } from 'dayjs';
2 | import type { DatePickerProps } from 'antd';
3 | import { DatePicker } from 'antd';
4 | import { string2Dayjs } from '../utils/helper';
5 |
6 | function BaseDatePicker(props: DatePickerProps) {
7 | const { value } = props;
8 | const params = { ...props };
9 |
10 | // 如果值不是dayjs类型则进行转换
11 | if (value) params.value = string2Dayjs(value as Dayjs);
12 |
13 | return ;
14 | }
15 |
16 | export default BaseDatePicker;
17 |
--------------------------------------------------------------------------------
/src/locales/zh/content.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | articleTitle: '文章管理',
3 | contentTitle: '内容管理',
4 | clipboard: '剪切板',
5 | clipboardMessage: '将“admin”传入复制按钮中',
6 | richText: '富文本',
7 | threeTierStructure: '三层结构',
8 | virtualScroll: '虚拟滚动',
9 | virtualScroll1: '虚拟滚动列表(10000条)',
10 | virtualScroll2: '虚拟滚动表格(10000条)',
11 | watermark: '水印',
12 | openWatermark: '打开水印',
13 | hideWatermark: '隐藏水印',
14 | nestedData: '嵌套数据',
15 | sensitiveInfo: '注:标题不能含有敏感信息!',
16 | creator: '创建者',
17 | updater: '更新者',
18 | author: '作者',
19 | };
20 |
--------------------------------------------------------------------------------
/src/components/Business/Selects/GameSelect.tsx:
--------------------------------------------------------------------------------
1 | import type { SelectProps } from 'antd';
2 | import { getGames } from '@/servers/platform/game';
3 | import { ApiSelect } from '@/components/Selects';
4 |
5 | /**
6 | * @description: 游戏下拉组件
7 | */
8 | function GameSelect(props: SelectProps) {
9 | return (
10 | <>
11 |
17 | >
18 | );
19 | }
20 |
21 | export default GameSelect;
22 |
--------------------------------------------------------------------------------
/src/menus/index.ts:
--------------------------------------------------------------------------------
1 | import type { SideMenu } from '#/public';
2 | import { demo } from './demo';
3 |
4 | /**
5 | * 弃用,改为动态菜单获取,如果需要静态菜单将/src/hooks/useCommonStore.ts中的useCommonStore中的menuList改为defaultMenus
6 | * import { defaultMenus } from '@/menus';
7 | * // 菜单数据
8 | * const menuList = defaultMenus;
9 | */
10 | export const defaultMenus: SideMenu[] = [
11 | {
12 | label: '仪表盘',
13 | labelEn: 'Dashboard',
14 | icon: 'la:tachometer-alt',
15 | key: '/dashboard',
16 | rule: '/dashboard',
17 | },
18 | ...(demo as SideMenu[]),
19 | ];
20 |
--------------------------------------------------------------------------------
/src/components/Dates/components/BaseTimeRangePicker.tsx:
--------------------------------------------------------------------------------
1 | import type { TimeRangePickerProps } from 'antd';
2 | import { TimePicker } from 'antd';
3 | import { stringRang2DayjsRang } from '../utils/helper';
4 |
5 | const { RangePicker } = TimePicker;
6 |
7 | function BaseTimePicker(props: TimeRangePickerProps) {
8 | const { value } = props;
9 | const params = { ...props };
10 |
11 | // 如果值不是dayjs类型则进行转换
12 | if (value) params.value = stringRang2DayjsRang(value);
13 |
14 | return ;
15 | }
16 |
17 | export default BaseTimePicker;
18 |
--------------------------------------------------------------------------------
/src/components/Dates/components/BaseRangePicker.tsx:
--------------------------------------------------------------------------------
1 | import { DatePicker } from 'antd';
2 | import type { RangePickerProps } from 'antd/es/date-picker';
3 | import { stringRang2DayjsRang } from '../utils/helper';
4 |
5 | const { RangePicker } = DatePicker;
6 |
7 | function BaseRangePicker(props: RangePickerProps) {
8 | const { value } = props;
9 | const params = { ...props };
10 |
11 | // 如果值不是dayjs类型则进行转换
12 | if (value) params.value = stringRang2DayjsRang(value);
13 |
14 | return ;
15 | }
16 |
17 | export default BaseRangePicker;
18 |
--------------------------------------------------------------------------------
/src/components/Table/utils/state.ts:
--------------------------------------------------------------------------------
1 | import type { Dispatch } from 'react';
2 | import { createContext } from 'react';
3 | import { TableAction } from './reducer';
4 |
5 | interface ScrollContextProps {
6 | dispatch?: Dispatch;
7 | renderLen: number;
8 | start: number;
9 | offsetStart: number;
10 | rowHeight: number;
11 | totalLen: number;
12 | }
13 |
14 | export const ScrollContext = createContext({
15 | dispatch: undefined,
16 | renderLen: 1,
17 | start: 0,
18 | offsetStart: 0,
19 | rowHeight: 46,
20 | totalLen: 0,
21 | });
22 |
--------------------------------------------------------------------------------
/src/components/Card/BaseCard.tsx:
--------------------------------------------------------------------------------
1 | import { HTMLAttributes } from 'react';
2 |
3 | function BaseCard(props: HTMLAttributes) {
4 | const { children, className } = props;
5 |
6 | return (
7 |
22 | {children}
23 |
24 | );
25 | }
26 |
27 | export default BaseCard;
28 |
--------------------------------------------------------------------------------
/src/locales/en/systems/menu.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | label: 'Label',
3 | labelEn: 'LabelEn',
4 | icon: 'Icon',
5 | router: 'Router',
6 | sort: 'Sort',
7 | rule: 'Rule',
8 | catalog: 'Catalog',
9 | menu: 'Menu',
10 | button: 'Button',
11 | parentMenu: 'Parent Menu',
12 | addChildMenu: 'Add a new level',
13 | helpIcon:
14 | 'Click the question mark to jump to the icon query, and after the query, pass the icon name value into the input box',
15 | changeState: 'Switch state',
16 | changeStateMsg: 'Do you want to change {{name}} to [{{state}}] state?',
17 | };
18 |
--------------------------------------------------------------------------------
/src/components/Github/index.tsx:
--------------------------------------------------------------------------------
1 | import { Tooltip } from 'antd';
2 | import { Icon } from '@iconify/react';
3 |
4 | function Github() {
5 | /** 跳转Github */
6 | const goGithub = () => {
7 | window.open('https://github.com/southliu/react-admin');
8 | };
9 |
10 | return (
11 |
12 |
13 |
17 |
18 |
19 | );
20 | }
21 |
22 | export default Github;
23 |
--------------------------------------------------------------------------------
/src/servers/platform/partner.ts:
--------------------------------------------------------------------------------
1 | import { request } from '@/utils/request';
2 |
3 | enum API {
4 | URL = '/platform/partner',
5 | }
6 |
7 | interface Result {
8 | id: string;
9 | name: string;
10 | }
11 |
12 | /**
13 | * 获取公司数据
14 | * @param data - 请求数据
15 | */
16 | export function getPartner(data?: unknown) {
17 | return request.get(API.URL, { params: data });
18 | }
19 |
20 | /**
21 | * 获取公司数据-展示用的接口
22 | * @param data - 请求数据
23 | */
24 | export function getPartnerDemo(url: string, data?: unknown) {
25 | return request.get(url, { params: data });
26 | }
27 |
--------------------------------------------------------------------------------
/packages/utils/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "jsx": "preserve",
5 | "lib": ["DOM", "ESNext"],
6 | "baseUrl": ".",
7 | "module": "ESNext",
8 | "moduleResolution": "node",
9 | "resolveJsonModule": true,
10 | "types": ["node"],
11 | "strict": true,
12 | "strictNullChecks": true,
13 | "noUnusedLocals": true,
14 | "allowSyntheticDefaultImports": true,
15 | "esModuleInterop": true,
16 | "forceConsistentCasingInFileNames": true
17 | },
18 | "include": ["src/**/*"],
19 | "exclude": ["node_modules", "dist"]
20 | }
21 |
--------------------------------------------------------------------------------
/packages/message/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "jsx": "preserve",
5 | "lib": ["DOM", "ESNext"],
6 | "baseUrl": ".",
7 | "module": "ESNext",
8 | "moduleResolution": "node",
9 | "resolveJsonModule": true,
10 | "types": ["node"],
11 | "strict": true,
12 | "strictNullChecks": true,
13 | "noUnusedLocals": true,
14 | "allowSyntheticDefaultImports": true,
15 | "esModuleInterop": true,
16 | "forceConsistentCasingInFileNames": true
17 | },
18 | "include": ["src/**/*"],
19 | "exclude": ["node_modules", "dist"]
20 | }
21 |
--------------------------------------------------------------------------------
/packages/request/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "jsx": "preserve",
5 | "lib": ["DOM", "ESNext"],
6 | "baseUrl": ".",
7 | "module": "ESNext",
8 | "moduleResolution": "node",
9 | "resolveJsonModule": true,
10 | "types": ["node"],
11 | "strict": true,
12 | "strictNullChecks": true,
13 | "noUnusedLocals": true,
14 | "allowSyntheticDefaultImports": true,
15 | "esModuleInterop": true,
16 | "forceConsistentCasingInFileNames": true
17 | },
18 | "include": ["src/**/*"],
19 | "exclude": ["node_modules", "dist"]
20 | }
21 |
--------------------------------------------------------------------------------
/src/pages/all.module.less:
--------------------------------------------------------------------------------
1 | .animation {
2 | animation: shake 0.6s ease-in-out infinite alternate;
3 | }
4 |
5 | @keyframes shake {
6 | 0% {
7 | transform: translate(-1px);
8 | }
9 |
10 | 10% {
11 | transform: translate(2px, 1px);
12 | }
13 |
14 | 30% {
15 | transform: translate(-3px, 2px);
16 | }
17 |
18 | 35% {
19 | filter: blur(4px);
20 | transform: translate(2px, -3px);
21 | }
22 |
23 | 45% {
24 | filter: blur(0);
25 | transform: translate(2px, 2px) skewY(-8deg) scaleX(0.96);
26 | }
27 |
28 | 50% {
29 | transform: translate(-3px, 1px);
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/locales/zh/login.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | login: '登录',
3 | systemLogin: '系统登录',
4 | oldPassword: '旧密码',
5 | newPassword: '新密码',
6 | password: '密码',
7 | username: '用户名',
8 | rememberMe: '记住我',
9 | phoneNumber: '手机号码',
10 | verificationCode: '验证码',
11 | getVerificationCode: '获取验证码',
12 | reacquire: '重新获取({{time}})',
13 | phoneNumberError: '请输入有效的11位手机号码',
14 | resetPassword: '重置密码',
15 | verificationPassed: '验证通过',
16 | forgetPassword: '忘记密码?',
17 | confirmPassword: '确认密码',
18 | confirmPasswordMessage: '密码和确认密码不相同!',
19 | notPermissions: '用户暂无权限登录',
20 | passwordRuleMessage: '密码为6-30位必须包含字母和数字!',
21 | };
22 |
--------------------------------------------------------------------------------
/src/hooks/useToken.ts:
--------------------------------------------------------------------------------
1 | import { setLocalInfo, getLocalInfo, removeLocalInfo } from '@south/utils';
2 | import { TOKEN } from '@/utils/config';
3 |
4 | /**
5 | * token存取方法
6 | */
7 | export function useToken() {
8 | /** 获取token */
9 | const getToken = () => {
10 | return getLocalInfo(TOKEN) || '';
11 | };
12 |
13 | /**
14 | * 设置token
15 | * @param value - token值
16 | */
17 | const setToken = (value: string) => {
18 | setLocalInfo(TOKEN, value);
19 | };
20 |
21 | /** 删除token */
22 | const removeToken = () => {
23 | removeLocalInfo(TOKEN);
24 | };
25 |
26 | return [getToken, setToken, removeToken] as const;
27 | }
28 |
--------------------------------------------------------------------------------
/src/pages/demo/level1/level2/level3.tsx:
--------------------------------------------------------------------------------
1 | import BaseContent from '@/components/Content/BaseContent';
2 | import { useCommonStore } from '@/hooks/useCommonStore';
3 | import { checkPermission } from '@/utils/permissions';
4 | import { useTranslation } from 'react-i18next';
5 |
6 | function Page() {
7 | const { t } = useTranslation();
8 | const { permissions } = useCommonStore();
9 | const isPermission = checkPermission('/demo/level', permissions);
10 |
11 | return (
12 |
13 | {t('content.threeTierStructure')}
14 |
15 | );
16 | }
17 |
18 | export default Page;
19 |
--------------------------------------------------------------------------------
/src/servers/login/index.ts:
--------------------------------------------------------------------------------
1 | import type { LoginData, LoginResult } from '@/pages/login/model';
2 | import { request } from '@/utils/request';
3 |
4 | /**
5 | * 登录
6 | * @param data - 请求数据
7 | */
8 | export function login(data: LoginData) {
9 | return request.post('/system/user/login', data);
10 | }
11 |
12 | /**
13 | * 修改密码
14 | * @param data - 请求数据
15 | */
16 | export function updatePassword(data: object) {
17 | return request.post('/system/user/updatePassword', data);
18 | }
19 |
20 | /**
21 | * 忘记密码
22 | * @param data - 请求数据
23 | */
24 | export function forgetPassword(data: object) {
25 | return request.post('/system/user/forgetPassword', data);
26 | }
27 |
--------------------------------------------------------------------------------
/src/hooks/useTime.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState, useRef } from 'react';
2 | import dayjs from 'dayjs';
3 |
4 | /**
5 | * @description 获取本地时间
6 | */
7 | export const useTimes = () => {
8 | const timer = useRef(null);
9 | const [time, setTime] = useState(dayjs().format('YYYY年MM月DD日 HH:mm:ss'));
10 | useEffect(() => {
11 | timer.current = setInterval(() => {
12 | setTime(dayjs().format('YYYY年MM月DD日 HH:mm:ss'));
13 | }, 1000);
14 | return () => {
15 | if (timer.current) {
16 | clearInterval(timer.current as NodeJS.Timeout);
17 | timer.current = null;
18 | }
19 | };
20 | }, [time]);
21 |
22 | return {
23 | time,
24 | };
25 | };
26 |
--------------------------------------------------------------------------------
/src/components/Content/BaseContent.tsx:
--------------------------------------------------------------------------------
1 | import type { ReactNode } from 'react';
2 | import Forbidden from '@/pages/403';
3 |
4 | interface Props {
5 | isPermission?: boolean;
6 | children: ReactNode;
7 | }
8 |
9 | function BaseContent(props: Props) {
10 | const { isPermission, children } = props;
11 |
12 | return (
13 | <>
14 | {isPermission !== false && (
15 |
16 | {children}
17 |
18 | )}
19 | {isPermission === false && (
20 |
21 |
22 |
23 | )}
24 | >
25 | );
26 | }
27 |
28 | export default BaseContent;
29 |
--------------------------------------------------------------------------------
/src/utils/request.ts:
--------------------------------------------------------------------------------
1 | import { TOKEN } from '@/utils/config';
2 | import { creteRequest } from '@south/request';
3 |
4 | // 生成环境所用的接口
5 | const prefixUrl = import.meta.env.VITE_BASE_URL as string;
6 | const baseURL = process.env.NODE_ENV !== 'development' ? prefixUrl : '/api';
7 |
8 | // 请求配置
9 | export const request = creteRequest(baseURL, TOKEN);
10 |
11 | // 创建多个请求
12 | // export const newRequest = creteRequest('/test', TOKEN);
13 |
14 | /**
15 | * 取消请求
16 | * @param url - 链接
17 | */
18 | export const cancelRequest = (url: string | string[]) => {
19 | return request.cancelRequest(url);
20 | };
21 |
22 | /** 取消全部请求 */
23 | export const cancelAllRequest = () => {
24 | return request.cancelAllRequest();
25 | };
26 |
--------------------------------------------------------------------------------
/packages/stylelintConfig/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@south/stylelint",
3 | "version": "0.0.1",
4 | "exports": {
5 | "import": "./index.mjs",
6 | "default": "./index.mjs"
7 | },
8 | "devDependencies": {
9 | "@stylistic/stylelint-plugin": "^3.1.0",
10 | "postcss": "^8.4.38",
11 | "postcss-html": "^1.6.0",
12 | "postcss-less": "^6.0.0",
13 | "prettier": "^3.3.3",
14 | "stylelint-config-recess-order": "^5.1.1",
15 | "stylelint-config-recommended": "^14.0.0",
16 | "stylelint-config-recommended-less": "^3.0.1",
17 | "stylelint-config-standard": "^36.0.0",
18 | "stylelint-less": "^3.0.1",
19 | "stylelint-order": "^6.0.4",
20 | "stylelint-prettier": "^5.0.2"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/locales/en/content.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | articleTitle: 'Article Management',
3 | contentTitle: 'Content Management',
4 | clipboard: 'Clipboard',
5 | clipboardMessage: 'Pass "admin" into the copy button',
6 | richText: 'Rich Text',
7 | threeTierStructure: 'Three-tier structure',
8 | virtualScroll: 'Virtual Scroll',
9 | virtualScroll1: 'virtual scrolling list (10000)',
10 | virtualScroll2: 'virtual scrolling table (10000)',
11 | watermark: 'Watermark',
12 | openWatermark: 'Open watermark',
13 | hideWatermark: 'Hide watermark',
14 | nestedData: 'Nested data',
15 | sensitiveInfo: 'Note: The title cannot contain sensitive information!',
16 | creator: 'Creator',
17 | updater: 'Updater',
18 | author: 'Author',
19 | };
20 |
--------------------------------------------------------------------------------
/src/components/Buttons/components/BaseBtn.tsx:
--------------------------------------------------------------------------------
1 | import type { ReactNode } from 'react';
2 | import type { ButtonProps } from 'antd';
3 | import { Button } from 'antd';
4 |
5 | interface Props extends ButtonProps {
6 | isLoading?: boolean;
7 | children?: ReactNode;
8 | }
9 |
10 | function BaseBtn(props: Props) {
11 | const { isLoading, loading, children, className } = props;
12 |
13 | // 清除自定义属性
14 | const params: Partial = { ...props };
15 | delete params.isLoading;
16 |
17 | return (
18 |
26 | );
27 | }
28 |
29 | export default BaseBtn;
30 |
--------------------------------------------------------------------------------
/.commitlintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["@commitlint/config-conventional"],
3 | "rules": {
4 | "body-leading-blank": [2, "always"],
5 | "footer-leading-blank": [1, "always"],
6 | "header-max-length": [2, "always", 108],
7 | "subject-empty": [2, "never"],
8 | "type-empty": [2, "never"],
9 | "subject-case": [0],
10 | "type-enum": [
11 | 2,
12 | "always",
13 | [
14 | "feat",
15 | "fix",
16 | "perf",
17 | "style",
18 | "docs",
19 | "test",
20 | "refactor",
21 | "build",
22 | "ci",
23 | "chore",
24 | "revert",
25 | "wip",
26 | "workflow",
27 | "types",
28 | "release"
29 | ]
30 | ]
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/components/Buttons/components/UpdateBtn.tsx:
--------------------------------------------------------------------------------
1 | import type { ButtonProps } from 'antd';
2 | import { Button } from 'antd';
3 | import { useTranslation } from 'react-i18next';
4 |
5 | interface Props extends ButtonProps {
6 | isLoading?: boolean;
7 | }
8 |
9 | function UpdateBtn(props: Props) {
10 | const { isLoading, loading, className } = props;
11 | const { t } = useTranslation();
12 |
13 | // 清除自定义属性
14 | const params: Partial = { ...props };
15 | delete params.isLoading;
16 |
17 | return (
18 |
26 | );
27 | }
28 |
29 | export default UpdateBtn;
30 |
--------------------------------------------------------------------------------
/src/hooks/useLogout.ts:
--------------------------------------------------------------------------------
1 | import { useKeepAliveRef } from 'keepalive-for-react';
2 |
3 | /**
4 | * 获取常用的状态数据
5 | */
6 | export const useLogout = () => {
7 | const [, , removeToken] = useToken();
8 | const { closeAllTab, setActiveKey } = useTabsStore((state) => state);
9 | const clearInfo = useUserStore((state) => state.clearInfo);
10 | const navigate = useNavigate();
11 | const location = useLocation();
12 | const aliveRef = useKeepAliveRef();
13 | /** 退出登录 */
14 | const handleLogout = () => {
15 | clearInfo();
16 | closeAllTab();
17 | setActiveKey('');
18 | removeToken();
19 | aliveRef.current?.destroyAll(); // 清除keepalive缓存
20 | navigate(`/login?redirect=${location.pathname}${location.search}`);
21 | };
22 |
23 | return [handleLogout] as const;
24 | };
25 |
--------------------------------------------------------------------------------
/src/pages/demo/editor/index.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import { useCommonStore } from '@/hooks/useCommonStore';
3 | import { checkPermission } from '@/utils/permissions';
4 | import WangEditor from '@/components/WangEditor';
5 | import BaseContent from '@/components/Content/BaseContent';
6 |
7 | function MyEditor() {
8 | const { permissions } = useCommonStore();
9 | // 编辑器内容
10 | const [html, setHtml] = useState('hello
');
11 | const isPermission = checkPermission('/demo/editor', permissions);
12 |
13 | return (
14 |
15 |
16 | setHtml(content)} />
17 |
18 |
19 | );
20 | }
21 |
22 | export default MyEditor;
23 |
--------------------------------------------------------------------------------
/src/hooks/useFullscreen.ts:
--------------------------------------------------------------------------------
1 | import { usePublicStore } from '@/stores/public';
2 | import { useCommonStore } from './useCommonStore';
3 |
4 | export function useFullscreen() {
5 | const { isFullscreen } = useCommonStore();
6 | const setFullscreen = usePublicStore((state) => state.setFullscreen);
7 |
8 | /** 切换全屏 */
9 | const toggleFullscreen = () => {
10 | // 全屏
11 | if (!isFullscreen && document.documentElement?.requestFullscreen) {
12 | document.documentElement.requestFullscreen();
13 | setFullscreen(true);
14 | return true;
15 | }
16 | // 退出全屏
17 | if (isFullscreen && document?.exitFullscreen) {
18 | document.exitFullscreen();
19 | setFullscreen(false);
20 | return true;
21 | }
22 | };
23 |
24 | return [isFullscreen, toggleFullscreen] as const;
25 | }
26 |
--------------------------------------------------------------------------------
/src/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useCallback } from 'react';
2 | import { useNavigate } from 'react-router-dom';
3 | import { getFirstMenu } from '@/menus/utils/helper';
4 | import { useCommonStore } from '@/hooks/useCommonStore';
5 |
6 | function Page() {
7 | const { permissions, menuList } = useCommonStore();
8 | const navigate = useNavigate();
9 |
10 | /** 跳转第一个有效菜单路径 */
11 | const goFirstMenu = useCallback(() => {
12 | const firstMenu = getFirstMenu(menuList, permissions);
13 | navigate(firstMenu);
14 | }, [menuList, navigate, permissions]);
15 |
16 | useEffect(() => {
17 | // 跳转第一个有效菜单路径
18 | goFirstMenu();
19 |
20 | // eslint-disable-next-line react-hooks/exhaustive-deps
21 | }, [menuList, permissions]);
22 |
23 | return ;
24 | }
25 |
26 | export default Page;
27 |
--------------------------------------------------------------------------------
/src/locales/config.ts:
--------------------------------------------------------------------------------
1 | import { initReactI18next } from 'react-i18next';
2 | import { getZhLang, getEnLang, getZhLangNamespaces, getEnLangNamespaces } from './utils/helper';
3 | import i18n from 'i18next';
4 | import Backend from 'i18next-http-backend';
5 | import LanguageDetector from 'i18next-browser-languagedetector';
6 |
7 | i18n
8 | .use(Backend)
9 | .use(LanguageDetector)
10 | .use(initReactI18next)
11 | .init({
12 | debug: true,
13 | fallbackLng: 'zh',
14 | interpolation: {
15 | escapeValue: false,
16 | },
17 | resources: {
18 | zh: {
19 | translation: getZhLang(),
20 | ...getZhLangNamespaces(),
21 | },
22 | en: {
23 | translation: getEnLang(),
24 | ...getEnLangNamespaces(),
25 | },
26 | },
27 | });
28 |
29 | export default i18n;
30 |
--------------------------------------------------------------------------------
/src/utils/is.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 是否是方法
3 | * @param val - 参数
4 | */
5 | export function isFunction(val: unknown): boolean {
6 | return typeof val === 'function';
7 | }
8 |
9 | /**
10 | * 是否是数字
11 | * @param obj - 值
12 | */
13 | export function isNumber(obj: unknown): boolean {
14 | return typeof obj === 'number' && isFinite(obj);
15 | }
16 |
17 | /**
18 | * 是否是URL
19 | * @param path - 路径
20 | */
21 | export function isUrl(path: string): boolean {
22 | const reg =
23 | /(((^https?:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+(?::\d+)?|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)$/;
24 | return reg.test(path);
25 | }
26 |
27 | /**
28 | * 是否是NULL
29 | * @param value - 值
30 | */
31 | export function isNull(value: unknown): boolean {
32 | return value === null;
33 | }
34 |
--------------------------------------------------------------------------------
/src/components/Selects/types.ts:
--------------------------------------------------------------------------------
1 | import type { SelectProps, TreeSelectProps } from 'antd';
2 | import type { ServerResult } from '@south/request';
3 |
4 | export type ApiFn = (params?: object | unknown[]) => Promise>;
5 |
6 | // api参数
7 | interface ApiParam {
8 | api?: ApiFn;
9 | params?: object | unknown[];
10 | apiResultKey?: string;
11 | }
12 |
13 | // 带分页的api参数
14 | interface ApiPageParam extends Omit {
15 | pageKey?: string;
16 | pageSizeKey?: string;
17 | queryKey?: string;
18 | page?: number;
19 | pageSize?: number;
20 | params?: object & {
21 | [key: string]: number;
22 | };
23 | }
24 |
25 | export type ApiSelectProps = ApiParam & SelectProps;
26 |
27 | export type ApiTreeSelectProps = ApiParam & TreeSelectProps;
28 |
29 | export type ApiPageSelectProps = ApiPageParam & SelectProps;
30 |
--------------------------------------------------------------------------------
/src/components/Transfer/BaseTransfer.tsx:
--------------------------------------------------------------------------------
1 | import type { TransferProps } from 'antd';
2 | import type { TransferItem } from 'antd/es/transfer';
3 | import { useState } from 'react';
4 | import { Transfer } from 'antd';
5 |
6 | interface Props {
7 | value: string[];
8 | onChange: (value: string[]) => void;
9 | }
10 |
11 | function BaseTransfer(props: Props) {
12 | const { value } = props;
13 | const [targetKeys, setTargetKeys] = useState(value || []);
14 |
15 | /**
16 | * 更改数据
17 | * @param targetKeys - 显示在右侧框数据的key集合
18 | */
19 | const onChange: TransferProps['onChange'] = (targetKeys) => {
20 | setTargetKeys(targetKeys as string[]);
21 | props?.onChange?.(targetKeys as string[]);
22 | };
23 |
24 | return ;
25 | }
26 |
27 | export default BaseTransfer;
28 |
--------------------------------------------------------------------------------
/src/locales/en/system.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | menuTitle: 'Menu Management',
3 | userTitle: 'User Management',
4 | permissions: 'Permissions',
5 | authorizationSuccessful: 'Authorization successful',
6 | state: 'state',
7 | module: 'module',
8 | controller: 'Controller',
9 | permissionButton: 'Permission Button',
10 | age: 'age',
11 | role: 'role',
12 | email: 'email',
13 | phone: 'phone',
14 | rightsProfile: 'Rights Profile',
15 | authority: 'authority system',
16 | platform: 'operating system',
17 | stat: 'statistical system',
18 | ad: 'delivery system',
19 | cs: 'customer Service System',
20 | log: 'log system',
21 | create: 'create',
22 | update: 'update',
23 | delete: 'delete',
24 | detail: 'details',
25 | export: 'export',
26 | status: 'status',
27 | description: 'description',
28 | authorize: 'authorize',
29 | };
30 |
--------------------------------------------------------------------------------
/src/menus/README.md:
--------------------------------------------------------------------------------
1 | ### 菜单路由说明
2 | * 顶级key使用顶级目录名
3 | * 次级都采用`/顶级key/当前目录/当前页`
4 | * 菜单key为跳转路由地址,需与文件目录结构相符
5 |
6 | ### 菜单路由key:
7 | ```
8 | ├─ 顶级Key
9 | | └─ /顶级key/当前目录
10 | | └─ /顶级key/当前目录/当前页
11 | ├─ system
12 | | ├─ /system/user
13 | | └─ /system/menu
14 | └─ demo
15 | ├─ /demo/test
16 | └─ /demo/level1
17 | └─ /demo/level1/level2
18 | └─ /demo/level1/level3
19 | ```
20 |
21 | ### 静态菜单方法:
22 | 如果需要静态菜单将/src/hooks/useCommonStore.ts中的useCommonStore中的menuList改为defaultMenus。
23 | ```js
24 | // src/hooks/useCommonStore.ts
25 | import { defaultMenus } from '@/menus';
26 |
27 | // const menuList = useMenuStore(state => state.menuList);
28 | // 菜单数据
29 | const menuList = defaultMenus;
30 | ```
31 |
32 | ### 菜单icon:
33 | 参考 [iconify官方地址](https://icon-sets.iconify.design/)
34 |
35 | ### 外链菜单:
36 | 将key设为一个url地址,前缀为`http`或`https`,则视为外链菜单,点击后直接跳转。
37 |
--------------------------------------------------------------------------------
/src/hooks/useSearchUrlParams.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 将搜索参数带入url中
3 | */
4 |
5 | export const useSearchUrlParams = () => {
6 | const [, setSearchParams] = useSearchParams();
7 | const { pathname } = useLocation();
8 | const { setTabs } = useTabsStore((state) => state);
9 |
10 | const handleSetSearchParams = (searchParams: BaseFormData) => {
11 | // 去除 values 中值为 undefined 的属性
12 | const filteredValues = Object.fromEntries(
13 | Object.entries(searchParams).filter(([, value]) => value !== undefined),
14 | ) as Record;
15 |
16 | // 将对象转换为 url 参数字符串
17 | let urlParams = new URLSearchParams(filteredValues).toString();
18 | if (urlParams?.length) {
19 | urlParams = `?${urlParams}`;
20 | }
21 |
22 | setSearchParams(filteredValues);
23 | setTabs(pathname, urlParams);
24 | };
25 |
26 | return [handleSetSearchParams];
27 | };
28 |
--------------------------------------------------------------------------------
/src/pages/demo/[id]/dynamic/index.tsx:
--------------------------------------------------------------------------------
1 | import { useParams } from 'react-router-dom';
2 | import { useCommonStore } from '@/hooks/useCommonStore';
3 | import { checkPermission } from '@/utils/permissions';
4 | import BaseCard from '@/components/Card/BaseCard';
5 | import BaseContent from '@/components/Content/BaseContent';
6 |
7 | function Dynamic() {
8 | const { id } = useParams();
9 | const { permissions } = useCommonStore();
10 | const isPermission = checkPermission('/demo/dynamic', permissions);
11 |
12 | return (
13 |
14 |
15 | /demo/123/dynamic中的123为动态参数,可自由修改,文件路径为:/demo/[id]/dynamic。
16 |
17 | id: {id}
18 |
19 |
20 |
21 | );
22 | }
23 |
24 | export default Dynamic;
25 |
--------------------------------------------------------------------------------
/src/layouts/components/TabRefresh.tsx:
--------------------------------------------------------------------------------
1 | import { Tooltip } from 'antd';
2 | import { Icon } from '@iconify/react';
3 | import { useTranslation } from 'react-i18next';
4 |
5 | interface Props {
6 | isRefresh: boolean;
7 | onClick: () => void;
8 | }
9 |
10 | function TabRefresh(props: Props) {
11 | const { t } = useTranslation();
12 | const { isRefresh, onClick } = props;
13 |
14 | return (
15 |
16 | onClick()}
27 | icon="ant-design:reload-outlined"
28 | />
29 |
30 | );
31 | }
32 |
33 | export default TabRefresh;
34 |
--------------------------------------------------------------------------------
/src/locales/en/login.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | login: 'Login',
3 | systemLogin: 'System Login',
4 | oldPassword: 'Old Password',
5 | newPassword: 'New Password',
6 | password: 'Password',
7 | username: 'Username',
8 | phoneNumber: 'Phone Number',
9 | verificationCode: 'Verification Code',
10 | getVerificationCode: 'Verification',
11 | reacquire: 'Reacquire({{time}})',
12 | phoneNumberError: 'Please enter a valid 11-digit phone number',
13 | rememberMe: 'Remember Me',
14 | resetPassword: 'Reset Password',
15 | verificationPassed: 'Verification Passed',
16 | forgetPassword: 'Forget Password?',
17 | confirmPassword: 'Confirm Password',
18 | confirmPasswordMessage: 'Password and confirmation password are not the same!',
19 | notPermissions: 'The user has no permission to log in',
20 | passwordRuleMessage: 'The password is 6-30 characters and must contain letters and numbers!',
21 | };
22 |
--------------------------------------------------------------------------------
/src/components/PasswordStrength/components/StrengthBar.tsx:
--------------------------------------------------------------------------------
1 | interface Props {
2 | strength: number;
3 | }
4 |
5 | const arr = new Array(5).fill(0).map((_, index) => index + 1);
6 |
7 | function StrengthBar(props: Props) {
8 | const { strength } = props;
9 |
10 | return (
11 |
12 | {arr.map((item) => (
13 |
3 ? '!bg-green-400' : ''}
23 | ${item <= strength && strength === 3 ? '!bg-yellow-400' : ''}
24 | ${item <= strength && strength < 3 ? '!bg-red-400' : ''}
25 | `}
26 | >
27 | ))}
28 |
29 | );
30 | }
31 |
32 | export default StrengthBar;
33 |
--------------------------------------------------------------------------------
/src/components/Pagination/BasePagination.tsx:
--------------------------------------------------------------------------------
1 | import type { PaginationProps } from 'antd';
2 | import { Pagination } from 'antd';
3 | import { useTranslation } from 'react-i18next';
4 | import './index.less';
5 |
6 | function BasePagination(props: PaginationProps) {
7 | const { t } = useTranslation();
8 |
9 | /**
10 | * 显示总数
11 | * @param total - 总数
12 | */
13 | const showTotal = (total?: number): string => {
14 | return t('public.totalNum', { num: total || 0 });
15 | };
16 |
17 | return (
18 |
32 | );
33 | }
34 |
35 | export default BasePagination;
36 |
--------------------------------------------------------------------------------
/src/components/Selects/BaseTreeSelect.tsx:
--------------------------------------------------------------------------------
1 | import { TreeSelect, type TreeSelectProps } from 'antd';
2 | import { useTranslation } from 'react-i18next';
3 | import { MAX_TAG_COUNT } from './index';
4 |
5 | function BaseTreeSelect(props: TreeSelectProps) {
6 | const { treeData } = props;
7 | const { t } = useTranslation();
8 |
9 | const currentTreeData =
10 | treeData?.map((item) => {
11 | // 如果数组不是对象,则拼接数组
12 | if (typeof item !== 'object') {
13 | return { label: item, value: item };
14 | }
15 | return item;
16 | }) || [];
17 |
18 | return (
19 |
28 | );
29 | }
30 |
31 | export default BaseTreeSelect;
32 |
--------------------------------------------------------------------------------
/src/layouts/components/TabMaximize.tsx:
--------------------------------------------------------------------------------
1 | import { Icon } from '@iconify/react';
2 | import { useCommonStore } from '@/hooks/useCommonStore';
3 | import { useTabsStore } from '@/stores';
4 |
5 | function TabMaximize() {
6 | // 是否窗口最大化
7 | const { isMaximize } = useCommonStore();
8 | const toggleMaximize = useTabsStore((state) => state.toggleMaximize);
9 |
10 | /** 点击最大化/最小化 */
11 | const onClick = () => {
12 | toggleMaximize(!isMaximize);
13 | };
14 |
15 | return (
16 |
17 |
22 |
23 |
28 |
29 | );
30 | }
31 |
32 | export default TabMaximize;
33 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "useDefineForClassFields": true,
5 | "lib": ["DOM", "DOM.Iterable", "ESNext"],
6 | "allowJs": false,
7 | "skipLibCheck": true,
8 | "esModuleInterop": false,
9 | "allowSyntheticDefaultImports": true,
10 | "strict": true,
11 | "forceConsistentCasingInFileNames": true,
12 | "module": "ESNext",
13 | "moduleResolution": "Bundler",
14 | "resolveJsonModule": true,
15 | "isolatedModules": true,
16 | "noUnusedLocals": true, // 有未使用的变量时,抛出错误
17 | "noEmit": true,
18 | "jsx": "react-jsx",
19 | "baseUrl": ".",
20 | "types": ["vite/client", "node"],
21 | "paths": {
22 | "@/*": ["src/*"],
23 | "#/*": ["types/*"]
24 | }
25 | },
26 | "include": ["src", "packages/*", "types/**/*.d.ts"],
27 | "exclude": ["dist", "node_modules", "cypress"],
28 | "references": [{ "path": "./tsconfig.node.json" }]
29 | }
30 |
--------------------------------------------------------------------------------
/src/components/Selects/BaseSelect.tsx:
--------------------------------------------------------------------------------
1 | import { Select, type SelectProps } from 'antd';
2 | import { useTranslation } from 'react-i18next';
3 | import { MAX_TAG_COUNT } from './index';
4 |
5 | /**
6 | * @description: 基础下拉组件
7 | */
8 | function BaseSelect(props: SelectProps) {
9 | const { options } = props;
10 | const { t } = useTranslation();
11 |
12 | const currentOptions =
13 | options?.map((item) => {
14 | // 如果数组不是对象,则拼接数组
15 | if (typeof item !== 'object') {
16 | return { label: item, value: item };
17 | }
18 | return item;
19 | }) || [];
20 |
21 | return (
22 |
31 | );
32 | }
33 |
34 | export default BaseSelect;
35 |
--------------------------------------------------------------------------------
/packages/request/src/types.ts:
--------------------------------------------------------------------------------
1 | import type { AxiosResponse, InternalAxiosRequestConfig, CreateAxiosDefaults, Cancel } from 'axios';
2 |
3 | export interface RequestCancel extends Cancel {
4 | data: object;
5 | response: {
6 | status: number;
7 | data: {
8 | code?: number;
9 | message?: string;
10 | };
11 | };
12 | }
13 |
14 | export interface RequestInterceptors {
15 | // 请求拦截
16 | requestInterceptors?: (config: InternalAxiosRequestConfig) => InternalAxiosRequestConfig;
17 | requestInterceptorsCatch?: (err: RequestCancel) => void;
18 | // 响应拦截
19 | responseInterceptors?: (config: T) => T;
20 | responseInterceptorsCatch?: (err: RequestCancel) => void;
21 | }
22 |
23 | // 自定义传入的参数
24 | export interface CreateRequestConfig extends CreateAxiosDefaults {
25 | interceptors?: RequestInterceptors;
26 | }
27 |
28 | // 接口响应数据
29 | export interface ServerResult {
30 | code: number;
31 | message?: string;
32 | data: T;
33 | }
34 |
--------------------------------------------------------------------------------
/src/components/Fullscreen/index.tsx:
--------------------------------------------------------------------------------
1 | import { Tooltip } from 'antd';
2 | import { Icon } from '@iconify/react';
3 | import { useTranslation } from 'react-i18next';
4 | import { useFullscreen } from '@/hooks/useFullscreen';
5 |
6 | /**
7 | * @description: 全屏组件
8 | */
9 | function Fullscreen() {
10 | const { t } = useTranslation();
11 | const [isFullscreen, toggleFullscreen] = useFullscreen();
12 |
13 | return (
14 |
15 |
19 |
23 |
24 |
25 |
26 | );
27 | }
28 |
29 | export default Fullscreen;
30 |
--------------------------------------------------------------------------------
/packages/message/src/index.ts:
--------------------------------------------------------------------------------
1 | import type { MessageInstance } from 'antd/es/message/interface';
2 | import type { NotificationInstance } from 'antd/es/notification/interface';
3 | import type { ModalStaticFunctions } from 'antd/es/modal/confirm';
4 | import {
5 | message as antdMessage,
6 | notification as antdNotification,
7 | Modal as antdModal,
8 | App,
9 | } from 'antd';
10 |
11 | let message: MessageInstance = antdMessage;
12 | let notification: NotificationInstance = antdNotification;
13 |
14 | const { ...resetFns } = antdModal;
15 | let modal: Omit = resetFns;
16 |
17 | /**
18 | * 该组件提供静态方法
19 | * 作用:跨页面message显示
20 | */
21 | function StaticMessage() {
22 | const staticFunctions = App.useApp();
23 |
24 | message = staticFunctions.message;
25 | notification = staticFunctions.notification;
26 | modal = staticFunctions.modal;
27 |
28 | return null;
29 | }
30 |
31 | export { message, notification, modal };
32 |
33 | export default StaticMessage;
34 |
--------------------------------------------------------------------------------
/src/components/GlobalSearch/index.tsx:
--------------------------------------------------------------------------------
1 | import type { SearchModalProps } from './components/SearchModal';
2 | import { useRef } from 'react';
3 | import { Tooltip } from 'antd';
4 | import { Icon } from '@iconify/react';
5 | import { useTranslation } from 'react-i18next';
6 | import SearchModal from './components/SearchModal';
7 |
8 | /**
9 | * @description: 全局搜索菜单组件
10 | */
11 | function GlobalSearch() {
12 | const { t } = useTranslation();
13 | const modalRef = useRef(null);
14 |
15 | /** 切换显示 */
16 | const toggle = () => {
17 | modalRef.current?.toggle();
18 | };
19 |
20 | return (
21 | <>
22 |
23 |
28 |
29 |
30 |
31 | >
32 | );
33 | }
34 |
35 | export default GlobalSearch;
36 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | 后台管理系统
10 |
11 |
12 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/packages/utils/src/crypto.ts:
--------------------------------------------------------------------------------
1 | import { encrypt, decrypt } from 'crypto-js/aes';
2 | import UTF8 from 'crypto-js/enc-utf8';
3 | import md5 from 'crypto-js/md5';
4 |
5 | /**
6 | * @description: 加密/解密封装,secret值建议从后台接口获取
7 | */
8 | const secretKey = '__Vite_Admin_Secret__';
9 |
10 | /**
11 | * 加密
12 | * @param data - 加密数据
13 | * @param secret - 加密密钥
14 | */
15 | export function encryption(data: object, secret: string = secretKey) {
16 | const code = JSON.stringify(data);
17 | return encrypt(code, secret).toString();
18 | }
19 |
20 | /**
21 | * 解密
22 | * @param data - 解密数据
23 | * @param secret - 解密密钥
24 | */
25 | export function decryption(data: string, secret: string = secretKey) {
26 | const bytes = decrypt(data, secret);
27 | const originalText = bytes.toString(UTF8);
28 | if (originalText) {
29 | return JSON.parse(originalText);
30 | }
31 | return null;
32 | }
33 |
34 | /**
35 | * md5加密
36 | * @param data - 加密数据
37 | */
38 | export function encryptMd5(data: string) {
39 | return md5(data).toString();
40 | }
41 |
--------------------------------------------------------------------------------
/src/components/Table/components/ResizableTitle.tsx:
--------------------------------------------------------------------------------
1 | import type { ResizeCallbackData } from 'react-resizable';
2 | import React from 'react';
3 | import { Resizable } from 'react-resizable';
4 |
5 | /** 自定义拖拽 */
6 | function ResizableTitle(
7 | props: React.HTMLAttributes & {
8 | onResize: (e: React.SyntheticEvent, data: ResizeCallbackData) => void;
9 | width: number;
10 | },
11 | ) {
12 | const { onResize, width, ...restProps } = props;
13 |
14 | if (!width) {
15 | return | ;
16 | }
17 |
18 | return (
19 | {
26 | e.stopPropagation();
27 | }}
28 | />
29 | }
30 | onResize={onResize}
31 | draggableOpts={{ enableUserSelectHack: false }}
32 | >
33 | |
34 |
35 | );
36 | }
37 |
38 | export default ResizableTitle;
39 |
--------------------------------------------------------------------------------
/src/pages/demo/virtualScroll/components/VirtualList.tsx:
--------------------------------------------------------------------------------
1 | import { List, type RowComponentProps } from 'react-window';
2 | import { useCommonStore } from '@/hooks/useCommonStore';
3 |
4 | function VirtualList() {
5 | const { theme } = useCommonStore();
6 |
7 | const names = useMemo(() => {
8 | return Array.from({ length: 10000 }, (_, i) => `Name ${i + 1}`);
9 | }, []);
10 |
11 | function RowComponent({
12 | index,
13 | names,
14 | style,
15 | }: RowComponentProps<{
16 | names: string[];
17 | }>) {
18 | return (
19 |
23 | {names[index]}
24 |
{`${index + 1} of ${names.length}`}
25 |
26 | );
27 | }
28 |
29 | return
;
30 | }
31 |
32 | export default VirtualList;
33 |
--------------------------------------------------------------------------------
/src/pages/dashboard/model.ts:
--------------------------------------------------------------------------------
1 | import type { ApiFn, BaseFormList } from '#/form';
2 | import type { TFunction } from 'i18next';
3 | import { getPartnerDemo } from '@/servers/platform/partner';
4 |
5 | // 搜索数据
6 | export const searchList = (t: TFunction): BaseFormList[] => [
7 | {
8 | label: t('public.date'),
9 | name: 'pay_date',
10 | component: 'RangePicker',
11 | componentProps: {
12 | allowClear: false,
13 | },
14 | },
15 | {
16 | label: t('dashboard.gameID'),
17 | name: 'game_ids',
18 | wrapperWidth: 200,
19 | component: 'GameSelect',
20 | },
21 | {
22 | label: t('dashboard.cooperativeCompany'),
23 | name: 'partners',
24 | wrapperWidth: 200,
25 | component: 'ApiSelect',
26 | componentProps: {
27 | api: getPartnerDemo as ApiFn,
28 | params: [
29 | '/platform/partner',
30 | {
31 | isAll: true,
32 | },
33 | ],
34 | fieldNames: {
35 | label: 'name',
36 | value: 'id',
37 | },
38 | },
39 | },
40 | ];
41 |
--------------------------------------------------------------------------------
/src/hooks/useKeyStroke.ts:
--------------------------------------------------------------------------------
1 | interface Options {
2 | ArrowUp?: () => void;
3 | ArrowDown?: () => void;
4 | ArrowLeft?: () => void;
5 | ArrowRight?: () => void;
6 | Enter?: () => void;
7 | }
8 |
9 | /**
10 | * 键盘按键事件
11 | * @param options
12 | */
13 | export function useKeyStroke(options: Options) {
14 | /**
15 | * 点击按键
16 | * @param even - 按键事件
17 | */
18 | const onKeyDown = (even: KeyboardEvent) => {
19 | switch (even.key) {
20 | // 上
21 | case 'ArrowUp':
22 | options.ArrowUp?.();
23 | break;
24 |
25 | // 下
26 | case 'ArrowDown':
27 | options.ArrowDown?.();
28 | break;
29 |
30 | // 左
31 | case 'ArrowLeft':
32 | options.ArrowLeft?.();
33 | break;
34 |
35 | // 右
36 | case 'ArrowRight':
37 | options.ArrowRight?.();
38 | break;
39 |
40 | // 回车
41 | case 'Enter':
42 | options.Enter?.();
43 | break;
44 |
45 | default:
46 | break;
47 | }
48 | };
49 |
50 | return [onKeyDown] as const;
51 | }
52 |
--------------------------------------------------------------------------------
/src/main.tsx:
--------------------------------------------------------------------------------
1 | import ReactDOM from 'react-dom/client';
2 | import Router from './router';
3 | import '@/assets/css/public.less';
4 | import '@/assets/fonts/font.less';
5 |
6 | // 样式
7 | import { StyleProvider, legacyLogicalPropertiesTransformer } from '@ant-design/cssinjs'; // 兼容低版本浏览器
8 | import 'uno.css';
9 | import 'nprogress/nprogress.css';
10 | import '@/assets/css/scrollbar.less';
11 | import '@/assets/css/theme-color.less';
12 |
13 | // 国际化i18n
14 | import './locales/config';
15 |
16 | // antd
17 | import '@/assets/css/antd.less';
18 |
19 | // 时间设为中文
20 | import dayjs from 'dayjs';
21 | import 'dayjs/locale/zh-cn';
22 | dayjs.locale('zh-cn');
23 |
24 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
25 |
26 |
27 | ,
28 | );
29 |
30 | // 关闭loading
31 | const firstElement = document.getElementById('first');
32 | if (firstElement && firstElement.style?.display !== 'none') {
33 | firstElement.style.display = 'none';
34 | }
35 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig, loadEnv } from 'vite';
2 | import { handleEnv } from './build/utils/helper';
3 | import { createProxy } from './build/vite/proxy';
4 | import { createVitePlugins } from './build/plugins';
5 | import { buildOptions } from './build/vite/build';
6 |
7 | // https://vitejs.dev/config/
8 | export default defineConfig(({ mode }) => {
9 | const root = process.cwd();
10 | const env = loadEnv(mode, root);
11 | const viteEnv = handleEnv(env);
12 | const { VITE_SERVER_PORT, VITE_PROXY } = viteEnv;
13 |
14 | return {
15 | plugins: createVitePlugins(),
16 | resolve: {
17 | alias: {
18 | '@': '/src',
19 | '#': '/types',
20 | },
21 | },
22 | css: {
23 | preprocessorOptions: {
24 | less: {
25 | javascriptEnabled: true,
26 | charset: false,
27 | },
28 | },
29 | },
30 | server: {
31 | open: true,
32 | port: VITE_SERVER_PORT,
33 | // 跨域处理
34 | proxy: createProxy(VITE_PROXY),
35 | },
36 | build: buildOptions(),
37 | };
38 | });
39 |
--------------------------------------------------------------------------------
/src/pages/demo/copy/index.tsx:
--------------------------------------------------------------------------------
1 | import { useTranslation } from 'react-i18next';
2 | import { useCommonStore } from '@/hooks/useCommonStore';
3 | import { checkPermission } from '@/utils/permissions';
4 | import CopyInput from '@/components/Copy/CopyInput';
5 | import CopyBtn from '@/components/Copy/CopyBtn';
6 | import BaseContent from '@/components/Content/BaseContent';
7 |
8 | function CopyPage() {
9 | const { t } = useTranslation();
10 | const { permissions } = useCommonStore();
11 | const isPermission = checkPermission('/demo/copy', permissions);
12 |
13 | return (
14 |
15 |
16 |
{t('content.clipboard')}:
17 |
18 |
19 |
20 | {t('content.clipboardMessage')}:
21 |
22 |
23 |
24 |
25 | );
26 | }
27 |
28 | export default CopyPage;
29 |
--------------------------------------------------------------------------------
/src/layouts/components/DraggableTabNode.tsx:
--------------------------------------------------------------------------------
1 | import type { CSSProperties, DetailedHTMLProps, HTMLAttributes, ReactElement } from 'react';
2 | import { cloneElement } from 'react';
3 | import { CSS } from '@dnd-kit/utilities';
4 | import { useSortable } from '@dnd-kit/sortable';
5 |
6 | export interface DraggableTabPaneProps extends HTMLAttributes {
7 | 'data-node-key': string;
8 | }
9 |
10 | const DraggableTabNode: FC> = ({ ...props }) => {
11 | const { attributes, listeners, setNodeRef, transform, transition } = useSortable({
12 | id: props['data-node-key'],
13 | });
14 |
15 | const style: CSSProperties = {
16 | ...props.style,
17 | transform: CSS.Translate.toString(transform),
18 | transition,
19 | cursor: 'move',
20 | };
21 |
22 | return cloneElement(
23 | props.children as ReactElement<
24 | DetailedHTMLProps, HTMLDivElement>
25 | >,
26 | {
27 | ref: setNodeRef,
28 | style,
29 | ...attributes,
30 | ...listeners,
31 | },
32 | );
33 | };
34 |
35 | export default DraggableTabNode;
36 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 southliu
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/components/Bottom/SubmitBottom.tsx:
--------------------------------------------------------------------------------
1 | import type { ReactNode } from 'react';
2 | import { Button } from 'antd';
3 | import { useTranslation } from 'react-i18next';
4 |
5 | interface Props {
6 | goBack: () => void;
7 | handleSubmit: () => void;
8 | isLoading?: boolean;
9 | children?: ReactNode;
10 | }
11 |
12 | function SubmitBottom(props: Props) {
13 | const { t } = useTranslation();
14 | const { goBack, handleSubmit, isLoading, children } = props;
15 |
16 | return (
17 |
34 | {children}
35 |
36 |
39 |
42 |
43 | );
44 | }
45 |
46 | export default SubmitBottom;
47 |
--------------------------------------------------------------------------------
/src/components/Pagination/index.less:
--------------------------------------------------------------------------------
1 | .ant-pagination-item {
2 | margin-right: 7px !important;
3 | background-color: #f4f4f5 !important;
4 | }
5 |
6 | .ant-pagination-item-active {
7 | background-color: #0960bd !important;
8 | }
9 |
10 | .ant-pagination-item-active a {
11 | color: #fff !important;
12 | }
13 |
14 | .ant-pagination-prev {
15 | margin-right: 7px !important;
16 | background-color: #f4f4f5 !important;
17 | }
18 |
19 | .ant-pagination-next {
20 | margin-right: 7px !important;
21 | background-color: #f4f4f5 !important;
22 | }
23 |
24 | .theme-dark {
25 | .ant-pagination-item {
26 | margin-right: 7px !important;
27 | background-color: rgb(29 29 29) !important;
28 | }
29 |
30 | .ant-pagination-item-active {
31 | background-color: #0960bd !important;
32 | }
33 |
34 | .ant-pagination-item-active a {
35 | color: #ffffffd9 !important;
36 | }
37 |
38 | .ant-pagination-prev {
39 | margin-right: 7px !important;
40 | background-color: rgb(29 29 29) !important;
41 | }
42 |
43 | .ant-pagination-next {
44 | margin-right: 7px !important;
45 | background-color: rgb(29 29 29) !important;
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/pages/demo/virtualScroll/components/VirtualTable.tsx:
--------------------------------------------------------------------------------
1 | import type { TableColumn } from '#/public';
2 | import { useTranslation } from 'react-i18next';
3 | import BaseTable from '@/components/Table/BaseTable';
4 |
5 | function VirtualTable() {
6 | const { t } = useTranslation();
7 | const [tableData, setTableData] = useState