├── .editorconfig ├── .env ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .stylelintrc.json ├── .travis.yml ├── .umirc.js ├── README.md ├── assets └── standard.md ├── config ├── id_rsa.enc └── theme.config.js ├── jest.config.js ├── manifest.json ├── mock ├── _utils.js ├── dashboard.js ├── post.js ├── route.js └── user.js ├── package.json ├── public ├── america.svg ├── avatar.png ├── china.svg ├── favicon.ico ├── logo.svg └── lu.jpg ├── scripts └── translate.js └── src ├── components ├── DropOption │ ├── DropOption.js │ └── package.json ├── Editor │ ├── Editor.js │ ├── Editor.less │ └── package.json ├── FilterItem │ ├── FilterItem.js │ ├── FilterItem.less │ └── package.json ├── Layout │ ├── Bread.js │ ├── Bread.less │ ├── Header.js │ ├── Header.less │ ├── Menu.js │ ├── Sider.js │ ├── Sider.less │ └── index.js ├── Loader │ ├── Loader.js │ ├── Loader.less │ └── package.json ├── Page │ ├── Page.js │ ├── Page.less │ └── package.json ├── ScrollBar │ ├── index.js │ └── index.less └── index.js ├── e2e └── login.e2e.js ├── layouts ├── BaseLayout.js ├── BaseLayout.less ├── PrimaryLayout.js ├── PrimaryLayout.less ├── PublicLayout.js └── index.js ├── locales └── zh │ └── messages.json ├── models └── app.js ├── pages ├── .umi │ ├── DvaContainer.js │ ├── initDva.js │ ├── initHistory.js │ ├── polyfills.js │ ├── router.js │ └── umi.js ├── 404.js ├── 404.less ├── UIElement │ └── editor │ │ └── index.js ├── auth.js ├── chart │ ├── Container.js │ ├── Container.less │ ├── ECharts │ │ ├── AirportCoordComponent.js │ │ ├── BubbleGradientComponent.js │ │ ├── CalendarComponent.js │ │ ├── ChartAPIComponent.js │ │ ├── ChartShowLoadingComponent.js │ │ ├── ChartWithEventComponent.js │ │ ├── DynamicChartComponent.js │ │ ├── EchartsComponent.js │ │ ├── GCalendarComponent.js │ │ ├── GaugeComponent.js │ │ ├── GraphComponent.js │ │ ├── LiquidfillComponent.js │ │ ├── LunarCalendarComponent.js │ │ ├── MainPageComponent.js │ │ ├── MapChartComponent.js │ │ ├── ModuleLoadChartComponent.js │ │ ├── SimpleChartComponent.js │ │ ├── ThemeChartComponent.js │ │ ├── TransparentBar3DComPonent.js │ │ ├── TreemapComponent.js │ │ ├── index.js │ │ ├── index.less │ │ └── theme │ │ │ ├── macarons.js │ │ │ └── shine.js │ ├── Recharts │ │ ├── AreaChartComponent.js │ │ ├── BarChartComponent.js │ │ ├── Container.js │ │ ├── Container.less │ │ ├── LineChartComponent.js │ │ ├── ReChartsComponent.js │ │ ├── index.js │ │ └── index.less │ └── highCharts │ │ ├── HighChartsComponent.js │ │ ├── HighMoreComponent.js │ │ ├── HighmapsComponent.js │ │ ├── HighstockComponent.js │ │ ├── index.js │ │ ├── index.less │ │ └── mapdata │ │ └── europe.js ├── dashboard │ ├── components │ │ ├── browser.js │ │ ├── browser.less │ │ ├── comments.js │ │ ├── comments.less │ │ ├── completed.js │ │ ├── completed.less │ │ ├── cpu.js │ │ ├── cpu.less │ │ ├── index.js │ │ ├── numberCard.js │ │ ├── numberCard.less │ │ ├── recentSales.js │ │ ├── recentSales.less │ │ ├── sales.js │ │ ├── sales.less │ │ ├── user-background.png │ │ ├── user.js │ │ ├── user.less │ │ ├── village-profile.js │ │ ├── village-profile.less │ │ ├── weather.js │ │ └── weather.less │ ├── index.js │ ├── index.less │ ├── model.js │ └── services │ │ ├── dashboard.js │ │ └── weather.js ├── dict │ ├── components │ │ ├── Filter.js │ │ ├── List.js │ │ ├── List.less │ │ └── Modal.js │ ├── index.js │ └── model.js ├── folk │ ├── category │ │ ├── components │ │ │ ├── Filter.js │ │ │ ├── List.js │ │ │ ├── List.less │ │ │ └── Modal.js │ │ ├── index.js │ │ └── model.js │ ├── categorycontent │ │ ├── $id │ │ │ ├── index.js │ │ │ ├── index.less │ │ │ └── models │ │ │ │ └── detail.js │ │ ├── components │ │ │ ├── Filter.js │ │ │ ├── List.js │ │ │ ├── List.less │ │ │ └── Modal.js │ │ ├── index.js │ │ └── model.js │ ├── cemetery │ │ ├── $id │ │ │ ├── index.js │ │ │ ├── index.less │ │ │ └── models │ │ │ │ └── detail.js │ │ ├── components │ │ │ ├── Filter.js │ │ │ ├── List.js │ │ │ ├── List.less │ │ │ └── Modal.js │ │ ├── index.js │ │ └── model.js │ ├── people │ │ ├── $id │ │ │ ├── index.js │ │ │ ├── index.less │ │ │ └── models │ │ │ │ └── detail.js │ │ ├── components │ │ │ ├── Filter.js │ │ │ ├── List.js │ │ │ ├── List.less │ │ │ ├── Modal.js │ │ │ └── RelationshipModal.js │ │ ├── index.js │ │ └── model.js │ ├── relationship │ │ ├── components │ │ │ ├── Filter.js │ │ │ ├── List.js │ │ │ ├── List.less │ │ │ └── Modal.js │ │ ├── index.js │ │ └── model.js │ └── tree │ │ ├── components │ │ └── Modal.js │ │ ├── index.js │ │ ├── index.less │ │ └── model.js ├── forgotpassword │ ├── index.js │ └── index.less ├── index.js ├── login │ ├── index.js │ ├── index.less │ └── model.js ├── post │ ├── components │ │ ├── List.js │ │ └── List.less │ ├── index.js │ └── model.js ├── region │ ├── components │ │ ├── Filter.js │ │ ├── List.js │ │ ├── List.less │ │ └── Modal.js │ ├── index.js │ └── model.js ├── request │ ├── index.js │ └── index.less ├── security │ ├── menu │ │ ├── components │ │ │ ├── Filter.js │ │ │ ├── List.js │ │ │ ├── List.less │ │ │ └── Modal.js │ │ ├── index.js │ │ └── model.js │ ├── role │ │ ├── components │ │ │ ├── Filter.js │ │ │ ├── List.js │ │ │ ├── List.less │ │ │ ├── Modal.js │ │ │ └── RoleMenuModal.js │ │ ├── index.js │ │ └── model.js │ └── rolemenu │ │ ├── components │ │ ├── Filter.js │ │ ├── List.js │ │ ├── List.less │ │ └── Modal.js │ │ ├── index.js │ │ └── model.js ├── signup │ ├── index.js │ ├── index.less │ └── model.js ├── user │ ├── $id │ │ ├── index.js │ │ ├── index.less │ │ └── models │ │ │ └── detail.js │ ├── components │ │ ├── Filter.js │ │ ├── List.js │ │ ├── List.less │ │ ├── MenuModal.js │ │ ├── Modal.js │ │ └── ResetPasswordModal.js │ ├── index.js │ └── model.js └── village │ ├── components │ ├── Filter.js │ ├── List.js │ ├── List.less │ └── Modal.js │ ├── images │ ├── index.js │ └── model.js │ ├── index.js │ └── model.js ├── plugins └── onError.js ├── services ├── api.js └── index.js ├── themes ├── default.less ├── index.less ├── mixin.less └── vars.less └── utils ├── city.js ├── config.js ├── constant.js ├── index.js ├── index.test.js ├── model.js ├── request.js └── theme.js /.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 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | BROWSER=none 2 | HOST=0.0.0.0 3 | PORT=7000 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | src/**/*-test.js 2 | src/public 3 | src/routes/chart/ECharts/theme 4 | src/routes/chart/highCharts/mapdata 5 | src/locales/_build/ 6 | src/locales/**/*.js 7 | docs/**/*.js 8 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "react-app", 3 | "rules": { 4 | "jsx-a11y/href-no-hash": "off", 5 | "no-console": "warn", 6 | "valid-jsdoc": "warn" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage 2 | dist 3 | node_modules 4 | npm-debug.log 5 | yarn-error.log 6 | yarn.lock 7 | package-lock.json 8 | 9 | # ide 10 | .idea 11 | 12 | # Mac General 13 | .DS_Store 14 | .AppleDouble 15 | .LSOverride 16 | 17 | # umi 18 | .umi 19 | .umi-production 20 | 21 | # jslingui 22 | src/locales/_build 23 | src/locales/**/*.js -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | *.svg 2 | *.ejs 3 | .DS_Store 4 | .umi 5 | .umi-production 6 | src/locales/**/*.json -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "trailingComma": "es5" 5 | } 6 | -------------------------------------------------------------------------------- /.stylelintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["stylelint-config-standard", "stylelint-config-prettier"], 3 | "rules": { 4 | "declaration-empty-line-before": null, 5 | "no-descending-specificity": null, 6 | "selector-pseudo-class-no-unknown": null, 7 | "selector-pseudo-element-colon-notation": null 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - node 4 | script: 5 | - npm run build 6 | before_install: 7 | - | 8 | if [ "$TRAVIS_BRANCH" = "develop" ]; then 9 | for prefixed_envvar in ${!DEV_*}; do 10 | eval export ${prefixed_envvar#DEV_}="${!prefixed_envvar}" 11 | done 12 | elif [ "$TRAVIS_BRANCH" = "master" ]; then 13 | for prefixed_envvar in ${!PROD_*}; do 14 | eval export ${prefixed_envvar#PROD_}="${!prefixed_envvar}" 15 | done 16 | else 17 | for prefixed_envvar in ${!TEST_*}; do 18 | eval export ${prefixed_envvar#TEST_}="${!prefixed_envvar}" 19 | done 20 | fi 21 | - openssl aes-256-cbc -K $encrypted_18b2305b78c9_key -iv $encrypted_18b2305b78c9_iv -in config/id_rsa.enc -out ~/.ssh/id_rsa -d 22 | - chmod 600 ~/.ssh/id_rsa 23 | - echo -e "Host $HOST\n\tStrictHostKeyChecking no\n" >> ~/.ssh/config 24 | - yarn global add now 25 | after_script: 26 | - scp -o stricthostkeychecking=no -r ./dist root@$HOST:$BUILD_DIR 27 | - cd ./docs && now -A $DOC_NOW_CONFIG -t $NOW_TOKEN && now alias -A $DOC_NOW_CONFIG -t $NOW_TOKEN 28 | -------------------------------------------------------------------------------- /.umirc.js: -------------------------------------------------------------------------------- 1 | // https://umijs.org/config/ 2 | import { resolve } from 'path' 3 | import { i18n } from './src/utils/config' 4 | 5 | export default { 6 | ignoreMomentLocale: true, 7 | targets: { ie: 9 }, 8 | treeShaking: true, 9 | plugins: [ 10 | [ 11 | // https://umijs.org/plugin/umi-plugin-react.html 12 | 'umi-plugin-react', 13 | { 14 | dva: { immer: true }, 15 | antd: true, 16 | dynamicImport: { 17 | webpackChunkName: true, 18 | loadingComponent: './components/Loader/Loader', 19 | }, 20 | routes: { 21 | exclude: [ 22 | /model\.(j|t)sx?$/, 23 | /service\.(j|t)sx?$/, 24 | /models\//, 25 | /components\//, 26 | /services\//, 27 | /chart\/Container\.js$/, 28 | /chart\/ECharts\/.+Component\.js$/, 29 | /chart\/ECharts\/.+ComPonent\.js$/, 30 | /chart\/ECharts\/theme\/.+\.js$/, 31 | /chart\/highCharts\/.+Component\.js$/, 32 | /chart\/highCharts\/mapdata\/.+\.js$/, 33 | /chart\/Recharts\/.+Component\.js$/, 34 | /chart\/Recharts\/Container\.js$/, 35 | ], 36 | update: routes => { 37 | if (!i18n) return routes 38 | 39 | const newRoutes = [] 40 | for (const item of routes[0].routes) { 41 | newRoutes.push(item) 42 | if (item.path) { 43 | newRoutes.push( 44 | Object.assign({}, item, { 45 | path: 46 | `/:lang(${i18n.languages 47 | .map(item => item.key) 48 | .join('|')})` + item.path, 49 | }) 50 | ) 51 | } 52 | } 53 | routes[0].routes = newRoutes 54 | 55 | return routes 56 | }, 57 | }, 58 | dll: { 59 | include: ['dva', 'dva/router', 'dva/saga', 'dva/fetch', 'antd/es'], 60 | }, 61 | pwa: { 62 | manifestOptions: { 63 | srcPath: 'manifest.json' 64 | }, 65 | } 66 | }, 67 | ], 68 | ], 69 | // Theme for antd 70 | // https://ant.design/docs/react/customize-theme 71 | theme: './config/theme.config.js', 72 | // Webpack Configuration 73 | proxy: { 74 | '/api/v1/': { 75 | target: 'http://localhost:8090', 76 | changeOrigin: true, 77 | pathRewrite: { '^/api/v1/': '' }, 78 | } 79 | }, 80 | alias: { 81 | api: resolve(__dirname, './src/services/'), 82 | components: resolve(__dirname, './src/components'), 83 | config: resolve(__dirname, './src/utils/config'), 84 | models: resolve(__dirname, './src/models'), 85 | routes: resolve(__dirname, './src/routes'), 86 | services: resolve(__dirname, './src/services'), 87 | themes: resolve(__dirname, './src/themes'), 88 | utils: resolve(__dirname, './src/utils'), 89 | }, 90 | extraBabelPresets: ['@lingui/babel-preset-react'], 91 | extraBabelPlugins: [ 92 | [ 93 | 'import', 94 | { 95 | libraryName: 'lodash', 96 | libraryDirectory: '', 97 | camel2DashComponentName: false, 98 | }, 99 | 'lodash', 100 | ], 101 | ], 102 | } 103 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # family_tree_ui 2 | 家谱系统的前端代码,使用蚂蚁金服的AntDesign作为Web UI 3 | -------------------------------------------------------------------------------- /assets/standard.md: -------------------------------------------------------------------------------- 1 | ### 开发环境 2 | 3 | - OS:Windows 4 | - 编辑器:Atom 5 | - cmd:Cmder 6 | 7 | ### Atom 插件 8 | 9 | - [atom-beautify](https://atom.io/packages/atom-beautify) 10 | 11 | #### Less 12 | 13 | - [x] Beautify On Save 14 | 15 | #### Javascript 16 | 17 | - [ ] Disable Beautifying Language 18 | 19 | #### Markdown 20 | 21 | - [x] Beautify On Save 22 | - [x] Default Beautifier:Remark 23 | 24 | #### HTML 25 | 26 | - [x] Beautify On Save 27 | 28 | - [linter](https://atom.io/packages/linter) 29 | - [ ] Lint on Change 30 | - [linter-eslint](https://atom.io/packages/linter-eslint) 31 | - [x] Fix errors on save 32 | 33 | ### Cmder 主题 34 | 35 | - [Panda-Theme-Cmder](https://github.com/HamidFaraji/Panda-Theme-Cmder) 36 | -------------------------------------------------------------------------------- /config/id_rsa.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luzhihua407/family_tree_ui/debf3185f1d2b85e348c75cb80a4b9e3c811631a/config/id_rsa.enc -------------------------------------------------------------------------------- /config/theme.config.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const lessToJs = require('less-vars-to-js') 4 | 5 | module.exports = () => { 6 | const themePath = path.join(__dirname, '../src/themes/default.less') 7 | return lessToJs(fs.readFileSync(themePath, 'utf8')) 8 | } 9 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testURL: 'http://localhost:8000', 3 | } 4 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Antd-Admin", 3 | "start_url": ".", 4 | "display": "standalone", 5 | "background_color": "#fff", 6 | "description": "A front-end solution for enterprise applications built upon Ant Design and UmiJS", 7 | "icons": [ 8 | { 9 | "src": "lu.svg", 10 | "sizes": "72x72 96x96 128x128 144x144 152x152 192x192 384x384 512x512" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /mock/_utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Query objects that specify keys and values in an array where all values are objects. 3 | * @param {array} array An array where all values are objects, like [{key:1},{key:2}]. 4 | * @param {string} key The key of the object that needs to be queried. 5 | * @param {string} value The value of the object that needs to be queried. 6 | * @return {object|undefined} Return frist object when query success. 7 | */ 8 | export function queryArray(array, key, value) { 9 | if (!Array.isArray(array)) { 10 | return 11 | } 12 | return array.filter(_ => _[key] === value) 13 | } 14 | 15 | export function randomNumber(min, max) { 16 | return Math.floor(Math.random() * (max - min) + min) 17 | } 18 | 19 | export function randomAvatar() { 20 | // https://uifaces.co 21 | const avatarList = [ 22 | 'https://randomuser.me/api/portraits/men/32.jpg', 23 | 'https://images.pexels.com/photos/415829/pexels-photo-415829.jpeg?h=350&auto=compress&cs=tinysrgb', 24 | 'https://d3iw72m71ie81c.cloudfront.net/female-17.jpg', 25 | 'https://randomuser.me/api/portraits/men/35.jpg', 26 | 'https://pbs.twimg.com/profile_images/835224725815246848/jdMBCxHS.jpg', 27 | 'https://pbs.twimg.com/profile_images/584098247641300992/N25WgvW_.png', 28 | 'https://d3iw72m71ie81c.cloudfront.net/male-5.jpg', 29 | 'https://images.pexels.com/photos/413723/pexels-photo-413723.jpeg?h=350&auto=compress&cs=tinysrgb', 30 | 'https://randomuser.me/api/portraits/women/44.jpg', 31 | 'https://randomuser.me/api/portraits/women/68.jpg', 32 | 'https://randomuser.me/api/portraits/women/65.jpg', 33 | 'https://randomuser.me/api/portraits/men/43.jpg', 34 | 'https://tinyfac.es/data/avatars/475605E3-69C5-4D2B-8727-61B7BB8C4699-500w.jpeg', 35 | 'https://pbs.twimg.com/profile_images/943227488292962306/teiNNAiy.jpg', 36 | 'https://randomuser.me/api/portraits/men/46.jpg' 37 | ] 38 | return avatarList[randomNumber(0, avatarList.length - 1)] 39 | } 40 | 41 | export const Constant = { 42 | ApiPrefix: '/api/v1', 43 | NotFound: { 44 | message: 'Not Found', 45 | documentation_url: '', 46 | }, 47 | Color: { 48 | green: '#64ea91', 49 | blue: '#8fc9fb', 50 | purple: '#d897eb', 51 | red: '#f69899', 52 | yellow: '#f8c82e', 53 | peach: '#f797d6', 54 | borderBase: '#e5e5e5', 55 | borderSplit: '#f4f4f4', 56 | grass: '#d6fbb5', 57 | sky: '#c1e0fc', 58 | }, 59 | } 60 | 61 | export Mock from 'mockjs' 62 | export qs from 'qs' 63 | -------------------------------------------------------------------------------- /mock/dashboard.js: -------------------------------------------------------------------------------- 1 | import { Mock, Constant } from './_utils' 2 | 3 | const { ApiPrefix, Color } = Constant 4 | 5 | const Dashboard = Mock.mock({ 6 | 'sales|8': [ 7 | { 8 | 'name|+1': 2008, 9 | 'Clothes|200-500': 1, 10 | 'Food|180-400': 1, 11 | 'Electronics|300-550': 1, 12 | }, 13 | ], 14 | cpu: { 15 | 'usage|50-600': 1, 16 | space: 825, 17 | 'cpu|40-90': 1, 18 | 'data|20': [ 19 | { 20 | 'cpu|20-80': 1, 21 | }, 22 | ], 23 | }, 24 | browser: [ 25 | { 26 | name: 'Google Chrome', 27 | percent: 43.3, 28 | status: 1, 29 | }, 30 | { 31 | name: 'Mozilla Firefox', 32 | percent: 33.4, 33 | status: 2, 34 | }, 35 | { 36 | name: 'Apple Safari', 37 | percent: 34.6, 38 | status: 3, 39 | }, 40 | { 41 | name: 'Internet Explorer', 42 | percent: 12.3, 43 | status: 4, 44 | }, 45 | { 46 | name: 'Opera Mini', 47 | percent: 3.3, 48 | status: 1, 49 | }, 50 | { 51 | name: 'Chromium', 52 | percent: 2.53, 53 | status: 1, 54 | }, 55 | ], 56 | user: { 57 | name: 'github', 58 | sales: 3241, 59 | sold: 3556, 60 | }, 61 | 'completed|12': [ 62 | { 63 | 'name|+1': 2008, 64 | 'Task complete|200-1000': 1, 65 | 'Cards Complete|200-1000': 1, 66 | }, 67 | ], 68 | 'comments|5': [ 69 | { 70 | name: '@last', 71 | 'status|1-3': 1, 72 | content: '@sentence', 73 | avatar() { 74 | return Mock.Random.image( 75 | '48x48', 76 | Mock.Random.color(), 77 | '#757575', 78 | 'png', 79 | this.name.substr(0, 1) 80 | ) 81 | }, 82 | date() { 83 | return `2016-${Mock.Random.date('MM-dd')} ${Mock.Random.time( 84 | 'HH:mm:ss' 85 | )}` 86 | }, 87 | }, 88 | ], 89 | 'recentSales|36': [ 90 | { 91 | 'id|+1': 1, 92 | name: '@last', 93 | 'status|1-4': 1, 94 | date() { 95 | return `${Mock.Random.integer(2015, 2016)}-${Mock.Random.date( 96 | 'MM-dd' 97 | )} ${Mock.Random.time('HH:mm:ss')}` 98 | }, 99 | 'price|10-200.1-2': 1, 100 | }, 101 | ], 102 | quote: { 103 | name: 'Joho Doe', 104 | title: 'Graphic Designer', 105 | content: 106 | "I'm selfish, impatient and a little insecure. I make mistakes, I am out of control and at times hard to handle. But if you can't handle me at my worst, then you sure as hell don't deserve me at my best.", 107 | avatar: 108 | 'http://img.hb.aicdn.com/bc442cf0cc6f7940dcc567e465048d1a8d634493198c4-sPx5BR_fw236', 109 | }, 110 | numbers: [ 111 | { 112 | icon: 'pay-circle-o', 113 | color: Color.green, 114 | title: 'Online Review', 115 | number: 2781, 116 | }, 117 | { 118 | icon: 'team', 119 | color: Color.blue, 120 | title: 'New Customers', 121 | number: 3241, 122 | }, 123 | { 124 | icon: 'message', 125 | color: Color.purple, 126 | title: 'Active Projects', 127 | number: 253, 128 | }, 129 | { 130 | icon: 'shopping-cart', 131 | color: Color.red, 132 | title: 'Referrals', 133 | number: 4324, 134 | }, 135 | ], 136 | }) 137 | 138 | module.exports = { 139 | [`GET ${ApiPrefix}/dashboard`](req, res) { 140 | res.json(Dashboard) 141 | }, 142 | } 143 | -------------------------------------------------------------------------------- /mock/post.js: -------------------------------------------------------------------------------- 1 | import { Mock, Constant } from './_utils' 2 | 3 | const { ApiPrefix } = Constant 4 | 5 | let postId = 0 6 | const database = Mock.mock({ 7 | 'data|100': [ 8 | { 9 | id() { 10 | postId += 1 11 | return postId + 10000 12 | }, 13 | 'status|1-2': 1, 14 | title: '@title', 15 | author: '@last', 16 | categories: '@word', 17 | tags: '@word', 18 | 'views|10-200': 1, 19 | 'comments|10-200': 1, 20 | visibility: () => { 21 | return Mock.mock( 22 | '@pick(["Public",' + '"Password protected", ' + '"Private"])' 23 | ) 24 | }, 25 | date: '@dateTime', 26 | image() { 27 | return Mock.Random.image( 28 | '100x100', 29 | Mock.Random.color(), 30 | '#757575', 31 | 'png', 32 | this.author.substr(0, 1) 33 | ) 34 | }, 35 | }, 36 | ], 37 | }).data 38 | 39 | module.exports = { 40 | [`GET ${ApiPrefix}/posts`](req, res) { 41 | const { query } = req 42 | let { pageSize, page, ...other } = query 43 | pageSize = pageSize || 10 44 | page = page || 1 45 | 46 | let newData = database 47 | for (let key in other) { 48 | if ({}.hasOwnProperty.call(other, key)) { 49 | newData = newData.filter(item => { 50 | if ({}.hasOwnProperty.call(item, key)) { 51 | return ( 52 | String(item[key]) 53 | .trim() 54 | .indexOf(decodeURI(other[key]).trim()) > -1 55 | ) 56 | } 57 | return true 58 | }) 59 | } 60 | } 61 | 62 | res.status(200).json({ 63 | data: newData.slice((page - 1) * pageSize, page * pageSize), 64 | total: newData.length, 65 | }) 66 | }, 67 | } 68 | -------------------------------------------------------------------------------- /mock/route.js: -------------------------------------------------------------------------------- 1 | import { Constant } from './_utils' 2 | const { ApiPrefix } = Constant 3 | 4 | const database = [ 5 | { 6 | id: '1', 7 | icon: 'dashboard', 8 | name: 'Dashboard', 9 | zh: { 10 | name: '仪表盘' 11 | }, 12 | 'pt-br': { 13 | name: 'Dashboard' 14 | }, 15 | route: '/dashboard', 16 | }, 17 | { 18 | id: '2', 19 | breadcrumbParentId: '1', 20 | name: 'Users', 21 | zh: { 22 | name: '用户管理' 23 | }, 24 | 'pt-br': { 25 | name: 'Usuário' 26 | }, 27 | icon: 'user', 28 | route: '/user', 29 | }, 30 | { 31 | id: '7', 32 | breadcrumbParentId: '1', 33 | name: 'Posts', 34 | zh: { 35 | name: '用户管理' 36 | }, 37 | 'pt-br': { 38 | name: 'Posts' 39 | }, 40 | icon: 'shopping-cart', 41 | route: '/post', 42 | }, 43 | { 44 | id: '21', 45 | menuParentId: '-1', 46 | breadcrumbParentId: '2', 47 | name: 'User Detail', 48 | zh: { 49 | name: '用户详情' 50 | }, 51 | 'pt-br': { 52 | name: 'Detalhes do usuário' 53 | }, 54 | route: '/user/:id', 55 | }, 56 | { 57 | id: '3', 58 | breadcrumbParentId: '1', 59 | name: 'Request', 60 | zh: { 61 | name: 'Request' 62 | }, 63 | 'pt-br': { 64 | name: 'Requisição' 65 | }, 66 | icon: 'api', 67 | route: '/request', 68 | }, 69 | { 70 | id: '4', 71 | breadcrumbParentId: '1', 72 | name: 'UI Element', 73 | zh: { 74 | name: 'UI组件' 75 | }, 76 | 'pt-br': { 77 | name: 'Elementos UI' 78 | }, 79 | icon: 'camera-o', 80 | }, 81 | { 82 | id: '45', 83 | breadcrumbParentId: '4', 84 | menuParentId: '4', 85 | name: 'Editor', 86 | zh: { 87 | name: 'Editor' 88 | }, 89 | 'pt-br': { 90 | name: 'Editor' 91 | }, 92 | icon: 'edit', 93 | route: '/UIElement/editor', 94 | }, 95 | { 96 | id: '5', 97 | breadcrumbParentId: '1', 98 | name: 'Charts', 99 | zh: { 100 | name: 'Charts' 101 | }, 102 | 'pt-br': { 103 | name: 'Graficos' 104 | }, 105 | icon: 'code-o', 106 | }, 107 | { 108 | id: '51', 109 | breadcrumbParentId: '5', 110 | menuParentId: '5', 111 | name: 'ECharts', 112 | zh: { 113 | name: 'ECharts' 114 | }, 115 | 'pt-br': { 116 | name: 'ECharts' 117 | }, 118 | icon: 'line-chart', 119 | route: '/chart/ECharts', 120 | }, 121 | { 122 | id: '52', 123 | breadcrumbParentId: '5', 124 | menuParentId: '5', 125 | name: 'HighCharts', 126 | zh: { 127 | name: 'HighCharts' 128 | }, 129 | 'pt-br': { 130 | name: 'HighCharts' 131 | }, 132 | icon: 'bar-chart', 133 | route: '/chart/highCharts', 134 | }, 135 | { 136 | id: '53', 137 | breadcrumbParentId: '5', 138 | menuParentId: '5', 139 | name: 'Rechartst', 140 | zh: { 141 | name: 'Rechartst' 142 | }, 143 | 'pt-br': { 144 | name: 'Rechartst' 145 | }, 146 | icon: 'area-chart', 147 | route: '/chart/Recharts', 148 | }, 149 | ] 150 | 151 | module.exports = { 152 | [`GET ${ApiPrefix}/routes`](req, res) { 153 | res.status(200).json(database) 154 | }, 155 | } 156 | -------------------------------------------------------------------------------- /public/america.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 16 | 18 | 20 | 22 | 24 | 26 | 28 | 30 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /public/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luzhihua407/family_tree_ui/debf3185f1d2b85e348c75cb80a4b9e3c811631a/public/avatar.png -------------------------------------------------------------------------------- /public/china.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 10 | 11 | 12 | 14 | 16 | 18 | 20 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luzhihua407/family_tree_ui/debf3185f1d2b85e348c75cb80a4b9e3c811631a/public/favicon.ico -------------------------------------------------------------------------------- /public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Created with Sketch. 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /public/lu.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luzhihua407/family_tree_ui/debf3185f1d2b85e348c75cb80a4b9e3c811631a/public/lu.jpg -------------------------------------------------------------------------------- /scripts/translate.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Youdao Translate, My private account is for testing purposes only. 3 | * Please go to the official account to apply for an account. Thank you for your cooperation. 4 | * APP ID:055c2d71f9a05214 5 | * Secret key:ZcpuQxQW3NkQeKVkqrXIKQYXH57g2KuN 6 | */ 7 | 8 | /* eslint-disable */ 9 | const md5 = require('md5') 10 | const qs = require('qs') 11 | const fs = require('fs') 12 | const path = require('path') 13 | const axios = require('axios') 14 | const jsonFormat = require('json-format') 15 | const { i18n } = require('../src/utils/config') 16 | const { languages, defaultLanguage } = i18n 17 | 18 | const locales = {} 19 | 20 | languages.forEach(item => { 21 | locales[item.key] = require(`../src/locales/${item.key}/messages.json`) 22 | }) 23 | 24 | const youdao = ({ q, from, to }) => 25 | new Promise((resolve, reject) => { 26 | { 27 | const appid = '055c2d71f9a05214' 28 | const appse = 'ZcpuQxQW3NkQeKVkqrXIKQYXH57g2KuN' 29 | const salt = Date.now() 30 | 31 | const sign = md5(appid + q + salt + appse) 32 | const query = qs.stringify({ 33 | q, 34 | from, 35 | to, 36 | appKey: appid, 37 | salt, 38 | sign, 39 | }) 40 | 41 | axios.get(`http://openapi.youdao.com/api?${query}`).then(({ data }) => { 42 | if (data.query && data.translation[0]) { 43 | resolve(data.translation[0]) 44 | } else { 45 | resolve(q) 46 | } 47 | }) 48 | } 49 | }) 50 | 51 | const transform = async ({ from, to, locales, outputPath }) => { 52 | for (const key in locales[from]) { 53 | if (locales[to][key]) { 54 | console.log(`add to skip: ${key}`) 55 | } else { 56 | let res = key 57 | let way = 'youdao' 58 | if (key.indexOf('/') !== 0) { 59 | const reg = '{([^{}]*)}' 60 | const tasks = key 61 | .match(new RegExp(`${reg}|((?<=(${reg}|^)).*?(?=(${reg}|$)))`, 'g')) 62 | .map(item => { 63 | if (new RegExp(reg).test(item)) { 64 | return Promise.resolve(item) 65 | } 66 | return youdao({ 67 | q: item, 68 | from, 69 | to, 70 | }) 71 | }) 72 | 73 | res = (await Promise.all(tasks)).join('') 74 | } else { 75 | res = `/${to + key}` 76 | way = 'link' 77 | } 78 | if (res !== key) { 79 | locales[to][key] = res 80 | console.log(`${way}: ${from} -> ${to}: ${key} -> ${res}`) 81 | } else { 82 | console.log(`same: ${from} -> ${to}: ${key}`) 83 | } 84 | } 85 | } 86 | await fs.writeFileSync( 87 | path.resolve(__dirname, outputPath), 88 | jsonFormat(locales[to], { 89 | type: 'space', 90 | size: 2, 91 | }) 92 | ) 93 | } 94 | ;(async () => { 95 | const tasks = languages 96 | .map(item => ({ 97 | from: defaultLanguage, 98 | to: item.key, 99 | })) 100 | .filter(item => item.from !== item.to) 101 | 102 | for (const item of tasks) { 103 | console.log(`start: ${item.from} -> ${item.to}`) 104 | await transform({ 105 | from: item.from, 106 | to: item.to, 107 | locales, 108 | outputPath: `../src/locales/${item.to}/messages.json`, 109 | }) 110 | console.log(`completed: ${item.from} -> ${item.to}`) 111 | } 112 | 113 | console.log('All translations have been completed.') 114 | })() 115 | -------------------------------------------------------------------------------- /src/components/DropOption/DropOption.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { Dropdown, Button, Icon, Menu } from 'antd' 4 | 5 | const DropOption = ({ 6 | onMenuClick, 7 | menuOptions = [], 8 | buttonStyle, 9 | dropdownProps, 10 | }) => { 11 | const menu = menuOptions.map(item => ( 12 | {item.name} 13 | )) 14 | return ( 15 | {menu}} 17 | {...dropdownProps} 18 | > 19 | 23 | 24 | ) 25 | } 26 | 27 | DropOption.propTypes = { 28 | onMenuClick: PropTypes.func, 29 | menuOptions: PropTypes.array.isRequired, 30 | buttonStyle: PropTypes.object, 31 | dropdownProps: PropTypes.object, 32 | } 33 | 34 | export default DropOption 35 | -------------------------------------------------------------------------------- /src/components/DropOption/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "DropOption", 3 | "version": "0.0.0", 4 | "private": true, 5 | "main": "./DropOption.js" 6 | } 7 | -------------------------------------------------------------------------------- /src/components/Editor/Editor.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Editor } from 'react-draft-wysiwyg' 3 | import 'react-draft-wysiwyg/dist/react-draft-wysiwyg.css' 4 | import styles from './Editor.less' 5 | 6 | const DraftEditor = props => { 7 | return ( 8 | 14 | ) 15 | } 16 | 17 | export default DraftEditor 18 | -------------------------------------------------------------------------------- /src/components/Editor/Editor.less: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | height: 500px; 3 | 4 | :global { 5 | .rdw-dropdownoption-default { 6 | padding: 6px; 7 | } 8 | 9 | .rdw-dropdown-optionwrapper { 10 | box-sizing: content-box; 11 | width: 100%; 12 | border-radius: 0 0 2px 2px; 13 | &:hover { 14 | box-shadow: none; 15 | } 16 | } 17 | 18 | .rdw-inline-wrapper { 19 | flex-wrap: wrap; 20 | margin-bottom: 0; 21 | 22 | .rdw-option-wrapper { 23 | margin-bottom: 6px; 24 | } 25 | } 26 | 27 | .rdw-option-active { 28 | box-shadow: 1px 1px 0 #e8e8e8 inset; 29 | } 30 | 31 | .rdw-colorpicker-option { 32 | box-shadow: none; 33 | } 34 | 35 | .rdw-colorpicker-modal, 36 | .rdw-embedded-modal, 37 | .rdw-emoji-modal, 38 | .rdw-image-modal, 39 | .rdw-link-modal { 40 | box-shadow: 4px 4px 40px rgba(0, 0, 0, 0.05); 41 | } 42 | 43 | .rdw-colorpicker-modal, 44 | .rdw-embedded-modal, 45 | .rdw-link-modal { 46 | height: auto; 47 | } 48 | 49 | .rdw-emoji-modal { 50 | width: 214px; 51 | } 52 | 53 | .rdw-colorpicker-modal { 54 | width: auto; 55 | } 56 | 57 | .rdw-embedded-modal-btn, 58 | .rdw-image-modal-btn, 59 | .rdw-link-modal-btn { 60 | height: 32px; 61 | margin-top: 12px; 62 | } 63 | 64 | .rdw-embedded-modal-input, 65 | .rdw-embedded-modal-size-input, 66 | .rdw-link-modal-input { 67 | padding: 2px 6px; 68 | height: 32px; 69 | } 70 | 71 | .rdw-dropdown-selectedtext { 72 | color: #000; 73 | } 74 | 75 | .rdw-dropdown-wrapper, 76 | .rdw-option-wrapper { 77 | min-width: 36px; 78 | transition: all 0.2s ease; 79 | height: 30px; 80 | 81 | &:active { 82 | box-shadow: 1px 1px 0 #e8e8e8 inset; 83 | } 84 | 85 | &:hover { 86 | box-shadow: 1px 1px 0 #e8e8e8; 87 | } 88 | } 89 | 90 | .rdw-dropdown-wrapper { 91 | min-width: 60px; 92 | } 93 | 94 | .rdw-editor-main { 95 | box-sizing: border-box; 96 | } 97 | } 98 | 99 | .editor { 100 | border: 1px solid #f1f1f1; 101 | padding: 5px; 102 | border-radius: 2px; 103 | height: auto; 104 | min-height: 200px; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/components/Editor/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Editor", 3 | "version": "0.0.0", 4 | "private": true, 5 | "main": "./Editor.js" 6 | } 7 | -------------------------------------------------------------------------------- /src/components/FilterItem/FilterItem.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import styles from './FilterItem.less' 4 | 5 | const FilterItem = ({ label = '', children }) => { 6 | const labelArray = label.split('') 7 | return ( 8 |
9 | {labelArray.length > 0 ? ( 10 |
11 | {labelArray.map((item, index) => ( 12 | 13 | {item} 14 | 15 | ))} 16 |
17 | ) : ( 18 | '' 19 | )} 20 |
{children}
21 |
22 | ) 23 | } 24 | 25 | FilterItem.propTypes = { 26 | label: PropTypes.string, 27 | children: PropTypes.element.isRequired, 28 | } 29 | 30 | export default FilterItem 31 | -------------------------------------------------------------------------------- /src/components/FilterItem/FilterItem.less: -------------------------------------------------------------------------------- 1 | .filterItem { 2 | display: flex; 3 | justify-content: space-between; 4 | 5 | .labelWrap { 6 | width: 64px; 7 | line-height: 28px; 8 | margin-right: 12px; 9 | justify-content: space-between; 10 | display: flex; 11 | overflow: hidden; 12 | } 13 | 14 | .item { 15 | flex: 1; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/components/FilterItem/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "FilterItem", 3 | "version": "0.0.0", 4 | "private": true, 5 | "main": "./FilterItem.js" 6 | } 7 | -------------------------------------------------------------------------------- /src/components/Layout/Bread.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent, Fragment } from 'react' 2 | import PropTypes from 'prop-types' 3 | import { Breadcrumb, Icon } from 'antd' 4 | import Link from 'umi/navlink' 5 | import withRouter from 'umi/withRouter' 6 | import { withI18n } from '@lingui/react' 7 | import { pathMatchRegexp, queryAncestors, addLangPrefix } from 'utils' 8 | import styles from './Bread.less' 9 | 10 | @withI18n() 11 | @withRouter 12 | class Bread extends PureComponent { 13 | generateBreadcrumbs = paths => { 14 | return paths.map((item, key) => { 15 | const content = item && ( 16 | 17 | {item.icon ? ( 18 | 19 | ) : null} 20 | {item.name} 21 | 22 | ) 23 | 24 | return ( 25 | item && ( 26 | 27 | {paths.length - 1 !== key ? ( 28 | {content} 29 | ) : ( 30 | content 31 | )} 32 | 33 | ) 34 | ) 35 | }) 36 | } 37 | render() { 38 | const { routeList, location, i18n } = this.props 39 | 40 | // Find a route that matches the pathname. 41 | const currentRoute = routeList.find( 42 | _ => _.route && pathMatchRegexp(_.route, location.pathname) 43 | ) 44 | 45 | // Find the breadcrumb navigation of the current route match and all its ancestors. 46 | const paths = currentRoute 47 | ? queryAncestors(routeList, currentRoute, 'breadcrumbParentId').reverse() 48 | : [ 49 | routeList[0], 50 | { 51 | id: 404, 52 | name: i18n.t`Not Found`, 53 | }, 54 | ] 55 | 56 | return ( 57 | 58 | {this.generateBreadcrumbs(paths)} 59 | 60 | ) 61 | } 62 | } 63 | 64 | Bread.propTypes = { 65 | routeList: PropTypes.array, 66 | } 67 | 68 | export default Bread 69 | -------------------------------------------------------------------------------- /src/components/Layout/Bread.less: -------------------------------------------------------------------------------- 1 | .bread { 2 | margin-bottom: 24px; 3 | 4 | :global { 5 | .ant-breadcrumb { 6 | display: flex; 7 | align-items: center; 8 | } 9 | } 10 | } 11 | 12 | @media (max-width: 767px) { 13 | .bread { 14 | margin-bottom: 12px; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/components/Layout/Sider.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react' 2 | import PropTypes from 'prop-types' 3 | import { Icon, Switch, Layout } from 'antd' 4 | import { withI18n, Trans } from '@lingui/react' 5 | import ScrollBar from '../ScrollBar' 6 | import { config } from 'utils' 7 | import SiderMenu from './Menu' 8 | import styles from './Sider.less' 9 | 10 | @withI18n() 11 | class Sider extends PureComponent { 12 | render() { 13 | const { 14 | i18n, 15 | menus, 16 | theme, 17 | isMobile, 18 | collapsed, 19 | onThemeChange, 20 | onCollapseChange, 21 | } = this.props 22 | 23 | return ( 24 | 34 |
35 |
36 | logo 37 | {collapsed ? null :

{config.siteName}

} 38 |
39 |
40 | 41 |
42 | 48 | 55 | 56 |
57 | {collapsed ? null : ( 58 |
59 | 60 | 61 | Switch Theme 62 | 63 | 72 |
73 | )} 74 |
75 | ) 76 | } 77 | } 78 | 79 | Sider.propTypes = { 80 | menus: PropTypes.array, 81 | theme: PropTypes.string, 82 | isMobile: PropTypes.bool, 83 | collapsed: PropTypes.bool, 84 | onThemeChange: PropTypes.func, 85 | onCollapseChange: PropTypes.func, 86 | } 87 | 88 | export default Sider 89 | -------------------------------------------------------------------------------- /src/components/Layout/Sider.less: -------------------------------------------------------------------------------- 1 | @import '~themes/vars.less'; 2 | 3 | .sider { 4 | box-shadow: fade(@primary-color, 10%) 0 0 28px 0; 5 | z-index: 10; 6 | :global { 7 | .ant-layout-sider-children { 8 | display: flex; 9 | flex-direction: column; 10 | justify-content: space-between; 11 | } 12 | } 13 | } 14 | 15 | .brand { 16 | z-index: 1; 17 | height: 72px; 18 | display: flex; 19 | align-items: center; 20 | justify-content: center; 21 | padding: 0 24px; 22 | box-shadow: 0 1px 9px -3px rgba(0, 0, 0, 0.2); 23 | .logo { 24 | display: flex; 25 | align-items: center; 26 | justify-content: center; 27 | 28 | img { 29 | width: 36px; 30 | margin-right: 8px; 31 | } 32 | 33 | h1 { 34 | vertical-align: text-bottom; 35 | font-size: 16px; 36 | text-transform: uppercase; 37 | display: inline-block; 38 | font-weight: 700; 39 | color: @primary-color; 40 | white-space: nowrap; 41 | margin-bottom: 0; 42 | .text-gradient(); 43 | 44 | :local { 45 | animation: fadeRightIn 300ms @ease-in-out; 46 | animation-fill-mode: both; 47 | } 48 | } 49 | } 50 | } 51 | 52 | .menuContainer { 53 | height: ~'calc(100vh - 120px)'; 54 | overflow-x: hidden; 55 | flex: 1; 56 | padding: 24px 0; 57 | 58 | &::-webkit-scrollbar-thumb { 59 | background-color: transparent; 60 | } 61 | 62 | &:hover { 63 | &::-webkit-scrollbar-thumb { 64 | background-color: rgba(0, 0, 0, 0.2); 65 | } 66 | } 67 | 68 | :global { 69 | .ant-menu-inline { 70 | border-right: none; 71 | } 72 | } 73 | } 74 | 75 | .switchTheme { 76 | width: 100%; 77 | height: 48px; 78 | display: flex; 79 | justify-content: space-between; 80 | align-items: center; 81 | padding: 0 16px; 82 | overflow: hidden; 83 | transition: all 0.3s; 84 | 85 | span { 86 | white-space: nowrap; 87 | overflow: hidden; 88 | font-size: 12px; 89 | } 90 | 91 | :global { 92 | .anticon { 93 | min-width: 14px; 94 | margin-right: 4px; 95 | font-size: 14px; 96 | } 97 | } 98 | } 99 | 100 | @keyframes fadeLeftIn { 101 | 0% { 102 | transform: translateX(5px); 103 | opacity: 0; 104 | } 105 | 106 | 100% { 107 | transform: translateX(0); 108 | opacity: 1; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/components/Layout/index.js: -------------------------------------------------------------------------------- 1 | import Header from './Header' 2 | import Menu from './Menu' 3 | import Bread from './Bread' 4 | import Sider from './Sider' 5 | 6 | export { Header, Menu, Bread, Sider } 7 | -------------------------------------------------------------------------------- /src/components/Loader/Loader.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import classNames from 'classnames' 4 | import styles from './Loader.less' 5 | 6 | const Loader = ({ spinning = true, fullScreen }) => { 7 | return ( 8 |
14 |
15 |
16 |
加载中
17 |
18 |
19 | ) 20 | } 21 | 22 | Loader.propTypes = { 23 | spinning: PropTypes.bool, 24 | fullScreen: PropTypes.bool, 25 | } 26 | 27 | export default Loader 28 | -------------------------------------------------------------------------------- /src/components/Loader/Loader.less: -------------------------------------------------------------------------------- 1 | .loader { 2 | background-color: #fff; 3 | width: 100%; 4 | position: absolute; 5 | top: 0; 6 | bottom: 0; 7 | left: 0; 8 | z-index: 100000; 9 | display: flex; 10 | justify-content: center; 11 | align-items: center; 12 | opacity: 1; 13 | text-align: center; 14 | 15 | &.fullScreen { 16 | position: fixed; 17 | } 18 | 19 | .warpper { 20 | width: 100px; 21 | height: 100px; 22 | display: inline-flex; 23 | flex-direction: column; 24 | justify-content: space-around; 25 | } 26 | 27 | .inner { 28 | width: 40px; 29 | height: 40px; 30 | margin: 0 auto; 31 | text-indent: -12345px; 32 | border-top: 1px solid rgba(0, 0, 0, 0.08); 33 | border-right: 1px solid rgba(0, 0, 0, 0.08); 34 | border-bottom: 1px solid rgba(0, 0, 0, 0.08); 35 | border-left: 1px solid rgba(0, 0, 0, 0.7); 36 | border-radius: 50%; 37 | z-index: 100001; 38 | 39 | :local { 40 | animation: spinner 600ms infinite linear; 41 | } 42 | } 43 | 44 | .text { 45 | width: 100px; 46 | height: 20px; 47 | text-align: center; 48 | font-size: 12px; 49 | letter-spacing: 4px; 50 | color: #000; 51 | } 52 | 53 | &.hidden { 54 | z-index: -1; 55 | opacity: 0; 56 | transition: opacity 1s ease 0.5s, z-index 0.1s ease 1.5s; 57 | } 58 | } 59 | @keyframes spinner { 60 | 0% { 61 | transform: rotate(0deg); 62 | } 63 | 64 | 100% { 65 | transform: rotate(360deg); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/components/Loader/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Loader", 3 | "version": "0.0.0", 4 | "private": true, 5 | "main": "./Loader.js" 6 | } 7 | -------------------------------------------------------------------------------- /src/components/Page/Page.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import classnames from 'classnames' 4 | import Loader from '../Loader' 5 | import styles from './Page.less' 6 | 7 | export default class Page extends Component { 8 | render() { 9 | const { className, children, loading = false, inner = false } = this.props 10 | const loadingStyle = { 11 | height: 'calc(100vh - 184px)', 12 | overflow: 'hidden', 13 | } 14 | return ( 15 |
21 | {loading ? : ''} 22 | {children} 23 |
24 | ) 25 | } 26 | } 27 | 28 | Page.propTypes = { 29 | className: PropTypes.string, 30 | children: PropTypes.node, 31 | loading: PropTypes.bool, 32 | inner: PropTypes.bool, 33 | } 34 | -------------------------------------------------------------------------------- /src/components/Page/Page.less: -------------------------------------------------------------------------------- 1 | @import '~themes/vars.less'; 2 | 3 | .contentInner { 4 | background: #fff; 5 | padding: 24px; 6 | box-shadow: @shadow-1; 7 | min-height: ~'calc(100vh - 230px)'; 8 | position: relative; 9 | } 10 | 11 | @media (max-width: 767px) { 12 | .contentInner { 13 | padding: 12px; 14 | min-height: ~'calc(100vh - 160px)'; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/components/Page/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Page", 3 | "version": "0.0.0", 4 | "private": true, 5 | "main": "./Page.js" 6 | } 7 | -------------------------------------------------------------------------------- /src/components/ScrollBar/index.js: -------------------------------------------------------------------------------- 1 | import ScrollBar from 'react-perfect-scrollbar' 2 | import 'react-perfect-scrollbar/dist/css/styles.css' 3 | import './index.less' 4 | 5 | export default ScrollBar 6 | -------------------------------------------------------------------------------- /src/components/ScrollBar/index.less: -------------------------------------------------------------------------------- 1 | :global { 2 | .ps--active-x > .ps__rail-x, 3 | .ps--active-y > .ps__rail-y { 4 | background-color: transparent; 5 | } 6 | 7 | .ps__rail-x:hover > .ps__thumb-x, 8 | .ps__rail-x:focus > .ps__thumb-x { 9 | height: 8px; 10 | } 11 | 12 | .ps__rail-y:hover > .ps__thumb-y, 13 | .ps__rail-y:focus > .ps__thumb-y { 14 | width: 8px; 15 | } 16 | 17 | .ps__rail-y, 18 | .ps__rail-x { 19 | z-index: 9; 20 | } 21 | 22 | .ps__thumb-y { 23 | width: 4px; 24 | right: 4px; 25 | } 26 | 27 | .ps__thumb-x { 28 | height: 4px; 29 | bottom: 4px; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/components/index.js: -------------------------------------------------------------------------------- 1 | import Editor from './Editor' 2 | import FilterItem from './FilterItem' 3 | import DropOption from './DropOption' 4 | import Loader from './Loader' 5 | import ScrollBar from './ScrollBar' 6 | import * as MyLayout from './Layout/index.js' 7 | import Page from './Page' 8 | 9 | export { MyLayout, Editor, FilterItem, DropOption, Loader, Page, ScrollBar } 10 | -------------------------------------------------------------------------------- /src/e2e/login.e2e.js: -------------------------------------------------------------------------------- 1 | import puppeteer from 'puppeteer' 2 | 3 | describe('Login', () => { 4 | let browser 5 | let page 6 | 7 | beforeAll(async () => { 8 | browser = await puppeteer.launch({ args: ['--no-sandbox'] }) 9 | }) 10 | 11 | beforeEach(async () => { 12 | page = await browser.newPage() 13 | await page.goto('http://localhost:8000/en/login', { 14 | waitUntil: 'networkidle2', 15 | }) 16 | }) 17 | 18 | afterEach(() => page.close()) 19 | 20 | it('should login with failure', async () => { 21 | await page.waitFor(selector => !!document.querySelector('#username'), { 22 | timeout: 3000, 23 | }) 24 | await page.type('#username', 'wrong_user') 25 | await page.type('#password', 'wrong_password') 26 | await page.click('button[type="button"]') 27 | await page.waitForSelector('.anticon-close-circle') // should display error 28 | }) 29 | 30 | it('should login successfully', async () => { 31 | await page.waitForSelector('#username', { timeout: 3000 }) 32 | await page.type('#username', 'admin') 33 | await page.type('#password', 'admin') 34 | await page.click('button[type="button"]') 35 | await page.waitForSelector('.ant-layout-footer') 36 | const text = await page.evaluate(() => document.body.innerHTML) 37 | expect(text).toContain('Ant Design Admin') 38 | }) 39 | 40 | afterAll(() => browser.close()) 41 | }) 42 | -------------------------------------------------------------------------------- /src/layouts/BaseLayout.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent, Fragment } from 'react' 2 | import PropTypes from 'prop-types' 3 | import { connect } from 'dva' 4 | import { Helmet } from 'react-helmet' 5 | import { Loader } from 'components' 6 | import { queryLayout } from 'utils' 7 | import NProgress from 'nprogress' 8 | import config from 'utils/config' 9 | import withRouter from 'umi/withRouter' 10 | 11 | import PublicLayout from './PublicLayout' 12 | import PrimaryLayout from './PrimaryLayout' 13 | import './BaseLayout.less' 14 | 15 | const LayoutMap = { 16 | primary: PrimaryLayout, 17 | public: PublicLayout, 18 | } 19 | 20 | @withRouter 21 | @connect(({ loading }) => ({ loading })) 22 | class BaseLayout extends PureComponent { 23 | previousPath = '' 24 | 25 | render() { 26 | const { loading, children, location } = this.props 27 | const Container = LayoutMap[queryLayout(config.layouts, location.pathname)] 28 | const currentPath = location.pathname + location.search 29 | if (currentPath !== this.previousPath) { 30 | NProgress.start() 31 | } 32 | 33 | if (!loading.global) { 34 | NProgress.done() 35 | this.previousPath = currentPath 36 | } 37 | 38 | return ( 39 | 40 | 41 | {config.siteName} 42 | 43 | 44 | {children} 45 | 46 | ) 47 | } 48 | } 49 | 50 | BaseLayout.propTypes = { 51 | loading: PropTypes.object, 52 | } 53 | 54 | export default BaseLayout 55 | -------------------------------------------------------------------------------- /src/layouts/BaseLayout.less: -------------------------------------------------------------------------------- 1 | @import '~themes/vars.less'; 2 | @import '~themes/index.less'; 3 | 4 | :global { 5 | #nprogress { 6 | pointer-events: none; 7 | 8 | .bar { 9 | background: @primary-color; 10 | position: fixed; 11 | z-index: 2048; 12 | top: 0; 13 | left: 0; 14 | right: 0; 15 | width: 100%; 16 | height: 2px; 17 | } 18 | 19 | .peg { 20 | display: block; 21 | position: absolute; 22 | right: 0; 23 | width: 100px; 24 | height: 100%; 25 | box-shadow: 0 0 10px @primary-color, 0 0 5px @primary-color; 26 | opacity: 1; 27 | transform: rotate(3deg) translate(0, -4px); 28 | } 29 | 30 | .spinner { 31 | display: block; 32 | position: fixed; 33 | z-index: 1031; 34 | top: 15px; 35 | right: 15px; 36 | } 37 | 38 | .spinner-icon { 39 | width: 18px; 40 | height: 18px; 41 | box-sizing: border-box; 42 | border: solid 2px transparent; 43 | border-top-color: @primary-color; 44 | border-left-color: @primary-color; 45 | border-radius: 50%; 46 | 47 | :local { 48 | animation: nprogress-spinner 400ms linear infinite; 49 | } 50 | } 51 | } 52 | 53 | .nprogress-custom-parent { 54 | overflow: hidden; 55 | position: relative; 56 | 57 | #nprogress { 58 | .bar, 59 | .spinner { 60 | position: absolute; 61 | } 62 | } 63 | } 64 | } 65 | 66 | @keyframes nprogress-spinner { 67 | 0% { 68 | transform: rotate(0deg); 69 | } 70 | 71 | 100% { 72 | transform: rotate(360deg); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/layouts/PrimaryLayout.less: -------------------------------------------------------------------------------- 1 | @import '~themes/vars.less'; 2 | 3 | .backTop { 4 | right: 50px; 5 | 6 | :global { 7 | .ant-back-top-content { 8 | background: @primary-color; 9 | opacity: 0.3; 10 | transition: all 0.3s; 11 | box-shadow: 0 0 15px 1px rgba(69, 65, 78, 0.1); 12 | 13 | &:hover { 14 | opacity: 1; 15 | } 16 | } 17 | } 18 | } 19 | 20 | .content { 21 | padding: 24px; 22 | min-height: ~'calc(100% - 72px)'; 23 | // overflow-y: scroll; 24 | } 25 | 26 | .container { 27 | height: 100vh; 28 | flex: 1; 29 | width: ~'calc(100% - 256px)'; 30 | overflow-y: scroll; 31 | overflow-x: hidden; 32 | } 33 | 34 | .footer { 35 | background: #fff; 36 | margin-top: 0; 37 | margin-bottom: 0; 38 | padding-top: 24px; 39 | padding-bottom: 24px; 40 | min-height: 72px; 41 | } 42 | 43 | @media (max-width: 767px) { 44 | .content { 45 | padding: 12px; 46 | } 47 | 48 | .backTop { 49 | right: 20px; 50 | bottom: 20px; 51 | } 52 | 53 | .container { 54 | height: 100vh; 55 | flex: 1; 56 | width: 100%; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/layouts/PublicLayout.js: -------------------------------------------------------------------------------- 1 | export default ({ children }) => { 2 | return children 3 | } 4 | -------------------------------------------------------------------------------- /src/layouts/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import withRouter from 'umi/withRouter' 3 | import { I18nProvider } from '@lingui/react' 4 | import { ConfigProvider } from 'antd' 5 | import { langFromPath, defaultLanguage } from 'utils' 6 | import zh_CN from 'antd/lib/locale-provider/zh_CN' 7 | import BaseLayout from './BaseLayout' 8 | const languages = { 9 | zh: zh_CN, 10 | } 11 | const locale = zh_CN 12 | @withRouter 13 | class Layout extends Component { 14 | state = { 15 | catalogs: {}, 16 | } 17 | 18 | language = defaultLanguage 19 | 20 | componentDidMount() { 21 | const language = langFromPath(this.props.location.pathname) 22 | this.language = language 23 | this.loadCatalog(language) 24 | } 25 | 26 | shouldComponentUpdate(nextProps, nextState) { 27 | const language = langFromPath(nextProps.location.pathname) 28 | const preLanguage = this.language 29 | const { catalogs } = nextState 30 | 31 | if (preLanguage !== language && !catalogs[language]) { 32 | this.loadCatalog(language) 33 | this.language = language 34 | return false 35 | } 36 | this.language = language 37 | 38 | return true 39 | } 40 | 41 | loadCatalog = async language => { 42 | const catalog = await import( 43 | /* webpackMode: "lazy", webpackChunkName: "i18n-[index]" */ 44 | `@lingui/loader!../locales/zh/messages.json` 45 | ) 46 | 47 | this.setState(state => ({ 48 | catalogs: { 49 | ...state.catalogs, 50 | [language]: catalog, 51 | }, 52 | })) 53 | } 54 | 55 | render() { 56 | const { location, children } = this.props 57 | const { catalogs } = this.state 58 | 59 | let language = langFromPath(location.pathname) 60 | // If the language pack is not loaded or is loading, use the default language 61 | if (!catalogs[language]) language = defaultLanguage 62 | 63 | return ( 64 | 65 | 66 | {children} 67 | 68 | 69 | ) 70 | } 71 | } 72 | 73 | export default Layout 74 | -------------------------------------------------------------------------------- /src/locales/zh/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "/dashboard": "/zh/dashboard", 3 | "Add Param": "添加参数", 4 | "Address": "地址", 5 | "Age": "年龄", 6 | "Are you sure delete this record?": "您确定要删除这条记录吗?", 7 | "Author": "作者", 8 | "Avatar": "头像", 9 | "Categories": "类别", 10 | "Clear notifications": "清空消息", 11 | "Comments": "评论数", 12 | "Create": "创建", 13 | "Create User": "创建用户", 14 | "CreateTime": "创建时间", 15 | "Dark": "暗", 16 | "Delete": "删除", 17 | "Email": "电子邮件", 18 | "Female": "女", 19 | "Gender": "性别", 20 | "Hi,": "你好,", 21 | "Image": "图像", 22 | "Light": "明", 23 | "Male": "男性", 24 | "Name": "名字", 25 | "NickName": "昵称", 26 | "Not Found": "未找到", 27 | "Operation": "操作", 28 | "Params": "参数", 29 | "Password": "密码", 30 | "Phone": "电话", 31 | "Pick an address": "选择地址", 32 | "Please pick an address": "选择地址", 33 | "Publised": "已发布", 34 | "Publish Date": "发布日期", 35 | "Reset": "重置", 36 | "Search": "搜索", 37 | "Search Name": "搜索名字", 38 | "Send": "发送", 39 | "Sign in": "登录", 40 | "Sign out": "退出登录", 41 | "Switch Theme": "切换主题", 42 | "Tags": "标签", 43 | "The input is not valid E-mail!": "输入的电子邮件无效!", 44 | "The input is not valid phone!": "输入无效的手机!", 45 | "Title": "标题", 46 | "Total {total} Items": "总共 {total} 条记录", 47 | "Unpublished": "未发布", 48 | "Update": "修改", 49 | "Update User": "修改用户", 50 | "Username": "用户名", 51 | "Views": "浏览数", 52 | "Visibility": "可见性", 53 | "You have viewed all notifications.": "您已查看所有通知" 54 | } 55 | -------------------------------------------------------------------------------- /src/pages/.umi/DvaContainer.js: -------------------------------------------------------------------------------- 1 | import { Component } from 'react'; 2 | 3 | class DvaContainer extends Component { 4 | render() { 5 | window.g_app.router(() => this.props.children); 6 | return window.g_app.start()(); 7 | } 8 | } 9 | 10 | export default DvaContainer; 11 | -------------------------------------------------------------------------------- /src/pages/.umi/initDva.js: -------------------------------------------------------------------------------- 1 | import dva from 'dva'; 2 | import createLoading from 'dva-loading'; 3 | 4 | const runtimeDva = window.g_plugins.mergeConfig('dva'); 5 | let app = dva({ 6 | history: window.g_history, 7 | 8 | ...(runtimeDva.config || {}), 9 | }); 10 | 11 | window.g_app = app; 12 | app.use(createLoading()); 13 | (runtimeDva.plugins || []).forEach(plugin => { 14 | app.use(plugin); 15 | }); 16 | app.use(require('../../plugins/onError.js').default); 17 | app.use(require('D:/antd-admin/antd_family_tree/node_modules/_dva-immer@0.2.4@dva-immer/lib/index.js').default()); 18 | app.model({ namespace: 'app', ...(require('D:/antd-admin/antd_family_tree/src/models/app.js').default) }); 19 | -------------------------------------------------------------------------------- /src/pages/.umi/initHistory.js: -------------------------------------------------------------------------------- 1 | // create history 2 | window.g_history = require('umi/_createHistory').default({ 3 | basename: window.routerBase, 4 | }); 5 | -------------------------------------------------------------------------------- /src/pages/.umi/polyfills.js: -------------------------------------------------------------------------------- 1 | import '@babel/polyfill'; 2 | 3 | // Include this seperatly since it's not included in core-js 4 | // ref: https://github.com/zloirock/core-js/issues/117 5 | import '../../../node_modules/_url-polyfill@1.1.3@url-polyfill/url-polyfill.js'; 6 | -------------------------------------------------------------------------------- /src/pages/.umi/umi.js: -------------------------------------------------------------------------------- 1 | import './polyfills'; 2 | import '@tmp/initHistory'; 3 | 4 | import React from 'react'; 5 | import ReactDOM from 'react-dom'; 6 | 7 | 8 | // runtime plugins 9 | window.g_plugins = require('umi/_runtimePlugin'); 10 | window.g_plugins.init({ 11 | validKeys: ['patchRoutes','render','rootContainer','modifyRouteProps','onRouteChange','dva',], 12 | }); 13 | window.g_plugins.use(require('../../../node_modules/_umi-plugin-dva@1.5.3@umi-plugin-dva/lib/runtime')); 14 | 15 | require('@tmp/initDva'); 16 | 17 | // render 18 | let oldRender = () => { 19 | const rootContainer = window.g_plugins.apply('rootContainer', { 20 | initialValue: React.createElement(require('./router').default), 21 | }); 22 | ReactDOM.render( 23 | rootContainer, 24 | document.getElementById('root'), 25 | ); 26 | }; 27 | const render = window.g_plugins.compose('render', { initialValue: oldRender }); 28 | 29 | const moduleBeforeRendererPromises = []; 30 | 31 | Promise.all(moduleBeforeRendererPromises).then(() => { 32 | render(); 33 | }).catch((err) => { 34 | window.console && window.console.error(err); 35 | }); 36 | 37 | 38 | 39 | // hot module replacement 40 | if (module.hot) { 41 | module.hot.accept('./router', () => { 42 | oldRender(); 43 | }); 44 | } 45 | -------------------------------------------------------------------------------- /src/pages/404.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Icon } from 'antd' 3 | import { Page } from 'components' 4 | import styles from './404.less' 5 | 6 | const Error = () => ( 7 | 8 |
9 | 10 |

404 找不到页面

11 |
12 |
13 | ) 14 | 15 | export default Error 16 | -------------------------------------------------------------------------------- /src/pages/404.less: -------------------------------------------------------------------------------- 1 | .error { 2 | color: black; 3 | text-align: center; 4 | position: absolute; 5 | top: 30%; 6 | margin-top: -50px; 7 | left: 50%; 8 | margin-left: -100px; 9 | width: 200px; 10 | 11 | :global .anticon { 12 | font-size: 48px; 13 | margin-bottom: 16px; 14 | } 15 | 16 | h1 { 17 | font-family: cursive; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/pages/UIElement/editor/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Editor } from 'components' 3 | import { convertToRaw } from 'draft-js' 4 | import { Row, Col, Card } from 'antd' 5 | import draftToHtml from 'draftjs-to-html' 6 | import draftToMarkdown from 'draftjs-to-markdown' 7 | 8 | export default class EditorPage extends React.Component { 9 | constructor(props) { 10 | super(props) 11 | this.state = { 12 | editorContent: null, 13 | } 14 | } 15 | 16 | onEditorStateChange = editorContent => { 17 | this.setState({ 18 | editorContent, 19 | }) 20 | } 21 | 22 | render() { 23 | const { editorContent } = this.state 24 | const colProps = { 25 | lg: 12, 26 | md: 24, 27 | style: { 28 | marginBottom: 32, 29 | }, 30 | } 31 | const textareaStyle = { 32 | minHeight: 496, 33 | width: '100%', 34 | background: '#f7f7f7', 35 | borderColor: '#F1F1F1', 36 | padding: '16px 8px', 37 | } 38 | return ( 39 |
40 | 41 | 42 | 43 | 53 | 54 | 55 | 56 | 57 |