初始化

This commit is contained in:
wangxiaoxian 2026-03-02 20:29:23 +08:00
commit cf9305fd93
35 changed files with 11118 additions and 0 deletions

16
.eslintrc.js Normal file
View File

@ -0,0 +1,16 @@
module.exports = {
root: true,
env: {
node: true
},
extends: [
'plugin:vue/essential'
],
parserOptions: {
parser: 'babel-eslint'
},
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off'
}
}

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
node_modules/
dist/
.idea/

17
index.html Normal file
View File

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title>租房管理系统</title>
</head>
<body>
<noscript>
<strong>We're sorry but rentease-web doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

6419
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

24
package.json Normal file
View File

@ -0,0 +1,24 @@
{
"name": "rentease-web",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"axios": "^0.21.1",
"element-ui": "^2.15.6",
"vue": "^2.6.14",
"vue-router": "^3.5.1"
},
"devDependencies": {
"@vue/cli-plugin-eslint": "^5.0.9",
"@vue/cli-plugin-router": "^5.0.9",
"@vue/cli-service": "^5.0.9",
"babel-eslint": "^10.1.0",
"eslint": "^7.32.0",
"eslint-plugin-vue": "^7.14.0"
}
}

123
src/App.vue Normal file
View File

@ -0,0 +1,123 @@
<template>
<div id="app">
<el-container>
<el-header height="60px" style="background-color: #333; color: white; display: flex; align-items: center; justify-content: space-between;">
<h1 style="margin: 0; font-size: 18px;">租房管理系统</h1>
<el-button type="primary" plain @click="logout">退出</el-button>
</el-header>
<el-container>
<el-aside width="200px" style="background-color: #f0f2f5; min-height: calc(100vh - 60px);">
<el-menu
:default-active="activeIndex"
class="el-menu-vertical-demo"
@select="handleSelect"
background-color="#f0f2f5"
text-color="#333"
active-text-color="#409EFF"
>
<el-menu-item index="dashboard">
<i class="el-icon-s-home"></i>
<span>首页</span>
</el-menu-item>
<el-menu-item index="region-list">
<i class="el-icon-location"></i>
<span>区域管理</span>
</el-menu-item>
<el-menu-item index="apartment-list">
<i class="el-icon-office-building"></i>
<span>公寓管理</span>
</el-menu-item>
<el-menu-item index="room-list">
<i class="el-icon-menu"></i>
<span>房间管理</span>
</el-menu-item>
<el-menu-item index="rental-list">
<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>
</el-menu-item>
<el-menu-item index="room-statistics">
<i class="el-icon-data-analysis"></i>
<span>房间状态统计</span>
</el-menu-item>
</el-menu>
</el-aside>
<el-main style="padding: 20px;">
<router-view />
</el-main>
</el-container>
</el-container>
</div>
</template>
<script>
export default {
name: 'App',
data() {
return {
activeIndex: 'dashboard'
}
},
methods: {
handleSelect(key, keyPath) {
this.activeIndex = key
//
const routeMap = {
'dashboard': '/',
'region-list': '/region/list',
'region-add': '/region/add',
'apartment-list': '/apartment/list',
'apartment-add': '/apartment/add',
'room-list': '/room/list',
'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'
}
if (routeMap[key] && this.$route.path !== routeMap[key]) {
this.$router.push(routeMap[key])
}
},
logout() {
// 退
this.$message.success('退出成功')
}
}
}
</script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
#app {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: #2c3e50;
}
.el-menu-vertical-demo:not(.el-menu--collapse) {
width: 200px;
min-height: 400px;
}
</style>

190
src/api/api.js Normal file
View File

@ -0,0 +1,190 @@
// API服务层用于与后端进行交互
const API_BASE_URL = 'http://localhost:3000/api';
// 通用请求函数
async function request(url, options = {}) {
try {
const response = await fetch(`${API_BASE_URL}${url}`, {
...options,
headers: {
'Content-Type': 'application/json',
...options.headers
}
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error('API request error:', error);
throw error;
}
}
// 区域管理API
export const regionApi = {
getAll: () => request('/regions'),
getById: (id) => request(`/regions/${id}`),
create: (data) => request('/regions', {
method: 'POST',
body: JSON.stringify(data)
}),
update: (id, data) => request(`/regions/${id}`, {
method: 'PUT',
body: JSON.stringify(data)
}),
delete: (id) => request(`/regions/${id}`, {
method: 'DELETE'
})
};
// 构建查询字符串
function buildQueryString(params) {
const query = Object.entries(params)
.filter(([key, value]) => value !== undefined && value !== null && value !== '')
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
.join('&');
return query ? `?${query}` : '';
}
// 公寓管理API
export const apartmentApi = {
getAll: (params = {}) => request(`/apartments${buildQueryString(params)}`),
getById: (id) => request(`/apartments/${id}`),
create: (data) => request('/apartments', {
method: 'POST',
body: JSON.stringify(data)
}),
update: (id, data) => request(`/apartments/${id}`, {
method: 'PUT',
body: JSON.stringify(data)
}),
delete: (id) => request(`/apartments/${id}`, {
method: 'DELETE'
})
};
// 房间管理API
export const roomApi = {
getAll: (params = {}) => request(`/rooms${buildQueryString(params)}`),
getById: (id) => request(`/rooms/${id}`),
create: (data) => request('/rooms', {
method: 'POST',
body: JSON.stringify(data)
}),
update: (id, data) => request(`/rooms/${id}`, {
method: 'PUT',
body: JSON.stringify(data)
}),
delete: (id) => request(`/rooms/${id}`, {
method: 'DELETE'
})
};
// 租客管理API
export const tenantApi = {
getAll: () => request('/tenants'),
getById: (id) => request(`/tenants/${id}`),
create: (data) => request('/tenants', {
method: 'POST',
body: JSON.stringify(data)
}),
update: (id, data) => request(`/tenants/${id}`, {
method: 'PUT',
body: JSON.stringify(data)
}),
delete: (id) => request(`/tenants/${id}`, {
method: 'DELETE'
})
};
// 合同管理API
export const contractApi = {
getAll: () => request('/contracts'),
getById: (id) => request(`/contracts/${id}`),
create: (data) => request('/contracts', {
method: 'POST',
body: JSON.stringify(data)
}),
update: (id, data) => request(`/contracts/${id}`, {
method: 'PUT',
body: JSON.stringify(data)
}),
delete: (id) => request(`/contracts/${id}`, {
method: 'DELETE'
})
};
// 租房管理API
export const rentalApi = {
getAll: (params = {}) => request(`/rentals${buildQueryString(params)}`),
getById: (id) => request(`/rentals/${id}`),
create: (data) => request('/rentals', {
method: 'POST',
body: JSON.stringify(data)
}),
update: (id, data) => request(`/rentals/${id}`, {
method: 'PUT',
body: JSON.stringify(data)
}),
delete: (id) => request(`/rentals/${id}`, {
method: 'DELETE'
})
};
// 统计分析API
export const statisticsApi = {
getRentData: () => request('/statistics/rent'),
getRoomStatus: () => request('/statistics/room-status'),
getRegionHouseStats: () => request('/statistics/region-house'),
getRegionApartmentHouseStats: () => request('/statistics/region-apartment-house')
};
// 水费管理API
export const waterBillApi = {
getAll: (params = {}) => request(`/water-bills${buildQueryString(params)}`),
getById: (id) => request(`/water-bills/${id}`),
create: (data) => request('/water-bills', {
method: 'POST',
body: JSON.stringify(data)
}),
update: (id, data) => request(`/water-bills/${id}`, {
method: 'PUT',
body: JSON.stringify(data)
}),
delete: (id) => request(`/water-bills/${id}`, {
method: 'DELETE'
})
};
// 电费管理API
export const electricityBillApi = {
getAll: (params = {}) => request(`/electricity-bills${buildQueryString(params)}`),
getById: (id) => request(`/electricity-bills/${id}`),
create: (data) => request('/electricity-bills', {
method: 'POST',
body: JSON.stringify(data)
}),
update: (id, data) => request(`/electricity-bills/${id}`, {
method: 'PUT',
body: JSON.stringify(data)
}),
delete: (id) => request(`/electricity-bills/${id}`, {
method: 'DELETE'
})
};
export default {
region: regionApi,
apartment: apartmentApi,
room: roomApi,
tenant: tenantApi,
contract: contractApi,
rental: rentalApi,
statistics: statisticsApi,
waterBill: waterBillApi,
electricityBill: electricityBillApi
};

14
src/main.js Normal file
View File

@ -0,0 +1,14 @@
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
Vue.use(ElementUI)
Vue.config.productionTip = false
new Vue({
router,
render: h => h(App)
}).$mount('#app')

101
src/mock/data.js Normal file
View File

@ -0,0 +1,101 @@
// 模拟数据
// 区域数据
export const regions = [
{ id: 1, name: '大商汇', description: '大商汇区域', createTime: '2023-01-06' },
{ id: 2, name: '丰源市场', description: '丰源市场区域', createTime: '2023-01-07' }
]
// 房间状态
const ROOM_STATUS = {
EMPTY: 'empty', // 空房(白色)
RENTED: 'rented', // 在租(绿色)
SOON_EXPIRE: 'soon_expire', // 即将到期(黄色)
EXPIRED: 'expired', // 已到期(红色)
CLEANING: 'cleaning', // 打扫中(灰色)
MAINTENANCE: 'maintenance' // 维修中(黑色)
}
// 公寓数据
export const apartments = [
{ id: 1, regionId: 1, name: '爱奇艺公寓', address: '大商汇区域', createTime: '2023-01-18' },
{ id: 2, regionId: 2, name: '碧云公寓', address: '丰源市场区域', createTime: '2023-01-19' }
]
// 房间数据
export const rooms = [
// 大商汇 - 爱奇艺公寓
{ id: 1, apartmentId: 1, roomNumber: '401', area: 40, price: 2500, status: ROOM_STATUS.EMPTY, createTime: '2023-01-18' },
{ id: 2, apartmentId: 1, roomNumber: '402', area: 40, price: 2500, status: ROOM_STATUS.EMPTY, createTime: '2023-01-18' },
{ id: 3, apartmentId: 1, roomNumber: '403', area: 45, price: 2800, status: ROOM_STATUS.RENTED, createTime: '2023-01-18' },
{ id: 4, apartmentId: 1, roomNumber: '404', area: 45, price: 2800, status: ROOM_STATUS.SOON_EXPIRE, createTime: '2023-01-18' },
{ id: 5, apartmentId: 1, roomNumber: '405', area: 50, price: 3000, status: ROOM_STATUS.EMPTY, createTime: '2023-01-18' },
{ id: 6, apartmentId: 1, roomNumber: '406', area: 50, price: 3000, status: ROOM_STATUS.EXPIRED, createTime: '2023-01-18' },
{ id: 7, apartmentId: 1, roomNumber: '407', area: 55, price: 3200, status: ROOM_STATUS.CLEANING, createTime: '2023-01-18' },
// 丰源市场 - 碧云公寓
{ id: 8, apartmentId: 2, roomNumber: '201', area: 35, price: 2200, status: ROOM_STATUS.EMPTY, createTime: '2023-01-19' },
{ id: 9, apartmentId: 2, roomNumber: '202', area: 35, price: 2200, status: ROOM_STATUS.RENTED, createTime: '2023-01-19' },
{ id: 10, apartmentId: 2, roomNumber: '203', area: 40, price: 2400, status: ROOM_STATUS.EMPTY, createTime: '2023-01-19' },
{ id: 11, apartmentId: 2, roomNumber: '205', area: 40, price: 2400, status: ROOM_STATUS.MAINTENANCE, createTime: '2023-01-19' },
{ id: 12, apartmentId: 2, roomNumber: '206', area: 45, price: 2600, status: ROOM_STATUS.RENTED, createTime: '2023-01-19' },
{ id: 13, apartmentId: 2, roomNumber: '208', area: 45, price: 2600, status: ROOM_STATUS.EMPTY, createTime: '2023-01-19' },
{ id: 14, apartmentId: 2, roomNumber: '209', area: 50, price: 2800, status: ROOM_STATUS.SOON_EXPIRE, createTime: '2023-01-19' }
]
// 租客数据
export const tenants = [
{ id: 1, name: '张三', phone: '13800138001', idCard: '110101199001011234', createTime: '2023-02-01' },
{ id: 2, name: '李四', phone: '13800138002', idCard: '110101199001011235', createTime: '2023-02-02' },
{ id: 3, name: '王五', phone: '13800138003', idCard: '110101199001011236', createTime: '2023-02-03' },
{ id: 4, name: '赵六', phone: '13800138004', idCard: '110101199001011237', createTime: '2023-02-04' },
{ id: 5, name: '钱七', phone: '13800138005', idCard: '110101199001011238', createTime: '2023-02-05' }
]
// 合同数据
export const contracts = [
{ id: 1, houseId: 2, tenantId: 1, startDate: '2023-03-01', endDate: '2024-03-01', rent: 8000, deposit: 16000, status: 'active', createTime: '2023-02-28' },
{ id: 2, houseId: 5, tenantId: 2, startDate: '2023-04-01', endDate: '2024-04-01', rent: 5500, deposit: 11000, status: 'active', createTime: '2023-03-31' },
{ id: 3, houseId: 8, tenantId: 3, startDate: '2023-05-01', endDate: '2024-05-01', rent: 5800, deposit: 11600, status: 'active', createTime: '2023-04-30' },
{ id: 4, houseId: 2, tenantId: 4, startDate: '2022-03-01', endDate: '2023-03-01', rent: 7500, deposit: 15000, status: 'expired', createTime: '2022-02-28' },
{ id: 5, houseId: 5, tenantId: 5, startDate: '2022-04-01', endDate: '2023-04-01', rent: 5000, deposit: 10000, status: 'expired', createTime: '2022-03-31' }
]
// 租房数据
export const rentals = [
{ id: 1, roomId: 3, tenantId: 1, contractId: 1, startDate: '2023-03-01', endDate: '2024-03-01', rent: 2800, deposit: 5600, status: 'active', createTime: '2023-02-28' },
{ id: 2, roomId: 9, tenantId: 2, contractId: 2, startDate: '2023-04-01', endDate: '2024-04-01', rent: 2200, deposit: 4400, status: 'active', createTime: '2023-03-31' },
{ id: 3, roomId: 12, tenantId: 3, contractId: 3, startDate: '2023-05-01', endDate: '2024-05-01', rent: 2600, deposit: 5200, status: 'active', createTime: '2023-04-30' },
{ id: 4, roomId: 4, tenantId: 4, contractId: 4, startDate: '2022-03-01', endDate: '2023-03-01', rent: 2800, deposit: 5600, status: 'expired', createTime: '2022-02-28' },
{ id: 5, roomId: 6, tenantId: 5, contractId: 5, startDate: '2022-04-01', endDate: '2023-04-01', rent: 3000, deposit: 6000, status: 'expired', createTime: '2022-03-31' }
]
// 租金统计数据
export const rentStatistics = [
{ month: '2023-01', amount: 25000 },
{ month: '2023-02', amount: 28000 },
{ month: '2023-03', amount: 30000 },
{ month: '2023-04', amount: 32000 },
{ month: '2023-05', amount: 35000 },
{ month: '2023-06', amount: 38000 }
]
// 房间状态统计数据
export const roomStatusStatistics = [
{ status: '空房', count: 7 },
{ status: '在租', count: 3 },
{ status: '即将到期', count: 2 },
{ status: '已到期', count: 1 },
{ status: '打扫中', count: 1 },
{ status: '维修中', count: 0 }
]
// 区域房屋统计数据
export const regionHouseStatistics = [
{ region: '大商汇', empty: 3, rented: 1, soon_expire: 1, expired: 1, cleaning: 1, maintenance: 0, total: 7 },
{ region: '丰源市场', empty: 4, rented: 2, soon_expire: 1, expired: 0, cleaning: 0, maintenance: 0, total: 7 }
]
// 导出常量
export const CONSTANTS = {
ROOM_STATUS
}

132
src/router/index.js Normal file
View File

@ -0,0 +1,132 @@
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'Dashboard',
component: () => import('../views/Dashboard.vue')
},
// 区域管理
{
path: '/region/list',
name: 'RegionList',
component: () => import('../views/region/List.vue')
},
{
path: '/region/add',
name: 'RegionAdd',
component: () => import('../views/region/Add.vue')
},
{
path: '/region/edit/:id',
name: 'RegionEdit',
component: () => import('../views/region/Edit.vue')
},
// 公寓管理
{
path: '/apartment/list',
name: 'ApartmentList',
component: () => import('../views/apartment/List.vue')
},
{
path: '/apartment/add',
name: 'ApartmentAdd',
component: () => import('../views/apartment/Add.vue')
},
{
path: '/apartment/edit/:id',
name: 'ApartmentEdit',
component: () => import('../views/apartment/Edit.vue')
},
// 房间管理
{
path: '/room/list',
name: 'RoomList',
component: () => import('../views/room/List.vue')
},
{
path: '/room/add',
name: 'RoomAdd',
component: () => import('../views/room/Add.vue')
},
{
path: '/room/edit/:id',
name: 'RoomEdit',
component: () => import('../views/room/Edit.vue')
},
// 租房管理
{
path: '/rental/list',
name: 'RentalList',
component: () => import('../views/rental/List.vue')
},
{
path: '/rental/add',
name: 'RentalAdd',
component: () => import('../views/rental/Add.vue')
},
{
path: '/rental/edit/:id',
name: 'RentalEdit',
component: () => import('../views/rental/Edit.vue')
},
{
path: '/rental/detail/:id',
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',
name: 'RentStatistics',
component: () => import('../views/statistics/Rent.vue')
},
{
path: '/statistics/room',
name: 'RoomStatistics',
component: () => import('../views/statistics/House.vue')
}
]
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
})
export default router

216
src/views/Dashboard.vue Normal file
View File

