Просмотр исходного кода

聊天附件、报价附件、高清图附件、其他附件、前端附件列表以及上传功能,前端跟进记录列表

chenyidong месяцев назад: 3
Родитель
Сommit
cc5cca26ac

+ 29 - 23
components/mk-upload/mk-upload.vue

@@ -4,59 +4,65 @@
4
 		:class="{'reverse': reverse}"
4
 		:class="{'reverse': reverse}"
5
 	>
5
 	>
6
 		<!--进行flex占位-->
6
 		<!--进行flex占位-->
7
-		<block v-if="imgList.length >= column && reverse">
8
-			<block v-if="imgItemAppendLength === 0">
7
+		<view v-if="imgList.length >= column && reverse">
8
+			<view v-if="imgItemAppendLength === 0">
9
 				<view
9
 				<view
10
 					class="mk-upload_item"
10
 					class="mk-upload_item"
11
 					:class="uploadItemColumn"
11
 					:class="uploadItemColumn"
12
+					:key="index"
12
 					v-for="(item, index) in (imgItemAppendLength + (column -1))"
13
 					v-for="(item, index) in (imgItemAppendLength + (column -1))"
13
 				/>
14
 				/>
14
-			</block>
15
-			<block v-if="imgItemAppendLength === 1">
16
-				<block v-if="column == 3">
15
+			</view>
16
+			<view v-if="imgItemAppendLength === 1">
17
+				<view v-if="column == 3">
17
 					<view
18
 					<view
18
 						class="mk-upload_item"
19
 						class="mk-upload_item"
20
+						:key="index"
19
 						:class="uploadItemColumn"
21
 						:class="uploadItemColumn"
20
 						v-for="(item, index) in imgItemAppendLength"
22
 						v-for="(item, index) in imgItemAppendLength"
21
 					/>
23
 					/>
22
-				</block>
23
-				<block v-if="column == 4">
24
+				</view>
25
+				<view v-if="column == 4">
24
 					<view
26
 					<view
25
 						class="mk-upload_item"
27
 						class="mk-upload_item"
26
 						:class="uploadItemColumn"
28
 						:class="uploadItemColumn"
29
+						:key="index"
27
 						v-for="(item, index) in (imgItemAppendLength + 1)"
30
 						v-for="(item, index) in (imgItemAppendLength + 1)"
28
 					/>
31
 					/>
29
-				</block>
30
-				<block v-if="column == 5">
32
+				</view>
33
+				<view v-if="column == 5">
31
 					<view
34
 					<view
32
 						class="mk-upload_item"
35
 						class="mk-upload_item"
33
 						:class="uploadItemColumn"
36
 						:class="uploadItemColumn"
37
+						:key="index"
34
 						v-for="(item, index) in (imgItemAppendLength + 2)"
38
 						v-for="(item, index) in (imgItemAppendLength + 2)"
35
 					/>
39
 					/>
36
-				</block>
37
-			</block>
38
-			<block v-if="imgItemAppendLength === 2">
39
-				<block v-if="column == 4">
40
+				</view>
41
+			</view>
42
+			<view v-if="imgItemAppendLength === 2">
43
+				<view v-if="column == 4">
40
 					<view
44
 					<view
41
 						class="mk-upload_item"
45
 						class="mk-upload_item"
42
 						:class="uploadItemColumn"
46
 						:class="uploadItemColumn"
47
+						:key="index"
43
 						v-for="(item, index) in (imgItemAppendLength - 1)"
48
 						v-for="(item, index) in (imgItemAppendLength - 1)"
44
 					/>
49
 					/>
45
-				</block>
46
-				<block v-if="column == 5">
50
+				</view>
51
+				<view v-if="column == 5">
47
 					<view
52
 					<view
48
 						class="mk-upload_item"
53
 						class="mk-upload_item"
49
 						:class="uploadItemColumn"
54
 						:class="uploadItemColumn"
55
+						:key="index"
50
 						v-for="(item, index) in imgItemAppendLength"
56
 						v-for="(item, index) in imgItemAppendLength"
51
 					/>
57
 					/>
52
-				</block>
53
-			</block>
54
-			<block v-if="imgItemAppendLength === 3">
55
-				<block v-if="column == 5">
58
+				</view>
59
+			</view>
60
+			<view v-if="imgItemAppendLength === 3">
61
+				<view v-if="column == 5">
56
 					<view class="mk-upload_item" :class="uploadItemColumn"/>
62
 					<view class="mk-upload_item" :class="uploadItemColumn"/>
57
-				</block>
58
-			</block>
59
-		</block>
63
+				</view>
64
+			</view>
65
+		</view>
60
 		<!--进行flex占位-->
66
 		<!--进行flex占位-->
61
 		
67
 		
62
 		<view 
68
 		<view 
@@ -272,7 +278,7 @@
272
 				align-items: flex-start;
278
 				align-items: flex-start;
273
 				opacity: 0.8;
279
 				opacity: 0.8;
274
 				position: absolute;
280
 				position: absolute;
275
-				z-index: 2;
281
+				z-index: 1;
276
 				cursor: pointer;
282
 				cursor: pointer;
277
 				box-sizing: border-box;
283
 				box-sizing: border-box;
278
 				text{
284
 				text{

+ 6 - 83
components/order-file-upload/order-file-upload.vue

@@ -26,14 +26,16 @@
26
 </template>
26
 </template>
27
 
27
 
28
 <script>
28
 <script>
29
+	import upload from '../../mixins/upload';
29
 	export default {
30
 	export default {
30
 		name: 'OrderFileUpload',
31
 		name: 'OrderFileUpload',
32
+		mixins : [upload],
31
 		props: {
33
 		props: {
32
 			title: {
34
 			title: {
33
 				type: String,
35
 				type: String,
34
 				default: '文件上传'
36
 				default: '文件上传'
35
 			},
37
 			},
36
-			fileType: {
38
+			orderFileType: {
37
 				type: String,
39
 				type: String,
38
 				required: true
40
 				required: true
39
 			},
41
 			},
@@ -45,18 +47,9 @@
45
 				type: String,
47
 				type: String,
46
 				default: '点击上传文件'
48
 				default: '点击上传文件'
47
 			},
49
 			},
48
-			maxSize: {
49
-				type: Number,
50
-				default: 10 * 1024 * 1024 // 10MB
51
-			},
52
-			allowTypes: {
53
-				type: Array,
54
-				default: () => ['pdf', 'doc', 'docx', 'txt', 'jpg', 'jpeg', 'png', 'gif', 'mp3', 'wav', 'mp4']
55
-			}
56
 		},
50
 		},
57
 		data() {
51
 		data() {
58
 			return {
52
 			return {
59
-				uploadList: []
60
 			}
53
 			}
61
 		},
54
 		},
62
 		watch: {
55
 		watch: {
@@ -68,79 +61,9 @@
68
 			}
61
 			}
69
 		},
62
 		},
