Kaynağa Gözat

新增域名统计功能,支持获取今日和所有域名的统计数据,并在DomainView组件中展示相关信息,优化用户体验。

wuyi 3 ay önce
ebeveyn
işleme
57cc1ccdce
2 değiştirilmiş dosya ile 254 ekleme ve 28 silme
  1. 15 0
      src/services/api.js
  2. 239 28
      src/views/DomainView.vue

+ 15 - 0
src/services/api.js

@@ -522,3 +522,18 @@ export const getTeamDomainsByTeamId = async (teamId) => {
   return response.data
 }
 
+// 获取域名今日统计数据
+export const getTeamDomainDailyStatistics = async (domain) => {
+  const params = {}
+  if (domain) params.domain = domain
+  const response = await api.get('/team-domains/statistics/daily', { params })
+  return response.data
+}
+
+// 获取域名所有统计数据
+export const getTeamDomainAllStatistics = async (domain) => {
+  const params = {}
+  if (domain) params.domain = domain
+  const response = await api.get('/team-domains/statistics/all', { params })
+  return response.data
+}

+ 239 - 28
src/views/DomainView.vue

@@ -6,7 +6,9 @@ import {
   updateTeamDomain,
   deleteTeamDomain,
   getTeamDomain,
-  showTeamDomains
+  showTeamDomains,
+  getTeamDomainDailyStatistics,
+  getTeamDomainAllStatistics
 } from '@/services/api'
 import { useToast } from 'primevue/usetoast'
 import { useConfirm } from 'primevue/useconfirm'
