PageTwo.vue 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768
  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. // 按照 fileIds 排序
  249. if (this.currentReceipt.fileIds && list && list.length > 0) {
  250. const sortedIds = this.currentReceipt.fileIds.split(',')
  251. list.sort((a, b) => {
  252. const indexA = sortedIds.indexOf(a.id)
  253. const indexB = sortedIds.indexOf(b.id)
  254. // 如果都不在列表中,保持原顺序
  255. if (indexA === -1 && indexB === -1) return 0
  256. // 如果 a 不在列表中,放到后面
  257. if (indexA === -1) return 1
  258. // 如果 b 不在列表中,放到后面
  259. if (indexB === -1) return -1
  260. // 都在列表中,按 index 排序
  261. return indexA - indexB
  262. })
  263. }
  264. this.detailImages = list || []
  265. } catch (error) {
  266. console.error('加载细节图失败:', error)
  267. }
  268. },
  269. /**
  270. * 检查跟进内容
  271. */
  272. checkFollowUpContent(followUpList) {
  273. const contentList = followUpList.map(item => item.content || '')
  274. contentList.forEach(item => {
  275. if (item.includes('联系师傅') && !this.selectedCheckbox.includes('contactMaster')) {
  276. this.selectedCheckbox.push('contactMaster')
  277. const phone = item.split(';')[1] || ''
  278. this.formData.contactPhone = phone
  279. }
  280. if (item.includes('师傅拍图技巧') && !this.selectedCheckbox.includes('photoTips')) {
  281. this.selectedCheckbox.push('photoTips')
  282. const tips = item.split(';')[1] || ''
  283. this.formData.photoTips = tips
  284. const urls = item.split(';')[2] || ''
  285. this.photoTipsImages = urls.split(',').filter(url => url.trim())
  286. }
  287. if (item.includes('到达客户面对面') && !this.selectedCheckbox.includes('faceToFace')) {
  288. this.selectedCheckbox.push('faceToFace')
  289. const notes = item.split(';')[1] || ''
  290. this.formData.faceToFaceNotes = notes
  291. const urls = item.split(';')[2] || ''
  292. this.faceToFaceImages = urls.split(',').filter(url => url.trim())
  293. }
  294. })
  295. },
  296. /**
  297. * 价格输入处理
  298. */
  299. onPriceInput(e) {
  300. let value = Number(e.detail.value)
  301. if (isNaN(value)) value = 0
  302. value = Math.max(0, value)
  303. this.approvedPrice = value
  304. },
  305. /**
  306. * 快速调整价格
  307. */
  308. quickChangePrice(amount) {
  309. let newPrice = this.approvedPrice + amount
  310. newPrice = Math.max(0, newPrice)
  311. this.approvedPrice = newPrice
  312. },
  313. /**
  314. * 上传图片(跟进清单)
  315. */
  316. async handleUpload(field) {
  317. try {
  318. const filePaths = await imageUpload.chooseImage(9)
  319. const uploadResults = await imageUpload.uploadFiles(filePaths)
  320. const urls = uploadResults.map(item => item.fileUrl)
  321. if (field === 'photoTips') {
  322. this.photoTipsImages = [...this.photoTipsImages, ...urls]
  323. } else if (field === 'faceToFace') {
  324. this.faceToFaceImages = [...this.faceToFaceImages, ...urls]
  325. }
  326. } catch (error) {
  327. console.error('上传失败:', error)
  328. uni.$u.toast('上传失败')
  329. }
  330. },
  331. /**
  332. * 上传细节图
  333. */
  334. async handleUploadImage() {
  335. try {
  336. const filePaths = await imageUpload.chooseImage(9)
  337. const uploadResults = await imageUpload.uploadFiles(filePaths)
  338. await imageUpload.bindOrderFile(
  339. this.currentReceipt.clueId,
  340. this.currentReceipt.id,
  341. '3',
  342. uploadResults
  343. )
  344. await this.loadDetailImages()
  345. // 更新 fileIds
  346. const fileIds = this.detailImages.map(item => item.id).join(',')
  347. await uni.$u.api.updateReceiptForm({
  348. id: this.currentReceipt.id,
  349. fileIds: fileIds
  350. })
  351. this.$emit('update-file-ids', fileIds)
  352. } catch (error) {
  353. console.error('上传失败:', error)
  354. }
  355. },
  356. /**
  357. * 删除图片
  358. */
  359. async handleDeleteImage(item) {
  360. uni.showModal({
  361. title: '提示',
  362. content: '确定要删除这张图片吗?',
  363. success: async (res) => {
  364. if (res.confirm) {
  365. try {
  366. await imageUpload.deleteFile(item.id)
  367. await this.loadDetailImages()
  368. // 更新 fileIds
  369. const fileIds = this.detailImages.map(item => item.id).join(',')
  370. await uni.$u.api.updateReceiptForm({
  371. id: this.currentReceipt.id,
  372. fileIds: fileIds
  373. })
  374. this.$emit('update-file-ids', fileIds)
  375. } catch (error) {
  376. console.error('删除失败:', error)
  377. }
  378. }
  379. }
  380. })
  381. },
  382. /**
  383. * 复制所有细节图
  384. */
  385. async copyAllDetailImages() {
  386. const allUrls = this.detailImages.map(item => item.fileUrl)
  387. if (allUrls.length === 0) {
  388. uni.showToast({
  389. title: '没有图片可复制',
  390. icon: 'none'
  391. })
  392. return
  393. }
  394. uni.showModal({
  395. title: '保存图片',
  396. content: `是否将 ${allUrls.length} 张图片保存到本地相册?`,
  397. confirmText: '保存',
  398. success: (res) => {
  399. if (res.confirm) {
  400. imageUpload.saveImagesToLocal(allUrls)
  401. }
  402. }
  403. })
  404. },
  405. /**
  406. * 预览细节图
  407. */
  408. previewImageDetail(src) {
  409. const urlList = this.detailImages.map(item => item.fileUrl)
  410. uni.previewImage({
  411. urls: urlList,
  412. current: src
  413. })
  414. },
  415. /**
  416. * 预览师傅拍图技巧图片
  417. */
  418. previewPhotoTips(src) {
  419. uni.previewImage({
  420. urls: this.photoTipsImages,
  421. current: src
  422. })
  423. },
  424. /**
  425. * 预览到达客户面对面图片
  426. */
  427. previewFaceToFace(src) {
  428. uni.previewImage({
  429. urls: this.faceToFaceImages,
  430. current: src
  431. })
  432. },
  433. /**
  434. * 下一步
  435. */
  436. async handleNext() {
  437. // 保存核准价
  438. await uni.$u.api.updateReceiptForm({
  439. id: this.currentReceipt.id,
  440. sellingPrice: this.approvedPrice
  441. })
  442. // 保存跟进记录
  443. if (this.selectedCheckbox.includes('contactMaster')) {
  444. await uni.$u.api.addOrderFollow({
  445. orderId: this.orderId,
  446. content: `联系师傅;${this.formData.contactPhone}`
  447. })
  448. }
  449. if (this.selectedCheckbox.includes('photoTips')) {
  450. const urls = this.photoTipsImages.join(',')
  451. await uni.$u.api.addOrderFollow({
  452. orderId: this.orderId,
  453. content: `师傅拍图技巧;${this.formData.photoTips};${urls}`
  454. })
  455. }
  456. if (this.selectedCheckbox.includes('faceToFace')) {
  457. const urls = this.faceToFaceImages.join(',')
  458. await uni.$u.api.addOrderFollow({
  459. orderId: this.orderId,
  460. content: `到达客户面对面;${this.formData.faceToFaceNotes};${urls}`
  461. })
  462. }
  463. this.$emit('next', {
  464. nowPage: 'formTwo',
  465. form: {
  466. approvedPrice: this.approvedPrice,
  467. detailImages: this.detailImages
  468. }
  469. })
  470. }
  471. }
  472. }
  473. </script>
  474. <style scoped lang="scss">
  475. @import '../styles/common.scss';
  476. .page-two-container {
  477. @extend .page-container;
  478. padding-bottom: 100rpx;
  479. }
  480. .address-section {
  481. padding: 20rpx;
  482. }
  483. .checklist-card {
  484. margin-top: 20rpx;
  485. }
  486. .checklist-section {
  487. padding: 20rpx;
  488. }
  489. .checklist-item {
  490. padding: 16rpx 0;
  491. border-bottom: 1rpx solid map-get($colors, border);
  492. &:last-child {
  493. border-bottom: none;
  494. }
  495. }
  496. .checkbox-text-container {
  497. @include flex-center;
  498. margin-bottom: 12rpx;
  499. }
  500. .checklist-text {
  501. @include font-styles;
  502. margin-left: 16rpx;
  503. }
  504. .checklist-input,
  505. .checklist-textarea {
  506. margin-top: 12rpx;
  507. margin-left: 56rpx;
  508. width: calc(100% - 72rpx);
  509. border-radius: 8rpx;
  510. border: 1rpx solid #e5e7eb;
  511. padding: 12rpx 16rpx;
  512. }
  513. .upload-btn-container {
  514. margin-top: 16rpx;
  515. margin-left: 56rpx;
  516. }
  517. .upload-btn {
  518. display: inline-flex;
  519. align-items: center;
  520. gap: 12rpx;
  521. padding: 20rpx 40rpx;
  522. border-radius: 8rpx;
  523. background-color: map-get($colors, bg);
  524. border: 2rpx dashed map-get($colors, primary);
  525. color: map-get($colors, primary);
  526. cursor: pointer;
  527. }
  528. .upload-btn-text {
  529. @include font-styles($size: small, $weight: medium);
  530. }
  531. .image-list {
  532. margin-top: 16rpx;
  533. margin-left: 56rpx;
  534. display: flex;
  535. flex-wrap: wrap;
  536. gap: 16rpx;
  537. }
  538. .image-item {
  539. width: 120rpx;
  540. height: 120rpx;
  541. border-radius: 8rpx;
  542. overflow: hidden;
  543. cursor: pointer;
  544. transition: opacity 0.2s;
  545. &:active {
  546. opacity: 0.7;
  547. }
  548. }
  549. .image-thumb {
  550. width: 100%;
  551. height: 100%;
  552. object-fit: cover;
  553. }
  554. .price-card {
  555. margin-top: 20rpx;
  556. }
  557. .price-section {
  558. padding: 20rpx;
  559. }
  560. .price-picker-container {
  561. display: flex;
  562. flex-direction: column;
  563. align-items: center;
  564. padding: 20rpx 0;
  565. }
  566. .quick-actions {
  567. display: flex;
  568. justify-content: center;
  569. gap: 16rpx;
  570. margin: 5rpx 0;
  571. width: 100%;
  572. }
  573. .quick-btn {
  574. flex: 1;
  575. border-radius: 12rpx;
  576. font-size: 36rpx;
  577. padding: 10rpx 0;
  578. text-align: center;
  579. font-weight: 600;
  580. cursor: pointer;
  581. &.increase {
  582. background-color: #e6f7ed;
  583. color: #00b42a;
  584. }
  585. &.decrease {
  586. background-color: #fff1f0;
  587. color: #f53f3f;
  588. }
  589. }
  590. .number-box-container {
  591. display: flex;
  592. align-items: center;
  593. margin: 20rpx 0;
  594. width: 100%;
  595. justify-content: center;
  596. }
  597. .price-input-box {
  598. flex: 1;
  599. max-width: 800rpx;
  600. background-color: #f5f7fa;
  601. border: 2rpx solid #e5e7eb;
  602. border-radius: 12rpx;
  603. padding: 0rpx 24rpx;
  604. display: flex;
  605. align-items: center;
  606. justify-content: space-between;
  607. }
  608. .price-label {
  609. font-size: 30rpx;
  610. font-weight: 700;
  611. min-width: 120rpx;
  612. }
  613. .price-input {
  614. width: 100%;
  615. border: none;
  616. outline: none;
  617. background-color: transparent;
  618. text-align: right;
  619. font-size: 48rpx;
  620. font-weight: 600;
  621. padding: 0 10rpx;
  622. }
  623. .detail-image-card {
  624. margin-top: 20rpx;
  625. }
  626. .detail-image-section {
  627. padding: 20rpx;
  628. }
  629. .detail-image-header {
  630. display: flex;
  631. justify-content: space-between;
  632. align-items: center;
  633. margin-bottom: 20rpx;
  634. padding-bottom: 20rpx;
  635. border-bottom: 1rpx solid map-get($colors, border);
  636. }
  637. .detail-image-title {
  638. @include font-styles($size: content, $weight: bold, $color: primary);
  639. }
  640. .copy-btn {
  641. border-radius: 20rpx;
  642. border: 1rpx solid #007AFF;
  643. background-color: transparent;
  644. color: #007AFF;
  645. padding: 0 24rpx;
  646. height: 64rpx;
  647. line-height: 64rpx;
  648. cursor: pointer;
  649. }
  650. .detail-image-list {
  651. display: flex;
  652. flex-wrap: wrap;
  653. gap: 20rpx;
  654. }
  655. .detail-image-item {
  656. position: relative;
  657. width: 200rpx;
  658. height: 200rpx;
  659. }
  660. .detail-delete-btn {
  661. position: absolute;
  662. top: -10rpx;
  663. right: -10rpx;
  664. width: 40rpx;
  665. height: 40rpx;
  666. background-color: #ff4d4f;
  667. color: #fff;
  668. border-radius: 50%;
  669. display: flex;
  670. align-items: center;
  671. justify-content: center;
  672. font-weight: bold;
  673. z-index: 10;
  674. cursor: pointer;
  675. }
  676. .detail-upload-btn {
  677. width: 200rpx;
  678. height: 200rpx;
  679. border: 8rpx dashed #ddd;
  680. border-radius: 30rpx;
  681. display: flex;
  682. align-items: center;
  683. justify-content: center;
  684. background-color: #f9f9f9;
  685. cursor: pointer;
  686. }
  687. .next-btn {
  688. position: fixed;
  689. bottom: 10rpx;
  690. left: 2.5%;
  691. width: 95%;
  692. height: 80rpx;
  693. line-height: 80rpx;
  694. text-align: center;
  695. border-radius: 20rpx;
  696. }
  697. </style>