Browse Source

feat: 企微数据 - 平台推广链接 - "列表"前端页面

zhengxy 2 years ago
parent
commit
4315143756

+ 41 - 0
project/src/components/dataBoard/platformPromote/index.vue

@@ -0,0 +1,41 @@
1
+<template>
2
+  <div>
3
+    <div class="topTagBox flex">
4
+      <div class="left flex-align-center">
5
+        <div :class="['tagItem', type === 1 ? 'tagItem_active' : '']" @click="changeType(1)">平台推广链接</div>
6
+        <div :class="['tagItem', type === 2 ? 'tagItem_active' : '']" @click="changeType(2)">平台推广剧集</div>
7
+      </div>
8
+    </div>
9
+    <promoteLink v-if="type === 1" />
10
+    <promotePlaylet v-if="type === 2" />
11
+  </div>
12
+</template>
13
+
14
+<script>
15
+import promoteLink from './promoteLink.vue'
16
+import promotePlaylet from './promotePlaylet.vue'
17
+export default {
18
+  components: {
19
+    promoteLink,
20
+    promotePlaylet,
21
+  },
22
+  data() {
23
+    return{
24
+      type: 1,
25
+    }
26
+  },
27
+  methods:{
28
+    changeType(type){
29
+      this.type = type
30
+    },
31
+  },
32
+}
33
+</script>
34
+
35
+<style lang="scss" scoped>
36
+@import "@/style/list.scss";
37
+.tableInfo{
38
+  height: auto;
39
+  padding: 10px 20px;
40
+}
41
+</style>

+ 251 - 0
project/src/components/dataBoard/platformPromote/promoteLink.vue

