|
|
@@ -0,0 +1,1252 @@
|
|
|
+<script setup>
|
|
|
+import { ref, onMounted, reactive, computed, inject } from 'vue'
|
|
|
+import {
|
|
|
+ listLandingDomainPools,
|
|
|
+ createLandingDomainPool,
|
|
|
+ updateLandingDomainPool,
|
|
|
+ deleteLandingDomainPool,
|
|
|
+ getLandingDomainPool,
|
|
|
+ showLandingDomainPools,
|
|
|
+ listMembers
|
|
|
+} 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 adminGroupedData = ref({})
|
|
|
+
|
|
|
+const selectedPool = ref(null)
|
|
|
+const dialogVisible = ref(false)
|
|
|
+const isEditing = ref(false)
|
|
|
+const poolModel = reactive({
|
|
|
+ teamId: null,
|
|
|
+ domain: '',
|
|
|
+ description: '',
|
|
|
+ userId: null,
|
|
|
+ domainType: 'landing' // 默认值为 landing
|
|
|
+})
|
|
|
+
|
|
|
+// 搜索表单
|
|
|
+const searchForm = ref({
|
|
|
+ domain: '',
|
|
|
+ teamId: null,
|
|
|
+ domainType: null // 域名类型筛选
|
|
|
+})
|
|
|
+
|
|
|
+// 计算当前用户的团队ID
|
|
|
+const currentTeamId = computed(() => {
|
|
|
+ if (isAdmin.value) {
|
|
|
+ return null
|
|
|
+ } else if (isTeam.value || isPromoter.value) {
|
|
|
+ return userStore.userInfo?.teamId
|
|
|
+ }
|
|
|
+ return null
|
|
|
+})
|
|
|
+
|
|
|
+// 计算是否有操作权限
|
|
|
+const canCreate = computed(() => isAdmin.value || isTeam.value)
|
|
|
+const canUpdate = computed(() => isAdmin.value || isTeam.value)
|
|
|
+const canDelete = computed(() => isAdmin.value || isTeam.value)
|
|
|
+const canView = computed(() => isAdmin.value || isTeam.value)
|
|
|
+
|
|
|
+const fetchData = async (page = 0) => {
|
|
|
+ try {
|
|
|
+ if (isAdmin.value) {
|
|
|
+ // 管理员使用分组接口
|
|
|
+ const result = await showLandingDomainPools(
|
|
|
+ undefined,
|
|
|
+ searchForm.value.teamId,
|
|
|
+ searchForm.value.domain || undefined,
|
|
|
+ undefined, // userId
|
|
|
+ searchForm.value.domainType || undefined // domainType
|
|
|
+ )
|
|
|
+ adminGroupedData.value = result || {}
|
|
|
+
|
|
|
+ // 收集所有落地域名池数据
|
|
|
+ const allPools = []
|
|
|
+ Object.values(adminGroupedData.value).forEach(pools => {
|
|
|
+ if (Array.isArray(pools)) {
|
|
|
+ allPools.push(...pools)
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ // 根据实际数据加载需要的团队成员数据
|
|
|
+ await loadTeamMembersForPools(allPools)
|
|
|
+ } else {
|
|
|
+ // 其他角色使用列表接口
|
|
|
+ let queryTeamId = searchForm.value.teamId || currentTeamId.value
|
|
|
+
|
|
|
+ const result = await listLandingDomainPools(
|
|
|
+ page,
|
|
|
+ tableData.value.meta?.size || 20,
|
|
|
+ undefined,
|
|
|
+ queryTeamId,
|
|
|
+ searchForm.value.domain || undefined,
|
|
|
+ undefined, // userId
|
|
|
+ searchForm.value.domainType || undefined // domainType
|
|
|
+ )
|
|
|
+ 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))
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 根据实际数据加载需要的团队成员数据
|
|
|
+ await loadTeamMembersForPools(result?.content || [])
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('获取落地域名池列表失败', error)
|
|
|
+ toast.add({ severity: 'error', summary: '错误', detail: '获取落地域名池列表失败', life: 3000 })
|
|
|
+ if (isAdmin.value) {
|
|
|
+ adminGroupedData.value = {}
|
|
|
+ } else {
|
|
|
+ tableData.value = {
|
|
|
+ data: [],
|
|
|
+ meta: {
|
|
|
+ total: 0,
|
|
|
+ page: 0,
|
|
|
+ size: 20,
|
|
|
+ totalPages: 0
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const handlePageChange = (event) => {
|
|
|
+ if (!isAdmin.value) {
|
|
|
+ fetchData(event.page)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const refreshData = () => {
|
|
|
+ if (isAdmin.value) {
|
|
|
+ fetchData(0)
|
|
|
+ } else {
|
|
|
+ const page = tableData.value.meta?.page || 0
|
|
|
+ fetchData(page)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const handleSearch = () => {
|
|
|
+ if (!isAdmin.value && tableData.value.meta) {
|
|
|
+ tableData.value.meta.page = 0
|
|
|
+ }
|
|
|
+ fetchData(0)
|
|
|
+}
|
|
|
+
|
|
|
+const handleRefresh = () => {
|
|
|
+ searchForm.value = {
|
|
|
+ domain: '',
|
|
|
+ teamId: null,
|
|
|
+ domainType: null
|
|
|
+ }
|
|
|
+ if (!isAdmin.value && tableData.value.meta) {
|
|
|
+ tableData.value.meta.page = 0
|
|
|
+ }
|
|
|
+ fetchData(0)
|
|
|
+}
|
|
|
+
|
|
|
+const formatDate = (date) => {
|
|
|
+ if (!date) return '-'
|
|
|
+ return useDateFormat(new Date(date), 'YYYY-MM-DD HH:mm:ss').value
|
|
|
+}
|
|
|
+
|
|
|
+// 复制域名到剪贴板
|
|
|
+const copyDomain = async (domain) => {
|
|
|
+ try {
|
|
|
+ await navigator.clipboard.writeText(domain)
|
|
|
+ toast.add({
|
|
|
+ severity: 'success',
|
|
|
+ summary: '成功',
|
|
|
+ detail: '域名已复制到剪贴板',
|
|
|
+ life: 2000
|
|
|
+ })
|
|
|
+ } catch (error) {
|
|
|
+ console.error('复制失败:', error)
|
|
|
+ toast.add({
|
|
|
+ severity: 'error',
|
|
|
+ summary: '复制失败',
|
|
|
+ detail: '无法复制到剪贴板',
|
|
|
+ life: 3000
|
|
|
+ })
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const resetModel = () => {
|
|
|
+ poolModel.teamId = currentTeamId.value
|
|
|
+ poolModel.domain = ''
|
|
|
+ poolModel.description = ''
|
|
|
+ poolModel.userId = null
|
|
|
+ poolModel.domainType = 'landing' // 重置为默认值
|
|
|
+}
|
|
|
+
|
|
|
+const onEdit = async (pool = null) => {
|
|
|
+ resetModel()
|
|
|
+
|
|
|
+ if (pool) {
|
|
|
+ isEditing.value = true
|
|
|
+ selectedPool.value = pool
|
|
|
+ try {
|
|
|
+ const detail = await getLandingDomainPool(pool.id)
|
|
|
+ poolModel.teamId = detail.teamId
|
|
|
+ poolModel.domain = detail.domain
|
|
|
+ poolModel.description = detail.description || ''
|
|
|
+ poolModel.userId = detail.userId || null
|
|
|
+ poolModel.domainType = detail.domainType || 'landing' // 读取域名类型
|
|
|
+
|
|
|
+ // 如果是管理员,加载团队成员数据
|
|
|
+ if (isAdmin.value && detail.teamId) {
|
|
|
+ await fetchTeamMembers(detail.teamId)
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ toast.add({ severity: 'error', summary: '错误', detail: '获取落地域名池详情失败', life: 3000 })
|
|
|
+ return
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ isEditing.value = false
|
|
|
+ // 创建时,如果不是管理员,加载团队成员数据
|
|
|
+ if (!isAdmin.value && currentTeamId.value) {
|
|
|
+ await fetchTeamMembersForCurrentUser()
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ dialogVisible.value = true
|
|
|
+}
|
|
|
+
|
|
|
+const onCancel = () => {
|
|
|
+ dialogVisible.value = false
|
|
|
+}
|
|
|
+
|
|
|
+// 解析域名字符串(支持逗号、分号、换行分隔)
|
|
|
+const parseDomains = (domainString) => {
|
|
|
+ if (!domainString) return []
|
|
|
+ return domainString
|
|
|
+ .split(/[,,;;\n\r]+/)
|
|
|
+ .map((d) => d.trim())
|
|
|
+ .filter((d) => d.length > 0)
|
|
|
+}
|
|
|
+
|
|
|
+const validateForm = () => {
|
|
|
+ if (!poolModel.domain) {
|
|
|
+ toast.add({ severity: 'warn', summary: '警告', detail: '域名不能为空', life: 3000 })
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ const domains = parseDomains(poolModel.domain)
|
|
|
+ if (domains.length === 0) {
|
|
|
+ toast.add({ severity: 'warn', summary: '警告', detail: '请输入有效的域名', life: 3000 })
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ // 管理员必须指定teamId
|
|
|
+ if (isAdmin.value && !poolModel.teamId) {
|
|
|
+ toast.add({ severity: 'warn', summary: '警告', detail: '请选择团队', life: 3000 })
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ return true
|
|
|
+}
|
|
|
+
|
|
|
+const onSubmit = async () => {
|
|
|
+ if (!validateForm()) return
|
|
|
+
|
|
|
+ try {
|
|
|
+ if (isEditing.value) {
|
|
|
+ // 编辑模式:只支持单个域名
|
|
|
+ const poolData = {
|
|
|
+ domain: poolModel.domain.trim(),
|
|
|
+ description: poolModel.description,
|
|
|
+ domainType: poolModel.domainType || 'landing' // 添加域名类型
|
|
|
+ }
|
|
|
+
|
|
|
+ // 管理员需要传递teamId
|
|
|
+ if (isAdmin.value) {
|
|
|
+ poolData.teamId = poolModel.teamId
|
|
|
+ }
|
|
|
+
|
|
|
+ // 添加用户绑定
|
|
|
+ if (poolModel.userId !== null && poolModel.userId !== '') {
|
|
|
+ poolData.userId = poolModel.userId
|
|
|
+ }
|
|
|
+
|
|
|
+ await updateLandingDomainPool(selectedPool.value.id, poolData)
|
|
|
+ toast.add({ severity: 'success', summary: '成功', detail: '更新落地域名池成功', life: 3000 })
|
|
|
+ } else {
|
|
|
+ // 创建模式:支持批量创建
|
|
|
+ const domains = parseDomains(poolModel.domain)
|
|
|
+ const poolData = {
|
|
|
+ domain: domains.join(','),
|
|
|
+ description: poolModel.description,
|
|
|
+ domainType: poolModel.domainType || 'landing' // 添加域名类型
|
|
|
+ }
|
|
|
+
|
|
|
+ // 管理员需要传递teamId
|
|
|
+ if (isAdmin.value) {
|
|
|
+ poolData.teamId = poolModel.teamId
|
|
|
+ }
|
|
|
+
|
|
|
+ // 添加用户绑定
|
|
|
+ if (poolModel.userId !== null && poolModel.userId !== '') {
|
|
|
+ poolData.userId = poolModel.userId
|
|
|
+ }
|
|
|
+
|
|
|
+ const result = await createLandingDomainPool(poolData)
|
|
|
+
|
|
|
+ // 批量创建返回的结果结构
|
|
|
+ if (result.success && result.success.length > 0) {
|
|
|
+ const successCount = result.success.length
|
|
|
+ const failCount = result.failed ? result.failed.length : 0
|
|
|
+ toast.add({
|
|
|
+ severity: 'success',
|
|
|
+ summary: '成功',
|
|
|
+ detail: `成功创建 ${successCount} 个域名${failCount > 0 ? `,${failCount} 个失败` : ''}`,
|
|
|
+ life: 3000
|
|
|
+ })
|
|
|
+ } else if (result.id) {
|
|
|
+ // 单个创建成功
|
|
|
+ toast.add({ severity: 'success', summary: '成功', detail: '创建落地域名池成功', life: 3000 })
|
|
|
+ } else {
|
|
|
+ toast.add({ severity: 'error', summary: '失败', detail: '创建失败', life: 3000 })
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ dialogVisible.value = false
|
|
|
+ refreshData()
|
|
|
+ } catch (error) {
|
|
|
+ const errorMessage = error?.message || error?.detail || (isEditing.value ? '更新失败' : '创建失败')
|
|
|
+ toast.add({
|
|
|
+ severity: 'error',
|
|
|
+ summary: '错误',
|
|
|
+ detail: errorMessage,
|
|
|
+ life: 3000
|
|
|
+ })
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const onDelete = async (pool) => {
|
|
|
+ confirm.require({
|
|
|
+ message: `确定要删除落地域名池 "${pool.domain}" 吗?`,
|
|
|
+ header: '删除确认',
|
|
|
+ icon: 'pi pi-exclamation-triangle',
|
|
|
+ rejectLabel: '取消',
|
|
|
+ rejectProps: {
|
|
|
+ label: '取消',
|
|
|
+ severity: 'secondary'
|
|
|
+ },
|
|
|
+ acceptLabel: '删除',
|
|
|
+ acceptProps: {
|
|
|
+ label: '删除',
|
|
|
+ severity: 'danger'
|
|
|
+ },
|
|
|
+ accept: async () => {
|
|
|
+ try {
|
|
|
+ await deleteLandingDomainPool(pool.id)
|
|
|
+ toast.add({ severity: 'success', summary: '成功', detail: '删除落地域名池成功', life: 3000 })
|
|
|
+ refreshData()
|
|
|
+ } catch (error) {
|
|
|
+ const errorMessage = error?.message || error?.detail || '删除失败'
|
|
|
+ toast.add({ severity: 'error', summary: '错误', detail: errorMessage, 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 : '-'
|
|
|
+}
|
|
|
+
|
|
|
+// 团队成员数据
|
|
|
+const teamMembers = ref([])
|
|
|
+
|
|
|
+// 计算团队成员选项
|
|
|
+const teamMemberOptions = computed(() => {
|
|
|
+ if (!poolModel.teamId) return []
|
|
|
+
|
|
|
+ return [
|
|
|
+ { label: '不绑定用户', value: null },
|
|
|
+ ...teamMembers.value.map((member) => ({
|
|
|
+ label: `${member.name} (${member.commissionRate || 0}%)`,
|
|
|
+ // 注意:落地域名池的 userId 对应团队成员表的 userId 字段,不是 id 字段
|
|
|
+ value: member.userId
|
|
|
+ }))
|
|
|
+ ]
|
|
|
+})
|
|
|
+
|
|
|
+// 域名类型选项
|
|
|
+const domainTypeOptions = [
|
|
|
+ { label: '全部类型', value: null },
|
|
|
+ { label: '落地域名', value: 'landing' },
|
|
|
+ { label: '留存域名', value: 'retention' }
|
|
|
+]
|
|
|
+
|
|
|
+// 获取域名类型标签
|
|
|
+const getDomainTypeLabel = (domainType) => {
|
|
|
+ if (!domainType) return '-'
|
|
|
+ return domainType === 'landing' ? '落地域名' : domainType === 'retention' ? '留存域名' : domainType
|
|
|
+}
|
|
|
+
|
|
|
+// 扁平化树状结构数据
|
|
|
+const flattenTeamMembersTree = (nodes, result = [], parentId = null) => {
|
|
|
+ if (!nodes || !Array.isArray(nodes)) return result
|
|
|
+ for (const node of nodes) {
|
|
|
+ // 跳过团队节点(type === 'team' 或 id === 0),只保留团队成员
|
|
|
+ if (node.type === 'teamMember' || (node.type !== 'team' && node.id !== 0 && node.id !== null)) {
|
|
|
+ // 创建扁平化的成员对象,移除children字段,添加parentId
|
|
|
+ const flattenedNode = {
|
|
|
+ ...node,
|
|
|
+ parentId: node.parentId !== undefined ? node.parentId : parentId
|
|
|
+ }
|
|
|
+ // 移除children字段
|
|
|
+ delete flattenedNode.children
|
|
|
+ result.push(flattenedNode)
|
|
|
+
|
|
|
+ // 递归处理子节点,传递当前节点的id作为parentId
|
|
|
+ if (node.children && Array.isArray(node.children) && node.children.length > 0) {
|
|
|
+ flattenTeamMembersTree(node.children, result, node.id)
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // 如果是团队节点,也需要递归处理其子节点
|
|
|
+ if (node.children && Array.isArray(node.children) && node.children.length > 0) {
|
|
|
+ flattenTeamMembersTree(node.children, result, parentId)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return result
|
|
|
+}
|
|
|
+
|
|
|
+// 获取团队成员数据
|
|
|
+const fetchTeamMembers = async (teamId) => {
|
|
|
+ if (!teamId) {
|
|
|
+ teamMembers.value = []
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ const response = await listMembers(0, 100, undefined, teamId)
|
|
|
+ // 处理返回的数据
|
|
|
+ if (response.content && Array.isArray(response.content) && response.content.length > 0) {
|
|
|
+ const hasNestedChildren = response.content.some(item =>
|
|
|
+ item.children && Array.isArray(item.children) && item.children.length > 0
|
|
|
+ )
|
|
|
+
|
|
|
+ if (hasNestedChildren) {
|
|
|
+ teamMembers.value = flattenTeamMembersTree(response.content)
|
|
|
+ } else {
|
|
|
+ teamMembers.value = (response.content || []).map(item => {
|
|
|
+ const { children, ...rest } = item
|
|
|
+ return {
|
|
|
+ ...rest,
|
|
|
+ parentId: item.parentId !== undefined ? item.parentId : null
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ teamMembers.value = []
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('获取团队成员失败:', error)
|
|
|
+ teamMembers.value = []
|
|
|
+ toast.add({
|
|
|
+ severity: 'error',
|
|
|
+ summary: '错误',
|
|
|
+ detail: '获取团队成员失败',
|
|
|
+ life: 3000
|
|
|
+ })
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 为当前用户获取团队成员数据
|
|
|
+const fetchTeamMembersForCurrentUser = async () => {
|
|
|
+ try {
|
|
|
+ const teamId = currentTeamId.value
|
|
|
+ if (teamId) {
|
|
|
+ const response = await listMembers(0, 100, undefined, teamId)
|
|
|
+ // 处理返回的数据
|
|
|
+ if (response.content && Array.isArray(response.content) && response.content.length > 0) {
|
|
|
+ const hasNestedChildren = response.content.some(item =>
|
|
|
+ item.children && Array.isArray(item.children) && item.children.length > 0
|
|
|
+ )
|
|
|
+
|
|
|
+ if (hasNestedChildren) {
|
|
|
+ teamMembers.value = flattenTeamMembersTree(response.content)
|
|
|
+ } else {
|
|
|
+ teamMembers.value = (response.content || []).map(item => {
|
|
|
+ const { children, ...rest } = item
|
|
|
+ return {
|
|
|
+ ...rest,
|
|
|
+ parentId: item.parentId !== undefined ? item.parentId : null
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ teamMembers.value = []
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ teamMembers.value = []
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('获取团队成员失败:', error)
|
|
|
+ teamMembers.value = []
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 为管理员加载所有团队的成员数据
|
|
|
+const loadAllTeamMembers = async () => {
|
|
|
+ try {
|
|
|
+ const allMembers = []
|
|
|
+ // 确保 teamStore.teams 已加载
|
|
|
+ if (teamStore.teams.length === 0) {
|
|
|
+ await teamStore.loadTeams()
|
|
|
+ }
|
|
|
+ for (const team of teamStore.teams) {
|
|
|
+ try {
|
|
|
+ const response = await listMembers(0, 100, undefined, team.id)
|
|
|
+ if (response.content) {
|
|
|
+ const hasNestedChildren = response.content.some(item =>
|
|
|
+ item.children && Array.isArray(item.children) && item.children.length > 0
|
|
|
+ )
|
|
|
+
|
|
|
+ if (hasNestedChildren) {
|
|
|
+ allMembers.push(...flattenTeamMembersTree(response.content))
|
|
|
+ } else {
|
|
|
+ allMembers.push(...(response.content || []).map(item => {
|
|
|
+ const { children, ...rest } = item
|
|
|
+ return {
|
|
|
+ ...rest,
|
|
|
+ parentId: item.parentId !== undefined ? item.parentId : null
|
|
|
+ }
|
|
|
+ }))
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (err) {
|
|
|
+ console.error(`获取团队 ${team.id} 的成员失败:`, err)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ teamMembers.value = allMembers
|
|
|
+ } catch (error) {
|
|
|
+ console.error('获取所有团队成员失败:', error)
|
|
|
+ teamMembers.value = []
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 根据落地域名池数据加载需要的团队成员数据
|
|
|
+const loadTeamMembersForPools = async (pools) => {
|
|
|
+ if (!pools || pools.length === 0) return
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 收集所有需要的 teamId
|
|
|
+ const teamIds = new Set()
|
|
|
+ pools.forEach(pool => {
|
|
|
+ if (pool.teamId) {
|
|
|
+ teamIds.add(pool.teamId)
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ // 加载这些团队的成员数据
|
|
|
+ const allMembers = []
|
|
|
+ for (const teamId of teamIds) {
|
|
|
+ try {
|
|
|
+ const response = await listMembers(0, 100, undefined, teamId)
|
|
|
+ if (response.content) {
|
|
|
+ allMembers.push(...response.content)
|
|
|
+ }
|
|
|
+ } catch (err) {
|
|
|
+ console.error(`获取团队 ${teamId} 的成员失败:`, err)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 合并到现有的团队成员列表中(避免重复)
|
|
|
+ const existingIds = new Set(teamMembers.value.map(m => m.id))
|
|
|
+ const newMembers = allMembers.filter(m => !existingIds.has(m.id))
|
|
|
+ teamMembers.value.push(...newMembers)
|
|
|
+ } catch (error) {
|
|
|
+ console.error('加载团队成员数据失败:', error)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 处理团队选择变化
|
|
|
+const handleTeamChange = async (event) => {
|
|
|
+ const teamId = event.value
|
|
|
+ poolModel.userId = null
|
|
|
+
|
|
|
+ if (teamId) {
|
|
|
+ await fetchTeamMembers(teamId)
|
|
|
+ } else {
|
|
|
+ teamMembers.value = []
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 获取绑定用户名
|
|
|
+const getBoundUserName = (pool) => {
|
|
|
+ if (!pool.userId) {
|
|
|
+ return '未绑定'
|
|
|
+ }
|
|
|
+
|
|
|
+ // 从已加载的团队成员列表中查找
|
|
|
+ // 注意:落地域名池的 userId 对应团队成员表的 userId 字段,不是 id 字段
|
|
|
+ const member = teamMembers.value.find(m =>
|
|
|
+ m.userId === pool.userId ||
|
|
|
+ m.userId === parseInt(pool.userId) ||
|
|
|
+ parseInt(m.userId) === pool.userId
|
|
|
+ )
|
|
|
+
|
|
|
+ if (member) {
|
|
|
+ return member.name
|
|
|
+ }
|
|
|
+
|
|
|
+ return '未知用户'
|
|
|
+}
|
|
|
+
|
|
|
+// 计算管理员的分组数据,转换为数组格式
|
|
|
+const adminGroupedList = computed(() => {
|
|
|
+ if (!isAdmin.value) return []
|
|
|
+
|
|
|
+ return Object.keys(adminGroupedData.value).map((teamId) => ({
|
|
|
+ teamId: parseInt(teamId),
|
|
|
+ teamName: getTeamName(parseInt(teamId)),
|
|
|
+ pools: adminGroupedData.value[teamId] || []
|
|
|
+ }))
|
|
|
+})
|
|
|
+
|
|
|
+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>
|
|
|
+ <!-- 操作栏 -->
|
|
|
+ <div class="search-toolbar">
|
|
|
+ <div class="toolbar-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="toolbar-right">
|
|
|
+ <div class="search-group">
|
|
|
+ <InputText
|
|
|
+ v-model="searchForm.domain"
|
|
|
+ placeholder="域名搜索"
|
|
|
+ size="small"
|
|
|
+ class="search-field"
|
|
|
+ @keyup.enter="handleSearch"
|
|
|
+ />
|
|
|
+ <Select
|
|
|
+ v-if="isAdmin"
|
|
|
+ v-model="searchForm.teamId"
|
|
|
+ :options="teamOptions"
|
|
|
+ optionLabel="label"
|
|
|
+ optionValue="value"
|
|
|
+ placeholder="选择团队"
|
|
|
+ size="small"
|
|
|
+ class="team-field"
|
|
|
+ clearable
|
|
|
+ />
|
|
|
+ <Select
|
|
|
+ v-model="searchForm.domainType"
|
|
|
+ :options="domainTypeOptions"
|
|
|
+ optionLabel="label"
|
|
|
+ optionValue="value"
|
|
|
+ placeholder="域名类型"
|
|
|
+ size="small"
|
|
|
+ class="team-field"
|
|
|
+ clearable
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <div class="action-group">
|
|
|
+ <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>
|
|
|
+
|
|
|
+ <!-- 管理员卡片展示 -->
|
|
|
+ <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.pools.length }} 个域名</span>
|
|
|
+ </div>
|
|
|
+ <div class="domains-grid">
|
|
|
+ <div v-for="pool in team.pools" :key="pool.id" class="domain-card">
|
|
|
+ <div class="domain-info">
|
|
|
+ <div class="domain-name" @click="copyDomain(pool.domain)" :title="'点击复制: ' + pool.domain">
|
|
|
+ {{ pool.domain }}
|
|
|
+ <i class="pi pi-copy copy-icon"></i>
|
|
|
+ </div>
|
|
|
+ <div class="domain-description">{{ pool.description || '暂无描述' }}</div>
|
|
|
+ <div class="domain-bound-user">
|
|
|
+ <span class="bound-label">绑定用户:</span>
|
|
|
+ <span class="bound-user-name">{{ getBoundUserName(pool) }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="domain-bound-user">
|
|
|
+ <span class="bound-label">域名类型:</span>
|
|
|
+ <span class="domain-type-badge" :class="pool.domainType === 'retention' ? 'retention' : 'landing'">
|
|
|
+ {{ getDomainTypeLabel(pool.domainType) }}
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ <div class="domain-time">创建时间: {{ formatDate(pool.createdAt) }}</div>
|
|
|
+ <div class="domain-time">更新时间: {{ formatDate(pool.updatedAt) }}</div>
|
|
|
+ </div>
|
|
|
+ <div class="domain-actions">
|
|
|
+ <Button v-if="canUpdate" icon="pi pi-pencil" @click="onEdit(pool)" size="small" text />
|
|
|
+ <Button
|
|
|
+ v-if="canDelete"
|
|
|
+ icon="pi pi-trash"
|
|
|
+ @click="onDelete(pool)"
|
|
|
+ size="small"
|
|
|
+ severity="danger"
|
|
|
+ text
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 空数据提示 -->
|
|
|
+ <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">
|
|
|
+ <template #body="slotProps">
|
|
|
+ <span
|
|
|
+ class="domain-link cursor-pointer text-blue-600 hover:text-blue-800"
|
|
|
+ @click="copyDomain(slotProps.data.domain)"
|
|
|
+ :title="'点击复制: ' + slotProps.data.domain"
|
|
|
+ >
|
|
|
+ {{ slotProps.data.domain }}
|
|
|
+ </span>
|
|
|
+ </template>
|
|
|
+ </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="boundUser" header="绑定用户" style="min-width: 150px" headerClass="font-bold">
|
|
|
+ <template #body="slotProps">
|
|
|
+ <span class="bound-user-text">
|
|
|
+ {{ getBoundUserName(slotProps.data) }}
|
|
|
+ </span>
|
|
|
+ </template>
|
|
|
+ </Column>
|
|
|
+ <Column field="domainType" header="域名类型" style="min-width: 120px" headerClass="font-bold">
|
|
|
+ <template #body="slotProps">
|
|
|
+ <span class="domain-type-badge" :class="slotProps.data.domainType === 'retention' ? 'retention' : 'landing'">
|
|
|
+ {{ getDomainTypeLabel(slotProps.data.domainType) }}
|
|
|
+ </span>
|
|
|
+ </template>
|
|
|
+ </Column>
|
|
|
+ <Column field="createdAt" header="创建时间" style="min-width: 150px" headerClass="font-bold">
|
|
|
+ <template #body="slotProps">
|
|
|
+ {{ formatDate(slotProps.data.createdAt) }}
|
|
|
+ </template>
|
|
|
+ </Column>
|
|
|
+ <Column field="updatedAt" header="更新时间" style="min-width: 150px" headerClass="font-bold">
|
|
|
+ <template #body="slotProps">
|
|
|
+ {{ formatDate(slotProps.data.updatedAt) }}
|
|
|
+ </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
|
|
|
+ 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="poolModel.teamId"
|
|
|
+ :options="teamOptions"
|
|
|
+ optionLabel="label"
|
|
|
+ optionValue="value"
|
|
|
+ placeholder="请选择团队"
|
|
|
+ :disabled="isEditing"
|
|
|
+ :showClear="true"
|
|
|
+ @change="handleTeamChange"
|
|
|
+ />
|
|
|
+ <small v-if="!poolModel.teamId" class="text-red-500">请选择团队</small>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 选择绑定用户 -->
|
|
|
+ <div v-if="poolModel.teamId || (!isAdmin && currentTeamId)" class="flex flex-col gap-2">
|
|
|
+ <label class="font-medium">绑定用户(可选)</label>
|
|
|
+ <Select
|
|
|
+ v-model="poolModel.userId"
|
|
|
+ :options="teamMemberOptions"
|
|
|
+ optionLabel="label"
|
|
|
+ optionValue="value"
|
|
|
+ placeholder="选择团队成员(可选)"
|
|
|
+ :showClear="true"
|
|
|
+ />
|
|
|
+ <small class="text-gray-500">不选择则只绑定到团队</small>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 选择域名类型 -->
|
|
|
+ <div class="flex flex-col gap-2">
|
|
|
+ <label class="font-medium">域名类型</label>
|
|
|
+ <Select
|
|
|
+ v-model="poolModel.domainType"
|
|
|
+ :options="domainTypeOptions.filter(opt => opt.value !== null)"
|
|
|
+ optionLabel="label"
|
|
|
+ optionValue="value"
|
|
|
+ placeholder="选择域名类型"
|
|
|
+ />
|
|
|
+ <small class="text-gray-500">选择域名的类型:落地域名或留存域名</small>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="flex flex-col gap-2">
|
|
|
+ <label class="font-medium">域名</label>
|
|
|
+ <Textarea
|
|
|
+ v-model="poolModel.domain"
|
|
|
+ :placeholder="isEditing ? '请输入域名' : '请输入域名,多个域名可用逗号、分号或换行分隔'"
|
|
|
+ rows="4"
|
|
|
+ />
|
|
|
+ <small v-if="!poolModel.domain" class="text-red-500">请输入域名</small>
|
|
|
+ <small v-else class="text-gray-500">
|
|
|
+ {{ isEditing ? '编辑时只能修改单个域名' : '支持分隔符:,(逗号)、;(分号)、换行' }}
|
|
|
+ </small>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="flex flex-col gap-2">
|
|
|
+ <label class="font-medium">描述</label>
|
|
|
+ <Textarea v-model="poolModel.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-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(350px, 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;
|
|
|
+ min-height: 150px;
|
|
|
+}
|
|
|
+
|
|
|
+.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-bottom: 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: 40px;
|
|
|
+ display: flex;
|
|
|
+ align-items: flex-start;
|
|
|
+}
|
|
|
+
|
|
|
+.domain-bound-user {
|
|
|
+ font-size: 13px;
|
|
|
+ color: #475569;
|
|
|
+ margin-bottom: 8px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 6px;
|
|
|
+}
|
|
|
+
|
|
|
+.bound-label {
|
|
|
+ font-weight: 500;
|
|
|
+ color: #64748b;
|
|
|
+}
|
|
|
+
|
|
|
+.bound-user-name {
|
|
|
+ font-weight: 600;
|
|
|
+ color: #1e40af;
|
|
|
+ background: #dbeafe;
|
|
|
+ padding: 2px 8px;
|
|
|
+ border-radius: 4px;
|
|
|
+ font-size: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.domain-time {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #94a3b8;
|
|
|
+ margin-top: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.domain-actions {
|
|
|
+ display: flex;
|
|
|
+ gap: 8px;
|
|
|
+ justify-content: flex-end;
|
|
|
+ margin-top: auto;
|
|
|
+}
|
|
|
+
|
|
|
+.domain-link {
|
|
|
+ text-decoration: underline;
|
|
|
+}
|
|
|
+
|
|
|
+.bound-user-text {
|
|
|
+ color: #1e40af;
|
|
|
+ font-weight: 500;
|
|
|
+ background: #dbeafe;
|
|
|
+ padding: 4px 8px;
|
|
|
+ border-radius: 4px;
|
|
|
+ font-size: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.domain-type-badge {
|
|
|
+ display: inline-block;
|
|
|
+ font-weight: 500;
|
|
|
+ padding: 4px 10px;
|
|
|
+ border-radius: 12px;
|
|
|
+ font-size: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.domain-type-badge.landing {
|
|
|
+ color: #059669;
|
|
|
+ background: #d1fae5;
|
|
|
+ border: 1px solid #6ee7b7;
|
|
|
+}
|
|
|
+
|
|
|
+.domain-type-badge.retention {
|
|
|
+ color: #dc2626;
|
|
|
+ background: #fee2e2;
|
|
|
+ border: 1px solid #fca5a5;
|
|
|
+}
|
|
|
+
|
|
|
+/* 搜索工具栏样式 */
|
|
|
+.search-toolbar {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ gap: 16px;
|
|
|
+ margin-bottom: 20px;
|
|
|
+ padding: 16px;
|
|
|
+ background: #f8fafc;
|
|
|
+ border-radius: 8px;
|
|
|
+ border: 1px solid #e2e8f0;
|
|
|
+}
|
|
|
+
|
|
|
+.toolbar-left {
|
|
|
+ display: flex;
|
|
|
+ gap: 8px;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+
|
|
|
+.toolbar-left .p-button {
|
|
|
+ font-size: 13px;
|
|
|
+ padding: 6px 12px;
|
|
|
+ border-radius: 6px;
|
|
|
+ font-weight: 500;
|
|
|
+}
|
|
|
+
|
|
|
+.toolbar-right {
|
|
|
+ display: flex;
|
|
|
+ gap: 12px;
|
|
|
+ align-items: center;
|
|
|
+ flex-wrap: wrap;
|
|
|
+}
|
|
|
+
|
|
|
+.search-group {
|
|
|
+ display: flex;
|
|
|
+ gap: 8px;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+
|
|
|
+.search-field {
|
|
|
+ width: 140px;
|
|
|
+ font-size: 13px;
|
|
|
+ padding: 6px 10px;
|
|
|
+ border-radius: 6px;
|
|
|
+ border: 1px solid #d1d5db;
|
|
|
+ transition: all 0.2s ease;
|
|
|
+}
|
|
|
+
|
|
|
+.search-field:focus {
|
|
|
+ border-color: #3b82f6;
|
|
|
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
|
|
+}
|
|
|
+
|
|
|
+.team-field {
|
|
|
+ width: 160px;
|
|
|
+ font-size: 13px;
|
|
|
+}
|
|
|
+
|
|
|
+.team-field .p-select {
|
|
|
+ border-radius: 6px;
|
|
|
+}
|
|
|
+
|
|
|
+.action-group {
|
|
|
+ display: flex;
|
|
|
+ gap: 8px;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+
|
|
|
+.action-group .p-button {
|
|
|
+ font-size: 13px;
|
|
|
+ padding: 6px 12px;
|
|
|
+ border-radius: 6px;
|
|
|
+ font-weight: 500;
|
|
|
+}
|
|
|
+
|
|
|
+/* 响应式设计 */
|
|
|
+@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 {
|
|
|
+ position: static;
|
|
|
+ justify-content: center;
|
|
|
+ margin-top: 8px;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 移动端搜索工具栏适配 */
|
|
|
+ .search-toolbar {
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 12px;
|
|
|
+ align-items: stretch;
|
|
|
+ padding: 12px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .toolbar-left {
|
|
|
+ justify-content: center;
|
|
|
+ gap: 8px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .toolbar-left .p-button {
|
|
|
+ flex: 1;
|
|
|
+ max-width: 120px;
|
|
|
+ font-size: 13px;
|
|
|
+ padding: 8px 12px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .toolbar-right {
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 12px;
|
|
|
+ align-items: stretch;
|
|
|
+ }
|
|
|
+
|
|
|
+ .search-group {
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 8px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .search-field,
|
|
|
+ .team-field {
|
|
|
+ width: 100%;
|
|
|
+ font-size: 14px;
|
|
|
+ padding: 10px 12px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .action-group {
|
|
|
+ justify-content: center;
|
|
|
+ gap: 8px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .action-group .p-button {
|
|
|
+ flex: 1;
|
|
|
+ max-width: 140px;
|
|
|
+ font-size: 13px;
|
|
|
+ padding: 8px 12px;
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|
|
|
+
|