OrderDetailView.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520
  1. <template>
  2. <view
  3. class="order-detail-view"
  4. @touchstart="onTouchStart"
  5. @touchmove="onTouchMove"
  6. >
  7. <!-- 单 scroll-view:先滚完当前页再进入下一页,避免误触下拉刷新 -->
  8. <scroll-view
  9. scroll-y
  10. class="page-scroll-view"
  11. :scroll-top="snapScrollTop >= 0 ? snapScrollTop : undefined"
  12. :scroll-into-view="scrollIntoView"
  13. :scroll-with-animation="true"
  14. :show-scrollbar="false"
  15. @scroll="onScroll"
  16. >
  17. <view id="page0" class="page-section" :style="{ minHeight: sectionMinHeight }">
  18. <view class="page-item">
  19. <PageOne :order-detail="orderDetail" :order-id="orderId" :current-receipt="currentReceipt" @next="handleNext" />
  20. </view>
  21. </view>
  22. <view id="page1" class="page-section" :style="{ minHeight: sectionMinHeight }">
  23. <view class="page-item">
  24. <PageTwo
  25. :order-detail="orderDetail"
  26. :order-id="orderId"
  27. :current-receipt="currentReceipt"
  28. :follow-up-list="followUpList"
  29. @next="handleNext"
  30. @update-file-ids="handleUpdateFileIds"
  31. @price-updated="$emit('price-updated')"
  32. @follow-saved="loadFollowUpList"
  33. />
  34. </view>
  35. </view>
  36. <view id="page2" class="page-section" :style="{ minHeight: sectionMinHeight }">
  37. <view class="page-item">
  38. <PageThree
  39. ref="pageThreeRef"
  40. :order-detail="orderDetail"
  41. :order-id="orderId"
  42. :current-receipt="currentReceipt"
  43. @next="handleNext"
  44. @save="handleNeedSave"
  45. @confirm-pay="handleConfirmPay"
  46. @update-file-ids="handleUpdateFileIds"
  47. @price-updated="$emit('price-updated')"
  48. />
  49. </view>
  50. </view>
  51. <view id="page3" class="page-section" :style="{ minHeight: sectionMinHeight }">
  52. <view class="page-item">
  53. <PageFour
  54. :order-detail="orderDetail"
  55. :current-receipt="currentReceipt"
  56. @next="handleNext"
  57. @confirm-warehouse="handleConfirmWarehouse"
  58. @update-order-detail="$emit('update-order-detail', $event)"
  59. />
  60. </view>
  61. </view>
  62. </scroll-view>
  63. <!-- 页面导航(点击仍可切换) -->
  64. <ul class="page-navigation">
  65. <li
  66. v-for="(tab, index) in tabs"
  67. :key="index"
  68. :class="{ active: activeIndex === index }"
  69. @click="handleTabClick(index)"
  70. >
  71. {{ tab }}
  72. </li>
  73. </ul>
  74. </view>
  75. </template>
  76. <script>
  77. import PageOne from './PageOne.vue'
  78. import PageTwo from './PageTwo.vue'
  79. import PageThree from './PageThree.vue'
  80. import PageFour from './PageFour.vue'
  81. export default {
  82. name: 'OrderDetailView',
  83. components: {
  84. PageOne,
  85. PageTwo,
  86. PageThree,
  87. PageFour
  88. },
  89. props: {
  90. orderDetail: {
  91. type: Object,
  92. default: () => ({})
  93. },
  94. topInfo: {
  95. type: Object,
  96. default: () => ({})
  97. },
  98. orderId: {
  99. type: String,
  100. default: ''
  101. },
  102. currentReceipt: {
  103. type: Object,
  104. default: () => ({})
  105. }
  106. },
  107. data() {
  108. const sys = typeof uni !== 'undefined' ? uni.getSystemInfoSync() : { windowHeight: 600 }
  109. const sectionMinHeight = (sys.windowHeight || 600) - 100 + 'px' // 一屏高度,留出导航等
  110. return {
  111. activeIndex: 0,
  112. tabs: ['一', '二', '三', '四'],
  113. scrollTop: 0,
  114. scrollIntoView: '',
  115. sectionMinHeight,
  116. sectionTops: [0],
  117. scrollTopLock: null,
  118. // 吸住效果:仅在有意义的数值时控制 scroll-top,-1 表示不控制
  119. snapScrollTop: -1,
  120. scrollEndTimer: null,
  121. snapInProgress: false,
  122. // 阻止在非顶部时触发页面下拉刷新(由 touch 捕获)
  123. touchStartY: 0,
  124. // 表单数据
  125. formData: {
  126. formOne: {},
  127. formTwo: {},
  128. formThree: {},
  129. formFour: {}
  130. },
  131. pageThreeForm: {},
  132. fileIds: '',
  133. // 跟进记录
  134. followUpList: [],
  135. _measureTopsTimer: null
  136. }
  137. },
  138. mounted() {
  139. this.$nextTick(() => this.measureSectionTops())
  140. },
  141. watch: {
  142. orderDetail: {
  143. handler(newVal) {
  144. if (newVal && newVal.clueId) {
  145. this.loadFollowUpList()
  146. }
  147. },
  148. deep: true,
  149. immediate: true
  150. },
  151. // 仅切换订单/收单时重新测量高度,避免 updated 每次渲染都测导致 sectionTops 被错误覆盖
  152. orderId() {
  153. this.$nextTick(() => this.measureSectionTopsDelayed())
  154. },
  155. currentReceipt: {
  156. handler() {
  157. this.$nextTick(() => this.measureSectionTopsDelayed())
  158. },
  159. deep: true
  160. }
  161. },
  162. methods: {
  163. /**
  164. * 加载跟进记录
  165. */
  166. async loadFollowUpList() {
  167. try {
  168. const res = await uni.$u.api.getDuplicateOrderFollowListByClueId({
  169. clueId: this.orderDetail.clueId
  170. })
  171. const data = res.data || {}
  172. const followUpList = []
  173. // 按日期键升序合并,保证“最后一条”=“最新一条”,回显才能拿到最新
  174. const dates = Object.keys(data).filter(Boolean).sort()
  175. for (const key of dates) {
  176. const list = data[key] || []
  177. followUpList.push(...list)
  178. }
  179. // 再按 createTime 排一次,同一天内多条时也保证时间顺序
  180. followUpList.sort((a, b) => {
  181. const t1 = a.createTime || ''
  182. const t2 = b.createTime || ''
  183. return t1.localeCompare(t2)
  184. })
  185. this.followUpList = followUpList
  186. } catch (error) {
  187. console.error('获取跟进记录失败:', error)
  188. uni.$u.toast('获取跟进记录失败')
  189. }
  190. },
  191. /**
  192. * 延迟测量,避免子组件未渲染完时测到全 0
  193. */
  194. measureSectionTopsDelayed() {
  195. if (this._measureTopsTimer) clearTimeout(this._measureTopsTimer)
  196. this._measureTopsTimer = setTimeout(() => {
  197. this._measureTopsTimer = null
  198. this.measureSectionTops()
  199. }, 300)
  200. },
  201. /**
  202. * 测量各段顶部位置,用于根据 scrollTop 计算当前页
  203. */
  204. measureSectionTops() {
  205. const query = uni.createSelectorQuery().in(this)
  206. query
  207. .selectAll('.page-section')
  208. .fields({ size: true }, (res) => {
  209. if (!res || res.length < 2) return
  210. const tops = [0]
  211. for (let i = 0; i < res.length - 1; i++) {
  212. tops.push(tops[i] + (res[i].height || 0))
  213. }
  214. // 只有测量结果合理(递增)时才覆盖,避免把正确值覆盖成 [0,0,0,0]
  215. const valid = tops.length >= 2 && tops.every((t, i) => i === 0 || t > tops[i - 1])
  216. if (valid) this.sectionTops = tops
  217. })
  218. .exec()
  219. },
  220. /**
  221. * 滚动时更新当前页索引
  222. */
  223. onScroll(e) {
  224. const scrollTop = e.detail?.scrollTop ?? 0
  225. if (this.scrollTopLock !== null) return
  226. this.scrollTop = scrollTop
  227. // 吸附过程中不更新页索引、不启动“滚动结束”定时器,避免释放控制后误触二次吸附到第一页
  228. if (this.snapInProgress) return
  229. const tops = this.sectionTops
  230. if (tops.length) {
  231. let idx = 0
  232. for (let i = tops.length - 1; i >= 0; i--) {
  233. if (scrollTop >= tops[i] - 10) {
  234. idx = i
  235. break
  236. }
  237. }
  238. if (idx !== this.activeIndex) {
  239. this.activeIndex = idx
  240. this.tryRefreshPageThree(idx)
  241. }
  242. }
  243. if (this.scrollEndTimer) clearTimeout(this.scrollEndTimer)
  244. this.scrollEndTimer = setTimeout(() => this.doSnap(), 220)
  245. },
  246. /**
  247. * 滚动停止后吸附到当前所在页的起始位置(轮播吸住感)
  248. */
  249. doSnap() {
  250. this.scrollEndTimer = null
  251. const tops = this.sectionTops
  252. // 测量结果无效(非递增或只有一项)时不吸附,避免误吸回第一页
  253. if (!tops.length || tops.length < 2) return
  254. const increasing = tops.every((t, i) => i === 0 || t > tops[i - 1])
  255. if (!increasing) return
  256. const scrollTop = this.scrollTop
  257. let nearest = tops.length - 1
  258. for (let i = tops.length - 1; i >= 0; i--) {
  259. if (scrollTop >= tops[i] - 2) {
  260. nearest = i
  261. break
  262. }
  263. }
  264. const targetTop = tops[nearest]
  265. if (Math.abs(scrollTop - targetTop) < 2) {
  266. this.snapInProgress = false
  267. return
  268. }
  269. this.snapInProgress = true
  270. this.snapScrollTop = targetTop
  271. this.activeIndex = nearest
  272. this.tryRefreshPageThree(nearest)
  273. setTimeout(() => {
  274. this.snapScrollTop = -1
  275. // 释放后仍保持 snapInProgress 一段时间,避免“释放”触发的 scroll 再次触发 doSnap 吸到第一页
  276. setTimeout(() => {
  277. this.snapInProgress = false
  278. }, 280)
  279. }, 350)
  280. },
  281. tryRefreshPageThree(index) {
  282. if (index === 2 && this.$refs.pageThreeRef) {
  283. const ref = Array.isArray(this.$refs.pageThreeRef) ? this.$refs.pageThreeRef[0] : this.$refs.pageThreeRef
  284. if (ref && ref.refreshImageList) ref.refreshImageList()
  285. }
  286. },
  287. /**
  288. * 触摸开始:记录 Y,用于避免误触页面下拉刷新
  289. */
  290. onTouchStart(e) {
  291. this.touchStartY = e.touches && e.touches[0] ? e.touches[0].clientY : 0
  292. },
  293. /**
  294. * 触摸移动:在非全局顶部时用 catch 阻止事件冒泡到页面,减少触发下拉刷新
  295. */
  296. onTouchMove(e) {
  297. if (this.scrollTop > 20 && e.cancelable) {
  298. e.stopPropagation()
  299. }
  300. },
  301. /**
  302. * 处理下一步:滚动到下一页
  303. */
  304. handleNext({ nowPage, form }) {
  305. if (nowPage) {
  306. this.formData[nowPage] = form
  307. }
  308. const nextIndex = Math.min(this.activeIndex + 1, this.tabs.length - 1)
  309. this.scrollToSection(nextIndex)
  310. this.activeIndex = nextIndex
  311. if (nextIndex === 2 && this.$refs.pageThreeRef) {
  312. const ref = Array.isArray(this.$refs.pageThreeRef) ? this.$refs.pageThreeRef[0] : this.$refs.pageThreeRef
  313. if (ref && ref.refreshImageList) ref.refreshImageList()
  314. }
  315. },
  316. /**
  317. * 处理保存
  318. */
  319. handleNeedSave({ nowPage, form, fileIds }) {
  320. this.pageThreeForm = form
  321. this.fileIds = fileIds
  322. },
  323. /**
  324. * 处理确认支付
  325. */
  326. async handleConfirmPay() {
  327. try {
  328. const response = await uni.$u.api.saveOrderFileAndTransfer({
  329. id: this.orderId,
  330. clueId: this.orderDetail.clueId
  331. })
  332. uni.$u.toast(response.msg || '支付成功')
  333. } catch (error) {
  334. console.error('支付失败:', error)
  335. uni.$u.toast(`支付失败:${error}`)
  336. //支付失败回滚支付信息
  337. await uni.$u.api.updateClueOrderForm({
  338. id: this.orderDetail.id,
  339. paymentMethod: ''
  340. })
  341. }
  342. },
  343. /**
  344. * 处理确认入库
  345. */
  346. async handleConfirmWarehouse({ warehouseInfo }) {
  347. try {
  348. const params = {
  349. searchValue: this.orderDetail.searchValue,
  350. createBy: this.orderDetail.createBy,
  351. createTime: this.orderDetail.createTime,
  352. updateBy: this.orderDetail.updateBy,
  353. updateTime: this.orderDetail.updateTime,
  354. params: this.orderDetail.params,
  355. id: this.currentReceipt.id,
  356. sendFormId: this.orderId,
  357. clueId: this.orderDetail.clueId,
  358. item: warehouseInfo.item || '',
  359. code: warehouseInfo.codeStorage || '',
  360. phone: this.orderDetail.phone,
  361. tableFee: warehouseInfo.watchPrice || '',
  362. benefitFee: warehouseInfo.benefitFee || '',
  363. freight: warehouseInfo.freight || '',
  364. checkCodeFee: warehouseInfo.checkCodeFee || '',
  365. receiptRemark: `${warehouseInfo.remarks || ''};${warehouseInfo.uploadedImage || ''}`,
  366. repairAmount: warehouseInfo.repairAmount || '',
  367. grossPerformance: warehouseInfo.grossPerformance || '',
  368. expressOrderNo: warehouseInfo.expressOrderNo || '',
  369. fileIds: this.fileIds,
  370. customerServiceName: warehouseInfo.customerServiceName || '1',
  371. deptId: this.orderDetail.deptId,
  372. category: warehouseInfo.category || this.orderDetail.category,
  373. delFlag: this.orderDetail.delFlag,
  374. idCard: this.pageThreeForm.idNumber || '',
  375. paymentMethod: '小葫芦线上支付',
  376. bankCardNumber: this.pageThreeForm.bankAccount || '',
  377. bankName: this.pageThreeForm.bankName || '',
  378. customName: this.pageThreeForm.customName || ''
  379. }
  380. if (this.currentReceipt.id) {
  381. await uni.$u.api.updateReceiptForm(params)
  382. } else {
  383. await uni.$u.api.addReceiptForm(params)
  384. }
  385. uni.$u.toast('入库成功')
  386. } catch (error) {
  387. console.error('入库失败:', error)
  388. uni.$u.toast('入库失败')
  389. }
  390. },
  391. /**
  392. * 滚动到指定页(用于点击导航、下一步)
  393. */
  394. scrollToSection(index) {
  395. if (this.scrollEndTimer) clearTimeout(this.scrollEndTimer)
  396. this.scrollEndTimer = null
  397. this.snapInProgress = true
  398. this.scrollIntoView = 'page' + index
  399. this.$nextTick(() => {
  400. setTimeout(() => {
  401. this.scrollIntoView = ''
  402. setTimeout(() => {
  403. this.snapInProgress = false
  404. }, 100)
  405. }, 300)
  406. })
  407. this.activeIndex = index
  408. this.tryRefreshPageThree(index)
  409. },
  410. /**
  411. * 处理标签点击
  412. */
  413. handleTabClick(index) {
  414. this.scrollToSection(index)
  415. },
  416. /**
  417. * 更新 fileIds
  418. */
  419. handleUpdateFileIds(fileIds) {
  420. if (this.currentReceipt) {
  421. this.$set(this.currentReceipt, 'fileIds', fileIds)
  422. this.fileIds = fileIds
  423. }
  424. }
  425. }
  426. }
  427. </script>
  428. <style scoped lang="scss">
  429. .order-detail-view {
  430. position: relative;
  431. padding: 20rpx;
  432. height: calc(100vh - 200rpx);
  433. min-height: calc(100vh - 200rpx);
  434. }
  435. .page-scroll-view {
  436. width: 100%;
  437. height: 100%;
  438. scroll-snap-type: y mandatory;
  439. -webkit-overflow-scrolling: touch;
  440. }
  441. .page-section {
  442. width: 100%;
  443. box-sizing: border-box;
  444. scroll-snap-align: start;
  445. scroll-snap-stop: always;
  446. }
  447. .page-item {
  448. width: 100%;
  449. min-height: 100%;
  450. box-sizing: border-box;
  451. }
  452. .page-navigation {
  453. position: fixed;
  454. right: 20rpx;
  455. top: 40%;
  456. display: flex;
  457. flex-direction: column;
  458. align-items: center;
  459. justify-content: center;
  460. list-style: none;
  461. color: #000;
  462. font-size: 20rpx;
  463. font-weight: 800;
  464. z-index: 100;
  465. li {
  466. opacity: 0.7;
  467. display: flex;
  468. align-items: center;
  469. justify-content: center;
  470. background-color: #fff;
  471. border-radius: 50%;
  472. width: 70rpx;
  473. height: 70rpx;
  474. line-height: 70rpx;
  475. text-align: center;
  476. margin-bottom: 20rpx;
  477. transition: all 0.3s ease-in-out;
  478. font-weight: 800;
  479. box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
  480. cursor: pointer;
  481. &.active {
  482. color: #fff;
  483. opacity: 1;
  484. background-color: rgb(37 99 235 / 1);
  485. }
  486. &:hover {
  487. opacity: 0.9;
  488. transform: scale(1.05);
  489. }
  490. }
  491. }
  492. </style>