PageTwo.vue 20 KB

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