This commit is contained in:
parent
dc892c150b
commit
166480323c
|
|
@ -6,7 +6,7 @@ import { get, post, put, del } from '../utils/request.js'
|
|||
|
||||
export default {
|
||||
/**
|
||||
* 获取公寓列表
|
||||
* 获取公寓列表(分页)
|
||||
* @param {Object} params - 查询参数
|
||||
* @param {number} params.page - 页码
|
||||
* @param {number} params.pageSize - 每页数量
|
||||
|
|
@ -17,6 +17,15 @@ export default {
|
|||
return get('/apartments', params)
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取公寓列表(全部)
|
||||
* @param {Object} params - 查询参数
|
||||
* @returns {Promise}
|
||||
*/
|
||||
list(params = {}) {
|
||||
return get('/apartments/list', params)
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取公寓详情
|
||||
* @param {number} id - 公寓ID
|
||||
|
|
|
|||
15
api/bill.js
15
api/bill.js
|
|
@ -10,11 +10,22 @@ export default {
|
|||
* @param {Object} params - 查询参数
|
||||
* @param {number} params.page - 页码
|
||||
* @param {number} params.pageSize - 每页数量
|
||||
* @param {string} params.month - 月份筛选
|
||||
* @param {string} params.status - 状态筛选
|
||||
* @param {string} params.type - 类型筛选 income/expense
|
||||
* @param {string} params.status - 状态筛选 unpaid/partial/paid/cancelled
|
||||
* @param {string} params.startDate - 开始日期
|
||||
* @param {string} params.endDate - 结束日期
|
||||
* @returns {Promise}
|
||||
*/
|
||||
getList(params = {}) {
|
||||
return get('/bills/list', params)
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取所有账单(与Web端兼容的别名方法)
|
||||
* @param {Object} params - 查询参数
|
||||
* @returns {Promise}
|
||||
*/
|
||||
getAll(params = {}) {
|
||||
return get('/bills', params)
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -11,9 +11,10 @@ export default {
|
|||
* @param {number} params.page - 页码
|
||||
* @param {number} params.pageSize - 每页数量
|
||||
* @param {string} params.keyword - 搜索关键词
|
||||
* @param {string} params.status - 状态
|
||||
* @returns {Promise}
|
||||
*/
|
||||
getList(params = {}) {
|
||||
getAll(params = {}) {
|
||||
return get('/renters', params)
|
||||
},
|
||||
|
||||
|
|
|
|||
13
api/room.js
13
api/room.js
|
|
@ -6,7 +6,7 @@ import { get, post, put, del } from '../utils/request.js'
|
|||
|
||||
export default {
|
||||
/**
|
||||
* 获取房间列表
|
||||
* 获取房间列表(分页)
|
||||
* @param {Object} params - 查询参数
|
||||
* @param {number} params.apartmentId - 公寓ID
|
||||
* @param {string} params.status - 房间状态
|
||||
|
|
@ -16,6 +16,17 @@ export default {
|
|||
return get('/rooms', params)
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取房间列表(全部)
|
||||
* @param {Object} params - 查询参数
|
||||
* @param {number} params.apartmentId - 公寓ID
|
||||
* @param {string} params.status - 房间状态
|
||||
* @returns {Promise}
|
||||
*/
|
||||
list(params = {}) {
|
||||
return get('/rooms/list', params)
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取房间详情
|
||||
* @param {number} id - 房间ID
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -30,85 +30,33 @@
|
|||
/>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="item-label">公寓地址</text>
|
||||
<text class="item-label">地址</text>
|
||||
<input
|
||||
type="text"
|
||||
v-model="form.address"
|
||||
placeholder="请输入公寓地址"
|
||||
placeholder="请输入地址"
|
||||
class="item-input"
|
||||
/>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="item-label">负责人</text>
|
||||
<input
|
||||
type="text"
|
||||
v-model="form.manager"
|
||||
placeholder="请输入负责人姓名"
|
||||
class="item-input"
|
||||
/>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="item-label">联系电话</text>
|
||||
<input
|
||||
type="number"
|
||||
v-model="form.phone"
|
||||
placeholder="请输入联系电话"
|
||||
class="item-input"
|
||||
maxlength="11"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 状态设置 -->
|
||||
<view class="form-section">
|
||||
<view class="section-title">状态设置</view>
|
||||
<view class="form-card">
|
||||
<view class="form-item">
|
||||
<text class="item-label">运营状态</text>
|
||||
<view class="status-options">
|
||||
<view
|
||||
v-for="(option, index) in statusOptions"
|
||||
:key="index"
|
||||
class="status-option"
|
||||
:class="{ active: form.status === option.value }"
|
||||
@click="form.status = option.value"
|
||||
>
|
||||
<text>{{option.label}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 备注 -->
|
||||
<view class="form-section">
|
||||
<view class="section-title">备注信息</view>
|
||||
<view class="form-card">
|
||||
<view class="form-item">
|
||||
<view class="form-item textarea-item">
|
||||
<text class="item-label">描述</text>
|
||||
<textarea
|
||||
v-model="form.remark"
|
||||
placeholder="请输入备注信息(选填)"
|
||||
v-model="form.description"
|
||||
placeholder="请输入描述"
|
||||
class="remark-input"
|
||||
maxlength="200"
|
||||
maxlength="500"
|
||||
/>
|
||||
<text class="word-count">{{form.remark.length}}/200</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 删除按钮(编辑模式) -->
|
||||
<view v-if="isEdit" class="delete-section">
|
||||
<button class="delete-btn" @click="confirmDelete">删除公寓</button>
|
||||
</view>
|
||||
|
||||
<view class="safe-area-bottom" style="height: 40rpx;"></view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { apartmentApi } from '../../api/index.js'
|
||||
import { apartmentApi } from '@/api/index.js'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
|
|
@ -118,38 +66,31 @@
|
|||
form: {
|
||||
name: '',
|
||||
address: '',
|
||||
manager: '',
|
||||
phone: '',
|
||||
status: 'active',
|
||||
remark: ''
|
||||
},
|
||||
statusOptions: [
|
||||
{ label: '运营中', value: 'active' },
|
||||
{ label: '已停用', value: 'inactive' },
|
||||
{ label: '装修中', value: 'renovating' }
|
||||
]
|
||||
description: ''
|
||||
}
|
||||
}
|
||||
},
|
||||
onLoad(options) {
|
||||
if (options.id) {
|
||||
// 判断是否为编辑模式
|
||||
if (options.mode === 'edit' && options.id) {
|
||||
this.isEdit = true
|
||||
this.apartmentId = options.id
|
||||
this.loadApartmentDetail(options.id)
|
||||
this.loadApartmentDetail()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async loadApartmentDetail(id) {
|
||||
// 加载公寓详情(编辑模式)
|
||||
async loadApartmentDetail() {
|
||||
try {
|
||||
uni.showLoading({ title: '加载中...' })
|
||||
const res = await apartmentApi.getDetail(id)
|
||||
const data = res.data || {}
|
||||
const res = await apartmentApi.getDetail(this.apartmentId)
|
||||
const data = res.data
|
||||
|
||||
// 填充表单数据
|
||||
this.form = {
|
||||
name: data.name || '',
|
||||
address: data.address || '',
|
||||
manager: data.manager || '',
|
||||
phone: data.phone || '',
|
||||
status: data.status || 'active',
|
||||
remark: data.remark || ''
|
||||
description: data.description || ''
|
||||
}
|
||||
} catch (error) {
|
||||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||
|
|
@ -168,9 +109,11 @@
|
|||
uni.showLoading({ title: '保存中...' })
|
||||
|
||||
if (this.isEdit) {
|
||||
// 编辑模式
|
||||
await apartmentApi.update(this.apartmentId, this.form)
|
||||
uni.showToast({ title: '更新成功', icon: 'success' })
|
||||
uni.showToast({ title: '修改成功', icon: 'success' })
|
||||
} else {
|
||||
// 添加模式
|
||||
await apartmentApi.create(this.form)
|
||||
uni.showToast({ title: '添加成功', icon: 'success' })
|
||||
}
|
||||
|
|
@ -179,35 +122,8 @@
|
|||
uni.navigateBack()
|
||||
}, 1500)
|
||||
} catch (error) {
|
||||
uni.showToast({ title: '保存失败', icon: 'none' })
|
||||
} finally {
|
||||
uni.hideLoading()
|
||||
}
|
||||
},
|
||||
|
||||
confirmDelete() {
|
||||
uni.showModal({
|
||||
title: '确认删除',
|
||||
content: '删除后无法恢复,确定要删除该公寓吗?',
|
||||
confirmColor: '#EF4444',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
this.deleteApartment()
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
async deleteApartment() {
|
||||
try {
|
||||
uni.showLoading({ title: '删除中...' })
|
||||
await apartmentApi.delete(this.apartmentId)
|
||||
uni.showToast({ title: '删除成功', icon: 'success' })
|
||||
setTimeout(() => {
|
||||
uni.navigateBack()
|
||||
}, 1500)
|
||||
} catch (error) {
|
||||
uni.showToast({ title: '删除失败', icon: 'none' })
|
||||
const msg = error.data && error.data.message
|
||||
uni.showToast({ title: msg || (this.isEdit ? '修改失败' : '添加失败'), icon: 'none' })
|
||||
} finally {
|
||||
uni.hideLoading()
|
||||
}
|
||||
|
|
@ -305,11 +221,21 @@
|
|||
border-bottom: none;
|
||||
}
|
||||
|
||||
.textarea-item {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.item-label {
|
||||
width: 160rpx;
|
||||
font-size: 30rpx;
|
||||
color: #1E293B;
|
||||
font-weight: 500;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.textarea-item .item-label {
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.item-label.required::after {
|
||||
|
|
@ -325,26 +251,6 @@
|
|||
text-align: right;
|
||||
}
|
||||
|
||||
.status-options {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.status-option {
|
||||
padding: 12rpx 24rpx;
|
||||
border-radius: 8rpx;
|
||||
background: #F1F5F9;
|
||||
font-size: 26rpx;
|
||||
color: #64748B;
|
||||
}
|
||||
|
||||
.status-option.active {
|
||||
background: #DBEAFE;
|
||||
color: #2563EB;
|
||||
}
|
||||
|
||||
.remark-input {
|
||||
width: 100%;
|
||||
height: 200rpx;
|
||||
|
|
@ -352,31 +258,4 @@
|
|||
color: #1E293B;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.word-count {
|
||||
position: absolute;
|
||||
right: 32rpx;
|
||||
bottom: 24rpx;
|
||||
font-size: 24rpx;
|
||||
color: #94A3B8;
|
||||
}
|
||||
|
||||
/* 删除区域 */
|
||||
.delete-section {
|
||||
margin-top: 48rpx;
|
||||
}
|
||||
|
||||
.delete-btn {
|
||||
width: 100%;
|
||||
height: 96rpx;
|
||||
background: #FEE2E2;
|
||||
color: #EF4444;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
border-radius: 16rpx;
|
||||
border: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -84,8 +84,8 @@
|
|||
<text class="stat-value rented">{{item.rentedCount || 0}}</text>
|
||||
</view>
|
||||
<view class="room-stat">
|
||||
<text class="stat-label">月租</text>
|
||||
<text class="stat-value">¥{{item.monthlyIncome || 0}}</text>
|
||||
<text class="stat-label">预订</text>
|
||||
<text class="stat-value">{{item.bookedCount || 0}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
|
|
@ -100,6 +100,10 @@
|
|||
<uni-icons type="home-filled" size="16" color="#64748B"></uni-icons>
|
||||
<text>房源</text>
|
||||
</view>
|
||||
<view class="action-btn delete" @click.stop="deleteApartment(item)">
|
||||
<uni-icons type="trash" size="16" color="#EF4444"></uni-icons>
|
||||
<text class="delete-text">删除</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
|
@ -123,7 +127,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { apartmentApi, roomApi } from '../../api/index.js'
|
||||
import { apartmentApi, statisticsApi } from '../../api/index.js'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
|
|
@ -154,25 +158,45 @@
|
|||
this.isLoading = true
|
||||
|
||||
try {
|
||||
// 加载公寓列表
|
||||
const res = await apartmentApi.list({
|
||||
keyword: this.searchKeyword,
|
||||
// 加载公寓列表 - 与web端保持一致,使用getAll(即getList)
|
||||
const res = await apartmentApi.getList({
|
||||
name: this.searchKeyword,
|
||||
page: this.page,
|
||||
pageSize: this.pageSize
|
||||
})
|
||||
|
||||
const apartments = res.data || []
|
||||
// 适配分页数据格式: { data: { list: [], total: n, page: n, pageSize: n } }
|
||||
const apartments = res.data.list || []
|
||||
const total = res.data.total || 0
|
||||
|
||||
// 加载每个公寓的房间统计
|
||||
for (let apt of apartments) {
|
||||
try {
|
||||
const roomRes = await roomApi.list({ apartmentId: apt.id })
|
||||
const rooms = roomRes.data || []
|
||||
apt.roomCount = rooms.length
|
||||
apt.emptyCount = rooms.filter(r => r.status === 'empty').length
|
||||
apt.rentedCount = rooms.filter(r => r.status === 'rented').length
|
||||
} catch (e) {
|
||||
console.error('加载房间统计失败:', e)
|
||||
// 加载公寓房间状态统计
|
||||
try {
|
||||
const statsRes = await statisticsApi.getApartmentRoomStatusStats()
|
||||
const apartmentStats = statsRes.data || []
|
||||
|
||||
// 将统计数据合并到公寓列表中
|
||||
for (let apt of apartments) {
|
||||
const stat = apartmentStats.find(s => s.apartmentId === apt.id)
|
||||
if (stat) {
|
||||
apt.roomCount = stat.total || 0
|
||||
apt.emptyCount = stat.empty || 0
|
||||
apt.rentedCount = stat.rented || 0
|
||||
apt.bookedCount = stat.reserved || 0
|
||||
} else {
|
||||
apt.roomCount = 0
|
||||
apt.emptyCount = 0
|
||||
apt.rentedCount = 0
|
||||
apt.bookedCount = 0
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载房间统计失败:', e)
|
||||
// 统计接口失败时,设置默认值
|
||||
for (let apt of apartments) {
|
||||
apt.roomCount = 0
|
||||
apt.emptyCount = 0
|
||||
apt.rentedCount = 0
|
||||
apt.bookedCount = 0
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -182,11 +206,13 @@
|
|||
this.apartmentList = [...this.apartmentList, ...apartments]
|
||||
}
|
||||
|
||||
this.hasMore = apartments.length >= this.pageSize
|
||||
// 根据total判断是否还有更多数据
|
||||
this.hasMore = this.apartmentList.length < total
|
||||
|
||||
// 更新统计
|
||||
this.updateStats()
|
||||
} catch (error) {
|
||||
console.error('加载公寓列表失败:', error)
|
||||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||
} finally {
|
||||
this.isLoading = false
|
||||
|
|
@ -242,6 +268,31 @@
|
|||
})
|
||||
},
|
||||
|
||||
deleteApartment(item) {
|
||||
uni.showModal({
|
||||
title: '确认删除',
|
||||
content: `确定要删除公寓"${item.name}"吗?`,
|
||||
confirmColor: '#EF4444',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
uni.showLoading({ title: '删除中...' })
|
||||
await apartmentApi.delete(item.id)
|
||||
uni.showToast({ title: '删除成功', icon: 'success' })
|
||||
// 刷新列表
|
||||
this.page = 1
|
||||
this.loadData()
|
||||
} catch (error) {
|
||||
const msg = error.data && error.data.message
|
||||
uni.showToast({ title: msg || '删除失败', icon: 'none' })
|
||||
} finally {
|
||||
uni.hideLoading()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
goBack() {
|
||||
uni.navigateBack()
|
||||
},
|
||||
|
|
@ -492,6 +543,14 @@
|
|||
color: #64748B;
|
||||
}
|
||||
|
||||
.action-btn.delete {
|
||||
color: #EF4444;
|
||||
}
|
||||
|
||||
.action-btn.delete .delete-text {
|
||||
color: #EF4444;
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
.empty-state {
|
||||
display: flex;
|
||||
|
|
|
|||
|
|
@ -18,10 +18,10 @@
|
|||
<!-- 类型选择 -->
|
||||
<view class="type-section">
|
||||
<view class="type-tabs">
|
||||
<view class="type-tab" :class="{ active: form.type === 'income' }" @click="form.type = 'income'">
|
||||
<view class="type-tab" :class="{ active: form.type === 'income' }" @click="switchType('income')">
|
||||
<text>收入</text>
|
||||
</view>
|
||||
<view class="type-tab" :class="{ active: form.type === 'expense' }" @click="form.type = 'expense'">
|
||||
<view class="type-tab" :class="{ active: form.type === 'expense' }" @click="switchType('expense')">
|
||||
<text>支出</text>
|
||||
</view>
|
||||
</view>
|
||||
|
|
@ -39,13 +39,6 @@
|
|||
<!-- 表单信息 -->
|
||||
<view class="form-section">
|
||||
<view class="form-card">
|
||||
<view class="form-item" @click="selectRoom">
|
||||
<text class="item-label">关联房间</text>
|
||||
<view class="item-value">
|
||||
<text :class="{ placeholder: !selectedRoom }">{{selectedRoom ? selectedRoom.roomNumber : '请选择房间'}}</text>
|
||||
<uni-icons type="right" size="16" color="#94A3B8"></uni-icons>
|
||||
</view>
|
||||
</view>
|
||||
<view class="form-item" @click="selectCategory">
|
||||
<text class="item-label required">收支类目</text>
|
||||
<view class="item-value">
|
||||
|
|
@ -53,6 +46,20 @@
|
|||
<uni-icons type="right" size="16" color="#94A3B8"></uni-icons>
|
||||
</view>
|
||||
</view>
|
||||
<view class="form-item" @click="selectRoom">
|
||||
<text class="item-label">关联房间</text>
|
||||
<view class="item-value">
|
||||
<text :class="{ placeholder: !selectedRoom }">{{selectedRoom ? selectedRoom.roomNumber : '请选择房间'}}</text>
|
||||
<uni-icons type="right" size="16" color="#94A3B8"></uni-icons>
|
||||
</view>
|
||||
</view>
|
||||
<view class="form-item" @click="selectRental" :class="{ disabled: !form.roomId }">
|
||||
<text class="item-label">关联租约</text>
|
||||
<view class="item-value">
|
||||
<text :class="{ placeholder: !selectedRental }">{{selectedRental ? selectedRental.tenantName : (form.roomId ? '请选择租约' : '请先选择房间')}}</text>
|
||||
<uni-icons type="right" size="16" color="#94A3B8"></uni-icons>
|
||||
</view>
|
||||
</view>
|
||||
<view class="form-item" @click="selectDate">
|
||||
<text class="item-label required">账单日期</text>
|
||||
<view class="item-value">
|
||||
|
|
@ -60,6 +67,13 @@
|
|||
<uni-icons type="right" size="16" color="#94A3B8"></uni-icons>
|
||||
</view>
|
||||
</view>
|
||||
<view class="form-item" @click="selectBillMonth">
|
||||
<text class="item-label">账单月份</text>
|
||||
<view class="item-value">
|
||||
<text :class="{ placeholder: !form.billMonth }">{{form.billMonth || '请选择账单月份'}}</text>
|
||||
<uni-icons type="right" size="16" color="#94A3B8"></uni-icons>
|
||||
</view>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="item-label">备注</text>
|
||||
<input type="text" v-model="form.remark" placeholder="请输入备注" class="item-input"/>
|
||||
|
|
@ -73,7 +87,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { billApi, roomApi } from '../../api/index.js'
|
||||
import { billApi, roomApi, rentalApi, settingApi } from '../../api/index.js'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
|
|
@ -81,54 +95,124 @@
|
|||
form: {
|
||||
type: 'income',
|
||||
receivableAmount: '',
|
||||
roomId: '',
|
||||
roomId: null,
|
||||
rentalId: null,
|
||||
category: '',
|
||||
billDate: this.formatDate(new Date()),
|
||||
billMonth: '',
|
||||
remark: ''
|
||||
},
|
||||
selectedRoom: null,
|
||||
categories: [
|
||||
{ code: 'rent', name: '租金', type: 'income' },
|
||||
{ code: 'deposit', name: '押金', type: 'income' },
|
||||
{ code: 'water', name: '水费', type: 'income' },
|
||||
{ code: 'electricity', name: '电费', type: 'income' },
|
||||
{ code: 'maintenance', name: '维修费', type: 'expense' },
|
||||
{ code: 'property', name: '物业费', type: 'expense' },
|
||||
{ code: 'other', name: '其他', type: 'all' }
|
||||
]
|
||||
selectedRental: null,
|
||||
rooms: [],
|
||||
roomRentals: [],
|
||||
incomeCategories: [],
|
||||
expenseCategories: []
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
this.loadRooms()
|
||||
this.loadCategories()
|
||||
// 设置默认账单日期为本月最后一天
|
||||
const today = new Date()
|
||||
const lastDay = new Date(today.getFullYear(), today.getMonth() + 1, 0)
|
||||
this.form.billDate = this.formatDate(lastDay)
|
||||
// 设置默认账单月份为当前月份
|
||||
this.form.billMonth = this.formatMonth(today)
|
||||
},
|
||||
methods: {
|
||||
async selectRoom() {
|
||||
// 切换类型
|
||||
switchType(type) {
|
||||
this.form.type = type
|
||||
this.form.category = ''
|
||||
},
|
||||
|
||||
// 加载类目列表
|
||||
async loadCategories() {
|
||||
try {
|
||||
const res = await roomApi.list()
|
||||
const rooms = res.data || []
|
||||
if (rooms.length === 0) {
|
||||
uni.showToast({ title: '暂无房间数据', icon: 'none' })
|
||||
return
|
||||
}
|
||||
const roomList = rooms.map(r => r.roomNumber)
|
||||
uni.showActionSheet({
|
||||
itemList: roomList,
|
||||
success: (res) => {
|
||||
this.selectedRoom = rooms[res.tapIndex]
|
||||
this.form.roomId = this.selectedRoom.id
|
||||
}
|
||||
})
|
||||
const res = await settingApi.getCategories()
|
||||
const categories = res.data || []
|
||||
this.incomeCategories = categories.filter(c => c.type === 'income')
|
||||
this.expenseCategories = categories.filter(c => c.type === 'expense')
|
||||
} catch (error) {
|
||||
uni.showToast({ title: '加载房间失败', icon: 'none' })
|
||||
console.error('加载类目列表失败:', error)
|
||||
}
|
||||
},
|
||||
selectCategory() {
|
||||
const filteredCategories = this.categories.filter(c => c.type === this.form.type || c.type === 'all')
|
||||
const categoryList = filteredCategories.map(c => c.name)
|
||||
|
||||
// 加载房间列表
|
||||
async loadRooms() {
|
||||
try {
|
||||
const res = await roomApi.list()
|
||||
this.rooms = res.data || []
|
||||
} catch (error) {
|
||||
uni.showToast({ title: '加载房间列表失败', icon: 'none' })
|
||||
}
|
||||
},
|
||||
|
||||
// 加载房间租约列表
|
||||
async loadRoomRentals(roomId) {
|
||||
try {
|
||||
const res = await rentalApi.getAll({ roomId, status: 'active' })
|
||||
this.roomRentals = res.data || []
|
||||
} catch (error) {
|
||||
console.error('加载租约列表失败:', error)
|
||||
}
|
||||
},
|
||||
|
||||
async selectRoom() {
|
||||
if (this.rooms.length === 0) {
|
||||
uni.showToast({ title: '暂无房间数据', icon: 'none' })
|
||||
return
|
||||
}
|
||||
const roomList = this.rooms.map(r => (r.apartmentName || '') + ' - ' + r.roomNumber)
|
||||
uni.showActionSheet({
|
||||
itemList: categoryList,
|
||||
itemList: roomList,
|
||||
success: (res) => {
|
||||
this.form.category = filteredCategories[res.tapIndex].code
|
||||
this.selectedRoom = this.rooms[res.tapIndex]
|
||||
this.form.roomId = this.selectedRoom.id
|
||||
// 清空租约选择
|
||||
this.selectedRental = null
|
||||
this.form.rentalId = null
|
||||
// 加载该房间的租约
|
||||
this.loadRoomRentals(this.form.roomId)
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
selectCategory() {
|
||||
const categories = this.form.type === 'income' ? this.incomeCategories : this.expenseCategories
|
||||
if (categories.length === 0) {
|
||||
uni.showToast({ title: '暂无类目数据', icon: 'none' })
|
||||
return
|
||||
}
|
||||
const categoryList = categories.map(c => c.name)
|
||||
uni.showActionSheet({
|
||||
itemList: categoryList,
|
||||
success: (res) => {
|
||||
this.form.category = categories[res.tapIndex].code
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
selectRental() {
|
||||
if (!this.form.roomId) {
|
||||
uni.showToast({ title: '请先选择房间', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (this.roomRentals.length === 0) {
|
||||
uni.showToast({ title: '该房间暂无租约', icon: 'none' })
|
||||
return
|
||||
}
|
||||
const rentalList = this.roomRentals.map(r => r.tenantName + ' (' + r.startDate + ' 至 ' + r.endDate + ')')
|
||||
uni.showActionSheet({
|
||||
itemList: rentalList,
|
||||
success: (res) => {
|
||||
this.selectedRental = this.roomRentals[res.tapIndex]
|
||||
this.form.rentalId = this.selectedRental.id
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
selectDate() {
|
||||
uni.showActionSheet({
|
||||
itemList: ['今天', '昨天', '自定义'],
|
||||
|
|
@ -150,7 +234,32 @@
|
|||
}
|
||||
})
|
||||
},
|
||||
|
||||
selectBillMonth() {
|
||||
// 使用日期选择器选择月份
|
||||
const today = new Date()
|
||||
const currentYear = today.getFullYear()
|
||||
const years = [currentYear - 1, currentYear, currentYear + 1].map(String)
|
||||
const months = ['01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12']
|
||||
|
||||
uni.showActionSheet({
|
||||
itemList: years,
|
||||
title: '选择年份',
|
||||
success: (yearRes) => {
|
||||
const selectedYear = years[yearRes.tapIndex]
|
||||
uni.showActionSheet({
|
||||
itemList: months.map(m => selectedYear + '-' + m),
|
||||
title: '选择月份',
|
||||
success: (monthRes) => {
|
||||
this.form.billMonth = selectedYear + '-' + months[monthRes.tapIndex]
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
async save() {
|
||||
// 表单验证
|
||||
if (!this.form.receivableAmount || parseFloat(this.form.receivableAmount) <= 0) {
|
||||
uni.showToast({ title: '请输入金额', icon: 'none' })
|
||||
return
|
||||
|
|
@ -159,9 +268,18 @@
|
|||
uni.showToast({ title: '请选择类目', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!this.form.billDate) {
|
||||
uni.showToast({ title: '请选择账单日期', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
uni.showLoading({ title: '保存中...' })
|
||||
await billApi.create(this.form)
|
||||
const data = {
|
||||
...this.form,
|
||||
receivableAmount: parseFloat(this.form.receivableAmount)
|
||||
}
|
||||
await billApi.create(data)
|
||||
uni.showToast({ title: '保存成功', icon: 'success' })
|
||||
setTimeout(() => { uni.navigateBack() }, 1500)
|
||||
} catch (error) {
|
||||
|
|
@ -170,12 +288,20 @@
|
|||
uni.hideLoading()
|
||||
}
|
||||
},
|
||||
|
||||
goBack() { uni.navigateBack() },
|
||||
|
||||
formatDate(date) {
|
||||
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
|
||||
},
|
||||
|
||||
formatMonth(date) {
|
||||
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`
|
||||
},
|
||||
|
||||
getCategoryText(code) {
|
||||
const category = this.categories.find(c => c.code === code)
|
||||
const allCategories = [...this.incomeCategories, ...this.expenseCategories]
|
||||
const category = allCategories.find(c => c.code === code)
|
||||
return category ? category.name : code
|
||||
}
|
||||
}
|
||||
|
|
@ -204,6 +330,7 @@
|
|||
.form-card { background: #FFFFFF; border-radius: 24rpx; overflow: hidden; box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.04); }
|
||||
.form-item { display: flex; align-items: center; justify-content: space-between; padding: 28rpx 32rpx; border-bottom: 2rpx solid #F8FAFC; }
|
||||
.form-item:last-child { border-bottom: none; }
|
||||
.form-item.disabled { opacity: 0.6; }
|
||||
.item-label { font-size: 30rpx; color: #1E293B; font-weight: 500; }
|
||||
.item-label.required::after { content: '*'; color: #EF4444; margin-left: 8rpx; }
|
||||
.item-value { display: flex; align-items: center; gap: 16rpx; font-size: 30rpx; color: #1E293B; }
|
||||
|
|
|
|||
|
|
@ -17,29 +17,32 @@
|
|||
<scroll-view scroll-y class="page-content" @scrolltolower="loadMore">
|
||||
<!-- 筛选栏 -->
|
||||
<view class="filter-section">
|
||||
<view class="filter-tabs">
|
||||
<view v-for="(tab, index) in filterTabs" :key="index" class="filter-tab" :class="{ active: currentFilter === tab.value }" @click="switchFilter(tab.value)">
|
||||
<text>{{tab.label}}</text>
|
||||
<view class="filter-row">
|
||||
<view class="filter-label">类型:</view>
|
||||
<view class="filter-options">
|
||||
<view v-for="(tab, index) in typeTabs" :key="index" class="filter-option" :class="{ active: searchForm.type === tab.value }" @click="switchTypeFilter(tab.value)">
|
||||
<text>{{tab.label}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 统计概览 -->
|
||||
<view class="stats-section">
|
||||
<view class="stats-card">
|
||||
<view class="stat-item">
|
||||
<text class="stat-label">应收总额</text>
|
||||
<text class="stat-value">¥{{stats.totalReceivable}}</text>
|
||||
<view class="filter-row">
|
||||
<view class="filter-label">状态:</view>
|
||||
<view class="filter-options">
|
||||
<view v-for="(tab, index) in statusTabs" :key="index" class="filter-option" :class="{ active: searchForm.status === tab.value }" @click="switchStatusFilter(tab.value)">
|
||||
<text>{{tab.label}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="stat-divider"></view>
|
||||
<view class="stat-item">
|
||||
<text class="stat-label">实收总额</text>
|
||||
<text class="stat-value received">¥{{stats.totalReceived}}</text>
|
||||
</view>
|
||||
<view class="stat-divider"></view>
|
||||
<view class="stat-item">
|
||||
<text class="stat-label">待收金额</text>
|
||||
<text class="stat-value pending">¥{{stats.totalPending}}</text>
|
||||
</view>
|
||||
<view class="filter-row">
|
||||
<view class="filter-label">日期:</view>
|
||||
<view class="date-picker-group">
|
||||
<picker mode="date" :value="searchForm.startDate" @change="onStartDateChange">
|
||||
<view class="date-picker">{{searchForm.startDate || '开始日期'}}</view>
|
||||
</picker>
|
||||
<text class="date-separator">至</text>
|
||||
<picker mode="date" :value="searchForm.endDate" @change="onEndDateChange">
|
||||
<view class="date-picker">{{searchForm.endDate || '结束日期'}}</view>
|
||||
</picker>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
|
@ -63,14 +66,20 @@
|
|||
</view>
|
||||
<view class="type-info">
|
||||
<text class="type-name">{{getCategoryText(item.category)}}</text>
|
||||
<text class="type-room">{{item.roomNumber || '-'}} · {{item.renterName || '-'}}</text>
|
||||
<text class="type-tag" :class="item.type">{{item.type === 'income' ? '收入' : '支出'}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="bill-amount">
|
||||
<text class="amount" :class="item.type">{{item.type === 'income' ? '+' : '-'}}¥{{item.receivableAmount}}</text>
|
||||
<text class="received" v-if="item.receivedAmount > 0">实收: ¥{{item.receivedAmount}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="bill-remark" v-if="item.remark">
|
||||
<text class="remark-label">备注:</text>
|
||||
<text class="remark-content">{{item.remark}}</text>
|
||||
</view>
|
||||
<view class="bill-footer">
|
||||
<text class="create-time">创建时间: {{item.createTime}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
|
|
@ -85,31 +94,45 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { billApi } from '../../api/index.js'
|
||||
import { billApi, settingApi } from '../../api/index.js'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
currentFilter: 'all',
|
||||
filterTabs: [
|
||||
{ label: '全部', value: 'all' },
|
||||
typeTabs: [
|
||||
{ label: '全部', value: '' },
|
||||
{ label: '收入', value: 'income' },
|
||||
{ label: '支出', value: 'expense' },
|
||||
{ label: '未收', value: 'unpaid' }
|
||||
{ label: '支出', value: 'expense' }
|
||||
],
|
||||
statusTabs: [
|
||||
{ label: '全部', value: '' },
|
||||
{ label: '未收', value: 'unpaid' },
|
||||
{ label: '部分收款', value: 'partial' },
|
||||
{ label: '已收清', value: 'paid' },
|
||||
{ label: '已取消', value: 'cancelled' }
|
||||
],
|
||||
searchForm: {
|
||||
type: '',
|
||||
status: '',
|
||||
startDate: '',
|
||||
endDate: ''
|
||||
},
|
||||
billList: [],
|
||||
isLoading: false,
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
hasMore: true,
|
||||
total: 0,
|
||||
stats: {
|
||||
totalReceivable: '0.00',
|
||||
totalReceived: '0.00',
|
||||
totalPending: '0.00'
|
||||
}
|
||||
},
|
||||
categoryMap: {}
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
this.loadCategories()
|
||||
this.loadData()
|
||||
},
|
||||
onShow() {
|
||||
|
|
@ -120,22 +143,24 @@
|
|||
if (this.isLoading) return
|
||||
this.isLoading = true
|
||||
try {
|
||||
const params = { page: this.page, pageSize: this.pageSize }
|
||||
if (this.currentFilter !== 'all') {
|
||||
if (this.currentFilter === 'unpaid') {
|
||||
params.status = 'unpaid'
|
||||
} else {
|
||||
params.type = this.currentFilter
|
||||
}
|
||||
const params = {
|
||||
page: this.page,
|
||||
pageSize: this.pageSize,
|
||||
type: this.searchForm.type,
|
||||
status: this.searchForm.status,
|
||||
startDate: this.searchForm.startDate,
|
||||
endDate: this.searchForm.endDate
|
||||
}
|
||||
const res = await billApi.list(params)
|
||||
const list = res.data || []
|
||||
const res = await billApi.getAll(params)
|
||||
// 适配分页数据格式: { data: { list: [], total: n, page: n, pageSize: n } }
|
||||
const list = res.data?.list || []
|
||||
this.total = res.data?.total || 0
|
||||
if (this.page === 1) {
|
||||
this.billList = list
|
||||
} else {
|
||||
this.billList = [...this.billList, ...list]
|
||||
}
|
||||
this.hasMore = list.length >= this.pageSize
|
||||
this.hasMore = this.billList.length < this.total
|
||||
this.updateStats()
|
||||
} catch (error) {
|
||||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||
|
|
@ -162,8 +187,33 @@
|
|||
this.loadData()
|
||||
}
|
||||
},
|
||||
switchFilter(value) {
|
||||
this.currentFilter = value
|
||||
switchTypeFilter(value) {
|
||||
this.searchForm.type = value
|
||||
this.page = 1
|
||||
this.loadData()
|
||||
},
|
||||
switchStatusFilter(value) {
|
||||
this.searchForm.status = value
|
||||
this.page = 1
|
||||
this.loadData()
|
||||
},
|
||||
onStartDateChange(e) {
|
||||
this.searchForm.startDate = e.detail.value
|
||||
this.page = 1
|
||||
this.loadData()
|
||||
},
|
||||
onEndDateChange(e) {
|
||||
this.searchForm.endDate = e.detail.value
|
||||
this.page = 1
|
||||
this.loadData()
|
||||
},
|
||||
resetSearch() {
|
||||
this.searchForm = {
|
||||
type: '',
|
||||
status: '',
|
||||
startDate: '',
|
||||
endDate: ''
|
||||
}
|
||||
this.page = 1
|
||||
this.loadData()
|
||||
},
|
||||
|
|
@ -175,12 +225,26 @@
|
|||
},
|
||||
goBack() { uni.navigateBack() },
|
||||
getStatusText(status) {
|
||||
const texts = { 'paid': '已付清', 'partial': '部分付款', 'unpaid': '未付款' }
|
||||
const texts = { 'paid': '已收清', 'partial': '部分收款', 'unpaid': '未收', 'cancelled': '已取消' }
|
||||
return texts[status] || status
|
||||
},
|
||||
// 加载类目列表
|
||||
async loadCategories() {
|
||||
try {
|
||||
const res = await settingApi.getCategories()
|
||||
const categories = res.data || []
|
||||
// 构建 code -> name 的映射
|
||||
const map = {}
|
||||
categories.forEach(cat => {
|
||||
map[cat.code] = cat.name
|
||||
})
|
||||
this.categoryMap = map
|
||||
} catch (error) {
|
||||
console.error('加载类目列表失败:', error)
|
||||
}
|
||||
},
|
||||
getCategoryText(category) {
|
||||
const texts = { 'rent': '租金', 'deposit': '押金', 'water': '水费', 'electricity': '电费', 'maintenance': '维修费', 'property': '物业费', 'other': '其他' }
|
||||
return texts[category] || category || '其他'
|
||||
return this.categoryMap[category] || category || '其他'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -194,10 +258,16 @@
|
|||
.nav-title { font-size: 36rpx; font-weight: 700; color: #1E293B; }
|
||||
.nav-actions { width: 60rpx; height: 60rpx; display: flex; align-items: center; justify-content: center; }
|
||||
.page-content { flex: 1; padding: 24rpx 32rpx; }
|
||||
.filter-section { margin-bottom: 24rpx; }
|
||||
.filter-tabs { display: flex; gap: 16rpx; }
|
||||
.filter-tab { padding: 16rpx 32rpx; background: #FFFFFF; border-radius: 12rpx; font-size: 28rpx; color: #64748B; }
|
||||
.filter-tab.active { background: #2563EB; color: #FFFFFF; }
|
||||
.filter-section { margin-bottom: 24rpx; background: #FFFFFF; border-radius: 24rpx; padding: 24rpx; }
|
||||
.filter-row { display: flex; align-items: center; margin-bottom: 20rpx; }
|
||||
.filter-row:last-child { margin-bottom: 0; }
|
||||
.filter-label { font-size: 26rpx; color: #64748B; min-width: 80rpx; }
|
||||
.filter-options { display: flex; flex-wrap: wrap; gap: 12rpx; flex: 1; }
|
||||
.filter-option { padding: 12rpx 24rpx; background: #F1F5F9; border-radius: 8rpx; font-size: 24rpx; color: #64748B; }
|
||||
.filter-option.active { background: #2563EB; color: #FFFFFF; }
|
||||
.date-picker-group { display: flex; align-items: center; gap: 16rpx; flex: 1; }
|
||||
.date-picker { padding: 12rpx 20rpx; background: #F1F5F9; border-radius: 8rpx; font-size: 24rpx; color: #64748B; min-width: 160rpx; text-align: center; }
|
||||
.date-separator { font-size: 24rpx; color: #94A3B8; }
|
||||
.stats-section { margin-bottom: 24rpx; }
|
||||
.stats-card { background: #FFFFFF; border-radius: 24rpx; padding: 32rpx; display: flex; align-items: center; justify-content: space-around; box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.04); }
|
||||
.stat-item { display: flex; flex-direction: column; align-items: center; gap: 8rpx; }
|
||||
|
|
@ -223,13 +293,19 @@
|
|||
.type-icon.expense { background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); }
|
||||
.type-info { display: flex; flex-direction: column; gap: 8rpx; }
|
||||
.type-name { font-size: 30rpx; font-weight: 600; color: #1E293B; }
|
||||
.type-room { font-size: 24rpx; color: #64748B; }
|
||||
.type-tag { font-size: 22rpx; padding: 4rpx 12rpx; border-radius: 8rpx; margin-top: 4rpx; }
|
||||
.type-tag.income { background: #DCFCE7; color: #16A34A; }
|
||||
.type-tag.expense { background: #FEE2E2; color: #DC2626; }
|
||||
.bill-amount { display: flex; flex-direction: column; align-items: flex-end; gap: 8rpx; }
|
||||
.amount { font-size: 36rpx; font-weight: 700; }
|
||||
.amount.income { color: #667eea; }
|
||||
.amount.expense { color: #f5576c; }
|
||||
.received { font-size: 24rpx; color: #94A3B8; }
|
||||
.amount.income { color: #16A34A; }
|
||||
.amount.expense { color: #DC2626; }
|
||||
.bill-remark { margin-top: 20rpx; padding-top: 20rpx; border-top: 2rpx solid #F1F5F9; display: flex; gap: 8rpx; }
|
||||
.remark-label { font-size: 24rpx; color: #64748B; flex-shrink: 0; }
|
||||
.remark-content { font-size: 24rpx; color: #1E293B; flex: 1; word-break: break-all; }
|
||||
.bill-footer { margin-top: 16rpx; }
|
||||
.create-time { font-size: 22rpx; color: #94A3B8; }
|
||||
.empty-state { display: flex; flex-direction: column; align-items: center; padding: 120rpx 0; }
|
||||
.empty-text { font-size: 28rpx; color: #94A3B8; margin-bottom: 32rpx; }
|
||||
.add-btn { background: linear-gradient(135deg, #2563EB 0%, #1D4ED8 100%); color: #FFFFFF; font-size: 28rpx; padding: 24rpx 64rpx; border-radius: 16rpx; border: none; }
|
||||
.add-btn { background: linear-gradient(135deg, #2563EB 0%, #1D4ED8 100%); color: #FFFFFF; font-size: 26rpx; padding: 16rpx 40rpx; border-radius: 12rpx; border: none; }
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -3,24 +3,26 @@
|
|||
<!-- 顶部欢迎区 -->
|
||||
<view class="welcome-section">
|
||||
<view class="welcome-content">
|
||||
<text class="welcome-title">欢迎使用租房管理系统</text>
|
||||
<text class="welcome-date">{{currentDate}}</text>
|
||||
<view class="user-info">
|
||||
<text class="welcome-title">欢迎使用租房管理系统</text>
|
||||
<text class="welcome-date">{{currentDate}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="quick-actions">
|
||||
<view class="action-btn" @click="navigateTo('/pages/property-add/property-add')">
|
||||
<view class="action-btn" @click="navigateTo('/pages/apartment-add/apartment-add')">
|
||||
<uni-icons type="plus-filled" size="20" color="#FFFFFF"></uni-icons>
|
||||
<text>添加房源</text>
|
||||
<text>添加公寓</text>
|
||||
</view>
|
||||
<view class="action-btn secondary" @click="navigateTo('/pages/bills/bills')">
|
||||
<uni-icons type="wallet-filled" size="20" color="#2563EB"></uni-icons>
|
||||
<text>账单管理</text>
|
||||
<view class="action-btn secondary" @click="navigateTo('/pages/bill-add/bill-add')">
|
||||
<uni-icons type="wallet-filled" size="20" color="#667eea"></uni-icons>
|
||||
<text>添加账单</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 核心KPI卡片区 -->
|
||||
<view class="kpi-section">
|
||||
<view class="kpi-card kpi-primary" @click="navigateTo('/pages/properties/properties')">
|
||||
<view class="kpi-card kpi-primary" @click="navigateTo('/pages/apartments/apartments')">
|
||||
<view class="kpi-icon">
|
||||
<uni-icons type="home-filled" size="28" color="#FFFFFF"></uni-icons>
|
||||
</view>
|
||||
|
|
@ -58,6 +60,51 @@
|
|||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 功能模块网格 -->
|
||||
<view class="module-section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">功能模块</text>
|
||||
</view>
|
||||
<view class="module-grid">
|
||||
<view class="module-item" @click="navigateTo('/pages/apartments/apartments')">
|
||||
<view class="module-icon" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);">
|
||||
<uni-icons type="home-filled" size="28" color="#FFFFFF"></uni-icons>
|
||||
</view>
|
||||
<text class="module-name">公寓管理</text>
|
||||
</view>
|
||||
<view class="module-item" @click="navigateTo('/pages/renters/renters')">
|
||||
<view class="module-icon" style="background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);">
|
||||
<uni-icons type="person-filled" size="28" color="#FFFFFF"></uni-icons>
|
||||
</view>
|
||||
<text class="module-name">租客管理</text>
|
||||
</view>
|
||||
<view class="module-item" @click="navigateTo('/pages/rentals/rentals')">
|
||||
<view class="module-icon" style="background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);">
|
||||
<uni-icons type="paperclip" size="28" color="#FFFFFF"></uni-icons>
|
||||
</view>
|
||||
<text class="module-name">租赁管理</text>
|
||||
</view>
|
||||
<view class="module-item" @click="navigateTo('/pages/bills/bills')">
|
||||
<view class="module-icon" style="background: linear-gradient(135deg, #fa709a 0%, #fee140 100%);">
|
||||
<uni-icons type="wallet-filled" size="28" color="#FFFFFF"></uni-icons>
|
||||
</view>
|
||||
<text class="module-name">账单管理</text>
|
||||
</view>
|
||||
<view class="module-item" @click="navigateTo('/pages/meter-readings/meter-readings')">
|
||||
<view class="module-icon" style="background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%);">
|
||||
<uni-icons type="settings-filled" size="28" color="#667eea"></uni-icons>
|
||||
</view>
|
||||
<text class="module-name">抄表管理</text>
|
||||
</view>
|
||||
<view class="module-item" @click="navigateTo('/pages/billing/billing-center')">
|
||||
<view class="module-icon" style="background: linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%);">
|
||||
<uni-icons type="vip-filled" size="28" color="#e67e22"></uni-icons>
|
||||
</view>
|
||||
<text class="module-name">计费中心</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 房间状态分布 -->
|
||||
<view class="status-section">
|
||||
<view class="section-header">
|
||||
|
|
@ -142,9 +189,12 @@
|
|||
|
||||
<!-- 预警提醒区 -->
|
||||
<view class="alert-section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">预警提醒</text>
|
||||
</view>
|
||||
<view class="alert-card alert-warning" @click="navigateToRentals('soon_expire')">
|
||||
<view class="alert-icon">
|
||||
<uni-icons type="clock-filled" size="24" color="#E6A23C"></uni-icons>
|
||||
<uni-icons type="notification-filled" size="24" color="#E6A23C"></uni-icons>
|
||||
<view v-if="soonExpireRoomCount > 0" class="badge">{{soonExpireRoomCount}}</view>
|
||||
</view>
|
||||
<view class="alert-content">
|
||||
|
|
@ -213,38 +263,7 @@
|
|||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 快捷功能入口 -->
|
||||
<view class="quick-menu">
|
||||
<view class="menu-title">快捷功能</view>
|
||||
<view class="menu-grid">
|
||||
<view class="menu-item" @click="navigateTo('/pages/rental-add/rental-add')">
|
||||
<view class="menu-icon" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);">
|
||||
<uni-icons type="plus-filled" size="24" color="#FFFFFF"></uni-icons>
|
||||
</view>
|
||||
<text>办理入住</text>
|
||||
</view>
|
||||
<view class="menu-item" @click="navigateTo('/pages/tenants/tenants')">
|
||||
<view class="menu-icon" style="background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);">
|
||||
<uni-icons type="person-filled" size="24" color="#FFFFFF"></uni-icons>
|
||||
</view>
|
||||
<text>租客管理</text>
|
||||
</view>
|
||||
<view class="menu-item" @click="navigateTo('/pages/rentals/rentals')">
|
||||
<view class="menu-icon" style="background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);">
|
||||
<uni-icons type="list-filled" size="24" color="#FFFFFF"></uni-icons>
|
||||
</view>
|
||||
<text>租赁记录</text>
|
||||
</view>
|
||||
<view class="menu-item" @click="navigateTo('/pages/stats/stats')">
|
||||
<view class="menu-icon" style="background: linear-gradient(135deg, #fa709a 0%, #fee140 100%);">
|
||||
<uni-icons type="chart-filled" size="24" color="#FFFFFF"></uni-icons>
|
||||
</view>
|
||||
<text>收支统计</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="safe-area-bottom" style="height: 40rpx;"></view>
|
||||
<view class="safe-area-bottom" style="height: 40rpx;"></view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
|
|
@ -266,7 +285,8 @@
|
|||
monthlyReceivable: 0,
|
||||
monthlyReceived: 0,
|
||||
unpaidBillCount: 0,
|
||||
apartmentHouseStats: []
|
||||
apartmentHouseStats: [],
|
||||
isLoading: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -280,7 +300,10 @@
|
|||
this.loadData()
|
||||
},
|
||||
onShow() {
|
||||
this.loadData()
|
||||
// 避免与onLoad重复加载,使用延迟确保onLoad先执行
|
||||
setTimeout(() => {
|
||||
this.loadData()
|
||||
}, 100)
|
||||
},
|
||||
onPullDownRefresh() {
|
||||
this.loadData().finally(() => {
|
||||
|
|
@ -299,6 +322,10 @@
|
|||
},
|
||||
|
||||
async loadData() {
|
||||
// 防止重复加载
|
||||
if (this.isLoading) return
|
||||
|
||||
this.isLoading = true
|
||||
try {
|
||||
await Promise.all([
|
||||
this.loadDashboardStats(),
|
||||
|
|
@ -307,6 +334,8 @@
|
|||
])
|
||||
} catch (error) {
|
||||
console.error('加载数据失败:', error)
|
||||
} finally {
|
||||
this.isLoading = false
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -321,8 +350,6 @@
|
|||
this.rentedRoomCount = data.rentedRoomCount || 0
|
||||
this.soonExpireRoomCount = data.soonExpireRoomCount || 0
|
||||
this.expiredRoomCount = data.expiredRoomCount || 0
|
||||
this.monthlyReceivable = data.monthlyReceivable || 0
|
||||
this.monthlyReceived = data.monthlyReceived || 0
|
||||
} catch (error) {
|
||||
console.error('加载仪表盘数据失败:', error)
|
||||
}
|
||||
|
|
@ -341,13 +368,24 @@
|
|||
try {
|
||||
const res = await billApi.getStatistics()
|
||||
const data = res.data || {}
|
||||
|
||||
// 适配API返回的数据结构
|
||||
const summary = data.summary || {}
|
||||
const statusList = data.status || []
|
||||
|
||||
// 计算未收账单数量(unpaid + partial)
|
||||
this.unpaidBillCount = statusList
|
||||
.filter(item => item.status === 'unpaid' || item.status === 'partial')
|
||||
.reduce((sum, item) => sum + (item.count || 0), 0)
|
||||
|
||||
// 从summary获取应收实收数据
|
||||
this.monthlyReceivable = parseFloat(summary.totalReceivable) || 0
|
||||
this.monthlyReceived = parseFloat(summary.totalReceived) || 0
|
||||
} catch (error) {
|
||||
console.error('加载账单统计失败:', error)
|
||||
this.unpaidBillCount = 0
|
||||
this.monthlyReceivable = 0
|
||||
this.monthlyReceived = 0
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -403,7 +441,7 @@
|
|||
<style scoped>
|
||||
.home-page {
|
||||
min-height: 100vh;
|
||||
background: #F8FAFC;
|
||||
background: linear-gradient(180deg, #F8FAFC 0%, #F1F5F9 100%);
|
||||
padding-bottom: 40rpx;
|
||||
}
|
||||
|
||||
|
|
@ -411,13 +449,21 @@
|
|||
.welcome-section {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
padding: 40rpx 32rpx;
|
||||
border-radius: 0 0 32rpx 32rpx;
|
||||
border-radius: 0 0 40rpx 40rpx;
|
||||
box-shadow: 0 8rpx 32rpx rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
|
||||
.welcome-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.welcome-title {
|
||||
display: block;
|
||||
font-size: 40rpx;
|
||||
|
|
@ -429,7 +475,7 @@
|
|||
.welcome-date {
|
||||
display: block;
|
||||
font-size: 26rpx;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
}
|
||||
|
||||
.quick-actions {
|
||||
|
|
@ -443,15 +489,26 @@
|
|||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12rpx;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
padding: 24rpx;
|
||||
border-radius: 16rpx;
|
||||
backdrop-filter: blur(10rpx);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.action-btn:active {
|
||||
transform: scale(0.98);
|
||||
background: rgba(255, 255, 255, 0.35);
|
||||
}
|
||||
|
||||
.action-btn.secondary {
|
||||
background: #FFFFFF;
|
||||
}
|
||||
|
||||
.action-btn.secondary:active {
|
||||
background: #F8FAFC;
|
||||
}
|
||||
|
||||
.action-btn text {
|
||||
font-size: 28rpx;
|
||||
color: #FFFFFF;
|
||||
|
|
@ -459,7 +516,7 @@
|
|||
}
|
||||
|
||||
.action-btn.secondary text {
|
||||
color: #2563EB;
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
/* KPI卡片区 */
|
||||
|
|
@ -467,8 +524,8 @@
|
|||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 20rpx;
|
||||
padding: 24rpx 32rpx;
|
||||
margin-top: -20rpx;
|
||||
padding: 32rpx;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.kpi-card {
|
||||
|
|
@ -477,7 +534,13 @@
|
|||
padding: 28rpx;
|
||||
border-radius: 20rpx;
|
||||
background: #FFFFFF;
|
||||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.08);
|
||||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.kpi-card:active {
|
||||
transform: translateY(-4rpx);
|
||||
box-shadow: 0 8rpx 30rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.kpi-icon {
|
||||
|
|
@ -491,19 +554,19 @@
|
|||
}
|
||||
|
||||
.kpi-primary .kpi-icon {
|
||||
background: linear-gradient(135deg, #409EFF 0%, #66b1ff 100%);
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
|
||||
.kpi-success .kpi-icon {
|
||||
background: linear-gradient(135deg, #67C23A 0%, #85ce61 100%);
|
||||
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
|
||||
}
|
||||
|
||||
.kpi-warning .kpi-icon {
|
||||
background: linear-gradient(135deg, #E6A23C 0%, #ebb563 100%);
|
||||
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||
}
|
||||
|
||||
.kpi-info .kpi-icon {
|
||||
background: linear-gradient(135deg, #909399 0%, #a6a9ad 100%);
|
||||
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
||||
}
|
||||
|
||||
.kpi-content {
|
||||
|
|
@ -524,15 +587,6 @@
|
|||
color: #64748B;
|
||||
}
|
||||
|
||||
/* 状态分布区 */
|
||||
.status-section {
|
||||
background: #FFFFFF;
|
||||
margin: 0 32rpx 24rpx;
|
||||
padding: 28rpx;
|
||||
border-radius: 20rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
|
@ -551,12 +605,59 @@
|
|||
color: #94A3B8;
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
/* 功能模块网格 */
|
||||
.module-section {
|
||||
background: #FFFFFF;
|
||||
margin: 0 32rpx 24rpx;
|
||||
padding: 28rpx;
|
||||
border-radius: 20rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.module-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.module-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
padding: 20rpx 12rpx;
|
||||
border-radius: 16rpx;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.module-item:active {
|
||||
background: #F8FAFC;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.module-icon {
|
||||
width: 100rpx;
|
||||
height: 100rpx;
|
||||
border-radius: 28rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
justify-content: center;
|
||||
box-shadow: 0 6rpx 16rpx rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.module-name {
|
||||
font-size: 26rpx;
|
||||
color: #409EFF;
|
||||
font-weight: 600;
|
||||
color: #1E293B;
|
||||
}
|
||||
|
||||
/* 状态分布区 */
|
||||
.status-section {
|
||||
background: #FFFFFF;
|
||||
margin: 0 32rpx 24rpx;
|
||||
padding: 28rpx;
|
||||
border-radius: 20rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.status-list {
|
||||
|
|
@ -569,6 +670,11 @@
|
|||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
padding: 12rpx 0;
|
||||
}
|
||||
|
||||
.status-item:active {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.status-info {
|
||||
|
|
@ -611,7 +717,7 @@
|
|||
.progress-fill {
|
||||
height: 100%;
|
||||
border-radius: 6rpx;
|
||||
transition: width 0.3s ease;
|
||||
transition: width 0.5s ease;
|
||||
}
|
||||
|
||||
.status-count {
|
||||
|
|
@ -624,8 +730,7 @@
|
|||
|
||||
/* 预警提醒区 */
|
||||
.alert-section {
|
||||
padding: 0 32rpx;
|
||||
margin-bottom: 24rpx;
|
||||
margin: 0 32rpx 24rpx;
|
||||
}
|
||||
|
||||
.alert-card {
|
||||
|
|
@ -637,6 +742,11 @@
|
|||
margin-bottom: 20rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
|
||||
border-left: 8rpx solid transparent;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.alert-card:active {
|
||||
transform: translateX(8rpx);
|
||||
}
|
||||
|
||||
.alert-warning {
|
||||
|
|
@ -718,6 +828,18 @@
|
|||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
font-size: 26rpx;
|
||||
color: #409EFF;
|
||||
}
|
||||
|
||||
.refresh-btn:active {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.apartment-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
@ -759,11 +881,16 @@
|
|||
}
|
||||
|
||||
.stat-tag {
|
||||
padding: 8rpx 16rpx;
|
||||
padding: 10rpx 18rpx;
|
||||
background: #F3F4F6;
|
||||
border-radius: 8rpx;
|
||||
font-size: 22rpx;
|
||||
color: #9CA3AF;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.stat-tag:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.stat-tag.active {
|
||||
|
|
@ -785,47 +912,4 @@
|
|||
background: #FEE2E2;
|
||||
color: #DC2626;
|
||||
}
|
||||
|
||||
/* 快捷菜单 */
|
||||
.quick-menu {
|
||||
background: #FFFFFF;
|
||||
margin: 0 32rpx;
|
||||
padding: 28rpx;
|
||||
border-radius: 20rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.menu-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
color: #1E293B;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.menu-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.menu-icon {
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
border-radius: 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.menu-item text {
|
||||
font-size: 24rpx;
|
||||
color: #64748B;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -3,7 +3,12 @@
|
|||
<!-- 自定义导航栏 -->
|
||||
<view class="custom-nav safe-area-top">
|
||||
<view class="nav-content">
|
||||
<text class="nav-title">租赁记录</text>
|
||||
<view class="nav-left">
|
||||
<view class="nav-btn back-btn" @click="goBack">
|
||||
<uni-icons type="left" size="22" color="#1E293B"></uni-icons>
|
||||
</view>
|
||||
<text class="nav-title">租赁记录</text>
|
||||
</view>
|
||||
<view class="nav-actions">
|
||||
<view class="nav-btn" @click="showFilter">
|
||||
<uni-icons type="settings-filled" size="22" color="#1E293B"></uni-icons>
|
||||
|
|
@ -31,24 +36,6 @@
|
|||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<view class="stats-bar">
|
||||
<view class="stat-item">
|
||||
<text class="stat-num">{{activeCount}}</text>
|
||||
<text class="stat-label">在租</text>
|
||||
</view>
|
||||
<view class="stat-divider"></view>
|
||||
<view class="stat-item">
|
||||
<text class="stat-num warning">{{expiringCount}}</text>
|
||||
<text class="stat-label">即将到期</text>
|
||||
</view>
|
||||
<view class="stat-divider"></view>
|
||||
<view class="stat-item">
|
||||
<text class="stat-num">{{expiredCount}}</text>
|
||||
<text class="stat-label">已到期</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 筛选标签 -->
|
||||
<scroll-view scroll-x class="filter-tabs" show-scrollbar="false">
|
||||
<view
|
||||
|
|
@ -211,12 +198,103 @@
|
|||
</view>
|
||||
</view>
|
||||
</uni-popup>
|
||||
|
||||
<!-- 筛选弹窗 -->
|
||||
<uni-popup ref="filterPopup" type="right">
|
||||
<view class="filter-drawer">
|
||||
<view class="filter-header">
|
||||
<text class="filter-title">筛选条件</text>
|
||||
<uni-icons type="close" size="20" color="#94A3B8" @click="closeFilterPopup"></uni-icons>
|
||||
</view>
|
||||
<scroll-view scroll-y class="filter-body">
|
||||
<!-- 公寓筛选 -->
|
||||
<view class="filter-section">
|
||||
<text class="filter-label">公寓</text>
|
||||
<view class="filter-options">
|
||||
<view
|
||||
v-for="apt in apartmentFilterOptions"
|
||||
:key="apt.value"
|
||||
class="filter-option"
|
||||
:class="{ active: filterForm.apartmentId === apt.value }"
|
||||
@click="selectApartment(apt.value)"
|
||||
>
|
||||
<text>{{apt.label}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 房间筛选 -->
|
||||
<view class="filter-section" v-if="filterForm.apartmentId">
|
||||
<text class="filter-label">房间</text>
|
||||
<view class="filter-options">
|
||||
<view
|
||||
v-for="room in roomFilterOptions"
|
||||
:key="room.value"
|
||||
class="filter-option"
|
||||
:class="{ active: filterForm.roomId === room.value }"
|
||||
@click="selectRoom(room.value)"
|
||||
>
|
||||
<text>{{room.label}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 状态筛选 -->
|
||||
<view class="filter-section">
|
||||
<text class="filter-label">租赁状态</text>
|
||||
<view class="filter-options">
|
||||
<view
|
||||
v-for="status in statusOptions"
|
||||
:key="status.value"
|
||||
class="filter-option"
|
||||
:class="{ active: filterForm.status === status.value }"
|
||||
@click="selectStatus(status.value)"
|
||||
>
|
||||
<text>{{status.label}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 付租方式筛选 -->
|
||||
<view class="filter-section">
|
||||
<text class="filter-label">付租方式</text>
|
||||
<view class="filter-options">
|
||||
<view
|
||||
v-for="type in paymentTypeOptions"
|
||||
:key="type.value"
|
||||
class="filter-option"
|
||||
:class="{ active: filterForm.paymentType === type.value }"
|
||||
@click="selectPaymentType(type.value)"
|
||||
>
|
||||
<text>{{type.label}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 租客姓名 -->
|
||||
<view class="filter-section">
|
||||
<text class="filter-label">租客姓名</text>
|
||||
<input
|
||||
class="filter-input"
|
||||
type="text"
|
||||
placeholder="请输入租客姓名"
|
||||
v-model="filterForm.renterName"
|
||||
/>
|
||||
</view>
|
||||
</scroll-view>
|
||||
<view class="filter-footer">
|
||||
<button class="btn-reset" @click="resetFilter">重置</button>
|
||||
<button class="btn-confirm" @click="confirmFilter">确定</button>
|
||||
</view>
|
||||
</view>
|
||||
</uni-popup>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import rentalApi from '@/api/rental.js'
|
||||
import apartmentApi from '@/api/apartment.js'
|
||||
import roomApi from '@/api/room.js'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
|
|
@ -234,6 +312,7 @@
|
|||
],
|
||||
rentals: [],
|
||||
apartments: [],
|
||||
rooms: [],
|
||||
selectedApartmentId: '',
|
||||
loadStatus: 'more',
|
||||
page: 1,
|
||||
|
|
@ -245,19 +324,32 @@
|
|||
electricityMeterEnd: '',
|
||||
refundDeposit: '',
|
||||
remark: ''
|
||||
}
|
||||
},
|
||||
// 筛选表单
|
||||
filterForm: {
|
||||
apartmentId: '',
|
||||
roomId: '',
|
||||
status: '',
|
||||
paymentType: '',
|
||||
renterName: ''
|
||||
},
|
||||
// 筛选选项
|
||||
statusOptions: [
|
||||
{ label: '全部', value: '' },
|
||||
{ label: '在租', value: 'active' },
|
||||
{ label: '已到期', value: 'expired' },
|
||||
{ label: '已终止', value: 'terminated' }
|
||||
],
|
||||
paymentTypeOptions: [
|
||||
{ label: '全部', value: '' },
|
||||
{ label: '月租', value: 'monthly' },
|
||||
{ label: '季租', value: 'quarterly' },
|
||||
{ label: '半年租', value: 'half_year' },
|
||||
{ label: '年租', value: 'yearly' }
|
||||
]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
activeCount() {
|
||||
return this.rentals.filter(r => r.status === 'active').length
|
||||
},
|
||||
expiringCount() {
|
||||
return this.rentals.filter(r => r.status === 'active' && this.isExpiringSoon(r.endDate)).length
|
||||
},
|
||||
expiredCount() {
|
||||
return this.rentals.filter(r => r.status === 'expired').length
|
||||
},
|
||||
filteredRentals() {
|
||||
let list = this.rentals
|
||||
|
||||
|
|
@ -285,10 +377,22 @@
|
|||
if (!this.selectedApartmentId) return '全部公寓'
|
||||
const apartment = this.apartments.find(a => a.id === this.selectedApartmentId)
|
||||
return apartment ? apartment.name : '全部公寓'
|
||||
},
|
||||
// 筛选弹窗用的公寓选项
|
||||
apartmentFilterOptions() {
|
||||
return [{ label: '全部', value: '' }, ...this.apartments.map(a => ({ label: a.name, value: a.id }))]
|
||||
},
|
||||
// 筛选弹窗用的房间选项
|
||||
roomFilterOptions() {
|
||||
const filteredRooms = this.filterForm.apartmentId
|
||||
? this.rooms.filter(r => r.apartmentId === this.filterForm.apartmentId)
|
||||
: this.rooms
|
||||
return [{ label: '全部', value: '' }, ...filteredRooms.map(r => ({ label: r.roomNumber, value: r.id }))]
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
this.loadApartments()
|
||||
this.loadRooms()
|
||||
this.loadRentals()
|
||||
},
|
||||
onShow() {
|
||||
|
|
@ -297,7 +401,7 @@
|
|||
methods: {
|
||||
async loadApartments() {
|
||||
try {
|
||||
const res = await apartmentApi.getList({ pageSize: 999 })
|
||||
const res = await apartmentApi.list()
|
||||
if (res.data) {
|
||||
this.apartments = res.data
|
||||
}
|
||||
|
|
@ -305,19 +409,41 @@
|
|||
console.error('加载公寓列表失败:', error)
|
||||
}
|
||||
},
|
||||
|
||||
async loadRooms() {
|
||||
try {
|
||||
const res = await roomApi.list()
|
||||
if (res.data) {
|
||||
this.rooms = res.data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载房间列表失败:', error)
|
||||
}
|
||||
},
|
||||
|
||||
async loadRentals() {
|
||||
// 防止重复请求
|
||||
if (this.isLoading) return
|
||||
|
||||
this.isLoading = true
|
||||
try {
|
||||
const params = {
|
||||
page: this.page,
|
||||
pageSize: this.pageSize,
|
||||
renterName: this.searchKeyword
|
||||
renterName: this.searchKeyword || this.filterForm.renterName,
|
||||
apartmentId: this.filterForm.apartmentId,
|
||||
roomId: this.filterForm.roomId,
|
||||
status: this.filterForm.status,
|
||||
paymentType: this.filterForm.paymentType
|
||||
}
|
||||
|
||||
const res = await rentalApi.getList(params)
|
||||
if (res.data) {
|
||||
const list = res.data.map(item => ({
|
||||
// 后端返回格式: { data: { list: [], total: n, page: n, pageSize: n } }
|
||||
const pageData = res.data || {}
|
||||
const rawList = pageData.list || []
|
||||
|
||||
if (rawList.length > 0 || this.page === 1) {
|
||||
const list = rawList.map(item => ({
|
||||
id: item.id,
|
||||
apartmentId: item.Room?.Apartment?.id,
|
||||
apartmentName: item.Room?.Apartment?.name || '--',
|
||||
|
|
@ -344,7 +470,7 @@
|
|||
this.rentals = [...this.rentals, ...list]
|
||||
}
|
||||
|
||||
this.total = res.total || 0
|
||||
this.total = pageData.total || 0
|
||||
this.loadStatus = this.rentals.length < this.total ? 'more' : 'noMore'
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
@ -390,11 +516,15 @@
|
|||
},
|
||||
|
||||
addRental() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/rental-add/rental-add'
|
||||
uni.switchTab({
|
||||
url: '/pages/add-record/add-record'
|
||||
})
|
||||
},
|
||||
|
||||
goBack() {
|
||||
uni.navigateBack()
|
||||
},
|
||||
|
||||
viewRentalDetail(item) {
|
||||
uni.navigateTo({
|
||||
url: `/pages/rental-detail/rental-detail?id=${item.id}`
|
||||
|
|
@ -462,11 +592,53 @@
|
|||
}
|
||||
},
|
||||
|
||||
// 筛选弹窗相关方法
|
||||
showFilter() {
|
||||
uni.showToast({
|
||||
title: '筛选功能',
|
||||
icon: 'none'
|
||||
})
|
||||
// 打开筛选弹窗前加载房间数据
|
||||
this.loadRooms()
|
||||
this.$refs.filterPopup.open()
|
||||
},
|
||||
|
||||
closeFilterPopup() {
|
||||
this.$refs.filterPopup.close()
|
||||
},
|
||||
|
||||
selectApartment(apartmentId) {
|
||||
this.filterForm.apartmentId = apartmentId
|
||||
// 切换公寓时重置房间选择
|
||||
this.filterForm.roomId = ''
|
||||
},
|
||||
|
||||
selectRoom(roomId) {
|
||||
this.filterForm.roomId = roomId
|
||||
},
|
||||
|
||||
selectStatus(status) {
|
||||
this.filterForm.status = status
|
||||
},
|
||||
|
||||
selectPaymentType(type) {
|
||||
this.filterForm.paymentType = type
|
||||
},
|
||||
|
||||
resetFilter() {
|
||||
this.filterForm = {
|
||||
apartmentId: '',
|
||||
roomId: '',
|
||||
status: '',
|
||||
paymentType: '',
|
||||
renterName: ''
|
||||
}
|
||||
this.searchKeyword = ''
|
||||
this.page = 1
|
||||
this.loadRentals()
|
||||
this.closeFilterPopup()
|
||||
},
|
||||
|
||||
confirmFilter() {
|
||||
this.page = 1
|
||||
this.loadRentals()
|
||||
this.closeFilterPopup()
|
||||
},
|
||||
|
||||
loadMore() {
|
||||
|
|
@ -534,6 +706,12 @@
|
|||
padding: 20rpx 32rpx;
|
||||
}
|
||||
|
||||
.nav-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
|
|
@ -545,6 +723,10 @@
|
|||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
margin-left: -8rpx;
|
||||
}
|
||||
|
||||
.nav-btn {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
|
|
@ -1034,4 +1216,105 @@
|
|||
font-weight: 600;
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* 筛选抽屉样式 */
|
||||
.filter-drawer {
|
||||
width: 600rpx;
|
||||
height: 100vh;
|
||||
background: #FFFFFF;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.filter-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 32rpx;
|
||||
border-bottom: 2rpx solid #F1F5F9;
|
||||
}
|
||||
|
||||
.filter-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
color: #1E293B;
|
||||
}
|
||||
|
||||
.filter-body {
|
||||
flex: 1;
|
||||
padding: 32rpx;
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #1E293B;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.filter-options {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.filter-option {
|
||||
padding: 16rpx 32rpx;
|
||||
background: #F1F5F9;
|
||||
border-radius: 12rpx;
|
||||
font-size: 26rpx;
|
||||
color: #64748B;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.filter-option.active {
|
||||
background: #DBEAFE;
|
||||
color: #2563EB;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.filter-input {
|
||||
width: 100%;
|
||||
height: 80rpx;
|
||||
background: #F8FAFC;
|
||||
border-radius: 12rpx;
|
||||
padding: 0 24rpx;
|
||||
font-size: 28rpx;
|
||||
color: #1E293B;
|
||||
border: 2rpx solid #E2E8F0;
|
||||
}
|
||||
|
||||
.filter-footer {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
padding: 24rpx 32rpx;
|
||||
border-top: 2rpx solid #F1F5F9;
|
||||
background: #FFFFFF;
|
||||
}
|
||||
|
||||
.filter-footer .btn-reset {
|
||||
flex: 1;
|
||||
height: 80rpx;
|
||||
background: #F3F4F6;
|
||||
border-radius: 12rpx;
|
||||
font-size: 28rpx;
|
||||
color: #64748B;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.filter-footer .btn-confirm {
|
||||
flex: 1;
|
||||
height: 80rpx;
|
||||
background: linear-gradient(135deg, #2563EB 0%, #1D4ED8 100%);
|
||||
border-radius: 12rpx;
|
||||
font-size: 28rpx;
|
||||
color: #FFFFFF;
|
||||
font-weight: 600;
|
||||
border: none;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -39,24 +39,6 @@
|
|||
<text class="item-label">身份证号</text>
|
||||
<text class="item-value">{{renter.idCard || '-'}}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="item-label">紧急联系人</text>
|
||||
<text class="item-value">{{renter.emergencyContact || '-'}}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="item-label">紧急电话</text>
|
||||
<view class="item-value">
|
||||
<text>{{renter.emergencyPhone || '-'}}</text>
|
||||
<uni-icons v-if="renter.emergencyPhone" type="phone-filled" size="18" color="#2563EB" @click="callPhone(renter.emergencyPhone)"></uni-icons>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="info-section" v-if="renter.remark">
|
||||
<view class="section-title">备注信息</view>
|
||||
<view class="info-card">
|
||||
<text class="remark-text">{{renter.remark}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
|
|
@ -96,7 +78,7 @@
|
|||
try {
|
||||
uni.showLoading({ title: '加载中...' })
|
||||
const res = await renterApi.getDetail(this.renterId)
|
||||
this.renter = res.data || {}
|
||||
this.renter = res.data
|
||||
} catch (error) {
|
||||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||
} finally {
|
||||
|
|
@ -110,14 +92,14 @@
|
|||
uni.navigateTo({ url: `/pages/rentals/rentals?renterId=${this.renterId}` })
|
||||
},
|
||||
addRental() {
|
||||
uni.navigateTo({ url: `/pages/add-record/add-record?renterId=${this.renterId}` })
|
||||
uni.switchTab({ url: `/pages/add-record/add-record?renterId=${this.renterId}` })
|
||||
},
|
||||
callPhone(phone) {
|
||||
uni.makePhoneCall({ phoneNumber: phone })
|
||||
},
|
||||
goBack() { uni.navigateBack() },
|
||||
getStatusText(status) {
|
||||
const texts = { 'active': '在租', 'inactive': '已退租' }
|
||||
const texts = { 'active': '正常', 'inactive': '停用' }
|
||||
return texts[status] || '未知'
|
||||
}
|
||||
}
|
||||
|
|
@ -146,7 +128,6 @@
|
|||
.info-item:last-child { border-bottom: none; }
|
||||
.item-label { font-size: 30rpx; color: #64748B; }
|
||||
.item-value { display: flex; align-items: center; gap: 16rpx; font-size: 30rpx; color: #1E293B; font-weight: 500; }
|
||||
.remark-text { font-size: 30rpx; color: #1E293B; line-height: 1.6; padding: 28rpx 0; }
|
||||
.action-section { display: flex; gap: 24rpx; margin-top: 48rpx; }
|
||||
.action-btn { flex: 1; height: 96rpx; background: #F1F5F9; color: #64748B; font-size: 32rpx; font-weight: 600; border-radius: 16rpx; border: none; display: flex; align-items: center; justify-content: center; }
|
||||
.action-btn.primary { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: #FFFFFF; }
|
||||
|
|
|
|||
|
|
@ -18,7 +18,15 @@
|
|||
<view class="search-section">
|
||||
<view class="search-box">
|
||||
<uni-icons type="search" size="18" color="#94A3B8"></uni-icons>
|
||||
<input type="text" v-model="searchKeyword" placeholder="搜索租客姓名或电话" @confirm="search"/>
|
||||
<input type="text" v-model="searchForm.keyword" placeholder="搜索姓名/电话/身份证" @confirm="search"/>
|
||||
</view>
|
||||
<view class="filter-box">
|
||||
<picker mode="selector" :range="statusOptions" range-key="label" :value="statusIndex" @change="onStatusChange">
|
||||
<view class="filter-item">
|
||||
<text>{{statusOptions[statusIndex].label}}</text>
|
||||
<uni-icons type="down" size="14" color="#64748B"></uni-icons>
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
|
|
@ -36,7 +44,7 @@
|
|||
</view>
|
||||
<view class="info-detail">
|
||||
<text class="detail-item">{{item.phone || '暂无电话'}}</text>
|
||||
<text class="detail-item" v-if="item.idCard">身份证: {{maskIdCard(item.idCard)}}</text>
|
||||
<text class="detail-item" v-if="item.idCard">身份证: {{item.idCard || '-'}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<uni-icons type="right" size="16" color="#94A3B8"></uni-icons>
|
||||
|
|
@ -59,8 +67,18 @@
|
|||
export default {
|
||||
data() {
|
||||
return {
|
||||
searchKeyword: '',
|
||||
searchForm: {
|
||||
keyword: '',
|
||||
status: ''
|
||||
},
|
||||
statusOptions: [
|
||||
{ label: '全部', value: '' },
|
||||
{ label: '正常', value: 'active' },
|
||||
{ label: '停用', value: 'inactive' }
|
||||
],
|
||||
statusIndex: 0,
|
||||
renterList: [],
|
||||
total: 0,
|
||||
isLoading: false,
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
|
|
@ -78,18 +96,22 @@
|
|||
if (this.isLoading) return
|
||||
this.isLoading = true
|
||||
try {
|
||||
const res = await renterApi.list({
|
||||
keyword: this.searchKeyword,
|
||||
const params = {
|
||||
keyword: this.searchForm.keyword,
|
||||
status: this.searchForm.status,
|
||||
page: this.page,
|
||||
pageSize: this.pageSize
|
||||
})
|
||||
const list = res.data || []
|
||||
}
|
||||
const res = await renterApi.getAll(params)
|
||||
const list = res.data.list
|
||||
const total = res.data.total
|
||||
if (this.page === 1) {
|
||||
this.renterList = list
|
||||
} else {
|
||||
this.renterList = [...this.renterList, ...list]
|
||||
}
|
||||
this.hasMore = list.length >= this.pageSize
|
||||
this.total = total
|
||||
this.hasMore = this.renterList.length < total
|
||||
} catch (error) {
|
||||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||
} finally {
|
||||
|
|
@ -106,6 +128,12 @@
|
|||
this.page = 1
|
||||
this.loadData()
|
||||
},
|
||||
onStatusChange(e) {
|
||||
this.statusIndex = e.detail.value
|
||||
this.searchForm.status = this.statusOptions[this.statusIndex].value
|
||||
this.page = 1
|
||||
this.loadData()
|
||||
},
|
||||
addRenter() {
|
||||
uni.navigateTo({ url: '/pages/renter-add/renter-add' })
|
||||
},
|
||||
|
|
@ -116,12 +144,8 @@
|
|||
uni.navigateBack()
|
||||
},
|
||||
getStatusText(status) {
|
||||
const texts = { 'active': '在租', 'inactive': '已退租' }
|
||||
const texts = { 'active': '正常', 'inactive': '停用' }
|
||||
return texts[status] || '未知'
|
||||
},
|
||||
maskIdCard(idCard) {
|
||||
if (!idCard || idCard.length < 8) return idCard
|
||||
return idCard.slice(0, 4) + '****' + idCard.slice(-4)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -135,9 +159,12 @@
|
|||
.nav-title { font-size: 36rpx; font-weight: 700; color: #1E293B; }
|
||||
.nav-actions { width: 60rpx; height: 60rpx; display: flex; align-items: center; justify-content: center; }
|
||||
.page-content { flex: 1; padding: 24rpx 32rpx; }
|
||||
.search-section { margin-bottom: 24rpx; }
|
||||
.search-box { display: flex; align-items: center; background: #FFFFFF; border-radius: 16rpx; padding: 20rpx 24rpx; gap: 16rpx; }
|
||||
.search-section { margin-bottom: 24rpx; display: flex; gap: 16rpx; }
|
||||
.search-box { flex: 1; display: flex; align-items: center; background: #FFFFFF; border-radius: 16rpx; padding: 20rpx 24rpx; gap: 16rpx; }
|
||||
.search-box input { flex: 1; font-size: 28rpx; color: #1E293B; }
|
||||
.filter-box { background: #FFFFFF; border-radius: 16rpx; padding: 0 24rpx; display: flex; align-items: center; }
|
||||
.filter-item { display: flex; align-items: center; gap: 8rpx; height: 72rpx; }
|
||||
.filter-item text { font-size: 28rpx; color: #64748B; }
|
||||
.renter-list { display: flex; flex-direction: column; gap: 24rpx; }
|
||||
.renter-card { background: #FFFFFF; border-radius: 24rpx; padding: 32rpx; display: flex; align-items: center; gap: 24rpx; box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.04); }
|
||||
.renter-avatar { width: 88rpx; height: 88rpx; border-radius: 50%; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); display: flex; align-items: center; justify-content: center; }
|
||||
|
|
|
|||
|
|
@ -16,63 +16,135 @@
|
|||
<view class="form-card">
|
||||
<view class="form-item">
|
||||
<text class="form-label">所属公寓 <text class="required">*</text></text>
|
||||
<picker mode="selector" :range="apartmentOptions" :value="selectedApartmentIndex" @change="onApartmentChange">
|
||||
<view class="picker-value" :class="{ placeholder: !form.apartmentId }">
|
||||
<text>{{selectedApartmentLabel}}</text>
|
||||
<uni-icons type="arrowright" size="16" color="#94A3B8"></uni-icons>
|
||||
</view>
|
||||
</picker>
|
||||
<view class="form-control">
|
||||
<picker mode="selector" :range="apartmentOptions" :value="selectedApartmentIndex" @change="onApartmentChange">
|
||||
<view class="picker-value" :class="{ placeholder: !form.apartmentId }">
|
||||
<text>{{selectedApartmentLabel}}</text>
|
||||
<uni-icons type="arrowright" size="16" color="#94A3B8"></uni-icons>
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="form-label">房间号 <text class="required">*</text></text>
|
||||
<input
|
||||
type="text"
|
||||
v-model="form.roomNumber"
|
||||
placeholder="请输入房间号,如:101"
|
||||
class="form-input"
|
||||
maxlength="20"
|
||||
/>
|
||||
<view class="form-control">
|
||||
<input
|
||||
type="text"
|
||||
v-model="form.roomNumber"
|
||||
placeholder="请输入房间号,如:101"
|
||||
class="form-input"
|
||||
maxlength="10"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="form-label">房间面积 (m²)</text>
|
||||
<input
|
||||
type="digit"
|
||||
v-model="form.area"
|
||||
placeholder="请输入房间面积"
|
||||
class="form-input"
|
||||
/>
|
||||
<text class="form-label">楼层</text>
|
||||
<view class="form-control">
|
||||
<input
|
||||
type="number"
|
||||
v-model="form.floor"
|
||||
placeholder="请输入楼层"
|
||||
class="form-input"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="form-label">房间朝向</text>
|
||||
<picker mode="selector" :range="orientationOptions" :value="orientationIndex" @change="onOrientationChange">
|
||||
<view class="picker-value" :class="{ placeholder: !form.orientation }">
|
||||
<text>{{form.orientation || '请选择朝向'}}</text>
|
||||
<uni-icons type="arrowright" size="16" color="#94A3B8"></uni-icons>
|
||||
</view>
|
||||
</picker>
|
||||
<text class="form-label">户型</text>
|
||||
<view class="form-control">
|
||||
<input
|
||||
type="text"
|
||||
v-model="form.roomType"
|
||||
placeholder="请输入户型,如一室一厅"
|
||||
class="form-input"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="form-label">面积 (m²)</text>
|
||||
<view class="form-control">
|
||||
<input
|
||||
type="digit"
|
||||
v-model="form.area"
|
||||
placeholder="请输入面积"
|
||||
class="form-input"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="form-label">月租金 (元)</text>
|
||||
<input
|
||||
type="digit"
|
||||
v-model="form.rent"
|
||||
placeholder="请输入月租金"
|
||||
class="form-input"
|
||||
/>
|
||||
<view class="form-control">
|
||||
<input
|
||||
type="digit"
|
||||
v-model="form.monthlyPrice"
|
||||
placeholder="请输入月租金"
|
||||
class="form-input"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="form-label">房间描述</text>
|
||||
<textarea
|
||||
v-model="form.description"
|
||||
placeholder="请输入房间描述(选填)"
|
||||
class="form-textarea"
|
||||
maxlength="200"
|
||||
/>
|
||||
<text class="form-label">年租金 (元)</text>
|
||||
<view class="form-control">
|
||||
<input
|
||||
type="digit"
|
||||
v-model="form.yearlyPrice"
|
||||
placeholder="请输入年租金"
|
||||
class="form-input"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="form-label">押金 (元)</text>
|
||||
<view class="form-control">
|
||||
<input
|
||||
type="digit"
|
||||
v-model="form.deposit"
|
||||
placeholder="请输入押金"
|
||||
class="form-input"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="form-label">排序</text>
|
||||
<view class="form-control">
|
||||
<input
|
||||
type="number"
|
||||
v-model="form.sortOrder"
|
||||
placeholder="数字越小越靠前"
|
||||
class="form-input"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="form-label">主状态</text>
|
||||
<view class="form-control">
|
||||
<picker mode="selector" :range="statusOptions" :value="statusIndex" @change="onStatusChange">
|
||||
<view class="picker-value" :class="{ placeholder: !form.status }">
|
||||
<text>{{getStatusLabel(form.status)}}</text>
|
||||
<uni-icons type="arrowright" size="16" color="#94A3B8"></uni-icons>
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-item" v-if="form.status === 'rented'">
|
||||
<text class="form-label">租房标识</text>
|
||||
<view class="form-control">
|
||||
<picker mode="selector" :range="rentalStatusOptions" :value="rentalStatusIndex" @change="onRentalStatusChange">
|
||||
<view class="picker-value" :class="{ placeholder: !form.rentalStatus }">
|
||||
<text>{{getRentalStatusLabel(form.rentalStatus)}}</text>
|
||||
<uni-icons type="arrowright" size="16" color="#94A3B8"></uni-icons>
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
|
|
@ -85,7 +157,7 @@
|
|||
<script>
|
||||
import roomApi from '@/api/room.js'
|
||||
import apartmentApi from '@/api/apartment.js'
|
||||
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
|
|
@ -93,14 +165,22 @@
|
|||
roomId: null,
|
||||
submitLoading: false,
|
||||
apartments: [],
|
||||
orientationOptions: ['东', '南', '西', '北', '东南', '东北', '西南', '西北'],
|
||||
statusOptions: ['空房', '预定', '在租'],
|
||||
statusValues: ['empty', 'reserved', 'rented'],
|
||||
rentalStatusOptions: ['正常', '即将到期', '已到期'],
|
||||
rentalStatusValues: ['normal', 'soon_expire', 'expired'],
|
||||
form: {
|
||||
apartmentId: '',
|
||||
roomNumber: '',
|
||||
floor: '',
|
||||
roomType: '',
|
||||
area: '',
|
||||
orientation: '',
|
||||
rent: '',
|
||||
description: ''
|
||||
monthlyPrice: '',
|
||||
yearlyPrice: '',
|
||||
deposit: '',
|
||||
sortOrder: 0,
|
||||
status: 'empty',
|
||||
rentalStatus: 'normal'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -116,8 +196,11 @@
|
|||
const apartment = this.apartments.find(a => a.id === this.form.apartmentId)
|
||||
return apartment ? apartment.name : '请选择公寓'
|
||||
},
|
||||
orientationIndex() {
|
||||
return this.orientationOptions.indexOf(this.form.orientation)
|
||||
statusIndex() {
|
||||
return this.statusValues.indexOf(this.form.status)
|
||||
},
|
||||
rentalStatusIndex() {
|
||||
return this.rentalStatusValues.indexOf(this.form.rentalStatus)
|
||||
}
|
||||
},
|
||||
onLoad(options) {
|
||||
|
|
@ -133,7 +216,7 @@
|
|||
methods: {
|
||||
async loadApartments() {
|
||||
try {
|
||||
const res = await apartmentApi.getList({ pageSize: 999 })
|
||||
const res = await apartmentApi.list()
|
||||
if (res.data) {
|
||||
this.apartments = res.data
|
||||
}
|
||||
|
|
@ -149,10 +232,15 @@
|
|||
this.form = {
|
||||
apartmentId: data.apartmentId,
|
||||
roomNumber: data.roomNumber,
|
||||
floor: data.floor ? String(data.floor) : '',
|
||||
roomType: data.roomType || '',
|
||||
area: data.area ? String(data.area) : '',
|
||||
orientation: data.orientation || '',
|
||||
rent: data.rent ? String(data.rent) : '',
|
||||
description: data.description || ''
|
||||
monthlyPrice: data.monthlyPrice ? String(data.monthlyPrice) : '',
|
||||
yearlyPrice: data.yearlyPrice ? String(data.yearlyPrice) : '',
|
||||
deposit: data.deposit ? String(data.deposit) : '',
|
||||
sortOrder: data.sortOrder !== undefined ? data.sortOrder : 0,
|
||||
status: data.status || 'empty',
|
||||
rentalStatus: data.rentalStatus || 'normal'
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
@ -163,8 +251,19 @@
|
|||
onApartmentChange(e) {
|
||||
this.form.apartmentId = this.apartments[e.detail.value].id
|
||||
},
|
||||
onOrientationChange(e) {
|
||||
this.form.orientation = this.orientationOptions[e.detail.value]
|
||||
onStatusChange(e) {
|
||||
this.form.status = this.statusValues[e.detail.value]
|
||||
},
|
||||
onRentalStatusChange(e) {
|
||||
this.form.rentalStatus = this.rentalStatusValues[e.detail.value]
|
||||
},
|
||||
getStatusLabel(value) {
|
||||
const index = this.statusValues.indexOf(value)
|
||||
return index >= 0 ? this.statusOptions[index] : '请选择状态'
|
||||
},
|
||||
getRentalStatusLabel(value) {
|
||||
const index = this.rentalStatusValues.indexOf(value)
|
||||
return index >= 0 ? this.rentalStatusOptions[index] : '请选择租房标识'
|
||||
},
|
||||
async saveRoom() {
|
||||
if (!this.form.apartmentId) {
|
||||
|
|
@ -181,10 +280,15 @@
|
|||
const data = {
|
||||
apartmentId: this.form.apartmentId,
|
||||
roomNumber: this.form.roomNumber.trim(),
|
||||
floor: this.form.floor ? parseInt(this.form.floor) : null,
|
||||
roomType: this.form.roomType.trim() || null,
|
||||
area: this.form.area ? parseFloat(this.form.area) : null,
|
||||
orientation: this.form.orientation || null,
|
||||
rent: this.form.rent ? parseFloat(this.form.rent) : null,
|
||||
description: this.form.description.trim() || null
|
||||
monthlyPrice: this.form.monthlyPrice ? parseFloat(this.form.monthlyPrice) : null,
|
||||
yearlyPrice: this.form.yearlyPrice ? parseFloat(this.form.yearlyPrice) : null,
|
||||
deposit: this.form.deposit ? parseFloat(this.form.deposit) : null,
|
||||
sortOrder: this.form.sortOrder !== '' ? parseInt(this.form.sortOrder) : 0,
|
||||
status: this.form.status,
|
||||
rentalStatus: this.form.rentalStatus
|
||||
}
|
||||
|
||||
if (this.isEdit) {
|
||||
|
|
@ -256,6 +360,8 @@
|
|||
margin-bottom: 40rpx;
|
||||
}
|
||||
.form-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 24rpx 0;
|
||||
border-bottom: 2rpx solid #F1F5F9;
|
||||
}
|
||||
|
|
@ -263,10 +369,13 @@
|
|||
border-bottom: none;
|
||||
}
|
||||
.form-label {
|
||||
display: block;
|
||||
font-size: 26rpx;
|
||||
width: 180rpx;
|
||||
font-size: 28rpx;
|
||||
color: #64748B;
|
||||
margin-bottom: 16rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.form-control {
|
||||
flex: 1;
|
||||
}
|
||||
.required {
|
||||
color: #EF4444;
|
||||
|
|
@ -275,7 +384,7 @@
|
|||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-size: 30rpx;
|
||||
font-size: 28rpx;
|
||||
color: #1E293B;
|
||||
}
|
||||
.picker-value.placeholder {
|
||||
|
|
@ -283,23 +392,23 @@
|
|||
}
|
||||
.form-input {
|
||||
width: 100%;
|
||||
height: 60rpx;
|
||||
font-size: 30rpx;
|
||||
height: 48rpx;
|
||||
font-size: 28rpx;
|
||||
color: #1E293B;
|
||||
text-align: right;
|
||||
}
|
||||
.form-textarea {
|
||||
width: 100%;
|
||||
height: 160rpx;
|
||||
font-size: 30rpx;
|
||||
color: #1E293B;
|
||||
.form-input::placeholder {
|
||||
color: #94A3B8;
|
||||
}
|
||||
.btn-submit {
|
||||
background: linear-gradient(135deg, #2563EB 0%, #1D4ED8 100%);
|
||||
color: #FFFFFF;
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
padding: 28rpx;
|
||||
height: 88rpx;
|
||||
line-height: 88rpx;
|
||||
border-radius: 16rpx;
|
||||
border: none;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -27,18 +27,48 @@
|
|||
</view>
|
||||
<view class="info-divider"></view>
|
||||
<view class="info-item">
|
||||
<text class="label">房间面积</text>
|
||||
<text class="label">房间号</text>
|
||||
<text class="value">{{room.roomNumber}}</text>
|
||||
</view>
|
||||
<view class="info-divider"></view>
|
||||
<view class="info-item">
|
||||
<text class="label">楼层</text>
|
||||
<text class="value">{{room.floor || '--'}}</text>
|
||||
</view>
|
||||
<view class="info-divider"></view>
|
||||
<view class="info-item">
|
||||
<text class="label">户型</text>
|
||||
<text class="value">{{room.roomType || '--'}}</text>
|
||||
</view>
|
||||
<view class="info-divider"></view>
|
||||
<view class="info-item">
|
||||
<text class="label">面积</text>
|
||||
<text class="value">{{room.area || '--'}} m²</text>
|
||||
</view>
|
||||
<view class="info-divider"></view>
|
||||
<view class="info-item">
|
||||
<text class="label">房间朝向</text>
|
||||
<text class="value">{{room.orientation || '--'}}</text>
|
||||
<text class="label">月租金</text>
|
||||
<text class="value price">¥{{room.monthlyPrice || 0}}</text>
|
||||
</view>
|
||||
<view class="info-divider"></view>
|
||||
<view class="info-item">
|
||||
<text class="label">月租金</text>
|
||||
<text class="value price">¥{{room.rent || 0}}</text>
|
||||
<text class="label">年租金</text>
|
||||
<text class="value price">¥{{room.yearlyPrice || 0}}</text>
|
||||
</view>
|
||||
<view class="info-divider"></view>
|
||||
<view class="info-item">
|
||||
<text class="label">押金</text>
|
||||
<text class="value price">¥{{room.deposit || 0}}</text>
|
||||
</view>
|
||||
<view class="info-divider"></view>
|
||||
<view class="info-item">
|
||||
<text class="label">主状态</text>
|
||||
<text class="value">{{room.statusText}}</text>
|
||||
</view>
|
||||
<view class="info-divider" v-if="room.status === 'rented' && room.rentalStatusText"></view>
|
||||
<view class="info-item" v-if="room.status === 'rented' && room.rentalStatusText">
|
||||
<text class="label">租房标识</text>
|
||||
<text class="value">{{room.rentalStatusText}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
|
@ -56,10 +86,34 @@
|
|||
</view>
|
||||
|
||||
<view class="action-section">
|
||||
<view class="action-btn primary" v-if="room.status === 'vacant'" @click="addRental">
|
||||
<uni-icons type="plus" size="20" color="#FFFFFF"></uni-icons>
|
||||
<text>办理入住</text>
|
||||
<!-- 空房状态:显示预定和办理入住按钮 -->
|
||||
<template v-if="room.status === 'empty'">
|
||||
<view class="action-btn warning" @click="reserveRoom">
|
||||
<uni-icons type="compose" size="20" color="#FFFFFF"></uni-icons>
|
||||
<text>预定</text>
|
||||
</view>
|
||||
<view class="action-btn primary" @click="addRental">
|
||||
<uni-icons type="plus" size="20" color="#FFFFFF"></uni-icons>
|
||||
<text>办理入住</text>
|
||||
</view>
|
||||
</template>
|
||||
<!-- 预定状态:显示取消预定和办理入住按钮 -->
|
||||
<template v-if="room.status === 'reserved'">
|
||||
<view class="action-btn warning" @click="cancelReserve">
|
||||
<uni-icons type="close" size="20" color="#FFFFFF"></uni-icons>
|
||||
<text>取消预定</text>
|
||||
</view>
|
||||
<view class="action-btn primary" @click="addRental">
|
||||
<uni-icons type="plus" size="20" color="#FFFFFF"></uni-icons>
|
||||
<text>办理入住</text>
|
||||
</view>
|
||||
</template>
|
||||
<!-- 已租状态:查看租赁 -->
|
||||
<view class="action-btn primary" v-if="room.status === 'rented'" @click="viewRentalDetail">
|
||||
<uni-icons type="eye" size="20" color="#FFFFFF"></uni-icons>
|
||||
<text>查看租赁</text>
|
||||
</view>
|
||||
<!-- 删除房间按钮 -->
|
||||
<view class="action-btn danger" @click="deleteRoom">
|
||||
<uni-icons type="trash" size="20" color="#FFFFFF"></uni-icons>
|
||||
<text>删除房间</text>
|
||||
|
|
@ -108,20 +162,25 @@
|
|||
this.room = {
|
||||
id: data.id,
|
||||
roomNumber: data.roomNumber,
|
||||
floor: data.floor,
|
||||
roomType: data.roomType,
|
||||
area: data.area,
|
||||
orientation: data.orientation,
|
||||
rent: data.rent,
|
||||
monthlyPrice: data.monthlyPrice,
|
||||
yearlyPrice: data.yearlyPrice,
|
||||
deposit: data.deposit,
|
||||
status: data.status || 'vacant',
|
||||
statusText: data.status === 'rented' ? '已出租' : '空置中',
|
||||
apartmentName: data.Apartment?.name || '--',
|
||||
statusText: this.getRoomStatusText(data.status),
|
||||
rentalStatus: data.rentalStatus,
|
||||
rentalStatusText: this.getRentalStatusText(data.rentalStatus),
|
||||
apartmentName: data.Apartment && data.Apartment.name || '--',
|
||||
rental: data.Rental ? {
|
||||
id: data.Rental.id,
|
||||
renterName: data.Rental.Renter?.name,
|
||||
renterName: data.Rental.Renter && data.Rental.Renter.name,
|
||||
startDate: data.Rental.startDate,
|
||||
endDate: data.Rental.endDate,
|
||||
rent: data.Rental.rent,
|
||||
status: data.Rental.status,
|
||||
statusText: this.getStatusText(data.Rental.status)
|
||||
statusText: this.getRentalStatusText(data.Rental.status)
|
||||
} : null
|
||||
}
|
||||
}
|
||||
|
|
@ -138,6 +197,46 @@
|
|||
addRental() {
|
||||
uni.navigateTo({ url: `/pages/rental-add/rental-add?roomId=${this.roomId}` })
|
||||
},
|
||||
// 预定房间
|
||||
async reserveRoom() {
|
||||
uni.showModal({
|
||||
title: '确认预定',
|
||||
content: '确定要预定这个房间吗?',
|
||||
confirmColor: '#2563EB',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
await roomApi.update(this.roomId, { status: 'reserved' })
|
||||
uni.showToast({ title: '预定成功', icon: 'success' })
|
||||
this.loadRoomDetail(this.roomId)
|
||||
} catch (error) {
|
||||
uni.showToast({ title: '预定失败', icon: 'none' })
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
// 取消预定
|
||||
async cancelReserve() {
|
||||
uni.showModal({
|
||||
title: '确认取消',
|
||||
content: '确定要取消预定吗?',
|
||||
confirmColor: '#EF4444',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
await roomApi.update(this.roomId, { status: 'empty' })
|
||||
uni.showToast({ title: '取消预定成功', icon: 'success' })
|
||||
this.loadRoomDetail(this.roomId)
|
||||
} catch (error) {
|
||||
uni.showToast({ title: '取消预定失败', icon: 'none' })
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
viewRentalDetail() {
|
||||
if (this.room.rental) {
|
||||
uni.navigateTo({ url: `/pages/rental-detail/rental-detail?id=${this.room.rental.id}` })
|
||||
|
|
@ -164,8 +263,12 @@
|
|||
goBack() {
|
||||
uni.navigateBack()
|
||||
},
|
||||
getStatusText(status) {
|
||||
const map = { active: '在租', expired: '已到期', terminated: '已终止' }
|
||||
getRoomStatusText(status) {
|
||||
const map = { empty: '空房', reserved: '预定', rented: '在租' }
|
||||
return map[status] || status
|
||||
},
|
||||
getRentalStatusText(status) {
|
||||
const map = { normal: '正常', soon_expire: '即将到期', expired: '已到期' }
|
||||
return map[status] || status
|
||||
}
|
||||
}
|
||||
|
|
@ -324,6 +427,10 @@
|
|||
background: linear-gradient(135deg, #2563EB 0%, #1D4ED8 100%);
|
||||
box-shadow: 0 8rpx 24rpx rgba(37, 99, 235, 0.3);
|
||||
}
|
||||
.action-btn.warning {
|
||||
background: linear-gradient(135deg, #F59E0B 0%, #D97706 100%);
|
||||
box-shadow: 0 8rpx 24rpx rgba(245, 158, 11, 0.3);
|
||||
}
|
||||
.action-btn.danger {
|
||||
background: linear-gradient(135deg, #EF4444 0%, #DC2626 100%);
|
||||
box-shadow: 0 8rpx 24rpx rgba(239, 68, 68, 0.3);
|
||||
|
|
|
|||
|
|
@ -32,10 +32,10 @@
|
|||
</view>
|
||||
|
||||
<view v-else class="room-grid">
|
||||
<view v-for="(item, index) in rooms" :key="index" class="room-card" @click="viewRoomDetail(item)">
|
||||
<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" :class="item.status">{{item.statusText}}</view>
|
||||
<view class="room-status">{{item.statusText}}</view>
|
||||
</view>
|
||||
<view class="room-info">
|
||||
<text class="info-item">面积: {{item.area || '--'}} m²</text>
|
||||
|
|
@ -97,13 +97,13 @@
|
|||
methods: {
|
||||
async loadApartments() {
|
||||
try {
|
||||
const res = await apartmentApi.getList({ pageSize: 999 })
|
||||
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
|
||||
}
|
||||
this.loadRooms()
|
||||
// 注意:loadRooms 由 onShow 触发,避免重复请求
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载公寓列表失败:', error)
|
||||
|
|
@ -119,23 +119,24 @@
|
|||
apartmentId: this.selectedApartmentId
|
||||
}
|
||||
const res = await roomApi.getList(params)
|
||||
if (res.data) {
|
||||
const list = res.data.map(item => ({
|
||||
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: item.status || 'vacant',
|
||||
statusText: item.status === 'rented' ? '已出租' : '空置中',
|
||||
renterName: item.Renter?.name
|
||||
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.total || 0) ? 'more' : 'noMore'
|
||||
this.loadStatus = this.rooms.length < (res.data.total || 0) ? 'more' : 'noMore'
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载房间列表失败:', error)
|
||||
|
|
@ -173,6 +174,32 @@
|
|||
},
|
||||
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 ''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -249,11 +276,12 @@
|
|||
.empty-btn {
|
||||
background: linear-gradient(135deg, #2563EB 0%, #1D4ED8 100%);
|
||||
color: #FFFFFF;
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
padding: 24rpx 60rpx;
|
||||
border-radius: 16rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
padding: 16rpx 40rpx;
|
||||
border-radius: 12rpx;
|
||||
border: none;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.room-grid {
|
||||
display: grid;
|
||||
|
|
@ -262,9 +290,37 @@
|
|||
}
|
||||
.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;
|
||||
|
|
@ -279,16 +335,11 @@
|
|||
}
|
||||
.room-status {
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: 6rpx;
|
||||
border-radius: 20rpx;
|
||||
font-size: 20rpx;
|
||||
}
|
||||
.room-status.vacant {
|
||||
background: #D1FAE5;
|
||||
color: #059669;
|
||||
}
|
||||
.room-status.rented {
|
||||
background: #DBEAFE;
|
||||
color: #2563EB;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
color: #606266;
|
||||
font-weight: 500;
|
||||
}
|
||||
.room-info {
|
||||
margin-bottom: 12rpx;
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@
|
|||
<view class="overview-row">
|
||||
<view class="overview-card pending">
|
||||
<view class="card-icon">
|
||||
<uni-icons type="time-filled" size="28" color="#FFFFFF"></uni-icons>
|
||||
<uni-icons type="refresh-filled" size="28" color="#FFFFFF"></uni-icons>
|
||||
</view>
|
||||
<view class="card-content">
|
||||
<text class="card-value">¥{{formatAmount(statistics.totalUnreceived)}}</text>
|
||||
|
|
@ -226,13 +226,37 @@
|
|||
<text>暂无明细数据</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="hasMore" class="load-more" @click="loadMore">
|
||||
<text>加载更多</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="safe-area-bottom" style="height: 40rpx;"></view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 自定义日期选择弹窗 -->
|
||||
<view class="custom-date-mask" v-if="showCustomDatePicker" @click="cancelCustomDate">
|
||||
<view class="date-picker-popup" @click.stop>
|
||||
<view class="popup-header">
|
||||
<text class="popup-title">选择日期范围</text>
|
||||
<view class="popup-close" @click="cancelCustomDate">
|
||||
<uni-icons type="close" size="20" color="#64748B"></uni-icons>
|
||||
</view>
|
||||
</view>
|
||||
<view class="popup-content">
|
||||
<uni-datetime-picker
|
||||
v-model="tempDateRange"
|
||||
type="daterange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
:border="false"
|
||||
:clear-icon="false"
|
||||
/>
|
||||
</view>
|
||||
<view class="popup-footer">
|
||||
<view class="btn btn-cancel" @click="cancelCustomDate">取消</view>
|
||||
<view class="btn btn-confirm" @click="confirmCustomDate">确定</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
|
|
@ -262,9 +286,9 @@
|
|||
],
|
||||
categoryMap: {},
|
||||
isLoading: false,
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
hasMore: true
|
||||
// 自定义日期选择
|
||||
showCustomDatePicker: false,
|
||||
tempDateRange: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -278,8 +302,6 @@
|
|||
this.loadData()
|
||||
},
|
||||
onPullDownRefresh() {
|
||||
this.page = 1
|
||||
this.hasMore = true
|
||||
this.loadData().finally(() => {
|
||||
uni.stopPullDownRefresh()
|
||||
})
|
||||
|
|
@ -317,15 +339,23 @@
|
|||
this.isLoading = true
|
||||
|
||||
try {
|
||||
console.log('开始加载数据,日期范围:', this.dateRange)
|
||||
const billRes = await billApi.list({
|
||||
startDate: this.dateRange[0],
|
||||
endDate: this.dateRange[1],
|
||||
page: this.page,
|
||||
pageSize: this.pageSize
|
||||
endDate: this.dateRange[1]
|
||||
})
|
||||
console.log('API返回结果:', billRes)
|
||||
|
||||
this.processData(billRes.data || [])
|
||||
if (billRes.code === 200) {
|
||||
this.processData(billRes.data || [])
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: billRes.message || '加载数据失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载数据失败:', error)
|
||||
uni.showToast({
|
||||
title: '加载数据失败',
|
||||
icon: 'none'
|
||||
|
|
@ -432,15 +462,8 @@
|
|||
// 处理趋势数据
|
||||
this.processTrendData(trendData)
|
||||
|
||||
if (this.page === 1) {
|
||||
this.incomeList = incomeList
|
||||
this.expenseList = expenseList
|
||||
} else {
|
||||
this.incomeList = [...this.incomeList, ...incomeList]
|
||||
this.expenseList = [...this.expenseList, ...expenseList]
|
||||
}
|
||||
|
||||
this.hasMore = bills.length >= this.pageSize
|
||||
this.incomeList = incomeList
|
||||
this.expenseList = expenseList
|
||||
},
|
||||
|
||||
// 处理趋势数据
|
||||
|
|
@ -468,9 +491,15 @@
|
|||
uni.showActionSheet({
|
||||
itemList: ['本周', '本月', '本季度', '本年', '自定义'],
|
||||
success: (res) => {
|
||||
const periods = ['本周', '本月', '本季度', '本年', '自定义']
|
||||
this.currentPeriod = periods[res.tapIndex]
|
||||
this.updateDateRange(res.tapIndex)
|
||||
if (res.tapIndex === 4) {
|
||||
// 自定义日期
|
||||
this.showCustomDatePicker = true
|
||||
this.tempDateRange = this.dateRange
|
||||
} else {
|
||||
const periods = ['本周', '本月', '本季度', '本年', '自定义']
|
||||
this.currentPeriod = periods[res.tapIndex]
|
||||
this.updateDateRange(res.tapIndex)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
|
@ -504,22 +533,38 @@
|
|||
}
|
||||
|
||||
this.dateRange = [this.formatDate(start), this.formatDate(end)]
|
||||
this.page = 1
|
||||
this.hasMore = true
|
||||
this.loadData()
|
||||
},
|
||||
|
||||
// 确认自定义日期
|
||||
confirmCustomDate() {
|
||||
if (this.tempDateRange && this.tempDateRange.length === 2) {
|
||||
this.dateRange = this.tempDateRange
|
||||
this.currentPeriod = `${this.tempDateRange[0]} 至 ${this.tempDateRange[1]}`
|
||||
this.showCustomDatePicker = false
|
||||
this.loadData()
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: '请选择开始和结束日期',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
// 取消自定义日期选择
|
||||
cancelCustomDate() {
|
||||
this.showCustomDatePicker = false
|
||||
this.tempDateRange = []
|
||||
},
|
||||
|
||||
// 切换明细标签
|
||||
switchTab(value) {
|
||||
this.activeTab = value
|
||||
},
|
||||
|
||||
// 加载更多
|
||||
// 加载更多(已移除分页,此方法保留但为空)
|
||||
loadMore() {
|
||||
if (this.hasMore && !this.isLoading) {
|
||||
this.page++
|
||||
this.loadData()
|
||||
}
|
||||
// 不再使用分页加载
|
||||
},
|
||||
|
||||
// 查看详情
|
||||
|
|
@ -1074,4 +1119,79 @@
|
|||
color: #64748B;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
/* 日期选择弹窗 */
|
||||
.custom-date-mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.date-picker-popup {
|
||||
background: #FFFFFF;
|
||||
border-radius: 24rpx;
|
||||
width: 600rpx;
|
||||
overflow: hidden;
|
||||
margin: 0 40rpx;
|
||||
}
|
||||
|
||||
.popup-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 32rpx;
|
||||
border-bottom: 2rpx solid #F1F5F9;
|
||||
}
|
||||
|
||||
.popup-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #1E293B;
|
||||
}
|
||||
|
||||
.popup-close {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 24rpx;
|
||||
background: #F8FAFC;
|
||||
}
|
||||
|
||||
.popup-content {
|
||||
padding: 48rpx 32rpx;
|
||||
}
|
||||
|
||||
.popup-footer {
|
||||
display: flex;
|
||||
padding: 24rpx 32rpx;
|
||||
gap: 24rpx;
|
||||
border-top: 2rpx solid #F1F5F9;
|
||||
}
|
||||
|
||||
.btn {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 24rpx 0;
|
||||
border-radius: 12rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.btn-cancel {
|
||||
background: #F1F5F9;
|
||||
color: #64748B;
|
||||
}
|
||||
|
||||
.btn-confirm {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: #FFFFFF;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -70,18 +70,19 @@ const getHeaders = (needAuth = true) => {
|
|||
|
||||
/**
|
||||
* 处理响应错误
|
||||
* 优先使用后端返回的 message
|
||||
* 根据后端统一响应格式处理错误
|
||||
* 后端格式: { code: 400/401/403/404/500, message: 'xxx', data: null }
|
||||
*/
|
||||
const handleResponseError = (response) => {
|
||||
const { statusCode: code, data } = response
|
||||
const { statusCode: httpCode, data } = response
|
||||
|
||||
// 优先使用后端返回的 message
|
||||
const message = data?.message
|
||||
const message = data?.message || '请求失败'
|
||||
|
||||
switch (code) {
|
||||
switch (httpCode) {
|
||||
case 400:
|
||||
uni.showToast({
|
||||
title: message || '请求参数错误',
|
||||
title: message,
|
||||
icon: 'none'
|
||||
})
|
||||
break
|
||||
|
|
@ -91,7 +92,7 @@ const handleResponseError = (response) => {
|
|||
uni.removeStorageSync(getStorageKey('token'))
|
||||
uni.removeStorageSync(getStorageKey('userInfo'))
|
||||
uni.showToast({
|
||||
title: message || '登录已过期,请重新登录',
|
||||
title: message,
|
||||
icon: 'none'
|
||||
})
|
||||
setTimeout(() => {
|
||||
|
|
@ -103,28 +104,28 @@ const handleResponseError = (response) => {
|
|||
|
||||
case 403:
|
||||
uni.showToast({
|
||||
title: message || '没有权限执行此操作',
|
||||
title: message,
|
||||
icon: 'none'
|
||||
})
|
||||
break
|
||||
|
||||
case 404:
|
||||
uni.showToast({
|
||||
title: message || '请求的资源不存在',
|
||||
title: message,
|
||||
icon: 'none'
|
||||
})
|
||||
break
|
||||
|
||||
case 500:
|
||||
uni.showToast({
|
||||
title: message || '服务器错误,请稍后重试',
|
||||
title: message,
|
||||
icon: 'none'
|
||||
})
|
||||
break
|
||||
|
||||
default:
|
||||
uni.showToast({
|
||||
title: message || '请求失败',
|
||||
title: message,
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
|
|
@ -160,23 +161,28 @@ const request = (options = {}, needAuth = true) => {
|
|||
uni.request({
|
||||
...config,
|
||||
success: (response) => {
|
||||
const { statusCode: code, data } = response
|
||||
const { statusCode: httpCode, data } = response
|
||||
|
||||
// 请求成功
|
||||
if (code >= 200 && code < 300) {
|
||||
// 如果后端返回的数据没有 code 字段,直接视为成功
|
||||
if (!data.code || data.code === 200) {
|
||||
// HTTP 请求成功 (2xx)
|
||||
if (httpCode >= 200 && httpCode < 300) {
|
||||
// 根据后端统一响应格式处理
|
||||
// 后端格式: { code: 200, message: 'xxx', data: xxx }
|
||||
|
||||
// 业务逻辑成功
|
||||
if (data.code === 200) {
|
||||
resolve(data)
|
||||
} else {
|
||||
// 业务逻辑失败
|
||||
// 业务逻辑失败(如参数错误等)
|
||||
// 后端格式: { code: 400/403/404/500, message: 'xxx', data: null }
|
||||
const errorMessage = data.message || '操作失败'
|
||||
uni.showToast({
|
||||
title: data.message || '操作失败',
|
||||
title: errorMessage,
|
||||
icon: 'none'
|
||||
})
|
||||
reject(data)
|
||||
}
|
||||
} else {
|
||||
// HTTP错误
|
||||
// HTTP 错误(4xx, 5xx)
|
||||
handleResponseError(response)
|
||||
reject(response)
|
||||
}
|
||||
|
|
@ -262,11 +268,13 @@ export const upload = (url, filePath, formData = {}, needAuth = true) => {
|
|||
header: needAuth && token ? { 'Authorization': `Bearer ${token}` } : {},
|
||||
success: (response) => {
|
||||
const data = JSON.parse(response.data)
|
||||
// 根据后端统一响应格式处理
|
||||
if (data.code === 200) {
|
||||
resolve(data)
|
||||
} else {
|
||||
const errorMessage = data.message || '上传失败'
|
||||
uni.showToast({
|
||||
title: data.message || '上传失败',
|
||||
title: errorMessage,
|
||||
icon: 'none'
|
||||
})
|
||||
reject(data)
|
||||
|
|
|
|||
Loading…
Reference in New Issue