From 8836a082381427a671d5e75e9536dfc1fef8da6b Mon Sep 17 00:00:00 2001 From: zxl <763096477@qq.com> Date: 星期五, 27 六月 2025 09:09:32 +0800 Subject: [PATCH] 客户详情:客户分析,视频浏览记录 --- manager/src/api/customer.js | 16 + manager/src/views/customer/WatchHistory.vue | 230 +++++++++++++++++++++++++ manager/src/views/customer/index.vue | 269 ++++++++++++++++++++++------- manager/package.json | 1 4 files changed, 452 insertions(+), 64 deletions(-) diff --git a/manager/package.json b/manager/package.json index 8de4220..2fc3c99 100644 --- a/manager/package.json +++ b/manager/package.json @@ -16,6 +16,7 @@ "core-js": "^3.6.5", "cos-js-sdk-v5": "^1.10.1", "dplayer": "^1.26.0", + "echarts": "^5.6.0", "js-cookie": "^2.2.1", "node-sass": "^4.14.1", "price-color": "1.0.2", diff --git a/manager/src/api/customer.js b/manager/src/api/customer.js index 9972185..59739f1 100644 --- a/manager/src/api/customer.js +++ b/manager/src/api/customer.js @@ -84,3 +84,19 @@ method: 'GET' }) } + +export const getFootVideoPage =(params) =>{ + return service({ + url:'/customerManager/videoFootPage', + method: 'GET', + params:params + }) +} + +export const memberActionAnalyse = (params) =>{ + return service({ + url:'/customerManager/memberActionAnalyse/'+params, + method:'GET', + }) +} + diff --git a/manager/src/views/customer/WatchHistory.vue b/manager/src/views/customer/WatchHistory.vue new file mode 100644 index 0000000..c86ed50 --- /dev/null +++ b/manager/src/views/customer/WatchHistory.vue @@ -0,0 +1,230 @@ +<template> + <div class="watch-history-simple"> + <!-- 缁熻鍗$墖 --> + <Row :gutter="16" class="stats-row"> + <Col :span="12"> + <Card dis-hover> + <p slot="title">鎬昏鐪嬫椂闀�</p> + <h1 style="color: #2d8cf0;">{{ stats.totalDuration || '0'}} 灏忔椂</h1> + </Card> + </Col> + <Col :span="12"> + <Card dis-hover> + <p slot="title">骞冲潎瀹屾垚鐜�</p> + <h1 style="color: #19be6b;">{{ stats.avgProgress || 0}}%</h1> + </Card> + </Col> + </Row> + + <!-- 鏁版嵁琛ㄦ牸 --> + <Card dis-hover class="table-card"> + <Table + :columns="columns" + :data="tableData" + :loading="loading" + @on-row-click="handleRowClick" + style="height: 600px" + > + <template slot-scope="{ row }" slot="video"> + <div class="video-cell"> + <img + :src="row.coverCOSUrl" + class="video-cover" + @click.stop="previewImage(row.coverCOSUrl)" + alt=""> + <span class="video-title">{{ row.title }}</span> + </div> + </template> + + <template slot-scope="{ row }" slot="progress"> + <Tooltip :content="getPercent(row)" style="width: 100%"> + <Progress :percent="getPercent(row)" + :status="getPercent(row) >= 100 ? 'success' : 'active'" + /> + </Tooltip> + </template> + +<!-- <template slot-scope="{ row }" slot="action" >--> +<!-- <Button--> +<!-- type="primary"--> +<!-- size="small"--> +<!-- @click.stop="showDetail(row)"--> +<!-- >--> +<!-- 璇︽儏--> +<!-- </Button>--> +<!-- </template>--> + </Table> + + <!-- 鍒嗛〉 --> + <Page + :current="searchForm.pageNumber" + :total="total" + :page-size="searchForm.pageSize" + @on-change="handlePageChange" + show-total + class="pagination" + /> + </Card> + + <!-- 鍥剧墖棰勮 --> + <Modal + v-model="previewVisible" + title="瑙嗛灏侀潰" + footer-hide + width="60%" + > + <img :src="previewImageUrl" class="preview-image" alt=""> + </Modal> + </div> +</template> + +<script> +import { getFootVideoPage } from '@/api/customer.js' +import { Progress, Tooltip } from 'view-design'; +export default { + props: ['memberId'], + watch: { + memberId(newVal) { + if (newVal) { + this.loadData(); + } + } + }, + components: { + Progress, + Tooltip, + }, + data() { + + return { + stats: { + totalDuration: '', + avgProgress: 0 + }, + loading: false, + tableData: [], + columns: [ + { + title: '瑙嗛鍐呭', + slot: 'video', + minWidth: 300 + }, + { + title: '瑙傜湅鏃堕棿', + key: 'viewDuration', + width: 180, + render: (h, { row }) => { + return h('span', row.viewDuration /1000 +"绉�") + } + }, + { + title: '瑙傜湅杩涘害', + slot: 'progress', + width: 150 + }, + // { + // title: '鎿嶄綔', + // slot: 'action', + // minWidth: 120, // 鏀逛负鏈�灏忓搴� + // align: 'center' + // } + ], + searchForm:{ + memberId: '', //id + pageNumber: 1, // 褰撳墠椤垫暟 + pageSize: 10, // 椤甸潰澶у皬 + }, + total:0, + previewVisible: false, + previewImageUrl: '', + } + }, + methods: { + async loadData() { + this.loading = true + this.searchForm.memberId = this.memberId; + getFootVideoPage(this.searchForm).then(res =>{ + this.loading = false; + if (res.code === 200){ + this.tableData = res.data.data; + this.total = res.data.total; + this.stats.avgProgress = res.data.avgProgress; + this.stats.totalDuration = (res.data.totalDuration / 1000/ 60/60).toFixed(2); + }else { + this.$Message.error(res.msg) + } + }) + + }, + handlePageChange(page) { + this.searchForm.pageNumber = page + this.loadData() + }, + handleRowClick(row) { + console.log('Row clicked:', row) + }, + showDetail(row) { + this.$Modal.info({ + title: '瑙傜湅璇︽儏', + content: ` + <p><strong>瑙嗛ID锛�</strong>${row.id}</p> + <p><strong>瀹屾暣杩涘害锛�</strong>${row.progress}%</p> + <p><strong>璁惧淇℃伅锛�</strong>${row.device}</p> + `, + width: 500 + }) + }, + previewImage(url) { + this.previewImageUrl = url + this.previewVisible = true + }, + getPercent(row) { + return Number((row.playProgress * 100).toFixed(0)); + }, + } +} +</script> + +<style scoped> +.watch-history-simple { + padding: 16px; +} + +.stats-row { + margin-bottom: 16px; +} + +.table-card { + margin-top: 16px; +} + +.video-cell { + display: flex; + align-items: center; +} + +.video-cover { + width: 80px; + height: 45px; + object-fit: cover; + margin-right: 10px; + cursor: pointer; + border-radius: 4px; +} + +.video-title { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.pagination { + margin-top: 16px; + text-align: right; +} + +.preview-image { + width: 100%; + border-radius: 4px; +} +</style> diff --git a/manager/src/views/customer/index.vue b/manager/src/views/customer/index.vue index 6208375..b4dffa6 100644 --- a/manager/src/views/customer/index.vue +++ b/manager/src/views/customer/index.vue @@ -91,73 +91,89 @@ <Modal v-model="showCustomerInfo" :title="modelTitle" - width="700" + width="850" :mask-closable="false" > - <div class="customer-detail"> - <div class="avatar-section"> - <Avatar :src="customerInfo.face" size="large" /> - <div class="basic-info"> - <h3>{{ customerInfo.nickName || '寰俊鐢ㄦ埛' }}</h3> - <p>ID: {{ customerInfo.id }}</p> - <p>鐢ㄦ埛鍚�: {{ customerInfo.username }}</p> - </div> - </div> - <Divider /> + <!-- 涓诲唴瀹瑰尯 --> + <Tabs v-model="activeTab" @on-click="handleTabChange"> + <TabPane label="鍩虹璧勬枡" name="basic"> + <div class="customer-detail"> + <div class="avatar-section"> + <Avatar :src="customerInfo.face" size="large" /> + <div class="basic-info"> + <h3>{{ customerInfo.nickName || '寰俊鐢ㄦ埛' }}</h3> + <p>ID: {{ customerInfo.id }}</p> + <p>鐢ㄦ埛鍚�: {{ customerInfo.username }}</p> + </div> + </div> - <div class="detail-grid"> - <div class="detail-row"> - <span class="detail-label">鎬у埆锛�</span> - <span class="detail-value">{{ customerInfo.sex === 0 ? "濂�" : "鐢�"}}</span> - </div> - <div class="detail-row"> - <span class="detail-label">鍦板尯锛�</span> - <span class="detail-value">{{ customerInfo.region || '鏈缃�' }}</span> - </div> - <div class="detail-row"> - <span class="detail-label">鎵嬫満鍙凤細</span> - <span class="detail-value">{{ customerInfo.mobile || '鏈粦瀹�' }}</span> - </div> - <div class="detail-row"> - <span class="detail-label">褰撳墠绉垎锛�</span> - <span class="detail-value">{{ customerInfo.point }}</span> - </div> - <div class="detail-row"> - <span class="detail-label">鎬荤Н鍒嗭細</span> - <span class="detail-value">{{ customerInfo.totalPoint }}</span> - </div> - <div class="detail-row"> - <span class="detail-label">璐﹀彿鐘舵�侊細</span> - <span class="detail-value"> + <Divider /> + <div class="detail-grid"> + <div class="detail-row"> + <span class="detail-label">鎬у埆锛�</span> + <span class="detail-value">{{ customerInfo.sex === 0 ? "濂�" : "鐢�"}}</span> + </div> + <div class="detail-row"> + <span class="detail-label">鍦板尯锛�</span> + <span class="detail-value">{{ customerInfo.region || '鏈缃�' }}</span> + </div> + <div class="detail-row"> + <span class="detail-label">鎵嬫満鍙凤細</span> + <span class="detail-value">{{ customerInfo.mobile || '鏈粦瀹�' }}</span> + </div> + <div class="detail-row"> + <span class="detail-label">褰撳墠绉垎锛�</span> + <span class="detail-value">{{ customerInfo.point }}</span> + </div> + <div class="detail-row"> + <span class="detail-label">鎬荤Н鍒嗭細</span> + <span class="detail-value">{{ customerInfo.totalPoint }}</span> + </div> + <div class="detail-row"> + <span class="detail-label">璐﹀彿鐘舵�侊細</span> + <span class="detail-value"> <Tag :color="customerInfo.disabled ? 'error' : 'success'"> {{ customerInfo.disabled ? '宸茬鐢�' : '姝e父' }} </Tag> </span> + </div> + <div class="detail-row"> + <span class="detail-label">鏄惁鍏宠仈搴楅摵锛�</span> + <span class="detail-value">{{ customerInfo.haveStore ? '鏄�' : '鍚�' }}</span> + </div> + <div class="detail-row"> + <span class="detail-label">娉ㄥ唽鏃堕棿锛�</span> + <span class="detail-value">{{ customerInfo.createTime }}</span> + </div> + <div class="detail-row"> + <span class="detail-label">鏈�鍚庣櫥褰曟椂闂达細</span> + <span class="detail-value">{{ customerInfo.lastLoginDate }}</span> + </div> </div> - <div class="detail-row"> - <span class="detail-label">鏄惁鍏宠仈搴楅摵锛�</span> - <span class="detail-value">{{ customerInfo.haveStore ? '鏄�' : '鍚�' }}</span> - </div> - <div class="detail-row"> - <span class="detail-label">娉ㄥ唽鏃堕棿锛�</span> - <span class="detail-value">{{ customerInfo.createTime }}</span> - </div> - <div class="detail-row"> - <span class="detail-label">鏈�鍚庣櫥褰曟椂闂达細</span> - <span class="detail-value">{{ customerInfo.lastLoginDate }}</span> + <div v-if="customerInfo.customerTagList && customerInfo.customerTagList.length > 0" class="tags-section"> + <h4>鐢ㄦ埛鏍囩</h4> + <div> + <Tag v-for="tag in customerInfo.customerTagList" :key="tag" color="default" style="margin-right: 8px;"> + {{ tag }} + </Tag> + </div> </div> </div> + </TabPane> + <TabPane label="瑙嗛娴忚鍘嗗彶" name="history"> + <watch-history + ref="watchHistoryRef" + :memberId="customerInfo.id" + key="history"/> + </TabPane> + <TabPane label="琛屼负鍒嗘瀽" name="actionAnalyse" > + <div class="chart-container"> + <div ref="chart" class="chart-content"></div> + </div> + </TabPane> + </Tabs> - <div v-if="customerInfo.customerTagList && customerInfo.customerTagList.length > 0" class="tags-section"> - <h4>鐢ㄦ埛鏍囩</h4> - <div> - <Tag v-for="tag in customerInfo.customerTagList" :key="tag" color="default" style="margin-right: 8px;"> - {{ tag }} - </Tag> - </div> - </div> - </div> <div slot="footer"> <Button type="primary" @click="showCustomerInfo = false">鍏抽棴</Button> @@ -202,16 +218,25 @@ <script> import JsonExcel from "vue-json-excel"; -import {getCustomerList,addCustomerTag,saveCustomerTagById,getTagList,getStoreSelectOptions,getCustomerInfo} from "@/api/customer"; +import {memberActionAnalyse,getCustomerList,addCustomerTag,saveCustomerTagById,getTagList,getStoreSelectOptions,getCustomerInfo} from "@/api/customer"; import {addCustomerBlackByPC} from "@/api/customer-black.js" +import WatchHistory from "./WatchHistory.vue"; +import * as echarts from 'echarts'; export default { name:"customer", components:{ - "download-excel": JsonExcel, + WatchHistory, + "download-excel": JsonExcel + }, + data(){ return{ + myChart: null, + activeTab: 'basic', // 褰撳墠婵�娲荤殑鏍囩椤� + userId: '', // 褰撳墠鏌ョ湅鐨勭敤鎴稩D + customerInfo: { birthday: null, blackId: null, @@ -352,14 +377,107 @@ blackParam:{ storeId:'', userId:'', - } + }, + + } }, + mounted(){ this.init(); }, methods:{ + showLoading() { + if (this.myChart) { + this.myChart.showLoading({ + text: '鏁版嵁鍔犺浇涓�...', + color: '#1890ff', + textColor: '#333', + maskColor: 'rgba(255, 255, 255, 0.8)', + zlevel: 0, + fontSize: 14, + showSpinner: true, + spinnerRadius: 10, + lineWidth: 2, + fontWeight: 'normal' + }); + } + }, + + hideLoading() { + if (this.myChart) { + this.myChart.hideLoading(); + } + }, + getEmptyOption() { + return { + title: { + text: '鏆傛棤鏁版嵁', + left: 'center', + top: 'center', + textStyle: { + color: '#999', + fontWeight: 'normal', + fontSize: 16 + } + }, + series: [] + }; + }, + initEcharts(id){ + // 閿�姣佹棫瀹炰緥 + if (this.myChart) { + this.myChart.dispose(); + } + + this.myChart = echarts.init(this.$refs.chart); + this.showLoading() + memberActionAnalyse(id).then(res =>{ + let option; + this.hideLoading() + if (res.code === 200){ + const chartData = Object.entries(res.data).map(([name, value]) => ({ + value, + name + })); + console.log(chartData) + if (chartData.length ===0){ + option = this.getEmptyOption(); + }else { + option = { + title: { + text: '鐢ㄦ埛琛屼负鍒嗘瀽', + left: 'center' + }, + + tooltip: { + trigger: 'item' + }, + series: [{ + radius: ['40%', '70%'], + type: 'pie', + data: chartData, + emphasis: { + itemStyle: { + shadowBlur: 10, + shadowOffsetX: 0, + shadowColor: 'rgba(0, 0, 0, 0.5)' + } + } + }] + }; + } + + this.myChart.setOption(option); + } + }) + }, + // 鏍囩椤靛垏鎹㈠鐞� + handleTabChange(tabName) { + this.activeTab = tabName; + }, + // 鑾峰緱鍟嗗涓嬫媺妗嗘暟鎹� getStoreSelectOptions(){ this.storeSelectLoading = true; @@ -384,12 +502,12 @@ // 鑾峰緱瀹㈡埛鏍囩涓嬫媺妗嗘暟鎹� getCustomerTagSelectOptions(){ this.selectLoading = true; - getTagList().then(res =>{ - this.selectLoading = false; - if (res.code === 200){ - this.tagList = res.data; - } - }) + // getTagList().then(res =>{ + // this.selectLoading = false; + // if (res.code === 200){ + // this.tagList = res.data; + // } + // }) }, init(){ this.getCustomerList(); @@ -403,10 +521,16 @@ this.selectList = e.map(d => d.id); this.selectCount = e.length; }, + memberActionAnalyse(id){ + + }, //鏌ョ湅璇︽儏 openInfo(row){ this.showCustomerInfo = true; this.modelTitle = "鐢ㄦ埛璇︽儏" + + this.activeTab = "basic" + getCustomerInfo(row.id).then(res =>{ if(res.code === 200){ this.customerInfo = { @@ -435,6 +559,8 @@ }; } }) + + this.initEcharts(row.id); }, // 缂栬緫鏍囩 openEdit(row){ @@ -495,6 +621,21 @@ </script> <style lang="scss" scoped> +.chart-container { + width: 100%; + height: 100%; /* 鏀逛负100%濉厖鐖跺鍣� */ + min-height: 400px; /* 鏈�灏忛珮搴︿繚闅� */ + display: flex; + justify-content: center; + align-items: center; + position: relative; + margin: auto; /* 鏂板 */ +} +.chart-content { + width: 400px; + height: 400px; + margin: 0 auto; +} .customer-detail { padding: 16px; } -- Gitblit v1.8.0