Browse Source

feat: 企微数据 - 用户流失趋势 - 前端页面

zhengxy 2 years ago
parent
commit
e8132c37e0
2 changed files with 700 additions and 0 deletions
  1. 687 0
      project/src/components/dataBoard/loseUserTrends.vue
  2. 13 0
      project/src/router/allRouter.js

+ 687 - 0
project/src/components/dataBoard/loseUserTrends.vue

@@ -0,0 +1,687 @@
1
+<template>
2
+  <div>
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
+      <!-- 企微主体 -->
12
+      <div style="margin-right: 30px;" class="common-screen-item">
13
+        <label class="common-screen-label" style="width: auto;">企微主体</label>
14
+        <!-- 系统管理员 -->
15
+        <el-cascader v-if="$cookie.getCookie('isSuperManage') == 1" v-model="system_enterprise" size="small" :options="enterpriseList" :props="{value:'self_id',label:'self_name',children:'manage_corp_list'}" @change="onChangeCorpidSystem" clearable placeholder="请选择企微主体" />
16
+        <!-- 非系统管理员 -->
17
+        <el-select v-else v-model="enterprise.corpid" size="small" placeholder="请选择企微主体" @change="onChangeCorpid" clearable>
18
+          <el-option v-for="(item, index) in enterpriseList" :key="index+'enterpriseList'" :label="item.corp_name?item.corp_name:item.corp_full_name?item.corp_full_name:item.corpid" :value="item.corpid" />
19
+        </el-select>
20
+      </div>
21
+      <!-- 收益截止日期 -->
22
+      <div style="margin-right: 30px;" class="common-screen-item">
23
+        <label class="common-screen-label">收益截止日期</label>
24
+        <el-date-picker v-model="filter.closing_date" value-format="yyyy-MM-dd" type="date" placeholder="选择日期" size="small" style="width:150px" @change="onChangeClosingDate" />
25
+      </div>
26
+      <!-- 推广类型 -->
27
+      <selfChannel style="margin-right: 30px;margin-left: -30px;" :reset="reset" title="推广类型" type="promotionType" labelWidth @channelDefine="onChangePromotionType" />
28
+      <div class="flex">
29
+        <!-- 回本率(范围) -->
30
+        <inputRange style="margin-right: 10px;" v-model="filter.totalRoi" label="回本率" />
31
+        <el-button size="mini" type="primary" plain @click="onClickSearch">确定</el-button>
32
+        <el-button size="mini" plain @click="onClickReset">重置</el-button>
33
+      </div>
34
+    </div>
35
+    <!-- E 筛选区 -->
36
+
37
+    <!-- S 折线图 -->
38
+    <div class="trendBox mt-10" v-loading="chartLoading">
39
+      <div class="legendBox">
40
+        <div class="legendItem" v-for="(item, index) in legendList" :key="index" @click="onClickLegend(item, index)">{{ item.name }}
41
+          <div :class="['checkbox', item.selectFlag ? 'checkbox_active': '']" :style="item.selectFlag ? `background: ${item.color};border-color: ${item.color};`: '' "><i class="el-icon-check" /></div>
42
+        </div>
43
+      </div>
44
+      <div id="trend" style="width: 100%; height: 250px;" />
45
+    </div>
46
+    <!-- E 折线图 -->
47
+
48
+    <!-- S 表 -->
49
+    <div v-loading="loading">
50
+      <!-- S 汇总表 summaryTable -->
51
+      <ux-grid class="summaryTable" ref="summaryTable" :border="false" @row-click="() => { return }" :header-cell-style="getHeaderCellStyle" show-footer-overflow="tooltip" show-overflow="tooltip" size="mini">
52
+        <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">
53
+          <template #header>
54
+            <div class="flex-align-jus-center">
55
+              {{ item.name }}
56
+              <el-tooltip v-if="item.notes" :content="item.notes" placement="top">
57
+                <div><i class="el-icon-question"></i></div>
58
+              </el-tooltip>
59
+            </div>
60
+          </template>
61
+          <template v-slot="{ row }">
62
+            <span>{{ row[item.column] ? $formatNum(row[item.column]) : '-' }}</span>
63
+          </template>
64
+        </ux-table-column>
65
+      </ux-grid>
66
+      <!-- E 汇总表 summaryTable -->
67
+      <!-- S 明细表 detailsTable -->
68
+      <ux-grid class="detailsTable" ref="detailsTable" :border="false" @row-click="() => { return }" :header-cell-style="getHeaderCellStyle" show-footer-overflow="tooltip" show-overflow="tooltip" size="mini">
69
+        <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">
70
+          <template #header>
71
+            <div class="flex-align-jus-center">
72
+              {{ item.name }}
73
+              <div v-if="item.isSort" class="sort-wrap">
74
+                <i class="el-icon-caret-top" :class="{ 'active': filter.sort_field === item.column && filter.sort_type === 'asc' }" @click="onClickSort(item.column, 'asc')" />
75
+                <i class="el-icon-caret-bottom" :class="{ 'active': filter.sort_field === item.column && filter.sort_type === 'desc' }" @click="onClickSort(item.column, 'desc')" />
76
+              </div>
77
+              <el-tooltip v-if="item.notes" :content="item.notes" placement="top">
78
+                <div><i class="el-icon-question"></i></div>
79
+              </el-tooltip>
80
+            </div>
81
+          </template>
82
+          <template v-slot="{ row }">
83
+            <span>{{ row[item.column] ? $formatNum(row[item.column]) : '-' }}</span>
84
+          </template>
85
+        </ux-table-column>
86
+      </ux-grid>
87
+      <div class="pagination" v-show="pagination.total > 0">
88
+        <el-pagination background :current-page="pagination.page" @current-change="handleCurrentChange" layout="prev, pager, next" :page-count='Number(pagination.pages)' />
89
+      </div>
90
+      <!-- E 明细表 detailsTable -->
91
+    </div>
92
+    <!-- E 表 -->
93
+  </div>
94
+</template>
95
+<script>
96
+import datePicker from '@/components/assembly/screen/datePicker.vue'
97
+import inputRange from '@/components/dataBoard/inputRange.vue'
98
+import selfChannel from '@/components/assembly/screen/channel.vue'
99
+import switchMpAdq from '@/components/assembly/screen/switchMpAdq.vue'
100
+import { orderTypeOptions } from '@/assets/js/staticTypes'
101
+import _lodash from 'lodash'
102
+
103
+export default {
104
+  components: {
105
+    datePicker,
106
+    inputRange,
107
+    selfChannel,
108
+    switchMpAdq,
109
+  },
110
+  data () {
111
+    const DEFAULT_TIME = [this.$getDay(-30, false), this.$getDay(0, false)]
112
+    return {
113
+      default_time: DEFAULT_TIME,
114
+      reset: false,
115
+
116
+      system_enterprise: [], // 企微主体数据
117
+      enterpriseList: [], // 企微主体数据
118
+      enterprise: {}, // 当前选择的企微信息
119
+
120
+      chartLoading: false,
121
+      myChart: null,
122
+      chartDataList: [],
123
+      legendList: [
124
+        { name: '消耗', key: 'cust_total_uc', color: '#2983DF', selectFlag: true },
125
+        { name: '3天倍率', key: 'cust_add_uc', color: '#00B38A', selectFlag: true },
126
+        { name: '7天倍率', key: 'cust_loss_uc', color: '#EB4315', selectFlag: true },
127
+        { name: '15天倍率', key: 'cust_gain_uc', color: '#AED570', selectFlag: true },
128
+        { name: '30天倍率', key: 'official_account_uc', color: '#EFAF47', selectFlag: true },
129
+      ],
130
+
131
+      loading: false,
132
+      summaryTableCol: [
133
+        { "column": "date", "name": "用户注册时间", "notes": ""},
134
+        { "column": "advertiser_cost", "name": "投放消耗", "notes": "",},
135
+        { "column": "follow_uv", "name": "企微关注数", "notes": ""},
136
+        { "column": "per_follow_cost", "name": "企微关注成本", "notes": ""},
137
+        { "column": "total_roi", "name": "总回收", "notes": "总回收金额/投放消耗"},
138
+        { "column": "day1_roi", "name": "day1", "notes": ""}
139
+      ],
140
+      detailsTableCol: [
141
+        { "column": "date", "name": "用户注册时间", "notes": ""},
142
+        { "column": "advertiser_cost", "name": "投放消耗", "notes": "", isSort: 1},
143
+        { "column": "follow_uv", "name": "企微关注数", "notes": "", isSort: 1},
144
+        { "column": "per_follow_cost", "name": "企微关注成本", "notes": ""},
145
+        { "column": "total_roi", "name": "总回收", "notes": "总回收金额/投放消耗", isSort: 1},
146
+        { "column": "day1_roi", "name": "day1", "notes": ""}
147
+      ],
148
+      pagination: {
149
+        page: 1,
150
+        page_size: 20,
151
+        pages: 0,
152
+        total: 0,
153
+      },
154
+      filter: {
155
+        order_type: orderTypeOptions.MP,
156
+        time: DEFAULT_TIME, // 自定义日期
157
+        corpid: '', // 企微主体
158
+        closing_date: '', // 收益截止日期
159
+        promotion_type: '', // 推广类型
160
+        totalRoi: ['', ''], // 回本率(范围)
161
+        sort_field: '', // 排序字段
162
+        sort_type: '', // 升序/降序
163
+      },
164
+    }
165
+  },
166
+  computed: {
167
+    // // 当前列表是否为"MP运营数据"
168
+    // isMP() {
169
+    //   return this.filter.order_type === orderTypeOptions.MP
170
+    // },
171
+    // // 当前列表是否为"ADQ运营数据"
172
+    // isADQ() {
173
+    //   return this.filter.order_type === orderTypeOptions.ADQ
174
+    // },
175
+  },
176
+  created () {
177
+    this.handleInitCorpOptions()
178
+    this.handleGetList()
179
+    this.handleGetChart()
180
+  },
181
+  beforeDestroy () {
182
+    this.myChart && this.myChart.clear()
183
+  },
184
+  methods: {
185
+    // 获取列表数据
186
+    async handleGetList() {
187
+      console.log('handleGetList => ',)
188
+      console.log('filter => ', JSON.parse(JSON.stringify(this.filter)))
189
+      console.log('pagination => ', JSON.parse(JSON.stringify(this.pagination)))
190
+      // mock
191
+      await this.$nextTick()
192
+      this.$refs.summaryTable.reloadData([{
193
+        "date": "2022-08-24",
194
+        "advertiser_cost": "105.57",
195
+        "first_order_ucnt_unique": 0,
196
+        "first_order_ucnt": 0,
197
+        "follow_uv": 5,
198
+        "total_cvt_amt": "0.00",
199
+        "per_follow_cost": "21.11",
200
+        "total_roi": "0%",
201
+        "charge_data": "[0]",
202
+        "first_day_charge": "0.00",
203
+        "first_day_roi": "0%",
204
+        "first_order_cost": "0.00",
205
+        "first_order_cost_unique": "0.00",
206
+        "day1_roi": "0%"
207
+      }])
208
+      this.$refs.detailsTable.reloadData([{
209
+        "date": "2022-08-24",
210
+        "advertiser_cost": "105.57",
211
+        "first_order_ucnt_unique": 0,
212
+        "first_order_ucnt": 0,
213
+        "follow_uv": 5,
214
+        "total_cvt_amt": "0.00",
215
+        "per_follow_cost": "21.11",
216
+        "total_roi": "0%",
217
+        "charge_data": "[0]",
218
+        "first_day_charge": "0.00",
219
+        "first_day_roi": "0%",
220
+        "first_order_cost": "0.00",
221
+        "first_order_cost_unique": "0.00",
222
+        "day1_roi": "0%"
223
+      }, {
224
+        "date": "2022-08-25",
225
+        "advertiser_cost": "200",
226
+        "first_order_ucnt_unique": 1,
227
+        "first_order_ucnt": 1,
228
+        "follow_uv": 6,
229
+        "total_cvt_amt": "1.00",
230
+        "per_follow_cost": "1.11",
231
+        "total_roi": "8%",
232
+        "charge_data": "[0]",
233
+        "first_day_charge": "0.00",
234
+        "first_day_roi": "0%",
235
+        "first_order_cost": "0.00",
236
+        "first_order_cost_unique": "0.00",
237
+        "day1_roi": "0%"
238
+      }])
239
+      this.pagination.total = 2
240
+      this.pagination.pages = 2
241
+      // mock
242
+      return // mock
243
+      this.loading = true
244
+      this.$axios.get(`${this.URL.BASEURL}${this.URL.statistics_reg_range_report_new}`, {
245
+        params: {
246
+          begin_date: this.filter.time[0],
247
+          end_date: this.filter.time[1],
248
+          total_roi_min: this.filter.totalRoi[0],
249
+          total_roi_max: this.filter.totalRoi[1],
250
+          page: this.pagination.page,
251
+          page_size: this.pagination.page_size,
252
+        }
253
+      }).then((res) => {
254
+        var res = res.data
255
+        this.loading = false
256
+        if (res && res.errno == 0) {
257
+          this.summaryTableCol = res.rst.data.head;
258
+          this.$nextTick((item) => {
259
+            this.datas = res.rst.data.list // 知道为啥datas不在 data()方法里面定义吗?嘻嘻
260
+            this.$refs.summaryTable.reloadData(this.datas)
261
+          })
262
+          this.pagination.total = res.rst.pageInfo.total;
263
+          this.pagination.pages = res.rst.pageInfo.pages;
264
+        } else if (res.errno != 4002) {
265
+          this.$message({
266
+            message: res.err,
267
+            type: "warning"
268
+          })
269
+        }
270
+      }).catch((err) => {
271
+        this.loading = false
272
+      });
273
+    },
274
+    // 获取折线图数据
275
+    handleGetChart() {
276
+      console.log('handleGetChart => ')
277
+      // return // mock
278
+      this.chartLoading = true
279
+      this.$axios.get(this.URL.BASEURL + this.URL.stat_custTrendsNew, {
280
+        params: {
281
+          corpid: 'wpezvKNwAA9d7LlcuOOAhvlx5ikwJjHg',
282
+          start: '2022-08-09',
283
+          end: '2022-09-08',
284
+        }
285
+      }).then((_res) => {
286
+        const res = _res.data
287
+        this.chartLoading = false
288
+        if (res && res.errno == 0) {
289
+          this.chartDataList = res.rst
290
+          this.$nextTick(() => {
291
+            this.handleDrawChart()
292
+          })
293
+        } else if (res.errno != 4002) {
294
+          this.$message({
295
+            message: res.err,
296
+            type: "warning"
297
+          })
298
+        }
299
+      }).catch((err) => {
300
+        this.chartLoading = false
301
+      });
302
+    },
303
+    // 绘制折线图
304
+    handleDrawChart() {
305
+      this.myChart && this.myChart.clear()
306
+      const _this = this;
307
+      const series = []
308
+      const yAxis = []
309
+      const xArr = this.chartDataList.map(v => v.idate)
310
+      this.legendList.forEach((item, index) => {
311
+        if (item.selectFlag) {
312
+          yAxis.push({
313
+            type: "value",
314
+            name: '',
315
+            show: false,
316
+            position: 'left',
317
+            axisTick: {
318
+              show: false
319
+            },
320
+            splitLine: {
321
+              lineStyle: {
322
+                color: '#F2F2f2',
323
+                type: "dashed"
324
+              }
325
+            },
326
+            axisLine: {
327
+              show: true,
328
+              lineStyle: {
329
+                color: '#F2F2f2'
330
+              }
331
+            },
332
+            nameTextStyle: {
333
+              color: "#999999",
334
+              fontSize: 13,
335
+            },
336
+            axisLabel: {
337
+              color: '#999999',
338
+              fontSize: 12,
339
+              show: true,
340
+              formatter: function(params) {
341
+                return _this.$NumberHandle(params)
342
+              }
343
+            }
344
+          })
345
+
346
+          const data = this.chartDataList.map(v => v[item.key] || v[item.key] == 0 ? v[item.key] : '-')
347
+
348
+          series.push({
349
+            type: "line",
350
+            smooth: true,
351
+            name: item.name,
352
+            yAxisIndex: 0,
353
+            data,
354
+            lineStyle: {
355
+              width: 2
356
+            },
357
+            symbol: xArr.length == 1 ? 'emptyCircle' : 'none',
358
+            itemStyle: {
359
+              color: item.color,
360
+              borderType: "emptyCircle"
361
+            },
362
+          })
363
+        }
364
+      })
365
+      yAxis.forEach((item, index) => {
366
+        item.show = index == 0 ? true : false
367
+      })
368
+      series.forEach((item, index) => {
369
+        item.yAxisIndex = index
370
+      })
371
+      const option = {
372
+        title: '',
373
+        tooltip: {
374
+          trigger: 'axis',
375
+          show: true,
376
+          formatter: function(params) {
377
+            let result = `${params[0].name}<br/>`
378
+            params.forEach(item => {
379
+              result += `${item.marker}${item.seriesName}:${_this.$formatNum(item.value)}<br/>`
380
+            });
381
+            return result;
382
+          }
383
+        },
384
+        legend: {
385
+          itemWidth: 8,
386
+          itemHeight: 2,
387
+          icon: "plain",
388
+          show: false,
389
+          textStyle: {
390
+            fontSize: 12,
391
+            color: '#666666'
392
+          },
393
+        },
394
+        grid: {
395
+          top: '6%',
396
+          left: '4%',
397
+          right: '4%',
398
+          bottom: '16%',
399
+          containLabel: false
400
+        },
401
+        xAxis: [{
402
+          type: "category",
403
+          data: xArr,
404
+          boundaryGap: false,//设置数据从头开始
405
+          axisLine: {
406
+            show: true,
407
+            lineStyle: {
408
+              color: '#F2F2f2'
409
+            }
410
+          },
411
+          axisTick: {
412
+            show: false
413
+          },
414
+          splitLine: {
415
+            show: false
416
+          },
417
+          axisLabel: {
418
+            color: '#666',
419
+            fontSize: 10,
420
+            rotate: 30,
421
+          },
422
+        }],
423
+        yAxis,
424
+        series,
425
+      }
426
+      // 初始化echarts实例
427
+      this.myChart = this.myChart ? this.myChart : this.$echarts.init(document.getElementById('trend'));
428
+      this.myChart.setOption(option);
429
+    },
430
+    // 监听点击图例
431
+    async onClickLegend(currentItem, index) {
432
+      const arr = this.legendList.filter(v => v.selectFlag)
433
+      if (arr.length == 1 && arr[0].key == currentItem.key) {
434
+        return this.$message.warning('至少存在一条曲线')
435
+      }
436
+      const item = _lodash.cloneDeep(currentItem)
437
+      item.selectFlag = !item.selectFlag
438
+      this.$set(this.legendList, index, item)
439
+      await this.$nextTick()
440
+      this.handleDrawChart()
441
+    },
442
+
443
+    onChangeCorpidSystem(val) {//二级联选择器
444
+      if (val.length < 1) {
445
+        this.enterprise = {}
446
+      } else {
447
+        this.enterpriseList.forEach((item) => {
448
+          item.manage_corp_list.forEach((item1) => {
449
+            if (item1.corpid == val[1]) {
450
+              this.enterprise = item1
451
+            }
452
+          })
453
+        })
454
+      }
455
+      this.filter.corpid = this.enterprise.corpid || ''
456
+      this.pagination.page = 1
457
+      this.handleGetList()
458
+    },
459
+    onChangeCorpid(val) {
460
+      if (!val) {
461
+        this.enterprise = {}
462
+      } else {
463
+        const res = this.enterpriseList.filter(v => v.corpid == val)[0];
464
+        this.enterprise = res || {}
465
+      }
466
+      this.filter.corpid = enterprise.corpid || ''
467
+      this.pagination.page = 1
468
+      this.handleGetList()
469
+    },
470
+    // 企业筛选初始化
471
+    handleInitCorpOptions() {
472
+      if (this.$cookie.getCookie('isSuperManage') == 1) {//系统管理员
473
+        const enterpriseList = this.$store.state.authorize_corpList;
474
+        enterpriseList.forEach(item => {//为了el-cascader更改props
475
+          item.self_id = item.group_id.toString();
476
+          item.self_name = item.group_name;
477
+          item.manage_corp_list.forEach(item1 => {
478
+            item1.self_id = item1.corpid;
479
+            item1.self_name = item1.corp_name;
480
+          })
481
+        });
482
+        this.enterpriseList = enterpriseList
483
+      } else {
484
+        this.enterpriseList = this.$store.state.authorize_corpList;
485
+      }
486
+    },
487
+
488
+    // 监听数据类型切换
489
+    onChangeOrderType() {
490
+      this.pagination.page = 1
491
+      this.handleGetList()
492
+    },
493
+    // 监听时间筛选变化
494
+    onChangeTime(time) {
495
+      this.filter.time = Array.isArray(time) ? time : []
496
+      this.pagination.page = 1
497
+      this.handleGetList()
498
+    },
499
+    // 监听“收益截止日期”筛选变化
500
+    onChangeClosingDate(val) {
501
+      this.filter.closing_date = val || ''
502
+      this.pagination.page = 1
503
+      this.handleGetList()
504
+    },
505
+    // 监听“推广类型”筛选变化
506
+    onChangePromotionType(val) {
507
+      this.filter.promotion_type = val || ''
508
+      this.pagination.page = 1
509
+      this.handleGetList()
510
+    },
511
+    // 监听当前页变化
512
+    handleCurrentChange(currentPage) {
513
+      this.pagination.page = currentPage
514
+      this.handleGetList()
515
+    },
516
+    // 监听点击"确定(搜索)"按钮
517
+    onClickSearch() {
518
+      this.pagination.page = 1
519
+      this.handleGetList()
520
+    },
521
+    // 监听排序变化
522
+    onClickSort(sort_field, sort_type) {
523
+      // sort_type:升序asc、降序desc
524
+      if (this.filter.sort_field === sort_field) {
525
+        if (this.filter.sort_type === sort_type) {
526
+          // 点击的是当前排序字段 && 是当前排序类型 => 重置 取消排序
527
+          this.filter.sort_field = ''
528
+          this.filter.sort_type = ''
529
+        } else {
530
+          // 点击的是当前排序字段 && 非当前排序类型 => 设置排序类型
531
+          this.filter.sort_type = sort_type
532
+        }
533
+      } else {
534
+        // 点击的不是当前排序字段 => 设置排序字段和类型
535
+        this.filter.sort_field = sort_field
536
+        this.filter.sort_type = sort_type
537
+      }
538
+      // 后端排序 => 获取最新数据
539
+      this.pagination.page = 1
540
+      this.handleGetList()
541
+    },
542
+    // 监听点击"重置"按钮
543
+    onClickReset() {
544
+      this.reset = !this.reset
545
+      this.system_enterprise = []
546
+      this.enterprise = {}
547
+      this.filter.order_type = orderTypeOptions.MP
548
+      this.filter.time = this.default_time
549
+      this.filter.corpid = ''
550
+      this.filter.closing_date = '',
551
+      this.filter.promotion_type = ''
552
+      this.filter.totalRoi = ['', '']
553
+      this.filter.sort_field = ''
554
+      this.filter.sort_type = ''
555
+      this.pagination.page = 1
556
+      this.handleGetList()
557
+    },
558
+    // 监听点击"导出"按钮
559
+    onClickExport() {
560
+      console.log('onClickExport => ')
561
+      if (!this.pagination.total) return this.$message.warning('暂无数据可导出')
562
+      this.loading = true
563
+      this.$axios.get(`${this.URL.BASEURL}${this.URL.statistics_reg_range_report_new}`, {
564
+        params: {
565
+          begin_date: this.filter.time[0],
566
+          end_date: this.filter.time[1],
567
+          total_roi_min: this.totalRoi[0],
568
+          total_roi_max: this.totalRoi[1],
569
+          page: 1,
570
+          page_size: this.$store.state.exportNumber,
571
+        }
572
+      }).then((res) => {
573
+        var res = res.data
574
+        this.loading = false
575
+        if (res && res.errno == 0) {
576
+          this.handleExport(res.rst.data.list)
577
+        } else if (res.errno != 4002) {
578
+          this.$message({
579
+            message: res.err,
580
+            type: "warning"
581
+          })
582
+        }
583
+      }).catch((err) => {
584
+        this.loading = false
585
+      });
586
+    },
587
+    // 执行导出逻辑
588
+    handleExport(data) {
589
+      let list = data;
590
+      let tHeader = this.summaryTableCol.map((v) => {
591
+        return v.name;
592
+      })
593
+      let filterVal = this.summaryTableCol.map((v) => {
594
+        return v.column
595
+      })
596
+      let excelDatas = [
597
+        {
598
+          tHeader: tHeader, // sheet表一头部
599
+          filterVal: filterVal, // 表一的数据字段
600
+          tableDatas: list, // 表一的整体json数据
601
+          sheetName: ''// 表一的sheet名字
602
+        }
603
+      ]
604
+      this.$exportOrder({ excelDatas, name: `账号数据趋势(导出时间:${this.$getDay(0)})` })
605
+    },
606
+    getHeaderCellStyle() {
607
+      return { backgroundColor: '#FFFFFF !important', border: 'none!important' }
608
+    },
609
+  }
610
+}
611
+</script>
612
+<style lang="scss" scoped>
613
+.screenBox {
614
+  position: relative;
615
+  background: #fff;
616
+  padding: 5px 20px;
617
+}
618
+.mt-10 {
619
+  margin-top: 10px;
620
+}
621
+.ml-10 {
622
+  margin-left: 10px;
623
+}
624
+.filter-wrap {
625
+  display: flex;
626
+  align-items: center;
627
+  flex-wrap: wrap;
628
+  padding-bottom: 10px;
629
+  & > div {
630
+    margin: 0 10px 10px 0;
631
+  }
632
+  & > button {
633
+    margin: -10px 0 0 10px;
634
+  }
635
+  .el-button+.el-button {
636
+    margin-left: 10px;
637
+  }
638
+}
639
+.trendBox {
640
+  background: #ffffff;
641
+  padding: 22px 23px;
642
+  position: relative;
643
+  .noData {
644
+    position: absolute;
645
+    top: 100px;
646
+    left: 0;
647
+    right: 0;
648
+    margin: auto;
649
+  }
650
+  .legendBox {
651
+    display: flex;
652
+    align-items: center;
653
+    .legendItem {
654
+      color: #333333;
655
+      font-size: 14px;
656
+      line-height: 20px;
657
+      display: flex;
658
+      align-items: center;
659
+      margin-right: 30px;
660
+      cursor: pointer;
661
+      user-select: none;
662
+    }
663
+  }
664
+}
665
+.summaryTable {
666
+  margin-top: 10px;
667
+}
668
+.detailsTable {
669
+  margin-top: 10px;
670
+}
671
+.sort-wrap {
672
+  display: flex;
673
+  flex-direction: column;
674
+  i {
675
+    cursor: pointer;
676
+    &.active {
677
+      color: #32B38A;
678
+    }
679
+  }
680
+  i:first-child {
681
+    margin-bottom: -3px;
682
+  }
683
+  i:last-child {
684
+    margin-top: -3px;
685
+  }
686
+}
687
+</style>

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

@@ -59,6 +59,8 @@ const accountTrends = () => import(/* webpackChunkName: 'accountTrends' */ '@/co
59 59
 const shortDramaTrends = () => import(/* webpackChunkName: 'shortDramaTrends' */ '@/components/dataBoard/shortDramaTrends.vue')
60 60
 // 数据看板 - 粉丝激活趋势
61 61
 const fansActiveTrends = () => import(/* webpackChunkName: 'fansActiveTrends' */ '@/components/dataBoard/fansActiveTrends.vue')
62
+// 数据看板 - 用户流失趋势
63
+const loseUserTrends = () => import(/* webpackChunkName: 'loseUserTrends' */ '@/components/dataBoard/loseUserTrends.vue')
62 64
 
63 65
 // name与菜单配置的页面路由一致
64 66
 // meta下isData:true为数据看板,否则为助手
@@ -464,6 +466,17 @@ export var allRouter = [
464 466
         }
465 467
       },
466 468
       {
469
+        path: 'loseUserTrends',
470
+        name: 'loseUserTrends',
471
+        component: loseUserTrends,
472
+        meta: {
473
+          keepAlive: false,
474
+          isLogin: true,
475
+          title: '用户流失趋势',
476
+          isData: true
477
+        }
478
+      },
479
+      {
467 480
         path: 'thePublic',
468 481
         name: 'thePublic',
469 482
         component: thePublic,