OrderDetailView.vue 14 KB

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