Explorar el Código

1、用户授权上传使用树形展示

qmj hace 4 meses
padre
commit
d4b584b279
Se han modificado 5 ficheros con 579 adiciones y 63 borrados
  1. 119 15
      src/views/agent/call.vue
  2. 115 12
      src/views/agent/image.vue
  3. 112 12
      src/views/agent/position.vue
  4. 119 13
      src/views/agent/sms.vue
  5. 114 11
      src/views/agent/userContact.vue

+ 119 - 15
src/views/agent/call.vue

@@ -22,23 +22,44 @@
         <el-button icon="Refresh" @click="resetQuery">重設</el-button>
       </el-form-item>
     </el-form>
-    <el-table v-loading="loading" :data="callList">
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-button 
+          type="info"
+          plain
+          icon="Sort"
+          @click="toggleExpandAll"
+        >展開/折疊</el-button>
+      </el-col>
+    </el-row>
+    <el-table 
+      v-if="refreshTable"
+      v-loading="loading" 
+      :data="treeData" 
+      :tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
+      row-key="id"
+      :default-expand-all="isExpandAll"
+    >
       <el-table-column type="selection" width="55" align="center" />
-      <el-table-column label="id" align="center" prop="id" />
-      <el-table-column label="用戶ID" align="center" prop="userId" />
-      <el-table-column label="所屬用戶" align="center" prop="userName" />
-      <el-table-column label="電話號碼" align="center" prop="phoneNumber" />
-      <el-table-column label="通話類型" align="center" prop="callType">
+      <el-table-column label="所屬用戶" prop="userName" width="200" show-overflow-tooltip>
         <template #default="{ row }">
-          <span>{{ callTypeText(row.callType) }}</span>
+          <span v-if="row.isParent" class="parent-user-name">{{ row.userName || row.userId || '-' }}</span>
+          <span v-else>{{ row.userName || '-' }}</span>
         </template>
       </el-table-column>
-      <el-table-column label="通話開始時間" align="center" prop="startTime" />
-      <el-table-column label="通話時長" align="center" prop="duration" />
-      <el-table-column label="通訊錄名稱" align="center" prop="contactName" />
+      <el-table-column label="用戶ID" prop="userId" width="120" align="center"/>
+      <el-table-column label="電話號碼" prop="phoneNumber" align="center" />
+      <el-table-column label="通話類型" prop="callType" align="center">
+        <template #default="{ row }">
+          <span v-if="!row.isParent">{{ callTypeText(row.callType) }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="通話開始時間" prop="startTime" align="center" />
+      <el-table-column label="通話時長" prop="duration" align="center" />
+      <el-table-column label="通訊錄名稱" prop="contactName" align="center" />
       <el-table-column label="操作" align="center" width="140" class-name="small-padding fixed-width">
         <template #default="{ row }">
-          <el-tooltip content="查看" placement="top">
+          <el-tooltip content="查看" placement="top" v-if="!row.isParent">
             <el-button link type="primary" icon="View" @click.stop="handleView(row)"></el-button>
           </el-tooltip>
         </template>
@@ -82,6 +103,7 @@ const { proxy } = getCurrentInstance()
 const { sys_show_hide, sys_normal_disable } = proxy.useDict("sys_show_hide", "sys_normal_disable")
 
 const callList = ref([])
+const treeData = ref([])
 const total = ref(0)
 const title = ref("")
 const open = ref(false)
@@ -89,12 +111,50 @@ const form = ref({})
 const formRef = ref()
 const loading = ref(true)
 const queryFormRef = ref()
+const isExpandAll = ref(false)
+const refreshTable = ref(true)
 const queryParams = reactive({
   pageNum: 1,
   pageSize: 10,
   username: null,
   userId:null,
 })
+
+// 将扁平数据转换为树形结构
+const convertToTree = (data) => {
+  if (!data || !Array.isArray(data)) {
+    return []
+  }
+  const treeMap = new Map()
+  const result = []
+  
+  // 先按userName和userId分组
+  data.forEach(item => {
+    const key = `${item.userName || ''}_${item.userId || ''}`
+    if (!treeMap.has(key)) {
+      treeMap.set(key, {
+        id: `parent_${key}`,
+        userName: item.userName || item.userId || '未知用户',
+        userId: item.userId,
+        isParent: true,
+        children: []
+      })
+    }
+    // 添加子节点
+    treeMap.get(key).children.push({
+      ...item,
+      id: item.id || `child_${key}_${treeMap.get(key).children.length}`
+    })
+  })
+  
+  // 转换为数组
+  treeMap.forEach(value => {
+    result.push(value)
+  })
+  
+  return result
+}
+
 onMounted(()=>{
   getList()
 })
@@ -154,8 +214,39 @@ const getList = () => {
     userId:queryParams.userId,
   }
   getCallPageList(params).then(response => {
-    callList.value = response.data.records
-    total.value = response.data.total
+    // 处理不同的API返回格式
+    // response 可能是 { code: 200, msg: "操作成功", data: [...] } 或直接是数组
+    let data = []
+    let totalCount = 0
+    
+    if (Array.isArray(response)) {
+      // 如果response直接是数组
+      data = response
+      totalCount = data.length
+    } else if (Array.isArray(response.data)) {
+      // 如果data是数组
+      data = response.data
+      totalCount = data.length
+    } else if (response.data && Array.isArray(response.data.records)) {
+      // 如果是分页格式 { records: [], total: ... }
+      data = response.data.records
+      totalCount = response.data.total || data.length
+    }
+    
+    // 如果数据已经是树形结构(包含isParent和children),直接使用
+    if (data.length > 0 && (data[0].isParent !== undefined || data[0].children !== undefined)) {
+      treeData.value = data
+      callList.value = data.flatMap(item => item.children || [])
+      total.value = totalCount
+    } else {
+      // 否则转换为树形结构
+      callList.value = data
+      treeData.value = convertToTree(data)
+      total.value = totalCount
+    }
+    loading.value = false
+  }).catch((error) => {
+    console.error('获取通话列表失败:', error)
     loading.value = false
   })
 }
