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