@ -0,0 +1,216 @@
<template>
<div class="dashboard">
<el-card class="welcome-card">
<h2>欢迎使用租房管理系统</h2>
<p>本系统提供区域管理房源管理租客管理合同管理和统计分析等功能</p>
</el-card>
<el-row :gutter="20" style="margin-top: 20px;">
<el-col :xs="24" :sm="12" :md="8">
<el-card class="stat-card">
<div class="stat-item">
<el-icon class="el-icon-location"></el-icon>
<div class="stat-info">
<div class="stat-value">{{ regionCount }}</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-building"></el-icon>
<div class="stat-info">
<div class="stat-value">{{ apartmentCount }}</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-home"></el-icon>
<div class="stat-info">
<div class="stat-value">{{ roomCount }}</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-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">
<el-icon class="el-icon-s-finance"></el-icon>
<div class="stat-info">
<div class="stat-value">{{ emptyRoomCount }}</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">{{ rentedRoomCount }}</div>
<div class="stat-label">在租数量</div>
</div>
</div>
</el-card>
</el-col>
</el-row>
<el-card style="margin-top: 20px;">
<template slot="header">
<div class="card-header">
<span>区域公寓房间状态分布</span>
</div>
</template>
<el-table :data="regionApartmentHouseStats" style="width: 100%">
<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>
<el-table-column prop="rented" label="在租" width="80"></el-table-column>
<el-table-column prop="soon_expire" label="即将到期" width="100"></el-table-column>
<el-table-column prop="expired" label="已到期" width="80"></el-table-column>
<el-table-column prop="cleaning" label="打扫中" width="80"></el-table-column>
<el-table-column prop="maintenance" label="维修中" width="80"></el-table-column>
<el-table-column prop="total" label="总数" width="80"></el-table-column>
</el-table>
</el-card>
</div>
</template>
<script>
import { regionApi, apartmentApi, roomApi, tenantApi, contractApi, statisticsApi } from '../api/api'
export default {
name: 'Dashboard',
data() {
return {
regionCount: 0,
apartmentCount: 0,
roomCount: 0,
tenantCount: 0,
contractCount: 0,
emptyRoomCount: 0,
rentedRoomCount: 0,
regionApartmentHouseStats: []
}
},
mounted() {
this.loadData()
},
methods: {
async loadData() {
try {
//
const [regionsResponse, apartmentsResponse, roomsResponse, tenantsResponse, contractsResponse, regionApartmentHouseStatsResponse] = await Promise.all([
regionApi.getAll(),
apartmentApi.getAll(),
roomApi.getAll(),
tenantApi.getAll(),
contractApi.getAll(),
statisticsApi.getRegionApartmentHouseStats()
])
//
const regions = regionsResponse.data || regionsResponse
const apartments = apartmentsResponse.data || apartmentsResponse
const rooms = roomsResponse.data || roomsResponse
const tenants = tenantsResponse.data || tenantsResponse
const contracts = contractsResponse.data || contractsResponse
this.regionCount = regions.length
this.apartmentCount = apartments.length
this.roomCount = rooms.length
this.tenantCount = tenants.length
this.contractCount = contracts.length
this.emptyRoomCount = rooms.filter(room => room.status === 'empty').length
this.rentedRoomCount = rooms.filter(room => room.status === 'rented').length
this.regionApartmentHouseStats = regionApartmentHouseStatsResponse
} catch (error) {
this.$message.error('加载数据失败')
}
}
}
}
</script>
<style scoped>
.dashboard {
padding: 20px 0;
}
.welcome-card {
text-align: center;
padding: 40px 0;
}
.stat-card {
height: 120px;
display: flex;
align-items: center;
justify-content: center;
}
.stat-item {
display: flex;
align-items: center;
width: 100%;
padding: 0 20px;
}
.stat-item .el-icon {
font-size: 36px;
color: #409EFF;
margin-right: 20px;
}
.stat-info {
flex: 1;
}
.stat-value {
font-size: 24px;
font-weight: bold;
color: #333;
}
.stat-label {
font-size: 14px;
color: #666;
margin-top: 4px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
</style>

106
src/views/apartment/Add.vue Normal file
View File

@ -0,0 +1,106 @@
<template>
<div class="apartment-add">
<el-card>
<template slot="header">
<div class="card-header">
<span>添加公寓</span>
</div>
</template>
<el-form :model="apartmentForm" :rules="rules" ref="apartmentForm" label-width="100px">
<el-form-item label="区域" prop="regionId">
<el-select v-model="apartmentForm.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="name">
<el-input v-model="apartmentForm.name" placeholder="请输入公寓名称"></el-input>
</el-form-item>
<el-form-item label="地址" prop="address">
<el-input v-model="apartmentForm.address" 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 { apartmentApi, regionApi } from '../../api/api'
export default {
name: 'ApartmentAdd',
data() {
return {
apartmentForm: {
regionId: '',
name: '',
address: ''
},
regions: [],
rules: {
regionId: [
{ 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' }
]
}
}
},
mounted() {
this.loadRegions()
},
methods: {
async loadRegions() {
try {
const response = await regionApi.getAll()
this.regions = response
} catch (error) {
this.$message.error('加载区域数据失败')
}
},
async submitForm() {
this.$refs.apartmentForm.validate(async (valid) => {
if (valid) {
try {
await apartmentApi.create(this.apartmentForm)
this.$message.success('添加成功')
this.$router.push('/apartment/list')
} catch (error) {
this.$message.error('添加失败')
}
} else {
return false
}
})
},
resetForm() {
this.$refs.apartmentForm.resetFields()
},
goBack() {
this.$router.push('/apartment/list')
}
}
}
</script>
<style scoped>
.apartment-add {
padding: 20px 0;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
</style>

View File

@ -0,0 +1,119 @@
<template>
<div class="apartment-edit">
<el-card>
<template slot="header">
<div class="card-header">
<span>编辑公寓</span>
</div>
</template>
<el-form :model="apartmentForm" :rules="rules" ref="apartmentForm" label-width="100px">
<el-form-item label="区域" prop="regionId">
<el-select v-model="apartmentForm.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="name">
<el-input v-model="apartmentForm.name" placeholder="请输入公寓名称"></el-input>
</el-form-item>
<el-form-item label="地址" prop="address">
<el-input v-model="apartmentForm.address" 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 { apartmentApi, regionApi } from '../../api/api'
export default {
name: 'ApartmentEdit',
data() {
return {
apartmentForm: {
id: '',
regionId: '',
name: '',
address: ''
},
regions: [],
rules: {
regionId: [
{ 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' }
]
}
}
},
mounted() {
this.loadRegions()
this.loadApartmentData()
},
methods: {
async loadRegions() {
try {
const response = await regionApi.getAll()
this.regions = response
} catch (error) {
this.$message.error('加载区域数据失败')
}
},
async loadApartmentData() {
try {
const id = this.$route.params.id
const apartment = await apartmentApi.getById(id)
if (apartment) {
this.apartmentForm = apartment
}
} catch (error) {
this.$message.error('加载公寓数据失败')
}
},
async submitForm() {
this.$refs.apartmentForm.validate(async (valid) => {
if (valid) {
try {
await apartmentApi.update(this.apartmentForm.id, this.apartmentForm)
this.$message.success('编辑成功')
this.$router.push('/apartment/list')
} catch (error) {
this.$message.error('编辑失败')
}
} else {
return false
}
})
},
resetForm() {
this.loadApartmentData()
},
goBack() {
this.$router.push('/apartment/list')
}
}
}
</script>
<style scoped>
.apartment-edit {
padding: 20px 0;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
</style>

View File

@ -0,0 +1,163 @@
<template>
<div class="apartment-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-input v-model="searchForm.name" 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="apartments" style="width: 100%">
<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="name" label="公寓名称"></el-table-column>
<el-table-column prop="address" label="地址"></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="prev, pager, next"
:total="total"
:page-size="pageSize"
:current-page="currentPage"
@current-change="handleCurrentChange"
></el-pagination>
</div>
</el-card>
</div>
</template>
<script>
import { apartmentApi, regionApi } from '../../api/api'
export default {
name: 'ApartmentList',
data() {
return {
regions: [],
apartments: [],
total: 0,
searchForm: {
regionId: '',
name: ''
},
currentPage: 1,
pageSize: 10
}
},
mounted() {
this.loadData()
},
methods: {
async loadData() {
try {
//
const regionsResponse = await regionApi.getAll()
this.regions = regionsResponse
//
const params = {
regionId: this.searchForm.regionId,
name: this.searchForm.name,
page: this.currentPage,
pageSize: this.pageSize
}
//
const apartmentsResponse = await apartmentApi.getAll(params)
this.apartments = apartmentsResponse.data.map(apartment => {
const region = regionsResponse.find(r => r.id == apartment.regionId)
return {
...apartment,
regionName: region ? region.name : ''
}
})
this.total = apartmentsResponse.total
} catch (error) {
this.$message.error('加载数据失败')
}
},
handleAdd() {
this.$router.push('/apartment/add')
},
handleEdit(id) {
this.$router.push(`/apartment/edit/${id}`)
},
async handleDelete(id) {
this.$confirm('确定要删除这个公寓吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
try {
await apartmentApi.delete(id)
this.$message.success('删除成功')
this.loadData()
} catch (error) {
this.$message.error('删除失败')
}
}).catch(() => {
//
})
},
handleSearch() {
this.currentPage = 1
this.loadData()
},
resetSearch() {
this.searchForm = {
regionId: '',
name: ''
}
this.currentPage = 1
this.loadData()
},
handleCurrentChange(val) {
this.currentPage = val
this.loadData()
}
}
}
</script>
<style scoped>
.apartment-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>

139
src/views/contract/Add.vue Normal file
View File

@ -0,0 +1,139 @@
<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.getAll()
this.tenants = response
} catch (error) {
this.$message.error('加载租客数据失败')
}
},
async loadRooms() {
try {
const response = await roomApi.getAll()
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>

152
src/views/contract/Edit.vue Normal file
View File

@ -0,0 +1,152 @@
<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.getAll()
this.tenants = response
} catch (error) {
this.$message.error('加载租客数据失败')
}
},
async loadRooms() {
try {
const response = await roomApi.getAll()
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>

154
src/views/contract/List.vue Normal file
View File

@ -0,0 +1,154 @@
<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="filteredContracts" style="width: 100%">
<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="prev, pager, next"
:total="filteredContracts.length"
:page-size="10"
:current-page="currentPage"
@current-change="handleCurrentChange"
></el-pagination>
</div>
</el-card>
</div>
</template>
<script>
import { contractApi } from '../../api/api'
export default {
name: 'ContractList',
data() {
return {
contracts: [],
searchForm: {
status: ''
},
currentPage: 1
}
},
computed: {
filteredContracts() {
return this.contracts.filter(contract => {
return !this.searchForm.status || contract.status == this.searchForm.status
})
}
},
mounted() {
this.loadContracts()
},
methods: {
async loadContracts() {
try {
//
const contractsResponse = await contractApi.getAll()
// 使
this.contracts = contractsResponse.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 : ''
}
})
} catch (error) {
this.$message.error('加载数据失败')
}
},
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() {
//
},
resetSearch() {
this.searchForm = {
status: ''
}
},
handleCurrentChange(val) {
this.currentPage = val
}
}
}
</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>

125
src/views/house/Add.vue Normal file
View File

@ -0,0 +1,125 @@
<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.getAll()
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>

138
src/views/house/Edit.vue Normal file
View File

@ -0,0 +1,138 @@
<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.getAll()
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>

184
src/views/house/List.vue Normal file
View File

@ -0,0 +1,184 @@
<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="filteredHouses" style="width: 100%">
<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="prev, pager, next"
:total="filteredHouses.length"
:page-size="10"
:current-page="currentPage"
@current-change="handleCurrentChange"
></el-pagination>
</div>
</el-card>
</div>
</template>
<script>
import { regionApi, houseApi } from '../../api/api'
export default {
name: 'HouseList',
data() {
return {
regions: [],
houses: [],
searchForm: {
regionId: '',
status: ''
},
currentPage: 1
}
},
computed: {
filteredHouses() {
return this.houses.filter(house => {
const regionMatch = !this.searchForm.regionId || house.regionId == this.searchForm.regionId
const statusMatch = !this.searchForm.status || house.status == this.searchForm.status
return regionMatch && statusMatch
})
}
},
mounted() {
this.loadData()
},
methods: {
async loadData() {
try {
//
const regionsResponse = await regionApi.getAll()
this.regions = regionsResponse
//
const housesResponse = await houseApi.getAll()
this.houses = housesResponse.map(house => {
const region = regionsResponse.find(r => r.id == house.regionId)
return {
...house,
regionName: region ? region.name : ''
}
})
} catch (error) {
this.$message.error('加载数据失败')
}
},
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() {
//
},
resetSearch() {
this.searchForm = {
regionId: '',
status: ''
}
},
handleCurrentChange(val) {
this.currentPage = val
}
}
}
</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>

85
src/views/region/Add.vue Normal file
View File

@ -0,0 +1,85 @@
<template>
<div class="region-add">
<el-card>
<template slot="header">
<div class="card-header">
<span>添加区域</span>
</div>
</template>
<el-form :model="regionForm" :rules="rules" ref="regionForm" label-width="100px">
<el-form-item label="区域名称" prop="name">
<el-input v-model="regionForm.name" placeholder="请输入区域名称"></el-input>
</el-form-item>
<el-form-item label="区域描述" prop="description">
<el-input type="textarea" v-model="regionForm.description" 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 { regionApi } from '../../api/api'
export default {
name: 'RegionAdd',
data() {
return {
regionForm: {
name: '',
description: ''
},
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' }
]
}
}
},
methods: {
async submitForm() {
this.$refs.regionForm.validate(async (valid) => {
if (valid) {
try {
await regionApi.create(this.regionForm)
this.$message.success('添加成功')
this.$router.push('/region/list')
} catch (error) {
this.$message.error('添加失败')
}
} else {
return false
}
})
},
resetForm() {
this.$refs.regionForm.resetFields()
},
goBack() {
this.$router.push('/region/list')
}
}
}
</script>
<style scoped>
.region-add {
padding: 20px 0;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
</style>

100
src/views/region/Edit.vue Normal file
View File

@ -0,0 +1,100 @@
<template>
<div class="region-edit">
<el-card>
<template slot="header">
<div class="card-header">
<span>编辑区域</span>
</div>
</template>
<el-form :model="regionForm" :rules="rules" ref="regionForm" label-width="100px">
<el-form-item label="区域名称" prop="name">
<el-input v-model="regionForm.name" placeholder="请输入区域名称"></el-input>
</el-form-item>
<el-form-item label="区域描述" prop="description">
<el-input type="textarea" v-model="regionForm.description" 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 { regionApi } from '../../api/api'
export default {
name: 'RegionEdit',
data() {
return {
regionForm: {
id: '',
name: '',
description: ''
},
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' }
]
}
}
},
mounted() {
this.loadRegionData()
},
methods: {
async loadRegionData() {
try {
const id = this.$route.params.id
const region = await regionApi.getById(id)
if (region) {
this.regionForm = region
}
} catch (error) {
this.$message.error('加载区域数据失败')
}
},
async submitForm() {
this.$refs.regionForm.validate(async (valid) => {
if (valid) {
try {
await regionApi.update(this.regionForm.id, this.regionForm)
this.$message.success('编辑成功')
this.$router.push('/region/list')
} catch (error) {
this.$message.error('编辑失败')
}
} else {
return false
}
})
},
resetForm() {
this.loadRegionData()
},
goBack() {
this.$router.push('/region/list')
}
}
}
</script>
<style scoped>
.region-edit {
padding: 20px 0;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
</style>

103
src/views/region/List.vue Normal file
View File

@ -0,0 +1,103 @@
<template>
<div class="region-list">
<el-card>
<template slot="header">
<div class="card-header">
<span>区域列表</span>
<el-button type="primary" @click="handleAdd">添加区域</el-button>
</div>
</template>
<el-table :data="regions" style="width: 100%">
<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="description" label="区域描述"></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="prev, pager, next"
:total="regions.length"
:page-size="10"
:current-page="currentPage"
@current-change="handleCurrentChange"
></el-pagination>
</div>
</el-card>
</div>
</template>
<script>
import { regionApi } from '../../api/api'
export default {
name: 'RegionList',
data() {
return {
regions: [],
currentPage: 1
}
},
mounted() {
this.loadRegions()
},
methods: {
async loadRegions() {
try {
const response = await regionApi.getAll()
this.regions = response
} catch (error) {
this.$message.error('加载区域数据失败')
}
},
handleAdd() {
this.$router.push('/region/add')
},
handleEdit(id) {
this.$router.push(`/region/edit/${id}`)
},
async handleDelete(id) {
this.$confirm('确定要删除这个区域吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
try {
await regionApi.delete(id)
this.$message.success('删除成功')
this.loadRegions()
} catch (error) {
this.$message.error('删除失败')
}
}).catch(() => {
this.$message.info('已取消删除')
})
},
handleCurrentChange(val) {
this.currentPage = val
}
}
}
</script>
<style scoped>
.region-list {
padding: 20px 0;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.pagination {
display: flex;
justify-content: flex-end;
}
</style>

181
src/views/rental/Add.vue Normal file
View File

@ -0,0 +1,181 @@
<template>
<div class="rental-add">
<el-card>
<template slot="header">
<div class="card-header">
<span>添加租房</span>
</div>
</template>
<el-form :model="rentalForm" :rules="rules" ref="rentalForm" label-width="120px">
<el-form-item label="房间" prop="roomId">
<el-select v-model="rentalForm.roomId" placeholder="请选择房间" style="width: 100%">
<el-option
v-for="room in rooms"
:key="room.id"
:label="`${getApartmentName(room.apartmentId)} - ${room.roomNumber}`"
:value="room.id.toString()"
></el-option>
</el-select>
</el-form-item>
<el-divider>租客信息</el-divider>
<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
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-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 { rentalApi, roomApi, apartmentApi } from '../../api/api'
export default {
name: 'RentalAdd',
data() {
return {
rentalForm: {
roomId: '',
tenantName: '',
tenantPhone: '',
tenantIdCard: '',
startDate: '',
endDate: '',
rent: '',
deposit: '',
status: 'active',
remark: ''
},
rules: {
roomId: [{ required: true, message: '请选择房间', trigger: 'blur' }],
tenantName: [{ required: true, message: '请输入租客姓名', trigger: 'blur' }],
tenantPhone: [{ required: true, message: '请输入租客电话', trigger: 'blur' }],
tenantIdCard: [{ required: true, message: '请输入身份证号', trigger: 'blur' }],
startDate: [{ required: true, message: '请选择开始日期', trigger: 'blur' }],
endDate: [{ required: true, message: '请选择结束日期', trigger: 'blur' }],
rent: [{ required: true, message: '请输入租金', trigger: 'blur' }],
deposit: [{ required: true, message: '请输入押金', trigger: 'blur' }],
status: [{ required: true, message: '请选择状态', trigger: 'blur' }]
},
rooms: [],
apartments: []
}
},
mounted() {
this.loadData()
},
methods: {
async loadData() {
try {
//
const [roomsResponse, apartmentsResponse] = await Promise.all([
roomApi.getAll(),
apartmentApi.getAll()
])
this.rooms = roomsResponse.data || roomsResponse
this.apartments = apartmentsResponse.data || apartmentsResponse
// URLroomId
const roomId = this.$route.query.roomId
if (roomId) {
// roomIdoptionvalue
this.rentalForm.roomId = roomId.toString()
//
const room = this.rooms.find(r => r.id.toString() == roomId.toString())
if (room) {
//
this.rentalForm.rent = room.price
}
}
} catch (error) {
this.$message.error('加载数据失败')
}
},
getApartmentName(apartmentId) {
if (!Array.isArray(this.apartments)) {
return ''
}
const apartment = this.apartments.find(a => a.id == apartmentId)
return apartment ? apartment.name : ''
},
async submitForm() {
this.$refs.rentalForm.validate(async (valid) => {
if (valid) {
try {
await rentalApi.create(this.rentalForm)
this.$message.success('添加成功')
this.$router.push('/rental/list')
} catch (error) {
this.$message.error('添加失败')
}
} else {
this.$message.error('请填写所有必填项')
return false
}
})
},
resetForm() {
this.$refs.rentalForm.resetFields()
},
goBack() {
this.$router.push('/rental/list')
}
}
}
</script>
<style scoped>
.rental-add {
padding: 20px 0;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
</style>

608
src/views/rental/Detail.vue Normal file
View File

@ -0,0 +1,608 @@
<template>
<div class="rental-detail">
<el-card>
<template slot="header">
<div class="card-header">
<span>房屋详情</span>
<div class="action-buttons">
<el-button v-if="room.status === 'empty'" type="primary" @click="handleRent">租房</el-button>
<el-button v-if="room.status === 'rented'" type="warning" @click="handleCheckout">退房</el-button>
<el-button v-if="room.status === 'empty'" type="info" @click="handleCleaning">打扫</el-button>
<el-button v-if="room.status === 'empty'" type="danger" @click="handleMaintenance">维修</el-button>
<el-button v-if="room.status === 'cleaning' || room.status === 'maintenance'" type="success" @click="handleComplete">完成</el-button>
<el-button type="primary" @click="goBack">返回</el-button>
</div>
</div>
</template>
<div v-loading="isLoading" class="room-info-section">
<h2>{{ room.apartmentName }} - {{ room.roomNumber }}</h2>
<div class="room-basic-info">
<div class="info-item">
<span class="label">面积:</span>
<span class="value">{{ room.area }}</span>
</div>
<div class="info-item">
<span class="label">租金:</span>
<span class="value">¥{{ room.price }}/</span>
</div>
<div class="info-item">
<span class="label">状态:</span>
<span class="value">
<el-tag :type="getStatusType(room.status)">{{ getStatusText(room.status) }}</el-tag>
</span>
</div>
<div class="info-item">
<span class="label">创建时间:</span>
<span class="value">{{ room.createTime }}</span>
</div>
</div>
</div>
<el-tabs v-model="activeTab">
<el-tab-pane label="租赁档案" name="rental">
<el-table :data="rentalHistory" style="width: 100%">
<el-table-column prop="tenantName" label="租客" width="120"></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="remark" label="备注" min-width="150"></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>
</el-tab-pane>
<el-tab-pane label="水费记录" name="water">
<div class="section-header">
<el-button type="primary" size="small" @click="handleAddWaterBill">添加水费</el-button>
</div>
<el-table :data="waterBills" style="width: 100%">
<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="startReading" label="起始度数" width="120"></el-table-column>
<el-table-column prop="endReading" label="结束度数" width="120"></el-table-column>
<el-table-column prop="usage" label="用水量(吨)" width="120"></el-table-column>
<el-table-column prop="unitPrice" label="单价(元/吨)" width="120"></el-table-column>
<el-table-column prop="amount" label="费用(元)" width="100"></el-table-column>
<el-table-column prop="status" label="状态" width="100">
<template slot-scope="scope">
<el-tag :type="scope.row.status === 'paid' ? 'success' : 'warning'">
{{ scope.row.status === 'paid' ? '已支付' : '未支付' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="150">
<template slot-scope="scope">
<el-button type="primary" size="small" @click="handleEditWaterBill(scope.row)">编辑</el-button>
<el-button type="danger" size="small" @click="handleDeleteWaterBill(scope.row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
<el-tab-pane label="电费记录" name="electricity">
<div class="section-header">
<el-button type="primary" size="small" @click="handleAddElectricityBill">添加电费</el-button>
</div>
<el-table :data="electricityBills" style="width: 100%">
<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="startReading" label="起始度数" width="120"></el-table-column>
<el-table-column prop="endReading" label="结束度数" width="120"></el-table-column>
<el-table-column prop="usage" label="用电量(度)" width="120"></el-table-column>
<el-table-column prop="unitPrice" label="单价(元/度)" width="120"></el-table-column>
<el-table-column prop="amount" label="费用(元)" width="100"></el-table-column>
<el-table-column prop="status" label="状态" width="100">
<template slot-scope="scope">
<el-tag :type="scope.row.status === 'paid' ? 'success' : 'warning'">
{{ scope.row.status === 'paid' ? '已支付' : '未支付' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="150">
<template slot-scope="scope">
<el-button type="primary" size="small" @click="handleEditElectricityBill(scope.row)">编辑</el-button>
<el-button type="danger" size="small" @click="handleDeleteElectricityBill(scope.row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
</el-tabs>
<!-- 水费编辑对话框 -->
<el-dialog title="编辑水费" :visible.sync="waterBillDialogVisible" width="500px">
<el-form :model="waterBillForm" :rules="waterBillRules" ref="waterBillForm">
<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.number="waterBillForm.startReading" placeholder="请输入起始度数"></el-input>
</el-form-item>
<el-form-item label="结束度数" prop="endReading">
<el-input v-model.number="waterBillForm.endReading" placeholder="请输入结束度数"></el-input>
</el-form-item>
<el-form-item label="单价(元/吨)" prop="unitPrice">
<el-input v-model.number="waterBillForm.unitPrice" placeholder="请输入单价"></el-input>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="waterBillForm.status" placeholder="请选择状态">
<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="handleSaveWaterBill">保存</el-button>
</span>
</el-dialog>
<!-- 电费编辑对话框 -->
<el-dialog title="编辑电费" :visible.sync="electricityBillDialogVisible" width="500px">
<el-form :model="electricityBillForm" :rules="electricityBillRules" ref="electricityBillForm">
<el-form-item label="开始日期" prop="startDate">
<el-date-picker v-model="electricityBillForm.startDate" type="date" placeholder="选择开始日期" style="width: 100%"></el-date-picker>
</el-form-item>
<el-form-item label="结束日期" prop="endDate">
<el-date-picker v-model="electricityBillForm.endDate" type="date" placeholder="选择结束日期" style="width: 100%"></el-date-picker>
</el-form-item>
<el-form-item label="起始度数" prop="startReading">
<el-input v-model.number="electricityBillForm.startReading" placeholder="请输入起始度数"></el-input>
</el-form-item>
<el-form-item label="结束度数" prop="endReading">
<el-input v-model.number="electricityBillForm.endReading" placeholder="请输入结束度数"></el-input>
</el-form-item>
<el-form-item label="单价(元/度)" prop="unitPrice">
<el-input v-model.number="electricityBillForm.unitPrice" placeholder="请输入单价"></el-input>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="electricityBillForm.status" placeholder="请选择状态">
<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="electricityBillDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSaveElectricityBill">保存</el-button>
</span>
</el-dialog>
</el-card>
</div>
</template>
<script>
import { roomApi, rentalApi, apartmentApi, waterBillApi, electricityBillApi } from '../../api/api'
export default {
name: 'RentalDetail',
data() {
return {
room: {},
rentals: [],
apartments: [],
rentalHistory: [],
waterBills: [],
electricityBills: [],
isLoading: false,
activeTab: 'rental',
waterBillDialogVisible: false,
electricityBillDialogVisible: false,
waterBillForm: {
id: '',
roomId: '',
startDate: '',
endDate: '',
startReading: '',
endReading: '',
unitPrice: '',
status: 'unpaid'
},
electricityBillForm: {
id: '',
roomId: '',
startDate: '',
endDate: '',
startReading: '',
endReading: '',
unitPrice: '',
status: 'unpaid'
},
waterBillRules: {
startDate: [{ required: true, message: '请选择开始日期', trigger: 'change' }],
endDate: [{ required: true, message: '请选择结束日期', trigger: 'change' }],
startReading: [{ required: true, message: '请输入起始度数', trigger: 'blur' }],
endReading: [{ required: true, message: '请输入结束度数', trigger: 'blur' }],
unitPrice: [{ required: true, message: '请输入单价', trigger: 'blur' }],
status: [{ required: true, message: '请选择状态', trigger: 'change' }]
},
electricityBillRules: {
startDate: [{ required: true, message: '请选择开始日期', trigger: 'change' }],
endDate: [{ required: true, message: '请选择结束日期', trigger: 'change' }],
startReading: [{ required: true, message: '请输入起始度数', trigger: 'blur' }],
endReading: [{ required: true, message: '请输入结束度数', trigger: 'blur' }],
unitPrice: [{ required: true, message: '请输入单价', trigger: 'blur' }],
status: [{ required: true, message: '请选择状态', trigger: 'change' }]
}
}
},
mounted() {
this.loadData()
},
methods: {
async loadData() {
this.isLoading = true
try {
const roomId = this.$route.params.id
//
const roomResponse = await roomApi.getById(roomId)
this.room = roomResponse
//
const apartmentsResponse = await apartmentApi.getAll()
this.apartments = apartmentsResponse.data || apartmentsResponse
//
if (this.room.apartmentId) {
const apartment = this.apartments.find(a => a.id == this.room.apartmentId)
this.room.apartmentName = apartment ? apartment.name : ''
}
//
const rentalsResponse = await rentalApi.getAll()
this.rentals = rentalsResponse.data || rentalsResponse
//
const waterBillsResponse = await waterBillApi.getAll({ roomId })
this.waterBills = waterBillsResponse.data || waterBillsResponse
//
const electricityBillsResponse = await electricityBillApi.getAll({ roomId })
this.electricityBills = electricityBillsResponse.data || electricityBillsResponse
//
this.loadRentalHistory()
} catch (error) {
this.$message.error('加载数据失败')
} finally {
this.isLoading = false
}
},
loadRentalHistory() {
const roomId = this.$route.params.id
this.rentalHistory = this.rentals
.filter(r => r.roomId == roomId)
.map(rental => {
return {
...rental,
tenantName: rental.Tenant ? rental.Tenant.name : ''
}
})
},
getStatusType(status) {
switch (status) {
case 'empty': return ''
case 'rented': return 'success'
case 'soon_expire': return 'warning'
case 'expired': return 'danger'
case 'cleaning': return 'info'
case 'maintenance': return 'danger'
default: return ''
}
},
getStatusText(status) {
switch (status) {
case 'empty': return '空房'
case 'rented': return '在租'
case 'soon_expire': return '即将到期'
case 'expired': return '到期'
case 'cleaning': return '打扫中'
case 'maintenance': return '维修中'
default: return status
}
},
handleRent() {
this.$router.push(`/rental/add?roomId=${this.room.id}`)
},
async handleCheckout() {
this.$confirm('确定要退房吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
try {
//
const roomId = this.$route.params.id
const rental = this.rentals.find(r => r.roomId == roomId && r.status === 'active')
if (rental) {
//
await rentalApi.update(rental.id, { status: 'expired' })
//
await roomApi.update(roomId, { status: 'empty' })
this.$message.success('退房成功')
this.loadData()
} else {
this.$message.error('未找到活跃的租房记录')
}
} catch (error) {
this.$message.error('退房失败')
}
}).catch(() => {
// 退
})
},
async handleCleaning() {
this.$confirm('确定要标记为打扫中吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'info'
}).then(async () => {
try {
const roomId = this.$route.params.id
await roomApi.update(roomId, { status: 'cleaning' })
this.$message.success('标记为打扫中成功')
this.loadData()
} catch (error) {
this.$message.error('操作失败')
}
}).catch(() => {
//
})
},
async handleMaintenance() {
this.$confirm('确定要标记为维修中吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'danger'
}).then(async () => {
try {
const roomId = this.$route.params.id
await roomApi.update(roomId, { status: 'maintenance' })
this.$message.success('标记为维修中成功')
this.loadData()
} catch (error) {
this.$message.error('操作失败')
}
}).catch(() => {
//
})
},
async handleComplete() {
this.$confirm('确定要完成打扫/维修并将状态改为空房吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'success'
}).then(async () => {
try {
const roomId = this.$route.params.id
await roomApi.update(roomId, { status: 'empty' })
this.$message.success('操作成功,房间状态已改为空房')
this.loadData()
} catch (error) {
this.$message.error('操作失败')
}
}).catch(() => {
//
})
},
handleAddWaterBill() {
this.waterBillForm = {
id: '',
roomId: this.$route.params.id,
startDate: '',
endDate: '',
startReading: '',
endReading: '',
unitPrice: '',
status: 'unpaid'
}
this.waterBillDialogVisible = true
},
handleEditWaterBill(bill) {
this.waterBillForm = {
...bill,
startDate: bill.startDate ? new Date(bill.startDate) : '',
endDate: bill.endDate ? new Date(bill.endDate) : ''
}
this.waterBillDialogVisible = true
},
async handleSaveWaterBill() {
try {
const roomId = this.$route.params.id
if (this.waterBillForm.id) {
//
await waterBillApi.update(this.waterBillForm.id, this.waterBillForm)
this.$message.success('水费记录更新成功')
} else {
//
await waterBillApi.create({
...this.waterBillForm,
roomId
})
this.$message.success('水费记录添加成功')
}
this.waterBillDialogVisible = false
this.loadData()
} catch (error) {
this.$message.error('操作失败')
}
},
async handleDeleteWaterBill(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(() => {
//
})
},
handleAddElectricityBill() {
this.electricityBillForm = {
id: '',
roomId: this.$route.params.id,
startDate: '',
endDate: '',
startReading: '',
endReading: '',
unitPrice: '',
status: 'unpaid'
}
this.electricityBillDialogVisible = true
},
handleEditElectricityBill(bill) {
this.electricityBillForm = {
...bill,
startDate: bill.startDate ? new Date(bill.startDate) : '',
endDate: bill.endDate ? new Date(bill.endDate) : ''
}
this.electricityBillDialogVisible = true
},
async handleSaveElectricityBill() {
try {
const roomId = this.$route.params.id
if (this.electricityBillForm.id) {
//
await electricityBillApi.update(this.electricityBillForm.id, this.electricityBillForm)
this.$message.success('电费记录更新成功')
} else {
//
await electricityBillApi.create({
...this.electricityBillForm,
roomId
})
this.$message.success('电费记录添加成功')
}
this.electricityBillDialogVisible = false
this.loadData()
} catch (error) {
this.$message.error('操作失败')
}
},
async handleDeleteElectricityBill(id) {
this.$confirm('确定要删除这条电费记录吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'danger'
}).then(async () => {
try {
await electricityBillApi.delete(id)
this.$message.success('电费记录删除成功')
this.loadData()
} catch (error) {
this.$message.error('删除失败')
}
}).catch(() => {
//
})
},
goBack() {
this.$router.push('/rental/list')
}
}
}
</script>
<style scoped>
.rental-detail {
padding: 20px 0;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.room-info-section {
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 1px solid #e4e7ed;
}
.room-info-section h2 {
margin-bottom: 20px;
font-size: 24px;
font-weight: 500;
}
.room-basic-info {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 15px;
}
.info-item {
display: flex;
align-items: center;
}
.info-item .label {
width: 80px;
color: #606266;
}
.info-item .value {
font-weight: 500;
}
.rental-history-section {
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 1px solid #e4e7ed;
}
.rental-history-section h3 {
margin-bottom: 15px;
font-size: 18px;
font-weight: 500;
}
.water-bill-section {
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 1px solid #e4e7ed;
}
.electricity-bill-section {
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 1px solid #e4e7ed;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.section-header h3 {
font-size: 18px;
font-weight: 500;
margin: 0;
}
.action-buttons {
display: flex;
gap: 10px;
}
</style>

177
src/views/rental/Edit.vue Normal file
View File

@ -0,0 +1,177 @@
<template>
<div class="rental-edit">
<el-card>
<template slot="header">
<div class="card-header">
<span>编辑租房</span>
</div>
</template>
<el-form :model="rentalForm" :rules="rules" ref="rentalForm" label-width="120px">
<el-form-item label="房间" prop="roomId">
<el-select v-model="rentalForm.roomId" placeholder="请选择房间" style="width: 100%">
<el-option
v-for="room in rooms"
:key="room.id"
:label="`${getApartmentName(room.apartmentId)} - ${room.roomNumber}`"
:value="room.id"
></el-option>
</el-select>
</el-form-item>
<el-divider>租客信息</el-divider>
<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
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>
<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 { rentalApi, roomApi, apartmentApi } from '../../api/api'
export default {
name: 'RentalEdit',
data() {
return {
rentalForm: {
id: '',
roomId: '',
tenantName: '',
tenantPhone: '',
tenantIdCard: '',
startDate: '',
endDate: '',
rent: '',
deposit: '',
status: ''
},
rules: {
roomId: [{ required: true, message: '请选择房间', trigger: 'blur' }],
tenantName: [{ required: true, message: '请输入租客姓名', trigger: 'blur' }],
tenantPhone: [{ required: true, message: '请输入租客电话', trigger: 'blur' }],
tenantIdCard: [{ required: true, message: '请输入身份证号', trigger: 'blur' }],
startDate: [{ required: true, message: '请选择开始日期', trigger: 'blur' }],
endDate: [{ required: true, message: '请选择结束日期', trigger: 'blur' }],
rent: [{ required: true, message: '请输入租金', trigger: 'blur' }],
deposit: [{ required: true, message: '请输入押金', trigger: 'blur' }],
status: [{ required: true, message: '请选择状态', trigger: 'blur' }]
},
rooms: [],
apartments: []
}
},
mounted() {
this.loadData()
this.loadRentalData()
},
methods: {
async loadData() {
try {
//
const [roomsResponse, apartmentsResponse] = await Promise.all([
roomApi.getAll(),
apartmentApi.getAll()
])
this.rooms = roomsResponse
this.apartments = apartmentsResponse
} catch (error) {
this.$message.error('加载数据失败')
}
},
async loadRentalData() {
try {
const id = this.$route.params.id
const rental = await rentalApi.getById(id)
if (rental) {
this.rentalForm = {
...rental,
startDate: new Date(rental.startDate),
endDate: new Date(rental.endDate)
}
}
} catch (error) {
this.$message.error('加载租房数据失败')
}
},
getApartmentName(apartmentId) {
const apartment = this.apartments.find(a => a.id == apartmentId)
return apartment ? apartment.name : ''
},
async submitForm() {
this.$refs.rentalForm.validate(async (valid) => {
if (valid) {
try {
await rentalApi.update(this.rentalForm.id, this.rentalForm)
this.$message.success('编辑成功')
this.$router.push('/rental/list')
} catch (error) {
this.$message.error('编辑失败')
}
} else {
this.$message.error('请填写所有必填项')
return false
}
})
},
resetForm() {
this.loadRentalData()
},
goBack() {
this.$router.push('/rental/list')
}
}
}
</script>
<style scoped>
.rental-edit {
padding: 20px 0;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
</style>

325
src/views/rental/List.vue Normal file
View File

@ -0,0 +1,325 @@
<template>
<div class="rental-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.apartmentId" placeholder="请选择公寓">
<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.status" placeholder="请选择状态">
<el-option label="全部" value=""></el-option>
<el-option label="空房" value="empty"></el-option>
<el-option label="在租" value="rented"></el-option>
<el-option label="即将到期" value="soon_expire"></el-option>
<el-option label="已到期" value="expired"></el-option>
<el-option label="打扫中" value="cleaning"></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>
<div class="room-cards">
<div
v-for="room in rooms"
:key="room.id"
@click="handleRoomClick(room.id)"
style="cursor: pointer;"
>
<el-card
:class="['room-card', `status-${room.status}`]"
>
<div class="room-card-header">
<h3>{{ getApartmentName(room.apartmentId) }}</h3>
<el-tag :type="getStatusType(room.status)">{{ getStatusText(room.status) }}</el-tag>
</div>
<div class="room-card-body">
<div class="room-info">
<span class="room-number">{{ room.roomNumber }}</span>
<span class="room-area">{{ room.area }}</span>
<span class="room-price">¥{{ room.price }}/</span>
</div>
<div class="rental-info" v-if="room.Rentals && room.Rentals.length > 0">
<p>租客: {{ room.Rentals[0].Tenant.name }}</p>
<p>租期: {{ room.Rentals[0].startDate }} {{ room.Rentals[0].endDate }}</p>
</div>
<div class="rental-info" v-else>
<p>暂无租客信息</p>
</div>
</div>
</el-card>
</div>
</div>
<div class="pagination" style="margin-top: 20px;">
<el-pagination
layout="total, sizes, prev, pager, next, jumper"
:total="total"
:page-size="pageSize"
:current-page="currentPage"
:page-sizes="[10, 20, 50, 100, 99999]"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
></el-pagination>
</div>
</el-card>
</div>
</template>
<script>
import { roomApi, apartmentApi } from '../../api/api'
export default {
name: 'RentalList',
data() {
return {
rooms: [],
apartments: [],
total: 0,
searchForm: {
apartmentId: '',
status: ''
},
currentPage: 1,
pageSize: 50,
isLoading: false
}
},
mounted() {
this.loadData()
},
methods: {
async loadData() {
if (this.isLoading) return
this.isLoading = true
try {
//
const apartmentsResponse = await apartmentApi.getAll()
this.apartments = apartmentsResponse.data || apartmentsResponse
//
const params = {
apartmentId: this.searchForm.apartmentId,
status: this.searchForm.status,
page: this.currentPage,
pageSize: this.pageSize
}
//
const roomsResponse = await roomApi.getAll(params)
if (roomsResponse.data) {
this.rooms = roomsResponse.data
this.total = roomsResponse.total
} else {
//
this.rooms = roomsResponse
this.total = roomsResponse.length
}
} catch (error) {
this.$message.error('加载数据失败')
} finally {
this.isLoading = false
}
},
getApartmentName(apartmentId) {
const apartment = this.apartments.find(a => a.id == apartmentId)
return apartment ? apartment.name : ''
},
getStatusType(status) {
switch (status) {
case 'empty': return ''
case 'rented': return 'success'
case 'soon_expire': return 'warning'
case 'expired': return 'danger'
case 'cleaning': return 'info'
case 'maintenance': return 'danger'
default: return ''
}
},
getStatusText(status) {
switch (status) {
case 'empty': return '空房'
case 'rented': return '在租'
case 'soon_expire': return '即将到期'
case 'expired': return '到期'
case 'cleaning': return '打扫中'
case 'maintenance': return '维修中'
default: return status
}
},
handleAdd() {
this.$router.push('/rental/add')
},
handleRoomClick(roomId) {
try {
this.$router.push(`/rental/detail/${roomId}`)
} catch (error) {
console.error('Navigation error:', error)
}
},
handleSearch() {
this.currentPage = 1
this.loadData()
},
resetSearch() {
this.searchForm = {
apartmentId: '',
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>
.rental-list {
padding: 20px 0;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.search-form {
margin-bottom: 20px;
}
.room-cards {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
margin-top: 20px;
}
.room-card {
cursor: pointer;
transition: all 0.3s ease;
border: 2px solid #e4e7ed;
position: relative;
z-index: 1;
height: 220px;
display: flex;
flex-direction: column;
}
.room-card * {
pointer-events: none;
}
.room-card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
}
.room-card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.room-card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
flex-shrink: 0;
}
.room-card-body {
flex: 1;
display: flex;
flex-direction: column;
}
.room-info {
display: flex;
justify-content: space-between;
margin-bottom: 15px;
padding-bottom: 15px;
border-bottom: 1px solid #e4e7ed;
flex-shrink: 0;
}
.room-number {
font-size: 24px;
font-weight: bold;
}
.room-area {
color: #606266;
}
.room-price {
color: #409EFF;
font-weight: 500;
}
.rental-info {
margin-top: 10px;
flex: 1;
display: flex;
flex-direction: column;
justify-content: flex-start;
}
.rental-info p {
margin: 5px 0;
color: #606266;
}
/* 状态样式 */
.status-empty {
border-color: #ffffff;
background-color: #f9f9f9;
}
.status-rented {
border-color: #67c23a;
background-color: #f0f9eb;
}
.status-soon_expire {
border-color: #e6a23c;
background-color: #fdf6ec;
}
.status-expired {
border-color: #f56c6c;
background-color: #fef0f0;
}
.status-cleaning {
border-color: #909399;
background-color: #f4f4f5;
}
.status-maintenance {
border-color: #303133;
background-color: #f4f4f5;
}
</style>

142
src/views/room/Add.vue Normal file
View File

@ -0,0 +1,142 @@
<template>
<div class="room-add">
<el-card>
<template slot="header">
<div class="card-header">
<span>添加房间</span>
</div>
</template>
<el-form :model="roomForm" :rules="rules" ref="roomForm" label-width="100px">
<el-form-item label="公寓" prop="apartmentId">
<el-select v-model="roomForm.apartmentId" placeholder="请选择公寓">
<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="房间号" prop="roomNumber">
<el-input v-model="roomForm.roomNumber" placeholder="请输入房间号"></el-input>
</el-form-item>
<el-form-item label="面积(㎡)" prop="area">
<el-input type="number" v-model="roomForm.area" placeholder="请输入面积"></el-input>
</el-form-item>
<el-form-item label="租金(元/月)" prop="price">
<el-input type="number" v-model="roomForm.price" placeholder="请输入租金"></el-input>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="roomForm.status" placeholder="请选择状态">
<el-option label="空房" value="empty"></el-option>
<el-option label="在租" value="rented"></el-option>
<el-option label="即将到期" value="soon_expire"></el-option>
<el-option label="已到期" value="expired"></el-option>
<el-option label="打扫中" value="cleaning"></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 { roomApi, apartmentApi } from '../../api/api'
export default {
name: 'RoomAdd',
data() {
return {
roomForm: {
apartmentId: '',
roomNumber: '',
area: '',
price: '',
status: 'empty'
},
apartments: [],
rules: {
apartmentId: [
{ required: true, message: '请选择公寓', trigger: 'blur' }
],
roomNumber: [
{ required: true, message: '请输入房间号', trigger: 'blur' },
{ min: 1, max: 10, message: '长度在 1 到 10 个字符', trigger: 'blur' }
],
area: [
{ validator: (rule, value, callback) => {
if (value === '' || value === null || value === undefined) {
callback()
} else if (isNaN(Number(value))) {
callback(new Error('请输入数字'))
} else {
callback()
}
}, trigger: 'blur' }
],
price: [
{ validator: (rule, value, callback) => {
if (value === '' || value === null || value === undefined) {
callback()
} else if (isNaN(Number(value))) {
callback(new Error('请输入数字'))
} else {
callback()
}
}, trigger: 'blur' }
],
status: [
{ required: true, message: '请选择状态', trigger: 'blur' }
]
}
}
},
mounted() {
this.loadApartments()
},
methods: {
async loadApartments() {
try {
const response = await apartmentApi.getAll()
this.apartments = response.data || response
} catch (error) {
this.$message.error('加载公寓数据失败')
}
},
async submitForm() {
this.$refs.roomForm.validate(async (valid) => {
if (valid) {
try {
await roomApi.create(this.roomForm)
this.$message.success('添加成功')
this.$router.push('/room/list')
} catch (error) {
this.$message.error('添加失败')
}
} else {
return false
}
})
},
resetForm() {
this.$refs.roomForm.resetFields()
},
goBack() {
this.$router.push('/room/list')
}
}
}
</script>
<style scoped>
.room-add {
padding: 20px 0;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
</style>

155
src/views/room/Edit.vue Normal file
View File

@ -0,0 +1,155 @@
<template>
<div class="room-edit">
<el-card>
<template slot="header">
<div class="card-header">
<span>编辑房间</span>
</div>
</template>
<el-form :model="roomForm" :rules="rules" ref="roomForm" label-width="100px">
<el-form-item label="公寓" prop="apartmentId">
<el-select v-model="roomForm.apartmentId" placeholder="请选择公寓">
<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="房间号" prop="roomNumber">
<el-input v-model="roomForm.roomNumber" placeholder="请输入房间号"></el-input>
</el-form-item>
<el-form-item label="面积(㎡)" prop="area">
<el-input type="number" v-model="roomForm.area" placeholder="请输入面积"></el-input>
</el-form-item>
<el-form-item label="租金(元/月)" prop="price">
<el-input type="number" v-model="roomForm.price" placeholder="请输入租金"></el-input>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="roomForm.status" placeholder="请选择状态">
<el-option label="空房" value="empty"></el-option>
<el-option label="在租" value="rented"></el-option>
<el-option label="即将到期" value="soon_expire"></el-option>
<el-option label="已到期" value="expired"></el-option>
<el-option label="打扫中" value="cleaning"></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 { roomApi, apartmentApi } from '../../api/api'
export default {
name: 'RoomEdit',
data() {
return {
roomForm: {
id: '',
apartmentId: '',
roomNumber: '',
area: '',
price: '',
status: ''
},
apartments: [],
rules: {
apartmentId: [
{ required: true, message: '请选择公寓', trigger: 'blur' }
],
roomNumber: [
{ required: true, message: '请输入房间号', trigger: 'blur' },
{ min: 1, max: 10, message: '长度在 1 到 10 个字符', trigger: 'blur' }
],
area: [
{ validator: (rule, value, callback) => {
if (value === '' || value === null || value === undefined) {
callback()
} else if (isNaN(Number(value))) {
callback(new Error('请输入数字'))
} else {
callback()
}
}, trigger: 'blur' }
],
price: [
{ validator: (rule, value, callback) => {
if (value === '' || value === null || value === undefined) {
callback()
} else if (isNaN(Number(value))) {
callback(new Error('请输入数字'))
} else {
callback()
}
}, trigger: 'blur' }
],
status: [
{ required: true, message: '请选择状态', trigger: 'blur' }
]
}
}
},
mounted() {
this.loadApartments()
this.loadRoomData()
},
methods: {
async loadApartments() {
try {
const response = await apartmentApi.getAll()
this.apartments = response.data || response
} catch (error) {
this.$message.error('加载公寓数据失败')
}
},
async loadRoomData() {
try {
const id = this.$route.params.id
const room = await roomApi.getById(id)
if (room) {
this.roomForm = room
}
} catch (error) {
this.$message.error('加载房间数据失败')
}
},
async submitForm() {
this.$refs.roomForm.validate(async (valid) => {
if (valid) {
try {
await roomApi.update(this.roomForm.id, this.roomForm)
this.$message.success('编辑成功')
this.$router.push('/room/list')
} catch (error) {
this.$message.error('编辑失败')
}
} else {
return false
}
})
},
resetForm() {
this.loadRoomData()
},
goBack() {
this.$router.push('/room/list')
}
}
}
</script>
<style scoped>
.room-edit {
padding: 20px 0;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
</style>

226
src/views/room/List.vue Normal file
View File

@ -0,0 +1,226 @@
<template>
<div class="room-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.apartmentId" placeholder="请选择公寓">
<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.status" placeholder="请选择状态">
<el-option label="全部" value=""></el-option>
<el-option label="空房" value="empty"></el-option>
<el-option label="在租" value="rented"></el-option>
<el-option label="即将到期" value="soon_expire"></el-option>
<el-option label="已到期" value="expired"></el-option>
<el-option label="打扫中" value="cleaning"></el-option>
<el-option label="维修中" value="maintenance"></el-option>
</el-select>
</el-form-item>
<el-form-item label="房间号">
<el-input v-model="searchForm.roomNumber" 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="rooms"
style="width: 100%"
>
<el-table-column prop="apartmentName" label="公寓" width="150"></el-table-column>
<el-table-column prop="roomNumber" label="房间号" width="100"></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="120">
<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="prev, pager, next"
:total="total"
:page-size="pageSize"
:current-page="currentPage"
@current-change="handleCurrentChange"
></el-pagination>
</div>
</el-card>
</div>
</template>
<script>
import { roomApi, apartmentApi } from '../../api/api'
export default {
name: 'RoomList',
data() {
return {
apartments: [],
rooms: [],
total: 0,
searchForm: {
apartmentId: '',
status: '',
roomNumber: ''
},
currentPage: 1,
pageSize: 10,
isLoading: false
}
},
mounted() {
this.loadData()
},
methods: {
async loadData() {
if (this.isLoading) return
this.isLoading = true
try {
//
const apartmentsResponse = await apartmentApi.getAll()
this.apartments = apartmentsResponse.data || apartmentsResponse
//
const params = {
apartmentId: this.searchForm.apartmentId,
status: this.searchForm.status,
roomNumber: this.searchForm.roomNumber,
page: this.currentPage,
pageSize: this.pageSize
}
//
const roomsResponse = await roomApi.getAll(params)
if (roomsResponse.data) {
this.rooms = roomsResponse.data.map(room => {
//
const apartment = this.apartments.find(a => a.id == room.apartmentId)
return {
...room,
apartmentName: apartment ? apartment.name : ''
}
})
this.total = roomsResponse.total
} else {
//
this.rooms = roomsResponse.map(room => {
const apartment = this.apartments.find(a => a.id == room.apartmentId)
return {
...room,
apartmentName: apartment ? apartment.name : ''
}
})
this.total = roomsResponse.length
}
} catch (error) {
this.$message.error('加载数据失败')
} finally {
this.isLoading = false
}
},
getStatusType(status) {
switch (status) {
case 'empty': return ''
case 'rented': return 'success'
case 'soon_expire': return 'warning'
case 'expired': return 'danger'
case 'cleaning': return 'info'
case 'maintenance': return 'danger'
default: return ''
}
},
getStatusText(status) {
switch (status) {
case 'empty': return '空房'
case 'rented': return '在租'
case 'soon_expire': return '即将到期'
case 'expired': return '到期'
case 'cleaning': return '打扫中'
case 'maintenance': return '维修中'
default: return status
}
},
handleAdd() {
this.$router.push('/room/add')
},
handleEdit(id) {
this.$router.push(`/room/edit/${id}`)
},
async handleDelete(id) {
this.$confirm('确定要删除这个房间吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
try {
await roomApi.delete(id)
this.$message.success('删除成功')
this.loadData()
} catch (error) {
this.$message.error('删除失败')
}
}).catch(() => {
//
})
},
handleSearch() {
this.currentPage = 1
this.loadData()
},
resetSearch() {
this.searchForm = {
apartmentId: '',
status: '',
roomNumber: ''
}
this.currentPage = 1
this.loadData()
},
handleCurrentChange(val) {
this.currentPage = val
this.loadData()
}
}
}
</script>
<style scoped>
.room-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

@ -0,0 +1,76 @@
<template>
<div class="room-statistics">
<el-card>
<template slot="header">
<div class="card-header">
<span>房间状态统计</span>
</div>
</template>
<div class="chart-container">
<el-empty description="图表功能暂未实现" style="margin: 40px 0;"></el-empty>
</div>
<el-table :data="roomStatusData" style="width: 100%; margin-top: 20px;">
<el-table-column prop="status" label="状态" width="120"></el-table-column>
<el-table-column prop="count" label="数量"></el-table-column>
<el-table-column prop="percentage" label="占比"></el-table-column>
</el-table>
<div class="total-count" style="margin-top: 20px; text-align: right;">
<el-tag size="large" type="primary">房间总数{{ totalCount }} </el-tag>
</div>
</el-card>
</div>
</template>
<script>
import { statisticsApi } from '../../api/api'
export default {
name: 'RoomStatistics',
data() {
return {
roomStatusData: []
}
},
computed: {
totalCount() {
return this.roomStatusData.reduce((sum, item) => sum + item.count, 0)
}
},
mounted() {
this.loadRoomStatusData()
},
methods: {
async loadRoomStatusData() {
try {
const response = await statisticsApi.getRoomStatus()
const data = response
const total = data.reduce((sum, item) => sum + item.count, 0)
this.roomStatusData = data.map(item => ({
...item,
percentage: `${((item.count / total) * 100).toFixed(2)}%`
}))
} catch (error) {
this.$message.error('加载房间状态统计数据失败')
}
}
}
}
</script>
<style scoped>
.room-statistics {
padding: 20px 0;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.chart-container {
margin: 20px 0;
display: flex;
justify-content: center;
}
</style>

View File

@ -0,0 +1,68 @@
<template>
<div class="rent-statistics">
<el-card>
<template slot="header">
<div class="card-header">
<span>租金统计</span>
</div>
</template>
<div class="chart-container">
<el-empty description="图表功能暂未实现" style="margin: 40px 0;"></el-empty>
</div>
<el-table :data="rentData" style="width: 100%; margin-top: 20px;">
<el-table-column prop="month" label="月份" width="120"></el-table-column>
<el-table-column prop="amount" label="租金收入(元)"></el-table-column>
</el-table>
<div class="total-amount" style="margin-top: 20px; text-align: right;">
<el-tag size="large" type="primary">总租金收入{{ totalAmount }} </el-tag>
</div>
</el-card>
</div>
</template>
<script>
import { statisticsApi } from '../../api/api'
export default {
name: 'RentStatistics',
data() {
return {
rentData: []
}
},
computed: {
totalAmount() {
return this.rentData.reduce((sum, item) => sum + item.amount, 0)
}
},
mounted() {
this.loadRentData()
},
methods: {
async loadRentData() {
try {
const response = await statisticsApi.getRentData()
this.rentData = response
} catch (error) {
this.$message.error('加载租金统计数据失败')
}
}
}
}
</script>
<style scoped>
.rent-statistics {
padding: 20px 0;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.chart-container {
margin: 20px 0;
}
</style>

93
src/views/tenant/Add.vue Normal file
View File

@ -0,0 +1,93 @@
<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>

108
src/views/tenant/Edit.vue Normal file
View File

@ -0,0 +1,108 @@
<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>

136
src/views/tenant/List.vue Normal file
View File

@ -0,0 +1,136 @@
<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="filteredTenants" style="width: 100%">
<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="prev, pager, next"
:total="filteredTenants.length"
:page-size="10"
:current-page="currentPage"
@current-change="handleCurrentChange"
></el-pagination>
</div>
</el-card>
</div>
</template>
<script>
import { tenantApi } from '../../api/api'
export default {
name: 'TenantList',
data() {
return {
tenants: [],
searchForm: {
name: '',
phone: ''
},
currentPage: 1
}
},
computed: {
filteredTenants() {
return this.tenants.filter(tenant => {
const nameMatch = !this.searchForm.name || tenant.name.includes(this.searchForm.name)
const phoneMatch = !this.searchForm.phone || tenant.phone.includes(this.searchForm.phone)
return nameMatch && phoneMatch
})
}
},
mounted() {
this.loadTenants()
},
methods: {
async loadTenants() {
try {
const response = await tenantApi.getAll()
this.tenants = response
} catch (error) {
this.$message.error('加载租客数据失败')
}
},
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() {
//
},
resetSearch() {
this.searchForm = {
name: '',
phone: ''
}
},
handleCurrentChange(val) {
this.currentPage = val
}
}
}
</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>