Files
property-resident-side/property-uniapp-project/pageSubPack/online-repair/personalRepair.vue
2026-08-19 10:21:58 +08:00

668 lines
18 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="form-container">
<!-- 报修房屋 -->
<view class="form-item">
<view class="label">
<text class="required">*</text>报修房屋
</view>
<view class="value" @click="goToSelectHouse">
{{ houseName || '请选择房屋' }}
<!-- <text class="switch" v-if="houseName">切换</text> -->
<uni-icons type="arrowright" size="16"></uni-icons>
</view>
</view>
<!-- 维修项目 -->
<view class="form-item">
<view class="label">
<text class="required">*</text>维修项目
</view>
<view class="value" @click="goToSelectProject">
{{ projectName || '请选择维修项目' }}
<uni-icons type="arrowright" size="16"></uni-icons>
</view>
</view>
<!-- 标题 -->
<view class="form-item">
<view class="label">
<text class="required">*</text>标题
</view>
<uni-easyinput v-model="form.title" placeholder="请输入内容最多30个字符" :inputBorder="false" maxlength="30"></uni-easyinput>
</view>
<!-- 问题描述 -->
<view class="form-item-wenti">
<text class="label">问题描述</text>
<view class="textarea-wrap">
<textarea v-model="form.desc" placeholder="请描述你的问题或位置最多200字" maxlength="200" class="desc-textarea" />
<text class="char-count">{{ form.desc.length }}/200</text>
</view>
<!-- <uni-easyinput v-model="form.desc" placeholder="请描述你的问题" type="textarea" maxlength="200"></uni-easyinput> -->
</view>
<!-- 手机号 -->
<view class="form-item">
<view class="label">
<text class="required">*</text>手机号
</view>
<uni-easyinput type="number" v-model="form.phone" placeholder="请输入手机号" :inputBorder="false" maxlength="11"></uni-easyinput>
</view>
<!-- 预约时间 -->
<view class="form-item time-item" @click="openTimePopup">
<view class="label">
<text class="required">*</text>预约时间
</view>
<text class="value time-value">{{ showTimeText || '请选择预约时间' }}</text>
</view>
<!-- 问题照片 -->
<view class="form-item-wenti">
<text class="label">问题照片</text>
<view class="upload-wrap">
<!-- 已上传图片 -->
<view class="upload-item" v-for="(img, index) in form.images" :key="index">
<image :src="img" mode="aspectFill" class="upload-img" @click="previewImage(img)"></image>
<image src="https://wuyeadmin.bugtc.com/images/imgs/delete.png" class="upload-delete" @click="deleteImage(index)"></image>
</view>
<!-- 上传按钮 -->
<view class="upload-btn" @click="chooseImage" v-if="form.images.length < 9">
<uni-icons type="plusempty" size="24"></uni-icons>
<!-- <text>上传照片</text> -->
</view>
</view>
</view>
<!-- 提交按钮 -->
<button class="submit-btn" :disabled="submitting" @click="submitForm">{{ submitting ? '提交中...' : '提交' }}</button>
<!-- 提交中遮罩 -->
<view class="submit-mask" v-if="submitting" @touchmove.stop.prevent></view>
<!-- 时间选择弹窗 -->
<uni-popup ref="timePopupRef" type="bottom" :mask-click="false" border-radius="16rpx">
<view class="time-popup-container">
<!-- 弹窗头部取消 + 标题 + 确认 -->
<view class="time-popup-header">
<text class="cancel-btn" @click="closeTimePopup">取消</text>
<text class="popup-title">预约时间({{ tempTime.format('YYYY-MM-DD HH:mm') }})</text>
<text class="confirm-btn" @click="confirmTime">确认</text>
</view>
<!-- 时间选择列日期 | 小时 | 分钟 -->
<view class="time-selector">
<!-- 日期列 -->
<scroll-view
class="time-column"
scroll-y
:scroll-top="dateScrollTop"
@scroll="onDateScroll"
scroll-with-animation
>
<view
class="time-item"
:class="{ active: dateItem.value === selectedDate }"
v-for="(dateItem, index) in dateList"
:key="index"
@click="selectDate(index)"
>
{{ dateItem.value }}{{dateItem.label}}
</view>
</scroll-view>
<!-- 小时列 -->
<scroll-view
class="time-column"
scroll-y
:scroll-top="hourScrollTop"
@scroll="onHourScroll"
scroll-with-animation
>
<view
class="time-item"
:class="{ active: hourItem.time === selectedHour }"
v-for="(hourItem, index) in hourList"
:key="index"
@click="selectHour(index)"
>
{{ hourItem.time }}
</view>
</scroll-view>
<!-- 分钟列 -->
<scroll-view
class="time-column"
scroll-y
:scroll-top="minuteScrollTop"
@scroll="onMinuteScroll"
scroll-with-animation
>
<view
class="time-item"
:class="{ active: minuteItem.minute === selectedMinute }"
v-for="(minuteItem, index) in minuteList"
:key="index"
@click="selectMinute(index)"
>
{{ minuteItem.minute }}
</view>
</scroll-view>
</view>
</view>
</uni-popup>
</view>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { onLoad, onUnload } from '@dcloudio/uni-app'
import { addQueryRepair, uploadFile } from '../api/api.js'
import { getUserProfile } from '../api/apiSub.js'
import moment from 'moment/moment'
// 选中的房屋/项目名称
const houseName = ref('')
const houseId = ref('')
const residentId = ref('')
const projectName = ref('')
const projectId = ref('')
// 提交状态锁
const submitting = ref(false)
// 表单数据
const form = ref({
title: '',
desc: '',
phone: '',
datetime: '',
images: []
})
// ========== 基础数据 ==========
const timePopupRef = ref(null)
// 默认预约时间:今天(向上取整到下一个整点/半点)
const getDefaultTime = () => {
const t = moment().second(0).millisecond(0)
if (t.minute() > 30) {
t.add(1, 'hour').minute(0)
} else if (t.minute() > 0) {
t.minute(30)
}
return t
}
const selectedTime = ref(getDefaultTime())
const tempTime = ref(getDefaultTime())
form.value.datetime = selectedTime.value.format('YYYY-MM-DD HH:mm')
// ========== 时间选择列数据 ==========
const dateList = ref([
{ label: '(今天)', value: moment().format('YYYY-MM-DD') },
{ label: '(明天)', value: moment().add(1, 'day').format('YYYY-MM-DD') },
{ label: '(后天)', value: moment().add(2, 'day').format('YYYY-MM-DD') }
])
const hourList = ref([
{ time: '08' }, { time: '09' }, { time: '10' }, { time: '11' },
{ time: '13' }, { time: '14' }, { time: '15' }, { time: '16' },
{ time: '17' }, { time: '18' }
])
const minuteList = ref([
{ minute: '00' },
{ minute: '30' }
])
// ========== 选中状态 ==========
const selectedDate = ref(selectedTime.value.format('YYYY-MM-DD'))
const selectedHour = ref(selectedTime.value.format('HH'))
const selectedMinute = ref(selectedTime.value.format('mm'))
// ========== 滚动配置 ==========
// 每项高度 / 可视高度rpx 转 px避免直接用数字导致滚动错位
const itemHeight = uni.upx2px(80)
const visibleHeight = uni.upx2px(400)
const centerOffset = (visibleHeight - itemHeight) / 2
const dateScrollTop = ref(0)
const hourScrollTop = ref(0)
const minuteScrollTop = ref(0)
// ========== 显示文本 ==========
const showTimeText = computed(() => {
if(!selectedTime.value) return ''
return selectedTime.value.format('YYYY-MM-DD HH:mm')
})
// ========== Popup 操作 ==========
const openTimePopup = () => {
tempTime.value = moment(selectedTime.value)
selectedDate.value = tempTime.value.format('YYYY-MM-DD')
selectedHour.value = tempTime.value.format('HH')
selectedMinute.value = tempTime.value.format('mm')
syncScrollPosition()
timePopupRef.value?.open()
}
const closeTimePopup = () => {
timePopupRef.value?.close()
}
const confirmTime = () => {
const selectedDateTime = `${selectedDate.value} ${selectedHour.value}:${selectedMinute.value}`
const now = moment()
if (moment(selectedDateTime, 'YYYY-MM-DD HH:mm').isBefore(now)) {
uni.showToast({ title: '预约时间不能早于当前时间', icon: 'none' })
return
}
selectedTime.value = moment(selectedDateTime, 'YYYY-MM-DD HH:mm')
form.value.datetime = selectedTime.value.format('YYYY-MM-DD HH:mm')
closeTimePopup()
uni.showToast({
title: `已选择 ${selectedTime.value.format('MM月DD日 HH:mm')}`,
icon: 'none'
})
}
// ========== 核心修复:只允许点击,滚动失效! ==========
const syncScrollPosition = () => {
const dateIdx = dateList.value.findIndex(i => i.value === selectedDate.value)
const hourIdx = hourList.value.findIndex(i => i.time === selectedHour.value)
const minIdx = minuteList.value.findIndex(i => i.minute === selectedMinute.value)
// 让选中项滚动到可视区域居中位置,并限制不小于 0
dateScrollTop.value = dateIdx >= 0 ? Math.max(0, dateIdx * itemHeight - centerOffset) : 0
hourScrollTop.value = hourIdx >= 0 ? Math.max(0, hourIdx * itemHeight - centerOffset) : 0
minuteScrollTop.value = minIdx >= 0 ? Math.max(0, minIdx * itemHeight - centerOffset) : 0
}
// 点击选择日期
const selectDate = (index) => {
selectedDate.value = dateList.value[index].value
syncScrollPosition()
updateTempTime()
}
// 点击选择小时
const selectHour = (index) => {
selectedHour.value = hourList.value[index].time
syncScrollPosition() // 选中后立刻复位滚动
updateTempTime()
}
// 点击选择分钟
const selectMinute = (index) => {
selectedMinute.value = minuteList.value[index].minute
syncScrollPosition() // 选中后立刻复位滚动
updateTempTime()
}
// 完全禁用滚动监听(去掉滚动干扰)
const onDateScroll = () => {}
const onHourScroll = () => {}
const onMinuteScroll = () => {}
const updateTempTime = () => {
tempTime.value = moment(`${selectedDate.value} ${selectedHour.value}:${selectedMinute.value}`, 'YYYY-MM-DD HH:mm')
}
onMounted(() => {
syncScrollPosition()
})
// ========== 页面通信 ==========
onLoad(() => {
uni.$on('selectHouse', (data) => {
houseName.value = data.name
houseId.value = data.id
residentId.value = data.residentId
})
uni.$on('selectProject', (data) => {
projectName.value = data.name
projectId.value = data.id
form.value.title = data.name
})
// 自动获取当前登录用户手机号
getUserProfile().then(res => {
if (res.code === 200 && res.data && res.data.phone) {
form.value.phone = res.data.phone
}
})
})
onUnload(() => {
uni.$off('selectHouse')
uni.$off('selectProject')
})
// 跳转
const goToSelectHouse = () => {
uni.navigateTo({ url: `/pageSubPack/online-repair/selectHouse?houseId=${houseId.value}` })
}
const goToSelectProject = () => {
uni.navigateTo({ url: '/pageSubPack/online-repair/personalSelectPoject' })
}
// 选择图片
const chooseImage = () => {
// 计算还能选几张
const remainCount = 9 - form.value.images.length;
if (remainCount <= 0) {
uni.showToast({
title: '最多只能上传9张图片',
icon: 'none'
})
return;
}
uni.chooseImage({
count: remainCount, // 动态限制可选数量
sizeType: ['original', 'compressed'],
sourceType: ['album', 'camera'],
success: (res) => {
const tempFilePaths = res.tempFilePaths
form.value.images = [...form.value.images, ...tempFilePaths]
}
})
}
// 预览图片
const previewImage = (url) => {
uni.previewImage({ urls: form.value.images, current: url })
}
const deleteImage = (index) => {
form.value.images.splice(index, 1)
}
// 上传图片
const uploadImages = async () => {
const urls = []
for (const path of form.value.images) {
if (path.startsWith('http')) {
urls.push(path)
continue
}
try {
const res = await uploadFile(path)
urls.push(res.url || '')
} catch (e) {
console.error('上传失败', e)
}
}
return urls
}
// ========== 表单提交 ==========
const submitForm = async () => {
if (!houseName.value) return uni.showToast({ title: '请选择报修房屋', icon: 'none' })
if (!projectName.value) return uni.showToast({ title: '请选择维修项目', icon: 'none' })
if (!form.value.title) return uni.showToast({ title: '请输入标题', icon: 'none' })
// if (!form.value.desc) return uni.showToast({ title: '请输入问题描述', icon: 'none' })
if (!/^1[3-9]\d{9}$/.test(form.value.phone)) return uni.showToast({ title: '手机号不正确', icon: 'none' })
if (!form.value.datetime) return uni.showToast({ title: '请选择预约时间', icon: 'none' })
if (moment(form.value.datetime, 'YYYY-MM-DD HH:mm').isBefore(moment())) return uni.showToast({ title: '预约时间不能早于当前时间', icon: 'none' })
const villageId = uni.getStorageSync('currentVillageId')
if (!villageId || villageId === '0') return uni.showToast({ title: '请先选择小区', icon: 'none' })
const userId = uni.getStorageSync('userId')
if (!userId) return uni.showToast({ title: '用户信息失效', icon: 'none' })
submitting.value = true
uni.showLoading({ title: '提交中...' ,mask:true})
try {
const imageUrls = await uploadImages()
const params = {
villageId,
houseId: houseId.value,
maintenanceItemId: projectId.value,
item: form.value.title,
problem: form.value.desc,
residentId: residentId.value,
orderDate: form.value.datetime,
phone: form.value.phone,
problemImageUrl: imageUrls.join(','),
projectType: '1',
problemStatus: ''
}
const res = await addQueryRepair(params)
if (res.code === 200) {
uni.hideLoading();
uni.showToast({ title: '提交成功', icon: 'success' })
form.value = { title: '', desc: '', phone: '', datetime: '', images: [] }
houseName.value = ''
houseId.value = ''
residentId.value = ''
projectName.value = ''
projectId.value = ''
setTimeout(() => {
uni.navigateBack({ delta: 1 })
}, 1500)
} else {
submitting.value = false
uni.showToast({ title: res.msg || '提交失败', icon: 'none' })
}
} catch (e) {
submitting.value = false
uni.showToast({ title: '提交失败,请检查网络或重试', icon: 'none' })
} finally {
uni.hideLoading()
}
}
</script>
<style scoped>
.form-container {
padding: 15px;
background: #fff;
min-height: 100vh;
}
.form-item {
display: flex;
margin-bottom: 30rpx;
align-items: flex-start;
border-bottom: 1px solid #eee;
}
.label {
width: 180rpx;
font-size: 28rpx;
color: #333;
line-height: 80rpx;
}
.required {
color: red;
margin-right: 4rpx;
}
.value {
flex: 1;
font-size: 28rpx;
color: #666;
line-height: 80rpx;
text-align: right;
display: flex;
justify-content: space-between;
}
.time-value {
white-space: nowrap;
font-size: 26rpx;
}
.input, .textarea {
flex: 1;
padding: 20rpx;
font-size: 28rpx;
border: 1px solid #eee;
border-radius: 8rpx;
}
.textarea {
min-height: 160rpx;
line-height: 40rpx;
}
.form-item-wenti {
margin-bottom: 20px;
/* border-bottom: 1px solid #eee; */
padding-bottom: 10px;
}
.textarea-wrap {
flex: 1;
position: relative;
}
.desc-textarea {
width: 100%;
height: 160rpx;
font-size: 28rpx;
padding: 10rpx 0;
box-sizing: border-box;
border: 1px solid #eee;
border-radius: 4px;
padding: 5px;
}
.char-count {
position: absolute;
right: 10rpx;
bottom: 10rpx;
font-size: 24rpx;
color: #999;
}
.switch {
font-size: 12px;
color: #007aff;
margin-right: 5px;
}
input, textarea {
font-size: 14px;
width: 100%;
padding: 8px 0;
box-sizing: border-box;
}
textarea {
resize: none;
}
.upload-wrap {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 8px;
}
.upload-item {
position: relative;
width: 80px;
height: 80px;
}
.upload-item .upload-img{
width: 100%;
height: 100%;
border-radius: 4px;
}
.upload-item .upload-delete{
position: absolute;
top: -5px;
right: -5px;
width: 32rpx;
height: 32rpx;
border-radius: 3rpx;
padding: 2px;
}
.upload-btn {
width: 80px;
height: 80px;
/* border: 1px dashed #ccc; */
background-color: #f3f3f3;
border-radius: 4px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
color: #999;
font-size: 12px;
}
/* 提交中遮罩 */
.submit-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.4);
z-index: 9999;
}
.submit-btn {
width: 100%;
background: #007aff;
color: #fff;
border-radius: 8px;
height: 44px;
line-height: 44px;
font-size: 16px;
margin-top: 20px;
}
/* ========== uni-popup 时间选择器样式 ========== */
.time-popup-container {
background-color: #fff;
border-top-left-radius: 16rpx;
border-top-right-radius: 16rpx;
}
/* 弹窗头部 */
.time-popup-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx 30rpx;
border-bottom: 1px solid #eee;
}
.cancel-btn {
font-size: 28rpx;
color: #666;
}
.popup-title {
font-size: 28rpx;
color: #333;
}
.confirm-btn {
font-size: 28rpx;
color: #007aff;
font-weight: 600;
}
/* 时间选择器容器 */
.time-selector {
display: flex;
height: 400rpx;
padding: 20rpx 0;
}
/* 单个时间列 */
.time-column {
flex: 1;
height: 100%;
text-align: center;
}
/* 时间项 */
.time-item {
height: 80rpx;
line-height: 80rpx;
font-size: 28rpx;
color: #BBBCBE;
position: relative;
}
/* 选中项样式(高亮) */
.time-item.active {
color: #1D2129;
font-weight: 600;
background-color: #F7F7F7;
}
</style>