This commit is contained in:
wangxiaoxian 2026-03-05 23:34:34 +08:00
parent 0a839183bc
commit 058aa4333d
20 changed files with 99 additions and 1409 deletions

View File

@ -35,14 +35,6 @@
<i class="el-icon-key"></i>
<span>租房管理</span>
</el-menu-item>
<el-menu-item index="tenant-list">
<i class="el-icon-user"></i>
<span>租客档案</span>
</el-menu-item>
<el-menu-item index="contract-list">
<i class="el-icon-document"></i>
<span>合同档案</span>
</el-menu-item>
<el-menu-item index="rent-statistics">
<i class="el-icon-data-analysis"></i>
<span>租金统计</span>
@ -83,10 +75,6 @@ export default {
'room-add': '/room/add',
'rental-list': '/rental/list',
'rental-add': '/rental/add',
'tenant-list': '/tenant/list',
'tenant-add': '/tenant/add',
'contract-list': '/contract/list',
'contract-add': '/contract/add',
'rent-statistics': '/statistics/rent',
'room-statistics': '/statistics/room'
}

View File

@ -78,38 +78,6 @@ const routes = [
name: 'RentalDetail',
component: () => import('../views/rental/Detail.vue')
},
// 租客管理
{
path: '/tenant/list',
name: 'TenantList',
component: () => import('../views/tenant/List.vue')
},
{
path: '/tenant/add',
name: 'TenantAdd',
component: () => import('../views/tenant/Add.vue')
},
{
path: '/tenant/edit/:id',
name: 'TenantEdit',
component: () => import('../views/tenant/Edit.vue')
},
// 合同管理
{
path: '/contract/list',
name: 'ContractList',
component: () => import('../views/contract/List.vue')
},
{
path: '/contract/add',
name: 'ContractAdd',
component: () => import('../views/contract/Add.vue')
},
{
path: '/contract/edit/:id',
name: 'ContractEdit',
component: () => import('../views/contract/Edit.vue')
},
// 统计分析
{
path: '/statistics/rent',

View File

@ -2,7 +2,7 @@
<div class="dashboard">
<el-card class="welcome-card">
<h2>欢迎使用租房管理系统</h2>
<p>本系统提供区域管理房源管理客管理合同管理和统计分析等功能</p>
<p>本系统提供区域管理房源管理管理和统计分析等功能</p>
</el-card>
<el-row :gutter="20" style="margin-top: 20px;">
@ -39,28 +39,7 @@
</div>
</el-card>
</el-col>
<el-col :xs="24" :sm="12" :md="8">
<el-card class="stat-card">
<div class="stat-item">
<el-icon class="el-icon-user"></el-icon>
<div class="stat-info">
<div class="stat-value">{{ tenantCount }}</div>
<div class="stat-label">租客数量</div>
</div>
</div>
</el-card>
</el-col>
<el-col :xs="24" :sm="12" :md="8">
<el-card class="stat-card">
<div class="stat-item">
<el-icon class="el-icon-document"></el-icon>
<div class="stat-info">
<div class="stat-value">{{ contractCount }}</div>
<div class="stat-label">合同数量</div>
</div>
</div>
</el-card>
</el-col>
<el-col :xs="24" :sm="12" :md="8">
<el-card class="stat-card">
<div class="stat-item">
@ -83,6 +62,28 @@
</div>
</el-card>
</el-col>
<el-col :xs="24" :sm="12" :md="8">
<el-card class="stat-card">
<div class="stat-item">
<el-icon class="el-icon-s-finance"></el-icon>
<div class="stat-info">
<div class="stat-value">{{ soonExpireRoomCount }}</div>
<div class="stat-label">即将到期</div>
</div>
</div>
</el-card>
</el-col>
<el-col :xs="24" :sm="12" :md="8">
<el-card class="stat-card">
<div class="stat-item">
<el-icon class="el-icon-s-finance"></el-icon>
<div class="stat-info">
<div class="stat-value">{{ expiredRoomCount }}</div>
<div class="stat-label">已到期</div>
</div>
</div>
</el-card>
</el-col>
<el-col :xs="24" :sm="12" :md="8">
<el-card class="stat-card">
<div class="stat-item">
@ -113,7 +114,7 @@
<span>区域公寓房间状态分布</span>
</div>
</template>
<el-table :data="regionApartmentHouseStats" style="width: 100%">
<el-table :data="regionApartmentHouseStats" style="width: 100%" show-summary :summary-method="getSummary">
<el-table-column prop="region" label="区域" width="180"></el-table-column>
<el-table-column prop="apartment" label="公寓" width="180"></el-table-column>
<el-table-column prop="empty" label="空房" width="80"></el-table-column>
@ -138,10 +139,10 @@ export default {
regionCount: 0,
apartmentCount: 0,
roomCount: 0,
tenantCount: 0,
contractCount: 0,
emptyRoomCount: 0,
rentedRoomCount: 0,
soonExpireRoomCount: 0,
expiredRoomCount: 0,
collectedRentAmount: 0,
collectedWaterAmount: 0,
regionApartmentHouseStats: []
@ -166,16 +167,40 @@ export default {
this.regionCount = dashboardStats.regionCount
this.apartmentCount = dashboardStats.apartmentCount
this.roomCount = dashboardStats.roomCount
this.tenantCount = dashboardStats.tenantCount
this.contractCount = dashboardStats.contractCount
this.emptyRoomCount = dashboardStats.emptyRoomCount
this.rentedRoomCount = dashboardStats.rentedRoomCount
this.soonExpireRoomCount = dashboardStats.soonExpireRoomCount
this.expiredRoomCount = dashboardStats.expiredRoomCount
this.collectedRentAmount = dashboardStats.collectedRentAmount
this.collectedWaterAmount = dashboardStats.collectedWaterAmount
this.regionApartmentHouseStats = regionApartmentHouseStatsResponse
} catch (error) {
this.$message.error('加载数据失败')
}
},
getSummary(param) {
const { columns, data } = param;
const sums = [];
columns.forEach((column, index) => {
if (index === 0) {
sums[index] = '合计';
return;
}
if (index === 1) {
sums[index] = '';
return;
}
const values = data.map(item => Number(item[column.property]) || 0);
if (values.every(value => !isNaN(value))) {
sums[index] = values.reduce((prev, curr) => {
const value = Number(curr) || 0;
return prev + value;
}, 0);
} else {
sums[index] = '';
}
});
return sums;
}
}
}

