Browse Source

xform request

傅行帆 5 years ago
parent
commit
2065afa5ab

+ 56
- 0
src/components/XForm/ImageUpload.jsx View File

@@ -0,0 +1,56 @@
1
+import React from 'react';
2
+import { Upload, Icon, message } from 'antd';
3
+import './style.less';
4
+
5
+class ImageUpload extends React.Component {
6
+  state = {
7
+    loading: false
8
+  };
9
+
10
+  handleChange = info => {
11
+    if (info.file.status === "uploading") {
12
+      this.setState({ loading: true });
13
+      return;
14
+    }
15
+
16
+    if (info.file.status === "done") {
17
+      const imageUrl = info.response.url
18
+
19
+      this.setState({
20
+        loading: false
21
+      })
22
+
23
+      this.props.onChange(imageUrl)
24
+    }
25
+  };
26
+
27
+  render () {
28
+    const uploadButton = (
29
+      <div>
30
+        <Icon style={{ fontSize: '2em', color: '#aaa' }} type={this.state.loading ? "loading" : "plus"} />        
31
+      </div>
32
+    );
33
+
34
+    const value = this.props.value;
35
+
36
+    return (
37
+      <Upload
38
+        name="avatar"
39
+        listType="picture-card"
40
+        className="avatar-uploader"
41
+        showUploadList={false}
42
+        action={this.props.action}
43
+        beforeUpload={this.props.beforeUpload}
44
+        onChange={this.handleChange}
45
+      >
46
+        {value ? (
47
+          <img src={value} alt="avatar" style={{ width: "100%" }} />
48
+        ) : (
49
+          uploadButton
50
+        )}
51
+      </Upload>
52
+    );
53
+  }
54
+}
55
+
56
+export default ImageUpload;

+ 31
- 0
src/components/XForm/README.md View File

@@ -0,0 +1,31 @@
1
+# XForm
2
+Antd Form 扩展
3
+
4
+## 使用说明
5
+
6
+```jsx
7
+import XForm, { FieldTypes } from '/path/to/XForm';
8
+
9
+const tplJSON = [
10
+  {},
11
+  {}
12
+]
13
+
14
+```
15
+
16
+通过 `json` 对象来实现自动构造 form 表单
17
+
18
+计划支持的属性
19
+
20
+[] label      显示的文字
21
+
22
+[] name       form name, 用于表单提交
23
+
24
+[] type       组件类型, 参见 FieldTypes
25
+
26
+[] rules      校验规则
27
+
28
+[] element    函数, 用于生成组件. (props) => <></> . 其中 props 包含 ...
29
+
30
+[] value      绑定的数据
31
+

+ 81
- 0
src/components/XForm/WrapperForm.jsx View File

