Explorar el Código

新增管理员获取团队域名列表的API接口,并在DomainView组件中实现管理员视图的分组展示,优化数据获取逻辑和用户体验。

wuyi hace 3 meses
padre
commit
3b24cb5e9b
Se han modificado 2 ficheros con 372 adiciones y 107 borrados
  1. 11 0
      src/services/api.js
  2. 361 107
      src/views/DomainView.vue

+ 11 - 0
src/services/api.js

@@ -476,6 +476,17 @@ export const createTeamDomain = async (domainData) => {
   return response.data
 }
 
+// 管理员获取团队域名列表
+export const showTeamDomains = async (id, teamId, domain) => {
+  const params = {}
+  if (id) params.id = id
+  if (teamId) params.teamId = teamId
+  if (domain) params.domain = domain
+  const response = await api.get('/team-domains/show', { params })
+  return response.data
+}
+
+
 // 获取团队域名列表
 export const listTeamDomains = async (page = 0, size = 20, id, teamId, domain) => {
   const params = { page, size }

+ 361 - 107
src/views/DomainView.vue

@@ -1,6 +1,13 @@
 <script setup>
 import { ref, onMounted, reactive, computed, inject } from 'vue'
-import { listTeamDomains, createTeamDomain, updateTeamDomain, deleteTeamDomain, getTeamDomain } from '@/services/api'
+import {
+  listTeamDomains,
+  createTeamDomain,
+  updateTeamDomain,
+  deleteTeamDomain,
+  getTeamDomain,
+  showTeamDomains
+} from '@/services/api'
 import { useToast } from 'primevue/usetoast'
 import { useConfirm } from 'primevue/useconfirm'
 import DataTable from 'primevue/datatable'
@@ -33,6 +40,9 @@ const tableData = ref({
     size: 20
   }
 })
+
+// 管理员专用的分组数据
+const adminGroupedData = ref({})
 const selectedDomain = ref(null)
 const dialogVisible = ref(false)
 const isEditing = ref(false)
