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