@@ -43,6 +45,10 @@ const tableData = ref({
 
 // 管理员专用的分组数据
 const adminGroupedData = ref({})
+
+// 域名统计数据
+const domainStatistics = ref({})
+const domainAllStatistics = ref({})
 const selectedDomain = ref(null)
 const dialogVisible = ref(false)
 const isEditing = ref(false)
@@ -85,6 +91,8 @@ const fetchData = async (page = 0) => {
       // 管理员使用新的分组接口
       const result = await showTeamDomains(undefined, searchForm.value.teamId, searchForm.value.domain || undefined)
       adminGroupedData.value = result || {}
+      // 同时获取统计数据
+      await fetchDomainStatistics()
     } else {
       // 其他角色使用原有接口
       const result = await listTeamDomains(
@@ -176,6 +184,43 @@ const copyDomain = async (domain) => {
   }
 }
 
+// 获取域名统计数据
+const fetchDomainStatistics = async () => {
+  if (!isAdmin.value) return
+
+  try {
+    // 获取今日统计数据
+    const todayResult = await getTeamDomainDailyStatistics()
+    const todayStatsMap = {}
+    if (Array.isArray(todayResult)) {
+      todayResult.forEach((stat) => {
+        todayStatsMap[stat.domain] = {
+          todayNewUsers: stat.todayNewUsers || 0,
+          todayIncome: stat.todayIncome || 0
+        }
+      })
+    }
+    domainStatistics.value = todayStatsMap
+
+    // 获取所有统计数据
+    const allResult = await getTeamDomainAllStatistics()
+    const allStatsMap = {}
+    if (Array.isArray(allResult)) {
+      allResult.forEach((stat) => {
+        allStatsMap[stat.domain] = {
+          totalNewUsers: stat.totalNewUsers || 0,
+          totalIncome: stat.totalIncome || 0
+        }
+      })
+    }
+    domainAllStatistics.value = allStatsMap
+  } catch (error) {
+    console.error('获取域名统计数据失败', error)
+    domainStatistics.value = {}
+    domainAllStatistics.value = {}
+  }
+}
+
 const resetModel = () => {
   domainModel.teamId = currentTeamId.value
   domainModel.domain = ''
@@ -217,7 +262,7 @@ const validateForm = () => {
   }
 
   // 验证域名格式
-  const domains = domainModel.domain.split('\n').filter(domain => domain.trim())
+  const domains = domainModel.domain.split('\n').filter((domain) => domain.trim())
   if (domains.length === 0) {
     toast.add({ severity: 'warn', summary: '警告', detail: '请输入有效的域名', life: 3000 })
     return false
@@ -252,7 +297,7 @@ const onSubmit = async () => {
       toast.add({ severity: 'success', summary: '成功', detail: '更新域名成功', life: 3000 })
     } else {
       // 创建模式:支持批量创建
-      const domains = domainModel.domain.split('\n').filter(domain => domain.trim())
+      const domains = domainModel.domain.split('\n').filter((domain) => domain.trim())
       let successCount = 0
       let failCount = 0
 
@@ -277,11 +322,11 @@ const onSubmit = async () => {
       }
 
       if (successCount > 0) {
-        toast.add({ 
-          severity: 'success', 
-          summary: '成功', 
-          detail: `成功创建 ${successCount} 个域名${failCount > 0 ? `,${failCount} 个失败` : ''}`, 
-          life: 3000 
+        toast.add({
+          severity: 'success',
+          summary: '成功',
+          detail: `成功创建 ${successCount} 个域名${failCount > 0 ? `,${failCount} 个失败` : ''}`,
+          life: 3000
         })
       } else {
         toast.add({ severity: 'error', summary: '失败', detail: '所有域名创建失败', life: 3000 })
@@ -372,17 +417,17 @@ onMounted(() => {
     <!-- 主要内容 -->
     <div v-else>
       <!-- 操作栏 -->
-      <div class="flex flex-wrap items-center justify-between gap-2 mb-4">
-        <div class="flex gap-2">
+      <div class="operation-bar">
+        <div class="operation-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="flex gap-2">
+        <div class="operation-right">
           <InputText
             v-model="searchForm.domain"
             placeholder="域名搜索"
             size="small"
-            class="w-32"
+            class="search-input"
             @keyup.enter="handleSearch"
           />
           <Select
@@ -393,7 +438,7 @@ onMounted(() => {
             optionValue="value"
             placeholder="选择团队"
             size="small"
-            class="w-32"
+            class="team-select"
             clearable
           />
           <Button icon="pi pi-search" @click="handleSearch" label="搜索" size="small" severity="secondary" />
@@ -418,16 +463,42 @@ onMounted(() => {
                 <div class="domain-description">{{ domain.description || '暂无描述' }}</div>
                 <div class="domain-time">创建时间: {{ formatDate(domain.createdAt) }}</div>
               </div>
-              <div class="domain-actions-bottom">
-                <Button v-if="canUpdate" icon="pi pi-pencil" @click="onEdit(domain)" size="small" text />
-                <Button
-                  v-if="canDelete"
-                  icon="pi pi-trash"
-                  @click="onDelete(domain)"
-                  size="small"
-                  severity="danger"
-                  text
-                />
+              <div class="domain-stats-section">
+                <div class="domain-stats-row">
+                  <span class="stat-label">今日:</span>
+                  <span class="stat-item">
+                    <i class="pi pi-user text-xs"></i>
+                    <span class="stat-text">{{ domainStatistics[domain.domain]?.todayNewUsers || 0 }}</span>
+                  </span>
+                  <span class="stat-item">
+                    <i class="pi pi-dollar text-xs"></i>
+                    <span class="stat-text">{{ (domainStatistics[domain.domain]?.todayIncome || 0).toFixed(2) }}</span>
+                  </span>
+                </div>
+                <div class="domain-stats-row">
+                  <span class="stat-label">总计:</span>
+                  <span class="stat-item">
+                    <i class="pi pi-user text-xs"></i>
+                    <span class="stat-text">{{ domainAllStatistics[domain.domain]?.totalNewUsers || 0 }}</span>
+                  </span>
+                  <span class="stat-item">
+                    <i class="pi pi-dollar text-xs"></i>
+                    <span class="stat-text">{{
+                      (domainAllStatistics[domain.domain]?.totalIncome || 0).toFixed(2)
+                    }}</span>
+                  </span>
+                </div>
+                <div class="domain-actions">
+                  <Button v-if="canUpdate" icon="pi pi-pencil" @click="onEdit(domain)" size="small" text />
+                  <Button
+                    v-if="canDelete"
+                    icon="pi pi-trash"
+                    @click="onDelete(domain)"
+                    size="small"
+                    severity="danger"
+                    text
+                  />
+                </div>
               </div>
             </div>
           </div>
@@ -656,13 +727,52 @@ onMounted(() => {
   flex-shrink: 0;
 }
 
-.domain-actions-bottom {
-  display: flex;
-  gap: 8px;
-  justify-content: flex-end;
+.domain-stats-section {
   margin-top: 12px;
   padding-top: 12px;
   border-top: 1px solid #e2e8f0;
+  position: relative;
+}
+
+.domain-stats-row {
+  display: flex;
+  gap: 12px;
+  align-items: center;
+  margin-bottom: 6px;
+}
+
+.domain-stats-row:last-of-type {
+  margin-bottom: 12px;
+}
+
+.domain-actions {
+  position: absolute;
+  bottom: 0;
+  right: 0;
+  display: flex;
+  gap: 8px;
+}
+
+.stat-label {
+  font-size: 12px;
+  color: #475569;
+  font-weight: 600;
+}
+
+.stat-item {
+  display: flex;
+  align-items: center;
+  gap: 4px;
+  font-size: 12px;
+  color: #64748b;
+}
+
+.stat-item i {
+  color: #94a3b8;
+}
+
+.stat-text {
+  font-weight: 500;
 }
 
 .domain-actions-bottom .p-button {
@@ -682,6 +792,49 @@ onMounted(() => {
   background: rgba(239, 68, 68, 0.1) !important;
 }
 
+/* 操作栏样式 */
+.operation-bar {
+  display: flex;
+  flex-wrap: wrap;
+  align-items: center;
+  justify-content: space-between;
+  gap: 16px;
+  margin-bottom: 16px;
+}
+
+.operation-left {
+  display: flex;
+  gap: 8px;
+}
+
+.operation-left .p-button {
+  font-size: 12px;
+  padding: 4px 8px;
+}
+
+.operation-right {
+  display: flex;
+  gap: 6px;
+  flex-wrap: wrap;
+  align-items: center;
+}
+
+.operation-right .p-button {
+  font-size: 12px;
+  padding: 4px 8px;
+}
+
+.search-input {
+  width: 120px;
+  font-size: 12px;
+  padding: 4px 8px;
+}
+
+.team-select {
+  width: 120px;
+  font-size: 12px;
+}
+
 /* 响应式设计 */
 @media (max-width: 768px) {
   .domains-grid {
@@ -698,8 +851,66 @@ onMounted(() => {
     flex-direction: column;
   }
 
-  .domain-actions-bottom {
+  .domain-stats-section {
+    position: relative;
+  }
+
+  .domain-stats-row {
     justify-content: center;
   }
+
+  .domain-actions {
+    position: static;
+    justify-content: center;
+    margin-top: 8px;
+  }
+
+  /* 移动端操作栏适配 */
+  .operation-bar {
+    flex-direction: column;
+    gap: 16px;
+    align-items: stretch;
+  }
+
+  .operation-left {
+    justify-content: center;
+    gap: 8px;
+  }
+
+  .operation-left .p-button {
+    flex: 1;
+    max-width: 100px;
+    font-size: 12px;
+    padding: 6px 12px;
+  }
+
+  .operation-right {
+    justify-content: center;
+    flex-direction: column;
+    gap: 8px;
+  }
+
+  .search-input,
+  .team-select {
+    width: 100%;
+    max-width: 280px;
+    font-size: 14px;
+    padding: 8px 12px;
+  }
+
+  .operation-right .p-button {
+    width: 100%;
+    max-width: 280px;
+    font-size: 12px;
+    padding: 6px 12px;
+  }
+
+  /* 移动端按钮组优化 */
+  .operation-right .p-button:nth-child(3),
+  .operation-right .p-button:nth-child(4) {
+    display: inline-flex;
+    width: calc(50% - 4px);
+    max-width: 136px;
+  }
 }
 </style>