Browse Source

first commit

张延森 6 years ago
commit
b3a4cfbcd8

+ 16
- 0
.editorconfig View File

@@ -0,0 +1,16 @@
1
+# http://editorconfig.org
2
+root = true
3
+
4
+[*]
5
+indent_style = space
6
+indent_size = 2
7
+end_of_line = lf
8
+charset = utf-8
9
+trim_trailing_whitespace = true
10
+insert_final_newline = true
11
+
12
+[*.md]
13
+trim_trailing_whitespace = false
14
+
15
+[Makefile]
16
+indent_style = tab

+ 2
- 0
.env View File

@@ -0,0 +1,2 @@
1
+BROWSER=none
2
+ESLINT=1

+ 3
- 0
.eslintrc View File

@@ -0,0 +1,3 @@
1
+{
2
+  "extends": "eslint-config-umi"
3
+}

+ 18
- 0
.gitignore View File

@@ -0,0 +1,18 @@
1
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2
+
3
+# dependencies
4
+/node_modules
5
+/npm-debug.log*
6
+/yarn-error.log
7
+/yarn.lock
8
+/package-lock.json
9
+
10
+# production
11
+/dist
12
+
13
+# misc
14
+.DS_Store
15
+
16
+# umi
17
+.umi
18
+.umi-production

+ 7
- 0
.prettierignore View File

@@ -0,0 +1,7 @@
1
+**/*.md
2
+**/*.svg
3
+**/*.ejs
4
+**/*.html
5
+package.json
6
+.umi
7
+.umi-production

+ 11
- 0
.prettierrc View File

@@ -0,0 +1,11 @@
1
+{
2
+  "singleQuote": true,
3
+  "trailingComma": "all",
4
+  "printWidth": 100,
5
+  "overrides": [
6
+    {
7
+      "files": ".prettierrc",
8
+      "options": { "parser": "json" }
9
+    }
10
+  ]
11
+}

+ 33
- 0
.umirc.js View File

@@ -0,0 +1,33 @@
1
+
2
+// ref: https://umijs.org/config/
3
+export default {
4
+  history: 'hash',
5
+  treeShaking: true,
6
+  proxy: {
7
+    "/api": {
8
+      "target": "http://127.0.0.1:8088/",
9
+      "changeOrigin": true,
10
+      // "pathRewrite": { "^/api" : "" }
11
+    }
12
+  },
13
+  plugins: [
14
+    // ref: https://umijs.org/plugin/umi-plugin-react.html
15
+    ['umi-plugin-react', {
16
+      antd: true,
17
+      dva: true,
18
+      dynamicImport: { webpackChunkName: true },
19
+      title: 'miniapp-service',
20
+      dll: false,
21
+      
22
+      routes: {
23
+        exclude: [
24
+          /models\//,
25
+          /services\//,
26
+          /model\.(t|j)sx?$/,
27
+          /service\.(t|j)sx?$/,
28
+          /components\//,
29
+        ],
30
+      },
31
+    }],
32
+  ],
33
+}

+ 0
- 0
mock/.gitkeep View File


+ 40
- 0
package.json View File

@@ -0,0 +1,40 @@
1
+{
2
+  "private": true,
3
+  "scripts": {
4
+    "start": "umi dev",
5
+    "build": "umi build",
6
+    "test": "umi test",
7
+    "lint": "eslint --ext .js src mock tests",
8
+    "precommit": "lint-staged"
9
+  },
10
+  "dependencies": {
11
+    "antd": "^3.15.0",
12
+    "dva": "^2.5.0-beta.2",
13
+    "md5": "^2.2.1",
14
+    "react": "^16.7.0",
15
+    "react-dom": "^16.7.0"
16
+  },
17
+  "devDependencies": {
18
+    "babel-eslint": "^9.0.0",
19
+    "eslint": "^5.4.0",
20
+    "eslint-config-umi": "^1.4.0",
21
+    "eslint-plugin-flowtype": "^2.50.0",
22
+    "eslint-plugin-import": "^2.14.0",
23
+    "eslint-plugin-jsx-a11y": "^5.1.1",
24
+    "eslint-plugin-react": "^7.11.1",
25
+    "husky": "^0.14.3",
26
+    "lint-staged": "^7.2.2",
27
+    "react-test-renderer": "^16.7.0",
28
+    "umi": "^2.6.3",
29
+    "umi-plugin-react": "^1.6.0"
30
+  },
31
+  "lint-staged": {
32
+    "*.{js,jsx}": [
33
+      "eslint --fix",
34
+      "git add"
35
+    ]
36
+  },
37
+  "engines": {
38
+    "node": ">=8.0.0"
39
+  }
40
+}

