租房日历模块
This commit is contained in:
parent
f64e449f50
commit
f0848012af
|
|
@ -47,7 +47,8 @@ export const rentalApi = {
|
||||||
getById: (id) => get(`/rentals/${id}`),
|
getById: (id) => get(`/rentals/${id}`),
|
||||||
create: (data) => post('/rentals', data),
|
create: (data) => post('/rentals', data),
|
||||||
update: (id, data) => put(`/rentals/${id}`, data),
|
update: (id, data) => put(`/rentals/${id}`, data),
|
||||||
delete: (id) => del(`/rentals/${id}`)
|
delete: (id) => del(`/rentals/${id}`),
|
||||||
|
getCalendar: (params = {}) => get('/rentals/calendar', params)
|
||||||
};
|
};
|
||||||
|
|
||||||
// 统计分析API
|
// 统计分析API
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,11 @@ const routes = [
|
||||||
name: 'RentalList',
|
name: 'RentalList',
|
||||||
component: () => import('@/views/rental/List.vue')
|
component: () => import('@/views/rental/List.vue')
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/rental/calendar',
|
||||||
|
name: 'RentalCalendar',
|
||||||
|
component: () => import('@/views/rental/Calendar.vue')
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/rental/add',
|
path: '/rental/add',
|
||||||
name: 'RentalAdd',
|
name: 'RentalAdd',
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,344 @@
|
||||||
|
<template>
|
||||||
|
<div class="rental-calendar">
|
||||||
|
<div class="page-header">
|
||||||
|
<h2>租房日历</h2>
|
||||||
|
<div class="legend">
|
||||||
|
<span class="legend-item"><span class="dot dot-active"></span> 在租</span>
|
||||||
|
<span class="legend-item"><span class="dot dot-soon"></span> 即将到期(≤5天)</span>
|
||||||
|
<span class="legend-item"><span class="dot dot-expired"></span> 已到期</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-card class="filter-bar">
|
||||||
|
<el-form :inline="true" size="small">
|
||||||
|
<el-form-item label="公寓">
|
||||||
|
<el-select v-model="filterApartmentId" placeholder="全部公寓" clearable style="width:160px" @change="loadData">
|
||||||
|
<el-option v-for="item in apartments" :key="item.id" :label="item.name" :value="item.id"></el-option>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="租客">
|
||||||
|
<el-input v-model="filterTenantName" placeholder="租客姓名" clearable style="width:160px" @keyup.enter.native="loadData"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="附属状态">
|
||||||
|
<el-select v-model="filterSubStatus" placeholder="全部" clearable style="width:140px" @change="handleFilterChange">
|
||||||
|
<el-option label="正常" value="normal"></el-option>
|
||||||
|
<el-option label="即将到期" value="soon_expire"></el-option>
|
||||||
|
<el-option label="已到期" value="expired"></el-option>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="loadData">查询</el-button>
|
||||||
|
<el-button @click="resetFilters">重置</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<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="cell-day">{{ data.day.split('-').pop() }}</div>
|
||||||
|
<div class="cell-items" v-if="getItemsForDate(data.day).length > 0">
|
||||||
|
<div
|
||||||
|
class="cell-item"
|
||||||
|
v-for="item in getItemsForDate(data.day).slice(0, 3)"
|
||||||
|
: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)">
|
||||||
|
+{{ getItemsForDate(data.day).length - 3 }} 更多
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-calendar>
|
||||||
|
</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>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { rentalApi, apartmentApi } from '@/api/api'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'RentalCalendar',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
currentDate: new Date(),
|
||||||
|
rentals: [],
|
||||||
|
loading: false,
|
||||||
|
selectedDate: null,
|
||||||
|
dayDialogVisible: false,
|
||||||
|
apartments: [],
|
||||||
|
filterApartmentId: null,
|
||||||
|
filterTenantName: '',
|
||||||
|
filterSubStatus: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
subStatusLabels() {
|
||||||
|
return { normal: '正常', soon_expire: '即将到期', expired: '已到期' }
|
||||||
|
},
|
||||||
|
allRentalMap() {
|
||||||
|
const map = {}
|
||||||
|
this.rentals.forEach(item => {
|
||||||
|
const subStatus = item.subStatus
|
||||||
|
const date = item.endDate
|
||||||
|
if (!map[date]) {
|
||||||
|
map[date] = []
|
||||||
|
}
|
||||||
|
map[date].push({
|
||||||
|
id: item.id,
|
||||||
|
roomId: item.roomId,
|
||||||
|
tenantName: item.tenantName,
|
||||||
|
roomNumber: item.roomNumber,
|
||||||
|
apartmentName: item.apartmentName,
|
||||||
|
endDate: item.endDate,
|
||||||
|
status: item.status,
|
||||||
|
subStatus,
|
||||||
|
rent: item.rent,
|
||||||
|
deposit: item.deposit,
|
||||||
|
statusClass: this.subStatusToClass(subStatus)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return map
|
||||||
|
},
|
||||||
|
rentalMap() {
|
||||||
|
if (!this.filterSubStatus) return this.allRentalMap
|
||||||
|
const filtered = {}
|
||||||
|
for (const [date, items] of Object.entries(this.allRentalMap)) {
|
||||||
|
const matched = items.filter(i => i.subStatus === this.filterSubStatus)
|
||||||
|
if (matched.length > 0) {
|
||||||
|
filtered[date] = matched
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return filtered
|
||||||
|
},
|
||||||
|
selectedDayItems() {
|
||||||
|
if (!this.selectedDate) return []
|
||||||
|
return this.getItemsForDate(this.selectedDate)
|
||||||
|
},
|
||||||
|
formatSelectedDate() {
|
||||||
|
if (!this.selectedDate) return ''
|
||||||
|
const parts = this.selectedDate.split('-')
|
||||||
|
return `${parts[0]}年${parseInt(parts[1])}月${parseInt(parts[2])}日`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.loadApartments()
|
||||||
|
this.loadData()
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
currentDate(newVal, oldVal) {
|
||||||
|
if (!oldVal || newVal.getMonth() !== oldVal.getMonth() || newVal.getFullYear() !== oldVal.getFullYear()) {
|
||||||
|
this.selectedDate = null
|
||||||
|
this.dayDialogVisible = false
|
||||||
|
this.loadData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async loadApartments() {
|
||||||
|
try {
|
||||||
|
const res = await apartmentApi.list()
|
||||||
|
this.apartments = res.data || res || []
|
||||||
|
} catch (e) {
|
||||||
|
this.apartments = []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async loadData() {
|
||||||
|
this.loading = true
|
||||||
|
try {
|
||||||
|
const year = this.currentDate.getFullYear()
|
||||||
|
const month = this.currentDate.getMonth() + 1
|
||||||
|
const params = { year, month }
|
||||||
|
if (this.filterApartmentId) params.apartmentId = this.filterApartmentId
|
||||||
|
if (this.filterTenantName) params.tenantName = this.filterTenantName
|
||||||
|
const response = await rentalApi.getCalendar(params)
|
||||||
|
this.rentals = response.data || response || []
|
||||||
|
} catch (error) {
|
||||||
|
this.$message.error('加载日历数据失败')
|
||||||
|
this.rentals = []
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleFilterChange() {
|
||||||
|
this.selectedDate = null
|
||||||
|
this.dayDialogVisible = false
|
||||||
|
},
|
||||||
|
resetFilters() {
|
||||||
|
this.filterApartmentId = null
|
||||||
|
this.filterTenantName = ''
|
||||||
|
this.filterSubStatus = ''
|
||||||
|
this.selectedDate = null
|
||||||
|
this.dayDialogVisible = false
|
||||||
|
},
|
||||||
|
showDayDetail(dateStr) {
|
||||||
|
this.selectedDate = dateStr
|
||||||
|
this.dayDialogVisible = true
|
||||||
|
},
|
||||||
|
getItemsForDate(dateStr) {
|
||||||
|
return this.rentalMap[dateStr] || []
|
||||||
|
},
|
||||||
|
subStatusToClass(subStatus) {
|
||||||
|
const map = { normal: 'status-active', soon_expire: 'status-soon', expired: 'status-expired' }
|
||||||
|
return map[subStatus] || ''
|
||||||
|
},
|
||||||
|
goToDetail(roomId) {
|
||||||
|
this.$router.push(`/rental/detail/${roomId}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.rental-calendar {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 20px;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot {
|
||||||
|
display: inline-block;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot-active {
|
||||||
|
background-color: #67c23a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot-soon {
|
||||||
|
background-color: #e6a23c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot-expired {
|
||||||
|
background-color: #f56c6c;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .el-calendar-day {
|
||||||
|
height: auto;
|
||||||
|
min-height: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-cell {
|
||||||
|
min-height: 100px;
|
||||||
|
padding: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell-day {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #909399;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-current-month .cell-day {
|
||||||
|
color: #303133;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell-items {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell-item {
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 1px 4px;
|
||||||
|
border-radius: 3px;
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
line-height: 1.6;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell-item:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell-item.status-active {
|
||||||
|
background-color: #f0f9eb;
|
||||||
|
color: #67c23a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell-item.status-soon {
|
||||||
|
background-color: #fdf6ec;
|
||||||
|
color: #e6a23c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell-item.status-expired {
|
||||||
|
background-color: #fef0f0;
|
||||||
|
color: #f56c6c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell-more {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #409eff;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 1px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell-more:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
@ -786,7 +786,11 @@ export default {
|
||||||
}).catch(() => {})
|
}).catch(() => {})
|
||||||
},
|
},
|
||||||
goBack() {
|
goBack() {
|
||||||
|
if (this.returnQuery && Object.keys(this.returnQuery).length > 0) {
|
||||||
this.$router.push({ path: '/rental/list', query: this.returnQuery })
|
this.$router.push({ path: '/rental/list', query: this.returnQuery })
|
||||||
|
} else {
|
||||||
|
this.$router.go(-1)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
getDefaultTenantName() {
|
getDefaultTenantName() {
|
||||||
const userInfo = getUserInfo()
|
const userInfo = getUserInfo()
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue