hookehuyr

feat(车辆发布): 重构车辆发布页面,使用NutUI组件优化表单交互

- 使用NutUI组件重构整个表单页面,包括导航栏、上传组件、选择器等
- 新增多个表单字段,完善车辆信息收集能力
- 实现表单验证逻辑,确保必填字段完整性
- 添加多种选择器弹窗,优化用户选择体验
- 更新全局组件声明文件,添加所需NutUI组件
...@@ -9,12 +9,20 @@ declare module 'vue' { ...@@ -9,12 +9,20 @@ declare module 'vue' {
9 export interface GlobalComponents { 9 export interface GlobalComponents {
10 NavBar: typeof import('./src/components/navBar.vue')['default'] 10 NavBar: typeof import('./src/components/navBar.vue')['default']
11 NutButton: typeof import('@nutui/nutui-taro')['Button'] 11 NutButton: typeof import('@nutui/nutui-taro')['Button']
12 - NutLoading: typeof import('@nutui/nutui-taro')['Loading'] 12 + NutConfigProvider: typeof import('@nutui/nutui-taro')['ConfigProvider']
13 + NutForm: typeof import('@nutui/nutui-taro')['Form']
14 + NutFormItem: typeof import('@nutui/nutui-taro')['FormItem']
15 + NutInput: typeof import('@nutui/nutui-taro')['Input']
13 NutMenu: typeof import('@nutui/nutui-taro')['Menu'] 16 NutMenu: typeof import('@nutui/nutui-taro')['Menu']
14 NutMenuItem: typeof import('@nutui/nutui-taro')['MenuItem'] 17 NutMenuItem: typeof import('@nutui/nutui-taro')['MenuItem']
18 + NutNavbar: typeof import('@nutui/nutui-taro')['Navbar']
19 + NutPicker: typeof import('@nutui/nutui-taro')['Picker']
20 + NutPopup: typeof import('@nutui/nutui-taro')['Popup']
15 NutSearchbar: typeof import('@nutui/nutui-taro')['Searchbar'] 21 NutSearchbar: typeof import('@nutui/nutui-taro')['Searchbar']
16 NutSwiper: typeof import('@nutui/nutui-taro')['Swiper'] 22 NutSwiper: typeof import('@nutui/nutui-taro')['Swiper']
17 NutSwiperItem: typeof import('@nutui/nutui-taro')['SwiperItem'] 23 NutSwiperItem: typeof import('@nutui/nutui-taro')['SwiperItem']
24 + NutTextarea: typeof import('@nutui/nutui-taro')['Textarea']
25 + NutUploader: typeof import('@nutui/nutui-taro')['Uploader']
18 Picker: typeof import('./src/components/time-picker-data/picker.vue')['default'] 26 Picker: typeof import('./src/components/time-picker-data/picker.vue')['default']
19 PosterBuilder: typeof import('./src/components/PosterBuilder/index.vue')['default'] 27 PosterBuilder: typeof import('./src/components/PosterBuilder/index.vue')['default']
20 RouterLink: typeof import('vue-router')['RouterLink'] 28 RouterLink: typeof import('vue-router')['RouterLink']
......
1 +/*
2 + * @Date: 2025-07-01 17:00:22
3 + * @LastEditors: hookehuyr hookehuyr@gmail.com
4 + * @LastEditTime: 2025-07-02 10:02:29
5 + * @FilePath: /jgdl/src/pages/index/index.config.js
6 + * @Description: 文件描述
7 + */
1 export default { 8 export default {
2 - navigationBarTitleText: '首页' 9 + navigationBarTitleText: '首页',
10 + enableShareAppMessage: true,
11 + // navigationBarBackgroundColor: '#fb923c'
3 } 12 }
......
1 +/*
2 + * @Date: 2025-07-01 17:55:11
3 + * @LastEditors: hookehuyr hookehuyr@gmail.com
4 + * @LastEditTime: 2025-07-02 10:02:19
5 + * @FilePath: /jgdl/src/pages/sell/index.config.js
6 + * @Description: 文件描述
7 + */
1 export default { 8 export default {
2 - navigationBarTitleText: '首页' 9 + navigationBarTitleText: '',
10 + // navigationBarBackgroundColor: '#fb923c'
3 } 11 }
......
1 <template> 1 <template>
2 <view class="sell-page"> 2 <view class="sell-page">
3 - <!-- 顶部标题 --> 3 + <!-- 顶部导航 -->
4 - <view class="header"> 4 + <nut-config-provider :theme-vars="themeVars">
5 - <text class="header-title">发布车辆</text> 5 + <nut-navbar
6 - <text class="header-subtitle">填写车辆信息,快速出售</text> 6 + title="发布车源"
7 - </view> 7 + left-show
8 + @on-click-back="goBack"
9 + >
10 + <template #left-show>
11 + <RectLeft />
12 + </template>
13 + </nut-navbar>
14 + </nut-config-provider>
8 15
9 <!-- 表单内容 --> 16 <!-- 表单内容 -->
10 <view class="form-container"> 17 <view class="form-container">
11 - <!-- 车辆图片 --> 18 + <!-- 车辆照片上传 -->
12 <view class="form-section"> 19 <view class="form-section">
13 - <text class="section-title">车辆图片 <text class="required">*</text></text> 20 + <text class="section-title">添加车辆照片</text>
14 - <view class="image-upload-container"> 21 + <nut-uploader
15 - <view 22 + v-model:file-list="fileList"
16 - v-for="(image, index) in vehicleImages" 23 + :maximum="4"
17 - :key="index" 24 + :multiple="true"
18 - class="image-item" 25 + :preview-type="'picture'"
19 - > 26 + @oversize="onOversize"
20 - <image :src="image" class="uploaded-image" mode="aspectFill" /> 27 + @delete="onDelete"
21 - <view class="image-delete" @click="removeImage(index)"> 28 + >
22 - <Close size="16" color="#ffffff" /> 29 + <nut-button type="success" size="small">
23 - </view> 30 + <template #icon>
24 - </view> 31 + <Plus />
25 - <view 32 + </template>
26 - v-if="vehicleImages.length < 6" 33 + 上传文件
27 - class="image-upload-btn" 34 + </nut-button>
28 - @click="uploadImage" 35 + </nut-uploader>
29 - > 36 + <view class="upload-tips">
30 - <Plus size="24" color="#9ca3af" /> 37 + <text class="tip-item">正面照</text>
31 - <text class="upload-text">添加图片</text> 38 + <text class="tip-item">左侧照</text>
32 - </view> 39 + <text class="tip-item">右侧照</text>
40 + <text class="tip-item">其他</text>
33 </view> 41 </view>
34 - <text class="form-tip">最多上传6张图片,第一张为封面图</text>
35 </view> 42 </view>
36 43
37 - <!-- 基本信息 --> 44 + <!-- 所在学校 -->
38 <view class="form-section"> 45 <view class="form-section">
39 - <text class="section-title">基本信息</text> 46 + <view class="form-item" @click="showSchoolPicker">
40 - 47 + <view class="form-item-left">
41 - <view class="form-item"> 48 + <Location class="form-icon" />
42 - <text class="form-label">车辆名称 <text class="required">*</text></text> 49 + <text class="form-label">所在学校</text>
43 - <input 50 + </view>
44 - v-model="formData.name" 51 + <view class="form-item-right">
45 - placeholder="请输入车辆名称" 52 + <text class="form-value">{{ formData.school || '上海理工大学' }}</text>
46 - class="form-input" 53 + <Right class="arrow-icon" />
47 - /> 54 + </view>
48 </view> 55 </view>
56 + </view>
49 57
50 - <view class="form-item"> 58 + <!-- 车辆详情表单 -->
51 - <text class="form-label">车辆品牌 <text class="required">*</text></text> 59 + <nut-form ref="formRef" :model-value="formData">
52 - <picker 60 + <view class="form-section">
53 - :range="brands" 61 + <!-- 车型品牌 -->
54 - :value="brandIndex" 62 + <nut-form-item
55 - @change="onBrandChange" 63 + label="车型品牌"
56 - class="form-picker" 64 + prop="brand"
65 + required
66 + :rules="[{ required: true, message: '请选择车型品牌' }]"
57 > 67 >
58 - <view class="picker-content"> 68 + <view class="form-item-content" @click="showBrandPicker">
59 - <text class="picker-text">{{ brands[brandIndex] || '请选择品牌' }}</text> 69 + <text class="form-value">{{ formData.brand || '请选择' }}</text>
60 - <Right size="16" color="#9ca3af" /> 70 + <Right class="arrow-icon" />
61 </view> 71 </view>
62 - </picker> 72 + </nut-form-item>
63 - </view>
64 73
65 - <view class="form-item"> 74 + <!-- 车辆型号 -->
66 - <text class="form-label">购买年份 <text class="required">*</text></text> 75 + <nut-form-item label="车辆型号" prop="model">
67 - <picker 76 + <view class="form-item-content" @click="showModelPicker">
68 - :range="years" 77 + <text class="form-value">{{ formData.model || '请选择' }}</text>
69 - :value="yearIndex" 78 + <Right class="arrow-icon" />
70 - @change="onYearChange"
71 - class="form-picker"
72 - >
73 - <view class="picker-content">
74 - <text class="picker-text">{{ years[yearIndex] || '请选择年份' }}</text>
75 - <Right size="16" color="#9ca3af" />
76 </view> 79 </view>
77 - </picker> 80 + </nut-form-item>
78 - </view>
79 81
80 - <view class="form-item"> 82 + <!-- 车辆出厂年份 -->
81 - <text class="form-label">车辆类型 <text class="required">*</text></text> 83 + <nut-form-item label="车辆出厂年份" prop="year">
82 - <picker 84 + <view class="form-item-content" @click="showYearPicker">
83 - :range="types" 85 + <text class="form-value">{{ formData.year || '请选择' }}</text>
84 - :value="typeIndex" 86 + <Right class="arrow-icon" />
85 - @change="onTypeChange" 87 + </view>
86 - class="form-picker" 88 + </nut-form-item>
89 +
90 + <!-- 新旧程度 -->
91 + <nut-form-item
92 + label="新旧程度"
93 + prop="condition"
94 + required
95 + :rules="[{ required: true, message: '请选择新旧程度' }]"
96 + >
97 + <view class="form-item-content" @click="showConditionPicker">
98 + <text class="form-value">{{ formData.condition || '请选择' }}</text>
99 + <Right class="arrow-icon" />
100 + </view>
101 + </nut-form-item>
102 +
103 + <!-- 行驶里程 -->
104 + <nut-form-item
105 + label="行驶里程"
106 + prop="mileage"
107 + required
108 + :rules="[{ required: true, message: '请输入行驶里程' }]"
87 > 109 >
88 - <view class="picker-content"> 110 + <nut-input
89 - <text class="picker-text">{{ types[typeIndex] || '请选择类型' }}</text> 111 + v-model="formData.mileage"
90 - <Right size="16" color="#9ca3af" /> 112 + placeholder="1200"
113 + type="number"
114 + input-align="right"
115 + >
116 + <template #right>
117 + <text class="unit">公里</text>
118 + </template>
119 + </nut-input>
120 + </nut-form-item>
121 +
122 + <!-- 续航里程 -->
123 + <nut-form-item label="续航里程" prop="range">
124 + <nut-input
125 + v-model="formData.range"
126 + placeholder="60"
127 + type="number"
128 + input-align="right"
129 + >
130 + <template #right>
131 + <text class="unit">公里</text>
132 + </template>
133 + </nut-input>
134 + </nut-form-item>
135 +
136 + <!-- 最高时速 -->
137 + <nut-form-item label="最高时速" prop="maxSpeed">
138 + <nut-input
139 + v-model="formData.maxSpeed"
140 + placeholder="25"
141 + type="number"
142 + input-align="right"
143 + >
144 + <template #right>
145 + <text class="unit">km/h</text>
146 + </template>
147 + </nut-input>
148 + </nut-form-item>
149 +
150 + <!-- 电池容量 -->
151 + <nut-form-item label="电池容量" prop="batteryCapacity">
152 + <nut-input
153 + v-model="formData.batteryCapacity"
154 + placeholder="20"
155 + type="number"
156 + input-align="right"
157 + >
158 + <template #right>
159 + <text class="unit">Ah</text>
160 + </template>
161 + </nut-input>
162 + </nut-form-item>
163 +
164 + <!-- 电池损耗度 -->
165 + <nut-form-item label="电池损耗度" prop="batteryWear">
166 + <view class="form-item-content" @click="showBatteryWearPicker">
167 + <text class="form-value">{{ formData.batteryWear || '请选择' }}</text>
168 + <Right class="arrow-icon" />
169 + </view>
170 + </nut-form-item>
171 +
172 + <!-- 刹车磨损度 -->
173 + <nut-form-item label="刹车磨损度" prop="brakeWear">
174 + <view class="form-item-content" @click="showBrakeWearPicker">
175 + <text class="form-value">{{ formData.brakeWear || '请选择' }}</text>
176 + <Right class="arrow-icon" />
91 </view> 177 </view>
92 - </picker> 178 + </nut-form-item>
179 +
180 + <!-- 轮胎磨损度 -->
181 + <nut-form-item label="轮胎磨损度" prop="tireWear">
182 + <view class="form-item-content" @click="showTireWearPicker">
183 + <text class="form-value">{{ formData.tireWear || '请选择' }}</text>
184 + <Right class="arrow-icon" />
185 + </view>
186 + </nut-form-item>
93 </view> 187 </view>
188 + </nut-form>
94 189
190 + <!-- 价格信息 -->
191 + <view class="form-section">
95 <view class="form-item"> 192 <view class="form-item">
96 - <text class="form-label">出售价格 <text class="required">*</text></text> 193 + <view class="form-item-left">
97 - <view class="price-input-container"> 194 + <Location class="form-icon" />
195 + <text class="form-label">出让价格</text>
196 + </view>
197 + <view class="form-item-right">
98 <text class="price-symbol">¥</text> 198 <text class="price-symbol">¥</text>
99 - <input 199 + <input
100 - v-model="formData.price" 200 + v-model="formData.sellingPrice"
101 - placeholder="请输入价格" 201 + placeholder="3,200"
102 - type="number" 202 + type="text"
103 class="price-input" 203 class="price-input"
104 /> 204 />
105 </view> 205 </view>
106 </view> 206 </view>
107 - </view>
108 -
109 - <!-- 车辆状况 -->
110 - <view class="form-section">
111 - <text class="section-title">车辆状况</text>
112 -
113 - <view class="form-item">
114 - <text class="form-label">使用时长</text>
115 - <picker
116 - :range="usagePeriods"
117 - :value="usageIndex"
118 - @change="onUsageChange"
119 - class="form-picker"
120 - >
121 - <view class="picker-content">
122 - <text class="picker-text">{{ usagePeriods[usageIndex] || '请选择使用时长' }}</text>
123 - <Right size="16" color="#9ca3af" />
124 - </view>
125 - </picker>
126 - </view>
127 207
128 <view class="form-item"> 208 <view class="form-item">
129 - <text class="form-label">车辆描述</text> 209 + <view class="form-item-left">
130 - <textarea 210 + <Location class="form-icon" />
131 - v-model="formData.description" 211 + <text class="form-label">市场价</text>
132 - placeholder="请描述车辆的具体情况,如外观、性能、配件等" 212 + </view>
133 - class="form-textarea" 213 + <view class="form-item-right">
134 - maxlength="500" 214 + <text class="market-price-symbol">¥</text>
135 - /> 215 + <input
136 - <text class="char-count">{{ formData.description.length }}/500</text> 216 + v-model="formData.marketPrice"
217 + placeholder="6,500"
218 + type="text"
219 + class="market-price-input"
220 + />
221 + </view>
137 </view> 222 </view>
138 </view> 223 </view>
139 224
140 - <!-- 联系方式 --> 225 + <!-- 车辆描述 -->
141 <view class="form-section"> 226 <view class="form-section">
142 - <text class="section-title">联系方式</text> 227 + <nut-textarea
143 - 228 + v-model="formData.description"
144 - <view class="form-item"> 229 + placeholder="请描述车辆详情,如使用感受、车况特点等"
145 - <text class="form-label">手机号码 <text class="required">*</text></text> 230 + :max-length="200"
146 - <input 231 + :rows="4"
147 - v-model="formData.phone" 232 + show-word-limit
148 - placeholder="请输入手机号码" 233 + />
149 - type="number"
150 - class="form-input"
151 - />
152 - </view>
153 -
154 - <view class="form-item">
155 - <text class="form-label">所在学校 <text class="required">*</text></text>
156 - <input
157 - v-model="formData.school"
158 - placeholder="请输入所在学校"
159 - class="form-input"
160 - />
161 - </view>
162 -
163 - <view class="form-item">
164 - <text class="form-label">交易地点</text>
165 - <input
166 - v-model="formData.location"
167 - placeholder="请输入具体交易地点"
168 - class="form-input"
169 - />
170 - </view>
171 </view> 234 </view>
172 </view> 235 </view>
173 236
174 <!-- 底部按钮 --> 237 <!-- 底部按钮 -->
175 <view class="bottom-actions"> 238 <view class="bottom-actions">
176 - <button class="preview-btn" @click="onPreview">预览</button> 239 + <nut-button
177 - <button class="publish-btn" @click="onPublish">发布车辆</button> 240 + color="#f97316"
241 + size="large"
242 + block
243 + @click="onPublish"
244 + >
245 + 确认发布
246 + </nut-button>
178 </view> 247 </view>
179 - 248 +
180 - <!-- 自定义TabBar --> 249 + <!-- 选择器弹窗 -->
181 - <TabBar /> 250 + <!-- 学校选择 -->
251 + <nut-popup v-model:visible="schoolPickerVisible" position="bottom">
252 + <nut-picker
253 + v-model="schoolValue"
254 + :columns="schoolOptions"
255 + title="选择学校"
256 + @confirm="onSchoolConfirm"
257 + @cancel="schoolPickerVisible = false"
258 + />
259 + </nut-popup>
260 +
261 + <!-- 品牌选择 -->
262 + <nut-popup v-model:visible="brandPickerVisible" position="bottom">
263 + <nut-picker
264 + v-model="brandValue"
265 + :columns="brandOptions"
266 + title="选择车型品牌"
267 + @confirm="onBrandConfirm"
268 + @cancel="brandPickerVisible = false"
269 + />
270 + </nut-popup>
271 +
272 + <!-- 型号选择 -->
273 + <nut-popup v-model:visible="modelPickerVisible" position="bottom">
274 + <nut-picker
275 + v-model="modelValue"
276 + :columns="modelOptions"
277 + title="选择车辆型号"
278 + @confirm="onModelConfirm"
279 + @cancel="modelPickerVisible = false"
280 + />
281 + </nut-popup>
282 +
283 + <!-- 年份选择 -->
284 + <nut-popup v-model:visible="yearPickerVisible" position="bottom">
285 + <nut-picker
286 + v-model="yearValue"
287 + :columns="yearOptions"
288 + title="选择出厂年份"
289 + @confirm="onYearConfirm"
290 + @cancel="yearPickerVisible = false"
291 + />
292 + </nut-popup>
293 +
294 + <!-- 新旧程度选择 -->
295 + <nut-popup v-model:visible="conditionPickerVisible" position="bottom">
296 + <nut-picker
297 + v-model="conditionValue"
298 + :columns="conditionOptions"
299 + title="选择新旧程度"
300 + @confirm="onConditionConfirm"
301 + @cancel="conditionPickerVisible = false"
302 + />
303 + </nut-popup>
304 +
305 + <!-- 电池损耗度选择 -->
306 + <nut-popup v-model:visible="batteryWearPickerVisible" position="bottom">
307 + <nut-picker
308 + v-model="batteryWearValue"
309 + :columns="wearLevelOptions"
310 + title="选择电池损耗度"
311 + @confirm="onBatteryWearConfirm"
312 + @cancel="batteryWearPickerVisible = false"
313 + />
314 + </nut-popup>
315 +
316 + <!-- 刹车磨损度选择 -->
317 + <nut-popup v-model:visible="brakeWearPickerVisible" position="bottom">
318 + <nut-picker
319 + v-model="brakeWearValue"
320 + :columns="wearLevelOptions"
321 + title="选择刹车磨损度"
322 + @confirm="onBrakeWearConfirm"
323 + @cancel="brakeWearPickerVisible = false"
324 + />
325 + </nut-popup>
326 +
327 + <!-- 轮胎磨损度选择 -->
328 + <nut-popup v-model:visible="tireWearPickerVisible" position="bottom">
329 + <nut-picker
330 + v-model="tireWearValue"
331 + :columns="wearLevelOptions"
332 + title="选择轮胎磨损度"
333 + @confirm="onTireWearConfirm"
334 + @cancel="tireWearPickerVisible = false"
335 + />
336 + </nut-popup>
182 </view> 337 </view>
183 </template> 338 </template>
184 339
185 <script setup> 340 <script setup>
186 import { ref, reactive } from 'vue' 341 import { ref, reactive } from 'vue'
187 -import { Close, Plus, Right } from '@nutui/icons-vue-taro' 342 +import { Plus, Right, Location, RectLeft } from '@nutui/icons-vue-taro'
188 import Taro from '@tarojs/taro' 343 import Taro from '@tarojs/taro'
189 -import TabBar from '@/components/TabBar.vue'
190 344
191 -// 响应式数据 345 +const themeVars = ref({
192 -const vehicleImages = ref([]) 346 + // navbarBackground: '#FFA135',
193 -const brandIndex = ref(-1) 347 + // navbarColor: '#ffffff',
194 -const yearIndex = ref(-1) 348 +})
195 -const typeIndex = ref(-1) 349 +
196 -const usageIndex = ref(-1) 350 +// 文件上传列表
351 +const fileList = ref([])
197 352
198 // 表单数据 353 // 表单数据
199 const formData = reactive({ 354 const formData = reactive({
200 - name: '', 355 + school: '',
201 brand: '', 356 brand: '',
357 + model: '',
202 year: '', 358 year: '',
203 - type: '', 359 + condition: '',
204 - price: '', 360 + mileage: '1200',
205 - usage: '', 361 + range: '60',
206 - description: '', 362 + maxSpeed: '25',
207 - phone: '', 363 + batteryCapacity: '20',
208 - school: '', 364 + batteryWear: '',
209 - location: '' 365 + brakeWear: '',
366 + tireWear: '',
367 + sellingPrice: '3,200',
368 + marketPrice: '6,500',
369 + description: ''
210 }) 370 })
211 371
212 -// 选择器数据 372 +// 选择器显示状态
213 -const brands = ['小牛电动', '雅迪', '爱玛', '台铃', '绿源', '新日', '立马', '其他'] 373 +const schoolPickerVisible = ref(false)
214 -const years = ['2024年', '2023年', '2022年', '2021年', '2020年', '2019年', '2018年', '更早'] 374 +const brandPickerVisible = ref(false)
215 -const types = ['电动自行车', '电动摩托车', '电动汽车', '平衡车', '滑板车', '其他'] 375 +const modelPickerVisible = ref(false)
216 -const usagePeriods = ['3个月以内', '3-6个月', '6个月-1年', '1-2年', '2-3年', '3年以上'] 376 +const yearPickerVisible = ref(false)
377 +const conditionPickerVisible = ref(false)
378 +const batteryWearPickerVisible = ref(false)
379 +const brakeWearPickerVisible = ref(false)
380 +const tireWearPickerVisible = ref(false)
381 +
382 +// 选择器值
383 +const schoolValue = ref([])
384 +const brandValue = ref([])
385 +const modelValue = ref([])
386 +const yearValue = ref([])
387 +const conditionValue = ref([])
388 +const batteryWearValue = ref([])
389 +const brakeWearValue = ref([])
390 +const tireWearValue = ref([])
391 +
392 +// 选择器选项数据
393 +const schoolOptions = ref([
394 + { text: '上海理工大学', value: '上海理工大学' },
395 + { text: '复旦大学', value: '复旦大学' },
396 + { text: '上海交通大学', value: '上海交通大学' },
397 + { text: '同济大学', value: '同济大学' },
398 + { text: '华东师范大学', value: '华东师范大学' },
399 + { text: '上海大学', value: '上海大学' },
400 + { text: '东华大学', value: '东华大学' },
401 + { text: '上海财经大学', value: '上海财经大学' }
402 +])
403 +
404 +const brandOptions = ref([
405 + { text: '小牛电动', value: '小牛电动' },
406 + { text: '雅迪', value: '雅迪' },
407 + { text: '爱玛', value: '爱玛' },
408 + { text: '台铃', value: '台铃' },
409 + { text: '绿源', value: '绿源' },
410 + { text: '新日', value: '新日' },
411 + { text: '立马', value: '立马' },
412 + { text: '其他', value: '其他' }
413 +])
414 +
415 +const modelOptions = ref([
416 + { text: 'NGT', value: 'NGT' },
417 + { text: 'UQi+', value: 'UQi+' },
418 + { text: 'Gova G2', value: 'Gova G2' },
419 + { text: 'MQi+', value: 'MQi+' },
420 + { text: 'NQi GTS', value: 'NQi GTS' },
421 + { text: 'RQi', value: 'RQi' },
422 + { text: '其他型号', value: '其他型号' }
423 +])
424 +
425 +const yearOptions = ref([
426 + { text: '2024年', value: '2024年' },
427 + { text: '2023年', value: '2023年' },
428 + { text: '2022年', value: '2022年' },
429 + { text: '2021年', value: '2021年' },
430 + { text: '2020年', value: '2020年' },
431 + { text: '2019年', value: '2019年' },
432 + { text: '2018年及以前', value: '2018年及以前' }
433 +])
434 +
435 +const conditionOptions = ref([
436 + { text: '全新', value: '全新' },
437 + { text: '9成新', value: '9成新' },
438 + { text: '8成新', value: '8成新' },
439 + { text: '7成新', value: '7成新' },
440 + { text: '6成新', value: '6成新' },
441 + { text: '5成新及以下', value: '5成新及以下' }
442 +])
443 +
444 +const wearLevelOptions = ref([
445 + { text: '全新', value: '全新' },
446 + { text: '轻微磨损', value: '轻微磨损' },
447 + { text: '中度磨损', value: '中度磨损' },
448 + { text: '重度磨损', value: '重度磨损' },
449 + { text: '需要更换', value: '需要更换' }
450 +])
451 +
452 +/**
453 + * 返回上一页
454 + */
455 +const goBack = () => {
456 + Taro.redirectTo({
457 + url: '/pages/index/index'
458 + })
459 +}
217 460
218 /** 461 /**
219 - * 上传图片 462 + * 文件上传超出限制
220 */ 463 */
221 -const uploadImage = () => { 464 +const onOversize = () => {
222 - Taro.chooseImage({ 465 + Taro.showToast({
223 - count: 6 - vehicleImages.value.length, 466 + title: '文件大小超出限制',
224 - sizeType: ['compressed'], 467 + icon: 'none'
225 - sourceType: ['album', 'camera'],
226 - success: (res) => {
227 - vehicleImages.value.push(...res.tempFilePaths)
228 - }
229 }) 468 })
230 } 469 }
231 470
232 /** 471 /**
233 - * 删除图片 472 + * 删除上传的文件
234 - * @param {number} index - 图片索引
235 */ 473 */
236 -const removeImage = (index) => { 474 +const onDelete = () => {
237 - vehicleImages.value.splice(index, 1) 475 + // 删除逻辑已由组件内部处理
238 } 476 }
239 477
240 /** 478 /**
241 - * 品牌选择事件 479 + * 显示学校选择器
242 - * @param {object} e - 事件对象
243 */ 480 */
244 -const onBrandChange = (e) => { 481 +const showSchoolPicker = () => {
245 - brandIndex.value = e.detail.value 482 + schoolPickerVisible.value = true
246 - formData.brand = brands[e.detail.value]
247 } 483 }
248 484
249 /** 485 /**
250 - * 年份选择事件 486 + * 显示品牌选择器
251 - * @param {object} e - 事件对象
252 */ 487 */
253 -const onYearChange = (e) => { 488 +const showBrandPicker = () => {
254 - yearIndex.value = e.detail.value 489 + brandPickerVisible.value = true
255 - formData.year = years[e.detail.value]
256 } 490 }
257 491
258 /** 492 /**
259 - * 类型选择事件 493 + * 显示型号选择器
260 - * @param {object} e - 事件对象
261 */ 494 */
262 -const onTypeChange = (e) => { 495 +const showModelPicker = () => {
263 - typeIndex.value = e.detail.value 496 + if (!formData.brand) {
264 - formData.type = types[e.detail.value] 497 + Taro.showToast({
498 + title: '请先选择品牌',
499 + icon: 'none'
500 + })
501 + return
502 + }
503 + modelPickerVisible.value = true
265 } 504 }
266 505
267 /** 506 /**
268 - * 使用时长选择事件 507 + * 显示年份选择器
269 - * @param {object} e - 事件对象
270 */ 508 */
271 -const onUsageChange = (e) => { 509 +const showYearPicker = () => {
272 - usageIndex.value = e.detail.value 510 + yearPickerVisible.value = true
273 - formData.usage = usagePeriods[e.detail.value]
274 } 511 }
275 512
276 /** 513 /**
277 - * 预览功能 514 + * 显示新旧程度选择器
278 */ 515 */
279 -const onPreview = () => { 516 +const showConditionPicker = () => {
280 - if (!validateForm()) return 517 + conditionPickerVisible.value = true
281 - 518 +}
282 - Taro.showToast({ 519 +
283 - title: '预览功能开发中', 520 +/**
284 - icon: 'none' 521 + * 显示电池损耗度选择器
285 - }) 522 + */
523 +const showBatteryWearPicker = () => {
524 + batteryWearPickerVisible.value = true
525 +}
526 +
527 +/**
528 + * 显示刹车磨损度选择器
529 + */
530 +const showBrakeWearPicker = () => {
531 + brakeWearPickerVisible.value = true
532 +}
533 +
534 +/**
535 + * 显示轮胎磨损度选择器
536 + */
537 +const showTireWearPicker = () => {
538 + tireWearPickerVisible.value = true
539 +}
540 +
541 +/**
542 + * 学校选择确认
543 + */
544 +const onSchoolConfirm = ({ selectedValue }) => {
545 + formData.school = selectedValue[0]
546 + schoolPickerVisible.value = false
547 +}
548 +
549 +/**
550 + * 品牌选择确认
551 + */
552 +const onBrandConfirm = ({ selectedValue }) => {
553 + formData.brand = selectedValue[0]
554 + formData.model = '' // 重置型号
555 + brandPickerVisible.value = false
556 +}
557 +
558 +/**
559 + * 型号选择确认
560 + */
561 +const onModelConfirm = ({ selectedValue }) => {
562 + formData.model = selectedValue[0]
563 + modelPickerVisible.value = false
564 +}
565 +
566 +/**
567 + * 年份选择确认
568 + */
569 +const onYearConfirm = ({ selectedValue }) => {
570 + formData.year = selectedValue[0]
571 + yearPickerVisible.value = false
572 +}
573 +
574 +/**
575 + * 新旧程度选择确认
576 + */
577 +const onConditionConfirm = ({ selectedValue }) => {
578 + formData.condition = selectedValue[0]
579 + conditionPickerVisible.value = false
580 +}
581 +
582 +/**
583 + * 电池损耗度选择确认
584 + */
585 +const onBatteryWearConfirm = ({ selectedValue }) => {
586 + formData.batteryWear = selectedValue[0]
587 + batteryWearPickerVisible.value = false
588 +}
589 +
590 +/**
591 + * 刹车磨损度选择确认
592 + */
593 +const onBrakeWearConfirm = ({ selectedValue }) => {
594 + formData.brakeWear = selectedValue[0]
595 + brakeWearPickerVisible.value = false
596 +}
597 +
598 +/**
599 + * 轮胎磨损度选择确认
600 + */
601 +const onTireWearConfirm = ({ selectedValue }) => {
602 + formData.tireWear = selectedValue[0]
603 + tireWearPickerVisible.value = false
286 } 604 }
287 605
288 /** 606 /**
...@@ -290,9 +608,9 @@ const onPreview = () => { ...@@ -290,9 +608,9 @@ const onPreview = () => {
290 */ 608 */
291 const onPublish = () => { 609 const onPublish = () => {
292 if (!validateForm()) return 610 if (!validateForm()) return
293 - 611 +
294 Taro.showLoading({ title: '发布中...' }) 612 Taro.showLoading({ title: '发布中...' })
295 - 613 +
296 // 模拟发布请求 614 // 模拟发布请求
297 setTimeout(() => { 615 setTimeout(() => {
298 Taro.hideLoading() 616 Taro.hideLoading()
...@@ -300,7 +618,7 @@ const onPublish = () => { ...@@ -300,7 +618,7 @@ const onPublish = () => {
300 title: '发布成功', 618 title: '发布成功',
301 icon: 'success' 619 icon: 'success'
302 }) 620 })
303 - 621 +
304 // 发布成功后跳转到首页 622 // 发布成功后跳转到首页
305 setTimeout(() => { 623 setTimeout(() => {
306 Taro.switchTab({ url: '/pages/index/index' }) 624 Taro.switchTab({ url: '/pages/index/index' })
...@@ -313,51 +631,31 @@ const onPublish = () => { ...@@ -313,51 +631,31 @@ const onPublish = () => {
313 * @returns {boolean} 验证结果 631 * @returns {boolean} 验证结果
314 */ 632 */
315 const validateForm = () => { 633 const validateForm = () => {
316 - if (vehicleImages.value.length === 0) { 634 + if (fileList.value.length === 0) {
317 Taro.showToast({ title: '请上传车辆图片', icon: 'none' }) 635 Taro.showToast({ title: '请上传车辆图片', icon: 'none' })
318 return false 636 return false
319 } 637 }
320 - 638 +
321 - if (!formData.name.trim()) {
322 - Taro.showToast({ title: '请输入车辆名称', icon: 'none' })
323 - return false
324 - }
325 -
326 if (!formData.brand) { 639 if (!formData.brand) {
327 - Taro.showToast({ title: '请选择车辆品牌', icon: 'none' }) 640 + Taro.showToast({ title: '请选择车型品牌', icon: 'none' })
328 - return false
329 - }
330 -
331 - if (!formData.year) {
332 - Taro.showToast({ title: '请选择购买年份', icon: 'none' })
333 - return false
334 - }
335 -
336 - if (!formData.type) {
337 - Taro.showToast({ title: '请选择车辆类型', icon: 'none' })
338 return false 641 return false
339 } 642 }
340 - 643 +
341 - if (!formData.price || formData.price <= 0) { 644 + if (!formData.condition) {
342 - Taro.showToast({ title: '请输入正确的价格', icon: 'none' }) 645 + Taro.showToast({ title: '请选择新旧程度', icon: 'none' })
343 - return false
344 - }
345 -
346 - if (!formData.phone.trim()) {
347 - Taro.showToast({ title: '请输入手机号码', icon: 'none' })
348 return false 646 return false
349 } 647 }
350 - 648 +
351 - if (!/^1[3-9]\d{9}$/.test(formData.phone)) { 649 + if (!formData.mileage || formData.mileage <= 0) {
352 - Taro.showToast({ title: '请输入正确的手机号码', icon: 'none' }) 650 + Taro.showToast({ title: '请输入正确的行驶里程', icon: 'none' })
353 return false 651 return false
354 } 652 }
355 - 653 +
356 - if (!formData.school.trim()) { 654 + if (!formData.sellingPrice) {
357 - Taro.showToast({ title: '请输入所在学校', icon: 'none' }) 655 + Taro.showToast({ title: '请输入出让价格', icon: 'none' })
358 return false 656 return false
359 } 657 }
360 - 658 +
361 return true 659 return true
362 } 660 }
363 </script> 661 </script>
...@@ -365,202 +663,128 @@ const validateForm = () => { ...@@ -365,202 +663,128 @@ const validateForm = () => {
365 <style lang="less"> 663 <style lang="less">
366 .sell-page { 664 .sell-page {
367 min-height: 100vh; 665 min-height: 100vh;
368 - background-color: #f9fafb; 666 + background-color: #f5f5f5;
369 - padding-bottom: 80px; 667 + padding-bottom: 120rpx;
370 -}
371 -
372 -.header {
373 - background-color: #ffffff;
374 - padding: 20px 16px;
375 - border-bottom: 1px solid #f3f4f6;
376 -}
377 -
378 -.header-title {
379 - font-size: 24px;
380 - font-weight: 700;
381 - color: #111827;
382 - display: block;
383 - margin-bottom: 4px;
384 -}
385 -
386 -.header-subtitle {
387 - font-size: 14px;
388 - color: #6b7280;
389 - display: block;
390 } 668 }
391 669
392 .form-container { 670 .form-container {
393 - padding: 0 16px; 671 + padding: 0 32rpx;
394 } 672 }
395 673
396 .form-section { 674 .form-section {
397 background-color: #ffffff; 675 background-color: #ffffff;
398 - border-radius: 12px; 676 + border-radius: 24rpx;
399 - padding: 20px; 677 + padding: 32rpx;
400 - margin: 12px 0; 678 + margin: 24rpx 0;
401 } 679 }
402 680
403 .section-title { 681 .section-title {
404 - font-size: 18px; 682 + font-size: 32rpx;
405 font-weight: 600; 683 font-weight: 600;
406 color: #111827; 684 color: #111827;
407 - margin-bottom: 16px; 685 + margin-bottom: 32rpx;
408 display: block; 686 display: block;
409 } 687 }
410 688
411 -.required { 689 +.upload-tips {
412 - color: #ef4444; 690 + display: flex;
413 -} 691 + margin-top: 16rpx;
414 - 692 + gap: 32rpx;
415 -.image-upload-container {
416 - display: grid;
417 - grid-template-columns: repeat(3, 1fr);
418 - gap: 12px;
419 - margin-bottom: 8px;
420 } 693 }
421 694
422 -.image-item { 695 +.tip-item {
423 - position: relative; 696 + font-size: 24rpx;
424 - aspect-ratio: 1; 697 + color: #9ca3af;
425 - border-radius: 8px;
426 - overflow: hidden;
427 } 698 }
428 699
429 -.uploaded-image { 700 +.form-item {
430 - width: 100%;
431 - height: 100%;
432 - object-fit: cover;
433 -}
434 -
435 -.image-delete {
436 - position: absolute;
437 - top: 4px;
438 - right: 4px;
439 - width: 24px;
440 - height: 24px;
441 - background-color: rgba(0, 0, 0, 0.6);
442 - border-radius: 50%;
443 display: flex; 701 display: flex;
702 + justify-content: space-between;
444 align-items: center; 703 align-items: center;
445 - justify-content: center; 704 + padding: 32rpx 0;
705 + border-bottom: 1rpx solid #f3f4f6;
446 } 706 }
447 707
448 -.image-upload-btn { 708 +.form-item:last-child {
449 - aspect-ratio: 1; 709 + border-bottom: none;
450 - border: 2px dashed #d1d5db;
451 - border-radius: 8px;
452 - display: flex;
453 - flex-direction: column;
454 - align-items: center;
455 - justify-content: center;
456 - gap: 4px;
457 - background-color: #f9fafb;
458 } 710 }
459 711
460 -.upload-text { 712 +.form-item-left {
461 - font-size: 12px; 713 + display: flex;
462 - color: #9ca3af; 714 + align-items: center;
463 } 715 }
464 716
465 -.form-tip { 717 +.form-icon {
466 - font-size: 12px; 718 + margin-right: 16rpx;
467 color: #9ca3af; 719 color: #9ca3af;
468 - display: block;
469 -}
470 -
471 -.form-item {
472 - margin-bottom: 16px;
473 } 720 }
474 721
475 .form-label { 722 .form-label {
476 - font-size: 14px; 723 + font-size: 28rpx;
477 - font-weight: 500;
478 color: #374151; 724 color: #374151;
479 - margin-bottom: 8px;
480 - display: block;
481 } 725 }
482 726
483 -.form-input { 727 +.form-item-right {
484 - width: 100%; 728 + display: flex;
485 - padding: 12px 16px; 729 + align-items: center;
486 - border: 1px solid #d1d5db;
487 - border-radius: 8px;
488 - font-size: 14px;
489 - color: #374151;
490 - background-color: #ffffff;
491 } 730 }
492 731
493 -.form-input:focus { 732 +.form-value {
494 - border-color: #f97316; 733 + font-size: 28rpx;
495 - outline: none; 734 + color: #9ca3af;
735 + margin-right: 16rpx;
496 } 736 }
497 737
498 -.form-picker { 738 +.arrow-icon {
499 - width: 100%; 739 + color: #9ca3af;
500 } 740 }
501 741
502 -.picker-content { 742 +.form-item-content {
503 display: flex; 743 display: flex;
504 justify-content: space-between; 744 justify-content: space-between;
505 align-items: center; 745 align-items: center;
506 - padding: 12px 16px; 746 + width: 100%;
507 - border: 1px solid #d1d5db; 747 + padding: 24rpx 0;
508 - border-radius: 8px;
509 - background-color: #ffffff;
510 -}
511 -
512 -.picker-text {
513 - font-size: 14px;
514 - color: #374151;
515 } 748 }
516 749
517 -.price-input-container { 750 +.unit {
518 - display: flex; 751 + font-size: 28rpx;
519 - align-items: center; 752 + color: #9ca3af;
520 - border: 1px solid #d1d5db; 753 + margin-left: 16rpx;
521 - border-radius: 8px;
522 - background-color: #ffffff;
523 } 754 }
524 755
525 .price-symbol { 756 .price-symbol {
526 - padding: 12px 0 12px 16px; 757 + font-size: 32rpx;
527 - font-size: 14px; 758 + color: #f97316;
528 - color: #374151; 759 + font-weight: 600;
529 - font-weight: 500; 760 + margin-right: 8rpx;
530 } 761 }
531 762
532 .price-input { 763 .price-input {
533 - flex: 1; 764 + font-size: 32rpx;
534 - padding: 12px 16px 12px 4px; 765 + color: #f97316;
766 + font-weight: 600;
767 + text-align: right;
535 border: none; 768 border: none;
536 - font-size: 14px; 769 + outline: none;
537 - color: #374151;
538 background: transparent; 770 background: transparent;
771 + width: 160rpx;
539 } 772 }
540 773
541 -.form-textarea { 774 +.market-price-symbol {
542 - width: 100%; 775 + font-size: 28rpx;
543 - min-height: 80px; 776 + color: #9ca3af;
544 - padding: 12px 16px; 777 + margin-right: 8rpx;
545 - border: 1px solid #d1d5db;
546 - border-radius: 8px;
547 - font-size: 14px;
548 - color: #374151;
549 - background-color: #ffffff;
550 - resize: none;
551 -}
552 -
553 -.form-textarea:focus {
554 - border-color: #f97316;
555 - outline: none;
556 } 778 }
557 779
558 -.char-count { 780 +.market-price-input {
559 - font-size: 12px; 781 + font-size: 28rpx;
560 color: #9ca3af; 782 color: #9ca3af;
561 text-align: right; 783 text-align: right;
562 - margin-top: 4px; 784 + border: none;
563 - display: block; 785 + outline: none;
786 + background: transparent;
787 + width: 160rpx;
564 } 788 }
565 789
566 .bottom-actions { 790 .bottom-actions {
...@@ -569,39 +793,85 @@ const validateForm = () => { ...@@ -569,39 +793,85 @@ const validateForm = () => {
569 left: 0; 793 left: 0;
570 right: 0; 794 right: 0;
571 background-color: #ffffff; 795 background-color: #ffffff;
572 - padding: 12px 16px; 796 + padding: 24rpx 32rpx;
573 - border-top: 1px solid #f3f4f6; 797 + border-top: 1rpx solid #f3f4f6;
574 - display: flex; 798 + z-index: 100;
575 - gap: 12px;
576 } 799 }
577 800
578 -.preview-btn { 801 +// NutUI组件样式覆盖
579 - flex: 1; 802 +:deep(.nut-form-item) {
580 - padding: 12px; 803 + padding: 0;
581 - border: 1px solid #f97316; 804 + margin-bottom: 0;
582 - border-radius: 8px; 805 + border-bottom: 1rpx solid #f3f4f6;
583 - background-color: #ffffff;
584 - color: #f97316;
585 - font-size: 16px;
586 - font-weight: 500;
587 } 806 }
588 807
589 -.publish-btn { 808 +// :deep(.nut-form-item:last-child) {
590 - flex: 2; 809 +// border-bottom: none;
591 - padding: 12px; 810 +// }
592 - border: none; 811 +
593 - border-radius: 8px; 812 +:deep(.nut-form-item__body) {
594 - background-color: #f97316; 813 + padding: 32rpx 0;
595 - color: #ffffff; 814 +}
596 - font-size: 16px; 815 +
597 - font-weight: 500; 816 +:deep(.nut-form-item__label) {
817 + font-size: 28rpx;
818 + color: #374151;
819 + margin-right: 32rpx;
820 +}
821 +
822 +:deep(.nut-input) {
823 + text-align: right;
824 +}
825 +
826 +:deep(.nut-input__input) {
827 + font-size: 28rpx;
828 + color: #374151;
829 + text-align: right;
830 +}
831 +
832 +:deep(.nut-textarea) {
833 + border: 1rpx solid #e5e7eb;
834 + border-radius: 16rpx;
835 + padding: 24rpx;
836 +}
837 +
838 +:deep(.nut-textarea__textarea) {
839 + font-size: 28rpx;
840 + color: #374151;
841 + line-height: 1.5;
598 } 842 }
599 843
600 -.preview-btn:active { 844 +:deep(.nut-uploader) {
601 - background-color: #fef7ed; 845 + margin-bottom: 16rpx;
602 } 846 }
603 847
604 -.publish-btn:active { 848 +:deep(.nut-uploader__preview) {
605 - background-color: #ea580c; 849 + display: grid;
850 + grid-template-columns: repeat(4, 1fr);
851 + gap: 24rpx;
852 + margin-bottom: 24rpx;
853 +}
854 +
855 +:deep(.nut-uploader__preview-img) {
856 + width: 160rpx;
857 + height: 160rpx;
858 + border-radius: 16rpx;
859 +}
860 +
861 +:deep(.nut-picker__toolbar) {
862 + padding: 24rpx 32rpx;
863 +}
864 +
865 +:deep(.nut-picker__cancel) {
866 + color: #9ca3af;
867 +}
868 +
869 +:deep(.nut-picker__confirm) {
870 + color: #f97316;
871 +}
872 +
873 +:deep(.nut-picker__title) {
874 + font-size: 32rpx;
875 + font-weight: 600;
606 } 876 }
607 -</style>
...\ No newline at end of file ...\ No newline at end of file
877 +</style>
......