├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .prettierignore ├── .prettierrc.js ├── .stylelintrc.js ├── CNAME ├── README.md ├── config ├── config.ts ├── defaultSettings.ts ├── plugin.config.ts └── router.config.ts ├── jest-puppeteer.config.js ├── jest.config.js ├── jsconfig.json ├── mock └── index.ts ├── package.json ├── public ├── favicon.png └── icons │ ├── icon-128x128.png │ ├── icon-192x192.png │ └── icon-512x512.png ├── src ├── assets │ ├── img │ │ ├── aliPay.png │ │ ├── common.png │ │ ├── dd.png │ │ ├── github.png │ │ ├── web.png │ │ ├── wechat.png │ │ ├── weex.png │ │ ├── weibo.png │ │ └── wx.png │ └── logo.svg ├── components │ ├── Authorized │ │ ├── Authorized.tsx │ │ ├── AuthorizedRoute.tsx │ │ ├── CheckPermissions.tsx │ │ ├── PromiseRender.tsx │ │ ├── Secured.tsx │ │ ├── index.tsx │ │ └── renderAuthorize.ts │ ├── Calendar │ │ └── index.tsx │ ├── CopyBlock │ │ ├── index.less │ │ └── index.tsx │ ├── DataTable │ │ ├── index.less │ │ └── index.tsx │ ├── EventVariateForm │ │ └── index.tsx │ ├── GlobalHeader │ │ ├── AvatarDropdown.tsx │ │ ├── NoticeIconView.tsx │ │ ├── RightContent.tsx │ │ └── index.less │ ├── GraphHOC │ │ ├── AccessSpeed.tsx │ │ ├── ApiDetail.tsx │ │ ├── ApiSuccess.tsx │ │ ├── JsError.tsx │ │ ├── JsErrorCluster.tsx │ │ ├── PvUv.tsx │ │ ├── index.tsx │ │ └── style.less │ ├── HeaderDropdown │ │ ├── index.less │ │ └── index.tsx │ ├── HeaderSearch │ │ ├── index.less │ │ └── index.tsx │ ├── NoticeIcon │ │ ├── NoticeList.less │ │ ├── NoticeList.tsx │ │ ├── index.less │ │ └── index.tsx │ ├── OptionList │ │ ├── index.tsx │ │ └── style.less │ ├── PageLoading │ │ └── index.tsx │ ├── PageVariateForm │ │ └── index.tsx │ ├── SelectDatetime │ │ ├── index.less │ │ └── index.tsx │ ├── SelectLang │ │ ├── index.less │ │ └── index.tsx │ ├── SelectProject │ │ ├── index.less │ │ └── index.tsx │ └── SettingDrawer │ │ └── themeColorClient.ts ├── config │ ├── config.local.ts │ ├── config.prod.ts │ └── index.ts ├── e2e │ ├── __mocks__ │ │ └── antd-pro-merge-less.js │ ├── baseLayout.e2e.js │ └── topMenu.e2e.js ├── global.less ├── global.tsx ├── layouts │ ├── BasicLayout.tsx │ ├── BlankLayout.tsx │ ├── HomeLayout.less │ ├── HomeLayout.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 │ │ └── viewdetail.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 │ │ └── viewdetail.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 │ ├── project.ts │ ├── setting.ts │ ├── user.ts │ └── viewDetail.ts ├── pages │ ├── 404.tsx │ ├── Authorized.tsx │ ├── Welcome.tsx │ ├── account │ │ └── settings │ │ │ ├── _mock.ts │ │ │ ├── components │ │ │ ├── BaseView.less │ │ │ ├── GeographicView.less │ │ │ ├── GeographicView.tsx │ │ │ ├── PhoneView.less │ │ │ ├── PhoneView.tsx │ │ │ ├── base.tsx │ │ │ ├── binding.tsx │ │ │ ├── notification.tsx │ │ │ └── security.tsx │ │ │ ├── data.d.ts │ │ │ ├── geographic │ │ │ ├── city.json │ │ │ └── province.json │ │ │ ├── index.tsx │ │ │ ├── locales │ │ │ ├── en-US.ts │ │ │ ├── zh-CN.ts │ │ │ └── zh-TW.ts │ │ │ ├── model.ts │ │ │ ├── service.ts │ │ │ └── style.less │ ├── document.ejs │ ├── projects │ │ └── list │ │ │ ├── index.tsx │ │ │ └── style.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 │ │ └── register │ │ │ ├── index.tsx │ │ │ ├── locales │ │ │ ├── en-US.ts │ │ │ └── zh-CN.ts │ │ │ └── style.less │ └── web │ │ ├── add │ │ ├── index.tsx │ │ └── style.less │ │ ├── dashboard │ │ ├── index.tsx │ │ └── style.less │ │ ├── product │ │ ├── circle │ │ │ ├── index.tsx │ │ │ └── style.less │ │ ├── eventvariate │ │ │ ├── index.tsx │ │ │ └── style.less │ │ └── group │ │ │ ├── index.tsx │ │ │ └── style.less │ │ ├── setting │ │ ├── index.tsx │ │ └── style.less │ │ ├── technology │ │ └── latitude │ │ │ ├── geography │ │ │ └── index.tsx │ │ │ ├── network │ │ │ └── index.tsx │ │ │ ├── terminal │ │ │ └── index.tsx │ │ │ └── url │ │ │ ├── TerminalDistribution.tsx │ │ │ ├── index.tsx │ │ │ └── style.less │ │ └── viewdetail │ │ ├── AllLog │ │ ├── SearchParam │ │ │ ├── index.less │ │ │ └── index.tsx │ │ ├── index.less │ │ └── index.tsx │ │ ├── index.tsx │ │ └── styles.less ├── service-worker.js ├── services │ ├── eventVariate.ts │ ├── latitude.ts │ ├── login.ts │ ├── pageVariate.ts │ ├── project.ts │ ├── user.ts │ └── viewDetail.ts ├── typings.d.ts ├── typings │ ├── index.d.ts │ └── project.d.ts └── utils │ ├── Authorized.ts │ ├── authority.test.ts │ ├── authority.ts │ ├── helper.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 | rules: { 10 | ...strictEslint.rules, 11 | 'no-underscore-dangle': 0, 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | **/node_modules 5 | # roadhog-api-doc ignore 6 | /src/utils/request-temp.js 7 | _roadhog-api-doc 8 | 9 | # production 10 | /dist 11 | /.vscode 12 | 13 | # misc 14 | .DS_Store 15 | npm-debug.log* 16 | yarn-error.log 17 | 18 | /coverage 19 | .idea 20 | yarn.lock 21 | package-lock.json 22 | *bak 23 | .vscode 24 | 25 | # visual studio code 26 | .history 27 | *.log 28 | functions/* 29 | .temp/** 30 | 31 | # umi 32 | .umi 33 | .umi-production 34 | 35 | # screenshot 36 | screenshot 37 | .firebase 38 | .eslintcache 39 | 40 | build 41 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | preview.pro.ant.design -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BOMBAYJS-ADMIN 2 | 3 | 此项目是bombayjs后台管理系统 4 | 5 | bombayjs是前端监控解决方案,包括bombayjs、bombayjs-server、bombayjs-admin三个项目 6 | 7 | 项目地址: 8 | 9 | * https://github.com/bombayjs/bombayjs (web sdk) 10 | * https://github.com/bombayjs/bombayjs-server (服务端,用于提供api) 11 | * https://github.com/bombayjs/bombayjs-admin (后台管理系统,可视化数据等) 12 | 13 | ## Environment Prepare 14 | 15 | Install `node_modules`: 16 | 17 | ```bash 18 | npm install 19 | ``` 20 | 21 | or 22 | 23 | ```bash 24 | yarn 25 | ``` 26 | 27 | ## Provided Scripts 28 | 29 | Ant Design Pro provides some useful script to help you quick start and build with web project, code style check and test. 30 | 31 | Scripts provided in `package.json`. It's safe to modify or add additional script: 32 | 33 | ### Start project 34 | 35 | mock 36 | ```bash 37 | npm start 38 | ``` 39 | 40 | if you are running the [server](https://github.com/bombayjs/bombayjs-server), you can run without mock 41 | 42 | ```bash 43 | npm run start:no-mock 44 | ``` 45 | 46 | ### Build project 47 | 48 | ```bash 49 | npm run build 50 | ``` 51 | 52 | ### Check code style 53 | 54 | ```bash 55 | npm run lint 56 | ``` 57 | 58 | You can also use script to auto fix some lint error: 59 | 60 | ```bash 61 | npm run lint:fix 62 | ``` 63 | 64 | ### Test code 65 | 66 | ```bash 67 | npm test 68 | ``` 69 | -------------------------------------------------------------------------------- /config/defaultSettings.ts: -------------------------------------------------------------------------------- 1 | import { MenuTheme } from 'antd/es/menu'; 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: false, 53 | colorWeak: false, 54 | menu: { 55 | locale: true, 56 | }, 57 | title: 'Bombayjs', 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 | -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bombayjs/bombayjs-admin/41f5815586418748f4ab2a6d00c619562b6237bc/public/favicon.png -------------------------------------------------------------------------------- /public/icons/icon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bombayjs/bombayjs-admin/41f5815586418748f4ab2a6d00c619562b6237bc/public/icons/icon-128x128.png -------------------------------------------------------------------------------- /public/icons/icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bombayjs/bombayjs-admin/41f5815586418748f4ab2a6d00c619562b6237bc/public/icons/icon-192x192.png -------------------------------------------------------------------------------- /public/icons/icon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bombayjs/bombayjs-admin/41f5815586418748f4ab2a6d00c619562b6237bc/public/icons/icon-512x512.png -------------------------------------------------------------------------------- /src/assets/img/aliPay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bombayjs/bombayjs-admin/41f5815586418748f4ab2a6d00c619562b6237bc/src/assets/img/aliPay.png -------------------------------------------------------------------------------- /src/assets/img/common.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bombayjs/bombayjs-admin/41f5815586418748f4ab2a6d00c619562b6237bc/src/assets/img/common.png -------------------------------------------------------------------------------- /src/assets/img/dd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bombayjs/bombayjs-admin/41f5815586418748f4ab2a6d00c619562b6237bc/src/assets/img/dd.png -------------------------------------------------------------------------------- /src/assets/img/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bombayjs/bombayjs-admin/41f5815586418748f4ab2a6d00c619562b6237bc/src/assets/img/github.png -------------------------------------------------------------------------------- /src/assets/img/web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bombayjs/bombayjs-admin/41f5815586418748f4ab2a6d00c619562b6237bc/src/assets/img/web.png -------------------------------------------------------------------------------- /src/assets/img/wechat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bombayjs/bombayjs-admin/41f5815586418748f4ab2a6d00c619562b6237bc/src/assets/img/wechat.png -------------------------------------------------------------------------------- /src/assets/img/weex.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bombayjs/bombayjs-admin/41f5815586418748f4ab2a6d00c619562b6237bc/src/assets/img/weex.png -------------------------------------------------------------------------------- /src/assets/img/weibo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bombayjs/bombayjs-admin/41f5815586418748f4ab2a6d00c619562b6237bc/src/assets/img/weibo.png -------------------------------------------------------------------------------- /src/assets/img/wx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bombayjs/bombayjs-admin/41f5815586418748f4ab2a6d00c619562b6237bc/src/assets/img/wx.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/Calendar/index.tsx: -------------------------------------------------------------------------------- 1 | /* eslint react/no-multi-comp:0, no-console:0 */ 2 | 3 | import React from 'react'; 4 | import RangeCalendar from 'rc-calendar/lib/RangeCalendar'; 5 | import zhCN from 'rc-calendar/lib/locale/zh_CN'; 6 | import enUS from 'rc-calendar/lib/locale/en_US'; 7 | import TimePickerPanel from 'rc-time-picker/lib/Panel'; 8 | import 'rc-calendar/assets/index.css'; 9 | import 'rc-time-picker/assets/index.css'; 10 | 11 | import moment from 'moment'; 12 | import 'moment/locale/zh-cn'; 13 | import 'moment/locale/en-gb'; 14 | 15 | const cn = window.location.search.indexOf('cn') !== -1; 16 | 17 | if (cn) { 18 | moment.locale('zh-cn'); 19 | } else { 20 | moment.locale('en-gb'); 21 | } 22 | 23 | const now = moment(); 24 | if (cn) { 25 | now.utcOffset(8); 26 | } else { 27 | now.utcOffset(0); 28 | } 29 | 30 | const defaultCalendarValue = now.clone(); 31 | defaultCalendarValue.add(-1, 'month'); 32 | 33 | const timePickerElement = ( 34 | 37 | ); 38 | 39 | function disabledDate(current: any) { 40 | const date = moment(); 41 | date.hour(0); 42 | date.minute(0); 43 | date.second(0); 44 | return current.isAfter(date); // can not select days before today 45 | } 46 | 47 | const formatStr = 'YYYY-MM-DD HH:mm:ss'; 48 | 49 | export interface CalendarPropsType { 50 | onChange?: (value: [moment.Moment, moment.Moment]) => void; 51 | onSelect: (value: [moment.Moment, moment.Moment]) => void; 52 | } 53 | 54 | const Calendar: React.FC = props => { 55 | const { onChange, onSelect } = props; 56 | return ( 57 | 70 | ); 71 | }; 72 | 73 | export default Calendar; 74 | -------------------------------------------------------------------------------- /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/DataTable/index.less: -------------------------------------------------------------------------------- 1 | .table-container { 2 | margin-top: 20px; 3 | } 4 | -------------------------------------------------------------------------------- /src/components/DataTable/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import { Table } from 'antd'; 3 | 4 | import styles from './index.less'; 5 | 6 | interface DataTableTypeProps { 7 | dataSource: Array; 8 | columns: Array; 9 | } 10 | 11 | interface DataTableTypeState {} 12 | 13 | class DataTable extends PureComponent { 14 | constructor(props: DataTableTypeProps) { 15 | super(props); 16 | this.state = {}; 17 | } 18 | 19 | render() { 20 | const { columns, dataSource } = this.props; 21 | return ( 22 |
23 | 24 | 25 | ); 26 | } 27 | } 28 | 29 | export default DataTable; 30 | -------------------------------------------------------------------------------- /src/components/EventVariateForm/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Drawer, Button, Form, Input } from 'antd'; 3 | import { FormComponentProps } from 'antd/es/form'; 4 | 5 | interface IProps { 6 | name: string; 7 | marker: string; 8 | form: FormComponentProps['form']; 9 | visible: boolean; 10 | onClose: () => void; 11 | handleSubmit: (values) => void; 12 | } 13 | 14 | const EventVariateForm: React.FC = props => { 15 | const { getFieldDecorator, getFieldsError } = props.form; 16 | const { name, marker, visible, onClose, handleSubmit } = props; 17 | 18 | const hasErrors = fieldsError => Object.keys(fieldsError).some(field => fieldsError[field]); 19 | 20 | function submit(e) { 21 | e.preventDefault(); 22 | props.form.validateFields(async (err, values) => { 23 | if (!err) { 24 | props.form.resetFields(); 25 | handleSubmit(values); 26 | } 27 | }); 28 | } 29 | 30 | return ( 31 | 32 |
33 | 34 | {getFieldDecorator('name', { 35 | initialValue: name, 36 | rules: [{ required: true, message: '请输入元素名称!', type: 'string' }], 37 | })()} 38 | 39 | 40 | {getFieldDecorator('marker', { 41 | initialValue: marker, 42 | })()} 43 | 44 | 45 | 48 | 49 | 50 |
51 | ); 52 | }; 53 | 54 | export default Form.create()(EventVariateForm); 55 | -------------------------------------------------------------------------------- /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 | router.push(`/account/${key}`); 33 | }; 34 | 35 | render(): React.ReactNode { 36 | const { currentUser = { avatar: '', user_name: '' }, menu } = this.props; 37 | 38 | const menuHeaderDropdown = ( 39 | 40 | {menu && ( 41 | 42 | 43 | 44 | 45 | )} 46 | {menu && ( 47 | 48 | 49 | 50 | 51 | )} 52 | {menu && } 53 | 54 | 55 | 56 | 57 | 58 | 59 | ); 60 | 61 | return currentUser && currentUser.user_name ? ( 62 | 63 | 64 | 70 | {currentUser.user_name.substring(0, 1)} 71 | 72 | {currentUser.user_name} 73 | 74 | 75 | ) : ( 76 | 77 | ); 78 | } 79 | } 80 | export default connect(({ user }: ConnectState) => ({ 81 | currentUser: user.currentUser, 82 | }))(AvatarDropdown); 83 | -------------------------------------------------------------------------------- /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 | :global(.ant-badge) { 89 | color: rgba(255, 255, 255, 0.85); 90 | } 91 | } 92 | } 93 | 94 | @media only screen and (max-width: @screen-md) { 95 | :global(.ant-divider-vertical) { 96 | vertical-align: unset; 97 | } 98 | .name { 99 | display: none; 100 | } 101 | i.trigger { 102 | padding: 22px 12px; 103 | } 104 | .logo { 105 | position: relative; 106 | padding-right: 12px; 107 | padding-left: 12px; 108 | } 109 | .right { 110 | position: absolute; 111 | top: 0; 112 | right: 12px; 113 | background: #fff; 114 | .account { 115 | .avatar { 116 | margin-right: 0; 117 | } 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/components/GraphHOC/AccessSpeed.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | // 引入组件 3 | import { Chart, Geom, Axis, Tooltip, Legend } from 'bizcharts'; 4 | import GraphHOCC from '@/components/GraphHOC'; 5 | 6 | function AccessSpeed(props: any) { 7 | const accessSpeedData: any[] = []; 8 | const { handleFormatTime } = props; 9 | props.graphData.data.data.forEach((propsDataItem: any) => { 10 | const time = handleFormatTime(propsDataItem.date); 11 | accessSpeedData.push( 12 | { type: 'DOM Ready', time, value: propsDataItem.avg_dom }, 13 | { type: '首次渲染', time, value: propsDataItem.avg_ready }, 14 | { type: '页面完全加载', time, value: propsDataItem.avg_load }, 15 | { type: 'cfpt', time, value: propsDataItem.avg_fpt }, 16 | { type: 'ctti', time, value: propsDataItem.avg_tti }, 17 | ); 18 | }); 19 | return ( 20 | 21 | 22 | 23 | 24 | 25 | 26 | 37 | 38 | ); 39 | } 40 | 41 | export default GraphHOCC({ 42 | graphTitle: '访问速度', 43 | })(AccessSpeed); 44 | -------------------------------------------------------------------------------- /src/components/GraphHOC/ApiDetail.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | // 引入组件 3 | import { Table } from 'antd'; 4 | import GraphHOCC from '@/components/GraphHOC'; 5 | 6 | function ApiDetail(props: any) { 7 | const apiDetailData: any[] = []; 8 | const apiDetailCol: any[] = [ 9 | { title: 'API 名称', dataIndex: 'url' }, 10 | { title: '请求次数', dataIndex: 'count', width: 90 }, 11 | { title: '平均耗时', dataIndex: 'avg_time', width: 90 }, 12 | ]; 13 | props.graphData.data.data.forEach((propsDataItem: any, propsDataIndex: number) => { 14 | apiDetailData.push({ 15 | key: propsDataIndex + 1, 16 | url: propsDataItem.url, 17 | count: propsDataItem.count, 18 | avg_time: `${(Number(propsDataItem.avg_time) / 1000).toFixed(2)}s`, 19 | }); 20 | }); 21 | return
; 22 | } 23 | 24 | export default GraphHOCC({ 25 | graphTitle: 'API 详情', 26 | })(ApiDetail); 27 | -------------------------------------------------------------------------------- /src/components/GraphHOC/ApiSuccess.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | // 引入组件 3 | import { Chart, Geom, Axis, Tooltip, Legend } from 'bizcharts'; 4 | import GraphHOCC from '@/components/GraphHOC'; 5 | 6 | function ApiSuccess(props: any) { 7 | const apiSuccessData = props.graphData.data.data.map((propsDataItem: any) => { 8 | propsDataItem.date = props.handleFormatTime(propsDataItem.date); 9 | return propsDataItem; 10 | }); 11 | return ( 12 | 13 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | ); 43 | } 44 | 45 | export default GraphHOCC({ 46 | graphTitle: 'Api 请求成功率', 47 | })(ApiSuccess); 48 | -------------------------------------------------------------------------------- /src/components/GraphHOC/JsError.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | // 引入组件 3 | import { Chart, Geom, Axis, Tooltip, Legend } from 'bizcharts'; 4 | import GraphHOCC from '@/components/GraphHOC'; 5 | 6 | function JsError(props: any) { 7 | const jsErrorData = props.graphData.data.data.map((propsDataItem: any) => { 8 | propsDataItem.date = props.handleFormatTime(propsDataItem.date); 9 | return propsDataItem; 10 | }); 11 | return ( 12 | 13 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | ); 43 | } 44 | 45 | export default GraphHOCC({ 46 | graphTitle: 'JS 错误率', 47 | })(JsError); 48 | -------------------------------------------------------------------------------- /src/components/GraphHOC/JsErrorCluster.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | // 引入组件 3 | import { Table } from 'antd'; 4 | import GraphHOCC from '@/components/GraphHOC'; 5 | 6 | function JsErrorCluster(props: any) { 7 | const jsErrorClusterData: any[] = []; 8 | const jsErrorClusterCol: any[] = [ 9 | { title: 'Msg', dataIndex: 'msg' }, 10 | { title: '错误次数', dataIndex: 'count', width: 90 }, 11 | { title: 'UV', dataIndex: 'uv', width: 90 }, 12 | ]; 13 | props.graphData.data.data.forEach((propsDataItem: any, propsDataIndex: number) => { 14 | jsErrorClusterData.push({ 15 | key: propsDataIndex + 1, 16 | msg: propsDataItem.msg, 17 | count: propsDataItem.count, 18 | uv: propsDataItem.uv, 19 | }); 20 | }); 21 | return
; 22 | } 23 | 24 | export default GraphHOCC({ 25 | graphTitle: 'JS 错误聚类', 26 | })(JsErrorCluster); 27 | -------------------------------------------------------------------------------- /src/components/GraphHOC/PvUv.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | // 引入组件 3 | import { Chart, Geom, Axis, Tooltip, Legend } from 'bizcharts'; 4 | import GraphHOCC from '@/components/GraphHOC'; 5 | 6 | function PvUv(props: any) { 7 | const pvUvData: any[] = []; 8 | const { handleFormatTime } = props; 9 | props.graphData.data.data.forEach((propsDataItem: any) => { 10 | const time = handleFormatTime(propsDataItem.date); 11 | pvUvData.push( 12 | { type: 'pv', time, value: propsDataItem.pv }, 13 | { type: 'uv', time, value: propsDataItem.uv }, 14 | ); 15 | }); 16 | return ( 17 | 18 | 19 | 20 | 21 | 22 | 23 | 34 | 35 | ); 36 | } 37 | 38 | export default GraphHOCC({ 39 | graphTitle: 'PV/UV', 40 | })(PvUv); 41 | -------------------------------------------------------------------------------- /src/components/GraphHOC/style.less: -------------------------------------------------------------------------------- 1 | div.graphContainer { 2 | width: 100%; 3 | margin-bottom: 20px; 4 | .graphContainerHeader { 5 | height: 40px; 6 | padding-left: 10px; 7 | color: #fff; 8 | line-height: 40px; 9 | background-color: #001529; 10 | border-bottom: none; 11 | border-radius: 4px 4px 0 0; 12 | .graphHeaderConfig { 13 | float: right; 14 | height: 40px; 15 | .graphCommonSelect { 16 | padding: 0 20px; 17 | font-size: 15px; 18 | line-height: 40px; 19 | text-align: center; 20 | border-left: 1px solid #fff; 21 | cursor: pointer; 22 | } 23 | } 24 | } 25 | .graphContainerContent { 26 | padding: 20px; 27 | background-color: #fff; 28 | .graphContainerSpin { 29 | width: 100%; 30 | height: 240px; 31 | line-height: 240px; 32 | text-align: center; 33 | } 34 | .graphContainerNoData { 35 | height: 240px; 36 | div { 37 | position: relative; 38 | top: 50%; 39 | text-align: center; 40 | transform: translateY(-50%); 41 | p { 42 | margin-top: 10px; 43 | } 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /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/NoticeList.tsx: -------------------------------------------------------------------------------- 1 | import { Avatar, List } from 'antd'; 2 | 3 | import React from 'react'; 4 | import classNames from 'classnames'; 5 | import { NoticeIconData } from './index'; 6 | import styles from './NoticeList.less'; 7 | 8 | export interface NoticeIconTabProps { 9 | count?: number; 10 | name?: string; 11 | showClear?: boolean; 12 | showViewMore?: boolean; 13 | style?: React.CSSProperties; 14 | title: string; 15 | tabKey: string; 16 | data?: NoticeIconData[]; 17 | onClick?: (item: NoticeIconData) => void; 18 | onClear?: () => void; 19 | emptyText?: string; 20 | clearText?: string; 21 | viewMoreText?: string; 22 | list: NoticeIconData[]; 23 | onViewMore?: (e: any) => void; 24 | } 25 | const NoticeList: React.SFC = ({ 26 | data = [], 27 | onClick, 28 | onClear, 29 | title, 30 | onViewMore, 31 | emptyText, 32 | showClear = true, 33 | clearText, 34 | viewMoreText, 35 | showViewMore = false, 36 | }) => { 37 | if (data.length === 0) { 38 | return ( 39 |
40 | not found 44 |
{emptyText}
45 |
46 | ); 47 | } 48 | return ( 49 |
50 | 51 | className={styles.list} 52 | dataSource={data} 53 | renderItem={(item, i) => { 54 | const itemCls = classNames(styles.item, { 55 | [styles.read]: item.read, 56 | }); 57 | // eslint-disable-next-line no-nested-ternary 58 | const leftIcon = item.avatar ? ( 59 | typeof item.avatar === 'string' ? ( 60 | 61 | ) : ( 62 | {item.avatar} 63 | ) 64 | ) : null; 65 | 66 | return ( 67 | onClick && onClick(item)} 71 | > 72 | 77 | {item.title} 78 |
{item.extra}
79 |
80 | } 81 | description={ 82 |
83 |
{item.description}
84 |
{item.datetime}
85 |
86 | } 87 | /> 88 | 89 | ); 90 | }} 91 | /> 92 |
93 | {showClear ? ( 94 |
95 | {clearText} {title} 96 |
97 | ) : null} 98 | {showViewMore ? ( 99 |
{ 101 | if (onViewMore) { 102 | onViewMore(e); 103 | } 104 | }} 105 | > 106 | {viewMoreText} 107 |
108 | ) : null} 109 |
110 | 111 | ); 112 | }; 113 | 114 | export default NoticeList; 115 | -------------------------------------------------------------------------------- /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/OptionList/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | // 引入组件 3 | import { Form, Icon, Input, Button, Tabs } from 'antd'; 4 | // 引入样式 5 | import styles from './style.less'; 6 | 7 | const { TabPane } = Tabs; 8 | interface OptionListProps { 9 | listTitle?: string; 10 | isNeedSearch?: boolean; // 是否展示查询表单UI 11 | isNeedTab?: boolean; // 是否需要Tab展示 12 | listData: { 13 | // 选项列表数据 14 | isActive: boolean; 15 | name: string; 16 | resultNum: string; 17 | }[]; 18 | listTabConfig?: { 19 | name: string; 20 | title: string; 21 | }[]; 22 | handleClickListItem(listDataItem: any): void; // 点击列表选项回调处理 23 | handleClickTabItem?(listTabItem: any): void; // 点击Tab选项回调处理 24 | } 25 | 26 | function OptionList(props: OptionListProps) { 27 | const { 28 | listTitle, 29 | isNeedSearch, 30 | isNeedTab, 31 | listData, 32 | listTabConfig, 33 | handleClickListItem, 34 | handleClickTabItem, 35 | } = props; 36 | const listContent = ( 37 | <> 38 | {/* 标题 */} 39 | {listTitle &&

