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

988 lines
22 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>