Explorar o código

Merge branch 'master' of http://1.14.104.71:10880/root/ylrz_his_scrm_companyUI

lmx hai 6 días
pai
achega
573d9bb572
Modificáronse 55 ficheiros con 8787 adicións e 373 borrados
  1. 53 0
      src/api/company/show.js
  2. 23 0
      src/api/company/withdrawDetail.js
  3. 96 0
      src/api/crm/business.js
  4. 18 0
      src/api/qw/customerProperty.js
  5. 12 0
      src/api/qw/qwAnalyze.js
  6. 9 0
      src/api/watch/common.js
  7. 121 5
      src/api/watch/deviceInfo.js
  8. 1 1
      src/components/DeviceInfo/AtrialFibrillation.vue
  9. 5 0
      src/components/DeviceInfo/BloodSugar.vue
  10. 8 3
      src/components/DeviceInfo/Bloodoxygen.vue
  11. 5 0
      src/components/DeviceInfo/Heartrate.vue
  12. 9 4
      src/components/DeviceInfo/Pressure.vue
  13. 291 0
      src/components/DeviceInfo/PressureValue.vue
  14. 6 1
      src/components/DeviceInfo/Pulse.vue
  15. 1942 0
      src/components/DeviceInfo/Report.vue
  16. 13 183
      src/components/DeviceInfo/SettingDialog/SetInfoDialog.vue
  17. 22 27
      src/components/DeviceInfo/Sleep.vue
  18. 6 1
      src/components/DeviceInfo/Sports.vue
  19. 9 5
      src/components/DeviceInfo/Temperature.vue
  20. 5 0
      src/components/DeviceInfo/Uricacid.vue
  21. 5 0
      src/components/DeviceInfo/Urineketones.vue
  22. 7 0
      src/types/crm/customerListRow.ts
  23. 15 2
      src/utils/common.js
  24. 218 0
      src/views/company/companyMoneyLogsDetail/index.vue
  25. 25 2
      src/views/company/companyVoiceRobotic/index.vue
  26. 18 0
      src/views/company/companyWorkflow/design.vue
  27. 0 38
      src/views/components/QwUserSelectTwo.vue
  28. 2 2
      src/views/crm/components/AiTagPanel.vue
  29. 570 0
      src/views/crm/components/addBusiness.vue
  30. 96 0
      src/views/crm/components/setColumn.vue
  31. 154 46
      src/views/crm/customer/customerDetail.vue
  32. 114 2
      src/views/crm/customer/index.vue
  33. 5 0
      src/views/crm/customer/line.vue
  34. 29 2
      src/views/crm/customer/my.vue
  35. 882 0
      src/views/crm/customerBusiness/index.vue
  36. 930 0
      src/views/crm/customerBusiness/my.vue
  37. 25 0
      src/views/hisStore/components/productAfterSalesOrder.vue
  38. 42 1
      src/views/hisStore/components/productOrder.vue
  39. 3 0
      src/views/hisStore/storeAfterSales/list.vue
  40. 17 1
      src/views/hisStore/storeAfterSales/myList.vue
  41. 7 0
      src/views/hisStore/storeOrder/healthStoreList.vue
  42. 7 0
      src/views/hisStore/storeOrder/index.vue
  43. 20 0
      src/views/live/components/productAfterSalesOrder.vue
  44. 3 0
      src/views/live/liveAfteraSales/index.vue
  45. 3 0
      src/views/live/liveOrder/index.vue
  46. 16 0
      src/views/live/liveOrder/liveOrderDetails.vue
  47. 2305 0
      src/views/qw/externalContact/customerDetail.vue
  48. 123 4
      src/views/qw/externalContact/deptIndex.vue
  49. 122 2
      src/views/qw/externalContact/index.vue
  50. 123 1
      src/views/qw/externalContact/myExternalContact.vue
  51. 3 0
      src/views/store/storeAfterSales/list.vue
  52. 3 0
      src/views/store/storeAfterSales/myList.vue
  53. 35 13
      src/views/watch/deviceInfo/details.vue
  54. 160 13
      src/views/watch/deviceInfo/index.vue
  55. 46 14
      src/views/watch/deviceInfo/my.vue

+ 53 - 0
src/api/company/show.js

@@ -0,0 +1,53 @@
+import request from '@/utils/request'
+
+// 查询用户展示字段列表
+export function listShow(query) {
+  return request({
+    url: '/company/show/list',
+    method: 'get',
+    params: query
+  })
+}
+
+// 查询用户展示字段详细
+export function getShow(type) {
+  return request({
+    url: '/company/show/' + type,
+    method: 'get'
+  })
+}
+
+// 新增用户展示字段
+export function addShow(data) {
+  return request({
+    url: '/company/show',
+    method: 'post',
+    data: data
+  })
+}
+
+// 修改用户展示字段
+export function updateShow(data) {
+  return request({
+    url: '/company/show',
+    method: 'put',
+    data: data
+  })
+}
+
+// 删除用户展示字段
+export function delShow(id) {
+  return request({
+    url: '/company/show/' + id,
+    method: 'delete'
+  })
+}
+
+// 导出用户展示字段
+export function exportShow(query) {
+  return request({
+    url: '/company/show/export',
+    method: 'get',
+    params: query
+  })
+}

+ 23 - 0
src/api/company/withdrawDetail.js

@@ -0,0 +1,23 @@
+import request from '@/utils/request'
+
+export function getWithdrawDetailSummary() {
+  return request({
+    url: '/company/withdrawDetail/summary',
+    method: 'get'
+  })
+}
+
+export function listWithdrawDetail(query) {
+  return request({
+    url: '/company/withdrawDetail/list',
+    method: 'get',
+    params: query
+  })
+}
+
+export function exportWithdrawDetail() {
+  return request({
+    url: '/company/withdrawDetail/export',
+    method: 'get'
+  })
+}

+ 96 - 0
src/api/crm/business.js

@@ -0,0 +1,96 @@
+import request from '@/utils/request'
+
+// 查询商机列表
+export function listBusiness(query) {
+  return request({
+    url: '/crm/business/list',
+    method: 'get',
+    params: query
+  })
+}
+
+// 查询商机列表
+export function myListBusiness(query) {
+  return request({
+    url: '/crm/business/myList',
+    method: 'get',
+    params: query
+  })
+}
+
+// 查询商机详细
+export function getBusiness(businessId) {
+  return request({
+    url: '/crm/business/' + businessId,
+    method: 'get'
+  })
+}
+
+// 新增商机
+export function addBusiness(data) {
+  return request({
+    url: '/crm/business',
+    method: 'post',
+    data: data
+  })
+}
+
+// 修改商机
+export function updateBusiness(data) {
+  return request({
+    url: '/crm/business',
+    method: 'put',
+    data: data
+  })
+}
+
+// 删除商机
+export function delBusiness(businessId) {
+  return request({
+    url: '/crm/business/' + businessId,
+    method: 'delete'
+  })
+}
+
+// 导出商机
+export function exportBusiness(query) {
+  return request({
+    url: '/crm/business/export',
+    method: 'get',
+    params: query
+  })
+}
+
+// 导出商机
+export function exportMyBusiness(query) {
+  return request({
+    url: '/crm/business/exportMy',
+    method: 'get',
+    params: query
+  })
+}
+
+//捞取商机
+export function gainBusiness(data) {
+  return request({
+    url: '/crm/business/gain',
+    method: 'post',
+    data: data
+  })
+}
+
+// 下载用户导入模板
+export function importTemplate() {
+  return request({
+    url: '/crm/business/importTemplate',
+    method: 'get'
+  })
+}
+
+// // 删除商机
+// export function setBusinessPool(businessId) {
+//   return request({
+//     url: '/crm/business/' + businessId,
+//     method: 'get'
+//   })
+// }

+ 18 - 0
src/api/qw/customerProperty.js

@@ -0,0 +1,18 @@
+import request from '@/utils/request'
+
+export function listByCustomerId(params) {
+  return request({
+    url: '/qw/customerProperty/list',
+    method: 'get',
+    params: params
+  })
+}
+// 调用ai分析标签
+export function analyzeAiTagByTrade(data) {
+  return request({
+    url: '/qw/customerProperty/analyzeAiTagByTrade',
+    method: 'post',
+    data: data
+  })
+}
+

+ 12 - 0
src/api/qw/qwAnalyze.js

@@ -0,0 +1,12 @@
+import request from '@/utils/request'
+
+// 查询客户聊天记录分析列表
+export function listAnalyze(query) {
+  return request({
+    url: '/qw/analyze/list',
+    method: 'get',
+    params: query
+  })
+}
+
+

+ 9 - 0
src/api/watch/common.js

@@ -0,0 +1,9 @@
+import request from '@/utils/request'
+import { status } from 'nprogress'
+
+const baseURL = process.env.VUE_APP_BASE_API
+// watch端 通用下载方法
+export function watchDownload(fileName) {
+    window.location.href = "/watch-api/watchCommon/download?fileName=" + encodeURI(fileName) + "&delete=" + true;
+}
+

+ 121 - 5
src/api/watch/deviceInfo.js

@@ -9,7 +9,6 @@ export function listDeviceInfo(query) {
     params: query
   })
 }
-
 // 查询设备信息列表
 export function myDeviceInfo(query) {
   return request({
@@ -18,7 +17,6 @@ export function myDeviceInfo(query) {
     params: query
   })
 }
-
 // 查询设备信息详细
 export function getDeviceInfo(deviceId) {
   return request({
@@ -90,11 +88,11 @@ export function exportMy(query) {
 }
 
   // 查询用户信息
-export function getUser(deviceId) {
+export function getUser(data) {
   return request({
     url: '/watch-api/device/set/up/getUserInfo',
     method: 'get',
-    params: { deviceId } 
+    params:data
   })
 }
 
@@ -123,6 +121,14 @@ export function queryByDate(date,deviceId) {
   })
 }
 
+export function queryHrList(data) {
+  return request({
+    url: `/watch-api/watch/heart/rate/list`,
+    method: 'get',
+    params: data
+  })
+}
+
 //exportHeartDate导出
 export function exportHeartDate(date,deviceId) {
   return request({
@@ -168,6 +174,15 @@ export function queryGlucoseDate(date,deviceId) {
   })
 }
 
+// 根据时间获取血糖数据
+export function queryGlucoseList(data) {
+  return request({
+    url: `/watch-api/watch/blood/glucose/list`,
+    method: 'get',
+    params: data
+  })
+}
+
 // 根据时间获取房颤数据
 export function queryAfDate(date,deviceId) {
   return request({
@@ -186,6 +201,15 @@ export function queryPressure(date,deviceId) {
   })
 }
 
+// 根据时间获取血压数据
+export function queryPressureList(data) {
+  return request({
+    url: `/watch-api/watch/blood/pressure/list`,
+    method: 'get',
+    params:data
+  })
+}
+
 // 根据时间获取温度数据
 export function queryTemperature(date,deviceId) {
   return request({
@@ -213,6 +237,15 @@ export function querySpo2(date,deviceId) {
   })
 }
 
+// 根据时间获取血氧数据
+export function querySpo2List(data) {
+  return request({
+    url: `/watch-api/watch/spo2/data/list`,
+    method: 'get',
+    params: data
+  })
+}
+
 // 根据时间获取睡眠数据
 export function querySleep(date,deviceId) {
   return request({
@@ -222,6 +255,15 @@ export function querySleep(date,deviceId) {
   })
 }
 
+// 根据时间获取睡眠数据
+export function querySleepList(data) {
+  return request({
+    url: `/watch-api/watch/sleep/data/list`,
+    method: 'get',
+    params: data
+  })
+}
+
 // 获取预警数据
 export function queryAlarm(status) {
   return request({
@@ -288,6 +330,15 @@ export function queryUaData(data) {
   })
 }
 
+//获取尿酸数据
+export function queryUaList(data) {
+  return request({
+      url: '/watch-api/watch/third/ua/list',
+      method: 'get',
+      params:data
+  })
+}
+
 //获最新健康数据
 export function queryLastHealthData(data) {
   return request({
@@ -350,4 +401,69 @@ export function queryTemperaturePageByDate(data) {
       method: 'get',
       params:data
   })
-}
+}
+
+//查询某个时间段的温度数据 分页
+export function queryBOPageByDate(data) {
+  return request({
+      url: '/watch-api/watch/continuous/spo2/data/queryPageByDateAndDeviceId',
+      method: 'get',
+      params:data
+  })
+}
+
+//物联网 分页
+export function queryIotList(data) {
+  return request({
+      url: '/watch-api/watch/deviceInfo/query',
+      method: 'get',
+      params:data
+  })
+}
+
+//查询某个时间段的压力数据 分页
+export function queryFatiguePageByDate(data) {
+  return request({
+      url: '/watch-api/watch/fatigue/page',
+      method: 'get',
+      params:data
+  })
+}
+
+// 根据时间获取压力数据
+export function queryFatigue(date,deviceId) {
+  return request({
+    url: `/watch-api/watch/fatigue/queryByDateAndDeviceId`,
+    method: 'get',
+    params: { date,deviceId } 
+  })
+}
+
+
+//健康周报-步数,卡路里,睡眠等信息
+export function getUserHealthInfoByDeviceId(data) {
+  return request({
+      url: '/watch-api/watch/sport/data/getUserHealthInfoByDeviceId',
+      method: 'get',
+      params:data
+  })
+}
+
+//健康周报-步数,卡路里,睡眠等信息
+export function queryHealthReport(data) {
+  return request({
+      url: '/watch-api/device/queryHealthReport',
+      method: 'get',
+      params:data
+  })
+}
+
+//舌诊
+export function queryTongueList(data) {
+  return request({
+      url: '/watch-api/device/getHealthTongueList',
+      method: 'get',
+      params:data
+  })
+}
+

+ 1 - 1
src/components/DeviceInfo/AtrialFibrillation.vue

@@ -21,7 +21,7 @@
     </div>
     <div class="afib-diagnosis">
       <div class="afib-diagnosis-title">诊断结论:</div>
-      <div class="afib-diagnosis-note">{{conclusion||'没有房颤数据'}}</div>
+      <div class="afib-diagnosis-note">{{conclusion||'今日暂无数据,请选择其他日期'}}</div>
       <div class="afib-diagnosis-tips">
         注:非医疗诊断,供参考,请以医院确诊结论为准。
       </div>

+ 5 - 0
src/components/DeviceInfo/BloodSugar.vue

@@ -17,6 +17,11 @@
             </el-table-column>
             <el-table-column prop="bloodGlucose" align="center" label="测量结果">
             </el-table-column>
+            <template #empty>
+                <div>
+                    今日暂无数据,请选择其他日期
+                </div>
+            </template>
         </el-table>
 
     </div>

+ 8 - 3
src/components/DeviceInfo/Bloodoxygen.vue

@@ -22,6 +22,11 @@
             <el-table-column type="index" label="序号" align="center"/>
             <el-table-column label="时间" align="center" prop="time" />
             <el-table-column label="血氧" align="center" prop="avgBoxy" />
+            <template #empty>
+                <div>
+                    今日暂无数据,请选择其他日期
+                </div>
+            </template>
             </el-table>
             <pagination v-show="(total>0) && detailsType" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getDataPage"/>
         </div>
@@ -87,9 +92,9 @@ export default {
                 xAxis: {
                     type: "time",
                     splitNumber: 6,
-                    axisLine: {
-                        show: false,
-                    },
+                    // axisLine: {
+                    //     show: false,
+                    // },
                     axisTick: {
                         show: false,
                     },

+ 5 - 0
src/components/DeviceInfo/Heartrate.vue

@@ -65,6 +65,11 @@
         <el-table-column label="最小心率" align="center" prop="minBpm" />
         <el-table-column label="状态" align="center" prop="status" :formatter="statusFormatter"/>
         <el-table-column label="时间" align="center" prop="createTime" />
+        <template #empty>
+            <div>
+                今日暂无数据,请选择其他日期
+            </div>
+        </template>
       </el-table>
       
     </div>

+ 9 - 4
src/components/DeviceInfo/Pressure.vue

@@ -24,8 +24,13 @@
                 <el-table-column label="sbp(mmgh)" align="center" prop="sbp" />
                 <el-table-column label="dbp(mmgh)" align="center" prop="dbp" />
                 <el-table-column label="hr(次/分)" align="center" prop="hr" />
+                <template #empty>
+                    <div>
+                        今日暂无数据,请选择其他日期
+                    </div>
+                </template>
             </el-table>
-            <pagination v-show="(dataListTotal > 0) && detailsType" :total="dataListTotal" :page.sync="tqueryParams.pageNum" :limit.sync="tqueryParams.pageSize" @pagination="getTableList"/>
+            <pagination v-show="(dataListTotal>0) && detailsType" :total="dataListTotal" :page.sync="tqueryParams.pageNum" :limit.sync="tqueryParams.pageSize" @pagination="getTableList"/>
         </div>
     </div>
 </template>
@@ -66,9 +71,9 @@ export default {
                     boundaryGap: false,
                     type: "time",
                     splitNumber: 6,
-                    axisLine: {
-                        show: false,
-                    },
+                    // axisLine: {
+                    //     show: false,
+                    // },
                     axisTick: {
                         show: false,
                     },

+ 291 - 0
src/components/DeviceInfo/PressureValue.vue

@@ -0,0 +1,291 @@
+<template>
+    <div>
+        <div class="box-header">
+            <div class="box-title boldtext">
+                压力 <span style="margin-left: 14px">{{ currentDate }}</span>
+            </div>
+            <div>
+                <!-- <el-button @click="exportData" size="small" style="margin-right: 20px"
+            >导出</el-button
+          > -->
+                <el-date-picker size="small" v-model="currentDatePicker" type="date" placeholder="选择日期"
+                    value-format="yyyy-MM-dd" @change="datePickerchange">
+                </el-date-picker>
+            </div>
+        </div>
+        <div v-show="!detailsType">
+            <div style="min-width: 841px; height: 320px; width: 100%;">
+                <div ref="heartrateChart" style="width: 100%; height: 100%;min-width: 783px;"></div>
+            </div>
+        </div>
+        <!-- 列表展示 -->
+        <div v-show="detailsType">
+            <el-table :data="dataList" style="width: 100%">
+                <el-table-column prop="createTime" label="时间" />
+                <el-table-column prop="pressure" label="压力值" />
+                <template #empty>
+                    <div>
+                        今日暂无数据,请选择其他日期
+                    </div>
+                </template>
+            </el-table>
+            <pagination v-show="(total > 0) && detailsType" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getDataPage"/>
+        </div>
+        
+    </div>
+</template>
+
+<script>
+import * as echarts from "echarts";
+import { queryFatiguePageByDate, queryFatigue} from "@/api/watch/deviceInfo";
+export default {
+    props: {
+        detailsType: { 
+            type: Boolean,
+            default: false
+        },
+        deviceId: {
+            type: String || Number,
+            default: "",
+        },
+    },
+    data() {
+        return {
+            total:0,
+            dataList:[],
+            queryParams:{
+                pageNum: 1,
+                pageSize: 10,
+                deviceId:"",
+                beginTime:"",
+                endTime:""
+            },
+            currentDate: this.parseTime(new Date(), "{y}-{m}-{d}"),
+            currentDatePicker: "",
+            exportLoading: false,
+            chart: null,
+            chartOptions: {
+                tooltip: {
+                    trigger: 'axis'
+                },
+                legend: {},
+                xAxis: {
+                    boundaryGap: false,
+                    type: "time",
+                    splitNumber: 6,
+                    axisTick: {
+                        show: false,
+                    },
+                    splitLine: {
+                        show: false,
+                    },
+                    axisLabel: {
+                        color: "#999999",
+                        margin: 20,
+                        // fontSize: 14,
+                        fontWeight: "bold",
+                        formatter: (value) => {
+                            return this.parseTime(value, "{h}:{i}");
+                        },
+                    }
+                },
+                yAxis: {
+                    type: 'value',
+                    max: 100, // 设置y轴最大值为100
+                    // splitLine: {
+                    //     show: false // 去掉y轴的横线
+                    // }
+                },
+                series: [
+                    {
+                        name: '压力值',
+                        type: 'line',
+                        data: [],
+                        smooth: true
+                    }
+                    
+                ]
+            }
+
+        }
+    },
+    methods: {
+        getDataPage(){
+            this.loading = true;
+            this.queryParams.deviceId = this.deviceId;
+            this.queryParams.beginTime = this.currentDate + " 00:00:00";
+            this.queryParams.endTime = this.currentDate + " 23:59:59";
+            queryFatiguePageByDate(this.queryParams)
+                .then((response) => {
+                if (response.data) {
+                    this.dataList = response.data.rows;
+                    this.total = response.data.total;
+                    this.loading = false;
+                } else {
+                    console.warn("分页数据返回为空或格式不正确:", response);
+                }
+                })
+                .catch((error) => {
+                console.error("获取分页数据失败:", error);
+                });
+        },
+        datePickerchange() {
+            this.currentDate = this.currentDatePicker || this.parseTime(new Date(), "{y}-{m}-{d}");
+            if(this.detailsType){
+                this.getDataPage();
+            } else{
+                this.getDetailsData();
+            }
+        },
+        initChart() {
+            const chartElement = this.$refs.heartrateChart;
+            const sportChartElement = this.$refs.sportHeartrate;
+            if (chartElement) {
+                this.chart = echarts.init(chartElement);
+                this.seChartOptions()
+            }
+            if (sportChartElement) {
+                this.sportChart = echarts.init(sportChartElement);
+                this.sportChart.setOption(this.sportChartOption);
+            }
+        },
+        seChartOptions() {
+            this.chartOptions.xAxis.min = this.currentDate + ' 00:00:00'
+            this.chartOptions.xAxis.max = this.currentDate + ' 23:59:59'
+            this.chart.setOption(this.chartOptions);
+        },
+        // 获取体温折线图
+        getDetailsData() {
+            queryFatigue(this.currentDate, this.deviceId)
+                .then((response) => {
+                    if (response && response.data) {
+                        // console.log("------------------" + JSON.stringify(response.data, null, 2))
+                        const data = response.data.map((item) => (
+                            {
+                                value: [
+                                    new Date(item.createTime).getTime(),
+                                    item.pressure,
+                                ],
+                            }));
+                     
+
+                        // 将数据分别赋值给不同的 series
+                        this.chartOptions.series[0].data = data;
+                        // 更新图表
+                        if (this.detailsType == false) {
+                            this.initChart();
+                        }
+                    } else {
+                        console.warn("数据返回为空或格式不正确:", response);
+                    }
+                })
+                .catch((error) => {
+                    console.error("获取数据失败:", error);
+                });
+        },
+       
+    },
+};
+</script>
+
+<style lang="scss" scoped>
+@mixin u-flex($flexD, $alignI, $justifyC) {
+    display: flex;
+    flex-direction: $flexD;
+    align-items: $alignI;
+    justify-content: $justifyC;
+}
+
+.fz15 {
+    font-size: 15px !important;
+}
+
+.boldtext {
+    font-size: 16px;
+    color: #292929;
+    line-height: 30px;
+    font-weight: 700;
+}
+
+.box-header {
+    padding: 0 15px;
+    @include u-flex(row, center, space-between);
+    height: 50px;
+}
+
+.box-title {
+    color: #292929;
+    font-size: 16px;
+    font-family: PingFang SC-Medium;
+    padding-left: 10px;
+    border-left: 4px solid #2284ff;
+    line-height: 13px;
+}
+
+.heartrate-data {
+    @include u-flex(row, center, space-around);
+    margin-top: 30px;
+
+    &-item {
+        height: 60px;
+        width: 196px;
+        border-radius: 4px;
+        box-shadow: 0 0 10px rgba(1, 24, 54, 0.1);
+        overflow: hidden;
+        @include u-flex(row, center, flex-start);
+
+        img {
+            height: 41px;
+            width: 41px;
+        }
+    }
+
+    &-imgbox {
+        flex-shrink: 0;
+        height: 60px;
+        width: 60px;
+        @include u-flex(row, center, center);
+    }
+
+    &-right {
+        flex: 1;
+        text-align: center;
+        font-size: 13px;
+        color: #606165;
+        font-family: PingFang SC-Medium;
+        background: #fff;
+        height: 60px;
+    }
+
+    &-num {
+        font-size: 22px;
+        color: #2c2c3b;
+        line-height: 38px;
+    }
+}
+
+.heartrate-sport {
+    @include u-flex(row, flex-start, flex-start);
+    margin-top: 20px;
+
+    &-l {
+        width: 490px;
+        flex-shrink: 0;
+    }
+
+    &-r {
+        flex: 1;
+        max-width: 720px;
+    }
+
+    &-lechart {
+        width: 490px;
+        height: 300px;
+    }
+
+    &-rechart {
+        width: 100%;
+        @include u-flex(row, center, center);
+    }
+}
+</style>

+ 6 - 1
src/components/DeviceInfo/Pulse.vue

@@ -70,8 +70,13 @@
           <el-table-column type="index" label="序号" align="center"/>
           <el-table-column label="时间" align="center" prop="time" />
           <el-table-column label="脉搏" align="center" prop="pulseRate" />
+          <template #empty>
+              <div>
+                  今日暂无数据,请选择其他日期
+              </div>
+          </template>
         </el-table>
-        <pagination v-show="(dataListTotal > 0) && detailsType" :total="dataListTotal" :page.sync="tqueryParams.pageNum" :limit.sync="tqueryParams.pageSize" @pagination="getTableList"/>
+        <pagination v-show="(dataListTotal>0) && detailsType" :total="dataListTotal" :page.sync="tqueryParams.pageNum" :limit.sync="tqueryParams.pageSize" @pagination="getTableList"/>
       </div>
     </div>
   </template>

+ 1942 - 0
src/components/DeviceInfo/Report.vue

@@ -0,0 +1,1942 @@
+<template>
+    <div class="weekly-report no-scrollbar">
+      <div class="report-header">
+        <div class="report-title">
+          <img class="title-icon" src="@/assets/images/watchApi/title-icon.png" />
+          <span>周报</span>
+        </div>
+        <div class="date-selector">
+          <el-date-picker
+            :picker-options="{ firstDayOfWeek: 1 }"
+            v-model="selectedDate"
+            type="week"
+            format="yyyy 第 WW 周"
+            placeholder="选择周"
+            @change="handleDateChange"
+          ></el-date-picker>
+        </div>
+      </div>
+  
+      <el-card class="report-card">
+        <div class="progress-section">
+          <div class="progress-container">
+            <el-progress type="circle" :percentage="healthIndex" :width="120" color="#FF9F43"></el-progress>
+            <div class="progress-info">
+              <div class="progress-label">健康指数</div>
+            </div>
+            <div class="health-status">{{ healthStatus }}</div>
+            <div class="health-description">{{ healthDesc }}</div>
+          </div>
+          
+          <div class="health-metrics-container">
+            <div class="health-metrics-row">
+              <div class="metric-item">
+                <div class="metric-data">
+                  <div class="metric-value">{{ height }}<span class="metric-unit">cm</span></div>
+                  <div class="metric-label">身高</div>
+                </div>
+                <div class="metric-icon orange">
+                  <i class="el-icon-sort"></i>
+                </div>
+              </div>
+              <div class="metric-item">
+                <div class="metric-data">
+                  <div class="metric-value">{{ weight }}<span class="metric-unit">kg</span></div>
+                <div class="metric-label">体重</div>
+                </div>
+                <div class="metric-icon green">
+                  <i class="el-icon-top"></i>
+                </div>
+              </div>
+            </div>
+            
+            <div class="health-metrics-row">
+              <div class="metric-item">
+                <div class="metric-data">
+                  <div class="metric-value">{{ bmiStatus }}</div>
+                <div class="metric-label">BMI</div>
+                </div>
+                <div class="metric-icon blue">
+                  <i class="el-icon-plus"></i>
+                </div>
+              </div>
+              <div class="metric-item">
+                <div class="metric-data">
+                  <div class="metric-value">{{ caloriesData.value }}<span class="metric-unit">{{ caloriesData.unit }}</span></div>
+                  <div class="metric-label">{{ caloriesData.label }}</div>
+                </div>
+                <div class="metric-icon red">
+                  <i class="el-icon-hot-water"></i>
+                </div>
+              </div>
+            </div>
+            
+            <div class="health-metrics-row">
+              <div class="metric-item">
+                <div class="metric-data">
+                  <div class="metric-value">{{ stepsData.value }}<span class="metric-unit">{{ stepsData.unit }}</span></div>
+                  <div class="metric-label">{{ stepsData.label }}</div>
+                </div>
+                <div class="metric-icon orange">
+                  <i class="el-icon-s-promotion"></i>
+                </div>
+              </div>
+              <div class="metric-item">
+                <div class="metric-data">
+                  <div class="metric-value">{{ sleepCardData.status }}</div>
+                <div class="metric-label">{{ sleepCardData.label }}</div>
+                </div>
+                <div class="metric-icon purple">
+                  <i class="el-icon-moon"></i>
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+  
+        <!-- 健康数据 (Health Data) Section -->
+        <div class="health-data-section">
+          <div class="section-title">健康数据</div>
+          
+          <!-- 血糖趋势 (Blood Sugar Trend) -->
+          <div class="trend-chart">
+            <div class="trend-title">血糖趋势</div>
+            <div class="trend-subtitle">单位: mmol/L</div>
+            <div class="line-chart-container" ref="bloodSugarChart"></div>
+            <div class="trend-advice">
+            </div>
+          </div>
+          
+          <!-- 尿酸趋势 (Uric Acid Trend) -->
+          <div class="trend-chart">
+            <div class="trend-title">尿酸趋势</div>
+            <div class="trend-subtitle">单位: μmol/L</div>
+            <div class="bar-chart-container" ref="uricAcidChart"></div>
+            <div class="legend">
+              <div class="legend-item">
+                <span class="legend-color" style="background-color: #4CD964;"></span>
+                <span>150~416μmol/L</span>
+              </div>
+              <div class="legend-item">
+                <span class="legend-color" style="background-color: #FFCC00;"></span>
+                <span>&lt;150μmol/L</span>
+              </div>
+              <div class="legend-item">
+                <span class="legend-color" style="background-color: #FF6B6B;"></span>
+                <span>&gt;416μmol/L</span>
+              </div>
+            </div>
+          </div>
+
+           <div class="trend-chart">
+            <div class="trend-title">血压趋势</div>
+            <div class="trend-subtitle">单位: mmgh</div>
+            <div class="line-chart-container" ref="bloodPressureChart"></div>
+          </div>
+
+           <div class="trend-chart">
+            <div class="trend-title">心率趋势</div>
+            <div class="trend-subtitle">单位: 次/分</div>
+            <div class="line-chart-container" ref="heartRateChart"></div>
+          </div>
+
+           <div class="trend-chart">
+            <div class="trend-title">血氧趋势</div>
+            <div class="trend-subtitle"></div>
+            <div class="line-chart-container" ref="bloodOxygenChart"></div>
+            <div class="legend">
+              <div class="legend-item">
+                <span class="legend-color" style="background-color: #4CD964;"></span>
+                <span>&lt;90%</span>
+              </div>
+              <div class="legend-item">
+                <span class="legend-color" style="background-color: #FFCC00;"></span>
+                <span>&lt;95%</span>
+              </div>
+              <div class="legend-item">
+                <span class="legend-color" style="background-color: #FF6B6B;"></span>
+                <span>其他</span>
+              </div>
+            </div>
+          </div>
+
+           <div class="trend-chart">
+            <div class="trend-title">睡眠趋势</div>
+            <div class="trend-subtitle"></div>
+            <div class="line-chart-container" ref="sleepChart"></div>
+            <div class="legend">
+              <div class="legend-item">
+                <span class="legend-color" style="background-color: #8C37E6;"></span>
+                <span>深睡</span>
+              </div>
+              <div class="legend-item">
+                <span class="legend-color" style="background-color: #D138CF;"></span>
+                <span>浅睡</span>
+              </div>
+              <div class="legend-item">
+                <span class="legend-color" style="background-color: #F88082;"></span>
+                <span>快速眼动</span>
+              </div>
+              <div class="legend-item">
+                <span class="legend-color" style="background-color: #FDBD27;"></span>
+                <span>清醒</span>
+              </div>
+            </div>
+          </div>
+          
+         
+        </div>
+  
+        
+        <div class="health-archive-section">
+          <div class="section-title">健康档案</div>
+          <div class="archive-items">
+            <!-- 健康预测 -->
+            <div class="archive-item">
+              <div class="archive-icon warning">
+                <i class="el-icon-warning-outline"></i>
+              </div>
+              <div class="archive-content">
+                <div class="archive-title">健康预测</div>
+                <div class="archive-text" v-for="(item, idx) in healthArchive" :key="'prediction' + idx">
+                  {{ item.prediction }}
+                </div>
+              </div>
+            </div>
+
+            <!-- 解析 -->
+            <div class="archive-item">
+              <div class="archive-icon analysis">
+                <i class="el-icon-data-analysis"></i>
+              </div>
+              <div class="archive-content">
+                <div class="archive-title">解析</div>
+                <div class="archive-text" v-for="(item, idx) in healthArchive" :key="'analysis' + idx">
+                  {{ item.analysis }}
+                </div>
+              </div>
+            </div>
+
+            <!-- 治疗建议 -->
+            <div class="archive-item">
+              <div class="archive-icon suggestion">
+                <i class="el-icon-first-aid-kit"></i>
+              </div>
+              <div class="archive-content">
+                <div class="archive-title">治疗建议</div>
+                <div class="archive-text" v-for="(item, idx) in healthArchive" :key="'suggestion' + idx">
+                  {{ item.suggestion }}
+                </div>
+              </div>
+            </div>
+
+            <!-- 注意事项 -->
+            <div class="archive-item">
+              <div class="archive-icon attention">
+                <i class="el-icon-info"></i>
+              </div>
+              <div class="archive-content">
+                <div class="archive-title">注意事项</div>
+                <div class="archive-text" v-for="(item, idx) in healthArchive" :key="'precautions' + idx">
+                  {{ item.precautions }}
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+        
+        <!-- AI舌诊 (AI Tongue Diagnosis) Section -->
+        <div class="ai-diagnosis-section">
+          <div class="section-title">AI舌诊</div>
+          <div class="date-selector-tabs">
+            <div 
+              v-for="date in weekDates" 
+              :key="date"
+              :class="['date-tab', selectedDiagnosisDate === date ? 'active' : '']"
+              @click="selectedDiagnosisDate = date"
+            >
+              {{ date }}
+            </div>
+          </div>
+          
+          <div class="diagnosis-content" v-if="tongueData">
+            <div class="diagnosis-time">检测时间 {{ tongueData.createTime || '未知时间' }}</div>
+            
+            <div class="diagnosis-result">
+              <div class="diagnosis-label">您属于</div>
+              <div class="diagnosis-type">{{ tongueData.typeName || '未知体质' }}</div>
+            </div>
+            
+            <div class="diagnosis-section">
+              <div class="diagnosis-section-title">舌苔特征</div>
+              <div class="diagnosis-feature">
+                <div class="feature-dot"></div>
+                <div class="feature-title">{{ tongueData.taiseName || '未知' }}</div>
+                <div class="feature-description">{{ tongueData.taiseDesc || '未知' }}</div>
+              </div>
+              <div class="diagnosis-feature">
+                <div class="feature-dot"></div>
+                <div class="feature-title">{{ tongueData.shemianName || '未知' }}</div>
+                <div class="feature-description">{{ tongueData.shemianDesc || '未知' }}</div>
+              </div>
+            </div>
+            
+            <div class="diagnosis-section">
+              <!-- <div class="diagnosis-section-title">体质解析</div> -->
+              <div 
+                class="diagnosis-analysis" 
+                v-for="(item, index) in tongueData.typeJsonObj" 
+                :key="index"
+              >
+                <div class="diagnosis-section-title">{{ item.name }}</div>
+                <div class="analysis-content">{{ item.value }}</div>
+              </div>
+            </div>
+          </div>
+          <div class="diagnosis-content" v-else>
+            <div class="no-data">暂无舌诊数据</div>
+          </div>
+        </div>
+      </el-card>
+    </div>
+  </template>
+  
+  <script>
+  import * as echarts from 'echarts';
+  import {getUserHealthInfoByDeviceId,queryHealthReport,queryGlucoseList,queryUaList,queryPressureList,queryHrList,querySpo2List,querySleepList,queryTongueList} from "@/api/watch/deviceInfo";
+  export default {
+    props: {
+        // detailsType: { 
+        //     type: Boolean,
+        //     default: false
+        // },
+        userInfo: {
+          type: Object,
+          default: () => ({})
+        },
+        deviceId: {
+            type: String || Number,
+            default: "",
+        },
+    },
+    name: 'WeeklyReport',
+    data() {
+      return {
+        sleepDefaultInfo: {
+          4: { name: "浅睡", color: "#D138CF", yValue: 7 },
+          3: { name: "深睡", color: "#8C37E6", yValue: 3 },
+          6: { name: "清醒", color: "#FDBD27", yValue: 9 },
+          7: { name: "快速眼动", color: "#F88082", yValue: 12 },
+        },
+        startTime: null,
+        endTime:  null,
+        // 基础数据
+        height: '--',
+        weight: '--',
+        bmi: 0,
+        bmiStatus: '--',
+        healthIndex: 0,
+        healthStatus: '--',
+        healthDesc:"",
+        // 健康指标数据
+        caloriesData: {
+          value: '--',
+          unit: 'kcal',
+          label: '卡路里'
+        },
+        stepsData: {
+          value: '--',
+          unit: '步',
+          label: '步数'
+        },
+        sleepCardData: {
+          status: '--',
+          label: '睡眠'
+        },
+        selectedDate: new Date(),
+        // 健康档案
+        healthArchive: [],
+        selectedDiagnosisDate: '12/27',
+        bloodSugarChart: null,
+        uricAcidChart: null,
+        bloodPressureChart:null,
+        heartRateChart:null,
+        bloodOxygenChart:null,
+        sleepChart:null,
+        bloodSugarData: {
+          values: []
+        },
+       
+        uricAcidData: {
+          values: [
+          ]
+        },
+        bloodPressureData:{},
+        heartRateData:{},
+        bloodOxygenData:{values: []},
+        sleepData:[],
+        tongueData:null
+      };
+    },
+    mounted() {
+      this.$nextTick(() => {
+        this.initBloodSugarChart();
+        this.initBloodPressureChart();
+        this.initUricAcidChart();
+        this.initHeartRateChart();
+        this.initBloodOxygenChart();
+        this.initSleepChart();
+        this.setInitialDiagnosisDate();
+      });
+    },
+    computed: {
+      weekDates() {
+        const dates = [];
+        const startOfWeek = new Date(this.selectedDate);
+        startOfWeek.setDate(this.selectedDate.getDate() - (this.selectedDate.getDay() || 7) + 1); // Set to Monday
+        for (let i = 0; i < 7; i++) {
+          const date = new Date(startOfWeek);
+          date.setDate(startOfWeek.getDate() + i);
+          dates.push(this.formatDate(date));
+        }
+        return dates;
+      }
+    },
+    watch: {
+      userInfo: {
+        handler(newVal) {
+          if (newVal) {
+            this.updateUserInfo();
+          }
+        },
+        immediate: true,
+        deep: true
+      },
+      selectedDate() {
+        this.setInitialDiagnosisDate();
+      },
+      selectedDiagnosisDate(newVal) {
+        if (newVal) {
+          this.getTongueDate();
+        }
+      }
+    },
+    methods: {
+      formatDate(date) {
+        return `${date.getMonth() + 1}/${date.getDate()}`;
+      },
+      setInitialDiagnosisDate() {
+        this.selectedDiagnosisDate = this.weekDates[0] || null;
+      },
+      getWeekDates(date) {
+        const startOfWeek = new Date(date);
+        const endOfWeek = new Date(date);
+        startOfWeek.setDate(date.getDate() - (date.getDay() || 7) + 1); // 如果是周日,getDay() 返回 0,需要加 7
+         // 将日期设置为周一,时间为00:00:00
+        // startOfWeek.setDate(date.getDate() - date.getDay());
+        startOfWeek.setHours(0, 0, 0, 0);
+
+        // 将日期设置为周日,时间为23:59:59
+        endOfWeek.setDate(date.getDate() - (date.getDay() || 7) + 7); // 如果是周日,getDay() 返回 0,需要加 7
+        // endOfWeek.setDate(date.getDate() - date.getDay() + 6);
+        endOfWeek.setHours(23, 59, 59, 999);
+       
+        this.startTime= startOfWeek,
+        this.endTime=  endOfWeek
+      },
+      // 更新基本信息
+      updateUserInfo() {
+        if (this.userInfo) {
+          // 设置身高体重
+          this.height = this.userInfo.height || '--';
+          this.weight = this.userInfo.weight || '--';
+          
+          // 计算BMI
+          this.calculateBMI();}
+      },
+      // 计算BMI
+      calculateBMI() {
+        if (this.height && this.weight && this.height !== '--' && this.weight !== '--') {
+          // BMI = 体重(kg) / 身高(m)²
+          const heightInMeters = parseFloat(this.height) / 100;
+          const weightInKg = parseFloat(this.weight);
+          
+          if (!isNaN(heightInMeters) && !isNaN(weightInKg) && heightInMeters > 0) {
+            this.bmi = (weightInKg / (heightInMeters * heightInMeters)).toFixed(1);
+            
+            // 设置BMI状态
+            if (this.bmi < 18.5) {
+              this.bmiStatus = '偏瘦';
+            } else if (this.bmi >= 18.5 && this.bmi < 24) {
+              this.bmiStatus = '正常';
+            } else if (this.bmi >= 24 && this.bmi < 28) {
+              this.bmiStatus = '超重';
+            } else {
+              this.bmiStatus = '肥胖';
+            }
+          }
+        }
+      },
+      async getSportData() {
+        try{
+          const params = {
+            deviceId: this.deviceId,
+            startTime: this.parseTime(this.startTime),
+            endTime: this.parseTime(this.endTime),
+          };
+          const response = await getUserHealthInfoByDeviceId(params);
+          if(response.data){
+            this.caloriesData.value = response.data.calorie;
+            this.stepsData.value = response.data.step;
+            this.sleepData.status = response.data.sleepStatus;
+          }
+        }catch(error){
+          console.error("获取步数,卡路里,睡眠等信息数据失败:", error);
+        }
+      },
+
+      async getHealthArchive() {
+        try{
+          const params = {
+            deviceId: this.deviceId,
+            startTime: this.parseTime(this.startTime),
+            endTime: this.parseTime(this.endTime),
+          };
+          const response = await queryHealthReport(params);
+          if(response.data){
+            this.healthArchive = response.data.list;
+            this.healthIndex = response.data.score;
+            this.healthStatus = response.data.title;
+            this.healthDesc = response.data.desc;
+
+          }
+        }catch(error){
+          console.error("获取步数,卡路里,睡眠等信息数据失败:", error);
+        }
+      },
+      async getGlucoseDate() {
+        try{
+          const params = {
+            deviceId: this.deviceId,
+            startTime: this.parseTime(this.startTime,'{y}/{m}/{d} {h}:{i}:{s}'),
+            endTime: this.parseTime(this.endTime,'{y}/{m}/{d} {h}:{i}:{s}'),
+          };
+          const response = await queryGlucoseList(params);
+          if(response.data && response.data.length > 0){
+            const values = response.data.map(item => {
+              const timestamp = new Date(item.createTime).getTime();
+              return [timestamp, item.bloodGlucose, item.createTime];
+            });
+            this.bloodSugarData = {
+              values: values
+            };
+        
+          } else {
+            this.bloodSugarData = {
+              values: []
+            };
+          }
+        }catch(error){
+          console.error("获取血糖数据失败:", error);
+          this.bloodSugarData = {
+            values: []
+          };
+        }
+      },
+
+      async getUaDate() {
+        try{
+          const params = {
+            deviceId: this.deviceId,
+            startTime: this.parseTime(this.startTime,'{y}/{m}/{d} {h}:{i}:{s}'),
+            endTime: this.parseTime(this.endTime,'{y}/{m}/{d} {h}:{i}:{s}'),
+          };
+          const response = await queryUaList(params);
+          if(response.data && response.data.length > 0){
+            const values = response.data.map(item => {
+              const timestamp = new Date(item.createTime).getTime();
+              const val = item.val/10;
+              let color;
+              if (val < 150) {
+                  color = '#FFCC00';
+              } else if (val < 416) {
+                color = '#4CD964';
+              } else {
+                color = '#FF6B6B';
+              }
+              return [timestamp, val, item.createTime,color];
+            });
+            this.uricAcidData.values = values;
+        
+          } else {
+            this.uricAcidChart.values = [];
+          }
+        }catch(error){
+          console.error("获取尿酸数据失败:", error);
+          this.uricAcidChart.values = [];
+        }
+      },
+      async getSleepDate() {
+        try{
+          const params = {
+            deviceId: this.deviceId,
+            startTime: this.parseTime(this.startTime,'{y}/{m}/{d} {h}:{i}:{s}'),
+            endTime: this.parseTime(this.endTime,'{y}/{m}/{d} {h}:{i}:{s}'),
+          };
+          const response = await querySleepList(params);
+          if(response.data.sleepSection && response.data.sleepSection.length > 0){
+            this.sleepData = response.data.sleepSection;
+          }
+        }catch(error){
+          console.error("获取睡眠数据失败:", error);
+        }
+      },
+      async querySpo2Data() {
+        try{
+          const params = {
+            deviceId: this.deviceId,
+            startTime: this.parseTime(this.startTime,'{y}/{m}/{d} {h}:{i}:{s}'),
+            endTime: this.parseTime(this.endTime,'{y}/{m}/{d} {h}:{i}:{s}'),
+          };
+          const response = await querySpo2List(params);
+          if(response.data.list && response.data.list.length > 0){
+            const values = response.data.list.map(item => {
+              const timestamp = new Date(item.createTime).getTime();
+              const val = item.avgBoxy;
+              let color;
+              if (val < 150) {
+                  color = '#FFCC00';
+              } else if (val < 416) {
+                color = '#4CD964';
+              } else {
+                color = '#FF6B6B';
+              }
+              return [timestamp, val, item.createTime,color];
+            });
+            this.bloodOxygenData.values = values;
+        
+          } else {
+            this.bloodOxygenData.values = [];
+          }
+        }catch(error){
+          console.error("获取尿酸数据失败:", error);
+          this.bloodOxygenData.values = [];
+        }
+      },
+
+      async getHrDate() {
+        try{
+          const params = {
+            deviceId: this.deviceId,
+            startTime: this.parseTime(this.startTime,'{y}/{m}/{d} {h}:{i}:{s}'),
+            endTime: this.parseTime(this.endTime,'{y}/{m}/{d} {h}:{i}:{s}'),
+          };
+          const response = await queryHrList(params);
+          if(response.data.list && response.data.list.length > 0){
+            const values = response.data.list.map(item => {
+              const timestamp = new Date(item.createTime).getTime();
+              return [timestamp, (item.maxBpm + item.minBpm)/2, item.createTime];
+            });
+            this.heartRateData = {
+              values: values
+            };
+           
+          } else {
+            this.heartRateData = {
+              values: []
+            };
+          }
+        }catch(error){
+          console.error("获取心率数据失败:", error);
+          this.bloodSugarData = {
+            values: []
+          };
+        }
+      },
+
+      
+      async queryPressureData() {
+        try{
+          const params = {
+            deviceId: this.deviceId,
+            startTime: this.parseTime(this.startTime,'{y}/{m}/{d} {h}:{i}:{s}'),
+            endTime: this.parseTime(this.endTime,'{y}/{m}/{d} {h}:{i}:{s}'),
+          };
+          const response = await queryPressureList(params);
+          if(response.data && response.data.length > 0){
+            
+
+            const sbp = response.data.map((item) => ({
+              value: [
+                new Date(item.createTime).getTime(),
+                item.sbp,
+              ]
+            }));
+            const dbp = response.data.map((item) => ({
+              value: [
+                  new Date(item.createTime).getTime(),
+                  item.dbp,
+              ]
+            }));
+            const hr = response.data.map((item) => ({
+              value: [
+                  new Date(item.createTime).getTime(),
+                  item.hr,
+              ]
+            }));
+            const createTime = response.data.map((item) => ({
+              value: [
+                  item.createTime
+              ]
+            }));
+            this.bloodPressureData = {
+              sbp : sbp,
+              dbp : dbp,
+              hr : hr,
+              createTime:createTime,
+            };
+          } else {
+            this.bloodPressureData = {
+              sbp : [],
+              dbp : [],
+              hr :  [],
+              createTime:[]
+            };
+          }
+        }catch(error){
+          console.error("获取血压数据失败:", error);
+          this.bloodPressureData = {
+            sbp : [],
+            dbp : [],
+            hr :  [],
+            createTime:[]
+          };
+        }
+      },
+
+      formatCreateDay(dateStr) {
+        const [month, day] = dateStr.split('/').map(Number);
+        const year = this.selectedDate.getFullYear();
+        const date = new Date(year, month - 1, day);
+        return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
+      },
+
+      async getTongueDate(){
+        if(this.userInfo && this.userInfo.userId){
+          const params = {
+            userId: this.userInfo.userId,
+            createDay: this.formatCreateDay(this.selectedDiagnosisDate),
+          };
+          
+          const response = await queryTongueList(params);
+          if(response && response.rows.length > 0){
+            this.tongueData = response.rows[0]
+            this.tongueData.typeJsonObj = JSON.parse(this.tongueData.typeJson || '[]')
+            console.log(this.tongueData)
+          } else {
+            this.tongueData = null;
+          }
+        }
+      },
+      // 获取周报详情数据
+      async getDetailsData() {
+        try {
+          this.getWeekDates(this.selectedDate);
+          // 获取健康周报-步数,卡路里,睡眠等信息
+          await this.getSportData();
+          await this.getHealthArchive();
+          await this.getGlucoseDate();
+          await this.getUaDate();
+          await this.queryPressureData();
+          await this.getHrDate();
+          await this.querySpo2Data();
+          await this.getSleepDate();
+          await this.getTongueDate();
+          this.updateCharts();
+        } catch (error) {
+          console.error("获取周报数据失败:", error);
+        }
+      },
+
+        
+      handleDateChange(date) {
+        this.selectedDate = date || this.parseTime(new Date(), "{y}-{m}-{d}");
+        this.getDetailsData();
+      },
+     
+      initBloodSugarChart() {
+        const chartDom = this.$refs.bloodSugarChart;
+        this.bloodSugarChart = echarts.init(chartDom);
+        const option = {
+          tooltip: {
+            trigger: 'item',
+            formatter: function(params) {
+              if (!params.data || params.data.length < 3) {
+                return '无数据';
+              }
+              const value = params.data[1];
+              const createTime = params.data[2];
+              return `时间: ${createTime}<br/>血糖: ${value} mmol/L`;
+            }
+          },
+          grid: {
+            left: '3%',
+            right: '4%',
+            bottom: '3%',
+            top: '3%',
+            containLabel: true
+          },
+          xAxis: {
+            type: 'time',
+            boundaryGap: false,
+            splitNumber: 7,
+            min:new Date(this.startTime).getTime(),
+            max:new Date(this.endTime).getTime() + 1,
+            axisLabel: {
+              color: "#999999",
+              margin: 20,
+              // fontSize: 14,
+              fontWeight: "bold",
+              formatter: (value) => {
+                return this.parseTime(value, "{m}/{d}");
+              },
+            },
+            axisTick: {
+              show: false,
+            },
+            splitLine: {
+              show: false,
+            },
+          },
+          yAxis: {
+            type: 'value',
+            min: 0,
+            max: 12,
+            interval: 2,
+            axisTick: {
+              show: false,
+            },
+            splitLine: {
+              show: false,
+            },
+          },
+          series: [
+            {
+              name: '血糖',
+              data: this.bloodSugarData.values,
+              type: 'line',
+              smooth: true,
+              symbol: 'circle',
+              symbolSize: 7,
+              itemStyle: {
+                color: '#FF9F43'
+              },
+              lineStyle: {
+                color: '#FF9F43'
+              },
+              areaStyle: {
+                color: {
+                  type: 'linear',
+                  x: 0,
+                  y: 0,
+                  x2: 0,
+                  y2: 1,
+                  colorStops: [
+                    {
+                      offset: 0,
+                      color: 'rgba(255, 159, 67, 0.3)'
+                    },
+                    {
+                      offset: 1,
+                      color: 'rgba(255, 159, 67, 0.1)'
+                    }
+                  ]
+                }
+              }
+            }
+          ]
+        };
+        
+        this.bloodSugarChart.setOption(option);
+        
+        // 响应式调整
+        window.addEventListener('resize', () => {
+          this.bloodSugarChart.resize();
+        });
+      },
+      initBloodPressureChart() {
+        const chartDom = this.$refs.bloodPressureChart;
+        this.bloodPressureChart = echarts.init(chartDom);
+        const option = {
+          tooltip: {
+            trigger: 'axis',
+            formatter: function(params) {
+              if (!params) {
+                return '无数据';
+              }
+              const sbp = params[0].data.value;
+              const dbp = params[1].data.value;
+              const hr = params[2].data.value;
+              const createTime = new Date(sbp[0]).toLocaleString();
+              return `时间: ${createTime}<br/>sbp: ${sbp[1]} mmgh<br/>bdp: ${dbp[1]} mmgh<br/>hr: ${hr[1]} 次/分`;
+            }
+          },
+          legend: {},
+          grid: {
+            left: '3%',
+            right: '4%',
+            bottom: '3%',
+            top: '3%',
+            containLabel: true
+          },
+          xAxis: {
+            type: "time",
+            boundaryGap: false,
+            splitNumber: 7,
+            min:new Date(this.startTime).getTime(),
+            max:new Date(this.endTime).getTime(),
+            axisLabel: {
+              color: "#999999",
+              margin: 20,
+              fontWeight: "bold",
+              formatter: (value) => {
+                return this.parseTime(value, "{m}/{d}");
+              },
+            },
+            axisTick: {
+                show: false,
+            },
+            splitLine: {
+                show: false,
+            },
+          },
+          yAxis: {
+              type: 'value',
+              min: 0,
+              max: 180,
+              interval: 30,
+              axisLabel: {
+                  formatter: '{value}'
+              },
+              axisTick: {
+                show: false,
+              },
+              splitLine: {
+                show: false,
+              },
+          },
+          
+          series: [
+              {
+                  name: 'sbp(mmgh)',
+                  type: "line",
+                  symbol: "none",
+                  data: this.bloodPressureData.sbp
+              },
+              {
+                  name: 'dbp(mmgh)',
+                  type: "line",
+                  symbol: "none",
+                  data: this.bloodPressureData.dbp
+              },
+              {
+                  name: 'hr(次/分)',
+                  type: "line",
+                  symbol: "none",
+                  data: this.bloodPressureData.hr
+              }
+          ]
+          
+        };
+        
+        this.bloodPressureChart.setOption(option);
+        
+        // 响应式调整
+        window.addEventListener('resize', () => {
+          this.bloodPressureChart.resize();
+        });
+      },
+      initUricAcidChart() {
+        const chartDom = this.$refs.uricAcidChart;
+        this.uricAcidChart = echarts.init(chartDom);
+
+        const data = this.uricAcidData.values.map((item, index) => {
+          return {
+            value: [item[0],item[1]],
+            itemStyle: {
+              color: item[3]
+            },
+            time:item[2]
+          };
+        });
+        
+        const option = {
+          tooltip: {
+            trigger: 'item',
+            formatter: function(params) {
+              if (!params.data || params.data.length < 4) {
+                return '无数据';
+              }
+              const value = params.data.value[1];
+              const createTime = params.data.time;
+              return `时间: ${createTime}<br/>尿酸: ${value} μmol/L`;
+            }
+          },
+          grid: {
+            left: '3%',
+            right: '4%',
+            bottom: '3%',
+            top: '3%',
+            containLabel: true
+          },
+          xAxis: {
+            type: 'time',
+            splitNumber: 7,
+            min:new Date(this.startTime).getTime(),
+            max:new Date(this.endTime).getTime() + 1,
+            axisLabel: {
+              color: "#999999",
+              margin: 20,
+              fontWeight: "bold",
+              formatter: (value) => {
+                return this.parseTime(value, "{m}/{d}");
+              },
+            },
+            axisTick: {
+              show: false,
+            },
+            splitLine: {
+              show: false,
+            },
+          },
+          yAxis: {
+            type: 'value',
+            min: 0,
+            max: 600,
+            interval: 100,
+            axisTick: {
+              show: false,
+            },
+            splitLine: {
+              show: false,
+            },
+          },
+          series: [
+            {
+              name: '尿酸',
+              type: 'bar',
+              data: data,
+              // barWidth: '40%'
+              barWidth: 10, // 设置柱子的宽度为 20 像素
+            }
+          ]
+        };
+        
+        this.uricAcidChart.setOption(option);
+        
+        // 响应式调整
+        window.addEventListener('resize', () => {
+          if (this.uricAcidChart) {
+            this.uricAcidChart.resize();
+          }
+        });
+      },
+      initHeartRateChart(){
+        const chartDom = this.$refs.heartRateChart;
+        this.heartRateChart = echarts.init(chartDom);
+        const option = {
+          tooltip: {
+            trigger: 'item',
+            formatter: function(params) {
+              if (!params.data || params.data.length < 3) {
+                return '无数据';
+              }
+              const value = params.data[1];
+              const createTime = params.data[2];
+              return `时间: ${createTime}<br/>心率: ${value} 次/分`;
+            }
+          },
+          grid: {
+            left: '3%',
+            right: '4%',
+            bottom: '3%',
+            top: '3%',
+            containLabel: true
+          },
+          xAxis: {
+            type: 'time',
+            boundaryGap: false,
+            splitNumber: 7,
+            min:new Date(this.startTime).getTime(),
+            max:new Date(this.endTime).getTime() + 1,
+            axisLabel: {
+              color: "#999999",
+              margin: 20,
+              // fontSize: 14,
+              fontWeight: "bold",
+              formatter: (value) => {
+                return this.parseTime(value, "{m}/{d}");
+              },
+            },
+            axisTick: {
+              show: false,
+            },
+            splitLine: {
+              show: false,
+            },
+          },
+          yAxis: {
+            type: 'value',
+            axisTick: {
+              show: false,
+            },
+            splitLine: {
+              show: false,
+            },
+          },
+          series: [
+            {
+              name: '心率',
+              data: this.heartRateData.values,
+              type: 'line',
+              smooth: true,
+              symbol: 'circle',
+              symbolSize: 7,
+              itemStyle: {
+                color: '#FF9F43'
+              },
+              lineStyle: {
+                color: '#FF9F43'
+              },
+              areaStyle: {
+                color: {
+                  type: 'linear',
+                  x: 0,
+                  y: 0,
+                  x2: 0,
+                  y2: 1,
+                  colorStops: [
+                    {
+                      offset: 0,
+                      color: 'rgba(255, 159, 67, 0.3)'
+                    },
+                    {
+                      offset: 1,
+                      color: 'rgba(255, 159, 67, 0.1)'
+                    }
+                  ]
+                }
+              }
+            }
+          ]
+        };
+        
+        this.heartRateChart.setOption(option);
+        
+        // 响应式调整
+        window.addEventListener('resize', () => {
+          this.heartRateChart.resize();
+        });
+      },
+      initBloodOxygenChart(){
+        const chartDom = this.$refs.bloodOxygenChart;
+        this.bloodOxygenChart = echarts.init(chartDom);
+
+        const data = this.bloodOxygenData.values.map((item, index) => {
+          return {
+            value: [item[0],item[1]],
+            itemStyle: {
+              color: item[3]
+            },
+            time:item[2]
+          };
+        });
+        
+        const option = {
+          tooltip: {
+            trigger: 'item',
+            formatter: function(params) {
+              if (!params.data || params.data.length < 4) {
+                return '无数据';
+              }
+              const value = params.data.value[1];
+              const createTime = params.data.time;
+              return `时间: ${createTime}<br/>血氧: ${value} %`;
+            }
+          },
+          grid: {
+            left: '3%',
+            right: '4%',
+            bottom: '3%',
+            top: '3%',
+            containLabel: true
+          },
+          xAxis: {
+            type: 'time',
+            splitNumber: 7,
+            min:new Date(this.startTime).getTime(),
+            max:new Date(this.endTime).getTime() + 1,
+            axisLabel: {
+              color: "#999999",
+              margin: 20,
+              fontWeight: "bold",
+              formatter: (value) => {
+                return this.parseTime(value, "{m}/{d}");
+              },
+            },
+            axisTick: {
+              show: false,
+            },
+            splitLine: {
+              show: false,
+            },
+          },
+          yAxis: {
+            type: 'value',
+            axisTick: {
+              show: false,
+            },
+            splitLine: {
+              show: false,
+            },
+          },
+          series: [
+            {
+              name: '血氧',
+              type: 'bar',
+              data: data,
+              // barWidth: '40%'
+              barWidth: 10, // 设置柱子的宽度为 20 像素
+            }
+          ]
+        };
+        
+        this.bloodOxygenChart.setOption(option);
+        
+        // 响应式调整
+        window.addEventListener('resize', () => {
+          if (this.bloodOxygenChart) {
+            this.bloodOxygenChart.resize();
+          }
+        });
+      },
+      initSleepChart(){
+        const chartDom = this.$refs.sleepChart;
+        this.sleepChart = echarts.init(chartDom);
+        const option = {
+          tooltip: {
+            show: true,
+            formatter: (params) => {
+              const start = params.value[3]
+              const end = params.value[4]
+              return `${start}到${end}<br/>${params.name}`;
+            },
+          },
+          grid: {
+            left: '3%',
+            right: '4%',
+            bottom: '3%',
+            top: '3%',
+            containLabel: true
+          },
+          xAxis: {
+            type: 'time',
+            boundaryGap: false,
+            splitNumber: 7,
+            min:new Date(this.startTime).getTime(),
+            max:new Date(this.endTime).getTime() + 1,
+            axisLabel: {
+              color: "#999999",
+              margin: 20,
+              // fontSize: 14,
+              fontWeight: "bold",
+              formatter: (value) => {
+                return this.parseTime(value, "{m}/{d}");
+              },
+            },
+            axisTick: {
+              show: false,
+            },
+            splitLine: {
+              show: false,
+            },
+          },
+          yAxis: {
+            max: 14,
+            show: false,
+            axisTick: {
+              show: false,
+            },
+            splitLine: {
+              show: false,
+            },
+          },
+          series: [
+          {
+            type: "custom",
+            renderItem: (params, api) => {
+              let yValue = api.value(2);
+              let start = api.coord([api.value(0), yValue]);
+              let size = api.size([api.value(1) - api.value(0), yValue]);
+              let style = api.style();
+              return {
+                type: "rect",
+                shape: {
+                  x: start[0],
+                  y: start[1],
+                  width: size[0],
+                  height: size[1],
+                },
+                style: style,
+              };
+            },
+            data: [],
+          },
+          ]
+        };
+        
+        this.sleepChart.setOption(option);
+        
+        // 响应式调整
+        window.addEventListener('resize', () => {
+          this.sleepChart.resize();
+        });
+      },
+      updateCharts() {
+        if (this.bloodSugarChart) {
+          // 更新血糖图表数据
+          this.bloodSugarChart.setOption({
+            series: [
+              {
+                data: this.bloodSugarData.values
+              }
+            ],
+            xAxis: {
+              min:new Date(this.startTime).getTime(),
+              max:new Date(this.endTime).getTime() + 1,
+            },
+          });
+        }
+
+        if (this.bloodPressureChart) {
+          this.bloodPressureChart.setOption({
+            series: [
+              {
+                name: 'sbp(mmgh)',
+                type: "line",
+                symbol: "none",
+                data: this.bloodPressureData.sbp
+              },
+              {
+                name: 'dbp(mmgh)',
+                type: "line",
+                symbol: "none",
+                data: this.bloodPressureData.dbp
+              },
+              {
+                name: 'hr(次/分)',
+                type: "line",
+                symbol: "none",
+                data: this.bloodPressureData.hr
+              },
+            ],
+            xAxis: {
+              min:new Date(this.startTime).getTime(),
+              max:new Date(this.endTime).getTime()  + 1,
+            },
+          });
+        }
+        
+        if (this.uricAcidChart) {
+          // 更新尿酸图表数据
+          const data = this.uricAcidData.values.map((item, index) => {
+            return {
+              value: [item[0],item[1]],
+              itemStyle: {
+                color: item[3]
+              },
+              time:item[2]
+            };
+          });
+          
+          this.uricAcidChart.setOption({
+            series: [
+              {
+                // type: 'bar',
+                data: data
+              }
+            ],
+            xAxis: {
+              min:new Date(this.startTime).getTime(),
+              max:new Date(this.endTime).getTime() + 1,
+            },
+          });
+        }
+        if (this.heartRateChart) {
+          this.heartRateChart.setOption({
+            series: [
+              {
+                data: this.heartRateData.values
+              }
+            ],
+            xAxis: {
+              min:new Date(this.startTime).getTime(),
+              max:new Date(this.endTime).getTime() + 1,
+            },
+          });
+        }
+        if (this.bloodOxygenChart) {
+          const data = this.bloodOxygenData.values.map((item, index) => {
+            return {
+              value: [item[0],item[1]],
+              itemStyle: {
+                color: item[3]
+              },
+              time:item[2]
+            };
+          });
+          
+          this.bloodOxygenChart.setOption({
+            series: [
+              {
+                // type: 'bar',
+                data: data
+              }
+            ],
+            xAxis: {
+              min:new Date(this.startTime).getTime(),
+              max:new Date(this.endTime).getTime() + 1,
+            },
+          });
+        }
+       
+        if (this.sleepChart) {
+          const chartData = this.sleepData.map((item, index) => {
+            return {
+              name: this.sleepDefaultInfo[item.type] && this.sleepDefaultInfo[item.type].name,
+              value: [
+                new Date(item.start).getTime(),
+                new Date(item.end).getTime(),
+                this.sleepDefaultInfo[item.type] && this.sleepDefaultInfo[item.type].yValue,
+                item.start,
+                item.end
+              ],
+              itemStyle: {
+                color: this.sleepDefaultInfo[item.type] && this.sleepDefaultInfo[item.type].color,
+              },
+            };
+          });
+          this.sleepChart.setOption({
+            series: [
+              {
+                data: chartData
+              }
+            ],
+            xAxis: {
+              min:new Date(this.startTime).getTime(),
+              max:new Date(this.endTime).getTime() + 1,
+            },
+          });
+        }
+      }
+    },
+    beforeDestroy() {
+      // 清除事件监听
+      window.removeEventListener('resize', this.resizeHandler);
+      
+      // 销毁图表实例
+      if (this.bloodSugarChart) {
+        this.bloodSugarChart.dispose();
+        this.bloodSugarChart = null;
+      }
+      if (this.bloodPressureChart) {
+        this.bloodPressureChart.dispose();
+        this.bloodPressureChart = null;
+      }
+      if (this.uricAcidChart) {
+        this.uricAcidChart.dispose();
+        this.uricAcidChart = null;
+      }
+      if (this.heartRateChart) {
+        this.heartRateChart.dispose();
+        this.heartRateChart = null;
+      }
+      if (this.bloodOxygenChart) {
+        this.bloodOxygenChart.dispose();
+        this.bloodOxygenChart = null;
+      }
+      if (this.sleepChart) {
+        this.sleepChart.dispose();
+        this.sleepChart = null;
+      }
+    }
+  };
+  </script>
+  
+  <style scoped lang="scss">
+  
+  /* 确保内容不会因为隐藏滚动条而被截断 */
+  .weekly-report {
+    padding: 20px;
+    background-color: #f5f7fa;
+    min-height: 100%;
+    overflow-y: auto;
+    overflow-x: hidden;
+  }
+  
+  .report-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 20px;
+  }
+  
+  .report-title {
+    display: flex;
+    align-items: center;
+    font-size: 18px;
+    font-weight: bold;
+    color: #2284ff;
+    
+    .title-icon {
+      width: 13px;
+      height: 20px;
+      margin-right: 10px;
+    }
+  }
+  
+  .report-card {
+    margin-bottom: 20px;
+    border-radius: 8px;
+    box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
+    overflow: hidden; /* 确保内容不会溢出卡片 */
+  }
+  
+  .progress-section {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    padding: 20px 0;
+    
+    @media (min-width: 768px) {
+      flex-direction: row;
+      justify-content: space-around;
+      align-items: flex-start;
+    }
+  }
+  
+  .progress-container {
+    position: relative;
+    margin-bottom: 20px;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    
+    @media (min-width: 768px) {
+      margin-bottom: 0;
+    }
+    
+    .progress-info {
+      position: absolute;
+      top: 50%;
+      left: 50%;
+      transform: translate(-50%, -50%);
+      text-align: center;
+      
+      .progress-value {
+        font-size: 24px;
+        font-weight: bold;
+        color: #FF9F43;
+        
+        .progress-unit {
+          font-size: 14px;
+        }
+      }
+      
+      .progress-label {
+        font-size: 12px;
+        color: #606266;
+      }
+    }
+    
+    .health-status {
+      margin-top: 10px;
+      font-size: 16px;
+      font-weight: bold;
+      color: #FF9F43;
+    }
+    .health-description {
+    margin-top: 8px;
+    font-size: 12px;
+    color: #606266;
+    text-align: center;
+    max-width: 240px;
+    line-height: 1.5;
+    padding: 0 10px;
+  }
+}
+  
+  .health-metrics-container {
+    display: flex;
+    flex-direction: column;
+    width: 100%;
+    
+    @media (min-width: 768px) {
+      width: 60%;
+    }
+  }
+  
+  .health-metrics-row {
+    display: flex;
+    justify-content: space-between;
+    margin-bottom: 10px;
+  }
+  
+  .metric-item {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    background-color: #fff;
+    border-radius: 8px;
+    padding: 10px 15px;
+    width: 48%;
+    box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
+    
+    .metric-data {
+      .metric-value {
+        font-size: 16px;
+        font-weight: bold;
+        
+        .metric-unit {
+          font-size: 12px;
+          font-weight: normal;
+        }
+      }
+      
+      .metric-label {
+        font-size: 12px;
+        color: #606266;
+      }
+    }
+    
+    .metric-icon {
+      width: 32px;
+      height: 32px;
+      border-radius: 50%;
+      display: flex;
+      justify-content: center;
+      align-items: center;
+      
+      i {
+        font-size: 16px;
+        color: white;
+      }
+      
+      &.green {
+        background-color: #4CD964;
+      }
+      
+      &.blue {
+        background-color: #2284FF;
+      }
+      
+      &.orange {
+        background-color: #FF9F43;
+      }
+      
+      &.red {
+        background-color: #FF6B6B;
+      }
+      
+      &.purple {
+        background-color: #8A70D6;
+      }
+    }
+  }
+  
+  .section-title {
+    font-size: 16px;
+    font-weight: bold;
+    margin: 20px 0 15px;
+    color: #303133;
+    padding-left: 10px;
+  }
+  
+  /* 健康数据部分 */
+  .health-data-section {
+    padding: 0 10px;
+  }
+  
+  .trend-chart {
+    background-color: #fff;
+    border-radius: 8px;
+    padding: 15px;
+    margin-bottom: 20px;
+    box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
+  }
+  
+  .trend-title {
+    font-size: 14px;
+    font-weight: bold;
+    color: #303133;
+  }
+  
+  .trend-subtitle {
+    font-size: 12px;
+    color: #909399;
+    margin-bottom: 10px;
+  }
+  
+  .line-chart-container,
+  .bar-chart-container {
+    height: 250px;
+    width: 100%;
+  }
+  
+  .trend-advice {
+    font-size: 12px;
+    color: #606266;
+    background-color: #f5f7fa;
+    padding: 10px;
+    border-radius: 4px;
+    margin-top: 10px;
+  }
+  
+  .legend {
+    display: flex;
+    flex-wrap: wrap;
+    margin-top: 10px;
+  }
+  
+  .legend-item {
+    display: flex;
+    align-items: center;
+    margin-right: 15px;
+    font-size: 12px;
+    color: #606266;
+  }
+  
+  .legend-color {
+    display: inline-block;
+    width: 12px;
+    height: 12px;
+    margin-right: 5px;
+    border-radius: 2px;
+  }
+  
+  .health-tabs {
+    margin-top: 20px;
+  }
+  
+  /* 健康档案部分 */
+  .health-archive-section {
+    padding: 0 10px;
+  }
+  
+  .archive-items {
+    display: flex;
+    flex-direction: column;
+    gap: 15px;
+  }
+  
+  .archive-item {
+    display: flex;
+    background-color: #fff;
+    border-radius: 8px;
+    padding: 15px;
+    box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
+  }
+  
+  .archive-icon {
+    width: 36px;
+    height: 36px;
+    border-radius: 50%;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    margin-right: 15px;
+    flex-shrink: 0;
+    
+    i {
+      font-size: 18px;
+      color: white;
+    }
+    
+    &.warning {
+      background-color: #FF6B6B;
+    }
+    
+    &.analysis {
+      background-color: #4CD964;
+    }
+    
+    &.suggestion {
+      background-color: #2284FF;
+    }
+    
+    &.attention {
+      background-color: #FF9F43;
+    }
+  }
+  
+  .archive-content {
+    flex: 1;
+  }
+  
+  .archive-title {
+    font-size: 14px;
+    font-weight: bold;
+    color: #303133;
+    margin-bottom: 5px;
+  }
+  
+  .archive-text {
+    font-size: 12px;
+    color: #606266;
+    line-height: 1.5;
+  }
+  
+  /* AI舌诊部分 */
+  .ai-diagnosis-section {
+    padding: 0 10px 20px;
+  }
+  
+  .date-selector-tabs {
+    display: flex;
+    overflow-x: auto;
+    margin-bottom: 15px;
+  }
+  
+  .date-tab {
+    padding: 8px 15px;
+    background-color: #f5f7fa;
+    border-radius: 4px;
+    margin-right: 8px;
+    font-size: 14px;
+    cursor: pointer;
+    white-space: nowrap;
+    
+    &.active {
+      background-color: #FF9F43;
+      color: white;
+    }
+  }
+  
+  .diagnosis-content {
+    background-color: #fff;
+    border-radius: 8px;
+    padding: 15px;
+    box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
+  }
+  
+  .diagnosis-time {
+    text-align: center;
+    font-size: 14px;
+    color: #909399;
+    margin-bottom: 15px;
+  }
+  
+  .diagnosis-result {
+    text-align: center;
+    margin-bottom: 20px;
+  }
+  
+  .diagnosis-label {
+    font-size: 14px;
+    color: #606266;
+  }
+  
+  .diagnosis-type {
+    font-size: 20px;
+    font-weight: bold;
+    color: #FF6B6B;
+    margin-top: 5px;
+  }
+  
+  .diagnosis-section {
+    margin-bottom: 20px;
+  }
+  
+  .diagnosis-section-title {
+    font-size: 16px;
+    font-weight: bold;
+    color: #FF9F43;
+    margin-bottom: 10px;
+    position: relative;
+    padding-left: 10px;
+    
+    &:before {
+      content: '';
+      position: absolute;
+      left: 0;
+      top: 50%;
+      transform: translateY(-50%);
+      width: 4px;
+      height: 16px;
+      background-color: #FF9F43;
+      border-radius: 2px;
+    }
+  }
+  
+  .diagnosis-feature {
+    margin-bottom: 10px;
+    display: flex;
+    flex-wrap: wrap;
+  }
+  
+  .feature-dot {
+    width: 8px;
+    height: 8px;
+    background-color: #FF9F43;
+    border-radius: 50%;
+    margin-right: 8px;
+    margin-top: 6px;
+  }
+  
+  .feature-title {
+    font-size: 14px;
+    font-weight: bold;
+    color: #303133;
+    margin-right: 10px;
+  }
+  
+  .feature-description {
+    font-size: 12px;
+    color: #606266;
+    flex: 1;
+  }
+  
+  .diagnosis-analysis {
+    margin-bottom: 15px;
+  }
+  
+  .analysis-title {
+    font-size: 14px;
+    font-weight: bold;
+    color: #303133;
+    margin-bottom: 5px;
+  }
+  
+  .analysis-content {
+    font-size: 12px;
+    color: #606266;
+    line-height: 1.5;
+  }
+  
+  .diagnosis-characteristics {
+    margin-bottom: 10px;
+  }
+  
+  .characteristics-title {
+    font-size: 13px;
+    font-weight: bold;
+    color: #303133;
+    margin-bottom: 3px;
+  }
+  
+  .characteristics-content {
+    font-size: 12px;
+    color: #606266;
+    line-height: 1.5;
+  }
+  </style>

+ 13 - 183
src/components/DeviceInfo/SettingDialog/SetInfoDialog.vue

@@ -2,7 +2,6 @@
   <el-dialog :title="title" :visible.sync="open" width="500px" append-to-body :close-on-click-modal="false">
     <el-form ref="form" :model="form" :rules="rules" label-width="80px">
       <el-row>
-        
         <el-col :span="24"
           v-if="type != 'goal' && type != 'languageSet' && type != 'messageSend' && type != 'fallcheckSensitivity' && type != 'measureIntervalHr' && type != 'measureIntervalOther' && type != 'phonebookSync'">
           <el-form-item label="开关设置" prop="flag">
@@ -10,51 +9,6 @@
             </el-switch>
           </el-form-item>
         </el-col>
-        <el-col :span="24" v-if="type == 'medication' && form.flag">
-          <el-form-item label="开始提醒时间" prop="medicationStart" label-width="110px">
-            <el-time-picker
-              value-format="HH:mm:ss"
-              v-model="form.medicationStart"
-              placeholder="请选择开始时间"
-              :picker-options="{
-                selectableRange: '00:00:00 - 23:59:59'
-              }"
-            ></el-time-picker>
-          </el-form-item>
-          <el-form-item label="结束提醒时间" prop="medicationEnd" label-width="110px">
-            <el-time-picker
-              value-format="HH:mm:ss"
-              v-model="form.medicationEnd"
-              placeholder="请选择结束时间"
-              :picker-options="{
-                selectableRange: '00:00:00 - 23:59:59'
-              }"
-            ></el-time-picker>
-          </el-form-item>
-        </el-col>
-        <el-col :span="24" v-if="type == 'study' && form.flag">
-          <el-form-item label="开始提醒时间" prop="studyStart" label-width="110px">
-            <el-time-picker
-              value-format="HH:mm:ss"
-              v-model="form.studyStart"
-              placeholder="请选择开始时间"
-              :picker-options="{
-                selectableRange: '00:00:00 - 23:59:59'
-              }"
-            ></el-time-picker>
-          </el-form-item>
-          <el-form-item label="结束提醒时间" prop="studyEnd" label-width="110px">
-            <el-time-picker
-              value-format="HH:mm:ss"
-              v-model="form.studyEnd"
-              placeholder="请选择结束时间"
-              :picker-options="{
-                selectableRange: '00:00:00 - 23:59:59'
-              }"
-            ></el-time-picker>
-          </el-form-item>
-        </el-col>
-
         <el-col :span="24" v-if="type == 'autolocate' || type == 'datafreq'">
           <el-form-item label="设备省电设置" prop="mode" label-width="105px">
             <el-radio-group v-model="form.mode">
@@ -303,8 +257,6 @@ import {
   fallcheck,
   autolocate,
   datafreq,
-  medication,
-  study,
   lcdgesture,
   hralarm,
   spo2alarm,
@@ -316,8 +268,7 @@ import {
   messageSend,
   fallcheckSensitivity,
   measureIntervalHr,
-  measureIntervalOther,
-  getSet
+  measureIntervalOther
 } from "@/api/watch/deviceInfoSet.js";
 
 export default {
@@ -329,50 +280,6 @@ export default {
         callback();
       }
     };
-    const checkStudyTimes = (rule, value, callback, type) => {
-      if (!value) {
-        return callback(new Error('时间不能为空'));
-      }
-
-      const start = this.form.studyStart;
-      const end = this.form.studyEnd;
-
-      if (type === 'start') {
-        if (end && start && new Date(start) >= new Date(end)) {
-          callback(new Error('开始时间必须小于结束时间'));
-        } else {
-          callback();
-        }
-      } else if (type === 'end') {
-        if (start && new Date(start) > new Date(end)) {
-          callback(new Error('结束时间必须大于开始时间'));
-        } else {
-          callback();
-        }
-      }
-    };
-    const checkMedicationTimes = (rule, value, callback, type) => {
-      if (!value) {
-        return callback(new Error('时间不能为空'));
-      }
-
-      const start = this.form.medicationStart;
-      const end = this.form.medicationEnd;
-
-      if (type === 'start') {
-        if (end && start && new Date(start) >= new Date(end)) {
-          callback(new Error('开始时间必须小于结束时间'));
-        } else {
-          callback();
-        }
-      } else if (type === 'end') {
-        if (start && new Date(start) > new Date(end)) {
-          callback(new Error('结束时间必须大于开始时间'));
-        } else {
-          callback();
-        }
-      }
-    };
     //  检查翻腕亮屏开始时间
     const checkStart = (rule, value, callback) => {
       if (value && this.form.end) {
@@ -568,12 +475,6 @@ export default {
       open: false,
       // 表单参数
       form: {},
-      //
-      param:{
-        flag:null,
-        start:null,
-        end:null
-      },
       // 表单校验
       rules: {
         time: [
@@ -587,22 +488,6 @@ export default {
           { required: true, message: "结束时间不能为空", trigger: ["blur", "change"] },
           { validator: checkEnd, trigger: ["blur", "change"] }
         ],
-        medicationStart: [
-          { required: true, message: "开始时间不能为空", trigger: "blur" },
-          { validator: (rule, value, callback) => checkMedicationTimes(rule, value, callback, 'start'), trigger: "blur" }
-        ],
-        medicationEnd: [
-          { required: true, message: "结束时间不能为空", trigger: "blur" },
-          { validator: (rule, value, callback) => checkMedicationTimes(rule, value, callback, 'end'), trigger: "blur" }
-        ],
-        studyStart: [
-          { required: true, message: "看课提醒开始时间不能为空", trigger: "blur" },
-          { validator: (rule, value, callback) => checkStudyTimes(rule, value, callback, 'start'), trigger: "blur" }
-        ],
-        studyEnd: [
-          { required: true, message: "看课提醒结束时间不能为空", trigger: "blur" },
-          { validator: (rule, value, callback) => checkStudyTimes(rule, value, callback, 'end'), trigger: "blur" }
-        ],
         high: [
           { required: true, message: "正常心率最高值不能为空", trigger: ["blur", "change"] },
           { validator: checkHigh, trigger: ["blur", "change"] }
@@ -732,14 +617,11 @@ export default {
         description: undefined, // 发送设备消息 描述
         fallThreshold: 14000, // 跌倒检测的灵敏度,默认14000
         measureIntervalHr: undefined, // 心率数据测量间隔设置 分钟
-        medicationStart: undefined, // 用药提醒的开始时间
-        medicationEnd: undefined, // 用药提醒的结束时间
-        studyStart: undefined, // 看课提醒的开始时间
-        studyEnd: undefined, // 看课提醒的结束时间
       };
       this.resetForm("form");
       this.btnLoading = false
     },
+    /** 修改按钮操作 */
     handleSet(deviceid, deviceNumber, item) {
       this.reset();
       this.form.deviceid = deviceid
@@ -748,35 +630,10 @@ export default {
       this.title = item.label;
       this.type = item.eventName
     },
-    handleTimeSet(item) {
-      console.log('设置用药提醒发送时间');
-      //查询对应设置
-      this.reset();
-      this.type = item.eventName;
-      this.open = true;
-      getSet({type:this.type}).then(response => {
-        this.param = response.data;
-        console.log("==========================",JSON.stringify(this.param,null,2));
-        this.form.flag = this.param.flag;
-        console.log("==========================开关:"+ this.form.flag);
-        this.title = item.label;
-        if(this.type == 'medication'){
-          this.form.medicationStart = this.param.start;
-          this.form.medicationEnd = this.param.end;
-        } else if(this.type == 'study'){
-          this.form.studyStart = this.param.start;
-          this.form.studyEnd = this.param.end;
-          console.log("==========================开关:"+ JSON.stringify(this.form,null,2));
-        }
-      });
-      
-    },
     /** 提交按钮 */
     submitForm() {
       this.$refs["form"].validate((valid) => {
-        if(this.type=='medication' || this.type=='study'){
-          this.confirmFun()
-        } else if (valid) {
+        if (valid) {
           if (this.form.deviceNumber != undefined) {
             this.confirmFun()
           } else {
@@ -791,31 +648,16 @@ export default {
         fallcheck: `是否确认${flagTxt}设备信息编号为${this.form.deviceNumber}的跌倒检测?`,
         autolocate: `是否确认${flagTxt}设备信息编号为${this.form.deviceNumber}的自动定位?`,
       }
-      if(this.type=='medication' || this.type=='study'){
-        this.$confirm(`是否确认对所有设备进行以上操作?`, "警告", {
-          confirmButtonText: "确定",
-          cancelButtonText: "取消",
-          type: "warning"
-        }).then(() => {
-          this.handleFun()
-        }).then(() => {
-          this.open = false
-          // this.$emit("refreshList")
-        }).catch(() => { });
-        
-      } else {
-        this.$confirm(typeTxt[this.type] || `是否确认对设备id为${this.form.deviceNumber}进行以上操作?`, "警告", {
-          confirmButtonText: "确定",
-          cancelButtonText: "取消",
-          type: "warning"
-        }).then(() => {
-          this.handleFun()
-        }).then(() => {
-          this.open = false
-          // this.$emit("refreshList")
-        }).catch(() => { });
-        }
-      
+      this.$confirm(typeTxt[this.type] || `是否确认对设备id为${this.form.deviceNumber}进行以上操作?`, "警告", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(() => {
+        this.handleFun()
+      }).then(() => {
+        this.open = false
+        // this.$emit("refreshList")
+      }).catch(() => { });
     },
     handleFun() {
       const options = {
@@ -838,18 +680,6 @@ export default {
           time: this.form.time,
           mode: this.form.mode
         }),
-        // 用药提醒设置
-        medication: () => medication({
-          flag: this.form.flag,
-          start: this.form.flag ? this.form.medicationStart : undefined,
-          end: this.form.flag ? this.form.medicationEnd : undefined
-        }),
-        // 看课提醒设置
-        study: () => study({
-          flag: this.form.flag,
-          start: this.form.flag ? this.form.studyStart : undefined,
-          end: this.form.flag ? this.form.studyEnd : undefined
-        }),
         // 翻腕亮屏设置
         lcdgesture: () => lcdgesture({
           deviceNumber: this.form.deviceNumber,

+ 22 - 27
src/components/DeviceInfo/Sleep.vue

@@ -117,8 +117,13 @@
             <span v-if="scope.row.type === 7">快速眼动</span>
           </template>
         </el-table-column>
+        <template #empty>
+            <div>
+                今日暂无数据,请选择其他日期
+            </div>
+        </template>
       </el-table>
-      <pagination v-show="(total > 0) && detailsType" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getDataPage"/>
+      <pagination v-show="(total>0) && detailsType" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getDataPage"/>
     </div> 
   </div>
 </template>
@@ -157,10 +162,10 @@ export default {
       startTime: "2024-08-04 20:58:00",
       endTime: "2024-08-05 00:27:00",
       sleepDefaultInfo: {
-        4: { name: "浅睡", color: "#93c0fa", yValue: 7 },
-        3: { name: "深睡", color: "#427ff7", yValue: 3 },
-        6: { name: "清醒", color: "#fadeaf", yValue: 9 },
-        7: { name: "快速眼动", color: "#9899f5", yValue: 12 },
+        1: { name: "浅睡", color: "#93c0fa", yValue: 7 },
+        2: { name: "深睡", color: "#427ff7", yValue: 3 },
+        3: { name: "清醒", color: "#fadeaf", yValue: 9 },
+        4: { name: "快速眼动", color: "#9899f5", yValue: 12 },
       },
       sleepInfo: {
         score: '',
@@ -390,58 +395,48 @@ export default {
 
             //统计
             this.sleepInfo.score = response.data.score==null?0:response.data.score;
-            
             //浅睡
             if (response.data.lightSleep != null) {
-              // this.sportChartOption.series[0].data[0] = { value: response.data.lightSleep, name: '浅睡', itemStyle: { color: "#93c0fa" } };
+              this.sportChartOption.series[0].data[0] = { value: response.data.lightSleep, name: '浅睡', itemStyle: { color: "#93c0fa" } };
               this.sleepInfo.light_sleep = response.data.lightSleep;
 
             } else{
-              // this.sportChartOption.series[0].data[0] = { value: response.data.lightSleep, name: '浅睡', itemStyle: { color: "#93c0fa" } };
+              this.sportChartOption.series[0].data[0] = { value: response.data.lightSleep, name: '浅睡', itemStyle: { color: "#93c0fa" } };
               this.sleepInfo.light_sleep = 0;
             }
-            this.sportChartOption.series[0].data[0] = { value: response.data.lightSleep, name: '浅睡', itemStyle: { color: "#93c0fa" } };
-
             //深睡
             if (response.data.deepSleep != null) {
-              // this.sportChartOption.series[0].data[1] = { value: response.data.deepSleep, name: '深睡', itemStyle: { color: "#427ff7" } };
+              this.sportChartOption.series[0].data[1] = { value: response.data.deepSleep, name: '深睡', itemStyle: { color: "#427ff7" } };
               this.sleepInfo.deep_sleep = response.data.deepSleep;
 
             } else{
-              // this.sportChartOption.series[0].data[1] = { value: response.data.deepSleep, name: '深睡', itemStyle: { color: "#427ff7" } };
+              this.sportChartOption.series[0].data[1] = { value: response.data.deepSleep, name: '深睡', itemStyle: { color: "#427ff7" } };
               this.sleepInfo.deep_sleep = 0;
             }
-            this.sportChartOption.series[0].data[1] = { value: response.data.deepSleep, name: '深睡', itemStyle: { color: "#427ff7" } };
             //清醒
             if (response.data.weakSleep != null) {
-              // this.sportChartOption.series[0].data[2] = { value: response.data.weakSleep, name: '清醒', itemStyle: { color: "#fadeaf" } };
+              this.sportChartOption.series[0].data[2] = { value: response.data.weakSleep, name: '清醒', itemStyle: { color: "#fadeaf" } };
               this.sleepInfo.weak_sleep = response.data.weakSleep;
 
             } else{
-              // this.sportChartOption.series[0].data[2] = { value: response.data.weakSleep, name: '清醒', itemStyle: { color: "#fadeaf" } };
+              this.sportChartOption.series[0].data[2] = { value: response.data.weakSleep, name: '清醒', itemStyle: { color: "#fadeaf" } };
               this.sleepInfo.weak_sleep = 0;
             }
-            this.sportChartOption.series[0].data[2] = { value: response.data.weakSleep, name: '清醒', itemStyle: { color: "#fadeaf" } };
             //快速眼动
             if (response.data.eyemoveSleep != null) {
-              // this.sportChartOption.series[0].data[3] = { value: response.data.eyemoveSleep, name: '快速眼动', itemStyle: { color: "#9899f5" } };
+              this.sportChartOption.series[0].data[3] = { value: response.data.eyemoveSleep, name: '快速眼动', itemStyle: { color: "#9899f5" } };
               this.sleepInfo.eyemove_sleep = response.data.eyemoveSleep;
 
             } else{
-              // this.sportChartOption.series[0].data[3] = { value: response.data.eyemoveSleep, name: '快速眼动', itemStyle: { color: "#9899f5" } };
+              this.sportChartOption.series[0].data[3] = { value: response.data.eyemoveSleep, name: '快速眼动', itemStyle: { color: "#9899f5" } };
               this.sleepInfo.eyemove_sleep = 0;
             }
-            this.sportChartOption.series[0].data[3] = { value: response.data.eyemoveSleep, name: '快速眼动', itemStyle: { color: "#9899f5" } };
-            // if (this.sportChart) {
-            //   this.sportChart.setOption(this.sportChartOption);
-            // }
+            if (this.sportChart) {
+              this.sportChart.setOption(this.sportChartOption);
+            }
 
           } else {
-            this.defaultChart();
-            console.warn("睡眠数据返回为空", response);
-          }
-          if (this.sportChart) {
-              this.sportChart.setOption(this.sportChartOption);
+            console.warn("睡眠数据返回为空或格式不正确:", response);
           }
         })
         .catch((error) => {

+ 6 - 1
src/components/DeviceInfo/Sports.vue

@@ -29,8 +29,13 @@
       <el-table-column prop="sportTime" label="运动时长(min)" />
       <el-table-column prop="startTime" label="开始时间" />
       <el-table-column prop="type" label="运动类型" />
+      <template #empty>
+          <div>
+              今日暂无数据,请选择其他日期
+          </div>
+      </template>
     </el-table>
-    <pagination v-show="(total > 0) && detailsType" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getDataPage"/>
+    <pagination v-show="(total>0) && detailsType" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getDataPage"/>
     </div>
   </div>
 </template>

+ 9 - 5
src/components/DeviceInfo/Temperature.vue

@@ -24,9 +24,13 @@
                 <el-table-column prop="createTime" label="时间" />
                 <el-table-column prop="estTemp" label="体温(℃)" />
                 <el-table-column prop="shellTemp" label="体表温度(℃)" />
-                
+                <template #empty>
+                    <div>
+                        今日暂无数据,请选择其他日期
+                    </div>
+                </template>
             </el-table>
-            <pagination v-show="(total > 0) && detailsType" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getDataPage"/>
+            <pagination v-show="(total>0) && detailsType" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getDataPage"/>
         </div>
         
     </div>
@@ -70,9 +74,9 @@ export default {
                     boundaryGap: false,
                     type: "time",
                     splitNumber: 6,
-                    axisLine: {
-                        show: false,
-                    },
+                    // axisLine: {
+                    //     show: false,
+                    // },
                     axisTick: {
                         show: false,
                     },

+ 5 - 0
src/components/DeviceInfo/Uricacid.vue

@@ -17,6 +17,11 @@
             </el-table-column>
             <el-table-column prop="val" label="测量结果">
             </el-table-column>
+            <template #empty>
+                <div>
+                    今日暂无数据,请选择其他日期
+                </div>
+            </template>
         </el-table>
 
     </div>

+ 5 - 0
src/components/DeviceInfo/Urineketones.vue

@@ -16,6 +16,11 @@
             </el-table-column>
             <el-table-column prop="val" align="center" label="测量结果">
             </el-table-column>
+            <template #empty>
+                <div>
+                    今日暂无数据,请选择其他日期
+                </div>
+            </template>
         </el-table>
 
     </div>

+ 7 - 0
src/types/crm/customerListRow.ts

@@ -0,0 +1,7 @@
+/**
+ * 客户列表行:getLineCustomerList / getMyCustomerList / getCustomerList 等接口返回的 rows 项。
+ * isReceive===0 与已领取行结构均包含 roboticCallOutCount。
+ */
+export interface CrmCustomerListRow {
+  roboticCallOutCount?: number;
+}

+ 15 - 2
src/utils/common.js

@@ -1,4 +1,4 @@
-/**
+/**
  * 通用js方法封装处理
  * Copyright (c) 2019 fs
  */
@@ -63,13 +63,26 @@ export function parseTime(time, pattern) {
 		if ((typeof time === 'string') && (/^[0-9]+$/.test(time))) {
 			time = parseInt(time)
 		} else if (typeof time === 'string') {
-			time = time.replace(new RegExp(/-/gm), '/');
+			let s = time.trim()
+			// ISO 8601:含 T 时不要全局把 - 换成 /,否则会破坏解析
+			if (s.indexOf('T') !== -1) {
+				// 将末尾 +0800 / -0800 规范为 +08:00,便于各环境 Date 解析(已是 +08:00 的不处理)
+				if (/[+-]\d{4}$/.test(s) && !/[+-]\d{2}:\d{2}$/.test(s)) {
+					s = s.replace(/([+-])(\d{2})(\d{2})$/, '$1$2:$3')
+				}
+				time = s
+			} else {
+				time = s.replace(new RegExp(/-/gm), '/')
+			}
 		}
 		if ((typeof time === 'number') && (time.toString().length === 10)) {
 			time = time * 1000
 		}
 		date = new Date(time)
 	}
+	if (isNaN(date.getTime())) {
+		return null
+	}
 	const formatObj = {
 		y: date.getFullYear(),
 		m: date.getMonth() + 1,

+ 218 - 0
src/views/company/companyMoneyLogsDetail/index.vue

@@ -0,0 +1,218 @@
+<template>
+  <div class="app-container withdraw-detail">
+    <div class="header-bar">
+      <span class="header-title">{{ headerTitle }}</span>
+      <span class="header-money">可提现金额:<em>{{ withdrawableText }}</em> 元</span>
+    </div>
+
+    <el-row class="mb8">
+      <el-col :span="1.5">
+        <el-button
+          type="primary"
+          icon="el-icon-download"
+          size="small"
+          @click="handleExport"
+          v-hasPermi="['company:companyMoneyLogsDetail:export']"
+        >导出</el-button>
+      </el-col>
+    </el-row>
+
+    <el-table
+      v-loading="loading"
+      :data="tableList"
+      border
+      stripe
+      :header-cell-style="{ textAlign: 'center' }"
+      :cell-style="{ textAlign: 'center' }"
+    >
+      <el-table-column label="序号" width="70" align="center">
+        <template slot-scope="scope">
+          {{ (queryParams.pageNum - 1) * queryParams.pageSize + scope.$index + 1 }}
+        </template>
+      </el-table-column>
+      <el-table-column label="公司名称" prop="companyName" min-width="140" show-overflow-tooltip />
+      <el-table-column label="所属销售" prop="salesName" min-width="100">
+        <template slot-scope="scope">
+          {{ scope.row.salesName || '-' }}
+        </template>
+      </el-table-column>
+      <el-table-column label="订单号" prop="orderCode" min-width="160" show-overflow-tooltip>
+        <template slot-scope="scope">
+          {{ scope.row.orderCode || '-' }}
+        </template>
+      </el-table-column>
+      <el-table-column label="交易单号" prop="tradeNo" min-width="180" show-overflow-tooltip>
+        <template slot-scope="scope">
+          {{ scope.row.tradeNo || '-' }}
+        </template>
+      </el-table-column>
+      <el-table-column label="订单状态" align="center" min-width="100">
+        <template slot-scope="scope">
+            <template v-if="scope.row.orderStatusText != null && scope.row.orderStatusText !== ''">
+                <el-tag
+                    v-for="(item, index) in statusOptions"
+                    :key="index"
+                    v-if="scope.row.orderStatusText == item.dictValue"
+                >{{ item.dictLabel }}</el-tag>
+                <el-tag v-if="!statusOptions.some(i => scope.row.orderStatusText == i.dictValue)">{{ scope.row.orderStatusText || scope.row.orderStatus }}</el-tag>
+            </template>
+            <el-tag v-else>{{ scope.row.orderStatusText || '-' }}</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="售后状态" prop="afterSalesStatusText" min-width="100">
+        <template slot-scope="scope">
+          {{ scope.row.afterSalesStatusText || '-' }}
+        </template>
+      </el-table-column>
+      <el-table-column label="订单记录时间" prop="recordTime" min-width="170" align="center">
+        <template slot-scope="scope">
+          {{ formatRecordTime(scope.row.recordTime) }}
+        </template>
+      </el-table-column>
+      <el-table-column label="明细类型" prop="detailTypeText" min-width="130" />
+      <el-table-column label="金额" prop="amount" min-width="120" align="right" header-align="center">
+        <template slot-scope="scope">
+          <span :class="amountClass(scope.row.amount)">{{ formatAmount(scope.row.amount) }}</span>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination
+      v-show="total > 0"
+      :total="total"
+      :page.sync="queryParams.pageNum"
+      :limit.sync="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </div>
+</template>
+
+<script>
+import { getWithdrawDetailSummary, listWithdrawDetail, exportWithdrawDetail } from '@/api/company/withdrawDetail'
+
+export default {
+  name: 'CompanyWithdrawDetail',
+  data() {
+    return {
+      loading: true,
+      companyName: '',
+      withdrawableMoney: null,
+      tableList: [],
+      total: 0,
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10
+      },
+      statusOptions: []
+    }
+  },
+  computed: {
+    headerTitle() {
+      return this.companyName || '—'
+    },
+    withdrawableText() {
+      if (this.withdrawableMoney == null || this.withdrawableMoney === '') return '—'
+      const n = Number(this.withdrawableMoney)
+      if (Number.isNaN(n)) return String(this.withdrawableMoney)
+      return n.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
+    }
+  },
+  created() {
+    this.getDicts('store_order_status').then(res => {
+      this.statusOptions = res.data || []
+    })
+    this.loadSummary()
+    this.getList()
+  },
+  methods: {
+    loadSummary() {
+      getWithdrawDetailSummary().then(res => {
+        this.companyName = res.companyName || ''
+        this.withdrawableMoney = res.withdrawableMoney
+      }).catch(() => {})
+    },
+    getList() {
+      this.loading = true
+      listWithdrawDetail(this.queryParams).then(response => {
+        this.tableList = response.rows || []
+        this.total = response.total || 0
+        this.loading = false
+        this.loadSummary()
+      }).catch(() => {
+        this.loading = false
+      })
+    },
+    formatRecordTime(val) {
+      if (!val) return '-'
+      if (typeof val === 'string') {
+        const norm = val.replace('T', ' ').replace(/-/g, '.')
+        if (norm.length >= 19) {
+          return norm.slice(0, 10) + '-' + norm.slice(11, 19)
+        }
+      }
+      return this.parseTime(val, '{y}.{m}.{d}-{h}:{i}:{s}')
+    },
+    formatAmount(val) {
+      if (val == null || val === '') return '-'
+      const n = Number(val)
+      if (Number.isNaN(n)) return String(val)
+      const abs = Math.abs(n).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
+      if (n > 0) return '+' + abs
+      if (n < 0) return '-' + abs
+      return '0.00'
+    },
+    amountClass(val) {
+      if (val == null || val === '') return ''
+      const n = Number(val)
+      if (n < 0) return 'amt-neg'
+      if (n > 0) return 'amt-pos'
+      return ''
+    },
+    handleExport() {
+      this.$confirm('是否确认导出全部提现明细?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        return exportWithdrawDetail()
+      }).then(response => {
+        this.download(response.msg)
+      }).catch(() => {})
+    }
+  }
+}
+</script>
+
+<style scoped>
+.withdraw-detail .header-bar {
+  display: flex;
+  align-items: center;
+  flex-wrap: wrap;
+  gap: 12px 24px;
+  margin-bottom: 16px;
+  padding: 12px 16px;
+  background: #f5f7fa;
+  border-radius: 4px;
+  border: 1px solid #e4e7ed;
+}
+.withdraw-detail .header-title {
+  font-size: 16px;
+  font-weight: 600;
+  color: #303133;
+}
+.withdraw-detail .header-money {
+  font-size: 14px;
+  color: #606266;
+}
+.withdraw-detail .header-money em {
+  font-style: normal;
+  color: #f56c6c;
+  font-weight: 600;
+}
+.withdraw-detail .amt-pos {
+  color: #67c23a;
+}
+.withdraw-detail .amt-neg {
+  color: #f56c6c;
+}
+</style>

+ 25 - 2
src/views/company/companyVoiceRobotic/index.vue

@@ -474,6 +474,11 @@
             <el-tag v-for="item in levelList" v-if="scope.row.intention == item.dictValue">{{item.dictLabel}}</el-tag>
           </template>
         </el-table-column>
+        <el-table-column label="呼出次数" align="right" prop="roboticCallOutCount" width="100">
+          <template slot-scope="scope">
+            {{ scope.row.roboticCallOutCount == null ? 0 : scope.row.roboticCallOutCount }}
+          </template>
+        </el-table-column>
         <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
           <template slot-scope="scope">
             <el-button
@@ -634,6 +639,15 @@
                       <div class="customer-detail">
                         <div class="customer-name-row">
                           <span class="customer-name">{{ record.customerName }}</span>
+
+                          <el-tag
+                            v-if="record.intention"
+                            size="small"
+                            type="info"
+                            class="status-tag">
+                            意向度:{{ getIntentionLabel(record.intention) }}
+                          </el-tag>
+
                           <el-tag
                             :type="getWorkflowStatusType(record.workflowStatus)"
                             size="small"
@@ -1548,7 +1562,8 @@ export default {
         })
         this.execLogs.list = (res.rows || []).map(item => ({
           ...item,
-          contentList: item.contentList || ''
+          contentList: item.contentList || '',
+          intention: item.intention || ''
         }))
         this.execLogs.total = res.total || 0
         // this.execLogs.stats = res.stats || {
@@ -1679,7 +1694,15 @@ export default {
       } catch (e) {
           return false
       }
-    }
+    },
+    getIntentionLabel(val) {
+      if (val === null || val === undefined || val === '') {
+          return '-'
+      }
+
+      const item = (this.levelList || []).find(e => String(e.dictValue) === String(val))
+      return item ? item.dictLabel : val
+    },
   }
 };
 </script>

+ 18 - 0
src/views/company/companyWorkflow/design.vue

@@ -401,6 +401,21 @@
                   />
                 </el-select>
               </el-form-item>
+              <el-form-item label="转人工业务组">
+                  <el-select
+                      v-model="selectedNode.nodeConfig.busiGroupId"
+                      filterable
+                      placeholder="请选择技能组"
+                      @change="handleConfigChange"
+                  >
+                      <el-option
+                          v-for="item in easyCallBusiGroupList"
+                          :key="item.groupId"
+                          :label="item.bizGroupName"
+                          :value="item.groupId"
+                      />
+                  </el-select>
+              </el-form-item>
               <el-form-item label="外呼线路">
                 <el-select
                   :disabled="editAiDisable"
@@ -837,6 +852,9 @@ export default {
         if (!this.selectedNode.nodeConfig.multiplier) {
           this.$set(this.selectedNode.nodeConfig, 'multiplier', 1)
         }
+          if (this.selectedNode.nodeConfig.busiGroupId === undefined) {
+              this.$set(this.selectedNode.nodeConfig, 'busiGroupId', null)
+          }
         if (this.selectedNode.nodeConfig.autoRecall === undefined) {
           this.$set(this.selectedNode.nodeConfig, 'autoRecall', 0)
         }

+ 0 - 38
src/views/components/QwUserSelectTwo.vue

@@ -5,18 +5,6 @@
       <el-card shadow="never" class="search-card">
         <el-form :model="queryParams" ref="queryForm" :inline="true" label-width="100px" class="search-form">
           <el-row :gutter="16">
-            <el-col :span="6">
-            <el-form-item label="销售公司" prop="companyId">
-              <el-select v-model="queryParams.companyId" placeholder="销售公司" size="small" @change="handleCompanyChange">
-                <el-option
-                  v-for="dict in qwCompanyList"
-                  :key="dict.companyId"
-                  :label="dict.companyName"
-                  :value="dict.companyId"
-                />
-              </el-select>
-            </el-form-item>
-            </el-col>
             <el-col :span="6">
               <el-form-item label="企微用户名" prop="qwUserName">
                 <el-input v-model="queryParams.qwUserName" placeholder="请输入企微用户名" clearable size="small" @keyup.enter.native="handleQuery"/>
@@ -75,7 +63,6 @@
 
 <script>
 import {queryQwList} from "../../api/qw/qwUser";
-import {listCompany} from "../../api/company/company";
 
 export default {
   name: "QwUserSelectTwo",
@@ -96,42 +83,21 @@ export default {
       total: 0,
       // 企微账号表格数据
       qwUserList: [],
-      qwCompanyList: [],
       // 查询参数
       queryParams: {
         pageNum: 1,
         pageSize: 10,
-        companyId: null,
         qwUserName: null,
         qwUserId: null
       }
     };
   },
   methods: {
-    // 获取企业数据
-    getCompanyData() {
-      listCompany().then(response => {
-        this.qwCompanyList = response.rows;
-        // 默认选中第一个企业
-        if (this.qwCompanyList && this.qwCompanyList.length > 0) {
-          this.queryParams.companyId = this.qwCompanyList[0].companyId;
-          // 重新加载页面数据
-          this.handleQuery();
-        }
-      });
-    },
-    // 企业改变时触发查询
-    handleCompanyChange() {
-      this.queryParams.pageNum = 1;
-      this.getList();
-    },
     setRows(rows){
       this.shows = true;
       this.rows = rows || [];
       this.selectedRows = rows || []; // 同步更新
       this.getList();
-      // 获取企业列表并默认选中第一个
-      this.getCompanyData();
     },
     initSelect(){
       let row = this.rows;
@@ -168,10 +134,6 @@ export default {
       this.resetForm("queryForm");
       this.queryParams.pageNum = 1;
       this.queryParams.pageSize = 10;
-      // 重置后选中第一个企业
-      if (this.qwCompanyList && this.qwCompanyList.length > 0) {
-        this.queryParams.companyId = this.qwCompanyList[0].companyId;
-      }
       this.getList();
     },
     // 多选框选中数据

+ 2 - 2
src/views/crm/components/AiTagPanel.vue

@@ -16,7 +16,7 @@
           <div class="info-item">
             <span class="info-label">
               {{ tag.propertyName }}
-              <el-button type="text" icon="el-icon-delete" size="mini" @click="handleDeleteTag(tag)" style="color: #f56c6c; margin-left: 4px;"></el-button>
+              <el-button v-hasPermi="['crm:customerProperty:delete']" type="text" icon="el-icon-delete" size="mini" @click="handleDeleteTag(tag)" style="color: #f56c6c; margin-left: 4px;"></el-button>
             </span>
             <span class="info-value">
               <el-tag
@@ -316,7 +316,7 @@ export default {
             likeRatio: this.form.likeRatio,
             remark: this.form.remark
           };
-          
+
           addOrUpdate(requestData).then(response => {
             if (response.code === 200) {
               this.$message.success("保存成功");

+ 570 - 0
src/views/crm/components/addBusiness.vue

@@ -0,0 +1,570 @@
+<template>
+    <div>
+        <el-form ref="form" :model="form" :rules="rules" label-width="100px">
+            <el-form-item label="线索来源" prop="source">
+                <el-select v-model="form.source" :disabled="form.source !== null" placeholder="请选择线索来源" clearable size="small">
+                <el-option
+                    v-for="item in clueSourceOptions"
+                    :key="item.dictValue"
+                    :label="item.dictLabel"
+                    :value="item.dictValue"
+                />
+                </el-select>
+            </el-form-item>
+            <el-form-item label="客户经理" prop="manager">
+                <el-input v-model="form.manager" placeholder="请输入客户经理" />
+            </el-form-item>
+            <el-form-item label="公司名称" prop="companyName">
+                <el-input v-model="form.companyName"  placeholder="请输入公司名称(只填公司全称,个人用户就填名字)" />
+            </el-form-item>
+            <el-form-item label="联系电话" v-show="form.customerId !== null" prop="mobile">
+                <el-input  v-model="form.mobile" :disabled="form.customerId !== null" placeholder="请输入电话号码"/>
+            </el-form-item>
+            <el-form-item label="其他联系方式">
+                <el-button type="text" @click="showAddContactDialog">添加其他联系电话</el-button>
+            </el-form-item>
+            <el-table border v-loading="loading" :data="mobileList" v-if="mobileList && mobileList.length > 0" >
+                <el-table-column label="姓名" align="center" prop="name" />
+                <el-table-column label="手机" align="center" prop="mobile" />
+                <el-table-column label="备注" align="center" prop="remark" />
+                <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+                <template slot-scope="scope">
+                    <el-button
+                    size="mini"
+                    type="text"
+                    icon="el-icon-delete"
+                    @click="handleDelete(scope.row)"
+                    v-hasPermi="['crm:customerContacts:remove']"
+                    v-if="scope.row.contactsId == null"
+                    >删除</el-button>
+                </template>
+                </el-table-column>
+            </el-table>
+
+            <el-form-item label="接口人角色" prop="contactRole">
+                <el-select v-model="form.contactRole" placeholder="请选择" clearable size="small">
+                    <el-option
+                        v-for="item in contactRoleOptions"
+                        :key="item.dictValue"
+                        :label="item.dictLabel"
+                        :value="item.dictValue"
+                    />
+                </el-select>
+            </el-form-item>
+            <el-form-item label="业务场景" prop="businessScenario">
+                <el-select v-model="form.businessScenario"  placeholder="请选择线索业务场景" clearable size="small">
+                <el-option
+                    v-for="item in businessScenarioOptions"
+                    :key="item.dictValue"
+                    :label="item.dictLabel"
+                    :value="item.dictValue"
+                />
+                </el-select>
+            </el-form-item>
+            <el-form-item label="意向产品" prop="product">
+                <el-input v-model="form.product" placeholder="请输入方案涉及产品" />
+            </el-form-item>
+            <el-form-item label="采购周期" prop="purchaseCycle">
+                <el-input v-model="form.purchaseCycle" placeholder="请输入采购周期(单位:天)" />
+            </el-form-item>
+            <el-form-item label="跟进状态" prop="businessStatus">
+                <el-radio-group v-model="form.businessStatus">
+                    <el-radio label=0>跟进中</el-radio>
+                    <el-radio label=1>已流失</el-radio>
+                    <el-radio label=2>已赢单</el-radio>
+                    <el-radio label=3>待验证</el-radio>
+                </el-radio-group>
+            </el-form-item>
+            <el-form-item label="项目阶段" prop="projectPhase">
+                <!-- 移除 filteredProjectPhaseOptions,直接使用 projectPhaseOptions -->
+                <el-select v-model="form.projectPhase" placeholder="请选择" clearable size="small">
+                    <el-option
+                        v-for="item in projectPhaseOptions"
+                        :key="item.dictValue"
+                        :label="item.dictLabel"
+                        :value="item.dictValue"
+                    />
+                </el-select>
+            </el-form-item>
+            <el-form-item label="意向等级" prop="level">
+                <el-select v-model="form.level" placeholder="请选择" clearable size="small">
+                    <el-option
+                        v-for="item in levelOptions"
+                        :key="item.dictValue"
+                        :label="item.dictLabel"
+                        :value="item.dictValue"
+                    />
+                </el-select>
+            </el-form-item>
+            <el-form-item label="是否绑定BP">
+                <el-radio-group v-model="form.isBp">
+                    <el-radio label="0">未注册</el-radio>
+                    <el-radio label="1">已注册未绑定</el-radio>
+                    <el-radio label="2">不明确</el-radio>
+                    <el-radio label="3">已绑定</el-radio>
+                </el-radio-group>
+            </el-form-item>
+            <el-form-item label="预计成单时间" prop="preTime">
+                <el-date-picker clearable size="small" style="width: 200px"
+                    v-model="form.preTime"
+                    type="date"
+                    value-format="yyyy-MM-dd"
+                    placeholder="选择预计成单时间">
+                </el-date-picker>
+            </el-form-item>
+            <el-form-item label="预计成交金额" prop="preMoney">
+                <el-input v-model="form.preMoney" placeholder="请输入预计成交金额(元)" />
+            </el-form-item>
+            <el-form-item label="首次跟进内容" prop="content" v-if="form.businessId == null">
+                <el-input v-model="form.content" placeholder="请输入首次跟进内容" />
+            </el-form-item>
+            <el-form-item label="下次跟进时间" prop="nextTime">
+                <el-date-picker clearable size="small" style="width: 200px"
+                    v-model="form.nextTime"
+                    type="date"
+                    value-format="yyyy-MM-dd"
+                    placeholder="选择下次跟进时间">
+                </el-date-picker>
+            </el-form-item>
+        </el-form>
+        <div class="footer">
+            <el-button type="primary" @click="submitForm">确 定</el-button>
+        </div>
+        <el-dialog  :title=title :visible.sync="open" width="500px" append-to-body>
+            <el-form ref="contactForm" :model="contactForm" :rules="contactRules" label-width="120px">
+            <el-form-item label="联系人名称" prop="name">
+                <el-input v-model="contactForm.name" placeholder="请输入联系人名称" />
+            </el-form-item>
+            <el-form-item label="手机" prop="mobile">
+                <el-input v-model="contactForm.mobile" placeholder="请输入手机" />
+            </el-form-item>
+            <el-form-item label="备注" prop="remark">
+                <el-input v-model="contactForm.remark" type="textarea" placeholder="请输入内容" />
+            </el-form-item>
+            </el-form>
+            <div slot="footer" class="dialog-footer">
+                <el-button type="primary" @click="submitContactForm">确 定</el-button>
+                <el-button @click="cancelContact">取 消</el-button>
+            </div>
+        </el-dialog>
+    </div>
+</template>
+
+<script>
+    import { addBusiness, updateBusiness } from "@/api/crm/business";
+    import { listCustomerContacts, getCustomerContacts, addCustomerContacts, exportCustomerContacts } from "@/api/crm/customerContacts";
+    export default {
+        name: "business",
+        data() {
+            return {
+                // 弹出层标题
+                title: "",
+                // 是否显示弹出层
+                open: false,
+                // 遮罩层
+                loading: true,
+                mobileList: [],
+                // dialogVisible:false,
+                //联系方式表格
+                contactForm: {
+                    contactsId: null,
+                    customerId: null,
+                    businessId: null,
+                    name: null,
+                    mobile: null,
+                    remark: null
+                },
+                businessScenarioOptions: [],
+                clueSourceOptions: [],
+                contactRoleOptions: [],
+                projectPhaseOptions: [],
+                levelOptions: [],
+                // 添加标志位防止循环触发 - 确保初始化为 false
+                isUpdatingStatus: false,
+                isUpdatingPhase: false,
+                form: {
+                    businessId: null,
+                    customerId: null,
+                    source: null,
+                    manager: null,
+                    companyName: null,
+                    mobile: null,
+                    mobileList: [],
+                    contactRole: null,
+                    businessScenario: null,
+                    product: null,
+                    purchaseCycle: null,
+                    // businessStatus: 0,
+                    remark: null,
+                    projectPhase: null,
+                    level: null,
+                    isBp: '0',
+                    bpAccount: null,
+                    preTime: null,
+                    preMoney: null,
+                    nextTime: null,
+                    createTime: null,
+                    updateTime: null,
+                    createBy: null,
+                    recoveryTime: null,
+                    status: '1'
+                },
+                // 表单校验
+                rules: {
+                    source: [
+                        { required: true, message: "客户来源不能为空", trigger: "change" }
+                    ],
+                    companyName: [
+                        { required: true, message: "客户名称不能为空", trigger: "blur" }
+                    ],
+                    businessScenario: [
+                        { required: true, message: "业务场景不能为空", trigger: "change" }
+                    ],
+                    businessStatus: [
+                        { required: true, message: "请选择跟进状态", trigger: "change" }
+                    ],
+                    projectPhase: [
+                        { required: true, message: "请选择项目阶段", trigger: "change" }
+                    ],
+
+                },
+                //联系方式表单校验
+                contactRules: {
+                    name: [
+                        { required: true, message: "联系人不能为空", trigger: "blur" }
+                    ],
+                    mobile: [
+                        { required: true, message: "手机不能为空", trigger: "blur" },
+                        { min: 8, max: 11, message: '长度在 8 到 11 个字符', trigger: 'blur' }
+                    ]
+                }
+
+            };
+        },
+        created() {
+            this.getDicts("crm_customer_source").then((response) => {
+                this.clueSourceOptions = response.data;
+            });
+            this.getDicts("crm_contact_role").then((response) => {
+                this.contactRoleOptions = response.data;
+            });
+            this.getDicts("crm_project_phase").then((response) => {
+                this.projectPhaseOptions = response.data;
+            });
+            this.getDicts("crm_level").then((response) => {
+                this.levelOptions = response.data;
+            });
+            this.getDicts("business_scenario_type").then((response) => {
+                this.businessScenarioOptions = response.data;
+            });
+        },
+        // 修改监听器:实现正确的自动匹配逻辑
+        watch: {
+            // 监听跟进状态变化,自动匹配项目阶段
+            'form.businessStatus': {
+                handler: function(newVal, oldVal) {
+                    console.log('跟进状态变化:', oldVal, '->', newVal);
+
+                    if (this.isUpdatingStatus) {
+                        console.log('跳过状态更新,防止循环');
+                        return;
+                    }
+
+                    // 设置标志位防止循环触发
+                    this.isUpdatingPhase = true;
+
+                    if (newVal == 2) {
+                        // 已赢单 -> 自动设置为100%转商
+                        const winOption = this.projectPhaseOptions.find(item => {
+                            return item.dictLabel === '100%转商' ||
+                                   item.dictValue === 'A';
+                        });
+                        console.log('找到的赢单选项:', winOption);
+                        if (winOption) {
+                            this.form.projectPhase = winOption.dictValue;
+                            console.log('设置项目阶段为:', winOption.dictValue);
+                        }
+                    } else if (newVal == 1) {
+                        // 已流失 -> 自动设置为丢单
+                        const lostOption = this.projectPhaseOptions.find(item => {
+                            return item.dictLabel === '丢单' ||
+                                   item.dictValue === 'L' ;
+                        });
+                        console.log('找到的丢单选项:', lostOption);
+                        if (lostOption) {
+                            this.form.projectPhase = lostOption.dictValue;
+                            console.log('设置项目阶段为:', lostOption.dictValue);
+                        }
+                    }
+
+                    // 重置标志位
+                    this.$nextTick(() => {
+                        this.isUpdatingPhase = false;
+                        console.log('重置isUpdatingPhase标志位');
+                    });
+                },
+                immediate: false
+            },
+
+            // 监听项目阶段变化,自动匹配跟进状态
+            'form.projectPhase': {
+                handler: function(newVal, oldVal) {
+                    console.log('项目阶段变化:', oldVal, '->', newVal);
+
+                    if (this.isUpdatingPhase) {
+                        console.log('跳过阶段更新,防止循环');
+                        return;
+                    }
+
+                    // 设置标志位防止循环触发
+                    this.isUpdatingStatus = true;
+
+                    // 根据项目阶段自动匹配跟进状态
+                    const selectedPhase = this.projectPhaseOptions.find(item => item.dictValue === newVal);
+                    console.log('选中的阶段选项:', selectedPhase);
+
+                    if (selectedPhase) {
+                        if (selectedPhase.dictLabel == '100%转商' ||
+                            selectedPhase.dictValue == 'A') {
+                            // 100%转商 -> 自动设置为已赢单
+                            this.form.businessStatus = '2';
+                            console.log('设置跟进状态为已赢单');
+                        } else if (selectedPhase.dictLabel == '丢单' ||
+                                  selectedPhase.dictValue == 'L' ) {
+                            // 丢单 -> 自动设置为已流失
+                            this.form.businessStatus = '1';
+                            console.log('设置跟进状态为已流失');
+                        }
+                    }
+
+                    // 重置标志位
+                    this.$nextTick(() => {
+                        this.isUpdatingStatus = false;
+                        console.log('重置isUpdatingStatus标志位');
+                    });
+                },
+                immediate: false
+            }
+        },
+        methods: {
+            addProduct() {
+            },
+            delCustomerContacts(mobile) {
+                const index = this.mobileList.findIndex(phone => phone.id === mobile);
+                if (index !== -1) {
+                    this.mobileList.splice(index, 1); // 删除找到的对象
+                }
+                this.open = false;
+                this.loading = false;
+                console.log("3333333333333", JSON.stringify(this.mobileList));
+            },
+            /** 删除按钮操作 */
+            handleDelete(row) {
+                this.loading = true;
+                const mobile = row.mobile;
+                const index = this.mobileList.findIndex(phone => phone.mobile === mobile);
+                if (index !== -1) {
+                    this.mobileList.splice(index, 1); // 删除找到的对象
+                }
+                this.open = false;
+                this.loading = false;
+
+            },
+
+            /** 提交按钮 */
+            submitContactForm() {
+                this.$refs["contactForm"].validate(valid => {
+                    if (valid) {
+                        //新增
+                        const newMobile = {
+                            mobile: this.contactForm.mobile,
+                            name: this.contactForm.name,
+                            remark: this.contactForm.remark,
+                        };
+                        this.mobileList.push(newMobile);
+                        this.open = false;
+                        this.loading = false;
+                    }
+                });
+            },
+            updateMobileList(mobile, newMobileData) {
+                // 查找要修改的对象
+                const index = this.mobileList.findIndex(phone => phone.mobile === mobile);
+                if (index !== -1) {
+                    // 更新找到的对象
+                    this.$set(this.mobileList, index, { ...this.mobileList[index], ...newMobileData });
+                }
+            },
+            // 取消按钮
+            cancelContact() {
+                this.open = false;
+                this.resetContact();
+            },
+            resetContact() {
+                this.contactForm = {
+                    contactsId: null,
+                    customerId: null,
+                    name: null,
+                    mobile: null,
+                    email: null,
+                    weixin: null,
+                    address: null,
+                    remark: null,
+                    createUserId: null,
+                    createTime: null,
+                    updateTime: null,
+                    isDel: null,
+                    companyId: null
+                };
+            },
+            //添加联系方式
+            showAddContactDialog() {
+                this.open = true; // 显示弹窗
+                this.contactForm.mobile = ''; // 清空表单数据
+            },
+            addContact() {
+                // 添加联系方式逻辑
+                this.form.mobile = this.contactForm.mobile; // 将联系方式添加到form.mobile
+                this.open = false; // 关闭弹窗
+            },
+            updateReset(row){
+                // 重置标志位
+                this.isUpdatingStatus = false;
+                this.isUpdatingPhase = false;
+
+                this.form = row;
+                if(row.source != null){
+                    this.form.source = String(row.source);
+                }
+                if(row.contactRole != null){
+                    this.form.contactRole = String(row.contactRole);
+                }
+                if(row.businessScenario != null){
+                    this.form.businessScenario = String(row.businessScenario);
+                }
+                this.mobileList = this.form.mobileList;
+                if(row.level != null){
+                    this.form.level = String(row.level);
+                }
+                if(row.isBp != null){
+                    this.form.isBp = String(row.isBp);
+                }
+                this.loading = false;
+            },
+            reset(row) {
+                // 重置标志位
+                this.isUpdatingStatus = false;
+                this.isUpdatingPhase = false;
+
+                this.form = row;
+                this.form.source=String(row.source);
+                // 电话
+                this.getContact(row.customerId,this.form.businessId);
+                // // 跟进状态
+                // this.form.businessStatus = 0
+                // 客户名字
+                if(row.customerCompanyName){
+                    this.form.companyName = row.customerCompanyName
+                }
+            },
+            getContact(customerId,businessId){
+                this.loading = true;
+                listCustomerContacts({ customerId: customerId, businessId: businessId }).then(response => {
+                    if (response.code === 200) {
+                        if (response.rows.length > 0)
+                            this.mobileList = response.rows;
+                        this.loading = false;
+                    } else {
+                        this.mobileList = []
+                    }
+                })
+            },
+            businessReset() {
+                // 重置标志位
+                this.isUpdatingStatus = false;
+                this.isUpdatingPhase = false;
+
+                this.form = {
+                    businessId: null,
+                    customerId: null,
+                    source: null,
+                    manager: null,
+                    companyName: null,
+                    mobile: null,
+                    contactRole: null,
+                    businessScenario: null,
+                    product: null,
+                    purchaseCycle: null,
+                    remark: null,
+                    projectPhase: null,
+                    level: null,
+                    isBp: null,
+                    bpAccount: null,
+                    preTime: null,
+                    preMoney: null,
+                    nextTime: null,
+                    createTime: null,
+                    updateTime: null,
+                    createBy: null,
+                    recoveryTime: null,
+                    status: 0,
+                    content:null
+                };
+                this.resetContact();
+            },
+            /** 提交按钮 */
+            submitForm() {
+                this.$refs["form"].validate(valid => {
+                    if (valid) {
+                        this.form.mobileList = this.mobileList;
+                        if (this.form.businessId != null) {
+                            updateBusiness(this.form).then(response => {
+                                if (response.code === 200) {
+                                    this.msgSuccess("修改成功");
+                                    this.businessReset();
+                                    this.$emit('closeBusiness');
+                                }
+                            });
+                        } else {
+                            addBusiness(this.form).then(response => {
+                                if (response.code === 200) {
+                                    this.msgSuccess("提交成功");
+                                    this.businessReset();
+                                    this.$emit('closeBusiness');
+                                }
+                            });
+                        }
+
+                    }
+                });
+            },
+        }
+    };
+</script>
+
+<style lang="scss" scoped>
+.contents {
+    height: 100%;
+    background-color: #fff;
+    padding: 20px;
+
+}
+.footer {
+    display: flex;
+    align-items: center;
+    justify-content: flex-end;
+}
+</style>
+
+<style scoped>
+.avatar-uploader .el-upload {
+    border: 1px dashed #d9d9d9;
+    border-radius: 6px;
+    cursor: pointer;
+    position: relative;
+    overflow: hidden;
+}
+.avatar-uploader .el-upload:hover {
+    border-color: #409EFF;
+}
+</style>

+ 96 - 0
src/views/crm/components/setColumn.vue

@@ -0,0 +1,96 @@
+<template>
+    <div>
+        <el-form ref="form" :model="form" :rules="rules" label-width="80px">
+            <el-form-item label="全部字段" prop="columns">
+                <p></p>
+                <el-checkbox-group v-model="form.columns">
+                    <!-- 循环遍历选项 -->
+                    <el-checkbox
+                        v-for="(item, index) in allColumns"
+                        :key="index"
+                        :label="item.prop"
+                        :disabled="item.prop == 'customerId' || item.prop == 'businessId'"
+                    >
+                        {{ item.label }}
+                    </el-checkbox>
+                </el-checkbox-group>
+            </el-form-item>
+            <div   class="footer">
+                <el-button type="primary" @click="submitForm">确定</el-button>
+                <el-button  @click="cancel">取消</el-button>
+            </div>
+        </el-form>
+    </div>
+</template>
+
+<script>
+    import {updateShow} from "@/api/company/show";
+
+    export default {
+        name: "business",
+        data() {
+            return {
+                form:{
+                    type:'',
+                    columns:[],
+                },
+                allColumns:[],
+                // 表单校验
+                rules: {
+                    type: [
+                        { required: true, message: "type不能为空", trigger: "blur" }
+                    ],
+                    columns: [
+                        { required: true, message: '请至少选择一个选项', trigger: 'change' },
+                        ],
+
+                },
+
+
+            }
+        },
+
+        created() {
+
+        },
+        methods: {
+            cancel(){
+                this.$emit('close');
+            },
+            reset(allColumns,selectedKeys,type,){
+                this.allColumns = allColumns;
+                this.form.columns = selectedKeys;
+                this.form.type = type;
+                console.log("==========",JSON.stringify(this.allColumns))
+                console.log("==========2",JSON.stringify(this.form.columns))
+            },
+            /** 提交按钮 */
+            submitForm() {
+                this.$refs["form"].validate(valid => {
+                if (valid) {
+                    console.log("==========2",JSON.stringify(this.form))
+                    updateShow(this.form).then(response => {
+                        if (response.code === 200) {
+                            this.msgSuccess("提交成功");
+                            this.$emit('close');
+                        }
+                    });
+                }
+                });
+            },
+        }
+    };
+</script>
+<style lang="scss" scoped>
+.contents{
+    height: 100%;
+    background-color: #fff;
+    padding: 20px;
+
+}
+.footer{
+    display: flex;
+    align-items: center;
+    justify-content: flex-end;
+}
+</style>

+ 154 - 46
src/views/crm/customer/customerDetail.vue

@@ -98,14 +98,13 @@
                     </div>
                 </div>
                 <!-- AI 沟通总结 -->
-                <div class="card card-highlight">
+                <div class="card">
                     <div class="card-header">
                         <h3><i class="fas fa-robot"></i> AI 沟通总结</h3>
                     </div>
-                    <div class="summary-content">
-                        <p class="summary-text">{{ getCommunicationSummary() }}</p>
+                    <div class="summary-text compact">
+                        {{ getCommunicationSummary() }}
                     </div>
-                    <div class="update-time-corner">沟通时间:{{ getUpdateTime() }}</div>
                 </div>
                 <!-- 沟通记录 -->
                 <div class="card card-table">
@@ -136,7 +135,7 @@
                                             {{ getIntentionDegreeFromRecord(record) }}
                                         </span>
                                 </td>
-                                <td class="record-cell">{{ record.createTime }}</td>
+                                <td class="record-cell">{{ parseTime(record.createTime, '{y}-{m}-{d} {h}:{i}:{s}') || '-' }}</td>
                                 <td class="record-cell">
                                     <button @click="viewChat(record)" class="btn-view-chat">
                                         <i class="fas fa-comments"></i> 聊天详情
@@ -260,7 +259,7 @@
                     </div>
                     <div class="intention-section">
                         <div class="intention-header">
-                            <span class="intention-label">客户意向度</span>
+                            <span class="intention-label"><i class="fas fa-heart"></i> 客户意向度</span>
                             <el-tooltip placement="top" effect="light">
                                 <i class="el-icon-info intention-info-icon"></i>
                                 <div slot="content" class="intention-tooltip">
@@ -879,29 +878,35 @@ export default {
     background: linear-gradient(90deg, #10b981 0%, #34d399 100%);
 }
 
+/* 徽章改为“标签”风格(类似 AI 标签) */
 .risk-unknown .risk-badge {
-    background: linear-gradient(135deg, #6b7280 0%, #4b5563 100%);
-    box-shadow: 0 2px 8px rgba(107, 114, 128, 0.3);
+    background: #f8fafc;
+    border-color: #e5eaf1;
+    color: #64748b;
 }
 
 .risk-none .risk-badge {
-    background: linear-gradient(135deg, #10b981 0%, #34d399 100%);
-    box-shadow: 0 2px 8px rgba(16, 185, 129, 0.3);
+    background: #f0fdf4;
+    border-color: #bbf7d0;
+    color: #16a34a;
 }
 
 .risk-low .risk-badge {
-    background: linear-gradient(135deg, #3b82f6 0%, #60a5fa 100%);
-    box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3);
+    background: #eff6ff;
+    border-color: #bfdbfe;
+    color: #2563eb;
 }
 
 .risk-medium .risk-badge {
-    background: linear-gradient(135deg, #f59e0b 0%, #fbbf24 100%);
-    box-shadow: 0 2px 8px rgba(245, 158, 11, 0.3);
+    background: #fffbeb;
+    border-color: #fde68a;
+    color: #d97706;
 }
 
 .risk-high .risk-badge {
-    background: linear-gradient(135deg, #ef4444 0%, #f87171 100%);
-    box-shadow: 0 2px 8px rgba(239, 68, 68, 0.3);
+    background: #fef2f2;
+    border-color: #fecaca;
+    color: #dc2626;
 }
 
 .risk-card:hover {
@@ -914,38 +919,26 @@ export default {
     display: inline-flex;
     align-items: center;
     gap: 6px;
-    padding: 6px 14px;
-    border-radius: 8px;
-    font-size: 16px;
-    font-weight: 700;
-    color: white;
-    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
-    transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
-    text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
+    padding: 5px 10px;
+    border-radius: 10px;
+    font-size: 13px;
+    font-weight: 600;
+    border: 1px solid #e5eaf1;
+    box-shadow: none;
+    transition: all 0.2s ease;
 }
 
 .risk-badge::before {
     content: '';
-    width: 6px;
-    height: 6px;
-    background: white;
-    border-radius: 50%;
-    animation: pulse 2s infinite;
-}
-
-@keyframes pulse {
-    0%, 100% {
-        opacity: 1;
-        transform: scale(1);
-    }
-    50% {
-        opacity: 0.5;
-        transform: scale(1.2);
-    }
+    width: 8px;
+    height: 8px;
+    border-radius: 3px;
+    background: currentColor;
+    opacity: 0.35;
 }
 
 .risk-card:hover .risk-badge {
-    transform: scale(1.05);
+    transform: translateY(-1px);
 }
 
 /* 风险分析内容 */
@@ -1864,7 +1857,7 @@ export default {
     color: #64748b;
     display: flex;
     align-items: center;
-    gap: 4px;
+    gap: 6px;
 }
 
 .intention-label::before {
@@ -1875,6 +1868,19 @@ export default {
     border-radius: 2px;
 }
 
+.intention-label i {
+    width: 20px;
+    height: 20px;
+    border-radius: 6px;
+    display: inline-flex;
+    align-items: center;
+    justify-content: center;
+    font-size: 12px;
+    color: #fff;
+    background: linear-gradient(135deg, #f59e0b 0%, #ef4444 100%);
+    box-shadow: 0 2px 6px rgba(245, 158, 11, 0.35);
+}
+
 /* 水印风格意向度显示 - 按等级着色 */
 .intention-watermark {
     font-size: 59px;
@@ -2034,11 +2040,17 @@ export default {
 }
 
 .card-header h3 i {
-    font-size: 20px;
+    width: 24px;
+    height: 24px;
+    border-radius: 7px;
+    display: inline-flex;
+    align-items: center;
+    justify-content: center;
+    font-size: 13px;
+    color: #fff;
     background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-    -webkit-background-clip: text;
-    -webkit-text-fill-color: transparent;
-    background-clip: text;
+    box-shadow: 0 3px 8px rgba(102, 126, 234, 0.35);
+    -webkit-text-fill-color: #fff;
 }
 
 .card-highlight .card-header h3 i {
@@ -2047,5 +2059,101 @@ export default {
     -webkit-text-fill-color: white;
 }
 
+/* 模仿参考图的企业微信分析台风格(覆盖) */
+.customer-container {
+    max-width: 100%;
+    padding: 12px;
+    background: #f4f6fa;
+}
+
+.main-grid-three-columns {
+    grid-template-columns: 300px minmax(640px, 1fr) 320px;
+    gap: 12px;
+    align-items: start;
+}
+
+.card {
+    border-radius: 10px;
+    border: 1px solid #e6ebf2;
+    box-shadow: 0 1px 3px rgba(15, 23, 42, 0.06);
+    margin-bottom: 10px;
+    padding: 12px;
+    background: #fff;
+}
+
+.card::before {
+    display: none;
+}
+
+.card:hover {
+    transform: none;
+    box-shadow: 0 2px 8px rgba(15, 23, 42, 0.08);
+    border-color: #dbe4f0;
+}
+
+.card-header {
+    border-bottom: 1px solid #edf1f7;
+    margin-bottom: 10px;
+    padding-bottom: 8px;
+}
+
+.card-header h3 {
+    font-size: 16px;
+    font-weight: 600;
+    color: #1f2937;
+}
+
+.card-header h3 i {
+    width: 18px;
+    height: 18px;
+    border-radius: 4px;
+    font-size: 10px;
+    background: #4f7cff;
+    box-shadow: none;
+}
+
+.summary-text.compact {
+    font-size: 14px;
+    line-height: 1.75;
+    color: #334155;
+    max-height: 132px;
+}
+
+.risk-card,
+.card-focus {
+    background: #fff;
+    border: 1px solid #e6ebf2;
+}
+
+.risk-analysis {
+    background: #f8fafc;
+    border: 1px solid #edf2f7;
+}
+
+.tag-item,
+.focus-item,
+.profile-item {
+    background: #f8fafc;
+    border-color: #e5eaf1;
+}
+
+.intention-label i {
+    width: 18px;
+    height: 18px;
+    border-radius: 4px;
+    font-size: 10px;
+    background: #4f7cff;
+    box-shadow: none;
+}
+
+.records-table th {
+    background: #f8fafc;
+    color: #475569;
+}
+
+.records-table tbody tr:hover {
+    background: #f8fbff;
+}
+
 
 </style>

+ 114 - 2
src/views/crm/customer/index.vue

@@ -242,6 +242,11 @@
               {{scope.row.mobile}}
             </template>
           </el-table-column>
+          <el-table-column label="AI外呼呼出次数" align="right" prop="roboticCallOutCount" width="130">
+            <template slot-scope="scope">
+              {{ scope.row.roboticCallOutCount == null ? 0 : scope.row.roboticCallOutCount }}
+            </template>
+          </el-table-column>
           <el-table-column  label="客户来源" align="center" prop="source">
             <template slot-scope="scope">
                 <el-tag prop="status" v-for="(item, index) in sourceOptions"    v-if="scope.row.source==item.dictValue">{{item.dictLabel}}</el-tag>
@@ -257,7 +262,45 @@
                 <el-tag prop="status" v-for="(item, index) in typeOptions"    v-if="scope.row.customerType==item.dictValue">{{item.dictLabel}}</el-tag>
             </template>
           </el-table-column>
-          <el-table-column label="标签" align="center" prop="tags" />
+          <!-- <el-table-column label="标签" align="center" prop="tags" /> -->
+          <el-table-column label="标签" align="center" prop="properties" width="200">
+            <template slot-scope="scope">
+              <div v-if="scope.row.properties && scope.row.properties.length" style="text-align: left;">
+                <el-tooltip
+                  v-for="(item, index) in scope.row.properties.slice(0, 3)"
+                  :key="index"
+                  placement="top"
+                  effect="light"
+                >
+                  <div slot="content" style="max-width: 420px; word-break: break-word;">
+                    {{ item.propertyName }}:{{ item.propertyValue }}
+                  </div>
+                  <el-tag style="margin: 0 6px 6px 0; max-width: 100%;">
+                    {{ shortenText(item.propertyName + ':' + item.propertyValue, 16) }}
+                  </el-tag>
+                </el-tooltip>
+                <el-tooltip
+                  v-if="scope.row.properties.length > 3"
+                  placement="top"
+                  effect="light"
+                >
+                  <div slot="content" style="max-width: 360px;">
+                    <div
+                      v-for="(item, idx) in scope.row.properties.slice(3)"
+                      :key="'more-' + idx"
+                      style="margin-bottom: 4px;"
+                    >
+                      {{ item.propertyName }}:{{ item.propertyValue }}
+                    </div>
+                  </div>
+                  <el-tag type="info" style="margin: 0 6px 6px 0;">
+                    +{{ scope.row.properties.length - 3 }}
+                  </el-tag>
+                </el-tooltip>
+              </div>
+              <span v-else>-</span>
+            </template>
+          </el-table-column>
           <el-table-column label="流失风险" align="center" prop="attritionLevel">
             <template slot-scope="scope">
               <el-tag v-if="scope.row.attritionLevel == null" type="info">未分析</el-tag>
@@ -270,7 +313,48 @@
           </el-table-column>
           <el-table-column label="意向度" align="center" prop="intentionDegree">
             <template slot-scope="scope">
-              {{ scope.row.intentionDegree != null ? scope.row.intentionDegree  : '' }}
+              <el-tag v-if="scope.row.intentionDegree !== null && scope.row.intentionDegree !== undefined && scope.row.intentionDegree !== ''">
+                {{ scope.row.intentionDegree }}
+              </el-tag>
+              <span v-else>-</span>
+            </template>
+          </el-table-column>
+          <el-table-column label="关注点" align="center" prop="customerFocusJson" width="220">
+            <template slot-scope="scope">
+              <div v-if="parseFocusPoints(scope.row.customerFocusJson).length" style="text-align: left;">
+                <el-tooltip
+                  v-for="(item, index) in parseFocusPoints(scope.row.customerFocusJson).slice(0, 2)"
+                  :key="index"
+                  placement="top"
+                  effect="light"
+                >
+                  <div slot="content" style="max-width: 420px; word-break: break-word;">
+                    {{ item }}
+                  </div>
+                  <el-tag style="margin: 0 6px 6px 0; max-width: 100%; background: #fff; border: 1px solid #dcdfe6; color: #606266;">
+                    {{ shortenText(item, 14) }}
+                  </el-tag>
+                </el-tooltip>
+                <el-tooltip
+                  v-if="parseFocusPoints(scope.row.customerFocusJson).length > 2"
+                  placement="top"
+                  effect="light"
+                >
+                  <div slot="content" style="max-width: 420px;">
+                    <div
+                      v-for="(item, idx) in parseFocusPoints(scope.row.customerFocusJson).slice(2)"
+                      :key="'focus-more-' + idx"
+                      style="margin-bottom: 4px;"
+                    >
+                      {{ item }}
+                    </div>
+                  </div>
+                  <el-tag style="margin: 0 6px 6px 0; background: #fff; border: 1px solid #dcdfe6; color: #606266;">
+                    +{{ parseFocusPoints(scope.row.customerFocusJson).length - 2 }}
+                  </el-tag>
+                </el-tooltip>
+              </div>
+              <span v-else>-</span>
             </template>
           </el-table-column>
           <el-table-column label="备注" align="center" prop="remark" />
@@ -599,6 +683,34 @@ export default {
     this.getList();
   },
   methods: {
+    shortenText(text, maxLen = 16) {
+      const str = text == null ? '' : String(text);
+      if (str.length <= maxLen) return str;
+      return str.slice(0, maxLen) + '...';
+    },
+    parseFocusPoints(value) {
+      if (!value) return [];
+      if (Array.isArray(value)) {
+        return value.map(item => String(item).trim()).filter(Boolean);
+      }
+      if (typeof value === 'string') {
+        const raw = value.trim();
+        if (!raw) return [];
+        try {
+          const parsed = JSON.parse(raw);
+          if (Array.isArray(parsed)) {
+            return parsed.map(item => String(item).trim()).filter(Boolean);
+          }
+          if (typeof parsed === 'string') {
+            return [parsed.trim()].filter(Boolean);
+          }
+        } catch (e) {
+          // ignore parse error and use raw fallback
+        }
+        return [raw.replace(/^\[|\]$/g, '').replace(/["']/g, '').trim()].filter(Boolean);
+      }
+      return [String(value).trim()].filter(Boolean);
+    },
     openAiDrawer(row) {
       this.aiAnalyze.customerId = row.customerId;
       this.aiAnalyze.customerRow = row;

+ 5 - 0
src/views/crm/customer/line.vue

@@ -172,6 +172,11 @@
             {{scope.row.mobile}}
           </template>
         </el-table-column>
+        <el-table-column label="AI外呼呼出次数" align="right" prop="roboticCallOutCount" width="130">
+          <template slot-scope="scope">
+            {{ scope.row.roboticCallOutCount == null ? 0 : scope.row.roboticCallOutCount }}
+          </template>
+        </el-table-column>
         <el-table-column  label="客户来源" align="center" prop="source">
           <template slot-scope="scope">
               <el-tag prop="status" v-for="(item, index) in sourceOptions"    v-if="scope.row.source==item.dictValue">{{item.dictLabel}}</el-tag>

+ 29 - 2
src/views/crm/customer/my.vue

@@ -170,6 +170,11 @@
           <el-button v-hasPermi="['crm:customer:addVisit']"  type="text" size="mini" @click="handleAddVisit(scope.row)">写跟进</el-button>
         </template>
       </el-table-column>
+      <el-table-column label="AI外呼呼出次数" align="right" prop="roboticCallOutCount" width="130">
+        <template slot-scope="scope">
+          {{ scope.row.roboticCallOutCount == null ? 0 : scope.row.roboticCallOutCount }}
+        </template>
+      </el-table-column>
       <el-table-column  label="客户来源" align="center" prop="source">
         <template slot-scope="scope">
             <el-tag prop="status" v-for="(item, index) in sourceOptions"    v-if="scope.row.source==item.dictValue">{{item.dictLabel}}</el-tag>
@@ -219,6 +224,7 @@
             @click="handleRecover(scope.row)"
             v-hasPermi="['crm:customer:recover']"
           >回收公海</el-button>
+          <el-button size="mini" type="text" @click="handleBusiness(scope.row)" v-hasPermi="['crm:business:add']">新建商机</el-button>
           <!-- <el-button
 
             size="mini"
@@ -269,6 +275,10 @@
     <el-dialog :title="addVisitStatus.title" :visible.sync="addVisitStatus.open" width="600px" append-to-body>
         <add-visit-status ref="visitStatus" @close="closeVisitStatus()"></add-visit-status>
     </el-dialog>
+
+      <el-dialog :title="business.title" :visible.sync="business.open" width="600px" append-to-body>
+          <add-business @closeBusiness="closeBusiness"   ref="addBusiness" />
+      </el-dialog>
   </div>
 </template>
 
@@ -285,9 +295,10 @@ import addTag from '../components/addTag.vue';
 import addRemark from '../components/addRemark.vue';
 import addCustomerType from '../components/addCustomerType.vue';
 import addVisitStatus from '../components/addVisitStatus.vue';
+import addBusiness from '../components/addBusiness.vue';
 export default {
   name: "Customer",
-  components: {addVisitStatus,addCustomerType,addRemark,addTag,assignUser,addOrEditCustomer,editSource, addBatchSms,customerDetails,addVisit,addVisit },
+  components: {addVisitStatus,addCustomerType,addRemark,addTag,assignUser,addOrEditCustomer,editSource, addBatchSms,customerDetails,addVisit,addBusiness },
   data() {
     return {
       addVisitStatus:{
@@ -423,7 +434,10 @@ export default {
           { required: true, message: "客户来源不能为空", trigger: "blur" }
         ],
       },
-      loading:null,
+        business:{
+            open:false,
+            title:"新建商机"
+        },
     };
   },
   created() {
@@ -452,6 +466,19 @@ export default {
     this.getList();
   },
   methods: {
+
+      closeBusiness(){
+          this.business.open=false;
+          this.getList();
+      },
+
+      handleBusiness(row){
+          this.business.open=true;
+          setTimeout(() => {
+              this.$refs.addBusiness.reset(row);
+          }, 200);
+      },
+
     closeVisitStatus(){
         this.addVisitStatus.open=false;
         this.getList();

+ 882 - 0
src/views/crm/customerBusiness/index.vue

@@ -0,0 +1,882 @@
+<template>
+  <div class="app-container"  customer-page-box>
+    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px">
+      <!-- <el-form-item label="客户名称" prop="companyName">
+        <el-input
+          v-model="queryParams.companyName"
+          placeholder="请输入客户名称"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item> -->
+      <el-form-item label="线索来源" prop="source">
+        <el-select v-model="queryParams.source" placeholder="请选择线索来源" clearable size="small">
+          <el-option
+            v-for="item in clueSourceOptions"
+              :key="item.dictValue"
+              :label="item.dictLabel"
+              :value="item.dictValue"
+          />
+        </el-select>
+      </el-form-item>
+      <!-- <el-form-item label="客户经理" prop="manager">
+        <el-input
+          v-model="queryParams.manager"
+          placeholder="请输入客户经理"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item> -->
+      <el-form-item label="企业名称" prop="companyName">
+        <el-input
+          v-model="queryParams.companyName"
+          placeholder="请输入企业名称"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="相关客户" prop="mobile">
+        <el-input
+          v-model="queryParams.mobile"
+          placeholder="请输入相关客户电话号码"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <!-- <el-form-item label="接口人角色" prop="contactRole">
+        <el-select v-model="queryParams.contactRole" placeholder="请选择" clearable size="small">
+          <el-option
+            v-for="item in contactRoleOptions"
+              :key="item.dictValue"
+              :label="item.dictLabel"
+              :value="item.dictValue"
+            />
+         </el-select>
+      </el-form-item> -->
+      <el-form-item label="业务场景" prop="businessScenario">
+        <el-input
+          v-model="queryParams.businessScenario"
+          placeholder="请输入业务场景"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <!-- <el-form-item label="方案涉及的产品" prop="product">
+        <el-input
+          v-model="queryParams.product"
+          placeholder="请输入方案涉及的产品"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item> -->
+      <!-- <el-form-item label="采购周期(单位:天)" prop="purchaseCycle">
+        <el-input
+          v-model="queryParams.purchaseCycle"
+          placeholder="请输入采购周期(单位:天)"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item> -->
+      <el-form-item label="跟进状态" prop="businessStatus">
+        <el-select v-model="queryParams.businessStatus" placeholder="请选择跟进状态" clearable size="small">
+          <el-option
+            v-for="item in businessStatusOptions"
+              :key="item.dictValue"
+              :label="item.dictLabel"
+              :value="item.dictValue"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="项目阶段" prop="projectPhase">
+        <el-select v-model="queryParams.projectPhase" placeholder="请选择" clearable size="small">
+          <el-option
+              v-for="item in projectPhaseOptions"
+              :key="item.dictValue"
+              :label="item.dictLabel"
+              :value="item.dictValue"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="意向等级" prop="level">
+        <el-select v-model="queryParams.level" placeholder="请选择" clearable size="small">
+          <el-option
+              v-for="item in levelOptions"
+              :key="item.dictValue"
+              :label="item.dictLabel"
+              :value="item.dictValue"
+          />
+        </el-select>
+      </el-form-item>
+      <!-- <el-form-item label="是否绑定BP账号" prop="isBp">
+        <el-input
+          v-model="queryParams.isBp"
+          placeholder="请输入是否绑定BP账号"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item> -->
+      <!-- <el-form-item label="BP账户" prop="bpAccount">
+        <el-input
+          v-model="queryParams.bpAccount"
+          placeholder="请输入BP账户"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item> -->
+      <!-- <el-form-item label="预计成单时间" prop="preTime">
+        <el-date-picker clearable size="small" style="width: 200px"
+          v-model="queryParams.preTime"
+          type="date"
+          value-format="yyyy-MM-dd"
+          placeholder="选择预计成单时间">
+        </el-date-picker>
+      </el-form-item> -->
+      <!-- <el-form-item label="预计付费(元)" prop="preMoney">
+        <el-input
+          v-model="queryParams.preMoney"
+          placeholder="请输入预计付费(元)"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item> -->
+      <!-- <el-form-item label="挖掘时间" prop="createTime">
+        <el-date-picker clearable size="small" style="width: 200px"
+          v-model="queryParams.createTime"
+          type="date"
+          value-format="yyyy-MM-dd"
+          placeholder="选择挖掘时间">
+        </el-date-picker>
+      </el-form-item> -->
+      <el-form-item label="挖掘时间" prop="createTimeRange">
+        <el-date-picker
+          style="width:205.4px"
+          clearable size="small"
+          v-model="createTimeRange"
+          type="daterange"
+          value-format="yyyy-MM-dd"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期">
+        </el-date-picker>
+      </el-form-item>
+      <!-- <el-form-item label="下次跟进时间" prop="nextTime">
+        <el-date-picker clearable size="small" style="width: 200px"
+          v-model="queryParams.nextTime"
+          type="date"
+          value-format="yyyy-MM-dd"
+          placeholder="选择下次跟进时间">
+        </el-date-picker>
+      </el-form-item> -->
+      <el-form-item label="下次跟进" prop="nextTimeRange">
+        <el-date-picker
+          style="width:205.4px"
+          clearable size="small"
+          v-model="nextTimeRange"
+          type="daterange"
+          value-format="yyyy-MM-dd"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期">
+        </el-date-picker>
+      </el-form-item>
+
+      <el-form-item>
+        <el-button type="cyan" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
+        <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
+      </el-form-item>
+    </el-form>
+
+    <el-row :gutter="10" class="mb8">
+      <!-- <el-col :span="1.5">
+        <el-button
+          type="primary"
+          icon="el-icon-plus"
+          size="mini"
+          @click="handleAdd"
+          v-hasPermi="['crm:business:add']"
+        >新增</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button
+          type="success"
+          icon="el-icon-edit"
+          size="mini"
+          :disabled="single"
+          @click="handleUpdate"
+          v-hasPermi="['crm:business:edit']"
+        >修改</el-button>
+      </el-col> -->
+      <!-- <el-col :span="1.5">
+        <el-button
+          type="danger"
+          icon="el-icon-delete"
+          size="mini"
+          :disabled="multiple"
+          @click="handleDelete"
+          v-hasPermi="['crm:business:remove']"
+        >删除</el-button>
+      </el-col> -->
+      <!-- <el-col :span="1.5">
+        <el-button
+          type="danger"
+          icon="el-icon-delete"
+          size="mini"
+          :disabled="multiple"
+          @click="handleDelete"
+        >删除</el-button>
+      </el-col> -->
+      <el-col :span="1.5">
+        <el-button
+          type="warning"
+          icon="el-icon-download"
+          size="mini"
+          @click="handleExport"
+          v-hasPermi="['crm:business:export']"
+        >导出</el-button>
+      </el-col>
+      <el-col :span="1.5">
+          <el-button size="mini" icon="el-icon-s-operation" @click="handleSetColumn()" >设置列表</el-button>
+      </el-col>
+
+	  <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+    </el-row>
+
+    <div>
+      <el-table  border v-loading="loading" :data="businessList"
+        @selection-change="handleSelectionChange"
+        @sort-change="handleSortChange"
+        >
+        <el-table-column type="selection" width="55" align="center" />
+        <!-- 动态渲染列 -->
+        <el-table-column
+          v-for="col in visibleColumns"
+          :key="col.prop"
+          :label="col.label"
+          :width="col.width"
+          align="center"
+          :prop="col.prop"
+          :sortable="col.prop === 'nextTime' ? 'custom' : false"
+          >
+
+          <template slot-scope="scope">
+
+            <!-- 如果是客户来源字段 -->
+            <template v-if="col.prop === 'source'">
+              <el-tag v-for="item in clueSourceOptions" :key="item.dictValue" v-show="scope.row.source == item.dictValue">
+                {{ item.dictLabel }}
+              </el-tag>
+            </template>
+
+
+
+            <!-- <template v-else-if="col.prop === 'mobile'">
+              <template>
+                {{scope.row.mobile}}
+                <el-button type="text"    size="mini" @click="callNumber(scope.row.customerId,null)" v-show="scope.row.mobile!=null">拨号</el-button>
+                <el-button v-hasPermi="['crm:customer:addVisit']"  type="text" size="mini" @click="handleAddVisit(scope.row)">写跟进</el-button>
+              </template>
+            </template>
+
+            <template v-else-if="col.prop === 'mobileList'">
+              <div>
+                <el-tag v-for="(phone, index) in scope.row.mobileList" :key="index" v-if="scope.row.mobileList">
+                  {{ phone.mobile }}
+                </el-tag>
+
+              </div>
+            </template>   -->
+
+             <!-- 如果是相关客户电话号码字段 -->
+             <template v-else-if="col.prop === 'mobile'">
+                <template>
+                  {{scope.row.mobile}}
+                  <!-- <el-button type="text"    size="mini" @click="callNumber(scope.row.customerId,null)" v-show="scope.row.mobile!=null">拨号</el-button>
+                  <el-button v-hasPermi="['crm:customer:addVisit']"  type="text" size="mini" @click="handleAddVisit(scope.row)" v-show="scope.row.mobile!=null">写跟进</el-button> -->
+                </template>
+              </template>
+
+              <!-- 如果是电话号码字段 -->
+              <template v-else-if="col.prop === 'mobileList'">
+                <div>
+                    <template>
+                        <div v-for="(phone, index) in scope.row.mobileList" :key="index" v-if="scope.row.mobileList">
+                            <el-tag >
+                                {{ phone.mobile }}
+                            </el-tag>
+                            <!-- <el-button type="text"    size="mini" @click="callNumber(null,phone.contactsId)" v-show="phone.mobile!=null">拨号</el-button> -->
+                        </div>
+                        <!-- <el-button v-hasPermi="['crm:customer:addVisit']"  type="text" size="mini" @click="handleAddVisit(scope.row)" v-show="scope.row.mobile==null">写跟进</el-button> -->
+                    </template>
+                </div>
+              </template>
+
+             <!-- 如果是接口人角色字段 -->
+             <template v-else-if="col.prop === 'contactRole'">
+              <span prop="contactRole" v-for="(item, index) in contactRoleOptions"    v-if="scope.row.contactRole==item.dictValue">{{item.dictLabel}}</span>
+            </template>
+
+            <!-- 如果是业务场景字段 -->
+            <template v-else-if="col.prop === 'businessScenario'">
+              <el-tag v-for="item in businessScenarioOptions" :key="item.dictValue" v-show="scope.row.businessScenario == item.dictValue">
+                {{ item.dictLabel }}
+              </el-tag>
+            </template>
+
+            <!-- 如果是采购周期字段 -->
+            <template v-else-if="col.prop === 'purchaseCycle'">
+              <span>{{scope.row.purchaseCycle||'--'}}天</span>
+            </template>
+
+            <!-- 如果是跟进状态字段 -->
+            <template v-else-if="col.prop === 'businessStatus'">
+              <span prop="businessStatus" v-for="(item, index) in businessStatusOptions"    v-if="scope.row.businessStatus==item.dictValue">{{item.dictLabel}}</span>
+            </template>
+
+            <!-- 如果是项目阶段字段 -->
+            <template v-else-if="col.prop === 'projectPhase'">
+              <span prop="projectPhase" v-for="(item, index) in projectPhaseOptions" v-if="scope.row.projectPhase==item.dictValue">{{item.dictLabel}}</span>
+            </template>
+
+            <!-- 如果是意向等级字段 -->
+            <template v-else-if="col.prop === 'level'">
+              <span prop="level" v-for="(item, index) in levelOptions" v-if="scope.row.level==item.dictValue">{{item.dictLabel}}</span>
+            </template>
+
+            <!-- 如果是是否绑定BP账号字段 -->
+            <template v-else-if="col.prop === 'isBp'">
+              <span prop="isBp" v-for="(item, index) in isBpOptions" v-if="scope.row.isBp==item.dictValue">{{item.dictLabel}}</span>
+            </template>
+
+            <!-- 如果是预计成单时间字段 -->
+            <template v-else-if="col.prop === 'preTime'">
+              <span>{{ parseTime(scope.row.preTime, '{y}-{m}-{d}') }}</span>
+            </template>
+
+            <!-- 如果是预计付费字段 -->
+            <template v-else-if="col.prop === 'preMoney'">
+              <span>{{scope.row.preMoney||'--'}}元</span>
+            </template>
+
+            <!-- 如果是下次跟进时间字段 -->
+            <template v-else-if="col.prop === 'nextTime'">
+              <span>{{ parseTime(scope.row.nextTime, '{y}-{m}-{d}') }}</span>
+            </template>
+
+            <!-- 如果是下次跟进时间 -->
+            <template v-else-if="col.prop === 'nextTime'">
+              <span style="color: red;">
+                {{ scope.row.nextTime }}
+              </span>
+            </template>
+            <!-- 其他普通字段 -->
+            <span v-else>{{ scope.row[col.prop] }}</span>
+          </template>
+
+
+        </el-table-column>
+
+        <!-- 操作列,始终显示 -->
+        <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+          <template slot-scope="scope">
+            <!-- <el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)" v-hasPermi="['crm:business:edit']">修改</el-button>
+            <el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)" v-hasPermi="['crm:business:remove']">删除</el-button> -->
+            <div v-if="scope.row.customerId !== null">
+              <el-button size="mini" type="text" icon="el-icon-s-custom" @click="handleShow(scope.row)" v-hasPermi="['crm:customer:query']">查看相关客户</el-button>
+            </div>
+            <div v-if="scope.row.customerId == null">
+                <el-button size="mini" type="text" icon="el-icon-s-custom" @click="handlevisitShow(scope.row)" v-hasPermi="['crm:customer:query']">查看商机跟进</el-button>
+              </div>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <pagination
+        v-show="total>0"
+        :total="total"
+        :page.sync="queryParams.pageNum"
+        :limit.sync="queryParams.pageSize"
+        @pagination="getList"
+      />
+
+      <el-dialog :title="title" :visible.sync="open" width="600px" append-to-body>
+        <add-business @closeBusiness="closeBusiness"   ref="addBusiness" />
+      </el-dialog>
+      <!-- 显示 allColumns 数据的对话框 -->
+      <el-dialog :visible.sync="columns.open" :title="columns.title">
+        <set-column ref="setColumn" @close="closeSetColumn"/>
+      </el-dialog>
+      <el-drawer size="75%" :title="show.title" :visible.sync="show.open">
+        <customer-details  ref="customerDetails" />
+      </el-drawer>
+      <el-dialog :title="visit.title" :visible.sync="visit.open" width="600px" append-to-body>
+        <add-visit @closeVisit="closeVisit"   ref="addVisit" />
+      </el-dialog>
+      <el-dialog :title="visitView.title" :visible.sync="visitView.open" width="1000px" append-to-body>
+          <customer-visit-list ref="visit"></customer-visit-list>
+      </el-dialog>
+    </div>
+  </div>
+</template>
+
+<script>
+import { listBusiness, getBusiness, delBusiness, updateBusiness, exportBusiness,setBusinessPool } from "@/api/crm/business";
+import addBusiness from '../components/addBusiness.vue';
+import setColumn from '../components/setColumn.vue';
+import addVisit from '../components/addVisit.vue';
+import {getShow} from "@/api/company/show";
+import customerDetails from '../components/customerDetails.vue';
+import customerVisitList from '../components/customerVisitList.vue';
+export default {
+  name: "Business",
+  components: {setColumn,addBusiness,customerDetails,addVisit,customerVisitList},
+  data() {
+    return {
+      visitView:{
+            open:false,
+            title:"跟进记录"
+        },
+      visit:{
+          open:false,
+          title:"写跟进"
+      },
+      show:{
+        title:"客户详情",
+        open:false,
+      },
+      columns:{
+        title:"设置列表",
+        open:false,
+      },
+      columnType:'business',
+      visibleColumns: [],
+       // 可选字段
+       allColumns: [
+       { prop: "businessId", label: "商机ID" },
+        // { prop: "companyName", label: "客户名称" },
+        { prop: "customerId", label: "客户ID" },
+        { prop: "source", label: "线索来源" },
+        { prop: "manager", label: "客户经理" },
+        { prop: "companyName", label: "企业名称" },
+        { prop: "mobile", label: "相关客户电话",width:120  },
+        { prop: "mobileList", label: "电话号码",width:120},
+        { prop: "contactRole", label: "接口人角色" },
+        { prop: "businessScenario", label: "业务场景" },
+        { prop: "product", label: "方案涉及产品" },
+        { prop: "purchaseCycle", label: "采购周期" },
+        { prop: "businessStatus", label: "跟进状态" },
+        { prop: "content", label: "最新跟进内容" },
+        { prop: "nextTime", label: "下次跟进时间",width: 180, sortable: 'custom' },// 启用自定义排序
+        { prop: "projectPhase", label: "项目阶段" },
+        { prop: "level", label: "意向等级" },
+        { prop: "isBp", label: "是否绑定BP账号" },
+        { prop: "bpAccount", label: "BP账户" },
+        { prop: "preTime", label: "预计成单时间", width: 105 },
+        { prop: "preMoney", label: "预计付费" },
+        { prop: "createTime", label: "挖掘时间", width: 105 },
+      ],
+      // 选中的字段(默认显示所有字段)
+      selectedKeys: [
+        "businessId",
+        "customerId",
+        "source",
+        "manager",
+        "companyName",
+        "mobile",
+        "mobileList",
+        "contactRole",
+        "businessScenario",
+        "product",
+        "purchaseCycle",
+        "businessStatus",
+        "content",
+        "nextTime",
+        "projectPhase",
+        "level",
+        "isBp",
+        "bpAccount",
+        "preTime",
+        "preMoney",
+        "createTime"
+      ],
+      createTimeRange:[],
+      nextTimeRange:[],
+      recoveryRange:[],
+      statusOptions:[
+        {
+          dictValue:1,
+          dictLabel:"已分配"
+        },
+        {
+          dictValue:2,
+          dictLabel:"进行中"
+        },
+        {
+          dictValue:3,
+          dictLabel:"回收"
+        }
+      ],
+      clueSourceOptions:[],
+      contactRoleOptions:[],
+      projectPhaseOptions:[],
+      businessScenarioOptions:[],
+      levelOptions:[],
+      businessStatusOptions:[],
+      isBpOptions:[
+        {
+          dictValue:0,
+          dictLabel:"未注册"
+        },
+        {
+          dictValue:1,
+          dictLabel:"已注册未绑定"
+        },
+        {
+          dictValue:2,
+          dictLabel:"不明确"
+        },
+        {
+          dictValue:3,
+          dictLabel:"已绑定"
+        }
+      ],
+
+      // 遮罩层
+      loading: true,
+      // 选中数组
+      ids: [],
+      // 非单个禁用
+      single: true,
+      // 非多个禁用
+      multiple: true,
+      // 显示搜索条件
+      showSearch: true,
+      // 总条数
+      total: 0,
+      // 商机表格数据
+      businessList: [],
+      // 弹出层标题
+      title: "",
+      // 是否显示弹出层
+      open: false,
+      // 查询参数
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        // customerId: null,
+        source: null,
+        manager: null,
+        companyName: null,
+        mobile: null,
+        contactRole: null,
+        businessScenario: null,
+        product: null,
+        purchaseCycle: null,
+        businessStatus: null,
+        projectPhase: null,
+        level: null,
+        isBp: null,
+        bpAccount: null,
+        preTime: null,
+        preMoney: null,
+        nextTime: null,
+        recoveryTime: null,
+        status: null,
+        companyName:null,
+        orderBy:null,//nextTime排序规则
+      },
+      // 表单参数
+      form: {},
+      // 表单校验
+      rules: {
+        customerId: [
+          { required: true, message: "客户id不能为空", trigger: "blur" }
+        ],
+      },
+
+    };
+  },
+  created() {
+    this.getDicts("business_scenario_type").then((response) => {
+      this.businessScenarioOptions = response.data;
+    });
+    this.getDicts("crm_customer_source").then((response) => {
+      this.clueSourceOptions = response.data;
+    });
+    this.getDicts("crm_contact_role").then((response) => {
+      this.contactRoleOptions = response.data;
+    });
+    this.getDicts("crm_project_phase").then((response) => {
+      this.projectPhaseOptions = response.data;
+    });
+    this.getDicts("crm_level").then((response) => {
+      this.levelOptions = response.data;
+    });
+    this.getDicts("crm_business_status").then((response) => {
+      this.businessStatusOptions = response.data;
+    });
+    this.getList();
+  },
+  methods: {
+    getData(customerId){
+        this.queryParams.customerId=customerId;
+        this.queryParams.pageNum=1;
+        this.getList();
+    },
+    handlevisitShow(row){
+        this.visitView.open=true;
+        setTimeout(() => {
+            this.$refs.visit. getBusinessData(row.businessId);
+        }, 200);
+
+      },
+    async handleSortChange({ prop, order }) {
+      try {
+        this.loading = true; // 开始加载
+        const sortOrder = order === 'ascending' ? 'asc' : 'desc'; // 转换排序顺序
+        // const params = {
+        //   sortOrder: sortOrder // 排序顺序
+        // };
+        this.queryParams.orderBy = sortOrder;
+
+        // 调用后端接口获取排序后的数据
+        const response = await this.getList();
+      } catch (error) {
+        console.error('排序失败:', error);
+      } finally {
+        this.loading = false; // 结束加载
+      }
+    },
+    handleAddVisit(row){
+        this.visit.open=true;
+        setTimeout(() => {
+            this.$refs.addVisit.reset(row.customerId);
+        }, 200);
+    },
+    closeVisit(){
+        this.visit.open=false;
+        this.getList();
+    },
+    handleShow(row){
+      this.show.open=true;
+      var that=this;
+      const tab = "visit";
+      setTimeout(() => {
+        that.$refs.customerDetails.getDetails(row.customerId);
+        that.$refs.customerDetails.handleClick(tab);
+
+      }, 200);
+    },
+    handleSetColumn(){
+      this.columns.open=true;
+      var that=this;
+      setTimeout(() => {
+            that.$refs.setColumn.reset(that.allColumns,that.selectedKeys,that.columnType);
+      }, 200);
+    },
+    closeSetColumn(){
+      this.columns.open=false;
+      this.getList();
+    },
+    handleBusiness(row){
+        this.business.open=true;
+        setTimeout(() => {
+            this.$refs.addBusiness.reset(row);
+        }, 200);
+    },
+    closeBusiness(){
+        this.business.open=false;
+        this.getList();
+    },
+    //查询显示字段
+    async getColumn(){
+      const response = await getShow(this.columnType);
+      if (response.code === 200 && response.data) {
+        this.selectedKeys = response.data.columns.split(",");
+        this.$set(this, "visibleColumns", this.allColumns.filter(col => this.selectedKeys.includes(col.prop)));
+
+      } else{
+        this.$set(this, "visibleColumns", this.allColumns);
+      }
+    },
+    /** 查询客户列表 */
+    async getList() {
+      this.loading = true;
+      await this.getColumn(); // 确保 visibleColumns 先获取到
+      if(this.createTimeRange!=null&&this.createTimeRange.length==2){
+        this.queryParams.createTimeRange=this.createTimeRange[0]+"--"+this.createTimeRange[1]
+      }
+      else{
+        this.queryParams.createTimeRange=null;
+      }
+      if(this.nextTimeRange!=null&&this.nextTimeRange.length==2){
+        this.queryParams.nextTimeRange=this.nextTimeRange[0]+"--"+this.nextTimeRange[1]
+      }
+      else{
+        this.queryParams.nextTimeRange=null;
+      }
+      if(this.recoveryRange!=null&&this.recoveryRange.length==2){
+        this.queryParams.recoveryRange=this.recoveryRange[0]+"--"+this.recoveryRange[1]
+      }
+      else{
+        this.queryParams.recoveryRange=null;
+      }
+      const response = await listBusiness(this.addDateRange(this.queryParams, this.dateRange));
+      this.businessList = response.rows
+      this.total = response.total;
+      this.loading = false;
+
+    },
+    // 取消按钮
+    cancel() {
+      this.open = false;
+      this.reset();
+    },
+    // 表单重置
+    reset() {
+      this.form = {
+        businessId: null,
+        customerId: null,
+        source: null,
+        manager: null,
+        companyName: null,
+        mobile: null,
+        contactRole: null,
+        businessScenario: null,
+        product: null,
+        purchaseCycle: null,
+        businessStatus: 0,
+        remark: null,
+        projectPhase: null,
+        level: null,
+        isBp: null,
+        bpAccount: null,
+        preTime: null,
+        preMoney: null,
+        nextTime: null,
+        createTime: null,
+        updateTime: null,
+        createBy: null,
+        recoveryTime: null,
+        status: 0,
+        // createTimeRange:null,
+        // nextTimeRange:null,
+        // recoveryRange:null
+      };
+      this.resetForm("form");
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.queryParams.pageNum = 1;
+      this.getList();
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.resetForm("queryForm");
+      this.handleQuery();
+    },
+    // 多选框选中数据
+    handleSelectionChange(selection) {
+      this.ids = selection.map(item => item.businessId)
+      this.single = selection.length!==1
+      this.multiple = !selection.length
+    },
+    /** 新增按钮操作 */
+    handleAdd() {
+      this.reset();
+      this.open = true;
+      this.title = "添加商机";
+    },
+    /** 修改按钮操作 */
+    handleUpdate(row) {
+      this.reset();
+      const businessId = row.businessId || this.ids
+      getBusiness(businessId).then(response => {
+        this.form = response.data;
+        this.form.source = String(response.data.source);
+        this.form.contactRole = String(response.data.contactRole);
+        this.form.level = String(response.data.level);
+        this.form.businessStatus = String(response.data.businessStatus);
+        console.log("111111111111111111111",JSON.stringify(this.form))
+        this.open = true;
+        this.title = "修改商机";
+        setTimeout(() => {
+            this.$refs.addBusiness.updateReset(row);
+        }, 200);
+      });
+    },
+    // /** 提交按钮 */
+    // submitForm() {
+    //   this.$refs["form"].validate(valid => {
+    //     if (valid) {
+    //       if (this.form.businessId != null) {
+    //         updateBusiness(this.form).then(response => {
+    //           if (response.code === 200) {
+    //             this.msgSuccess("修改成功");
+    //             this.open = false;
+    //             this.getList();
+    //           }
+    //         });
+    //       } else {
+    //         addBusiness(this.form).then(response => {
+    //           if (response.code === 200) {
+    //             this.msgSuccess("新增成功");
+    //             this.open = false;
+    //             this.getList();
+    //           }
+    //         });
+    //       }
+    //     }
+    //   });
+    // },
+    /** 删除按钮操作 */
+    handleDelete(row) {
+      const businessIds = row.businessId || this.ids;
+      this.$confirm('是否确认删除商机编号为"' + businessIds + '"的数据项?', "警告", {
+          confirmButtonText: "确定",
+          cancelButtonText: "取消",
+          type: "warning"
+        }).then(function() {
+          return delBusiness(businessIds);
+        }).then(() => {
+          this.getList();
+          this.msgSuccess("删除成功");
+        }).catch(function() {});
+    },
+    // /** 退回公海按钮操作 */
+    // handleDelete(row) {
+    //   const businessIds = row.businessId || this.ids;
+    //   this.$confirm('是否确认删除商机编号为"' + businessIds + '"的数据项?', "警告", {
+    //       confirmButtonText: "确定",
+    //       cancelButtonText: "取消",
+    //       type: "warning"
+    //     }).then(function() {
+    //       return setBusinessPool(businessIds);
+    //     }).then(() => {
+    //       this.getList();
+    //       this.msgSuccess("删除成功");
+    //     }).catch(function() {});
+    // },
+    /** 导出按钮操作 */
+    handleExport() {
+      const queryParams = this.queryParams;
+      this.$confirm('是否确认导出所有商机数据项?', "警告", {
+          confirmButtonText: "确定",
+          cancelButtonText: "取消",
+          type: "warning"
+        }).then(function() {
+          return exportBusiness(queryParams);
+        }).then(response => {
+          this.download(response.msg);
+        }).catch(function() {});
+    }
+  }
+};
+</script>

+ 930 - 0
src/views/crm/customerBusiness/my.vue

@@ -0,0 +1,930 @@
+<template>
+    <div class="app-container"  customer-page-box>
+      <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px">
+        <!-- <el-form-item label="客户名称" prop="companyName">
+          <el-input
+            v-model="queryParams.companyName"
+            placeholder="请输入客户名称"
+            clearable
+            size="small"
+            @keyup.enter.native="handleQuery"
+          />
+        </el-form-item> -->
+        <el-form-item label="线索来源" prop="source">
+          <el-select v-model="queryParams.source" placeholder="请选择线索来源" clearable size="small">
+            <el-option
+              v-for="item in clueSourceOptions"
+                :key="item.dictValue"
+                :label="item.dictLabel"
+                :value="item.dictValue"
+            />
+          </el-select>
+        </el-form-item>
+        <!-- <el-form-item label="客户经理" prop="manager">
+          <el-input
+            v-model="queryParams.manager"
+            placeholder="请输入客户经理"
+            clearable
+            size="small"
+            @keyup.enter.native="handleQuery"
+          />
+        </el-form-item> -->
+        <el-form-item label="企业名称" prop="companyName">
+          <el-input
+            v-model="queryParams.companyName"
+            placeholder="请输入企业名称"
+            clearable
+            size="small"
+            @keyup.enter.native="handleQuery"
+          />
+        </el-form-item>
+        <el-form-item label="相关客户" prop="mobile">
+          <el-input
+            v-model="queryParams.mobile"
+            placeholder="请输入相关客户电话号码"
+            clearable
+            size="small"
+            @keyup.enter.native="handleQuery"
+          />
+        </el-form-item>
+        <!-- <el-form-item label="接口人角色" prop="contactRole">
+          <el-select v-model="queryParams.contactRole" placeholder="请选择" clearable size="small">
+            <el-option
+              v-for="item in contactRoleOptions"
+                :key="item.dictValue"
+                :label="item.dictLabel"
+                :value="item.dictValue"
+              />
+           </el-select>
+        </el-form-item> -->
+        <el-form-item label="业务场景" prop="businessScenario">
+          <el-input
+            v-model="queryParams.businessScenario"
+            placeholder="请输入业务场景"
+            clearable
+            size="small"
+            @keyup.enter.native="handleQuery"
+          />
+        </el-form-item>
+        <!-- <el-form-item label="方案涉及的产品" prop="product">
+          <el-input
+            v-model="queryParams.product"
+            placeholder="请输入方案涉及的产品"
+            clearable
+            size="small"
+            @keyup.enter.native="handleQuery"
+          />
+        </el-form-item> -->
+        <!-- <el-form-item label="采购周期(单位:天)" prop="purchaseCycle">
+          <el-input
+            v-model="queryParams.purchaseCycle"
+            placeholder="请输入采购周期(单位:天)"
+            clearable
+            size="small"
+            @keyup.enter.native="handleQuery"
+          />
+        </el-form-item> -->
+        <el-form-item label="跟进状态" prop="businessStatus">
+          <el-select v-model="queryParams.businessStatus" placeholder="请选择跟进状态" clearable size="small">
+            <el-option
+              v-for="item in businessStatusOptions"
+                :key="item.dictValue"
+                :label="item.dictLabel"
+                :value="item.dictValue"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="项目阶段" prop="projectPhase">
+          <el-select v-model="queryParams.projectPhase" placeholder="请选择" clearable size="small">
+            <el-option
+                v-for="item in projectPhaseOptions"
+                :key="item.dictValue"
+                :label="item.dictLabel"
+                :value="item.dictValue"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="意向等级" prop="level">
+          <el-select v-model="queryParams.level" placeholder="请选择" clearable size="small">
+            <el-option
+                v-for="item in levelOptions"
+                :key="item.dictValue"
+                :label="item.dictLabel"
+                :value="item.dictValue"
+            />
+          </el-select>
+        </el-form-item>
+        <!-- <el-form-item label="是否绑定BP账号" prop="isBp">
+          <el-input
+            v-model="queryParams.isBp"
+            placeholder="请输入是否绑定BP账号"
+            clearable
+            size="small"
+            @keyup.enter.native="handleQuery"
+          />
+        </el-form-item> -->
+        <!-- <el-form-item label="BP账户" prop="bpAccount">
+          <el-input
+            v-model="queryParams.bpAccount"
+            placeholder="请输入BP账户"
+            clearable
+            size="small"
+            @keyup.enter.native="handleQuery"
+          />
+        </el-form-item> -->
+        <!-- <el-form-item label="预计成单时间" prop="preTime">
+          <el-date-picker clearable size="small" style="width: 200px"
+            v-model="queryParams.preTime"
+            type="date"
+            value-format="yyyy-MM-dd"
+            placeholder="选择预计成单时间">
+          </el-date-picker>
+        </el-form-item> -->
+        <!-- <el-form-item label="预计付费(元)" prop="preMoney">
+          <el-input
+            v-model="queryParams.preMoney"
+            placeholder="请输入预计付费(元)"
+            clearable
+            size="small"
+            @keyup.enter.native="handleQuery"
+          />
+        </el-form-item> -->
+        <!-- <el-form-item label="挖掘时间" prop="createTime">
+          <el-date-picker clearable size="small" style="width: 200px"
+            v-model="queryParams.createTime"
+            type="date"
+            value-format="yyyy-MM-dd"
+            placeholder="选择挖掘时间">
+          </el-date-picker>
+        </el-form-item> -->
+        <el-form-item label="挖掘时间" prop="createTimeRange">
+          <el-date-picker
+            style="width:205.4px"
+            clearable size="small"
+            v-model="createTimeRange"
+            type="daterange"
+            value-format="yyyy-MM-dd"
+            start-placeholder="开始日期"
+            end-placeholder="结束日期">
+          </el-date-picker>
+        </el-form-item>
+        <!-- <el-form-item label="下次跟进时间" prop="nextTime">
+          <el-date-picker clearable size="small" style="width: 200px"
+            v-model="queryParams.nextTime"
+            type="date"
+            value-format="yyyy-MM-dd"
+            placeholder="选择下次跟进时间">
+          </el-date-picker>
+        </el-form-item> -->
+        <el-form-item label="下次跟进" prop="nextTimeRange">
+          <el-date-picker
+            style="width:205.4px"
+            clearable size="small"
+            v-model="nextTimeRange"
+            type="daterange"
+            value-format="yyyy-MM-dd"
+            start-placeholder="开始日期"
+            end-placeholder="结束日期">
+          </el-date-picker>
+        </el-form-item>
+
+        <el-form-item>
+          <el-button type="cyan" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
+          <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
+        </el-form-item>
+      </el-form>
+
+      <el-row :gutter="10" class="mb8">
+<!--        <el-col :span="1.5">-->
+<!--          <el-button-->
+<!--            type="primary"-->
+<!--            icon="el-icon-plus"-->
+<!--            size="mini"-->
+<!--            @click="handleAdd"-->
+<!--            v-hasPermi="['crm:business:add']"-->
+<!--          >新增</el-button>-->
+<!--        </el-col>-->
+        <el-col :span="1.5">
+          <el-button
+            type="success"
+            icon="el-icon-edit"
+            size="mini"
+            :disabled="single"
+            @click="handleUpdate"
+            v-hasPermi="['crm:business:edit']"
+          >修改</el-button>
+        </el-col>
+        <!-- <el-col :span="1.5">
+          <el-button
+            type="danger"
+            icon="el-icon-delete"
+            size="mini"
+            :disabled="multiple"
+            @click="handleDelete"
+            v-hasPermi="['crm:business:remove']"
+          >删除</el-button>
+        </el-col> -->
+        <el-col :span="1.5">
+          <el-button
+            type="danger"
+            icon="el-icon-delete"
+            size="mini"
+            :disabled="multiple"
+            @click="handleDelete"
+          >删除</el-button>
+        </el-col>
+        <el-col :span="1.5">
+          <el-button
+            type="warning"
+            icon="el-icon-download"
+            size="mini"
+            @click="handleExport"
+            v-hasPermi="['crm:business:export']"
+          >导出</el-button>
+        </el-col>
+        <el-col :span="1.5">
+          <el-button
+            type="warning"
+            icon="el-icon-upload2"
+            size="mini"
+            @click="handleImport"
+            v-hasPermi="['crm:business:import']"
+          >导入</el-button>
+        </el-col>
+
+        <el-col :span="1.5">
+            <el-button size="mini" icon="el-icon-s-operation" @click="handleSetColumn()" >设置列表</el-button>
+        </el-col>
+
+        <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+      </el-row>
+
+      <div>
+        <el-table  border v-loading="loading" :data="businessList"
+          @selection-change="handleSelectionChange"
+          @sort-change="handleSortChange"
+          >
+          <el-table-column type="selection" width="55" align="center" />
+          <!-- 动态渲染列 -->
+          <el-table-column
+            v-for="col in visibleColumns"
+            :key="col.prop"
+            :label="col.label"
+            :width="col.width"
+            align="center"
+            :prop="col.prop"
+            :sortable="col.prop === 'nextTime' ? 'custom' : false"
+            >
+
+            <template slot-scope="scope">
+
+              <!-- 如果是客户来源字段 -->
+              <template v-if="col.prop === 'source'">
+                <el-tag v-for="item in clueSourceOptions" :key="item.dictValue" v-show="scope.row.source == item.dictValue">
+                  {{ item.dictLabel }}
+                </el-tag>
+              </template>
+
+
+
+              <!-- 如果是相关客户电话号码字段 -->
+              <template v-else-if="col.prop === 'mobile'">
+                <template>
+                  {{scope.row.mobile}}
+                  <el-button type="text"    size="mini" @click="callNumber(scope.row.customerId,null)" v-show="scope.row.mobile!=null">拨号</el-button>
+                  <!-- <el-button v-hasPermi="['crm:customer:addVisit']"  type="text" size="mini" @click="handleAddVisit(scope.row)" v-show="scope.row.mobile!=null">写跟进</el-button> -->
+                  <el-button v-hasPermi="['crm:customer:addVisit']"  type="text" size="mini" @click="handleAddVisitByBusiness(scope.row)" v-show="scope.row.mobile!=null">写跟进</el-button>
+                </template>
+              </template>
+
+              <!-- 如果是电话号码字段 -->
+              <template v-else-if="col.prop === 'mobileList'">
+                <div>
+                    <template>
+                        <div v-for="(phone, index) in scope.row.mobileList" :key="index" v-if="scope.row.mobileList">
+                            <el-tag >
+                                {{ phone.mobile }}
+                            </el-tag>
+                            <el-button type="text"    size="mini" @click="callNumber(null,phone.contactsId)" v-show="phone.mobile!=null">拨号</el-button>
+                        </div>
+                        <el-button v-hasPermi="['crm:customer:addVisit']"  type="text" size="mini" @click="handleAddVisitByBusiness(scope.row)" v-show="scope.row.mobile==null">写跟进</el-button>
+                    </template>
+                </div>
+              </template>
+
+               <!-- 如果是接口人角色字段 -->
+               <template v-else-if="col.prop === 'contactRole'">
+                <span prop="contactRole" v-for="(item, index) in contactRoleOptions"    v-if="scope.row.contactRole==item.dictValue">{{item.dictLabel}}</span>
+              </template>
+
+              <!-- 如果是业务场景字段 -->
+              <template v-else-if="col.prop === 'businessScenario'">
+                <el-tag v-for="item in businessScenarioOptions" :key="item.dictValue" v-show="scope.row.businessScenario == item.dictValue">
+                  {{ item.dictLabel }}
+                </el-tag>
+              </template>
+
+              <!-- 如果是采购周期字段 -->
+              <template v-else-if="col.prop === 'purchaseCycle'">
+                <span>{{scope.row.purchaseCycle||'--'}}天</span>
+              </template>
+
+              <!-- 如果是跟进状态字段 -->
+              <template v-else-if="col.prop === 'businessStatus'">
+                <span prop="businessStatus" v-for="(item, index) in businessStatusOptions"    v-if="scope.row.businessStatus==item.dictValue">{{item.dictLabel}}</span>
+              </template>
+
+              <!-- 如果是项目阶段字段 -->
+              <template v-else-if="col.prop === 'projectPhase'">
+                <span prop="projectPhase" v-for="(item, index) in projectPhaseOptions" v-if="scope.row.projectPhase==item.dictValue">{{item.dictLabel}}</span>
+              </template>
+
+              <!-- 如果是意向等级字段 -->
+              <template v-else-if="col.prop === 'level'">
+                <span prop="level" v-for="(item, index) in levelOptions" v-if="scope.row.level==item.dictValue">{{item.dictLabel}}</span>
+              </template>
+
+              <!-- 如果是是否绑定BP账号字段 -->
+              <template v-else-if="col.prop === 'isBp'">
+                <span prop="isBp" v-for="(item, index) in isBpOptions" v-if="scope.row.isBp==item.dictValue">{{item.dictLabel}}</span>
+              </template>
+
+              <!-- 如果是预计成单时间字段 -->
+              <template v-else-if="col.prop === 'preTime'">
+                <span>{{ parseTime(scope.row.preTime, '{y}-{m}-{d}') }}</span>
+              </template>
+
+              <!-- 如果是预计付费字段 -->
+              <template v-else-if="col.prop === 'preMoney'">
+                <span>{{scope.row.preMoney||'--'}}元</span>
+              </template>
+
+              <!-- 如果是下次跟进时间字段 -->
+              <template v-else-if="col.prop === 'nextTime'">
+                <span>{{ parseTime(scope.row.nextTime, '{y}-{m}-{d}') }}</span>
+              </template>
+
+              <!-- 如果是下次跟进时间 -->
+              <template v-else-if="col.prop === 'nextTime'">
+                <span style="color: red;">
+                  {{ scope.row.nextTime }}
+                </span>
+              </template>
+              <!-- 其他普通字段 -->
+              <span v-else>{{ scope.row[col.prop] }}</span>
+            </template>
+
+
+          </el-table-column>
+
+          <!-- 操作列,始终显示 -->
+          <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+            <template slot-scope="scope">
+              <el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)" v-hasPermi="['crm:business:edit']">修改</el-button>
+              <el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)" v-hasPermi="['crm:business:remove']">删除</el-button>
+              <div v-if="scope.row.customerId !== null">
+                <el-button size="mini" type="text" icon="el-icon-s-custom" @click="handleShow(scope.row)" v-hasPermi="['crm:customer:query']">查看相关客户</el-button>
+              </div>
+              <div v-if="scope.row.customerId == null">
+                <el-button size="mini" type="text" icon="el-icon-s-custom" @click="handlevisitShow(scope.row)" v-hasPermi="['crm:customer:query']">查看商机跟进</el-button>
+              </div>
+            </template>
+          </el-table-column>
+        </el-table>
+
+        <pagination
+          v-show="total>0"
+          :total="total"
+          :page.sync="queryParams.pageNum"
+          :limit.sync="queryParams.pageSize"
+          @pagination="getList"
+        />
+
+        <el-dialog :title="title" :visible.sync="open" width="600px" append-to-body>
+          <add-business @closeBusiness="closeBusiness"   ref="addBusiness" />
+        </el-dialog>
+        <!-- 显示 allColumns 数据的对话框 -->
+        <el-dialog :visible.sync="columns.open" :title="columns.title">
+          <set-column ref="setColumn" @close="closeSetColumn"/>
+        </el-dialog>
+        <el-drawer size="75%" :title="show.title" :visible.sync="show.open">
+          <customer-details  ref="customerDetails" />
+        </el-drawer>
+        <el-dialog :title="visit.title" :visible.sync="visit.open" width="600px" append-to-body>
+          <add-visit @closeVisit="closeVisit"   ref="addVisit" />
+        </el-dialog>
+        <el-dialog :title="visitView.title" :visible.sync="visitView.open" width="1000px" append-to-body>
+            <customer-visit-list ref="visit"></customer-visit-list>
+        </el-dialog>
+
+        <el-dialog :title="upload.title" :visible.sync="upload.open" width="400px" append-to-body>
+            <el-upload ref="upload" :limit="1" accept=".xlsx, .xls" :headers="upload.headers" :action="upload.url + '?updateSupport=' + upload.updateSupport + '&type=0'" :disabled="upload.isUploading" :on-progress="handleFileUploadProgress" :on-success="handleFileSuccess" :auto-upload="false" drag>
+              <i class="el-icon-upload"></i>
+              <div class="el-upload__text">
+                将文件拖到此处,或
+                <em>点击上传</em>
+              </div>
+              <div class="el-upload__tip" slot="tip">
+                <el-link type="info" style="font-size:12px" @click="importTemplate">下载模板</el-link>
+              </div>
+              <div class="el-upload__tip" style="color:red" slot="tip">提示:仅允许导入“xls”或“xlsx”格式文件!</div>
+            </el-upload>
+            <div slot="footer" class="dialog-footer">
+              <el-button type="primary" @click="submitFileForm">确 定</el-button>
+              <el-button @click="upload.open = false">取 消</el-button>
+            </div>
+        </el-dialog>
+        <el-dialog title="导入结果" :close-on-press-escape="false" :close-on-click-modal="false" :visible.sync="importMsgOpen" width="500px" append-to-body>
+          <div class="import-msg" v-html="importMsg">
+          </div>
+        </el-dialog>
+      </div>
+    </div>
+  </template>
+
+  <script>
+  import { myListBusiness, getBusiness, delBusiness, updateBusiness, exportMyBusiness,setBusinessPool,importTemplate } from "@/api/crm/business";
+  import addBusiness from '../components/addBusiness.vue';
+  import setColumn from '../components/setColumn.vue';
+  import addVisit from '../components/addVisit.vue';
+  import {getShow} from "@/api/company/show";
+  import customerDetails from '../components/customerDetails.vue';
+  import customerVisitList from '../components/customerVisitList.vue';
+  import { getToken } from "@/utils/auth";
+  export default {
+    name: "Business",
+    components: {setColumn,addBusiness,customerDetails,addVisit,customerVisitList},
+    data() {
+      return {
+        // 用户导入参数
+        upload: {
+          // 是否显示弹出层(用户导入)
+          open: false,
+          // 弹出层标题(用户导入)
+          title: "",
+          // 是否禁用上传
+          isUploading: false,
+          // 设置上传的请求头部
+          headers: { Authorization: "Bearer " + getToken() },
+          // 上传的地址
+          url: process.env.VUE_APP_BASE_API + "/crm/business/importBusinessData",
+        },
+        importMsgOpen:false,
+        importMsg:"",
+        visitView:{
+            open:false,
+            title:"跟进记录"
+        },
+        visit:{
+            open:false,
+            title:"写跟进"
+        },
+        show:{
+          title:"客户详情",
+          open:false,
+        },
+        columns:{
+          title:"设置列表",
+          open:false,
+        },
+        columnType:'business',
+        visibleColumns: [],
+         // 可选字段
+         allColumns: [
+         { prop: "businessId", label: "商机ID" },
+          // { prop: "companyName", label: "客户名称" },
+          { prop: "customerId", label: "客户ID" },
+          { prop: "source", label: "线索来源" },
+          { prop: "manager", label: "客户经理" },
+          { prop: "companyName", label: "企业名称" },
+          { prop: "mobile", label: "相关客户电话",width:120  },
+          { prop: "mobileList", label: "电话号码",width:120},
+          { prop: "contactRole", label: "接口人角色" },
+          { prop: "businessScenario", label: "业务场景" },
+          { prop: "product", label: "方案涉及产品" },
+          { prop: "purchaseCycle", label: "采购周期" },
+          { prop: "businessStatus", label: "跟进状态" },
+          { prop: "content", label: "最新跟进内容" },
+          { prop: "nextTime", label: "下次跟进时间",width: 180, sortable: 'custom' },// 启用自定义排序
+          { prop: "projectPhase", label: "项目阶段" },
+          { prop: "level", label: "意向等级" },
+          { prop: "isBp", label: "是否绑定BP账号" },
+          { prop: "bpAccount", label: "BP账户" },
+          { prop: "preTime", label: "预计成单时间", width: 105 },
+          { prop: "preMoney", label: "预计付费" },
+          { prop: "createTime", label: "挖掘时间", width: 105 },
+        ],
+        // 选中的字段(默认显示所有字段)
+        selectedKeys: [
+          "businessId",
+          "customerId",
+          "source",
+          "manager",
+          "companyName",
+          "mobile",
+          "mobileList",
+          "contactRole",
+          "businessScenario",
+          "product",
+          "purchaseCycle",
+          "businessStatus",
+          "content",
+          "nextTime",
+          "projectPhase",
+          "level",
+          "isBp",
+          "bpAccount",
+          "preTime",
+          "preMoney",
+          "createTime"
+        ],
+        createTimeRange:[],
+        nextTimeRange:[],
+        recoveryRange:[],
+        statusOptions:[
+          {
+            dictValue:1,
+            dictLabel:"已分配"
+          },
+          {
+            dictValue:2,
+            dictLabel:"进行中"
+          },
+          {
+            dictValue:3,
+            dictLabel:"回收"
+          }
+        ],
+        clueSourceOptions:[],
+        contactRoleOptions:[],
+        projectPhaseOptions:[],
+        businessScenarioOptions:[],
+        levelOptions:[],
+        businessStatusOptions:[],
+        isBpOptions:[
+          {
+            dictValue:0,
+            dictLabel:"未注册"
+          },
+          {
+            dictValue:1,
+            dictLabel:"已注册未绑定"
+          },
+          {
+            dictValue:2,
+            dictLabel:"不明确"
+          },
+          {
+            dictValue:3,
+            dictLabel:"已绑定"
+          }
+        ],
+
+        // 遮罩层
+        loading: true,
+        // 选中数组
+        ids: [],
+        // 非单个禁用
+        single: true,
+        // 非多个禁用
+        multiple: true,
+        // 显示搜索条件
+        showSearch: true,
+        // 总条数
+        total: 0,
+        // 商机表格数据
+        businessList: [],
+        // 弹出层标题
+        title: "",
+        // 是否显示弹出层
+        open: false,
+        // 查询参数
+        queryParams: {
+          pageNum: 1,
+          pageSize: 10,
+          // customerId: null,
+          source: null,
+          manager: null,
+          companyName: null,
+          mobile: null,
+          contactRole: null,
+          businessScenario: null,
+          product: null,
+          purchaseCycle: null,
+          businessStatus: null,
+          projectPhase: null,
+          level: null,
+          isBp: null,
+          bpAccount: null,
+          preTime: null,
+          preMoney: null,
+          nextTime: null,
+          recoveryTime: null,
+          status: null,
+          companyName:null,
+          orderBy:null,//nextTime排序规则
+        },
+        // 表单参数
+        form: {},
+        // 表单校验
+        rules: {
+          customerId: [
+            { required: true, message: "客户id不能为空", trigger: "blur" }
+          ],
+        },
+
+      };
+    },
+    created() {
+      this.getDicts("business_scenario_type").then((response) => {
+        this.businessScenarioOptions = response.data;
+      });
+      this.getDicts("crm_customer_source").then((response) => {
+        this.clueSourceOptions = response.data;
+      });
+      this.getDicts("crm_contact_role").then((response) => {
+        this.contactRoleOptions = response.data;
+      });
+      this.getDicts("crm_project_phase").then((response) => {
+        this.projectPhaseOptions = response.data;
+      });
+      this.getDicts("crm_level").then((response) => {
+        this.levelOptions = response.data;
+      });
+      this.getDicts("crm_business_status").then((response) => {
+        this.businessStatusOptions = response.data;
+      });
+      this.getList();
+    },
+    methods: {
+      async handleSortChange({ prop, order }) {
+        try {
+          this.loading = true; // 开始加载
+          const sortOrder = order === 'ascending' ? 'asc' : 'desc'; // 转换排序顺序
+          // const params = {
+          //   sortOrder: sortOrder // 排序顺序
+          // };
+          this.queryParams.orderBy = sortOrder;
+
+          // 调用后端接口获取排序后的数据
+          const response = await this.getList();
+        } catch (error) {
+          console.error('排序失败:', error);
+        } finally {
+          this.loading = false; // 结束加载
+        }
+      },
+      handleAddVisit(row){
+          this.visit.open=true;
+          setTimeout(() => {
+              this.$refs.addVisit.reset(row.customerId);
+          }, 200);
+      },
+      handleAddVisitByBusiness(row){
+          this.visit.open=true;
+          setTimeout(() => {
+              this.$refs.addVisit.resetBusiness(row.businessId);
+          }, 200);
+      },
+      closeVisit(){
+          this.visit.open=false;
+          this.getList();
+      },
+      handlevisitShow(row){
+        this.visitView.open=true;
+        setTimeout(() => {
+            this.$refs.visit. getBusinessData(row.businessId);
+        }, 200);
+
+      },
+      handleShow(row){
+        this.show.open=true;
+        var that=this;
+        const tab = "visit";
+        setTimeout(() => {
+          that.$refs.customerDetails.getDetails(row.customerId);
+          that.$refs.customerDetails.handleClick(tab);
+
+        }, 200);
+      },
+      handleSetColumn(){
+        this.columns.open=true;
+        var that=this;
+        setTimeout(() => {
+              that.$refs.setColumn.reset(that.allColumns,that.selectedKeys,that.columnType);
+        }, 200);
+      },
+      closeSetColumn(){
+        this.columns.open=false;
+        this.getList();
+      },
+      handleBusiness(row){
+          this.business.open=true;
+          setTimeout(() => {
+              this.$refs.addBusiness.reset(row);
+          }, 200);
+      },
+      closeBusiness(){
+          this.open=false;
+          this.getList();
+      },
+      //查询显示字段
+      async getColumn(){
+        const response = await getShow(this.columnType);
+        if (response.code === 200 && response.data) {
+          this.selectedKeys = response.data.columns.split(",");
+          this.$set(this, "visibleColumns", this.allColumns.filter(col => this.selectedKeys.includes(col.prop)));
+
+        } else{
+          this.$set(this, "visibleColumns", this.allColumns);
+        }
+      },
+      /** 查询客户列表 */
+      async getList() {
+        this.loading = true;
+        await this.getColumn(); // 确保 visibleColumns 先获取到
+        if(this.createTimeRange!=null&&this.createTimeRange.length==2){
+          this.queryParams.createTimeRange=this.createTimeRange[0]+"--"+this.createTimeRange[1]
+        }
+        else{
+          this.queryParams.createTimeRange=null;
+        }
+        if(this.nextTimeRange!=null&&this.nextTimeRange.length==2){
+          this.queryParams.nextTimeRange=this.nextTimeRange[0]+"--"+this.nextTimeRange[1]
+        }
+        else{
+          this.queryParams.nextTimeRange=null;
+        }
+        if(this.recoveryRange!=null&&this.recoveryRange.length==2){
+          this.queryParams.recoveryRange=this.recoveryRange[0]+"--"+this.recoveryRange[1]
+        }
+        else{
+          this.queryParams.recoveryRange=null;
+        }
+        const response = await myListBusiness(this.addDateRange(this.queryParams, this.dateRange));
+        this.businessList = response.rows
+        this.total = response.total;
+        this.loading = false;
+
+      },
+      // 取消按钮
+      cancel() {
+        this.open = false;
+        this.reset();
+      },
+      // 表单重置
+      reset() {
+        this.form = {
+          businessId: null,
+          customerId: null,
+          source: null,
+          manager: null,
+          companyName: null,
+          mobile: null,
+          contactRole: null,
+          businessScenario: null,
+          product: null,
+          purchaseCycle: null,
+          businessStatus: 0,
+          remark: null,
+          projectPhase: null,
+          level: null,
+          isBp: null,
+          bpAccount: null,
+          preTime: null,
+          preMoney: null,
+          nextTime: null,
+          createTime: null,
+          updateTime: null,
+          createBy: null,
+          recoveryTime: null,
+          status: 0,
+          // createTimeRange:null,
+          // nextTimeRange:null,
+          // recoveryRange:null
+        };
+        this.resetForm("form");
+      },
+      /** 搜索按钮操作 */
+      handleQuery() {
+        this.queryParams.pageNum = 1;
+        this.getList();
+      },
+      /** 重置按钮操作 */
+      resetQuery() {
+        this.resetForm("queryForm");
+        this.handleQuery();
+      },
+      // 多选框选中数据
+      handleSelectionChange(selection) {
+        this.ids = selection.map(item => item.businessId)
+        this.single = selection.length!==1
+        this.multiple = !selection.length
+      },
+      /** 新增按钮操作 */
+      handleAdd() {
+        this.reset();
+        this.open = true;
+        this.title = "添加商机";
+      },
+      /** 修改按钮操作 */
+      handleUpdate(row) {
+        this.reset();
+        const businessId = row.businessId || this.ids
+        getBusiness(businessId).then(response => {
+          this.form = response.data;
+          this.form.source = String(response.data.source);
+          this.form.contactRole = String(response.data.contactRole);
+          this.form.level = String(response.data.level);
+          this.form.businessStatus =response.data.businessStatus;
+          console.log("111111111111111111111",JSON.stringify(this.form))
+          this.open = true;
+          this.title = "修改商机";
+          setTimeout(() => {
+              this.$refs.addBusiness.updateReset(row);
+          }, 200);
+        });
+      },
+      // /** 提交按钮 */
+      // submitForm() {
+      //   this.$refs["form"].validate(valid => {
+      //     if (valid) {
+      //       if (this.form.businessId != null) {
+      //         updateBusiness(this.form).then(response => {
+      //           if (response.code === 200) {
+      //             this.msgSuccess("修改成功");
+      //             this.open = false;
+      //             this.getList();
+      //           }
+      //         });
+      //       } else {
+      //         addBusiness(this.form).then(response => {
+      //           if (response.code === 200) {
+      //             this.msgSuccess("新增成功");
+      //             this.open = false;
+      //             this.getList();
+      //           }
+      //         });
+      //       }
+      //     }
+      //   });
+      // },
+      /** 删除按钮操作 */
+      handleDelete(row) {
+        const businessIds = row.businessId || this.ids;
+        this.$confirm('是否确认删除商机编号为"' + businessIds + '"的数据项?', "警告", {
+            confirmButtonText: "确定",
+            cancelButtonText: "取消",
+            type: "warning"
+          }).then(function() {
+            return delBusiness(businessIds);
+          }).then(() => {
+            this.getList();
+            this.msgSuccess("删除成功");
+          }).catch(function() {});
+      },
+     /** 导入按钮操作 */
+      handleImport() {
+        this.upload.title = "商机导入";
+        this.upload.open = true;
+
+      },
+      /** 下载模板操作 */
+      importTemplate() {
+        importTemplate().then((response) => {
+          this.download(response.msg);
+        });
+      },
+      // 文件上传中处理
+      handleFileUploadProgress(event, file, fileList) {
+        this.upload.isUploading = true;
+      },
+      // 文件上传成功处理
+      handleFileSuccess(response, file, fileList) {
+        this.upload.open = false;
+        this.upload.isUploading = false;
+        this.$refs.upload.clearFiles();
+        this.importMsgOpen=true;
+        this.importMsg=response.msg
+        this.getList();
+      },
+      // 提交上传文件
+      submitFileForm() {
+        this.$refs.upload.submit();
+      },
+      /** 导出按钮操作 */
+      handleExport() {
+        const queryParams = this.queryParams;
+        this.$confirm('是否确认导出所有商机数据项?', "警告", {
+            confirmButtonText: "确定",
+            cancelButtonText: "取消",
+            type: "warning"
+          }).then(function() {
+            return exportMyBusiness(queryParams);
+          }).then(response => {
+            this.download(response.msg);
+          }).catch(function() {});
+      }
+    }
+  };
+  </script>

+ 25 - 0
src/views/hisStore/components/productAfterSalesOrder.vue

@@ -68,6 +68,13 @@
                   <el-tag  v-for="(item, index) in salesStatusOptions"    v-if="afterSales.salesStatus==item.dictValue" >{{item.dictLabel}}</el-tag>
                 </span>
             </el-descriptions-item>
+
+            <el-descriptions-item label="财务审核原因" v-if="isRefundOrder">
+                <span>{{ financeAuditReasonText }}</span>
+            </el-descriptions-item>
+            <el-descriptions-item label="财务审核备注" v-if="isRefundOrder">
+                <span>{{ financeAuditRemarkText }}</span>
+            </el-descriptions-item>
         </el-descriptions>
         <div style="margin: 20px 0px">
           <span class="font-small">收货信息</span>
@@ -289,6 +296,24 @@ export default {
         this.serviceTypeOptions = response.data;
     });
   },
+  computed: {
+    isRefundOrder() {
+      return this.order != null && (this.order.status === -1 || this.order.status === -2);
+    },
+    financeAuditReasonText() {
+      if (!this.afterSales) {
+        return '';
+      }
+      const parts = [this.afterSales.reasonValue1, this.afterSales.reasonValue2].filter(Boolean);
+      if (parts.length) {
+        return parts.join(' / ');
+      }
+      return this.afterSales.auditReasonName || '';
+    },
+    financeAuditRemarkText() {
+      return this.afterSales ? (this.afterSales.auditRemark || '') : '';
+    }
+  },
   methods: {
     submitForm() {
       var id=this.afterSales.id;

+ 42 - 1
src/views/hisStore/components/productOrder.vue

@@ -178,6 +178,13 @@
                 </span>
           </el-descriptions-item>
 
+            <el-descriptions-item label="财务审核原因" v-if="isRefundOrder">
+                <span>{{ financeAuditReasonText }}</span>
+            </el-descriptions-item>
+            <el-descriptions-item label="财务审核备注" v-if="isRefundOrder">
+                <span>{{ financeAuditRemarkText }}</span>
+            </el-descriptions-item>
+
         </el-descriptions>
         <div style="margin: 20px 0px" v-if="order!=null">
         <span class="font-small">
@@ -1267,7 +1274,13 @@ export default {
       // 其它商品信息分页数据
       otherProductsPage: 1,
       otherProductsPageSize: 5,
-      otherProductsTotal: 0
+      otherProductsTotal: 0,
+      afterSales: {
+        auditReasonName: null,
+        auditRemark: null,
+        reasonValue1: null,
+        reasonValue2: null
+      }
     };
   },
   created() {
@@ -1326,6 +1339,28 @@ export default {
       }
       return `历史订单商品 ${this.currentOrderCode} - 商品详情`;
     },
+    /** 退款中 / 已退款 */
+    isRefundOrder() {
+      if (!this.order) return false;
+      const s = Number(this.order.status);
+      return s === -1 || s === -2;
+    },
+    /** 财务审核原因:售后一级 + 二级,兼容旧数据 auditReasonName */
+    financeAuditReasonText() {
+      const a = this.afterSales;
+      if (!a) return '—';
+      const parts = [a.reasonValue1, a.reasonValue2].filter(
+        x => x != null && String(x).trim() !== ''
+      );
+      if (parts.length > 0) return parts.join(' / ');
+      if (a.auditReasonName) return a.auditReasonName;
+      return '—';
+    },
+    financeAuditRemarkText() {
+      const a = this.afterSales;
+      if (!a || a.auditRemark == null || String(a.auditRemark).trim() === '') return '—';
+      return a.auditRemark;
+    }
 
   },
   methods: {
@@ -1961,6 +1996,12 @@ export default {
         this.payments = response.payments;
         this.customerInfo = response.customer;
         this.auditLogs = response.auditLogs;
+        this.afterSales = response.afterSales || {
+          auditReasonName: null,
+          auditRemark: null,
+          reasonValue1: null,
+          reasonValue2: null
+        };
 
         // 调用分页接口获取其他订单商品信息
         if (this.order && this.user) {

+ 3 - 0
src/views/hisStore/storeAfterSales/list.vue

@@ -130,6 +130,9 @@
               <div prop="serviceType" v-for="(item, index) in serviceTypeOptions"    v-if="scope.row.serviceType==item.dictValue">{{item.dictLabel}}</div>
           </template>
       </el-table-column>
+      <el-table-column label="售后原因一级" align="center" prop="reasonValue1" min-width="120" show-overflow-tooltip />
+      <el-table-column label="售后原因二级" align="center" prop="reasonValue2" min-width="120" show-overflow-tooltip />
+      <el-table-column label="售后备注" align="center" prop="auditRemark" min-width="140" show-overflow-tooltip />
       <el-table-column label="申请原因" align="center" prop="reasons" />
       <el-table-column label="说明" align="center" prop="explains" />
       <el-table-column label="状态" align="center" prop="status" >

+ 17 - 1
src/views/hisStore/storeAfterSales/myList.vue

@@ -47,6 +47,18 @@
       </el-form-item>
     </el-form>
 
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-button
+          type="warning"
+          icon="el-icon-download"
+          size="mini"
+          @click="handleExport"
+        >导出</el-button>
+      </el-col>
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+    </el-row>
+
     <el-table  height="500" border v-loading="loading" :data="storeAfterSalesList" @selection-change="handleSelectionChange">
       <el-table-column type="selection" width="55" align="center" />
       <el-table-column label="ID" align="center" prop="id" />
@@ -60,6 +72,9 @@
               <div prop="serviceType" v-for="(item, index) in serviceTypeOptions"    v-if="scope.row.serviceType==item.dictValue">{{item.dictLabel}}</div>
           </template>
       </el-table-column>
+      <el-table-column label="售后原因一级" align="center" prop="reasonValue1" min-width="120" show-overflow-tooltip />
+      <el-table-column label="售后原因二级" align="center" prop="reasonValue2" min-width="120" show-overflow-tooltip />
+      <el-table-column label="售后备注" align="center" prop="auditRemark" min-width="140" show-overflow-tooltip />
       <el-table-column label="申请原因" align="center" prop="reasons" />
       <el-table-column label="说明" align="center" prop="explains" />
       <el-table-column label="状态" align="center" prop="status" >
@@ -107,8 +122,9 @@
 <script>
 import { myListStoreAfterSales, getStoreAfterSales, delStoreAfterSales, addStoreAfterSales, updateStoreAfterSales, exportStoreAfterSales } from "@/api/hisStore/storeAfterSales";
 import productAfterSalesOrder from "../components/productAfterSalesOrder";
+import RightToolbar from "@/components/RightToolbar";
 export default {
-  components: { productAfterSalesOrder },
+  components: { productAfterSalesOrder, RightToolbar },
   name: "StoreAfterSales",
   data() {
     return {

+ 7 - 0
src/views/hisStore/storeOrder/healthStoreList.vue

@@ -535,6 +535,10 @@
         </template>
       </el-table-column>
 
+      <el-table-column align="center" label="售后原因一级" prop="reasonValue1" min-width="110" show-overflow-tooltip />
+      <el-table-column align="center" label="售后原因二级" prop="reasonValue2" min-width="110" show-overflow-tooltip />
+      <el-table-column align="center" label="售后备注" prop="auditRemark" min-width="120" show-overflow-tooltip />
+
       <el-table-column align="center" fixed="right" label="操作" width="80px">
         <template slot-scope="scope">
           <el-button
@@ -1106,6 +1110,9 @@ export default {
         { key: 'deliveryName', label: '快递公司', checked: false },
         { key: 'deliveryId', label: '快递单号', checked: false },
         { key: 'remark', label: '备注', checked: false },
+        { key: 'reasonValue1', label: '售后原因一级', checked: false },
+        { key: 'reasonValue2', label: '售后原因二级', checked: false },
+        { key: 'auditRemark', label: '售后备注', checked: false },
 
       ],
       appMallOptions:[],

+ 7 - 0
src/views/hisStore/storeOrder/index.vue

@@ -553,6 +553,10 @@
           </template>
       </el-table-column>
 
+        <el-table-column label="售后原因一级" align="center" prop="reasonValue1" width="120px" />
+        <el-table-column label="售后原因二级" align="center" prop="reasonValue2" width="120px" />
+        <el-table-column label="售后备注" align="center" prop="auditRemark" width="150px" />
+
       <el-table-column label="操作" fixed="right" width="80px" align="center" class-name="small-padding fixed-width">
         <template slot-scope="scope">
           <el-button
@@ -1117,6 +1121,9 @@ export default {
         { key: 'remark', label: '备注', checked: false },
         { key: 'erpPhone', label: 'ERP电话', checked: false },
         { key: 'erpAccount', label: 'ERP账号', checked: false },
+          { key: 'reasonValue1', label: '售后原因一级', checked: false },
+          { key: 'reasonValue2', label: '售后原因二级', checked: false },
+          { key: 'auditRemark', label: '售后备注', checked: false },
 
       ],
 

+ 20 - 0
src/views/live/components/productAfterSalesOrder.vue

@@ -64,6 +64,12 @@
                   <el-tag  v-for="(item, index) in salesStatusOptions"    v-if="afterSales.salesStatus==item.dictValue" >{{item.dictLabel}}</el-tag>
                 </span>
             </el-descriptions-item>
+            <el-descriptions-item label="财务审核原因" v-if="isRefundOrder">
+                <span>{{ financeAuditReasonText }}</span>
+            </el-descriptions-item>
+            <el-descriptions-item label="财务审核备注" v-if="isRefundOrder">
+                <span>{{ financeAuditRemarkText }}</span>
+            </el-descriptions-item>
         </el-descriptions>
         <div style="margin: 20px 0px">
           <span class="font-small">收货信息</span>
@@ -285,6 +291,20 @@ export default {
         this.serviceTypeOptions = response.data;
     });
   },
+  computed: {
+    isRefundOrder() {
+      return this.order != null && (this.order.status === -1 || this.order.status === -2);
+    },
+    financeAuditReasonText() {
+      if (!this.afterSales) return '';
+      const parts = [this.afterSales.reasonValue1, this.afterSales.reasonValue2].filter(Boolean);
+      if (parts.length) return parts.join(' / ');
+      return this.afterSales.auditReasonName || '';
+    },
+    financeAuditRemarkText() {
+      return this.afterSales ? (this.afterSales.auditRemark || '') : '';
+    }
+  },
   methods: {
     submitForm() {
       var id=this.afterSales.id;

+ 3 - 0
src/views/live/liveAfteraSales/index.vue

@@ -152,6 +152,9 @@
           <div prop="serviceType" v-for="(item, index) in serviceTypeOptions"    v-if="scope.row.serviceType==item.dictValue">{{item.dictLabel}}</div>
         </template>
       </el-table-column>
+      <el-table-column label="售后原因一级" align="center" prop="reasonValue1" min-width="120" show-overflow-tooltip />
+      <el-table-column label="售后原因二级" align="center" prop="reasonValue2" min-width="120" show-overflow-tooltip />
+      <el-table-column label="售后备注" align="center" prop="auditRemark" min-width="140" show-overflow-tooltip />
       <el-table-column label="申请原因" align="center" prop="reasons" />
       <el-table-column label="说明" align="center" prop="explains" />
       <el-table-column label="状态" align="center" prop="status" >

+ 3 - 0
src/views/live/liveOrder/index.vue

@@ -218,6 +218,9 @@
           <span>{{ formatDateTime(scope.row.createTime) }}</span>
         </template>
       </el-table-column>
+      <el-table-column label="售后原因一级" align="center" prop="reasonValue1" min-width="120" show-overflow-tooltip />
+      <el-table-column label="售后原因二级" align="center" prop="reasonValue2" min-width="120" show-overflow-tooltip />
+      <el-table-column label="售后备注" align="center" prop="auditRemark" min-width="140" show-overflow-tooltip />
       <el-table-column label="订单状态" align="center" prop="status">
         <template slot-scope="scope">
           <dict-tag :options="orderStatusOptions" :value="scope.row.status"/>

+ 16 - 0
src/views/live/liveOrder/liveOrderDetails.vue

@@ -106,6 +106,8 @@
           <el-descriptions-item label=" 公众号/渠道" ><span v-if="item!=null">{{item.channel}}</span></el-descriptions-item>
           <el-descriptions-item label=" 渠道" ><span v-if="item!=null"><dict-tag :options="channelOptions" :value="item.orderChannel"/></span></el-descriptions-item>
           <el-descriptions-item label=" 企微主体" ><span v-if="item!=null"><dict-tag :options="qwSubjectOptions" :value="item.qwSubject"/></span></el-descriptions-item>
+          <el-descriptions-item label="财务审核原因" v-if="isRefundOrder"><span>{{ financeAuditReasonText }}</span></el-descriptions-item>
+          <el-descriptions-item label="财务审核备注" v-if="isRefundOrder"><span>{{ financeAuditRemarkText }}</span></el-descriptions-item>
         </el-descriptions>
       </el-card>
     </div>
@@ -430,6 +432,20 @@ export default {
       this.deliveryTypeOptions = response.data;
     });
   },
+  computed: {
+    isRefundOrder() {
+      return this.item != null && (this.item.status === -1 || this.item.status === -2);
+    },
+    financeAuditReasonText() {
+      if (!this.item) return '';
+      const parts = [this.item.reasonValue1, this.item.reasonValue2].filter(Boolean);
+      if (parts.length) return parts.join(' / ');
+      return this.item.auditReasonName || '';
+    },
+    financeAuditRemarkText() {
+      return this.item ? (this.item.auditRemark || '') : '';
+    }
+  },
   methods: {
 
     followMsg(row){

+ 2305 - 0
src/views/qw/externalContact/customerDetail.vue

@@ -0,0 +1,2305 @@
+<template>
+    <div class="customer-container">
+        <div class="main-grid-three-columns">
+            <div class="left-column">
+                <!-- 客户画像 (成交要素) -->
+                <div class="card">
+                    <div class="card-header">
+                        <h3><i class="fas fa-id-card"></i> 客户画像(成交要素)</h3>
+                    </div>
+                    <div class="profile-grid">
+                        <div class="profile-item profile-item-main">
+                            <span class="label"><i class="fas fa-user"></i> 客户姓名:</span>
+                            <span class="value highlight">{{ (customerData && (customerData.customerName || customerData.name)) || '-' }}</span>
+                        </div>
+                        <template v-for="(value, key) in customerPortraitData">
+                            <div
+                                v-if="key !== '需求'"
+                                :key="key"
+                                class="profile-item"
+                            >
+                                <span class="label">
+                                    <i class="fas fa-info-circle"></i> {{ key }}:
+                                </span>
+                                <span class="value">{{ value }}</span>
+                            </div>
+                        </template>
+                        <!-- 需求单独显示,占满整行 -->
+                        <div
+                            v-if="customerPortraitData['需求']"
+                            key="需求"
+                            class="profile-item profile-item-full"
+                        >
+                            <span class="label">
+                                <i class="fas fa-bullseye"></i> 需求:
+                            </span>
+                            <span class="value long-text">{{ customerPortraitData['需求'] }}</span>
+                        </div>
+                    </div>
+                </div>
+                <!-- AI 标签 -->
+                <div class="card">
+                    <div class="card-header">
+                        <h3>
+                            <i class="fas fa-tags"></i> AI 标签
+                        </h3>
+                        <!-- <el-button
+                            v-if="allAiTags.length === 0"
+                            size="mini"
+                            type="primary"
+                            @click="handleAnalyzeTag"
+                        >
+                            AI分析标签
+                        </el-button> -->
+                    </div>
+                    <div class="tags-container">
+                        <div v-if="allAiTags.length > 0" class="tags-list">
+                            <div
+                                v-for="(item, index) in visibleTags"
+                                :key="item.id"
+                                class="tag-item"
+                                :class="{ 'tag-highlight': index < 3 }"
+                            >
+                                <span class="tag-key">{{ item.propertyName }}</span>
+                                <span class="tag-separator">:</span>
+                                <span class="tag-value">{{ item.propertyValue }}</span>
+                            </div>
+                        </div>
+                        <div v-else class="empty-tags">
+                            <i class="fas fa-inbox"></i>
+                            <span>暂无 AI 标签</span>
+                        </div>
+
+                        <!-- 加载更多按钮 -->
+                        <div v-if="allAiTags.length > tagsPageSize" class="tags-actions">
+                            <button
+                                v-if="!isExpanded"
+                                @click="loadMoreTags"
+                                class="btn-expand-tags"
+                                type="button"
+                            >
+                                <i class="fas fa-chevron-down"></i> 展开全部 ({{ allAiTags.length - tagsPageSize }})
+                            </button>
+
+                            <!-- 收起按钮 -->
+                            <button
+                                v-else
+                                @click="collapseTags"
+                                class="btn-collapse-tags"
+                                type="button"
+                            >
+                                <i class="fas fa-chevron-up"></i> 收起标签
+                            </button>
+                        </div>
+                    </div>
+                </div>
+            </div>
+
+            <div class="middle-column">
+                <!-- 沟通摘要 -->
+                <div class="card">
+                    <div class="card-header">
+                        <h3><i class="fas fa-comment-dots"></i> 沟通摘要</h3>
+                    </div>
+                    <div class="summary-text compact">
+                        {{ getCommunicationAbstract() }}
+                    </div>
+                </div>
+                <!-- AI 沟通总结 -->
+                <div class="card">
+                    <div class="card-header">
+                        <h3><i class="fas fa-robot"></i> AI 沟通总结</h3>
+                    </div>
+                    <div class="summary-text compact">
+                        {{ getCommunicationSummary() }}
+                    </div>
+                </div>
+                <!-- 沟通记录 -->
+                <div class="card card-table">
+                    <div class="card-header">
+                        <h3><i class="fas fa-history"></i> 沟通记录</h3>
+                    </div>
+                    <div class="records-table-wrapper">
+                        <table class="records-table">
+                            <thead>
+                            <tr>
+                                <th><i class="fas fa-user"></i> 客户名称</th>
+                                <th><i class="fas fa-chart-line"></i> 流失等级</th>
+                                <th><i class="fas fa-heart"></i> 客户意向度</th>
+                                <th><i class="far fa-clock"></i> 创建时间</th>
+                                <th><i class="fas fa-cog"></i> 操作</th>
+                            </tr>
+                            </thead>
+                            <tbody>
+                            <tr v-for="record in communicationRecords" :key="record.id" class="record-row">
+                                <td class="record-cell">{{ (customerData && (customerData.customerName || customerData.name)) || '-' }}</td>
+                                <td class="record-cell">
+                                        <span class="risk-level-tag" :class="getRecordRiskLevelClass(record)">
+                                            {{ getRecordRiskLevelLabel(record) }}
+                                        </span>
+                                </td>
+                                <td class="record-cell">
+                                        <span class="intention-degree">
+                                            {{ getIntentionDegreeFromRecord(record) }}
+                                        </span>
+                                </td>
+                                <td class="record-cell">{{ parseTime(record.createTime, '{y}-{m}-{d} {h}:{i}:{s}') || '-' }}</td>
+                                <td class="record-cell">
+                                    <button @click="viewChat(record)" class="btn-view-chat">
+                                        <i class="fas fa-comments"></i> 聊天详情
+                                    </button>
+                                </td>
+                            </tr>
+                            <tr v-if="!communicationRecords.length">
+                                <td colspan="5" class="empty-tip">
+                                    <i class="fas fa-inbox"></i> 暂无沟通记录
+                                </td>
+                            </tr>
+                            </tbody>
+                        </table>
+
+                        <!-- 分页组件 -->
+                        <div class="pagination-container" v-if="communicationRecordsTotal > 0">
+                            <el-pagination
+                                @current-change="handleCommunicationRecordsPageChange"
+                                @size-change="handleCommunicationRecordsSizeChange"
+                                :current-page="communicationRecordsPageNum"
+                                :page-sizes="[4, 10, 20, 50]"
+                                :page-size="communicationRecordsPageSize"
+                                layout="total, sizes, prev, pager, next, jumper"
+                                :total="communicationRecordsTotal"
+                            />
+                        </div>
+                    </div>
+                </div>
+                <!-- 微信风格聊天弹窗 -->
+                <div v-if="chatDialogVisible" class="chat-dialog-overlay" @click.self="closeChatDialog">
+                    <div class="chat-dialog">
+                        <div class="chat-dialog-header">
+                            <div class="chat-title">
+                                <i class="fas fa-comments"></i>
+                                <span>{{
+                                        (currentChatRecord && currentChatRecord.customerName) || (customerData && customerData.customerName)
+                                    }} - 历史聊天记录</span>
+                            </div>
+                            <button @click="closeChatDialog" class="btn-close">
+                                ×
+                            </button>
+                        </div>
+                        <div class="chat-dialog-body">
+                            <div class="chat-messages">
+                                <!-- 根据 aiChatRecord 数组循环显示聊天记录 -->
+                                <div
+                                    v-for="(msg, index) in parseChatMessages(currentChatRecord && currentChatRecord.aiChatRecord)"
+                                    :key="index"
+                                    class="message-item"
+                                    :class="msg.type === 'ai' ? 'message-left' : 'message-right'"
+                                >
+                                    <!-- AI 消息:头像在左,名称在聊天内容上方靠左 -->
+                                    <div v-if="msg.type === 'ai'" class="message-wrapper message-wrapper-left">
+                                        <div class="message-avatar message-avatar-ai">
+                                            <img src="/static/images/ai-avatar.svg" alt="AI"
+                                                 @error="handleAvatarError($event, 'ai')"/>
+                                        </div>
+                                        <div class="message-content">
+                                            <div class="message-name message-name-ai">AI</div>
+                                            <div class="message-bubble">
+                                                {{ msg.content }}
+                                            </div>
+                                        </div>
+                                    </div>
+
+                                    <!-- 客户消息:强制头像在右侧 -->
+                                    <div v-else class="message-item message-item-customer">
+                                        <div class="message-content-right">
+                                            <div class="message-bubble message-bubble-right">
+                                                {{ msg.content }}
+                                            </div>
+                                        </div>
+                                        <div class="message-avatar message-avatar-customer">
+                                            <img src="/static/images/customer-avatar.svg" alt="客户"
+                                                 @error="handleAvatarError($event, 'customer')"/>
+                                        </div>
+                                    </div>
+                                </div>
+
+                                <!-- 空数据提示 -->
+                                <div
+                                    v-if="!parseChatMessages(currentChatRecord && currentChatRecord.aiChatRecord).length"
+                                    class="empty-chat-tip">
+                                    <i class="fas fa-inbox"></i> 暂无聊天内容
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+
+            <!-- 流失风险等级 + 客户关注点 & 意向度 -->
+            <div class="right-column">
+                <!-- 流失风险等级 -->
+                <div class="card risk-card" :class="getRiskLevelClass()">
+                    <div class="card-header">
+                        <h3><i class="fas fa-chart-line"></i> 流失风险等级</h3>
+                        <span class="risk-badge" :class="getRiskLevelBadgeClass()">{{ getRiskLevelLabel() }}</span>
+                    </div>
+                    <div class="risk-analysis">
+                        <p class="risk-text">{{ getRiskLevelAnalysis() }}</p>
+                        <div class="risk-tip" v-if="getRiskLevelTip()">
+                            <i class="fas fa-exclamation-triangle"></i> {{ getRiskLevelTip() }}
+                        </div>
+                    </div>
+                </div>
+                <!-- 客户关注点 & 意向度 -->
+                <div class="card card-focus">
+                    <div class="card-header">
+                        <h3><i class="fas fa-lightbulb"></i> 客户关注点 & 意向度</h3>
+                    </div>
+                    <div class="focus-points">
+                        <div class="focus-title">
+                            <i class="fas fa-search"></i> 核心关注点:
+                        </div>
+                        <ul class="focus-list">
+                            <li class="focus-item">{{customerFocusPoints}}</li>
+                            <i class="fas fa-dot-circle"></i> {{ point }}
+                        </li>
+                        </ul>
+                    </div>
+                    <div class="intention-section">
+                        <div class="intention-header">
+                            <span class="intention-label">客户意向度</span>
+                            <el-tooltip placement="top" effect="light">
+                                <i class="el-icon-info intention-info-icon"></i>
+                                <div slot="content" class="intention-tooltip">
+                                    <div><strong>A 级</strong> - 最高意向度</div>
+                                    <div><strong>B 级</strong> - 高意向度</div>
+                                    <div><strong>C 级</strong> - 中等意向度</div>
+                                    <div><strong>D 级</strong> - 较低意向度</div>
+                                    <div><strong>E 级</strong> - 低意向度</div>
+                                    <div><strong>F 级</strong> - 最低意向度</div>
+                                </div>
+                            </el-tooltip>
+                        </div>
+                        <div class="intention-watermark"
+                             v-if="getIntentionDegree()"
+                             :class="getIntentionColorClass(getIntentionDegree())">
+                            {{ getIntentionDegree() }}
+                        </div>
+                        <div class="no-intention-tip" v-else>
+                            暂无评级
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+        <el-dialog
+        title="AI分析标签"
+        :visible.sync="analyzeTagDialogVisible"
+        width="420px"
+        append-to-body
+    >
+        <el-form label-width="90px">
+            <el-form-item label="行业类型">
+                <el-select
+                    v-model="selectedTradeType"
+                    placeholder="请选择行业类型"
+                    clearable
+                    style="width: 100%;"
+                >
+                    <el-option
+                        v-for="item in tradeTypeOptions"
+                        :key="item.dictValue"
+                        :label="item.dictLabel"
+                        :value="item.dictValue"
+                    />
+                </el-select>
+            </el-form-item>
+        </el-form>
+        <div slot="footer" class="dialog-footer">
+            <el-button @click="analyzeTagDialogVisible = false">取 消</el-button>
+            <el-button type="primary" :loading="analyzeTagSubmitting" @click="confirmAnalyzeTag">确 定</el-button>
+        </div>
+    </el-dialog>
+    </div>
+
+
+</template>
+
+<script>
+
+import {listByCustomerId,analyzeAiTagByTrade} from "../../../api/qw/customerProperty";
+import {listAnalyze} from "../../../api/qw/qwAnalyze";
+
+export default {
+    props: {
+        // 抽屉模式参数(父组件传入)
+        analyzeUserId: {
+            type: [String, Number],
+            default: null
+        },
+        analyzeExternalUserId: {
+            type: String,
+            default: null
+        },
+        analyzeCorpId: {
+            type: String,
+            default: null
+        },
+        customerRow: {
+            type: Object,
+            default: null
+        }
+    },
+    data() {
+        return {
+            userId: null,
+            externalUserId: null,
+            corpId: null,
+            customerData: null, // 从列表页传递过来的完整客户数据
+            aiTags: [],// 需要显示的 AI 标签
+            allAiTags: [], // 存储所有 AI 标签
+            tagsPageSize: 5,//默认展开标签的数量
+            isExpanded: false, // 是否已展开显示全部标签
+            // 聊天记录分页相关
+            communicationRecords: [],
+            communicationRecordsTotal: 0,
+            communicationRecordsPageNum: 1,
+            communicationRecordsPageSize: 4,
+            // 聊天弹窗相关
+            chatDialogVisible: false, // 聊天弹窗是否显示
+            currentChatRecord: null, // 当前查看的聊天记录
+            analyzeTagDialogVisible: false,
+            analyzeTagSubmitting: false,
+            selectedTradeType: null,
+            tradeTypeOptions:[],
+        }
+    },
+    computed: {
+        // 根据是否展开控制显示的标签数量
+        visibleTags() {
+            if (this.isExpanded) {
+                return this.allAiTags;
+            } else {
+                // 未展开时只显示前 3 条
+                return this.allAiTags.slice(0, this.tagsPageSize);
+            }
+        },
+        // 客户画像数据(从最新的沟通记录中获取)
+        customerPortraitData() {
+            if (!this.communicationRecords || this.communicationRecords.length === 0) {
+                return {};
+            }
+            // 获取最新的沟通记录
+            const latestRecord = this.communicationRecords[0];
+            if (latestRecord && latestRecord.customerPortraitJson) {
+                try {
+                    // 如果是字符串,解析为 JSON 对象
+                    if (typeof latestRecord.customerPortraitJson === 'string') {
+                        return JSON.parse(latestRecord.customerPortraitJson);
+                    }
+                    // 如果已经是对象,直接返回
+                    return latestRecord.customerPortraitJson;
+                } catch (error) {
+                    console.error('解析客户画像 JSON 失败:', error);
+                    return {};
+                }
+            }
+            return {};
+        },
+        // 客户关注点数据(从最新的沟通记录中获取)
+        customerFocusPoints() {
+            if (!this.communicationRecords || this.communicationRecords.length === 0) {
+                return ['暂无分析数据'];
+            }
+            const latestRecord = this.communicationRecords[0];
+            if (latestRecord && latestRecord.customerFocusJson) {
+                return this.normalizeFocusPoints(latestRecord.customerFocusJson);
+            }
+            return ['暂无分析数据'];
+        }
+    },
+    created() {
+        this.initFromParentOrRoute();
+        this.getDicts("trade_type").then((response) => {
+      this.tradeTypeOptions = response.data;
+    });
+    },
+    watch: {
+        analyzeUserId: {
+            immediate: false,
+            handler() {
+                // 抽屉重复打开/切换客户时刷新
+                this.initFromParentOrRoute();
+            }
+        },
+        analyzeExternalUserId() {
+            this.initFromParentOrRoute();
+        },
+        analyzeCorpId() {
+            this.initFromParentOrRoute();
+        }
+    },
+    methods: {
+        normalizeFocusPoints(value) {
+            if (value === null || value === undefined) return [];
+            if (Array.isArray(value)) {
+                return value.map(v => String(v)).filter(Boolean);
+            }
+
+            // 字符串:可能是 JSON 数组字符串,也可能是普通字符串
+            if (typeof value === 'string') {
+                const raw = value.trim();
+                if (!raw) return [];
+
+                // JSON 数组 / JSON 字符串尝试解析
+                if (
+                    (raw.startsWith('[') && raw.endsWith(']')) ||
+                    (raw.startsWith('"') && raw.endsWith('"')) ||
+                    (raw.startsWith("'") && raw.endsWith("'"))
+                ) {
+                    try {
+                        const parsed = JSON.parse(raw);
+                        if (Array.isArray(parsed)) {
+                            return parsed.map(v => String(v)).map(s => s.trim()).filter(Boolean);
+                        }
+                        if (typeof parsed === 'string') {
+                            return this.normalizeFocusPoints(parsed);
+                        }
+                    } catch (e) {
+                        // ignore,走后面的兜底清洗
+                    }
+                }
+
+                // 兜底:去掉中括号/引号后按分隔符拆分
+                let cleaned = raw;
+                if (cleaned.startsWith('[') && cleaned.endsWith(']')) {
+                    cleaned = cleaned.slice(1, -1);
+                }
+                cleaned = cleaned.replace(/["']/g, '');
+
+                const parts = cleaned
+                    .split(/[,,、;;\n]/g)
+                    .map(s => s.trim())
+                    .filter(Boolean);
+
+                return parts.length ? parts : [cleaned.trim()].filter(Boolean);
+            }
+
+            // 其它类型兜底
+            return [String(value)].filter(Boolean);
+        },
+        initFromParentOrRoute() {
+            // 优先用父组件传参(抽屉模式)
+            if (
+                (this.analyzeUserId !== null && this.analyzeUserId !== undefined && this.analyzeUserId !== '') ||
+                (this.analyzeExternalUserId !== null && this.analyzeExternalUserId !== undefined && this.analyzeExternalUserId !== '')
+            ) {
+                this.userId = this.analyzeUserId;
+                this.externalUserId = this.analyzeExternalUserId || (this.customerRow && (this.customerRow.externalUserId || this.customerRow.id));
+                this.corpId = this.analyzeCorpId;
+                this.customerData = this.customerRow || null;
+            } else {
+                // 路由模式兜底
+                this.userId = this.$route.query.userId || this.$route.query.qwUserId || this.$route.params.userId || this.$route.params.qwUserId;
+                this.externalUserId = this.$route.query.externalUserId || this.$route.query.id || this.$route.params.externalUserId || this.$route.params.id;
+                this.corpId = this.$route.query.corpId || this.$route.params.corpId;
+                if (this.$route.query.customerData) {
+                    try {
+                        this.customerData = JSON.parse(this.$route.query.customerData);
+                    } catch (error) {
+                        console.error('解析客户数据失败:', error);
+                    }
+                }
+            }
+
+            // 重置分页(切换客户时)
+            this.communicationRecordsPageNum = 1;
+            // 获取客户标签
+            this.loadCustomerTags();
+            // 加载客户分析信息
+            this.getCustomerInfoList();
+        },
+        loadCustomerTags() {
+            const params = {
+                externalUserId: this.externalUserId,
+                corpId: this.corpId,
+                qwUserId: this.userId,
+            };
+            return listByCustomerId(params).then((response) => {
+                if (response.code === 200) {
+                    this.allAiTags = response.data || [];
+                    // 强制 Vue 更新视图
+                    this.$forceUpdate();
+                } else {
+                    console.error('获取 AI 标签失败:', response);
+                }
+            }).catch(error => {
+                console.error('获取 AI 标签异常:', error);
+            });
+        },
+        handleAnalyzeTag() {
+            this.analyzeTagDialogVisible = true;
+        },
+        confirmAnalyzeTag() {
+            if (!this.selectedTradeType) {
+                this.$message.warning('请选择行业类型');
+                return;
+            }
+            if (!this.externalUserId || !this.corpId || !this.userId) {
+                this.$message.warning('客户参数不完整,无法分析标签');
+                return;
+            }
+            const data = {
+                externalUserId: this.externalUserId,
+                corpId: this.corpId,
+                qwUserId: this.userId,
+                tradeType: this.selectedTradeType
+            };
+            this.analyzeTagSubmitting = true;
+            analyzeAiTagByTrade(data).then((res) => {
+                if (res && res.code === 200) {
+                    this.$message.success(res.msg || 'AI分析标签成功');
+                    this.analyzeTagDialogVisible = false;
+                    this.loadCustomerTags();
+                } else {
+                    this.$message.error((res && res.msg) || 'AI分析标签失败');
+                }
+            }).catch(() => {
+                this.$message.error('AI分析标签失败');
+            }).finally(() => {
+                this.analyzeTagSubmitting = false;
+            });
+        },
+        getCustomerInfoList() {
+            const params = {
+                pageNum: this.communicationRecordsPageNum,
+                pageSize: this.communicationRecordsPageSize,
+                qwUserId: this.userId,
+                externalUserId: this.externalUserId,
+                corpId: this.corpId
+            };
+            return listAnalyze(params).then((response) => {
+                if (response.code === 200) {
+                    this.communicationRecords = response.rows || [];
+                    this.communicationRecordsTotal = response.total || 0;
+                } else {
+                    console.error('获取客户信息失败:', response);
+                }
+            }).catch(error => {
+                console.error('获取客户信息异常:', error);
+            });
+        },
+        // 加载更多标签 - 显示全部
+        loadMoreTags() {
+            this.isExpanded = true;
+        },
+        // 收起标签 - 只显示前 3 条
+        collapseTags() {
+            this.isExpanded = false;
+        },
+        // 查看聊天内容
+        viewChat(record) {
+            this.currentChatRecord = record;
+            this.chatDialogVisible = true;
+        },
+        // 关闭聊天弹窗
+        closeChatDialog() {
+            this.chatDialogVisible = false;
+            this.currentChatRecord = null;
+        },
+        // 解析聊天消息数组
+        parseChatMessages(content) {
+            if (!content) {
+                return [];
+            }
+            // 如果 content 是字符串,尝试解析为 JSON 数组
+            if (typeof content === 'string') {
+                try {
+                    const parsed = JSON.parse(content);
+                    // 如果是数组,直接返回
+                    if (Array.isArray(parsed)) {
+                        return parsed.map(item => ({
+                            content: item.ai || item.user,
+                            type: item.ai ? 'ai' : 'user'
+                        }));
+                    }
+                    // 如果是对象,转换为数组
+                    return [{content: parsed.content, type: parsed.type || 'ai'}];
+                } catch (e) {
+                    // 解析失败,返回空数组
+                    console.error('解析聊天记录失败:', e);
+                    return [];
+                }
+            }
+            // 如果已经是数组,直接返回
+            if (Array.isArray(content)) {
+                return content;
+            }
+            // 如果是对象,转换为数组
+            return [content];
+        },
+        // 处理头像加载失败
+        handleAvatarError(event, type) {
+            const img = event.target;
+            if (type === 'ai') {
+                // AI 头像加载失败时,使用渐变色背景 + 机器人图标
+                img.style.display = 'none';
+                img.parentElement.innerHTML = '<i class="fas fa-robot" style="font-size: 24px; color: white;"></i>';
+            } else {
+                // 客户头像加载失败时,使用渐变色背景 + 用户图标
+                img.style.display = 'none';
+                img.parentElement.innerHTML = '<i class="fas fa-user" style="font-size: 24px; color: white;"></i>';
+            }
+        },
+
+        // 分页改变事件
+        handleCommunicationRecordsPageChange(pageNum) {
+            this.communicationRecordsPageNum = pageNum;
+            this.getCustomerInfoList();
+        },
+        // 每页条数改变事件
+        handleCommunicationRecordsSizeChange(pageSize) {
+            this.communicationRecordsPageSize = pageSize;
+            this.communicationRecordsPageNum = 1; // 重置为第一页
+            this.getCustomerInfoList();
+        },
+        // 获取沟通摘要
+        getCommunicationAbstract() {
+            if (!this.communicationRecords || this.communicationRecords.length === 0) {
+                return '暂无沟通记录';
+            }
+            const latestRecord = this.communicationRecords[0];
+            return latestRecord.communicationAbstract || '暂无沟通摘要';
+        },
+        // 获取 AI 沟通总结
+        getCommunicationSummary() {
+            if (!this.communicationRecords || this.communicationRecords.length === 0) {
+                return '暂无沟通记录';
+            }
+            const latestRecord = this.communicationRecords[0];
+            return latestRecord.communicationSummary || '暂无 AI 沟通总结';
+        },
+        // 获取最后更新时间
+        getUpdateTime() {
+            if (!this.communicationRecords || this.communicationRecords.length === 0) {
+                return '-';
+            }
+            const latestRecord = this.communicationRecords[0];
+            return latestRecord.createTime || '-';
+        },
+        // 获取流失风险等级数值
+        getAttritionLevel() {
+            if (!this.communicationRecords || this.communicationRecords.length === 0) {
+                return 0;
+            }
+            const latestRecord = this.communicationRecords[0];
+            return parseInt(latestRecord.attritionLevel) || 0;
+        },
+        // 获取流失风险等级标签
+        getRiskLevelLabel() {
+            const level = this.getAttritionLevel();
+            const labels = ['未知', '无风险', '低风险', '中风险', '高风险'];
+            return labels[level] || '未知';
+        },
+        // 获取流失风险等级徽章样式类
+        getRiskLevelBadgeClass() {
+            const level = this.getAttritionLevel();
+            const badgeClasses = [
+                'badge-unknown',
+                'badge-none',
+                'badge-low',
+                'badge-medium',
+                'badge-high'
+            ];
+            return badgeClasses[level] || 'badge-unknown';
+        },
+        // 获取客户意向度
+        getIntentionDegree() {
+            if (!this.communicationRecords || this.communicationRecords.length === 0) {
+                return '';
+            }
+            const latestRecord = this.communicationRecords[0];
+            return latestRecord.intentionDegree || '';
+        },
+        // 根据意向度等级获取颜色样式类
+        getIntentionColorClass(grade) {
+            if (!grade) return '';
+            const gradeUpper = grade.toUpperCase();
+            const colorMap = {
+                'A': 'intention-grade-a',
+                'B': 'intention-grade-b',
+                'C': 'intention-grade-c',
+                'D': 'intention-grade-d',
+                'E': 'intention-grade-e',
+                'F': 'intention-grade-f'
+            };
+            return colorMap[gradeUpper] || '';
+        },
+        // 获取单条记录的风险等级数值
+        getRecordAttritionLevel(record) {
+            if (!record) return 0;
+            return parseInt(record.attritionLevel) || 0;
+        },
+        // 获取单条记录的风险等级标签
+        getRecordRiskLevelLabel(record) {
+            const level = this.getRecordAttritionLevel(record);
+            const labels = ['未知', '无风险', '低风险', '中风险', '高风险'];
+            return labels[level] || '未知';
+        },
+        // 获取单条记录的风险等级样式类
+        getRecordRiskLevelClass(record) {
+            const level = this.getRecordAttritionLevel(record);
+            const classes = ['risk-unknown', 'risk-none', 'risk-low', 'risk-medium', 'risk-high'];
+            return classes[level] || 'risk-unknown';
+        },
+        // 获取单条记录的客户意向度
+        getIntentionDegreeFromRecord(record) {
+            if (!record) return 0;
+            return record.intentionDegree;
+        },
+        // 获取流失风险等级样式类
+        getRiskLevelClass() {
+            const level = this.getAttritionLevel();
+            const classes = ['risk-unknown', 'risk-none', 'risk-low', 'risk-medium', 'risk-high'];
+            return classes[level] || 'risk-unknown';
+        },
+        // 获取流失风险分析
+        getRiskLevelAnalysis() {
+            const level = this.getAttritionLevel();
+            if (!this.communicationRecords || this.communicationRecords.length === 0) {
+                return '暂无分析数据';
+            }
+            const latestRecord = this.communicationRecords[0];
+            return latestRecord.attritionLevelPrompt || "暂无分析数据";
+        },
+        // 获取流失风险提示
+        getRiskLevelTip() {
+            const level = this.getAttritionLevel();
+            const tips = [
+                '暂无分析',
+                '客户稳定,可以放心。',
+                '建议定期回访,了解客户最新需求。',
+                '建议安排专项跟进,深入了解客户痛点和需求。',
+                '建议立即联系客户,了解问题原因并提供解决方案。'
+            ];
+            return tips[level] || '';
+        },
+    }
+}</script>
+
+<style scoped>
+* {
+    box-sizing: border-box;
+}
+
+.left-column {
+    display: flex;
+    flex-direction: column;
+    gap: 12px;
+}
+
+.middle-column {
+    display: flex;
+    flex-direction: column;
+    gap: 12px;
+}
+
+.right-column {
+    display: flex;
+    flex-direction: column;
+    gap: 12px;
+}
+
+.customer-container {
+    max-width: 1600px;
+    margin: 0 auto;
+    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
+    background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
+    padding: 8px;
+    min-height: calc(100vh - 50px);
+}
+
+@keyframes pulse {
+    0%, 100% {
+        transform: scale(1);
+    }
+    50% {
+        transform: scale(1.1);
+    }
+}
+
+.main-grid-three-columns {
+    display: grid;
+    grid-template-columns: 380px 1fr 340px;
+    gap: 12px;
+    animation: fadeIn 0.6s ease-in-out;
+    align-items: stretch;
+}
+
+@keyframes fadeIn {
+    from {
+        opacity: 0;
+        transform: translateY(20px);
+    }
+    to {
+        opacity: 1;
+        transform: translateY(0);
+    }
+}
+
+@media (max-width: 1400px) {
+    .main-grid-three-columns {
+        grid-template-columns: 360px 1fr 320px;
+        gap: 24px;
+    }
+}
+
+@media (max-width: 1200px) {
+    .main-grid-three-columns {
+        grid-template-columns: 1fr;
+    }
+}
+
+.card {
+    background: white;
+    border-radius: 12px;
+    padding: 12px;
+    margin-bottom: 12px;
+    box-shadow: 0 4px 16px rgba(0, 0, 0, 0.06);
+    transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
+    border: 1px solid rgba(226, 232, 240, 0.5);
+    position: relative;
+    overflow: hidden;
+}
+
+.card::before {
+    content: '';
+    position: absolute;
+    top: 0;
+    left: 0;
+    right: 0;
+    height: 3px;
+    background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
+    transform: scaleX(0);
+    transition: transform 0.4s ease;
+}
+
+.card:hover::before {
+    transform: scaleX(1);
+}
+
+.card:hover {
+    box-shadow: 0 12px 48px rgba(0, 0, 0, 0.15);
+    transform: translateY(-4px);
+    border-color: rgba(102, 126, 234, 0.3);
+}
+
+/* 高亮卡片(AI 总结) */
+.card-highlight {
+    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+    color: white;
+    border: none;
+    box-shadow: 0 8px 32px rgba(102, 126, 234, 0.4);
+    position: relative;
+    overflow: hidden;
+}
+
+.card-highlight::after {
+    content: '';
+    position: absolute;
+    top: -50%;
+    right: -50%;
+    width: 200%;
+    height: 200%;
+    background: radial-gradient(circle, rgba(255, 255, 255, 0.1) 0%, transparent 70%);
+    animation: shimmer 3s infinite;
+}
+
+@keyframes shimmer {
+    0%, 100% {
+        transform: translate(0, 0);
+    }
+    50% {
+        transform: translate(-30%, -30%);
+    }
+}
+
+.card-highlight .card-header h3 {
+    color: white;
+}
+
+.card-highlight .summary-text {
+    color: white;
+    font-size: 17px;
+    line-height: 1.6;
+    max-height: 120px;
+    overflow-y: auto;
+    overflow-x: hidden;
+    padding-right: 4px;
+}
+
+.card-highlight .summary-text::-webkit-scrollbar {
+    width: 6px;
+}
+
+.card-highlight .summary-text::-webkit-scrollbar-track {
+    background: rgba(255, 255, 255, 0.2);
+    border-radius: 3px;
+}
+
+.card-highlight .summary-text::-webkit-scrollbar-thumb {
+    background: rgba(255, 255, 255, 0.4);
+    border-radius: 3px;
+}
+
+.card-highlight .summary-text::-webkit-scrollbar-thumb:hover {
+    background: rgba(255, 255, 255, 0.6);
+}
+
+.card-highlight .summary-meta span {
+    color: rgba(255, 255, 255, 0.9);
+}
+
+/* 表格卡片 */
+.card-table {
+    background: white;
+}
+
+/* 风险卡片 */
+.risk-card {
+    border-left: 4px solid #10b981;
+    background: linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%);
+    transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+.risk-card.risk-unknown {
+    border-left-color: #6b7280;
+    background: linear-gradient(135deg, #f9fafb 0%, #f3f4f6 100%);
+}
+
+.risk-card.risk-unknown::before {
+    background: linear-gradient(90deg, #6b7280 0%, #4b5563 100%);
+}
+
+.risk-card::before {
+    background: linear-gradient(90deg, #10b981 0%, #34d399 100%);
+}
+
+.risk-unknown .risk-badge {
+    background: linear-gradient(135deg, #6b7280 0%, #4b5563 100%);
+    box-shadow: 0 2px 8px rgba(107, 114, 128, 0.3);
+}
+
+.risk-none .risk-badge {
+    background: linear-gradient(135deg, #10b981 0%, #34d399 100%);
+    box-shadow: 0 2px 8px rgba(16, 185, 129, 0.3);
+}
+
+.risk-low .risk-badge {
+    background: linear-gradient(135deg, #3b82f6 0%, #60a5fa 100%);
+    box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3);
+}
+
+.risk-medium .risk-badge {
+    background: linear-gradient(135deg, #f59e0b 0%, #fbbf24 100%);
+    box-shadow: 0 2px 8px rgba(245, 158, 11, 0.3);
+}
+
+.risk-high .risk-badge {
+    background: linear-gradient(135deg, #ef4444 0%, #f87171 100%);
+    box-shadow: 0 2px 8px rgba(239, 68, 68, 0.3);
+}
+
+.risk-card:hover {
+    transform: translateY(-2px);
+    box-shadow: 0 12px 48px rgba(0, 0, 0, 0.1);
+}
+
+/* 风险等级标签 */
+.risk-badge {
+    display: inline-flex;
+    align-items: center;
+    gap: 6px;
+    padding: 6px 14px;
+    border-radius: 8px;
+    font-size: 16px;
+    font-weight: 700;
+    color: white;
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
+    transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+    text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
+}
+
+.risk-badge::before {
+    content: '';
+    width: 6px;
+    height: 6px;
+    background: white;
+    border-radius: 50%;
+    animation: pulse 2s infinite;
+}
+
+@keyframes pulse {
+    0%, 100% {
+        opacity: 1;
+        transform: scale(1);
+    }
+    50% {
+        opacity: 0.5;
+        transform: scale(1.2);
+    }
+}
+
+.risk-card:hover .risk-badge {
+    transform: scale(1.05);
+}
+
+/* 风险分析内容 */
+.risk-analysis {
+    margin-top: 10px;
+    padding: 10px;
+    background: rgba(255, 255, 255, 0.7);
+    border-radius: 10px;
+    backdrop-filter: blur(10px);
+    max-height: 180px;
+    overflow-y: auto;
+    overflow-x: hidden;
+    padding-right: 4px;
+}
+
+.risk-analysis::-webkit-scrollbar {
+    width: 6px;
+}
+
+.risk-analysis::-webkit-scrollbar-track {
+    background: rgba(255, 255, 255, 0.5);
+    border-radius: 3px;
+}
+
+.risk-analysis::-webkit-scrollbar-thumb {
+    background: linear-gradient(180deg, #cbd5e1 0%, #94a3b8 100%);
+    border-radius: 3px;
+}
+
+.risk-analysis::-webkit-scrollbar-thumb:hover {
+    background: linear-gradient(180deg, #94a3b8 0%, #64748b 100%);
+}
+
+.risk-text {
+    font-size: 16px;
+    line-height: 1.7;
+    color: #475569;
+    margin: 0;
+}
+
+.risk-tip {
+    margin-top: 8px;
+    padding: 8px;
+    background: linear-gradient(135deg, rgba(255, 255, 255, 0.9) 0%, rgba(248, 250, 252, 0.9) 100%);
+    border-left: 3px solid #f59e0b;
+    border-radius: 6px;
+    font-size: 15px;
+    color: #92400e;
+    display: flex;
+    align-items: flex-start;
+    gap: 6px;
+}
+
+.risk-tip i {
+    font-size: 12px;
+    margin-top: 1px;
+    color: #f59e0b;
+}
+
+/* 关注点卡片 */
+.card-focus {
+    background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);
+    border: 2px solid #bae6fd;
+    box-shadow: 0 4px 16px rgba(186, 230, 253, 0.3);
+}
+
+.card-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    border-bottom: 1px solid #eef2ff;
+    padding-bottom: 6px;
+    margin-bottom: 10px;
+    flex-wrap: wrap;
+}
+
+.card-header h3 {
+    font-size: 19px;
+    font-weight: 700;
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    color: #0f172a;
+    letter-spacing: -0.02em;
+}
+
+.records-table-wrapper {
+    overflow-x: auto;
+}
+
+/* 分页容器样式 */
+.pagination-container {
+    padding: 16px 0 12px;
+    display: flex;
+    justify-content: center;
+    background: white;
+    border-top: 1px solid #f1f5f9;
+    margin-top: 12px;
+}
+
+/* Element UI 分页样式覆盖 */
+
+.pagination-container .el-pagination li {
+    min-width: 32px;
+    height: 32px;
+    line-height: 32px;
+    border-radius: 6px;
+    transition: all 0.3s ease;
+}
+
+.pagination-container .el-pagination li:hover {
+    background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%);
+    border-color: #667eea;
+}
+
+.records-table {
+    width: 100%;
+    border-collapse: collapse;
+    font-size: 15px;
+}
+
+.records-table thead {
+    background: transparent;
+    color: #475569;
+}
+
+.records-table th {
+    padding: 12px 16px;
+    text-align: center !important;
+    font-weight: 600;
+    font-size: 14px;
+    border-bottom: 2px solid #e2e8f0;
+    color: #64748b;
+}
+
+.records-table th i {
+    margin-right: 6px;
+    opacity: 0.8;
+}
+
+.records-table tbody tr {
+    border-bottom: 1px solid #f1f5f9;
+    transition: all 0.2s ease;
+}
+
+.records-table tbody tr:hover {
+    background-color: #f8fafc;
+    transform: none;
+    box-shadow: none;
+}
+
+.records-table td {
+    padding: 12px 16px;
+    vertical-align: middle;
+}
+
+.record-cell {
+    font-size: 15px;
+    color: #334155;
+    text-align: center !important;
+}
+
+/* 风险等级标签 */
+.risk-level-tag {
+    display: inline-block;
+    padding: 4px 12px;
+    border-radius: 6px;
+    font-size: 13px;
+    font-weight: 500;
+    border: 1px solid;
+}
+
+/* 客户意向度 */
+.intention-degree {
+    display: inline-block;
+    padding: 4px 12px;
+    border-radius: 6px;
+    font-size: 14px;
+    font-weight: 600;
+    color: #6366f1;
+    background: rgba(99, 102, 241, 0.08);
+    border: 1px solid rgba(99, 102, 241, 0.2);
+}
+
+.btn-view-chat {
+    background: transparent;
+    color: #667eea;
+    border: 1px solid #667eea;
+    padding: 6px 12px;
+    border-radius: 6px;
+    font-size: 14px;
+    cursor: pointer;
+    display: inline-flex;
+    align-items: center;
+    gap: 4px;
+    transition: all 0.2s ease;
+}
+
+.btn-view-chat:hover {
+    background: #f0f4ff;
+}
+
+.empty-tip {
+    color: #94a3b8;
+    font-size: 16px;
+    text-align: center;
+    padding: 40px 20px;
+    background: transparent;
+    border: none;
+}
+
+/* 沟通摘要样式 */
+.summary-text.compact {
+    max-height: 100px;
+    overflow-y: auto;
+    overflow-x: hidden;
+    line-height: 1.6;
+    font-size: 16px;
+    color: #475569;
+    padding-right: 4px;
+}
+
+.summary-text.compact::-webkit-scrollbar {
+    width: 6px;
+}
+
+.summary-text.compact::-webkit-scrollbar-track {
+    background: #f1f5f9;
+    border-radius: 3px;
+}
+
+.summary-text.compact::-webkit-scrollbar-thumb {
+    background: linear-gradient(180deg, #cbd5e1 0%, #94a3b8 100%);
+    border-radius: 3px;
+}
+
+.summary-text.compact::-webkit-scrollbar-thumb:hover {
+    background: linear-gradient(180deg, #94a3b8 0%, #64748b 100%);
+}
+
+/* AI 标签美化样式 */
+.tags-container {
+    padding: 0;
+    min-height: calc(16px * 5 + 6px * 4 + 12px * 2);
+    overflow-y: auto;
+    overflow-x: hidden;
+    display: flex;
+    flex-direction: column;
+}
+
+.tags-container::-webkit-scrollbar {
+    width: 6px;
+}
+
+.tags-container::-webkit-scrollbar-track {
+    background: #f1f5f9;
+    border-radius: 3px;
+}
+
+.tags-container::-webkit-scrollbar-thumb {
+    background: linear-gradient(180deg, #cbd5e1 0%, #94a3b8 100%);
+    border-radius: 3px;
+}
+
+.tags-container::-webkit-scrollbar-thumb:hover {
+    background: linear-gradient(180deg, #94a3b8 0%, #64748b 100%);
+}
+
+/* 标签列表 - 每行一个 */
+.tags-list {
+    display: flex;
+    flex-direction: column;
+    gap: 6px;
+    margin-bottom: 6px;
+}
+
+.tag-item {
+    display: flex;
+    align-items: center;
+    gap: 6px;
+    padding: 6px 10px;
+    background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
+    border: 1px solid #e2e8f0;
+    border-radius: 6px;
+    font-size: 15px;
+    transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+    cursor: default;
+    position: relative;
+    overflow: hidden;
+}
+
+.tag-item::before {
+    content: '';
+    position: absolute;
+    top: 0;
+    left: 0;
+    width: 4px;
+    height: 100%;
+    background: linear-gradient(180deg, #94a3b8 0%, #64748b 100%);
+    transition: width 0.3s ease;
+}
+
+.tag-item:hover {
+    background: linear-gradient(135deg, #f1f5f9 0%, #e2e8f0 100%);
+    border-color: #cbd5e1;
+    transform: translateX(6px);
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
+}
+
+.tag-item:hover::before {
+    width: 5px;
+    background: linear-gradient(180deg, #3b82f6 0%, #2563eb 100%);
+}
+
+.tag-highlight {
+    background: linear-gradient(135deg, #eff6ff 0%, #dbeafe 100%);
+    border-color: #bfdbfe;
+}
+
+.tag-highlight::before {
+    background: linear-gradient(180deg, #3b82f6 0%, #2563eb 100%);
+}
+
+.tag-key {
+    font-weight: 600;
+    color: #475569;
+    white-space: nowrap;
+    flex-shrink: 0;
+    font-size: 15px;
+}
+
+.tag-separator {
+    color: #94a3b8;
+    font-weight: 300;
+    flex-shrink: 0;
+    font-size: 15px;
+}
+
+.tag-value {
+    color: #1e293b;
+    font-weight: 500;
+    word-break: break-word;
+    flex: 1;
+    min-width: 0;
+    font-size: 15px;
+}
+
+.empty-tags {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    padding: 40px 20px;
+    color: #94a3b8;
+    font-size: 17px;
+    background: linear-gradient(135deg, rgba(248, 250, 252, 0.5) 0%, rgba(241, 245, 249, 0.5) 100%);
+    border-radius: 12px;
+    border: 2px dashed #e2e8f0;
+}
+
+.empty-tags i {
+    font-size: 32px;
+    margin-bottom: 12px;
+    opacity: 0.5;
+}
+
+.tags-actions {
+    display: flex;
+    justify-content: center;
+    padding: 16px 0;
+    border-top: 1px solid #f1f5f9;
+    margin-top: auto;
+    background: white;
+    position: sticky;
+    bottom: 0;
+    z-index: 10;
+}
+
+.btn-expand-tags,
+.btn-collapse-tags {
+    background: transparent;
+    color: #667eea;
+    border: 1px solid #667eea;
+    padding: 8px 16px;
+    border-radius: 6px;
+    font-size: 16px;
+    cursor: pointer;
+    display: inline-flex;
+    align-items: center;
+    gap: 6px;
+    transition: all 0.2s ease;
+}
+
+.btn-expand-tags:hover,
+.btn-collapse-tags:hover {
+    background: rgba(102, 126, 234, 0.05);
+    border-color: #5a67d8;
+    color: #5a67d8;
+}
+
+.btn-expand-tags:active,
+.btn-collapse-tags:active {
+    transform: scale(0.98);
+}
+
+/* 微信风格聊天弹窗 */
+.chat-dialog-overlay {
+    position: fixed;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    background: rgba(0, 0, 0, 0.5);
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    z-index: 9999;
+    backdrop-filter: blur(4px);
+}
+
+.chat-dialog {
+    background: white;
+    border-radius: 16px;
+    width: 90%;
+    max-width: 800px;
+    height: 600px;
+    display: flex;
+    flex-direction: column;
+    box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
+    animation: slideIn 0.3s ease-out;
+    position: relative;
+}
+
+@keyframes slideIn {
+    from {
+        opacity: 0;
+        transform: translateY(-20px);
+    }
+    to {
+        opacity: 1;
+        transform: translateY(0);
+    }
+}
+
+.chat-dialog-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    padding: 16px 20px;
+    border-bottom: 1px solid #eef2ff;
+    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+    border-radius: 16px 16px 0 0;
+    color: white;
+}
+
+.chat-title {
+    display: flex;
+    align-items: center;
+    gap: 10px;
+    font-size: 16px;
+    font-weight: 700;
+}
+
+.btn-close {
+    position: absolute;
+    top: 16px;
+    right: 16px;
+    background: white;
+    border: 2px solid #e2e8f0;
+    color: #1a202c;
+    width: 36px;
+    height: 36px;
+    border-radius: 50%;
+    cursor: pointer;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    transition: all 0.3s ease;
+    font-size: 24px;
+    font-weight: bold;
+    line-height: 1;
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
+    z-index: 10;
+}
+
+.btn-close:hover {
+    background: #ef4444;
+    border-color: #ef4444;
+    color: white;
+    transform: rotate(90deg) scale(1.1);
+    box-shadow: 0 4px 16px rgba(239, 68, 68, 0.4);
+}
+
+.chat-dialog-body {
+    flex: 1;
+    overflow-y: auto;
+    padding: 20px;
+    background: #f5f7fa;
+}
+
+.chat-messages {
+    display: flex;
+    flex-direction: column;
+    gap: 16px;
+}
+
+.message-item {
+    display: flex;
+    align-items: flex-start;
+    margin-bottom: 8px;
+}
+
+.message-left {
+    justify-content: flex-start;
+}
+
+.message-right {
+    justify-content: flex-end;
+}
+
+/* 客户消息强制布局:头像在右 */
+.message-item-customer {
+    display: flex !important;
+    flex-direction: row !important;
+    justify-content: flex-end !important;
+    gap: 10px !important;
+}
+
+.message-wrapper {
+    display: flex;
+    align-items: flex-start;
+    gap: 10px;
+    max-width: 75%;
+}
+
+.message-wrapper-left {
+    flex-direction: row;
+}
+
+.message-name {
+    font-size: 12px;
+    color: #94a3b8;
+    white-space: nowrap;
+    text-align: left;
+    margin-bottom: 4px;
+    line-height: 1.2;
+}
+
+.message-name-ai {
+    color: #667eea;
+    font-weight: 500;
+}
+
+.message-avatar {
+    width: 32px;
+    height: 32px;
+    border-radius: 6px;
+    overflow: hidden;
+    flex-shrink: 0;
+    box-shadow: 0 2px 6px rgba(0, 0, 0, 0.12);
+    background: #f0f0f0;
+}
+
+.message-avatar img {
+    width: 100%;
+    height: 100%;
+    object-fit: cover;
+    transition: transform 0.3s ease;
+}
+
+.message-avatar:hover img {
+    transform: scale(1.1);
+}
+
+.message-wrapper-left .message-avatar {
+    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    flex-shrink: 0;
+}
+
+.message-wrapper-right .message-avatar {
+    background: linear-gradient(135deg, #10b981 0%, #059669 100%);
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    flex-shrink: 0;
+}
+
+.message-content {
+    display: flex;
+    flex-direction: column;
+    max-width: calc(100% - 42px);
+}
+
+.message-wrapper-left .message-content {
+    align-items: flex-start;
+    margin-left: 4px;
+}
+
+.message-wrapper-right .message-content {
+    align-items: flex-end !important;
+    margin-right: 4px;
+}
+
+/* 客户聊天内容容器 */
+.message-content-right {
+    flex: 0 0 auto !important;
+    max-width: calc(100% - 42px) !important;
+    display: flex;
+    align-items: flex-start !important;
+}
+
+.message-bubble {
+    background: white;
+    padding: 9px 13px;
+    border-radius: 6px;
+    font-size: 14px;
+    line-height: 1.5;
+    color: #0f172a;
+    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
+    word-break: break-word;
+    position: relative;
+    max-width: 500px;
+}
+
+.message-bubble::before {
+    content: '';
+    position: absolute;
+    left: -6px;
+    top: 12px;
+    width: 0;
+    height: 0;
+    border-top: 5px solid transparent;
+    border-bottom: 5px solid transparent;
+    border-right: 6px solid white;
+}
+
+.message-bubble-right {
+    background: #d9fdd3;
+    color: #0f172a;
+    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
+    display: inline-block;
+    position: relative;
+}
+
+.message-bubble-right::before {
+    content: '';
+    position: absolute;
+    right: -6px;
+    left: auto;
+    top: 16px !important;
+    transform: none !important;
+    width: 0;
+    height: 0;
+    border-top: 5px solid transparent;
+    border-bottom: 5px solid transparent;
+    border-left: 6px solid #d9fdd3;
+    border-right: none;
+}
+
+.empty-chat-tip {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    padding: 60px 20px;
+    color: #94a3b8;
+    font-size: 14px;
+}
+
+.empty-chat-tip i {
+    font-size: 48px;
+    margin-bottom: 12px;
+    opacity: 0.5;
+}
+
+/* 客户画像样式 */
+.profile-grid {
+    display: flex;
+    flex-direction: column;
+    gap: 4px;
+    max-height: 350px;
+    overflow-y: auto;
+    overflow-x: hidden;
+    padding-right: 4px;
+    padding-top: 0;
+}
+
+.profile-grid::-webkit-scrollbar {
+    width: 6px;
+}
+
+.profile-grid::-webkit-scrollbar-track {
+    background: #f1f5f9;
+    border-radius: 3px;
+}
+
+.profile-grid::-webkit-scrollbar-thumb {
+    background: linear-gradient(180deg, #cbd5e1 0%, #94a3b8 100%);
+    border-radius: 3px;
+}
+
+.profile-grid::-webkit-scrollbar-thumb:hover {
+    background: linear-gradient(180deg, #94a3b8 0%, #64748b 100%);
+}
+
+.profile-item {
+    display: grid;
+    grid-template-columns: 42% 58%;
+    align-items: flex-start;
+    gap: 6px;
+    padding: 4px 10px;
+    border-radius: 6px;
+    transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+    background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
+    border: 1px solid #e2e8f0;
+    word-break: break-word;
+}
+
+.profile-item:hover {
+    background: linear-gradient(135deg, #f1f5f9 0%, #e2e8f0 100%);
+    transform: translateX(4px);
+    box-shadow: 0 2px 6px rgba(0, 0, 0, 0.06);
+    border-color: #cbd5e1;
+}
+
+.profile-item-main {
+    background: linear-gradient(135deg, #eff6ff 0%, #dbeafe 100%);
+    border-color: #bfdbfe;
+    position: sticky;
+    top: 0;
+    z-index: 10;
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
+}
+
+.profile-item-main:hover {
+    background: linear-gradient(135deg, #dbeafe 0%, #bfdbfe 100%);
+}
+
+.profile-item-full {
+    grid-template-columns: 42% 58%;
+}
+
+.profile-item .label {
+    font-size: 15px;
+    color: #64748b;
+    font-weight: 600;
+    white-space: normal;
+    display: flex;
+    align-items: center;
+    gap: 4px;
+    line-height: 1.4;
+    min-width: 0;
+}
+
+.profile-item .label i {
+    color: #94a3b8;
+    font-size: 12px;
+    width: 14px;
+    text-align: center;
+    flex-shrink: 0;
+}
+
+.profile-item .value {
+    font-size: 16px;
+    color: #0f172a;
+    font-weight: 500;
+    word-break: break-word;
+    line-height: 1.4;
+    min-width: 0;
+}
+
+.profile-item .value.highlight {
+    color: #0369a1;
+    font-size: 16px;
+    font-weight: 600;
+}
+
+.profile-item .value.long-text {
+    color: #334155;
+    font-weight: 400;
+}
+
+.update-time-corner {
+    position: absolute;
+    bottom: 12px;
+    right: 16px;
+    font-size: 12px;
+    color: #94a3b8;
+    font-style: italic;
+}
+
+/* 客户关注点 & 意向度样式 */
+.focus-points {
+    padding: 0;
+    margin-bottom: 10px;
+}
+
+.focus-title {
+    font-size: 17px;
+    color: #64748b;
+    font-weight: 600;
+    margin-bottom: 8px;
+    display: flex;
+    align-items: center;
+    gap: 6px;
+}
+
+.focus-title i {
+    color: #3b82f6;
+    font-size: 14px;
+}
+
+.focus-list {
+    list-style: none;
+    padding: 0;
+    margin: 0;
+    max-height: 200px;
+    overflow-y: auto;
+    overflow-x: hidden;
+    padding-right: 4px;
+}
+
+.focus-list::-webkit-scrollbar {
+    width: 6px;
+}
+
+.focus-list::-webkit-scrollbar-track {
+    background: #f1f5f9;
+    border-radius: 3px;
+}
+
+.focus-list::-webkit-scrollbar-thumb {
+    background: linear-gradient(180deg, #cbd5e1 0%, #94a3b8 100%);
+    border-radius: 3px;
+}
+
+.focus-list::-webkit-scrollbar-thumb:hover {
+    background: linear-gradient(180deg, #94a3b8 0%, #64748b 100%);
+}
+
+.focus-item {
+    display: flex;
+    align-items: flex-start;
+    gap: 6px;
+    padding: 6px 10px;
+    margin-bottom: 6px;
+    background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
+    border-radius: 6px;
+    border: 1px solid #e2e8f0;
+    transition: all 0.3s ease;
+    font-size: 15px;
+    color: #334155;
+    line-height: 1.5;
+}
+
+.focus-item:hover {
+    background: linear-gradient(135deg, #eff6ff 0%, #dbeafe 100%);
+    border-color: #bfdbfe;
+    transform: translateX(4px);
+    box-shadow: 0 2px 6px rgba(59, 130, 246, 0.1);
+}
+
+.focus-item i {
+    color: #3b82f6;
+    font-size: 10px;
+    margin-top: 1px;
+    flex-shrink: 0;
+}
+
+/* 意向度样式 - 水印风格 */
+.intention-section {
+    margin-top: 10px;
+    padding-top: 8px;
+    border-top: 1px solid #e2e8f0;
+}
+
+.intention-header {
+    margin-bottom: 6px;
+    display: flex;
+    align-items: center;
+    gap: 6px;
+}
+
+.intention-info-icon {
+    color: #94a3b8;
+    font-size: 16px;
+    cursor: pointer;
+    transition: all 0.3s ease;
+}
+
+.intention-info-icon:hover {
+    color: #3b82f6;
+}
+
+/* 意向度提示框样式 */
+.intention-tooltip {
+    line-height: 1.8;
+    font-size: 13px;
+}
+
+.intention-tooltip div {
+    padding: 2px 0;
+}
+
+.intention-tooltip strong {
+    color: #1e293b;
+    font-weight: 600;
+}
+
+.intention-label {
+    font-size: 16px;
+    font-weight: 600;
+    color: #64748b;
+    display: flex;
+    align-items: center;
+    gap: 4px;
+}
+
+.intention-label::before {
+    content: '';
+    width: 3px;
+    height: 14px;
+    background: linear-gradient(180deg, #3b82f6 0%, #2563eb 100%);
+    border-radius: 2px;
+}
+
+/* 水印风格意向度显示 - 按等级着色 */
+.intention-watermark {
+    font-size: 59px;
+    font-weight: 800;
+    text-align: center;
+    padding: 18px 12px;
+    border-radius: 10px;
+    border: 2px solid;
+    position: relative;
+    overflow: hidden;
+    letter-spacing: 6px;
+    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
+    transition: all 0.3s ease;
+}
+
+/* A 级 - 金色/绿色,最高级别 */
+.intention-grade-a {
+    background: linear-gradient(135deg, #fef3c7 0%, #fde68a 50%, #fef3c7 100%);
+    border-color: #f59e0b;
+    color: #92400e;
+    text-shadow: 0 2px 4px rgba(146, 64, 14, 0.2);
+}
+
+.intention-grade-a::after {
+    content: 'A';
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, -50%) rotate(-25deg);
+    font-size: 90px;
+    font-weight: 900;
+    color: rgba(245, 158, 11, 0.08);
+    z-index: 0;
+    pointer-events: none;
+}
+
+/* B 级 - 蓝色 */
+.intention-grade-b {
+    background: linear-gradient(135deg, #dbeafe 0%, #bfdbfe 50%, #dbeafe 100%);
+    border-color: #3b82f6;
+    color: #1e40af;
+    text-shadow: 0 2px 4px rgba(30, 64, 175, 0.2);
+}
+
+.intention-grade-b::after {
+    content: 'B';
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, -50%) rotate(-25deg);
+    font-size: 90px;
+    font-weight: 900;
+    color: rgba(59, 130, 246, 0.08);
+    z-index: 0;
+    pointer-events: none;
+}
+
+/* C 级 - 紫色 */
+.intention-grade-c {
+    background: linear-gradient(135deg, #e9d5ff 0%, #d8b4fe 50%, #e9d5ff 100%);
+    border-color: #a855f7;
+    color: #6b21a8;
+    text-shadow: 0 2px 4px rgba(107, 33, 168, 0.2);
+}
+
+.intention-grade-c::after {
+    content: 'C';
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, -50%) rotate(-25deg);
+    font-size: 90px;
+    font-weight: 900;
+    color: rgba(168, 85, 247, 0.08);
+    z-index: 0;
+    pointer-events: none;
+}
+
+/* D 级 - 橙色 */
+.intention-grade-d {
+    background: linear-gradient(135deg, #fed7aa 0%, #fdba74 50%, #fed7aa 100%);
+    border-color: #f97316;
+    color: #9a3412;
+    text-shadow: 0 2px 4px rgba(154, 52, 18, 0.2);
+}
+
+.intention-grade-d::after {
+    content: 'D';
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, -50%) rotate(-25deg);
+    font-size: 90px;
+    font-weight: 900;
+    color: rgba(249, 115, 22, 0.08);
+    z-index: 0;
+    pointer-events: none;
+}
+
+/* E 级 - 粉红色 */
+.intention-grade-e {
+    background: linear-gradient(135deg, #fbcfe8 0%, #f9a8d4 50%, #fbcfe8 100%);
+    border-color: #ec4899;
+    color: #9d174d;
+    text-shadow: 0 2px 4px rgba(157, 23, 77, 0.2);
+}
+
+.intention-grade-e::after {
+    content: 'E';
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, -50%) rotate(-25deg);
+    font-size: 90px;
+    font-weight: 900;
+    color: rgba(236, 72, 153, 0.08);
+    z-index: 0;
+    pointer-events: none;
+}
+
+/* F 级 - 红色,最低级别 */
+.intention-grade-f {
+    background: linear-gradient(135deg, #fecaca 0%, #fca5a5 50%, #fecaca 100%);
+    border-color: #ef4444;
+    color: #991b1b;
+    text-shadow: 0 2px 4px rgba(153, 27, 27, 0.2);
+}
+
+.intention-grade-f::after {
+    content: 'F';
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, -50%) rotate(-25deg);
+    font-size: 90px;
+    font-weight: 900;
+    color: rgba(239, 68, 68, 0.08);
+    z-index: 0;
+    pointer-events: none;
+}
+
+.intention-watermark span {
+    position: relative;
+    z-index: 1;
+}
+
+/* 暂无评级提示 */
+.no-intention-tip {
+    text-align: center;
+    padding: 18px 12px;
+    font-size: 16px;
+    color: #94a3b8;
+    font-style: italic;
+    background: linear-gradient(135deg, rgba(248, 250, 252, 0.5) 0%, rgba(241, 245, 249, 0.5) 100%);
+    border-radius: 10px;
+    border: 2px dashed #e2e8f0;
+}
+
+.card-header h3 i {
+    font-size: 20px;
+    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+    -webkit-background-clip: text;
+    -webkit-text-fill-color: transparent;
+    background-clip: text;
+}
+
+.card-highlight .card-header h3 i {
+    color: white;
+    background: none;
+    -webkit-text-fill-color: white;
+}
+
+/* 统一为 CRM 客户分析报告风格(覆盖) */
+.customer-container {
+    max-width: 100%;
+    padding: 12px;
+    background: #f4f6fa;
+}
+
+.main-grid-three-columns {
+    grid-template-columns: 300px minmax(640px, 1fr) 320px;
+    gap: 12px;
+    align-items: start;
+}
+
+.card {
+    border-radius: 10px;
+    border: 1px solid #e6ebf2;
+    box-shadow: 0 1px 3px rgba(15, 23, 42, 0.06);
+    margin-bottom: 10px;
+    padding: 12px;
+    background: #fff;
+}
+
+.card::before {
+    display: none;
+}
+
+.card:hover {
+    transform: none;
+    box-shadow: 0 2px 8px rgba(15, 23, 42, 0.08);
+    border-color: #dbe4f0;
+}
+
+.card-header {
+    border-bottom: 1px solid #edf1f7;
+    margin-bottom: 10px;
+    padding-bottom: 8px;
+}
+
+.card-header h3 {
+    font-size: 16px;
+    font-weight: 600;
+    color: #1f2937;
+}
+
+.card-header h3 i {
+    width: 18px;
+    height: 18px;
+    border-radius: 4px;
+    font-size: 10px;
+    background: #4f7cff;
+    box-shadow: none;
+    -webkit-text-fill-color: #fff;
+}
+
+.summary-text.compact {
+    font-size: 14px;
+    line-height: 1.75;
+    color: #334155;
+    max-height: 132px;
+}
+
+.risk-card,
+.card-focus {
+    background: #fff;
+    border: 1px solid #e6ebf2;
+}
+
+.risk-analysis {
+    background: #f8fafc;
+    border: 1px solid #edf2f7;
+}
+
+/* 风险徽章改为标签风格 */
+.risk-badge {
+    display: inline-flex;
+    align-items: center;
+    gap: 6px;
+    padding: 5px 10px;
+    border-radius: 10px;
+    font-size: 13px;
+    font-weight: 600;
+    border: 1px solid #e5eaf1;
+    box-shadow: none;
+    transition: all 0.2s ease;
+    text-shadow: none;
+}
+
+.risk-badge::before {
+    content: '';
+    width: 8px;
+    height: 8px;
+    border-radius: 3px;
+    background: currentColor;
+    opacity: 0.35;
+}
+
+.risk-card:hover .risk-badge {
+    transform: translateY(-1px);
+}
+
+.risk-unknown .risk-badge {
+    background: #f8fafc;
+    border-color: #e5eaf1;
+    color: #64748b;
+}
+
+.risk-none .risk-badge {
+    background: #f0fdf4;
+    border-color: #bbf7d0;
+    color: #16a34a;
+}
+
+.risk-low .risk-badge {
+    background: #eff6ff;
+    border-color: #bfdbfe;
+    color: #2563eb;
+}
+
+.risk-medium .risk-badge {
+    background: #fffbeb;
+    border-color: #fde68a;
+    color: #d97706;
+}
+
+.risk-high .risk-badge {
+    background: #fef2f2;
+    border-color: #fecaca;
+    color: #dc2626;
+}
+
+/* 客户意向度:回滚为原来的“大水印渐变”样式(使用上方原始定义) */
+
+.tag-item,
+.focus-item,
+.profile-item {
+    background: #f8fafc;
+    border-color: #e5eaf1;
+}
+
+.records-table th {
+    background: #f8fafc;
+    color: #475569;
+}
+
+.records-table tbody tr:hover {
+    background: #f8fbff;
+}
+
+
+</style>

+ 123 - 4
src/views/qw/externalContact/deptIndex.vue

@@ -365,7 +365,62 @@
           </div>
         </template>
       </el-table-column>
-
+      <el-table-column label="流失风险" align="center" prop="attritionLevel">
+        <template slot-scope="scope">
+          <el-tag v-if="scope.row.attritionLevel == null" type="info">未分析</el-tag>
+          <el-tag v-if="scope.row.attritionLevel === 0" type="info">未知</el-tag>
+          <el-tag v-else-if="scope.row.attritionLevel === 1" type="success">无风险</el-tag>
+          <el-tag v-else-if="scope.row.attritionLevel === 2" type="info">低风险</el-tag>
+          <el-tag v-else-if="scope.row.attritionLevel === 3" type="warning">中风险</el-tag>
+          <el-tag v-else-if="scope.row.attritionLevel === 4" type="danger">高风险</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="意向度" align="center" prop="intentionDegree">
+        <template slot-scope="scope">
+          <el-tag v-if="scope.row.intentionDegree !== null && scope.row.intentionDegree !== undefined && scope.row.intentionDegree !== ''">
+            {{ scope.row.intentionDegree }}
+          </el-tag>
+          <span v-else>-</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="关注点" align="center" prop="customerFocusJson" width="220">
+        <template slot-scope="scope">
+          <div v-if="parseFocusPoints(scope.row.customerFocusJson).length" style="text-align: left;">
+            <el-tooltip
+              v-for="(item, index) in parseFocusPoints(scope.row.customerFocusJson).slice(0, 2)"
+              :key="index"
+              placement="top"
+              effect="light"
+            >
+              <div slot="content" style="max-width: 420px; word-break: break-word;">
+                {{ item }}
+              </div>
+              <el-tag style="margin: 0 6px 6px 0; max-width: 100%; background: #fff; border: 1px solid #dcdfe6; color: #606266;">
+                {{ shortenText(item, 14) }}
+              </el-tag>
+            </el-tooltip>
+            <el-tooltip
+              v-if="parseFocusPoints(scope.row.customerFocusJson).length > 2"
+              placement="top"
+              effect="light"
+            >
+              <div slot="content" style="max-width: 420px;">
+                <div
+                  v-for="(item, idx) in parseFocusPoints(scope.row.customerFocusJson).slice(2)"
+                  :key="'focus-more-' + idx"
+                  style="margin-bottom: 4px;"
+                >
+                  {{ item }}
+                </div>
+              </div>
+              <el-tag style="margin: 0 6px 6px 0; background: #fff; border: 1px solid #dcdfe6; color: #606266;">
+                +{{ parseFocusPoints(scope.row.customerFocusJson).length - 2 }}
+              </el-tag>
+            </el-tooltip>
+          </div>
+          <span v-else>-</span>
+        </template>
+      </el-table-column>
       <el-table-column label="状态" align="center" prop="status" width="120px" >
         <template slot-scope="scope">
           <dict-tag :options="statusOptions" :value="scope.row.status"/>
@@ -507,6 +562,13 @@
             @click="healthHandledetails(scope.row)"
           >健康档案
           </el-button>
+          <el-button
+          v-if="scope.row.attritionLevel !== null && scope.row.attritionLevel !== undefined"
+          size="mini"
+          type="text"
+          @click="openAiDrawer(scope.row)"
+          v-hasPermi="['qw:externalContact:analyze:list']"
+        >AI 分析</el-button>
         </template>
       </el-table-column>
     </el-table>
@@ -800,6 +862,20 @@
       :visible.sync="showUser.open">
       <userDetails  ref="userDetails" />
     </el-drawer>
+    <el-drawer
+    size="75%"
+    :title="aiAnalyze.title"
+    :visible.sync="aiAnalyze.open"
+    append-to-body
+  >
+    <customer-detail
+      ref="customerAiDetail"
+      :analyze-user-id="aiAnalyze.userId"
+      :analyze-external-user-id="aiAnalyze.externalUserId"
+      :analyze-corp-id="aiAnalyze.corpId"
+      :customer-row="aiAnalyze.customerRow"
+    />
+  </el-drawer>
   </div>
 </template>
 
@@ -836,10 +912,11 @@ import {editTalk} from "@/api/qw/externalContactInfo";
 import healthRecordDetails from '@/views/store/components/healthRecordDetails.vue'
 import userDetails from '@/views/store/components/userDetails.vue';
 import PaginationMore from "../../../components/PaginationMore/index.vue";
+import customerDetail from './customerDetail.vue';
 
 export default {
   name: "deptExternalContact",
-  components:{PaginationMore, mycustomer,customerDetails,SopDialog,selectUser,info,healthRecordDetails,userDetails},
+  components:{PaginationMore, mycustomer,customerDetails,SopDialog,selectUser,info,healthRecordDetails,userDetails,customerDetail},
   data() {
     return {
 
@@ -1052,7 +1129,15 @@ export default {
       },
       tongueReportParams: {
         userId: null,
-      }
+      },
+      aiAnalyze: {
+        title: "AI 分析",
+        open: false,
+        userId: null,
+        externalUserId: null,
+        corpId: null,
+        customerRow: null,
+      },
     };
   },
   created() {
@@ -1102,7 +1187,41 @@ export default {
 
   },
   methods: {
-
+    shortenText(text, maxLen = 16) {
+      const str = text == null ? '' : String(text);
+      if (str.length <= maxLen) return str;
+      return str.slice(0, maxLen) + '...';
+    },
+    parseFocusPoints(value) {
+      if (!value) return [];
+      if (Array.isArray(value)) {
+        return value.map(item => String(item).trim()).filter(Boolean);
+      }
+      if (typeof value === 'string') {
+        const raw = value.trim();
+        if (!raw) return [];
+        try {
+          const parsed = JSON.parse(raw);
+          if (Array.isArray(parsed)) {
+            return parsed.map(item => String(item).trim()).filter(Boolean);
+          }
+          if (typeof parsed === 'string') {
+            return [parsed.trim()].filter(Boolean);
+          }
+        } catch (e) {
+          // ignore parse error and use raw fallback
+        }
+        return [raw.replace(/^\[|\]$/g, '').replace(/["']/g, '').trim()].filter(Boolean);
+      }
+      return [String(value).trim()].filter(Boolean);
+    },
+    openAiDrawer(row) {
+      this.aiAnalyze.userId = row.userId;
+      this.aiAnalyze.externalUserId = row.operUserid ;
+      this.aiAnalyze.corpId = row.corpId ;
+      this.aiAnalyze.customerRow = row;
+      this.aiAnalyze.open = true;
+    },
     onQwUserNameClear() {
       this.queryParams.qwUserId = null;  // 同时清空 qwUserId
     },

+ 122 - 2
src/views/qw/externalContact/index.vue

@@ -401,6 +401,62 @@
           </div>
         </template>
       </el-table-column>
+      <el-table-column label="流失风险" align="center" prop="attritionLevel">
+        <template slot-scope="scope">
+          <el-tag v-if="scope.row.attritionLevel == null" type="info">未分析</el-tag>
+          <el-tag v-if="scope.row.attritionLevel === 0" type="info">未知</el-tag>
+          <el-tag v-else-if="scope.row.attritionLevel === 1" type="success">无风险</el-tag>
+          <el-tag v-else-if="scope.row.attritionLevel === 2" type="info">低风险</el-tag>
+          <el-tag v-else-if="scope.row.attritionLevel === 3" type="warning">中风险</el-tag>
+          <el-tag v-else-if="scope.row.attritionLevel === 4" type="danger">高风险</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="意向度" align="center" prop="intentionDegree">
+        <template slot-scope="scope">
+          <el-tag v-if="scope.row.intentionDegree !== null && scope.row.intentionDegree !== undefined && scope.row.intentionDegree !== ''">
+            {{ scope.row.intentionDegree }}
+          </el-tag>
+          <span v-else>-</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="关注点" align="center" prop="customerFocusJson" width="220">
+        <template slot-scope="scope">
+          <div v-if="parseFocusPoints(scope.row.customerFocusJson).length" style="text-align: left;">
+            <el-tooltip
+              v-for="(item, index) in parseFocusPoints(scope.row.customerFocusJson).slice(0, 2)"
+              :key="index"
+              placement="top"
+              effect="light"
+            >
+              <div slot="content" style="max-width: 420px; word-break: break-word;">
+                {{ item }}
+              </div>
+              <el-tag style="margin: 0 6px 6px 0; max-width: 100%; background: #fff; border: 1px solid #dcdfe6; color: #606266;">
+                {{ shortenText(item, 14) }}
+              </el-tag>
+            </el-tooltip>
+            <el-tooltip
+              v-if="parseFocusPoints(scope.row.customerFocusJson).length > 2"
+              placement="top"
+              effect="light"
+            >
+              <div slot="content" style="max-width: 420px;">
+                <div
+                  v-for="(item, idx) in parseFocusPoints(scope.row.customerFocusJson).slice(2)"
+                  :key="'focus-more-' + idx"
+                  style="margin-bottom: 4px;"
+                >
+                  {{ item }}
+                </div>
+              </div>
+              <el-tag style="margin: 0 6px 6px 0; background: #fff; border: 1px solid #dcdfe6; color: #606266;">
+                +{{ parseFocusPoints(scope.row.customerFocusJson).length - 2 }}
+              </el-tag>
+            </el-tooltip>
+          </div>
+          <span v-else>-</span>
+        </template>
+      </el-table-column>
       <el-table-column label="是否回复" align="center" prop="isReply" width="120px" >
         <template slot-scope="scope">
           <span v-if="scope.row.isReply === 1"><el-tag type="success">已回复</el-tag></span>
@@ -566,6 +622,13 @@
           >
             修改状态
           </el-button>
+          <el-button
+                v-if="scope.row.attritionLevel !== null && scope.row.attritionLevel !== undefined"
+                size="mini"
+                type="text"
+                @click="openAiDrawer(scope.row)"
+                v-hasPermi="['qw:externalContact:analyze:list']"
+              >AI 分析</el-button>
         </template>
       </el-table-column>
     </el-table>
@@ -577,7 +640,20 @@
       :limit.sync="queryParams.pageSize"
       @pagination="getList"
     />
-
+    <el-drawer
+    size="75%"
+    :title="aiAnalyze.title"
+    :visible.sync="aiAnalyze.open"
+    append-to-body
+  >
+    <customer-detail
+      ref="customerAiDetail"
+      :analyze-user-id="aiAnalyze.userId"
+      :analyze-external-user-id="aiAnalyze.externalUserId"
+      :analyze-corp-id="aiAnalyze.corpId"
+      :customer-row="aiAnalyze.customerRow"
+    />
+  </el-drawer>
     <el-drawer size="75%" :title="show.title" :visible.sync="show.open">
       <customer-details  ref="customerDetails" @refreshList="refreshList"/>
     </el-drawer>
@@ -958,9 +1034,10 @@ import PaginationMore from "../../../components/PaginationMore/index.vue";
 import userDetails from '@/views/store/components/userDetails.vue';
 import {courseList, videoList} from "@/api/course/courseRedPacketLog";
 import Collection from './collection.vue';
+import customerDetail from './customerDetail.vue';
 export default {
   name: "ExternalContact",
-  components:{PaginationMore, mycustomer,customerDetails,SopDialog,selectUser,info,userDetails,collection},
+  components:{PaginationMore, mycustomer,customerDetails,customerDetail,SopDialog,selectUser,info,userDetails,collection},
   data() {
     return {
       projectOptions: [],
@@ -1190,6 +1267,14 @@ export default {
       rules: {
       },
       userId:null,
+      aiAnalyze: {
+        title: "AI 分析",
+        open: false,
+        userId: null,
+        externalUserId: null,
+        corpId: null,
+        customerRow: null,
+      },
     };
   },
   created() {
@@ -1242,6 +1327,41 @@ export default {
 
   },
   methods: {
+    shortenText(text, maxLen = 16) {
+      const str = text == null ? '' : String(text);
+      if (str.length <= maxLen) return str;
+      return str.slice(0, maxLen) + '...';
+    },
+    parseFocusPoints(value) {
+      if (!value) return [];
+      if (Array.isArray(value)) {
+        return value.map(item => String(item).trim()).filter(Boolean);
+      }
+      if (typeof value === 'string') {
+        const raw = value.trim();
+        if (!raw) return [];
+        try {
+          const parsed = JSON.parse(raw);
+          if (Array.isArray(parsed)) {
+            return parsed.map(item => String(item).trim()).filter(Boolean);
+          }
+          if (typeof parsed === 'string') {
+            return [parsed.trim()].filter(Boolean);
+          }
+        } catch (e) {
+          // ignore parse error and use raw fallback
+        }
+        return [raw.replace(/^\[|\]$/g, '').replace(/["']/g, '').trim()].filter(Boolean);
+      }
+      return [String(value).trim()].filter(Boolean);
+    },
+    openAiDrawer(row) {
+      this.aiAnalyze.userId = row.userId;
+      this.aiAnalyze.externalUserId = row.operUserid ;
+      this.aiAnalyze.corpId = row.corpId ;
+      this.aiAnalyze.customerRow = row;
+      this.aiAnalyze.open = true;
+    },
     /** 重粉查看操作 */
     showLog(row) {
       this.log.queryParams.fsUserId = row.fsUserId;

+ 123 - 1
src/views/qw/externalContact/myExternalContact.vue

@@ -384,6 +384,62 @@
           </div>
         </template>
       </el-table-column>
+      <el-table-column label="流失风险" align="center" prop="attritionLevel">
+        <template slot-scope="scope">
+          <el-tag v-if="scope.row.attritionLevel == null" type="info">未分析</el-tag>
+          <el-tag v-if="scope.row.attritionLevel === 0" type="info">未知</el-tag>
+          <el-tag v-else-if="scope.row.attritionLevel === 1" type="success">无风险</el-tag>
+          <el-tag v-else-if="scope.row.attritionLevel === 2" type="info">低风险</el-tag>
+          <el-tag v-else-if="scope.row.attritionLevel === 3" type="warning">中风险</el-tag>
+          <el-tag v-else-if="scope.row.attritionLevel === 4" type="danger">高风险</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="意向度" align="center" prop="intentionDegree">
+        <template slot-scope="scope">
+          <el-tag v-if="scope.row.intentionDegree !== null && scope.row.intentionDegree !== undefined && scope.row.intentionDegree !== ''">
+            {{ scope.row.intentionDegree }}
+          </el-tag>
+          <span v-else>-</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="关注点" align="center" prop="customerFocusJson" width="220">
+        <template slot-scope="scope">
+          <div v-if="parseFocusPoints(scope.row.customerFocusJson).length" style="text-align: left;">
+            <el-tooltip
+              v-for="(item, index) in parseFocusPoints(scope.row.customerFocusJson).slice(0, 2)"
+              :key="index"
+              placement="top"
+              effect="light"
+            >
+              <div slot="content" style="max-width: 420px; word-break: break-word;">
+                {{ item }}
+              </div>
+              <el-tag style="margin: 0 6px 6px 0; max-width: 100%; background: #fff; border: 1px solid #dcdfe6; color: #606266;">
+                {{ shortenText(item, 14) }}
+              </el-tag>
+            </el-tooltip>
+            <el-tooltip
+              v-if="parseFocusPoints(scope.row.customerFocusJson).length > 2"
+              placement="top"
+              effect="light"
+            >
+              <div slot="content" style="max-width: 420px;">
+                <div
+                  v-for="(item, idx) in parseFocusPoints(scope.row.customerFocusJson).slice(2)"
+                  :key="'focus-more-' + idx"
+                  style="margin-bottom: 4px;"
+                >
+                  {{ item }}
+                </div>
+              </div>
+              <el-tag style="margin: 0 6px 6px 0; background: #fff; border: 1px solid #dcdfe6; color: #606266;">
+                +{{ parseFocusPoints(scope.row.customerFocusJson).length - 2 }}
+              </el-tag>
+            </el-tooltip>
+          </div>
+          <span v-else>-</span>
+        </template>
+      </el-table-column>
       <el-table-column label="是否回复" align="center" prop="isReply" width="120px" >
         <template slot-scope="scope">
           <span v-if="scope.row.isReply === 1"><el-tag type="success">已回复</el-tag></span>
@@ -562,6 +618,13 @@
         >
           修改状态
         </el-button>
+        <el-button
+                v-if="scope.row.attritionLevel !== null && scope.row.attritionLevel !== undefined"
+                size="mini"
+                type="text"
+                @click="openAiDrawer(scope.row)"
+                v-hasPermi="['qw:externalContact:analyze:list']"
+              >AI 分析</el-button>
         </template>
       </el-table-column>
     </el-table>
@@ -980,6 +1043,20 @@
 	 :title="show.title" :visible.sync="show.open">
 	  <info  ref="Details" />
 	</el-drawer>
+  <el-drawer
+  size="75%"
+  :title="aiAnalyze.title"
+  :visible.sync="aiAnalyze.open"
+  append-to-body
+>
+  <customer-detail
+    ref="customerAiDetail"
+    :analyze-user-id="aiAnalyze.userId"
+    :analyze-external-user-id="aiAnalyze.externalUserId"
+    :analyze-corp-id="aiAnalyze.corpId"
+    :customer-row="aiAnalyze.customerRow"
+  />
+</el-drawer>
     <el-dialog :title="user.title" :visible.sync="user.open" width="800px" append-to-body>
       <selectUser ref="selectUser" @bindMiniCustomerId="bindMiniCustomerId"></selectUser>
     </el-dialog>
@@ -1043,11 +1120,20 @@ import PaginationMore from "../../../components/PaginationMore/index.vue";
 import Collection from './collection.vue';
 import {courseList, videoList} from "@/api/course/courseRedPacketLog";
 import userDetails from '@/views/store/components/userDetails.vue';
+import customerDetail from './customerDetail.vue';
 export default {
   name: "ExternalContact",
-  components:{PaginationMore, mycustomer,customerDetails,SopDialog,selectUser,info,Collection,userDetails},
+  components:{PaginationMore, mycustomer,customerDetails,SopDialog,selectUser,info,Collection,userDetails,customerDetail},
   data() {
     return {
+      aiAnalyze: {
+        title: "AI 分析",
+        open: false,
+        userId: null,
+        externalUserId: null,
+        corpId: null,
+        customerRow: null,
+      },
       member:{
         title:"客户详情",
         open:false,
@@ -1314,6 +1400,42 @@ export default {
 
   },
   methods: {
+    shortenText(text, maxLen = 16) {
+      const str = text == null ? '' : String(text);
+      if (str.length <= maxLen) return str;
+      return str.slice(0, maxLen) + '...';
+    },
+    parseFocusPoints(value) {
+      if (!value) return [];
+      if (Array.isArray(value)) {
+        return value.map(item => String(item).trim()).filter(Boolean);
+      }
+      if (typeof value === 'string') {
+        const raw = value.trim();
+        if (!raw) return [];
+        try {
+          const parsed = JSON.parse(raw);
+          if (Array.isArray(parsed)) {
+            return parsed.map(item => String(item).trim()).filter(Boolean);
+          }
+          if (typeof parsed === 'string') {
+            return [parsed.trim()].filter(Boolean);
+          }
+        } catch (e) {
+          // ignore parse error and use raw fallback
+        }
+        return [raw.replace(/^\[|\]$/g, '').replace(/["']/g, '').trim()].filter(Boolean);
+      }
+      return [String(value).trim()].filter(Boolean);
+    },
+    openAiDrawer(row) {
+      this.aiAnalyze.userId = row.userId;
+      this.aiAnalyze.externalUserId = row.operUserid ;
+      this.aiAnalyze.corpId = row.corpId ;
+      this.aiAnalyze.customerRow = row;
+      console.log(this.aiAnalyze);
+      this.aiAnalyze.open = true;
+    },
     /** 重粉查看操作 */
     showLog(row) {
       this.log.queryParams.fsUserId = row.fsUserId;

+ 3 - 0
src/views/store/storeAfterSales/list.vue

@@ -99,6 +99,9 @@
               <dict-tag :options="refundTypeOptions" :value="scope.row.refundType"/>
             </template>
       </el-table-column>
+      <el-table-column label="售后原因一级" align="center" prop="reasonValue1" min-width="120" show-overflow-tooltip />
+      <el-table-column label="售后原因二级" align="center" prop="reasonValue2" min-width="120" show-overflow-tooltip />
+      <el-table-column label="售后备注" align="center" prop="auditRemark" min-width="140" show-overflow-tooltip />
       <el-table-column label="状态" align="center" prop="status"  width="130px">
         <template slot-scope="scope">
           <dict-tag :options="salesStatusOptions" :value="scope.row.status"/>

+ 3 - 0
src/views/store/storeAfterSales/myList.vue

@@ -98,6 +98,9 @@
               <dict-tag :options="refundTypeOptions" :value="scope.row.refundType"/>
             </template>
       </el-table-column>
+      <el-table-column label="售后原因一级" align="center" prop="reasonValue1" min-width="120" show-overflow-tooltip />
+      <el-table-column label="售后原因二级" align="center" prop="reasonValue2" min-width="120" show-overflow-tooltip />
+      <el-table-column label="售后备注" align="center" prop="auditRemark" min-width="140" show-overflow-tooltip />
       <el-table-column label="状态" align="center" prop="status"  width="130px">
         <template slot-scope="scope">
           <dict-tag :options="salesStatusOptions" :value="scope.row.status"/>

+ 35 - 13
src/views/watch/deviceInfo/details.vue

@@ -64,7 +64,7 @@
           </div>
         </div>
         <div class="equipment-day">
-          已绑定 <span>400</span> 天, 佩戴 <span>4365</span> 天
+          <!-- 已绑定 <span>400</span> 天, 佩戴 <span>4365</span> 天 -->
         </div>
         <div class="equipment-btn">
           <el-button type="primary" size="small">一键上传</el-button>
@@ -83,7 +83,7 @@
       </el-tabs>
       
       <keep-alive>
-        <component :ref="currentTabComponent" :is="currentTabComponent" :deviceId="deviceId" :basicInfo="basicInfo" :detailsType="detailsType">
+        <component :ref="currentTabComponent" :is="currentTabComponent" :deviceId="deviceId" :basicInfo="basicInfo" :detailsType="detailsType" :userInfo="userInfo">
         </component>
       </keep-alive>
     </el-main>
@@ -115,6 +115,8 @@ import Overview from '@/components/DeviceInfo/Overview.vue';
 import Bloodoxygen from '@/components/DeviceInfo/Bloodoxygen.vue';
 import Uricacid from '@/components/DeviceInfo/Uricacid.vue';
 import Urineketones from '@/components/DeviceInfo/Urineketones.vue';
+import PressureValue from '@/components/DeviceInfo/PressureValue.vue';
+import Report from '@/components/DeviceInfo/Report.vue';
 
 export default {
   name: "DeviceDetails",
@@ -134,7 +136,9 @@ export default {
     Overview,
     Bloodoxygen,
     Uricacid,
-    Urineketones
+    Urineketones,
+    PressureValue,
+    Report
   },
   data() {
     return {
@@ -179,10 +183,10 @@ export default {
           label: '位置',
           name: 'location'
         },
-        {
-          label: '脉搏',
-          name: 'pulse'
-        },
+        // {
+        //   label: '脉搏',
+        //   name: 'pulse'
+        // },
         {
           label: '血糖',
           name: 'bloodsugar'
@@ -191,10 +195,10 @@ export default {
           label: '血压',
           name: 'bloodpressure'
         },
-        {
-          label: '血酮',
-          name: 'urineketones'
-        },
+        // {
+        //   label: '血酮',
+        //   name: 'urineketones'
+        // },
         {
           label: '尿酸',
           name: 'uricacid'
@@ -211,6 +215,14 @@ export default {
         //   label: '血脂',
         //   name: 'bloodfat'
         // },
+        {
+          label: '压力',
+          name: 'pressureValue'
+        },
+        {
+          label: '周报',
+          name: 'report'
+        },
       ]
     };
   },
@@ -238,7 +250,9 @@ export default {
         overview: Overview,
         bloodoxygen: Bloodoxygen,
         uricacid: Uricacid,
-        urineketones: Urineketones
+        urineketones: Urineketones,
+        pressureValue:PressureValue,
+        report:Report
 
 
 
@@ -342,9 +356,12 @@ export default {
      */
     async getUserinfo(deviceId) {
       try {
-        const response = await getUser(deviceId);
+        const userId = this.$route.params.userId;
+        const selectType = this.$route.params.selectType;
+        const response = await getUser({deviceId:deviceId,userId:userId,selectType});
         if (response && response.data) {
           this.userInfo = response.data; // 确保数据被正确赋值
+          this.userInfo.userId = userId;
           // console.log("用户信息:", this.userInfo);
         } else {
           console.warn("用户信息返回为空或格式不正确:", response);
@@ -517,6 +534,11 @@ export default {
   // line-height: 160px;
   padding: 0;
 }
+::v-deep .el-main::-webkit-scrollbar {
+  width: 0;
+  display: none;  /* Chrome, Safari and Opera */
+}
+
 
 body>.el-container {
   margin-bottom: 40px;

+ 160 - 13
src/views/watch/deviceInfo/index.vue

@@ -5,10 +5,17 @@
         <el-input v-model="queryParams.doctorName" placeholder="请输入所属医生" clearable size="small"
           @keyup.enter.native="handleQuery" />
       </el-form-item>
-      <el-form-item label="绑定用户" prop="watchUserName">
+      <el-form-item label="所属员工" prop="companyUserId">
+        <el-select v-model="queryParams.companyUserId" placeholder="请选择所属员工" clearable size="small">
+          <el-option v-for="item in companyUserList" :key="item.userId" :label="item.nickName"
+            :value="item.userId">
+          </el-option>
+        </el-select>
+      </el-form-item>
+      <!-- <el-form-item label="绑定用户" prop="watchUserName">
         <el-input v-model="queryParams.watchUserName" placeholder="请输入绑定用户" clearable size="small"
           @keyup.enter.native="handleQuery" />
-      </el-form-item>
+      </el-form-item> -->
       <el-form-item label="设备编号" prop="deviceNumber">
         <el-input v-model="queryParams.deviceNumber" placeholder="请输入设备编号" clearable size="small"
           @keyup.enter.native="handleQuery" />
@@ -27,6 +34,12 @@
           </el-option>
         </el-select>
       </el-form-item>
+      <el-form-item label="健康状态" prop="isNormal">
+        <el-select v-model="queryParams.isNormal" placeholder="请选择健康状态" clearable size="small">
+          <el-option v-for="item in healthOptions" :key="item.value" :label="item.label" :value="item.value">
+          </el-option>
+        </el-select>
+      </el-form-item>
       <el-form-item>
         <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
         <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
@@ -82,8 +95,29 @@
     <el-table v-loading="loading" :data="deviceInfoList" @selection-change="handleSelectionChange">
       <el-table-column type="selection" width="55" align="center" />
       <el-table-column label="设备id" align="center" prop="deviceId" />
+      <el-table-column label="所属员工" align="center" prop="companyUserName" />
       <el-table-column label="所属医生" align="center" prop="doctorName" />
-      <el-table-column label="绑定用户" align="center" prop="watchUserName" />
+      <el-table-column  label="绑定用户"  align="center" prop="watchUserName">
+        <!-- <template slot-scope="scope"> 
+          <div v-if="scope.row.users && scope.row.users!=null">
+              <el-link :underline="false" @click="handleDetails(scope.row.deviceId, scope.row.users.userId)">
+                {{ scope.row.users.nickName }}
+              </el-link>
+          </div>
+        </template> -->
+        
+      </el-table-column>
+      <el-table-column  label="绑定家人用户"  align="center" prop="familyUserName">
+          <!-- <template slot-scope="scope">
+            <div v-if="scope.row.familyUsers">
+              <div v-for="(item, index) in scope.row.familyUsers" :key="index">
+                <el-link :underline="false" @click="handleDetails(scope.row.deviceId, item.userId)">
+                  {{ item.nickName }}
+                </el-link>
+              </div>
+            </div>
+          </template> -->
+      </el-table-column>
       <el-table-column label="设备编号" align="center" prop="deviceNumber" width="150"/>
       <el-table-column label="电量" align="center" prop="battery" />
       <el-table-column label="信号" align="center" prop="rssi" />
@@ -97,20 +131,71 @@
           >
             最新数据
           </span>
-          <el-tag size="mini" :type="scope.row.isNormal == '异常'?'danger':'primary'">{{scope.row.isNormal}}</el-tag>
+          <el-tag size="mini" :type="scope.row.isNormal == 0?'danger':'primary'">{{ scope.row.isNormal === 1 ? '正常' : '异常' }}</el-tag>
 
         </template>
       </el-table-column>
       <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
         <template slot-scope="scope">
-          <el-button size="mini" type="text" icon="el-icon-view" @click="handleDetails(scope.row.deviceId)"
+          <el-button size="mini" type="text" icon="el-icon-view" @click="handleDetailsAndType(scope.row)"
             v-hasPermi="['watchApi:deviceInfo:query']">查询详情</el-button>
           <el-button size="mini" type="text" icon="el-icon-phone-outline" @click="handleSoSPeople(scope.row.deviceNumber)"
             v-hasPermi="['watchApi:deviceInfo:query']">查看紧急联系人</el-button>
+          <el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)"
+            v-hasPermi="['watchApi:deviceInfo:edit']">修改</el-button>  
         </template>
       </el-table-column>
     </el-table>
 
+    <!-- 添加或修改设备信息对话框 -->
+    <el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
+      <el-form ref="form" :model="form" :rules="rules" label-width="80px">
+        
+        <el-form-item label="所属员工" prop="companyUserId">
+          <template #label>
+            <span class="required-label">所属员工</span>
+          </template>
+          <el-select v-model="form.companyUserId" placeholder="请选择所属员工" clearable size="small">
+            <el-option v-for="item in companyUserList" :key="item.userId" :label="item.nickName"
+              :value="item.userId">
+            </el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item label="所属医生" prop="doctorId">
+          <template #label>
+            <span class="required-label">所属医生</span>
+          </template>
+          <el-select v-model="form.doctorId" placeholder="请选择所属医生" clearable size="small">
+            <el-option v-for="item in doctorList" :key="item.doctorId" :label="item.doctorName"
+              :value="item.doctorId">
+            </el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item label="设备编号" prop="deviceNumber">
+          <el-input v-model="form.deviceNumber" placeholder="请输入设备编号" disabled/>
+        </el-form-item>
+        <el-form-item label="电量" prop="battery">
+          <el-input v-model="form.battery" placeholder="请输入电量" disabled/>
+        </el-form-item>
+        <el-form-item label="信号" prop="rssi">
+          <el-input v-model="form.rssi" placeholder="请输入信号" disabled/>
+        </el-form-item>
+        <el-form-item label="设备状态">
+          <el-radio-group v-model="form.status">
+            <el-radio :label="0">OFFLINE</el-radio>
+            <el-radio :label="1">ONLINE</el-radio>
+            <el-radio :label="2">UNACTIVE</el-radio>
+            <el-radio :label="3">DISABLE</el-radio>
+            <el-radio :label="4">NOT EXIST</el-radio>
+          </el-radio-group>
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="submitForm">确 定</el-button>
+        <el-button @click="cancel">取 消</el-button>
+      </div>
+    </el-dialog>
+
     <!-- 弹窗展示紧急联系人列表 -->
     <el-dialog :visible.sync="sosVisible" title="紧急联系人" width="50%">
         <el-table v-loading="sosLoading" :data="sosPeopleList">
@@ -169,15 +254,15 @@
 
 <script>
 import { listDeviceInfo, getDeviceInfo, delDeviceInfo, addDeviceInfo, updateDeviceInfo, exportDeviceInfo, queryAlarm, setStatus, queryPageAlarm,queryLastHealthData,querySos} from "@/api/watch/deviceInfo";
-import { getAllUserlist } from "@/api/company/companyUser";
+import { getSelectUserList } from "@/api/company/companyUser";
 import { messageSend } from "@/api/watch/deviceInfoSet.js";
+import { listDoctor } from "@/api/doctor/doctor";
 import setUpUserInfoDialog from "@/components/DeviceInfo/SettingDialog/setUpUserInfoDialog.vue";
 import SetInfoDialog from "@/components/DeviceInfo/SettingDialog/SetInfoDialog.vue";
 import Phonebook from "@/components/DeviceInfo/SettingDialog/Phonebook.vue";
 import Clockalarm from "@/components/DeviceInfo/SettingDialog/Clockalarm.vue";
 import Sedentary from "@/components/DeviceInfo/SettingDialog/Sedentary.vue";
-// import {getInfo} from "@/api/login"
-
+import {watchDownload} from "@/api/watch/common";
 export default {
   name: "DeviceInfo",
   components: {
@@ -273,6 +358,13 @@ export default {
           { max: 240, message: '详情最多240个字节', trigger: 'blur' }
         ]
       },
+      healthOptions: [{
+          value: 0,
+          label: '异常'
+        }, {
+          value: 1,
+          label: '正常'
+        }],
       options: [{
         value: '0',
         label: 'OFFLINE'
@@ -311,6 +403,8 @@ export default {
   },
   created() {
     this.getList();
+    this.getCompanyUserList();
+    this.getDoctorList();
   },
   computed: {
     // 计算属性来控制按钮的disabled状态
@@ -319,9 +413,51 @@ export default {
     }
   },
   methods: {
+    getDoctorList(){
+      listDoctor({ doctorType:1,isAudit:1 }).then(response => {
+        this.doctorList = response.rows;
+      });
+    },
+    getCompanyUserList(){
+      getSelectUserList().then(response => {
+          this.companyUserList = response.rows;
+        }).catch(error => {
+          console.error("获取员工列表失败:", error);
+        });
+    },
+    /** 修改按钮操作 */
+    handleUpdate(row) {
+      this.reset();
+      const deviceId = row.deviceId || this.ids
+      getDeviceInfo(deviceId).then(response => {
+        this.form = response.data;
+        this.open = true;
+        this.title = "修改设备信息";
+      });
+    },
+    /** 提交按钮 */
+    submitForm() {
+      this.$refs["form"].validate(valid => {
+        if (valid) {
+          if (this.form.deviceId != null) {
+            console.log("===============================" + JSON.stringify(this.form, null, 2))
+            updateDeviceInfo(this.form).then(response => {
+              this.msgSuccess("修改成功");
+              this.open = false;
+              this.getList();
+            });
+          } else {
+            addDeviceInfo(this.form).then(response => {
+              this.msgSuccess("新增成功");
+              this.open = false;
+              this.getList();
+            });
+          }
+        }
+      });
+    },
     rowClassName({row}) {
-      if (row.status && row.status.includes('偏')) {
-        // console.log("=============================" + JSON.stringify(row.status, null, 2))
+      if (row.status && (!row.status.includes('正常') && !row.status.includes('得分') && !row.status.includes('无结果') && !row.status.includes('窦性心率'))) {
         return 'text-red';
         // return true
       }
@@ -403,9 +539,18 @@ export default {
 
       }
     },
+    handleDetailsAndType(row){
+      if(row.userId != null){
+        this.handleDetails(row.deviceId,row.userId)
+      } else if(row.familyUserId !=null){
+        this.handleDetails(row.deviceId,row.familyUserId)
+      } else {
+        this.handleDetails(row.deviceId,null);
+      }
+    },
     /** 处理查询详情按钮点击事件 */
-    handleDetails(deviceId) {
-      this.$router.push({ name: 'details', params: { deviceId } });
+    handleDetails(deviceId,userId,type) {
+      this.$router.push({ name: 'details', params: { deviceId:deviceId,userId:userId,selectType:type } });
     },
 
     /** 查询设备信息列表 */
@@ -501,7 +646,9 @@ export default {
         this.exportLoading = true;
         return exportDeviceInfo(queryParams);
       }).then(response => {
-        this.download(response.msg);
+        debugger;
+        // this.download(response.msg);
+        watchDownload(response.msg);
         this.exportLoading = false;
       }).catch(() => { });
     },

+ 46 - 14
src/views/watch/deviceInfo/my.vue

@@ -27,6 +27,12 @@
             </el-option>
           </el-select>
         </el-form-item>
+        <el-form-item label="健康状态" prop="isNormal">
+        <el-select v-model="queryParams.isNormal" placeholder="请选择健康状态" clearable size="small">
+          <el-option v-for="item in healthOptions" :key="item.value" :label="item.label" :value="item.value">
+          </el-option>
+        </el-select>
+      </el-form-item>
         <el-form-item>
           <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
           <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
@@ -83,15 +89,26 @@
         <el-table-column type="selection" width="55" align="center" />
         <el-table-column label="设备id" align="center" prop="deviceId" />
         <el-table-column label="所属医生" align="center" prop="doctorName" />
-        <el-table-column  label="绑定用户"  align="center">
-          <template slot-scope="scope">
-              <el-link :underline="false" prop="item.userId" v-for="(item, index) in scope.row.users"  v-if="scope.row.users"  @click="handleDetails(scope.row.deviceId,item.userId,0)">{{item.nickName}}</el-link>
-          </template>
+        <el-table-column  label="绑定用户"  align="center" prop="watchUserName">
+          <!-- <template slot-scope="scope"> 
+          <div v-if="scope.row.users && scope.row.users!=null">
+              <el-link :underline="false" @click="handleDetails(scope.row.deviceId, scope.row.users.userId)">
+                {{ scope.row.users.nickName }}
+              </el-link>
+          </div>
+        </template> -->
+          
         </el-table-column>
-        <el-table-column  label="绑定家人用户"  align="center">
-            <template slot-scope="scope">
-                <el-link :underline="false" prop="item.userId" v-for="(item, index) in scope.row.familyUsers"  v-if="scope.row.familyUsers"  @click="handleDetails(scope.row.deviceId,item.userId,1)">{{item.nickName}}</el-link>
-            </template>
+        <el-table-column  label="绑定家人用户"  align="center" prop="familyUserName">
+            <!-- <template slot-scope="scope">
+              <div v-if="scope.row.familyUsers">
+                <div v-for="(item, index) in scope.row.familyUsers" :key="index">
+                  <el-link :underline="false" @click="handleDetails(scope.row.deviceId, item.userId)">
+                    {{ item.nickName }}
+                  </el-link>
+                </div>
+              </div>
+            </template> -->
         </el-table-column>
         <el-table-column label="设备编号" align="center" prop="deviceNumber" width="150"/>
         <el-table-column label="电量" align="center" prop="battery" />
@@ -106,13 +123,13 @@
             >
               最新数据
             </span>
-            <el-tag size="mini" :type="scope.row.isNormal == '异常'?'danger':'primary'">{{scope.row.isNormal}}</el-tag>
+            <el-tag size="mini" :type="scope.row.isNormal == 0?'danger':'primary'">{{ scope.row.isNormal === 1 ? '正常' : '异常' }}</el-tag>
   
           </template>
         </el-table-column>
         <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
           <template slot-scope="scope">
-            <el-button size="mini" type="text" icon="el-icon-view" @click="handleDetails(scope.row.deviceId,null,0)"
+            <el-button size="mini" type="text" icon="el-icon-view" @click="handleDetailsAndType(scope.row)"
               v-hasPermi="['watchApi:deviceInfo:query']">查询详情</el-button>
             <el-button size="mini" type="text" icon="el-icon-phone-outline" @click="handleSoSPeople(scope.row.deviceNumber)"
               v-hasPermi="['watchApi:deviceInfo:query']">查看紧急联系人</el-button>
@@ -186,7 +203,7 @@
   import Clockalarm from "@/components/DeviceInfo/SettingDialog/Clockalarm.vue";
   import Sedentary from "@/components/DeviceInfo/SettingDialog/Sedentary.vue";
   // import {getInfo} from "@/api/login"
-  
+  import {watchDownload} from "@/api/watch/common";
   export default {
     name: "my",
     components: {
@@ -282,6 +299,13 @@
             { max: 240, message: '详情最多240个字节', trigger: 'blur' }
           ]
         },
+        healthOptions: [{
+          value: 0,
+          label: '异常'
+        }, {
+          value: 1,
+          label: '正常'
+        }],
         options: [{
           value: '0',
           label: 'OFFLINE'
@@ -329,8 +353,7 @@
     },
     methods: {
       rowClassName({row}) {
-        if (row.status && row.status.includes('偏')) {
-          // console.log("=============================" + JSON.stringify(row.status, null, 2))
+        if (row.status && (!row.status.includes('正常') && !row.status.includes('得分') && !row.status.includes('无结果') && !row.status.includes('窦性心率'))) {
           return 'text-red';
           // return true
         }
@@ -412,6 +435,15 @@
   
         }
       },
+      handleDetailsAndType(row){
+        if(row.userId != null){
+          this.handleDetails(row.deviceId,row.userId,0)
+        } else if(row.familyUserId !=null ){
+          this.handleDetails(row.deviceId,row.familyUserId,1)
+        } else {
+          this.handleDetails(row.deviceId,null,0);
+        }
+      },
       /** 处理查询详情按钮点击事件 */
       handleDetails(deviceId,userId,type) {
         this.$router.push({ name: 'details', params: { deviceId:deviceId,userId:userId,selectType:type } });
@@ -510,7 +542,7 @@
           this.exportLoading = true;
           return exportMy(queryParams);
         }).then(response => {
-          this.download(response.msg);
+          watchDownload(response.msg);
           this.exportLoading = false;
         }).catch(() => { });
       },