its-calendar.vue 17 KB

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