xsh_1997 21 godzin temu
rodzic
commit
6be8f96f94

+ 11 - 1
huimv-employment/app/api/notification.js

@@ -1,4 +1,4 @@
1
-import { get } from '@/utils/request.js'
1
+import { get, post } from '@/utils/request.js'
2 2
 
3 3
 /**
4 4
  * 我的消息与待办
@@ -28,3 +28,13 @@ export function listMyNotifications(params = {}, options = {}) {
28 28
 	if (params.size != null) data.size = params.size
29 29
 	return get('/notifications', data, options)
30 30
 }
31
+
32
+/**
33
+ * 标记消息已读
34
+ * POST /api/v1/mp/notifications/{id}/read
35
+ * @param {number|string} id 消息 ID
36
+ * @returns {Promise<{ item?: object, unreadCount?: number, unread_count?: number }>}
37
+ */
38
+export function markNotificationRead(id, options = {}) {
39
+	return post(`/notifications/${id}/read`, {}, options)
40
+}

+ 2 - 2
huimv-employment/app/common/config.js

@@ -1,6 +1,6 @@
1 1
 /** 小程序直连 API(体验版/开发版统一;有正式 HTTPS 域名后再改 env) */
2
-// const MP_API = 'http://192.168.1.6:8081/api/v1/mp'
3
-const MP_API = 'https://img.ifarmcloud.com/deploymentApi/api/v1/mp'
2
+const MP_API = 'http://192.168.1.6:8081/api/v1/mp'
3
+// const MP_API = 'https://img.ifarmcloud.com/deploymentApi/api/v1/mp'
4 4
 
5 5
 /** H5 开发走 devServer 代理,避免浏览器跨域 */
6 6
 const H5_DEV_PROXY_API = '/api/v1/mp'

+ 1 - 0
huimv-employment/app/packageA/components/chat/FeRecruitSheet.vue

@@ -479,6 +479,7 @@ export default {
479 479
 	font-size: 26rpx;
480 480
 	font-weight: 700;
481 481
 	color: $fe-foreground;
482
+	line-height: 1.4;
482 483
 	word-break: break-all;
483 484
 }
484 485
 

+ 38 - 40
huimv-employment/app/packageA/components/chat/FeSettlementSheet.vue

@@ -131,9 +131,8 @@
131 131
 <script>
132 132
 import { showToast } from '@/common/chat-data.js'
133 133
 import { getOrderSettlement, confirmPaySettlement, startSettlement } from '@/api/order.js'
134
-import { getDraftDetail } from '@/api/draft.js'
135 134
 import { mapSettlementSummary } from '@/utils/settlement.js'
136
-import { normalizeWorkerGroups, mapDraftDetailToCard } from '@/utils/draft.js'
135
+import { normalizeWorkerGroups } from '@/utils/draft.js'
137 136
 
138 137
 const EMPTY_VIEW = {
139 138
 	sourceTitle: '',
@@ -166,9 +165,7 @@ export default {
166 165
 			loading: false,
167 166
 			loadError: '',
168 167
 			paying: false,
169
-			view: Object.assign({}, EMPTY_VIEW),
170
-			draftGroups: [],
171
-			draftTitle: ''
168
+			view: Object.assign({}, EMPTY_VIEW)
172 169
 		}
173 170
 	},
174 171
 	computed: {
@@ -182,9 +179,6 @@ export default {
182 179
 		},
183 180
 		orderId() {
184 181
 			if (this.active) this.loadDetail()
185
-		},
186
-		draftId() {
187
-			if (this.active) this.loadDetail()
188 182
 		}
189 183
 	},