@@ -71,51 +81,68 @@ const canView = computed(() => isAdmin.value || isTeam.value || isPromoter.value
 
 const fetchData = async (page = 0) => {
   try {
-    const result = await listTeamDomains(
-      page,
-      tableData.value.meta?.size || 20,
-      undefined,
-      searchForm.value.teamId || currentTeamId.value,
-      searchForm.value.domain || undefined
-    )
-    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))
+    if (isAdmin.value) {
+      // 管理员使用新的分组接口
+      const result = await showTeamDomains(undefined, searchForm.value.teamId, searchForm.value.domain || undefined)
+      adminGroupedData.value = result || {}
+    } else {
+      // 其他角色使用原有接口
+      const result = await listTeamDomains(
+        page,
+        tableData.value.meta?.size || 20,
+        undefined,
+        searchForm.value.teamId || currentTeamId.value,
+        searchForm.value.domain || undefined
+      )
+      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))
+        }
       }
     }
   } catch (error) {
     console.error('获取域名列表失败', error)
     toast.add({ severity: 'error', summary: '错误', detail: '获取域名列表失败', life: 3000 })
-    tableData.value = {
-      data: [],
-      meta: {
-        total: 0,
-        page: 0,
-        size: 20,
-        totalPages: 0
+    if (isAdmin.value) {
+      adminGroupedData.value = {}
+    } else {
+      tableData.value = {
+        data: [],
+        meta: {
+          total: 0,
+          page: 0,
+          size: 20,
+          totalPages: 0
+        }
       }
     }
   }
 }
 
 const handlePageChange = (event) => {
-  fetchData(event.page)
+  if (!isAdmin.value) {
+    fetchData(event.page)
+  }
 }
 
 const refreshData = () => {
-  const page = tableData.value.meta?.page || 0
-  fetchData(page)
+  if (isAdmin.value) {
+    fetchData(0)
+  } else {
+    const page = tableData.value.meta?.page || 0
+    fetchData(page)
+  }
 }
 
 const handleSearch = () => {
-  if (tableData.value.meta) {
+  if (!isAdmin.value && tableData.value.meta) {
     tableData.value.meta.page = 0
   }
-  fetchData()
+  fetchData(0)
 }
 
 const handleRefresh = () => {
@@ -123,10 +150,10 @@ const handleRefresh = () => {
     domain: '',
     teamId: null
   }
-  if (tableData.value.meta) {
+  if (!isAdmin.value && tableData.value.meta) {
     tableData.value.meta.page = 0
   }
-  fetchData()
+  fetchData(0)
 }
 
 const formatDate = (date) => {
@@ -134,6 +161,21 @@ const formatDate = (date) => {
   return useDateFormat(new Date(date), 'YYYY-MM-DD HH:mm:ss').value
 }
 
+// 复制域名到剪贴板
+const copyDomain = async (domain) => {
+  try {
+    await navigator.clipboard.writeText(domain)
+  } catch (error) {
+    console.error('复制失败:', error)
+    toast.add({
+      severity: 'error',
+      summary: '复制失败',
+      detail: '无法复制到剪贴板',
+      life: 3000
+    })
+  }
+}
+
 const resetModel = () => {
   domainModel.teamId = currentTeamId.value
   domainModel.domain = ''
@@ -263,6 +305,17 @@ const getTeamName = (teamId) => {
   return team ? team.name : '-'
 }
 
+// 计算管理员的分组数据,转换为数组格式
+const adminGroupedList = computed(() => {
+  if (!isAdmin.value) return []
+
+  return Object.keys(adminGroupedData.value).map((teamId) => ({
+    teamId: parseInt(teamId),
+    teamName: getTeamName(parseInt(teamId)),
+    domains: adminGroupedData.value[teamId] || []
+  }))
+})
+
 onMounted(() => {
   fetchData()
 })
@@ -277,87 +330,118 @@ onMounted(() => {
 
     <!-- 主要内容 -->
     <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
-      >
-        <template #header>
-          <div class="flex flex-wrap items-center justify-between gap-2">
-            <div class="flex gap-2">
-              <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">
-              <InputText
-                v-model="searchForm.domain"
-                placeholder="域名搜索"
-                size="small"
-                class="w-32"
-                @keyup.enter="handleSearch"
-              />
-              <Select
-                v-if="isAdmin"
-                v-model="searchForm.teamId"
-                :options="teamOptions"
-                optionLabel="label"
-                optionValue="value"
-                placeholder="选择团队"
-                size="small"
-                class="w-32"
-                clearable
-              />
-              <Button icon="pi pi-search" @click="handleSearch" label="搜索" size="small" severity="secondary" />
-              <Button icon="pi pi-refresh" @click="handleRefresh" label="重置" size="small" />
+      <!-- 操作栏 -->
+      <div class="flex flex-wrap items-center justify-between gap-2 mb-4">
+        <div class="flex gap-2">
+          <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">
+          <InputText
+            v-model="searchForm.domain"
+            placeholder="域名搜索"
+            size="small"
+            class="w-32"
+            @keyup.enter="handleSearch"
+          />
+          <Select
+            v-if="isAdmin"
+            v-model="searchForm.teamId"
+            :options="teamOptions"
+            optionLabel="label"
+            optionValue="value"
+            placeholder="选择团队"
+            size="small"
+            class="w-32"
+            clearable
+          />
+          <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 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.domains.length }} 个域名</span>
+          </div>
+          <div class="domains-grid">
+            <div v-for="domain in team.domains" :key="domain.id" class="domain-card">
+              <div class="domain-info">
+                <div class="domain-name" @click="copyDomain(domain.domain)" :title="'点击复制: ' + domain.domain">
+                  {{ domain.domain }}
+                  <i class="pi pi-copy copy-icon"></i>
+                </div>
+                <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>
             </div>
           </div>
-        </template>
+        </div>
 
-        <Column v-if="isAdmin" 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="domain" header="域名" style="min-width: 200px" headerClass="font-bold"></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="createdAt" header="创建时间" style="min-width: 150px" headerClass="font-bold">
-          <template #body="slotProps">
-            {{ formatDate(slotProps.data.createdAt) }}
-          </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 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"></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="createdAt" header="创建时间" style="min-width: 150px" headerClass="font-bold">
+            <template #body="slotProps">
+              {{ formatDate(slotProps.data.createdAt) }}
+            </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
@@ -407,4 +491,174 @@ onMounted(() => {
   color: #7c3aed;
   font-weight: 500;
 }
+
+/* 管理员卡片样式 */
+.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(300px, 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;
+}
+
+.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-right: 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: 60px;
+  display: flex;
+  align-items: flex-start;
+}
+
+.domain-time {
+  font-size: 12px;
+  color: #94a3b8;
+}
+
+.domain-actions {
+  display: flex;
+  gap: 8px;
+  flex-shrink: 0;
+}
+
+.domain-actions-bottom {
+  display: flex;
+  gap: 8px;
+  justify-content: flex-end;
+  margin-top: 12px;
+  padding-top: 12px;
+  border-top: 1px solid #e2e8f0;
+}
+
+.domain-actions-bottom .p-button {
+  width: 28px;
+  height: 28px;
+  padding: 0;
+  background: transparent !important;
+  border: none !important;
+  box-shadow: none !important;
+}
+
+.domain-actions-bottom .p-button:hover {
+  background: rgba(0, 0, 0, 0.05) !important;
+}
+
+.domain-actions-bottom .p-button.p-button-danger:hover {
+  background: rgba(239, 68, 68, 0.1) !important;
+}
+
+/* 响应式设计 */
+@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-bottom {
+    justify-content: center;
+  }
+}
 </style>