@@ -0,0 +1,251 @@
1
+<template>
2
+  <div class="loseUserTrends-wrap" v-loading="pageLoading">
3
+    <div class="screenBox flex">
4
+      <!-- <div />
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
+      <selfChannel style="margin-right: 30px;margin-left: -30px;" :reset="reset" title="创建人" type='circleCreate' labelWidth @channelDefine="onChangeCreatorId" />
13
+      <div class="flex">
14
+        <el-button size="mini" type="primary" plain @click="onClickSearch">确定</el-button>
15
+        <el-button size="mini" plain @click="onClickReset">重置</el-button>
16
+      </div>
17
+    </div>
18
+    <!-- E 筛选区 -->
19
+
20
+    <!-- S 表 -->
21
+      <!-- S 明细表 detailsTable -->
22
+      <div v-loading="detailLoading">
23
+        <ux-grid class="detailsTable" ref="detailsTable" :border="false" @row-click="() => { return }" :header-cell-style="getHeaderCellStyle" show-footer-overflow="tooltip" show-overflow="tooltip" size="mini" :height="height">
24
+          <ux-table-column v-for="(item, idx) in detailsTableCol" :key="item.column + idx" :resizable="true" :field="item.column" :title="item.name" :min-width="item.min_width ? item.min_width : 140" :fixed="item.fixed ? item.fixed : ''" align="center">
25
+            <template #header>
26
+              <div class="flex-align-jus-center">
27
+                {{ item.name }}
28
+                <el-tooltip v-if="item.notes" :content="item.notes" placement="top">
29
+                  <div><i class="el-icon-question"></i></div>
30
+                </el-tooltip>
31
+                <div v-if="item.enable_to_sort" class="sort-wrap">
32
+                  <i class="el-icon-caret-top" :class="{ 'active': filter.sort_field === item.column && filter.sort_type === 'asc' }" @click="onClickSort(item.column, 'asc')" />
33
+                  <i class="el-icon-caret-bottom" :class="{ 'active': filter.sort_field === item.column && filter.sort_type === 'desc' }" @click="onClickSort(item.column, 'desc')" />
34
+                </div>
35
+              </div>
36
+            </template>
37
+            <template v-slot="{ row }">
38
+              <!-- 时间 -->
39
+              <div v-if="item.column === 'ref_date'">
40
+                <span>{{ row['expense_date'] }}</span><span v-if="row['expense_date_end']"> ~ {{ row['expense_date_end'] }}</span>
41
+              </div>
42
+              <!-- 其他 -->
43
+              <span v-else>{{ row[item.column] ? $formatNum(row[item.column]) : '-' }}</span>
44
+            </template>
45
+          </ux-table-column>
46
+        </ux-grid>
47
+        <div class="pagination" v-show="pagination.total > 0">
48
+          <el-pagination background :current-page="pagination.page" @current-change="handleCurrentChange" layout="prev, pager, next" :page-count='Number(pagination.pages)' />
49
+        </div>
50
+      </div>
51
+      <!-- E 明细表 detailsTable -->
52
+    <!-- E 表 -->
53
+  </div>
54
+</template>
55
+<script>
56
+import datePicker from '@/components/assembly/screen/datePicker.vue'
57
+import selfChannel from '@/components/assembly/screen/channel.vue'
58
+import _lodash from 'lodash'
59
+
60
+export default {
61
+  components: {
62
+    datePicker,
63
+    selfChannel,
64
+  },
65
+  data () {
66
+    const DEFAULT_TIME = [this.$getDay(-30, false), this.$getDay(0, false)]
67
+    return {
68
+      default_time: DEFAULT_TIME,
69
+      reset: false,
70
+      height: '',
71
+      pageLoading: false,
72
+
73
+      detailLoading: false,
74
+      detailsTableCol: [],
75
+      pagination: {
76
+        page: 1,
77
+        page_size: 20,
78
+        pages: 0,
79
+        total: 0,
80
+      },
81
+      filter: {
82
+        time: DEFAULT_TIME, // 自定义日期
83
+        creator_id: '', // 创建人
84
+        sort_field: 'paid', // 排序字段 - 默认值
85
+        sort_type: 'desc', // 升序/降序
86
+      },
87
+    }
88
+  },
89
+  created () {
90
+    this.initTableHeight()
91
+    this.handleGetList()
92
+  },
93
+  methods: {
94
+    initTableHeight() {
95
+      this.height = document.documentElement.clientHeight - 200 > 400 ? document.documentElement.clientHeight - 200 : 400
96
+    },
97
+    // 获取列表数据
98
+    async handleGetList() {
99
+      console.log('handleGetList => ',)
100
+      try {
101
+        this.detailLoading = true
102
+        const params = {
103
+          page: this.pagination.page,
104
+          page_size: this.pagination.page_size,
105
+          start_date: this.filter.time[0],
106
+          end_date: this.filter.time[1],
107
+          creator_id: this.filter.creator_id,
108
+          sort_field: this.filter.sort_field,
109
+          sort_type: this.filter.sort_type,
110
+        }
111
+        const url = `${this.URL.BASEURL}${this.URL.dataBoard_loseUser_account}`
112
+        const { data: res = {} } = await this.$axios.get(url, { params })
113
+        if (res && res.errno == 0) {
114
+          res.rst.extra[0].fixed = 'left' // 前2列固定左侧
115
+          res.rst.extra[1].fixed = 'left' // 前2列固定左侧
116
+          const detailsTableCol = []
117
+          res.rst.extra.forEach(item => {
118
+            if (item.name && item.name.length > 6) { // 长字符宽度
119
+              item['min_width'] = item.name.length * 20
120
+            }
121
+            detailsTableCol.push(item) // 收集普通表头
122
+          })
123
+          this.detailsTableCol = Object.freeze(detailsTableCol)
124
+          await this.$nextTick()
125
+          const detailsTableList = Array.isArray(res.rst.data) ? res.rst.data : []
126
+          this.$refs.detailsTable.reloadData(detailsTableList)
127
+          this.pagination.total = res.rst.pageInfo.total
128
+          this.pagination.pages = res.rst.pageInfo.pages
129
+        } else if (res.errno != 4002) {
130
+          this.$message.warning(res.err)
131
+          this.$refs.detailsTable.reloadData([])
132
+          this.pagination.total = 0
133
+          this.pagination.pages = 0
134
+        }
135
+      } catch (error) {
136
+        console.log(error)
137
+        this.$refs.detailsTable.reloadData([])
138
+        this.pagination.total = 0
139
+        this.pagination.pages = 0
140
+      } finally {
141
+        this.detailLoading = false
142
+      }
143
+    },
144
+    // 监听时间筛选变化
145
+    onChangeTime(time) {
146
+      this.filter.time = Array.isArray(time) ? time : []
147
+      this.pagination.page = 1
148
+      this.handleGetList()
149
+    },
150
+    // 监听创建人筛选变化
151
+    onChangeCreatorId(val) {
152
+      this.filter.creator_id = val
153
+      this.pagination.page = 1
154
+      this.handleGetList()
155
+    },
156
+    // 监听当前页变化
157
+    handleCurrentChange(currentPage) {
158
+      this.pagination.page = currentPage
159
+      this.handleGetList()
160
+    },
161
+    // 监听点击"确定(搜索)"按钮
162
+    onClickSearch() {
163
+      this.pagination.page = 1
164
+      this.handleGetList()
165
+    },
166
+    // 监听排序变化
167
+    onClickSort(sort_field, sort_type) {
168
+      // sort_type:升序asc、降序desc
169
+      if (this.filter.sort_field === sort_field) {
170
+        if (this.filter.sort_type === sort_type) {
171
+          // 点击的是当前排序字段 && 是当前排序类型 => 重置 取消排序
172
+          this.filter.sort_field = 'paid'
173
+          this.filter.sort_type = 'desc'
174
+        } else {
175
+          // 点击的是当前排序字段 && 非当前排序类型 => 设置排序类型
176
+          this.filter.sort_type = sort_type
177
+        }
178
+      } else {
179
+        // 点击的不是当前排序字段 => 设置排序字段和类型
180
+        this.filter.sort_field = sort_field
181
+        this.filter.sort_type = sort_type
182
+      }
183
+      // 后端排序 => 获取最新数据
184
+      this.pagination.page = 1
185
+      this.handleGetList()
186
+    },
187
+    // 监听点击"重置"按钮
188
+    onClickReset() {
189
+      this.reset = !this.reset
190
+      this.filter.time = this.default_time
191
+      this.filter.creator_id = ''
192
+      this.filter.sort_field = 'paid'
193
+      this.filter.sort_type = 'desc'
194
+      this.pagination.page = 1
195
+      this.handleGetList()
196
+    },
197
+    getHeaderCellStyle() {
198
+      return { backgroundColor: '#FFFFFF !important', border: 'none!important' }
199
+    },
200
+  }
201
+}
202
+</script>
203
+<style lang="scss" scoped>
204
+.loseUserTrends-wrap {
205
+  min-width: 1125px;
206
+}
207
+.screenBox {
208
+  position: relative;
209
+  background: #fff;
210
+  padding: 5px 20px;
211
+}
212
+.mt-10 {
213
+  margin-top: 10px;
214
+}
215
+.ml-10 {
216
+  margin-left: 10px;
217
+}
218
+.filter-wrap {
219
+  display: flex;
220
+  align-items: center;
221
+  flex-wrap: wrap;
222
+  padding-bottom: 10px;
223
+  & > div {
224
+    margin: 0 10px 10px 0;
225
+  }
226
+  & > button {
227
+    margin: -10px 0 0 10px;
228
+  }
229
+  .el-button+.el-button {
230
+    margin-left: 10px;
231
+  }
232
+}
233
+
234
+
235
+.sort-wrap {
236
+  display: flex;
237
+  flex-direction: column;
238
+  i {
239
+    cursor: pointer;
240
+    &.active {
241
+      color: #32B38A;
242
+    }
243
+  }
244
+  i:first-child {
245
+    margin-bottom: -3px;
246
+  }
247
+  i:last-child {
248
+    margin-top: -3px;
249
+  }
250
+}
251
+</style>

