张延森 4 年 前
コミット
4948f38340

+ 1
- 0
package.json ファイルの表示

@@ -19,6 +19,7 @@
19 19
     "codemirror": "^5.56.0",
20 20
     "core-js": "^3.6.5",
21 21
     "dayjs": "^1.8.33",
22
+    "echarts": "^4.8.0",
22 23
     "element-ui": "2.13.2",
23 24
     "md5": "^2.3.0",
24 25
     "normalize.css": "7.0.0",

+ 7
- 0
src/api/activity.js ファイルの表示

@@ -54,3 +54,10 @@ export function saveVoteItems(activityId, data) {
54 54
     data
55 55
   }))
56 56
 }
57
+
58
+export function getVoteRank(activityId) {
59
+  return pureResponseData(request({
60
+    url: `/api/admin/activity/${activityId}/voteitem/rank`,
61
+    method: 'get'
62
+  }))
63
+}

+ 105
- 0
src/components/ECharts/index.vue ファイルの表示

@@ -0,0 +1,105 @@
1
+<template>
2
+  <div ref="echarts"></div>
3
+</template>
4
+
5
+<script>
6
+import echarts from 'echarts'
7
+
8
+export default {
9
+  props: {
10
+    options: {
11
+      type: Object,
12
+      default: () => {}
13
+    },
14
+    dataSource: {
15
+      type: Array,
16
+      default: () => []
17
+    },
18
+    loading: {
19
+      type: Boolean,
20
+      default: false
21
+    },
22
+    show: {
23
+      type: Boolean,
24
+      default: true
25
+    }
26
+  },
27
+
28
+  data() {
29
+    return {
30
+      myChart: null
31
+    }
32
+  },
33
+
34
+  computed: {
35
+    config() {
36
+      return {
37
+        ...this.options,
38
+        dataset: {
39
+          ...(this.options.dataset || {}),
40
+          source: this.dataSource
41
+        }
42
+      }
43
+    }
44
+  },
45
+
46
+  watch: {
47
+    options(val) {
48
+      this.$nextTick(() => {
49
+        this.renderChart()
50
+      })
51
+    },
52
+    dataSource(val) {
53
+      this.$nextTick(() => {
54
+        this.renderChart()
55
+      })
56
+    },
57
+    loading(val) {
58
+      if (this.myChart) {
59
+        if (val) {
60
+          this.myChart.showLoading()
61
+        } else {
62
+          this.myChart.hideLoading()
63
+        }
64
+      }
65
+    },
66
+    show: {
67
+      handler(nw, od) {
68
+        if (this.myChart && !nw) {
69
+          this.destroyCharts()
70
+        } else if (nw && !od) {
71
+          this.renderChart()
72
+        }
73
+      },
74
+      immediate: true
75
+    }
76
+  },
77
+
78
+  mounted() {
79
+    this.renderChart()
80
+  },
81
+
82
+  beforeDestroy() {
83
+    this.destroyCharts()
84
+  },
85
+
86
+  methods: {
87
+    renderChart() {
88
+      const el = this.$refs.echarts
89
+      if (el && this.show) {
90
+        if (!this.myChart) {
91
+          this.myChart = echarts.init(el)
92
+        }
93
+        this.myChart.setOption(this.config)
94
+      }
95
+    },
96
+
97
+    destroyCharts() {
98
+      if (this.myChart) {
99
+        this.myChart.dispose()
100
+        this.myChart = null
101
+      }
102
+    }
103
+  }
104
+}
105
+</script>

+ 86
- 0
src/components/Pagination/index.vue ファイルの表示

