|
|
@@ -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>
|