hookehuyr

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

- 新增 BrandModelPicker 组件用于统一选择品牌和型号
- 替换原表单中分开的品牌和型号选择字段
- 添加表单验证逻辑和组合字段显示
......@@ -7,6 +7,7 @@ export {}
declare module 'vue' {
export interface GlobalComponents {
BrandModelPicker: typeof import('./src/components/BrandModelPicker.vue')['default']
NavBar: typeof import('./src/components/navBar.vue')['default']
NutActionSheet: typeof import('@nutui/nutui-taro')['ActionSheet']
NutButton: typeof import('@nutui/nutui-taro')['Button']
......
<template>
<view>
<!-- 品牌选择弹框 -->
<nut-popup v-model:visible="brandPickerVisible" position="bottom" :style="{ height: '80%' }">
<view class="brand-model-picker">
<view class="picker-header">
<text class="picker-title">选择品牌</text>
<nut-button size="small" type="primary" @click="closePicker">关闭</nut-button>
</view>
<view class="brand-container">
<scroll-view
class="brand-content"
:scroll-y="true"
:style="{ height: contentHeight + 'rpx' }"
ref="brandContentRef"
>
<view class="brand-list">
<view
v-for="brand in allBrands"
:key="brand.value"
class="brand-item"
@click="selectBrand(brand)"
>
<view class="brand-info">
<view class="brand-icon">
<text class="brand-initial">{{ brand.text.charAt(0) }}</text>
</view>
<text class="brand-name">{{ brand.text }}</text>
</view>
<Right class="brand-arrow" />
</view>
</view>
</scroll-view>
</view>
</view>
</nut-popup>
<!-- 型号选择弹框 -->
<nut-popup v-model:visible="modelPickerVisible" position="bottom" :style="{ height: '50%' }">
<view class="model-picker">
<view class="picker-header">
<nut-button size="small" @click="goBackToBrandPicker">返回</nut-button>
<text class="picker-title">选择{{ selectedBrandName }}型号</text>
<nut-button size="small" type="primary" @click="closePicker">关闭</nut-button>
</view>
<view class="model-list">
<view
v-for="model in currentBrandModels"
:key="model.value"
class="model-item"
@click="selectModel(model)"
>
<text class="model-name">{{ model.text }}</text>
</view>
</view>
</view>
</nut-popup>
</view>
</template>
<script setup>
import { ref } from 'vue'
import { Right } from '@nutui/icons-vue-taro'
// 定义事件
const emit = defineEmits(['confirm', 'cancel'])
// 响应式数据
const brandPickerVisible = ref(false)
const modelPickerVisible = ref(false)
const selectedBrandName = ref('')
const currentBrandModels = ref([])
const contentHeight = ref(800) // 默认高度 rpx
const allBrands = ref([
{ text: '爱玛', value: 'aima' },
{ text: '雅迪', value: 'yadi' },
{ text: '小牛', value: 'xiaoniu' },
{ text: '台铃', value: 'tailing' },
{ text: '绿源', value: 'lvyuan' },
{ text: '立马', value: 'lima' },
{ text: '新日', value: 'xinri' },
{ text: '宗申', value: 'zongshen' },
{ text: '奇瑞', value: 'qirui' },
{ text: '比德文', value: 'bidewen' },
{ text: '欧派', value: 'oupai' },
{ text: '捷安特', value: 'jiante' },
{ text: '美利达', value: 'meilida' },
{ text: '凤凰', value: 'fenghuang' },
{ text: '永久', value: 'yongjiu' },
{ text: '飞鸽', value: 'feige' },
{ text: '小刀', value: 'xiaodao' },
{ text: '速派奇', value: 'supaiqi' },
{ text: '金箭', value: 'jinjian' },
{ text: '森蓝', value: 'senlan' }
])
// 品牌型号映射
const brandModelMap = ref({
'aima': [
{ text: 'A1 Pro', value: 'aima_a1_pro' },
{ text: 'A2 Max', value: 'aima_a2_max' },
{ text: 'A3 Plus', value: 'aima_a3_plus' },
{ text: 'A4 Elite', value: 'aima_a4_elite' }
],
'yadi': [
{ text: 'Y1 智享版', value: 'yadi_y1_smart' },
{ text: 'Y2 豪华版', value: 'yadi_y2_luxury' },
{ text: 'Y3 运动版', value: 'yadi_y3_sport' },
{ text: 'Y4 旗舰版', value: 'yadi_y4_flagship' }
],
'xiaoniu': [
{ text: 'N1S', value: 'xiaoniu_n1s' },
{ text: 'N-GT', value: 'xiaoniu_ngt' },
{ text: 'NGT Pro', value: 'xiaoniu_ngt_pro' },
{ text: 'U1 Pro', value: 'xiaoniu_u1_pro' }
],
'tailing': [
{ text: 'T1 经典版', value: 'tailing_t1_classic' },
{ text: 'T2 时尚版', value: 'tailing_t2_fashion' },
{ text: 'T3 豪华版', value: 'tailing_t3_luxury' }
],
'lvyuan': [
{ text: 'L1 标准版', value: 'lvyuan_l1_standard' },
{ text: 'L2 升级版', value: 'lvyuan_l2_upgrade' },
{ text: 'L3 旗舰版', value: 'lvyuan_l3_flagship' }
],
'lima': [
{ text: 'LM-1', value: 'lima_lm1' },
{ text: 'LM-2', value: 'lima_lm2' },
{ text: 'LM-3 Pro', value: 'lima_lm3_pro' }
],
'xinri': [
{ text: 'XR-1', value: 'xinri_xr1' },
{ text: 'XR-2 Plus', value: 'xinri_xr2_plus' },
{ text: 'XR-3 Max', value: 'xinri_xr3_max' }
],
'zongshen': [
{ text: 'ZS-1', value: 'zongshen_zs1' },
{ text: 'ZS-2 Pro', value: 'zongshen_zs2_pro' }
],
'qirui': [
{ text: 'QR-1', value: 'qirui_qr1' },
{ text: 'QR-2 智能版', value: 'qirui_qr2_smart' }
],
'bidewen': [
{ text: 'BD-1', value: 'bidewen_bd1' },
{ text: 'BD-2 Plus', value: 'bidewen_bd2_plus' }
],
'oupai': [
{ text: 'OP-1', value: 'oupai_op1' },
{ text: 'OP-2 Pro', value: 'oupai_op2_pro' }
],
'jiante': [
{ text: 'JT-1', value: 'jiante_jt1' },
{ text: 'JT-2 运动版', value: 'jiante_jt2_sport' }
],
'meilida': [
{ text: 'MD-1', value: 'meilida_md1' },
{ text: 'MD-2 Pro', value: 'meilida_md2_pro' }
],
'fenghuang': [
{ text: 'FH-1 经典', value: 'fenghuang_fh1_classic' },
{ text: 'FH-2 现代', value: 'fenghuang_fh2_modern' }
],
'yongjiu': [
{ text: 'YJ-1', value: 'yongjiu_yj1' },
{ text: 'YJ-2 Plus', value: 'yongjiu_yj2_plus' }
],
'feige': [
{ text: 'FG-1', value: 'feige_fg1' },
{ text: 'FG-2 Pro', value: 'feige_fg2_pro' }
],
'xiaodao': [
{ text: 'XD-1', value: 'xiaodao_xd1' },
{ text: 'XD-2 Max', value: 'xiaodao_xd2_max' }
],
'supaiqi': [
{ text: 'SP-1', value: 'supaiqi_sp1' },
{ text: 'SP-2 Pro', value: 'supaiqi_sp2_pro' }
],
'jinjian': [
{ text: 'JJ-1', value: 'jinjian_jj1' },
{ text: 'JJ-2 Plus', value: 'jinjian_jj2_plus' }
],
'senlan': [
{ text: 'SL-1', value: 'senlan_sl1' },
{ text: 'SL-2 Pro', value: 'senlan_sl2_pro' }
]
})
// 计算内容高度
const calculateContentHeight = () => {
// 弹框总高度 - 头部高度
const popupHeight = 1200 // rpx
const headerHeight = 120 // rpx (头部标题和按钮的高度)
contentHeight.value = popupHeight - headerHeight
}
// 显示品牌选择器
const show = () => {
calculateContentHeight() // 计算内容高度
brandPickerVisible.value = true
}
// 选择品牌
const selectBrand = (brand) => {
selectedBrandName.value = brand.text
currentBrandModels.value = brandModelMap.value[brand.value] || []
brandPickerVisible.value = false
modelPickerVisible.value = true
}
// 选择型号
const selectModel = (model) => {
const result = {
brand: selectedBrandName.value,
model: model.text,
brandValue: selectedBrandName.value,
modelValue: model.value
}
// 发送确认事件
emit('confirm', result)
// 关闭所有弹框
closeAllPickers()
}
// 返回品牌选择
const goBackToBrandPicker = () => {
modelPickerVisible.value = false
brandPickerVisible.value = true
}
// 关闭选择器
const closePicker = () => {
closeAllPickers()
emit('cancel')
}
// 关闭所有弹框
const closeAllPickers = () => {
brandPickerVisible.value = false
modelPickerVisible.value = false
selectedBrandName.value = ''
currentBrandModels.value = []
}
// 暴露方法给父组件
defineExpose({
show,
close: closeAllPickers
})
</script>
<style lang="less">
.brand-model-picker {
padding: 20rpx;
height: 100%;
display: flex;
flex-direction: column;
.picker-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx 0;
border-bottom: 1rpx solid #f0f0f0;
margin-bottom: 20rpx;
.picker-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
}
}
.brand-container {
flex: 1;
height: 100%;
.brand-content {
background-color: #f8f9fa;
.brand-list {
padding: 20rpx;
.brand-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24rpx 30rpx;
margin-bottom: 16rpx;
background-color: white;
border-radius: 16rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
transition: all 0.3s ease;
&:active {
transform: scale(0.98);
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
}
&:last-child {
margin-bottom: 0;
}
.brand-info {
display: flex;
align-items: center;
.brand-icon {
width: 80rpx;
height: 80rpx;
border-radius: 40rpx;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
align-items: center;
justify-content: center;
margin-right: 24rpx;
.brand-initial {
font-size: 32rpx;
color: white;
font-weight: bold;
}
}
.brand-name {
font-size: 32rpx;
color: #333;
font-weight: 500;
}
}
.brand-arrow {
width: 32rpx;
height: 32rpx;
color: #ccc;
}
}
}
}
}
}
.model-picker {
padding: 20rpx;
height: 100%;
display: flex;
flex-direction: column;
.picker-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx 0;
border-bottom: 1rpx solid #f0f0f0;
margin-bottom: 20rpx;
.picker-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
}
}
.model-list {
flex: 1;
overflow-y: auto;
.model-item {
display: flex;
align-items: center;
padding: 30rpx 20rpx;
border-bottom: 1rpx solid #f8f8f8;
transition: background-color 0.2s;
&:active {
background-color: #f5f5f5;
}
.model-name {
font-size: 30rpx;
color: #333;
}
}
}
}
</style>
......@@ -103,20 +103,32 @@
<!-- 车辆详情表单 -->
<nut-form ref="formRef" :model-value="formData">
<view class="form-section">
<!-- 车型品牌 -->
<nut-form-item label-position="top" label="车型品牌" prop="brand" required :rules="[{ required: true, message: '请选择车型品牌' }]">
<!--<!~~ 车型品牌 ~~>
<nut-form-item label-position="top" label="车型品牌" prop="brand" required
:rules="[{ required: true, message: '请选择车型品牌' }]">
<view class="form-item-content" @click="showBrandPicker">
<text class="form-value">{{ formData.brand || '请选择' }}</text>
<Right class="arrow-icon" />
</view>
</nut-form-item>
<!-- 车辆型号 -->
<!~~ 车辆型号 ~~>
<nut-form-item label-position="top" label="车辆型号" prop="model">
<view class="form-item-content" @click="showModelPicker">
<text class="form-value">{{ formData.model || '请选择' }}</text>
<Right class="arrow-icon" />
</view>
</nut-form-item>-->
<!-- 品牌型号选择(新版) -->
<nut-form-item label-position="top" label="品牌型号选择" prop="brandModel" required
:rules="[{ required: true, message: '请选择品牌型号' }]">
<view class="form-item-content" @click="showBrandModelPicker">
<text class="form-value">
{{ formData.brand && formData.model ? `${formData.brand} ${formData.model}` : '请选择品牌型号' }}
</text>
<Right class="arrow-icon" />
</view>
</nut-form-item>
<!-- 车辆出厂年份 -->
......@@ -243,12 +255,14 @@
</view>
<!-- 选择器弹窗 -->
<!-- 学校选择 -->
<!-- 学校选择 -->
<nut-popup v-model:visible="schoolPickerVisible" position="bottom">
<nut-picker v-model="schoolValue" :columns="schoolOptions" title="选择学校" @confirm="onSchoolConfirm"
@cancel="schoolPickerVisible = false" />
</nut-popup>
<!-- TODO: 如果车型品牌选择其他,车辆型号选择其他型号,允许用户自己填写自定义的内容, 需要等待真实数据后再考虑 -->
<!-- 品牌选择 -->
<nut-popup v-model:visible="brandPickerVisible" position="bottom">
<nut-picker v-model="brandValue" :columns="brandOptions" title="选择车型品牌" @confirm="onBrandConfirm"
......@@ -261,6 +275,9 @@
@cancel="modelPickerVisible = false" />
</nut-popup>
<!-- 品牌型号选择器组件 -->
<BrandModelPicker ref="brandModelPickerRef" @confirm="onBrandModelConfirm" @cancel="onBrandModelCancel" />
<!-- 年份选择 -->
<nut-popup v-model:visible="yearPickerVisible" position="bottom">
<nut-picker v-model="yearValue" :columns="yearOptions" title="选择出厂年份" @confirm="onYearConfirm"
......@@ -298,6 +315,7 @@ import { ref, reactive, onMounted } from 'vue'
import { Plus, Right, Location, Close, RectLeft } from '@nutui/icons-vue-taro'
import Taro from '@tarojs/taro'
import BASE_URL from '@/utils/config';
import BrandModelPicker from '@/components/BrandModelPicker.vue'
import './index.less'
const themeVars = ref({
......@@ -350,6 +368,7 @@ const formData = reactive({
school: '',
brand: '',
model: '',
brandModel: '', // 品牌型号组合字段,用于表单验证
year: '',
condition: '',
mileage: '1200',
......@@ -374,6 +393,10 @@ const batteryWearPickerVisible = ref(false)
const brakeWearPickerVisible = ref(false)
const tireWearPickerVisible = ref(false)
// 新的品牌型号选择器状态
// 品牌型号选择器组件引用
const brandModelPickerRef = ref(null)
// 选择器值
const schoolValue = ref([])
const brandValue = ref([])
......@@ -444,6 +467,8 @@ const wearLevelOptions = ref([
{ text: '需要更换', value: '需要更换' }
])
// 品牌型号数据已移至 BrandModelPicker 组件中
/**
......@@ -508,7 +533,7 @@ const uploadImage = (filePath, type) => {
}
});
},
fail: function (res) {
fail: function () {
Taro.hideLoading({
success: () => {
Taro.showToast({
......@@ -682,6 +707,36 @@ const onTireWearConfirm = ({ selectedValue }) => {
}
/**
* 显示品牌型号选择器
*/
const showBrandModelPicker = () => {
brandModelPickerRef.value?.show()
}
/**
* 品牌型号选择确认回调
*/
const onBrandModelConfirm = (result) => {
formData.brand = result.brand
formData.model = result.model
// 设置组合字段用于表单验证
formData.brandModel = `${result.brand} ${result.model}`
Taro.showToast({
title: `已选择 ${result.brand} ${result.model}`,
icon: 'success',
duration: 2000
})
}
/**
* 品牌型号选择取消回调
*/
const onBrandModelCancel = () => {
// 可以在这里处理取消逻辑
}
/**
* 发布/保存车辆
*/
const onPublish = () => {
......