@@ -165,7 +256,7 @@ const handleView = (row) => {
   reset()
   form.value = {...row}
   open.value = true
-  title.value = "查看圖片資訊"
+  title.value = "查看通話資訊"
   nextTick(() => {
     if (formRef.value) formRef.value.clearValidate && formRef.value.clearValidate()
   })
@@ -175,7 +266,20 @@ const cancel = () => {
   open.value = false
   reset()
 }
+
+/** 展开/折叠操作 */
+const toggleExpandAll = () => {
+  refreshTable.value = false
+  isExpandAll.value = !isExpandAll.value
+  nextTick(() => {
+    refreshTable.value = true
+  })
+}
 </script>
 <style scoped lang="scss">
-
+.parent-user-name {
+  color: #409EFF;
+  font-weight: 500;
+  font-size: 14px;
+}
 </style>

+ 115 - 12
src/views/agent/image.vue

@@ -22,21 +22,42 @@
         <el-button icon="Refresh" @click="resetQuery">重設</el-button>
       </el-form-item>
     </el-form>
-    <el-table v-loading="loading" :data="imageList">
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-button 
+          type="info"
+          plain
+          icon="Sort"
+          @click="toggleExpandAll"
+        >展開/折疊</el-button>
+      </el-col>
+    </el-row>
+    <el-table 
+      v-if="refreshTable"
+      v-loading="loading" 
+      :data="treeData" 
+      :tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
+      row-key="id"
+      :default-expand-all="isExpandAll"
+    >
       <el-table-column type="selection" width="55" align="center" />
-      <el-table-column label="id" align="center" prop="id" />
-      <el-table-column label="用戶id" align="center" prop="userId" />
-      <el-table-column label="所屬用戶" align="center" prop="userName" />
-      <el-table-column label="圖片" align="center" prop="url" width="120">
+      <el-table-column label="所屬用戶" prop="userName" width="200" show-overflow-tooltip>
         <template #default="{ row }">
-          <image-preview v-if="row.url" :src="getImageUrl(row.url)" :width="80" :height="80"/>
-          <span v-else style="color: #909399;">-</span>
+          <span v-if="row.isParent" class="parent-user-name">{{ row.userName || row.userId || '-' }}</span>
+          <span v-else>{{ row.userName || '-' }}</span>
         </template>
       </el-table-column>
-      <el-table-column label="圖片時間" align="center" prop="createDate" />
+      <el-table-column label="用戶id" prop="userId" width="120" align="center"/>
+      <el-table-column label="圖片" prop="url" width="120" align="center">
+        <template #default="{ row }">
+          <image-preview v-if="!row.isParent && row.url" :src="getImageUrl(row.url)" :width="80" :height="80"/>
+          <span v-else-if="!row.isParent" style="color: #909399;">-</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="圖片時間" prop="createDate" align="center" />
       <el-table-column label="操作" align="center" width="140" class-name="small-padding fixed-width">
         <template #default="{ row }">
-          <el-tooltip content="查看" placement="top">
+          <el-tooltip content="查看" placement="top" v-if="!row.isParent">
             <el-button link type="primary" icon="View" @click.stop="handleView(row)"></el-button>
           </el-tooltip>
         </template>
@@ -77,6 +98,7 @@ import { isExternal } from '@/utils/validate'
 const filePath = import.meta.env.VITE_APP_FILE_PATH || ''
 
 const imageList = ref([])
+const treeData = ref([])
 const total = ref(0)
 const title = ref("")
 const open = ref(false)
@@ -84,6 +106,8 @@ const form = ref({})
 const formRef = ref()
 const loading = ref(true)
 const queryFormRef = ref()
+const isExpandAll = ref(false)
+const refreshTable = ref(true)
 const queryParams = reactive({
   pageNum: 1,
   pageSize: 10,
@@ -91,6 +115,41 @@ const queryParams = reactive({
   userId:null,
 })
 
+// 将扁平数据转换为树形结构
+const convertToTree = (data) => {
+  if (!data || !Array.isArray(data)) {
+    return []
+  }
+  const treeMap = new Map()
+  const result = []
+  
+  // 先按userName和userId分组
+  data.forEach(item => {
+    const key = `${item.userName || ''}_${item.userId || ''}`
+    if (!treeMap.has(key)) {
+      treeMap.set(key, {
+        id: `parent_${key}`,
+        userName: item.userName || item.userId || '未知用户',
+        userId: item.userId,
+        isParent: true,
+        children: []
+      })
+    }
+    // 添加子节点
+    treeMap.get(key).children.push({
+      ...item,
+      id: item.id || `child_${key}_${treeMap.get(key).children.length}`
+    })
+  })
+  
+  // 转换为数组
+  treeMap.forEach(value => {
+    result.push(value)
+  })
+  
+  return result
+}
+
 // 获取完整的图片URL
 const getImageUrl = (url) => {
   if (!url) return ''
@@ -150,8 +209,39 @@ const getList = () => {
     userId: queryParams.userId,
   }
   getImagePageList(params).then(response => {
-    imageList.value = response.data.records
-    total.value = response.data.total
+    // 处理不同的API返回格式
+    // response 可能是 { code: 200, msg: "操作成功", data: [...] } 或直接是数组
+    let data = []
+    let totalCount = 0
+    
+    if (Array.isArray(response)) {
+      // 如果response直接是数组
+      data = response
+      totalCount = data.length
+    } else if (Array.isArray(response.data)) {
+      // 如果data是数组
+      data = response.data
+      totalCount = data.length
+    } else if (response.data && Array.isArray(response.data.records)) {
+      // 如果是分页格式 { records: [], total: ... }
+      data = response.data.records
+      totalCount = response.data.total || data.length
+    }
+    
+    // 如果数据已经是树形结构(包含isParent和children),直接使用
+    if (data.length > 0 && (data[0].isParent !== undefined || data[0].children !== undefined)) {
+      treeData.value = data
+      imageList.value = data.flatMap(item => item.children || [])
+      total.value = totalCount
+    } else {
+      // 否则转换为树形结构
+      imageList.value = data
+      treeData.value = convertToTree(data)
+      total.value = totalCount
+    }
+    loading.value = false
+  }).catch((error) => {
+    console.error('获取图片列表失败:', error)
     loading.value = false
   })
 }
@@ -171,7 +261,20 @@ const cancel = () => {
   open.value = false
   reset()
 }
+
+/** 展开/折叠操作 */
+const toggleExpandAll = () => {
+  refreshTable.value = false
+  isExpandAll.value = !isExpandAll.value
+  nextTick(() => {
+    refreshTable.value = true
+  })
+}
 </script>
 <style scoped lang="scss">
-
+.parent-user-name {
+  color: #409EFF;
+  font-weight: 500;
+  font-size: 14px;
+}
 </style>

+ 112 - 12
src/views/agent/position.vue

@@ -22,16 +22,37 @@
         <el-button icon="Refresh" @click="resetQuery">重設</el-button>
       </el-form-item>
     </el-form>
-    <el-table v-loading="loading" :data="positionList">
-      <el-table-column label="id" align="center" prop="id"/>
-      <el-table-column label="用戶id" align="center" prop="userId"/>
-      <el-table-column label="所屬用戶" align="center" prop="userName"/>
-      <el-table-column label="經度" align="center" prop="longitude"/>
-      <el-table-column label="緯度" align="center" prop="latitude"/>
-      <el-table-column label="備註" align="center" prop="notes"/>
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-button 
+          type="info"
+          plain
+          icon="Sort"
+          @click="toggleExpandAll"
+        >展開/折疊</el-button>
+      </el-col>
+    </el-row>
+    <el-table 
+      v-if="refreshTable"
+      v-loading="loading" 
+      :data="treeData" 
+      :tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
+      row-key="id"
+      :default-expand-all="isExpandAll"
+    >
+      <el-table-column label="所屬用戶" prop="userName" width="200" show-overflow-tooltip>
+        <template #default="{ row }">
+          <span v-if="row.isParent" class="parent-user-name">{{ row.userName || row.userId || '-' }}</span>
+          <span v-else>{{ row.userName || '-' }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="用戶id" prop="userId" width="120" align="center"/>
+      <el-table-column label="經度" prop="longitude" align="center"/>
+      <el-table-column label="緯度" prop="latitude" align="center"/>
+      <el-table-column label="備註" prop="notes" align="center"/>
       <el-table-column label="操作" align="center" width="140" class-name="small-padding fixed-width">
         <template #default="{ row }">
-          <el-tooltip content="查看" placement="top">
+          <el-tooltip content="查看" placement="top" v-if="!row.isParent">
             <el-button link type="primary" icon="View" @click.stop="handleView(row)"></el-button>
           </el-tooltip>
         </template>
@@ -70,6 +91,7 @@ import {ref, reactive, getCurrentInstance, onMounted, nextTick} from 'vue'
 import {getPositionPageList} from '@/api/system/position.js'
 
 const positionList = ref([])
+const treeData = ref([])
 const total = ref(0)
 const title = ref("")
 const open = ref(false)
@@ -77,6 +99,8 @@ const form = ref({})
 const formRef = ref()
 const loading = ref(true)
 const queryFormRef = ref()
+const isExpandAll = ref(false)
+const refreshTable = ref(true)
 const queryParams = reactive({
   pageNum: 1,
   pageSize: 10,
@@ -84,6 +108,40 @@ const queryParams = reactive({
   userId:null,
 })
 
+// 将扁平数据转换为树形结构
+const convertToTree = (data) => {
+  if (!data || !Array.isArray(data)) {
+    return []
+  }
+  const treeMap = new Map()
+  const result = []
+  
+  // 先按userName和userId分组
+  data.forEach(item => {
+    const key = `${item.userName || ''}_${item.userId || ''}`
+    if (!treeMap.has(key)) {
+      treeMap.set(key, {
+        id: `parent_${key}`,
+        userName: item.userName || item.userId || '未知用户',
+        userId: item.userId,
+        isParent: true,
+        children: []
+      })
+    }
+    // 添加子节点
+    treeMap.get(key).children.push({
+      ...item,
+      id: item.id || `child_${key}_${treeMap.get(key).children.length}`
+    })
+  })
+  
+  // 转换为数组
+  treeMap.forEach(value => {
+    result.push(value)
+  })
+  
+  return result
+}
 
 onMounted(()=>{
   getList()
@@ -122,8 +180,39 @@ const getList = () => {
     userId: queryParams.userId,
   }
   getPositionPageList(params).then(response => {
-    positionList.value = response.data.records
-    total.value = response.data.total
+    // 处理不同的API返回格式
+    // response 可能是 { code: 200, msg: "操作成功", data: [...] } 或直接是数组
+    let data = []
+    let totalCount = 0
+    
+    if (Array.isArray(response)) {
+      // 如果response直接是数组
+      data = response
+      totalCount = data.length
+    } else if (Array.isArray(response.data)) {
+      // 如果data是数组
+      data = response.data
+      totalCount = data.length
+    } else if (response.data && Array.isArray(response.data.records)) {
+      // 如果是分页格式 { records: [], total: ... }
+      data = response.data.records
+      totalCount = response.data.total || data.length
+    }
+    
+    // 如果数据已经是树形结构(包含isParent和children),直接使用
+    if (data.length > 0 && (data[0].isParent !== undefined || data[0].children !== undefined)) {
+      treeData.value = data
+      positionList.value = data.flatMap(item => item.children || [])
+      total.value = totalCount
+    } else {
+      // 否则转换为树形结构
+      positionList.value = data
+      treeData.value = convertToTree(data)
+      total.value = totalCount
+    }
+    loading.value = false
+  }).catch((error) => {
+    console.error('获取定位列表失败:', error)
     loading.value = false
   })
 }
@@ -144,8 +233,19 @@ const cancel = () => {
   reset()
 }
 
-
+/** 展开/折叠操作 */
+const toggleExpandAll = () => {
+  refreshTable.value = false
+  isExpandAll.value = !isExpandAll.value
+  nextTick(() => {
+    refreshTable.value = true
+  })
+}
 </script>
 <style scoped lang="scss">
-
+.parent-user-name {
+  color: #409EFF;
+  font-weight: 500;
+  font-size: 14px;
+}
 </style>

+ 119 - 13
src/views/agent/sms.vue

@@ -22,26 +22,47 @@
         <el-button icon="Refresh" @click="resetQuery">重設</el-button>
       </el-form-item>
     </el-form>
-    <el-table v-loading="loading" :data="smsList">
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-button 
+          type="info"
+          plain
+          icon="Sort"
+          @click="toggleExpandAll"
+        >展開/折疊</el-button>
+      </el-col>
+    </el-row>
+    <el-table 
+      v-if="refreshTable"
+      v-loading="loading" 
+      :data="treeData" 
+      :tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
+      row-key="id"
+      :default-expand-all="isExpandAll"
+    >
       <el-table-column type="selection" width="55" align="center" />
-      <el-table-column label="id" align="center" prop="id" />
-      <el-table-column label="用戶id" align="center" prop="userId" />
-      <el-table-column label="所屬用戶" align="center" prop="userName" />
-      <el-table-column label="簡訊類型" align="center" prop="smsType">
+      <el-table-column label="所屬用戶" prop="userName" width="200" show-overflow-tooltip>
         <template #default="{ row }">
-          <span>{{ smsTypeText(row.smsType) }}</span>
+          <span v-if="row.isParent" class="parent-user-name">{{ row.userName || row.userId || '-' }}</span>
+          <span v-else>{{ row.userName || '-' }}</span>
         </template>
       </el-table-column>
-      <el-table-column label="對方號碼" align="center" prop="phoneNumber" />
-      <el-table-column label="簡訊內容" align="center" prop="content" width="200">
+      <el-table-column label="用戶id" prop="userId" width="120" align="center"/>
+      <el-table-column label="簡訊類型" prop="smsType" align="center">
         <template #default="{ row }">
-          <div class="content-cell-text">{{ row.content }}</div>
+          <span v-if="!row.isParent">{{ smsTypeText(row.smsType) }}</span>
         </template>
       </el-table-column>
-      <el-table-column label="簡訊時間" align="center" prop="smsTime" />
+      <el-table-column label="對方號碼" prop="phoneNumber" align="center" />
+      <el-table-column label="簡訊內容" prop="content" width="200" align="center">
+        <template #default="{ row }">
+          <div v-if="!row.isParent" class="content-cell-text">{{ row.content }}</div>
+        </template>
+      </el-table-column>
+      <el-table-column label="簡訊時間" prop="smsTime" align="center" />
       <el-table-column label="操作" align="center" width="140" class-name="small-padding fixed-width">
         <template #default="{ row }">
-          <el-tooltip content="查看" placement="top">
+          <el-tooltip content="查看" placement="top" v-if="!row.isParent">
             <el-button link type="primary" icon="View" @click.stop="handleView(row)"></el-button>
           </el-tooltip>
         </template>
@@ -82,6 +103,7 @@ const { proxy } = getCurrentInstance()
 const { sys_show_hide, sys_normal_disable } = proxy.useDict("sys_show_hide", "sys_normal_disable")
 
 const smsList = ref([])
+const treeData = ref([])
 const total = ref(0)
 const title = ref("")
 const open = ref(false)
@@ -89,12 +111,50 @@ const form = ref({})
 const formRef = ref()
 const loading = ref(true)
 const queryFormRef = ref()
+const isExpandAll = ref(false)
+const refreshTable = ref(true)
 const queryParams = reactive({
   pageNum: 1,
   pageSize: 10,
   username: null,
   userId:null,
 })
+
+// 将扁平数据转换为树形结构
+const convertToTree = (data) => {
+  if (!data || !Array.isArray(data)) {
+    return []
+  }
+  const treeMap = new Map()
+  const result = []
+  
+  // 先按userName和userId分组
+  data.forEach(item => {
+    const key = `${item.userName || ''}_${item.userId || ''}`
+    if (!treeMap.has(key)) {
+      treeMap.set(key, {
+        id: `parent_${key}`,
+        userName: item.userName || item.userId || '未知用户',
+        userId: item.userId,
+        isParent: true,
+        children: []
+      })
+    }
+    // 添加子节点
+    treeMap.get(key).children.push({
+      ...item,
+      id: item.id || `child_${key}_${treeMap.get(key).children.length}`
+    })
+  })
+  
+  // 转换为数组
+  treeMap.forEach(value => {
+    result.push(value)
+  })
+  
+  return result
+}
+
 onMounted(()=>{
   getList()
 })
@@ -147,8 +207,39 @@ const getList = () => {
     userId: queryParams.userId,
   }
   getSmsPageList(params).then(response => {
-    smsList.value = response.data.records
-    total.value = response.data.total
+    // 处理不同的API返回格式
+    // response 可能是 { code: 200, msg: "操作成功", data: [...] } 或直接是数组
+    let data = []
+    let totalCount = 0
+    
+    if (Array.isArray(response)) {
+      // 如果response直接是数组
+      data = response
+      totalCount = data.length
+    } else if (Array.isArray(response.data)) {
+      // 如果data是数组
+      data = response.data
+      totalCount = data.length
+    } else if (response.data && Array.isArray(response.data.records)) {
+      // 如果是分页格式 { records: [], total: ... }
+      data = response.data.records
+      totalCount = response.data.total || data.length
+    }
+    
+    // 如果数据已经是树形结构(包含isParent和children),直接使用
+    if (data.length > 0 && (data[0].isParent !== undefined || data[0].children !== undefined)) {
+      treeData.value = data
+      smsList.value = data.flatMap(item => item.children || [])
+      total.value = totalCount
+    } else {
+      // 否则转换为树形结构
+      smsList.value = data
+      treeData.value = convertToTree(data)
+      total.value = totalCount
+    }
+    loading.value = false
+  }).catch((error) => {
+    console.error('获取短信列表失败:', error)
     loading.value = false
   })
 }
@@ -168,6 +259,15 @@ const cancel = () => {
   open.value = false
   reset()
 }
+
+/** 展开/折叠操作 */
+const toggleExpandAll = () => {
+  refreshTable.value = false
+  isExpandAll.value = !isExpandAll.value
+  nextTick(() => {
+    refreshTable.value = true
+  })
+}
 </script>
 <style scoped lang="scss">
 .content-cell {
@@ -188,6 +288,12 @@ const cancel = () => {
   word-break: break-all;
   width: 100%;
 }
+
+.parent-user-name {
+  color: #409EFF;
+  font-weight: 500;
+  font-size: 14px;
+}
 </style>
 
 <style lang="scss">

+ 114 - 11
src/views/agent/userContact.vue

@@ -30,16 +30,37 @@
         <el-button icon="Refresh" @click="resetQuery">重設</el-button>
       </el-form-item>
     </el-form>
-    <el-table v-loading="loading" :data="contactList">
-      <el-table-column label="id" align="center" prop="id"/>
-      <el-table-column label="用戶id" align="center" prop="userId"/>
-      <el-table-column label="所屬用戶" align="center" prop="userName"/>
-      <el-table-column label="聯絡人名稱" align="center" prop="name"/>
-      <el-table-column label="聯絡電話" align="center" prop="phones"/>
-      <el-table-column label="創建時間" align="center" prop="createTime"/>
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-button 
+          type="info"
+          plain
+          icon="Sort"
+          @click="toggleExpandAll"
+        >展開/折疊</el-button>
+      </el-col>
+    </el-row>
+    <el-table 
+      v-if="refreshTable"
+      v-loading="loading" 
+      :data="treeData" 
+      :tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
+      row-key="id"
+      :default-expand-all="isExpandAll"
+    >
+      <el-table-column label="所屬用戶" prop="userName" width="200" show-overflow-tooltip>
+        <template #default="{ row }">
+          <span v-if="row.isParent" class="parent-user-name">{{ row.userName || row.userId || '-' }}</span>
+          <span v-else>{{ row.userName || '-' }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="用戶id" prop="userId" width="120" align="center"/>
+      <el-table-column label="聯絡人名稱" prop="name" align="center"/>
+      <el-table-column label="聯絡電話" prop="phones" align="center"/>
+      <el-table-column label="創建時間" prop="createTime" align="center"/>
       <el-table-column label="操作" align="center" width="140" class-name="small-padding fixed-width">
         <template #default="{ row }">
-          <el-tooltip content="查看" placement="top">
+          <el-tooltip content="查看" placement="top" v-if="!row.isParent">
             <el-button link type="primary" icon="View" @click.stop="handleView(row)"></el-button>
           </el-tooltip>
         </template>
@@ -90,8 +111,46 @@ const queryParams = reactive({
   userId:null,
 })
 const contactList = ref([])
+const treeData = ref([])
 const total = ref(0)
 const queryFormRef = ref()
+const isExpandAll = ref(false)
+const refreshTable = ref(true)
+
+// 将扁平数据转换为树形结构
+const convertToTree = (data) => {
+  if (!data || !Array.isArray(data)) {
+    return []
+  }
+  const treeMap = new Map()
+  const result = []
+  
+  // 先按userName和userId分组
+  data.forEach(item => {
+    const key = `${item.userName || ''}_${item.userId || ''}`
+    if (!treeMap.has(key)) {
+      treeMap.set(key, {
+        id: `parent_${key}`,
+        userName: item.userName || item.userId || '未知用户',
+        userId: item.userId,
+        isParent: true,
+        children: []
+      })
+    }
+    // 添加子节点
+    treeMap.get(key).children.push({
+      ...item,
+      id: item.id || `child_${key}_${treeMap.get(key).children.length}`
+    })
+  })
+  
+  // 转换为数组
+  treeMap.forEach(value => {
+    result.push(value)
+  })
+  
+  return result
+}
 
 onMounted(() => {
   getList()
@@ -106,8 +165,39 @@ const getList = () => {
     userId: queryParams.userId,
   }
   getContactPageList(params).then(response => {
-    contactList.value = response.data.records
-    total.value = response.data.total
+    // 处理不同的API返回格式
+    // response 可能是 { code: 200, msg: "操作成功", data: [...] } 或直接是数组
+    let data = []
+    let totalCount = 0
+    
+    if (Array.isArray(response)) {
+      // 如果response直接是数组
+      data = response
+      totalCount = data.length
+    } else if (Array.isArray(response.data)) {
+      // 如果data是数组
+      data = response.data
+      totalCount = data.length
+    } else if (response.data && Array.isArray(response.data.records)) {
+      // 如果是分页格式 { records: [], total: ... }
+      data = response.data.records
+      totalCount = response.data.total || data.length
+    }
+    
+    // 如果数据已经是树形结构(包含isParent和children),直接使用
+    if (data.length > 0 && (data[0].isParent !== undefined || data[0].children !== undefined)) {
+      treeData.value = data
+      contactList.value = data.flatMap(item => item.children || [])
+      total.value = totalCount
+    } else {
+      // 否则转换为树形结构
+      contactList.value = data
+      treeData.value = convertToTree(data)
+      total.value = totalCount
+    }
+    loading.value = false
+  }).catch((error) => {
+    console.error('获取联系人列表失败:', error)
     loading.value = false
   })
 }
@@ -152,7 +242,20 @@ const cancel = () => {
   open.value = false
   reset()
 }
+
+/** 展开/折叠操作 */
+const toggleExpandAll = () => {
+  refreshTable.value = false
+  isExpandAll.value = !isExpandAll.value
+  nextTick(() => {
+    refreshTable.value = true
+  })
+}
 </script>
 <style scoped lang="scss">
-
+.parent-user-name {
+  color: #409EFF;
+  font-weight: 500;
+  font-size: 14px;
+}
 </style>