190 184
 	methods: {
@@ -192,18 +186,32 @@ export default {
192 186
 		onClose() {
193 187
 			this.$emit('close')
194 188
 		},
195
-		async loadDraftGroups() {
196
-			this.draftGroups = []
197
-			this.draftTitle = ''
198
-			if (this.draftId == null || this.draftId === '') return
189
+		hasWorkerGroups(raw) {
190
+			return normalizeWorkerGroups(raw || {}).length > 0
191
+		},
192
+		/** GET 详情才带 worker_groups;发起/支付响应若缺失则再拉一次 */
193
+		async fetchSettlementDetail() {
194
+			let raw = null
199 195
 			try {
200
-				const detail = await getDraftDetail(this.draftId, { showError: false })
201
-				const card = mapDraftDetailToCard(detail || {})
202
-				this.draftGroups = normalizeWorkerGroups(detail || {})
203
-				this.draftTitle = (card && card.title) || (detail && detail.title) || ''
196
+				raw = await getOrderSettlement(this.orderId, { showError: false })
204 197
 			} catch (e) {
205
-				this.draftGroups = []
198
+				if (this.readonly) throw e
199
+				try {
200
+					await startSettlement(this.orderId, { allowBeforeEndDate: false }, { showError: false })
201
+				} catch (e2) {
202
+					throw e
203
+				}
204
+				raw = await getOrderSettlement(this.orderId, { showError: false })
206 205
 			}
206
+			if (raw && !this.hasWorkerGroups(raw)) {
207
+				try {
208
+					const again = await getOrderSettlement(this.orderId, { showError: false })
209
+					if (again) raw = again
210
+				} catch (e3) {
211
+					// 保留已有 raw
212
+				}
213
+			}
214
+			return raw
207 215
 		},
208 216
 		async loadDetail() {
209 217
 			if (this.orderId == null || this.orderId === '') {
@@ -214,23 +222,8 @@ export default {
214 222
 			this.loading = true
215 223
 			this.loadError = ''
216 224
 			try {
217
-				await this.loadDraftGroups()
218
-				let raw = null
219
-				try {
220
-					raw = await getOrderSettlement(this.orderId, { showError: false })
221
-				} catch (e) {
222
-					// 只读查看不发起结算;否则尚无结算单时尝试发起(幂等)
223
-					if (this.readonly) throw e
224
-					try {
225
-						raw = await startSettlement(this.orderId, { allowBeforeEndDate: false }, { showError: false })
226
-					} catch (e2) {
227
-						throw e
228
-					}
229
-				}
230
-				this.view = mapSettlementSummary(raw || {}, {
231
-					workerGroups: this.draftGroups,
232
-					title: this.draftTitle
233
-				})
225
+				const raw = await this.fetchSettlementDetail()
226
+				this.view = mapSettlementSummary(raw || {})
234 227
 			} catch (e) {
235 228
 				this.view = Object.assign({}, EMPTY_VIEW)
236 229
 				this.loadError = (e && (e.msg || e.message)) || '结算单加载失败'
@@ -251,16 +244,21 @@ export default {
251 244
 			}
252 245
 			this.paying = true
253 246
 			try {
254
-				const raw = await confirmPaySettlement(
247
+				const payRaw = await confirmPaySettlement(
255 248
 					this.orderId,
256 249
 					{ paymentChannel: 'wechat' },
257 250
 					{ showError: true }
258 251
 				)
259
-				this.view = mapSettlementSummary(raw || {}, {
260
-					workerGroups: this.draftGroups,
261
-					title: this.draftTitle
262
-				})
263
-				showToast((raw && raw.message) || '支付完成')
252
+				let raw = payRaw
253
+				if (!this.hasWorkerGroups(raw)) {
254
+					try {
255
+						raw = await getOrderSettlement(this.orderId, { showError: false }) || payRaw
256
+					} catch (e) {
257
+						raw = payRaw
258
+					}
259
+				}
260
+				this.view = mapSettlementSummary(raw || {})
261
+				showToast((payRaw && payRaw.message) || '支付完成')
264 262
 				this.$emit('paid', {
265 263
 					orderId: this.orderId,
266 264
 					settlementId: this.view.settlementId,

+ 76 - 52
huimv-employment/app/packageA/components/chat/FeTaskPublishSheet.vue

@@ -25,37 +25,29 @@
25 25
 						<text class="tp-hero__desc">{{ statusDesc }}</text>
26 26
 					</view>
27 27
 
28
-					<!-- 任务信息:按工种多组 -->
28
+					<!-- 任务信息:多工种逗号拼接,单卡展示 -->
29 29
 					<view class="tp-section">
30 30
 						<text class="tp-section__title">任务信息</text>
31
-						<view
32
-							v-for="(g, gi) in groups"
33
-							:key="g.rowKey"
34
-							class="tp-card"
35
-						>
36
-							<text v-if="groups.length > 1" class="tp-card__badge">工种 {{ gi + 1 }} · {{ g.workType }}</text>
31
+						<view class="tp-card">
37 32
 							<view class="tp-grid">
38 33
 								<view class="tp-grid__cell">
39 34
 									<text class="tp-grid__label">岗位类型</text>
40
-									<text class="tp-grid__value">{{ g.workType || '—' }}</text>
35
+									<text class="tp-grid__value">{{ taskInfoSummary.jobTypeText }}</text>
41 36
 								</view>
42 37
 								<view class="tp-grid__cell">
43 38
 									<text class="tp-grid__label">用工人数</text>
44
-									<text class="tp-grid__value">{{ g.workerCountText }}</text>
39
+									<text class="tp-grid__value">{{ taskInfoSummary.workerCountText }}</text>
45 40
 								</view>
46 41
 								<view class="tp-grid__cell">
47 42
 									<text class="tp-grid__label">工作地点</text>
48
-									<text class="tp-grid__value">{{ g.workLocation || '—' }}</text>
43
+									<text class="tp-grid__value">{{ taskInfoSummary.locationText }}</text>
49 44
 								</view>
50 45
 								<view class="tp-grid__cell">
51
-									<text class="tp-grid__label">{{ g.durationLabel }}</text>
52
-									<text class="tp-grid__value">{{ g.durationText }}</text>
46
+									<text class="tp-grid__label">{{ taskInfoSummary.durationLabel }}</text>
47
+									<text class="tp-grid__value">{{ taskInfoSummary.durationText }}</text>
53 48
 								</view>
54 49
 							</view>
55 50
 						</view>
56
-						<view v-if="!groups.length" class="tp-card">
57
-							<text class="tp-empty__text">暂无工种信息</text>
58
-						</view>
59 51
 					</view>
60 52
 
61 53
 					<!-- 工作时间:按工种多组 -->
@@ -186,7 +178,6 @@
186 178
 </template>
187 179
 
188 180
 <script>
189
-import store from '@/common/store.js'
190 181
 import { showToast } from '@/common/chat-data.js'
191 182
 import { getDraftDetail, getDraftWorkflowProgress } from '@/api/draft.js'
192 183
 import { getOrderPublishTask, publishOrderTask } from '@/api/order.js'
@@ -198,7 +189,7 @@ import {
198 189
 	normalizeDateYmd,
199 190
 	mapDraftDetailToCard
200 191
 } from '@/utils/draft.js'
201
-import { isStaffingStep, isEnterpriseCertSkipped } from '@/utils/process-card.js'
192
+import { isTaskPublishStep } from '@/utils/process-card.js'
202 193
 import { mapRegistrationBatchDetail } from '@/utils/registration-batch.js'
203 194
 
204 195
 /** 规范化 HH:mm */
@@ -292,7 +283,8 @@ export default {
292 283
 			groups: [],
293 284
 			statusDesc: '请确认任务信息后发布',
294 285
 			orderIdInner: null,
295
-			registrationDone: false,
286
+			/** 流程接口任务发布步骤:pending | active | done */
287
+			taskStepStatus: 'pending',
296 288
 			/** 已登记 < 计划人数,发布时需 allow_understaffed */
297 289
 			understaffed: false,
298 290
 			registeredCount: 0,
@@ -303,11 +295,8 @@ export default {
303 295
 		heroTitle() {
304 296
 			return this.readonly ? '任务发布详情' : '确认发布任务'
305 297
 		},
306
-		enterpriseOk() {
307
-			return store.isEnterpriseRegistered() === true
308
-		},
309 298
 		canPublish() {
310
-			return this.enterpriseOk && this.registrationDone
299
+			return this.taskStepStatus === 'active'
311 300
 		},
312 301
 		scheduleComplete() {
313 302
 			const list = this.groups || []
@@ -320,11 +309,8 @@ export default {
320 309
 			return true
321 310
 		},
322 311
 		gateTip() {
323
-			if (!this.enterpriseOk && !this.registrationDone) {
324
-				return '请先完成企业认证与人员登记后,再确认发布'
325
-			}
326
-			if (!this.enterpriseOk) return '请先完成企业认证后,再确认发布'
327
-			if (!this.registrationDone) return '请先完成人员登记后,再确认发布'
312
+			if (this.taskStepStatus === 'done') return '任务已发布'
313
+			if (this.taskStepStatus !== 'active') return '当前步骤尚未开始,请等待流程推进到任务发布'
328 314
 			return ''
329 315
 		},
330 316
 		confirmDisabled() {
@@ -334,6 +320,51 @@ export default {
334 320
 			if (this.readonly) return '已发布'
335 321
 			if (this.publishing) return '发布中...'
336 322
 			return '确认发布'
323
+		},
324
+		/** 任务信息汇总:多工种逗号拼接,单卡展示 */
325
+		taskInfoSummary() {
326
+			const list = this.groups || []
327
+			if (!list.length) {
328
+				return {
329
+					jobTypeText: '—',
330
+					workerCountText: '—',
331
+					locationText: '—',
332
+					durationLabel: '工作天数',
333
+					durationText: '—'
334
+				}
335
+			}
336
+			const types = []
337
+			const counts = []
338
+			const locations = []
339
+			const durations = []
340
+			let hasMonth = false
341
+			let hasDay = false
342
+			for (let i = 0; i < list.length; i++) {
343
+				const g = list[i]
344
+				const type = String(g.workType || '').trim() || '待确认'
345
+				types.push(type)
346
+				if (g.workerCount != null && g.workerCount !== '') {
347
+					counts.push(`${type}*${g.workerCount}人`)
348
+				}
349
+				const loc = String(g.workLocation || '').trim()
350
+				if (loc && locations.indexOf(loc) < 0) locations.push(loc)
351
+				const unit = formatWorkDurationUnit(g.settlementMode)
352
+				if (unit === '月') hasMonth = true
353
+				else hasDay = true
354
+				if (g.workDays != null && g.workDays !== '') {
355
+					durations.push(`${type}*${g.workDays}${unit}`)
356
+				}
357
+			}
358
+			let durationLabel = '工作天数'
359
+			if (hasMonth && !hasDay) durationLabel = '工作月数'
360
+			else if (hasMonth && hasDay) durationLabel = '工作工期'
361
+			return {
362
+				jobTypeText: types.join(',') || '—',
363
+				workerCountText: counts.length ? counts.join(',') : '—',
364
+				locationText: locations.join(',') || '—',
365
+				durationLabel,
366
+				durationText: durations.length ? durations.join(',') : '—'
367
+			}
337 368
 		}
338 369
 	},
339 370
 	watch: {
@@ -382,7 +413,6 @@ export default {
382 413
 			const emptyDate = viewMode ? '—' : '选择日期'
383 414
 			const emptyTime = viewMode ? '—' : '选择时间'
384 415
 			const list = Array.isArray(groups) ? groups : []
385
-			const rawHasTime = (raw) => raw != null && raw !== '' && /\d{1,2}:\d{2}/.test(String(raw))
386 416
 			return list.map((g, idx) => {
387 417
 				const startParts = splitDateTimeParts(g.workStartDate)
388 418
 				const endParts = splitDateTimeParts(g.workEndDate)
@@ -390,9 +420,9 @@ export default {
390 420
 				const days = g.workDays
391 421
 				let startDate = normalizeDateYmd(startParts.date)
392 422
 				let endDate = normalizeDateYmd(endParts.date)
393
-				let startTime = startDate && rawHasTime(g.workStartDate) ? normalizeTimeHm(startParts.time) : ''
394
-				let endTime = endDate && rawHasTime(g.workEndDate) ? normalizeTimeHm(endParts.time) : ''
395
-				// 可编辑发布:默认开始=明天 08:00,结束=按工期、18:00
423
+				// 接口返回若含时间,统一赋成 HH:mm;编辑态无时间时用默认
424
+				let startTime = startParts.time ? normalizeTimeHm(startParts.time) : ''
425
+				let endTime = endParts.time ? normalizeTimeHm(endParts.time) : ''
396 426
 				if (!viewMode) {
397 427
 					if (!startDate) startDate = tomorrowYmd()
398 428
 					if (!startTime) startTime = '08:00'
@@ -549,14 +579,15 @@ export default {
549 579
 			}
550 580
 			return ''
551 581
 		},
552
-		resolveRegistrationDone(workflowRaw) {
582
+		resolveTaskStepStatus(workflowRaw) {
553 583
 			const detail = mapRegistrationBatchDetail(workflowRaw || {})
554 584
 			const steps = detail.steps || []
555
-			const regDone = steps.some((s) => isStaffingStep(s) && s.status === 'done')
556
-			if (regDone) return true
557
-			const progress = detail.progress || {}
558
-			if (progress.total > 0 && progress.current >= progress.total) return true
559
-			return false
585
+			const task = steps.find((s) => isTaskPublishStep(s))
586
+			if (!task) return 'pending'
587
+			const s = String(task.status || 'pending').toLowerCase()
588
+			if (s === 'done' || s === 'completed' || s === 'finish' || s === 'finished') return 'done'
589
+			if (s === 'active' || s === 'current' || s === 'in_progress' || s === 'processing') return 'active'
590
+			return 'pending'
560 591
 		},
561 592
 		buildStatusDesc(workflowRaw) {
562 593
 			const detail = mapRegistrationBatchDetail(workflowRaw || {})
@@ -567,10 +598,11 @@ export default {
567 598
 				if (total > 0 && current < total) {
568 599
 					return `已登记 ${current}/${total},未招满发布需确认缺编`
569 600
 				}
570
-				return '企业认证与人员登记已完成,可发布任务'
601
+				return '流程已到任务发布,可确认发布'
571 602
 			}
572
-			if (total > 0) return `已登记 ${current}/${total},完成认证与登记后方可发布`
573
-			return '请先完成企业认证与人员登记'
603
+			if (this.taskStepStatus === 'done') return '任务已发布'
604
+			if (total > 0) return `已登记 ${current}/${total},等待流程推进到任务发布`
605
+			return '等待流程推进到任务发布'
574 606
 		},
575 607
 		syncStaffingFromWorkflow(workflowRaw) {
576 608
 			const detail = mapRegistrationBatchDetail(workflowRaw || {})
@@ -583,11 +615,12 @@ export default {
583 615
 		/**
584 616
 		 * TaskPublishRequest:
585 617
 		 * { work_items: [{ id?, group_no?, job_type?, work_start_date, work_end_date }], allow_understaffed? }
618
+		 * work_start_date / work_end_date:YYYY-MM-DD HH:mm:ss(日期+时间拼成一个字段)
586 619
 		 */
587 620
 		buildPublishPayload(allowUnderstaffed = false) {
588 621
 			const workItems = (this.groups || []).map((g) => {
589
-				const start = normalizeDateYmd(g.startDate)
590
-				const end = normalizeDateYmd(g.endDate)
622
+				const start = joinDateTimePayload(g.startDate, g.startTime)
623
+				const end = joinDateTimePayload(g.endDate, g.endTime)
591 624
 				const item = {
592 625
 					work_start_date: start,
593 626
 					work_end_date: end
@@ -647,7 +680,7 @@ export default {
647 680
 				}
648 681
 
649 682
 				this.syncStaffingFromWorkflow(workflowRaw)
650
-				this.registrationDone = this.resolveRegistrationDone(workflowRaw)
683
+				this.taskStepStatus = this.resolveTaskStepStatus(workflowRaw)
651 684
 				this.statusDesc = this.buildStatusDesc(workflowRaw)
652 685
 			} catch (e) {
653 686
 				this.loadError = (e && (e.msg || e.message)) || '任务信息加载失败'
@@ -667,15 +700,6 @@ export default {
667 700
 				showToast(this.gateTip || '暂不可发布')
668 701
 				return
669 702
 			}
670
-			// 跳过认证不算「认证完」
671
-			if (!this.enterpriseOk) {
672
-				showToast('请先完成企业认证')
673
-				return
674
-			}
675
-			if (isEnterpriseCertSkipped() && !store.isEnterpriseRegistered()) {
676
-				showToast('请先完成企业认证')
677
-				return
678
-			}
679 703
 			const orderId = this.orderIdInner
680 704
 			if (orderId == null || orderId === '') {
681 705
 				showToast('缺少订单信息,暂无法发布')

+ 115 - 89
huimv-employment/app/packageA/components/home/EnterpriseHome.vue

@@ -892,8 +892,7 @@ import { getDraftDetail, getDraftCostComparison, getDraftWorkflowProgress } from
892 892
 import { buildWorkflowChatCardMessage, mapRegistrationBatchDetail } from '@/utils/registration-batch.js'
893 893
 import {
894 894
 	markEnterpriseCertSkipped,
895
-	clearEnterpriseCertSkipped,
896
-	syncStepProcessPanel
895
+	clearEnterpriseCertSkipped
897 896
 } from '@/utils/process-card.js'
898 897
 import {
899 898
 	getNewChatGreeting,
@@ -1130,7 +1129,7 @@ export default {
1130 1129
 			this.user = s.user
1131 1130
 			this.enterprise = s.enterprise
1132 1131
 			await this.fetchSessions({ autoLoad: true })
1133
-				this.refreshWorkflowProcessPanels()
1132
+				await this.refreshWorkflowProcessPanels()
1134 1133
 			})().finally(() => {
1135 1134
 				this._initHomePromise = null
1136 1135
 			})
@@ -2045,7 +2044,7 @@ export default {
2045 2044
 					silent: true
2046 2045
 				})
2047 2046
 			}
2048
-			this.refreshWorkflowProcessPanels()
2047
+			await this.refreshWorkflowProcessPanels()
2049 2048
 		},
2050 2049
 		/**
2051 2050
 		 * 汇总需要展示进度卡的草稿 id:已确认草稿卡 / card_order / 会话进度列表
@@ -2660,48 +2659,68 @@ export default {
2660 2659
 			this.$set(msg.workflow, 'stepIndex', stepIdx)
2661 2660
 		},
2662 2661
 		/**
2663
-		 * 从登记页返回:轻量同步企业认证面板。
2664
-		 * 接口 steps 为 active 时不得被本地 registered 盖成 done。
2662
+		 * 操作前拉 workflow-progress,用最新步骤 status 做门禁
2663
+		 * @returns {{ msgIdx: number, msg: object, step: object }|null}
2665 2664
 		 */
2666
-		refreshWorkflowProcessPanels() {
2667
-			const registered = store.isEnterpriseRegistered()
2668
-			if (registered) clearEnterpriseCertSkipped()
2665
+		async resolveWorkflowStepFromApi(msgIdx, stepIdx, expectedCardType) {
2666
+			const msg0 = this.messages[msgIdx]
2667
+			if (!msg0 || msg0.type !== 'workflow' || !msg0.workflow) return null
2668
+			const draftId = msg0.draftId != null ? msg0.draftId : msg0.workflow.draftId
2669
+			if (draftId != null && draftId !== '') {
2670
+				await this.refreshWorkflowCardByDraftId(draftId)
2671
+			}
2672
+			let nextIdx = msgIdx
2673
+			for (let i = this.messages.length - 1; i >= 0; i--) {
2674
+				const m = this.messages[i]
2675
+				if (!m || m.type !== 'workflow') continue
2676
+				const id = m.draftId != null ? m.draftId : (m.workflow && m.workflow.draftId)
2677
+				if (draftId != null && draftId !== '' && String(id) === String(draftId)) {
2678
+					nextIdx = i
2679
+					break
2680
+				}
2681
+			}
2682
+			const msg = this.messages[nextIdx]
2683
+			if (!msg || !msg.workflow || !msg.workflow.steps) return null
2684
+			let step = msg.workflow.steps[stepIdx]
2685
+			if (expectedCardType && (!step || !step.processPanel || step.processPanel.cardType !== expectedCardType)) {
2686
+				step = msg.workflow.steps.find((s) => s
2687
+					&& s.processPanel
2688
+					&& s.processPanel.cardType === expectedCardType) || null
2689
+			}
2690
+			if (!step) return null
2691
+			return { msgIdx: nextIdx, msg, step }
2692
+		},
2693
+		/**
2694
+		 * 从登记页返回:按流程接口重拉进度卡(不再用本地态改写 status)
2695
+		 */
2696
+		async refreshWorkflowProcessPanels() {
2697
+			if (store.isEnterpriseRegistered()) clearEnterpriseCertSkipped()
2698
+			const ids = []
2699
+			const seen = {}
2669 2700
 			for (let i = 0; i < this.messages.length; i++) {
2670 2701
 				const msg = this.messages[i]
2671
-				if (!msg || msg.type !== 'workflow' || !msg.workflow || !msg.workflow.steps) continue
2672
-				const steps = msg.workflow.steps
2673
-				for (let s = 0; s < steps.length; s++) {
2674
-					const step = steps[s]
2675
-					if (!step || !step.hasProcessPanel || !step.processPanel) continue
2676
-					if (step.processPanel.cardType !== 'enterprise_cert') continue
2677
-					let nextStatus = step.status || 'pending'
2678
-					// 仅 pending → done;active/done 保持卡片已有状态(来自 workflow-progress)
2679
-					if (registered && nextStatus === 'pending') {
2680
-						nextStatus = 'done'
2681
-						this.$set(step, 'status', 'done')
2682
-						this.$set(step, 'statusLabel', '已完成')
2683
-						this.$set(step, 'nodeIcon', '✓')
2684
-						this.$set(step, 'nodeClass', 'fe-stepper__node fe-stepper__node--done')
2685
-						this.$set(step, 'labelClass', 'fe-stepper__label fe-stepper__label--done')
2686
-						this.$set(step, 'rootClass', 'fe-workflow-step fe-workflow-step--process fe-workflow-step--done')
2687
-					}
2688
-					syncStepProcessPanel(step, nextStatus, {
2689
-						skipped: !registered && step.processPanel.processStatus === 'skipped'
2690
-					})
2691
-					this.$set(steps, s, step)
2692
-				}
2693
-				this.$set(this.messages, i, msg)
2702
+				if (!msg || msg.type !== 'workflow' || !msg.workflow) continue
2703
+				const id = msg.draftId != null ? msg.draftId : msg.workflow.draftId
2704
+				if (id == null || id === '' || seen[String(id)]) continue
2705
+				seen[String(id)] = true
2706
+				ids.push(id)
2707
+			}
2708
+			for (let i = 0; i < ids.length; i++) {
2709
+				await this.refreshWorkflowCardByDraftId(ids[i])
2694 2710
 			}
2695 2711
 		},
2696
-		goWorkflowCertByIndex(msgIdx, stepIdx) {
2697
-			const msg = this.messages[msgIdx]
2698
-			if (!msg || msg.type !== 'workflow') return
2699
-			const step = msg.workflow && msg.workflow.steps && msg.workflow.steps[stepIdx]
2700
-			if (!step || !step.processPanel || step.processPanel.cardType !== 'enterprise_cert') return
2712
+		async goWorkflowCertByIndex(msgIdx, stepIdx) {
2713
+			const resolved = await this.resolveWorkflowStepFromApi(msgIdx, stepIdx, 'enterprise_cert')
2714
+			if (!resolved) return
2715
+			const { msg, step } = resolved
2716
+			if (!step.processPanel || step.processPanel.cardType !== 'enterprise_cert') return
2717
+			if (this.isWorkflowActionBlocked(step)) {
2718
+				showToast((step.processPanel && step.processPanel.lockHint) || '当前步骤尚未开始')
2719
+				return
2720
+			}
2701 2721
 			const draftId = msg.draftId != null
2702 2722
 				? msg.draftId
2703 2723
 				: (msg.workflow && msg.workflow.draftId)
2704
-			// 以流程步骤为准:active=未完成本单认证需提交;done=仅查看
2705 2724
 			const isDone = step.status === 'done'
2706 2725
 				|| (step.processPanel && step.processPanel.processStatus === 'done')
2707 2726
 			const readonly = !!isDone
@@ -2722,29 +2741,25 @@ export default {
2722 2741
 				// 忽略:仍按本地已登记状态刷新进度卡
2723 2742
 			}
2724 2743
 			if (store.isEnterpriseRegistered()) clearEnterpriseCertSkipped()
2725
-			// 重建进度卡步骤条(门禁/连线/面板一并更新)
2726 2744
 			if (draftId != null && draftId !== '') {
2727 2745
 				await this.refreshWorkflowCardByDraftId(draftId)
2728 2746
 			} else {
2729
-				this.refreshWorkflowProcessPanels()
2747
+				await this.refreshWorkflowProcessPanels()
2730 2748
 			}
2731 2749
 			this.ensureChatBottom()
2732 2750
 		},
2751
+		/** 门禁:登记/招聘不看 pending;其余 done 可查看、active 可操作、pending 禁用 */
2733 2752
 		isWorkflowActionBlocked(step) {
2734 2753
 			if (!step || !step.processPanel) return true
2735 2754
 			const panel = step.processPanel
2736
-			// 已完成:仍可进弹层查看
2737
-			if (step.status === 'done' || panel.processStatus === 'done' || panel.viewOnly) return false
2738
-			// 登记/招聘:方案确认后可点,不因接口仍 pending 拦截
2739
-			const isStaffingPanel = panel.cardType === 'registration_qr'
2740
-				|| panel.cardType === 'worker_recruitment'
2741
-			if (isStaffingPanel) {
2742
-				return panel.actionDisabled === true || panel.actionLocked === true
2755
+			// 人员登记 / 临时工招聘:始终可进
2756
+			if (panel.cardType === 'registration_qr' || panel.cardType === 'worker_recruitment') {
2757
+				return false
2743 2758
 			}
2744
-			if (panel.actionDisabled === true || panel.actionLocked === true) return true
2745
-			if (panel.processStatus === 'pending' || step.status === 'pending') return true
2746
-			if (panel.processStatus !== 'current' && step.status !== 'active') return true
2747
-			return false
2759
+			if (step.status === 'done' || panel.processStatus === 'done' || panel.viewOnly) return false
2760
+			if (step.status === 'active' || panel.processStatus === 'current') return false
2761
+			if (panel.processStatus === 'skipped') return false
2762
+			return true
2748 2763
 		},
2749 2764
 		isWorkflowStepViewOnly(step) {
2750 2765
 			if (!step || !step.processPanel) return false
@@ -2752,15 +2767,17 @@ export default {
2752 2767
 				|| step.processPanel.processStatus === 'done'
2753 2768
 				|| step.processPanel.viewOnly === true
2754 2769
 		},
2755
-		goWorkflowRecruitByIndex(msgIdx, stepIdx) {
2756
-			const msg = this.messages[msgIdx]
2757
-			if (!msg || msg.type !== 'workflow' || !msg.workflow) return
2758
-			const step = msg.workflow.steps && msg.workflow.steps[stepIdx]
2759
-			if (!step || !step.processPanel || step.processPanel.cardType !== 'worker_recruitment') return
2760
-			if (this.isWorkflowActionBlocked(step)) return
2770
+		async goWorkflowRecruitByIndex(msgIdx, stepIdx) {
2771
+			const resolved = await this.resolveWorkflowStepFromApi(msgIdx, stepIdx, 'worker_recruitment')
2772
+			if (!resolved) return
2773
+			const { msg, step } = resolved
2774
+			if (!step.processPanel || step.processPanel.cardType !== 'worker_recruitment') return
2775
+			if (this.isWorkflowActionBlocked(step)) {
2776
+				showToast((step.processPanel && step.processPanel.lockHint) || '当前步骤尚未开始')
2777
+				return
2778
+			}
2761 2779
 			const draftId = msg.draftId != null ? msg.draftId : msg.workflow.draftId
2762 2780
 			const panel = step.processPanel
2763
-			// 查看招聘:与人员登记进度弹层同一套内容,仅标题改为「临时工招聘」
2764 2781
 			const isViewRecruit = this.isWorkflowStepViewOnly(step)
2765 2782
 				|| panel.viewOnly === true
2766 2783
 				|| panel.primaryLabel === '查看招聘'
@@ -2786,12 +2803,15 @@ export default {
2786 2803
 			}
2787 2804
 			this.ensureChatBottom()
2788 2805
 		},
2789
-		goWorkflowTaskPublishByIndex(msgIdx, stepIdx) {
2790
-			const msg = this.messages[msgIdx]
2791
-			if (!msg || msg.type !== 'workflow' || !msg.workflow) return
2792
-			const step = msg.workflow.steps && msg.workflow.steps[stepIdx]
2793
-			if (!step || !step.processPanel || step.processPanel.cardType !== 'task_publish') return
2794
-			if (this.isWorkflowActionBlocked(step)) return
2806
+		async goWorkflowTaskPublishByIndex(msgIdx, stepIdx) {
2807
+			const resolved = await this.resolveWorkflowStepFromApi(msgIdx, stepIdx, 'task_publish')
2808
+			if (!resolved) return
2809
+			const { msg, step } = resolved
2810
+			if (!step.processPanel || step.processPanel.cardType !== 'task_publish') return
2811
+			if (this.isWorkflowActionBlocked(step)) {
2812
+				showToast((step.processPanel && step.processPanel.lockHint) || '当前步骤尚未开始')
2813
+				return
2814
+			}
2795 2815
 			const draftId = msg.draftId != null ? msg.draftId : msg.workflow.draftId
2796 2816
 			const detail = msg.workflow.detail || {}
2797 2817
 			const orderId = detail.orderId || detail.order_id || null
@@ -2814,12 +2834,15 @@ export default {
2814 2834
 			}
2815 2835
 			this.ensureChatBottom()
2816 2836
 		},
2817
-		goWorkflowSettlementByIndex(msgIdx, stepIdx) {
2818
-			const msg = this.messages[msgIdx]
2819
-			if (!msg || msg.type !== 'workflow' || !msg.workflow) return
2820
-			const step = msg.workflow.steps && msg.workflow.steps[stepIdx]
2821
-			if (!step || !step.processPanel || step.processPanel.cardType !== 'settlement') return
2822
-			if (this.isWorkflowActionBlocked(step)) return
2837
+		async goWorkflowSettlementByIndex(msgIdx, stepIdx) {
2838
+			const resolved = await this.resolveWorkflowStepFromApi(msgIdx, stepIdx, 'settlement')
2839
+			if (!resolved) return
2840
+			const { msg, step } = resolved
2841
+			if (!step.processPanel || step.processPanel.cardType !== 'settlement') return
2842
+			if (this.isWorkflowActionBlocked(step)) {
2843
+				showToast((step.processPanel && step.processPanel.lockHint) || '当前步骤尚未开始')
2844
+				return
2845
+			}
2823 2846
 			const detail = msg.workflow.detail || {}
2824 2847
 			const orderId = detail.orderId || detail.order_id || ''
2825 2848
 			const draftId = msg.draftId != null ? msg.draftId : msg.workflow.draftId
@@ -2830,12 +2853,15 @@ export default {
2830 2853
 			}
2831 2854
 			this.openProgressDetail(draftId)
2832 2855
 		},
2833
-		goWorkflowInvoiceByIndex(msgIdx, stepIdx) {
2834
-			const msg = this.messages[msgIdx]
2835
-			if (!msg || msg.type !== 'workflow' || !msg.workflow) return
2836
-			const step = msg.workflow.steps && msg.workflow.steps[stepIdx]
2837
-			if (!step || !step.processPanel || step.processPanel.cardType !== 'invoice_archive') return
2838
-			if (this.isWorkflowActionBlocked(step)) return
2856
+		async goWorkflowInvoiceByIndex(msgIdx, stepIdx) {
2857
+			const resolved = await this.resolveWorkflowStepFromApi(msgIdx, stepIdx, 'invoice_archive')
2858
+			if (!resolved) return
2859
+			const { msg, step } = resolved
2860
+			if (!step.processPanel || step.processPanel.cardType !== 'invoice_archive') return
2861
+			if (this.isWorkflowActionBlocked(step)) {
2862
+				showToast((step.processPanel && step.processPanel.lockHint) || '当前步骤尚未开始')
2863
+				return
2864
+			}
2839 2865
 			const detail = msg.workflow.detail || {}
2840 2866
 			const draftId = msg.draftId != null ? msg.draftId : msg.workflow.draftId
2841 2867
 			const orderId = detail.orderId || detail.order_id || null
@@ -2994,12 +3020,12 @@ export default {
2994 3020
 		},
2995 3021
 		/** 人员登记:查看/分享(数据来自 workflow-progress) */
2996 3022
 		async openWorkflowQrByIndex(msgIdx, stepIdx) {
2997
-			const msg = this.messages[msgIdx]
2998
-			if (!msg || msg.type !== 'workflow' || !msg.workflow) return
2999
-			const step = msg.workflow.steps && msg.workflow.steps[stepIdx]
3000
-			if (!step || !step.processPanel || step.processPanel.cardType !== 'registration_qr') return
3001
-			if (step.processPanel.actionDisabled) {
3002
-				showToast(step.processPanel.lockHint || '当前步骤尚未开始')
3023
+			const resolved = await this.resolveWorkflowStepFromApi(msgIdx, stepIdx, 'registration_qr')
3024
+			if (!resolved) return
3025
+			const { msg, step } = resolved
3026
+			if (!step.processPanel || step.processPanel.cardType !== 'registration_qr') return
3027
+			if (this.isWorkflowActionBlocked(step)) {
3028
+				showToast((step.processPanel && step.processPanel.lockHint) || '当前步骤尚未开始')
3003 3029
 				return
3004 3030
 			}
3005 3031
 			const draftId = msg.draftId != null ? msg.draftId : msg.workflow.draftId
@@ -3046,13 +3072,13 @@ export default {
3046 3072
 			}
3047 3073
 		},
3048 3074
 		/** 人员登记:查看进度(FeProgressSheet 走 workflow-progress) */
3049
-		openWorkflowQrProgressByIndex(msgIdx, stepIdx) {
3050
-			const msg = this.messages[msgIdx]
3051
-			if (!msg || msg.type !== 'workflow') return
3052
-			const step = msg.workflow && msg.workflow.steps && msg.workflow.steps[stepIdx]
3053
-			if (!step || !step.processPanel || step.processPanel.cardType !== 'registration_qr') return
3054
-			if (step.processPanel.actionDisabled) {
3055
-				showToast(step.processPanel.lockHint || '当前步骤尚未开始')
3075
+		async openWorkflowQrProgressByIndex(msgIdx, stepIdx) {
3076
+			const resolved = await this.resolveWorkflowStepFromApi(msgIdx, stepIdx, 'registration_qr')
3077
+			if (!resolved) return
3078
+			const { msg, step } = resolved
3079
+			if (!step.processPanel || step.processPanel.cardType !== 'registration_qr') return
3080
+			if (this.isWorkflowActionBlocked(step)) {
3081
+				showToast((step.processPanel && step.processPanel.lockHint) || '当前步骤尚未开始')
3056 3082
 				return
3057 3083
 			}
3058 3084
 			this.openProgressDetail(msg.draftId || (msg.workflow && msg.workflow.draftId))

+ 21 - 3
huimv-employment/app/packageA/enterprise/messages.vue

@@ -50,8 +50,8 @@ import FeBottomNav from '@/packageA/components/FeBottomNav.vue'
50 50
 import store from '@/common/store.js'
51 51
 import { getToken } from '@/utils/request.js'
52 52
 import { showToast } from '@/common/chat-data.js'
53
-import { listMyNotifications } from '@/api/notification.js'
54
-import { mapNotificationPage } from '@/utils/notification.js'
53
+import { listMyNotifications, markNotificationRead } from '@/api/notification.js'
54
+import { mapNotificationPage, mapNotificationItem, applyNotificationLocalRead } from '@/utils/notification.js'
55 55
 
56 56
 const PAGE_SIZE = 20
57 57
 
@@ -129,9 +129,27 @@ export default {
129 129
 				this.loadingMore = false
130 130
 			}
131 131
 		},
132
-		onItemByIndex(idx) {
132
+		async markReadIfNeeded(idx) {
133
+			const item = this.inbox[idx]
134
+			if (!item || item.readFlag === true) return
135
+			if (item.id == null || item.id === '') return
136
+			try {
137
+				const raw = await markNotificationRead(item.id, { showError: false })
138
+				const unread = raw && (raw.unreadCount != null ? raw.unreadCount : raw.unread_count)
139
+				if (unread != null) this.unreadCount = Number(unread)
140
+				const apiItem = raw && raw.item
141
+				const next = apiItem
142
+					? mapNotificationItem(apiItem, idx)
143
+					: applyNotificationLocalRead(item)
144
+				this.$set(this.inbox, idx, next)
145
+			} catch (e) {
146
+				// 已读失败不阻断跳转
147
+			}
148
+		},
149
+		async onItemByIndex(idx) {
133 150
 			const item = this.inbox[idx]
134 151
 			if (!item) return
152
+			await this.markReadIfNeeded(idx)
135 153
 			const url = item.actionUrl
136 154
 			if (!url || typeof url !== 'string' || url.indexOf('/packageA/') !== 0) {
137 155
 				showToast(item.title || '消息详情')

+ 0 - 10
huimv-employment/app/packageA/enterprise/mine.vue

@@ -12,9 +12,6 @@
12 12
 						<text class="guest-card__mobile">{{ mobileMask || '未绑定企业' }}</text>
13 13
 						<view class="guest-card__badge">未认证</view>
14 14
 						<text class="guest-card__tip">完成企业认证后,可使用全部企业能力</text>
15
-						<view class="fe-btn fe-btn--primary fe-btn--lg fe-btn--full guest-card__cta" @tap="goRegister">
16
-							去企业认证
17
-						</view>
18 15
 					</view>
19 16
 
20 17
 					<view class="guest-logout-panel">
@@ -79,9 +76,6 @@ export default {
79 76
 			const s = store.getState()
80 77
 			this.mobileMask = (s.user && s.user.mobile) || ''
81 78
 		},
82
-		goRegister() {
83
-			uni.navigateTo({ url: '/packageA/enterprise/register' })
84
-		},
85 79
 		logout() {
86 80
 			store.logout()
87 81
 			uni.reLaunch({ url: '/packageA/auth/login' })
@@ -170,10 +164,6 @@ export default {
170 164
 	line-height: 1.5;
171 165
 }
172 166
 
173
-.guest-card__cta {
174
-	margin-top: 36rpx;
175
-}
176
-
177 167
 .guest-logout-panel {
178 168
 	background: #ffffff;
179 169
 	border-radius: 20rpx;

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

@@ -544,15 +544,21 @@ function pickDateTimeRaw(g, camel, snake) {
544 544
 	return v == null ? '' : String(v)
545 545
 }
546 546
 
547
-/** 解析接口日期时间为 date(YYYY-MM-DD) + time(HH:mm) */
547
+/** 解析接口日期时间为 date(YYYY-MM-DD) + time(HH:mm,不含秒) */
548 548
 export function splitDateTimeParts(raw) {
549 549
 	if (raw == null || raw === '') {
550 550
 		return { date: '', time: '' }
551 551
 	}
552 552
 	let s = String(raw).trim().replace('T', ' ').replace(/\//g, '-')
553
-	const m = s.match(/^(\d{4}-\d{2}-\d{2})(?:\s+(\d{2}:\d{2})(?::\d{2})?)?/)
553
+	// 去掉时区尾巴:+08:00 / Z
554
+	s = s.replace(/([+-]\d{2}:?\d{2}|Z)$/i, '').trim()
555
+	const m = s.match(/^(\d{4}-\d{2}-\d{2})(?:[ T]+(\d{1,2}):(\d{2})(?::\d{2})?(?:\.\d+)?)?/)
554 556
 	if (!m) return { date: '', time: '' }
555
-	return { date: m[1], time: m[2] || '08:00' }
557
+	const date = m[1]
558
+	if (!m[2]) return { date, time: '' }
559
+	const hh = String(Math.min(23, Math.max(0, Number(m[2])))).padStart(2, '0')
560
+	const mm = String(m[3]).padStart(2, '0')
561
+	return { date, time: `${hh}:${mm}` }
556 562
 }
557 563
 
558 564
 /** 统一为 YYYY-MM-DD(去掉 /) */

+ 8 - 0
huimv-employment/app/utils/notification.js

@@ -60,6 +60,14 @@ function buildItemClass(readFlag, handledFlag, notifyType) {
60 60
 	return parts.join(' ')
61 61
 }
62 62
 
63
+/** 本地标记已读后刷新卡片 class */
64
+export function applyNotificationLocalRead(item) {
65
+	if (!item) return item
66
+	const next = Object.assign({}, item, { readFlag: true })
67
+	next.itemClass = buildItemClass(true, next.handledFlag, next.notifyType)
68
+	return next
69
+}
70
+
63 71
 function formatRelativeTime(value) {
64 72
 	if (value == null || value === '') return ''
65 73
 	const s = String(value).replace('T', ' ')

+ 37 - 54
huimv-employment/app/utils/process-card.js

@@ -79,10 +79,10 @@ function buildSecondaryBtnClass(disabled) {
79 79
 }
80 80
 
81 81
 /**
82
- * 串行步骤入口:锁定或待开始不可点;「当前 / 已完成」可进弹层(完成态弹层内再禁操作)
82
+ * 串行/引导步骤入口:以流程接口 status 为准
83
+ * pending → 禁用;active(current) / done / skipped → 可进(done 只读)
83 84
  */
84
-function isSequentialActionDisabled(processStatus, actionLocked) {
85
-	if (actionLocked) return true
85
+function isActionDisabledByApiStatus(processStatus) {
86 86
 	if (processStatus === PROCESS_STATUS.PENDING) return true
87 87
 	return false
88 88
 }
@@ -180,7 +180,7 @@ export function buildEnterpriseCertProcessPanel(stepStatus, options = {}) {
180 180
 	const processStatus = mapStepStatusToProcessStatus(stepStatus, options)
181 181
 	const statusMeta = buildProcessStatusMeta(processStatus)
182 182
 	const isDone = processStatus === PROCESS_STATUS.DONE
183
-	// 可与人员登记并行:未完成即可认证;已完成只读查看;已跳过仍可回来认证
183
+	const actionDisabled = isActionDisabledByApiStatus(processStatus)
184 184
 	const showActions = true
185 185
 	return {
186 186
 		cardType: 'enterprise_cert',
@@ -193,14 +193,15 @@ export function buildEnterpriseCertProcessPanel(stepStatus, options = {}) {
193 193
 			{ msgKey: 'b3', text: '法人身份验证' }
194 194
 		],
195 195
 		showActions,
196
-		showSkip: !isDone && processStatus !== PROCESS_STATUS.SKIPPED,
196
+		showSkip: !isDone && processStatus !== PROCESS_STATUS.SKIPPED && !actionDisabled,
197 197
 		primaryLabel: isDone ? '查看认证' : '去认证',
198 198
 		secondaryLabel: '跳过',
199 199
 		readonly: isDone,
200
-		actionLocked: false,
201
-		actionDisabled: false,
202
-		primaryBtnClass: buildPrimaryBtnClass(false),
203
-		secondaryBtnClass: buildSecondaryBtnClass(false),
200
+		actionLocked: actionDisabled,
201
+		actionDisabled,
202
+		lockHint: '当前步骤尚未开始',
203
+		primaryBtnClass: buildPrimaryBtnClass(actionDisabled),
204
+		secondaryBtnClass: buildSecondaryBtnClass(actionDisabled),
204 205
 		...statusMeta
205 206
 	}
206 207
 }
@@ -208,13 +209,11 @@ export function buildEnterpriseCertProcessPanel(stepStatus, options = {}) {
208 209
 /**
209 210
  * 任务发布步骤面板
210 211
  * @param {string} stepStatus done|active|pending
211
- * @param {{ actionLocked?: boolean }} [options]
212 212
  */
213
-export function buildTaskPublishProcessPanel(stepStatus, options = {}) {
213
+export function buildTaskPublishProcessPanel(stepStatus) {
214 214
 	const processStatus = mapStepStatusToProcessStatus(stepStatus)
215 215
 	const statusMeta = buildProcessStatusMeta(processStatus)
216
-	const actionLocked = options.actionLocked === true
217
-	const actionDisabled = isSequentialActionDisabled(processStatus, actionLocked)
216
+	const actionDisabled = isActionDisabledByApiStatus(processStatus)
218 217
 	const viewOnly = processStatus === PROCESS_STATUS.DONE
219 218
 	return {
220 219
 		cardType: 'task_publish',
@@ -227,10 +226,10 @@ export function buildTaskPublishProcessPanel(stepStatus, options = {}) {
227 226
 			{ msgKey: 'b3', text: '开启考勤打卡' }
228 227
 		],
229 228
 		showActions: true,
230
-		actionLocked,
229
+		actionLocked: actionDisabled,
231 230
 		actionDisabled,
232 231
 		viewOnly,
233
-		lockHint: '请先完成企业认证与人员登记',
232
+		lockHint: '当前步骤尚未开始',
234 233
 		primaryLabel: viewOnly ? '查看发布' : '去发布',
235 234
 		primaryIcon: '✈',
236 235
 		primaryBtnClass: buildPrimaryBtnClass(actionDisabled),
@@ -241,13 +240,11 @@ export function buildTaskPublishProcessPanel(stepStatus, options = {}) {
241 240
 /**
242 241
  * 结算打款步骤面板
243 242
  * @param {string} stepStatus done|active|pending
244
- * @param {{ actionLocked?: boolean }} [options]
245 243
  */
246
-export function buildSettlementProcessPanel(stepStatus, options = {}) {
244
+export function buildSettlementProcessPanel(stepStatus) {
247 245
 	const processStatus = mapStepStatusToProcessStatus(stepStatus)
248 246
 	const statusMeta = buildProcessStatusMeta(processStatus)
249
-	const actionLocked = options.actionLocked === true
250
-	const actionDisabled = isSequentialActionDisabled(processStatus, actionLocked)
247
+	const actionDisabled = isActionDisabledByApiStatus(processStatus)
251 248
 	const viewOnly = processStatus === PROCESS_STATUS.DONE
252 249
 	return {
253 250
 		cardType: 'settlement',
@@ -260,10 +257,10 @@ export function buildSettlementProcessPanel(stepStatus, options = {}) {
260 257
 			{ msgKey: 'b3', text: '一键打款到工人账户' }
261 258
 		],
262 259
 		showActions: true,
263
-		actionLocked,
260
+		actionLocked: actionDisabled,
264 261
 		actionDisabled,
265 262
 		viewOnly,
266
-		lockHint: '请先完成任务发布',
263
+		lockHint: '当前步骤尚未开始',
267 264
 		primaryLabel: '查看结算单',
268 265
 		primaryIcon: '◎',
269 266
 		primaryBtnClass: buildPrimaryBtnClass(actionDisabled),
@@ -274,13 +271,11 @@ export function buildSettlementProcessPanel(stepStatus, options = {}) {
274 271
 /**
275 272
  * 开票归档步骤面板
276 273
  * @param {string} stepStatus done|active|pending
277
- * @param {{ actionLocked?: boolean }} [options]
278 274
  */
279
-export function buildInvoiceArchiveProcessPanel(stepStatus, options = {}) {
275
+export function buildInvoiceArchiveProcessPanel(stepStatus) {
280 276
 	const processStatus = mapStepStatusToProcessStatus(stepStatus)
281 277
 	const statusMeta = buildProcessStatusMeta(processStatus)
282
-	const actionLocked = options.actionLocked === true
283
-	const actionDisabled = isSequentialActionDisabled(processStatus, actionLocked)
278
+	const actionDisabled = isActionDisabledByApiStatus(processStatus)
284 279
 	const viewOnly = processStatus === PROCESS_STATUS.DONE
285 280
 	return {
286 281
 		cardType: 'invoice_archive',
@@ -293,10 +288,10 @@ export function buildInvoiceArchiveProcessPanel(stepStatus, options = {}) {
293 288
 			{ msgKey: 'b3', text: '导出合规证据链' }
294 289
 		],
295 290
 		showActions: true,
296
-		actionLocked,
291
+		actionLocked: actionDisabled,
297 292
 		actionDisabled,
298 293
 		viewOnly,
299
-		lockHint: '请先完成结算打款',
294
+		lockHint: '当前步骤尚未开始',
300 295
 		primaryLabel: viewOnly ? '查看开票' : '去开票',
301 296
 		primaryIcon: '📄',
302 297
 		primaryBtnClass: buildPrimaryBtnClass(actionDisabled),
@@ -315,8 +310,7 @@ export function buildInvoiceArchiveProcessPanel(stepStatus, options = {}) {
315 310
  *   qrToken?: string,
316 311
  *   registered?: number,
317 312
  *   total?: number,
318
- *   highlight?: string,
319
- *   actionLocked?: boolean
313
+ *   highlight?: string
320 314
  * }} [qr]
321 315
  */
322 316
 export function buildRegistrationQrProcessPanel(stepStatus, qr = {}) {
@@ -326,9 +320,8 @@ export function buildRegistrationQrProcessPanel(stepStatus, qr = {}) {
326 320
 	const total = qr.total != null ? Number(qr.total) : 0
327 321
 	let progressText = qr.highlight || ''
328 322
 	if (!progressText && total > 0) progressText = `已登记 ${registered}/${total}`
329
-	// 以门禁 actionLocked 为准:方案确认后即使接口 pending 也可操作
330
-	const actionLocked = qr.actionLocked === true
331
-	const actionDisabled = actionLocked
323
+	// 登记/招聘:不因接口 pending 禁用,随时可操作
324
+	const actionDisabled = false
332 325
 	return {
333 326
 		cardType: 'registration_qr',
334 327
 		title: '人员登记',
@@ -342,9 +335,9 @@ export function buildRegistrationQrProcessPanel(stepStatus, qr = {}) {
342 335
 		total,
343 336
 		progressText,
344 337
 		showActions: true,
345
-		actionLocked,
338
+		actionLocked: false,
346 339
 		actionDisabled,
347
-		lockHint: '请先完成方案确认',
340
+		lockHint: '当前步骤尚未开始',
348 341
 		primaryLabel: '查看 / 分享',
349 342
 		secondaryLabel: '查看进度',
350 343
 		primaryBtnClass: buildPrimaryBtnClass(actionDisabled),
@@ -359,8 +352,7 @@ export function buildRegistrationQrProcessPanel(stepStatus, qr = {}) {
359 352
  * @param {{
360 353
  *   registered?: number,
361 354
  *   total?: number,
362
- *   highlight?: string,
363
- *   actionLocked?: boolean
355
+ *   highlight?: string
364 356
  * }} [options]
365 357
  */
366 358
 export function buildWorkerRecruitmentProcessPanel(stepStatus, options = {}) {
@@ -369,9 +361,8 @@ export function buildWorkerRecruitmentProcessPanel(stepStatus, options = {}) {
369 361
 	const registered = options.registered != null ? Number(options.registered) : 0
370 362
 	const total = options.total != null ? Number(options.total) : 0
371 363
 	const progressText = `已登记 ${registered}/${total || 0}`
372
-	// 以门禁 actionLocked 为准:方案确认后即使接口 pending 也可操作
373
-	const actionLocked = options.actionLocked === true
374
-	const actionDisabled = actionLocked
364
+	// 登记/招聘:不因接口 pending 禁用;done 仍可点「查看招聘」
365
+	const actionDisabled = false
375 366
 	const viewOnly = processStatus === PROCESS_STATUS.DONE
376 367
 	return {
377 368
 		cardType: 'worker_recruitment',
@@ -387,10 +378,10 @@ export function buildWorkerRecruitmentProcessPanel(stepStatus, options = {}) {
387 378
 		total,
388 379
 		progressText,
389 380
 		showActions: true,
390
-		actionLocked,
381
+		actionLocked: false,
391 382
 		actionDisabled,
392 383
 		viewOnly,
393
-		lockHint: '请先完成方案确认',
384
+		lockHint: '当前步骤尚未开始',
394 385
 		primaryLabel: viewOnly ? '查看招聘' : '去招聘',
395 386
 		primaryIcon: '🔍',
396 387
 		primaryBtnClass: buildPrimaryBtnClass(actionDisabled),
@@ -415,28 +406,20 @@ export function syncStepProcessPanel(step, stepStatus, options = {}) {
415 406
 			qrToken: prev.qrToken,
416 407
 			registered: prev.registered,
417 408
 			total: prev.total,
418
-			highlight: prev.progressText,
419
-			actionLocked: prev.actionLocked === true
409
+			highlight: prev.progressText
420 410
 		})
421 411
 	} else if (type === 'worker_recruitment') {
422 412
 		panel = buildWorkerRecruitmentProcessPanel(stepStatus || step.status, {
423 413
 			registered: prev.registered,
424 414
 			total: prev.total,
425
-			highlight: prev.progressText,
426
-			actionLocked: prev.actionLocked === true
415
+			highlight: prev.progressText
427 416
 		})
428 417
 	} else if (type === 'task_publish') {
429
-		panel = buildTaskPublishProcessPanel(stepStatus || step.status, {
430
-			actionLocked: prev.actionLocked === true
431
-		})
418
+		panel = buildTaskPublishProcessPanel(stepStatus || step.status)
432 419
 	} else if (type === 'settlement') {
433
-		panel = buildSettlementProcessPanel(stepStatus || step.status, {
434
-			actionLocked: prev.actionLocked === true
435
-		})
420
+		panel = buildSettlementProcessPanel(stepStatus || step.status)
436 421
 	} else if (type === 'invoice_archive') {
437
-		panel = buildInvoiceArchiveProcessPanel(stepStatus || step.status, {
438
-			actionLocked: prev.actionLocked === true
439
-		})
422
+		panel = buildInvoiceArchiveProcessPanel(stepStatus || step.status)
440 423
 	} else {
441 424
 		panel = buildEnterpriseCertProcessPanel(stepStatus || step.status, options)
442 425
 	}

+ 24 - 8
huimv-employment/app/utils/recruit.js

@@ -2,6 +2,8 @@
2 2
  * 临时工招聘:条件展示 / 可用人员映射 / 提交载荷
3 3
  */
4 4
 
5
+import { formatWorkDurationUnit } from '@/utils/draft.js'
6
+
5 7
 function pick(raw, camel, snake) {
6 8
 	if (!raw || typeof raw !== 'object') return null
7 9
 	if (raw[camel] != null && raw[camel] !== '') return raw[camel]
@@ -24,23 +26,37 @@ export function syncRecruitGroupMeta(group) {
24 26
 	group.chevronText = group.expanded ? '▼' : '▶'
25 27
 }
26 28
 
27
-/** 搜索条件(多工种逗号分隔) */
29
+/**
30
+ * 搜索条件
31
+ * 需求人数:工种*人数(如 搬运*10人,分拣*5人)
32
+ * 工作天数:工种*{x}天/月(月结用月,其余用天)
33
+ */
28 34
 export function buildRecruitCriteria(workerGroups = []) {
29 35
 	const groups = Array.isArray(workerGroups) ? workerGroups.filter((g) => g && g.workType) : []
30 36
 	const criteriaTypes = groups.map((g) => g.workType).filter(Boolean)
31
-	const totalNeed = groups.reduce((sum, g) => sum + (Number(g.workerCount) || 0), 0)
37
+	const needParts = []
38
+	const dayParts = []
32 39
 	const locations = []
33
-	const daysSet = []
34 40
 	groups.forEach((g) => {
35
-		if (g.workLocation && locations.indexOf(g.workLocation) < 0) locations.push(g.workLocation)
36
-		if (g.workDays != null && g.workDays !== '') daysSet.push(Number(g.workDays))
41
+		const type = String(g.workType || '').trim()
42
+		if (!type) return
43
+		const count = Number(g.workerCount)
44
+		if (g.workerCount != null && g.workerCount !== '' && !Number.isNaN(count) && count > 0) {
45
+			needParts.push(`${type}*${count}人`)
46
+		}
47
+		const days = Number(g.workDays)
48
+		if (g.workDays != null && g.workDays !== '' && !Number.isNaN(days)) {
49
+			dayParts.push(`${type}*${days}${formatWorkDurationUnit(g.settlementMode)}`)
50
+		}
51
+		if (g.workLocation && locations.indexOf(g.workLocation) < 0) {
52
+			locations.push(g.workLocation)
53
+		}
37 54
 	})
38
-	const days = daysSet.length ? daysSet[0] : null
39 55
 	return {
40 56
 		jobTypeText: criteriaTypes.join(',') || '—',
41
-		needCountText: totalNeed > 0 ? `${totalNeed}人` : '—',
57
+		needCountText: needParts.length ? needParts.join(',') : '—',
42 58
 		locationText: locations.join(',') || '—',
43
-		daysText: days != null && !Number.isNaN(days) ? `${days}天` : '—'
59
+		daysText: dayParts.length ? dayParts.join(',') : '—'
44 60
 	}
45 61
 }
46 62
 

+ 15 - 121
huimv-employment/app/utils/registration-batch.js

@@ -3,13 +3,12 @@ import {
3 3
 	isEnterpriseCertSkipped,
4 4
 	isRegistrationStep,
5 5
 	isWorkerRecruitmentStep,
6
-	isStaffingStep,
7
-	buildWorkerRecruitmentProcessPanel,
8 6
 	isTaskPublishStep,
9 7
 	isSettlementStep,
10 8
 	isInvoiceArchiveStep,
11 9
 	buildEnterpriseCertProcessPanel,
12 10
 	buildRegistrationQrProcessPanel,
11
+	buildWorkerRecruitmentProcessPanel,
13 12
 	buildTaskPublishProcessPanel,
14 13
 	buildSettlementProcessPanel,
15 14
 	buildInvoiceArchiveProcessPanel
@@ -233,100 +232,17 @@ function planConfirmedOrParallelReady(steps) {
233 232
 	return list.some((s) => isPlanConfirmedStep(s) && s.status === 'done')
234 233
 }
235 234
 
236
-/**
237
- * 流程门禁:
238
- * - 企业认证 / 人员登记|招聘:可并行(方案确认后可操作;登记/招聘 status 以接口为准)
239
- * - 任务发布 → 结算打款 → 开票归档:须上一环节完成
240
- */
241
-function applyParallelAndSequentialGates(steps, options = {}) {
242
-	const list = (Array.isArray(steps) ? steps : []).map((s) => Object.assign({}, s))
243
-	const registered = options.enterpriseRegistered === true
244
-	const certSkipped = options.certSkipped === true
245
-
246
-	const hasPlan = list.some(isPlanConfirmedStep)
247
-	const planDone = !hasPlan || list.some((s) => isPlanConfirmedStep(s) && s.status === 'done')
248
-
249
-	// 1) 并行段:认证 + 登记(接口已给 active/done 时以接口为准,勿用本地认证态覆盖)
250
-	for (let i = 0; i < list.length; i++) {
251
-		const s = list[i]
252
-		if (isEnterpriseAuthStep(s)) {
253
-			if (registered && s.status === 'pending') {
254
-				// 本地已认证且接口仍 pending(偶发滞后)时抬升
255
-				s.status = 'done'
256
-			} else if (!registered && certSkipped) {
257
-				s.status = 'pending'
258
-			} else if (!registered && planDone && s.status === 'pending') {
259
-				s.status = 'active'
260
-			}
261
-		} else if (isStaffingStep(s)) {
262
-			// 状态以接口为准(pending 保持灰色「未开始」);方案确认后仅解锁操作
263
-			if (s.status === 'done') {
264
-				s._actionLocked = false
265
-			} else if (!planDone) {
266
-				s._actionLocked = true
267
-			} else {
268
-				s._actionLocked = false
269
-			}
270
-		}
271
-	}
272
-
273
-	const authOk = registered
274
-		|| certSkipped
275
-		|| list.some((s) => isEnterpriseAuthStep(s) && s.status === 'done')
276
-	// 有登记/招聘节点时必须 done;无该节点才视为不需要登记
277
-	const hasRegStep = list.some(isStaffingStep)
278
-	const regOk = hasRegStep
279
-		? list.some((s) => isStaffingStep(s) && s.status === 'done')
280
-		: false
281
-	const parallelReady = authOk && regOk
282
-
283
-	const taskDone = list.some((x) => isTaskPublishStep(x) && x.status === 'done')
284
-	const settleDone = list.some((x) => isSettlementStep(x) && x.status === 'done')
285
-		|| list.some((x) => {
286
-			const key = String((x && x.key) || '').toLowerCase()
287
-			const title = String((x && x.title) || '')
288
-			return (key === 'settlement_paid' || title.indexOf('已支付') >= 0) && x.status === 'done'
289
-		})
290
-
291
-	// 2) 串行段:任务发布 → 结算 → 开票(强制:未满足前置一律 pending + 锁定)
292
-	for (let i = 0; i < list.length; i++) {
293
-		const s = list[i]
294
-		if (isTaskPublishStep(s)) {
295
-			s._actionLocked = !parallelReady
296
-			if (s.status === 'done') {
297
-				s._actionLocked = false
298
-			} else if (!parallelReady) {
299
-				s.status = 'pending'
300
-			} else if (s.status !== 'active') {
301
-				// 前置已齐:仅允许 active 可点,其它保持 pending(等后端推进)
302
-				s.status = 'pending'
303
-				s._actionLocked = true
304
-			}
305
-		} else if (isSettlementStep(s)) {
306
-			s._actionLocked = !taskDone
307
-			if (s.status === 'done') {
308
-				s._actionLocked = false
309
-			} else if (!taskDone || s.status !== 'active') {
310
-				s.status = 'pending'
311
-				s._actionLocked = true
312
-			}
313
-		} else if (isInvoiceArchiveStep(s)) {
314
-			s._actionLocked = !settleDone
315
-			if (s.status === 'done') {
316
-				s._actionLocked = false
317
-			} else if (!settleDone || s.status !== 'active') {
318
-				s.status = 'pending'
319
-				s._actionLocked = true
320
-			}
321
-		}
322
-	}
323
-
324
-	return list
235
+/** 归一化流程接口步骤状态 → pending | active | done */
236
+function normalizeApiStepStatus(status) {
237
+	const s = String(status || 'pending').toLowerCase()
238
+	if (s === 'done' || s === 'completed' || s === 'finish' || s === 'finished') return 'done'
239
+	if (s === 'active' || s === 'current' || s === 'in_progress' || s === 'processing') return 'active'
240
+	return 'pending'
325 241
 }
326 242
 
327 243
 /**
328 244
  * workflow-progress / registration-batch 详情 → 聊天气泡进度卡片
329
- * 总进度按办理步骤(排除草稿节点)计算;不展示登记人数进度
245
+ * 步骤状态与可否操作一律以流程接口 status 为准(pending / active / done)
330 246
  * @param {object} raw API 原始响应或已 map 的 detail
331 247
  * @param {number|string} msgKey
332 248
  * @param {{ enterpriseRegistered?: boolean }} [options]
@@ -338,29 +254,10 @@ export function buildWorkflowChatCardMessage(raw, msgKey, options = {}) {
338 254
 	const registered = options.enterpriseRegistered === true
339 255
 	const certSkipped = !registered && isEnterpriseCertSkipped()
340 256
 
341
-	// 先归一化各步 status,再套并行/串行门禁,最后算连线与面板
342
-	let normalized = filtered.map((s) => {
343
-		const status = (s && s.status) || 'pending'
257
+	const normalized = filtered.map((s) => {
258
+		const status = normalizeApiStepStatus(s && s.status)
344 259
 		return Object.assign({}, s, { status })
345 260
 	})
346
-	// 已支付完成时,待结算引导卡同步为 done
347
-	const paidDone = normalized.some((s) => {
348
-		const key = String((s && s.key) || '').toLowerCase()
349
-		const title = String((s && s.title) || '')
350
-		return (key === 'settlement_paid' || title.indexOf('已支付') >= 0)
351
-			&& (s.status === 'done' || s.status === 'active')
352
-	})
353
-	if (paidDone) {
354
-		for (let i = 0; i < normalized.length; i++) {
355
-			if (isSettlementStep(normalized[i])) {
356
-				normalized[i] = Object.assign({}, normalized[i], { status: 'done' })
357
-			}
358
-		}
359
-	}
360
-	normalized = applyParallelAndSequentialGates(normalized, {
361
-		enterpriseRegistered: registered,
362
-		certSkipped
363
-	})
364 261
 
365 262
 	const qrCodeUrl = detail.qrCodeUrl || ''
366 263
 	const qrToken = detail.qrToken || ''
@@ -379,7 +276,6 @@ export function buildWorkflowChatCardMessage(raw, msgKey, options = {}) {
379 276
 		const isTask = isTaskPublishStep(s)
380 277
 		const isSettle = isSettlementStep(s)
381 278
 		const isInvoice = isInvoiceArchiveStep(s)
382
-		const actionLocked = s._actionLocked === true
383 279
 		// 步骤条展示名统一
384 280
 		let title = s.title || `步骤${i + 1}`
385 281
 		if (isReg) title = '人员登记'
@@ -415,22 +311,20 @@ export function buildWorkflowChatCardMessage(raw, msgKey, options = {}) {
415 311
 				qrToken,
416 312
 				registered: progressCurrent,
417 313
 				total: progressTotal,
418
-				highlight: s.highlight || '',
419
-				actionLocked
314
+				highlight: s.highlight || ''
420 315
 			})
421 316
 		} else if (isRecruit) {
422 317
 			processPanel = buildWorkerRecruitmentProcessPanel(status, {
423 318
 				registered: progressCurrent,
424 319
 				total: progressTotal,
425
-				highlight: s.highlight || '',
426
-				actionLocked
320
+				highlight: s.highlight || ''
427 321
 			})
428 322
 		} else if (isTask) {
429
-			processPanel = buildTaskPublishProcessPanel(status, { actionLocked })
323
+			processPanel = buildTaskPublishProcessPanel(status)
430 324
 		} else if (isSettle) {
431
-			processPanel = buildSettlementProcessPanel(status, { actionLocked })
325
+			processPanel = buildSettlementProcessPanel(status)
432 326
 		} else if (isInvoice) {
433
-			processPanel = buildInvoiceArchiveProcessPanel(status, { actionLocked })
327
+			processPanel = buildInvoiceArchiveProcessPanel(status)
434 328
 		}
435 329
 
436 330
 		let processMod = ''

+ 7 - 11
huimv-employment/app/utils/settlement.js

@@ -1,5 +1,6 @@
1 1
 /**
2 2
  * 结算单摘要映射(含多工种拆分)
3
+ * 工种组一律取自结算详情 worker_groups,不再依赖草稿详情
3 4
  */
4 5
 import { formatMoney } from '@/utils/progress-action.js'
5 6
 import { normalizeWorkerGroups } from '@/utils/draft.js'
@@ -39,13 +40,11 @@ function mapFees(raw = {}) {
39 40
 }
40 41
 
41 42
 /**
42
- * 按草稿工种组拆分:企业人均成本 / 员工人均到手 / 工种实发合计
43
+ * 按结算详情 worker_groups 拆分:企业人均成本 / 员工人均到手 / 工种实发合计
43 44
  * 无工种组时退化为整体一条
44 45
  */
45
-export function buildSettlementWorkTypeGroups(raw = {}, draftGroups = []) {
46
-	const groups = Array.isArray(draftGroups) && draftGroups.length
47
-		? draftGroups
48
-		: normalizeWorkerGroups(raw)
46
+export function buildSettlementWorkTypeGroups(raw = {}) {
47
+	const groups = normalizeWorkerGroups(raw)
49 48
 	const totalOut = num(raw.totalOutflow != null ? raw.totalOutflow : raw.total_outflow)
50 49
 	const netTotal = num(raw.netAmount != null ? raw.netAmount : raw.net_amount)
51 50
 	const workerCount = num(raw.workerCount != null ? raw.workerCount : raw.worker_count)
@@ -102,8 +101,8 @@ export function buildSettlementWorkTypeGroups(raw = {}, draftGroups = []) {
102 101
 }
103 102
 
104 103
 /**
105
- * @param {object} raw 结算接口响应
106
- * @param {{ workerGroups?: Array, title?: string }} [ctx]
104
+ * @param {object} raw 结算接口响应(含 worker_groups)
105
+ * @param {{ title?: string }} [ctx]
107 106
  */
108 107
 export function mapSettlementSummary(raw = {}, ctx = {}) {
109 108
 	const fees = mapFees(raw)
@@ -113,10 +112,7 @@ export function mapSettlementSummary(raw = {}, ctx = {}) {
113 112
 	const total = raw.totalOutflow != null ? raw.totalOutflow : raw.total_outflow
114 113
 	const status = raw.settlementStatus || raw.settlement_status || ''
115 114
 	const workerCount = raw.workerCount != null ? raw.workerCount : raw.worker_count
116
-	const workTypeGroups = buildSettlementWorkTypeGroups(
117
-		Object.assign({}, raw, { title: ctx.title }),
118
-		ctx.workerGroups || []
119
-	)
115
+	const workTypeGroups = buildSettlementWorkTypeGroups(raw)
120 116
 
121 117
 	const enrichedRaw = Object.assign({}, raw, {
122 118
 		title: ctx.title || raw.title,