Browse Source

feat: 客户群数据报表&下拉选择组件交互逻辑

zhengxy 1 year ago
parent
commit
9b007c0338

+ 2 - 0
project/src/assets/config/interface_api.js

@@ -307,6 +307,8 @@ var api = {
307 307
   dataBoard_recharge_list: "/api/statistics/cumulativeRecoveryData", // 充值数据 - 列表
308 308
   dataBoard_customerStaff_list: "/api/statistics/customerServiceData", // 客服数据统计 - 列表
309 309
   dataBoard_customerStaff_setStatus: "/api/user/updateUserStatus", // 客服数据统计 - 更新客服状态
310
+  dataBoard_chatGroupData_list: "/api/chatGroup/chatGroupStatistic", // 客户群数据 - 列表
311
+  dataBoard_chatGroupData_summary: "/api/chatGroup/chatGroupCondition", // 客户群数据 - 汇总
310 312
 
311 313
   getPlatformOptions: "/api/intelligentMassSending/platformIndex", // 平台筛选选项
312 314
   accountManage_accountIndex: "/api/intelligentMassSending/accountIndex", // 平台账号管理 - 账号列表

+ 10 - 0
project/src/assets/js/staticTypes.js

@@ -74,3 +74,13 @@ export const customerStaffStatusMap = new Map([
74 74
   [customerStaffStatusCode.STOPPED, '已停用'],
75 75
   [customerStaffStatusCode.BANNED, '已封禁'],
76 76
 ])
77
+
78
+// 客服状态
79
+export const chatGroupStatusCode = {
80
+  'NORMAL': 1,
81
+  'DISSOLVE': 2,
82
+}
83
+export const chatGroupStatusCodeMap = new Map([
84
+  [chatGroupStatusCode.NORMAL, { label: '正常', color: '#00B38A' }],
85
+  [chatGroupStatusCode.DISSOLVE, { label: '解散', color: '#EB4315' }],
86
+])

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

@@ -29,6 +29,7 @@
29 29
       filterable
30 30
       :disabled="disabled"
31 31
       @change="change"
32
+      @visible-change="onVisibleChange"
32 33
     >
33 34
       <el-option v-for="item in options" :key="item.key" :label="item.val" :value="item.key" />
34 35
     </el-select>
@@ -247,12 +248,39 @@ export default {
247 248
         { key: 15, val: '15天' },
248 249
       ]
249 250
       this.placeholderVal = '请选择'
251
+    } else if (this.type == 'chatGroupOwner') { // 群主
252
+      this.getChatGroupOwner()
253
+      this.placeholderVal = '请选择群主'
250 254
     } else {
251 255
       this.init()
252 256
     }
253 257
 
254 258
   },
