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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------