@@ -0,0 +1,81 @@
1
+import React from 'react';
2
+import PropTypes from 'prop-types';
3
+import { Form, Button } from 'antd';
4
+import WrapperItem from './WrapperItem';
5
+
6
+const formItemLayout = {
7
+  labelCol: {
8
+    xs: { span: 24 },
9
+    sm: { span: 8 },
10
+  },
11
+  wrapperCol: {
12
+    xs: { span: 24 },
13
+    sm: { span: 16 },
14
+  },
15
+};
16
+
17
+class WrapperForm extends React.Component {
18
+  constructor(props) {
19
+    super(props);
20
+
21
+    this.state = {}
22
+  }
23
+
24
+  handleSubmit = e => {
25
+    e.preventDefault();
26
+    this.props.form.validateFieldsAndScroll((err, values) => {
27
+      if (err) {
28
+        if (typeof this.props.onError === 'function') {
29
+          typeof this.props.onError(err)
30
+        } else {
31
+          window.console.error(err)
32
+        }
33
+      } else {
34
+        if (typeof this.props.onSubmit === 'function') {
35
+          typeof this.props.onSubmit(values)
36
+        }
37
+      }
38
+    });
39
+  }
40
+
41
+  handleCancel = e => {
42
+    e.preventDefault();
43
+
44
+    if (typeof this.props.onCancel === 'function') {
45
+      typeof this.props.onCancel()
46
+    }
47
+  }
48
+
49
+  renderDefaultAction = (submitBtn, cancelBtn) => {
50
+    let FieldSubmit = null
51
+    let FieldCancel = null
52
+    if (submitBtn !== false) {
53
+      FieldSubmit = <Button htmlType="submit" type="primary">提交</Button>
54
+    }
55
+    if (cancelBtn !== false) {
56
+      FieldCancel = <Button htmlType="button" onClick={this.handleCancel} style={{ marginLeft: '48px' }}>取消</Button>
57
+    }
58
+
59
+    return FieldSubmit || FieldCancel ? <WrapperItem action render={<>{FieldSubmit}{FieldCancel}</>} /> : null
60
+  }
61
+
62
+  render () {
63
+    const {fields, form, children, submitBtn, cancelBtn, ...formProps} = this.props;
64
+
65
+    const FeildItems = (fields || []).map((props, inx) => (<WrapperItem key={inx} {...props} form={form} />))
66
+    
67
+    return (
68
+      <Form {...formItemLayout} {...formProps} onSubmit={this.handleSubmit}>
69
+        {FeildItems}
70
+        {this.renderDefaultAction(submitBtn, cancelBtn)}
71
+        {children}
72
+      </Form>
73
+    );
74
+  }
75
+}
76
+
77
+WrapperForm.propTypes = {
78
+  fields: PropTypes.array.isRequired
79
+}
80
+
81
+export default WrapperForm;

+ 141
- 0
src/components/XForm/WrapperItem.jsx View File