+ 9
- 0
src/app.js View File

@@ -0,0 +1,9 @@
1
+
2
+export const dva = {
3
+  config: {
4
+    onError(err) {
5
+      err.preventDefault();
6
+      console.error(err.message);
7
+    },
8
+  },
9
+};

+ 13
- 0
src/global.css View File

@@ -0,0 +1,13 @@
1
+
2
+html, body, #root {
3
+  height: 100%;
4
+}
5
+
6
+body {
7
+  margin: 0;
8
+}
9
+
10
+.main {
11
+  background: '#fff';
12
+  padding: 24;
13
+}

+ 14
- 0
src/layouts/__tests__/index.test.js View File

@@ -0,0 +1,14 @@
1
+import BasicLayout from '..';
2
+import renderer from 'react-test-renderer';
3
+
4
+describe('Layout: BasicLayout', () => {
5
+  it('Render correctly', () => {
6
+    const wrapper = renderer.create(<BasicLayout />);
7
+    expect(wrapper.root.children.length).toBe(1);
8
+    const outerLayer = wrapper.root.children[0];
9
+    expect(outerLayer.type).toBe('div');
10
+    const title = outerLayer.children[0];
11
+    expect(title.type).toBe('h1');
12
+    expect(title.children[0]).toBe('Yay! Welcome to umi!');
13
+  });
14
+});

+ 26
- 0
src/layouts/components/User.js View File

@@ -0,0 +1,26 @@
1
+import React from 'react';
2
+import { Menu, Dropdown, Icon } from 'antd';
3
+
4
+const User = (props) => {
5
+  const handleMenuClick = ({ key }) => {
6
+    if (key === 'logout') {
7
+      props.onLogout()
8
+    }
9
+  }
10
+
11
+  const menu = (
12
+    <Menu onClick={handleMenuClick}>
13
+      <Menu.Item key="logout">退 出</Menu.Item>
14
+    </Menu>
15
+  );
16
+
17
+  return (
18
+    <Dropdown overlay={menu}>
19
+      <div style={{ color: '#fff' }}>
20
+        <span>{props.profile.userName} <Icon type="down" /></span>
21
+      </div>
22
+    </Dropdown>
23
+  );
24
+}
25
+
26
+export default User;

+ 51
- 0
src/layouts/index.js View File

@@ -0,0 +1,51 @@
1
+import { Layout, Row, Col } from 'antd';
2
+import { connect } from 'dva';
3
+import withRouter from 'umi/withRouter';
4
+import router from 'umi/router';
5
+
6
+import User from './components/User';
7
+import styles from './index.less'
8
+import { getToken } from '../utils/token';
9
+
10
+const { Header, Footer, Content } = Layout;
11
+
12
+const BasicLayout = (props) => {
13
+  const token = getToken()
14
+  if (!token && props.location.pathname != '/login') {
15
+    router.push('/login')
16
+    return (<></>)
17
+  }
18
+
19
+  if (props.location.pathname === '/login') {
20
+    return (<>{ props.children }</>)
21
+  }
22
+
23
+  const tkUser = JSON.parse(token)
24
+
25
+  const handleUserLogout = () => {
26
+    props.dispatch({ type: 'app/logout' })
27
+    router.push('/login')
28
+  }
29
+
30
+  return (
31
+    <Layout className={styles.layout}>
32
+        <Header className={styles.header}>
33
+          <Row>
34
+            <Col span={20}>
35
+              <div className="logo" />
36
+              <h2>小程序微服务配置中心</h2>
37
+            </Col>
38
+            <Col span={4} style={{ textAlign: "right" }}>
39
+              <User profile={props.user || tkUser} onLogout={handleUserLogout} />
40
+            </Col>
41
+          </Row>
42
+        </Header>
43
+        <Content className={styles.content}>
44
+          { props.children }
45
+        </Content>
46
+        <Footer className={styles.footer}>©2019 Created by HuiFang</Footer>
47
+      </Layout>
48
+  )
49
+};
50
+
51
+export default withRouter(connect(s => ({ user: s.app.user }))(BasicLayout));

