yeg
2026-09-17 75e20ed49063feac161beca13aba24147424d5a7
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
<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) + '&registrationId=' + 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>