feat(品牌型号选择器): 添加品牌型号搜索功能并优化选择流程
- 在BrandModelPicker组件中添加搜索功能,支持按品牌名称过滤 - 将品牌和型号选择合并为单一流程,简化用户操作 - 更新setAuthCar页面使用新的品牌型号选择器组件 - 移除旧的独立品牌和型号选择器
Showing
3 changed files
with
119 additions
and
93 deletions
| ... | @@ -4,9 +4,18 @@ | ... | @@ -4,9 +4,18 @@ |
| 4 | <nut-popup v-model:visible="brandPickerVisible" position="bottom" :style="{ height: '80%' }"> | 4 | <nut-popup v-model:visible="brandPickerVisible" position="bottom" :style="{ height: '80%' }"> |
| 5 | <view class="brand-model-picker"> | 5 | <view class="brand-model-picker"> |
| 6 | <view class="picker-header"> | 6 | <view class="picker-header"> |
| 7 | - <text class="picker-title">选择品牌</text> | 7 | + <text class="picker-title">选择品牌型号</text> |
| 8 | <nut-button size="small" type="primary" @click="closePicker">关闭</nut-button> | 8 | <nut-button size="small" type="primary" @click="closePicker">关闭</nut-button> |
| 9 | </view> | 9 | </view> |
| 10 | + <view class="search-container"> | ||
| 11 | + <nut-searchbar | ||
| 12 | + v-model="searchKeyword" | ||
| 13 | + placeholder="搜索品牌名称" | ||
| 14 | + shape="round" | ||
| 15 | + background="transparent" | ||
| 16 | + class="search-bar" | ||
| 17 | + /> | ||
| 18 | + </view> | ||
| 10 | <view class="brand-container"> | 19 | <view class="brand-container"> |
| 11 | <scroll-view | 20 | <scroll-view |
| 12 | class="brand-content" | 21 | class="brand-content" |
| ... | @@ -16,7 +25,7 @@ | ... | @@ -16,7 +25,7 @@ |
| 16 | > | 25 | > |
| 17 | <view class="brand-list"> | 26 | <view class="brand-list"> |
| 18 | <view | 27 | <view |
| 19 | - v-for="brand in allBrands" | 28 | + v-for="brand in filteredBrands" |
| 20 | :key="brand.value" | 29 | :key="brand.value" |
| 21 | class="brand-item" | 30 | class="brand-item" |
| 22 | @click="selectBrand(brand)" | 31 | @click="selectBrand(brand)" |
| ... | @@ -59,7 +68,7 @@ | ... | @@ -59,7 +68,7 @@ |
| 59 | </template> | 68 | </template> |
| 60 | 69 | ||
| 61 | <script setup> | 70 | <script setup> |
| 62 | -import { ref } from 'vue' | 71 | +import { ref, computed } from 'vue' |
| 63 | import { Right } from '@nutui/icons-vue-taro' | 72 | import { Right } from '@nutui/icons-vue-taro' |
| 64 | 73 | ||
| 65 | // 定义事件 | 74 | // 定义事件 |
| ... | @@ -71,6 +80,7 @@ const modelPickerVisible = ref(false) | ... | @@ -71,6 +80,7 @@ const modelPickerVisible = ref(false) |
| 71 | const selectedBrandName = ref('') | 80 | const selectedBrandName = ref('') |
| 72 | const currentBrandModels = ref([]) | 81 | const currentBrandModels = ref([]) |
| 73 | const contentHeight = ref(800) // 默认高度 rpx | 82 | const contentHeight = ref(800) // 默认高度 rpx |
| 83 | +const searchKeyword = ref('') // 搜索关键词 | ||
| 74 | 84 | ||
| 75 | const allBrands = ref([ | 85 | const allBrands = ref([ |
| 76 | { text: '爱玛', value: 'aima' }, | 86 | { text: '爱玛', value: 'aima' }, |
| ... | @@ -189,6 +199,16 @@ const brandModelMap = ref({ | ... | @@ -189,6 +199,16 @@ const brandModelMap = ref({ |
| 189 | ] | 199 | ] |
| 190 | }) | 200 | }) |
| 191 | 201 | ||
| 202 | +// 过滤后的品牌列表 | ||
| 203 | +const filteredBrands = computed(() => { | ||
| 204 | + if (!searchKeyword.value.trim()) { | ||
| 205 | + return allBrands.value | ||
| 206 | + } | ||
| 207 | + return allBrands.value.filter(brand => | ||
| 208 | + brand.text.toLowerCase().includes(searchKeyword.value.toLowerCase()) | ||
| 209 | + ) | ||
| 210 | +}) | ||
| 211 | + | ||
| 192 | // 计算内容高度 | 212 | // 计算内容高度 |
| 193 | const calculateContentHeight = () => { | 213 | const calculateContentHeight = () => { |
| 194 | // 弹框总高度 - 头部高度 | 214 | // 弹框总高度 - 头部高度 |
| ... | @@ -200,6 +220,7 @@ const calculateContentHeight = () => { | ... | @@ -200,6 +220,7 @@ const calculateContentHeight = () => { |
| 200 | // 显示品牌选择器 | 220 | // 显示品牌选择器 |
| 201 | const show = () => { | 221 | const show = () => { |
| 202 | calculateContentHeight() // 计算内容高度 | 222 | calculateContentHeight() // 计算内容高度 |
| 223 | + searchKeyword.value = '' // 清空搜索关键词 | ||
| 203 | brandPickerVisible.value = true | 224 | brandPickerVisible.value = true |
| 204 | } | 225 | } |
| 205 | 226 | ||
| ... | @@ -276,6 +297,58 @@ defineExpose({ | ... | @@ -276,6 +297,58 @@ defineExpose({ |
| 276 | } | 297 | } |
| 277 | } | 298 | } |
| 278 | 299 | ||
| 300 | + .search-container { | ||
| 301 | + padding: 0 0 24rpx 0; | ||
| 302 | + margin-bottom: 20rpx; | ||
| 303 | + | ||
| 304 | + .search-bar { | ||
| 305 | + // background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | ||
| 306 | + border-radius: 20rpx; | ||
| 307 | + padding: 4rpx; | ||
| 308 | + // box-shadow: 0 8rpx 24rpx rgba(102, 126, 234, 0.15); | ||
| 309 | + transition: all 0.3s ease; | ||
| 310 | + | ||
| 311 | + &:hover { | ||
| 312 | + box-shadow: 0 12rpx 32rpx rgba(102, 126, 234, 0.25); | ||
| 313 | + transform: translateY(-2rpx); | ||
| 314 | + } | ||
| 315 | + | ||
| 316 | + :deep(.nut-searchbar__search-input) { | ||
| 317 | + background-color: white; | ||
| 318 | + border-radius: 16rpx; | ||
| 319 | + border: none; | ||
| 320 | + padding: 24rpx 20rpx; | ||
| 321 | + font-size: 28rpx; | ||
| 322 | + color: #333; | ||
| 323 | + transition: all 0.3s ease; | ||
| 324 | + | ||
| 325 | + &::placeholder { | ||
| 326 | + color: #999; | ||
| 327 | + font-weight: 400; | ||
| 328 | + } | ||
| 329 | + | ||
| 330 | + &:focus { | ||
| 331 | + background-color: #fafbfc; | ||
| 332 | + box-shadow: inset 0 2rpx 8rpx rgba(0, 0, 0, 0.06); | ||
| 333 | + } | ||
| 334 | + } | ||
| 335 | + | ||
| 336 | + :deep(.nut-searchbar__search-icon) { | ||
| 337 | + color: #666; | ||
| 338 | + width: 32rpx; | ||
| 339 | + height: 32rpx; | ||
| 340 | + } | ||
| 341 | + | ||
| 342 | + :deep(.nut-searchbar__input-clear) { | ||
| 343 | + color: #ccc; | ||
| 344 | + | ||
| 345 | + &:hover { | ||
| 346 | + color: #999; | ||
| 347 | + } | ||
| 348 | + } | ||
| 349 | + } | ||
| 350 | + } | ||
| 351 | + | ||
| 279 | .brand-container { | 352 | .brand-container { |
| 280 | flex: 1; | 353 | flex: 1; |
| 281 | height: 100%; | 354 | height: 100%; | ... | ... |
| ... | @@ -722,11 +722,11 @@ const onBrandModelConfirm = (result) => { | ... | @@ -722,11 +722,11 @@ const onBrandModelConfirm = (result) => { |
| 722 | // 设置组合字段用于表单验证 | 722 | // 设置组合字段用于表单验证 |
| 723 | formData.brandModel = `${result.brand} ${result.model}` | 723 | formData.brandModel = `${result.brand} ${result.model}` |
| 724 | 724 | ||
| 725 | - Taro.showToast({ | 725 | + // Taro.showToast({ |
| 726 | - title: `已选择 ${result.brand} ${result.model}`, | 726 | + // title: `已选择 ${result.brand} ${result.model}`, |
| 727 | - icon: 'success', | 727 | + // icon: 'success', |
| 728 | - duration: 2000 | 728 | + // duration: 2000 |
| 729 | - }) | 729 | + // }) |
| 730 | } | 730 | } |
| 731 | 731 | ||
| 732 | /** | 732 | /** | ... | ... |
| ... | @@ -86,20 +86,13 @@ | ... | @@ -86,20 +86,13 @@ |
| 86 | <!-- 车辆详情表单 --> | 86 | <!-- 车辆详情表单 --> |
| 87 | <nut-form ref="formRef" :model-value="formData"> | 87 | <nut-form ref="formRef" :model-value="formData"> |
| 88 | <view class="form-section"> | 88 | <view class="form-section"> |
| 89 | - <!-- 车型品牌 --> | 89 | + <!-- 品牌型号选择 --> |
| 90 | - <nut-form-item label-position="top" label="车型品牌" prop="brand" required | 90 | + <nut-form-item label-position="top" label="品牌型号选择" prop="brandModel" required |
| 91 | - :rules="[{ required: true, message: '请选择车型品牌' }]"> | 91 | + :rules="[{ required: true, message: '请选择品牌型号' }]"> |
| 92 | - <view class="form-item-content" @click="showBrandPicker"> | 92 | + <view class="form-item-content" @click="showBrandModelPicker"> |
| 93 | - <text class="form-value">{{ formData.brand || '请选择' }}</text> | 93 | + <text class="form-value"> |
| 94 | - <Right class="arrow-icon" /> | 94 | + {{ formData.brand && formData.model ? `${formData.brand} ${formData.model}` : '请选择品牌型号' }} |
| 95 | - </view> | 95 | + </text> |
| 96 | - </nut-form-item> | ||
| 97 | - | ||
| 98 | - <!-- 车辆型号 --> | ||
| 99 | - <nut-form-item label-position="top" label="车辆型号" prop="model" required | ||
| 100 | - :rules="[{ required: true, message: '请选择车辆型号' }]"> | ||
| 101 | - <view class="form-item-content" @click="showModelPicker"> | ||
| 102 | - <text class="form-value">{{ formData.model || '请选择' }}</text> | ||
| 103 | <Right class="arrow-icon" /> | 96 | <Right class="arrow-icon" /> |
| 104 | </view> | 97 | </view> |
| 105 | </nut-form-item> | 98 | </nut-form-item> |
| ... | @@ -140,18 +133,12 @@ | ... | @@ -140,18 +133,12 @@ |
| 140 | </nut-button> | 133 | </nut-button> |
| 141 | </view> | 134 | </view> |
| 142 | 135 | ||
| 143 | - <!-- 选择器弹窗 --> | 136 | + <!-- 品牌型号选择器 --> |
| 144 | - <!-- 品牌选择 --> | 137 | + <BrandModelPicker |
| 145 | - <nut-popup v-model:visible="brandPickerVisible" position="bottom"> | 138 | + ref="brandModelPickerRef" |
| 146 | - <nut-picker v-model="brandValue" :columns="brandOptions" title="选择车型品牌" @confirm="onBrandConfirm" | 139 | + @confirm="onBrandModelConfirm" |
| 147 | - @cancel="brandPickerVisible = false" /> | 140 | + @cancel="onBrandModelCancel" |
| 148 | - </nut-popup> | 141 | + /> |
| 149 | - | ||
| 150 | - <!-- 型号选择 --> | ||
| 151 | - <nut-popup v-model:visible="modelPickerVisible" position="bottom"> | ||
| 152 | - <nut-picker v-model="modelValue" :columns="modelOptions" title="选择车辆型号" @confirm="onModelConfirm" | ||
| 153 | - @cancel="modelPickerVisible = false" /> | ||
| 154 | - </nut-popup> | ||
| 155 | </view> | 142 | </view> |
| 156 | </template> | 143 | </template> |
| 157 | 144 | ||
| ... | @@ -161,6 +148,7 @@ import { Plus, Right, Close } from '@nutui/icons-vue-taro' | ... | @@ -161,6 +148,7 @@ import { Plus, Right, Close } from '@nutui/icons-vue-taro' |
| 161 | import Taro from '@tarojs/taro' | 148 | import Taro from '@tarojs/taro' |
| 162 | import './index.less' | 149 | import './index.less' |
| 163 | import BASE_URL from '@/utils/config'; | 150 | import BASE_URL from '@/utils/config'; |
| 151 | +import BrandModelPicker from '@/components/BrandModelPicker.vue' | ||
| 164 | 152 | ||
| 165 | // 获取页面参数 | 153 | // 获取页面参数 |
| 166 | const instance = Taro.getCurrentInstance() | 154 | const instance = Taro.getCurrentInstance() |
| ... | @@ -189,40 +177,14 @@ const closePreview = () => { | ... | @@ -189,40 +177,14 @@ const closePreview = () => { |
| 189 | const formData = reactive({ | 177 | const formData = reactive({ |
| 190 | brand: '', | 178 | brand: '', |
| 191 | model: '', | 179 | model: '', |
| 180 | + brandModel: '', // 品牌型号组合字段,用于表单验证 | ||
| 192 | range: '', | 181 | range: '', |
| 193 | maxSpeed: '', | 182 | maxSpeed: '', |
| 194 | description: '' | 183 | description: '' |
| 195 | }) | 184 | }) |
| 196 | 185 | ||
| 197 | -// 选择器显示状态 | 186 | +// 品牌型号选择器组件引用 |
| 198 | -const brandPickerVisible = ref(false) | 187 | +const brandModelPickerRef = ref(null) |
| 199 | -const modelPickerVisible = ref(false) | ||
| 200 | - | ||
| 201 | -// 选择器值 | ||
| 202 | -const brandValue = ref([]) | ||
| 203 | -const modelValue = ref([]) | ||
| 204 | - | ||
| 205 | -// 选择器选项数据 | ||
| 206 | -const brandOptions = ref([ | ||
| 207 | - { text: '小牛电动', value: '小牛电动' }, | ||
| 208 | - { text: '雅迪', value: '雅迪' }, | ||
| 209 | - { text: '爱玛', value: '爱玛' }, | ||
| 210 | - { text: '台铃', value: '台铃' }, | ||
| 211 | - { text: '绿源', value: '绿源' }, | ||
| 212 | - { text: '新日', value: '新日' }, | ||
| 213 | - { text: '立马', value: '立马' }, | ||
| 214 | - { text: '其他', value: '其他' } | ||
| 215 | -]) | ||
| 216 | - | ||
| 217 | -const modelOptions = ref([ | ||
| 218 | - { text: 'NGT', value: 'NGT' }, | ||
| 219 | - { text: 'UQi+', value: 'UQi+' }, | ||
| 220 | - { text: 'Gova G2', value: 'Gova G2' }, | ||
| 221 | - { text: 'MQi+', value: 'MQi+' }, | ||
| 222 | - { text: 'NQi GTS', value: 'NQi GTS' }, | ||
| 223 | - { text: 'RQi', value: 'RQi' }, | ||
| 224 | - { text: '其他型号', value: '其他型号' } | ||
| 225 | -]) | ||
| 226 | 188 | ||
| 227 | 189 | ||
| 228 | 190 | ||
| ... | @@ -325,35 +287,33 @@ const previewImage = (imageUrl) => { | ... | @@ -325,35 +287,33 @@ const previewImage = (imageUrl) => { |
| 325 | } | 287 | } |
| 326 | 288 | ||
| 327 | /** | 289 | /** |
| 328 | - * 显示品牌选择器 | 290 | + * 显示品牌型号选择器 |
| 329 | - */ | ||
| 330 | -const showBrandPicker = () => { | ||
| 331 | - brandPickerVisible.value = true | ||
| 332 | -} | ||
| 333 | - | ||
| 334 | -/** | ||
| 335 | - * 显示型号选择器 | ||
| 336 | */ | 291 | */ |
| 337 | -const showModelPicker = () => { | 292 | +const showBrandModelPicker = () => { |
| 338 | - modelPickerVisible.value = true | 293 | + brandModelPickerRef.value?.show() |
| 339 | } | 294 | } |
| 340 | 295 | ||
| 341 | /** | 296 | /** |
| 342 | - * 品牌选择确认 | 297 | + * 品牌型号选择确认 |
| 343 | - * @param {Object} options - 选择的选项 | 298 | + * @param {Object} data - 选择的品牌和型号数据 |
| 344 | */ | 299 | */ |
| 345 | -const onBrandConfirm = (options) => { | 300 | +const onBrandModelConfirm = (data) => { |
| 346 | - formData.brand = options.selectedOptions[0]?.text || '' | 301 | + formData.brand = data.brand |
| 347 | - brandPickerVisible.value = false | 302 | + formData.model = data.model |
| 303 | + formData.brandModel = `${data.brand} ${data.model}` // 设置组合字段用于表单验证 | ||
| 304 | + | ||
| 305 | + // Taro.showToast({ | ||
| 306 | + // title: `已选择 ${data.brand} ${data.model}`, | ||
| 307 | + // icon: 'success', | ||
| 308 | + // duration: 2000 | ||
| 309 | + // }) | ||
| 348 | } | 310 | } |
| 349 | 311 | ||
| 350 | /** | 312 | /** |
| 351 | - * 型号选择确认 | 313 | + * 品牌型号选择取消回调 |
| 352 | - * @param {Object} options - 选择的选项 | ||
| 353 | */ | 314 | */ |
| 354 | -const onModelConfirm = (options) => { | 315 | +const onBrandModelCancel = () => { |
| 355 | - formData.model = options.selectedOptions[0]?.text || '' | 316 | + // 可以在这里处理取消逻辑 |
| 356 | - modelPickerVisible.value = false | ||
| 357 | } | 317 | } |
| 358 | 318 | ||
| 359 | /** | 319 | /** |
| ... | @@ -361,17 +321,9 @@ const onModelConfirm = (options) => { | ... | @@ -361,17 +321,9 @@ const onModelConfirm = (options) => { |
| 361 | */ | 321 | */ |
| 362 | const onSubmit = () => { | 322 | const onSubmit = () => { |
| 363 | // 表单验证 | 323 | // 表单验证 |
| 364 | - if (!formData.brand) { | 324 | + if (!formData.brandModel) { |
| 365 | - Taro.showToast({ | ||
| 366 | - title: '请选择车型品牌', | ||
| 367 | - icon: 'none' | ||
| 368 | - }) | ||
| 369 | - return | ||
| 370 | - } | ||
| 371 | - | ||
| 372 | - if (!formData.model) { | ||
| 373 | Taro.showToast({ | 325 | Taro.showToast({ |
| 374 | - title: '请选择车辆型号', | 326 | + title: '请选择品牌型号', |
| 375 | icon: 'none' | 327 | icon: 'none' |
| 376 | }) | 328 | }) |
| 377 | return | 329 | return |
| ... | @@ -475,6 +427,7 @@ const loadAuthData = async () => { | ... | @@ -475,6 +427,7 @@ const loadAuthData = async () => { |
| 475 | Object.assign(formData, { | 427 | Object.assign(formData, { |
| 476 | brand: mockAuthData.brand, | 428 | brand: mockAuthData.brand, |
| 477 | model: mockAuthData.model, | 429 | model: mockAuthData.model, |
| 430 | + brandModel: `${mockAuthData.brand} ${mockAuthData.model}`, // 设置组合字段 | ||
| 478 | range: mockAuthData.range, | 431 | range: mockAuthData.range, |
| 479 | maxSpeed: mockAuthData.maxSpeed, | 432 | maxSpeed: mockAuthData.maxSpeed, |
| 480 | description: mockAuthData.description | 433 | description: mockAuthData.description | ... | ... |
-
Please register or login to post a comment