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