<template>
|
<view class="page" @tap="closeParkingResults">
|
<view class="page-header">
|
<view class="safe-space" :style="'height:' + navTop + 'px'"></view>
|
<view class="nav-row" :style="'height:' + navHeight + 'px;padding-right:' + navRight + 'px'">
|
<view class="back-button" @tap="goBack"><image src="/static/news-assets/arrow-left.svg" mode="aspectFit"></image></view>
|
<text class="nav-title">车牌信息</text>
|
<view class="capsule-space" :style="'width:' + navWidth + 'px'"></view>
|
</view>
|
</view>
|
|
<view class="form-content">
|
<view v-if="requiresParking" class="field-group parking-field" @tap.stop>
|
<text class="field-label">停车场</text>
|
<view class="input-box" @tap.stop="openParkingResults"><input v-model="parkingKeyword" placeholder="搜索停车场" placeholder-class="input-placeholder" confirm-type="search" @focus="openParkingResults" @confirm="openParkingResults" @input="handleParkingInput" /><view class="input-search-button"><image src="/static/benefits-assets/search.svg" mode="aspectFit"></image></view></view>
|
<text v-if="parkingError != ''" class="field-error">{{parkingError}}</text>
|
</view>
|
|
<!-- 搜索结果必须挂在 .form-content 上(它 position:relative 且足够高)。
|
如果放在“停车场”字段里面,会被该字段自带的 overflow:hidden 裁掉,只会露出半行文字。 -->
|
<view v-if="requiresParking && showParkingResults" class="parking-results" :style="'height:' + parkingResultsHeight">
|
<view v-if="parkingSearchLoading" class="parking-result-empty"><text>正在搜索停车场</text></view>
|
<view v-else-if="filteredParkingLots.length > 0" class="parking-result-list">
|
<view class="parking-result-item" v-for="item in visibleParkingLots" :key="item.id" @tap.stop="selectParking(item)"><text class="parking-result-name">{{item.name}}</text><text class="parking-result-address">{{item.address}}</text></view>
|
<view v-if="filteredParkingLots.length > visibleParkingLots.length" class="parking-result-more"><text>还有 {{filteredParkingLots.length - visibleParkingLots.length}} 个停车场,请继续输入关键词</text></view>
|
</view>
|
<view v-else class="parking-result-empty"><text>暂无匹配停车场</text></view>
|
</view>
|
|
<view class="field-group plate-field" :class="requiresParking ? 'with-parking' : ''">
|
<text class="field-label">车牌号</text>
|
<view class="plate-composer">
|
<picker class="plate-picker" :range="provinces" :value="provinceIndex" @change="changeProvince"><view class="plate-prefix" :class="provinceIndex == 0 ? 'placeholder' : 'selected'"><text>{{provinces[provinceIndex]}}</text><view class="picker-caret"></view></view></picker>
|
<picker class="plate-picker letter-picker" :range="letters" :value="letterIndex" @change="changeLetter"><view class="plate-prefix" :class="letterIndex == 0 ? 'placeholder' : 'selected'"><text>{{letters[letterIndex]}}</text><view class="picker-caret"></view></view></picker>
|
<view class="plate-suffix"><input :value="plateSuffix" maxlength="6" placeholder="5-6位数字或字母" placeholder-class="input-placeholder" @input="handlePlateSuffixInput" /></view>
|
</view>
|
<text v-if="plateError != ''" class="field-error">{{plateError}}</text>
|
</view>
|
|
<view class="submit-area" :class="requiresParking ? 'with-parking' : ''">
|
<view class="submit-button" @tap="submitPlate"><text>提交车牌号</text></view>
|
<text class="submit-tip">请确保填写车牌信息准确,提交后即可用于停车权益核验。</text>
|
</view>
|
</view>
|
</view>
|
</template>
|
|
<script setup lang="uts">
|
import { apiEnvironment } from '../../config/api.uts'
|
import { getParkingMerchantDetail, searchParkingLots, submitParkingPlateRegistration } from '../../services/portal.uts'
|
import { getSession } from '../../services/session.uts'
|
import { safeBack } from '../../utils/navigation.uts'
|
|
const navTop = ref(28)
|
const navHeight = ref(32)
|
const navWidth = ref(88)
|
const navRight = ref(10)
|
const requiresParking = ref(false)
|
const parkingKeyword = ref('')
|
const selectedParkingId = ref(0)
|
const showParkingResults = ref(false)
|
const parkingSearchLoading = ref(false)
|
const parkingSearchVersion = ref(0)
|
let parkingSearchTimer = 0
|
const provinces = ['省份', '京', '津', '冀', '晋', '蒙', '辽', '吉', '黑', '沪', '苏', '浙', '皖', '闽', '赣', '鲁', '豫', '鄂', '湘', '粤', '桂', '琼', '渝', '川', '贵', '云', '藏', '陕', '甘', '青', '宁', '新']
|
const letters = ['字母', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', 'L', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z']
|
const provinceIndex = ref(0)
|
const letterIndex = ref(0)
|
const plateSuffix = ref('')
|
const parkingError = ref('')
|
const plateError = ref('')
|
const isSubmitting = ref(false)
|
const merchantId = ref('')
|
const benefitId = ref('')
|
const merchantName = ref('')
|
const talentId = ref('')
|
const talentCardId = ref('')
|
const registrationKey = ref('')
|
const remoteParkingLots = ref<any[]>([])
|
const parkingLots = [
|
{ id: 1, name: '盐源居小区停车场', address: '盐华路38号2栋' },
|
{ id: 2, name: '东林巷停车场', address: '东林巷29号1栋、2栋、3栋' },
|
{ id: 3, name: '东区政府第2号停车场', address: '东阳巷19号' },
|
{ id: 4, name: '东区政府第3号停车场', address: '原环保局办公楼' },
|
{ id: 5, name: '东区政府第4号停车场', address: '东阳巷8号' },
|
{ id: 6, name: '东区机关食堂外停车场', address: '东阳巷23号旁红砖房' },
|
{ id: 7, name: '攀枝花大道558号停车场', address: '攀枝花大道中段558号6栋' },
|
{ id: 8, name: '五十四交通银行旁停车场', address: '供销社背后金星巷3号' }
|
]
|
const filteredParkingLots = ref(parkingLots)
|
// 下拉最多展示 5 条:再多就提示继续输入关键词,避免下拉把页面撑爆或需要内部滚动。
|
const parkingResultLimit = 5
|
const visibleParkingLots = computed((): any[] => {
|
const all: any[] = filteredParkingLots.value as any[]
|
return all.slice(0, parkingResultLimit)
|
})
|
// 下拉高度由 JS 明确算出来(列表 + 可能出现的“还有 N 个”提示行 + 上下边框),
|
// 不依赖容器自动撑高,避免部分机型上下拉被压扁只露出半行。
|
const parkingResultsHeight = computed((): string => {
|
if (parkingSearchLoading.value) return '116rpx'
|
const list: any[] = visibleParkingLots.value as any[]
|
if (list.length == 0) return '116rpx'
|
let height = list.length * 92
|
if (filteredParkingLots.value.length > list.length) height += 64
|
return '' + (height + 4) + 'rpx'
|
})
|
|
onLoad((options: any) => {
|
// #ifdef MP-WEIXIN
|
const capsule = uni.getMenuButtonBoundingClientRect()
|
const info = uni.getSystemInfoSync()
|
navTop.value = capsule.top
|
navHeight.value = capsule.height
|
navWidth.value = capsule.width
|
navRight.value = info.windowWidth - capsule.right
|
// #endif
|
if (options != null && options['mode'] != null && options['mode'] == 'withParking') requiresParking.value = true
|
if (options != null && options['merchantId'] != null) merchantId.value = options['merchantId'] as string
|
if (options != null && options['benefitId'] != null) benefitId.value = options['benefitId'] as string
|
if (options != null && options['name'] != null) merchantName.value = decodeURIComponent(options['name'] as string)
|
if (options != null && options['talentCardId'] != null) talentCardId.value = options['talentCardId'] as string
|
const session = getSession()
|
if (session != null && session.talentId != null) talentId.value = session.talentId as string
|
if (talentId.value == '' && apiEnvironment == 'development') talentId.value = '2'
|
filteredParkingLots.value = []
|
loadParkingDetail()
|
})
|
const loadParkingDetail = () => {
|
if (!/^\d+$/.test(merchantId.value)) { plateError.value = '停车权益配置缺失'; return }
|
getParkingMerchantDetail(merchantId.value, talentCardId.value, (data: any) => {
|
if (data != null && data.benefit != null && data.benefit.benefitId != null) benefitId.value = '' + data.benefit.benefitId
|
if (data != null && data.merchantName != null) merchantName.value = data.merchantName as string
|
requiresParking.value = data != null && data.plateFormMode == 'withParking'
|
}, (error) => { plateError.value = error.message })
|
}
|
const goBack = () => {
|
if (merchantId.value == '') { safeBack('/pages/transport-benefits/transport-benefits'); return }
|
safeBack('/pages/merchant/merchant?type=transport&merchantType=parking&merchantId=' + encodeURIComponent(merchantId.value) + '&name=' + encodeURIComponent(merchantName.value))
|
}
|
const filterParkingLots = (keyword: string) => {
|
const next = []
|
let i = 0
|
while (i < remoteParkingLots.value.length) {
|
const item = remoteParkingLots.value[i]
|
if (keyword == '' || item.name.indexOf(keyword) >= 0 || item.address.indexOf(keyword) >= 0) next.push(item)
|
i++
|
}
|
filteredParkingLots.value = next
|
}
|
const scheduleParkingSearch = (keyword: string) => {
|
const requestVersion = parkingSearchVersion.value + 1
|
parkingSearchVersion.value = requestVersion
|
if (parkingSearchTimer != 0) clearTimeout(parkingSearchTimer)
|
parkingSearchLoading.value = true
|
parkingSearchTimer = setTimeout(() => {
|
if (requestVersion != parkingSearchVersion.value) return
|
if (!/^\d+$/.test(merchantId.value)) { parkingError.value = '停车权益配置缺失'; parkingSearchLoading.value = false; return }
|
searchParkingLots(merchantId.value, keyword, 1, 20, (data: any) => {
|
if (requestVersion != parkingSearchVersion.value) return
|
const records: any[] = data != null && data.records instanceof Array ? data.records as any[] : []
|
remoteParkingLots.value = records.map((item: any) => ({ id: '' + item.id, name: item.parkingLotName != null ? item.parkingLotName as string : '', address: item.address != null ? item.address as string : '' }))
|
filteredParkingLots.value = remoteParkingLots.value
|
parkingSearchLoading.value = false
|
parkingSearchTimer = 0
|
}, (error) => {
|
if (requestVersion != parkingSearchVersion.value) return
|
remoteParkingLots.value = []
|
filteredParkingLots.value = []
|
parkingError.value = error.message
|
parkingSearchLoading.value = false
|
parkingSearchTimer = 0
|
})
|
}, 250)
|
}
|
// 打开下拉的瞬间,同一次点击可能会冒泡到页面根的关闭处理;
|
// 记录打开时间,短时间内到达的关闭请求忽略,避免"点了搜索没反应"。
|
let parkingResultsOpenedAt = 0
|
const openParkingResults = () => {
|
parkingResultsOpenedAt = Date.now()
|
showParkingResults.value = true
|
scheduleParkingSearch(parkingKeyword.value.trim())
|
}
|
const closeParkingResults = () => {
|
if (Date.now() - parkingResultsOpenedAt < 250) return
|
showParkingResults.value = false
|
}
|
const handleParkingInput = (event: any) => {
|
const keyword = event.detail.value as string
|
parkingKeyword.value = keyword
|
selectedParkingId.value = 0
|
parkingError.value = ''
|
showParkingResults.value = true
|
scheduleParkingSearch(keyword.trim())
|
}
|
const selectParking = (item: any) => {
|
parkingSearchVersion.value++
|
if (parkingSearchTimer != 0) { clearTimeout(parkingSearchTimer); parkingSearchTimer = 0 }
|
parkingSearchLoading.value = false
|
selectedParkingId.value = item.id
|
parkingKeyword.value = item.name
|
parkingError.value = ''
|
showParkingResults.value = false
|
}
|
onUnload(() => {
|
parkingSearchVersion.value++
|
if (parkingSearchTimer != 0) clearTimeout(parkingSearchTimer)
|
})
|
const changeProvince = (event: any) => { provinceIndex.value = parseInt(event.detail.value as string); plateError.value = '' }
|
const changeLetter = (event: any) => { letterIndex.value = parseInt(event.detail.value as string); plateError.value = '' }
|
const handlePlateSuffixInput = (event: any) => {
|
const raw = (event.detail.value as string).toUpperCase()
|
plateSuffix.value = raw.replace(/[^A-Z0-9]/g, '').slice(0, 6)
|
plateError.value = ''
|
}
|
const submitPlate = () => {
|
if (isSubmitting.value) return
|
parkingError.value = ''
|
plateError.value = ''
|
if (requiresParking.value && selectedParkingId.value == 0) parkingError.value = '请从搜索结果中选择停车场'
|
if (provinceIndex.value == 0) plateError.value = '请选择省份简称'
|
else if (letterIndex.value == 0) plateError.value = '请选择车牌首字母'
|
else if (!/^[A-Z0-9]{5,6}$/.test(plateSuffix.value)) plateError.value = '请输入后 5-6 位数字或字母'
|
if (parkingError.value != '' || plateError.value != '') return
|
if (merchantId.value == '' || benefitId.value == '' || talentId.value == '') {
|
plateError.value = merchantId.value == '' || benefitId.value == '' ? '停车权益暂未配置' : '请登录后提交车牌'
|
return
|
}
|
isSubmitting.value = true
|
const plateNumber = provinces[provinceIndex.value] + letters[letterIndex.value] + plateSuffix.value
|
if (registrationKey.value == '') registrationKey.value = 'parking-registration-' + merchantId.value + '-' + Date.now()
|
submitParkingPlateRegistration(merchantId.value, talentId.value, benefitId.value, selectedParkingId.value == 0 ? '' : '' + selectedParkingId.value, plateNumber, registrationKey.value, (data: any) => {
|
isSubmitting.value = false
|
provinceIndex.value = 0
|
letterIndex.value = 0
|
plateSuffix.value = ''
|
parkingKeyword.value = ''
|
selectedParkingId.value = 0
|
uni.navigateTo({ url: '/pages/plate-submit-success/plate-submit-success?merchantId=' + encodeURIComponent(merchantId.value) + '&name=' + encodeURIComponent(merchantName.value) + '®istrationId=' + encodeURIComponent(data != null && data.registrationId != null ? '' + data.registrationId : '') })
|
}, (error) => {
|
isSubmitting.value = false
|
plateError.value = error.message
|
})
|
}
|
</script>
|
|
<style>
|
.page { width: 100%; height: 100vh; overflow: hidden; background-color: #F4F7FB; }
|
.page-header { padding-bottom: 60rpx; background-image: linear-gradient(180deg, #FFE8C5 0%, #FFF1DB 100%); }
|
.safe-space { height: 28px; }
|
.nav-row { display: flex; box-sizing: border-box; padding-left: 20rpx; flex-direction: row; align-items: center; justify-content: space-between; }
|
.back-button { display: flex; width: 70rpx; height: 64rpx; margin-left: -4rpx; justify-content: center; align-items: center; }
|
.back-button image { width: 36rpx; height: 36rpx; }
|
.nav-title { position: absolute; left: 50%; color: #171B22; font-size: 37rpx; line-height: 48rpx; font-weight: 700; transform: translateX(-50%); white-space: nowrap; }
|
.capsule-space { height: 100%; flex-shrink: 0; }
|
/* relative 供搜索结果下拉定位;overflow 放开,避免把下拉裁掉。 */
|
.form-content { position: relative; padding: 47rpx 54rpx 0; overflow: visible; }
|
.field-group { display: flex; position: relative; flex-direction: column; }
|
.parking-field { z-index: 20; overflow: visible; }
|
.plate-field { z-index: 1; }
|
.field-label { color: #70798A; font-size: 25rpx; line-height: 36rpx; font-weight: 600; }
|
.input-box { display: flex; height: 86rpx; margin-top: 17rpx; box-sizing: border-box; padding: 0 24rpx; border-width: 2rpx; border-style: solid; border-color: #E1E5EB; border-radius: 14rpx; background-color: #FFFFFF; flex-direction: row; align-items: center; }
|
.input-box input { height: 82rpx; min-width: 0; flex: 1; color: #2D333C; font-size: 28rpx; }
|
.input-box image { width: 39rpx; height: 39rpx; margin-left: 16rpx; flex-shrink: 0; }
|
/* 放大镜的点击区域放大到整块右侧区域,避免小图标点不中。 */
|
.input-search-button { display: flex; width: 72rpx; height: 86rpx; margin-left: 6rpx; margin-right: -14rpx; flex-shrink: 0; justify-content: center; align-items: center; }
|
.input-search-button image { width: 39rpx; height: 39rpx; }
|
.input-placeholder { color: #BBC1CA; font-size: 27rpx; }
|
.field-error { margin-top: 9rpx; color: #F05C3A; font-size: 22rpx; }
|
.plate-field { margin-top: 83rpx; }
|
.plate-field.with-parking { margin-top: 52rpx; }
|
.plate-composer { display: flex; height: 92rpx; margin-top: 17rpx; flex-direction: row; align-items: center; }
|
.plate-picker { width: 112rpx; height: 92rpx; flex-shrink: 0; }
|
.letter-picker { width: 104rpx; margin-left: 12rpx; }
|
.plate-prefix { display: flex; width: 100%; height: 92rpx; box-sizing: border-box; padding: 0 15rpx; border-width: 2rpx; border-style: solid; border-color: #E0E5EC; border-radius: 15rpx; background-color: #FFFFFF; color: #2D333C; font-size: 28rpx; font-weight: 600; flex-direction: row; justify-content: space-between; align-items: center; transition: border-color 160ms ease-out, background-color 160ms ease-out; }
|
.plate-prefix.placeholder { color: #AEB5C0; font-size: 23rpx; font-weight: 400; }
|
.plate-prefix.selected { border-color: #FFB58D; background-color: #FFF8F3; color: #E95E32; }
|
.picker-caret { width: 0; height: 0; margin-left: 7rpx; border-left-width: 7rpx; border-left-style: solid; border-left-color: transparent; border-right-width: 7rpx; border-right-style: solid; border-right-color: transparent; border-top-width: 8rpx; border-top-style: solid; border-top-color: #9CA4AF; }
|
.plate-prefix.selected .picker-caret { border-top-color: #F07A4D; }
|
.plate-suffix { display: flex; height: 92rpx; min-width: 0; margin-left: 12rpx; flex: 1; box-sizing: border-box; padding: 0 19rpx; border-width: 2rpx; border-style: solid; border-color: #E0E5EC; border-radius: 15rpx; background-color: #FFFFFF; align-items: center; }
|
.plate-suffix input { width: 100%; height: 88rpx; color: #2D333C; font-size: 27rpx; }
|
.plate-suffix .input-placeholder { color: #B5BBC5; font-size: 23rpx; }
|
.submit-area { margin-top: 351rpx; }
|
.submit-area.with-parking { margin-top: 259rpx; }
|
.submit-button { display: flex; height: 86rpx; border-radius: 44rpx; background-image: linear-gradient(90deg, #FF4B32 0%, #FF8A1E 100%); justify-content: center; align-items: center; }
|
.submit-button text { color: #FFFFFF; font-size: 29rpx; line-height: 40rpx; font-weight: 600; }
|
.submit-tip { display: block; margin-top: 31rpx; color: #A6ADB8; font-size: 21rpx; line-height: 32rpx; text-align: center; }
|
/* 挂在 .form-content 上:left/right 用表单左右内边距,正好和输入框等宽;
|
top = 47rpx(表单上边距) + 36rpx(标签) + 17rpx(间距) + 86rpx(输入框) = 186rpx,即输入框正下方。 */
|
.parking-results { position: absolute; z-index: 30; left: 54rpx; right: 54rpx; top: 186rpx; overflow: visible; border-width: 2rpx; border-style: solid; border-color: #E1E5EB; border-radius: 14rpx; background-color: #FFFFFF; box-shadow: 0 12rpx 30rpx rgba(62,73,90,.12); }
|
/* 高度由 parkingResultsHeight 内联给出;列表只负责把条目排开。 */
|
.parking-result-list { display: flex; flex-direction: column; }
|
.parking-result-item { display: flex; height: 92rpx; min-height: 92rpx; box-sizing: border-box; padding: 13rpx 22rpx; border-bottom-width: 1rpx; border-bottom-style: solid; border-bottom-color: #EEF0F4; flex-direction: column; flex-shrink: 0; justify-content: center; }
|
.parking-result-name { color: #333A45; font-size: 25rpx; line-height: 34rpx; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
.parking-result-address { margin-top: 3rpx; color: #929AA7; font-size: 20rpx; line-height: 29rpx; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
.parking-result-more { display: flex; height: 64rpx; justify-content: center; align-items: center; }
|
.parking-result-more text { color: #A4ABB5; font-size: 21rpx; }
|
.parking-result-empty { display: flex; height: 112rpx; justify-content: center; align-items: center; }
|
.parking-result-empty text { color: #A4ABB5; font-size: 23rpx; }
|
</style>
|