@@ -0,0 +1,86 @@
1
+<template>
2
+  <div class="pagination">
3
+    <el-pagination
4
+      :current-page="config.current"
5
+      :page-size="config.size"
6
+      :total="total"
7
+      :page-sizes="sizeList"
8
+      :layout="layStr"
9
+      @size-change="handleSizeChange"
10
+      @current-change="handleCurrentChange"
11
+    />
12
+  </div>
13
+</template>
14
+
15
+<script>
16
+export default {
17
+  props: {
18
+    size: {
19
+      type: Number,
20
+      default: 10
21
+    },
22
+    current: {
23
+      type: Number,
24
+      default: 1
25
+    },
26
+    total: {
27
+      type: Number,
28
+      default: 0
29
+    },
30
+    layout: {
31
+      type: Array,
32
+      default: () => ['total', 'sizes', 'prev', 'pager', 'next']
33
+    },
34
+    sizeList: {
35
+      type: Array,
36
+      default: () => [10, 20, 50, 100]
37
+    }
38
+  },
39
+
40
+  data() {
41
+    return {
42
+      config: {
43
+        current: this.current,
44
+        size: this.size
45
+      }
46
+    }
47
+  },
48
+
49
+  computed: {
50
+    layStr() {
51
+      return this.layout.join(',')
52
+    }
53
+  },
54
+
55
+  methods: {
56
+    handleSizeChange(v) {
57
+      this.config.size = v
58
+      this.$emit('update:size', v)
59
+      this.$emit('change', { current: this.config.current, size: v })
60
+    },
61
+    handleCurrentChange(v) {
62
+      this.config.current = v
63
+      this.$emit('update:current', v)
64
+      this.$emit('change', { current: v, size: this.config.size })
65
+    }
66
+  }
67
+}
68
+</script>
69
+
70
+<style lang="scss" scoped>
71
+.pagination {
72
+  padding: 16px;
73
+  & > div {
74
+    float: right;
75
+  }
76
+
77
+  &::after {
78
+    visibility: hidden;
79
+    display: block;
80
+    font-size: 0;
81
+    content: " ";
82
+    clear: both;
83
+    height: 0;
84
+  }
85
+}
86
+</style>

+ 3
- 0
src/main.js ファイルの表示

@@ -17,6 +17,9 @@ import '@/permission' // permission control
17 17
 
18 18
 import '@/utils/filters' // 注册全局过滤器
19 19
 
