浏览代码

Merge branch 'master' of http://106.52.242.177:3000/askqvn/crm-app into priceCenter

zhangxin 1 月之前
父节点
当前提交
8697644ed2

+ 1 - 2
components/soundRecorder/soundRecorder.vue

@@ -42,8 +42,7 @@ export default {
42 42
     },
43 43
     methods: {
44 44
         handleDelete() {
45
-            console.log('点击了删除录音的按钮')
46
-            // this.$emit('delete', this.dataInner)
45
+            this.$emit('handleDelectThisSoundRecord', this.dataInner)
47 46
         }
48 47
     }
49 48
 

+ 10 - 2
pages.json

@@ -258,6 +258,14 @@
258 258
 			}
259 259
 		},
260 260
 		{
261
+			"path": "pages/orderDetailRefactored/index",
262
+			"style": {
263
+				"navigationBarTitleText": "",
264
+				"enablePullDownRefresh": true,
265
+				"navigationStyle": "custom"
266
+			}
267
+		},
268
+		{
261 269
 			"path": "pages/receiptForm/index",
262 270
 			"style": {
263 271
 				"navigationBarTitleText": "",
@@ -288,7 +296,7 @@
288 296
 			"path": "pages/pagereceivecenter/pagereceivecenter",
289 297
 			"style": {
290 298
 				"navigationBarTitleText": "接单中心",
291
-				"enablePullDownRefresh": false,
299
+				"enablePullDownRefresh": true,
292 300
 				"navigationBarBackgroundColor": "#108cff",
293 301
 				"navigationStyle": "custom"
294 302
 			}
@@ -369,7 +377,7 @@
369 377
 			},
370 378
 			{
371 379
 				"text": "接单中心",
372
-				"pagePath": "pages/order/index",
380
+				"pagePath": "pages/pagereceivecenter/pagereceivecenter",
373 381
 				"iconPath": "static/tabs/order.png",
374 382
 				"selectedIconPath": "static/tabs/order1.png",
375 383
 				"visible": false

+ 4 - 1
pages/clueDetail/page/clueDetail.vue

@@ -125,7 +125,7 @@
125 125
 				@click="handleAddFollow"></u-tabbar-item>
126 126
 				<u-tabbar-item text="上传录音" icon="../../static/caseDetail/icon-record.png"
127 127
 				@click="handleUploadRecord"></u-tabbar-item>
128
-				<u-tabbar-item text="分配" icon="../../static/clueDetail/icon-clue.png"
128
+				<u-tabbar-item text="分配" icon="../../static/clueDetail/icon-clue.png" v-if="isShowDistribution()"
129 129
 				@click="handleDistribution"></u-tabbar-item>
130 130
 		</u-tabbar>
131 131
 
@@ -209,6 +209,9 @@
209 209
 			}
210 210
 		},
