Form.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455
  1. <template>
  2. <Dialog
  3. scroll
  4. @close="dialogVisible = false"
  5. style="max-width: 100vw; min-width: 800px"
  6. v-model="dialogVisible"
  7. :title="dialogTitle"
  8. class="OutwardRegistrationBak"
  9. noPaddingEL="OutwardRegistrationBak"
  10. >
  11. <el-form ref="formRef" :model="dataForm" :label-width="labelWidth">
  12. <div style="display: flex; flex-direction: column; margin-top: 20px">
  13. <text>当前规则:</text>
  14. <el-radio-group v-model="dataForm.type" class="ml-4">
  15. <el-radio v-if="!isTodayAdd" :value="1" size="large">每天</el-radio>
  16. <el-radio v-if="isTodayAdd" :value="2" size="large">单次</el-radio>
  17. </el-radio-group>
  18. </div>
  19. <div style="display: flex; align-items: center; flex-direction: row" v-if="!isEdit">
  20. <el-button v-if="!isEdit" @click="addTimes" type="primary" style="margin-left: 10px">
  21. 新增时间段
  22. </el-button>
  23. <template v-if="formMode === 'dailyAdd'">
  24. <el-text style="margin-left: 8vw">可提前</el-text>
  25. <el-input
  26. :maxlength="2"
  27. style="max-width: 66px; margin-left: 8px"
  28. v-model="dataForm.advancedDays"
  29. />
  30. <el-text style="margin-left: 8px">天预约</el-text>
  31. </template>
  32. </div>
  33. <div class="info-wrap">
  34. <el-row
  35. v-for="(item, index) in dataForm.slots"
  36. :key="index"
  37. style="
  38. position: relative;
  39. padding: 10px 0;
  40. margin-bottom: 10px;
  41. border: #e0e0e0 solid 1px;
  42. border-radius: 6px;
  43. "
  44. >
  45. <el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
  46. <div style="flex-direction: row; display: flex; align-items: center; width: 100%">
  47. <text
  48. style="
  49. display: flex;
  50. width: 20px;
  51. height: 20px;
  52. margin-left: 10px;
  53. border: #ddd solid 1px;
  54. border-radius: 50%;
  55. align-items: center;
  56. justify-content: center;
  57. "
  58. >{{ index + 1 }}</text
  59. >
  60. <div style="display: flex; flex-direction: column; margin-left: -12px">
  61. <el-form-item label="开始时间">
  62. <el-select
  63. v-model="item.hourS"
  64. class="m-2"
  65. placeholder="Select"
  66. style="width: 64px"
  67. >
  68. <el-option
  69. v-for="items in optionsH"
  70. :key="items.value"
  71. :label="items.label"
  72. :value="items.value"
  73. />
  74. </el-select>
  75. <el-select
  76. v-model="item.minuteS"
  77. class="m-2"
  78. placeholder="Select"
  79. style="width: 64px"
  80. >
  81. <el-option
  82. v-for="items in optionsM"
  83. :key="items.value"
  84. :label="items.label"
  85. :value="items.value"
  86. />
  87. </el-select>
  88. </el-form-item>
  89. <el-form-item label="结束时间">
  90. <el-select
  91. v-model="item.hourE"
  92. class="m-2"
  93. placeholder="Select"
  94. style="width: 64px"
  95. >
  96. <el-option
  97. v-for="items in optionsH"
  98. :key="items.value"
  99. :label="items.label"
  100. :value="items.value"
  101. />
  102. </el-select>
  103. <el-select
  104. v-model="item.minuteE"
  105. class="m-2"
  106. placeholder="Select"
  107. style="width: 64px"
  108. >
  109. <el-option
  110. v-for="items in optionsM"
  111. :key="items.value"
  112. :label="items.label"
  113. :value="items.value"
  114. />
  115. </el-select>
  116. </el-form-item>
  117. </div>
  118. <div style="align-items: center; display: flex; flex-direction: row">
  119. <text style="margin-left: 2vw">此时间段可预约次数:</text>
  120. <el-input
  121. :maxlength="5"
  122. placeholder="输入次数"
  123. style="width: 78px; margin-left: 3px"
  124. v-model="item.totalQuota"
  125. />
  126. </div>
  127. <el-icon
  128. v-if="!isEdit"
  129. @click="deleteItem(index)"
  130. color="red"
  131. size="large"
  132. style="
  133. position: absolute;
  134. top: 0;
  135. right: 0;
  136. width: 30px;
  137. height: 30px;
  138. margin-left: auto;
  139. cursor: pointer;
  140. "
  141. ><Close
  142. /></el-icon>
  143. </div>
  144. </el-col>
  145. </el-row>
  146. </div>
  147. </el-form>
  148. <template #footer>
  149. <el-button @click="handleClosed">关闭</el-button>
  150. <el-button v-loading="formLoading" type="primary" @click="submitForm">确定</el-button>
  151. </template>
  152. </Dialog>
  153. </template>
  154. <script lang="ts" setup>
  155. import { computed, ref } from 'vue'
  156. import { useMediaQuery } from '@vueuse/core'
  157. import { Close } from '@element-plus/icons-vue'
  158. import fetchHttp from '@/config/axios/fetchHttp'
  159. import { compareTimeStrings, isValidTimeFormat } from '@/utils/dateUtil'
  160. import { useUserStore } from '@/store/modules/user'
  161. type FormMode = 'dailyAdd' | 'dailyEdit' | 'todayAdd'
  162. type TimeslotEditItem = {
  163. id?: number
  164. timeslot: string
  165. hourS: string
  166. minuteS: string
  167. hourE: string
  168. minuteE: string
  169. totalQuota: string | number
  170. }
  171. type DailyEditApiItem = {
  172. id: number
  173. timeQuantum: string
  174. totalQuota: number
  175. }
  176. const { t } = useI18n()
  177. const message = useMessage()
  178. const userStore = useUserStore()
  179. const formRef = ref()
  180. const formLoading = ref(false)
  181. const dialogVisible = ref(false)
  182. const formMode = ref<FormMode>('dailyAdd')
  183. const currentWidth = useMediaQuery('(max-width: 800px)')
  184. const labelWidth = computed(() => (currentWidth.value ? '110px' : '120px'))
  185. const isEdit = computed(() => formMode.value === 'dailyEdit')
  186. const isTodayAdd = computed(() => formMode.value === 'todayAdd')
  187. const isDailyAdd = computed(() => formMode.value === 'dailyAdd')
  188. const dialogTitle = computed(() => {
  189. if (formMode.value === 'dailyEdit') return '编辑预约时间段'
  190. if (formMode.value === 'todayAdd') return '新增单日时间段'
  191. return '新增预约时间段'
  192. })
  193. const dataForm = ref({
  194. organizationId: undefined as any,
  195. date: '',
  196. advancedDays: '',
  197. type: 1,
  198. slots: [] as TimeslotEditItem[]
  199. })
  200. const optionsH = ref(
  201. Array.from({ length: 24 }, (_, i) => {
  202. const v = String(i).padStart(2, '0')
  203. return { value: v, label: v }
  204. })
  205. )
  206. const optionsM = ref([
  207. { value: '00', label: '00' },
  208. { value: '30', label: '30' }
  209. ])
  210. const makeEmptySlot = (): TimeslotEditItem => ({
  211. timeslot: '',
  212. hourS: '00',
  213. minuteS: '00',
  214. hourE: '00',
  215. minuteE: '00',
  216. totalQuota: '1'
  217. })
  218. const buildTimeslot = (slot: TimeslotEditItem) => {
  219. const sTime = `${slot.hourS}:${slot.minuteS}`
  220. const eTime = `${slot.hourE}:${slot.minuteE}`
  221. if (!isValidTimeFormat(sTime) || !isValidTimeFormat(eTime)) {
  222. throw new Error('时间格式不正确,请使用HH:mm格式')
  223. }
  224. const result = compareTimeStrings(sTime, eTime)
  225. if (result !== -1) {
  226. throw new Error('开始时间不能大于等于结束时间!')
  227. }
  228. return `${sTime}-${eTime}`
  229. }
  230. const normalizeAndValidateSlots = () => {
  231. for (const slot of dataForm.value.slots) {
  232. if (slot.totalQuota === '' || slot.totalQuota === undefined || slot.totalQuota === null) {
  233. throw new Error('请选择时间和输入次数')
  234. }
  235. const total = Number.parseInt(String(slot.totalQuota), 10)
  236. if (Number.isNaN(total)) {
  237. throw new Error('请输入数字!')
  238. }
  239. slot.totalQuota = total
  240. slot.timeslot = buildTimeslot(slot) || ''
  241. }
  242. }
  243. const addTimes = () => {
  244. dataForm.value.slots.push(makeEmptySlot())
  245. }
  246. const deleteItem = (index: number) => {
  247. dataForm.value.slots.splice(index, 1)
  248. }
  249. const resetForm = () => {
  250. dataForm.value = {
  251. organizationId: undefined,
  252. date: '',
  253. advancedDays: '',
  254. type: 1,
  255. slots: []
  256. }
  257. formRef.value?.resetFields()
  258. }
  259. const open = async (payload:{date: string, organizationId: number}) => {
  260. formMode.value = 'dailyAdd'
  261. resetForm()
  262. dialogVisible.value = true
  263. dataForm.value.date = payload.date
  264. dataForm.value.type = 1
  265. dataForm.value.organizationId = payload.organizationId
  266. }
  267. const openEdit = async (payload: { date: string; organizationId: number; list: DailyEditApiItem[] }) => {
  268. formMode.value = 'dailyEdit'
  269. resetForm()
  270. dialogVisible.value = true
  271. dataForm.value.date = payload.date
  272. dataForm.value.organizationId = payload.organizationId
  273. dataForm.value.type = 1
  274. dataForm.value.slots = (payload.list || []).map((it) => {
  275. const [start, end] = (it.timeQuantum || '').split('-')
  276. const [hourS, minuteS] = (start || '').split(':')
  277. const [hourE, minuteE] = (end || '').split(':')
  278. return {
  279. id: it.id,
  280. timeslot: it.timeQuantum,
  281. hourS: hourS || '00',
  282. minuteS: minuteS || '00',
  283. hourE: hourE || '00',
  284. minuteE: minuteE || '00',
  285. totalQuota: String(it.totalQuota ?? 1)
  286. }
  287. })
  288. }
  289. const openToday = async (payload: { date: string; organizationId: number }) => {
  290. formMode.value = 'todayAdd'
  291. resetForm()
  292. dialogVisible.value = true
  293. dataForm.value.date = payload.date
  294. dataForm.value.organizationId = payload.organizationId
  295. dataForm.value.type = 2
  296. if (dataForm.value.slots.length === 0) addTimes()
  297. }
  298. defineExpose({ open, openEdit, openToday })
  299. const emit = defineEmits(['success'])
  300. const submitForm = async () => {
  301. if (formLoading.value) return
  302. formLoading.value = true
  303. try {
  304. try {
  305. if (!formRef.value) return
  306. const valid = await formRef.value.validate()
  307. if (!valid) return
  308. if (isDailyAdd.value) {
  309. const num = Number.parseInt(String(dataForm.value.advancedDays), 10)
  310. if (Number.isNaN(num)) {
  311. message.error('天数请输入数字!')
  312. return
  313. }
  314. }
  315. } catch {
  316. if (isDailyAdd.value) {
  317. message.error('天数请输入数字!')
  318. return
  319. }
  320. }
  321. try {
  322. normalizeAndValidateSlots()
  323. } catch (e: any) {
  324. message.error(e?.message || '提交失败')
  325. return
  326. }
  327. if (formMode.value === 'dailyEdit') {
  328. const submitSlots = dataForm.value.slots.map((s) => ({
  329. slotId: s.id,
  330. timeslot: s.timeslot,
  331. totalQuota: Number(s.totalQuota)
  332. }))
  333. const res = await fetchHttp.post('/admin/appointment/updateTimeslotInstance', {
  334. slots: submitSlots
  335. })
  336. if (res) {
  337. dialogVisible.value = false
  338. emit('success')
  339. }
  340. return
  341. }
  342. if (formMode.value === 'todayAdd') {
  343. const submitSlots = dataForm.value.slots.map((s) => ({
  344. timeslot: s.timeslot,
  345. totalQuota: Number(s.totalQuota)
  346. }))
  347. if(!submitSlots.length) {
  348. message.success('请添加时间段!')
  349. return
  350. }
  351. const res = await fetchHttp.post('/admin/appointment/addTimeslotForOneDay', {
  352. organizationId: dataForm.value.organizationId,
  353. date: dataForm.value.date,
  354. slots: submitSlots
  355. })
  356. if (res) {
  357. dialogVisible.value = false
  358. emit('success')
  359. }
  360. return
  361. }
  362. let params = {
  363. ...dataForm.value,
  364. advancedDays: Number(dataForm.value.advancedDays),
  365. slots: dataForm.value.slots.map((s) => ({
  366. timeslot: s.timeslot,
  367. totalQuota: Number(s.totalQuota)
  368. }))
  369. }
  370. const res = await fetchHttp.post('/admin/appointment/setTimeslot', params)
  371. if (res) {
  372. dialogVisible.value = false
  373. emit('success')
  374. }
  375. } finally {
  376. setTimeout(() => {
  377. formLoading.value = false
  378. }, 500)
  379. }
  380. }
  381. const handleClosed = () => {
  382. dialogVisible.value = false
  383. formMode.value = 'dailyAdd'
  384. resetForm()
  385. }
  386. </script>
  387. <style lang="scss" scoped>
  388. .OutwardRegistrationBak {
  389. @media (width <= 1200) {
  390. :deep(.el-form-item--default .el-form-item__label) {
  391. line-height: 0 !important;
  392. }
  393. }
  394. .el-form {
  395. padding: 15px !important;
  396. }
  397. .info-title {
  398. max-width: 180px;
  399. min-width: 75px;
  400. padding: 15px;
  401. font-weight: bolder;
  402. background-color: #f3f6fb;
  403. border-top-right-radius: 50px;
  404. border-bottom-right-radius: 50px;
  405. }
  406. .info-wrap {
  407. margin: 15px;
  408. }
  409. }
  410. :deep(.el-form-item--default) {
  411. display: flex;
  412. flex-direction: row;
  413. align-items: center;
  414. }
  415. </style>