@@ -0,0 +1,141 @@
1
+import React from 'react';
2
+import {
3
+  Form,
4
+  Input,
5
+  InputNumber,
6
+  Checkbox,
7
+  Radio,
8
+  DatePicker,
9
+  TimePicker,
10
+  Select,
11
+} from 'antd';
12
+import ImageUploader from './ImageUpload';
13
+
14
+const Item = Form.Item
15
+const { Option } = Select;
16
+const { MonthPicker, RangePicker } = DatePicker
17
+
18
+const FieldTypes = {
19
+  Text: 'Text',
20
+  Password: 'Password',
21
+  Number: 'Number',
22
+  Switch: 'Switch',
23
+  TimePicker: 'TimePicker',
24
+  DatePicker: 'DatePicker',
25
+  RangePicker: 'RangePicker',
26
+  MonthPicker: 'MonthPicker',
27
+  Select: 'Select',
28
+  ImageUploader: 'ImageUploader',
29
+}
30
+
31
+const tailFormItemLayout = {
32
+  wrapperCol: {
33
+    xs: {
34
+      span: 24,
35
+      offset: 0,
36
+    },
37
+    sm: {
38
+      span: 16,
39
+      offset: 8,
40
+    },
41
+  },
42
+};
43
+
44
+const WrapperItem = (props) => {
45
+  const {
46
+    form,
47
+    label,
48
+    name,
49
+    type,
50
+    placeholder,
51
+    render,
52
+    rules,
53
+    value,
54
+    dict,
55
+    action,
56
+    props: fieldProps,
57
+    ...restProps
58
+  } = props;
59
+
60
+  const { getFieldDecorator } = form || {};
61
+
62
+  let config = {
63
+    rules,
64
+    initialValue: value
65
+  }
66
+
67
+  if (type === FieldTypes.Switch) {
68
+    config = {
69
+      ...config,
70
+      valuePropName: 'checked',
71
+    };
72
+  }
73
+  
74
+  if (type === FieldTypes.ImageUploader) {
75
+    config = {
76
+      ...config,
77
+      getValueFromEvent: x => x,
78
+    };
79
+  }
80
+
81
+  // 没有类型与组件, 生成隐藏字段
82
+  if (!type && !render) {
83
+    getFieldDecorator(name, config);
84
+    return <></>;
85
+  }
86
+
87
+  const SelectOpts = (dict || []).map((item, index) => (<Option value={item.value}>{item.name}</Option>))
88
+
89
+  let Field = <></>;
90
+  if (render) {
91
+    Field = typeof render === 'function' ? render(props) : render
92
+  } else {
93
+    switch (type) {
94
+      case FieldTypes.Text:
95
+        Field = <Input placeholder={placeholder} style={{ width: '100%' }} {...fieldProps}/>;
96
+        break;
97
+      case FieldTypes.Password:
98
+        Field = <Input.Password placeholder={placeholder} style={{ width: '100%' }} {...fieldProps}/>;
99
+        break;
100
+      case FieldTypes.Number:
101
+        Field = <InputNumber placeholder={placeholder} style={{ width: '100%' }} {...fieldProps}/>;
102
+        break;
103
+      case FieldTypes.Switch:
104
+        Field = <Switch {...fieldProps} />;
105
+        break;
106
+      case FieldTypes.TimePicker:
107
+        Field = <TimePicker {...fieldProps} style={{ width: '100%' }}/>;
108
+        break;
109
+      case FieldTypes.DatePicker:
110
+        Field = <DatePicker {...fieldProps} style={{ width: '100%' }}/>;
111
+        break;
112
+      case FieldTypes.RangePicker:
113
+        Field = <RangePicker {...fieldProps} style={{ width: '100%' }}/>;
114
+        break;
115
+      case FieldTypes.MonthPicker:
116
+        Field = <MonthPicker {...fieldProps} style={{ width: '100%' }}/>;
117
+        break;
118
+      case FieldTypes.Select:
119
+        Field = <Select placeholder={placeholder} style={{ width: '100%' }} {...fieldProps}>{SelectOpts}</Select>
120
+        break;
121
+      case FieldTypes.ImageUploader:
122
+        Field = <ImageUploader {...fieldProps}/>
123
+        break;
124
+      default:
125
+        throw new Error(`暂时不支持的组件类型: ${type}`)
126
+    }
127
+  }
128
+
129
+  if (!label && !name && !action) return Field;
130
+  
131
+  const itemProps = action ? { ...restProps, ...tailFormItemLayout } : restProps
132
+
133
+  return (
134
+    <Item label={label} {...itemProps}>
135
+      {action ? Field : getFieldDecorator(name, config)(Field)}
136
+    </Item>
137
+  )
138
+};
139
+
140
+export default WrapperItem;
141
+export { FieldTypes };

+ 7
- 0
src/components/XForm/index.jsx View File

@@ -0,0 +1,7 @@
1
+import React from 'react';
2
+import { Form } from 'antd';
3
+import WrapperForm from './WrapperForm';
4
+import { FieldTypes } from './WrapperItem';
5
+
6
+export default Form.create()(WrapperForm);
7
+export { FieldTypes };

+ 8
- 0
src/components/XForm/style.less View File

@@ -0,0 +1,8 @@
1
+:global {
2
+  .avatar-uploader {
3
+    & > .ant-upload {
4
+      width: 128px;
5
+      height: 128px;
6
+    }
7
+  }
8
+}

+ 22
- 24
src/pages/activity/ActivityList.jsx View File

@@ -1,10 +1,11 @@
1 1
 import React from 'react';
2
-import { Form, Input, Button, Icon, Select, message, Table, Divider, Tag, Pagination, Modal } from 'antd';
2
+import { Form, Input, Button, Icon, Select, message, Table, Divider, Tag, Pagination, Modal, DatePicker } from 'antd';
3 3
 import { FormattedMessage } from 'umi-plugin-react/locale';
4 4
 import styles from '../style/GoodsList.less';
5 5
 import router from 'umi/router';
6 6
 
7 7
 const { Option } = Select;
8
+const { MonthPicker, RangePicker, WeekPicker } = DatePicker;
8 9
 // 提交事件
