its-calendar.vue 17 KB

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