70
 		methods: {
63
 		methods: {
71
-			// 处理文件上传
72
-			handleUpload() {
73
-				const lemonjkFileSelect = uni.requireNativePlugin('lemonjk-FileSelect');
74
-				//4.0.0+ 使用示例及高级筛选器配置示例
75
-				lemonjkFileSelect.showNativePicker({
76
-					pathScope: "/DCIM/Camera",
77
-					mimeType: "*/*",
78
-					utisType: "public.data",
79
-					multi: 'yes'
80
-				}, async result => {
81
-					const {
82
-						code,
83
-						files
84
-					} = result;
85
-					if (code == 0) {
86
-						// 选择的是否全部为图片
87
-						const tempFilePaths = files.map(v => "file://" + v.filePath);
88
-						Promise.all(tempFilePaths.map(v => this.uploadFile(v))).then(() => {
89
-							uni.$u.toast('上传成功');
90
-						}).catch(() => {
91
-							uni.$u.toast("上传失败");
92
-						})
93
-					} else if (result.code == 1001) {
94
-						uni.showModal({
95
-							title: "需要文件访问权限",
96
-							content: "您还未授权本应用读取文件。为保证您可以正常上传文件,请在权限设置页面打开文件访问权限(不同手机厂商表述可能略有差异)请根据自己手机品牌设置",
97
-							confirmText: "去授权",
98
-							cancelText: "算了",
99
-							success(e) {
100
-								if (e.confirm) {
101
-									// 跳转到应用设置页
102
-									lemonjkFileSelect.gotoSetting();
103
-								}
104
-							}
105
-						})
106
-					}
107
-				})
64
+			handleUploadSuccess(){
65
+				uni.$u.toast("上传成功");
108
 			},
66
 			},
109
-
110
-			// 上传文件
111
-			async uploadFile(fileUrl) {
112
-				try {
113
-					uni.showLoading({
114
-						title: '上传中...',
115
-						mask: true
116
-					});
117
-
118
-					// 调用统一的上传接口
119
-					const {
120
-						data
121
-					} = await uni.$u.api.uploadFile(fileUrl);
122
-
123
-					const fileObj = {
124
-						fileSize: data.fileSize,
125
-						fileSuffix: data.fileSuffix,
126
-						fileName: data.name,
127
-						fileUrl: data.url,
128
-						orderFileType : this.fileType
129
-					};
130
-
131
-					this.uploadList.push(fileObj);
132
-					this.$emit('update:fileList', this.uploadList);
133
-					this.$emit('change', this.uploadList);
134
-
135
-					uni.hideLoading();
136
-
137
-				} catch (error) {
138
-					uni.hideLoading();
139
-					console.error('文件上传失败:', error);
140
-					uni.$u.toast('上传失败,请重试');
141
-				}
142
-			},
143
-
144
 			// 删除文件
67
 			// 删除文件
145
 			handleDelete(index) {
68
 			handleDelete(index) {
146
 				uni.showModal({
69
 				uni.showModal({
@@ -151,7 +74,7 @@
151
 							this.uploadList.splice(index, 1);
74
 							this.uploadList.splice(index, 1);
152
 							this.$emit('update:fileList', this.uploadList);
75
 							this.$emit('update:fileList', this.uploadList);
153
 							this.$emit('change', {
76
 							this.$emit('change', {
154
-								type: this.fileType,
77
+								type: this.orderFileType,
155
 								fileList: this.uploadList
78
 								fileList: this.uploadList
156
 							});
79
 							});
157
 						}
80
 						}

+ 131 - 0
mixins/upload.js

@@ -0,0 +1,131 @@
1
+export default {
2
+    data() {
3
+        return {
4
+            uploadList: []
5
+        }
6
+    },
7
+    computed: {
8
+    },
9
+    methods: {
10
+        // 处理文件上传
11
+        handleUpload() {
12
+            // #ifdef H5
13
+            this.handleH5Upload();
14
+            // #endif
15
+
16
+            // #ifdef APP-PLUS
17
+            this.handleAppUpload();
18
+            // #endif
19
+        },
20
+        // H5 平台文件上传
21
+        handleH5Upload() {
22
+            uni.chooseFile({
23
+                count: 100, // 最多可以选择100个文件
24
+                type: 'all', // 选择所有类型文件
25
+                success: async (res) => {
26
+                    const { tempFilePaths, tempFiles } = res;
27
+
28
+                    if (!tempFiles || tempFiles.length === 0) {
29
+                        return;
30
+                    }
31
+
32
+                    // 验证文件大小和类型
33
+                    const validFiles = [];
34
+                    for (let i = 0; i < tempFiles.length; i++) {
35
+                        const filePath = tempFilePaths[i];
36
+                        validFiles.push(filePath);
37
+                    }
38
+
39
+                    if (validFiles.length === 0) {
40
+                        return;
41
+                    }
42
+
43
+                    try {
44
+                        await Promise.all(validFiles.map(filePath => this.uploadFile(filePath)));
45
+                        this.handleUploadSuccess();
46
+                    } catch (error) {
47
+                        console.error('上传失败:', error);
48
+                        uni.$u.toast('上传失败');
49
+                    }
50
+                },
51
+                fail: (err) => {
52
+                    console.error('选择文件失败:', err);
53
+                    uni.$u.toast('选择文件失败');
54
+                }
55
+            });
56
+        },
57
+
58
+        // 上传文件
59
+        async uploadFile(fileUrl) {
60
+            try {
61
+                uni.showLoading({
62
+                    title: '上传中...',
63
+                    mask: true
64
+                });
65
+
66
+                // 调用统一的上传接口
67
+                const {
68
+                    data
69
+                } = await uni.$u.api.uploadFile(fileUrl);
70
+
71
+                const fileObj = {
72
+                    fileSize: data.fileSize,
73
+                    fileSuffix: data.fileSuffix,
74
+                    fileName: data.name,
75
+                    fileUrl: data.url,
76
+                    orderFileType: this.orderFileType
77
+                };
78
+
79
+                this.uploadList.push(fileObj);
80
+                this.$emit('update:fileList', this.uploadList);
81
+                this.$emit('change', this.uploadList);
82
+
83
+                uni.hideLoading();
84
+
85
+            } catch (error) {
86
+                uni.hideLoading();
87
+                console.error('文件上传失败:', error);
88
+                uni.$u.toast('上传失败,请重试');
89
+            }
90
+        },
91
+
92
+        // App 平台文件上传
93
+        handleAppUpload() {
94
+            const lemonjkFileSelect = uni.requireNativePlugin('lemonjk-FileSelect');
95
+            //4.0.0+ 使用示例及高级筛选器配置示例
96
+            lemonjkFileSelect.showNativePicker({
97
+                pathScope: "/DCIM/Camera",
98
+                mimeType: "*/*",
99
+                utisType: "public.data",
100
+                multi: 'yes'
101
+            }, async result => {
102
+                const {
103
+                    code,
104
+                    files
105
+                } = result;
106
+                if (code == 0) {
107
+                    // 选择的是否全部为图片
108
+                    const tempFilePaths = files.map(v => "file://" + v.filePath);
109
+                    Promise.all(tempFilePaths.map(v => this.uploadFile(v))).then(() => {
110
+                        this.handleUploadSuccess();
111
+                    }).catch(() => {
112
+                        uni.$u.toast("上传失败");
113
+                    })
114
+                } else if (result.code == 1001) {
115
+                    uni.showModal({
116
+                        title: "需要文件访问权限",
117
+                        content: "您还未授权本应用读取文件。为保证您可以正常上传文件,请在权限设置页面打开文件访问权限(不同手机厂商表述可能略有差异)请根据自己手机品牌设置",
118
+                        confirmText: "去授权",
119
+                        cancelText: "算了",
120
+                        success(e) {
121
+                            if (e.confirm) {
122
+                                // 跳转到应用设置页
123
+                                lemonjkFileSelect.gotoSetting();
124
+                            }
125
+                        }
126
+                    })
127
+                }
128
+            })
129
+        },
130
+    },
131
+}

+ 0 - 6
pages/clueDetail/tabs/followRecord/index.vue

@@ -45,7 +45,6 @@
45
 </template>
45
 </template>
46
 <script>
46
 <script>
47
 	import {
47
 	import {
48
-		groupBy,
49
 		isEmpty
48
 		isEmpty
50
 	} from "lodash";
49
 	} from "lodash";
51
 	export default {
50
 	export default {
@@ -60,11 +59,6 @@
60
 			type: {
59
 			type: {
61
 				type: String,
60
 				type: String,
62
 				required: true
61
 				required: true
63
-				// '1' : 线索跟进
64
-				// '2' : 订单跟进(通过orderId)
65
-				// '3' : 订单跟进(通过clueId)
66
-				// '4' : 重复线索跟进
67
-				// 其他 : 重复订单跟进
68
 			}
62
 			}
69
 		},
63
 		},
70
 		data() {
64
 		data() {

+ 3 - 2
pages/order/components/orderCenter/orderCenter.vue

@@ -121,10 +121,11 @@
121
 			handleToDetail(row) {
121
 			handleToDetail(row) {
122
 				const {
122
 				const {
123
 					id,
123
 					id,
124
-					item
124
+					item,
125
+					clueId
125
 				} = row;
126
 				} = row;
126
 				uni.navigateTo({
127
 				uni.navigateTo({
127
-					url: `/pages/orderDetail/index?orderId=${id}&item=${item}&type=${this.queryParams.type}`,
128
+					url: `/pages/orderDetail/index?orderId=${id}&item=${item}&type=${this.queryParams.type}&clueId=${clueId}`,
128
 				});
129
 				});
129
 			},
130
 			},
130
 			handleKeyword() {
131
 			handleKeyword() {

+ 4 - 3
pages/orderDetail/index.vue

@@ -7,7 +7,7 @@
7
 				</view>
7
 				</view>
8
 			</template>
8
 			</template>
9
 		</u-navbar>
9
 		</u-navbar>
10
-		<detail :orderId="orderId" :params="params"></detail>
10
+		<detail :orderId="orderId" :clueId="clueId" :params="params"></detail>
11
 	</view>
11
 	</view>
12
 </template>
12
 </template>
13
 
13
 
@@ -26,11 +26,11 @@
26
 			})
26
 			})
27
 		},
27
 		},
28
 		onLoad(option) {
28
 		onLoad(option) {
29
-			const {	item , orderId , type } = option;
30
-			console.log(option);
29
+			const {	item , orderId , type , clueId } = option;
31
 			this.item = item;
30
 			this.item = item;
32
 			this.orderId = orderId;
31
 			this.orderId = orderId;
33
 			this.params = option;
32
 			this.params = option;
33
+			this.clueId = clueId;
34
 			uni.setNavigationBarTitle({
34
 			uni.setNavigationBarTitle({
35
 				title: item
35
 				title: item
36
 			});
36
 			});
@@ -39,6 +39,7 @@
39
 			return {
39
 			return {
40
 				item: "", 
40
 				item: "", 
41
 				orderId: "",
41
 				orderId: "",
42
+				clueId: "",
42
 				params : {},
43
 				params : {},
43
 			}
44
 			}
44
 		},
45
 		},

Разница между файлами не показана из-за своего большого размера
+ 631 - 483
pages/orderDetail/page/detail.vue


+ 0 - 196
pages/orderDetail/tabs/advertising/index.vue

@@ -1,196 +0,0 @@
1
-<template>
2
-	<view class="caseInfo_wrap">
3
-		<template>
4
-			<view class="caseInfo_content_wrap">
5
-				<view class="caseInfo_title">
6
-					<image src="/static/clueDetail/icon-caseInfo.png" mode=""></image>
7
-					<text class="info_text">广告信息</text>
8
-				</view>
9
-				<view class="caseInfo_main_wrap">
10
-					<view class="Info_item" v-for="(item,index) in caseInfoColumn" :key="item.prop">
11
-						<text class="label">{{item.label}}</text>
12
-						<view v-if="item.prop === 'advName'" class="advName_wrap">
13
-							<view class="copy_btn" @click="handleCopy(clueAdInfo[item.prop])">复制</view>
14
-							<text class="value">{{clueAdInfo[item.prop] ? clueAdInfo[item.prop] : '-'}}</text>
15
-						</view>
16
-						<text v-else-if="item.color" class="value highlight" :style="handleStyle(item.color)">
17
-							{{clueAdInfo[item.prop]}}
18
-						</text>
19
-						<show-real-text v-else-if="item.dsProp" class="value" :real="clueAdInfo[item.prop]"
20
-							:normal="clueAdInfo[item.dsProp]" :accessType="item.accessType" :key="item.prop + index">
21
-						</show-real-text>
22
-						<text v-else class="value">{{clueAdInfo[item.prop] ? clueAdInfo[item.prop] : '-'}}</text>
23
-					</view>
24
-				</view>
25
-			</view>
26
-		</template>
27
-	</view>
28
-</template>
29
-
30
-<script>
31
-	const caseInfoColumn = [{
32
-			prop: 'advId',
33
-			label: '广告主ID',
34
-		},
35
-		{
36
-			prop: 'advName',
37
-			label: '广告主名称',
38
-		},
39
-		{
40
-			prop: 'promotionId',
41
-			label: '广告ID',
42
-		},
43
-		{
44
-			prop: 'promotionName',
45
-			label: '广告名称',
46
-		},
47
-		{
48
-			prop: 'originExternalUrl',
49
-			label: '落地页链接',
50
-		},
51
-		{
52
-			prop: 'titleId',
53
-			label: '标题ID',
54
-		},
55
-		{
56
-			prop: 'videoId',
57
-			label: '视频ID',
58
-		},
59
-		{
60
-			prop: 'imageId',
61
-			label: '图片ID',
62
-		},
63
-		{
64
-			prop: 'douyinId',
65
-			label: '企业号ID',
66
-		},
67
-		{
68
-			prop: 'douyinName',
69
-			label: '企业号名称',
70
-		},
71
-		{
72
-			prop: 'corporateToolName',
73
-			label: '企业号工具',
74
-		},
75
-		{
76
-			prop: 'clueDataSourceDetail',
77
-			label: '线索渠道详情',
78
-		},
79
-	]
80
-	export default {
81
-		props: {
82
-			clueId: {
83
-				required: true
84
-			},
85
-		},
86
-		methods: {
87
-			handleCopy(value) {
88
-				uni.setClipboardData({
89
-					data: value,
90
-					success: function() {
91
-						uni.$u.toast("复制成功");
92
-					}
93
-				});
94
-			},
95
-			async getData() {
96
-				const {
97
-					data
98
-				} = await uni.$u.api.getClueAdInfoByClueId({
99
-					clueId: this.clueId
100
-				});
101
-				this.clueAdInfo = data ? data : {};
102
-			}
103
-		},
104
-		created() {
105
-			this.getData();
106
-		},
107
-		data() {
108
-			return {
109
-				clueAdInfo: {},
110
-
111
-				caseInfoColumn,
112
-			}
113
-		}
114
-	}
115
-</script>
116
-
117
-<style lang="scss" scoped>
118
-	.caseInfo_wrap {
119
-		min-height: 400px;
120
-		.caseInfo_title {
121
-			display: flex;
122
-			align-items: center;
123
-			height: 80rpx;
124
-			background: #f4f4f6;
125
-			padding-left: 40rpx;
126
-
127
-			image {
128
-				width: 24rpx;
129
-				height: 24rpx;
130
-				margin-right: 10rpx;
131
-			}
132
-
133
-			.info_text {
134
-				font-size: 24rpx;
135
-				color: #202020;
136
-			}
137
-
138
-			.tabs_wrap {
139
-				display: flex;
140
-
141
-				.tab_item {
142
-					margin-left: 30rpx;
143
-				}
144
-			}
145
-		}
146
-
147
-		.caseInfo_main_wrap {
148
-			.caseCards_list_wrap {
149
-				background: #ebf6ff;
150
-				margin-bottom: 10rpx;
151
-				margin-left: 10rpx;
152
-				margin-right: 10rpx;
153
-			}
154
-			
155
-
156
-			.Info_item {
157
-				padding: 18rpx 40rpx;
158
-				display: flex;
159
-				justify-content: space-between;
160
-
161
-				.label {
162
-					color: #999999;
163
-					font-size: 26rpx;
164
-					width: 160rpx;
165
-				}
166
-
167
-				.value {
168
-					font-size: 26rpx;
169
-					color: #202020;
170
-					border-radius: 100rpx;
171
-					text-align: right;
172
-					width: 500rpx;
173
-				}
174
-
175
-				.highlight {
176
-					font-size: 22rpx;
177
-					font-weight: 900;
178
-					padding: 6rpx 20rpx;
179
-				}
180
-			}
181
-			
182
-			.advName_wrap{
183
-				display: flex;
184
-				align-items: center;
185
-				.copy_btn {
186
-					color: #4fa5fe;
187
-					margin-right: 10px;
188
-				}
189
-				.value{
190
-					width: auto;
191
-				}
192
-			}
193
-		}
194
-
195
-	}
196
-</style>

+ 0 - 204
pages/orderDetail/tabs/callRecord/index.vue

@@ -1,204 +0,0 @@
1
-<template>
2
-	<view class="callRecord_wrap">
3
-		<view v-if="dataList.length > 0" class="timeline-list">
4
-			<view v-for="(data, index) in dataList" :key="index" class="timeline-item">
5
-				<view class="timeline-icon">
6
-					<uni-icons type="phone" size="20" color="#6cc040"></uni-icons>
7
-				</view>
8
-				<view class="timeline-content">
9
-					<view class="timestamp">{{ titleText(data) }}</view>
10
-					<view class="info_top">
11
-						<view class="info_left">
12
-							<text class="name_text">{{ data.caller }} 打给 {{ data.callee }}</text>
13
-						</view>
14
-						<view class="info_right">
15
-							{{ data.type === 1 ? data.followState : '' }}
16
-						</view>
17
-						<view class="delete-btn" @tap="handleDelete({ id: data.id })">
18
-							<uni-icons type="trash" size="20" color="#ff6666"></uni-icons>
19
-						</view>
20
-					</view>
21
-					<view class="file_name" v-if="data.type === '3'">{{data.fileName}}</view>
22
-					<view class="call_file">
23
-						<view class="audio-container">
24
-							<audio :src="data.fileUrl" controls ></audio>
25
-						</view>
26
-					</view>
27
-				</view>
28
-			</view>
29
-		</view>
30
-		<view v-else class="empty_wrap">
31
-			<uni-icons type="empty" size="60" color="#cccccc"></uni-icons>
32
-			<text class="empty-text">暂无通话记录</text>
33
-		</view>
34
-	</view>
35
-</template>
36
-<script>
37
-	export default {
38
-		props: {
39
-			clueId: {
40
-				type: [String, Number],
41
-				required: true
42
-			},
43
-			clueDetail: {
44
-				type: Object,
45
-				default: () => ({})
46
-			}
47
-		},
48
-		data() {
49
-			return {
50
-				dataList: []
51
-			}
52
-		},
53
-		methods: {
54
-			titleText(data) {
55
-				return `${data.createTime} 号码(${data.caller}) ${(data.type === 1 ?  data.hangupCauseName: '上传录音')}`
56
-			},
57
-			handleDelete({
58
-				id
59
-			}) {
60
-				uni.showModal({
61
-					title: '提示',
62
-					content: '是否确定删除?',
63
-					confirmColor: '#6cc040',
64
-					success: async (res) => {
65
-						if (res.confirm) {
66
-							try {
67
-								await uni.$u.api.deleteClueFile([id])
68
-								uni.showToast({
69
-									title: '删除成功',
70
-									icon: 'success',
71
-									duration: 2000
72
-								})
73
-								this.getData()
74
-							} catch (error) {
75
-								uni.showToast({
76
-									title: '删除失败',
77
-									icon: 'error',
78
-									duration: 2000
79
-								})
80
-							}
81
-						}
82
-					}
83
-				})
84
-			},
85
-			async getData() {
86
-				const {
87
-					data
88
-				} = await uni.$u.api.getCallClueFileByClueId({
89
-					clueId: this.clueId
90
-				});
91
-				this.dataList = data;
92
-			},
93
-		},
94
-		mounted() {
95
-			this.getData();
96
-		},
97
-		beforeDestroy(){
98
-			uni.$off('uploadRecordSuccess');  
99
-		},
100
-		created() {
101
-			uni.$on('uploadRecordSuccess',()=>{
102
-				this.getData();
103
-			});
104
-		}
105
-	}
106
-</script>
107
-<style lang="scss" scoped>
108
-	.callRecord_wrap {
109
-		position: relative;
110
-		padding: 20rpx;
111
-		min-height: 400px;
112
-		box-sizing: border-box;
113
-
114
-		.loading-mask {
115
-			position: absolute;
116
-			top: 0;
117
-			left: 0;
118
-			right: 0;
119
-			bottom: 0;
120
-			background-color: rgba(255, 255, 255, 0.8);
121
-			display: flex;
122
-			justify-content: center;
123
-			align-items: center;
124
-			z-index: 999;
125
-		}
126
-
127
-		.timeline-list {
128
-			.timeline-item {
129
-				display: flex;
130
-				margin-bottom: 30rpx;
131
-
132
-				.timeline-icon {
133
-					width: 60rpx;
134
-					height: 60rpx;
135
-					border-radius: 50%;
136
-					background-color: #f1f3f4;
137
-					display: flex;
138
-					justify-content: center;
139
-					align-items: center;
140
-					margin-right: 20rpx;
141
-					flex-shrink: 0;
142
-				}
143
-
144
-				.timeline-content {
145
-					width:690rpx;
146
-
147
-					.timestamp {
148
-						font-size: 24rpx;
149
-						color: #999;
150
-						margin-bottom: 10rpx;
151
-					}
152
-
153
-					.info_top {
154
-						display: flex;
155
-						justify-content: space-between;
156
-						align-items: center;
157
-						margin-bottom: 20rpx;
158
-
159
-						.info_left {
160
-							.name_text {
161
-								font-size: 28rpx;
162
-								font-weight: 500;
163
-							}
164
-						}
165
-
166
-						.info_right {
167
-							color: #6cc040;
168
-							font-size: 26rpx;
169
-						}
170
-					}
171
-					.file_name{
172
-						color: #999;
173
-						font-size: 26rpx;
174
-					}
175
-
176
-					.call_file {
177
-						width: 100%;
178
-						border-radius: 12rpx;
179
-						display: flex;
180
-						align-items: center;
181
-						justify-content: space-between;
182
-						box-sizing: border-box;
183
-
184
-						.audio-container {
185
-							width: 100%;
186
-						}
187
-					}
188
-				}
189
-			}
190
-		}
191
-
192
-		.empty_wrap {
193
-			text-align: center;
194
-			padding: 100rpx 0;
195
-
196
-			.empty-text {
197
-				display: block;
198
-				margin-top: 30rpx;
199
-				color: #999;
200
-				font-size: 28rpx;
201
-			}
202
-		}
203
-	}
204
-</style>

+ 0 - 232
pages/orderDetail/tabs/clueInfo/index.vue

@@ -1,232 +0,0 @@
1
-<template>
2
-	<view class="caseInfo_wrap">
3
-		<template v-if="clueDetail.id">
4
-			<view class="caseInfo_content_wrap">
5
-				<view class="caseInfo_title">
6
-					<image src="/static/clueDetail/icon-caseInfo.png" mode=""></image>
7
-					<text class="info_text">线索基础信息</text>
8
-				</view>
9
-				<view class="caseInfo_main_wrap">
10
-					<template v-if="params.type === '1'">
11
-						<view class="Info_item" v-for="(item,index) in caseInfoColumn" :key="item.prop">
12
-							<text class="label">{{item.label}}</text>
13
-							<text v-if="item.color" class="value highlight" :style="handleStyle(item.color)">
14
-								{{clueDetail[item.prop]}}
15
-							</text>
16
-							<show-real-text :real="clueDetail.telephone" :type='params.type'
17
-								v-if="item.prop === 'telephone'"></show-real-text>
18
-							<text v-else class="value">{{clueDetail[item.prop]}}</text>
19
-						</view>
20
-					</template>
21
-					<template v-if="params.type === '2'">
22
-						<view class="Info_item" v-for="(item,index) in caseInfoColumn" :key="item.prop">
23
-							<text class="label">{{item.label}}</text>
24
-							<view class="input_wrap" v-if="item.input">
25
-								<input v-model="clueDetail[item.prop]" style="text-align: right; padding-right: 10px;"
26
-									@blur="updateClueMainInfo" />
27
-							</view>
28
-							<text v-else-if="item.color" class="value highlight" :style="handleStyle(item.color)">
29
-								{{clueDetail[item.prop]}}
30
-							</text>
31
-							<text v-else class="value">{{clueDetail[item.prop]}}</text>
32
-						</view>
33
-					</template>
34
-				</view>
35
-			</view>
36
-		</template>
37
-	</view>
38
-</template>
39
-
40
-<script>
41
-	import {
42
-		cloneDeep,
43
-		isEqual
44
-	} from 'lodash';
45
-
46
-	const caseInfoColumn = [{
47
-			prop: 'name',
48
-			label: '姓名',
49
-			input: true
50
-		},
51
-		{
52
-			prop: 'telephone',
53
-			label: '电话',
54
-			input: true
55
-		},
56
-		{
57
-			prop: 'genderTypeCode',
58
-			label: '性别',
59
-		},
60
-		{
61
-			prop: 'age',
62
-			label: '年龄',
63
-		},
64
-		{
65
-			prop: 'weixin',
66
-			label: '微信',
67
-			input: true
68
-		},
69
-		{
70
-			prop: 'corporateStatus',
71
-			label: '企业号状态',
72
-		},
73
-		{
74
-			prop: 'userDouyinId',
75
-			label: '抖音号',
76
-		},
77
-		{
78
-			prop: 'autoAddress',
79
-			label: '自动定位城市',
80
-		},
81
-		{
82
-			prop: 'telAddr',
83
-			label: '手机号归属地',
84
-		},
85
-		{
86
-			prop: 'createTime',
87
-			label: '线索创建时间',
88
-		},
89
-		{
90
-			prop: 'updateTime',
91
-			label: '最新修改时间',
92
-		},
93
-		{
94
-			prop: 'address',
95
-			label: '详细地址',
96
-		},
97
-		{
98
-			prop: 'remark',
99
-			label: '备注',
100
-		},
101
-		{
102
-			prop: 'qq',
103
-			label: 'QQ号',
104
-		},
105
-		{
106
-			prop: 'email',
107
-			label: '邮箱',
108
-		},
109
-		{
110
-			prop: 'date',
111
-			label: '日期',
112
-		},
113
-		{
114
-			prop: 'manualAddressCode',
115
-			label: '手动填写地域',
116
-		}
117
-	]
118
-	export default {
119
-		props: {
120
-			clueId: {
121
-				required: true
122
-			},
123
-			params: {
124
-				type: Object,
125
-				required: true
126
-			},
127
-		},
128
-		methods: {
129
-			async updateClueMainInfo() {
130
-				if (!this.clueDetail.name) {
131
-					uni.$u.toast("姓名不能为空");
132
-					return;
133
-				}
134
-				if (!isEqual(this.cloneClueDetail, this.clueDetail)) {
135
-					await uni.$u.api.updateClueMainInfo(this.clueDetail);
136
-					uni.$u.toast("修改成功");
137
-					this.getData();
138
-				}
139
-			},
140
-			async getData() {
141
-				const {
142
-					data
143
-				} = await uni.$u.api.getClueMainInfoById({
144
-					id: this.clueId
145
-				});
146
-				this.clueDetail = data;
147
-				this.cloneClueDetail = cloneDeep(data);
148
-			}
149
-		},
150
-		created() {
151
-			this.getData();
152
-		},
153
-		data() {
154
-			return {
155
-				cloneClueDetail: {},
156
-				clueDetail: {},
157
-
158
-				caseInfoColumn,
159
-			}
160
-		}
161
-	}
162
-</script>
163
-
164
-<style lang="scss" scoped>
165
-	.caseInfo_wrap {
166
-		min-height: 400px;
167
-
168
-		.caseInfo_title {
169
-			display: flex;
170
-			align-items: center;
171
-			height: 80rpx;
172
-			background: #f4f4f6;
173
-			padding-left: 40rpx;
174
-
175
-			image {
176
-				width: 24rpx;
177
-				height: 24rpx;
178
-				margin-right: 10rpx;
179
-			}
180
-
181
-			.info_text {
182
-				font-size: 24rpx;
183
-				color: #202020;
184
-			}
185
-
186
-			.tabs_wrap {
187
-				display: flex;
188
-
189
-				.tab_item {
190
-					margin-left: 30rpx;
191
-				}
192
-			}
193
-		}
194
-
195
-		.caseInfo_main_wrap {
196
-			.caseCards_list_wrap {
197
-				background: #ebf6ff;
198
-				margin-bottom: 10rpx;
199
-				margin-left: 10rpx;
200
-				margin-right: 10rpx;
201
-			}
202
-
203
-			.Info_item {
204
-				padding: 18rpx 40rpx;
205
-				display: flex;
206
-				justify-content: space-between;
207
-
208
-				.label {
209
-					color: #999999;
210
-					font-size: 26rpx;
211
-				}
212
-
213
-				.input_wrap {
214
-					border-bottom: 1px solid #ddd;
215
-				}
216
-
217
-				.value {
218
-					font-size: 26rpx;
219
-					color: #202020;
220
-					border-radius: 100rpx;
221
-				}
222
-
223
-				.highlight {
224
-					font-size: 22rpx;
225
-					font-weight: 900;
226
-					padding: 6rpx 20rpx;
227
-				}
228
-			}
229
-		}
230
-
231
-	}
232
-</style>

+ 348 - 0
pages/orderDetail/tabs/commissionFormList/commissionFormList.vue

@@ -0,0 +1,348 @@
1
+<template>
2
+	<view class="commission-form-list">
3
+		<!-- 头部按钮组 -->
4
+		<view class="commission-header">
5
+			<u-button type="primary" size="mini" icon="plus" @click="handleAdd">新增分成</u-button>
6
+		</view>
7
+
8
+		<!-- 分成信息卡片列表 -->
9
+		<view v-if="loading" class="loading_wrap">
10
+			<u-loading-icon text="加载中" textSize="18"></u-loading-icon>
11
+		</view>
12
+		<view v-else-if="commissionList.length === 0" class="empty_wrap">
13
+			<u-empty text="暂无分成数据"></u-empty>
14
+		</view>
15
+		<view v-else class="card_list">
16
+			<view class="card_item" v-for="(row, index) in commissionList" :key="row.id">
17
+				<view class="card_header">
18
+					<view class="card_title">{{ row.userName || '-' }}</view>
19
+					<view class="card_actions">
20
+						<u-button size="mini" type="text" @click="handleEdit(row)">编辑</u-button>
21
+						<u-button size="mini" type="text" @click="handleDelete(row.id)" style="color: #f56c6c">删除</u-button>
22
+					</view>
23
+				</view>
24
+				<view class="card_body">
25
+					<view class="info_row">
26
+						<view class="info_label">公司:</view>
27
+						<view class="info_value">{{ row.orgName || '-' }}</view>
28
+					</view>
29
+					<view class="info_row">
30
+						<view class="info_label">账户类型:</view>
31
+						<view class="info_value">{{ row.accountType === '1' ? '前端' : '后端' }}</view>
32
+					</view>
33
+					<view class="info_row">
34
+						<view class="info_label">分成比例:</view>
35
+						<view class="info_value highlight">{{ row.commissionRate || '0' }}%</view>
36
+					</view>
37
+					<view class="info_row">
38
+						<view class="info_label">创建时间:</view>
39
+						<view class="info_value">{{ formatTime(row.createTime) }}</view>
40
+					</view>
41
+				</view>
42
+			</view>
43
+		</view>
44
+
45
+		<!-- 分页组件 -->
46
+		<u-pagination
47
+			v-if="total > 0"
48
+			:total="total"
49
+			:current="queryParams.pageNum"
50
+			:page-size="queryParams.pageSize"
51
+			@change="handlePageChange"
52
+			:page-sizes="[10, 20, 50, 100]"
53
+			:show-total="true"
54
+		></u-pagination>
55
+
56
+		<!-- 新增/编辑对话框 -->
57
+		<u-popup v-model="dialogVisible" mode="center" width="500rpx" border-radius="16">
58
+			<view class="dialog_header">{{ dialogTitle }}</view>
59
+			<view class="dialog_body">
60
+				<u-form :model="commissionForm" ref="commissionFormRef" label-width="120rpx">
61
+					<u-form-item label="账户类型" prop="accountType" :required="true">
62
+						<u-radio-group v-model="commissionForm.accountType" :disabled="!isEdit">
63
+							<u-radio name="1" label="前端"></u-radio>
64
+							<u-radio name="2" label="后端"></u-radio>
65
+						</u-radio-group>
66
+					</u-form-item>
67
+					<u-form-item label="分成人" prop="userName" :required="true">
68
+						<u-input v-model="commissionForm.userName" placeholder="请选择分成人" :disabled="!isEdit" />
69
+					</u-form-item>
70
+					<u-form-item label="分成比例(%)" prop="commissionRate" :required="true">
71
+						<u-input v-model.number="commissionForm.commissionRate" type="number" placeholder="请输入分成比例(0-100)" />
72
+					</u-form-item>
73
+				</u-form>
74
+			</view>
75
+			<view class="dialog_footer">
76
+				<u-button @click="handleClose">取消</u-button>
77
+				<u-button type="primary" @click="handleSubmit">确定</u-button>
78
+			</view>
79
+		</u-popup>
80
+	</view>
81
+</template>
82
+
83
+<script>
84
+export default {
85
+	name: 'CommissionFormList',
86
+	props: {
87
+		sendFormId: {
88
+			type: [Number, String],
89
+			required: true
90
+		},
91
+		clueId: {
92
+			type: [Number, String],
93
+			required: true
94
+		}
95
+	},
96
+	data() {
97
+		return {
98
+			commissionList: [],
99
+			total: 0,
100
+			loading: false,
101
+			queryParams: {
102
+				pageNum: 1,
103
+				pageSize: 10
104
+			},
105
+			dialogVisible: false,
106
+			dialogTitle: '新增分成',
107
+			isEdit: true,
108
+			commissionForm: {
109
+				id: null,
110
+				sendFormId: null,
111
+				clueId: null,
112
+				accountType: '1',
113
+				userName: null,
114
+				userId: null,
115
+				commissionRate: 0
116
+			},
117
+			rules: {
118
+				accountType: [{ required: true, message: '请选择账户类型', trigger: 'change' }],
119
+				userName: [{ required: true, message: '请选择分成人', trigger: 'blur' }],
120
+				commissionRate: [
121
+					{ required: true, message: '请输入分成比例', trigger: 'blur' },
122
+					{ type: 'number', min: 0, max: 100, message: '分成比例必须在0-100之间', trigger: 'blur' }
123
+				]
124
+			}
125
+		}
126
+	},
127
+	methods: {
128
+		async getList() {
129
+			this.loading = true
130
+			try {
131
+				const params = {
132
+					...this.queryParams,
133
+					sendFormId: this.sendFormId,
134
+				}
135
+				const res = await uni.$u.api.clueCommissionForm.list(params)
136
+				this.commissionList = res.rows || []
137
+				this.total = res.total || 0
138
+			} catch (error) {
139
+				uni.$u.toast('获取分成列表失败')
140
+			} finally {
141
+				this.loading = false
142
+			}
143
+		},
144
+		// 格式化时间
145
+		formatTime(time) {
146
+			if (!time) return '-'
147
+			const date = new Date(time)
148
+			const year = date.getFullYear()
149
+			const month = String(date.getMonth() + 1).padStart(2, '0')
150
+			const day = String(date.getDate()).padStart(2, '0')
151
+			const hours = String(date.getHours()).padStart(2, '0')
152
+			const minutes = String(date.getMinutes()).padStart(2, '0')
153
+			const seconds = String(date.getSeconds()).padStart(2, '0')
154
+			return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
155
+		},
156
+		handleAdd() {
157
+			this.dialogTitle = '新增分成'
158
+			this.commissionForm = {
159
+				id: null,
160
+				sendFormId: this.sendFormId,
161
+				clueId: this.clueId,
162
+				accountType: '1',
163
+				userName: null,
164
+				userId: null,
165
+				commissionRate: 0
166
+			}
167
+			this.$nextTick(() => {
168
+				if (this.$refs.commissionFormRef) {
169
+					this.$refs.commissionFormRef.clearValidate()
170
+				}
171
+			})
172
+			this.dialogVisible = true
173
+		},
174
+		handleEdit(row) {
175
+			this.dialogTitle = '编辑分成'
176
+			// 深拷贝对象,避免直接修改原数据
177
+			this.commissionForm = JSON.parse(JSON.stringify(row))
178
+			this.dialogVisible = true
179
+		},
180
+		handleDelete(id) {
181
+			uni.showModal({
182
+				title: '警告',
183
+				content: '确定要删除这条分成记录吗?删除后将无法恢复!',
184
+				confirmButtonText: '确定删除',
185
+				cancelButtonText: '取消',
186
+				success: (res) => {
187
+					if (res.confirm) {
188
+						this.deleteRow(id)
189
+					}
190
+				}
191
+			})
192
+		},
193
+		async deleteRow(id) {
194
+			try {
195
+				await uni.$u.api.clueCommissionForm.remove([id])
196
+				uni.$u.toast('删除成功')
197
+				this.getList()
198
+			} catch (error) {
199
+				uni.$u.toast('删除失败,请稍后重试')
200
+			}
201
+		},
202
+		handleSubmit() {
203
+			this.$refs.commissionFormRef.validate(async (valid) => {
204
+				if (valid) {
205
+					// 显示加载状态
206
+					const loading = uni.showLoading({
207
+						title: this.commissionForm.id ? '编辑中...' : '新增中...',
208
+						mask: true
209
+					})
210
+
211
+					try {
212
+						if (this.commissionForm.id) {
213
+							await uni.$u.api.clueCommissionForm.update(this.commissionForm)
214
+							uni.$u.toast('编辑成功')
215
+						} else {
216
+							await uni.$u.api.clueCommissionForm.add(this.commissionForm)
217
+							uni.$u.toast('新增成功')
218
+						}
219
+						this.dialogVisible = false
220
+						this.getList()
221
+					} catch (error) {
222
+						uni.$u.toast('操作失败,请重试')
223
+					} finally {
224
+						uni.hideLoading(loading)
225
+					}
226
+				}
227
+			})
228
+		},
229
+		handleClose() {
230
+			this.dialogVisible = false
231
+			// 关闭对话框后重置表单
232
+			this.$nextTick(() => {
233
+				if (this.$refs.commissionFormRef) {
234
+					this.$refs.commissionFormRef.clearValidate()
235
+				}
236
+			})
237
+		},
238
+		handlePageChange(page) {
239
+			this.queryParams.pageNum = page
240
+			this.getList()
241
+		},
242
+		// 外部调用获取列表
243
+		getListByExternal() {
244
+			this.getList()
245
+		}
246
+	},
247
+	created() {
248
+		this.getList()
249
+	}
250
+}
251
+</script>
252
+
253
+<style lang="scss" scoped>
254
+.commission-form-list {
255
+	padding: 0 20px 20px;
256
+}
257
+
258
+.commission-header {
259
+	margin-bottom: 16px;
260
+}
261
+
262
+.card_list {
263
+	display: flex;
264
+	flex-direction: column;
265
+	gap: 16px;
266
+}
267
+
268
+.card_item {
269
+	background-color: #fff;
270
+	border-radius: 12px;
271
+	padding: 20px;
272
+	box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
273
+}
274
+
275
+.card_header {
276
+	display: flex;
277
+	justify-content: space-between;
278
+	align-items: center;
279
+	margin-bottom: 16px;
280
+	padding-bottom: 12px;
281
+	border-bottom: 1px solid #f0f0f0;
282
+}
283
+
284
+.card_title {
285
+	font-size: 18px;
286
+	font-weight: bold;
287
+	color: #202020;
288
+}
289
+
290
+.card_actions {
291
+	display: flex;
292
+	gap: 8px;
293
+}
294
+
295
+.card_body {
296
+	display: flex;
297
+	flex-direction: column;
298
+	gap: 12px;
299
+}
300
+
301
+.info_row {
302
+	display: flex;
303
+	align-items: center;
304
+	font-size: 14px;
305
+}
306
+
307
+.info_label {
308
+	color: #666;
309
+	min-width: 80px;
310
+	font-weight: 500;
311
+}
312
+
313
+.info_value {
314
+	color: #202020;
315
+	flex: 1;
316
+	
317
+	&.highlight {
318
+		color: #108cff;
319
+		font-weight: bold;
320
+		font-size: 16px;
321
+	}
322
+}
323
+
324
+.loading_wrap,
325
+.empty_wrap {
326
+	padding: 40px 20px;
327
+	text-align: center;
328
+}
329
+
330
+.dialog_header {
331
+	padding: 20px;
332
+	font-size: 32rpx;
333
+	font-weight: bold;
334
+	text-align: center;
335
+	border-bottom: 1px solid #e9ecef;
336
+}
337
+
338
+.dialog_body {
339
+	padding: 20px;
340
+}
341
+
342
+.dialog_footer {
343
+	padding: 20px;
344
+	display: flex;
345
+	justify-content: space-around;
346
+	border-top: 1px solid #e9ecef;
347
+}
348
+</style>

+ 1 - 9
pages/orderDetail/tabs/followRecord/index.vue

@@ -45,7 +45,6 @@
45
 </template>
45
 </template>
46
 <script>
46
 <script>
47
 	import {
47
 	import {
48
-		groupBy,
49
 		isEmpty
48
 		isEmpty
50
 	} from "lodash";
49
 	} from "lodash";
51
 	export default {
50
 	export default {
@@ -60,11 +59,6 @@
60
 			type: {
59
 			type: {
61
 				type: String,
60
 				type: String,
62
 				required: true
61
 				required: true
63
-				// '1' : 线索跟进
64
-				// '2' : 订单跟进(通过orderId)
65
-				// '3' : 订单跟进(通过clueId)
66
-				// '4' : 重复线索跟进
67
-				// 其他 : 重复订单跟进
68
 			}
62
 			}
69
 		},
63
 		},
70
 		data() {
64
 		data() {
@@ -95,9 +89,8 @@
95
 						clueId: this.clueId
89
 						clueId: this.clueId
96
 					});
90
 					});
97
 				} else if (this.type === '2') {
91
 				} else if (this.type === '2') {
98
-					// 订单跟进(通过orderId)
99
 					data = await uni.$u.api.getOrderFollowList({
92
 					data = await uni.$u.api.getOrderFollowList({
100
-						orderId: this.orderId
93
+						clueId: this.clueId
101
 					});
94
 					});
102
 				} else if (this.type === '3') {
95
 				} else if (this.type === '3') {
103
 					// 订单跟进(通过clueId)
96
 					// 订单跟进(通过clueId)
@@ -126,7 +119,6 @@
126
 						// 线索跟进和重复线索跟进使用deleteClueFollow
119
 						// 线索跟进和重复线索跟进使用deleteClueFollow
127
 						await uni.$u.api.deleteClueFollow([this.currentDeleteFollowId]);
120
 						await uni.$u.api.deleteClueFollow([this.currentDeleteFollowId]);
128
 					} else {
121
 					} else {
129
-						// 订单跟进(通过orderId)、订单跟进(通过clueId)、重复订单跟进使用deleteOrderFollow
130
 						await uni.$u.api.deleteOrderFollow([this.currentDeleteFollowId]);
122
 						await uni.$u.api.deleteOrderFollow([this.currentDeleteFollowId]);
131
 					}
123
 					}