9 10
 function handleSubmit(e, props) {
10 11
   e.preventDefault();
@@ -55,39 +56,39 @@ const dataSource = [
55 56
 
56 57
 const columns = [
57 58
   {
58
-    title: '商品图片',
59
+    title: '活动标题',
59 60
     dataIndex: 'img',
60 61
     key: 'img',
61 62
     align: 'center',
62 63
     render: (text, record) => <img src={record.img} className={styles.touxiang} />,
63 64
   },
64 65
   {
65
-    title: '商品名称',
66
+    title: '活动时间',
66 67
     dataIndex: 'name',
67 68
     key: 'name',
68 69
     align: 'center',
69 70
 
70 71
   },
71 72
   {
72
-    title: '所属积分',
73
+    title: '已参加人数',
73 74
     dataIndex: 'integral',
74 75
     key: 'integral',
75 76
     align: 'center',
76 77
   },
77 78
   {
78
-    title: '总数量',
79
+    title: '阅读量',
79 80
     dataIndex: 'total',
80 81
     key: 'total',
81 82
     align: 'center',
82 83
   },
83 84
   {
84
-    title: '已兑换数量',
85
+    title: '转发量',
85 86
     dataIndex: 'exchanged',
86 87
     key: 'exchanged',
87 88
     align: 'center',
88 89
   },
89 90
   {
90
-    title: '剩余数量',
91
+    title: '收藏数',
91 92
     dataIndex: 'rest',
92 93
     key: 'rest',
93 94
     align: 'center',
@@ -127,17 +128,9 @@ const header = (props) => {
127 128
 
128 129
     <>
129 130
       <Form layout="inline" onSubmit={e => handleSubmit(e, props)}>
130
-        <Form.Item>
131
-          {getFieldDecorator('name')(
132
-            <Input
133
-              prefix={<Icon type="text" style={{ color: 'rgba(0,0,0,.25)' }} />}
134
-              placeholder="商品名称"
135
-            />,
136
-          )}
137
-        </Form.Item>
138 131
         <Form.Item>
139 132
           {getFieldDecorator('goodState')(
140
-            <Select style={{ width: '180px' }} placeholder="状态" onChange={handleSelectChange}>
133
+            <Select style={{ width: '180px' }} placeholder="请选择城市" onChange={handleSelectChange}>
141 134
               <Option value="1">上架</Option>
142 135
               <Option value="0">下架</Option>
143 136
             </Select>,
@@ -145,26 +138,31 @@ const header = (props) => {
145 138
         </Form.Item>
146 139
         <Form.Item>
147 140
           {getFieldDecorator('isMain')(
148
-            <Select style={{ width: '180px' }} placeholder="所属项目" onChange={handleSelectChange}>
141
+            <Select style={{ width: '180px' }} placeholder="请选择项目" onChange={handleSelectChange}>
149 142
               <Option value="1">首页推荐</Option>
150 143
               <Option value="0">首页未推荐</Option>
151 144
             </Select>,
152 145
           )}
153 146
         </Form.Item>
154 147
         <Form.Item>
155
-          {getFieldDecorator('min')(
148
+          {getFieldDecorator('name')(
156 149
             <Input
157 150
               prefix={<Icon type="text" style={{ color: 'rgba(0,0,0,.25)' }} />}
158
-              placeholder="最小积分"
151
+              placeholder="请输入标题"
159 152
             />,
160 153
           )}
161 154
         </Form.Item>
162 155
         <Form.Item>
163
-          {getFieldDecorator('max')(
164
-            <Input
165
-              prefix={<Icon type="text" style={{ color: 'rgba(0,0,0,.25)' }} />}
166
-              placeholder="最大积分"
167
-            />,
156
+          {getFieldDecorator('goodState')(
157
+            <Select style={{ width: '180px' }} placeholder="是否报名" onChange={handleSelectChange}>
158
+              <Option value="1">上架</Option>
159
+              <Option value="0">下架</Option>
160
+            </Select>,
161
+          )}
162
+        </Form.Item>
163
+        <Form.Item>
164
+          {getFieldDecorator('min')(
165
+            <DatePicker />
168 166
           )}
169 167
         </Form.Item>
170 168
         <Form.Item>

+ 69
- 23
src/utils/request.js View File

@@ -2,7 +2,7 @@
2 2
  * request 网络请求工具
3 3
  * 更详细的 api 文档: https://github.com/umijs/umi-request
4 4
  */
5
-import { extend } from 'umi-request';
5
+import request from 'umi-request';
6 6
 import { notification } from 'antd';
7 7
 const codeMessage = {
8 8
   200: '服务器成功返回请求的数据。',
@@ -21,36 +21,82 @@ const codeMessage = {
21 21
   503: '服务不可用,服务器暂时过载或维护。',
22 22
   504: '网关超时。',
23 23
 };
24
-/**
25
- * 异常处理程序
26
- */
27 24
 
28
-const errorHandler = error => {
29
-  const { response } = error;
25
+const replaceURLParams = (url, params = {}) => {
26
+  return Object.keys(params).reduce((acc, k) => { // 此方法对每个元素进行处理
27
+    const re = new RegExp(`:${k}(?!w)`, 'i')
28
+    return acc.replace(re, args[k])
29
+  }, url)
30
+}
31
+
32
+request.interceptors.request.use((url, options) => {
33
+  const { urlData, headers = {}, logout = false, data, ...opts } = options
34
+  const apiURL = urlData ? replaceURLParams(url, urlData) : url
35
+  const token = window.localStorage.getItem('x-token')
36
+  const authHeader = token ? { Authorization: `Bearer ${token}` } : {}
37
+
38
+  if (logout) {
39
+    window.localStorage.removeItem('x-token')
40
+  }
30 41
 
42
+  return (
43
+    {
44
+      url: apiURL,
45
+      options: {
46
+        ...opts,
47
+        headers: {
48
+          ...authHeader,
49
+          ...headers,
50
+        },
51
+        data,
52
+        requestType: data instanceof FormData ? 'form' : 'json',
53
+        credentials: 'include', // 带 cookie
54
+        interceptors: true
55
+      },
56
+    }
57
+  );
58
+});
59
+
60
+request.interceptors.response.use(async (response, options) => {
61
+  
31 62
   if (response && response.status) {
32
-    const errorText = codeMessage[response.status] || response.statusText;
33
-    const { status, url } = response;
34
-    notification.error({
35
-      message: `请求错误 ${status}: ${url}`,
36
-      description: errorText,
37
-    });
63
+    if (response.status != 200) {
64
+      const errorText = codeMessage[response.status] || response.statusText;
65
+      const { status, url } = response;
66
+      notification.error({
67
+        message: `请求错误 ${status}: ${url}`,
68
+        description: errorText,
69
+      });
70
+      throw new Error(response.statusText);
71
+    } else {
72
+      const { code, data, message } = await response.clone().json();
73
+      if (code != 1000) {
74
+        notification.error({
75
+          message: `请求错误`,
76
+          description: message,
77
+        });
78
+        throw new Error(message);
79
+      }
80
+
81
+      if (data.token) {
82
+        window.localStorage.setItem('x-token', data.token)
83
+      }
84
+
85
+      return data;
86
+    }
38 87
   } else if (!response) {
39 88
     notification.error({
40 89
       description: '您的网络发生异常,无法连接服务器',
41 90
       message: '网络异常',
42 91
     });
43 92
   }
93
+});
44 94
 
45
-  return response;
95
+export default config => {
96
+  if (typeof config === 'string') {
97
+    return request(config);
98
+  } else {
99
+    const {url, ...options} = config;
100
+    return request(url, options);
101
+  }
46 102
 };
47
-/**
48
- * 配置request请求时的默认参数
49
- */
50
-
51
-const request = extend({
52
-  errorHandler,
53
-  // 默认错误处理
54
-  credentials: 'include', // 默认请求是否带上cookie
55
-});
56
-export default request;