├── .DS_Store
├── .eslintignore
├── .eslintrc.js
├── .gitignore
├── .stylelintrc.js
├── LICENSE
├── README.md
├── config
├── config.dev.ts
├── config.ts
├── defaultSettings.ts
├── oneapi.json
├── proxy.ts
└── routes.ts
├── jest.config.js
├── jsconfig.json
├── mock
├── listTableList.ts
├── notices.ts
├── route.ts
└── user.ts
├── package.json
├── public
├── .DS_Store
├── CNAME
├── favicon.ico
├── icons
│ ├── icon-128x128.png
│ ├── icon-192x192.png
│ └── icon-512x512.png
├── logo.svg
├── logo1.svg
└── pro_icon.svg
├── src
├── access.ts
├── app.tsx
├── components
│ ├── Footer
│ │ └── index.tsx
│ ├── HeaderDropdown
│ │ ├── index.less
│ │ └── index.tsx
│ ├── HeaderSearch
│ │ ├── index.less
│ │ └── index.tsx
│ ├── NoticeIcon
│ │ ├── NoticeIcon.tsx
│ │ ├── NoticeList.less
│ │ ├── NoticeList.tsx
│ │ ├── index.less
│ │ └── index.tsx
│ ├── RightContent
│ │ ├── AvatarDropdown.tsx
│ │ ├── index.less
│ │ └── index.tsx
│ ├── ToolBar
│ │ └── index.tsx
│ └── index.md
├── e2e
│ └── baseLayout.e2e.spec.ts
├── global.less
├── global.tsx
├── layouts
│ └── BaseLayout.tsx
├── manifest.json
├── pages
│ ├── 404.tsx
│ ├── Admin.tsx
│ ├── Table
│ │ ├── index.less
│ │ └── index.tsx
│ ├── TableList
│ │ ├── components
│ │ │ └── UpdateForm.tsx
│ │ └── index.tsx
│ ├── Welcome.less
│ ├── Welcome.tsx
│ ├── admin
│ │ ├── logs
│ │ │ ├── index.less
│ │ │ └── index.tsx
│ │ ├── menus
│ │ │ ├── components
│ │ │ │ ├── BaseFormItems.tsx
│ │ │ │ ├── CreateForm.tsx
│ │ │ │ └── UpdateForm.tsx
│ │ │ ├── index.less
│ │ │ └── index.tsx
│ │ ├── permissions
│ │ │ ├── components
│ │ │ │ ├── CreateForm.tsx
│ │ │ │ ├── ShowForm.tsx
│ │ │ │ └── UpdateForm.tsx
│ │ │ └── index.tsx
│ │ ├── roles
│ │ │ ├── components
│ │ │ │ ├── CreateForm.tsx
│ │ │ │ ├── ShowForm.tsx
│ │ │ │ └── UpdateForm.tsx
│ │ │ ├── index.less
│ │ │ └── index.tsx
│ │ └── users
│ │ │ ├── components
│ │ │ ├── CreateForm.tsx
│ │ │ ├── ShowForm.tsx
│ │ │ └── UpdateForm.tsx
│ │ │ ├── index.less
│ │ │ └── index.tsx
│ ├── document.ejs
│ └── user
│ │ └── Login
│ │ ├── index.less
│ │ └── index.tsx
├── service-worker.js
├── services
│ ├── API.d.ts
│ ├── admin
│ │ ├── logs.d.ts
│ │ ├── logs.ts
│ │ ├── menu.d.ts
│ │ ├── menu.ts
│ │ ├── permission.d.ts
│ │ ├── permission.ts
│ │ ├── role.d.ts
│ │ ├── role.ts
│ │ ├── user.d.ts
│ │ └── user.ts
│ ├── ant-design-pro
│ │ ├── api.ts
│ │ ├── index.ts
│ │ ├── login.ts
│ │ └── typings.d.ts
│ └── swagger
│ │ ├── index.ts
│ │ ├── pet.ts
│ │ ├── store.ts
│ │ ├── typings.d.ts
│ │ └── user.ts
├── typings.d.ts
└── utils
│ ├── utils.less
│ └── utils.ts
├── tests
├── run-tests.js
└── setupTests.js
└── tsconfig.json
/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OutUI/out-admin-react/3470d8b926d17d75529268a61edcf2bf96f4ce00/.DS_Store
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | /lambda/
2 | /scripts
3 | /config
4 | .history
5 | /src/.umi/**/*.*
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: [require.resolve("@umijs/fabric/dist/eslint")],
3 | globals: {
4 | ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION: true,
5 | page: true,
6 | REACT_APP_ENV: true,
7 | },
8 | };
9 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | src/.umi
3 | dist
4 | yarn.lock
5 |
--------------------------------------------------------------------------------
/.stylelintrc.js:
--------------------------------------------------------------------------------
1 | const fabric = require("@umijs/fabric");
2 |
3 | module.exports = {
4 | ...fabric.stylelint,
5 | };
6 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 OutUI
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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## out-admin-react
2 |
3 |
out-admin-react是使用 react + antd pro v5 开发的一个后台权限管理系统系统
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | ## 运行
14 |
15 | ### npm
16 |
17 | ```bash
18 | $ npm i
19 | $ npm run dev
20 | ```
21 |
22 | ### yarn
23 |
24 | ```bash
25 | $ yarn
26 | $ yarn run dev
27 | ```
28 |
29 | ## 登录账号
30 |
31 | ```
32 | admin/123456
33 | ```
34 |
35 | ## 项目演示:
36 |
37 | 
38 |
39 | 
40 |
41 | 
42 |
43 | ## 项目后端
44 |
45 | 配套的后端工程请移步 https://github.com/Outjs/out-admin-midway
46 | 启动后端可以体验完整功能
47 |
48 | ## 欢迎Star和PR
49 | 如果项目对你有帮助,点颗星支持一下,后续还会推出更多功能
50 |
--------------------------------------------------------------------------------
/config/config.dev.ts:
--------------------------------------------------------------------------------
1 | // https://umijs.org/config/
2 | import { defineConfig } from "umi";
3 |
4 | export default defineConfig({
5 | plugins: [
6 | // https://github.com/zthxxx/react-dev-inspector
7 | "react-dev-inspector/plugins/umi/react-inspector",
8 | ],
9 | // https://github.com/zthxxx/react-dev-inspector#inspector-loader-props
10 | inspectorConfig: {
11 | exclude: [],
12 | babelPlugins: [],
13 | babelOptions: {},
14 | },
15 | });
16 |
--------------------------------------------------------------------------------
/config/config.ts:
--------------------------------------------------------------------------------
1 | // https://umijs.org/config/
2 | import { defineConfig } from "umi";
3 | import { join } from "path";
4 | import defaultSettings from "./defaultSettings";
5 | import proxy from "./proxy";
6 | import routes from "./routes";
7 | const { REACT_APP_ENV } = process.env;
8 | export default defineConfig({
9 | hash: true,
10 | antd: {},
11 | dva: {
12 | hmr: true,
13 | },
14 | layout: {
15 | // https://umijs.org/zh-CN/plugins/plugin-layout
16 | locale: true,
17 | siderWidth: 105,
18 | ...defaultSettings,
19 | },
20 | dynamicImport: {
21 | loading: "@ant-design/pro-layout/es/PageLoading",
22 | },
23 | targets: {
24 | ie: 11,
25 | },
26 | // umi routes: https://umijs.org/docs/routing
27 | routes,
28 | // Theme for antd: https://ant.design/docs/react/customize-theme-cn
29 | theme: {
30 | "root-entry-name": "variable",
31 | "font-size-base": "12px",
32 | },
33 | // esbuild is father build tools
34 | // https://umijs.org/plugins/plugin-esbuild
35 | esbuild: {},
36 | title: false,
37 | ignoreMomentLocale: true,
38 | proxy: proxy[REACT_APP_ENV || "dev"],
39 | manifest: {
40 | basePath: "/",
41 | },
42 | // Fast Refresh 热更新
43 | fastRefresh: {},
44 | openAPI: [
45 | {
46 | requestLibPath: "import { request } from 'umi'",
47 | // 或者使用在线的版本
48 | // schemaPath: "https://gw.alipayobjects.com/os/antfincdn/M%24jrzTTYJN/oneapi.json"
49 | schemaPath: join(__dirname, "oneapi.json"),
50 | mock: false,
51 | },
52 | {
53 | requestLibPath: "import { request } from 'umi'",
54 | schemaPath:
55 | "https://gw.alipayobjects.com/os/antfincdn/CA1dOm%2631B/openapi.json",
56 | projectName: "swagger",
57 | },
58 | ],
59 | nodeModulesTransform: {
60 | type: "none",
61 | },
62 | mfsu: {},
63 | webpack5: {},
64 | exportStatic: {},
65 | });
66 |
--------------------------------------------------------------------------------
/config/defaultSettings.ts:
--------------------------------------------------------------------------------
1 | import { Settings as LayoutSettings } from "@ant-design/pro-layout";
2 |
3 | const Settings: LayoutSettings & {
4 | pwa?: boolean;
5 | logo?: string;
6 | } = {
7 | title: false,
8 | navTheme: "dark",
9 | primaryColor: "#1890ff",
10 | layout: "side",
11 | contentWidth: "Fluid",
12 | fixedHeader: true,
13 | fixSiderbar: true,
14 | pwa: false,
15 | logo: "/logo.svg",
16 | headerHeight: 48,
17 | splitMenus: false,
18 | //headerRender: false,
19 | footerRender: false,
20 | };
21 |
22 | export default Settings;
23 |
--------------------------------------------------------------------------------
/config/proxy.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 在生产环境 代理是无法生效的,所以这里没有生产环境的配置
3 | * -------------------------------
4 | * The agent cannot take effect in the production environment
5 | * so there is no configuration of the production environment
6 | * For details, please see
7 | * https://pro.ant.design/docs/deploy
8 | */
9 | export default {
10 | dev: {
11 | // localhost:8000/api/** -> https://preview.pro.ant.design/api/**
12 | "/api/": {
13 | // 要代理的地址
14 | target: "http://localhost:7001",
15 | // 配置了这个可以从 http 代理到 https
16 | // 依赖 origin 的功能可能需要这个,比如 cookie
17 | changeOrigin: true,
18 | pathRewrite: { "/api": "/api" },
19 | },
20 | },
21 | test: {
22 | "/api/": {
23 | target: "https://proapi.azurewebsites.net",
24 | changeOrigin: true,
25 | pathRewrite: { "^": "" },
26 | },
27 | },
28 | pre: {
29 | "/api/": {
30 | target: "your pre url",
31 | changeOrigin: true,
32 | pathRewrite: { "^": "" },
33 | },
34 | },
35 | };
36 |
--------------------------------------------------------------------------------
/config/routes.ts:
--------------------------------------------------------------------------------
1 | export default [
2 | {
3 | path: "/user",
4 | layout: false,
5 | routes: [
6 | {
7 | path: "/user",
8 | routes: [
9 | { name: "登录", path: "/user/login", component: "./user/Login" },
10 | ],
11 | },
12 | { component: "./404" },
13 | ],
14 | },
15 | {
16 | path: "/table",
17 | name: "桌台",
18 | icon: "icon-zhuozi",
19 | access: "canAdmin",
20 | component: "./Table",
21 | },
22 | {
23 | path: "/welcome",
24 | name: "订餐",
25 | icon: "icon-diancan",
26 | component: "./Welcome",
27 | },
28 | {
29 | name: "订单",
30 | icon: "icon-dingdan",
31 | path: "/list",
32 | component: "./TableList",
33 | },
34 | {
35 | path: "/report",
36 | name: "统计",
37 | icon: "icon-shuju",
38 | access: "canAdmin",
39 | component: "./Admin",
40 | },
41 | {
42 | path: "/admin",
43 | name: "管理",
44 | icon: "icon-xitongguanli",
45 | access: "canAdmin",
46 | //component: './admin',
47 | routes: [
48 | {
49 | path: "/admin/user",
50 | name: "用户管理",
51 | //hideInMenu: true,
52 | icon: "smile",
53 | component: "./admin/users",
54 | },
55 | {
56 | path: "/admin/role",
57 | name: "角色管理",
58 | ///hideInMenu: true,
59 | icon: "smile",
60 | component: "./admin/roles",
61 | },
62 | {
63 | path: "/admin/permission",
64 | name: "权限管理",
65 | ///hideInMenu: true,
66 | icon: "smile",
67 | component: "./admin/permissions",
68 | },
69 | {
70 | path: "/admin/menu",
71 | name: "菜单管理",
72 | ///hideInMenu: true,
73 | icon: "smile",
74 | component: "./admin/menus",
75 | },
76 | {
77 | path: "/admin/log",
78 | name: "操作日志",
79 | ///hideInMenu: true,
80 | icon: "smile",
81 | component: "./admin/logs",
82 | },
83 | { component: "./404" },
84 | ],
85 | },
86 | { path: "/", redirect: "/welcome" },
87 | { component: "./404" },
88 | ];
89 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | testURL: "http://localhost:8000",
3 | verbose: false,
4 | extraSetupFiles: ["./tests/setupTests.js"],
5 | globals: {
6 | ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION: false,
7 | localStorage: null,
8 | },
9 | };
10 |
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "jsx": "react-jsx",
4 | "emitDecoratorMetadata": true,
5 | "experimentalDecorators": true,
6 | "baseUrl": ".",
7 | "paths": {
8 | "@/*": ["./src/*"]
9 | }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/mock/listTableList.ts:
--------------------------------------------------------------------------------
1 | import { Request, Response } from "express";
2 | import moment from "moment";
3 | import { parse } from "url";
4 |
5 | // mock tableListDataSource
6 | const genList = (current: number, pageSize: number) => {
7 | const tableListDataSource: API.RuleListItem[] = [];
8 |
9 | for (let i = 0; i < pageSize; i += 1) {
10 | const index = (current - 1) * 10 + i;
11 | tableListDataSource.push({
12 | key: index,
13 | disabled: i % 6 === 0,
14 | href: "https://ant.design",
15 | avatar: [
16 | "https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png",
17 | "https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png",
18 | ][i % 2],
19 | name: `TradeCode ${index}`,
20 | owner: "曲丽丽",
21 | desc: "这是一段描述",
22 | callNo: Math.floor(Math.random() * 1000),
23 | status: Math.floor(Math.random() * 10) % 4,
24 | updatedAt: moment().format("YYYY-MM-DD"),
25 | createdAt: moment().format("YYYY-MM-DD"),
26 | progress: Math.ceil(Math.random() * 100),
27 | });
28 | }
29 | tableListDataSource.reverse();
30 | return tableListDataSource;
31 | };
32 |
33 | let tableListDataSource = genList(1, 100);
34 |
35 | function getRule(req: Request, res: Response, u: string) {
36 | let realUrl = u;
37 | if (
38 | !realUrl ||
39 | Object.prototype.toString.call(realUrl) !== "[object String]"
40 | ) {
41 | realUrl = req.url;
42 | }
43 | const { current = 1, pageSize = 10 } = req.query;
44 | const params = parse(realUrl, true).query as unknown as API.PageParams &
45 | API.RuleListItem & {
46 | sorter: any;
47 | filter: any;
48 | };
49 |
50 | let dataSource = [...tableListDataSource].slice(
51 | ((current as number) - 1) * (pageSize as number),
52 | (current as number) * (pageSize as number)
53 | );
54 | if (params.sorter) {
55 | const sorter = JSON.parse(params.sorter);
56 | dataSource = dataSource.sort((prev, next) => {
57 | let sortNumber = 0;
58 | Object.keys(sorter).forEach((key) => {
59 | if (sorter[key] === "descend") {
60 | if (prev[key] - next[key] > 0) {
61 | sortNumber += -1;
62 | } else {
63 | sortNumber += 1;
64 | }
65 | return;
66 | }
67 | if (prev[key] - next[key] > 0) {
68 | sortNumber += 1;
69 | } else {
70 | sortNumber += -1;
71 | }
72 | });
73 | return sortNumber;
74 | });
75 | }
76 | if (params.filter) {
77 | const filter = JSON.parse(params.filter as any) as {
78 | [key: string]: string[];
79 | };
80 | if (Object.keys(filter).length > 0) {
81 | dataSource = dataSource.filter((item) => {
82 | return Object.keys(filter).some((key) => {
83 | if (!filter[key]) {
84 | return true;
85 | }
86 | if (filter[key].includes(`${item[key]}`)) {
87 | return true;
88 | }
89 | return false;
90 | });
91 | });
92 | }
93 | }
94 |
95 | if (params.name) {
96 | dataSource = dataSource.filter((data) =>
97 | data?.name?.includes(params.name || "")
98 | );
99 | }
100 | const result = {
101 | data: dataSource,
102 | total: tableListDataSource.length,
103 | success: true,
104 | pageSize,
105 | current: parseInt(`${params.current}`, 10) || 1,
106 | };
107 |
108 | return res.json(result);
109 | }
110 |
111 | function postRule(req: Request, res: Response, u: string, b: Request) {
112 | let realUrl = u;
113 | if (
114 | !realUrl ||
115 | Object.prototype.toString.call(realUrl) !== "[object String]"
116 | ) {
117 | realUrl = req.url;
118 | }
119 |
120 | const body = (b && b.body) || req.body;
121 | const { method, name, desc, key } = body;
122 |
123 | switch (method) {
124 | /* eslint no-case-declarations:0 */
125 | case "delete":
126 | tableListDataSource = tableListDataSource.filter(
127 | (item) => key.indexOf(item.key) === -1
128 | );
129 | break;
130 | case "post":
131 | (() => {
132 | const i = Math.ceil(Math.random() * 10000);
133 | const newRule: API.RuleListItem = {
134 | key: tableListDataSource.length,
135 | href: "https://ant.design",
136 | avatar: [
137 | "https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png",
138 | "https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png",
139 | ][i % 2],
140 | name,
141 | owner: "曲丽丽",
142 | desc,
143 | callNo: Math.floor(Math.random() * 1000),
144 | status: Math.floor(Math.random() * 10) % 2,
145 | updatedAt: moment().format("YYYY-MM-DD"),
146 | createdAt: moment().format("YYYY-MM-DD"),
147 | progress: Math.ceil(Math.random() * 100),
148 | };
149 | tableListDataSource.unshift(newRule);
150 | return res.json(newRule);
151 | })();
152 | return;
153 |
154 | case "update":
155 | (() => {
156 | let newRule = {};
157 | tableListDataSource = tableListDataSource.map((item) => {
158 | if (item.key === key) {
159 | newRule = { ...item, desc, name };
160 | return { ...item, desc, name };
161 | }
162 | return item;
163 | });
164 | return res.json(newRule);
165 | })();
166 | return;
167 | default:
168 | break;
169 | }
170 |
171 | const result = {
172 | list: tableListDataSource,
173 | pagination: {
174 | total: tableListDataSource.length,
175 | },
176 | };
177 |
178 | res.json(result);
179 | }
180 |
181 | export default {
182 | "GET /api/rule": getRule,
183 | "POST /api/rule": postRule,
184 | };
185 |
--------------------------------------------------------------------------------
/mock/notices.ts:
--------------------------------------------------------------------------------
1 | import { Request, Response } from "express";
2 |
3 | const getNotices = (req: Request, res: Response) => {
4 | res.json({
5 | data: [
6 | {
7 | id: "000000001",
8 | avatar:
9 | "https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png",
10 | title: "你收到了 14 份新周报",
11 | datetime: "2017-08-09",
12 | type: "notification",
13 | },
14 | {
15 | id: "000000002",
16 | avatar:
17 | "https://gw.alipayobjects.com/zos/rmsportal/OKJXDXrmkNshAMvwtvhu.png",
18 | title: "你推荐的 曲妮妮 已通过第三轮面试",
19 | datetime: "2017-08-08",
20 | type: "notification",
21 | },
22 | {
23 | id: "000000003",
24 | avatar:
25 | "https://gw.alipayobjects.com/zos/rmsportal/kISTdvpyTAhtGxpovNWd.png",
26 | title: "这种模板可以区分多种通知类型",
27 | datetime: "2017-08-07",
28 | read: true,
29 | type: "notification",
30 | },
31 | {
32 | id: "000000004",
33 | avatar:
34 | "https://gw.alipayobjects.com/zos/rmsportal/GvqBnKhFgObvnSGkDsje.png",
35 | title: "左侧图标用于区分不同的类型",
36 | datetime: "2017-08-07",
37 | type: "notification",
38 | },
39 | {
40 | id: "000000005",
41 | avatar:
42 | "https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png",
43 | title: "内容不要超过两行字,超出时自动截断",
44 | datetime: "2017-08-07",
45 | type: "notification",
46 | },
47 | {
48 | id: "000000006",
49 | avatar:
50 | "https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg",
51 | title: "曲丽丽 评论了你",
52 | description: "描述信息描述信息描述信息",
53 | datetime: "2017-08-07",
54 | type: "message",
55 | clickClose: true,
56 | },
57 | {
58 | id: "000000007",
59 | avatar:
60 | "https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg",
61 | title: "朱偏右 回复了你",
62 | description: "这种模板用于提醒谁与你发生了互动,左侧放『谁』的头像",
63 | datetime: "2017-08-07",
64 | type: "message",
65 | clickClose: true,
66 | },
67 | {
68 | id: "000000008",
69 | avatar:
70 | "https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg",
71 | title: "标题",
72 | description: "这种模板用于提醒谁与你发生了互动,左侧放『谁』的头像",
73 | datetime: "2017-08-07",
74 | type: "message",
75 | clickClose: true,
76 | },
77 | {
78 | id: "000000009",
79 | title: "任务名称",
80 | description: "任务需要在 2017-01-12 20:00 前启动",
81 | extra: "未开始",
82 | status: "todo",
83 | type: "event",
84 | },
85 | {
86 | id: "000000010",
87 | title: "第三方紧急代码变更",
88 | description:
89 | "冠霖提交于 2017-01-06,需在 2017-01-07 前完成代码变更任务",
90 | extra: "马上到期",
91 | status: "urgent",
92 | type: "event",
93 | },
94 | {
95 | id: "000000011",
96 | title: "信息安全考试",
97 | description: "指派竹尔于 2017-01-09 前完成更新并发布",
98 | extra: "已耗时 8 天",
99 | status: "doing",
100 | type: "event",
101 | },
102 | {
103 | id: "000000012",
104 | title: "ABCD 版本发布",
105 | description:
106 | "冠霖提交于 2017-01-06,需在 2017-01-07 前完成代码变更任务",
107 | extra: "进行中",
108 | status: "processing",
109 | type: "event",
110 | },
111 | ],
112 | });
113 | };
114 |
115 | export default {
116 | "GET /api/notices": getNotices,
117 | };
118 |
--------------------------------------------------------------------------------
/mock/route.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | "/api/auth_routes": {
3 | "/form/advanced-form": { authority: ["admin", "user"] },
4 | },
5 | };
6 |
--------------------------------------------------------------------------------
/mock/user.ts:
--------------------------------------------------------------------------------
1 | import { Request, Response } from "express";
2 |
3 | const waitTime = (time: number = 100) => {
4 | return new Promise((resolve) => {
5 | setTimeout(() => {
6 | resolve(true);
7 | }, time);
8 | });
9 | };
10 |
11 | async function getFakeCaptcha(req: Request, res: Response) {
12 | await waitTime(2000);
13 | return res.json("captcha-xxx");
14 | }
15 |
16 | const { ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION } = process.env;
17 |
18 | /**
19 | * 当前用户的权限,如果为空代表没登录
20 | * current user access, if is '', user need login
21 | * 如果是 pro 的预览,默认是有权限的
22 | */
23 | let access =
24 | ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION === "site" ? "admin" : "";
25 |
26 | const getAccess = () => {
27 | return access;
28 | };
29 |
30 | // 代码中会兼容本地 service mock 以及部署站点的静态数据
31 | export default {
32 | // 支持值为 Object 和 Array
33 | "GET /api/auth/currentUser": (req: Request, res: Response) => {
34 | if (!getAccess()) {
35 | res.status(401).send({
36 | data: {
37 | isLogin: false,
38 | },
39 | errorCode: "401",
40 | errorMessage: "请先登录!",
41 | success: true,
42 | });
43 | return;
44 | }
45 | res.send({
46 | code: 200,
47 | success: true,
48 | data: {
49 | name: "admin",
50 | avatar:
51 | "https://gw.alipayobjects.com/zos/antfincdn/XAosXuNZyF/BiazfanxmamNRoxxVxka.png",
52 | userid: "00000001",
53 | email: "antdesign@alipay.com",
54 | signature: "海纳百川,有容乃大",
55 | title: "交互专家",
56 | group: "蚂蚁金服-某某某事业群-某某平台部-某某技术部-UED",
57 | tags: [
58 | {
59 | key: "0",
60 | label: "很有想法的",
61 | },
62 | {
63 | key: "1",
64 | label: "专注设计",
65 | },
66 | {
67 | key: "2",
68 | label: "辣~",
69 | },
70 | {
71 | key: "3",
72 | label: "大长腿",
73 | },
74 | {
75 | key: "4",
76 | label: "川妹子",
77 | },
78 | {
79 | key: "5",
80 | label: "海纳百川",
81 | },
82 | ],
83 | notifyCount: 12,
84 | unreadCount: 11,
85 | country: "China",
86 | access: getAccess(),
87 | geographic: {
88 | province: {
89 | label: "浙江省",
90 | key: "330000",
91 | },
92 | city: {
93 | label: "杭州市",
94 | key: "330100",
95 | },
96 | },
97 | address: "西湖区工专路 77 号",
98 | phone: "0752-268888888",
99 | },
100 | });
101 | },
102 | // GET POST 可省略
103 | "GET /api/users": [
104 | {
105 | key: "1",
106 | name: "John Brown",
107 | age: 32,
108 | address: "New York No. 1 Lake Park",
109 | },
110 | {
111 | key: "2",
112 | name: "Jim Green",
113 | age: 42,
114 | address: "London No. 1 Lake Park",
115 | },
116 | {
117 | key: "3",
118 | name: "Joe Black",
119 | age: 32,
120 | address: "Sidney No. 1 Lake Park",
121 | },
122 | ],
123 | "POST /api/auth/login": async (req: Request, res: Response) => {
124 | const { password, username, type } = req.body;
125 | await waitTime(2000);
126 | if (password === "123456" && username === "admin") {
127 | res.send({
128 | code: 200,
129 | success: true,
130 | data: {
131 | status: "ok",
132 | type,
133 | currentAuthority: "admin",
134 | token:
135 | "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjEiLCJuYW1lIjoiYWRtaW4iLCJpYXQiOjE2NjAyMjkwNDgsImV4cCI6MTY2MDI0MDE1OX0.e8vj83BFANAl9nlOL_M5hEHD4fy60dQWJ58pMcmCLxY",
136 | },
137 | });
138 | access = "admin";
139 | return;
140 | }
141 | if (password === "123456" && username === "user") {
142 | res.send({
143 | code: 200,
144 | success: true,
145 | data: {
146 | status: "ok",
147 | type,
148 | currentAuthority: "user",
149 | token:
150 | "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjEiLCJuYW1lIjoiYWRtaW4iLCJpYXQiOjE2NjAyMjkwNDgsImV4cCI6MTY2MDI0MDE1OX0.e8vj83BFANAl9nlOL_M5hEHD4fy60dQWJ58pMcmCLxY",
151 | },
152 | });
153 | access = "user";
154 | return;
155 | }
156 |
157 | res.send({
158 | code: 200,
159 | success: false,
160 | data: {
161 | status: "error",
162 | type,
163 | currentAuthority: "guest",
164 | },
165 | });
166 | access = "guest";
167 | },
168 | "POST /api/login/outLogin": (req: Request, res: Response) => {
169 | access = "";
170 | res.send({ data: {}, success: true });
171 | },
172 | "POST /api/register": (req: Request, res: Response) => {
173 | res.send({ status: "ok", currentAuthority: "user", success: true });
174 | },
175 | "GET /api/500": (req: Request, res: Response) => {
176 | res.status(500).send({
177 | timestamp: 1513932555104,
178 | status: 500,
179 | error: "error",
180 | message: "error",
181 | path: "/base/category/list",
182 | });
183 | },
184 | "GET /api/404": (req: Request, res: Response) => {
185 | res.status(404).send({
186 | timestamp: 1513932643431,
187 | status: 404,
188 | error: "Not Found",
189 | message: "No message available",
190 | path: "/base/category/list/2121212",
191 | });
192 | },
193 | "GET /api/403": (req: Request, res: Response) => {
194 | res.status(403).send({
195 | timestamp: 1513932555104,
196 | status: 403,
197 | error: "Forbidden",
198 | message: "Forbidden",
199 | path: "/base/category/list",
200 | });
201 | },
202 | "GET /api/401": (req: Request, res: Response) => {
203 | res.status(401).send({
204 | timestamp: 1513932555104,
205 | status: 401,
206 | error: "Unauthorized",
207 | message: "Unauthorized",
208 | path: "/base/category/list",
209 | });
210 | },
211 |
212 | "GET /api/login/captcha": getFakeCaptcha,
213 | };
214 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ant-design-pro",
3 | "version": "5.2.0",
4 | "private": true,
5 | "description": "An out-of-box UI solution for enterprise applications",
6 | "scripts": {
7 | "analyze": "cross-env ANALYZE=1 umi build",
8 | "build": "umi build",
9 | "deploy": "npm run build && npm run gh-pages",
10 | "dev": "npm run start:dev",
11 | "gh-pages": "gh-pages -d dist",
12 | "i18n-remove": "pro i18n-remove --locale=zh-CN --write",
13 | "postinstall": "umi g tmp",
14 | "lint": "umi g tmp && npm run lint:js && npm run lint:style && npm run lint:prettier && npm run tsc",
15 | "lint-staged": "lint-staged",
16 | "lint-staged:js": "eslint --ext .js,.jsx,.ts,.tsx ",
17 | "lint:fix": "eslint --fix --cache --ext .js,.jsx,.ts,.tsx --format=pretty ./src && npm run lint:style",
18 | "lint:js": "eslint --cache --ext .js,.jsx,.ts,.tsx --format=pretty ./src",
19 | "lint:prettier": "prettier -c --write \"src/**/*\" --end-of-line auto",
20 | "lint:style": "stylelint --fix \"src/**/*.less\" --syntax less",
21 | "openapi": "umi openapi",
22 | "playwright": "playwright install && playwright test",
23 | "precommit": "lint-staged",
24 | "prettier": "prettier -c --write \"src/**/*\"",
25 | "serve": "umi-serve",
26 | "start": "cross-env UMI_ENV=dev umi dev",
27 | "start:dev": "cross-env REACT_APP_ENV=dev UMI_ENV=dev umi dev",
28 | "start:no-mock": "cross-env MOCK=none UMI_ENV=dev umi dev",
29 | "start:no-ui": "cross-env UMI_UI=none UMI_ENV=dev umi dev",
30 | "start:pre": "cross-env REACT_APP_ENV=pre UMI_ENV=dev umi dev",
31 | "start:test": "cross-env REACT_APP_ENV=test MOCK=none UMI_ENV=dev umi dev",
32 | "test": "umi test",
33 | "test:component": "umi test ./src/components",
34 | "test:e2e": "node ./tests/run-tests.js",
35 | "tsc": "tsc --noEmit"
36 | },
37 | "lint-staged": {
38 | "**/*.less": "stylelint --syntax less",
39 | "**/*.{js,jsx,ts,tsx}": "npm run lint-staged:js",
40 | "**/*.{js,jsx,tsx,ts,less,md,json}": [
41 | "prettier --write"
42 | ]
43 | },
44 | "browserslist": [
45 | "> 1%",
46 | "last 2 versions",
47 | "not ie <= 10"
48 | ],
49 | "dependencies": {
50 | "@ant-design/icons": "^4.7.0",
51 | "@ant-design/pro-descriptions": "^1.10.0",
52 | "@ant-design/pro-form": "^1.52.0",
53 | "@ant-design/pro-layout": "^6.32.0",
54 | "@ant-design/pro-table": "^2.61.0",
55 | "@umijs/route-utils": "^2.0.0",
56 | "antd": "^4.17.0",
57 | "antd-nestable": "^1.0.17",
58 | "classnames": "^2.3.0",
59 | "lodash": "^4.17.0",
60 | "moment": "^2.29.0",
61 | "omit.js": "^2.0.2",
62 | "rc-menu": "^9.1.0",
63 | "rc-util": "^5.16.0",
64 | "react": "^17.0.0",
65 | "react-dev-inspector": "^1.7.0",
66 | "react-dom": "^17.0.0",
67 | "react-helmet-async": "^1.2.0",
68 | "umi": "^3.5.0"
69 | },
70 | "devDependencies": {
71 | "@ant-design/pro-cli": "^2.1.0",
72 | "@playwright/test": "^1.17.0",
73 | "@types/express": "^4.17.0",
74 | "@types/history": "^4.7.0",
75 | "@types/jest": "^26.0.0",
76 | "@types/lodash": "^4.14.0",
77 | "@types/react": "^17.0.0",
78 | "@types/react-dom": "^17.0.0",
79 | "@types/react-helmet": "^6.1.0",
80 | "@umijs/fabric": "^2.8.0",
81 | "@umijs/openapi": "^1.3.0",
82 | "@umijs/plugin-blocks": "^2.2.0",
83 | "@umijs/plugin-esbuild": "^1.4.0",
84 | "@umijs/plugin-openapi": "^1.3.0",
85 | "@umijs/preset-ant-design-pro": "^1.3.0",
86 | "@umijs/preset-dumi": "^1.1.0",
87 | "@umijs/preset-react": "^1.8.17",
88 | "@umijs/yorkie": "^2.0.5",
89 | "carlo": "^0.9.46",
90 | "cross-env": "^7.0.0",
91 | "cross-port-killer": "^1.3.0",
92 | "detect-installer": "^1.0.0",
93 | "enzyme": "^3.11.0",
94 | "eslint": "^7.32.0",
95 | "express": "^4.17.0",
96 | "gh-pages": "^3.2.0",
97 | "jsdom-global": "^3.0.0",
98 | "lint-staged": "^10.0.0",
99 | "mockjs": "^1.1.0",
100 | "prettier": "^2.5.0",
101 | "puppeteer-core": "^8.0.0",
102 | "stylelint": "^13.0.0",
103 | "swagger-ui-react": "^3.52.0",
104 | "typescript": "^4.5.0",
105 | "umi-serve": "^1.9.10"
106 | },
107 | "engines": {
108 | "node": ">=10.0.0"
109 | },
110 | "gitHooks": {
111 | "commit-msg": "fabric verify-commit"
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/public/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OutUI/out-admin-react/3470d8b926d17d75529268a61edcf2bf96f4ce00/public/.DS_Store
--------------------------------------------------------------------------------
/public/CNAME:
--------------------------------------------------------------------------------
1 | preview.pro.ant.design
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OutUI/out-admin-react/3470d8b926d17d75529268a61edcf2bf96f4ce00/public/favicon.ico
--------------------------------------------------------------------------------
/public/icons/icon-128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OutUI/out-admin-react/3470d8b926d17d75529268a61edcf2bf96f4ce00/public/icons/icon-128x128.png
--------------------------------------------------------------------------------
/public/icons/icon-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OutUI/out-admin-react/3470d8b926d17d75529268a61edcf2bf96f4ce00/public/icons/icon-192x192.png
--------------------------------------------------------------------------------
/public/icons/icon-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OutUI/out-admin-react/3470d8b926d17d75529268a61edcf2bf96f4ce00/public/icons/icon-512x512.png
--------------------------------------------------------------------------------
/public/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/logo1.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/pro_icon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/access.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @see https://umijs.org/zh-CN/plugins/plugin-access
3 | * */
4 | export default function access(initialState: {
5 | currentUser?: API.CurrentUser | undefined;
6 | }) {
7 | const { currentUser } = initialState || {};
8 | return {
9 | canAdmin: currentUser && currentUser.name === "admin",
10 | };
11 | }
12 |
--------------------------------------------------------------------------------
/src/app.tsx:
--------------------------------------------------------------------------------
1 | import type { Settings as LayoutSettings } from "@ant-design/pro-layout";
2 | import { SettingDrawer } from "@ant-design/pro-layout";
3 | import { PageLoading } from "@ant-design/pro-layout";
4 | import type { RequestConfig, RunTimeLayoutConfig } from "umi";
5 | import { history } from "umi";
6 | import type { RequestOptionsInit } from "umi-request";
7 | import RightContent from "@/components/RightContent";
8 | import Footer from "@/components/Footer";
9 | import { currentUser as queryCurrentUser } from "./services/ant-design-pro/api";
10 | import { LogoutOutlined } from "@ant-design/icons";
11 | import defaultSettings from "../config/defaultSettings";
12 | import { outLogin } from "@/services/ant-design-pro/api";
13 | import { stringify } from "querystring";
14 | import { notification } from "antd";
15 |
16 | const loginPath = "/user/login";
17 | const orderingPath = "/food";
18 |
19 | /** 获取用户信息比较慢的时候会展示一个 loading */
20 | export const initialStateConfig = {
21 | loading: ,
22 | };
23 |
24 | /**
25 | * 退出登录,并且将当前的 url 保存
26 | */
27 | const loginOut = async () => {
28 | await outLogin();
29 | const { query = {}, search, pathname } = history.location;
30 | const { redirect } = query;
31 | // Note: There may be security issues, please note
32 | if (window.location.pathname !== "/user/login" && !redirect) {
33 | history.replace({
34 | pathname: "/user/login",
35 | search: stringify({
36 | redirect: pathname + search,
37 | }),
38 | });
39 | }
40 | };
41 |
42 | /**
43 | * @see https://umijs.org/zh-CN/plugins/plugin-initial-state
44 | * */
45 | export async function getInitialState(): Promise<{
46 | settings?: Partial;
47 | currentUser?: API.CurrentUser;
48 | loading?: boolean;
49 | fetchUserInfo?: () => Promise;
50 | }> {
51 | const fetchUserInfo = async () => {
52 | try {
53 | const msg = await queryCurrentUser();
54 | return msg.data;
55 | } catch (error) {
56 | history.push(loginPath);
57 | }
58 | return undefined;
59 | };
60 | // 如果是登录页面,不执行
61 | if (
62 | history.location.pathname !== loginPath &&
63 | history.location.pathname !== orderingPath
64 | ) {
65 | const currentUser = await fetchUserInfo();
66 | return {
67 | fetchUserInfo,
68 | currentUser,
69 | settings: defaultSettings,
70 | };
71 | }
72 | return {
73 | fetchUserInfo,
74 | settings: defaultSettings,
75 | };
76 | }
77 |
78 | // ProLayout 支持的api https://procomponents.ant.design/components/layout
79 | export const layout: RunTimeLayoutConfig = ({
80 | initialState,
81 | setInitialState,
82 | }) => {
83 | const updateSize = document.querySelector("body")?.offsetWidth;
84 | return {
85 | iconfontUrl: "//at.alicdn.com/t/font_3278601_3g0uzbi4yim.js",
86 | menuHeaderRender: (logo) => (
87 | <>
88 | {logo}
89 | {initialState?.currentUser?.name}
90 | >
91 | ),
92 | links: [
93 | {
96 | setInitialState((s) => ({ ...s, currentUser: undefined }));
97 | loginOut();
98 | }}
99 | >
100 |
101 | 退出
102 | ,
103 | ],
104 | headerRender: updateSize && updateSize < 765,
105 | breakpoint: "xs",
106 | //collapsed: false,
107 | //collapsedButtonRender: false,
108 | rightContentRender: () => ,
109 | disableContentMargin: false,
110 | footerRender: () => ,
111 | onPageChange: () => {
112 | const { location } = history;
113 | // 如果没有登录,重定向到 login
114 | if (
115 | !initialState?.currentUser &&
116 | location.pathname !== loginPath &&
117 | location.pathname !== orderingPath
118 | ) {
119 | history.push(loginPath);
120 | }
121 | },
122 | // 自定义 403 页面
123 | // unAccessible: unAccessible
,
124 | // 增加一个 loading 的状态
125 | childrenRender: (children, props) => {
126 | // if (initialState?.loading) return ;
127 | return (
128 | <>
129 | {children}
130 | {!props.location?.pathname?.includes("/login") &&
131 | !props.location?.pathname?.includes(orderingPath) &&
132 | true && (
133 | {
137 | setInitialState((preInitialState) => ({
138 | ...preInitialState,
139 | settings,
140 | }));
141 | }}
142 | />
143 | )}
144 | >
145 | );
146 | },
147 | ...initialState?.settings,
148 | };
149 | };
150 |
151 | // src/app.ts
152 | const authHeaderInterceptor = (url: string, options: RequestOptionsInit) => {
153 | const token: string | null = sessionStorage.getItem("token");
154 | const authHeader = { Authorization: `Bearer ${token}` };
155 | return {
156 | url: `${url}`,
157 | options: { ...options, interceptors: true, headers: authHeader },
158 | };
159 | };
160 |
161 | const codeMessage = {
162 | 200: "服务器成功返回请求的数据。",
163 | 201: "新建或修改数据成功。",
164 | 202: "一个请求已经进入后台排队(异步任务)。",
165 | 204: "删除数据成功。",
166 | 400: "发出的请求有错误,服务器没有进行新建或修改数据的操作。",
167 | 401: "用户没有权限(令牌、用户名、密码错误)。",
168 | 403: "用户得到授权,但是访问是被禁止的。",
169 | 404: "发出的请求针对的是不存在的记录,服务器没有进行操作。",
170 | 405: "请求方法不被允许。",
171 | 406: "请求的格式不可得。",
172 | 410: "请求的资源被永久删除,且不会再得到的。",
173 | 422: "当创建一个对象时,发生一个验证错误。",
174 | 500: "服务器发生错误,请检查服务器。",
175 | 502: "网关错误。",
176 | 503: "服务不可用,服务器暂时过载或维护。",
177 | 504: "网关超时。",
178 | };
179 |
180 | /**
181 | * 异常处理程序
182 | */
183 | const errorHandler = (error: {
184 | response: Response & Record;
185 | data: API.Response;
186 | }) => {
187 | const { response, data } = error;
188 | if (response && response.status) {
189 | const errorText =
190 | data?.message ||
191 | response?.message ||
192 | codeMessage[response.status] ||
193 | response.statusText;
194 | const { status } = response;
195 |
196 | notification.error({
197 | message: `请求错误 ${status}`,
198 | description: errorText,
199 | });
200 |
201 | if (status === 401) {
202 | history.push("/user/login");
203 | }
204 | }
205 |
206 | if (!response) {
207 | notification.error({
208 | description: "您的网络发生异常,无法连接服务器",
209 | message: "网络异常",
210 | });
211 | }
212 | throw new Error(error as unknown as string);
213 | };
214 |
215 | export const request: RequestConfig = {
216 | // 新增自动添加AccessToken的请求前拦截器
217 | requestInterceptors: [authHeaderInterceptor],
218 | errorHandler,
219 | errorConfig: {
220 | adaptor: (resData) => {
221 | return {
222 | ...resData,
223 | success: resData.code >= 200 && resData.code < 300,
224 | errorMessage: resData.message,
225 | };
226 | },
227 | },
228 | };
229 |
--------------------------------------------------------------------------------
/src/components/Footer/index.tsx:
--------------------------------------------------------------------------------
1 | import { DefaultFooter } from "@ant-design/pro-layout";
2 |
3 | const Footer: React.FC = () => {
4 | const defaultMessage = "";
5 | const currentYear = new Date().getFullYear();
6 | return ;
7 | };
8 |
9 | export default Footer;
10 |
--------------------------------------------------------------------------------
/src/components/HeaderDropdown/index.less:
--------------------------------------------------------------------------------
1 | @import (reference) "~antd/es/style/themes/index";
2 |
3 | .container > * {
4 | background-color: @popover-bg;
5 | border-radius: 4px;
6 | box-shadow: @shadow-1-down;
7 | }
8 |
9 | @media screen and (max-width: @screen-xs) {
10 | .container {
11 | width: 100% !important;
12 | }
13 | .container > * {
14 | border-radius: 0 !important;
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/components/HeaderDropdown/index.tsx:
--------------------------------------------------------------------------------
1 | import type { DropDownProps } from "antd/es/dropdown";
2 | import { Dropdown } from "antd";
3 | import React from "react";
4 | import classNames from "classnames";
5 | import styles from "./index.less";
6 |
7 | export type HeaderDropdownProps = {
8 | overlayClassName?: string;
9 | overlay: React.ReactNode | (() => React.ReactNode) | any;
10 | placement?:
11 | | "bottomLeft"
12 | | "bottomRight"
13 | | "topLeft"
14 | | "topCenter"
15 | | "topRight"
16 | | "bottomCenter";
17 | } & Omit;
18 |
19 | const HeaderDropdown: React.FC = ({
20 | overlayClassName: cls,
21 | ...restProps
22 | }) => (
23 |
27 | );
28 |
29 | export default HeaderDropdown;
30 |
--------------------------------------------------------------------------------
/src/components/HeaderSearch/index.less:
--------------------------------------------------------------------------------
1 | @import (reference) "~antd/es/style/themes/index";
2 |
3 | .headerSearch {
4 | display: inline-flex;
5 | align-items: center;
6 | .input {
7 | width: 0;
8 | min-width: 0;
9 | overflow: hidden;
10 | background: transparent;
11 | border-radius: 0;
12 | transition: width 0.3s, margin-left 0.3s;
13 | :global(.ant-select-selection) {
14 | background: transparent;
15 | }
16 | input {
17 | box-shadow: none !important;
18 | }
19 |
20 | &.show {
21 | width: 210px;
22 | margin-left: 8px;
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/components/HeaderSearch/index.tsx:
--------------------------------------------------------------------------------
1 | import { SearchOutlined } from "@ant-design/icons";
2 | import { AutoComplete, Input } from "antd";
3 | import useMergedState from "rc-util/es/hooks/useMergedState";
4 | import type { AutoCompleteProps } from "antd/es/auto-complete";
5 | import React, { useRef } from "react";
6 |
7 | import classNames from "classnames";
8 | import styles from "./index.less";
9 |
10 | export type HeaderSearchProps = {
11 | onSearch?: (value?: string) => void;
12 | onChange?: (value?: string) => void;
13 | onVisibleChange?: (b: boolean) => void;
14 | className?: string;
15 | placeholder?: string;
16 | options: AutoCompleteProps["options"];
17 | defaultVisible?: boolean;
18 | visible?: boolean;
19 | defaultValue?: string;
20 | value?: string;
21 | };
22 |
23 | const HeaderSearch: React.FC = (props) => {
24 | const {
25 | className,
26 | defaultValue,
27 | onVisibleChange,
28 | placeholder,
29 | visible,
30 | defaultVisible,
31 | ...restProps
32 | } = props;
33 |
34 | const inputRef = useRef(null);
35 |
36 | const [value, setValue] = useMergedState(defaultValue, {
37 | value: props.value,
38 | onChange: props.onChange,
39 | });
40 |
41 | const [searchMode, setSearchMode] = useMergedState(defaultVisible ?? false, {
42 | value: props.visible,
43 | onChange: onVisibleChange,
44 | });
45 |
46 | const inputClass = classNames(styles.input, {
47 | [styles.show]: searchMode,
48 | });
49 | return (
50 | {
53 | setSearchMode(true);
54 | if (searchMode && inputRef.current) {
55 | inputRef.current.focus();
56 | }
57 | }}
58 | onTransitionEnd={({ propertyName }) => {
59 | if (propertyName === "width" && !searchMode) {
60 | if (onVisibleChange) {
61 | onVisibleChange(searchMode);
62 | }
63 | }
64 | }}
65 | >
66 |
72 |
79 | {
86 | if (e.key === "Enter") {
87 | if (restProps.onSearch) {
88 | restProps.onSearch(value);
89 | }
90 | }
91 | }}
92 | onBlur={() => {
93 | setSearchMode(false);
94 | }}
95 | />
96 |
97 |
98 | );
99 | };
100 |
101 | export default HeaderSearch;
102 |
--------------------------------------------------------------------------------
/src/components/NoticeIcon/NoticeIcon.tsx:
--------------------------------------------------------------------------------
1 | import { BellOutlined } from "@ant-design/icons";
2 | import { Badge, Spin, Tabs } from "antd";
3 | import useMergedState from "rc-util/es/hooks/useMergedState";
4 | import React from "react";
5 | import classNames from "classnames";
6 | import type { NoticeIconTabProps } from "./NoticeList";
7 | import NoticeList from "./NoticeList";
8 | import HeaderDropdown from "../HeaderDropdown";
9 | import styles from "./index.less";
10 |
11 | const { TabPane } = Tabs;
12 |
13 | export type NoticeIconProps = {
14 | count?: number;
15 | bell?: React.ReactNode;
16 | className?: string;
17 | loading?: boolean;
18 | onClear?: (tabName: string, tabKey: string) => void;
19 | onItemClick?: (
20 | item: API.NoticeIconItem,
21 | tabProps: NoticeIconTabProps
22 | ) => void;
23 | onViewMore?: (tabProps: NoticeIconTabProps, e: MouseEvent) => void;
24 | onTabChange?: (tabTile: string) => void;
25 | style?: React.CSSProperties;
26 | onPopupVisibleChange?: (visible: boolean) => void;
27 | popupVisible?: boolean;
28 | clearText?: string;
29 | viewMoreText?: string;
30 | clearClose?: boolean;
31 | emptyImage?: string;
32 | children?: React.ReactElement[];
33 | };
34 |
35 | const NoticeIcon: React.FC & {
36 | Tab: typeof NoticeList;
37 | } = (props) => {
38 | const getNotificationBox = (): React.ReactNode => {
39 | const {
40 | children,
41 | loading,
42 | onClear,
43 | onTabChange,
44 | onItemClick,
45 | onViewMore,
46 | clearText,
47 | viewMoreText,
48 | } = props;
49 | if (!children) {
50 | return null;
51 | }
52 | const panes: React.ReactNode[] = [];
53 | React.Children.forEach(
54 | children,
55 | (child: React.ReactElement): void => {
56 | if (!child) {
57 | return;
58 | }
59 | const { list, title, count, tabKey, showClear, showViewMore } =
60 | child.props;
61 | const len = list && list.length ? list.length : 0;
62 | const msgCount = count || count === 0 ? count : len;
63 | const tabTitle: string =
64 | msgCount > 0 ? `${title} (${msgCount})` : title;
65 | panes.push(
66 |
67 | onClear && onClear(title, tabKey)}
73 | onClick={(item): void =>
74 | onItemClick && onItemClick(item, child.props)
75 | }
76 | onViewMore={(event): void =>
77 | onViewMore && onViewMore(child.props, event)
78 | }
79 | showClear={showClear}
80 | showViewMore={showViewMore}
81 | title={title}
82 | />
83 |
84 | );
85 | }
86 | );
87 | return (
88 | <>
89 |
90 |
91 | {panes}
92 |
93 |
94 | >
95 | );
96 | };
97 |
98 | const { className, count, bell } = props;
99 |
100 | const [visible, setVisible] = useMergedState(false, {
101 | value: props.popupVisible,
102 | onChange: props.onPopupVisibleChange,
103 | });
104 | const noticeButtonClass = classNames(className, styles.noticeButton);
105 | const notificationBox = getNotificationBox();
106 | const NoticeBellIcon = bell || ;
107 | const trigger = (
108 |
109 |
114 | {NoticeBellIcon}
115 |
116 |
117 | );
118 | if (!notificationBox) {
119 | return trigger;
120 | }
121 |
122 | return (
123 |
131 | {trigger}
132 |
133 | );
134 | };
135 |
136 | NoticeIcon.defaultProps = {
137 | emptyImage:
138 | "https://gw.alipayobjects.com/zos/rmsportal/wAhyIChODzsoKIOBHcBk.svg",
139 | };
140 |
141 | NoticeIcon.Tab = NoticeList;
142 |
143 | export default NoticeIcon;
144 |
--------------------------------------------------------------------------------
/src/components/NoticeIcon/NoticeList.less:
--------------------------------------------------------------------------------
1 | @import (reference) "~antd/es/style/themes/index";
2 |
3 | .list {
4 | max-height: 400px;
5 | overflow: auto;
6 | &::-webkit-scrollbar {
7 | display: none;
8 | }
9 | .item {
10 | padding-right: 24px;
11 | padding-left: 24px;
12 | overflow: hidden;
13 | cursor: pointer;
14 | transition: all 0.3s;
15 |
16 | .meta {
17 | width: 100%;
18 | }
19 |
20 | .avatar {
21 | margin-top: 4px;
22 | background: @component-background;
23 | }
24 | .iconElement {
25 | font-size: 32px;
26 | }
27 |
28 | &.read {
29 | opacity: 0.4;
30 | }
31 | &:last-child {
32 | border-bottom: 0;
33 | }
34 | &:hover {
35 | background: @primary-1;
36 | }
37 | .title {
38 | margin-bottom: 8px;
39 | font-weight: normal;
40 | }
41 | .description {
42 | font-size: 12px;
43 | line-height: @line-height-base;
44 | }
45 | .datetime {
46 | margin-top: 4px;
47 | font-size: 12px;
48 | line-height: @line-height-base;
49 | }
50 | .extra {
51 | float: right;
52 | margin-top: -1.5px;
53 | margin-right: 0;
54 | color: @text-color-secondary;
55 | font-weight: normal;
56 | }
57 | }
58 | .loadMore {
59 | padding: 8px 0;
60 | color: @primary-6;
61 | text-align: center;
62 | cursor: pointer;
63 | &.loadedAll {
64 | color: rgba(0, 0, 0, 0.25);
65 | cursor: unset;
66 | }
67 | }
68 | }
69 |
70 | .notFound {
71 | padding: 73px 0 88px;
72 | color: @text-color-secondary;
73 | text-align: center;
74 | img {
75 | display: inline-block;
76 | height: 76px;
77 | margin-bottom: 16px;
78 | }
79 | }
80 |
81 | .bottomBar {
82 | height: 46px;
83 | color: @text-color;
84 | line-height: 46px;
85 | text-align: center;
86 | border-top: 1px solid @border-color-split;
87 | border-radius: 0 0 @border-radius-base @border-radius-base;
88 | transition: all 0.3s;
89 | div {
90 | display: inline-block;
91 | width: 50%;
92 | cursor: pointer;
93 | transition: all 0.3s;
94 | user-select: none;
95 |
96 | &:only-child {
97 | width: 100%;
98 | }
99 | &:not(:only-child):last-child {
100 | border-left: 1px solid @border-color-split;
101 | }
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/src/components/NoticeIcon/NoticeList.tsx:
--------------------------------------------------------------------------------
1 | import { Avatar, List } from "antd";
2 |
3 | import React from "react";
4 | import classNames from "classnames";
5 | import styles from "./NoticeList.less";
6 |
7 | export type NoticeIconTabProps = {
8 | count?: number;
9 | showClear?: boolean;
10 | showViewMore?: boolean;
11 | style?: React.CSSProperties;
12 | title: string;
13 | tabKey: API.NoticeIconItemType;
14 | onClick?: (item: API.NoticeIconItem) => void;
15 | onClear?: () => void;
16 | emptyText?: string;
17 | clearText?: string;
18 | viewMoreText?: string;
19 | list: API.NoticeIconItem[];
20 | onViewMore?: (e: any) => void;
21 | };
22 | const NoticeList: React.FC = ({
23 | list = [],
24 | onClick,
25 | onClear,
26 | title,
27 | onViewMore,
28 | emptyText,
29 | showClear = true,
30 | clearText,
31 | viewMoreText,
32 | showViewMore = false,
33 | }) => {
34 | if (!list || list.length === 0) {
35 | return (
36 |
37 |

41 |
{emptyText}
42 |
43 | );
44 | }
45 | return (
46 |
47 |
48 | className={styles.list}
49 | dataSource={list}
50 | renderItem={(item, i) => {
51 | const itemCls = classNames(styles.item, {
52 | [styles.read]: item.read,
53 | });
54 | // eslint-disable-next-line no-nested-ternary
55 | const leftIcon = item.avatar ? (
56 | typeof item.avatar === "string" ? (
57 |
58 | ) : (
59 | {item.avatar}
60 | )
61 | ) : null;
62 |
63 | return (
64 | {
68 | onClick?.(item);
69 | }}
70 | >
71 |
76 | {item.title}
77 | {item.extra}
78 |
79 | }
80 | description={
81 |
82 |
{item.description}
83 |
{item.datetime}
84 |
85 | }
86 | />
87 |
88 | );
89 | }}
90 | />
91 |
92 | {showClear ? (
93 |
94 | {clearText} {title}
95 |
96 | ) : null}
97 | {showViewMore ? (
98 |
{
100 | if (onViewMore) {
101 | onViewMore(e);
102 | }
103 | }}
104 | >
105 | {viewMoreText}
106 |
107 | ) : null}
108 |
109 |
110 | );
111 | };
112 |
113 | export default NoticeList;
114 |
--------------------------------------------------------------------------------
/src/components/NoticeIcon/index.less:
--------------------------------------------------------------------------------
1 | @import (reference) "~antd/es/style/themes/index";
2 |
3 | .popover {
4 | position: relative;
5 | width: 336px;
6 | }
7 |
8 | .noticeButton {
9 | display: inline-block;
10 | cursor: pointer;
11 | transition: all 0.3s;
12 | }
13 | .icon {
14 | padding: 4px;
15 | vertical-align: middle;
16 | }
17 |
18 | .badge {
19 | font-size: 16px;
20 | }
21 |
22 | .tabs {
23 | :global {
24 | .ant-tabs-nav-list {
25 | margin: auto;
26 | }
27 |
28 | .ant-tabs-nav-scroll {
29 | text-align: center;
30 | }
31 | .ant-tabs-nav {
32 | margin-bottom: 0;
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/components/NoticeIcon/index.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import { Tag, message } from "antd";
3 | import { groupBy } from "lodash";
4 | import moment from "moment";
5 | import { useModel, useRequest } from "umi";
6 | import { getNotices } from "@/services/ant-design-pro/api";
7 |
8 | import NoticeIcon from "./NoticeIcon";
9 | import styles from "./index.less";
10 |
11 | export type GlobalHeaderRightProps = {
12 | fetchingNotices?: boolean;
13 | onNoticeVisibleChange?: (visible: boolean) => void;
14 | onNoticeClear?: (tabName?: string) => void;
15 | };
16 |
17 | const getNoticeData = (
18 | notices: API.NoticeIconItem[]
19 | ): Record => {
20 | if (!notices || notices.length === 0 || !Array.isArray(notices)) {
21 | return {};
22 | }
23 |
24 | const newNotices = notices.map((notice) => {
25 | const newNotice = { ...notice };
26 |
27 | if (newNotice.datetime) {
28 | newNotice.datetime = moment(notice.datetime as string).fromNow();
29 | }
30 |
31 | if (newNotice.id) {
32 | newNotice.key = newNotice.id;
33 | }
34 |
35 | if (newNotice.extra && newNotice.status) {
36 | const color = {
37 | todo: "",
38 | processing: "blue",
39 | urgent: "red",
40 | doing: "gold",
41 | }[newNotice.status];
42 | newNotice.extra = (
43 |
49 | {newNotice.extra}
50 |
51 | ) as any;
52 | }
53 |
54 | return newNotice;
55 | });
56 | return groupBy(newNotices, "type");
57 | };
58 |
59 | const getUnreadData = (noticeData: Record) => {
60 | const unreadMsg: Record = {};
61 | Object.keys(noticeData).forEach((key) => {
62 | const value = noticeData[key];
63 |
64 | if (!unreadMsg[key]) {
65 | unreadMsg[key] = 0;
66 | }
67 |
68 | if (Array.isArray(value)) {
69 | unreadMsg[key] = value.filter((item) => !item.read).length;
70 | }
71 | });
72 | return unreadMsg;
73 | };
74 |
75 | const NoticeIconView: React.FC = () => {
76 | const { initialState } = useModel("@@initialState");
77 | const { currentUser } = initialState || {};
78 | const [notices, setNotices] = useState([]);
79 | const { data } = useRequest(getNotices);
80 |
81 | useEffect(() => {
82 | setNotices(data || []);
83 | }, [data]);
84 |
85 | const noticeData = getNoticeData(notices);
86 | const unreadMsg = getUnreadData(noticeData || {});
87 |
88 | const changeReadState = (id: string) => {
89 | setNotices(
90 | notices.map((item) => {
91 | const notice = { ...item };
92 | if (notice.id === id) {
93 | notice.read = true;
94 | }
95 | return notice;
96 | })
97 | );
98 | };
99 |
100 | const clearReadState = (title: string, key: string) => {
101 | setNotices(
102 | notices.map((item) => {
103 | const notice = { ...item };
104 | if (notice.type === key) {
105 | notice.read = true;
106 | }
107 | return notice;
108 | })
109 | );
110 | message.success(`${"清空了"} ${title}`);
111 | };
112 |
113 | return (
114 | {
118 | changeReadState(item.id!);
119 | }}
120 | onClear={(title: string, key: string) => clearReadState(title, key)}
121 | loading={false}
122 | clearText="清空"
123 | viewMoreText="查看更多"
124 | onViewMore={() => message.info("Click on view more")}
125 | clearClose
126 | >
127 |
135 |
143 |
151 |
152 | );
153 | };
154 |
155 | export default NoticeIconView;
156 |
--------------------------------------------------------------------------------
/src/components/RightContent/AvatarDropdown.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback } from "react";
2 | import {
3 | LogoutOutlined,
4 | SettingOutlined,
5 | UserOutlined,
6 | } from "@ant-design/icons";
7 | import { Avatar, Menu, Spin } from "antd";
8 | import { history, useModel } from "umi";
9 | import { stringify } from "querystring";
10 | import HeaderDropdown from "../HeaderDropdown";
11 | import styles from "./index.less";
12 | import { outLogin } from "@/services/ant-design-pro/api";
13 | import type { MenuInfo } from "rc-menu/lib/interface";
14 |
15 | export type GlobalHeaderRightProps = {
16 | menu?: boolean;
17 | };
18 |
19 | /**
20 | * 退出登录,并且将当前的 url 保存
21 | */
22 | const loginOut = async () => {
23 | await outLogin();
24 | const { query = {}, search, pathname } = history.location;
25 | const { redirect } = query;
26 | // Note: There may be security issues, please note
27 | if (window.location.pathname !== "/user/login" && !redirect) {
28 | history.replace({
29 | pathname: "/user/login",
30 | search: stringify({
31 | redirect: pathname + search,
32 | }),
33 | });
34 | }
35 | };
36 |
37 | const AvatarDropdown: React.FC = ({ menu }) => {
38 | const { initialState, setInitialState } = useModel("@@initialState");
39 |
40 | const onMenuClick = useCallback(
41 | (event: MenuInfo) => {
42 | const { key } = event;
43 | if (key === "logout") {
44 | setInitialState((s) => ({ ...s, currentUser: undefined }));
45 | loginOut();
46 | return;
47 | }
48 | history.push(`/account/${key}`);
49 | },
50 | [setInitialState]
51 | );
52 |
53 | const loading = (
54 |
55 |
62 |
63 | );
64 |
65 | if (!initialState) {
66 | return loading;
67 | }
68 |
69 | const { currentUser } = initialState;
70 |
71 | if (!currentUser || !currentUser.name) {
72 | return loading;
73 | }
74 |
75 | const menuHeaderDropdown = (
76 |
96 | );
97 | return (
98 |
99 |
100 |
106 | {currentUser.name}
107 |
108 |
109 | );
110 | };
111 |
112 | export default AvatarDropdown;
113 |
--------------------------------------------------------------------------------
/src/components/RightContent/index.less:
--------------------------------------------------------------------------------
1 | @import (reference) "~antd/es/style/themes/index";
2 |
3 | @pro-header-hover-bg: rgba(0, 0, 0, 0.025);
4 |
5 | .menu {
6 | :global(.anticon) {
7 | margin-right: 8px;
8 | }
9 | :global(.ant-dropdown-menu-item) {
10 | min-width: 160px;
11 | }
12 | }
13 |
14 | .right {
15 | display: flex;
16 | float: right;
17 | height: 48px;
18 | margin-left: auto;
19 | overflow: hidden;
20 | .action {
21 | display: flex;
22 | align-items: center;
23 | height: 48px;
24 | padding: 0 12px;
25 | cursor: pointer;
26 | transition: all 0.3s;
27 | > span {
28 | vertical-align: middle;
29 | }
30 | &:hover {
31 | background: @pro-header-hover-bg;
32 | }
33 | &:global(.opened) {
34 | background: @pro-header-hover-bg;
35 | }
36 | }
37 | .search {
38 | padding: 0 12px;
39 | &:hover {
40 | background: transparent;
41 | }
42 | }
43 | .account {
44 | .avatar {
45 | margin-right: 8px;
46 | color: @primary-color;
47 | vertical-align: top;
48 | background: rgba(255, 255, 255, 0.85);
49 | }
50 | }
51 | }
52 |
53 | .dark {
54 | .action {
55 | &:hover {
56 | background: #252a3d;
57 | }
58 | &:global(.opened) {
59 | background: #252a3d;
60 | }
61 | }
62 | }
63 |
64 | @media only screen and (max-width: @screen-md) {
65 | :global(.ant-divider-vertical) {
66 | vertical-align: unset;
67 | }
68 | .name {
69 | display: none;
70 | }
71 | .right {
72 | position: absolute;
73 | top: 0;
74 | right: 12px;
75 | .account {
76 | .avatar {
77 | margin-right: 0;
78 | }
79 | }
80 | .search {
81 | display: none;
82 | }
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/src/components/RightContent/index.tsx:
--------------------------------------------------------------------------------
1 | import { Space } from "antd";
2 | import React from "react";
3 | import { useModel } from "umi";
4 | import Avatar from "./AvatarDropdown";
5 | import styles from "./index.less";
6 | export type SiderTheme = "light" | "dark";
7 |
8 | const GlobalHeaderRight: React.FC = () => {
9 | const { initialState } = useModel("@@initialState");
10 |
11 | if (!initialState || !initialState.settings) {
12 | return null;
13 | }
14 |
15 | const { navTheme, layout } = initialState.settings;
16 | let className = styles.right;
17 |
18 | if ((navTheme === "dark" && layout === "top") || layout === "mix") {
19 | className = `${styles.right} ${styles.dark}`;
20 | }
21 |
22 | return (
23 |
24 |
25 |
26 | );
27 | };
28 |
29 | export default GlobalHeaderRight;
30 |
--------------------------------------------------------------------------------
/src/components/ToolBar/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import "@ant-design/pro-table";
3 |
4 | interface ToolBarProps {
5 | headerTitle?: React.ReactNode; // 标题
6 | toolBarRender?: () => React.ReactNode[];
7 | toolBarOptionRender?: () => React.ReactNode[];
8 | }
9 |
10 | const ToolBar: React.FC = ({
11 | headerTitle,
12 | toolBarRender,
13 | toolBarOptionRender,
14 | }) => {
15 | const actions = toolBarRender ? toolBarRender() : [];
16 | const options = toolBarOptionRender ? toolBarOptionRender() : [];
17 | return (
18 |
19 |
20 | {headerTitle || "查询表格"}
21 |
22 |
23 |
24 | {actions
25 | .filter((item) => item)
26 | .map((node, index) => (
27 | // eslint-disable-next-line react/no-array-index-key
28 |
29 | {node}
30 |
31 | ))}
32 |
33 |
34 |
35 |
36 | {options
37 | .filter((item) => item)
38 | .map((node, index) => (
39 | // eslint-disable-next-line react/no-array-index-key
40 |
41 | {node}
42 |
43 | ))}
44 |
45 |
46 |
47 |
48 | );
49 | };
50 | export default ToolBar;
51 |
--------------------------------------------------------------------------------
/src/components/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: 业务组件
3 | sidemenu: false
4 | ---
5 |
6 | > 此功能由[dumi](https://d.umijs.org/zh-CN/guide/advanced#umi-%E9%A1%B9%E7%9B%AE%E9%9B%86%E6%88%90%E6%A8%A1%E5%BC%8F)提供,dumi 是一个 📖 为组件开发场景而生的文档工具,用过的都说好。
7 |
8 | # 业务组件
9 |
10 | 这里列举了 Pro 中所有用到的组件,这些组件不适合作为组件库,但是在业务中却真实需要。所以我们准备了这个文档,来指导大家是否需要使用这个组件。
11 |
12 | ## Footer 页脚组件
13 |
14 | 这个组件自带了一些 Pro 的配置,你一般都需要改掉它的信息。
15 |
16 | ```tsx
17 | /**
18 | * background: '#f0f2f5'
19 | */
20 | import React from "react";
21 | import Footer from "@/components/Footer";
22 |
23 | export default () => ;
24 | ```
25 |
26 | ## HeaderDropdown 头部下拉列表
27 |
28 | HeaderDropdown 是 antd Dropdown 的封装,但是增加了移动端的特殊处理,用法也是相同的。
29 |
30 | ```tsx
31 | /**
32 | * background: '#f0f2f5'
33 | */
34 | import { Button, Menu } from "antd";
35 | import React from "react";
36 | import HeaderDropdown from "@/components/HeaderDropdown";
37 |
38 | export default () => {
39 | const menuHeaderDropdown = (
40 |
46 | );
47 | return (
48 |
49 |
50 |
51 | );
52 | };
53 | ```
54 |
55 | ## HeaderSearch 头部搜索框
56 |
57 | 一个带补全数据的输入框,支持收起和展开 Input
58 |
59 | ```tsx
60 | /**
61 | * background: '#f0f2f5'
62 | */
63 | import { Button, Menu } from "antd";
64 | import React from "react";
65 | import HeaderSearch from "@/components/HeaderSearch";
66 |
67 | export default () => {
68 | return (
69 | {
88 | console.log("input", value);
89 | }}
90 | />
91 | );
92 | };
93 | ```
94 |
95 | ### API
96 |
97 | | 参数 | 说明 | 类型 | 默认值 |
98 | | --------------- | ---------------------------------- | ---------------------------- | ------ |
99 | | value | 输入框的值 | `string` | - |
100 | | onChange | 值修改后触发 | `(value?: string) => void` | - |
101 | | onSearch | 查询后触发 | `(value?: string) => void` | - |
102 | | options | 选项菜单的的列表 | `{label,value}[]` | - |
103 | | defaultVisible | 输入框默认是否显示,只有第一次生效 | `boolean` | - |
104 | | visible | 输入框是否显示 | `boolean` | - |
105 | | onVisibleChange | 输入框显示隐藏的回调函数 | `(visible: boolean) => void` | - |
106 |
107 | ## NoticeIcon 通知工具
108 |
109 | 通知工具提供一个展示多种通知信息的界面。
110 |
111 | ```tsx
112 | /**
113 | * background: '#f0f2f5'
114 | */
115 | import { message } from "antd";
116 | import React from "react";
117 | import NoticeIcon from "@/components/NoticeIcon/NoticeIcon";
118 |
119 | export default () => {
120 | const list = [
121 | {
122 | id: "000000001",
123 | avatar:
124 | "https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png",
125 | title: "你收到了 14 份新周报",
126 | datetime: "2017-08-09",
127 | type: "notification",
128 | },
129 | {
130 | id: "000000002",
131 | avatar:
132 | "https://gw.alipayobjects.com/zos/rmsportal/OKJXDXrmkNshAMvwtvhu.png",
133 | title: "你推荐的 曲妮妮 已通过第三轮面试",
134 | datetime: "2017-08-08",
135 | type: "notification",
136 | },
137 | ];
138 | return (
139 | {
142 | message.info(`${item.title} 被点击了`);
143 | }}
144 | onClear={(title: string, key: string) => message.info("点击了清空更多")}
145 | loading={false}
146 | clearText="清空"
147 | viewMoreText="查看更多"
148 | onViewMore={() => message.info("点击了查看更多")}
149 | clearClose
150 | >
151 |
159 |
167 |
175 |
176 | );
177 | };
178 | ```
179 |
180 | ### NoticeIcon API
181 |
182 | | 参数 | 说明 | 类型 | 默认值 |
183 | | -------------------- | -------------------------- | ------------------------------------------------------------------ | ------ |
184 | | count | 有多少未读通知 | `number` | - |
185 | | bell | 铃铛的图表 | `ReactNode` | - |
186 | | onClear | 点击清空数据按钮 | `(tabName: string, tabKey: string) => void` | - |
187 | | onItemClick | 未读消息列被点击 | `(item: API.NoticeIconData, tabProps: NoticeIconTabProps) => void` | - |
188 | | onViewMore | 查看更多的按钮点击 | `(tabProps: NoticeIconTabProps, e: MouseEvent) => void` | - |
189 | | onTabChange | 通知 Tab 的切换 | `(tabTile: string) => void;` | - |
190 | | popupVisible | 通知显示是否展示 | `boolean` | - |
191 | | onPopupVisibleChange | 通知信息显示隐藏的回调函数 | `(visible: boolean) => void` | - |
192 | | clearText | 清空按钮的文字 | `string` | - |
193 | | viewMoreText | 查看更多的按钮文字 | `string` | - |
194 | | clearClose | 展示清空按钮 | `boolean` | - |
195 | | emptyImage | 列表为空时的兜底展示 | `ReactNode` | - |
196 |
197 | ### NoticeIcon.Tab API
198 |
199 | | 参数 | 说明 | 类型 | 默认值 |
200 | | ------------ | ------------------ | ------------------------------------ | ------ |
201 | | count | 有多少未读通知 | `number` | - |
202 | | title | 通知 Tab 的标题 | `ReactNode` | - |
203 | | showClear | 展示清除按钮 | `boolean` | `true` |
204 | | showViewMore | 展示加载更 | `boolean` | `true` |
205 | | tabKey | Tab 的唯一 key | `string` | - |
206 | | onClick | 子项的单击事件 | `(item: API.NoticeIconData) => void` | - |
207 | | onClear | 清楚按钮的点击 | `()=>void` | - |
208 | | emptyText | 为空的时候测试 | `()=>void` | - |
209 | | viewMoreText | 查看更多的按钮文字 | `string` | - |
210 | | onViewMore | 查看更多的按钮点击 | `( e: MouseEvent) => void` | - |
211 | | list | 通知信息的列表 | `API.NoticeIconData` | - |
212 |
213 | ### NoticeIconData
214 |
215 | ```tsx | pure
216 | export interface NoticeIconData {
217 | id: string;
218 | key: string;
219 | avatar: string;
220 | title: string;
221 | datetime: string;
222 | type: string;
223 | read?: boolean;
224 | description: string;
225 | clickClose?: boolean;
226 | extra: any;
227 | status: string;
228 | }
229 | ```
230 |
231 | ## RightContent
232 |
233 | RightContent 是以上几个组件的组合,同时新增了 plugins 的 `SelectLang` 插件。
234 |
235 | ```tsx | pure
236 |
237 | umi ui,
243 | value: "umi ui",
244 | },
245 | {
246 | label: Ant Design,
247 | value: "Ant Design",
248 | },
249 | {
250 | label: Pro Table,
251 | value: "Pro Table",
252 | },
253 | {
254 | label: Pro Layout,
255 | value: "Pro Layout",
256 | },
257 | ]}
258 | />
259 |
260 | {
263 | window.location.href = "https://pro.ant.design/docs/getting-started";
264 | }}
265 | >
266 |
267 |
268 |
269 |
270 | {REACT_APP_ENV && (
271 |
272 | {REACT_APP_ENV}
273 |
274 | )}
275 |
276 |
277 | ```
278 |
--------------------------------------------------------------------------------
/src/e2e/baseLayout.e2e.spec.ts:
--------------------------------------------------------------------------------
1 | import type { Page } from "@playwright/test";
2 | import { test, expect } from "@playwright/test";
3 | const { uniq } = require("lodash");
4 | const RouterConfig = require("../../config/routes").default;
5 |
6 | const BASE_URL = `http://localhost:${process.env.PORT || 8001}`;
7 |
8 | function formatter(routes: any, parentPath = ""): string[] {
9 | const fixedParentPath = parentPath.replace(/\/{1,}/g, "/");
10 | let result: string[] = [];
11 | routes.forEach((item: { path: string; routes: string }) => {
12 | if (item.path && !item.path.startsWith("/")) {
13 | result.push(`${fixedParentPath}/${item.path}`.replace(/\/{1,}/g, "/"));
14 | }
15 | if (item.path && item.path.startsWith("/")) {
16 | result.push(`${item.path}`.replace(/\/{1,}/g, "/"));
17 | }
18 | if (item.routes) {
19 | result = result.concat(
20 | formatter(
21 | item.routes,
22 | item.path ? `${fixedParentPath}/${item.path}` : parentPath
23 | )
24 | );
25 | }
26 | });
27 | return uniq(result.filter((item) => !!item));
28 | }
29 |
30 | const testPage = (path: string, page: Page) => async () => {
31 | await page.evaluate(() => {
32 | localStorage.setItem("antd-pro-authority", '["admin"]');
33 | });
34 | await page.goto(`${BASE_URL}${path}`);
35 | await page.waitForSelector("footer", {
36 | timeout: 2000,
37 | });
38 | const haveFooter = await page.evaluate(
39 | () => document.getElementsByTagName("footer").length > 0
40 | );
41 | expect(haveFooter).toBeTruthy();
42 | };
43 |
44 | const routers = formatter(RouterConfig);
45 |
46 | routers.forEach((route) => {
47 | test(`test route page ${route}`, async ({ page }) => {
48 | await testPage(route, page);
49 | });
50 | });
51 |
--------------------------------------------------------------------------------
/src/global.less:
--------------------------------------------------------------------------------
1 | @import "~antd/es/style/variable.less";
2 |
3 | html,
4 | body,
5 | #root {
6 | height: 100%;
7 | }
8 |
9 | .colorWeak {
10 | filter: invert(80%);
11 | }
12 |
13 | .ant-layout {
14 | min-height: 100vh;
15 | }
16 | .ant-pro-sider.ant-layout-sider.ant-pro-sider-fixed {
17 | left: unset;
18 | }
19 | .ant-pro-sider-logo {
20 | flex-direction: column;
21 | justify-content: center;
22 | padding: 5px 16px 0;
23 | .user {
24 | display: block;
25 | margin-top: 5px;
26 | color: #fff;
27 | }
28 | }
29 | .ant-menu-item .ant-menu-item-icon,
30 | .ant-menu-submenu-title .ant-menu-item-icon,
31 | .ant-menu-item .anticon,
32 | .ant-menu-submenu-title .anticon {
33 | min-width: 18px;
34 | font-size: 18px;
35 | }
36 | .ant-menu-item .ant-menu-item-icon + span,
37 | .ant-menu-submenu-title .ant-menu-item-icon + span,
38 | .ant-menu-item .anticon + span,
39 | .ant-menu-submenu-title .anticon + span {
40 | margin-left: 5px;
41 | }
42 | .ant-menu-vertical > .ant-menu-item,
43 | .ant-menu-vertical-left > .ant-menu-item,
44 | .ant-menu-vertical-right > .ant-menu-item,
45 | .ant-menu-inline > .ant-menu-item,
46 | .ant-menu-vertical > .ant-menu-submenu > .ant-menu-submenu-title,
47 | .ant-menu-vertical-left > .ant-menu-submenu > .ant-menu-submenu-title,
48 | .ant-menu-vertical-right > .ant-menu-submenu > .ant-menu-submenu-title,
49 | .ant-menu-inline > .ant-menu-submenu > .ant-menu-submenu-title {
50 | height: 60px;
51 | line-height: 60px;
52 | }
53 | .ant-menu-sub.ant-menu-inline > .ant-menu-item,
54 | .ant-menu-sub.ant-menu-inline > .ant-menu-submenu > .ant-menu-submenu-title {
55 | padding-left: 15px !important;
56 | font-size: 12px;
57 | }
58 | .ant-menu-submenu-expand-icon,
59 | .ant-menu-submenu-arrow {
60 | right: 10px;
61 | }
62 | .ant-menu-inline .ant-menu-item-group-list .ant-menu-submenu-title,
63 | .ant-menu-inline .ant-menu-submenu-title {
64 | padding-right: 20px;
65 | }
66 | .ant-menu {
67 | font-size: 16px;
68 | }
69 |
70 | canvas {
71 | display: block;
72 | }
73 |
74 | body {
75 | text-rendering: optimizeLegibility;
76 | -webkit-font-smoothing: antialiased;
77 | -moz-osx-font-smoothing: grayscale;
78 | }
79 |
80 | ul,
81 | ol {
82 | list-style: none;
83 | }
84 |
85 | @media (max-width: @screen-xs) {
86 | .ant-table {
87 | width: 100%;
88 | overflow-x: auto;
89 | &-thead > tr,
90 | &-tbody > tr {
91 | > th,
92 | > td {
93 | white-space: pre;
94 | > span {
95 | display: block;
96 | }
97 | }
98 | }
99 | }
100 | }
101 |
102 | // Compatible with IE11
103 | @media screen and(-ms-high-contrast: active), (-ms-high-contrast: none) {
104 | body .ant-design-pro > .ant-layout {
105 | min-height: 100vh;
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/src/global.tsx:
--------------------------------------------------------------------------------
1 | import { Button, message, notification } from "antd";
2 | import defaultSettings from "../config/defaultSettings";
3 | const { pwa } = defaultSettings;
4 | const isHttps = document.location.protocol === "https:";
5 |
6 | const clearCache = () => {
7 | // remove all caches
8 | if (window.caches) {
9 | caches
10 | .keys()
11 | .then((keys) => {
12 | keys.forEach((key) => {
13 | caches.delete(key);
14 | });
15 | })
16 | .catch((e) => console.log(e));
17 | }
18 | }; // if pwa is true
19 |
20 | if (pwa) {
21 | // Notify user if offline now
22 | window.addEventListener("sw.offline", () => {
23 | message.warning("当前处于离线状态");
24 | }); // Pop up a prompt on the page asking the user if they want to use the latest version
25 |
26 | window.addEventListener("sw.updated", (event: Event) => {
27 | const e = event as CustomEvent;
28 |
29 | const reloadSW = async () => {
30 | // Check if there is sw whose state is waiting in ServiceWorkerRegistration
31 | // https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration
32 | const worker = e.detail && e.detail.waiting;
33 |
34 | if (!worker) {
35 | return true;
36 | } // Send skip-waiting event to waiting SW with MessageChannel
37 |
38 | await new Promise((resolve, reject) => {
39 | const channel = new MessageChannel();
40 |
41 | channel.port1.onmessage = (msgEvent) => {
42 | if (msgEvent.data.error) {
43 | reject(msgEvent.data.error);
44 | } else {
45 | resolve(msgEvent.data);
46 | }
47 | };
48 |
49 | worker.postMessage(
50 | {
51 | type: "skip-waiting",
52 | },
53 | [channel.port2]
54 | );
55 | });
56 | clearCache();
57 | window.location.reload();
58 | return true;
59 | };
60 |
61 | const key = `open${Date.now()}`;
62 | const btn = (
63 |
72 | );
73 | notification.open({
74 | message: "有新内容",
75 | description: "请点击“刷新”按钮或者手动刷新页面",
76 | btn,
77 | key,
78 | onClose: async () => null,
79 | });
80 | });
81 | } else if ("serviceWorker" in navigator && isHttps) {
82 | // unregister service worker
83 | const { serviceWorker } = navigator;
84 |
85 | if (serviceWorker.getRegistrations) {
86 | serviceWorker.getRegistrations().then((sws) => {
87 | sws.forEach((sw) => {
88 | sw.unregister();
89 | });
90 | });
91 | }
92 |
93 | serviceWorker.getRegistration().then((sw) => {
94 | if (sw) sw.unregister();
95 | });
96 | clearCache();
97 | }
98 |
--------------------------------------------------------------------------------
/src/layouts/BaseLayout.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Ant Design Pro v4 use `@ant-design/pro-layout` to handle Layout.
3 | *
4 | * @see You can view component api by: https://github.com/ant-design/ant-design-pro-layout
5 | */
6 | import type {
7 | MenuDataItem,
8 | BasicLayoutProps as ProLayoutProps,
9 | Settings,
10 | } from "@ant-design/pro-layout";
11 | import ProLayout from "@ant-design/pro-layout";
12 | import React from "react";
13 | import { Link } from "umi";
14 | import RightContent from "@/components/RightContent";
15 | import { history } from "umi";
16 |
17 | export type BasicLayoutProps = {
18 | breadcrumbNameMap: Record;
19 | route: ProLayoutProps["route"] & {
20 | authority: string[];
21 | };
22 | settings: Settings;
23 | } & ProLayoutProps;
24 |
25 | export type BasicLayoutContext = { [K in "location"]: BasicLayoutProps[K] } & {
26 | breadcrumbNameMap: Record;
27 | };
28 |
29 | const menuDataRender = (menuList: MenuDataItem[]): MenuDataItem[] =>
30 | menuList.map((item) => {
31 | return {
32 | ...item,
33 | children: item.children ? menuDataRender(item.children) : undefined,
34 | };
35 | });
36 |
37 | const BasicLayout: React.FC = (props) => {
38 | const {
39 | children,
40 | location = {
41 | pathname: "/",
42 | },
43 | } = props;
44 |
45 | return (
46 | history.push("/")}
49 | menuItemRender={(menuItemProps, defaultDom) => {
50 | if (
51 | menuItemProps.isUrl ||
52 | !menuItemProps.path ||
53 | location.pathname === menuItemProps.path
54 | ) {
55 | return defaultDom;
56 | }
57 | return {defaultDom};
58 | }}
59 | breadcrumbRender={(routers = []) => [
60 | {
61 | path: "/",
62 | breadcrumbName: "",
63 | },
64 | ...routers,
65 | ]}
66 | itemRender={(route, params, routes, paths) => {
67 | const first = routes.indexOf(route) === 0;
68 | return first ? (
69 | {route.breadcrumbName}
70 | ) : (
71 | {route.breadcrumbName}
72 | );
73 | }}
74 | //footerRender={false}
75 | menuDataRender={menuDataRender}
76 | menuFooterRender={() => 11111
}
77 | rightContentRender={() => }
78 | collapsed={false}
79 | >
80 | {children}
81 |
82 | );
83 | };
84 |
85 | export default BasicLayout;
86 |
--------------------------------------------------------------------------------
/src/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Ant Design Pro",
3 | "short_name": "Ant Design Pro",
4 | "display": "standalone",
5 | "start_url": "./?utm_source=homescreen",
6 | "theme_color": "#002140",
7 | "background_color": "#001529",
8 | "icons": [
9 | {
10 | "src": "icons/icon-192x192.png",
11 | "sizes": "192x192"
12 | },
13 | {
14 | "src": "icons/icon-128x128.png",
15 | "sizes": "128x128"
16 | },
17 | {
18 | "src": "icons/icon-512x512.png",
19 | "sizes": "512x512"
20 | }
21 | ]
22 | }
23 |
--------------------------------------------------------------------------------
/src/pages/404.tsx:
--------------------------------------------------------------------------------
1 | import { Button, Result } from "antd";
2 | import React from "react";
3 | import { history } from "umi";
4 |
5 | const NoFoundPage: React.FC = () => (
6 | history.push("/")}>
12 | Back Home
13 |
14 | }
15 | />
16 | );
17 |
18 | export default NoFoundPage;
19 |
--------------------------------------------------------------------------------
/src/pages/Admin.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { HeartTwoTone, SmileTwoTone } from "@ant-design/icons";
3 | import { Card, Typography, Alert } from "antd";
4 | import { PageHeaderWrapper } from "@ant-design/pro-layout";
5 |
6 | const Admin: React.FC = () => {
7 | return (
8 |
9 |
10 |
20 |
26 | Ant Design Pro{" "}
27 | You
28 |
29 |
30 |
36 | Want to add more pages? Please refer to{" "}
37 |
42 | use block
43 |
44 | 。
45 |
46 |
47 | );
48 | };
49 |
50 | export default Admin;
51 |
--------------------------------------------------------------------------------
/src/pages/Table/index.less:
--------------------------------------------------------------------------------
1 | .status {
2 | margin-top: 20px;
3 | text-align: center;
4 |
5 | :global {
6 | .ant-badge {
7 | margin-right: 30px;
8 | }
9 | .ant-badge-status-dot {
10 | width: 10px;
11 | height: 10px;
12 | }
13 | }
14 | }
15 | .detailDrawer {
16 | :global {
17 | .ant-drawer-header {
18 | padding: 10px 15px;
19 | }
20 | .ant-drawer-close {
21 | order: 2;
22 | }
23 | .ant-drawer-body {
24 | padding: 15px;
25 | }
26 | .ant-btn {
27 | min-width: 70px;
28 | }
29 |
30 | .title {
31 | color: #999;
32 | }
33 | }
34 | }
35 | .orderDetail {
36 | margin-top: 15px;
37 | }
38 | :global {
39 | .__tables {
40 | .ant-pro-card-extra {
41 | color: #999;
42 | }
43 | .__green {
44 | .ant-pro-card-header .ant-pro-card-extra {
45 | color: #666;
46 | }
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/pages/Table/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { Affix, Badge, Button, Col, Drawer, Row, Space, Table } from "antd";
3 | import ProCard from "@ant-design/pro-card";
4 | import styles from "./index.less";
5 | import { DeleteOutlined } from "@ant-design/icons";
6 |
7 | const colSpan = { xs: 12, sm: 8, md: 4, lg: 3, xl: 3 };
8 |
9 | const setStatus = (i: number) => {
10 | let color = "";
11 | if (i === 20) {
12 | color = "#ffac27";
13 | } else if (i === 21) {
14 | color = "#53c7a0";
15 | } else if (i === 22) {
16 | color = "#ed6855";
17 | } else if (i === 23) {
18 | color = "#7ebeec";
19 | }
20 | return color;
21 | };
22 |
23 | const renderTables: any = ({ openDrawer }: any) => {
24 | const tables: any = [];
25 | for (let i = 1; i <= 60; i++) {
26 | tables.push(
27 | {i}号桌}
32 | extra={"8座"}
33 | size="small"
34 | bordered
35 | style={{ minHeight: 90, background: setStatus(i) }}
36 | bodyStyle={{ padding: "12px 12px 5px" }}
37 | onClick={openDrawer}
38 | >
39 | {i >= 20 && i <= 23 && (
40 | <>
41 |
42 | 500元
43 |
44 |
45 | 3人
46 |
47 | 12:00
48 |
49 |
50 | >
51 | )}
52 |
53 | );
54 | }
55 | return tables;
56 | };
57 |
58 | const Tables: React.FC = () => {
59 | const [drawerVisible, setDrawerVisible] = useState(false);
60 | const [, setCurrentTable] = useState();
61 |
62 | const drawerProps = {
63 | onClose() {
64 | setDrawerVisible(false);
65 | },
66 | };
67 |
68 | const openDrawer = () => {
69 | setDrawerVisible(true);
70 | setCurrentTable(undefined);
71 | };
72 |
73 | const dataSource = [
74 | {
75 | key: "1",
76 | name: "辣椒炒肉",
77 | total: 1,
78 | money: 30,
79 | remark: "少辣,少葱",
80 | },
81 | {
82 | key: "2",
83 | name: "土豆丝",
84 | total: 1,
85 | money: 10,
86 | },
87 | {
88 | key: "3",
89 | name: "黄焖鸡",
90 | total: 1,
91 | money: 58,
92 | },
93 | {
94 | key: "4",
95 | name: "精品特色铁板-烧牛肉",
96 | total: 3,
97 | money: 50,
98 | remark: "少辣",
99 | },
100 | {
101 | key: "44",
102 | name: "小龙虾",
103 | total: 3,
104 | money: 50,
105 | },
106 | {
107 | key: "5",
108 | name: "小龙虾",
109 | total: 3,
110 | money: 50,
111 | },
112 | {
113 | key: "6",
114 | name: "小龙虾",
115 | total: 3,
116 | money: 50,
117 | },
118 | ];
119 |
120 | const columns = [
121 | {
122 | title: "名称",
123 | dataIndex: "name",
124 | key: "name",
125 | width: "35%",
126 | },
127 | {
128 | title: "数量",
129 | dataIndex: "total",
130 | key: "total",
131 | width: "15%",
132 | },
133 | {
134 | title: "金额",
135 | dataIndex: "money",
136 | key: "money",
137 | width: "18%",
138 | },
139 | {
140 | title: "口味",
141 | dataIndex: "remark",
142 | key: "remark",
143 | width: "20%",
144 | },
145 | {
146 | title: "",
147 | key: "action",
148 | with: 20,
149 | render: () => (
150 |
151 |
152 |
153 |
154 |
155 | ),
156 | },
157 | ];
158 |
159 | return (
160 |
161 |
162 | {renderTables({ openDrawer })}
163 |
164 |
165 |
166 |
167 |
168 |
169 |
177 |
178 |
179 | 订单号:001
180 |
181 |
182 | 桌位号:20
183 |
184 |
185 | 开台时间:2022-05-01 12:00:00
186 |
187 |
188 | 就餐人数:3
189 |
190 |
191 | 用餐时间:30分钟58秒
192 |
193 |
194 | 订单状态:已开单
195 |
196 |
197 | 备注:少辣、少葱、少油
198 |
199 |
200 | (
207 |
208 |
209 | 共8份
210 |
211 |
212 | 合计:¥80
213 |
214 |
215 | )}
216 | />
217 |
218 |
226 |
227 |
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 | );
245 | };
246 |
247 | export default Tables;
248 |
--------------------------------------------------------------------------------
/src/pages/TableList/components/UpdateForm.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Modal } from "antd";
3 | import {
4 | ProFormSelect,
5 | ProFormText,
6 | ProFormTextArea,
7 | StepsForm,
8 | ProFormRadio,
9 | ProFormDateTimePicker,
10 | } from "@ant-design/pro-form";
11 | export type FormValueType = {
12 | target?: string;
13 | template?: string;
14 | type?: string;
15 | time?: string;
16 | frequency?: string;
17 | } & Partial;
18 | export type UpdateFormProps = {
19 | onCancel: (flag?: boolean, formVals?: FormValueType) => void;
20 | onSubmit: (values: FormValueType) => Promise;
21 | updateModalVisible: boolean;
22 | values: Partial;
23 | };
24 |
25 | const UpdateForm: React.FC = (props) => {
26 | return (
27 | {
32 | return (
33 | {
43 | props.onCancel();
44 | }}
45 | >
46 | {dom}
47 |
48 | );
49 | }}
50 | onFinish={props.onSubmit}
51 | >
52 |
59 |
70 |
83 |
84 |
91 |
100 |
109 |
123 |
124 |
131 |
142 |
151 |
152 |
153 | );
154 | };
155 |
156 | export default UpdateForm;
157 |
--------------------------------------------------------------------------------
/src/pages/TableList/index.tsx:
--------------------------------------------------------------------------------
1 | import { PlusOutlined } from "@ant-design/icons";
2 | import { Button, message, Input, Drawer } from "antd";
3 | import React, { useState, useRef } from "react";
4 | import { PageContainer, FooterToolbar } from "@ant-design/pro-layout";
5 | import type { ProColumns, ActionType } from "@ant-design/pro-table";
6 | import ProTable from "@ant-design/pro-table";
7 | import { ModalForm, ProFormText, ProFormTextArea } from "@ant-design/pro-form";
8 | import type { ProDescriptionsItemProps } from "@ant-design/pro-descriptions";
9 | import ProDescriptions from "@ant-design/pro-descriptions";
10 | import type { FormValueType } from "./components/UpdateForm";
11 | import UpdateForm from "./components/UpdateForm";
12 | import {
13 | rule,
14 | addRule,
15 | updateRule,
16 | removeRule,
17 | } from "@/services/ant-design-pro/api";
18 | /**
19 | * @en-US Add node
20 | * @zh-CN 添加节点
21 | * @param fields
22 | */
23 |
24 | const handleAdd = async (fields: API.RuleListItem) => {
25 | const hide = message.loading("正在添加");
26 |
27 | try {
28 | await addRule({ ...fields });
29 | hide();
30 | message.success("Added successfully");
31 | return true;
32 | } catch (error) {
33 | hide();
34 | message.error("Adding failed, please try again!");
35 | return false;
36 | }
37 | };
38 | /**
39 | * @en-US Update node
40 | * @zh-CN 更新节点
41 | *
42 | * @param fields
43 | */
44 |
45 | const handleUpdate = async (fields: FormValueType) => {
46 | const hide = message.loading("Configuring");
47 |
48 | try {
49 | await updateRule({
50 | name: fields.name,
51 | desc: fields.desc,
52 | key: fields.key,
53 | });
54 | hide();
55 | message.success("Configuration is successful");
56 | return true;
57 | } catch (error) {
58 | hide();
59 | message.error("Configuration failed, please try again!");
60 | return false;
61 | }
62 | };
63 | /**
64 | * Delete node
65 | * @zh-CN 删除节点
66 | *
67 | * @param selectedRows
68 | */
69 |
70 | const handleRemove = async (selectedRows: API.RuleListItem[]) => {
71 | const hide = message.loading("正在删除");
72 | if (!selectedRows) return true;
73 |
74 | try {
75 | await removeRule({
76 | key: selectedRows.map((row) => row.key),
77 | });
78 | hide();
79 | message.success("Deleted successfully and will refresh soon");
80 | return true;
81 | } catch (error) {
82 | hide();
83 | message.error("Delete failed, please try again");
84 | return false;
85 | }
86 | };
87 |
88 | const TableList: React.FC = () => {
89 | /**
90 | * @en-US Pop-up window of new window
91 | * @zh-CN 新建窗口的弹窗
92 | * */
93 | const [createModalVisible, handleModalVisible] = useState(false);
94 | /**
95 | * @en-US The pop-up window of the distribution update window
96 | * @zh-CN 分布更新窗口的弹窗
97 | * */
98 |
99 | const [updateModalVisible, handleUpdateModalVisible] =
100 | useState(false);
101 | const [showDetail, setShowDetail] = useState(false);
102 | const actionRef = useRef();
103 | const [currentRow, setCurrentRow] = useState();
104 | const [selectedRowsState, setSelectedRows] = useState([]);
105 | /**
106 | * @en-US International configuration
107 | * @zh-CN 国际化配置
108 | * */
109 |
110 | const columns: ProColumns[] = [
111 | {
112 | title: "规则名称",
113 | dataIndex: "name",
114 | tip: "The rule name is the unique key",
115 | render: (dom, entity) => {
116 | return (
117 | {
119 | setCurrentRow(entity);
120 | setShowDetail(true);
121 | }}
122 | >
123 | {dom}
124 |
125 | );
126 | },
127 | },
128 | {
129 | title: "描述",
130 | dataIndex: "desc",
131 | valueType: "textarea",
132 | },
133 | {
134 | title: "服务调用次数",
135 | dataIndex: "callNo",
136 | sorter: true,
137 | hideInForm: true,
138 | renderText: (val: string) => `${val}${"万"}`,
139 | },
140 | {
141 | title: "状态",
142 | dataIndex: "status",
143 | hideInForm: true,
144 | valueEnum: {
145 | 0: {
146 | text: "关闭",
147 | status: "Default",
148 | },
149 | 1: {
150 | text: "运行中",
151 | status: "Processing",
152 | },
153 | 2: {
154 | text: "已上线",
155 | status: "Success",
156 | },
157 | 3: {
158 | text: "异常",
159 | status: "Error",
160 | },
161 | },
162 | },
163 | {
164 | title: "上次调度时间",
165 | sorter: true,
166 | dataIndex: "updatedAt",
167 | valueType: "dateTime",
168 | renderFormItem: (item, { defaultRender, ...rest }, form) => {
169 | const status = form.getFieldValue("status");
170 |
171 | if (`${status}` === "0") {
172 | return false;
173 | }
174 |
175 | if (`${status}` === "3") {
176 | return ;
177 | }
178 |
179 | return defaultRender(item);
180 | },
181 | },
182 | {
183 | title: "操作",
184 | dataIndex: "option",
185 | valueType: "option",
186 | render: (_, record) => [
187 | {
190 | handleUpdateModalVisible(true);
191 | setCurrentRow(record);
192 | }}
193 | >
194 | 配置
195 | ,
196 |
197 | 订阅警报
198 | ,
199 | ],
200 | },
201 | ];
202 | return (
203 |
204 |
205 | headerTitle={"查询表格"}
206 | actionRef={actionRef}
207 | rowKey="key"
208 | search={{
209 | labelWidth: 120,
210 | }}
211 | toolBarRender={() => [
212 | ,
221 | ]}
222 | request={rule}
223 | columns={columns}
224 | rowSelection={{
225 | onChange: (_, selectedRows) => {
226 | setSelectedRows(selectedRows);
227 | },
228 | }}
229 | />
230 | {selectedRowsState?.length > 0 && (
231 |
234 | 已选择{" "}
235 |
240 | {selectedRowsState.length}
241 | {" "}
242 | 项
243 |
244 | 服务调用次数总计{" "}
245 | {selectedRowsState.reduce((pre, item) => pre + item.callNo!, 0)}{" "}
246 | 万
247 |
248 |
249 | }
250 | >
251 |
260 |
261 |
262 | )}
263 | {
269 | const success = await handleAdd(value as API.RuleListItem);
270 |
271 | if (success) {
272 | handleModalVisible(false);
273 |
274 | if (actionRef.current) {
275 | actionRef.current.reload();
276 | }
277 | }
278 | }}
279 | >
280 |
290 |
291 |
292 | {
294 | const success = await handleUpdate(value);
295 |
296 | if (success) {
297 | handleUpdateModalVisible(false);
298 | setCurrentRow(undefined);
299 |
300 | if (actionRef.current) {
301 | actionRef.current.reload();
302 | }
303 | }
304 | }}
305 | onCancel={() => {
306 | handleUpdateModalVisible(false);
307 |
308 | if (!showDetail) {
309 | setCurrentRow(undefined);
310 | }
311 | }}
312 | updateModalVisible={updateModalVisible}
313 | values={currentRow || {}}
314 | />
315 |
316 | {
320 | setCurrentRow(undefined);
321 | setShowDetail(false);
322 | }}
323 | closable={false}
324 | >
325 | {currentRow?.name && (
326 |
327 | column={2}
328 | title={currentRow?.name}
329 | request={async () => ({
330 | data: currentRow || {},
331 | })}
332 | params={{
333 | id: currentRow?.name,
334 | }}
335 | columns={columns as ProDescriptionsItemProps[]}
336 | />
337 | )}
338 |
339 |
340 | );
341 | };
342 |
343 | export default TableList;
344 |
--------------------------------------------------------------------------------
/src/pages/Welcome.less:
--------------------------------------------------------------------------------
1 | @import (reference) "~antd/es/style/themes/index";
2 |
3 | .pre {
4 | margin: 12px 0;
5 | padding: 12px 20px;
6 | background: @input-bg;
7 | box-shadow: @card-shadow;
8 | }
9 |
--------------------------------------------------------------------------------
/src/pages/Welcome.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { PageContainer } from "@ant-design/pro-layout";
3 | import { Card, Alert, Typography } from "antd";
4 | import styles from "./Welcome.less";
5 |
6 | const CodePreview: React.FC = ({ children }) => (
7 |
8 |
9 | {children}
10 |
11 |
12 | );
13 |
14 | const Welcome: React.FC = () => {
15 | return (
16 |
17 |
18 |
28 |
29 | 高级表格{" "}
30 |
35 | 欢迎使用
36 |
37 |
38 | yarn add @ant-design/pro-table
39 |
45 | 高级布局{" "}
46 |
51 | 欢迎使用
52 |
53 |
54 | yarn add @ant-design/pro-layout
55 |
56 |
57 | );
58 | };
59 |
60 | export default Welcome;
61 |
--------------------------------------------------------------------------------
/src/pages/admin/logs/index.less:
--------------------------------------------------------------------------------
1 | @import "~antd/es/style/themes/default.less";
2 |
3 | .main {
4 | // width: 100%;
5 | // background: @component-background;
6 | }
7 |
--------------------------------------------------------------------------------
/src/pages/admin/logs/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useRef } from "react";
2 | import { PageContainer } from "@ant-design/pro-layout";
3 | import { DownOutlined } from "@ant-design/icons";
4 | import {
5 | Button,
6 | Divider,
7 | Dropdown,
8 | Menu,
9 | message,
10 | Tag,
11 | Popconfirm,
12 | } from "antd";
13 | import type { ProColumns, ActionType } from "@ant-design/pro-table";
14 | import ProTable from "@ant-design/pro-table";
15 | import type { SorterResult } from "antd/es/table/interface";
16 |
17 | import { queryLogs, removeLogs } from "@/services/admin/logs";
18 | import type { LogListItem } from "@/services/admin/logs.d";
19 |
20 | /**
21 | * 删除
22 | * @param selectedRows
23 | */
24 | const handleRemove = async (selectedRows: LogListItem[]) => {
25 | const hide = message.loading("正在删除");
26 | if (!selectedRows) return true;
27 | try {
28 | await removeLogs({
29 | ids: selectedRows.map((row) => row.id),
30 | });
31 | hide();
32 | message.success("删除成功");
33 | return true;
34 | } catch (error) {
35 | hide();
36 | message.error("删除失败,请重试");
37 | return false;
38 | }
39 | };
40 |
41 | const MethodTag: React.FC<{ text: string }> = ({ text }) => {
42 | let color = "#108ee9";
43 | switch (text) {
44 | case "ANY":
45 | color = "#108ee9";
46 | break;
47 | case "GET":
48 | color = "#52c41a";
49 | break;
50 | case "POST":
51 | color = "#faad14";
52 | break;
53 | case "PUT":
54 | color = "#1890ff";
55 | break;
56 | case "DELETE":
57 | color = "#ff4d4f";
58 | break;
59 | case "PATCH":
60 | color = "#13c2c2";
61 | break;
62 | case "OPTIONS":
63 | color = "#2f54eb";
64 | break;
65 | case "HEAD":
66 | color = "lime";
67 | break;
68 | default:
69 | }
70 | return (
71 |
72 | {text}
73 |
74 | );
75 | };
76 |
77 | export default () => {
78 | const [sorter, setSorter] = useState("");
79 | const actionRef = useRef();
80 | const columns: ProColumns[] = [
81 | {
82 | title: "ID",
83 | dataIndex: "id",
84 | hideInForm: true,
85 | ellipsis: true,
86 | fixed: "left",
87 | width: 80,
88 | },
89 | {
90 | title: "用户",
91 | dataIndex: "name",
92 | ellipsis: true,
93 | width: 120,
94 | formItemProps: {
95 | rules: [
96 | {
97 | required: true,
98 | message: "名称为必填项",
99 | },
100 | ],
101 | },
102 | },
103 | {
104 | title: "HTTP方法",
105 | dataIndex: "method",
106 | width: 140,
107 | valueEnum: {
108 | ANY: { text: "ANY", status: "Default" },
109 | GET: { text: "GET", status: "Default" },
110 | POST: { text: "POST", status: "Default" },
111 | PUT: { text: "PUT", status: "Default" },
112 | DELETE: { text: "DELETE", status: "Default" },
113 | PATCH: { text: "PATCH", status: "Default" },
114 | OPTIONS: { text: "OPTIONS", status: "Default" },
115 | HEAD: { text: "HEAD", status: "Default" },
116 | },
117 | formItemProps: {
118 | rules: [
119 | {
120 | required: true,
121 | message: "HTTP方法为必填项",
122 | },
123 | ],
124 | },
125 | render: (_, record: LogListItem) => (
126 | <>
127 | {record.method?.length > 0 ? (
128 |
129 | ) : (
130 | ANY
131 | )}
132 | >
133 | ),
134 | },
135 | {
136 | title: "HTTP路径",
137 | dataIndex: "path",
138 | render: (_, record: LogListItem) => (
139 | {record.path}
140 | ),
141 | },
142 | {
143 | title: "IP",
144 | dataIndex: "ip",
145 | render: (_, record: LogListItem) => (
146 | {record.ip}
147 | ),
148 | },
149 | {
150 | title: "参数",
151 | dataIndex: "input",
152 | hideInSearch: true,
153 | },
154 | {
155 | title: "创建时间",
156 | dataIndex: "createdAt",
157 | sorter: true,
158 | valueType: "dateTime",
159 | hideInSearch: true,
160 | hideInForm: true,
161 | },
162 | {
163 | title: "操作",
164 | dataIndex: "option",
165 | valueType: "option",
166 | width: 80,
167 | fixed: "right",
168 | render: (_, record) => (
169 | <>
170 |
171 | {
175 | // 不论是否删除成功,都重新加载列表数据
176 | await handleRemove([record]);
177 | actionRef?.current?.reload();
178 | }}
179 | style={{ width: 220 }}
180 | okText="确定"
181 | cancelText="取消"
182 | >
183 | 删除
184 |
185 | >
186 | ),
187 | },
188 | ];
189 |
190 | return (
191 |
192 |
193 | headerTitle="查询表格"
194 | actionRef={actionRef}
195 | rowKey="id"
196 | onChange={(_, _filter, _sorter) => {
197 | const sorterResult = _sorter as SorterResult;
198 | if (sorterResult.field && sorterResult.order) {
199 | setSorter(`${sorterResult.field}_${sorterResult.order}`);
200 | } else {
201 | setSorter("");
202 | }
203 | }}
204 | params={{
205 | sorter,
206 | }}
207 | toolBarRender={(action, { selectedRows }) => [
208 | selectedRows && selectedRows.length > 0 && (
209 | {
213 | if (e.key === "remove") {
214 | await handleRemove(selectedRows);
215 | action?.reload();
216 | }
217 | }}
218 | selectedKeys={[]}
219 | >
220 | 批量删除
221 |
222 | }
223 | >
224 |
227 |
228 | ),
229 | ]}
230 | tableAlertRender={({ selectedRowKeys }) => (
231 |
235 | )}
236 | request={async (params) => {
237 | const { list, total } = (await queryLogs(params)).data;
238 | return { data: list, total, success: true };
239 | }}
240 | columns={columns}
241 | rowSelection={{}}
242 | scroll={{ x: 1400 }}
243 | />
244 |
245 | );
246 | };
247 |
--------------------------------------------------------------------------------
/src/pages/admin/menus/components/BaseFormItems.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Form, Input, Transfer, Select, TreeSelect } from "antd";
3 | import { useRequest } from "umi";
4 |
5 | import { queryPermission } from "@/services/admin/permission";
6 | import { queryMenu } from "@/services/admin/menu";
7 | import { queryRole } from "@/services/admin/role";
8 |
9 | import { arrayTransTree } from "@/utils/utils";
10 |
11 | interface CustomFormItemProps {
12 | value?: any;
13 | onChange?: (values: any) => void;
14 | id?: string;
15 | }
16 |
17 | const BaseFormItems: React.FC<{
18 | disabledParentKeys?: string[];
19 | }> = ({ disabledParentKeys }) => {
20 | // 加载菜单数据
21 | const {
22 | data: menuData,
23 | loading: menuLoading,
24 | error: menuError,
25 | } = useRequest(() => {
26 | return queryMenu({ pageSize: 1000 });
27 | });
28 |
29 | // 预先加载权限选择器数据
30 | const {
31 | data: permissionData,
32 | loading: permissionLoading,
33 | error: permissionError,
34 | } = useRequest(() => {
35 | return queryPermission({ pageSize: 1000 });
36 | });
37 |
38 | // 预先加载角色选择器数据
39 | const {
40 | data: roleData,
41 | loading: roleLoading,
42 | error: roleError,
43 | } = useRequest(() => {
44 | return queryRole({ pageSize: 1000 });
45 | });
46 |
47 | const ParentFormItem: React.FC = ({
48 | value,
49 | onChange,
50 | }) => {
51 | const newValue = value || null;
52 | // 过滤默认选择的数据格式
53 | if (menuError) {
54 | return failed to load
;
55 | }
56 | if (menuLoading) {
57 | return loading...
;
58 | }
59 | const treeData =
60 | arrayTransTree(
61 | (menuData?.list as any[]).map((item) => ({
62 | ...item,
63 | title: item.name,
64 | value: item.id,
65 | disabled: disabledParentKeys?.includes(item.id),
66 | })),
67 | "parentId"
68 | ) || [];
69 | return (
70 |
80 | );
81 | };
82 |
83 | const PermissionsFormItem: React.FC = ({
84 | value,
85 | onChange,
86 | }) => {
87 | // 过滤默认选择的数据格式
88 | if (permissionError) {
89 | return failed to load
;
90 | }
91 | if (permissionLoading) {
92 | return loading...
;
93 | }
94 | return (
95 |