This commit is contained in:
wangxiaoxian 2026-03-03 23:35:20 +08:00
parent 4d23e028bb
commit c63230239d
19 changed files with 832 additions and 305 deletions

3
.env Normal file
View File

@ -0,0 +1,3 @@
# 默认环境配置
NODE_ENV=development
VUE_APP_API_BASE_URL=/api

3
.env.production Normal file
View File

@ -0,0 +1,3 @@
# 生产环境配置
NODE_ENV=production
VUE_APP_API_BASE_URL=/api

3
.env.test Normal file
View File

@ -0,0 +1,3 @@
# 测试环境配置
NODE_ENV=test
VUE_APP_API_BASE_URL=/api

44
package-lock.json generated
View File

@ -780,6 +780,29 @@
"webpack-merge": "^5.7.3",
"webpack-virtual-modules": "^0.4.2",
"whatwg-fetch": "^3.6.2"
},
"dependencies": {
"@vue/vue-loader-v15": {
"version": "npm:vue-loader@15.11.1",
"resolved": "https://registry.npmmirror.com/vue-loader/-/vue-loader-15.11.1.tgz",
"integrity": "sha512-0iw4VchYLePqJfJu9s62ACWUXeSqM30SQqlIftbYWM3C+jpPcEHKSPUZBLjSF9au4HTHQ/naF6OGnO3Q/qGR3Q==",
"dev": true,
"requires": {
"@vue/component-compiler-utils": "^3.1.0",
"hash-sum": "^1.0.2",
"loader-utils": "^1.1.0",
"vue-hot-reload-api": "^2.3.0",
"vue-style-loader": "^4.1.0"
},
"dependencies": {
"hash-sum": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/hash-sum/-/hash-sum-1.0.2.tgz",
"integrity": "sha512-fUs4B4L+mlt8/XAtSOGMUO1TXmAelItBPtJG7CyHJfYTdDjwisntGO2JQz7oUsatOY9o68+57eziUVNw/mRHmA==",
"dev": true
}
}
}
}
},
"@vue/cli-shared-utils": {
@ -870,27 +893,6 @@
}
}
},
"@vue/vue-loader-v15": {
"version": "npm:vue-loader@15.11.1",
"resolved": "https://registry.npmmirror.com/vue-loader/-/vue-loader-15.11.1.tgz",
"integrity": "sha512-0iw4VchYLePqJfJu9s62ACWUXeSqM30SQqlIftbYWM3C+jpPcEHKSPUZBLjSF9au4HTHQ/naF6OGnO3Q/qGR3Q==",
"dev": true,
"requires": {
"@vue/component-compiler-utils": "^3.1.0",
"hash-sum": "^1.0.2",
"loader-utils": "^1.1.0",
"vue-hot-reload-api": "^2.3.0",
"vue-style-loader": "^4.1.0"
},
"dependencies": {
"hash-sum": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/hash-sum/-/hash-sum-1.0.2.tgz",
"integrity": "sha512-fUs4B4L+mlt8/XAtSOGMUO1TXmAelItBPtJG7CyHJfYTdDjwisntGO2JQz7oUsatOY9o68+57eziUVNw/mRHmA==",
"dev": true
}
}
},
"@vue/web-component-wrapper": {
"version": "1.3.0",
"resolved": "https://registry.npmmirror.com/@vue/web-component-wrapper/-/web-component-wrapper-1.3.0.tgz",

View File

@ -1,180 +1,84 @@
// API服务层用于与后端进行交互
const API_BASE_URL = '/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;
}
}
import { get, post, put, del } from './request';
// 区域管理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'
})
getAll: () => get('/regions'),
getById: (id) => get(`/regions/${id}`),
create: (data) => post('/regions', data),
update: (id, data) => put(`/regions/${id}`, data),
delete: (id) => del(`/regions/${id}`)
};
// 构建查询字符串
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'
})
getAll: (params = {}) => get('/apartments', params),
getById: (id) => get(`/apartments/${id}`),
create: (data) => post('/apartments', data),
update: (id, data) => put(`/apartments/${id}`, data),
delete: (id) => del(`/apartments/${id}`)
};
// 房间管理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'
})
getAll: (params = {}) => get('/rooms', params),
getById: (id) => get(`/rooms/${id}`),
create: (data) => post('/rooms', data),
update: (id, data) => put(`/rooms/${id}`, data),
delete: (id) => del(`/rooms/${id}`)
};
// 租客管理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'
})
getAll: () => get('/tenants'),
getById: (id) => get(`/tenants/${id}`),
create: (data) => post('/tenants', data),
update: (id, data) => put(`/tenants/${id}`, data),
delete: (id) => del(`/tenants/${id}`)
};
// 合同管理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'
})
getAll: () => get('/contracts'),
getById: (id) => get(`/contracts/${id}`),
create: (data) => post('/contracts', data),
update: (id, data) => put(`/contracts/${id}`, data),
delete: (id) => del(`/contracts/${id}`)
};
// 租房管理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'
})
getAll: (params = {}) => get('/rentals', params),
getById: (id) => get(`/rentals/${id}`),
create: (data) => post('/rentals', data),
update: (id, data) => put(`/rentals/${id}`, data),
delete: (id) => del(`/rentals/${id}`)
};
// 统计分析API
export const statisticsApi = {
getRentData: () => request('/statistics/rent'),
getRoomStatus: () => request('/statistics/room-status'),
getRegionHouseStats: () => request('/statistics/region-house'),
getRegionApartmentHouseStats: () => request('/statistics/region-apartment-house')
getRentData: () => get('/statistics/rent'),
getRoomStatus: () => get('/statistics/room-status'),
getRegionHouseStats: () => get('/statistics/region-house'),
getRegionApartmentHouseStats: () => get('/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'
})
getAll: (params = {}) => get('/water-bills', params),
getById: (id) => get(`/water-bills/${id}`),
create: (data) => post('/water-bills', data),
update: (id, data) => put(`/water-bills/${id}`, data),
delete: (id) => del(`/water-bills/${id}`)
};
// 电费管理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'
})
getAll: (params = {}) => get('/electricity-bills', params),
getById: (id) => get(`/electricity-bills/${id}`),
create: (data) => post('/electricity-bills', data),
update: (id, data) => put(`/electricity-bills/${id}`, data),
delete: (id) => del(`/electricity-bills/${id}`)
};
export default {

70
src/api/request.js Normal file
View File

@ -0,0 +1,70 @@
// 统一请求处理
const API_BASE_URL = process.env.VUE_APP_API_BASE_URL || '/api';
// 通用请求函数
export 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;
}
}
// GET 请求
export function get(url, params = {}) {
const queryString = Object.entries(params)
.filter(([key, value]) => value !== undefined && value !== null && value !== '')
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
.join('&');
const fullUrl = queryString ? `${url}?${queryString}` : url;
return request(fullUrl, {
method: 'GET'
});
}
// POST 请求
export function post(url, data = {}) {
return request(url, {
method: 'POST',
body: JSON.stringify(data)
});
}
// PUT 请求
export function put(url, data = {}) {
return request(url, {
method: 'PUT',
body: JSON.stringify(data)
});
}
// DELETE 请求
export function del(url) {
return request(url, {
method: 'DELETE'
});
}
export default {
get,
post,
put,
delete: del,
request
};

