瀏覽代碼

添加主题切换和ban人

wilhelm wong 2 月之前
父節點
當前提交
1931838f7d
共有 4 個文件被更改,包括 1201 次插入76 次删除
  1. 6 0
      src/services/api.js
  2. 1 1
      src/stores/user.js
  3. 882 73
      src/views/DashboardView.vue
  4. 312 2
      src/views/TeamView.vue

+ 6 - 0
src/services/api.js

@@ -296,6 +296,12 @@ export const updateTeam = async (id, teamData) => {
   return response.data
 }
 
+// 更新团队主题颜色(TEAM 或 ADMIN 更新自身团队)
+export const updateTeamThemeColor = async (themeColor) => {
+  const response = await api.put('/teams/theme-color', { themeColor })
+  return response.data
+}
+
 // 更新团队收入
 export const updateTeamRevenue = async (id, revenueData) => {
   const response = await api.patch(`/teams/${id}/revenue`, revenueData)

+ 1 - 1
src/stores/user.js

@@ -43,4 +43,4 @@ export const useUserStore = defineStore('user', () => {
     logout,
     sync
   }
-})
+})

+ 882 - 73
src/views/DashboardView.vue

@@ -40,67 +40,208 @@
         </div>
       </div>
 
-      <!-- 队长视图 -->
-      <div v-else-if="isTeam" class="team-stats">
-        <div class="card">
-          <h3>团队名称</h3>
-          <p class="stat-value">{{ teamStats?.name || '-' }}</p>
-        </div>
-        <div class="card revenue-card">
-          <h3>总分润收入</h3>
-          <p class="stat-value revenue-value">{{ formatAmount(teamStats?.totalRevenue) }}</p>
-          <p class="stat-label">实际收入</p>
-        </div>
-        <div class="card sales-card">
-          <h3>总售卖金额</h3>
-          <p class="stat-value sales-value">{{ formatAmount(teamStats?.totalSales) }}</p>
-          <p class="stat-label">订单金额</p>
-        </div>
-        <div class="card revenue-card">
-          <h3>今日分润收入</h3>
-          <p class="stat-value revenue-value">{{ formatAmount(teamStats?.todayRevenue) }}</p>
-          <p class="stat-label">今日实际收入</p>
-        </div>
-        <div class="card sales-card">
-          <h3>今日售卖金额</h3>
-          <p class="stat-value sales-value">{{ formatAmount(teamStats?.todaySales) }}</p>
-          <p class="stat-label">今日订单金额</p>
-        </div>
-        <div class="card user-card">
-          <h3>今日日活用户</h3>
-          <p class="stat-value user-value">{{ teamStats?.todayDAU || 0 }}</p>
-          <p class="stat-label">活跃用户数</p>
-        </div>
-        <div class="card new-user-card">
-          <h3>今日新增用户</h3>
-          <p class="stat-value new-user-value">{{ teamStats?.todayNewUsers || 0 }}</p>
-          <p class="stat-label">新注册用户</p>
+      <!-- 队长视图 - 隐藏原来的内容 -->
+
+      <!-- 推广员视图 - 隐藏原来的内容 -->
+    </div>
+
+    <!-- 推广员视图 - 与团队用户保持一致 -->
+    <div v-if="isPromoter && memberData" class="py-2">
+      <div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
+        <!-- 左侧:基本信息卡片(与右侧均分) -->
+        <div>
+          <div class="rounded-lg border border-gray-200 bg-white p-6 shadow-sm">
+            <div class="text-xl font-extrabold mb-4 text-gray-900">我的信息</div>
+            <div class="grid grid-cols-1 sm:grid-cols-2 gap-y-5 gap-x-6">
+              <div class="flex flex-col">
+                <span class="text-gray-500 text-sm mb-1">队员姓名</span>
+                <span class="text-base font-bold text-gray-900">{{ memberData.name }}</span>
+              </div>
+              <div class="flex flex-col">
+                <span class="text-gray-500 text-sm mb-1">分成比例</span>
+                <span class="text-base font-bold text-purple-700">{{ memberData.commissionRate }}%</span>
+              </div>
+              <div class="flex flex-col">
+                <span class="text-gray-500 text-sm mb-1">推广码</span>
+                <span
+                  class="text-base font-bold text-orange-600 font-mono copyable-text cursor-pointer"
+                  :title="memberData.affCode || '-'"
+                  @click="memberData.affCode && copyToClipboard(memberData.affCode)"
+                >
+                  {{ memberData.affCode || '-' }}
+                </span>
+              </div>
+              <div class="flex flex-col">
+                <span class="text-gray-500 text-sm mb-1">总用户数</span>
+                <span class="text-lg font-extrabold text-teal-600">{{ memberData.totalUsers || 0 }}</span>
+              </div>
+            </div>
+          </div>
         </div>