+ 18
- 0
src/layouts/index.less View File

@@ -0,0 +1,18 @@
1
+.layout {
2
+  min-height: 100%;
3
+
4
+  .header {
5
+    h2 {
6
+      color: #fff;
7
+    }
8
+  }
9
+
10
+  .content {
11
+    padding: 50px;
12
+    // overflow: 'initial';
13
+  }
14
+
15
+  .footer {
16
+    text-align: center;
17
+  }
18
+}

+ 0
- 0
src/models/.gitkeep View File


+ 63
- 0
src/models/app.js View File

@@ -0,0 +1,63 @@
1
+import md5 from 'md5';
2
+import request from '../utils/request';
3
+import { setToken, clearToken } from '../utils/token';
4
+
5
+export default {
6
+  namespace: 'app',
7
+  state: {
8
+    user: '',
9
+  },
10
+  reducers: {
11
+    syncState (state, { payload = {} }) {
12
+      return {
13
+        ...state,
14
+        ...payload,
15
+      }
16
+    }
17
+  },
18
+  effects: {
19
+    *login({ payload }, { call, put }) {
20
+      try {
21
+        const api = {
22
+          url: '/api/admin/login',
23
+          method: 'POST',
24
+          data: {
25
+            ...payload,
26
+            password: md5(payload.password),
27
+          }
28
+        }
29
+
30
+        const user = yield call(request, api);
31
+        yield put({
32
+          type: 'syncState',
33
+          payload: { user },
34
+        })
35
+
36
+        setToken(JSON.stringify(user))
37
+
38
+        return Promise.resolve(user)
39
+      } catch (error) {
40
+        return Promise.reject(error)
41
+      }
42
+    },
43
+
44
+    *logout(_, { call, put }) {
45
+
46
+      try {
47
+        const api = {
48
+          url: '/api/admin/logout',
49
+          method: 'POST'
50
+        }
51
+        yield call(request, api);
52
+        yield put({
53
+          type: 'syncState',
54
+          payload: { user: '' },
55
+        })
56
+      } catch (err) {
57
+        console.log(err)
58
+      }
59
+
60
+      clearToken()
61
+    }
62
+  }
63
+}

+ 14
- 0
src/pages/__tests__/index.test.js View File

@@ -0,0 +1,14 @@
1
+import Index from '..';
2
+import renderer from 'react-test-renderer';
3
+
4
+
5
+describe('Page: index', () => {
6
+  it('Render correctly', () => {
7
+    const wrapper = renderer.create(<Index />);
8
+    expect(wrapper.root.children.length).toBe(1);
9
+    const outerLayer = wrapper.root.children[0];
10
+    expect(outerLayer.type).toBe('div');
11
+    expect(outerLayer.children.length).toBe(2);
12
+    
13
+  });
14
+});

+ 9
- 0
src/pages/components/IconText.js View File

@@ -0,0 +1,9 @@
1
+
2
+import { Icon } from 'antd';
3
+
4
+export default ({ type, text }) => (
5
+  <span>
6
+      <Icon type={type} style={{ marginRight: 8 }} />
7
+      {text}
8
+    </span>
9
+)

+ 131
- 0
src/pages/components/MiniAppEdit.js View File