132
 					
124
 					

+ 454 - 0
pages/orderDetail/tabs/receiptFormList/receiptForm/index.vue

@@ -0,0 +1,454 @@
1
+<template>
2
+	<u-popup :show="visible" :mode="dialogMode" :width="dialogWidth" border-radius="16" @close="handleDialogClose">
3
+		<view class="receipt-form">
4
+			<view class="dialog_header">{{ dialogTitle }}</view>
5
+			<view class="dialog_body">
6
+				<view v-if="loading" class="loading_wrap">
7
+					<u-loading-icon text="加载中" textSize="18"></u-loading-icon>
8
+				</view>
9
+				<u-form :model="form" ref="receiptFormRef" label-width="100rpx" :label-style="labelStyle">
10
+					<!-- 第一行:最后修改时间、客服 -->
11
+					<u-row gutter="20">
12
+						<u-col span="12">
13
+							<u-form-item label="最后修改时间">
14
+								<u-input v-model="form.updateTime" disabled placeholder="最后修改时间" />
15
+							</u-form-item>
16
+						</u-col>
17
+						<u-col span="12">
18
+							<u-form-item label="客服" prop="customerServiceName">
19
+								<u-input v-model="form.customerServiceName" :disabled="!isEdit" placeholder="请输入客服姓名" />
20
+							</u-form-item>
21
+						</u-col>
22
+					</u-row>
23
+
24
+					<!-- 第二行:物品、类别 -->
25
+					<u-row gutter="20">
26
+						<u-col span="12">
27
+							<u-form-item label="物品" prop="item" :required="true">
28
+								<u-input v-model="form.item" :disabled="!isEdit" placeholder="请输入物品名称" />
29
+							</u-form-item>
30
+						</u-col>
31
+						<u-col span="12">
32
+							<u-form-item label="类别" prop="category" :required="true">
33
+								<ld-select 
34
+									:load="true"
35
+									dict-type="crm_form_category" 
36
+									v-model="form.category" 
37
+									:select-attributes="{
38
+										clearable: true,
39
+										disabled: !isEdit,
40
+										placeholder: '请选择类别'
41
+									}"
42
+								></ld-select>
43
+							</u-form-item>
44
+						</u-col>
45
+					</u-row>
46
+
47
+					<!-- 第三行:品牌、是否需要查码 -->
48
+					<u-row gutter="20">
49
+						<u-col span="12">
50
+							<u-form-item label="品牌" prop="brand">
51
+								<u-input v-model="form.brand" :disabled="!isEdit" placeholder="请输入品牌" />
52
+							</u-form-item>
53
+						</u-col>
54
+						<u-col span="12">
55
+							<u-form-item label="是否需要查码" prop="needCheckCode">
56
+								<u-select v-model="form.needCheckCode" :disabled="!isEdit" placeholder="请选择是否需要查码"
57
+									:list="needCheckCodeOptions"></u-select>
58
+							</u-form-item>
59
+						</u-col>
60
+					</u-row>
61
+
62
+					<!-- 第四行:编码、查码费 -->
63
+					<u-row gutter="20">
64
+						<u-col span="12">
65
+							<u-form-item label="编码" prop="code">
66
+								<u-input v-model="form.code" :disabled="form.needCheckCode !== 1 || !isEdit" placeholder="请输入编码" />
67
+							</u-form-item>
68
+						</u-col>
69
+						<u-col span="12">
70
+							<u-form-item label="查码费" prop="checkCodeFee">
71
+								<u-input v-model.number="form.checkCodeFee" :disabled="form.needCheckCode !== 1 || !isEdit" placeholder="请输入查码费" />
72
+							</u-form-item>
73
+						</u-col>
74
+					</u-row>
75
+
76
+					<!-- 第五行:表款、好处费、运费 -->
77
+					<u-row gutter="20">
78
+						<u-col span="8">
79
+							<u-form-item label="表款" prop="tableFee">
80
+								<u-input v-model.number="form.tableFee" :disabled="!isEdit" placeholder="请输入表款" />
81
+							</u-form-item>
82
+						</u-col>
83
+						<u-col span="8">
84
+							<u-form-item label="好处费" prop="benefitFee">
85
+								<u-input v-model.number="form.benefitFee" :disabled="!isEdit" placeholder="请输入好处费" />
86
+							</u-form-item>
87
+						</u-col>
88
+						<u-col span="8">
89
+							<u-form-item label="运费" prop="freight">
90
+								<u-input v-model.number="form.freight" :disabled="!isEdit" placeholder="请输入运费" />
91
+							</u-form-item>
92
+						</u-col>
93
+					</u-row>
94
+
95
+					<!-- 第六行:成本合计、卖价、业绩 -->
96
+					<u-row gutter="20">
97
+						<u-col span="8">
98
+							<u-form-item label="成本合计" prop="totalCost">
99
+								<u-input v-model.number="form.totalCost" disabled placeholder="自动计算成本合计" />
100
+							</u-form-item>
101
+						</u-col>
102
+						<u-col span="8">
103
+							<u-form-item label="卖价" prop="sellingPrice">
104
+								<u-input v-model.number="form.sellingPrice" :disabled="!isEdit" placeholder="请输入卖价" />
105
+							</u-form-item>
106
+						</u-col>
107
+						<u-col span="8">
108
+							<u-form-item label="业绩" prop="performance">
109
+								<u-input v-model.number="form.performance" disabled placeholder="自动计算业绩" />
110
+							</u-form-item>
111
+						</u-col>
112
+					</u-row>
113
+
114
+					<!-- 第七行:分单比例、维修金额、毛业绩 -->
115
+					<u-row gutter="20">
116
+						<u-col span="8">
117
+							<u-form-item label="分单比例" prop="splitRatio">
118
+								<u-input v-model="form.splitRatio" type="number" step="0.01" :disabled="!isEdit"
119
+									@blur="validateSplitRatio" placeholder="0-100" />
120
+							</u-form-item>
121
+						</u-col>
122
+						<u-col span="8">
123
+							<u-form-item label="维修金额" prop="repairAmount">
124
+								<u-input v-model.number="form.repairAmount" :disabled="!isEdit" placeholder="请输入维修金额" />
125
+							</u-form-item>
126
+						</u-col>
127
+						<u-col span="8">
128
+							<u-form-item label="毛业绩" prop="grossPerformance">
129
+								<u-input v-model.number="form.grossPerformance" :disabled="!canEditGrossPerformance"
130
+									placeholder="自动计算毛业绩" />
131
+							</u-form-item>
132
+						</u-col>
133
+					</u-row>
134
+
135
+					<!-- 第八行:快递单号、附件 -->
136
+					<u-row gutter="20">
137
+						<u-col span="12">
138
+							<u-form-item label="快递单号" prop="expressOrderNo">
139
+								<u-input v-model="form.expressOrderNo" :disabled="!isEdit" placeholder="请输入快递单号" />
140
+							</u-form-item>
141
+						</u-col>
142
+						<u-col span="12">
143
+							<u-form-item label="附件" prop="fileIds">
144
+								<u-select
145
+									v-model="fileIds"
146
+									:disabled="!isEdit"
147
+									placeholder="请选择附件"
148
+									multiple
149
+									:list="fileOptions"
150
+									:custom-label="handleShowLabel"
151
+									:custom-value="file => file.id"
152
+									:show-value="false"
153
+								></u-select>
154
+							</u-form-item>
155
+						</u-col>
156
+					</u-row>
157
+
158
+					<!-- 第九行:收单备注 -->
159
+					<u-row gutter="20">
160
+						<u-col span="24">
161
+							<u-form-item label="收单备注" prop="receiptRemark">
162
+								<u-textarea v-model="form.receiptRemark" :disabled="!isEdit" placeholder="请输入收单备注"
163
+									:maxlength="500" :show-word-limit="true" />
164
+							</u-form-item>
165
+						</u-col>
166
+					</u-row>
167
+				</u-form>
168
+			</view>
169
+
170
+			<view class="dialog_footer" v-if="canShowSaveButton">
171
+				<u-button @click="handleDialogClose">取消</u-button>
172
+				<u-button type="primary" @click="submitForm">保存</u-button>
173
+			</view>
174
+		</view>
175
+	</u-popup>
176
+</template>
177
+
178
+<script>
179
+import { cloneDeep } from 'lodash'
180
+import ldSelect from '@/components/ld-select/ld-select.vue'
181
+
182
+export default {
183
+	name: 'ReceiptForm',
184
+	components: {
185
+		ldSelect
186
+	},
187
+	props: {
188
+		receiptDetail: {
189
+			type: Object,
190
+			required: true
191
+		},
192
+		visible: {
193
+			type: Boolean,
194
+			default: false
195
+		},
196
+		dialogTitle: {
197
+			type: String,
198
+			default: '收单信息'
199
+		},
200
+		dialogWidth: {
201
+			type: String,
202
+			default: '860rpx'
203
+		},
204
+		dialogMode: {
205
+			type: String,
206
+			default: 'center'
207
+		}
208
+	},
209
+	data() {
210
+		return {
211
+			loading: false,
212
+			form: {},
213
+			fileOptions: [],
214
+			needCheckCodeOptions: [
215
+				{ label: '是', value: 1 },
216
+				{ label: '否', value: 2 }
217
+			],
218
+			labelStyle: {
219
+				fontSize: '28rpx',
220
+				color: '#606266'
221
+			},
222
+			// 表单验证规则
223
+			rules: {
224
+				item: [{ required: true, message: '请输入物品名称', trigger: 'blur' }],
225
+				category: [{ required: true, message: '请选择类别', trigger: 'change' }]
226
+			}
227
+		}
228
+	},
229
+	computed: {
230
+		fileIds: {
231
+			get() {
232
+				if (this.form.fileIds) {
233
+					return this.form.fileIds.split(',').filter(id => id)
234
+				} else {
235
+					return []
236
+				}
237
+			},
238
+			set(value) {
239
+				this.form.fileIds = value.join(',')
240
+			}
241
+		},
242
+		isEdit() {
243
+			return true
244
+		},
245
+		// 判断是否显示保存按钮:status=2时,只有接单人本人可以编辑
246
+		canShowSaveButton() {
247
+			const { status, identification } = this.receiptDetail
248
+			const currentUserId = this.$store.state.user.userId
249
+
250
+			// 如果status不是"2",显示保存按钮
251
+			if (status !== '2') {
252
+				return true
253
+			}
254
+
255
+			// 如果status是"2",只有接单人本人可以看到保存按钮
256
+			return String(identification) === String(currentUserId)
257
+		},
258
+		// 自动计算成本合计 = 表款 + 好处费 + 运费 + 查码费 + 维修金额
259
+		calculatedTotalCost() {
260
+			const { tableFee, benefitFee, freight, checkCodeFee, repairAmount } = this.form
261
+			return (
262
+				(tableFee || 0) +
263
+				(benefitFee || 0) +
264
+				(freight || 0) +
265
+				(checkCodeFee || 0) +
266
+				(repairAmount || 0)
267
+			)
268
+		},
269
+		// 自动计算业绩 = 卖价 - 成本合计
270
+		// 卖价为空时不计算,避免出现负数业绩
271
+		calculatedPerformance() {
272
+			const { sellingPrice } = this.form
273
+			// 卖价为空或为0时,不计算业绩
274
+			if (!sellingPrice || sellingPrice <= 0) {
275
+				return null
276
+			}
277
+			return sellingPrice - this.calculatedTotalCost
278
+		},
279
+		// 自动计算毛业绩 = 业绩 × 分单比例(仅当两者都不为空时)
280
+		// 智能判断:<=1直接乘,>1先除以100再乘
281
+		// 四舍五入保留两位小数
282
+		calculatedGrossPerformance() {
283
+			const { splitRatio } = this.form
284
+			if (this.calculatedPerformance && splitRatio) {
285
+				// 智能判断:<=1直接乘,>1先除以100再乘
286
+				const ratio = splitRatio <= 1 ? splitRatio : splitRatio / 100
287
+				const result = this.calculatedPerformance * ratio
288
+				// 四舍五入保留两位小数
289
+				return Math.round(result * 100) / 100
290
+			}
291
+			return null
292
+		},
293
+		// 毛业绩是否可编辑:业绩不为空 且 比例为空
294
+		canEditGrossPerformance() {
295
+			return this.calculatedPerformance && !this.form.splitRatio && this.isEdit
296
+		}
297
+	},
298
+	watch: {
299
+		// 监听成本合计变化,自动更新form
300
+		calculatedTotalCost(val) {
301
+			this.form.totalCost = val
302
+		},
303
+		// 监听业绩业绩变化,自动更新form
304
+		calculatedPerformance(val) {
305
+			this.form.performance = val
306
+		},
307
+		// 监听毛业绩变化,仅当比例不为空时自动更新form
308
+		calculatedGrossPerformance(val) {
309
+			if (this.form.splitRatio && val !== null) {
310
+				this.form.grossPerformance = val
311
+			}
312
+		},
313
+		visible: {
314
+			handler(val) {
315
+				if (val) {
316
+					this.initForm()
317
+					this.fetchFileOptions()
318
+				}
319
+			},
320
+			immediate: true
321
+		}
322
+	},
323
+	methods: {
324
+		handleShowLabel(file) {
325
+			// 获取文件在fileIds数组中的索引,如果存在则显示序号
326
+			const index = this.fileIds.indexOf(file.id.toString())
327
+			let label = ''
328
+			if (index !== -1) {
329
+				label = `[${index + 1}] `
330
+			}
331
+			label += file.fileName + (file.remark ? '(' + file.remark + ')' : '')
332
+			if (file.receiptFormId !== null && file.receiptFormId !== this.form.id) {
333
+				label += '-已绑定' + file.item
334
+			}
335
+			return label
336
+		},
337
+		initForm() {
338
+			// 克隆父组件传递的驼峰格式数据到form
339
+			const form = cloneDeep(this.receiptDetail)
340
+			this.form = form
341
+		},
342
+		// 获取附件选项列表
343
+		async fetchFileOptions() {
344
+			try {
345
+				const params = {
346
+					clueId: this.receiptDetail.clueId,
347
+					sourceId: this.receiptDetail.sendFormId,
348
+					notBound: '1',
349
+					orderFileType: '3',
350
+					type: '2'
351
+				}
352
+				const response = await uni.$u.api.selectClueFileByDto(params)
353
+				this.fileOptions = response.rows || []
354
+			} catch (error) {
355
+				console.error('获取附件列表失败:', error)
356
+				uni.$u.toast('获取附件列表失败')
357
+			}
358
+		},
359
+		// 分单比例验证:非空时范围必须在0-100之间
360
+		validateSplitRatio() {
361
+			const { splitRatio } = this.form
362
+			if (splitRatio === null || splitRatio === undefined || splitRatio === '') {
363
+				return // 允许为空
364
+			}
365
+			// 范围验证:0-100
366
+			if (splitRatio < 0) {
367
+				uni.$u.toast('分单比例不能小于0')
368
+				this.form.splitRatio = 0
369
+			} else if (splitRatio > 100) {
370
+				uni.$u.toast('分单比例不能大于100')
371
+				this.form.splitRatio = 100
372
+			}
373
+		},
374
+		// 提交表单
375
+		submitForm() {
376
+			this.$refs.receiptFormRef.validate(async (valid) => {
377
+				if (valid) {
378
+					await this.handleUpdate()
379
+				} else {
380
+					uni.$u.toast('请检查表单填写是否正确')
381
+					return false
382
+				}
383
+			})
384
+		},
385
+		// 在提交表单前,将数组形式的fileIds转换为逗号分隔的字符串
386
+		async handleUpdate() {
387
+			this.loading = true
388
+			try {
389
+				if (this.form.id) {
390
+					await uni.$u.api.updateReceiptForm(this.form)
391
+					uni.$u.toast('修改成功')
392
+				} else {
393
+					await uni.$u.api.addReceiptForm(this.form)
394
+					uni.$u.toast('新增成功')
395
+				}
396
+				this.handleDialogClose()
397
+				this.$emit('updateSuccess')
398
+			} catch (error) {
399
+				uni.$u.toast('修改失败,请重试')
400
+			} finally {
401
+				this.loading = false
402
+			}
403
+		},
404
+		// 处理对话框关闭
405
+		handleDialogClose() {
406
+			this.$emit('update:visible', false)
407
+			this.$emit('dialogClosed')
408
+		}
409
+	}
410
+}
411
+</script>
412
+
413
+<style lang="scss" scoped>
414
+.receipt-form {
415
+	max-width: 100%;
416
+}
417
+
418
+.dialog_header {
419
+	padding: 20px;
420
+	font-size: 32rpx;
421
+	font-weight: bold;
422
+	text-align: center;
423
+	border-bottom: 1px solid #e9ecef;
424
+}
425
+
426
+.dialog_body {
427
+	padding: 20px;
428
+	max-height: 70vh;
429
+	overflow-y: auto;
430
+}
431
+
432
+.dialog_footer {
433
+	padding: 20px;
434
+	display: flex;
435
+	justify-content: space-around;
436
+	border-top: 1px solid #e9ecef;
437
+}
438
+
439
+.loading_wrap {
440
+	display: flex;
441
+	justify-content: center;
442
+	align-items: center;
443
+	height: 200px;
444
+}
445
+
446
+.u-form-item {
447
+	margin-bottom: 20px;
448
+}
449
+
450
+.u-textarea {
451
+	border-radius: 8px;
452
+	border: 1px solid #dcdfe6;
453
+}
454
+</style>

