通知统计

This commit is contained in:
wangxiaoxian 2026-03-09 21:28:08 +08:00
parent 75bc71b532
commit 4576eef238
6 changed files with 390 additions and 36 deletions

View File

@ -14,6 +14,27 @@
<i class="el-icon-s-fold" style="font-size: 24px; color: white;"></i> <i class="el-icon-s-fold" style="font-size: 24px; color: white;"></i>
</el-button> </el-button>
<h1 class="app-title">租房管理系统</h1> <h1 class="app-title">租房管理系统</h1>
<!-- 通知模块 -->
<el-dropdown trigger="click" @command="handleNotificationCommand" class="notification-dropdown">
<div class="notification-icon">
<i class="el-icon-bell" style="font-size: 20px; color: white;"></i>
<span v-if="notificationCount > 0" class="notification-badge">{{ notificationCount }}</span>
</div>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item command="viewAll">
查看所有通知
<span v-if="notificationCount > 0" class="notification-badge-small">{{ notificationCount }}</span>
</el-dropdown-item>
<el-dropdown-item divided command="expired">
已到期房间
<span v-if="expiredCount > 0" class="notification-badge-small">{{ expiredCount }}</span>
</el-dropdown-item>
<el-dropdown-item command="soonExpire">
即将到期房间
<span v-if="soonExpireCount > 0" class="notification-badge-small">{{ soonExpireCount }}</span>
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div> </div>
<div class="header-right"> <div class="header-right">
<el-dropdown @command="handleUserCommand"> <el-dropdown @command="handleUserCommand">
@ -129,6 +150,7 @@
<script> <script>
import { getUserInfo, getUserMenus, clearAuth } from '@/utils/auth' import { getUserInfo, getUserMenus, clearAuth } from '@/utils/auth'
import { statisticsApi } from '@/api/api'
export default { export default {
name: 'MainLayout', name: 'MainLayout',
@ -139,7 +161,11 @@ export default {
drawerVisible: false, drawerVisible: false,
drawerWidth: '70%', drawerWidth: '70%',
username: '', username: '',
menuList: [] menuList: [],
//
expiredCount: 0,
soonExpireCount: 0,
notificationShown: false
} }
}, },
mounted() { mounted() {
@ -148,6 +174,8 @@ export default {
this.loadUserInfo() this.loadUserInfo()
this.loadMenus() this.loadMenus()
this.setActiveMenu() this.setActiveMenu()
//
this.getNotificationData()
}, },
beforeDestroy() { beforeDestroy() {
window.removeEventListener('resize', this.checkDevice) window.removeEventListener('resize', this.checkDevice)
@ -155,6 +183,12 @@ export default {
watch: { watch: {
'$route': 'setActiveMenu' '$route': 'setActiveMenu'
}, },
computed: {
//
notificationCount() {
return this.expiredCount + this.soonExpireCount
}
},
methods: { methods: {
checkDevice() { checkDevice() {
const width = window.innerWidth const width = window.innerWidth
@ -258,6 +292,81 @@ export default {
clearAuth() clearAuth()
this.$message.success('退出成功') this.$message.success('退出成功')
this.$router.push('/login') this.$router.push('/login')
},
//
async getNotificationData() {
try {
// APIDashboard
const response = await statisticsApi.getDashboardStats()
const data = response.data || response
//
this.expiredCount = data.expiredRoomCount || 0
this.soonExpireCount = data.soonExpireRoomCount || 0
//
if (!this.notificationShown && (this.expiredCount > 0 || this.soonExpireCount > 0)) {
this.showNotification()
this.notificationShown = true
}
} catch (error) {
console.error('获取通知数据失败:', error)
}
},
//
showNotification() {
//
if (this.expiredCount > 0) {
this.$notify({
title: '已到期提醒',
message: `${this.expiredCount} 个房间已到期`,
type: 'error',
position: 'top-right',
offset: 0,
onClick: () => this.handleNotificationClick('expired')
})
}
if (this.soonExpireCount > 0) {
this.$notify({
title: '即将到期提醒',
message: `${this.soonExpireCount} 个房间即将到期`,
type: 'warning',
position: 'top-right',
offset: this.expiredCount > 0 ? 80 : 0,
onClick: () => this.handleNotificationClick('soonExpire')
})
}
},
//
handleNotificationClick(type) {
//
const timestamp = Date.now()
switch (type) {
case 'expired':
this.$router.push(`/rental/list?subStatus=expired&t=${timestamp}`)
break
case 'soonExpire':
this.$router.push(`/rental/list?subStatus=soon_expire&t=${timestamp}`)
break
}
},
//
handleNotificationCommand(command) {
switch (command) {
case 'viewAll':
//
this.$router.push('/rental/list')
break
case 'expired':
//
this.$router.push('/rental/list?subStatus=expired')
break
case 'soonExpire':
//
this.$router.push('/rental/list?subStatus=soon_expire')
break
}
} }
} }
} }
@ -342,6 +451,53 @@ export default {
font-size: 18px; font-size: 18px;
} }
/* 通知模块样式 */
.notification-dropdown {
margin-left: 20px;
}
.notification-icon {
position: relative;
cursor: pointer;
padding: 8px;
border-radius: 50%;
transition: all 0.3s ease;
}
.notification-icon:hover {
background-color: rgba(255, 255, 255, 0.1);
}
.notification-badge {
position: absolute;
top: 0;
right: 0;
background-color: #f56c6c;
color: white;
font-size: 12px;
font-weight: bold;
min-width: 18px;
height: 18px;
line-height: 18px;
text-align: center;
border-radius: 9px;
padding: 0 4px;
}
.notification-badge-small {
background-color: #f56c6c;
color: white;
font-size: 12px;
font-weight: bold;
min-width: 16px;
height: 16px;
line-height: 16px;
text-align: center;
border-radius: 8px;
padding: 0 4px;
margin-left: 8px;
}
/* 侧边栏样式 */ /* 侧边栏样式 */
.app-aside { .app-aside {
background-color: #f0f2f5; background-color: #f0f2f5;

View File

@ -123,14 +123,46 @@
class="stats-table" class="stats-table"
> >
<el-table-column prop="apartment" label="公寓" min-width="100"></el-table-column> <el-table-column prop="apartment" label="公寓" min-width="100"></el-table-column>
<el-table-column prop="empty" label="空房" width="70"></el-table-column> <el-table-column prop="empty" label="空房" width="70">
<el-table-column prop="reserved" label="预订" width="70"></el-table-column> <template slot-scope="scope">
<el-table-column prop="rented" label="在租" width="70"></el-table-column> <span class="clickable-cell" @click="navigateToRentalList(scope.row.apartmentId, 'status', 'empty')">{{ scope.row.empty }}</span>
<el-table-column prop="soon_expire" label="即将到期" width="90"></el-table-column> </template>
<el-table-column prop="expired" label="已到期" width="70"></el-table-column> </el-table-column>
<el-table-column prop="cleaning" label="打扫中" width="70"></el-table-column> <el-table-column prop="reserved" label="预订" width="70">
<el-table-column prop="maintenance" label="维修中" width="70"></el-table-column> <template slot-scope="scope">
<el-table-column prop="total" label="总数" width="70"></el-table-column> <span class="clickable-cell" @click="navigateToRentalList(scope.row.apartmentId, 'status', 'reserved')">{{ scope.row.reserved }}</span>
</template>
</el-table-column>
<el-table-column prop="rented" label="在租" width="70">
<template slot-scope="scope">
<span class="clickable-cell" @click="navigateToRentalList(scope.row.apartmentId, 'status', 'rented')">{{ scope.row.rented }}</span>
</template>
</el-table-column>
<el-table-column prop="soon_expire" label="即将到期" width="90">
<template slot-scope="scope">
<span class="clickable-cell" @click="navigateToRentalList(scope.row.apartmentId, 'subStatus', 'soon_expire')">{{ scope.row.soon_expire }}</span>
</template>
</el-table-column>
<el-table-column prop="expired" label="已到期" width="70">
<template slot-scope="scope">
<span class="clickable-cell" @click="navigateToRentalList(scope.row.apartmentId, 'subStatus', 'expired')">{{ scope.row.expired }}</span>
</template>
</el-table-column>
<el-table-column prop="cleaning" label="打扫中" width="70">
<template slot-scope="scope">
<span class="clickable-cell" @click="navigateToRentalList(scope.row.apartmentId, 'otherStatus', 'cleaning')">{{ scope.row.cleaning }}</span>
</template>
</el-table-column>
<el-table-column prop="maintenance" label="维修中" width="70">
<template slot-scope="scope">
<span class="clickable-cell" @click="navigateToRentalList(scope.row.apartmentId, 'otherStatus', 'maintenance')">{{ scope.row.maintenance }}</span>
</template>
</el-table-column>
<el-table-column prop="total" label="总数" width="70">
<template slot-scope="scope">
<span class="clickable-cell" @click="navigateToRentalList(scope.row.apartmentId)">{{ scope.row.total }}</span>
</template>
</el-table-column>
</el-table> </el-table>
</div> </div>
</el-card> </el-card>
@ -215,6 +247,17 @@ export default {
} }
}); });
return sums; return sums;
},
navigateToRentalList(apartmentId, paramType, paramValue) {
const timestamp = Date.now()
let query = `t=${timestamp}`
if (apartmentId) {
query += `&apartmentId=${apartmentId}`
}
if (paramType && paramValue) {
query += `&${paramType}=${paramValue}`
}
this.$router.push(`/rental/list?${query}`)
} }
} }
} }
@ -291,6 +334,17 @@ export default {
min-width: 800px; min-width: 800px;
} }
.clickable-cell {
cursor: pointer;
color: #409EFF;
transition: color 0.3s;
}
.clickable-cell:hover {
color: #66b1ff;
text-decoration: underline;
}
/* 移动端适配 */ /* 移动端适配 */
@media screen and (max-width: 768px) { @media screen and (max-width: 768px) {
.welcome-card { .welcome-card {

View File

@ -55,6 +55,13 @@
<el-table-column prop="endDate" label="结束日期" min-width="100"></el-table-column> <el-table-column prop="endDate" label="结束日期" min-width="100"></el-table-column>
<el-table-column prop="rent" label="租金(元)" min-width="100"></el-table-column> <el-table-column prop="rent" label="租金(元)" min-width="100"></el-table-column>
<el-table-column prop="deposit" label="押金(元)" min-width="90"></el-table-column> <el-table-column prop="deposit" label="押金(元)" min-width="90"></el-table-column>
<el-table-column prop="refundedDeposit" label="已退押金(元)" min-width="100">
<template slot-scope="scope">
<span :class="{ 'text-success': scope.row.refundedDeposit > 0 }">
{{ scope.row.refundedDeposit || 0 }}
</span>
</template>
</el-table-column>
<el-table-column prop="remark" label="备注" min-width="120"></el-table-column> <el-table-column prop="remark" label="备注" min-width="120"></el-table-column>
<el-table-column prop="status" label="状态" min-width="80"> <el-table-column prop="status" label="状态" min-width="80">
<template slot-scope="scope"> <template slot-scope="scope">
@ -181,6 +188,9 @@
<el-form-item label="押金(元)" prop="deposit"> <el-form-item label="押金(元)" prop="deposit">
<el-input v-model.number="rentalForm.deposit" placeholder="请输入押金"></el-input> <el-input v-model.number="rentalForm.deposit" placeholder="请输入押金"></el-input>
</el-form-item> </el-form-item>
<el-form-item label="已退押金(元)" prop="refundedDeposit">
<el-input v-model.number="rentalForm.refundedDeposit" placeholder="请输入已退押金"></el-input>
</el-form-item>
<el-form-item label="状态" prop="status"> <el-form-item label="状态" prop="status">
<el-select v-model="rentalForm.status" placeholder="请选择状态" style="width: 100%"> <el-select v-model="rentalForm.status" placeholder="请选择状态" style="width: 100%">
<el-option label="有效" value="active"></el-option> <el-option label="有效" value="active"></el-option>
@ -258,6 +268,7 @@ export default {
endDate: '', endDate: '',
rent: '', rent: '',
deposit: '', deposit: '',
refundedDeposit: '',
status: 'active', status: 'active',
remark: '' remark: ''
}, },
@ -328,9 +339,8 @@ export default {
const roomResponse = await roomApi.getById(roomId) const roomResponse = await roomApi.getById(roomId)
this.room = roomResponse this.room = roomResponse
await this.loadRentalHistory() await this.loadRentalHistory()
if (this.activeTab === 'water') { // tab便退
await this.loadWaterBills() await this.loadWaterBills()
}
} catch (error) { } catch (error) {
this.$message.error('加载数据失败') this.$message.error('加载数据失败')
} finally { } finally {
@ -395,10 +405,6 @@ export default {
case 'empty': return '' case 'empty': return ''
case 'reserved': return 'warning' case 'reserved': return 'warning'
case 'rented': return 'success' case 'rented': return 'success'
case 'soon_expire': return 'warning'
case 'expired': return 'danger'
case 'cleaning': return 'info'
case 'maintenance': return 'danger'
default: return '' default: return ''
} }
}, },
@ -407,10 +413,6 @@ export default {
case 'empty': return '空房' case 'empty': return '空房'
case 'reserved': return '预订' case 'reserved': return '预订'
case 'rented': return '在租' case 'rented': return '在租'
case 'soon_expire': return '即将到期'
case 'expired': return '到期'
case 'cleaning': return '打扫中'
case 'maintenance': return '维修中'
default: return status default: return status
} }
}, },
@ -498,28 +500,66 @@ export default {
}).catch(() => {}) }).catch(() => {})
}, },
async handleCheckout() { async handleCheckout() {
console.log('开始退房操作')
if (!this.room.id) { if (!this.room.id) {
this.$message.error('房间信息加载失败,无法进行退房操作') this.$message.error('房间信息加载失败,无法进行退房操作')
return return
} }
this.$confirm('确定要退房吗?', '提示', { const roomId = this.$route.params.id
console.log('房间ID:', roomId)
const rental = this.rentalHistory.find(r => r.roomId == roomId && r.status === 'active')
console.log('找到的租房记录:', rental)
if (!rental) {
this.$message.error('未找到活跃的租房记录')
return
}
//
let depositMessage = ''
console.log('押金金额:', rental.deposit)
console.log('已退押金:', rental.refundedDeposit)
const deposit = parseFloat(rental.deposit) || 0
const refundedDeposit = parseFloat(rental.refundedDeposit) || 0
console.log('处理后的押金:', deposit)
console.log('处理后的已退押金:', refundedDeposit)
if (deposit > 0 && refundedDeposit === 0) {
depositMessage = `押金 ${deposit} 元尚未退还,`
console.log('押金提示信息:', depositMessage)
}
//
let waterMessage = ''
console.log('水费列表:', this.waterBills)
const pendingWaterBills = (this.waterBills || []).filter(bill =>
(bill.status === 'unbilled' || bill.status === 'unpaid')
)
console.log('未处理水费:', pendingWaterBills)
if (pendingWaterBills.length > 0) {
const totalWaterAmount = pendingWaterBills.reduce((sum, bill) => sum + (bill.amount || 0), 0)
waterMessage = `存在 ${pendingWaterBills.length} 笔未处理水费,总金额 ${totalWaterAmount} 元,`
console.log('水费提示信息:', waterMessage)
}
//
let confirmMessage = '确定要退房吗?'
if (depositMessage || waterMessage) {
confirmMessage = `注意:${depositMessage}${waterMessage}确定要退房吗?`
}
console.log('最终确认信息:', confirmMessage)
this.$confirm(confirmMessage, '提示', {
confirmButtonText: '确定', confirmButtonText: '确定',
cancelButtonText: '取消', cancelButtonText: '取消',
type: 'warning' type: 'warning'
}).then(async () => { }).then(async () => {
try { try {
const roomId = this.$route.params.id
const rental = this.rentalHistory.find(r => r.roomId == roomId && r.status === 'active')
if (rental) {
await rentalApi.update(rental.id, { status: 'expired' }) await rentalApi.update(rental.id, { status: 'expired' })
await roomApi.update(roomId, { status: 'empty' }) await roomApi.update(roomId, { status: 'empty', subStatus: 'normal' })
this.$message.success('退房成功') this.$message.success('退房成功')
this.loadData() this.loadData()
} else {
this.$message.error('未找到活跃的租房记录')
}
} catch (error) { } catch (error) {
console.error('退房失败:', error) console.error('退房失败:', error)
this.$message.error('退房失败:' + (error.message || '未知错误')) this.$message.error('退房失败:' + (error.message || '未知错误'))
@ -679,8 +719,42 @@ export default {
this.$message.success('租赁记录添加成功') this.$message.success('租赁记录添加成功')
if (this.renewingRentalId) { if (this.renewingRentalId) {
//
try {
const response = await rentalApi.getById(this.renewingRentalId)
const oldRental = response
if (oldRental) {
console.log('原租赁记录:', oldRental)
const deposit = parseFloat(oldRental.deposit) || 0
if (deposit > 0) {
// 0
const newRemark = (oldRental.remark ? oldRental.remark + '' : '') + `押金${deposit}元已转移至下一租赁周期`
console.log('新备注:', newRemark)
await rentalApi.update(this.renewingRentalId, {
status: 'expired',
deposit: 0,
refundedDeposit: 0,
remark: newRemark
})
this.$message.success('原租赁记录状态已更新为已到期,押金已转移')
} else {
// 0
await rentalApi.update(this.renewingRentalId, { status: 'expired' }) await rentalApi.update(this.renewingRentalId, { status: 'expired' })
this.$message.success('原租赁记录状态已更新为已到期') this.$message.success('原租赁记录状态已更新为已到期')
}
} else {
//
await rentalApi.update(this.renewingRentalId, { status: 'expired' })
this.$message.success('原租赁记录状态已更新为已到期')
}
} catch (error) {
console.error('获取原租赁记录失败:', error)
// 使
await rentalApi.update(this.renewingRentalId, { status: 'expired' })
this.$message.success('原租赁记录状态已更新为已到期')
}
this.renewingRentalId = null this.renewingRentalId = null
} }
} }
@ -811,6 +885,11 @@ export default {
font-weight: bold; font-weight: bold;
} }
.text-success {
color: #67c23a;
font-weight: bold;
}
/* 移动端适配 */ /* 移动端适配 */
@media screen and (max-width: 768px) { @media screen and (max-width: 768px) {
.card-header { .card-header {

View File

@ -127,7 +127,16 @@ export default {
}, },
computed: { computed: {
paginationLayout() { paginationLayout() {
return this.isMobile ? 'prev, pager, next' : 'total, sizes, prev, pager, next, jumper' return this.isMobile ? 'prev, pager, next' : 'total, sizes, prev, pager, next'
}
},
watch: {
'$route': {
handler() {
this.initFromQuery()
this.loadData()
},
deep: true
} }
}, },
mounted() { mounted() {

View File

@ -14,7 +14,7 @@
<el-table-column prop="status" label="状态" min-width="100"></el-table-column> <el-table-column prop="status" label="状态" min-width="100"></el-table-column>
<el-table-column prop="count" label="数量" min-width="80"> <el-table-column prop="count" label="数量" min-width="80">
<template slot-scope="scope"> <template slot-scope="scope">
<span class="count-value">{{ scope.row.count }}</span> <span class="count-value clickable-cell" @click="handleCountClick(scope.row)">{{ scope.row.count }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="percentage" label="占比" min-width="100"> <el-table-column prop="percentage" label="占比" min-width="100">
@ -66,6 +66,34 @@ export default {
} catch (error) { } catch (error) {
this.$message.error('加载房间状态统计数据失败') this.$message.error('加载房间状态统计数据失败')
} }
},
handleCountClick(row) {
const timestamp = Date.now()
let query = `t=${timestamp}`
//
const statusMap = {
'空房': 'empty',
'预订': 'reserved',
'在租': 'rented',
'即将到期': 'soon_expire',
'已到期': 'expired',
'打扫中': 'cleaning',
'维修中': 'maintenance'
}
const statusKey = statusMap[row.status]
if (statusKey) {
if (['empty', 'reserved', 'rented'].includes(statusKey)) {
query += `&status=${statusKey}`
} else if (['soon_expire', 'expired'].includes(statusKey)) {
query += `&subStatus=${statusKey}`
} else {
query += `&otherStatus=${statusKey}`
}
}
this.$router.push(`/rental/list?${query}`)
} }
} }
} }
@ -101,6 +129,16 @@ export default {
color: #409EFF; color: #409EFF;
} }
.clickable-cell {
cursor: pointer;
transition: color 0.3s;
}
.clickable-cell:hover {
color: #66b1ff;
text-decoration: underline;
}
.total-count { .total-count {
margin-top: 20px; margin-top: 20px;
text-align: right; text-align: right;

View File

@ -17,10 +17,22 @@
<span class="amount-value">¥{{ scope.row.amount }}</span> <span class="amount-value">¥{{ scope.row.amount }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="depositReceived" label="已收押金(元)" min-width="150">
<template slot-scope="scope">
<span class="amount-value">¥{{ scope.row.depositReceived || 0 }}</span>
</template>
</el-table-column>
<el-table-column prop="depositRefunded" label="已退押金(元)" min-width="150">
<template slot-scope="scope">
<span class="amount-value">¥{{ scope.row.depositRefunded || 0 }}</span>
</template>
</el-table-column>
</el-table> </el-table>
</div> </div>
<div class="total-amount"> <div class="total-amount">
<el-tag size="medium" type="primary">总租金收入{{ totalAmount }} </el-tag> <el-tag size="medium" type="primary">总租金收入{{ totalAmount }} </el-tag>
<el-tag size="medium" type="success" style="margin-left: 10px;">已收押金{{ totalDepositReceived }} </el-tag>
<el-tag size="medium" type="info" style="margin-left: 10px;">已退押金{{ totalDepositRefunded }} </el-tag>
</div> </div>
</el-card> </el-card>
</div> </div>
@ -39,6 +51,12 @@ export default {
computed: { computed: {
totalAmount() { totalAmount() {
return this.rentData.reduce((sum, item) => sum + item.amount, 0) return this.rentData.reduce((sum, item) => sum + item.amount, 0)
},
totalDepositReceived() {
return this.rentData.reduce((sum, item) => sum + (item.depositReceived || 0), 0)
},
totalDepositRefunded() {
return this.rentData.reduce((sum, item) => sum + (item.depositRefunded || 0), 0)
} }
}, },
mounted() { mounted() {