+ 783 - 0
project/src/components/dataBoard/platformPromote/promotePlaylet.vue

@@ -0,0 +1,783 @@
1
+<template>
2
+  <div class="loseUserTrends-wrap" v-loading="pageLoading">
3
+    <div class="screenBox flex">
4
+      <div /><!-- 占位 -->
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
+      <div class="flex">
12
+        <!-- 企微主体 -->
13
+        <div style="margin-right: 30px;" class="common-screen-item">
14
+          <label class="common-screen-label" style="width: auto;">企微主体</label>
15
+          <!-- 系统管理员 -->
16
+          <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="请选择企微主体" />
17
+          <!-- 非系统管理员 -->
18
+          <el-select v-else v-model="enterprise.corpid" size="small" placeholder="请选择企微主体" @change="onChangeCorpid" clearable>
19
+            <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" />
20
+          </el-select>
21
+        </div>
22
+        <el-button size="mini" type="primary" plain @click="onClickSearch">确定</el-button>
23
+        <el-button size="mini" plain @click="onClickReset">重置</el-button>
24
+      </div>
25
+    </div>
26
+    <!-- E 筛选区 -->
27
+    <!-- S 折线图 -->
28
+    <div class="trendBox mt-10" v-loading="chartLoading">
29
+      <div class="legendBox">
30
+        <div class="legendItem" v-for="(item, index) in legendList" :key="index" @click="onClickLegend(item, index)">{{ item.name }}
31
+          <div :class="['checkbox', item.selectFlag ? 'checkbox_active': '']" :style="item.selectFlag ? `background: ${item.color};border-color: ${item.color};`: '' "><i class="el-icon-check" /></div>
32
+        </div>
33
+      </div>
34
+      <div id="trend" style="width: 100%; height: 250px;" />
35
+      <div v-if="!chartDataList || !chartDataList.length" style="text-align: center; color: #333; font-size: 14px; position: relative; top: -100px;">暂无数据</div>
36
+    </div>
37
+    <!-- E 折线图 -->
38
+
39
+    <!-- S 表 -->
40
+
41
+      <!-- S 汇总表 summaryTable -->
42
+      <div v-loading="summaryLoading">
43
+        <ux-grid class="summaryTable" ref="summaryTable" :border="false" @row-click="() => { return }" :header-cell-style="getHeaderCellStyle" show-footer-overflow="tooltip" show-overflow="tooltip" size="mini">
44
+          <ux-table-column v-for="item in summaryTableCol" :key="item.column + item.name" :resizable="true" :field="item.column" :title="item.name" :min-width="item.min_width ? item.min_width : 140" :fixed="item.fixed ? item.fixed : ''" align="center">
45
+            <template #header>
46
+              <div class="flex-align-jus-center">
47
+                {{ item.name }}
48
+                <el-tooltip v-if="item.notes" :content="item.notes" placement="top">
49
+                  <div><i class="el-icon-question"></i></div>
50
+                </el-tooltip>
51
+              </div>
52
+            </template>
53
+            <template v-slot="{ row }">
54
+              <!-- 时间 -->
55
+              <div v-if="item.name === '时间' || item.name === '企微主体'">
56
+                <span>汇总</span>
57
+              </div>
58
+              <span v-else>{{ row[item.column] ? $formatNum(row[item.column]) : '-' }}</span>
59
+            </template>
60
+          </ux-table-column>
61
+        </ux-grid>
62
+      </div>
63
+      <!-- E 汇总表 summaryTable -->
64
+      <!-- S 明细表 detailsTable -->
65
+      <div v-loading="detailLoading">
66
+        <ux-grid class="detailsTable" ref="detailsTable" :border="false" @row-click="() => { return }" :header-cell-style="getHeaderCellStyle" show-footer-overflow="tooltip" show-overflow="tooltip" size="mini" :height="height">
67
+          <ux-table-column v-for="(item, idx) in detailsTableCol" :key="item.column + idx" :resizable="true" :field="item.column" :title="item.name" :min-width="item.min_width ? item.min_width : 140" :fixed="item.fixed ? item.fixed : ''" align="center">
68
+            <template #header>
69
+              <div class="flex-align-jus-center">
70
+                {{ item.name }}
71
+                <el-tooltip v-if="item.notes" :content="item.notes" placement="top">
72
+                  <div><i class="el-icon-question"></i></div>
73
+                </el-tooltip>
74
+                <div v-if="item.enable_to_sort" class="sort-wrap">
75
+                  <i class="el-icon-caret-top" :class="{ 'active': filter.sort_field === item.column && filter.sort_type === 'asc' }" @click="onClickSort(item.column, 'asc')" />
76
+                  <i class="el-icon-caret-bottom" :class="{ 'active': filter.sort_field === item.column && filter.sort_type === 'desc' }" @click="onClickSort(item.column, 'desc')" />
77
+                </div>
78
+              </div>
79
+            </template>
80
+            <template v-slot="{ row }">
81
+              <!-- 时间 -->
82
+              <div v-if="item.column === 'ref_date'">
83
+                <span>{{ row['expense_date'] }}</span><span v-if="row['expense_date_end']"> ~ {{ row['expense_date_end'] }}</span>
84
+              </div>
85
+              <!-- 其他 -->
86
+              <span v-else>{{ row[item.column] ? $formatNum(row[item.column]) : '-' }}</span>
87
+            </template>
88
+          </ux-table-column>
89
+          <!-- DAY1、DAY2... -->
90
+          <ux-table-column width="120" v-for="(dayItem, dayIdx) in detailsTableColDays" :key="dayIdx + 'extra'">
91
+            <template #header>
92
+              <div class="flex-align-jus-center">{{ dayItem.name }}
93
+                <el-tooltip placement="top" v-if="dayIdx === 0">
94
+                  <div slot="content">流失人数:这天的流失人数<br />流失倍率:累计流失人数/第一天流失人数</div>
95
+                  <div><i class="el-icon-question" /></div>
96
+                </el-tooltip>
97
+              </div>
98
+            </template>
99
+            <template v-slot="{ row }">
100
+              <div>
101
+                <span class="font" style="color:#2C9841">流失人数:</span>
102
+                <span>{{ row && row.day_info && row.day_info[dayIdx] && row.day_info[dayIdx].loss_count ?
103
+                    row.day_info[dayIdx].loss_count : '-'
104
+                }}</span>
105
+              </div>
106
+              <div>
107
+                <span class="font" style="color:#EB4315">流失倍率:</span>
108
+                <span>{{ row && row.day_info && row.day_info[dayIdx] && row.day_info[dayIdx].loss_rate ?
109
+                    row.day_info[dayIdx].loss_rate : '-'
110
+                }}</span>
111
+              </div>
112
+            </template>
113
+          </ux-table-column>
114
+        </ux-grid>
115
+        <div class="pagination" v-show="pagination.total > 0">
116
+          <el-pagination background :current-page="pagination.page" @current-change="handleCurrentChange" layout="prev, pager, next" :page-count='Number(pagination.pages)' />
117
+        </div>
118
+      </div>
119
+      <!-- E 明细表 detailsTable -->
120
+    <!-- E 表 -->
121
+  </div>
122
+</template>
123
+<script>
124
+import datePicker from '@/components/assembly/screen/datePicker.vue'
125
+import _lodash from 'lodash'
126
+
127
+export default {
128
+  components: { datePicker },
129
+  data () {
130
+    const DEFAULT_TIME = [this.$getDay(-30, false), this.$getDay(0, false)]
131
+    return {
132
+      default_time: DEFAULT_TIME,
133
+      reset: false,
134
+      height: '',
135
+      pageLoading: false,
136
+
137
+      system_enterprise: [], // 企微主体数据
138
+      enterpriseList: [], // 企微主体数据
139
+      enterprise: {}, // 当前选择的企微信息
140
+
141
+      chartLoading: false,
142
+      myChart: null,
143
+      chartDataList: [],
144
+      legendList: [
145
+        { name: '消耗', key: 'paid', color: '#2983DF', selectFlag: true },
146
+        { name: '3天流失人数', key: 'third_loss_count', color: '#EB4315', selectFlag: true },
147
+        { name: '7天流失人数', key: 'seventh_loss_count', color: '#84CDFC', selectFlag: true },
148
+        { name: '15天流失人数', key: 'fifteenth_loss_count', color: '#00B38A', selectFlag: true },
149
+        { name: '30天流失人数', key: 'thirtieth_loss_count', color: '#AED570', selectFlag: true },
150
+        { name: '累计流失人数', key: 'loss_count', color: '#7366FF', selectFlag: true },
151
+      ],
152
+      percentFields: Object.freeze([]),
153
+      // percentFields: Object.freeze(['首日roi', '3天倍率', '7天倍率', '15天倍率', '30天倍率']),
154
+
155
+      summaryLoading: false,
156
+      summaryTableCol: [],
157
+      detailLoading: false,
158
+      detailsTableCol: [],
159
+      detailsTableColDays: [],
160
+      pagination: {
161
+        page: 1,
162
+        page_size: 20,
163
+        pages: 0,
164
+        total: 0,
165
+      },
166
+      filter: {
167
+        time: DEFAULT_TIME, // 自定义日期
168
+        corpid: '', // 企微主体
169
+        sort_field: 'paid', // 排序字段 - 默认值
170
+        sort_type: 'desc', // 升序/降序
171
+      },
172
+    }
173
+  },
174
+  computed: {
175
+    // 当前"时间"列是否展示的为"时间段" => 是时间段 默认排序"消耗"、非时间段 默认排序时间
176
+    isShowTimes() {
177
+      // (企微主体)
178
+      if (this.filter.corpid) {
179
+        return false
180
+      } else {
181
+        return true
182
+      }
183
+    },
184
+  },
185
+  created () {
186
+    this.initTableHeight()
187
+    this.handleInitCorpOptions()
188
+    this.handleGetChart()
189
+    this.handleGetSummaryList()
190
+    this.handleGetList()
191
+  },
192
+  beforeDestroy () {
193
+    this.myChart && this.myChart.clear()
194
+  },
195
+  methods: {
196
+    initTableHeight() {
197
+      this.height = document.documentElement.clientHeight - 200 > 400 ? document.documentElement.clientHeight - 200 : 400
198
+    },
199
+    // 获取汇总表数据
200
+    async handleGetSummaryList() {
201
+      console.log('handleGetSummaryList => ',)
202
+      try {
203
+        this.summaryLoading = true
204
+        const params = {
205
+          start_date: this.filter.time[0],
206
+          end_date: this.filter.time[1],
207
+          corpid: this.filter.corpid,
208
+        }
209
+        const url = `${this.URL.BASEURL}${this.URL.dataBoard_loseUser_summary}`
210
+        const { data: res = {} } = await this.$axios.get(url, { params })
211
+        if (res && res.errno == 0) {
212
+          // res.rst.header[0].fixed = 'left' // 前2列固定左侧
213
+          // res.rst.header[1].fixed = 'left' // 前2列固定左侧
214
+          const summaryTableCol = []
215
+          res.rst.header.forEach(item => {
216
+            if (item.name && item.name.length > 6) { // 长字符宽度
217
+              item['min_width'] = item.name.length * 20
218
+            }
219
+            summaryTableCol.push(item) // 收集普通表头
220
+          })
221
+          this.summaryTableCol = Object.freeze(summaryTableCol)
222
+          await this.$nextTick()
223
+          const summaryTableList = Array.isArray(res.rst.data) ? res.rst.data : [res.rst.data]
224
+          this.$refs.summaryTable.reloadData(summaryTableList)
225
+        } else if (res.errno != 4002) {
226
+          this.$message.warning(res.err)
227
+          this.$refs.summaryTable.reloadData([])
228
+        }
229
+      } catch (error) {
230
+        console.log(error)
231
+        this.$refs.summaryTable.reloadData([])
232
+      } finally {
233
+        this.summaryLoading = false
234
+      }
235
+    },
236
+    // 获取列表数据
237
+    async handleGetList() {
238
+      console.log('handleGetList => ',)
239
+      try {
240
+        this.detailLoading = true
241
+        const params = {
242
+          corpid: this.filter.corpid,
243
+          page: this.pagination.page,
244
+          page_size: this.pagination.page_size,
245
+          start_date: this.filter.time[0],
246
+          end_date: this.filter.time[1],
247
+          sort_field: this.filter.sort_field,
248
+          sort_type: this.filter.sort_type,
249
+        }
250
+        const url = `${this.URL.BASEURL}${this.URL.dataBoard_loseUser_account}`
251
+        const { data: res = {} } = await this.$axios.get(url, { params })
252
+        if (res && res.errno == 0) {
253
+          res.rst.extra[0].fixed = 'left' // 前2列固定左侧
254
+          res.rst.extra[1].fixed = 'left' // 前2列固定左侧
255
+          const detailsTableCol = []
256
+          const detailsTableColDays = []
257
+          res.rst.extra.forEach(item => {
258
+            if (item.name.includes('DAY')) { // 收集 DAY1、DAY2...表头
259
+              detailsTableColDays.push(item)
260
+            } else { // 普通表头
261
+              if (item.name && item.name.length > 6) { // 长字符宽度
262
+                item['min_width'] = item.name.length * 20
263
+              }
264
+              if (item.column === 'ref_date') { // 时间(日期)列宽度
265
+                item['min_width'] = this.isShowTimes ? 200 : 140
266
+              }
267
+              detailsTableCol.push(item) // 收集普通表头
268
+            }
269
+          })
270
+          this.detailsTableCol = Object.freeze(detailsTableCol)
271
+          this.detailsTableColDays = Object.freeze(detailsTableColDays)
272
+          await this.$nextTick()
273
+          const detailsTableList = Array.isArray(res.rst.data) ? res.rst.data : []
274
+          this.$refs.detailsTable.reloadData(detailsTableList)
275
+          this.pagination.total = res.rst.pageInfo.total
276
+          this.pagination.pages = res.rst.pageInfo.pages
277
+        } else if (res.errno != 4002) {
278
+          this.$message.warning(res.err)
279
+          this.$refs.detailsTable.reloadData([])
280
+          this.pagination.total = 0
281
+          this.pagination.pages = 0
282
+        }
283
+      } catch (error) {
284
+        console.log(error)
285
+        this.$refs.detailsTable.reloadData([])
286
+        this.pagination.total = 0
287
+        this.pagination.pages = 0
288
+      } finally {
289
+        this.detailLoading = false
290
+      }
291
+    },
292
+    // 获取折线图数据
293
+    async handleGetChart() {
294
+      console.log('handleGetChart => ')
295
+      try {
296
+        this.chartLoading = true
297
+        const params = {
298
+          start_date: this.filter.time[0],
299
+          end_date: this.filter.time[1],
300
+          corpid: this.filter.corpid,
301
+        }
302
+        const url = `${this.URL.BASEURL}${this.URL.dataBoard_loseUser_curve}`
303
+        const { data: res = {} } = await this.$axios.get(url, { params })
304
+        if (res && res.errno == 0) {
305
+          this.chartDataList = Array.isArray(res.rst.data) ? res.rst.data : []
306
+          await this.$nextTick()
307
+          this.handleDrawChart()
308
+        } else if (res.errno != 4002) {
309
+          this.$message.warning(res.err)
310
+          this.chartDataList = []
311
+          await this.$nextTick()
312
+          this.myChart && this.myChart.clear()
313
+        }
314
+      } catch (error) {
315
+        console.log(error)
316
+        this.chartDataList = []
317
+        await this.$nextTick()
318
+        this.myChart && this.myChart.clear()
319
+      } finally {
320
+        this.chartLoading = false
321
+      }
322
+    },
323
+    // 绘制折线图
324
+    handleDrawChart() {
325
+      this.myChart && this.myChart.clear()
326
+      const _this = this;
327
+      const series = []
328
+      const yAxis = []
329
+      const xArr = this.chartDataList.map(v => v.expense_date)
330
+      this.legendList.forEach(item => {
331
+        if (item.selectFlag) {
332
+          yAxis.push({
333
+            type: "value",
334
+            name: '',
335
+            show: false,
336
+            position: 'left',
337
+            axisTick: {
338
+              show: false
339
+            },
340
+            splitLine: {
341
+              lineStyle: {
342
+                color: '#F2F2f2',
343
+                type: "dashed"
344
+              }
345
+            },
346
+            axisLine: {
347
+              show: true,
348
+              lineStyle: {
349
+                color: '#F2F2f2'
350
+              }
351
+            },
352
+            nameTextStyle: {
353
+              color: "#999999",
354
+              fontSize: 13,
355
+            },
356
+            axisLabel: {
357
+              color: '#999999',
358
+              fontSize: 12,
359
+              show: true,
360
+              formatter: function(params) {
361
+                if (_this.percentFields.includes(item.name)) { // 百分比字段
362
+                  return `${_this.$formatNum(params)}%`
363
+                } else {
364
+                  return _this.$NumberHandle(params)
365
+                }
366
+              }
367
+            }
368
+          })
369
+
370
+          const data = this.chartDataList.map(v => v[item.key] || v[item.key] == 0 ? v[item.key] : '-')
371
+
372
+          series.push({
373
+            type: "line",
374
+            smooth: true,
375
+            name: item.name,
376
+            yAxisIndex: 0,
377
+            data,
378
+            lineStyle: {
379
+              width: 2
380
+            },
381
+            symbol: xArr.length == 1 ? 'emptyCircle' : 'none',
382
+            itemStyle: {
383
+              color: item.color,
384
+              borderType: "emptyCircle"
385
+            },
386
+          })
387
+        }
388
+      })
389
+      yAxis.forEach((item, index) => {
390
+        item.show = index == 0 ? true : false
391
+      })
392
+      series.forEach((item, index) => {
393
+        item.yAxisIndex = index
394
+      })
395
+
396
+      const option = {
397
+        title: '',
398
+        tooltip: {
399
+          trigger: 'axis',
400
+          show: true,
401
+          formatter: function(params) {
402
+            let result = `${params[0].name}<br/>`
403
+            params.forEach(item => {
404
+              if (_this.percentFields.includes(item.seriesName)) { // 百分比字段
405
+                result += `${item.marker}${item.seriesName}:${_this.$formatNum(item.value)}%<br/>`
406
+              } else {
407
+                result += `${item.marker}${item.seriesName}:${_this.$formatNum(item.value)}<br/>`
408
+              }
409
+            });
410
+            return result;
411
+          }
412
+        },
413
+        legend: {
414
+          itemWidth: 8,
415
+          itemHeight: 2,
416
+          icon: "plain",
417
+          show: false,
418
+          textStyle: {
419
+            fontSize: 12,
420
+            color: '#666666'
421
+          },
422
+        },
423
+        grid: {
424
+          top: '6%',
425
+          left: '5%',
426
+          right: '4%',
427
+          bottom: '16%',
428
+          containLabel: false
429
+        },
430
+        xAxis: [{
431
+          type: "category",
432
+          data: xArr,
433
+          boundaryGap: false,//设置数据从头开始
434
+          axisLine: {
435
+            show: true,
436
+            lineStyle: {
437
+              color: '#F2F2f2'
438
+            }
439
+          },
440
+          axisTick: {
441
+            show: false
442
+          },
443
+          splitLine: {
444
+            show: false
445
+          },
446
+          axisLabel: {
447
+            color: '#666',
448
+            fontSize: 10,
449
+            rotate: 30,
450
+          },
451
+        }],
452
+        yAxis,
453
+        series,
454
+      }
455
+      // 初始化echarts实例
456
+      this.myChart = this.myChart ? this.myChart : this.$echarts.init(document.getElementById('trend'));
457
+      this.myChart.setOption(option);
458
+    },
459
+    // 监听点击图例
460
+    async onClickLegend(currentItem, index) {
461
+      const arr = this.legendList.filter(v => v.selectFlag)
462
+      if (arr.length == 1 && arr[0].key == currentItem.key) {
463
+        return this.$message.warning('至少存在一条曲线')
464
+      }
465
+      const item = _lodash.cloneDeep(currentItem)
466
+      item.selectFlag = !item.selectFlag
467
+      this.$set(this.legendList, index, item)
468
+      await this.$nextTick()
469
+      this.handleDrawChart()
470
+    },
471
+
472
+    // S 企微主体数据
473
+    onChangeCorpidSystem(val) {//二级联选择器
474
+      if (val.length < 1) {
475
+        this.enterprise = {}
476
+      } else {
477
+        this.enterpriseList.forEach((item) => {
478
+          item.manage_corp_list.forEach((item1) => {
479
+            if (item1.corpid == val[1]) {
480
+              this.enterprise = item1
481
+            }
482
+          })
483
+        })
484
+      }
485
+      this.filter.corpid = this.enterprise.corpid || ''
486
+      this.handleGetChart()
487
+      this.handleGetSummaryList()
488
+      this.filter.sort_field = this.isShowTimes ? 'paid' : 'expense_date'
489
+      this.filter.sort_type = 'desc'
490
+      this.pagination.page = 1
491
+      this.handleGetList()
492
+    },
493
+    onChangeCorpid(val) {
494
+      if (!val) {
495
+        this.enterprise = {}
496
+      } else {
497
+        const res = this.enterpriseList.filter(v => v.corpid == val)[0];
498
+        this.enterprise = res || {}
499
+      }
500
+      this.filter.corpid = enterprise.corpid || ''
501
+      this.handleGetChart()
502
+      this.handleGetSummaryList()
503
+      this.filter.sort_field = this.isShowTimes ? 'paid' : 'expense_date'
504
+      this.filter.sort_type = 'desc'
505
+      this.pagination.page = 1
506
+      this.handleGetList()
507
+    },
508
+    // 企业筛选初始化
509
+    handleInitCorpOptions() {
510
+      if (this.$cookie.getCookie('isSuperManage') == 1) {//系统管理员
511
+        const enterpriseList = this.$store.state.authorize_corpList;
512
+        enterpriseList.forEach(item => {//为了el-cascader更改props
513
+          item.self_id = item.group_id.toString();
514
+          item.self_name = item.group_name;
515
+          item.manage_corp_list.forEach(item1 => {
516
+            item1.self_id = item1.corpid;
517
+            item1.self_name = item1.corp_name;
518
+          })
519
+        });
520
+        this.enterpriseList = enterpriseList
521
+      } else {
522
+        this.enterpriseList = this.$store.state.authorize_corpList;
523
+      }
524
+    },
525
+    // E 企微主体数据
526
+
527
+    // 监听时间筛选变化
528
+    onChangeTime(time) {
529
+      this.filter.time = Array.isArray(time) ? time : []
530
+      this.handleGetChart()
531
+      this.handleGetSummaryList()
532
+      this.pagination.page = 1
533
+      this.handleGetList()
534
+    },
535
+    // 监听当前页变化
536
+    handleCurrentChange(currentPage) {
537
+      this.handleGetChart()
538
+      this.handleGetSummaryList()
539
+      this.pagination.page = currentPage
540
+      this.handleGetList()
541
+    },
542
+    // 监听点击"确定(搜索)"按钮
543
+    onClickSearch() {
544
+      this.handleGetChart()
545
+      this.handleGetSummaryList()
546
+      this.pagination.page = 1
547
+      this.handleGetList()
548
+    },
549
+    // 监听排序变化
550
+    onClickSort(sort_field, sort_type) {
551
+      // sort_type:升序asc、降序desc
552
+      if (this.filter.sort_field === sort_field) {
553
+        if (this.filter.sort_type === sort_type) {
554
+          // 点击的是当前排序字段 && 是当前排序类型 => 重置 取消排序
555
+          this.filter.sort_field = this.isShowTimes ? 'paid' : 'expense_date'
556
+          this.filter.sort_type = 'desc'
557
+        } else {
558
+          // 点击的是当前排序字段 && 非当前排序类型 => 设置排序类型
559
+          this.filter.sort_type = sort_type
560
+        }
561
+      } else {
562
+        // 点击的不是当前排序字段 => 设置排序字段和类型
563
+        this.filter.sort_field = sort_field
564
+        this.filter.sort_type = sort_type
565
+      }
566
+      // 后端排序 => 获取最新数据
567
+      this.pagination.page = 1
568
+      this.handleGetList()
569
+      // 获取其他表数据
570
+      this.handleGetChart()
571
+      this.handleGetSummaryList()
572
+    },
573
+    // 监听点击"重置"按钮
574
+    onClickReset() {
575
+      this.reset = !this.reset
576
+      this.filter.time = this.default_time
577
+      this.system_enterprise = []
578
+      this.enterprise = {}
579
+      this.filter.corpid = ''
580
+      this.filter.sort_field = 'paid'
581
+      this.filter.sort_type = 'desc'
582
+      this.pagination.page = 1
583
+      this.handleGetChart()
584
+      this.handleGetSummaryList()
585
+      this.handleGetList()
586
+    },
587
+    // 监听点击"导出"按钮
588
+    async onClickExport() {
589
+      console.log('onClickExport => ')
590
+      if (!this.pagination.total) return this.$message.warning('暂无数据可导出')
591
+      try {
592
+        this.pageLoading = true
593
+        const url = {
594
+          summary: '',
595
+          detail: '',
596
+        }
597
+        const params = {
598
+          start_date: this.filter.time[0],
599
+          end_date: this.filter.time[1],
600
+          corpid: this.filter.corpid,
601
+        }
602
+
603
+        url.summary = `${this.URL.BASEURL}${this.URL.dataBoard_loseUser_summary}`
604
+        url.detail = `${this.URL.BASEURL}${this.URL.dataBoard_loseUser_account}`
605
+
606
+        const [{ data: summaryRes = {} }, { data: detailRes = {} }] = await Promise.all([
607
+          this.$axios.get(url.summary, { params }),
608
+          this.$axios.get(url.detail, {
609
+            params: {
610
+              ...params,
611
+              sort_field: this.filter.sort_field,
612
+              sort_type: this.filter.sort_type,
613
+              page: 1,
614
+              page_size: this.$store.state.exportNumber,
615
+            }
616
+          })
617
+        ])
618
+        if ((summaryRes && summaryRes.errno == 0) && (detailRes && detailRes.errno == 0)) {
619
+          this.handleExport({
620
+            summaryData: summaryRes.rst,
621
+            detailData: detailRes.rst,
622
+          })
623
+        } else if (summaryRes.errno != 4002 || detailRes.errno != 4002) {
624
+          this.$message.warning(summaryRes.err)
625
+          this.$message.warning(detailRes.err)
626
+        }
627
+      } catch (error) {
628
+        console.log(error)
629
+        this.$message.warning('导出失败,请重试')
630
+      } finally {
631
+        this.pageLoading = false
632
+      }
633
+    },
634
+    // 执行导出逻辑
635
+    handleExport({ summaryData = {}, detailData = {} }) {
636
+      let daysData = []
637
+      let tHeader = []
638
+      let filterVal = []
639
+      let tableDatas = []
640
+
641
+      daysData = detailData.extra.filter(item => item.name.includes('DAY'))
642
+
643
+      summaryData.header.forEach(h => {
644
+        if (h.name === '时间') {
645
+          h.column = 'ref_date'
646
+        } else if (h.name === '企微主体') {
647
+          h.column = 'corp_name'
648
+        }
649
+      })
650
+
651
+      summaryData.data['ref_date'] = '汇总'
652
+      summaryData.data['corp_name'] = '汇总'
653
+      detailData.data.forEach((item) => {
654
+        item.ref_date = `${item.expense_date}${item.expense_date_end ? (' ~ ' + item.expense_date_end) : ''}`
655
+        // item.corp_name = item.corp_name
656
+        daysData.forEach((day, idx) => {
657
+          item[day.column] = `流失人数: ${item && item.day_info && item.day_info[idx] && (item.day_info[idx].loss_count || item.day_info[idx].loss_count == 0) ? item.day_info[idx].loss_count : '-'}  流失倍率: ${item && item.day_info && item.day_info[idx] && (item.day_info[idx].loss_rate || item.day_info[idx].loss_rate == 0) ? item.day_info[idx].loss_rate : '-'}`
658
+        })
659
+      })
660
+
661
+      tHeader = [
662
+        ...summaryData.header.map(h => h.name),
663
+        ...daysData.map(d => d.name),
664
+      ]
665
+
666
+      filterVal = [
667
+        ...summaryData.header.map(h => h.column),
668
+        ...daysData.map(d => d.column),
669
+      ]
670
+
671
+      tableDatas = [
672
+        summaryData.data,
673
+        ...detailData.data,
674
+      ]
675
+
676
+      const excelDatas = [
677
+        {
678
+          tHeader, // sheet表一头部
679
+          filterVal, // 表一的数据字段
680
+          tableDatas, // 表一的整体json数据
681
+          sheetName: ''// 表一的sheet名字
682
+        }
683
+      ]
684
+      this.$exportOrder({ excelDatas, name: `用户流失趋势(导出时间:${this.$getDay(0)})` })
685
+    },
686
+    getHeaderCellStyle() {
687
+      return { backgroundColor: '#FFFFFF !important', border: 'none!important' }
688
+    },
689
+  }
690
+}
691
+</script>
692
+<style lang="scss">
693
+.loseUserTrends-wrap .detailsTable {
694
+  .elx-table.elx-editable.size--mini .elx-body--column,
695
+  .elx-table.size--mini .elx-body--column.col--ellipsis,
696
+  .elx-table.size--mini .elx-footer--column.col--ellipsis {
697
+    height: 120px !important;
698
+  }
699
+  .elx-table.size--mini .elx-body--column.col--ellipsis>.elx-cell,
700
+  .elx-table.size--mini .elx-footer--column.col--ellipsis>.elx-cell {
701
+    max-height: initial !important;
702
+  }
703
+}
704
+</style>
705
+<style lang="scss" scoped>
706
+.loseUserTrends-wrap {
707
+  min-width: 1125px;
708
+}
709
+.screenBox {
710
+  position: relative;
711
+  background: #fff;
712
+  padding: 5px 20px;
713
+}
714
+.mt-10 {
715
+  margin-top: 10px;
716
+}
717
+.ml-10 {
718
+  margin-left: 10px;
719
+}
720
+.filter-wrap {
721
+  display: flex;
722
+  align-items: center;
723
+  flex-wrap: wrap;
724
+  padding-bottom: 10px;
725
+  & > div {
726
+    margin: 0 10px 10px 0;
727
+  }
728
+  & > button {
729
+    margin: -10px 0 0 10px;
730
+  }
731
+  .el-button+.el-button {
732
+    margin-left: 10px;
733
+  }
734
+}
735
+.trendBox {
736
+  background: #ffffff;
737
+  padding: 22px 23px;
738
+  position: relative;
739
+  .noData {
740
+    position: absolute;
741
+    top: 100px;
742
+    left: 0;
743
+    right: 0;
744
+    margin: auto;
745
+  }
746
+  .legendBox {
747
+    display: flex;
748
+    align-items: center;
749
+    .legendItem {
750
+      color: #333333;
751
+      font-size: 14px;
752
+      line-height: 20px;
753
+      display: flex;
754
+      align-items: center;
755
+      margin-right: 30px;
756
+      cursor: pointer;
757
+      user-select: none;
758
+    }
759
+  }
760
+}
761
+.summaryTable {
762
+  margin-top: 10px;
763
+}
764
+.detailsTable {
765
+  margin-top: 10px;
766
+}
767
+.sort-wrap {
768
+  display: flex;
769
+  flex-direction: column;
770
+  i {
771
+    cursor: pointer;
772
+    &.active {
773
+      color: #32B38A;
774
+    }
775
+  }
776
+  i:first-child {
777
+    margin-bottom: -3px;
778
+  }
779
+  i:last-child {
780
+    margin-top: -3px;
781
+  }
782
+}
783
+</style>

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