{listTitle}

} 40 | {/* 查询表单UI */} 41 | {isNeedSearch && ( 42 |
43 | 44 | 47 | 48 | )} 49 | {/* 选项列表UI */} 50 | {listData.length ? ( 51 |
    52 | {listData.map(listDataItem => ( 53 |
  • { 57 | handleClickListItem(listDataItem); 58 | }} 59 | > 60 | {listDataItem.name} 61 | {listDataItem.resultNum} 62 |
  • 63 | ))} 64 |
65 | ) : ( 66 |
67 | 68 |

暂无数据

69 |
70 | )} 71 | 72 | ); 73 | return ( 74 |
75 | {!isNeedTab ? ( 76 | listContent 77 | ) : ( 78 | 79 | {listTabConfig && 80 | listTabConfig.map(tabItem => ( 81 | 86 | {listContent} 87 | 88 | ))} 89 | 90 | )} 91 |
92 | ); 93 | } 94 | 95 | export default OptionList; 96 | -------------------------------------------------------------------------------- /src/components/OptionList/style.less: -------------------------------------------------------------------------------- 1 | div.optionListContainer { 2 | flex: 0 0 auto; 3 | width: 382px; 4 | overflow-y: scroll; 5 | border-right: 1px solid #cecece; 6 | p { 7 | font-weight: bold; 8 | } 9 | .listSeachInput { 10 | width: 290px; 11 | } 12 | .listSearchButton { 13 | width: 57px; 14 | margin-left: 3px; 15 | color: #fff; 16 | background: #1890ff; 17 | } 18 | .listContent { 19 | width: 352px; 20 | padding: 0; 21 | li { 22 | display: block; 23 | height: 40px; 24 | margin-top: 20px; 25 | padding: 0 10px; 26 | line-height: 40px; 27 | // background-color: #f6fafe; 28 | background-color: #fff; 29 | border-radius: 4px; 30 | cursor: pointer; 31 | span { 32 | &:first-child { 33 | float: left; 34 | max-width: 220px; 35 | overflow: hidden; 36 | white-space: nowrap; 37 | text-overflow: ellipsis; 38 | } 39 | &:last-child { 40 | float: right; 41 | } 42 | } 43 | &:hover { 44 | color: #fff; 45 | background-color: #90c9ff; 46 | } 47 | &.active { 48 | color: #fff; 49 | background-color: #1890ff; 50 | } 51 | } 52 | } 53 | .listNoData { 54 | position: relative; 55 | left: 50%; 56 | display: inline-block; 57 | margin: 100px auto 0; 58 | text-align: center; 59 | transform: translateX(-50%); 60 | p { 61 | margin-top: 10px; 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /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/PageVariateForm/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Drawer, Button, Form, Input } from 'antd'; 3 | import { FormComponentProps } from 'antd/es/form'; 4 | 5 | interface IProps { 6 | name: string; 7 | path: string; 8 | form: FormComponentProps['form']; 9 | visible: boolean; 10 | onClose: () => void; 11 | handleSubmit: (values) => void; 12 | } 13 | 14 | const PageVariateForm: React.FC = props => { 15 | const { getFieldDecorator, getFieldsError } = props.form; 16 | const { name, path, visible, onClose, handleSubmit } = props; 17 | 18 | const hasErrors = fieldsError => Object.keys(fieldsError).some(field => fieldsError[field]); 19 | 20 | function submit(e) { 21 | e.preventDefault(); 22 | props.form.validateFields(async (err, values) => { 23 | if (!err) { 24 | props.form.resetFields(); 25 | handleSubmit(values); 26 | } 27 | }); 28 | } 29 | 30 | return ( 31 | 32 |
33 | 34 | {getFieldDecorator('name', { 35 | initialValue: name, 36 | rules: [{ required: true, message: '请输入页面名称!', type: 'string' }], 37 | })()} 38 | 39 | 40 | {getFieldDecorator('path', { 41 | initialValue: path, 42 | })()} 45 | 46 | 47 | 50 | 51 | 52 |
53 | ); 54 | }; 55 | 56 | export default Form.create()(PageVariateForm); 57 | -------------------------------------------------------------------------------- /src/components/SelectDatetime/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 | .btn { 26 | padding: 0 !important; 27 | } 28 | .row { 29 | margin-bottom: 10px; 30 | } 31 | -------------------------------------------------------------------------------- /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/SelectProject/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/SelectProject/index.tsx: -------------------------------------------------------------------------------- 1 | import { Icon, Menu, Dropdown } from 'antd'; 2 | import { ClickParam } from 'antd/es/menu'; 3 | import React from 'react'; 4 | import styles from './index.less'; 5 | 6 | interface SelectProjectProps { 7 | className?: string; 8 | projectList: IProjectType[]; 9 | value: string; 10 | onChange: ({ key }: ClickParam) => void; 11 | } 12 | const SelectProject: React.FC = props => { 13 | const { projectList, value, onChange } = props; 14 | 15 | const project = projectList.find(item => item.token === value); 16 | const projectMenu = ( 17 | 18 | {projectList.map(item => ( 19 | {item.project_name} 20 | ))} 21 | 22 | ); 23 | return ( 24 | 25 | 26 | {project ? project.project_name : '请选择'} 27 | 28 | 29 | ); 30 | }; 31 | 32 | export default SelectProject; 33 | -------------------------------------------------------------------------------- /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 | return lightens.concat(colorPalettes); 15 | }, 16 | changeColor(color?: string): Promise { 17 | if (!color) { 18 | return Promise.resolve(); 19 | } 20 | const options = { 21 | // new colors array, one-to-one corresponde with `matchColors` 22 | newColors: this.getAntdSerials(color), 23 | changeUrl(cssUrl: string): string { 24 | // while router is not `hash` mode, it needs absolute path 25 | return `/${cssUrl}`; 26 | }, 27 | }; 28 | return client.changer.changeColor(options, Promise); 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /src/config/config.local.ts: -------------------------------------------------------------------------------- 1 | const { MOCK } = process.env; 2 | 3 | export default { 4 | requestPrefix: MOCK === 'none' ? 'http://127.0.0.1:7002' : 'http://127.0.0.1:7002', 5 | }; 6 | -------------------------------------------------------------------------------- /src/config/config.prod.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | requestPrefix: 'http://127.0.0.1:7002', 3 | }; 4 | -------------------------------------------------------------------------------- /src/config/index.ts: -------------------------------------------------------------------------------- 1 | import localConfig from './config.local'; 2 | import prodConfig from './config.prod'; 3 | 4 | const config = process.env.NODE_ENV !== 'production' ? localConfig : prodConfig; 5 | 6 | export default { 7 | ...config, 8 | }; 9 | -------------------------------------------------------------------------------- /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/HomeLayout.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | .header { 4 | padding: 0; 5 | background: #fff; 6 | box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08); 7 | } 8 | 9 | .logo { 10 | display: inline-block; 11 | height: 32px; 12 | margin-right: 24px; 13 | margin-left: 24px; 14 | vertical-align: middle; 15 | } 16 | .avatar { 17 | display: flex; 18 | justify-content: flex-end; 19 | padding-right: 24px; 20 | } 21 | .content { 22 | margin: 24px; 23 | padding-top: 0; 24 | background-color: #fff; 25 | } 26 | -------------------------------------------------------------------------------- /src/layouts/HomeLayout.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Ant Design Pro v4 use `@ant-design/pro-layout` to handle Layout. 3 | * You can view component api by: 4 | * https://github.com/ant-design/ant-design-pro-layout 5 | */ 6 | import { Icon, Layout, Row, Col } from 'antd'; 7 | import React, { useEffect } from 'react'; 8 | import { Dispatch } from 'redux'; 9 | import { connect } from 'dva'; 10 | 11 | import { ConnectState } from '@/models/connect'; 12 | import logo from '@/assets/logo.svg'; 13 | import styles from './HomeLayout.less'; 14 | import Avatar from '@/components/GlobalHeader/AvatarDropdown'; 15 | 16 | const { Header, Footer, Content } = Layout; 17 | 18 | export interface BasicLayoutProps { 19 | dispatch: Dispatch; 20 | } 21 | 22 | const footerRender: BasicLayoutProps['footerRender'] = _ => ( 23 | <> 24 |
30 | Copyright 2019 bombayjs出品 31 |
32 | 33 | ); 34 | 35 | const BasicLayout: React.FC = props => { 36 | const { dispatch, children } = props; 37 | /** 38 | * constructor 39 | */ 40 | 41 | useEffect(() => { 42 | if (dispatch) { 43 | dispatch({ 44 | type: 'user/fetchCurrent', 45 | }); 46 | } 47 | }, []); 48 | 49 | return ( 50 | 51 |
52 | 53 |
54 | logo 55 | Bombayjs前端监控平台 56 | 57 | 58 | 59 | 60 | 61 | 62 | {children} 63 |
{footerRender()}
64 | 65 | ); 66 | }; 67 | 68 | export default connect(({ global, settings }: ConnectState) => ({ 69 | collapsed: global.collapsed, 70 | settings, 71 | }))(BasicLayout); 72 | -------------------------------------------------------------------------------- /src/layouts/SecurityLayout.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'dva'; 3 | import { Redirect } from 'umi'; 4 | import { ConnectState, ConnectProps } from '@/models/connect'; 5 | import { CurrentUser } from '@/models/user'; 6 | import PageLoading from '@/components/PageLoading'; 7 | 8 | interface SecurityLayoutProps extends ConnectProps { 9 | loading: boolean; 10 | currentUser: CurrentUser; 11 | } 12 | 13 | interface SecurityLayoutState { 14 | isReady: boolean; 15 | } 16 | 17 | class SecurityLayout extends React.Component { 18 | state: SecurityLayoutState = { 19 | isReady: false, 20 | }; 21 | 22 | componentDidMount() { 23 | this.setState({ 24 | isReady: true, 25 | }); 26 | const { dispatch } = this.props; 27 | if (dispatch) { 28 | dispatch({ 29 | type: 'user/fetchCurrent', 30 | }); 31 | } 32 | } 33 | 34 | render() { 35 | const { isReady } = this.state; 36 | const { children, loading, currentUser } = this.props; 37 | if ((!currentUser.id && loading) || !isReady) { 38 | return ; 39 | } 40 | if (!currentUser.id) { 41 | return ; 42 | } 43 | return children; 44 | } 45 | } 46 | 47 | export default connect(({ user, loading }: ConnectState) => ({ 48 | currentUser: user.currentUser, 49 | loading: loading.models.user, 50 | }))(SecurityLayout); 51 | -------------------------------------------------------------------------------- /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 | console.log(routes, getMenuData(routes)); 32 | return ( 33 | 41 |
42 |
43 | 44 |
45 |
46 |
47 |
48 | 49 | {/* logo */} 50 | Bombayjs Admin 51 | 52 |
53 |
Bombayjs Admin是Bombayjs前端监控管理后台
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 | import viewdetail from './en-US/viewdetail'; 8 | 9 | export default { 10 | 'navBar.lang': 'Languages', 11 | 'layout.user.link.help': 'Help', 12 | 'layout.user.link.privacy': 'Privacy', 13 | 'layout.user.link.terms': 'Terms', 14 | 'app.preview.down.block': 'Download this page to your local project', 15 | 'app.welcome.link.fetch-blocks': 'Get all block', 16 | 'app.welcome.link.block-list': 'Quickly build standard, pages based on `block` development', 17 | ...globalHeader, 18 | ...menu, 19 | ...settingDrawer, 20 | ...settings, 21 | ...pwa, 22 | ...component, 23 | ...viewdetail, 24 | }; 25 | -------------------------------------------------------------------------------- /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': 'Analysis', 10 | 'menu.dashboard.monitor': 'Monitor', 11 | 'menu.dashboard.workplace': 'Workplace', 12 | 'menu.exception.403': '403', 13 | 'menu.exception.404': '404', 14 | 'menu.exception.500': '500', 15 | 'menu.form': 'Form', 16 | 'menu.form.basic-form': 'Basic Form', 17 | 'menu.form.step-form': 'Step Form', 18 | 'menu.form.step-form.info': 'Step Form(write transfer information)', 19 | 'menu.form.step-form.confirm': 'Step Form(confirm transfer information)', 20 | 'menu.form.step-form.result': 'Step Form(finished)', 21 | 'menu.form.advanced-form': 'Advanced Form', 22 | 'menu.list': 'List', 23 | 'menu.list.table-list': 'Search Table', 24 | 'menu.list.basic-list': 'Basic List', 25 | 'menu.list.card-list': 'Card List', 26 | 'menu.list.search-list': 'Search List', 27 | 'menu.list.search-list.articles': 'Search List(articles)', 28 | 'menu.list.search-list.projects': 'Search List(projects)', 29 | 'menu.list.search-list.applications': 'Search List(applications)', 30 | 'menu.profile': 'Profile', 31 | 'menu.profile.basic': 'Basic Profile', 32 | 'menu.profile.advanced': 'Advanced Profile', 33 | 'menu.result': 'Result', 34 | 'menu.result.success': 'Success', 35 | 'menu.result.fail': 'Fail', 36 | 'menu.exception': 'Exception', 37 | 'menu.exception.not-permission': '403', 38 | 'menu.exception.not-find': '404', 39 | 'menu.exception.server-error': '500', 40 | 'menu.exception.trigger': 'Trigger', 41 | 'menu.account': 'Account', 42 | 'menu.account.center': 'Account Center', 43 | 'menu.account.settings': 'Account Settings', 44 | 'menu.account.trigger': 'Trigger Error', 45 | 'menu.account.logout': 'Logout', 46 | 'menu.editor': 'Graphic Editor', 47 | 'menu.editor.flow': 'Flow Editor', 48 | 'menu.editor.mind': 'Mind Editor', 49 | 'menu.editor.koni': 'Koni Editor', 50 | 51 | 'menu.setting': 'setting', 52 | 'menu.technology': 'Technical operation', 53 | 'menu.technology.application': 'Application', 54 | 'menu.technology.latitude': 'Latitude', 55 | 'menu.technology.latitude.url': 'Url', 56 | 'menu.technology.latitude.geography': 'Geography', 57 | 'menu.technology.latitude.terminal': 'Terminal', 58 | 'menu.technology.latitude.network': 'Network', 59 | 'menu.viewdetail': 'View Detail', 60 | }; 61 | -------------------------------------------------------------------------------- /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/en-US/settings.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.settings.menuMap.basic': 'Basic Settings', 3 | 'app.settings.menuMap.security': 'Security Settings', 4 | 'app.settings.menuMap.binding': 'Account Binding', 5 | 'app.settings.menuMap.notification': 'New Message Notification', 6 | 'app.settings.basic.avatar': 'Avatar', 7 | 'app.settings.basic.change-avatar': 'Change avatar', 8 | 'app.settings.basic.email': 'Email', 9 | 'app.settings.basic.email-message': 'Please input your email!', 10 | 'app.settings.basic.nickname': 'Nickname', 11 | 'app.settings.basic.nickname-message': 'Please input your Nickname!', 12 | 'app.settings.basic.profile': 'Personal profile', 13 | 'app.settings.basic.profile-message': 'Please input your personal profile!', 14 | 'app.settings.basic.profile-placeholder': 'Brief introduction to yourself', 15 | 'app.settings.basic.country': 'Country/Region', 16 | 'app.settings.basic.country-message': 'Please input your country!', 17 | 'app.settings.basic.geographic': 'Province or city', 18 | 'app.settings.basic.geographic-message': 'Please input your geographic info!', 19 | 'app.settings.basic.address': 'Street Address', 20 | 'app.settings.basic.address-message': 'Please input your address!', 21 | 'app.settings.basic.phone': 'Phone Number', 22 | 'app.settings.basic.phone-message': 'Please input your phone!', 23 | 'app.settings.basic.update': 'Update Information', 24 | 'app.settings.security.strong': 'Strong', 25 | 'app.settings.security.medium': 'Medium', 26 | 'app.settings.security.weak': 'Weak', 27 | 'app.settings.security.password': 'Account Password', 28 | 'app.settings.security.password-description': 'Current password strength', 29 | 'app.settings.security.phone': 'Security Phone', 30 | 'app.settings.security.phone-description': 'Bound phone', 31 | 'app.settings.security.question': 'Security Question', 32 | 'app.settings.security.question-description': 33 | 'The security question is not set, and the security policy can effectively protect the account security', 34 | 'app.settings.security.email': 'Backup Email', 35 | 'app.settings.security.email-description': 'Bound Email', 36 | 'app.settings.security.mfa': 'MFA Device', 37 | 'app.settings.security.mfa-description': 38 | 'Unbound MFA device, after binding, can be confirmed twice', 39 | 'app.settings.security.modify': 'Modify', 40 | 'app.settings.security.set': 'Set', 41 | 'app.settings.security.bind': 'Bind', 42 | 'app.settings.binding.taobao': 'Binding Taobao', 43 | 'app.settings.binding.taobao-description': 'Currently unbound Taobao account', 44 | 'app.settings.binding.alipay': 'Binding Alipay', 45 | 'app.settings.binding.alipay-description': 'Currently unbound Alipay account', 46 | 'app.settings.binding.dingding': 'Binding DingTalk', 47 | 'app.settings.binding.dingding-description': 'Currently unbound DingTalk account', 48 | 'app.settings.binding.bind': 'Bind', 49 | 'app.settings.notification.password': 'Account Password', 50 | 'app.settings.notification.password-description': 51 | 'Messages from other users will be notified in the form of a station letter', 52 | 'app.settings.notification.messages': 'System Messages', 53 | 'app.settings.notification.messages-description': 54 | 'System messages will be notified in the form of a station letter', 55 | 'app.settings.notification.todo': 'To-do Notification', 56 | 'app.settings.notification.todo-description': 57 | 'The to-do list will be notified in the form of a letter from the station', 58 | 'app.settings.open': 'Open', 59 | 'app.settings.close': 'Close', 60 | }; 61 | -------------------------------------------------------------------------------- /src/locales/en-US/viewdetail.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'viewdetail.alllog.pageurl': 'Page Url', 3 | 'viewdetail.alllog.uid': 'UID', 4 | 'viewdetail.alllog.logType': 'Log Types', 5 | 'viewdetail.alllog.select_enter_condition': 'Select or enter conditions', 6 | 'viewdetail.alllog.device': 'Device', 7 | 'viewdetail.alllog.jsfile_url': 'JS file URL', 8 | 'viewdetail.alllog.js_errmsg': 'JS error message', 9 | 'viewdetail.alllog.page_full_loadtime': 'Page full load time(ms)', 10 | 'viewdetail.alllog.geography': 'Geography', 11 | 'viewdetail.alllog.api_requesturl': 'API request URL', 12 | 'viewdetail.alllog.api_request_time_exceeded': 'API request time exceeded(ms)', 13 | 'viewdetail.alllog.apimsg': 'APImessage', 14 | 'viewdetail.alllog.api_report_error': 'Whether the API reports an error', 15 | 'viewdetail.alllog.traceid': 'traceId', 16 | 'viewdetail.alllog.code': 'code', 17 | 'viewdetail.alllog.browser': 'Browser', 18 | 'viewdetail.alllog.oper_sys': 'Operating system', 19 | 'viewdetail.alllog.resolution': 'Resolution', 20 | 'viewdetail.alllog.client': 'Client', 21 | 'viewdetail.alllog.referrer': 'Referrer', 22 | 'viewdetail.alllog.ip': 'IP', 23 | 'viewdetail.alllog.sid': 'Sid', 24 | 'viewdetail.alllog.tag': 'Tag', 25 | 'viewdetail.alllog.table.titleColumns.time': 'Time', 26 | 'viewdetail.alllog.table.titleColumns.logTime': 'Log Types', 27 | 'viewdetail.alllog.table.titleColumns.pageUrl': 'Page Url', 28 | 'viewdetail.alllog.table.titleColumns.uid': 'UID', 29 | 'viewdetail.alllog.table.titleColumns.device': 'Device', 30 | 'viewdetail.alllog.table.titleColumns.jsFileUrl': 'JS file URL', 31 | 'viewdetail.alllog.table.titleColumns.jsErrMsg': 'JS error message', 32 | 'viewdetail.alllog.table.titleColumns.pageLoadTime': 'Page full load time(ms)', 33 | 'viewdetail.alllog.table.titleColumns.geography': 'Geography', 34 | 'viewdetail.alllog.table.titleColumns.apiReqUrl': 'API request URL', 35 | 'viewdetail.alllog.table.titleColumns.apiReponseTime': 'API response time(ms)', 36 | 'viewdetail.alllog.table.titleColumns.apiMsg': 'APImessage', 37 | 'viewdetail.alllog.table.titleColumns.apiWhether': 'Whether the API reports an error', 38 | 'viewdetail.alllog.table.titleColumns.traceId': 'traceId', 39 | 'viewdetail.alllog.table.titleColumns.code': 'code', 40 | 'viewdetail.alllog.table.titleColumns.browser': 'Browser', 41 | 'viewdetail.alllog.table.titleColumns.osys': 'Operating system', 42 | 'viewdetail.alllog.table.titleColumns.resolution': 'Resolution', 43 | 'viewdetail.alllog.table.titleColumns.client': 'Client', 44 | 'viewdetail.alllog.table.titleColumns.ref': 'Referrer', 45 | 'viewdetail.alllog.table.titleColumns.ip': 'IP', 46 | 'viewdetail.alllog.table.titleColumns.sid': 'sid', 47 | 'viewdetail.alllog.table.titleColumns.alllog': 'All Log', 48 | 'viewdetail.alllog.table.titleColumns.tag': 'Tag', 49 | 'viewdetail.alllog.table.titleColumns.customLog': 'Custom Log', 50 | 'viewdetail.alllog.table.titleColumns.operation': 'Operation', 51 | }; 52 | -------------------------------------------------------------------------------- /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/pt-BR/settings.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.settings.menuMap.basic': 'Configurações Básicas', 3 | 'app.settings.menuMap.security': 'Configurações de Segurança', 4 | 'app.settings.menuMap.binding': 'Vinculação de Conta', 5 | 'app.settings.menuMap.notification': 'Mensagens de Notificação', 6 | 'app.settings.basic.avatar': 'Avatar', 7 | 'app.settings.basic.change-avatar': 'Alterar avatar', 8 | 'app.settings.basic.email': 'Email', 9 | 'app.settings.basic.email-message': 'Por favor insira seu email!', 10 | 'app.settings.basic.nickname': 'Nome de usuário', 11 | 'app.settings.basic.nickname-message': 'Por favor insira seu nome de usuário!', 12 | 'app.settings.basic.profile': 'Perfil pessoal', 13 | 'app.settings.basic.profile-message': 'Por favor insira seu perfil pessoal!', 14 | 'app.settings.basic.profile-placeholder': 'Breve introdução sua', 15 | 'app.settings.basic.country': 'País/Região', 16 | 'app.settings.basic.country-message': 'Por favor insira país!', 17 | 'app.settings.basic.geographic': 'Província, estado ou cidade', 18 | 'app.settings.basic.geographic-message': 'Por favor insira suas informações geográficas!', 19 | 'app.settings.basic.address': 'Endereço', 20 | 'app.settings.basic.address-message': 'Por favor insira seu endereço!', 21 | 'app.settings.basic.phone': 'Número de telefone', 22 | 'app.settings.basic.phone-message': 'Por favor insira seu número de telefone!', 23 | 'app.settings.basic.update': 'Atualizar Informações', 24 | 'app.settings.security.strong': 'Forte', 25 | 'app.settings.security.medium': 'Média', 26 | 'app.settings.security.weak': 'Fraca', 27 | 'app.settings.security.password': 'Senha da Conta', 28 | 'app.settings.security.password-description': 'Força da senha', 29 | 'app.settings.security.phone': 'Telefone de Seguraça', 30 | 'app.settings.security.phone-description': 'Telefone vinculado', 31 | 'app.settings.security.question': 'Pergunta de Segurança', 32 | 'app.settings.security.question-description': 33 | 'A pergunta de segurança não está definida e a política de segurança pode proteger efetivamente a segurança da conta', 34 | 'app.settings.security.email': 'Email de Backup', 35 | 'app.settings.security.email-description': 'Email vinculado', 36 | 'app.settings.security.mfa': 'Dispositivo MFA', 37 | 'app.settings.security.mfa-description': 38 | 'O dispositivo MFA não vinculado, após a vinculação, pode ser confirmado duas vezes', 39 | 'app.settings.security.modify': 'Modificar', 40 | 'app.settings.security.set': 'Atribuir', 41 | 'app.settings.security.bind': 'Vincular', 42 | 'app.settings.binding.taobao': 'Vincular Taobao', 43 | 'app.settings.binding.taobao-description': 'Atualmente não vinculado à conta Taobao', 44 | 'app.settings.binding.alipay': 'Vincular Alipay', 45 | 'app.settings.binding.alipay-description': 'Atualmente não vinculado à conta Alipay', 46 | 'app.settings.binding.dingding': 'Vincular DingTalk', 47 | 'app.settings.binding.dingding-description': 'Atualmente não vinculado à conta DingTalk', 48 | 'app.settings.binding.bind': 'Vincular', 49 | 'app.settings.notification.password': 'Senha da Conta', 50 | 'app.settings.notification.password-description': 51 | 'Mensagens de outros usuários serão notificadas na forma de uma estação de letra', 52 | 'app.settings.notification.messages': 'Mensagens de Sistema', 53 | 'app.settings.notification.messages-description': 54 | 'Mensagens de sistema serão notificadas na forma de uma estação de letra', 55 | 'app.settings.notification.todo': 'Notificação de To-do', 56 | 'app.settings.notification.todo-description': 57 | 'A lista de to-do será notificada na forma de uma estação de letra', 58 | 'app.settings.open': 'Aberto', 59 | 'app.settings.close': 'Fechado', 60 | }; 61 | -------------------------------------------------------------------------------- /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 | import viewdetail from './zh-CN/viewdetail'; 8 | 9 | export default { 10 | 'navBar.lang': '语言', 11 | 'layout.user.link.help': '帮助', 12 | 'layout.user.link.privacy': '隐私', 13 | 'layout.user.link.terms': '条款', 14 | 'app.preview.down.block': '下载此页面到本地项目', 15 | 'app.welcome.link.fetch-blocks': '获取全部区块', 16 | 'app.welcome.link.block-list': '基于 block 开发,快速构建标准页面', 17 | ...globalHeader, 18 | ...menu, 19 | ...settingDrawer, 20 | ...settings, 21 | ...pwa, 22 | ...component, 23 | ...viewdetail, 24 | }; 25 | -------------------------------------------------------------------------------- /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.dashboard.monitor': '监控页', 11 | 'menu.dashboard.workplace': '工作台', 12 | 'menu.exception.403': '403', 13 | 'menu.exception.404': '404', 14 | 'menu.exception.500': '500', 15 | 'menu.form': '表单页', 16 | 'menu.form.basic-form': '基础表单', 17 | 'menu.form.step-form': '分步表单', 18 | 'menu.form.step-form.info': '分步表单(填写转账信息)', 19 | 'menu.form.step-form.confirm': '分步表单(确认转账信息)', 20 | 'menu.form.step-form.result': '分步表单(完成)', 21 | 'menu.form.advanced-form': '高级表单', 22 | 'menu.list': '列表页', 23 | 'menu.list.table-list': '查询表格', 24 | 'menu.list.basic-list': '标准列表', 25 | 'menu.list.card-list': '卡片列表', 26 | 'menu.list.search-list': '搜索列表', 27 | 'menu.list.search-list.articles': '搜索列表(文章)', 28 | 'menu.list.search-list.projects': '搜索列表(项目)', 29 | 'menu.list.search-list.applications': '搜索列表(应用)', 30 | 'menu.profile': '详情页', 31 | 'menu.profile.basic': '基础详情页', 32 | 'menu.profile.advanced': '高级详情页', 33 | 'menu.result': '结果页', 34 | 'menu.result.success': '成功页', 35 | 'menu.result.fail': '失败页', 36 | 'menu.exception': '异常页', 37 | 'menu.exception.not-permission': '403', 38 | 'menu.exception.not-find': '404', 39 | 'menu.exception.server-error': '500', 40 | 'menu.exception.trigger': '触发错误', 41 | 'menu.account': '个人页', 42 | 'menu.account.center': '个人中心', 43 | 'menu.account.settings': '个人设置', 44 | 'menu.account.trigger': '触发报错', 45 | 'menu.account.logout': '退出登录', 46 | 'menu.editor': '图形编辑器', 47 | 'menu.editor.flow': '流程编辑器', 48 | 'menu.editor.mind': '脑图编辑器', 49 | 'menu.editor.koni': '拓扑编辑器', 50 | 51 | 'menu.setting': '项目设置', 52 | 'menu.product': '产品运营', 53 | 'menu.product.circle': '圈选', 54 | 'menu.product.eventvariate': '事件管理', 55 | 'menu.technology': '技术运营', 56 | 'menu.technology.application': '应用', 57 | 'menu.technology.latitude': '纬度', 58 | 'menu.technology.latitude.url': '页面', 59 | 'menu.product.group': '用户分群', 60 | 'menu.technology.latitude.geography': '地理', 61 | 'menu.technology.latitude.terminal': '终端', 62 | 'menu.technology.latitude.network': '网络', 63 | 'menu.viewdetail': '访问明细', 64 | }; 65 | -------------------------------------------------------------------------------- /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-CN/viewdetail.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'viewdetail.alllog.pageurl': '页面地址', 3 | 'viewdetail.alllog.uid': 'UID', 4 | 'viewdetail.alllog.logType': '日志类型', 5 | 'viewdetail.alllog.select_enter_condition': '选择或输入条件', 6 | 'viewdetail.alllog.device': '设备', 7 | 'viewdetail.alllog.jsfile_url': 'JS文件URL', 8 | 'viewdetail.alllog.js_errmsg': 'JS错误消息', 9 | 'viewdetail.alllog.page_full_loadtime': '页面完全加载时间(ms)', 10 | 'viewdetail.alllog.geography': '地域', 11 | 'viewdetail.alllog.api_requesturl': 'API请求URL', 12 | 'viewdetail.alllog.api_request_time_exceeded': 'API请求时间超过(ms)', 13 | 'viewdetail.alllog.apimsg': 'API消息', 14 | 'viewdetail.alllog.api_report_error': 'API是否报错', 15 | 'viewdetail.alllog.traceid': 'traceId', 16 | 'viewdetail.alllog.code': 'code', 17 | 'viewdetail.alllog.browser': '浏览器', 18 | 'viewdetail.alllog.oper_sys': '操作系统', 19 | 'viewdetail.alllog.resolution': '分辨率', 20 | 'viewdetail.alllog.client': '客户端', 21 | 'viewdetail.alllog.referrer': '页面来源(Referrer)', 22 | 'viewdetail.alllog.ip': 'IP', 23 | 'viewdetail.alllog.sid': 'Sid', 24 | 'viewdetail.alllog.tag': 'Tag', 25 | 'viewdetail.alllog.table.titleColumns.time': '时间', 26 | 'viewdetail.alllog.table.titleColumns.logTime': '日志类型', 27 | 'viewdetail.alllog.table.titleColumns.pageUrl': '页面地址', 28 | 'viewdetail.alllog.table.titleColumns.uid': 'UID', 29 | 'viewdetail.alllog.table.titleColumns.device': '设备', 30 | 'viewdetail.alllog.table.titleColumns.jsFileUrl': 'JS文件URL', 31 | 'viewdetail.alllog.table.titleColumns.jsErrMsg': 'JS错误消息', 32 | 'viewdetail.alllog.table.titleColumns.pageLoadTime': '页面完全加载时间(ms)', 33 | 'viewdetail.alllog.table.titleColumns.geography': '地域', 34 | 'viewdetail.alllog.table.titleColumns.apiReqUrl': 'API请求URL', 35 | 'viewdetail.alllog.table.titleColumns.apiReponseTime': 'API请求时间(ms)', 36 | 'viewdetail.alllog.table.titleColumns.apiMsg': 'API消息', 37 | 'viewdetail.alllog.table.titleColumns.apiWhether': 'API是否报错', 38 | 'viewdetail.alllog.table.titleColumns.traceId': 'traceId', 39 | 'viewdetail.alllog.table.titleColumns.code': 'code', 40 | 'viewdetail.alllog.table.titleColumns.browser': '浏览器', 41 | 'viewdetail.alllog.table.titleColumns.osys': '操作系统', 42 | 'viewdetail.alllog.table.titleColumns.resolution': '分辨率', 43 | 'viewdetail.alllog.table.titleColumns.client': '客户端', 44 | 'viewdetail.alllog.table.titleColumns.ref': '页面来源(Referrer)', 45 | 'viewdetail.alllog.table.titleColumns.ip': 'IP', 46 | 'viewdetail.alllog.table.titleColumns.sid': 'sid', 47 | 'viewdetail.alllog.table.titleColumns.alllog': '完整日志', 48 | 'viewdetail.alllog.table.titleColumns.tag': 'Tag', 49 | 'viewdetail.alllog.table.titleColumns.customLog': '自定义日志', 50 | 'viewdetail.alllog.table.titleColumns.operation': '操作', 51 | }; 52 | -------------------------------------------------------------------------------- /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 | import { ProjectStateType } from './project'; 9 | 10 | export { GlobalModelState, SettingModelState, UserModelState }; 11 | 12 | export interface Loading { 13 | global: boolean; 14 | effects: { [key: string]: boolean | undefined }; 15 | models: { 16 | global?: boolean; 17 | menu?: boolean; 18 | setting?: boolean; 19 | user?: boolean; 20 | login?: boolean; 21 | project?: boolean; 22 | }; 23 | } 24 | 25 | export interface ConnectState { 26 | global: GlobalModelState; 27 | loading: Loading; 28 | settings: SettingModelState; 29 | user: UserModelState; 30 | login: LoginModelType; 31 | project: ProjectStateType; 32 | } 33 | 34 | export interface Route extends MenuDataItem { 35 | routes?: Route[]; 36 | } 37 | 38 | /** 39 | * @type T: Params matched in dynamic routing 40 | */ 41 | export interface ConnectProps extends Partial> { 42 | dispatch?: Dispatch; 43 | } 44 | -------------------------------------------------------------------------------- /src/models/login.ts: -------------------------------------------------------------------------------- 1 | import { Reducer } from 'redux'; 2 | import { routerRedux } from 'dva/router'; 3 | import { Effect } from 'dva'; 4 | import { stringify } from 'querystring'; 5 | 6 | import { fakeAccountLogin, getFakeCaptcha } from '@/services/login'; 7 | import { setAuthority, setToken } from '@/utils/authority'; 8 | import { getPageQuery } from '@/utils/utils'; 9 | 10 | type Status = 'ok' | 'error'; 11 | type CurrentAuthority = 'user' | 'guest' | 'admin'; 12 | 13 | export interface StateType { 14 | status?: Status; 15 | type?: string; 16 | currentAuthority?: CurrentAuthority; 17 | } 18 | 19 | export interface LoginModelType { 20 | namespace: string; 21 | state: StateType; 22 | effects: { 23 | login: Effect; 24 | getCaptcha: Effect; 25 | logout: Effect; 26 | }; 27 | reducers: { 28 | changeLoginStatus: Reducer; 29 | }; 30 | } 31 | 32 | const Model: LoginModelType = { 33 | namespace: 'login', 34 | 35 | state: { 36 | status: undefined, 37 | }, 38 | 39 | effects: { 40 | *login({ payload }, { call, put }) { 41 | const response = yield call(fakeAccountLogin, payload); 42 | console.log(response); 43 | yield put({ 44 | type: 'changeLoginStatus', 45 | payload: { ...response, type: payload.type }, 46 | }); 47 | // Login successfully 48 | if (response.code === 200) { 49 | const urlParams = new URL(window.location.href); 50 | const params = getPageQuery(); 51 | let { redirect } = params as { redirect: string }; 52 | if (redirect) { 53 | const redirectUrlParams = new URL(redirect); 54 | if (redirectUrlParams.origin === urlParams.origin) { 55 | redirect = redirect.substr(urlParams.origin.length); 56 | if (redirect.match(/^\/.*#/)) { 57 | redirect = redirect.substr(redirect.indexOf('#') + 1); 58 | } 59 | } else { 60 | window.location.href = redirect; 61 | return; 62 | } 63 | } 64 | yield put(routerRedux.replace(redirect || '/')); 65 | } 66 | }, 67 | 68 | *getCaptcha({ payload }, { call }) { 69 | yield call(getFakeCaptcha, payload); 70 | }, 71 | *logout(_, { put }) { 72 | const { redirect } = getPageQuery(); 73 | // redirect 74 | if (window.location.pathname !== '/user/login' && !redirect) { 75 | // 清理token 76 | setToken(''); 77 | yield put( 78 | routerRedux.replace({ 79 | pathname: '/user/login', 80 | search: stringify({ 81 | redirect: window.location.href, 82 | }), 83 | }), 84 | ); 85 | } 86 | }, 87 | }, 88 | 89 | reducers: { 90 | changeLoginStatus(state, { payload }) { 91 | // if (payload) 92 | const status: Status = payload.code === 200 ? 'ok' : 'error'; 93 | let currentAuthority: CurrentAuthority = 'guest'; 94 | if (payload.code === 200) { 95 | currentAuthority = payload.data.level === 0 ? 'admin' : 'user'; 96 | setAuthority(currentAuthority); 97 | setToken(payload.data.token); 98 | } 99 | return { 100 | ...state, 101 | status, 102 | type: payload.type, 103 | currentAuthority, 104 | }; 105 | }, 106 | }, 107 | }; 108 | 109 | export default Model; 110 | -------------------------------------------------------------------------------- /src/models/project.ts: -------------------------------------------------------------------------------- 1 | import { Reducer } from 'redux'; 2 | import { Effect } from 'dva'; 3 | import { stringify } from 'querystring'; 4 | 5 | import { getWebProjectListDao } from '@/services/project'; 6 | import { getPageQuery } from '@/utils/utils'; 7 | 8 | export interface ProjectStateType { 9 | projectToken?: string; 10 | projectList?: IProjectType[]; 11 | } 12 | 13 | export interface ProjectModelType { 14 | namespace: string; 15 | state: ProjectStateType; 16 | effects: { 17 | fetchProjectList: Effect; 18 | setProjectToken: Effect; 19 | }; 20 | reducers: { 21 | changeProjectList: Reducer; 22 | changeProjectToken: Reducer; 23 | }; 24 | } 25 | 26 | const Model: ProjectModelType = { 27 | namespace: 'project', 28 | 29 | state: { 30 | projectToken: '', 31 | projectList: [], 32 | }, 33 | 34 | effects: { 35 | *fetchProjectList({ payload }, { call, put }) { 36 | const response = yield call(getWebProjectListDao, payload); 37 | if (response.code === 200) { 38 | yield put({ 39 | type: 'changeProjectList', 40 | payload: response.data, 41 | }); 42 | } 43 | }, 44 | *setProjectToken({ payload }, { call, put }) { 45 | const params = getPageQuery(); 46 | const { token } = params; 47 | yield put({ 48 | type: 'changeProjectToken', 49 | payload: token, 50 | }); 51 | }, 52 | }, 53 | 54 | reducers: { 55 | changeProjectList(state, { payload }) { 56 | return { 57 | ...state, 58 | projectList: payload, 59 | }; 60 | }, 61 | changeProjectToken(state, { payload }) { 62 | return { 63 | ...state, 64 | projectToken: payload, 65 | }; 66 | }, 67 | }, 68 | }; 69 | 70 | export default Model; 71 | -------------------------------------------------------------------------------- /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 | system_ids?: string[]; 8 | is_use?: number; 9 | level?: number; 10 | id?: string; 11 | create_time?: string; 12 | user_name?: string; 13 | } 14 | 15 | export interface UserModelState { 16 | currentUser?: CurrentUser; 17 | } 18 | 19 | export interface UserModelType { 20 | namespace: 'user'; 21 | state: UserModelState; 22 | effects: { 23 | fetch: Effect; 24 | fetchCurrent: Effect; 25 | }; 26 | reducers: { 27 | saveCurrentUser: Reducer; 28 | changeNotifyCount: Reducer; 29 | }; 30 | } 31 | 32 | const UserModel: UserModelType = { 33 | namespace: 'user', 34 | 35 | state: { 36 | currentUser: {}, 37 | }, 38 | 39 | effects: { 40 | *fetch(_, { call, put }) { 41 | const response = yield call(queryUsers); 42 | yield put({ 43 | type: 'save', 44 | payload: response, 45 | }); 46 | }, 47 | *fetchCurrent(_, { call, put }) { 48 | const response = yield call(queryCurrent); 49 | yield put({ 50 | type: 'saveCurrentUser', 51 | payload: response.data, 52 | }); 53 | }, 54 | }, 55 | 56 | reducers: { 57 | saveCurrentUser(state, action) { 58 | return { 59 | ...state, 60 | currentUser: action.payload || {}, 61 | }; 62 | }, 63 | changeNotifyCount( 64 | state = { 65 | currentUser: {}, 66 | }, 67 | action, 68 | ) { 69 | return { 70 | ...state, 71 | currentUser: { 72 | ...state.currentUser, 73 | notifyCount: action.payload.totalCount, 74 | unreadCount: action.payload.unreadCount, 75 | }, 76 | }; 77 | }, 78 | }, 79 | }; 80 | 81 | export default UserModel; 82 | -------------------------------------------------------------------------------- /src/models/viewDetail.ts: -------------------------------------------------------------------------------- 1 | import { Reducer } from 'redux'; 2 | import { Effect } from 'dva'; 3 | import { getAllLog } from '@/services/viewDetail'; 4 | 5 | export interface logDataType { 6 | t: string; 7 | page: string; 8 | times: number; 9 | v: string; 10 | token: string; 11 | e: string; 12 | begin: number; 13 | uid: string; 14 | sid: string; 15 | sr: string; 16 | vp: string; 17 | ct: string; 18 | ul: string; 19 | _v: string; 20 | o: string; 21 | errcount: number; 22 | apisucc: number; 23 | apifail: number; 24 | healthy: number; 25 | stay: number; 26 | time?: number; 27 | load?: number; 28 | detector: { 29 | device: { 30 | name: string; 31 | version: number; 32 | fullVersion: string; 33 | iphone: number; 34 | }; 35 | os: { 36 | name: string; 37 | version: number; 38 | fullVersion: string; 39 | ios: number; 40 | }; 41 | engine: { 42 | name: string; 43 | version: number; 44 | fullVersion: string; 45 | mode: number; 46 | fullMode: string; 47 | compatible: boolean; 48 | webkit: number; 49 | }; 50 | browser: { 51 | name: string; 52 | version: number; 53 | fullVersion: string; 54 | mode: number; 55 | fullMode: string; 56 | compatible: boolean; 57 | safari: number; 58 | }; 59 | }; 60 | body: {}; 61 | location: { 62 | lat: number; 63 | lon: number; 64 | }; 65 | ad_info: { 66 | nation: string; 67 | province: string; 68 | city: string; 69 | district: string; 70 | adcode: number; 71 | }; 72 | ip: string; 73 | pv: number; 74 | uv: string; 75 | user_agent: string; 76 | '@timestamp': string; 77 | } 78 | 79 | export interface viewDetailStateType { 80 | allLog: { 81 | isLoadingData: boolean; 82 | total: number; 83 | allLogDataList: Array; 84 | }; 85 | jsErrorLog?: {}; 86 | apiLog?: {}; 87 | pagePerformanceLog?: {}; 88 | pvLog?: {}; 89 | } 90 | 91 | export interface viewDetailModalType { 92 | namespace: string; 93 | state: viewDetailStateType; 94 | effects: { 95 | getAllLogAction: Effect; 96 | }; 97 | reducers: { 98 | updateAllLogDataList: Reducer; 99 | updateAllLogTotal: Reducer; 100 | switchAllLogIsLoadingData: Reducer; 101 | }; 102 | } 103 | 104 | const initState = { 105 | allLog: { 106 | isLoadingData: false, 107 | total: 0, 108 | allLogDataList: [], 109 | }, 110 | }; 111 | 112 | const viewDetailModal: viewDetailModalType = { 113 | namespace: 'viewdetail', 114 | state: initState, 115 | effects: { 116 | *getAllLogAction({ payload }, { call, put }) { 117 | const allLogResult = yield call(getAllLog, payload); 118 | yield put({ type: 'isLoadingData' }); 119 | if (allLogResult.code === 200) { 120 | yield put({ type: 'updateAllLogTotal', payload: allLogResult.data.total }); 121 | yield put({ type: 'updateAllLogDataList', payload: allLogResult.data.data }); 122 | } 123 | }, 124 | }, 125 | reducers: { 126 | updateAllLogDataList(state, { payload }) { 127 | state.allLog.allLogDataList = payload; 128 | return state; 129 | }, 130 | 131 | updateAllLogTotal(state, { payload }) { 132 | const { allLog } = state; 133 | allLog.total = payload; 134 | return Object.assign(state, allLog); 135 | }, 136 | 137 | switchAllLogIsLoadingData(state, { payload }) { 138 | const { allLog } = state; 139 | allLog.isLoadingData = payload; 140 | return Object.assign(state, allLog); 141 | }, 142 | }, 143 | }; 144 | 145 | export default viewDetailModal; 146 | -------------------------------------------------------------------------------- /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/Welcome.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Card, Typography } from 'antd'; 3 | import { PageHeaderWrapper } from '@ant-design/pro-layout'; 4 | import { FormattedMessage } from 'umi-plugin-react/locale'; 5 | 6 | const CodePreview: React.FC<{}> = ({ children }) => ( 7 |
14 |     
15 |       {children}
16 |     
17 |   
18 | ); 19 | 20 | export default (): React.ReactNode => ( 21 | 22 | 23 | 24 | 25 | 29 | 30 | 31 | npx umi block list 32 | 38 | 43 | 44 | 45 | 46 | npm run fetch:blocks 47 | 48 |

49 | Want to add more pages? Please refer to{' '} 50 | 51 | use block 52 | 53 | 。 54 |

55 |
56 | ); 57 | -------------------------------------------------------------------------------- /src/pages/account/settings/_mock.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | import city from './geographic/city.json'; 3 | import province from './geographic/province.json'; 4 | 5 | function getProvince(_: Request, res: Response) { 6 | return res.json(province); 7 | } 8 | 9 | function getCity(req: Request, res: Response) { 10 | return res.json(city[req.params.province]); 11 | } 12 | // 代码中会兼容本地 service mock 以及部署站点的静态数据 13 | export default { 14 | // 支持值为 Object 和 Array 15 | 'GET /api/currentUser': { 16 | name: 'Serati Ma', 17 | avatar: 'https://gw.alipayobjects.com/zos/rmsportal/BiazfanxmamNRoxxVxka.png', 18 | userid: '00000001', 19 | email: 'antdesign@alipay.com', 20 | signature: '海纳百川,有容乃大', 21 | title: '交互专家', 22 | group: '蚂蚁金服-某某某事业群-某某平台部-某某技术部-UED', 23 | tags: [ 24 | { 25 | key: '0', 26 | label: '很有想法的', 27 | }, 28 | { 29 | key: '1', 30 | label: '专注设计', 31 | }, 32 | { 33 | key: '2', 34 | label: '辣~', 35 | }, 36 | { 37 | key: '3', 38 | label: '大长腿', 39 | }, 40 | { 41 | key: '4', 42 | label: '川妹子', 43 | }, 44 | { 45 | key: '5', 46 | label: '海纳百川', 47 | }, 48 | ], 49 | notifyCount: 12, 50 | unreadCount: 11, 51 | country: 'China', 52 | geographic: { 53 | province: { 54 | label: '浙江省', 55 | key: '330000', 56 | }, 57 | city: { 58 | label: '杭州市', 59 | key: '330100', 60 | }, 61 | }, 62 | address: '西湖区工专路 77 号', 63 | phone: '0752-268888888', 64 | }, 65 | 'GET /api/geographic/province': getProvince, 66 | 'GET /api/geographic/city/:province': getCity, 67 | }; 68 | -------------------------------------------------------------------------------- /src/pages/account/settings/components/BaseView.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | .baseView { 4 | display: flex; 5 | padding-top: 12px; 6 | 7 | .left { 8 | min-width: 224px; 9 | max-width: 448px; 10 | } 11 | .right { 12 | flex: 1; 13 | padding-left: 104px; 14 | .avatar_title { 15 | height: 22px; 16 | margin-bottom: 8px; 17 | color: @heading-color; 18 | font-size: @font-size-base; 19 | line-height: 22px; 20 | } 21 | .avatar { 22 | width: 144px; 23 | height: 144px; 24 | margin-bottom: 12px; 25 | overflow: hidden; 26 | img { 27 | width: 100%; 28 | } 29 | } 30 | .button_view { 31 | width: 144px; 32 | text-align: center; 33 | } 34 | } 35 | } 36 | 37 | @media screen and (max-width: @screen-xl) { 38 | .baseView { 39 | flex-direction: column-reverse; 40 | 41 | .right { 42 | display: flex; 43 | flex-direction: column; 44 | align-items: center; 45 | max-width: 448px; 46 | padding: 20px; 47 | .avatar_title { 48 | display: none; 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/pages/account/settings/components/GeographicView.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | .row { 4 | .item { 5 | width: 50%; 6 | max-width: 220px; 7 | } 8 | .item:first-child { 9 | width: ~'calc(50% - 8px)'; 10 | margin-right: 8px; 11 | } 12 | } 13 | 14 | @media screen and (max-width: @screen-sm) { 15 | .item:first-child { 16 | margin: 0; 17 | margin-bottom: 8px; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/pages/account/settings/components/PhoneView.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | .area_code { 4 | width: 30%; 5 | max-width: 128px; 6 | margin-right: 8px; 7 | } 8 | .phone_number { 9 | width: ~'calc(70% - 8px)'; 10 | max-width: 312px; 11 | } 12 | -------------------------------------------------------------------------------- /src/pages/account/settings/components/PhoneView.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment, PureComponent } from 'react'; 2 | 3 | import { Input } from 'antd'; 4 | import styles from './PhoneView.less'; 5 | 6 | interface PhoneViewProps { 7 | value?: string; 8 | onChange?: (value: string) => void; 9 | } 10 | 11 | class PhoneView extends PureComponent { 12 | render() { 13 | const { value, onChange } = this.props; 14 | let values = ['', '']; 15 | if (value) { 16 | values = value.split('-'); 17 | } 18 | return ( 19 | 20 | { 24 | if (onChange) { 25 | onChange(`${e.target.value}-${values[1]}`); 26 | } 27 | }} 28 | /> 29 | { 32 | if (onChange) { 33 | onChange(`${values[0]}-${e.target.value}`); 34 | } 35 | }} 36 | value={values[1]} 37 | /> 38 | 39 | ); 40 | } 41 | } 42 | 43 | export default PhoneView; 44 | -------------------------------------------------------------------------------- /src/pages/account/settings/components/binding.tsx: -------------------------------------------------------------------------------- 1 | import { Icon, List } from 'antd'; 2 | import React, { Component, Fragment } from 'react'; 3 | 4 | class BindingView extends Component { 5 | getData = () => [ 6 | { 7 | title: '绑定淘宝', 8 | description: '当前未绑定淘宝账号', 9 | actions: [绑定], 10 | avatar: , 11 | }, 12 | { 13 | title: '绑定支付宝', 14 | description: '当前未绑定支付宝账号', 15 | actions: [绑定], 16 | avatar: , 17 | }, 18 | { 19 | title: '绑定钉钉', 20 | description: '当前未绑定钉钉账号', 21 | actions: [绑定], 22 | avatar: , 23 | }, 24 | ]; 25 | 26 | render() { 27 | return ( 28 | 29 | ( 33 | 34 | 39 | 40 | )} 41 | /> 42 | 43 | ); 44 | } 45 | } 46 | 47 | export default BindingView; 48 | -------------------------------------------------------------------------------- /src/pages/account/settings/components/notification.tsx: -------------------------------------------------------------------------------- 1 | import { List, Switch } from 'antd'; 2 | import React, { Component, Fragment } from 'react'; 3 | 4 | type Unpacked = T extends (infer U)[] ? U : T; 5 | 6 | class NotificationView extends Component { 7 | getData = () => { 8 | const Action = ; 9 | return [ 10 | { 11 | title: '账户密码', 12 | description: '其他用户的消息将以站内信的形式通知', 13 | actions: [Action], 14 | }, 15 | { 16 | title: '系统消息', 17 | description: '系统消息将以站内信的形式通知', 18 | actions: [Action], 19 | }, 20 | { 21 | title: '待办任务', 22 | description: '待办任务将以站内信的形式通知', 23 | actions: [Action], 24 | }, 25 | ]; 26 | }; 27 | 28 | render() { 29 | const data = this.getData(); 30 | return ( 31 | 32 | > 33 | itemLayout="horizontal" 34 | dataSource={data} 35 | renderItem={item => ( 36 | 37 | 38 | 39 | )} 40 | /> 41 | 42 | ); 43 | } 44 | } 45 | 46 | export default NotificationView; 47 | -------------------------------------------------------------------------------- /src/pages/account/settings/components/security.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from 'react'; 2 | import { List } from 'antd'; 3 | 4 | type Unpacked = T extends (infer U)[] ? U : T; 5 | const passwordStrength = { 6 | strong: , 7 | medium: , 8 | weak: 弱 Weak, 9 | }; 10 | 11 | class SecurityView extends Component { 12 | getData = () => [ 13 | { 14 | title: '账户密码', 15 | description: ( 16 | 17 | 当前密码强度:: 18 | {passwordStrength.strong} 19 | 20 | ), 21 | actions: [修改], 22 | }, 23 | { 24 | title: '密保手机', 25 | description: `${'已绑定手机:'}:138****8293`, 26 | actions: [修改], 27 | }, 28 | { 29 | title: '密保问题', 30 | description: '未设置密保问题,密保问题可有效保护账户安全', 31 | actions: [设置], 32 | }, 33 | { 34 | title: '备用邮箱', 35 | description: `${'已绑定邮箱:'}:ant***sign.com`, 36 | actions: [修改], 37 | }, 38 | { 39 | title: 'MFA 设备', 40 | description: '未绑定 MFA 设备,绑定后,可以进行二次确认', 41 | actions: [绑定], 42 | }, 43 | ]; 44 | 45 | render() { 46 | const data = this.getData(); 47 | return ( 48 | 49 | > 50 | itemLayout="horizontal" 51 | dataSource={data} 52 | renderItem={item => ( 53 | 54 | 55 | 56 | )} 57 | /> 58 | 59 | ); 60 | } 61 | } 62 | 63 | export default SecurityView; 64 | -------------------------------------------------------------------------------- /src/pages/account/settings/data.d.ts: -------------------------------------------------------------------------------- 1 | export interface TagType { 2 | key: string; 3 | label: string; 4 | } 5 | 6 | export interface GeographicItemType { 7 | name: string; 8 | id: string; 9 | } 10 | 11 | export interface GeographicType { 12 | province: GeographicItemType; 13 | city: GeographicItemType; 14 | } 15 | 16 | export interface NoticeType { 17 | id: string; 18 | title: string; 19 | logo: string; 20 | description: string; 21 | updatedAt: string; 22 | member: string; 23 | href: string; 24 | memberLink: string; 25 | } 26 | 27 | export interface CurrentUser { 28 | name: string; 29 | avatar: string; 30 | userid: string; 31 | notice: NoticeType[]; 32 | email: string; 33 | signature: string; 34 | title: string; 35 | group: string; 36 | tags: TagType[]; 37 | notifyCount: number; 38 | unreadCount: number; 39 | country: string; 40 | geographic: GeographicType; 41 | address: string; 42 | phone: string; 43 | } 44 | -------------------------------------------------------------------------------- /src/pages/account/settings/geographic/province.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "北京市", 4 | "id": "110000" 5 | }, 6 | { 7 | "name": "天津市", 8 | "id": "120000" 9 | }, 10 | { 11 | "name": "河北省", 12 | "id": "130000" 13 | }, 14 | { 15 | "name": "山西省", 16 | "id": "140000" 17 | }, 18 | { 19 | "name": "内蒙古自治区", 20 | "id": "150000" 21 | }, 22 | { 23 | "name": "辽宁省", 24 | "id": "210000" 25 | }, 26 | { 27 | "name": "吉林省", 28 | "id": "220000" 29 | }, 30 | { 31 | "name": "黑龙江省", 32 | "id": "230000" 33 | }, 34 | { 35 | "name": "上海市", 36 | "id": "310000" 37 | }, 38 | { 39 | "name": "江苏省", 40 | "id": "320000" 41 | }, 42 | { 43 | "name": "浙江省", 44 | "id": "330000" 45 | }, 46 | { 47 | "name": "安徽省", 48 | "id": "340000" 49 | }, 50 | { 51 | "name": "福建省", 52 | "id": "350000" 53 | }, 54 | { 55 | "name": "江西省", 56 | "id": "360000" 57 | }, 58 | { 59 | "name": "山东省", 60 | "id": "370000" 61 | }, 62 | { 63 | "name": "河南省", 64 | "id": "410000" 65 | }, 66 | { 67 | "name": "湖北省", 68 | "id": "420000" 69 | }, 70 | { 71 | "name": "湖南省", 72 | "id": "430000" 73 | }, 74 | { 75 | "name": "广东省", 76 | "id": "440000" 77 | }, 78 | { 79 | "name": "广西壮族自治区", 80 | "id": "450000" 81 | }, 82 | { 83 | "name": "海南省", 84 | "id": "460000" 85 | }, 86 | { 87 | "name": "重庆市", 88 | "id": "500000" 89 | }, 90 | { 91 | "name": "四川省", 92 | "id": "510000" 93 | }, 94 | { 95 | "name": "贵州省", 96 | "id": "520000" 97 | }, 98 | { 99 | "name": "云南省", 100 | "id": "530000" 101 | }, 102 | { 103 | "name": "西藏自治区", 104 | "id": "540000" 105 | }, 106 | { 107 | "name": "陕西省", 108 | "id": "610000" 109 | }, 110 | { 111 | "name": "甘肃省", 112 | "id": "620000" 113 | }, 114 | { 115 | "name": "青海省", 116 | "id": "630000" 117 | }, 118 | { 119 | "name": "宁夏回族自治区", 120 | "id": "640000" 121 | }, 122 | { 123 | "name": "新疆维吾尔自治区", 124 | "id": "650000" 125 | }, 126 | { 127 | "name": "台湾省", 128 | "id": "710000" 129 | }, 130 | { 131 | "name": "香港特别行政区", 132 | "id": "810000" 133 | }, 134 | { 135 | "name": "澳门特别行政区", 136 | "id": "820000" 137 | } 138 | ] 139 | -------------------------------------------------------------------------------- /src/pages/account/settings/locales/en-US.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'account-settings.menuMap.basic': 'Basic Settings', 3 | 'account-settings.menuMap.security': 'Security Settings', 4 | 'account-settings.menuMap.binding': 'Account Binding', 5 | 'account-settings.menuMap.notification': 'New Message Notification', 6 | 'account-settings.basic.avatar': 'Avatar', 7 | 'account-settings.basic.change-avatar': 'Change avatar', 8 | 'account-settings.basic.email': 'Email', 9 | 'account-settings.basic.email-message': 'Please input your email!', 10 | 'account-settings.basic.nickname': 'Nickname', 11 | 'account-settings.basic.nickname-message': 'Please input your Nickname!', 12 | 'account-settings.basic.profile': 'Personal profile', 13 | 'account-settings.basic.profile-message': 'Please input your personal profile!', 14 | 'account-settings.basic.profile-placeholder': 'Brief introduction to yourself', 15 | 'account-settings.basic.country': 'Country/Region', 16 | 'account-settings.basic.country-message': 'Please input your country!', 17 | 'account-settings.basic.geographic': 'Province or city', 18 | 'account-settings.basic.geographic-message': 'Please input your geographic info!', 19 | 'account-settings.basic.address': 'Street Address', 20 | 'account-settings.basic.address-message': 'Please input your address!', 21 | 'account-settings.basic.phone': 'Phone Number', 22 | 'account-settings.basic.phone-message': 'Please input your phone!', 23 | 'account-settings.basic.update': 'Update Information', 24 | 'account-settings.basic.update.success': 'Update basic information successfully', 25 | 'account-settings.security.strong': 'Strong', 26 | 'account-settings.security.medium': 'Medium', 27 | 'account-settings.security.weak': 'Weak', 28 | 'account-settings.security.password': 'Account Password', 29 | 'account-settings.security.password-description': 'Current password strength:', 30 | 'account-settings.security.phone': 'Security Phone', 31 | 'account-settings.security.phone-description': 'Bound phone:', 32 | 'account-settings.security.question': 'Security Question', 33 | 'account-settings.security.question-description': 34 | 'The security question is not set, and the security policy can effectively protect the account security', 35 | 'account-settings.security.email': 'Backup Email', 36 | 'account-settings.security.email-description': 'Bound Email:', 37 | 'account-settings.security.mfa': 'MFA Device', 38 | 'account-settings.security.mfa-description': 39 | 'Unbound MFA device, after binding, can be confirmed twice', 40 | 'account-settings.security.modify': 'Modify', 41 | 'account-settings.security.set': 'Set', 42 | 'account-settings.security.bind': 'Bind', 43 | 'account-settings.binding.taobao': 'Binding Taobao', 44 | 'account-settings.binding.taobao-description': 'Currently unbound Taobao account', 45 | 'account-settings.binding.alipay': 'Binding Alipay', 46 | 'account-settings.binding.alipay-description': 'Currently unbound Alipay account', 47 | 'account-settings.binding.dingding': 'Binding DingTalk', 48 | 'account-settings.binding.dingding-description': 'Currently unbound DingTalk account', 49 | 'account-settings.binding.bind': 'Bind', 50 | 'account-settings.notification.password': 'Account Password', 51 | 'account-settings.notification.password-description': 52 | 'Messages from other users will be notified in the form of a station letter', 53 | 'account-settings.notification.messages': 'System Messages', 54 | 'account-settings.notification.messages-description': 55 | 'System messages will be notified in the form of a station letter', 56 | 'account-settings.notification.todo': 'To-do Notification', 57 | 'account-settings.notification.todo-description': 58 | 'The to-do list will be notified in the form of a letter from the station', 59 | 'account-settings.settings.open': 'Open', 60 | 'account-settings.settings.close': 'Close', 61 | }; 62 | -------------------------------------------------------------------------------- /src/pages/account/settings/locales/zh-CN.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'account-settings.menuMap.basic': '基本设置', 3 | 'account-settings.menuMap.security': '安全设置', 4 | 'account-settings.menuMap.binding': '账号绑定', 5 | 'account-settings.menuMap.notification': '新消息通知', 6 | 'account-settings.basic.avatar': '头像', 7 | 'account-settings.basic.change-avatar': '更换头像', 8 | 'account-settings.basic.email': '邮箱', 9 | 'account-settings.basic.email-message': '请输入您的邮箱!', 10 | 'account-settings.basic.nickname': '昵称', 11 | 'account-settings.basic.nickname-message': '请输入您的昵称!', 12 | 'account-settings.basic.profile': '个人简介', 13 | 'account-settings.basic.profile-message': '请输入个人简介!', 14 | 'account-settings.basic.profile-placeholder': '个人简介', 15 | 'account-settings.basic.country': '国家/地区', 16 | 'account-settings.basic.country-message': '请输入您的国家或地区!', 17 | 'account-settings.basic.geographic': '所在省市', 18 | 'account-settings.basic.geographic-message': '请输入您的所在省市!', 19 | 'account-settings.basic.address': '街道地址', 20 | 'account-settings.basic.address-message': '请输入您的街道地址!', 21 | 'account-settings.basic.phone': '联系电话', 22 | 'account-settings.basic.phone-message': '请输入您的联系电话!', 23 | 'account-settings.basic.update': '更新基本信息', 24 | 'account-settings.basic.update.success': '更新基本信息成功', 25 | 'account-settings.security.strong': '强', 26 | 'account-settings.security.medium': '中', 27 | 'account-settings.security.weak': '弱', 28 | 'account-settings.security.password': '账户密码', 29 | 'account-settings.security.password-description': '当前密码强度:', 30 | 'account-settings.security.phone': '密保手机', 31 | 'account-settings.security.phone-description': '已绑定手机:', 32 | 'account-settings.security.question': '密保问题', 33 | 'account-settings.security.question-description': '未设置密保问题,密保问题可有效保护账户安全', 34 | 'account-settings.security.email': '备用邮箱', 35 | 'account-settings.security.email-description': '已绑定邮箱:', 36 | 'account-settings.security.mfa': 'MFA 设备', 37 | 'account-settings.security.mfa-description': '未绑定 MFA 设备,绑定后,可以进行二次确认', 38 | 'account-settings.security.modify': '修改', 39 | 'account-settings.security.set': '设置', 40 | 'account-settings.security.bind': '绑定', 41 | 'account-settings.binding.taobao': '绑定淘宝', 42 | 'account-settings.binding.taobao-description': '当前未绑定淘宝账号', 43 | 'account-settings.binding.alipay': '绑定支付宝', 44 | 'account-settings.binding.alipay-description': '当前未绑定支付宝账号', 45 | 'account-settings.binding.dingding': '绑定钉钉', 46 | 'account-settings.binding.dingding-description': '当前未绑定钉钉账号', 47 | 'account-settings.binding.bind': '绑定', 48 | 'account-settings.notification.password': '账户密码', 49 | 'account-settings.notification.password-description': '其他用户的消息将以站内信的形式通知', 50 | 'account-settings.notification.messages': '系统消息', 51 | 'account-settings.notification.messages-description': '系统消息将以站内信的形式通知', 52 | 'account-settings.notification.todo': '待办任务', 53 | 'account-settings.notification.todo-description': '待办任务将以站内信的形式通知', 54 | 'account-settings.settings.open': '开', 55 | 'account-settings.settings.close': '关', 56 | }; 57 | -------------------------------------------------------------------------------- /src/pages/account/settings/locales/zh-TW.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'account-settings.menuMap.basic': '基本設置', 3 | 'account-settings.menuMap.security': '安全設置', 4 | 'account-settings.menuMap.binding': '賬號綁定', 5 | 'account-settings.menuMap.notification': '新消息通知', 6 | 'account-settings.basic.avatar': '頭像', 7 | 'account-settings.basic.change-avatar': '更換頭像', 8 | 'account-settings.basic.email': '郵箱', 9 | 'account-settings.basic.email-message': '請輸入您的郵箱!', 10 | 'account-settings.basic.nickname': '昵稱', 11 | 'account-settings.basic.nickname-message': '請輸入您的昵稱!', 12 | 'account-settings.basic.profile': '個人簡介', 13 | 'account-settings.basic.profile-message': '請輸入個人簡介!', 14 | 'account-settings.basic.profile-placeholder': '個人簡介', 15 | 'account-settings.basic.country': '國家/地區', 16 | 'account-settings.basic.country-message': '請輸入您的國家或地區!', 17 | 'account-settings.basic.geographic': '所在省市', 18 | 'account-settings.basic.geographic-message': '請輸入您的所在省市!', 19 | 'account-settings.basic.address': '街道地址', 20 | 'account-settings.basic.address-message': '請輸入您的街道地址!', 21 | 'account-settings.basic.phone': '聯系電話', 22 | 'account-settings.basic.phone-message': '請輸入您的聯系電話!', 23 | 'account-settings.basic.update': '更新基本信息', 24 | 'account-settings.basic.update.success': '更新基本信息成功', 25 | 'account-settings.security.strong': '強', 26 | 'account-settings.security.medium': '中', 27 | 'account-settings.security.weak': '弱', 28 | 'account-settings.security.password': '賬戶密碼', 29 | 'account-settings.security.password-description': '當前密碼強度:', 30 | 'account-settings.security.phone': '密保手機', 31 | 'account-settings.security.phone-description': '已綁定手機:', 32 | 'account-settings.security.question': '密保問題', 33 | 'account-settings.security.question-description': '未設置密保問題,密保問題可有效保護賬戶安全', 34 | 'account-settings.security.email': '備用郵箱', 35 | 'account-settings.security.email-description': '已綁定郵箱:', 36 | 'account-settings.security.mfa': 'MFA 設備', 37 | 'account-settings.security.mfa-description': '未綁定 MFA 設備,綁定後,可以進行二次確認', 38 | 'account-settings.security.modify': '修改', 39 | 'account-settings.security.set': '設置', 40 | 'account-settings.security.bind': '綁定', 41 | 'account-settings.binding.taobao': '綁定淘寶', 42 | 'account-settings.binding.taobao-description': '當前未綁定淘寶賬號', 43 | 'account-settings.binding.alipay': '綁定支付寶', 44 | 'account-settings.binding.alipay-description': '當前未綁定支付寶賬號', 45 | 'account-settings.binding.dingding': '綁定釘釘', 46 | 'account-settings.binding.dingding-description': '當前未綁定釘釘賬號', 47 | 'account-settings.binding.bind': '綁定', 48 | 'account-settings.notification.password': '賬戶密碼', 49 | 'account-settings.notification.password-description': '其他用戶的消息將以站內信的形式通知', 50 | 'account-settings.notification.messages': '系統消息', 51 | 'account-settings.notification.messages-description': '系統消息將以站內信的形式通知', 52 | 'account-settings.notification.todo': '待辦任務', 53 | 'account-settings.notification.todo-description': '待辦任務將以站內信的形式通知', 54 | 'account-settings.settings.open': '開', 55 | 'account-settings.settings.close': '關', 56 | }; 57 | -------------------------------------------------------------------------------- /src/pages/account/settings/model.ts: -------------------------------------------------------------------------------- 1 | import { AnyAction, Reducer } from 'redux'; 2 | import { EffectsCommandMap } from 'dva'; 3 | import { CurrentUser, GeographicItemType } from './data.d'; 4 | import { queryCity, queryCurrent, queryProvince, query as queryUsers } from './service'; 5 | 6 | export interface ModalState { 7 | currentUser?: Partial; 8 | province?: GeographicItemType[]; 9 | city?: GeographicItemType[]; 10 | isLoading?: boolean; 11 | } 12 | 13 | export type Effect = ( 14 | action: AnyAction, 15 | effects: EffectsCommandMap & { select: (func: (state: ModalState) => T) => T }, 16 | ) => void; 17 | 18 | export interface ModelType { 19 | namespace: string; 20 | state: ModalState; 21 | effects: { 22 | fetchCurrent: Effect; 23 | fetch: Effect; 24 | fetchProvince: Effect; 25 | fetchCity: Effect; 26 | }; 27 | reducers: { 28 | saveCurrentUser: Reducer; 29 | changeNotifyCount: Reducer; 30 | setProvince: Reducer; 31 | setCity: Reducer; 32 | changeLoading: Reducer; 33 | }; 34 | } 35 | 36 | const Model: ModelType = { 37 | namespace: 'accountSettings', 38 | 39 | state: { 40 | currentUser: {}, 41 | province: [], 42 | city: [], 43 | isLoading: false, 44 | }, 45 | 46 | effects: { 47 | *fetch(_, { call, put }) { 48 | const response = yield call(queryUsers); 49 | yield put({ 50 | type: 'save', 51 | payload: response, 52 | }); 53 | }, 54 | *fetchCurrent(_, { call, put }) { 55 | const response = yield call(queryCurrent); 56 | yield put({ 57 | type: 'saveCurrentUser', 58 | payload: response, 59 | }); 60 | }, 61 | *fetchProvince(_, { call, put }) { 62 | yield put({ 63 | type: 'changeLoading', 64 | payload: true, 65 | }); 66 | const response = yield call(queryProvince); 67 | yield put({ 68 | type: 'setProvince', 69 | payload: response, 70 | }); 71 | }, 72 | *fetchCity({ payload }, { call, put }) { 73 | const response = yield call(queryCity, payload); 74 | yield put({ 75 | type: 'setCity', 76 | payload: response, 77 | }); 78 | }, 79 | }, 80 | 81 | reducers: { 82 | saveCurrentUser(state, action) { 83 | return { 84 | ...state, 85 | currentUser: action.payload || {}, 86 | }; 87 | }, 88 | changeNotifyCount(state = {}, action) { 89 | return { 90 | ...state, 91 | currentUser: { 92 | ...state.currentUser, 93 | notifyCount: action.payload.totalCount, 94 | unreadCount: action.payload.unreadCount, 95 | }, 96 | }; 97 | }, 98 | setProvince(state, action) { 99 | return { 100 | ...state, 101 | province: action.payload, 102 | }; 103 | }, 104 | setCity(state, action) { 105 | return { 106 | ...state, 107 | city: action.payload, 108 | }; 109 | }, 110 | changeLoading(state, action) { 111 | return { 112 | ...state, 113 | isLoading: action.payload, 114 | }; 115 | }, 116 | }, 117 | }; 118 | 119 | export default Model; 120 | -------------------------------------------------------------------------------- /src/pages/account/settings/service.ts: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request'; 2 | 3 | export async function queryCurrent() { 4 | return request('/api/currentUser'); 5 | } 6 | 7 | export async function queryProvince() { 8 | return request('/api/geographic/province'); 9 | } 10 | 11 | export async function queryCity(province: string) { 12 | return request(`/api/geographic/city/${province}`); 13 | } 14 | 15 | export async function query() { 16 | return request('/api/users'); 17 | } 18 | -------------------------------------------------------------------------------- /src/pages/account/settings/style.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | .main { 4 | display: flex; 5 | width: 100%; 6 | height: 100%; 7 | padding-top: 16px; 8 | padding-bottom: 16px; 9 | overflow: auto; 10 | background-color: @menu-bg; 11 | .leftMenu { 12 | width: 224px; 13 | border-right: @border-width-base @border-style-base @border-color-split; 14 | :global { 15 | .ant-menu-inline { 16 | border: none; 17 | } 18 | .ant-menu:not(.ant-menu-horizontal) .ant-menu-item-selected { 19 | font-weight: bold; 20 | } 21 | } 22 | } 23 | .right { 24 | flex: 1; 25 | padding-top: 8px; 26 | padding-right: 40px; 27 | padding-bottom: 8px; 28 | padding-left: 40px; 29 | .title { 30 | margin-bottom: 12px; 31 | color: @heading-color; 32 | font-weight: 500; 33 | font-size: 20px; 34 | line-height: 28px; 35 | } 36 | } 37 | :global { 38 | .ant-list-split .ant-list-item:last-child { 39 | border-bottom: 1px solid @border-color-split; 40 | } 41 | .ant-list-item { 42 | padding-top: 14px; 43 | padding-bottom: 14px; 44 | } 45 | } 46 | } 47 | :global { 48 | .ant-list-item-meta { 49 | // 账号绑定图标 50 | .taobao { 51 | display: block; 52 | color: #ff4000; 53 | font-size: 48px; 54 | line-height: 48px; 55 | border-radius: @border-radius-base; 56 | } 57 | .dingding { 58 | margin: 2px; 59 | padding: 6px; 60 | color: #fff; 61 | font-size: 32px; 62 | line-height: 32px; 63 | background-color: #2eabff; 64 | border-radius: @border-radius-base; 65 | } 66 | .alipay { 67 | color: #2eabff; 68 | font-size: 48px; 69 | line-height: 48px; 70 | border-radius: @border-radius-base; 71 | } 72 | } 73 | 74 | // 密码强度 75 | font.strong { 76 | color: @success-color; 77 | } 78 | font.medium { 79 | color: @warning-color; 80 | } 81 | font.weak { 82 | color: @error-color; 83 | } 84 | } 85 | 86 | @media screen and (max-width: @screen-md) { 87 | .main { 88 | flex-direction: column; 89 | .leftMenu { 90 | width: 100%; 91 | border: none; 92 | } 93 | .right { 94 | padding: 40px; 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /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 |