rentease-app/pages/rental-add/rental-add.vue

988 lines
22 KiB
Vue
Raw Normal View History

2026-04-20 06:23:11 +00:00
<template>
<view class="rental-add-page">
<!-- 自定义导航栏 -->
<view class="custom-nav safe-area-top">
<view class="nav-content">
<view class="nav-btn" @click="goBack">
<uni-icons type="left" size="22" color="#1E293B"></uni-icons>
</view>
<text class="nav-title">{{isEdit ? '编辑租赁' : '新增租赁'}}</text>
<view class="nav-btn" @click="saveRental">
<text class="save-text">保存</text>
</view>
</view>
</view>
<!-- 表单内容 -->
<scroll-view scroll-y class="form-content">
<!-- 步骤指示器 -->
<view class="step-indicator">
<view class="step-item" :class="{ active: currentStep >= 1 }">
<view class="step-num">1</view>
<text class="step-text">选择房源</text>
</view>
<view class="step-line" :class="{ active: currentStep >= 2 }"></view>
<view class="step-item" :class="{ active: currentStep >= 2 }">
<view class="step-num">2</view>
<text class="step-text">选择租客</text>
</view>
<view class="step-line" :class="{ active: currentStep >= 3 }"></view>
<view class="step-item" :class="{ active: currentStep >= 3 }">
<view class="step-num">3</view>
<text class="step-text">租赁信息</text>
</view>
</view>
<!-- 步骤1选择房源 -->
<view v-if="currentStep === 1" class="step-content">
<view class="section-title">选择公寓</view>
<view class="select-list">
<view
v-for="item in apartments"
:key="item.id"
class="select-item"
:class="{ active: form.apartmentId === item.id }"
@click="selectApartment(item)"
>
<text class="item-name">{{item.name}}</text>
<text class="item-address">{{item.address}}</text>
<uni-icons v-if="form.apartmentId === item.id" type="checkmarkempty" size="20" color="#2563EB"></uni-icons>
</view>
</view>
<view class="section-title" v-if="form.apartmentId">选择房间</view>
<view class="select-list" v-if="form.apartmentId">
<view
v-for="item in rooms"
:key="item.id"
class="select-item"
:class="{ active: form.roomId === item.id, disabled: item.status === 'rented' }"
@click="selectRoom(item)"
>
<view class="item-info">
<text class="item-name">{{item.roomNumber}}</text>
<text class="item-status" :class="item.status">{{item.status === 'rented' ? '已出租' : '空置中'}}</text>
</view>
<uni-icons v-if="form.roomId === item.id" type="checkmarkempty" size="20" color="#2563EB"></uni-icons>
</view>
<view v-if="rooms.length === 0" class="empty-tip">该公寓暂无房间</view>
</view>
<button class="btn-next" :disabled="!form.roomId" @click="nextStep">下一步</button>
</view>
<!-- 步骤2选择租客 -->
<view v-if="currentStep === 2" class="step-content">
<view class="section-title">选择租客</view>
<view class="search-bar">
<uni-icons type="search" size="18" color="#94A3B8"></uni-icons>
<input
class="search-input"
type="text"
placeholder="搜索租客姓名或电话"
v-model="renterSearchKeyword"
@input="searchRenters"
/>
</view>
<view class="select-list">
<view
v-for="item in renters"
:key="item.id"
class="select-item renter-item"
:class="{ active: form.renterId === item.id }"
@click="selectRenter(item)"
>
<view class="renter-avatar">
<text>{{item.name.charAt(0)}}</text>
</view>
<view class="renter-info">
<text class="renter-name">{{item.name}}</text>
<text class="renter-phone">{{item.phone}}</text>
</view>
<uni-icons v-if="form.renterId === item.id" type="checkmarkempty" size="20" color="#2563EB"></uni-icons>
</view>
</view>
<view class="divider">
<text></text>
</view>
<button class="btn-add-renter" @click="showAddRenter = true">
<uni-icons type="plusempty" size="18" color="#2563EB"></uni-icons>
<text>添加新租客</text>
</button>
<view class="btn-group">
<button class="btn-prev" @click="prevStep">上一步</button>
<button class="btn-next" :disabled="!form.renterId && !isNewRenter" @click="nextStep">下一步</button>
</view>
</view>
<!-- 步骤3租赁信息 -->
<view v-if="currentStep === 3" class="step-content">
<view class="section-title">租赁信息</view>
<view class="form-card">
<view class="form-item">
<text class="form-label">付租方式 <text class="required">*</text></text>
<picker mode="selector" :range="paymentTypeOptions" :value="paymentTypeIndex" @change="onPaymentTypeChange">
<view class="picker-value">
<text>{{form.paymentTypeText}}</text>
<uni-icons type="arrowright" size="16" color="#94A3B8"></uni-icons>
</view>
</picker>
</view>
<view class="form-item">
<text class="form-label">开始日期 <text class="required">*</text></text>
<picker mode="date" :value="form.startDate" @change="onStartDateChange">
<view class="picker-value">
<text>{{form.startDate || '请选择开始日期'}}</text>
<uni-icons type="arrowright" size="16" color="#94A3B8"></uni-icons>
</view>
</picker>
</view>
<view class="form-item">
<text class="form-label">结束日期 <text class="required">*</text></text>
<picker mode="date" :value="form.endDate" @change="onEndDateChange">
<view class="picker-value">
<text>{{form.endDate || '请选择结束日期'}}</text>
<uni-icons type="arrowright" size="16" color="#94A3B8"></uni-icons>
</view>
</picker>
</view>
<view class="form-item">
<text class="form-label">月租金 <text class="required">*</text></text>
<input
type="digit"
v-model="form.rent"
placeholder="请输入月租金"
class="form-input"
/>
</view>
<view class="form-item">
<text class="form-label">押金</text>
<input
type="digit"
v-model="form.deposit"
placeholder="请输入押金金额"
class="form-input"
/>
</view>
<view class="form-item">
<text class="form-label">水表起始读数</text>
<input
type="digit"
v-model="form.waterMeterStart"
placeholder="请输入水表起始读数"
class="form-input"
/>
</view>
<view class="form-item">
<text class="form-label">电表起始读数</text>
<input
type="digit"
v-model="form.electricityMeterStart"
placeholder="请输入电表起始读数"
class="form-input"
/>
</view>
<view class="form-item">
<text class="form-label">备注</text>
<textarea
v-model="form.remark"
placeholder="请输入备注(选填)"
class="form-textarea"
/>
</view>
</view>
<view class="btn-group">
<button class="btn-prev" @click="prevStep">上一步</button>
<button class="btn-submit" :loading="submitLoading" @click="submitForm">确认提交</button>
</view>
</view>
<view class="safe-area-bottom" style="height: 40rpx;"></view>
</scroll-view>
<!-- 添加新租客弹窗 -->
<uni-popup ref="addRenterPopup" type="center">
<view class="popup-content">
<view class="popup-header">
<text class="popup-title">添加新租客</text>
<uni-icons type="close" size="20" color="#94A3B8" @click="closeAddRenterPopup"></uni-icons>
</view>
<view class="popup-body">
<view class="form-item">
<text class="form-label">姓名 <text class="required">*</text></text>
<input
type="text"
v-model="newRenter.name"
placeholder="请输入租客姓名"
class="form-input"
/>
</view>
<view class="form-item">
<text class="form-label">电话 <text class="required">*</text></text>
<input
type="number"
v-model="newRenter.phone"
placeholder="请输入租客电话"
class="form-input"
maxlength="11"
/>
</view>
<view class="form-item">
<text class="form-label">身份证号</text>
<input
type="idcard"
v-model="newRenter.idCard"
placeholder="请输入身份证号(选填)"
class="form-input"
maxlength="18"
/>
</view>
</view>
<view class="popup-footer">
<button class="btn-cancel" @click="closeAddRenterPopup">取消</button>
<button class="btn-confirm" @click="confirmAddRenter">确认添加</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'
import renterApi from '@/api/renter.js'
export default {
data() {
return {
isEdit: false,
rentalId: null,
currentStep: 1,
submitLoading: false,
apartments: [],
rooms: [],
renters: [],
renterSearchKeyword: '',
isNewRenter: false,
form: {
apartmentId: '',
roomId: '',
renterId: '',
paymentType: 'monthly',
paymentTypeText: '月租',
startDate: '',
endDate: '',
rent: '',
deposit: '',
waterMeterStart: '',
electricityMeterStart: '',
remark: ''
},
newRenter: {
name: '',
phone: '',
idCard: ''
},
paymentTypeOptions: ['月租', '季租', '半年租', '年租'],
paymentTypeMap: {
'月租': 'monthly',
'季租': 'quarterly',
'半年租': 'half_year',
'年租': 'yearly'
}
}
},
computed: {
paymentTypeIndex() {
return this.paymentTypeOptions.indexOf(this.form.paymentTypeText)
}
},
onLoad(options) {
this.loadApartments()
if (options.id) {
this.isEdit = true
this.rentalId = options.id
this.loadRentalDetail(options.id)
} else {
// 设置默认开始日期为今天
this.form.startDate = this.formatDate(new Date())
}
},
methods: {
async loadApartments() {
try {
const res = await apartmentApi.getList({ pageSize: 999 })
if (res.data) {
this.apartments = res.data
}
} catch (error) {
console.error('加载公寓列表失败:', error)
}
},
async loadRooms(apartmentId) {
try {
const res = await roomApi.getList({ apartmentId, pageSize: 999 })
if (res.data) {
this.rooms = res.data
}
} catch (error) {
console.error('加载房间列表失败:', error)
}
},
async loadRenters() {
try {
const res = await renterApi.getOptions({ keyword: this.renterSearchKeyword })
if (res.data) {
this.renters = res.data
}
} catch (error) {
console.error('加载租客列表失败:', error)
}
},
async loadRentalDetail(id) {
try {
const res = await rentalApi.getDetail(id)
if (res.data) {
const data = res.data
this.form = {
apartmentId: data.Room?.Apartment?.id,
roomId: data.roomId,
renterId: data.renterId,
paymentType: data.paymentType,
paymentTypeText: this.getPaymentTypeText(data.paymentType),
startDate: data.startDate,
endDate: data.endDate,
rent: String(data.rent),
deposit: String(data.deposit || ''),
waterMeterStart: String(data.waterMeterStart || ''),
electricityMeterStart: String(data.electricityMeterStart || ''),
remark: data.remark || ''
}
this.loadRooms(this.form.apartmentId)
}
} catch (error) {
console.error('加载租赁详情失败:', error)
}
},
selectApartment(item) {
this.form.apartmentId = item.id
this.form.roomId = ''
this.loadRooms(item.id)
},
selectRoom(item) {
if (item.status === 'rented') {
uni.showToast({ title: '该房间已出租', icon: 'none' })
return
}
this.form.roomId = item.id
},
selectRenter(item) {
this.form.renterId = item.id
this.isNewRenter = false
},
searchRenters() {
this.loadRenters()
},
nextStep() {
if (this.currentStep < 3) {
this.currentStep++
if (this.currentStep === 2) {
this.loadRenters()
}
}
},
prevStep() {
if (this.currentStep > 1) {
this.currentStep--
}
},
onPaymentTypeChange(e) {
const text = this.paymentTypeOptions[e.detail.value]
this.form.paymentTypeText = text
this.form.paymentType = this.paymentTypeMap[text]
},
onStartDateChange(e) {
this.form.startDate = e.detail.value
},
onEndDateChange(e) {
this.form.endDate = e.detail.value
},
showAddRenterPopup() {
this.$refs.addRenterPopup.open()
},
closeAddRenterPopup() {
this.$refs.addRenterPopup.close()
this.newRenter = { name: '', phone: '', idCard: '' }
},
confirmAddRenter() {
if (!this.newRenter.name.trim()) {
uni.showToast({ title: '请输入租客姓名', icon: 'none' })
return
}
if (!this.newRenter.phone.trim()) {
uni.showToast({ title: '请输入租客电话', icon: 'none' })
return
}
this.isNewRenter = true
this.form.renterId = ''
this.closeAddRenterPopup()
uni.showToast({ title: '新租客信息已保存', icon: 'success' })
},
async submitForm() {
// 表单验证
if (!this.form.roomId) {
uni.showToast({ title: '请选择房间', icon: 'none' })
return
}
if (!this.form.renterId && !this.isNewRenter) {
uni.showToast({ title: '请选择或添加租客', icon: 'none' })
return
}
if (!this.form.startDate) {
uni.showToast({ title: '请选择开始日期', icon: 'none' })
return
}
if (!this.form.endDate) {
uni.showToast({ title: '请选择结束日期', icon: 'none' })
return
}
if (!this.form.rent) {
uni.showToast({ title: '请输入月租金', icon: 'none' })
return
}
this.submitLoading = true
try {
const data = {
roomId: this.form.roomId,
paymentType: this.form.paymentType,
startDate: this.form.startDate,
endDate: this.form.endDate,
rent: parseFloat(this.form.rent),
deposit: parseFloat(this.form.deposit) || 0,
waterMeterStart: this.form.waterMeterStart ? parseFloat(this.form.waterMeterStart) : null,
electricityMeterStart: this.form.electricityMeterStart ? parseFloat(this.form.electricityMeterStart) : null,
remark: this.form.remark
}
if (this.isNewRenter) {
// 创建租赁同时创建租客
data.renter = this.newRenter
await rentalApi.createWithRenter(data)
} else {
data.renterId = this.form.renterId
if (this.isEdit) {
await rentalApi.update(this.rentalId, data)
} else {
await rentalApi.create(data)
}
}
uni.showToast({
title: this.isEdit ? '更新成功' : '创建成功',
icon: 'success'
})
setTimeout(() => {
uni.navigateBack()
}, 1500)
} catch (error) {
console.error('保存失败:', error)
uni.showToast({
title: this.isEdit ? '更新失败' : '创建失败',
icon: 'none'
})
} finally {
this.submitLoading = false
}
},
goBack() {
uni.navigateBack()
},
getPaymentTypeText(type) {
const map = {
monthly: '月租',
quarterly: '季租',
half_year: '半年租',
yearly: '年租'
}
return map[type] || type
},
formatDate(date) {
const d = new Date(date)
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
}
}
}
</script>
<style scoped>
.rental-add-page {
min-height: 100vh;
background: #F8FAFC;
display: flex;
flex-direction: column;
}
/* 自定义导航栏 */
.custom-nav {
background: #FFFFFF;
border-bottom: 2rpx solid #F1F5F9;
}
.nav-content {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20rpx 32rpx;
}
.nav-btn {
width: 72rpx;
height: 72rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
}
.nav-btn:active {
background: #F1F5F9;
}
.nav-title {
font-size: 34rpx;
font-weight: 600;
color: #1E293B;
}
.save-text {
font-size: 28rpx;
color: #2563EB;
font-weight: 600;
}
/* 表单内容 */
.form-content {
flex: 1;
padding: 24rpx 32rpx;
}
/* 步骤指示器 */
.step-indicator {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 40rpx;
padding: 24rpx;
background: #FFFFFF;
border-radius: 16rpx;
}
.step-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 8rpx;
}
.step-num {
width: 48rpx;
height: 48rpx;
border-radius: 50%;
background: #E2E8F0;
color: #64748B;
font-size: 24rpx;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
}
.step-item.active .step-num {
background: #2563EB;
color: #FFFFFF;
}
.step-text {
font-size: 22rpx;
color: #64748B;
}
.step-item.active .step-text {
color: #2563EB;
font-weight: 500;
}
.step-line {
width: 80rpx;
height: 2rpx;
background: #E2E8F0;
margin: 0 16rpx;
margin-bottom: 30rpx;
}
.step-line.active {
background: #2563EB;
}
/* 步骤内容 */
.step-content {
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateX(20rpx); }
to { opacity: 1; transform: translateX(0); }
}
/* 标题 */
.section-title {
font-size: 28rpx;
font-weight: 600;
color: #1E293B;
margin-bottom: 16rpx;
}
/* 选择列表 */
.select-list {
background: #FFFFFF;
border-radius: 16rpx;
overflow: hidden;
margin-bottom: 24rpx;
}
.select-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24rpx;
border-bottom: 2rpx solid #F1F5F9;
}
.select-item:last-child {
border-bottom: none;
}
.select-item.active {
background: #EFF6FF;
}
.select-item.disabled {
opacity: 0.5;
}
.item-name {
font-size: 28rpx;
color: #1E293B;
font-weight: 500;
}
.item-address {
font-size: 24rpx;
color: #94A3B8;
margin-top: 4rpx;
}
.item-info {
display: flex;
align-items: center;
gap: 16rpx;
}
.item-status {
font-size: 22rpx;
padding: 4rpx 12rpx;
border-radius: 6rpx;
}
.item-status.vacant {
background: #D1FAE5;
color: #059669;
}
.item-status.rented {
background: #FEE2E2;
color: #DC2626;
}
.empty-tip {
padding: 48rpx;
text-align: center;
font-size: 26rpx;
color: #94A3B8;
}
/* 租客项 */
.renter-item {
padding: 20rpx 24rpx;
}
.renter-avatar {
width: 64rpx;
height: 64rpx;
border-radius: 50%;
background: linear-gradient(135deg, #2563EB 0%, #1D4ED8 100%);
display: flex;
align-items: center;
justify-content: center;
}
.renter-avatar text {
font-size: 24rpx;
font-weight: 600;
color: #FFFFFF;
}
.renter-info {
flex: 1;
display: flex;
flex-direction: column;
}
.renter-name {
font-size: 28rpx;
color: #1E293B;
font-weight: 500;
}
.renter-phone {
font-size: 24rpx;
color: #64748B;
}
/* 搜索栏 */
.search-bar {
display: flex;
align-items: center;
background: #FFFFFF;
border-radius: 16rpx;
padding: 20rpx 24rpx;
margin-bottom: 24rpx;
gap: 16rpx;
}
.search-input {
flex: 1;
font-size: 28rpx;
color: #1E293B;
}
/* 分隔线 */
.divider {
display: flex;
align-items: center;
justify-content: center;
margin: 32rpx 0;
}
.divider::before,
.divider::after {
content: '';
flex: 1;
height: 2rpx;
background: #E2E8F0;
}
.divider text {
padding: 0 24rpx;
font-size: 24rpx;
color: #94A3B8;
}
/* 添加租客按钮 */
.btn-add-renter {
display: flex;
align-items: center;
justify-content: center;
gap: 12rpx;
background: #FFFFFF;
border: 2rpx dashed #2563EB;
border-radius: 16rpx;
padding: 28rpx;
margin-bottom: 40rpx;
}
.btn-add-renter text {
font-size: 28rpx;
color: #2563EB;
font-weight: 500;
}
/* 表单卡片 */
.form-card {
background: #FFFFFF;
border-radius: 16rpx;
padding: 0 24rpx;
margin-bottom: 40rpx;
}
.form-item {
padding: 24rpx 0;
border-bottom: 2rpx solid #F1F5F9;
}
.form-item:last-child {
border-bottom: none;
}
.form-label {
display: block;
font-size: 26rpx;
color: #64748B;
margin-bottom: 16rpx;
}
.required {
color: #EF4444;
}
.picker-value {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 30rpx;
color: #1E293B;
}
.form-input {
width: 100%;
font-size: 30rpx;
color: #1E293B;
}
.form-textarea {
width: 100%;
height: 160rpx;
font-size: 30rpx;
color: #1E293B;
}
/* 按钮 */
.btn-next {
background: linear-gradient(135deg, #2563EB 0%, #1D4ED8 100%);
color: #FFFFFF;
font-size: 30rpx;
font-weight: 600;
padding: 28rpx;
border-radius: 16rpx;
border: none;
}
.btn-next[disabled] {
opacity: 0.5;
}
.btn-group {
display: flex;
gap: 24rpx;
}
.btn-prev {
flex: 1;
background: #F3F4F6;
color: #64748B;
font-size: 30rpx;
font-weight: 600;
padding: 28rpx;
border-radius: 16rpx;
border: none;
}
.btn-submit {
flex: 2;
background: linear-gradient(135deg, #2563EB 0%, #1D4ED8 100%);
color: #FFFFFF;
font-size: 30rpx;
font-weight: 600;
padding: 28rpx;
border-radius: 16rpx;
border: none;
}
/* 弹窗 */
.popup-content {
background: #FFFFFF;
border-radius: 24rpx;
width: 600rpx;
}
.popup-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 32rpx;
border-bottom: 2rpx solid #F1F5F9;
}
.popup-title {
font-size: 32rpx;
font-weight: 700;
color: #1E293B;
}
.popup-body {
padding: 32rpx;
}
.popup-footer {
display: flex;
gap: 20rpx;
padding: 24rpx 32rpx;
border-top: 2rpx solid #F1F5F9;
}
.btn-cancel {
flex: 1;
height: 80rpx;
background: #F3F4F6;
border-radius: 12rpx;
font-size: 28rpx;
color: #64748B;
border: none;
}
.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>