View File

@ -46,12 +46,7 @@ export default {
{ required: true, message: '请选择区域', trigger: 'blur' }
],
name: [
{ required: true, message: '请输入公寓名称', trigger: 'blur' },
{ min: 2, max: 20, message: '长度在 2 到 20 个字符', trigger: 'blur' }
],
address: [
{ required: true, message: '请输入公寓地址', trigger: 'blur' },
{ min: 5, max: 100, message: '长度在 5 到 100 个字符', trigger: 'blur' }
{ required: true, message: '请输入公寓名称', trigger: 'blur' }
]
}
}

View File

@ -47,12 +47,7 @@ export default {
{ required: true, message: '请选择区域', trigger: 'blur' }
],
name: [
{ required: true, message: '请输入公寓名称', trigger: 'blur' },
{ min: 2, max: 20, message: '长度在 2 到 20 个字符', trigger: 'blur' }
],
address: [
{ required: true, message: '请输入公寓地址', trigger: 'blur' },
{ min: 5, max: 100, message: '长度在 5 到 100 个字符', trigger: 'blur' }
{ required: true, message: '请输入公寓名称', trigger: 'blur' }
]
}
}

View File

@ -1,139 +0,0 @@
<template>
<div class="contract-add">
<el-card>
<template slot="header">
<div class="card-header">
<span>添加合同</span>
</div>
</template>
<el-form :model="contractForm" :rules="rules" ref="contractForm" label-width="120px">
<el-form-item label="房间" prop="roomId">
<el-select v-model="contractForm.roomId" placeholder="请选择房间">
<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="租客" prop="tenantId">
<el-select v-model="contractForm.tenantId" placeholder="请选择租客">
<el-option v-for="tenant in tenants" :key="tenant.id" :label="tenant.name" :value="tenant.id"></el-option>
</el-select>
</el-form-item>
<el-form-item label="开始日期" prop="startDate">
<el-date-picker v-model="contractForm.startDate" type="date" placeholder="选择开始日期" style="width: 100%"></el-date-picker>
</el-form-item>
<el-form-item label="结束日期" prop="endDate">
<el-date-picker v-model="contractForm.endDate" type="date" placeholder="选择结束日期" style="width: 100%"></el-date-picker>
</el-form-item>
<el-form-item label="租金(元/月)" prop="rent">
<el-input type="number" v-model="contractForm.rent" placeholder="请输入租金"></el-input>
</el-form-item>
<el-form-item label="押金(元)" prop="deposit">
<el-input type="number" v-model="contractForm.deposit" placeholder="请输入押金"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm">提交</el-button>
<el-button @click="resetForm">重置</el-button>
<el-button @click="goBack">返回</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</template>
<script>
import { contractApi, tenantApi, roomApi } from '../../api/api'
export default {
name: 'ContractAdd',
data() {
return {
contractForm: {
houseId: '',
tenantId: '',
startDate: '',
endDate: '',
rent: '',
deposit: ''
},
tenants: [],
rooms: [],
rules: {
houseId: [
{ required: true, message: '请选择房间', trigger: 'blur' }
],
tenantId: [
{ required: true, message: '请选择租客', trigger: 'blur' }
],
startDate: [
{ required: true, message: '请选择开始日期', trigger: 'blur' }
],
endDate: [
{ required: true, message: '请选择结束日期', trigger: 'blur' }
],
rent: [
{ required: true, message: '请输入租金', trigger: 'blur' },
{ type: 'number', message: '请输入数字', trigger: 'blur' }
],
deposit: [
{ required: true, message: '请输入押金', trigger: 'blur' },
{ type: 'number', message: '请输入数字', trigger: 'blur' }
]
}
}
},
mounted() {
this.loadTenants()
this.loadRooms()
},
methods: {
async loadTenants() {
try {
const response = await tenantApi.list()
this.tenants = response
} catch (error) {
this.$message.error('加载租客数据失败')
}
},
async loadRooms() {
try {
const response = await roomApi.list()
this.rooms = response
} catch (error) {
this.$message.error('加载房间数据失败')
}
},
async submitForm() {
this.$refs.contractForm.validate(async (valid) => {
if (valid) {
try {
await contractApi.create(this.contractForm)
this.$message.success('添加成功')
this.$router.push('/contract/list')
} catch (error) {
this.$message.error('添加失败')
}
} else {
return false
}
})
},
resetForm() {
this.$refs.contractForm.resetFields()
},
goBack() {
this.$router.push('/contract/list')
}
}
}
</script>
<style scoped>
.contract-add {
padding: 20px 0;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
</style>

View File

@ -1,152 +0,0 @@
<template>
<div class="contract-edit">
<el-card>
<template slot="header">
<div class="card-header">
<span>编辑合同</span>
</div>
</template>
<el-form :model="contractForm" :rules="rules" ref="contractForm" label-width="120px">
<el-form-item label="房间" prop="roomId">
<el-select v-model="contractForm.roomId" placeholder="请选择房间">
<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="租客" prop="tenantId">
<el-select v-model="contractForm.tenantId" placeholder="请选择租客">
<el-option v-for="tenant in tenants" :key="tenant.id" :label="tenant.name" :value="tenant.id"></el-option>
</el-select>
</el-form-item>
<el-form-item label="开始日期" prop="startDate">
<el-date-picker v-model="contractForm.startDate" type="date" placeholder="选择开始日期" style="width: 100%"></el-date-picker>
</el-form-item>
<el-form-item label="结束日期" prop="endDate">
<el-date-picker v-model="contractForm.endDate" type="date" placeholder="选择结束日期" style="width: 100%"></el-date-picker>
</el-form-item>
<el-form-item label="租金(元/月)" prop="rent">
<el-input type="number" v-model="contractForm.rent" placeholder="请输入租金"></el-input>
</el-form-item>
<el-form-item label="押金(元)" prop="deposit">
<el-input type="number" v-model="contractForm.deposit" placeholder="请输入押金"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm">提交</el-button>
<el-button @click="resetForm">重置</el-button>
<el-button @click="goBack">返回</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</template>
<script>
import { contractApi, tenantApi, roomApi } from '../../api/api'
export default {
name: 'ContractEdit',
data() {
return {
contractForm: {
id: '',
houseId: '',
tenantId: '',
startDate: '',
endDate: '',
rent: '',
deposit: ''
},
tenants: [],
rooms: [],
rules: {
houseId: [
{ required: true, message: '请选择房间', trigger: 'blur' }
],
tenantId: [
{ required: true, message: '请选择租客', trigger: 'blur' }
],
startDate: [
{ required: true, message: '请选择开始日期', trigger: 'blur' }
],
endDate: [
{ required: true, message: '请选择结束日期', trigger: 'blur' }
],
rent: [
{ required: true, message: '请输入租金', trigger: 'blur' },
{ type: 'number', message: '请输入数字', trigger: 'blur' }
],
deposit: [
{ required: true, message: '请输入押金', trigger: 'blur' },
{ type: 'number', message: '请输入数字', trigger: 'blur' }
]
}
}
},
mounted() {
this.loadTenants()
this.loadRooms()
this.loadContractData()
},
methods: {
async loadTenants() {
try {
const response = await tenantApi.list()
this.tenants = response
} catch (error) {
this.$message.error('加载租客数据失败')
}
},
async loadRooms() {
try {
const response = await roomApi.list()
this.rooms = response
} catch (error) {
this.$message.error('加载房间数据失败')
}
},
async loadContractData() {
try {
const id = this.$route.params.id
const contract = await contractApi.getById(id)
if (contract) {
this.contractForm = contract
}
} catch (error) {
this.$message.error('加载合同数据失败')
}
},
async submitForm() {
this.$refs.contractForm.validate(async (valid) => {
if (valid) {
try {
await contractApi.update(this.contractForm.id, this.contractForm)
this.$message.success('编辑成功')
this.$router.push('/contract/list')
} catch (error) {
this.$message.error('编辑失败')
}
} else {
return false
}
})
},
resetForm() {
this.loadContractData()
},
goBack() {
this.$router.push('/contract/list')
}
}
}
</script>
<style scoped>
.contract-edit {
padding: 20px 0;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
</style>

View File

@ -1,171 +0,0 @@
<template>
<div class="contract-list">
<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.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>
<el-table :data="contracts" style="width: 100%" v-loading="isLoading">
<el-table-column prop="id" label="ID" width="80"></el-table-column>
<el-table-column prop="regionName" label="区域"></el-table-column>
<el-table-column prop="apartmentName" label="公寓"></el-table-column>
<el-table-column prop="roomNumber" label="房间号"></el-table-column>
<el-table-column prop="tenantName" label="租客姓名"></el-table-column>
<el-table-column prop="startDate" label="开始日期" width="150"></el-table-column>
<el-table-column prop="endDate" label="结束日期" width="150"></el-table-column>
<el-table-column prop="rent" label="租金(元/月)" width="120"></el-table-column>
<el-table-column prop="deposit" label="押金(元)" width="120"></el-table-column>
<el-table-column prop="status" label="状态" width="100">
<template slot-scope="scope">
<el-tag :type="scope.row.status === 'active' ? 'success' : 'danger'">{{ scope.row.status === 'active' ? '有效' : '过期' }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="180"></el-table-column>
</el-table>
<div class="pagination" style="margin-top: 20px;">
<el-pagination
layout="total, sizes, prev, pager, next, jumper"
:total="total"
:page-size="pageSize"
:page-sizes="[10, 20, 50, 100]"
:current-page="currentPage"
@current-change="handleCurrentChange"
@size-change="handleSizeChange"
></el-pagination>
</div>
</el-card>
</div>
</template>
<script>
import { contractApi } from '../../api/api'
export default {
name: 'ContractList',
data() {
return {
contracts: [],
total: 0,
searchForm: {
status: ''
},
currentPage: 1,
pageSize: 10,
isLoading: false
}
},
mounted() {
this.loadContracts()
},
methods: {
async loadContracts() {
this.isLoading = true
try {
//
const params = {
status: this.searchForm.status,
page: this.currentPage,
pageSize: this.pageSize
}
const contractsResponse = await contractApi.getAll(params)
// 使
const contracts = contractsResponse.data || contractsResponse
this.contracts = contracts.map(contract => {
return {
...contract,
roomNumber: contract.Room ? contract.Room.roomNumber : '',
tenantName: contract.Tenant ? contract.Tenant.name : '',
apartmentName: contract.Room && contract.Room.Apartment ? contract.Room.Apartment.name : '',
regionName: contract.Room && contract.Room.Apartment && contract.Room.Apartment.Region ? contract.Room.Apartment.Region.name : ''
}
})
this.total = contractsResponse.total || 0
} catch (error) {
this.$message.error('加载数据失败')
} finally {
this.isLoading = false
}
},
handleAdd() {
this.$router.push('/contract/add')
},
handleEdit(id) {
this.$router.push(`/contract/edit/${id}`)
},
async handleDelete(id) {
this.$confirm('确定要删除这个合同吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
try {
await contractApi.delete(id)
this.$message.success('删除成功')
this.loadContracts()
} catch (error) {
this.$message.error('删除失败')
}
}).catch(() => {
//
})
},
handleSearch() {
this.currentPage = 1
this.loadContracts()
},
resetSearch() {
this.searchForm = {
status: ''
}
this.currentPage = 1
this.loadContracts()
},
handleCurrentChange(val) {
this.currentPage = val
this.loadContracts()
},
handleSizeChange(val) {
this.pageSize = val
this.currentPage = 1
this.loadContracts()
}
}
}
</script>
<style scoped>
.contract-list {
padding: 20px 0;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.search-form {
margin-bottom: 20px;
}
.pagination {
display: flex;
justify-content: flex-end;
}
</style>

View File

@ -1,125 +0,0 @@
<template>
<div class="house-add">
<el-card>
<template slot="header">
<div class="card-header">
<span>添加房源</span>
</div>
</template>
<el-form :model="houseForm" :rules="rules" ref="houseForm" label-width="100px">
<el-form-item label="区域" prop="regionId">
<el-select v-model="houseForm.regionId" placeholder="请选择区域">
<el-option v-for="region in regions" :key="region.id" :label="region.name" :value="region.id"></el-option>
</el-select>
</el-form-item>
<el-form-item label="地址" prop="address">
<el-input v-model="houseForm.address" placeholder="请输入地址"></el-input>
</el-form-item>
<el-form-item label="面积(㎡)" prop="area">
<el-input type="number" v-model="houseForm.area" placeholder="请输入面积"></el-input>
</el-form-item>
<el-form-item label="租金(元/月)" prop="price">
<el-input type="number" v-model="houseForm.price" placeholder="请输入租金"></el-input>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="houseForm.status" placeholder="请选择状态">
<el-option label="可租" value="available"></el-option>
<el-option label="已租" value="rented"></el-option>
<el-option label="维护中" value="maintenance"></el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm">提交</el-button>
<el-button @click="resetForm">重置</el-button>
<el-button @click="goBack">返回</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</template>
<script>
import { regionApi, houseApi } from '../../api/api'
export default {
name: 'HouseAdd',
data() {
return {
houseForm: {
regionId: '',
address: '',
area: '',
price: '',
status: 'available'
},
regions: [],
rules: {
regionId: [
{ required: true, message: '请选择区域', trigger: 'blur' }
],
address: [
{ required: true, message: '请输入地址', trigger: 'blur' },
{ min: 5, max: 100, message: '长度在 5 到 100 个字符', trigger: 'blur' }
],
area: [
{ required: true, message: '请输入面积', trigger: 'blur' },
{ type: 'number', message: '请输入数字', trigger: 'blur' }
],
price: [
{ required: true, message: '请输入租金', trigger: 'blur' },
{ type: 'number', message: '请输入数字', trigger: 'blur' }
],
status: [
{ required: true, message: '请选择状态', trigger: 'blur' }
]
}
}
},
mounted() {
this.loadRegions()
},
methods: {
async loadRegions() {
try {
const response = await regionApi.list()
this.regions = response
} catch (error) {
this.$message.error('加载区域数据失败')
}
},
async submitForm() {
this.$refs.houseForm.validate(async (valid) => {
if (valid) {
try {
await houseApi.create(this.houseForm)
this.$message.success('添加成功')
this.$router.push('/house/list')
} catch (error) {
this.$message.error('添加失败')
}
} else {
return false
}
})
},
resetForm() {
this.$refs.houseForm.resetFields()
},
goBack() {
this.$router.push('/house/list')
}
}
}
</script>
<style scoped>
.house-add {
padding: 20px 0;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
</style>

View File

@ -1,138 +0,0 @@
<template>
<div class="house-edit">
<el-card>
<template slot="header">
<div class="card-header">
<span>编辑房源</span>
</div>
</template>
<el-form :model="houseForm" :rules="rules" ref="houseForm" label-width="100px">
<el-form-item label="区域" prop="regionId">
<el-select v-model="houseForm.regionId" placeholder="请选择区域">
<el-option v-for="region in regions" :key="region.id" :label="region.name" :value="region.id"></el-option>
</el-select>
</el-form-item>
<el-form-item label="地址" prop="address">
<el-input v-model="houseForm.address" placeholder="请输入地址"></el-input>
</el-form-item>
<el-form-item label="面积(㎡)" prop="area">
<el-input type="number" v-model="houseForm.area" placeholder="请输入面积"></el-input>
</el-form-item>
<el-form-item label="租金(元/月)" prop="price">
<el-input type="number" v-model="houseForm.price" placeholder="请输入租金"></el-input>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="houseForm.status" placeholder="请选择状态">
<el-option label="可租" value="available"></el-option>
<el-option label="已租" value="rented"></el-option>
<el-option label="维护中" value="maintenance"></el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm">提交</el-button>
<el-button @click="resetForm">重置</el-button>
<el-button @click="goBack">返回</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</template>
<script>
import { regionApi, houseApi } from '../../api/api'
export default {
name: 'HouseEdit',
data() {
return {
houseForm: {
id: '',
regionId: '',
address: '',
area: '',
price: '',
status: ''
},
regions: [],
rules: {
regionId: [
{ required: true, message: '请选择区域', trigger: 'blur' }
],
address: [
{ required: true, message: '请输入地址', trigger: 'blur' },
{ min: 5, max: 100, message: '长度在 5 到 100 个字符', trigger: 'blur' }
],
area: [
{ required: true, message: '请输入面积', trigger: 'blur' },
{ type: 'number', message: '请输入数字', trigger: 'blur' }
],
price: [
{ required: true, message: '请输入租金', trigger: 'blur' },
{ type: 'number', message: '请输入数字', trigger: 'blur' }
],
status: [
{ required: true, message: '请选择状态', trigger: 'blur' }
]
}
}
},
mounted() {
this.loadRegions()
this.loadHouseData()
},
methods: {
async loadRegions() {
try {
const response = await regionApi.list()
this.regions = response
} catch (error) {
this.$message.error('加载区域数据失败')
}
},
async loadHouseData() {
try {
const id = this.$route.params.id
const house = await houseApi.getById(id)
if (house) {
this.houseForm = house
}
} catch (error) {
this.$message.error('加载房源数据失败')
}
},
async submitForm() {
this.$refs.houseForm.validate(async (valid) => {
if (valid) {
try {
await houseApi.update(this.houseForm.id, this.houseForm)
this.$message.success('编辑成功')
this.$router.push('/house/list')
} catch (error) {
this.$message.error('编辑失败')
}
} else {
return false
}
})
},
resetForm() {
this.loadHouseData()
},
goBack() {
this.$router.push('/house/list')
}
}
}
</script>
<style scoped>
.house-edit {
padding: 20px 0;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
</style>

View File

@ -1,200 +0,0 @@
<template>
<div class="house-list">
<el-card>
<template slot="header">
<div class="card-header">
<span>房源管理</span>
<el-button type="primary" @click="handleAdd">添加房源</el-button>
</div>
</template>
<el-form :inline="true" :model="searchForm" class="search-form">
<el-form-item label="区域">
<el-select v-model="searchForm.regionId" placeholder="请选择区域">
<el-option label="全部" value=""></el-option>
<el-option v-for="region in regions" :key="region.id" :label="region.name" :value="region.id"></el-option>
</el-select>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="searchForm.status" placeholder="请选择状态">
<el-option label="全部" value=""></el-option>
<el-option label="可租" value="available"></el-option>
<el-option label="已租" value="rented"></el-option>
<el-option label="维护中" value="maintenance"></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>
<el-table :data="houses" style="width: 100%" v-loading="isLoading">
<el-table-column prop="id" label="ID" width="80"></el-table-column>
<el-table-column prop="regionName" label="区域"></el-table-column>
<el-table-column prop="address" label="地址"></el-table-column>
<el-table-column prop="area" label="面积(㎡)" width="100"></el-table-column>
<el-table-column prop="price" label="租金(元/月)" width="120"></el-table-column>
<el-table-column prop="status" label="状态" width="100">
<template slot-scope="scope">
<el-tag :type="getStatusType(scope.row.status)">{{ getStatusText(scope.row.status) }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="180"></el-table-column>
<el-table-column label="操作" width="150">
<template slot-scope="scope">
<el-button size="small" type="primary" @click="handleEdit(scope.row.id)">编辑</el-button>
<el-button size="small" type="danger" @click="handleDelete(scope.row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination" style="margin-top: 20px;">
<el-pagination
layout="total, sizes, prev, pager, next, jumper"
:total="total"
:page-size="pageSize"
:page-sizes="[10, 20, 50, 100]"
:current-page="currentPage"
@current-change="handleCurrentChange"
@size-change="handleSizeChange"
></el-pagination>
</div>
</el-card>
</div>
</template>
<script>
import { regionApi, houseApi } from '../../api/api'
export default {
name: 'HouseList',
data() {
return {
regions: [],
houses: [],
total: 0,
searchForm: {
regionId: '',
status: ''
},
currentPage: 1,
pageSize: 10,
isLoading: false
}
},
mounted() {
this.loadData()
},
methods: {
async loadData() {
this.isLoading = true
try {
//
const regionsResponse = await regionApi.getAll()
this.regions = regionsResponse
//
const params = {
regionId: this.searchForm.regionId,
status: this.searchForm.status,
page: this.currentPage,
pageSize: this.pageSize
}
const housesResponse = await houseApi.getAll(params)
const houses = housesResponse.data || housesResponse
this.houses = houses.map(house => {
const region = regionsResponse.find(r => r.id == house.regionId)
return {
...house,
regionName: region ? region.name : ''
}
})
this.total = housesResponse.total || 0
} catch (error) {
this.$message.error('加载数据失败')
} finally {
this.isLoading = false
}
},
getStatusType(status) {
switch (status) {
case 'available': return 'success'
case 'rented': return 'warning'
case 'maintenance': return 'danger'
default: return ''
}
},
getStatusText(status) {
switch (status) {
case 'available': return '可租'
case 'rented': return '已租'
case 'maintenance': return '维护中'
default: return status
}
},
handleAdd() {
this.$router.push('/house/add')
},
handleEdit(id) {
this.$router.push(`/house/edit/${id}`)
},
async handleDelete(id) {
this.$confirm('确定要删除这个房源吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
try {
await houseApi.delete(id)
this.$message.success('删除成功')
this.loadData()
} catch (error) {
this.$message.error('删除失败')
}
}).catch(() => {
//
})
},
handleSearch() {
this.currentPage = 1
this.loadData()
},
resetSearch() {
this.searchForm = {
regionId: '',
status: ''
}
this.currentPage = 1
this.loadData()
},
handleCurrentChange(val) {
this.currentPage = val
this.loadData()
},
handleSizeChange(val) {
this.pageSize = val
this.currentPage = 1
this.loadData()
}
}
}
</script>
<style scoped>
.house-list {
padding: 20px 0;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.search-form {
margin-bottom: 20px;
}
.pagination {
display: flex;
justify-content: flex-end;
}
</style>

View File

@ -36,12 +36,7 @@ export default {
},
rules: {
name: [
{ required: true, message: '请输入区域名称', trigger: 'blur' },
{ min: 2, max: 20, message: '长度在 2 到 20 个字符', trigger: 'blur' }
],
description: [
{ required: true, message: '请输入区域描述', trigger: 'blur' },
{ min: 5, max: 100, message: '长度在 5 到 100 个字符', trigger: 'blur' }
{ required: true, message: '请输入区域名称', trigger: 'blur' }
]
}
}

View File

@ -37,12 +37,7 @@ export default {
},
rules: {
name: [
{ required: true, message: '请输入区域名称', trigger: 'blur' },
{ min: 2, max: 20, message: '长度在 2 到 20 个字符', trigger: 'blur' }
],
description: [
{ required: true, message: '请输入区域描述', trigger: 'blur' },
{ min: 5, max: 100, message: '长度在 5 到 100 个字符', trigger: 'blur' }
{ required: true, message: '请输入区域名称', trigger: 'blur' }
]
}
}

View File

@ -21,12 +21,6 @@
<el-form-item label="租客姓名" prop="tenantName">
<el-input v-model="rentalForm.tenantName" placeholder="请输入租客姓名"></el-input>
</el-form-item>
<el-form-item label="租客电话" prop="tenantPhone">
<el-input v-model="rentalForm.tenantPhone" placeholder="请输入租客电话"></el-input>
</el-form-item>
<el-form-item label="身份证号" prop="tenantIdCard">
<el-input v-model="rentalForm.tenantIdCard" placeholder="请输入身份证号"></el-input>
</el-form-item>
<el-divider>合同信息</el-divider>
<el-form-item label="开始日期" prop="startDate">
<el-date-picker
@ -90,8 +84,6 @@ export default {
rentalForm: {
roomId: '',
tenantName: '',
tenantPhone: '',
tenantIdCard: '',
startDate: '',
endDate: '',
rent: '',
@ -179,12 +171,22 @@ export default {
try {
await rentalApi.create(this.rentalForm)
this.$message.success('添加成功')
//
if (this.rentalForm.roomId) {
this.$router.push({
path: `/rental/detail/${this.rentalForm.roomId}`,
query: this.returnQuery
})
} else {
// ID
this.$router.push({
path: '/rental/list',
query: this.returnQuery
})
}
} catch (error) {
this.$message.error('添加失败')
console.error('添加失败:', error)
this.$message.error('添加失败:' + (error.message || '未知错误'))
}
} else {
this.$message.error('请填写所有必填项')

View File

@ -16,7 +16,7 @@
</div>
</template>
<div v-loading="isLoading" class="room-info-section">
<h2>{{ room.Apartment.name }} - {{ room.roomNumber }}</h2>
<h2>{{ room.Apartment ? room.Apartment.name : '' }} - {{ room.roomNumber }}</h2>
<div class="room-basic-info">
<div class="info-item">
<span class="label">面积:</span>
@ -161,12 +161,6 @@
<el-form-item label="租客姓名" prop="tenantName">
<el-input v-model="rentalForm.tenantName" placeholder="请输入租客姓名"></el-input>
</el-form-item>
<el-form-item label="租客电话" prop="tenantPhone">
<el-input v-model="rentalForm.tenantPhone" placeholder="请输入租客电话"></el-input>
</el-form-item>
<el-form-item label="身份证号" prop="tenantIdCard">
<el-input v-model="rentalForm.tenantIdCard" placeholder="请输入身份证号"></el-input>
</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>
@ -245,8 +239,6 @@ export default {
id: '',
roomId: '',
tenantName: '',
tenantPhone: '',
tenantIdCard: '',
startDate: '',
endDate: '',
rent: '',
@ -338,8 +330,7 @@ export default {
const data = response.data || response
this.rentalHistory = data.map(rental => {
return {
...rental,
tenantName: rental.Tenant ? rental.Tenant.name : ''
...rental
}
})
@ -431,6 +422,11 @@ export default {
})
},
async handleCheckout() {
if (!this.room.id) {
this.$message.error('房间信息加载失败,无法进行退房操作')
return
}
this.$confirm('确定要退房吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
@ -439,7 +435,7 @@ export default {
try {
//
const roomId = this.$route.params.id
const rental = this.rentals.find(r => r.roomId == roomId && r.status === 'active')
const rental = this.rentalHistory.find(r => r.roomId == roomId && r.status === 'active')
if (rental) {
//
@ -454,7 +450,8 @@ export default {
this.$message.error('未找到活跃的租房记录')
}
} catch (error) {
this.$message.error('退房失败')
console.error('退房失败:', error)
this.$message.error('退房失败:' + (error.message || '未知错误'))
}
}).catch(() => {
// 退
@ -635,11 +632,26 @@ export default {
type: 'danger'
}).then(async () => {
try {
//
const rental = this.rentalHistory.find(r => r.id == id)
//
await rentalApi.delete(id)
this.$message.success('租赁记录删除成功')
//
if (rental && rental.status === 'active' && rental.roomId) {
await roomApi.update(rental.roomId, {
status: 'empty',
subStatus: 'normal'
})
this.$message.success('房间状态已更新为空房')
}
this.loadData()
} catch (error) {
this.$message.error('删除失败')
console.error('删除失败:', error)
this.$message.error('删除失败:' + (error.message || '未知错误'))
}
}).catch(() => {
//

View File

@ -21,12 +21,6 @@
<el-form-item label="租客姓名" prop="tenantName">
<el-input v-model="rentalForm.tenantName" placeholder="请输入租客姓名"></el-input>
</el-form-item>
<el-form-item label="租客电话" prop="tenantPhone">
<el-input v-model="rentalForm.tenantPhone" placeholder="请输入租客电话"></el-input>
</el-form-item>
<el-form-item label="身份证号" prop="tenantIdCard">
<el-input v-model="rentalForm.tenantIdCard" placeholder="请输入身份证号"></el-input>
</el-form-item>
<el-divider>合同信息</el-divider>
<el-form-item label="开始日期" prop="startDate">
<el-date-picker
@ -77,8 +71,6 @@ export default {
id: '',
roomId: '',
tenantName: '',
tenantPhone: '',
tenantIdCard: '',
startDate: '',
endDate: '',
rent: '',

View File

@ -71,7 +71,7 @@
</div>
</div>
<div class="rental-info" v-if="room.Rentals && room.Rentals.length > 0">
<p>租客: {{ room.Rentals[0].Tenant.name }}</p>
<p>租客: {{ room.Rentals[0].tenantName }}</p>
<p>租期: {{ room.Rentals[0].startDate }} {{ room.Rentals[0].endDate }}</p>
</div>
<div class="rental-info" v-else>

View File

@ -1,93 +0,0 @@
<template>
<div class="tenant-add">
<el-card>
<template slot="header">
<div class="card-header">
<span>添加租客</span>
</div>
</template>
<el-form :model="tenantForm" :rules="rules" ref="tenantForm" label-width="100px">
<el-form-item label="姓名" prop="name">
<el-input v-model="tenantForm.name" placeholder="请输入姓名"></el-input>
</el-form-item>
<el-form-item label="电话" prop="phone">
<el-input v-model="tenantForm.phone" placeholder="请输入电话"></el-input>
</el-form-item>
<el-form-item label="身份证号" prop="idCard">
<el-input v-model="tenantForm.idCard" placeholder="请输入身份证号"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm">提交</el-button>
<el-button @click="resetForm">重置</el-button>
<el-button @click="goBack">返回</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</template>
<script>
import { tenantApi } from '../../api/api'
export default {
name: 'TenantAdd',
data() {
return {
tenantForm: {
name: '',
phone: '',
idCard: ''
},
rules: {
name: [
{ required: true, message: '请输入姓名', trigger: 'blur' },
{ min: 2, max: 10, message: '长度在 2 到 10 个字符', trigger: 'blur' }
],
phone: [
{ required: true, message: '请输入电话', trigger: 'blur' },
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的电话号码', trigger: 'blur' }
],
idCard: [
{ required: true, message: '请输入身份证号', trigger: 'blur' },
{ pattern: /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/, message: '请输入正确的身份证号码', trigger: 'blur' }
]
}
}
},
methods: {
async submitForm() {
this.$refs.tenantForm.validate(async (valid) => {
if (valid) {
try {
await tenantApi.create(this.tenantForm)
this.$message.success('添加成功')
this.$router.push('/tenant/list')
} catch (error) {
this.$message.error('添加失败')
}
} else {
return false
}
})
},
resetForm() {
this.$refs.tenantForm.resetFields()
},
goBack() {
this.$router.push('/tenant/list')
}
}
}
</script>
<style scoped>
.tenant-add {
padding: 20px 0;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
</style>

View File

@ -1,108 +0,0 @@
<template>
<div class="tenant-edit">
<el-card>
<template slot="header">
<div class="card-header">
<span>编辑租客</span>
</div>
</template>
<el-form :model="tenantForm" :rules="rules" ref="tenantForm" label-width="100px">
<el-form-item label="姓名" prop="name">
<el-input v-model="tenantForm.name" placeholder="请输入姓名"></el-input>
</el-form-item>
<el-form-item label="电话" prop="phone">
<el-input v-model="tenantForm.phone" placeholder="请输入电话"></el-input>
</el-form-item>
<el-form-item label="身份证号" prop="idCard">
<el-input v-model="tenantForm.idCard" placeholder="请输入身份证号"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm">提交</el-button>
<el-button @click="resetForm">重置</el-button>
<el-button @click="goBack">返回</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</template>
<script>
import { tenantApi } from '../../api/api'
export default {
name: 'TenantEdit',
data() {
return {
tenantForm: {
id: '',
name: '',
phone: '',
idCard: ''
},
rules: {
name: [
{ required: true, message: '请输入姓名', trigger: 'blur' },
{ min: 2, max: 10, message: '长度在 2 到 10 个字符', trigger: 'blur' }
],
phone: [
{ required: true, message: '请输入电话', trigger: 'blur' },
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的电话号码', trigger: 'blur' }
],
idCard: [
{ required: true, message: '请输入身份证号', trigger: 'blur' },
{ pattern: /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/, message: '请输入正确的身份证号码', trigger: 'blur' }
]
}
}
},
mounted() {
this.loadTenantData()
},
methods: {
async loadTenantData() {
try {
const id = this.$route.params.id
const tenant = await tenantApi.getById(id)
if (tenant) {
this.tenantForm = tenant
}
} catch (error) {
this.$message.error('加载租客数据失败')
}
},
async submitForm() {
this.$refs.tenantForm.validate(async (valid) => {
if (valid) {
try {
await tenantApi.update(this.tenantForm.id, this.tenantForm)
this.$message.success('编辑成功')
this.$router.push('/tenant/list')
} catch (error) {
this.$message.error('编辑失败')
}
} else {
return false
}
})
},
resetForm() {
this.loadTenantData()
},
goBack() {
this.$router.push('/tenant/list')
}
}
}
</script>
<style scoped>
.tenant-edit {
padding: 20px 0;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
</style>

View File

@ -1,151 +0,0 @@
<template>
<div class="tenant-list">
<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-input v-model="searchForm.name" placeholder="请输入姓名"></el-input>
</el-form-item>
<el-form-item label="电话">
<el-input v-model="searchForm.phone" placeholder="请输入电话"></el-input>
</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>
<el-table :data="tenants" style="width: 100%" v-loading="isLoading">
<el-table-column prop="id" label="ID" width="80"></el-table-column>
<el-table-column prop="name" label="姓名"></el-table-column>
<el-table-column prop="phone" label="电话"></el-table-column>
<el-table-column prop="idCard" label="身份证号"></el-table-column>
<el-table-column prop="createTime" label="创建时间" width="180"></el-table-column>
</el-table>
<div class="pagination" style="margin-top: 20px;">
<el-pagination
layout="total, sizes, prev, pager, next, jumper"
:total="total"
:page-size="pageSize"
:page-sizes="[10, 20, 50, 100]"
:current-page="currentPage"
@current-change="handleCurrentChange"
@size-change="handleSizeChange"
></el-pagination>
</div>
</el-card>
</div>
</template>
<script>
import { tenantApi } from '../../api/api'
export default {
name: 'TenantList',
data() {
return {
tenants: [],
total: 0,
searchForm: {
name: '',
phone: ''
},
currentPage: 1,
pageSize: 10,
isLoading: false
}
},
mounted() {
this.loadTenants()
},
methods: {
async loadTenants() {
this.isLoading = true
try {
const params = {
name: this.searchForm.name,
phone: this.searchForm.phone,
page: this.currentPage,
pageSize: this.pageSize
}
const response = await tenantApi.getAll(params)
this.tenants = response.data || response
this.total = response.total || 0
} catch (error) {
this.$message.error('加载租客数据失败')
} finally {
this.isLoading = false
}
},
handleAdd() {
this.$router.push('/tenant/add')
},
handleEdit(id) {
this.$router.push(`/tenant/edit/${id}`)
},
async handleDelete(id) {
this.$confirm('确定要删除这个租客吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
try {
await tenantApi.delete(id)
this.$message.success('删除成功')
this.loadTenants()
} catch (error) {
this.$message.error('删除失败')
}
}).catch(() => {
//
})
},
handleSearch() {
this.currentPage = 1
this.loadTenants()
},
resetSearch() {
this.searchForm = {
name: '',
phone: ''
}
this.currentPage = 1
this.loadTenants()
},
handleCurrentChange(val) {
this.currentPage = val
this.loadTenants()
},
handleSizeChange(val) {
this.pageSize = val
this.currentPage = 1
this.loadTenants()
}
}
}
</script>
<style scoped>
.tenant-list {
padding: 20px 0;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.search-form {
margin-bottom: 20px;
}
.pagination {
display: flex;
justify-content: flex-end;
}
</style>