+ 302 - 0
pages/orderDetail/tabs/receiptFormList/receiptFormList.vue

@@ -0,0 +1,302 @@
1
+<template>
2
+	<view class="receipt-form-list">
3
+		<!-- 头部按钮组 -->
4
+		<view class="receipt-form-header">
5
+			<u-button type="primary" size="mini" icon="plus" @click="handleAdd">新增收单信息</u-button>
6
+			<u-button type="primary" size="mini" icon="share" @click="handleTransfer">去小葫芦线上支付</u-button>
7
+		</view>
8
+
9
+		<!-- 收单信息卡片列表 -->
10
+		<view v-if="loading" class="loading_wrap">
11
+			<u-loading-icon text="加载中" textSize="18"></u-loading-icon>
12
+		</view>
13
+		<view v-else-if="receiptFormList.length === 0" class="empty_wrap">
14
+			<u-empty text="暂无收单数据"></u-empty>
15
+		</view>
16
+		<view v-else class="card_list">
17
+			<view class="card_item" v-for="(row, index) in receiptFormList" :key="row.id">
18
+				<view class="card_header">
19
+					<view class="card_title">{{ row.item || '-' }}</view>
20
+					<view class="card_actions">
21
+						<u-button size="mini" type="text" @click="handleUpdate(row)">详情</u-button>
22
+						<u-button size="mini" type="text" @click="handleDelete(row)" style="color: #f56c6c">删除</u-button>
23
+					</view>
24
+				</view>
25
+				<view class="card_body">
26
+					<view class="info_row">
27
+						<view class="info_label">品牌:</view>
28
+						<view class="info_value">{{ row.brand || '-' }}</view>
29
+					</view>
30
+					<view class="info_row">
31
+						<view class="info_label">分单比例:</view>
32
+						<view class="info_value">{{ row.splitRatio || '-' }}</view>
33
+					</view>
34
+					<view class="info_row">
35
+						<view class="info_label">卖价:</view>
36
+						<view class="info_value">¥{{ formatPrice(row.sellingPrice) }}</view>
37
+					</view>
38
+					<view class="info_row">
39
+						<view class="info_label">业绩:</view>
40
+						<view class="info_value highlight">¥{{ formatPrice(row.performance) }}</view>
41
+					</view>
42
+				</view>
43
+			</view>
44
+		</view>
45
+
46
+		<!-- 收单表单对话框 -->
47
+		<u-popup v-model="dialogVisible" mode="center" :width="dialogWidth" border-radius="16">
48
+			<view class="dialog_header">{{ dialogTitle }}</view>
49
+			<view class="dialog_body">
50
+				<receiptForm
51
+					:receipt-detail="form"
52
+					:send-form-id="sendFormId"
53
+					:clue-id="clueId"
54
+					@update-success="handleUpdateSuccess"
55
+				></receiptForm>
56
+			</view>
57
+		</u-popup>
58
+	</view>
59
+</template>
60
+
61
+<script>
62
+import receiptForm from './receiptForm/index.vue'
63
+
64
+export default {
65
+	name: 'ReceiptFormList',
66
+	components: {
67
+		receiptForm
68
+	},
69
+	props: {
70
+		receiptDetail: {
71
+			type: Object,
72
+			default: () => {}
73
+		},
74
+		sendFormId: {
75
+			type: [Number, String],
76
+			required: true
77
+		},
78
+		clueId: {
79
+			type: [Number, String],
80
+			required: true
81
+		}
82
+	},
83
+	data() {
84
+		return {
85
+			// 对话框控制
86
+			dialogVisible: false,
87
+			dialogTitle: '收单信息',
88
+			dialogWidth: '860rpx',
89
+
90
+			// 遮罩层
91
+			loading: false,
92
+			// 收单信息列表
93
+			receiptFormList: [],
94
+			// 表单参数
95
+			form: {}
96
+		}
97
+	},
98
+	methods: {
99
+		/** 查询收单信息列表 */
100
+		async getList() {
101
+			this.loading = true
102
+			try {
103
+				const response = await uni.$u.api.listReceiptFormByOrderId(this.sendFormId)
104
+				this.receiptFormList = response.data || []
105
+			} catch (error) {
106
+				uni.$u.toast(`获取列表失败:${error.message}`)
107
+				this.receiptFormList = []
108
+			} finally {
109
+				this.loading = false
110
+			}
111
+		},
112
+
113
+		// 格式化价格
114
+		formatPrice(price) {
115
+			if (!price && price !== 0) return '-'
116
+			return Number(price).toFixed(2)
117
+		},
118
+
119
+		/** 去小葫芦线上支付按钮操作 */
120
+		async handleTransfer() {
121
+			try {
122
+				const response = await uni.$u.api.saveOrderFileAndTransfer({
123
+					id: this.sendFormId,
124
+					clueId: this.clueId
125
+				})
126
+				uni.$u.toast(response.msg || '支付成功')
127
+			} catch (error) {
128
+				uni.$u.toast(`支付失败:${error.message}`)
129
+			}
130
+		},
131
+
132
+		/** 新增按钮操作 */
133
+		handleAdd() {
134
+			this.reset()
135
+			this.dialogTitle = '新增收单信息'
136
+			this.dialogVisible = true
137
+		},
138
+
139
+		/** 修改按钮操作 */
140
+		async handleUpdate(row) {
141
+			this.reset()
142
+			try {
143
+				const response = await uni.$u.api.getReceiptForm(row.id)
144
+				this.form = response.data
145
+				this.dialogTitle = '修改收单信息'
146
+				this.dialogVisible = true
147
+			} catch (error) {
148
+				uni.$u.toast(`获取详情失败:${error.message}`)
149
+			}
150
+		},
151
+
152
+		// 处理更新成功事件
153
+		handleUpdateSuccess() {
154
+			this.dialogVisible = false
155
+			this.getList()
156
+		},
157
+
158
+		/** 删除按钮操作 */
159
+		handleDelete(row) {
160
+			uni.showModal({
161
+				title: '警告',
162
+				content: '是否确认删除收单信息?',
163
+				success: (res) => {
164
+					if (res.confirm) {
165
+						this.deleteRow(row.id)
166
+					}
167
+				}
168
+			})
169
+		},
170
+
171
+		async deleteRow(id) {
172
+			try {
173
+				await uni.$u.api.delReceiptForm(id)
174
+				uni.$u.toast('删除成功')
175
+				this.getList()
176
+			} catch (error) {
177
+				uni.$u.toast(`删除失败:${error.message}`)
178
+			}
179
+		},
180
+
181
+		/** 表单重置 */
182
+		reset() {
183
+			this.form = Object.assign({
184
+				id: undefined,
185
+				sendFormId: this.sendFormId,
186
+				clueId: this.clueId,
187
+				item: '',
188
+				brand: '',
189
+				needCheckCode: 1,
190
+				code: '',
191
+				tableFee: undefined,
192
+				benefitFee: undefined,
193
+				freight: undefined,
194
+				checkCodeFee: undefined,
195
+				totalCost: undefined,
196
+				sellingPrice: undefined,
197
+				performance: undefined,
198
+				receiptRemark: '',
199
+				repairAmount: undefined,
200
+				grossPerformance: undefined,
201
+				expressOrderNo: '',
202
+				fileIds: ''
203
+			}, this.receiptDetail)
204
+		}
205
+	},
206
+	created() {
207
+		this.getList()
208
+	}
209
+}
210
+</script>
211
+
212
+<style lang="scss" scoped>
213
+.receipt-form-list {
214
+	padding: 0 20px 20px;
215
+}
216
+
217
+.receipt-form-header {
218
+	margin-bottom: 16px;
219
+	display: flex;
220
+	gap: 10px;
221
+}
222
+
223
+.card_list {
224
+	display: flex;
225
+	flex-direction: column;
226
+	gap: 16px;
227
+}
228
+
229
+.card_item {
230
+	background-color: #fff;
231
+	border-radius: 12px;
232
+	padding: 20px;
233
+	box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
234
+}
235
+
236
+.card_header {
237
+	display: flex;
238
+	justify-content: space-between;
239
+	align-items: center;
240
+	margin-bottom: 16px;
241
+	padding-bottom: 12px;
242
+	border-bottom: 1px solid #f0f0f0;
243
+}
244
+
245
+.card_title {
246
+	font-size: 18px;
247
+	font-weight: bold;
248
+	color: #202020;
249
+}
250
+
251
+.card_actions {
252
+	display: flex;
253
+	gap: 8px;
254
+}
255
+
256
+.card_body {
257
+	display: flex;
258
+	flex-direction: column;
259
+	gap: 12px;
260
+}
261
+
262
+.info_row {
263
+	display: flex;
264
+	align-items: center;
265
+	font-size: 14px;
266
+}
267
+
268
+.info_label {
269
+	color: #666;
270
+	min-width: 80px;
271
+	font-weight: 500;
272
+}
273
+
274
+.info_value {
275
+	color: #202020;
276
+	flex: 1;
277
+	
278
+	&.highlight {
279
+		color: #f56c6c;
280
+		font-weight: bold;
281
+		font-size: 16px;
282
+	}
283
+}
284
+
285
+.loading_wrap,
286
+.empty_wrap {
287
+	padding: 40px 20px;
288
+	text-align: center;
289
+}
290
+
291
+.dialog_header {
292
+	padding: 20px;
293
+	font-size: 16px;
294
+	font-weight: bold;
295
+	text-align: center;
296
+	border-bottom: 1px solid #e9ecef;
297
+}
298
+
299
+.dialog_body {
300
+	padding: 0;
301
+}
302
+</style>

