Selaa lähdekoodia

修改接单中心详情页为滑动,并增加原代码备份

Yannay 2 viikkoa sitten
vanhempi
commit
eac9fa5cf7

+ 1 - 1
pages.json

@@ -261,7 +261,7 @@
261 261
 			"path": "pages/orderDetailRefactored/index",
262 262
 			"style": {
263 263
 				"navigationBarTitleText": "",
264
-				"enablePullDownRefresh": true,
264
+				"enablePullDownRefresh": false,
265 265
 				"navigationStyle": "custom"
266 266
 			}
267 267
 		},

+ 244 - 37
pages/orderDetailRefactored/components/OrderDetailView.vue

@@ -1,31 +1,73 @@
1 1
 <template>
2
-  <view class="order-detail-view">
3
-    <!-- 页面切换 -->
4
-    <view class="page-item" v-show="activeIndex === 0">
5
-      <PageOne :order-detail="orderDetail" :order-id="orderId" :current-receipt="currentReceipt" @next="handleNext" />
6
-    </view>
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>
7 62
 
8
-    <view class="page-item" v-show="activeIndex === 1">
9
-      <PageTwo :order-detail="orderDetail" :order-id="orderId" :current-receipt="currentReceipt"
10
-        :follow-up-list="followUpList" @next="handleNext" @update-file-ids="handleUpdateFileIds"
11
-        @price-updated="$emit('price-updated')" />
12
-    </view>
13
-
14
-    <view class="page-item" v-show="activeIndex === 2">
15
-      <PageThree :order-detail="orderDetail" :order-id="orderId" :current-receipt="currentReceipt" @next="handleNext"
16
-        @save="handleNeedSave" @confirm-pay="handleConfirmPay" @update-file-ids="handleUpdateFileIds"
17
-        @price-updated="$emit('price-updated')" ref="pageThreeRef" />
18
-    </view>
19
-
20
-    <view class="page-item" v-show="activeIndex === 3">
21
-      <PageFour :order-detail="orderDetail" :current-receipt="currentReceipt" @next="handleNext"
22
-        @confirm-warehouse="handleConfirmWarehouse" />
23
-    </view>
24
-
25
-    <!-- 页面导航 -->
63
+    <!-- 页面导航(点击仍可切换) -->
26 64
     <ul class="page-navigation">
27
-      <li v-for="(tab, index) in tabs" :key="index" :class="{ 'active': activeIndex === index }"
28
-        @click="handleTabClick(index)">
65
+      <li
66
+        v-for="(tab, index) in tabs"
67
+        :key="index"
68
+        :class="{ active: activeIndex === index }"
69
+        @click="handleTabClick(index)"
70
+      >
29 71
         {{ tab }}
30 72
       </li>
31 73
     </ul>
@@ -65,9 +107,22 @@ export default {
65 107
     }
66 108
   },
