hookehuyr

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

- 使用NutUI组件重构整个表单页面,包括导航栏、上传组件、选择器等
- 新增多个表单字段,完善车辆信息收集能力
- 实现表单验证逻辑,确保必填字段完整性
- 添加多种选择器弹窗,优化用户选择体验
- 更新全局组件声明文件,添加所需NutUI组件
......@@ -9,12 +9,20 @@ declare module 'vue' {
export interface GlobalComponents {
NavBar: typeof import('./src/components/navBar.vue')['default']
NutButton: typeof import('@nutui/nutui-taro')['Button']
NutLoading: typeof import('@nutui/nutui-taro')['Loading']
NutConfigProvider: typeof import('@nutui/nutui-taro')['ConfigProvider']
NutForm: typeof import('@nutui/nutui-taro')['Form']
NutFormItem: typeof import('@nutui/nutui-taro')['FormItem']
NutInput: typeof import('@nutui/nutui-taro')['Input']
NutMenu: typeof import('@nutui/nutui-taro')['Menu']
NutMenuItem: typeof import('@nutui/nutui-taro')['MenuItem']
NutNavbar: typeof import('@nutui/nutui-taro')['Navbar']
NutPicker: typeof import('@nutui/nutui-taro')['Picker']
NutPopup: typeof import('@nutui/nutui-taro')['Popup']
NutSearchbar: typeof import('@nutui/nutui-taro')['Searchbar']
NutSwiper: typeof import('@nutui/nutui-taro')['Swiper']
NutSwiperItem: typeof import('@nutui/nutui-taro')['SwiperItem']
NutTextarea: typeof import('@nutui/nutui-taro')['Textarea']
NutUploader: typeof import('@nutui/nutui-taro')['Uploader']
Picker: typeof import('./src/components/time-picker-data/picker.vue')['default']
PosterBuilder: typeof import('./src/components/PosterBuilder/index.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
......
/*
* @Date: 2025-07-01 17:00:22
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-07-02 10:02:29
* @FilePath: /jgdl/src/pages/index/index.config.js
* @Description: 文件描述
*/
export default {
navigationBarTitleText: '首页'
navigationBarTitleText: '首页',
enableShareAppMessage: true,
// navigationBarBackgroundColor: '#fb923c'
}
......
/*
* @Date: 2025-07-01 17:55:11
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-07-02 10:02:19
* @FilePath: /jgdl/src/pages/sell/index.config.js
* @Description: 文件描述
*/
export default {
navigationBarTitleText: '首页'
navigationBarTitleText: '',
// navigationBarBackgroundColor: '#fb923c'
}
......
<template>
<view class="sell-page">
<!-- 顶部标题 -->
<view class="header">
<text class="header-title">发布车辆</text>
<text class="header-subtitle">填写车辆信息,快速出售</text>
</view>
<!-- 顶部导航 -->
<nut-config-provider :theme-vars="themeVars">
<nut-navbar
title="发布车源"
left-show
@on-click-back="goBack"
>
<template #left-show>
<RectLeft />
</template>
</nut-navbar>
</nut-config-provider>
<!-- 表单内容 -->
<view class="form-container">
<!-- 车辆图片 -->
<!-- 车辆照片上传 -->
<view class="form-section">
<text class="section-title">车辆图片 <text class="required">*</text></text>
<view class="image-upload-container">
<view
v-for="(image, index) in vehicleImages"
:key="index"
class="image-item"
>
<image :src="image" class="uploaded-image" mode="aspectFill" />
<view class="image-delete" @click="removeImage(index)">
<Close size="16" color="#ffffff" />
</view>
</view>
<view
v-if="vehicleImages.length < 6"
class="image-upload-btn"
@click="uploadImage"
>
<Plus size="24" color="#9ca3af" />
<text class="upload-text">添加图片</text>
</view>
<text class="section-title">添加车辆照片</text>
<nut-uploader
v-model:file-list="fileList"
:maximum="4"
:multiple="true"
:preview-type="'picture'"
@oversize="onOversize"
@delete="onDelete"
>
<nut-button type="success" size="small">
<template #icon>
<Plus />
</template>
上传文件
</nut-button>
</nut-uploader>
<view class="upload-tips">
<text class="tip-item">正面照</text>
<text class="tip-item">左侧照</text>
<text class="tip-item">右侧照</text>
<text class="tip-item">其他</text>
</view>
<text class="form-tip">最多上传6张图片,第一张为封面图</text>
</view>
<!-- 基本信息 -->
<!-- 所在学校 -->
<view class="form-section">
<text class="section-title">基本信息</text>
<view class="form-item">
<text class="form-label">车辆名称 <text class="required">*</text></text>
<input
v-model="formData.name"
placeholder="请输入车辆名称"
class="form-input"
/>
<view class="form-item" @click="showSchoolPicker">
<view class="form-item-left">
<Location class="form-icon" />
<text class="form-label">所在学校</text>
</view>
<view class="form-item-right">
<text class="form-value">{{ formData.school || '上海理工大学' }}</text>
<Right class="arrow-icon" />
</view>
</view>
</view>
<view class="form-item">
<text class="form-label">车辆品牌 <text class="required">*</text></text>
<picker
:range="brands"
:value="brandIndex"
@change="onBrandChange"
class="form-picker"
<!-- 车辆详情表单 -->
<nut-form ref="formRef" :model-value="formData">
<view class="form-section">
<!-- 车型品牌 -->
<nut-form-item
label="车型品牌"
prop="brand"
required
:rules="[{ required: true, message: '请选择车型品牌' }]"
>
<view class="picker-content">
<text class="picker-text">{{ brands[brandIndex] || '请选择品牌' }}</text>
<Right size="16" color="#9ca3af" />
<view class="form-item-content" @click="showBrandPicker">
<text class="form-value">{{ formData.brand || '请选择' }}</text>
<Right class="arrow-icon" />
</view>
</picker>
</view>
</nut-form-item>
<view class="form-item">
<text class="form-label">购买年份 <text class="required">*</text></text>
<picker
:range="years"
:value="yearIndex"
@change="onYearChange"
class="form-picker"
>
<view class="picker-content">
<text class="picker-text">{{ years[yearIndex] || '请选择年份' }}</text>
<Right size="16" color="#9ca3af" />
<!-- 车辆型号 -->
<nut-form-item label="车辆型号" prop="model">
<view class="form-item-content" @click="showModelPicker">
<text class="form-value">{{ formData.model || '请选择' }}</text>
<Right class="arrow-icon" />
</view>
</picker>
</view>
</nut-form-item>
<view class="form-item">
<text class="form-label">车辆类型 <text class="required">*</text></text>
<picker
:range="types"
:value="typeIndex"
@change="onTypeChange"
class="form-picker"
<!-- 车辆出厂年份 -->
<nut-form-item label="车辆出厂年份" prop="year">
<view class="form-item-content" @click="showYearPicker">
<text class="form-value">{{ formData.year || '请选择' }}</text>
<Right class="arrow-icon" />
</view>
</nut-form-item>
<!-- 新旧程度 -->
<nut-form-item
label="新旧程度"
prop="condition"
required
:rules="[{ required: true, message: '请选择新旧程度' }]"
>
<view class="form-item-content" @click="showConditionPicker">
<text class="form-value">{{ formData.condition || '请选择' }}</text>
<Right class="arrow-icon" />
</view>
</nut-form-item>
<!-- 行驶里程 -->
<nut-form-item
label="行驶里程"
prop="mileage"
required
:rules="[{ required: true, message: '请输入行驶里程' }]"
>
<view class="picker-content">
<text class="picker-text">{{ types[typeIndex] || '请选择类型' }}</text>
<Right size="16" color="#9ca3af" />
<nut-input
v-model="formData.mileage"
placeholder="1200"
type="number"
input-align="right"
>
<template #right>
<text class="unit">公里</text>
</template>
</nut-input>
</nut-form-item>
<!-- 续航里程 -->
<nut-form-item label="续航里程" prop="range">
<nut-input
v-model="formData.range"
placeholder="60"
type="number"
input-align="right"
>
<template #right>
<text class="unit">公里</text>
</template>
</nut-input>
</nut-form-item>
<!-- 最高时速 -->
<nut-form-item label="最高时速" prop="maxSpeed">
<nut-input
v-model="formData.maxSpeed"
placeholder="25"
type="number"
input-align="right"
>
<template #right>
<text class="unit">km/h</text>
</template>
</nut-input>
</nut-form-item>
<!-- 电池容量 -->
<nut-form-item label="电池容量" prop="batteryCapacity">
<nut-input
v-model="formData.batteryCapacity"
placeholder="20"
type="number"
input-align="right"
>
<template #right>
<text class="unit">Ah</text>
</template>
</nut-input>
</nut-form-item>
<!-- 电池损耗度 -->
<nut-form-item label="电池损耗度" prop="batteryWear">
<view class="form-item-content" @click="showBatteryWearPicker">
<text class="form-value">{{ formData.batteryWear || '请选择' }}</text>
<Right class="arrow-icon" />
</view>
</nut-form-item>
<!-- 刹车磨损度 -->
<nut-form-item label="刹车磨损度" prop="brakeWear">
<view class="form-item-content" @click="showBrakeWearPicker">
<text class="form-value">{{ formData.brakeWear || '请选择' }}</text>
<Right class="arrow-icon" />
</view>
</picker>
</nut-form-item>
<!-- 轮胎磨损度 -->
<nut-form-item label="轮胎磨损度" prop="tireWear">
<view class="form-item-content" @click="showTireWearPicker">
<text class="form-value">{{ formData.tireWear || '请选择' }}</text>
<Right class="arrow-icon" />
</view>
</nut-form-item>
</view>
</nut-form>
<!-- 价格信息 -->
<view class="form-section">
<view class="form-item">
<text class="form-label">出售价格 <text class="required">*</text></text>
<view class="price-input-container">
<view class="form-item-left">
<Location class="form-icon" />
<text class="form-label">出让价格</text>
</view>
<view class="form-item-right">
<text class="price-symbol">¥</text>
<input
v-model="formData.price"
placeholder="请输入价格"
type="number"
<input
v-model="formData.sellingPrice"
placeholder="3,200"
type="text"
class="price-input"
/>
</view>
</view>
</view>
<!-- 车辆状况 -->
<view class="form-section">
<text class="section-title">车辆状况</text>
<view class="form-item">
<text class="form-label">使用时长</text>
<picker
:range="usagePeriods"
:value="usageIndex"
@change="onUsageChange"
class="form-picker"
>
<view class="picker-content">
<text class="picker-text">{{ usagePeriods[usageIndex] || '请选择使用时长' }}</text>
<Right size="16" color="#9ca3af" />
</view>
</picker>
</view>
<view class="form-item">
<text class="form-label">车辆描述</text>
<textarea
v-model="formData.description"
placeholder="请描述车辆的具体情况,如外观、性能、配件等"
class="form-textarea"
maxlength="500"
/>
<text class="char-count">{{ formData.description.length }}/500</text>
<view class="form-item-left">
<Location class="form-icon" />
<text class="form-label">市场价</text>
</view>
<view class="form-item-right">
<text class="market-price-symbol">¥</text>
<input
v-model="formData.marketPrice"
placeholder="6,500"
type="text"
class="market-price-input"
/>
</view>
</view>
</view>
<!-- 联系方式 -->
<!-- 车辆描述 -->
<view class="form-section">
<text class="section-title">联系方式</text>
<view class="form-item">
<text class="form-label">手机号码 <text class="required">*</text></text>
<input
v-model="formData.phone"
placeholder="请输入手机号码"
type="number"
class="form-input"
/>
</view>
<view class="form-item">
<text class="form-label">所在学校 <text class="required">*</text></text>
<input
v-model="formData.school"
placeholder="请输入所在学校"
class="form-input"
/>
</view>
<view class="form-item">
<text class="form-label">交易地点</text>
<input
v-model="formData.location"
placeholder="请输入具体交易地点"
class="form-input"
/>
</view>
<nut-textarea
v-model="formData.description"
placeholder="请描述车辆详情,如使用感受、车况特点等"
:max-length="200"
:rows="4"
show-word-limit
/>
</view>
</view>
<!-- 底部按钮 -->
<view class="bottom-actions">
<button class="preview-btn" @click="onPreview">预览</button>
<button class="publish-btn" @click="onPublish">发布车辆</button>
<nut-button
color="#f97316"
size="large"
block
@click="onPublish"
>
确认发布
</nut-button>
</view>
<!-- 自定义TabBar -->
<TabBar />
<!-- 选择器弹窗 -->
<!-- 学校选择 -->
<nut-popup v-model:visible="schoolPickerVisible" position="bottom">
<nut-picker
v-model="schoolValue"
:columns="schoolOptions"
title="选择学校"
@confirm="onSchoolConfirm"
@cancel="schoolPickerVisible = false"
/>
</nut-popup>
<!-- 品牌选择 -->
<nut-popup v-model:visible="brandPickerVisible" position="bottom">
<nut-picker
v-model="brandValue"
:columns="brandOptions"
title="选择车型品牌"
@confirm="onBrandConfirm"
@cancel="brandPickerVisible = false"
/>
</nut-popup>
<!-- 型号选择 -->
<nut-popup v-model:visible="modelPickerVisible" position="bottom">
<nut-picker
v-model="modelValue"
:columns="modelOptions"
title="选择车辆型号"
@confirm="onModelConfirm"
@cancel="modelPickerVisible = false"
/>
</nut-popup>
<!-- 年份选择 -->
<nut-popup v-model:visible="yearPickerVisible" position="bottom">
<nut-picker
v-model="yearValue"
:columns="yearOptions"
title="选择出厂年份"
@confirm="onYearConfirm"
@cancel="yearPickerVisible = false"
/>
</nut-popup>
<!-- 新旧程度选择 -->
<nut-popup v-model:visible="conditionPickerVisible" position="bottom">
<nut-picker
v-model="conditionValue"
:columns="conditionOptions"
title="选择新旧程度"
@confirm="onConditionConfirm"
@cancel="conditionPickerVisible = false"
/>
</nut-popup>
<!-- 电池损耗度选择 -->
<nut-popup v-model:visible="batteryWearPickerVisible" position="bottom">
<nut-picker
v-model="batteryWearValue"
:columns="wearLevelOptions"
title="选择电池损耗度"
@confirm="onBatteryWearConfirm"
@cancel="batteryWearPickerVisible = false"
/>
</nut-popup>
<!-- 刹车磨损度选择 -->
<nut-popup v-model:visible="brakeWearPickerVisible" position="bottom">
<nut-picker
v-model="brakeWearValue"
:columns="wearLevelOptions"
title="选择刹车磨损度"
@confirm="onBrakeWearConfirm"
@cancel="brakeWearPickerVisible = false"
/>
</nut-popup>
<!-- 轮胎磨损度选择 -->
<nut-popup v-model:visible="tireWearPickerVisible" position="bottom">
<nut-picker
v-model="tireWearValue"
:columns="wearLevelOptions"
title="选择轮胎磨损度"
@confirm="onTireWearConfirm"
@cancel="tireWearPickerVisible = false"
/>
</nut-popup>
</view>
</template>
<script setup>
import { ref, reactive } from 'vue'
import { Close, Plus, Right } from '@nutui/icons-vue-taro'
import { Plus, Right, Location, RectLeft } from '@nutui/icons-vue-taro'
import Taro from '@tarojs/taro'
import TabBar from '@/components/TabBar.vue'
// 响应式数据
const vehicleImages = ref([])
const brandIndex = ref(-1)
const yearIndex = ref(-1)
const typeIndex = ref(-1)
const usageIndex = ref(-1)
const themeVars = ref({
// navbarBackground: '#FFA135',
// navbarColor: '#ffffff',
})
// 文件上传列表
const fileList = ref([])
// 表单数据
const formData = reactive({
name: '',
school: '',
brand: '',
model: '',
year: '',
type: '',
price: '',
usage: '',
description: '',
phone: '',
school: '',
location: ''
condition: '',
mileage: '1200',
range: '60',
maxSpeed: '25',
batteryCapacity: '20',
batteryWear: '',
brakeWear: '',
tireWear: '',
sellingPrice: '3,200',
marketPrice: '6,500',
description: ''
})
// 选择器数据
const brands = ['小牛电动', '雅迪', '爱玛', '台铃', '绿源', '新日', '立马', '其他']
const years = ['2024年', '2023年', '2022年', '2021年', '2020年', '2019年', '2018年', '更早']
const types = ['电动自行车', '电动摩托车', '电动汽车', '平衡车', '滑板车', '其他']
const usagePeriods = ['3个月以内', '3-6个月', '6个月-1年', '1-2年', '2-3年', '3年以上']
// 选择器显示状态
const schoolPickerVisible = ref(false)
const brandPickerVisible = ref(false)
const modelPickerVisible = ref(false)
const yearPickerVisible = ref(false)
const conditionPickerVisible = ref(false)
const batteryWearPickerVisible = ref(false)
const brakeWearPickerVisible = ref(false)
const tireWearPickerVisible = ref(false)
// 选择器值
const schoolValue = ref([])
const brandValue = ref([])
const modelValue = ref([])
const yearValue = ref([])
const conditionValue = ref([])
const batteryWearValue = ref([])
const brakeWearValue = ref([])
const tireWearValue = ref([])
// 选择器选项数据
const schoolOptions = ref([
{ text: '上海理工大学', value: '上海理工大学' },
{ text: '复旦大学', value: '复旦大学' },
{ text: '上海交通大学', value: '上海交通大学' },
{ text: '同济大学', value: '同济大学' },
{ text: '华东师范大学', value: '华东师范大学' },
{ text: '上海大学', value: '上海大学' },
{ text: '东华大学', value: '东华大学' },
{ text: '上海财经大学', value: '上海财经大学' }
])
const brandOptions = ref([
{ text: '小牛电动', value: '小牛电动' },
{ text: '雅迪', value: '雅迪' },
{ text: '爱玛', value: '爱玛' },
{ text: '台铃', value: '台铃' },
{ text: '绿源', value: '绿源' },
{ text: '新日', value: '新日' },
{ text: '立马', value: '立马' },
{ text: '其他', value: '其他' }
])
const modelOptions = ref([
{ text: 'NGT', value: 'NGT' },
{ text: 'UQi+', value: 'UQi+' },
{ text: 'Gova G2', value: 'Gova G2' },
{ text: 'MQi+', value: 'MQi+' },
{ text: 'NQi GTS', value: 'NQi GTS' },
{ text: 'RQi', value: 'RQi' },
{ text: '其他型号', value: '其他型号' }
])
const yearOptions = ref([
{ text: '2024年', value: '2024年' },
{ text: '2023年', value: '2023年' },
{ text: '2022年', value: '2022年' },
{ text: '2021年', value: '2021年' },
{ text: '2020年', value: '2020年' },
{ text: '2019年', value: '2019年' },
{ text: '2018年及以前', value: '2018年及以前' }
])
const conditionOptions = ref([
{ text: '全新', value: '全新' },
{ text: '9成新', value: '9成新' },
{ text: '8成新', value: '8成新' },
{ text: '7成新', value: '7成新' },
{ text: '6成新', value: '6成新' },
{ text: '5成新及以下', value: '5成新及以下' }
])
const wearLevelOptions = ref([
{ text: '全新', value: '全新' },
{ text: '轻微磨损', value: '轻微磨损' },
{ text: '中度磨损', value: '中度磨损' },
{ text: '重度磨损', value: '重度磨损' },
{ text: '需要更换', value: '需要更换' }
])
/**
* 返回上一页
*/
const goBack = () => {
Taro.redirectTo({
url: '/pages/index/index'
})
}
/**
* 上传图片
* 文件上传超出限制
*/
const uploadImage = () => {
Taro.chooseImage({
count: 6 - vehicleImages.value.length,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: (res) => {
vehicleImages.value.push(...res.tempFilePaths)
}
const onOversize = () => {
Taro.showToast({
title: '文件大小超出限制',
icon: 'none'
})
}
/**
* 删除图片
* @param {number} index - 图片索引
* 删除上传的文件
*/
const removeImage = (index) => {
vehicleImages.value.splice(index, 1)
const onDelete = () => {
// 删除逻辑已由组件内部处理
}
/**
* 品牌选择事件
* @param {object} e - 事件对象
* 显示学校选择器
*/
const onBrandChange = (e) => {
brandIndex.value = e.detail.value
formData.brand = brands[e.detail.value]
const showSchoolPicker = () => {
schoolPickerVisible.value = true
}
/**
* 年份选择事件
* @param {object} e - 事件对象
* 显示品牌选择器
*/
const onYearChange = (e) => {
yearIndex.value = e.detail.value
formData.year = years[e.detail.value]
const showBrandPicker = () => {
brandPickerVisible.value = true
}
/**
* 类型选择事件
* @param {object} e - 事件对象
* 显示型号选择器
*/
const onTypeChange = (e) => {
typeIndex.value = e.detail.value
formData.type = types[e.detail.value]
const showModelPicker = () => {
if (!formData.brand) {
Taro.showToast({
title: '请先选择品牌',
icon: 'none'
})
return
}
modelPickerVisible.value = true
}
/**
* 使用时长选择事件
* @param {object} e - 事件对象
* 显示年份选择器
*/
const onUsageChange = (e) => {
usageIndex.value = e.detail.value
formData.usage = usagePeriods[e.detail.value]
const showYearPicker = () => {
yearPickerVisible.value = true
}
/**
* 预览功能
* 显示新旧程度选择器
*/
const onPreview = () => {
if (!validateForm()) return
Taro.showToast({
title: '预览功能开发中',
icon: 'none'
})
const showConditionPicker = () => {
conditionPickerVisible.value = true
}
/**
* 显示电池损耗度选择器
*/
const showBatteryWearPicker = () => {
batteryWearPickerVisible.value = true
}
/**
* 显示刹车磨损度选择器
*/
const showBrakeWearPicker = () => {
brakeWearPickerVisible.value = true
}
/**
* 显示轮胎磨损度选择器
*/
const showTireWearPicker = () => {
tireWearPickerVisible.value = true
}
/**
* 学校选择确认
*/
const onSchoolConfirm = ({ selectedValue }) => {
formData.school = selectedValue[0]
schoolPickerVisible.value = false
}
/**
* 品牌选择确认
*/
const onBrandConfirm = ({ selectedValue }) => {
formData.brand = selectedValue[0]
formData.model = '' // 重置型号
brandPickerVisible.value = false
}
/**
* 型号选择确认
*/
const onModelConfirm = ({ selectedValue }) => {
formData.model = selectedValue[0]
modelPickerVisible.value = false
}
/**
* 年份选择确认
*/
const onYearConfirm = ({ selectedValue }) => {
formData.year = selectedValue[0]
yearPickerVisible.value = false
}
/**
* 新旧程度选择确认
*/
const onConditionConfirm = ({ selectedValue }) => {
formData.condition = selectedValue[0]
conditionPickerVisible.value = false
}
/**
* 电池损耗度选择确认
*/
const onBatteryWearConfirm = ({ selectedValue }) => {
formData.batteryWear = selectedValue[0]
batteryWearPickerVisible.value = false
}
/**
* 刹车磨损度选择确认
*/
const onBrakeWearConfirm = ({ selectedValue }) => {
formData.brakeWear = selectedValue[0]
brakeWearPickerVisible.value = false
}
/**
* 轮胎磨损度选择确认
*/
const onTireWearConfirm = ({ selectedValue }) => {
formData.tireWear = selectedValue[0]
tireWearPickerVisible.value = false
}
/**
......@@ -290,9 +608,9 @@ const onPreview = () => {
*/
const onPublish = () => {
if (!validateForm()) return
Taro.showLoading({ title: '发布中...' })
// 模拟发布请求
setTimeout(() => {
Taro.hideLoading()
......@@ -300,7 +618,7 @@ const onPublish = () => {
title: '发布成功',
icon: 'success'
})
// 发布成功后跳转到首页
setTimeout(() => {
Taro.switchTab({ url: '/pages/index/index' })
......@@ -313,51 +631,31 @@ const onPublish = () => {
* @returns {boolean} 验证结果
*/
const validateForm = () => {
if (vehicleImages.value.length === 0) {
if (fileList.value.length === 0) {
Taro.showToast({ title: '请上传车辆图片', icon: 'none' })
return false
}
if (!formData.name.trim()) {
Taro.showToast({ title: '请输入车辆名称', icon: 'none' })
return false
}
if (!formData.brand) {
Taro.showToast({ title: '请选择车辆品牌', icon: 'none' })
return false
}
if (!formData.year) {
Taro.showToast({ title: '请选择购买年份', icon: 'none' })
return false
}
if (!formData.type) {
Taro.showToast({ title: '请选择车辆类型', icon: 'none' })
Taro.showToast({ title: '请选择车型品牌', icon: 'none' })
return false
}
if (!formData.price || formData.price <= 0) {
Taro.showToast({ title: '请输入正确的价格', icon: 'none' })
return false
}
if (!formData.phone.trim()) {
Taro.showToast({ title: '请输入手机号码', icon: 'none' })
if (!formData.condition) {
Taro.showToast({ title: '请选择新旧程度', icon: 'none' })
return false
}
if (!/^1[3-9]\d{9}$/.test(formData.phone)) {
Taro.showToast({ title: '请输入正确的手机号码', icon: 'none' })
if (!formData.mileage || formData.mileage <= 0) {
Taro.showToast({ title: '请输入正确的行驶里程', icon: 'none' })
return false
}
if (!formData.school.trim()) {
Taro.showToast({ title: '请输入所在学校', icon: 'none' })
if (!formData.sellingPrice) {
Taro.showToast({ title: '请输入出让价格', icon: 'none' })
return false
}
return true
}
</script>
......@@ -365,202 +663,128 @@ const validateForm = () => {
<style lang="less">
.sell-page {
min-height: 100vh;
background-color: #f9fafb;
padding-bottom: 80px;
}
.header {
background-color: #ffffff;
padding: 20px 16px;
border-bottom: 1px solid #f3f4f6;
}
.header-title {
font-size: 24px;
font-weight: 700;
color: #111827;
display: block;
margin-bottom: 4px;
}
.header-subtitle {
font-size: 14px;
color: #6b7280;
display: block;
background-color: #f5f5f5;
padding-bottom: 120rpx;
}
.form-container {
padding: 0 16px;
padding: 0 32rpx;
}
.form-section {
background-color: #ffffff;
border-radius: 12px;
padding: 20px;
margin: 12px 0;
border-radius: 24rpx;
padding: 32rpx;
margin: 24rpx 0;
}
.section-title {
font-size: 18px;
font-size: 32rpx;
font-weight: 600;
color: #111827;
margin-bottom: 16px;
margin-bottom: 32rpx;
display: block;
}
.required {
color: #ef4444;
}
.image-upload-container {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
margin-bottom: 8px;
.upload-tips {
display: flex;
margin-top: 16rpx;
gap: 32rpx;
}
.image-item {
position: relative;
aspect-ratio: 1;
border-radius: 8px;
overflow: hidden;
.tip-item {
font-size: 24rpx;
color: #9ca3af;
}
.uploaded-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.image-delete {
position: absolute;
top: 4px;
right: 4px;
width: 24px;
height: 24px;
background-color: rgba(0, 0, 0, 0.6);
border-radius: 50%;
.form-item {
display: flex;
justify-content: space-between;
align-items: center;
justify-content: center;
padding: 32rpx 0;
border-bottom: 1rpx solid #f3f4f6;
}
.image-upload-btn {
aspect-ratio: 1;
border: 2px dashed #d1d5db;
border-radius: 8px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 4px;
background-color: #f9fafb;
.form-item:last-child {
border-bottom: none;
}
.upload-text {
font-size: 12px;
color: #9ca3af;
.form-item-left {
display: flex;
align-items: center;
}
.form-tip {
font-size: 12px;
.form-icon {
margin-right: 16rpx;
color: #9ca3af;
display: block;
}
.form-item {
margin-bottom: 16px;
}
.form-label {
font-size: 14px;
font-weight: 500;
font-size: 28rpx;
color: #374151;
margin-bottom: 8px;
display: block;
}
.form-input {
width: 100%;
padding: 12px 16px;
border: 1px solid #d1d5db;
border-radius: 8px;
font-size: 14px;
color: #374151;
background-color: #ffffff;
.form-item-right {
display: flex;
align-items: center;
}
.form-input:focus {
border-color: #f97316;
outline: none;
.form-value {
font-size: 28rpx;
color: #9ca3af;
margin-right: 16rpx;
}
.form-picker {
width: 100%;
.arrow-icon {
color: #9ca3af;
}
.picker-content {
.form-item-content {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
border: 1px solid #d1d5db;
border-radius: 8px;
background-color: #ffffff;
}
.picker-text {
font-size: 14px;
color: #374151;
width: 100%;
padding: 24rpx 0;
}
.price-input-container {
display: flex;
align-items: center;
border: 1px solid #d1d5db;
border-radius: 8px;
background-color: #ffffff;
.unit {
font-size: 28rpx;
color: #9ca3af;
margin-left: 16rpx;
}
.price-symbol {
padding: 12px 0 12px 16px;
font-size: 14px;
color: #374151;
font-weight: 500;
font-size: 32rpx;
color: #f97316;
font-weight: 600;
margin-right: 8rpx;
}
.price-input {
flex: 1;
padding: 12px 16px 12px 4px;
font-size: 32rpx;
color: #f97316;
font-weight: 600;
text-align: right;
border: none;
font-size: 14px;
color: #374151;
outline: none;
background: transparent;
width: 160rpx;
}
.form-textarea {
width: 100%;
min-height: 80px;
padding: 12px 16px;
border: 1px solid #d1d5db;
border-radius: 8px;
font-size: 14px;
color: #374151;
background-color: #ffffff;
resize: none;
}
.form-textarea:focus {
border-color: #f97316;
outline: none;
.market-price-symbol {
font-size: 28rpx;
color: #9ca3af;
margin-right: 8rpx;
}
.char-count {
font-size: 12px;
.market-price-input {
font-size: 28rpx;
color: #9ca3af;
text-align: right;
margin-top: 4px;
display: block;
border: none;
outline: none;
background: transparent;
width: 160rpx;
}
.bottom-actions {
......@@ -569,39 +793,85 @@ const validateForm = () => {
left: 0;
right: 0;
background-color: #ffffff;
padding: 12px 16px;
border-top: 1px solid #f3f4f6;
display: flex;
gap: 12px;
padding: 24rpx 32rpx;
border-top: 1rpx solid #f3f4f6;
z-index: 100;
}
.preview-btn {
flex: 1;
padding: 12px;
border: 1px solid #f97316;
border-radius: 8px;
background-color: #ffffff;
color: #f97316;
font-size: 16px;
font-weight: 500;
// NutUI组件样式覆盖
:deep(.nut-form-item) {
padding: 0;
margin-bottom: 0;
border-bottom: 1rpx solid #f3f4f6;
}
.publish-btn {
flex: 2;
padding: 12px;
border: none;
border-radius: 8px;
background-color: #f97316;
color: #ffffff;
font-size: 16px;
font-weight: 500;
// :deep(.nut-form-item:last-child) {
// border-bottom: none;
// }
:deep(.nut-form-item__body) {
padding: 32rpx 0;
}
:deep(.nut-form-item__label) {
font-size: 28rpx;
color: #374151;
margin-right: 32rpx;
}
:deep(.nut-input) {
text-align: right;
}
:deep(.nut-input__input) {
font-size: 28rpx;
color: #374151;
text-align: right;
}
:deep(.nut-textarea) {
border: 1rpx solid #e5e7eb;
border-radius: 16rpx;
padding: 24rpx;
}
:deep(.nut-textarea__textarea) {
font-size: 28rpx;
color: #374151;
line-height: 1.5;
}
.preview-btn:active {
background-color: #fef7ed;
:deep(.nut-uploader) {
margin-bottom: 16rpx;
}
.publish-btn:active {
background-color: #ea580c;
:deep(.nut-uploader__preview) {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 24rpx;
margin-bottom: 24rpx;
}
:deep(.nut-uploader__preview-img) {
width: 160rpx;
height: 160rpx;
border-radius: 16rpx;
}
:deep(.nut-picker__toolbar) {
padding: 24rpx 32rpx;
}
:deep(.nut-picker__cancel) {
color: #9ca3af;
}
:deep(.nut-picker__confirm) {
color: #f97316;
}
:deep(.nut-picker__title) {
font-size: 32rpx;
font-weight: 600;
}
</style>
\ No newline at end of file
</style>
......