hookehuyr

feat(品牌型号选择): 添加自定义品牌型号输入功能

refactor(车辆发布): 移除电池损耗度选择并优化表单验证
- 将磨损程度文字与数字分数相互转换
- 直接使用表单数据存储照片URL
- 简化品牌型号选择验证逻辑
...@@ -65,6 +65,47 @@ ...@@ -65,6 +65,47 @@
65 </view> 65 </view>
66 </view> 66 </view>
67 </nut-popup> 67 </nut-popup>
68 +
69 + <!-- 自定义输入弹框 -->
70 + <nut-popup v-model:visible="customInputVisible" position="bottom" :style="{ height: '60%' }">
71 + <view class="custom-input-picker">
72 + <view class="picker-header">
73 + <nut-button size="small" @click="goBackToBrandPicker">返回</nut-button>
74 + <text class="picker-title">自定义品牌型号</text>
75 + <nut-button size="small" type="primary" @click="closePicker">关闭</nut-button>
76 + </view>
77 + <view class="custom-input-content">
78 + <view class="input-group">
79 + <text class="input-label">品牌名称</text>
80 + <nut-input
81 + v-model="customBrand"
82 + placeholder="请输入品牌名称"
83 + class="custom-input"
84 + />
85 + </view>
86 + <view class="input-group">
87 + <text class="input-label">型号名称</text>
88 + <nut-input
89 + v-model="customModel"
90 + placeholder="请输入型号名称"
91 + class="custom-input"
92 + />
93 + </view>
94 + <view class="button-group">
95 + <nut-button
96 + type="primary"
97 + size="large"
98 + color="orange"
99 + @click="confirmCustomInput"
100 + :disabled="!customBrand.trim() || !customModel.trim()"
101 + class="confirm-button"
102 + >
103 + 确认
104 + </nut-button>
105 + </view>
106 + </view>
107 + </view>
108 + </nut-popup>
68 </view> 109 </view>
69 </template> 110 </template>
70 111
...@@ -80,10 +121,13 @@ const emit = defineEmits(['confirm', 'cancel']) ...@@ -80,10 +121,13 @@ const emit = defineEmits(['confirm', 'cancel'])
80 // 响应式数据 121 // 响应式数据
81 const brandPickerVisible = ref(false) 122 const brandPickerVisible = ref(false)
82 const modelPickerVisible = ref(false) 123 const modelPickerVisible = ref(false)
124 +const customInputVisible = ref(false)
83 const selectedBrandName = ref('') 125 const selectedBrandName = ref('')
84 const currentBrandModels = ref([]) 126 const currentBrandModels = ref([])
85 const contentHeight = ref(800) // 默认高度 rpx 127 const contentHeight = ref(800) // 默认高度 rpx
86 const searchKeyword = ref('') // 搜索关键词 128 const searchKeyword = ref('') // 搜索关键词
129 +const customBrand = ref('') // 自定义品牌
130 +const customModel = ref('') // 自定义型号
87 131
88 const allBrands = ref([ 132 const allBrands = ref([
89 { text: '爱玛', value: 'aima' }, 133 { text: '爱玛', value: 'aima' },
...@@ -105,7 +149,8 @@ const allBrands = ref([ ...@@ -105,7 +149,8 @@ const allBrands = ref([
105 { text: '小刀', value: 'xiaodao' }, 149 { text: '小刀', value: 'xiaodao' },
106 { text: '速派奇', value: 'supaiqi' }, 150 { text: '速派奇', value: 'supaiqi' },
107 { text: '金箭', value: 'jinjian' }, 151 { text: '金箭', value: 'jinjian' },
108 - { text: '森蓝', value: 'senlan' } 152 + { text: '森蓝', value: 'senlan' },
153 + { text: '其他', value: 'other' }
109 ]) 154 ])
110 155
111 // 品牌型号映射 156 // 品牌型号映射
...@@ -261,10 +306,19 @@ const show = () => { ...@@ -261,10 +306,19 @@ const show = () => {
261 306
262 // 选择品牌 307 // 选择品牌
263 const selectBrand = (brand) => { 308 const selectBrand = (brand) => {
264 - selectedBrandName.value = brand.text 309 + if (brand.value === 'other') {
265 - currentBrandModels.value = brandModelMap.value[brand.value] || [] 310 + // 选择其他,显示自定义输入弹窗
266 - brandPickerVisible.value = false 311 + brandPickerVisible.value = false
267 - modelPickerVisible.value = true 312 + customInputVisible.value = true
313 + // 清空之前的自定义输入
314 + customBrand.value = ''
315 + customModel.value = ''
316 + } else {
317 + selectedBrandName.value = brand.text
318 + currentBrandModels.value = brandModelMap.value[brand.value] || []
319 + brandPickerVisible.value = false
320 + modelPickerVisible.value = true
321 + }
268 } 322 }
269 323
270 // 选择型号 324 // 选择型号
...@@ -283,9 +337,32 @@ const selectModel = (model) => { ...@@ -283,9 +337,32 @@ const selectModel = (model) => {
283 closeAllPickers() 337 closeAllPickers()
284 } 338 }
285 339
340 +/**
341 + * 确认自定义输入
342 + */
343 +const confirmCustomInput = () => {
344 + if (!customBrand.value.trim() || !customModel.value.trim()) {
345 + return
346 + }
347 +
348 + const result = {
349 + brand: customBrand.value.trim(),
350 + model: customModel.value.trim(),
351 + brandValue: customBrand.value.trim(),
352 + modelValue: customModel.value.trim()
353 + }
354 +
355 + // 发送确认事件
356 + emit('confirm', result)
357 +
358 + // 关闭所有弹框
359 + closeAllPickers()
360 +}
361 +
286 // 返回品牌选择 362 // 返回品牌选择
287 const goBackToBrandPicker = () => { 363 const goBackToBrandPicker = () => {
288 modelPickerVisible.value = false 364 modelPickerVisible.value = false
365 + customInputVisible.value = false
289 brandPickerVisible.value = true 366 brandPickerVisible.value = true
290 } 367 }
291 368
...@@ -299,8 +376,11 @@ const closePicker = () => { ...@@ -299,8 +376,11 @@ const closePicker = () => {
299 const closeAllPickers = () => { 376 const closeAllPickers = () => {
300 brandPickerVisible.value = false 377 brandPickerVisible.value = false
301 modelPickerVisible.value = false 378 modelPickerVisible.value = false
379 + customInputVisible.value = false
302 selectedBrandName.value = '' 380 selectedBrandName.value = ''
303 currentBrandModels.value = [] 381 currentBrandModels.value = []
382 + customBrand.value = ''
383 + customModel.value = ''
304 } 384 }
305 385
306 // 组件挂载时初始化 386 // 组件挂载时初始化
...@@ -503,4 +583,70 @@ defineExpose({ ...@@ -503,4 +583,70 @@ defineExpose({
503 } 583 }
504 } 584 }
505 } 585 }
586 +
587 +/* 自定义输入弹窗样式 */
588 +.custom-input-picker {
589 + padding: 20rpx;
590 + height: 100%;
591 + display: flex;
592 + flex-direction: column;
593 +
594 + .picker-header {
595 + display: flex;
596 + justify-content: space-between;
597 + align-items: center;
598 + padding: 20rpx 0;
599 + border-bottom: 1rpx solid #f0f0f0;
600 + margin-bottom: 20rpx;
601 +
602 + .picker-title {
603 + font-size: 32rpx;
604 + font-weight: 600;
605 + color: #333;
606 + }
607 + }
608 +
609 + .custom-input-content {
610 + flex: 1;
611 + display: flex;
612 + flex-direction: column;
613 +
614 + .input-group {
615 + margin-bottom: 40rpx;
616 +
617 + .input-label {
618 + display: block;
619 + font-size: 28rpx;
620 + color: #333;
621 + margin-bottom: 20rpx;
622 + font-weight: 500;
623 + }
624 +
625 + .custom-input {
626 + width: 100%;
627 + height: 80rpx;
628 + border: 1rpx solid #e0e0e0;
629 + border-radius: 8rpx;
630 + padding: 0 20rpx;
631 + font-size: 28rpx;
632 + background: #fff;
633 +
634 + &:focus {
635 + border-color: #1890ff;
636 + }
637 + }
638 + }
639 +
640 + .button-group {
641 + margin-top: auto;
642 + padding-top: 40rpx;
643 +
644 + .confirm-button {
645 + width: 100%;
646 + height: 80rpx;
647 + border-radius: 8rpx;
648 + }
649 + }
650 + }
651 +}
506 </style> 652 </style>
......
...@@ -121,8 +121,7 @@ ...@@ -121,8 +121,7 @@
121 </nut-form-item>--> 121 </nut-form-item>-->
122 122
123 <!-- 品牌型号选择(新版) --> 123 <!-- 品牌型号选择(新版) -->
124 - <nut-form-item label-position="top" label="品牌型号选择" prop="brandModel" required 124 + <nut-form-item label-position="top" label="品牌型号选择" prop="brandModel">
125 - :rules="[{ required: true, message: '请选择品牌型号' }]">
126 <view class="form-item-content" @click="showBrandModelPicker"> 125 <view class="form-item-content" @click="showBrandModelPicker">
127 <text class="form-value"> 126 <text class="form-value">
128 {{ formData.brand && formData.model ? `${formData.brand} ${formData.model}` : '请选择品牌型号' }} 127 {{ formData.brand && formData.model ? `${formData.brand} ${formData.model}` : '请选择品牌型号' }}
...@@ -295,10 +294,10 @@ ...@@ -295,10 +294,10 @@
295 </nut-popup> 294 </nut-popup>
296 295
297 <!-- 电池损耗度选择 --> 296 <!-- 电池损耗度选择 -->
298 - <nut-popup v-model:visible="batteryWearPickerVisible" position="bottom"> 297 + <!-- <nut-popup v-model:visible="batteryWearPickerVisible" position="bottom">
299 <nut-picker v-model="batteryWearValue" :columns="wearLevelOptions" title="选择电池损耗度" 298 <nut-picker v-model="batteryWearValue" :columns="wearLevelOptions" title="选择电池损耗度"
300 @confirm="onBatteryWearConfirm" @cancel="batteryWearPickerVisible = false" /> 299 @confirm="onBatteryWearConfirm" @cancel="batteryWearPickerVisible = false" />
301 - </nut-popup> 300 + </nut-popup> -->
302 301
303 <!-- 刹车磨损度选择 --> 302 <!-- 刹车磨损度选择 -->
304 <nut-popup v-model:visible="brakeWearPickerVisible" position="bottom"> 303 <nut-popup v-model:visible="brakeWearPickerVisible" position="bottom">
...@@ -324,7 +323,8 @@ import { checkPermission, PERMISSION_TYPES } from '@/utils/permission' ...@@ -324,7 +323,8 @@ import { checkPermission, PERMISSION_TYPES } from '@/utils/permission'
324 import './index.less' 323 import './index.less'
325 324
326 // 导入接口 325 // 导入接口
327 -import { addVehicleAPI, editVehicleAPI, getVehicleDetailAPI } from '@/api/car'; 326 +// import { addVehicleAPI, editVehicleAPI, getVehicleDetailAPI } from '@/api/car';
327 +import { addVehicleAPI } from '@/api/car';
328 328
329 const themeVars = ref({ 329 const themeVars = ref({
330 navbarBackground: '#fb923c', 330 navbarBackground: '#fb923c',
...@@ -377,12 +377,17 @@ const formData = reactive({ ...@@ -377,12 +377,17 @@ const formData = reactive({
377 range_km: '60', 377 range_km: '60',
378 maxSpeed: '25', 378 maxSpeed: '25',
379 battery_capacity_ah: '20', 379 battery_capacity_ah: '20',
380 - batteryWear: '', 380 + // batteryWear: '',
381 brake_wear_level: '', 381 brake_wear_level: '',
382 tire_wear_level: '', 382 tire_wear_level: '',
383 price: '3200', 383 price: '3200',
384 market_price: '6500', 384 market_price: '6500',
385 - note: '' 385 + note: '',
386 + // 车辆照片字段
387 + front_photo: '',
388 + left_photo: '',
389 + right_photo: '',
390 + other_photo: ''
386 }) 391 })
387 392
388 // 选择器显示状态 393 // 选择器显示状态
...@@ -391,7 +396,7 @@ const brandPickerVisible = ref(false) ...@@ -391,7 +396,7 @@ const brandPickerVisible = ref(false)
391 const modelPickerVisible = ref(false) 396 const modelPickerVisible = ref(false)
392 const yearPickerVisible = ref(false) 397 const yearPickerVisible = ref(false)
393 const conditionPickerVisible = ref(false) 398 const conditionPickerVisible = ref(false)
394 -const batteryWearPickerVisible = ref(false) 399 +// const batteryWearPickerVisible = ref(false)
395 const brakeWearPickerVisible = ref(false) 400 const brakeWearPickerVisible = ref(false)
396 const tireWearPickerVisible = ref(false) 401 const tireWearPickerVisible = ref(false)
397 402
...@@ -405,7 +410,7 @@ const brandValue = ref([]) ...@@ -405,7 +410,7 @@ const brandValue = ref([])
405 const modelValue = ref([]) 410 const modelValue = ref([])
406 const yearValue = ref(new Date()) 411 const yearValue = ref(new Date())
407 const conditionValue = ref([]) 412 const conditionValue = ref([])
408 -const batteryWearValue = ref([]) 413 +// const batteryWearValue = ref([])
409 const brakeWearValue = ref([]) 414 const brakeWearValue = ref([])
410 const tireWearValue = ref([]) 415 const tireWearValue = ref([])
411 416
...@@ -508,6 +513,8 @@ const uploadImage = (filePath, type) => { ...@@ -508,6 +513,8 @@ const uploadImage = (filePath, type) => {
508 success: () => { 513 success: () => {
509 if (res.statusCode === 200) { 514 if (res.statusCode === 200) {
510 uploadedImages[type] = upload_data.data.src; 515 uploadedImages[type] = upload_data.data.src;
516 + // 同时更新formData中对应的照片字段
517 + formData[`${type}_photo`] = upload_data.data.src;
511 Taro.showToast({ 518 Taro.showToast({
512 title: '上传成功', 519 title: '上传成功',
513 icon: 'success' 520 icon: 'success'
...@@ -552,6 +559,8 @@ const previewImage = (imageUrl) => { ...@@ -552,6 +559,8 @@ const previewImage = (imageUrl) => {
552 */ 559 */
553 const deleteImage = (type) => { 560 const deleteImage = (type) => {
554 uploadedImages[type] = '' 561 uploadedImages[type] = ''
562 + // 同时清空formData中对应的照片字段
563 + formData[`${type}_photo`] = ''
555 Taro.showToast({ 564 Taro.showToast({
556 title: '删除成功', 565 title: '删除成功',
557 icon: 'success' 566 icon: 'success'
...@@ -665,10 +674,10 @@ const onConditionConfirm = ({ selectedValue }) => { ...@@ -665,10 +674,10 @@ const onConditionConfirm = ({ selectedValue }) => {
665 /** 674 /**
666 * 电池损耗度选择确认 675 * 电池损耗度选择确认
667 */ 676 */
668 -const onBatteryWearConfirm = ({ selectedValue }) => { 677 +// const onBatteryWearConfirm = ({ selectedValue }) => {
669 - formData.batteryWear = selectedValue[0] 678 +// formData.batteryWear = selectedValue[0]
670 - batteryWearPickerVisible.value = false 679 +// batteryWearPickerVisible.value = false
671 -} 680 +// }
672 681
673 /** 682 /**
674 * 刹车磨损度选择确认 683 * 刹车磨损度选择确认
...@@ -716,6 +725,58 @@ const onBrandModelCancel = () => { ...@@ -716,6 +725,58 @@ const onBrandModelCancel = () => {
716 // 可以在这里处理取消逻辑 725 // 可以在这里处理取消逻辑
717 } 726 }
718 727
728 +/**
729 + * 将文字转换为对应的数字分数
730 + */
731 +const convertTextToScore = (text, type) => {
732 + if (type === 'condition') {
733 + const conditionMap = {
734 + '全新': 5,
735 + '8成新': 4,
736 + '7成新': 3,
737 + '6成新': 2,
738 + '5成新及以下': 1
739 + }
740 + return conditionMap[text] || 0
741 + } else if (type === 'wear') {
742 + const wearMap = {
743 + '全新': 5,
744 + '轻微磨损': 4,
745 + '中度磨损': 3,
746 + '重度磨损': 2,
747 + '需要更换': 1
748 + }
749 + return wearMap[text] || 0
750 + }
751 + return 0
752 +}
753 +
754 +/**
755 + * 将数字分数转换为对应的文字
756 + */
757 +const convertScoreToText = (score, type) => {
758 + if (type === 'condition') {
759 + const scoreMap = {
760 + 5: '全新',
761 + 4: '8成新',
762 + 3: '7成新',
763 + 2: '6成新',
764 + 1: '5成新及以下'
765 + }
766 + return scoreMap[score] || ''
767 + } else if (type === 'wear') {
768 + const scoreMap = {
769 + 5: '全新',
770 + 4: '轻微磨损',
771 + 3: '中度磨损',
772 + 2: '重度磨损',
773 + 1: '需要更换'
774 + }
775 + return scoreMap[score] || ''
776 + }
777 + return ''
778 +}
779 +
719 const createCar = async (data) => { 780 const createCar = async (data) => {
720 const { code } = await addVehicleAPI({ ...data }) 781 const { code } = await addVehicleAPI({ ...data })
721 if (code) { 782 if (code) {
...@@ -747,19 +808,12 @@ const onPublish = async () => { ...@@ -747,19 +808,12 @@ const onPublish = async () => {
747 const loadingTitle = isEditMode.value ? '保存中...' : '发布中...' 808 const loadingTitle = isEditMode.value ? '保存中...' : '发布中...'
748 Taro.showLoading({ title: loadingTitle }) 809 Taro.showLoading({ title: loadingTitle })
749 810
750 - // 收集所有上传的图片URL 811 + // 构建提交数据,将文字转换为数字
751 - const images = {
752 - front: uploadedImages.front || '',
753 - left: uploadedImages.left || '',
754 - right: uploadedImages.right || '',
755 - other: uploadedImages.other || ''
756 - }
757 -
758 - // 构建提交数据
759 const submitData = { 812 const submitData = {
760 ...formData, 813 ...formData,
761 - images: images, 814 + new_level: convertTextToScore(formData.new_level, 'condition'),
762 - photo_meta_ids: Object.values(images).filter(url => url) // 过滤空URL 815 + brake_wear_level: convertTextToScore(formData.brake_wear_level, 'wear'),
816 + tire_wear_level: convertTextToScore(formData.tire_wear_level, 'wear')
763 } 817 }
764 818
765 console.warn(submitData); 819 console.warn(submitData);
...@@ -801,16 +855,16 @@ const onPublish = async () => { ...@@ -801,16 +855,16 @@ const onPublish = async () => {
801 */ 855 */
802 const validateForm = () => { 856 const validateForm = () => {
803 // 检查是否至少上传了一张图片 857 // 检查是否至少上传了一张图片
804 - const hasImages = uploadedImages.front || uploadedImages.left || uploadedImages.right || uploadedImages.other 858 + const hasImages = formData.front_photo || formData.left_photo || formData.right_photo || formData.other_photo
805 if (!hasImages) { 859 if (!hasImages) {
806 Taro.showToast({ title: '请上传车辆图片', icon: 'none' }) 860 Taro.showToast({ title: '请上传车辆图片', icon: 'none' })
807 return false 861 return false
808 } 862 }
809 863
810 - if (!formData.brand) { 864 + // if (!formData.brand) {
811 - Taro.showToast({ title: '请选择车型品牌', icon: 'none' }) 865 + // Taro.showToast({ title: '请选择车型品牌', icon: 'none' })
812 - return false 866 + // return false
813 - } 867 + // }
814 868
815 // if (!formData.new_level) { 869 // if (!formData.new_level) {
816 // Taro.showToast({ title: '请选择新旧程度', icon: 'none' }) 870 // Taro.showToast({ title: '请选择新旧程度', icon: 'none' })
...@@ -853,7 +907,7 @@ const loadCarData = async () => { ...@@ -853,7 +907,7 @@ const loadCarData = async () => {
853 range_km: '60', 907 range_km: '60',
854 maxSpeed: '25', 908 maxSpeed: '25',
855 battery_capacity_ah: '20', 909 battery_capacity_ah: '20',
856 - batteryWear: '轻微磨损', 910 + // batteryWear: '轻微磨损',
857 brake_wear_level: '轻微磨损', 911 brake_wear_level: '轻微磨损',
858 tire_wear_level: '轻微磨损', 912 tire_wear_level: '轻微磨损',
859 price: '3200', 913 price: '3200',
...@@ -867,20 +921,20 @@ const loadCarData = async () => { ...@@ -867,20 +921,20 @@ const loadCarData = async () => {
867 } 921 }
868 } 922 }
869 923
870 - // 填充表单数据 924 + // 填充表单数据,如果是数字则转换为文字
871 Object.assign(formData, { 925 Object.assign(formData, {
872 school: mockCarData.school, 926 school: mockCarData.school,
873 brand: mockCarData.brand, 927 brand: mockCarData.brand,
874 model: mockCarData.model, 928 model: mockCarData.model,
875 manufacture_year: mockCarData.manufacture_year, 929 manufacture_year: mockCarData.manufacture_year,
876 - new_level: mockCarData.new_level, 930 + new_level: typeof mockCarData.new_level === 'number' ? convertScoreToText(mockCarData.new_level, 'condition') : mockCarData.new_level,
877 total_mileage_km: mockCarData.total_mileage_km, 931 total_mileage_km: mockCarData.total_mileage_km,
878 range_km: mockCarData.range_km, 932 range_km: mockCarData.range_km,
879 maxSpeed: mockCarData.maxSpeed, 933 maxSpeed: mockCarData.maxSpeed,
880 battery_capacity_ah: mockCarData.battery_capacity_ah, 934 battery_capacity_ah: mockCarData.battery_capacity_ah,
881 - batteryWear: mockCarData.batteryWear, 935 + // batteryWear: mockCarData.batteryWear,
882 - brake_wear_level: mockCarData.brake_wear_level, 936 + brake_wear_level: typeof mockCarData.brake_wear_level === 'number' ? convertScoreToText(mockCarData.brake_wear_level, 'wear') : mockCarData.brake_wear_level,
883 - tire_wear_level: mockCarData.tire_wear_level, 937 + tire_wear_level: typeof mockCarData.tire_wear_level === 'number' ? convertScoreToText(mockCarData.tire_wear_level, 'wear') : mockCarData.tire_wear_level,
884 price: mockCarData.price, 938 price: mockCarData.price,
885 market_price: mockCarData.market_price, 939 market_price: mockCarData.market_price,
886 note: mockCarData.note 940 note: mockCarData.note
...@@ -888,6 +942,11 @@ const loadCarData = async () => { ...@@ -888,6 +942,11 @@ const loadCarData = async () => {
888 942
889 // 填充图片数据 943 // 填充图片数据
890 Object.assign(uploadedImages, mockCarData.images) 944 Object.assign(uploadedImages, mockCarData.images)
945 + // 同时填充formData中的照片字段
946 + formData.front_photo = mockCarData.images.front || ''
947 + formData.left_photo = mockCarData.images.left || ''
948 + formData.right_photo = mockCarData.images.right || ''
949 + formData.other_photo = mockCarData.images.other || ''
891 950
892 Taro.hideLoading() 951 Taro.hideLoading()
893 } catch (error) { 952 } catch (error) {
......