Parcourir la source

新增团队配置页面及相关API,更新路由配置,优化权限管理,提升用户体验。

wuyi il y a 3 mois
Parent
commit
6eb5511a23
4 fichiers modifiés avec 807 ajouts et 6 suppressions
  1. 5 0
      src/router/index.js
  2. 45 5
      src/services/api.js
  3. 7 1
      src/views/MainView.vue
  4. 750 0
      src/views/TeamConfigView.vue

+ 5 - 0
src/router/index.js

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

+ 45 - 5
src/services/api.js

@@ -85,27 +85,27 @@ export const listSysConfig = async (page = 0, size = 20, type) => {
   const params = { page, size }
   if (type) params.type = type
 
-  const response = await api.get('/sys-config', { params })
+  const response = await api.get('/config', { params })
   return response.data
 }
 
 export const createSysConfig = async (configData) => {
-  const response = await api.post('/sys-config', configData)
+  const response = await api.post('/config', configData)
   return response.data
 }
 
 export const updateSysConfig = async (name, configData) => {
-  const response = await api.post(`/sys-config/update/${name}`, configData)
+  const response = await api.post(`/config/update/${name}`, configData)
   return response.data
 }
 
 export const deleteSysConfig = async (name) => {
-  const response = await api.post(`/sys-config/delete/${name}`)
+  const response = await api.post(`/config/delete/${name}`)
   return response.data
 }
 
 export const getSysConfigByName = async (name) => {
-  const response = await api.get(`/sys-config/${name}`)
+  const response = await api.get(`/config/${name}`)
   return response.data
 }
 
@@ -428,3 +428,43 @@ export const getLinkStatistics = async () => {
   const response = await api.get('/links/statistics/summary')
   return response.data
 }
+
+// ==================== 团队配置相关API ====================
+
+// 创建团队配置
+export const createTeamConfig = async (configData) => {
+  const response = await api.post('/config/team', configData)
+  return response.data
+}
+
+// 更新团队配置
+export const updateTeamConfig = async (name, configData) => {
+  const response = await api.post(`/config/team/${name}`, configData)
+  return response.data
+}
+
+// 删除团队配置
+export const deleteTeamConfig = async (name, teamId) => {
+  const response = await api.post(`/config/team/${name}/delete`, { teamId })
+  return response.data
+}
+
+// 获取单个团队配置
+export const getTeamConfigByName = async (name, teamId) => {
+  const params = {}
+  if (teamId) params.teamId = teamId
+  const response = await api.get(`/config/team/${name}`, { params })
+  return response.data
+}
+
+// 获取团队配置列表
+export const listTeamConfig = async (page = 0, size = 20, name, type, teamId) => {
+  const params = { page, size }
+  if (name) params.name = name
+  if (type) params.type = type
+  if (teamId) params.teamId = teamId
+
+  const response = await api.get('/config/team', { params })
+  return response.data
+}
+

+ 7 - 1
src/views/MainView.vue

