its-calendar.vue 16 KB

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