@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.
- package/dist/designer-components.css +1 -1
- package/dist/index.mjs +15939 -15624
- package/package.json +1 -1
- package/src/apiService/simpleDataService.js +129 -116
- package/src/components/EbizMeetingRoomSelector.vue +898 -411
- package/src/components/senior/EbizSDialog/index.vue +4 -1
|
@@ -1,209 +1,283 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
<
|
|
6
|
-
<
|
|
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="
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
:key="room.id"
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
</
|
|
80
|
+
</div>
|
|
96
81
|
</div>
|
|
97
82
|
</div>
|
|
98
83
|
</div>
|
|
99
84
|
</div>
|
|
100
|
-
</div>
|
|
101
85
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
</
|
|
118
|
-
</
|
|
120
|
+
</slot>
|
|
121
|
+
</div>
|
|
119
122
|
</div>
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
<div class="
|
|
126
|
-
<
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
<
|
|
131
|
-
|
|
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="
|
|
134
|
-
<
|
|
135
|
-
<span>已选择</span>
|
|
150
|
+
<div class="success-actions">
|
|
151
|
+
<t-button theme="primary" @click="hideSuccessMessage">确定</t-button>
|
|
136
152
|
</div>
|
|
137
153
|
</div>
|
|
138
|
-
</
|
|
139
|
-
</div>
|
|
154
|
+
</div>
|
|
140
155
|
</div>
|
|
141
156
|
</template>
|
|
142
157
|
|
|
143
|
-
<script setup
|
|
158
|
+
<script setup>
|
|
144
159
|
import { ref, computed, watch, defineProps, defineEmits } from 'vue'
|
|
145
|
-
import {
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
disabled
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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([
|
|
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 =
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
const
|
|
194
|
-
const
|
|
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(() =>
|
|
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
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
|
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
|
-
|
|
242
|
-
|
|
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
|
-
|
|
246
|
-
|
|
442
|
+
// 隐藏成功信息
|
|
443
|
+
const hideSuccessMessage = () => {
|
|
444
|
+
showSuccessMessage.value = false
|
|
445
|
+
successData.value = {}
|
|
446
|
+
emit('booking-success', successData.value)
|
|
247
447
|
}
|
|
248
448
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|
|
269
|
-
|
|
270
|
-
|
|
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 = ()
|
|
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
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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
|
|
313
|
-
|
|
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
|
-
|
|
319
|
-
|
|
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
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
selectedTimeSlots.value = [slot.value]
|
|
599
|
+
// 事件处理方法
|
|
600
|
+
const handleRoomSelect = (room) => {
|
|
601
|
+
if (selectedRoom.value?.id === room.id) {
|
|
602
|
+
return
|
|
327
603
|
} else {
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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
|
|
361
|
-
if (
|
|
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
|
-
|
|
369
|
-
|
|
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
|
-
|
|
375
|
-
|
|
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
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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:
|
|
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
|
-
.
|
|
408
|
-
|
|
409
|
-
|
|
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
|
-
.
|
|
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
|
-
.
|
|
789
|
+
.room-name {
|
|
420
790
|
margin: 0;
|
|
421
|
-
font-size:
|
|
791
|
+
font-size: 16px;
|
|
422
792
|
font-weight: 600;
|
|
423
793
|
color: #333;
|
|
424
794
|
}
|
|
425
795
|
|
|
426
|
-
.
|
|
796
|
+
.room-controls {
|
|
427
797
|
display: flex;
|
|
428
798
|
align-items: center;
|
|
429
799
|
gap: 8px;
|
|
430
800
|
}
|
|
431
801
|
|
|
432
|
-
.
|
|
433
|
-
|
|
434
|
-
color: #666;
|
|
802
|
+
.time-filter-btn {
|
|
803
|
+
flex-shrink: 0;
|
|
435
804
|
}
|
|
436
805
|
|
|
437
|
-
.
|
|
438
|
-
|
|
439
|
-
|
|
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
|
-
.
|
|
443
|
-
|
|
813
|
+
.room-status {
|
|
814
|
+
padding: 4px 8px;
|
|
815
|
+
border-radius: 12px;
|
|
816
|
+
font-size: 12px;
|
|
817
|
+
font-weight: 500;
|
|
444
818
|
}
|
|
445
819
|
|
|
446
|
-
.
|
|
447
|
-
|
|
448
|
-
|
|
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-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
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
|
-
.
|
|
835
|
+
.room-details {
|
|
466
836
|
display: flex;
|
|
467
|
-
|
|
837
|
+
gap: 16px;
|
|
838
|
+
margin-bottom: 12px;
|
|
468
839
|
}
|
|
469
840
|
|
|
470
|
-
.
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
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
|
-
.
|
|
482
|
-
|
|
483
|
-
overflow-y: auto;
|
|
849
|
+
.icon {
|
|
850
|
+
font-size: 16px;
|
|
484
851
|
}
|
|
485
852
|
|
|
486
|
-
.
|
|
853
|
+
.equipment {
|
|
487
854
|
display: flex;
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
.room-row:hover {
|
|
493
|
-
background: #f9f9f9;
|
|
855
|
+
flex-wrap: wrap;
|
|
856
|
+
gap: 6px;
|
|
494
857
|
}
|
|
495
858
|
|
|
496
|
-
.
|
|
497
|
-
|
|
859
|
+
.equipment-tag {
|
|
860
|
+
font-size: 12px;
|
|
498
861
|
}
|
|
499
862
|
|
|
500
|
-
.
|
|
501
|
-
|
|
502
|
-
min-width: 280px;
|
|
863
|
+
.time-selection {
|
|
864
|
+
border-top: 1px solid #f0f0f0;
|
|
503
865
|
padding: 16px;
|
|
504
|
-
|
|
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
|
-
.
|
|
514
|
-
|
|
869
|
+
.time-header {
|
|
870
|
+
display: flex;
|
|
871
|
+
justify-content: space-between;
|
|
872
|
+
align-items: center;
|
|
873
|
+
margin-bottom: 12px;
|
|
515
874
|
}
|
|
516
875
|
|
|
517
|
-
.
|
|
518
|
-
margin: 0
|
|
519
|
-
font-size:
|
|
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
|
-
.
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
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:
|
|
543
|
-
|
|
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
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
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
|
-
|
|
902
|
+
transition: all 0.2s ease;
|
|
903
|
+
min-height: 80px;
|
|
554
904
|
display: flex;
|
|
555
|
-
|
|
905
|
+
flex-direction: column;
|
|
556
906
|
justify-content: center;
|
|
557
907
|
}
|
|
558
908
|
|
|
559
|
-
.time-slot:
|
|
560
|
-
|
|
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: #
|
|
570
|
-
color:
|
|
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: #
|
|
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
|
-
|
|
585
|
-
|
|
932
|
+
display: flex;
|
|
933
|
+
flex-direction: column;
|
|
934
|
+
align-items: center;
|
|
935
|
+
gap: 4px;
|
|
586
936
|
}
|
|
587
937
|
|
|
588
|
-
.
|
|
589
|
-
|
|
938
|
+
.slot-time {
|
|
939
|
+
font-size: 14px;
|
|
590
940
|
font-weight: 500;
|
|
591
941
|
}
|
|
592
942
|
|
|
593
|
-
.
|
|
594
|
-
|
|
595
|
-
|
|
943
|
+
.status-icon {
|
|
944
|
+
font-size: 14px;
|
|
945
|
+
color: #666;
|
|
946
|
+
word-break: break-all;
|
|
596
947
|
}
|
|
597
948
|
|
|
598
949
|
.booking-panel {
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
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
|
-
|
|
605
|
-
|
|
606
|
-
|
|
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:
|
|
990
|
+
margin-bottom: 24px;
|
|
613
991
|
}
|
|
614
992
|
|
|
615
|
-
.
|
|
616
|
-
|
|
617
|
-
|
|
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
|
-
.
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
justify-content: flex-end;
|
|
1006
|
+
.value {
|
|
1007
|
+
color: #333;
|
|
1008
|
+
font-weight: 600;
|
|
625
1009
|
}
|
|
626
1010
|
|
|
627
|
-
.
|
|
628
|
-
|
|
629
|
-
background: #fafafa;
|
|
630
|
-
border-top: 1px solid #e7e7e7;
|
|
1011
|
+
.booking-actions {
|
|
1012
|
+
margin-top: 16px;
|
|
631
1013
|
}
|
|
632
1014
|
|
|
633
|
-
|
|
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
|
-
|
|
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
|
-
.
|
|
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
|
-
|
|
643
|
-
|
|
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
|
-
.
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
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
|
-
.
|
|
655
|
-
|
|
1089
|
+
.preempt-info-title {
|
|
1090
|
+
font-size: 14px;
|
|
1091
|
+
color: #d46b08;
|
|
1092
|
+
font-weight: 600;
|
|
1093
|
+
margin-bottom: 8px;
|
|
656
1094
|
}
|
|
657
1095
|
|
|
658
|
-
.
|
|
659
|
-
|
|
1096
|
+
.preempt-info-content {
|
|
1097
|
+
font-size: 14px;
|
|
1098
|
+
color: #d46b08;
|
|
1099
|
+
line-height: 1.4;
|
|
660
1100
|
}
|
|
661
1101
|
|
|
662
|
-
.
|
|
663
|
-
|
|
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>
|