@@ -38,7 +38,7 @@ const allNavItems = [
   {
     label: '团队管理',
     icon: 'pi pi-fw pi-building',
-    roles: ['admin', 'team'],
+    roles: ['admin', 'team', 'promoter'],
     items: [
       {
         label: '团队信息',
@@ -51,6 +51,12 @@ const allNavItems = [
         icon: 'pi pi-fw pi-users',
         name: 'team-members',
         roles: ['admin', 'team']
+      },
+      {
+        label: '团队配置',
+        icon: 'pi pi-fw pi-sliders-h',
+        name: 'team-config',
+        roles: ['admin', 'team', 'promoter']
       }
     ]
   },

+ 750 - 0
src/views/TeamConfigView.vue

@@ -0,0 +1,750 @@
+<script setup>
+import { ref, onMounted, reactive, computed, inject } from 'vue'
+import {
+  listTeamConfig,
+  createTeamConfig,
+  updateTeamConfig,
+  deleteTeamConfig,
+  getTeamConfigByName,
+  uploadFile as uploadFileAPI
+} from '@/services/api'
+import { ConfigType } from '@/enums/index'
+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 InputNumber from 'primevue/inputnumber'
+import DatePicker from 'primevue/datepicker'
+import Textarea from 'primevue/textarea'
+import { useDateFormat } from '@vueuse/core'
+import RadioButton from 'primevue/radiobutton'
+import Slider from 'primevue/slider'
+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 query = ref({})
+const tableData = ref({
+  data: [],
+  meta: {
+    total: 0,
+    page: 0,
+    size: 20
+  }
+})
+const selectedConfig = ref(null)
+const configTypes = ref([])
+const dialogVisible = ref(false)
+const isEditing = ref(false)
+const uploading = ref(false)
+const configModel = reactive({
+  name: '',
+  type: 'string',
+  value: '',
+  remark: '',
+  teamId: null
+})
+
+// 复杂类型值的临时存储
+const tempValue = ref({
+  object: {},
+  file: '',
+  time_range: [],
+  range: []
+})
+
+// 搜索表单
+const searchForm = ref({
+  name: '',
+  type: ''
+})
+
+// 计算当前用户的团队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 || isTeam.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 listTeamConfig(
+      page,
+      tableData.value.meta.size,
+      searchForm.value.name || undefined,
+      searchForm.value.type || undefined,
+      currentTeamId.value
+    )
+    tableData.value = result || {
+      data: [],
+      meta: {
+        total: 0,
+        page: 0,
+        size: 20,
+        totalPages: 0
+      }
+    }
+  } 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 fetchConfigTypes = () => {
+  configTypes.value = Object.entries(ConfigType).map(([key, value]) => ({
+    label: value,
+    value: key
+  }))
+}
+
+const handlePageChange = (event) => {
+  fetchData(event.page)
+}
+
+const refreshData = () => {
+  const page = tableData.value.meta?.page || 0
+  fetchData(page)
+}
+
+const handleSearch = () => {
+  tableData.value.meta.page = 0
+  fetchData()
+}
+
+const handleRefresh = () => {
+  searchForm.value = {
+    name: '',
+    type: ''
+  }
+  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 = () => {
+  configModel.name = ''
+  configModel.type = 'string'
+  configModel.value = ''
+  configModel.remark = ''
+  configModel.teamId = currentTeamId.value
+  tempValue.value = {
+    object: {},
+    file: '',
+    time_range: [],
+    range: [0, 0]
+  }
+}
+
+const onEdit = async (config = null) => {
+  resetModel()
+
+  if (config) {
+    isEditing.value = true
+    selectedConfig.value = config
+    try {
+      const detail = await getTeamConfigByName(config.name, currentTeamId.value)
+
+      // 填充表单数据
+      configModel.name = detail.name
+      configModel.type = detail.type
+      configModel.remark = detail.remark
+      configModel.teamId = detail.teamId
+
+      // 根据类型处理值
+      if (detail.type === 'object') {
+        try {
+          tempValue.value.object = typeof detail.value === 'string' ? JSON.parse(detail.value) : detail.value
+          tempValue.value.object = JSON.stringify(tempValue.value.object, null, 2)
+        } catch (e) {
+          tempValue.value.object = '{}'
+        }
+      } else if (detail.type === 'file') {
+        configModel.value = detail.value
+      } else if (detail.type === 'boolean') {
+        configModel.value = detail.value === true || detail.value === 'true' || detail.value === '1' ? '1' : '0'
+      } else if (detail.type === 'time_range') {
+        if (Array.isArray(detail.value)) {
+          tempValue.value.time_range = detail.value
+        } else if (typeof detail.value === 'string' && detail.value.includes(',')) {
+          const timeParts = detail.value.split(',')
+          if (timeParts.length === 2) {
+            const today = new Date()
+            const startTime = new Date(today)
+            const endTime = new Date(today)
+
+            let startParts = timeParts[0].split(':')
+            let endParts = timeParts[1].split(':')
+
+            if (startParts.length >= 2 && endParts.length >= 2) {
+              startTime.setHours(parseInt(startParts[0]), parseInt(startParts[1]), 0)
+              endTime.setHours(parseInt(endParts[0]), parseInt(endParts[1]), 0)
+              tempValue.value.time_range = [startTime, endTime]
+            } else {
+              tempValue.value.time_range = []
+            }
+          } else {
+            tempValue.value.time_range = []
+          }
+        } else {
+          tempValue.value.time_range = []
+        }
+      } else if (detail.type === 'range') {
+        if (Array.isArray(detail.value)) {
+          tempValue.value.range = detail.value
+        } else if (typeof detail.value === 'string' && detail.value.includes(',')) {
+          tempValue.value.range = detail.value.split(',').map(Number)
+        } else {
+          tempValue.value.range = [0, 100]
+        }
+      } else {
+        configModel.value = detail.value
+      }
+    } 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 (!configModel.name) {
+    toast.add({ severity: 'warn', summary: '警告', detail: '配置名称不能为空', life: 3000 })
+    return false
+  }
+
+  if (!configModel.type) {
+    toast.add({ severity: 'warn', summary: '警告', detail: '配置类型不能为空', life: 3000 })
+    return false
+  }
+
+  if (configModel.type === 'time_range') {
+    if (
+      !Array.isArray(tempValue.value.time_range) ||
+      tempValue.value.time_range.length !== 2 ||
+      !tempValue.value.time_range[0] ||
+      !tempValue.value.time_range[1]
+    ) {
+      toast.add({ severity: 'warn', summary: '警告', detail: '请选择有效的时间范围', life: 3000 })
+      return false
+    }
+  }
+
+  // 管理员必须指定teamId
+  if (isAdmin.value && !configModel.teamId) {
+    toast.add({ severity: 'warn', summary: '警告', detail: '请选择团队', life: 3000 })
+    return false
+  }
+
+  return true
+}
+
+const getValueForSubmit = () => {
+  const type = configModel.type
+
+  if (type === 'object') {
+    try {
+      const parsedObj =
+        typeof tempValue.value.object === 'string' ? JSON.parse(tempValue.value.object) : tempValue.value.object
+      return JSON.stringify(parsedObj)
+    } catch (e) {
+      console.error('JSON解析错误', e)
+      return '{}'
+    }
+  } else if (type === 'time_range') {
+    if (Array.isArray(tempValue.value.time_range) && tempValue.value.time_range.length === 2) {
+      const formattedTimes = tempValue.value.time_range.map((date) => {
+        if (date instanceof Date) {
+          return useDateFormat(date, 'HH:mm').value + ':00'
+        }
+        return date
+      })
+      return formattedTimes.join(',')
+    }
+    return Array.isArray(tempValue.value.time_range) ? tempValue.value.time_range.join(',') : tempValue.value.time_range
+  } else if (type === 'date') {
+    if (configModel.value instanceof Date) {
+      return useDateFormat(configModel.value, 'YYYY-MM-DD').value
+    }
+    return configModel.value
+  } else if (type === 'range') {
+    return Array.isArray(tempValue.value.range) ? tempValue.value.range.join(',') : tempValue.value.range
+  } else if (type === 'boolean') {
+    return configModel.value === '1'
+  } else if (type === 'number') {
+    return Number(configModel.value)
+  } else if (type === 'file') {
+    return configModel.value
+  } else {
+    return configModel.value
+  }
+}
+
+const onSubmit = async () => {
+  if (!validateForm()) return
+
+  try {
+    const configData = {
+      name: configModel.name,
+      type: configModel.type,
+      value: getValueForSubmit(),
+      remark: configModel.remark
+    }
+
+    // 管理员需要传递teamId
+    if (isAdmin.value) {
+      configData.teamId = configModel.teamId
+    }
+
+    if (isEditing.value) {
+      await updateTeamConfig(configModel.name, configData)
+      toast.add({ severity: 'success', summary: '成功', detail: '更新配置成功', life: 3000 })
+    } else {
+      await createTeamConfig(configData)
+      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 (config) => {
+  confirm.require({
+    message: `确定要删除配置 "${config.name}" 吗?`,
+    header: '删除确认',
+    icon: 'pi pi-exclamation-triangle',
+    rejectLabel: '取消',
+    rejectProps: {
+      label: '取消',
+      severity: 'secondary'
+    },
+    acceptLabel: '删除',
+    acceptProps: {
+      label: '删除',
+      severity: 'danger'
+    },
+    accept: async () => {
+      try {
+        await deleteTeamConfig(config.name, currentTeamId.value)
+        toast.add({ severity: 'success', summary: '成功', detail: '删除配置成功', life: 3000 })
+        refreshData()
+      } catch (error) {
+        toast.add({ severity: 'error', summary: '错误', detail: '删除配置失败', life: 3000 })
+      }
+    }
+  })
+}
+
+const formatValue = (value, type) => {
+  if (value === null || value === undefined) return '-'
+
+  switch (type) {
+    case 'boolean':
+      return value === true || value === 'true' || value === '1' ? '是' : '否'
+    case 'object':
+      try {
+        const obj = typeof value === 'string' ? JSON.parse(value) : value
+        return JSON.stringify(obj)
+      } catch (e) {
+        return String(value)
+      }
+    case 'time_range':
+      if (typeof value === 'string') {
+        return value
+      } else if (Array.isArray(value)) {
+        return value
+          .map((time) => {
+            if (time instanceof Date) {
+              return useDateFormat(time, 'HH:mm').value + ':00'
+            }
+            return time
+          })
+          .join(',')
+      }
+      return String(value)
+    case 'range':
+      return Array.isArray(value) ? value.join(',') : String(value)
+    default:
+      return String(value)
+  }
+}
+
+const handleTypeChange = () => {
+  configModel.value = ''
+
+  if (configModel.type === 'boolean') {
+    configModel.value = '0'
+  } else if (configModel.type === 'number') {
+    configModel.value = 0
+  } else if (configModel.type === 'range') {
+    tempValue.value.range = [0, 100]
+  } else if (configModel.type === 'time_range') {
+    const today = new Date()
+    const startTime = new Date(today)
+    const endTime = new Date(today)
+
+    startTime.setHours(8, 0, 0)
+    endTime.setHours(18, 0, 0)
+
+    tempValue.value.time_range = [startTime, endTime]
+  }
+}
+
+const uploadFile = async () => {
+  const input = document.createElement('input')
+  input.type = 'file'
+  input.onchange = async (e) => {
+    const file = e.target.files[0]
+    if (!file) return
+
+    uploading.value = true
+
+    try {
+      const result = await uploadFileAPI(file)
+      configModel.value = result.data.url
+      toast.add({ severity: 'success', summary: '成功', detail: '文件上传成功', life: 3000 })
+    } catch (error) {
+      console.error('文件上传失败', error)
+      toast.add({ severity: 'error', summary: '错误', detail: '文件上传失败: ' + (error.message || error), life: 3000 })
+    } finally {
+      uploading.value = false
+    }
+  }
+
+  input.click()
+}
+
+// 计算团队选项(仅管理员需要)
+const teamOptions = computed(() => {
+  if (!isAdmin.value) return []
+  return 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()
+  fetchConfigTypes()
+})
+</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="tableData.meta.size"
+        :rowsPerPageOptions="[10, 20, 50, 100]"
+        :totalRecords="tableData.meta.total"
+        @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.name"
+                placeholder="配置名称"
+                size="small"
+                class="w-32"
+                @keyup.enter="handleSearch"
+              />
+              <Select
+                v-model="searchForm.type"
+                :options="configTypes"
+                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="name" header="配置名称" style="min-width: 150px" headerClass="font-bold"></Column>
+        <Column field="remark" header="备注" style="min-width: 200px" headerClass="font-bold"></Column>
+        <Column field="value" header="配置值" style="min-width: 250px" headerClass="font-bold">
+          <template #body="slotProps">
+            <div class="max-w-xl truncate">
+              {{ formatValue(slotProps.data.value, slotProps.data.type) }}
+            </div>
+          </template>
+        </Column>
+        <Column field="type" header="配置类型" style="min-width: 100px" headerClass="font-bold">
+          <template #body="slotProps">
+            {{ ConfigType[slotProps.data.type] || slotProps.data.type }}
+          </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="configModel.teamId"
+              :options="teamOptions"
+              optionLabel="label"
+              optionValue="value"
+              placeholder="请选择团队"
+              :disabled="isEditing"
+            />
+            <small v-if="!configModel.teamId" class="p-error">请选择团队</small>
+          </div>
+
+          <div class="flex flex-col gap-2">
+            <label class="font-medium">配置名称</label>
+            <InputText v-model="configModel.name" placeholder="请输入配置名称" :disabled="isEditing" />
+            <small v-if="!configModel.name && configModel.name !== 0" class="p-error">请输入配置名称</small>
+          </div>
+
+          <div class="flex flex-col gap-2">
+            <label class="font-medium">备注</label>
+            <InputText v-model="configModel.remark" placeholder="请输入配置备注" />
+            <small v-if="!configModel.remark && configModel.remark !== 0" class="p-error">请输入备注</small>
+          </div>
+
+          <div class="flex flex-col gap-2">
+            <label class="font-medium">配置类型</label>
+            <Select
+              v-model="configModel.type"
+              :options="configTypes"
+              optionLabel="label"
+              optionValue="value"
+              @change="handleTypeChange"
+              :disabled="isEditing"
+              placeholder="请选择配置类型"
+            />
+            <small v-if="!configModel.type" class="p-error">请选择配置类型</small>
+          </div>
+
+          <!-- 根据类型显示不同的输入控件 -->
+          <div class="flex flex-col gap-2">
+            <label class="font-medium">配置值</label>
+
+            <!-- 文件类型 -->
+            <div v-if="configModel.type === 'file'" class="flex gap-2">
+              <InputText v-model="configModel.value" placeholder="请选择文件" readonly class="flex-1" />
+              <Button type="button" icon="pi pi-upload" @click="uploadFile" :loading="uploading" />
+            </div>
+
+            <!-- 布尔类型 -->
+            <div v-else-if="configModel.type === 'boolean'" class="flex gap-4">
+              <div class="flex items-center gap-2">
+                <RadioButton v-model="configModel.value" value="1" inputId="yes" />
+                <label for="yes">是</label>
+              </div>
+              <div class="flex items-center gap-2">
+                <RadioButton v-model="configModel.value" value="0" inputId="no" />
+                <label for="no">否</label>
+              </div>
+            </div>
+
+            <!-- 时间范围类型 -->
+            <div v-else-if="configModel.type === 'time_range'" class="flex flex-col gap-2">
+              <div class="grid grid-cols-2 gap-4">
+                <div class="flex flex-col gap-1">
+                  <label class="text-sm">开始时间</label>
+                  <DatePicker
+                    v-model="tempValue.time_range[0]"
+                    timeOnly
+                    showTime
+                    format="HH:mm"
+                    hourFormat="24"
+                    placeholder="开始时间"
+                    :showIcon="true"
+                    :showButtonBar="true"
+                  />
+                </div>
+                <div class="flex flex-col gap-1">
+                  <label class="text-sm">结束时间</label>
+                  <DatePicker
+                    v-model="tempValue.time_range[1]"
+                    timeOnly
+                    showTime
+                    format="HH:mm"
+                    hourFormat="24"
+                    placeholder="结束时间"
+                    :showIcon="true"
+                    :showButtonBar="true"
+                  />
+                </div>
+              </div>
+              <div class="flex justify-between text-sm text-gray-500 px-1 mt-1">
+                <span
+                  >当前选择:
+                  {{ tempValue.time_range[0] ? useDateFormat(tempValue.time_range[0], 'HH:mm').value : '--:--' }}</span
+                >
+                <span>至</span>
+                <span>{{
+                  tempValue.time_range[1] ? useDateFormat(tempValue.time_range[1], 'HH:mm').value : '--:--'
+                }}</span>
+              </div>
+            </div>
+
+            <!-- 范围类型 -->
+            <div v-else-if="configModel.type === 'range'" class="flex flex-col gap-2">
+              <Slider v-model="tempValue.range" range :max="500" :step="5" />
+              <div class="flex justify-between">
+                <span>{{ tempValue.range[0] }}</span>
+                <span>{{ tempValue.range[1] }}</span>
+              </div>
+            </div>
+
+            <!-- 数字类型 -->
+            <InputNumber
+              v-else-if="configModel.type === 'number'"
+              v-model="configModel.value"
+              placeholder="请输入数字值"
+            />
+
+            <!-- 日期类型 -->
+            <DatePicker
+              v-else-if="configModel.type === 'date'"
+              v-model="configModel.value"
+              dateFormat="yy-mm-dd"
+              :showIcon="true"
+              :showButtonBar="true"
+              placeholder="请选择日期"
+            />
+
+            <!-- 对象类型 -->
+            <Textarea
+              v-else-if="configModel.type === 'object'"
+              v-model="tempValue.object"
+              placeholder="请输入JSON对象"
+              rows="5"
+            />
+
+            <!-- 字符串类型(默认) -->
+            <Textarea v-else v-model="configModel.value" 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" autofocus />
+        </template>
+      </Dialog>
+    </div>
+  </div>
+</template>
+
+<style scoped>
+.team-name-text {
+  color: #7c3aed;
+  font-weight: 500;
+}
+</style>