@@ -0,0 +1,131 @@
1
+import React, { useState } from 'react';
2
+import { Form, Input, Select, Modal, notification } from 'antd';
3
+
4
+import request from '../../utils/request';
5
+
6
+const formItemLayout = {
7
+  labelCol: {
8
+    xs: { span: 24 },
9
+    sm: { span: 4 },
10
+  },
11
+  wrapperCol: {
12
+    xs: { span: 24 },
13
+    sm: { span: 18 },
14
+  },
15
+};
16
+
17
+const MiniAppForm = (props) => {
18
+  const { data = {}, visible } = props || {};
19
+  const { getFieldDecorator } = props.form;
20
+  const [ confirmLoading, setConfirmLoading ] = useState(false);
21
+  
22
+  const handleSubmit = (e) => {
23
+    // e.preventDefault();
24
+    props.form.validateFields((err, values) => {
25
+      if (err) return;
26
+
27
+      setConfirmLoading(true);
28
+
29
+      const appidForURL = data.appid ? `/${data.appid}` : '';
30
+
31
+      request({
32
+        url: `/api/admin/miniapp${appidForURL}`,
33
+        method: data.appid ? 'PUT' : 'POST',
34
+        data: values,
35
+      }).then((data) => {
36
+        setConfirmLoading(false);
37
+        props.onSuccess(data || values)
38
+      }).catch((err) => {
39
+        setConfirmLoading(false);
40
+        notification.error({
41
+          message: '编辑信息错误',
42
+          description: err.message,
43
+        })
44
+      })
45
+    });
46
+  }
47
+
48
+  const handleCancel = () => {
49
+    setConfirmLoading(false);
50
+    props.onCancel()
51
+  }
52
+  
53
+  return (
54
+    <Modal
55
+      title="小程序信息维护"
56
+      visible={visible}
57
+      confirmLoading={confirmLoading}
58
+      onOk={handleSubmit}
59
+      onCancel={handleCancel}
60
+    >
61
+      <Form {...formItemLayout} onSubmit={e => e.preventDefault()}>
62
+        <Form.Item label="AppID">
63
+          {getFieldDecorator('appid', {
64
+            initialValue: data.appid,
65
+            rules: [
66
+              {
67
+                required: true,
68
+                message: '请输入 小程序 AppID',
69
+              },
70
+            ],
71
+          })(<Input disabled={data.appid} />)}
72
+        </Form.Item>
73
+        <Form.Item label="名称">
74
+          {getFieldDecorator('appname', {
75
+            initialValue: data.appname,
76
+            rules: [
77
+              {
78
+                required: true,
79
+                message: '请输入 小程序 名称',
80
+              },
81
+            ],
82
+          })(<Input />)}
83
+        </Form.Item>
84
+        <Form.Item label="Secret">
85
+          {getFieldDecorator('secret', {
86
+            initialValue: data.secret,
87
+            rules: [
88
+              {
89
+                required: true,
90
+                message: '请输入 小程序 Secret',
91
+              },
92
+            ],
93
+          })(<Input />)}
94
+        </Form.Item>
95
+        <Form.Item label="Token">
96
+          {getFieldDecorator('token', {
97
+            initialValue: data.token,
98
+          })(<Input />)}
99
+        </Form.Item>
100
+        <Form.Item label="AesKey">
101
+          {getFieldDecorator('aesKey', {
102
+            initialValue: data.aesKey,
103
+          })(<Input />)}
104
+        </Form.Item>
105
+        <Form.Item label="消息格式">
106
+          {getFieldDecorator('msgDataFormat', {
107
+            initialValue: data.msgDataFormat || 'json',
108
+          })(
109
+            <Select placeholder="请选择消息格式">
110
+              <Select.Option value="json">JSON</Select.Option>
111
+              <Select.Option value="xml">XML</Select.Option>
112
+            </Select>
113
+          )}
114
+        </Form.Item>
115
+        <Form.Item label="回调地址">
116
+          {getFieldDecorator('callbackUrl', {
117
+            initialValue: data.callbackUrl,
118
+            rules: [
119
+              {
120
+                required: true,
121
+                message: '请输入 回调地址',
122
+              },
123
+            ],
124
+          })(<Input />)}
125
+        </Form.Item>
126
+      </Form>
127
+    </Modal>
128
+  );
129
+}
130
+
131
+export default Form.create()(MiniAppForm);

