feat(发布页): 重构车辆图片上传组件并添加图片预览功能
- 将原有的通用上传组件替换为分类上传网格布局 - 为每种车辆照片类型添加独立的上传和删除功能 - 新增图片预览组件支持点击查看大图 - 优化表单样式和布局 - 移除不再使用的NutUploader组件声明
Showing
2 changed files
with
371 additions
and
180 deletions
| ... | @@ -12,6 +12,7 @@ declare module 'vue' { | ... | @@ -12,6 +12,7 @@ declare module 'vue' { |
| 12 | NutConfigProvider: typeof import('@nutui/nutui-taro')['ConfigProvider'] | 12 | NutConfigProvider: typeof import('@nutui/nutui-taro')['ConfigProvider'] |
| 13 | NutForm: typeof import('@nutui/nutui-taro')['Form'] | 13 | NutForm: typeof import('@nutui/nutui-taro')['Form'] |
| 14 | NutFormItem: typeof import('@nutui/nutui-taro')['FormItem'] | 14 | NutFormItem: typeof import('@nutui/nutui-taro')['FormItem'] |
| 15 | + NutImagePreview: typeof import('@nutui/nutui-taro')['ImagePreview'] | ||
| 15 | NutInput: typeof import('@nutui/nutui-taro')['Input'] | 16 | NutInput: typeof import('@nutui/nutui-taro')['Input'] |
| 16 | NutMenu: typeof import('@nutui/nutui-taro')['Menu'] | 17 | NutMenu: typeof import('@nutui/nutui-taro')['Menu'] |
| 17 | NutMenuItem: typeof import('@nutui/nutui-taro')['MenuItem'] | 18 | NutMenuItem: typeof import('@nutui/nutui-taro')['MenuItem'] |
| ... | @@ -22,7 +23,6 @@ declare module 'vue' { | ... | @@ -22,7 +23,6 @@ declare module 'vue' { |
| 22 | NutSwiper: typeof import('@nutui/nutui-taro')['Swiper'] | 23 | NutSwiper: typeof import('@nutui/nutui-taro')['Swiper'] |
| 23 | NutSwiperItem: typeof import('@nutui/nutui-taro')['SwiperItem'] | 24 | NutSwiperItem: typeof import('@nutui/nutui-taro')['SwiperItem'] |
| 24 | NutTextarea: typeof import('@nutui/nutui-taro')['Textarea'] | 25 | NutTextarea: typeof import('@nutui/nutui-taro')['Textarea'] |
| 25 | - NutUploader: typeof import('@nutui/nutui-taro')['Uploader'] | ||
| 26 | Picker: typeof import('./src/components/time-picker-data/picker.vue')['default'] | 26 | Picker: typeof import('./src/components/time-picker-data/picker.vue')['default'] |
| 27 | PosterBuilder: typeof import('./src/components/PosterBuilder/index.vue')['default'] | 27 | PosterBuilder: typeof import('./src/components/PosterBuilder/index.vue')['default'] |
| 28 | RouterLink: typeof import('vue-router')['RouterLink'] | 28 | RouterLink: typeof import('vue-router')['RouterLink'] | ... | ... |
| ... | @@ -2,15 +2,11 @@ | ... | @@ -2,15 +2,11 @@ |
| 2 | <view class="sell-page"> | 2 | <view class="sell-page"> |
| 3 | <!-- 顶部导航 --> | 3 | <!-- 顶部导航 --> |
| 4 | <nut-config-provider :theme-vars="themeVars"> | 4 | <nut-config-provider :theme-vars="themeVars"> |
| 5 | - <nut-navbar | 5 | + <nut-navbar title="发布车源" left-show @on-click-back="goBack"> |
| 6 | - title="发布车源" | 6 | + <template #left-show> |
| 7 | - left-show | 7 | + <RectLeft /> |
| 8 | - @on-click-back="goBack" | 8 | + </template> |
| 9 | - > | 9 | + </nut-navbar> |
| 10 | - <template #left-show> | ||
| 11 | - <RectLeft /> | ||
| 12 | - </template> | ||
| 13 | - </nut-navbar> | ||
| 14 | </nut-config-provider> | 10 | </nut-config-provider> |
| 15 | 11 | ||
| 16 | <!-- 表单内容 --> | 12 | <!-- 表单内容 --> |
| ... | @@ -18,27 +14,74 @@ | ... | @@ -18,27 +14,74 @@ |
| 18 | <!-- 车辆照片上传 --> | 14 | <!-- 车辆照片上传 --> |
| 19 | <view class="form-section"> | 15 | <view class="form-section"> |
| 20 | <text class="section-title">添加车辆照片</text> | 16 | <text class="section-title">添加车辆照片</text> |
| 21 | - <nut-uploader | 17 | + <view class="upload-grid"> |
| 22 | - v-model:file-list="fileList" | 18 | + <!-- 正面照 --> |
| 23 | - :maximum="4" | 19 | + <view class="upload-item"> |
| 24 | - :multiple="true" | 20 | + <!-- 上传按钮 --> |
| 25 | - :preview-type="'picture'" | 21 | + <view v-if="!uploadedImages.front" class="upload-button" @click="triggerUpload('front')"> |
| 26 | - @oversize="onOversize" | 22 | + <Plus class="upload-icon" /> |
| 27 | - @delete="onDelete" | 23 | + </view> |
| 28 | - > | 24 | + <!-- 图片预览 --> |
| 29 | - <nut-button type="success" size="small"> | 25 | + <view v-else class="image-preview" @click="previewImage(uploadedImages.front)"> |
| 30 | - <template #icon> | 26 | + <image :src="uploadedImages.front" class="preview-image" mode="aspectFill" /> |
| 31 | - <Plus /> | 27 | + <view class="delete-btn" @click.stop="deleteImage('front')"> |
| 32 | - </template> | 28 | + <Close class="delete-icon" /> |
| 33 | - 上传文件 | 29 | + </view> |
| 34 | - </nut-button> | 30 | + </view> |
| 35 | - </nut-uploader> | 31 | + <text class="upload-label">正面照</text> |
| 36 | - <view class="upload-tips"> | 32 | + </view> |
| 37 | - <text class="tip-item">正面照</text> | 33 | + |
| 38 | - <text class="tip-item">左侧照</text> | 34 | + <!-- 左侧照 --> |
| 39 | - <text class="tip-item">右侧照</text> | 35 | + <view class="upload-item"> |
| 40 | - <text class="tip-item">其他</text> | 36 | + <!-- 上传按钮 --> |
| 37 | + <view v-if="!uploadedImages.left" class="upload-button" @click="triggerUpload('left')"> | ||
| 38 | + <Plus class="upload-icon" /> | ||
| 39 | + </view> | ||
| 40 | + <!-- 图片预览 --> | ||
| 41 | + <view v-else class="image-preview" @click="previewImage(uploadedImages.left)"> | ||
| 42 | + <image :src="uploadedImages.left" class="preview-image" mode="aspectFill" /> | ||
| 43 | + <view class="delete-btn" @click.stop="deleteImage('left')"> | ||
| 44 | + <Close class="delete-icon" /> | ||
| 45 | + </view> | ||
| 46 | + </view> | ||
| 47 | + <text class="upload-label">左侧照</text> | ||
| 48 | + </view> | ||
| 49 | + | ||
| 50 | + <!-- 右侧照 --> | ||
| 51 | + <view class="upload-item"> | ||
| 52 | + <!-- 上传按钮 --> | ||
| 53 | + <view v-if="!uploadedImages.right" class="upload-button" @click="triggerUpload('right')"> | ||
| 54 | + <Plus class="upload-icon" /> | ||
| 55 | + </view> | ||
| 56 | + <!-- 图片预览 --> | ||
| 57 | + <view v-else class="image-preview" @click="previewImage(uploadedImages.right)"> | ||
| 58 | + <image :src="uploadedImages.right" class="preview-image" mode="aspectFill" /> | ||
| 59 | + <view class="delete-btn" @click.stop="deleteImage('right')"> | ||
| 60 | + <Close class="delete-icon" /> | ||
| 61 | + </view> | ||
| 62 | + </view> | ||
| 63 | + <text class="upload-label">右侧照</text> | ||
| 64 | + </view> | ||
| 65 | + | ||
| 66 | + <!-- 其他 --> | ||
| 67 | + <view class="upload-item"> | ||
| 68 | + <!-- 上传按钮 --> | ||
| 69 | + <view v-if="!uploadedImages.other" class="upload-button" @click="triggerUpload('other')"> | ||
| 70 | + <Plus class="upload-icon" /> | ||
| 71 | + </view> | ||
| 72 | + <!-- 图片预览 --> | ||
| 73 | + <view v-else class="image-preview" @click="previewImage(uploadedImages.other)"> | ||
| 74 | + <image :src="uploadedImages.other" class="preview-image" mode="aspectFill" /> | ||
| 75 | + <view class="delete-btn" @click.stop="deleteImage('other')"> | ||
| 76 | + <Close class="delete-icon" /> | ||
| 77 | + </view> | ||
| 78 | + </view> | ||
| 79 | + <text class="upload-label">其他</text> | ||
| 80 | + </view> | ||
| 41 | </view> | 81 | </view> |
| 82 | + | ||
| 83 | + <!-- 图片预览组件 --> | ||
| 84 | + <nut-image-preview v-model:show="previewVisible" :images="previewImages" :init-no="previewIndex" /> | ||
| 42 | </view> | 85 | </view> |
| 43 | 86 | ||
| 44 | <!-- 所在学校 --> | 87 | <!-- 所在学校 --> |
| ... | @@ -59,12 +102,7 @@ | ... | @@ -59,12 +102,7 @@ |
| 59 | <nut-form ref="formRef" :model-value="formData"> | 102 | <nut-form ref="formRef" :model-value="formData"> |
| 60 | <view class="form-section"> | 103 | <view class="form-section"> |
| 61 | <!-- 车型品牌 --> | 104 | <!-- 车型品牌 --> |
| 62 | - <nut-form-item | 105 | + <nut-form-item label-position="top" label="车型品牌" prop="brand" required :rules="[{ required: true, message: '请选择车型品牌' }]"> |
| 63 | - label="车型品牌" | ||
| 64 | - prop="brand" | ||
| 65 | - required | ||
| 66 | - :rules="[{ required: true, message: '请选择车型品牌' }]" | ||
| 67 | - > | ||
| 68 | <view class="form-item-content" @click="showBrandPicker"> | 106 | <view class="form-item-content" @click="showBrandPicker"> |
| 69 | <text class="form-value">{{ formData.brand || '请选择' }}</text> | 107 | <text class="form-value">{{ formData.brand || '请选择' }}</text> |
| 70 | <Right class="arrow-icon" /> | 108 | <Right class="arrow-icon" /> |
| ... | @@ -72,7 +110,7 @@ | ... | @@ -72,7 +110,7 @@ |
| 72 | </nut-form-item> | 110 | </nut-form-item> |
| 73 | 111 | ||
| 74 | <!-- 车辆型号 --> | 112 | <!-- 车辆型号 --> |
| 75 | - <nut-form-item label="车辆型号" prop="model"> | 113 | + <nut-form-item label-position="top" label="车辆型号" prop="model"> |
| 76 | <view class="form-item-content" @click="showModelPicker"> | 114 | <view class="form-item-content" @click="showModelPicker"> |
| 77 | <text class="form-value">{{ formData.model || '请选择' }}</text> | 115 | <text class="form-value">{{ formData.model || '请选择' }}</text> |
| 78 | <Right class="arrow-icon" /> | 116 | <Right class="arrow-icon" /> |
| ... | @@ -80,7 +118,7 @@ | ... | @@ -80,7 +118,7 @@ |
| 80 | </nut-form-item> | 118 | </nut-form-item> |
| 81 | 119 | ||
| 82 | <!-- 车辆出厂年份 --> | 120 | <!-- 车辆出厂年份 --> |
| 83 | - <nut-form-item label="车辆出厂年份" prop="year"> | 121 | + <nut-form-item label-position="top" label="车辆出厂年份" prop="year"> |
| 84 | <view class="form-item-content" @click="showYearPicker"> | 122 | <view class="form-item-content" @click="showYearPicker"> |
| 85 | <text class="form-value">{{ formData.year || '请选择' }}</text> | 123 | <text class="form-value">{{ formData.year || '请选择' }}</text> |
| 86 | <Right class="arrow-icon" /> | 124 | <Right class="arrow-icon" /> |
| ... | @@ -88,12 +126,8 @@ | ... | @@ -88,12 +126,8 @@ |
| 88 | </nut-form-item> | 126 | </nut-form-item> |
| 89 | 127 | ||
| 90 | <!-- 新旧程度 --> | 128 | <!-- 新旧程度 --> |
| 91 | - <nut-form-item | 129 | + <nut-form-item label-position="top" label="新旧程度" prop="condition" required |
| 92 | - label="新旧程度" | 130 | + :rules="[{ required: true, message: '请选择新旧程度' }]"> |
| 93 | - prop="condition" | ||
| 94 | - required | ||
| 95 | - :rules="[{ required: true, message: '请选择新旧程度' }]" | ||
| 96 | - > | ||
| 97 | <view class="form-item-content" @click="showConditionPicker"> | 131 | <view class="form-item-content" @click="showConditionPicker"> |
| 98 | <text class="form-value">{{ formData.condition || '请选择' }}</text> | 132 | <text class="form-value">{{ formData.condition || '请选择' }}</text> |
| 99 | <Right class="arrow-icon" /> | 133 | <Right class="arrow-icon" /> |
| ... | @@ -101,18 +135,9 @@ | ... | @@ -101,18 +135,9 @@ |
| 101 | </nut-form-item> | 135 | </nut-form-item> |
| 102 | 136 | ||
| 103 | <!-- 行驶里程 --> | 137 | <!-- 行驶里程 --> |
| 104 | - <nut-form-item | 138 | + <nut-form-item label="行驶里程" prop="mileage" required |
| 105 | - label="行驶里程" | 139 | + :rules="[{ required: true, message: '请输入行驶里程' }]"> |
| 106 | - prop="mileage" | 140 | + <nut-input v-model="formData.mileage" placeholder="1200" type="number" input-align="right"> |
| 107 | - required | ||
| 108 | - :rules="[{ required: true, message: '请输入行驶里程' }]" | ||
| 109 | - > | ||
| 110 | - <nut-input | ||
| 111 | - v-model="formData.mileage" | ||
| 112 | - placeholder="1200" | ||
| 113 | - type="number" | ||
| 114 | - input-align="right" | ||
| 115 | - > | ||
| 116 | <template #right> | 141 | <template #right> |
| 117 | <text class="unit">公里</text> | 142 | <text class="unit">公里</text> |
| 118 | </template> | 143 | </template> |
| ... | @@ -121,12 +146,7 @@ | ... | @@ -121,12 +146,7 @@ |
| 121 | 146 | ||
| 122 | <!-- 续航里程 --> | 147 | <!-- 续航里程 --> |
| 123 | <nut-form-item label="续航里程" prop="range"> | 148 | <nut-form-item label="续航里程" prop="range"> |
| 124 | - <nut-input | 149 | + <nut-input v-model="formData.range" placeholder="60" type="number" input-align="right"> |
| 125 | - v-model="formData.range" | ||
| 126 | - placeholder="60" | ||
| 127 | - type="number" | ||
| 128 | - input-align="right" | ||
| 129 | - > | ||
| 130 | <template #right> | 150 | <template #right> |
| 131 | <text class="unit">公里</text> | 151 | <text class="unit">公里</text> |
| 132 | </template> | 152 | </template> |
| ... | @@ -135,12 +155,7 @@ | ... | @@ -135,12 +155,7 @@ |
| 135 | 155 | ||
| 136 | <!-- 最高时速 --> | 156 | <!-- 最高时速 --> |
| 137 | <nut-form-item label="最高时速" prop="maxSpeed"> | 157 | <nut-form-item label="最高时速" prop="maxSpeed"> |
| 138 | - <nut-input | 158 | + <nut-input v-model="formData.maxSpeed" placeholder="25" type="number" input-align="right"> |
| 139 | - v-model="formData.maxSpeed" | ||
| 140 | - placeholder="25" | ||
| 141 | - type="number" | ||
| 142 | - input-align="right" | ||
| 143 | - > | ||
| 144 | <template #right> | 159 | <template #right> |
| 145 | <text class="unit">km/h</text> | 160 | <text class="unit">km/h</text> |
| 146 | </template> | 161 | </template> |
| ... | @@ -149,12 +164,8 @@ | ... | @@ -149,12 +164,8 @@ |
| 149 | 164 | ||
| 150 | <!-- 电池容量 --> | 165 | <!-- 电池容量 --> |
| 151 | <nut-form-item label="电池容量" prop="batteryCapacity"> | 166 | <nut-form-item label="电池容量" prop="batteryCapacity"> |
| 152 | - <nut-input | 167 | + <nut-input v-model="formData.batteryCapacity" placeholder="20" type="number" |
| 153 | - v-model="formData.batteryCapacity" | 168 | + input-align="right"> |
| 154 | - placeholder="20" | ||
| 155 | - type="number" | ||
| 156 | - input-align="right" | ||
| 157 | - > | ||
| 158 | <template #right> | 169 | <template #right> |
| 159 | <text class="unit">Ah</text> | 170 | <text class="unit">Ah</text> |
| 160 | </template> | 171 | </template> |
| ... | @@ -162,7 +173,7 @@ | ... | @@ -162,7 +173,7 @@ |
| 162 | </nut-form-item> | 173 | </nut-form-item> |
| 163 | 174 | ||
| 164 | <!-- 电池损耗度 --> | 175 | <!-- 电池损耗度 --> |
| 165 | - <nut-form-item label="电池损耗度" prop="batteryWear"> | 176 | + <nut-form-item label-position="top" label="电池损耗度" prop="batteryWear"> |
| 166 | <view class="form-item-content" @click="showBatteryWearPicker"> | 177 | <view class="form-item-content" @click="showBatteryWearPicker"> |
| 167 | <text class="form-value">{{ formData.batteryWear || '请选择' }}</text> | 178 | <text class="form-value">{{ formData.batteryWear || '请选择' }}</text> |
| 168 | <Right class="arrow-icon" /> | 179 | <Right class="arrow-icon" /> |
| ... | @@ -170,7 +181,7 @@ | ... | @@ -170,7 +181,7 @@ |
| 170 | </nut-form-item> | 181 | </nut-form-item> |
| 171 | 182 | ||
| 172 | <!-- 刹车磨损度 --> | 183 | <!-- 刹车磨损度 --> |
| 173 | - <nut-form-item label="刹车磨损度" prop="brakeWear"> | 184 | + <nut-form-item label-position="top" label="刹车磨损度" prop="brakeWear"> |
| 174 | <view class="form-item-content" @click="showBrakeWearPicker"> | 185 | <view class="form-item-content" @click="showBrakeWearPicker"> |
| 175 | <text class="form-value">{{ formData.brakeWear || '请选择' }}</text> | 186 | <text class="form-value">{{ formData.brakeWear || '请选择' }}</text> |
| 176 | <Right class="arrow-icon" /> | 187 | <Right class="arrow-icon" /> |
| ... | @@ -178,7 +189,7 @@ | ... | @@ -178,7 +189,7 @@ |
| 178 | </nut-form-item> | 189 | </nut-form-item> |
| 179 | 190 | ||
| 180 | <!-- 轮胎磨损度 --> | 191 | <!-- 轮胎磨损度 --> |
| 181 | - <nut-form-item label="轮胎磨损度" prop="tireWear"> | 192 | + <nut-form-item label-position="top" label="轮胎磨损度" prop="tireWear"> |
| 182 | <view class="form-item-content" @click="showTireWearPicker"> | 193 | <view class="form-item-content" @click="showTireWearPicker"> |
| 183 | <text class="form-value">{{ formData.tireWear || '请选择' }}</text> | 194 | <text class="form-value">{{ formData.tireWear || '请选择' }}</text> |
| 184 | <Right class="arrow-icon" /> | 195 | <Right class="arrow-icon" /> |
| ... | @@ -196,12 +207,7 @@ | ... | @@ -196,12 +207,7 @@ |
| 196 | </view> | 207 | </view> |
| 197 | <view class="form-item-right"> | 208 | <view class="form-item-right"> |
| 198 | <text class="price-symbol">¥</text> | 209 | <text class="price-symbol">¥</text> |
| 199 | - <input | 210 | + <input v-model="formData.sellingPrice" placeholder="3,200" type="text" class="price-input" /> |
| 200 | - v-model="formData.sellingPrice" | ||
| 201 | - placeholder="3,200" | ||
| 202 | - type="text" | ||
| 203 | - class="price-input" | ||
| 204 | - /> | ||
| 205 | </view> | 211 | </view> |
| 206 | </view> | 212 | </view> |
| 207 | 213 | ||
| ... | @@ -212,36 +218,22 @@ | ... | @@ -212,36 +218,22 @@ |
| 212 | </view> | 218 | </view> |
| 213 | <view class="form-item-right"> | 219 | <view class="form-item-right"> |
| 214 | <text class="market-price-symbol">¥</text> | 220 | <text class="market-price-symbol">¥</text> |
| 215 | - <input | 221 | + <input v-model="formData.marketPrice" placeholder="6,500" type="text" |
| 216 | - v-model="formData.marketPrice" | 222 | + class="market-price-input" /> |
| 217 | - placeholder="6,500" | ||
| 218 | - type="text" | ||
| 219 | - class="market-price-input" | ||
| 220 | - /> | ||
| 221 | </view> | 223 | </view> |
| 222 | </view> | 224 | </view> |
| 223 | </view> | 225 | </view> |
| 224 | 226 | ||
| 225 | <!-- 车辆描述 --> | 227 | <!-- 车辆描述 --> |
| 226 | <view class="form-section"> | 228 | <view class="form-section"> |
| 227 | - <nut-textarea | 229 | + <nut-textarea v-model="formData.description" placeholder="请描述车辆详情,如使用感受、车况特点等" :max-length="200" |
| 228 | - v-model="formData.description" | 230 | + :rows="4" show-word-limit /> |
| 229 | - placeholder="请描述车辆详情,如使用感受、车况特点等" | ||
| 230 | - :max-length="200" | ||
| 231 | - :rows="4" | ||
| 232 | - show-word-limit | ||
| 233 | - /> | ||
| 234 | </view> | 231 | </view> |
| 235 | </view> | 232 | </view> |
| 236 | 233 | ||
| 237 | <!-- 底部按钮 --> | 234 | <!-- 底部按钮 --> |
| 238 | <view class="bottom-actions"> | 235 | <view class="bottom-actions"> |
| 239 | - <nut-button | 236 | + <nut-button color="#f97316" size="large" block @click="onPublish"> |
| 240 | - color="#f97316" | ||
| 241 | - size="large" | ||
| 242 | - block | ||
| 243 | - @click="onPublish" | ||
| 244 | - > | ||
| 245 | 确认发布 | 237 | 确认发布 |
| 246 | </nut-button> | 238 | </nut-button> |
| 247 | </view> | 239 | </view> |
| ... | @@ -249,106 +241,83 @@ | ... | @@ -249,106 +241,83 @@ |
| 249 | <!-- 选择器弹窗 --> | 241 | <!-- 选择器弹窗 --> |
| 250 | <!-- 学校选择 --> | 242 | <!-- 学校选择 --> |
| 251 | <nut-popup v-model:visible="schoolPickerVisible" position="bottom"> | 243 | <nut-popup v-model:visible="schoolPickerVisible" position="bottom"> |
| 252 | - <nut-picker | 244 | + <nut-picker v-model="schoolValue" :columns="schoolOptions" title="选择学校" @confirm="onSchoolConfirm" |
| 253 | - v-model="schoolValue" | 245 | + @cancel="schoolPickerVisible = false" /> |
| 254 | - :columns="schoolOptions" | ||
| 255 | - title="选择学校" | ||
| 256 | - @confirm="onSchoolConfirm" | ||
| 257 | - @cancel="schoolPickerVisible = false" | ||
| 258 | - /> | ||
| 259 | </nut-popup> | 246 | </nut-popup> |
| 260 | 247 | ||
| 261 | <!-- 品牌选择 --> | 248 | <!-- 品牌选择 --> |
| 262 | <nut-popup v-model:visible="brandPickerVisible" position="bottom"> | 249 | <nut-popup v-model:visible="brandPickerVisible" position="bottom"> |
| 263 | - <nut-picker | 250 | + <nut-picker v-model="brandValue" :columns="brandOptions" title="选择车型品牌" @confirm="onBrandConfirm" |
| 264 | - v-model="brandValue" | 251 | + @cancel="brandPickerVisible = false" /> |
| 265 | - :columns="brandOptions" | ||
| 266 | - title="选择车型品牌" | ||
| 267 | - @confirm="onBrandConfirm" | ||
| 268 | - @cancel="brandPickerVisible = false" | ||
| 269 | - /> | ||
| 270 | </nut-popup> | 252 | </nut-popup> |
| 271 | 253 | ||
| 272 | <!-- 型号选择 --> | 254 | <!-- 型号选择 --> |
| 273 | <nut-popup v-model:visible="modelPickerVisible" position="bottom"> | 255 | <nut-popup v-model:visible="modelPickerVisible" position="bottom"> |
| 274 | - <nut-picker | 256 | + <nut-picker v-model="modelValue" :columns="modelOptions" title="选择车辆型号" @confirm="onModelConfirm" |
| 275 | - v-model="modelValue" | 257 | + @cancel="modelPickerVisible = false" /> |
| 276 | - :columns="modelOptions" | ||
| 277 | - title="选择车辆型号" | ||
| 278 | - @confirm="onModelConfirm" | ||
| 279 | - @cancel="modelPickerVisible = false" | ||
| 280 | - /> | ||
| 281 | </nut-popup> | 258 | </nut-popup> |
| 282 | 259 | ||
| 283 | <!-- 年份选择 --> | 260 | <!-- 年份选择 --> |
| 284 | <nut-popup v-model:visible="yearPickerVisible" position="bottom"> | 261 | <nut-popup v-model:visible="yearPickerVisible" position="bottom"> |
| 285 | - <nut-picker | 262 | + <nut-picker v-model="yearValue" :columns="yearOptions" title="选择出厂年份" @confirm="onYearConfirm" |
| 286 | - v-model="yearValue" | 263 | + @cancel="yearPickerVisible = false" /> |
| 287 | - :columns="yearOptions" | ||
| 288 | - title="选择出厂年份" | ||
| 289 | - @confirm="onYearConfirm" | ||
| 290 | - @cancel="yearPickerVisible = false" | ||
| 291 | - /> | ||
| 292 | </nut-popup> | 264 | </nut-popup> |
| 293 | 265 | ||
| 294 | <!-- 新旧程度选择 --> | 266 | <!-- 新旧程度选择 --> |
| 295 | <nut-popup v-model:visible="conditionPickerVisible" position="bottom"> | 267 | <nut-popup v-model:visible="conditionPickerVisible" position="bottom"> |
| 296 | - <nut-picker | 268 | + <nut-picker v-model="conditionValue" :columns="conditionOptions" title="选择新旧程度" |
| 297 | - v-model="conditionValue" | 269 | + @confirm="onConditionConfirm" @cancel="conditionPickerVisible = false" /> |
| 298 | - :columns="conditionOptions" | ||
| 299 | - title="选择新旧程度" | ||
| 300 | - @confirm="onConditionConfirm" | ||
| 301 | - @cancel="conditionPickerVisible = false" | ||
| 302 | - /> | ||
| 303 | </nut-popup> | 270 | </nut-popup> |
| 304 | 271 | ||
| 305 | <!-- 电池损耗度选择 --> | 272 | <!-- 电池损耗度选择 --> |
| 306 | <nut-popup v-model:visible="batteryWearPickerVisible" position="bottom"> | 273 | <nut-popup v-model:visible="batteryWearPickerVisible" position="bottom"> |
| 307 | - <nut-picker | 274 | + <nut-picker v-model="batteryWearValue" :columns="wearLevelOptions" title="选择电池损耗度" |
| 308 | - v-model="batteryWearValue" | 275 | + @confirm="onBatteryWearConfirm" @cancel="batteryWearPickerVisible = false" /> |
| 309 | - :columns="wearLevelOptions" | ||
| 310 | - title="选择电池损耗度" | ||
| 311 | - @confirm="onBatteryWearConfirm" | ||
| 312 | - @cancel="batteryWearPickerVisible = false" | ||
| 313 | - /> | ||
| 314 | </nut-popup> | 276 | </nut-popup> |
| 315 | 277 | ||
| 316 | <!-- 刹车磨损度选择 --> | 278 | <!-- 刹车磨损度选择 --> |
| 317 | <nut-popup v-model:visible="brakeWearPickerVisible" position="bottom"> | 279 | <nut-popup v-model:visible="brakeWearPickerVisible" position="bottom"> |
| 318 | - <nut-picker | 280 | + <nut-picker v-model="brakeWearValue" :columns="wearLevelOptions" title="选择刹车磨损度" |
| 319 | - v-model="brakeWearValue" | 281 | + @confirm="onBrakeWearConfirm" @cancel="brakeWearPickerVisible = false" /> |
| 320 | - :columns="wearLevelOptions" | ||
| 321 | - title="选择刹车磨损度" | ||
| 322 | - @confirm="onBrakeWearConfirm" | ||
| 323 | - @cancel="brakeWearPickerVisible = false" | ||
| 324 | - /> | ||
| 325 | </nut-popup> | 282 | </nut-popup> |
| 326 | 283 | ||
| 327 | <!-- 轮胎磨损度选择 --> | 284 | <!-- 轮胎磨损度选择 --> |
| 328 | <nut-popup v-model:visible="tireWearPickerVisible" position="bottom"> | 285 | <nut-popup v-model:visible="tireWearPickerVisible" position="bottom"> |
| 329 | - <nut-picker | 286 | + <nut-picker v-model="tireWearValue" :columns="wearLevelOptions" title="选择轮胎磨损度" @confirm="onTireWearConfirm" |
| 330 | - v-model="tireWearValue" | 287 | + @cancel="tireWearPickerVisible = false" /> |
| 331 | - :columns="wearLevelOptions" | ||
| 332 | - title="选择轮胎磨损度" | ||
| 333 | - @confirm="onTireWearConfirm" | ||
| 334 | - @cancel="tireWearPickerVisible = false" | ||
| 335 | - /> | ||
| 336 | </nut-popup> | 288 | </nut-popup> |
| 337 | </view> | 289 | </view> |
| 338 | </template> | 290 | </template> |
| 339 | 291 | ||
| 340 | <script setup> | 292 | <script setup> |
| 341 | import { ref, reactive } from 'vue' | 293 | import { ref, reactive } from 'vue' |
| 342 | -import { Plus, Right, Location, RectLeft } from '@nutui/icons-vue-taro' | 294 | +import { Plus, Right, Location, RectLeft, Close } from '@nutui/icons-vue-taro' |
| 343 | import Taro from '@tarojs/taro' | 295 | import Taro from '@tarojs/taro' |
| 296 | +// import BASE_URL from '@/utils/config'; | ||
| 344 | 297 | ||
| 345 | const themeVars = ref({ | 298 | const themeVars = ref({ |
| 346 | // navbarBackground: '#FFA135', | 299 | // navbarBackground: '#FFA135', |
| 347 | // navbarColor: '#ffffff', | 300 | // navbarColor: '#ffffff', |
| 348 | }) | 301 | }) |
| 349 | 302 | ||
| 350 | -// 文件上传列表 | 303 | +// 文件上传相关 |
| 351 | -const fileList = ref([]) | 304 | +// const frontFileList = ref([]) // 正面照(保留用于兼容旧代码) |
| 305 | +// const leftFileList = ref([]) // 左侧照(保留用于兼容旧代码) | ||
| 306 | +// const rightFileList = ref([]) // 右侧照(保留用于兼容旧代码) | ||
| 307 | +// const otherFileList = ref([]) // 其他照片(保留用于兼容旧代码) | ||
| 308 | + | ||
| 309 | +// 已上传图片的URL | ||
| 310 | +const uploadedImages = reactive({ | ||
| 311 | + front: '', | ||
| 312 | + left: '', | ||
| 313 | + right: '', | ||
| 314 | + other: '' | ||
| 315 | +}) | ||
| 316 | + | ||
| 317 | +// 图片预览相关 | ||
| 318 | +const previewVisible = ref(false) | ||
| 319 | +const previewImages = ref([]) | ||
| 320 | +const previewIndex = ref(0) | ||
| 352 | 321 | ||
| 353 | // 表单数据 | 322 | // 表单数据 |
| 354 | const formData = reactive({ | 323 | const formData = reactive({ |
| ... | @@ -459,23 +428,133 @@ const goBack = () => { | ... | @@ -459,23 +428,133 @@ const goBack = () => { |
| 459 | } | 428 | } |
| 460 | 429 | ||
| 461 | /** | 430 | /** |
| 462 | - * 文件上传超出限制 | 431 | + * 触发图片上传 |
| 432 | + * @param {String} type - 图片类型 (front/left/right/other) | ||
| 463 | */ | 433 | */ |
| 464 | -const onOversize = () => { | 434 | +const triggerUpload = (type) => { |
| 465 | - Taro.showToast({ | 435 | + Taro.chooseImage({ |
| 466 | - title: '文件大小超出限制', | 436 | + count: 1, |
| 467 | - icon: 'none' | 437 | + sizeType: ['compressed'], |
| 438 | + sourceType: ['album', 'camera'], | ||
| 439 | + success: function (res) { | ||
| 440 | + const tempFilePath = res.tempFilePaths[0] | ||
| 441 | + uploadImage(tempFilePath, type) | ||
| 442 | + }, | ||
| 443 | + fail: function () { | ||
| 444 | + Taro.showToast({ | ||
| 445 | + title: '选择图片失败', | ||
| 446 | + icon: 'none' | ||
| 447 | + }) | ||
| 448 | + } | ||
| 468 | }) | 449 | }) |
| 469 | } | 450 | } |
| 470 | 451 | ||
| 471 | /** | 452 | /** |
| 472 | - * 删除上传的文件 | 453 | + * 上传图片到服务器 |
| 454 | + * @param {String} filePath - 图片文件路径 | ||
| 455 | + * @param {String} type - 图片类型 (front/left/right/other) | ||
| 473 | */ | 456 | */ |
| 474 | -const onDelete = () => { | 457 | +const uploadImage = (filePath, type) => { |
| 475 | - // 删除逻辑已由组件内部处理 | 458 | + // 显示上传中提示 |
| 459 | + Taro.showLoading({ | ||
| 460 | + title: '上传中', | ||
| 461 | + mask: true | ||
| 462 | + }) | ||
| 463 | + | ||
| 464 | + // 模拟上传成功(实际项目中替换为真实的上传逻辑) | ||
| 465 | + setTimeout(() => { | ||
| 466 | + Taro.hideLoading() | ||
| 467 | + | ||
| 468 | + // 模拟服务器返回的图片URL | ||
| 469 | + const mockImageUrl = filePath // 在实际项目中,这里应该是服务器返回的URL | ||
| 470 | + | ||
| 471 | + // 更新对应类型的图片 | ||
| 472 | + uploadedImages[type] = mockImageUrl | ||
| 473 | + | ||
| 474 | + Taro.showToast({ | ||
| 475 | + title: '上传成功', | ||
| 476 | + icon: 'success' | ||
| 477 | + }) | ||
| 478 | + }, 1500) | ||
| 479 | + | ||
| 480 | + // 真实上传逻辑(注释掉的代码供参考) | ||
| 481 | + /* | ||
| 482 | + Taro.uploadFile({ | ||
| 483 | + url: BASE_URL + '/admin/?m=srv&a=upload', | ||
| 484 | + filePath: filePath, | ||
| 485 | + name: 'file', | ||
| 486 | + header: { | ||
| 487 | + 'content-type': 'multipart/form-data' | ||
| 488 | + }, | ||
| 489 | + success: function (res) { | ||
| 490 | + try { | ||
| 491 | + const upload_data = JSON.parse(res.data) | ||
| 492 | + Taro.hideLoading() | ||
| 493 | + | ||
| 494 | + if (res.statusCode === 200 && upload_data.data) { | ||
| 495 | + // 上传成功,更新对应类型的图片 | ||
| 496 | + uploadedImages[type] = upload_data.data.src | ||
| 497 | + | ||
| 498 | + Taro.showToast({ | ||
| 499 | + title: '上传成功', | ||
| 500 | + icon: 'success' | ||
| 501 | + }) | ||
| 502 | + } else { | ||
| 503 | + throw new Error('上传失败') | ||
| 504 | + } | ||
| 505 | + } catch (error) { | ||
| 506 | + Taro.hideLoading() | ||
| 507 | + Taro.showToast({ | ||
| 508 | + icon: 'error', | ||
| 509 | + title: '服务器错误,稍后重试!', | ||
| 510 | + mask: true | ||
| 511 | + }) | ||
| 512 | + } | ||
| 513 | + }, | ||
| 514 | + fail: function () { | ||
| 515 | + Taro.hideLoading() | ||
| 516 | + Taro.showToast({ | ||
| 517 | + icon: 'error', | ||
| 518 | + title: '上传失败,请重试', | ||
| 519 | + mask: true | ||
| 520 | + }) | ||
| 521 | + } | ||
| 522 | + }) | ||
| 523 | + */ | ||
| 476 | } | 524 | } |
| 477 | 525 | ||
| 478 | /** | 526 | /** |
| 527 | + * 预览图片 | ||
| 528 | + * @param {String} imageUrl - 图片URL | ||
| 529 | + */ | ||
| 530 | +const previewImage = (imageUrl) => { | ||
| 531 | + previewImages.value = [{ src: imageUrl }] | ||
| 532 | + previewIndex.value = 0 | ||
| 533 | + previewVisible.value = true | ||
| 534 | +} | ||
| 535 | + | ||
| 536 | +/** | ||
| 537 | + * 删除图片 | ||
| 538 | + * @param {String} type - 图片类型 (front/left/right/other) | ||
| 539 | + */ | ||
| 540 | +const deleteImage = (type) => { | ||
| 541 | + uploadedImages[type] = '' | ||
| 542 | + Taro.showToast({ | ||
| 543 | + title: '删除成功', | ||
| 544 | + icon: 'success' | ||
| 545 | + }) | ||
| 546 | +} | ||
| 547 | + | ||
| 548 | +// 保留旧的上传逻辑作为备用(已注释) | ||
| 549 | +// const afterRead = (event, type) => { ... } | ||
| 550 | + | ||
| 551 | +// 保留旧的删除逻辑作为备用(已注释) | ||
| 552 | +// const onDeleteFront = (file, index) => { frontFileList.value.splice(index, 1) } | ||
| 553 | +// const onDeleteLeft = (file, index) => { leftFileList.value.splice(index, 1) } | ||
| 554 | +// const onDeleteRight = (file, index) => { rightFileList.value.splice(index, 1) } | ||
| 555 | +// const onDeleteOther = (file, index) => { otherFileList.value.splice(index, 1) } | ||
| 556 | + | ||
| 557 | +/** | ||
| 479 | * 显示学校选择器 | 558 | * 显示学校选择器 |
| 480 | */ | 559 | */ |
| 481 | const showSchoolPicker = () => { | 560 | const showSchoolPicker = () => { |
| ... | @@ -611,6 +690,25 @@ const onPublish = () => { | ... | @@ -611,6 +690,25 @@ const onPublish = () => { |
| 611 | 690 | ||
| 612 | Taro.showLoading({ title: '发布中...' }) | 691 | Taro.showLoading({ title: '发布中...' }) |
| 613 | 692 | ||
| 693 | + // 收集所有上传的图片URL | ||
| 694 | + const images = { | ||
| 695 | + front: uploadedImages.front || '', | ||
| 696 | + left: uploadedImages.left || '', | ||
| 697 | + right: uploadedImages.right || '', | ||
| 698 | + other: uploadedImages.other || '' | ||
| 699 | + } | ||
| 700 | + | ||
| 701 | + // 构建提交数据 | ||
| 702 | + const submitData = { | ||
| 703 | + ...formData, | ||
| 704 | + images: images, | ||
| 705 | + imageUrls: Object.values(images).filter(url => url) // 过滤空URL | ||
| 706 | + } | ||
| 707 | + | ||
| 708 | + // 发布车辆信息数据已准备完成 | ||
| 709 | + // TODO: 在此处调用实际的API接口提交数据 | ||
| 710 | + // console.log('提交数据:', submitData) | ||
| 711 | + | ||
| 614 | // 模拟发布请求 | 712 | // 模拟发布请求 |
| 615 | setTimeout(() => { | 713 | setTimeout(() => { |
| 616 | Taro.hideLoading() | 714 | Taro.hideLoading() |
| ... | @@ -631,7 +729,9 @@ const onPublish = () => { | ... | @@ -631,7 +729,9 @@ const onPublish = () => { |
| 631 | * @returns {boolean} 验证结果 | 729 | * @returns {boolean} 验证结果 |
| 632 | */ | 730 | */ |
| 633 | const validateForm = () => { | 731 | const validateForm = () => { |
| 634 | - if (fileList.value.length === 0) { | 732 | + // 检查是否至少上传了一张图片 |
| 733 | + const hasImages = uploadedImages.front || uploadedImages.left || uploadedImages.right || uploadedImages.other | ||
| 734 | + if (!hasImages) { | ||
| 635 | Taro.showToast({ title: '请上传车辆图片', icon: 'none' }) | 735 | Taro.showToast({ title: '请上传车辆图片', icon: 'none' }) |
| 636 | return false | 736 | return false |
| 637 | } | 737 | } |
| ... | @@ -669,6 +769,7 @@ const validateForm = () => { | ... | @@ -669,6 +769,7 @@ const validateForm = () => { |
| 669 | 769 | ||
| 670 | .form-container { | 770 | .form-container { |
| 671 | padding: 0 32rpx; | 771 | padding: 0 32rpx; |
| 772 | + margin-bottom: 2rem; | ||
| 672 | } | 773 | } |
| 673 | 774 | ||
| 674 | .form-section { | 775 | .form-section { |
| ... | @@ -686,17 +787,84 @@ const validateForm = () => { | ... | @@ -686,17 +787,84 @@ const validateForm = () => { |
| 686 | display: block; | 787 | display: block; |
| 687 | } | 788 | } |
| 688 | 789 | ||
| 689 | -.upload-tips { | 790 | +.upload-grid { |
| 690 | - display: flex; | 791 | + display: grid; |
| 691 | - margin-top: 16rpx; | 792 | + grid-template-columns: repeat(2, 1fr); |
| 692 | gap: 32rpx; | 793 | gap: 32rpx; |
| 794 | + margin-top: 24rpx; | ||
| 693 | } | 795 | } |
| 694 | 796 | ||
| 695 | -.tip-item { | 797 | +.upload-item { |
| 696 | - font-size: 24rpx; | 798 | + display: flex; |
| 799 | + flex-direction: column; | ||
| 800 | + align-items: center; | ||
| 801 | + gap: 16rpx; | ||
| 802 | +} | ||
| 803 | + | ||
| 804 | +.upload-button { | ||
| 805 | + width: 160rpx; | ||
| 806 | + height: 160rpx; | ||
| 807 | + border: 2rpx dashed #d1d5db; | ||
| 808 | + border-radius: 16rpx; | ||
| 809 | + display: flex; | ||
| 810 | + align-items: center; | ||
| 811 | + justify-content: center; | ||
| 812 | + background-color: #f9fafb; | ||
| 813 | + transition: all 0.3s ease; | ||
| 814 | +} | ||
| 815 | + | ||
| 816 | +.upload-button:active { | ||
| 817 | + background-color: #f3f4f6; | ||
| 818 | + border-color: #f97316; | ||
| 819 | +} | ||
| 820 | + | ||
| 821 | +.upload-icon { | ||
| 822 | + font-size: 48rpx; | ||
| 697 | color: #9ca3af; | 823 | color: #9ca3af; |
| 698 | } | 824 | } |
| 699 | 825 | ||
| 826 | +.upload-label { | ||
| 827 | + font-size: 24rpx; | ||
| 828 | + color: #6b7280; | ||
| 829 | + text-align: center; | ||
| 830 | +} | ||
| 831 | + | ||
| 832 | +.image-preview { | ||
| 833 | + position: relative; | ||
| 834 | + width: 160rpx; | ||
| 835 | + height: 160rpx; | ||
| 836 | + border-radius: 16rpx; | ||
| 837 | + overflow: hidden; | ||
| 838 | +} | ||
| 839 | + | ||
| 840 | +.preview-image { | ||
| 841 | + width: 100%; | ||
| 842 | + height: 100%; | ||
| 843 | + object-fit: cover; | ||
| 844 | +} | ||
| 845 | + | ||
| 846 | +.delete-btn { | ||
| 847 | + position: absolute; | ||
| 848 | + top: 8rpx; | ||
| 849 | + right: 8rpx; | ||
| 850 | + width: 32rpx; | ||
| 851 | + height: 32rpx; | ||
| 852 | + background-color: #ef4444; | ||
| 853 | + border-radius: 50%; | ||
| 854 | + display: flex; | ||
| 855 | + align-items: center; | ||
| 856 | + justify-content: center; | ||
| 857 | + color: white; | ||
| 858 | + font-size: 16rpx; | ||
| 859 | + z-index: 10; | ||
| 860 | + box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.15); | ||
| 861 | +} | ||
| 862 | + | ||
| 863 | +.delete-icon { | ||
| 864 | + font-size: 16rpx; | ||
| 865 | + color: white; | ||
| 866 | +} | ||
| 867 | + | ||
| 700 | .form-item { | 868 | .form-item { |
| 701 | display: flex; | 869 | display: flex; |
| 702 | justify-content: space-between; | 870 | justify-content: space-between; |
| ... | @@ -842,20 +1010,43 @@ const validateForm = () => { | ... | @@ -842,20 +1010,43 @@ const validateForm = () => { |
| 842 | } | 1010 | } |
| 843 | 1011 | ||
| 844 | :deep(.nut-uploader) { | 1012 | :deep(.nut-uploader) { |
| 845 | - margin-bottom: 16rpx; | 1013 | + margin-bottom: 0; |
| 846 | } | 1014 | } |
| 847 | 1015 | ||
| 848 | :deep(.nut-uploader__preview) { | 1016 | :deep(.nut-uploader__preview) { |
| 849 | - display: grid; | 1017 | + margin-bottom: 0; |
| 850 | - grid-template-columns: repeat(4, 1fr); | ||
| 851 | - gap: 24rpx; | ||
| 852 | - margin-bottom: 24rpx; | ||
| 853 | } | 1018 | } |
| 854 | 1019 | ||
| 855 | :deep(.nut-uploader__preview-img) { | 1020 | :deep(.nut-uploader__preview-img) { |
| 856 | width: 160rpx; | 1021 | width: 160rpx; |
| 857 | height: 160rpx; | 1022 | height: 160rpx; |
| 858 | border-radius: 16rpx; | 1023 | border-radius: 16rpx; |
| 1024 | + object-fit: cover; | ||
| 1025 | +} | ||
| 1026 | + | ||
| 1027 | +:deep(.nut-uploader__upload) { | ||
| 1028 | + width: 160rpx; | ||
| 1029 | + height: 160rpx; | ||
| 1030 | +} | ||
| 1031 | + | ||
| 1032 | +:deep(.nut-uploader__input) { | ||
| 1033 | + width: 100%; | ||
| 1034 | + height: 100%; | ||
| 1035 | +} | ||
| 1036 | + | ||
| 1037 | +:deep(.nut-uploader__preview-delete) { | ||
| 1038 | + position: absolute; | ||
| 1039 | + top: 8rpx; | ||
| 1040 | + right: 8rpx; | ||
| 1041 | + width: 32rpx; | ||
| 1042 | + height: 32rpx; | ||
| 1043 | + background-color: rgba(0, 0, 0, 0.6); | ||
| 1044 | + border-radius: 50%; | ||
| 1045 | + display: flex; | ||
| 1046 | + align-items: center; | ||
| 1047 | + justify-content: center; | ||
| 1048 | + color: #ffffff; | ||
| 1049 | + font-size: 20rpx; | ||
| 859 | } | 1050 | } |
| 860 | 1051 | ||
| 861 | :deep(.nut-picker__toolbar) { | 1052 | :deep(.nut-picker__toolbar) { | ... | ... |
-
Please register or login to post a comment