OrderDetailView.vue 14 KB

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