Sfoglia il codice sorgente

添加多级分销 和 域名分配

wilhelm wong 1 mese fa
parent
commit
a891160d43

+ 5 - 0
src/router/index.js

@@ -88,6 +88,11 @@ const router = createRouter({
           name: 'banner',
           component: () => import('@/views/BannerView.vue')
         },
+        {
+          path: 'landing-domain-pool',
+          name: 'landing-domain-pool',
+          component: () => import('@/views/LandingDomainPoolView.vue')
+        },
       ]
     },
     {

+ 107 - 0
src/services/api.js

@@ -365,6 +365,14 @@ export const listMembers = async (page = 0, size = 20, name, teamId, userId) =>
 }
 
 // 创建团队成员
+// memberData 支持以下字段:
+// - name: 团队成员名称(必填)
+// - teamId: 团队ID(必填)
+// - memberId: 父级团队成员ID(可选,不提供则创建二级代理,提供则创建父级成员的下级)
+// - password: 密码(可选,默认:password123)
+// - commissionRate: 分成比例(可选,默认:0)
+// - totalRevenue: 总收入(可选,默认:0)
+// - todayRevenue: 今日收入(可选,默认:0)
 export const createMember = async (memberData) => {
   const response = await api.post('/team-members', memberData)
   return response.data
@@ -713,4 +721,103 @@ export const recordBannerClick = async (id) => {
 export const getBannerStatistics = async () => {
   const response = await api.get('/banners/statistics/summary')
   return response.data
+}
+
+// ==================== 页面点击记录相关API ====================
+
+// 记录页面点击
+export const recordPageClick = async (pageType) => {
+  const response = await api.post('/page-clicks/click', { pageType })
+  return response.data
+}
+
+// 获取页面点击统计
+export const getPageClickStatistics = async (pageType, startDate, endDate) => {
+  const params = {}
+  if (pageType) params.pageType = pageType
+  if (startDate) params.startDate = startDate
+  if (endDate) params.endDate = endDate
+
+  const response = await api.get('/page-clicks/statistics', { params })
+  return response.data
+}
+
+// 获取今日点击统计汇总
+export const getTodayPageClickSummary = async () => {
+  const response = await api.get('/page-clicks/statistics/today')
+  return response.data
+}
+
+// 获取指定页面的点击量
+export const getPageClickCount = async (pageType, date) => {
+  const params = { pageType }
+  if (date) params.date = date
+
+  const response = await api.get('/page-clicks/count', { params })
+  return response.data
+}
+
+// ==================== 落地域名池相关API ====================
+
+// 创建落地域名池(支持批量)
+export const createLandingDomainPool = async (poolData) => {
+  const response = await api.post('/landing-domain-pools', poolData)
+  return response.data
+}
+
+// 获取落地域名池列表
+export const listLandingDomainPools = async (page = 0, size = 20, id, teamId, domain, userId, domainType) => {
+  const params = { page, size }
+  if (id) params.id = id
+  if (teamId) params.teamId = teamId
+  if (domain) params.domain = domain
+  if (userId) params.userId = userId
+  if (domainType) params.domainType = domainType
+
+  const response = await api.get('/landing-domain-pools', { params })
+  return response.data
+}
+
+// 管理员获取所有落地域名池(按团队分组)
+export const showLandingDomainPools = async (id, teamId, domain, userId, domainType) => {
+  const params = {}
+  if (id) params.id = id
+  if (teamId) params.teamId = teamId
+  if (domain) params.domain = domain
+  if (userId) params.userId = userId
+  if (domainType) params.domainType = domainType
+
+  const response = await api.get('/landing-domain-pools/show', { params })
+  return response.data
+}
+
+// 获取单个落地域名池
+export const getLandingDomainPool = async (id) => {
+  const response = await api.get(`/landing-domain-pools/${id}`)
+  return response.data
+}
+
+// 更新落地域名池
+export const updateLandingDomainPool = async (id, poolData) => {
+  const response = await api.put(`/landing-domain-pools/${id}`, poolData)
+  return response.data
+}
+
+// 删除落地域名池
+export const deleteLandingDomainPool = async (id) => {
+  const response = await api.delete(`/landing-domain-pools/${id}`)
+  return response.data
+}
+
+// 根据团队ID获取落地域名池列表
+export const getLandingDomainPoolsByTeamId = async (teamId) => {
+  const response = await api.get(`/landing-domain-pools/team/${teamId}`)
+  return response.data
+}
+
+// 根据域名(team-domain)获取落地域名池列表
+export const getLandingDomainPoolsByDomain = async (domain) => {
+  const params = { domain }
+  const response = await api.get('/landing-domain-pools/by-domain', { params })
+  return response.data
 }

+ 155 - 1
src/views/DashboardView.vue

@@ -300,6 +300,16 @@
       </div>
     </div>
 
+    <!-- 页面访问量统计图表(仅管理员可见) -->
+    <div v-if="isAdmin" class="chart-container">
+      <div class="chart-header">
+        <h2>最近7天页面访问量统计</h2>
+      </div>
+      <div class="chart-wrapper page-click-chart-wrapper">
+        <canvas id="pageClickChart" height="300"></canvas>
+      </div>
+    </div>
+
     <!-- 主题选择弹窗 -->
     <Dialog
       v-model:visible="themeDialog"
@@ -439,7 +449,7 @@
 import { ref, onMounted, onUnmounted, computed, inject, watch } from 'vue'
 import { useUserStore } from '@/stores/user'
 import { useTeamStore } from '@/stores/team'
-import { getAllTeamStatistics, getIncomeStatistics, listMembers, getTeamIpConversionRate, listTeams, getTeamDomainDailyStatistics, getTeamDomainAllStatistics, updateTeamThemeColor, generatePromoCode, getPromotionLink, getMyPromotionInfo, generateMyPromoCode, getTeamMemberIpConversionRate, getTeamMemberAllStatistics, getTeamMemberDailyStatistics } from '@/services/api'
+import { getAllTeamStatistics, getIncomeStatistics, listMembers, getTeamIpConversionRate, listTeams, getTeamDomainDailyStatistics, getTeamDomainAllStatistics, updateTeamThemeColor, generatePromoCode, getPromotionLink, getMyPromotionInfo, generateMyPromoCode, getTeamMemberIpConversionRate, getTeamMemberAllStatistics, getTeamMemberDailyStatistics, getPageClickStatistics } from '@/services/api'
 import { useToast } from 'primevue/usetoast'
 import Chart from 'chart.js/auto'
 import Select from 'primevue/select'
@@ -468,6 +478,8 @@ const teams = ref([])
 const chartInstance = ref(null)
 const selectedChartTeamId = ref(null) // 用于图表的团队选择
 const ipStats = ref(null) // IP成交率数据
+const pageClickStats = ref(null) // 页面点击统计数据
+const pageClickChartInstance = ref(null) // 页面点击统计图表实例
 const teamListData = ref(null) // 团队列表数据(包含基础信息如affCode等)
 const promoLink = ref(null) // 推广链接
 const promoDialog = ref(false) // 推广码/链接弹窗
@@ -962,6 +974,18 @@ const destroyChart = () => {
   }
 }
 
+// 销毁页面点击统计图表实例
+const destroyPageClickChart = () => {
+  if (pageClickChartInstance.value) {
+    try {
+      pageClickChartInstance.value.destroy()
+    } catch (e) {
+      console.warn('销毁页面点击统计图表实例失败:', e)
+    }
+    pageClickChartInstance.value = null
+  }
+}
+
 // 创建图表配置
 const createChartConfig = (labels, datasets) => {
   return {
@@ -1153,6 +1177,131 @@ watch(selectedChartTeamId, () => {
   loadIncomeStats()
 })
 
+// 加载页面点击统计数据
+const loadPageClickStats = async () => {
+  if (!isAdmin.value) return
+  
+  try {
+    loading.value = true
+    const { startDate, endDate } = getDateRange()
+    
+    // 获取数据前先销毁旧图表
+    destroyPageClickChart()
+    
+    const data = await getPageClickStatistics(null, startDate, endDate)
+    
+    // 处理统计数据,按日期和页面类型组织数据
+    const statistics = data?.statistics || []
+    
+    // 获取所有唯一的日期
+    const dates = [...new Set(statistics.map(s => s.date))].sort()
+    
+    // 按页面类型分组数据
+    const homeData = {}
+    const videoData = {}
+    
+    statistics.forEach(stat => {
+      if (stat.pageType === 'home') {
+        homeData[stat.date] = stat.clickCount
+      } else if (stat.pageType === 'video') {
+        videoData[stat.date] = stat.clickCount
+      }
+    })
+    
+    // 构建图表数据数组
+    const homeClickCounts = dates.map(date => homeData[date] || 0)
+    const videoClickCounts = dates.map(date => videoData[date] || 0)
+    
+    pageClickStats.value = {
+      dates,
+      homeClickCounts,
+      videoClickCounts
+    }
+    
+    // 渲染图表
+    renderPageClickChart()
+  } catch (error) {
+    console.error('加载页面点击统计数据失败:', error)
+  } finally {
+    loading.value = false
+  }
+}
+
+// 创建页面点击统计图表画布
+const createPageClickCanvas = () => {
+  const chartContainer = document.querySelector('.page-click-chart-wrapper')
+  if (!chartContainer) {
+    console.error('找不到页面点击统计图表容器')
+    return null
+  }
+  
+  // 清空容器
+  chartContainer.innerHTML = ''
+  
+  // 创建新画布
+  const canvas = document.createElement('canvas')
+  canvas.id = 'pageClickChart'
+  canvas.height = 300
+  chartContainer.appendChild(canvas)
+  
+  return canvas
+}
+
+// 渲染页面点击统计图表
+const renderPageClickChart = () => {
+  // 检查数据有效性
+  if (!pageClickStats.value || !pageClickStats.value.dates || !pageClickStats.value.dates.length) {
+    console.warn('没有有效的页面点击统计数据')
+    return
+  }
+  
+  // 创建新画布
+  const canvas = createPageClickCanvas()
+  if (!canvas) return
+  
+  // 获取绘图上下文
+  const ctx = canvas.getContext('2d')
+  if (!ctx) {
+    console.error('无法获取绘图上下文')
+    return
+  }
+  
+  // 准备数据
+  const { dates, homeClickCounts, videoClickCounts } = pageClickStats.value
+  
+  // 创建数据集
+  const datasets = [
+    {
+      label: '首页访问量',
+      data: homeClickCounts,
+      borderColor: '#2563eb',
+      backgroundColor: 'rgba(37, 99, 235, 0.1)',
+      fill: true,
+      tension: 0.3
+    },
+    {
+      label: '视频页访问量',
+      data: videoClickCounts,
+      borderColor: '#10b981',
+      backgroundColor: 'rgba(16, 185, 129, 0.1)',
+      fill: true,
+      tension: 0.3
+    }
+  ]
+  
+  // 使用setTimeout确保DOM已更新
+  setTimeout(() => {
+    try {
+      // 创建图表
+      const config = createChartConfig(dates, datasets)
+      pageClickChartInstance.value = new Chart(ctx, config)
+    } catch (error) {
+      console.error('页面点击统计图表渲染失败:', error)
+      pageClickChartInstance.value = null
+    }
+  }, 0)
+}
+
 // 组件挂载时加载数据
 onMounted(async () => {
   if (isAdmin.value) {
@@ -1168,12 +1317,17 @@ onMounted(async () => {
   // 延迟加载图表数据,确保DOM已渲染
   setTimeout(() => {
     loadIncomeStats()
+    // 管理员加载页面访问量统计
+    if (isAdmin.value) {
+      loadPageClickStats()
+    }
   }, 100)
 })
 
 // 组件卸载时清理图表实例
 onUnmounted(() => {
   destroyChart()
+  destroyPageClickChart()
 })
 </script>
 

+ 91 - 6
src/views/DomainView.vue

@@ -478,6 +478,35 @@ const teamMemberOptions = computed(() => {
   ]
 })
 
+// 扁平化树状结构数据
+const flattenTeamMembersTree = (nodes, result = [], parentId = null) => {
+  if (!nodes || !Array.isArray(nodes)) return result
+  for (const node of nodes) {
+    // 跳过团队节点(type === 'team' 或 id === 0),只保留团队成员
+    if (node.type === 'teamMember' || (node.type !== 'team' && node.id !== 0 && node.id !== null)) {
+      // 创建扁平化的成员对象,移除children字段,添加parentId
+      const flattenedNode = {
+        ...node,
+        parentId: node.parentId !== undefined ? node.parentId : parentId
+      }
+      // 移除children字段
+      delete flattenedNode.children
+      result.push(flattenedNode)
+      
+      // 递归处理子节点,传递当前节点的id作为parentId
+      if (node.children && Array.isArray(node.children) && node.children.length > 0) {
+        flattenTeamMembersTree(node.children, result, node.id)
+      }
+    } else {
+      // 如果是团队节点,也需要递归处理其子节点
+      if (node.children && Array.isArray(node.children) && node.children.length > 0) {
+        flattenTeamMembersTree(node.children, result, parentId)
+      }
+    }
+  }
+  return result
+}
+
 // 获取团队成员数据
 const fetchTeamMembers = async (teamId) => {
   if (!teamId) {
@@ -487,7 +516,28 @@ const fetchTeamMembers = async (teamId) => {
   
   try {
     const response = await listMembers(0, 100, undefined, teamId)
-    teamMembers.value = response.content || []
+    // 处理返回的数据:可能是扁平数组,但每个成员可能有children子成员
+    if (response.content && Array.isArray(response.content) && response.content.length > 0) {
+      // 检查是否有嵌套的children结构
+      const hasNestedChildren = response.content.some(item => 
+        item.children && Array.isArray(item.children) && item.children.length > 0
+      )
+      
+      if (hasNestedChildren) {
+        teamMembers.value = flattenTeamMembersTree(response.content)
+      } else {
+        // 没有嵌套结构,但需要确保每个成员都有parentId字段
+        teamMembers.value = (response.content || []).map(item => {
+          const { children, ...rest } = item
+          return {
+            ...rest,
+            parentId: item.parentId !== undefined ? item.parentId : null
+          }
+        })
+      }
+    } else {
+      teamMembers.value = []
+    }
   } catch (error) {
     console.error('获取团队成员失败:', error)
     teamMembers.value = []
@@ -505,19 +555,40 @@ const fetchTeamMembersForCurrentUser = async () => {
   try {
     console.log('正在获取当前用户的团队成员数据')
     
+    let response
     if (isPromoter.value) {
       // 推广员只获取自己的信息
       const currentUserId = userStore.userInfo?.id
       if (currentUserId) {
-        const response = await listMembers(0, 100, undefined, undefined, currentUserId)
-        teamMembers.value = response.content || []
+        response = await listMembers(0, 100, undefined, undefined, currentUserId)
       } else {
         teamMembers.value = []
+        return
       }
     } else {
       // 队长和管理员获取所有团队成员
-      const response = await listMembers(0, 100)
-      teamMembers.value = response.content || []
+      response = await listMembers(0, 100)
+    }
+    
+    // 处理返回的数据
+    if (response.content && Array.isArray(response.content) && response.content.length > 0) {
+      const hasNestedChildren = response.content.some(item => 
+        item.children && Array.isArray(item.children) && item.children.length > 0
+      )
+      
+      if (hasNestedChildren) {
+        teamMembers.value = flattenTeamMembersTree(response.content)
+      } else {
+        teamMembers.value = (response.content || []).map(item => {
+          const { children, ...rest } = item
+          return {
+            ...rest,
+            parentId: item.parentId !== undefined ? item.parentId : null
+          }
+        })
+      }
+    } else {
+      teamMembers.value = []
     }
     
     console.log('获取到的团队成员数据:', teamMembers.value)
@@ -540,7 +611,21 @@ const loadAllTeamMembers = async () => {
     for (const team of teamStore.teams) {
       const response = await listMembers(0, 100, undefined, team.id)
       if (response.content) {
-        allMembers.push(...response.content)
+        const hasNestedChildren = response.content.some(item => 
+          item.children && Array.isArray(item.children) && item.children.length > 0
+        )
+        
+        if (hasNestedChildren) {
+          allMembers.push(...flattenTeamMembersTree(response.content))
+        } else {
+          allMembers.push(...(response.content || []).map(item => {
+            const { children, ...rest } = item
+            return {
+              ...rest,
+              parentId: item.parentId !== undefined ? item.parentId : null
+            }
+          }))
+        }
       }
     }
     teamMembers.value = allMembers

+ 313 - 3
src/views/IncomeView.vue

@@ -219,7 +219,7 @@
 
       <Column
         header="操作"
-        style="min-width: 150px; width: 150px"
+        style="min-width: 200px; width: 200px"
         align-frozen="right"
         frozen
         :pt="{
@@ -230,6 +230,15 @@
       >
         <template #body="slotProps">
           <div class="flex justify-center gap-1">
+            <Button
+              icon="pi pi-dollar"
+              severity="success"
+              size="small"
+              text
+              rounded
+              aria-label="查看分润"
+              @click="openCommissionDialog(slotProps.data)"
+            />
             <Button
               icon="pi pi-pencil"
               severity="info"
@@ -253,6 +262,81 @@
       </Column>
     </DataTable>
 
+    <!-- 分润详情弹窗 -->
+    <Dialog
+      v-model:visible="commissionDialog"
+      :modal="true"
+      header="分润详情"
+      :style="{ width: '800px' }"
+      position="center"
+    >
+      <div v-if="commissionLoading" class="flex justify-center items-center py-8">
+        <i class="pi pi-spin pi-spinner text-2xl"></i>
+        <span class="ml-2">加载中...</span>
+      </div>
+      <div v-else-if="commissionDetails && commissionDetails.length > 0" class="commission-details">
+        <div class="mb-4 p-3 bg-blue-50 rounded-lg">
+          <div class="grid grid-cols-2 gap-4 text-sm">
+            <div>
+              <span class="text-gray-600">订单号:</span>
+              <span class="font-mono font-semibold">{{ currentIncomeRecord?.orderNo || '-' }}</span>
+            </div>
+            <div>
+              <span class="text-gray-600">订单价格:</span>
+              <span class="font-semibold text-green-600">¥{{ formatAmount(currentIncomeRecord?.orderPrice) }}</span>
+            </div>
+          </div>
+        </div>
+        <DataTable
+          :value="commissionDetails"
+          :paginator="false"
+          scrollable
+          class="commission-table"
+        >
+          <Column field="level" header="层级" style="width: 80px">
+            <template #body="slotProps">
+              <Tag :value="`第${slotProps.data.level}级`" severity="info" />
+            </template>
+          </Column>
+          <Column field="agentId" header="代理ID" style="width: 100px">
+            <template #body="slotProps">
+              <span class="font-mono text-sm">{{ slotProps.data.agentId }}</span>
+            </template>
+          </Column>
+          <Column field="agentName" header="代理名称" style="min-width: 150px">
+            <template #body="slotProps">
+              <span class="text-sm">{{ getAgentName(slotProps.data.agentId) }}</span>
+            </template>
+          </Column>
+          <Column field="rate" header="分润比例" style="width: 120px">
+            <template #body="slotProps">
+              <span class="font-semibold text-blue-600">{{ slotProps.data.rate }}%</span>
+            </template>
+          </Column>
+          <Column field="amount" header="分润金额" style="width: 150px">
+            <template #body="slotProps">
+              <span class="font-semibold text-green-600">¥{{ formatAmount(slotProps.data.amount) }}</span>
+            </template>
+          </Column>
+        </DataTable>
+        <div class="mt-4 p-3 bg-green-50 rounded-lg">
+          <div class="flex justify-between items-center">
+            <span class="text-gray-700 font-medium">总分润金额:</span>
+            <span class="text-2xl font-bold text-green-600">
+              ¥{{ formatAmount(totalCommissionAmount) }}
+            </span>
+          </div>
+        </div>
+      </div>
+      <div v-else class="text-center py-8 text-gray-500">
+        <i class="pi pi-info-circle text-3xl mb-2"></i>
+        <p>暂无分润信息</p>
+      </div>
+      <template #footer>
+        <Button label="关闭" severity="secondary" @click="commissionDialog = false" />
+      </template>
+    </Dialog>
+
     <!-- 编辑弹窗 -->
     <Dialog
       v-model:visible="editDialog"
@@ -385,9 +469,10 @@ import Dialog from 'primevue/dialog'
 import Select from 'primevue/select'
 import InputText from 'primevue/inputtext'
 import InputNumber from 'primevue/inputnumber'
+import Tag from 'primevue/tag'
 import { useConfirm } from 'primevue/useconfirm'
 import { useToast } from 'primevue/usetoast'
-import { listIncome, updateIncome, deleteIncome, listMembers } from '@/services/api'
+import { listIncome, updateIncome, deleteIncome, listMembers, getIncome } from '@/services/api'
 import { IncomeType, OrderType } from '@/enums'
 import { useTeamStore } from '@/stores/team'
 import { useUserStore } from '@/stores/user'
@@ -412,6 +497,9 @@ const tableData = ref({
 // 团队成员列表数据
 const membersList = ref([])
 
+// 代理信息缓存(用于存储动态查询的代理信息)
+const agentInfoCache = ref(new Map())
+
 // 加载状态
 const loading = ref(false)
 
@@ -432,6 +520,12 @@ const editForm = ref({
   payNo: null
 })
 
+// 分润详情相关
+const commissionDialog = ref(false)
+const commissionLoading = ref(false)
+const commissionDetails = ref([])
+const currentIncomeRecord = ref(null)
+
 // 搜索表单
 const searchForm = ref({
   id: null,
@@ -561,12 +655,63 @@ const formatDateTime = (dateString) => {
   return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
 }
 
+// 扁平化树形结构,提取所有层级的成员
+const flattenMembers = (members) => {
+  const result = []
+  if (!members || !Array.isArray(members)) return result
+  
+  const traverse = (nodes) => {
+    if (!nodes || !Array.isArray(nodes)) return
+    for (const node of nodes) {
+      if (node.type === 'teamMember' && node.userId) {
+        result.push({
+          userId: node.userId,
+          name: node.name,
+          id: node.id,
+          teamId: node.teamId
+        })
+      }
+      // 递归处理子节点
+      if (node.children && Array.isArray(node.children) && node.children.length > 0) {
+        traverse(node.children)
+      }
+    }
+  }
+  
+  traverse(members)
+  return result
+}
+
 // 获取团队成员列表
 const fetchMembers = async () => {
   try {
     // 获取所有团队成员(不分页)
+    // 先获取第一页数据
     const response = await listMembers(0, 1000) // 获取大量数据
-    membersList.value = response.content || []
+    let allMembers = []
+    
+    // 扁平化树形结构,提取所有层级的成员
+    if (response.content && Array.isArray(response.content)) {
+      allMembers = flattenMembers(response.content)
+    }
+    
+    // 如果还有更多数据,继续获取(递归获取所有页)
+    if (response.metadata && response.metadata.total > response.metadata.size) {
+      const totalPages = Math.ceil(response.metadata.total / response.metadata.size)
+      const additionalPromises = []
+      for (let page = 1; page < totalPages; page++) {
+        additionalPromises.push(listMembers(page, 1000))
+      }
+      const additionalResponses = await Promise.all(additionalPromises)
+      additionalResponses.forEach(res => {
+        if (res.content && Array.isArray(res.content)) {
+          const flattened = flattenMembers(res.content)
+          allMembers.push(...flattened)
+        }
+      })
+    }
+    
+    membersList.value = allMembers
   } catch (error) {
     console.error('获取团队成员列表失败:', error)
   }
@@ -730,6 +875,157 @@ const saveEdit = async () => {
   }
 }
 
+// 批量查询代理信息
+const fetchAgentInfo = async (agentIds) => {
+  if (!agentIds || agentIds.length === 0) return
+  
+  // 过滤出需要查询的 agentId(不在缓存中的)
+  const idsToFetch = agentIds.filter(id => {
+    if (!id) return false
+    // 检查是否已在缓存中
+    if (agentInfoCache.value.has(id)) return false
+    // 检查是否在团队成员列表中
+    if (membersList.value.find(m => m.userId === id)) return false
+    // 检查是否在团队列表中
+    if (teamStore.teams && teamStore.teams.find(t => t.userId === id)) return false
+    return true
+  })
+  
+  if (idsToFetch.length === 0) return
+  
+  try {
+    // 直接获取所有团队成员列表,而不是逐个查询
+    const response = await listMembers(0, 1000)
+    let allMembers = []
+    
+    // 扁平化树形结构,提取所有层级的成员
+    if (response.content && Array.isArray(response.content)) {
+      allMembers = flattenMembers(response.content)
+    }
+    
+    // 如果还有更多数据,继续获取
+    if (response.metadata && response.metadata.total > response.metadata.size) {
+      const totalPages = Math.ceil(response.metadata.total / response.metadata.size)
+      const additionalPromises = []
+      for (let page = 1; page < totalPages; page++) {
+        additionalPromises.push(listMembers(page, 1000))
+      }
+      const additionalResponses = await Promise.all(additionalPromises)
+      additionalResponses.forEach(res => {
+        if (res.content && Array.isArray(res.content)) {
+          const flattened = flattenMembers(res.content)
+          allMembers.push(...flattened)
+        }
+      })
+    }
+    
+    // 从获取的团队成员列表中查找需要的代理信息并缓存
+    idsToFetch.forEach(agentId => {
+      const member = allMembers.find(m => m.userId === agentId)
+      if (member) {
+        agentInfoCache.value.set(agentId, {
+          type: 'member',
+          name: member.name,
+          userId: member.userId
+        })
+      }
+    })
+  } catch (error) {
+    console.error('批量查询代理信息失败:', error)
+  }
+}
+
+// 打开分润详情弹窗
+const openCommissionDialog = async (income) => {
+  commissionDialog.value = true
+  commissionLoading.value = true
+  currentIncomeRecord.value = income
+  commissionDetails.value = []
+  
+  try {
+    // 获取收入记录详情(包含 commissionDetails)
+    const detail = await getIncome(income.id)
+    
+    // 解析分润信息
+    if (detail.commissionDetails) {
+      // 如果已经是数组,直接使用
+      if (Array.isArray(detail.commissionDetails)) {
+        commissionDetails.value = detail.commissionDetails
+      } 
+      // 如果是字符串,需要解析 JSON
+      else if (typeof detail.commissionDetails === 'string') {
+        try {
+          commissionDetails.value = JSON.parse(detail.commissionDetails)
+        } catch (e) {
+          console.error('解析分润信息失败:', e)
+          commissionDetails.value = []
+        }
+      }
+    }
+    
+    // 收集所有的 agentId 并批量查询代理信息
+    if (commissionDetails.value && commissionDetails.value.length > 0) {
+      const agentIds = commissionDetails.value
+        .map(item => item.agentId)
+        .filter(id => id) // 过滤掉空值
+      await fetchAgentInfo(agentIds)
+    }
+    
+    // 更新当前收入记录信息
+    currentIncomeRecord.value = detail
+  } catch (error) {
+    console.error('获取分润详情失败:', error)
+    toast.add({
+      severity: 'error',
+      summary: '错误',
+      detail: '获取分润详情失败',
+      life: 3000
+    })
+    commissionDetails.value = []
+  } finally {
+    commissionLoading.value = false
+  }
+}
+
+// 获取代理名称(从团队或团队成员中查找)
+const getAgentName = (agentId) => {
+  if (!agentId) return '-'
+  
+  // 先从缓存中查找
+  if (agentInfoCache.value.has(agentId)) {
+    const cached = agentInfoCache.value.get(agentId)
+    return `${cached.name} (${cached.type === 'member' ? '团队成员' : '团队'})`
+  }
+  
+  // 再从团队成员列表中查找
+  if (membersList.value.length > 0) {
+    const member = membersList.value.find(m => m.userId === agentId)
+    if (member) {
+      return `${member.name} (团队成员)`
+    }
+  }
+  
+  // 再从团队列表中查找
+  if (teamStore.teams && teamStore.teams.length > 0) {
+    const team = teamStore.teams.find(t => t.userId === agentId)
+    if (team) {
+      return `${team.name} (团队)`
+    }
+  }
+  
+  return `ID: ${agentId}`
+}
+
+// 计算总分润金额
+const totalCommissionAmount = computed(() => {
+  if (!commissionDetails.value || commissionDetails.value.length === 0) {
+    return 0
+  }
+  return commissionDetails.value.reduce((sum, item) => {
+    return sum + (Number(item.amount) || 0)
+  }, 0)
+})
+
 // 初始化
 onMounted(async () => {
   // 获取团队成员列表
@@ -866,4 +1162,18 @@ onMounted(async () => {
   white-space: nowrap;
   display: inline-block;
 }
+
+.commission-details {
+  max-height: 600px;
+  overflow-y: auto;
+}
+
+.commission-table {
+  width: 100%;
+}
+
+.commission-table .p-datatable-wrapper {
+  max-height: 400px;
+  overflow-y: auto;
+}
 </style>

+ 1252 - 0
src/views/LandingDomainPoolView.vue

@@ -0,0 +1,1252 @@
+<script setup>
+import { ref, onMounted, reactive, computed, inject } from 'vue'
+import {
+  listLandingDomainPools,
+  createLandingDomainPool,
+  updateLandingDomainPool,
+  deleteLandingDomainPool,
+  getLandingDomainPool,
+  showLandingDomainPools,
+  listMembers
+} from '@/services/api'
+import { useToast } from 'primevue/usetoast'
+import { useConfirm } from 'primevue/useconfirm'
+import DataTable from 'primevue/datatable'
+import Column from 'primevue/column'
+import Button from 'primevue/button'
+import InputText from 'primevue/inputtext'
+import Select from 'primevue/select'
+import Dialog from 'primevue/dialog'
+import Textarea from 'primevue/textarea'
+import { useDateFormat } from '@vueuse/core'
+import { useUserStore } from '@/stores/user'
+import { useTeamStore } from '@/stores/team'
+
+const toast = useToast()
+const confirm = useConfirm()
+const userStore = useUserStore()
+const teamStore = useTeamStore()
+
+// 注入权限信息
+const isAdmin = inject('isAdmin')
+const isTeam = inject('isTeam')
+const isPromoter = inject('isPromoter')
+
+const tableRef = ref(null)
+const tableData = ref({
+  data: [],
+  meta: {
+    total: 0,
+    page: 0,
+    size: 20
+  }
+})
+
+// 管理员专用的分组数据
+const adminGroupedData = ref({})
+
+const selectedPool = ref(null)
+const dialogVisible = ref(false)
+const isEditing = ref(false)
+const poolModel = reactive({
+  teamId: null,
+  domain: '',
+  description: '',
+  userId: null,
+  domainType: 'landing' // 默认值为 landing
+})
+
+// 搜索表单
+const searchForm = ref({
+  domain: '',
+  teamId: null,
+  domainType: null // 域名类型筛选
+})
+
+// 计算当前用户的团队ID
+const currentTeamId = computed(() => {
+  if (isAdmin.value) {
+    return null
+  } else if (isTeam.value || isPromoter.value) {
+    return userStore.userInfo?.teamId
+  }
+  return null
+})
+
+// 计算是否有操作权限
+const canCreate = computed(() => isAdmin.value || isTeam.value)
+const canUpdate = computed(() => isAdmin.value || isTeam.value)
+const canDelete = computed(() => isAdmin.value || isTeam.value)
+const canView = computed(() => isAdmin.value || isTeam.value)
+
+const fetchData = async (page = 0) => {
+  try {
+    if (isAdmin.value) {
+      // 管理员使用分组接口
+      const result = await showLandingDomainPools(
+        undefined,
+        searchForm.value.teamId,
+        searchForm.value.domain || undefined,
+        undefined, // userId
+        searchForm.value.domainType || undefined // domainType
+      )
+      adminGroupedData.value = result || {}
+      
+      // 收集所有落地域名池数据
+      const allPools = []
+      Object.values(adminGroupedData.value).forEach(pools => {
+        if (Array.isArray(pools)) {
+          allPools.push(...pools)
+        }
+      })
+      
+      // 根据实际数据加载需要的团队成员数据
+      await loadTeamMembersForPools(allPools)
+    } else {
+      // 其他角色使用列表接口
+      let queryTeamId = searchForm.value.teamId || currentTeamId.value
+
+      const result = await listLandingDomainPools(
+        page,
+        tableData.value.meta?.size || 20,
+        undefined,
+        queryTeamId,
+        searchForm.value.domain || undefined,
+        undefined, // userId
+        searchForm.value.domainType || undefined // domainType
+      )
+      tableData.value = {
+        data: result?.content || [],
+        meta: {
+          total: result?.metadata?.total || 0,
+          page: result?.metadata?.page || 0,
+          size: result?.metadata?.size || 20,
+          totalPages: Math.ceil((result?.metadata?.total || 0) / (result?.metadata?.size || 20))
+        }
+      }
+      
+      // 根据实际数据加载需要的团队成员数据
+      await loadTeamMembersForPools(result?.content || [])
+    }
+  } catch (error) {
+    console.error('获取落地域名池列表失败', error)
+    toast.add({ severity: 'error', summary: '错误', detail: '获取落地域名池列表失败', life: 3000 })
+    if (isAdmin.value) {
+      adminGroupedData.value = {}
+    } else {
+      tableData.value = {
+        data: [],
+        meta: {
+          total: 0,
+          page: 0,
+          size: 20,
+          totalPages: 0
+        }
+      }
+    }
+  }
+}
+
+const handlePageChange = (event) => {
+  if (!isAdmin.value) {
+    fetchData(event.page)
+  }
+}
+
+const refreshData = () => {
+  if (isAdmin.value) {
+    fetchData(0)
+  } else {
+    const page = tableData.value.meta?.page || 0
+    fetchData(page)
+  }
+}
+
+const handleSearch = () => {
+  if (!isAdmin.value && tableData.value.meta) {
+    tableData.value.meta.page = 0
+  }
+  fetchData(0)
+}
+
+const handleRefresh = () => {
+  searchForm.value = {
+    domain: '',
+    teamId: null,
+    domainType: null
+  }
+  if (!isAdmin.value && tableData.value.meta) {
+    tableData.value.meta.page = 0
+  }
+  fetchData(0)
+}
+
+const formatDate = (date) => {
+  if (!date) return '-'
+  return useDateFormat(new Date(date), 'YYYY-MM-DD HH:mm:ss').value
+}
+
+// 复制域名到剪贴板
+const copyDomain = async (domain) => {
+  try {
+    await navigator.clipboard.writeText(domain)
+    toast.add({
+      severity: 'success',
+      summary: '成功',
+      detail: '域名已复制到剪贴板',
+      life: 2000
+    })
+  } catch (error) {
+    console.error('复制失败:', error)
+    toast.add({
+      severity: 'error',
+      summary: '复制失败',
+      detail: '无法复制到剪贴板',
+      life: 3000
+    })
+  }
+}
+
+const resetModel = () => {
+  poolModel.teamId = currentTeamId.value
+  poolModel.domain = ''
+  poolModel.description = ''
+  poolModel.userId = null
+  poolModel.domainType = 'landing' // 重置为默认值
+}
+
+const onEdit = async (pool = null) => {
+  resetModel()
+
+  if (pool) {
+    isEditing.value = true
+    selectedPool.value = pool
+    try {
+      const detail = await getLandingDomainPool(pool.id)
+      poolModel.teamId = detail.teamId
+      poolModel.domain = detail.domain
+      poolModel.description = detail.description || ''
+      poolModel.userId = detail.userId || null
+      poolModel.domainType = detail.domainType || 'landing' // 读取域名类型
+      
+      // 如果是管理员,加载团队成员数据
+      if (isAdmin.value && detail.teamId) {
+        await fetchTeamMembers(detail.teamId)
+      }
+    } catch (error) {
+      toast.add({ severity: 'error', summary: '错误', detail: '获取落地域名池详情失败', life: 3000 })
+      return
+    }
+  } else {
+    isEditing.value = false
+    // 创建时,如果不是管理员,加载团队成员数据
+    if (!isAdmin.value && currentTeamId.value) {
+      await fetchTeamMembersForCurrentUser()
+    }
+  }
+
+  dialogVisible.value = true
+}
+
+const onCancel = () => {
+  dialogVisible.value = false
+}
+
+// 解析域名字符串(支持逗号、分号、换行分隔)
+const parseDomains = (domainString) => {
+  if (!domainString) return []
+  return domainString
+    .split(/[,,;;\n\r]+/)
+    .map((d) => d.trim())
+    .filter((d) => d.length > 0)
+}
+
+const validateForm = () => {
+  if (!poolModel.domain) {
+    toast.add({ severity: 'warn', summary: '警告', detail: '域名不能为空', life: 3000 })
+    return false
+  }
+
+  const domains = parseDomains(poolModel.domain)
+  if (domains.length === 0) {
+    toast.add({ severity: 'warn', summary: '警告', detail: '请输入有效的域名', life: 3000 })
+    return false
+  }
+
+  // 管理员必须指定teamId
+  if (isAdmin.value && !poolModel.teamId) {
+    toast.add({ severity: 'warn', summary: '警告', detail: '请选择团队', life: 3000 })
+    return false
+  }
+
+  return true
+}
+
+const onSubmit = async () => {
+  if (!validateForm()) return
+
+  try {
+    if (isEditing.value) {
+      // 编辑模式:只支持单个域名
+      const poolData = {
+        domain: poolModel.domain.trim(),
+        description: poolModel.description,
+        domainType: poolModel.domainType || 'landing' // 添加域名类型
+      }
+
+      // 管理员需要传递teamId
+      if (isAdmin.value) {
+        poolData.teamId = poolModel.teamId
+      }
+
+      // 添加用户绑定
+      if (poolModel.userId !== null && poolModel.userId !== '') {
+        poolData.userId = poolModel.userId
+      }
+
+      await updateLandingDomainPool(selectedPool.value.id, poolData)
+      toast.add({ severity: 'success', summary: '成功', detail: '更新落地域名池成功', life: 3000 })
+    } else {
+      // 创建模式:支持批量创建
+      const domains = parseDomains(poolModel.domain)
+      const poolData = {
+        domain: domains.join(','),
+        description: poolModel.description,
+        domainType: poolModel.domainType || 'landing' // 添加域名类型
+      }
+
+      // 管理员需要传递teamId
+      if (isAdmin.value) {
+        poolData.teamId = poolModel.teamId
+      }
+
+      // 添加用户绑定
+      if (poolModel.userId !== null && poolModel.userId !== '') {
+        poolData.userId = poolModel.userId
+      }
+
+      const result = await createLandingDomainPool(poolData)
+
+      // 批量创建返回的结果结构
+      if (result.success && result.success.length > 0) {
+        const successCount = result.success.length
+        const failCount = result.failed ? result.failed.length : 0
+        toast.add({
+          severity: 'success',
+          summary: '成功',
+          detail: `成功创建 ${successCount} 个域名${failCount > 0 ? `,${failCount} 个失败` : ''}`,
+          life: 3000
+        })
+      } else if (result.id) {
+        // 单个创建成功
+        toast.add({ severity: 'success', summary: '成功', detail: '创建落地域名池成功', life: 3000 })
+      } else {
+        toast.add({ severity: 'error', summary: '失败', detail: '创建失败', life: 3000 })
+      }
+    }
+
+    dialogVisible.value = false
+    refreshData()
+  } catch (error) {
+    const errorMessage = error?.message || error?.detail || (isEditing.value ? '更新失败' : '创建失败')
+    toast.add({
+      severity: 'error',
+      summary: '错误',
+      detail: errorMessage,
+      life: 3000
+    })
+  }
+}
+
+const onDelete = async (pool) => {
+  confirm.require({
+    message: `确定要删除落地域名池 "${pool.domain}" 吗?`,
+    header: '删除确认',
+    icon: 'pi pi-exclamation-triangle',
+    rejectLabel: '取消',
+    rejectProps: {
+      label: '取消',
+      severity: 'secondary'
+    },
+    acceptLabel: '删除',
+    acceptProps: {
+      label: '删除',
+      severity: 'danger'
+    },
+    accept: async () => {
+      try {
+        await deleteLandingDomainPool(pool.id)
+        toast.add({ severity: 'success', summary: '成功', detail: '删除落地域名池成功', life: 3000 })
+        refreshData()
+      } catch (error) {
+        const errorMessage = error?.message || error?.detail || '删除失败'
+        toast.add({ severity: 'error', summary: '错误', detail: errorMessage, life: 3000 })
+      }
+    }
+  })
+}
+
+// 计算团队选项(仅管理员需要)
+const teamOptions = computed(() => {
+  if (!isAdmin.value) return []
+  return [
+    { label: '全部团队', value: null },
+    ...teamStore.teams.map((team) => ({
+      label: team.name,
+      value: team.id
+    }))
+  ]
+})
+
+// 获取团队名称
+const getTeamName = (teamId) => {
+  if (!teamId) return '-'
+  const team = teamStore.teams.find((t) => t.id === teamId)
+  return team ? team.name : '-'
+}
+
+// 团队成员数据
+const teamMembers = ref([])
+
+// 计算团队成员选项
+const teamMemberOptions = computed(() => {
+  if (!poolModel.teamId) return []
+  
+  return [
+    { label: '不绑定用户', value: null },
+    ...teamMembers.value.map((member) => ({
+      label: `${member.name} (${member.commissionRate || 0}%)`,
+      // 注意:落地域名池的 userId 对应团队成员表的 userId 字段,不是 id 字段
+      value: member.userId
+    }))
+  ]
+})
+
+// 域名类型选项
+const domainTypeOptions = [
+  { label: '全部类型', value: null },
+  { label: '落地域名', value: 'landing' },
+  { label: '留存域名', value: 'retention' }
+]
+
+// 获取域名类型标签
+const getDomainTypeLabel = (domainType) => {
+  if (!domainType) return '-'
+  return domainType === 'landing' ? '落地域名' : domainType === 'retention' ? '留存域名' : domainType
+}
+
+// 扁平化树状结构数据
+const flattenTeamMembersTree = (nodes, result = [], parentId = null) => {
+  if (!nodes || !Array.isArray(nodes)) return result
+  for (const node of nodes) {
+    // 跳过团队节点(type === 'team' 或 id === 0),只保留团队成员
+    if (node.type === 'teamMember' || (node.type !== 'team' && node.id !== 0 && node.id !== null)) {
+      // 创建扁平化的成员对象,移除children字段,添加parentId
+      const flattenedNode = {
+        ...node,
+        parentId: node.parentId !== undefined ? node.parentId : parentId
+      }
+      // 移除children字段
+      delete flattenedNode.children
+      result.push(flattenedNode)
+      
+      // 递归处理子节点,传递当前节点的id作为parentId
+      if (node.children && Array.isArray(node.children) && node.children.length > 0) {
+        flattenTeamMembersTree(node.children, result, node.id)
+      }
+    } else {
+      // 如果是团队节点,也需要递归处理其子节点
+      if (node.children && Array.isArray(node.children) && node.children.length > 0) {
+        flattenTeamMembersTree(node.children, result, parentId)
+      }
+    }
+  }
+  return result
+}
+
+// 获取团队成员数据
+const fetchTeamMembers = async (teamId) => {
+  if (!teamId) {
+    teamMembers.value = []
+    return
+  }
+  
+  try {
+    const response = await listMembers(0, 100, undefined, teamId)
+    // 处理返回的数据
+    if (response.content && Array.isArray(response.content) && response.content.length > 0) {
+      const hasNestedChildren = response.content.some(item => 
+        item.children && Array.isArray(item.children) && item.children.length > 0
+      )
+      
+      if (hasNestedChildren) {
+        teamMembers.value = flattenTeamMembersTree(response.content)
+      } else {
+        teamMembers.value = (response.content || []).map(item => {
+          const { children, ...rest } = item
+          return {
+            ...rest,
+            parentId: item.parentId !== undefined ? item.parentId : null
+          }
+        })
+      }
+    } else {
+      teamMembers.value = []
+    }
+  } catch (error) {
+    console.error('获取团队成员失败:', error)
+    teamMembers.value = []
+    toast.add({
+      severity: 'error',
+      summary: '错误',
+      detail: '获取团队成员失败',
+      life: 3000
+    })
+  }
+}
+
+// 为当前用户获取团队成员数据
+const fetchTeamMembersForCurrentUser = async () => {
+  try {
+    const teamId = currentTeamId.value
+    if (teamId) {
+      const response = await listMembers(0, 100, undefined, teamId)
+      // 处理返回的数据
+      if (response.content && Array.isArray(response.content) && response.content.length > 0) {
+        const hasNestedChildren = response.content.some(item => 
+          item.children && Array.isArray(item.children) && item.children.length > 0
+        )
+        
+        if (hasNestedChildren) {
+          teamMembers.value = flattenTeamMembersTree(response.content)
+        } else {
+          teamMembers.value = (response.content || []).map(item => {
+            const { children, ...rest } = item
+            return {
+              ...rest,
+              parentId: item.parentId !== undefined ? item.parentId : null
+            }
+          })
+        }
+      } else {
+        teamMembers.value = []
+      }
+    } else {
+      teamMembers.value = []
+    }
+  } catch (error) {
+    console.error('获取团队成员失败:', error)
+    teamMembers.value = []
+  }
+}
+
+// 为管理员加载所有团队的成员数据
+const loadAllTeamMembers = async () => {
+  try {
+    const allMembers = []
+    // 确保 teamStore.teams 已加载
+    if (teamStore.teams.length === 0) {
+      await teamStore.loadTeams()
+    }
+    for (const team of teamStore.teams) {
+      try {
+        const response = await listMembers(0, 100, undefined, team.id)
+        if (response.content) {
+          const hasNestedChildren = response.content.some(item => 
+            item.children && Array.isArray(item.children) && item.children.length > 0
+          )
+          
+          if (hasNestedChildren) {
+            allMembers.push(...flattenTeamMembersTree(response.content))
+          } else {
+            allMembers.push(...(response.content || []).map(item => {
+              const { children, ...rest } = item
+              return {
+                ...rest,
+                parentId: item.parentId !== undefined ? item.parentId : null
+              }
+            }))
+          }
+        }
+      } catch (err) {
+        console.error(`获取团队 ${team.id} 的成员失败:`, err)
+      }
+    }
+    teamMembers.value = allMembers
+  } catch (error) {
+    console.error('获取所有团队成员失败:', error)
+    teamMembers.value = []
+  }
+}
+
+// 根据落地域名池数据加载需要的团队成员数据
+const loadTeamMembersForPools = async (pools) => {
+  if (!pools || pools.length === 0) return
+  
+  try {
+    // 收集所有需要的 teamId
+    const teamIds = new Set()
+    pools.forEach(pool => {
+      if (pool.teamId) {
+        teamIds.add(pool.teamId)
+      }
+    })
+    
+    // 加载这些团队的成员数据
+    const allMembers = []
+    for (const teamId of teamIds) {
+      try {
+        const response = await listMembers(0, 100, undefined, teamId)
+        if (response.content) {
+          allMembers.push(...response.content)
+        }
+      } catch (err) {
+        console.error(`获取团队 ${teamId} 的成员失败:`, err)
+      }
+    }
+    
+    // 合并到现有的团队成员列表中(避免重复)
+    const existingIds = new Set(teamMembers.value.map(m => m.id))
+    const newMembers = allMembers.filter(m => !existingIds.has(m.id))
+    teamMembers.value.push(...newMembers)
+  } catch (error) {
+    console.error('加载团队成员数据失败:', error)
+  }
+}
+
+// 处理团队选择变化
+const handleTeamChange = async (event) => {
+  const teamId = event.value
+  poolModel.userId = null
+  
+  if (teamId) {
+    await fetchTeamMembers(teamId)
+  } else {
+    teamMembers.value = []
+  }
+}
+
+// 获取绑定用户名
+const getBoundUserName = (pool) => {
+  if (!pool.userId) {
+    return '未绑定'
+  }
+  
+  // 从已加载的团队成员列表中查找
+  // 注意:落地域名池的 userId 对应团队成员表的 userId 字段,不是 id 字段
+  const member = teamMembers.value.find(m => 
+    m.userId === pool.userId || 
+    m.userId === parseInt(pool.userId) ||
+    parseInt(m.userId) === pool.userId
+  )
+  
+  if (member) {
+    return member.name
+  }
+  
+  return '未知用户'
+}
+
+// 计算管理员的分组数据,转换为数组格式
+const adminGroupedList = computed(() => {
+  if (!isAdmin.value) return []
+
+  return Object.keys(adminGroupedData.value).map((teamId) => ({
+    teamId: parseInt(teamId),
+    teamName: getTeamName(parseInt(teamId)),
+    pools: adminGroupedData.value[teamId] || []
+  }))
+})
+
+onMounted(() => {
+  fetchData()
+})
+</script>
+
+<template>
+  <div class="rounded-lg p-4 bg-[var(--p-content-background)]">
+    <!-- 权限检查 -->
+    <div v-if="!canView" class="text-center py-8">
+      <p class="text-gray-500">您没有权限访问落地域名池管理</p>
+    </div>
+
+    <!-- 主要内容 -->
+    <div v-else>
+      <!-- 操作栏 -->
+      <div class="search-toolbar">
+        <div class="toolbar-left">
+          <Button icon="pi pi-refresh" @click="refreshData" size="small" label="刷新" />
+          <Button v-if="canCreate" icon="pi pi-plus" @click="onEdit()" label="添加" size="small" />
+        </div>
+        <div class="toolbar-right">
+          <div class="search-group">
+            <InputText
+              v-model="searchForm.domain"
+              placeholder="域名搜索"
+              size="small"
+              class="search-field"
+              @keyup.enter="handleSearch"
+            />
+            <Select
+              v-if="isAdmin"
+              v-model="searchForm.teamId"
+              :options="teamOptions"
+              optionLabel="label"
+              optionValue="value"
+              placeholder="选择团队"
+              size="small"
+              class="team-field"
+              clearable
+            />
+            <Select
+              v-model="searchForm.domainType"
+              :options="domainTypeOptions"
+              optionLabel="label"
+              optionValue="value"
+              placeholder="域名类型"
+              size="small"
+              class="team-field"
+              clearable
+            />
+          </div>
+          <div class="action-group">
+            <Button icon="pi pi-search" @click="handleSearch" label="搜索" size="small" severity="secondary" />
+            <Button icon="pi pi-refresh" @click="handleRefresh" label="重置" size="small" />
+          </div>
+        </div>
+      </div>
+
+      <!-- 管理员卡片展示 -->
+      <div v-if="isAdmin" class="space-y-6">
+        <div v-for="team in adminGroupedList" :key="team.teamId" class="team-card">
+          <div class="team-header">
+            <h3 class="team-name">{{ team.teamName }}</h3>
+            <span class="domain-count">{{ team.pools.length }} 个域名</span>
+          </div>
+          <div class="domains-grid">
+            <div v-for="pool in team.pools" :key="pool.id" class="domain-card">
+              <div class="domain-info">
+                <div class="domain-name" @click="copyDomain(pool.domain)" :title="'点击复制: ' + pool.domain">
+                  {{ pool.domain }}
+                  <i class="pi pi-copy copy-icon"></i>
+                </div>
+                <div class="domain-description">{{ pool.description || '暂无描述' }}</div>
+                <div class="domain-bound-user">
+                  <span class="bound-label">绑定用户:</span>
+                  <span class="bound-user-name">{{ getBoundUserName(pool) }}</span>
+                </div>
+                <div class="domain-bound-user">
+                  <span class="bound-label">域名类型:</span>
+                  <span class="domain-type-badge" :class="pool.domainType === 'retention' ? 'retention' : 'landing'">
+                    {{ getDomainTypeLabel(pool.domainType) }}
+                  </span>
+                </div>
+                <div class="domain-time">创建时间: {{ formatDate(pool.createdAt) }}</div>
+                <div class="domain-time">更新时间: {{ formatDate(pool.updatedAt) }}</div>
+              </div>
+              <div class="domain-actions">
+                <Button v-if="canUpdate" icon="pi pi-pencil" @click="onEdit(pool)" size="small" text />
+                <Button
+                  v-if="canDelete"
+                  icon="pi pi-trash"
+                  @click="onDelete(pool)"
+                  size="small"
+                  severity="danger"
+                  text
+                />
+              </div>
+            </div>
+          </div>
+        </div>
+
+        <!-- 空数据提示 -->
+        <div v-if="adminGroupedList.length === 0" class="text-center py-8 text-gray-500">暂无落地域名池数据</div>
+      </div>
+
+      <!-- 其他角色表格展示 -->
+      <div v-else>
+        <DataTable
+          ref="tableRef"
+          :value="tableData.data"
+          :paginator="true"
+          paginatorTemplate="CurrentPageReport FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink RowsPerPageDropdown JumpToPageInput"
+          currentPageReportTemplate="{totalRecords} 条记录 "
+          :rows="Number(tableData.meta?.size || 20)"
+          :rowsPerPageOptions="[10, 20, 50, 100]"
+          :totalRecords="tableData.meta?.total || 0"
+          @page="handlePageChange"
+          lazy
+          scrollable
+          stripedRows
+          showGridlines
+        >
+          <Column field="domain" header="域名" style="min-width: 200px" headerClass="font-bold">
+            <template #body="slotProps">
+              <span
+                class="domain-link cursor-pointer text-blue-600 hover:text-blue-800"
+                @click="copyDomain(slotProps.data.domain)"
+                :title="'点击复制: ' + slotProps.data.domain"
+              >
+                {{ slotProps.data.domain }}
+              </span>
+            </template>
+          </Column>
+          <Column field="description" header="描述" style="min-width: 250px" headerClass="font-bold">
+            <template #body="slotProps">
+              <div class="max-w-xl truncate">
+                {{ slotProps.data.description || '-' }}
+              </div>
+            </template>
+          </Column>
+          <Column field="boundUser" header="绑定用户" style="min-width: 150px" headerClass="font-bold">
+            <template #body="slotProps">
+              <span class="bound-user-text">
+                {{ getBoundUserName(slotProps.data) }}
+              </span>
+            </template>
+          </Column>
+          <Column field="domainType" header="域名类型" style="min-width: 120px" headerClass="font-bold">
+            <template #body="slotProps">
+              <span class="domain-type-badge" :class="slotProps.data.domainType === 'retention' ? 'retention' : 'landing'">
+                {{ getDomainTypeLabel(slotProps.data.domainType) }}
+              </span>
+            </template>
+          </Column>
+          <Column field="createdAt" header="创建时间" style="min-width: 150px" headerClass="font-bold">
+            <template #body="slotProps">
+              {{ formatDate(slotProps.data.createdAt) }}
+            </template>
+          </Column>
+          <Column field="updatedAt" header="更新时间" style="min-width: 150px" headerClass="font-bold">
+            <template #body="slotProps">
+              {{ formatDate(slotProps.data.updatedAt) }}
+            </template>
+          </Column>
+          <Column header="操作" style="min-width: 120px" headerClass="font-bold" align="center">
+            <template #body="slotProps">
+              <div class="flex gap-2 justify-center">
+                <Button v-if="canUpdate" icon="pi pi-pencil" @click="onEdit(slotProps.data)" size="small" />
+                <Button
+                  v-if="canDelete"
+                  icon="pi pi-trash"
+                  @click="onDelete(slotProps.data)"
+                  size="small"
+                  severity="danger"
+                />
+              </div>
+            </template>
+          </Column>
+        </DataTable>
+      </div>
+
+      <!-- 添加/编辑落地域名池对话框 -->
+      <Dialog
+        v-model:visible="dialogVisible"
+        :modal="true"
+        :header="isEditing ? '编辑落地域名池' : '添加落地域名池'"
+        :style="{ width: '550px' }"
+      >
+        <div class="grid grid-cols-1 gap-4 p-4">
+          <!-- 管理员选择团队 -->
+          <div v-if="isAdmin" class="flex flex-col gap-2">
+            <label class="font-medium">选择团队</label>
+            <Select
+              v-model="poolModel.teamId"
+              :options="teamOptions"
+              optionLabel="label"
+              optionValue="value"
+              placeholder="请选择团队"
+              :disabled="isEditing"
+              :showClear="true"
+              @change="handleTeamChange"
+            />
+            <small v-if="!poolModel.teamId" class="text-red-500">请选择团队</small>
+          </div>
+
+          <!-- 选择绑定用户 -->
+          <div v-if="poolModel.teamId || (!isAdmin && currentTeamId)" class="flex flex-col gap-2">
+            <label class="font-medium">绑定用户(可选)</label>
+            <Select
+              v-model="poolModel.userId"
+              :options="teamMemberOptions"
+              optionLabel="label"
+              optionValue="value"
+              placeholder="选择团队成员(可选)"
+              :showClear="true"
+            />
+            <small class="text-gray-500">不选择则只绑定到团队</small>
+          </div>
+
+          <!-- 选择域名类型 -->
+          <div class="flex flex-col gap-2">
+            <label class="font-medium">域名类型</label>
+            <Select
+              v-model="poolModel.domainType"
+              :options="domainTypeOptions.filter(opt => opt.value !== null)"
+              optionLabel="label"
+              optionValue="value"
+              placeholder="选择域名类型"
+            />
+            <small class="text-gray-500">选择域名的类型:落地域名或留存域名</small>
+          </div>
+
+          <div class="flex flex-col gap-2">
+            <label class="font-medium">域名</label>
+            <Textarea
+              v-model="poolModel.domain"
+              :placeholder="isEditing ? '请输入域名' : '请输入域名,多个域名可用逗号、分号或换行分隔'"
+              rows="4"
+            />
+            <small v-if="!poolModel.domain" class="text-red-500">请输入域名</small>
+            <small v-else class="text-gray-500">
+              {{ isEditing ? '编辑时只能修改单个域名' : '支持分隔符:,(逗号)、;(分号)、换行' }}
+            </small>
+          </div>
+
+          <div class="flex flex-col gap-2">
+            <label class="font-medium">描述</label>
+            <Textarea v-model="poolModel.description" placeholder="请输入描述信息(可选)" rows="3" />
+          </div>
+        </div>
+        <template #footer>
+          <Button label="取消" icon="pi pi-times" @click="onCancel" class="p-button-text" />
+          <Button label="保存" icon="pi pi-check" @click="onSubmit" />
+        </template>
+      </Dialog>
+    </div>
+  </div>
+</template>
+
+<style scoped>
+/* 管理员卡片样式 */
+.team-card {
+  background: white;
+  border-radius: 12px;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+  overflow: hidden;
+  transition: box-shadow 0.3s ease;
+}
+
+.team-card:hover {
+  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
+}
+
+.team-header {
+  background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%);
+  color: #475569;
+  padding: 16px 20px;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.team-name {
+  font-size: 18px;
+  font-weight: 600;
+  margin: 0;
+}
+
+.domain-count {
+  background: rgba(71, 85, 105, 0.1);
+  color: #475569;
+  padding: 4px 12px;
+  border-radius: 20px;
+  font-size: 14px;
+  font-weight: 500;
+}
+
+.domains-grid {
+  padding: 20px;
+  display: grid;
+  grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
+  gap: 16px;
+}
+
+.domain-card {
+  background: #f8fafc;
+  border: 1px solid #e2e8f0;
+  border-radius: 8px;
+  padding: 16px;
+  display: flex;
+  flex-direction: column;
+  transition: all 0.3s ease;
+  min-height: 150px;
+}
+
+.domain-card:hover {
+  background: #f1f5f9;
+  border-color: #cbd5e1;
+  transform: translateY(-2px);
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+}
+
+.domain-info {
+  flex: 1;
+  margin-bottom: 12px;
+}
+
+.domain-name {
+  font-size: 16px;
+  font-weight: 600;
+  color: #1e40af;
+  margin-bottom: 8px;
+  background: #dbeafe;
+  padding: 8px 12px;
+  border-radius: 6px;
+  cursor: pointer;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  transition: all 0.3s ease;
+  border: 1px solid #bfdbfe;
+}
+
+.domain-name:hover {
+  background: #bfdbfe;
+  border-color: #93c5fd;
+  transform: translateY(-1px);
+  box-shadow: 0 2px 8px rgba(37, 99, 235, 0.15);
+}
+
+.copy-icon {
+  font-size: 14px;
+  opacity: 0.6;
+  transition: opacity 0.3s ease;
+}
+
+.domain-name:hover .copy-icon {
+  opacity: 1;
+}
+
+.domain-description {
+  font-size: 14px;
+  color: #475569;
+  margin-bottom: 8px;
+  line-height: 1.4;
+  background: #f1f5f9;
+  padding: 12px;
+  border-radius: 6px;
+  min-height: 40px;
+  display: flex;
+  align-items: flex-start;
+}
+
+.domain-bound-user {
+  font-size: 13px;
+  color: #475569;
+  margin-bottom: 8px;
+  display: flex;
+  align-items: center;
+  gap: 6px;
+}
+
+.bound-label {
+  font-weight: 500;
+  color: #64748b;
+}
+
+.bound-user-name {
+  font-weight: 600;
+  color: #1e40af;
+  background: #dbeafe;
+  padding: 2px 8px;
+  border-radius: 4px;
+  font-size: 12px;
+}
+
+.domain-time {
+  font-size: 12px;
+  color: #94a3b8;
+  margin-top: 4px;
+}
+
+.domain-actions {
+  display: flex;
+  gap: 8px;
+  justify-content: flex-end;
+  margin-top: auto;
+}
+
+.domain-link {
+  text-decoration: underline;
+}
+
+.bound-user-text {
+  color: #1e40af;
+  font-weight: 500;
+  background: #dbeafe;
+  padding: 4px 8px;
+  border-radius: 4px;
+  font-size: 12px;
+}
+
+.domain-type-badge {
+  display: inline-block;
+  font-weight: 500;
+  padding: 4px 10px;
+  border-radius: 12px;
+  font-size: 12px;
+}
+
+.domain-type-badge.landing {
+  color: #059669;
+  background: #d1fae5;
+  border: 1px solid #6ee7b7;
+}
+
+.domain-type-badge.retention {
+  color: #dc2626;
+  background: #fee2e2;
+  border: 1px solid #fca5a5;
+}
+
+/* 搜索工具栏样式 */
+.search-toolbar {
+  display: flex;
+  flex-wrap: wrap;
+  align-items: center;
+  justify-content: space-between;
+  gap: 16px;
+  margin-bottom: 20px;
+  padding: 16px;
+  background: #f8fafc;
+  border-radius: 8px;
+  border: 1px solid #e2e8f0;
+}
+
+.toolbar-left {
+  display: flex;
+  gap: 8px;
+  align-items: center;
+}
+
+.toolbar-left .p-button {
+  font-size: 13px;
+  padding: 6px 12px;
+  border-radius: 6px;
+  font-weight: 500;
+}
+
+.toolbar-right {
+  display: flex;
+  gap: 12px;
+  align-items: center;
+  flex-wrap: wrap;
+}
+
+.search-group {
+  display: flex;
+  gap: 8px;
+  align-items: center;
+}
+
+.search-field {
+  width: 140px;
+  font-size: 13px;
+  padding: 6px 10px;
+  border-radius: 6px;
+  border: 1px solid #d1d5db;
+  transition: all 0.2s ease;
+}
+
+.search-field:focus {
+  border-color: #3b82f6;
+  box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
+}
+
+.team-field {
+  width: 160px;
+  font-size: 13px;
+}
+
+.team-field .p-select {
+  border-radius: 6px;
+}
+
+.action-group {
+  display: flex;
+  gap: 8px;
+  align-items: center;
+}
+
+.action-group .p-button {
+  font-size: 13px;
+  padding: 6px 12px;
+  border-radius: 6px;
+  font-weight: 500;
+}
+
+/* 响应式设计 */
+@media (max-width: 768px) {
+  .domains-grid {
+    grid-template-columns: 1fr;
+  }
+
+  .team-header {
+    flex-direction: column;
+    gap: 8px;
+    text-align: center;
+  }
+
+  .domain-card {
+    flex-direction: column;
+  }
+
+  .domain-actions {
+    position: static;
+    justify-content: center;
+    margin-top: 8px;
+  }
+
+  /* 移动端搜索工具栏适配 */
+  .search-toolbar {
+    flex-direction: column;
+    gap: 12px;
+    align-items: stretch;
+    padding: 12px;
+  }
+
+  .toolbar-left {
+    justify-content: center;
+    gap: 8px;
+  }
+
+  .toolbar-left .p-button {
+    flex: 1;
+    max-width: 120px;
+    font-size: 13px;
+    padding: 8px 12px;
+  }
+
+  .toolbar-right {
+    flex-direction: column;
+    gap: 12px;
+    align-items: stretch;
+  }
+
+  .search-group {
+    flex-direction: column;
+    gap: 8px;
+  }
+
+  .search-field,
+  .team-field {
+    width: 100%;
+    font-size: 14px;
+    padding: 10px 12px;
+  }
+
+  .action-group {
+    justify-content: center;
+    gap: 8px;
+  }
+
+  .action-group .p-button {
+    flex: 1;
+    max-width: 140px;
+    font-size: 13px;
+    padding: 8px 12px;
+  }
+}
+</style>
+

+ 71 - 2
src/views/LinkView.vue

@@ -789,6 +789,35 @@ const openEditDialog = (link) => {
   editDialog.value = true
 }
 
+// 扁平化树状结构数据
+const flattenTeamMembersTree = (nodes, result = [], parentId = null) => {
+  if (!nodes || !Array.isArray(nodes)) return result
+  for (const node of nodes) {
+    // 跳过团队节点(type === 'team' 或 id === 0),只保留团队成员
+    if (node.type === 'teamMember' || (node.type !== 'team' && node.id !== 0 && node.id !== null)) {
+      // 创建扁平化的成员对象,移除children字段,添加parentId
+      const flattenedNode = {
+        ...node,
+        parentId: node.parentId !== undefined ? node.parentId : parentId
+      }
+      // 移除children字段
+      delete flattenedNode.children
+      result.push(flattenedNode)
+      
+      // 递归处理子节点,传递当前节点的id作为parentId
+      if (node.children && Array.isArray(node.children) && node.children.length > 0) {
+        flattenTeamMembersTree(node.children, result, node.id)
+      }
+    } else {
+      // 如果是团队节点,也需要递归处理其子节点
+      if (node.children && Array.isArray(node.children) && node.children.length > 0) {
+        flattenTeamMembersTree(node.children, result, parentId)
+      }
+    }
+  }
+  return result
+}
+
 // 获取团队成员列表
 const fetchTeamMembers = async () => {
   try {
@@ -808,7 +837,27 @@ const fetchTeamMembers = async () => {
       // 其他情况获取所有成员
       response = await listMembers(0, 1000)
     }
-    teamMembers.value = response.content || []
+    
+    // 处理返回的数据
+    if (response.content && Array.isArray(response.content) && response.content.length > 0) {
+      const hasNestedChildren = response.content.some(item => 
+        item.children && Array.isArray(item.children) && item.children.length > 0
+      )
+      
+      if (hasNestedChildren) {
+        teamMembers.value = flattenTeamMembersTree(response.content)
+      } else {
+        teamMembers.value = (response.content || []).map(item => {
+          const { children, ...rest } = item
+          return {
+            ...rest,
+            parentId: item.parentId !== undefined ? item.parentId : null
+          }
+        })
+      }
+    } else {
+      teamMembers.value = []
+    }
   } catch (error) {
     console.error('获取团队成员失败:', error)
     teamMembers.value = []
@@ -974,7 +1023,27 @@ const fetchGenerateTeamMembers = async () => {
       // 其他情况获取所有成员
       response = await listMembers(0, 1000)
     }
-    generateTeamMembers.value = response.content || []
+    
+    // 处理返回的数据
+    if (response.content && Array.isArray(response.content) && response.content.length > 0) {
+      const hasNestedChildren = response.content.some(item => 
+        item.children && Array.isArray(item.children) && item.children.length > 0
+      )
+      
+      if (hasNestedChildren) {
+        generateTeamMembers.value = flattenTeamMembersTree(response.content)
+      } else {
+        generateTeamMembers.value = (response.content || []).map(item => {
+          const { children, ...rest } = item
+          return {
+            ...rest,
+            parentId: item.parentId !== undefined ? item.parentId : null
+          }
+        })
+      }
+    } else {
+      generateTeamMembers.value = []
+    }
   } catch (error) {
     console.error('获取团队成员失败:', error)
     generateTeamMembers.value = []

+ 1 - 1
src/views/LoginView.vue

@@ -29,7 +29,7 @@ const loginForm = ref({
 const resolver = zodResolver(
   z.object({
     name: z.string().min(1, { message: '用户名不能为空' }),
-    password: z.string().min(8, { message: '密码至少8位' })
+    password: z.string().min(1, { message: '密码不能为空' })
   })
 )
 

+ 6 - 0
src/views/MainView.vue

@@ -63,6 +63,12 @@ const allNavItems = [
         icon: 'pi pi-fw pi-sliders-h',
         name: 'team-config',
         roles: ['admin', 'team']
+      },
+      {
+        label: '落地域名池',
+        icon: 'pi pi-fw pi-server',
+        name: 'landing-domain-pool',
+        roles: ['admin', 'team']
       }
     ]
   },

+ 37 - 2
src/views/SysConfigView.vue

@@ -1,5 +1,5 @@
 <script setup>
-import { ref, onMounted, reactive, computed } from 'vue'
+import { ref, onMounted, reactive, computed, inject } from 'vue'
 import {
   listSysConfig,
   createSysConfig,
@@ -23,9 +23,14 @@ import Textarea from 'primevue/textarea'
 import { useDateFormat } from '@vueuse/core'
 import RadioButton from 'primevue/radiobutton'
 import Slider from 'primevue/slider'
+import { useTeamStore } from '@/stores/team'
 
 const toast = useToast()
 const confirm = useConfirm()
+const teamStore = useTeamStore()
+
+// 注入权限信息
+const isAdmin = inject('isAdmin')
 const tableRef = ref(null)
 const query = ref({})
 const tableData = ref({
@@ -430,6 +435,14 @@ const uploadFile = async () => {
   input.click()
 }
 
+// 获取团队名称
+const getTeamName = (teamId) => {
+  if (teamId === 0) return '默认配置'
+  if (!teamId) return '-'
+  const team = teamStore.teams.find((t) => t.id === teamId)
+  return team ? team.name : '-'
+}
+
 onMounted(() => {
   fetchData()
   fetchConfigTypes()
@@ -438,7 +451,14 @@ onMounted(() => {
 
 <template>
   <div class="rounded-lg p-4 bg-[var(--p-content-background)]">
-    <DataTable
+    <!-- 权限检查 -->
+    <div v-if="!isAdmin" class="text-center py-8">
+      <p class="text-gray-500">您没有权限访问系统配置</p>
+    </div>
+
+    <!-- 主要内容 -->
+    <div v-else>
+      <DataTable
       ref="tableRef"
       :value="tableData.data"
       :paginator="true"
@@ -464,6 +484,13 @@ onMounted(() => {
 
       <Column field="id" header="ID" style="min-width: 100px" headerClass="font-bold"></Column>
       <Column field="name" header="配置名称" style="min-width: 150px" headerClass="font-bold"></Column>
+      <Column field="teamId" header="所属团队" style="min-width: 120px" headerClass="font-bold">
+        <template #body="slotProps">
+          <span class="team-name-text font-medium">
+            {{ getTeamName(slotProps.data.teamId) }}
+          </span>
+        </template>
+      </Column>
       <Column field="remark" header="备注" style="min-width: 200px" headerClass="font-bold"></Column>
       <Column field="value" header="配置值" style="min-width: 250px" headerClass="font-bold">
         <template #body="slotProps">
@@ -628,5 +655,13 @@ onMounted(() => {
         <Button label="保存" icon="pi pi-check" @click="onSubmit" autofocus />
       </template>
     </Dialog>
+    </div>
   </div>
 </template>
+
+<style scoped>
+.team-name-text {
+  color: #7c3aed;
+  font-weight: 500;
+}
+</style>

+ 5 - 3
src/views/TeamConfigView.vue

@@ -301,8 +301,8 @@ const validateForm = () => {
     }
   }
 
-  // 管理员必须指定teamId
-  if (isAdmin.value && !configModel.teamId) {
+  // 管理员必须指定teamId(0表示默认配置,也是有效的)
+  if (isAdmin.value && (configModel.teamId === null || configModel.teamId === undefined)) {
     toast.add({ severity: 'warn', summary: '警告', detail: '请选择团队', life: 3000 })
     return false
   }
@@ -508,6 +508,7 @@ const teamOptions = computed(() => {
   if (!isAdmin.value) return []
   return [
     { label: '全部团队', value: null },
+    { label: '默认配置', value: 0 },
     ...teamStore.teams.map((team) => ({
       label: team.name,
       value: team.id
@@ -517,6 +518,7 @@ const teamOptions = computed(() => {
 
 // 获取团队名称
 const getTeamName = (teamId) => {
+  if (teamId === 0) return '默认配置'
   if (!teamId) return '-'
   const team = teamStore.teams.find((t) => t.id === teamId)
   return team ? team.name : '-'
@@ -658,7 +660,7 @@ onMounted(() => {
               filterPlaceholder="搜索团队"
               :showClear="true"
             />
-            <small v-if="!configModel.teamId" class="p-error">请选择团队</small>
+            <small v-if="configModel.teamId === null || configModel.teamId === undefined" class="p-error">请选择团队</small>
           </div>
 
           <div class="flex flex-col gap-2">

+ 389 - 69
src/views/TeamMembersView.vue

@@ -1,18 +1,13 @@
 <template>
-  <div class="rounded-lg p-4 bg-[var(--p-content-background)]">
-
-    <DataTable
-      :value="tableData.content"
-      :paginator="true"
-      paginatorTemplate="CurrentPageReport FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink RowsPerPageDropdown JumpToPageInput"
-      currentPageReportTemplate="{totalRecords} 条记录 "
-      :rows="tableData.metadata.size"
-      :rowsPerPageOptions="[10, 20, 50, 100]"
-      :totalRecords="tableData.metadata.total"
-      @page="handlePageChange"
-      lazy
+  <div class="team-members-container">
+    <TreeTable
+      :value="treeData"
+      :expandedKeys="expandedKeys"
+      @node-toggle="onNodeToggle"
       scrollable
+      :scrollHeight="tableScrollHeight"
       class="members-table"
+      :loading="loading"
     >
       <template #header>
         <div class="flex flex-wrap items-center gap-2">
@@ -55,26 +50,26 @@
         </div>
       </template>
 
-      <Column field="id" header="ID" style="width: 80px" frozen>
+      <Column field="id" header="ID" style="width: 80px">
         <template #body="slotProps">
           <span
             class="font-mono text-sm copyable-text"
-            :title="slotProps.data.id"
-            @click="copyToClipboard(slotProps.data.id)"
+            :title="slotProps.node.id"
+            @click="copyToClipboard(slotProps.node.id)"
           >
-            {{ slotProps.data.id }}
+            {{ slotProps.node.id }}
           </span>
         </template>
       </Column>
 
-      <Column field="name" header="成员名称" style="min-width: 150px; max-width: 200px">
+      <Column field="name" header="成员名称" style="min-width: 150px; max-width: 200px" expander>
         <template #body="slotProps">
           <span
             class="font-medium member-name-text copyable-text"
-            :title="slotProps.data.name"
-            @click="copyToClipboard(slotProps.data.name)"
+            :title="slotProps.node.name"
+            @click="copyToClipboard(slotProps.node.name)"
           >
-            {{ slotProps.data.name }}
+            {{ slotProps.node.name }}
           </span>
         </template>
       </Column>
@@ -82,7 +77,7 @@
       <Column v-if="isAdmin" field="teamId" header="所属团队" style="min-width: 120px">
         <template #body="slotProps">
           <span class="team-name-text font-medium">
-            {{ getTeamName(slotProps.data.teamId) }}
+            {{ getTeamName(slotProps.node.teamId) }}
           </span>
         </template>
       </Column>
@@ -90,7 +85,19 @@
       <Column field="commissionRate" header="分成比例" style="min-width: 120px">
         <template #body="slotProps">
           <span class="commission-rate-text font-semibold">
-            {{ slotProps.data.commissionRate || 0 }}%
+            {{ slotProps.node.commissionRate || 0 }}%
+          </span>
+        </template>
+      </Column>
+
+      <Column field="parentId" header="父级成员ID" style="min-width: 120px">
+        <template #body="slotProps">
+          <span v-if="slotProps.node.parentId" class="parent-id-text font-medium text-gray-600">
+            {{ slotProps.node.parentId }}
+            <span class="text-xs text-gray-400 ml-1">(下级)</span>
+          </span>
+          <span v-else class="text-gray-400 text-sm">
+            二级代理
           </span>
         </template>
       </Column>
@@ -99,7 +106,7 @@
       <Column v-if="isTeam" field="totalRevenue" header="总收入" style="min-width: 120px">
         <template #body="slotProps">
           <span class="total-revenue-text font-semibold text-blue-600">
-            ¥{{ formatAmount(getMemberTotalRevenue(slotProps.data.id)) }}
+            ¥{{ formatAmount(getMemberTotalRevenue(slotProps.node.id)) }}
           </span>
         </template>
       </Column>
@@ -107,7 +114,7 @@
       <Column v-if="isTeam" field="todayRevenue" header="今日收入" style="min-width: 120px">
         <template #body="slotProps">
           <span class="today-revenue-text font-semibold text-green-600">
-            ¥{{ formatAmount(getMemberTodayRevenue(slotProps.data.id)) }}
+            ¥{{ formatAmount(getMemberTodayRevenue(slotProps.node.id)) }}
           </span>
         </template>
       </Column>
@@ -116,7 +123,7 @@
       <Column v-if="isTeam" field="totalSales" header="总销售额" style="min-width: 120px">
         <template #body="slotProps">
           <span class="total-sales-text font-semibold text-purple-600">
-            ¥{{ formatAmount(getMemberTotalSales(slotProps.data.id)) }}
+            ¥{{ formatAmount(getMemberTotalSales(slotProps.node.id)) }}
           </span>
         </template>
       </Column>
@@ -124,7 +131,7 @@
       <Column v-if="isTeam" field="todaySales" header="今日销售额" style="min-width: 120px">
         <template #body="slotProps">
           <span class="today-sales-text font-semibold text-orange-600">
-            ¥{{ formatAmount(getMemberTodaySales(slotProps.data.id)) }}
+            ¥{{ formatAmount(getMemberTodaySales(slotProps.node.id)) }}
           </span>
         </template>
       </Column>
@@ -133,11 +140,11 @@
       <Column v-if="isTeam" field="actualIncome" header="队长收入(比例)" style="min-width: 150px">
         <template #body="slotProps">
           <div class="flex flex-col">
-            <span v-if="isTeamLeader(slotProps.data.id)" class="team-leader-income-text font-semibold text-red-600">
-              ¥{{ formatAmount(getTeamLeaderTotalIncome(slotProps.data.id)) }}({{ getMemberActualRate(slotProps.data.id) }}%)
+            <span v-if="isTeamLeader(slotProps.node.id)" class="team-leader-income-text font-semibold text-red-600">
+              ¥{{ formatAmount(getTeamLeaderTotalIncome(slotProps.node.id)) }}({{ getMemberActualRate(slotProps.node.id) }}%)
             </span>
             <span v-else class="actual-rate-text font-semibold text-blue-600">
-              ¥{{ formatAmount(getMemberTotalRevenue(slotProps.data.id)) }}({{ getMemberActualRate(slotProps.data.id) }}%)
+              ¥{{ formatAmount(getMemberTotalRevenue(slotProps.node.id)) }}({{ getMemberActualRate(slotProps.node.id) }}%)
             </span>
           </div>
         </template>
@@ -147,11 +154,11 @@
       <Column v-if="isTeam" field="todayActualIncome" header="队长今日收入(比例)" style="min-width: 150px">
         <template #body="slotProps">
           <div class="flex flex-col">
-            <span v-if="isTeamLeader(slotProps.data.id)" class="team-leader-today-income-text font-semibold text-red-600">
-              ¥{{ formatAmount(getTeamLeaderTodayIncome(slotProps.data.id)) }}({{ getMemberActualRate(slotProps.data.id) }}%)
+            <span v-if="isTeamLeader(slotProps.node.id)" class="team-leader-today-income-text font-semibold text-red-600">
+              ¥{{ formatAmount(getTeamLeaderTodayIncome(slotProps.node.id)) }}({{ getMemberActualRate(slotProps.node.id) }}%)
             </span>
             <span v-else class="today-revenue-text font-semibold text-green-600">
-              ¥{{ formatAmount(getMemberTodayRevenue(slotProps.data.id)) }}({{ getMemberActualRate(slotProps.data.id) }}%)
+              ¥{{ formatAmount(getMemberTodayRevenue(slotProps.node.id)) }}({{ getMemberActualRate(slotProps.node.id) }}%)
             </span>
           </div>
         </template>
@@ -160,7 +167,7 @@
       <Column field="createdAt" header="创建时间" style="min-width: 150px">
         <template #body="slotProps">
           <span class="text-sm">
-            {{ formatDateTime(slotProps.data.createdAt) }}
+            {{ formatDateTime(slotProps.node.createdAt) }}
           </span>
         </template>
       </Column>
@@ -168,8 +175,6 @@
       <Column
         header="操作"
         style="min-width: 280px; width: 280px"
-        align-frozen="right"
-        frozen
         :pt="{
           columnHeaderContent: {
             class: 'justify-center'
@@ -186,7 +191,7 @@
               rounded
               aria-label="生成链接"
               title="生成推广链接"
-              @click="handleGenerateLink(slotProps.data)"
+              @click="handleGenerateLink(slotProps.node)"
             />
             <Button
               icon="pi pi-qrcode"
@@ -196,7 +201,7 @@
               rounded
               aria-label="生成推广码"
               title="生成推广码"
-              @click="handleGeneratePromoCode(slotProps.data)"
+              @click="handleGeneratePromoCode(slotProps.node)"
             />
             <Button
               icon="pi pi-pencil"
@@ -205,7 +210,7 @@
               text
               rounded
               aria-label="编辑"
-              @click="openEditDialog(slotProps.data)"
+              @click="openEditDialog(slotProps.node)"
             />
             <Button
               icon="pi pi-trash"
@@ -214,12 +219,12 @@
               text
               rounded
               aria-label="删除"
-              @click="confirmDelete(slotProps.data)"
+              @click="confirmDelete(slotProps.node)"
             />
           </div>
         </template>
       </Column>
-    </DataTable>
+    </TreeTable>
 
     <!-- 新增/编辑弹窗 -->
     <Dialog
@@ -246,6 +251,25 @@
             placeholder="请选择团队"
             class="w-full"
             showClear
+            @change="handleTeamIdChange"
+          />
+        </div>
+
+        <div v-if="editForm.teamId" class="field mt-4">
+          <label for="edit-memberId" class="font-medium text-sm mb-2 block">
+            父级团队成员(可选)
+            <span v-if="!isEdit" class="text-gray-500 text-xs ml-1">不选择则创建二级代理</span>
+            <span v-else class="text-gray-500 text-xs ml-1">不选择则为二级代理</span>
+          </label>
+          <Select
+            id="edit-memberId"
+            v-model="editForm.memberId"
+            :options="parentMemberOptions"
+            optionLabel="label"
+            optionValue="value"
+            :placeholder="isEdit ? '请选择父级团队成员(可选,清空则为二级代理)' : '请选择父级团队成员(可选)'"
+            class="w-full"
+            showClear
           />
         </div>
 
@@ -350,7 +374,7 @@
 import { ref, onMounted, computed, inject } from 'vue'
 import Button from 'primevue/button'
 import Column from 'primevue/column'
-import DataTable from 'primevue/datatable'
+import TreeTable from 'primevue/treetable'
 import Dialog from 'primevue/dialog'
 import Select from 'primevue/select'
 import InputText from 'primevue/inputtext'
@@ -398,7 +422,14 @@ const getTeamUserId = (teamId) => {
   return team ? team.userId : null
 }
 
-// 表格数据
+// 获取父级成员名称
+const getParentMemberName = (parentId) => {
+  if (!parentId) return '-'
+  const parent = allTeamMembers.value.find(m => m.id === parentId)
+  return parent ? parent.name : `ID: ${parentId}`
+}
+
+// 表格数据(保留用于分页等元数据)
 const tableData = ref({
   content: [],
   metadata: {
@@ -408,6 +439,18 @@ const tableData = ref({
   }
 })
 
+// 树形数据
+const treeData = ref([])
+
+// 展开的节点keys
+const expandedKeys = ref({})
+
+// 表格滚动高度(动态计算)
+const tableScrollHeight = computed(() => {
+  // 计算可用高度:视口高度 - header(60px) - 主内容padding(32px) - 容器padding(24px) - 搜索栏和按钮区域(约100px)
+  return 'calc(100vh - 216px)'
+})
+
 // 加载状态
 const loading = ref(false)
 const statsLoading = ref(false)
@@ -423,9 +466,15 @@ const editForm = ref({
   id: null,
   name: null,
   teamId: null,
-  commissionRate: null
+  memberId: null,  // 父级团队成员ID(可选)
+  commissionRate: null,
+  password: null,
+  confirmPassword: null
 })
 
+// 所有团队成员列表(用于选择父级成员)
+const allTeamMembers = ref([])
+
 // 推广码/链接弹窗相关
 const promoDialog = ref(false)
 const promoDialogTitle = ref('')
@@ -463,73 +512,211 @@ const formatDateTime = (dateString) => {
 }
 
 
+// 递归查找树形结构中的成员
+const findMemberInTree = (nodes, memberId) => {
+  if (!nodes || !Array.isArray(nodes)) return null
+  
+  for (const node of nodes) {
+    // 使用 id 字段匹配(API返回的数据使用 id 而不是 memberId)
+    if (node.id === memberId) {
+      return node
+    }
+    // 递归查找子节点
+    if (node.children && Array.isArray(node.children) && node.children.length > 0) {
+      const found = findMemberInTree(node.children, memberId)
+      if (found) return found
+    }
+  }
+  return null
+}
+
 // 获取成员的实际比例
 const getMemberActualRate = (memberId) => {
   if (!teamStats.value?.membersStats) return 0
-  const member = teamStats.value.membersStats.find(m => m.memberId === memberId)
+  const member = findMemberInTree(teamStats.value.membersStats, memberId)
   return member?.actualRate || 0
 }
 
 // 获取成员的总收入
 const getMemberTotalRevenue = (memberId) => {
   if (!teamStats.value?.membersStats) return 0
-  const member = teamStats.value.membersStats.find(m => m.memberId === memberId)
+  const member = findMemberInTree(teamStats.value.membersStats, memberId)
   return member?.totalRevenue || 0
 }
 
 // 获取成员的今日收入
 const getMemberTodayRevenue = (memberId) => {
   if (!teamStats.value?.membersStats) return 0
-  const member = teamStats.value.membersStats.find(m => m.memberId === memberId)
+  const member = findMemberInTree(teamStats.value.membersStats, memberId)
   return member?.todayRevenue || 0
 }
 
 // 获取成员的总销售额
 const getMemberTotalSales = (memberId) => {
   if (!teamStats.value?.membersStats) return 0
-  const member = teamStats.value.membersStats.find(m => m.memberId === memberId)
+  const member = findMemberInTree(teamStats.value.membersStats, memberId)
   return member?.totalSales || 0
 }
 
 // 获取成员的今日销售额
 const getMemberTodaySales = (memberId) => {
   if (!teamStats.value?.membersStats) return 0
-  const member = teamStats.value.membersStats.find(m => m.memberId === memberId)
+  const member = findMemberInTree(teamStats.value.membersStats, memberId)
   return member?.todaySales || 0
 }
 
 // 判断是否为队长
 const isTeamLeader = (memberId) => {
   if (!teamStats.value?.membersStats) return false
-  const member = teamStats.value.membersStats.find(m => m.memberId === memberId)
+  const member = findMemberInTree(teamStats.value.membersStats, memberId)
   return member?.teamLeaderTotalIncome !== undefined || member?.teamLeaderTodayIncome !== undefined
 }
 
 // 获取队长的实际总收入
 const getTeamLeaderTotalIncome = (memberId) => {
   if (!teamStats.value?.membersStats) return 0
-  const member = teamStats.value.membersStats.find(m => m.memberId === memberId)
+  const member = findMemberInTree(teamStats.value.membersStats, memberId)
   return member?.teamLeaderTotalIncome || 0
 }
 
 // 获取队长的实际今日收入
 const getTeamLeaderTodayIncome = (memberId) => {
   if (!teamStats.value?.membersStats) return 0
-  const member = teamStats.value.membersStats.find(m => m.memberId === memberId)
+  const member = findMemberInTree(teamStats.value.membersStats, memberId)
   return member?.teamLeaderTodayIncome || 0
 }
 
+// 处理树状数据,保留树形结构用于TreeTable展示
+// 过滤掉团队节点,只保留团队成员,但保留children结构
+const processTreeData = (nodes) => {
+  if (!nodes || !Array.isArray(nodes)) return []
+  
+  const result = []
+  for (const node of nodes) {
+    // 跳过团队节点(type === 'team' 或 id === 0),只保留团队成员
+    if (node.type === 'teamMember' || (node.type !== 'team' && node.id !== 0 && node.id !== null)) {
+      // TreeTable 需要的数据结构:直接使用节点数据,children 字段会自动处理
+      const processedNode = {
+        key: String(node.id),  // TreeTable需要key字段(字符串)
+        ...node,  // 保留所有原始数据
+        children: undefined // 先设为undefined
+      }
+      
+      // 递归处理子节点
+      if (node.children && Array.isArray(node.children) && node.children.length > 0) {
+        const processedChildren = processTreeData(node.children)
+        if (processedChildren.length > 0) {
+          processedNode.children = processedChildren
+        }
+      }
+      
+      result.push(processedNode)
+    } else {
+      // 如果是团队节点,递归处理其子节点
+      if (node.children && Array.isArray(node.children) && node.children.length > 0) {
+        const processedChildren = processTreeData(node.children)
+        result.push(...processedChildren)
+      }
+    }
+  }
+  return result
+}
+
+// 获取所有团队成员(用于父级成员选择)
+const fetchAllTeamMembers = async (teamId) => {
+  try {
+    const response = await listMembers(0, 1000, undefined, teamId)
+    if (response.content && Array.isArray(response.content) && response.content.length > 0) {
+      // 检查是否有嵌套的children结构
+      const hasNestedChildren = response.content.some(item => 
+        item.children && Array.isArray(item.children) && item.children.length > 0
+      )
+      
+      if (hasNestedChildren) {
+        // 扁平化树状结构(用于选择父级成员)
+        const flattenTree = (nodes, result = []) => {
+          if (!nodes || !Array.isArray(nodes)) return result
+          for (const node of nodes) {
+            if (node.type === 'teamMember' || (node.type !== 'team' && node.id !== 0 && node.id !== null)) {
+              const flattenedNode = { ...node }
+              delete flattenedNode.children
+              result.push(flattenedNode)
+              if (node.children && Array.isArray(node.children) && node.children.length > 0) {
+                flattenTree(node.children, result)
+              }
+            } else {
+              if (node.children && Array.isArray(node.children) && node.children.length > 0) {
+                flattenTree(node.children, result)
+              }
+            }
+          }
+          return result
+        }
+        allTeamMembers.value = flattenTree(response.content || [])
+      } else {
+        // 没有嵌套结构,直接使用,但确保有parentId字段
+        allTeamMembers.value = (response.content || []).map(item => {
+          const { children, ...rest } = item
+          return {
+            ...rest,
+            parentId: item.parentId !== undefined ? item.parentId : null
+          }
+        })
+      }
+    } else {
+      allTeamMembers.value = []
+    }
+  } catch (error) {
+    console.error('获取团队成员列表失败:', error)
+    allTeamMembers.value = []
+  }
+}
+
 // 获取数据
 const fetchData = async () => {
   loading.value = true
   try {
     const response = await listMembers(
-      tableData.value.metadata.page,
-      tableData.value.metadata.size,
+      0,  // 树形表格不使用分页,获取所有数据
+      1000,  // 获取足够多的数据
       searchForm.value.name || undefined,
       searchForm.value.teamId || undefined
     )
-    tableData.value = response
+    // 处理返回的数据,保留树状结构
+    if (response.content && Array.isArray(response.content) && response.content.length > 0) {
+      // 处理树状数据,保留children结构
+      const processedTree = processTreeData(response.content)
+      treeData.value = processedTree
+      
+      // 保存原始数据用于其他用途
+      tableData.value = {
+        content: response.content,
+        metadata: response.metadata || {
+          total: response.content.length,
+          page: 0,
+          size: 1000
+        }
+      }
+      
+      // 默认展开所有节点
+      const allKeys = {}
+      const collectKeys = (nodes) => {
+        if (!nodes || !Array.isArray(nodes)) return
+        for (const node of nodes) {
+          if (node.key) {
+            allKeys[node.key] = true
+          }
+          if (node.children && node.children.length > 0) {
+            collectKeys(node.children)
+          }
+        }
+      }
+      collectKeys(processedTree)
+      expandedKeys.value = allKeys
+    } else {
+      treeData.value = []
+      tableData.value = response
+    }
   } catch {
     toast.add({
       severity: 'error',
@@ -542,6 +729,11 @@ const fetchData = async () => {
   }
 }
 
+// 节点展开/折叠处理
+const onNodeToggle = (event) => {
+  expandedKeys.value = { ...expandedKeys.value, ...event }
+}
+
 // 获取团队统计数据
 const fetchTeamStats = async () => {
   if (!isTeam.value) return
@@ -572,16 +764,16 @@ const fetchTeamStats = async () => {
   }
 }
 
-// 分页处理
+// 分页处理(树形表格不使用分页,但保留函数以防需要)
 const handlePageChange = (event) => {
-  tableData.value.metadata.page = event.page
-  tableData.value.metadata.size = event.rows
-  fetchData()
+  // 树形表格不使用分页
+  // tableData.value.metadata.page = event.page
+  // tableData.value.metadata.size = event.rows
+  // fetchData()
 }
 
 // 搜索处理
 const handleSearch = () => {
-  tableData.value.metadata.page = 0
   fetchData()
 }
 
@@ -592,10 +784,21 @@ const handleRefresh = () => {
     name: null,
     teamId: null
   }
-  tableData.value.metadata.page = 0
   fetchData()
 }
 
+// 处理团队ID变化
+const handleTeamIdChange = async () => {
+  // 清空父级成员选择
+  editForm.value.memberId = null
+  // 重新加载该团队的成员列表
+  if (editForm.value.teamId) {
+    await fetchAllTeamMembers(editForm.value.teamId)
+  } else {
+    allTeamMembers.value = []
+  }
+}
+
 // 确认删除
 const confirmDelete = (member) => {
   confirm.require({
@@ -654,32 +857,88 @@ const copyToClipboard = async (text) => {
   }
 }
 
+// 父级团队成员选项(计算属性)
+const parentMemberOptions = computed(() => {
+  if (!editForm.value.teamId) return []
+  // 过滤出当前团队的所有成员,排除自己(如果是编辑模式)
+  return allTeamMembers.value
+    .filter(member => {
+      // 只显示当前团队的成员
+      if (member.teamId !== editForm.value.teamId) return false
+      // 编辑模式下,排除自己
+      if (isEdit.value && member.id === editForm.value.id) return false
+      return true
+    })
+    .map(member => ({
+      label: `${member.name} (ID: ${member.id})`,
+      value: member.id
+    }))
+})
+
 // 打开新增弹窗
-const openAddDialog = () => {
+const openAddDialog = async () => {
   isEdit.value = false
   editForm.value = {
     id: null,
     name: null,
     teamId: null,
-    commissionRate: null
+    memberId: null,  // 父级团队成员ID
+    commissionRate: null,
+    password: null,
+    confirmPassword: null
+  }
+  // 如果是团队用户,自动设置teamId
+  if (isTeam.value && teamStore.teams && teamStore.teams.length > 0) {
+    editForm.value.teamId = teamStore.teams[0].id
+  }
+  // 加载团队成员列表用于选择父级成员
+  if (editForm.value.teamId) {
+    await fetchAllTeamMembers(editForm.value.teamId)
   }
   editDialog.value = true
 }
 
 // 打开编辑弹窗
-const openEditDialog = (member) => {
+const openEditDialog = async (member) => {
   isEdit.value = true
   editForm.value = {
     id: member.id,
     name: member.name || null,
     teamId: member.teamId || null,
+    memberId: member.parentId || null,  // 父级团队成员ID
     commissionRate: member.commissionRate || null
   }
+  // 加载团队成员列表用于选择父级成员
+  if (editForm.value.teamId) {
+    await fetchAllTeamMembers(editForm.value.teamId)
+  }
   editDialog.value = true
 }
 
 // 保存编辑
 const saveEdit = async () => {
+  // 如果是新增且密码不匹配,显示错误
+  if (!isEdit.value && editForm.value.password !== editForm.value.confirmPassword) {
+    toast.add({
+      severity: 'error',
+      summary: '错误',
+      detail: '两次输入的密码不一致',
+      life: 3000
+    })
+    return
+  }
+
+  // 如果是新增且密码为空,显示错误
+  if (!isEdit.value && (!editForm.value.password || editForm.value.password.trim() === '')) {
+    toast.add({
+      severity: 'error',
+      summary: '错误',
+      detail: '密码不能为空',
+      life: 3000
+    })
+    return
+  }
+
   editLoading.value = true
   try {
     const formData = {}
@@ -693,9 +952,33 @@ const saveEdit = async () => {
 
     if (editForm.value.teamId !== null && editForm.value.teamId !== '') {
       formData.teamId = editForm.value.teamId
-      // 只有在创建新成员时才设置userId,编辑时保持原有userId
-      if (!isEdit.value) {
-        formData.userId = getTeamUserId(editForm.value.teamId)
+    }
+
+    // 新增时添加密码字段(可选,默认:password123)
+    if (!isEdit.value && editForm.value.password) {
+      formData.password = editForm.value.password
+    }
+
+    // 处理memberId/parentId字段(父级团队成员ID,可选)
+    // 新增时:如果提供memberId:创建父级团队成员的下级;如果不提供memberId:创建二级代理
+    // 编辑时:可以修改parentId,清空则改为二级代理
+    if (!isEdit.value) {
+      // 新增时添加memberId字段
+      if (editForm.value.memberId !== null && editForm.value.memberId !== '') {
+        formData.memberId = editForm.value.memberId
+      }
+    } else {
+      // 编辑时添加parentId字段(更新父级关系)
+      // 如果memberId为null或空字符串,表示改为二级代理(parentId = null)
+      if (editForm.value.memberId !== null && editForm.value.memberId !== '') {
+        formData.parentId = editForm.value.memberId
+        // 同时发送memberId以保持兼容性
+        formData.memberId = editForm.value.memberId
+      } else {
+        // 明确设置为null,表示改为二级代理
+        formData.parentId = null
+        // 也发送memberId为null以保持兼容性
+        formData.memberId = null
       }
     }
 
@@ -791,6 +1074,17 @@ onMounted(() => {
 </script>
 
 <style scoped>
+.team-members-container {
+  display: flex;
+  flex-direction: column;
+  height: calc(100vh - 120px);
+  min-height: 600px;
+  padding: 0.75rem;
+  background-color: var(--p-content-background);
+  border-radius: 0.5rem;
+  overflow: hidden;
+}
+
 .p-datatable-sm .p-datatable-tbody > tr > td {
   padding: 0.5rem;
   vertical-align: top;
@@ -802,13 +1096,39 @@ onMounted(() => {
 
 .members-table {
   width: 100%;
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+  flex: 1;
+  min-height: 0;
+}
+
+.members-table .p-treetable {
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+}
+
+.members-table .p-treetable-wrapper {
+  flex: 1;
+  overflow: auto;
+  min-height: 0;
+}
+
+.members-table .p-treetable-scrollable-wrapper {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  min-height: 0;
 }
 
-.members-table .p-datatable-wrapper {
-  overflow-x: auto;
+.members-table .p-treetable-scrollable-body {
+  flex: 1;
+  overflow: auto;
+  min-height: 0;
 }
 
-.members-table .p-datatable-thead th {
+.members-table .p-treetable-thead th {
   white-space: nowrap;
   min-width: 100px;
 }