-        <div class="card">
-          <h3>佣金率</h3>
-          <p class="stat-value">{{ formatRate(teamStats?.averageCommissionRate) }}%</p>
+
+        <!-- 右侧:统计快照(与左侧均分) -->
+        <div class="grid grid-cols-2 gap-4">
+          <div class="stat-card stat-yellow">
+            <div class="icon-badge icon-yellow"><i class="pi pi-dollar"></i></div>
+            <div class="stat-body">
+              <div class="stat-title">今日分润收入</div>
+              <div class="stat-value text-yellow-700">¥{{ formatAmount(memberData.todayRevenue || memberData.todayCommission) }}</div>
+            </div>
+          </div>
+          <div class="stat-card stat-blue">
+            <div class="icon-badge icon-blue"><i class="pi pi-wallet"></i></div>
+            <div class="stat-body">
+              <div class="stat-title">总分润收入</div>
+              <div class="stat-value text-blue-700">¥{{ formatAmount(memberData.totalRevenue || memberData.totalCommission) }}</div>
+            </div>
+          </div>
+          <div class="stat-card stat-green">
+            <div class="icon-badge icon-green"><i class="pi pi-shopping-cart"></i></div>
+            <div class="stat-body">
+              <div class="stat-title">今日销售额</div>
+              <div class="stat-value text-green-700">¥{{ formatAmount(memberData.todaySales || 0) }}</div>
+            </div>
+          </div>
+          <div class="stat-card stat-orange">
+            <div class="icon-badge icon-orange"><i class="pi pi-chart-line"></i></div>
+            <div class="stat-body">
+              <div class="stat-title">总销售额</div>
+              <div class="stat-value text-orange-700">¥{{ formatAmount(memberData.totalSales || 0) }}</div>
+            </div>
+          </div>
+          <div class="stat-card stat-indigo">
+            <div class="icon-badge icon-indigo"><i class="pi pi-users"></i></div>
+            <div class="stat-body">
+              <div class="stat-title">今日日活</div>
+              <div class="stat-value text-indigo-700">{{ memberData.todayDAU || 0 }}</div>
+            </div>
+          </div>
+          <div class="stat-card stat-slate">
+            <div class="icon-badge icon-slate"><i class="pi pi-database"></i></div>
+            <div class="stat-body">
+              <div class="stat-title">总用户</div>
+              <div class="stat-value text-slate-800">{{ memberData.totalUsers || 0 }}</div>
+            </div>
+          </div>
+          <div class="stat-card stat-indigo">
+            <div class="icon-badge icon-indigo"><i class="pi pi-chart-bar"></i></div>
+            <div class="stat-body">
+              <div class="stat-title">今日IP成交率</div>
+              <div class="stat-value text-indigo-700">{{ formatPercent(memberData.todayIpConversionRate) }}</div>
+            </div>
+          </div>
+          <div class="stat-card stat-slate">
+            <div class="icon-badge icon-slate"><i class="pi pi-chart-line"></i></div>
+            <div class="stat-body">
+              <div class="stat-title">总IP成交率</div>
+              <div class="stat-value text-slate-800">{{ formatPercent(memberData.totalIpConversionRate) }}</div>
+            </div>
+          </div>
         </div>
       </div>
+    </div>
 
-      <!-- 推广员视图 -->
-      <div v-else-if="isPromoter" class="promoter-stats">
-        <div class="card">
-          <h3>队员姓名</h3>
-          <p class="stat-value">{{ memberStats?.name || '-' }}</p>
+    <!-- 团队用户视图 - 与TeamView保持一致 -->
+    <div v-if="isTeam && teamData" class="py-2">
+      <div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
+        <!-- 左侧:基本信息卡片(与右侧均分) -->
+        <div>
+          <div class="rounded-lg border border-gray-200 bg-white p-6 shadow-sm">
+            <div class="flex items-center justify-between mb-4">
+              <div class="text-xl font-extrabold text-gray-900">我的团队信息</div>
+              <Button
+                icon="pi pi-palette"
+                label="选择主题"
+                size="small"
+                severity="secondary"
+                @click="openThemeDialog"
+              />
+            </div>
+            <div class="grid grid-cols-1 sm:grid-cols-2 gap-y-5 gap-x-6">
+              <div class="flex flex-col">
+                <span class="text-gray-500 text-sm mb-1">团队名称</span>
+                <span class="text-base font-bold text-gray-900">{{ teamData.name }}</span>
+              </div>
+              <div class="flex flex-col">
+                <span class="text-gray-500 text-sm mb-1">佣金比例</span>
+                <span class="text-base font-bold text-purple-700">{{ teamData.commissionRate }}%</span>
+              </div>
+              <div class="flex flex-col">
+                <span class="text-gray-500 text-sm mb-1">推广码</span>
+                <span
+                  class="text-base font-bold text-orange-600 font-mono copyable-text cursor-pointer"
+                  :title="teamData.affCode"
+                  @click="copyToClipboard(teamData.affCode)"
+                >
+                  {{ teamData.affCode }}
+                </span>
+              </div>
+              <div class="flex flex-col">
+                <span class="text-gray-500 text-sm mb-1">总用户数</span>
+                <span class="text-lg font-extrabold text-teal-600">{{ teamData.totalUsers || 0 }}</span>
+              </div>
+            </div>
+          </div>
         </div>
