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

878 lines
22 KiB
Vue
Raw Normal View History

2026-04-20 06:23:11 +00:00
<template>
<view class="add-record-page">
<!-- 自定义导航栏 -->
<view class="custom-nav safe-area-top">
<view class="nav-content">
<view class="nav-btn" @click="goBack">
<uni-icons type="left" size="22" color="#1E293B"></uni-icons>
</view>
<text class="nav-title">办理入住</text>
<view class="nav-btn" @click="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, completed: 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, completed: 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-panel">
<view class="panel-title">选择公寓</view>
<view class="apartment-list">
<view
v-for="apt in apartments"
:key="apt.id"
class="apartment-card"
:class="{ active: selectedApartmentId === apt.id }"
@click="selectApartment(apt.id)"
>
<view class="apt-header">
<text class="apt-name">{{apt.name}}</text>
<uni-icons v-if="selectedApartmentId === apt.id" type="checkmarkempty" size="20" color="#409EFF"></uni-icons>
</view>
<text class="apt-address">{{apt.address || '暂无地址'}}</text>
<view class="apt-stats">
<text>空房 {{apt.emptyCount || 0}}</text>
<text class="divider">|</text>
<text> {{apt.roomCount || 0}}</text>
</view>
</view>
</view>
<view class="panel-title" v-if="selectedApartmentId">选择房间</view>
<view class="room-grid" v-if="selectedApartmentId">
<view
v-for="room in availableRooms"
:key="room.id"
class="room-item"
:class="{ active: form.roomId === room.id }"
@click="selectRoom(room)"
>
<text class="room-num">{{room.roomNumber}}</text>
<text class="room-price">¥{{room.rent || 0}}/</text>
<text class="room-area" v-if="room.area">{{room.area}}m²</text>
</view>
<view v-if="availableRooms.length === 0" class="empty-tip">
<text>该公寓暂无空房</text>
</view>
</view>
<button class="btn-next" :disabled="!form.roomId" @click="nextStep">下一步</button>
</view>
<!-- 步骤2租客信息 -->
<view v-if="currentStep === 2" class="step-panel">
<view class="panel-title">租客信息</view>
<view class="form-card">
<view class="form-item">
<text class="form-label">租客姓名 <text class="required">*</text></text>
<input
type="text"
v-model="form.renterName"
placeholder="请输入租客姓名"
class="form-input"
maxlength="20"
/>
</view>
<view class="form-item">
<text class="form-label">联系电话</text>
<input
type="number"
v-model="form.renterPhone"
placeholder="请输入联系电话(选填)"
class="form-input"
maxlength="11"
/>
</view>
<view class="form-item">
<text class="form-label">身份证号</text>
<input
type="idcard"
v-model="form.renterIdCard"
placeholder="请输入身份证号(选填)"
class="form-input"
maxlength="18"
/>
</view>
</view>
<view class="btn-group">
<button class="btn-prev" @click="prevStep">上一步</button>
<button class="btn-next" :disabled="!form.renterName" @click="nextStep">下一步</button>
</view>
</view>
<!-- 步骤3合同信息 -->
<view v-if="currentStep === 3" class="step-panel">
<view class="panel-title">合同信息</view>
<view class="form-card">
<view class="form-item">
<text class="form-label">付租方式 <text class="required">*</text></text>
<view class="radio-group">
<view
v-for="type in paymentTypes"
:key="type.value"
class="radio-item"
:class="{ active: form.paymentType === type.value }"
@click="selectPaymentType(type.value)"
>
<text>{{type.label}}</text>
</view>
</view>
</view>
<view class="form-item">
<text class="form-label">租期 <text class="required">*</text></text>
<view class="period-selector">
<view class="period-options">
<view
v-for="period in periodOptions"
:key="period.value"
class="period-item"
:class="{ active: leasePeriod === period.value }"
@click="selectPeriod(period.value)"
>
<text>{{period.label}}</text>
</view>
</view>
</view>
</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>
<view class="static-value">{{form.endDate || '自动计算'}}</view>
</view>
<view class="form-item">
<text class="form-label">{{form.paymentType === 'monthly' ? '月租金' : '年租金'}} <text class="required">*</text></text>
<input
type="digit"
v-model="form.rent"
:placeholder="form.paymentType === 'monthly' ? '请输入月租金' : '请输入年租金'"
class="form-input"
/>
<text class="input-suffix"></text>
</view>
<view class="form-item">
<text class="form-label">押金</text>
<input
type="digit"
v-model="form.deposit"
placeholder="请输入押金金额"
class="form-input"
/>
<text class="input-suffix"></text>
</view>
<view class="form-item">
<text class="form-label">水表起始读数</text>
<input
type="digit"
v-model="form.waterStartReading"
placeholder="请输入水表起始读数(选填)"
class="form-input"
/>
<text class="input-suffix"></text>
</view>
<view class="form-item">
<text class="form-label">电表起始读数</text>
<input
type="digit"
v-model="form.electricStartReading"
placeholder="请输入电表起始读数(选填)"
class="form-input"
/>
<text class="input-suffix"></text>
</view>
<view class="form-item">
<text class="form-label">备注</text>
<textarea
v-model="form.remark"
placeholder="请输入备注信息(选填)"
class="form-textarea"
maxlength="200"
/>
</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>
</view>
</template>
<script>
import rentalApi from '@/api/rental.js'
import apartmentApi from '@/api/apartment.js'
import roomApi from '@/api/room.js'
export default {
data() {
return {
currentStep: 1,
submitLoading: false,
apartments: [],
rooms: [],
selectedApartmentId: null,
leasePeriod: 1,
form: {
roomId: '',
renterName: '',
renterPhone: '',
renterIdCard: '',
paymentType: 'monthly',
startDate: '',
endDate: '',
rent: '',
deposit: '',
waterStartReading: '',
electricStartReading: '',
remark: ''
},
paymentTypes: [
{ label: '月租', value: 'monthly' },
{ label: '季租', value: 'quarterly' },
{ label: '半年租', value: 'half_year' },
{ label: '年租', value: 'yearly' }
]
}
},
computed: {
availableRooms() {
return this.rooms.filter(room => room.status === 'empty' || room.status === 'vacant')
},
periodOptions() {
if (this.form.paymentType === 'monthly') {
return Array.from({ length: 12 }, (_, i) => ({
label: `${i + 1}个月`,
value: i + 1
}))
} else if (this.form.paymentType === 'quarterly') {
return Array.from({ length: 4 }, (_, i) => ({
label: `${i + 1}个季度`,
value: i + 1
}))
} else if (this.form.paymentType === 'half_year') {
return Array.from({ length: 2 }, (_, i) => ({
label: `${i + 1}个半年`,
value: i + 1
}))
} else {
return Array.from({ length: 5 }, (_, i) => ({
label: `${i + 1}`,
value: i + 1
}))
}
}
},
onLoad(options) {
this.loadApartments()
// 设置默认开始日期为今天
this.form.startDate = this.formatDate(new Date())
if (options.roomId) {
this.preSelectRoom(options.roomId)
}
},
watch: {
'form.paymentType': function() {
this.leasePeriod = 1
this.updateEndDate()
},
leasePeriod: function() {
this.updateEndDate()
},
'form.startDate': function() {
this.updateEndDate()
}
},
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 preSelectRoom(roomId) {
try {
const res = await roomApi.getDetail(roomId)
if (res.data) {
const room = res.data
this.selectedApartmentId = room.apartmentId
await this.loadRooms(room.apartmentId)
this.form.roomId = roomId
this.form.rent = room.rent || ''
this.currentStep = 2
}
} catch (error) {
console.error('加载房间详情失败:', error)
}
},
selectApartment(id) {
this.selectedApartmentId = id
this.form.roomId = ''
this.loadRooms(id)
},
selectRoom(room) {
this.form.roomId = room.id
this.form.rent = room.rent || ''
},
selectPaymentType(type) {
this.form.paymentType = type
},
selectPeriod(period) {
this.leasePeriod = period
},
onStartDateChange(e) {
this.form.startDate = e.detail.value
},
updateEndDate() {
if (!this.form.startDate) return
const startDate = new Date(this.form.startDate)
const endDate = new Date(startDate)
switch(this.form.paymentType) {
case 'monthly':
endDate.setMonth(endDate.getMonth() + this.leasePeriod)
break
case 'quarterly':
endDate.setMonth(endDate.getMonth() + (this.leasePeriod * 3))
break
case 'half_year':
endDate.setMonth(endDate.getMonth() + (this.leasePeriod * 6))
break
case 'yearly':
endDate.setFullYear(endDate.getFullYear() + this.leasePeriod)
break
}
this.form.endDate = this.formatDate(endDate)
},
nextStep() {
if (this.currentStep < 3) {
this.currentStep++
}
},
prevStep() {
if (this.currentStep > 1) {
this.currentStep--
}
},
async submitForm() {
// 表单验证
if (!this.form.roomId) {
uni.showToast({ title: '请选择房间', icon: 'none' })
this.currentStep = 1
return
}
if (!this.form.renterName.trim()) {
uni.showToast({ title: '请输入租客姓名', icon: 'none' })
this.currentStep = 2
return
}
if (!this.form.startDate) {
uni.showToast({ title: '请选择开始日期', icon: 'none' })
return
}
if (!this.form.rent) {
uni.showToast({ title: '请输入租金', icon: 'none' })
return
}
this.submitLoading = true
try {
const submitData = {
roomId: this.form.roomId,
renterName: this.form.renterName.trim(),
renterPhone: this.form.renterPhone.trim() || null,
renterIdCard: this.form.renterIdCard.trim() || null,
paymentType: this.form.paymentType,
startDate: this.form.startDate,
endDate: this.form.endDate,
rent: parseFloat(this.form.rent),
deposit: parseFloat(this.form.deposit) || 0,
waterStartReading: this.form.waterStartReading ? parseFloat(this.form.waterStartReading) : null,
electricStartReading: this.form.electricStartReading ? parseFloat(this.form.electricStartReading) : null,
remark: this.form.remark.trim() || null
}
await rentalApi.createWithRenter(submitData)
uni.showToast({
title: '办理入住成功',
icon: 'success'
})
setTimeout(() => {
uni.switchTab({ url: '/pages/properties/properties' })
}, 1500)
} catch (error) {
console.error('办理入住失败:', error)
uni.showToast({
title: '办理失败',
icon: 'none'
})
} finally {
this.submitLoading = false
}
},
formatDate(date) {
const d = new Date(date)
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
},
goBack() {
if (this.currentStep > 1) {
this.prevStep()
} else {
uni.navigateBack()
}
},
saveRental() {
if (this.currentStep < 3) {
this.nextStep()
} else {
this.submitForm()
}
}
}
}
</script>
<style scoped>
.add-record-page {
min-height: 100vh;
background: #F8FAFC;
display: flex;
flex-direction: column;
}
/* 导航栏 */
.custom-nav {
background: #FFFFFF;
border-bottom: 2rpx solid #F1F5F9;
}
.nav-content {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20rpx 32rpx;
}
.nav-btn {
width: 72rpx;
height: 72rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
}
.nav-title {
font-size: 34rpx;
font-weight: 600;
color: #1E293B;
}
.save-text {
font-size: 28rpx;
color: #409EFF;
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: #E4E7ED;
color: #909399;
font-size: 24rpx;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
}
.step-item.active .step-num {
background: #409EFF;
color: #FFFFFF;
}
.step-item.completed .step-num {
background: #67C23A;
color: #FFFFFF;
}
.step-text {
font-size: 22rpx;
color: #909399;
}
.step-item.active .step-text {
color: #409EFF;
font-weight: 500;
}
.step-line {
width: 80rpx;
height: 2rpx;
background: #E4E7ED;
margin: 0 16rpx;
margin-bottom: 30rpx;
}
.step-line.active {
background: #409EFF;
}
/* 步骤面板 */
.step-panel {
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateX(20rpx); }
to { opacity: 1; transform: translateX(0); }
}
.panel-title {
font-size: 30rpx;
font-weight: 600;
color: #1E293B;
margin-bottom: 20rpx;
}
/* 公寓列表 */
.apartment-list {
margin-bottom: 32rpx;
}
.apartment-card {
background: #FFFFFF;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 16rpx;
border: 2rpx solid transparent;
}
.apartment-card.active {
border-color: #409EFF;
background: #F0F9FF;
}
.apt-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12rpx;
}
.apt-name {
font-size: 30rpx;
font-weight: 600;
color: #1E293B;
}
.apt-address {
font-size: 24rpx;
color: #909399;
margin-bottom: 12rpx;
display: block;
}
.apt-stats {
display: flex;
align-items: center;
gap: 16rpx;
font-size: 24rpx;
color: #64748B;
}
.apt-stats .divider {
color: #E4E7ED;
}
/* 房间网格 */
.room-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16rpx;
margin-bottom: 40rpx;
}
.room-item {
background: #FFFFFF;
border-radius: 12rpx;
padding: 20rpx;
text-align: center;
border: 2rpx solid #E4E7ED;
}
.room-item.active {
border-color: #409EFF;
background: #F0F9FF;
}
.room-num {
display: block;
font-size: 32rpx;
font-weight: 700;
color: #1E293B;
margin-bottom: 8rpx;
}
.room-price {
display: block;
font-size: 24rpx;
color: #F56C6C;
margin-bottom: 4rpx;
}
.room-area {
display: block;
font-size: 20rpx;
color: #909399;
}
.empty-tip {
grid-column: span 3;
text-align: center;
padding: 48rpx;
color: #909399;
font-size: 26rpx;
}
/* 表单卡片 */
.form-card {
background: #FFFFFF;
border-radius: 16rpx;
padding: 0 24rpx;
margin-bottom: 40rpx;
}
.form-item {
padding: 24rpx 0;
border-bottom: 2rpx solid #F1F5F9;
position: relative;
}
.form-item:last-child {
border-bottom: none;
}
.form-label {
display: block;
font-size: 26rpx;
color: #64748B;
margin-bottom: 16rpx;
}
.required {
color: #F56C6C;
}
.form-input {
width: 100%;
height: 60rpx;
font-size: 30rpx;
color: #1E293B;
}
.input-suffix {
position: absolute;
right: 0;
bottom: 36rpx;
font-size: 28rpx;
color: #909399;
}
.form-textarea {
width: 100%;
height: 160rpx;
font-size: 30rpx;
color: #1E293B;
}
.picker-value {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 30rpx;
color: #1E293B;
}
.static-value {
font-size: 30rpx;
color: #909399;
}
/* 单选组 */
.radio-group {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
}
.radio-item {
padding: 16rpx 32rpx;
background: #F5F7FA;
border-radius: 8rpx;
font-size: 28rpx;
color: #606266;
}
.radio-item.active {
background: #409EFF;
color: #FFFFFF;
}
/* 租期选择 */
.period-options {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
}
.period-item {
padding: 12rpx 24rpx;
background: #F5F7FA;
border-radius: 8rpx;
font-size: 26rpx;
color: #606266;
}
.period-item.active {
background: #409EFF;
color: #FFFFFF;
}
/* 按钮 */
.btn-next {
background: linear-gradient(135deg, #409EFF 0%, #66b1ff 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: #F5F7FA;
color: #606266;
font-size: 30rpx;
font-weight: 600;
padding: 28rpx;
border-radius: 16rpx;
border: none;
}
.btn-submit {
flex: 2;
background: linear-gradient(135deg, #67C23A 0%, #85ce61 100%);
color: #FFFFFF;
font-size: 30rpx;
font-weight: 600;
padding: 28rpx;
border-radius: 16rpx;
border: none;
}
</style>