View File

@ -37,11 +37,13 @@
</el-table>
<div class="pagination" style="margin-top: 20px;">
<el-pagination
layout="prev, pager, next"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
:page-size="pageSize"
:page-sizes="[10, 20, 50, 100]"
:current-page="currentPage"
@current-change="handleCurrentChange"
@size-change="handleSizeChange"
></el-pagination>
</div>
</el-card>
@ -135,7 +137,12 @@ export default {
},
handleCurrentChange(val) {
this.currentPage = val
this.loadData()
this.loadApartments()
},
handleSizeChange(val) {
this.pageSize = val
this.currentPage = 1
this.loadApartments()
}
}
}

View File

@ -19,7 +19,7 @@
<el-button @click="resetSearch">重置</el-button>
</el-form-item>
</el-form>
<el-table :data="filteredContracts" style="width: 100%">
<el-table :data="contracts" style="width: 100%" v-loading="isLoading">
<el-table-column prop="id" label="ID" width="80"></el-table-column>
<el-table-column prop="regionName" label="区域"></el-table-column>
<el-table-column prop="apartmentName" label="公寓"></el-table-column>
@ -39,11 +39,13 @@
</el-table>
<div class="pagination" style="margin-top: 20px;">
<el-pagination
layout="prev, pager, next"
:total="filteredContracts.length"
:page-size="10"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
:page-size="pageSize"
:page-sizes="[10, 20, 50, 100]"
:current-page="currentPage"
@current-change="handleCurrentChange"
@size-change="handleSizeChange"
></el-pagination>
</div>
</el-card>
@ -58,17 +60,13 @@ export default {
data() {
return {
contracts: [],
total: 0,
searchForm: {
status: ''
},
currentPage: 1
}
},
computed: {
filteredContracts() {
return this.contracts.filter(contract => {
return !this.searchForm.status || contract.status == this.searchForm.status
})
currentPage: 1,
pageSize: 10,
isLoading: false
}
},
mounted() {
@ -76,12 +74,19 @@ export default {
},
methods: {
async loadContracts() {
this.isLoading = true
try {
//
const contractsResponse = await contractApi.getAll()
const params = {
status: this.searchForm.status,
page: this.currentPage,
pageSize: this.pageSize
}
const contractsResponse = await contractApi.getAll(params)
// 使
this.contracts = contractsResponse.map(contract => {
const contracts = contractsResponse.data || contractsResponse
this.contracts = contracts.map(contract => {
return {
...contract,
roomNumber: contract.Room ? contract.Room.roomNumber : '',
@ -90,8 +95,11 @@ export default {
regionName: contract.Room && contract.Room.Apartment && contract.Room.Apartment.Region ? contract.Room.Apartment.Region.name : ''
}
})
this.total = contractsResponse.total || 0
} catch (error) {
this.$message.error('加载数据失败')
} finally {
this.isLoading = false
}
},
handleAdd() {
@ -118,15 +126,24 @@ export default {
})
},
handleSearch() {
//
this.currentPage = 1
this.loadContracts()
},
resetSearch() {
this.searchForm = {
status: ''
}
this.currentPage = 1
this.loadContracts()
},
handleCurrentChange(val) {
this.currentPage = val
this.loadContracts()
},
handleSizeChange(val) {
this.pageSize = val
this.currentPage = 1
this.loadContracts()
}
}
}

View File

@ -27,7 +27,7 @@
<el-button @click="resetSearch">重置</el-button>
</el-form-item>
</el-form>
<el-table :data="filteredHouses" style="width: 100%">
<el-table :data="houses" style="width: 100%" v-loading="isLoading">
<el-table-column prop="id" label="ID" width="80"></el-table-column>
<el-table-column prop="regionName" label="区域"></el-table-column>
<el-table-column prop="address" label="地址"></el-table-column>
@ -48,11 +48,13 @@
</el-table>
<div class="pagination" style="margin-top: 20px;">
<el-pagination
layout="prev, pager, next"
:total="filteredHouses.length"
:page-size="10"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
:page-size="pageSize"
:page-sizes="[10, 20, 50, 100]"
:current-page="currentPage"
@current-change="handleCurrentChange"
@size-change="handleSizeChange"
></el-pagination>
</div>
</el-card>
@ -68,20 +70,14 @@ export default {
return {
regions: [],
houses: [],
total: 0,
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
})
currentPage: 1,
pageSize: 10,
isLoading: false
}
},
mounted() {
@ -89,22 +85,33 @@ export default {
},
methods: {
async loadData() {
this.isLoading = true
try {
//
const regionsResponse = await regionApi.getAll()
this.regions = regionsResponse
//
const housesResponse = await houseApi.getAll()
this.houses = housesResponse.map(house => {
const params = {
regionId: this.searchForm.regionId,
status: this.searchForm.status,
page: this.currentPage,
pageSize: this.pageSize
}
const housesResponse = await houseApi.getAll(params)
const houses = housesResponse.data || housesResponse
this.houses = houses.map(house => {
const region = regionsResponse.find(r => r.id == house.regionId)
return {
...house,
regionName: region ? region.name : ''
}
})
this.total = housesResponse.total || 0
} catch (error) {
this.$message.error('加载数据失败')
} finally {
this.isLoading = false
}
},
getStatusType(status) {
@ -147,16 +154,25 @@ export default {
})
},
handleSearch() {
//
this.currentPage = 1
this.loadData()
},
resetSearch() {
this.searchForm = {
regionId: '',
status: ''
}
this.currentPage = 1
this.loadData()
},
handleCurrentChange(val) {
this.currentPage = val
this.loadData()
},
handleSizeChange(val) {
this.pageSize = val
this.currentPage = 1
this.loadData()
}
}
}

View File

@ -7,7 +7,7 @@
<el-button type="primary" @click="handleAdd">添加区域</el-button>
</div>
</template>
<el-table :data="regions" style="width: 100%">
<el-table :data="regions" style="width: 100%" v-loading="isLoading">
<el-table-column prop="id" label="ID" width="80"></el-table-column>
<el-table-column prop="name" label="区域名称"></el-table-column>
<el-table-column prop="description" label="区域描述"></el-table-column>
@ -21,11 +21,13 @@
</el-table>
<div class="pagination" style="margin-top: 20px;">
<el-pagination
layout="prev, pager, next"
:total="regions.length"
:page-size="10"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
:page-size="pageSize"
:page-sizes="[10, 20, 50, 100]"
:current-page="currentPage"
@current-change="handleCurrentChange"
@size-change="handleSizeChange"
></el-pagination>
</div>
</el-card>
@ -40,7 +42,10 @@ export default {
data() {
return {
regions: [],
currentPage: 1
total: 0,
currentPage: 1,
pageSize: 10,
isLoading: false
}
},
mounted() {
@ -48,11 +53,19 @@ export default {
},
methods: {
async loadRegions() {
this.isLoading = true
try {
const response = await regionApi.getAll()
this.regions = response
const params = {
page: this.currentPage,
pageSize: this.pageSize
}
const response = await regionApi.getAll(params)
this.regions = response.data || response
this.total = response.total || 0
} catch (error) {
this.$message.error('加载区域数据失败')
} finally {
this.isLoading = false
}
},
handleAdd() {
@ -62,7 +75,7 @@ export default {
this.$router.push(`/region/edit/${id}`)
},
async handleDelete(id) {
this.$confirm('确定要删除这个区域吗?', '提示', {
this.$confirm('确定要删除这个区域吗', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
@ -75,11 +88,17 @@ export default {
this.$message.error('删除失败')
}
}).catch(() => {
this.$message.info('已取消删除')
//
})
},
handleCurrentChange(val) {
this.currentPage = val
this.loadRegions()
},
handleSizeChange(val) {
this.pageSize = val
this.currentPage = 1
this.loadRegions()
}
}
}

View File

@ -34,8 +34,19 @@
type="date"
placeholder="选择开始日期"
style="width: 100%"
@change="updateEndDate"
></el-date-picker>
</el-form-item>
<el-form-item label="租期" prop="leaseMonths">
<el-select v-model="leaseMonths" placeholder="请选择租期" style="width: 100%" @change="updateEndDate">
<el-option
v-for="option in monthOptions"
:key="option.value"
:label="option.label"
:value="option.value"
></el-option>
</el-select>
</el-form-item>
<el-form-item label="结束日期" prop="endDate">
<el-date-picker
v-model="rentalForm.endDate"
@ -44,7 +55,7 @@
style="width: 100%"
></el-date-picker>
</el-form-item>
<el-form-item label="租金(元/月)" prop="rent">
<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">
@ -88,22 +99,45 @@ export default {
status: 'active',
remark: ''
},
//
returnQuery: {},
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' }],
tenantPhone: [{ message: '请输入租客电话', trigger: 'blur' }],
tenantIdCard: [{ 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' }],
deposit: [{ message: '请输入押金', trigger: 'blur' }],
status: [{ required: true, message: '请选择状态', trigger: 'blur' }]
},
rooms: [],
apartments: []
apartments: [],
leaseMonths: 1,
monthOptions: [
{ label: '1个月', value: 1 },
{ label: '2个月', value: 2 },
{ label: '3个月', value: 3 },
{ label: '4个月', value: 4 },
{ label: '5个月', value: 5 },
{ label: '6个月', value: 6 },
{ label: '7个月', value: 7 },
{ label: '8个月', value: 8 },
{ label: '9个月', value: 9 },
{ label: '10个月', value: 10 },
{ label: '11个月', value: 11 },
{ label: '12个月', value: 12 }
]
}
},
mounted() {
//
this.returnQuery = this.$route.query
//
this.rentalForm.startDate = new Date()
//
this.updateEndDate()
this.loadData()
},
methods: {
@ -128,7 +162,7 @@ export default {
const room = this.rooms.find(r => r.id.toString() == roomId.toString())
if (room) {
//
this.rentalForm.rent = room.price
this.rentalForm.rent = room.monthlyPrice
}
}
} catch (error) {
@ -148,7 +182,10 @@ export default {
try {
await rentalApi.create(this.rentalForm)
this.$message.success('添加成功')
this.$router.push('/rental/list')
this.$router.push({
path: '/rental/list',
query: this.returnQuery
})
} catch (error) {
this.$message.error('添加失败')
}
@ -162,7 +199,17 @@ export default {
this.$refs.rentalForm.resetFields()
},
goBack() {
this.$router.push('/rental/list')
this.$router.push({
path: '/rental/list',
query: this.returnQuery
})
},
updateEndDate() {
if (this.rentalForm.startDate) {
const endDate = new Date(this.rentalForm.startDate)
endDate.setMonth(endDate.getMonth() + this.leaseMonths)
this.rentalForm.endDate = endDate
}
}
}
}

View File

@ -7,9 +7,10 @@
<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 v-if="!room.otherStatus || room.otherStatus === ''" type="info" @click="handleCleaning">打扫</el-button>
<el-button v-if="!room.otherStatus || room.otherStatus === ''" type="danger" @click="handleMaintenance">维修</el-button>
<el-button v-if="room.otherStatus === 'cleaning'" type="success" @click="handleComplete">打扫完成</el-button>
<el-button v-if="room.otherStatus === 'maintenance'" type="success" @click="handleComplete">维修完成</el-button>
<el-button type="primary" @click="goBack">返回</el-button>
</div>
</div>
@ -23,12 +24,17 @@
</div>
<div class="info-item">
<span class="label">租金:</span>
<span class="value">¥{{ room.price }}/</span>
<span class="value">
¥{{ room.monthlyPrice }}/
<span v-if="room.yearlyPrice" style="margin-left: 10px;">¥{{ room.yearlyPrice }}/</span>
</span>
</div>
<div class="info-item">
<span class="label">状态:</span>
<span class="value">
<span class="value" style="display: flex; gap: 8px;">
<el-tag :type="getStatusType(room.status)">{{ getStatusText(room.status) }}</el-tag>
<el-tag v-if="room.status === 'rented' && room.subStatus" :type="getSubStatusType(room.subStatus)">{{ getSubStatusText(room.subStatus) }}</el-tag>
<el-tag v-if="room.otherStatus && room.otherStatus !== ''" :type="getOtherStatusType(room.otherStatus)">{{ getOtherStatusText(room.otherStatus) }}</el-tag>
</span>
</div>
<div class="info-item">
@ -55,6 +61,12 @@
</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 type="primary" size="small" @click="handleEditRental(scope.row)">编辑</el-button>
<el-button type="danger" size="small" @click="handleDeleteRental(scope.row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
<el-tab-pane label="水费记录" name="water">
@ -174,6 +186,46 @@
<el-button type="primary" @click="handleSaveElectricityBill">保存</el-button>
</span>
</el-dialog>
<!-- 租赁编辑对话框 -->
<el-dialog title="编辑租赁记录" :visible.sync="rentalDialogVisible" width="500px">
<el-form :model="rentalForm" :rules="rentalRules" ref="rentalForm">
<el-form-item label="租客姓名" prop="tenantName">
<el-input v-model="rentalForm.tenantName" placeholder="请输入租客姓名"></el-input>
</el-form-item>
<el-form-item label="租客电话" prop="tenantPhone">
<el-input v-model="rentalForm.tenantPhone" placeholder="请输入租客电话"></el-input>
</el-form-item>
<el-form-item label="身份证号" prop="tenantIdCard">
<el-input v-model="rentalForm.tenantIdCard" placeholder="请输入身份证号"></el-input>
</el-form-item>
<el-form-item label="开始日期" prop="startDate">
<el-date-picker v-model="rentalForm.startDate" type="date" placeholder="选择开始日期" style="width: 100%"></el-date-picker>
</el-form-item>
<el-form-item label="结束日期" prop="endDate">
<el-date-picker v-model="rentalForm.endDate" type="date" placeholder="选择结束日期" style="width: 100%"></el-date-picker>
</el-form-item>
<el-form-item label="租金(元)" prop="rent">
<el-input v-model.number="rentalForm.rent" placeholder="请输入租金"></el-input>
</el-form-item>
<el-form-item label="押金(元)" prop="deposit">
<el-input v-model.number="rentalForm.deposit" placeholder="请输入押金"></el-input>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="rentalForm.status" placeholder="请选择状态" style="width: 100%">
<el-option label="有效" value="active"></el-option>
<el-option label="到期" value="expired"></el-option>
</el-select>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="rentalForm.remark" type="textarea" rows="3" placeholder="请输入备注信息"></el-input>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="rentalDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSaveRental">保存</el-button>
</span>
</el-dialog>
</el-card>
</div>
</template>
@ -193,6 +245,8 @@ export default {
electricityBills: [],
isLoading: false,
activeTab: 'rental',
//
returnQuery: {},
waterBillDialogVisible: false,
electricityBillDialogVisible: false,
waterBillForm: {
@ -230,10 +284,34 @@ export default {
endReading: [{ required: true, message: '请输入结束度数', trigger: 'blur' }],
unitPrice: [{ required: true, message: '请输入单价', trigger: 'blur' }],
status: [{ required: true, message: '请选择状态', trigger: 'change' }]
},
rentalDialogVisible: false,
rentalForm: {
id: '',
roomId: '',
tenantName: '',
tenantPhone: '',
tenantIdCard: '',
startDate: '',
endDate: '',
rent: '',
deposit: '',
status: 'active',
remark: ''
},
rentalRules: {
tenantName: [{ required: true, message: '请输入租客姓名', trigger: 'blur' }],
startDate: [{ required: true, message: '请选择开始日期', trigger: 'blur' }],
endDate: [{ required: true, message: '请选择结束日期', trigger: 'blur' }],
rent: [{ required: true, message: '请输入租金', trigger: 'blur' }],
deposit: [{ message: '请输入押金', trigger: 'blur' }],
status: [{ required: true, message: '请选择状态', trigger: 'blur' }]
}
}
},
mounted() {
//
this.returnQuery = this.$route.query
this.loadData()
},
methods: {
@ -309,8 +387,44 @@ export default {
default: return status
}
},
getSubStatusType(status) {
switch (status) {
case 'normal': return 'success'
case 'soon_expire': return 'warning'
case 'expired': return 'danger'
default: return ''
}
},
getSubStatusText(status) {
switch (status) {
case 'normal': return '正常'
case 'soon_expire': return '即将到期'
case 'expired': return '已到期'
default: return status
}
},
getOtherStatusType(status) {
switch (status) {
case 'cleaning': return 'info'
case 'maintenance': return 'danger'
default: return ''
}
},
getOtherStatusText(status) {
switch (status) {
case 'cleaning': return '打扫中'
case 'maintenance': return '维修中'
default: return status
}
},
handleRent() {
this.$router.push(`/rental/add?roomId=${this.room.id}`)
this.$router.push({
path: `/rental/add`,
query: {
roomId: this.room.id,
...this.returnQuery
}
})
},
async handleCheckout() {
this.$confirm('确定要退房吗?', '提示', {
@ -350,7 +464,7 @@ export default {
}).then(async () => {
try {
const roomId = this.$route.params.id
await roomApi.update(roomId, { status: 'cleaning' })
await roomApi.update(roomId, { otherStatus: 'cleaning' })
this.$message.success('标记为打扫中成功')
this.loadData()
} catch (error) {
@ -368,7 +482,7 @@ export default {
}).then(async () => {
try {
const roomId = this.$route.params.id
await roomApi.update(roomId, { status: 'maintenance' })
await roomApi.update(roomId, { otherStatus: 'maintenance' })
this.$message.success('标记为维修中成功')
this.loadData()
} catch (error) {
@ -379,15 +493,15 @@ export default {
})
},
async handleComplete() {
this.$confirm('确定要完成打扫/维修并将状态改为空房吗?', '提示', {
this.$confirm('确定要完成打扫/维修吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'success'
}).then(async () => {
try {
const roomId = this.$route.params.id
await roomApi.update(roomId, { status: 'empty' })
this.$message.success('操作成功,房间状态已改为空房')
await roomApi.update(roomId, { otherStatus: '' })
this.$message.success('操作成功,已完成打扫/维修')
this.loadData()
} catch (error) {
this.$message.error('操作失败')
@ -514,8 +628,53 @@ export default {
//
})
},
handleEditRental(rental) {
this.rentalForm = {
...rental,
startDate: rental.startDate ? new Date(rental.startDate) : '',
endDate: rental.endDate ? new Date(rental.endDate) : ''
}
this.rentalDialogVisible = true
},
async handleSaveRental() {
try {
if (this.rentalForm.id) {
//
await rentalApi.update(this.rentalForm.id, this.rentalForm)
this.$message.success('租赁记录更新成功')
} else {
//
await rentalApi.create(this.rentalForm)
this.$message.success('租赁记录添加成功')
}
this.rentalDialogVisible = false
this.loadData()
} catch (error) {
this.$message.error('操作失败')
}
},
async handleDeleteRental(id) {
this.$confirm('确定要删除这条租赁记录吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'danger'
}).then(async () => {
try {
await rentalApi.delete(id)
this.$message.success('租赁记录删除成功')
this.loadData()
} catch (error) {
this.$message.error('删除失败')
}
}).catch(() => {
//
})
},
goBack() {
this.$router.push('/rental/list')
this.$router.push({
path: '/rental/list',
query: this.returnQuery
})
}
}
}

View File

@ -44,7 +44,7 @@
style="width: 100%"
></el-date-picker>
</el-form-item>
<el-form-item label="租金(元/月)" prop="rent">
<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">
@ -85,15 +85,17 @@ export default {
deposit: '',
status: ''
},
//
returnQuery: {},
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' }],
tenantPhone: [{ message: '请输入租客电话', trigger: 'blur' }],
tenantIdCard: [{ 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' }],
deposit: [{ message: '请输入押金', trigger: 'blur' }],
status: [{ required: true, message: '请选择状态', trigger: 'blur' }]
},
rooms: [],
@ -101,6 +103,8 @@ export default {
}
},
mounted() {
//
this.returnQuery = this.$route.query
this.loadData()
this.loadRentalData()
},
@ -144,7 +148,10 @@ export default {
try {
await rentalApi.update(this.rentalForm.id, this.rentalForm)
this.$message.success('编辑成功')
this.$router.push('/rental/list')
this.$router.push({
path: '/rental/list',
query: this.returnQuery
})
} catch (error) {
this.$message.error('编辑失败')
}
@ -158,7 +165,10 @@ export default {
this.loadRentalData()
},
goBack() {
this.$router.push('/rental/list')
this.$router.push({
path: '/rental/list',
query: this.returnQuery
})
}
}
}

View File

@ -13,17 +13,31 @@
<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-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-select>
</el-form-item>
<el-form-item label="附属状态">
<el-select v-model="searchForm.subStatus" placeholder="请选择附属状态">
<el-option label="全部" value=""></el-option>
<el-option label="正常" value="normal"></el-option>
<el-option label="即将到期" value="soon_expire"></el-option>
<el-option label="已到期" value="expired"></el-option>
</el-select>
</el-form-item>
<el-form-item label="其他状态">
<el-select v-model="searchForm.otherStatus" placeholder="请选择其他状态">
<el-option label="全部" value=""></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>
@ -37,17 +51,24 @@
style="cursor: pointer;"
>
<el-card
:class="['room-card', `status-${room.status}`]"
:class="['room-card', `status-${room.status === 'rented' ? room.subStatus : room.status}`]"
>
<div class="room-card-header">
<h3>{{ getApartmentName(room.apartmentId) }}</h3>
<el-tag :type="getStatusType(room.status)">{{ getStatusText(room.status) }}</el-tag>
<div style="display: flex; gap: 8px;">
<el-tag v-if="room.status === 'rented'" :type="getSubStatusType(room.subStatus)">{{ getSubStatusText(room.subStatus) }}</el-tag>
<el-tag v-else>空房</el-tag>
<el-tag v-if="room.otherStatus" :type="getOtherStatusType(room.otherStatus)">{{ getOtherStatusText(room.otherStatus) }}</el-tag>
</div>
</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 class="price-info">
<span class="room-price">¥{{ room.monthlyPrice }}/</span>
<span v-if="room.yearlyPrice" class="room-price yearly">¥{{ room.yearlyPrice }}/</span>
</div>
</div>
<div class="rental-info" v-if="room.Rentals && room.Rentals.length > 0">
<p>租客: {{ room.Rentals[0].Tenant.name }}</p>
@ -87,7 +108,10 @@ export default {
total: 0,
searchForm: {
apartmentId: '',
status: ''
status: '',
subStatus: '',
otherStatus: '',
roomNumber: ''
},
currentPage: 1,
pageSize: 50,
@ -95,9 +119,26 @@ export default {
}
},
mounted() {
//
this.initFromQuery()
this.loadData()
},
methods: {
//
initFromQuery() {
const query = this.$route.query
if (query) {
//
if (query.apartmentId) this.searchForm.apartmentId = parseInt(query.apartmentId)
if (query.status) this.searchForm.status = query.status
if (query.subStatus) this.searchForm.subStatus = query.subStatus
if (query.otherStatus) this.searchForm.otherStatus = query.otherStatus
if (query.roomNumber) this.searchForm.roomNumber = query.roomNumber
//
if (query.page) this.currentPage = parseInt(query.page)
if (query.pageSize) this.pageSize = parseInt(query.pageSize)
}
},
async loadData() {
if (this.isLoading) return
@ -111,6 +152,9 @@ export default {
const params = {
apartmentId: this.searchForm.apartmentId,
status: this.searchForm.status,
subStatus: this.searchForm.subStatus,
otherStatus: this.searchForm.otherStatus,
roomNumber: this.searchForm.roomNumber,
page: this.currentPage,
pageSize: this.pageSize
}
@ -135,23 +179,31 @@ export default {
const apartment = this.apartments.find(a => a.id == apartmentId)
return apartment ? apartment.name : ''
},
getStatusType(status) {
getSubStatusType(status) {
switch (status) {
case 'empty': return ''
case 'rented': return 'success'
case 'normal': return 'success'
case 'soon_expire': return 'warning'
case 'expired': return 'danger'
default: return ''
}
},
getSubStatusText(status) {
switch (status) {
case 'normal': return '正常'
case 'soon_expire': return '即将到期'
case 'expired': return '已到期'
default: return status
}
},
getOtherStatusType(status) {
switch (status) {
case 'cleaning': return 'info'
case 'maintenance': return 'danger'
default: return ''
}
},
getStatusText(status) {
getOtherStatusText(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
@ -163,7 +215,15 @@ export default {
},
handleRoomClick(roomId) {
try {
this.$router.push(`/rental/detail/${roomId}`)
//
this.$router.push({
path: `/rental/detail/${roomId}`,
query: {
...this.searchForm,
page: this.currentPage,
pageSize: this.pageSize
}
})
} catch (error) {
console.error('Navigation error:', error)
}
@ -175,18 +235,39 @@ export default {
resetSearch() {
this.searchForm = {
apartmentId: '',
status: ''
status: '',
subStatus: '',
otherStatus: '',
roomNumber: ''
}
this.currentPage = 1
this.loadData()
},
handleCurrentChange(val) {
this.currentPage = val
//
this.$router.push({
path: this.$route.path,
query: {
...this.searchForm,
page: this.currentPage,
pageSize: this.pageSize
}
})
this.loadData()
},
handleSizeChange(val) {
this.pageSize = val
this.currentPage = 1
//
this.$router.push({
path: this.$route.path,
query: {
...this.searchForm,
page: this.currentPage,
pageSize: this.pageSize
}
})
this.loadData()
}
}
@ -259,6 +340,7 @@ export default {
.room-info {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
padding-bottom: 15px;
border-bottom: 1px solid #e4e7ed;
@ -274,11 +356,23 @@ export default {
color: #606266;
}
.price-info {
display: flex;
flex-direction: column;
align-items: flex-end;
}
.room-price {
color: #409EFF;
font-weight: 500;
}
.room-price.yearly {
font-size: 12px;
color: #909399;
margin-top: 2px;
}
.rental-info {
margin-top: 10px;
flex: 1;
@ -298,7 +392,7 @@ export default {
background-color: #f9f9f9;
}
.status-rented {
.status-normal {
border-color: #67c23a;
background-color: #f0f9eb;
}
@ -312,14 +406,4 @@ export default {
border-color: #f56c6c;
background-color: #fef0f0;
}
.status-cleaning {
border-color: #909399;
background-color: #f4f4f5;
}
.status-maintenance {
border-color: #303133;
background-color: #f4f4f5;
}
</style>

View File

@ -18,15 +18,30 @@
<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 label="月租金(元)" prop="monthlyPrice">
<el-input type="number" v-model="roomForm.monthlyPrice" placeholder="请输入月租金"></el-input>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="roomForm.status" placeholder="请选择状态">
<el-form-item label="年租金(元)" prop="yearlyPrice">
<el-input type="number" v-model="roomForm.yearlyPrice" 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-select>
</el-form-item>
<el-form-item label="附属状态" prop="subStatus" v-if="roomForm.status === 'rented'">
<el-select v-model="roomForm.subStatus" placeholder="请选择附属状态">
<el-option label="正常" value="normal"></el-option>
<el-option label="即将到期" value="soon_expire"></el-option>
<el-option label="已到期" value="expired"></el-option>
</el-select>
</el-form-item>
<el-form-item label="其他状态" prop="otherStatus">
<el-select v-model="roomForm.otherStatus" placeholder="请选择其他状态">
<el-option label="无" value=""></el-option>
<el-option label="打扫中" value="cleaning"></el-option>
<el-option label="维修中" value="maintenance"></el-option>
</el-select>
@ -52,8 +67,11 @@ export default {
apartmentId: '',
roomNumber: '',
area: '',
price: '',
status: 'empty'
monthlyPrice: '',
yearlyPrice: '',
status: 'empty',
otherStatus: '',
subStatus: 'normal'
},
apartments: [],
rules: {
@ -75,7 +93,18 @@ export default {
}
}, trigger: 'blur' }
],
price: [
monthlyPrice: [
{ validator: (rule, value, callback) => {
if (value === '' || value === null || value === undefined) {
callback()
} else if (isNaN(Number(value))) {
callback(new Error('请输入数字'))
} else {
callback()
}
}, trigger: 'blur' }
],
yearlyPrice: [
{ validator: (rule, value, callback) => {
if (value === '' || value === null || value === undefined) {
callback()

View File

@ -18,15 +18,30 @@
<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 label="月租金(元)" prop="monthlyPrice">
<el-input type="number" v-model="roomForm.monthlyPrice" placeholder="请输入月租金"></el-input>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="roomForm.status" placeholder="请选择状态">
<el-form-item label="年租金(元)" prop="yearlyPrice">
<el-input type="number" v-model="roomForm.yearlyPrice" 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-select>
</el-form-item>
<el-form-item label="附属状态" prop="subStatus" v-if="roomForm.status === 'rented'">
<el-select v-model="roomForm.subStatus" placeholder="请选择附属状态">
<el-option label="正常" value="normal"></el-option>
<el-option label="即将到期" value="soon_expire"></el-option>
<el-option label="已到期" value="expired"></el-option>
</el-select>
</el-form-item>
<el-form-item label="其他状态" prop="otherStatus">
<el-select v-model="roomForm.otherStatus" placeholder="请选择其他状态">
<el-option label="无" value=""></el-option>
<el-option label="打扫中" value="cleaning"></el-option>
<el-option label="维修中" value="maintenance"></el-option>
</el-select>
@ -53,9 +68,14 @@ export default {
apartmentId: '',
roomNumber: '',
area: '',
price: '',
status: ''
monthlyPrice: '',
yearlyPrice: '',
status: '',
otherStatus: '',
subStatus: 'normal'
},
//
returnQuery: {},
apartments: [],
rules: {
apartmentId: [
@ -76,7 +96,18 @@ export default {
}
}, trigger: 'blur' }
],
price: [
monthlyPrice: [
{ validator: (rule, value, callback) => {
if (value === '' || value === null || value === undefined) {
callback()
} else if (isNaN(Number(value))) {
callback(new Error('请输入数字'))
} else {
callback()
}
}, trigger: 'blur' }
],
yearlyPrice: [
{ validator: (rule, value, callback) => {
if (value === '' || value === null || value === undefined) {
callback()
@ -94,6 +125,8 @@ export default {
}
},
mounted() {
//
this.returnQuery = this.$route.query
this.loadApartments()
this.loadRoomData()
},
@ -123,7 +156,10 @@ export default {
try {
await roomApi.update(this.roomForm.id, this.roomForm)
this.$message.success('编辑成功')
this.$router.push('/room/list')
this.$router.push({
path: '/room/list',
query: this.returnQuery
})
} catch (error) {
this.$message.error('编辑失败')
}
@ -136,7 +172,10 @@ export default {
this.loadRoomData()
},
goBack() {
this.$router.push('/room/list')
this.$router.push({
path: '/room/list',
query: this.returnQuery
})
}
}
}

View File

@ -14,13 +14,24 @@
<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-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-select>
</el-form-item>
<el-form-item label="附属状态">
<el-select v-model="searchForm.subStatus" placeholder="请选择附属状态">
<el-option label="全部" value=""></el-option>
<el-option label="正常" value="normal"></el-option>
<el-option label="即将到期" value="soon_expire"></el-option>
<el-option label="已到期" value="expired"></el-option>
</el-select>
</el-form-item>
<el-form-item label="其他状态">
<el-select v-model="searchForm.otherStatus" placeholder="请选择其他状态">
<el-option label="全部" value=""></el-option>
<el-option label="打扫中" value="cleaning"></el-option>
<el-option label="维修中" value="maintenance"></el-option>
</el-select>
@ -40,14 +51,29 @@
<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">
<el-table-column prop="monthlyPrice" label="月租金(元)" width="120"></el-table-column>
<el-table-column prop="yearlyPrice" 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="subStatus" label="附属状态" width="120">
<template slot-scope="scope">
<el-tag v-if="scope.row.status === 'rented'" :type="getSubStatusType(scope.row.subStatus)">{{ getSubStatusText(scope.row.subStatus) }}</el-tag>
<span v-else></span>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="180"></el-table-column>
<el-table-column label="操作" width="150">
<el-table-column prop="otherStatus" label="其他状态" width="120">
<template slot-scope="scope">
<el-tag v-if="scope.row.otherStatus" :type="getOtherStatusType(scope.row.otherStatus)">{{ getOtherStatusText(scope.row.otherStatus) }}</el-tag>
<span v-else></span>
</template>
</el-table-column>
<el-table-column label="操作" width="200">
<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>
@ -56,11 +82,13 @@
</el-table>
<div class="pagination" style="margin-top: 20px;">
<el-pagination
layout="prev, pager, next"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
:page-size="pageSize"
:page-sizes="[10, 20, 50, 100]"
:current-page="currentPage"
@current-change="handleCurrentChange"
@size-change="handleSizeChange"
></el-pagination>
</div>
</el-card>
@ -74,12 +102,14 @@ export default {
name: 'RoomList',
data() {
return {
apartments: [],
rooms: [],
apartments: [],
total: 0,
searchForm: {
apartmentId: '',
status: '',
subStatus: '',
otherStatus: '',
roomNumber: ''
},
currentPage: 1,
@ -88,9 +118,26 @@ export default {
}
},
mounted() {
//
this.initFromQuery()
this.loadData()
},
methods: {
//
initFromQuery() {
const query = this.$route.query
if (query) {
//
if (query.apartmentId) this.searchForm.apartmentId = parseInt(query.apartmentId)
if (query.status) this.searchForm.status = query.status
if (query.subStatus) this.searchForm.subStatus = query.subStatus
if (query.otherStatus) this.searchForm.otherStatus = query.otherStatus
if (query.roomNumber) this.searchForm.roomNumber = query.roomNumber
//
if (query.page) this.currentPage = parseInt(query.page)
if (query.pageSize) this.pageSize = parseInt(query.pageSize)
}
},
async loadData() {
if (this.isLoading) return
@ -104,6 +151,8 @@ export default {
const params = {
apartmentId: this.searchForm.apartmentId,
status: this.searchForm.status,
otherStatus: this.searchForm.otherStatus,
subStatus: this.searchForm.subStatus,
roomNumber: this.searchForm.roomNumber,
page: this.currentPage,
pageSize: this.pageSize
@ -142,10 +191,6 @@ export default {
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 ''
}
},
@ -153,18 +198,52 @@ export default {
switch (status) {
case 'empty': return '空房'
case 'rented': return '在租'
case 'soon_expire': return '即将到期'
case 'expired': return '到期'
default: return status
}
},
getOtherStatusType(status) {
switch (status) {
case 'cleaning': return 'info'
case 'maintenance': return 'danger'
default: return ''
}
},
getOtherStatusText(status) {
switch (status) {
case 'cleaning': return '打扫中'
case 'maintenance': return '维修中'
default: return status
}
},
getSubStatusType(status) {
switch (status) {
case 'normal': return 'success'
case 'soon_expire': return 'warning'
case 'expired': return 'danger'
default: return ''
}
},
getSubStatusText(status) {
switch (status) {
case 'normal': return '正常'
case 'soon_expire': return '即将到期'
case 'expired': return '已到期'
default: return status
}
},
handleAdd() {
this.$router.push('/room/add')
},
handleEdit(id) {
this.$router.push(`/room/edit/${id}`)
//
this.$router.push({
path: `/room/edit/${id}`,
query: {
...this.searchForm,
page: this.currentPage,
pageSize: this.pageSize
}
})
},
async handleDelete(id) {
this.$confirm('确定要删除这个房间吗?', '提示', {
@ -191,6 +270,8 @@ export default {
this.searchForm = {
apartmentId: '',
status: '',
otherStatus: '',
subStatus: '',
roomNumber: ''
}
this.currentPage = 1
@ -199,6 +280,11 @@ export default {
handleCurrentChange(val) {
this.currentPage = val
this.loadData()
},
handleSizeChange(val) {
this.pageSize = val
this.currentPage = 1
this.loadData()
}
}
}

View File

@ -18,7 +18,7 @@
<el-button @click="resetSearch">重置</el-button>
</el-form-item>
</el-form>
<el-table :data="filteredTenants" style="width: 100%">
<el-table :data="tenants" style="width: 100%" v-loading="isLoading">
<el-table-column prop="id" label="ID" width="80"></el-table-column>
<el-table-column prop="name" label="姓名"></el-table-column>
<el-table-column prop="phone" label="电话"></el-table-column>
@ -28,11 +28,13 @@
</el-table>
<div class="pagination" style="margin-top: 20px;">
<el-pagination
layout="prev, pager, next"
:total="filteredTenants.length"
:page-size="10"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
:page-size="pageSize"
:page-sizes="[10, 20, 50, 100]"
:current-page="currentPage"
@current-change="handleCurrentChange"
@size-change="handleSizeChange"
></el-pagination>
</div>
</el-card>
@ -47,20 +49,14 @@ export default {
data() {
return {
tenants: [],
total: 0,
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
})
currentPage: 1,
pageSize: 10,
isLoading: false
}
},
mounted() {
@ -68,11 +64,21 @@ export default {
},
methods: {
async loadTenants() {
this.isLoading = true
try {
const response = await tenantApi.getAll()
this.tenants = response
const params = {
name: this.searchForm.name,
phone: this.searchForm.phone,
page: this.currentPage,
pageSize: this.pageSize
}
const response = await tenantApi.getAll(params)
this.tenants = response.data || response
this.total = response.total || 0
} catch (error) {
this.$message.error('加载租客数据失败')
} finally {
this.isLoading = false
}
},
handleAdd() {
@ -99,16 +105,25 @@ export default {
})
},
handleSearch() {
//
this.currentPage = 1
this.loadTenants()
},
resetSearch() {
this.searchForm = {
name: '',
phone: ''
}
this.currentPage = 1
this.loadTenants()
},
handleCurrentChange(val) {
this.currentPage = val
this.loadTenants()
},
handleSizeChange(val) {
this.pageSize = val
this.currentPage = 1
this.loadTenants()
}
}
}

14
vue.config.js Normal file
View File

@ -0,0 +1,14 @@
module.exports = {
devServer: {
proxy: {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true,
pathRewrite: {
'^/api': '/api'
}
}
}
},
lintOnSave: false
};