租房日历模块

This commit is contained in:
xiaoxian 2026-05-23 23:48:20 +08:00
parent 74ec7491f6
commit 18a7febec9
1 changed files with 185 additions and 31 deletions

View File

@ -36,7 +36,7 @@
<el-card v-loading="loading" style="margin-top:20px">
<el-calendar v-model="currentDate">
<template slot="dateCell" slot-scope="{ date, data }">
<div class="calendar-cell" :class="{ 'is-current-month': data.type === 'current-month' }">
<div class="calendar-cell" :class="{ 'is-current-month': data.type === 'current-month' }" @click="data.type === 'current-month' && showDayDetail(data.day)">
<div class="cell-day">{{ data.day.split('-').pop() }}</div>
<div class="cell-items" v-if="getItemsForDate(data.day).length > 0">
<div
@ -45,11 +45,10 @@
:key="item.id"
:class="item.statusClass"
:title="item.tenantName + '(' + item.apartmentName + '-' + item.roomNumber + ')'"
@click="goToDetail(item.roomId)"
>
{{ item.tenantName }}({{ item.apartmentName }}-{{ item.roomNumber }})
</div>
<div class="cell-more" v-if="getItemsForDate(data.day).length > 3" @click="showDayDetail(data.day)">
<div class="cell-more" v-if="getItemsForDate(data.day).length > 3">
+{{ getItemsForDate(data.day).length - 3 }} 更多
</div>
</div>
@ -59,29 +58,29 @@
</el-card>
<el-dialog :visible.sync="dayDialogVisible" :title="formatSelectedDate + ' —— ' + selectedDayItems.length + ' 条)'" width="70%" @close="selectedDate = null">
<el-table :data="selectedDayItems" stripe style="width: 100%">
<el-table-column prop="apartmentName" label="公寓名称" min-width="120"></el-table-column>
<el-table-column prop="roomNumber" label="房间号" width="100"></el-table-column>
<el-table-column prop="tenantName" label="租客姓名" min-width="120"></el-table-column>
<el-table-column prop="rent" label="租金" width="120">
<template slot-scope="scope">¥{{ scope.row.rent }}</template>
</el-table-column>
<el-table-column prop="deposit" label="押金" width="120">
<template slot-scope="scope">¥{{ scope.row.deposit }}</template>
</el-table-column>
<el-table-column label="状态" width="120">
<template slot-scope="scope">
<el-tag :type="scope.row.subStatus === 'expired' ? 'danger' : scope.row.subStatus === 'soon_expire' ? 'warning' : 'success'" size="small">
{{ subStatusLabels[scope.row.subStatus] }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="80">
<template slot-scope="scope">
<el-button type="text" size="small" @click="goToDetail(scope.row.roomId)">查看</el-button>
</template>
</el-table-column>
</el-table>
<div class="day-card-list">
<el-card v-for="item in selectedDayItems" :key="item.id" class="day-card" :class="item.statusClass" shadow="hover" @click.native="goToDetail(item.roomId)">
<div class="card-accent"></div>
<div class="card-inner">
<div class="card-apartment">{{ item.apartmentName }}<span class="card-room"> {{ item.roomNumber }}</span></div>
<div class="card-top">
<div class="card-tenant">{{ item.tenantName }}</div>
<el-tag :type="item.subStatus === 'expired' ? 'danger' : item.subStatus === 'soon_expire' ? 'warning' : 'success'" size="small">
{{ subStatusLabels[item.subStatus] }}
</el-tag>
</div>
<div class="card-dates">
<span class="date-range">{{ item.startDate }} {{ item.endDate }}</span>
</div>
<div class="card-amounts">
<span class="amount-item">¥{{ item.rent }}/</span>
<span class="amount-divider"></span>
<span class="amount-item">押金 ¥{{ item.deposit }}</span>
</div>
</div>
</el-card>
<el-empty v-if="selectedDayItems.length === 0" description="暂无数据"></el-empty>
</div>
</el-dialog>
</div>
</template>
@ -101,7 +100,8 @@ export default {
apartments: [],
filterApartmentId: null,
filterTenantName: '',
filterSubStatus: ''
filterSubStatus: '',
_afterLoadCallback: null
}
},
computed: {
@ -122,6 +122,7 @@ export default {
tenantName: item.tenantName,
roomNumber: item.roomNumber,
apartmentName: item.apartmentName,
startDate: item.startDate,
endDate: item.endDate,
status: item.status,
subStatus,
@ -156,12 +157,20 @@ export default {
mounted() {
this.loadApartments()
this.loadData()
this.$nextTick(() => {
this._setupTodayListener()
})
},
beforeDestroy() {
this._cleanupTodayListener()
},
watch: {
currentDate(newVal, oldVal) {
if (!oldVal || newVal.getMonth() !== oldVal.getMonth() || newVal.getFullYear() !== oldVal.getFullYear()) {
this.selectedDate = null
this.dayDialogVisible = false
if (!this._afterLoadCallback) {
this.selectedDate = null
this.dayDialogVisible = false
}
this.loadData()
}
}
@ -190,6 +199,10 @@ export default {
this.rentals = []
} finally {
this.loading = false
if (this._afterLoadCallback) {
this._afterLoadCallback()
this._afterLoadCallback = null
}
}
},
handleFilterChange() {
@ -216,6 +229,29 @@ export default {
},
goToDetail(roomId) {
this.$router.push(`/rental/detail/${roomId}`)
},
_setupTodayListener() {
const btn = this.$el.querySelector('.el-calendar__button-group .el-button:nth-child(2)')
if (!btn) return
this._todayBtnHandler = () => {
const today = new Date()
const ds = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`
const cur = this.currentDate
if (cur.getMonth() === today.getMonth() && cur.getFullYear() === today.getFullYear()) {
this.showDayDetail(ds)
} else {
this._afterLoadCallback = () => {
this.selectedDate = ds
this.dayDialogVisible = true
}
}
}
btn.addEventListener('click', this._todayBtnHandler)
},
_cleanupTodayListener() {
if (!this._todayBtnHandler) return
const btn = this.$el && this.$el.querySelector('.el-calendar__button-group .el-button:nth-child(2)')
if (btn) btn.removeEventListener('click', this._todayBtnHandler)
}
}
}
@ -388,7 +424,7 @@ export default {
}
::v-deep .el-calendar-table td.is-today {
background: #f0f5ff;
background: #c7d2fe !important;
}
::v-deep .el-calendar-table td.is-today .calendar-cell .cell-day {
@ -397,6 +433,7 @@ export default {
-webkit-text-fill-color: transparent;
background-clip: text;
font-weight: 700;
transform: scale(1.1);
}
::v-deep .el-calendar-table td.is-selected {
@ -458,7 +495,7 @@ export default {
}
.cell-item {
font-size: 11px;
font-size: 13px;
padding: 2px 6px;
border-radius: 4px;
cursor: pointer;
@ -570,4 +607,121 @@ export default {
::v-deep .el-dialog__body {
padding: 20px 24px;
}
.day-card-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 16px;
}
.day-card {
cursor: pointer;
position: relative;
overflow: hidden;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.day-card:hover {
transform: translateY(-3px);
}
.day-card ::v-deep .el-card__body {
padding: 0;
display: flex;
}
.card-accent {
width: 4px;
flex-shrink: 0;
}
.day-card.status-active .card-accent {
background: #52c41a;
}
.day-card.status-soon .card-accent {
background: #faad14;
}
.day-card.status-expired .card-accent {
background: #ff4d4f;
}
.day-card.status-active {
background: linear-gradient(135deg, #f0f9eb 0%, #e8f5e0 100%);
}
.day-card.status-soon {
background: linear-gradient(135deg, #fff7e6 0%, #fff1cc 100%);
}
.day-card.status-expired {
background: linear-gradient(135deg, #fff1f0 0%, #ffe3e0 100%);
}
.card-inner {
flex: 1;
padding: 16px 20px;
display: flex;
flex-direction: column;
gap: 6px;
}
.card-apartment {
font-size: 16px;
font-weight: 600;
color: #1a1a2e;
line-height: 1.4;
}
.card-room {
font-size: 13px;
font-weight: 400;
color: #909399;
margin-left: 6px;
}
.card-top {
display: flex;
justify-content: space-between;
align-items: center;
gap: 8px;
}
.card-tenant {
font-size: 15px;
font-weight: 500;
color: #303133;
line-height: 1.5;
}
.card-dates {
margin-top: 2px;
}
.date-range {
font-size: 13px;
color: #667eea;
font-weight: 500;
}
.card-amounts {
display: flex;
align-items: center;
gap: 10px;
margin-top: 2px;
padding-top: 8px;
border-top: 1px solid rgba(0, 0, 0, 0.05);
}
.amount-item {
font-size: 12px;
color: #bfc4cc;
}
.amount-divider {
width: 1px;
height: 12px;
background: #e0e0e0;
}
</style>