+ 69
- 0
src/pages/components/MiniAppList.js View File

@@ -0,0 +1,69 @@
1
+import { Card, List, Row, Col } from 'antd';
2
+
3
+const cardStyle = {
4
+  width: '45%',
5
+  minWidth: 400,
6
+  display: 'inline-block',
7
+  margin: '0 24px 24px 0',
8
+}
9
+
10
+const MiniAppList = (props) => {
11
+  const handleEdit = (item) => () => {
12
+    props.onEdit(item)
13
+  }
14
+
15
+  return (
16
+    <>
17
+      {
18
+        props.data.map((item) => (
19
+          <Card key={item.appid} style={cardStyle} hoverable>
20
+            <Row>
21
+              <Col span={20}><h2>{item.appname}</h2></Col>
22
+              <Col span={4} style={{ textAlign: 'right' }}><a onClick={handleEdit(item)}>修改</a></Col>
23
+            </Row>
24
+            <List size="large">
25
+              <List.Item>
26
+                <Row style={{ width: '100%' }}>
27
+                  <Col span={4}>AppID </Col>
28
+                  <Col span={18}>{item.appid}</Col>
29
+                </Row>
30
+              </List.Item>
31
+              <List.Item>
32
+                <Row style={{ width: '100%' }}>
33
+                  <Col span={4}>Secret </Col>
34
+                  <Col span={18}>{item.secret}</Col>
35
+                </Row>
36
+              </List.Item>
37
+              <List.Item>
38
+                <Row style={{ width: '100%' }}>
39
+                  <Col span={4}>Token </Col>
40
+                  <Col span={18}>{item.token}</Col>
41
+                </Row>
42
+              </List.Item>
43
+              <List.Item>
44
+                <Row style={{ width: '100%' }}>
45
+                  <Col span={4}>AesKey </Col>
46
+                  <Col span={18}>{item.aesKey}</Col>
47
+                </Row>
48
+              </List.Item>
49
+              <List.Item>
50
+                <Row style={{ width: '100%' }}>
51
+                  <Col span={4}>消息格式 </Col>
52
+                  <Col span={18}>{item.msgDataFormat}</Col>
53
+                </Row>
54
+              </List.Item>
55
+              <List.Item>
56
+                <Row style={{ width: '100%' }}>
57
+                  <Col span={4}>回调地址 </Col>
58
+                  <Col span={18}>{item.callbackUrl}</Col>
59
+                </Row>
60
+              </List.Item>
61
+            </List>
62
+          </Card>
63
+        ))
64
+      }
65
+    </>
66
+  );
67
+}
68
+
69
+export default MiniAppList;

+ 95
- 0
src/pages/index.js View File

