├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitignore ├── README.md ├── index.html ├── mock ├── index.js └── utils.js ├── package.json ├── proxy.config.js ├── src ├── carno │ ├── components │ │ ├── FilterBox │ │ │ ├── demo │ │ │ │ └── Base.js │ │ │ ├── doc.md │ │ │ ├── index.js │ │ │ └── index.less │ │ ├── Form │ │ │ ├── Form.js │ │ │ ├── FormItem.js │ │ │ ├── SearchForm.js │ │ │ ├── demo │ │ │ │ ├── HFormBase.js │ │ │ │ └── HFormItemBase.js │ │ │ ├── doc.md │ │ │ └── index.less │ │ ├── Modal │ │ │ ├── demo │ │ │ │ ├── ConfirmModalBase.js │ │ │ │ └── ModalBase.js │ │ │ ├── doc.md │ │ │ └── index.js │ │ └── SearchBar │ │ │ ├── demo │ │ │ └── Base.js │ │ │ ├── doc.md │ │ │ ├── index.js │ │ │ └── index.less │ ├── index.js │ ├── styles │ │ ├── mixins.less │ │ ├── themes.less │ │ └── variables.less │ └── utils │ │ ├── form │ │ ├── doc.md │ │ ├── fieldTypes.js │ │ └── index.js │ │ ├── http │ │ ├── doc.md │ │ ├── index.js │ │ └── middlewares.js │ │ ├── index.js │ │ ├── model │ │ ├── doc.md │ │ └── index.js │ │ └── table │ │ ├── doc.md │ │ ├── fieldTypes.js │ │ └── index.js ├── components │ ├── BlacklistManage │ │ ├── BlacklistModal.js │ │ ├── SearchMore.js │ │ └── index.js │ ├── Layout │ │ └── index.js │ └── UserManage │ │ ├── UserModal.js │ │ └── index.js ├── configs │ ├── constants.js │ ├── index.js │ ├── menus.js │ └── servers.js ├── index.css ├── index.html ├── index.js ├── models │ ├── blacklistManage │ │ ├── fields.js │ │ └── index.js │ └── userManage │ │ ├── fields.js │ │ └── index.js ├── pages │ ├── BlacklistManage.js │ ├── UserManage.js │ └── index.js ├── router.js ├── services │ └── index.js ├── styles │ └── common.less └── utils │ ├── common.js │ └── http.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015", 4 | "stage-0", 5 | "react" 6 | ], 7 | "plugins": [ 8 | "transform-runtime", 9 | ["import", { "libraryName": "antd", "style": "css" }] 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = native 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 | .eslintignore 2 | proxy.config.js 3 | webpack.config.js 4 | mock/** 5 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "indent": [1, 2, { 4 | "SwitchCase": 1 5 | }], // 缩进使用2个空格 6 | "no-unused-expressions": [1, { 7 | "allowShortCircuit": true, 8 | "allowTernary": true 9 | }], 10 | "max-len": [0, 100], // 一行最多100个字符 11 | "import/no-unresolved": 0, // 关闭导入文件必须是es6写法的限制 12 | "global-require": 0, // 关闭不允许在局部代码块中使用require的限制 13 | "new-cap": 0, // 关闭方法名首字母不能大写的限制 14 | "comma-dangle": 0, // 关闭对象中最后一值必须有个逗号的限制 15 | "no-case-declarations": 0, // 关闭case中不能申明变量的限制 16 | "eqeqeq": 0, // 关闭强制使用===的限制 17 | "no-confusing-arrow": 0, // 关闭箭头函数不能直接用三元表达式的限制 18 | "arrow-body-style": 0, 19 | "react/prop-types": 0, 20 | "no-param-reassign": 0, 21 | "react/jsx-indent": [1, 2], // 缩进使用2个空格 22 | "react/jsx-indent-props": [1, 2], 23 | "react/jsx-no-bind": 0, // 关闭jsx中不能使用bind的限制 24 | "react/prefer-stateless-function": 0 // 关闭纯组件的限制 25 | }, 26 | "extends": "airbnb", 27 | "parserOptions": { 28 | "ecmaVersion": 6, 29 | "ecmaFeatures": { 30 | "experimentalObjectRestSpread": true 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | npm-debug.log 4 | *.sublime-project 5 | .idea 6 | .bat 7 | debug.log 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # admin-demo 2 | 3 | ### 使用 4 | 5 | 执行如下命令: 6 | 7 | ```shell 8 | npm install 9 | npm run start 10 | ``` 11 | 12 | 然后在浏览器中打开 `localhost:8082/` 13 | 14 | ### 说明 15 | 16 | 提供对`dva model`以及`antd`部分组件扩展, 参考: [关于dva实际应用的一些经验以及疑惑](https://github.com/dvajs/dva/issues/886) 17 | `src/carno` 为公共的组件主要提供如下扩展, 对应的源码下面都有文档以及`demo`展示. 18 | 19 | 详细文档如下: 20 | 21 | - [model](./src/carno/utils/model/doc.md) 22 | - [modal](./src/carno/components/Modal/doc.md) 23 | - [form](./src/carno/components/Form/doc.md) 24 | - [table](./src/carno/utils/table/doc.md) 25 | 26 | 27 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | demo 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /mock/index.js: -------------------------------------------------------------------------------- 1 | const mock = require('mockjs'); 2 | const utils = require('./utils'); 3 | 4 | const mockData = mock.mock({ 5 | 'users|1-10': [{ 6 | 'id|+1': 1, 7 | 'age|+1': 15, 8 | 'name|+1': 'john', 9 | 'career': '软件工程师', 10 | 'gender': 'male', 11 | 'createTime': 1482705955000 12 | }], 13 | 'user': { 14 | 'username': 'curie', 15 | 'id': '123', 16 | 'token': 'pass' 17 | }, 18 | 'blacklists|1-10': [{ 19 | "id|+1": 4980, 20 | "type": 'PHONE', 21 | "userId": 0, 22 | "content": "rt345", 23 | "createDate": 1496375531000, 24 | "reason": "测试方要求禁用" 25 | }] 26 | }); 27 | 28 | if (!global.users) { 29 | Object.assign(global, mockData); 30 | } 31 | 32 | 33 | module.exports = { 34 | 'GET /web/user/list': utils.createResponse('users'), 35 | 'POST /web/user/save': utils.createSaveResponse('users'), 36 | 'POST /web/login': utils.createResponse('user'), 37 | "GET /web/blacklist/list": utils.createPageResponse("blacklists"), 38 | "POST /web/blacklist/save": utils.createSaveResponse("blacklists"), 39 | }; 40 | 41 | -------------------------------------------------------------------------------- /mock/utils.js: -------------------------------------------------------------------------------- 1 | const qs = require('qs'); 2 | const delay = 500; 3 | 4 | module.exports = { 5 | 6 | createResponse: function(dataKey) { 7 | return function(req, res) { 8 | setTimeout(function() { 9 | res.json({ 10 | success: true, 11 | content: global[dataKey] 12 | }); 13 | }, delay); 14 | } 15 | }, 16 | 17 | createPageResponse: function(dataKey) { 18 | return function(req, res) { 19 | setTimeout(function() { 20 | res.json({ 21 | success: true, 22 | content: { 23 | pn: 1, 24 | ps: 10, 25 | content: global[dataKey] 26 | } 27 | }); 28 | }, delay); 29 | } 30 | }, 31 | 32 | createDetailResponse: function(detailKey, childKey, childDataKey) { 33 | return function(req, res) { 34 | setTimeout(function() { 35 | var detail = global[detailKey][0]; 36 | if (childKey) { 37 | detail[childKey] = global[childDataKey] 38 | } 39 | res.json({ 40 | success: true, 41 | content: detail 42 | }); 43 | }, delay); 44 | }; 45 | }, 46 | 47 | createSaveResponse: function(dataKey) { 48 | return function(req, res) { 49 | const item = JSON.parse(req.body); 50 | console.log(item); 51 | 52 | if (item.id) { 53 | const originItem = global[dataKey].find((entity) => entity.id == item.id); 54 | Object.assign(originItem, item); 55 | } else { 56 | global[dataKey].push(item); 57 | item.id = 10000 * Math.random(); 58 | } 59 | 60 | setTimeout(function() { 61 | res.json({ 62 | success: true, 63 | content: [] 64 | }); 65 | }, delay); 66 | }; 67 | } 68 | }; 69 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo", 3 | "version": "1.0.0", 4 | "description": "front demo repo", 5 | "private": true, 6 | "entry": { 7 | "index": "./src/index.js" 8 | }, 9 | "scripts": { 10 | "start": "dora -p 8081 --plugins \"proxy?port=8082&watchDirs=./mock,webpack?disableNpmInstall,webpack-hmr\"", 11 | "build": "rimraf dist && atool-build --hash && curie html-assets-rename", 12 | "dev": "export NODE_ENV=dev || set NODE_ENV=dev&& rimraf dist/dev && atool-build -o dist/dev", 13 | "qa": "export NODE_ENV=qa || set NODE_ENV=qa&& rimraf dist/qa && atool-build -o dist/qa", 14 | "prod": "export NODE_ENV=production || set NODE_ENV=production&& atool-build -o dist/prod/20170606" 15 | }, 16 | "dependencies": { 17 | "antd": "^2.5.2", 18 | "dva": "^1.1.0", 19 | "react": "^15.3.2", 20 | "react-dom": "^15.3.2", 21 | "js-cookie": "^2.1.3", 22 | "qs": "^6.3.0", 23 | "ramda": "^0.23.0", 24 | "es6-promise": "^4.0.5", 25 | "babel-loader": "^6.2.9", 26 | "classnames": "^2.2.5", 27 | "less-loader": "^2.2.3", 28 | "path-to-regexp": "^1.7.0", 29 | "react-router": "^3.0.0", 30 | "react-router-redux": "^4.0.5", 31 | "whatwg-fetch": "^2.0.2" 32 | }, 33 | "devDependencies": { 34 | "atool-build": "^0.9.0", 35 | "atool-test-mocha": "^0.1.5", 36 | "babel-eslint": "^7.1.1", 37 | "babel-plugin-dev-expression": "^0.2.1", 38 | "babel-plugin-dva-hmr": "^0.2.0", 39 | "babel-plugin-import": "^1.1.0", 40 | "babel-plugin-transform-runtime": "^6.9.0", 41 | "babel-runtime": "^6.9.2", 42 | "dora": "^0.4.3", 43 | "dora-plugin-proxy": "^0.8.4", 44 | "dora-plugin-webpack": "^0.8.1", 45 | "dora-plugin-webpack-hmr": "^0.2.1", 46 | "eslint": "^2.9.0", 47 | "eslint-config-airbnb": "^9.0.1", 48 | "eslint-plugin-import": "^1.8.0", 49 | "eslint-plugin-jsx-a11y": "^1.2.0", 50 | "eslint-plugin-react": "^5.1.1", 51 | "expect": "^1.20.2", 52 | "mockjs": "^1.0.1-beta3", 53 | "redbox-react": "^1.3.2" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /proxy.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const mock = {}; 4 | 5 | require('fs').readdirSync(require('path').join(__dirname + '/mock')) 6 | .forEach(function (file) { 7 | Object.assign(mock, require('./mock/' + file)); 8 | }); 9 | 10 | module.exports = mock; 11 | -------------------------------------------------------------------------------- /src/carno/components/FilterBox/demo/Base.js: -------------------------------------------------------------------------------- 1 | import { Button, Input, Row, Col } from 'antd'; 2 | import { FilterBox } from 'carno'; 3 | 4 | function Base() { 5 | const btns = ( 6 |
7 | 8 | 9 |
10 | ); 11 | 12 | return ( 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | ); 21 | } 22 | 23 | export default Base; 24 | -------------------------------------------------------------------------------- /src/carno/components/FilterBox/doc.md: -------------------------------------------------------------------------------- 1 | ## FilterBox 过滤盒组件 2 | 3 | 用于包裹过滤的筛选条件 4 | 5 | ## 何时使用 6 | 7 | 一般在列表页使用 8 | 9 | ## 代码演示 10 | -------------------------------------------------------------------------------- /src/carno/components/FilterBox/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Button, Icon } from 'antd'; 3 | import classNames from 'classnames'; 4 | 5 | import styles from './index.less'; 6 | 7 | class FilterBox extends Component { 8 | constructor() { 9 | super(); 10 | 11 | this.state = { 12 | visible: true 13 | }; 14 | 15 | this.timer = ''; 16 | } 17 | 18 | handleFilterClick(visible) { 19 | this.setState({ 20 | visible: !visible 21 | }); 22 | } 23 | 24 | render() { 25 | const { visible } = this.state; 26 | 27 | const btnsCls = classNames({ 28 | [styles.filterBtns]: true, 29 | [styles.filterActive]: visible, 30 | }); 31 | 32 | const filterBtnCls = classNames({ 33 | [styles.active]: visible, 34 | }); 35 | 36 | const filterBoxCls = classNames({ 37 | [styles.filterBox]: true, 38 | [styles.active]: visible, 39 | }); 40 | 41 | return ( 42 |
43 |
44 | 45 | {this.props.overlay} 46 |
47 | {visible && 48 |
49 | {this.props.children} 50 |
51 | } 52 |
53 | ); 54 | } 55 | } 56 | 57 | export default FilterBox; 58 | -------------------------------------------------------------------------------- /src/carno/components/FilterBox/index.less: -------------------------------------------------------------------------------- 1 | .filterBtns { 2 | display: flex; 3 | margin-bottom: 16px; 4 | height: 28px; 5 | & > button { 6 | height: 28px; 7 | & + button { 8 | margin-left: 10px; 9 | } 10 | &.active { 11 | height: 33px; 12 | color: #2baee9; 13 | border-bottom-left-radius: 0; 14 | border-bottom-right-radius: 0; 15 | border-color: #2baee9; 16 | border-bottom: 1px solid #fff; 17 | background: #fff; 18 | i { 19 | vertical-align: 2px; 20 | transform: rotateZ(180deg); 21 | } 22 | } 23 | i { 24 | vertical-align: -1px; 25 | -webkit-transition: -webkit-transform 0.2s ease-out; 26 | } 27 | } 28 | &.filterActive { 29 | height: 16px; 30 | } 31 | } 32 | .filterBox { 33 | padding: 15px 15px 0 15px; 34 | border: 1px solid #ddd; 35 | &.active { 36 | border-color: #2baee9; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/carno/components/Form/Form.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Form } from 'antd'; 3 | import Utils from 'carno/utils'; 4 | 5 | const FormUtil = Utils.Form; 6 | const FormItem = Form.Item; 7 | 8 | export default ({ fields, item, form, layout = {}, ...others }) => { 9 | return ( 10 |
11 | {fields.map((field) => 12 | ( 13 | {FormUtil.createFieldDecorator(field, item, form.getFieldDecorator)} 14 | ) 15 | )} 16 |
17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /src/carno/components/Form/FormItem.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Form } from 'antd'; 3 | import Utils from 'carno/utils'; 4 | 5 | const FormUtil = Utils.Form; 6 | const FormItem = Form.Item; 7 | 8 | const FORM_ITEM_KEYS = ['label', 'labelCol', 'wrapperCol', 'help', 'extra', 'required', 'validateStatus', 'hasFeedback', 'colon']; 9 | const DECORATOR_KEYS = ['trigger', 'valuePropName', 'getValueFromEvent', 'validateTriggger', 'exclusive']; 10 | 11 | function pick(obj, keys) { 12 | return keys.map(k => k in obj ? { [k]: obj[k] } : {}) 13 | .reduce((res, o) => Object.assign(res, o), {}); 14 | } 15 | 16 | function extend(dest = {}, source = {}) { 17 | const result = Object.assign({}, dest); 18 | for (const key in source) { 19 | if (source.hasOwnProperty(key) && source[key] !== undefined) { 20 | result[key] = source[key]; 21 | } 22 | } 23 | return result; 24 | } 25 | 26 | /** 27 | * HFormItem 28 | * 对FormItem组件封装,统一FormItem与getFieldDecorator的属性,方便使用 29 | * @props form 表单对象 30 | * @props field 字段定义对象 31 | * @props item 默认值数据对象 32 | * @props rules 校验规则 33 | * @props onChange 控件改变事件 34 | * @props initialValue 控件初始值,会覆盖item中对应key的value 35 | * @props placeholder 如果为false则不显示placeholder 36 | * @props {...ForItemProps Form.Item 属性集} 包含所有Form.Item属性,参考Form.Item文档 37 | * @props {...DecoratorOptions 属性集} 包含所有DecoratorOptions属性,参考DecoratorOptions文档 38 | * 39 | */ 40 | export default function (props) { 41 | let formItemProps = pick(props, FORM_ITEM_KEYS); 42 | const decoratorOpts = pick(props, DECORATOR_KEYS); 43 | 44 | let { inputProps } = props; 45 | let { label, help, hasFeedback } = formItemProps; 46 | const { form, field, item, rules, initialValue, placeholder, onChange } = props; 47 | const { key, name } = field; 48 | 49 | label = label === undefined ? name : label; 50 | help = help === undefined ? field.help : help; 51 | 52 | if (field.hasFeedback === false || hasFeedback === false) { 53 | hasFeedback = false; 54 | } else { 55 | hasFeedback = true; 56 | } 57 | 58 | const dataItem = item || { [key]: initialValue }; 59 | const fieldItem = extend(field, { rules }); 60 | 61 | formItemProps = extend(formItemProps, { label, help, hasFeedback, key }); 62 | inputProps = extend(inputProps, { onChange }); 63 | 64 | return ( 65 | 66 | {FormUtil.createFieldDecorator(fieldItem, dataItem, form.getFieldDecorator, placeholder, inputProps, decoratorOpts)} 67 | 68 | ); 69 | } 70 | -------------------------------------------------------------------------------- /src/carno/components/Form/SearchForm.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Utils from 'carno/utils'; 3 | import { Form, Button, Row, Col } from 'antd'; 4 | import styles from './index.less'; 5 | 6 | const FormUtil = Utils.Form; 7 | const FormItem = Form.Item; 8 | 9 | /** 10 | * 查询表单 11 | * 12 | * @props fields 表单字段定义 13 | * @props search 查询字段初始值 14 | * @props form antd form 15 | * @props showLabel 是否显示输入框名称 16 | * @props showReset 是否显示重置按钮 17 | * @props formItemLayout 查询框布局定义 18 | * @props onSearch 查询回调函数 19 | * @props ...others 其他属性 20 | * 21 | */ 22 | const HSearchForm = ({ fields, search = {}, form, showLabel, showReset, formItemLayout = {}, onSearch, onReset, ...others }) => { 23 | formItemLayout = { 24 | itemCol: { 25 | span: 6 26 | }, 27 | labelCol: { 28 | span: showLabel ? 6 : 0 29 | }, 30 | wrapperCol: { 31 | span: showLabel ? 18 : 24 32 | }, 33 | btnCol: { 34 | span: 6 35 | }, 36 | ...formItemLayout, 37 | }; 38 | 39 | const handleSubmit = () => { 40 | FormUtil.validate(form, fields)(onSearch); 41 | }; 42 | 43 | const handleReset = () => { 44 | form.resetFields(); 45 | onReset && onReset(); 46 | }; 47 | 48 | const getLabelName = (field) => { 49 | return showLabel ? `${field.name}:` : ''; 50 | }; 51 | 52 | return ( 53 |
54 | 55 | {fields.map((field, index) => 56 | 57 | 58 | {FormUtil.createFieldDecorator(field, search, form.getFieldDecorator)} 59 | 60 | 61 | )} 62 | 63 | 64 | 65 | 66 | {showReset && 67 | 68 | 69 | 70 | } 71 | 72 | 73 |
74 | ); 75 | }; 76 | 77 | export default Form.create()(HSearchForm); 78 | -------------------------------------------------------------------------------- /src/carno/components/Form/demo/HFormBase.js: -------------------------------------------------------------------------------- 1 | import { Form, Button } from 'antd'; 2 | import { FormGen } from 'carno'; 3 | import Utils from 'utils'; 4 | 5 | const { getFields, validate } = Utils.Form; 6 | 7 | const fields = [ 8 | { 9 | key: 'name', 10 | name: '名称', 11 | required: true, 12 | }, { 13 | key: 'gender', 14 | name: '性别', 15 | enums: { 16 | MALE: '男', 17 | FAMALE: '女' 18 | } 19 | }, { 20 | key: 'birthday', 21 | name: '生日', 22 | type: 'date' 23 | }, { 24 | key: 'desc', 25 | name: '自我介绍', 26 | type: 'textarea' 27 | } 28 | ]; 29 | 30 | let results = {}; 31 | 32 | function HFormGenBase({ form }) { 33 | const formProps = { 34 | fields: getFields(fields).values(), 35 | item: {}, 36 | form 37 | }; 38 | const onSubmit = () => { 39 | validate(form, fields)((values) => { 40 | results = values; 41 | }); 42 | }; 43 | 44 | return ( 45 |
46 | 47 | 48 |
49 |
{JSON.stringify(results, null, 2)}
50 |
51 |
52 | ); 53 | } 54 | 55 | export default Form.create()(HFormGenBase); 56 | -------------------------------------------------------------------------------- /src/carno/components/Form/demo/HFormItemBase.js: -------------------------------------------------------------------------------- 1 | import { Form, Row, Col, Button } from 'antd'; 2 | import { HFormItem } from 'carno'; 3 | import Utils from 'utils'; 4 | 5 | const { validate, getFields } = Utils.Form; 6 | 7 | const fields = [ 8 | { 9 | key: 'name', 10 | name: '名称', 11 | required: true 12 | }, { 13 | key: 'gender', 14 | name: '性别', 15 | enums: { 16 | MALE: '男', 17 | FAMALE: '女' 18 | } 19 | }, { 20 | key: 'age', 21 | name: '年龄' 22 | }, { 23 | key: 'birthday', 24 | name: '生日', 25 | type: 'date' 26 | }, { 27 | key: 'desc', 28 | name: '自我介绍', 29 | type: 'textarea' 30 | } 31 | ]; 32 | 33 | 34 | function HFormItemBase({ form }) { 35 | 36 | const layout = { 37 | labelCol: { span: 6 }, 38 | wrapperCol: { span: 18 } 39 | }; 40 | const itemProps = { form, item: {}, ...layout }; 41 | const onChange = () => { 42 | console.log('changed'); 43 | }; 44 | const rules = { 45 | number: [{ pattern: /^\d+$/, message: '请输入数字' }] 46 | }; 47 | const submit = () => { 48 | validate(form, fields)((values) => { 49 | console.log(values); 50 | }); 51 | }; 52 | const fieldMap = getFields(fields).toMapValues(); 53 | 54 | return ( 55 |
56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 |
71 | ); 72 | } 73 | 74 | export default Form.create()(HFormItemBase); 75 | -------------------------------------------------------------------------------- /src/carno/components/Form/doc.md: -------------------------------------------------------------------------------- 1 | ## Form相关组件 2 | 3 | 4 | ### HForm 表单组件 5 | 6 | 表单组件, 是对`antd`中`form`的封装,支持通过字段配置直接生成表单, 需要配合[Utils.Form](#/components/Form)类使用, 7 | `HForm`内部的表单控件布局默认为`horizontal`. 8 | 9 | ### HFormItem 表单Item组件 10 | 11 | Item组件,是对`antd`中`FormItem`以及`getFieldDecorator`的封装,提供统一的接口,以简化使用. 12 | 提供单个控件的封装组件以方便实现表单的灵活布局. 13 | 14 | ## 代码演示 15 | 16 | ## DEMOS 17 | 18 | ## API 19 | 20 | ## HForm属性 21 | 22 | | 参数 | 说明 | 类型 | 默认值 | 23 | |-----------|------------------------------------------|------------|-------| 24 | | fields | 表单控件定义数组,详细属性请参考Field说明 | array | | 25 | | item | 默认数据对象 | object | - | 26 | | form | antd form对象 | object | - | 27 | | layout | 表单控件布局属性,参考antd formitem中的布局,示例: {labelCol: {span: 3, offset: 0}, wrapperCol: { span: 20 }} | object | - | 28 | | ...others | 传递给antd form的其它属性, 请参考ant.form属性 | - | - | 29 | 30 | 31 | ## HFormIten属性 32 | `antd`中`FormItem`的层级包括`FormItem/FieldDecorator/Input`, 使用起来太过繁琐, 我们提供统一的组件,将三层结构压缩为一级结构,将三层结构的属性,合并层一级结构的属性,并且将常用的属性提示到顶级属性上以方便使用. 33 | 34 | | 参数 | 说明 | 类型 | 默认值 | 35 | |-----------|------------------------------------------|------------|-------| 36 | | form | antd form实例对象 | object | | 37 | | field | 表单控件定义对象,参考下面的FIELD属性 | object | - | 38 | | item | 表单初试数据对象 | object | - | 39 | | rules | 表单校验规则,参考antd rules 规则, 此属性会覆盖field里面的rules配置 | array | 40 | | initialValue | 控件初始值,此属性会覆盖item中定义的初试数据 | any | - | 41 | | onChange | 表单控件改变事件 | function | - | 42 | | placeholder | placeholder如果为false则不显示,如果为string则显示对应的值,默认为label | boolean|string | - | 43 | | inputProps | 传入给控件的属性对象 | object | - | 44 | | ...FormItem | 包含全部的FormItem属性,参考antd FormItem属性定义 | - | - | 45 | | ...DecoratorOptions | 包含全部的DecoratorOptions属性,参考antd DecoratorOptions属性定义 | - | - | 46 | 47 | ## FIELD属性 48 | `fields`为`form/table`共用的数据结构对象,其属性说明如下: 49 | 50 | | 属性 | 说明 | 类型 | 默认值 | 51 | |-----------|------------------------------------------|------------|-------| 52 | | key | 字段key | string | | 53 | | name | 字段名称,在form中即为header名称,table中作为lable名称 | string | - | 54 | | type | 字段类型,目前支持如下类型:date,datetime,datetimeRange,enum,boolean,number,textarea,text | string | text | 55 | | meta | 字段额外配置属性,支持:min,max,rows | object | - | 56 | | enums | 字段枚举定义, 如果字段拥有此属性,则字段类型为enmu,示例: enums:{ ENABLED: '启用', DISABLED: '禁用'} | {} | - | 57 | | required | form专用属性,是否必填字段 | boolean | - | 58 | | render | table专用属性,自定义渲染 | boolean | - | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /src/carno/components/Form/index.less: -------------------------------------------------------------------------------- 1 | .formItem{ 2 | width: calc(~"100% - 10px"); 3 | margin-bottom: 15px!important; 4 | } 5 | -------------------------------------------------------------------------------- /src/carno/components/Modal/demo/ConfirmModalBase.js: -------------------------------------------------------------------------------- 1 | import {Button} from 'antd'; 2 | import {HModal} from 'carno'; 3 | 4 | function DetailModal(props) { 5 | 6 | return ( 7 | 8 | 这是一个测试的modal 9 | 10 | ) 11 | } 12 | 13 | export default class ModalBase extends React.Component { 14 | 15 | constructor(props) { 16 | super(props); 17 | this.state = { 18 | visible: false, 19 | confirmLoading: false 20 | }; 21 | } 22 | 23 | handleModal() { 24 | this.setState({visible: Symbol()}); 25 | } 26 | 27 | handleSave() { 28 | // 实际业务中,在modal中处理confirmLoading状态 29 | const self = this; 30 | this.setState({confirmLoading: true}); 31 | setTimeout(function () { 32 | self.setState({confirmLoading: false}); 33 | }, 3000); 34 | } 35 | 36 | render() { 37 | const {visible, confirmLoading} = this.state; 38 | const modalProps = { 39 | visible, 40 | confirmLoading, 41 | onOk: () => this.handleSave() 42 | } 43 | return ( 44 |
45 | 46 | 47 |
48 | ); 49 | 50 | } 51 | } -------------------------------------------------------------------------------- /src/carno/components/Modal/demo/ModalBase.js: -------------------------------------------------------------------------------- 1 | import { Button } from 'antd'; 2 | import { HModal } from 'carno'; 3 | 4 | function DetailModal(props) { 5 | 6 | return ( 7 | 8 | 这是一个测试的modal 9 | 10 | ) 11 | } 12 | 13 | export default class ModalBase extends React.Component { 14 | 15 | constructor(props) { 16 | super(props); 17 | this.state = { visible: false }; 18 | } 19 | 20 | handleModal(){ 21 | this.setState({ visible: Symbol() }); 22 | } 23 | 24 | render(){ 25 | const { visible } = this.state; 26 | const modalProps = { 27 | visible, 28 | onOk: () => console.log('close') 29 | } 30 | return ( 31 |
32 | 33 | 34 |
35 | ); 36 | 37 | } 38 | } -------------------------------------------------------------------------------- /src/carno/components/Modal/doc.md: -------------------------------------------------------------------------------- 1 | ## HModal 模态框组件 2 | 3 | 对`antd modal`的封装,简化对模态框的状态控制. 4 | 5 | 首先来看看目前项目中对`antd.modal`的使用方式: 6 | 7 | ```javascript 8 | 9 | export default class Test extend React.component { 10 | constructor(props){ 11 | super(props); 12 | this.state = { 13 | state: false 14 | } 15 | } 16 | 17 | handleModal() { 18 | this.state = { 19 | visible: true 20 | } 21 | } 22 | 23 | handleCancel() { 24 | this.sttae = { 25 | visible: false 26 | } 27 | } 28 | 29 | render() { 30 | const { visible } = this.state 31 | 32 | const modalProps = { 33 | visible, 34 | onCancel: this.handleCancel, 35 | onOk: this.props.onSave 36 | } 37 | 38 | return ( 39 |
40 | ... 41 | { visible && } 42 |
43 | ) 44 | } 45 | 46 | } 47 | 48 | ``` 49 | 50 | 在上述代码中,需要处理`visible`状态来控制`modal`的打开与关闭,特别是关闭状态,既需要在`onCancel`中将`visible`设置为`false`, 又需要在`onOk`中处理. 51 | 如果是将`visible`状态存入`model`中,又会导致`visible`状态控制更为的繁琐,而本质上`visible`仅仅只是一个中间组件的状态,对于实际的业务而已并没有什么意义. 52 | 53 | `HModal`的引入正是为了解决以上问题,在`antd.modal`属性的基础上,我们做了如下调整: 54 | 55 | - **visible** 56 | `visible`数据类型调整为原子数据(`Symbol`), 每次改变的时候,则会打开`modal` 57 | - **onCancel** 58 | `onCancel`属性不再是必须,当触发`cancel`时,组件内部自动处理`visble`状态 59 | - **confirmLoading** 60 | `confirmLoading`属性除了控制btn状态,还会关联`visible`属性,如果没有设置`confirmLoading`属性,点确定时会自动关闭`modal`, 如果设置了`confirmLoading`属性,则点击确定后,`confirm`从`true`变为`false`的时候,将关闭`modal` 61 | - **form** 62 | 与`form`配合使用,如果传递了`form`属性,则点击确定时候,只有在`form` `validate`成功后,才会触发`cancel`的处理,并且`onOk`属性会传递`form values`数据 63 | ## 代码演示 64 | 65 | ## DEMOS 66 | 67 | ## API 68 | 69 | | 参数 | 说明 | 类型 | 默认值 | 70 | |-----------|------------------------------------------|------------|-------| 71 | | visible | 原子类型,每次改变值的时候,会显示modal框 | Symbol | | 72 | | form | 如果配置了form属性,则只有form validate成功后,才会触发关闭modal的处理 | antd form对象 | | 73 | | ...others | 传递给antd modal的其它属性, 请参考ant.modal属性 | - | - | 74 | -------------------------------------------------------------------------------- /src/carno/components/Modal/index.js: -------------------------------------------------------------------------------- 1 | import { Modal } from 'antd'; 2 | import Utils from 'carno/utils'; 3 | const { validate } = Utils.Form; 4 | /** 5 | * 模态框组件 6 | * 7 | * @props visible Symbol类型参数,每次visible改变的时候,都会显示模态框 8 | * @props form 如果配置了form属性,则onOk属性会传递values,且只有在form validate success之后,才触发cancel逻辑 9 | * @props {...modalProps} 参考antd 模态框组件 10 | */ 11 | export default class HModal extends React.Component { 12 | constructor(props) { 13 | super(props); 14 | const { visible } = props; 15 | this.state = { 16 | visible: !!visible 17 | }; 18 | this.handleCancel = this.handleCancel.bind(this); 19 | this.handleOk = this.handleOk.bind(this); 20 | } 21 | 22 | componentWillReceiveProps({ visible, confirmLoading }) { 23 | // 如果props中的visible属性改变,则显示modal 24 | if (visible && (visible !== this.props.visible)) { 25 | this.setState({ 26 | visible: true 27 | }); 28 | } 29 | // 如果confirmLoading 从true转变为flase,则隐藏modal 30 | if (confirmLoading == false && this.props.confirmLoading) { 31 | this.setState({ 32 | visible: false 33 | }); 34 | } 35 | } 36 | 37 | handleCancel() { 38 | if (this.props.onCancel) { 39 | this.props.onCancel(); 40 | } 41 | 42 | this.setState({ 43 | visible: false 44 | }); 45 | } 46 | 47 | handleOk() { 48 | const { confirmLoading, form, onOk } = this.props; 49 | const hideModal = () => { 50 | // 如果没有传递confirmLoading,则直接关闭窗口 51 | if (confirmLoading == undefined) { 52 | this.handleCancel(); 53 | } 54 | }; 55 | 56 | if (onOk && form) { 57 | // 如果配置了form属性,则验证成功后才关闭表单 58 | validate(form)((values, originValues) => { 59 | onOk(values, originValues); 60 | hideModal(); 61 | }); 62 | } else { 63 | onOk && onOk(); 64 | hideModal(); 65 | } 66 | 67 | } 68 | 69 | render() { 70 | const modalProps = { ...this.props, visible: true, onOk: this.handleOk, onCancel: this.handleCancel }; 71 | return ( 72 |
{this.state.visible && {this.props.children}}
73 | ); 74 | } 75 | } -------------------------------------------------------------------------------- /src/carno/components/SearchBar/demo/Base.js: -------------------------------------------------------------------------------- 1 | import { Button } from 'antd'; 2 | import { SearchBar } from 'carno'; 3 | 4 | function Base() { 5 | const btns = ( 6 |
7 | 8 |
9 | ); 10 | 11 | const fields = [{ 12 | key: 'serviceName', 13 | name: '服务名称', 14 | }, { 15 | key: 'serviceVersion', 16 | name: '版本', 17 | }, { 18 | key: 'serviceImage', 19 | name: '镜像', 20 | }, { 21 | key: 'appName', 22 | name: '应用名称', 23 | }, { 24 | key: 'departmentName', 25 | name: '业务线', 26 | }, { 27 | key: 'deployType', 28 | name: '环境', 29 | }, { 30 | key: 'createdTime', 31 | name: '开始时间', 32 | type: 'datetime', 33 | }, { 34 | key: 'createdTime', 35 | name: '结束时间', 36 | type: 'datetime', 37 | }]; 38 | 39 | const onSearch = (values) => { 40 | console.log(values); 41 | }; 42 | 43 | const searchBarProps = { 44 | showLabel: true, 45 | showReset: true, 46 | btns, 47 | fields, 48 | onSearch, 49 | }; 50 | 51 | return ( 52 | 53 | ); 54 | } 55 | 56 | export default Base; 57 | -------------------------------------------------------------------------------- /src/carno/components/SearchBar/doc.md: -------------------------------------------------------------------------------- 1 | ## 搜索栏组件 2 | 3 | 对FilterBox与FormSearchGen组件的封装 4 | 5 | ## 何时使用 6 | 7 | 一般在列表页使用 8 | 9 | ## 代码演示 10 | 11 | ## DEMOS 12 | 13 | ## API 14 | 15 | ### SearchBar 16 | 17 | | 参数 | 说明 | 类型 | 默认值 | 18 | |---------------|--------------------------|-----------------|---------| 19 | | layout | 表单布局(antd@2.8 之后支持) | horizontal,vertical,inline |horizontal | 20 | | btns | 自定义按钮 | Element | - | 21 | | onSearch | 查询回调函数 | Function() | - | 22 | | fields | 查询字段配置,参考model中的fields格式 | Array | - | 23 | | search | 查询字段初始值 | Object | - | 24 | | formItemLayout | 查询框布局属性: { itemCol: { span: 6 }, labelCol: { span: 6 }, wrapperCol: { span: 6 }, btnCol: { span: 4 }} | Object | - | 25 | | showLabel | 是否显示label | Object | - | 26 | | showReset | 是否显示重置按钮 | Boolean | - | 27 | 28 | -------------------------------------------------------------------------------- /src/carno/components/SearchBar/index.js: -------------------------------------------------------------------------------- 1 | import FilterBox from '../FilterBox'; 2 | import HSearchForm from '../Form/SearchForm'; 3 | import styles from './index.less'; 4 | /** 5 | * 搜索栏组件 6 | * @props btns 自定义按钮 7 | * @props ...formProps 参考HSearchForm组件属性 8 | */ 9 | 10 | //type: box, bar 11 | //trigger: change, submit 12 | function SearchBar(props) { 13 | const { btns, ...formProps } = props; 14 | return ( 15 | 16 | 17 | 18 | ); 19 | } 20 | 21 | export default SearchBar; 22 | 23 | -------------------------------------------------------------------------------- /src/carno/components/SearchBar/index.less: -------------------------------------------------------------------------------- 1 | .searchForm{ 2 | margin-right: 15px; 3 | } 4 | -------------------------------------------------------------------------------- /src/carno/index.js: -------------------------------------------------------------------------------- 1 | import './styles/themes.less'; 2 | 3 | export { default as HForm } from './components/Form/Form'; 4 | export { default as HSearchForm } from './components/Form/SearchForm'; 5 | export { default as HFormItem } from './components/Form/FormItem'; 6 | export { default as HModal } from './components/Modal'; 7 | 8 | export { default as FilterBox } from './components/FilterBox'; 9 | export { default as SearchBar } from './components/SearchBar'; 10 | 11 | export { default as Utils } from './utils'; 12 | 13 | export { default as Http } from './utils/http'; 14 | export { default as Model } from './utils/model'; 15 | 16 | -------------------------------------------------------------------------------- /src/carno/styles/mixins.less: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laketea/admin-demo/d57ab4769791fe60adf90ca1b7bfbef355e51ad3/src/carno/styles/mixins.less -------------------------------------------------------------------------------- /src/carno/styles/themes.less: -------------------------------------------------------------------------------- 1 | @import "./variables"; 2 | 3 | @aside-bg: #ececec; 4 | @sider-bg: #2f3e53; 5 | @menu-bg: #2f3e53; 6 | @menu-dark-bg: #253042; 7 | @menu-light-bg: #2bb1ee; 8 | @menu-link-color: #748398; 9 | @header-bg: #fff; 10 | @border-color: #e9e9e9; 11 | @content-bg: #fff; 12 | @logo-color: #2bb1ee; 13 | 14 | :global { 15 | .ant-theme-blue { 16 | &.ant-layout-aside { 17 | background-color: @aside-bg; 18 | } 19 | .ant-layout-sider { 20 | background: @sider-bg; 21 | .ant-layout-logo { 22 | color: @logo-color; 23 | } 24 | } 25 | .ant-layout-main { 26 | .ant-layout-header { 27 | background-color: @header-bg; 28 | border: 1px solid @border-color; 29 | } 30 | .ant-layout-content { 31 | background-color: @content-bg; 32 | } 33 | } 34 | } 35 | .ant-menu-blue.ant-menu { 36 | background-color: @menu-bg; 37 | color: @menu-link-color; 38 | border-right: 0; 39 | .ant-menu-sub { 40 | color: @menu-link-color; 41 | background: @menu-bg; 42 | } 43 | .ant-menu-inline.ant-menu-sub { 44 | background: @menu-dark-bg; 45 | } 46 | .ant-menu-item { 47 | color: @menu-link-color; 48 | } 49 | .ant-menu-item>a { 50 | color: @menu-link-color; 51 | } 52 | .ant-menu-item-selected { 53 | border-right: 0; 54 | color: @white; 55 | } 56 | .ant-menu-item-selected>a { 57 | color: @white; 58 | &:hover { 59 | color: @white; 60 | } 61 | } 62 | .ant-menu-horizontal { 63 | border-bottom-color: @menu-bg; 64 | } 65 | .ant-menu-horizontal>.ant-menu-item { 66 | border-bottom: 0; 67 | border-color: @menu-bg; 68 | top: 0; 69 | } 70 | .ant-menu-horizontal>.ant-menu-submenu { 71 | border-bottom: 0; 72 | border-color: @menu-bg; 73 | top: 0; 74 | } 75 | .ant-menu-inline { 76 | border-right: 0; 77 | .ant-menu-item { 78 | border-right: 0; 79 | left: 0; 80 | margin-left: 0; 81 | } 82 | .ant-menu-item-selected { 83 | background-color: @menu-light-bg !important; 84 | } 85 | } 86 | .ant-menu-vertical { 87 | border-right: 0; 88 | .ant-menu-item { 89 | border-right: 0; 90 | left: 0; 91 | margin-left: 0; 92 | } 93 | .ant-menu-item-selected { 94 | background-color: @menu-light-bg !important; 95 | } 96 | } 97 | .ant-menu-item:hover, 98 | .ant-menu-item-active, 99 | .ant-menu-submenu-active, 100 | .ant-menu-submenu-selected, 101 | .ant-menu-submenu:hover, 102 | .ant-menu-submenu-title:hover { 103 | background-color: transparent; 104 | color: @white; 105 | } 106 | .ant-menu-item:hover>a, 107 | .ant-menu-item-active>a, 108 | .ant-menu-submenu-active>a, 109 | .ant-menu-submenu-selected>a, 110 | .ant-menu-submenu:hover>a, 111 | .ant-menu-submenu-title:hover>a { 112 | color: @white; 113 | } 114 | .ant-menu-vertical>.ant-menu-item, 115 | .ant-menu-inline>.ant-menu-item, 116 | .ant-menu-item-group-list>.ant-menu-item, 117 | .ant-menu-vertical>.ant-menu-submenu>.ant-menu-submenu-title, 118 | .ant-menu-inline>.ant-menu-submenu>.ant-menu-submenu-title, 119 | .ant-menu-item-group-list>.ant-menu-submenu>.ant-menu-submenu-title { 120 | padding: 0px 16px 0 24px; 121 | } 122 | } 123 | .fold .ant-menu-blue >.ant-menu-submenu-selected { 124 | color: @white; 125 | background-color: @menu-light-bg; 126 | } 127 | .ant-menu-blue.ant-menu:not(.ant-menu-horizontal) { 128 | .ant-menu-item-selected { 129 | background-color: @menu-light-bg; 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/carno/styles/variables.less: -------------------------------------------------------------------------------- 1 | //colors 2 | @white: #ffffff; 3 | @primary-color: #2db7f5; 4 | @transition-ease-in : all .3s cubic-bezier(0.55, 0.055, 0.675, 0.19); 5 | @transition-ease-out : all .3s cubic-bezier(0.55, 0.055, 0.675, 0.19); 6 | -------------------------------------------------------------------------------- /src/carno/utils/form/doc.md: -------------------------------------------------------------------------------- 1 | # Form工具类 2 | 3 | 后台系统业务大部分都是表格+表单的形式,故我们在`model`层,统一定义模型的数据结构,以方便在`table+form`中复用,简化实际的开发工作. 4 | 这里主要介绍下`Form`工具类的使用. 5 | 6 | ### 使用场景 7 | field提供统一的数据格式,以方便在form以及table中复用,参考如下: 8 | 9 | ``` javascript 10 | 11 | const fields = [ 12 | { 13 | key: 'name', // 字段key 14 | name: '名称' // 字段name 15 | type: 'text' // 字段类型支持如下类型: date|datetime|datetimeRange|enum|boolean|number|textarea|text 16 | meta: { 17 | min: 0, 18 | max: 100, 19 | rows: 12 20 | }, 21 | required: true 22 | } 23 | ] 24 | 25 | ``` 26 | 27 | Form类的主要作用是将以上通用的`field`格式,转换为`createFieldDecorator`内部函数支持的格式 28 | 29 | ### 如何使用 30 | form工具类通过Utils类中引入. 31 | 32 | > import { Utils } from 'carno'; 33 | > const { getFields } = Utils; 34 | 35 | form工具类主要提供以下接口: 36 | 37 | - getFields 转换field为form field格式 38 | - combineTypes 扩展支持的字段类型 39 | - validate form数据验证扩展 40 | - getDateValue 转换datetime数据格式 41 | - createFieldDecorator 创建新的fieldDecorator 42 | 43 | ##### getFields 44 | 核心方法,转换通用字段类型为表单field格式, getFields返回的数据需要配合[FormGen](#/components/FormGen)组件使用. 45 | 46 | 参数: 47 | 48 | - originFields 通用的fields定义,一般由model中定义 49 | - fieldKeys 需要pick的keys, 通用的fields往往是个字段的超级,在form中一般只需要显示部分字段 50 | - extraFields 扩展的字段定义, 可以对通用字段的属性扩展 51 | 52 | getFields返回的是一个链式对象,需要调用`values`方法才能返回最终的结果。 53 | 链式对象支持如下方法: 54 | 55 | - pick 参数与fieldKeys格式一致 56 | - excludes 排除部分字段 57 | - enhance 参数与extraFields格式一致 58 | - values 返回数据结果 59 | 60 | ```javascript 61 | 62 | import { Form, Button } from 'antd'; 63 | import { Utils, FormGen } from 'carno'; 64 | 65 | const { getFields, validate } = Utils.Form; 66 | 67 | const fields = [ 68 | { 69 | key: 'name', 70 | name: '名称', 71 | required: true 72 | }, { 73 | key: 'gender', 74 | name: '性别', 75 | enums: { 76 | MALE: '男', 77 | FAMALE: '女' 78 | } 79 | }, { 80 | key: 'birthday', 81 | name: '生日', 82 | type: 'date' 83 | }, { 84 | key: 'desc', 85 | name: '自我介绍', 86 | type: 'textarea' 87 | } 88 | ]; 89 | 90 | let results = {}; 91 | 92 | function FormGenBase({ form }) { 93 | const formProps = { 94 | fields: getFields(fields).values(), 95 | item: {}, 96 | form 97 | }; 98 | 99 | return ( 100 |
101 | 102 |
103 | ); 104 | } 105 | 106 | export default Form.create()(FormGenBase); 107 | 108 | ``` 109 | 110 | ##### combineTypes 111 | 112 | 扩展通用字段定义支持的表单类型, 自定义字段类型写法参考如下: 113 | 114 | ```javascript 115 | combineTypes({ 116 | //参数:初始值,meta(字段meta数据,例如: rows,min,max等), field字段定义对象 117 | text: (initialValue, meta, field, showPlaceholder) => { 118 | const placeholder = meta.placeholder || (showPlaceholder ? `${field.name}` : ''); 119 | //返回值为一个对象,需要返回 120 | // input: 表单控件 121 | // initialValue: 初试值 122 | return { 123 | input: , 124 | initialValue 125 | }; 126 | } 127 | }) 128 | 129 | ``` 130 | 131 | ##### validate 132 | 133 | validate对象是对antd validate方法的包装,主要作用是统一处理form中的datatime类型的数据,将moment数据结构转换为number类型. 134 | 135 | 参数: 136 | 137 | - form antd表单对象 138 | 139 | ``` 140 | const onSuccess = (values) => { 141 | // values 中的datatime类型已自动转换为number 142 | } 143 | const onError = () => { 144 | 145 | } 146 | validate(form)(onSuccess, onError) 147 | 148 | ``` 149 | 150 | ##### getDateValue 151 | 152 | 提供一个工具函数,方便转换moment类型为number 153 | 154 | ```javascript 155 | 156 | getDateValue(item.updateTime); 157 | 158 | ``` 159 | 160 | ##### createFieldDecorator 161 | 162 | 创建antd的fieldDecorator对象,目前此函数主要是提供给`FormGen`组件使用 163 | 164 | 参数如下: 165 | 166 | - field 字段定义 167 | - item 初始化数据对象 168 | - getFieldDecorator antd form对象中的getFieldDecorator 169 | - showPlaceholder 是否显示Placeholder 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | -------------------------------------------------------------------------------- /src/carno/utils/form/fieldTypes.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import moment from 'moment'; 3 | import { DatePicker, Select, Input, Checkbox, InputNumber } from 'antd'; 4 | 5 | const Option = Select.Option; 6 | const RangePicker = DatePicker.RangePicker; 7 | 8 | /* 9 | * 表单字段类型 10 | */ 11 | const fieldTypes = { 12 | date: ({ initialValue, inputProps }) => { 13 | return { 14 | input: , 15 | initialValue: initialValue ? moment(parseInt(initialValue, 10)) : null 16 | }; 17 | }, 18 | datetime: ({ initialValue, inputProps }) => { 19 | return { 20 | input: , 21 | initialValue: initialValue ? moment(parseInt(initialValue, 10)) : null 22 | }; 23 | }, 24 | datetimeRange: ({ inputProps }) => { 25 | return ; 26 | }, 27 | enum: ({ field, placeholder, inputProps }) => { 28 | const enumsArray = Object.keys(field.enums).reduce((occ, key) => { 29 | occ.push({ 30 | key, 31 | value: field.enums[key] 32 | }); 33 | return occ; 34 | }, []); 35 | placeholder = placeholder == false ? '' : (placeholder || `请选择${field.name}`); 36 | return ( 37 | 44 | ); 45 | }, 46 | boolean: ({ inputProps }) => { 47 | return ; 48 | }, 49 | number: ({ meta = {}, inputProps }) => { 50 | return ; 51 | }, 52 | textarea: ({ meta = {}, field, placeholder, inputProps }) => { 53 | placeholder = placeholder == false ? '' : (placeholder || meta.placeholder || `请输入${field.name}`); 54 | return ; 55 | }, 56 | text: ({ meta = {}, field, placeholder, inputProps }) => { 57 | placeholder = placeholder == false ? '' : (placeholder || meta.placeholder || `请输入${field.name}`); 58 | return ; 59 | } 60 | }; 61 | 62 | /* 63 | * 扩展表单字段类型 64 | */ 65 | export function combineTypes(extras) { 66 | Object.assign(fieldTypes, extras); 67 | } 68 | 69 | export default fieldTypes; 70 | -------------------------------------------------------------------------------- /src/carno/utils/form/index.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | import { default as fieldTypes, combineTypes } from './fieldTypes'; 3 | 4 | /* 5 | * 获取date数据的时间戳 6 | */ 7 | const getDateValue = (value, defaultValue = undefined) => { 8 | return value ? value.valueOf() : defaultValue; 9 | }; 10 | 11 | /* 12 | * 获取表单field数组 13 | * 示例: 14 | * const formFields = getFields(fields,['name','author'],{ name: { rules: []}}).values(); 15 | * const formFields = getFields(fields).excludes(['id','desc']).values(); 16 | * const formFields = getFields(fields).pick(['name','author','openTime']).enhance({name:{ rules: [] }}).values(); 17 | * @param originField 原始fields 18 | * @param fieldKeys 需要包含的字段keys 19 | * @param extraFields 扩展的fields 20 | * @result 链式写法,返回链式对象(包含pick,excludes,enhance,values方法), 需要调用values返回最终的数据 21 | */ 22 | const getFields = (originFields, fieldKeys, extraFields) => { 23 | const chain = {}; 24 | let fields = [...originFields]; 25 | 26 | const pick = (keys) => { 27 | keys = [].concat(keys); 28 | fields = keys.map(key => { 29 | let field = fields.find(item => key == item.key); 30 | if (!field) { 31 | // 如果field不存在,则默认类型的field 32 | field = { 33 | key, 34 | name: key 35 | }; 36 | } 37 | return field; 38 | }); 39 | return chain; 40 | }; 41 | 42 | const excludes = (keys) => { 43 | keys = [].concat(keys); 44 | fields = fields.filter(field => !keys.includes(field.key)); 45 | return chain; 46 | }; 47 | 48 | const enhance = (_extraFields) => { 49 | if (!Array.isArray(_extraFields)) { 50 | _extraFields = Object.keys(_extraFields).map(key => { 51 | return Object.assign(_extraFields[key], { 52 | key 53 | }); 54 | }); 55 | } 56 | _extraFields.forEach(extraField => { 57 | const field = fields.find(item => item.key == extraField.key); 58 | if (field) { 59 | Object.assign(field, extraField); 60 | } else { 61 | fields.push(extraField); 62 | } 63 | }); 64 | return chain; 65 | }; 66 | 67 | const values = () => { 68 | return fields; 69 | }; 70 | 71 | const toMapValues = () => { 72 | return fields.reduce((map, field) => { 73 | map[field.key] = field; 74 | return map; 75 | }, {}); 76 | }; 77 | 78 | const mixins = (keys) => { 79 | keys = [].concat(keys); 80 | fields = keys.map(key => { 81 | let field; 82 | if (typeof key == 'string') { 83 | field = fields.find(item => key == item.key) || { key }; 84 | } else { 85 | field = key; 86 | } 87 | return field; 88 | }); 89 | return chain; 90 | }; 91 | 92 | if (fieldKeys) { 93 | mixins(fieldKeys); 94 | } 95 | 96 | if (extraFields) { 97 | enhance(extraFields); 98 | } 99 | 100 | return Object.assign(chain, { 101 | pick, 102 | excludes, 103 | enhance, 104 | values, 105 | toMapValues 106 | }); 107 | }; 108 | 109 | /* 110 | * 创建antd fieldDecorator 111 | */ 112 | const createFieldDecorator = (field, item, getFieldDecorator, placeholder, inputProps = {}, decoratorOpts = {}) => { 113 | let { type, rules } = field; 114 | const { key, enums, meta, required, render } = field; 115 | type = (fieldTypes.hasOwnProperty(type) && type) || (enums && 'enum') || 'text'; 116 | 117 | const typedItem = (render || fieldTypes[type])({ initialValue: item[key], meta, field, inputProps, placeholder }); 118 | let { input, initialValue } = typedItem; 119 | 120 | if (React.isValidElement(typedItem)) { 121 | input = typedItem; 122 | initialValue = item[key]; 123 | } 124 | 125 | if (required && !rules) { 126 | rules = [{ 127 | required: true, 128 | message: `请输入${field.name}` 129 | }]; 130 | } 131 | 132 | return getFieldDecorator(key, { initialValue, rules, inputProps, ...decoratorOpts })(input); 133 | }; 134 | 135 | /* 136 | * 包装antd form validateFields 137 | * 主要用途自动转换date类型数据,validateFields提供的错误处理大部分情况下都用不到,故提供一个包装函数,简化使用 138 | * 示例: 139 | * validate(form, fields)((values) => { 140 | * onSave({ 141 | * ...values, 142 | * }); 143 | * }); 144 | * @param form, antd form对象 145 | * @param 返回result函数,参数为: onSuccess, onError 146 | */ 147 | const validate = (form) => { 148 | const { validateFields, getFieldsValue } = form; 149 | 150 | const transformValues = (values) => { 151 | const newValues = {}; 152 | Object.keys(values).forEach((key) => { 153 | const value = values[key]; 154 | const isDateTimeType = value && value instanceof moment; 155 | const newValue = isDateTimeType ? getDateValue(values[key]) : values[key]; 156 | // 如果value为undefined,则不赋值到values对象上 157 | if (newValue != undefined) { 158 | newValues[key] = newValue; 159 | } 160 | }); 161 | return newValues; 162 | }; 163 | 164 | return (onSuccess, onError) => { 165 | validateFields((errors) => { 166 | if (errors) { 167 | onError && onError(errors); 168 | } else { 169 | const originValues = { ...getFieldsValue() }; 170 | onSuccess(transformValues(originValues), originValues); 171 | } 172 | }); 173 | }; 174 | }; 175 | 176 | export default { combineTypes, getFields, validate, getDateValue, createFieldDecorator }; 177 | 178 | -------------------------------------------------------------------------------- /src/carno/utils/http/doc.md: -------------------------------------------------------------------------------- 1 | ## HTTP 请求类 2 | 3 | 由于不同的后端服务交互的数据格式不一致,故提供通用的http类,以屏蔽底层细节,方便复用,并且实现了中间件的机制以方便扩展。 4 | 5 | ### 如何创建http实例 6 | `Http`是实现的一个通用请求`class`, 实际使用的时候,需要创建一个实例化的对象, 可通过以下两种方式创建新的实例 7 | 8 | - Http.create(config,middlewares) 9 | - new Http(config,middlewares) 10 | 11 | 通过`Http`类静态方法`create`创建的实例,默认会添加`defaults.middlewares`中的中间件. 12 | 也可以直接实例化`Http`对象,这种方式创建的实例,不会继续默认的`middlewares`. 13 | 14 | ```javascript 15 | /* 16 | * http默认配置 17 | */ 18 | const defaults = { 19 | middlewares: [ 20 | middlewares.status(), 21 | middlewares.json(), 22 | middlewares.dataStatus(), 23 | middlewares.onerror(), 24 | middlewares.authFailed() 25 | ], 26 | config: {} 27 | }; 28 | ``` 29 | 30 | `http`实例提供了get,post,del,put等方法,默认支持`json`&`formdata`数据格式 31 | 通过`http`实例上的`create`方法, 可以创建新的实例,且新的实例会自动继承父`http`的`config`以及`middlewares` 32 | 33 | ```javascript 34 | //http.js 35 | import { Http } from 'carno'; 36 | const {} = Http'; 37 | 38 | //建议在应用系统先创建一个基础的http类,其他的http在通过create方法来扩展 39 | //create方法支持4种参数场景: 40 | //create('domain') 41 | //create(config) 42 | //create(middlewares) 43 | //create(config,mddlewares) 44 | 45 | const { domain, queryToken, dataTransform } = Http.middlewares; 46 | 47 | const middlewares = [ 48 | domain(getServer()), 49 | dataTransform((data, options) => { 50 | if (options.dataType == 'form') { 51 | const formData = new FormData(); 52 | Object.keys(data).forEach(key => { 53 | formData.append(key, data[key]); 54 | }); 55 | data = formData; 56 | } else { 57 | options.headers = Object.assign(options.headers || {}, { 58 | 'Content-type': 'application/x-www-form-urlencoded; charset=UTF-8' 59 | }); 60 | data = qs.stringify(data); 61 | } 62 | return { 63 | data, 64 | options 65 | }; 66 | }) 67 | ]; 68 | 69 | export default Http.create(middlewares); 70 | 71 | const http = Http.create() 72 | 73 | // server.js 74 | import http from 'utils/http'; 75 | const {get, post} = http.create('abc'); 76 | ``` 77 | 78 | ### 中间件 79 | 中间件主要在发送请求以及响应之后,做一些数据以及参数处理,http中提供了一些常用的中间件,部分中间件支参数配置, 使用时,可以根据自己的业务特点灵活配置. 80 | 81 | 如果需要添加自定义中间件,则参考如下代码: 82 | 83 | const middle = { 84 | //请求前 85 | request: (_request_) => { 86 | //do something 87 | return _request; 88 | }, 89 | //请求错误 90 | requestError: (_request) => { 91 | //do something 92 | return _request; 93 | }, 94 | //数据响应后 95 | response: (_reponse, _request) => { 96 | //do something 97 | return _reponse; 98 | }, 99 | //数据响应失败后 100 | responseError: (_reponse, _request) => { 101 | //do something 102 | return _reponse; 103 | } 104 | } 105 | 106 | 下面再详细说明下内置的一些常用的中间件: 107 | 108 | #### domain 109 | 服务域中间件, 自动将url加上host路径, 使用时需传入hosts 110 | 111 | const domainMiddles = middlewares.domain({ 112 | api: '//api.xx.cn', 113 | ax: '//ax.xx.cn' 114 | }); 115 | 116 | ### dataTransform 117 | 响应数据转换中间件, 使用时需传入转换函数 118 | 119 | // dataType, 在发送请求的时候传入 120 | // data: 参数数据 121 | // options: 请求配置项 122 | const dataTransform = middlewares.dataTransform((data, options) => { 123 | if (options.dataType == 'form') { 124 | const formData = new FormData(); 125 | Object.keys(data).forEach(key => { 126 | formData.append(key, data[key]); 127 | }); 128 | data = formData; 129 | } else { 130 | options.headers = Object.assign(options.headers || {}, { 131 | 'Content-type': 'application/x-www-form-urlencoded; charset=UTF-8' 132 | }); 133 | data = qs.stringify(data); 134 | } 135 | return { 136 | data, 137 | options 138 | }; 139 | }) 140 | 141 | // 请求时配置dataType为form 142 | export function upload(data) { 143 | return post('/server/upload', data, { 144 | dataType: 'form' 145 | }); 146 | } 147 | 148 | ### queryToken 149 | token中间件,参数附加在url中, 使用时,需传入token对象 150 | 151 | const queryTokenMiddle = middlewares.queryToken(() => { 152 | return { 153 | sid: 'xxx', 154 | st: 'xxx' 155 | } 156 | }); 157 | 158 | 159 | ### headerToken 160 | token中间件,参数存放在header中,使用方式与queryToken类似 161 | 162 | const headerTokenMiddle = middlewares.headeroken(() => { 163 | return { 164 | authorization: 'xxx', 165 | } 166 | }); 167 | 168 | ### onerror 169 | 错误处理中间件, 拦截res&req错误,并弹出modal框显示错误详情,如单个请求需屏蔽错误弹出框,则在请求的config中设置参数`ignoreErrorModal`为`true` 170 | 171 | ### status 172 | 响应状态处理中间件 173 | 174 | ### json 175 | 响应数据json处理 176 | 177 | ### content 178 | 响应数据content处理中间件, 后端数据一般会将真实数据设置一个统一key值(例如:content,data等),使用中间件的时候,可以传入contentKey 179 | 180 | const headerTokenMiddle = middlewares.content('data'); 181 | 182 | ### authFailed 183 | 授权失败中间件, 授权失败后,默认会跳转到登陆页面, 支持配置授权失败的code以及登陆hash 184 | 185 | const authFailedMiddle = middlewares.authFailed({ 186 | codes: ['401', '404'], 187 | hash: '/login' 188 | }); 189 | 190 | 191 | 192 | -------------------------------------------------------------------------------- /src/carno/utils/http/index.js: -------------------------------------------------------------------------------- 1 | import qs from 'qs'; 2 | import { Promise } from 'es6-promise'; 3 | import 'whatwg-fetch'; 4 | import middlewares from './middlewares'; 5 | 6 | const baseFetch = Symbol(); 7 | 8 | /* 9 | * http默认配置 10 | */ 11 | const defaults = { 12 | middlewares: [ 13 | middlewares.status(), 14 | middlewares.json(), 15 | middlewares.dataStatus(), 16 | middlewares.onerror(), 17 | middlewares.authFailed() 18 | ], 19 | config: { 20 | } 21 | }; 22 | 23 | /* 24 | * 通用Http类,提供中间件来扩展 25 | * 方法: create, get, post, put, del, patch, head 26 | */ 27 | class Http { 28 | 29 | constructor(_config, _middlewares) { 30 | if (_config instanceof Array) { 31 | _middlewares = _config; 32 | } 33 | this.config = _config || {}; 34 | this.middlewares = _middlewares || []; 35 | const bindKeys = ['fetch', 'get', 'post', 'del', 'patch', 'put', 'head', 'getRequestInit', baseFetch]; 36 | bindKeys.forEach(key => { 37 | this[key] = this[key].bind(this); 38 | }); 39 | } 40 | 41 | /* 42 | * 创建新的http实例,新的实例会继承当前实例的config & middlewares; 43 | */ 44 | create(_config = {}, _middlewares = []) { 45 | if (typeof _config == 'string') { 46 | _config = { domain: _config }; 47 | } 48 | if (_config instanceof Array) { 49 | _middlewares = _config; 50 | _config = {}; 51 | } 52 | 53 | _config = { ...this.config, ..._config }; 54 | _middlewares = [...this.middlewares, ..._middlewares]; 55 | 56 | return new Http(_config, _middlewares); 57 | } 58 | 59 | getRequestInit(args) { 60 | const requestInit = {}; 61 | const requestInitKeys = ['method', 'headers', 'body', 'referrer', 'mode', 'credentials', 'cache', 'redirect']; 62 | Object.keys(args).forEach(key => { 63 | requestInitKeys.includes(key) && (requestInit[key] = args[key]); 64 | }); 65 | return requestInit; 66 | } 67 | 68 | [baseFetch]({ url, options, method, data }) { 69 | const defaultMethod = !data && (!!options && !options.body) ? 'GET' : 'POST'; 70 | options.method = method || options.method || defaultMethod; 71 | 72 | if (!!data) { 73 | if (data instanceof window.Blob || data instanceof window.FormData || 74 | typeof data === 'string') { 75 | options.body = data; 76 | } else { 77 | options.body = window.JSON.stringify(data); 78 | } 79 | } 80 | 81 | return fetch(url, this.getRequestInit(options)); 82 | } 83 | 84 | request(url, options, method, data) { 85 | options = Object.assign({}, this.config, options); 86 | const request = { url, options, method, data }; 87 | 88 | let promise = Promise.resolve(request); 89 | const chain = [this[baseFetch].bind(this), undefined]; 90 | 91 | const wrapResponse = (fn, req, reject) => { 92 | // return fn; 93 | return (res) => { 94 | return fn ? fn(res, req) : (reject ? Promise.reject(res) : res); 95 | }; 96 | }; 97 | 98 | for (const middleware of this.middlewares) { 99 | chain.unshift(middleware.request, middleware.requestError); 100 | chain.push(wrapResponse(middleware.response, request), wrapResponse(middleware.responseError, request, true)); 101 | } 102 | 103 | while (!!chain.length) { 104 | promise = promise.then(chain.shift(), chain.shift()); 105 | } 106 | 107 | return promise; 108 | } 109 | 110 | addMiddlewares(_middlewares, overwirte) { 111 | overwirte && this.clearMiddlewares(); 112 | this.middlewares.push(..._middlewares); 113 | return this; 114 | } 115 | 116 | clearMiddlewares() { 117 | while (this.middlewares.length) { 118 | this.middlewares.pop(); 119 | } 120 | return this; 121 | } 122 | 123 | fetch(url, options) { 124 | return this.request(url, options); 125 | } 126 | 127 | get(url, data, options) { 128 | if (data) { 129 | url = `${url}${url.includes('?') ? '&' : '?'}${qs.stringify(data)}`; 130 | } 131 | return this.request(url, options, 'GET'); 132 | } 133 | 134 | post(url, data, options) { 135 | return this.request(url, options, 'POST', data); 136 | } 137 | 138 | patch(url, data, options) { 139 | return this.request(url, options, 'PATCH', data); 140 | } 141 | 142 | put(url, data, options) { 143 | return this.request(url, options, 'PUT', data); 144 | } 145 | 146 | del(url, options) { 147 | return this.request(url, options, 'DELETE'); 148 | } 149 | 150 | head(url, options) { 151 | return this.request(url, options, 'HEAD'); 152 | } 153 | } 154 | 155 | Http.middlewares = middlewares; 156 | Http.defaults = defaults; 157 | 158 | /* 159 | * 创建http对象,继承默认配置 160 | */ 161 | Http.create = (_config, _middlewares) => { 162 | return new Http(defaults.config, defaults.middlewares).create(_config, _middlewares); 163 | }; 164 | 165 | export default Http; 166 | -------------------------------------------------------------------------------- /src/carno/utils/http/middlewares.js: -------------------------------------------------------------------------------- 1 | import qs from 'qs'; 2 | import { Modal } from 'antd'; 3 | import { Promise } from 'es6-promise'; 4 | 5 | const getParamValue = (paramOrFn) => { 6 | return typeof paramOrFn == 'function' ? paramOrFn() : paramOrFn; 7 | }; 8 | 9 | const transform = (defaults) => { 10 | return { 11 | request(_request) { 12 | const requestTransforms = _request.config.requestTransform || defaults.requestTransform || []; 13 | if (requestTransforms.length) { 14 | _request.options.pass = true; 15 | requestTransforms.forEach(requestTransform => { 16 | _request = requestTransform(_request); 17 | }); 18 | } 19 | return _request; 20 | }, 21 | response(_reponse, _request) { 22 | const responseTransforms = _reponse.config.responseTransform || defaults.responseTransform || []; 23 | responseTransforms.forEach(responseTransform => { 24 | _reponse = responseTransform(_reponse); 25 | }); 26 | _request.options.pass = true; 27 | return _reponse; 28 | } 29 | }; 30 | }; 31 | 32 | // 参数数据转化中间件 33 | const dataTransform = (_transform) => { 34 | return { 35 | request(_request) { 36 | let transRequest; 37 | if (_transform) { 38 | transRequest = _transform(_request.data, _request.options, _request); 39 | } 40 | return Object.assign(_request, transRequest); 41 | } 42 | }; 43 | }; 44 | 45 | // domain中间件 46 | const domain = (_hosts) => { 47 | return { 48 | request(_request) { 49 | const hosts = getParamValue(_hosts); 50 | const { url, options } = _request; 51 | const host = hosts[options.domain]; 52 | _request.url = `${host}${url}`; 53 | return _request; 54 | } 55 | }; 56 | }; 57 | 58 | // 查询token中间件 59 | const queryToken = (_tokens) => { 60 | return { 61 | request(_request) { 62 | const tokens = getParamValue(_tokens); 63 | const url = _request.url; 64 | const tokenStr = qs.stringify(tokens); 65 | const connector = url.includes('?') ? '&' : '?'; 66 | _request.url = `${url}${connector}${tokenStr}`; 67 | return _request; 68 | } 69 | }; 70 | }; 71 | 72 | // header中间件 73 | const headerToken = (_tokens) => { 74 | return { 75 | request(_request) { 76 | const tokens = getParamValue(_tokens); 77 | _request.options.headers = Object.assign({}, _request.options.headers, tokens); 78 | return _request; 79 | } 80 | }; 81 | }; 82 | 83 | // 授权失败中间件 84 | const authFailed = (_options) => { 85 | const defaultOptions = { 86 | codes: ['404', '401'], 87 | hash: '/login' 88 | }; 89 | return { 90 | responseError(_responseError) { 91 | const options = getParamValue(_options) || defaultOptions; 92 | if (options.codes.includes(_responseError.errorCode)) { 93 | window.location.hash = options.hash; 94 | } 95 | return Promise.reject(_responseError); 96 | } 97 | }; 98 | }; 99 | 100 | 101 | 102 | const dataStatus = (validateStateError) => { 103 | const defaultErrorValidate = (_response) => { 104 | return _response.status && _response.status.toUpperCase() == 'ERROR'; 105 | }; 106 | return { 107 | response(_response) { 108 | if ((validateStateError || defaultErrorValidate)(_response)) { 109 | return Promise.reject(_response); 110 | } 111 | return _response; 112 | } 113 | }; 114 | }; 115 | 116 | // 错误处理中间件 117 | const onerror = () => { 118 | const DEFAULT_RES_ERROR = '请求错误'; 119 | const DEFAULT_REQ_ERROR = '请求异常'; 120 | let hasErrorModal = false; 121 | return { 122 | responseError(_responseError, _request) { 123 | if (!_request.options.ignoreErrorModal && !hasErrorModal) { 124 | const title = _responseError.message || _responseError.errorMsg || DEFAULT_RES_ERROR; 125 | Modal.error({ 126 | title, 127 | onOk: () => { 128 | hasErrorModal = false; 129 | }, 130 | onCancel: () => {} 131 | }); 132 | hasErrorModal = true; 133 | } 134 | return Promise.reject(_responseError); 135 | }, 136 | requestError(_requestError) { 137 | if (!hasErrorModal) { 138 | Modal.error({ 139 | title: DEFAULT_REQ_ERROR, 140 | onOk: () => { 141 | hasErrorModal = false; 142 | } 143 | }); 144 | hasErrorModal = true; 145 | } 146 | return Promise.reject(_requestError); 147 | } 148 | }; 149 | }; 150 | 151 | // response状态码中间件 152 | const status = () => { 153 | return { 154 | response(_response) { 155 | if (_response.status >= 200 && _response.status < 300) { 156 | return _response; 157 | } 158 | return Promise.reject(_response); 159 | } 160 | }; 161 | }; 162 | 163 | // response json中间件 164 | const json = () => { 165 | return { 166 | response(_response) { 167 | return _response.json(); 168 | } 169 | }; 170 | }; 171 | 172 | // content中间件 173 | const content = (contentKey) => { 174 | const DEFAULT_CONTENT_KEY = 'content'; 175 | return { 176 | response(_response) { 177 | return _response[contentKey || DEFAULT_CONTENT_KEY]; 178 | } 179 | }; 180 | }; 181 | 182 | export default { 183 | domain, 184 | transform, 185 | dataTransform, 186 | queryToken, 187 | headerToken, 188 | onerror, 189 | status, 190 | dataStatus, 191 | json, 192 | content, 193 | authFailed 194 | }; 195 | -------------------------------------------------------------------------------- /src/carno/utils/index.js: -------------------------------------------------------------------------------- 1 | import Form from './form/index'; 2 | import Table from './table/index'; 3 | import Model from './model/index'; 4 | 5 | export default { 6 | Form, 7 | Table, 8 | Model 9 | }; 10 | -------------------------------------------------------------------------------- /src/carno/utils/model/doc.md: -------------------------------------------------------------------------------- 1 | ## Model 工具类 2 | 3 | 对`dva model`的扩展,使得`model`更实用. 4 | 5 | ## extend 6 | 7 | 主要作用是继承默认的`model`配置, 8 | 参数: 9 | 10 | - defaults: 默认model 11 | - properties: 属性集 12 | 13 | 如果`defaults`为空,则继承自默认的`model` 14 | 15 | 默认配置如下: 16 | 17 | ```javascript 18 | { 19 | state: { 20 | visible: false, 21 | loading: false, 22 | spinning: false, 23 | confirmLoading: false 24 | }, 25 | subscriptions: {}, 26 | effects: {}, 27 | reducers: { 28 | updateLoading: createNestedReducer('loading'), 29 | updateConfirmLoading: createNestedReducer('confirmLoading'), 30 | updateSpinner: createNestedReducer('spinning'), 31 | showLoading: createNestedValueRecuder('loading', true), 32 | hideLoading: createNestedValueRecuder('loading', false), 33 | showConfirmLoading: createNestedValueRecuder('confirmLoading', true), 34 | hideConfirmLoading: createNestedValueRecuder('confirmLoading', false),, 35 | showSpinning: createNestedValueRecuder('spinning', true),, 36 | hideSpinning: createNestedValueRecuder('spinning', true),, 37 | updateState(state, { payload }) { 38 | return { 39 | ...state, 40 | ...payload 41 | }; 42 | }, 43 | resetState(state) { 44 | return { 45 | ...initialState 46 | } 47 | } 48 | } 49 | } 50 | 51 | ``` 52 | 53 | 使用示例: 54 | 55 | ``` 56 | import { Model } from 'carno'; 57 | 58 | export default Model.extend({ 59 | namespace: 'user', 60 | 61 | subscriptions: {}, 62 | 63 | effects: {}, 64 | 65 | reducers: {} 66 | }); 67 | ``` 68 | 部分业务场景中,`model`需要多个`spinning/loading/confirmLoading`状态进行控制,`model`中的默认状态`reducer`都支持嵌套数据更新, 下面我们以`loading`为例(spinning, confirmLoading类似) 69 | 70 | - showLoading 71 | 支持单状态以及嵌套状态, 72 | 单状态: yield put({ type: 'showLoading' }) 73 | 多状态: yield put({ type: 'showLoading', payload: { key: 'users' } }) 74 | - hideLoading 75 | 支持单状态以及嵌套状态, 76 | 单状态: yield put({ type: 'hideLoading' }) 77 | 多状态: yield put({ type: 'hideLoading', payload: { key: 'users' } }) 78 | - updateLoading 79 | 仅支持嵌套状态 80 | yield put({ type: 'updateLoading', payload: { user: true } }) 81 | - callWithLoading 82 | 支持单状态以及嵌套状态, 83 | 单状态: yield callWithLoading(services.getUsers); 84 | 多状态: yield callWithLoading(services.getUsers, null, { key: users}); 85 | 86 | > 注意: 在同一个业务不要混合发送单状态以及多状态的reducer 87 | 88 | ```javascript 89 | 90 | import { Model } from 'carno'; 91 | 92 | export default Model.extend({ 93 | 94 | state: { 95 | // 如果同一个页面中,有多处confirmLoading或者spinner, 可以参考如下定义state 96 | spinning: { 97 | users: false, 98 | logs: false 99 | } 100 | }, 101 | 102 | effects: { 103 | *fetchUsers({ payload }, { put }) { 104 | yield put({ type: 'showSpinning', payload: { key: 'users' }}); 105 | yield call(services.getUsers); 106 | yield put({ type: 'hideSpinning', payload: { key: 'users' }}); 107 | 108 | // 也可以使用如下写法: 109 | yield callWithSpinning(services.getUsers, null, { key: 'users' }); 110 | }, 111 | *fetchLogs({ payload }, { put }) { 112 | yield put({ type: 'showSpinning', payload: { key: 'logs' }}); 113 | yield call(services.getLogs); 114 | yield put({ type: 'hideSpinning', payload: { key: 'logs' }}); 115 | } 116 | } 117 | }) 118 | 119 | 120 | ``` 121 | 122 | 123 | 124 | 如果项目中需要扩展`defaultModel`,可以自行包装一个`extend`方法,参考如下代码: 125 | 126 | ```javascript 127 | 128 | import { Model } from 'carno'; 129 | 130 | const extend = (properties) => { 131 | const defaultModel = { 132 | ... 133 | }; 134 | 135 | return Model.extend(defaultModel, properties); 136 | } 137 | 138 | export extend; 139 | ``` 140 | 141 | 142 | ### subsciptions扩展 143 | 144 | 为方便对`path`的监听,在`model`的subscriptions配置函数参数中,额外添加了扩展方法`listen` 145 | `listen`函数参数如下: 146 | 147 | - pathReg 148 | 需要监听的pathName 149 | - action 150 | action既可以是 redux action,也可以是一个回调函数 151 | 如果action是函数,调用时,将传入{ ...location, params }作为其参数 152 | 153 | listen函数也支持同时多多个pathname的监听,传入的参数需要为`{pathReg: action}`健值对的对象. 154 | 155 | ```javascript 156 | import { Model } from 'carno'; 157 | 158 | export default Model.extend({ 159 | 160 | namespance: 'user', 161 | 162 | pathSubscriptions: { 163 | setup({ dispatch, listen }) { 164 | 165 | //action为 redux action 166 | listen('/user/list', { type: 'fetchUsers'}); 167 | 168 | //action为 回调函数 169 | listen('/user/query', ({ query, params }) => { 170 | dispatch({ 171 | type: 'fetchUsers', 172 | payload: params 173 | }) 174 | }); 175 | 176 | //支持对多个path的监听 177 | listen({ 178 | '/user/list': ({ query, params }) => {}, 179 | '/user/query': ({ query, params }) => {}, 180 | }); 181 | } 182 | }) 183 | 184 | 185 | ``` 186 | 187 | ### effects扩展 188 | 189 | 此外,我们对`effects`也做了一些扩充,方便处理加载状态以及指定的成功/失败消息. 190 | 扩展方法如下: 191 | 192 | - put 扩展put方法,支持双参数模式 put(type, payload) 193 | - update udpateState的快捷方法 update({ accounts }) 194 | - callWithLoading 调用请求时,自动处理loading状态 195 | - callWithConfirmLoading 调用请求时,自动处理confirmLoading状态 196 | - callWithSpinning 调用请求时,自动处理spinning状态 197 | - callWithMessage 调用请求后,显示指定的成功或者失败的消息 198 | - callWithExtra 原始扩展方法,支持config(loading,confirmloading,success,error)参数 199 | 200 | 以上函数都支持第三个参数,message = { successMsg, errorMsg } 201 | 202 | ```javascript 203 | Model.extend({ 204 | state: {}, 205 | effects: { 206 | *fetchUsers({payload}, {put,select,call,callWithLoading,callWithConfirmLoading,callWithMessage,callWithExtra}){ 207 | 208 | //发送请求前,显示loading状态,完成后结束loading状态.如果请求成功则提示加载用户成功,失败则提示 209 | const users = yeild callWithLoading(service.user.getList,null,{successMsg:'加载用户成功',errorMsg:'加载用户失败'}); 210 | 211 | //发送请求前,显示ConfirmLoading状态,完成后结束ConfirmLoading状态.如果请求成功则提示加载用户成功,失败则提示 212 | const users = yeild callWithConfirmLoading(service.user.getList,null,{successMsg:'加载用户成功',errorMsg:'加载用户失败'}); 213 | 214 | //发送请求前,显示spinning状态,完成后结束spinning状态.如果请求成功则提示加载用户成功,失败则提示 215 | const users = yeild callWithSpinning(service.user.getList,null,{successMsg:'加载用户成功',errorMsg:'加载用户失败'}); 216 | 217 | //仅处理成功/失败的消息提示 218 | const users = yeild callWithMessage(service.user.getList,null,{successMsg:'加载用户成功',errorMsg:'加载用户失败'}); 219 | 220 | //支持config参数的call方法,目前仅支持: loading,confirmloading,success,error 221 | const users = yield callWithExtra(service.user.getList,null,{ 222 | loading: ture, 223 | confirmLoading: true, 224 | successMsg:'加载用户成功', 225 | errorMsg:'加载用户失败' 226 | }); 227 | 228 | //更新当前model的state 229 | yield update({ users }) 230 | // update 方法等同于以下方法 231 | yield put({ 232 | type: 'updateState', 233 | payload: { 234 | users 235 | } 236 | }) 237 | 238 | //扩展put方法 239 | yield put('updateItem', { item }); 240 | // 等同于以下方法 241 | yield put({ 242 | type: 'updateItem', 243 | payload: { 244 | item 245 | } 246 | }) 247 | 248 | } 249 | } 250 | }) 251 | 252 | ``` 253 | -------------------------------------------------------------------------------- /src/carno/utils/model/index.js: -------------------------------------------------------------------------------- 1 | import pathToRegexp from 'path-to-regexp'; 2 | import { message as Message, Modal } from 'antd'; 3 | 4 | const PATH_SUBSCRIBER_KEY = '_pathSubscriberKey'; 5 | 6 | const createNestedValueRecuder = (parentKey, value) => (state, { payload: { key } }) => { 7 | let parentState = state[parentKey]; 8 | 9 | if (key) { 10 | parentState = typeof parentState == 'boolean' ? {} : parentState; 11 | parentState = { ...parentState, [key]: value }; 12 | } else { 13 | // 兼容旧版本,如果type不存在,则直接对parent赋值 14 | parentState = value; 15 | } 16 | 17 | return { 18 | ...state, 19 | [parentKey]: parentState 20 | }; 21 | }; 22 | 23 | const createNestedRecuder = (parentKey) => (state, { payload }) => { 24 | let parentState = state[parentKey]; 25 | parentState = typeof parentState == 'boolean' ? {} : parentState; 26 | 27 | return { 28 | ...state, 29 | [parentKey]: { 30 | ...parentState, 31 | payload 32 | } 33 | } 34 | } 35 | 36 | const getDefaultModel = () => { 37 | return { 38 | // 为了兼容旧版本,初始值依旧为false.如果应用中需要多个控制状态,则在model中覆盖初始属性 39 | state: { 40 | visible: false, 41 | spinning: false, 42 | loading: false, 43 | confirmLoading: false 44 | }, 45 | subscriptions: {}, 46 | effects: {}, 47 | reducers: { 48 | showLoading: createNestedValueRecuder('loading', true), 49 | hideLoading: createNestedValueRecuder('loading', false), 50 | showConfirmLoading: createNestedValueRecuder('confirmLoading', true), 51 | hideConfirmLoading: createNestedValueRecuder('confirmLoading', false), 52 | showSpinning: createNestedValueRecuder('spinning', true), 53 | hideSpinning: createNestedValueRecuder('spinning', false), 54 | updateLoading: createNestedRecuder('loading'), 55 | updateSpinner: createNestedRecuder('spinning'), 56 | updateConfirmLoading: createNestedRecuder('confirmLoading'), 57 | updateState(state, { payload }) { 58 | return { 59 | ...state, 60 | ...payload 61 | }; 62 | } 63 | } 64 | }; 65 | }; 66 | 67 | /** 68 | * 扩展subscription函数的参数,支持listen方法,方便监听path改变 69 | * 70 | * listen函数参数如下: 71 | * pathReg 需要监听的pathname 72 | * action 匹配path后的回调函数,action即可以是redux的action,也可以是回调函数 73 | * listen函数同时也支持对多个path的监听,参数为{ pathReg: action, ...} 格式的对象 74 | * 75 | * 示例: 76 | * subscription({ dispath, history, listen }) { 77 | * listen('/user/list', { type: 'fetchUsers'}); 78 | * listen('/user/query', ({ query, params }) => { 79 | * dispatch({ 80 | * type: 'fetchUsers', 81 | * payload: params 82 | * }) 83 | * }); 84 | * listen({ 85 | * '/user/list': ({ query, params }) => {}, 86 | * '/user/query': ({ query, params }) => {}, 87 | * }); 88 | * } 89 | */ 90 | const enhanceSubscriptions = (subscriptions = {}) => { 91 | return Object 92 | .keys(subscriptions) 93 | .reduce((wrappedSubscriptions, key) => { 94 | wrappedSubscriptions[key] = createWrappedSubscriber(subscriptions[key]); 95 | return wrappedSubscriptions; 96 | }, {}); 97 | 98 | function createWrappedSubscriber(subscriber) { 99 | return (props) => { 100 | const { dispatch, history } = props; 101 | 102 | const listen = (pathReg, action) => { 103 | let listeners = {}; 104 | if (typeof pathReg == 'object') { 105 | listeners = pathReg; 106 | } else { 107 | listeners[pathReg] = action; 108 | } 109 | 110 | history.listen((location) => { 111 | const { pathname } = location; 112 | Object.keys(listeners).forEach(key => { 113 | const _pathReg = key; 114 | const _action = listeners[key]; 115 | const match = pathToRegexp(_pathReg).exec(pathname); 116 | 117 | if (match) { 118 | if (typeof _action == 'object') { 119 | dispatch(_action); 120 | } else if (typeof _action == 'function') { 121 | _action({ ...location, params: match.slice(1) }); 122 | } 123 | } 124 | }); 125 | }); 126 | }; 127 | 128 | subscriber({ ...props, listen }); 129 | }; 130 | } 131 | }; 132 | 133 | /** 134 | * 扩展effect函数中的sagaEffects参数 135 | * 支持: 136 | * put 扩展put方法,支持双参数模式: put(type, payload) 137 | * update 扩展自put方法,方便直接更新state数据,update({ item: item}); 138 | * callWithLoading, 139 | * callWithConfirmLoading, 140 | * callWithSpinning, 141 | * callWithMessage, 142 | * callWithExtra 143 | * 以上函数都支持第三个参数,message = { successMsg, errorMsg } 144 | */ 145 | const enhanceEffects = (effects = {}) => { 146 | const wrappedEffects = {}; 147 | Object 148 | .keys(effects) 149 | .forEach(key => { 150 | wrappedEffects[key] = function* (action, sagaEffects) { 151 | const extraSagaEffects = { 152 | ...sagaEffects, 153 | put: createPutEffect(sagaEffects), 154 | update: createUpdateEffect(sagaEffects), 155 | callWithLoading: createExtraCall(sagaEffects, { loading: true }), 156 | callWithConfirmLoading: createExtraCall(sagaEffects, { confirmLoading: true }), 157 | callWithSpinning: createExtraCall(sagaEffects, { spinning: true }), 158 | callWithMessage: createExtraCall(sagaEffects), 159 | callWithExtra: (serviceFn, args, config) => { createExtraCall(sagaEffects, config)(serviceFn, args, config); } 160 | }; 161 | 162 | yield effects[key](action, extraSagaEffects); 163 | }; 164 | }); 165 | 166 | return wrappedEffects; 167 | 168 | function createPutEffect(sagaEffects) { 169 | const { put } = sagaEffects; 170 | return function* putEffect(type, payload) { 171 | let action = { type, payload }; 172 | if (arguments.length == 1 && typeof type == 'object') { 173 | action = arguments[0]; 174 | } 175 | yield put(action); 176 | }; 177 | } 178 | 179 | function createUpdateEffect(sagaEffects) { 180 | const { put } = sagaEffects; 181 | return function* updateEffect(payload) { 182 | yield put({ type: 'updateState', payload }); 183 | }; 184 | } 185 | 186 | function createExtraCall(sagaEffects, config = {}) { 187 | const { put, call } = sagaEffects; 188 | return function* extraCallEffect(serviceFn, args, message = {}) { 189 | let result; 190 | const { loading, confirmLoading, spinning } = config; 191 | const { successMsg, errorMsg, key } = message; 192 | 193 | if (loading) { 194 | yield put({ type: 'showLoading', payload: { key } }); 195 | } 196 | if (confirmLoading) { 197 | yield put({ type: 'showConfirmLoading', payload: { key } }); 198 | } 199 | if (spinning) { 200 | yield put({ type: 'showSpinning', payload: { key } }); 201 | } 202 | 203 | try { 204 | result = yield call(serviceFn, args); 205 | successMsg && Message.success(successMsg); 206 | } catch (e) { 207 | errorMsg && Modal.error({ title: errorMsg }); 208 | throw e; 209 | } finally { 210 | if (loading) { 211 | yield put({ type: 'hideLoading', payload: { key } }); 212 | } 213 | if (confirmLoading) { 214 | yield put({ type: 'hideConfirmLoading', payload: { key } }); 215 | } 216 | if (spinning) { 217 | yield put({ type: 'hideSpinning', payload: { key } }); 218 | } 219 | } 220 | 221 | return result; 222 | }; 223 | } 224 | }; 225 | 226 | /** 227 | * 模型继承方法 228 | * 229 | * 如果参数只有一个,则继承默认model 230 | * @param defaults 231 | * @param properties 232 | */ 233 | function extend(defaults, properties) { 234 | if (!properties) { 235 | properties = defaults; 236 | defaults = null; 237 | } 238 | 239 | const model = defaults || getDefaultModel(); 240 | const modelAssignKeys = ['state', 'subscriptions', 'effects', 'reducers']; 241 | const { namespace } = properties; 242 | 243 | modelAssignKeys.forEach((key) => { 244 | if (key == 'subscriptions') { 245 | properties[key] = enhanceSubscriptions(properties[key]); 246 | } 247 | if (key == 'effects') { 248 | properties[key] = enhanceEffects(properties[key]); 249 | } 250 | Object.assign(model[key], properties[key]); 251 | }); 252 | 253 | const initialState = { 254 | ...model.state 255 | }; 256 | 257 | Object.assign(model.reducers, { 258 | resetState() { 259 | return { 260 | ...initialState 261 | }; 262 | } 263 | }); 264 | 265 | return Object.assign(model, { namespace }); 266 | } 267 | 268 | export default { 269 | extend 270 | }; 271 | -------------------------------------------------------------------------------- /src/carno/utils/table/doc.md: -------------------------------------------------------------------------------- 1 | # Table工具类 2 | 3 | 后台系统业务大部分都是表格+表单的形式,故我们在`model`层,统一定义模型的数据结构,以方便在`table+form`中复用,简化实际的开发工作. 4 | 这里主要介绍下`Table`工具类的使用. 5 | 6 | ### 使用场景 7 | field提供统一的数据格式,以方便在form以及table中复用,参考如下: 8 | 9 | ``` javascript 10 | 11 | const fields = [ 12 | { 13 | key: 'name', // 字段key 14 | name: '名称' // 字段name 15 | type: 'text' // 字段类型支持如下类型: date|datetime|datetimeRange|enum|boolean|number|textarea|text 16 | meta: { 17 | min: 0, 18 | max: 100, 19 | rows: 12 20 | }, 21 | enums: [ //枚举数据,如果包含enums属性,则field默认为每句类型 22 | { ENABLED: '启用'}, 23 | { DISABLED: '禁用'} 24 | ], 25 | required: true 26 | } 27 | ] 28 | 29 | ``` 30 | 31 | Table类的主要作用是将以上通用的`field`格式,转换为`antd`中`table`支持的`column`定义 32 | 33 | ### 如何使用 34 | 35 | 与`form`类类似,通过`utils`引入 36 | 37 | > import { Utils } from 'carno'; 38 | > const { getColumn } = Utils.Table; 39 | 40 | Table工具类主要提供以下接口: 41 | 42 | - getColumns 转换field为column格式 43 | - combineTypes 扩展支持的字段类型 44 | - getFieldValue 获取数据显示值,传入field定义 45 | 46 | ##### getColumns 47 | 核心方法,转换通用字段类型为column格式, getColumns需要配合`antd.Table`组件使用. 48 | 49 | 参数: 50 | 51 | - originFields 通用的fields定义,一般由model中定义 52 | - fieldKeys 需要pick的keys, 通用的fields往往是个字段的超级,在table中一般只需要显示部分字段 53 | - extraFields 扩展的字段定义, 可以对通用字段的属性扩展 54 | 55 | getColums返回的是一个链式对象,需要调用`values`方法才能返回最终的结果。 56 | 链式对象支持如下方法: 57 | 58 | - pick 参数与fieldKeys格式一致 59 | - excludes 排除部分字段 60 | - enhance 参数与extraFields格式一致 61 | - values 返回数据结果 62 | 63 | ```javascript 64 | 65 | import { Utils } from 'carno'; 66 | 67 | const { getColumns } = Utils.Table; 68 | 69 | const fields = [{ 70 | key: 'name', 71 | name: '名称' 72 | }, { 73 | key: 'author', 74 | name: '作者' 75 | }, { 76 | key: 'desc', 77 | name: '简介' 78 | }]; 79 | 80 | function UserList({ users }) { 81 | 82 | const operatorColumn = [{ 83 | key: 'operator', 84 | name: '操作', 85 | //扩展字段的render支持自定义渲染 86 | render: (value, record) => { 87 | return ( 88 | 93 | ); 94 | } 95 | }] 96 | 97 | const tableColumns = getColumns(fields,['name','author'],operatorColumn).values(); 98 | //排除id,desc字段 99 | const tableColumns = getColumns(fields).excludes(['id','desc']).enhance(operatorColumn).values(); 100 | //pick name|author|openTime字段,并且扩展name字段的rules属性 101 | const tableColumns = getColumns(fields).pick(['name','author','openTime']).enhance(operatorColumn).values(); 102 | 103 | const tableProps = { 104 | dataSource: users, 105 | columns: tableColumns 106 | } 107 | 108 | return ; 109 | } 110 | 111 | //pick name|author字段 112 | 113 | 114 | ``` 115 | 116 | ##### combineTypes 117 | 118 | 扩展通用字段定义支持的表格字段类型, 自定义字段类型写法参考如下: 119 | 120 | ```javascript 121 | combineTypes({ 122 | //参数:value: item值, field: 字段定义 123 | datetime: (value, field) => { 124 | return moment(new Date(parseInt(value, 10))).format('YYYY-MM-DD HH:mm:ss'); 125 | }, 126 | }) 127 | 128 | ``` 129 | 130 | ##### getFieldValue 131 | 132 | 通过传入字段定义,获取对应字段的值,此函数会根据默认的fieldTypes来做数据转换 133 | 134 | ```javascript 135 | const rowData = { createTime: 14300000231823 }; 136 | const field = { 137 | key: 'createTime', 138 | type: 'datatime' 139 | } 140 | getFieldValue(rowData.createTime,field);// 返回日期时间格式:2017-12-12 10:10:10 141 | 142 | ``` 143 | -------------------------------------------------------------------------------- /src/carno/utils/table/fieldTypes.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | 3 | /* 4 | * column类型定义 5 | */ 6 | const fieldTypes = { 7 | normal: (value) => value, 8 | number: (value) => value, 9 | textarea: (value) => value, 10 | datetime: (value) => { 11 | return value ? moment(new Date(parseInt(value, 10))).format('YYYY-MM-DD HH:mm:ss') : ''; 12 | }, 13 | date: (value) => { 14 | return value ? moment(new Date(value)).format('YYYY-MM-DD') : ''; 15 | }, 16 | enum: (value, field) => { 17 | return field.enums[value]; 18 | }, 19 | boolean: (value) => { 20 | return (value == 'true' || value === true) ? '是' : '否'; 21 | } 22 | }; 23 | 24 | /* 25 | * 扩展column类型定义 26 | */ 27 | export const combineTypes = (types) => { 28 | Object.assign(fieldTypes, types); 29 | }; 30 | 31 | export default fieldTypes; 32 | 33 | -------------------------------------------------------------------------------- /src/carno/utils/table/index.js: -------------------------------------------------------------------------------- 1 | import { default as fieldTypes, combineTypes } from './fieldTypes'; 2 | 3 | /* 4 | * 获取column中显示的filedValue 5 | */ 6 | function getFieldValue(value, field = {}) { 7 | let type = field.type || (field.enums && 'enum'); 8 | type = fieldTypes.hasOwnProperty(type) ? type : 'normal'; 9 | return fieldTypes[type](value, field); 10 | } 11 | 12 | /* 13 | * 获取表格column数组 14 | * 示例: 15 | * const columns = getColumns(fields,['name','author'],{ name: { render: ()=>{} }}).values(); 16 | * const columns = getColumns(fields).excludes(['id','desc']).values(); 17 | * const columns = getColumns(fields).pick(['name','author','openTime']).enhance({name:{ render: ()=>{} }}).values(); 18 | * @param originField 原始fields 19 | * @param fieldKeys 需要包含的字段keys 20 | * @param extraFields 扩展的fields 21 | * @result 链式写法,返回链式对象(包含pick,excludes,enhance,values方法), 需要调用values返回最终的数据 22 | */ 23 | function getColumns(fields, fieldKeys, extraFields) { 24 | 25 | const chain = {}; 26 | let columns = []; 27 | 28 | const transform = (_fields) => { 29 | return _fields.map(field => { 30 | let { dataIndex, title, key, name, render, ...others } = field; 31 | 32 | if (!render) { 33 | render = (value) => { 34 | return getFieldValue(value, field); 35 | }; 36 | } 37 | 38 | return { 39 | dataIndex: key || dataIndex, 40 | title: name || title, 41 | render, 42 | ...others 43 | }; 44 | }); 45 | }; 46 | 47 | const pick = (_fieldKeys) => { 48 | _fieldKeys = [].concat(_fieldKeys); 49 | columns = _fieldKeys.map(fieldKey => { 50 | let column = columns.find(item => fieldKey == (item.key || item.dataIndex)); 51 | if (!column) { 52 | // 如果fieldKey不存在,则创建text类型的column 53 | column = { 54 | dataIndex: fieldKey, 55 | title: fieldKey, 56 | render: (value) => { 57 | return getFieldValue(value); 58 | } 59 | }; 60 | } 61 | return column; 62 | }); 63 | return chain; 64 | }; 65 | 66 | const excludes = (_fieldKeys) => { 67 | _fieldKeys = [].concat(_fieldKeys); 68 | columns = columns.filter(column => !_fieldKeys.includes(column.dataIndex)); 69 | return chain; 70 | }; 71 | 72 | const enhance = (_extraColumns) => { 73 | if (!Array.isArray(_extraColumns)) { 74 | _extraColumns = Object.keys(_extraColumns).map(key => { 75 | return Object.assign(_extraColumns[key], { 76 | key 77 | }); 78 | }); 79 | } 80 | _extraColumns.forEach(extraColumn => { 81 | let { dataIndex, title, key, name, ...others } = extraColumn; 82 | extraColumn = { 83 | dataIndex: key || dataIndex, 84 | title: name || title, 85 | ...others 86 | }; 87 | 88 | const column = columns.find(item => item.dataIndex == extraColumn.dataIndex); 89 | if (column) { 90 | Object.assign(column, extraColumn); 91 | } else { 92 | columns.push(extraColumn); 93 | } 94 | }); 95 | 96 | return chain; 97 | }; 98 | 99 | const values = () => { 100 | return columns; 101 | }; 102 | 103 | columns = transform(fields); 104 | 105 | if (fieldKeys) { 106 | pick(fieldKeys); 107 | } 108 | 109 | if (extraFields) { 110 | enhance(extraFields); 111 | } 112 | 113 | return Object.assign(chain, { 114 | pick, 115 | excludes, 116 | enhance, 117 | values 118 | }); 119 | } 120 | 121 | export default { 122 | combineTypes, 123 | getFieldValue, 124 | getColumns 125 | }; 126 | -------------------------------------------------------------------------------- /src/components/BlacklistManage/BlacklistModal.js: -------------------------------------------------------------------------------- 1 | import { Form } from 'antd'; 2 | import { HForm, HModal, Utils } from 'carno'; 3 | 4 | const { getFields } = Utils.Form; 5 | 6 | const layout = { 7 | labelCol: { 8 | span: 6, 9 | }, 10 | wrapperCol: { 11 | span: 14, 12 | }, 13 | }; 14 | 15 | class BlacklistModal extends React.Component { 16 | 17 | constructor(props) { 18 | super(props); 19 | this.state = {}; 20 | } 21 | 22 | handleSubmit(values) { 23 | const { blacklist: { id }, onOk } = this.props 24 | onOk({ ...values, id }); 25 | } 26 | 27 | render() { 28 | const { visible, form, fields, confirmLoading, blacklist, onOk } = this.props; 29 | const formProps = { 30 | form, 31 | layout, 32 | item: blacklist, 33 | fields: getFields(fields).values() 34 | }; 35 | return ( 36 | 37 | 38 | 39 | ); 40 | } 41 | } 42 | 43 | export default Form.create()(BlacklistModal); 44 | -------------------------------------------------------------------------------- /src/components/BlacklistManage/SearchMore.js: -------------------------------------------------------------------------------- 1 | import { Button } from 'antd'; 2 | import { SearchBar as HSearchBar, Utils } from 'carno'; 3 | 4 | const { getFields } = Utils.Form; 5 | 6 | function SearchMore({ search = {}, fields, onAdd, onSearch }) { 7 | 8 | const btns = ( 9 | 10 | ); 11 | 12 | const searchFields = getFields(fields).pick(['type', 'userId', 'content']).values(); 13 | const searchBarProps = { search, btns, onSearch, layout: 'inline', fields: searchFields, showReset: true, formItemLayout: { itemCol: { span: 4 }, btnCol: { span: 8 } } }; 14 | 15 | return ( 16 | 17 | ); 18 | } 19 | 20 | export default SearchMore; 21 | -------------------------------------------------------------------------------- /src/components/BlacklistManage/index.js: -------------------------------------------------------------------------------- 1 | import { Table, Button, Popconfirm } from 'antd'; 2 | import { Utils } from 'carno'; 3 | 4 | import BlacklistModal from './BlacklistModal'; 5 | import SearchMore from './SearchMore'; 6 | 7 | const { getColumns } = Utils.Table; 8 | 9 | class Blacklist extends React.Component { 10 | constructor(props) { 11 | super(props); 12 | 13 | this.state = { 14 | operateType: '', // add|update 15 | visible: false, 16 | blacklist: {}, 17 | }; 18 | } 19 | 20 | getInitalColumns(fields) { 21 | const extraFields = [{ 22 | key: 'operator', 23 | name: '操作', 24 | render: (value, record) => { 25 | return ( 26 |
27 | this.handleModal('update', record)}>修改 28 | 29 | this.handleDelete(record.id)} 33 | okText="是" 34 | cancelText="否" 35 | > 36 | 删除 37 | 38 |
39 | ); 40 | } 41 | }]; 42 | 43 | return getColumns(fields).enhance(extraFields).values(); 44 | } 45 | 46 | // 显示添加或修改的弹出框 47 | handleModal(type, blacklist = {}) { 48 | this.setState({ 49 | operateType: type, 50 | visible: Symbol(), 51 | blacklist 52 | }); 53 | } 54 | 55 | // 保存 56 | handleSave(data) { 57 | this.props.onSave(data); 58 | } 59 | 60 | // 删除 61 | handleDelete(uuid) { 62 | this.props.onDelete(uuid); 63 | } 64 | 65 | render() { 66 | const { fields, blacklists, total, search, loading, confirmLoading, onSearch } = this.props; 67 | const { pn, ps } = search; 68 | const { operateType, visible, blacklist } = this.state; 69 | const columns = this.getInitalColumns(fields); 70 | 71 | const pagination = { 72 | current: pn, 73 | total, 74 | pageSize: ps, 75 | onChange: page => onSearch({ pn: page }) 76 | }; 77 | 78 | const tableProps = { 79 | dataSource: blacklists, 80 | columns, 81 | loading, 82 | rowKey: 'id', 83 | pagination 84 | }; 85 | 86 | const modalProps = { 87 | operateType, 88 | confirmLoading, 89 | blacklist, 90 | visible, 91 | fields, 92 | onOk: this.handleSave.bind(this), 93 | }; 94 | 95 | const searchBarProps = { 96 | onAdd: () => { this.handleModal('add'); }, 97 | onSearch, 98 | fields 99 | }; 100 | 101 | return ( 102 |
103 | 104 |
105 | 106 | 107 | ); 108 | } 109 | } 110 | 111 | export default Blacklist; 112 | -------------------------------------------------------------------------------- /src/components/Layout/index.js: -------------------------------------------------------------------------------- 1 | import { Layout, Menu } from 'antd'; 2 | import { Link } from 'react-router'; 3 | 4 | const { Header, Content } = Layout; 5 | 6 | export default function HLayout({ children }) { 7 | return ( 8 | 9 |
10 |
11 | 17 | 18 | 用户管理 19 | 20 | 21 | 黑名单管理 22 | 23 | 24 |
25 | 26 |
{children}
27 |
28 |
29 | ); 30 | } -------------------------------------------------------------------------------- /src/components/UserManage/UserModal.js: -------------------------------------------------------------------------------- 1 | import { Form, Row, Col } from 'antd'; 2 | import { HModal, HFormItem, Utils } from 'carno'; 3 | 4 | const { getFields } = Utils.Form; 5 | 6 | const layout = { 7 | labelCol: { 8 | span: 6, 9 | }, 10 | wrapperCol: { 11 | span: 14, 12 | }, 13 | }; 14 | 15 | class UserModal extends React.Component { 16 | 17 | render() { 18 | const { visible, fields, form, confirmLoading, onOk } = this.props; 19 | const itemProps = { form, item: {}, ...layout }; 20 | const fieldMap = getFields(fields).toMapValues(); 21 | 22 | return ( 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | ); 46 | } 47 | } 48 | 49 | export default Form.create()(UserModal); 50 | -------------------------------------------------------------------------------- /src/components/UserManage/index.js: -------------------------------------------------------------------------------- 1 | import { Table, Button, Popconfirm } from 'antd'; 2 | import { Utils } from 'carno'; 3 | 4 | import UserModal from './UserModal'; 5 | 6 | const { getColumns } = Utils.Table; 7 | 8 | class UserManage extends React.Component { 9 | constructor(props) { 10 | super(props); 11 | 12 | this.state = { 13 | visible: false, 14 | }; 15 | } 16 | 17 | getInitalColumns(fields) { 18 | const extraFields = [{ 19 | key: 'operator', 20 | name: '操作', 21 | render: (value, record) => { 22 | return ( 23 |
24 | this.handleDelete(record.id)} 28 | okText="是" 29 | cancelText="否" 30 | > 31 | 删除 32 | 33 |
34 | ); 35 | } 36 | }]; 37 | 38 | return getColumns(fields).enhance(extraFields).values(); 39 | } 40 | 41 | // 显示添加或修改的弹出框 42 | handleModal() { 43 | this.setState({ 44 | visible: Symbol() 45 | }); 46 | } 47 | 48 | // 保存 49 | handleSave(data) { 50 | this.props.onSave(data); 51 | } 52 | 53 | // 删除 54 | handleDelete(uuid) { 55 | this.props.onDelete(uuid); 56 | } 57 | 58 | render() { 59 | const { fields, users, loading, confirmLoading } = this.props; 60 | const { visible } = this.state; 61 | const columns = this.getInitalColumns(fields); 62 | 63 | const tableProps = { 64 | dataSource: users, 65 | columns, 66 | loading, 67 | rowKey: 'id', 68 | pagination: false, 69 | }; 70 | 71 | const modalProps = { 72 | fields, 73 | visible, 74 | confirmLoading, 75 | onOk: this.handleSave.bind(this), 76 | }; 77 | 78 | return ( 79 |
80 |
81 | 82 |
83 |
84 | 85 | 86 | ); 87 | } 88 | } 89 | 90 | export default UserManage; 91 | -------------------------------------------------------------------------------- /src/configs/constants.js: -------------------------------------------------------------------------------- 1 | export const PAGE_SIZE = 10; -------------------------------------------------------------------------------- /src/configs/index.js: -------------------------------------------------------------------------------- 1 | 2 | export servers from './servers'; 3 | 4 | export menus from './menus'; 5 | -------------------------------------------------------------------------------- /src/configs/menus.js: -------------------------------------------------------------------------------- 1 | const menus = [{ 2 | title: '系统管里', 3 | key: 'app', 4 | icon: 'laptop', 5 | children: [{ 6 | title: '用户管理', 7 | key: 'user/manage', 8 | path: 'user/manage', 9 | icon: 'laptop' 10 | }] 11 | }]; 12 | 13 | export default menus; 14 | -------------------------------------------------------------------------------- /src/configs/servers.js: -------------------------------------------------------------------------------- 1 | const servers = { 2 | proxy: { 3 | demo: '' 4 | }, 5 | dev: { 6 | demo: '' 7 | }, 8 | qa: { 9 | }, 10 | production: { 11 | } 12 | }; 13 | 14 | export default servers; 15 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | 2 | :global { 3 | html, body, #root { 4 | height: 100%; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | demo 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import dva from 'dva'; 2 | import './index.html'; 3 | import 'antd/dist/antd.css'; 4 | import './styles/common.less'; 5 | 6 | // 1. Initialize 7 | const app = dva({ 8 | onError(e) { 9 | console.log(e); 10 | } 11 | }); 12 | 13 | // 3. Model 14 | app.model(require('./models/userManage')); 15 | app.model(require('./models/blacklistManage')); 16 | 17 | 18 | // 4. Router 19 | app.router(require('./router')); 20 | 21 | // 5. Start 22 | app.start('#root'); 23 | -------------------------------------------------------------------------------- /src/models/blacklistManage/fields.js: -------------------------------------------------------------------------------- 1 | const TYPES = { 2 | 'QQ': 'QQ', 3 | 'PHONE': '手机', 4 | 'TELE': '电话', 5 | 'USER': '用户', 6 | }; 7 | const fields = [ 8 | { 9 | key: 'type', 10 | name: '类型', 11 | enums: TYPES 12 | }, { 13 | key: 'userId', 14 | name: '用户ID', 15 | }, { 16 | key: 'content', 17 | name: '内容' 18 | }, { 19 | key: 'reason', 20 | name: '说明', 21 | type: 'textarea' 22 | }, { 23 | key: 'createDate', 24 | name: '创建时间', 25 | type: 'datetime' 26 | } 27 | ]; 28 | 29 | export default fields; 30 | -------------------------------------------------------------------------------- /src/models/blacklistManage/index.js: -------------------------------------------------------------------------------- 1 | import { Model } from 'carno'; 2 | import * as services from 'services'; 3 | import fields from './fields'; 4 | 5 | const initialSearch = { 6 | pn: 1, 7 | ps: 10 8 | }; 9 | 10 | export default Model.extend({ 11 | namespace: 'blacklist', 12 | 13 | state: { 14 | fields, 15 | total: 0, 16 | search: initialSearch, 17 | blacklists: [] 18 | }, 19 | 20 | subscriptions: { 21 | setupSubscriber({ dispatch, listen }) { 22 | listen('/blacklist/manage', () => { 23 | dispatch({ type: 'resetState' }); 24 | dispatch({ type: 'fetchBlacklists' }); 25 | }); 26 | } 27 | }, 28 | 29 | effects: { 30 | *fetchBlacklists({ payload }, { select, update, callWithLoading }) { 31 | let { search } = yield select(({ blacklist }) => blacklist); 32 | search = { ...search, ...payload }; 33 | const { content: blacklists, tc: total } = yield callWithLoading(services.getBlacklists, search); 34 | yield update({ blacklists, total, search }); 35 | }, 36 | *saveBlacklist({ payload: { data } }, { put, callWithConfirmLoading }) { 37 | /* 换行分割‘内容’ 将‘内容’分别存储 */ 38 | const contents = data.content.split(/\n/); 39 | for (let i = 0; i < contents.length; i++) { 40 | const content = contents[i]; 41 | yield callWithConfirmLoading(services.saveBlacklist, { ...data, content }); 42 | } 43 | yield put({ type: 'fetchBlacklists' }); 44 | }, 45 | *deleteBlacklist({ payload: { id } }, { put, callWithLoading }) { 46 | yield callWithLoading(services.blacklist.deleteBlacklist, id); 47 | yield put({ type: 'fetchBlacklists' }); 48 | } 49 | }, 50 | 51 | reducers: { 52 | updateSearch(state, { payload: { search } }) { 53 | return { 54 | ...state, 55 | search: { ...state.search, pn: 1, ...search } 56 | }; 57 | } 58 | } 59 | }); 60 | -------------------------------------------------------------------------------- /src/models/userManage/fields.js: -------------------------------------------------------------------------------- 1 | const male = { 2 | male: '男', 3 | female: '女' 4 | }; 5 | 6 | const fields = [{ 7 | key: 'name', 8 | name: '用户名', 9 | required: true 10 | }, { 11 | key: 'gender', 12 | name: '性别', 13 | enums: male, 14 | required: true 15 | }, { 16 | key: 'age', 17 | name: '年龄', 18 | type: 'number', 19 | required: true 20 | }, { 21 | key: 'career', 22 | name: '职业', 23 | required: true 24 | }, { 25 | key: 'nation', 26 | name: '民族', 27 | required: true 28 | }, { 29 | key: 'createTime', 30 | name: '创建时间', 31 | type: 'datetime', 32 | required: true 33 | }]; 34 | 35 | export default fields; 36 | -------------------------------------------------------------------------------- /src/models/userManage/index.js: -------------------------------------------------------------------------------- 1 | import { Model } from 'carno'; 2 | import * as services from 'services'; 3 | import fields from './fields'; 4 | 5 | export default Model.extend({ 6 | namespace: 'userManage', 7 | 8 | state: { 9 | fields, 10 | users: [], 11 | }, 12 | 13 | subscriptions: { 14 | setupSubscriber({ dispatch, listen }) { 15 | listen('/user/manage', () => { 16 | dispatch({ type: 'resetState' }); 17 | dispatch({ type: 'fetchUsers' }); 18 | }); 19 | } 20 | }, 21 | 22 | effects: { 23 | * fetchUsers({ payload }, { put, callWithLoading, update }) { 24 | const users = yield callWithLoading(services.getUsers); 25 | yield update({ users }); 26 | }, 27 | * saveUser({ payload }, { put, update, callWithConfirmLoading }) { 28 | yield callWithConfirmLoading(services.saveUser, payload, { successMsg: '保存用户成功!' }); 29 | yield put({ type: 'fetchUserList' }); 30 | } 31 | }, 32 | 33 | reducers: {} 34 | }); 35 | -------------------------------------------------------------------------------- /src/pages/BlacklistManage.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'dva'; 2 | import BlacklistManage from 'components/BlacklistManage'; 3 | 4 | function mapStateToProps({ blacklist }) { 5 | return { 6 | ...blacklist, 7 | }; 8 | } 9 | 10 | function mapDispatchToProps(dispatch) { 11 | return { 12 | onSave(data) { 13 | dispatch({ type: 'blacklist/saveBlacklist', payload: { data } }); 14 | }, 15 | onDelete(id) { 16 | dispatch({ type: 'blacklist/deleteBlacklist', payload: { id } }); 17 | }, 18 | onSearch(search) { 19 | dispatch({ type: 'blacklist/updateSearch', payload: { search } }); 20 | dispatch({ type: 'blacklist/fetchBlacklists' }); 21 | } 22 | }; 23 | } 24 | 25 | export default connect(mapStateToProps, mapDispatchToProps)(BlacklistManage); 26 | -------------------------------------------------------------------------------- /src/pages/UserManage.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'dva'; 2 | 3 | import UserManage from 'components/UserManage'; 4 | 5 | function mapStateToProps({ userManage }) { 6 | return { 7 | ...userManage, 8 | }; 9 | } 10 | 11 | function mapDispatchToProps(dispatch) { 12 | return { 13 | onSave(data) { 14 | dispatch({ type: 'userManage/saveUser', payload: data }); 15 | }, 16 | onDelete(id) { 17 | dispatch({ type: 'userManage/deleteUser', payload: id }); 18 | }, 19 | }; 20 | } 21 | 22 | export default connect(mapStateToProps, mapDispatchToProps)(UserManage); 23 | -------------------------------------------------------------------------------- /src/pages/index.js: -------------------------------------------------------------------------------- 1 | import UserManage from './UserManage'; 2 | import BlacklistManage from './BlacklistManage'; 3 | 4 | export default { 5 | UserManage, 6 | BlacklistManage 7 | }; 8 | -------------------------------------------------------------------------------- /src/router.js: -------------------------------------------------------------------------------- 1 | import { Router, Route, IndexRedirect } from 'dva/router'; 2 | 3 | import pages from './pages'; 4 | import Layout from './components/Layout'; 5 | 6 | export default function ({ history }) { 7 | return ( 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /src/services/index.js: -------------------------------------------------------------------------------- 1 | import http from 'utils/http'; 2 | 3 | const { get, post } = http.create('demo'); 4 | 5 | export function getUsers() { 6 | return get('/web/user/list'); 7 | } 8 | 9 | export function saveUser(user) { 10 | return post('/web/user/save', user); 11 | } 12 | 13 | export function getBlacklists(params) { 14 | return get('/web/blacklist/list', params); 15 | } 16 | 17 | export function saveBlacklist(blacklist) { 18 | return post('/web/blacklist/save', blacklist); 19 | } 20 | -------------------------------------------------------------------------------- /src/styles/common.less: -------------------------------------------------------------------------------- 1 | :global { 2 | .actions { 3 | margin-bottom: 10px; 4 | button { 5 | margin-right: 5px; 6 | } 7 | } 8 | .pagination { 9 | float: right; 10 | margin-top: 10px; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/utils/common.js: -------------------------------------------------------------------------------- 1 | import { servers as serverConfigs } from 'configs'; 2 | 3 | export function getServer(servers = serverConfigs) { 4 | return servers[process.env.NODE_ENV || localStorage.getItem('NODE_ENV') || 'dev']; 5 | }; 6 | -------------------------------------------------------------------------------- /src/utils/http.js: -------------------------------------------------------------------------------- 1 | import { Http } from 'carno'; 2 | import { getServer } from 'utils/common'; 3 | 4 | const { domain, headerToken, content } = Http.middlewares; 5 | 6 | const domainMiddleware = domain(getServer()); 7 | const contentMiddleware = content(); 8 | const headerMiddleware = headerToken(() => { 9 | return { 10 | 'Content-Type': 'application/json' 11 | }; 12 | }); 13 | 14 | export default Http.create([domainMiddleware, contentMiddleware, headerMiddleware]); 15 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('atool-build/lib/webpack'); 2 | 3 | module.exports = function(webpackConfig, env) { 4 | // Support hmr 5 | if (env === 'development') { 6 | webpackConfig.devtool = '#eval'; 7 | webpackConfig.babel.plugins.push('dva-hmr'); 8 | } else { 9 | webpackConfig.babel.plugins.push('dev-expression'); 10 | } 11 | 12 | // Don't extract common.js and common.css 13 | webpackConfig.plugins = webpackConfig.plugins.filter(function(plugin) { 14 | return !(plugin instanceof webpack.optimize.CommonsChunkPlugin); 15 | }); 16 | 17 | // 全局暴露React 18 | webpackConfig.plugins.push( 19 | new webpack.ProvidePlugin({ 20 | React: 'react', 21 | }), 22 | new webpack.DefinePlugin({ 23 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV) 24 | }) 25 | ); 26 | 27 | // Support CSS Modules 28 | // Parse all less files as css module. 29 | webpackConfig.module.loaders.forEach(function(loader, index) { 30 | if (typeof loader.test === 'function' && loader.test.toString().indexOf('\\.less$') > -1) { 31 | loader.include = /node_modules/; 32 | loader.test = /\.less$/; 33 | } 34 | if (loader.test.toString() === '/\\.module\\.less$/') { 35 | loader.exclude = /node_modules/; 36 | loader.test = /\.less$/; 37 | } 38 | if (typeof loader.test === 'function' && loader.test.toString().indexOf('\\.css$') > -1) { 39 | loader.include = /node_modules/; 40 | loader.test = /\.css$/; 41 | } 42 | if (loader.test.toString() === '/\\.module\\.css$/') { 43 | loader.exclude = /node_modules/; 44 | loader.test = /\.css$/; 45 | } 46 | }); 47 | 48 | webpackConfig.resolve.alias = { 49 | carno: __dirname + '/src/carno', 50 | components: __dirname + '/src/components', 51 | models: __dirname + '/src/models', 52 | pages: __dirname + '/src/pages', 53 | services: __dirname + '/src/services', 54 | utils: __dirname + '/src/utils', 55 | configs: __dirname + '/src/configs' 56 | } 57 | 58 | return webpackConfig; 59 | }; 60 | --------------------------------------------------------------------------------