calendar.vue 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  1. <!--
  2. * @Author: wanglj wanglijie@dashoo.cn
  3. * @Date: 2025-03-24 16:28:47
  4. * @LastEditors: wanglj wanglijie@dashoo.cn
  5. * @LastEditTime: 2025-03-28 11:47:37
  6. * @FilePath: \labsop_h5\src\view\instr\components\appoint-dialog.vue
  7. * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
  8. -->
  9. <template>
  10. <div class="calendar-container">
  11. <header>
  12. <p>
  13. <strong><van-icon name="calendar-o" :size="16" />预约时间:</strong>
  14. <span v-if="state.selected.length === 0">请选择开始时间段</span>
  15. <span v-else-if="state.selected.length === 1">{{ formatDate(new Date(state.selected[0].startStamp), 'mm-dd HH:MM') }}~{{ formatDate(new Date(state.selected[0].endStamp), 'mm-dd HH:MM') }}</span>
  16. <span v-else-if="state.selected.length === 2">{{ formatDate(new Date(state.selected[0].startStamp), 'mm-dd HH:MM') }}~{{ formatDate(new Date(state.selected[1].endStamp), 'mm-dd HH:MM') }}</span>
  17. </p>
  18. <p>
  19. <strong><van-icon name="clock-o" :size="16" />预约时长:</strong>
  20. <span>{{ getAppointTime() }}</span>
  21. </p>
  22. </header>
  23. <div class="main">
  24. <template v-for="(item, index) in state.calendar">
  25. <h4 v-if="item.startTime == '00:00'">{{ formatDate(new Date(item.startStamp), 'YYYY-mm-dd/WWW') }}</h4>
  26. <div class="tag" :class="{ disabled: item.disabled, selected: item.selected }" @click="onTagClick(item, index)">
  27. {{ item.startTime }}-{{ item.endTime }}
  28. </div>
  29. </template>
  30. </div>
  31. </div>
  32. <van-action-bar placeholder>
  33. <van-action-bar-button class="w100" type="primary" text="提交" @click="onClickButton" />
  34. </van-action-bar>
  35. </template>
  36. <script lang="ts" setup>
  37. import { onMounted, reactive, ref } from 'vue'
  38. import { formatDate } from '/@/utils/formatTime'
  39. import { showNotify } from 'vant'
  40. import { useRoute, useRouter } from 'vue-router'
  41. import moment from 'moment'
  42. import to from 'await-to-js'
  43. import { useInstrApi } from '/@/api/instr'
  44. const route = useRoute()
  45. const router = useRouter()
  46. const instId = ref(0)
  47. const instApi = useInstrApi()
  48. const state = reactive({
  49. instInfo: {} as any,
  50. intervalTime: 30,
  51. furtherLimit: '',
  52. instrBusinessTime: '',
  53. begin_at: '00:00:00',
  54. ent_at: '23:50:00',
  55. currentWeekAppointList: [],
  56. selected: [],
  57. calendar: []
  58. })
  59. // 获取系统设置时间间隔
  60. const getTimeSplit = async () => {
  61. const [err, res]: ToResponse = await to(
  62. instApi.getSettingDetail({
  63. instId: Number(instId.value),
  64. code: 'InstCfgAppoint'
  65. })
  66. )
  67. if (err) return
  68. if (res.code == 200) {
  69. state.intervalTime = res.data?.config?.timeSplit
  70. state.begin_at = res.data?.config?.timeRange?.[0].start
  71. state.ent_at = res.data?.config?.timeRange?.[0].end
  72. appointTimeInfo()
  73. }
  74. }
  75. const appointTimeInfo = async () => {
  76. const currentDate = formatDate(new Date(), 'YYYY-mm-dd')
  77. const nextWeek = formatDate(new Date(new Date().getTime() + 604800000), 'YYYY-mm-dd')
  78. let params = {
  79. instId: instId.value,
  80. date: currentDate,
  81. dateType: 'week'
  82. }
  83. await Promise.all([
  84. instApi.getAppointInfo({ ...params }),
  85. instApi.getAppointInfo({...params, date: nextWeek})
  86. ]).then(([now, next]) => {
  87. const { appoint, unavailable, furtherLimit } = now.data
  88. const { appoint: appointNext, unavailable: unavailableNext } = next.data
  89. let allDate = [...(appoint || []), ...(unavailable || []), ...(appointNext || []), ...(unavailableNext || [])].map((item) => ({
  90. ...item,
  91. start: item.startTime ? item.startTime : item.start,
  92. startStamp: new Date(item.startTime ? item.startTime : item.start).getTime(),
  93. end: item.endTime ? item.endTime : item.end,
  94. endStamp: new Date(item.endTime ? item.endTime : item.end).getTime(),
  95. }))
  96. state.currentWeekAppointList = allDate
  97. state.furtherLimit = furtherLimit ? nearFurtherLimit(furtherLimit, state.intervalTime) : ''
  98. // 设备的工作时间
  99. state.instrBusinessTime = `${state.begin_at}-${state.ent_at}`
  100. initCalendar()
  101. })
  102. }
  103. const nearFurtherLimit = (time, split) => {
  104. // 将目标时间转换为Moment对象
  105. const targetMoment = moment(time)
  106. // 计算距离目标时间最近的能够被时间间隔整除的时间
  107. const remainder = targetMoment.minute() % split
  108. const divisibleTime = targetMoment.clone().subtract(remainder, 'minutes')
  109. const nearestAvailableTime = divisibleTime.format('YYYY/MM/DD HH:mm') + ':00'
  110. return nearestAvailableTime
  111. }
  112. const initCalendar = () => {
  113. const split = state.intervalTime
  114. const now = new Date().getTime()
  115. const start = formatDate(new Date(now), 'YYYY-mm-dd 00:00:00')
  116. const end = formatDate(new Date(now + 1000 * 60 * 60 * 24 * 7), 'YYYY-mm-dd 23:59:59')
  117. let stamp = new Date(start).getTime()
  118. while (stamp <= new Date(end).getTime()) {
  119. const obj = {
  120. startStamp: stamp,
  121. endStamp: stamp + 1000 * split * 60,
  122. startTime: formatDate(new Date(stamp), 'HH:MM'),
  123. endTime: formatDate(new Date(stamp + 1000 * split * 60), 'HH:MM'),
  124. disabled: stamp < now,
  125. selected: false
  126. }
  127. // 禁用不可预约时间段和已预约时间段
  128. for(const item of state.currentWeekAppointList) {
  129. if(obj.endStamp <= item.endStamp) {
  130. obj.disabled = true
  131. continue
  132. }
  133. }
  134. // 最早预约时间
  135. if(state.furtherLimit && obj.startStamp >= new Date(state.furtherLimit).getTime()) {
  136. obj.disabled = true
  137. }
  138. state.calendar.push(obj)
  139. stamp += 1000 * split * 60
  140. }
  141. }
  142. const onTagClick = (item: any, idx: number) => {
  143. if (item.disabled) return
  144. if (item.selected && state.selected[0].startStamp === item.startStamp) {
  145. return
  146. }
  147. if (state.selected.length === 2) {
  148. state.selected = []
  149. for (const item of state.calendar) {
  150. item.selected = false
  151. }
  152. } else if (state.selected.length === 1) {
  153. // 验证选中时间段是否有占用的
  154. const selected = JSON.parse(JSON.stringify(state.selected))
  155. selected.push(item)
  156. selected.sort((a, b) => {
  157. return a.startStamp - b.startStamp
  158. })
  159. let flag = false
  160. for (const item of state.calendar) {
  161. if (item.startStamp >= selected[0].startStamp && item.endStamp <= selected[1].endStamp && item.disabled) {
  162. flag = true
  163. break
  164. }
  165. }
  166. if (flag) {
  167. showNotify({
  168. message: '选中时间段已被占用,请重新选择',
  169. type: 'warning'
  170. })
  171. return
  172. }
  173. }
  174. state.selected.push(item)
  175. item.selected = true
  176. state.selected.sort((a, b) => {
  177. return a.startStamp - b.startStamp
  178. })
  179. if (state.selected.length == 2) {
  180. for (const item of state.calendar) {
  181. if (item.startStamp >= state.selected[0].startStamp && item.endStamp <= state.selected[1].endStamp) {
  182. item.selected = true
  183. }
  184. }
  185. }
  186. }
  187. const getAppointTime = () => {
  188. let startDate: Date = new Date()
  189. let endDate: Date = new Date()
  190. if(state.selected.length === 1) {
  191. startDate = new Date(state.selected[0].startStamp)
  192. endDate = new Date(state.selected[0].endStamp)
  193. } else if(state.selected.length === 2) {
  194. startDate = new Date(state.selected[0].startStamp)
  195. endDate = new Date(state.selected[1].endStamp)
  196. } else {
  197. return '-'
  198. }
  199. // 计算两个日期之间的时间差(以毫秒为单位)
  200. const timeDifference = endDate.getTime() - startDate.getTime()
  201. // 计算天数
  202. const days = Math.floor(timeDifference / (1000 * 60 * 60 * 24))
  203. // 计算剩余的毫秒数
  204. const remainingMilliseconds = timeDifference % (1000 * 60 * 60 * 24)
  205. // 计算小时数
  206. const hours = Math.floor(remainingMilliseconds / (1000 * 60 * 60))
  207. // 计算剩余的毫秒数
  208. const remainingMillisecondsAfterHours = remainingMilliseconds % (1000 * 60 * 60)
  209. // 计算分钟数
  210. const minutes = Math.floor(remainingMillisecondsAfterHours / (1000 * 60))
  211. return `${days}天${hours}小时${minutes}分`
  212. }
  213. const onClickButton = () => {
  214. if(!state.selected.length) {
  215. showNotify({
  216. message: '请选择预约时间',
  217. type: 'warning'
  218. })
  219. return
  220. }
  221. let startTime = 0
  222. let endTime = 0
  223. if(state.selected.length === 1) {
  224. startTime = state.selected[0].startStamp
  225. endTime = state.selected[0].endStamp
  226. } else {
  227. startTime = state.selected[0].startStamp
  228. endTime = state.selected[1].endStamp
  229. }
  230. router.push({
  231. path: '/instr-appoint',
  232. query: {
  233. id: instId.value,
  234. startTime,
  235. endTime
  236. }
  237. })
  238. }
  239. onMounted(() => {
  240. instId.value = route.query.id ? +route.query.id : 0
  241. getTimeSplit()
  242. })
  243. </script>
  244. <style lang="scss" scoped>
  245. .calendar-container {
  246. flex: 1;
  247. overflow: hidden;
  248. display: flex;
  249. flex-direction: column;
  250. header {
  251. padding: 10px;
  252. border-bottom: 1px solid #dcdfe6;
  253. p {
  254. padding: 4px;
  255. i {
  256. margin-right: 4px;
  257. }
  258. }
  259. }
  260. .main {
  261. flex: 1;
  262. overflow-y: auto;
  263. overflow-x: hidden;
  264. padding: 10px 6px;
  265. display: flex;
  266. flex-wrap: wrap;
  267. color: #323233;
  268. h4 {
  269. flex: 0 0 100%;
  270. margin: 4px 0 0 4px;
  271. height: 18px;
  272. line-height: 18px;
  273. display: flex;
  274. &::before {
  275. display: inline-block;
  276. content: '';
  277. width: 3px;
  278. height: 18px;
  279. background-color: #1c9bfd;
  280. margin-right: 4px;
  281. vertical-align: middle;
  282. }
  283. }
  284. .tag {
  285. font-size: 12px;
  286. text-align: center;
  287. flex: 0 0 calc(25% - 14px);
  288. padding: 4px;
  289. border: 1px solid #dcdfe6;
  290. border-radius: 4px;
  291. margin-left: 4px;
  292. margin-top: 4px;
  293. &.disabled {
  294. background-color: #dcdfe6;
  295. }
  296. &.selected {
  297. background-color: #1989fa;
  298. border-color: #1989fa;
  299. color: #fff;
  300. }
  301. }
  302. }
  303. }
  304. </style>