its-calendar.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634
  1. <template>
  2. <view>
  3. <view class="calendar">
  4. <scroll-view
  5. class="calendar_day_scroll"
  6. scroll-x="true"
  7. show-scrollbar="false"
  8. >
  9. <view class="calendar_day">
  10. <view
  11. class="day_x"
  12. :style="{ color: day_index == index ? '#FE3B3C' : '' }"
  13. v-for="(item, index) in dayArr"
  14. :key="index"
  15. @click.stop="dayList(item, index)"
  16. >
  17. <view class="day_x_a">{{ item.weeks }}</view>
  18. <view class="day_x_b">{{ item.days }}</view>
  19. </view>
  20. </view>
  21. </scroll-view>
  22. <scroll-view
  23. class="calendar_time_scroll"
  24. scroll-y="true"
  25. show-scrollbar="false"
  26. >
  27. <view class="calendar_time">
  28. <view
  29. class="time_x"
  30. :class="{
  31. time_x_sty: item?.checked,
  32. 'disabled-time': item?.disabled || item?.hasReservation === 1,
  33. }"
  34. v-for="(item, index) in timeHostArr[day_index]"
  35. :key="index"
  36. @click="handleTimeClick(item)"
  37. >
  38. <text>{{ item?.hours }}</text>
  39. </view>
  40. </view>
  41. </scroll-view>
  42. </view>
  43. <view class="calendar_footer">
  44. <view class="selected-time-info">
  45. {{ selectRange.join('\n') }}
  46. </view>
  47. </view>
  48. </view>
  49. </template>
  50. <script>
  51. export default {
  52. props: {
  53. timeArr: {
  54. type: Array,
  55. default: () => [],
  56. },
  57. timeHostArr: {
  58. type: Array,
  59. default: () => [],
  60. },
  61. businessDuration: {
  62. // 从父组件接收的服务时长(分钟)
  63. type: Number,
  64. default: 0,
  65. },
  66. minQuantity: {
  67. // 从父组件接收的购买次数(次数)
  68. type: Number,
  69. default: 1,
  70. },
  71. businessTierName: {
  72. type: String,
  73. default: '',
  74. }
  75. },
  76. data() {
  77. return {
  78. dayArr: [],
  79. day_index: 0,
  80. host_index: '',
  81. nowTimes: new Date().getTime(), // 只保留一个定义
  82. disableAfterTime: null,
  83. selectedTime: null,
  84. selectedDay: null, // 新增:记录选择的日期
  85. selectedTimeSlots: {}, // 存储每个日期的选择状态
  86. upItem: null, // 保留上一个点击的item,外部情况下再刷新状态
  87. filteredSlots: [],
  88. }
  89. },
  90. watch: {
  91. timeArr: {
  92. handler(newVal) {
  93. let dateArr = newVal.map((item) => {
  94. let day = new Date(item)
  95. const daysOfWeek = [
  96. '周日',
  97. '周一',
  98. '周二',
  99. '周三',
  100. '周四',
  101. '周五',
  102. '周六',
  103. ]
  104. return {
  105. weeks: daysOfWeek[day.getDay()],
  106. days: item.slice(5),
  107. }
  108. })
  109. this.dayArr = dateArr
  110. },
  111. immediate: true,
  112. deep: true,
  113. },
  114. timeHostArr: {
  115. handler(newVal) {
  116. // 当timeHostArr更新时,恢复之前的选择状态
  117. if (newVal && newVal.length > 0) {
  118. this.restoreSelections()
  119. }
  120. },
  121. deep: true,
  122. },
  123. },
  124. mounted() {
  125. },
  126. computed: {
  127. // 计算选中区域
  128. selectRange() {
  129. const reservations = this.timeHostArr[this.day_index]
  130. // return current
  131. const result = []
  132. let currentGroup = null
  133. for (const item of reservations) {
  134. if (item.checked) {
  135. // 遇到新的checked项,创建新组
  136. if (currentGroup) {
  137. result.push(currentGroup)
  138. }
  139. currentGroup = [item]
  140. } else if (currentGroup && item.disabled && item.hasReservation === 0) {
  141. // 如果当前有活跃的组,且符合disabled和hasReservation条件,加入当前组
  142. currentGroup.push(item)
  143. } else if (currentGroup) {
  144. // 不符合连续条件,结束当前组
  145. result.push(currentGroup)
  146. currentGroup = null
  147. }
  148. }
  149. // 添加最后一组(如果有)
  150. if (currentGroup) {
  151. result.push(currentGroup)
  152. }
  153. const timeRangeArr = result.map((arr) => {
  154. // 获取选中的日期
  155. const selectedDate = this.timeArr[this.day_index]
  156. const timeRange = arr[0].hours + '-' + arr[arr.length - 1].hours
  157. return `${this.businessTierName}\n预约上门时间:${selectedDate} ${timeRange}`
  158. })
  159. return timeRangeArr
  160. },
  161. },
  162. methods: {
  163. // 恢复选择状态
  164. restoreSelections() {
  165. if (!this.timeHostArr[this.day_index]) return
  166. // 遍历当前日期的时间槽
  167. this.timeHostArr[this.day_index].forEach((slot) => {
  168. // 检查这个时间槽是否在之前被选中
  169. const dateKey = this.timeArr[this.day_index]
  170. if (
  171. this.selectedTimeSlots[dateKey] &&
  172. this.selectedTimeSlots[dateKey].includes(slot.timeStamp)
  173. ) {
  174. slot.checked = true
  175. // 计算并禁用后续的时间段
  176. const seconds = this.businessDuration * this.minQuantity * 60
  177. // const startTimestamp =
  178. const endTimestamp = slot.timeStamp + seconds
  179. // 禁用后续的时间段
  180. this.timeHostArr[this.day_index].forEach((s) => {
  181. // 差一个范围控制变量,是否在可选择范围内
  182. if (s.timeStamp > slot.timeStamp && s.timeStamp <= endTimestamp) {
  183. s.disabled = true
  184. }
  185. })
  186. }
  187. })
  188. },
  189. handleTimeClick(item) {
  190. this.$emit('changeMinQuantityMax', 99999)
  191. this.upItem = item
  192. const durationMs = this.businessDuration * this.minQuantity * 60 * 1000
  193. const itemTime =
  194. item.timeStamp > 9999999999 ? item.timeStamp : item.timeStamp * 1000
  195. const slots = this.timeHostArr[this.day_index] || []
  196. const currentDate = this.timeArr[this.day_index]
  197. // 1. 判断当前时间是否在任何已预约时间段的服务时长范围内
  198. // let inReservedRange = false
  199. // for (let slot of slots) {
  200. // if (slot.hasReservation === 1) {
  201. // const reservedTime =
  202. // slot.timeStamp > 9999999999 ? slot.timeStamp : slot.timeStamp * 1000
  203. // const start = reservedTime - durationMs
  204. // const end = reservedTime + durationMs
  205. // if (itemTime >= start && itemTime < end) {
  206. // inReservedRange = true
  207. // break
  208. // }
  209. // }
  210. // }
  211. // if (inReservedRange) {
  212. // uni.showToast({ title: '服务时长不足,请选择其他时间段', icon: 'none' })
  213. // return false
  214. // }
  215. // 2. 判断当前点击时间与所有已选时间段的服务时长有无重叠(多选判断)
  216. const selectedArr = this.selectedTimeSlots[currentDate] || []
  217. if (!item.checked) {
  218. // 只在选中时判断
  219. for (let t of selectedArr) {
  220. if (t === item.timeStamp) continue // 跳过自己
  221. const tStart = t > 9999999999 ? t : t * 1000
  222. const tEnd = tStart + durationMs
  223. if (
  224. (itemTime >= tStart && itemTime < tEnd) ||
  225. (itemTime < tStart && itemTime + durationMs > tStart)
  226. ) {
  227. uni.showToast({ title: '时间段不在服务范围内', icon: 'none' })
  228. return false
  229. }
  230. }
  231. }
  232. // 3. 检查当前时间到结束是否有足够的时间段
  233. const seconds = this.businessDuration * this.minQuantity * 60
  234. const endTimestamp = item.timeStamp + seconds
  235. // 找出当前时间到服务结束时间之间的所有时间段
  236. const filteredSlots = this.timeHostArr[this.day_index].filter((i) => {
  237. return i.timeStamp > item.timeStamp && i.timeStamp <= endTimestamp
  238. })
  239. this.filteredSlots = filteredSlots
  240. // 检查这些时间段中是否有已预约的时间
  241. const hasReservedSlot = filteredSlots.some(
  242. (slot) => slot.hasReservation === 1
  243. )
  244. if (hasReservedSlot) {
  245. uni.showToast({
  246. title: '所选时间段内有已预约时间,请选择其他时间',
  247. icon: 'none',
  248. })
  249. return false
  250. }
  251. // 时间差值(数组中的最后一项 - 当前点击项)
  252. const timestampDifferenceValue = filteredSlots.length
  253. ? (filteredSlots[filteredSlots.length - 1].timeStamp - item.timeStamp) *
  254. 1000
  255. : 0
  256. // 选择时间,后续服务时间是否充足,不充足结束逻辑执行 timeStamp
  257. if (timestampDifferenceValue < durationMs) {
  258. // 所选时间差值 小于 服务时间值 结束执行
  259. uni.showToast({ title: '所选时间的服务时间不充足!', icon: 'none' })
  260. return false
  261. }
  262. if (!item.checked) {
  263. filteredSlots.forEach((v) => {
  264. v.disabled = true
  265. })
  266. item.checked = true
  267. if (!this.selectedTimeSlots[currentDate]) {
  268. this.selectedTimeSlots[currentDate] = []
  269. }
  270. this.selectedTimeSlots[currentDate].push(item.timeStamp)
  271. } else {
  272. filteredSlots.forEach((v) => {
  273. if (v.hasReservation !== 1) v.disabled = false
  274. })
  275. item.checked = false
  276. if (this.selectedTimeSlots[currentDate]) {
  277. const index = this.selectedTimeSlots[currentDate].indexOf(
  278. item.timeStamp
  279. )
  280. if (index > -1) {
  281. this.selectedTimeSlots[currentDate].splice(index, 1)
  282. }
  283. }
  284. }
  285. this.hosts(item)
  286. },
  287. async handleTimeClick2() {
  288. if (!this.upItem) return
  289. let item = this.upItem
  290. const currentDate = this.timeArr[this.day_index]
  291. const durationMs = this.businessDuration * this.minQuantity * 60 * 1000
  292. const itemTime =
  293. item.timeStamp > 9999999999 ? item.timeStamp : item.timeStamp * 1000
  294. const slots = this.timeHostArr[this.day_index] || []
  295. // 保留之前的选择状态,只重置disabled状态
  296. this.timeHostArr[this.day_index].forEach((s) => {
  297. if (s.hasReservation !== 1 && !s.checked) {
  298. s.disabled = false
  299. }
  300. })
  301. // 1. 检查是否与已预约时间冲突
  302. // let inReservedRange = false
  303. // for (let slot of slots) {
  304. // if (slot.hasReservation === 1) {
  305. // const reservedTime =
  306. // slot.timeStamp > 9999999999 ? slot.timeStamp : slot.timeStamp * 1000
  307. // const start = reservedTime - durationMs
  308. // const end = reservedTime + durationMs
  309. // if (itemTime >= start && itemTime < end) {
  310. // inReservedRange = true
  311. // break
  312. // }
  313. // }
  314. // }
  315. // if (inReservedRange) {
  316. // uni.showToast({ title: '服务时长不足,请选择其他时间段', icon: 'none' })
  317. // return false
  318. // }
  319. // 2. 检查是否有足够的后续时间段
  320. const seconds = this.businessDuration * this.minQuantity * 60
  321. const endTimestamp = item.timeStamp + seconds
  322. // 找出从当前时间到结束时间之间的所有时间段
  323. const filteredSlots = this.timeHostArr[this.day_index].filter((i) => {
  324. return i.timeStamp > item.timeStamp && i.timeStamp <= endTimestamp
  325. })
  326. this.filteredSlots = filteredSlots
  327. // 检查这些时间段中是否有已预约的时间
  328. const hasReservedSlot = filteredSlots.some(
  329. (slot) => slot.hasReservation === 1
  330. )
  331. if (hasReservedSlot) {
  332. uni.showToast({
  333. title: '所选时间段内有已预约时间,请选择其他时间',
  334. icon: 'none',
  335. })
  336. return false
  337. }
  338. // 检查时间是否足够
  339. const timestampDifferenceValue = filteredSlots.length
  340. ? (filteredSlots[filteredSlots.length - 1].timeStamp - item.timeStamp) *
  341. 1000
  342. : 0
  343. if (timestampDifferenceValue < durationMs) {
  344. uni.showToast({ title: '所选时间的服务时间不充足!', icon: 'none' })
  345. return false
  346. }
  347. // 所有检查通过,更新disabled状态
  348. // filteredSlots.forEach(v => {
  349. // if (v.hasReservation !== 1 && !v.checked) {
  350. // v.disabled = true;
  351. // }
  352. // });
  353. // this.hosts(item)
  354. },
  355. // 点击日期
  356. dayList(e, index) {
  357. this.day_index = index
  358. this.$emit('getByDate', this.timeArr[index])
  359. },
  360. // 转换时间戳为毫秒
  361. ensureMillisecond(timestamp) {
  362. return timestamp > 9999999999 ? timestamp : timestamp * 1000
  363. },
  364. shouldDisable(item) {
  365. if (!item) return true
  366. // 已预约
  367. if (item.hasReservation === 1) return true
  368. const itemTime = this.ensureMillisecond(item.timeStamp)
  369. // 过去时间
  370. if (this.nowTimes > itemTime) return true
  371. return false
  372. },
  373. hosts(item) {
  374. const itemTime = this.ensureMillisecond(item.timeStamp)
  375. this.host_index = item.timeStamp // 显示用原始值
  376. this.selectedTime = itemTime
  377. this.disableAfterTime =
  378. itemTime + this.businessDuration * this.minQuantity * 60 * 1000
  379. this.$emit('getByTime', {
  380. ...item,
  381. timeStamp: itemTime, // 传递转换后的值
  382. })
  383. },
  384. // 点击立即预约
  385. sub() {
  386. if (this.host_index == '') {
  387. this.$tool.toast('请选择时间')
  388. } else {
  389. let day = this.dayArr[this.day_index]
  390. let time = this.times(this.host_index)
  391. let comTime = {
  392. days: day.days,
  393. weeks: day.weeks,
  394. hours: this.host_All.hours,
  395. timeStamp: this.host_All.timeStamp,
  396. time: time,
  397. }
  398. this.$emit('getTime', comTime)
  399. }
  400. },
  401. // 格式化时间
  402. times(data) {
  403. let date = new Date(data * 1000)
  404. let h = date.getHours()
  405. h = h < 10 ? '0' + h : h // 小时补0
  406. let m = date.getMinutes()
  407. m = m < 10 ? '0' + m : m // 分钟补0
  408. return h + ':' + m
  409. },
  410. time(data, type) {
  411. let date = new Date(data * 1000)
  412. let y = date.getFullYear()
  413. let MM = date.getMonth() + 1
  414. MM = MM < 10 ? '0' + MM : MM // 月补0
  415. let d = date.getDate()
  416. d = d < 10 ? '0' + d : d // 天补0
  417. let h = date.getHours()
  418. h = h < 10 ? '0' + h : h // 小时补0
  419. let m = date.getMinutes()
  420. m = m < 10 ? '0' + m : m // 分钟补0
  421. let s = date.getSeconds()
  422. s = s < 10 ? '0' + s : s // 秒补0
  423. if (type == 'yymmdd') {
  424. return y + '-' + MM + '-' + d
  425. } else if (type == 'hhmmss') {
  426. return h + ':' + m + ':' + s
  427. } else {
  428. return y + '-' + MM + '-' + d + ' ' + h + ':' + m + ':' + s
  429. }
  430. },
  431. },
  432. }
  433. </script>
  434. <style lang="scss">
  435. page {
  436. background-color: #f4f4f4;
  437. }
  438. .calendar {
  439. width: 710rpx;
  440. height: 460rpx;
  441. background-color: #ffffff;
  442. margin: 20rpx auto 10rpx;
  443. border-radius: 8rpx;
  444. }
  445. .calendar_day_scroll {
  446. width: 100%;
  447. overflow-x: auto;
  448. overflow-y: hidden;
  449. white-space: nowrap;
  450. &::-webkit-scrollbar {
  451. display: none;
  452. }
  453. }
  454. .calendar_day {
  455. display: flex;
  456. flex-direction: row;
  457. width: max-content;
  458. height: 120rpx;
  459. }
  460. .day_x {
  461. display: flex;
  462. flex-direction: column;
  463. justify-content: center;
  464. align-items: center;
  465. width: 100rpx;
  466. height: 100%;
  467. font-size: 30rpx;
  468. color: #333333;
  469. flex-shrink: 0;
  470. margin-right: 10rpx;
  471. background: #f8f8f8;
  472. border-radius: 12rpx;
  473. cursor: pointer;
  474. }
  475. .day_x_a {
  476. font-weight: bold;
  477. margin-bottom: 8rpx;
  478. }
  479. .day_x_b {
  480. font-size: 28rpx;
  481. color: #666;
  482. }
  483. .calendar_time_scroll {
  484. width: 100%;
  485. height: 380rpx;
  486. overflow-y: auto;
  487. background: #fff;
  488. border-radius: 8rpx;
  489. margin-bottom: 20rpx;
  490. }
  491. .calendar_time {
  492. display: flex;
  493. width: 100%;
  494. flex-flow: row wrap;
  495. align-content: flex-start;
  496. margin: 20rpx 0;
  497. overflow-y: auto;
  498. }
  499. .time_x {
  500. display: flex;
  501. flex-flow: row;
  502. justify-content: center;
  503. align-items: center;
  504. width: 20%;
  505. height: 54rpx;
  506. border-radius: 26rpx;
  507. margin: 10rpx 0;
  508. font-size: 30rpx;
  509. color: #333333;
  510. display: flex;
  511. flex-direction: column;
  512. .hasRe-text {
  513. font-size: 20rpx;
  514. color: #999999;
  515. }
  516. }
  517. .time_x_sty {
  518. background-color: #ffe97b;
  519. color: #000000 !important;
  520. }
  521. .sub {
  522. display: flex;
  523. justify-content: center;
  524. align-items: center;
  525. width: 710rpx;
  526. height: 100rpx;
  527. border-radius: 50rpx;
  528. margin: 30rpx auto;
  529. color: #ffffff;
  530. font-size: 36rpx;
  531. background-color: #fe3b3c;
  532. }
  533. .time_x {
  534. /* 正常状态样式 */
  535. &.disabled-time {
  536. background-color: #f2f2f2;
  537. color: #999999;
  538. pointer-events: none;
  539. }
  540. &.time_x_sty {
  541. background-color: #ffe97b;
  542. color: #000000;
  543. }
  544. }
  545. .calendar_footer {
  546. width: 100%;
  547. background: #fff;
  548. border-radius: 12rpx;
  549. margin: 0 auto 20rpx auto;
  550. padding: 24rpx 32rpx;
  551. box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1);
  552. display: flex;
  553. align-items: center;
  554. min-height: 60rpx;
  555. white-space: pre-line;
  556. font-size: 26rpx;
  557. color: #333;
  558. line-height: 1.8;
  559. border-left: 6rpx solid #FE3B3C;
  560. }
  561. .selected-time-info {
  562. color: #333;
  563. word-break: break-all;
  564. flex: 1;
  565. font-weight: 500;
  566. padding-left: 15rpx;
  567. line-height: 48rpx;
  568. }
  569. .selected-time-info .business-name {
  570. color: #FE3B3C;
  571. font-weight: bold;
  572. }
  573. </style>