its-calendar.vue 17 KB

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