Pārlūkot izejas kodu

feat: 企微数据 - 账号数据趋势 - 前端页面

zhengxy 2 gadi atpakaļ
vecāks
revīzija
ba82bc085f

+ 6 - 0
project/src/components/assembly/screen/channel.vue

@@ -152,6 +152,12 @@ export default {
152 152
         { key: 4, val: '待分配' },
153 153
       ]
154 154
       this.placeholderVal = '添加状态'
155
+    } else if (this.type == 'promotionType') { // 推广类型
156
+      this.options = [
157
+        { key: 1, val: 'H5' },
158
+        { key: 2, val: '小程序' },
159
+      ]
160
+      this.placeholderVal = '推广类型'
155 161
     } else {
156 162
       this.init()
157 163
     }

+ 420 - 0
project/src/components/dataBoard/accountTrends.vue

@@ -0,0 +1,420 @@
1
+<template>
2
+  <div v-loading="loading">
3
+    <div class="screenBox flex">
4
+      <switchMpAdq v-model="filter.order_type" @change="onChangeOrderType" />
5
+      <el-button type="primary" size="mini" @click="onClickExport">导出Excel</el-button>
6
+    </div>
7
+    <!-- S 筛选区 -->
8
+    <div class="screenBox filter-wrap">
9
+      <!-- 日期 -->
10
+      <datePicker style="margin-right: 30px" :reset="reset" title="自定义" :quickFlag="true" :afferent_time="default_time" :clearFlag="false" @changeTime="onChangeTime" />
11
+      <!-- MP账号(公众号) -->
12
+      <selfChannel style="margin-left: -30px;" v-show="isMP" :reset="reset" title="账号" type="thePublic" labelWidth @channelDefine="onChangeAccountId" />
13
+      <!-- ADQ账号(ADQ) -->
14
+      <selfChannel style="margin-left: -30px;" v-show="isADQ" :reset="reset" title="账号" type="thePublic" labelWidth @channelDefine="onChangeAccountIdAdq" />
15
+      <!-- 收益截止日期 -->
16
+      <div class="common-screen-item" style="margin-left: 10px;">
17
+        <label class="common-screen-label">收益截止日期</label>
18
+        <el-date-picker v-model="filter.closing_date" value-format="yyyy-MM-dd" type="date" placeholder="选择日期" size="small" style="width:150px" @change="onChangeClosingDate" />
19
+      </div>
20
+      <!-- 推广类型 -->
21
+      <selfChannel style="margin-left: -30px;" :reset="reset" title="推广类型" type="promotionType" labelWidth @channelDefine="onChangePromotionType" />
22
+      <!-- 回本率(范围) -->
23
+      <inputRange style="margin-left: 20px;" v-model="filter.totalRoi" label="回本率" />
24
+      <div>
25
+        <el-button size="mini" type="primary" plain @click="onClickSearch">确定</el-button>
26
+        <el-button size="mini" plain @click="onClickReset">重置</el-button>
27
+      </div>
28
+    </div>
29
+    <!-- E 筛选区 -->
30
+
31
+    <!-- S 汇总表 summaryTable -->
32
+    <ux-grid class="summaryTable" ref="summaryTable" :border="false" @row-click="() => { return }" :header-cell-style="getHeaderCellStyle" show-footer-overflow="tooltip" show-overflow="tooltip" size="mini">
33
+      <ux-table-column v-for="item in summaryTableCol" :key="item.column" :resizable="true" :field="item.column" :title="item.name" :min-width="item.min_width ? item.min_width : 120" :fixed="item.fixed ? item.fixed : ''" align="center">
34
+        <template #header>
35
+          <div class="flex-align-jus-center">
36
+            {{ item.name }}
37
+            <el-tooltip v-if="item.notes" :content="item.notes" placement="top">
38
+              <div><i class="el-icon-question"></i></div>
39
+            </el-tooltip>
40
+          </div>
41
+        </template>
42
+        <template v-slot="{ row }">
43
+          <span>{{ row[item.column] ? $formatNum(row[item.column]) : '-' }}</span>
44
+        </template>
45
+      </ux-table-column>
46
+    </ux-grid>
47
+    <!-- E 汇总表 summaryTable -->
48
+
49
+    <!-- S 明细表 detailsTable -->
50
+    <ux-grid class="detailsTable" ref="detailsTable" :border="false" @row-click="() => { return }" :header-cell-style="getHeaderCellStyle" show-footer-overflow="tooltip" show-overflow="tooltip" size="mini">
51
+      <ux-table-column v-for="item in detailsTableCol" :key="item.column" :resizable="true" :field="item.column" :title="item.name" :min-width="item.min_width ? item.min_width : 120" :fixed="item.fixed ? item.fixed : ''" align="center">
52
+        <template #header>
53
+          <div class="flex-align-jus-center">
54
+            {{ item.name }}
55
+            <div v-if="item.isSort" class="sort-wrap">
56
+              <i class="el-icon-caret-top" :class="{ 'active': filter.sort_field === item.column && filter.sort_type === 'asc' }" @click="onClickSort(item.column, 'asc')" />
57
+              <i class="el-icon-caret-bottom" :class="{ 'active': filter.sort_field === item.column && filter.sort_type === 'desc' }" @click="onClickSort(item.column, 'desc')" />
58
+            </div>
59
+            <el-tooltip v-if="item.notes" :content="item.notes" placement="top">
60
+              <div><i class="el-icon-question"></i></div>
61
+            </el-tooltip>
62
+          </div>
63
+        </template>
64
+        <template v-slot="{ row }">
65
+          <span>{{ row[item.column] ? $formatNum(row[item.column]) : '-' }}</span>
66
+        </template>
67
+      </ux-table-column>
68
+    </ux-grid>
69
+    <div class="pagination" v-show="pagination.total > 0">
70
+      <el-pagination background :current-page="pagination.page" @current-change="handleCurrentChange" layout="prev, pager, next" :page-count='Number(pagination.pages)' />
71
+    </div>
72
+    <!-- E 明细表 detailsTable -->
73
+  </div>
74
+</template>
75
+<script>
76
+import datePicker from '@/components/assembly/screen/datePicker.vue'
77
+import inputRange from '@/components/dataBoard/inputRange.vue'
78
+import selfChannel from '@/components/assembly/screen/channel.vue'
79
+import switchMpAdq from '@/components/assembly/screen/switchMpAdq.vue'
80
+import { orderTypeOptions } from '@/assets/js/staticTypes'
81
+export default {
82
+  components: {
83
+    datePicker,
84
+    inputRange,
85
+    selfChannel,
86
+    switchMpAdq,
87
+  },
88
+  data () {
89
+    const DEFAULT_TIME = [this.$getDay(-30, false), this.$getDay(0, false)]
90
+    return {
91
+      loading: false,
92
+      default_time: DEFAULT_TIME,
93
+      reset: false,
94
+      summaryTableCol: [
95
+        { "column": "date", "name": "用户注册时间", "notes": ""},
96
+        { "column": "advertiser_cost", "name": "投放消耗", "notes": "",},
97
+        { "column": "follow_uv", "name": "企微关注数", "notes": ""},
98
+        { "column": "per_follow_cost", "name": "企微关注成本", "notes": ""},
99
+        { "column": "total_roi", "name": "总回收", "notes": "总回收金额/投放消耗"},
100
+        { "column": "day1_roi", "name": "day1", "notes": ""}
101
+      ],
102
+      detailsTableCol: [
103
+        { "column": "date", "name": "用户注册时间", "notes": ""},
104
+        { "column": "advertiser_cost", "name": "投放消耗", "notes": "", isSort: 1},
105
+        { "column": "follow_uv", "name": "企微关注数", "notes": "", isSort: 1},
106
+        { "column": "per_follow_cost", "name": "企微关注成本", "notes": ""},
107
+        { "column": "total_roi", "name": "总回收", "notes": "总回收金额/投放消耗", isSort: 1},
108
+        { "column": "day1_roi", "name": "day1", "notes": ""}
109
+      ],
110
+      pagination: {
111
+        page: 1,
112
+        page_size: 20,
113
+        pages: 0,
114
+        total: 0,
115
+      },
116
+      filter: {
117
+        order_type: orderTypeOptions.MP,
118
+        time: DEFAULT_TIME, // 自定义日期
119
+        account_id: '', // 账号MP
120
+        account_id_adq: '', // 账号ADQ
121
+        closing_date: '', // 收益截止日期
122
+        promotion_type: '', // 推广类型
123
+        totalRoi: ['', ''], // 回本率(范围)
124
+        sort_field: '', // 排序字段
125
+        sort_type: '', // 升序/降序
126
+      },
127
+    }
128
+  },
129
+  computed: {
130
+    // 当前列表是否为"MP运营数据"
131
+    isMP() {
132
+      return this.filter.order_type === orderTypeOptions.MP
133
+    },
134
+    // 当前列表是否为"ADQ运营数据"
135
+    isADQ() {
136
+      return this.filter.order_type === orderTypeOptions.ADQ
137
+    },
138
+  },
139
+  created () {
140
+    this.handleGetList()
141
+  },
142
+  methods: {
143
+    // 获取列表数据
144
+    async handleGetList() {
145
+      console.log('handleGetList => ',)
146
+      console.log('filter => ', JSON.parse(JSON.stringify(this.filter)))
147
+      console.log('pagination => ', JSON.parse(JSON.stringify(this.pagination)))
148
+      // mock
149
+      await this.$nextTick()
150
+      this.$refs.summaryTable.reloadData([{
151
+        "date": "2022-08-24",
152
+        "advertiser_cost": "105.57",
153
+        "first_order_ucnt_unique": 0,
154
+        "first_order_ucnt": 0,
155
+        "follow_uv": 5,
156
+        "total_cvt_amt": "0.00",
157
+        "per_follow_cost": "21.11",
158
+        "total_roi": "0%",
159
+        "charge_data": "[0]",
160
+        "first_day_charge": "0.00",
161
+        "first_day_roi": "0%",
162
+        "first_order_cost": "0.00",
163
+        "first_order_cost_unique": "0.00",
164
+        "day1_roi": "0%"
165
+      }])
166
+      this.$refs.detailsTable.reloadData([{
167
+        "date": "2022-08-24",
168
+        "advertiser_cost": "105.57",
169
+        "first_order_ucnt_unique": 0,
170
+        "first_order_ucnt": 0,
171
+        "follow_uv": 5,
172
+        "total_cvt_amt": "0.00",
173
+        "per_follow_cost": "21.11",
174
+        "total_roi": "0%",
175
+        "charge_data": "[0]",
176
+        "first_day_charge": "0.00",
177
+        "first_day_roi": "0%",
178
+        "first_order_cost": "0.00",
179
+        "first_order_cost_unique": "0.00",
180
+        "day1_roi": "0%"
181
+      }, {
182
+        "date": "2022-08-25",
183
+        "advertiser_cost": "200",
184
+        "first_order_ucnt_unique": 1,
185
+        "first_order_ucnt": 1,
186
+        "follow_uv": 6,
187
+        "total_cvt_amt": "1.00",
188
+        "per_follow_cost": "1.11",
189
+        "total_roi": "8%",
190
+        "charge_data": "[0]",
191
+        "first_day_charge": "0.00",
192
+        "first_day_roi": "0%",
193
+        "first_order_cost": "0.00",
194
+        "first_order_cost_unique": "0.00",
195
+        "day1_roi": "0%"
196
+      }])
197
+      this.pagination.total = 2
198
+      this.pagination.pages = 2
199
+      // mock
200
+      return // mock
201
+      this.loading = true
202
+      this.$axios.get(`${this.URL.BASEURL}${this.URL.statistics_reg_range_report_new}`, {
203
+        params: {
204
+          begin_date: this.filter.time[0],
205
+          end_date: this.filter.time[1],
206
+          total_roi_min: this.filter.totalRoi[0],
207
+          total_roi_max: this.filter.totalRoi[1],
208
+          page: this.pagination.page,
209
+          page_size: this.pagination.page_size,
210
+        }
211
+      }).then((res) => {
212
+        var res = res.data
213
+        this.loading = false
214
+        if (res && res.errno == 0) {
215
+          this.summaryTableCol = res.rst.data.head;
216
+          this.$nextTick((item) => {
217
+            this.datas = res.rst.data.list // 知道为啥datas不在 data()方法里面定义吗?嘻嘻
218
+            this.$refs.summaryTable.reloadData(this.datas)
219
+          })
220
+          this.pagination.total = res.rst.pageInfo.total;
221
+          this.pagination.pages = res.rst.pageInfo.pages;
222
+        } else if (res.errno != 4002) {
223
+          this.$message({
224
+            message: res.err,
225
+            type: "warning"
226
+          })
227
+        }
228
+      }).catch((err) => {
229
+        this.loading = false
230
+      });
231
+    },
232
+    onChangeOrderType() {
233
+      this.pagination.page = 1
234
+      this.handleGetList()
235
+    },
236
+    // 监听时间筛选变化
237
+    onChangeTime(time) {
238
+      this.filter.time = Array.isArray(time) ? time : []
239
+      this.pagination.page = 1
240
+      this.handleGetList()
241
+    },
242
+    // 监听“MP账号”筛选变化
243
+    onChangeAccountId(val) {
244
+      this.filter.account_id = val
245
+      this.pagination.page = 1
246
+      this.handleGetList()
247
+    },
248
+    // 监听“MP账号”筛选变化
249
+    onChangeAccountIdAdq(val) {
250
+      this.filter.account_id_adq = val
251
+      this.pagination.page = 1
252
+      this.handleGetList()
253
+    },
254
+    // 监听“收益截止日期”筛选变化
255
+    onChangeClosingDate(val) {
256
+      this.filter.closing_date = val || ''
257
+      this.pagination.page = 1
258
+      this.handleGetList()
259
+    },
260
+    // 监听“推广类型”筛选变化
261
+    onChangePromotionType(val) {
262
+      this.filter.promotion_type = val || ''
263
+      this.pagination.page = 1
264
+      this.handleGetList()
265
+    },
266
+    // 监听当前页变化
267
+    handleCurrentChange(currentPage) {
268
+      this.pagination.page = currentPage
269
+      this.handleGetList()
270
+    },
271
+    // 监听点击"确定(搜索)"按钮
272
+    onClickSearch() {
273
+      this.pagination.page = 1
274
+      this.handleGetList()
275
+    },
276
+    // 监听排序变化
277
+    onClickSort(sort_field, sort_type) {
278
+      // sort_type:升序asc、降序desc
279
+      if (this.filter.sort_field === sort_field) {
280
+        if (this.filter.sort_type === sort_type) {
281
+          // 点击的是当前排序字段 && 是当前排序类型 => 重置 取消排序
282
+          this.filter.sort_field = ''
283
+          this.filter.sort_type = ''
284
+        } else {
285
+          // 点击的是当前排序字段 && 非当前排序类型 => 设置排序类型
286
+          this.filter.sort_type = sort_type
287
+        }
288
+      } else {
289
+        // 点击的不是当前排序字段 => 设置排序字段和类型
290
+        this.filter.sort_field = sort_field
291
+        this.filter.sort_type = sort_type
292
+      }
293
+      // 后端排序 => 获取最新数据
294
+      this.pagination.page = 1
295
+      this.handleGetList()
296
+    },
297
+    // 监听点击"重置"按钮
298
+    onClickReset() {
299
+      this.reset = !this.reset
300
+      this.filter.order_type = orderTypeOptions.MP
301
+      this.filter.time = this.default_time
302
+      this.filter.account_id = ''
303
+      this.filter.account_id_adq = ''
304
+      this.filter.closing_date = '',
305
+      this.filter.promotion_type = ''
306
+      this.filter.totalRoi = ['', '']
307
+      this.filter.sort_field = ''
308
+      this.filter.sort_type = ''
309
+      this.pagination.page = 1
310
+      this.handleGetList()
311
+    },
312
+    // 监听点击"导出"按钮
313
+    onClickExport() {
314
+      console.log('onClickExport => ')
315
+      if (!this.pagination.total) return this.$message.warning('暂无数据可导出')
316
+      this.loading = true
317
+      this.$axios.get(`${this.URL.BASEURL}${this.URL.statistics_reg_range_report_new}`, {
318
+        params: {
319
+          begin_date: this.filter.time[0],
320
+          end_date: this.filter.time[1],
321
+          first_day_roi_min: this.firstDayRoi[0],
322
+          first_day_roi_max: this.firstDayRoi[1],
323
+          first_order_cost_min: this.firstOrderCost[0],
324
+          first_order_cost_max: this.firstOrderCost[1],
325
+          first_order_cost_unique_min: this.firstOrderCostUnique[0],
326
+          first_order_cost_unique_max: this.firstOrderCostUnique[1],
327
+          per_follow_cost_min: this.perFollowCost[0],
328
+          per_follow_cost_max: this.perFollowCost[1],
329
+          total_roi_min: this.totalRoi[0],
330
+          total_roi_max: this.totalRoi[1],
331
+          page: 1,
332
+          page_size: this.$store.state.exportNumber,
333
+        }
334
+      }).then((res) => {
335
+        var res = res.data
336
+        this.loading = false
337
+        if (res && res.errno == 0) {
338
+          this.handleExport(res.rst.data.list)
339
+        } else if (res.errno != 4002) {
340
+          this.$message({
341
+            message: res.err,
342
+            type: "warning"
343
+          })
344
+        }
345
+      }).catch((err) => {
346
+        this.loading = false
347
+      });
348
+    },
349
+    // 执行导出逻辑
350
+    handleExport(data) {
351
+      let list = data;
352
+      let tHeader = this.summaryTableCol.map((v) => {
353
+        return v.name;
354
+      })
355
+      let filterVal = this.summaryTableCol.map((v) => {
356
+        return v.column
357
+      })
358
+      let excelDatas = [
359
+        {
360
+          tHeader: tHeader, // sheet表一头部
361
+          filterVal: filterVal, // 表一的数据字段
362
+          tableDatas: list, // 表一的整体json数据
363
+          sheetName: ''// 表一的sheet名字
364
+        }
365
+      ]
366
+      this.$exportOrder({ excelDatas, name: `账号数据趋势(导出时间:${this.$getDay(0)})` })
367
+    },
368
+    getHeaderCellStyle() {
369
+      return { backgroundColor: '#FFFFFF !important', border: 'none!important' }
370
+    },
371
+  }
372
+}
373
+</script>
374
+<style lang="scss" scoped>
375
+.screenBox {
376
+  position: relative;
377
+  background: #fff;
378
+  padding: 5px 20px;
379
+}
380
+.ml-10 {
381
+  margin-left: 10px;
382
+}
383
+.filter-wrap {
384
+  display: flex;
385
+  align-items: center;
386
+  flex-wrap: wrap;
387
+  padding-bottom: 10px;
388
+  & > div {
389
+    margin: 0 10px 10px 0;
390
+  }
391
+  & > button {
392
+    margin: -10px 0 0 10px;
393
+  }
394
+  .el-button+.el-button {
395
+    margin-left: 5px;
396
+  }
397
+}
398
+.summaryTable {
399
+  margin-top: 10px;
400
+}
401
+.detailsTable {
402
+  margin-top: 10px;
403
+}
404
+.sort-wrap {
405
+  display: flex;
406
+  flex-direction: column;
407
+  i {
408
+    cursor: pointer;
409
+    &.active {
410
+      color: #32B38A;
411
+    }
412
+  }
413
+  i:first-child {
414
+    margin-bottom: -3px;
415
+  }
416
+  i:last-child {
417
+    margin-top: -3px;
418
+  }
419
+}
420
+</style>

