This commit is contained in:
wangxiaoxian 2026-05-09 17:02:01 +08:00
parent 47d263d914
commit fa56423073
8 changed files with 179 additions and 1551 deletions

View File

@ -8,7 +8,7 @@
</div>
<div class="quick-actions">
<el-button type="primary" icon="el-icon-plus" @click="$router.push('/room/add')">添加房间</el-button>
<el-button icon="el-icon-document" @click="$router.push('/bill')">账单管理</el-button>
<el-button icon="el-icon-document" @click="$router.push('/bill/add')">添加账单</el-button>
</div>
</div>
@ -225,10 +225,10 @@
</el-button>
</div>
<div class="table-wrapper">
<el-table
:data="apartmentHouseStats"
style="width: 100%"
show-summary
<el-table
:data="apartmentHouseStats"
style="width: 100%"
show-summary
:summary-method="getSummary"
class="stats-table"
stripe
@ -241,8 +241,8 @@
</el-table-column>
<el-table-column prop="empty" label="空房" width="80" align="center">
<template slot-scope="scope">
<el-tag
:type="scope.row.empty > 0 ? 'info' : 'success'"
<el-tag
:type="scope.row.empty > 0 ? 'info' : 'success'"
size="small"
class="clickable-tag"
@click="navigateToRentalList(scope.row.apartmentId, 'status', 'empty')"
@ -253,8 +253,8 @@
</el-table-column>
<el-table-column prop="reserved" label="预订" width="80" align="center">
<template slot-scope="scope">
<el-tag
:type="scope.row.reserved > 0 ? 'warning' : 'success'"
<el-tag
:type="scope.row.reserved > 0 ? 'warning' : 'success'"
size="small"
class="clickable-tag"
@click="navigateToRentalList(scope.row.apartmentId, 'status', 'reserved')"
@ -265,8 +265,8 @@
</el-table-column>
<el-table-column prop="rented" label="在租" width="80" align="center">
<template slot-scope="scope">
<el-tag
type="success"
<el-tag
type="success"
size="small"
class="clickable-tag"
@click="navigateToRentalList(scope.row.apartmentId, 'status', 'rented')"
@ -277,8 +277,8 @@
</el-table-column>
<el-table-column prop="soon_expire" label="即将到期" width="100" align="center">
<template slot-scope="scope">
<el-tag
:type="scope.row.soon_expire > 0 ? 'warning' : 'success'"
<el-tag
:type="scope.row.soon_expire > 0 ? 'warning' : 'success'"
size="small"
class="clickable-tag"
@click="navigateToRentalList(scope.row.apartmentId, 'subStatus', 'soon_expire')"
@ -289,8 +289,8 @@
</el-table-column>
<el-table-column prop="expired" label="已到期" width="80" align="center">
<template slot-scope="scope">
<el-tag
:type="scope.row.expired > 0 ? 'danger' : 'success'"
<el-tag
:type="scope.row.expired > 0 ? 'danger' : 'success'"
size="small"
class="clickable-tag"
@click="navigateToRentalList(scope.row.apartmentId, 'subStatus', 'expired')"
@ -301,8 +301,8 @@
</el-table-column>
<el-table-column prop="total" label="总数" width="80" align="center" fixed="right">
<template slot-scope="scope">
<el-tag
type="primary"
<el-tag
type="primary"
size="small"
class="clickable-tag total-tag"
@click="navigateToRentalList(scope.row.apartmentId)"
@ -333,23 +333,23 @@ export default {
rentedRoomCount: 0,
soonExpireRoomCount: 0,
expiredRoomCount: 0,
//
monthlyReceivable: 0,
monthlyReceived: 0,
collectedRentAmount: 0,
collectedWaterAmount: 0,
//
unpaidBillCount: 0,
billStatusStats: [],
//
monthlyTrend: [],
//
apartmentHouseStats: [],
//
statusColors: {
empty: '#909399',
@ -359,12 +359,12 @@ export default {
expired: '#F56C6C',
rate: '#409EFF'
},
//
statusChartInstance: null,
trendChartInstance: null,
billChartInstance: null,
currentDate: ''
}
},
@ -393,7 +393,7 @@ export default {
const weekday = weekdays[now.getDay()]
this.currentDate = `${year}${month}${day}${weekday}`
},
async loadData() {
try {
await Promise.all([
@ -410,11 +410,11 @@ export default {
console.error(error)
}
},
async loadDashboardStats() {
const response = await statisticsApi.getDashboardStats()
const data = response.data || response
this.apartmentCount = data.apartmentCount || 0
this.roomCount = data.roomCount || 0
this.emptyRoomCount = data.emptyRoomCount || 0
@ -425,22 +425,22 @@ export default {
this.collectedRentAmount = data.collectedRentAmount || 0
this.collectedWaterAmount = data.collectedWaterAmount || 0
},
async loadApartmentStats() {
const response = await statisticsApi.getApartmentRoomStatusStats()
this.apartmentHouseStats = response.data || response || []
},
async loadBillStats() {
try {
//
const response = await billApi.getStatistics()
const data = response.data || response || {}
// API
const summary = data.summary || {}
const statusList = data.status || []
//
this.billStatusStats = statusList.map(item => ({
status: item.status,
@ -449,12 +449,12 @@ export default {
totalReceivable: item.totalReceivable,
totalReceived: item.totalReceived
}))
// unpaid + partial
this.unpaidBillCount = statusList
.filter(item => item.status === 'unpaid' || item.status === 'partial')
.reduce((sum, item) => sum + (item.count || 0), 0)
// summary
this.monthlyReceivable = parseFloat(summary.totalReceivable) || 0
this.monthlyReceived = parseFloat(summary.totalReceived) || 0
@ -464,7 +464,7 @@ export default {
this.unpaidBillCount = 0
}
},
getStatusText(status) {
const statusMap = {
'unpaid': '未收',
@ -474,7 +474,7 @@ export default {
}
return statusMap[status] || status
},
async loadTrendData() {
try {
const response = await statisticsApi.getRentStatistics()
@ -484,19 +484,19 @@ export default {
this.monthlyTrend = []
}
},
initCharts() {
this.initStatusChart()
this.initTrendChart()
this.initBillChart()
},
initStatusChart() {
const chartDom = this.$refs.statusChart
if (!chartDom) return
this.statusChartInstance = echarts.init(chartDom)
const option = {
tooltip: {
trigger: 'item',
@ -540,19 +540,19 @@ export default {
}
]
}
this.statusChartInstance.setOption(option)
},
initTrendChart() {
const chartDom = this.$refs.trendChart
if (!chartDom) return
this.trendChartInstance = echarts.init(chartDom)
const months = this.monthlyTrend.map(item => item.month).reverse()
const amounts = this.monthlyTrend.map(item => item.amount).reverse()
const option = {
tooltip: {
trigger: 'axis',
@ -622,35 +622,35 @@ export default {
}
]
}
this.trendChartInstance.setOption(option)
},
initBillChart() {
const chartDom = this.$refs.billChart
if (!chartDom) return
this.billChartInstance = echarts.init(chartDom)
//
const defaultData = [
{ value: 0, name: '未收', itemStyle: { color: '#F56C6C' } },
{ value: 0, name: '部分收款', itemStyle: { color: '#E6A23C' } },
{ value: 0, name: '已收清', itemStyle: { color: '#67C23A' } }
]
// 使
const chartData = this.billStatusStats.length > 0
const chartData = this.billStatusStats.length > 0
? this.billStatusStats.map(item => ({
value: item.count,
name: item.statusText,
itemStyle: {
color: item.status === 'unpaid' ? '#F56C6C' :
itemStyle: {
color: item.status === 'unpaid' ? '#F56C6C' :
item.status === 'partial' ? '#E6A23C' : '#67C23A'
}
}))
: defaultData
const option = {
tooltip: {
trigger: 'item',
@ -689,27 +689,27 @@ export default {
}
]
}
this.billChartInstance.setOption(option)
},
handleResize() {
this.statusChartInstance && this.statusChartInstance.resize()
this.trendChartInstance && this.trendChartInstance.resize()
this.billChartInstance && this.billChartInstance.resize()
},
disposeCharts() {
this.statusChartInstance && this.statusChartInstance.dispose()
this.trendChartInstance && this.trendChartInstance.dispose()
this.billChartInstance && this.billChartInstance.dispose()
},
getPercentage(count) {
if (this.roomCount === 0) return 0
return Math.round((count / this.roomCount) * 100)
},
formatMoney(value) {
if (!value || value === 0) return '¥0'
if (value >= 10000) {
@ -717,7 +717,7 @@ export default {
}
return '¥' + value.toLocaleString()
},
getSummary(param) {
const { columns, data } = param
const sums = []
@ -737,7 +737,7 @@ export default {
})
return sums
},
navigateToRentalList(apartmentId, paramType, paramValue) {
const timestamp = Date.now()
let query = `t=${timestamp}`
@ -749,7 +749,7 @@ export default {
}
this.$router.push(`/rental?${query}`)
},
refreshData() {
this.loadData()
this.$message.success('数据已刷新')
@ -1103,62 +1103,62 @@ export default {
.dashboard {
padding: 12px;
}
.welcome-section {
flex-direction: column;
align-items: flex-start;
gap: 16px;
padding: 16px;
}
.welcome-title {
font-size: 18px;
}
.quick-actions {
width: 100%;
}
.quick-actions .el-button {
flex: 1;
}
.kpi-card {
padding: 16px;
margin-bottom: 12px;
}
.kpi-icon {
width: 40px;
height: 40px;
margin-right: 12px;
}
.kpi-icon i {
font-size: 20px;
}
.kpi-value {
font-size: 20px;
}
.kpi-label {
font-size: 12px;
}
.status-chart {
height: 180px;
margin-top: 16px;
}
.finance-chart {
height: 220px;
}
.alert-card {
margin-bottom: 12px;
}
.section-title {
font-size: 14px;
}
@ -1168,24 +1168,24 @@ export default {
.welcome-title {
font-size: 16px;
}
.kpi-value {
font-size: 18px;
}
.alert-icon {
width: 40px;
height: 40px;
}
.alert-icon i {
font-size: 20px;
}
.alert-title {
font-size: 14px;
}
.alert-desc {
font-size: 12px;
}

View File

@ -13,13 +13,12 @@
v-model="rentalForm.roomId"
placeholder="请选择房间"
style="width: 100%"
:disabled="!!preSelectedRoomId"
@change="onRoomChange"
>
<el-option
v-for="room in availableRooms"
:key="room.id"
:label="`${getApartmentName(room.apartmentId)} - ${room.roomNumber}`"
:label="`${room.Apartment.name || ''} - ${room.roomNumber}`"
:value="room.id"
></el-option>
</el-select>
@ -124,7 +123,7 @@
</template>
<script>
import { rentalApi, roomApi, apartmentApi } from '../../api/api'
import { rentalApi, roomApi } from '../../api/api'
import { getUserInfo } from '../../utils/auth'
export default {
@ -145,14 +144,38 @@ export default {
}
}
const validateNumber = (rule, value, callback) => {
if (value === '' || value === null || value === undefined) {
callback(new Error('请输入金额'))
} else if (isNaN(Number(value)) || Number(value) < 0) {
if (isNaN(Number(value)) || Number(value) < 0) {
callback(new Error('金额必须为大于等于0的数字'))
} else {
callback()
}
}
const validateOptionalNumber = (rule, value, callback) => {
if (value === '' || value === null || value === undefined) {
callback()
} else if (isNaN(Number(value)) || Number(value) < 0) {
callback(new Error('请输入有效的数字'))
} else {
callback()
}
}
const validateEndDate = (rule, value, callback) => {
if (!value) {
callback(new Error('请选择结束日期'))
} else if (this.rentalForm.startDate) {
const start = new Date(this.rentalForm.startDate)
const end = new Date(value)
start.setHours(0, 0, 0, 0)
end.setHours(0, 0, 0, 0)
if (end <= start) {
callback(new Error('结束日期必须晚于开始日期'))
} else {
callback()
}
} else {
callback()
}
}
return {
rentalForm: {
@ -182,20 +205,25 @@ export default {
renterIdCard: [{ validator: validateIdCard, trigger: 'blur' }],
paymentType: [{ required: true, message: '请选择付租方式', trigger: 'change' }],
startDate: [{ required: true, message: '请选择开始日期', trigger: 'change' }],
endDate: [{ required: true, message: '请选择结束日期', trigger: 'change' }],
endDate: [
{ required: true, message: '请选择结束日期', trigger: 'change' },
{ validator: validateEndDate, trigger: 'change' }
],
rent: [
{ required: true, message: '请输入租金', trigger: 'blur' },
{ validator: validateNumber, trigger: 'blur' }
]
],
deposit: [{ validator: validateOptionalNumber, trigger: 'blur' }],
waterStartReading: [{ validator: validateOptionalNumber, trigger: 'blur' }],
electricStartReading: [{ validator: validateOptionalNumber, trigger: 'blur' }]
},
rooms: [],
apartments: [],
leasePeriod: 1
}
},
computed: {
availableRooms() {
return this.rooms.filter(room => room.status === 'available')
return this.rooms.filter(room => room.status !== 'rented')
},
selectedRoomInfo() {
if (!this.rentalForm.roomId) return null
@ -203,7 +231,7 @@ export default {
if (!room) return null
return {
...room,
apartmentName: this.getApartmentName(room.apartmentId)
apartmentName: room.Apartment.name
}
},
periodOptions() {
@ -224,10 +252,11 @@ export default {
'rentalForm.paymentType': function(newVal, oldVal) {
this.leasePeriod = 1
this.updateEndDate()
if (!this.selectedRoomInfo) return
if (newVal === 'monthly') {
this.rentalForm.rent = (this.selectedRoomInfo && this.selectedRoomInfo.monthlyPrice) || ''
this.rentalForm.rent = this.selectedRoomInfo.monthlyPrice || ''
} else {
this.rentalForm.rent = (this.selectedRoomInfo && this.selectedRoomInfo.monthlyPrice) ? this.selectedRoomInfo.monthlyPrice * 12 : ''
this.rentalForm.rent = this.selectedRoomInfo.yearlyPrice || this.selectedRoomInfo.monthlyPrice * 12 || ''
}
}
},
@ -242,13 +271,8 @@ export default {
methods: {
async loadData() {
try {
const [roomsResponse, apartmentsResponse] = await Promise.all([
roomApi.list(),
apartmentApi.list()
])
const roomsResponse = await roomApi.list()
this.rooms = Array.isArray(roomsResponse) ? roomsResponse : (roomsResponse.data || [])
this.apartments = Array.isArray(apartmentsResponse) ? apartmentsResponse : (apartmentsResponse.data || [])
if (this.preSelectedRoomId) {
this.rentalForm.roomId = this.preSelectedRoomId
@ -257,7 +281,10 @@ export default {
if (this.rentalForm.paymentType === 'monthly') {
this.rentalForm.rent = room.monthlyPrice
} else {
this.rentalForm.rent = room.monthlyPrice ? room.monthlyPrice * 12 : ''
this.rentalForm.rent = room.yearlyPrice || (room.monthlyPrice ? room.monthlyPrice * 12 : '')
}
if (room.deposit) {
this.rentalForm.deposit = room.deposit
}
}
}
@ -266,20 +293,16 @@ export default {
this.$message.error('加载数据失败')
}
},
getApartmentName(apartmentId) {
if (!Array.isArray(this.apartments)) {
return ''
}
const apartment = this.apartments.find(a => a.id == apartmentId)
return apartment ? apartment.name : ''
},
onRoomChange(roomId) {
const room = this.rooms.find(r => r.id === roomId)
if (room) {
if (this.rentalForm.paymentType === 'monthly') {
this.rentalForm.rent = room.monthlyPrice
} else {
this.rentalForm.rent = room.monthlyPrice ? room.monthlyPrice * 12 : ''
this.rentalForm.rent = room.yearlyPrice || (room.monthlyPrice ? room.monthlyPrice * 12 : '')
}
if (room.deposit) {
this.rentalForm.deposit = room.deposit
}
}
},
@ -321,7 +344,7 @@ export default {
startDate: this.formatDate(this.rentalForm.startDate),
endDate: this.formatDate(this.rentalForm.endDate),
rent: this.rentalForm.rent,
deposit: this.rentalForm.deposit || 0,
deposit: this.rentalForm.deposit || null,
operator: this.rentalForm.operator,
remark: this.rentalForm.remark,
//

View File

@ -158,18 +158,6 @@
<span class="info-value">¥{{ currentRental.deposit || 0 }}</span>
</div>
</el-col>
<el-col :xs="24" :sm="12" :md="8" :lg="6">
<div class="info-item">
<span class="info-label">水表起始</span>
<span class="info-value">{{ currentRental.waterMeterStart || '--' }} </span>
</div>
</el-col>
<el-col :xs="24" :sm="12" :md="8" :lg="6">
<div class="info-item">
<span class="info-label">电表起始</span>
<span class="info-value">{{ currentRental.electricityMeterStart || '--' }} </span>
</div>
</el-col>
<el-col :xs="24" :sm="24" :md="16" :lg="12" v-if="currentRental.remark">
<div class="info-item">
<span class="info-label">备注</span>
@ -255,11 +243,21 @@
</el-tag>
</template>
</el-table-column>
<el-table-column prop="startDate" label="开始日期" min-width="100"></el-table-column>
<el-table-column prop="endDate" label="结束日期" min-width="100"></el-table-column>
<el-table-column prop="startDate" label="开始日期" min-width="100">
<template slot-scope="scope">
<el-link type="primary" :underline="false" @click="handleViewRental(scope.row)">{{ scope.row.startDate }}</el-link>
</template>
</el-table-column>
<el-table-column prop="endDate" label="结束日期" min-width="100">
<template slot-scope="scope">
<el-link type="primary" :underline="false" @click="handleViewRental(scope.row)">{{ scope.row.endDate }}</el-link>
</template>
</el-table-column>
<el-table-column prop="rent" label="月租金" min-width="100">
<template slot-scope="scope">
<span class="price">¥{{ scope.row.rent }}</span>
<el-link type="primary" :underline="false" @click="handleViewRental(scope.row)">
<span class="price">¥{{ scope.row.rent }}</span>
</el-link>
</template>
</el-table-column>
<el-table-column prop="deposit" label="押金" min-width="100">
@ -274,11 +272,13 @@
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" min-width="250" fixed="right">
<el-table-column prop="createdAt" label="创建时间" min-width="110">
<template slot-scope="scope">
{{ formatDate(scope.row.createdAt) }}
</template>
</el-table-column>
<el-table-column label="操作" min-width="200" fixed="right">
<template slot-scope="scope">
<el-button type="primary" size="mini" icon="el-icon-view" @click="handleViewRental(scope.row)">
详情
</el-button>
<el-button type="warning" size="mini" icon="el-icon-edit" @click="handleEditRental(scope.row)" v-if="scope.row.status === 'active' && hasPermission('rental:edit')">
编辑
</el-button>
@ -760,24 +760,6 @@
<el-input-number v-model="editRentalForm.deposit" :min="0" :precision="2" style="width: 100%"></el-input-number>
</el-form-item>
<el-divider content-position="left">水电表初始读数</el-divider>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="水表读数" prop="waterMeterStart">
<el-input-number v-model="editRentalForm.waterMeterStart" :min="0" :precision="2" style="width: 100%">
<template slot="append"></template>
</el-input-number>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="电表读数" prop="electricityMeterStart">
<el-input-number v-model="editRentalForm.electricityMeterStart" :min="0" :precision="2" style="width: 100%">
<template slot="append"></template>
</el-input-number>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="备注" prop="remark">
<el-input v-model="editRentalForm.remark" type="textarea" :rows="3" placeholder="请输入备注信息(选填)"></el-input>
</el-form-item>

View File

@ -67,17 +67,6 @@
<el-form-item label="经办人" prop="operator">
<el-input v-model="rentalForm.operator" placeholder="请输入经办人"></el-input>
</el-form-item>
<el-divider>水电表信息</el-divider>
<el-form-item label="水表起始读数" prop="waterMeterStart">
<el-input v-model.number="rentalForm.waterMeterStart" placeholder="请输入水表起始读数(选填)">
<template slot="append"></template>
</el-input>
</el-form-item>
<el-form-item label="电表起始读数" prop="electricityMeterStart">
<el-input v-model.number="rentalForm.electricityMeterStart" placeholder="请输入电表起始读数(选填)">
<template slot="append"></template>
</el-input>
</el-form-item>
<el-divider>其他信息</el-divider>
<el-form-item label="状态" prop="status">
<el-select v-model="rentalForm.status" placeholder="请选择状态" style="width: 100%">
@ -115,8 +104,6 @@ export default {
rent: '',
deposit: '',
operator: '',
waterMeterStart: '',
electricityMeterStart: '',
status: '',
remark: ''
},

View File

@ -33,8 +33,6 @@
<el-form-item label="付租方式">
<el-select v-model="searchForm.paymentType" placeholder="请选择" style="width: 120px" clearable>
<el-option label="月租" value="monthly"></el-option>
<el-option label="季租" value="quarterly"></el-option>
<el-option label="半年租" value="half_year"></el-option>
<el-option label="年租" value="yearly"></el-option>
</el-select>
</el-form-item>
@ -106,12 +104,11 @@
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" min-width="140"></el-table-column>
<el-table-column label="操作" min-width="250" fixed="right">
<el-table-column label="操作" min-width="200" fixed="right">
<template slot-scope="scope">
<el-button type="primary" size="mini" @click="handleViewDetail(scope.row)">详情</el-button>
<el-button type="warning" size="mini" @click="handleEdit(scope.row)" v-if="scope.row.status === 'active' && hasPermission('rental:update')">编辑</el-button>
<el-button type="success" size="mini" @click="handleRenew(scope.row)" v-if="scope.row.status === 'active' && hasPermission('rental:add')">续租</el-button>
<el-button type="danger" size="mini" @click="handleTerminate(scope.row)" v-if="scope.row.status === 'active' && hasPermission('rental:update')">退租</el-button>
<el-button type="warning" size="mini" @click="handleEdit(scope.row)" v-if="hasPermission('rental:edit')">编辑</el-button>
<el-button type="danger" size="mini" @click="handleDelete(scope.row)" v-if="hasPermission('rental:delete')">删除</el-button>
</template>
</el-table-column>
</el-table>
@ -142,8 +139,6 @@
<el-descriptions-item label="结束日期">{{ selectedRental.endDate }}</el-descriptions-item>
<el-descriptions-item label="月租金">¥{{ selectedRental.rent }}</el-descriptions-item>
<el-descriptions-item label="押金">¥{{ selectedRental.deposit || 0 }}</el-descriptions-item>
<el-descriptions-item label="水表起始">{{ selectedRental.waterMeterStart || '--' }} </el-descriptions-item>
<el-descriptions-item label="电表起始">{{ selectedRental.electricityMeterStart || '--' }} </el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag :type="getStatusType(selectedRental.status)" size="small">
{{ getStatusText(selectedRental.status) }}
@ -158,233 +153,7 @@
</div>
</el-dialog>
<!-- 续租对话框 -->
<el-dialog title="续租" :visible.sync="renewDialogVisible" width="500px">
<el-form :model="renewForm" :rules="renewRules" ref="renewForm" label-width="100px">
<el-form-item label="租客">
<span class="form-static-text">{{ selectedRental && selectedRental.Renter ? selectedRental.Renter.name : '--' }}</span>
</el-form-item>
<el-form-item label="原结束日期">
<span class="form-static-text">{{ selectedRental ? selectedRental.endDate : '--' }}</span>
</el-form-item>
<el-form-item label="新开始日期" prop="startDate">
<el-date-picker v-model="renewForm.startDate" type="date" placeholder="选择开始日期" style="width: 100%" value-format="yyyy-MM-dd"></el-date-picker>
</el-form-item>
<el-form-item label="新结束日期" prop="endDate">
<el-date-picker v-model="renewForm.endDate" type="date" placeholder="选择结束日期" style="width: 100%" value-format="yyyy-MM-dd"></el-date-picker>
</el-form-item>
<el-form-item label="租金" prop="rent">
<el-input-number v-model="renewForm.rent" :min="0" :precision="2" style="width: 100%"></el-input-number>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="renewForm.remark" type="textarea" :rows="3" placeholder="请输入备注"></el-input>
</el-form-item>
</el-form>
<div slot="footer">
<el-button @click="renewDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleConfirmRenew" :loading="submitLoading">确认续租</el-button>
</div>
</el-dialog>
<!-- 退租对话框 -->
<el-dialog title="办理退租" :visible.sync="terminateDialogVisible" width="500px">
<el-form :model="terminateForm" ref="terminateForm" label-width="120px">
<el-form-item label="租客">
<span class="form-static-text">{{ selectedRental && selectedRental.Renter ? selectedRental.Renter.name : '--' }}</span>
</el-form-item>
<el-form-item label="押金金额">
<span class="form-static-text price">¥{{ selectedRental ? selectedRental.deposit || 0 : 0 }}</span>
</el-form-item>
<el-form-item label="水表最终读数">
<el-input-number v-model="terminateForm.waterMeterEnd" :min="0" :precision="2" style="width: 100%">
<template slot="append"></template>
</el-input-number>
</el-form-item>
<el-form-item label="电表最终读数">
<el-input-number v-model="terminateForm.electricityMeterEnd" :min="0" :precision="2" style="width: 100%">
<template slot="append"></template>
</el-input-number>
</el-form-item>
<el-form-item label="退还押金">
<el-input-number v-model="terminateForm.refundDeposit" :min="0" :precision="2" style="width: 100%"></el-input-number>
</el-form-item>
<el-form-item label="退租备注">
<el-input v-model="terminateForm.remark" type="textarea" :rows="3" placeholder="请输入退租备注"></el-input>
</el-form-item>
</el-form>
<div slot="footer">
<el-button @click="terminateDialogVisible = false">取消</el-button>
<el-button type="danger" @click="handleConfirmTerminate" :loading="submitLoading">确认退租</el-button>
</div>
</el-dialog>
<!-- 编辑租约对话框 -->
<el-dialog title="编辑租约" :visible.sync="editDialogVisible" width="700px" :close-on-click-modal="false">
<el-form :model="editForm" :rules="editRules" ref="editForm" label-width="120px">
<el-divider content-position="left">租客信息</el-divider>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="租客姓名" prop="renterName">
<el-input v-model="editForm.renterName" placeholder="请输入租客姓名"></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="联系电话" prop="renterPhone">
<el-input v-model="editForm.renterPhone" placeholder="请输入联系电话"></el-input>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="身份证号" prop="renterIdCard">
<el-input v-model="editForm.renterIdCard" placeholder="请输入身份证号(选填)"></el-input>
</el-form-item>
<el-divider content-position="left">租约信息</el-divider>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="付租方式" prop="paymentType">
<el-select v-model="editForm.paymentType" placeholder="请选择付租方式" style="width: 100%">
<el-option label="月租" value="monthly"></el-option>
<el-option label="季租" value="quarterly"></el-option>
<el-option label="半年租" value="half_year"></el-option>
<el-option label="年租" value="yearly"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="月租金" prop="rent">
<el-input-number v-model="editForm.rent" :min="0" :precision="2" style="width: 100%"></el-input-number>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="开始日期" prop="startDate">
<el-date-picker v-model="editForm.startDate" type="date" placeholder="选择开始日期" style="width: 100%" value-format="yyyy-MM-dd"></el-date-picker>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="结束日期" prop="endDate">
<el-date-picker v-model="editForm.endDate" type="date" placeholder="选择结束日期" style="width: 100%" value-format="yyyy-MM-dd"></el-date-picker>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="押金" prop="deposit">
<el-input-number v-model="editForm.deposit" :min="0" :precision="2" style="width: 100%"></el-input-number>
</el-form-item>
<el-divider content-position="left">水电表初始读数</el-divider>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="水表读数" prop="waterMeterStart">
<el-input-number v-model="editForm.waterMeterStart" :min="0" :precision="2" style="width: 100%">
<template slot="append"></template>
</el-input-number>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="电表读数" prop="electricityMeterStart">
<el-input-number v-model="editForm.electricityMeterStart" :min="0" :precision="2" style="width: 100%">
<template slot="append"></template>
</el-input-number>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="备注" prop="remark">
<el-input v-model="editForm.remark" type="textarea" :rows="3" placeholder="请输入备注信息(选填)"></el-input>
</el-form-item>
</el-form>
<div slot="footer">
<el-button @click="editDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleConfirmEdit" :loading="submitLoading">保存修改</el-button>
</div>
</el-dialog>
<!-- 续租对话框 -->
<el-dialog title="办理续租" :visible.sync="renewDialogVisible" width="700px" :close-on-click-modal="false">
<el-form :model="renewForm" :rules="renewRules" ref="renewForm" label-width="120px">
<el-alert
title="续租将创建新的租约记录,当前租约将标记为已到期"
type="info"
:closable="false"
style="margin-bottom: 20px">
</el-alert>
<el-divider content-position="left">当前租约信息</el-divider>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="租客姓名">
<span class="form-static-text">{{ selectedRental && selectedRental.Renter ? selectedRental.Renter.name : '--' }}</span>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="原结束日期">
<span class="form-static-text">{{ selectedRental ? selectedRental.endDate : '--' }}</span>
</el-form-item>
</el-col>
</el-row>
<el-divider content-position="left">新租约信息</el-divider>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="付租方式" prop="paymentType">
<el-select v-model="renewForm.paymentType" placeholder="请选择付租方式" style="width: 100%">
<el-option label="月租" value="monthly"></el-option>
<el-option label="季租" value="quarterly"></el-option>
<el-option label="半年租" value="half_year"></el-option>
<el-option label="年租" value="yearly"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="月租金" prop="rent">
<el-input-number v-model="renewForm.rent" :min="0" :precision="2" style="width: 100%"></el-input-number>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="开始日期" prop="startDate">
<el-date-picker v-model="renewForm.startDate" type="date" placeholder="选择开始日期" style="width: 100%" value-format="yyyy-MM-dd"></el-date-picker>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="结束日期" prop="endDate">
<el-date-picker v-model="renewForm.endDate" type="date" placeholder="选择结束日期" style="width: 100%" value-format="yyyy-MM-dd"></el-date-picker>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="押金" prop="deposit">
<el-input-number v-model="renewForm.deposit" :min="0" :precision="2" style="width: 100%"></el-input-number>
</el-form-item>
<el-divider content-position="left">水电表初始读数</el-divider>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="水表读数" prop="waterStartReading">
<el-input-number v-model="renewForm.waterStartReading" :min="0" :precision="2" style="width: 100%">
<template slot="append"></template>
</el-input-number>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="电表读数" prop="electricStartReading">
<el-input-number v-model="renewForm.electricStartReading" :min="0" :precision="2" style="width: 100%">
<template slot="append"></template>
</el-input-number>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="备注" prop="remark">
<el-input v-model="renewForm.remark" type="textarea" :rows="3" placeholder="请输入备注信息(选填)"></el-input>
</el-form-item>
</el-form>
<div slot="footer">
<el-button @click="renewDialogVisible = false">取消</el-button>
<el-button type="success" @click="handleConfirmRenew" :loading="submitLoading">确认续租</el-button>
</div>
</el-dialog>
</div>
</template>
@ -415,78 +184,7 @@ export default {
//
detailDialogVisible: false,
selectedRental: null,
//
renewDialogVisible: false,
renewForm: {
startDate: '',
endDate: '',
rent: 0,
remark: ''
},
renewRules: {
startDate: [{ required: true, message: '请选择开始日期', trigger: 'change' }],
endDate: [{ required: true, message: '请选择结束日期', trigger: 'change' }],
rent: [{ required: true, message: '请输入租金', trigger: 'blur' }]
},
// 退
terminateDialogVisible: false,
terminateForm: {
waterMeterEnd: 0,
electricityMeterEnd: 0,
refundDeposit: 0,
remark: ''
},
//
editDialogVisible: false,
editForm: {
id: null,
renterId: null,
renterName: '',
renterPhone: '',
renterIdCard: '',
paymentType: 'monthly',
rent: 0,
deposit: 0,
startDate: '',
endDate: '',
waterMeterStart: 0,
electricityMeterStart: 0,
remark: ''
},
editRules: {
renterName: [{ required: true, message: '请输入租客姓名', trigger: 'blur' }],
paymentType: [{ required: true, message: '请选择付租方式', trigger: 'change' }],
rent: [{ required: true, message: '请输入租金', trigger: 'blur' }],
startDate: [{ required: true, message: '请选择开始日期', trigger: 'change' }],
endDate: [{ required: true, message: '请选择结束日期', trigger: 'change' }]
},
//
renewDialogVisible: false,
renewForm: {
roomId: null,
renterName: '',
renterPhone: '',
renterIdCard: '',
paymentType: 'monthly',
rent: 0,
deposit: 0,
startDate: '',
endDate: '',
waterStartReading: 0,
electricStartReading: 0,
remark: ''
},
renewRules: {
paymentType: [{ required: true, message: '请选择付租方式', trigger: 'change' }],
rent: [{ required: true, message: '请输入租金', trigger: 'blur' }],
startDate: [{ required: true, message: '请选择开始日期', trigger: 'change' }],
endDate: [{ required: true, message: '请选择结束日期', trigger: 'change' }]
}
selectedRental: null
}
},
computed: {
@ -594,159 +292,41 @@ export default {
}
},
//
// -
handleEdit(row) {
this.selectedRental = row
this.editForm = {
id: row.id,
renterId: row.renterId,
renterName: row.Renter ? row.Renter.name : '',
renterPhone: row.Renter ? row.Renter.phone : '',
renterIdCard: row.Renter ? row.Renter.idCard : '',
paymentType: row.paymentType || 'monthly',
rent: parseFloat(row.rent) || 0,
deposit: parseFloat(row.deposit) || 0,
startDate: row.startDate,
endDate: row.endDate,
waterMeterStart: parseFloat(row.waterMeterStart) || 0,
electricityMeterStart: parseFloat(row.electricityMeterStart) || 0,
remark: row.remark || ''
}
this.editDialogVisible = true
},
//
async handleConfirmEdit() {
this.$refs.editForm.validate(async valid => {
if (!valid) return
this.submitLoading = true
try {
// 1.
if (this.editForm.renterId) {
await renterApi.update(this.editForm.renterId, {
name: this.editForm.renterName,
phone: this.editForm.renterPhone,
idCard: this.editForm.renterIdCard
})
}
// 2.
const rentalData = {
roomId: this.selectedRental.roomId,
renterId: this.editForm.renterId,
paymentType: this.editForm.paymentType,
rent: this.editForm.rent,
deposit: this.editForm.deposit,
startDate: this.editForm.startDate,
endDate: this.editForm.endDate,
waterMeterStart: this.editForm.waterMeterStart,
electricityMeterStart: this.editForm.electricityMeterStart,
remark: this.editForm.remark
}
await rentalApi.update(this.editForm.id, rentalData)
this.$message.success('租约更新成功')
this.editDialogVisible = false
this.loadData()
} catch (error) {
this.$message.error('租约更新失败')
console.error(error)
} finally {
this.submitLoading = false
this.$router.push({
path: `/rental/edit/${row.id}`,
query: {
page: this.currentPage,
pageSize: this.pageSize,
...this.searchForm
}
})
},
//
handleRenew(row) {
this.selectedRental = row
const oldEndDate = new Date(row.endDate)
const newStartDate = row.endDate
//
const newEndDate = new Date(oldEndDate)
newEndDate.setMonth(newEndDate.getMonth() + 1)
this.renewForm = {
roomId: row.roomId,
renterName: row.Renter ? row.Renter.name : '',
renterPhone: row.Renter ? row.Renter.phone : '',
renterIdCard: row.Renter ? row.Renter.idCard : '',
paymentType: row.paymentType || 'monthly',
rent: parseFloat(row.rent) || 0,
deposit: parseFloat(row.deposit) || 0,
startDate: newStartDate,
endDate: this.formatDate(newEndDate),
waterStartReading: parseFloat(row.waterMeterEnd) || parseFloat(row.waterMeterStart) || 0,
electricStartReading: parseFloat(row.electricityMeterEnd) || parseFloat(row.electricityMeterStart) || 0,
remark: '续租'
}
this.renewDialogVisible = true
},
//
async handleConfirmRenew() {
this.$refs.renewForm.validate(async valid => {
if (!valid) return
//
async handleDelete(row) {
this.$confirm('确定要删除此租约吗?删除后将同时删除关联的账单和抄表记录,此操作不可恢复!', '警告', {
confirmButtonText: '确定删除',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
this.submitLoading = true
try {
const data = {
roomId: this.renewForm.roomId,
renterName: this.renewForm.renterName,
renterPhone: this.renewForm.renterPhone,
renterIdCard: this.renewForm.renterIdCard,
paymentType: this.renewForm.paymentType,
rent: this.renewForm.rent,
deposit: this.renewForm.deposit,
startDate: this.renewForm.startDate,
endDate: this.renewForm.endDate,
waterStartReading: this.renewForm.waterStartReading,
electricStartReading: this.renewForm.electricStartReading,
remark: this.renewForm.remark
}
await rentalApi.createWithRenter(data)
this.$message.success('续租成功')
this.renewDialogVisible = false
await rentalApi.delete(row.id)
this.$message.success('删除成功')
this.loadData()
} catch (error) {
this.$message.error('续租失败')
console.error(error)
this.$message.error('删除失败:' + (error.message || '未知错误'))
console.error('删除失败:', error)
} finally {
this.submitLoading = false
}
}).catch(() => {
this.$message.info('已取消删除')
})
},
handleTerminate(row) {
this.selectedRental = row
this.terminateForm = {
waterMeterEnd: 0,
electricityMeterEnd: 0,
refundDeposit: row.deposit || 0,
remark: ''
}
this.terminateDialogVisible = true
},
async handleConfirmTerminate() {
if (!this.selectedRental) return
this.submitLoading = true
try {
await rentalApi.terminate(this.selectedRental.id, this.terminateForm)
this.$message.success('退租办理成功')
this.terminateDialogVisible = false
this.loadData()
} catch (error) {
this.$message.error('退租办理失败')
console.error(error)
} finally {
this.submitLoading = false
}
},
formatDate(date) {
if (!date) return ''
const d = new Date(date)

View File

@ -1,470 +0,0 @@
<template>
<div class="rental-archive">
<el-card>
<template slot="header">
<div class="card-header">
<span>租赁档案</span>
</div>
</template>
<el-form :inline="true" :model="searchForm" class="search-form">
<el-form-item label="公寓">
<el-select v-model="searchForm.apartmentId" placeholder="请选择公寓" @change="handleApartmentChange">
<el-option label="全部" value=""></el-option>
<el-option v-for="apartment in apartments" :key="apartment.id" :label="apartment.name" :value="apartment.id"></el-option>
</el-select>
</el-form-item>
<el-form-item label="房间">
<el-select v-model="searchForm.roomId" placeholder="请选择房间" :disabled="!searchForm.apartmentId">
<el-option label="全部" value=""></el-option>
<el-option v-for="room in rooms" :key="room.id" :label="room.roomNumber" :value="room.id"></el-option>
</el-select>
</el-form-item>
<el-form-item label="开始日期">
<el-date-picker
v-model="searchForm.startDate"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="yyyy-MM-dd">
</el-date-picker>
</el-form-item>
<el-form-item label="结束日期">
<el-date-picker
v-model="searchForm.endDate"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="yyyy-MM-dd">
</el-date-picker>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="searchForm.status" placeholder="请选择状态">
<el-option label="全部" value=""></el-option>
<el-option label="在租" value="active"></el-option>
<el-option label="已到期" value="expired"></el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">搜索</el-button>
<el-button @click="resetSearch">重置</el-button>
</el-form-item>
</el-form>
<!-- PC端表格 -->
<div class="table-wrapper hidden-xs-only">
<el-table :data="rentalList" style="width: 100%" v-loading="isLoading">
<el-table-column prop="tenantName" label="租客" min-width="100"></el-table-column>
<el-table-column label="公寓" min-width="150">
<template slot-scope="scope">
{{ scope.row.Room.Apartment.name }}
</template>
</el-table-column>
<el-table-column label="房间" min-width="150">
<template slot-scope="scope">
{{ scope.row.Room.roomNumber}}
</template>
</el-table-column>
<el-table-column prop="startDate" 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="deposit" label="押金(元)" min-width="100"></el-table-column>
<el-table-column prop="remark" label="备注" min-width="120"></el-table-column>
<el-table-column prop="status" label="状态" min-width="80">
<template slot-scope="scope">
<el-tag :type="scope.row.status === 'active' ? 'success' : 'danger'" size="small">
{{ scope.row.status === 'active' ? '在租' : '已到期' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" min-width="150"></el-table-column>
<el-table-column label="操作" min-width="220" fixed="right">
<template slot-scope="scope">
<el-button type="primary" size="mini" @click="handleEdit(scope.row)" v-if="hasPermission('rental:edit')">编辑</el-button>
<el-button type="success" size="mini" @click="handleRenew(scope.row)" v-if="hasPermission('rental:add')">续租</el-button>
<el-button type="danger" size="mini" @click="handleDelete(scope.row.id)" v-if="hasPermission('rental:delete')">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
<!-- 移动端卡片列表 -->
<div class="mobile-list hidden-sm-and-up">
<div v-for="item in rentalList" :key="item.id" class="mobile-card">
<div class="mobile-card-header">
<span class="mobile-card-title">{{ item.tenantName }}</span>
<el-tag :type="item.status === 'active' ? 'success' : 'danger'" size="mini">
{{ item.status === 'active' ? '在租' : '已到期' }}
</el-tag>
</div>
<div class="mobile-card-body">
<div class="mobile-card-item">
<span class="mobile-card-label">公寓</span>
<span class="mobile-card-value">{{ item.Room.Apartment.name }}</span>
</div>
<div class="mobile-card-item">
<span class="mobile-card-label">房间</span>
<span class="mobile-card-value">{{ item.Room.roomNumber }}</span>
</div>
<div class="mobile-card-item">
<span class="mobile-card-label">租期</span>
<span class="mobile-card-value">{{ item.startDate }} {{ item.endDate }}</span>
</div>
<div class="mobile-card-item">
<span class="mobile-card-label">租金</span>
<span class="mobile-card-value">¥{{ item.rent }}/</span>
</div>
<div class="mobile-card-item">
<span class="mobile-card-label">押金</span>
<span class="mobile-card-value">¥{{ item.deposit }}</span>
</div>
<div class="mobile-card-item" v-if="item.remark">
<span class="mobile-card-label">备注</span>
<span class="mobile-card-value">{{ item.remark }}</span>
</div>
</div>
<div class="mobile-card-footer">
<el-button type="primary" size="mini" @click="handleEdit(item)" v-if="hasPermission('rental:edit')">编辑</el-button>
<el-button type="success" size="mini" @click="handleRenew(item)" v-if="hasPermission('rental:add')">续租</el-button>
<el-button type="danger" size="mini" @click="handleDelete(item.id)" v-if="hasPermission('rental:delete')">删除</el-button>
</div>
</div>
</div>
<div class="pagination-wrapper">
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="currentPage"
:page-sizes="[10, 20, 50, 100]"
:page-size="pageSize"
:layout="isMobile ? 'prev, pager, next' : 'total, sizes, prev, pager, next, jumper'"
:total="total">
</el-pagination>
</div>
</el-card>
<el-dialog :title="rentalForm.id ? '编辑租赁记录' : '新增租赁记录'" :visible.sync="rentalDialogVisible" width="500px">
<el-form :model="rentalForm" :rules="rentalRules" ref="rentalForm" label-width="80px">
<el-form-item label="租客姓名" prop="tenantName">
<el-input v-model="rentalForm.tenantName" placeholder="请输入租客姓名"></el-input>
</el-form-item>
<el-form-item label="房间" prop="roomId">
<el-select v-model="rentalForm.roomId" placeholder="请选择房间" style="width: 100%">
<el-option v-for="room in allRooms" :key="room.id" :label="`${room.Apartment.name} - ${room.roomNumber}`" :value="room.id"></el-option>
</el-select>
</el-form-item>
<el-form-item label="开始日期" prop="startDate">
<el-date-picker v-model="rentalForm.startDate" type="date" placeholder="选择开始日期" style="width: 100%"></el-date-picker>
</el-form-item>
<el-form-item label="结束日期" prop="endDate">
<el-date-picker v-model="rentalForm.endDate" type="date" placeholder="选择结束日期" style="width: 100%"></el-date-picker>
</el-form-item>
<el-form-item label="租金(元)" prop="rent">
<el-input v-model.number="rentalForm.rent" placeholder="请输入租金"></el-input>
</el-form-item>
<el-form-item label="押金(元)" prop="deposit">
<el-input v-model.number="rentalForm.deposit" placeholder="请输入押金"></el-input>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="rentalForm.status" placeholder="请选择状态" style="width: 100%">
<el-option label="有效" value="active"></el-option>
<el-option label="到期" value="expired"></el-option>
</el-select>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="rentalForm.remark" type="textarea" rows="3" placeholder="请输入备注信息"></el-input>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="rentalDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSave">保存</el-button>
</span>
</el-dialog>
</div>
</template>
<script>
import { rentalApi, apartmentApi, roomApi } from '../../api/api'
import { hasPermission } from '../../utils/permission'
import { getUserInfo } from '../../utils/auth'
export default {
name: 'RentalArchive',
data() {
return {
rentalList: [],
apartments: [],
rooms: [],
allRooms: [],
isLoading: false,
currentPage: 1,
pageSize: 10,
total: 0,
searchForm: {
apartmentId: '',
roomId: '',
startDate: '',
endDate: '',
status: ''
},
rentalDialogVisible: false,
rentalForm: {
id: '',
roomId: '',
tenantName: '',
startDate: '',
endDate: '',
rent: '',
deposit: '',
status: 'active',
remark: ''
},
rentalRules: {
tenantName: [{ required: true, message: '请输入租客姓名', trigger: 'blur' }],
roomId: [{ required: true, message: '请选择房间', trigger: 'change' }],
startDate: [{ required: true, message: '请选择开始日期', trigger: 'blur' }],
endDate: [{ required: true, message: '请选择结束日期', trigger: 'blur' }],
rent: [{ required: true, message: '请输入租金', trigger: 'blur' }],
status: [{ required: true, message: '请选择状态', trigger: 'blur' }]
}
}
},
computed: {
isMobile() {
return window.innerWidth <= 768
}
},
mounted() {
this.loadApartments()
this.loadAllRooms()
this.loadData()
window.addEventListener('resize', this.handleResize)
},
beforeDestroy() {
window.removeEventListener('resize', this.handleResize)
},
methods: {
hasPermission,
handleResize() {
this.$forceUpdate()
},
async loadApartments() {
try {
const response = await apartmentApi.list()
this.apartments = response.data || response
} catch (error) {
this.$message.error('加载公寓数据失败')
}
},
async loadAllRooms() {
try {
const response = await roomApi.list()
this.allRooms = response.data || response
} catch (error) {
this.$message.error('加载房间数据失败')
}
},
async loadRooms() {
if (!this.searchForm.apartmentId) {
this.rooms = []
return
}
try {
const response = await roomApi.list({ apartmentId: this.searchForm.apartmentId })
this.rooms = response.data || response
} catch (error) {
this.$message.error('加载房间数据失败')
}
},
handleApartmentChange() {
this.searchForm.roomId = ''
this.loadRooms()
},
async loadData() {
this.isLoading = true
try {
const params = {
page: this.currentPage,
pageSize: this.pageSize
}
if (this.searchForm.apartmentId) {
params.apartmentId = this.searchForm.apartmentId
}
if (this.searchForm.roomId) {
params.roomId = this.searchForm.roomId
}
if (this.searchForm.status) {
params.status = this.searchForm.status
}
if (this.searchForm.startDate && Array.isArray(this.searchForm.startDate) && this.searchForm.startDate.length === 2) {
params.startDateFrom = this.searchForm.startDate[0]
params.startDateTo = this.searchForm.startDate[1]
}
if (this.searchForm.endDate && Array.isArray(this.searchForm.endDate) && this.searchForm.endDate.length === 2) {
params.endDateFrom = this.searchForm.endDate[0]
params.endDateTo = this.searchForm.endDate[1]
}
const response = await rentalApi.getAll(params)
// : { data: { list: [], total: n, page: n, pageSize: n } }
this.rentalList = response.data.list
this.total = response.data.total
} catch (error) {
this.$message.error('加载数据失败')
} finally {
this.isLoading = false
}
},
handleSearch() {
this.currentPage = 1
this.loadData()
},
resetSearch() {
this.searchForm = {
apartmentId: '',
roomId: '',
startDate: '',
endDate: '',
status: ''
}
this.rooms = []
this.currentPage = 1
this.loadData()
},
handleSizeChange(size) {
this.pageSize = size
this.currentPage = 1
this.loadData()
},
handleCurrentChange(current) {
this.currentPage = current
this.loadData()
},
getApartmentName(roomId) {
const room = this.allRooms.find(r => r.id == roomId)
if (room) {
const apartment = this.apartments.find(a => a.id == room.apartmentId)
return apartment ? apartment.name : ''
}
return ''
},
getRoomNumber(roomId) {
const room = this.allRooms.find(r => r.id == roomId)
return room ? room.roomNumber : ''
},
handleEdit(rental) {
this.rentalForm = {
...rental,
startDate: rental.startDate ? new Date(rental.startDate) : '',
endDate: rental.endDate ? new Date(rental.endDate) : ''
}
this.rentalDialogVisible = true
},
handleRenew(rental) {
const oldEndDate = new Date(rental.endDate)
const newStartDate = new Date(oldEndDate)
const oldStartDate = new Date(rental.startDate)
const monthsDiff = (oldEndDate.getFullYear() - oldStartDate.getFullYear()) * 12 + (oldEndDate.getMonth() - oldStartDate.getMonth())
const newEndDate = new Date(newStartDate)
newEndDate.setMonth(newEndDate.getMonth() + monthsDiff)
this.rentalForm = {
id: '',
roomId: rental.roomId,
tenantName: rental.tenantName,
startDate: newStartDate,
endDate: newEndDate,
rent: rental.rent,
deposit: rental.deposit,
status: 'active',
remark: '续租'
}
this.rentalDialogVisible = true
},
async handleSave() {
try {
if (this.rentalForm.id) {
await rentalApi.update(this.rentalForm.id, this.rentalForm)
this.$message.success('租赁记录更新成功')
} else {
await rentalApi.create(this.rentalForm)
this.$message.success('租赁记录添加成功')
}
this.rentalDialogVisible = false
this.loadData()
} catch (error) {
this.$message.error('操作失败')
}
},
async handleDelete(id) {
this.$confirm('确定要删除这条租赁记录吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'danger'
}).then(async () => {
try {
await rentalApi.delete(id)
this.$message.success('租赁记录删除成功')
this.loadData()
} catch (error) {
this.$message.error('删除失败')
}
}).catch(() => {})
},
getDefaultTenantName() {
const userInfo = getUserInfo()
return userInfo && userInfo.nickname ? userInfo.nickname : ''
}
}
}
</script>
<style scoped>
.rental-archive {
padding: 0;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.search-form {
margin-bottom: 20px;
}
.pagination-wrapper {
margin-top: 20px;
display: flex;
justify-content: flex-end;
}
/* 移动端适配 */
@media screen and (max-width: 768px) {
.search-form .el-form-item {
display: block;
margin-right: 0;
margin-bottom: 10px;
}
.search-form .el-form-item__content {
width: 100%;
}
.search-form .el-select,
.search-form .el-date-picker {
width: 100%;
}
.pagination-wrapper {
justify-content: center;
}
}
</style>

View File

@ -1,479 +0,0 @@
<template>
<div class="water-archive">
<el-card>
<template slot="header">
<div class="card-header">
<span>水费档案</span>
<el-button type="primary" size="small" @click="handleAdd" v-if="hasPermission('water:add')">添加水费</el-button>
</div>
</template>
<el-form :inline="true" :model="searchForm" class="search-form">
<el-form-item label="公寓">
<el-select v-model="searchForm.apartmentId" placeholder="请选择公寓" @change="handleApartmentChange">
<el-option label="全部" value=""></el-option>
<el-option v-for="apartment in apartments" :key="apartment.id" :label="apartment.name" :value="apartment.id"></el-option>
</el-select>
</el-form-item>
<el-form-item label="房间">
<el-select v-model="searchForm.roomId" placeholder="请选择房间" :disabled="!searchForm.apartmentId">
<el-option label="全部" value=""></el-option>
<el-option v-for="room in rooms" :key="room.id" :label="room.roomNumber" :value="room.id"></el-option>
</el-select>
</el-form-item>
<el-form-item label="计费周期">
<el-date-picker
v-model="searchForm.billingPeriod"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="yyyy-MM-dd">
</el-date-picker>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="searchForm.status" placeholder="请选择状态">
<el-option label="全部" value=""></el-option>
<el-option label="未出账" value="unbilled"></el-option>
<el-option label="未支付" value="unpaid"></el-option>
<el-option label="已支付" value="paid"></el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">搜索</el-button>
<el-button @click="resetSearch">重置</el-button>
</el-form-item>
</el-form>
<!-- PC端表格 -->
<div class="table-wrapper hidden-xs-only">
<el-table :data="waterBillList" style="width: 100%" v-loading="isLoading">
<el-table-column label="房间" min-width="150">
<template slot-scope="scope">
{{ getApartmentName(scope.row.roomId) }} - {{ getRoomNumber(scope.row.roomId) }}
</template>
</el-table-column>
<el-table-column prop="startDate" label="开始日期" min-width="100"></el-table-column>
<el-table-column prop="endDate" label="结束日期" min-width="100"></el-table-column>
<el-table-column prop="startReading" label="起始度数" min-width="90"></el-table-column>
<el-table-column prop="endReading" label="结束度数" min-width="90"></el-table-column>
<el-table-column prop="usage" label="用水量(吨)" min-width="100"></el-table-column>
<el-table-column prop="unitPrice" label="单价(元/吨)" min-width="100"></el-table-column>
<el-table-column prop="amount" label="费用(元)" min-width="90">
<template slot-scope="scope">
<span class="amount-value">¥{{ scope.row.amount }}</span>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" min-width="80">
<template slot-scope="scope">
<el-tag :type="getWaterBillStatusType(scope.row.status)" size="small">
{{ getWaterBillStatusText(scope.row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" min-width="150"></el-table-column>
<el-table-column label="操作" min-width="150" fixed="right">
<template slot-scope="scope">
<el-button type="primary" size="mini" @click="handleEdit(scope.row)" v-if="hasPermission('water:edit')">编辑</el-button>
<el-button type="danger" size="mini" @click="handleDelete(scope.row.id)" v-if="hasPermission('water:delete')">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
<!-- 移动端卡片列表 -->
<div class="mobile-list hidden-sm-and-up">
<div v-for="item in waterBillList" :key="item.id" class="mobile-card">
<div class="mobile-card-header">
<span class="mobile-card-title">{{ getApartmentName(item.roomId) }} - {{ getRoomNumber(item.roomId) }}</span>
<el-tag :type="getWaterBillStatusType(item.status)" size="mini">
{{ getWaterBillStatusText(item.status) }}
</el-tag>
</div>
<div class="mobile-card-body">
<div class="mobile-card-item">
<span class="mobile-card-label">计费周期</span>
<span class="mobile-card-value">{{ item.startDate }} {{ item.endDate }}</span>
</div>
<div class="mobile-card-item">
<span class="mobile-card-label">用水量</span>
<span class="mobile-card-value">{{ item.usage }} </span>
</div>
<div class="mobile-card-item">
<span class="mobile-card-label">费用</span>
<span class="mobile-card-value amount-value">¥{{ item.amount }}</span>
</div>
<div class="mobile-card-item">
<span class="mobile-card-label">读数</span>
<span class="mobile-card-value">{{ item.startReading }} {{ item.endReading }}</span>
</div>
<div class="mobile-card-item">
<span class="mobile-card-label">单价</span>
<span class="mobile-card-value">¥{{ item.unitPrice }}/</span>
</div>
</div>
<div class="mobile-card-footer">
<el-button type="primary" size="mini" @click="handleEdit(item)" v-if="hasPermission('water:edit')">编辑</el-button>
<el-button type="danger" size="mini" @click="handleDelete(item.id)" v-if="hasPermission('water:delete')">删除</el-button>
</div>
</div>
</div>
<div class="pagination-wrapper">
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="currentPage"
:page-sizes="[10, 20, 50, 100]"
:page-size="pageSize"
:layout="isMobile ? 'prev, pager, next' : 'total, sizes, prev, pager, next, jumper'"
:total="total">
</el-pagination>
</div>
</el-card>
<el-dialog :title="waterBillForm.id ? '编辑水费' : '添加水费'" :visible.sync="waterBillDialogVisible" width="500px">
<el-form :model="waterBillForm" :rules="waterBillRules" ref="waterBillForm" label-width="120px">
<el-form-item label="房间" prop="roomId">
<el-select v-model="waterBillForm.roomId" placeholder="请选择房间" style="width: 100%">
<el-option v-for="room in allRooms" :key="room.id" :label="`${getApartmentName(room.id)} - ${room.roomNumber}`" :value="room.id"></el-option>
</el-select>
</el-form-item>
<el-form-item label="开始日期" prop="startDate">
<el-date-picker v-model="waterBillForm.startDate" type="date" placeholder="选择开始日期" style="width: 100%"></el-date-picker>
</el-form-item>
<el-form-item label="结束日期" prop="endDate">
<el-date-picker v-model="waterBillForm.endDate" type="date" placeholder="选择结束日期" style="width: 100%"></el-date-picker>
</el-form-item>
<el-form-item label="起始度数" prop="startReading">
<el-input v-model="waterBillForm.startReading" placeholder="请输入起始度数"></el-input>
</el-form-item>
<el-form-item label="结束度数" prop="endReading">
<el-input v-model="waterBillForm.endReading" placeholder="请输入结束度数"></el-input>
</el-form-item>
<el-form-item label="单价(元/吨)" prop="unitPrice">
<el-input v-model="waterBillForm.unitPrice" placeholder="请输入单价"></el-input>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="waterBillForm.status" placeholder="请选择状态" style="width: 100%">
<el-option label="未出账" value="unbilled"></el-option>
<el-option label="未支付" value="unpaid"></el-option>
<el-option label="已支付" value="paid"></el-option>
</el-select>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="waterBillDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSave">保存</el-button>
</span>
</el-dialog>
</div>
</template>
<script>
import { waterBillApi, apartmentApi, roomApi } from '../../api/api'
import { hasPermission } from '../../utils/permission'
export default {
name: 'WaterArchive',
data() {
return {
waterBillList: [],
apartments: [],
rooms: [],
allRooms: [],
isLoading: false,
currentPage: 1,
pageSize: 10,
total: 0,
searchForm: {
apartmentId: '',
roomId: '',
billingPeriod: '',
status: ''
},
waterBillDialogVisible: false,
waterBillForm: {
id: '',
roomId: '',
startDate: '',
endDate: '',
startReading: '',
endReading: '',
unitPrice: '',
status: 'unbilled'
},
waterBillRules: {
roomId: [{ required: true, message: '请选择房间', trigger: 'change' }],
startDate: [{ required: true, message: '请选择开始日期', trigger: 'change' }],
startReading: [
{ required: true, message: '请输入起始度数', trigger: 'blur' },
{ pattern: /^\d+(\.\d{1,2})?$/, message: '请输入有效的数字,最多保留两位小数', trigger: 'blur' }
],
endReading: [
{ pattern: /^\d+(\.\d{1,2})?$/, message: '请输入有效的数字,最多保留两位小数', trigger: 'blur' }
],
unitPrice: [
{ required: true, message: '请输入单价', trigger: 'blur' },
{ pattern: /^\d+(\.\d{1,2})?$/, message: '请输入有效的数字,最多保留两位小数', trigger: 'blur' }
],
status: [{ required: true, message: '请选择状态', trigger: 'change' }]
}
}
},
computed: {
isMobile() {
return window.innerWidth <= 768
}
},
mounted() {
this.loadApartments()
this.loadAllRooms()
this.loadData()
window.addEventListener('resize', this.handleResize)
},
beforeDestroy() {
window.removeEventListener('resize', this.handleResize)
},
methods: {
hasPermission,
formatDate(date) {
if (!date) return null
const d = new Date(date)
const year = d.getFullYear()
const month = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
},
getWaterBillStatusType(status) {
switch (status) {
case 'paid': return 'success'
case 'unpaid': return 'warning'
case 'unbilled': return 'info'
default: return ''
}
},
getWaterBillStatusText(status) {
switch (status) {
case 'paid': return '已支付'
case 'unpaid': return '未支付'
case 'unbilled': return '未出账'
default: return status
}
},
handleResize() {
this.$forceUpdate()
},
async loadApartments() {
try {
const response = await apartmentApi.list()
this.apartments = response.data || response
} catch (error) {
this.$message.error('加载公寓数据失败')
}
},
async loadAllRooms() {
try {
const response = await roomApi.list()
this.allRooms = response.data || response
} catch (error) {
this.$message.error('加载房间数据失败')
}
},
async loadRooms() {
if (!this.searchForm.apartmentId) {
this.rooms = []
return
}
try {
const response = await roomApi.list({ apartmentId: this.searchForm.apartmentId })
this.rooms = response.data || response
} catch (error) {
this.$message.error('加载房间数据失败')
}
},
handleApartmentChange() {
this.searchForm.roomId = ''
this.loadRooms()
},
async loadData() {
this.isLoading = true
try {
const params = {
page: this.currentPage,
pageSize: this.pageSize
}
if (this.searchForm.apartmentId) {
params.apartmentId = this.searchForm.apartmentId
}
if (this.searchForm.roomId) {
params.roomId = this.searchForm.roomId
}
if (this.searchForm.status) {
params.status = this.searchForm.status
}
if (this.searchForm.billingPeriod && Array.isArray(this.searchForm.billingPeriod) && this.searchForm.billingPeriod.length === 2) {
params.startDateFrom = this.searchForm.billingPeriod[0]
params.endDateTo = this.searchForm.billingPeriod[1]
}
const response = await waterBillApi.getAll(params)
// : { data: { list: [], total: n, page: n, pageSize: n } }
this.waterBillList = response.data.list
this.total = response.data.total
} catch (error) {
this.$message.error('加载数据失败')
} finally {
this.isLoading = false
}
},
handleSearch() {
this.currentPage = 1
this.loadData()
},
resetSearch() {
this.searchForm = {
apartmentId: '',
roomId: '',
billingPeriod: '',
status: ''
}
this.rooms = []
this.currentPage = 1
this.loadData()
},
handleSizeChange(size) {
this.pageSize = size
this.currentPage = 1
this.loadData()
},
handleCurrentChange(current) {
this.currentPage = current
this.loadData()
},
getApartmentName(roomId) {
const room = this.allRooms.find(r => r.id == roomId)
if (room) {
const apartment = this.apartments.find(a => a.id == room.apartmentId)
return apartment ? apartment.name : ''
}
return ''
},
getRoomNumber(roomId) {
const room = this.allRooms.find(r => r.id == roomId)
return room ? room.roomNumber : ''
},
handleAdd() {
this.waterBillForm = {
id: '',
roomId: '',
startDate: new Date(),
endDate: '',
startReading: '',
endReading: '',
unitPrice: '',
status: 'unbilled'
}
this.waterBillDialogVisible = true
},
handleEdit(waterBill) {
this.waterBillForm = {
...waterBill,
startDate: waterBill.startDate ? new Date(waterBill.startDate) : '',
endDate: waterBill.endDate ? new Date(waterBill.endDate) : ''
}
this.waterBillDialogVisible = true
},
async handleSave() {
try {
const data = {
roomId: this.waterBillForm.roomId,
startDate: this.waterBillForm.startDate ? this.formatDate(this.waterBillForm.startDate) : null,
endDate: this.waterBillForm.endDate ? this.formatDate(this.waterBillForm.endDate) : null,
startReading: this.waterBillForm.startReading !== '' ? parseFloat(this.waterBillForm.startReading) : null,
endReading: this.waterBillForm.endReading !== '' ? parseFloat(this.waterBillForm.endReading) : null,
unitPrice: this.waterBillForm.unitPrice !== '' ? parseFloat(this.waterBillForm.unitPrice) : null,
status: this.waterBillForm.status
}
if (this.waterBillForm.id) {
await waterBillApi.update(this.waterBillForm.id, data)
this.$message.success('水费记录更新成功')
} else {
await waterBillApi.create(data)
this.$message.success('水费记录添加成功')
}
this.waterBillDialogVisible = false
this.loadData()
} catch (error) {
this.$message.error('操作失败')
}
},
async handleDelete(id) {
this.$confirm('确定要删除这条水费记录吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'danger'
}).then(async () => {
try {
await waterBillApi.delete(id)
this.$message.success('水费记录删除成功')
this.loadData()
} catch (error) {
this.$message.error('删除失败')
}
}).catch(() => {})
}
}
}
</script>
<style scoped>
.water-archive {
padding: 0;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.search-form {
margin-bottom: 20px;
}
.pagination-wrapper {
margin-top: 20px;
display: flex;
justify-content: flex-end;
}
.amount-value {
color: #f56c6c;
font-weight: bold;
}
/* 移动端适配 */
@media screen and (max-width: 768px) {
.search-form .el-form-item {
display: block;
margin-right: 0;
margin-bottom: 10px;
}
.search-form .el-form-item__content {
width: 100%;
}
.search-form .el-select,
.search-form .el-date-picker {
width: 100%;
}
.pagination-wrapper {
justify-content: center;
}
}
</style>

View File

@ -47,6 +47,7 @@
</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" min-width="160"></el-table-column>
<el-table-column label="操作" width="150" fixed="right">
<template slot-scope="scope">
<el-button size="mini" type="primary" @click="handleEdit(scope.row.id)" v-if="hasPermission('resident:edit')">编辑</el-button>
@ -73,6 +74,10 @@
<span class="mobile-card-label">身份证:</span>
<span class="mobile-card-value">{{ item.idCard || '-' }}</span>
</div>
<div class="mobile-card-item">
<span class="mobile-card-label">创建时间:</span>
<span class="mobile-card-value">{{ item.createTime || '-' }}</span>
</div>
</div>
<div class="mobile-card-footer">
<el-button size="mini" type="primary" @click="handleEdit(item.id)" v-if="hasPermission('resident:edit')">编辑</el-button>