Files
Academic-Affairs-System/web/src/views/ClassroomReservationsView.vue
T
2026-07-26 20:11:56 +08:00

1003 lines
29 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { computed, onMounted, reactive, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import http, { apiErrorMessage } from '../api/http'
import { useAuthStore } from '../stores/auth'
interface TermOption {
id: string
name: string
startDate: string
endDate: string
isCurrent: boolean
hasPublishedTimetable: boolean
}
interface CampusOption {
id: string
code: string
name: string
}
interface BuildingOption {
id: string
code: string
name: string
campusId: string
}
interface ClassroomOption {
id: string
code: string
name: string
buildingId: string
capacity: number
roomType: string
}
interface AvailableClassroom extends ClassroomOption {
buildingName: string
campusId: string
campusName: string
}
interface TimeSlot {
periodNumber: number
name: string
startTime: string
endTime: string
}
interface Applicant {
id: string
displayName: string
collegeId: string
collegeName: string
}
interface Reservation {
id: string
applicantUserId: string
applicantName: string
applicantCollegeId: string
applicantCollegeName: string
academicTermId: string
academicTermName: string
classroomId: string
classroomCode: string
classroomName: string
buildingName: string
campusName: string
reservationDate: string
startPeriod: number
periodCount: number
attendeeCount: number
purpose: string
contactPhone: string
notes?: string
status: 'Submitted' | 'Approved' | 'Rejected' | 'Cancelled'
reviewComment?: string
reviewedAt?: string
cancelledAt?: string
createdAt: string
}
interface PagedResponse {
items: Reservation[]
total: number
page: number
pageSize: number
}
const auth = useAuthStore()
const isCollegeAdmin = computed(() =>
auth.user?.roles.includes('CollegeAdmin') ?? false,
)
const activeTab = ref<'mine' | 'review'>('mine')
const loading = ref(false)
const submitting = ref(false)
const availabilityLoading = ref(false)
const applicant = ref<Applicant | null>(null)
const applicantProblem = ref('')
const terms = ref<TermOption[]>([])
const campuses = ref<CampusOption[]>([])
const buildings = ref<BuildingOption[]>([])
const classrooms = ref<ClassroomOption[]>([])
const timeSlots = ref<TimeSlot[]>([])
const availableRooms = ref<AvailableClassroom[]>([])
const availabilityChecked = ref(false)
const mine = ref<Reservation[]>([])
const mineTotal = ref(0)
const minePage = ref(1)
const reviewItems = ref<Reservation[]>([])
const reviewTotal = ref(0)
const reviewPage = ref(1)
const pageSize = 12
const reviewStatus = ref<Reservation['status'] | ''>('Submitted')
const form = reactive({
academicTermId: '',
reservationDate: '',
campusId: '',
buildingId: '',
classroomId: '',
startPeriod: 1,
periodCount: 2,
attendeeCount: 1,
purpose: '',
contactPhone: '',
notes: '',
})
const selectedTerm = computed(() =>
terms.value.find((term) => term.id === form.academicTermId),
)
const filteredBuildings = computed(() =>
buildings.value.filter((building) =>
!form.campusId || building.campusId === form.campusId,
),
)
const availablePeriodCounts = computed(() => {
const enabled = new Set(timeSlots.value.map((slot) => slot.periodNumber))
const counts: number[] = []
for (let count = 1; count <= 6; count += 1) {
if (!enabled.has(form.startPeriod + count - 1)) break
counts.push(count)
}
return counts
})
const statusMeta: Record<Reservation['status'], { label: string; type: 'warning' | 'success' | 'danger' | 'info' }> = {
Submitted: { label: '待学院审核', type: 'warning' },
Approved: { label: '已批准', type: 'success' },
Rejected: { label: '已驳回', type: 'danger' },
Cancelled: { label: '已取消', type: 'info' },
}
function statusInfo(status: unknown) {
return statusMeta[status as Reservation['status']]
?? { label: '未知状态', type: 'info' as const }
}
function todayText() {
const formatter = new Intl.DateTimeFormat('en-CA', {
timeZone: 'Asia/Shanghai',
year: 'numeric',
month: '2-digit',
day: '2-digit',
})
return formatter.format(new Date())
}
function initialReservationDate(term?: TermOption) {
const today = todayText()
if (!term) return today
if (today < term.startDate) return term.startDate
if (today > term.endDate) return ''
return today
}
function invalidateAvailability() {
availabilityChecked.value = false
availableRooms.value = []
form.classroomId = ''
}
async function loadOptions(termId?: string) {
const { data } = await http.get('/classroom-reservations/options', {
params: termId ? { academicTermId: termId } : {},
})
applicant.value = data.applicant
applicantProblem.value = data.applicantProblem ?? ''
terms.value = data.terms
campuses.value = data.campuses
buildings.value = data.buildings
classrooms.value = data.classrooms
timeSlots.value = data.timeSlots
const nextTermId = termId || data.selectedTermId || ''
form.academicTermId = nextTermId
if (!timeSlots.value.some((slot) => slot.periodNumber === form.startPeriod)) {
form.startPeriod = timeSlots.value[0]?.periodNumber ?? 1
}
if (!availablePeriodCounts.value.includes(form.periodCount)) {
form.periodCount = availablePeriodCounts.value[0] ?? 1
}
if (!form.reservationDate ||
form.reservationDate < (selectedTerm.value?.startDate ?? '') ||
form.reservationDate > (selectedTerm.value?.endDate ?? '9999-12-31')) {
form.reservationDate = initialReservationDate(selectedTerm.value)
}
invalidateAvailability()
}
async function changeTerm(termId: string) {
try {
await loadOptions(termId)
} catch (error) {
ElMessage.error(apiErrorMessage(error))
}
}
function changeCampus() {
if (form.buildingId &&
!filteredBuildings.value.some((building) => building.id === form.buildingId)) {
form.buildingId = ''
}
invalidateAvailability()
}
function isDateDisabled(value: Date) {
const term = selectedTerm.value
if (!term) return true
const date = [
value.getFullYear(),
String(value.getMonth() + 1).padStart(2, '0'),
String(value.getDate()).padStart(2, '0'),
].join('-')
return date < todayText() || date < term.startDate || date > term.endDate
}
async function queryAvailability() {
if (!form.academicTermId || !form.reservationDate) {
ElMessage.warning('请先选择学期和预约日期。')
return
}
availabilityLoading.value = true
try {
const { data } = await http.get('/classroom-reservations/availability', {
params: {
academicTermId: form.academicTermId,
reservationDate: form.reservationDate,
startPeriod: form.startPeriod,
periodCount: form.periodCount,
campusId: form.campusId || undefined,
buildingId: form.buildingId || undefined,
attendeeCount: form.attendeeCount,
},
})
availableRooms.value = data.items
availabilityChecked.value = true
if (!availableRooms.value.some((room) => room.id === form.classroomId)) {
form.classroomId = ''
}
if (availableRooms.value.length === 0) {
ElMessage.warning('当前条件下没有可预约教室,请调整日期、节次或人数。')
}
} catch (error) {
availabilityChecked.value = false
ElMessage.error(apiErrorMessage(error))
} finally {
availabilityLoading.value = false
}
}
async function submitReservation() {
if (!applicant.value) {
ElMessage.error(applicantProblem.value || '当前账号没有可用的学院归属。')
return
}
if (!availabilityChecked.value || !form.classroomId) {
ElMessage.warning('请先查询可用教室并选择教室。')
return
}
if (!form.purpose.trim() || !form.contactPhone.trim()) {
ElMessage.warning('请填写借用用途和联系电话。')
return
}
submitting.value = true
try {
await http.post('/classroom-reservations', {
academicTermId: form.academicTermId,
classroomId: form.classroomId,
reservationDate: form.reservationDate,
startPeriod: form.startPeriod,
periodCount: form.periodCount,
attendeeCount: form.attendeeCount,
purpose: form.purpose.trim(),
contactPhone: form.contactPhone.trim(),
notes: form.notes.trim() || null,
})
ElMessage.success('申请已提交,将由您所在学院审核。')
form.purpose = ''
form.notes = ''
invalidateAvailability()
activeTab.value = 'mine'
minePage.value = 1
await loadMine()
} catch (error) {
ElMessage.error(apiErrorMessage(error))
} finally {
submitting.value = false
}
}
async function loadMine() {
loading.value = true
try {
const { data } = await http.get<PagedResponse>('/classroom-reservations/mine', {
params: { page: minePage.value, pageSize },
})
mine.value = data.items
mineTotal.value = data.total
} catch (error) {
ElMessage.error(apiErrorMessage(error))
} finally {
loading.value = false
}
}
async function loadReview() {
if (!isCollegeAdmin.value) return
loading.value = true
try {
const { data } = await http.get<PagedResponse>('/classroom-reservations/review', {
params: {
status: reviewStatus.value || undefined,
page: reviewPage.value,
pageSize,
},
})
reviewItems.value = data.items
reviewTotal.value = data.total
} catch (error) {
ElMessage.error(apiErrorMessage(error))
} finally {
loading.value = false
}
}
async function changeTab(name: string | number) {
activeTab.value = name as 'mine' | 'review'
if (activeTab.value === 'review') await loadReview()
}
async function cancelReservation(item: Reservation | Record<string, any>) {
try {
await ElMessageBox.confirm(
item.status === 'Approved'
? '该预约已经批准,取消后教室将立即释放。确认取消吗?'
: '确认撤回这条待审核申请吗?',
'取消教室预约',
{ type: 'warning', confirmButtonText: '确认取消', cancelButtonText: '暂不取消' },
)
await http.post(`/classroom-reservations/${item.id}/cancel`)
ElMessage.success('预约已取消。')
await loadMine()
} catch (error) {
if (error === 'cancel' || error === 'close') return
ElMessage.error(apiErrorMessage(error))
}
}
async function approveReservation(item: Reservation | Record<string, any>) {
try {
await ElMessageBox.confirm(
`确认批准 ${item.applicantName}${item.buildingName} ${item.classroomName} 的借用申请吗?系统会再次检查占用冲突。`,
'批准教室借用',
{ type: 'success', confirmButtonText: '批准', cancelButtonText: '返回' },
)
await http.post(`/classroom-reservations/${item.id}/approve`, { comment: null })
ElMessage.success('申请已批准。')
await loadReview()
} catch (error) {
if (error === 'cancel' || error === 'close') return
ElMessage.error(apiErrorMessage(error))
}
}
async function rejectReservation(item: Reservation | Record<string, any>) {
try {
const result = await ElMessageBox.prompt(
'请填写驳回原因,申请人将在消息中心收到该意见。',
'驳回教室借用',
{
inputType: 'textarea',
inputPlaceholder: '请输入具体原因',
inputValidator: (value) => value.trim().length > 0 || '驳回原因不能为空',
confirmButtonText: '确认驳回',
cancelButtonText: '返回',
type: 'warning',
},
)
await http.post(`/classroom-reservations/${item.id}/reject`, {
comment: result.value.trim(),
})
ElMessage.success('申请已驳回。')
await loadReview()
} catch (error) {
if (error === 'cancel' || error === 'close') return
ElMessage.error(apiErrorMessage(error))
}
}
function periodText(item: Reservation | Record<string, any>) {
const end = item.startPeriod + item.periodCount - 1
return item.periodCount === 1
? `第 ${item.startPeriod} 节`
: `第 ${item.startPeriod}${end} 节`
}
function roomLabel(room: AvailableClassroom) {
return `${room.campusName} · ${room.buildingName} · ${room.name}${room.capacity} 人)`
}
function formatDateTime(value?: string) {
return value ? new Date(value).toLocaleString('zh-CN', { hour12: false }) : '—'
}
onMounted(async () => {
try {
await Promise.all([loadOptions(), loadMine()])
} catch (error) {
ElMessage.error(apiErrorMessage(error))
}
})
</script>
<template>
<section class="reservation-page">
<header class="page-heading">
<div>
<p class="eyebrow">TEACHING SPACE</p>
<h2>教室借用与预约</h2>
<p>选择正式课表之外的空闲时段提交申请由申请人所在学院审核</p>
</div>
<el-tag v-if="applicant" type="success" effect="plain" size="large">
审核学院{{ applicant.collegeName }}
</el-tag>
</header>
<el-alert
v-if="applicantProblem"
:title="applicantProblem"
type="warning"
show-icon
:closable="false"
class="scope-alert"
/>
<el-card class="application-card" shadow="never">
<template #header>
<div class="card-title">
<div>
<h3>提交借用申请</h3>
<p>系统会避让正式课程已发布考试及已批准的教室预约</p>
</div>
<span v-if="applicant">{{ applicant.displayName }} · {{ applicant.collegeName }}</span>
</div>
</template>
<el-form label-position="top" :disabled="!applicant">
<div class="form-grid">
<el-form-item label="学期" required>
<el-select
v-model="form.academicTermId"
placeholder="选择学期"
@change="changeTerm"
>
<el-option
v-for="term in terms"
:key="term.id"
:value="term.id"
:label="`${term.name}${term.hasPublishedTimetable ? '' : '(课表未发布)'}`"
:disabled="!term.hasPublishedTimetable"
/>
</el-select>
</el-form-item>
<el-form-item label="预约日期" required>
<el-date-picker
v-model="form.reservationDate"
type="date"
value-format="YYYY-MM-DD"
format="YYYY-MM-DD"
placeholder="选择日期"
:disabled-date="isDateDisabled"
@change="invalidateAvailability"
/>
</el-form-item>
<el-form-item label="开始节次" required>
<el-select v-model="form.startPeriod" @change="invalidateAvailability">
<el-option
v-for="slot in timeSlots"
:key="slot.periodNumber"
:value="slot.periodNumber"
:label="`${slot.name}${slot.startTime ? `${slot.startTime}—${slot.endTime}` : ''}`"
/>
</el-select>
</el-form-item>
<el-form-item label="连续节数" required>
<el-select v-model="form.periodCount" @change="invalidateAvailability">
<el-option
v-for="count in availablePeriodCounts"
:key="count"
:value="count"
:label="`${count} 节`"
/>
</el-select>
</el-form-item>
<el-form-item label="预计人数" required>
<el-input-number
v-model="form.attendeeCount"
:min="1"
:max="10000"
controls-position="right"
@change="invalidateAvailability"
/>
</el-form-item>
<el-form-item label="校区">
<el-select
v-model="form.campusId"
clearable
placeholder="全部校区"
@change="changeCampus"
>
<el-option
v-for="campus in campuses"
:key="campus.id"
:value="campus.id"
:label="campus.name"
/>
</el-select>
</el-form-item>
<el-form-item label="教学楼">
<el-select
v-model="form.buildingId"
clearable
placeholder="全部教学楼"
@change="invalidateAvailability"
>
<el-option
v-for="building in filteredBuildings"
:key="building.id"
:value="building.id"
:label="building.name"
/>
</el-select>
</el-form-item>
<el-form-item label="可用教室" required>
<div class="room-picker">
<el-select
v-model="form.classroomId"
filterable
:disabled="!availabilityChecked"
:placeholder="availabilityChecked ? '选择可用教室' : '请先查询可用教室'"
>
<el-option
v-for="room in availableRooms"
:key="room.id"
:value="room.id"
:label="roomLabel(room)"
/>
</el-select>
<el-button
:loading="availabilityLoading"
type="primary"
plain
@click="queryAvailability"
>
查询可用教室
</el-button>
</div>
</el-form-item>
<el-form-item label="借用用途" required class="span-2">
<el-input
v-model="form.purpose"
maxlength="200"
show-word-limit
placeholder="例如:学院学术讲座、班级主题活动"
/>
</el-form-item>
<el-form-item label="联系电话" required>
<el-input
v-model="form.contactPhone"
maxlength="30"
placeholder="便于审核人联系"
/>
</el-form-item>
<el-form-item label="补充说明" class="span-2">
<el-input
v-model="form.notes"
type="textarea"
:rows="3"
maxlength="500"
show-word-limit
placeholder="可填写设备、布置或其他需求"
/>
</el-form-item>
</div>
<div class="submit-row">
<p v-if="availabilityChecked">
已找到 <b>{{ availableRooms.length }}</b> 间符合条件的空闲教室
</p>
<span v-else>日期节次人数变化后需重新查询可用教室</span>
<el-button
type="primary"
size="large"
:loading="submitting"
@click="submitReservation"
>
提交学院审核
</el-button>
</div>
</el-form>
</el-card>
<el-card class="records-card" shadow="never">
<el-tabs v-model="activeTab" @tab-change="changeTab">
<el-tab-pane label="我的申请" name="mine">
<div v-loading="loading">
<el-table :data="mine" class="desktop-table">
<el-table-column label="日期与节次" min-width="150">
<template #default="{ row }">
<b>{{ row.reservationDate }}</b>
<small>{{ periodText(row) }}</small>
</template>
</el-table-column>
<el-table-column label="教室" min-width="190">
<template #default="{ row }">
<b>{{ row.buildingName }} · {{ row.classroomName }}</b>
<small>{{ row.campusName }} · {{ row.attendeeCount }} </small>
</template>
</el-table-column>
<el-table-column prop="purpose" label="用途" min-width="180" show-overflow-tooltip />
<el-table-column label="状态" width="120">
<template #default="{ row }">
<el-tag :type="statusInfo(row.status).type">
{{ statusInfo(row.status).label }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="审核意见" min-width="170">
<template #default="{ row }">{{ row.reviewComment || '—' }}</template>
</el-table-column>
<el-table-column label="操作" width="90" fixed="right">
<template #default="{ row }">
<el-button
v-if="['Submitted', 'Approved'].includes(row.status)"
link
type="danger"
@click="cancelReservation(row)"
>
取消
</el-button>
</template>
</el-table-column>
</el-table>
<div class="mobile-records">
<article v-for="item in mine" :key="item.id" class="record-item">
<header>
<div>
<b>{{ item.reservationDate }} · {{ periodText(item) }}</b>
<span>{{ item.buildingName }} {{ item.classroomName }}</span>
</div>
<el-tag :type="statusInfo(item.status).type">
{{ statusInfo(item.status).label }}
</el-tag>
</header>
<p>{{ item.purpose }}</p>
<small>{{ item.attendeeCount }} · {{ item.contactPhone }}</small>
<blockquote v-if="item.reviewComment">{{ item.reviewComment }}</blockquote>
<el-button
v-if="['Submitted', 'Approved'].includes(item.status)"
type="danger"
plain
@click="cancelReservation(item)"
>
取消预约
</el-button>
</article>
</div>
<el-empty v-if="mine.length === 0" description="还没有教室借用申请" />
<el-pagination
v-if="mineTotal > pageSize"
v-model:current-page="minePage"
:page-size="pageSize"
:total="mineTotal"
layout="prev, pager, next"
@current-change="loadMine"
/>
</div>
</el-tab-pane>
<el-tab-pane v-if="isCollegeAdmin" label="本院审核" name="review">
<div class="review-toolbar">
<div>
<h3>{{ applicant?.collegeName || auth.user?.displayName }}审核队列</h3>
<p>这里只显示申请人归属本学院的教室借用申请</p>
</div>
<el-select
v-model="reviewStatus"
style="width: 150px"
@change="reviewPage = 1; loadReview()"
>
<el-option label="待审核" value="Submitted" />
<el-option label="已批准" value="Approved" />
<el-option label="已驳回" value="Rejected" />
<el-option label="已取消" value="Cancelled" />
<el-option label="全部状态" value="" />
</el-select>
</div>
<div v-loading="loading">
<el-table :data="reviewItems" class="desktop-table">
<el-table-column label="申请人" min-width="140">
<template #default="{ row }">
<b>{{ row.applicantName }}</b>
<small>{{ row.applicantCollegeName }}</small>
</template>
</el-table-column>
<el-table-column label="日期与教室" min-width="220">
<template #default="{ row }">
<b>{{ row.reservationDate }} · {{ periodText(row) }}</b>
<small>{{ row.campusName }} · {{ row.buildingName }} {{ row.classroomName }}</small>
</template>
</el-table-column>
<el-table-column label="用途与人数" min-width="220">
<template #default="{ row }">
<span>{{ row.purpose }}</span>
<small>{{ row.attendeeCount }} · {{ row.contactPhone }}</small>
</template>
</el-table-column>
<el-table-column label="提交时间" width="170">
<template #default="{ row }">{{ formatDateTime(row.createdAt) }}</template>
</el-table-column>
<el-table-column label="状态" width="120">
<template #default="{ row }">
<el-tag :type="statusInfo(row.status).type">
{{ statusInfo(row.status).label }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="150" fixed="right">
<template #default="{ row }">
<template v-if="row.status === 'Submitted'">
<el-button link type="success" @click="approveReservation(row)">批准</el-button>
<el-button link type="danger" @click="rejectReservation(row)">驳回</el-button>
</template>
<span v-else>{{ row.reviewComment || '—' }}</span>
</template>
</el-table-column>
</el-table>
<div class="mobile-records">
<article v-for="item in reviewItems" :key="item.id" class="record-item">
<header>
<div>
<b>{{ item.applicantName }} · {{ item.reservationDate }}</b>
<span>{{ periodText(item) }} · {{ item.buildingName }} {{ item.classroomName }}</span>
</div>
<el-tag :type="statusInfo(item.status).type">
{{ statusInfo(item.status).label }}
</el-tag>
</header>
<p>{{ item.purpose }}</p>
<small>{{ item.attendeeCount }} · {{ item.contactPhone }}</small>
<div v-if="item.status === 'Submitted'" class="review-actions">
<el-button type="success" @click="approveReservation(item)">批准</el-button>
<el-button type="danger" plain @click="rejectReservation(item)">驳回</el-button>
</div>
<blockquote v-else-if="item.reviewComment">{{ item.reviewComment }}</blockquote>
</article>
</div>
<el-empty v-if="reviewItems.length === 0" description="当前筛选下没有申请" />
<el-pagination
v-if="reviewTotal > pageSize"
v-model:current-page="reviewPage"
:page-size="pageSize"
:total="reviewTotal"
layout="prev, pager, next"
@current-change="loadReview"
/>
</div>
</el-tab-pane>
</el-tabs>
</el-card>
</section>
</template>
<style scoped>
.reservation-page {
display: grid;
gap: 20px;
}
.page-heading,
.card-title,
.submit-row,
.review-toolbar,
.record-item header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
}
.page-heading h2,
.card-title h3,
.review-toolbar h3 {
margin: 0;
color: #183b56;
}
.page-heading p,
.card-title p,
.review-toolbar p {
margin: 5px 0 0;
color: #6c7f90;
}
.eyebrow {
color: #17867c !important;
font-size: 12px;
font-weight: 800;
letter-spacing: .16em;
}
.scope-alert {
border: 1px solid #f3d7a5;
}
.application-card,
.records-card {
border: 1px solid #dfe9ec;
border-radius: 12px;
}
.card-title > span {
color: #17867c;
font-weight: 700;
}
.form-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 0 16px;
}
.span-2 {
grid-column: span 2;
}
.room-picker {
display: flex;
width: 100%;
gap: 8px;
}
.room-picker .el-select {
flex: 1;
}
.submit-row {
padding-top: 10px;
border-top: 1px dashed #dfe9ec;
}
.submit-row p,
.submit-row span {
margin: 0;
color: #6c7f90;
font-size: 13px;
}
.submit-row b {
color: #17867c;
}
.desktop-table b,
.desktop-table span,
.desktop-table small {
display: block;
}
.desktop-table small {
margin-top: 4px;
color: #7a8a99;
}
.review-toolbar {
margin-bottom: 16px;
}
.mobile-records {
display: none;
}
.record-item {
align-items: stretch;
flex-direction: column;
padding: 15px;
border: 1px solid #dfe9ec;
border-radius: 10px;
background: #fff;
}
.record-item + .record-item {
margin-top: 10px;
}
.record-item header {
align-items: flex-start;
}
.record-item header b,
.record-item header span,
.record-item small {
display: block;
}
.record-item header span,
.record-item small {
margin-top: 4px;
color: #738596;
}
.record-item p {
margin: 12px 0 2px;
color: #29495f;
}
.record-item blockquote {
margin: 10px 0 0;
padding: 9px 12px;
border-left: 3px solid #e4a74e;
background: #fff9ef;
color: #805d2c;
}
.review-actions {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
margin-top: 12px;
}
.el-pagination {
justify-content: flex-end;
margin-top: 16px;
}
@media (max-width: 1100px) {
.form-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 720px) {
.page-heading,
.card-title,
.submit-row,
.review-toolbar {
align-items: flex-start;
flex-direction: column;
}
.form-grid {
grid-template-columns: 1fr;
}
.span-2 {
grid-column: span 1;
}
.room-picker {
flex-direction: column;
}
.submit-row .el-button,
.review-toolbar .el-select {
width: 100% !important;
}
.desktop-table {
display: none;
}
.mobile-records {
display: block;
}
.el-pagination {
justify-content: center;
}
}
</style>