@@ -0,0 +1,95 @@
1
+import React, { Component } from 'react';
2
+import { Button, Icon, notification } from 'antd';
3
+
4
+import MiniAppList from './components/MiniAppList';
5
+import MiniAppEdit from './components/MiniAppEdit';
6
+import request from '../utils/request';
7
+
8
+class Index extends Component {
9
+  constructor(props) {
10
+    super(props)
11
+
12
+    this.state = {
13
+      list: [],
14
+      detail: {},
15
+      showMiniappForm: false,
16
+    }
17
+  }
18
+
19
+  getData = (params) => {    
20
+    request({
21
+      url: '/api/admin/miniapp',
22
+      params,
23
+    }).then((data) => {
24
+      const { records } = data
25
+      this.setState({ list: records })
26
+    }).catch((err) => {
27
+      notification.error({
28
+        message: '查询失败',
29
+        description: err.message,
30
+      })
31
+    })
32
+  }
33
+
34
+  handleCreate = () => {
35
+    this.setState({
36
+      detail: {},
37
+      showMiniappForm: true,
38
+    })
39
+  }
40
+
41
+  handleEdit = (item) => {
42
+    this.setState({
43
+      detail: item,
44
+      showMiniappForm: true,
45
+    })
46
+  }
47
+
48
+  handleMiniappFormSuccess = (item) => {
49
+    this.setState({
50
+      detail: {},
51
+      showMiniappForm: false,
52
+    })
53
+
54
+    if (item) {
55
+      const list = this.state.list.map((x) => (x.appid === item.appid ? item : x))
56
+      this.setState({list})
57
+    }
58
+  }
59
+
60
+  handleMiniappFormCancel = () => {
61
+    this.setState({
62
+      detail: {},
63
+      showMiniappForm: false,
64
+    })
65
+  }
66
+
67
+  componentDidMount() {
68
+    const page = this.props.location.query.page || 1;
69
+    this.getData({page})
70
+  }
71
+
72
+  render() {
73
+    return (
74
+      <div>
75
+        <div style={{ marginBottom: '24px' }}>
76
+          <Button size="large" onClick={this.handleCreate}><Icon type="plus" />新增</Button>
77
+        </div>
78
+
79
+        <MiniAppList
80
+          data={this.state.list}
81
+          onEdit={this.handleEdit}
82
+        />
83
+
84
+        <MiniAppEdit
85
+          data={this.state.detail}
86
+          visible={this.state.showMiniappForm}
87
+          onSuccess={this.handleMiniappFormSuccess}
88
+          onCancel={this.handleMiniappFormCancel}
89
+        />
90
+      </div>
91
+    );
92
+  }
93
+}
94
+
95
+export default Index;

+ 69
- 0
src/pages/login/index.js View File

@@ -0,0 +1,69 @@
1
+import React, { useState } from 'react';
2
+import { Form, Icon, Button, Input, notification } from 'antd';
3
+import { connect } from 'dva';
4
+import router from 'umi/router';
5
+
6
+import styles from './style.less';
7
+import request from '../../utils/request';
8
+import { setToken } from '../../utils/token';
9
+
10
+const LogIn = (props) => {
11
+  const [ loginData, setData ] =  useState({ user: '', password: '' });
12
+
13
+  const handleSubmit = (e) => {
14
+    e.preventDefault();
15
+
16
+    props.dispatch({
17
+      type: 'app/login',
18
+      payload: loginData,
19
+    }).then(() => {
20
+      const from = props.location.query.from
21
+      if (from) {
22
+        router.push(decodeURIComponent(from))
23
+      } else {
24
+        router.push(decodeURIComponent('/'))
25
+      }
26
+    }).catch((err) => {
27
+      notification.error({
28
+        message: '登录错误',
29
+        description: err.message,
30
+      })
31
+    })
32
+  }
33
+
34
+  const syncData = (key) => (e) => {
35
+    setData({ ...loginData, [`${key}`]: e.target.value })
36
+  }
37
+
38
+  return (
39
+    <div className={styles.login}>
40
+      <div className={styles.logo}>
41
+        <h2>小程序微服务配置中心</h2>
42
+      </div>
43
+      <Form onSubmit={handleSubmit}>
44
+        <Form.Item>
45
+          <Input
46
+            size="large"
47
+            prefix={<Icon type="user" style={{ color: 'rgba(0,0,0,.25)' }} />}
48
+            placeholder="用户名"
49
+            onChange={syncData('user')}
50
+          />
51
+        </Form.Item>
52
+        <Form.Item>
53
+          <Input
54
+            size="large"
55
+            prefix={<Icon type="lock" style={{ color: 'rgba(0,0,0,.25)' }} />}
56
+            type="password"
57
+            placeholder="密码"
58
+            onChange={syncData('password')}
59
+          />
60
+        </Form.Item>
61
+        <Form.Item>
62
+          <Button size="large" type="primary" htmlType="submit">登 录</Button>
63
+        </Form.Item>
64
+      </Form>
65
+    </div>
66
+  );
67
+}
68
+
69
+export default connect()(LogIn);

+ 22
- 0
src/pages/login/style.less View File

