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