+ 12 - 0
project/src/router/allRouter.js

@@ -53,6 +53,7 @@ const groupCodeIndex = () => import(/* webpackChunkName: 'groupCodeIndex' */ '@/
53 53
 const createGroupCode = () => import(/* webpackChunkName: 'createGroupCode' */ '@/components/groupCode/createGroupCode.vue')
54 54
 const groupCodeAnalyse = () => import(/* webpackChunkName: 'groupCodeAnalyse' */ '@/components/groupCode/groupCodeAnalyse.vue')
55 55
 
56
+const accountTrends = () => import(/* webpackChunkName: 'accountTrends' */ '@/components/dataBoard/accountTrends.vue')
56 57
 
57 58
 // name与菜单配置的页面路由一致
58 59
 // meta下isData:true为数据看板,否则为助手
@@ -425,6 +426,17 @@ export var allRouter = [
425 426
         }
426 427
       },
427 428
       {
429
+        path: 'accountTrends',
430
+        name: 'accountTrends',
431
+        component: accountTrends,
432
+        meta: {
433
+          keepAlive: false,
434
+          isLogin: true,
435
+          title: '账号数据趋势',
436
+          isData: true
437
+        }
438
+      },
439
+      {
428 440
         path: 'thePublic',
429 441
         name: 'thePublic',
430 442
         component: thePublic,