@@ -0,0 +1,22 @@
1
+.login {
2
+  background: #f0f2f5;
3
+  background-image: url(https://gw.alipayobjects.com/zos/rmsportal/TVYTbAXWheQpRcWDaDMu.svg);
4
+  width: 100%;
5
+  height: 100%;
6
+
7
+  .logo {
8
+    width: 100%;
9
+    height: 200px;
10
+    text-align: center;
11
+    padding-top: 100px;
12
+  }
13
+
14
+  form {
15
+    width: 400px;
16
+    margin: 0 auto;
17
+
18
+    button {
19
+      width: 100%;
20
+    }
21
+  }
22
+}

+ 85
- 0
src/utils/request.js View File

@@ -0,0 +1,85 @@
1
+import fetch from 'dva/fetch';
2
+
3
+function checkStatus(response) {
4
+  if (response.status >= 200 && response.status < 300) {
5
+    return response;
6
+  }
7
+
8
+  const error = new Error(response.statusText);
9
+  error.response = response;
10
+  throw error;
11
+}
12
+
13
+function checkResult ({ code, message, data }) {
14
+  if (code === 0) {
15
+    return data;
16
+  }
17
+
18
+  throw new Error(message);
19
+}
20
+
21
+function transformOpt(options) {
22
+  const { url, data, body, params, headers = {}, ...others } = options || {};
23
+
24
+  let config = {
25
+    url,
26
+    credentials: 'include',
27
+    ...others
28
+  }
29
+
30
+  if (body) {
31
+    // body 发送 multipart 数据
32
+    config.body = Object.keys(data).reduce((form, k) => {
33
+      if (Array.isArray(data[k])) {
34
+        data[k].forEach((v) => form.append(k, v))
35
+      } else {
36
+        form.append(k, data[k])
37
+      }
38
+
39
+      return form
40
+    }, new FormData)
41
+
42
+    config.headers = {
43
+      ...headers,
44
+      'content-type': 'multipart/form-data',
45
+    }
46
+  } else if (data) {
47
+    // body 发送 json 数据
48
+    config.body = JSON.stringify(data)
49
+
50
+    config.headers = {
51
+      ...headers,
52
+      'content-type': 'application/json',
53
+    }
54
+  }
55
+
56
+  if (params) {
57
+    // 仅支持 param 是对象
58
+    // 且不支持数组元素
59
+    const searchStr = Object.keys(params).map((k) => `${k}=${params[k]}`).join('&')
60
+
61
+    config.url = url.indexOf('?') > 0 ? `${url}&${searchStr}` : `${url}?${searchStr}`
62
+  }
63
+
64
+  return config;
65
+}
66
+
67
+/**
68
+ * Requests a URL, returning a promise.
69
+ *
70
+ * @param  {object} [options] The options we want to pass to "fetch"
71
+ * @return {object}           An object containing either "data" or "err"
72
+ */
73
+async function request(options) {
74
+  const { url, ...config } = transformOpt(options);
75
+
76
+  const response = await fetch(url, config); // eslint-disable-line
77
+
78
+  checkStatus(response);
79
+
80
+  const data = await response.json();
81
+
82
+  return checkResult(data);
83
+}
84
+
85
+export default request;

+ 12
- 0
src/utils/token.js View File

@@ -0,0 +1,12 @@
1
+
2
+export function getToken() {
3
+  return localStorage.getItem('miniapp-token')
4
+}
5
+
6
+export function setToken(tk) {
7
+  localStorage.setItem('miniapp-token', tk)
8
+}
9
+
10
+export function clearToken() {
11
+  localStorage.removeItem('miniapp-token')
12
+}

+ 12
- 0
webpack.config.js View File

@@ -0,0 +1,12 @@
1
+/**
2
+ * 不是真实的 webpack 配置,仅为兼容 webstorm 和 intellij idea 代码跳转
3
+ * ref: https://github.com/umijs/umi/issues/1109#issuecomment-423380125
4
+ */
5
+
6
+module.exports = {
7
+  resolve: {
8
+    alias: {
9
+      '@': require('path').resolve(__dirname, 'src'),
10
+    },
11
+  },
12
+};