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 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 34
 export function resolveApiAssetUrl(path) {
34 35
 	if (!path) return ''
@@ -38,12 +39,22 @@ export function resolveApiAssetUrl(path) {
38 39
 
39 40
 	const normalized = raw.startsWith('/') ? raw : `/${raw}`
40 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 60
 /** 本地存储 Token 键名 */

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

@@ -330,12 +330,38 @@ page {
330 330
 
331 331
 .fe-biz-card--draft {
332 332
 	border-left: 6rpx solid $fe-warning;
333
+	position: relative;
334
+	overflow: hidden;
333 335
 }
334 336
 
335 337
 .fe-biz-card--draft-confirmed {
336 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 365
 .fe-biz-card__header {
340 366
 	display: flex;
341 367
 	align-items: center;

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

@@ -116,13 +116,14 @@
116 116
 					<view v-else-if="msg.type === 'draft'" class="fe-chat-msg fe-chat-msg--ai" :id="'msg-' + idx">
117 117
 						<view class="fe-chat-avatar fe-chat-avatar--ai">AI</view>
118 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 124
 								<view class="fe-biz-card__header">
121 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 127
 								</view>
127 128
 								<view class="fe-biz-card__row">
128 129
 									<text>岗位类型</text>
@@ -144,6 +145,7 @@
144 145
 									<view class="fe-biz-btn fe-biz-btn--primary" @tap.stop="openDraftByIndex(idx)">查看详情</view>
145 146
 									<text class="fe-biz-card__hint">如需修改草稿,请直接在对话中说明</text>
146 147
 								</view>
148
+								<view v-if="msg.voided" class="fe-draft-void-stamp">已作废</view>
147 149
 							</view>
148 150
 						</view>
149 151
 					</view>
@@ -419,6 +421,7 @@ import {
419 421
 	isDraftCardContentType,
420 422
 	buildDraftCardMessage,
421 423
 	mapDraftDetailToCard,
424
+	syncDraftCardViewMeta,
422 425
 	getDraftStatus,
423 426
 	pickDraftQrCodeUrl,
424 427
 	pickDraftQrToken,
@@ -484,6 +487,8 @@ export default {
484 487
 			modal: '',
485 488
 			confirmInfo: null,
486 489
 			qrBatch: { ...BATCH_QR },
490
+			/** 已处理过「方案已确认 + 二维码」的草稿 id,同会话内只执行一次 */
491
+			orderConfirmHandledIds: {},
487 492
 			inbox: MESSAGES,
488 493
 			reportCardVisible: true,
489 494
 			reportObserver: null,
@@ -618,8 +623,10 @@ export default {
618 623
 			this.currentConversationId = session ? session.id : null
619 624
 			this.resetMessagePagination()
620 625
 			this.messages = []
626
+			this.orderConfirmHandledIds = {}
621 627
 			this.reportCardVisible = true
622 628
 			await this.fetchLatestMessages({ scrollBottom: true })
629
+			this.markOrderConfirmHandledFromMessages()
623 630
 			this.$nextTick(() => {
624 631
 				this.setupReportObserver()
625 632
 				setTimeout(() => this.checkReportVisibility(), 120)
@@ -951,11 +958,11 @@ export default {
951 958
 					} else if (!this.messages[aiIdx].content) {
952 959
 						this.$set(this.messages[aiIdx], 'content', '暂无回复,请稍后重试')
953 960
 					}
954
-					// 仅当本轮 SSE 明确提到某草稿 id(card_draft / card_order)时,才将该草稿卡沉底
961
+					// 本轮 SSE 带草稿卡:card_draft 作废旧卡并末尾新卡;card_order 仅首次补卡
955 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 966
 					if (shouldShowOrderQrcode(cardMeta)) {
960 967
 						await this.showOrderConfirmFromChat(cardMeta)
961 968
 					}
@@ -986,16 +993,32 @@ export default {
986 993
 			this.canSend = true
987 994
 			this.sendMsg()
988 995
 		},
996
+		isDraftVoidedByIndex(idx) {
997
+			const msg = this.messages[idx]
998
+			return !!(msg && msg.type === 'draft' && msg.voided)
999
+		},
989 1000
 		isDraftConfirmedByIndex(idx) {
990 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 1005
 		draftCardStatusLabelByIndex(idx) {
1006
+			if (this.isDraftVoidedByIndex(idx)) return '已作废'
994 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 1015
 		async openDraftByIndex(idx) {
997 1016
 			const msg = this.messages[idx]
998 1017
 			if (!msg || msg.type !== 'draft' || !msg.draft) return
1018
+			if (msg.voided) {
1019
+				this.toast('该草稿已作废')
1020
+				return
1021
+			}
999 1022
 			let draft = msg.draft
1000 1023
 			this.closeAll()
1001 1024
 			this.activeDraft = draft
@@ -1033,42 +1056,84 @@ export default {
1033 1056
 			if (draftId == null) return
1034 1057
 			const id = Number(draftId)
1035 1058
 			if (Number.isNaN(id)) return
1059
+			const key = String(id)
1036 1060
 			for (let i = this.messages.length - 1; i >= 0; i--) {
1037 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 1063
 					this.messages.splice(i, 1)
1040 1064
 				}
1041 1065
 			}
1042 1066
 		},
1043 1067
 		/**
1044
-		 * 更新草稿卡片
1068
+		 * 更新草稿卡片(默认原地更新,不沉底)
1045 1069
 		 * @param {object} draftCard
1046
-		 * @param {{ sinkToBottom?: boolean }} [options] sinkToBottom 默认 true:同 id 旧卡移除后沉底
1070
+		 * @param {{ sinkToBottom?: boolean }} [options] 仅显式传 true 时才沉底
1047 1071
 		 */
1048 1072
 		onDraftUpdated(draftCard, options = {}) {
1049 1073
 			if (!draftCard || draftCard.draftId == null) return
1050 1074
 			this.activeDraft = draftCard
1051
-			const sinkToBottom = options.sinkToBottom !== false
1075
+			const sinkToBottom = options.sinkToBottom === true
1076
+			const draftKey = String(draftCard.draftId)
1052 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 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 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 1105
 				return
1061 1106
 			}
1062 1107
 			this.removeDraftCardById(draftCard.draftId)
1063
-			this.messages.push({
1108
+			this.messages.push(syncDraftCardViewMeta({
1064 1109
 				type: 'draft',
1065 1110
 				role: 'ai',
1066 1111
 				msgKey: this.nextLocalMsgKey(),
1067 1112
 				draftId: draftCard.draftId,
1113
+				voided: false,
1068 1114
 				draft: draftCard
1069
-			})
1115
+			}))
1070 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 1138
 		 * 历史消息:每个 related_draft_id 对应一张草稿卡,插到「提到该 id」的文本之后;其它草稿互不影响
1074 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 1216
 			if (draftId == null) return null
1103 1217
 			const id = Number(draftId)
1104 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 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 1246
 			try {
1115 1247
 				const detail = await getDraftDetail(id, { showError: false })
1116 1248
 				if (!detail || detail.id == null) return null
1117 1249
 				const cardMsg = buildDraftCardMessage(detail, this.nextLocalMsgKey())
1250
+				cardMsg.voided = false
1118 1251
 
1119 1252
 				if (options.afterRelatedText) {
1120 1253
 					let insertAt = -1
@@ -1138,7 +1271,6 @@ export default {
1138 1271
 							}
1139 1272
 						}
1140 1273
 					}
1141
-					// 只插到「提到该草稿 id」的消息后面,找不到则不误沉底
1142 1274
 					if (insertAt < 0) return null
1143 1275
 					this.messages.splice(insertAt, 0, cardMsg)
1144 1276
 				} else {
@@ -1267,18 +1399,14 @@ export default {
1267 1399
 			}
1268 1400
 		},
1269 1401
 		/**
1270
-		 * 确认方案后在对话框追加二维码卡片(同 draft 只保留一张
1402
+		 * 确认方案后在对话框追加二维码卡片(同 draft 已有则不再追加
1271 1403
 		 */
1272 1404
 		appendQrCardMessage(batch) {
1273 1405
 			if (!batch) return
1274 1406
 			const draftId = batch.draftId
1275 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 1411
 			this.messages.push({
1284 1412
 				type: 'qr',
@@ -1331,13 +1459,16 @@ export default {
1331 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 1465
 		async showOrderConfirmFromChat(cardMeta) {
1337 1466
 			const payload = parseChatCardPayload(cardMeta && cardMeta.cardPayload)
1338 1467
 			const draftId = (cardMeta && cardMeta.relatedDraftId != null)
1339 1468
 				? cardMeta.relatedDraftId
1340 1469
 				: (payload && (payload.draft_id != null ? payload.draft_id : payload.draftId))
1470
+			if (this.isOrderConfirmHandled(draftId)) return
1471
+
1341 1472
 			let draft = null
1342 1473
 			if (this.activeDraft && String(this.activeDraft.draftId) === String(draftId)) {
1343 1474
 				draft = this.activeDraft
@@ -1372,30 +1503,6 @@ export default {
1372 1503
 			let qrToken = (payload && (payload.qr_token || payload.qrToken))
1373 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 1507
 			const scene = qrToken || orderId || ''
1401 1508
 			this.qrBatch = this.buildQrBatch(draft, {
@@ -1413,16 +1520,16 @@ export default {
1413 1520
 				if (fromDetail && fromDetail.qrCodeUrl) {
1414 1521
 					qrCodeUrl = fromDetail.qrCodeUrl
1415 1522
 					this.$set(this.qrBatch, 'qrCodeUrl', fromDetail.qrCodeUrl)
1416
-					this.$set(this.confirmInfo, 'qrCodeUrl', fromDetail.qrCodeUrl)
1417 1523
 				}
1418 1524
 				if (fromDetail && fromDetail.qrToken) {
1419 1525
 					qrToken = fromDetail.qrToken
1420 1526
 					this.$set(this.qrBatch, 'qrToken', fromDetail.qrToken)
1421 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 1533
 			if (draft && draftId != null) {
1427 1534
 				this.onDraftUpdated(Object.assign({}, draft, {
1428 1535
 					status: 'confirmed',
@@ -1430,12 +1537,18 @@ export default {
1430 1537
 					qrToken,
1431 1538
 					qrCodeUrl,
1432 1539
 					scene: qrToken || orderId || ''
1433
-				}), { sinkToBottom: true })
1540
+				}), { sinkToBottom: false })
1434 1541
 			}
1435 1542
 
1436
-			this.appendQrCardMessage(this.qrBatch)
1437 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 1553
 		async confirmPlan(payload) {
1441 1554
 			const scheme = (payload && payload.scheme) || null
@@ -1510,6 +1623,7 @@ export default {
1510 1623
 				msgKey: this.nextLocalMsgKey(),
1511 1624
 				content: '方案已确认。请将下方二维码分享给工人扫码登记。'
1512 1625
 			})
1626
+			this.markOrderConfirmHandled(draftId)
1513 1627
 			this.appendQrCardMessage(this.qrBatch)
1514 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 16
  * 从 SSE 事件 JSON 提取卡片元数据(camelCase / snake_case 兼容)
17 17
  * - content_type=card_draft|card_order|card_cost
18
- * - 或 next_action=show_order_qrcode(方案已确认弹框
18
+ * - 或 next_action=show_order_qrcode(对话写入确认文案 + 二维码卡
19 19
  */
20 20
 export function extractChatCardMeta(data) {
21 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 82
 export function shouldShowOrderQrcode(meta) {
83 83
 	if (!meta) return false
@@ -314,6 +314,7 @@ export function mapDraftDetailToCard(detail = {}) {
314 314
 			|| detail.selected_scheme_code
315 315
 			|| null,
316 316
 		status: detail.status,
317
+		orderId: detail.orderId || detail.order_id || '',
317 318
 		missingFields: detail.missingFields || [],
318 319
 		qrCodeUrl: pickDraftQrCodeUrl(detail),
319 320
 		qrToken: pickDraftQrToken(detail),
@@ -325,11 +326,30 @@ export function mapDraftDetailToCard(detail = {}) {
325 326
  * 构造聊天气泡:用工草稿卡片
326 327
  */
327 328
 export function buildDraftCardMessage(detail, msgKey) {
328
-	return {
329
+	const msg = {
329 330
 		type: 'draft',
330 331
 		role: 'ai',
331 332
 		msgKey,
332 333
 		draftId: detail.id,
334
+		voided: false,
333 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
 }