@ebiz/designer-components 0.1.97 → 0.1.99

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,209 +1,283 @@
1
1
  <template>
2
- <div class="ebiz-meeting-room-selector">
3
- <!-- 头部区域 -->
4
- <div class="selector-header">
5
- <slot name="header">
6
- <div class="header-content">
7
- <h3 class="title">会议室预约</h3>
8
- <div class="date-selector">
9
- <label>选择日期:</label>
10
- <t-date-picker
11
- v-model="currentDate"
12
- :disabled="disabled"
13
- :min-date="new Date()"
14
- format="YYYY-MM-DD"
15
- @change="handleDateChange"
16
- />
17
- </div>
18
- </div>
19
- </slot>
20
- </div>
21
-
22
- <!-- 加载状态 -->
23
- <div v-if="loading" class="loading-container">
24
- <t-loading size="large" text="加载中..." />
25
- </div>
26
-
27
- <!-- 主要内容区域 -->
28
- <div v-else class="selector-content">
29
- <!-- 时间轴表头 -->
30
- <div class="time-header">
31
- <div class="room-column-header">会议室</div>
32
- <div class="time-slots-header">
33
- <div
34
- v-for="slot in timeSlotList"
35
- :key="slot.value"
36
- class="time-slot-header"
37
- >
38
- {{ slot.label }}
39
- </div>
40
- </div>
2
+ <div>
3
+ <div class="ebiz-meeting-room-selector">
4
+ <!-- 头部区域 -->
5
+ <div v-if="loading" class="loading-container">
6
+ <t-loading size="large" text="加载中..." />
41
7
  </div>
42
8
 
43
- <!-- 会议室列表 -->
44
- <div class="rooms-container">
45
- <div
46
- v-for="room in roomList"
47
- :key="room.id"
48
- class="room-row"
49
- :class="{ 'selected': selectedRoom?.id === room.id }"
50
- >
51
- <!-- 会议室信息 -->
52
- <div class="room-info" @click="handleRoomSelect(room)">
53
- <slot name="room-info" :room="room">
54
- <div class="room-basic-info">
55
- <h4 class="room-name">{{ room.name }}</h4>
56
- <p class="room-details">
57
- <span class="capacity">容量: {{ room.capacity }}人</span>
58
- <span class="location">位置: {{ room.location }}</span>
59
- </p>
9
+ <!-- 主要内容区域 -->
10
+ <div v-else class="selector-content">
11
+ <!-- 会议室列表 -->
12
+ <div class="rooms-list">
13
+ <div v-for="room in roomList" :key="room.id" class="room-card"
14
+ :class="{ 'selected': selectedRoom?.id === room.id }" @click="handleRoomSelect(room)">
15
+ <!-- 会议室信息 -->
16
+ <div class="room-info">
17
+ <slot name="room-info" :room="room">
18
+ <div class="room-header">
19
+ <h4 class="room-name">🏢 {{ room.name }}</h4>
20
+ <div class="room-controls">
21
+ <div class="room-status" :class="getRoomStatusClass(room)">
22
+ {{ getRoomStatusText(room) }}
23
+ </div>
24
+ <div v-if="selectedRoom?.id === room.id" class="time-filter-btn">
25
+ <t-button size="small" variant="outline" theme="primary" @click.stop="toggleTimeFilter">
26
+ {{ showAllTimeSlots ? '常用时间' : '全部时间' }}
27
+ </t-button>
28
+ </div>
29
+ </div>
30
+ </div>
31
+ <div class="room-details">
32
+ <div class="detail-item">
33
+ <span class="icon">👥</span>
34
+ <span>{{ room.capacity }}人</span>
35
+ </div>
36
+ <div class="detail-item">
37
+ <span class="icon">📍</span>
38
+ <span>{{ room.location }}</span>
39
+ </div>
40
+ </div>
60
41
  <div v-if="room.equipment?.length" class="equipment">
61
- <t-tag
62
- v-for="item in room.equipment"
63
- :key="item"
64
- size="small"
65
- variant="light"
66
- >
42
+ <t-tag v-for="item in room.equipment" :key="item" size="small" variant="light" class="equipment-tag">
67
43
  {{ item }}
68
44
  </t-tag>
69
45
  </div>
46
+ </slot>
47
+ </div>
48
+
49
+ <!-- 时间段选择区域 -->
50
+ <div v-if="selectedRoom?.id === room.id" class="time-selection">
51
+ <div class="time-header">
52
+ <h5>⏰ 选择时间段</h5>
53
+ <div class="time-info">
54
+ <span v-if="selectedTimeSlots.length > 0" class="selected-info">
55
+ 已选 {{ selectedTimeSlots.length * 0.5 }}小时
56
+ </span>
57
+ </div>
70
58
  </div>
71
- </slot>
72
- </div>
73
59
 
74
- <!-- 时间段网格 -->
75
- <div class="time-slots-grid">
76
- <div
77
- v-for="slot in timeSlotList"
78
- :key="slot.value"
79
- class="time-slot"
80
- :class="{
81
- 'booked': isSlotBooked(room.id, slot.value),
82
- 'selected': isSlotSelected(room.id, slot.value),
83
- 'selecting': isSlotSelecting(room.id, slot.value),
84
- 'disabled': disabled || isSlotDisabled(room.id, slot.value)
85
- }"
86
- @click="handleTimeSlotClick(room, slot)"
87
- @mouseenter="handleTimeSlotHover(room, slot)"
88
- @mouseleave="handleTimeSlotLeave()"
89
- >
90
- <slot name="time-slot" :room="room" :slot="slot" :is-booked="isSlotBooked(room.id, slot.value)">
91
- <div class="slot-content">
92
- <span v-if="isSlotBooked(room.id, slot.value)" class="booked-indicator">已约</span>
93
- <span v-else-if="isSlotSelected(room.id, slot.value)" class="selected-indicator">已选</span>
60
+ <!-- 时间段网格 -->
61
+ <div class="time-slots-grid">
62
+ <div v-for="slot in filteredTimeSlotList" :key="slot.value" class="time-slot" :class="{
63
+ 'booked': isSlotBooked(room.id, slot.value),
64
+ 'selected': isSlotSelected(room.id, slot.value),
65
+ 'disabled': props.disabled || isSlotDisabled(room.id, slot.value)
66
+ }" @click="handleTimeSlotClick(room, slot)">
67
+ <div class="slot-content">
68
+ <div class="slot-time">{{ slot.label }}</div>
69
+ <div class="slot-status">
70
+ <span v-if="isSlotBooked(room.id, slot.value)" class="status-icon booked">
71
+ <span style="display:block;margin-bottom: 4px;">已预约</span>
72
+ <span style="display: block;">{{ bookedInfo(room.id, slot.value, 'name') }} </span>
73
+ <span style="display: block;">{{ bookedInfo(room.id, slot.value, 'no') }} </span>
74
+ </span>
75
+ <span v-else-if="isSlotSelected(room.id, slot.value)" class="status-icon selected">✅</span>
76
+ <span v-else class="status-icon available">空闲</span>
77
+ </div>
78
+ </div>
94
79
  </div>
95
- </slot>
80
+ </div>
96
81
  </div>
97
82
  </div>
98
83
  </div>
99
84
  </div>
100
- </div>
101
85
 
102
- <!-- 预约确认区域 -->
103
- <div v-if="selectedTimeSlots.length > 0" class="booking-panel">
104
- <slot name="booking-form" :selection="currentSelection">
105
- <div class="booking-form">
106
- <h4>预约确认</h4>
107
- <div class="booking-info">
108
- <p><strong>会议室:</strong>{{ selectedRoom?.name }}</p>
109
- <p><strong>日期:</strong>{{ currentDate }}</p>
110
- <p><strong>时间:</strong>{{ formatSelectedTime() }}</p>
111
- <p><strong>时长:</strong>{{ selectedTimeSlots.length * 0.5 }}小时</p>
112
- </div>
113
- <div class="booking-actions">
114
- <t-button variant="outline" @click="handleCancelSelection">取消</t-button>
115
- <t-button theme="primary" @click="handleConfirmBooking">确认预约</t-button>
86
+ <!-- 预约确认浮动面板 -->
87
+ <div v-if="selectedTimeSlots.length > 0" class="booking-panel">
88
+ <slot name="booking-form" :selection="currentSelection">
89
+ <div class="booking-form">
90
+ <div class="booking-header">
91
+ <h4>📋 预约确认</h4>
92
+ <t-button variant="text" size="small" @click="handleCancelSelection">
93
+ 取消
94
+ </t-button>
95
+ </div>
96
+ <div class="booking-info">
97
+ <div class="info-row">
98
+ <span class="label">会议室:</span>
99
+ <span class="value">{{ selectedRoom?.name }}</span>
100
+ </div>
101
+ <div class="info-row">
102
+ <span class="label">日期:</span>
103
+ <span class="value">{{ formatDate(selectedDate) }}</span>
104
+ </div>
105
+ <div class="info-row">
106
+ <span class="label">时间:</span>
107
+ <span class="value">{{ formatSelectedTime() }}</span>
108
+ </div>
109
+ <div class="info-row">
110
+ <span class="label">时长:</span>
111
+ <span class="value">{{ selectedTimeSlots.length * 0.5 }}小时</span>
112
+ </div>
113
+ </div>
114
+ <div class="booking-actions">
115
+ <t-button theme="primary" size="large" block :loading="bookingLoading" @click="handleConfirmBooking">
116
+ {{ confirmButtonText }}
117
+ </t-button>
118
+ </div>
116
119
  </div>
117
- </div>
118
- </slot>
120
+ </slot>
121
+ </div>
119
122
  </div>
120
-
121
- <!-- 底部操作区域 -->
122
- <div class="selector-footer">
123
- <slot name="footer">
124
- <div class="legend">
125
- <div class="legend-item">
126
- <span class="legend-color available"></span>
127
- <span>可预约</span>
128
- </div>
129
- <div class="legend-item">
130
- <span class="legend-color booked"></span>
131
- <span>已预约</span>
123
+ <!-- 预约成功提示组件 -->
124
+ <div v-if="showSuccessMessage" class="success-overlay" @click="hideSuccessMessage">
125
+ <div class="success-container" @click.stop>
126
+ <div class="success-icon">✅</div>
127
+ <div class="success-title">{{ successTitle }}</div>
128
+ <div class="success-content">
129
+ <div class="success-item">
130
+ <span class="success-label">会议室:</span>
131
+ <span class="success-value">{{ successData.roomName }}</span>
132
+ </div>
133
+ <div class="success-item">
134
+ <span class="success-label">日期:</span>
135
+ <span class="success-value">{{ successData.date }}</span>
136
+ </div>
137
+ <div class="success-item">
138
+ <span class="success-label">时间:</span>
139
+ <span class="success-value">{{ successData.time }}</span>
140
+ </div>
141
+ <div class="success-item">
142
+ <span class="success-label">时长:</span>
143
+ <span class="success-value">{{ successData.duration }}小时</span>
144
+ </div>
145
+ <div v-if="successData.isPreemptive" class="success-preempt-info">
146
+ <div class="preempt-info-title">已取消的会议:</div>
147
+ <div class="preempt-info-content">{{ successData.cancelledMeetings }}</div>
148
+ </div>
132
149
  </div>
133
- <div class="legend-item">
134
- <span class="legend-color selected"></span>
135
- <span>已选择</span>
150
+ <div class="success-actions">
151
+ <t-button theme="primary" @click="hideSuccessMessage">确定</t-button>
136
152
  </div>
137
153
  </div>
138
- </slot>
139
- </div>
154
+ </div>
140
155
  </div>
141
156
  </template>
142
157
 
143
- <script setup lang="ts">
158
+ <script setup>
144
159
  import { ref, computed, watch, defineProps, defineEmits } from 'vue'
145
- import { DatePicker as TDatePicker, Loading as TLoading, Tag as TTag, Button as TButton } from 'tdesign-vue-next'
160
+ import { Loading as TLoading, Tag as TTag, Button as TButton, MessagePlugin } from 'tdesign-vue-next'
161
+ import { dataService } from '../index.js'
146
162
 
147
163
  // 组件属性接口