+ 408 - 0
pages/orderDetail/tabs/uploadFile/index.vue

@@ -0,0 +1,408 @@
1
+<template>
2
+	<view class="uploadFile_wrap">
3
+		<!-- 上传按钮 -->
4
+		<view class="upload_btn_wrap" @click="handleUpload">
5
+			<view class="upload_btn_content">
6
+				<view class="upload_icon">
7
+					<u-icon name="plus-circle" size="24" color="#fff"></u-icon>
8
+				</view>
9
+				<text class="upload_text">上传文件</text>
10
+			</view>
11
+		</view>
12
+
13
+		<!-- 文件列表分类展示 -->
14
+		<view class="fileList_wrap" v-if="!loading && dataList.length > 0">
15
+			<view class="file_item_wrap" v-if="imgList.length > 0">
16
+				<view class="item_title">图片文件:</view>
17
+				<mk-upload 
18
+					:imgList="imgList" 
19
+					@onDelete="handleDelFile" 
20
+					:controlShow="false" 
21
+					type="image"
22
+					@onPreviewTake="handlePreviewImage"
23
+				></mk-upload>
24
+			</view>
25
+			<view class="file_item_wrap" v-if="recordList.length > 0">
26
+				<view class="item_title">录音文件:</view>
27
+				<mk-upload 
28
+					:imgList="recordList" 
29
+					@onDelete="handleDelFile" 
30
+					:controlShow="false" 
31
+					type="record" 
32
+					@onPreviewTake="handlePreviewRecord"
33
+				></mk-upload>
34
+			</view>
35
+			<view class="file_item_wrap" v-if="videoList.length > 0">
36
+				<view class="item_title">视频文件:</view>
37
+				<mk-upload 
38
+					:imgList="videoList" 
39
+					@onDelete="handleDelFile" 
40
+					:controlShow="false" 
41
+					type="video"
42
+					@onPreviewTake="handlePreviewVideo"
43
+				></mk-upload>
44
+			</view>
45
+			<view class="file_item_wrap" v-if="otherList.length > 0">
46
+				<view class="item_title">其他文件:</view>
47
+				<mk-upload 
48
+					:imgList="otherList" 
49
+					@onDelete="handleDelFile" 
50
+					:controlShow="false" 
51
+					type="file"
52
+					@onPreviewTake="handlePreviewFile"
53
+				></mk-upload>
54
+			</view>
55
+		</view>
56
+
57
+		<!-- 加载状态 -->
58
+		<view v-if="loading" class="loading_wrap">
59
+			<u-loading-icon text="加载中" textSize="18"></u-loading-icon>
60
+		</view>
61
+
62
+		<!-- 空状态 -->
63
+		<view v-else-if="dataList.length === 0" class="empty_wrap">
64
+			<u-empty text="暂无文件数据"></u-empty>
65
+		</view>
66
+
67
+		<!-- 图片预览 -->
68
+		<u-image
69
+			ref="previewImg"
70
+			:src="previewImageSrc"
71
+			mode="widthFix"
72
+			v-if="previewImageSrc"
73
+			@click="previewImageSrc = ''"
74
+		></u-image>
75
+
76
+		<!-- 文件预览模态框 -->
77
+		<u-modal :show="showFileModal" :title="currentFile.fileName" @confirm="handleFileConfirm">
78
+			<view v-if="currentFile && currentFile.fileUrl" class="currentFile_wrap">
79
+				<audio 
80
+					style="text-align: left" 
81
+					:src="currentFile.fileUrl" 
82
+					:name="'点击播放'" 
83
+					controls
84
+					v-if="['wav','mp3','aac','wma'].includes(currentFile.fileSuffix)"
85
+				></audio>
86
+				<video 
87
+					:src="currentFile.fileUrl" 
88
+					mode="" 
89
+					class="currentFile_image" 
90
+					v-else-if="showFileModal" 
91
+					show-fullscreen-btn
92
+				></video>
93
+			</view>
94
+			<view v-else>
95
+				文件异常
96
+			</view>
97
+		</u-modal>
98
+
99
+		<!-- 编辑对话框 -->
100
+		<u-popup v-model="dialogVisible" mode="center" width="500rpx" border-radius="16">
101
+			<view class="dialog_header">{{ dialogTitle }}</view>
102
+			<view class="dialog_body">
103
+				<u-form :model="fileForm" ref="fileFormRef" label-width="100px">
104
+					<u-form-item label="文件名称" prop="fileName">
105
+						<u-input v-model="fileForm.fileName" placeholder="请输入文件名称" />
106
+					</u-form-item>
107
+					<u-form-item label="备注" prop="remark">
108
+						<u-input v-model="fileForm.remark" type="textarea" placeholder="请输入备注信息" />
109
+					</u-form-item>
110
+				</u-form>
111
+			</view>
112
+			<view class="dialog_footer">
113
+				<u-button @click="handleClose">取消</u-button>
114
+				<u-button type="primary" @click="handleEditSubmit">确定</u-button>
115
+			</view>
116
+		</u-popup>
117
+	</view>
118
+</template>
119
+
120
+<script>
121
+import mkUpload from "@/components/mk-upload/mk-upload.vue"
122
+import upload from '@/mixins/upload';
123
+export default {
124
+	name: 'UploadFile',
125
+	mixins : [upload],
126
+	components: {
127
+		mkUpload
128
+	},
129
+	props: {
130
+		clueId: { type: [String, Number], required: true },
131
+		sourceId: { type: [String, Number], default: '' },
132
+		type: { type: String, required: true },
133
+		orderFileType: { type: String, default: '' },
134
+		isDuplicate: { type: String, default: '2' }
135
+	},
136
+	computed: {
137
+		imgList() {
138
+			return this.dataList.filter(v => ['png','gif','bmp','jpg','jpeg','webp'].includes((v.fileSuffix || '').toLowerCase()));
139
+		},
140
+		recordList() {
141
+			return this.dataList.filter(v => ['wav','mp3','aac','wma'].includes((v.fileSuffix || '').toLowerCase()));
142
+		},
143
+		videoList() {
144
+			return this.dataList.filter(v => ['avi','mkv','mov','wmv','mp4'].includes((v.fileSuffix || '').toLowerCase()));
145
+		},
146
+		otherList() {
147
+			const list = this.imgList.concat(this.recordList).concat(this.videoList);
148
+			return this.dataList.filter(v => !list.some(j => j.id === v.id));
149
+		}
150
+	},
151
+	data() {
152
+		return {
153
+			loading: false,
154
+			dataList: [],
155
+			previewImageSrc: '',
156
+			showFileModal: false,
157
+			currentFile: {},
158
+			// 编辑对话框相关
159
+			dialogVisible: false,
160
+			dialogTitle: '编辑文件',
161
+			fileForm: {
162
+				id: null,
163
+				fileName: '',
164
+				remark: ''
165
+			}
166
+		}
167
+	},
168
+	methods: {
169
+		async handleUploadSuccess(){
170
+			await uni.$u.api.saveClueFile({
171
+				clueId : this.clueId,
172
+				list : this.uploadList,
173
+				sourceId: this.sourceId,
174
+				type : this.type,
175
+				orderFileType : this.orderFileType
176
+			});
177
+			uni.$u.toast("上传成功");
178
+			// 清空
179
+			this.uploadList = [];
180
+			this.getList();
181
+		},
182
+		// 获取文件列表
183
+		async getList() {
184
+			// 检查必要参数是否存在
185
+			if (!this.clueId) {
186
+				return;
187
+			}
188
+			this.loading = true
189
+			try {
190
+				const params = {
191
+					clueId: this.clueId,
192
+					sourceId: this.sourceId,
193
+					type: this.type,
194
+					orderFileType: this.orderFileType,
195
+					isDuplicate: this.isDuplicate,
196
+					pageNum: 1,
197
+					pageSize: 1000 // 设置一个较大的值以获取所有数据
198
+				}
199
+				const response = await uni.$u.api.selectClueFileByDto(params)
200
+				this.dataList = response.rows || []
201
+			} catch (error) {
202
+				uni.$u.toast(`获取列表失败:${error.message}`)
203
+				this.dataList = []
204
+			} finally {
205
+				this.loading = false
206
+			}
207
+		},
208
+
209
+		// 删除文件
210
+		async handleDelFile(item) {
211
+			try {
212
+				await uni.$u.api.deleteClueFile([item.id])
213
+				uni.$u.toast('删除成功')
214
+				this.getList()
215
+			} catch (error) {
216
+				uni.$u.toast(`删除失败:${error.message}`)
217
+			}
218
+		},
219
+
220
+		// 预览图片
221
+		handlePreviewImage(item) {
222
+			const imgList = this.imgList.map(v => v.fileUrl);
223
+			const currentIndex = imgList.findIndex(url => url === item.fileUrl);
224
+			uni.previewImage({
225
+				current: currentIndex >= 0 ? currentIndex : 0,
226
+				urls: imgList
227
+			});
228
+		},
229
+
230
+		// 预览录音
231
+		handlePreviewRecord(item) {
232
+			this.currentFile = item;
233
+			this.showFileModal = true;
234
+		},
235
+
236
+		// 预览视频
237
+		handlePreviewVideo(item) {
238
+			this.currentFile = item;
239
+			this.showFileModal = true;
240
+		},
241
+
242
+		// 预览其他文件
243
+		handlePreviewFile(item) {
244
+			uni.$u.toast("其他文件不支持预览,可前往网页端查看");
245
+		},
246
+
247
+		// 关闭文件预览
248
+		handleFileConfirm() {
249
+			this.showFileModal = false;
250
+		},
251
+
252
+		// 编辑文件
253
+		handleEdit(row) {
254
+			this.dialogTitle = '编辑文件'
255
+			this.fileForm = {
256
+				id: row.id,
257
+				fileName: row.fileName,
258
+				remark: row.remark
259
+			}
260
+			this.dialogVisible = true
261
+		},
262
+
263
+		// 提交编辑表单
264
+		async handleEditSubmit() {
265
+			try {
266
+				await uni.$u.api.updateClueFile(this.fileForm)
267
+				uni.$u.toast('编辑成功')
268
+				this.dialogVisible = false
269
+				this.getList()
270
+			} catch (error) {
271
+				uni.$u.toast(`编辑失败:${error.message}`)
272
+			}
273
+		},
274
+
275
+		// 关闭对话框
276
+		handleClose() {
277
+			this.dialogVisible = false
278
+			this.fileForm = {
279
+				id: null,
280
+				fileName: '',
281
+				remark: ''
282
+			}
283
+		}
284
+	},
285
+	created() {
286
+		this.getList();
287
+	}
288
+}
289
+</script>
290
+
291
+<style lang="scss" scoped>
292
+.uploadFile_wrap {
293
+	padding: 0 20px 20px;
294
+}
295
+
296
+.upload_btn_wrap {
297
+	margin-top: 10px;
298
+	margin-bottom: 10px;
299
+	background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
300
+	border-radius: 12px;
301
+	padding: 16px 20px;
302
+	box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
303
+	transition: all 0.3s ease;
304
+	position: relative;
305
+	overflow: hidden;
306
+}
307
+
308
+.upload_btn_wrap::before {
309
+	content: '';
310
+	position: absolute;
311
+	top: 0;
312
+	left: -100%;
313
+	width: 100%;
314
+	height: 100%;
315
+	background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
316
+	transition: left 0.5s;
317
+}
318
+
319
+.upload_btn_wrap:active::before {
320
+	left: 100%;
321
+}
322
+
323
+.upload_btn_wrap:active {
324
+	transform: scale(0.98);
325
+	box-shadow: 0 2px 8px rgba(102, 126, 234, 0.4);
326
+}
327
+
328
+.upload_btn_content {
329
+	display: flex;
330
+	align-items: center;
331
+	justify-content: center;
332
+	gap: 8px;
333
+	position: relative;
334
+	z-index: 1;
335
+}
336
+
337
+.upload_icon {
338
+	display: flex;
339
+	align-items: center;
340
+	justify-content: center;
341
+	width: 32px;
342
+	height: 32px;
343
+	background: rgba(255, 255, 255, 0.2);
344
+	border-radius: 50%;
345
+	backdrop-filter: blur(10px);
346
+}
347
+
348
+.upload_text {
349
+	color: #fff;
350
+	font-size: 16px;
351
+	font-weight: 600;
352
+	letter-spacing: 0.5px;
353
+}
354
+
355
+.fileList_wrap {
356
+	padding: 0;
357
+	color: #666666;
358
+	font-size: 24rpx;
359
+	
360
+	.file_item_wrap {
361
+		margin-bottom: 20px;
362
+		
363
+		.item_title {
364
+			margin-bottom: 10px;
365
+			font-weight: bold;
366
+			font-size: 16px;
367
+			color: #202020;
368
+		}
369
+	}
370
+}
371
+
372
+.currentFile_wrap {
373
+	.file_audio {
374
+		height: 60px;
375
+		width: 100%;
376
+	}
377
+
378
+	.currentFile_image {
379
+		height: 42vh;
380
+		object-fit: contain;
381
+	}
382
+}
383
+
384
+.loading_wrap,
385
+.empty_wrap {
386
+	padding: 40px 20px;
387
+	text-align: center;
388
+}
389
+
390
+.dialog_header {
391
+	padding: 20px;
392
+	font-size: 16px;
393
+	font-weight: bold;
394
+	text-align: center;
395
+	border-bottom: 1px solid #e9ecef;
396
+}
397
+
398
+.dialog_body {
399
+	padding: 20px;
400
+}
401
+
402
+.dialog_footer {
403
+	padding: 20px;
404
+	display: flex;
405
+	justify-content: space-around;
406
+	border-top: 1px solid #e9ecef;
407
+}
408
+</style>

