sales-publish.vue 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119
  1. <template>
  2. <view class="sales-publish-container">
  3. <!-- 表单内容 -->
  4. <view class="form-container">
  5. <!-- 产品名称 -->
  6. <view class="form-item">
  7. <view class="item-label">
  8. <text class="label-text">产品名称</text>
  9. <text class="required">*</text>
  10. </view>
  11. <view class="item-content">
  12. <input class="form-input" v-model="formData.title" placeholder="请输入产品名称"
  13. placeholder-style="color: #999;" maxlength="20" @input="onNameInput" />
  14. <view class="char-count">{{ nameLength }}/20</view>
  15. </view>
  16. </view>
  17. <!-- 产品分类 -->
  18. <view class="form-item">
  19. <view class="item-label">
  20. <text class="label-text">产品分类</text>
  21. <text class="required">*</text>
  22. </view>
  23. <view class="item-content">
  24. <view class="category-selector" @click="showCategoryPicker = true">
  25. <text class="selector-text" :class="{ placeholder: !formData.categoryId }">
  26. <!-- {{ formData.categoryLabel || '请选择产品分类' }} -->
  27. {{getDictLabel('agricultural_category',formData.categoryId) || '请选择产品分类' }}
  28. </text>
  29. <text class="arrow-icon">></text>
  30. </view>
  31. </view>
  32. </view>
  33. <view class="form-item">
  34. <!-- 所在地 -->
  35. <view class="item-label">
  36. <text class="label-text">所在地</text>
  37. <text class="required">*</text>
  38. </view>
  39. <LocationPicker
  40. v-model="formData.location"
  41. mode="edit"
  42. />
  43. </view>
  44. <!-- 销售价格 -->
  45. <view class="form-item">
  46. <view class="item-label">
  47. <text class="label-text">销售价格</text>
  48. <text class="required">*</text>
  49. </view>
  50. <view class="item-content">
  51. <view class="input-with-unit">
  52. <text class="currency-symbol">¥</text>
  53. <input class="form-input" v-model="formData.price" placeholder="请输入销售价格"
  54. placeholder-style="color: #999;" type="digit" />
  55. <!-- <text class="unit-text">元/斤</text> -->
  56. </view>
  57. </view>
  58. </view>
  59. <!-- 单位-->
  60. <view class="form-item">
  61. <view class="item-label">
  62. <text class="label-text">单位</text>
  63. <text class="required">*</text>
  64. </view>
  65. <view class="item-content">
  66. <view class="category-selector" @click="showUnitPicker = true">
  67. <text class="selector-text" :class="{ placeholder: !formData.unit }">
  68. <!-- {{ formData.dictLabel || '请选择价格单位' }} -->
  69. {{ getDictLabel('agricultural_unit',formData.unit) || '请选择价格单位'}}
  70. </text>
  71. <text class="arrow-icon">></text>
  72. </view>
  73. </view>
  74. </view>
  75. <!-- 收购数量 -->
  76. <view class="form-item">
  77. <view class="item-label">
  78. <text class="label-text">产品数量</text>
  79. <text class="required">*</text>
  80. </view>
  81. <view class="item-content">
  82. <view class="input-with-unit">
  83. <input
  84. class="form-input"
  85. v-model="formData.quantity"
  86. placeholder="请输入产品数量"
  87. placeholder-style="color: #999;"
  88. type="number"
  89. />
  90. <!-- <text class="unit-text">斤</text> -->
  91. </view>
  92. </view>
  93. </view>
  94. <!-- 产品简介 -->
  95. <view class="form-item">
  96. <view class="item-label">
  97. <text class="label-text">产品简介</text>
  98. </view>
  99. <view class="item-content">
  100. <textarea class="form-textarea" v-model="formData.description" placeholder="请简单介绍您的农产品特色、种植方式等"
  101. placeholder-style="color: #999;" maxlength="80" auto-height @input="onDescInput" />
  102. <view class="char-count">{{ descLength }}/80</view>
  103. </view>
  104. </view>
  105. <!-- 产品图片 -->
  106. <view class="form-item">
  107. <view class="item-label">
  108. <text class="label-text">产品图片</text>
  109. <text class="required">*</text>
  110. <text class="optional">(最多6张)</text>
  111. </view>
  112. <view class="item-content">
  113. <view class="image-upload-area">
  114. <view class="image-item" v-for="(image, index) in formData.images" :key="index">
  115. <image class="uploaded-image" :src="image.url" mode="aspectFill"></image>
  116. <view class="image-delete" @click="removeImage(index)">×</view>
  117. </view>
  118. <view class="image-upload-btn" v-if="formData.images.length < 6" @click="chooseImage">
  119. <text class="upload-icon">+</text>
  120. <text class="upload-text">上传图片</text>
  121. </view>
  122. </view>
  123. </view>
  124. </view>
  125. <!-- 产地信息 -->
  126. <!-- <view class="form-item">
  127. <view class="item-label">
  128. <text class="label-text">产地信息</text>
  129. </view>
  130. <view class="contact-info">
  131. <view class="contact-row">
  132. <text class="contact-label">产地:</text>
  133. <text class="contact-value">{{ locationInfo.address }}</text>
  134. </view>
  135. <view class="contact-row">
  136. <text class="contact-label">地块:</text>
  137. <text class="contact-value">{{ locationInfo.field }}</text>
  138. </view>
  139. </view>
  140. </view> -->
  141. <!-- 联系人信息 -->
  142. <view class="form-item">
  143. <view class="item-label">
  144. <text class="label-text">联系人信息</text>
  145. </view>
  146. <view class="item-content">
  147. <view class="contact-input-row">
  148. <text class="contact-label">联系人:</text>
  149. <input class="contact-input" v-model="formData.contactName" placeholder="请输入联系人姓名"
  150. placeholder-style="color: #999;" maxlength="10" />
  151. </view>
  152. <view class="contact-input-row">
  153. <text class="contact-label">电话:</text>
  154. <input class="contact-input" v-model="formData.contactPhone" placeholder="请输入联系电话"
  155. placeholder-style="color: #999;" type="number" maxlength="11" />
  156. </view>
  157. </view>
  158. </view>
  159. </view>
  160. <!-- 底部提交按钮 -->
  161. <view class="bottom-action-bar">
  162. <view class="submit-btn" @click="submitForm">
  163. <text class="btn-text">{{ isEditMode ? '保存修改' : '提交审核' }}</text>
  164. </view>
  165. </view>
  166. <!-- 分类选择弹窗 -->
  167. <view class="picker-modal" v-if="showCategoryPicker" @click="showCategoryPicker = false">
  168. <view class="picker-content" @click.stop>
  169. <view class="picker-header">
  170. <text class="picker-title">选择产品分类</text>
  171. <text class="picker-close" @click="showCategoryPicker = false">×</text>
  172. </view>
  173. <view class="picker-options">
  174. <view class="picker-option"
  175. v-for="category in (dictDataOptions.agricultural_category || [])"
  176. :key="category.dictCode"
  177. @click="selectCategory(category)"
  178. :class="{ 'active': category.dictValue == formData.categoryId }">
  179. <text class="option-text">{{ category.dictLabel }}</text>
  180. </view>
  181. </view>
  182. </view>
  183. </view>
  184. <!-- 单位选择弹窗 -->
  185. <view class="picker-modal" v-if="showUnitPicker" @click="showUnitPicker = false">
  186. <view class="picker-content" @click.stop>
  187. <view class="picker-header">
  188. <text class="picker-title">选择价格单位</text>
  189. <text class="picker-close" @click="showUnitPicker = false">×</text>
  190. </view>
  191. <view class="picker-options">
  192. <view class="picker-option"
  193. v-for="unit in (dictDataOptions.agricultural_unit || [])"
  194. :key="unit.dictCode"
  195. @click="selectUnit(unit)"
  196. :class="{ 'active': unit.dictValue == formData.unit }">
  197. <text class="option-text">{{ unit.dictLabel }}</text>
  198. </view>
  199. </view>
  200. </view>
  201. </view>
  202. </view>
  203. </template>
  204. <script setup>
  205. import { ref, reactive, computed, onMounted } from 'vue'
  206. import LocationPicker from "@/components/common/LocationPicker.vue"
  207. import api from "@/config/api.js"
  208. import storage from "@/utils/storage.js"
  209. import { addProductInfo, getProductInfoById, editProductInfo } from '@/api/services/productInfo.js'
  210. import { useDict } from '@/utils/composables/useDict'
  211. import { getFormattedTime } from '@/utils/dateUtils'
  212. // 使用 useDict 替换 dictMixin
  213. const { dictData } = useDict(['agricultural_unit', 'agricultural_category'])
  214. // 响应式状态
  215. const isEditMode = ref(false)
  216. const editProductId = ref('')
  217. const productType = ref('0')
  218. const showCategoryPicker = ref(false)
  219. const showUnitPicker = ref(false)
  220. const nameLength = ref(0)
  221. const descLength = ref(0)
  222. // 使用 reactive 管理复杂的表单对象
  223. const formData = reactive({
  224. title: '',
  225. categoryId: '',
  226. price: '',
  227. description: '',
  228. imageUrl: '',
  229. images: [],
  230. unit: '',
  231. quantity: '',
  232. contactName: '',
  233. contactPhone: '',
  234. location: '',
  235. publishTime: getFormattedTime(),
  236. userId: storage.getUserInfo().userid,
  237. type: 0, // 出售
  238. status: 1 // 审核中
  239. })
  240. const locationInfo = reactive({
  241. address: '张家村',
  242. field: '水稻田A区'
  243. })
  244. const currentUserInfo = storage.getUserInfo()
  245. // 计算属性:dictDataOptions
  246. const dictDataOptions = computed(() => dictData)
  247. // onLoad 替换为 onMounted + getCurrentPages
  248. onMounted(() => {
  249. console.log("dictDataOptions",dictDataOptions.value);
  250. const pages = getCurrentPages()
  251. const currentPage = pages[pages.length - 1]
  252. const options = currentPage.options
  253. // 检查是否为编辑模式
  254. if (options.action === 'edit' && options.id) {
  255. isEditMode.value = true
  256. editProductId.value = options.id
  257. productType.value = options.type || '0'
  258. // 设置页面标题
  259. uni.setNavigationBarTitle({
  260. title: productType.value === '0' ? '编辑销售信息' : '编辑收购信息'
  261. })
  262. // 加载产品数据
  263. loadProductData()
  264. } else {
  265. // 新建模式
  266. isEditMode.value = false
  267. uni.setNavigationBarTitle({
  268. title: '发布农产品'
  269. })
  270. }
  271. })
  272. // 方法定义
  273. // 加载产品数据(编辑模式)
  274. const loadProductData = () => {
  275. uni.showLoading({
  276. title: '加载中...'
  277. })
  278. getProductInfoById(editProductId.value).then(res => {
  279. if (res.data.code === 200) {
  280. Object.assign(formData, res.data.data)
  281. // 更新字符计数
  282. nameLength.value = formData.title.length
  283. descLength.value = formData.description.length
  284. console.log("this.goodsDetail", formData)
  285. // 处理图片数据
  286. if (formData.imageUrl) {
  287. try {
  288. formData.images = formData.imageUrl.split(',').map(url => ({ url, status: 'success' }))
  289. console.log('解析后的图片数据:', formData.images)
  290. } catch (e) {
  291. console.error('解析图片数据失败:', e)
  292. formData.images = []
  293. }
  294. } else {
  295. formData.images = []
  296. }
  297. uni.hideLoading()
  298. } else {
  299. uni.showToast({
  300. title: res.data.msg || '获取农品信息失败',
  301. icon: 'none'
  302. })
  303. }
  304. })
  305. }
  306. // 产品名称输入处理
  307. const onNameInput = (e) => {
  308. nameLength.value = e.detail.value.length
  309. }
  310. // 描述输入处理
  311. const onDescInput = (e) => {
  312. descLength.value = e.detail.value.length
  313. }
  314. // 选择分类
  315. const selectCategory = (category) => {
  316. formData.categoryId = category.dictValue
  317. formData.categoryLabel = category.dictLabel
  318. showCategoryPicker.value = false
  319. }
  320. // 选择单位
  321. const selectUnit = (unit) => {
  322. formData.unit = unit.dictValue
  323. formData.dictLabel = unit.dictLabel
  324. showUnitPicker.value = false
  325. }
  326. // 获取字典标签
  327. const getDictLabel = (dictKey, value) => {
  328. if (!dictData || !dictData[dictKey]) {
  329. return ''
  330. }
  331. const list = dictData[dictKey] || []
  332. const item = list.find(u => u.dictValue == value)
  333. return item ? item.dictLabel : ''
  334. }
  335. // 选择图片(优化跨平台兼容性)
  336. const chooseImage = () => {
  337. uni.chooseImage({
  338. count: 6 - formData.images.length,
  339. sizeType: ['original', 'compressed'],
  340. sourceType: ['album', 'camera'],
  341. success: (res) => {
  342. console.log('选择图片成功:', res);
  343. console.log('tempFiles:', res.tempFiles);
  344. console.log('tempFilePaths:', res.tempFilePaths);
  345. // 验证文件类型和大小
  346. const validFiles = [];
  347. const invalidFiles = [];
  348. const maxSize = 5 * 1024 * 1024; // 5MB 最大限制
  349. // 兼容不同平台的文件信息获取方式
  350. if (res.tempFiles && res.tempFiles.length > 0) {
  351. // 检查每个文件
  352. res.tempFiles.forEach((file, index) => {
  353. console.log(`文件 ${index}:`, file);
  354. // 获取文件路径
  355. const filePath = res.tempFilePaths[index];
  356. // 获取文件名(兼容不同平台)
  357. let fileName = '';
  358. if (file.name) {
  359. fileName = file.name;
  360. } else if (file.path) {
  361. // 从路径中提取文件名
  362. fileName = file.path.split('/').pop();
  363. } else if (filePath) {
  364. fileName = filePath.split('/').pop();
  365. }
  366. console.log(`文件名: ${fileName}, 大小: ${file.size}`);
  367. // 检查文件类型(兼容没有扩展名的情况)
  368. let isImage = true;
  369. if (fileName) {
  370. isImage = /\.(jpg|jpeg|png|gif|bmp|webp)$/i.test(fileName);
  371. }
  372. // 检查文件大小
  373. const fileSize = file.size || 0;
  374. const isValidSize = fileSize <= maxSize;
  375. if (isImage && isValidSize) {
  376. validFiles.push(filePath);
  377. } else {
  378. invalidFiles.push({
  379. path: filePath,
  380. size: fileSize,
  381. name: fileName,
  382. reason: !isImage ? '文件格式不支持' : `文件大于5MB (${(fileSize / 1024 / 1024).toFixed(2)}MB)`
  383. });
  384. }
  385. });
  386. } else {
  387. // 如果没有 tempFiles,直接使用 tempFilePaths(某些平台可能不返回详细信息)
  388. console.warn('未获取到 tempFiles,直接使用 tempFilePaths');
  389. res.tempFilePaths.forEach(path => {
  390. validFiles.push(path);
  391. });
  392. }
  393. console.log('有效文件:', validFiles);
  394. console.log('无效文件:', invalidFiles);
  395. // 显示无效文件提示
  396. if (invalidFiles.length > 0) {
  397. const reasons = invalidFiles.map(f => `${f.name}: ${f.reason}`).join('\n');
  398. uni.showModal({
  399. title: '部分文件无效',
  400. content: `${invalidFiles.length}个文件无效:\n${reasons}`,
  401. showCancel: false
  402. });
  403. }
  404. // 如果有有效文件,则上传
  405. if (validFiles.length > 0) {
  406. uploadImages(validFiles);
  407. } else if (invalidFiles.length > 0) {
  408. uni.showToast({
  409. title: '没有可上传的文件',
  410. icon: 'none',
  411. duration: 2000
  412. });
  413. }
  414. },
  415. fail: (err) => {
  416. console.error('选择图片失败:', err);
  417. uni.showToast({
  418. title: '选择图片失败',
  419. icon: 'none',
  420. duration: 2000
  421. });
  422. }
  423. });
  424. }
  425. // 上传图片到服务器(优化跨平台兼容性)
  426. const uploadImages = (tempFilePaths) => {
  427. uni.showLoading({
  428. title: '上传中...',
  429. mask: true
  430. });
  431. // 上传成功的图片计数
  432. let successCount = 0;
  433. let failCount = 0;
  434. const totalFiles = tempFilePaths.length;
  435. const newImages = [];
  436. // 遍历处理每张图片
  437. tempFilePaths.forEach((path, index) => {
  438. // 调用上传API
  439. uni.uploadFile({
  440. url: api.serve + '/file/upload',
  441. filePath: path,
  442. name: 'file',
  443. formData: {
  444. type: 'task'
  445. },
  446. header: {
  447. 'Authorization': `Bearer ${storage.getAccessToken()}`
  448. },
  449. success: (res) => {
  450. try {
  451. console.log('上传原始响应:', res);
  452. console.log('响应数据类型:', typeof res.data);
  453. console.log('响应状态码:', res.statusCode);
  454. let response;
  455. // 兼容不同平台的响应格式
  456. if (typeof res.data === 'string') {
  457. // H5 端返回字符串,需要解析
  458. response = JSON.parse(res.data);
  459. } else if (typeof res.data === 'object') {
  460. // Android/鸿蒙端可能直接返回对象
  461. response = res.data;
  462. } else {
  463. throw new Error('未知的响应格式');
  464. }
  465. console.log('解析后的响应:', response);
  466. // 检查响应是否成功
  467. if (response.code === 200 || response.code === '200') {
  468. // 兼容不同的数据结构
  469. let imageUrl = '';
  470. // 情况1: response.data 是字符串(直接是URL)
  471. if (typeof response.data === 'string') {
  472. imageUrl = response.data;
  473. }
  474. // 情况2: response.data 是对象,包含 url 字段
  475. else if (response.data && response.data.url) {
  476. imageUrl = response.data.url;
  477. }
  478. // 情况3: response 直接包含 url 字段
  479. else if (response.url) {
  480. imageUrl = response.url;
  481. }
  482. // 情况4: response.data 是数组
  483. else if (Array.isArray(response.data) && response.data.length > 0) {
  484. imageUrl = response.data[0].url || response.data[0];
  485. }
  486. if (imageUrl) {
  487. console.log('上传成功,图片URL:', imageUrl);
  488. // 上传成功,将图片信息添加到数组
  489. newImages.push({
  490. url: imageUrl,
  491. path: path,
  492. status: 'success',
  493. fileName: (response.data && response.data.fileName) || ''
  494. });
  495. successCount++;
  496. } else {
  497. console.error('无法从响应中提取图片URL:', response);
  498. failCount++;
  499. uni.showToast({
  500. title: '图片URL解析失败',
  501. icon: 'none',
  502. duration: 2000
  503. });
  504. }
  505. } else {
  506. failCount++;
  507. console.error('上传失败,错误信息:', response.msg || response.message);
  508. uni.showToast({
  509. title: response.msg || response.message || '上传失败',
  510. icon: 'none',
  511. duration: 2000
  512. });
  513. }
  514. } catch (e) {
  515. failCount++;
  516. console.error('解析响应失败:', e);
  517. console.error('原始响应数据:', res.data);
  518. uni.showToast({
  519. title: `解析失败: ${e.message}`,
  520. icon: 'none',
  521. duration: 2000
  522. });
  523. }
  524. },
  525. fail: (err) => {
  526. failCount++;
  527. console.error('上传请求失败:', err);
  528. uni.showToast({
  529. title: '上传请求失败',
  530. icon: 'none',
  531. duration: 2000
  532. });
  533. },
  534. complete: () => {
  535. // 当所有文件都已处理完成
  536. if (successCount + failCount === totalFiles) {
  537. uni.hideLoading();
  538. if (newImages.length > 0) {
  539. // 将新上传的图片添加到已有图片列表
  540. formData.images = [...formData.images, ...newImages];
  541. // 更新imageUrl字段,将图片URL用逗号连接
  542. formData.imageUrl = formData.images.map(item => item.url).join(',');
  543. console.log('所有图片上传完成:', formData.images);
  544. console.log('imageUrl:', formData.imageUrl);
  545. // 显示成功提示
  546. uni.showToast({
  547. title: `成功上传${successCount}张图片`,
  548. icon: 'success',
  549. duration: 2000
  550. });
  551. } else {
  552. // 全部失败
  553. uni.showToast({
  554. title: '图片上传失败,请重试',
  555. icon: 'none',
  556. duration: 2000
  557. });
  558. }
  559. // 如果有部分失败
  560. if (failCount > 0 && successCount > 0) {
  561. uni.showToast({
  562. title: `成功${successCount}张,失败${failCount}张`,
  563. icon: 'none',
  564. duration: 2000
  565. });
  566. }
  567. }
  568. }
  569. });
  570. });
  571. }
  572. const removeImage = (index) => {
  573. uni.showModal({
  574. title: '确认删除',
  575. content: '确定要删除这张图片吗?',
  576. success: (res) => {
  577. if (res.confirm) {
  578. console.log("删除前:", formData.images, index)
  579. formData.images.splice(index, 1)
  580. // 更新taskImages字段
  581. formData.imageUrl = formData.images.map(item => item.url).join(',')
  582. console.log("删除后:", formData.imageUrl)
  583. uni.showToast({
  584. title: '已删除',
  585. icon: 'none'
  586. })
  587. }
  588. }
  589. })
  590. }
  591. // 表单验证
  592. const validateForm = () => {
  593. if (!formData.title.trim()) {
  594. uni.showToast({
  595. title: '请输入产品名称',
  596. icon: 'none'
  597. })
  598. return false
  599. }
  600. if (!formData.categoryId) {
  601. uni.showToast({
  602. title: '请选择产品分类',
  603. icon: 'none'
  604. })
  605. return false
  606. }
  607. if (!formData.price || formData.price <= 0) {
  608. uni.showToast({
  609. title: '请输入有效的销售价格',
  610. icon: 'none'
  611. })
  612. return false
  613. }
  614. if (!formData.quantity || formData.quantity <= 0) {
  615. uni.showToast({
  616. title: '请输入有效的收购数量',
  617. icon: 'none'
  618. })
  619. return false
  620. }
  621. if (formData.imageUrl.length === 0) {
  622. uni.showToast({
  623. title: '请至少上传一张产品图片',
  624. icon: 'none'
  625. })
  626. return false
  627. }
  628. if (!formData.contactName.trim()) {
  629. uni.showToast({
  630. title: '请输入联系人姓名',
  631. icon: 'none'
  632. })
  633. return false
  634. }
  635. if (!formData.contactPhone.trim()) {
  636. uni.showToast({
  637. title: '请输入联系电话',
  638. icon: 'none'
  639. })
  640. return false
  641. }
  642. // 简单的手机号格式验证
  643. const phoneRegex = /^1[3-9]\d{9}$/
  644. if (!phoneRegex.test(formData.contactPhone)) {
  645. uni.showToast({
  646. title: '请输入正确的手机号码',
  647. icon: 'none'
  648. })
  649. return false
  650. }
  651. return true
  652. }
  653. // 提交表单
  654. const submitForm = () => {
  655. if (!validateForm()) {
  656. return
  657. }
  658. // 显示加载提示
  659. const loadingTitle = isEditMode.value ? '保存中...' : '提交中...'
  660. uni.showLoading({
  661. title: loadingTitle
  662. })
  663. uni.hideLoading()
  664. if (isEditMode.value) {
  665. editProductInfo(formData).then(res => {
  666. if (res.data.code === 200) {
  667. uni.showModal({
  668. title: '保存成功',
  669. content: '您的修改已保存成功!',
  670. showCancel: false,
  671. success: () => {
  672. uni.navigateBack()
  673. }
  674. })
  675. } else {
  676. uni.showToast({
  677. title: res.data.msg || '提交失败,请稍后重试',
  678. icon: 'none'
  679. })
  680. }
  681. }).catch(err => {
  682. console.error("提交异常:", err)
  683. uni.showToast({
  684. title: '网络错误,请检查后重试',
  685. icon: 'none'
  686. })
  687. })
  688. } else {
  689. // 新建模式 - 提交审核
  690. console.log("this.formData", formData)
  691. addProductInfo(formData).then(res => {
  692. console.log("新增出售农产品", res)
  693. if (res.data.code === 200) {
  694. uni.showModal({
  695. title: '提交成功',
  696. content: '您的农产品信息已提交审核,审核通过后将在销售页面展示。',
  697. showCancel: false,
  698. success: () => {
  699. uni.navigateBack()
  700. }
  701. })
  702. } else {
  703. uni.showToast({
  704. title: res.data.msg || '提交失败,请稍后重试',
  705. icon: 'none'
  706. })
  707. }
  708. }).catch(err => {
  709. console.error("提交异常:", err)
  710. uni.showToast({
  711. title: '网络错误,请检查后重试',
  712. icon: 'none'
  713. })
  714. })
  715. }
  716. }
  717. </script>
  718. <style lang="scss">
  719. .sales-publish-container {
  720. min-height: 100vh;
  721. background-color: #f5f5f5;
  722. padding-bottom: calc(128rpx + env(safe-area-inset-bottom));
  723. }
  724. .form-container {
  725. padding: 20rpx;
  726. }
  727. .form-item {
  728. background-color: #fff;
  729. border-radius: 12rpx;
  730. padding: 24rpx;
  731. margin-bottom: 16rpx;
  732. box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
  733. }
  734. .item-label {
  735. display: flex;
  736. align-items: center;
  737. margin-bottom: 16rpx;
  738. }
  739. .label-text {
  740. font-size: 28rpx;
  741. color: #333;
  742. font-weight: bold;
  743. }
  744. .required {
  745. color: #ff4d4f;
  746. font-size: 28rpx;
  747. margin-left: 4rpx;
  748. }
  749. .optional {
  750. color: #999;
  751. font-size: 24rpx;
  752. margin-left: 8rpx;
  753. }
  754. .item-content {
  755. position: relative;
  756. }
  757. .form-input {
  758. width: 100%;
  759. height: 44rpx;
  760. font-size: 28rpx;
  761. color: #333;
  762. line-height: 44rpx;
  763. }
  764. .form-textarea {
  765. width: 100%;
  766. min-height: 120rpx;
  767. font-size: 28rpx;
  768. color: #333;
  769. line-height: 1.5;
  770. }
  771. .char-count {
  772. position: absolute;
  773. right: 0;
  774. bottom: -4rpx;
  775. font-size: 24rpx;
  776. color: #999;
  777. }
  778. // 分类选择器
  779. .category-selector {
  780. display: flex;
  781. align-items: center;
  782. justify-content: space-between;
  783. padding: 12rpx 0;
  784. border-bottom: 1rpx solid #f0f0f0;
  785. }
  786. .selector-text {
  787. font-size: 28rpx;
  788. color: #333;
  789. &.placeholder {
  790. color: #999;
  791. }
  792. }
  793. .arrow-icon {
  794. font-size: 24rpx;
  795. color: #999;
  796. }
  797. // 带单位的输入框
  798. .input-with-unit {
  799. display: flex;
  800. align-items: center;
  801. border-bottom: 1rpx solid #f0f0f0;
  802. padding: 12rpx 0;
  803. }
  804. .currency-symbol {
  805. font-size: 28rpx;
  806. color: #333;
  807. margin-right: 8rpx;
  808. }
  809. .unit-text {
  810. font-size: 28rpx;
  811. color: #666;
  812. margin-left: 8rpx;
  813. white-space: nowrap;
  814. }
  815. // 图片上传
  816. .image-upload-area {
  817. display: flex;
  818. flex-wrap: wrap;
  819. gap: 16rpx;
  820. }
  821. .image-item {
  822. position: relative;
  823. width: 160rpx;
  824. height: 160rpx;
  825. border-radius: 8rpx;
  826. overflow: hidden;
  827. }
  828. .uploaded-image {
  829. width: 100%;
  830. height: 100%;
  831. }
  832. .image-delete {
  833. position: absolute;
  834. top: -8rpx;
  835. right: -8rpx;
  836. width: 32rpx;
  837. height: 32rpx;
  838. background-color: #ff4d4f;
  839. color: #fff;
  840. border-radius: 50%;
  841. display: flex;
  842. align-items: center;
  843. justify-content: center;
  844. font-size: 20rpx;
  845. font-weight: bold;
  846. }
  847. .image-upload-btn {
  848. width: 160rpx;
  849. height: 160rpx;
  850. border: 2rpx dashed #ddd;
  851. border-radius: 8rpx;
  852. display: flex;
  853. flex-direction: column;
  854. align-items: center;
  855. justify-content: center;
  856. background-color: #fafafa;
  857. }
  858. .upload-icon {
  859. font-size: 48rpx;
  860. color: #999;
  861. margin-bottom: 8rpx;
  862. }
  863. .upload-text {
  864. font-size: 24rpx;
  865. color: #999;
  866. }
  867. // 产地信息
  868. .contact-info {
  869. background-color: #f8f9fa;
  870. border-radius: 8rpx;
  871. padding: 20rpx;
  872. }
  873. .contact-row {
  874. display: flex;
  875. align-items: center;
  876. margin-bottom: 12rpx;
  877. &:last-child {
  878. margin-bottom: 0;
  879. }
  880. }
  881. .contact-value {
  882. font-size: 26rpx;
  883. color: #333;
  884. }
  885. // 联系人信息输入
  886. .contact-input-row {
  887. display: flex;
  888. align-items: center;
  889. margin-bottom: 20rpx;
  890. &:last-child {
  891. margin-bottom: 0;
  892. }
  893. }
  894. .contact-label {
  895. font-size: 28rpx;
  896. color: #333;
  897. width: 120rpx;
  898. font-weight: bold;
  899. }
  900. .contact-input {
  901. flex: 1;
  902. height: 44rpx;
  903. font-size: 28rpx;
  904. color: #333;
  905. line-height: 44rpx;
  906. border-bottom: 1rpx solid #f0f0f0;
  907. padding: 12rpx 0;
  908. }
  909. // 底部提交按钮
  910. .bottom-action-bar {
  911. position: fixed;
  912. bottom: 0;
  913. left: 0;
  914. right: 0;
  915. width: 100%;
  916. background-color: #fff;
  917. border-top: 1rpx solid #f0f0f0;
  918. padding: 20rpx 30rpx;
  919. padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
  920. z-index: 1000;
  921. box-shadow: 0 -2rpx 8rpx rgba(0, 0, 0, 0.05);
  922. box-sizing: border-box;
  923. }
  924. .submit-btn {
  925. width: 100%;
  926. height: 88rpx;
  927. background-color: #4CAF50;
  928. border-radius: 12rpx;
  929. display: flex;
  930. align-items: center;
  931. justify-content: center;
  932. transition: background-color 0.2s ease;
  933. box-sizing: border-box;
  934. &:active {
  935. background-color: #45a049;
  936. }
  937. .btn-text {
  938. font-size: 32rpx;
  939. color: #fff;
  940. font-weight: bold;
  941. }
  942. }
  943. // 分类选择弹窗
  944. .picker-modal {
  945. position: fixed;
  946. top: 0;
  947. left: 0;
  948. width: 100%;
  949. height: 100%;
  950. background-color: rgba(0, 0, 0, 0.5);
  951. z-index: 2000;
  952. display: flex;
  953. align-items: flex-end;
  954. }
  955. .picker-content {
  956. width: 100%;
  957. background-color: #fff;
  958. border-radius: 24rpx 24rpx 0 0;
  959. padding: 0;
  960. }
  961. .picker-header {
  962. display: flex;
  963. justify-content: space-between;
  964. align-items: center;
  965. padding: 30rpx;
  966. border-bottom: 1rpx solid #f0f0f0;
  967. }
  968. .picker-title {
  969. font-size: 32rpx;
  970. font-weight: bold;
  971. color: #333;
  972. }
  973. .picker-close {
  974. font-size: 36rpx;
  975. color: #999;
  976. }
  977. .picker-options {
  978. padding: 20rpx 0;
  979. }
  980. .picker-option {
  981. padding: 24rpx 30rpx;
  982. border-bottom: 1rpx solid #f8f8f8;
  983. transition: background-color 0.2s ease;
  984. &:last-child {
  985. border-bottom: none;
  986. }
  987. &:active {
  988. // background-color: #f5f5f5;
  989. background-color: #f0f0f0;
  990. color: #007aff;
  991. font-weight: bold;
  992. }
  993. }
  994. .picker-option.active {
  995. background-color: #f0f0f0;
  996. color: #007aff;
  997. font-weight: bold;
  998. }
  999. .option-text {
  1000. font-size: 30rpx;
  1001. color: #333;
  1002. }
  1003. </style>