sales-publish.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802
  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.productName" 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.category }">
  26. {{ formData.category || '请选择产品分类' }}
  27. </text>
  28. <text class="arrow-icon">></text>
  29. </view>
  30. </view>
  31. </view>
  32. <!-- 销售价格 -->
  33. <view class="form-item">
  34. <view class="item-label">
  35. <text class="label-text">销售价格</text>
  36. <text class="required">*</text>
  37. </view>
  38. <view class="item-content">
  39. <view class="input-with-unit">
  40. <text class="currency-symbol">¥</text>
  41. <input class="form-input" v-model="formData.price" placeholder="请输入销售价格"
  42. placeholder-style="color: #999;" type="digit" />
  43. <text class="unit-text">元/斤</text>
  44. </view>
  45. </view>
  46. </view>
  47. <!-- 产品简介 -->
  48. <view class="form-item">
  49. <view class="item-label">
  50. <text class="label-text">产品简介</text>
  51. </view>
  52. <view class="item-content">
  53. <textarea class="form-textarea" v-model="formData.description" placeholder="请简单介绍您的农产品特色、种植方式等"
  54. placeholder-style="color: #999;" maxlength="80" auto-height @input="onDescInput" />
  55. <view class="char-count">{{ descLength }}/80</view>
  56. </view>
  57. </view>
  58. <!-- 产品图片 -->
  59. <view class="form-item">
  60. <view class="item-label">
  61. <text class="label-text">产品图片</text>
  62. <text class="required">*</text>
  63. <text class="optional">(最多3张)</text>
  64. </view>
  65. <view class="item-content">
  66. <view class="image-upload-area">
  67. <view class="image-item" v-for="(image, index) in formData.images" :key="index">
  68. <image class="uploaded-image" :src="image" mode="aspectFill"></image>
  69. <view class="image-delete" @click="removeImage(index)">×</view>
  70. </view>
  71. <view class="image-upload-btn" v-if="formData.images.length < 3" @click="chooseImage">
  72. <text class="upload-icon">+</text>
  73. <text class="upload-text">上传图片</text>
  74. </view>
  75. </view>
  76. </view>
  77. </view>
  78. <!-- 产地信息 -->
  79. <view class="form-item">
  80. <view class="item-label">
  81. <text class="label-text">产地信息</text>
  82. </view>
  83. <view class="contact-info">
  84. <view class="contact-row">
  85. <text class="contact-label">产地:</text>
  86. <text class="contact-value">{{ locationInfo.address }}</text>
  87. </view>
  88. <view class="contact-row">
  89. <text class="contact-label">地块:</text>
  90. <text class="contact-value">{{ locationInfo.field }}</text>
  91. </view>
  92. </view>
  93. </view>
  94. <uni-data-picker v-slot:default="{data, error, options}" :localdata="localData" popup-title="请选择省市区"
  95. @change="onchange" @nodeclick="onnodeclick">
  96. <view class="selectedAddress">
  97. <view v-if="data.length == 0 && curLocation">{{ curLocation }}</view>
  98. <view v-if="data.length" class="selected">
  99. <view v-for="(item,index) in data" :key="index" class="selected-item">
  100. <text>{{item.text}} </text>
  101. </view>
  102. </view>
  103. <view class="addrlocation">
  104. <uni-icons type="location" color="#ec4149" size="24"></uni-icons>
  105. </view>
  106. </view>
  107. </uni-data-picker>
  108. <!-- 联系人信息 -->
  109. <view class="form-item">
  110. <view class="item-label">
  111. <text class="label-text">联系人信息</text>
  112. </view>
  113. <view class="item-content">
  114. <view class="contact-input-row">
  115. <text class="contact-label">联系人:</text>
  116. <input class="contact-input" v-model="formData.contactName" placeholder="请输入联系人姓名"
  117. placeholder-style="color: #999;" maxlength="10" />
  118. </view>
  119. <view class="contact-input-row">
  120. <text class="contact-label">电话:</text>
  121. <input class="contact-input" v-model="formData.contactPhone" placeholder="请输入联系电话"
  122. placeholder-style="color: #999;" type="number" maxlength="11" />
  123. </view>
  124. </view>
  125. </view>
  126. </view>
  127. <!-- 底部提交按钮 -->
  128. <view class="bottom-action-bar">
  129. <view class="submit-btn" @click="submitForm">
  130. <text class="btn-text">{{ isEditMode ? '保存修改' : '提交审核' }}</text>
  131. </view>
  132. </view>
  133. <!-- 分类选择弹窗 -->
  134. <view class="picker-modal" v-if="showCategoryPicker" @click="showCategoryPicker = false">
  135. <view class="picker-content" @click.stop>
  136. <view class="picker-header">
  137. <text class="picker-title">选择产品分类</text>
  138. <text class="picker-close" @click="showCategoryPicker = false">×</text>
  139. </view>
  140. <view class="picker-options">
  141. <view class="picker-option" v-for="category in categoryOptions" :key="category"
  142. @click="selectCategory(category)">
  143. <text class="option-text">{{ category }}</text>
  144. </view>
  145. </view>
  146. </view>
  147. </view>
  148. </view>
  149. </template>
  150. <script>
  151. import cityRows from '@/utils/data.json';
  152. export default {
  153. data() {
  154. return {
  155. isEditMode: false, // 是否为编辑模式
  156. editProductId: '', // 编辑的产品ID
  157. productType: 'sale', // 产品类型:sale(销售)或 purchase(收购)
  158. formData: {
  159. productName: '',
  160. category: '',
  161. price: '',
  162. description: '',
  163. images: [],
  164. contactName: '',
  165. contactPhone: ''
  166. },
  167. locationInfo: {
  168. address: '张家村',
  169. field: '水稻田A区'
  170. },
  171. categoryOptions: ['蔬菜', '水果', '粮食', '畜产品', '水产品', '中药材'],
  172. showCategoryPicker: false,
  173. nameLength: 0,
  174. descLength: 0,
  175. localData: [], //省市区地址
  176. curLocation: uni.getStorageSync('location_address')
  177. }
  178. },
  179. onLoad(options) {
  180. this.localData = this.get_city_tree()
  181. // 检查是否为编辑模式
  182. if (options.action === 'edit' && options.id) {
  183. this.isEditMode = true;
  184. this.editProductId = options.id;
  185. this.productType = options.type || 'sale';
  186. // 设置页面标题
  187. uni.setNavigationBarTitle({
  188. title: this.productType === 'sale' ? '编辑销售信息' : '编辑收购信息'
  189. });
  190. // 加载产品数据
  191. this.loadProductData();
  192. } else {
  193. // 新建模式
  194. this.isEditMode = false;
  195. uni.setNavigationBarTitle({
  196. title: '发布农产品'
  197. });
  198. }
  199. },
  200. methods: {
  201. onchange(e) {
  202. const value = e.detail.value
  203. console.log("value",value);
  204. },
  205. onnodeclick(node) {
  206. console.log("node",node);
  207. console.log("curLocation",this.curLocation);
  208. },
  209. // 省市区数据树生成
  210. get_city_tree() {
  211. let res = []
  212. if (cityRows.length) {
  213. // 递归生成
  214. res = this.handleTree(cityRows)
  215. }
  216. return res
  217. },
  218. handleTree(data, parent_code = null) {
  219. let res = []
  220. let keys = {
  221. id: 'code',
  222. pid: 'parent_code',
  223. children: 'children',
  224. text: 'name',
  225. value: 'code'
  226. }
  227. let oneItemDEMO = {
  228. text: '',
  229. value: '',
  230. children: []
  231. }
  232. let oneItem = {}
  233. // 循环
  234. for (let index in data) {
  235. // 判断
  236. if (parent_code === null) {
  237. // 顶级菜单 - 省
  238. if (!data[index].hasOwnProperty(keys.pid) || data[index][keys.pid] == parent_code) {
  239. // 不存在parent_code,或者已匹配
  240. oneItem = JSON.parse(JSON.stringify(oneItemDEMO))
  241. oneItem.text = data[index][keys.text]
  242. oneItem.value = data[index][keys.value]
  243. // 递归下去
  244. oneItem.children = this.handleTree(data, data[index][keys.id])
  245. res.push(oneItem)
  246. }
  247. } else {
  248. // 非顶级菜单 - 市、区、街道
  249. if (data[index].hasOwnProperty(keys.pid) && data[index][keys.pid] == parent_code) {
  250. // 已匹配
  251. oneItem = JSON.parse(JSON.stringify(oneItemDEMO))
  252. oneItem.text = data[index][keys.text]
  253. oneItem.value = data[index][keys.value]
  254. // 递归下去
  255. oneItem.children = this.handleTree(data, data[index][keys.id])
  256. res.push(oneItem)
  257. }
  258. }
  259. }
  260. return res
  261. },
  262. // 加载产品数据(编辑模式)
  263. loadProductData() {
  264. // 模拟从API加载数据,实际应用中需要根据ID和类型调用相应的API
  265. uni.showLoading({
  266. title: '加载中...'
  267. });
  268. setTimeout(() => {
  269. let mockData;
  270. if (this.productType === 'sale') {
  271. // 销售信息的模拟数据
  272. mockData = {
  273. productName: '2024年红富士苹果',
  274. category: '水果',
  275. price: '8.5',
  276. description: '自家果园种植的红富士苹果,个大味甜,果形端正,色泽鲜艳。采用有机种植方式,无农药残留,口感清脆香甜。',
  277. images: [
  278. '/static/images/products/corn-seeds-new.jpg',
  279. '/static/images/products/rice-seeds.jpg'
  280. ],
  281. contactName: '张先生',
  282. contactPhone: '18812341234'
  283. };
  284. } else {
  285. // 收购信息的模拟数据
  286. mockData = {
  287. productName: '高价收购优质土豆',
  288. category: '蔬菜',
  289. price: '3.5',
  290. description: '大量收购优质土豆,要求新鲜无病害,无青皮,规格统一。支持长期合作,价格优惠,现金结算。',
  291. images: [
  292. '/static/images/products/agriculture-tools.jpg'
  293. ],
  294. contactName: '李先生',
  295. contactPhone: '13856785678'
  296. };
  297. }
  298. // 填充表单数据
  299. this.formData = {
  300. ...mockData
  301. };
  302. // 更新字符计数
  303. this.nameLength = this.formData.productName.length;
  304. this.descLength = this.formData.description.length;
  305. uni.hideLoading();
  306. }, 1000);
  307. },
  308. // 产品名称输入处理
  309. onNameInput(e) {
  310. this.nameLength = e.detail.value.length;
  311. },
  312. // 描述输入处理
  313. onDescInput(e) {
  314. this.descLength = e.detail.value.length;
  315. },
  316. // 选择分类
  317. selectCategory(category) {
  318. this.formData.category = category;
  319. this.showCategoryPicker = false;
  320. },
  321. // 选择图片
  322. chooseImage() {
  323. const remainingCount = 3 - this.formData.images.length;
  324. uni.chooseImage({
  325. count: remainingCount,
  326. sizeType: ['compressed'],
  327. sourceType: ['album', 'camera'],
  328. success: (res) => {
  329. this.formData.images = this.formData.images.concat(res.tempFilePaths);
  330. },
  331. fail: (err) => {
  332. console.error('选择图片失败:', err);
  333. }
  334. });
  335. },
  336. // 删除图片
  337. removeImage(index) {
  338. this.formData.images.splice(index, 1);
  339. },
  340. // 表单验证
  341. validateForm() {
  342. if (!this.formData.productName.trim()) {
  343. uni.showToast({
  344. title: '请输入产品名称',
  345. icon: 'none'
  346. });
  347. return false;
  348. }
  349. if (!this.formData.category) {
  350. uni.showToast({
  351. title: '请选择产品分类',
  352. icon: 'none'
  353. });
  354. return false;
  355. }
  356. if (!this.formData.price || this.formData.price <= 0) {
  357. uni.showToast({
  358. title: '请输入有效的销售价格',
  359. icon: 'none'
  360. });
  361. return false;
  362. }
  363. if (this.formData.images.length === 0) {
  364. uni.showToast({
  365. title: '请至少上传一张产品图片',
  366. icon: 'none'
  367. });
  368. return false;
  369. }
  370. if (!this.formData.contactName.trim()) {
  371. uni.showToast({
  372. title: '请输入联系人姓名',
  373. icon: 'none'
  374. });
  375. return false;
  376. }
  377. if (!this.formData.contactPhone.trim()) {
  378. uni.showToast({
  379. title: '请输入联系电话',
  380. icon: 'none'
  381. });
  382. return false;
  383. }
  384. // 简单的手机号格式验证
  385. const phoneRegex = /^1[3-9]\d{9}$/;
  386. if (!phoneRegex.test(this.formData.contactPhone)) {
  387. uni.showToast({
  388. title: '请输入正确的手机号码',
  389. icon: 'none'
  390. });
  391. return false;
  392. }
  393. return true;
  394. },
  395. // 提交表单
  396. submitForm() {
  397. if (!this.validateForm()) {
  398. return;
  399. }
  400. // 显示加载提示
  401. const loadingTitle = this.isEditMode ? '保存中...' : '提交中...';
  402. uni.showLoading({
  403. title: loadingTitle
  404. });
  405. // 模拟提交
  406. setTimeout(() => {
  407. uni.hideLoading();
  408. if (this.isEditMode) {
  409. // 编辑模式 - 保存修改
  410. uni.showModal({
  411. title: '保存成功',
  412. content: '您的修改已保存成功!',
  413. showCancel: false,
  414. success: () => {
  415. uni.navigateBack();
  416. }
  417. });
  418. } else {
  419. // 新建模式 - 提交审核
  420. uni.showModal({
  421. title: '提交成功',
  422. content: '您的农产品信息已提交审核,审核通过后将在销售页面展示。',
  423. showCancel: false,
  424. success: () => {
  425. uni.navigateBack();
  426. }
  427. });
  428. }
  429. }, 2000);
  430. }
  431. }
  432. }
  433. </script>
  434. <style lang="scss">
  435. .sales-publish-container {
  436. min-height: 100vh;
  437. background-color: #f5f5f5;
  438. padding-bottom: calc(128rpx + env(safe-area-inset-bottom));
  439. }
  440. .form-container {
  441. padding: 20rpx;
  442. }
  443. .form-item {
  444. background-color: #fff;
  445. border-radius: 12rpx;
  446. padding: 24rpx;
  447. margin-bottom: 16rpx;
  448. box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
  449. }
  450. .item-label {
  451. display: flex;
  452. align-items: center;
  453. margin-bottom: 16rpx;
  454. }
  455. .label-text {
  456. font-size: 28rpx;
  457. color: #333;
  458. font-weight: bold;
  459. }
  460. .required {
  461. color: #ff4d4f;
  462. font-size: 28rpx;
  463. margin-left: 4rpx;
  464. }
  465. .optional {
  466. color: #999;
  467. font-size: 24rpx;
  468. margin-left: 8rpx;
  469. }
  470. .item-content {
  471. position: relative;
  472. }
  473. .form-input {
  474. width: 100%;
  475. height: 44rpx;
  476. font-size: 28rpx;
  477. color: #333;
  478. line-height: 44rpx;
  479. }
  480. .form-textarea {
  481. width: 100%;
  482. min-height: 120rpx;
  483. font-size: 28rpx;
  484. color: #333;
  485. line-height: 1.5;
  486. }
  487. .char-count {
  488. position: absolute;
  489. right: 0;
  490. bottom: -4rpx;
  491. font-size: 24rpx;
  492. color: #999;
  493. }
  494. // 分类选择器
  495. .category-selector {
  496. display: flex;
  497. align-items: center;
  498. justify-content: space-between;
  499. padding: 12rpx 0;
  500. border-bottom: 1rpx solid #f0f0f0;
  501. }
  502. .selector-text {
  503. font-size: 28rpx;
  504. color: #333;
  505. &.placeholder {
  506. color: #999;
  507. }
  508. }
  509. .arrow-icon {
  510. font-size: 24rpx;
  511. color: #999;
  512. }
  513. // 带单位的输入框
  514. .input-with-unit {
  515. display: flex;
  516. align-items: center;
  517. border-bottom: 1rpx solid #f0f0f0;
  518. padding: 12rpx 0;
  519. }
  520. .currency-symbol {
  521. font-size: 28rpx;
  522. color: #333;
  523. margin-right: 8rpx;
  524. }
  525. .unit-text {
  526. font-size: 28rpx;
  527. color: #666;
  528. margin-left: 8rpx;
  529. white-space: nowrap;
  530. }
  531. // 图片上传
  532. .image-upload-area {
  533. display: flex;
  534. flex-wrap: wrap;
  535. gap: 16rpx;
  536. }
  537. .image-item {
  538. position: relative;
  539. width: 160rpx;
  540. height: 160rpx;
  541. border-radius: 8rpx;
  542. overflow: hidden;
  543. }
  544. .uploaded-image {
  545. width: 100%;
  546. height: 100%;
  547. }
  548. .image-delete {
  549. position: absolute;
  550. top: -8rpx;
  551. right: -8rpx;
  552. width: 32rpx;
  553. height: 32rpx;
  554. background-color: #ff4d4f;
  555. color: #fff;
  556. border-radius: 50%;
  557. display: flex;
  558. align-items: center;
  559. justify-content: center;
  560. font-size: 20rpx;
  561. font-weight: bold;
  562. }
  563. .image-upload-btn {
  564. width: 160rpx;
  565. height: 160rpx;
  566. border: 2rpx dashed #ddd;
  567. border-radius: 8rpx;
  568. display: flex;
  569. flex-direction: column;
  570. align-items: center;
  571. justify-content: center;
  572. background-color: #fafafa;
  573. }
  574. .upload-icon {
  575. font-size: 48rpx;
  576. color: #999;
  577. margin-bottom: 8rpx;
  578. }
  579. .upload-text {
  580. font-size: 24rpx;
  581. color: #999;
  582. }
  583. // 产地信息
  584. .contact-info {
  585. background-color: #f8f9fa;
  586. border-radius: 8rpx;
  587. padding: 20rpx;
  588. }
  589. .contact-row {
  590. display: flex;
  591. align-items: center;
  592. margin-bottom: 12rpx;
  593. &:last-child {
  594. margin-bottom: 0;
  595. }
  596. }
  597. .contact-value {
  598. font-size: 26rpx;
  599. color: #333;
  600. }
  601. // 联系人信息输入
  602. .contact-input-row {
  603. display: flex;
  604. align-items: center;
  605. margin-bottom: 20rpx;
  606. &:last-child {
  607. margin-bottom: 0;
  608. }
  609. }
  610. .contact-label {
  611. font-size: 28rpx;
  612. color: #333;
  613. width: 120rpx;
  614. font-weight: bold;
  615. }
  616. .contact-input {
  617. flex: 1;
  618. height: 44rpx;
  619. font-size: 28rpx;
  620. color: #333;
  621. line-height: 44rpx;
  622. border-bottom: 1rpx solid #f0f0f0;
  623. padding: 12rpx 0;
  624. }
  625. // 底部提交按钮
  626. .bottom-action-bar {
  627. position: fixed;
  628. bottom: 0;
  629. left: 0;
  630. right: 0;
  631. width: 100%;
  632. background-color: #fff;
  633. border-top: 1rpx solid #f0f0f0;
  634. padding: 20rpx 30rpx;
  635. padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
  636. z-index: 1000;
  637. box-shadow: 0 -2rpx 8rpx rgba(0, 0, 0, 0.05);
  638. box-sizing: border-box;
  639. }
  640. .submit-btn {
  641. width: 100%;
  642. height: 88rpx;
  643. background-color: #4CAF50;
  644. border-radius: 12rpx;
  645. display: flex;
  646. align-items: center;
  647. justify-content: center;
  648. transition: background-color 0.2s ease;
  649. box-sizing: border-box;
  650. &:active {
  651. background-color: #45a049;
  652. }
  653. .btn-text {
  654. font-size: 32rpx;
  655. color: #fff;
  656. font-weight: bold;
  657. }
  658. }
  659. // 分类选择弹窗
  660. .picker-modal {
  661. position: fixed;
  662. top: 0;
  663. left: 0;
  664. width: 100%;
  665. height: 100%;
  666. background-color: rgba(0, 0, 0, 0.5);
  667. z-index: 2000;
  668. display: flex;
  669. align-items: flex-end;
  670. }
  671. .picker-content {
  672. width: 100%;
  673. background-color: #fff;
  674. border-radius: 24rpx 24rpx 0 0;
  675. padding: 0;
  676. }
  677. .picker-header {
  678. display: flex;
  679. justify-content: space-between;
  680. align-items: center;
  681. padding: 30rpx;
  682. border-bottom: 1rpx solid #f0f0f0;
  683. }
  684. .picker-title {
  685. font-size: 32rpx;
  686. font-weight: bold;
  687. color: #333;
  688. }
  689. .picker-close {
  690. font-size: 36rpx;
  691. color: #999;
  692. }
  693. .picker-options {
  694. padding: 20rpx 0;
  695. }
  696. .picker-option {
  697. padding: 24rpx 30rpx;
  698. border-bottom: 1rpx solid #f8f8f8;
  699. transition: background-color 0.2s ease;
  700. &:last-child {
  701. border-bottom: none;
  702. }
  703. &:active {
  704. background-color: #f5f5f5;
  705. }
  706. }
  707. .option-text {
  708. font-size: 30rpx;
  709. color: #333;
  710. }
  711. </style>