张延森 4 years ago
parent
commit
7117b6ec0d

+ 1
- 0
.npmrc View File

@@ -0,0 +1 @@
1
+registry=https://registry.npm.taobao.org

+ 4
- 1
package.json View File

@@ -14,13 +14,16 @@
14 14
     "test:ci": "npm run lint && npm run test:unit"
15 15
   },
16 16
   "dependencies": {
17
+    "@toast-ui/vue-editor": "^2.3.1",
17 18
     "axios": "0.18.1",
18
-    "core-js": "3.6.5",
19
+    "codemirror": "^5.56.0",
20
+    "core-js": "^3.6.5",
19 21
     "element-ui": "2.13.2",
20 22
     "md5": "^2.3.0",
21 23
     "normalize.css": "7.0.0",
22 24
     "nprogress": "0.2.0",
23 25
     "path-to-regexp": "2.4.0",
26
+    "tui-editor": "^1.4.10",
24 27
     "vue": "2.6.10",
25 28
     "vue-router": "3.0.6",
26 29
     "vuex": "3.1.0"

+ 15
- 0
src/api/comm.js View File

@@ -0,0 +1,15 @@
1
+import request from '@/utils/request'
2
+
3
+export function upload(file) {
4
+  const data = new FormData()
5
+  data.set('file', file)
6
+
7
+  return request({
8
+    url: '/api/admin/upload',
9
+    method: 'post',
10
+    headers: {
11
+      'Content-Type': 'multipart/form-data'
12
+    },
13
+    data
14
+  })
15
+}

+ 28
- 0
src/components/MarkdownEditor/default-options.js View File

@@ -0,0 +1,28 @@
1
+// doc: https://nhnent.github.io/tui.editor/api/latest/ToastUIEditor.html#ToastUIEditor
2
+export default {
3
+  minHeight: '200px',
4
+  previewStyle: 'vertical',
5
+  useCommandShortcut: true,
6
+  useDefaultHTMLSanitizer: true,
7
+  usageStatistics: false,
8
+  hideModeSwitch: false,
9
+  toolbarItems: [
10
+    'heading',
11
+    'bold',
12
+    'italic',
13
+    'strike',
14
+    'divider',
15
+    'hr',
16
+    'quote',
17
+    'divider',
18
+    'ul',
19
+    'ol',
20
+    'task',
21
+    'indent',
22
+    'outdent',
23
+    'divider',
24
+    'table',
25
+    'image',
26
+    'divider'
27
+  ]
28
+}

+ 95
- 0
src/components/MarkdownEditor/index.vue View File

@@ -0,0 +1,95 @@
1
+<template>
2
+  <editor
3
+    :initialValue="value"
4
+    :initialEditType="mode"
5
+    :options="options"
6
+    :height="height"
7
+    previewStyle="vertical"
8
+    ref="toastuiEditor"
9
+    @change="handleEditorChange"
10
+    />
11
+</template>
12
+
13
+<script>
14
+// deps for editor
15
+import 'codemirror/lib/codemirror.css'
16
+import '@toast-ui/editor/dist/toastui-editor.css'
17
+
18
+import { Editor } from '@toast-ui/vue-editor'
19
+import { upload } from '@/api/comm'
20
+import defaultOptions from './default-options'
21
+
22
+export default {
23
+  name: 'MarkdownEditor',
24
+  components: {
25
+    editor: Editor
26
+  },
27
+  props: {
28
+    value: {
29
+      type: String,
30
+      default: ''
31
+    },
32
+    options: {
33
+      type: Object,
34
+      default() {
35
+        return defaultOptions
36
+      }
37
+    },
38
+    mode: {
39
+      type: String,
40
+      default: 'markdown'
41
+    },
42
+    height: {
43
+      type: String,
44
+      required: false,
45
+      default: '480px'
46
+    },
47
+    language: {
48
+      type: String,
49
+      required: false,
50
+      default: 'en_US' // https://github.com/nhnent/tui.editor/tree/master/src/js/langs
51
+    }
52
+  },
53
+  data() {
54
+    return {
55
+    }
56
+  },
57
+  watch: {
58
+    value(newValue, preValue) {
59
+      if (newValue !== preValue && newValue !== this.getValue()) {
60
+        this.setValue(newValue)
61
+      }
62
+    }
63
+  },
64
+  mounted() {
65
+    this.$nextTick(() => {
66
+      this.$refs.toastuiEditor.invoke('addHook', 'addImageBlobHook', this.uploadImage)
67
+    })
68
+  },
69
+  destroyed() {
70
+  },
71
+  methods: {
72
+    handleEditorChange() {
73
+      this.$emit('input', this.getValue())
74
+    },
75
+    setValue(value) {
76
+      this.$refs.toastuiEditor.invoke('setMarkdown', value)
77
+    },
78
+    getValue() {
79
+      return this.$refs.toastuiEditor.invoke('getMarkdown')
80
+    },
81
+    uploadImage(file, callback) {
82
+      upload(file).then(res => {
83
+        callback(res.data.url)
84
+      })
85
+    }
86
+  }
87
+}
88
+</script>
89
+
90
+<style>
91
+/* 修复 css bug */
92
+.te-mode-switch {
93
+  display: flex;
94
+}
95
+</style>

+ 94
- 10
src/router/index.js View File

@@ -56,23 +56,107 @@ export const constantRoutes = [
56 56
   },
57 57
 
58 58
   {
59
-    path: '/example',
59
+    path: '/life',
60 60
     component: Layout,
61
-    redirect: '/example/table',
62
-    name: 'Example',
63
-    meta: { title: 'Example', icon: 'el-icon-s-help' },
61
+    redirect: '/life/warm',
62
+    name: 'life',
63
+    meta: { title: '荟生活', icon: 'el-icon-camera-solid' },
64 64
     children: [
65 65
       {
66
-        path: 'table',
67
-        name: 'Table',
66
+        path: 'warm',
67
+        name: 'warm',
68
+        component: () => import('@/views/life/Warm'),
69
+        meta: { title: '暖场活动', icon: 'el-icon-position' }
70
+      },
71
+      {
72
+        path: 'travel',
73
+        name: 'travel',
74
+        component: () => import('@/views/tree/index'),
75
+        meta: { title: '美好出游季', icon: 'el-icon-bicycle' }
76
+      },
77
+      {
78
+        path: 'school',
79
+        name: 'school',
80
+        component: () => import('@/views/tree/index'),
81
+        meta: { title: '六点半学堂', icon: 'el-icon-school' }
82
+      },
83
+      {
84
+        path: 'sport',
85
+        name: 'sport',
86
+        component: () => import('@/views/tree/index'),
87
+        meta: { title: '夜跑行动', icon: 'el-icon-moon' }
88
+      },
89
+      {
90
+        path: 'warm-history',
91
+        name: 'warm-history',
92
+        component: () => import('@/views/tree/index'),
93
+        meta: { title: '暖场活动回顾', icon: 'tree' }
94
+      }
95
+    ]
96
+  },
97
+
98
+  {
99
+    path: '/rights',
100
+    component: Layout,
101
+    redirect: '/rights/seasons',
102
+    name: 'rights',
103
+    meta: { title: '荟权益', icon: 'el-icon-s-check' },
104
+    children: [
105
+      {
106
+        path: 'seasons',
107
+        name: 'seasons',
108
+        component: () => import('@/views/table/index'),
109
+        meta: { title: '惊喜四季节', icon: 'el-icon-grape' }
110
+      },
111
+      {
112
+        path: 'letter',
113
+        name: 'letter',
114
+        component: () => import('@/views/tree/index'),
115
+        meta: { title: '丽园家书', icon: 'el-icon-notebook-2' }
116
+      }
117
+    ]
118
+  },
119
+
120
+  {
121
+    path: '/love',
122
+    component: Layout,
123
+    redirect: '/love/clothes',
124
+    name: 'love',
125
+    meta: { title: '荟爱心', icon: 'el-icon-s-help' },
126
+    children: [
127
+      {
128
+        path: 'clothes',
129
+        name: 'clothes',
130
+        component: () => import('@/views/table/index'),
131
+        meta: { title: '旧衣飞行', icon: 'el-icon-aim' }
132
+      },
133
+      {
134
+        path: 'help',
135
+        name: 'help',
136
+        component: () => import('@/views/tree/index'),
137
+        meta: { title: '丽园助农', icon: 'el-icon-bangzhu' }
138
+      }
139
+    ]
140
+  },
141
+
142
+  {
143
+    path: '/customer',
144
+    component: Layout,
145
+    redirect: '/customer/list',
146
+    name: 'customer',
147
+    meta: { title: '会员管理', icon: 'el-icon-s-custom' },
148
+    children: [
149
+      {
150
+        path: 'list',
151
+        name: 'list',
68 152
         component: () => import('@/views/table/index'),
69
-        meta: { title: 'Table', icon: 'table' }
153
+        meta: { title: '信息统计', icon: 'el-icon-data-analysis' }
70 154
       },
71 155
       {
72
-        path: 'tree',
73
-        name: 'Tree',
156
+        path: 'recommender',
157
+        name: 'recommender',
74 158
         component: () => import('@/views/tree/index'),
75
-        meta: { title: 'Tree', icon: 'tree' }
159
+        meta: { title: '推荐记录', icon: 'el-icon-notebook-1' }
76 160
       }
77 161
     ]
78 162
   },

+ 11
- 0
src/utils/uploadOption.js View File

@@ -0,0 +1,11 @@
1
+import { getToken } from "./auth";
2
+
3
+const options = {
4
+  action: '/api/admin/upload',
5
+  headers: {
6
+    'x-authorization-jwt': getToken()
7
+  },
8
+  name: 'file'
9
+}
10
+
11
+export default options;

+ 258
- 0
src/views/components/Activity/index.vue View File

@@ -0,0 +1,258 @@
1
+<template>
2
+  <el-form ref="activityForm" :model="form" :rules="rules" label-width="120px">
3
+    <el-form-item label="活动名称" prop="name">
4
+      <el-input v-model="form.name" />
5
+    </el-form-item>
6
+    <el-form-item label="活动时间" required="">
7
+      <el-date-picker
8
+        v-model="dtRange1"
9
+        type="daterange"
10
+        start-placeholder="开始日期"
11
+        end-placeholder="结束日期"
12
+        style="width: 100%;" />
13
+    </el-form-item>
14
+    <el-form-item label="活动封面" prop="thumb">
15
+      <el-upload
16
+        v-bind="uploadOption"
17
+        class="activty-uploader"
18
+        :show-file-list="false"
19
+        :on-success="handleUploadSuccess"
20
+        :before-upload="beforeUpload">
21
+        <img v-if="form.thumb" :src="form.thumb" class="activty-thumb">
22
+        <i v-else class="el-icon-plus avatar-uploader-icon"></i>
23
+      </el-upload>
24
+    </el-form-item>
25
+    <el-form-item label="活动地点" prop="address">
26
+      <el-input v-model="form.address" />
27
+    </el-form-item>
28
+    <template v-if="enroll">
29
+      <el-form-item label="报名时间">
30
+        <el-date-picker
31
+          v-model="dtRange2"
32
+          type="daterange"
33
+          start-placeholder="开始日期"
34
+          end-placeholder="结束日期"
35
+          style="width: 100%;" />
36
+      </el-form-item>
37
+    </template>
38
+    <template v-if="type == 2">
39
+      <el-form-item label="是否投票" prop="is_vote">
40
+        <el-switch v-model="form.is_vote" />
41
+      </el-form-item>
42
+      <el-form-item label="投票时间">
43
+        <el-date-picker
44
+          v-model="dtRange3"
45
+          :disabled="!form.is_vote"
46
+          type="daterange"
47
+          start-placeholder="开始日期"
48
+          end-placeholder="结束日期"
49
+          style="width: 100%;" />
50
+      </el-form-item>
51
+    </template>
52
+    <el-form-item label="活动详情" prop="detail">
53
+      <markdown v-model="form.detail" />
54
+    </el-form-item>
55
+    <el-form-item label="分享标题" prop="shareTitle">
56
+      <el-input v-model="form.shareTitle" />
57
+    </el-form-item>
58
+    <el-form-item label="分享封面" prop="shareImg">
59
+      <el-upload
60
+        v-bind="uploadOption"
61
+        class="activty-uploader"
62
+        action=""
63
+        :show-file-list="false"
64
+        :on-success="handleUploadSuccess"
65
+        :before-upload="beforeUpload">
66
+        <img v-if="form.shareImg" :src="form.shareImg" class="activty-thumb">
67
+        <i v-else class="el-icon-plus avatar-uploader-icon"></i>
68
+      </el-upload>
69
+    </el-form-item>
70
+    <el-form-item>
71
+      <el-button type="primary" @click="handleSubmit">保 存</el-button>
72
+      <el-button @click="handleCancel">取 消</el-button>
73
+    </el-form-item>
74
+  </el-form>
75
+</template>
76
+
77
+<script>
78
+import uploadOption from '@/utils/uploadOption'
79
+
80
+export default {
81
+  components: {
82
+    markdown: () => import('@/components/MarkdownEditor')
83
+  },
84
+
85
+  props: {
86
+    type: {
87
+      type: String,
88
+      required: true
89
+    },
90
+    dataset: {
91
+      type: Object,
92
+      default: undefined
93
+    },
94
+    enroll: Boolean,
95
+    vote: Boolean
96
+  },
97
+
98
+  data() {
99
+    return {
100
+      uploadOption,
101
+      formDataInited: false,
102
+      form: {
103
+        activityId: 0,
104
+        name: null,
105
+        typeId: 0,
106
+        typeName: null,
107
+        summary: null,
108
+        startDate: new Date(),
109
+        endDate: new Date(),
110
+        address: null,
111
+        enrollStart: new Date(),
112
+        enrollEnd: new Date(),
113
+        voteStart: new Date(),
114
+        voteEnd: new Date(),
115
+        createDate: new Date(),
116
+        status: null,
117
+        thumb: null,
118
+        detail: null,
119
+        isWarm: false,
120
+        isVote: false,
121
+        voteNum: null,
122
+        enrollNum: null,
123
+        shareImg: null,
124
+        shareTitle: null,
125
+        weight: null
126
+      },
127
+      rules: {
128
+        name: [
129
+          { required: true, message: '请输入活动名称', trigger: 'blur' }
130
+        ],
131
+        thumb: [
132
+          { required: true, message: '请设置活动封面', trigger: 'change' }
133
+        ]
134
+      }
135
+    }
136
+  },
137
+
138
+  computed: {
139
+    dtRange1: {
140
+      get() {
141
+        return [this.form.startDate, this.form.endDate]
142
+      },
143
+      set(val) {
144
+        this.form.startDate = val[0]
145
+        this.form.endDate = val[1]
146
+      }
147
+    },
148
+
149
+    dtRange2: {
150
+      get() {
151
+        return [this.form.enrollStart, this.form.enrollEnd]
152
+      },
153
+      set(val) {
154
+        this.form.enrollStart = val[0]
155
+        this.form.enrollEnd = val[1]
156
+      }
157
+    },
158
+
159
+    dtRange3: {
160
+      get() {
161
+        return [this.form.voteStart, this.form.voteEnd]
162
+      },
163
+      set(val) {
164
+        this.form.voteStart = val[0]
165
+        this.form.voteEnd = val[1]
166
+      }
167
+    }
168
+  },
169
+
170
+  watch: {
171
+    'form.name'(newVal, oldVal) {
172
+      if (!this.form.shareTitle || this.form.shareTitle === oldVal) {
173
+        this.form.shareTitle = newVal
174
+      }
175
+    },
176
+
177
+    'form.thumb'(newVal, oldVal) {
178
+      if (!this.form.shareImg || this.form.shareImg === oldVal) {
179
+        this.form.shareImg = newVal
180
+      }
181
+    },
182
+
183
+    dataset(val) {
184
+      if (val && !this.formDataInited) {
185
+        this.form = val
186
+        this.formDataInited = true
187
+      }
188
+    }
189
+  },
190
+
191
+  mounted() {
192
+    if (this.dataset) {
193
+      this.form = this.dataset
194
+      this.formDataInited = true
195
+    }
196
+  },
197
+
198
+  methods: {
199
+    handleSubmit() {
200
+      this.$refs.activityForm.validate(valid => {
201
+        if (valid) {
202
+          const data = {
203
+            ...this.form,
204
+            typeId: this.type
205
+          }
206
+          this.$emit('submit', data)
207
+        }
208
+      })
209
+    },
210
+
211
+    handleCancel() {
212
+      this.$emit('cancel')
213
+    },
214
+
215
+    beforeUpload(file) {
216
+      // const isJPG = file.type === 'image/jpeg';
217
+      // const isLt2M = file.size / 1024 / 1024 < 2;
218
+
219
+      return true
220
+    },
221
+
222
+    handleUploadSuccess(res, file) {
223
+      this.form.thumb = res.data.url
224
+    }
225
+  }
226
+}
227
+</script>
228
+
229
+<style lang="scss">
230
+.activty-uploader {
231
+  .el-upload {
232
+    border: 1px dashed #d9d9d9;
233
+    border-radius: 6px;
234
+    cursor: pointer;
235
+    position: relative;
236
+    overflow: hidden;
237
+
238
+    &:hover {
239
+      border-color: #409EFF;
240
+    }
241
+
242
+    .avatar-uploader-icon {
243
+      font-size: 28px;
244
+      color: #8c939d;
245
+      width: 178px;
246
+      height: 178px;
247
+      line-height: 178px;
248
+      text-align: center;
249
+    }
250
+  }
251
+}
252
+
253
+.activty-thumb {
254
+  width: 178px;
255
+  height: 178px;
256
+  display: block;
257
+}
258
+</style>

+ 15
- 0
src/views/life/Warm.vue View File

@@ -0,0 +1,15 @@
1
+<template>
2
+  <div class="app-container">
3
+    <div style="max-width: 60vw">
4
+      <activity type="2" />
5
+    </div>
6
+  </div>
7
+</template>
8
+
9
+<script>
10
+export default {
11
+  components: {
12
+    activity: () => import('@/views/components/Activity')
13
+  }
14
+}
15
+</script>

+ 5
- 0
yarn.lock View File

@@ -2909,6 +2909,11 @@ coa@^2.0.2:
2909 2909
     chalk "^2.4.1"
2910 2910
     q "^1.1.2"
2911 2911
 
2912
+codemirror@^5.56.0:
2913
+  version "5.56.0"
2914
+  resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.56.0.tgz#675640fcc780105cd22d3faa738b5d7ea6426f61"
2915
+  integrity sha512-MfKVmYgifXjQpLSgpETuih7A7WTTIsxvKfSLGseTY5+qt0E1UD1wblZGM6WLenORo8sgmf+3X+WTe2WF7mufyw==
2916
+
2912 2917
 collection-visit@^1.0.0:
2913 2918
   version "1.0.0"
2914 2919
   resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0"