-        <div class="card">
-          <h3>分成比例</h3>
-          <p class="stat-value">{{ formatRate(memberStats?.commissionRate) }}%</p>
-        </div>
-        <div class="card revenue-card">
-          <h3>总分成收入</h3>
-          <p class="stat-value revenue-value">{{ formatAmount(memberStats?.totalCommission) }}</p>
-          <p class="stat-label">累计分成</p>
-        </div>
-        <div class="card revenue-card">
-          <h3>今日分成收入</h3>
-          <p class="stat-value revenue-value">{{ formatAmount(memberStats?.todayCommission) }}</p>
-          <p class="stat-label">今日分成</p>
+
+        <!-- 右侧:统计快照(与左侧均分) -->
+        <div class="grid grid-cols-2 gap-4">
+          <div class="stat-card stat-yellow">
+            <div class="icon-badge icon-yellow"><i class="pi pi-dollar"></i></div>
+            <div class="stat-body">
+              <div class="stat-title">今日分润收入</div>
+              <div class="stat-value text-yellow-700">¥{{ formatAmount(teamData.todayRevenue) }}</div>
+            </div>
+          </div>
+          <div class="stat-card stat-blue">
+            <div class="icon-badge icon-blue"><i class="pi pi-wallet"></i></div>
+            <div class="stat-body">
+              <div class="stat-title">总分润收入</div>
+              <div class="stat-value text-blue-700">¥{{ formatAmount(teamData.totalRevenue) }}</div>
+            </div>
+          </div>
+          <div class="stat-card stat-green">
+            <div class="icon-badge icon-green"><i class="pi pi-shopping-cart"></i></div>
+            <div class="stat-body">
+              <div class="stat-title">今日销售额</div>
+              <div class="stat-value text-green-700">¥{{ formatAmount(teamData.todaySales) }}</div>
+            </div>
+          </div>
+          <div class="stat-card stat-orange">
+            <div class="icon-badge icon-orange"><i class="pi pi-chart-line"></i></div>
+            <div class="stat-body">
+              <div class="stat-title">总销售额</div>
+              <div class="stat-value text-orange-700">¥{{ formatAmount(teamData.totalSales) }}</div>
+            </div>
+          </div>
+          <div class="stat-card stat-indigo">
+            <div class="icon-badge icon-indigo"><i class="pi pi-users"></i></div>
+            <div class="stat-body">
+              <div class="stat-title">今日日活</div>
+              <div class="stat-value text-indigo-700">{{ teamData.todayDAU || 0 }}</div>
+            </div>
+          </div>
+          <div class="stat-card stat-slate">
+            <div class="icon-badge icon-slate"><i class="pi pi-database"></i></div>
+            <div class="stat-body">
+              <div class="stat-title">总用户</div>
+              <div class="stat-value text-slate-800">{{ teamData.totalUsers || 0 }}</div>
+            </div>
+          </div>
+          <div class="stat-card stat-indigo">
+            <div class="icon-badge icon-indigo"><i class="pi pi-chart-bar"></i></div>
+            <div class="stat-body">
+              <div class="stat-title">今日IP成交率</div>
+              <div class="stat-value text-indigo-700">{{ formatPercent(ipStats?.todayIpConversionRate) }}</div>
+            </div>
+          </div>
+          <div class="stat-card stat-slate">
+            <div class="icon-badge icon-slate"><i class="pi pi-chart-line"></i></div>
+            <div class="stat-body">
+              <div class="stat-title">总IP成交率</div>
+              <div class="stat-value text-slate-800">{{ formatPercent(ipStats?.totalIpConversionRate) }}</div>
+            </div>
+          </div>
         </div>
       </div>
     </div>
@@ -125,6 +266,57 @@
       </div>
     </div>
 
+    <!-- 主题选择弹窗 -->
+    <Dialog
+      v-model:visible="themeDialog"
+      :modal="true"
+      header="选择UI主题"
+      :style="{ width: '700px' }"
+      position="center"
+    >
+      <div class="theme-selector">
+        <div class="theme-grid">
+          <div
+            v-for="theme in themeOptions"
+            :key="theme.value"
+            class="theme-card"
+            :class="{ 'theme-selected': selectedTheme === theme.value }"
+            @click="selectTheme(theme.value)"
+          >
+            <div
+              class="theme-preview"
+              :data-theme="theme.value"
+              :style="{
+                '--preview-brand': theme.colors.brand,
+                '--preview-surface': theme.colors.surface,
+                '--preview-text': theme.colors.text
+              }"
+            >
+              <div class="preview-header">
+                <div class="preview-brand-color" :style="{ backgroundColor: theme.colors.brand }"></div>
+                <span class="preview-title" :style="{ color: theme.colors.text }">示例标题</span>
+              </div>
+              <div class="preview-body" :style="{ backgroundColor: theme.colors.surface }">
+                <div class="preview-content" :style="{ color: theme.colors.text }">
+                  <div class="preview-text">这是预览文本</div>
+                  <div class="preview-button" :style="{ backgroundColor: theme.colors.brand, color: '#fff' }">
+                    按钮
+                  </div>
+                </div>
+              </div>
+            </div>
+            <div class="theme-name">{{ theme.label }}</div>
+          </div>
+        </div>
+      </div>
+      <template #footer>
+        <div class="flex justify-end gap-3">
+          <Button label="取消" severity="secondary" @click="themeDialog = false" />
+          <Button label="保存" severity="success" @click="saveTheme" :loading="themeLoading" />
+        </div>
+      </template>
+    </Dialog>
+
     <!-- 团队数据表格 (仅管理员可见) -->
     <div v-if="isAdmin" class="teams-table-container">
       <h2>团队数据一览</h2>
@@ -170,14 +362,18 @@
 import { ref, onMounted, onUnmounted, computed, inject, watch } from 'vue'
 import { useUserStore } from '@/stores/user'
 import { useTeamStore } from '@/stores/team'
-import { getAllTeamStatistics, getIncomeStatistics, listMembers } from '@/services/api'
+import { getAllTeamStatistics, getIncomeStatistics, listMembers, getTeamIpConversionRate, listTeams, getTeamDomainDailyStatistics, getTeamDomainAllStatistics, updateTeamThemeColor } from '@/services/api'
+import { useToast } from 'primevue/usetoast'
 import Chart from 'chart.js/auto'
 import Select from 'primevue/select'
 import DataTable from 'primevue/datatable'
 import Column from 'primevue/column'
+import Button from 'primevue/button'
+import Dialog from 'primevue/dialog'
 
 const userStore = useUserStore()
 const teamStore = useTeamStore()
+const toast = useToast()
 const role = computed(() => userStore.userInfo?.role)
 
 const isAdmin = inject('isAdmin')
@@ -193,6 +389,185 @@ const loading = ref(true)
 const teams = ref([])
 const chartInstance = ref(null)
 const selectedChartTeamId = ref(null) // 用于图表的团队选择
