├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── question.md ├── .prettierignore ├── .prettierrc.js ├── .stylelintrc.js ├── LICENSE ├── README.md ├── config ├── config.ts ├── defaultSettings.ts └── plugin.config.ts ├── jest-puppeteer.config.js ├── jest.config.js ├── jsconfig.json ├── mock ├── notices.ts ├── route.ts └── user.ts ├── package.json ├── public ├── favicon.png └── icons │ ├── icon-128x128.png │ ├── icon-192x192.png │ └── icon-512x512.png ├── src ├── assets │ └── logo.svg ├── components │ ├── Authorized │ │ ├── Authorized.tsx │ │ ├── AuthorizedRoute.tsx │ │ ├── CheckPermissions.tsx │ │ ├── PromiseRender.tsx │ │ ├── Secured.tsx │ │ ├── index.tsx │ │ └── renderAuthorize.ts │ ├── CopyBlock │ │ ├── index.less │ │ └── index.tsx │ ├── GlobalHeader │ │ ├── AvatarDropdown.tsx │ │ ├── NoticeIconView.tsx │ │ ├── RightContent.tsx │ │ └── index.less │ ├── HeaderDropdown │ │ ├── index.less │ │ └── index.tsx │ ├── HeaderSearch │ │ ├── index.less │ │ └── index.tsx │ ├── NoticeIcon │ │ ├── NoticeList.less │ │ ├── NoticeList.tsx │ │ ├── index.less │ │ └── index.tsx │ ├── PageLoading │ │ └── index.tsx │ ├── SelectLang │ │ ├── index.less │ │ └── index.tsx │ └── SettingDrawer │ │ └── themeColorClient.ts ├── e2e │ ├── __mocks__ │ │ └── antd-pro-merge-less.js │ ├── baseLayout.e2e.js │ └── topMenu.e2e.js ├── global.less ├── global.tsx ├── layouts │ ├── BasicLayout.tsx │ ├── BlankLayout.tsx │ ├── SecurityLayout.tsx │ ├── UserLayout.less │ └── UserLayout.tsx ├── locales │ ├── en-US.ts │ ├── en-US │ │ ├── component.ts │ │ ├── globalHeader.ts │ │ ├── menu.ts │ │ ├── pwa.ts │ │ ├── settingDrawer.ts │ │ └── settings.ts │ ├── pt-BR.ts │ ├── pt-BR │ │ ├── component.ts │ │ ├── globalHeader.ts │ │ ├── menu.ts │ │ ├── pwa.ts │ │ ├── settingDrawer.ts │ │ └── settings.ts │ ├── zh-CN.ts │ ├── zh-CN │ │ ├── component.ts │ │ ├── globalHeader.ts │ │ ├── menu.ts │ │ ├── pwa.ts │ │ ├── settingDrawer.ts │ │ └── settings.ts │ ├── zh-TW.ts │ └── zh-TW │ │ ├── component.ts │ │ ├── globalHeader.ts │ │ ├── menu.ts │ │ ├── pwa.ts │ │ ├── settingDrawer.ts │ │ └── settings.ts ├── manifest.json ├── models │ ├── connect.d.ts │ ├── global.ts │ ├── login.ts │ ├── setting.ts │ └── user.ts ├── pages │ ├── 404.tsx │ ├── Authorized.tsx │ ├── analysis │ │ ├── _mock.ts │ │ ├── components │ │ │ ├── Charts │ │ │ │ ├── Bar │ │ │ │ │ └── index.tsx │ │ │ │ ├── ChartCard │ │ │ │ │ ├── index.less │ │ │ │ │ └── index.tsx │ │ │ │ ├── Field │ │ │ │ │ ├── index.less │ │ │ │ │ └── index.tsx │ │ │ │ ├── Gauge │ │ │ │ │ └── index.tsx │ │ │ │ ├── MiniArea │ │ │ │ │ └── index.tsx │ │ │ │ ├── MiniBar │ │ │ │ │ └── index.tsx │ │ │ │ ├── MiniProgress │ │ │ │ │ ├── index.less │ │ │ │ │ └── index.tsx │ │ │ │ ├── Pie │ │ │ │ │ ├── index.less │ │ │ │ │ └── index.tsx │ │ │ │ ├── TagCloud │ │ │ │ │ ├── index.less │ │ │ │ │ └── index.tsx │ │ │ │ ├── TimelineChart │ │ │ │ │ ├── index.less │ │ │ │ │ └── index.tsx │ │ │ │ ├── WaterWave │ │ │ │ │ ├── index.less │ │ │ │ │ └── index.tsx │ │ │ │ ├── autoHeight.tsx │ │ │ │ ├── bizcharts.d.ts │ │ │ │ ├── bizcharts.tsx │ │ │ │ ├── index.less │ │ │ │ └── index.tsx │ │ │ ├── IntroduceRow.tsx │ │ │ ├── NumberInfo │ │ │ │ ├── index.less │ │ │ │ └── index.tsx │ │ │ ├── OfflineData.tsx │ │ │ ├── PageLoading │ │ │ │ └── index.tsx │ │ │ ├── ProportionSales.tsx │ │ │ ├── SalesCard.tsx │ │ │ ├── TopSearch.tsx │ │ │ └── Trend │ │ │ │ ├── index.less │ │ │ │ └── index.tsx │ │ ├── data.d.ts │ │ ├── index.tsx │ │ ├── locales │ │ │ ├── en-US.ts │ │ │ ├── pt-BR.ts │ │ │ ├── zh-CN.ts │ │ │ └── zh-TW.ts │ │ ├── models │ │ │ └── analysis.ts │ │ ├── service.tsx │ │ ├── style.less │ │ └── utils │ │ │ ├── Yuan.tsx │ │ │ ├── utils.less │ │ │ └── utils.ts │ ├── configuration │ │ ├── jobList │ │ │ ├── components │ │ │ │ ├── CreateOrUpdateForm.tsx │ │ │ │ ├── StandardTable │ │ │ │ │ ├── index.less │ │ │ │ │ └── index.tsx │ │ │ │ └── style.less │ │ │ ├── data.d.ts │ │ │ ├── index.tsx │ │ │ ├── models │ │ │ │ └── jobList.ts │ │ │ ├── service.ts │ │ │ ├── style.less │ │ │ └── utils │ │ │ │ └── utils.less │ │ └── triggerList │ │ │ ├── components │ │ │ ├── CreateOrUpdateForm.tsx │ │ │ ├── StandardTable │ │ │ │ ├── index.less │ │ │ │ └── index.tsx │ │ │ └── style.less │ │ │ ├── data.d.ts │ │ │ ├── index.tsx │ │ │ ├── models │ │ │ └── triggerList.ts │ │ │ ├── service.ts │ │ │ ├── style.less │ │ │ └── utils │ │ │ └── utils.less │ ├── document.ejs │ ├── platform │ │ ├── jobRunDetail │ │ │ ├── data.d.ts │ │ │ ├── index.tsx │ │ │ ├── models │ │ │ │ └── springBatch.ts │ │ │ ├── service.ts │ │ │ ├── style.less │ │ │ └── utils │ │ │ │ └── utils.less │ │ ├── quartzJob │ │ │ ├── data.d.ts │ │ │ ├── index.tsx │ │ │ ├── models │ │ │ │ └── quartzJob.ts │ │ │ ├── service.ts │ │ │ ├── style.less │ │ │ └── utils │ │ │ │ └── utils.less │ │ └── quartzTriggerList │ │ │ ├── data.d.ts │ │ │ ├── index.tsx │ │ │ ├── models │ │ │ └── quartzTriggerList.ts │ │ │ ├── service.ts │ │ │ ├── style.less │ │ │ └── utils │ │ │ └── utils.less │ ├── runtime │ │ └── jobExecutionHistory │ │ │ ├── data.d.ts │ │ │ ├── index.tsx │ │ │ ├── models │ │ │ └── jobExecutionHistory.ts │ │ │ ├── service.ts │ │ │ ├── style.less │ │ │ └── utils │ │ │ └── utils.less │ └── user │ │ └── login │ │ ├── components │ │ └── Login │ │ │ ├── LoginContext.tsx │ │ │ ├── LoginItem.tsx │ │ │ ├── LoginSubmit.tsx │ │ │ ├── LoginTab.tsx │ │ │ ├── index.less │ │ │ ├── index.tsx │ │ │ └── map.tsx │ │ ├── index.tsx │ │ ├── locales │ │ ├── en-US.ts │ │ ├── zh-CN.ts │ │ └── zh-TW.ts │ │ └── style.less ├── service-worker.js ├── services │ ├── login.ts │ └── user.ts ├── typings.d.ts └── utils │ ├── Authorized.ts │ ├── authority.test.ts │ ├── authority.ts │ ├── request.ts │ ├── utils.less │ ├── utils.test.ts │ └── utils.ts ├── tests └── run-tests.js └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [Makefile] 16 | indent_style = tab 17 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /lambda/ 2 | /scripts 3 | /config 4 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | const { strictEslint } = require('@umijs/fabric'); 2 | 3 | module.exports = { 4 | ...strictEslint, 5 | globals: { 6 | ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION: true, 7 | page: true, 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: '报告Bug 🐛' 3 | about: 报告 Ant Design Pro 的 bug 4 | title: '[BUG]' 5 | labels: bug 6 | assignees: '' 7 | --- 8 | 9 | **bug 描述** [详细地描述 bug,让大家都能理解] 10 | 11 | **复现步骤** [清晰描述复现步骤,让别人也能看到问题] 12 | 13 | **期望结果** [描述你原本期望看到的结果] 14 | 15 | **复现代码** [提供可复现的代码,仓库,或线上示例] 16 | 17 | **版本信息:** 18 | 19 | - Ant Design Pro 版本: [e.g. 4.0.0] 20 | - umi 版本 21 | - 浏览器环境 22 | - 开发环境 [e.g. mac OS] 23 | 24 | **其他信息** [如截图等其他信息可以贴在这里] 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: '功能需求 ✨' 3 | about: 对 Ant Design Pro 的需求或建议 4 | title: '[需求]' 5 | labels: feature 6 | assignees: '' 7 | --- 8 | 9 | **需求描述** [详细地描述需求,让大家都能理解] 10 | 11 | **解决方案** [如果你有解决方案,在这里清晰地阐述] 12 | 13 | **其他信息** [如截图等其他信息可以贴在这里] 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: '疑问或需要帮助 ❓' 3 | about: 对 Ant Design Pro 使用的疑问或需要帮助 4 | title: '[问题]' 5 | labels: question 6 | assignees: '' 7 | --- 8 | 9 | **问题描述** [详细地描述问题,让大家都能理解] 10 | 11 | **示例代码** [如果有必要,展示代码,线上示例,或仓库] 12 | 13 | **其他信息** [如截图等其他信息可以贴在这里] 14 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | **/*.svg 2 | package.json 3 | .umi 4 | .umi-production 5 | /dist 6 | .dockerignore 7 | .DS_Store 8 | .eslintignore 9 | *.png 10 | *.toml 11 | docker 12 | .editorconfig 13 | Dockerfile* 14 | .gitignore 15 | .prettierignore 16 | LICENSE 17 | .eslintcache 18 | *.lock 19 | yarn-error.log -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | const fabric = require('@umijs/fabric'); 2 | 3 | module.exports = { 4 | ...fabric.prettier, 5 | }; 6 | -------------------------------------------------------------------------------- /.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) 2019 linghuxiong 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 | ## 如何启动项目: 2 | ### 如何启动后端: 3 | 1. 下载代码:https://github.com/linghuxiong/spring-batch-admin-backend 4 | 2. 导入工程:推荐是用IDEA,直接打开下载的目录即可。 5 | 3. 初始化数据库: 找到目录:/spring-batch-admin-backend/src/main/db,里面有两个文件,一个是数据库创建脚本,一个是表结构+数据的脚本,先执行创建库的,如果想在已经存在的库里面运行程序,可以省略这一步,直接运行数据脚本即可。 6 | 4. 启动项目:如果使用的是上一步中默认的数据库建库语句,可以直接启动,如果是使用的自己的数据库,修改/spring-batch-admin-backend/src/main/resources/application-localhost.properties里面是数据配置信息,然后启动SpringBatchAdminApplication.java即可 7 | 8 | ![](https://raw.githubusercontent.com/linghuxiong/picback/master/img15747839886243.jpg) 9 | 10 | ### 如何启动前端 11 | 1. 下载代码:https://github.com/linghuxiong/spring-batch-admin-ui 12 | 2. npm install:在根目录运行`npm install` 13 | 3. npm start: install 之后,运行 `npm start` 14 | 15 | ![](https://raw.githubusercontent.com/linghuxiong/picback/master/img15747840782809.jpg) 16 | 17 | ### 查看效果 18 | 浏览器打开 http://localhost:8000 19 | 进入登录页面,输入用户名密码: 20 | * 用户名:admin 21 | * 密码:123456 22 | 23 | 登录成功之后即可进入系统查看 24 | 25 | ![](https://raw.githubusercontent.com/linghuxiong/picback/master/img15747838805787.jpg) 26 | 27 | ![](https://raw.githubusercontent.com/linghuxiong/picback/master/img15747851141862.jpg) 28 | 29 | ![](https://raw.githubusercontent.com/linghuxiong/picback/master/img15747839342373.jpg) 30 | 31 | -------------------------------------------------------------------------------- /config/defaultSettings.ts: -------------------------------------------------------------------------------- 1 | import { MenuTheme } from 'antd/es/menu/MenuContext'; 2 | 3 | export type ContentWidth = 'Fluid' | 'Fixed'; 4 | 5 | export interface DefaultSettings { 6 | /** 7 | * theme for nav menu 8 | */ 9 | navTheme: MenuTheme; 10 | /** 11 | * primary color of ant design 12 | */ 13 | primaryColor: string; 14 | /** 15 | * nav menu position: `sidemenu` or `topmenu` 16 | */ 17 | layout: 'sidemenu' | 'topmenu'; 18 | /** 19 | * layout of content: `Fluid` or `Fixed`, only works when layout is topmenu 20 | */ 21 | contentWidth: ContentWidth; 22 | /** 23 | * sticky header 24 | */ 25 | fixedHeader: boolean; 26 | /** 27 | * auto hide header 28 | */ 29 | autoHideHeader: boolean; 30 | /** 31 | * sticky siderbar 32 | */ 33 | fixSiderbar: boolean; 34 | menu: { locale: boolean }; 35 | title: string; 36 | pwa: boolean; 37 | // Your custom iconfont Symbol script Url 38 | // eg://at.alicdn.com/t/font_1039637_btcrd5co4w.js 39 | // 注意:如果需要图标多色,Iconfont 图标项目里要进行批量去色处理 40 | // Usage: https://github.com/ant-design/ant-design-pro/pull/3517 41 | iconfontUrl: string; 42 | colorWeak: boolean; 43 | } 44 | 45 | export default { 46 | navTheme: 'dark', 47 | primaryColor: '#1890FF', 48 | layout: 'sidemenu', 49 | contentWidth: 'Fluid', 50 | fixedHeader: false, 51 | autoHideHeader: false, 52 | fixSiderbar: true, 53 | colorWeak: false, 54 | menu: { 55 | locale: true, 56 | }, 57 | title: '批处理管理平台', 58 | pwa: false, 59 | iconfontUrl: '', 60 | } as DefaultSettings; 61 | -------------------------------------------------------------------------------- /jest-puppeteer.config.js: -------------------------------------------------------------------------------- 1 | // ps https://github.com/GoogleChrome/puppeteer/issues/3120 2 | module.exports = { 3 | launch: { 4 | args: [ 5 | '--disable-gpu', 6 | '--disable-dev-shm-usage', 7 | '--no-first-run', 8 | '--no-zygote', 9 | '--no-sandbox', 10 | ], 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testURL: 'http://localhost:8000', 3 | preset: 'jest-puppeteer', 4 | globals: { 5 | ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION: false, 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "emitDecoratorMetadata": true, 4 | "experimentalDecorators": true, 5 | "baseUrl": ".", 6 | "paths": { 7 | "@/*": ["./src/*"] 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /mock/notices.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | 3 | const getNotices = (req: Request, res: Response) => { 4 | res.json([ 5 | { 6 | id: '000000001', 7 | avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png', 8 | title: '你收到了 14 份新周报', 9 | datetime: '2017-08-09', 10 | type: 'notification', 11 | }, 12 | { 13 | id: '000000002', 14 | avatar: 'https://gw.alipayobjects.com/zos/rmsportal/OKJXDXrmkNshAMvwtvhu.png', 15 | title: '你推荐的 曲妮妮 已通过第三轮面试', 16 | datetime: '2017-08-08', 17 | type: 'notification', 18 | }, 19 | { 20 | id: '000000003', 21 | avatar: 'https://gw.alipayobjects.com/zos/rmsportal/kISTdvpyTAhtGxpovNWd.png', 22 | title: '这种模板可以区分多种通知类型', 23 | datetime: '2017-08-07', 24 | read: true, 25 | type: 'notification', 26 | }, 27 | { 28 | id: '000000004', 29 | avatar: 'https://gw.alipayobjects.com/zos/rmsportal/GvqBnKhFgObvnSGkDsje.png', 30 | title: '左侧图标用于区分不同的类型', 31 | datetime: '2017-08-07', 32 | type: 'notification', 33 | }, 34 | { 35 | id: '000000005', 36 | avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png', 37 | title: '内容不要超过两行字,超出时自动截断', 38 | datetime: '2017-08-07', 39 | type: 'notification', 40 | }, 41 | { 42 | id: '000000006', 43 | avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg', 44 | title: '曲丽丽 评论了你', 45 | description: '描述信息描述信息描述信息', 46 | datetime: '2017-08-07', 47 | type: 'message', 48 | clickClose: true, 49 | }, 50 | { 51 | id: '000000007', 52 | avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg', 53 | title: '朱偏右 回复了你', 54 | description: '这种模板用于提醒谁与你发生了互动,左侧放『谁』的头像', 55 | datetime: '2017-08-07', 56 | type: 'message', 57 | clickClose: true, 58 | }, 59 | { 60 | id: '000000008', 61 | avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg', 62 | title: '标题', 63 | description: '这种模板用于提醒谁与你发生了互动,左侧放『谁』的头像', 64 | datetime: '2017-08-07', 65 | type: 'message', 66 | clickClose: true, 67 | }, 68 | { 69 | id: '000000009', 70 | title: '任务名称', 71 | description: '任务需要在 2017-01-12 20:00 前启动', 72 | extra: '未开始', 73 | status: 'todo', 74 | type: 'event', 75 | }, 76 | { 77 | id: '000000010', 78 | title: '第三方紧急代码变更', 79 | description: '冠霖提交于 2017-01-06,需在 2017-01-07 前完成代码变更任务', 80 | extra: '马上到期', 81 | status: 'urgent', 82 | type: 'event', 83 | }, 84 | { 85 | id: '000000011', 86 | title: '信息安全考试', 87 | description: '指派竹尔于 2017-01-09 前完成更新并发布', 88 | extra: '已耗时 8 天', 89 | status: 'doing', 90 | type: 'event', 91 | }, 92 | { 93 | id: '000000012', 94 | title: 'ABCD 版本发布', 95 | description: '冠霖提交于 2017-01-06,需在 2017-01-07 前完成代码变更任务', 96 | extra: '进行中', 97 | status: 'processing', 98 | type: 'event', 99 | }, 100 | ]); 101 | }; 102 | 103 | export default { 104 | 'GET /api/notices': getNotices, 105 | }; 106 | -------------------------------------------------------------------------------- /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 | function getFakeCaptcha(req: Request, res: Response) { 4 | return res.json('captcha-xxx'); 5 | } 6 | // 代码中会兼容本地 service mock 以及部署站点的静态数据 7 | export default { 8 | 'POST /api/login/account': (req: Request, res: Response) => { 9 | const { password, userName, type } = req.body; 10 | if (password === 'ant.design' && userName === 'admin') { 11 | res.send({ 12 | status: 'ok', 13 | type, 14 | currentAuthority: 'admin', 15 | }); 16 | return; 17 | } 18 | if (password === 'ant.design' && userName === 'user') { 19 | res.send({ 20 | status: 'ok', 21 | type, 22 | currentAuthority: 'user', 23 | }); 24 | return; 25 | } 26 | res.send({ 27 | status: 'error', 28 | type, 29 | currentAuthority: 'guest', 30 | }); 31 | }, 32 | 'POST /api/register': (req: Request, res: Response) => { 33 | res.send({ status: 'ok', currentAuthority: 'user' }); 34 | }, 35 | 'GET /api/500': (req: Request, res: Response) => { 36 | res.status(500).send({ 37 | timestamp: 1513932555104, 38 | status: 500, 39 | error: 'error', 40 | message: 'error', 41 | path: '/base/category/list', 42 | }); 43 | }, 44 | 'GET /api/404': (req: Request, res: Response) => { 45 | res.status(404).send({ 46 | timestamp: 1513932643431, 47 | status: 404, 48 | error: 'Not Found', 49 | message: 'No message available', 50 | path: '/base/category/list/2121212', 51 | }); 52 | }, 53 | 'GET /api/403': (req: Request, res: Response) => { 54 | res.status(403).send({ 55 | timestamp: 1513932555104, 56 | status: 403, 57 | error: 'Unauthorized', 58 | message: 'Unauthorized', 59 | path: '/base/category/list', 60 | }); 61 | }, 62 | 'GET /api/401': (req: Request, res: Response) => { 63 | res.status(401).send({ 64 | timestamp: 1513932555104, 65 | status: 401, 66 | error: 'Unauthorized', 67 | message: 'Unauthorized', 68 | path: '/base/category/list', 69 | }); 70 | }, 71 | 72 | 'GET /api/login/captcha': getFakeCaptcha, 73 | }; 74 | -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linghuxiong/spring-batch-admin-ui/bb0451aa41a43fa76ea3deca44af027ac5d8e90e/public/favicon.png -------------------------------------------------------------------------------- /public/icons/icon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linghuxiong/spring-batch-admin-ui/bb0451aa41a43fa76ea3deca44af027ac5d8e90e/public/icons/icon-128x128.png -------------------------------------------------------------------------------- /public/icons/icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linghuxiong/spring-batch-admin-ui/bb0451aa41a43fa76ea3deca44af027ac5d8e90e/public/icons/icon-192x192.png -------------------------------------------------------------------------------- /public/icons/icon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linghuxiong/spring-batch-admin-ui/bb0451aa41a43fa76ea3deca44af027ac5d8e90e/public/icons/icon-512x512.png -------------------------------------------------------------------------------- /src/components/Authorized/Authorized.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import check, { IAuthorityType } from './CheckPermissions'; 3 | 4 | import AuthorizedRoute from './AuthorizedRoute'; 5 | import Secured from './Secured'; 6 | 7 | interface AuthorizedProps { 8 | authority: IAuthorityType; 9 | noMatch?: React.ReactNode; 10 | } 11 | 12 | type IAuthorizedType = React.FunctionComponent & { 13 | Secured: typeof Secured; 14 | check: typeof check; 15 | AuthorizedRoute: typeof AuthorizedRoute; 16 | }; 17 | 18 | const Authorized: React.FunctionComponent = ({ 19 | children, 20 | authority, 21 | noMatch = null, 22 | }) => { 23 | const childrenRender: React.ReactNode = typeof children === 'undefined' ? null : children; 24 | const dom = check(authority, childrenRender, noMatch); 25 | return <>{dom}; 26 | }; 27 | 28 | export default Authorized as IAuthorizedType; 29 | -------------------------------------------------------------------------------- /src/components/Authorized/AuthorizedRoute.tsx: -------------------------------------------------------------------------------- 1 | import { Redirect, Route } from 'umi'; 2 | 3 | import React from 'react'; 4 | import Authorized from './Authorized'; 5 | import { IAuthorityType } from './CheckPermissions'; 6 | 7 | interface AuthorizedRoutePops { 8 | currentAuthority: string; 9 | component: React.ComponentClass; 10 | render: (props: any) => React.ReactNode; 11 | redirectPath: string; 12 | authority: IAuthorityType; 13 | } 14 | 15 | const AuthorizedRoute: React.SFC = ({ 16 | component: Component, 17 | render, 18 | authority, 19 | redirectPath, 20 | ...rest 21 | }) => ( 22 | } />} 25 | > 26 | (Component ? : render(props))} 29 | /> 30 | 31 | ); 32 | 33 | export default AuthorizedRoute; 34 | -------------------------------------------------------------------------------- /src/components/Authorized/CheckPermissions.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { CURRENT } from './renderAuthorize'; 3 | // eslint-disable-next-line import/no-cycle 4 | import PromiseRender from './PromiseRender'; 5 | 6 | export type IAuthorityType = 7 | | undefined 8 | | string 9 | | string[] 10 | | Promise 11 | | ((currentAuthority: string | string[]) => IAuthorityType); 12 | 13 | /** 14 | * 通用权限检查方法 15 | * Common check permissions method 16 | * @param { 权限判定 | Permission judgment } authority 17 | * @param { 你的权限 | Your permission description } currentAuthority 18 | * @param { 通过的组件 | Passing components } target 19 | * @param { 未通过的组件 | no pass components } Exception 20 | */ 21 | const checkPermissions = ( 22 | authority: IAuthorityType, 23 | currentAuthority: string | string[], 24 | target: T, 25 | Exception: K, 26 | ): T | K | React.ReactNode => { 27 | // 没有判定权限.默认查看所有 28 | // Retirement authority, return target; 29 | if (!authority) { 30 | return target; 31 | } 32 | // 数组处理 33 | if (Array.isArray(authority)) { 34 | if (Array.isArray(currentAuthority)) { 35 | if (currentAuthority.some(item => authority.includes(item))) { 36 | return target; 37 | } 38 | } else if (authority.includes(currentAuthority)) { 39 | return target; 40 | } 41 | return Exception; 42 | } 43 | // string 处理 44 | if (typeof authority === 'string') { 45 | if (Array.isArray(currentAuthority)) { 46 | if (currentAuthority.some(item => authority === item)) { 47 | return target; 48 | } 49 | } else if (authority === currentAuthority) { 50 | return target; 51 | } 52 | return Exception; 53 | } 54 | // Promise 处理 55 | if (authority instanceof Promise) { 56 | return ok={target} error={Exception} promise={authority} />; 57 | } 58 | // Function 处理 59 | if (typeof authority === 'function') { 60 | try { 61 | const bool = authority(currentAuthority); 62 | // 函数执行后返回值是 Promise 63 | if (bool instanceof Promise) { 64 | return ok={target} error={Exception} promise={bool} />; 65 | } 66 | if (bool) { 67 | return target; 68 | } 69 | return Exception; 70 | } catch (error) { 71 | throw error; 72 | } 73 | } 74 | throw new Error('unsupported parameters'); 75 | }; 76 | 77 | export { checkPermissions }; 78 | 79 | function check(authority: IAuthorityType, target: T, Exception: K): T | K | React.ReactNode { 80 | return checkPermissions(authority, CURRENT, target, Exception); 81 | } 82 | 83 | export default check; 84 | -------------------------------------------------------------------------------- /src/components/Authorized/PromiseRender.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Spin } from 'antd'; 3 | import isEqual from 'lodash/isEqual'; 4 | import { isComponentClass } from './Secured'; 5 | // eslint-disable-next-line import/no-cycle 6 | 7 | interface PromiseRenderProps { 8 | ok: T; 9 | error: K; 10 | promise: Promise; 11 | } 12 | 13 | interface PromiseRenderState { 14 | component: React.ComponentClass | React.FunctionComponent; 15 | } 16 | 17 | export default class PromiseRender extends React.Component< 18 | PromiseRenderProps, 19 | PromiseRenderState 20 | > { 21 | state: PromiseRenderState = { 22 | component: () => null, 23 | }; 24 | 25 | componentDidMount() { 26 | this.setRenderComponent(this.props); 27 | } 28 | 29 | shouldComponentUpdate = (nextProps: PromiseRenderProps, nextState: PromiseRenderState) => { 30 | const { component } = this.state; 31 | if (!isEqual(nextProps, this.props)) { 32 | this.setRenderComponent(nextProps); 33 | } 34 | if (nextState.component !== component) return true; 35 | return false; 36 | }; 37 | 38 | // set render Component : ok or error 39 | setRenderComponent(props: PromiseRenderProps) { 40 | const ok = this.checkIsInstantiation(props.ok); 41 | const error = this.checkIsInstantiation(props.error); 42 | props.promise 43 | .then(() => { 44 | this.setState({ 45 | component: ok, 46 | }); 47 | return true; 48 | }) 49 | .catch(() => { 50 | this.setState({ 51 | component: error, 52 | }); 53 | }); 54 | } 55 | 56 | // Determine whether the incoming component has been instantiated 57 | // AuthorizedRoute is already instantiated 58 | // Authorized render is already instantiated, children is no instantiated 59 | // Secured is not instantiated 60 | checkIsInstantiation = ( 61 | target: React.ReactNode | React.ComponentClass, 62 | ): React.FunctionComponent => { 63 | if (isComponentClass(target)) { 64 | const Target = target as React.ComponentClass; 65 | return (props: any) => ; 66 | } 67 | if (React.isValidElement(target)) { 68 | return (props: any) => React.cloneElement(target, props); 69 | } 70 | return () => target as (React.ReactNode & null); 71 | }; 72 | 73 | render() { 74 | const { component: Component } = this.state; 75 | const { ok, error, promise, ...rest } = this.props; 76 | 77 | return Component ? ( 78 | 79 | ) : ( 80 |
89 | 90 |
91 | ); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/components/Authorized/Secured.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import CheckPermissions from './CheckPermissions'; 3 | 4 | /** 5 | * 默认不能访问任何页面 6 | * default is "NULL" 7 | */ 8 | const Exception403 = () => 403; 9 | 10 | export const isComponentClass = (component: React.ComponentClass | React.ReactNode): boolean => { 11 | if (!component) return false; 12 | const proto = Object.getPrototypeOf(component); 13 | if (proto === React.Component || proto === Function.prototype) return true; 14 | return isComponentClass(proto); 15 | }; 16 | 17 | // Determine whether the incoming component has been instantiated 18 | // AuthorizedRoute is already instantiated 19 | // Authorized render is already instantiated, children is no instantiated 20 | // Secured is not instantiated 21 | const checkIsInstantiation = (target: React.ComponentClass | React.ReactNode) => { 22 | if (isComponentClass(target)) { 23 | const Target = target as React.ComponentClass; 24 | return (props: any) => ; 25 | } 26 | if (React.isValidElement(target)) { 27 | return (props: any) => React.cloneElement(target, props); 28 | } 29 | return () => target; 30 | }; 31 | 32 | /** 33 | * 用于判断是否拥有权限访问此 view 权限 34 | * authority 支持传入 string, () => boolean | Promise 35 | * e.g. 'user' 只有 user 用户能访问 36 | * e.g. 'user,admin' user 和 admin 都能访问 37 | * e.g. ()=>boolean 返回true能访问,返回false不能访问 38 | * e.g. Promise then 能访问 catch不能访问 39 | * e.g. authority support incoming string, () => boolean | Promise 40 | * e.g. 'user' only user user can access 41 | * e.g. 'user, admin' user and admin can access 42 | * e.g. () => boolean true to be able to visit, return false can not be accessed 43 | * e.g. Promise then can not access the visit to catch 44 | * @param {string | function | Promise} authority 45 | * @param {ReactNode} error 非必需参数 46 | */ 47 | const authorize = (authority: string, error?: React.ReactNode) => { 48 | /** 49 | * conversion into a class 50 | * 防止传入字符串时找不到staticContext造成报错 51 | * String parameters can cause staticContext not found error 52 | */ 53 | let classError: boolean | React.FunctionComponent = false; 54 | if (error) { 55 | classError = (() => error) as React.FunctionComponent; 56 | } 57 | if (!authority) { 58 | throw new Error('authority is required'); 59 | } 60 | return function decideAuthority(target: React.ComponentClass | React.ReactNode) { 61 | const component = CheckPermissions(authority, target, classError || Exception403); 62 | return checkIsInstantiation(component); 63 | }; 64 | }; 65 | 66 | export default authorize; 67 | -------------------------------------------------------------------------------- /src/components/Authorized/index.tsx: -------------------------------------------------------------------------------- 1 | import Authorized from './Authorized'; 2 | import AuthorizedRoute from './AuthorizedRoute'; 3 | import Secured from './Secured'; 4 | import check from './CheckPermissions'; 5 | import renderAuthorize from './renderAuthorize'; 6 | 7 | Authorized.Secured = Secured; 8 | Authorized.AuthorizedRoute = AuthorizedRoute; 9 | Authorized.check = check; 10 | 11 | const RenderAuthorize = renderAuthorize(Authorized); 12 | 13 | export default RenderAuthorize; 14 | -------------------------------------------------------------------------------- /src/components/Authorized/renderAuthorize.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable eslint-comments/disable-enable-pair */ 2 | /* eslint-disable import/no-mutable-exports */ 3 | let CURRENT: string | string[] = 'NULL'; 4 | 5 | type CurrentAuthorityType = string | string[] | (() => typeof CURRENT); 6 | /** 7 | * use authority or getAuthority 8 | * @param {string|()=>String} currentAuthority 9 | */ 10 | const renderAuthorize = (Authorized: T): ((currentAuthority: CurrentAuthorityType) => T) => ( 11 | currentAuthority: CurrentAuthorityType, 12 | ): T => { 13 | if (currentAuthority) { 14 | if (typeof currentAuthority === 'function') { 15 | CURRENT = currentAuthority(); 16 | } 17 | if ( 18 | Object.prototype.toString.call(currentAuthority) === '[object String]' || 19 | Array.isArray(currentAuthority) 20 | ) { 21 | CURRENT = currentAuthority as string[]; 22 | } 23 | } else { 24 | CURRENT = 'NULL'; 25 | } 26 | return Authorized; 27 | }; 28 | 29 | export { CURRENT }; 30 | export default (Authorized: T) => renderAuthorize(Authorized); 31 | -------------------------------------------------------------------------------- /src/components/CopyBlock/index.less: -------------------------------------------------------------------------------- 1 | .copy-block { 2 | position: fixed; 3 | right: 80px; 4 | bottom: 40px; 5 | z-index: 99; 6 | display: flex; 7 | flex-direction: column; 8 | align-items: center; 9 | justify-content: center; 10 | width: 40px; 11 | height: 40px; 12 | font-size: 20px; 13 | background: #fff; 14 | border-radius: 40px; 15 | box-shadow: 0 2px 4px -1px rgba(0, 0, 0, 0.2), 0 4px 5px 0 rgba(0, 0, 0, 0.14), 16 | 0 1px 10px 0 rgba(0, 0, 0, 0.12); 17 | cursor: pointer; 18 | } 19 | 20 | .copy-block-view { 21 | position: relative; 22 | .copy-block-code { 23 | display: inline-block; 24 | margin: 0 0.2em; 25 | padding: 0.2em 0.4em 0.1em; 26 | font-size: 85%; 27 | border-radius: 3px; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/components/CopyBlock/index.tsx: -------------------------------------------------------------------------------- 1 | import { Icon, Popover, Typography } from 'antd'; 2 | import React, { useRef } from 'react'; 3 | 4 | import { FormattedMessage } from 'umi-plugin-react/locale'; 5 | import { connect } from 'dva'; 6 | import { isAntDesignPro } from '@/utils/utils'; 7 | import styles from './index.less'; 8 | 9 | const firstUpperCase = (pathString: string): string => 10 | pathString 11 | .replace('.', '') 12 | .split(/\/|-/) 13 | .map((s): string => s.toLowerCase().replace(/( |^)[a-z]/g, L => L.toUpperCase())) 14 | .filter((s): boolean => !!s) 15 | .join(''); 16 | 17 | // when click block copy, send block url to ga 18 | const onBlockCopy = (label: string) => { 19 | if (!isAntDesignPro()) { 20 | return; 21 | } 22 | 23 | const ga = window && window.ga; 24 | if (ga) { 25 | ga('send', 'event', { 26 | eventCategory: 'block', 27 | eventAction: 'copy', 28 | eventLabel: label, 29 | }); 30 | } 31 | }; 32 | 33 | const BlockCodeView: React.SFC<{ 34 | url: string; 35 | }> = ({ url }) => { 36 | const blockUrl = `npx umi block add ${firstUpperCase(url)} --path=${url}`; 37 | return ( 38 |
39 | onBlockCopy(url), 43 | }} 44 | style={{ 45 | display: 'flex', 46 | }} 47 | > 48 |
49 |           {blockUrl}
50 |         
51 |
52 |
53 | ); 54 | }; 55 | 56 | interface RoutingType { 57 | location: { 58 | pathname: string; 59 | }; 60 | } 61 | 62 | export default connect(({ routing }: { routing: RoutingType }) => ({ 63 | location: routing.location, 64 | }))(({ location }: RoutingType) => { 65 | const url = location.pathname; 66 | const divDom = useRef(null); 67 | return ( 68 | } 70 | placement="topLeft" 71 | content={} 72 | trigger="click" 73 | getPopupContainer={dom => (divDom.current ? divDom.current : dom)} 74 | > 75 |
76 | 77 |
78 |
79 | ); 80 | }); 81 | -------------------------------------------------------------------------------- /src/components/GlobalHeader/AvatarDropdown.tsx: -------------------------------------------------------------------------------- 1 | import { Avatar, Icon, Menu, Spin } from 'antd'; 2 | import { ClickParam } from 'antd/es/menu'; 3 | import { FormattedMessage } from 'umi-plugin-react/locale'; 4 | import React from 'react'; 5 | import { connect } from 'dva'; 6 | import router from 'umi/router'; 7 | 8 | import { ConnectProps, ConnectState } from '@/models/connect'; 9 | import { CurrentUser } from '@/models/user'; 10 | import HeaderDropdown from '../HeaderDropdown'; 11 | import styles from './index.less'; 12 | 13 | export interface GlobalHeaderRightProps extends ConnectProps { 14 | currentUser?: CurrentUser; 15 | menu?: boolean; 16 | } 17 | 18 | class AvatarDropdown extends React.Component { 19 | onMenuClick = (event: ClickParam) => { 20 | const { key } = event; 21 | 22 | if (key === 'logout') { 23 | const { dispatch } = this.props; 24 | if (dispatch) { 25 | dispatch({ 26 | type: 'login/logout', 27 | }); 28 | } 29 | 30 | return; 31 | } 32 | console.log(key); 33 | router.push(`/account/${key}`); 34 | }; 35 | 36 | render(): React.ReactNode { 37 | const { currentUser = { avatar: '', name: '' }, menu } = this.props; 38 | 39 | const menuHeaderDropdown = ( 40 | 41 | {menu && ( 42 | 43 | 44 | 45 | 46 | )} 47 | {menu && ( 48 | 49 | 50 | 51 | 52 | )} 53 | {menu && } 54 | 55 | 56 | 57 | 58 | 59 | 60 | ); 61 | 62 | return currentUser && currentUser.name ? ( 63 | 64 | 65 | 66 | {currentUser.name} 67 | 68 | 69 | ) : ( 70 | 71 | ); 72 | } 73 | } 74 | export default connect(({ user }: ConnectState) => ({ 75 | currentUser: user.currentUser, 76 | }))(AvatarDropdown); 77 | -------------------------------------------------------------------------------- /src/components/GlobalHeader/RightContent.tsx: -------------------------------------------------------------------------------- 1 | import { Icon, Tooltip } from 'antd'; 2 | import React from 'react'; 3 | import { connect } from 'dva'; 4 | import { formatMessage } from 'umi-plugin-react/locale'; 5 | import { ConnectProps, ConnectState } from '@/models/connect'; 6 | 7 | import Avatar from './AvatarDropdown'; 8 | import Notice from './NoticeIconView'; 9 | import HeaderSearch from '../HeaderSearch'; 10 | import SelectLang from '../SelectLang'; 11 | import styles from './index.less'; 12 | 13 | export type SiderTheme = 'light' | 'dark'; 14 | export interface GlobalHeaderRightProps extends ConnectProps { 15 | theme?: SiderTheme; 16 | layout: 'sidemenu' | 'topmenu'; 17 | } 18 | 19 | const GlobalHeaderRight: React.SFC = props => { 20 | const { theme, layout } = props; 21 | let className = styles.right; 22 | 23 | if (theme === 'dark' && layout === 'topmenu') { 24 | className = `${styles.right} ${styles.dark}`; 25 | } 26 | 27 | return ( 28 |
29 | { 46 | console.log('input', value); 47 | }} 48 | onPressEnter={value => { 49 | console.log('enter', value); 50 | }} 51 | /> 52 | 57 | 63 | 64 | 65 | 66 | 67 | 68 | 69 |
70 | ); 71 | }; 72 | 73 | export default connect(({ settings }: ConnectState) => ({ 74 | theme: settings.navTheme, 75 | layout: settings.layout, 76 | }))(GlobalHeaderRight); 77 | -------------------------------------------------------------------------------- /src/components/GlobalHeader/index.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | @pro-header-hover-bg: rgba(0, 0, 0, 0.025); 4 | 5 | .logo { 6 | display: inline-block; 7 | height: @layout-header-height; 8 | padding: 0 0 0 24px; 9 | font-size: 20px; 10 | line-height: @layout-header-height; 11 | vertical-align: top; 12 | cursor: pointer; 13 | img { 14 | display: inline-block; 15 | vertical-align: middle; 16 | } 17 | } 18 | 19 | .menu { 20 | :global(.anticon) { 21 | margin-right: 8px; 22 | } 23 | :global(.ant-dropdown-menu-item) { 24 | min-width: 160px; 25 | } 26 | } 27 | 28 | .trigger { 29 | height: @layout-header-height; 30 | padding: ~'calc((@{layout-header-height} - 20px) / 2)' 24px; 31 | font-size: 20px; 32 | cursor: pointer; 33 | transition: all 0.3s, padding 0s; 34 | &:hover { 35 | background: @pro-header-hover-bg; 36 | } 37 | } 38 | 39 | .right { 40 | float: right; 41 | height: 100%; 42 | overflow: hidden; 43 | .action { 44 | display: inline-block; 45 | height: 100%; 46 | padding: 0 12px; 47 | cursor: pointer; 48 | transition: all 0.3s; 49 | > i { 50 | color: @text-color; 51 | vertical-align: middle; 52 | } 53 | &:hover { 54 | background: @pro-header-hover-bg; 55 | } 56 | &:global(.opened) { 57 | background: @pro-header-hover-bg; 58 | } 59 | } 60 | .search { 61 | padding: 0 12px; 62 | &:hover { 63 | background: transparent; 64 | } 65 | } 66 | .account { 67 | .avatar { 68 | margin: ~'calc((@{layout-header-height} - 24px) / 2)' 0; 69 | margin-right: 8px; 70 | color: @primary-color; 71 | vertical-align: top; 72 | background: rgba(255, 255, 255, 0.85); 73 | } 74 | } 75 | } 76 | 77 | .dark { 78 | height: @layout-header-height; 79 | .action { 80 | color: rgba(255, 255, 255, 0.85); 81 | > i { 82 | color: rgba(255, 255, 255, 0.85); 83 | } 84 | &:hover, 85 | &:global(.opened) { 86 | background: @primary-color; 87 | } 88 | } 89 | } 90 | 91 | :global(.ant-pro-global-header) { 92 | .dark { 93 | .action { 94 | color: @text-color; 95 | > i { 96 | color: @text-color; 97 | } 98 | &:hover { 99 | color: rgba(255, 255, 255, 0.85); 100 | > i { 101 | color: rgba(255, 255, 255, 0.85); 102 | } 103 | } 104 | } 105 | } 106 | } 107 | 108 | @media only screen and (max-width: @screen-md) { 109 | :global(.ant-divider-vertical) { 110 | vertical-align: unset; 111 | } 112 | .name { 113 | display: none; 114 | } 115 | i.trigger { 116 | padding: 22px 12px; 117 | } 118 | .logo { 119 | position: relative; 120 | padding-right: 12px; 121 | padding-left: 12px; 122 | } 123 | .right { 124 | position: absolute; 125 | top: 0; 126 | right: 12px; 127 | .account { 128 | .avatar { 129 | margin-right: 0; 130 | } 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/components/HeaderDropdown/index.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | .container > * { 4 | background-color: #fff; 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 { 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 | declare type OverlayFunc = () => React.ReactNode; 8 | 9 | export interface HeaderDropdownProps extends DropDownProps { 10 | overlayClassName?: string; 11 | overlay: React.ReactNode | OverlayFunc; 12 | placement?: 'bottomLeft' | 'bottomRight' | 'topLeft' | 'topCenter' | 'topRight' | 'bottomCenter'; 13 | } 14 | 15 | const HeaderDropdown: React.FC = ({ overlayClassName: cls, ...restProps }) => ( 16 | 17 | ); 18 | 19 | export default HeaderDropdown; 20 | -------------------------------------------------------------------------------- /src/components/HeaderSearch/index.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | .headerSearch { 4 | :global(.anticon-search) { 5 | font-size: 16px; 6 | cursor: pointer; 7 | } 8 | .input { 9 | width: 0; 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 | padding-right: 0; 18 | padding-left: 0; 19 | border: 0; 20 | box-shadow: none !important; 21 | } 22 | &, 23 | &:hover, 24 | &:focus { 25 | border-bottom: 1px solid @border-color-base; 26 | } 27 | &.show { 28 | width: 210px; 29 | margin-left: 8px; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/components/NoticeIcon/NoticeList.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 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: #fff; 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 | &:hover { 96 | color: @heading-color; 97 | } 98 | &:only-child { 99 | width: 100%; 100 | } 101 | &:not(:only-child):last-child { 102 | border-left: 1px solid @border-color-split; 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/components/NoticeIcon/index.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 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-scroll { 25 | text-align: center; 26 | } 27 | .ant-tabs-bar { 28 | margin-bottom: 0; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/components/PageLoading/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Spin } from 'antd'; 3 | 4 | // loading components from code split 5 | // https://umijs.org/plugin/umi-plugin-react.html#dynamicimport 6 | const PageLoading: React.FC = () => ( 7 |
8 | 9 |
10 | ); 11 | export default PageLoading; 12 | -------------------------------------------------------------------------------- /src/components/SelectLang/index.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | .menu { 4 | :global(.anticon) { 5 | margin-right: 8px; 6 | } 7 | :global(.ant-dropdown-menu-item) { 8 | min-width: 160px; 9 | } 10 | } 11 | 12 | .dropDown { 13 | line-height: @layout-header-height; 14 | vertical-align: top; 15 | cursor: pointer; 16 | > i { 17 | font-size: 16px !important; 18 | transform: none !important; 19 | svg { 20 | position: relative; 21 | top: -1px; 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/components/SelectLang/index.tsx: -------------------------------------------------------------------------------- 1 | import { Icon, Menu } from 'antd'; 2 | import { formatMessage, getLocale, setLocale } from 'umi-plugin-react/locale'; 3 | 4 | import { ClickParam } from 'antd/es/menu'; 5 | import React from 'react'; 6 | import classNames from 'classnames'; 7 | import HeaderDropdown from '../HeaderDropdown'; 8 | import styles from './index.less'; 9 | 10 | interface SelectLangProps { 11 | className?: string; 12 | } 13 | const SelectLang: React.FC = props => { 14 | const { className } = props; 15 | const selectedLang = getLocale(); 16 | const changeLang = ({ key }: ClickParam): void => setLocale(key, false); 17 | const locales = ['zh-CN', 'zh-TW', 'en-US', 'pt-BR']; 18 | const languageLabels = { 19 | 'zh-CN': '简体中文', 20 | 'zh-TW': '繁体中文', 21 | 'en-US': 'English', 22 | 'pt-BR': 'Português', 23 | }; 24 | const languageIcons = { 25 | 'zh-CN': '🇨🇳', 26 | 'zh-TW': '🇭🇰', 27 | 'en-US': '🇺🇸', 28 | 'pt-BR': '🇧🇷', 29 | }; 30 | const langMenu = ( 31 | 32 | {locales.map(locale => ( 33 | 34 | 35 | {languageIcons[locale]} 36 | {' '} 37 | {languageLabels[locale]} 38 | 39 | ))} 40 | 41 | ); 42 | return ( 43 | 44 | 45 | 46 | 47 | 48 | ); 49 | }; 50 | 51 | export default SelectLang; 52 | -------------------------------------------------------------------------------- /src/components/SettingDrawer/themeColorClient.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line eslint-comments/disable-enable-pair 2 | /* eslint-disable import/no-extraneous-dependencies */ 3 | import client from 'webpack-theme-color-replacer/client'; 4 | import generate from '@ant-design/colors/lib/generate'; 5 | 6 | export default { 7 | getAntdSerials(color: string): string[] { 8 | const lightCount = 9; 9 | const divide = 10; 10 | // 淡化(即less的tint) 11 | let lightens = new Array(lightCount).fill(0); 12 | lightens = lightens.map((_, i) => client.varyColor.lighten(color, i / divide)); 13 | const colorPalettes = generate(color); 14 | const rgb = client.varyColor.toNum3(color.replace('#', '')).join(','); 15 | return lightens.concat(colorPalettes).concat(rgb); 16 | }, 17 | changeColor(color?: string): Promise { 18 | if (!color) { 19 | return Promise.resolve(); 20 | } 21 | const options = { 22 | // new colors array, one-to-one corresponde with `matchColors` 23 | newColors: this.getAntdSerials(color), 24 | changeUrl(cssUrl: string): string { 25 | // while router is not `hash` mode, it needs absolute path 26 | return `/${cssUrl}`; 27 | }, 28 | }; 29 | return client.changer.changeColor(options, Promise); 30 | }, 31 | }; 32 | -------------------------------------------------------------------------------- /src/e2e/__mocks__/antd-pro-merge-less.js: -------------------------------------------------------------------------------- 1 | export default undefined; 2 | -------------------------------------------------------------------------------- /src/e2e/baseLayout.e2e.js: -------------------------------------------------------------------------------- 1 | const RouterConfig = require('../../config/config').default.routes; 2 | const { uniq } = require('lodash'); 3 | 4 | const BASE_URL = `http://localhost:${process.env.PORT || 8000}`; 5 | 6 | function formatter(routes, parentPath = '') { 7 | const fixedParentPath = parentPath.replace(/\/{1,}/g, '/'); 8 | let result = []; 9 | routes.forEach(item => { 10 | if (item.path) { 11 | result.push(`${fixedParentPath}/${item.path}`.replace(/\/{1,}/g, '/')); 12 | } 13 | if (item.routes) { 14 | result = result.concat( 15 | formatter(item.routes, item.path ? `${fixedParentPath}/${item.path}` : parentPath), 16 | ); 17 | } 18 | }); 19 | return uniq(result.filter(item => !!item)); 20 | } 21 | 22 | describe('Ant Design Pro E2E test', () => { 23 | const testPage = path => async () => { 24 | await page.goto(`${BASE_URL}${path}`); 25 | await page.waitForSelector('footer', { 26 | timeout: 2000, 27 | }); 28 | const haveFooter = await page.evaluate( 29 | () => document.getElementsByTagName('footer').length > 0, 30 | ); 31 | expect(haveFooter).toBeTruthy(); 32 | }; 33 | 34 | const routers = formatter(RouterConfig); 35 | console.log('routers', routers); 36 | routers.forEach(route => { 37 | it(`test pages ${route}`, testPage(route)); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /src/e2e/topMenu.e2e.js: -------------------------------------------------------------------------------- 1 | const BASE_URL = `http://localhost:${process.env.PORT || 8000}`; 2 | 3 | describe('Homepage', () => { 4 | it('topmenu should have footer', async () => { 5 | const params = '/form/basic-form?navTheme=light&layout=topmenu'; 6 | await page.goto(`${BASE_URL}${params}`); 7 | await page.waitForSelector('footer', { 8 | timeout: 2000, 9 | }); 10 | const haveFooter = await page.evaluate( 11 | () => document.getElementsByTagName('footer').length > 0, 12 | ); 13 | expect(haveFooter).toBeTruthy(); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /src/global.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.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 | 17 | canvas { 18 | display: block; 19 | } 20 | 21 | body { 22 | text-rendering: optimizeLegibility; 23 | -webkit-font-smoothing: antialiased; 24 | -moz-osx-font-smoothing: grayscale; 25 | } 26 | 27 | ul, 28 | ol { 29 | list-style: none; 30 | } 31 | 32 | @media (max-width: @screen-xs) { 33 | .ant-table { 34 | width: 100%; 35 | overflow-x: auto; 36 | &-thead > tr, 37 | &-tbody > tr { 38 | > th, 39 | > td { 40 | white-space: pre; 41 | > span { 42 | display: block; 43 | } 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/global.tsx: -------------------------------------------------------------------------------- 1 | import { Button, message, notification } from 'antd'; 2 | 3 | import React from 'react'; 4 | import { formatMessage } from 'umi-plugin-react/locale'; 5 | import defaultSettings from '../config/defaultSettings'; 6 | 7 | const { pwa } = defaultSettings; 8 | // if pwa is true 9 | if (pwa) { 10 | // Notify user if offline now 11 | window.addEventListener('sw.offline', () => { 12 | message.warning(formatMessage({ id: 'app.pwa.offline' })); 13 | }); 14 | 15 | // Pop up a prompt on the page asking the user if they want to use the latest version 16 | window.addEventListener('sw.updated', (event: Event) => { 17 | const e = event as CustomEvent; 18 | const reloadSW = async () => { 19 | // Check if there is sw whose state is waiting in ServiceWorkerRegistration 20 | // https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration 21 | const worker = e.detail && e.detail.waiting; 22 | if (!worker) { 23 | return true; 24 | } 25 | // Send skip-waiting event to waiting SW with MessageChannel 26 | await new Promise((resolve, reject) => { 27 | const channel = new MessageChannel(); 28 | channel.port1.onmessage = msgEvent => { 29 | if (msgEvent.data.error) { 30 | reject(msgEvent.data.error); 31 | } else { 32 | resolve(msgEvent.data); 33 | } 34 | }; 35 | worker.postMessage({ type: 'skip-waiting' }, [channel.port2]); 36 | }); 37 | // Refresh current page to use the updated HTML and other assets after SW has skiped waiting 38 | window.location.reload(true); 39 | return true; 40 | }; 41 | const key = `open${Date.now()}`; 42 | const btn = ( 43 | 52 | ); 53 | notification.open({ 54 | message: formatMessage({ id: 'app.pwa.serviceworker.updated' }), 55 | description: formatMessage({ id: 'app.pwa.serviceworker.updated.hint' }), 56 | btn, 57 | key, 58 | onClose: async () => {}, 59 | }); 60 | }); 61 | } else if ('serviceWorker' in navigator) { 62 | // unregister service worker 63 | const { serviceWorker } = navigator; 64 | if (serviceWorker.getRegistrations) { 65 | serviceWorker.getRegistrations().then(sws => { 66 | sws.forEach(sw => { 67 | sw.unregister(); 68 | }); 69 | }); 70 | } 71 | serviceWorker.getRegistration().then(sw => { 72 | if (sw) sw.unregister(); 73 | }); 74 | 75 | // remove all caches 76 | if (window.caches && window.caches.keys) { 77 | caches.keys().then(keys => { 78 | keys.forEach(key => { 79 | caches.delete(key); 80 | }); 81 | }); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/layouts/BlankLayout.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Layout: React.FC = ({ children }) =>
{children}
; 4 | 5 | export default Layout; 6 | -------------------------------------------------------------------------------- /src/layouts/SecurityLayout.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'dva'; 3 | import { Redirect } from 'umi'; 4 | import { stringify } from 'querystring'; 5 | import { ConnectState, ConnectProps } from '@/models/connect'; 6 | import { CurrentUser } from '@/models/user'; 7 | import PageLoading from '@/components/PageLoading'; 8 | 9 | interface SecurityLayoutProps extends ConnectProps { 10 | loading: boolean; 11 | currentUser: CurrentUser; 12 | } 13 | 14 | interface SecurityLayoutState { 15 | isReady: boolean; 16 | } 17 | 18 | class SecurityLayout extends React.Component { 19 | state: SecurityLayoutState = { 20 | isReady: false, 21 | }; 22 | 23 | componentDidMount() { 24 | const { dispatch } = this.props; 25 | if (dispatch) { 26 | dispatch({ 27 | type: 'user/fetchCurrent', 28 | callback: () => { 29 | this.setState({ 30 | isReady: true, 31 | }); 32 | }, 33 | }); 34 | } 35 | } 36 | 37 | render() { 38 | const { isReady } = this.state; 39 | const { children, loading, currentUser } = this.props; 40 | // You can replace it to your authentication rule (such as check token exists) 41 | // 你可以把它替换成你自己的登录认证规则(比如判断 token 是否存在) 42 | const isLogin = currentUser && currentUser.userid; 43 | const queryString = stringify({ 44 | redirect: window.location.href, 45 | }); 46 | 47 | if ((!isLogin && loading) || !isReady ) { 48 | return ; 49 | } 50 | if (!isLogin) { 51 | return ; 52 | } 53 | return children; 54 | } 55 | } 56 | 57 | export default connect(({ user, loading }: ConnectState) => ({ 58 | currentUser: user.currentUser, 59 | loading: loading.models.user, 60 | }))(SecurityLayout); 61 | -------------------------------------------------------------------------------- /src/layouts/UserLayout.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | .container { 4 | display: flex; 5 | flex-direction: column; 6 | height: 100vh; 7 | overflow: auto; 8 | background: @layout-body-background; 9 | } 10 | 11 | .lang { 12 | width: 100%; 13 | height: 40px; 14 | line-height: 44px; 15 | text-align: right; 16 | :global(.ant-dropdown-trigger) { 17 | margin-right: 24px; 18 | } 19 | } 20 | 21 | .content { 22 | flex: 1; 23 | padding: 32px 0; 24 | } 25 | 26 | @media (min-width: @screen-md-min) { 27 | .container { 28 | background-image: url('https://gw.alipayobjects.com/zos/rmsportal/TVYTbAXWheQpRcWDaDMu.svg'); 29 | background-repeat: no-repeat; 30 | background-position: center 110px; 31 | background-size: 100%; 32 | } 33 | 34 | .content { 35 | padding: 32px 0 24px; 36 | } 37 | } 38 | 39 | .top { 40 | text-align: center; 41 | } 42 | 43 | .header { 44 | height: 44px; 45 | line-height: 44px; 46 | a { 47 | text-decoration: none; 48 | } 49 | } 50 | 51 | .logo { 52 | height: 44px; 53 | margin-right: 16px; 54 | vertical-align: top; 55 | } 56 | 57 | .title { 58 | position: relative; 59 | top: 2px; 60 | color: @heading-color; 61 | font-weight: 600; 62 | font-size: 33px; 63 | font-family: Avenir, 'Helvetica Neue', Arial, Helvetica, sans-serif; 64 | } 65 | 66 | .desc { 67 | margin-top: 12px; 68 | margin-bottom: 40px; 69 | color: @text-color-secondary; 70 | font-size: @font-size-base; 71 | } 72 | -------------------------------------------------------------------------------- /src/layouts/UserLayout.tsx: -------------------------------------------------------------------------------- 1 | import { DefaultFooter, MenuDataItem, getMenuData, getPageTitle } from '@ant-design/pro-layout'; 2 | import DocumentTitle from 'react-document-title'; 3 | import Link from 'umi/link'; 4 | import React from 'react'; 5 | import { connect } from 'dva'; 6 | import { formatMessage } from 'umi-plugin-react/locale'; 7 | 8 | import SelectLang from '@/components/SelectLang'; 9 | import { ConnectProps, ConnectState } from '@/models/connect'; 10 | import logo from '../assets/logo.svg'; 11 | import styles from './UserLayout.less'; 12 | 13 | export interface UserLayoutProps extends ConnectProps { 14 | breadcrumbNameMap: { [path: string]: MenuDataItem }; 15 | } 16 | 17 | const UserLayout: React.SFC = props => { 18 | const { 19 | route = { 20 | routes: [], 21 | }, 22 | } = props; 23 | const { routes = [] } = route; 24 | const { 25 | children, 26 | location = { 27 | pathname: '', 28 | }, 29 | } = props; 30 | const { breadcrumb } = getMenuData(routes); 31 | 32 | return ( 33 | 41 |
42 |
43 | 44 |
45 |
46 |
47 |
48 | 49 | logo 50 | 批处理平台 51 | 52 |
53 |
Ant Design 是西湖区最具影响力的 Web 设计规范
54 |
55 | {children} 56 |
57 | 58 |
59 |
60 | ); 61 | }; 62 | 63 | export default connect(({ settings }: ConnectState) => ({ 64 | ...settings, 65 | }))(UserLayout); 66 | -------------------------------------------------------------------------------- /src/locales/en-US.ts: -------------------------------------------------------------------------------- 1 | import component from './en-US/component'; 2 | import globalHeader from './en-US/globalHeader'; 3 | import menu from './en-US/menu'; 4 | import pwa from './en-US/pwa'; 5 | import settingDrawer from './en-US/settingDrawer'; 6 | import settings from './en-US/settings'; 7 | 8 | export default { 9 | 'navBar.lang': 'Languages', 10 | 'layout.user.link.help': 'Help', 11 | 'layout.user.link.privacy': 'Privacy', 12 | 'layout.user.link.terms': 'Terms', 13 | 'app.preview.down.block': 'Download this page to your local project', 14 | 'app.welcome.link.fetch-blocks': 'Get all block', 15 | 'app.welcome.link.block-list': 'Quickly build standard, pages based on `block` development', 16 | ...globalHeader, 17 | ...menu, 18 | ...settingDrawer, 19 | ...settings, 20 | ...pwa, 21 | ...component, 22 | }; 23 | -------------------------------------------------------------------------------- /src/locales/en-US/component.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'component.tagSelect.expand': 'Expand', 3 | 'component.tagSelect.collapse': 'Collapse', 4 | 'component.tagSelect.all': 'All', 5 | }; 6 | -------------------------------------------------------------------------------- /src/locales/en-US/globalHeader.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'component.globalHeader.search': 'Search', 3 | 'component.globalHeader.search.example1': 'Search example 1', 4 | 'component.globalHeader.search.example2': 'Search example 2', 5 | 'component.globalHeader.search.example3': 'Search example 3', 6 | 'component.globalHeader.help': 'Help', 7 | 'component.globalHeader.notification': 'Notification', 8 | 'component.globalHeader.notification.empty': 'You have viewed all notifications.', 9 | 'component.globalHeader.message': 'Message', 10 | 'component.globalHeader.message.empty': 'You have viewed all messsages.', 11 | 'component.globalHeader.event': 'Event', 12 | 'component.globalHeader.event.empty': 'You have viewed all events.', 13 | 'component.noticeIcon.clear': 'Clear', 14 | 'component.noticeIcon.cleared': 'Cleared', 15 | 'component.noticeIcon.empty': 'No notifications', 16 | 'component.noticeIcon.view-more': 'View more', 17 | }; 18 | -------------------------------------------------------------------------------- /src/locales/en-US/menu.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'menu.welcome': 'Welcome', 3 | 'menu.more-blocks': 'More Blocks', 4 | 'menu.home': 'Home', 5 | 'menu.login': 'Login', 6 | 'menu.register': 'Register', 7 | 'menu.register.result': 'Register Result', 8 | 'menu.dashboard': 'Dashboard', 9 | 'menu.dashboard.analysis': 'Run Detail', 10 | 'menu.platform':'Platform Run Data', 11 | 'menu.platform.jobRunDetail':'Spring Batch Job Run Detail', 12 | 'menu.platform.quartzTriggerList':'Quartz Trigger List', 13 | 'menu.platform.quartzJob':'Quartz Job Execution History', 14 | 'menu.configuration':'Configuration', 15 | 'menu.configuration.triggerList':'Config Trigger', 16 | 'menu.configuration.jobList':'Config Job', 17 | 'menu.runtime':'Run Data', 18 | 'menu.runtime.jobExecutionHistory':'Job Run History', 19 | 'menu.dashboard.monitor': 'Monitor', 20 | 'menu.dashboard.workplace': 'Workplace', 21 | 'menu.exception.403': '403', 22 | 'menu.exception.404': '404', 23 | 'menu.exception.500': '500', 24 | 'menu.form': 'Form', 25 | 'menu.form.basic-form': 'Basic Form', 26 | 'menu.form.step-form': 'Step Form', 27 | 'menu.form.step-form.info': 'Step Form(write transfer information)', 28 | 'menu.form.step-form.confirm': 'Step Form(confirm transfer information)', 29 | 'menu.form.step-form.result': 'Step Form(finished)', 30 | 'menu.form.advanced-form': 'Advanced Form', 31 | 'menu.list': 'List', 32 | 'menu.list.table-list': 'Search Table', 33 | 'menu.list.basic-list': 'Basic List', 34 | 'menu.list.card-list': 'Card List', 35 | 'menu.list.search-list': 'Search List', 36 | 'menu.list.search-list.articles': 'Search List(articles)', 37 | 'menu.list.search-list.projects': 'Search List(projects)', 38 | 'menu.list.search-list.applications': 'Search List(applications)', 39 | 'menu.profile': 'Profile', 40 | 'menu.profile.basic': 'Basic Profile', 41 | 'menu.profile.advanced': 'Advanced Profile', 42 | 'menu.result': 'Result', 43 | 'menu.result.success': 'Success', 44 | 'menu.result.fail': 'Fail', 45 | 'menu.exception': 'Exception', 46 | 'menu.exception.not-permission': '403', 47 | 'menu.exception.not-find': '404', 48 | 'menu.exception.server-error': '500', 49 | 'menu.exception.trigger': 'Trigger', 50 | 'menu.account': 'Account', 51 | 'menu.account.center': 'Account Center', 52 | 'menu.account.settings': 'Account Settings', 53 | 'menu.account.trigger': 'Trigger Error', 54 | 'menu.account.logout': 'Logout', 55 | 'menu.editor': 'Graphic Editor', 56 | 'menu.editor.flow': 'Flow Editor', 57 | 'menu.editor.mind': 'Mind Editor', 58 | 'menu.editor.koni': 'Koni Editor', 59 | }; 60 | -------------------------------------------------------------------------------- /src/locales/en-US/pwa.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.pwa.offline': 'You are offline now', 3 | 'app.pwa.serviceworker.updated': 'New content is available', 4 | 'app.pwa.serviceworker.updated.hint': 'Please press the "Refresh" button to reload current page', 5 | 'app.pwa.serviceworker.updated.ok': 'Refresh', 6 | }; 7 | -------------------------------------------------------------------------------- /src/locales/en-US/settingDrawer.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.setting.pagestyle': 'Page style setting', 3 | 'app.setting.pagestyle.dark': 'Dark style', 4 | 'app.setting.pagestyle.light': 'Light style', 5 | 'app.setting.content-width': 'Content Width', 6 | 'app.setting.content-width.fixed': 'Fixed', 7 | 'app.setting.content-width.fluid': 'Fluid', 8 | 'app.setting.themecolor': 'Theme Color', 9 | 'app.setting.themecolor.dust': 'Dust Red', 10 | 'app.setting.themecolor.volcano': 'Volcano', 11 | 'app.setting.themecolor.sunset': 'Sunset Orange', 12 | 'app.setting.themecolor.cyan': 'Cyan', 13 | 'app.setting.themecolor.green': 'Polar Green', 14 | 'app.setting.themecolor.daybreak': 'Daybreak Blue (default)', 15 | 'app.setting.themecolor.geekblue': 'Geek Glue', 16 | 'app.setting.themecolor.purple': 'Golden Purple', 17 | 'app.setting.navigationmode': 'Navigation Mode', 18 | 'app.setting.sidemenu': 'Side Menu Layout', 19 | 'app.setting.topmenu': 'Top Menu Layout', 20 | 'app.setting.fixedheader': 'Fixed Header', 21 | 'app.setting.fixedsidebar': 'Fixed Sidebar', 22 | 'app.setting.fixedsidebar.hint': 'Works on Side Menu Layout', 23 | 'app.setting.hideheader': 'Hidden Header when scrolling', 24 | 'app.setting.hideheader.hint': 'Works when Hidden Header is enabled', 25 | 'app.setting.othersettings': 'Other Settings', 26 | 'app.setting.weakmode': 'Weak Mode', 27 | 'app.setting.copy': 'Copy Setting', 28 | 'app.setting.copyinfo': 'copy success,please replace defaultSettings in src/models/setting.js', 29 | 'app.setting.production.hint': 30 | 'Setting panel shows in development environment only, please manually modify', 31 | }; 32 | -------------------------------------------------------------------------------- /src/locales/pt-BR.ts: -------------------------------------------------------------------------------- 1 | import component from './pt-BR/component'; 2 | import globalHeader from './pt-BR/globalHeader'; 3 | import menu from './pt-BR/menu'; 4 | import pwa from './pt-BR/pwa'; 5 | import settingDrawer from './pt-BR/settingDrawer'; 6 | import settings from './pt-BR/settings'; 7 | 8 | export default { 9 | 'navBar.lang': 'Idiomas', 10 | 'layout.user.link.help': 'ajuda', 11 | 'layout.user.link.privacy': 'política de privacidade', 12 | 'layout.user.link.terms': 'termos de serviços', 13 | 'app.preview.down.block': 'Download this page to your local project', 14 | ...globalHeader, 15 | ...menu, 16 | ...settingDrawer, 17 | ...settings, 18 | ...pwa, 19 | ...component, 20 | }; 21 | -------------------------------------------------------------------------------- /src/locales/pt-BR/component.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'component.tagSelect.expand': 'Expandir', 3 | 'component.tagSelect.collapse': 'Diminuir', 4 | 'component.tagSelect.all': 'Todas', 5 | }; 6 | -------------------------------------------------------------------------------- /src/locales/pt-BR/globalHeader.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'component.globalHeader.search': 'Busca', 3 | 'component.globalHeader.search.example1': 'Exemplo de busca 1', 4 | 'component.globalHeader.search.example2': 'Exemplo de busca 2', 5 | 'component.globalHeader.search.example3': 'Exemplo de busca 3', 6 | 'component.globalHeader.help': 'Ajuda', 7 | 'component.globalHeader.notification': 'Notificação', 8 | 'component.globalHeader.notification.empty': 'Você visualizou todas as notificações.', 9 | 'component.globalHeader.message': 'Mensagem', 10 | 'component.globalHeader.message.empty': 'Você visualizou todas as mensagens.', 11 | 'component.globalHeader.event': 'Evento', 12 | 'component.globalHeader.event.empty': 'Você visualizou todos os eventos.', 13 | 'component.noticeIcon.clear': 'Limpar', 14 | 'component.noticeIcon.cleared': 'Limpo', 15 | 'component.noticeIcon.empty': 'Sem notificações', 16 | 'component.noticeIcon.loaded': 'Carregado', 17 | 'component.noticeIcon.view-more': 'Veja mais', 18 | }; 19 | -------------------------------------------------------------------------------- /src/locales/pt-BR/menu.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'menu.welcome': 'Welcome', 3 | 'menu.more-blocks': 'More Blocks', 4 | 5 | 'menu.home': 'Início', 6 | 'menu.login': 'Login', 7 | 'menu.register': 'Registro', 8 | 'menu.register.result': 'Resultado de registro', 9 | 'menu.dashboard': 'Dashboard', 10 | 'menu.dashboard.analysis': 'Análise', 11 | 'menu.dashboard.monitor': 'Monitor', 12 | 'menu.dashboard.workplace': 'Ambiente de Trabalho', 13 | 'menu.exception.403': '403', 14 | 'menu.exception.404': '404', 15 | 'menu.exception.500': '500', 16 | 'menu.form': 'Formulário', 17 | 'menu.form.basic-form': 'Formulário Básico', 18 | 'menu.form.step-form': 'Formulário Assistido', 19 | 'menu.form.step-form.info': 'Formulário Assistido(gravar informações de transferência)', 20 | 'menu.form.step-form.confirm': 'Formulário Assistido(confirmar informações de transferência)', 21 | 'menu.form.step-form.result': 'Formulário Assistido(finalizado)', 22 | 'menu.form.advanced-form': 'Formulário Avançado', 23 | 'menu.list': 'Lista', 24 | 'menu.list.table-list': 'Tabela de Busca', 25 | 'menu.list.basic-list': 'Lista Básica', 26 | 'menu.list.card-list': 'Lista de Card', 27 | 'menu.list.search-list': 'Lista de Busca', 28 | 'menu.list.search-list.articles': 'Lista de Busca(artigos)', 29 | 'menu.list.search-list.projects': 'Lista de Busca(projetos)', 30 | 'menu.list.search-list.applications': 'Lista de Busca(aplicações)', 31 | 'menu.profile': 'Perfil', 32 | 'menu.profile.basic': 'Perfil Básico', 33 | 'menu.profile.advanced': 'Perfil Avançado', 34 | 'menu.result': 'Resultado', 35 | 'menu.result.success': 'Sucesso', 36 | 'menu.result.fail': 'Falha', 37 | 'menu.exception': 'Exceção', 38 | 'menu.exception.not-permission': '403', 39 | 'menu.exception.not-find': '404', 40 | 'menu.exception.server-error': '500', 41 | 'menu.exception.trigger': 'Disparar', 42 | 'menu.account': 'Conta', 43 | 'menu.account.center': 'Central da Conta', 44 | 'menu.account.settings': 'Configurar Conta', 45 | 'menu.account.trigger': 'Disparar Erro', 46 | 'menu.account.logout': 'Sair', 47 | 'menu.editor': 'Graphic Editor', 48 | 'menu.editor.flow': 'Flow Editor', 49 | 'menu.editor.mind': 'Mind Editor', 50 | 'menu.editor.koni': 'Koni Editor', 51 | }; 52 | -------------------------------------------------------------------------------- /src/locales/pt-BR/pwa.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.pwa.offline': 'Você está offline agora', 3 | 'app.pwa.serviceworker.updated': 'Novo conteúdo está disponível', 4 | 'app.pwa.serviceworker.updated.hint': 5 | 'Por favor, pressione o botão "Atualizar" para recarregar a página atual', 6 | 'app.pwa.serviceworker.updated.ok': 'Atualizar', 7 | }; 8 | -------------------------------------------------------------------------------- /src/locales/pt-BR/settingDrawer.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.setting.pagestyle': 'Configuração de estilo da página', 3 | 'app.setting.pagestyle.dark': 'Dark style', 4 | 'app.setting.pagestyle.light': 'Light style', 5 | 'app.setting.content-width': 'Largura do conteúdo', 6 | 'app.setting.content-width.fixed': 'Fixo', 7 | 'app.setting.content-width.fluid': 'Fluido', 8 | 'app.setting.themecolor': 'Cor do Tema', 9 | 'app.setting.themecolor.dust': 'Dust Red', 10 | 'app.setting.themecolor.volcano': 'Volcano', 11 | 'app.setting.themecolor.sunset': 'Sunset Orange', 12 | 'app.setting.themecolor.cyan': 'Cyan', 13 | 'app.setting.themecolor.green': 'Polar Green', 14 | 'app.setting.themecolor.daybreak': 'Daybreak Blue (default)', 15 | 'app.setting.themecolor.geekblue': 'Geek Glue', 16 | 'app.setting.themecolor.purple': 'Golden Purple', 17 | 'app.setting.navigationmode': 'Modo de Navegação', 18 | 'app.setting.sidemenu': 'Layout do Menu Lateral', 19 | 'app.setting.topmenu': 'Layout do Menu Superior', 20 | 'app.setting.fixedheader': 'Cabeçalho fixo', 21 | 'app.setting.fixedsidebar': 'Barra lateral fixa', 22 | 'app.setting.fixedsidebar.hint': 'Funciona no layout do menu lateral', 23 | 'app.setting.hideheader': 'Esconder o cabeçalho quando rolar', 24 | 'app.setting.hideheader.hint': 'Funciona quando o esconder cabeçalho está abilitado', 25 | 'app.setting.othersettings': 'Outras configurações', 26 | 'app.setting.weakmode': 'Weak Mode', 27 | 'app.setting.copy': 'Copiar Configuração', 28 | 'app.setting.copyinfo': 29 | 'copiado com sucesso,por favor trocar o defaultSettings em src/models/setting.js', 30 | 'app.setting.production.hint': 31 | 'O painel de configuração apenas é exibido no ambiente de desenvolvimento, por favor modifique manualmente o', 32 | }; 33 | -------------------------------------------------------------------------------- /src/locales/zh-CN.ts: -------------------------------------------------------------------------------- 1 | import component from './zh-CN/component'; 2 | import globalHeader from './zh-CN/globalHeader'; 3 | import menu from './zh-CN/menu'; 4 | import pwa from './zh-CN/pwa'; 5 | import settingDrawer from './zh-CN/settingDrawer'; 6 | import settings from './zh-CN/settings'; 7 | 8 | export default { 9 | 'navBar.lang': '语言', 10 | 'layout.user.link.help': '帮助', 11 | 'layout.user.link.privacy': '隐私', 12 | 'layout.user.link.terms': '条款', 13 | 'app.preview.down.block': '下载此页面到本地项目', 14 | 'app.welcome.link.fetch-blocks': '获取全部区块', 15 | 'app.welcome.link.block-list': '基于 block 开发,快速构建标准页面', 16 | ...globalHeader, 17 | ...menu, 18 | ...settingDrawer, 19 | ...settings, 20 | ...pwa, 21 | ...component, 22 | }; 23 | -------------------------------------------------------------------------------- /src/locales/zh-CN/component.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'component.tagSelect.expand': '展开', 3 | 'component.tagSelect.collapse': '收起', 4 | 'component.tagSelect.all': '全部', 5 | }; 6 | -------------------------------------------------------------------------------- /src/locales/zh-CN/globalHeader.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'component.globalHeader.search': '站内搜索', 3 | 'component.globalHeader.search.example1': '搜索提示一', 4 | 'component.globalHeader.search.example2': '搜索提示二', 5 | 'component.globalHeader.search.example3': '搜索提示三', 6 | 'component.globalHeader.help': '使用文档', 7 | 'component.globalHeader.notification': '通知', 8 | 'component.globalHeader.notification.empty': '你已查看所有通知', 9 | 'component.globalHeader.message': '消息', 10 | 'component.globalHeader.message.empty': '您已读完所有消息', 11 | 'component.globalHeader.event': '待办', 12 | 'component.globalHeader.event.empty': '你已完成所有待办', 13 | 'component.noticeIcon.clear': '清空', 14 | 'component.noticeIcon.cleared': '清空了', 15 | 'component.noticeIcon.empty': '暂无数据', 16 | 'component.noticeIcon.view-more': '查看更多', 17 | }; 18 | -------------------------------------------------------------------------------- /src/locales/zh-CN/menu.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'menu.welcome': '欢迎', 3 | 'menu.more-blocks': '更多区块', 4 | 'menu.home': '首页', 5 | 'menu.login': '登录', 6 | 'menu.register': '注册', 7 | 'menu.register.result': '注册结果', 8 | 'menu.dashboard': 'Dashboard', 9 | 'menu.dashboard.analysis': '首页', 10 | 'menu.platform':'底层运行数据', 11 | 'menu.platform.jobRunDetail':'Spring Batch 任务执行记录', 12 | 'menu.platform.quartzTriggerList':'Quartz 触发器列表', 13 | 'menu.platform.quartzJob':'Quartz 任务触发记录', 14 | 'menu.configuration':'配置触发器和任务', 15 | 'menu.configuration.triggerList':'触发器列表', 16 | 'menu.configuration.jobList':'任务器列表', 17 | 'menu.runtime':'运行数据', 18 | 'menu.runtime.jobExecutionHistory':'任务运行记录', 19 | 'menu.dashboard.monitor': '监控页', 20 | 'menu.dashboard.workplace': '工作台', 21 | 'menu.exception.403': '403', 22 | 'menu.exception.404': '404', 23 | 'menu.exception.500': '500', 24 | 'menu.form': '表单页', 25 | 'menu.form.basic-form': '基础表单', 26 | 'menu.form.step-form': '分步表单', 27 | 'menu.form.step-form.info': '分步表单(填写转账信息)', 28 | 'menu.form.step-form.confirm': '分步表单(确认转账信息)', 29 | 'menu.form.step-form.result': '分步表单(完成)', 30 | 'menu.form.advanced-form': '高级表单', 31 | 'menu.list': '列表页', 32 | 'menu.list.table-list': '查询表格', 33 | 'menu.list.basic-list': '标准列表', 34 | 'menu.list.card-list': '卡片列表', 35 | 'menu.list.search-list': '搜索列表', 36 | 'menu.list.search-list.articles': '搜索列表(文章)', 37 | 'menu.list.search-list.projects': '搜索列表(项目)', 38 | 'menu.list.search-list.applications': '搜索列表(应用)', 39 | 'menu.profile': '详情页', 40 | 'menu.profile.basic': '基础详情页', 41 | 'menu.profile.advanced': '高级详情页', 42 | 'menu.result': '结果页', 43 | 'menu.result.success': '成功页', 44 | 'menu.result.fail': '失败页', 45 | 'menu.exception': '异常页', 46 | 'menu.exception.not-permission': '403', 47 | 'menu.exception.not-find': '404', 48 | 'menu.exception.server-error': '500', 49 | 'menu.exception.trigger': '触发错误', 50 | 'menu.account': '个人页', 51 | 'menu.account.center': '个人中心', 52 | 'menu.account.settings': '个人设置', 53 | 'menu.account.trigger': '触发报错', 54 | 'menu.account.logout': '退出登录', 55 | 'menu.editor': '图形编辑器', 56 | 'menu.editor.flow': '流程编辑器', 57 | 'menu.editor.mind': '脑图编辑器', 58 | 'menu.editor.koni': '拓扑编辑器', 59 | }; 60 | -------------------------------------------------------------------------------- /src/locales/zh-CN/pwa.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.pwa.offline': '当前处于离线状态', 3 | 'app.pwa.serviceworker.updated': '有新内容', 4 | 'app.pwa.serviceworker.updated.hint': '请点击“刷新”按钮或者手动刷新页面', 5 | 'app.pwa.serviceworker.updated.ok': '刷新', 6 | }; 7 | -------------------------------------------------------------------------------- /src/locales/zh-CN/settingDrawer.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.setting.pagestyle': '整体风格设置', 3 | 'app.setting.pagestyle.dark': '暗色菜单风格', 4 | 'app.setting.pagestyle.light': '亮色菜单风格', 5 | 'app.setting.content-width': '内容区域宽度', 6 | 'app.setting.content-width.fixed': '定宽', 7 | 'app.setting.content-width.fluid': '流式', 8 | 'app.setting.themecolor': '主题色', 9 | 'app.setting.themecolor.dust': '薄暮', 10 | 'app.setting.themecolor.volcano': '火山', 11 | 'app.setting.themecolor.sunset': '日暮', 12 | 'app.setting.themecolor.cyan': '明青', 13 | 'app.setting.themecolor.green': '极光绿', 14 | 'app.setting.themecolor.daybreak': '拂晓蓝(默认)', 15 | 'app.setting.themecolor.geekblue': '极客蓝', 16 | 'app.setting.themecolor.purple': '酱紫', 17 | 'app.setting.navigationmode': '导航模式', 18 | 'app.setting.sidemenu': '侧边菜单布局', 19 | 'app.setting.topmenu': '顶部菜单布局', 20 | 'app.setting.fixedheader': '固定 Header', 21 | 'app.setting.fixedsidebar': '固定侧边菜单', 22 | 'app.setting.fixedsidebar.hint': '侧边菜单布局时可配置', 23 | 'app.setting.hideheader': '下滑时隐藏 Header', 24 | 'app.setting.hideheader.hint': '固定 Header 时可配置', 25 | 'app.setting.othersettings': '其他设置', 26 | 'app.setting.weakmode': '色弱模式', 27 | 'app.setting.copy': '拷贝设置', 28 | 'app.setting.copyinfo': '拷贝成功,请到 src/defaultSettings.js 中替换默认配置', 29 | 'app.setting.production.hint': 30 | '配置栏只在开发环境用于预览,生产环境不会展现,请拷贝后手动修改配置文件', 31 | }; 32 | -------------------------------------------------------------------------------- /src/locales/zh-CN/settings.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.settings.menuMap.basic': '基本设置', 3 | 'app.settings.menuMap.security': '安全设置', 4 | 'app.settings.menuMap.binding': '账号绑定', 5 | 'app.settings.menuMap.notification': '新消息通知', 6 | 'app.settings.basic.avatar': '头像', 7 | 'app.settings.basic.change-avatar': '更换头像', 8 | 'app.settings.basic.email': '邮箱', 9 | 'app.settings.basic.email-message': '请输入您的邮箱!', 10 | 'app.settings.basic.nickname': '昵称', 11 | 'app.settings.basic.nickname-message': '请输入您的昵称!', 12 | 'app.settings.basic.profile': '个人简介', 13 | 'app.settings.basic.profile-message': '请输入个人简介!', 14 | 'app.settings.basic.profile-placeholder': '个人简介', 15 | 'app.settings.basic.country': '国家/地区', 16 | 'app.settings.basic.country-message': '请输入您的国家或地区!', 17 | 'app.settings.basic.geographic': '所在省市', 18 | 'app.settings.basic.geographic-message': '请输入您的所在省市!', 19 | 'app.settings.basic.address': '街道地址', 20 | 'app.settings.basic.address-message': '请输入您的街道地址!', 21 | 'app.settings.basic.phone': '联系电话', 22 | 'app.settings.basic.phone-message': '请输入您的联系电话!', 23 | 'app.settings.basic.update': '更新基本信息', 24 | 'app.settings.security.strong': '强', 25 | 'app.settings.security.medium': '中', 26 | 'app.settings.security.weak': '弱', 27 | 'app.settings.security.password': '账户密码', 28 | 'app.settings.security.password-description': '当前密码强度', 29 | 'app.settings.security.phone': '密保手机', 30 | 'app.settings.security.phone-description': '已绑定手机', 31 | 'app.settings.security.question': '密保问题', 32 | 'app.settings.security.question-description': '未设置密保问题,密保问题可有效保护账户安全', 33 | 'app.settings.security.email': '备用邮箱', 34 | 'app.settings.security.email-description': '已绑定邮箱', 35 | 'app.settings.security.mfa': 'MFA 设备', 36 | 'app.settings.security.mfa-description': '未绑定 MFA 设备,绑定后,可以进行二次确认', 37 | 'app.settings.security.modify': '修改', 38 | 'app.settings.security.set': '设置', 39 | 'app.settings.security.bind': '绑定', 40 | 'app.settings.binding.taobao': '绑定淘宝', 41 | 'app.settings.binding.taobao-description': '当前未绑定淘宝账号', 42 | 'app.settings.binding.alipay': '绑定支付宝', 43 | 'app.settings.binding.alipay-description': '当前未绑定支付宝账号', 44 | 'app.settings.binding.dingding': '绑定钉钉', 45 | 'app.settings.binding.dingding-description': '当前未绑定钉钉账号', 46 | 'app.settings.binding.bind': '绑定', 47 | 'app.settings.notification.password': '账户密码', 48 | 'app.settings.notification.password-description': '其他用户的消息将以站内信的形式通知', 49 | 'app.settings.notification.messages': '系统消息', 50 | 'app.settings.notification.messages-description': '系统消息将以站内信的形式通知', 51 | 'app.settings.notification.todo': '待办任务', 52 | 'app.settings.notification.todo-description': '待办任务将以站内信的形式通知', 53 | 'app.settings.open': '开', 54 | 'app.settings.close': '关', 55 | }; 56 | -------------------------------------------------------------------------------- /src/locales/zh-TW.ts: -------------------------------------------------------------------------------- 1 | import component from './zh-TW/component'; 2 | import globalHeader from './zh-TW/globalHeader'; 3 | import menu from './zh-TW/menu'; 4 | import pwa from './zh-TW/pwa'; 5 | import settingDrawer from './zh-TW/settingDrawer'; 6 | import settings from './zh-TW/settings'; 7 | 8 | export default { 9 | 'navBar.lang': '語言', 10 | 'layout.user.link.help': '幫助', 11 | 'layout.user.link.privacy': '隱私', 12 | 'layout.user.link.terms': '條款', 13 | 'app.preview.down.block': '下載此頁面到本地項目', 14 | ...globalHeader, 15 | ...menu, 16 | ...settingDrawer, 17 | ...settings, 18 | ...pwa, 19 | ...component, 20 | }; 21 | -------------------------------------------------------------------------------- /src/locales/zh-TW/component.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'component.tagSelect.expand': '展開', 3 | 'component.tagSelect.collapse': '收起', 4 | 'component.tagSelect.all': '全部', 5 | }; 6 | -------------------------------------------------------------------------------- /src/locales/zh-TW/globalHeader.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'component.globalHeader.search': '站內搜索', 3 | 'component.globalHeader.search.example1': '搜索提示壹', 4 | 'component.globalHeader.search.example2': '搜索提示二', 5 | 'component.globalHeader.search.example3': '搜索提示三', 6 | 'component.globalHeader.help': '使用手冊', 7 | 'component.globalHeader.notification': '通知', 8 | 'component.globalHeader.notification.empty': '妳已查看所有通知', 9 | 'component.globalHeader.message': '消息', 10 | 'component.globalHeader.message.empty': '您已讀完所有消息', 11 | 'component.globalHeader.event': '待辦', 12 | 'component.globalHeader.event.empty': '妳已完成所有待辦', 13 | 'component.noticeIcon.clear': '清空', 14 | 'component.noticeIcon.cleared': '清空了', 15 | 'component.noticeIcon.empty': '暫無資料', 16 | 'component.noticeIcon.view-more': '查看更多', 17 | }; 18 | -------------------------------------------------------------------------------- /src/locales/zh-TW/menu.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'menu.welcome': '歡迎', 3 | 'menu.more-blocks': '更多區塊', 4 | 5 | 'menu.home': '首頁', 6 | 'menu.login': '登錄', 7 | 'menu.exception.403': '403', 8 | 'menu.exception.404': '404', 9 | 'menu.exception.500': '500', 10 | 'menu.register': '註冊', 11 | 'menu.register.result': '註冊結果', 12 | 'menu.dashboard': 'Dashboard', 13 | 'menu.dashboard.analysis': '分析頁', 14 | 'menu.dashboard.monitor': '監控頁', 15 | 'menu.dashboard.workplace': '工作臺', 16 | 'menu.form': '表單頁', 17 | 'menu.form.basic-form': '基礎表單', 18 | 'menu.form.step-form': '分步表單', 19 | 'menu.form.step-form.info': '分步表單(填寫轉賬信息)', 20 | 'menu.form.step-form.confirm': '分步表單(確認轉賬信息)', 21 | 'menu.form.step-form.result': '分步表單(完成)', 22 | 'menu.form.advanced-form': '高級表單', 23 | 'menu.list': '列表頁', 24 | 'menu.list.table-list': '查詢表格', 25 | 'menu.list.basic-list': '標淮列表', 26 | 'menu.list.card-list': '卡片列表', 27 | 'menu.list.search-list': '搜索列表', 28 | 'menu.list.search-list.articles': '搜索列表(文章)', 29 | 'menu.list.search-list.projects': '搜索列表(項目)', 30 | 'menu.list.search-list.applications': '搜索列表(應用)', 31 | 'menu.profile': '詳情頁', 32 | 'menu.profile.basic': '基礎詳情頁', 33 | 'menu.profile.advanced': '高級詳情頁', 34 | 'menu.result': '結果頁', 35 | 'menu.result.success': '成功頁', 36 | 'menu.result.fail': '失敗頁', 37 | 'menu.account': '個人頁', 38 | 'menu.account.center': '個人中心', 39 | 'menu.account.settings': '個人設置', 40 | 'menu.account.trigger': '觸發報錯', 41 | 'menu.account.logout': '退出登錄', 42 | 'menu.exception': '异常页', 43 | 'menu.exception.not-permission': '403', 44 | 'menu.exception.not-find': '404', 45 | 'menu.exception.server-error': '500', 46 | 'menu.exception.trigger': '触发错误', 47 | 'menu.editor': '圖形編輯器', 48 | 'menu.editor.flow': '流程編輯器', 49 | 'menu.editor.mind': '腦圖編輯器', 50 | 'menu.editor.koni': '拓撲編輯器', 51 | }; 52 | -------------------------------------------------------------------------------- /src/locales/zh-TW/pwa.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.pwa.offline': '當前處於離線狀態', 3 | 'app.pwa.serviceworker.updated': '有新內容', 4 | 'app.pwa.serviceworker.updated.hint': '請點擊“刷新”按鈕或者手動刷新頁面', 5 | 'app.pwa.serviceworker.updated.ok': '刷新', 6 | }; 7 | -------------------------------------------------------------------------------- /src/locales/zh-TW/settingDrawer.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.setting.pagestyle': '整體風格設置', 3 | 'app.setting.pagestyle.dark': '暗色菜單風格', 4 | 'app.setting.pagestyle.light': '亮色菜單風格', 5 | 'app.setting.content-width': '內容區域寬度', 6 | 'app.setting.content-width.fixed': '定寬', 7 | 'app.setting.content-width.fluid': '流式', 8 | 'app.setting.themecolor': '主題色', 9 | 'app.setting.themecolor.dust': '薄暮', 10 | 'app.setting.themecolor.volcano': '火山', 11 | 'app.setting.themecolor.sunset': '日暮', 12 | 'app.setting.themecolor.cyan': '明青', 13 | 'app.setting.themecolor.green': '極光綠', 14 | 'app.setting.themecolor.daybreak': '拂曉藍(默認)', 15 | 'app.setting.themecolor.geekblue': '極客藍', 16 | 'app.setting.themecolor.purple': '醬紫', 17 | 'app.setting.navigationmode': '導航模式', 18 | 'app.setting.sidemenu': '側邊菜單布局', 19 | 'app.setting.topmenu': '頂部菜單布局', 20 | 'app.setting.fixedheader': '固定 Header', 21 | 'app.setting.fixedsidebar': '固定側邊菜單', 22 | 'app.setting.fixedsidebar.hint': '側邊菜單布局時可配置', 23 | 'app.setting.hideheader': '下滑時隱藏 Header', 24 | 'app.setting.hideheader.hint': '固定 Header 時可配置', 25 | 'app.setting.othersettings': '其他設置', 26 | 'app.setting.weakmode': '色弱模式', 27 | 'app.setting.copy': '拷貝設置', 28 | 'app.setting.copyinfo': '拷貝成功,請到 src/defaultSettings.js 中替換默認配置', 29 | 'app.setting.production.hint': 30 | '配置欄只在開發環境用於預覽,生產環境不會展現,請拷貝後手動修改配置文件', 31 | }; 32 | -------------------------------------------------------------------------------- /src/locales/zh-TW/settings.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.settings.menuMap.basic': '基本設置', 3 | 'app.settings.menuMap.security': '安全設置', 4 | 'app.settings.menuMap.binding': '賬號綁定', 5 | 'app.settings.menuMap.notification': '新消息通知', 6 | 'app.settings.basic.avatar': '頭像', 7 | 'app.settings.basic.change-avatar': '更換頭像', 8 | 'app.settings.basic.email': '郵箱', 9 | 'app.settings.basic.email-message': '請輸入您的郵箱!', 10 | 'app.settings.basic.nickname': '昵稱', 11 | 'app.settings.basic.nickname-message': '請輸入您的昵稱!', 12 | 'app.settings.basic.profile': '個人簡介', 13 | 'app.settings.basic.profile-message': '請輸入個人簡介!', 14 | 'app.settings.basic.profile-placeholder': '個人簡介', 15 | 'app.settings.basic.country': '國家/地區', 16 | 'app.settings.basic.country-message': '請輸入您的國家或地區!', 17 | 'app.settings.basic.geographic': '所在省市', 18 | 'app.settings.basic.geographic-message': '請輸入您的所在省市!', 19 | 'app.settings.basic.address': '街道地址', 20 | 'app.settings.basic.address-message': '請輸入您的街道地址!', 21 | 'app.settings.basic.phone': '聯系電話', 22 | 'app.settings.basic.phone-message': '請輸入您的聯系電話!', 23 | 'app.settings.basic.update': '更新基本信息', 24 | 'app.settings.security.strong': '強', 25 | 'app.settings.security.medium': '中', 26 | 'app.settings.security.weak': '弱', 27 | 'app.settings.security.password': '賬戶密碼', 28 | 'app.settings.security.password-description': '當前密碼強度', 29 | 'app.settings.security.phone': '密保手機', 30 | 'app.settings.security.phone-description': '已綁定手機', 31 | 'app.settings.security.question': '密保問題', 32 | 'app.settings.security.question-description': '未設置密保問題,密保問題可有效保護賬戶安全', 33 | 'app.settings.security.email': '備用郵箱', 34 | 'app.settings.security.email-description': '已綁定郵箱', 35 | 'app.settings.security.mfa': 'MFA 設備', 36 | 'app.settings.security.mfa-description': '未綁定 MFA 設備,綁定後,可以進行二次確認', 37 | 'app.settings.security.modify': '修改', 38 | 'app.settings.security.set': '設置', 39 | 'app.settings.security.bind': '綁定', 40 | 'app.settings.binding.taobao': '綁定淘寶', 41 | 'app.settings.binding.taobao-description': '當前未綁定淘寶賬號', 42 | 'app.settings.binding.alipay': '綁定支付寶', 43 | 'app.settings.binding.alipay-description': '當前未綁定支付寶賬號', 44 | 'app.settings.binding.dingding': '綁定釘釘', 45 | 'app.settings.binding.dingding-description': '當前未綁定釘釘賬號', 46 | 'app.settings.binding.bind': '綁定', 47 | 'app.settings.notification.password': '賬戶密碼', 48 | 'app.settings.notification.password-description': '其他用戶的消息將以站內信的形式通知', 49 | 'app.settings.notification.messages': '系統消息', 50 | 'app.settings.notification.messages-description': '系統消息將以站內信的形式通知', 51 | 'app.settings.notification.todo': '待辦任務', 52 | 'app.settings.notification.todo-description': '待辦任務將以站內信的形式通知', 53 | 'app.settings.open': '開', 54 | 'app.settings.close': '關', 55 | }; 56 | -------------------------------------------------------------------------------- /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/models/connect.d.ts: -------------------------------------------------------------------------------- 1 | import { AnyAction, Dispatch } from 'redux'; 2 | import { MenuDataItem } from '@ant-design/pro-layout'; 3 | import { RouterTypes } from 'umi'; 4 | import { GlobalModelState } from './global'; 5 | import { DefaultSettings as SettingModelState } from '../../config/defaultSettings'; 6 | import { UserModelState } from './user'; 7 | import { LoginModelType } from './login'; 8 | 9 | export { GlobalModelState, SettingModelState, UserModelState }; 10 | 11 | export interface Loading { 12 | global: boolean; 13 | effects: { [key: string]: boolean | undefined }; 14 | models: { 15 | global?: boolean; 16 | menu?: boolean; 17 | setting?: boolean; 18 | user?: boolean; 19 | login?: boolean; 20 | }; 21 | } 22 | 23 | export interface ConnectState { 24 | global: GlobalModelState; 25 | loading: Loading; 26 | settings: SettingModelState; 27 | user: UserModelState; 28 | login: LoginModelType; 29 | } 30 | 31 | export interface Route extends MenuDataItem { 32 | routes?: Route[]; 33 | } 34 | 35 | /** 36 | * @type T: Params matched in dynamic routing 37 | */ 38 | export interface ConnectProps extends Partial> { 39 | dispatch?: Dispatch; 40 | } 41 | -------------------------------------------------------------------------------- /src/models/setting.ts: -------------------------------------------------------------------------------- 1 | import { Reducer } from 'redux'; 2 | import { message } from 'antd'; 3 | import defaultSettings, { DefaultSettings } from '../../config/defaultSettings'; 4 | import themeColorClient from '../components/SettingDrawer/themeColorClient'; 5 | 6 | export interface SettingModelType { 7 | namespace: 'settings'; 8 | state: DefaultSettings; 9 | reducers: { 10 | getSetting: Reducer; 11 | changeSetting: Reducer; 12 | }; 13 | } 14 | 15 | const updateTheme = (newPrimaryColor?: string) => { 16 | if (newPrimaryColor) { 17 | const timeOut = 0; 18 | const hideMessage = message.loading('正在切换主题!', timeOut); 19 | themeColorClient.changeColor(newPrimaryColor).finally(() => hideMessage()); 20 | } 21 | }; 22 | 23 | const updateColorWeak: (colorWeak: boolean) => void = colorWeak => { 24 | const root = document.getElementById('root'); 25 | if (root) { 26 | root.className = colorWeak ? 'colorWeak' : ''; 27 | } 28 | }; 29 | 30 | const SettingModel: SettingModelType = { 31 | namespace: 'settings', 32 | state: defaultSettings, 33 | reducers: { 34 | getSetting(state = defaultSettings) { 35 | const setting: Partial = {}; 36 | const urlParams = new URL(window.location.href); 37 | Object.keys(state).forEach(key => { 38 | if (urlParams.searchParams.has(key)) { 39 | const value = urlParams.searchParams.get(key); 40 | setting[key] = value === '1' ? true : value; 41 | } 42 | }); 43 | const { primaryColor, colorWeak } = setting; 44 | 45 | if (primaryColor && state.primaryColor !== primaryColor) { 46 | updateTheme(primaryColor); 47 | } 48 | updateColorWeak(!!colorWeak); 49 | return { 50 | ...state, 51 | ...setting, 52 | }; 53 | }, 54 | changeSetting(state = defaultSettings, { payload }) { 55 | const urlParams = new URL(window.location.href); 56 | Object.keys(defaultSettings).forEach(key => { 57 | if (urlParams.searchParams.has(key)) { 58 | urlParams.searchParams.delete(key); 59 | } 60 | }); 61 | Object.keys(payload).forEach(key => { 62 | if (key === 'collapse') { 63 | return; 64 | } 65 | let value = payload[key]; 66 | if (value === true) { 67 | value = 1; 68 | } 69 | if (defaultSettings[key] !== value) { 70 | urlParams.searchParams.set(key, value); 71 | } 72 | }); 73 | const { primaryColor, colorWeak, contentWidth } = payload; 74 | if (primaryColor && state.primaryColor !== primaryColor) { 75 | updateTheme(primaryColor); 76 | } 77 | if (state.contentWidth !== contentWidth && window.dispatchEvent) { 78 | window.dispatchEvent(new Event('resize')); 79 | } 80 | updateColorWeak(!!colorWeak); 81 | window.history.replaceState(null, 'setting', urlParams.href); 82 | return { 83 | ...state, 84 | ...payload, 85 | }; 86 | }, 87 | }, 88 | }; 89 | export default SettingModel; 90 | -------------------------------------------------------------------------------- /src/models/user.ts: -------------------------------------------------------------------------------- 1 | import { Effect } from 'dva'; 2 | import { Reducer } from 'redux'; 3 | 4 | import { queryCurrent, query as queryUsers } from '@/services/user'; 5 | 6 | export interface CurrentUser { 7 | avatar?: string; 8 | name?: string; 9 | title?: string; 10 | group?: string; 11 | signature?: string; 12 | tags?: { 13 | key: string; 14 | label: string; 15 | }[]; 16 | userid?: string; 17 | unreadCount?: number; 18 | } 19 | 20 | export interface UserModelState { 21 | currentUser?: CurrentUser; 22 | } 23 | 24 | export interface UserModelType { 25 | namespace: 'user'; 26 | state: UserModelState; 27 | effects: { 28 | fetch: Effect; 29 | fetchCurrent: Effect; 30 | }; 31 | reducers: { 32 | saveCurrentUser: Reducer; 33 | changeNotifyCount: Reducer; 34 | }; 35 | } 36 | 37 | const UserModel: UserModelType = { 38 | namespace: 'user', 39 | 40 | state: { 41 | currentUser: {}, 42 | }, 43 | 44 | effects: { 45 | *fetch({callback}, { call, put }) { 46 | const response = yield call(queryUsers); 47 | yield put({ 48 | type: 'save', 49 | payload: response, 50 | }); 51 | if (callback && typeof callback === 'function') { 52 | callback(); 53 | } 54 | }, 55 | *fetchCurrent({callback}, { call, put }) { 56 | const response = yield call(queryCurrent); 57 | yield put({ 58 | type: 'saveCurrentUser', 59 | payload: response, 60 | }); 61 | if (callback && typeof callback === 'function') { 62 | callback(); 63 | } 64 | }, 65 | }, 66 | 67 | reducers: { 68 | saveCurrentUser(state, action) { 69 | return { 70 | ...state, 71 | currentUser: action.payload || {}, 72 | }; 73 | }, 74 | changeNotifyCount( 75 | state = { 76 | currentUser: {}, 77 | }, 78 | action, 79 | ) { 80 | return { 81 | ...state, 82 | currentUser: { 83 | ...state.currentUser, 84 | notifyCount: action.payload.totalCount, 85 | unreadCount: action.payload.unreadCount, 86 | }, 87 | }; 88 | }, 89 | }, 90 | }; 91 | 92 | export default UserModel; 93 | -------------------------------------------------------------------------------- /src/pages/404.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Result } from 'antd'; 2 | import React from 'react'; 3 | import router from 'umi/router'; 4 | 5 | // 这里应该使用 antd 的 404 result 组件, 6 | // 但是还没发布,先来个简单的。 7 | 8 | const NoFoundPage: React.FC<{}> = () => ( 9 | router.push('/')}> 15 | Back Home 16 | 17 | } 18 | > 19 | ); 20 | 21 | export default NoFoundPage; 22 | -------------------------------------------------------------------------------- /src/pages/Authorized.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Redirect from 'umi/redirect'; 3 | import { connect } from 'dva'; 4 | import pathToRegexp from 'path-to-regexp'; 5 | import Authorized from '@/utils/Authorized'; 6 | import { ConnectProps, ConnectState, Route, UserModelState } from '@/models/connect'; 7 | 8 | interface AuthComponentProps extends ConnectProps { 9 | user: UserModelState; 10 | } 11 | 12 | const getRouteAuthority = (path: string, routeData: Route[]) => { 13 | let authorities: string[] | string | undefined; 14 | routeData.forEach(route => { 15 | // match prefix 16 | if (pathToRegexp(`${route.path}(.*)`).test(path)) { 17 | // exact match 18 | if (route.path === path) { 19 | authorities = route.authority || authorities; 20 | } 21 | // get children authority recursively 22 | if (route.routes) { 23 | authorities = getRouteAuthority(path, route.routes) || authorities; 24 | } 25 | } 26 | }); 27 | return authorities; 28 | }; 29 | 30 | const AuthComponent: React.FC = ({ 31 | children, 32 | route = { 33 | routes: [], 34 | }, 35 | location = { 36 | pathname: '', 37 | }, 38 | user, 39 | }) => { 40 | const { currentUser } = user; 41 | const { routes = [] } = route; 42 | const isLogin = currentUser && currentUser.name; 43 | return ( 44 | : } 47 | > 48 | {children} 49 | 50 | ); 51 | }; 52 | 53 | export default connect(({ user }: ConnectState) => ({ 54 | user, 55 | }))(AuthComponent); 56 | -------------------------------------------------------------------------------- /src/pages/analysis/components/Charts/ChartCard/index.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | .chartCard { 4 | position: relative; 5 | .chartTop { 6 | position: relative; 7 | width: 100%; 8 | overflow: hidden; 9 | } 10 | .chartTopMargin { 11 | margin-bottom: 12px; 12 | } 13 | .chartTopHasMargin { 14 | margin-bottom: 20px; 15 | } 16 | .metaWrap { 17 | float: left; 18 | } 19 | .avatar { 20 | position: relative; 21 | top: 4px; 22 | float: left; 23 | margin-right: 20px; 24 | img { 25 | border-radius: 100%; 26 | } 27 | } 28 | .meta { 29 | height: 22px; 30 | color: @text-color-secondary; 31 | font-size: @font-size-base; 32 | line-height: 22px; 33 | } 34 | .action { 35 | position: absolute; 36 | top: 4px; 37 | right: 0; 38 | line-height: 1; 39 | cursor: pointer; 40 | } 41 | .total { 42 | height: 38px; 43 | margin-top: 4px; 44 | margin-bottom: 0; 45 | overflow: hidden; 46 | color: @heading-color; 47 | font-size: 30px; 48 | line-height: 38px; 49 | white-space: nowrap; 50 | text-overflow: ellipsis; 51 | word-break: break-all; 52 | } 53 | .content { 54 | position: relative; 55 | width: 100%; 56 | margin-bottom: 12px; 57 | } 58 | .contentFixed { 59 | position: absolute; 60 | bottom: 0; 61 | left: 0; 62 | width: 100%; 63 | } 64 | .footer { 65 | margin-top: 8px; 66 | padding-top: 9px; 67 | border-top: 1px solid @border-color-split; 68 | & > * { 69 | position: relative; 70 | } 71 | } 72 | .footerMargin { 73 | margin-top: 20px; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/pages/analysis/components/Charts/ChartCard/index.tsx: -------------------------------------------------------------------------------- 1 | import { Card } from 'antd'; 2 | import { CardProps } from 'antd/es/card'; 3 | import React from 'react'; 4 | import classNames from 'classnames'; 5 | import styles from './index.less'; 6 | 7 | type totalType = () => React.ReactNode; 8 | 9 | const renderTotal = (total?: number | totalType | React.ReactNode) => { 10 | if (!total) { 11 | return null; 12 | } 13 | let totalDom; 14 | switch (typeof total) { 15 | case 'undefined': 16 | totalDom = null; 17 | break; 18 | case 'function': 19 | totalDom =
{total()}
; 20 | break; 21 | default: 22 | totalDom =
{total}
; 23 | } 24 | return totalDom; 25 | }; 26 | 27 | export interface ChartCardProps extends CardProps { 28 | title: React.ReactNode; 29 | action?: React.ReactNode; 30 | total?: React.ReactNode | number | (() => React.ReactNode | number); 31 | footer?: React.ReactNode; 32 | contentHeight?: number; 33 | avatar?: React.ReactNode; 34 | style?: React.CSSProperties; 35 | } 36 | 37 | class ChartCard extends React.Component { 38 | renderContent = () => { 39 | const { contentHeight, title, avatar, action, total, footer, children, loading } = this.props; 40 | if (loading) { 41 | return false; 42 | } 43 | return ( 44 |
45 |
50 |
{avatar}
51 |
52 |
53 | {title} 54 | {action} 55 |
56 | {renderTotal(total)} 57 |
58 |
59 | {children && ( 60 |
61 |
{children}
62 |
63 | )} 64 | {footer && ( 65 |
70 | {footer} 71 |
72 | )} 73 |
74 | ); 75 | }; 76 | 77 | render() { 78 | const { 79 | loading = false, 80 | contentHeight, 81 | title, 82 | avatar, 83 | action, 84 | total, 85 | footer, 86 | children, 87 | ...rest 88 | } = this.props; 89 | return ( 90 | 91 | {this.renderContent()} 92 | 93 | ); 94 | } 95 | } 96 | 97 | export default ChartCard; 98 | -------------------------------------------------------------------------------- /src/pages/analysis/components/Charts/Field/index.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | .field { 4 | margin: 0; 5 | overflow: hidden; 6 | white-space: nowrap; 7 | text-overflow: ellipsis; 8 | .label, 9 | .number { 10 | font-size: @font-size-base; 11 | line-height: 22px; 12 | } 13 | .number { 14 | margin-left: 8px; 15 | color: @heading-color; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/pages/analysis/components/Charts/Field/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from './index.less'; 3 | 4 | export interface FieldProps { 5 | label: React.ReactNode; 6 | value: React.ReactNode; 7 | style?: React.CSSProperties; 8 | } 9 | 10 | const Field: React.FC = ({ label, value, ...rest }) => ( 11 |
12 | {label} 13 | {value} 14 |
15 | ); 16 | 17 | export default Field; 18 | -------------------------------------------------------------------------------- /src/pages/analysis/components/Charts/MiniArea/index.tsx: -------------------------------------------------------------------------------- 1 | import { Axis, Chart, Geom, Tooltip, AxisProps } from 'bizcharts'; 2 | 3 | import React from 'react'; 4 | import autoHeight from '../autoHeight'; 5 | import styles from '../index.less'; 6 | 7 | export interface MiniAreaProps { 8 | color?: string; 9 | height?: number; 10 | borderColor?: string; 11 | line?: boolean; 12 | animate?: boolean; 13 | xAxis?: AxisProps; 14 | forceFit?: boolean; 15 | scale?: { 16 | x?: { 17 | tickCount: number; 18 | }; 19 | y?: { 20 | tickCount: number; 21 | }; 22 | }; 23 | yAxis?: Partial; 24 | borderWidth?: number; 25 | data: { 26 | x: number | string; 27 | y: number; 28 | }[]; 29 | } 30 | 31 | const MiniArea: React.FC = props => { 32 | const { 33 | height = 1, 34 | data = [], 35 | forceFit = true, 36 | color = 'rgba(24, 144, 255, 0.2)', 37 | borderColor = '#1089ff', 38 | scale = { x: {}, y: {} }, 39 | borderWidth = 2, 40 | line, 41 | xAxis, 42 | yAxis, 43 | animate = true, 44 | } = props; 45 | 46 | const padding: [number, number, number, number] = [36, 5, 30, 5]; 47 | 48 | const scaleProps = { 49 | x: { 50 | type: 'cat', 51 | range: [0, 1], 52 | ...scale.x, 53 | }, 54 | y: { 55 | min: 0, 56 | ...scale.y, 57 | }, 58 | }; 59 | 60 | const tooltip: [string, (...args: any[]) => { name?: string; value: string }] = [ 61 | 'x*y', 62 | (x: string, y: string) => ({ 63 | name: x, 64 | value: y, 65 | }), 66 | ]; 67 | 68 | const chartHeight = height + 54; 69 | 70 | return ( 71 |
72 |
73 | {height > 0 && ( 74 | 82 | 91 | 100 | 101 | 111 | {line ? ( 112 | 120 | ) : ( 121 | 122 | )} 123 | 124 | )} 125 |
126 |
127 | ); 128 | }; 129 | 130 | export default autoHeight()(MiniArea); 131 | -------------------------------------------------------------------------------- /src/pages/analysis/components/Charts/MiniBar/index.tsx: -------------------------------------------------------------------------------- 1 | import { Chart, Geom, Tooltip } from 'bizcharts'; 2 | 3 | import React from 'react'; 4 | import autoHeight from '../autoHeight'; 5 | import styles from '../index.less'; 6 | 7 | export interface MiniBarProps { 8 | color?: string; 9 | height?: number; 10 | data: { 11 | x: number | string; 12 | y: number; 13 | }[]; 14 | forceFit?: boolean; 15 | style?: React.CSSProperties; 16 | } 17 | 18 | const MiniBar: React.FC = props => { 19 | const { height = 0, forceFit = true, color = '#1890FF', data = [] } = props; 20 | 21 | const scale = { 22 | x: { 23 | type: 'cat', 24 | }, 25 | y: { 26 | min: 0, 27 | }, 28 | }; 29 | 30 | const padding: [number, number, number, number] = [36, 5, 30, 5]; 31 | 32 | const tooltip: [string, (...args: any[]) => { name?: string; value: string }] = [ 33 | 'x*y', 34 | (x: string, y: string) => ({ 35 | name: x, 36 | value: y, 37 | }), 38 | ]; 39 | 40 | // for tooltip not to be hide 41 | const chartHeight = height + 54; 42 | 43 | return ( 44 |
45 |
46 | 47 | 48 | 49 | 50 |
51 |
52 | ); 53 | }; 54 | export default autoHeight()(MiniBar); 55 | -------------------------------------------------------------------------------- /src/pages/analysis/components/Charts/MiniProgress/index.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | .miniProgress { 4 | position: relative; 5 | width: 100%; 6 | padding: 5px 0; 7 | .progressWrap { 8 | position: relative; 9 | background-color: @background-color-base; 10 | } 11 | .progress { 12 | width: 0; 13 | height: 100%; 14 | background-color: @primary-color; 15 | border-radius: 1px 0 0 1px; 16 | transition: all 0.4s cubic-bezier(0.08, 0.82, 0.17, 1) 0s; 17 | } 18 | .target { 19 | position: absolute; 20 | top: 0; 21 | bottom: 0; 22 | z-index: 9; 23 | width: 20px; 24 | span { 25 | position: absolute; 26 | top: 0; 27 | left: 0; 28 | width: 2px; 29 | height: 4px; 30 | border-radius: 100px; 31 | } 32 | span:last-child { 33 | top: auto; 34 | bottom: 0; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/pages/analysis/components/Charts/MiniProgress/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Tooltip } from 'antd'; 3 | import styles from './index.less'; 4 | 5 | export interface MiniProgressProps { 6 | target: number; 7 | targetLabel?: string; 8 | color?: string; 9 | strokeWidth?: number; 10 | percent?: number; 11 | style?: React.CSSProperties; 12 | } 13 | 14 | const MiniProgress: React.FC = ({ 15 | targetLabel, 16 | target, 17 | color = 'rgb(19, 194, 194)', 18 | strokeWidth, 19 | percent, 20 | }) => ( 21 |
22 | 23 |
24 | 25 | 26 |
27 |
28 |
29 |
37 |
38 |
39 | ); 40 | 41 | export default MiniProgress; 42 | -------------------------------------------------------------------------------- /src/pages/analysis/components/Charts/Pie/index.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | .pie { 4 | position: relative; 5 | .chart { 6 | position: relative; 7 | } 8 | &.hasLegend .chart { 9 | width: ~'calc(100% - 240px)'; 10 | } 11 | .legend { 12 | position: absolute; 13 | top: 50%; 14 | right: 0; 15 | min-width: 200px; 16 | margin: 0 20px; 17 | padding: 0; 18 | list-style: none; 19 | transform: translateY(-50%); 20 | li { 21 | height: 22px; 22 | margin-bottom: 16px; 23 | line-height: 22px; 24 | cursor: pointer; 25 | &:last-child { 26 | margin-bottom: 0; 27 | } 28 | } 29 | } 30 | .dot { 31 | position: relative; 32 | top: -1px; 33 | display: inline-block; 34 | width: 8px; 35 | height: 8px; 36 | margin-right: 8px; 37 | border-radius: 8px; 38 | } 39 | .line { 40 | display: inline-block; 41 | width: 1px; 42 | height: 16px; 43 | margin-right: 8px; 44 | background-color: @border-color-split; 45 | } 46 | .legendTitle { 47 | color: @text-color; 48 | } 49 | .percent { 50 | color: @text-color-secondary; 51 | } 52 | .value { 53 | position: absolute; 54 | right: 0; 55 | } 56 | .title { 57 | margin-bottom: 8px; 58 | } 59 | .total { 60 | position: absolute; 61 | top: 50%; 62 | left: 50%; 63 | max-height: 62px; 64 | text-align: center; 65 | transform: translate(-50%, -50%); 66 | & > h4 { 67 | height: 22px; 68 | margin-bottom: 8px; 69 | color: @text-color-secondary; 70 | font-weight: normal; 71 | font-size: 14px; 72 | line-height: 22px; 73 | } 74 | & > p { 75 | display: block; 76 | height: 32px; 77 | color: @heading-color; 78 | font-size: 1.2em; 79 | line-height: 32px; 80 | white-space: nowrap; 81 | } 82 | } 83 | } 84 | 85 | .legendBlock { 86 | &.hasLegend .chart { 87 | width: 100%; 88 | margin: 0 0 32px 0; 89 | } 90 | .legend { 91 | position: relative; 92 | transform: none; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/pages/analysis/components/Charts/TagCloud/index.less: -------------------------------------------------------------------------------- 1 | .tagCloud { 2 | overflow: hidden; 3 | canvas { 4 | transform-origin: 0 0; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/pages/analysis/components/Charts/TimelineChart/index.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | .timelineChart { 4 | background: @component-background; 5 | } 6 | -------------------------------------------------------------------------------- /src/pages/analysis/components/Charts/WaterWave/index.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | .waterWave { 4 | position: relative; 5 | display: inline-block; 6 | transform-origin: left; 7 | .text { 8 | position: absolute; 9 | top: 32px; 10 | left: 0; 11 | width: 100%; 12 | text-align: center; 13 | span { 14 | color: @text-color-secondary; 15 | font-size: 14px; 16 | line-height: 22px; 17 | } 18 | h4 { 19 | color: @heading-color; 20 | font-size: 24px; 21 | line-height: 32px; 22 | } 23 | } 24 | .waterWaveCanvasWrapper { 25 | transform: scale(0.5); 26 | transform-origin: 0 0; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/pages/analysis/components/Charts/autoHeight.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export type IReactComponent

= 4 | | React.StatelessComponent

5 | | React.ComponentClass

6 | | React.ClassicComponentClass

; 7 | 8 | function computeHeight(node: HTMLDivElement) { 9 | const { style } = node; 10 | style.height = '100%'; 11 | const totalHeight = parseInt(`${getComputedStyle(node).height}`, 10); 12 | const padding = 13 | parseInt(`${getComputedStyle(node).paddingTop}`, 10) + 14 | parseInt(`${getComputedStyle(node).paddingBottom}`, 10); 15 | return totalHeight - padding; 16 | } 17 | 18 | function getAutoHeight(n: HTMLDivElement | undefined) { 19 | if (!n) { 20 | return 0; 21 | } 22 | 23 | const node = n; 24 | 25 | let height = computeHeight(node); 26 | const parentNode = node.parentNode as HTMLDivElement; 27 | if (parentNode) { 28 | height = computeHeight(parentNode); 29 | } 30 | 31 | return height; 32 | } 33 | 34 | interface AutoHeightProps { 35 | height?: number; 36 | } 37 | 38 | function autoHeight() { 39 | return

( 40 | WrappedComponent: React.ComponentClass

| React.FC

, 41 | ): React.ComponentClass

=> { 42 | class AutoHeightComponent extends React.Component

{ 43 | state = { 44 | computedHeight: 0, 45 | }; 46 | 47 | root: HTMLDivElement | undefined = undefined; 48 | 49 | componentDidMount() { 50 | const { height } = this.props; 51 | if (!height) { 52 | let h = getAutoHeight(this.root); 53 | this.setState({ computedHeight: h }); 54 | if (h < 1) { 55 | h = getAutoHeight(this.root); 56 | this.setState({ computedHeight: h }); 57 | } 58 | } 59 | } 60 | 61 | handleRoot = (node: HTMLDivElement) => { 62 | this.root = node; 63 | }; 64 | 65 | render() { 66 | const { height } = this.props; 67 | const { computedHeight } = this.state; 68 | const h = height || computedHeight; 69 | return ( 70 |

71 | {h > 0 && } 72 |
73 | ); 74 | } 75 | } 76 | return AutoHeightComponent; 77 | }; 78 | } 79 | export default autoHeight; 80 | -------------------------------------------------------------------------------- /src/pages/analysis/components/Charts/bizcharts.d.ts: -------------------------------------------------------------------------------- 1 | import * as BizChart from 'bizcharts'; 2 | 3 | export = BizChart; 4 | -------------------------------------------------------------------------------- /src/pages/analysis/components/Charts/bizcharts.tsx: -------------------------------------------------------------------------------- 1 | import * as BizChart from 'bizcharts'; 2 | 3 | export default BizChart; 4 | -------------------------------------------------------------------------------- /src/pages/analysis/components/Charts/index.less: -------------------------------------------------------------------------------- 1 | .miniChart { 2 | position: relative; 3 | width: 100%; 4 | .chartContent { 5 | position: absolute; 6 | bottom: -28px; 7 | width: 100%; 8 | > div { 9 | margin: 0 -5px; 10 | overflow: hidden; 11 | } 12 | } 13 | .chartLoading { 14 | position: absolute; 15 | top: 16px; 16 | left: 50%; 17 | margin-left: -7px; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/pages/analysis/components/Charts/index.tsx: -------------------------------------------------------------------------------- 1 | import numeral from 'numeral'; 2 | import Bar from './Bar'; 3 | import ChartCard from './ChartCard'; 4 | import Field from './Field'; 5 | import Gauge from './Gauge'; 6 | import MiniArea from './MiniArea'; 7 | import MiniBar from './MiniBar'; 8 | import MiniProgress from './MiniProgress'; 9 | import Pie from './Pie'; 10 | import TagCloud from './TagCloud'; 11 | import TimelineChart from './TimelineChart'; 12 | import WaterWave from './WaterWave'; 13 | 14 | const yuan = (val: number | string) => `¥ ${numeral(val).format('0,0')}`; 15 | 16 | const Charts = { 17 | yuan, 18 | Bar, 19 | Pie, 20 | Gauge, 21 | MiniBar, 22 | MiniArea, 23 | MiniProgress, 24 | ChartCard, 25 | Field, 26 | WaterWave, 27 | TagCloud, 28 | TimelineChart, 29 | }; 30 | 31 | export { 32 | Charts as default, 33 | yuan, 34 | Bar, 35 | Pie, 36 | Gauge, 37 | MiniBar, 38 | MiniArea, 39 | MiniProgress, 40 | ChartCard, 41 | Field, 42 | WaterWave, 43 | TagCloud, 44 | TimelineChart, 45 | }; 46 | -------------------------------------------------------------------------------- /src/pages/analysis/components/NumberInfo/index.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | .numberInfo { 4 | .suffix { 5 | margin-left: 4px; 6 | color: @text-color; 7 | font-size: 16px; 8 | font-style: normal; 9 | } 10 | .numberInfoTitle { 11 | margin-bottom: 16px; 12 | color: @text-color; 13 | font-size: @font-size-lg; 14 | transition: all 0.3s; 15 | } 16 | .numberInfoSubTitle { 17 | height: 22px; 18 | overflow: hidden; 19 | color: @text-color-secondary; 20 | font-size: @font-size-base; 21 | line-height: 22px; 22 | white-space: nowrap; 23 | text-overflow: ellipsis; 24 | word-break: break-all; 25 | } 26 | .numberInfoValue { 27 | margin-top: 4px; 28 | overflow: hidden; 29 | font-size: 0; 30 | white-space: nowrap; 31 | text-overflow: ellipsis; 32 | word-break: break-all; 33 | & > span { 34 | display: inline-block; 35 | height: 32px; 36 | margin-right: 32px; 37 | color: @heading-color; 38 | font-size: 24px; 39 | line-height: 32px; 40 | } 41 | .subTotal { 42 | margin-right: 0; 43 | color: @text-color-secondary; 44 | font-size: @font-size-lg; 45 | vertical-align: top; 46 | i { 47 | margin-left: 4px; 48 | font-size: 12px; 49 | transform: scale(0.82); 50 | } 51 | :global { 52 | .anticon-caret-up { 53 | color: @red-6; 54 | } 55 | .anticon-caret-down { 56 | color: @green-6; 57 | } 58 | } 59 | } 60 | } 61 | } 62 | .numberInfolight { 63 | .numberInfoValue { 64 | & > span { 65 | color: @text-color; 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/pages/analysis/components/NumberInfo/index.tsx: -------------------------------------------------------------------------------- 1 | import { Icon } from 'antd'; 2 | import React from 'react'; 3 | import classNames from 'classnames'; 4 | import styles from './index.less'; 5 | 6 | export interface NumberInfoProps { 7 | title?: React.ReactNode | string; 8 | subTitle?: React.ReactNode | string; 9 | total?: React.ReactNode | string; 10 | status?: 'up' | 'down'; 11 | theme?: string; 12 | gap?: number; 13 | subTotal?: number; 14 | suffix?: string; 15 | style?: React.CSSProperties; 16 | } 17 | const NumberInfo: React.FC = ({ 18 | theme, 19 | title, 20 | subTitle, 21 | total, 22 | subTotal, 23 | status, 24 | suffix, 25 | gap, 26 | ...rest 27 | }) => ( 28 |
34 | {title && ( 35 |
36 | {title} 37 |
38 | )} 39 | {subTitle && ( 40 |
44 | {subTitle} 45 |
46 | )} 47 |
48 | 49 | {total} 50 | {suffix && {suffix}} 51 | 52 | {(status || subTotal) && ( 53 | 54 | {subTotal} 55 | {status && } 56 | 57 | )} 58 |
59 |
60 | ); 61 | 62 | export default NumberInfo; 63 | -------------------------------------------------------------------------------- /src/pages/analysis/components/OfflineData.tsx: -------------------------------------------------------------------------------- 1 | import { Card, Col, Row, Tabs } from 'antd'; 2 | import { FormattedMessage, formatMessage } from 'umi-plugin-react/locale'; 3 | import React from 'react'; 4 | import { OfflineChartData, OfflineDataType } from '../data.d'; 5 | 6 | import { TimelineChart, Pie } from './Charts'; 7 | import NumberInfo from './NumberInfo'; 8 | import styles from '../style.less'; 9 | 10 | const CustomTab = ({ 11 | data, 12 | currentTabKey: currentKey, 13 | }: { 14 | data: OfflineDataType; 15 | currentTabKey: string; 16 | }) => ( 17 | 18 | 19 | 26 | } 27 | gap={2} 28 | total={`${data.cvr * 100}%`} 29 | theme={currentKey !== data.name ? 'light' : undefined} 30 | /> 31 | 32 | 33 | 41 | 42 | 43 | ); 44 | 45 | const { TabPane } = Tabs; 46 | 47 | const OfflineData = ({ 48 | activeKey, 49 | loading, 50 | offlineData, 51 | offlineChartData, 52 | handleTabChange, 53 | }: { 54 | activeKey: string; 55 | loading: boolean; 56 | offlineData: OfflineDataType[]; 57 | offlineChartData: OfflineChartData[]; 58 | handleTabChange: (activeKey: string) => void; 59 | }) => ( 60 | 61 | 62 | {offlineData.map(shop => ( 63 | } key={shop.name}> 64 |
65 | 73 |
74 |
75 | ))} 76 |
77 |
78 | ); 79 | 80 | export default OfflineData; 81 | -------------------------------------------------------------------------------- /src/pages/analysis/components/PageLoading/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Spin } from 'antd'; 3 | 4 | // loading components from code split 5 | // https://umijs.org/plugin/umi-plugin-react.html#dynamicimport 6 | export default () => ( 7 |
8 | 9 |
10 | ); 11 | -------------------------------------------------------------------------------- /src/pages/analysis/components/ProportionSales.tsx: -------------------------------------------------------------------------------- 1 | import { Card, Radio } from 'antd'; 2 | 3 | import { FormattedMessage } from 'umi-plugin-react/locale'; 4 | import { RadioChangeEvent } from 'antd/es/radio'; 5 | import React from 'react'; 6 | import { VisitDataType } from '../data.d'; 7 | import { Pie } from './Charts'; 8 | import Yuan from '../utils/Yuan'; 9 | import styles from '../style.less'; 10 | 11 | const ProportionSales = ({ 12 | dropdownGroup, 13 | salesType, 14 | loading, 15 | salesPieData, 16 | handleChangeSalesType, 17 | }: { 18 | loading: boolean; 19 | dropdownGroup: React.ReactNode; 20 | salesType: 'all' | 'online' | 'stores'; 21 | salesPieData: VisitDataType[]; 22 | handleChangeSalesType?: (e: RadioChangeEvent) => void; 23 | }) => ( 24 | 33 | } 34 | style={{ 35 | height: '100%', 36 | }} 37 | extra={ 38 |
39 | {dropdownGroup} 40 |
41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 |
53 |
54 | } 55 | > 56 |
57 |

58 | 59 |

60 | } 63 | total={() => {salesPieData.reduce((pre, now) => now.y + pre, 0)}} 64 | data={salesPieData} 65 | valueFormat={value => {value}} 66 | height={248} 67 | lineWidth={4} 68 | /> 69 |
70 |
71 | ); 72 | 73 | export default ProportionSales; 74 | -------------------------------------------------------------------------------- /src/pages/analysis/components/Trend/index.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | .trendItem { 4 | display: inline-block; 5 | font-size: @font-size-base; 6 | line-height: 22px; 7 | 8 | .up, 9 | .down { 10 | position: relative; 11 | top: 1px; 12 | margin-left: 4px; 13 | i { 14 | font-size: 12px; 15 | transform: scale(0.83); 16 | } 17 | } 18 | .up { 19 | color: @red-6; 20 | } 21 | .down { 22 | top: -1px; 23 | color: @green-6; 24 | } 25 | 26 | &.trendItemGrey .up, 27 | &.trendItemGrey .down { 28 | color: @text-color; 29 | } 30 | 31 | &.reverseColor .up { 32 | color: @green-6; 33 | } 34 | &.reverseColor .down { 35 | color: @red-6; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/pages/analysis/components/Trend/index.tsx: -------------------------------------------------------------------------------- 1 | import { Icon } from 'antd'; 2 | import React from 'react'; 3 | import classNames from 'classnames'; 4 | import styles from './index.less'; 5 | 6 | export interface TrendProps { 7 | colorful?: boolean; 8 | flag: 'up' | 'down'; 9 | style?: React.CSSProperties; 10 | reverseColor?: boolean; 11 | className?: string; 12 | } 13 | 14 | const Trend: React.FC = ({ 15 | colorful = true, 16 | reverseColor = false, 17 | flag, 18 | children, 19 | className, 20 | ...rest 21 | }) => { 22 | const classString = classNames( 23 | styles.trendItem, 24 | { 25 | [styles.trendItemGrey]: !colorful, 26 | [styles.reverseColor]: reverseColor && colorful, 27 | }, 28 | className, 29 | ); 30 | return ( 31 |
32 | {children} 33 | {flag && ( 34 | 35 | 36 | 37 | )} 38 |
39 | ); 40 | }; 41 | 42 | export default Trend; 43 | -------------------------------------------------------------------------------- /src/pages/analysis/data.d.ts: -------------------------------------------------------------------------------- 1 | export interface VisitDataType { 2 | x: string; 3 | y: number; 4 | } 5 | 6 | export interface SearchDataType { 7 | index: number; 8 | keyword: string; 9 | count: number; 10 | range: number; 11 | status: number; 12 | } 13 | 14 | export interface OfflineDataType { 15 | name: string; 16 | cvr: number; 17 | } 18 | 19 | export interface OfflineChartData { 20 | x: any; 21 | y1: number; 22 | y2: number; 23 | } 24 | 25 | export interface RadarData { 26 | name: string; 27 | label: string; 28 | value: number; 29 | } 30 | 31 | export interface AnalysisData { 32 | visitData: VisitDataType[]; 33 | visitData2: VisitDataType[]; 34 | salesData: VisitDataType[]; 35 | searchData: SearchDataType[]; 36 | offlineData: OfflineDataType[]; 37 | offlineChartData: OfflineChartData[]; 38 | salesTypeData: VisitDataType[]; 39 | salesTypeDataOnline: VisitDataType[]; 40 | salesTypeDataOffline: VisitDataType[]; 41 | radarData: RadarData[]; 42 | } 43 | -------------------------------------------------------------------------------- /src/pages/analysis/locales/en-US.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'analysis.analysis.test': 'Gongzhuan No.{no} shop', 3 | 'analysis.analysis.introduce': 'Introduce', 4 | 'analysis.analysis.total-sales': 'Total Sales', 5 | 'analysis.analysis.day-sales': 'Daily Sales', 6 | 'analysis.analysis.visits': 'Visits', 7 | 'analysis.analysis.visits-trend': 'Visits Trend', 8 | 'analysis.analysis.visits-ranking': 'Visits Ranking', 9 | 'analysis.analysis.day-visits': 'Daily Visits', 10 | 'analysis.analysis.week': 'WoW Change', 11 | 'analysis.analysis.day': 'DoD Change', 12 | 'analysis.analysis.payments': 'Payments', 13 | 'analysis.analysis.conversion-rate': 'Conversion Rate', 14 | 'analysis.analysis.operational-effect': 'Operational Effect', 15 | 'analysis.analysis.sales-trend': 'Stores Sales Trend', 16 | 'analysis.analysis.sales-ranking': 'Sales Ranking', 17 | 'analysis.analysis.all-year': 'All Year', 18 | 'analysis.analysis.all-month': 'All Month', 19 | 'analysis.analysis.all-week': 'All Week', 20 | 'analysis.analysis.all-day': 'All day', 21 | 'analysis.analysis.search-users': 'Search Users', 22 | 'analysis.analysis.per-capita-search': 'Per Capita Search', 23 | 'analysis.analysis.online-top-search': 'Online Top Search', 24 | 'analysis.analysis.the-proportion-of-sales': 'The Proportion Of Sales', 25 | 'analysis.channel.all': 'ALL', 26 | 'analysis.channel.online': 'Online', 27 | 'analysis.channel.stores': 'Stores', 28 | 'analysis.analysis.sales': 'Sales', 29 | 'analysis.analysis.traffic': 'Traffic', 30 | 'analysis.table.rank': 'Rank', 31 | 'analysis.table.search-keyword': 'Keyword', 32 | 'analysis.table.users': 'Users', 33 | 'analysis.table.weekly-range': 'Weekly Range', 34 | }; 35 | -------------------------------------------------------------------------------- /src/pages/analysis/locales/pt-BR.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'analysis.analysis.test': 'Gongzhuan No.{no} shop', 3 | 'analysis.analysis.introduce': 'Introduzir', 4 | 'analysis.analysis.total-sales': 'Vendas Totais', 5 | 'analysis.analysis.day-sales': 'Vendas do Dia', 6 | 'analysis.analysis.visits': 'Visitas', 7 | 'analysis.analysis.visits-trend': 'Tendência de Visitas', 8 | 'analysis.analysis.visits-ranking': 'Ranking de Visitas', 9 | 'analysis.analysis.day-visits': 'Visitas do Dia', 10 | 'analysis.analysis.week': 'Taxa Semanal', 11 | 'analysis.analysis.day': 'Taxa Diária', 12 | 'analysis.analysis.payments': 'Pagamentos', 13 | 'analysis.analysis.conversion-rate': 'Taxa de Conversão', 14 | 'analysis.analysis.operational-effect': 'Efeito Operacional', 15 | 'analysis.analysis.sales-trend': 'Tendência de Vendas das Lojas', 16 | 'analysis.analysis.sales-ranking': 'Ranking de Vendas', 17 | 'analysis.$2': 'Todo ano', 18 | 'analysis.analysis.all-month': 'Todo mês', 19 | 'analysis.analysis.all-week': 'Toda semana', 20 | 'analysis.analysis.all-day': 'Todo dia', 21 | 'analysis.analysis.search-users': 'Pesquisa de Usuários', 22 | 'analysis.analysis.per-capita-search': 'Busca Per Capta', 23 | 'analysis.analysis.online-top-search': 'Mais Buscadas Online', 24 | 'analysis.analysis.the-proportion-of-sales': 'The Proportion Of Sales', 25 | 'analysis.channel.all': 'Tudo', 26 | 'analysis.channel.online': 'Online', 27 | 'analysis.channel.stores': 'Lojas', 28 | 'analysis.analysis.sales': 'Vendas', 29 | 'analysis.analysis.traffic': 'Tráfego', 30 | 'analysis.table.rank': 'Rank', 31 | 'analysis.table.search-keyword': 'Palavra chave', 32 | 'analysis.table.users': 'Usuários', 33 | 'analysis.table.weekly-range': 'Faixa Semanal', 34 | }; 35 | -------------------------------------------------------------------------------- /src/pages/analysis/locales/zh-CN.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'analysis.analysis.test': '工专路 {no} 号店', 3 | 'analysis.analysis.introduce': '指标说明', 4 | 'analysis.analysis.total-sales': '总销售额', 5 | 'analysis.analysis.day-sales': '日销售额', 6 | 'analysis.analysis.visits': '访问量', 7 | 'analysis.analysis.visits-trend': '访问量趋势', 8 | 'analysis.analysis.visits-ranking': '门店访问量排名', 9 | 'analysis.analysis.day-visits': '日访问量', 10 | 'analysis.analysis.week': '周同比', 11 | 'analysis.analysis.day': '日同比', 12 | 'analysis.analysis.payments': '支付笔数', 13 | 'analysis.analysis.conversion-rate': '转化率', 14 | 'analysis.analysis.operational-effect': '运营活动效果', 15 | 'analysis.analysis.sales-trend': '销售趋势', 16 | 'analysis.analysis.sales-ranking': '门店销售额排名', 17 | 'analysis.analysis.all-year': '全年', 18 | 'analysis.analysis.all-month': '本月', 19 | 'analysis.analysis.all-week': '本周', 20 | 'analysis.analysis.all-day': '今日', 21 | 'analysis.analysis.search-users': '搜索用户数', 22 | 'analysis.analysis.per-capita-search': '人均搜索次数', 23 | 'analysis.analysis.online-top-search': '线上热门搜索', 24 | 'analysis.analysis.the-proportion-of-sales': '销售额类别占比', 25 | 'analysis.channel.all': '全部渠道', 26 | 'analysis.channel.online': '线上', 27 | 'analysis.channel.stores': '门店', 28 | 'analysis.analysis.sales': '销售额', 29 | 'analysis.analysis.traffic': '客流量', 30 | 'analysis.table.rank': '排名', 31 | 'analysis.table.search-keyword': '搜索关键词', 32 | 'analysis.table.users': '用户数', 33 | 'analysis.table.weekly-range': '周涨幅', 34 | }; 35 | -------------------------------------------------------------------------------- /src/pages/analysis/locales/zh-TW.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'analysis.analysis.test': '工專路 {no} 號店', 3 | 'analysis.analysis.introduce': '指標說明', 4 | 'analysis.analysis.total-sales': '總銷售額', 5 | 'analysis.analysis.day-sales': '日銷售額', 6 | 'analysis.analysis.visits': '訪問量', 7 | 'analysis.analysis.visits-trend': '訪問量趨勢', 8 | 'analysis.analysis.visits-ranking': '門店訪問量排名', 9 | 'analysis.analysis.day-visits': '日訪問量', 10 | 'analysis.analysis.week': '周同比', 11 | 'analysis.analysis.day': '日同比', 12 | 'analysis.analysis.payments': '支付筆數', 13 | 'analysis.analysis.conversion-rate': '轉化率', 14 | 'analysis.analysis.operational-effect': '運營活動效果', 15 | 'analysis.analysis.sales-trend': '銷售趨勢', 16 | 'analysis.analysis.sales-ranking': '門店銷售額排名', 17 | 'analysis.analysis.all-year': '全年', 18 | 'analysis.analysis.all-month': '本月', 19 | 'analysis.analysis.all-week': '本周', 20 | 'analysis.analysis.all-day': '今日', 21 | 'analysis.analysis.search-users': '搜索用戶數', 22 | 'analysis.analysis.per-capita-search': '人均搜索次數', 23 | 'analysis.analysis.online-top-search': '線上熱門搜索', 24 | 'analysis.analysis.the-proportion-of-sales': '銷售額類別占比', 25 | 'analysis.channel.all': '全部渠道', 26 | 'analysis.channel.online': '線上', 27 | 'analysis.channel.stores': '門店', 28 | 'analysis.analysis.sales': '銷售額', 29 | 'analysis.analysis.traffic': '客流量', 30 | 'analysis.table.rank': '排名', 31 | 'analysis.table.search-keyword': '搜索關鍵詞', 32 | 'analysis.table.users': '用戶數', 33 | 'analysis.table.weekly-range': '周漲幅', 34 | }; 35 | -------------------------------------------------------------------------------- /src/pages/analysis/models/analysis.ts: -------------------------------------------------------------------------------- 1 | import { AnyAction, Reducer } from 'redux'; 2 | 3 | import { EffectsCommandMap } from 'dva'; 4 | import { AnalysisData } from '../data'; 5 | import { fakeChartData } from '../service'; 6 | 7 | export type Effect = ( 8 | action: AnyAction, 9 | effects: EffectsCommandMap & { select: (func: (state: AnalysisData) => T) => T }, 10 | ) => void; 11 | 12 | export interface AnalysisType { 13 | namespace: String; 14 | state: AnalysisData; 15 | effects: { 16 | fetch: Effect; 17 | fetchSalesData: Effect; 18 | }; 19 | reducers: { 20 | save: Reducer; 21 | clear: Reducer; 22 | }; 23 | } 24 | 25 | const initState = { 26 | visitData: [], 27 | visitData2: [], 28 | salesData: [], 29 | searchData: [], 30 | offlineData: [], 31 | offlineChartData: [], 32 | salesTypeData: [], 33 | salesTypeDataOnline: [], 34 | salesTypeDataOffline: [], 35 | radarData: [], 36 | }; 37 | 38 | const Analysis: AnalysisType = { 39 | namespace: 'analysis', 40 | 41 | state: initState, 42 | 43 | effects: { 44 | *fetch(_, { call, put }) { 45 | const response = yield call(fakeChartData); 46 | yield put({ 47 | type: 'save', 48 | payload: response, 49 | }); 50 | }, 51 | *fetchSalesData(_, { call, put }) { 52 | const response = yield call(fakeChartData); 53 | yield put({ 54 | type: 'save', 55 | payload: { 56 | salesData: response.salesData, 57 | }, 58 | }); 59 | }, 60 | }, 61 | 62 | reducers: { 63 | save(state, { payload }) { 64 | return { 65 | ...state, 66 | ...payload, 67 | }; 68 | }, 69 | clear() { 70 | return initState; 71 | }, 72 | }, 73 | }; 74 | 75 | export default Analysis; 76 | -------------------------------------------------------------------------------- /src/pages/analysis/service.tsx: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request'; 2 | 3 | export async function fakeChartData() { 4 | return request('/api/fake_chart_data'); 5 | } 6 | -------------------------------------------------------------------------------- /src/pages/analysis/utils/Yuan.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { yuan } from '../components/Charts'; 3 | /** 4 | * 减少使用 dangerouslySetInnerHTML 5 | */ 6 | export default class Yuan extends React.Component<{ 7 | children: React.ReactText; 8 | }> { 9 | main: HTMLSpanElement | undefined | null = null; 10 | 11 | componentDidMount() { 12 | this.renderToHtml(); 13 | } 14 | 15 | componentDidUpdate() { 16 | this.renderToHtml(); 17 | } 18 | 19 | renderToHtml = () => { 20 | const { children } = this.props; 21 | if (this.main) { 22 | this.main.innerHTML = yuan(children); 23 | } 24 | }; 25 | 26 | render() { 27 | return ( 28 | { 30 | this.main = ref; 31 | }} 32 | /> 33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/pages/analysis/utils/utils.less: -------------------------------------------------------------------------------- 1 | .textOverflow() { 2 | overflow: hidden; 3 | white-space: nowrap; 4 | text-overflow: ellipsis; 5 | word-break: break-all; 6 | } 7 | 8 | .textOverflowMulti(@line: 3, @bg: #fff) { 9 | position: relative; 10 | max-height: @line * 1.5em; 11 | margin-right: -1em; 12 | padding-right: 1em; 13 | overflow: hidden; 14 | line-height: 1.5em; 15 | text-align: justify; 16 | &::before { 17 | position: absolute; 18 | right: 14px; 19 | bottom: 0; 20 | padding: 0 1px; 21 | background: @bg; 22 | content: '...'; 23 | } 24 | &::after { 25 | position: absolute; 26 | right: 14px; 27 | width: 1em; 28 | height: 1em; 29 | margin-top: 0.2em; 30 | background: white; 31 | content: ''; 32 | } 33 | } 34 | 35 | // mixins for clearfix 36 | // ------------------------ 37 | .clearfix() { 38 | zoom: 1; 39 | &::before, 40 | &::after { 41 | display: table; 42 | content: ' '; 43 | } 44 | &::after { 45 | clear: both; 46 | height: 0; 47 | font-size: 0; 48 | visibility: hidden; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/pages/analysis/utils/utils.ts: -------------------------------------------------------------------------------- 1 | import { RangePickerValue } from 'antd/es/date-picker/interface'; 2 | import moment from 'moment'; 3 | 4 | export function fixedZero(val: number) { 5 | return val * 1 < 10 ? `0${val}` : val; 6 | } 7 | 8 | export function getTimeDistance(type: 'today' | 'week' | 'month' | 'year'): RangePickerValue { 9 | const now = new Date(); 10 | const oneDay = 1000 * 60 * 60 * 24; 11 | 12 | if (type === 'today') { 13 | now.setHours(0); 14 | now.setMinutes(0); 15 | now.setSeconds(0); 16 | return [moment(now), moment(now.getTime() + (oneDay - 1000))]; 17 | } 18 | 19 | if (type === 'week') { 20 | let day = now.getDay(); 21 | now.setHours(0); 22 | now.setMinutes(0); 23 | now.setSeconds(0); 24 | 25 | if (day === 0) { 26 | day = 6; 27 | } else { 28 | day -= 1; 29 | } 30 | 31 | const beginTime = now.getTime() - day * oneDay; 32 | 33 | return [moment(beginTime), moment(beginTime + (7 * oneDay - 1000))]; 34 | } 35 | const year = now.getFullYear(); 36 | 37 | if (type === 'month') { 38 | const month = now.getMonth(); 39 | const nextDate = moment(now).add(1, 'months'); 40 | const nextYear = nextDate.year(); 41 | const nextMonth = nextDate.month(); 42 | 43 | return [ 44 | moment(`${year}-${fixedZero(month + 1)}-01 00:00:00`), 45 | moment(moment(`${nextYear}-${fixedZero(nextMonth + 1)}-01 00:00:00`).valueOf() - 1000), 46 | ]; 47 | } 48 | 49 | return [moment(`${year}-01-01 00:00:00`), moment(`${year}-12-31 23:59:59`)]; 50 | } 51 | -------------------------------------------------------------------------------- /src/pages/configuration/jobList/components/StandardTable/index.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | .standardTable { 4 | :global { 5 | .ant-table-pagination { 6 | margin-top: 24px; 7 | } 8 | } 9 | 10 | .tableAlert { 11 | margin-bottom: 16px; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/pages/configuration/jobList/components/StandardTable/index.tsx: -------------------------------------------------------------------------------- 1 | import {Table } from 'antd'; 2 | import { ColumnProps, TableProps } from 'antd/es/table'; 3 | import React, { Component} from 'react'; 4 | 5 | import { TableListItem, TableListPagination } from '../../data'; 6 | import styles from './index.less'; 7 | 8 | type Omit = Pick>; 9 | 10 | export interface StandardTableProps extends Omit, 'columns'> { 11 | columns: ColumnProps[]; 12 | data: { 13 | content: TableListItem[]; 14 | pageable: Partial; 15 | totalElements:number; 16 | }; 17 | } 18 | class StandardTable extends Component> { 19 | 20 | handleTableChange: TableProps['onChange'] = ( 21 | pagination, 22 | ...rest 23 | ) => { 24 | const { onChange } = this.props; 25 | if (onChange) { 26 | onChange(pagination, ...rest); 27 | } 28 | }; 29 | 30 | render() { 31 | const { data, ...rest } = this.props; 32 | const { content = [], pageable = false, totalElements = 0 } = data || {}; 33 | 34 | const paginationProps = pageable 35 | ? { 36 | showSizeChanger: true, 37 | showQuickJumper: true, 38 | total: totalElements, 39 | showTotal: ((total: number) => { 40 | return `共 ${total} 条`; 41 | }), 42 | current: pageable.pageNumber ? pageable.pageNumber + 1 : 1, 43 | pageSize: pageable.pageSize, 44 | } 45 | : false; 46 | 47 | return ( 48 |
49 | row.id+""} 51 | dataSource={content} 52 | pagination={paginationProps} 53 | onChange={this.handleTableChange} 54 | {...rest} 55 | /> 56 | 57 | ); 58 | } 59 | } 60 | 61 | export default StandardTable; 62 | -------------------------------------------------------------------------------- /src/pages/configuration/jobList/components/style.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | .card { 4 | margin-bottom: 24px; 5 | } 6 | 7 | .heading { 8 | margin: 0 0 16px 0; 9 | font-size: 14px; 10 | line-height: 22px; 11 | } 12 | 13 | .steps:global(.ant-steps) { 14 | max-width: 750px; 15 | margin: 16px auto; 16 | } 17 | 18 | .errorIcon { 19 | margin-right: 24px; 20 | color: @error-color; 21 | cursor: pointer; 22 | i { 23 | margin-right: 4px; 24 | } 25 | } 26 | 27 | .errorPopover { 28 | :global { 29 | .ant-popover-inner-content { 30 | min-width: 256px; 31 | max-height: 290px; 32 | padding: 0; 33 | overflow: auto; 34 | } 35 | } 36 | } 37 | 38 | .errorListItem { 39 | padding: 8px 16px; 40 | list-style: none; 41 | border-bottom: 1px solid @border-color-split; 42 | cursor: pointer; 43 | transition: all 0.3s; 44 | &:hover { 45 | background: @primary-1; 46 | } 47 | &:last-child { 48 | border: 0; 49 | } 50 | .errorIcon { 51 | float: left; 52 | margin-top: 4px; 53 | margin-right: 12px; 54 | padding-bottom: 22px; 55 | color: @error-color; 56 | } 57 | .errorField { 58 | margin-top: 2px; 59 | color: @text-color-secondary; 60 | font-size: 12px; 61 | } 62 | } 63 | 64 | .editable { 65 | td { 66 | padding-top: 13px !important; 67 | padding-bottom: 12.5px !important; 68 | } 69 | } 70 | 71 | // custom footer for fixed footer toolbar 72 | .advancedForm + div { 73 | padding-bottom: 64px; 74 | } 75 | 76 | .advancedForm { 77 | :global { 78 | .ant-form .ant-row:last-child .ant-form-item { 79 | margin-bottom: 24px; 80 | } 81 | .ant-table td { 82 | transition: none !important; 83 | } 84 | } 85 | } 86 | 87 | .optional { 88 | color: @text-color-secondary; 89 | font-style: normal; 90 | } 91 | -------------------------------------------------------------------------------- /src/pages/configuration/jobList/data.d.ts: -------------------------------------------------------------------------------- 1 | export interface TableListItem { 2 | id: number; 3 | name: string; 4 | type: number; 5 | estimatedTime: number; 6 | status: number; 7 | triggerName: string; 8 | springJobName: string; 9 | jobDesc: string; 10 | params:{ [key: string]: string }; 11 | callbachUrl:string; 12 | updatedAt: Date; 13 | createdAt: Date; 14 | } 15 | 16 | export interface TableListPagination { 17 | pageSize: number; 18 | pageNumber: number; 19 | current:number; 20 | } 21 | 22 | export interface TableListData { 23 | content: TableListItem[]; 24 | pageable: Partial; 25 | totalElements:number; 26 | } 27 | 28 | export interface TableListParams { 29 | status: number; 30 | name: string; 31 | type:number; 32 | triggerName:string; 33 | springJobName:string; 34 | pageSize: number; 35 | currentPage: number; 36 | } 37 | -------------------------------------------------------------------------------- /src/pages/configuration/jobList/models/jobList.ts: -------------------------------------------------------------------------------- 1 | import { AnyAction, Reducer } from 'redux'; 2 | import { EffectsCommandMap } from 'dva'; 3 | import { queryJob,saveJob,removeJob,toggleStatus, launch} from '../service'; 4 | 5 | import { TableListData } from '../data'; 6 | 7 | export interface StateType { 8 | data: TableListData; 9 | } 10 | 11 | export type Effect = ( 12 | action: AnyAction, 13 | effects: EffectsCommandMap & { select: (func: (state: StateType) => T) => T }, 14 | ) => void; 15 | 16 | export interface JobListModelType { 17 | namespace: string; 18 | state: StateType; 19 | effects: { 20 | fetch: Effect; 21 | saveJob: Effect; 22 | remove: Effect; 23 | toggleStatus: Effect; 24 | launch: Effect; 25 | }; 26 | reducers: { 27 | save: Reducer; 28 | }; 29 | } 30 | 31 | const JobListModel: JobListModelType = { 32 | namespace: 'jobList', 33 | 34 | state: { 35 | data: { 36 | content: [], 37 | pageable: {}, 38 | totalElements:0, 39 | }, 40 | }, 41 | 42 | effects: { 43 | *fetch({ payload }, { call, put }) { 44 | const response = yield call(queryJob, payload); 45 | yield put({ 46 | type: 'save', 47 | payload: response, 48 | }); 49 | }, 50 | *saveJob({ payload, callback }, { call, put }) { 51 | yield call(saveJob, payload); 52 | const response = yield call(queryJob); 53 | yield put({ 54 | type: 'save', 55 | payload: response, 56 | }); 57 | if (callback) callback(); 58 | }, 59 | *remove({ payload, callback }, { call, put }) { 60 | yield call(removeJob, payload); 61 | const response = yield call(queryJob); 62 | yield put({ 63 | type: 'save', 64 | payload: response, 65 | }); 66 | if (callback) callback(); 67 | }, 68 | *launch({ payload, callback }, { call, put }) { 69 | yield call(launch, payload); 70 | if (callback) callback(); 71 | }, 72 | *toggleStatus({payload, callback},{call, put}){ 73 | yield call(toggleStatus, payload); 74 | const response = yield call(queryJob); 75 | yield put({ 76 | type: 'save', 77 | payload: response, 78 | }); 79 | if (callback) callback(); 80 | } 81 | }, 82 | 83 | reducers: { 84 | save(state, action) { 85 | return { 86 | ...state, 87 | data: action.payload, 88 | }; 89 | }, 90 | }, 91 | }; 92 | 93 | export default JobListModel; 94 | -------------------------------------------------------------------------------- /src/pages/configuration/jobList/service.ts: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request'; 2 | import { TableListParams } from './data'; 3 | 4 | export async function queryJob(params: TableListParams) { 5 | return request('/job/load', { 6 | params, 7 | }); 8 | } 9 | 10 | export async function removeJob(params: TableListParams) { 11 | return request('/job/delete', { 12 | method: 'POST', 13 | params, 14 | }); 15 | } 16 | 17 | export async function launch(params: TableListParams) { 18 | return request('/job/launch', { 19 | method: 'POST', 20 | params, 21 | }); 22 | } 23 | 24 | export async function saveJob(params: TableListParams) { 25 | return request('/job/save', { 26 | method: 'POST', 27 | data: { 28 | ...params, 29 | }, 30 | }); 31 | } 32 | 33 | export async function toggleStatus(params: TableListParams) { 34 | return request('/job/toggleStatus', { 35 | method: 'POST', 36 | params, 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /src/pages/configuration/jobList/style.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | @import './utils/utils.less'; 3 | 4 | .tableList { 5 | .tableListOperator { 6 | margin-bottom: 16px; 7 | button { 8 | margin-right: 8px; 9 | } 10 | } 11 | } 12 | 13 | .tableListForm { 14 | :global { 15 | .ant-form-item { 16 | display: flex; 17 | margin-right: 0; 18 | margin-bottom: 24px; 19 | > .ant-form-item-label { 20 | width: auto; 21 | padding-right: 8px; 22 | line-height: 32px; 23 | } 24 | .ant-form-item-control { 25 | line-height: 32px; 26 | } 27 | } 28 | .ant-form-item-control-wrapper { 29 | flex: 1; 30 | } 31 | } 32 | .submitButtons { 33 | display: block; 34 | margin-bottom: 24px; 35 | white-space: nowrap; 36 | } 37 | } 38 | 39 | @media screen and (max-width: @screen-lg) { 40 | .tableListForm :global(.ant-form-item) { 41 | margin-right: 24px; 42 | } 43 | } 44 | 45 | @media screen and (max-width: @screen-md) { 46 | .tableListForm :global(.ant-form-item) { 47 | margin-right: 8px; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/pages/configuration/jobList/utils/utils.less: -------------------------------------------------------------------------------- 1 | .textOverflow() { 2 | overflow: hidden; 3 | white-space: nowrap; 4 | text-overflow: ellipsis; 5 | word-break: break-all; 6 | } 7 | 8 | .textOverflowMulti(@line: 3, @bg: #fff) { 9 | position: relative; 10 | max-height: @line * 1.5em; 11 | margin-right: -1em; 12 | padding-right: 1em; 13 | overflow: hidden; 14 | line-height: 1.5em; 15 | text-align: justify; 16 | &::before { 17 | position: absolute; 18 | right: 14px; 19 | bottom: 0; 20 | padding: 0 1px; 21 | background: @bg; 22 | content: '...'; 23 | } 24 | &::after { 25 | position: absolute; 26 | right: 14px; 27 | width: 1em; 28 | height: 1em; 29 | margin-top: 0.2em; 30 | background: white; 31 | content: ''; 32 | } 33 | } 34 | 35 | // mixins for clearfix 36 | // ------------------------ 37 | .clearfix() { 38 | zoom: 1; 39 | &::before, 40 | &::after { 41 | display: table; 42 | content: ' '; 43 | } 44 | &::after { 45 | clear: both; 46 | height: 0; 47 | font-size: 0; 48 | visibility: hidden; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/pages/configuration/triggerList/components/StandardTable/index.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | .standardTable { 4 | :global { 5 | .ant-table-pagination { 6 | margin-top: 24px; 7 | } 8 | } 9 | 10 | .tableAlert { 11 | margin-bottom: 16px; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/pages/configuration/triggerList/components/StandardTable/index.tsx: -------------------------------------------------------------------------------- 1 | import {Table } from 'antd'; 2 | import { ColumnProps, TableProps } from 'antd/es/table'; 3 | import React, { Component} from 'react'; 4 | 5 | import { TableListItem, TableListPagination } from '../../data'; 6 | import styles from './index.less'; 7 | 8 | type Omit = Pick>; 9 | 10 | export interface StandardTableProps extends Omit, 'columns'> { 11 | columns: ColumnProps[]; 12 | data: { 13 | content: TableListItem[]; 14 | pageable: Partial; 15 | totalElements:number; 16 | }; 17 | } 18 | class StandardTable extends Component> { 19 | 20 | handleTableChange: TableProps['onChange'] = ( 21 | pagination, 22 | ...rest 23 | ) => { 24 | const { onChange } = this.props; 25 | if (onChange) { 26 | onChange(pagination, ...rest); 27 | } 28 | }; 29 | 30 | render() { 31 | const { data, ...rest } = this.props; 32 | const { content = [], pageable = false,totalElements = 0 } = data || {}; 33 | 34 | const paginationProps = pageable 35 | ? { 36 | showSizeChanger: true, 37 | showQuickJumper: true, 38 | total: totalElements, 39 | showTotal: ((total: number) => { 40 | return `共 ${total} 条`; 41 | }), 42 | current: pageable.pageNumber ? pageable.pageNumber + 1 : 1, 43 | pageSize: pageable.pageSize, 44 | } 45 | : false; 46 | 47 | return ( 48 |
49 |
row.id+""} 51 | dataSource={content} 52 | pagination={paginationProps} 53 | onChange={this.handleTableChange} 54 | {...rest} 55 | /> 56 | 57 | ); 58 | } 59 | } 60 | 61 | export default StandardTable; 62 | -------------------------------------------------------------------------------- /src/pages/configuration/triggerList/components/style.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | .card { 4 | margin-bottom: 24px; 5 | } 6 | 7 | .heading { 8 | margin: 0 0 16px 0; 9 | font-size: 14px; 10 | line-height: 22px; 11 | } 12 | 13 | .steps:global(.ant-steps) { 14 | max-width: 750px; 15 | margin: 16px auto; 16 | } 17 | 18 | .errorIcon { 19 | margin-right: 24px; 20 | color: @error-color; 21 | cursor: pointer; 22 | i { 23 | margin-right: 4px; 24 | } 25 | } 26 | 27 | .errorPopover { 28 | :global { 29 | .ant-popover-inner-content { 30 | min-width: 256px; 31 | max-height: 290px; 32 | padding: 0; 33 | overflow: auto; 34 | } 35 | } 36 | } 37 | 38 | .errorListItem { 39 | padding: 8px 16px; 40 | list-style: none; 41 | border-bottom: 1px solid @border-color-split; 42 | cursor: pointer; 43 | transition: all 0.3s; 44 | &:hover { 45 | background: @primary-1; 46 | } 47 | &:last-child { 48 | border: 0; 49 | } 50 | .errorIcon { 51 | float: left; 52 | margin-top: 4px; 53 | margin-right: 12px; 54 | padding-bottom: 22px; 55 | color: @error-color; 56 | } 57 | .errorField { 58 | margin-top: 2px; 59 | color: @text-color-secondary; 60 | font-size: 12px; 61 | } 62 | } 63 | 64 | .editable { 65 | td { 66 | padding-top: 13px !important; 67 | padding-bottom: 12.5px !important; 68 | } 69 | } 70 | 71 | // custom footer for fixed footer toolbar 72 | .advancedForm + div { 73 | padding-bottom: 64px; 74 | } 75 | 76 | .advancedForm { 77 | :global { 78 | .ant-form .ant-row:last-child .ant-form-item { 79 | margin-bottom: 24px; 80 | } 81 | .ant-table td { 82 | transition: none !important; 83 | } 84 | } 85 | } 86 | 87 | .optional { 88 | color: @text-color-secondary; 89 | font-style: normal; 90 | } 91 | -------------------------------------------------------------------------------- /src/pages/configuration/triggerList/data.d.ts: -------------------------------------------------------------------------------- 1 | export interface TableListItem { 2 | id: number; 3 | name: string; 4 | group: string; 5 | cronExpression: string; 6 | timeInterval: number; 7 | status: number; 8 | updatedAt: Date; 9 | createdAt: Date; 10 | } 11 | 12 | export interface TableListPagination { 13 | pageSize: number; 14 | pageNumber: number; 15 | current:number; 16 | } 17 | 18 | export interface TableListData { 19 | content: TableListItem[]; 20 | pageable: Partial; 21 | totalElements:number; 22 | } 23 | 24 | export interface TableListParams { 25 | sorter: string; 26 | triggerStatus: number; 27 | triggerName: string; 28 | triggerGroup: string; 29 | pageSize: number; 30 | currentPage: number; 31 | } 32 | -------------------------------------------------------------------------------- /src/pages/configuration/triggerList/models/triggerList.ts: -------------------------------------------------------------------------------- 1 | import { AnyAction, Reducer } from 'redux'; 2 | import { EffectsCommandMap } from 'dva'; 3 | import { queryTrigger, saveTrigger, removeTrigger, toggleTriggerStatus} from '../service'; 4 | 5 | import { TableListData } from '../data'; 6 | 7 | export interface StateType { 8 | data: TableListData; 9 | } 10 | 11 | export type Effect = ( 12 | action: AnyAction, 13 | effects: EffectsCommandMap & { select: (func: (state: StateType) => T) => T }, 14 | ) => void; 15 | 16 | export interface TriggerListModelType { 17 | namespace: string; 18 | state: StateType; 19 | effects: { 20 | fetch: Effect; 21 | saveTrigger: Effect; 22 | remove: Effect; 23 | toggleStatus: Effect; 24 | }; 25 | reducers: { 26 | save: Reducer; 27 | }; 28 | } 29 | 30 | const TriggerList: TriggerListModelType = { 31 | namespace: 'triggerList', 32 | 33 | state: { 34 | data: { 35 | content:[], 36 | pageable: {}, 37 | totalElements:0, 38 | }, 39 | }, 40 | 41 | effects: { 42 | *fetch({ payload }, { call, put }) { 43 | const response = yield call(queryTrigger, payload); 44 | yield put({ 45 | type: 'save', 46 | payload: response, 47 | }); 48 | }, 49 | *saveTrigger({ payload, callback }, { call, put }) { 50 | yield call(saveTrigger, payload); 51 | const response = yield call(queryTrigger); 52 | yield put({ 53 | type: 'save', 54 | payload: response, 55 | }); 56 | if (callback) callback(); 57 | }, 58 | *remove({ payload, callback }, { call, put }) { 59 | yield call(removeTrigger, payload); 60 | const response = yield call(queryTrigger); 61 | yield put({ 62 | type: 'save', 63 | payload: response, 64 | }); 65 | if (callback) callback(); 66 | }, 67 | *toggleStatus({payload, callback},{call, put}){ 68 | yield call(toggleTriggerStatus, payload); 69 | const response = yield call(queryTrigger); 70 | yield put({ 71 | type: 'save', 72 | payload: response, 73 | }); 74 | if (callback) callback(); 75 | } 76 | }, 77 | 78 | reducers: { 79 | save(state, action) { 80 | return { 81 | ...state, 82 | data: action.payload, 83 | }; 84 | }, 85 | }, 86 | }; 87 | 88 | export default TriggerList; 89 | -------------------------------------------------------------------------------- /src/pages/configuration/triggerList/service.ts: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request'; 2 | import { TableListParams } from './data'; 3 | 4 | export async function queryTrigger(params: TableListParams) { 5 | return request('/trigger/load', { 6 | params, 7 | }); 8 | } 9 | 10 | export async function removeTrigger(params: TableListParams) { 11 | return request('/trigger/delete', { 12 | method: 'POST', 13 | params, 14 | }); 15 | } 16 | 17 | export async function saveTrigger(params: TableListParams) { 18 | return request('/trigger/save', { 19 | method: 'POST', 20 | data: { 21 | ...params, 22 | }, 23 | }); 24 | } 25 | 26 | export async function toggleTriggerStatus(params: TableListParams) { 27 | return request('/trigger/toggleStatus', { 28 | method: 'POST', 29 | params, 30 | }); 31 | } 32 | -------------------------------------------------------------------------------- /src/pages/configuration/triggerList/style.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | @import './utils/utils.less'; 3 | 4 | .tableList { 5 | .tableListOperator { 6 | margin-bottom: 16px; 7 | button { 8 | margin-right: 8px; 9 | } 10 | } 11 | } 12 | 13 | .tableListForm { 14 | :global { 15 | .ant-form-item { 16 | display: flex; 17 | margin-right: 0; 18 | margin-bottom: 24px; 19 | > .ant-form-item-label { 20 | width: auto; 21 | padding-right: 8px; 22 | line-height: 32px; 23 | } 24 | .ant-form-item-control { 25 | line-height: 32px; 26 | } 27 | } 28 | .ant-form-item-control-wrapper { 29 | flex: 1; 30 | } 31 | } 32 | .submitButtons { 33 | display: block; 34 | margin-bottom: 24px; 35 | white-space: nowrap; 36 | } 37 | } 38 | 39 | @media screen and (max-width: @screen-lg) { 40 | .tableListForm :global(.ant-form-item) { 41 | margin-right: 24px; 42 | } 43 | } 44 | 45 | @media screen and (max-width: @screen-md) { 46 | .tableListForm :global(.ant-form-item) { 47 | margin-right: 8px; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/pages/configuration/triggerList/utils/utils.less: -------------------------------------------------------------------------------- 1 | .textOverflow() { 2 | overflow: hidden; 3 | white-space: nowrap; 4 | text-overflow: ellipsis; 5 | word-break: break-all; 6 | } 7 | 8 | .textOverflowMulti(@line: 3, @bg: #fff) { 9 | position: relative; 10 | max-height: @line * 1.5em; 11 | margin-right: -1em; 12 | padding-right: 1em; 13 | overflow: hidden; 14 | line-height: 1.5em; 15 | text-align: justify; 16 | &::before { 17 | position: absolute; 18 | right: 14px; 19 | bottom: 0; 20 | padding: 0 1px; 21 | background: @bg; 22 | content: '...'; 23 | } 24 | &::after { 25 | position: absolute; 26 | right: 14px; 27 | width: 1em; 28 | height: 1em; 29 | margin-top: 0.2em; 30 | background: white; 31 | content: ''; 32 | } 33 | } 34 | 35 | // mixins for clearfix 36 | // ------------------------ 37 | .clearfix() { 38 | zoom: 1; 39 | &::before, 40 | &::after { 41 | display: table; 42 | content: ' '; 43 | } 44 | &::after { 45 | clear: both; 46 | height: 0; 47 | font-size: 0; 48 | visibility: hidden; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/pages/platform/jobRunDetail/data.d.ts: -------------------------------------------------------------------------------- 1 | export interface TableListItem { 2 | jobExecutionId: string; 3 | jobName: string; 4 | version:number; 5 | exitCode:string; 6 | exitMessage:string; 7 | status: number; 8 | lastUpdated: Date; 9 | createTime: Date; 10 | endTime:Date; 11 | startTime:Date; 12 | } 13 | 14 | export interface TableListPagination { 15 | pageSize: number; 16 | pageNumber: number; 17 | current:number; 18 | } 19 | 20 | export interface TableListData { 21 | content: TableListItem[]; 22 | pageable: Partial; 23 | totalElements:number; 24 | } 25 | 26 | export interface TableListParams { 27 | status: string; 28 | name: string; 29 | pageSize: number; 30 | currentPage: number; 31 | } 32 | -------------------------------------------------------------------------------- /src/pages/platform/jobRunDetail/models/springBatch.ts: -------------------------------------------------------------------------------- 1 | import { AnyAction, Reducer } from 'redux'; 2 | import { EffectsCommandMap } from 'dva'; 3 | import { loadBatchJob, stopBatchJob, abandonBatchJob, restartBatchJob, startNextInstanceBatchJob } from '../service'; 4 | 5 | import { TableListData } from '../data'; 6 | 7 | export interface StateType { 8 | data: TableListData; 9 | } 10 | 11 | export type Effect = ( 12 | action: AnyAction, 13 | effects: EffectsCommandMap & { select: (func: (state: StateType) => T) => T }, 14 | ) => void; 15 | 16 | export interface JobRunDetailModelType { 17 | namespace: string; 18 | state: StateType; 19 | effects: { 20 | load: Effect; 21 | stop: Effect; 22 | abandon: Effect; 23 | restart: Effect; 24 | startNextInstance: Effect; 25 | }; 26 | reducers: { 27 | save: Reducer; 28 | }; 29 | } 30 | 31 | const JobRunDetailModel: JobRunDetailModelType = { 32 | namespace: 'springBatch', 33 | 34 | state: { 35 | data: { 36 | content: [], 37 | pageable: {}, 38 | totalElements: 0, 39 | }, 40 | }, 41 | 42 | effects: { 43 | *load({ payload }, { call, put }) { 44 | const response = yield call(loadBatchJob, payload); 45 | yield put({ 46 | type: 'save', 47 | payload: response, 48 | }); 49 | }, 50 | *stop({ payload, callback }, { call, put }) { 51 | yield call(stopBatchJob, payload); 52 | const response = yield call(loadBatchJob, payload); 53 | yield put({ 54 | type: 'save', 55 | payload: response, 56 | }); 57 | if (callback) callback(); 58 | }, 59 | *abandon({ payload, callback }, { call, put }) { 60 | yield call(abandonBatchJob, payload); 61 | const response = yield call(loadBatchJob, payload); 62 | yield put({ 63 | type: 'save', 64 | payload: response, 65 | }); 66 | if (callback) callback(); 67 | }, 68 | *restart({ payload, callback }, { call, put }) { 69 | yield call(restartBatchJob, payload); 70 | const response = yield call(loadBatchJob, payload); 71 | yield put({ 72 | type: 'save', 73 | payload: response, 74 | }); 75 | if (callback) callback(); 76 | }, 77 | *startNextInstance({ payload, callback }, { call, put }) { 78 | yield call(startNextInstanceBatchJob, payload); 79 | const response = yield call(loadBatchJob, payload); 80 | yield put({ 81 | type: 'save', 82 | payload: response, 83 | }); 84 | if (callback) callback(); 85 | }, 86 | }, 87 | 88 | reducers: { 89 | save(state, action) { 90 | return { 91 | ...state, 92 | data: action.payload, 93 | }; 94 | }, 95 | }, 96 | }; 97 | 98 | export default JobRunDetailModel; 99 | -------------------------------------------------------------------------------- /src/pages/platform/jobRunDetail/service.ts: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request'; 2 | import { TableListParams } from './data'; 3 | 4 | export async function loadBatchJob(params: TableListParams) { 5 | return request('/batch/load', { 6 | params, 7 | }); 8 | } 9 | 10 | export async function stopBatchJob(params: TableListParams) { 11 | return request('/batch/stop', { 12 | method: 'POST', 13 | params, 14 | }); 15 | } 16 | 17 | export async function abandonBatchJob(params: TableListParams) { 18 | return request('/batch/abandon', { 19 | method: 'POST', 20 | params, 21 | }); 22 | } 23 | 24 | export async function restartBatchJob(params: TableListParams) { 25 | return request('/batch/restart', { 26 | method: 'POST', 27 | params, 28 | }); 29 | } 30 | 31 | export async function startNextInstanceBatchJob(params: TableListParams) { 32 | return request('/batch/startNextInstance', { 33 | method: 'POST', 34 | params, 35 | }); 36 | } 37 | -------------------------------------------------------------------------------- /src/pages/platform/jobRunDetail/style.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | @import './utils/utils.less'; 3 | 4 | .tableList { 5 | .tableListOperator { 6 | margin-bottom: 16px; 7 | button { 8 | margin-right: 8px; 9 | } 10 | } 11 | } 12 | 13 | .tableListForm { 14 | :global { 15 | .ant-form-item { 16 | display: flex; 17 | margin-right: 0; 18 | margin-bottom: 24px; 19 | > .ant-form-item-label { 20 | width: auto; 21 | padding-right: 8px; 22 | line-height: 32px; 23 | } 24 | .ant-form-item-control { 25 | line-height: 32px; 26 | } 27 | } 28 | .ant-form-item-control-wrapper { 29 | flex: 1; 30 | } 31 | } 32 | .submitButtons { 33 | display: block; 34 | margin-bottom: 24px; 35 | white-space: nowrap; 36 | } 37 | } 38 | 39 | @media screen and (max-width: @screen-lg) { 40 | .tableListForm :global(.ant-form-item) { 41 | margin-right: 24px; 42 | } 43 | } 44 | 45 | @media screen and (max-width: @screen-md) { 46 | .tableListForm :global(.ant-form-item) { 47 | margin-right: 8px; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/pages/platform/jobRunDetail/utils/utils.less: -------------------------------------------------------------------------------- 1 | .textOverflow() { 2 | overflow: hidden; 3 | white-space: nowrap; 4 | text-overflow: ellipsis; 5 | word-break: break-all; 6 | } 7 | 8 | .textOverflowMulti(@line: 3, @bg: #fff) { 9 | position: relative; 10 | max-height: @line * 1.5em; 11 | margin-right: -1em; 12 | padding-right: 1em; 13 | overflow: hidden; 14 | line-height: 1.5em; 15 | text-align: justify; 16 | &::before { 17 | position: absolute; 18 | right: 14px; 19 | bottom: 0; 20 | padding: 0 1px; 21 | background: @bg; 22 | content: '...'; 23 | } 24 | &::after { 25 | position: absolute; 26 | right: 14px; 27 | width: 1em; 28 | height: 1em; 29 | margin-top: 0.2em; 30 | background: white; 31 | content: ''; 32 | } 33 | } 34 | 35 | // mixins for clearfix 36 | // ------------------------ 37 | .clearfix() { 38 | zoom: 1; 39 | &::before, 40 | &::after { 41 | display: table; 42 | content: ' '; 43 | } 44 | &::after { 45 | clear: both; 46 | height: 0; 47 | font-size: 0; 48 | visibility: hidden; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/pages/platform/quartzJob/data.d.ts: -------------------------------------------------------------------------------- 1 | export interface TableListItem { 2 | fireInstanceId: string; 3 | triggerName:string; 4 | triggerGroup:string; 5 | jobName:string; 6 | jobGroup:string; 7 | status:number; 8 | message:string; 9 | nextFireTime:Date; 10 | prevFireTime:Date; 11 | fireTime:Date; 12 | finishTime:Date; 13 | } 14 | 15 | export interface TableListPagination { 16 | pageSize: number; 17 | pageNumber: number; 18 | current:number; 19 | } 20 | 21 | export interface TableListData { 22 | content: TableListItem[]; 23 | pageable: Partial; 24 | totalElements:number; 25 | } 26 | 27 | export interface TableListParams { 28 | status: number; 29 | triggerName: string; 30 | jobName:string; 31 | jobGroup:string; 32 | triggerGroup:string; 33 | pageSize: number; 34 | currentPage: number; 35 | } 36 | -------------------------------------------------------------------------------- /src/pages/platform/quartzJob/models/quartzJob.ts: -------------------------------------------------------------------------------- 1 | import { AnyAction, Reducer } from 'redux'; 2 | import { EffectsCommandMap } from 'dva'; 3 | import { loadQuartzTrigger } from '../service'; 4 | 5 | import { TableListData } from '../data'; 6 | 7 | export interface StateType { 8 | data: TableListData; 9 | } 10 | 11 | export type Effect = ( 12 | action: AnyAction, 13 | effects: EffectsCommandMap & { select: (func: (state: StateType) => T) => T }, 14 | ) => void; 15 | 16 | export interface QuartzJobModelType { 17 | namespace: string; 18 | state: StateType; 19 | effects: { 20 | fetch: Effect; 21 | }; 22 | reducers: { 23 | save: Reducer; 24 | }; 25 | } 26 | 27 | const QuartzJobModel: QuartzJobModelType = { 28 | namespace: 'quartzJob', 29 | 30 | state: { 31 | data: { 32 | content: [], 33 | pageable: {}, 34 | totalElements:0, 35 | }, 36 | }, 37 | 38 | effects: { 39 | *fetch({ payload }, { call, put }) { 40 | const response = yield call(loadQuartzTrigger, payload); 41 | yield put({ 42 | type: 'save', 43 | payload: response, 44 | }); 45 | }, 46 | }, 47 | 48 | reducers: { 49 | save(state, action) { 50 | return { 51 | ...state, 52 | data: action.payload, 53 | }; 54 | }, 55 | }, 56 | }; 57 | 58 | export default QuartzJobModel; 59 | -------------------------------------------------------------------------------- /src/pages/platform/quartzJob/service.ts: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request'; 2 | import { TableListParams } from './data'; 3 | 4 | export async function loadQuartzTrigger(params: TableListParams) { 5 | return request('/quartz/job/load', { 6 | params, 7 | }); 8 | } 9 | -------------------------------------------------------------------------------- /src/pages/platform/quartzJob/style.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | @import './utils/utils.less'; 3 | 4 | .tableList { 5 | .tableListOperator { 6 | margin-bottom: 16px; 7 | button { 8 | margin-right: 8px; 9 | } 10 | } 11 | } 12 | 13 | .tableListForm { 14 | :global { 15 | .ant-form-item { 16 | display: flex; 17 | margin-right: 0; 18 | margin-bottom: 24px; 19 | > .ant-form-item-label { 20 | width: auto; 21 | padding-right: 8px; 22 | line-height: 32px; 23 | } 24 | .ant-form-item-control { 25 | line-height: 32px; 26 | } 27 | } 28 | .ant-form-item-control-wrapper { 29 | flex: 1; 30 | } 31 | } 32 | .submitButtons { 33 | display: block; 34 | margin-bottom: 24px; 35 | white-space: nowrap; 36 | } 37 | } 38 | 39 | @media screen and (max-width: @screen-lg) { 40 | .tableListForm :global(.ant-form-item) { 41 | margin-right: 24px; 42 | } 43 | } 44 | 45 | @media screen and (max-width: @screen-md) { 46 | .tableListForm :global(.ant-form-item) { 47 | margin-right: 8px; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/pages/platform/quartzJob/utils/utils.less: -------------------------------------------------------------------------------- 1 | .textOverflow() { 2 | overflow: hidden; 3 | white-space: nowrap; 4 | text-overflow: ellipsis; 5 | word-break: break-all; 6 | } 7 | 8 | .textOverflowMulti(@line: 3, @bg: #fff) { 9 | position: relative; 10 | max-height: @line * 1.5em; 11 | margin-right: -1em; 12 | padding-right: 1em; 13 | overflow: hidden; 14 | line-height: 1.5em; 15 | text-align: justify; 16 | &::before { 17 | position: absolute; 18 | right: 14px; 19 | bottom: 0; 20 | padding: 0 1px; 21 | background: @bg; 22 | content: '...'; 23 | } 24 | &::after { 25 | position: absolute; 26 | right: 14px; 27 | width: 1em; 28 | height: 1em; 29 | margin-top: 0.2em; 30 | background: white; 31 | content: ''; 32 | } 33 | } 34 | 35 | // mixins for clearfix 36 | // ------------------------ 37 | .clearfix() { 38 | zoom: 1; 39 | &::before, 40 | &::after { 41 | display: table; 42 | content: ' '; 43 | } 44 | &::after { 45 | clear: both; 46 | height: 0; 47 | font-size: 0; 48 | visibility: hidden; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/pages/platform/quartzTriggerList/data.d.ts: -------------------------------------------------------------------------------- 1 | export interface TableListItem { 2 | id: string; 3 | schedName:string; 4 | triggerName:string; 5 | triggerGroup:string; 6 | jobName:string; 7 | jobGroup:string; 8 | nextFireTime:Date; 9 | prevFireTime:Date; 10 | triggerState:string; 11 | priority:number; 12 | startTime:Date; 13 | endTime:Date; 14 | misFireNum:number; 15 | } 16 | 17 | export interface TableListPagination { 18 | pageSize: number; 19 | pageNumber: number; 20 | current:number; 21 | } 22 | 23 | export interface TableListData { 24 | content: TableListItem[]; 25 | pageable: Partial; 26 | totalElements:number; 27 | } 28 | 29 | export interface TableListParams { 30 | triggerStatus: any; 31 | triggerName: any; 32 | schedName:string; 33 | jobName:string; 34 | jobGroup:string; 35 | triggerGroup:string; 36 | pageSize: number; 37 | currentPage: number; 38 | } 39 | -------------------------------------------------------------------------------- /src/pages/platform/quartzTriggerList/models/quartzTriggerList.ts: -------------------------------------------------------------------------------- 1 | import { AnyAction, Reducer } from 'redux'; 2 | import { EffectsCommandMap } from 'dva'; 3 | import { loadQuartzTrigger, pauseQuartzTrigger, resumeQuartzTrigger } from '../service'; 4 | 5 | import { TableListData } from '../data'; 6 | 7 | export interface StateType { 8 | data: TableListData; 9 | } 10 | 11 | export type Effect = ( 12 | action: AnyAction, 13 | effects: EffectsCommandMap & { select: (func: (state: StateType) => T) => T }, 14 | ) => void; 15 | 16 | export interface QuartzTriggerListModelType { 17 | namespace: string; 18 | state: StateType; 19 | effects: { 20 | fetch: Effect; 21 | pause:Effect; 22 | resume:Effect; 23 | }; 24 | reducers: { 25 | save: Reducer; 26 | }; 27 | } 28 | 29 | const QuartzTriggerListModel: QuartzTriggerListModelType = { 30 | namespace: 'quartzTriggerList', 31 | 32 | state: { 33 | data: { 34 | content: [], 35 | pageable: {}, 36 | totalElements:0, 37 | }, 38 | }, 39 | 40 | effects: { 41 | *fetch({ payload }, { call, put }) { 42 | const response = yield call(loadQuartzTrigger, payload); 43 | yield put({ 44 | type: 'save', 45 | payload: response, 46 | }); 47 | }, 48 | *pause({ payload, callback }, { call, put }) { 49 | yield call(pauseQuartzTrigger, payload); 50 | const response = yield call(loadQuartzTrigger); 51 | yield put({ 52 | type: 'save', 53 | payload: response, 54 | }); 55 | if (callback) callback(); 56 | }, 57 | *resume({ payload, callback }, { call, put }) { 58 | yield call(resumeQuartzTrigger, payload); 59 | const response = yield call(loadQuartzTrigger); 60 | yield put({ 61 | type: 'save', 62 | payload: response, 63 | }); 64 | if (callback) callback(); 65 | }, 66 | }, 67 | 68 | reducers: { 69 | save(state, action) { 70 | return { 71 | ...state, 72 | data: action.payload, 73 | }; 74 | }, 75 | }, 76 | }; 77 | 78 | export default QuartzTriggerListModel; 79 | -------------------------------------------------------------------------------- /src/pages/platform/quartzTriggerList/service.ts: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request'; 2 | import { TableListParams } from './data'; 3 | 4 | export async function loadQuartzTrigger(params: TableListParams) { 5 | return request('/quartz/load', { 6 | params, 7 | }); 8 | } 9 | 10 | export async function pauseQuartzTrigger(params: TableListParams) { 11 | return request('/quartz/pause', { 12 | method: 'POST', 13 | params, 14 | }); 15 | } 16 | 17 | export async function resumeQuartzTrigger(params: TableListParams) { 18 | return request('/quartz/resume', { 19 | method: 'POST', 20 | params, 21 | }); 22 | } 23 | -------------------------------------------------------------------------------- /src/pages/platform/quartzTriggerList/style.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | @import './utils/utils.less'; 3 | 4 | .tableList { 5 | .tableListOperator { 6 | margin-bottom: 16px; 7 | button { 8 | margin-right: 8px; 9 | } 10 | } 11 | } 12 | 13 | .tableListForm { 14 | :global { 15 | .ant-form-item { 16 | display: flex; 17 | margin-right: 0; 18 | margin-bottom: 24px; 19 | > .ant-form-item-label { 20 | width: auto; 21 | padding-right: 8px; 22 | line-height: 32px; 23 | } 24 | .ant-form-item-control { 25 | line-height: 32px; 26 | } 27 | } 28 | .ant-form-item-control-wrapper { 29 | flex: 1; 30 | } 31 | } 32 | .submitButtons { 33 | display: block; 34 | margin-bottom: 24px; 35 | white-space: nowrap; 36 | } 37 | } 38 | 39 | @media screen and (max-width: @screen-lg) { 40 | .tableListForm :global(.ant-form-item) { 41 | margin-right: 24px; 42 | } 43 | } 44 | 45 | @media screen and (max-width: @screen-md) { 46 | .tableListForm :global(.ant-form-item) { 47 | margin-right: 8px; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/pages/platform/quartzTriggerList/utils/utils.less: -------------------------------------------------------------------------------- 1 | .textOverflow() { 2 | overflow: hidden; 3 | white-space: nowrap; 4 | text-overflow: ellipsis; 5 | word-break: break-all; 6 | } 7 | 8 | .textOverflowMulti(@line: 3, @bg: #fff) { 9 | position: relative; 10 | max-height: @line * 1.5em; 11 | margin-right: -1em; 12 | padding-right: 1em; 13 | overflow: hidden; 14 | line-height: 1.5em; 15 | text-align: justify; 16 | &::before { 17 | position: absolute; 18 | right: 14px; 19 | bottom: 0; 20 | padding: 0 1px; 21 | background: @bg; 22 | content: '...'; 23 | } 24 | &::after { 25 | position: absolute; 26 | right: 14px; 27 | width: 1em; 28 | height: 1em; 29 | margin-top: 0.2em; 30 | background: white; 31 | content: ''; 32 | } 33 | } 34 | 35 | // mixins for clearfix 36 | // ------------------------ 37 | .clearfix() { 38 | zoom: 1; 39 | &::before, 40 | &::after { 41 | display: table; 42 | content: ' '; 43 | } 44 | &::after { 45 | clear: both; 46 | height: 0; 47 | font-size: 0; 48 | visibility: hidden; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/pages/runtime/jobExecutionHistory/data.d.ts: -------------------------------------------------------------------------------- 1 | export interface TableListItem { 2 | id: number; 3 | jobName: string; 4 | userName: string; 5 | runId:number; 6 | exitCode:string; 7 | exitMessage:string; 8 | status: string; 9 | updatedAt: Date; 10 | createAt: Date; 11 | endAt:Date; 12 | startAt:Date; 13 | } 14 | 15 | export interface TableListPagination { 16 | pageSize: number; 17 | pageNumber: number; 18 | current:number; 19 | } 20 | 21 | export interface TableListData { 22 | content: TableListItem[]; 23 | pageable: Partial; 24 | totalElements:number; 25 | } 26 | 27 | export interface TableListParams { 28 | status: string; 29 | jobName: string; 30 | userName:string; 31 | runId:number; 32 | pageSize: number; 33 | currentPage: number; 34 | } 35 | -------------------------------------------------------------------------------- /src/pages/runtime/jobExecutionHistory/models/jobExecutionHistory.ts: -------------------------------------------------------------------------------- 1 | import { AnyAction, Reducer } from 'redux'; 2 | import { EffectsCommandMap } from 'dva'; 3 | import { queryJobExecutionHistory } from '../service'; 4 | 5 | import { TableListData } from '../data'; 6 | 7 | export interface StateType { 8 | data: TableListData; 9 | } 10 | 11 | export type Effect = ( 12 | action: AnyAction, 13 | effects: EffectsCommandMap & { select: (func: (state: StateType) => T) => T }, 14 | ) => void; 15 | 16 | export interface JobExecutionHistoryModelType { 17 | namespace: string; 18 | state: StateType; 19 | effects: { 20 | fetch: Effect; 21 | }; 22 | reducers: { 23 | save: Reducer; 24 | }; 25 | } 26 | 27 | const JobExecutionHistoryModel: JobExecutionHistoryModelType = { 28 | namespace: 'jobExecutionHistory', 29 | 30 | state: { 31 | data: { 32 | content: [], 33 | pageable: {}, 34 | totalElements:0, 35 | }, 36 | }, 37 | 38 | effects: { 39 | *fetch({ payload }, { call, put }) { 40 | const response = yield call(queryJobExecutionHistory, payload); 41 | yield put({ 42 | type: 'save', 43 | payload: response, 44 | }); 45 | }, 46 | }, 47 | 48 | reducers: { 49 | save(state, action) { 50 | return { 51 | ...state, 52 | data: action.payload, 53 | }; 54 | }, 55 | }, 56 | }; 57 | 58 | export default JobExecutionHistoryModel; 59 | -------------------------------------------------------------------------------- /src/pages/runtime/jobExecutionHistory/service.ts: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request'; 2 | import { TableListParams } from './data'; 3 | 4 | export async function queryJobExecutionHistory(params: TableListParams) { 5 | return request('/jobHistory/load', { 6 | params, 7 | }); 8 | } 9 | -------------------------------------------------------------------------------- /src/pages/runtime/jobExecutionHistory/style.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | @import './utils/utils.less'; 3 | 4 | .tableList { 5 | .tableListOperator { 6 | margin-bottom: 16px; 7 | button { 8 | margin-right: 8px; 9 | } 10 | } 11 | } 12 | 13 | .tableListForm { 14 | :global { 15 | .ant-form-item { 16 | display: flex; 17 | margin-right: 0; 18 | margin-bottom: 24px; 19 | > .ant-form-item-label { 20 | width: auto; 21 | padding-right: 8px; 22 | line-height: 32px; 23 | } 24 | .ant-form-item-control { 25 | line-height: 32px; 26 | } 27 | } 28 | .ant-form-item-control-wrapper { 29 | flex: 1; 30 | } 31 | } 32 | .submitButtons { 33 | display: block; 34 | margin-bottom: 24px; 35 | white-space: nowrap; 36 | } 37 | } 38 | 39 | @media screen and (max-width: @screen-lg) { 40 | .tableListForm :global(.ant-form-item) { 41 | margin-right: 24px; 42 | } 43 | } 44 | 45 | @media screen and (max-width: @screen-md) { 46 | .tableListForm :global(.ant-form-item) { 47 | margin-right: 8px; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/pages/runtime/jobExecutionHistory/utils/utils.less: -------------------------------------------------------------------------------- 1 | .textOverflow() { 2 | overflow: hidden; 3 | white-space: nowrap; 4 | text-overflow: ellipsis; 5 | word-break: break-all; 6 | } 7 | 8 | .textOverflowMulti(@line: 3, @bg: #fff) { 9 | position: relative; 10 | max-height: @line * 1.5em; 11 | margin-right: -1em; 12 | padding-right: 1em; 13 | overflow: hidden; 14 | line-height: 1.5em; 15 | text-align: justify; 16 | &::before { 17 | position: absolute; 18 | right: 14px; 19 | bottom: 0; 20 | padding: 0 1px; 21 | background: @bg; 22 | content: '...'; 23 | } 24 | &::after { 25 | position: absolute; 26 | right: 14px; 27 | width: 1em; 28 | height: 1em; 29 | margin-top: 0.2em; 30 | background: white; 31 | content: ''; 32 | } 33 | } 34 | 35 | // mixins for clearfix 36 | // ------------------------ 37 | .clearfix() { 38 | zoom: 1; 39 | &::before, 40 | &::after { 41 | display: table; 42 | content: ' '; 43 | } 44 | &::after { 45 | clear: both; 46 | height: 0; 47 | font-size: 0; 48 | visibility: hidden; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/pages/user/login/components/Login/LoginContext.tsx: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | 3 | export interface LoginContextProps { 4 | tabUtil?: { 5 | addTab: (id: string) => void; 6 | removeTab: (id: string) => void; 7 | }; 8 | updateActive?: (activeItem: { [key: string]: string } | string) => void; 9 | } 10 | 11 | const LoginContext: React.Context = createContext({}); 12 | 13 | export default LoginContext; 14 | -------------------------------------------------------------------------------- /src/pages/user/login/components/Login/LoginSubmit.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Form } from 'antd'; 2 | 3 | import { ButtonProps } from 'antd/es/button'; 4 | import React from 'react'; 5 | import classNames from 'classnames'; 6 | import styles from './index.less'; 7 | 8 | const FormItem = Form.Item; 9 | 10 | interface LoginSubmitProps extends ButtonProps { 11 | className?: string; 12 | } 13 | 14 | const LoginSubmit: React.FC = ({ className, ...rest }) => { 15 | const clsString = classNames(styles.submit, className); 16 | return ( 17 | 18 |