xsh_1997 1 tydzień temu
rodzic
commit
29ed7f788e

+ 17 - 6
huimv-employment/app/common/config.js

@@ -28,7 +28,8 @@ export const PUBLIC_BASE_API = (() => {
28
 
28
 
29
 /**
29
 /**
30
  * 将后端返回的相对资源路径(如登记二维码)拼成可访问 URL。
30
  * 将后端返回的相对资源路径(如登记二维码)拼成可访问 URL。
31
- * 与接口同主机:BASE_API=http://host:port/api/v1/mp → 资源 /api/v1/public/... → http://host:port/api/v1/public/...
31
+ * BASE_API=https://host/deploymentApi/api/v1/mp
32
+ * → 相对路径 /api/v1/public/... 拼成 https://host/deploymentApi/api/v1/public/...
32
  */
33
  */
33
 export function resolveApiAssetUrl(path) {
34
 export function resolveApiAssetUrl(path) {
34
 	if (!path) return ''
35
 	if (!path) return ''
@@ -38,12 +39,22 @@ export function resolveApiAssetUrl(path) {
38
 
39
 
39
 	const normalized = raw.startsWith('/') ? raw : `/${raw}`
40
 	const normalized = raw.startsWith('/') ? raw : `/${raw}`
40
 	const base = (BASE_API || '').replace(/\/$/, '')
41
 	const base = (BASE_API || '').replace(/\/$/, '')
41
-	const originMatch = base.match(/^(https?:\/\/[^/]+)/i)
42
-	if (originMatch) {
43
-		return `${originMatch[1]}${normalized}`
42
+	const originMatch = base.match(/^(https?:\/\/[^/]+)(\/.*)?$/i)
43
+	if (!originMatch) {
44
+		// H5 代理模式:BASE_API 为 /api/v1/mp,同源相对路径直接可用
45
+		return normalized
44
 	}
46
 	}
45
-	// H5 代理模式:BASE_API 为 /api/v1/mp,同源相对路径直接可用
46
-	return normalized
47
+
48
+	const origin = originMatch[1]
49
+	// 保留 /api 前的网关前缀(如 /deploymentApi)
50
+	const pathPart = originMatch[2] || ''
51
+	const apiIdx = pathPart.toLowerCase().indexOf('/api/')
52
+	const gatewayPrefix = apiIdx >= 0 ? pathPart.slice(0, apiIdx) : ''
53
+
54
+	if (gatewayPrefix && normalized.toLowerCase().indexOf(gatewayPrefix.toLowerCase()) === 0) {
55
+		return `${origin}${normalized}`
56
+	}
57
+	return `${origin}${gatewayPrefix}${normalized}`
47
 }
58
 }
48
 
59
 
49
 /** 本地存储 Token 键名 */
60
 /** 本地存储 Token 键名 */

+ 26 - 0
huimv-employment/app/common/fe.scss

@@ -330,12 +330,38 @@ page {
330
 
330
 
331
 .fe-biz-card--draft {
331
 .fe-biz-card--draft {
332
 	border-left: 6rpx solid $fe-warning;
332
 	border-left: 6rpx solid $fe-warning;
333
+	position: relative;
334
+	overflow: hidden;
333
 }
335
 }
334
 
336
 
335
 .fe-biz-card--draft-confirmed {
337
 .fe-biz-card--draft-confirmed {
336
 	border-left-color: $fe-success;
338
 	border-left-color: $fe-success;
337
 }
339
 }
338
 
340
 
341
+.fe-biz-card--draft-voided {
342
+	opacity: 0.62;
343
+	filter: grayscale(0.35);
344
+	border-left-color: #94A3B8;
345
+}
346
+
347
+.fe-draft-void-stamp {
348
+	position: absolute;
349
+	right: 20rpx;
350
+	bottom: 28rpx;
351
+	z-index: 2;
352
+	padding: 8rpx 18rpx;
353
+	border: 4rpx solid rgba(239, 68, 68, 0.85);
354
+	border-radius: 8rpx;
355
+	color: rgba(239, 68, 68, 0.9);
356
+	font-size: 28rpx;
357
+	font-weight: 700;
358
+	letter-spacing: 4rpx;
359
+	line-height: 1.2;
360
+	transform: rotate(-28deg);
361
+	background: rgba(255, 255, 255, 0.72);
362
+	pointer-events: none;
363
+}
364
+
339
 .fe-biz-card__header {
365
 .fe-biz-card__header {
340
 	display: flex;
366
 	display: flex;
341
 	align-items: center;
367
 	align-items: center;

+ 182 - 68
huimv-employment/app/packageA/components/home/EnterpriseHome.vue

@@ -116,13 +116,14 @@
116
 					<view v-else-if="msg.type === 'draft'" class="fe-chat-msg fe-chat-msg--ai" :id="'msg-' + idx">
116
 					<view v-else-if="msg.type === 'draft'" class="fe-chat-msg fe-chat-msg--ai" :id="'msg-' + idx">
117
 						<view class="fe-chat-avatar fe-chat-avatar--ai">AI</view>
117
 						<view class="fe-chat-avatar fe-chat-avatar--ai">AI</view>
118
 						<view class="fe-chat-bubble">
118
 						<view class="fe-chat-bubble">
119
-							<view class="fe-biz-card fe-biz-card--draft" :class="{ 'fe-biz-card--draft-confirmed': isDraftConfirmedByIndex(idx) }" @tap="openDraftByIndex(idx)">
119
+							<view
120
+								class="fe-biz-card fe-biz-card--draft"
121
+								:class="msg.draftRootClass"
122
+								@tap="openDraftByIndex(idx)"
123
+							>
120
 								<view class="fe-biz-card__header">
124
 								<view class="fe-biz-card__header">
121
 									<text class="fe-biz-card__header-title">📄 {{ (msg.draft && msg.draft.title) || '用工草稿' }}</text>
125
 									<text class="fe-biz-card__header-title">📄 {{ (msg.draft && msg.draft.title) || '用工草稿' }}</text>
122
-									<text
123
-										class="fe-draft-card-status"
124
-										:class="isDraftConfirmedByIndex(idx) ? 'fe-draft-card-status--confirmed' : 'fe-draft-card-status--pending'"
125
-									>{{ draftCardStatusLabelByIndex(idx) }}</text>
126
+									<text class="fe-draft-card-status" :class="msg.draftStatusClass">{{ msg.draftStatusLabel }}</text>
126
 								</view>
127
 								</view>
127
 								<view class="fe-biz-card__row">
128
 								<view class="fe-biz-card__row">
128
 									<text>岗位类型</text>
129
 									<text>岗位类型</text>
@@ -144,6 +145,7 @@
144
 									<view class="fe-biz-btn fe-biz-btn--primary" @tap.stop="openDraftByIndex(idx)">查看详情</view>
145
 									<view class="fe-biz-btn fe-biz-btn--primary" @tap.stop="openDraftByIndex(idx)">查看详情</view>
145
 									<text class="fe-biz-card__hint">如需修改草稿,请直接在对话中说明</text>
146
 									<text class="fe-biz-card__hint">如需修改草稿,请直接在对话中说明</text>
146
 								</view>
147
 								</view>
148
+								<view v-if="msg.voided" class="fe-draft-void-stamp">已作废</view>
147
 							</view>
149
 							</view>
148
 						</view>
150
 						</view>
149
 					</view>
151
 					</view>
@@ -419,6 +421,7 @@ import {
419
 	isDraftCardContentType,
421
 	isDraftCardContentType,
420
 	buildDraftCardMessage,
422
 	buildDraftCardMessage,
421
 	mapDraftDetailToCard,
423
 	mapDraftDetailToCard,
424
+	syncDraftCardViewMeta,
422
 	getDraftStatus,
425
 	getDraftStatus,
423
 	pickDraftQrCodeUrl,
426
 	pickDraftQrCodeUrl,
424
 	pickDraftQrToken,
427
 	pickDraftQrToken,
@@ -484,6 +487,8 @@ export default {
484
 			modal: '',
487
 			modal: '',
485
 			confirmInfo: null,
488
 			confirmInfo: null,
486
 			qrBatch: { ...BATCH_QR },
489
 			qrBatch: { ...BATCH_QR },
490
+			/** 已处理过「方案已确认 + 二维码」的草稿 id,同会话内只执行一次 */
491
+			orderConfirmHandledIds: {},
487
 			inbox: MESSAGES,
492
 			inbox: MESSAGES,
488
 			reportCardVisible: true,
493
 			reportCardVisible: true,
489
 			reportObserver: null,
494
 			reportObserver: null,
@@ -618,8 +623,10 @@ export default {
618
 			this.currentConversationId = session ? session.id : null
623
 			this.currentConversationId = session ? session.id : null
619
 			this.resetMessagePagination()
624
 			this.resetMessagePagination()
620
 			this.messages = []
625
 			this.messages = []
626
+			this.orderConfirmHandledIds = {}
621
 			this.reportCardVisible = true
627
 			this.reportCardVisible = true
622
 			await this.fetchLatestMessages({ scrollBottom: true })
628
 			await this.fetchLatestMessages({ scrollBottom: true })
629
+			this.markOrderConfirmHandledFromMessages()
623
 			this.$nextTick(() => {
630
 			this.$nextTick(() => {
624
 				this.setupReportObserver()
631
 				this.setupReportObserver()
625
 				setTimeout(() => this.checkReportVisibility(), 120)
632
 				setTimeout(() => this.checkReportVisibility(), 120)
@@ -951,11 +958,11 @@ export default {
951
 					} else if (!this.messages[aiIdx].content) {
958
 					} else if (!this.messages[aiIdx].content) {
952
 						this.$set(this.messages[aiIdx], 'content', '暂无回复,请稍后重试')
959
 						this.$set(this.messages[aiIdx], 'content', '暂无回复,请稍后重试')
953
 					}
960
 					}
954
-					// 仅当本轮 SSE 明确提到某草稿 id(card_draft / card_order)时,才将该草稿卡沉底
961
+					// 本轮 SSE 带草稿卡:card_draft 作废旧卡并末尾新卡;card_order 仅首次补卡
955
 					if (shouldFetchDraftCard(cardMeta)) {
962
 					if (shouldFetchDraftCard(cardMeta)) {
956
-						await this.appendDraftCardById(cardMeta.relatedDraftId, { sinkToBottom: true })
963
+						await this.handleDraftCardFromChat(cardMeta)
957
 					}
964
 					}
958
-					// next_action=show_order_qrcode → 弹出「方案已确认」
965
+					// next_action=show_order_qrcode:同草稿只生成一次确认文案 + 二维码
959
 					if (shouldShowOrderQrcode(cardMeta)) {
966
 					if (shouldShowOrderQrcode(cardMeta)) {
960
 						await this.showOrderConfirmFromChat(cardMeta)
967
 						await this.showOrderConfirmFromChat(cardMeta)
961
 					}
968
 					}
@@ -986,16 +993,32 @@ export default {
986
 			this.canSend = true
993
 			this.canSend = true
987
 			this.sendMsg()
994
 			this.sendMsg()
988
 		},
995
 		},
996
+		isDraftVoidedByIndex(idx) {
997
+			const msg = this.messages[idx]
998
+			return !!(msg && msg.type === 'draft' && msg.voided)
999
+		},
989
 		isDraftConfirmedByIndex(idx) {
1000
 		isDraftConfirmedByIndex(idx) {
990
 			const msg = this.messages[idx]
1001
 			const msg = this.messages[idx]
991
-			return !!(msg && msg.draft && getDraftStatus(msg.draft) === 'confirmed')
1002
+			if (!msg || msg.voided) return false
1003
+			return !!(msg.draft && getDraftStatus(msg.draft) === 'confirmed')
992
 		},
1004
 		},
993
 		draftCardStatusLabelByIndex(idx) {
1005
 		draftCardStatusLabelByIndex(idx) {
1006
+			if (this.isDraftVoidedByIndex(idx)) return '已作废'
994
 			return this.isDraftConfirmedByIndex(idx) ? '已确认' : '未确认'
1007
 			return this.isDraftConfirmedByIndex(idx) ? '已确认' : '未确认'
995
 		},
1008
 		},
1009
+		draftCardStatusClassByIndex(idx) {
1010
+			if (this.isDraftVoidedByIndex(idx)) return 'fe-draft-card-status--cancelled'
1011
+			return this.isDraftConfirmedByIndex(idx)
1012
+				? 'fe-draft-card-status--confirmed'
1013
+				: 'fe-draft-card-status--pending'
1014
+		},
996
 		async openDraftByIndex(idx) {
1015
 		async openDraftByIndex(idx) {
997
 			const msg = this.messages[idx]
1016
 			const msg = this.messages[idx]
998
 			if (!msg || msg.type !== 'draft' || !msg.draft) return
1017
 			if (!msg || msg.type !== 'draft' || !msg.draft) return
1018
+			if (msg.voided) {
1019
+				this.toast('该草稿已作废')
1020
+				return
1021
+			}
999
 			let draft = msg.draft
1022
 			let draft = msg.draft
1000
 			this.closeAll()
1023
 			this.closeAll()
1001
 			this.activeDraft = draft
1024
 			this.activeDraft = draft
@@ -1033,42 +1056,84 @@ export default {
1033
 			if (draftId == null) return
1056
 			if (draftId == null) return
1034
 			const id = Number(draftId)
1057
 			const id = Number(draftId)
1035
 			if (Number.isNaN(id)) return
1058
 			if (Number.isNaN(id)) return
1059
+			const key = String(id)
1036
 			for (let i = this.messages.length - 1; i >= 0; i--) {
1060
 			for (let i = this.messages.length - 1; i >= 0; i--) {
1037
 				const msg = this.messages[i]
1061
 				const msg = this.messages[i]
1038
-				if (msg && msg.type === 'draft' && msg.draftId === id) {
1062
+				if (msg && msg.type === 'draft' && String(msg.draftId) === key) {
1039
 					this.messages.splice(i, 1)
1063
 					this.messages.splice(i, 1)
1040
 				}
1064
 				}
1041
 			}
1065
 			}
1042
 		},
1066
 		},
1043
 		/**
1067
 		/**
1044
-		 * 更新草稿卡片
1068
+		 * 更新草稿卡片(默认原地更新,不沉底)
1045
 		 * @param {object} draftCard
1069
 		 * @param {object} draftCard
1046
-		 * @param {{ sinkToBottom?: boolean }} [options] sinkToBottom 默认 true:同 id 旧卡移除后沉底
1070
+		 * @param {{ sinkToBottom?: boolean }} [options] 仅显式传 true 时才沉底
1047
 		 */
1071
 		 */
1048
 		onDraftUpdated(draftCard, options = {}) {
1072
 		onDraftUpdated(draftCard, options = {}) {
1049
 			if (!draftCard || draftCard.draftId == null) return
1073
 			if (!draftCard || draftCard.draftId == null) return
1050
 			this.activeDraft = draftCard
1074
 			this.activeDraft = draftCard
1051
-			const sinkToBottom = options.sinkToBottom !== false
1075
+			const sinkToBottom = options.sinkToBottom === true
1076
+			const draftKey = String(draftCard.draftId)
1052
 			if (!sinkToBottom) {
1077
 			if (!sinkToBottom) {
1053
-				for (let i = 0; i < this.messages.length; i++) {
1078
+				for (let i = this.messages.length - 1; i >= 0; i--) {
1054
 					const msg = this.messages[i]
1079
 					const msg = this.messages[i]
1055
-					if (msg.type === 'draft' && msg.draftId === draftCard.draftId) {
1080
+					if (
1081
+						msg.type === 'draft'
1082
+						&& String(msg.draftId) === draftKey
1083
+						&& !msg.voided
1084
+					) {
1056
 						this.$set(this.messages[i], 'draft', draftCard)
1085
 						this.$set(this.messages[i], 'draft', draftCard)
1057
-						break
1086
+						syncDraftCardViewMeta(this.messages[i])
1087
+						return
1058
 					}
1088
 					}
1059
 				}
1089
 				}
1090
+				// 仅有已作废同草稿卡时不「复活」新卡(避免普通对话末尾再插一张)
1091
+				const hasSameDraft = this.messages.some(
1092
+					(m) => m && m.type === 'draft' && String(m.draftId) === draftKey
1093
+				)
1094
+				if (hasSameDraft) return
1095
+
1096
+				// 会话中尚无该草稿卡时追加一次
1097
+				this.messages.push(syncDraftCardViewMeta({
1098
+					type: 'draft',
1099
+					role: 'ai',
1100
+					msgKey: this.nextLocalMsgKey(),
1101
+					draftId: draftCard.draftId,
1102
+					voided: false,
1103
+					draft: draftCard
1104
+				}))
1060
 				return
1105
 				return
1061
 			}
1106
 			}
1062
 			this.removeDraftCardById(draftCard.draftId)
1107
 			this.removeDraftCardById(draftCard.draftId)
1063
-			this.messages.push({
1108
+			this.messages.push(syncDraftCardViewMeta({
1064
 				type: 'draft',
1109
 				type: 'draft',
1065
 				role: 'ai',
1110
 				role: 'ai',
1066
 				msgKey: this.nextLocalMsgKey(),
1111
 				msgKey: this.nextLocalMsgKey(),
1067
 				draftId: draftCard.draftId,
1112
 				draftId: draftCard.draftId,
1113
+				voided: false,
1068
 				draft: draftCard
1114
 				draft: draftCard
1069
-			})
1115
+			}))
1070
 			this.ensureChatBottom()
1116
 			this.ensureChatBottom()
1071
 		},
1117
 		},
1118
+		/**
1119
+		 * SSE 草稿卡:
1120
+		 * - card_draft:作废旧卡并在末尾生成新卡(对话改草稿)
1121
+		 * - card_order:后端在「已有确认订单+二维码」时每轮都会注入,已有有效卡则跳过,避免普通问答反复插卡
1122
+		 */
1123
+		async handleDraftCardFromChat(cardMeta) {
1124
+			if (!shouldFetchDraftCard(cardMeta)) return null
1125
+			const draftId = cardMeta.relatedDraftId
1126
+			if (cardMeta.contentType === 'card_order') {
1127
+				const exists = this.messages.some(
1128
+					(m) => m
1129
+						&& m.type === 'draft'
1130
+						&& !m.voided
1131
+						&& String(m.draftId) === String(draftId)
1132
+				)
1133
+				if (exists) return null
1134
+			}
1135
+			return this.appendDraftCardFromChat(draftId)
1136
+		},
1072
 		/**
1137
 		/**
1073
 		 * 历史消息:每个 related_draft_id 对应一张草稿卡,插到「提到该 id」的文本之后;其它草稿互不影响
1138
 		 * 历史消息:每个 related_draft_id 对应一张草稿卡,插到「提到该 id」的文本之后;其它草稿互不影响
1074
 		 * @param {Array} [sourceMessages]
1139
 		 * @param {Array} [sourceMessages]
@@ -1092,29 +1157,97 @@ export default {
1092
 			}
1157
 			}
1093
 		},
1158
 		},
1094
 		/**
1159
 		/**
1095
-		 * 查询草稿详情并插入聊天气泡卡片
1096
-		 * @param {number} draftId
1097
-		 * @param {{ afterRelatedText?: boolean, sinkToBottom?: boolean }} [options]
1098
-		 *   afterRelatedText:插到提到该草稿 id 的文本后(历史还原)
1099
-		 *   sinkToBottom:本轮对话提到该 id 时,移除同 id 旧卡并沉底(其它草稿卡不动)
1160
+		 * 根据当前消息中已有的二维码卡 / 已确认草稿,标记对应草稿已处理过确认流程
1100
 		 */
1161
 		 */
1101
-		async appendDraftCardById(draftId, options = {}) {
1162
+		markOrderConfirmHandledFromMessages() {
1163
+			const map = Object.assign({}, this.orderConfirmHandledIds)
1164
+			for (let i = 0; i < this.messages.length; i++) {
1165
+				const msg = this.messages[i]
1166
+				if (!msg) continue
1167
+				if (msg.type === 'qr' && msg.draftId != null) {
1168
+					map[String(msg.draftId)] = true
1169
+				}
1170
+				if (
1171
+					msg.type === 'draft'
1172
+					&& !msg.voided
1173
+					&& msg.draftId != null
1174
+					&& msg.draft
1175
+					&& getDraftStatus(msg.draft) === 'confirmed'
1176
+				) {
1177
+					map[String(msg.draftId)] = true
1178
+				}
1179
+			}
1180
+			this.orderConfirmHandledIds = map
1181
+		},
1182
+		isOrderConfirmHandled(draftId) {
1183
+			if (draftId == null) return false
1184
+			return !!this.orderConfirmHandledIds[String(draftId)]
1185
+		},
1186
+		markOrderConfirmHandled(draftId) {
1187
+			if (draftId == null) return
1188
+			this.$set(this.orderConfirmHandledIds, String(draftId), true)
1189
+		},
1190
+		/**
1191
+		 * 将会话中同草稿 / 同订单的有效草稿卡标记为已作废
1192
+		 */
1193
+		voidRelatedDraftCards(options = {}) {
1194
+			const draftId = options.draftId
1195
+			const orderId = options.orderId
1196
+			const draftKey = draftId != null ? String(draftId) : ''
1197
+			const orderKey = orderId != null && orderId !== '' ? String(orderId) : ''
1198
+			if (!draftKey && !orderKey) return
1199
+
1200
+			for (let i = 0; i < this.messages.length; i++) {
1201
+				const msg = this.messages[i]
1202
+				if (!msg || msg.type !== 'draft' || msg.voided) continue
1203
+				const sameDraft = draftKey && String(msg.draftId) === draftKey
1204
+				const msgOrderId = msg.draft && (msg.draft.orderId || msg.draft.order_id)
1205
+				const sameOrder = orderKey && msgOrderId != null && String(msgOrderId) === orderKey
1206
+				if (sameDraft || sameOrder) {
1207
+					this.$set(this.messages[i], 'voided', true)
1208
+					syncDraftCardViewMeta(this.messages[i])
1209
+				}
1210
+			}
1211
+		},
1212
+		/**
1213
+		 * SSE 返回草稿卡:作废上方同订单/同草稿旧卡,并在会话末尾生成新卡
1214
+		 */
1215
+		async appendDraftCardFromChat(draftId) {
1102
 			if (draftId == null) return null
1216
 			if (draftId == null) return null
1103
 			const id = Number(draftId)
1217
 			const id = Number(draftId)
1104
 			if (Number.isNaN(id)) return null
1218
 			if (Number.isNaN(id)) return null
1105
-			const exists = this.messages.some((m) => m.type === 'draft' && m.draftId === id)
1106
-			if (exists && options.sinkToBottom) {
1107
-				this.removeDraftCardById(id)
1108
-			} else if (exists && options.afterRelatedText) {
1109
-				return null
1110
-			} else if (exists) {
1219
+
1220
+			try {
1221
+				const detail = await getDraftDetail(id, { showError: false })
1222
+				if (!detail || detail.id == null) return null
1223
+				const orderId = detail.orderId || detail.order_id || ''
1224
+				this.voidRelatedDraftCards({ draftId: id, orderId })
1225
+				const cardMsg = buildDraftCardMessage(detail, this.nextLocalMsgKey())
1226
+				cardMsg.voided = false
1227
+				this.messages.push(cardMsg)
1228
+				this.ensureChatBottom()
1229
+				return cardMsg
1230
+			} catch (e) {
1111
 				return null
1231
 				return null
1112
 			}
1232
 			}
1233
+		},
1234
+		/**
1235
+		 * 历史消息还原:插到提到该草稿 id 的文本后(不作废链路)
1236
+		 * @param {number} draftId
1237
+		 * @param {{ afterRelatedText?: boolean }} [options]
1238
+		 */
1239
+		async appendDraftCardById(draftId, options = {}) {
1240
+			if (draftId == null) return null
1241
+			const id = Number(draftId)
1242
+			if (Number.isNaN(id)) return null
1243
+			const exists = this.messages.some((m) => m.type === 'draft' && m.draftId === id && !m.voided)
1244
+			if (exists) return null
1113
 
1245
 
1114
 			try {
1246
 			try {
1115
 				const detail = await getDraftDetail(id, { showError: false })
1247
 				const detail = await getDraftDetail(id, { showError: false })
1116
 				if (!detail || detail.id == null) return null
1248
 				if (!detail || detail.id == null) return null
1117
 				const cardMsg = buildDraftCardMessage(detail, this.nextLocalMsgKey())
1249
 				const cardMsg = buildDraftCardMessage(detail, this.nextLocalMsgKey())
1250
+				cardMsg.voided = false
1118
 
1251
 
1119
 				if (options.afterRelatedText) {
1252
 				if (options.afterRelatedText) {
1120
 					let insertAt = -1
1253
 					let insertAt = -1
@@ -1138,7 +1271,6 @@ export default {
1138
 							}
1271
 							}
1139
 						}
1272
 						}
1140
 					}
1273
 					}
1141
-					// 只插到「提到该草稿 id」的消息后面,找不到则不误沉底
1142
 					if (insertAt < 0) return null
1274
 					if (insertAt < 0) return null
1143
 					this.messages.splice(insertAt, 0, cardMsg)
1275
 					this.messages.splice(insertAt, 0, cardMsg)
1144
 				} else {
1276
 				} else {
@@ -1267,18 +1399,14 @@ export default {
1267
 			}
1399
 			}
1268
 		},
1400
 		},
1269
 		/**
1401
 		/**
1270
-		 * 确认方案后在对话框追加二维码卡片(同 draft 只保留一张
1402
+		 * 确认方案后在对话框追加二维码卡片(同 draft 已有则不再追加
1271
 		 */
1403
 		 */
1272
 		appendQrCardMessage(batch) {
1404
 		appendQrCardMessage(batch) {
1273
 			if (!batch) return
1405
 			if (!batch) return
1274
 			const draftId = batch.draftId
1406
 			const draftId = batch.draftId
1275
 			if (draftId != null) {
1407
 			if (draftId != null) {
1276
-				for (let i = this.messages.length - 1; i >= 0; i--) {
1277
-					const m = this.messages[i]
1278
-					if (m && m.type === 'qr' && m.draftId === draftId) {
1279
-						this.messages.splice(i, 1)
1280
-					}
1281
-				}
1408
+				const exists = this.messages.some((m) => m && m.type === 'qr' && m.draftId === draftId)
1409
+				if (exists) return
1282
 			}
1410
 			}
1283
 			this.messages.push({
1411
 			this.messages.push({
1284
 				type: 'qr',
1412
 				type: 'qr',
@@ -1331,13 +1459,16 @@ export default {
1331
 			this.openProgressDetail(draftId)
1459
 			this.openProgressDetail(draftId)
1332
 		},
1460
 		},
1333
 		/**
1461
 		/**
1334
-		 * AI 对话 next_action=show_order_qrcode:弹出方案已确认弹框(并可查看二维码)
1462
+		 * AI 对话 next_action=show_order_qrcode:
1463
+		 * 同草稿只执行一次:写确认文案 + 二维码卡,草稿卡原地标已确认(不沉底)
1335
 		 */
1464
 		 */
1336
 		async showOrderConfirmFromChat(cardMeta) {
1465
 		async showOrderConfirmFromChat(cardMeta) {
1337
 			const payload = parseChatCardPayload(cardMeta && cardMeta.cardPayload)
1466
 			const payload = parseChatCardPayload(cardMeta && cardMeta.cardPayload)
1338
 			const draftId = (cardMeta && cardMeta.relatedDraftId != null)
1467
 			const draftId = (cardMeta && cardMeta.relatedDraftId != null)
1339
 				? cardMeta.relatedDraftId
1468
 				? cardMeta.relatedDraftId
1340
 				: (payload && (payload.draft_id != null ? payload.draft_id : payload.draftId))
1469
 				: (payload && (payload.draft_id != null ? payload.draft_id : payload.draftId))
1470
+			if (this.isOrderConfirmHandled(draftId)) return
1471
+
1341
 			let draft = null
1472
 			let draft = null
1342
 			if (this.activeDraft && String(this.activeDraft.draftId) === String(draftId)) {
1473
 			if (this.activeDraft && String(this.activeDraft.draftId) === String(draftId)) {
1343
 				draft = this.activeDraft
1474
 				draft = this.activeDraft
@@ -1372,30 +1503,6 @@ export default {
1372
 			let qrToken = (payload && (payload.qr_token || payload.qrToken))
1503
 			let qrToken = (payload && (payload.qr_token || payload.qrToken))
1373
 				|| pickDraftQrToken(draft)
1504
 				|| pickDraftQrToken(draft)
1374
 				|| ''
1505
 				|| ''
1375
-			const totalOutflow = payload && (payload.total_outflow != null ? payload.total_outflow : payload.totalOutflow)
1376
-			const totalText = totalOutflow != null && totalOutflow !== ''
1377
-				? formatDraftMoney(totalOutflow)
1378
-				: ((draft && draft.estimatedTotalText) || '')
1379
-			const workerCount = (payload && (payload.worker_count != null ? payload.worker_count : payload.workerCount))
1380
-				|| (draft && draft.workerCount)
1381
-			let avgCost = ''
1382
-			if (totalOutflow != null && workerCount > 0) {
1383
-				avgCost = `${formatDraftMoney(Number(totalOutflow) / Number(workerCount))}/人`
1384
-			} else if (draft && draft.confirmedPlan && draft.confirmedPlan.avgCost) {
1385
-				avgCost = draft.confirmedPlan.avgCost
1386
-			}
1387
-
1388
-			this.confirmInfo = {
1389
-				title: title || '方案已确认',
1390
-				code: (draft && draft.selectedSchemeCode) || '',
1391
-				total: totalText,
1392
-				avgCost,
1393
-				tagsText: '',
1394
-				orderId,
1395
-				qrToken,
1396
-				qrCodeUrl,
1397
-				draftId
1398
-			}
1399
 
1506
 
1400
 			const scene = qrToken || orderId || ''
1507
 			const scene = qrToken || orderId || ''
1401
 			this.qrBatch = this.buildQrBatch(draft, {
1508
 			this.qrBatch = this.buildQrBatch(draft, {
@@ -1413,16 +1520,16 @@ export default {
1413
 				if (fromDetail && fromDetail.qrCodeUrl) {
1520
 				if (fromDetail && fromDetail.qrCodeUrl) {
1414
 					qrCodeUrl = fromDetail.qrCodeUrl
1521
 					qrCodeUrl = fromDetail.qrCodeUrl
1415
 					this.$set(this.qrBatch, 'qrCodeUrl', fromDetail.qrCodeUrl)
1522
 					this.$set(this.qrBatch, 'qrCodeUrl', fromDetail.qrCodeUrl)
1416
-					this.$set(this.confirmInfo, 'qrCodeUrl', fromDetail.qrCodeUrl)
1417
 				}
1523
 				}
1418
 				if (fromDetail && fromDetail.qrToken) {
1524
 				if (fromDetail && fromDetail.qrToken) {
1419
 					qrToken = fromDetail.qrToken
1525
 					qrToken = fromDetail.qrToken
1420
 					this.$set(this.qrBatch, 'qrToken', fromDetail.qrToken)
1526
 					this.$set(this.qrBatch, 'qrToken', fromDetail.qrToken)
1421
 					this.$set(this.qrBatch, 'scene', fromDetail.qrToken)
1527
 					this.$set(this.qrBatch, 'scene', fromDetail.qrToken)
1422
-					this.$set(this.confirmInfo, 'qrToken', fromDetail.qrToken)
1423
 				}
1528
 				}
1424
 			}
1529
 			}
1425
 
1530
 
1531
+			this.markOrderConfirmHandled(draftId)
1532
+
1426
 			if (draft && draftId != null) {
1533
 			if (draft && draftId != null) {
1427
 				this.onDraftUpdated(Object.assign({}, draft, {
1534
 				this.onDraftUpdated(Object.assign({}, draft, {
1428
 					status: 'confirmed',
1535
 					status: 'confirmed',
@@ -1430,12 +1537,18 @@ export default {
1430
 					qrToken,
1537
 					qrToken,
1431
 					qrCodeUrl,
1538
 					qrCodeUrl,
1432
 					scene: qrToken || orderId || ''
1539
 					scene: qrToken || orderId || ''
1433
-				}), { sinkToBottom: true })
1540
+				}), { sinkToBottom: false })
1434
 			}
1541
 			}
1435
 
1542
 
1436
-			this.appendQrCardMessage(this.qrBatch)
1437
 			this.closeAll()
1543
 			this.closeAll()
1438
-			setTimeout(() => { this.modal = 'confirm' }, 300)
1544
+			this.messages.push({
1545
+				type: 'text',
1546
+				role: 'ai',
1547
+				msgKey: this.nextLocalMsgKey(),
1548
+				content: '方案已确认。请将下方二维码分享给工人扫码登记。'
1549
+			})
1550
+			this.appendQrCardMessage(this.qrBatch)
1551
+			this.ensureChatBottom()
1439
 		},
1552
 		},
1440
 		async confirmPlan(payload) {
1553
 		async confirmPlan(payload) {
1441
 			const scheme = (payload && payload.scheme) || null
1554
 			const scheme = (payload && payload.scheme) || null
@@ -1510,6 +1623,7 @@ export default {
1510
 				msgKey: this.nextLocalMsgKey(),
1623
 				msgKey: this.nextLocalMsgKey(),
1511
 				content: '方案已确认。请将下方二维码分享给工人扫码登记。'
1624
 				content: '方案已确认。请将下方二维码分享给工人扫码登记。'
1512
 			})
1625
 			})
1626
+			this.markOrderConfirmHandled(draftId)
1513
 			this.appendQrCardMessage(this.qrBatch)
1627
 			this.appendQrCardMessage(this.qrBatch)
1514
 			setTimeout(() => { this.modal = 'confirm' }, 400)
1628
 			setTimeout(() => { this.modal = 'confirm' }, 400)
1515
 		},
1629
 		},

+ 23 - 3
huimv-employment/app/utils/draft.js

@@ -15,7 +15,7 @@ export const NEXT_ACTION_SHOW_ORDER_QRCODE = 'show_order_qrcode'
15
 /**
15
 /**
16
  * 从 SSE 事件 JSON 提取卡片元数据(camelCase / snake_case 兼容)
16
  * 从 SSE 事件 JSON 提取卡片元数据(camelCase / snake_case 兼容)
17
  * - content_type=card_draft|card_order|card_cost
17
  * - content_type=card_draft|card_order|card_cost
18
- * - 或 next_action=show_order_qrcode(方案已确认弹框
18
+ * - 或 next_action=show_order_qrcode(对话写入确认文案 + 二维码卡
19
  */
19
  */
20
 export function extractChatCardMeta(data) {
20
 export function extractChatCardMeta(data) {
21
 	if (!data || typeof data !== 'object') return null
21
 	if (!data || typeof data !== 'object') return null
@@ -77,7 +77,7 @@ export function shouldFetchDraftCard(meta) {
77
 }
77
 }
78
 
78
 
79
 /**
79
 /**
80
- * AI 对话:下一步为展示订单二维码(方案已确认弹框
80
+ * AI 对话:下一步为展示订单二维码(对话内确认文案 + 二维码卡
81
  */
81
  */
82
 export function shouldShowOrderQrcode(meta) {
82
 export function shouldShowOrderQrcode(meta) {
83
 	if (!meta) return false
83
 	if (!meta) return false
@@ -314,6 +314,7 @@ export function mapDraftDetailToCard(detail = {}) {
314
 			|| detail.selected_scheme_code
314
 			|| detail.selected_scheme_code
315
 			|| null,
315
 			|| null,
316
 		status: detail.status,
316
 		status: detail.status,
317
+		orderId: detail.orderId || detail.order_id || '',
317
 		missingFields: detail.missingFields || [],
318
 		missingFields: detail.missingFields || [],
318
 		qrCodeUrl: pickDraftQrCodeUrl(detail),
319
 		qrCodeUrl: pickDraftQrCodeUrl(detail),
319
 		qrToken: pickDraftQrToken(detail),
320
 		qrToken: pickDraftQrToken(detail),
@@ -325,11 +326,30 @@ export function mapDraftDetailToCard(detail = {}) {
325
  * 构造聊天气泡:用工草稿卡片
326
  * 构造聊天气泡:用工草稿卡片
326
  */
327
  */
327
 export function buildDraftCardMessage(detail, msgKey) {
328
 export function buildDraftCardMessage(detail, msgKey) {
328
-	return {
329
+	const msg = {
329
 		type: 'draft',
330
 		type: 'draft',
330
 		role: 'ai',
331
 		role: 'ai',
331
 		msgKey,
332
 		msgKey,
332
 		draftId: detail.id,
333
 		draftId: detail.id,
334
+		voided: false,
333
 		draft: mapDraftDetailToCard(detail)
335
 		draft: mapDraftDetailToCard(detail)
334
 	}
336
 	}
337
+	return syncDraftCardViewMeta(msg)
338
+}
339
+
340
+/**
341
+ * 同步草稿卡展示字段(供小程序模板直接绑定,避免 :class 调方法)
342
+ */
343
+export function syncDraftCardViewMeta(msg) {
344
+	if (!msg || msg.type !== 'draft') return msg
345
+	const voided = !!msg.voided
346
+	const confirmed = !voided && !!msg.draft && getDraftStatus(msg.draft) === 'confirmed'
347
+	msg.draftRootClass = voided
348
+		? 'fe-biz-card--draft-voided'
349
+		: (confirmed ? 'fe-biz-card--draft-confirmed' : '')
350
+	msg.draftStatusClass = voided
351
+		? 'fe-draft-card-status--cancelled'
352
+		: (confirmed ? 'fe-draft-card-status--confirmed' : 'fe-draft-card-status--pending')
353
+	msg.draftStatusLabel = voided ? '已作废' : (confirmed ? '已确认' : '未确认')
354
+	return msg
335
 }
355
 }