+const ipStats = ref(null) // IP成交率数据
+const teamListData = ref(null) // 团队列表数据(包含基础信息如affCode等)
+
+// 主题选择相关
+const themeDialog = ref(false)
+const themeLoading = ref(false)
+const selectedTheme = ref('')
+const themeOptions = [
+  {
+    value: '',
+    label: '默认',
+    colors: {
+      brand: '#10b981',
+      surface: '#0f172a',
+      text: '#f1f5f9'
+    }
+  },
+  {
+    value: 'dark',
+    label: '深色',
+    colors: {
+      brand: '#10b981',
+      surface: '#1e293b',
+      text: '#f1f5f9'
+    }
+  },
+  {
+    value: 'light',
+    label: '浅色',
+    colors: {
+      brand: '#059669',
+      surface: '#f8fafc',
+      text: '#1e293b'
+    }
+  },
+  {
+    value: 'black',
+    label: '黑色',
+    colors: {
+      brand: '#60a5fa',
+      surface: '#0a0a0a',
+      text: '#f1f5f9'
+    }
+  },
+  {
+    value: 'purple',
+    label: '紫色',
+    colors: {
+      brand: '#a855f7',
+      surface: '#3b1f5e',
+      text: '#f1f5f9'
+    }
+  },
+  {
+    value: 'orange',
+    label: '橙色',
+    colors: {
+      brand: '#fb923c',
+      surface: '#7c2d12',
+      text: '#f1f5f9'
+    }
+  },
+  {
+    value: 'green',
+    label: '绿色',
+    colors: {
+      brand: '#22c55e',
+      surface: '#14532d',
+      text: '#f1f5f9'
+    }
+  },
+  {
+    value: 'galaxy',
+    label: '星空',
+    colors: {
+      brand: '#06b6d4',
+      surface: '#1e1b4b',
+      text: '#f1f5f9'
+    }
+  },
+  {
+    value: 'rose',
+    label: '玫瑰',
+    colors: {
+      brand: '#f43f5e',
+      surface: '#831843',
+      text: '#f1f5f9'
+    }
+  },
+  {
+    value: 'blue',
+    label: '蓝色',
+    colors: {
+      brand: '#3b82f6',
+      surface: '#1e3a8a',
+      text: '#f1f5f9'
+    }
+  },
+  {
+    value: 'red',
+    label: '红色',
+    colors: {
+      brand: '#f87171',
+      surface: '#7f1d1d',
+      text: '#f1f5f9'
+    }
+  },
+  {
+    value: 'yellow',
+    label: '黄色',
+    colors: {
+      brand: '#fbbf24',
+      surface: '#78350f',
+      text: '#f1f5f9'
+    }
+  }
+]
+
+const teamData = computed(() => {
+  if (isTeam.value) {
+    // 优先从listTeams返回的数据中获取基础信息(包含affCode等)
+    if (teamListData.value?.content && teamListData.value.content.length > 0) {
+      const baseTeamInfo = teamListData.value.content[0]
+      // 合并统计数据
+      if (teamStats.value) {
+        return {
+          ...baseTeamInfo,
+          todayRevenue: teamStats.value.todayRevenue || baseTeamInfo.todayRevenue || 0,
+          totalRevenue: teamStats.value.totalRevenue || baseTeamInfo.totalRevenue || 0,
+          todaySales: teamStats.value.todaySales || baseTeamInfo.todaySales || 0,
+          totalSales: teamStats.value.totalSales || baseTeamInfo.totalSales || 0,
+          todayDAU: teamStats.value.todayDAU || baseTeamInfo.todayDAU || 0,
+          totalUsers: teamStats.value.totalUsers || baseTeamInfo.totalUsers || 0
+        }
+      }
+      return baseTeamInfo
+    }
+    // 如果没有listTeams数据,从teamStats中构建(fallback)
+    if (teamStats.value) {
+      if (teamStats.value.allTeams && teamStats.value.allTeams.length > 0) {
+        return teamStats.value.allTeams[0]
+      }
+      return {
+        name: teamStats.value.name || '-',
+        commissionRate: teamStats.value.averageCommissionRate || 0,
+        affCode: teamStats.value.affCode || '-',
+        totalUsers: teamStats.value.totalUsers || 0,
+        todayRevenue: teamStats.value.todayRevenue || 0,
+        totalRevenue: teamStats.value.totalRevenue || 0,
+        todaySales: teamStats.value.todaySales || 0,
+        totalSales: teamStats.value.totalSales || 0,
+        todayDAU: teamStats.value.todayDAU || 0
+      }
+    }
+  }
+  return null
+})
+
+// 推广员数据计算属性
+const memberData = computed(() => {
+  if (isPromoter.value && memberStats.value) {
+    return {
+      name: memberStats.value.name || '-',
+      commissionRate: memberStats.value.commissionRate || 0,
+      affCode: memberStats.value.affCode || null,
+      todayRevenue: memberStats.value.todayRevenue || memberStats.value.todayCommission || 0,
+      totalRevenue: memberStats.value.totalRevenue || memberStats.value.totalCommission || 0,
+      todayCommission: memberStats.value.todayCommission || 0,
+      totalCommission: memberStats.value.totalCommission || 0,
+      todaySales: memberStats.value.todaySales || 0,
+      totalSales: memberStats.value.totalSales || 0,
+      todayDAU: memberStats.value.todayDAU || 0,
+      totalUsers: memberStats.value.totalUsers || 0,
+      todayIpConversionRate: memberStats.value.todayIpConversionRate || null,
+      totalIpConversionRate: memberStats.value.totalIpConversionRate || null
+    }
+  }
+  return null
+})
 
 // 团队选项
 const teamOptions = computed(() => {
@@ -218,6 +593,53 @@ const formatRate = (rate) => {
   return Number(rate).toFixed(2)
 }
 
+// 百分比格式化(0-1 -> 0.00%)
+const formatPercent = (rate) => {
+  if (rate === undefined || rate === null) return '0.00%'
+  const num = Number(rate)
+  if (Number.isNaN(num)) return '0.00%'
+  return `${(num * 100).toFixed(2)}%`
+}
+
+// 复制到剪贴板
+const copyToClipboard = async (text) => {
+  try {
+    await navigator.clipboard.writeText(text)
+    toast.add({
+      severity: 'success',
+      summary: '成功',
+      detail: '已复制到剪贴板',
+      life: 2000
+    })
+  } catch {
+    const textArea = document.createElement('textarea')
+    textArea.value = text
+    document.body.appendChild(textArea)
+    textArea.select()
+    document.execCommand('copy')
+    document.body.removeChild(textArea)
+    toast.add({
+      severity: 'success',
+      summary: '成功',
+      detail: '已复制到剪贴板',
+      life: 2000
+    })
+  }
+}
+
+// 加载IP成交率数据
+const loadIpConversionRate = async () => {
+  if (!isTeam.value) return
+  try {
+    const data = await getTeamIpConversionRate()
+    ipStats.value = data
+  } catch (e) {
+    // 团队角色无权限外的错误提示
+    // 接口做了权限限制,这里仅在请求失败时给出轻提示
+    console.warn('获取IP成交率失败:', e)
+  }
+}
+
 // 获取当前日期和7天前的日期
 const getDateRange = () => {
   const endDate = new Date()
@@ -238,6 +660,53 @@ const formatDate = (date) => {
   return `${year}-${month}-${day}`
 }
 
+// 打开主题选择弹窗
+const openThemeDialog = () => {
+  selectedTheme.value = teamData.value?.themeColor || ''
+  themeDialog.value = true
+}
+
+// 选择主题
+const selectTheme = (themeValue) => {
+  selectedTheme.value = themeValue
+}
+
+// 保存主题
+const saveTheme = async () => {
+  if (!teamData.value?.id) {
+    toast.add({
+      severity: 'error',
+      summary: '错误',
+      detail: '团队信息不存在',
+      life: 3000
+    })
+    return
+  }
+
+  themeLoading.value = true
+  try {
+    await updateTeamThemeColor(selectedTheme.value)
+    toast.add({
+      severity: 'success',
+      summary: '成功',
+      detail: '主题保存成功',
+      life: 3000
+    })
+    themeDialog.value = false
+    // 刷新数据以获取最新的themeColor
+    await loadTeamStats()
+  } catch {
+    toast.add({
+      severity: 'error',
+      summary: '错误',
+      detail: '主题保存失败',
+      life: 3000
+    })
+  } finally {
+    themeLoading.value = false
+  }
+}
+
 // 加载团队统计数据
 const loadTeamStats = async () => {
   try {
@@ -251,28 +720,65 @@ const loadTeamStats = async () => {
         selectedTeamId.value = teams.value[0].id
       }
     } else if (isTeam.value) {
-      const data = await getAllTeamStatistics()
+      // 团队用户需要同时调用listTeams获取基础信息和getAllTeamStatistics获取统计数据
+      const [teamsResponse, statsData] = await Promise.all([
+        listTeams(0, 20),
+        getAllTeamStatistics()
+      ])
+      teamListData.value = teamsResponse
       // 为 team 角色添加团队名称
-      if (data.allTeams && data.allTeams.length > 0) {
-        data.name = data.allTeams[0].name
+      if (statsData.allTeams && statsData.allTeams.length > 0) {
+        statsData.name = statsData.allTeams[0].name
       }
-      teamStats.value = data
+      teamStats.value = statsData
     } else if (isPromoter.value) {
-      // 推广员角色加载个人统计数据,使用团队成员列表接口
+      // 推广员角色加载个人统计数据,使用团队成员列表接口和域名统计接口
       const currentUserId = userStore.userInfo?.id
       if (currentUserId) {
-        const response = await listMembers(0, 1, undefined, undefined, currentUserId)
+        // 并行请求团队成员数据、域名统计数据和IP成交率数据
+        const promises = [
+          listMembers(0, 1, undefined, undefined, currentUserId),
+          getTeamDomainDailyStatistics(),
+          getTeamDomainAllStatistics(),
+          getTeamIpConversionRate().catch(() => null) // 如果接口不支持推广员,返回null
+        ]
+        const [membersResponse, dailyStatsResponse, allStatsResponse, ipStatsData] = await Promise.all(promises)
+        
         // 从列表接口返回的数据中提取第一个成员(当前用户)的统计数据
-        if (response.content && response.content.length > 0) {
-          const memberData = response.content[0]
-          memberStats.value = {
-            name: memberData.name || '-',
-            commissionRate: memberData.commissionRate || 0,
-            totalCommission: memberData.totalRevenue || 0,
-            totalOrders: 0, // 列表接口没有返回订单数,设为0
-            todayCommission: memberData.todayRevenue || 0,
-            todayOrders: 0 // 列表接口没有返回订单数,设为0
-          }
+        let memberDataItem = null
+        if (membersResponse.content && membersResponse.content.length > 0) {
+          memberDataItem = membersResponse.content[0]
+        }
+        
+        // 从域名统计数据中提取(绑定的只返回一条数据)
+        const dailyStats = Array.isArray(dailyStatsResponse) && dailyStatsResponse.length > 0 ? dailyStatsResponse[0] : null
+        const allStats = Array.isArray(allStatsResponse) && allStatsResponse.length > 0 ? allStatsResponse[0] : null
+        
+        // 合并数据,优先使用域名统计数据
+        memberStats.value = {
+          name: memberDataItem?.name || '-',
+          commissionRate: memberDataItem?.commissionRate || 0,
+          affCode: memberDataItem?.affCode || null,
+          // 优先使用域名统计数据,如果没有则使用成员数据
+          todayRevenue: dailyStats?.todayIncome || memberDataItem?.todayRevenue || 0,
+          totalRevenue: allStats?.totalIncome || memberDataItem?.totalRevenue || 0,
+          todayCommission: dailyStats?.todayIncome || memberDataItem?.todayRevenue || 0,
+          totalCommission: allStats?.totalIncome || memberDataItem?.totalRevenue || 0,
+          todaySales: dailyStats?.todaySales || memberDataItem?.todaySales || 0,
+          totalSales: allStats?.totalSales || memberDataItem?.totalSales || 0,
+          todayDAU: dailyStats?.todayActiveUsers || allStats?.todayActiveUsers || 0,
+          totalUsers: allStats?.totalNewUsers || 0,
+          todayNewUsers: dailyStats?.todayNewUsers || 0,
+          // IP成交率数据
+          todayIpConversionRate: ipStatsData?.todayIpConversionRate || null,
+          totalIpConversionRate: ipStatsData?.totalIpConversionRate || null,
+          totalOrders: 0, // 列表接口没有返回订单数,设为0
+          todayOrders: 0 // 列表接口没有返回订单数,设为0
+        }
+        
+        // 保存IP成交率数据到ipStats,供模板使用
+        if (ipStatsData) {
+          ipStats.value = ipStatsData
         }
       }
     }
@@ -493,6 +999,11 @@ onMounted(async () => {
   }
   await loadTeamStats()
   
+  // 团队用户加载IP成交率
+  if (isTeam.value) {
+    await loadIpConversionRate()
+  }
+  
   // 延迟加载图表数据,确保DOM已渲染
   setTimeout(() => {
     loadIncomeStats()
@@ -729,4 +1240,302 @@ onUnmounted(() => {
   color: #8b5cf6;
   font-weight: 600;
 }
+
+/* 团队用户视图样式 - 与TeamView保持一致 */
+.py-2 {
+  padding-top: 0.5rem;
+  padding-bottom: 0.5rem;
+}
+
+.grid {
+  display: grid;
+}
+
+.grid-cols-1 {
+  grid-template-columns: repeat(1, minmax(0, 1fr));
+}
+
+.grid-cols-2 {
+  grid-template-columns: repeat(2, minmax(0, 1fr));
+}
+
+@media (min-width: 1024px) {
+  .lg\:grid-cols-2 {
+    grid-template-columns: repeat(2, minmax(0, 1fr));
+  }
+}
+
+@media (min-width: 640px) {
+  .sm\:grid-cols-2 {
+    grid-template-columns: repeat(2, minmax(0, 1fr));
+  }
+}
+
+.gap-4 {
+  gap: 1rem;
+}
+
+.gap-x-6 {
+  column-gap: 1.5rem;
+}
+
+.gap-y-5 {
+  row-gap: 1.25rem;
+}
+
+.rounded-lg {
+  border-radius: 0.5rem;
+}
+
+.border {
+  border-width: 1px;
+}
+
+.border-gray-200 {
+  border-color: #e5e7eb;
+}
+
+.bg-white {
+  background-color: white;
+}
+
+.p-6 {
+  padding: 1.5rem;
+}
+
+.shadow-sm {
+  box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
+}
+
+.text-xl {
+  font-size: 1.25rem;
+  line-height: 1.75rem;
+}
+
+.font-extrabold {
+  font-weight: 800;
+}
+
+.mb-4 {
+  margin-bottom: 1rem;
+}
+
+.text-gray-900 {
+  color: #111827;
+}
+
+.text-gray-500 {
+  color: #6b7280;
+}
+
+.text-sm {
+  font-size: 0.875rem;
+  line-height: 1.25rem;
+}
+
+.mb-1 {
+  margin-bottom: 0.25rem;
+}
+
+.text-base {
+  font-size: 1rem;
+  line-height: 1.5rem;
+}
+
+.font-bold {
+  font-weight: 700;
+}
+
+.text-purple-700 {
+  color: #7c3aed;
+}
+
+.text-orange-600 {
+  color: #ea580c;
+}
+
+.font-mono {
+  font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
+}
+
+.cursor-pointer {
+  cursor: pointer;
+}
+
+.text-lg {
+  font-size: 1.125rem;
+  line-height: 1.75rem;
+}
+
+.text-teal-600 {
+  color: #0d9488;
+}
+
+.flex {
+  display: flex;
+}
+
+.flex-col {
+  flex-direction: column;
+}
+
+.copyable-text {
+  transition: all 0.2s ease;
+  user-select: none;
+}
+
+.copyable-text:hover {
+  background-color: #e5e7eb;
+  border-radius: 4px;
+}
+
+.copyable-text:active {
+  background-color: #d1d5db;
+  transform: scale(0.98);
+}
+
+/* 统计卡片样式 */
+.stat-card {
+  border-radius: 0.5rem;
+  border: 1px solid #fde68a; /* amber-300 */
+  padding: 1rem;
+  background: #fffbeb; /* amber-50 统一浅黄色背景 */
+  display: flex;
+  align-items: center;
+  gap: 1rem;
+}
+
+.stat-body {
+  flex: 1;
+}
+
+.stat-title {
+  font-size: 0.875rem; /* text-sm */
+  color: #4b5563; /* gray-600 */
+}
+
+.stat-value {
+  margin-top: 0.25rem;
+  font-size: 1.5rem; /* text-2xl */
+  font-weight: 800; /* extrabold */
+}
+
+.icon-badge {
+  width: 40px;
+  height: 40px;
+  border-radius: 9999px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 1.125rem; /* text-lg */
+  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
+}
+
+.icon-yellow { background: #fef3c7; border: 1px solid #fde68a; color: #ca8a04; }
+.icon-blue { background: #dbeafe; border: 1px solid #bfdbfe; color: #2563eb; }
+.icon-green { background: #dcfce7; border: 1px solid #bbf7d0; color: #16a34a; }
+.icon-orange { background: #ffedd5; border: 1px solid #fed7aa; color: #ea580c; }
+.icon-indigo { background: #e0e7ff; border: 1px solid #c7d2fe; color: #4f46e5; }
+.icon-slate { background: #f1f5f9; border: 1px solid #e2e8f0; color: #475569; }
+
+.stat-yellow { border-color: #fde68a; background: #fffbeb; }
+.stat-blue { border-color: #bfdbfe; background: #eff6ff; }
+.stat-green { border-color: #bbf7d0; background: #f0fdf4; }
+.stat-orange { border-color: #fed7aa; background: #fff7ed; }
+.stat-indigo { border-color: #c7d2fe; background: #eef2ff; }
+.stat-slate { border-color: #e2e8f0; background: #f8fafc; }
+
+.text-yellow-700 { color: #a16207; }
+.text-blue-700 { color: #1d4ed8; }
+.text-green-700 { color: #15803d; }
+.text-orange-700 { color: #c2410c; }
+.text-indigo-700 { color: #4338ca; }
+.text-slate-800 { color: #1e293b; }
+/* 主题选择器样式 */
+.theme-selector {
+  padding: 1rem 0;
+}
+
+.theme-grid {
+  display: grid;
+  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
+  gap: 1rem;
+}
+
+.theme-card {
+  cursor: pointer;
+  transition: all 0.2s ease;
+  border-radius: 8px;
+  padding: 0.5rem;
+  border: 2px solid transparent;
+}
+
+.theme-card:hover {
+  background-color: #f3f4f6;
+}
+
+.theme-selected {
+  border-color: #3b82f6;
+  background-color: #eff6ff;
+}
+
+.theme-preview {
+  width: 100%;
+  height: 120px;
+  border-radius: 6px;
+  overflow: hidden;
+  margin-bottom: 0.5rem;
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+
+.preview-header {
+  height: 30px;
+  background-color: var(--preview-surface);
+  display: flex;
+  align-items: center;
+  padding: 0 8px;
+  gap: 8px;
+}
+
+.preview-brand-color {
+  width: 12px;
+  height: 12px;
+  border-radius: 2px;
+}
+
+.preview-title {
+  font-size: 10px;
+  font-weight: 600;
+}
+
+.preview-body {
+  height: calc(100% - 30px);
+  padding: 8px;
+}
+
+.preview-content {
+  display: flex;
+  flex-direction: column;
+  gap: 6px;
+}
+
+.preview-text {
+  font-size: 9px;
+  opacity: 0.8;
+}
+
+.preview-button {
+  width: fit-content;
+  padding: 2px 8px;
+  border-radius: 4px;
+  font-size: 8px;
+  font-weight: 600;
+}
+
+.theme-name {
+  text-align: center;
+  font-size: 0.875rem;
+  font-weight: 500;
+  color: #374151;
+}
 </style>

+ 312 - 2
src/views/TeamView.vue

@@ -158,7 +158,16 @@
         <!-- 左侧:基本信息卡片(与右侧均分) -->
         <div>
           <div class="rounded-lg border border-gray-200 bg-white p-6 shadow-sm">
-            <div class="text-xl font-extrabold mb-4 text-gray-900">我的团队信息</div>
+            <div class="flex items-center justify-between mb-4">
+              <div class="text-xl font-extrabold text-gray-900">我的团队信息</div>
+              <Button
+                icon="pi pi-palette"
+                label="选择主题"
+                size="small"
+                severity="secondary"
+                @click="openThemeDialog"
+              />
+            </div>
             <div class="grid grid-cols-1 sm:grid-cols-2 gap-y-5 gap-x-6">
               <div class="flex flex-col">
                 <span class="text-gray-500 text-sm mb-1">团队名称</span>
@@ -249,6 +258,57 @@
       </div>
     </div>
 
+    <!-- 主题选择弹窗 -->
+    <Dialog
+      v-model:visible="themeDialog"
+      :modal="true"
+      header="选择UI主题"
+      :style="{ width: '700px' }"
+      position="center"
+    >
+      <div class="theme-selector">
+        <div class="theme-grid">
+          <div
+            v-for="theme in themeOptions"
+            :key="theme.value"
+            class="theme-card"
+            :class="{ 'theme-selected': selectedTheme === theme.value }"
+            @click="selectTheme(theme.value)"
+          >
+            <div
+              class="theme-preview"
+              :data-theme="theme.value"
+              :style="{
+                '--preview-brand': theme.colors.brand,
+                '--preview-surface': theme.colors.surface,
+                '--preview-text': theme.colors.text
+              }"
+            >
+              <div class="preview-header">
+                <div class="preview-brand-color" :style="{ backgroundColor: theme.colors.brand }"></div>
+                <span class="preview-title" :style="{ color: theme.colors.text }">示例标题</span>
+              </div>
+              <div class="preview-body" :style="{ backgroundColor: theme.colors.surface }">
+                <div class="preview-content" :style="{ color: theme.colors.text }">
+                  <div class="preview-text">这是预览文本</div>
+                  <div class="preview-button" :style="{ backgroundColor: theme.colors.brand, color: '#fff' }">
+                    按钮
+                  </div>
+                </div>
+              </div>
+            </div>
+            <div class="theme-name">{{ theme.label }}</div>
+          </div>
+        </div>
+      </div>
+      <template #footer>
+        <div class="flex justify-end gap-3">
+          <Button label="取消" severity="secondary" @click="themeDialog = false" />
+          <Button label="保存" severity="success" @click="saveTheme" :loading="themeLoading" />
+        </div>
+      </template>
+    </Dialog>
+
     <!-- 新增/编辑弹窗 -->
     <Dialog
       v-model:visible="editDialog"
@@ -338,7 +398,7 @@ import InputNumber from 'primevue/inputnumber'
 import Password from 'primevue/password'
 import { useConfirm } from 'primevue/useconfirm'
 import { useToast } from 'primevue/usetoast'
-import { listTeams, createTeam, updateTeam, deleteTeam, getTeamIpConversionRate } from '@/services/api'
+import { listTeams, createTeam, updateTeam, deleteTeam, getTeamIpConversionRate, updateTeamThemeColor } from '@/services/api'
 
 const toast = useToast()
 const confirm = useConfirm()
@@ -371,6 +431,121 @@ const editForm = ref({
   confirmPassword: null
 })
 
+// 主题选择相关
+const themeDialog = ref(false)
+const themeLoading = ref(false)
+const selectedTheme = ref('')
+const themeOptions = [
+  {
+    value: '',
+    label: '默认',
+    colors: {
+      brand: '#10b981',
+      surface: '#0f172a',
+      text: '#f1f5f9'
+    }
+  },
+  {
+    value: 'dark',
+    label: '深色',
+    colors: {
+      brand: '#10b981',
+      surface: '#1e293b',
+      text: '#f1f5f9'
+    }
+  },
+  {
+    value: 'light',
+    label: '浅色',
+    colors: {
+      brand: '#059669',
+      surface: '#f8fafc',
+      text: '#1e293b'
+    }
+  },
+  {
+    value: 'black',
+    label: '黑色',
+    colors: {
+      brand: '#60a5fa',
+      surface: '#0a0a0a',
+      text: '#f1f5f9'
+    }
+  },
+  {
+    value: 'purple',
+    label: '紫色',
+    colors: {
+      brand: '#a855f7',
+      surface: '#3b1f5e',
+      text: '#f1f5f9'
+    }
+  },
+  {
+    value: 'orange',
+    label: '橙色',
+    colors: {
+      brand: '#fb923c',
+      surface: '#7c2d12',
+      text: '#f1f5f9'
+    }
+  },
+  {
+    value: 'green',
+    label: '绿色',
+    colors: {
+      brand: '#22c55e',
+      surface: '#14532d',
+      text: '#f1f5f9'
+    }
+  },
+  {
+    value: 'galaxy',
+    label: '星空',
+    colors: {
+      brand: '#06b6d4',
+      surface: '#1e1b4b',
+      text: '#f1f5f9'
+    }
+  },
+  {
+    value: 'rose',
+    label: '玫瑰',
+    colors: {
+      brand: '#f43f5e',
+      surface: '#831843',
+      text: '#f1f5f9'
+    }
+  },
+  {
+    value: 'blue',
+    label: '蓝色',
+    colors: {
+      brand: '#3b82f6',
+      surface: '#1e3a8a',
+      text: '#f1f5f9'
+    }
+  },
+  {
+    value: 'red',
+    label: '红色',
+    colors: {
+      brand: '#f87171',
+      surface: '#7f1d1d',
+      text: '#f1f5f9'
+    }
+  },
+  {
+    value: 'yellow',
+    label: '黄色',
+    colors: {
+      brand: '#fbbf24',
+      surface: '#78350f',
+      text: '#f1f5f9'
+    }
+  }
+]
+
 // 搜索表单
 const searchForm = ref({
   id: null,
@@ -541,6 +716,53 @@ const openEditDialog = (team) => {
   editDialog.value = true
 }
 
+// 打开主题选择弹窗
+const openThemeDialog = () => {
+  selectedTheme.value = teamData.value?.themeColor || ''
+  themeDialog.value = true
+}
+
+// 选择主题
+const selectTheme = (themeValue) => {
+  selectedTheme.value = themeValue
+}
+
+// 保存主题
+const saveTheme = async () => {
+  if (!teamData.value?.id) {
+    toast.add({
+      severity: 'error',
+      summary: '错误',
+      detail: '团队信息不存在',
+      life: 3000
+    })
+    return
+  }
+
+  themeLoading.value = true
+  try {
+    await updateTeamThemeColor(selectedTheme.value)
+    toast.add({
+      severity: 'success',
+      summary: '成功',
+      detail: '主题保存成功',
+      life: 3000
+    })
+    themeDialog.value = false
+    // 刷新数据以获取最新的themeColor
+    await fetchData()
+  } catch {
+    toast.add({
+      severity: 'error',
+      summary: '错误',
+      detail: '主题保存失败',
+      life: 3000
+    })
+  } finally {
+    themeLoading.value = false
+  }
+}
+
 // 保存编辑
 const saveEdit = async () => {
   // 如果是新增且密码不匹配,显示错误
@@ -807,4 +1029,92 @@ const teamData = computed(() => tableData.value?.content?.[0] || null)
 .icon-orange { background: #ffedd5; border: 1px solid #fed7aa; color: #ea580c; }
 .icon-indigo { background: #e0e7ff; border: 1px solid #c7d2fe; color: #4f46e5; }
 .icon-slate { background: #f1f5f9; border: 1px solid #e2e8f0; color: #475569; }
+
+/* 主题选择器样式 */
+.theme-selector {
+  padding: 1rem 0;
+}
+
+.theme-grid {
+  display: grid;
+  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
+  gap: 1rem;
+}
+
+.theme-card {
+  cursor: pointer;
+  transition: all 0.2s ease;
+  border-radius: 8px;
+  padding: 0.5rem;
+  border: 2px solid transparent;
+}
+
+.theme-card:hover {
+  background-color: #f3f4f6;
+}
+
+.theme-selected {
+  border-color: #3b82f6;
+  background-color: #eff6ff;
+}
+
+.theme-preview {
+  width: 100%;
+  height: 120px;
+  border-radius: 6px;
+  overflow: hidden;
+  margin-bottom: 0.5rem;
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+
+.preview-header {
+  height: 30px;
+  background-color: var(--preview-surface);
+  display: flex;
+  align-items: center;
+  padding: 0 8px;
+  gap: 8px;
+}
+
+.preview-brand-color {
+  width: 12px;
+  height: 12px;
+  border-radius: 2px;
+}
+
+.preview-title {
+  font-size: 10px;
+  font-weight: 600;
+}
+
+.preview-body {
+  height: calc(100% - 30px);
+  padding: 8px;
+}
+
+.preview-content {
+  display: flex;
+  flex-direction: column;
+  gap: 6px;
+}
+
+.preview-text {
+  font-size: 9px;
+  opacity: 0.8;
+}
+
+.preview-button {
+  width: fit-content;
+  padding: 2px 8px;
+  border-radius: 4px;
+  font-size: 8px;
+  font-weight: 600;
+}
+
+.theme-name {
+  text-align: center;
+  font-size: 0.875rem;
+  font-weight: 500;
+  color: #374151;
+}
 </style>