255 259
   methods: {
260
+    onVisibleChange(val) {
261
+      this.$emit('visible-change', val)
262
+    },
263
+    async getChatGroupOwner() {
264
+      const url = `${this.URL.BASEURL}${this.URL.chatGroup_chatGroupOwnerIndex}`
265
+      const params = {}
266
+
267
+      const { isSelectedCorp = false, corpid = '' } = this.afferent_params
268
+      if (isSelectedCorp && !corpid) { // 先选择企微主体 => 没选主体清空群主
269
+        this.options = []
270
+        return
271
+      } else if (isSelectedCorp && corpid) { // 先选择企微主体 => 根据已选主体获取群主
272
+        params.corpid = corpid
273
+      }
274
+
275
+      const { data: res = {} } = await this.$axios.get(url, { params })
276
+      if (res && res.errno == 0) {
277
+        this.options = Array.isArray(res.rst) ? res.rst : []
278
+        this.options.forEach(item => {
279
+          item.val = item.name;
280
+          item.key = item.user_id
281
+        })
282
+      }
283
+    },
256 284
     getPicturesGroup() { //图片组
257 285
       this.$axios.get(this.URL.BASEURL + this.URL.media_groupList, {
258 286
         params: {

+ 28 - 0
project/src/components/assembly/screen/channelMultiple.vue

@@ -14,6 +14,7 @@
14 14
       multiple
15 15
       collapse-tags
16 16
       @change="change"
17
+      @visible-change="onVisibleChange"
17 18
     >
18 19
       <el-option v-for="item in options" :key="item.key" :label="item.val" :value="item.key" />
19 20
     </el-select>
@@ -227,12 +228,39 @@ export default {
227 228
         { key: 15, val: '15天' },
228 229
       ]
229 230
       this.placeholderVal = '请选择'
231
+    } else if (this.type == 'chatGroupOwner') { // 群主
232
+      this.getChatGroupOwner()
233
+      this.placeholderVal = '请选择群主'
230 234
     } else {
231 235
       this.init()
232 236
     }
233 237
 
234 238
   },
235 239
   methods: {
240
+    onVisibleChange(val) {
241
+      this.$emit('visible-change', val)
242
+    },
243
+    async getChatGroupOwner() {
244
+      const url = `${this.URL.BASEURL}${this.URL.chatGroup_chatGroupOwnerIndex}`
245
+      const params = {}
246
+
247
+      const { isSelectedCorp = false, corpid = '' } = this.afferent_params
248
+      if (isSelectedCorp && !corpid) { // 先选择企微主体 => 没选主体清空群主
249
+        this.options = []
250
+        return
251
+      } else if (isSelectedCorp && corpid) { // 先选择企微主体 => 根据已选主体获取群主
252
+        params.corpid = corpid
253
+      }
254
+
255
+      const { data: res = {} } = await this.$axios.get(url, { params })
256
+      if (res && res.errno == 0) {
257
+        this.options = Array.isArray(res.rst) ? res.rst : []
258
+        this.options.forEach(item => {
259
+          item.val = item.name;
260
+          item.key = item.user_id
261
+        })
262
+      }
263
+    },
236 264
     getOperatorGroup() { // 运营组
237 265
       this.$axios.get(this.URL.BASEURL + this.URL.operatorGroup_groupList, {
238 266
         params: {

+ 28 - 0
project/src/components/assembly/screen/channelV2.vue

@@ -15,6 +15,7 @@
15 15
       :collapse-tags="collapseTags"
16 16
       :multiple-limit="multipleLimit"
17 17
       @change="onChangeResultVal"
18
+      @visible-change="onVisibleChange"
18 19
     >
19 20
       <el-option v-for="item in options" :key="item.key" :label="item.val" :value="item.key" />
20 21
     </el-select>
@@ -98,6 +99,9 @@ export default {
98 99
     this.handleGetOptions()
99 100
   },
100 101
   methods: {
102
+    onVisibleChange(val) {
103
+      this.$emit('visible-change', val)
104
+    },
101 105
     onChangeResultVal(val) {
102 106
       this.$emit('change', val)
103 107
     },
@@ -319,6 +323,27 @@ export default {
319 323
         });
320 324
       }
321 325
     },
326
+    async getChatGroupOwner() {
327
+      const url = `${this.URL.BASEURL}${this.URL.chatGroup_chatGroupOwnerIndex}`
328
+      const params = {}
329
+
330
+      const { isSelectedCorp = false, corpid = '' } = this.afferent_params
331
+      if (isSelectedCorp && !corpid) { // 先选择企微主体 => 没选主体清空群主
332
+        this.options = []
333
+        return
334
+      } else if (isSelectedCorp && corpid) { // 先选择企微主体 => 根据已选主体获取群主
335
+        params.corpid = corpid
336
+      }
337
+
338
+      const { data: res = {} } = await this.$axios.get(url, { params })
339
+      if (res && res.errno == 0) {
340
+        this.options = Array.isArray(res.rst) ? res.rst : []
341
+        this.options.forEach(item => {
342
+          item.val = item.name;
343
+          item.key = item.user_id
344
+        })
345
+      }
346
+    },
322 347
     handleGetOptions() {
323 348
       if (this.type == 'assignment_status') {
324 349
         this.options = [
@@ -466,6 +491,9 @@ export default {
466 491
           { key: 15, val: '15天' },
467 492
         ]
468 493
         this.placeholderVal = '请选择'
494
+      } else if (this.type == 'chatGroupOwner') { // 群主
495
+        this.getChatGroupOwner()
496
+        this.placeholderVal = '请选择群主'
469 497
       } else {
470 498
         this.init()
471 499
       }

+ 327 - 0
project/src/components/dataBoard/chatGroupData.vue

@@ -0,0 +1,327 @@
1
+<template>
2
+  <div>
3
+    <div class="screenBox">
4
+      <div class="flex-align-center" style="flex-wrap: wrap;margin-right: 60px;">
5
+        <!-- <datePicker title="" :quickFlag="true" :afferent_time="default_time" :clearFlag='false' :reset="resetFlag" @changeTime="onChangeTime" style="margin-right: 10px;" /> -->
6
+        <!-- 企微主体 -->
7
+        <selfSelectCorp style="margin-right: 30px;" v-model="filter.corpid" @change="onChangeCorpid" />
8
+        <!-- 群主 -->
9
+        <selfChannelV2 v-model="filter.owner" title="群主" type="chatGroupOwner" ref="selectOwnerEl" labelWidth :afferent_params="{ isSelectedCorp: true, corpid: filter.corpid }" @change="onChangeOwner" @visible-change="onSelectOwnerVisibleChange" />
10
+      </div>
11
+      <div class="reset" @click="resetEvent">重置</div>
12
+      <!-- <el-button v-if="isCanExport" class="export-btn" type="primary" size="mini" @click="onClickExport">导出Excel</el-button> -->
13
+    </div>
14
+    <div class="dataInfoBox" v-loading="dataLoading">
15
+      <div class="dataInfoItem" v-for="(item, index) in dataInfoArrs" :key="index">
16
+        <div class="dataItemTitle">
17
+          <img
18
+            v-if="item.img"
19
+            :src="item.img"
20
+            :style="{height: item.size ? item.size + 'px' : '13px', width: item.size ? item.size + 'px' : '13px',}"
21
+            class="titleIcon"
22
+          />
23
+          <span>{{ item.label }}</span>
24
+        </div>
25
+        <div class="dataItem-data">
26
+          {{ dataInfo && (dataInfo[item.prop] || dataInfo[item.prop] == 0) ? $formatNum(dataInfo[item.prop]) : '-' }}
27
+        </div>
28
+      </div>
29
+    </div>
30
+    <div v-loading="loading">
31
+      <ux-grid class="uxGridBox" ref="plxTable" :border="false" @row-click="() => { return }"
32
+        :header-cell-style="headerColor" :height="height" show-footer-overflow="tooltip" show-overflow="tooltip"
33
+        size="mini">
34
+        <ux-table-column v-for="item in desCol" :key="item.prop" :resizable="true" :field="item.prop"
35
+          :title="item.label" :min-width="item.min_width ? item.min_width : 120" :fixed="item.fixed ? item.fixed : ''"
36
+          align="center">
37
+          <template #header>
38
+            <div class="flex-align-jus-center">{{ item.label }}
39
+              <el-tooltip v-if="item.notes" :content="item.notes" placement="top">
40
+                <div><i class="el-icon-question"></i></div>
41
+              </el-tooltip>
42
+            </div>
43
+          </template>
44
+          <template v-slot="{ row, $index }">
45
+            <span v-if="item.prop == 'status'" :style="{ color: getChatGroupStatusDesc(row['status']).color }">
46
+              {{ getChatGroupStatusDesc(row['status']).label || '-' }}
47
+            </span>
48
+            <span v-else>{{ row[item.prop] === 0 ? row[item.prop] : (row[item.prop] ? $formatNum(row[item.prop]) : '-') }}</span>
49
+          </template>
50
+        </ux-table-column>
51
+      </ux-grid>
52
+      <div class="pagination" v-show="total > 0">
53
+        <el-pagination background :current-page="page" @current-change="handleCurrentChange" layout="prev, pager, next" :page-count='Number(pages)' />
54
+      </div>
55
+    </div>
56
+  </div>
57
+</template>
58
+<script>
59
+import datePicker from '@/components/assembly/screen/datePicker.vue'
60
+import selfChannelV2 from '@/components/assembly/screen/channelV2.vue'
61
+import selfSelectCorp from '@/components/assembly/screen/selectCorp.vue'
62
+import { chatGroupStatusCodeMap } from '@/assets/js/staticTypes'
63
+
64
+export default {
65
+  name: 'orderData',
66
+  components: {
67
+    datePicker,
68
+    selfChannelV2,
69
+    selfSelectCorp,
70
+  },
71
+  data () {
72
+    return {
73
+      dataLoading: false,
74
+      loading: false,
75
+      page: 1,
76
+      pages: 0,
77
+      total: 0,
78
+      page_size: 20,
79
+      default_time: [this.$getDay(-30, false), this.$getDay(0, false)],
80
+      height: '',
81
+      resetFlag: false,
82
+
83
+      dataInfo: {},
84
+      dataInfoArrs: [
85
+        {
86
+          prop: 'member_total',
87
+          label: '累计群人数',
88
+          img: require('@/assets/img/iconNew/充值人数@2x.png'),
89
+        },
90
+        {
91
+          prop: 'member_count',
92
+          label: '留存群人数',
93
+          img: require('@/assets/img/iconNew/充值人数@2x.png'),
94
+        },
95
+        {
96
+          prop: 'member_quit_count',
97
+          label: '流失群人数',
98
+          img: require('@/assets/img/iconNew/充值人数@2x.png'),
99
+        },
100
+        {
101
+          prop: 'group_count',
102
+          label: '群聊总数',
103
+          img: require('@/assets/img/iconNew/总充值人数@2x.png'),
104
+        },
105
+        {
106
+          prop: 'today_join_count',
107
+          label: '今日净增群人数',
108
+          img: require('@/assets/img/icon/今日@2x.png'),
109
+          size: '17',
110
+        },
111
+        {
112
+          prop: 'today_create_count',
113
+          label: '今日新增群聊数',
114
+          img: require('@/assets/img/icon/今日@2x.png'),
115
+          size: '17',
116
+        },
117
+      ],
118
+      desCol: [
119
+        { prop: "name", label: "群名称", min_width: 180, fixed: 'left' },
120
+        { prop: "owner_name", label: "群主", min_width: 160 },
121
+        { prop: "create_time", label: "创建时间", min_width: 160 },
122
+        { prop: "corp_name", label: "主体", min_width: 180 },
123
+        { prop: "member_count", label: "群人数", min_width: 160 },
124
+        { prop: "member_total", label: "累计群人数", min_width: 160 },
125
+        { prop: "member_quit_count", label: "流失群人数", min_width: 160 },
126
+        { prop: "status", label: "群状态", min_width: 160 },
127
+      ],
128
+      filter: {
129
+        time: [],
130
+        corpid: '', // 企微主体
131
+        owner: '', // 群主
132
+      },
133
+    }
134
+  },
135
+  computed: {
136
+    // 是否有“导出”权限
137
+    isCanExport() {
138
+      return !!this.$store.state.dataBoardAuth.can_export
139
+    },
140
+  },
141
+  created () {
142
+    this.height = document.documentElement.clientHeight - 280 > 400 ? document.documentElement.clientHeight - 280 : 400
143
+    this.filter.time = this.default_time
144
+    this.init(1)
145
+    this.init_predata()
146
+  },
147
+  methods: {
148
+    getChatGroupStatusDesc(code) {
149
+      return chatGroupStatusCodeMap.get(code) || {}
150
+    },
151
+    headerColor ({ row, column, rowIndex, columnIndex }) {
152
+      return { backgroundColor: '#FFFFFF !important', border: 'none!important' }
153
+    },
154
+    async init_predata () {
155
+      const url = this.URL.BASEURL + this.URL.dataBoard_chatGroupData_summary
156
+      const params = {
157
+        corpid: this.filter.corpid,
158
+        owner: this.filter.owner,
159
+      }
160
+      try {
161
+        this.dataLoading = true
162
+        const { data: res = {} } = await this.$axios.post(url, params)
163
+        if (res && res.errno == 0) {
164
+          this.dataInfo = {...res.rst}
165
+        } else if (res.errno != 4002) {
166
+          this.$message({
167
+            message: res.err,
168
+            type: "warning"
169
+          })
170
+        }
171
+      } catch (error) {
172
+        console.log('error => ', error)
173
+      } finally {
174
+        this.dataLoading = false
175
+      }
176
+    },
177
+    async init (page, type) {
178
+      this.page = page ? page : this.page;
179
+      this.loading = true
180
+      const url = `${this.URL.BASEURL}${this.URL.dataBoard_chatGroupData_list}`
181
+      const params = {
182
+        corpid: this.filter.corpid,
183
+        owner: this.filter.owner,
184
+        page: this.page,
185
+        page_size: this.page_size,
186
+      }
187
+      try {
188
+        const { data: res = {} } = await this.$axios.post(url, params)
189
+        if (res && res.errno == 0) {
190
+          this.datas = res.rst.data // 知道为啥datas不在 data()方法里面定义吗?嘻嘻
191
+          this.$refs.plxTable.reloadData(this.datas)
192
+          this.total = res.rst.pageInfo.total;
193
+          this.pages = res.rst.pageInfo.pages;
194
+        } else if (res.errno != 4002) {
195
+          this.$message.warning(res.err)
196
+        }
197
+      } catch (error) {
198
+        console.log('error => ', error)
199
+      } finally {
200
+        this.loading = false
201
+      }
202
+    },
203
+    //筛选时间变化
204
+    onChangeTime(time) {
205
+      if (!time || time && time.length == 0) {
206
+        this.filter.time = []
207
+      } else {
208
+        this.filter.time = time
209
+      }
210
+      this.init_predata()
211
+      this.init(1)
212
+    },
213
+    async onChangeCorpid(corp) {
214
+      this.filter.corpid = corp ? corp.corpid : ''
215
+      this.filter.owner = ''
216
+      await this.$nextTick()
217
+      this.$refs.selectOwnerEl && this.$refs.selectOwnerEl.handleGetOptions()
218
+      this.init(1);
219
+      this.init_predata();
220
+    },
221
+    onSelectOwnerVisibleChange(isOpen) {
222
+      if (isOpen && !this.filter.corpid) {
223
+        this.$message.warning('请选择企微主体')
224
+      }
225
+    },
226
+    onChangeOwner(val) {
227
+      this.filter.owner = val;
228
+      this.init(1);
229
+      this.init_predata();
230
+    },
231
+    handleCurrentChange (val) {
232
+      this.init(val)
233
+    },
234
+    async resetEvent () {//重置
235
+      this.resetFlag = !this.resetFlag
236
+      this.filter.time = [this.$getDay(-30, false), this.$getDay(0, false)]
237
+      this.filter.corpid = ''
238
+      this.filter.owner = ''
239
+      await this.$nextTick()
240
+      this.$refs.selectOwnerEl && this.$refs.selectOwnerEl.handleGetOptions()
241
+      this.init_predata()
242
+      this.init(1)
243
+    },
244
+  }
245
+}
246
+</script>
247
+<style lang="scss" scoped>
248
+.disabled {
249
+  cursor: not-allowed;
250
+}
251
+.screenBox {
252
+  background: #fff;
253
+  padding: 5px 20px 26px;
254
+  position: relative;
255
+  .export-btn {
256
+    position: absolute;
257
+    top: 17px;
258
+    right: 4px;
259
+  }
260
+
261
+  .reset {
262
+    width: 80px;
263
+    height: 30px;
264
+    background: #00b38a;
265
+    border-radius: 100px 3px 3px 3px;
266
+    border: 1px solid #d2d2d2;
267
+    color: #ffffff;
268
+    font-size: 14px;
269
+    line-height: 30px;
270
+    text-align: center;
271
+    letter-spacing: 2px;
272
+    cursor: pointer;
273
+    position: absolute;
274
+    right: 0;
275
+    bottom: 0;
276
+  }
277
+}
278
+
279
+.dataInfoBox {
280
+  display: flex;
281
+  margin-top: 10px;
282
+  // justify-content: space-between;
283
+  flex-wrap: wrap;
284
+
285
+  .dataInfoItem {
286
+    background: #ffffff;
287
+    border-radius: 8px;
288
+    margin-right: 10px;
289
+    margin-bottom: 10px;
290
+    //padding: 15px 80px 19px 20px;
291
+    min-width: 200px;
292
+    //max-width: 240px;
293
+    height: 85px;
294
+    padding-left: 20px;
295
+    display: flex;
296
+    flex-direction: column;
297
+    justify-content: center;
298
+    align-items: start;
299
+
300
+    .dataItemTitle {
301
+      display: flex;
302
+      align-items: center;
303
+      color: #6f6f6f;
304
+      font-size: 13px;
305
+      line-height: 17px;
306
+      font-weight: bold;
307
+
308
+      .titleIcon {
309
+        height: 16px;
310
+        margin-right: 4px;
311
+      }
312
+    }
313
+
314
+    .dataItem-data {
315
+      color: #000000;
316
+      font-size: 19px;
317
+      line-height: 28px;
318
+      font-weight: bold;
319
+      margin-top: 2px;
320
+      display: flex;
321
+      align-items: center;
322
+      width: 100%;
323
+      height: 50px;
324
+    }
325
+  }
326
+}
327
+</style>

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

@@ -46,6 +46,7 @@ const throwPerson = () => import(/* webpackChunkName: 'throwPerson' */ '@/compon
46 46
 const regRangeReport = () => import(/* webpackChunkName: 'regRangeReport' */ '@/components/dataBoard/regRangeReport.vue')
47 47
 const regRangeReportHS = () => import(/* webpackChunkName: 'regRangeReportHS' */ '@/components/dataBoard/regRangeReportHS.vue')
48 48
 const orderData = () => import(/* webpackChunkName: 'orderData' */ '@/components/dataBoard/orderData.vue')
49
+const chatGroupData = () => import(/* webpackChunkName: 'chatGroupData' */ '@/components/dataBoard/chatGroupData.vue')
49 50
 const dramaManage = () => import(/* webpackChunkName: 'dramaManage' */ '@/components/dataBoard/dramaManage.vue')
50 51
 const charge = () => import(/* webpackChunkName: 'charge' */ '@/components/orderManage/charge.vue')
51 52
 const wxAccountList = () => import(/* webpackChunkName: 'wxAccountList' */ '@/components/dataBoard/wxAccount/list.vue')
@@ -859,6 +860,17 @@ export var allRouter = [
859 860
         }
860 861
       },
861 862
       {
863
+        path: 'chatGroupData',
864
+        name: 'chatGroupData',
865
+        component: chatGroupData,
866
+        meta: {
867
+          keepAlive: false,
868
+          isLogin: true,
869
+          title: '客户群数据',
870
+          isData: true
871
+        }
872
+      },
873
+      {
862 874
         path: 'dramaManage',
863 875
         name: 'dramaManage',
864 876
         component: dramaManage,