图书馆小程序
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

453 lines
11 KiB

1 month ago
  1. <template>
  2. <view class="booking-detail">
  3. <view class="section">
  4. <view class="section-title">日期选择</view>
  5. <view class="calendar">
  6. <view class="calendar-header">
  7. <text class="month">{{ currentYear }}{{ currentMonth }}</text>
  8. </view>
  9. <view class="calendar-weekdays">
  10. <text v-for="day in weekdays" :key="day" class="weekday">{{ day }}</text>
  11. </view>
  12. <view class="calendar-days">
  13. <view
  14. v-for="(day, index) in calendarDays"
  15. :key="index"
  16. class="day-item"
  17. :class="{
  18. 'other-month': !day.currentMonth,
  19. 'selected': day.date === selectedDate,
  20. 'disabled': !day.available,
  21. 'today': day.isToday
  22. }"
  23. @click="selectDate(day)"
  24. >
  25. <text class="day-num">{{ day.day }}</text>
  26. <text v-if="day.available && !day.isToday" class="available-tag">可预约</text>
  27. <text v-if="day.isToday" class="today-tag">今天</text>
  28. </view>
  29. </view>
  30. </view>
  31. </view>
  32. <view class="section">
  33. <view class="section-title">时间选择</view>
  34. <view class="time-slots">
  35. <view
  36. v-for="slot in timeSlots"
  37. :key="slot.id"
  38. class="time-slot"
  39. :class="{ 'selected': selectedTime === slot.time }"
  40. @click="selectTime(slot)"
  41. >
  42. <text class="time-text">{{ slot.time }}</text>
  43. <text v-if="slot.available" class="available-badge">可选</text>
  44. <text v-else class="unavailable-badge">已满</text>
  45. </view>
  46. </view>
  47. </view>
  48. <view class="bottom-bar">
  49. <button class="btn-primary" @click="confirmBooking">确认预约</button>
  50. </view>
  51. </view>
  52. </template>
  53. <script>
  54. export default {
  55. data() {
  56. return {
  57. currentYear: new Date().getFullYear(),
  58. currentMonth: new Date().getMonth() + 1,
  59. selectedDate: '',
  60. selectedTime: '',
  61. weekdays: ['日', '一', '二', '三', '四', '五', '六'],
  62. calendarDays: [],
  63. timeSlots: [],
  64. // 后台预约配置
  65. reserveConfig: null
  66. };
  67. },
  68. onLoad(options) {
  69. // 模拟后台返回数据,实际替换成接口请求
  70. this.reserveConfig = {
  71. "frequency": "30",
  72. "weekDays": [
  73. {
  74. "id": 1,
  75. "name": "周一",
  76. "checked": true,
  77. "times": [
  78. "00:00 - 23:59"
  79. ],
  80. "showPicker": false,
  81. "newTime": ""
  82. },
  83. {
  84. "id": 2,
  85. "name": "周二",
  86. "checked": false,
  87. "times": [],
  88. "showPicker": false,
  89. "newTime": ""
  90. },
  91. {
  92. "id": 3,
  93. "name": "周三",
  94. "checked": false,
  95. "times": [],
  96. "showPicker": false,
  97. "newTime": ""
  98. },
  99. {
  100. "id": 4,
  101. "name": "周四",
  102. "checked": false,
  103. "times": [],
  104. "showPicker": false,
  105. "newTime": ""
  106. },
  107. {
  108. "id": 5,
  109. "name": "周五",
  110. "checked": false,
  111. "times": [],
  112. "showPicker": false,
  113. "newTime": ""
  114. },
  115. {
  116. "id": 6,
  117. "name": "周六",
  118. "checked": false,
  119. "times": [],
  120. "showPicker": false,
  121. "newTime": ""
  122. },
  123. {
  124. "id": 7,
  125. "name": "周日",
  126. "checked": false,
  127. "showPicker": false,
  128. "newTime": ""
  129. }
  130. ]
  131. }
  132. this.generateCalendar();
  133. },
  134. methods: {
  135. // 根据日期字符串获取星期 1~7 (周一=1,周日=7)
  136. getWeekNum(dateStr) {
  137. const d = new Date(dateStr);
  138. let day = d.getDay(); // 0周日,1周一...6周六
  139. if(day === 0) return 7
  140. return day
  141. },
  142. generateCalendar() {
  143. const year = this.currentYear;
  144. const month = this.currentMonth;
  145. const firstDay = new Date(year, month - 1, 1);
  146. const lastDay = new Date(year, month, 0);
  147. const daysInMonth = lastDay.getDate();
  148. const startWeekday = firstDay.getDay();
  149. const today = new Date();
  150. const todayStr = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;
  151. // 1. 根据 frequency 生成允许的日期范围
  152. let allowDateList = [];
  153. const freq = Number(this.reserveConfig.frequency);
  154. for(let i = 0; i < freq; i++){
  155. const temp = new Date(today);
  156. temp.setDate(today.getDate() + i);
  157. const ds = `${temp.getFullYear()}-${String(temp.getMonth()+1).padStart(2,'0')}-${String(temp.getDate()).padStart(2,'0')}`
  158. allowDateList.push(ds)
  159. }
  160. const days = [];
  161. const prevMonthLastDay = new Date(year, month - 1, 0).getDate();
  162. for (let i = startWeekday - 1; i >= 0; i--) {
  163. days.push({
  164. day: prevMonthLastDay - i,
  165. date: '',
  166. currentMonth: false,
  167. available: false,
  168. isToday: false
  169. });
  170. }
  171. // 生成当月每一天
  172. for (let i = 1; i <= daysInMonth; i++) {
  173. const dateStr = `${year}-${String(month).padStart(2, '0')}-${String(i).padStart(2, '0')}`;
  174. const isToday = dateStr === todayStr;
  175. // 是否在frequency时间范围内
  176. const inFreqRange = allowDateList.includes(dateStr);
  177. let available = false;
  178. if(inFreqRange){
  179. // 获取当前是周几 1~7
  180. const weekNo = this.getWeekNum(dateStr);
  181. const weekCfg = this.reserveConfig.weekDays.find(w => w.id === weekNo)
  182. if(weekCfg && weekCfg.checked){
  183. available = true
  184. }
  185. }
  186. days.push({
  187. day: i,
  188. date: dateStr,
  189. currentMonth: true,
  190. available: available,
  191. isToday: isToday
  192. });
  193. }
  194. const remainingDays = 42 - days.length;
  195. for (let i = 1; i <= remainingDays; i++) {
  196. days.push({
  197. day: i,
  198. date: '',
  199. currentMonth: false,
  200. available: false,
  201. isToday: false
  202. });
  203. }
  204. this.calendarDays = days;
  205. // 默认选中第一个可用日期
  206. const firstAvail = this.calendarDays.find(d => d.available);
  207. if(firstAvail){
  208. this.selectedDate = firstAvail.date
  209. this.refreshTimeSlots(this.selectedDate)
  210. }else{
  211. this.selectedDate = ''
  212. this.timeSlots = []
  213. }
  214. },
  215. // 根据选中日期刷新时间段
  216. refreshTimeSlots(dateStr){
  217. this.selectedTime = ''
  218. if(!dateStr) {
  219. this.timeSlots = []
  220. return
  221. }
  222. const weekNo = this.getWeekNum(dateStr)
  223. const weekCfg = this.reserveConfig.weekDays.find(w => w.id === weekNo)
  224. if(!weekCfg || !weekCfg.checked){
  225. this.timeSlots = []
  226. return
  227. }
  228. // times数组转为时间槽
  229. this.timeSlots = weekCfg.times.map((t, idx)=>{
  230. return {
  231. id: idx + 1,
  232. time: t,
  233. available: true
  234. }
  235. })
  236. },
  237. selectDate(day) {
  238. if (!day.available || !day.currentMonth) return;
  239. this.selectedDate = day.date;
  240. // 切换日期,重新加载时间段
  241. this.refreshTimeSlots(day.date)
  242. },
  243. selectTime(slot) {
  244. if (!slot.available) return;
  245. this.selectedTime = slot.time;
  246. },
  247. confirmBooking() {
  248. if (!this.selectedDate) {
  249. uni.showToast({ title: '请选择日期', icon: 'none' });
  250. return;
  251. }
  252. if (!this.selectedTime) {
  253. uni.showToast({ title: '请选择时间段', icon: 'none' });
  254. return;
  255. }
  256. uni.showLoading({ title: '预约中...' });
  257. setTimeout(() => {
  258. uni.hideLoading();
  259. uni.showModal({
  260. title: '预约成功',
  261. content: `您已成功预约\n日期:${this.selectedDate}\n时间:${this.selectedTime}`,
  262. showCancel: false,
  263. confirmText: '确定',
  264. success: () => {
  265. uni.navigateBack({ delta: 2 });
  266. }
  267. });
  268. }, 1500);
  269. }
  270. }
  271. };
  272. </script>
  273. <style lang="scss" scoped>
  274. .booking-detail {
  275. min-height: 100vh;
  276. background-color: #f5f5f5;
  277. padding-bottom: 70px;
  278. }
  279. .section {
  280. background-color: #fff;
  281. margin: 10px;
  282. border-radius: 12px;
  283. padding: 16px;
  284. }
  285. .section-title {
  286. font-size: 16px;
  287. font-weight: bold;
  288. color: #333;
  289. margin-bottom: 16px;
  290. }
  291. /* 日历 */
  292. .calendar-header {
  293. text-align: center;
  294. margin-bottom: 12px;
  295. }
  296. .month {
  297. font-size: 16px;
  298. color: #333;
  299. font-weight: bold;
  300. }
  301. .calendar-weekdays {
  302. display: flex;
  303. justify-content: space-between;
  304. margin-bottom: 10px;
  305. }
  306. .weekday {
  307. width: 14.28%;
  308. text-align: center;
  309. font-size: 13px;
  310. color: #999;
  311. }
  312. .calendar-days {
  313. display: flex;
  314. flex-wrap: wrap;
  315. }
  316. .day-item {
  317. width: 14.28%;
  318. aspect-ratio: 1;
  319. display: flex;
  320. flex-direction: column;
  321. align-items: center;
  322. justify-content: center;
  323. margin-bottom: 6px;
  324. border-radius: 6px;
  325. position: relative;
  326. }
  327. .day-item.other-month .day-num {
  328. color: #ccc;
  329. }
  330. .day-item.disabled .day-num {
  331. color: #ccc;
  332. }
  333. .day-item.selected {
  334. background-color: #01a4fe;
  335. }
  336. .day-item.selected .day-num {
  337. color: #fff;
  338. font-size: 15px;
  339. font-weight: bold;
  340. }
  341. .day-item.selected .available-tag,
  342. .day-item.selected .today-tag {
  343. color: rgba(255, 255, 255, 0.9);
  344. font-size: 10px;
  345. }
  346. .day-item.today:not(.selected) {
  347. background-color: #fff3cd;
  348. }
  349. .day-item.today:not(.selected) .day-num {
  350. color: #856404;
  351. }
  352. .day-num {
  353. font-size: 14px;
  354. color: #333;
  355. }
  356. .available-tag {
  357. font-size: 10px;
  358. color: #01a4fe;
  359. margin-top: 2px;
  360. }
  361. .today-tag {
  362. font-size: 10px;
  363. color: #856404;
  364. margin-top: 2px;
  365. }
  366. /* 时间选择 */
  367. .time-slots {
  368. display: flex;
  369. flex-wrap: wrap;
  370. gap: 10px;
  371. }
  372. .time-slot {
  373. width: calc(50% - 5px);
  374. display: flex;
  375. align-items: center;
  376. justify-content: space-between;
  377. padding: 12px;
  378. background-color: #f8f8f8;
  379. border-radius: 8px;
  380. border: 1px solid transparent;
  381. }
  382. .time-slot.selected {
  383. background-color: #e8f4fd;
  384. border-color: #01a4fe;
  385. }
  386. .time-text {
  387. font-size: 14px;
  388. color: #333;
  389. }
  390. .available-badge {
  391. font-size: 12px;
  392. color: #01a4fe;
  393. background-color: #e8f4fd;
  394. padding: 2px 8px;
  395. border-radius: 10px;
  396. }
  397. .unavailable-badge {
  398. font-size: 12px;
  399. color: #999;
  400. background-color: #f0f0f0;
  401. padding: 2px 8px;
  402. border-radius: 10px;
  403. }
  404. /* 底部按钮 */
  405. .bottom-bar {
  406. position: fixed;
  407. bottom: 0;
  408. left: 0;
  409. right: 0;
  410. padding: 10px;
  411. background-color: #fff;
  412. box-shadow: 0 -1px 5px rgba(0, 0, 0, 0.1);
  413. }
  414. .btn-primary {
  415. width: 100%;
  416. height: 44px;
  417. line-height: 44px;
  418. background-color: #01a4fe;
  419. color: #fff;
  420. border-radius: 22px;
  421. font-size: 15px;
  422. border: none;
  423. }
  424. .btn-primary::after {
  425. border: none;
  426. }
  427. </style>