PageTwo.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703
  1. <template>
  2. <view class="page-two-container">
  3. <!-- 上门地址卡片 -->
  4. <view class="card-wrap">
  5. <view class="address-section">
  6. <view class="address-header">
  7. <u-icon name="map" size="36rpx" color="#108cff" class="location-icon" />
  8. <text class="address-title">上门地址</text>
  9. </view>
  10. <view class="address-content">
  11. <text class="address-text">{{ orderDetail.address || '未填写' }}</text>
  12. </view>
  13. </view>
  14. </view>
  15. <!-- 跟进清单卡片 -->
  16. <view class="card-wrap checklist-card">
  17. <view class="checklist-section">
  18. <u-checkbox-group v-model="selectedCheckbox" placement="column">
  19. <!-- 联系师傅 -->
  20. <view class="checklist-item">
  21. <view class="checkbox-text-container">
  22. <u-checkbox name="contactMaster" size="40rpx" color="#108cff" />
  23. <text class="checklist-text">联系师傅</text>
  24. </view>
  25. <u-input
  26. v-if="selectedCheckbox.includes('contactMaster')"
  27. v-model="formData.contactPhone"
  28. placeholder="请输入师傅手机号"
  29. class="checklist-input"
  30. />
  31. </view>
  32. <!-- 师傅拍图技巧 -->
  33. <view class="checklist-item">
  34. <view class="checkbox-text-container">
  35. <u-checkbox name="photoTips" size="40rpx" color="#108cff" />
  36. <text class="checklist-text">师傅拍图技巧</text>
  37. </view>
  38. <u-input
  39. v-if="selectedCheckbox.includes('photoTips')"
  40. v-model="formData.photoTips"
  41. type="textarea"
  42. placeholder="请输入拍图技巧"
  43. rows="3"
  44. class="checklist-textarea"
  45. />
  46. <view v-if="selectedCheckbox.includes('photoTips')" class="upload-btn-container">
  47. <view class="upload-btn" @click="handleUpload('photoTips')">
  48. <u-icon name="camera" size="32rpx" color="#108cff" />
  49. <text class="upload-btn-text">上传图片</text>
  50. </view>
  51. </view>
  52. <view
  53. v-if="selectedCheckbox.includes('photoTips') && photoTipsImages.length > 0"
  54. class="image-list"
  55. >
  56. <view
  57. v-for="(image, index) in photoTipsImages"
  58. :key="index"
  59. class="image-item"
  60. >
  61. <image :src="image" mode="aspectFill" class="image-thumb" />
  62. </view>
  63. </view>
  64. </view>
  65. <!-- 到达客户面对面 -->
  66. <view class="checklist-item">
  67. <view class="checkbox-text-container">
  68. <u-checkbox name="faceToFace" size="40rpx" color="#108cff" />
  69. <text class="checklist-text">到达客户面对面</text>
  70. </view>
  71. <u-input
  72. v-if="selectedCheckbox.includes('faceToFace')"
  73. v-model="formData.faceToFaceNotes"
  74. type="textarea"
  75. placeholder="请输入备注信息"
  76. rows="3"
  77. class="checklist-textarea"
  78. />
  79. <view v-if="selectedCheckbox.includes('faceToFace')" class="upload-btn-container">
  80. <view class="upload-btn" @click="handleUpload('faceToFace')">
  81. <u-icon name="camera" size="32rpx" color="#108cff" />
  82. <text class="upload-btn-text">上传图片</text>
  83. </view>
  84. </view>
  85. <view
  86. v-if="selectedCheckbox.includes('faceToFace') && faceToFaceImages.length > 0"
  87. class="image-list"
  88. >
  89. <view
  90. v-for="(image, index) in faceToFaceImages"
  91. :key="index"
  92. class="image-item"
  93. >
  94. <image :src="image" mode="aspectFill" class="image-thumb" />
  95. </view>
  96. </view>
  97. </view>
  98. </u-checkbox-group>
  99. </view>
  100. </view>
  101. <!-- 核准价卡片 -->
  102. <view class="card-wrap price-card">
  103. <view class="price-section">
  104. <view class="price-picker-container">
  105. <view class="quick-actions top-actions">
  106. <view class="quick-btn increase" @click="quickChangePrice(100)">+100</view>
  107. <view class="quick-btn increase" @click="quickChangePrice(1000)">+1000</view>
  108. </view>
  109. <view class="number-box-container">
  110. <view class="price-input-box">
  111. <text class="price-label">核准价¥</text>
  112. <input
  113. type="number"
  114. v-model="approvedPrice"
  115. class="price-input"
  116. placeholder="0"
  117. min="0"
  118. @input="onPriceInput"
  119. />
  120. </view>
  121. </view>
  122. <view class="quick-actions bottom-actions">
  123. <view class="quick-btn decrease" @click="quickChangePrice(-100)">-100</view>
  124. <view class="quick-btn decrease" @click="quickChangePrice(-1000)">-1000</view>
  125. </view>
  126. </view>
  127. </view>
  128. </view>
  129. <!-- 高清细节图卡片 -->
  130. <view class="card-wrap detail-image-card">
  131. <view class="detail-image-section">
  132. <view class="detail-image-header">
  133. <text class="detail-image-title">上传高清细节图(支持多选)</text>
  134. <view class="copy-btn" @click="copyAllDetailImages">
  135. <text>一键复制</text>
  136. </view>
  137. </view>
  138. <view class="detail-image-upload-container">
  139. <view class="detail-image-list">
  140. <view
  141. v-for="(item, index) in detailImages"
  142. :key="`detail-${index}`"
  143. class="detail-image-item"
  144. >
  145. <PicComp
  146. :src="item.fileUrl"
  147. @needPreviewPic="previewImageDetail"
  148. />
  149. <view class="detail-delete-btn" @click="handleDeleteImage(item)">×</view>
  150. </view>
  151. <view
  152. class="detail-upload-btn"
  153. @click="handleUploadImage('detailImages')"
  154. >
  155. <u-icon name="plus" size="40rpx" color="#999" />
  156. </view>
  157. </view>
  158. </view>
  159. </view>
  160. </view>
  161. <!-- 下一步按钮 -->
  162. <u-button
  163. class="next-btn"
  164. @click="handleNext"
  165. type="primary"
  166. size="middle"
  167. >
  168. 下一步
  169. </u-button>
  170. </view>
  171. </template>
  172. <script>
  173. import PicComp from './PicComp.vue'
  174. import imageUpload from '../utils/imageUpload.js'
  175. export default {
  176. name: 'PageTwo',
  177. components: {
  178. PicComp
  179. },
  180. props: {
  181. orderDetail: {
  182. type: Object,
  183. default: () => ({})
  184. },
  185. orderId: {
  186. type: String,
  187. default: ''
  188. },
  189. currentReceipt: {
  190. type: Object,
  191. default: () => ({})
  192. },
  193. followUpList: {
  194. type: Array,
  195. default: () => []
  196. }
  197. },
  198. data() {
  199. return {
  200. selectedCheckbox: [],
  201. formData: {
  202. contactPhone: '',
  203. photoTips: '',
  204. faceToFaceNotes: ''
  205. },
  206. photoTipsImages: [],
  207. faceToFaceImages: [],
  208. approvedPrice: 0,
  209. detailImages: []
  210. }
  211. },
  212. watch: {
  213. currentReceipt: {
  214. handler(newVal) {
  215. if (newVal && newVal.id) {
  216. this.approvedPrice = Number(newVal.sellingPrice) || 0
  217. this.loadDetailImages()
  218. }
  219. },
  220. immediate: true,
  221. deep: true
  222. },
  223. followUpList: {
  224. handler(newVal) {
  225. if (newVal && newVal.length > 0) {
  226. this.checkFollowUpContent(newVal)
  227. }
  228. },
  229. deep: true
  230. }
  231. },
  232. methods: {
  233. /**
  234. * 加载细节图
  235. */
  236. async loadDetailImages() {
  237. if (!this.currentReceipt.id || !this.orderDetail.itemBrand) return
  238. try {
  239. const list = await imageUpload.getFileList(
  240. '2',
  241. '3',
  242. this.currentReceipt.id,
  243. this.orderDetail.itemBrand,
  244. this.currentReceipt.clueId
  245. )
  246. this.detailImages = list || []
  247. } catch (error) {
  248. console.error('加载细节图失败:', error)
  249. }
  250. },
  251. /**
  252. * 检查跟进内容
  253. */
  254. checkFollowUpContent(followUpList) {
  255. const contentList = followUpList.map(item => item.content || '')
  256. contentList.forEach(item => {
  257. if (item.includes('联系师傅') && !this.selectedCheckbox.includes('contactMaster')) {
  258. this.selectedCheckbox.push('contactMaster')
  259. const phone = item.split(';')[1] || ''
  260. this.formData.contactPhone = phone
  261. }
  262. if (item.includes('师傅拍图技巧') && !this.selectedCheckbox.includes('photoTips')) {
  263. this.selectedCheckbox.push('photoTips')
  264. const tips = item.split(';')[1] || ''
  265. this.formData.photoTips = tips
  266. const urls = item.split(';')[2] || ''
  267. this.photoTipsImages = urls.split(',').filter(url => url.trim())
  268. }
  269. if (item.includes('到达客户面对面') && !this.selectedCheckbox.includes('faceToFace')) {
  270. this.selectedCheckbox.push('faceToFace')
  271. const notes = item.split(';')[1] || ''
  272. this.formData.faceToFaceNotes = notes
  273. const urls = item.split(';')[2] || ''
  274. this.faceToFaceImages = urls.split(',').filter(url => url.trim())
  275. }
  276. })
  277. },
  278. /**
  279. * 价格输入处理
  280. */
  281. onPriceInput(e) {
  282. let value = Number(e.detail.value)
  283. if (isNaN(value)) value = 0
  284. value = Math.max(0, value)
  285. this.approvedPrice = value
  286. },
  287. /**
  288. * 快速调整价格
  289. */
  290. quickChangePrice(amount) {
  291. let newPrice = this.approvedPrice + amount
  292. newPrice = Math.max(0, newPrice)
  293. this.approvedPrice = newPrice
  294. },
  295. /**
  296. * 上传图片(跟进清单)
  297. */
  298. async handleUpload(field) {
  299. try {
  300. const filePaths = await imageUpload.chooseImage(9)
  301. const uploadResults = await imageUpload.uploadFiles(filePaths)
  302. const urls = uploadResults.map(item => item.fileUrl)
  303. if (field === 'photoTips') {
  304. this.photoTipsImages = [...this.photoTipsImages, ...urls]
  305. } else if (field === 'faceToFace') {
  306. this.faceToFaceImages = [...this.faceToFaceImages, ...urls]
  307. }
  308. } catch (error) {
  309. console.error('上传失败:', error)
  310. uni.$u.toast('上传失败')
  311. }
  312. },
  313. /**
  314. * 上传细节图
  315. */
  316. async handleUploadImage() {
  317. try {
  318. const filePaths = await imageUpload.chooseImage(9)
  319. const uploadResults = await imageUpload.uploadFiles(filePaths)
  320. await imageUpload.bindOrderFile(
  321. this.currentReceipt.clueId,
  322. this.currentReceipt.id,
  323. '3',
  324. uploadResults
  325. )
  326. this.loadDetailImages()
  327. } catch (error) {
  328. console.error('上传失败:', error)
  329. }
  330. },
  331. /**
  332. * 删除图片
  333. */
  334. async handleDeleteImage(item) {
  335. uni.showModal({
  336. title: '提示',
  337. content: '确定要删除这张图片吗?',
  338. success: async (res) => {
  339. if (res.confirm) {
  340. try {
  341. await imageUpload.deleteFile(item.id)
  342. this.loadDetailImages()
  343. } catch (error) {
  344. console.error('删除失败:', error)
  345. }
  346. }
  347. }
  348. })
  349. },
  350. /**
  351. * 复制所有细节图
  352. */
  353. async copyAllDetailImages() {
  354. const allUrls = this.detailImages.map(item => item.fileUrl)
  355. if (allUrls.length === 0) {
  356. uni.showToast({
  357. title: '没有图片可复制',
  358. icon: 'none'
  359. })
  360. return
  361. }
  362. uni.showModal({
  363. title: '保存图片',
  364. content: `是否将 ${allUrls.length} 张图片保存到本地相册?`,
  365. confirmText: '保存',
  366. success: (res) => {
  367. if (res.confirm) {
  368. imageUpload.saveImagesToLocal(allUrls)
  369. }
  370. }
  371. })
  372. },
  373. /**
  374. * 预览细节图
  375. */
  376. previewImageDetail(src) {
  377. const urlList = this.detailImages.map(item => item.fileUrl)
  378. uni.previewImage({
  379. urls: urlList,
  380. current: src
  381. })
  382. },
  383. /**
  384. * 下一步
  385. */
  386. async handleNext() {
  387. // 保存核准价
  388. await uni.$u.api.updateReceiptForm({
  389. id: this.currentReceipt.id,
  390. sellingPrice: this.approvedPrice
  391. })
  392. // 保存跟进记录
  393. if (this.selectedCheckbox.includes('contactMaster')) {
  394. await uni.$u.api.addOrderFollow({
  395. orderId: this.orderId,
  396. content: `联系师傅;${this.formData.contactPhone}`
  397. })
  398. }
  399. if (this.selectedCheckbox.includes('photoTips')) {
  400. const urls = this.photoTipsImages.join(',')
  401. await uni.$u.api.addOrderFollow({
  402. orderId: this.orderId,
  403. content: `师傅拍图技巧;${this.formData.photoTips};${urls}`
  404. })
  405. }
  406. if (this.selectedCheckbox.includes('faceToFace')) {
  407. const urls = this.faceToFaceImages.join(',')
  408. await uni.$u.api.addOrderFollow({
  409. orderId: this.orderId,
  410. content: `到达客户面对面;${this.formData.faceToFaceNotes};${urls}`
  411. })
  412. }
  413. this.$emit('next', {
  414. nowPage: 'formTwo',
  415. form: {
  416. approvedPrice: this.approvedPrice,
  417. detailImages: this.detailImages
  418. }
  419. })
  420. }
  421. }
  422. }
  423. </script>
  424. <style scoped lang="scss">
  425. @import '../styles/common.scss';
  426. .page-two-container {
  427. @extend .page-container;
  428. padding-bottom: 100rpx;
  429. }
  430. .address-section {
  431. padding: 20rpx;
  432. }
  433. .checklist-card {
  434. margin-top: 20rpx;
  435. }
  436. .checklist-section {
  437. padding: 20rpx;
  438. }
  439. .checklist-item {
  440. padding: 16rpx 0;
  441. border-bottom: 1rpx solid map-get($colors, border);
  442. &:last-child {
  443. border-bottom: none;
  444. }
  445. }
  446. .checkbox-text-container {
  447. @include flex-center;
  448. margin-bottom: 12rpx;
  449. }
  450. .checklist-text {
  451. @include font-styles;
  452. margin-left: 16rpx;
  453. }
  454. .checklist-input,
  455. .checklist-textarea {
  456. margin-top: 12rpx;
  457. margin-left: 56rpx;
  458. width: calc(100% - 72rpx);
  459. border-radius: 8rpx;
  460. border: 1rpx solid #e5e7eb;
  461. padding: 12rpx 16rpx;
  462. }
  463. .upload-btn-container {
  464. margin-top: 16rpx;
  465. margin-left: 56rpx;
  466. }
  467. .upload-btn {
  468. display: inline-flex;
  469. align-items: center;
  470. gap: 12rpx;
  471. padding: 20rpx 40rpx;
  472. border-radius: 8rpx;
  473. background-color: map-get($colors, bg);
  474. border: 2rpx dashed map-get($colors, primary);
  475. color: map-get($colors, primary);
  476. cursor: pointer;
  477. }
  478. .upload-btn-text {
  479. @include font-styles($size: small, $weight: medium);
  480. }
  481. .image-list {
  482. margin-top: 16rpx;
  483. margin-left: 56rpx;
  484. display: flex;
  485. flex-wrap: wrap;
  486. gap: 16rpx;
  487. }
  488. .image-item {
  489. width: 120rpx;
  490. height: 120rpx;
  491. border-radius: 8rpx;
  492. overflow: hidden;
  493. }
  494. .image-thumb {
  495. width: 100%;
  496. height: 100%;
  497. object-fit: cover;
  498. }
  499. .price-card {
  500. margin-top: 20rpx;
  501. }
  502. .price-section {
  503. padding: 20rpx;
  504. }
  505. .price-picker-container {
  506. display: flex;
  507. flex-direction: column;
  508. align-items: center;
  509. padding: 20rpx 0;
  510. }
  511. .quick-actions {
  512. display: flex;
  513. justify-content: center;
  514. gap: 16rpx;
  515. margin: 5rpx 0;
  516. width: 100%;
  517. }
  518. .quick-btn {
  519. flex: 1;
  520. border-radius: 12rpx;
  521. font-size: 36rpx;
  522. padding: 10rpx 0;
  523. text-align: center;
  524. font-weight: 600;
  525. cursor: pointer;
  526. &.increase {
  527. background-color: #e6f7ed;
  528. color: #00b42a;
  529. }
  530. &.decrease {
  531. background-color: #fff1f0;
  532. color: #f53f3f;
  533. }
  534. }
  535. .number-box-container {
  536. display: flex;
  537. align-items: center;
  538. margin: 20rpx 0;
  539. width: 100%;
  540. justify-content: center;
  541. }
  542. .price-input-box {
  543. flex: 1;
  544. max-width: 800rpx;
  545. background-color: #f5f7fa;
  546. border: 2rpx solid #e5e7eb;
  547. border-radius: 12rpx;
  548. padding: 0rpx 24rpx;
  549. display: flex;
  550. align-items: center;
  551. justify-content: space-between;
  552. }
  553. .price-label {
  554. font-size: 30rpx;
  555. font-weight: 700;
  556. min-width: 120rpx;
  557. }
  558. .price-input {
  559. width: 100%;
  560. border: none;
  561. outline: none;
  562. background-color: transparent;
  563. text-align: right;
  564. font-size: 48rpx;
  565. font-weight: 600;
  566. padding: 0 10rpx;
  567. }
  568. .detail-image-card {
  569. margin-top: 20rpx;
  570. }
  571. .detail-image-section {
  572. padding: 20rpx;
  573. }
  574. .detail-image-header {
  575. display: flex;
  576. justify-content: space-between;
  577. align-items: center;
  578. margin-bottom: 20rpx;
  579. padding-bottom: 20rpx;
  580. border-bottom: 1rpx solid map-get($colors, border);
  581. }
  582. .detail-image-title {
  583. @include font-styles($size: content, $weight: bold, $color: primary);
  584. }
  585. .copy-btn {
  586. border-radius: 20rpx;
  587. border: 1rpx solid #007AFF;
  588. background-color: transparent;
  589. color: #007AFF;
  590. padding: 0 24rpx;
  591. height: 64rpx;
  592. line-height: 64rpx;
  593. cursor: pointer;
  594. }
  595. .detail-image-list {
  596. display: flex;
  597. flex-wrap: wrap;
  598. gap: 20rpx;
  599. }
  600. .detail-image-item {
  601. position: relative;
  602. width: 200rpx;
  603. height: 200rpx;
  604. }
  605. .detail-delete-btn {
  606. position: absolute;
  607. top: -10rpx;
  608. right: -10rpx;
  609. width: 40rpx;
  610. height: 40rpx;
  611. background-color: #ff4d4f;
  612. color: #fff;
  613. border-radius: 50%;
  614. display: flex;
  615. align-items: center;
  616. justify-content: center;
  617. font-weight: bold;
  618. z-index: 10;
  619. cursor: pointer;
  620. }
  621. .detail-upload-btn {
  622. width: 200rpx;
  623. height: 200rpx;
  624. border: 8rpx dashed #ddd;
  625. border-radius: 30rpx;
  626. display: flex;
  627. align-items: center;
  628. justify-content: center;
  629. background-color: #f9f9f9;
  630. cursor: pointer;
  631. }
  632. .next-btn {
  633. position: fixed;
  634. bottom: 10rpx;
  635. left: 2.5%;
  636. width: 95%;
  637. height: 80rpx;
  638. line-height: 80rpx;
  639. text-align: center;
  640. border-radius: 20rpx;
  641. }
  642. </style>