@@ -68,6 +68,8 @@ const accountManage = () => import(/* webpackChunkName: 'accountManage' */ '@/co
68 68
 const miniProManage = () => import(/* webpackChunkName: 'miniProManage' */ '@/components/manage/miniProManage/miniProManage.vue')
69 69
 // 企微助手 - 剧集管理
70 70
 const playletManage = () => import(/* webpackChunkName: 'playletManage' */ '@/components/manage/playletManage/playletManage.vue')
71
+// 数据看板 - 平台推广数据
72
+const platformPromote = () => import(/* webpackChunkName: 'platformPromote' */ '@/components/dataBoard/platformPromote/index.vue')
71 73
 
72 74
 // name与菜单配置的页面路由一致
73 75
 // meta下isData:true为数据看板,否则为助手
@@ -569,6 +571,17 @@ export var allRouter = [
569 571
         }
570 572
       },
571 573
       {
574
+        path: 'platformPromote',
575
+        name: 'platformPromote',
576
+        component: platformPromote,
577
+        meta: {
578
+          keepAlive: false,
579
+          isLogin: true,
580
+          title: '平台推广数据',
581
+          isData: true
582
+        }
583
+      },
584
+      {
572 585
         path: 'populariz',
573 586
         name: 'populariz',
574 587
         component: populariz,