67 109
   data() {
110
+    const sys = typeof uni !== 'undefined' ? uni.getSystemInfoSync() : { windowHeight: 600 }
111
+    const sectionMinHeight = (sys.windowHeight || 600) - 100 + 'px' // 一屏高度,留出导航等
68 112
     return {
69 113
       activeIndex: 0,
70 114
       tabs: ['一', '二', '三', '四'],
115
+      scrollTop: 0,
116
+      scrollIntoView: '',
117
+      sectionMinHeight,
118
+      sectionTops: [0],
119
+      scrollTopLock: null,
120
+      // 吸住效果:仅在有意义的数值时控制 scroll-top,-1 表示不控制
121
+      snapScrollTop: -1,
122
+      scrollEndTimer: null,
123
+      snapInProgress: false,
124
+      // 阻止在非顶部时触发页面下拉刷新(由 touch 捕获)
125
+      touchStartY: 0,
71 126
       // 表单数据
72 127
       formData: {
73 128
         formOne: {},
@@ -81,6 +136,12 @@ export default {
81 136
       followUpList: []
82 137
     }
83 138
   },
139
+  mounted() {
140
+    this.$nextTick(() => this.measureSectionTops())
141
+  },
142
+  updated() {
143
+    this.$nextTick(() => this.measureSectionTops())
144
+  },
84 145
   watch: {
85 146
     orderDetail: {
86 147
       handler(newVal) {
@@ -103,9 +164,18 @@ export default {
103 164
         })
104 165
         const data = res.data || {}
105 166
         const followUpList = []
106
-        for (const key in data) {
107
-          followUpList.push(...(data[key] || []))
167
+        // 按日期键升序合并,保证“最后一条”=“最新一条”,回显才能拿到最新
168
+        const dates = Object.keys(data).filter(Boolean).sort()
169
+        for (const key of dates) {
170
+          const list = data[key] || []
171
+          followUpList.push(...list)
108 172
         }
173
+        // 再按 createTime 排一次,同一天内多条时也保证时间顺序
174
+        followUpList.sort((a, b) => {
175
+          const t1 = a.createTime || ''
176
+          const t2 = b.createTime || ''
177
+          return t1.localeCompare(t2)
178
+        })
109 179
         this.followUpList = followUpList
110 180
       } catch (error) {
111 181
         console.error('获取跟进记录失败:', error)
@@ -114,16 +184,119 @@ export default {
114 184
     },
115 185
 
116 186
     /**
117
-     * 处理下一步
187
+     * 测量各段顶部位置,用于根据 scrollTop 计算当前页
188
+     */
189
+    measureSectionTops() {
190
+      const query = uni.createSelectorQuery().in(this)
191
+      query
192
+        .selectAll('.page-section')
193
+        .fields({ size: true }, (res) => {
194
+          if (!res || !res.length) return
195
+          const tops = [0]
196
+          for (let i = 0; i < res.length - 1; i++) {
197
+            tops.push(tops[i] + (res[i].height || 0))
198
+          }
199
+          this.sectionTops = tops
200
+        })
201
+        .exec()
202
+    },
203
+
204
+    /**
205
+     * 滚动时更新当前页索引
206
+     */
207
+    onScroll(e) {
208
+      const scrollTop = e.detail?.scrollTop ?? 0
209
+      if (this.scrollTopLock !== null) return
210
+      this.scrollTop = scrollTop
211
+      // 吸附过程中不更新页索引、不启动“滚动结束”定时器,避免释放控制后误触二次吸附到第一页
212
+      if (this.snapInProgress) return
213
+      const tops = this.sectionTops
214
+      if (tops.length) {
215
+        let idx = 0
216
+        for (let i = tops.length - 1; i >= 0; i--) {
217
+          if (scrollTop >= tops[i] - 10) {
218
+            idx = i
219
+            break
220
+          }
221
+        }
222
+        if (idx !== this.activeIndex) {
223
+          this.activeIndex = idx
224
+          this.tryRefreshPageThree(idx)
225
+        }
226
+      }
227
+      if (this.scrollEndTimer) clearTimeout(this.scrollEndTimer)
228
+      this.scrollEndTimer = setTimeout(() => this.doSnap(), 150)
229
+    },
230
+
231
+    /**
232
+     * 滚动停止后吸附到当前所在页的起始位置(轮播吸住感)
233
+     */
234
+    doSnap() {
235
+      this.scrollEndTimer = null
236
+      const tops = this.sectionTops
237
+      if (!tops.length) return
238
+      const scrollTop = this.scrollTop
239
+      let nearest = tops.length - 1
240
+      for (let i = tops.length - 1; i >= 0; i--) {
241
+        if (scrollTop >= tops[i] - 2) {
242
+          nearest = i
243
+          break
244
+        }
245
+      }
246
+      const targetTop = tops[nearest]
247
+      if (Math.abs(scrollTop - targetTop) < 2) {
248
+        this.snapInProgress = false
249
+        return
250
+      }
251
+      this.snapInProgress = true
252
+      this.snapScrollTop = targetTop
253
+      this.activeIndex = nearest
254
+      this.tryRefreshPageThree(nearest)
255
+      setTimeout(() => {
256
+        this.snapScrollTop = -1
257
+        // 释放后仍保持 snapInProgress 一段时间,避免“释放”触发的 scroll 事件再次触发 doSnap 吸到第一页
258
+        setTimeout(() => {
259
+          this.snapInProgress = false
260
+        }, 200)
261
+      }, 350)
262
+    },
263
+
264
+    tryRefreshPageThree(index) {
265
+      if (index === 2 && this.$refs.pageThreeRef) {
266
+        const ref = Array.isArray(this.$refs.pageThreeRef) ? this.$refs.pageThreeRef[0] : this.$refs.pageThreeRef
267
+        if (ref && ref.refreshImageList) ref.refreshImageList()
268
+      }
269
+    },
270
+
271
+    /**
272
+     * 触摸开始:记录 Y,用于避免误触页面下拉刷新
273
+     */
274
+    onTouchStart(e) {
275
+      this.touchStartY = e.touches && e.touches[0] ? e.touches[0].clientY : 0
276
+    },
277
+
278
+    /**
279
+     * 触摸移动:在非全局顶部时用 catch 阻止事件冒泡到页面,减少触发下拉刷新
280
+     */
281
+    onTouchMove(e) {
282
+      if (this.scrollTop > 20 && e.cancelable) {
283
+        e.stopPropagation()
284
+      }
285
+    },
286
+
287
+    /**
288
+     * 处理下一步:滚动到下一页
118 289
      */
119 290
     handleNext({ nowPage, form }) {
120
-      this.activeIndex++
121 291
       if (nowPage) {
122 292
         this.formData[nowPage] = form
123 293
       }
124
-      // 当切换到第三页时,更新第三页的图片列表
125
-      if (nowPage === 'formTwo' && this.$refs.pageThreeRef) {
126
-        this.$refs.pageThreeRef.refreshImageList()
294
+      const nextIndex = Math.min(this.activeIndex + 1, this.tabs.length - 1)
295
+      this.scrollToSection(nextIndex)
296
+      this.activeIndex = nextIndex
297
+      if (nextIndex === 2 && this.$refs.pageThreeRef) {
298
+        const ref = Array.isArray(this.$refs.pageThreeRef) ? this.$refs.pageThreeRef[0] : this.$refs.pageThreeRef
299
+        if (ref && ref.refreshImageList) ref.refreshImageList()
127 300
       }
128 301
     },
129 302
 
@@ -208,14 +381,30 @@ export default {
208 381
     },
209 382
 
210 383
     /**
384
+     * 滚动到指定页(用于点击导航、下一步)
385
+     */
386
+    scrollToSection(index) {
387
+      if (this.scrollEndTimer) clearTimeout(this.scrollEndTimer)
388
+      this.scrollEndTimer = null
389
+      this.snapInProgress = true
390
+      this.scrollIntoView = 'page' + index
391
+      this.$nextTick(() => {
392
+        setTimeout(() => {
393
+          this.scrollIntoView = ''
394
+          setTimeout(() => {
395
+            this.snapInProgress = false
396
+          }, 100)
397
+        }, 300)
398
+      })
399
+      this.activeIndex = index
400
+      this.tryRefreshPageThree(index)
401
+    },
402
+
403
+    /**
211 404
      * 处理标签点击
212 405
      */
213 406
     handleTabClick(index) {
214
-      this.activeIndex = index
215
-      // 切换到第三页时刷新图片列表
216
-      if (index === 2 && this.$refs.pageThreeRef) {
217
-        this.$refs.pageThreeRef.refreshImageList()
218
-      }
407
+      this.scrollToSection(index)
219 408
     },
220 409
 
221 410
     /**
@@ -233,12 +422,30 @@ export default {
233 422
 
234 423
 <style scoped lang="scss">
235 424
 .order-detail-view {
425
+  position: relative;
236 426
   padding: 20rpx;
427
+  height: calc(100vh - 200rpx);
237 428
   min-height: calc(100vh - 200rpx);
238 429
 }
239 430
 
431
+.page-scroll-view {
432
+  width: 100%;
433
+  height: 100%;
434
+  scroll-snap-type: y mandatory;
435
+  -webkit-overflow-scrolling: touch;
436
+}
437
+
438
+.page-section {
439
+  width: 100%;
440
+  box-sizing: border-box;
441
+  scroll-snap-align: start;
442
+  scroll-snap-stop: always;
443
+}
444
+
240 445
 .page-item {
241 446
   width: 100%;
447
+  min-height: 100%;
448
+  box-sizing: border-box;
242 449
 }
243 450
 
244 451
 .page-navigation {

+ 50 - 10
pages/orderDetailRefactored/components/PageFour.vue

@@ -15,7 +15,8 @@
15 15
         <u-row class="info-row" justify="space-between">
16 16
           <u-col span="5.8">
17 17
             <u-form-item label="收单物品" prop="item">
18
-              <u--input v-model="warehouseInfo.item" placeholder="请输入收单物品" class="info-input" />
18
+              <u--input v-model="warehouseInfo.item" placeholder="请输入收单物品" class="info-input"
19
+                @blur="saveWarehouseInfoOnBlur" />
19 20
             </u-form-item>
20 21
           </u-col>
21 22
           <u-col span="5.8">
@@ -62,12 +63,13 @@
62 63
           <u-col span="4.5">
63 64
             <u-form-item label="编码" prop="codeStorage">
64 65
               <u--input v-model="warehouseInfo.codeStorage" placeholder="请输入编码" class="info-input"
65
-                :disabled="warehouseInfo.needCheckCode === '2'" />
66
+                :disabled="warehouseInfo.needCheckCode === '2'" @blur="saveWarehouseInfoOnBlur" />
66 67
             </u-form-item>
67 68
           </u-col>
68 69
           <u-col span="4.5">
69 70
             <u-form-item label="快递单号" prop="expressOrderNo">
70
-              <u--input v-model="warehouseInfo.expressOrderNo" placeholder="请输入快递单号" class="info-input" />
71
+              <u--input v-model="warehouseInfo.expressOrderNo" placeholder="请输入快递单号" class="info-input"
72
+                @blur="saveWarehouseInfoOnBlur" />
71 73
             </u-form-item>
72 74
           </u-col>
73 75
           <u-col span="2">
@@ -85,13 +87,14 @@
85 87
         <u-row class="info-row" justify="space-between">
86 88
           <u-col span="5.8">
87 89
             <u-form-item label="表款">
88
-              <u--input v-model="warehouseInfo.watchPrice" placeholder="请输入表款" class="info-input" type="number" />
90
+              <u--input v-model="warehouseInfo.watchPrice" placeholder="请输入表款" class="info-input" type="number"
91
+                @blur="saveWarehouseInfoOnBlur" />
89 92
             </u-form-item>
90 93
           </u-col>
91 94
           <u-col span="5.8">
92 95
             <u-form-item label="查码费">
93 96
               <u--input v-model="warehouseInfo.checkCodeFee" placeholder="请输入查码费" class="info-input" type="number"
94
-                :disabled="warehouseInfo.needCheckCode === '2'" />
97
+                :disabled="warehouseInfo.needCheckCode === '2'" @blur="saveWarehouseInfoOnBlur" />
95 98
             </u-form-item>
96 99
           </u-col>
97 100
         </u-row>
@@ -114,20 +117,22 @@
114 117
         <u-row class="info-row" justify="space-between">
115 118
           <u-col span="5.8">
116 119
             <u-form-item label="维修金额">
117
-              <u--input v-model="warehouseInfo.repairAmount" placeholder="请输入维修金额" class="info-input" type="number" />
120
+              <u--input v-model="warehouseInfo.repairAmount" placeholder="请输入维修金额" class="info-input" type="number"
121
+                @blur="saveWarehouseInfoOnBlur" />
118 122
             </u-form-item>
119 123
           </u-col>
120 124
           <u-col span="5.8">
121 125
             <u-form-item label="分单比例(0~100)">
122 126
               <u--input v-model="warehouseInfo.splitRatio" placeholder="请输入分单比例(0~100)" class="info-input"
123
-                type="number" />
127
+                type="number" @blur="saveWarehouseInfoOnBlur" />
124 128
             </u-form-item>
125 129
           </u-col>
126 130
         </u-row>
127 131
         <u-row class="info-row" justify="space-between">
128 132
           <u-col span="5.8">
129 133
             <u-form-item label="型号" required prop="model"> 
130
-              <u--input v-model="warehouseInfo.model" placeholder="请输入型号" class="info-input"/>
134
+              <u--input v-model="warehouseInfo.model" placeholder="请输入型号" class="info-input"
135
+                @blur="saveWarehouseInfoOnBlur" />
131 136
             </u-form-item>
132 137
           </u-col>
133 138
         </u-row>
@@ -173,7 +178,7 @@
173 178
           <u-col span="12">
174 179
             <u-form-item label="收单备注">
175 180
               <u--textarea v-model="warehouseInfo.remarks" placeholder="请输入收单备注" class="info-textarea"
176
-                confirmType="done" rows="4" />
181
+                confirmType="done" rows="4" @blur="saveWarehouseInfoOnBlur" />
177 182
             </u-form-item>
178 183
           </u-col>
179 184
         </u-row>
@@ -461,7 +466,38 @@ export default {
461 466
     },
462 467
 
463 468
     /**
464
-     * 选择物流图片
469
+     * 入库信息失焦 / 物流图片变更后保存
470
+     */
471
+    async saveWarehouseInfoOnBlur() {
472
+      if (!this.currentReceipt || !this.currentReceipt.id) return
473
+      try {
474
+        await uni.$u.api.updateReceiptForm({
475
+          id: this.currentReceipt.id,
476
+          code: this.warehouseInfo.codeStorage || '',
477
+          expressOrderNo: this.warehouseInfo.expressOrderNo || '',
478
+          item: this.warehouseInfo.item || '',
479
+          checkCodeFee: this.warehouseInfo.checkCodeFee || '',
480
+          tableFee: this.warehouseInfo.watchPrice || '',
481
+          benefitFee: this.warehouseInfo.benefitFee || '',
482
+          freight: this.warehouseInfo.freight || '',
483
+          repairAmount: this.warehouseInfo.repairAmount || '',
484
+          grossPerformance: this.computedGrossPerformance || '',
485
+          performance: this.computedPerformance || '',
486
+          splitRatio: this.warehouseInfo.splitRatio || '',
487
+          receiptRemark: `${this.warehouseInfo.remarks || ''};${this.warehouseInfo.uploadedImage || ''}`,
488
+          customerServiceName: this.warehouseInfo.customerServiceName || '',
489
+          category: this.warehouseInfo.category || '',
490
+          needCheckCode: this.warehouseInfo.needCheckCode || '',
491
+          totalCost: this.computedTotalCost || '',
492
+          model: this.warehouseInfo.model || ''
493
+        })
494
+      } catch (e) {
495
+        console.error('保存入库信息失败:', e)
496
+      }
497
+    },
498
+
499
+    /**
500
+     * 选择物流图片(选完后即保存)
465 501
      */
466 502
     async selectImage() {
467 503
       try {
@@ -474,6 +510,7 @@ export default {
474 510
         const rep = await uni.$u.api.uploadFile(tempFilePath)
475 511
         if (rep.code == 200) {
476 512
           this.warehouseInfo.uploadedImage = rep.data.url
513
+          await this.saveWarehouseInfoOnBlur()
477 514
         }
478 515
       } catch (error) {
479 516
         console.error('上传图片失败:', error)
@@ -704,6 +741,7 @@ export default {
704 741
       this.warehouseInfo.customerServiceNameLabel = value[0].label
705 742
       this.warehouseInfo.customerServiceName = value[0].value
706 743
       this.showCustomerServicePicker = false
744
+      this.saveWarehouseInfoOnBlur()
707 745
     },
708 746
 
709 747
     /**
@@ -720,6 +758,7 @@ export default {
720 758
       this.warehouseInfo.categoryLabel = value[0].label
721 759
       this.warehouseInfo.category = value[0].value
722 760
       this.showCategoryPicker = false
761
+      this.saveWarehouseInfoOnBlur()
723 762
     },
724 763
 
725 764
     /**
@@ -741,6 +780,7 @@ export default {
741 780
         this.warehouseInfo.checkCodeFee = ''
742 781
       }
743 782
       this.showNeedCheckCodePicker = false
783
+      this.saveWarehouseInfoOnBlur()
744 784
     },
745 785
 
746 786
     /**

+ 2 - 2
pages/orderDetailRefactored/components/PageOne.vue

@@ -115,9 +115,9 @@
115 115
 
116 116
     <!-- 下一步按钮 -->
117 117
     <view class="space-block"></view>
118
-    <u-button class="next-btn" @click="handleNext" type="primary" size="middle">
118
+    <!-- <u-button class="next-btn" @click="handleNext" type="primary" size="middle">
119 119
       下一步
120
-    </u-button>
120
+    </u-button> -->
121 121
   </view>
122 122
 </template>
123 123
 

+ 38 - 13
pages/orderDetailRefactored/components/PageThree.vue

@@ -19,7 +19,7 @@
19 19
         <u-col span="12">
20 20
           <view class="info-label">{{ bankAccountLabel }}</view>
21 21
           <u-input v-model="paymentInfo.bankAccount" :placeholder="bankAccountPlaceholder" class="info-input"
22
-            :type="isAlipayPayment ? 'text' : 'number'" @input="handleBankAccountInput" />
22
+            :type="isAlipayPayment ? 'text' : 'number'" @input="handleBankAccountInput" @blur="handleBankAccountBlur" />
23 23
         </u-col>
24 24
       </u-row>
25 25
       <u-row class="info-row">
@@ -119,9 +119,9 @@
119 119
     </view>
120 120
 
121 121
     <!-- 下一步按钮 -->
122
-    <u-button class="next-btn" @click="handleNext" type="primary" size="middle">
122
+    <!-- <u-button class="next-btn" @click="handleNext" type="primary" size="middle">
123 123
       下一步
124
-    </u-button>
124
+    </u-button> -->
125 125
 
126 126
     <!-- 未收评级模态窗 -->
127 127
     <u-modal showCancelButton showConfirmButton @confirm="confirmUnpaid" @cancel="onUnpaidModalCancel"
@@ -385,39 +385,64 @@ export default {
385 385
     },
386 386
 
387 387
     /**
388
-     * 开户人输入处理 - 只允许中文
388
+     * 失焦后保存支付信息(开户人/银行名称/账号/身份证)
389
+     */
390
+    async savePaymentInfoOnBlur() {
391
+      if (!this.currentReceipt || !this.currentReceipt.id) return
392
+      try {
393
+        await uni.$u.api.updateReceiptForm({
394
+          id: this.currentReceipt.id,
395
+          sendFormId: this.currentReceipt.sendFormId || this.orderDetail.id,
396
+          customName: this.paymentInfo.customName || '',
397
+          bankName: this.paymentInfo.bankName || '',
398
+          bankCardNumber: this.paymentInfo.bankAccount || '',
399
+          idCard: this.paymentInfo.idNumber || ''
400
+        })
401
+      } catch (e) {
402
+        console.error('保存支付信息失败:', e)
403
+      }
404
+    },
405
+
406
+    /**
407
+     * 开户人输入处理 - 只允许中文,失焦保存
389 408
      */
390 409
     handleCustomNameInput(value) {
391
-      // 只保留中文字符(包括中文标点)
392 410
       const chineseReg = /[^\u4e00-\u9fa5]/g
393
-      this.paymentInfo.customName = String(value || '').replace(chineseReg, '')
411
+      this.paymentInfo.customName = String(value ?? this.paymentInfo.customName ?? '').replace(chineseReg, '')
412
+      this.savePaymentInfoOnBlur()
394 413
     },
395 414
 
396 415
     /**
397
-     * 银行名称输入处理 - 只允许中文
416
+     * 银行名称输入处理 - 只允许中文,失焦保存
398 417
      */
399 418
     handleBankNameInput(value) {
400
-      // 只保留中文字符(包括中文标点)
401 419
       const chineseReg = /[^\u4e00-\u9fa5]/g
402
-      this.paymentInfo.bankName = String(value || '').replace(chineseReg, '')
420
+      this.paymentInfo.bankName = String(value ?? this.paymentInfo.bankName ?? '').replace(chineseReg, '')
421
+      this.savePaymentInfoOnBlur()
403 422
     },
404 423
 
405 424
     /**
406 425
      * 银行账号输入处理 - 只允许数字
407 426
      */
408 427
     handleBankAccountInput(value) {
409
-      // 只保留数字
410 428
       const numberReg = /[^\d]/g
411 429
       this.paymentInfo.bankAccount = String(value || '').replace(numberReg, '')
412 430
     },
413 431
 
414 432
     /**
415
-     * 身份证号输入处理 - 允许数字和X
433
+     * 银行账号失焦保存
434
+     */
435
+    handleBankAccountBlur() {
436
+      this.savePaymentInfoOnBlur()
437
+    },
438
+
439
+    /**
440
+     * 身份证号输入处理 - 允许数字和X,失焦保存
416 441
      */
417 442
     handleIdNumberInput(value) {
418
-      // 只保留数字和X
419 443
       const reg = /[^\dxX]/g
420
-      this.paymentInfo.idNumber = String(value || '').replace(reg, '')
444
+      this.paymentInfo.idNumber = String(value ?? this.paymentInfo.idNumber ?? '').replace(reg, '')
445
+      this.savePaymentInfoOnBlur()
421 446
     },
422 447
 
423 448
     /**

+ 86 - 27
pages/orderDetailRefactored/components/PageTwo.vue

@@ -27,7 +27,8 @@
27 27
               v-if="selectedCheckbox.includes('contactMaster')" 
28 28
               v-model="formData.contactPhone"
29 29
               placeholder="请输入师傅手机号" 
30
-              class="checklist-input" 
30
+              class="checklist-input"
31
+              @blur="saveContactMaster"
31 32
             />
32 33
           </view>
33 34
 
@@ -43,7 +44,8 @@
43 44
               type="textarea" 
44 45
               placeholder="请输入拍图技巧" 
45 46
               rows="3" 
46
-              class="checklist-textarea" 
47
+              class="checklist-textarea"
48
+              @blur="savePhotoTips"
47 49
             />
48 50
             <view v-if="selectedCheckbox.includes('photoTips')" class="upload-btn-container">
49 51
               <view class="upload-btn" @click="handleUpload('photoTips')">
@@ -78,7 +80,8 @@
78 80
               type="textarea" 
79 81
               placeholder="请输入备注信息" 
80 82
               rows="3" 
81
-              class="checklist-textarea" 
83
+              class="checklist-textarea"
84
+              @blur="saveFaceToFace"
82 85
             />
83 86
             <view v-if="selectedCheckbox.includes('faceToFace')" class="upload-btn-container">
84 87
               <view class="upload-btn" @click="handleUpload('faceToFace')">
@@ -170,7 +173,7 @@
170 173
       </view>
171 174
     </view>
172 175
 
173
-    <!-- 下一步按钮 -->
176
+    <!-- 下一步按钮
174 177
     <u-button 
175 178
       class="next-btn" 
176 179
       @click="handleNext" 
@@ -178,7 +181,7 @@
178 181
       size="middle"
179 182
     >
180 183
       下一步
181
-    </u-button>
184
+    </u-button> -->
182 185
   </view>
183 186
 </template>
184 187
 
@@ -284,32 +287,36 @@ export default {
284 287
     },
285 288
 
286 289
     /**
287
-     * 检查跟进内容
290
+     * 检查跟进内容:用最新一条记录回显(支持再次进入时回显)
288 291
      */
289 292
     checkFollowUpContent(followUpList) {
293
+      if (!followUpList || followUpList.length === 0) return
290 294
       const contentList = followUpList.map(item => item.content || '')
291
-      
295
+      // 按顺序遍历,后面的覆盖前面的,得到每类最新一条
296
+      let lastContactMaster = null
297
+      let lastPhotoTips = null
298
+      let lastFaceToFace = null
292 299
       contentList.forEach(item => {
293
-        if (item.includes('联系师傅') && !this.selectedCheckbox.includes('contactMaster')) {
294
-          this.selectedCheckbox.push('contactMaster')
295
-          const phone = item.split(';')[1] || ''
296
-          this.formData.contactPhone = phone
297
-        }
298
-        if (item.includes('师傅拍图技巧') && !this.selectedCheckbox.includes('photoTips')) {
299
-          this.selectedCheckbox.push('photoTips')
300
-          const tips = item.split(';')[1] || ''
301
-          this.formData.photoTips = tips
302
-          const urls = item.split(';')[2] || ''
303
-          this.photoTipsImages = urls.split(',').filter(url => url.trim())
304
-        }
305
-        if (item.includes('到达客户面对面') && !this.selectedCheckbox.includes('faceToFace')) {
306
-          this.selectedCheckbox.push('faceToFace')
307
-          const notes = item.split(';')[1] || ''
308
-          this.formData.faceToFaceNotes = notes
309
-          const urls = item.split(';')[2] || ''
310
-          this.faceToFaceImages = urls.split(',').filter(url => url.trim())
311
-        }
300
+        if (item.includes('联系师傅')) lastContactMaster = item
301
+        if (item.includes('师傅拍图技巧')) lastPhotoTips = item
302
+        if (item.includes('到达客户面对面')) lastFaceToFace = item
312 303
       })
304
+      if (lastContactMaster) {
305
+        if (!this.selectedCheckbox.includes('contactMaster')) this.selectedCheckbox.push('contactMaster')
306
+        this.formData.contactPhone = (lastContactMaster.split(';')[1] || '').trim()
307
+      }
308
+      if (lastPhotoTips) {
309
+        if (!this.selectedCheckbox.includes('photoTips')) this.selectedCheckbox.push('photoTips')
310
+        this.formData.photoTips = (lastPhotoTips.split(';')[1] || '').trim()
311
+        const urls = (lastPhotoTips.split(';')[2] || '').split(',').filter(url => url.trim())
312
+        this.photoTipsImages = urls
313
+      }
314
+      if (lastFaceToFace) {
315
+        if (!this.selectedCheckbox.includes('faceToFace')) this.selectedCheckbox.push('faceToFace')
316
+        this.formData.faceToFaceNotes = (lastFaceToFace.split(';')[1] || '').trim()
317
+        const urls = (lastFaceToFace.split(';')[2] || '').split(',').filter(url => url.trim())
318
+        this.faceToFaceImages = urls
319
+      }
313 320
     },
314 321
 
315 322
     /**
@@ -345,7 +352,7 @@ export default {
345 352
     },
346 353
 
347 354
     /**
348
-     * 上传图片(跟进清单)
355
+     * 上传图片(跟进清单):有新图片即保存
349 356
      */
350 357
     async handleUpload(field) {
351 358
       try {
@@ -355,8 +362,10 @@ export default {
355 362
         
356 363
         if (field === 'photoTips') {
357 364
           this.photoTipsImages = [...this.photoTipsImages, ...urls]
365
+          await this.savePhotoTips()
358 366
         } else if (field === 'faceToFace') {
359 367
           this.faceToFaceImages = [...this.faceToFaceImages, ...urls]
368
+          await this.saveFaceToFace()
360 369
         }
361 370
       } catch (error) {
362 371
         console.error('上传失败:', error)
@@ -365,6 +374,56 @@ export default {
365 374
     },
366 375
 
367 376
     /**
377
+     * 失焦/有图时保存:联系师傅
378
+     */
379
+    async saveContactMaster() {
380
+      if (!this.selectedCheckbox.includes('contactMaster')) return
381
+      try {
382
+        await uni.$u.api.addOrderFollow({
383
+          orderId: this.orderId,
384
+          content: `联系师傅;${this.formData.contactPhone || ''}`
385
+        })
386
+        this.$emit('follow-saved')
387
+      } catch (e) {
388
+        console.error('保存联系师傅失败:', e)
389
+      }
390
+    },
391
+
392
+    /**
393
+     * 失焦/有图时保存:师傅拍图技巧
394
+     */
395
+    async savePhotoTips() {
396
+      if (!this.selectedCheckbox.includes('photoTips')) return
397
+      try {
398
+        const urls = (this.photoTipsImages || []).join(',')
399
+        await uni.$u.api.addOrderFollow({
400
+          orderId: this.orderId,
401
+          content: `师傅拍图技巧;${this.formData.photoTips || ''};${urls}`
402
+        })
403
+        this.$emit('follow-saved')
404
+      } catch (e) {
405
+        console.error('保存师傅拍图技巧失败:', e)
406
+      }
407
+    },
408
+
409
+    /**
410
+     * 失焦/有图时保存:到达客户面对面
411
+     */
412
+    async saveFaceToFace() {
413
+      if (!this.selectedCheckbox.includes('faceToFace')) return
414
+      try {
415
+        const urls = (this.faceToFaceImages || []).join(',')
416
+        await uni.$u.api.addOrderFollow({
417
+          orderId: this.orderId,
418
+          content: `到达客户面对面;${this.formData.faceToFaceNotes || ''};${urls}`
419
+        })
420
+        this.$emit('follow-saved')
421
+      } catch (e) {
422
+        console.error('保存到达客户面对面失败:', e)
423
+      }
424
+    },
425
+
426
+    /**
368 427
      * 上传细节图
369 428
      */
370 429
     async handleUploadImage() {

+ 130 - 0
pages/orderDetailRefactoredOld/components/CustomModal.vue

@@ -0,0 +1,130 @@
1
+<template>
2
+  <u-modal 
3
+    :show="visible" 
4
+    :show-cancel-button="false" 
5
+    :show-confirm-button="false"
6
+  >
7
+    <view class="modal-content">
8
+      <view class="modal-header">
9
+        <text class="modal-title">{{ title }}</text>
10
+      </view>
11
+      <view class="modal-body">
12
+        <u-input 
13
+          v-model="localValue" 
14
+          :placeholder="placeholder" 
15
+          class="modal-input"
16
+        />
17
+      </view>
18
+      <view class="modal-footer">
19
+        <text @click="handleCancel" class="btn cancel-btn">取消</text>
20
+        <text @click="handleConfirm" class="btn confirm-btn">确定</text>
21
+      </view>
22
+    </view>
23
+  </u-modal>
24
+</template>
25
+
26
+<script>
27
+export default {
28
+  name: 'CustomModal',
29
+  props: {
30
+    visible: {
31
+      type: Boolean,
32
+      default: false
33
+    },
34
+    title: {
35
+      type: String,
36
+      default: ''
37
+    },
38
+    value: {
39
+      type: String,
40
+      default: ''
41
+    },
42
+    placeholder: {
43
+      type: String,
44
+      default: ''
45
+    }
46
+  },
47
+  data() {
48
+    return {
49
+      localValue: this.value
50
+    }
51
+  },
52
+  watch: {
53
+    value(newVal) {
54
+      this.localValue = newVal
55
+    },
56
+    visible(newVal) {
57
+      if (newVal) {
58
+        this.localValue = this.value
59
+      }
60
+    }
61
+  },
62
+  methods: {
63
+    handleCancel() {
64
+      this.$emit('cancel')
65
+    },
66
+    handleConfirm() {
67
+      this.$emit('confirm', this.localValue)
68
+    }
69
+  }
70
+}
71
+</script>
72
+
73
+<style scoped lang="scss">
74
+.modal-content {
75
+  display: flex;
76
+  flex-direction: column;
77
+  gap: 30rpx;
78
+  padding: 20rpx;
79
+}
80
+
81
+.modal-header {
82
+  display: flex;
83
+  justify-content: center;
84
+  padding: 10rpx 0;
85
+}
86
+
87
+.modal-title {
88
+  font-size: 32rpx;
89
+  font-weight: bold;
90
+  color: #333;
91
+}
92
+
93
+.modal-body {
94
+  padding: 10rpx 0;
95
+}
96
+
97
+.modal-input {
98
+  border: 2rpx solid #e0e0e0;
99
+  border-radius: 10rpx;
100
+  padding: 20rpx;
101
+  font-size: 28rpx;
102
+}
103
+
104
+.modal-footer {
105
+  display: flex;
106
+  justify-content: space-between;
107
+  gap: 20rpx;
108
+  padding: 10rpx 0;
109
+}
110
+
111
+.btn {
112
+  flex: 1;
113
+  text-align: center;
114
+  padding: 20rpx;
115
+  border-radius: 10rpx;
116
+  font-size: 28rpx;
117
+  font-weight: 500;
118
+  cursor: pointer;
119
+}
120
+
121
+.cancel-btn {
122
+  background-color: #f5f5f5;
123
+  color: #666;
124
+}
125
+
126
+.confirm-btn {
127
+  background-color: blueviolet;
128
+  color: white;
129
+}
130
+</style>

+ 54 - 0
pages/orderDetailRefactoredOld/components/FollowCard.vue

@@ -0,0 +1,54 @@
1
+<template>
2
+  <view class="follow-card-container">
3
+    <clue-follow :clueId="clueId" :type="type" />
4
+  </view>
5
+</template>
6
+
7
+<script>
8
+import clueFollow from '@/pages/orderDetail/tabs/followRecord/index.vue'
9
+
10
+export default {
11
+  name: 'FollowCard',
12
+  components: {
13
+    clueFollow
14
+  },
15
+  props: {
16
+    clueId: {
17
+      type: [String, Number],
18
+      default: ''
19
+    },
20
+    type: {
21
+      type: [String, Number],
22
+      required: true
23
+    }
24
+  }
25
+}
26
+</script>
27
+
28
+<style scoped lang="scss">
29
+.follow-card-container {
30
+  padding: 20rpx;
31
+  background-color: #ffffff;
32
+  min-height: auto;
33
+
34
+  // 覆盖 clue-follow 组件的样式
35
+  ::v-deep .followRecord_wrap {
36
+    min-height: auto;
37
+    background: transparent;
38
+    padding: 0;
39
+  }
40
+
41
+  ::v-deep .followRecord_timeLine_wrap {
42
+    background: transparent;
43
+  }
44
+
45
+  ::v-deep .empty_wrap {
46
+    position: static;
47
+    transform: none;
48
+    padding: 40rpx 0;
49
+    text-align: center;
50
+    color: #999999;
51
+    font-size: 28rpx;
52
+  }
53
+}
54
+</style>

+ 287 - 0
pages/orderDetailRefactoredOld/components/OrderDetailView.vue

@@ -0,0 +1,287 @@
1
+<template>
2
+  <view class="order-detail-view">
3
+    <!-- 页面切换 -->
4
+    <view class="page-item" v-show="activeIndex === 0">
5
+      <PageOne :order-detail="orderDetail" :order-id="orderId" :current-receipt="currentReceipt" @next="handleNext" />
6
+    </view>
7
+
8
+    <view class="page-item" v-show="activeIndex === 1">
9
+      <PageTwo :order-detail="orderDetail" :order-id="orderId" :current-receipt="currentReceipt"
10
+        :follow-up-list="followUpList" @next="handleNext" @update-file-ids="handleUpdateFileIds"
11
+        @price-updated="$emit('price-updated')" />
12
+    </view>
13
+
14
+    <view class="page-item" v-show="activeIndex === 2">
15
+      <PageThree :order-detail="orderDetail" :order-id="orderId" :current-receipt="currentReceipt" @next="handleNext"
16
+        @save="handleNeedSave" @confirm-pay="handleConfirmPay" @update-file-ids="handleUpdateFileIds"
17
+        @price-updated="$emit('price-updated')" ref="pageThreeRef" />
18
+    </view>
19
+
20
+    <view class="page-item" v-show="activeIndex === 3">
21
+      <PageFour :order-detail="orderDetail" :current-receipt="currentReceipt" @next="handleNext"
22
+        @confirm-warehouse="handleConfirmWarehouse" />
23
+    </view>
24
+
25
+    <!-- 页面导航 -->
26
+    <ul class="page-navigation">
27
+      <li v-for="(tab, index) in tabs" :key="index" :class="{ 'active': activeIndex === index }"
28
+        @click="handleTabClick(index)">
29
+        {{ tab }}
30
+      </li>
31
+    </ul>
32
+  </view>
33
+</template>
34
+
35
+<script>
36
+import PageOne from './PageOne.vue'
37
+import PageTwo from './PageTwo.vue'
38
+import PageThree from './PageThree.vue'
39
+import PageFour from './PageFour.vue'
40
+
41
+export default {
42
+  name: 'OrderDetailView',
43
+  components: {
44
+    PageOne,
45
+    PageTwo,
46
+    PageThree,
47
+    PageFour
48
+  },
49
+  props: {
50
+    orderDetail: {
51
+      type: Object,
52
+      default: () => ({})
53
+    },
54
+    topInfo: {
55
+      type: Object,
56
+      default: () => ({})
57
+    },
58
+    orderId: {
59
+      type: String,
60
+      default: ''
61
+    },
62
+    currentReceipt: {
63
+      type: Object,
64
+      default: () => ({})
65
+    }
66
+  },
67
+  data() {
68
+    return {
69
+      activeIndex: 0,
70
+      tabs: ['一', '二', '三', '四'],
71
+      // 表单数据
72
+      formData: {
73
+        formOne: {},
74
+        formTwo: {},
75
+        formThree: {},
76
+        formFour: {}
77
+      },
78
+      pageThreeForm: {},
79
+      fileIds: '',
80
+      // 跟进记录
81
+      followUpList: []
82
+    }
83
+  },
84
+  watch: {
85
+    orderDetail: {
86
+      handler(newVal) {
87
+        if (newVal && newVal.clueId) {
88
+          this.loadFollowUpList()
89
+        }
90
+      },
91
+      deep: true,
92
+      immediate: true
93
+    }
94
+  },
95
+  methods: {
96
+    /**
97
+     * 加载跟进记录
98
+     */
99
+    async loadFollowUpList() {
100
+      try {
101
+        const res = await uni.$u.api.getDuplicateOrderFollowListByClueId({
102
+          clueId: this.orderDetail.clueId
103
+        })
104
+        const data = res.data || {}
105
+        const followUpList = []
106
+        for (const key in data) {
107
+          followUpList.push(...(data[key] || []))
108
+        }
109
+        this.followUpList = followUpList
110
+      } catch (error) {
111
+        console.error('获取跟进记录失败:', error)
112
+        uni.$u.toast('获取跟进记录失败')
113
+      }
114
+    },
115
+
116
+    /**
117
+     * 处理下一步
118
+     */
119
+    handleNext({ nowPage, form }) {
120
+      this.activeIndex++
121
+      if (nowPage) {
122
+        this.formData[nowPage] = form
123
+      }
124
+      // 当切换到第三页时,更新第三页的图片列表
125
+      if (nowPage === 'formTwo' && this.$refs.pageThreeRef) {
126
+        this.$refs.pageThreeRef.refreshImageList()
127
+      }
128
+    },
129
+
130
+    /**
131
+     * 处理保存
132
+     */
133
+    handleNeedSave({ nowPage, form, fileIds }) {
134
+      this.pageThreeForm = form
135
+      this.fileIds = fileIds
136
+    },
137
+
138
+    /**
139
+     * 处理确认支付
140
+     */
141
+    async handleConfirmPay() {
142
+      try {
143
+        const response = await uni.$u.api.saveOrderFileAndTransfer({
144
+          id: this.orderId,
145
+          clueId: this.orderDetail.clueId
146
+        })
147
+        uni.$u.toast(response.msg || '支付成功')
148
+      } catch (error) {
149
+        console.error('支付失败:', error)
150
+        uni.$u.toast(`支付失败:${error}`)
151
+
152
+        //支付失败回滚支付信息
153
+        await uni.$u.api.updateClueOrderForm({
154
+          id: this.orderDetail.id,
155
+          paymentMethod: ''
156
+        })
157
+      }
158
+    },
159
+
160
+    /**
161
+     * 处理确认入库
162
+     */
163
+    async handleConfirmWarehouse({ warehouseInfo }) {
164
+      try {
165
+        const params = {
166
+          searchValue: this.orderDetail.searchValue,
167
+          createBy: this.orderDetail.createBy,
168
+          createTime: this.orderDetail.createTime,
169
+          updateBy: this.orderDetail.updateBy,
170
+          updateTime: this.orderDetail.updateTime,
171
+          params: this.orderDetail.params,
172
+          id: this.currentReceipt.id,
173
+          sendFormId: this.orderId,
174
+          clueId: this.orderDetail.clueId,
175
+          item: warehouseInfo.item || '',
176
+          code: warehouseInfo.codeStorage || '',
177
+          phone: this.orderDetail.phone,
178
+          tableFee: warehouseInfo.watchPrice || '',
179
+          benefitFee: warehouseInfo.benefitFee || '',
180
+          freight: warehouseInfo.freight || '',
181
+          checkCodeFee: warehouseInfo.checkCodeFee || '',
182
+          receiptRemark: `${warehouseInfo.remarks || ''};${warehouseInfo.uploadedImage || ''}`,
183
+          repairAmount: warehouseInfo.repairAmount || '',
184
+          grossPerformance: warehouseInfo.grossPerformance || '',
185
+          expressOrderNo: warehouseInfo.expressOrderNo || '',
186
+          fileIds: this.fileIds,
187
+          customerServiceName: warehouseInfo.customerServiceName || '1',
188
+          deptId: this.orderDetail.deptId,
189
+          category: warehouseInfo.category || this.orderDetail.category,
190
+          delFlag: this.orderDetail.delFlag,
191
+          idCard: this.pageThreeForm.idNumber || '',
192
+          paymentMethod: '小葫芦线上支付',
193
+          bankCardNumber: this.pageThreeForm.bankAccount || '',
194
+          bankName: this.pageThreeForm.bankName || '',
195
+          customName: this.pageThreeForm.customName || ''
196
+        }
197
+
198
+        if (this.currentReceipt.id) {
199
+          await uni.$u.api.updateReceiptForm(params)
200
+        } else {
201
+          await uni.$u.api.addReceiptForm(params)
202
+        }
203
+        uni.$u.toast('入库成功')
204
+      } catch (error) {
205
+        console.error('入库失败:', error)
206
+        uni.$u.toast('入库失败')
207
+      }
208
+    },
209
+
210
+    /**
211
+     * 处理标签点击
212
+     */
213
+    handleTabClick(index) {
214
+      this.activeIndex = index
215
+      // 切换到第三页时刷新图片列表
216
+      if (index === 2 && this.$refs.pageThreeRef) {
217
+        this.$refs.pageThreeRef.refreshImageList()
218
+      }
219
+    },
220
+
221
+    /**
222
+     * 更新 fileIds
223
+     */
224
+    handleUpdateFileIds(fileIds) {
225
+      if (this.currentReceipt) {
226
+        this.$set(this.currentReceipt, 'fileIds', fileIds)
227
+        this.fileIds = fileIds
228
+      }
229
+    }
230
+  }
231
+}
232
+</script>
233
+
234
+<style scoped lang="scss">
235
+.order-detail-view {
236
+  padding: 20rpx;
237
+  min-height: calc(100vh - 200rpx);
238
+}
239
+
240
+.page-item {
241
+  width: 100%;
242
+}
243
+
244
+.page-navigation {
245
+  position: fixed;
246
+  right: 20rpx;
247
+  top: 40%;
248
+  display: flex;
249
+  flex-direction: column;
250
+  align-items: center;
251
+  justify-content: center;
252
+  list-style: none;
253
+  color: #000;
254
+  font-size: 20rpx;
255
+  font-weight: 800;
256
+  z-index: 100;
257
+
258
+  li {
259
+    opacity: 0.7;
260
+    display: flex;
261
+    align-items: center;
262
+    justify-content: center;
263
+    background-color: #fff;
264
+    border-radius: 50%;
265
+    width: 70rpx;
266
+    height: 70rpx;
267
+    line-height: 70rpx;
268
+    text-align: center;
269
+    margin-bottom: 20rpx;
270
+    transition: all 0.3s ease-in-out;
271
+    font-weight: 800;
272
+    box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
273
+    cursor: pointer;
274
+
275
+    &.active {
276
+      color: #fff;
277
+      opacity: 1;
278
+      background-color: rgb(37 99 235 / 1);
279
+    }
280
+
281
+    &:hover {
282
+      opacity: 0.9;
283
+      transform: scale(1.05);
284
+    }
285
+  }
286
+}
287
+</style>

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 1173 - 0
pages/orderDetailRefactoredOld/components/PageFour.vue


+ 748 - 0
pages/orderDetailRefactoredOld/components/PageOne.vue

@@ -0,0 +1,748 @@
1
+<template>
2
+  <view class="page-one-container">
3
+    <!-- 图片资料标题 -->
4
+    <view class="page-header">
5
+
6
+      <view class="detail-image-header">
7
+        <text class="detail-image-title">图片资料</text>
8
+        <view class="copy-btn" @click="handleSaveAllImages">
9
+          <text>一键下载实物图到相册</text>
10
+        </view>
11
+      </view>
12
+    </view>
13
+
14
+    <!-- 实物图卡片 -->
15
+    <view class="card-wrap">
16
+      <view class="card-title">实物图</view>
17
+      <view class="image-upload-container">
18
+        <view class="image-list">
19
+          <view v-for="(item, index) in truePicList" :key="`truePic-${index}`" class="image-item">
20
+            <PicComp :src="item.fileUrl" @needPreviewPic="previewTrueImage" />
21
+            <view class="delete-btn" @click="handleDeleteImage(item)">×</view>
22
+          </view>
23
+          <view class="upload-btn" @click="handleUploadImage('truePic')">
24
+            <u-icon name="plus" size="40" color="#999" />
25
+          </view>
26
+        </view>
27
+      </view>
28
+    </view>
29
+
30
+
31
+    <!-- 聊天记录/通话记录/前端跟进/跟进记录 -->
32
+    <view class="card-wrap">
33
+      <view class="card-title">
34
+        <text :class="{ 'active': recordType === 'chat' }" @click="recordType = 'chat'">
35
+          聊天记录
36
+        </text>
37
+        <text class="divider">|</text>
38
+        <text :class="{ 'active': recordType === 'call' }" @click="recordType = 'call'">
39
+          通话记录
40
+        </text>
41
+        <text class="divider">|</text>
42
+        <text :class="{ 'active': recordType === 'frontendFollow' }" @click="recordType = 'frontendFollow'">
43
+          前端跟进
44
+        </text>
45
+        <text class="divider">|</text>
46
+        <text :class="{ 'active': recordType === 'followRecord' }" @click="recordType = 'followRecord'">
47
+          跟进记录
48
+        </text>
49
+      </view>
50
+
51
+      <!-- 聊天记录 -->
52
+      <view v-if="recordType === 'chat'" class="image-upload-container">
53
+        <view class="image-list">
54
+          <view v-for="(item, index) in chatRecordsList" :key="`chat-${index}`" class="image-item">
55
+            <PicComp :src="item.fileUrl" @needPreviewPic="previewImage" />
56
+            <view class="delete-btn" @click="handleDeleteImage(item)">×</view>
57
+          </view>
58
+          <view class="upload-btn" @click="handleUploadImage('chatRecords')">
59
+            <u-icon name="plus" size="40" color="#999" />
60
+          </view>
61
+        </view>
62
+      </view>
63
+
64
+      <!-- 通话录音 -->
65
+      <view v-if="recordType === 'call'" class="call-records-container">
66
+        <sound-recorder v-for="item in soundRecordList" :key="item.fileName" :data="item"
67
+          @handleDelectThisSoundRecord="handleDeleteSoundRecord" />
68
+      </view>
69
+
70
+      <!-- 前端跟进 -->
71
+      <follow-card v-if="recordType === 'frontendFollow'" :key="'frontendFollow'" :clue-id="currentClueId" type="4" />
72
+
73
+      <!-- 跟进记录 -->
74
+      <follow-card v-if="recordType === 'followRecord'" :key="'followRecord'" :clue-id="currentClueId" type="5" />
75
+    </view>
76
+
77
+    <!-- 基本信息卡片 -->
78
+    <view class="info-card">
79
+      <view class="info-card-title">基本信息</view>
80
+      <u-row class="info-row">
81
+        <u-col span="6">
82
+          <view class="info-label">发单人</view>
83
+          <view class="info-value">{{ orderDetail.createNickName || '未填写' }}</view>
84
+        </u-col>
85
+        <u-col span="6">
86
+          <view class="info-label">型号</view>
87
+          <view class="info-value">{{ orderDetail.model || '未填写' }}</view>
88
+        </u-col>
89
+      </u-row>
90
+      <u-row class="info-row">
91
+        <u-col span="6">
92
+          <view class="info-label">上门时间</view>
93
+          <view class="info-value">{{ orderDetail.visitTime || '未填写' }}</view>
94
+        </u-col>
95
+        <u-col span="6">
96
+          <view class="info-label">地址</view>
97
+          <view class="info-value">{{ orderDetail.address || '未填写' }}</view>
98
+        </u-col>
99
+      </u-row>
100
+    </view>
101
+
102
+    <!-- 联系方式卡片 -->
103
+    <view class="contact-card">
104
+      <view class="contact-item phone-card" @click="handlePhoneClick">
105
+        <u-icon name="phone" size="40" color="#07C160" />
106
+        <view class="contact-title">电话</view>
107
+        <view v-if="orderDetail.phone" class="red-dot"></view>
108
+      </view>
109
+      <view class="contact-item wechat-card" @click="handleWechatClick">
110
+        <u-icon name="chat" size="40" color="#07C160" />
111
+        <view class="contact-title">微信</view>
112
+        <view v-if="orderDetail.wechat" class="red-dot"></view>
113
+      </view>
114
+    </view>
115
+
116
+    <!-- 下一步按钮 -->
117
+    <view class="space-block"></view>
118
+    <u-button class="next-btn" @click="handleNext" type="primary" size="middle">
119
+      下一步
120
+    </u-button>
121
+  </view>
122
+</template>
123
+
124
+<script>
125
+import PicComp from './PicComp.vue'
126
+import soundRecorder from '@/components/soundRecorder/soundRecorder.vue'
127
+import FollowCard from './FollowCard.vue'
128
+import imageUpload from '../utils/imageUpload.js'
129
+
130
+export default {
131
+  name: 'PageOne',
132
+  components: {
133
+    PicComp,
134
+    soundRecorder,
135
+    FollowCard
136
+  },
137
+  props: {
138
+    orderDetail: {
139
+      type: Object,
140
+      default: () => ({})
141
+    },
142
+    orderId: {
143
+      type: String,
144
+      default: ''
145
+    },
146
+    currentReceipt: {
147
+      type: Object,
148
+      default: () => ({})
149
+    }
150
+  },
151
+  data() {
152
+    return {
153
+      recordType: 'chat', // 'chat' | 'call' | 'frontendFollow' | 'followRecord'
154
+      chatRecordsList: [],
155
+      truePicList: [],
156
+      soundRecordList: []
157
+    }
158
+  },
159
+  computed: {
160
+    currentClueId() {
161
+      return (this.currentReceipt && this.currentReceipt.clueId) || (this.orderDetail && this.orderDetail.clueId) || ''
162
+    }
163
+  },
164
+  watch: {
165
+    recordType(newVal) {
166
+      if (newVal === 'call') {
167
+        this.loadCallRecords()
168
+      }
169
+    },
170
+    currentReceipt: {
171
+      handler(newVal) {
172
+        if (newVal && newVal.id) {
173
+          this.loadImageList()
174
+          this.loadCallRecords()
175
+        }
176
+      },
177
+      immediate: true,
178
+      deep: true
179
+    }
180
+  },
181
+  methods: {
182
+    /**
183
+     * 加载图片列表
184
+     */
185
+    async loadImageList() {
186
+      if (!this.currentReceipt.id || !this.orderDetail.itemBrand) return
187
+
188
+      try {
189
+        // 加载聊天记录
190
+        const chatList = await imageUpload.getFileList(
191
+          '2',
192
+          '1',
193
+          this.currentReceipt.id,
194
+          this.orderDetail.itemBrand,
195
+          this.currentReceipt.clueId
196
+        )
197
+        this.chatRecordsList = chatList || []
198
+
199
+        // 加载实物图
200
+        const truePicList = await imageUpload.getFileList(
201
+          '2',
202
+          '2',
203
+          this.currentReceipt.id,
204
+          this.orderDetail.itemBrand,
205
+          this.currentReceipt.clueId
206
+        )
207
+        this.truePicList = truePicList || []
208
+      } catch (error) {
209
+        console.error('加载图片列表失败:', error)
210
+      }
211
+    },
212
+
213
+    /**
214
+     * 加载通话记录
215
+     */
216
+    async loadCallRecords() {
217
+      if (!this.currentReceipt.clueId) return
218
+
219
+      try {
220
+        const { data } = await uni.$u.api.getCallClueFileByClueId({
221
+          clueId: this.currentReceipt.clueId
222
+        })
223
+        this.soundRecordList = data || []
224
+      } catch (error) {
225
+        console.error('加载通话记录失败:', error)
226
+      }
227
+    },
228
+
229
+    /**
230
+     * 上传图片
231
+     */
232
+    async handleUploadImage(type) {
233
+      try {
234
+        const filePaths = await imageUpload.chooseImage(9)
235
+        const uploadResults = await imageUpload.uploadFiles(filePaths)
236
+
237
+        // 绑定订单文件
238
+        const orderFileType = type === 'truePic' ? '2' : '1'
239
+        await imageUpload.bindOrderFile(
240
+          this.currentReceipt.clueId,
241
+          this.currentReceipt.id,
242
+          orderFileType,
243
+          uploadResults
244
+        )
245
+
246
+        // 刷新列表
247
+        this.loadImageList()
248
+      } catch (error) {
249
+        console.error('上传图片失败:', error)
250
+      }
251
+    },
252
+
253
+    /**
254
+     * 删除图片
255
+     */
256
+    async handleDeleteImage(item) {
257
+      uni.showModal({
258
+        title: '提示',
259
+        content: '确定要删除这张图片吗?',
260
+        success: async (res) => {
261
+          if (res.confirm) {
262
+            try {
263
+              await imageUpload.deleteFile(item.id)
264
+              this.loadImageList()
265
+            } catch (error) {
266
+              console.error('删除图片失败:', error)
267
+            }
268
+          }
269
+        }
270
+      })
271
+    },
272
+
273
+    /**
274
+     * 删除录音
275
+     */
276
+    async handleDeleteSoundRecord({ id }) {
277
+      uni.showModal({
278
+        title: '提示',
279
+        content: '是否确定删除?',
280
+        success: async (res) => {
281
+          if (res.confirm) {
282
+            try {
283
+              await uni.$u.api.deleteClueFile([id])
284
+              uni.showToast({
285
+                title: '删除成功',
286
+                icon: 'success'
287
+              })
288
+              this.loadCallRecords()
289
+            } catch (error) {
290
+              uni.showToast({
291
+                title: '删除失败',
292
+                icon: 'error'
293
+              })
294
+            }
295
+          }
296
+        }
297
+      })
298
+    },
299
+
300
+    /**
301
+     * 预览图片
302
+     */
303
+    previewImage(src) {
304
+      const urlList = this.chatRecordsList.map(item => item.fileUrl)
305
+      uni.previewImage({
306
+        urls: urlList,
307
+        current: src
308
+      })
309
+    },
310
+
311
+    /**
312
+     * 预览实物图
313
+     */
314
+    previewTrueImage(src) {
315
+      const urlList = this.truePicList.map(item => item.fileUrl)
316
+      uni.previewImage({
317
+        urls: urlList,
318
+        current: src
319
+      })
320
+    },
321
+
322
+    //一键复制
323
+    handleSaveAllImages() {
324
+      // 合并所有图片
325
+      const allImages = [...this.truePicList]
326
+      //取出所有图的url
327
+      const allUrls = allImages.map(item => item.fileUrl)
328
+      if (allUrls.length > 0) {
329
+        // 显示保存图片确认弹窗
330
+        uni.showModal({
331
+          title: '保存图片',
332
+          content: `是否将 ${allUrls.length} 张图片保存到本地相册?`,
333
+          confirmText: '保存',
334
+          // cancelText: '仅复制链接',
335
+          success: (res) => {
336
+            if (res.confirm) {
337
+              // 用户选择保存图片
338
+              this.saveImagesToLocal(allUrls)
339
+            } else if (res.cancel) {
340
+              // 用户选择仅复制链接
341
+              this.copyImageUrls(allUrls)
342
+            }
343
+          }
344
+        })
345
+      } else {
346
+        uni.showToast({
347
+          title: '没有图片可保存',
348
+          icon: 'none'
349
+        })
350
+      }
351
+    },
352
+
353
+    // 保存图片到本地相册
354
+    async saveImagesToLocal(imageUrls) {
355
+      try {
356
+        uni.showLoading({
357
+          title: '正在保存图片...',
358
+          mask: true
359
+        })
360
+
361
+        const savedImages = []
362
+        const failedImages = []
363
+
364
+        // 逐个保存图片
365
+        for (let i = 0; i < imageUrls.length; i++) {
366
+          const url = imageUrls[i]
367
+          try {
368
+            await this.saveSingleImage(url)
369
+            savedImages.push(url)
370
+          } catch (error) {
371
+            console.error(`保存图片失败: ${url}`, error)
372
+            failedImages.push(url)
373
+          }
374
+
375
+          // 更新进度
376
+          uni.showLoading({
377
+            title: `正在保存图片... (${i + 1}/${imageUrls.length})`,
378
+            mask: true
379
+          })
380
+        }
381
+
382
+        uni.hideLoading()
383
+
384
+        // 显示结果
385
+        let message = `成功保存 ${savedImages.length} 张图片`
386
+        if (failedImages.length > 0) {
387
+          message += `,${failedImages.length} 张保存失败`
388
+        }
389
+
390
+        uni.showToast({
391
+          title: message,
392
+          icon: 'none',
393
+          duration: 3000
394
+        })
395
+
396
+        // 如果有失败的图片,也复制链接作为备选
397
+        if (failedImages.length > 0) {
398
+          const allUrls = [...savedImages, ...failedImages]
399
+          this.copyImageUrls(allUrls)
400
+        }
401
+      } catch (error) {
402
+        uni.hideLoading()
403
+        console.error('保存图片过程中发生错误:', error)
404
+        uni.showToast({
405
+          title: '保存图片失败',
406
+          icon: 'error'
407
+        })
408
+      }
409
+    },
410
+
411
+    // 保存单张图片
412
+    saveSingleImage(url) {
413
+      return new Promise((resolve, reject) => {
414
+        // 先下载图片
415
+        uni.downloadFile({
416
+          url: url,
417
+          success: (res) => {
418
+            if (res.statusCode === 200) {
419
+              // 保存到相册
420
+              uni.saveImageToPhotosAlbum({
421
+                filePath: res.tempFilePath,
422
+                success: () => {
423
+                  console.log('图片保存成功:', url)
424
+                  resolve()
425
+                },
426
+                fail: (err) => {
427
+                  console.error('保存到相册失败:', err)
428
+                  // 如果是权限问题,尝试请求权限
429
+                  if (err.errMsg.includes('auth denied')) {
430
+                    uni.showModal({
431
+                      title: '权限不足',
432
+                      content: '需要访问相册权限来保存图片,是否去设置?',
433
+                      success: (modalRes) => {
434
+                        if (modalRes.confirm) {
435
+                          // 打开设置页面
436
+                          uni.openSetting({
437
+                            success: (settingRes) => {
438
+                              console.log('设置页面结果:', settingRes)
439
+                            }
440
+                          })
441
+                        }
442
+                      }
443
+                    })
444
+                  }
445
+                  reject(err)
446
+                }
447
+              })
448
+            } else {
449
+              reject(new Error('下载失败'))
450
+            }
451
+          },
452
+          fail: (err) => {
453
+            console.error('下载图片失败:', err)
454
+            reject(err)
455
+          }
456
+        })
457
+      })
458
+    },
459
+
460
+    // 复制图片链接
461
+    copyImageUrls(urls) {
462
+      uni.setClipboardData({
463
+        data: JSON.stringify(urls),
464
+        success: () => {
465
+          uni.showToast({
466
+            title: '图片链接已复制',
467
+            icon: 'none'
468
+          })
469
+        }
470
+      })
471
+    },
472
+
473
+    /**
474
+     * 电话点击
475
+     */
476
+    handlePhoneClick() {
477
+      if (!this.orderDetail.phone) {
478
+        uni.showToast({
479
+          title: '该订单暂时没有电话号码',
480
+          icon: 'none'
481
+        })
482
+        return
483
+      }
484
+
485
+      uni.makePhoneCall({
486
+        phoneNumber: this.orderDetail.phone,
487
+        // phoneNumber:'13813737524',//开发者测试手机号
488
+        success: () => {
489
+          this.$store.commit('call/SET_FORM', {
490
+            clueId: this.orderDetail.clueId,
491
+            type: '3',
492
+            callee: this.orderDetail.phone
493
+          })
494
+        }
495
+      })
496
+    },
497
+
498
+    /**
499
+     * 微信点击
500
+     */
501
+    handleWechatClick() {
502
+      if (!this.orderDetail.wechat) {
503
+        uni.showToast({
504
+          title: '该订单暂时没有微信号',
505
+          icon: 'none'
506
+        })
507
+        return
508
+      }
509
+
510
+      uni.setClipboardData({
511
+        data: this.orderDetail.wechat,
512
+        success: () => {
513
+          uni.showToast({
514
+            title: '微信号已复制',
515
+            icon: 'none'
516
+          })
517
+        }
518
+      })
519
+    },
520
+
521
+    /**
522
+     * 下一步
523
+     */
524
+    handleNext() {
525
+      this.$emit('next', {
526
+        nowPage: 'formOne',
527
+        form: {}
528
+      })
529
+    }
530
+  }
531
+}
532
+</script>
533
+
534
+<style scoped lang="scss">
535
+@import '../styles/common.scss';
536
+
537
+.page-one-container {
538
+  @extend .page-container;
539
+  padding-bottom: 100rpx;
540
+}
541
+
542
+.page-header {
543
+  display: flex;
544
+  justify-content: space-between;
545
+  align-items: center;
546
+  margin-bottom: 20rpx;
547
+}
548
+
549
+.page-title {
550
+  @include font-styles($size: title, $weight: bold, $color: primary);
551
+}
552
+
553
+.save-all-btn {
554
+  border-radius: 20rpx;
555
+  border-color: #007AFF;
556
+  color: #007AFF;
557
+}
558
+
559
+.card-wrap {
560
+  @extend .card-wrap;
561
+  margin-top: 20rpx;
562
+}
563
+
564
+.card-title {
565
+  padding: 20rpx 15rpx;
566
+  border-bottom: 1rpx solid map-get($colors, border);
567
+  display: flex;
568
+  align-items: center;
569
+  white-space: nowrap;
570
+  overflow-x: auto;
571
+  -webkit-overflow-scrolling: touch;
572
+
573
+  text {
574
+    padding: 0 6rpx;
575
+    cursor: pointer;
576
+    font-size: 26rpx;
577
+    white-space: nowrap;
578
+    flex-shrink: 0;
579
+
580
+    &.active {
581
+      color: map-get($colors, primary);
582
+      font-weight: bold;
583
+    }
584
+  }
585
+
586
+  .divider {
587
+    margin: 0 4rpx;
588
+    color: #ddd;
589
+    font-size: 24rpx;
590
+    flex-shrink: 0;
591
+  }
592
+}
593
+
594
+.image-upload-container {
595
+  padding: 20rpx;
596
+}
597
+
598
+.image-list {
599
+  display: flex;
600
+  flex-wrap: wrap;
601
+  gap: 20rpx;
602
+}
603
+
604
+.image-item {
605
+  position: relative;
606
+  width: 200rpx;
607
+  height: 200rpx;
608
+  box-sizing: border-box;
609
+}
610
+
611
+.delete-btn {
612
+  position: absolute;
613
+  top: -10rpx;
614
+  right: -10rpx;
615
+  width: 40rpx;
616
+  height: 40rpx;
617
+  background-color: #ff4d4f;
618
+  color: #fff;
619
+  border-radius: 50%;
620
+  display: flex;
621
+  align-items: center;
622
+  justify-content: center;
623
+  font-size: 30rpx;
624
+  font-weight: bold;
625
+  z-index: 10;
626
+  cursor: pointer;
627
+}
628
+
629
+.upload-btn {
630
+  width: 200rpx;
631
+  height: 200rpx;
632
+  border: 8rpx dashed #ddd;
633
+  border-radius: 30rpx;
634
+  display: flex;
635
+  align-items: center;
636
+  justify-content: center;
637
+  background-color: #f9f9f9;
638
+  box-sizing: border-box;
639
+  cursor: pointer;
640
+}
641
+
642
+.call-records-container {
643
+  padding: 20rpx;
644
+}
645
+
646
+.info-card {
647
+  @extend .card-wrap;
648
+  padding: 20rpx;
649
+  margin-top: 20rpx;
650
+  box-sizing: border-box;
651
+  width: 100%;
652
+  max-width: 100%;
653
+}
654
+
655
+.info-card-title {
656
+  @include font-styles($size: title, $weight: bold, $color: primary);
657
+  margin-bottom: 25rpx;
658
+  padding-bottom: 15rpx;
659
+  border-bottom: 1rpx solid map-get($colors, border);
660
+}
661
+
662
+.info-row {
663
+  margin-bottom: 20rpx;
664
+}
665
+
666
+.info-label {
667
+  @include font-styles($size: tiny, $weight: regular, $color: tertiary);
668
+  margin-bottom: 8rpx;
669
+}
670
+
671
+.info-value {
672
+  @include font-styles($size: small, $weight: regular, $color: secondary);
673
+  word-break: break-all;
674
+}
675
+
676
+.contact-card {
677
+  display: flex;
678
+  justify-content: space-between;
679
+  margin: 20rpx 0;
680
+  gap: 20rpx;
681
+}
682
+
683
+.contact-item {
684
+  flex: 1;
685
+  @extend .card-wrap;
686
+  padding: 20rpx;
687
+  display: flex;
688
+  flex-direction: column;
689
+  align-items: center;
690
+  position: relative;
691
+  cursor: pointer;
692
+}
693
+
694
+.contact-title {
695
+  @include font-styles($size: tiny, $weight: regular, $color: tertiary);
696
+  margin-top: 10rpx;
697
+}
698
+
699
+.red-dot {
700
+  position: absolute;
701
+  top: 15rpx;
702
+  right: 15rpx;
703
+  width: 25rpx;
704
+  height: 25rpx;
705
+  background-color: #ff4d4f;
706
+  border-radius: 50%;
707
+  box-shadow: 0 0 4rpx rgba(255, 77, 79, 0.3);
708
+}
709
+
710
+.space-block {
711
+  height: 100rpx;
712
+}
713
+
714
+.next-btn {
715
+  position: fixed;
716
+  bottom: 10rpx;
717
+  left: 2.5%;
718
+  width: 95%;
719
+  height: 80rpx;
720
+  line-height: 80rpx;
721
+  text-align: center;
722
+  border-radius: 20rpx;
723
+}
724
+
725
+
726
+.detail-image-header {
727
+  display: flex;
728
+  justify-content: space-between;
729
+  align-items: center;
730
+  width: 100%;
731
+  border-bottom: 1rpx solid map-get($colors, border);
732
+}
733
+
734
+.detail-image-title {
735
+  @include font-styles($size: content, $weight: bold, $color: primary);
736
+}
737
+
738
+.copy-btn {
739
+  border-radius: 20rpx;
740
+  border: 1rpx solid #007AFF;
741
+  background-color: transparent;
742
+  color: #007AFF;
743
+  padding: 0 24rpx;
744
+  height: 64rpx;
745
+  line-height: 64rpx;
746
+  cursor: pointer;
747
+}
748
+</style>

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 1172 - 0
pages/orderDetailRefactoredOld/components/PageThree.vue


+ 800 - 0
pages/orderDetailRefactoredOld/components/PageTwo.vue

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

+ 43 - 0
pages/orderDetailRefactoredOld/components/PicComp.vue

@@ -0,0 +1,43 @@
1
+<template>
2
+  <view class="pic-comp-container">
3
+    <image 
4
+      class="pic-comp-image" 
5
+      :src="src" 
6
+      mode="aspectFill" 
7
+      @click="handleClick"
8
+    />
9
+  </view>
10
+</template>
11
+
12
+<script>
13
+export default {
14
+  name: 'PicComp',
15
+  props: {
16
+    src: {
17
+      type: String,
18
+      default: ''
19
+    }
20
+  },
21
+  methods: {
22
+    handleClick() {
23
+      this.$emit('needPreviewPic', this.src)
24
+    }
25
+  }
26
+}
27
+</script>
28
+
29
+<style scoped lang="scss">
30
+.pic-comp-container {
31
+  width: 100%;
32
+  height: 100%;
33
+  box-sizing: border-box;
34
+  overflow: hidden;
35
+  border-radius: 30rpx;
36
+}
37
+
38
+.pic-comp-image {
39
+  width: 100% !important;
40
+  height: 100% !important;
41
+  object-fit: cover;
42
+}
43
+</style>

+ 457 - 0
pages/orderDetailRefactoredOld/index.vue

@@ -0,0 +1,457 @@
1
+<template>
2
+  <view class="order-detail-container">
3
+    <!-- 顶部导航栏 -->
4
+    <u-navbar :autoBack="true" :placeholder="true" v-hideNav>
5
+      <template slot="center">
6
+        <view class="navbar-center">
7
+          <text class="navbar-item" @click="handleBrandClick">{{ topInfo.brand }}</text>
8
+          <text class="navbar-divider">|</text>
9
+          <text class="navbar-item" @click="handleModelClick">{{ topInfo.model }}</text>
10
+          <text class="navbar-divider">|</text>
11
+          <text class="navbar-item price" @click="handlePriceClick">¥{{ topInfo.price }}</text>
12
+        </view>
13
+      </template>
14
+      <template slot="right">
15
+        <view class="navbar-right" @click="handleAddClick">
16
+          <image src="/static/icons/plus.png" mode="scaleToFill" class="add-icon" />
17
+          <text class="add-text">加一单</text>
18
+        </view>
19
+      </template>
20
+    </u-navbar>
21
+
22
+    <!-- 收单列表切换 -->
23
+    <u-tabs keyName="brand" :list="receiptList" @click="handleReceiptClick" class="receipt-tabs" />
24
+
25
+    <!-- 订单详情视图 -->
26
+    <OrderDetailView :order-detail="orderDetail" :top-info="topInfo" :order-id="orderId"
27
+      :current-receipt="currentReceipt" @price-updated="refreshCurrentReceipt" />
28
+
29
+    <!-- 加一单模态窗 -->
30
+    <u-modal :show="addOneModalVisible" title="加一单" showCancelButton @cancel="handleAddOneCancel"
31
+      @confirm="handleAddOneConfirm">
32
+      <view class="add-one-modal-content">
33
+        <view class="add-one-form-item">
34
+          <text class="form-label">品牌<text class="required">*</text></text>
35
+          <u-button type="primary" plain @click="showBrandSelector = true" class="brand-select-btn">
36
+            {{ currentAddBrand.dictLabel || '点击请选择品牌' }}
37
+          </u-button>
38
+        </view>
39
+        <view class="add-one-form-item">
40
+          <text class="form-label">型号</text>
41
+          <u-input v-model="currentAddModel" placeholder="请输入型号" class="form-input" />
42
+        </view>
43
+        <view class="add-one-form-item">
44
+          <text class="form-label">价格</text>
45
+          <u-input v-model="currentAddPrice" placeholder="请输入价格" type="number" class="form-input" />
46
+        </view>
47
+      </view>
48
+    </u-modal>
49
+
50
+    <!-- 品牌选择器 -->
51
+    <u-picker :show="showBrandSelector" :columns="brandColumns" keyName="dictLabel" @confirm="handleBrandConfirm"
52
+      @cancel="showBrandSelector = false" />
53
+
54
+    <!-- 编辑品牌选择器 -->
55
+    <u-picker :show="editBrandSelectorVisible" :columns="brandColumns" keyName="dictLabel"
56
+      @confirm="handleEditBrandConfirm" @cancel="editBrandSelectorVisible = false" />
57
+
58
+    <!-- 修改型号/价格弹窗 -->
59
+    <CustomModal :visible="modalVisible" :title="modalConfig.title" :value="modalConfig.value"
60
+      :placeholder="modalConfig.placeholder" @cancel="handleModalCancel" @confirm="handleModalConfirm" />
61
+  </view>
62
+</template>
63
+
64
+<script>
65
+import OrderDetailView from './components/OrderDetailView.vue'
66
+import CustomModal from './components/CustomModal.vue'
67
+
68
+export default {
69
+  name: 'OrderDetailIndex',
70
+  components: {
71
+    OrderDetailView,
72
+    CustomModal
73
+  },
74
+  data() {
75
+    return {
76
+      // 顶部信息
77
+      topInfo: {
78
+        brand: '',
79
+        model: '',
80
+        price: ''
81
+      },
82
+      // 路由参数
83
+      orderId: '',
84
+      clueId: '',
85
+      item: '',
86
+      type: '',
87
+      // 订单详情
88
+      orderDetail: {},
89
+      // 收单列表
90
+      receiptList: [],
91
+      // 当前选中的收单
92
+      currentReceipt: {},
93
+      // 模态窗状态
94
+      addOneModalVisible: false,
95
+      showBrandSelector: false,
96
+      editBrandSelectorVisible: false,
97
+      modalVisible: false,
98
+      // 品牌选择相关
99
+      brandColumns: [[]],
100
+      currentAddBrand: {},
101
+      currentAddModel: '',
102
+      currentAddPrice: '',
103
+      // 模态窗配置
104
+      modalConfig: {
105
+        title: '',
106
+        value: '',
107
+        placeholder: ''
108
+      },
109
+      currentEditField: ''
110
+    }
111
+  },
112
+  onLoad(options) {
113
+    this.initParams(options)
114
+    this.loadOrderDetail()
115
+    this.loadReceiptList()
116
+  },
117
+  onPullDownRefresh() {
118
+    // 下拉刷新时重新加载所有数据
119
+    Promise.all([
120
+      this.loadOrderDetail(),
121
+      this.loadReceiptList()
122
+    ]).finally(() => {
123
+      uni.stopPullDownRefresh()
124
+      uni.$u.toast('刷新成功')
125
+    })
126
+  },
127
+  methods: {
128
+    /**
129
+     * 初始化参数
130
+     */
131
+    initParams(options) {
132
+      const { item, orderId, type, clueId } = options
133
+      this.item = item || ''
134
+      this.orderId = orderId || ''
135
+      this.type = type || ''
136
+      this.clueId = clueId || ''
137
+    },
138
+
139
+    /**
140
+     * 加载订单详情
141
+     */
142
+    async loadOrderDetail() {
143
+      try {
144
+        const res = await uni.$u.api.getClueSendFormVoByOrderId({
145
+          id: this.orderId
146
+        })
147
+        if (res.code === 200) {
148
+          this.orderDetail = res.data || {}
149
+        }
150
+      } catch (error) {
151
+        console.error('加载订单详情失败:', error)
152
+        uni.$u.toast('加载订单详情失败')
153
+      }
154
+    },
155
+
156
+    /**
157
+     * 加载收单列表
158
+     */
159
+    async loadReceiptList() {
160
+      try {
161
+        const res = await uni.$u.api.clueReceiptFormListByOrderId(this.orderId)
162
+        if (res.code === 200) {
163
+          this.receiptList = res.data || []
164
+          // 默认选择第一个收单
165
+          if (this.receiptList.length > 0) {
166
+            this.handleReceiptClick(this.receiptList[0])
167
+          }
168
+        }
169
+      } catch (error) {
170
+        console.error('加载收单列表失败:', error)
171
+        uni.$u.toast('加载收单列表失败')
172
+      }
173
+    },
174
+
175
+    /**
176
+     * 刷新当前收单(用于价格等修改后同步顶部信息)
177
+     */
178
+    async refreshCurrentReceipt() {
179
+      if (!this.currentReceipt || !this.currentReceipt.id) return
180
+      try {
181
+        const res = await uni.$u.api.getReceiptForm(this.currentReceipt.id)
182
+        if (res.code === 200 && res.data) {
183
+          this.currentReceipt = res.data
184
+          this.topInfo.brand = res.data.brand || '暂无'
185
+          this.topInfo.model = res.data.model || '暂无'
186
+          this.topInfo.price = res.data.sellingPrice ?? '暂无'
187
+        }
188
+      } catch (error) {
189
+        console.error('刷新当前收单失败:', error)
190
+      }
191
+    },
192
+
193
+    /**
194
+     * 点击收单项
195
+     */
196
+    async handleReceiptClick(item) {
197
+      console.log('点击了收单', item)
198
+      //获取当前的收单form详情
199
+      const res = await uni.$u.api.getReceiptForm(item.id)
200
+      console.log('收单详情', res)
201
+
202
+      if (res.code === 200) {
203
+        this.currentReceipt = res.data || {}
204
+        this.topInfo.brand = res.data.brand || '暂无'
205
+        this.topInfo.model = res.data.model || '暂无'
206
+        this.topInfo.price = res.data.sellingPrice || '暂无'
207
+      }
208
+    },
209
+
210
+    /**
211
+     * 点击品牌
212
+     */
213
+    handleBrandClick() {
214
+      this.editBrandSelectorVisible = true
215
+      this.loadBrandList()
216
+    },
217
+
218
+    /**
219
+     * 点击型号
220
+     */
221
+    handleModelClick() {
222
+      this.modalConfig = {
223
+        title: '修改型号',
224
+        value: this.currentReceipt.model || '',
225
+        placeholder: '请输入型号'
226
+      }
227
+      this.currentEditField = 'model'
228
+      this.modalVisible = true
229
+    },
230
+
231
+    /**
232
+     * 点击价格
233
+     */
234
+    handlePriceClick() {
235
+      this.modalConfig = {
236
+        title: '修改价格',
237
+        value: this.currentReceipt.sellingPrice?.toString() || '',
238
+        placeholder: '请输入价格'
239
+      }
240
+      this.currentEditField = 'price'
241
+      this.modalVisible = true
242
+    },
243
+
244
+    /**
245
+     * 点击加一单
246
+     */
247
+    handleAddClick() {
248
+      this.addOneModalVisible = true
249
+      this.loadBrandList()
250
+    },
251
+
252
+    /**
253
+     * 加载品牌列表
254
+     */
255
+    async loadBrandList() {
256
+      try {
257
+        const res = await this.$getDicts('crm_form_brand')
258
+        this.brandColumns = [res]
259
+      } catch (error) {
260
+        console.error('加载品牌列表失败:', error)
261
+      }
262
+    },
263
+
264
+    /**
265
+     * 确认修改模态窗
266
+     */
267
+    async handleModalConfirm(value) {
268
+      try {
269
+        if (this.currentEditField === 'model') {
270
+          await uni.$u.api.updateReceiptForm({
271
+            model: value,
272
+            id: this.currentReceipt.id
273
+          })
274
+        } else if (this.currentEditField === 'price') {
275
+          await uni.$u.api.updateReceiptForm({
276
+            sellingPrice: value,
277
+            id: this.currentReceipt.id
278
+          })
279
+        }
280
+        uni.$u.toast('修改成功')
281
+        this.loadReceiptList()
282
+      } catch (error) {
283
+        console.error('修改失败:', error)
284
+        uni.$u.toast('修改失败')
285
+      } finally {
286
+        this.modalVisible = false
287
+      }
288
+    },
289
+
290
+    /**
291
+     * 取消修改模态窗
292
+     */
293
+    handleModalCancel() {
294
+      this.modalVisible = false
295
+    },
296
+
297
+    /**
298
+     * 确认编辑品牌
299
+     */
300
+    async handleEditBrandConfirm(data) {
301
+      try {
302
+        await uni.$u.api.updateReceiptForm({
303
+          brand: data.value[0].dictValue,
304
+          id: this.currentReceipt.id
305
+        })
306
+        uni.$u.toast('修改成功')
307
+        this.loadReceiptList()
308
+      } catch (error) {
309
+        console.error('修改品牌失败:', error)
310
+        uni.$u.toast('修改失败')
311
+      } finally {
312
+        this.editBrandSelectorVisible = false
313
+      }
314
+    },
315
+
316
+    /**
317
+     * 确认选择品牌(加一单)
318
+     */
319
+    handleBrandConfirm(data) {
320
+      this.currentAddBrand = data.value[0]
321
+      this.showBrandSelector = false
322
+    },
323
+
324
+    /**
325
+     * 确认加一单
326
+     */
327
+    async handleAddOneConfirm() {
328
+      // 验证品牌是否已选择(必选)
329
+      if (!this.currentAddBrand.dictValue) {
330
+        uni.$u.toast('请选择品牌')
331
+        return
332
+      }
333
+
334
+      try {
335
+        await uni.$u.api.addReceiptForm({
336
+          brand: this.currentAddBrand.dictValue,
337
+          model: this.currentAddModel || '',
338
+          sellingPrice: this.currentAddPrice || '',
339
+          sendFormId: this.orderId,
340
+          clueId: this.clueId
341
+        })
342
+        uni.$u.toast('添加成功')
343
+        this.loadReceiptList()
344
+      } catch (error) {
345
+        console.error('添加失败:', error)
346
+        uni.$u.toast('添加失败')
347
+      } finally {
348
+        this.addOneModalVisible = false
349
+        this.currentAddBrand = {}
350
+        this.currentAddModel = ''
351
+        this.currentAddPrice = ''
352
+      }
353
+    },
354
+
355
+    /**
356
+     * 取消加一单
357
+     */
358
+    handleAddOneCancel() {
359
+      this.addOneModalVisible = false
360
+      this.currentAddBrand = {}
361
+      this.currentAddModel = ''
362
+      this.currentAddPrice = ''
363
+    }
364
+  }
365
+}
366
+</script>
367
+
368
+<style scoped lang="scss">
369
+.order-detail-container {
370
+  min-height: 100vh;
371
+  background-color: #f9fafb;
372
+}
373
+
374
+.navbar-center {
375
+  display: flex;
376
+  align-items: center;
377
+  background-color: #fff;
378
+  border-radius: 40rpx;
379
+  padding: 10rpx 20rpx;
380
+  box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
381
+}
382
+
383
+.navbar-item {
384
+  font-weight: bold;
385
+  color: #333;
386
+  font-size: 28rpx;
387
+
388
+  &.price {
389
+    color: blueviolet;
390
+  }
391
+}
392
+
393
+.navbar-divider {
394
+  margin: 0 15rpx;
395
+  color: #ddd;
396
+  font-size: 28rpx;
397
+}
398
+
399
+.navbar-right {
400
+  display: flex;
401
+  flex-direction: column;
402
+  align-items: center;
403
+  justify-content: center;
404
+  font-size: 20rpx;
405
+  color: blueviolet;
406
+}
407
+
408
+.add-icon {
409
+  width: 30rpx;
410
+  height: 30rpx;
411
+}
412
+
413
+.add-text {
414
+  margin-top: 4rpx;
415
+}
416
+
417
+.receipt-tabs {
418
+  background-color: #fff;
419
+}
420
+
421
+.add-one-modal-content {
422
+  padding: 20rpx 0;
423
+}
424
+
425
+.add-one-form-item {
426
+  margin-bottom: 30rpx;
427
+
428
+  &:last-child {
429
+    margin-bottom: 0;
430
+  }
431
+}
432
+
433
+.form-label {
434
+  display: block;
435
+  font-size: 28rpx;
436
+  color: #333;
437
+  margin-bottom: 15rpx;
438
+  font-weight: 500;
439
+}
440
+
441
+.required {
442
+  color: #f56c6c;
443
+  margin-left: 4rpx;
444
+}
445
+
446
+.brand-select-btn {
447
+  width: 100%;
448
+}
449
+
450
+.form-input {
451
+  width: 100%;
452
+  border: 2rpx solid #e0e0e0;
453
+  border-radius: 10rpx;
454
+  padding: 20rpx;
455
+  font-size: 28rpx;
456
+}
457
+</style>

+ 174 - 0
pages/orderDetailRefactoredOld/styles/common.scss

@@ -0,0 +1,174 @@
1
+// 公共SCSS变量定义
2
+$colors: (
3
+  primary: #108cff,
4
+  primary-light: rgba(16, 140, 255, 0.08),
5
+  bg: #f9fafb,
6
+  card: #ffffff,
7
+  border: #f5f5f5,
8
+  text-primary: #1f2937,
9
+  text-secondary: #374151,
10
+  shadow: rgba(0, 0, 0, 0.05)
11
+);
12
+
13
+$sizes: (
14
+  radius: 16rpx,
15
+  padding: 32rpx,
16
+  padding-sm: 28rpx,
17
+  margin-sm: 18rpx,
18
+  margin-xs: 16rpx,
19
+  icon-padding: 14rpx,
20
+  font-title: 34rpx,
21
+  font-content: 32rpx,
22
+  line-height: 56rpx,
23
+  checkbox-size: 32rpx
24
+);
25
+
26
+// 字体优化
27
+$font: (
28
+  family: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif',
29
+  smoothing: antialiased
30
+);
31
+
32
+// 字体大小变量
33
+$font-sizes: (
34
+  title: 38rpx,
35
+  content: 34rpx,
36
+  sub-content: 30rpx,
37
+  small: 26rpx,
38
+  tiny: 24rpx
39
+);
40
+
41
+// 字体权重变量
42
+$font-weights: (
43
+  bold: 700,
44
+  semi-bold: 600,
45
+  medium: 500,
46
+  regular: 450,
47
+  light: 400
48
+);
49
+
50
+// 行高变量
51
+$line-heights: (
52
+  large: 56rpx,
53
+  medium: 48rpx,
54
+  small: 40rpx
55
+);
56
+
57
+// 字间距变量
58
+$letter-spacings: (
59
+  large: 1rpx,
60
+  medium: 0.8rpx,
61
+  small: 0.5rpx
62
+);
63
+
64
+// 文本颜色变量
65
+$text-colors: (
66
+  primary: map-get($colors, text-primary),
67
+  secondary: map-get($colors, text-secondary),
68
+  tertiary: #666666,
69
+  placeholder: #999999
70
+);
71
+
72
+// 公共混合宏
73
+@mixin flex-center {
74
+  display: flex;
75
+  align-items: center;
76
+}
77
+
78
+@mixin shadow($level: 1) {
79
+  $shadow-levels: (
80
+    1: 0 2rpx 12rpx map-get($colors, shadow),
81
+    2: 0 4rpx 20rpx map-get($colors, shadow),
82
+    3: 0 8rpx 30rpx map-get($colors, shadow)
83
+  );
84
+  box-shadow: map-get($shadow-levels, $level);
85
+  transition: box-shadow 0.3s ease;
86
+}
87
+
88
+@mixin card {
89
+  width: 100%;
90
+  max-width: 100%;
91
+  margin: 0;
92
+  background-color: map-get($colors, card);
93
+  border-radius: map-get($sizes, radius);
94
+  box-sizing: border-box;
95
+  @include shadow;
96
+}
97
+
98
+// 字体样式混合宏
99
+@mixin font-styles(
100
+  $size: content,
101
+  $weight: regular,
102
+  $color: primary,
103
+  $line-height: medium,
104
+  $letter-spacing: medium
105
+) {
106
+  font-size: map-get($font-sizes, $size);
107
+  font-weight: map-get($font-weights, $weight);
108
+  color: map-get($text-colors, $color);
109
+  line-height: map-get($line-heights, $line-height);
110
+  letter-spacing: map-get($letter-spacings, $letter-spacing);
111
+  font-family: map-get($font, family);
112
+}
113
+
114
+// 公共页面容器样式
115
+.page-container {
116
+  box-sizing: border-box;
117
+  padding: 0;
118
+  background-color: map-get($colors, bg);
119
+  font-family: map-get($font, family);
120
+  -webkit-font-smoothing: map-get($font, smoothing);
121
+  font-smoothing: map-get($font, smoothing);
122
+}
123
+
124
+// 公共卡片样式
125
+.card-wrap {
126
+  @include card;
127
+  margin-bottom: 20rpx;
128
+  box-sizing: border-box;
129
+  max-width: 100%;
130
+  overflow: hidden;
131
+
132
+  &:hover {
133
+    @include shadow(2);
134
+  }
135
+}
136
+
137
+// 公共地址标题样式
138
+.address-header {
139
+  display: flex;
140
+  align-items: center;
141
+  margin-bottom: map-get($sizes, margin-sm);
142
+  padding-bottom: map-get($sizes, margin-sm);
143
+  border-bottom: 1rpx solid map-get($colors, border);
144
+
145
+  .location-icon {
146
+    margin-right: map-get($sizes, margin-xs);
147
+    background-color: map-get($colors, primary-light);
148
+    padding: map-get($sizes, icon-padding);
149
+    border-radius: 50%;
150
+    flex-shrink: 0;
151
+  }
152
+
153
+  .address-title {
154
+    margin-left: 16rpx;
155
+    @include font-styles($size: title, $weight: bold, $color: primary);
156
+  }
157
+
158
+  .add-button {
159
+    display: flex;
160
+    align-items: center;
161
+    justify-content: center;
162
+    padding: 8rpx 24rpx;
163
+    border: 1rpx solid #108cff;
164
+    border-radius: 40rpx;
165
+    background-color: transparent;
166
+    color: #108cff;
167
+    font-size: 24rpx;
168
+    cursor: pointer;
169
+    
170
+    text {
171
+      margin-left: 8rpx;
172
+    }
173
+  }
174
+}

+ 327 - 0
pages/orderDetailRefactoredOld/utils/imageUpload.js

@@ -0,0 +1,327 @@
1
+/**
2
+ * 图片上传和下载工具类
3
+ */
4
+export default {
5
+  /**
6
+   * 获取文件列表
7
+   * @param {String} type - 类型
8
+   * @param {String} orderFileType - 订单文件类型 (1:聊天记录, 2:实物图, 3:细节图)
9
+   * @param {String} receiptId - 收单ID
10
+   * @param {String} itemBrand - 物品品牌
11
+   * @param {String} clueId - 线索ID
12
+   */
13
+  async getFileList(type, orderFileType, receiptId, itemBrand, clueId) {
14
+    try {
15
+      const params = {
16
+        clueId,
17
+        sourceId: receiptId,
18
+        type,
19
+        orderFileType,
20
+        isDuplicate: '1',
21
+        pageNum: 1,
22
+        pageSize: 1000
23
+      }
24
+      const response = await uni.$u.api.selectClueFileByDto(params)
25
+      const rows = response.rows || []
26
+      
27
+      // 如果品牌包含逗号,说明是多个品牌,需要过滤
28
+      // if (itemBrand && itemBrand.indexOf(',') !== -1) {
29
+        return rows.filter(item => item.sourceId === receiptId)
30
+      // }
31
+      // return rows
32
+    } catch (error) {
33
+      console.error('获取文件列表失败:', error)
34
+      uni.$u.toast('获取文件列表失败')
35
+      return []
36
+    }
37
+  },
38
+
39
+  /**
40
+   * 选择图片
41
+   * @param {Number} count - 最多选择数量
42
+   * @returns {Promise<Array>} 图片路径数组
43
+   */
44
+  chooseImage(count = 9) {
45
+    return new Promise((resolve, reject) => {
46
+      uni.chooseImage({
47
+        count,
48
+        sizeType: ['compressed'],
49
+        sourceType: ['album', 'camera'],
50
+        success: (res) => {
51
+          resolve(res.tempFilePaths)
52
+        },
53
+        fail: (err) => {
54
+          console.error('选择图片失败:', err)
55
+          reject(err)
56
+        }
57
+      })
58
+    })
59
+  },
60
+
61
+  /**
62
+   * 上传文件
63
+   * @param {String} filePath - 文件路径
64
+   * @returns {Promise<Object>} 上传结果
65
+   */
66
+  async uploadFile(filePath) {
67
+    try {
68
+      uni.showLoading({
69
+        title: '上传中...',
70
+        mask: true
71
+      })
72
+      const { data } = await uni.$u.api.uploadFile(filePath)
73
+      return {
74
+        fileSize: data.fileSize,
75
+        fileSuffix: data.fileSuffix,
76
+        fileName: data.name,
77
+        fileUrl: data.url
78
+      }
79
+    } catch (error) {
80
+      console.error('文件上传失败:', error)
81
+      uni.$u.toast('上传失败,请重试')
82
+      throw error
83
+    } finally {
84
+      uni.hideLoading()
85
+    }
86
+  },
87
+
88
+  /**
89
+   * 批量上传文件
90
+   * @param {Array<String>} filePaths - 文件路径数组
91
+   * @returns {Promise<Array>} 上传结果数组
92
+   */
93
+  async uploadFiles(filePaths) {
94
+    try {
95
+      const uploadPromises = filePaths.map(filePath => this.uploadFile(filePath))
96
+      return await Promise.all(uploadPromises)
97
+    } catch (error) {
98
+      console.error('批量上传失败:', error)
99
+      throw error
100
+    }
101
+  },
102
+
103
+  /**
104
+   * 绑定订单文件
105
+   * @param {String} clueId - 线索ID
106
+   * @param {String} receiptId - 收单ID
107
+   * @param {String} orderFileType - 订单文件类型
108
+   * @param {Array} fileList - 文件列表
109
+   */
110
+  async bindOrderFile(clueId, receiptId, orderFileType, fileList) {
111
+    try {
112
+      const list = fileList.map(file => ({
113
+        fileSize: file.fileSize,
114
+        fileSuffix: file.fileSuffix,
115
+        fileName: file.fileName,
116
+        fileUrl: file.fileUrl,
117
+        orderFileType
118
+      }))
119
+      
120
+      await uni.$u.api.saveClueFile({
121
+        clueId,
122
+        list,
123
+        sourceId: receiptId,
124
+        type: '2',
125
+        orderFileType
126
+      })
127
+      uni.$u.toast('上传成功')
128
+    } catch (error) {
129
+      console.error('绑定订单文件失败:', error)
130
+      uni.$u.toast('上传失败')
131
+      throw error
132
+    }
133
+  },
134
+
135
+  /**
136
+   * 删除文件
137
+   * @param {String|Array} fileIds - 文件ID或ID数组
138
+   */
139
+  async deleteFile(fileIds) {
140
+    try {
141
+      const ids = Array.isArray(fileIds) ? fileIds : [fileIds]
142
+      await uni.$u.api.deleteClueFile(ids)
143
+      uni.showToast({
144
+        title: '删除成功',
145
+        icon: 'success',
146
+        duration: 2000
147
+      })
148
+    } catch (error) {
149
+      console.error('删除文件失败:', error)
150
+      uni.showToast({
151
+        title: '删除失败',
152
+        icon: 'error',
153
+        duration: 2000
154
+      })
155
+      throw error
156
+    }
157
+  },
158
+
159
+  /**
160
+   * 保存图片到本地相册
161
+   * @param {String} url - 图片URL
162
+   * @returns {Promise}
163
+   */
164
+  saveImageToLocal(url) {
165
+    return new Promise((resolve, reject) => {
166
+      this._doSaveImage(url, resolve, reject)
167
+    })
168
+  },
169
+
170
+  /**
171
+   * 执行保存图片操作
172
+   * @private
173
+   */
174
+  _doSaveImage(url, resolve, reject) {
175
+    uni.downloadFile({
176
+      url,
177
+      success: (res) => {
178
+        if (res.statusCode === 200 && res.tempFilePath) {
179
+          uni.saveImageToPhotosAlbum({
180
+            filePath: res.tempFilePath,
181
+            success: () => {
182
+              resolve()
183
+            },
184
+            fail: (err) => {
185
+              console.error('保存到相册失败:', err)
186
+              // 错误代码 12 通常表示权限问题
187
+              const isPermissionError = err.code === 12 || 
188
+                                       err.errCode === 12 || 
189
+                                       err.errMsg.includes('auth denied') || 
190
+                                       err.errMsg.includes('permission') ||
191
+                                       err.errMsg.includes('UNKOWN ERROR')
192
+              
193
+              if (isPermissionError) {
194
+                uni.showModal({
195
+                  title: '权限不足',
196
+                  content: '需要访问相册权限来保存图片,是否前往设置开启权限?',
197
+                  confirmText: '去设置',
198
+                  cancelText: '取消',
199
+                  success: (modalRes) => {
200
+                    if (modalRes.confirm) {
201
+                      uni.openSetting({
202
+                        success: (settingRes) => {
203
+                          // 检查是否开启了相册权限
204
+                          const hasPermission = settingRes.authSetting && 
205
+                                              settingRes.authSetting['scope.writePhotosAlbum']
206
+                          if (hasPermission) {
207
+                            uni.showToast({
208
+                              title: '权限已开启,请重试保存',
209
+                              icon: 'none',
210
+                              duration: 2000
211
+                            })
212
+                          } else {
213
+                            uni.showToast({
214
+                              title: '请在设置中开启相册权限',
215
+                              icon: 'none',
216
+                              duration: 2000
217
+                            })
218
+                          }
219
+                        },
220
+                        fail: () => {
221
+                          uni.showToast({
222
+                            title: '无法打开设置',
223
+                            icon: 'none'
224
+                          })
225
+                        }
226
+                      })
227
+                    }
228
+                  }
229
+                })
230
+              } else {
231
+                uni.showToast({
232
+                  title: '保存失败,请稍后重试',
233
+                  icon: 'none'
234
+                })
235
+              }
236
+              reject(err)
237
+            }
238
+          })
239
+        } else {
240
+          const errorMsg = `下载失败,状态码: ${res.statusCode}`
241
+          console.error(errorMsg)
242
+          uni.showToast({
243
+            title: '下载图片失败',
244
+            icon: 'none'
245
+          })
246
+          reject(new Error(errorMsg))
247
+        }
248
+      },
249
+      fail: (err) => {
250
+        console.error('下载图片失败:', err)
251
+        uni.showToast({
252
+          title: '下载图片失败,请检查网络',
253
+          icon: 'none'
254
+        })
255
+        reject(err)
256
+      }
257
+    })
258
+  },
259
+
260
+  /**
261
+   * 批量保存图片到本地
262
+   * @param {Array<String>} urls - 图片URL数组
263
+   */
264
+  async saveImagesToLocal(urls) {
265
+    try {
266
+      uni.showLoading({
267
+        title: '正在保存图片...',
268
+        mask: true
269
+      })
270
+
271
+      const savedImages = []
272
+      const failedImages = []
273
+
274
+      for (let i = 0; i < urls.length; i++) {
275
+        const url = urls[i]
276
+        try {
277
+          await this.saveImageToLocal(url)
278
+          savedImages.push(url)
279
+        } catch (error) {
280
+          console.error(`保存图片失败: ${url}`, error)
281
+          failedImages.push(url)
282
+        }
283
+
284
+        uni.showLoading({
285
+          title: `正在保存图片... (${i + 1}/${urls.length})`,
286
+          mask: true
287
+        })
288
+      }
289
+
290
+      uni.hideLoading()
291
+
292
+      let message = `成功保存 ${savedImages.length} 张图片`
293
+      if (failedImages.length > 0) {
294
+        message += `,${failedImages.length} 张保存失败`
295
+      }
296
+
297
+      uni.showToast({
298
+        title: message,
299
+        icon: 'none',
300
+        duration: 3000
301
+      })
302
+    } catch (error) {
303
+      uni.hideLoading()
304
+      console.error('保存图片过程中发生错误:', error)
305
+      uni.showToast({
306
+        title: '保存图片失败',
307
+        icon: 'error'
308
+      })
309
+    }
310
+  },
311
+
312
+  /**
313
+   * 复制图片链接
314
+   * @param {Array<String>} urls - 图片URL数组
315
+   */
316
+  copyImageUrls(urls) {
317
+    uni.setClipboardData({
318
+      data: JSON.stringify(urls),
319
+      success: () => {
320
+        uni.showToast({
321
+          title: '图片链接已复制',
322
+          icon: 'none'
323
+        })
324
+      }
325
+    })
326
+  }
327
+}