TeamConfigView.vue 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914
  1. <script setup>
  2. import { ref, onMounted, reactive, computed, inject } from 'vue'
  3. import {
  4. listTeamConfig,
  5. createTeamConfig,
  6. updateTeamConfig,
  7. deleteTeamConfig,
  8. getTeamConfigByName,
  9. uploadFile as uploadFileAPI
  10. } from '@/services/api'
  11. import { ConfigType } from '@/enums/index'
  12. import { useToast } from 'primevue/usetoast'
  13. import { useConfirm } from 'primevue/useconfirm'
  14. import DataTable from 'primevue/datatable'
  15. import Column from 'primevue/column'
  16. import Button from 'primevue/button'
  17. import InputText from 'primevue/inputtext'
  18. import Select from 'primevue/select'
  19. import Dialog from 'primevue/dialog'
  20. import InputNumber from 'primevue/inputnumber'
  21. import DatePicker from 'primevue/datepicker'
  22. import Textarea from 'primevue/textarea'
  23. import { useDateFormat } from '@vueuse/core'
  24. import RadioButton from 'primevue/radiobutton'
  25. import Slider from 'primevue/slider'
  26. import { useUserStore } from '@/stores/user'
  27. import { useTeamStore } from '@/stores/team'
  28. const toast = useToast()
  29. const confirm = useConfirm()
  30. const userStore = useUserStore()
  31. const teamStore = useTeamStore()
  32. // 注入权限信息
  33. const isAdmin = inject('isAdmin')
  34. const isTeam = inject('isTeam')
  35. const isPromoter = inject('isPromoter')
  36. const tableRef = ref(null)
  37. const query = ref({})
  38. const tableData = ref({
  39. data: [],
  40. meta: {
  41. total: 0,
  42. page: 0,
  43. size: 20
  44. }
  45. })
  46. const selectedConfig = ref(null)
  47. const configTypes = ref([])
  48. const dialogVisible = ref(false)
  49. const isEditing = ref(false)
  50. const uploading = ref(false)
  51. const configModel = reactive({
  52. name: '',
  53. type: 'string',
  54. value: '',
  55. remark: '',
  56. teamId: null
  57. })
  58. // 复杂类型值的临时存储
  59. const tempValue = ref({
  60. object: {},
  61. file: '',
  62. time_range: [],
  63. range: []
  64. })
  65. // 搜索表单
  66. const searchForm = ref({
  67. name: '',
  68. type: '',
  69. teamId: null
  70. })
  71. // 计算当前用户的团队ID
  72. const currentTeamId = computed(() => {
  73. if (isAdmin.value) {
  74. // 管理员可以选择团队,这里先返回null,在创建/编辑时手动指定
  75. return null
  76. } else if (isTeam.value) {
  77. // 队长从team表获取teamId
  78. return userStore.userInfo?.teamId
  79. } else if (isPromoter.value) {
  80. // 推广员从team-members表获取teamId
  81. return userStore.userInfo?.teamId
  82. }
  83. return null
  84. })
  85. // 计算是否有操作权限
  86. const canCreate = computed(() => isAdmin.value || isTeam.value)
  87. const canUpdate = computed(() => isAdmin.value || isTeam.value)
  88. const canDelete = computed(() => isAdmin.value)
  89. const canView = computed(() => isAdmin.value || isTeam.value || isPromoter.value)
  90. const fetchData = async (page = 0) => {
  91. try {
  92. const result = await listTeamConfig(
  93. page,
  94. tableData.value.meta.size,
  95. searchForm.value.name || undefined,
  96. searchForm.value.type || undefined,
  97. searchForm.value.teamId || currentTeamId.value
  98. )
  99. tableData.value = result || {
  100. data: [],
  101. meta: {
  102. total: 0,
  103. page: 0,
  104. size: 20,
  105. totalPages: 0
  106. }
  107. }
  108. } catch (error) {
  109. console.error('获取团队配置列表失败', error)
  110. toast.add({ severity: 'error', summary: '错误', detail: '获取团队配置列表失败', life: 3000 })
  111. tableData.value = {
  112. data: [],
  113. meta: {
  114. total: 0,
  115. page: 0,
  116. size: 20,
  117. totalPages: 0
  118. }
  119. }
  120. }
  121. }
  122. const fetchConfigTypes = () => {
  123. configTypes.value = Object.entries(ConfigType).map(([key, value]) => ({
  124. label: value,
  125. value: key
  126. }))
  127. }
  128. const handlePageChange = (event) => {
  129. fetchData(event.page)
  130. }
  131. const refreshData = () => {
  132. const page = tableData.value.meta?.page || 0
  133. fetchData(page)
  134. }
  135. const handleSearch = () => {
  136. tableData.value.meta.page = 0
  137. fetchData()
  138. }
  139. const handleRefresh = () => {
  140. searchForm.value = {
  141. name: '',
  142. type: '',
  143. teamId: null
  144. }
  145. tableData.value.meta.page = 0
  146. fetchData()
  147. }
  148. const formatDate = (date) => {
  149. if (!date) return '-'
  150. return useDateFormat(new Date(date), 'YYYY-MM-DD HH:mm:ss').value
  151. }
  152. const resetModel = () => {
  153. configModel.name = ''
  154. configModel.type = 'string'
  155. configModel.value = ''
  156. configModel.remark = ''
  157. configModel.teamId = currentTeamId.value
  158. tempValue.value = {
  159. object: {},
  160. file: '',
  161. time_range: [],
  162. range: [0, 0]
  163. }
  164. }
  165. const onEdit = async (config = null) => {
  166. resetModel()
  167. if (config) {
  168. isEditing.value = true
  169. selectedConfig.value = config
  170. try {
  171. const detail = await getTeamConfigByName(config.name, currentTeamId.value)
  172. // 填充表单数据
  173. configModel.name = detail.name
  174. configModel.type = detail.type
  175. configModel.remark = detail.remark
  176. configModel.teamId = detail.teamId
  177. // 根据类型处理值
  178. if (detail.type === 'object') {
  179. try {
  180. tempValue.value.object = typeof detail.value === 'string' ? JSON.parse(detail.value) : detail.value
  181. tempValue.value.object = JSON.stringify(tempValue.value.object, null, 2)
  182. } catch (e) {
  183. tempValue.value.object = '{}'
  184. }
  185. } else if (detail.type === 'file') {
  186. configModel.value = detail.value
  187. } else if (detail.type === 'boolean') {
  188. configModel.value = detail.value === true || detail.value === 'true' || detail.value === '1' ? '1' : '0'
  189. } else if (detail.type === 'time_range') {
  190. if (Array.isArray(detail.value)) {
  191. tempValue.value.time_range = detail.value
  192. } else if (typeof detail.value === 'string' && detail.value.includes(',')) {
  193. const timeParts = detail.value.split(',')
  194. if (timeParts.length === 2) {
  195. const today = new Date()
  196. const startTime = new Date(today)
  197. const endTime = new Date(today)
  198. let startParts = timeParts[0].split(':')
  199. let endParts = timeParts[1].split(':')
  200. if (startParts.length >= 2 && endParts.length >= 2) {
  201. startTime.setHours(parseInt(startParts[0]), parseInt(startParts[1]), 0)
  202. endTime.setHours(parseInt(endParts[0]), parseInt(endParts[1]), 0)
  203. tempValue.value.time_range = [startTime, endTime]
  204. } else {
  205. tempValue.value.time_range = []
  206. }
  207. } else {
  208. tempValue.value.time_range = []
  209. }
  210. } else {
  211. tempValue.value.time_range = []
  212. }
  213. } else if (detail.type === 'range') {
  214. if (Array.isArray(detail.value)) {
  215. tempValue.value.range = detail.value
  216. } else if (typeof detail.value === 'string' && detail.value.includes(',')) {
  217. tempValue.value.range = detail.value.split(',').map(Number)
  218. } else {
  219. tempValue.value.range = [0, 100]
  220. }
  221. } else {
  222. configModel.value = detail.value
  223. }
  224. } catch (error) {
  225. toast.add({ severity: 'error', summary: '错误', detail: '获取配置详情失败', life: 3000 })
  226. return
  227. }
  228. } else {
  229. isEditing.value = false
  230. }
  231. dialogVisible.value = true
  232. }
  233. const onCancel = () => {
  234. dialogVisible.value = false
  235. }
  236. const validateForm = () => {
  237. if (!configModel.name) {
  238. toast.add({ severity: 'warn', summary: '警告', detail: '配置名称不能为空', life: 3000 })
  239. return false
  240. }
  241. if (!configModel.type) {
  242. toast.add({ severity: 'warn', summary: '警告', detail: '配置类型不能为空', life: 3000 })
  243. return false
  244. }
  245. if (configModel.type === 'time_range') {
  246. if (
  247. !Array.isArray(tempValue.value.time_range) ||
  248. tempValue.value.time_range.length !== 2 ||
  249. !tempValue.value.time_range[0] ||
  250. !tempValue.value.time_range[1]
  251. ) {
  252. toast.add({ severity: 'warn', summary: '警告', detail: '请选择有效的时间范围', life: 3000 })
  253. return false
  254. }
  255. }
  256. // 管理员必须指定teamId
  257. if (isAdmin.value && !configModel.teamId) {
  258. toast.add({ severity: 'warn', summary: '警告', detail: '请选择团队', life: 3000 })
  259. return false
  260. }
  261. return true
  262. }
  263. const getValueForSubmit = () => {
  264. const type = configModel.type
  265. if (type === 'object') {
  266. try {
  267. const parsedObj =
  268. typeof tempValue.value.object === 'string' ? JSON.parse(tempValue.value.object) : tempValue.value.object
  269. return JSON.stringify(parsedObj)
  270. } catch (e) {
  271. console.error('JSON解析错误', e)
  272. return '{}'
  273. }
  274. } else if (type === 'time_range') {
  275. if (Array.isArray(tempValue.value.time_range) && tempValue.value.time_range.length === 2) {
  276. const formattedTimes = tempValue.value.time_range.map((date) => {
  277. if (date instanceof Date) {
  278. return useDateFormat(date, 'HH:mm').value + ':00'
  279. }
  280. return date
  281. })
  282. return formattedTimes.join(',')
  283. }
  284. return Array.isArray(tempValue.value.time_range) ? tempValue.value.time_range.join(',') : tempValue.value.time_range
  285. } else if (type === 'date') {
  286. if (configModel.value instanceof Date) {
  287. return useDateFormat(configModel.value, 'YYYY-MM-DD').value
  288. }
  289. return configModel.value
  290. } else if (type === 'range') {
  291. return Array.isArray(tempValue.value.range) ? tempValue.value.range.join(',') : tempValue.value.range
  292. } else if (type === 'boolean') {
  293. return configModel.value === '1'
  294. } else if (type === 'number') {
  295. return Number(configModel.value)
  296. } else if (type === 'file') {
  297. return configModel.value
  298. } else {
  299. return configModel.value
  300. }
  301. }
  302. const onSubmit = async () => {
  303. if (!validateForm()) return
  304. try {
  305. const configData = {
  306. name: configModel.name,
  307. type: configModel.type,
  308. value: getValueForSubmit(),
  309. remark: configModel.remark
  310. }
  311. // 管理员需要传递teamId
  312. if (isAdmin.value) {
  313. configData.teamId = configModel.teamId
  314. }
  315. if (isEditing.value) {
  316. await updateTeamConfig(configModel.name, configData)
  317. toast.add({ severity: 'success', summary: '成功', detail: '更新配置成功', life: 3000 })
  318. } else {
  319. await createTeamConfig(configData)
  320. toast.add({ severity: 'success', summary: '成功', detail: '创建配置成功', life: 3000 })
  321. }
  322. dialogVisible.value = false
  323. refreshData()
  324. } catch (error) {
  325. toast.add({
  326. severity: 'error',
  327. summary: '错误',
  328. detail: isEditing.value ? '更新配置失败' : '创建配置失败',
  329. life: 3000
  330. })
  331. }
  332. }
  333. const onDelete = async (config) => {
  334. confirm.require({
  335. message: `确定要删除配置 "${config.name}" 吗?`,
  336. header: '删除确认',
  337. icon: 'pi pi-exclamation-triangle',
  338. rejectLabel: '取消',
  339. rejectProps: {
  340. label: '取消',
  341. severity: 'secondary'
  342. },
  343. acceptLabel: '删除',
  344. acceptProps: {
  345. label: '删除',
  346. severity: 'danger'
  347. },
  348. accept: async () => {
  349. try {
  350. await deleteTeamConfig(config.name, currentTeamId.value)
  351. toast.add({ severity: 'success', summary: '成功', detail: '删除配置成功', life: 3000 })
  352. refreshData()
  353. } catch (error) {
  354. toast.add({ severity: 'error', summary: '错误', detail: '删除配置失败', life: 3000 })
  355. }
  356. }
  357. })
  358. }
  359. const formatValue = (value, type) => {
  360. if (value === null || value === undefined) return '-'
  361. switch (type) {
  362. case 'boolean':
  363. return value === true || value === 'true' || value === '1' ? '是' : '否'
  364. case 'object':
  365. try {
  366. const obj = typeof value === 'string' ? JSON.parse(value) : value
  367. return JSON.stringify(obj)
  368. } catch (e) {
  369. return String(value)
  370. }
  371. case 'time_range':
  372. if (typeof value === 'string') {
  373. return value
  374. } else if (Array.isArray(value)) {
  375. return value
  376. .map((time) => {
  377. if (time instanceof Date) {
  378. return useDateFormat(time, 'HH:mm').value + ':00'
  379. }
  380. return time
  381. })
  382. .join(',')
  383. }
  384. return String(value)
  385. case 'range':
  386. return Array.isArray(value) ? value.join(',') : String(value)
  387. default:
  388. return String(value)
  389. }
  390. }
  391. const handleTypeChange = () => {
  392. configModel.value = ''
  393. if (configModel.type === 'boolean') {
  394. configModel.value = '0'
  395. } else if (configModel.type === 'number') {
  396. configModel.value = 0
  397. } else if (configModel.type === 'range') {
  398. tempValue.value.range = [0, 100]
  399. } else if (configModel.type === 'time_range') {
  400. const today = new Date()
  401. const startTime = new Date(today)
  402. const endTime = new Date(today)
  403. startTime.setHours(8, 0, 0)
  404. endTime.setHours(18, 0, 0)
  405. tempValue.value.time_range = [startTime, endTime]
  406. }
  407. }
  408. const uploadFile = async () => {
  409. const input = document.createElement('input')
  410. input.type = 'file'
  411. input.onchange = async (e) => {
  412. const file = e.target.files[0]
  413. if (!file) return
  414. uploading.value = true
  415. try {
  416. const result = await uploadFileAPI(file)
  417. configModel.value = result.data.url
  418. toast.add({ severity: 'success', summary: '成功', detail: '文件上传成功', life: 3000 })
  419. } catch (error) {
  420. console.error('文件上传失败', error)
  421. toast.add({ severity: 'error', summary: '错误', detail: '文件上传失败: ' + (error.message || error), life: 3000 })
  422. } finally {
  423. uploading.value = false
  424. }
  425. }
  426. input.click()
  427. }
  428. // 计算团队选项(仅管理员需要)
  429. const teamOptions = computed(() => {
  430. if (!isAdmin.value) return []
  431. return [
  432. { label: '全部团队', value: null },
  433. ...teamStore.teams.map((team) => ({
  434. label: team.name,
  435. value: team.id
  436. }))
  437. ]
  438. })
  439. // 获取团队名称
  440. const getTeamName = (teamId) => {
  441. if (!teamId) return '-'
  442. const team = teamStore.teams.find((t) => t.id === teamId)
  443. return team ? team.name : '-'
  444. }
  445. onMounted(() => {
  446. fetchData()
  447. fetchConfigTypes()
  448. })
  449. </script>
  450. <template>
  451. <div class="rounded-lg p-4 bg-[var(--p-content-background)]">
  452. <!-- 权限检查 -->
  453. <div v-if="!canView" class="text-center py-8">
  454. <p class="text-gray-500">您没有权限访问团队配置</p>
  455. </div>
  456. <!-- 主要内容 -->
  457. <div v-else>
  458. <DataTable
  459. ref="tableRef"
  460. :value="tableData.data"
  461. :paginator="true"
  462. paginatorTemplate="CurrentPageReport FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink RowsPerPageDropdown JumpToPageInput"
  463. currentPageReportTemplate="{totalRecords} 条记录 "
  464. :rows="Number(tableData.meta.size)"
  465. :rowsPerPageOptions="[10, 20, 50, 100]"
  466. :totalRecords="tableData.meta.total"
  467. @page="handlePageChange"
  468. lazy
  469. scrollable
  470. stripedRows
  471. showGridlines
  472. >
  473. <template #header>
  474. <div class="search-toolbar">
  475. <div class="toolbar-left">
  476. <Button icon="pi pi-refresh" @click="refreshData" size="small" label="刷新" />
  477. <Button v-if="canCreate" icon="pi pi-plus" @click="onEdit()" label="添加" size="small" />
  478. </div>
  479. <div class="toolbar-right">
  480. <div class="search-group">
  481. <InputText
  482. v-model="searchForm.name"
  483. placeholder="配置名称"
  484. size="small"
  485. class="search-field"
  486. @keyup.enter="handleSearch"
  487. />
  488. <Select
  489. v-model="searchForm.type"
  490. :options="configTypes"
  491. optionLabel="label"
  492. optionValue="value"
  493. placeholder="配置类型"
  494. size="small"
  495. class="type-field"
  496. clearable
  497. />
  498. <Select
  499. v-if="isAdmin"
  500. v-model="searchForm.teamId"
  501. :options="teamOptions"
  502. optionLabel="label"
  503. optionValue="value"
  504. placeholder="选择团队"
  505. size="small"
  506. class="team-field"
  507. clearable
  508. filter
  509. filterPlaceholder="搜索团队"
  510. />
  511. </div>
  512. <div class="action-group">
  513. <Button icon="pi pi-search" @click="handleSearch" label="搜索" size="small" severity="secondary" />
  514. <Button icon="pi pi-refresh" @click="handleRefresh" label="重置" size="small" />
  515. </div>
  516. </div>
  517. </div>
  518. </template>
  519. <Column v-if="isAdmin" field="teamId" header="所属团队" style="min-width: 120px" headerClass="font-bold">
  520. <template #body="slotProps">
  521. <span class="team-name-text font-medium">
  522. {{ getTeamName(slotProps.data.teamId) }}
  523. </span>
  524. </template>
  525. </Column>
  526. <Column field="name" header="配置名称" style="min-width: 150px" headerClass="font-bold"></Column>
  527. <Column field="remark" header="备注" style="min-width: 200px" headerClass="font-bold"></Column>
  528. <Column field="value" header="配置值" style="min-width: 250px" headerClass="font-bold">
  529. <template #body="slotProps">
  530. <div class="max-w-xl truncate">
  531. {{ formatValue(slotProps.data.value, slotProps.data.type) }}
  532. </div>
  533. </template>
  534. </Column>
  535. <Column field="type" header="配置类型" style="min-width: 100px" headerClass="font-bold">
  536. <template #body="slotProps">
  537. {{ ConfigType[slotProps.data.type] || slotProps.data.type }}
  538. </template>
  539. </Column>
  540. <Column header="操作" style="min-width: 120px" headerClass="font-bold" align="center">
  541. <template #body="slotProps">
  542. <div class="flex gap-2 justify-center">
  543. <Button v-if="canUpdate" icon="pi pi-pencil" @click="onEdit(slotProps.data)" size="small" />
  544. <Button
  545. v-if="canDelete"
  546. icon="pi pi-trash"
  547. @click="onDelete(slotProps.data)"
  548. size="small"
  549. severity="danger"
  550. />
  551. </div>
  552. </template>
  553. </Column>
  554. </DataTable>
  555. <!-- 添加/编辑配置对话框 -->
  556. <Dialog
  557. v-model:visible="dialogVisible"
  558. :modal="true"
  559. :header="isEditing ? '编辑团队配置' : '添加团队配置'"
  560. :style="{ width: '550px' }"
  561. >
  562. <div class="grid grid-cols-1 gap-4 p-4">
  563. <!-- 管理员选择团队 -->
  564. <div v-if="isAdmin" class="flex flex-col gap-2">
  565. <label class="font-medium">选择团队</label>
  566. <Select
  567. v-model="configModel.teamId"
  568. :options="teamOptions"
  569. optionLabel="label"
  570. optionValue="value"
  571. placeholder="请选择团队"
  572. :disabled="isEditing"
  573. filter
  574. filterPlaceholder="搜索团队"
  575. :showClear="true"
  576. />
  577. <small v-if="!configModel.teamId" class="p-error">请选择团队</small>
  578. </div>
  579. <div class="flex flex-col gap-2">
  580. <label class="font-medium">配置名称</label>
  581. <InputText v-model="configModel.name" placeholder="请输入配置名称" :disabled="isEditing" />
  582. <small v-if="!configModel.name && configModel.name !== 0" class="p-error">请输入配置名称</small>
  583. </div>
  584. <div class="flex flex-col gap-2">
  585. <label class="font-medium">备注</label>
  586. <InputText v-model="configModel.remark" placeholder="请输入配置备注" />
  587. <small v-if="!configModel.remark && configModel.remark !== 0" class="p-error">请输入备注</small>
  588. </div>
  589. <div class="flex flex-col gap-2">
  590. <label class="font-medium">配置类型</label>
  591. <Select
  592. v-model="configModel.type"
  593. :options="configTypes"
  594. optionLabel="label"
  595. optionValue="value"
  596. @change="handleTypeChange"
  597. :disabled="isEditing"
  598. placeholder="请选择配置类型"
  599. />
  600. <small v-if="!configModel.type" class="p-error">请选择配置类型</small>
  601. </div>
  602. <!-- 根据类型显示不同的输入控件 -->
  603. <div class="flex flex-col gap-2">
  604. <label class="font-medium">配置值</label>
  605. <!-- 文件类型 -->
  606. <div v-if="configModel.type === 'file'" class="flex gap-2">
  607. <InputText v-model="configModel.value" placeholder="请选择文件" readonly class="flex-1" />
  608. <Button type="button" icon="pi pi-upload" @click="uploadFile" :loading="uploading" />
  609. </div>
  610. <!-- 布尔类型 -->
  611. <div v-else-if="configModel.type === 'boolean'" class="flex gap-4">
  612. <div class="flex items-center gap-2">
  613. <RadioButton v-model="configModel.value" value="1" inputId="yes" />
  614. <label for="yes">是</label>
  615. </div>
  616. <div class="flex items-center gap-2">
  617. <RadioButton v-model="configModel.value" value="0" inputId="no" />
  618. <label for="no">否</label>
  619. </div>
  620. </div>
  621. <!-- 时间范围类型 -->
  622. <div v-else-if="configModel.type === 'time_range'" class="flex flex-col gap-2">
  623. <div class="grid grid-cols-2 gap-4">
  624. <div class="flex flex-col gap-1">
  625. <label class="text-sm">开始时间</label>
  626. <DatePicker
  627. v-model="tempValue.time_range[0]"
  628. timeOnly
  629. showTime
  630. format="HH:mm"
  631. hourFormat="24"
  632. placeholder="开始时间"
  633. :showIcon="true"
  634. :showButtonBar="true"
  635. />
  636. </div>
  637. <div class="flex flex-col gap-1">
  638. <label class="text-sm">结束时间</label>
  639. <DatePicker
  640. v-model="tempValue.time_range[1]"
  641. timeOnly
  642. showTime
  643. format="HH:mm"
  644. hourFormat="24"
  645. placeholder="结束时间"
  646. :showIcon="true"
  647. :showButtonBar="true"
  648. />
  649. </div>
  650. </div>
  651. <div class="flex justify-between text-sm text-gray-500 px-1 mt-1">
  652. <span
  653. >当前选择:
  654. {{ tempValue.time_range[0] ? useDateFormat(tempValue.time_range[0], 'HH:mm').value : '--:--' }}</span
  655. >
  656. <span>至</span>
  657. <span>{{
  658. tempValue.time_range[1] ? useDateFormat(tempValue.time_range[1], 'HH:mm').value : '--:--'
  659. }}</span>
  660. </div>
  661. </div>
  662. <!-- 范围类型 -->
  663. <div v-else-if="configModel.type === 'range'" class="flex flex-col gap-2">
  664. <Slider v-model="tempValue.range" range :max="500" :step="5" />
  665. <div class="flex justify-between">
  666. <span>{{ tempValue.range[0] }}</span>
  667. <span>{{ tempValue.range[1] }}</span>
  668. </div>
  669. </div>
  670. <!-- 数字类型 -->
  671. <InputNumber
  672. v-else-if="configModel.type === 'number'"
  673. v-model="configModel.value"
  674. placeholder="请输入数字值"
  675. />
  676. <!-- 日期类型 -->
  677. <DatePicker
  678. v-else-if="configModel.type === 'date'"
  679. v-model="configModel.value"
  680. dateFormat="yy-mm-dd"
  681. :showIcon="true"
  682. :showButtonBar="true"
  683. placeholder="请选择日期"
  684. />
  685. <!-- 对象类型 -->
  686. <Textarea
  687. v-else-if="configModel.type === 'object'"
  688. v-model="tempValue.object"
  689. placeholder="请输入JSON对象"
  690. rows="5"
  691. />
  692. <!-- 字符串类型(默认) -->
  693. <Textarea v-else v-model="configModel.value" placeholder="请输入值" rows="3" />
  694. </div>
  695. </div>
  696. <template #footer>
  697. <Button label="取消" icon="pi pi-times" @click="onCancel" class="p-button-text" />
  698. <Button label="保存" icon="pi pi-check" @click="onSubmit" />
  699. </template>
  700. </Dialog>
  701. </div>
  702. </div>
  703. </template>
  704. <style scoped>
  705. .team-name-text {
  706. color: #7c3aed;
  707. font-weight: 500;
  708. }
  709. /* 搜索工具栏样式 */
  710. .search-toolbar {
  711. display: flex;
  712. flex-wrap: wrap;
  713. align-items: center;
  714. justify-content: space-between;
  715. gap: 16px;
  716. margin-bottom: 20px;
  717. padding: 16px;
  718. background: #f8fafc;
  719. border-radius: 8px;
  720. border: 1px solid #e2e8f0;
  721. }
  722. .toolbar-left {
  723. display: flex;
  724. gap: 8px;
  725. align-items: center;
  726. }
  727. .toolbar-left .p-button {
  728. font-size: 13px;
  729. padding: 6px 12px;
  730. border-radius: 6px;
  731. font-weight: 500;
  732. }
  733. .toolbar-right {
  734. display: flex;
  735. gap: 12px;
  736. align-items: center;
  737. flex-wrap: wrap;
  738. }
  739. .search-group {
  740. display: flex;
  741. gap: 8px;
  742. align-items: center;
  743. }
  744. .search-field {
  745. width: 140px;
  746. font-size: 13px;
  747. padding: 6px 10px;
  748. border-radius: 6px;
  749. border: 1px solid #d1d5db;
  750. transition: all 0.2s ease;
  751. }
  752. .search-field:focus {
  753. border-color: #3b82f6;
  754. box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
  755. }
  756. .type-field {
  757. width: 140px;
  758. font-size: 13px;
  759. }
  760. .type-field .p-select {
  761. border-radius: 6px;
  762. }
  763. .team-field {
  764. width: 160px;
  765. font-size: 13px;
  766. }
  767. .team-field .p-select {
  768. border-radius: 6px;
  769. }
  770. .action-group {
  771. display: flex;
  772. gap: 8px;
  773. align-items: center;
  774. }
  775. .action-group .p-button {
  776. font-size: 13px;
  777. padding: 6px 12px;
  778. border-radius: 6px;
  779. font-weight: 500;
  780. }
  781. /* 响应式设计 */
  782. @media (max-width: 768px) {
  783. /* 移动端搜索工具栏适配 */
  784. .search-toolbar {
  785. flex-direction: column;
  786. gap: 12px;
  787. align-items: stretch;
  788. padding: 12px;
  789. }
  790. .toolbar-left {
  791. justify-content: center;
  792. gap: 8px;
  793. }
  794. .toolbar-left .p-button {
  795. flex: 1;
  796. max-width: 120px;
  797. font-size: 13px;
  798. padding: 8px 12px;
  799. }
  800. .toolbar-right {
  801. flex-direction: column;
  802. gap: 12px;
  803. align-items: stretch;
  804. }
  805. .search-group {
  806. flex-direction: column;
  807. gap: 8px;
  808. }
  809. .search-field,
  810. .type-field,
  811. .team-field {
  812. width: 100%;
  813. font-size: 14px;
  814. padding: 10px 12px;
  815. }
  816. .action-group {
  817. justify-content: center;
  818. gap: 8px;
  819. }
  820. .action-group .p-button {
  821. flex: 1;
  822. max-width: 140px;
  823. font-size: 13px;
  824. padding: 8px 12px;
  825. }
  826. }
  827. </style>