PageTwo.vue 18 KB

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