+ 4 - 4
pages/orderForm/index.vue

@@ -63,22 +63,22 @@
63
 		<!-- 文件上传区域 -->
63
 		<!-- 文件上传区域 -->
64
 		<view class="upload_area">
64
 		<view class="upload_area">
65
 			<!-- 聊天记录附件 -->
65
 			<!-- 聊天记录附件 -->
66
-			<order-file-upload title="聊天记录附件" file-type="1" :file-list="form.chatAttachmentList"
66
+			<order-file-upload title="聊天记录附件" orderFileType="1" :file-list="form.chatAttachmentList"
67
 				@update:fileList="updateChatFiles" 
67
 				@update:fileList="updateChatFiles" 
68
 				tip-text="上传聊天截图、录音等文件"></order-file-upload>
68
 				tip-text="上传聊天截图、录音等文件"></order-file-upload>
69
 
69
 
70
 			<!-- 报价附件 -->
70
 			<!-- 报价附件 -->
71
-			<order-file-upload title="报价附件" file-type="2" :file-list="form.quoteAttachmentList"
71
+			<order-file-upload title="报价附件" orderFileType="2" :file-list="form.quoteAttachmentList"
72
 				@update:fileList="updateQuoteFiles" 
72
 				@update:fileList="updateQuoteFiles" 
