Ver Fonte

新增域名管理功能,包括路由配置、API接口及视图组件,优化权限管理,提升用户体验。

wuyi há 3 meses atrás
pai
commit
e90e19f17d
4 ficheiros alterados com 465 adições e 1 exclusões
  1. 5 0
      src/router/index.js
  2. 43 0
      src/services/api.js
  3. 410 0
      src/views/DomainView.vue
  4. 7 1
      src/views/MainView.vue

+ 5 - 0
src/router/index.js

@@ -61,6 +61,11 @@ const router = createRouter({
               name: 'team-members',
               component: () => import('@/views/TeamMembersView.vue')
             },
+            {
+              path: 'domain',
+              name: 'team-domain',
+              component: () => import('@/views/DomainView.vue')
+            },
             {
               path: 'config',
               name: 'team-config',

+ 43 - 0
src/services/api.js

@@ -468,3 +468,46 @@ export const listTeamConfig = async (page = 0, size = 20, name, type, teamId) =>
   return response.data
 }
 
+// ==================== 团队域名管理相关API ====================
+
+// 创建团队域名
+export const createTeamDomain = async (domainData) => {
+  const response = await api.post('/team-domains', domainData)
+  return response.data
+}
+
+// 获取团队域名列表
+export const listTeamDomains = async (page = 0, size = 20, id, teamId, domain) => {
+  const params = { page, size }
+  if (id) params.id = id
+  if (teamId) params.teamId = teamId
+  if (domain) params.domain = domain
+
+  const response = await api.get('/team-domains', { params })
+  return response.data
+}
+
+// 获取单个团队域名
+export const getTeamDomain = async (id) => {
+  const response = await api.get(`/team-domains/${id}`)
+  return response.data
+}
+
+// 更新团队域名
+export const updateTeamDomain = async (id, domainData) => {
+  const response = await api.put(`/team-domains/${id}`, domainData)
+  return response.data
+}
+
+// 删除团队域名
+export const deleteTeamDomain = async (id) => {
+  const response = await api.delete(`/team-domains/${id}`)
+  return response.data
+}
+
+// 根据团队ID获取域名列表
+export const getTeamDomainsByTeamId = async (teamId) => {
+  const response = await api.get(`/team-domains/team/${teamId}`)
+  return response.data
+}
+

+ 410 - 0
src/views/DomainView.vue

@@ -0,0 +1,410 @@
+<script setup>
+import { ref, onMounted, reactive, computed, inject } from 'vue'
+import { listTeamDomains, createTeamDomain, updateTeamDomain, deleteTeamDomain, getTeamDomain } 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 selectedDomain = ref(null)
+const dialogVisible = ref(false)
+const isEditing = ref(false)
+const domainModel = reactive({
+  teamId: null,
+  domain: '',
+  description: ''
+})
+
+// 搜索表单
+const searchForm = ref({
+  domain: '',
+  teamId: null
+})
+
+// 计算当前用户的团队ID
+const currentTeamId = computed(() => {
+  if (isAdmin.value) {
+    // 管理员可以选择团队,这里先返回null,在创建/编辑时手动指定
+    return null
+  } else if (isTeam.value) {
+    // 队长从team表获取teamId
+    return userStore.userInfo?.teamId
+  } else if (isPromoter.value) {
+    // 推广员从team-members表获取teamId
+    return userStore.userInfo?.teamId
+  }
+  return null
+})
+
+// 计算是否有操作权限
+const canCreate = computed(() => isAdmin.value)
+const canUpdate = computed(() => isAdmin.value || isTeam.value)
+const canDelete = computed(() => isAdmin.value || isTeam.value)
+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))
+      }
+    }
+  } 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
+      }
+    }
+  }
+}
+
+const handlePageChange = (event) => {
+  fetchData(event.page)
+}
+
+const refreshData = () => {
+  const page = tableData.value.meta?.page || 0
+  fetchData(page)
+}
+
+const handleSearch = () => {
+  if (tableData.value.meta) {
+    tableData.value.meta.page = 0
+  }
+  fetchData()
+}
+
+const handleRefresh = () => {
+  searchForm.value = {
+    domain: '',
+    teamId: null
+  }
+  if (tableData.value.meta) {
+    tableData.value.meta.page = 0
+  }
+  fetchData()
+}
+
+const formatDate = (date) => {
+  if (!date) return '-'
+  return useDateFormat(new Date(date), 'YYYY-MM-DD HH:mm:ss').value
+}
+
+const resetModel = () => {
+  domainModel.teamId = currentTeamId.value
+  domainModel.domain = ''
+  domainModel.description = ''
+}
+
+const onEdit = async (domain = null) => {
+  resetModel()
+
+  if (domain) {
+    isEditing.value = true
+    selectedDomain.value = domain
+    try {
+      const detail = await getTeamDomain(domain.id)
+
+      // 填充表单数据
+      domainModel.teamId = detail.teamId
+      domainModel.domain = detail.domain
+      domainModel.description = detail.description
+    } catch (error) {
+      toast.add({ severity: 'error', summary: '错误', detail: '获取域名详情失败', life: 3000 })
+      return
+    }
+  } else {
+    isEditing.value = false
+  }
+
+  dialogVisible.value = true
+}
+
+const onCancel = () => {
+  dialogVisible.value = false
+}
+
+const validateForm = () => {
+  if (!domainModel.domain) {
+    toast.add({ severity: 'warn', summary: '警告', detail: '域名不能为空', life: 3000 })
+    return false
+  }
+
+  // 管理员必须指定teamId
+  if (isAdmin.value && !domainModel.teamId) {
+    toast.add({ severity: 'warn', summary: '警告', detail: '请选择团队', life: 3000 })
+    return false
+  }
+
+  return true
+}
+
+const onSubmit = async () => {
+  if (!validateForm()) return
+
+  try {
+    const domainData = {
+      domain: domainModel.domain,
+      description: domainModel.description
+    }
+
+    // 管理员需要传递teamId
+    if (isAdmin.value) {
+      domainData.teamId = domainModel.teamId
+    }
+
+    if (isEditing.value) {
+      await updateTeamDomain(selectedDomain.value.id, domainData)
+      toast.add({ severity: 'success', summary: '成功', detail: '更新域名成功', life: 3000 })
+    } else {
+      await createTeamDomain(domainData)
+      toast.add({ severity: 'success', summary: '成功', detail: '创建域名成功', life: 3000 })
+    }
+
+    dialogVisible.value = false
+    refreshData()
+  } catch (error) {
+    toast.add({
+      severity: 'error',
+      summary: '错误',
+      detail: isEditing.value ? '更新域名失败' : '创建域名失败',
+      life: 3000
+    })
+  }
+}
+
+const onDelete = async (domain) => {
+  confirm.require({
+    message: `确定要删除域名 "${domain.domain}" 吗?`,
+    header: '删除确认',
+    icon: 'pi pi-exclamation-triangle',
+    rejectLabel: '取消',
+    rejectProps: {
+      label: '取消',
+      severity: 'secondary'
+    },
+    acceptLabel: '删除',
+    acceptProps: {
+      label: '删除',
+      severity: 'danger'
+    },
+    accept: async () => {
+      try {
+        await deleteTeamDomain(domain.id)
+        toast.add({ severity: 'success', summary: '成功', detail: '删除域名成功', life: 3000 })
+        refreshData()
+      } catch (error) {
+        toast.add({ severity: 'error', summary: '错误', detail: '删除域名失败', 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 : '-'
+}
+
+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>
+      <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>
+          </div>
+        </template>
+
+        <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>
+
+      <!-- 添加/编辑域名对话框 -->
+      <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="domainModel.teamId"
+              :options="teamOptions"
+              optionLabel="label"
+              optionValue="value"
+              placeholder="请选择团队"
+              :disabled="isEditing"
+              :showClear="true"
+            />
+            <small v-if="!domainModel.teamId" class="text-red-500">请选择团队</small>
+          </div>
+
+          <div class="flex flex-col gap-2">
+            <label class="font-medium">域名</label>
+            <InputText v-model="domainModel.domain" placeholder="请输入域名" />
+            <small v-if="!domainModel.domain" class="text-red-500">请输入域名</small>
+          </div>
+
+          <div class="flex flex-col gap-2">
+            <label class="font-medium">描述</label>
+            <Textarea v-model="domainModel.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-name-text {
+  color: #7c3aed;
+  font-weight: 500;
+}
+</style>

+ 7 - 1
src/views/MainView.vue

@@ -52,11 +52,17 @@ const allNavItems = [
         name: 'team-members',
         roles: ['admin', 'team']
       },
+      {
+        label: '域名管理',
+        icon: 'pi pi-fw pi-globe',
+        name: 'team-domain',
+        roles: ['admin', 'team', 'promoter']
+      },
       {
         label: '团队配置',
         icon: 'pi pi-fw pi-sliders-h',
         name: 'team-config',
-        roles: ['admin', 'team', 'promoter']
+        roles: ['admin', 'team']
       }
     ]
   },