148
- interface Room {
149
- id: string
150
- name: string
151
- capacity: number
152
- location: string
153
- equipment?: string[]
154
- }
155
-
156
- interface TimeSlot {
157
- value: string
158
- label: string
159
- hour: number
160
- minute: number
161
- }
162
-
163
- interface Props {
164
- rooms?: Room[]
165
- selectedDate?: string
166
- timeSlots?: TimeSlot[]
167
- bookedSlots?: Record<string, string[]>
168
- loading?: boolean
169
- disabled?: boolean
170
- minDuration?: number
171
- maxDuration?: number
172
- }
173
-
174
- // 定义属性
175
- const props = withDefaults(defineProps<Props>(), {
176
- rooms: () => [],
177
- selectedDate: "",
178
- timeSlots: () => [],
179
- bookedSlots: () => ({}),
180
- loading: false,
181
- disabled: false,
182
- minDuration: 1,
183
- maxDuration: 8
164
+ const props = defineProps({
165
+ rooms: {
166
+ type: Array,
167
+ default: () => []
168
+ },
169
+ selectedDate: {
170
+ type: String,
171
+ default: ""
172
+ },
173
+ timeSlots: {
174
+ type: Array,
175
+ default: () => []
176
+ },
177
+ bookedSlots: {
178
+ type: Object,
179
+ default: () => ({})
180
+ },
181
+ loading: {
182
+ type: Boolean,
183
+ default: false
184
+ },
185
+ disabled: {
186
+ type: Boolean,
187
+ default: false
188
+ },
189
+ minDuration: {
190
+ type: Number,
191
+ default: 1
192
+ },
193
+ maxDuration: {
194
+ type: Number,
195
+ default: 8
196
+ },
197
+ // 新增API配置属性
198
+ roomQueryApiConfig: {
199
+ type: Object,
200
+ default: () => ({})
201
+ },
202
+ bookingApiConfig: {
203
+ type: Object,
204
+ default: () => ({})
205
+ },
206
+ bookingFormData: {
207
+ type: Object,
208
+ default: () => ({})
209
+ },
210
+ confirmButtonText: {
211
+ type: String,
212
+ default: '确认预约'
213
+ },
214
+ successTitle: {
215
+ type: String,
216
+ default: '预约成功'
217
+ },
218
+ preemptSuccessTitle: {
219
+ type: String,
220
+ default: '抢占预约成功'
221
+ }
184
222
  })
185
223
 
186
224
  // 定义事件
187
- const emit = defineEmits(['room-select', 'time-select', 'booking-confirm', 'date-change'])
225
+ const emit = defineEmits([
226
+ 'room-select',
227
+ 'time-select',
228
+ 'booking-confirm',
229
+ 'date-change',
230
+ 'booking-success',
231
+ 'booking-error',
232
+ 'room-query-success',
233
+ 'room-query-error'
234
+ ])
188
235
 
189
236
  // 响应式数据
190
- const currentDate = ref(props.selectedDate || new Date().toISOString().split('T')[0])
191
- const selectedRoom = ref<Room | null>(null)
192
- const selectedTimeSlots = ref<string[]>([])
193
- const selectingStart = ref<string | null>(null)
194
- const hoveringSlot = ref<string | null>(null)
237
+ const currentDate = computed(() => {
238
+ return formatDate(props.selectedDate)
239
+ })
240
+ const selectedRoom = ref(null)
241
+ const selectedTimeSlots = ref([])
242
+ const bookingLoading = ref(false)
243
+ const roomQueryLoading = ref(false)
244
+ const showSuccessMessage = ref(false)
245
+ const successData = ref({})
246
+ const apiRoomList = ref([]) // 存储从API获取的会议室数据
247
+ const showAllTimeSlots = ref(false)
195
248
 
196
249
  // 计算属性
197
- const roomList = computed(() => props.rooms)
250
+ const roomList = computed(() => {
251
+ if (apiRoomList.value.length > 0) {
252
+ return apiRoomList.value.map(room => ({
253
+ id: room.conferenceId,
254
+ name: room.conferenceName,
255
+ capacity: room.capacity,
256
+ location: room.location,
257
+ equipment: [],
258
+ timeSlots: room.timeSlots
259
+ }))
260
+ }
261
+ return props.rooms
262
+ })
198
263
 
199
264
  const timeSlotList = computed(() => {
200
265
  if (props.timeSlots.length > 0) {
201
266
  return props.timeSlots
202
267
  }
203
-
204
- // 默认生成9:00-18:00的时间段,每30分钟一个
205
- const slots: TimeSlot[] = []
206
- for (let hour = 9; hour < 18; hour++) {
268
+
269
+ if (apiRoomList.value.length > 0 && apiRoomList.value[0]?.timeSlots) {
270
+ return apiRoomList.value[0].timeSlots.map(slot => ({
271
+ value: slot.time,
272
+ label: slot.time,
273
+ hour: parseInt(slot.time.split(':')[0]),
274
+ minute: parseInt(slot.time.split(':')[1]),
275
+ userInfo: slot?.userInfo ?? null
276
+ }))
277
+ }
278
+
279
+ const slots = []
280
+ for (let hour = 0; hour < 24; hour++) {
207
281
  for (let minute = 0; minute < 60; minute += 30) {
208
282
  const timeValue = `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`
209
283
  slots.push({
@@ -217,20 +291,132 @@ const timeSlotList = computed(() => {
217
291
  return slots
218
292
  })
219
293
 
220
- // 辅助方法
221
- const getEndTime = (startTime: string): string => {
294
+ // 工具方法
295
+ const timeToMinutes = (timeStr) => {
296
+ const [hour, minute] = timeStr.split(':').map(Number)
297
+ return hour * 60 + minute
298
+ }
299
+
300
+ const compareTimes = (timeA, timeB) => {
301
+ const minutesA = timeToMinutes(timeA)
302
+ const minutesB = timeToMinutes(timeB)
303
+ if (minutesA === minutesB) return 0
304
+ return minutesA < minutesB ? -1 : 1
305
+ }
306
+
307
+ const _isTimeOverlap = (start1, end1, start2, end2) => {
308
+ const s1 = timeToMinutes(start1)
309
+ const e1 = timeToMinutes(end1)
310
+ const s2 = timeToMinutes(start2)
311
+ const e2 = timeToMinutes(end2)
312
+
313
+ return s1 < e2 && s2 < e1
314
+ }
315
+
316
+ const getEndTime = (startTime) => {
222
317
  const [hour, minute] = startTime.split(':').map(Number)
223
318
  const endMinute = minute + 30
224
319
  const endHour = endMinute >= 60 ? hour + 1 : hour
225
320
  const finalMinute = endMinute >= 60 ? endMinute - 60 : endMinute
226
-
321
+
227
322
  return `${endHour.toString().padStart(2, '0')}:${finalMinute.toString().padStart(2, '0')}`
228
323
  }
229
324
 
325
+ const minutesToTime = (minutes) => {
326
+ const hour = Math.floor(minutes / 60)
327
+ const minute = minutes % 60
328
+ return `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`
329
+ }
330
+
331
+ const getCurrentTime = () => {
332
+ const now = new Date()
333
+ const hour = now.getHours().toString().padStart(2, '0')
334
+ const minute = now.getMinutes().toString().padStart(2, '0')
335
+ return `${hour}:${minute}`
336
+ }
337
+
338
+ // 状态检查方法
339
+ const isSlotBooked = (roomId, timeSlot) => {
340
+ if (apiRoomList.value.length > 0) {
341
+ const room = apiRoomList.value.find(r => r.conferenceId == roomId)
342
+ if (room && room.timeSlots) {
343
+ const slot = room.timeSlots.find(s => s.time === timeSlot)
344
+ return slot ? slot.occupied : false
345
+ }
346
+ }
347
+ return props.bookedSlots[roomId]?.includes(timeSlot) || false
348
+ }
349
+
350
+ const bookedInfo = (roomId, timeSlot, type = 'name') => {
351
+ if (apiRoomList.value.length > 0) {
352
+ const room = apiRoomList.value.find(r => r.conferenceId == roomId)
353
+ if (room && room.timeSlots) {
354
+ const slot = room.timeSlots.find(s => s.time === timeSlot)
355
+ if (type === 'name') {
356
+ return slot?.userInfo?.name ? slot.userInfo.name : ''
357
+ } else {
358
+ return slot?.userInfo?.no ? slot.userInfo.no : ''
359
+ }
360
+ }
361
+ }
362
+ return ''
363
+ }
364
+
365
+ const isSlotSelected = (roomId, timeSlot) => {
366
+ return selectedRoom.value?.id === roomId && selectedTimeSlots.value.includes(timeSlot)
367
+ }
368
+
369
+ const isSlotDisabled = (_roomId, _timeSlot) => {
370
+ const today = formatDate(new Date().getTime());
371
+ if (currentDate.value === today && compareTimes(getCurrentTime(), _timeSlot) === 1) {
372
+ return true
373
+ }
374
+ return false
375
+ }
376
+
377
+ // 智能选择时间段 - 自动填充中间的时间段
378
+ const getSmartSelectedSlots = (slots, roomId) => {
379
+ if (slots.length <= 1) return slots
380
+
381
+ const slotsInMinutes = slots.map(timeToMinutes).sort((a, b) => a - b)
382
+ const minTime = slotsInMinutes[0]
383
+ const maxTime = slotsInMinutes[slotsInMinutes.length - 1]
384
+
385
+ const continuousSlots = []
386
+ for (let time = minTime; time <= maxTime; time += 30) {
387
+ const timeStr = minutesToTime(time)
388
+
389
+ if (!isSlotBooked(roomId, timeStr)) {
390
+ continuousSlots.push(timeStr)
391
+ } else {
392
+ break
393
+ }
394
+ }
395
+
396
+ return continuousSlots
397
+ }
398
+
399
+ // 限制选择的时间段数量,保留以最新选择为中心的连续时间段
400
+ const getLimitedSlots = (slots, latestSlot, maxDuration) => {
401
+ const latestMinutes = timeToMinutes(latestSlot)
402
+ const slotsInMinutes = slots.map(timeToMinutes).sort((a, b) => a - b)
403
+
404
+ const latestIndex = slotsInMinutes.indexOf(latestMinutes)
405
+
406
+ const halfDuration = Math.floor(maxDuration / 2)
407
+ const startIndex = Math.max(0, latestIndex - halfDuration)
408
+ const endIndex = Math.min(slotsInMinutes.length, startIndex + maxDuration)
409
+
410
+ const adjustedStartIndex = Math.max(0, endIndex - maxDuration)
411
+
412
+ return slotsInMinutes
413
+ .slice(adjustedStartIndex, endIndex)
414
+ .map(minutesToTime)
415
+ }
416
+
417
+ // 清理方法
230
418
  const clearTimeSelection = () => {
231
419
  selectedTimeSlots.value = []
232
- selectingStart.value = null
233
- hoveringSlot.value = null
234
420
  }
235
421
 
236
422
  const clearSelection = () => {
@@ -238,45 +424,64 @@ const clearSelection = () => {
238
424
  clearTimeSelection()
239
425
  }
240
426
 
241
- const isSlotBooked = (roomId: string, timeSlot: string): boolean => {
242
- return props.bookedSlots[roomId]?.includes(timeSlot) || false
427
+ // 显示预约成功信息
428
+ const showBookingSuccess = (bookingData) => {
429
+ successData.value = {
430
+ roomName: bookingData.room.name,
431
+ date: bookingData.date,
432
+ time: `${bookingData.startTime} - ${bookingData.endTime}`,
433
+ duration: bookingData.duration,
434
+ isPreemptive: bookingData.isPreemptive,
435
+ cancelledMeetings: bookingData.isPreemptive
436
+ ? bookingData.conflictMeetings?.map(m => m.title).join('、') || ''
437
+ : ''
438
+ }
439
+ showSuccessMessage.value = true
243
440
  }
244
441
 
245
- const isSlotSelected = (roomId: string, timeSlot: string): boolean => {
246
- return selectedRoom.value?.id === roomId && selectedTimeSlots.value.includes(timeSlot)
442
+ // 隐藏成功信息
443
+ const hideSuccessMessage = () => {
444
+ showSuccessMessage.value = false
445
+ successData.value = {}
446
+ emit('booking-success', successData.value)
247
447
  }
248
448
 
249
- const isSlotSelecting = (roomId: string, timeSlot: string): boolean => {
250
- if (!selectingStart.value || selectedRoom.value?.id !== roomId || !hoveringSlot.value) {
251
- return false
252
- }
253
-
254
- const startIndex = timeSlotList.value.findIndex(s => s.value === selectingStart.value)
255
- const hoverIndex = timeSlotList.value.findIndex(s => s.value === hoveringSlot.value)
256
- const currentIndex = timeSlotList.value.findIndex(s => s.value === timeSlot)
257
-
258
- if (startIndex === -1 || hoverIndex === -1 || currentIndex === -1) {
259
- return false
260
- }
261
-
262
- const minIndex = Math.min(startIndex, hoverIndex)
263
- const maxIndex = Math.max(startIndex, hoverIndex)
264
-
265
- return currentIndex >= minIndex && currentIndex <= maxIndex
449
+ // 状态获取方法
450
+ const getRoomStatusClass = (room) => {
451
+ const commonTimeSlots = timeSlotList.value.filter(slot =>
452
+ slot.hour >= 7 && slot.hour < 22
453
+ )
454
+
455
+ const availableSlots = commonTimeSlots.filter(slot =>
456
+ !isSlotBooked(room.id, slot.value)
457
+ ).length
458
+
459
+ if (availableSlots === 0) return 'fully-booked'
460
+ if (availableSlots < commonTimeSlots.length / 2) return 'partially-available'
461
+ return 'available'
266
462
  }
267
463
 
268
- const isSlotDisabled = (_roomId: string, _timeSlot: string): boolean => {
269
- // 可以根据业务需求添加更多禁用逻辑
270
- return false
464
+ const getRoomStatusText = (room) => {
465
+ const commonTimeSlots = timeSlotList.value.filter(slot =>
466
+ slot.hour >= 7 && slot.hour < 22
467
+ )
468
+
469
+ const availableSlots = commonTimeSlots.filter(slot =>
470
+ !isSlotBooked(room.id, slot.value)
471
+ ).length
472
+
473
+ if (availableSlots === 0) return '已满'
474
+ if (availableSlots < commonTimeSlots.length / 2) return '紧张'
475
+ return '充足'
271
476
  }
272
477
 
273
- const formatSelectedTime = (): string => {
478
+ const formatSelectedTime = () => {
274
479
  if (selectedTimeSlots.value.length === 0) return ''
275
-
480
+
276
481
  const sortedSlots = [...selectedTimeSlots.value].sort()
277
482
  const startTime = sortedSlots[0]
278
483
  const endTime = getEndTime(sortedSlots[sortedSlots.length - 1])
279
-
484
+
280
485
  return `${startTime} - ${endTime}`
281
486
  }
282
487
 
@@ -284,382 +489,664 @@ const currentSelection = computed(() => {
284
489
  if (!selectedRoom.value || selectedTimeSlots.value.length === 0) {
285
490
  return null
286
491
  }
287
-
492
+
288
493
  const sortedSlots = [...selectedTimeSlots.value].sort()
289
494
  return {
290
495
  room: selectedRoom.value,
291
496
  date: currentDate.value,
292
497
  startTime: sortedSlots[0],
293
498
  endTime: getEndTime(sortedSlots[sortedSlots.length - 1]),
294
- duration: selectedTimeSlots.value.length,
499
+ duration: selectedTimeSlots.value.length * 0.5,
295
500
  timeSlots: sortedSlots
296
501
  }
297
502
  })
298
503
 
299
- // 事件处理方法
300
- const handleDateChange = (date: string) => {
301
- currentDate.value = date
302
- clearSelection()
303
- emit('date-change', date)
504
+ const formatDate = (timestamp) => {
505
+ const date = new Date(timestamp)
506
+ const year = date.getFullYear()
507
+ const month = String(date.getMonth() + 1).padStart(2, '0')
508
+ const day = String(date.getDate()).padStart(2, '0')
509
+ return `${year}-${month}-${day}`
304
510
  }
305
511
 
306
- const handleRoomSelect = (room: Room) => {
307
- selectedRoom.value = room
308
- clearTimeSelection()
309
- emit('room-select', room)
512
+ // API调用方法
513
+ const queryRooms = async (date) => {
514
+ try {
515
+ roomQueryLoading.value = true
516
+
517
+ if (!date) {
518
+ date = props.selectedDate
519
+ }
520
+ const queryParams = {
521
+ date: props.selectedDate,
522
+ requestTime: new Date().getTime()
523
+ }
524
+
525
+ let response
526
+ if (props.roomQueryApiConfig && Object.keys(props.roomQueryApiConfig).length > 0) {
527
+ response = await dataService.fetch(queryParams, props.roomQueryApiConfig)
528
+ } else {
529
+ if (typeof props.selectedDate === 'number') {
530
+ queryParams.date = formatDate(props.selectedDate)
531
+ }
532
+
533
+ response = await dataService.fetch(queryParams, {}, "/appdata/execute/plugin?key=get-meetingRoomOccupy-info&date=" + queryParams.date)
534
+ }
535
+
536
+ const roomData = response.meetingRooms
537
+ apiRoomList.value = roomData
538
+
539
+ emit('room-query-success', {
540
+ success: true,
541
+ message: '查询成功',
542
+ data: roomData
543
+ })
544
+ } catch (error) {
545
+ emit('room-query-error', error)
546
+ MessagePlugin.error({
547
+ duration: 3000,
548
+ content: error.message || '查询会议室失败'
549
+ })
550
+ } finally {
551
+ roomQueryLoading.value = false
552
+ }
310
553
  }
311
554
 
312
- const handleTimeSlotClick = (room: Room, slot: TimeSlot) => {
313
- if (disabled || isSlotDisabled(room.id, slot.value) || isSlotBooked(room.id, slot.value)) {
555
+ const handleBeforeSubmit = (bookingData) => {
556
+ try {
557
+ emit('before-submit', bookingData)
558
+ } catch (error) {
559
+ throw new Error(error.message)
560
+ }
561
+ }
562
+ const submitBooking = async (bookingData) => {
563
+ if (!props.bookingApiConfig || Object.keys(props.bookingApiConfig).length === 0) {
564
+ emit('booking-confirm', bookingData)
314
565
  return
315
566
  }
316
567
 
317
- // 如果点击的不是当前选中的会议室,先选中会议室
318
- if (selectedRoom.value?.id !== room.id) {
319
- handleRoomSelect(room)
568
+ try {
569
+ bookingLoading.value = true
570
+
571
+ let date = formatDate(props.selectedDate)
572
+ const submitData = {
573
+ ...props.bookingFormData,
574
+ conference_id: bookingData.room.id,
575
+ roomName: bookingData.room.name,
576
+ date: date,
577
+ start_time: new Date(date + " " + bookingData.startTime + ":00").getTime(),
578
+ end_time: new Date(date + " " + bookingData.endTime + ":00") - 1,
579
+ duration: bookingData.duration,
580
+ timeSlots: bookingData.timeSlots,
581
+ requestTime: new Date().getTime(),
582
+ }
583
+
584
+ const response = await dataService.fetch({ saveData: submitData }, props.bookingApiConfig)
585
+
586
+ showBookingSuccess(bookingData)
587
+
588
+ } catch (error) {
589
+ emit('booking-error', error)
590
+ MessagePlugin.error({
591
+ duration: 3000,
592
+ content: error.message || '预约失败'
593
+ })
594
+ } finally {
595
+ bookingLoading.value = false
320
596
  }
597
+ }
321
598
 
322
- // 处理时间段选择
323
- if (!selectingStart.value) {
324
- // 开始选择
325
- selectingStart.value = slot.value
326
- selectedTimeSlots.value = [slot.value]
599
+ // 事件处理方法
600
+ const handleRoomSelect = (room) => {
601
+ if (selectedRoom.value?.id === room.id) {
602
+ return
327
603
  } else {
328
- // 完成选择
329
- const startIndex = timeSlotList.value.findIndex(s => s.value === selectingStart.value)
330
- const endIndex = timeSlotList.value.findIndex(s => s.value === slot.value)
331
-
332
- if (startIndex !== -1 && endIndex !== -1) {
333
- const minIndex = Math.min(startIndex, endIndex)
334
- const maxIndex = Math.max(startIndex, endIndex)
335
- const duration = maxIndex - minIndex + 1
336
-
337
- // 检查时长限制
338
- if (duration < props.minDuration || duration > props.maxDuration) {
339
- clearTimeSelection()
340
- return
341
- }
342
-
343
- // 检查选择范围内是否有已预约的时间段
344
- const selectedRange = timeSlotList.value.slice(minIndex, maxIndex + 1)
345
- const hasBookedSlot = selectedRange.some(s => isSlotBooked(room.id, s.value))
346
-
347
- if (hasBookedSlot) {
348
- clearTimeSelection()
349
- return
350
- }
351
-
352
- selectedTimeSlots.value = selectedRange.map(s => s.value)
353
- selectingStart.value = null
354
-
355
- emit('time-select', currentSelection.value)
356
- }
604
+ selectedRoom.value = room
605
+ clearTimeSelection()
606
+ emit('room-select', room)
357
607
  }
358
608
  }
359
609
 
360
- const handleTimeSlotHover = (room: Room, slot: TimeSlot) => {
361
- if (!selectingStart.value || selectedRoom.value?.id !== room.id) {
610
+ const handleTimeSlotClick = (room, slot) => {
611
+ if (props.disabled || isSlotDisabled(room.id, slot.value) || isSlotBooked(room.id, slot.value)) {
362
612
  return
363
613
  }
364
-
365
- hoveringSlot.value = slot.value
366
- }
367
614
 
368
- const handleTimeSlotLeave = () => {
369
- hoveringSlot.value = null
615
+ if (!selectedRoom.value || selectedRoom.value.id !== room.id) {
616
+ selectedRoom.value = room
617
+ emit('room-select', room)
618
+ }
619
+
620
+ const slotIndex = selectedTimeSlots.value.indexOf(slot.value)
621
+
622
+ if (slotIndex > -1) {
623
+ selectedTimeSlots.value.splice(slotIndex, 1)
624
+ } else {
625
+ if (selectedTimeSlots.value.length === 0) {
626
+ selectedTimeSlots.value.push(slot.value)
627
+ } else {
628
+ const newSlots = [...selectedTimeSlots.value, slot.value]
629
+ selectedTimeSlots.value = getSmartSelectedSlots(newSlots, room.id)
630
+ }
631
+
632
+ if (selectedTimeSlots.value.length > props.maxDuration) {
633
+ selectedTimeSlots.value = getLimitedSlots(selectedTimeSlots.value, slot.value, props.maxDuration)
634
+ }
635
+ }
636
+
637
+ selectedTimeSlots.value = [...selectedTimeSlots.value]
638
+
639
+ emit('time-select', currentSelection.value)
370
640
  }
371
641
 
372
- const handleConfirmBooking = () => {
373
- if (currentSelection.value) {
374
- emit('booking-confirm', currentSelection.value)
375
- clearSelection()
642
+ const handleConfirmBooking = async () => {
643
+ if (!currentSelection.value) return
644
+
645
+ const bookingData = {
646
+ room: selectedRoom.value,
647
+ date: currentDate.value,
648
+ startTime: currentSelection.value.startTime,
649
+ endTime: currentSelection.value.endTime,
650
+ duration: currentSelection.value.duration,
651
+ timeSlots: currentSelection.value.timeSlots,
652
+ isPreemptive: false,
653
+ conflictMeetings: []
376
654
  }
655
+
656
+ handleBeforeSubmit(bookingData)
377
657
  }
378
658
 
379
659
  const handleCancelSelection = () => {
380
660
  clearSelection()
381
661
  }
382
662
 
663
+ // 时间筛选功能
664
+ const toggleTimeFilter = () => {
665
+ showAllTimeSlots.value = !showAllTimeSlots.value
666
+ }
667
+
383
668
  // 监听属性变化
384
669
  watch(() => props.selectedDate, (newDate) => {
385
- if (newDate && newDate !== currentDate.value) {
386
- currentDate.value = newDate
387
- clearSelection()
670
+ })
671
+
672
+ // 新增计算属性
673
+ const filteredTimeSlotList = computed(() => {
674
+ if (showAllTimeSlots.value) {
675
+ return timeSlotList.value
676
+ } else {
677
+ return timeSlotList.value.filter(slot =>
678
+ slot.hour >= 7 && slot.hour < 22
679
+ )
388
680
  }
389
681
  })
390
682
 
683
+ // 辅助方法:将时间戳转换为时间段数组
684
+ const getTimeSlotsFromRange = (startTime, endTime) => {
685
+ const slots = []
686
+ let current = new Date(startTime)
687
+ const end = new Date(endTime)
688
+ while (current < end) {
689
+ const hour = current.getHours().toString().padStart(2, '0')
690
+ const minutes = current.getMinutes().toString().padStart(2, '0')
691
+ slots.push(`${hour}:${minutes}`)
692
+ current.setMinutes(current.getMinutes() + 30)
693
+ }
694
+ return slots
695
+ }
696
+
697
+ // 初始化选中的会议室和时间段
698
+ const initializeSelection = async (bookingData) => {
699
+ const roomId = bookingData.conference_id
700
+ const startTime = bookingData.start_time
701
+ const endTime = bookingData.end_time
702
+
703
+ if (!startTime || !endTime) return
704
+
705
+ const rawSlots = getTimeSlotsFromRange(startTime, endTime)
706
+
707
+ if (apiRoomList.value.length === 0) {
708
+ await queryRooms()
709
+ }
710
+
711
+ const room = roomList.value.find(r => r.id === roomId)
712
+ if (!room) return
713
+
714
+ const validSlots = rawSlots.filter(slot => !isSlotBooked(roomId, slot))
715
+
716
+ selectedRoom.value = room
717
+
718
+ selectedTimeSlots.value = validSlots
719
+ }
720
+
721
+ // 监听bookingFormData变化,进行初始化选中
722
+ watch(() => props.bookingFormData, async (newVal) => {
723
+ if (newVal && newVal.conference_id && newVal.start_time && newVal.end_time) {
724
+ await initializeSelection(newVal)
725
+ }
726
+ }, { deep: true, immediate: true })
727
+
391
728
  // 暴露组件实例方法
392
729
  defineExpose({
393
730
  clearSelection,
394
731
  clearTimeSelection,
395
- getCurrentSelection: () => currentSelection.value
732
+ getCurrentSelection: () => currentSelection.value,
733
+ queryRooms,
734
+ submitBooking,
735
+ showBookingSuccess,
736
+ hideSuccessMessage,
737
+ toggleTimeFilter,
738
+ getApiRoomList: () => apiRoomList.value,
739
+ refreshRooms: queryRooms,
740
+ getShowAllTimeSlots: () => showAllTimeSlots.value,
741
+ setShowAllTimeSlots: (value) => { showAllTimeSlots.value = value },
742
+ compareTimes,
743
+ getCurrentTime
396
744
  })
397
745
  </script>
398
746
 
399
747
  <style scoped>
400
748
  .ebiz-meeting-room-selector {
749
+ padding-bottom: 280px;
750
+ position: relative;
751
+ transform: translateX(0px);
752
+ }
753
+
754
+ .selector-content {
755
+ padding: 16px;
756
+ }
757
+
758
+ .rooms-list {
759
+ display: flex;
760
+ flex-direction: column;
761
+ gap: 16px;
762
+ }
763
+
764
+ .room-card {
401
765
  background: #fff;
402
- border-radius: 8px;
403
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
766
+ border-radius: 12px;
404
767
  overflow: hidden;
768
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
769
+ transition: all 0.3s ease;
405
770
  }
406
771
 
407
- .selector-header {
408
- padding: 20px;
409
- border-bottom: 1px solid #e7e7e7;
410
- background: #fafafa;
772
+ .room-card.selected {
773
+ border: 2px solid #1890ff;
774
+ box-shadow: 0 4px 12px rgba(24, 144, 255, 0.2);
411
775
  }
412
776
 
413
- .header-content {
777
+ .room-info {
778
+ padding: 16px;
779
+ cursor: pointer;
780
+ }
781
+
782
+ .room-header {
414
783
  display: flex;
415
784
  justify-content: space-between;
416
785
  align-items: center;
786
+ margin-bottom: 12px;
417
787
  }
418
788
 
419
- .title {
789
+ .room-name {
420
790
  margin: 0;
421
- font-size: 18px;
791
+ font-size: 16px;
422
792
  font-weight: 600;
423
793
  color: #333;
424
794
  }
425
795
 
426
- .date-selector {
796
+ .room-controls {
427
797
  display: flex;
428
798
  align-items: center;
429
799
  gap: 8px;
430
800
  }
431
801
 
432
- .date-selector label {
433
- font-weight: 500;
434
- color: #666;
802
+ .time-filter-btn {
803
+ flex-shrink: 0;
435
804
  }
436
805
 
437
- .loading-container {
438
- padding: 60px;
439
- text-align: center;
806
+ .time-filter-btn .t-button {
807
+ height: 24px;
808
+ padding: 0 8px;
809
+ font-size: 11px;
810
+ border-radius: 12px;
440
811
  }
441
812
 
442
- .selector-content {
443
- overflow-x: auto;
813
+ .room-status {
814
+ padding: 4px 8px;
815
+ border-radius: 12px;
816
+ font-size: 12px;
817
+ font-weight: 500;
444
818
  }
445
819
 
446
- .time-header {
447
- display: flex;
448
- background: #f5f5f5;
449
- border-bottom: 2px solid #e7e7e7;
450
- position: sticky;
451
- top: 0;
452
- z-index: 10;
820
+ .room-status.available {
821
+ background: #f6ffed;
822
+ color: #52c41a;
453
823
  }
454
824
 
455
- .room-column-header {
456
- width: 280px;
457
- min-width: 280px;
458
- padding: 12px 16px;
459
- font-weight: 600;
460
- color: #333;
461
- border-right: 1px solid #e7e7e7;
462
- background: #f5f5f5;
825
+ .room-status.partially-available {
826
+ background: #fff7e6;
827
+ color: #fa8c16;
828
+ }
829
+
830
+ .room-status.fully-booked {
831
+ background: #fff2f0;
832
+ color: #ff4d4f;
463
833
  }
464
834
 
465
- .time-slots-header {
835
+ .room-details {
466
836
  display: flex;
467
- flex: 1;
837
+ gap: 16px;
838
+ margin-bottom: 12px;
468
839
  }
469
840
 
470
- .time-slot-header {
471
- width: 80px;
472
- min-width: 80px;
473
- padding: 12px 8px;
474
- text-align: center;
475
- font-size: 12px;
476
- font-weight: 500;
841
+ .detail-item {
842
+ display: flex;
843
+ align-items: center;
844
+ gap: 4px;
845
+ font-size: 14px;
477
846
  color: #666;
478
- border-right: 1px solid #e7e7e7;
479
847
  }
480
848
 
481
- .rooms-container {
482
- max-height: 600px;
483
- overflow-y: auto;
849
+ .icon {
850
+ font-size: 16px;
484
851
  }
485
852
 
486
- .room-row {
853
+ .equipment {
487
854
  display: flex;
488
- border-bottom: 1px solid #e7e7e7;
489
- transition: background-color 0.2s;
490
- }
491
-
492
- .room-row:hover {
493
- background: #f9f9f9;
855
+ flex-wrap: wrap;
856
+ gap: 6px;
494
857
  }
495
858
 
496
- .room-row.selected {
497
- background: #e6f7ff;
859
+ .equipment-tag {
860
+ font-size: 12px;
498
861
  }
499
862
 
500
- .room-info {
501
- width: 280px;
502
- min-width: 280px;
863
+ .time-selection {
864
+ border-top: 1px solid #f0f0f0;
503
865
  padding: 16px;
504
- border-right: 1px solid #e7e7e7;
505
- cursor: pointer;
506
- transition: background-color 0.2s;
507
- }
508
-
509
- .room-info:hover {
510
- background: rgba(24, 144, 255, 0.05);
866
+ background: #fafafa;
511
867
  }
512
868
 
513
- .room-basic-info {
514
- height: 100%;
869
+ .time-header {
870
+ display: flex;
871
+ justify-content: space-between;
872
+ align-items: center;
873
+ margin-bottom: 12px;
515
874
  }
516
875
 
517
- .room-name {
518
- margin: 0 0 8px 0;
519
- font-size: 16px;
876
+ .time-header h5 {
877
+ margin: 0;
878
+ font-size: 14px;
520
879
  font-weight: 600;
521
880
  color: #333;
522
881
  }
523
882
 
524
- .room-details {
525
- margin: 0 0 8px 0;
526
- font-size: 14px;
527
- color: #666;
528
- line-height: 1.4;
529
- }
530
-
531
- .room-details span {
532
- display: block;
533
- }
534
-
535
- .equipment {
536
- display: flex;
537
- flex-wrap: wrap;
538
- gap: 4px;
883
+ .selected-info {
884
+ font-size: 12px;
885
+ color: #1890ff;
886
+ font-weight: 500;
539
887
  }
540
888
 
541
889
  .time-slots-grid {
542
- display: flex;
543
- flex: 1;
890
+ display: grid;
891
+ grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
892
+ gap: 12px;
544
893
  }
545
894
 
546
895
  .time-slot {
547
- width: 80px;
548
- min-width: 80px;
549
- height: 80px;
550
- border-right: 1px solid #e7e7e7;
896
+ background: #f6ffed;
897
+ border: 1px solid #52c41a;
898
+ border-radius: 8px;
899
+ padding: 12px;
900
+ text-align: center;
551
901
  cursor: pointer;
552
- transition: all 0.2s;
553
- position: relative;
902
+ transition: all 0.2s ease;
903
+ min-height: 80px;
554
904
  display: flex;
555
- align-items: center;
905
+ flex-direction: column;
556
906
  justify-content: center;
557
907
  }
558
908
 
559
- .time-slot:hover:not(.booked):not(.disabled) {
560
- background: rgba(24, 144, 255, 0.1);
909
+ .time-slot:active {
910
+ transform: scale(0.95);
561
911
  }
562
912
 
563
913
  .time-slot.booked {
564
914
  background: #f5f5f5;
915
+ border-color: #d9d9d9;
565
916
  cursor: not-allowed;
566
917
  }
567
918
 
568
919
  .time-slot.selected {
569
- background: #1890ff;
570
- color: white;
571
- }
572
-
573
- .time-slot.selecting {
574
- background: rgba(24, 144, 255, 0.3);
920
+ background: #e6f7ff;
921
+ border-color: #1890ff;
922
+ color: #1890ff;
575
923
  }
576
924
 
577
925
  .time-slot.disabled {
578
- background: #f0f0f0;
926
+ background: #f5f5f5;
927
+ border-color: #d9d9d9;
579
928
  cursor: not-allowed;
580
- opacity: 0.5;
581
929
  }
582
930
 
583
931
  .slot-content {
584
- font-size: 12px;
585
- text-align: center;
932
+ display: flex;
933
+ flex-direction: column;
934
+ align-items: center;
935
+ gap: 4px;
586
936
  }
587
937
 
588
- .booked-indicator {
589
- color: #999;
938
+ .slot-time {
939
+ font-size: 14px;
590
940
  font-weight: 500;
591
941
  }
592
942
 
593
- .selected-indicator {
594
- color: white;
595
- font-weight: 500;
943
+ .status-icon {
944
+ font-size: 14px;
945
+ color: #666;
946
+ word-break: break-all;
596
947
  }
597
948
 
598
949
  .booking-panel {
599
- padding: 20px;
600
- background: #f9f9f9;
601
- border-top: 1px solid #e7e7e7;
950
+ position: fixed;
951
+ bottom: 0;
952
+ left: 8px;
953
+ right: 8px;
954
+ background: #fff;
955
+ border-top: 1px solid #eee;
956
+ box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.1);
957
+ z-index: 1000;
958
+ animation: slideUp 0.3s ease;
602
959
  }
603
960
 
604
- .booking-form h4 {
605
- margin: 0 0 16px 0;
606
- font-size: 16px;
961
+ @keyframes slideUp {
962
+ from {
963
+ transform: translateY(100%);
964
+ }
965
+
966
+ to {
967
+ transform: translateY(0);
968
+ }
969
+ }
970
+
971
+ .booking-form {
972
+ padding: 24px;
973
+ }
974
+
975
+ .booking-header {
976
+ display: flex;
977
+ justify-content: space-between;
978
+ align-items: center;
979
+ margin-bottom: 16px;
980
+ }
981
+
982
+ .booking-header h4 {
983
+ margin: 0;
984
+ font-size: 18px;
607
985
  font-weight: 600;
608
986
  color: #333;
609
987
  }
610
988
 
611
989
  .booking-info {
612
- margin-bottom: 16px;
990
+ margin-bottom: 24px;
613
991
  }
614
992
 
615
- .booking-info p {
616
- margin: 0 0 8px 0;
617
- font-size: 14px;
993
+ .info-row {
994
+ display: flex;
995
+ justify-content: space-between;
996
+ align-items: center;
997
+ padding: 8px 0;
998
+ font-size: 16px;
999
+ }
1000
+
1001
+ .label {
618
1002
  color: #666;
1003
+ font-weight: 500;
619
1004
  }
620
1005
 
621
- .booking-actions {
622
- display: flex;
623
- gap: 12px;
624
- justify-content: flex-end;
1006
+ .value {
1007
+ color: #333;
1008
+ font-weight: 600;
625
1009
  }
626
1010
 
627
- .selector-footer {
628
- padding: 16px 20px;
629
- background: #fafafa;
630
- border-top: 1px solid #e7e7e7;
1011
+ .booking-actions {
1012
+ margin-top: 16px;
631
1013
  }
632
1014
 
633
- .legend {
1015
+ /* 预约成功提示样式 */
1016
+ .success-overlay {
1017
+ position: fixed;
1018
+ top: 0;
1019
+ left: 0;
1020
+ right: 0;
1021
+ bottom: 0;
1022
+ background: rgba(0, 0, 0, 0.5);
634
1023
  display: flex;
635
- gap: 24px;
1024
+ align-items: center;
636
1025
  justify-content: center;
1026
+ z-index: 1000;
1027
+ animation: fadeIn 0.3s ease;
1028
+ }
1029
+
1030
+ .success-container {
1031
+ background: #fff;
1032
+ border-radius: 16px;
1033
+ padding: 32px;
1034
+ margin: 20px;
1035
+ max-width: 500px;
1036
+ width: 100%;
1037
+ text-align: center;
1038
+ animation: slideUpSuccess 0.3s ease;
1039
+ }
1040
+
1041
+ .success-icon {
1042
+ font-size: 40px;
1043
+ margin-bottom: 24px;
1044
+ }
1045
+
1046
+ .success-title {
1047
+ font-size: 24px;
1048
+ font-weight: 600;
1049
+ color: #333;
1050
+ margin-bottom: 24px;
637
1051
  }
638
1052
 
639
- .legend-item {
1053
+ .success-content {
1054
+ text-align: left;
1055
+ margin-bottom: 32px;
1056
+ }
1057
+
1058
+ .success-item {
640
1059
  display: flex;
1060
+ justify-content: space-between;
641
1061
  align-items: center;
642
- gap: 8px;
643
- font-size: 14px;
1062
+ padding: 12px 0;
1063
+ border-bottom: 1px solid #f0f0f0;
1064
+ }
1065
+
1066
+ .success-item:last-child {
1067
+ border-bottom: none;
1068
+ }
1069
+
1070
+ .success-label {
1071
+ font-size: 16px;
644
1072
  color: #666;
645
1073
  }
646
1074
 
647
- .legend-color {
648
- width: 16px;
649
- height: 16px;
650
- border-radius: 2px;
651
- border: 1px solid #e7e7e7;
1075
+ .success-value {
1076
+ font-size: 16px;
1077
+ font-weight: 500;
1078
+ color: #333;
1079
+ }
1080
+
1081
+ .success-preempt-info {
1082
+ margin-top: 16px;
1083
+ padding: 16px;
1084
+ background: #fff7e6;
1085
+ border-radius: 8px;
1086
+ border-left: 4px solid #fa8c16;
652
1087
  }
653
1088
 
654
- .legend-color.available {
655
- background: white;
1089
+ .preempt-info-title {
1090
+ font-size: 14px;
1091
+ color: #d46b08;
1092
+ font-weight: 600;
1093
+ margin-bottom: 8px;
656
1094
  }
657
1095
 
658
- .legend-color.booked {
659
- background: #f5f5f5;
1096
+ .preempt-info-content {
1097
+ font-size: 14px;
1098
+ color: #d46b08;
1099
+ line-height: 1.4;
660
1100
  }
661
1101
 
662
- .legend-color.selected {
663
- background: #1890ff;
1102
+ .success-actions {
1103
+ text-align: center;
1104
+ }
1105
+
1106
+ @keyframes fadeIn {
1107
+ from {
1108
+ opacity: 0;
1109
+ }
1110
+
1111
+ to {
1112
+ opacity: 1;
1113
+ }
1114
+ }
1115
+
1116
+ @keyframes slideUpSuccess {
1117
+ from {
1118
+ transform: translateY(30px);
1119
+ opacity: 0;
1120
+ }
1121
+
1122
+ to {
1123
+ transform: translateY(0);
1124
+ opacity: 1;
1125
+ }
1126
+ }
1127
+
1128
+ /* 响应式设计 */
1129
+ @media (max-width: 768px) {
1130
+ .time-slots-grid {
1131
+ grid-template-columns: repeat(auto-fill, minmax(80px, 1fr));
1132
+ gap: 8px;
1133
+ }
1134
+
1135
+ .time-slot {
1136
+ padding: 8px;
1137
+ min-height: 60px;
1138
+ }
1139
+
1140
+ .slot-time {
1141
+ font-size: 12px;
1142
+ }
1143
+
1144
+ .status-icon {
1145
+ font-size: 12px;
1146
+ }
1147
+
1148
+ .success-container {
1149
+ margin: 10px;
1150
+ }
664
1151
  }
665
- </style>
1152
+ </style>