73
 				tip-text="上传报价附件"></order-file-upload>
73
 				tip-text="上传报价附件"></order-file-upload>
74
 
74
 
75
 			<!-- 高清图附件 -->
75
 			<!-- 高清图附件 -->
76
-			<order-file-upload title="高清图附件" file-type="3" :file-list="form.hdImageAttachmentList"
76
+			<order-file-upload title="高清图附件" orderFileType="3" :file-list="form.hdImageAttachmentList"
77
 				@update:fileList="updateHdImageFiles" 
77
 				@update:fileList="updateHdImageFiles" 
78
 				tip-text="上传物品高清图片"></order-file-upload>
78
 				tip-text="上传物品高清图片"></order-file-upload>
79
 
79
 
80
 			<!-- 其他附件 -->
80
 			<!-- 其他附件 -->
81
-			<order-file-upload title="其他附件" file-type="4" :file-list="form.otherAttachmentList"
81
+			<order-file-upload title="其他附件" orderFileType="4" :file-list="form.otherAttachmentList"
82
 				@update:fileList="updateOtherFiles" 
82
 				@update:fileList="updateOtherFiles" 
83
 				tip-text="上传其他相关文件"></order-file-upload>
83
 				tip-text="上传其他相关文件"></order-file-upload>
84
 		</view>
