hookehuyr

feat(表单): 添加品牌型号选择器组件并集成到销售表单

- 新增 BrandModelPicker 组件用于统一选择品牌和型号
- 替换原表单中分开的品牌和型号选择字段
- 添加表单验证逻辑和组合字段显示
...@@ -7,6 +7,7 @@ export {} ...@@ -7,6 +7,7 @@ export {}
7 7
8 declare module 'vue' { 8 declare module 'vue' {
9 export interface GlobalComponents { 9 export interface GlobalComponents {
10 + BrandModelPicker: typeof import('./src/components/BrandModelPicker.vue')['default']
10 NavBar: typeof import('./src/components/navBar.vue')['default'] 11 NavBar: typeof import('./src/components/navBar.vue')['default']
11 NutActionSheet: typeof import('@nutui/nutui-taro')['ActionSheet'] 12 NutActionSheet: typeof import('@nutui/nutui-taro')['ActionSheet']
12 NutButton: typeof import('@nutui/nutui-taro')['Button'] 13 NutButton: typeof import('@nutui/nutui-taro')['Button']
......
1 +<template>
2 + <view>
3 + <!-- 品牌选择弹框 -->
4 + <nut-popup v-model:visible="brandPickerVisible" position="bottom" :style="{ height: '80%' }">
5 + <view class="brand-model-picker">
6 + <view class="picker-header">
7 + <text class="picker-title">选择品牌</text>
8 + <nut-button size="small" type="primary" @click="closePicker">关闭</nut-button>
9 + </view>
10 + <view class="brand-container">
11 + <scroll-view
12 + class="brand-content"
13 + :scroll-y="true"
14 + :style="{ height: contentHeight + 'rpx' }"
15 + ref="brandContentRef"
16 + >
17 + <view class="brand-list">
18 + <view
19 + v-for="brand in allBrands"
20 + :key="brand.value"
21 + class="brand-item"
22 + @click="selectBrand(brand)"
23 + >
24 + <view class="brand-info">
25 + <view class="brand-icon">
26 + <text class="brand-initial">{{ brand.text.charAt(0) }}</text>
27 + </view>
28 + <text class="brand-name">{{ brand.text }}</text>
29 + </view>
30 + <Right class="brand-arrow" />
31 + </view>
32 + </view>
33 + </scroll-view>
34 + </view>
35 + </view>
36 + </nut-popup>
37 +
38 + <!-- 型号选择弹框 -->
39 + <nut-popup v-model:visible="modelPickerVisible" position="bottom" :style="{ height: '50%' }">
40 + <view class="model-picker">
41 + <view class="picker-header">
42 + <nut-button size="small" @click="goBackToBrandPicker">返回</nut-button>
43 + <text class="picker-title">选择{{ selectedBrandName }}型号</text>
44 + <nut-button size="small" type="primary" @click="closePicker">关闭</nut-button>
45 + </view>
46 + <view class="model-list">
47 + <view
48 + v-for="model in currentBrandModels"
49 + :key="model.value"
50 + class="model-item"
51 + @click="selectModel(model)"
52 + >
53 + <text class="model-name">{{ model.text }}</text>
54 + </view>
55 + </view>
56 + </view>
57 + </nut-popup>
58 + </view>
59 +</template>
60 +
61 +<script setup>
62 +import { ref } from 'vue'
63 +import { Right } from '@nutui/icons-vue-taro'
64 +
65 +// 定义事件
66 +const emit = defineEmits(['confirm', 'cancel'])
67 +
68 +// 响应式数据
69 +const brandPickerVisible = ref(false)
70 +const modelPickerVisible = ref(false)
71 +const selectedBrandName = ref('')
72 +const currentBrandModels = ref([])
73 +const contentHeight = ref(800) // 默认高度 rpx
74 +
75 +const allBrands = ref([
76 + { text: '爱玛', value: 'aima' },
77 + { text: '雅迪', value: 'yadi' },
78 + { text: '小牛', value: 'xiaoniu' },
79 + { text: '台铃', value: 'tailing' },
80 + { text: '绿源', value: 'lvyuan' },
81 + { text: '立马', value: 'lima' },
82 + { text: '新日', value: 'xinri' },
83 + { text: '宗申', value: 'zongshen' },
84 + { text: '奇瑞', value: 'qirui' },
85 + { text: '比德文', value: 'bidewen' },
86 + { text: '欧派', value: 'oupai' },
87 + { text: '捷安特', value: 'jiante' },
88 + { text: '美利达', value: 'meilida' },
89 + { text: '凤凰', value: 'fenghuang' },
90 + { text: '永久', value: 'yongjiu' },
91 + { text: '飞鸽', value: 'feige' },
92 + { text: '小刀', value: 'xiaodao' },
93 + { text: '速派奇', value: 'supaiqi' },
94 + { text: '金箭', value: 'jinjian' },
95 + { text: '森蓝', value: 'senlan' }
96 +])
97 +
98 +// 品牌型号映射
99 +const brandModelMap = ref({
100 + 'aima': [
101 + { text: 'A1 Pro', value: 'aima_a1_pro' },
102 + { text: 'A2 Max', value: 'aima_a2_max' },
103 + { text: 'A3 Plus', value: 'aima_a3_plus' },
104 + { text: 'A4 Elite', value: 'aima_a4_elite' }
105 + ],
106 + 'yadi': [
107 + { text: 'Y1 智享版', value: 'yadi_y1_smart' },
108 + { text: 'Y2 豪华版', value: 'yadi_y2_luxury' },
109 + { text: 'Y3 运动版', value: 'yadi_y3_sport' },
110 + { text: 'Y4 旗舰版', value: 'yadi_y4_flagship' }
111 + ],
112 + 'xiaoniu': [
113 + { text: 'N1S', value: 'xiaoniu_n1s' },
114 + { text: 'N-GT', value: 'xiaoniu_ngt' },
115 + { text: 'NGT Pro', value: 'xiaoniu_ngt_pro' },
116 + { text: 'U1 Pro', value: 'xiaoniu_u1_pro' }
117 + ],
118 + 'tailing': [
119 + { text: 'T1 经典版', value: 'tailing_t1_classic' },
120 + { text: 'T2 时尚版', value: 'tailing_t2_fashion' },
121 + { text: 'T3 豪华版', value: 'tailing_t3_luxury' }
122 + ],
123 + 'lvyuan': [
124 + { text: 'L1 标准版', value: 'lvyuan_l1_standard' },
125 + { text: 'L2 升级版', value: 'lvyuan_l2_upgrade' },
126 + { text: 'L3 旗舰版', value: 'lvyuan_l3_flagship' }
127 + ],
128 + 'lima': [
129 + { text: 'LM-1', value: 'lima_lm1' },
130 + { text: 'LM-2', value: 'lima_lm2' },
131 + { text: 'LM-3 Pro', value: 'lima_lm3_pro' }
132 + ],
133 + 'xinri': [
134 + { text: 'XR-1', value: 'xinri_xr1' },
135 + { text: 'XR-2 Plus', value: 'xinri_xr2_plus' },
136 + { text: 'XR-3 Max', value: 'xinri_xr3_max' }
137 + ],
138 + 'zongshen': [
139 + { text: 'ZS-1', value: 'zongshen_zs1' },
140 + { text: 'ZS-2 Pro', value: 'zongshen_zs2_pro' }
141 + ],
142 + 'qirui': [
143 + { text: 'QR-1', value: 'qirui_qr1' },
144 + { text: 'QR-2 智能版', value: 'qirui_qr2_smart' }
145 + ],
146 + 'bidewen': [
147 + { text: 'BD-1', value: 'bidewen_bd1' },
148 + { text: 'BD-2 Plus', value: 'bidewen_bd2_plus' }
149 + ],
150 + 'oupai': [
151 + { text: 'OP-1', value: 'oupai_op1' },
152 + { text: 'OP-2 Pro', value: 'oupai_op2_pro' }
153 + ],
154 + 'jiante': [
155 + { text: 'JT-1', value: 'jiante_jt1' },
156 + { text: 'JT-2 运动版', value: 'jiante_jt2_sport' }
157 + ],
158 + 'meilida': [
159 + { text: 'MD-1', value: 'meilida_md1' },
160 + { text: 'MD-2 Pro', value: 'meilida_md2_pro' }
161 + ],
162 + 'fenghuang': [
163 + { text: 'FH-1 经典', value: 'fenghuang_fh1_classic' },
164 + { text: 'FH-2 现代', value: 'fenghuang_fh2_modern' }
165 + ],
166 + 'yongjiu': [
167 + { text: 'YJ-1', value: 'yongjiu_yj1' },
168 + { text: 'YJ-2 Plus', value: 'yongjiu_yj2_plus' }
169 + ],
170 + 'feige': [
171 + { text: 'FG-1', value: 'feige_fg1' },
172 + { text: 'FG-2 Pro', value: 'feige_fg2_pro' }
173 + ],
174 + 'xiaodao': [
175 + { text: 'XD-1', value: 'xiaodao_xd1' },
176 + { text: 'XD-2 Max', value: 'xiaodao_xd2_max' }
177 + ],
178 + 'supaiqi': [
179 + { text: 'SP-1', value: 'supaiqi_sp1' },
180 + { text: 'SP-2 Pro', value: 'supaiqi_sp2_pro' }
181 + ],
182 + 'jinjian': [
183 + { text: 'JJ-1', value: 'jinjian_jj1' },
184 + { text: 'JJ-2 Plus', value: 'jinjian_jj2_plus' }
185 + ],
186 + 'senlan': [
187 + { text: 'SL-1', value: 'senlan_sl1' },
188 + { text: 'SL-2 Pro', value: 'senlan_sl2_pro' }
189 + ]
190 +})
191 +
192 +// 计算内容高度
193 +const calculateContentHeight = () => {
194 + // 弹框总高度 - 头部高度
195 + const popupHeight = 1200 // rpx
196 + const headerHeight = 120 // rpx (头部标题和按钮的高度)
197 + contentHeight.value = popupHeight - headerHeight
198 +}
199 +
200 +// 显示品牌选择器
201 +const show = () => {
202 + calculateContentHeight() // 计算内容高度
203 + brandPickerVisible.value = true
204 +}
205 +
206 +// 选择品牌
207 +const selectBrand = (brand) => {
208 + selectedBrandName.value = brand.text
209 + currentBrandModels.value = brandModelMap.value[brand.value] || []
210 + brandPickerVisible.value = false
211 + modelPickerVisible.value = true
212 +}
213 +
214 +// 选择型号
215 +const selectModel = (model) => {
216 + const result = {
217 + brand: selectedBrandName.value,
218 + model: model.text,
219 + brandValue: selectedBrandName.value,
220 + modelValue: model.value
221 + }
222 +
223 + // 发送确认事件
224 + emit('confirm', result)
225 +
226 + // 关闭所有弹框
227 + closeAllPickers()
228 +}
229 +
230 +// 返回品牌选择
231 +const goBackToBrandPicker = () => {
232 + modelPickerVisible.value = false
233 + brandPickerVisible.value = true
234 +}
235 +
236 +// 关闭选择器
237 +const closePicker = () => {
238 + closeAllPickers()
239 + emit('cancel')
240 +}
241 +
242 +// 关闭所有弹框
243 +const closeAllPickers = () => {
244 + brandPickerVisible.value = false
245 + modelPickerVisible.value = false
246 + selectedBrandName.value = ''
247 + currentBrandModels.value = []
248 +}
249 +
250 +// 暴露方法给父组件
251 +defineExpose({
252 + show,
253 + close: closeAllPickers
254 +})
255 +</script>
256 +
257 +<style lang="less">
258 +.brand-model-picker {
259 + padding: 20rpx;
260 + height: 100%;
261 + display: flex;
262 + flex-direction: column;
263 +
264 + .picker-header {
265 + display: flex;
266 + justify-content: space-between;
267 + align-items: center;
268 + padding: 20rpx 0;
269 + border-bottom: 1rpx solid #f0f0f0;
270 + margin-bottom: 20rpx;
271 +
272 + .picker-title {
273 + font-size: 32rpx;
274 + font-weight: 600;
275 + color: #333;
276 + }
277 + }
278 +
279 + .brand-container {
280 + flex: 1;
281 + height: 100%;
282 +
283 + .brand-content {
284 + background-color: #f8f9fa;
285 +
286 + .brand-list {
287 + padding: 20rpx;
288 +
289 + .brand-item {
290 + display: flex;
291 + align-items: center;
292 + justify-content: space-between;
293 + padding: 24rpx 30rpx;
294 + margin-bottom: 16rpx;
295 + background-color: white;
296 + border-radius: 16rpx;
297 + box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
298 + transition: all 0.3s ease;
299 +
300 + &:active {
301 + transform: scale(0.98);
302 + box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
303 + }
304 +
305 + &:last-child {
306 + margin-bottom: 0;
307 + }
308 +
309 + .brand-info {
310 + display: flex;
311 + align-items: center;
312 +
313 + .brand-icon {
314 + width: 80rpx;
315 + height: 80rpx;
316 + border-radius: 40rpx;
317 + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
318 + display: flex;
319 + align-items: center;
320 + justify-content: center;
321 + margin-right: 24rpx;
322 +
323 + .brand-initial {
324 + font-size: 32rpx;
325 + color: white;
326 + font-weight: bold;
327 + }
328 + }
329 +
330 + .brand-name {
331 + font-size: 32rpx;
332 + color: #333;
333 + font-weight: 500;
334 + }
335 + }
336 +
337 + .brand-arrow {
338 + width: 32rpx;
339 + height: 32rpx;
340 + color: #ccc;
341 + }
342 + }
343 + }
344 + }
345 + }
346 +}
347 +
348 +.model-picker {
349 + padding: 20rpx;
350 + height: 100%;
351 + display: flex;
352 + flex-direction: column;
353 +
354 + .picker-header {
355 + display: flex;
356 + justify-content: space-between;
357 + align-items: center;
358 + padding: 20rpx 0;
359 + border-bottom: 1rpx solid #f0f0f0;
360 + margin-bottom: 20rpx;
361 +
362 + .picker-title {
363 + font-size: 32rpx;
364 + font-weight: 600;
365 + color: #333;
366 + }
367 + }
368 +
369 + .model-list {
370 + flex: 1;
371 + overflow-y: auto;
372 +
373 + .model-item {
374 + display: flex;
375 + align-items: center;
376 + padding: 30rpx 20rpx;
377 + border-bottom: 1rpx solid #f8f8f8;
378 + transition: background-color 0.2s;
379 +
380 + &:active {
381 + background-color: #f5f5f5;
382 + }
383 +
384 + .model-name {
385 + font-size: 30rpx;
386 + color: #333;
387 + }
388 + }
389 + }
390 +}
391 +</style>
...@@ -103,20 +103,32 @@ ...@@ -103,20 +103,32 @@
103 <!-- 车辆详情表单 --> 103 <!-- 车辆详情表单 -->
104 <nut-form ref="formRef" :model-value="formData"> 104 <nut-form ref="formRef" :model-value="formData">
105 <view class="form-section"> 105 <view class="form-section">
106 - <!-- 车型品牌 --> 106 + <!--<!~~ 车型品牌 ~~>
107 - <nut-form-item label-position="top" label="车型品牌" prop="brand" required :rules="[{ required: true, message: '请选择车型品牌' }]"> 107 + <nut-form-item label-position="top" label="车型品牌" prop="brand" required
108 + :rules="[{ required: true, message: '请选择车型品牌' }]">
108 <view class="form-item-content" @click="showBrandPicker"> 109 <view class="form-item-content" @click="showBrandPicker">
109 <text class="form-value">{{ formData.brand || '请选择' }}</text> 110 <text class="form-value">{{ formData.brand || '请选择' }}</text>
110 <Right class="arrow-icon" /> 111 <Right class="arrow-icon" />
111 </view> 112 </view>
112 </nut-form-item> 113 </nut-form-item>
113 114
114 - <!-- 车辆型号 --> 115 + <!~~ 车辆型号 ~~>
115 <nut-form-item label-position="top" label="车辆型号" prop="model"> 116 <nut-form-item label-position="top" label="车辆型号" prop="model">
116 <view class="form-item-content" @click="showModelPicker"> 117 <view class="form-item-content" @click="showModelPicker">
117 <text class="form-value">{{ formData.model || '请选择' }}</text> 118 <text class="form-value">{{ formData.model || '请选择' }}</text>
118 <Right class="arrow-icon" /> 119 <Right class="arrow-icon" />
119 </view> 120 </view>
121 + </nut-form-item>-->
122 +
123 + <!-- 品牌型号选择(新版) -->
124 + <nut-form-item label-position="top" label="品牌型号选择" prop="brandModel" required
125 + :rules="[{ required: true, message: '请选择品牌型号' }]">
126 + <view class="form-item-content" @click="showBrandModelPicker">
127 + <text class="form-value">
128 + {{ formData.brand && formData.model ? `${formData.brand} ${formData.model}` : '请选择品牌型号' }}
129 + </text>
130 + <Right class="arrow-icon" />
131 + </view>
120 </nut-form-item> 132 </nut-form-item>
121 133
122 <!-- 车辆出厂年份 --> 134 <!-- 车辆出厂年份 -->
...@@ -243,12 +255,14 @@ ...@@ -243,12 +255,14 @@
243 </view> 255 </view>
244 256
245 <!-- 选择器弹窗 --> 257 <!-- 选择器弹窗 -->
246 - <!-- 学校选择 --> 258 + <!-- 学校选择 -->
247 <nut-popup v-model:visible="schoolPickerVisible" position="bottom"> 259 <nut-popup v-model:visible="schoolPickerVisible" position="bottom">
248 <nut-picker v-model="schoolValue" :columns="schoolOptions" title="选择学校" @confirm="onSchoolConfirm" 260 <nut-picker v-model="schoolValue" :columns="schoolOptions" title="选择学校" @confirm="onSchoolConfirm"
249 @cancel="schoolPickerVisible = false" /> 261 @cancel="schoolPickerVisible = false" />
250 </nut-popup> 262 </nut-popup>
251 263
264 + <!-- TODO: 如果车型品牌选择其他,车辆型号选择其他型号,允许用户自己填写自定义的内容, 需要等待真实数据后再考虑 -->
265 +
252 <!-- 品牌选择 --> 266 <!-- 品牌选择 -->
253 <nut-popup v-model:visible="brandPickerVisible" position="bottom"> 267 <nut-popup v-model:visible="brandPickerVisible" position="bottom">
254 <nut-picker v-model="brandValue" :columns="brandOptions" title="选择车型品牌" @confirm="onBrandConfirm" 268 <nut-picker v-model="brandValue" :columns="brandOptions" title="选择车型品牌" @confirm="onBrandConfirm"
...@@ -261,6 +275,9 @@ ...@@ -261,6 +275,9 @@
261 @cancel="modelPickerVisible = false" /> 275 @cancel="modelPickerVisible = false" />
262 </nut-popup> 276 </nut-popup>
263 277
278 + <!-- 品牌型号选择器组件 -->
279 + <BrandModelPicker ref="brandModelPickerRef" @confirm="onBrandModelConfirm" @cancel="onBrandModelCancel" />
280 +
264 <!-- 年份选择 --> 281 <!-- 年份选择 -->
265 <nut-popup v-model:visible="yearPickerVisible" position="bottom"> 282 <nut-popup v-model:visible="yearPickerVisible" position="bottom">
266 <nut-picker v-model="yearValue" :columns="yearOptions" title="选择出厂年份" @confirm="onYearConfirm" 283 <nut-picker v-model="yearValue" :columns="yearOptions" title="选择出厂年份" @confirm="onYearConfirm"
...@@ -298,6 +315,7 @@ import { ref, reactive, onMounted } from 'vue' ...@@ -298,6 +315,7 @@ import { ref, reactive, onMounted } from 'vue'
298 import { Plus, Right, Location, Close, RectLeft } from '@nutui/icons-vue-taro' 315 import { Plus, Right, Location, Close, RectLeft } from '@nutui/icons-vue-taro'
299 import Taro from '@tarojs/taro' 316 import Taro from '@tarojs/taro'
300 import BASE_URL from '@/utils/config'; 317 import BASE_URL from '@/utils/config';
318 +import BrandModelPicker from '@/components/BrandModelPicker.vue'
301 import './index.less' 319 import './index.less'
302 320
303 const themeVars = ref({ 321 const themeVars = ref({
...@@ -350,6 +368,7 @@ const formData = reactive({ ...@@ -350,6 +368,7 @@ const formData = reactive({
350 school: '', 368 school: '',
351 brand: '', 369 brand: '',
352 model: '', 370 model: '',
371 + brandModel: '', // 品牌型号组合字段,用于表单验证
353 year: '', 372 year: '',
354 condition: '', 373 condition: '',
355 mileage: '1200', 374 mileage: '1200',
...@@ -374,6 +393,10 @@ const batteryWearPickerVisible = ref(false) ...@@ -374,6 +393,10 @@ const batteryWearPickerVisible = ref(false)
374 const brakeWearPickerVisible = ref(false) 393 const brakeWearPickerVisible = ref(false)
375 const tireWearPickerVisible = ref(false) 394 const tireWearPickerVisible = ref(false)
376 395
396 +// 新的品牌型号选择器状态
397 +// 品牌型号选择器组件引用
398 +const brandModelPickerRef = ref(null)
399 +
377 // 选择器值 400 // 选择器值
378 const schoolValue = ref([]) 401 const schoolValue = ref([])
379 const brandValue = ref([]) 402 const brandValue = ref([])
...@@ -444,6 +467,8 @@ const wearLevelOptions = ref([ ...@@ -444,6 +467,8 @@ const wearLevelOptions = ref([
444 { text: '需要更换', value: '需要更换' } 467 { text: '需要更换', value: '需要更换' }
445 ]) 468 ])
446 469
470 +// 品牌型号数据已移至 BrandModelPicker 组件中
471 +
447 472
448 473
449 /** 474 /**
...@@ -508,7 +533,7 @@ const uploadImage = (filePath, type) => { ...@@ -508,7 +533,7 @@ const uploadImage = (filePath, type) => {
508 } 533 }
509 }); 534 });
510 }, 535 },
511 - fail: function (res) { 536 + fail: function () {
512 Taro.hideLoading({ 537 Taro.hideLoading({
513 success: () => { 538 success: () => {
514 Taro.showToast({ 539 Taro.showToast({
...@@ -682,6 +707,36 @@ const onTireWearConfirm = ({ selectedValue }) => { ...@@ -682,6 +707,36 @@ const onTireWearConfirm = ({ selectedValue }) => {
682 } 707 }
683 708
684 /** 709 /**
710 + * 显示品牌型号选择器
711 + */
712 +const showBrandModelPicker = () => {
713 + brandModelPickerRef.value?.show()
714 +}
715 +
716 +/**
717 + * 品牌型号选择确认回调
718 + */
719 +const onBrandModelConfirm = (result) => {
720 + formData.brand = result.brand
721 + formData.model = result.model
722 + // 设置组合字段用于表单验证
723 + formData.brandModel = `${result.brand} ${result.model}`
724 +
725 + Taro.showToast({
726 + title: `已选择 ${result.brand} ${result.model}`,
727 + icon: 'success',
728 + duration: 2000
729 + })
730 +}
731 +
732 +/**
733 + * 品牌型号选择取消回调
734 + */
735 +const onBrandModelCancel = () => {
736 + // 可以在这里处理取消逻辑
737 +}
738 +
739 +/**
685 * 发布/保存车辆 740 * 发布/保存车辆
686 */ 741 */
687 const onPublish = () => { 742 const onPublish = () => {
......