211 211
 		methods: {
212
+			isShowDistribution() {
213
+				return this.$store.state.user.userInfo.roles.filter(i=>i.roleKey == 'ZHUGUANG-CRM').length > 0
214
+			},
212 215
 			distributionSuccess() {
213 216
 				this.handleInit();
214 217
 			},

+ 0 - 1
pages/orderDetailNew/components/common.scss

@@ -184,7 +184,6 @@ transition: box-shadow 0.3s ease;
184 184
 // 公共地址标题样式
185 185
 .address-header {
186 186
     display: flex;
187
-    justify-content: space-between;
188 187
     align-items: center;
189 188
     margin-bottom: map-get($sizes, margin-sm);
190 189
     padding-bottom: map-get($sizes, margin-sm);

+ 0 - 1
pages/orderDetailNew/components/pageFour.vue

@@ -991,7 +991,6 @@ export default {
991 991
 .address-header {
992 992
     display: flex;
993 993
     align-items: center;
994
-    justify-content: space-between;
995 994
     margin-bottom: 20rpx;
996 995
 
997 996
     .address-title {

+ 49 - 17
pages/orderDetailNew/components/pageOne.vue

@@ -10,21 +10,7 @@
10 10
             </u-col>
11 11
         </u-row>
12 12
 
13
-        <!-- 实物图卡片 -->
14
-        <view class="card_wrap">
15
-            <view class="card-title">实物图</view>
16
-            <view class="image-upload-container">
17
-                <view class="image-list">
18
-                    <view class="image-item" v-for="(item, index) in trueUploadList" :key="'truePic-' + index">
19
-                        <pic-comp @needPreviewPic='previewImageTrue' :src="item.fileUrl"></pic-comp>
20
-                        <view class="delete-btn" @click="deleteImage(item)">×</view>
21
-                    </view>
22
-                    <view class="upload-btn" @click="uploadImage('truePic', currentReceipt.id)">
23
-                        <u-icon name="plus" size="40" color="#999"></u-icon>
24
-                    </view>
25
-                </view>
26
-            </view>
27
-        </view>
13
+        
28 14
 
29 15
         <!-- 聊天记录卡片 -->
30 16
         <view class="card_wrap">
@@ -50,13 +36,32 @@
50 36
             <view class="image-upload-container" v-if="chatRecordOrCallRecord === 'callRecords'">
51 37
                 <view>
52 38
 
53
-                    <sound-recorder v-for="item in soundRecordList" :key="item.fileName" :data="item"></sound-recorder>
39
+                    <sound-recorder v-for="item in soundRecordList" :key="item.fileName" :data="item"
40
+                        @handleDelectThisSoundRecord='handleDelectThisSoundRecord'></sound-recorder>
41
+                    <!-- <view class="add-btn" @click="addCallRecord">添加通话记录</view> -->
54 42
 
55 43
                 </view>
56 44
 
57 45
             </view>
58 46
         </view>
59 47
 
48
+        <!-- 实物图卡片 -->
49
+        <view class="card_wrap">
50
+            <view class="card-title">实物图</view>
51
+            <view class="image-upload-container">
52
+                <view class="image-list">
53
+                    <view class="image-item" v-for="(item, index) in trueUploadList" :key="'truePic-' + index">
54
+                        <pic-comp @needPreviewPic='previewImageTrue' :src="item.fileUrl"></pic-comp>
55
+                        <view class="delete-btn" @click="deleteImage(item)">×</view>
56
+                    </view>
57
+                    <view class="upload-btn" @click="uploadImage('truePic', currentReceipt.id)">
58
+                        <u-icon name="plus" size="40" color="#999"></u-icon>
59
+                    </view>
60
+                </view>
61
+            </view>
62
+        </view>
63
+        
64
+
60 65
         <!-- 基本信息卡片 -->
61 66
         <view class="info-card">
62 67
             <view class="info-card-title">基本信息</view>
@@ -421,7 +426,34 @@ export default {
421 426
             console.log('通话记录:', data);
422 427
             this.soundRecordList = data;
423 428
         },
424
-
429
+        //删除录音
430
+        handleDelectThisSoundRecord({ id }) {
431
+            console.log('当前需要删除的录音的id是', id)
432
+            uni.showModal({
433
+                title: '提示',
434
+                content: '是否确定删除?',
435
+                confirmColor: '#6cc040',
436
+                success: async (res) => {
437
+                    if (res.confirm) {
438
+                        try {
439
+                            await uni.$u.api.deleteClueFile([id])
440
+                            uni.showToast({
441
+                                title: '删除成功',
442
+                                icon: 'success',
443
+                                duration: 2000
444
+                            })
445
+                            this.getData()
446
+                        } catch (error) {
447
+                            uni.showToast({
448
+                                title: '删除失败',
449
+                                icon: 'error',
450
+                                duration: 2000
451
+                            })
452
+                        }
453
+                    }
454
+                }
455
+            })
456
+        },
425 457
     },
426 458
 
427 459
 }

+ 6 - 6
pages/orderDetailNew/mixin/imgUploadAndDownLoad.js

@@ -48,15 +48,15 @@ export default {
48 48
                     pageSize: 1000 // 设置一个较大的值以获取所有数据
49 49
                 }
50 50
                 const response = await uni.$u.api.selectClueFileByDto(params)
51
-                if (orderFileType == '1') {
51
+                //聊天记录1,实物图2,细节图3
52
+                if (orderFileType == '2') {
52 53
                     //如果itemBrand里面有逗号的话说明是多个品牌
53
-
54 54
                     if (itemBrand.indexOf(',') != -1) {
55 55
                         this.trueUploadList = response.rows.filter(item => item.sourceId == receiptID) || []
56 56
                     } else {
57 57
                         this.trueUploadList = response.rows
58 58
                     }
59
-                } else if (orderFileType == '2') {
59
+                } else if (orderFileType == '1') {
60 60
                     if (itemBrand.indexOf(',') != -1) {
61 61
                         this.chatRecordsUploadList = response.rows.filter(item => item.sourceId == receiptID) || []
62 62
                     } else {
@@ -96,8 +96,8 @@ export default {
96 96
         async uploadToServer(filePaths, type, receiptID) {
97 97
             // 实际的上传逻辑
98 98
             try {
99
-                //实物图的话就是1,聊天记录的话就是2,高清细节图的话就是3
100
-                let p = type == 'truePic' ? '1' : type == 'chatRecords' ? '2' : type == 'detailImages' ? '3' : ''
99
+                //聊天记录的话就是1,实物图的话就是2,高清细节图的话就是3
100
+                let p = type == 'truePic' ? '2' : type == 'chatRecords' ? '1' : type == 'detailImages' ? '3' : ''
101 101
                 console.log('当前上传的图片类型是', p)
102 102
                 const res = await Promise.all(filePaths.map(filePath => this.uploadFile(filePath, p)));
103 103
 
@@ -141,7 +141,7 @@ export default {
141 141
         },
142 142
         //绑定订单
143 143
         async bindOrder(orderFileType, receiptID) {
144
-            console.log('当前的收单id', receiptID)
144
+            console.log('当前的收单id', receiptID,'当前的订单文件类型', orderFileType)
145 145
             const res = await uni.$u.api.saveClueFile({
146 146
                 clueId: this.orderDetail.clueId,//线索id
147 147
                 list: this.bindList,

+ 130 - 0
pages/orderDetailRefactored/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/orderDetailRefactored/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>

+ 318 - 0
pages/orderDetailRefactored/components/OrderDetailView.vue

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

+ 945 - 0
pages/orderDetailRefactored/components/PageFour.vue

@@ -0,0 +1,945 @@
1
+<template>
2
+  <view class="page-four-container">
3
+    <!-- 入库信息卡片 -->
4
+    <view class="card-wrap">
5
+      <u--form labelPosition="top" :model="warehouseInfo" ref="form" class="address-section">
6
+        <view class="address-header">
7
+          <u-icon name="car-fill" size="36rpx" color="#108cff" class="location-icon" />
8
+          <text class="address-title">入库信息</text>
9
+        </view>
10
+
11
+        <!-- 编码、快递单号、物流图片 -->
12
+
13
+
14
+        <!-- 收单物品、收单类型 -->
15
+        <u-row class="info-row" justify="space-between">
16
+          <u-col span="5.8">
17
+            <u-form-item label="收单物品" prop="item">
18
+              <u--input v-model="warehouseInfo.item" placeholder="请输入收单物品" class="info-input" />
19
+            </u-form-item>
20
+          </u-col>
21
+          <u-col span="5.8">
22
+            <view @tap="selectCustomerServiceName">
23
+              <u-form-item label="收单类型" prop="customerServiceNameLabel">
24
+                <view class="click-wrapper info-input">
25
+                  {{ warehouseInfo.customerServiceNameLabel || '点击选择收单类型' }}
26
+                </view>
27
+              </u-form-item>
28
+              <u-picker :show="showCustomerServicePicker" :columns="customerServiceColumns" confirm keyName="label"
29
+                @confirm="handleConfirmCustomerService" @cancel="showCustomerServicePicker = false" />
30
+            </view>
31
+          </u-col>
32
+        </u-row>
33
+
34
+        <!-- 类别、是否需要查码 -->
35
+        <u-row class="info-row" justify="space-between">
36
+          <u-col span="5.8">
37
+            <view @tap="selectCategory">
38
+              <u-form-item label="类别" prop="category">
39
+                <view class="click-wrapper info-input">
40
+                  {{ warehouseInfo.categoryLabel || '点击选择类别' }}
41
+                </view>
42
+              </u-form-item>
43
+              <u-picker :show="showCategoryPicker" :columns="categoryColumns" confirm keyName="label"
44
+                @confirm="handleConfirmCategory" @cancel="showCategoryPicker = false" />
45
+            </view>
46
+          </u-col>
47
+          <u-col span="5.8">
48
+            <view @tap="selectNeedCheckCode">
49
+              <u-form-item label="是否需要查码" prop="needCheckCode">
50
+                <view class="click-wrapper info-input">
51
+                  {{ warehouseInfo.needCheckCodeLabel || '点击选择是否需要查码' }}
52
+                </view>
53
+              </u-form-item>
54
+              <u-picker :show="showNeedCheckCodePicker" :columns="needCheckCodeColumns" confirm keyName="label"
55
+                @confirm="handleConfirmNeedCheckCode" @cancel="showNeedCheckCodePicker = false" />
56
+            </view>
57
+          </u-col>
58
+        </u-row>
59
+
60
+
61
+        <u-row class="info-row" justify="space-between">
62
+          <u-col span="4.5">
63
+            <u-form-item label="编码" prop="codeStorage">
64
+              <u--input v-model="warehouseInfo.codeStorage" placeholder="请输入编码" class="info-input"
65
+                :disabled="warehouseInfo.needCheckCode === '2'" />
66
+            </u-form-item>
67
+          </u-col>
68
+          <u-col span="4.5">
69
+            <u-form-item label="快递单号" prop="expressOrderNo">
70
+              <u--input v-model="warehouseInfo.expressOrderNo" placeholder="请输入快递单号" class="info-input" />
71
+            </u-form-item>
72
+          </u-col>
73
+          <u-col span="2">
74
+            <u-form-item label="物流图片" prop="uploadedImage">
75
+              <view class="image-uploader" @click="selectImage">
76
+                <u-icon v-if="!warehouseInfo.uploadedImage" name="camera-fill" size="48rpx" color="#909399"
77
+                  class="camera-icon" />
78
+                <image v-else :src="warehouseInfo.uploadedImage" mode="aspectFill" class="image-preview" />
79
+              </view>
80
+            </u-form-item>
81
+          </u-col>
82
+        </u-row>
83
+
84
+        <!-- 表款、查码费 -->
85
+        <u-row class="info-row" justify="space-between">
86
+          <u-col span="5.8">
87
+            <u-form-item label="表款">
88
+              <u--input v-model="warehouseInfo.watchPrice" placeholder="请输入表款" class="info-input" type="number" />
89
+            </u-form-item>
90
+          </u-col>
91
+          <u-col span="5.8">
92
+            <u-form-item label="查码费">
93
+              <u--input v-model="warehouseInfo.checkCodeFee" placeholder="请输入查码费" class="info-input" type="number"
94
+                :disabled="warehouseInfo.needCheckCode === '2'" />
95
+            </u-form-item>
96
+          </u-col>
97
+        </u-row>
98
+
99
+        <!-- 好处费、运费 -->
100
+        <u-row class="info-row" justify="space-between">
101
+          <u-col span="5.8">
102
+            <u-form-item label="好处费">
103
+              <u--input v-model="warehouseInfo.benefitFee" placeholder="请输入好处费" class="info-input" type="number" />
104
+            </u-form-item>
105
+          </u-col>
106
+          <u-col span="5.8">
107
+            <u-form-item label="运费">
108
+              <u--input v-model="warehouseInfo.freight" placeholder="请输入运费" class="info-input" type="number" />
109
+            </u-form-item>
110
+          </u-col>
111
+        </u-row>
112
+
113
+        <!-- 维修金额、分单比例 -->
114
+        <u-row class="info-row" justify="space-between">
115
+          <u-col span="5.8">
116
+            <u-form-item label="维修金额">
117
+              <u--input v-model="warehouseInfo.repairAmount" placeholder="请输入维修金额" class="info-input" type="number" />
118
+            </u-form-item>
119
+          </u-col>
120
+          <u-col span="5.8">
121
+            <u-form-item label="分单比例(0~100)">
122
+              <u--input v-model="warehouseInfo.splitRatio" placeholder="请输入分单比例(0~100)" class="info-input"
123
+                type="number" />
124
+            </u-form-item>
125
+          </u-col>
126
+        </u-row>
127
+
128
+        <!-- 成本合计、业绩、毛业绩 -->
129
+        <u-row class="info-row" justify="space-between">
130
+          <u-col span="3.8">
131
+            <u-form-item label="成本合计">
132
+              <u--input :disabled="true" :value="computedTotalCost" placeholder="成本合计自动计算" class="info-input"
133
+                type="number" />
134
+            </u-form-item>
135
+          </u-col>
136
+          <u-col span="3.8">
137
+            <u-form-item label="业绩">
138
+              <u--input :disabled="true" :value="computedPerformance" placeholder="业绩自动计算" class="info-input"
139
+                type="number" />
140
+            </u-form-item>
141
+          </u-col>
142
+          <u-col span="3.8">
143
+            <u-form-item label="毛业绩">
144
+              <u--input :disabled="true" :value="computedGrossPerformance" placeholder="毛业绩自动计算" class="info-input"
145
+                type="number" />
146
+            </u-form-item>
147
+          </u-col>
148
+        </u-row>
149
+
150
+        <!-- 收单备注 -->
151
+        <u-row class="info-row">
152
+          <u-col span="12">
153
+            <u-form-item label="收单备注">
154
+              <u--textarea v-model="warehouseInfo.remarks" placeholder="请输入收单备注" class="info-textarea"
155
+                confirmType="done" rows="4" />
156
+            </u-form-item>
157
+          </u-col>
158
+        </u-row>
159
+      </u--form>
160
+    </view>
161
+
162
+    <!-- 分成信息卡片 -->
163
+    <view class="card-wrap">
164
+      <view class="address-section">
165
+        <view class="address-header add-button-container">
166
+          <text class="address-title">分成信息</text>
167
+
168
+          <view class="add-button" @click="addSplit">
169
+            <u-icon name="plus" size="24rpx" color="#108cff" />
170
+            <text>添加</text>
171
+          </view>
172
+        </view>
173
+
174
+        <!-- 分成信息表格 -->
175
+        <view class="split-table">
176
+          <u-row class="split-table-header">
177
+            <u-col span="4"><text class="header-text">关联</text></u-col>
178
+            <u-col span="2"><text class="header-text">分成人</text></u-col>
179
+            <u-col span="2"><text class="header-text">比例</text></u-col>
180
+            <u-col span="1"><text class="header-text">类型</text></u-col>
181
+            <u-col span="1"><text class="header-text">公司</text></u-col>
182
+            <u-col span="2" class="action-column"><text class="header-text">操作</text></u-col>
183
+          </u-row>
184
+          <u-row v-for="(item, index) in profitSharingList" :key="item.uuid" class="split-table-row">
185
+            <u-col span="4">
186
+              <view class="table-cell">
187
+                <u-button @click="handleSelectOrg(item)" :text="item.orgName || '选择组织'" plain />
188
+              </view>
189
+            </u-col>
190
+            <u-col span="2">
191
+              <view class="table-cell">
192
+                <u-button @click="handleSelectPerson(item)" :text="item.userName || '选择人员'" plain />
193
+              </view>
194
+            </u-col>
195
+            <u-col span="2">
196
+              <view class="table-cell">
197
+                <u--input v-model="item.commissionRate" type="number" class="percentage-input"
198
+                  @input="handlePercentageInput(item)" min="0" max="100" precision="0" />
199
+              </view>
200
+            </u-col>
201
+            <u-col span="1">
202
+              <view class="table-cell">
203
+                <view :class="['account-type', item.accountType == '1' ? 'frontend' : 'backend']"
204
+                  @click="toggleAccountType(item)">
205
+                  {{ item.accountType == '1' ? '前' : '后' }}
206
+                </view>
207
+              </view>
208
+            </u-col>
209
+            <u-col span="1">
210
+              <view class="table-cell">
211
+                <view class="radio-wrapper" @click="toggleBelongToCompany(item)">
212
+                  <view :class="['radio-circle', item.isCompanyPerformance == '1' ? 'active' : '']">
213
+                    <u-icon v-if="item.isCompanyPerformance == '1'" name="checkmark" size="20rpx" color="#fff" />
214
+                  </view>
215
+                </view>
216
+              </view>
217
+            </u-col>
218
+            <u-col span="2" class="action-column">
219
+              <view class="table-cell">
220
+                <u-button type="error" plain shape="circle" size="mini" @click="deleteRow(item.id, item.uuid)"
221
+                  class="delete-btn">
222
+                  <u-icon name="trash" size="20rpx" color="#ff6b6b" />
223
+                </u-button>
224
+              </view>
225
+            </u-col>
226
+          </u-row>
227
+        </view>
228
+      </view>
229
+    </view>
230
+
231
+    <!-- 确认入库按钮 -->
232
+    <view class="confirm-button-container">
233
+      <u-button class="next-btn" type="success" @click="confirmWarehouseEntry">
234
+        <u-icon name="checkmark-circle-fill" size="28rpx" color="#fff" />
235
+        <text style="margin-left: 8rpx;">确认入库</text>
236
+      </u-button>
237
+    </view>
238
+
239
+    <!-- 组织选择器 -->
240
+    <u-picker :show="showOrgPicker" title="请选择组织" :columns="columnsOrgList" keyName="label" @confirm="handleOrgConfirm"
241
+      @cancel="showOrgPicker = false" />
242
+
243
+    <!-- 人员选择器 -->
244
+    <u-picker :show="showPersonPicker" title="请选择分成人" :columns="columnsPersonList" keyName="label"
245
+      @confirm="handleConfirmPerson" @cancel="handleCancelPerson" />
246
+
247
+    <u-toast ref="uToast" />
248
+  </view>
249
+</template>
250
+
251
+<script>
252
+import imageUpload from '../utils/imageUpload.js'
253
+
254
+export default {
255
+  name: 'PageFour',
256
+  props: {
257
+    orderDetail: {
258
+      type: Object,
259
+      default: () => ({})
260
+    },
261
+    currentReceipt: {
262
+      type: Object,
263
+      default: () => ({})
264
+    }
265
+  },
266
+  data() {
267
+    return {
268
+      warehouseInfo: {
269
+        codeStorage: '',
270
+        expressOrderNo: '',
271
+        uploadedImage: '',
272
+        item: '',
273
+        checkCodeFee: '',
274
+        watchPrice: '',
275
+        benefitFee: '',
276
+        freight: '',
277
+        repairAmount: '',
278
+        grossPerformance: '',
279
+        performance: '',
280
+        splitRatio: '',
281
+        remarks: '',
282
+        customerServiceNameLabel: '',
283
+        customerServiceName: '',
284
+        categoryLabel: '',
285
+        category: '',
286
+        needCheckCodeLabel: '',
287
+        needCheckCode: ''
288
+      },
289
+      profitSharingList: [],
290
+      showOrgPicker: false,
291
+      showPersonPicker: false,
292
+      columnsOrgList: [],
293
+      columnsPersonList: [],
294
+      showCustomerServicePicker: false,
295
+      customerServiceColumns: [[
296
+        { label: '收单类', value: '1' },
297
+        { label: '维修类', value: '2' },
298
+        { label: '销售类', value: '3' }
299
+      ]],
300
+      showCategoryPicker: false,
301
+      categoryColumns: [[
302
+        { label: '腕表', value: '1' },
303
+        { label: '包包', value: '2' },
304
+        { label: '首饰', value: '4' },
305
+        { label: '其他', value: '3' }
306
+      ]],
307
+      showNeedCheckCodePicker: false,
308
+      needCheckCodeColumns: [[
309
+        { label: '是', value: '1' },
310
+        { label: '否', value: '2' }
311
+      ]],
312
+      currentEditItem: ''
313
+    }
314
+  },
315
+  computed: {
316
+    computedTotalCost() {
317
+      const freight = Number(this.warehouseInfo.freight) || 0
318
+      const benefitFee = Number(this.warehouseInfo.benefitFee) || 0
319
+      const checkCodeFee = Number(this.warehouseInfo.checkCodeFee) || 0
320
+      const watchPrice = Number(this.warehouseInfo.watchPrice) || 0
321
+      const repairAmount = Number(this.warehouseInfo.repairAmount) || 0
322
+      return freight + benefitFee + checkCodeFee + watchPrice + repairAmount
323
+    },
324
+    computedPerformance() {
325
+      const sellingPrice = Number(this.currentReceipt.sellingPrice) || 0
326
+      return sellingPrice - this.computedTotalCost
327
+    },
328
+    computedGrossPerformance() {
329
+      const performance = this.computedPerformance || 0
330
+      const splitRatio = Number(this.warehouseInfo.splitRatio) / 100 || 0
331
+      return (performance * splitRatio).toFixed(2)
332
+    }
333
+  },
334
+  watch: {
335
+    currentReceipt: {
336
+      handler(newVal) {
337
+        if (newVal) {
338
+          this.initWarehouseInfo(newVal)
339
+          this.loadShareList()
340
+        }
341
+      },
342
+      immediate: true,
343
+      deep: true
344
+    }
345
+  },
346
+  mounted() {
347
+    this.loadCommissionUserList()
348
+  },
349
+  methods: {
350
+    /**
351
+     * 初始化入库信息
352
+     */
353
+    initWarehouseInfo(data) {
354
+      const receiptRemark = data.receiptRemark || ''
355
+      const remarkParts = receiptRemark.split(';')
356
+
357
+      // 设置默认值:收单类型默认为"收单类",类别默认为"腕表",是否需要查码默认为"是"
358
+      const defaultCustomerServiceName = data.customerServiceName || '1'
359
+      const defaultCategory = data.category || '1'
360
+      const defaultNeedCheckCode = data.needCheckCode || '1'
361
+
362
+      this.warehouseInfo = {
363
+        codeStorage: data.code || '',
364
+        expressOrderNo: data.expressOrderNo || '',
365
+        uploadedImage: remarkParts[1] || '',
366
+        item: data.item || '',
367
+        checkCodeFee: data.checkCodeFee || '',
368
+        watchPrice: data.tableFee || '',
369
+        benefitFee: data.benefitFee || '',
370
+        freight: data.freight || '',
371
+        repairAmount: data.repairAmount || '',
372
+        grossPerformance: data.grossPerformance || '',
373
+        performance: data.performance || '',
374
+        splitRatio: data.splitRatio || '',
375
+        remarks: remarkParts[0] || '',
376
+        customerServiceName: defaultCustomerServiceName,
377
+        customerServiceNameLabel: this.getLabelByValue(
378
+          this.customerServiceColumns[0],
379
+          defaultCustomerServiceName
380
+        ),
381
+        category: defaultCategory,
382
+        categoryLabel: this.getLabelByValue(
383
+          this.categoryColumns[0],
384
+          defaultCategory
385
+        ),
386
+        needCheckCode: defaultNeedCheckCode,
387
+        needCheckCodeLabel: this.getLabelByValue(
388
+          this.needCheckCodeColumns[0],
389
+          defaultNeedCheckCode
390
+        )
391
+      }
392
+    },
393
+
394
+    /**
395
+     * 根据值获取标签
396
+     */
397
+    getLabelByValue(columns, value) {
398
+      const item = columns.find(col => col.value == value)
399
+      return item ? item.label : ''
400
+    },
401
+
402
+    /**
403
+     * 选择物流图片
404
+     */
405
+    async selectImage() {
406
+      try {
407
+        const tempFilePaths = await imageUpload.chooseImage(1)
408
+        if (!tempFilePaths || tempFilePaths.length === 0) {
409
+          uni.$u.toast('未获取到图片路径,请重试')
410
+          return
411
+        }
412
+        const tempFilePath = tempFilePaths[0]
413
+        const rep = await uni.$u.api.uploadFile(tempFilePath)
414
+        if (rep.code == 200) {
415
+          this.warehouseInfo.uploadedImage = rep.data.url
416
+        }
417
+      } catch (error) {
418
+        console.error('上传图片失败:', error)
419
+      }
420
+    },
421
+
422
+    /**
423
+     * 加载分成人名单
424
+     */
425
+    async loadCommissionUserList() {
426
+      try {
427
+        const res = await uni.$u.api.getCustomerManagerAllList()
428
+        this.columnsOrgList = [res.data[0].children]
429
+      } catch (error) {
430
+        console.error('加载分成人名单失败:', error)
431
+      }
432
+    },
433
+
434
+    /**
435
+     * 加载分成列表
436
+     */
437
+    async loadShareList() {
438
+      if (!this.currentReceipt.sendFormId) return
439
+
440
+      try {
441
+        const { rows } = await uni.$u.api.selectCommissionList({
442
+          pageSize: 9999,
443
+          pageNum: 1
444
+        }, { sendFormId: this.currentReceipt.sendFormId })
445
+
446
+        const newRows = rows
447
+          .filter(item => item.receiptFormId == this.currentReceipt.id)
448
+          .map(item => ({
449
+            ...item,
450
+            uuid: Math.random()
451
+          }))
452
+        this.profitSharingList = newRows
453
+      } catch (error) {
454
+        console.error('加载分成列表失败:', error)
455
+      }
456
+    },
457
+
458
+    /**
459
+     * 添加分成行
460
+     */
461
+    addSplit() {
462
+      this.profitSharingList.push({
463
+        deptId: '',
464
+        accountType: '1',
465
+        userId: '',
466
+        commissionRate: 0,
467
+        isCompanyPerformance: '2',
468
+        orgName: '',
469
+        userName: '',
470
+        id: '',
471
+        uuid: Math.random()
472
+      })
473
+      this.calculateTotalPercentage()
474
+    },
475
+
476
+    /**
477
+     * 重新计算分成比例
478
+     */
479
+    calculateTotalPercentage() {
480
+      const frontItems = this.profitSharingList.filter(item => item.accountType == '1')
481
+      const backItems = this.profitSharingList.filter(item => item.accountType == '2')
482
+      const totalFrontItems = frontItems.length || 1
483
+      const totalBackItems = backItems.length || 1
484
+
485
+      this.profitSharingList.forEach(item => {
486
+        if (item.accountType == '1') {
487
+          item.commissionRate = Math.floor(100 / totalFrontItems)
488
+        }
489
+        if (item.accountType == '2') {
490
+          item.commissionRate = Math.floor(100 / totalBackItems)
491
+        }
492
+      })
493
+    },
494
+
495
+    /**
496
+     * 切换账户类型
497
+     */
498
+    toggleAccountType(item) {
499
+      item.accountType = item.accountType == '1' ? '2' : '1'
500
+      this.calculateTotalPercentage()
501
+    },
502
+
503
+    /**
504
+     * 处理百分比输入
505
+     */
506
+    handlePercentageInput(item) {
507
+      let value = Number(item.commissionRate)
508
+      if (isNaN(value)) value = 0
509
+      if (value < 0) value = 0
510
+      if (value > 100) value = 100
511
+      item.commissionRate = Math.floor(value)
512
+    },
513
+
514
+    /**
515
+     * 切换归属公司
516
+     */
517
+    toggleBelongToCompany(item) {
518
+      item.isCompanyPerformance = item.isCompanyPerformance == '1' ? '2' : '1'
519
+    },
520
+
521
+    /**
522
+     * 选择组织
523
+     */
524
+    handleSelectOrg(item) {
525
+      this.currentEditItem = item.uuid
526
+      this.showPersonPicker = true
527
+      this.columnsPersonList = this.columnsOrgList
528
+    },
529
+
530
+    /**
531
+     * 选择人员
532
+     */
533
+    handleSelectPerson(item) {
534
+      this.currentEditItem = item.uuid
535
+      const deptId = item.deptId
536
+      const org = this.findOrg(this.columnsOrgList[0], deptId)
537
+      if (org) {
538
+        this.columnsPersonList = [org.children]
539
+      }
540
+      this.showPersonPicker = true
541
+    },
542
+
543
+    /**
544
+     * 递归查找组织
545
+     */
546
+    findOrg(orgList, deptId) {
547
+      for (const org of orgList) {
548
+        if (org.id == deptId) {
549
+          return org
550
+        }
551
+        if (org.children && org.children.length > 0) {
552
+          const found = this.findOrg(org.children, deptId)
553
+          if (found) return found
554
+        }
555
+      }
556
+      return null
557
+    },
558
+
559
+    /**
560
+     * 确认选择组织
561
+     */
562
+    handleOrgConfirm({ value }) {
563
+      this.profitSharingList.forEach(item => {
564
+        if (item.uuid == this.currentEditItem) {
565
+          item.orgName = value[0].label
566
+          item.deptId = value[0].id
567
+          item.userId = ''
568
+          item.userName = ''
569
+        }
570
+      })
571
+      this.showOrgPicker = false
572
+    },
573
+
574
+    /**
575
+     * 确认选择人员
576
+     */
577
+    handleConfirmPerson({ value }) {
578
+      if (value[0].isUser) {
579
+        this.profitSharingList.forEach(item => {
580
+          if (item.uuid == this.currentEditItem) {
581
+            item.userName = value[0].label
582
+            item.userId = value[0].id
583
+          }
584
+        })
585
+        this.columnsPersonList = []
586
+        this.showPersonPicker = false
587
+      } else {
588
+        this.profitSharingList.forEach(item => {
589
+          if (item.uuid == this.currentEditItem) {
590
+            item.orgName = value[0].label
591
+            item.deptId = value[0].id
592
+          }
593
+        })
594
+        this.columnsPersonList = [value[0].children]
595
+      }
596
+    },
597
+
598
+    /**
599
+     * 取消选择人员
600
+     */
601
+    handleCancelPerson() {
602
+      this.columnsPersonList = []
603
+      this.showPersonPicker = false
604
+    },
605
+
606
+    /**
607
+     * 删除分成行
608
+     */
609
+    async deleteRow(id, uuid) {
610
+      uni.showModal({
611
+        title: '确认删除',
612
+        content: '是否确认删除当前行分成比例?',
613
+        success: async (res) => {
614
+          if (res.confirm) {
615
+            if (!id) {
616
+              this.profitSharingList = this.profitSharingList.filter(item => item.uuid != uuid)
617
+              this.calculateTotalPercentage()
618
+              return
619
+            }
620
+            try {
621
+              await uni.$u.api.deleteClueCommissionForm(id)
622
+              uni.$u.toast('删除成功')
623
+              this.loadShareList()
624
+            } catch (error) {
625
+              uni.$u.toast('删除失败')
626
+            }
627
+          }
628
+        }
629
+      })
630
+    },
631
+
632
+    /**
633
+     * 选择收单类型
634
+     */
635
+    selectCustomerServiceName() {
636
+      this.showCustomerServicePicker = true
637
+    },
638
+
639
+    /**
640
+     * 确认收单类型
641
+     */
642
+    handleConfirmCustomerService({ value }) {
643
+      this.warehouseInfo.customerServiceNameLabel = value[0].label
644
+      this.warehouseInfo.customerServiceName = value[0].value
645
+      this.showCustomerServicePicker = false
646
+    },
647
+
648
+    /**
649
+     * 选择类别
650
+     */
651
+    selectCategory() {
652
+      this.showCategoryPicker = true
653
+    },
654
+
655
+    /**
656
+     * 确认类别
657
+     */
658
+    handleConfirmCategory({ value }) {
659
+      this.warehouseInfo.categoryLabel = value[0].label
660
+      this.warehouseInfo.category = value[0].value
661
+      this.showCategoryPicker = false
662
+    },
663
+
664
+    /**
665
+     * 选择是否需要查码
666
+     */
667
+    selectNeedCheckCode() {
668
+      this.showNeedCheckCodePicker = true
669
+    },
670
+
671
+    /**
672
+     * 确认是否需要查码
673
+     */
674
+    handleConfirmNeedCheckCode({ value }) {
675
+      this.warehouseInfo.needCheckCodeLabel = value[0].label
676
+      this.warehouseInfo.needCheckCode = value[0].value
677
+      // 如果选择"否",清空编码和查码费
678
+      if (value[0].value === '2') {
679
+        this.warehouseInfo.codeStorage = ''
680
+        this.warehouseInfo.checkCodeFee = ''
681
+      }
682
+      this.showNeedCheckCodePicker = false
683
+    },
684
+
685
+    /**
686
+     * 确认入库
687
+     */
688
+    async confirmWarehouseEntry() {
689
+      uni.showModal({
690
+        title: '确认入库',
691
+        content: `是否确认入库该订单:${this.orderDetail.item}?`,
692
+        success: async (res) => {
693
+          if (res.confirm) {
694
+            try {
695
+              // 修改订单状态
696
+              await uni.$u.api.oderForm({
697
+                status: '3',
698
+                id: this.orderDetail.id
699
+              })
700
+
701
+              // 更新收单表单
702
+              await uni.$u.api.updateReceiptForm({
703
+                id: this.currentReceipt.id,
704
+                code: this.warehouseInfo.codeStorage || '',
705
+                expressOrderNo: this.warehouseInfo.expressOrderNo || '',
706
+                item: this.warehouseInfo.item || '',
707
+                checkCodeFee: this.warehouseInfo.checkCodeFee || '',
708
+                tableFee: this.warehouseInfo.watchPrice || '',
709
+                benefitFee: this.warehouseInfo.benefitFee || '',
710
+                freight: this.warehouseInfo.freight || '',
711
+                repairAmount: this.warehouseInfo.repairAmount || '',
712
+                grossPerformance: this.computedGrossPerformance || '',
713
+                performance: this.computedPerformance || '',
714
+                splitRatio: this.warehouseInfo.splitRatio || '',
715
+                receiptRemark: `${this.warehouseInfo.remarks || ''};${this.warehouseInfo.uploadedImage || ''}`,
716
+                customerServiceName: this.warehouseInfo.customerServiceName || '',
717
+                category: this.warehouseInfo.category || '',
718
+                needCheckCode: this.warehouseInfo.needCheckCode || '',
719
+                totalCost: this.computedTotalCost || ''
720
+              })
721
+
722
+              // 上传分成数据
723
+              await this.addShare()
724
+
725
+              this.$refs.uToast.show({
726
+                type: 'success',
727
+                message: '入库成功',
728
+                complete: () => {
729
+                  uni.navigateBack({
730
+                    delta: 1
731
+                  })
732
+                }
733
+              })
734
+            } catch (error) {
735
+              console.error('入库失败:', error)
736
+              uni.$u.toast('入库失败')
737
+            }
738
+          }
739
+        }
740
+      })
741
+    },
742
+
743
+    /**
744
+     * 上传分成
745
+     */
746
+    async addShare() {
747
+      for (const item of this.profitSharingList) {
748
+        const data = {
749
+          id: item.id || '',
750
+          accountType: item.accountType == '1' ? 1 : 2,
751
+          clueId: this.currentReceipt.clueId,
752
+          commissionRate: item.commissionRate,
753
+          isCompanyPerformance: item.isCompanyPerformance == '1' ? 1 : 2,
754
+          sendFormId: this.currentReceipt.sendFormId,
755
+          userId: item.userId,
756
+          userName: item.userName,
757
+          receiptFormId: this.currentReceipt.id
758
+        }
759
+        if (item.id) {
760
+          await uni.$u.api.clueCommissionUpdate(data)
761
+        } else {
762
+          await uni.$u.api.clueCommissionAdd(data)
763
+        }
764
+      }
765
+    }
766
+  }
767
+}
768
+</script>
769
+
770
+<style scoped lang="scss">
771
+@import '../styles/common.scss';
772
+
773
+.page-four-container {
774
+  @extend .page-container;
775
+  padding-bottom: 100rpx;
776
+}
777
+
778
+.address-section {
779
+  padding: 20rpx;
780
+
781
+  // 防止 label 换行
782
+  ::v-deep .u-form-item__body__left__content__label {
783
+    white-space: nowrap !important;
784
+    overflow: visible;
785
+  }
786
+}
787
+
788
+.info-row {
789
+  margin-bottom: 20rpx;
790
+}
791
+
792
+.info-input {
793
+  height: 65rpx;
794
+  border-radius: 8rpx;
795
+  border: 1rpx solid #e5e7eb;
796
+  padding: 20rpx 16rpx;
797
+  width: 100%;
798
+  box-sizing: border-box;
799
+}
800
+
801
+.info-textarea {
802
+  width: 100%;
803
+  box-sizing: border-box;
804
+
805
+  ::v-deep .u-textarea {
806
+    min-height: 200rpx;
807
+    border-radius: 8rpx;
808
+    border: 1rpx solid #e5e7eb;
809
+    padding: 20rpx 16rpx;
810
+    line-height: 1.5;
811
+  }
812
+
813
+  ::v-deep textarea {
814
+    min-height: 200rpx;
815
+    border-radius: 8rpx;
816
+    width: 100%;
817
+    box-sizing: border-box;
818
+    line-height: 1.5;
819
+    font-size: 28rpx;
820
+  }
821
+}
822
+
823
+.click-wrapper {
824
+  width: 100%;
825
+  cursor: pointer;
826
+  display: flex;
827
+  align-items: center;
828
+}
829
+
830
+.image-uploader {
831
+  width: 100%;
832
+  height: 65rpx;
833
+  border: 2rpx dashed #409eff;
834
+  border-radius: 8rpx;
835
+  display: flex;
836
+  align-items: center;
837
+  justify-content: center;
838
+  cursor: pointer;
839
+  background-color: #ecf5ff;
840
+}
841
+
842
+.image-preview {
843
+  width: 100%;
844
+  height: 100%;
845
+  object-fit: cover;
846
+  border-radius: 10rpx;
847
+}
848
+
849
+.split-table {
850
+  width: 100%;
851
+  margin-top: 20rpx;
852
+}
853
+
854
+.split-table-header {
855
+  background-color: #f5f7fa;
856
+  padding: 15rpx 0;
857
+  border-bottom: 1rpx solid #e4e7ed;
858
+}
859
+
860
+.split-table-row {
861
+  padding: 15rpx 0;
862
+  border-bottom: 1rpx solid #e4e7ed;
863
+  align-items: center;
864
+}
865
+
866
+.header-text {
867
+  @include font-styles($size: tiny, $weight: regular, $color: tertiary);
868
+  text-align: center;
869
+  display: block;
870
+}
871
+
872
+.table-cell {
873
+  display: flex;
874
+  align-items: center;
875
+  justify-content: center;
876
+  padding: 0 10rpx;
877
+}
878
+
879
+.percentage-input {
880
+  text-align: center;
881
+  background-color: #f9f9f9;
882
+  padding: 0rpx !important;
883
+}
884
+
885
+.account-type {
886
+  padding: 4rpx 16rpx;
887
+  border-radius: 12rpx;
888
+  font-size: 24rpx;
889
+  font-weight: 500;
890
+  color: #fff;
891
+  text-align: center;
892
+  cursor: pointer;
893
+
894
+  &.frontend {
895
+    background-color: #409eff;
896
+  }
897
+
898
+  &.backend {
899
+    background-color: #909399;
900
+  }
901
+}
902
+
903
+.radio-wrapper {
904
+  cursor: pointer;
905
+  display: flex;
906
+  align-items: center;
907
+  justify-content: center;
908
+}
909
+
910
+.radio-circle {
911
+  width: 32rpx;
912
+  height: 32rpx;
913
+  border-radius: 50%;
914
+  border: 2rpx solid #dcdfe6;
915
+  display: flex;
916
+  align-items: center;
917
+  justify-content: center;
918
+  transition: all 0.3s ease;
919
+
920
+  &.active {
921
+    border-color: #67c23a;
922
+    background-color: #67c23a;
923
+  }
924
+}
925
+
926
+.confirm-button-container {
927
+  display: flex;
928
+  justify-content: center;
929
+  align-items: center;
930
+  padding: 40rpx 0;
931
+  margin-top: 20rpx;
932
+}
933
+
934
+.next-btn {
935
+  width: 95%;
936
+  height: 80rpx;
937
+  line-height: 80rpx;
938
+  text-align: center;
939
+  border-radius: 11px;
940
+}
941
+
942
+.add-button-container {
943
+  justify-content: space-between;
944
+}
945
+</style>

+ 750 - 0
pages/orderDetailRefactored/components/PageOne.vue

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

+ 920 - 0
pages/orderDetailRefactored/components/PageThree.vue

@@ -0,0 +1,920 @@
1
+<template>
2
+  <view class="page-three-container">
3
+    <!-- 支付信息卡片 -->
4
+    <view class="info-card">
5
+      <view class="info-card-title">支付信息</view>
6
+      <u-row class="info-row" justify="space-between">
7
+        <u-col span="5.8">
8
+          <view class="info-label">开户人</view>
9
+          <u-input v-model="paymentInfo.customName" placeholder="请输入开户人姓名" class="info-input"
10
+            @blur="handleCustomNameInput" />
11
+        </u-col>
12
+        <u-col span="5.8">
13
+          <view class="info-label">银行名称</view>
14
+          <u-input v-model="paymentInfo.bankName" placeholder="请输入银行名称" class="info-input"
15
+            @blur="handleBankNameInput" />
16
+        </u-col>
17
+      </u-row>
18
+      <u-row class="info-row">
19
+        <u-col span="12">
20
+          <view class="info-label">银行账号</view>
21
+          <u-input v-model="paymentInfo.bankAccount" placeholder="请输入银行账号" class="info-input" type="number"
22
+            @input="handleBankAccountInput" />
23
+        </u-col>
24
+      </u-row>
25
+      <u-row class="info-row">
26
+        <u-col span="12">
27
+          <view class="info-label">身份证号</view>
28
+          <u-input v-model="paymentInfo.idNumber" placeholder="请输入身份证号" class="info-input"
29
+            @blur="handleIdNumberInput" />
30
+        </u-col>
31
+      </u-row>
32
+      <u-row class="info-row">
33
+        <u-col span="12">
34
+          <view class="info-label">支付方式选择</view>
35
+          <u-radio-group iconPlacement="left" v-model="paymentMethodRadio" placement="row"
36
+            @change="handlePaymentMethodRadioChange">
37
+            <u-radio shape="circle" label="小葫芦线上支付" name="online" />
38
+            <u-radio shape="circle" label="线下支付" name="offline" />
39
+          </u-radio-group>
40
+        </u-col>
41
+      </u-row>
42
+      <u-row class="info-row">
43
+        <u-col span="12">
44
+          <view class="info-label">支付方式</view>
45
+          <u-input :disabled="paymentMethodRadio === 'online'" v-model="paymentMethod" placeholder="请输入支付方式"
46
+            class="info-input" />
47
+        </u-col>
48
+      </u-row>
49
+    </view>
50
+
51
+    <!-- 高清实物图卡片 -->
52
+    <view class="card-wrap">
53
+      <view class="detail-image-section">
54
+        <view class="detail-image-header">
55
+          <text class="detail-image-title">高清实物图(拖拽排序)</text>
56
+        </view>
57
+        <view class="detail-image-content">
58
+          <view class="detail-image-list">
59
+            <view v-for="(item, index) in displayImages" :key="item.id || `detail-${index}`" class="detail-image-item"
60
+              :class="{
61
+                'dragging': draggingIndex === index,
62
+                'can-drop': canDropIndex === index && draggingIndex !== index
63
+              }" :style="draggingIndex === index ? draggingStyle : ''" @touchstart.stop="onTouchStart($event, index)"
64
+              @touchmove.stop="onTouchMove($event, index)" @touchend.stop="onTouchEnd">
65
+              <PicComp :src="item.fileUrl" @needPreviewPic="previewImageDetail" />
66
+              <view class="image-type-tag">{{ getImageType(index) }}</view>
67
+              <view class="detail-delete-btn" @click.stop="handleHideImage(item, index)">
68
+                ×
69
+              </view>
70
+            </view>
71
+            <view class="detail-upload-btn" @click="handleUploadImage">
72
+              <u-icon name="plus" size="40rpx" color="#999" />
73
+            </view>
74
+          </view>
75
+        </view>
76
+      </view>
77
+    </view>
78
+
79
+    <!-- 支付总额卡片 -->
80
+    <view class="card-wrap payment-card">
81
+      <view class="payment-section">
82
+        <view class="payment-total-container">
83
+          <text class="payment-label">支付总额</text>
84
+          <u-input v-model="paymentAmount" class="payment-amount" type="number" decimal="2" prefix="¥" />
85
+        </view>
86
+        <view class="payment-buttons-row">
87
+          <view class="payment-button" @click="handleUnpaidClick">
88
+            <u-icon name="star" size="40rpx" color="#ff9500" />
89
+            <text class="button-text">未收</text>
90
+          </view>
91
+          <view class="payment-button" @click="handleFollowUpClick">
92
+            <u-icon name="chat" size="40rpx" color="#108cff" />
93
+            <text class="button-text">待跟进</text>
94
+          </view>
95
+        </view>
96
+        <view class="pay-now-button" @click="handlePayNowClick">
97
+          <text class="button-text">立即支付</text>
98
+        </view>
99
+      </view>
100
+    </view>
101
+
102
+    <!-- 下一步按钮 -->
103
+    <u-button class="next-btn" @click="handleNext" type="primary" size="middle">
104
+      下一步
105
+    </u-button>
106
+
107
+    <!-- 未收评级模态窗 -->
108
+    <u-modal showCancelButton showConfirmButton @confirm="confirmUnpaid" @cancel="unpaidModalVisible = false"
109
+      :show="unpaidModalVisible" title="未收评级">
110
+      <view class="modal-content">
111
+        <u-rate v-model="unpaidRating" :count="5" :size="50" active-color="#ff9500" />
112
+      </view>
113
+    </u-modal>
114
+
115
+    <!-- 待跟进模态窗 -->
116
+    <u-modal showCancelButton showConfirmButton @confirm="confirmFollowUp" @cancel="followUpModalVisible = false"
117
+      :show="followUpModalVisible" title="填写跟进细节">
118
+      <view class="modal-content">
119
+        <u--textarea v-model="followUpNotes" placeholder="请输入情况" confirm-type="done"
120
+          style="width: 100%; margin-bottom: 30rpx;" />
121
+      </view>
122
+    </u-modal>
123
+
124
+    <!-- 确认支付模态窗 -->
125
+    <u-modal showCancelButton showConfirmButton @confirm="confirmRiskWarning" @cancel="riskWarningModalVisible = false"
126
+      confirmText="确认并继续付款" :show="riskWarningModalVisible" title="付款警示" :content="'湖南耒阳地区身份证号请确认!'">
127
+    </u-modal>
128
+
129
+    <u-modal :show="payNowModalVisible" title="确认支付信息" :showConfirmButton="false">
130
+      <view class="modal-content">
131
+        <view class="payment-amount-display">¥{{ paymentAmount }}</view>
132
+        <view class="payment-info-section">
133
+          <view class="info-item">
134
+            <text class="info-label">收款姓名:</text>
135
+            <text class="info-value">{{ paymentInfo.customName || '未填写' }}</text>
136
+          </view>
137
+          <view class="info-item">
138
+            <text class="info-label">开户银行:</text>
139
+            <text class="info-value">{{ paymentInfo.bankName || '未填写' }}</text>
140
+          </view>
141
+          <view class="info-item">
142
+            <text class="info-label">银行卡号:</text>
143
+            <text class="info-value">{{ paymentInfo.bankAccount || '未填写' }}</text>
144
+          </view>
145
+          <view class="info-item">
146
+            <text class="info-label">支付方式:</text>
147
+            <text class="info-value">{{ paymentMethod || '未填写' }}</text>
148
+          </view>
149
+        </view>
150
+        <u-button type="primary" size="large" @click="confirmTransfer" style="margin-top: 40rpx;">
151
+          确认转账
152
+        </u-button>
153
+        <u-button type="primary" :plain="true" @click="payNowModalVisible = false" size="large"
154
+          style="margin-top: 20rpx;">
155
+          取消
156
+        </u-button>
157
+      </view>
158
+    </u-modal>
159
+  </view>
160
+</template>
161
+
162
+<script>
163
+import PicComp from './PicComp.vue'
164
+import imageUpload from '../utils/imageUpload.js'
165
+
166
+export default {
167
+  name: 'PageThree',
168
+  components: {
169
+    PicComp
170
+  },
171
+  props: {
172
+    orderDetail: {
173
+      type: Object,
174
+      default: () => ({})
175
+    },
176
+    orderId: {
177
+      type: String,
178
+      default: ''
179
+    },
180
+    currentReceipt: {
181
+      type: Object,
182
+      default: () => ({})
183
+    }
184
+  },
185
+  data() {
186
+    return {
187
+      paymentInfo: {
188
+        customName: '',
189
+        bankName: '',
190
+        bankAccount: '',
191
+        idNumber: ''
192
+      },
193
+      paymentMethodRadio: 'offline',
194
+      paymentMethod: '',
195
+      paymentAmount: '0.00',
196
+      detailImages: [],
197
+      // 拖拽相关
198
+      draggingIndex: -1,
199
+      canDropIndex: -1,
200
+      startX: 0,
201
+      startY: 0,
202
+      currentX: 0,
203
+      currentY: 0,
204
+      // 模态窗
205
+      unpaidModalVisible: false,
206
+      followUpModalVisible: false,
207
+      payNowModalVisible: false,
208
+      riskWarningModalVisible: false,
209
+      unpaidRating: 0,
210
+      followUpNotes: ''
211
+    }
212
+  },
213
+  computed: {
214
+    // 显示前6个图片用于拖拽
215
+    displayImages() {
216
+      return this.detailImages.slice(0, 6)
217
+    },
218
+    // 拖拽时的样式
219
+    draggingStyle() {
220
+      if (this.draggingIndex === -1) return ''
221
+      return {
222
+        transform: `translate(${this.currentX - this.startX}px, ${this.currentY - this.startY}px)`,
223
+        zIndex: 1000
224
+      }
225
+    }
226
+  },
227
+  watch: {
228
+    orderDetail: {
229
+      handler(newVal) {
230
+        if (newVal) {
231
+          this.paymentInfo.customName = newVal.customName || ''
232
+          this.paymentInfo.bankName = newVal.bankName || ''
233
+          this.paymentInfo.bankAccount = newVal.bankCardNumber || ''
234
+          this.paymentInfo.idNumber = newVal.idCard || ''
235
+          this.initPaymentMethod(newVal.paymentMethod)
236
+        }
237
+      },
238
+      deep: true,
239
+      immediate: true
240
+    },
241
+    currentReceipt: {
242
+      handler(newVal) {
243
+        if (newVal) {
244
+          this.paymentAmount = newVal.tableFee || '0.00'
245
+          this.loadDetailImages()
246
+        }
247
+      },
248
+      deep: true,
249
+      immediate: true
250
+    }
251
+  },
252
+  methods: {
253
+    /**
254
+     * 刷新图片列表(供父组件调用)
255
+     */
256
+    async refreshImageList() {
257
+      await this.loadDetailImages()
258
+    },
259
+
260
+    /**
261
+     * 加载细节图
262
+     */
263
+    async loadDetailImages() {
264
+      if (!this.currentReceipt.id || !this.orderDetail.itemBrand) return
265
+
266
+      try {
267
+        const list = await imageUpload.getFileList(
268
+          '2',
269
+          '3',
270
+          this.currentReceipt.id,
271
+          this.orderDetail.itemBrand,
272
+          this.currentReceipt.clueId
273
+        )
274
+
275
+        // 按照 fileIds 排序
276
+        if (this.currentReceipt.fileIds && list && list.length > 0) {
277
+          const sortedIds = this.currentReceipt.fileIds.split(',')
278
+          list.sort((a, b) => {
279
+            const indexA = sortedIds.indexOf(a.id)
280
+            const indexB = sortedIds.indexOf(b.id)
281
+
282
+            // 如果都不在列表中,保持原顺序
283
+            if (indexA === -1 && indexB === -1) return 0
284
+            // 如果 a 不在列表中,放到后面
285
+            if (indexA === -1) return 1
286
+            // 如果 b 不在列表中,放到后面
287
+            if (indexB === -1) return -1
288
+            // 都在列表中,按 index 排序
289
+            return indexA - indexB
290
+          })
291
+        }
292
+
293
+        this.detailImages = list || []
294
+      } catch (error) {
295
+        console.error('加载细节图失败:', error)
296
+      }
297
+    },
298
+
299
+    /**
300
+     * 初始化支付方式
301
+     */
302
+    initPaymentMethod(value) {
303
+      if (value === '小葫芦线上支付') {
304
+        this.paymentMethod = '小葫芦线上支付'
305
+        this.paymentMethodRadio = 'online'
306
+      } else {
307
+        this.paymentMethod = value || ''
308
+        this.paymentMethodRadio = 'offline'
309
+      }
310
+    },
311
+
312
+    /**
313
+     * 支付方式选择改变
314
+     */
315
+    handlePaymentMethodRadioChange(value) {
316
+      if (value === 'online') {
317
+        this.paymentMethod = '小葫芦线上支付'
318
+      } else {
319
+        this.paymentMethod = ''
320
+      }
321
+    },
322
+
323
+    /**
324
+     * 开户人输入处理 - 只允许中文
325
+     */
326
+    handleCustomNameInput(value) {
327
+      // 只保留中文字符(包括中文标点)
328
+      const chineseReg = /[^\u4e00-\u9fa5]/g
329
+      this.paymentInfo.customName = String(value || '').replace(chineseReg, '')
330
+    },
331
+
332
+    /**
333
+     * 银行名称输入处理 - 只允许中文
334
+     */
335
+    handleBankNameInput(value) {
336
+      // 只保留中文字符(包括中文标点)
337
+      const chineseReg = /[^\u4e00-\u9fa5]/g
338
+      this.paymentInfo.bankName = String(value || '').replace(chineseReg, '')
339
+    },
340
+
341
+    /**
342
+     * 银行账号输入处理 - 只允许数字
343
+     */
344
+    handleBankAccountInput(value) {
345
+      // 只保留数字
346
+      const numberReg = /[^\d]/g
347
+      this.paymentInfo.bankAccount = String(value || '').replace(numberReg, '')
348
+    },
349
+
350
+    /**
351
+     * 身份证号输入处理 - 允许数字和X
352
+     */
353
+    handleIdNumberInput(value) {
354
+      // 只保留数字和X
355
+      const reg = /[^\dxX]/g
356
+      this.paymentInfo.idNumber = String(value || '').replace(reg, '')
357
+    },
358
+
359
+    /**
360
+     * 获取图片类型
361
+     */
362
+    getImageType(index) {
363
+      const types = ['正面', '反面', '侧面', '扣子', '编号']
364
+      return types[index] || `细节${index - 4}`
365
+    },
366
+
367
+    /**
368
+     * 触摸开始
369
+     */
370
+    onTouchStart(event, index) {
371
+      if (this.draggingIndex !== -1) return
372
+      this.draggingIndex = index
373
+      const touch = event.touches[0]
374
+      this.startX = touch.clientX
375
+      this.startY = touch.clientY
376
+      this.currentX = touch.clientX
377
+      this.currentY = touch.clientY
378
+    },
379
+
380
+    /**
381
+     * 触摸移动
382
+     */
383
+    onTouchMove(event, index) {
384
+      if (this.draggingIndex === -1 || this.draggingIndex !== index) return
385
+      const touch = event.touches[0]
386
+      this.currentX = touch.clientX
387
+      this.currentY = touch.clientY
388
+      this.findTargetIndex(touch.clientX, touch.clientY)
389
+    },
390
+
391
+    /**
392
+     * 触摸结束
393
+     */
394
+    async onTouchEnd() {
395
+      if (this.draggingIndex === -1) return
396
+
397
+      if (this.canDropIndex !== -1 && this.canDropIndex !== this.draggingIndex) {
398
+        // 交换位置
399
+        const images = [...this.detailImages]
400
+        const temp = images[this.draggingIndex]
401
+        images[this.draggingIndex] = images[this.canDropIndex]
402
+        images[this.canDropIndex] = temp
403
+        this.detailImages = images
404
+      }
405
+
406
+      this.resetDragState()
407
+
408
+      //每次拖拽结束后把新的顺序传送给接口
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
+
417
+    /**
418
+     * 查找目标索引
419
+     */
420
+    findTargetIndex(x, y) {
421
+      const query = uni.createSelectorQuery().in(this)
422
+      query.selectAll('.detail-image-item').boundingClientRect((rects) => {
423
+        if (!rects || rects.length === 0) return
424
+        let targetIndex = -1
425
+        for (let i = 0; i < rects.length; i++) {
426
+          if (i === this.draggingIndex) continue
427
+          const rect = rects[i]
428
+          if (x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom) {
429
+            targetIndex = i
430
+            break
431
+          }
432
+        }
433
+        this.canDropIndex = targetIndex
434
+      }).exec()
435
+    },
436
+
437
+    /**
438
+     * 重置拖拽状态
439
+     */
440
+    resetDragState() {
441
+      this.draggingIndex = -1
442
+      this.canDropIndex = -1
443
+      this.startX = 0
444
+      this.startY = 0
445
+      this.currentX = 0
446
+      this.currentY = 0
447
+    },
448
+
449
+    /**
450
+     * 上传图片
451
+     */
452
+    async handleUploadImage() {
453
+      try {
454
+        const filePaths = await imageUpload.chooseImage(9)
455
+        const uploadResults = await imageUpload.uploadFiles(filePaths)
456
+        await imageUpload.bindOrderFile(
457
+          this.currentReceipt.clueId,
458
+          this.currentReceipt.id,
459
+          '3',
460
+          uploadResults
461
+        )
462
+        await this.loadDetailImages()
463
+
464
+        //上传新图片完成之后也要把新数据给接口
465
+        const fileIds = this.detailImages.map(item => item.id).join(',')
466
+        await uni.$u.api.updateReceiptForm({
467
+          id: this.currentReceipt.id,
468
+          fileIds: fileIds
469
+        })
470
+        this.$emit('update-file-ids', fileIds)
471
+
472
+      } catch (error) {
473
+        console.error('上传失败:', error)
474
+      }
475
+    },
476
+
477
+    /**
478
+     * 隐藏图片
479
+     */
480
+    async handleHideImage(item, index) {
481
+      const itemIndex = this.detailImages.findIndex(img => img.id === item.id || img.fileUrl === item.fileUrl)
482
+      if (itemIndex !== -1) {
483
+        this.detailImages.splice(itemIndex, 1)
484
+
485
+        // 更新 fileIds
486
+        const fileIds = this.detailImages.map(item => item.id).join(',')
487
+        try {
488
+          await uni.$u.api.updateReceiptForm({
489
+            id: this.currentReceipt.id,
490
+            fileIds: fileIds
491
+          })
492
+          this.$emit('update-file-ids', fileIds)
493
+          uni.$u.toast('图片已隐藏')
494
+        } catch (error) {
495
+          console.error('更新失败:', error)
496
+        }
497
+      }
498
+    },
499
+
500
+    /**
501
+     * 预览图片
502
+     */
503
+    previewImageDetail(src) {
504
+      const urlList = this.detailImages.map(item => item.fileUrl)
505
+      uni.previewImage({
506
+        urls: urlList,
507
+        current: src
508
+      })
509
+    },
510
+
511
+    /**
512
+     * 未收点击
513
+     */
514
+    handleUnpaidClick() {
515
+      this.unpaidModalVisible = true
516
+    },
517
+
518
+    /**
519
+     * 待跟进点击
520
+     */
521
+    handleFollowUpClick() {
522
+      this.followUpModalVisible = true
523
+    },
524
+
525
+    /**
526
+     * 立即支付点击
527
+     */
528
+    async handlePayNowClick() {
529
+      // 保存支付信息
530
+      await uni.$u.api.updateReceiptForm({
531
+        id: this.currentReceipt.id,
532
+        tableFee: this.paymentAmount,
533
+        fileIds: this.detailImages.map(item => item.id).join(',')
534
+      })
535
+
536
+      await uni.$u.api.updateClueOrderForm({
537
+        id: this.orderDetail.id,
538
+        customName: this.paymentInfo.customName || '',
539
+        bankName: this.paymentInfo.bankName || '',
540
+        bankCardNumber: this.paymentInfo.bankAccount || '',
541
+        idCard: this.paymentInfo.idNumber || '',
542
+        // paymentMethod: this.paymentMethod || ''
543
+      })
544
+
545
+      if (!this.paymentInfo.customName || !this.paymentInfo.bankName ||
546
+        !this.paymentInfo.bankAccount || !this.paymentInfo.idNumber || !this.paymentMethod) {
547
+        uni.$u.toast('请填写完整的支付信息')
548
+        return
549
+      }
550
+      if (this.paymentAmount <= 0) {
551
+        uni.$u.toast('请填写正确的支付总额')
552
+        return
553
+      }
554
+
555
+      //判断当前账号是否敏感地区
556
+      if (String(this.paymentInfo.idNumber).startsWith('430481')) {
557
+        this.riskWarningModalVisible = true
558
+        return
559
+      }
560
+
561
+      this.payNowModalVisible = true
562
+    },
563
+
564
+    /**
565
+     * 确认风险警示
566
+     */
567
+    confirmRiskWarning() {
568
+      this.riskWarningModalVisible = false
569
+      this.payNowModalVisible = true
570
+    },
571
+
572
+    /**
573
+     * 确认转账
574
+     */
575
+    async confirmTransfer() {
576
+      this.payNowModalVisible = false
577
+
578
+      await uni.$u.api.updateClueOrderForm({
579
+        id: this.orderDetail.id,
580
+        paymentMethod: this.paymentMethod || ''
581
+      })
582
+      //如果是小葫芦线上刚支付就调用接口,如果不是小葫芦线上支付就不要调用接口,直接点击下一步
583
+      if (this.paymentMethod === '小葫芦线上支付') {
584
+        this.$emit('confirm-pay')
585
+      } else {
586
+        this.handleNext()
587
+      }
588
+    },
589
+
590
+    /**
591
+     * 确认未收
592
+     */
593
+    async confirmUnpaid() {
594
+      try {
595
+        await uni.$u.api.addOrderFollow({
596
+          orderId: this.orderId,
597
+          content: `未收评分_${this.unpaidRating}`
598
+        })
599
+        await uni.$u.api.oderForm({
600
+          status: '4',
601
+          id: this.orderId
602
+        })
603
+        uni.$u.toast('提交未收评级成功')
604
+        this.unpaidModalVisible = false
605
+      } catch (error) {
606
+        console.error('提交失败:', error)
607
+        uni.$u.toast('提交失败')
608
+      }
609
+    },
610
+
611
+    /**
612
+     * 确认跟进
613
+     */
614
+    async confirmFollowUp() {
615
+      try {
616
+        await uni.$u.api.addOrderFollow({
617
+          orderId: this.orderId,
618
+          content: `待跟进_${this.followUpNotes}`
619
+        })
620
+        uni.$u.toast('提交待跟进记录成功')
621
+        this.followUpModalVisible = false
622
+        this.followUpNotes = ''
623
+      } catch (error) {
624
+        console.error('提交失败:', error)
625
+        uni.$u.toast('提交失败')
626
+      }
627
+    },
628
+
629
+    /**
630
+     * 下一步
631
+     */
632
+    async handleNext() {
633
+      await uni.$u.api.updateReceiptForm({
634
+        id: this.currentReceipt.id,
635
+        tableFee: this.paymentAmount,
636
+        fileIds: this.detailImages.map(item => item.id).join(',')
637
+      })
638
+
639
+      await uni.$u.api.updateClueOrderForm({
640
+        id: this.orderDetail.id,
641
+        customName: this.paymentInfo.customName || '',
642
+        bankName: this.paymentInfo.bankName || '',
643
+        bankCardNumber: this.paymentInfo.bankAccount || '',
644
+        idCard: this.paymentInfo.idNumber || ''
645
+      })
646
+
647
+      this.$emit('save', {
648
+        nowPage: 'formThree',
649
+        form: {
650
+          ...this.paymentInfo
651
+        },
652
+        fileIds: this.detailImages.map(item => item.id).join(',')
653
+      })
654
+
655
+      this.$emit('next', {
656
+        nowPage: 'formThree',
657
+        form: {
658
+          ...this.paymentInfo
659
+        }
660
+      })
661
+    }
662
+  }
663
+}
664
+</script>
665
+
666
+<style scoped lang="scss">
667
+@import '../styles/common.scss';
668
+
669
+.page-three-container {
670
+  @extend .page-container;
671
+  padding-bottom: 100rpx;
672
+}
673
+
674
+.info-card {
675
+  @extend .card-wrap;
676
+  padding: 20rpx;
677
+  margin-top: 20rpx;
678
+  box-sizing: border-box;
679
+  width: 100%;
680
+  max-width: 100%;
681
+}
682
+
683
+.info-card-title {
684
+  @include font-styles($size: title, $weight: bold, $color: primary);
685
+  margin-bottom: 25rpx;
686
+  padding-bottom: 15rpx;
687
+  border-bottom: 1rpx solid map-get($colors, border);
688
+}
689
+
690
+.info-row {
691
+  margin-bottom: 20rpx;
692
+}
693
+
694
+.info-label {
695
+  @include font-styles($size: tiny, $weight: regular, $color: tertiary);
696
+  margin-bottom: 8rpx;
697
+  display: block;
698
+}
699
+
700
+.info-input {
701
+  border-radius: 8rpx;
702
+  border: 1rpx solid #e5e7eb;
703
+  padding: 12rpx 16rpx;
704
+  width: 100%;
705
+  box-sizing: border-box;
706
+}
707
+
708
+.detail-image-section {
709
+  padding: 20rpx;
710
+}
711
+
712
+.detail-image-header {
713
+  display: flex;
714
+  justify-content: space-between;
715
+  align-items: center;
716
+  margin-bottom: 20rpx;
717
+  padding-bottom: 20rpx;
718
+  border-bottom: 1rpx solid map-get($colors, border);
719
+}
720
+
721
+.detail-image-title {
722
+  @include font-styles($size: content, $weight: bold, $color: primary);
723
+}
724
+
725
+.detail-image-list {
726
+  display: flex;
727
+  flex-wrap: wrap;
728
+  gap: 20rpx;
729
+  position: relative;
730
+}
731
+
732
+.detail-image-item {
733
+  position: relative;
734
+  width: 200rpx;
735
+  height: 200rpx;
736
+  touch-action: none;
737
+  transition: transform 0.2s ease, opacity 0.2s ease;
738
+  z-index: 1;
739
+  box-sizing: border-box;
740
+
741
+  &.dragging {
742
+    opacity: 0.6;
743
+    transform: scale(1.05);
744
+    z-index: 999;
745
+    box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.2);
746
+  }
747
+
748
+  &.can-drop {
749
+    outline: 2rpx dashed #108cff;
750
+    outline-offset: -2rpx;
751
+    background-color: rgba(16, 140, 255, 0.1);
752
+    border-radius: 8rpx;
753
+  }
754
+}
755
+
756
+.image-type-tag {
757
+  position: absolute;
758
+  top: 10rpx;
759
+  left: 10rpx;
760
+  background-color: rgba(0, 0, 0, 0.6);
761
+  color: white;
762
+  padding: 5rpx 10rpx;
763
+  border-radius: 12rpx;
764
+  font-size: 22rpx;
765
+  z-index: 1;
766
+}
767
+
768
+.detail-delete-btn {
769
+  position: absolute;
770
+  top: -10rpx;
771
+  right: -10rpx;
772
+  width: 40rpx;
773
+  height: 40rpx;
774
+  background-color: #ff4d4f;
775
+  color: #fff;
776
+  border-radius: 50%;
777
+  display: flex;
778
+  align-items: center;
779
+  justify-content: center;
780
+  font-weight: bold;
781
+  z-index: 10;
782
+  cursor: pointer;
783
+}
784
+
785
+.detail-upload-btn {
786
+  width: 200rpx;
787
+  height: 200rpx;
788
+  border: 8rpx dashed #ddd;
789
+  border-radius: 30rpx;
790
+  display: flex;
791
+  align-items: center;
792
+  justify-content: center;
793
+  background-color: #f9f9f9;
794
+  cursor: pointer;
795
+}
796
+
797
+.payment-card {
798
+  margin-top: 20rpx;
799
+}
800
+
801
+.payment-section {
802
+  padding: 20rpx;
803
+}
804
+
805
+.payment-total-container {
806
+  background-color: #f5f7fa;
807
+  border: 2rpx solid #e5e7eb;
808
+  border-radius: 12rpx;
809
+  padding: 0rpx 30rpx;
810
+  display: flex;
811
+  align-items: center;
812
+  justify-content: space-between;
813
+  margin-bottom: 24rpx;
814
+}
815
+
816
+.payment-label {
817
+  font-size: 36rpx;
818
+  font-weight: 700;
819
+  min-width: 140rpx;
820
+}
821
+
822
+.payment-amount {
823
+  font-size: 48rpx;
824
+  font-weight: 600;
825
+  color: #f53f3f;
826
+  width: auto;
827
+  min-width: 150rpx;
828
+  border: none;
829
+  text-align: right !important;
830
+}
831
+
832
+.payment-buttons-row {
833
+  display: flex;
834
+  gap: 20rpx;
835
+  margin-bottom: 24rpx;
836
+}
837
+
838
+.payment-button {
839
+  flex: 1;
840
+  border-radius: 12rpx;
841
+  padding: 5rpx 0;
842
+  display: flex;
843
+  flex-direction: column;
844
+  align-items: center;
845
+  justify-content: center;
846
+  background-color: #f5f7fa;
847
+  border: 2rpx solid #e5e7eb;
848
+  cursor: pointer;
849
+  gap: 12rpx;
850
+}
851
+
852
+.pay-now-button {
853
+  width: 100%;
854
+  border-radius: 12rpx;
855
+  padding: 32rpx 0;
856
+  display: flex;
857
+  flex-direction: column;
858
+  align-items: center;
859
+  justify-content: center;
860
+  background-color: #108cff;
861
+  color: #fff;
862
+  cursor: pointer;
863
+  box-shadow: 0 4rpx 12rpx rgba(16, 140, 255, 0.2);
864
+}
865
+
866
+.button-text {
867
+  font-size: 28rpx;
868
+  font-weight: 500;
869
+  color: inherit;
870
+}
871
+
872
+.modal-content {
873
+  width: 100%;
874
+  padding: 40rpx 20rpx;
875
+  display: flex;
876
+  flex-direction: column;
877
+  align-items: center;
878
+}
879
+
880
+.payment-amount-display {
881
+  font-size: 64rpx;
882
+  font-weight: bold;
883
+  color: #108cff;
884
+  text-align: center;
885
+  margin-bottom: 40rpx;
886
+  width: 100%;
887
+}
888
+
889
+.payment-info-section {
890
+  width: 100%;
891
+  background-color: #f5f7fa;
892
+  border: 2rpx solid #e5e7eb;
893
+  border-radius: 12rpx;
894
+  padding: 30rpx 20rpx;
895
+  margin-bottom: 40rpx;
896
+}
897
+
898
+.info-item {
899
+  display: flex;
900
+  margin-bottom: 20rpx;
901
+  align-items: center;
902
+  justify-content: space-between;
903
+
904
+  &:last-child {
905
+    margin-bottom: 0;
906
+  }
907
+}
908
+
909
+.next-btn {
910
+  position: fixed;
911
+  bottom: 10rpx;
912
+  left: 2.5%;
913
+  width: 95%;
914
+  height: 80rpx;
915
+  line-height: 80rpx;
916
+  text-align: center;
917
+  border-radius: 20rpx;
918
+  z-index: 1000;
919
+}
920
+</style>

+ 767 - 0
pages/orderDetailRefactored/components/PageTwo.vue

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

+ 43 - 0
pages/orderDetailRefactored/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>

+ 439 - 0
pages/orderDetailRefactored/index.vue

@@ -0,0 +1,439 @@
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" />
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 handleReceiptClick(item) {
179
+      console.log('点击了收单', item)
180
+      //获取当前的收单form详情
181
+      const res = await uni.$u.api.getReceiptForm(item.id)
182
+      console.log('收单详情', res)
183
+
184
+      if (res.code === 200) {
185
+        this.currentReceipt = res.data || {}
186
+        this.topInfo.brand = res.data.brand || '暂无'
187
+        this.topInfo.model = res.data.model || '暂无'
188
+        this.topInfo.price = res.data.sellingPrice || '暂无'
189
+      }
190
+    },
191
+
192
+    /**
193
+     * 点击品牌
194
+     */
195
+    handleBrandClick() {
196
+      this.editBrandSelectorVisible = true
197
+      this.loadBrandList()
198
+    },
199
+
200
+    /**
201
+     * 点击型号
202
+     */
203
+    handleModelClick() {
204
+      this.modalConfig = {
205
+        title: '修改型号',
206
+        value: this.currentReceipt.model || '',
207
+        placeholder: '请输入型号'
208
+      }
209
+      this.currentEditField = 'model'
210
+      this.modalVisible = true
211
+    },
212
+
213
+    /**
214
+     * 点击价格
215
+     */
216
+    handlePriceClick() {
217
+      this.modalConfig = {
218
+        title: '修改价格',
219
+        value: this.currentReceipt.sellingPrice?.toString() || '',
220
+        placeholder: '请输入价格'
221
+      }
222
+      this.currentEditField = 'price'
223
+      this.modalVisible = true
224
+    },
225
+
226
+    /**
227
+     * 点击加一单
228
+     */
229
+    handleAddClick() {
230
+      this.addOneModalVisible = true
231
+      this.loadBrandList()
232
+    },
233
+
234
+    /**
235
+     * 加载品牌列表
236
+     */
237
+    async loadBrandList() {
238
+      try {
239
+        const res = await this.$getDicts('crm_form_brand')
240
+        this.brandColumns = [res]
241
+      } catch (error) {
242
+        console.error('加载品牌列表失败:', error)
243
+      }
244
+    },
245
+
246
+    /**
247
+     * 确认修改模态窗
248
+     */
249
+    async handleModalConfirm(value) {
250
+      try {
251
+        if (this.currentEditField === 'model') {
252
+          await uni.$u.api.updateReceiptForm({
253
+            model: value,
254
+            id: this.currentReceipt.id
255
+          })
256
+        } else if (this.currentEditField === 'price') {
257
+          await uni.$u.api.updateReceiptForm({
258
+            sellingPrice: value,
259
+            id: this.currentReceipt.id
260
+          })
261
+        }
262
+        uni.$u.toast('修改成功')
263
+        this.loadReceiptList()
264
+      } catch (error) {
265
+        console.error('修改失败:', error)
266
+        uni.$u.toast('修改失败')
267
+      } finally {
268
+        this.modalVisible = false
269
+      }
270
+    },
271
+
272
+    /**
273
+     * 取消修改模态窗
274
+     */
275
+    handleModalCancel() {
276
+      this.modalVisible = false
277
+    },
278
+
279
+    /**
280
+     * 确认编辑品牌
281
+     */
282
+    async handleEditBrandConfirm(data) {
283
+      try {
284
+        await uni.$u.api.updateReceiptForm({
285
+          brand: data.value[0].dictValue,
286
+          id: this.currentReceipt.id
287
+        })
288
+        uni.$u.toast('修改成功')
289
+        this.loadReceiptList()
290
+      } catch (error) {
291
+        console.error('修改品牌失败:', error)
292
+        uni.$u.toast('修改失败')
293
+      } finally {
294
+        this.editBrandSelectorVisible = false
295
+      }
296
+    },
297
+
298
+    /**
299
+     * 确认选择品牌(加一单)
300
+     */
301
+    handleBrandConfirm(data) {
302
+      this.currentAddBrand = data.value[0]
303
+      this.showBrandSelector = false
304
+    },
305
+
306
+    /**
307
+     * 确认加一单
308
+     */
309
+    async handleAddOneConfirm() {
310
+      // 验证品牌是否已选择(必选)
311
+      if (!this.currentAddBrand.dictValue) {
312
+        uni.$u.toast('请选择品牌')
313
+        return
314
+      }
315
+
316
+      try {
317
+        await uni.$u.api.addReceiptForm({
318
+          brand: this.currentAddBrand.dictValue,
319
+          model: this.currentAddModel || '',
320
+          sellingPrice: this.currentAddPrice || '',
321
+          sendFormId: this.orderId,
322
+          clueId: this.clueId
323
+        })
324
+        uni.$u.toast('添加成功')
325
+        this.loadReceiptList()
326
+      } catch (error) {
327
+        console.error('添加失败:', error)
328
+        uni.$u.toast('添加失败')
329
+      } finally {
330
+        this.addOneModalVisible = false
331
+        this.currentAddBrand = {}
332
+        this.currentAddModel = ''
333
+        this.currentAddPrice = ''
334
+      }
335
+    },
336
+
337
+    /**
338
+     * 取消加一单
339
+     */
340
+    handleAddOneCancel() {
341
+      this.addOneModalVisible = false
342
+      this.currentAddBrand = {}
343
+      this.currentAddModel = ''
344
+      this.currentAddPrice = ''
345
+    }
346
+  }
347
+}
348
+</script>
349
+
350
+<style scoped lang="scss">
351
+.order-detail-container {
352
+  min-height: 100vh;
353
+  background-color: #f9fafb;
354
+}
355
+
356
+.navbar-center {
357
+  display: flex;
358
+  align-items: center;
359
+  background-color: #fff;
360
+  border-radius: 40rpx;
361
+  padding: 10rpx 20rpx;
362
+  box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
363
+}
364
+
365
+.navbar-item {
366
+  font-weight: bold;
367
+  color: #333;
368
+  font-size: 28rpx;
369
+
370
+  &.price {
371
+    color: blueviolet;
372
+  }
373
+}
374
+
375
+.navbar-divider {
376
+  margin: 0 15rpx;
377
+  color: #ddd;
378
+  font-size: 28rpx;
379
+}
380
+
381
+.navbar-right {
382
+  display: flex;
383
+  flex-direction: column;
384
+  align-items: center;
385
+  justify-content: center;
386
+  font-size: 20rpx;
387
+  color: blueviolet;
388
+}
389
+
390
+.add-icon {
391
+  width: 30rpx;
392
+  height: 30rpx;
393
+}
394
+
395
+.add-text {
396
+  margin-top: 4rpx;
397
+}
398
+
399
+.receipt-tabs {
400
+  background-color: #fff;
401
+}
402
+
403
+.add-one-modal-content {
404
+  padding: 20rpx 0;
405
+}
406
+
407
+.add-one-form-item {
408
+  margin-bottom: 30rpx;
409
+
410
+  &:last-child {
411
+    margin-bottom: 0;
412
+  }
413
+}
414
+
415
+.form-label {
416
+  display: block;
417
+  font-size: 28rpx;
418
+  color: #333;
419
+  margin-bottom: 15rpx;
420
+  font-weight: 500;
421
+}
422
+
423
+.required {
424
+  color: #f56c6c;
425
+  margin-left: 4rpx;
426
+}
427
+
428
+.brand-select-btn {
429
+  width: 100%;
430
+}
431
+
432
+.form-input {
433
+  width: 100%;
434
+  border: 2rpx solid #e0e0e0;
435
+  border-radius: 10rpx;
436
+  padding: 20rpx;
437
+  font-size: 28rpx;
438
+}
439
+</style>

+ 174 - 0
pages/orderDetailRefactored/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/orderDetailRefactored/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
+}

+ 168 - 1
pages/orderForm/index.vue

@@ -126,6 +126,7 @@ import orderFileUpload from '@/components/order-file-upload/order-file-upload.vu
126 126
 import dateTimePicker from "@/components/dateTimePicker/dateTimePicker.vue"
127 127
 import ldSelect from "@/components/ld-select/ld-select.vue"
128 128
 import BrandSelect from "@/components/brand-select/brand-select.vue"
129
+const CHINA_REGIONS = require('@/components/pick-regions/regions.json')
129 130
 export default {
130 131
 	components: {
131 132
 		orderFileUpload,
@@ -289,7 +290,173 @@ export default {
289 290
 		},
290 291
 
291 292
 		// 地址失焦时自动解析地址信息
292
-		handleAddressBlur() {
293
+		async handleAddressBlur() {
294
+			if (!this.form.address || !this.form.address.trim()) {
295
+				return;
296
+			}
297
+			
298
+			const address = this.form.address.trim();
299
+			const parsedRegion = this.parseAddress(address);
300
+			
301
+			if (parsedRegion && parsedRegion.province) {
302
+				this.form.province = parsedRegion.province;
303
+				this.form.city = parsedRegion.city || '';
304
+				this.form.area = parsedRegion.area || '';
305
+				// 更新 defaultRegion,用于更新 pick-regions 组件的显示
306
+				this.defaultRegion = [
307
+					parsedRegion.province,
308
+					parsedRegion.city || '',
309
+					parsedRegion.area || ''
310
+				];
311
+			}
312
+		},
313
+		
314
+		// 基于地址字典解析地址,提取省市区信息
315
+		parseAddress(address) {
316
+			if (!address || !CHINA_REGIONS || CHINA_REGIONS.length === 0) {
317
+				return null;
318
+			}
319
+			
320
+			let bestMatch = null;
321
+			let bestMatchScore = 0; // 匹配分数:完整匹配得分更高
322
+			
323
+			// 遍历所有省份
324
+			for (const province of CHINA_REGIONS) {
325
+				const provinceName = province.name;
326
+				// 移除可能的"省"、"市"、"自治区"、"特别行政区"等后缀进行匹配
327
+				const provinceNameShort = provinceName.replace(/省|市|自治区|特别行政区|壮族自治区|维吾尔自治区|回族自治区/g, '');
328
+				
329
+				// 检查地址中是否包含省份名称
330
+				const hasProvinceFull = address.includes(provinceName);
331
+				const hasProvinceShort = address.includes(provinceNameShort);
332
+				
333
+				if (hasProvinceFull || hasProvinceShort) {
334
+					// 找到省份在地址中的位置
335
+					const provinceIndex = hasProvinceFull 
336
+						? address.indexOf(provinceName) 
337
+						: address.indexOf(provinceNameShort);
338
+					
339
+					if (provinceIndex === -1) continue;
340
+					
341
+					// 获取省份后的剩余地址
342
+					const provinceMatchLength = hasProvinceFull ? provinceName.length : provinceNameShort.length;
343
+					const remainingAfterProvince = address.substring(provinceIndex + provinceMatchLength);
344
+					
345
+					// 直辖市特殊处理
346
+					const municipalities = ['北京市', '上海市', '天津市', '重庆市'];
347
+					if (municipalities.includes(provinceName)) {
348
+						// 直辖市:市名就是省名,直接查找区/县
349
+						const cityName = provinceName;
350
+						let matchedArea = null;
351
+						let maxAreaLength = 0;
352
+						
353
+						// 遍历该省份下的所有市(通常是"市辖区")
354
+						if (province.childs && province.childs.length > 0) {
355
+							for (const city of province.childs) {
356
+								// 遍历该市下的所有区/县
357
+								if (city.childs && city.childs.length > 0) {
358
+									for (const area of city.childs) {
359
+										const areaName = area.name;
360
+										// 检查剩余地址中是否包含区/县名称
361
+										if (remainingAfterProvince.includes(areaName)) {
362
+											// 找到最长的匹配(更准确)
363
+											if (areaName.length > maxAreaLength) {
364
+												maxAreaLength = areaName.length;
365
+												matchedArea = areaName;
366
+											}
367
+										}
368
+									}
369
+								}
370
+							}
371
+						}
372
+						
373
+						// 计算匹配分数:有区/县得3分,只有省市区得2分
374
+						const matchScore = matchedArea ? 3 : 2;
375
+						if (matchScore > bestMatchScore) {
376
+							bestMatchScore = matchScore;
377
+							bestMatch = {
378
+								province: provinceName,
379
+								city: cityName,
380
+								area: matchedArea || ''
381
+							};
382
+							// 如果找到了完整的省市区,可以提前返回
383
+							if (matchedArea) {
384
+								return bestMatch;
385
+							}
386
+						}
387
+						continue;
388
+					}
389
+					
390
+					// 非直辖市:查找市
391
+					if (province.childs && province.childs.length > 0) {
392
+						for (const city of province.childs) {
393
+							const cityName = city.name;
394
+							// 移除可能的"市"、"州"、"盟"、"地区"、"自治州"等后缀
395
+							const cityNameShort = cityName.replace(/市|州|盟|地区|自治州/g, '');
396
+							
397
+							// 检查剩余地址中是否包含市名称
398
+							const hasCityFull = remainingAfterProvince.includes(cityName);
399
+							const hasCityShort = remainingAfterProvince.includes(cityNameShort);
400
+							
401
+							if (hasCityFull || hasCityShort) {
402
+								const cityIndex = hasCityFull
403
+									? remainingAfterProvince.indexOf(cityName)
404
+									: remainingAfterProvince.indexOf(cityNameShort);
405
+								
406
+								if (cityIndex === -1) continue;
407
+								
408
+								// 获取市后的剩余地址
409
+								const cityMatchLength = hasCityFull ? cityName.length : cityNameShort.length;
410
+								const remainingAfterCity = remainingAfterProvince.substring(cityIndex + cityMatchLength);
411
+								
412
+								// 查找区/县
413
+								let matchedArea = null;
414
+								let maxAreaLength = 0;
415
+								if (city.childs && city.childs.length > 0) {
416
+									for (const area of city.childs) {
417
+										const areaName = area.name;
418
+										// 检查剩余地址中是否包含区/县名称
419
+										if (remainingAfterCity.includes(areaName)) {
420
+											// 找到最长的匹配(更准确)
421
+											if (areaName.length > maxAreaLength) {
422
+												maxAreaLength = areaName.length;
423
+												matchedArea = areaName;
424
+											}
425
+										}
426
+									}
427
+								}
428
+								
429
+								// 计算匹配分数:完整省市区得3分,只有省市得2分
430
+								const matchScore = matchedArea ? 3 : 2;
431
+								if (matchScore > bestMatchScore) {
432
+									bestMatchScore = matchScore;
433
+									bestMatch = {
434
+										province: provinceName,
435
+										city: cityName,
436
+										area: matchedArea || ''
437
+									};
438
+									// 如果找到了完整的省市区,可以提前返回
439
+									if (matchedArea) {
440
+										return bestMatch;
441
+									}
442
+								}
443
+							}
444
+						}
445
+					}
446
+					
447
+					// 如果只找到了省份,也记录(得分1分)
448
+					if (!bestMatch || bestMatchScore < 1) {
449
+						bestMatch = {
450
+							province: provinceName,
451
+							city: '',
452
+							area: ''
453
+						};
454
+						bestMatchScore = 1;
455
+					}
456
+				}
457
+			}
458
+			
459
+			return bestMatch;
293 460
 		},
294 461
 
295 462
 		// 更新各类附件

文件差异内容过多而无法显示
+ 644 - 194
pages/pagereceivecenter/pagereceivecenter.vue


+ 2 - 2
pages/person/index.vue

@@ -42,11 +42,11 @@
42 42
 							<image src="@/static/parson/icon-myMessage.png" mode=""></image>
43 43
 							<text>我的消息</text>
44 44
 						</navigator>
45
-						<navigator url='/pages/pagereceivecenter/pagereceivecenter' open-type="navigate"
45
+						<!-- <navigator url='/pages/order/index' open-type="navigate"
46 46
 							class="menu_item" hover-class="none">
47 47
 							<image src="@/static/parson/icon-mycase.png" mode=""></image>
48 48
 							<text>接单中心</text>
49
-						</navigator>
49
+						</navigator> -->
50 50
 						<navigator url='/pages/privateClue/index' open-type="switchTab" class="menu_item"
51 51
 							hover-class="none">
52 52
 							<image src="@/static/parson/clue.png" mode=""></image>

+ 204 - 204
pages/uploadRecord/index.vue

@@ -7,17 +7,17 @@
7 7
 		</u-navbar>
8 8
 		<view class="form_wrap">
9 9
 			<u--form labelPosition="left" labelWidth="80" :model="form" :rules="rules" ref="form" class="form_wrap">
10
-				
10
+
11 11
 				<u-form-item label="线索id">
12
-					{{clueDetail && clueDetail.id ? clueDetail.id : "-"}}
12
+					{{ clueDetail && clueDetail.id ? clueDetail.id : "-" }}
13 13
 				</u-form-item>
14
-				
14
+
15 15
 				<u-form-item label="线索名称">
16
-					{{clueDetail && clueDetail.name ? clueDetail.name : "-"}}
16
+					{{ clueDetail && clueDetail.name ? clueDetail.name : "-" }}
17 17
 				</u-form-item>
18
-				
18
+
19 19
 				<u-form-item label="操作人">
20
-					{{userInfo.nickName}}
20
+					{{ userInfo.nickName }}
21 21
 				</u-form-item>
22 22
 
23 23
 				<u-form-item label="主叫号码" prop="caller">
@@ -40,14 +40,14 @@
40 40
 				</u-form-item>
41 41
 			</u--form>
42 42
 		</view>
43
-		
43
+
44 44
 		<!-- 录音确认对话框 -->
45 45
 		<u-popup :show="showConfirmDialog" mode="center" :closeable="false" :closeOnClickOverlay="false">
46 46
 			<view class="confirm-dialog">
47 47
 				<view class="dialog-title">确认录音</view>
48 48
 				<view class="dialog-content">
49 49
 					<view class="file-info">
50
-						<view class="file-name">{{selectedFile.fileName}}</view>
50
+						<view class="file-name">{{ selectedFile.fileName }}</view>
51 51
 					</view>
52 52
 					<view class="play-section">
53 53
 						<audio :src="selectedFile.filePath" controls class="audio-player"></audio>
@@ -59,226 +59,226 @@
59 59
 				</view>
60 60
 			</view>
61 61
 		</u-popup>
62
-		
62
+
63 63
 		<!-- <drag-button :isDock="true" /> -->
64 64
 	</view>
65 65
 </template>
66 66
 
67 67
 <script>
68
-	export default {
69
-		computed: {
70
-			userInfo() {
71
-				return this.$store.state.user.userInfo;
72
-			},
73
-			filelist(){
74
-				return this.$store.state.call.filelist;
75
-			},
76
-			storeForm() {
77
-				return this.$store.state.call.form;
78
-			},
68
+export default {
69
+	computed: {
70
+		userInfo() {
71
+			return this.$store.state.user.userInfo;
79 72
 		},
80
-		data() {
81
-			return {
82
-				rules: {
83
-					'caller': {
84
-						type: 'string',
85
-						required: true,
86
-						message: '请输入内容',
87
-						trigger: ['blur', 'change']
88
-					},
89
-					'callee': {
90
-						type: 'string',
91
-						required: true,
92
-						message: '请输入内容',
93
-						trigger: ['blur', 'change']
94
-					},
95
-					'fileUrl': {
96
-						type: 'string',
97
-						required: true,
98
-						message: '请输入内容',
99
-						trigger: ['blur', 'change']
100
-					},
73
+		filelist() {
74
+			return this.$store.state.call.filelist;
75
+		},
76
+		storeForm() {
77
+			return this.$store.state.call.form;
78
+		},
79
+	},
80
+	data() {
81
+		return {
82
+			rules: {
83
+				'caller': {
84
+					type: 'string',
85
+					required: true,
86
+					message: '请输入内容',
87
+					trigger: ['blur', 'change']
101 88
 				},
102
-				form: {
103
-					clueId: undefined,
104
-					remark: '',
105
-					fileUrl : undefined,
106
-					fileName : undefined,
107
-					type : '3',
108
-					list : [],
109
-					caller : '',
110
-					callee : ''
89
+				'callee': {
90
+					type: 'string',
91
+					required: true,
92
+					message: '请输入内容',
93
+					trigger: ['blur', 'change']
111 94
 				},
112
-				clueDetail : {},
113
-				// 对话框相关状态
114
-				showConfirmDialog: false,
115
-				selectedFile: {
116
-					filePath: '',
117
-					fileName: ''
95
+				'fileUrl': {
96
+					type: 'string',
97
+					required: true,
98
+					message: '请输入内容',
99
+					trigger: ['blur', 'change']
118 100
 				},
119
-			}
120
-		},
121
-		methods: {
122
-			initFormFromStore() {
123
-				const storeFormData = this.storeForm;
124
-				this.form = {
125
-					...this.form,
126
-					caller: storeFormData.caller || '',
127
-					callee: storeFormData.callee || '',
128
-					fileUrl: storeFormData.fileUrl,
129
-					fileName: storeFormData.fileName,
130
-					remark: storeFormData.remark,
131
-					type: storeFormData.type,
132
-					list: storeFormData.list
133
-				};
134
-				
135
-				uni.$u.api.getClueMainInfoById({id : this.form.clueId}).then(({data})=>{
136
-					this.clueDetail = data;
137
-				})
138
-				
139
-				if(!this.form.callee){
140
-					// 没有准备被叫号码 直接去查线索的telephone
141
-					this.form.callee = this.clueDetail.telephone;
142
-				};
143
-				if(!this.form.caller){
144
-					// 没有准备主叫号码 直接去尝试获取
145
-					this.$store.dispatch("call/getUserPhoneNumber").then(phone=>{
146
-						this.form.caller = phone;
147
-					})
148
-				}
149
-				if(storeFormData.fileUrl){
150
-					// 初始化带了fileUrl 说明是从监听那边跳转的
151
-					this.handleFileChange({ filePath : storeFormData.fileUrl , fileName : storeFormData.fileName });
152
-				}
153
-				this.$store.dispatch("call/resetForm");
154 101
 			},
155
-			
156
-			handleFileChange(file){
157
-				// {
158
-				    // "filePath": "/storage/emulated/0/Recordings/Record/Call/15099989786 2025-11-11 09-41-57.m4a",
159
-				    // "fileName": "15099989786 2025-11-11 09-41-57.m4a"
160
-				// }
161
-				
162
-				const {filePath ,  fileName} = file;
163
-				
164
-				// 显示确认对话框
165
-				this.selectedFile = { filePath, fileName };
166
-				this.showConfirmDialog = true;
102
+			form: {
103
+				clueId: undefined,
104
+				remark: '',
105
+				fileUrl: undefined,
106
+				fileName: undefined,
107
+				type: '3',
108
+				list: [],
109
+				caller: '',
110
+				callee: ''
167 111
 			},
168
-			
169
-			// 取消确认
170
-			cancelConfirm() {
171
-				this.showConfirmDialog = false;
172
-				this.selectedFile = { filePath: '', fileName: '' };
173
-				this.form.fileUrl = undefined;
174
-				this.form.fileName = undefined;
175
-			},
176
-			
177
-			// 确认上传
178
-			async confirmUpload() {
179
-				try {
180
-					// 调用上传接口
181
-					const result = await uni.$u.api.uploadFile(this.selectedFile.filePath);
182
-					if (result && result.data) {
183
-						result.data.remark = this.form.remark;
184
-						result.data.fileName = result.data.name;
185
-						result.data.fileUrl = result.data.url;
186
-						this.form.list = [result.data];
187
-					}
188
-					// 设置表单值
189
-					this.form.fileUrl = this.selectedFile.filePath;
190
-					this.form.fileName = this.selectedFile.fileName;
191
-					
192
-					// 关闭对话框
193
-					this.showConfirmDialog = false;
194
-				} catch (error) {
195
-					uni.hideLoading();
196
-					console.error('上传失败:', error);
197
-					uni.$u.toast('上传失败,请重试');
198
-				}
112
+			clueDetail: {},
113
+			// 对话框相关状态
114
+			showConfirmDialog: false,
115
+			selectedFile: {
116
+				filePath: '',
117
+				fileName: ''
199 118
 			},
200
-			
201
-			handleNavSaveClick() {
202
-				if(!this.form.clueId){
203
-					uni.$u.toast("相关线索id为空");
204
-				}
205
-				this.$refs.form.validate().then(async () => {
206
-					await uni.$u.api.saveClueFile(this.form);
207
-					uni.$u.toast("保存成功");
208
-					this.timer = setTimeout(() => {
209
-						uni.$emit('uploadRecordSuccess');
210
-						uni.navigateBack();
211
-						clearTimeout(this.timer);
212
-					}, 1000)
119
+		}
120
+	},
121
+	methods: {
122
+		initFormFromStore() {
123
+			const storeFormData = this.storeForm;
124
+			this.form = {
125
+				...this.form,
126
+				caller: storeFormData.caller || '',
127
+				callee: storeFormData.callee || '',
128
+				fileUrl: storeFormData.fileUrl,
129
+				fileName: storeFormData.fileName,
130
+				remark: storeFormData.remark,
131
+				type: storeFormData.type,
132
+				list: storeFormData.list
133
+			};
134
+
135
+			uni.$u.api.getClueMainInfoById({ id: this.form.clueId }).then(({ data }) => {
136
+				this.clueDetail = data;
137
+			})
138
+
139
+			if (!this.form.callee) {
140
+				// 没有准备被叫号码 直接去查线索的telephone
141
+				this.form.callee = this.clueDetail.telephone;
142
+			};
143
+			if (!this.form.caller) {
144
+				// 没有准备主叫号码 直接去尝试获取
145
+				this.$store.dispatch("call/getUserPhoneNumber").then(phone => {
146
+					this.form.caller = phone;
213 147
 				})
214
-			},
148
+			}
149
+			if (storeFormData.fileUrl) {
150
+				// 初始化带了fileUrl 说明是从监听那边跳转的
151
+				this.handleFileChange({ filePath: storeFormData.fileUrl, fileName: storeFormData.fileName });
152
+			}
153
+			this.$store.dispatch("call/resetForm");
215 154
 		},
216
-		onLoad(option) {
217
-			this.$store.dispatch("call/getFileList");
218
-			this.form.clueId = option.clueId;
219
-			this.initFormFromStore();
155
+
156
+		handleFileChange(file) {
157
+			// {
158
+			// "filePath": "/storage/emulated/0/Recordings/Record/Call/15099989786 2025-11-11 09-41-57.m4a",
159
+			// "fileName": "15099989786 2025-11-11 09-41-57.m4a"
160
+			// }
161
+
162
+			const { filePath, fileName } = file;
163
+
164
+			// 显示确认对话框
165
+			this.selectedFile = { filePath, fileName };
166
+			this.showConfirmDialog = true;
220 167
 		},
221
-	}
168
+
169
+		// 取消确认
170
+		cancelConfirm() {
171
+			this.showConfirmDialog = false;
172
+			this.selectedFile = { filePath: '', fileName: '' };
173
+			this.form.fileUrl = undefined;
174
+			this.form.fileName = undefined;
175
+		},
176
+
177
+		// 确认上传
178
+		async confirmUpload() {
179
+			try {
180
+				// 调用上传接口
181
+				const result = await uni.$u.api.uploadFile(this.selectedFile.filePath);
182
+				if (result && result.data) {
183
+					result.data.remark = this.form.remark;
184
+					result.data.fileName = result.data.name;
185
+					result.data.fileUrl = result.data.url;
186
+					this.form.list = [result.data];
187
+				}
188
+				// 设置表单值
189
+				this.form.fileUrl = this.selectedFile.filePath;
190
+				this.form.fileName = this.selectedFile.fileName;
191
+
192
+				// 关闭对话框
193
+				this.showConfirmDialog = false;
194
+			} catch (error) {
195
+				uni.hideLoading();
196
+				console.error('上传失败:', error);
197
+				uni.$u.toast('上传失败,请重试');
198
+			}
199
+		},
200
+
201
+		handleNavSaveClick() {
202
+			if (!this.form.clueId) {
203
+				uni.$u.toast("相关线索id为空");
204
+			}
205
+			this.$refs.form.validate().then(async () => {
206
+				await uni.$u.api.saveClueFile(this.form);
207
+				uni.$u.toast("保存成功");
208
+				this.timer = setTimeout(() => {
209
+					uni.$emit('uploadRecordSuccess');
210
+					uni.navigateBack();
211
+					clearTimeout(this.timer);
212
+				}, 1000)
213
+			})
214
+		},
215
+	},
216
+	onLoad(option) {
217
+		this.$store.dispatch("call/getFileList");
218
+		this.form.clueId = option.clueId;
219
+		this.initFormFromStore();
220
+	},
221
+}
222 222
 </script>
223 223
 
224 224
 <style lang="scss" scoped>
225
-	.form_wrap {
226
-		background-color: #fff;
227
-		margin: 20rpx 0;
225
+.form_wrap {
226
+	background-color: #fff;
227
+	margin: 20rpx 0;
228 228
 
229
-		.form_wrap {
230
-			::v-deep .u-form-item__body {
231
-				padding: 20rpx 40rpx;
232
-			}
229
+	.form_wrap {
230
+		::v-deep .u-form-item__body {
231
+			padding: 20rpx 40rpx;
233 232
 		}
234 233
 	}
235
-	
236
-	/* 录音确认对话框样式 */
237
-	.confirm-dialog {
238
-		padding: 40rpx;
239
-		min-width: 500rpx;
240
-		background: #fff;
241
-		border-radius: 20rpx;
242
-		
243
-		.dialog-title {
244
-			text-align: center;
245
-			font-size: 32rpx;
246
-			font-weight: bold;
247
-			color: #333;
248
-			margin-bottom: 40rpx;
249
-		}
250
-		
251
-		.dialog-content {
252
-			margin-bottom: 40rpx;
253
-			
254
-			.file-info {
255
-				margin-bottom: 30rpx;
256
-				
257
-				.file-name {
258
-					background: #f5f5f5;
259
-					padding: 20rpx;
260
-					border-radius: 10rpx;
261
-					font-size: 28rpx;
262
-					color: #333;
263
-					word-break: break-all;
264
-					text-align: center;
265
-				}
266
-			}
267
-			
268
-			.play-section {
269
-				.audio-player {
270
-					width: 100%;
271
-				}
234
+}
235
+
236
+/* 录音确认对话框样式 */
237
+.confirm-dialog {
238
+	padding: 40rpx;
239
+	min-width: 500rpx;
240
+	background: #fff;
241
+	border-radius: 20rpx;
242
+
243
+	.dialog-title {
244
+		text-align: center;
245
+		font-size: 32rpx;
246
+		font-weight: bold;
247
+		color: #333;
248
+		margin-bottom: 40rpx;
249
+	}
250
+
251
+	.dialog-content {
252
+		margin-bottom: 40rpx;
253
+
254
+		.file-info {
255
+			margin-bottom: 30rpx;
256
+
257
+			.file-name {
258
+				background: #f5f5f5;
259
+				padding: 20rpx;
260
+				border-radius: 10rpx;
261
+				font-size: 28rpx;
262
+				color: #333;
263
+				word-break: break-all;
264
+				text-align: center;
272 265
 			}
273 266
 		}
274
-		
275
-		.dialog-buttons {
276
-			display: flex;
277
-			gap: 30rpx;
278
-			
279
-			::v-deep .u-button {
280
-				flex: 1;
267
+
268
+		.play-section {
269
+			.audio-player {
270
+				width: 100%;
281 271
 			}
282 272
 		}
283 273
 	}
274
+
275
+	.dialog-buttons {
276
+		display: flex;
277
+		gap: 30rpx;
278
+
279
+		::v-deep .u-button {
280
+			flex: 1;
281
+		}
282
+	}
283
+}
284 284
 </style>