|
|
@@ -0,0 +1,676 @@
|
|
|
+<script setup>
|
|
|
+import { ref, onMounted, computed, inject } from 'vue'
|
|
|
+import {
|
|
|
+ listDomainManagement,
|
|
|
+ createDomainManagement,
|
|
|
+ updateDomainManagement,
|
|
|
+ deleteDomainManagement,
|
|
|
+ getDomainManagement,
|
|
|
+ getDomainManagementTypes
|
|
|
+} from '@/services/api'
|
|
|
+import { useToast } from 'primevue/usetoast'
|
|
|
+import { useConfirm } from 'primevue/useconfirm'
|
|
|
+import { Form } from '@primevue/forms'
|
|
|
+import { zodResolver } from '@primevue/forms/resolvers/zod'
|
|
|
+import { useDateFormat } from '@vueuse/core'
|
|
|
+import { useTeamStore } from '@/stores/team'
|
|
|
+import { z } from 'zod'
|
|
|
+import Button from 'primevue/button'
|
|
|
+import Column from 'primevue/column'
|
|
|
+import DataTable from 'primevue/datatable'
|
|
|
+import Dialog from 'primevue/dialog'
|
|
|
+import InputText from 'primevue/inputtext'
|
|
|
+import Select from 'primevue/select'
|
|
|
+import FloatLabel from 'primevue/floatlabel'
|
|
|
+import IconField from 'primevue/iconfield'
|
|
|
+import InputIcon from 'primevue/inputicon'
|
|
|
+import InputNumber from 'primevue/inputnumber'
|
|
|
+import Textarea from 'primevue/textarea'
|
|
|
+import Message from 'primevue/message'
|
|
|
+import ToggleSwitch from 'primevue/toggleswitch'
|
|
|
+
|
|
|
+const toast = useToast()
|
|
|
+const confirm = useConfirm()
|
|
|
+const teamStore = useTeamStore()
|
|
|
+
|
|
|
+// 注入权限信息
|
|
|
+const isAdmin = inject('isAdmin')
|
|
|
+
|
|
|
+// 表格数据
|
|
|
+const tableData = ref({
|
|
|
+ data: [],
|
|
|
+ meta: {
|
|
|
+ page: 0,
|
|
|
+ size: 20,
|
|
|
+ total: 0,
|
|
|
+ totalPages: 0
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+// 搜索条件
|
|
|
+const searchForm = ref({
|
|
|
+ teamId: null,
|
|
|
+ domainType: null,
|
|
|
+ domain: '',
|
|
|
+ enabled: null
|
|
|
+})
|
|
|
+
|
|
|
+// 对话框相关
|
|
|
+const dialogVisible = ref(false)
|
|
|
+const isEditMode = ref(false)
|
|
|
+const formLoading = ref(false)
|
|
|
+
|
|
|
+// 表单数据
|
|
|
+const domainForm = ref({
|
|
|
+ id: null,
|
|
|
+ teamId: null,
|
|
|
+ domainType: 'primary',
|
|
|
+ domain: '',
|
|
|
+ remark: '',
|
|
|
+ enabled: true
|
|
|
+})
|
|
|
+
|
|
|
+// 域名类型选项
|
|
|
+const domainTypes = ref([
|
|
|
+ { label: '一级域名', value: 'primary' },
|
|
|
+ { label: '二级域名', value: 'secondary' }
|
|
|
+])
|
|
|
+
|
|
|
+// 团队选项
|
|
|
+const teamOptions = computed(() => {
|
|
|
+ return teamStore.teams.map((team) => ({
|
|
|
+ label: team.name,
|
|
|
+ value: team.id
|
|
|
+ }))
|
|
|
+})
|
|
|
+
|
|
|
+// 表单验证规则
|
|
|
+const formResolver = computed(() => {
|
|
|
+ return zodResolver(
|
|
|
+ z.object({
|
|
|
+ teamId: z.number().min(1, { message: '请选择团队' }),
|
|
|
+ domainType: z.enum(['primary', 'secondary'], { message: '请选择域名类型' }),
|
|
|
+ domain: z.string().min(1, { message: '请输入域名' }).max(255, { message: '域名长度不能超过255个字符' }),
|
|
|
+ remark: z.string().max(500, { message: '备注长度不能超过500个字符' }).optional().nullable(),
|
|
|
+ enabled: z.boolean().optional()
|
|
|
+ })
|
|
|
+ )
|
|
|
+})
|
|
|
+
|
|
|
+// 获取域名列表
|
|
|
+const fetchData = async (page = 0) => {
|
|
|
+ try {
|
|
|
+ // 确保page和size是数字类型
|
|
|
+ const pageNum = Number(page) || 0
|
|
|
+ const sizeNum = Number(tableData.value.meta.size) || 20
|
|
|
+
|
|
|
+ const params = {
|
|
|
+ ...searchForm.value
|
|
|
+ }
|
|
|
+ // 移除空值,但保留page和size
|
|
|
+ Object.keys(params).forEach((key) => {
|
|
|
+ if (params[key] === '' || params[key] === null || params[key] === undefined) {
|
|
|
+ delete params[key]
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ const result = await listDomainManagement(
|
|
|
+ pageNum,
|
|
|
+ sizeNum,
|
|
|
+ params.teamId,
|
|
|
+ params.domainType,
|
|
|
+ params.domain,
|
|
|
+ params.enabled
|
|
|
+ )
|
|
|
+ tableData.value = result || {
|
|
|
+ data: [],
|
|
|
+ meta: {
|
|
|
+ page: 0,
|
|
|
+ size: 20,
|
|
|
+ total: 0,
|
|
|
+ totalPages: 0
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('获取域名列表失败', error)
|
|
|
+ toast.add({
|
|
|
+ severity: 'error',
|
|
|
+ summary: '错误',
|
|
|
+ detail: error.message || '获取域名列表失败',
|
|
|
+ life: 3000
|
|
|
+ })
|
|
|
+ tableData.value = {
|
|
|
+ data: [],
|
|
|
+ meta: {
|
|
|
+ page: 0,
|
|
|
+ size: 20,
|
|
|
+ total: 0,
|
|
|
+ totalPages: 0
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 分页变化
|
|
|
+const handlePageChange = (event) => {
|
|
|
+ tableData.value.meta.page = event.page
|
|
|
+ tableData.value.meta.size = event.rows
|
|
|
+ fetchData(event.page)
|
|
|
+}
|
|
|
+
|
|
|
+// 刷新数据
|
|
|
+const handleRefresh = () => {
|
|
|
+ searchForm.value = {
|
|
|
+ teamId: null,
|
|
|
+ domainType: null,
|
|
|
+ domain: '',
|
|
|
+ enabled: null
|
|
|
+ }
|
|
|
+ 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 getDomainTypeLabel = (type) => {
|
|
|
+ const option = domainTypes.value.find((opt) => opt.value === type)
|
|
|
+ return option ? option.label : type
|
|
|
+}
|
|
|
+
|
|
|
+// 获取团队名称
|
|
|
+const getTeamName = (teamId) => {
|
|
|
+ if (!teamId) return '-'
|
|
|
+ const team = teamStore.teams.find((t) => t.id === teamId)
|
|
|
+ return team ? team.name : '-'
|
|
|
+}
|
|
|
+
|
|
|
+// 打开新建对话框
|
|
|
+const openNewDialog = () => {
|
|
|
+ domainForm.value = {
|
|
|
+ id: null,
|
|
|
+ teamId: null,
|
|
|
+ domainType: 'primary',
|
|
|
+ domain: '',
|
|
|
+ remark: '',
|
|
|
+ enabled: true
|
|
|
+ }
|
|
|
+ isEditMode.value = false
|
|
|
+ dialogVisible.value = true
|
|
|
+}
|
|
|
+
|
|
|
+// 打开编辑对话框
|
|
|
+const openEditDialog = async (domain) => {
|
|
|
+ try {
|
|
|
+ const detail = await getDomainManagement(domain.id)
|
|
|
+ domainForm.value = {
|
|
|
+ id: detail.id,
|
|
|
+ teamId: detail.teamId,
|
|
|
+ domainType: detail.domainType,
|
|
|
+ domain: detail.domain,
|
|
|
+ remark: detail.remark || '',
|
|
|
+ enabled: detail.enabled !== undefined ? detail.enabled : true
|
|
|
+ }
|
|
|
+ isEditMode.value = true
|
|
|
+ dialogVisible.value = true
|
|
|
+ } catch (error) {
|
|
|
+ toast.add({
|
|
|
+ severity: 'error',
|
|
|
+ summary: '错误',
|
|
|
+ detail: error.message || '获取域名详情失败',
|
|
|
+ life: 3000
|
|
|
+ })
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 保存域名
|
|
|
+const saveDomain = async ({ valid, values }) => {
|
|
|
+ if (!valid) return
|
|
|
+
|
|
|
+ formLoading.value = true
|
|
|
+ try {
|
|
|
+ const submitData = {
|
|
|
+ teamId: values.teamId,
|
|
|
+ domainType: values.domainType,
|
|
|
+ domain: values.domain,
|
|
|
+ enabled: values.enabled !== undefined ? values.enabled : true
|
|
|
+ }
|
|
|
+
|
|
|
+ if (values.remark) {
|
|
|
+ submitData.remark = values.remark
|
|
|
+ }
|
|
|
+
|
|
|
+ if (isEditMode.value) {
|
|
|
+ await updateDomainManagement(domainForm.value.id, submitData)
|
|
|
+ toast.add({
|
|
|
+ severity: 'success',
|
|
|
+ summary: '成功',
|
|
|
+ detail: '域名更新成功',
|
|
|
+ life: 3000
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ await createDomainManagement(submitData)
|
|
|
+ toast.add({
|
|
|
+ severity: 'success',
|
|
|
+ summary: '成功',
|
|
|
+ detail: '域名创建成功',
|
|
|
+ life: 3000
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ dialogVisible.value = false
|
|
|
+ fetchData(tableData.value.meta.page)
|
|
|
+ } catch (error) {
|
|
|
+ const errorMsg = error.message || (isEditMode.value ? '更新域名失败' : '创建域名失败')
|
|
|
+ toast.add({
|
|
|
+ severity: 'error',
|
|
|
+ summary: '错误',
|
|
|
+ detail: errorMsg,
|
|
|
+ life: 3000
|
|
|
+ })
|
|
|
+ } finally {
|
|
|
+ formLoading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 删除域名
|
|
|
+const handleDelete = (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 deleteDomainManagement(domain.id)
|
|
|
+ toast.add({
|
|
|
+ severity: 'success',
|
|
|
+ summary: '成功',
|
|
|
+ detail: '删除域名成功',
|
|
|
+ life: 3000
|
|
|
+ })
|
|
|
+ fetchData(tableData.value.meta.page)
|
|
|
+ } catch (error) {
|
|
|
+ toast.add({
|
|
|
+ severity: 'error',
|
|
|
+ summary: '错误',
|
|
|
+ detail: error.message || '删除域名失败',
|
|
|
+ life: 3000
|
|
|
+ })
|
|
|
+ }
|
|
|
+ }
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+// 初始化
|
|
|
+onMounted(() => {
|
|
|
+ fetchData()
|
|
|
+})
|
|
|
+</script>
|
|
|
+
|
|
|
+<template>
|
|
|
+ <div class="rounded-lg p-4 bg-[var(--p-content-background)]">
|
|
|
+ <!-- 权限检查 -->
|
|
|
+ <div v-if="!isAdmin" class="text-center py-8">
|
|
|
+ <p class="text-gray-500">您没有权限访问域名管理</p>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 主要内容 -->
|
|
|
+ <div v-else>
|
|
|
+ <DataTable
|
|
|
+ :value="tableData.data"
|
|
|
+ :paginator="true"
|
|
|
+ paginatorTemplate="CurrentPageReport FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink RowsPerPageDropdown JumpToPageInput"
|
|
|
+ currentPageReportTemplate="{totalRecords} 条记录 "
|
|
|
+ :rows="tableData.meta.size"
|
|
|
+ :rowsPerPageOptions="[10, 20, 50, 100]"
|
|
|
+ :totalRecords="tableData.meta.total"
|
|
|
+ @page="handlePageChange"
|
|
|
+ lazy
|
|
|
+ scrollable
|
|
|
+ stripedRows
|
|
|
+ showGridlines
|
|
|
+ >
|
|
|
+ <template #header>
|
|
|
+ <div class="search-toolbar">
|
|
|
+ <div class="toolbar-left">
|
|
|
+ <Button icon="pi pi-refresh" @click="fetchData" size="small" label="刷新" />
|
|
|
+ <Button
|
|
|
+ icon="pi pi-plus"
|
|
|
+ @click="openNewDialog"
|
|
|
+ label="新增域名"
|
|
|
+ severity="success"
|
|
|
+ size="small"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <div class="toolbar-right">
|
|
|
+ <div class="search-group">
|
|
|
+ <Select
|
|
|
+ v-model="searchForm.teamId"
|
|
|
+ :options="teamOptions"
|
|
|
+ optionLabel="label"
|
|
|
+ optionValue="value"
|
|
|
+ placeholder="选择团队"
|
|
|
+ size="small"
|
|
|
+ class="search-field"
|
|
|
+ clearable
|
|
|
+ />
|
|
|
+ <Select
|
|
|
+ v-model="searchForm.domainType"
|
|
|
+ :options="domainTypes"
|
|
|
+ optionLabel="label"
|
|
|
+ optionValue="value"
|
|
|
+ placeholder="域名类型"
|
|
|
+ size="small"
|
|
|
+ class="search-field"
|
|
|
+ clearable
|
|
|
+ />
|
|
|
+ <InputText
|
|
|
+ v-model="searchForm.domain"
|
|
|
+ placeholder="搜索域名"
|
|
|
+ size="small"
|
|
|
+ class="search-field"
|
|
|
+ @keyup.enter="fetchData"
|
|
|
+ />
|
|
|
+ <Select
|
|
|
+ v-model="searchForm.enabled"
|
|
|
+ :options="[
|
|
|
+ { label: '可用', value: true },
|
|
|
+ { label: '不可用', value: false }
|
|
|
+ ]"
|
|
|
+ optionLabel="label"
|
|
|
+ optionValue="value"
|
|
|
+ placeholder="状态"
|
|
|
+ size="small"
|
|
|
+ class="search-field"
|
|
|
+ clearable
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <div class="action-group">
|
|
|
+ <Button icon="pi pi-search" @click="fetchData" label="搜索" size="small" severity="secondary" />
|
|
|
+ <Button icon="pi pi-refresh" @click="handleRefresh" label="重置" size="small" />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <Column field="id" header="ID" style="min-width: 80px" headerClass="font-bold"></Column>
|
|
|
+ <Column 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="domainType" header="域名类型" style="min-width: 100px" headerClass="font-bold">
|
|
|
+ <template #body="slotProps">
|
|
|
+ <span class="px-2 py-1 rounded-md text-sm">
|
|
|
+ {{ getDomainTypeLabel(slotProps.data.domainType) }}
|
|
|
+ </span>
|
|
|
+ </template>
|
|
|
+ </Column>
|
|
|
+ <Column field="domain" header="域名" style="min-width: 200px" headerClass="font-bold"></Column>
|
|
|
+ <Column field="remark" header="备注" style="min-width: 150px" headerClass="font-bold">
|
|
|
+ <template #body="slotProps">
|
|
|
+ {{ slotProps.data.remark || '-' }}
|
|
|
+ </template>
|
|
|
+ </Column>
|
|
|
+ <Column field="enabled" header="状态" style="min-width: 80px" headerClass="font-bold">
|
|
|
+ <template #body="slotProps">
|
|
|
+ <span :class="slotProps.data.enabled ? 'text-green-600' : 'text-red-600'" class="font-medium">
|
|
|
+ {{ slotProps.data.enabled ? '可用' : '不可用' }}
|
|
|
+ </span>
|
|
|
+ </template>
|
|
|
+ </Column>
|
|
|
+ <Column field="createdAt" header="创建时间" style="min-width: 180px" headerClass="font-bold">
|
|
|
+ <template #body="slotProps">
|
|
|
+ {{ formatDate(slotProps.data.createdAt) }}
|
|
|
+ </template>
|
|
|
+ </Column>
|
|
|
+ <Column field="updatedAt" header="更新时间" style="min-width: 180px" headerClass="font-bold">
|
|
|
+ <template #body="slotProps">
|
|
|
+ {{ formatDate(slotProps.data.updatedAt) }}
|
|
|
+ </template>
|
|
|
+ </Column>
|
|
|
+ <Column header="操作" style="min-width: 150px" headerClass="font-bold" align="center">
|
|
|
+ <template #body="slotProps">
|
|
|
+ <div class="flex gap-2 justify-center">
|
|
|
+ <Button
|
|
|
+ icon="pi pi-pencil"
|
|
|
+ severity="info"
|
|
|
+ size="small"
|
|
|
+ text
|
|
|
+ rounded
|
|
|
+ aria-label="编辑"
|
|
|
+ @click="openEditDialog(slotProps.data)"
|
|
|
+ />
|
|
|
+ <Button
|
|
|
+ icon="pi pi-trash"
|
|
|
+ severity="danger"
|
|
|
+ size="small"
|
|
|
+ text
|
|
|
+ rounded
|
|
|
+ aria-label="删除"
|
|
|
+ @click="handleDelete(slotProps.data)"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </Column>
|
|
|
+ </DataTable>
|
|
|
+
|
|
|
+ <!-- 域名表单对话框 -->
|
|
|
+ <Dialog
|
|
|
+ v-model:visible="dialogVisible"
|
|
|
+ :modal="true"
|
|
|
+ :header="isEditMode ? '编辑域名' : '创建域名'"
|
|
|
+ :style="{ width: '500px' }"
|
|
|
+ position="center"
|
|
|
+ >
|
|
|
+ <Form
|
|
|
+ v-slot="$form"
|
|
|
+ :resolver="formResolver"
|
|
|
+ :initialValues="domainForm"
|
|
|
+ @submit="saveDomain"
|
|
|
+ class="p-fluid"
|
|
|
+ >
|
|
|
+ <div class="field mt-4">
|
|
|
+ <FloatLabel variant="on">
|
|
|
+ <Select
|
|
|
+ id="teamId"
|
|
|
+ name="teamId"
|
|
|
+ v-model="domainForm.teamId"
|
|
|
+ :options="teamOptions"
|
|
|
+ optionLabel="label"
|
|
|
+ optionValue="value"
|
|
|
+ fluid
|
|
|
+ />
|
|
|
+ <label for="teamId">团队 <span class="text-red-500">*</span></label>
|
|
|
+ </FloatLabel>
|
|
|
+ <Message v-if="$form.teamId?.invalid" severity="error" size="small" variant="simple">
|
|
|
+ {{ $form.teamId.error?.message }}
|
|
|
+ </Message>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="field mt-4">
|
|
|
+ <FloatLabel variant="on">
|
|
|
+ <Select
|
|
|
+ id="domainType"
|
|
|
+ name="domainType"
|
|
|
+ v-model="domainForm.domainType"
|
|
|
+ :options="domainTypes"
|
|
|
+ optionLabel="label"
|
|
|
+ optionValue="value"
|
|
|
+ fluid
|
|
|
+ />
|
|
|
+ <label for="domainType">域名类型 <span class="text-red-500">*</span></label>
|
|
|
+ </FloatLabel>
|
|
|
+ <Message v-if="$form.domainType?.invalid" severity="error" size="small" variant="simple">
|
|
|
+ {{ $form.domainType.error?.message }}
|
|
|
+ </Message>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="field mt-4">
|
|
|
+ <FloatLabel variant="on">
|
|
|
+ <IconField>
|
|
|
+ <InputIcon class="pi pi-globe" />
|
|
|
+ <InputText id="domain" name="domain" v-model="domainForm.domain" autocomplete="off" fluid />
|
|
|
+ </IconField>
|
|
|
+ <label for="domain">域名 <span class="text-red-500">*</span></label>
|
|
|
+ </FloatLabel>
|
|
|
+ <Message v-if="$form.domain?.invalid" severity="error" size="small" variant="simple">
|
|
|
+ {{ $form.domain.error?.message }}
|
|
|
+ </Message>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="field mt-4">
|
|
|
+ <FloatLabel variant="on">
|
|
|
+ <IconField>
|
|
|
+ <InputIcon class="pi pi-comment" />
|
|
|
+ <Textarea
|
|
|
+ id="remark"
|
|
|
+ name="remark"
|
|
|
+ v-model="domainForm.remark"
|
|
|
+ rows="3"
|
|
|
+ autocomplete="off"
|
|
|
+ fluid
|
|
|
+ />
|
|
|
+ </IconField>
|
|
|
+ <label for="remark">备注</label>
|
|
|
+ </FloatLabel>
|
|
|
+ <Message v-if="$form.remark?.invalid" severity="error" size="small" variant="simple">
|
|
|
+ {{ $form.remark.error?.message }}
|
|
|
+ </Message>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="field mt-4">
|
|
|
+ <div class="flex items-center gap-3">
|
|
|
+ <ToggleSwitch id="enabled" name="enabled" v-model="domainForm.enabled" />
|
|
|
+ <label for="enabled" class="font-medium">是否可用</label>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="flex justify-end gap-2 mt-6">
|
|
|
+ <Button
|
|
|
+ label="取消"
|
|
|
+ severity="secondary"
|
|
|
+ type="button"
|
|
|
+ @click="dialogVisible = false"
|
|
|
+ :disabled="formLoading"
|
|
|
+ />
|
|
|
+ <Button label="保存" type="submit" :loading="formLoading" />
|
|
|
+ </div>
|
|
|
+ </Form>
|
|
|
+ </Dialog>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+/* 搜索工具栏样式 */
|
|
|
+.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;
|
|
|
+ flex-wrap: wrap;
|
|
|
+}
|
|
|
+
|
|
|
+.search-field {
|
|
|
+ width: 150px;
|
|
|
+ font-size: 13px;
|
|
|
+}
|
|
|
+
|
|
|
+.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;
|
|
|
+}
|
|
|
+
|
|
|
+.team-name-text {
|
|
|
+ color: #7c3aed;
|
|
|
+ font-weight: 500;
|
|
|
+}
|
|
|
+
|
|
|
+/* 响应式设计 */
|
|
|
+@media (max-width: 768px) {
|
|
|
+ .search-toolbar {
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 12px;
|
|
|
+ align-items: stretch;
|
|
|
+ padding: 12px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .toolbar-left {
|
|
|
+ justify-content: center;
|
|
|
+ gap: 8px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .toolbar-right {
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 12px;
|
|
|
+ align-items: stretch;
|
|
|
+ }
|
|
|
+
|
|
|
+ .search-group {
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 8px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .search-field {
|
|
|
+ width: 100%;
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|
|
|
+
|