2026-04-20 06:23:11 +00:00
|
|
|
|
<template>
|
|
|
|
|
|
<view class="rooms-page">
|
|
|
|
|
|
<view class="custom-nav safe-area-top">
|
|
|
|
|
|
<view class="nav-content">
|
|
|
|
|
|
<view class="nav-btn" @click="goBack">
|
|
|
|
|
|
<uni-icons type="left" size="22" color="#1E293B"></uni-icons>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<text class="nav-title">房间管理</text>
|
|
|
|
|
|
<view class="nav-btn" @click="addRoom">
|
|
|
|
|
|
<uni-icons type="plus" size="22" color="#2563EB"></uni-icons>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
|
|
|
|
|
|
<view class="apartment-filter" v-if="apartments.length > 0">
|
|
|
|
|
|
<picker mode="selector" :range="apartmentOptions" :value="selectedApartmentIndex" @change="onApartmentChange">
|
|
|
|
|
|
<view class="picker-value">
|
|
|
|
|
|
<text>{{selectedApartmentLabel}}</text>
|
|
|
|
|
|
<uni-icons type="arrowdown" size="14" color="#64748B"></uni-icons>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</picker>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
|
|
|
|
|
|
<scroll-view scroll-y class="room-list" @scrolltolower="loadMore" refresher-enabled :refresher-triggered="isRefreshing" @refresherrefresh="onRefresh">
|
|
|
|
|
|
<view v-if="rooms.length === 0 && !isLoading" class="empty-state">
|
|
|
|
|
|
<view class="empty-icon">
|
|
|
|
|
|
<uni-icons type="home-filled" size="80" color="#CBD5E1"></uni-icons>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<text class="empty-title">暂无房间</text>
|
|
|
|
|
|
<text class="empty-desc">点击右上角添加房间</text>
|
|
|
|
|
|
<button class="empty-btn" @click="addRoom">添加房间</button>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
|
|
|
|
|
|
<view v-else class="room-grid">
|
2026-04-22 06:47:04 +00:00
|
|
|
|
<view v-for="(item, index) in rooms" :key="index" class="room-card" :class="getRoomStatusClass(item)" @click="viewRoomDetail(item)">
|
2026-04-20 06:23:11 +00:00
|
|
|
|
<view class="room-header">
|
|
|
|
|
|
<text class="room-number">{{item.roomNumber}}</text>
|
2026-04-22 06:47:04 +00:00
|
|
|
|
<view class="room-status">{{item.statusText}}</view>
|
2026-04-20 06:23:11 +00:00
|
|
|
|
</view>
|
|
|
|
|
|
<view class="room-info">
|
|
|
|
|
|
<text class="info-item">面积: {{item.area || '--'}} m²</text>
|
|
|
|
|
|
<text class="info-item">朝向: {{item.orientation || '--'}}</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view class="room-price" v-if="item.rent">¥{{item.rent}}/月</view>
|
|
|
|
|
|
<view class="room-tenant" v-if="item.renterName">
|
|
|
|
|
|
<uni-icons type="person" size="14" color="#64748B"></uni-icons>
|
|
|
|
|
|
<text>{{item.renterName}}</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
|
|
|
|
|
|
<uni-load-more v-if="rooms.length > 0" :status="loadStatus"></uni-load-more>
|
|
|
|
|
|
<view class="safe-area-bottom" style="height: 40rpx;"></view>
|
|
|
|
|
|
</scroll-view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
|
import roomApi from '@/api/room.js'
|
|
|
|
|
|
import apartmentApi from '@/api/apartment.js'
|
|
|
|
|
|
|
|
|
|
|
|
export default {
|
|
|
|
|
|
data() {
|
|
|
|
|
|
return {
|
|
|
|
|
|
isLoading: false,
|
|
|
|
|
|
isRefreshing: false,
|
|
|
|
|
|
rooms: [],
|
|
|
|
|
|
apartments: [],
|
|
|
|
|
|
selectedApartmentId: '',
|
|
|
|
|
|
loadStatus: 'more',
|
|
|
|
|
|
page: 1,
|
|
|
|
|
|
pageSize: 20
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
computed: {
|
|
|
|
|
|
apartmentOptions() {
|
|
|
|
|
|
return this.apartments.map(a => a.name)
|
|
|
|
|
|
},
|
|
|
|
|
|
selectedApartmentIndex() {
|
|
|
|
|
|
const index = this.apartments.findIndex(a => a.id === this.selectedApartmentId)
|
|
|
|
|
|
return index >= 0 ? index : 0
|
|
|
|
|
|
},
|
|
|
|
|
|
selectedApartmentLabel() {
|
|
|
|
|
|
const apartment = this.apartments.find(a => a.id === this.selectedApartmentId)
|
|
|
|
|
|
return apartment ? apartment.name : '选择公寓'
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
onLoad(options) {
|
|
|
|
|
|
if (options.apartmentId) {
|
|
|
|
|
|
this.selectedApartmentId = parseInt(options.apartmentId)
|
|
|
|
|
|
}
|
|
|
|
|
|
this.loadApartments()
|
|
|
|
|
|
},
|
|
|
|
|
|
onShow() {
|
|
|
|
|
|
this.loadRooms()
|
|
|
|
|
|
},
|
|
|
|
|
|
methods: {
|
|
|
|
|
|
async loadApartments() {
|
|
|
|
|
|
try {
|
2026-04-22 06:47:04 +00:00
|
|
|
|
const res = await apartmentApi.list()
|
2026-04-20 06:23:11 +00:00
|
|
|
|
if (res.data) {
|
|
|
|
|
|
this.apartments = res.data
|
|
|
|
|
|
if (!this.selectedApartmentId && this.apartments.length > 0) {
|
|
|
|
|
|
this.selectedApartmentId = this.apartments[0].id
|
|
|
|
|
|
}
|
2026-04-22 06:47:04 +00:00
|
|
|
|
// 注意:loadRooms 由 onShow 触发,避免重复请求
|
2026-04-20 06:23:11 +00:00
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('加载公寓列表失败:', error)
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
async loadRooms() {
|
|
|
|
|
|
if (!this.selectedApartmentId) return
|
|
|
|
|
|
this.isLoading = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const params = {
|
|
|
|
|
|
page: this.page,
|
|
|
|
|
|
pageSize: this.pageSize,
|
|
|
|
|
|
apartmentId: this.selectedApartmentId
|
|
|
|
|
|
}
|
|
|
|
|
|
const res = await roomApi.getList(params)
|
2026-04-22 06:47:04 +00:00
|
|
|
|
if (res.data && res.data.list) {
|
|
|
|
|
|
const list = res.data.list.map(item => ({
|
2026-04-20 06:23:11 +00:00
|
|
|
|
id: item.id,
|
|
|
|
|
|
roomNumber: item.roomNumber,
|
|
|
|
|
|
area: item.area,
|
|
|
|
|
|
orientation: item.orientation,
|
|
|
|
|
|
rent: item.rent,
|
2026-04-22 06:47:04 +00:00
|
|
|
|
status: this.getRoomStatus(item),
|
|
|
|
|
|
rentalStatus: item.rentalStatus,
|
|
|
|
|
|
statusText: this.getRoomStatusText(item),
|
|
|
|
|
|
renterName: item.Renter && item.Renter.name
|
2026-04-20 06:23:11 +00:00
|
|
|
|
}))
|
|
|
|
|
|
if (this.page === 1) {
|
|
|
|
|
|
this.rooms = list
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.rooms = [...this.rooms, ...list]
|
|
|
|
|
|
}
|
2026-04-22 06:47:04 +00:00
|
|
|
|
this.loadStatus = this.rooms.length < (res.data.total || 0) ? 'more' : 'noMore'
|
2026-04-20 06:23:11 +00:00
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('加载房间列表失败:', error)
|
|
|
|
|
|
uni.showToast({ title: '加载失败', icon: 'none' })
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
this.isLoading = false
|
|
|
|
|
|
this.isRefreshing = false
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
onRefresh() {
|
|
|
|
|
|
this.isRefreshing = true
|
|
|
|
|
|
this.page = 1
|
|
|
|
|
|
this.loadRooms()
|
|
|
|
|
|
},
|
|
|
|
|
|
onApartmentChange(e) {
|
|
|
|
|
|
this.selectedApartmentId = this.apartments[e.detail.value].id
|
|
|
|
|
|
this.page = 1
|
|
|
|
|
|
this.loadRooms()
|
|
|
|
|
|
},
|
|
|
|
|
|
addRoom() {
|
|
|
|
|
|
if (!this.selectedApartmentId) {
|
|
|
|
|
|
uni.showToast({ title: '请先选择公寓', icon: 'none' })
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
uni.navigateTo({ url: `/pages/room-add/room-add?apartmentId=${this.selectedApartmentId}` })
|
|
|
|
|
|
},
|
|
|
|
|
|
viewRoomDetail(item) {
|
|
|
|
|
|
uni.navigateTo({ url: `/pages/room-detail/room-detail?id=${item.id}` })
|
|
|
|
|
|
},
|
|
|
|
|
|
loadMore() {
|
|
|
|
|
|
if (this.loadStatus === 'noMore') return
|
|
|
|
|
|
this.loadStatus = 'loading'
|
|
|
|
|
|
this.page++
|
|
|
|
|
|
this.loadRooms()
|
|
|
|
|
|
},
|
|
|
|
|
|
goBack() {
|
|
|
|
|
|
uni.navigateBack()
|
2026-04-22 06:47:04 +00:00
|
|
|
|
},
|
|
|
|
|
|
getRoomStatus(room) {
|
|
|
|
|
|
// 与Web端Status.vue保持一致,直接使用room.status
|
|
|
|
|
|
return room.status || 'empty'
|
|
|
|
|
|
},
|
|
|
|
|
|
getRoomStatusText(room) {
|
|
|
|
|
|
// 与Web端Status.vue#getRoomStatusText保持一致
|
|
|
|
|
|
if (room.status === 'empty') return '空置'
|
|
|
|
|
|
if (room.status === 'rented') {
|
|
|
|
|
|
if (room.rentalStatus === 'expired') return '已到期'
|
|
|
|
|
|
if (room.rentalStatus === 'soon_expire') return '即将到期'
|
|
|
|
|
|
return '已租'
|
|
|
|
|
|
}
|
|
|
|
|
|
if (room.status === 'reserved') return '预定'
|
|
|
|
|
|
return room.status
|
|
|
|
|
|
},
|
|
|
|
|
|
getRoomStatusClass(room) {
|
|
|
|
|
|
// 根据status和rentalStatus确定样式类,与Web端保持一致
|
|
|
|
|
|
if (room.status === 'empty') return 'status-empty'
|
|
|
|
|
|
if (room.status === 'rented') {
|
|
|
|
|
|
if (room.rentalStatus === 'expired') return 'status-expired'
|
|
|
|
|
|
if (room.rentalStatus === 'soon_expire') return 'status-soon-expire'
|
|
|
|
|
|
return 'status-rented'
|
|
|
|
|
|
}
|
|
|
|
|
|
if (room.status === 'reserved') return 'status-reserved'
|
|
|
|
|
|
return ''
|
2026-04-20 06:23:11 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
.rooms-page {
|
|
|
|
|
|
min-height: 100vh;
|
|
|
|
|
|
background: #F8FAFC;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
}
|
|
|
|
|
|
.custom-nav {
|
|
|
|
|
|
background: #FFFFFF;
|
|
|
|
|
|
border-bottom: 2rpx solid #F1F5F9;
|
|
|
|
|
|
}
|
|
|
|
|
|
.nav-content {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
padding: 20rpx 32rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
.nav-btn {
|
|
|
|
|
|
width: 72rpx;
|
|
|
|
|
|
height: 72rpx;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
}
|
|
|
|
|
|
.nav-title {
|
|
|
|
|
|
font-size: 36rpx;
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
color: #1E293B;
|
|
|
|
|
|
}
|
|
|
|
|
|
.apartment-filter {
|
|
|
|
|
|
background: #FFFFFF;
|
|
|
|
|
|
padding: 20rpx 32rpx;
|
|
|
|
|
|
border-bottom: 2rpx solid #F1F5F9;
|
|
|
|
|
|
}
|
|
|
|
|
|
.picker-value {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 8rpx;
|
|
|
|
|
|
font-size: 28rpx;
|
|
|
|
|
|
color: #1E293B;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
}
|
|
|
|
|
|
.room-list {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
padding: 24rpx 32rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
.empty-state {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
padding: 120rpx 60rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
.empty-icon {
|
|
|
|
|
|
margin-bottom: 32rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
.empty-title {
|
|
|
|
|
|
font-size: 32rpx;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: #1E293B;
|
|
|
|
|
|
margin-bottom: 12rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
.empty-desc {
|
|
|
|
|
|
font-size: 26rpx;
|
|
|
|
|
|
color: #94A3B8;
|
|
|
|
|
|
margin-bottom: 40rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
.empty-btn {
|
|
|
|
|
|
background: linear-gradient(135deg, #2563EB 0%, #1D4ED8 100%);
|
|
|
|
|
|
color: #FFFFFF;
|
2026-04-22 06:47:04 +00:00
|
|
|
|
font-size: 28rpx;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
padding: 16rpx 40rpx;
|
|
|
|
|
|
border-radius: 12rpx;
|
2026-04-20 06:23:11 +00:00
|
|
|
|
border: none;
|
2026-04-22 06:47:04 +00:00
|
|
|
|
line-height: 1.5;
|
2026-04-20 06:23:11 +00:00
|
|
|
|
}
|
|
|
|
|
|
.room-grid {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: repeat(2, 1fr);
|
|
|
|
|
|
gap: 20rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
.room-card {
|
|
|
|
|
|
background: #FFFFFF;
|
2026-04-22 06:47:04 +00:00
|
|
|
|
border: 2rpx solid #EBEEF5;
|
2026-04-20 06:23:11 +00:00
|
|
|
|
border-radius: 16rpx;
|
|
|
|
|
|
padding: 24rpx;
|
|
|
|
|
|
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
|
2026-04-22 06:47:04 +00:00
|
|
|
|
transition: all 0.2s;
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
}
|
|
|
|
|
|
.room-card:active {
|
|
|
|
|
|
transform: scale(0.98);
|
|
|
|
|
|
}
|
|
|
|
|
|
/* 状态样式 */
|
|
|
|
|
|
.room-card.status-empty {
|
|
|
|
|
|
border-color: #67C23A;
|
|
|
|
|
|
background: #F0F9FF;
|
|
|
|
|
|
}
|
|
|
|
|
|
.room-card.status-rented {
|
|
|
|
|
|
border-color: #409EFF;
|
|
|
|
|
|
background: #ECF5FF;
|
|
|
|
|
|
}
|
|
|
|
|
|
.room-card.status-soon-expire {
|
|
|
|
|
|
border-color: #E6A23C;
|
|
|
|
|
|
background: #FDF6EC;
|
|
|
|
|
|
}
|
|
|
|
|
|
.room-card.status-expired {
|
|
|
|
|
|
border-color: #F56C6C;
|
|
|
|
|
|
background: #FEF0F0;
|
|
|
|
|
|
}
|
|
|
|
|
|
.room-card.status-reserved {
|
|
|
|
|
|
border-color: #909399;
|
|
|
|
|
|
background: #F4F4F5;
|
2026-04-20 06:23:11 +00:00
|
|
|
|
}
|
|
|
|
|
|
.room-header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
margin-bottom: 16rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
.room-number {
|
|
|
|
|
|
font-size: 32rpx;
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
color: #1E293B;
|
|
|
|
|
|
}
|
|
|
|
|
|
.room-status {
|
|
|
|
|
|
padding: 4rpx 12rpx;
|
2026-04-22 06:47:04 +00:00
|
|
|
|
border-radius: 20rpx;
|
2026-04-20 06:23:11 +00:00
|
|
|
|
font-size: 20rpx;
|
2026-04-22 06:47:04 +00:00
|
|
|
|
background: rgba(255, 255, 255, 0.9);
|
|
|
|
|
|
color: #606266;
|
|
|
|
|
|
font-weight: 500;
|
2026-04-20 06:23:11 +00:00
|
|
|
|
}
|
|
|
|
|
|
.room-info {
|
|
|
|
|
|
margin-bottom: 12rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
.info-item {
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
font-size: 24rpx;
|
|
|
|
|
|
color: #64748B;
|
|
|
|
|
|
margin-bottom: 4rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
.room-price {
|
|
|
|
|
|
font-size: 28rpx;
|
|
|
|
|
|
color: #EF4444;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
margin-bottom: 8rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
.room-tenant {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 8rpx;
|
|
|
|
|
|
font-size: 24rpx;
|
|
|
|
|
|
color: #64748B;
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|