84
 		</view>

BIN
static/orderDetail/cx.png


BIN
static/orderDetail/jd.png


BIN
static/orderDetail/sd.png


BIN
static/orderDetail/ws.png


+ 6 - 1
utils/api.js

@@ -89,9 +89,14 @@ const install = (Vue, vm) => {
89
 		getDuplicateOrderFollowListByClueId:(params = {},config = {})=>http.get(store.state.user.path + '/orderFollow/getDuplicateOrderFollowListByClueId',{ params }),
89
 		getDuplicateOrderFollowListByClueId:(params = {},config = {})=>http.get(store.state.user.path + '/orderFollow/getDuplicateOrderFollowListByClueId',{ params }),
90
 		deleteOrderFollow:(data,config={})=>http.post(store.state.user.path + '/orderFollow/deleteOrderFollow',data),
90
 		deleteOrderFollow:(data,config={})=>http.post(store.state.user.path + '/orderFollow/deleteOrderFollow',data),
91
 		getOrderFollowList:(params = {},config = {})=>http.get(store.state.user.path + '/orderFollow/getOrderFollowList',{ params }),
91
 		getOrderFollowList:(params = {},config = {})=>http.get(store.state.user.path + '/orderFollow/getOrderFollowList',{ params }),
92
-		getDuplicateClueFollowByClueId:(params = {},config = {})=>http.get(store.state.user.path + '/clueFollow/getDuplicateClueFollowByClueId',{ params }),
93
 		
92
 		
94
 		selectCommissionList:(params={},data={})=>http.post(store.state.user.path + '/clueCommissionForm/list?' + qs.stringify(params),data),
93
 		selectCommissionList:(params={},data={})=>http.post(store.state.user.path + '/clueCommissionForm/list?' + qs.stringify(params),data),
94
+		getClueSendFormVoByOrderId:(params = {},config = {})=>http.get(store.state.user.path + '/clueSendForm/getClueSendFormVoByOrderId',{ params }),
95
+		// 跟进记录相关接口
96
+		getDuplicateClueFollowByClueId:(params,config={})=>http.get(store.state.user.path + '/clueFollow/getDuplicateClueFollowByClueId',{params,...config}),
97
+		// 文件相关接口
98
+		selectClueFileByDto:(data,config={})=>http.post(store.state.user.path + '/clueFile/selectClueFileByDto',data,config),
99
+		updateClueFile:(data,config={})=>http.put(store.state.user.path + '/clueFile/updateClueFile',data),
95
 	}
100
 	}
96
 }
101
 }
97
 
102