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

878 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="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>