367 lines
10 KiB
Vue
367 lines
10 KiB
Vue
<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">
|
||
<view v-for="(item, index) in rooms" :key="index" class="room-card" :class="getRoomStatusClass(item)" @click="viewRoomDetail(item)">
|
||
<view class="room-header">
|
||
<text class="room-number">{{item.roomNumber}}</text>
|
||
<view class="room-status">{{item.statusText}}</view>
|
||
</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 {
|
||
const res = await apartmentApi.list()
|
||
if (res.data) {
|
||
this.apartments = res.data
|
||
if (!this.selectedApartmentId && this.apartments.length > 0) {
|
||
this.selectedApartmentId = this.apartments[0].id
|
||
}
|
||
// 注意:loadRooms 由 onShow 触发,避免重复请求
|
||
}
|
||
} 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)
|
||
if (res.data && res.data.list) {
|
||
const list = res.data.list.map(item => ({
|
||
id: item.id,
|
||
roomNumber: item.roomNumber,
|
||
area: item.area,
|
||
orientation: item.orientation,
|
||
rent: item.rent,
|
||
status: this.getRoomStatus(item),
|
||
rentalStatus: item.rentalStatus,
|
||
statusText: this.getRoomStatusText(item),
|
||
renterName: item.Renter && item.Renter.name
|
||
}))
|
||
if (this.page === 1) {
|
||
this.rooms = list
|
||
} else {
|
||
this.rooms = [...this.rooms, ...list]
|
||
}
|
||
this.loadStatus = this.rooms.length < (res.data.total || 0) ? 'more' : 'noMore'
|
||
}
|
||
} 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()
|
||
},
|
||
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 ''
|
||
}
|
||
}
|
||
}
|
||
</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;
|
||
font-size: 28rpx;
|
||
font-weight: 500;
|
||
padding: 16rpx 40rpx;
|
||
border-radius: 12rpx;
|
||
border: none;
|
||
line-height: 1.5;
|
||
}
|
||
.room-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, 1fr);
|
||
gap: 20rpx;
|
||
}
|
||
.room-card {
|
||
background: #FFFFFF;
|
||
border: 2rpx solid #EBEEF5;
|
||
border-radius: 16rpx;
|
||
padding: 24rpx;
|
||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
|
||
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;
|
||
}
|
||
.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;
|
||
border-radius: 20rpx;
|
||
font-size: 20rpx;
|
||
background: rgba(255, 255, 255, 0.9);
|
||
color: #606266;
|
||
font-weight: 500;
|
||
}
|
||
.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>
|