zxl
2025-06-27 8836a082381427a671d5e75e9536dfc1fef8da6b
客户详情:客户分析,视频浏览记录
3个文件已修改
1个文件已添加
516 ■■■■ 已修改文件
manager/package.json 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
manager/src/api/customer.js 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
manager/src/views/customer/WatchHistory.vue 230 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
manager/src/views/customer/index.vue 269 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
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",
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',
  })
}
manager/src/views/customer/WatchHistory.vue
New file
@@ -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>
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 ? '已禁用' : '正常' }}
          </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: '', // 当前查看的用户ID
      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;
}