20
+// 全局组件
21
+Vue.component('pagination', () => import('@/components/Pagination'))
22
+
20 23
 /**
21 24
  * If you don't want to use mock-server
22 25
  * you want to use MockJs for mock api

+ 12
- 2
src/views/activity/Detail.vue ファイルの表示

@@ -12,6 +12,9 @@
12 12
       <el-tab-pane label="投票设置" name="vote">
13 13
         <voteedit :activity-id="detail.activityId" />
14 14
       </el-tab-pane>
15
+      <el-tab-pane label="投票排行" name="rank">
16
+        <voterank :show="showRank" :activity-id="detail.activityId" style="width: 60vw; min-height: 400px" />
17
+      </el-tab-pane>
15 18
     </el-tabs>
16 19
   </div>
17 20
 </template>
@@ -23,7 +26,8 @@ export default {
23 26
   components: {
24 27
     activity: () => import('./components/Detail'),
25 28
     enrollList: () => import('./components/EnrollList'),
26
-    voteedit: () => import('./components/VoteEdit')
29
+    voteedit: () => import('./components/VoteEdit'),
30
+    voterank: () => import('./components/VoteRank')
27 31
   },
28 32
 
29 33
   props: {
@@ -36,11 +40,17 @@ export default {
36 40
   data() {
37 41
     return {
38 42
       id: null,
39
-      detail: null,
43
+      detail: {},
40 44
       currentTab: 'detail'
41 45
     }
42 46
   },
43 47
 
48
+  computed: {
49
+    showRank() {
50
+      return this.currentTab === 'rank'
51
+    }
52
+  },
53
+
44 54
   created() {
45 55
     const { id } = this.$route.query
46 56
     if (id) {

+ 14
- 32
src/views/activity/List.vue ファイルの表示

@@ -36,17 +36,12 @@
36 36
         </template>
37 37
       </el-table-column>
38 38
     </el-table>
39
-    <div class="pagination">
40
-      <el-pagination
41
-        :current-page="pagination.current"
42
-        :page-size="pagination.size"
43
-        :total="pagination.total"
44
-        :page-sizes="[10, 20, 50, 100]"
45
-        layout="total, sizes, prev, pager, next"
46
-        @size-change="handleSizeChange"
47
-        @current-change="handleCurrentChange"
48
-      />
49
-    </div>
39
+    <pagination
40
+      :total="page.total"
41
+      :size.sync="page.size"
42
+      :current.sync="page.current"
43
+      @change="handlePageChange"
44
+    ></pagination>
50 45
   </div>
51 46
 </template>
52 47
 
@@ -55,6 +50,7 @@ import { getActivityList } from '@/api/activity'
55 50
 
56 51
 export default {
57 52
   components: {
53
+    // pagination: () => import('@/components/Pagination'),
58 54
     FilterForm: () => import('./components/FilterForm')
59 55
   },
60 56
 
@@ -98,7 +94,7 @@ export default {
98 94
       list: null,
99 95
       listLoading: true,
100 96
       detailRouter: null,
101
-      pagination: {
97
+      page: {
102 98
         total: 0,
103 99
         size: 10,
104 100
         current: 1
@@ -119,15 +115,11 @@ export default {
119 115
       getActivityList({
120 116
         ...this.filters,
121 117
         typeId: this.typeId,
122
-        pageNum: this.pagination.current,
123
-        pageSize: this.pagination.size
118
+        pageNum: this.page.current,
119
+        pageSize: this.page.size
124 120
       }).then(res => {
125 121
         this.list = res.records
126
-        this.pagination = {
127
-          total: res.total,
128
-          size: res.size,
129
-          current: res.current
130
-        }
122
+        this.page.total = res.total
131 123
         this.listLoading = false
132 124
       })
133 125
     },
@@ -147,12 +139,9 @@ export default {
147 139
       })
148 140
     },
149 141
 
150
-    handleSizeChange(size) {
151
-      this.pagination.size = size
152
-    },
153
-
154
-    handleCurrentChange(current) {
155
-      this.pagination.current = current
142
+    handlePageChange({ current, size }) {
143
+      this.page.current = current
144
+      this.page.size = size
156 145
       this.fetchData()
157 146
     },
158 147
 
@@ -175,11 +164,4 @@ export default {
175 164
   justify-content: space-between;
176 165
   align-items: center;
177 166
 }
178
-
179
-.pagination {
180
-  padding: 16px;
181
-  & > div {
182
-    float: right;
183
-  }
184
-}
185 167
 </style>

+ 13
- 34
src/views/activity/components/EnrollList.vue ファイルの表示

@@ -27,17 +27,12 @@
27 27
       <el-table-column label="昵称" prop="personNickname" />
28 28
       <el-table-column label="电话" prop="phone" />
29 29
     </el-table>
30
-    <div class="pagination">
31
-      <el-pagination
32
-        :current-page="pagination.current"
33
-        :page-size="pagination.size"
34
-        :total="pagination.total"
35
-        :page-sizes="[10, 20, 50, 100]"
36
-        layout="total, sizes, prev, pager, next"
37
-        @size-change="handleSizeChange"
38
-        @current-change="handleCurrentChange"
39
-      />
40
-    </div>
30
+    <pagination
31
+      :total="page.total"
32
+      :size.sync="page.size"
33
+      :current.sync="page.current"
34
+      @change="handlePageChange"
35
+    ></pagination>
41 36
   </div>
42 37
 </template>
43 38
 
@@ -52,7 +47,7 @@ export default {
52 47
     return {
53 48
       list: null,
54 49
       listLoading: true,
55
-      pagination: {
50
+      page: {
56 51
         total: 0,
57 52
         size: 10,
58 53
         current: 1
@@ -71,36 +66,20 @@ export default {
71 66
       this.listLoading = true
72 67
       getEnrollList({
73 68
         activityId: this.activityId,
74
-        pageNum: this.pagination.current,
75
-        pageSize: this.pagination.size
69
+        pageNum: this.page.current,
70
+        pageSize: this.page.size
76 71
       }).then(res => {
77 72
         this.list = res.records
78
-        this.pagination = {
79
-          total: res.total,
80
-          size: res.size,
81
-          current: res.current
82
-        }
73
+        this.page.total = res.total
83 74
         this.listLoading = false
84 75
       })
85 76
     },
86 77
 
87
-    handleSizeChange(size) {
88
-      this.pagination.size = size
89
-    },
90
-
91
-    handleCurrentChange(current) {
92
-      this.pagination.current = current
78
+    handlePageChange({ current, size }) {
79
+      this.page.current = current
80
+      this.page.size = size
93 81
       this.fetchData()
94 82
     }
95 83
   }
96 84
 }
97 85
 </script>
98
-
99
-<style lang="scss" scoped>
100
-.pagination {
101
-  padding: 16px;
102
-  & > div {
103
-    float: right;
104
-  }
105
-}
106
-</style>

+ 0
- 0
src/views/activity/components/VoteDetail.vue ファイルの表示


+ 116
- 0
src/views/activity/components/VoteRank.vue ファイルの表示

@@ -0,0 +1,116 @@
1
+<template>
2
+  <echarts
3
+    :show="show"
4
+    :options="options"
5
+    :data-source="source"
6
+    :loading="loading"></echarts>
7
+</template>
8
+
9
+<script>
10
+import { getVoteRank } from '@/api/activity'
11
+
12
+export default {
13
+  components: {
14
+    echarts: () => import('@/components/ECharts')
15
+  },
16
+
17
+  props: {
18
+    activityId: {
19
+      type: Number,
20
+      default: 0
21
+    },
22
+    show: {
23
+      type: Boolean,
24
+      default: true
25
+    }
26
+  },
27
+
28
+  data() {
29
+    return {
30
+      defaultOpt: {
31
+        title: {
32
+          text: '投票结果排行'
33
+        },
34
+        tooltip: {
35
+          trigger: 'axis'
36
+        },
37
+        dataset: {},
38
+        xAxis: { name: '票数' },
39
+        yAxis: {
40
+          type: 'category',
41
+          inverse: true
42
+        },
43
+        visualMap: {
44
+          orient: 'horizontal',
45
+          left: 'center',
46
+          min: 0,
47
+          max: 100,
48
+          text: ['高', '低'],
49
+          // 映射 score
50
+          dimension: 1,
51
+          splitNumber: 5,
52
+          color: ['#d94e5d', '#eac736', '#50a3ba']
53
+        },
54
+        series: [
55
+          {
56
+            type: 'bar',
57
+            encode: {
58
+              x: 'score',
59
+              y: 'item'
60
+            }
61
+          }
62
+        ]
63
+      },
64
+      // source 按照从大到小排序
65
+      source: ['item', 'score'],
66
+      loading: false
67
+    }
68
+  },
69
+
70
+  computed: {
71
+    options() {
72
+      const min = this.source.length ? this.source[this.source.length - 1][1] : 0
73
+      const max = this.source.length ? this.source[1][1] : 0
74
+
75
+      return {
76
+        ...this.defaultOpt,
77
+        visualMap: {
78
+          ...this.defaultOpt.visualMap,
79
+          min,
80
+          max
81
+        }
82
+      }
83
+    }
84
+  },
85
+
86
+  watch: {
87
+    activityId(val) {
88
+      if (val) {
89
+        this.getVoteResult()
90
+      }
91
+    }
92
+  },
93
+
94
+  created() {
95
+    this.getVoteResult()
96
+  },
97
+
98
+  methods: {
99
+    getVoteResult() {
100
+      if (this.activityId) {
101
+        this.loading = true
102
+        getVoteRank(this.activityId).then(res => {
103
+          this.loading = false
104
+
105
+          if (res && res.length) {
106
+            // 因为使用了 visualMap
107
+            // source 子元素必须是数组
108
+            this.source = [['item', 'score']].concat(res.map(x => [x.item, x.score]))
109
+          }
110
+        })
111
+      }
112
+    }
113
+  }
114
+
115
+}
116
+</script>