xsh_1997 23 часов назад
Родитель
Сommit
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
 	if (params.size != null) data.size = params.size
28
 	if (params.size != null) data.size = params.size
29
 	return get('/notifications', data, options)
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
 /** 小程序直连 API(体验版/开发版统一;有正式 HTTPS 域名后再改 env) */
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
 /** H5 开发走 devServer 代理,避免浏览器跨域 */
5
 /** H5 开发走 devServer 代理,避免浏览器跨域 */
6
 const H5_DEV_PROXY_API = '/api/v1/mp'
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
 	font-size: 26rpx;
479
 	font-size: 26rpx;
480
 	font-weight: 700;
480
 	font-weight: 700;
481
 	color: $fe-foreground;
481
 	color: $fe-foreground;
482
+	line-height: 1.4;
482
 	word-break: break-all;
483
 	word-break: break-all;
483
 }
484
 }
484
 
485
 

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

@@ -131,9 +131,8 @@
131
 <script>
131
 <script>
132
 import { showToast } from '@/common/chat-data.js'
132
 import { showToast } from '@/common/chat-data.js'
133
 import { getOrderSettlement, confirmPaySettlement, startSettlement } from '@/api/order.js'
133
 import { getOrderSettlement, confirmPaySettlement, startSettlement } from '@/api/order.js'
134
-import { getDraftDetail } from '@/api/draft.js'
135
 import { mapSettlementSummary } from '@/utils/settlement.js'
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
 const EMPTY_VIEW = {
137
 const EMPTY_VIEW = {
139
 	sourceTitle: '',
138
 	sourceTitle: '',
@@ -166,9 +165,7 @@ export default {
166
 			loading: false,
165
 			loading: false,
167
 			loadError: '',
166
 			loadError: '',
168
 			paying: false,
167
 			paying: false,
169
-			view: Object.assign({}, EMPTY_VIEW),
170
-			draftGroups: [],
171
-			draftTitle: ''
168
+			view: Object.assign({}, EMPTY_VIEW)
172
 		}
169
 		}
173
 	},
170
 	},
174
 	computed: {
171
 	computed: {
@@ -182,9 +179,6 @@ export default {
182
 		},
179
 		},
183
 		orderId() {
180
 		orderId() {
184
 			if (this.active) this.loadDetail()
181
 			if (this.active) this.loadDetail()
185
-		},
186
-		draftId() {
187
-			if (this.active) this.loadDetail()
188
 		}
182
 		}
189
 	},
183
 	},
190
 	methods: {
184
 	methods: {
@@ -192,18 +186,32 @@ export default {
192
 		onClose() {
186
 		onClose() {
193
 			this.$emit('close')
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
 			try {
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
 			} catch (e) {
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
 		async loadDetail() {
216
 		async loadDetail() {
209
 			if (this.orderId == null || this.orderId === '') {
217
 			if (this.orderId == null || this.orderId === '') {
@@ -214,23 +222,8 @@ export default {
214
 			this.loading = true
222
 			this.loading = true
215
 			this.loadError = ''
223
 			this.loadError = ''
216
 			try {
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
 			} catch (e) {
227
 			} catch (e) {
235
 				this.view = Object.assign({}, EMPTY_VIEW)
228
 				this.view = Object.assign({}, EMPTY_VIEW)
236
 				this.loadError = (e && (e.msg || e.message)) || '结算单加载失败'
229
 				this.loadError = (e && (e.msg || e.message)) || '结算单加载失败'
@@ -251,16 +244,21 @@ export default {
251
 			}
244
 			}
252
 			this.paying = true
245
 			this.paying = true
253
 			try {
246
 			try {
254
-				const raw = await confirmPaySettlement(
247
+				const payRaw = await confirmPaySettlement(
255
 					this.orderId,
248
 					this.orderId,
256
 					{ paymentChannel: 'wechat' },
249
 					{ paymentChannel: 'wechat' },
257
 					{ showError: true }
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
 				this.$emit('paid', {
262
 				this.$emit('paid', {
265
 					orderId: this.orderId,
263
 					orderId: this.orderId,
266
 					settlementId: this.view.settlementId,
264
 					settlementId: this.view.settlementId,

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

@@ -25,37 +25,29 @@
25
 						<text class="tp-hero__desc">{{ statusDesc }}</text>
25
 						<text class="tp-hero__desc">{{ statusDesc }}</text>
26
 					</view>
26
 					</view>
27
 
27
 
28
-					<!-- 任务信息:按工种多组 -->
28
+					<!-- 任务信息:多工种逗号拼接,单卡展示 -->
29
 					<view class="tp-section">
29
 					<view class="tp-section">
30
 						<text class="tp-section__title">任务信息</text>
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
 							<view class="tp-grid">
32
 							<view class="tp-grid">
38
 								<view class="tp-grid__cell">
33
 								<view class="tp-grid__cell">
39
 									<text class="tp-grid__label">岗位类型</text>
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
 								</view>
36
 								</view>
42
 								<view class="tp-grid__cell">
37
 								<view class="tp-grid__cell">
43
 									<text class="tp-grid__label">用工人数</text>
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
 								</view>
40
 								</view>
46
 								<view class="tp-grid__cell">
41
 								<view class="tp-grid__cell">
47
 									<text class="tp-grid__label">工作地点</text>
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
 								</view>
44
 								</view>
50
 								<view class="tp-grid__cell">
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
 								</view>
48
 								</view>
54
 							</view>
49
 							</view>
55
 						</view>
50
 						</view>
56
-						<view v-if="!groups.length" class="tp-card">
57
-							<text class="tp-empty__text">暂无工种信息</text>
58
-						</view>
59
 					</view>
51
 					</view>
60
 
52
 
61
 					<!-- 工作时间:按工种多组 -->
53
 					<!-- 工作时间:按工种多组 -->
@@ -186,7 +178,6 @@
186
 </template>
178
 </template>
187
 
179
 
188
 <script>
180
 <script>
189
-import store from '@/common/store.js'
190
 import { showToast } from '@/common/chat-data.js'
181
 import { showToast } from '@/common/chat-data.js'
191
 import { getDraftDetail, getDraftWorkflowProgress } from '@/api/draft.js'
182
 import { getDraftDetail, getDraftWorkflowProgress } from '@/api/draft.js'
192
 import { getOrderPublishTask, publishOrderTask } from '@/api/order.js'
183
 import { getOrderPublishTask, publishOrderTask } from '@/api/order.js'
@@ -198,7 +189,7 @@ import {
198
 	normalizeDateYmd,
189
 	normalizeDateYmd,
199
 	mapDraftDetailToCard
190
 	mapDraftDetailToCard
200
 } from '@/utils/draft.js'
191
 } from '@/utils/draft.js'
201
-import { isStaffingStep, isEnterpriseCertSkipped } from '@/utils/process-card.js'
192
+import { isTaskPublishStep } from '@/utils/process-card.js'
202
 import { mapRegistrationBatchDetail } from '@/utils/registration-batch.js'
193
 import { mapRegistrationBatchDetail } from '@/utils/registration-batch.js'
203
 
194
 
204
 /** 规范化 HH:mm */
195
 /** 规范化 HH:mm */
@@ -292,7 +283,8 @@ export default {
292
 			groups: [],
283
 			groups: [],
293
 			statusDesc: '请确认任务信息后发布',
284
 			statusDesc: '请确认任务信息后发布',
294
 			orderIdInner: null,
285
 			orderIdInner: null,
295
-			registrationDone: false,
286
+			/** 流程接口任务发布步骤:pending | active | done */
287
+			taskStepStatus: 'pending',
296
 			/** 已登记 < 计划人数,发布时需 allow_understaffed */
288
 			/** 已登记 < 计划人数,发布时需 allow_understaffed */
297
 			understaffed: false,
289
 			understaffed: false,
298
 			registeredCount: 0,
290
 			registeredCount: 0,
@@ -303,11 +295,8 @@ export default {
303
 		heroTitle() {
295
 		heroTitle() {
304
 			return this.readonly ? '任务发布详情' : '确认发布任务'
296
 			return this.readonly ? '任务发布详情' : '确认发布任务'
305
 		},
297
 		},
306
-		enterpriseOk() {
307
-			return store.isEnterpriseRegistered() === true
308
-		},
309
 		canPublish() {
298
 		canPublish() {
310
-			return this.enterpriseOk && this.registrationDone
299
+			return this.taskStepStatus === 'active'
311
 		},
300
 		},
312
 		scheduleComplete() {
301
 		scheduleComplete() {
313
 			const list = this.groups || []
302
 			const list = this.groups || []
@@ -320,11 +309,8 @@ export default {
320
 			return true
309
 			return true
321
 		},
310
 		},
322
 		gateTip() {
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
 			return ''
314
 			return ''
329
 		},
315
 		},
330
 		confirmDisabled() {
316
 		confirmDisabled() {
@@ -334,6 +320,51 @@ export default {
334
 			if (this.readonly) return '已发布'
320
 			if (this.readonly) return '已发布'
335
 			if (this.publishing) return '发布中...'
321
 			if (this.publishing) return '发布中...'
336
 			return '确认发布'
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
 	watch: {
370
 	watch: {
@@ -382,7 +413,6 @@ export default {
382
 			const emptyDate = viewMode ? '—' : '选择日期'
413
 			const emptyDate = viewMode ? '—' : '选择日期'
383
 			const emptyTime = viewMode ? '—' : '选择时间'
414
 			const emptyTime = viewMode ? '—' : '选择时间'
384
 			const list = Array.isArray(groups) ? groups : []
415
 			const list = Array.isArray(groups) ? groups : []
385
-			const rawHasTime = (raw) => raw != null && raw !== '' && /\d{1,2}:\d{2}/.test(String(raw))
386
 			return list.map((g, idx) => {
416
 			return list.map((g, idx) => {
387
 				const startParts = splitDateTimeParts(g.workStartDate)
417
 				const startParts = splitDateTimeParts(g.workStartDate)
388
 				const endParts = splitDateTimeParts(g.workEndDate)
418
 				const endParts = splitDateTimeParts(g.workEndDate)
@@ -390,9 +420,9 @@ export default {
390
 				const days = g.workDays
420
 				const days = g.workDays
391
 				let startDate = normalizeDateYmd(startParts.date)
421
 				let startDate = normalizeDateYmd(startParts.date)
392
 				let endDate = normalizeDateYmd(endParts.date)
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
 				if (!viewMode) {
426
 				if (!viewMode) {
397
 					if (!startDate) startDate = tomorrowYmd()
427
 					if (!startDate) startDate = tomorrowYmd()
398
 					if (!startTime) startTime = '08:00'
428
 					if (!startTime) startTime = '08:00'
@@ -549,14 +579,15 @@ export default {
549
 			}
579
 			}
550
 			return ''
580
 			return ''
551
 		},
581
 		},
552
-		resolveRegistrationDone(workflowRaw) {
582
+		resolveTaskStepStatus(workflowRaw) {
553
 			const detail = mapRegistrationBatchDetail(workflowRaw || {})
583
 			const detail = mapRegistrationBatchDetail(workflowRaw || {})
554
 			const steps = detail.steps || []
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
 		buildStatusDesc(workflowRaw) {
592
 		buildStatusDesc(workflowRaw) {
562
 			const detail = mapRegistrationBatchDetail(workflowRaw || {})
593
 			const detail = mapRegistrationBatchDetail(workflowRaw || {})
@@ -567,10 +598,11 @@ export default {
567
 				if (total > 0 && current < total) {
598
 				if (total > 0 && current < total) {
568
 					return `已登记 ${current}/${total},未招满发布需确认缺编`
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
 		syncStaffingFromWorkflow(workflowRaw) {
607
 		syncStaffingFromWorkflow(workflowRaw) {
576
 			const detail = mapRegistrationBatchDetail(workflowRaw || {})
608
 			const detail = mapRegistrationBatchDetail(workflowRaw || {})
@@ -583,11 +615,12 @@ export default {
583
 		/**
615
 		/**
584
 		 * TaskPublishRequest:
616
 		 * TaskPublishRequest:
585
 		 * { work_items: [{ id?, group_no?, job_type?, work_start_date, work_end_date }], allow_understaffed? }
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
 		buildPublishPayload(allowUnderstaffed = false) {
620
 		buildPublishPayload(allowUnderstaffed = false) {
588
 			const workItems = (this.groups || []).map((g) => {
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
 				const item = {
624
 				const item = {
592
 					work_start_date: start,
625
 					work_start_date: start,
593
 					work_end_date: end
626
 					work_end_date: end
@@ -647,7 +680,7 @@ export default {
647
 				}
680
 				}
648
 
681
 
649
 				this.syncStaffingFromWorkflow(workflowRaw)
682
 				this.syncStaffingFromWorkflow(workflowRaw)
650
-				this.registrationDone = this.resolveRegistrationDone(workflowRaw)
683
+				this.taskStepStatus = this.resolveTaskStepStatus(workflowRaw)
651
 				this.statusDesc = this.buildStatusDesc(workflowRaw)
684
 				this.statusDesc = this.buildStatusDesc(workflowRaw)
652
 			} catch (e) {
685
 			} catch (e) {
653
 				this.loadError = (e && (e.msg || e.message)) || '任务信息加载失败'
686
 				this.loadError = (e && (e.msg || e.message)) || '任务信息加载失败'
@@ -667,15 +700,6 @@ export default {
667
 				showToast(this.gateTip || '暂不可发布')
700
 				showToast(this.gateTip || '暂不可发布')
668
 				return
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
 			const orderId = this.orderIdInner
703
 			const orderId = this.orderIdInner
680
 			if (orderId == null || orderId === '') {
704
 			if (orderId == null || orderId === '') {
681
 				showToast('缺少订单信息,暂无法发布')
705
 				showToast('缺少订单信息,暂无法发布')

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

@@ -892,8 +892,7 @@ import { getDraftDetail, getDraftCostComparison, getDraftWorkflowProgress } from
892
 import { buildWorkflowChatCardMessage, mapRegistrationBatchDetail } from '@/utils/registration-batch.js'
892
 import { buildWorkflowChatCardMessage, mapRegistrationBatchDetail } from '@/utils/registration-batch.js'
893
 import {
893
 import {
894
 	markEnterpriseCertSkipped,
894
 	markEnterpriseCertSkipped,
895
-	clearEnterpriseCertSkipped,
896
-	syncStepProcessPanel
895
+	clearEnterpriseCertSkipped
897
 } from '@/utils/process-card.js'
896
 } from '@/utils/process-card.js'
898
 import {
897
 import {
899
 	getNewChatGreeting,
898
 	getNewChatGreeting,
@@ -1130,7 +1129,7 @@ export default {
1130
 			this.user = s.user
1129
 			this.user = s.user
1131
 			this.enterprise = s.enterprise
1130
 			this.enterprise = s.enterprise
1132
 			await this.fetchSessions({ autoLoad: true })
1131
 			await this.fetchSessions({ autoLoad: true })
1133
-				this.refreshWorkflowProcessPanels()
1132
+				await this.refreshWorkflowProcessPanels()
1134
 			})().finally(() => {
1133
 			})().finally(() => {
1135
 				this._initHomePromise = null
1134
 				this._initHomePromise = null
1136
 			})
1135
 			})
@@ -2045,7 +2044,7 @@ export default {
2045
 					silent: true
2044
 					silent: true
2046
 				})
2045
 				})
2047
 			}
2046
 			}
2048
-			this.refreshWorkflowProcessPanels()
2047
+			await this.refreshWorkflowProcessPanels()
2049
 		},
2048
 		},
2050
 		/**
2049
 		/**
2051
 		 * 汇总需要展示进度卡的草稿 id:已确认草稿卡 / card_order / 会话进度列表
2050
 		 * 汇总需要展示进度卡的草稿 id:已确认草稿卡 / card_order / 会话进度列表
@@ -2660,48 +2659,68 @@ export default {
2660
 			this.$set(msg.workflow, 'stepIndex', stepIdx)
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
 			for (let i = 0; i < this.messages.length; i++) {
2700
 			for (let i = 0; i < this.messages.length; i++) {
2670
 				const msg = this.messages[i]
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
 			const draftId = msg.draftId != null
2721
 			const draftId = msg.draftId != null
2702
 				? msg.draftId
2722
 				? msg.draftId
2703
 				: (msg.workflow && msg.workflow.draftId)
2723
 				: (msg.workflow && msg.workflow.draftId)
2704
-			// 以流程步骤为准:active=未完成本单认证需提交;done=仅查看
2705
 			const isDone = step.status === 'done'
2724
 			const isDone = step.status === 'done'
2706
 				|| (step.processPanel && step.processPanel.processStatus === 'done')
2725
 				|| (step.processPanel && step.processPanel.processStatus === 'done')
2707
 			const readonly = !!isDone
2726
 			const readonly = !!isDone
@@ -2722,29 +2741,25 @@ export default {
2722
 				// 忽略:仍按本地已登记状态刷新进度卡
2741
 				// 忽略:仍按本地已登记状态刷新进度卡
2723
 			}
2742
 			}
2724
 			if (store.isEnterpriseRegistered()) clearEnterpriseCertSkipped()
2743
 			if (store.isEnterpriseRegistered()) clearEnterpriseCertSkipped()
2725
-			// 重建进度卡步骤条(门禁/连线/面板一并更新)
2726
 			if (draftId != null && draftId !== '') {
2744
 			if (draftId != null && draftId !== '') {
2727
 				await this.refreshWorkflowCardByDraftId(draftId)
2745
 				await this.refreshWorkflowCardByDraftId(draftId)
2728
 			} else {
2746
 			} else {
2729
-				this.refreshWorkflowProcessPanels()
2747
+				await this.refreshWorkflowProcessPanels()
2730
 			}
2748
 			}
2731
 			this.ensureChatBottom()
2749
 			this.ensureChatBottom()
2732
 		},
2750
 		},
2751
+		/** 门禁:登记/招聘不看 pending;其余 done 可查看、active 可操作、pending 禁用 */
2733
 		isWorkflowActionBlocked(step) {
2752
 		isWorkflowActionBlocked(step) {
2734
 			if (!step || !step.processPanel) return true
2753
 			if (!step || !step.processPanel) return true
2735
 			const panel = step.processPanel
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
 		isWorkflowStepViewOnly(step) {
2764
 		isWorkflowStepViewOnly(step) {
2750
 			if (!step || !step.processPanel) return false
2765
 			if (!step || !step.processPanel) return false
@@ -2752,15 +2767,17 @@ export default {
2752
 				|| step.processPanel.processStatus === 'done'
2767
 				|| step.processPanel.processStatus === 'done'
2753
 				|| step.processPanel.viewOnly === true
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
 			const draftId = msg.draftId != null ? msg.draftId : msg.workflow.draftId
2779
 			const draftId = msg.draftId != null ? msg.draftId : msg.workflow.draftId
2762
 			const panel = step.processPanel
2780
 			const panel = step.processPanel
2763
-			// 查看招聘:与人员登记进度弹层同一套内容,仅标题改为「临时工招聘」
2764
 			const isViewRecruit = this.isWorkflowStepViewOnly(step)
2781
 			const isViewRecruit = this.isWorkflowStepViewOnly(step)
2765
 				|| panel.viewOnly === true
2782
 				|| panel.viewOnly === true
2766
 				|| panel.primaryLabel === '查看招聘'
2783
 				|| panel.primaryLabel === '查看招聘'
@@ -2786,12 +2803,15 @@ export default {
2786
 			}
2803
 			}
2787
 			this.ensureChatBottom()
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
 			const draftId = msg.draftId != null ? msg.draftId : msg.workflow.draftId
2815
 			const draftId = msg.draftId != null ? msg.draftId : msg.workflow.draftId
2796
 			const detail = msg.workflow.detail || {}
2816
 			const detail = msg.workflow.detail || {}
2797
 			const orderId = detail.orderId || detail.order_id || null
2817
 			const orderId = detail.orderId || detail.order_id || null
@@ -2814,12 +2834,15 @@ export default {
2814
 			}
2834
 			}
2815
 			this.ensureChatBottom()
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
 			const detail = msg.workflow.detail || {}
2846
 			const detail = msg.workflow.detail || {}
2824
 			const orderId = detail.orderId || detail.order_id || ''
2847
 			const orderId = detail.orderId || detail.order_id || ''
2825
 			const draftId = msg.draftId != null ? msg.draftId : msg.workflow.draftId
2848
 			const draftId = msg.draftId != null ? msg.draftId : msg.workflow.draftId
@@ -2830,12 +2853,15 @@ export default {
2830
 			}
2853
 			}
2831
 			this.openProgressDetail(draftId)
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
 			const detail = msg.workflow.detail || {}
2865
 			const detail = msg.workflow.detail || {}
2840
 			const draftId = msg.draftId != null ? msg.draftId : msg.workflow.draftId
2866
 			const draftId = msg.draftId != null ? msg.draftId : msg.workflow.draftId
2841
 			const orderId = detail.orderId || detail.order_id || null
2867
 			const orderId = detail.orderId || detail.order_id || null
@@ -2994,12 +3020,12 @@ export default {
2994
 		},
3020
 		},
2995
 		/** 人员登记:查看/分享(数据来自 workflow-progress) */
3021
 		/** 人员登记:查看/分享(数据来自 workflow-progress) */
2996
 		async openWorkflowQrByIndex(msgIdx, stepIdx) {
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
 				return
3029
 				return
3004
 			}
3030
 			}
3005
 			const draftId = msg.draftId != null ? msg.draftId : msg.workflow.draftId
3031
 			const draftId = msg.draftId != null ? msg.draftId : msg.workflow.draftId
@@ -3046,13 +3072,13 @@ export default {
3046
 			}
3072
 			}
3047
 		},
3073
 		},
3048
 		/** 人员登记:查看进度(FeProgressSheet 走 workflow-progress) */
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
 				return
3082
 				return
3057
 			}
3083
 			}
3058
 			this.openProgressDetail(msg.draftId || (msg.workflow && msg.workflow.draftId))
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
 import store from '@/common/store.js'
50
 import store from '@/common/store.js'
51
 import { getToken } from '@/utils/request.js'
51
 import { getToken } from '@/utils/request.js'
52
 import { showToast } from '@/common/chat-data.js'
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
 const PAGE_SIZE = 20
56
 const PAGE_SIZE = 20
57
 
57
 
@@ -129,9 +129,27 @@ export default {
129
 				this.loadingMore = false
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
 			const item = this.inbox[idx]
150
 			const item = this.inbox[idx]
134
 			if (!item) return
151
 			if (!item) return
152
+			await this.markReadIfNeeded(idx)
135
 			const url = item.actionUrl
153
 			const url = item.actionUrl
136
 			if (!url || typeof url !== 'string' || url.indexOf('/packageA/') !== 0) {
154
 			if (!url || typeof url !== 'string' || url.indexOf('/packageA/') !== 0) {
137
 				showToast(item.title || '消息详情')
155
 				showToast(item.title || '消息详情')

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

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

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

@@ -544,15 +544,21 @@ function pickDateTimeRaw(g, camel, snake) {
544
 	return v == null ? '' : String(v)
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
 export function splitDateTimeParts(raw) {
548
 export function splitDateTimeParts(raw) {
549
 	if (raw == null || raw === '') {
549
 	if (raw == null || raw === '') {
550
 		return { date: '', time: '' }
550
 		return { date: '', time: '' }
551
 	}
551
 	}
552
 	let s = String(raw).trim().replace('T', ' ').replace(/\//g, '-')
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
 	if (!m) return { date: '', time: '' }
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
 /** 统一为 YYYY-MM-DD(去掉 /) */
564
 /** 统一为 YYYY-MM-DD(去掉 /) */

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

@@ -60,6 +60,14 @@ function buildItemClass(readFlag, handledFlag, notifyType) {
60
 	return parts.join(' ')
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
 function formatRelativeTime(value) {
71
 function formatRelativeTime(value) {
64
 	if (value == null || value === '') return ''
72
 	if (value == null || value === '') return ''
65
 	const s = String(value).replace('T', ' ')
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
 	if (processStatus === PROCESS_STATUS.PENDING) return true
86
 	if (processStatus === PROCESS_STATUS.PENDING) return true
87
 	return false
87
 	return false
88
 }
88
 }
@@ -180,7 +180,7 @@ export function buildEnterpriseCertProcessPanel(stepStatus, options = {}) {
180
 	const processStatus = mapStepStatusToProcessStatus(stepStatus, options)
180
 	const processStatus = mapStepStatusToProcessStatus(stepStatus, options)
181
 	const statusMeta = buildProcessStatusMeta(processStatus)
181
 	const statusMeta = buildProcessStatusMeta(processStatus)
182
 	const isDone = processStatus === PROCESS_STATUS.DONE
182
 	const isDone = processStatus === PROCESS_STATUS.DONE
183
-	// 可与人员登记并行:未完成即可认证;已完成只读查看;已跳过仍可回来认证
183
+	const actionDisabled = isActionDisabledByApiStatus(processStatus)
184
 	const showActions = true
184
 	const showActions = true
185
 	return {
185
 	return {
186
 		cardType: 'enterprise_cert',
186
 		cardType: 'enterprise_cert',
@@ -193,14 +193,15 @@ export function buildEnterpriseCertProcessPanel(stepStatus, options = {}) {
193
 			{ msgKey: 'b3', text: '法人身份验证' }
193
 			{ msgKey: 'b3', text: '法人身份验证' }
194
 		],
194
 		],
195
 		showActions,
195
 		showActions,
196
-		showSkip: !isDone && processStatus !== PROCESS_STATUS.SKIPPED,
196
+		showSkip: !isDone && processStatus !== PROCESS_STATUS.SKIPPED && !actionDisabled,
197
 		primaryLabel: isDone ? '查看认证' : '去认证',
197
 		primaryLabel: isDone ? '查看认证' : '去认证',
198
 		secondaryLabel: '跳过',
198
 		secondaryLabel: '跳过',
199
 		readonly: isDone,
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
 		...statusMeta
205
 		...statusMeta
205
 	}
206
 	}
206
 }
207
 }
@@ -208,13 +209,11 @@ export function buildEnterpriseCertProcessPanel(stepStatus, options = {}) {
208
 /**
209
 /**
209
  * 任务发布步骤面板
210
  * 任务发布步骤面板
210
  * @param {string} stepStatus done|active|pending
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
 	const processStatus = mapStepStatusToProcessStatus(stepStatus)
214
 	const processStatus = mapStepStatusToProcessStatus(stepStatus)
215
 	const statusMeta = buildProcessStatusMeta(processStatus)
215
 	const statusMeta = buildProcessStatusMeta(processStatus)
216
-	const actionLocked = options.actionLocked === true
217
-	const actionDisabled = isSequentialActionDisabled(processStatus, actionLocked)
216
+	const actionDisabled = isActionDisabledByApiStatus(processStatus)
218
 	const viewOnly = processStatus === PROCESS_STATUS.DONE
217
 	const viewOnly = processStatus === PROCESS_STATUS.DONE
219
 	return {
218
 	return {
220
 		cardType: 'task_publish',
219
 		cardType: 'task_publish',
@@ -227,10 +226,10 @@ export function buildTaskPublishProcessPanel(stepStatus, options = {}) {
227
 			{ msgKey: 'b3', text: '开启考勤打卡' }
226
 			{ msgKey: 'b3', text: '开启考勤打卡' }
228
 		],
227
 		],
229
 		showActions: true,
228
 		showActions: true,
230
-		actionLocked,
229
+		actionLocked: actionDisabled,
231
 		actionDisabled,
230
 		actionDisabled,
232
 		viewOnly,
231
 		viewOnly,
233
-		lockHint: '请先完成企业认证与人员登记',
232
+		lockHint: '当前步骤尚未开始',
234
 		primaryLabel: viewOnly ? '查看发布' : '去发布',
233
 		primaryLabel: viewOnly ? '查看发布' : '去发布',
235
 		primaryIcon: '✈',
234
 		primaryIcon: '✈',
236
 		primaryBtnClass: buildPrimaryBtnClass(actionDisabled),
235
 		primaryBtnClass: buildPrimaryBtnClass(actionDisabled),
@@ -241,13 +240,11 @@ export function buildTaskPublishProcessPanel(stepStatus, options = {}) {
241
 /**
240
 /**
242
  * 结算打款步骤面板
241
  * 结算打款步骤面板
243
  * @param {string} stepStatus done|active|pending
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
 	const processStatus = mapStepStatusToProcessStatus(stepStatus)
245
 	const processStatus = mapStepStatusToProcessStatus(stepStatus)
248
 	const statusMeta = buildProcessStatusMeta(processStatus)
246
 	const statusMeta = buildProcessStatusMeta(processStatus)
249
-	const actionLocked = options.actionLocked === true
250
-	const actionDisabled = isSequentialActionDisabled(processStatus, actionLocked)
247
+	const actionDisabled = isActionDisabledByApiStatus(processStatus)
251
 	const viewOnly = processStatus === PROCESS_STATUS.DONE
248
 	const viewOnly = processStatus === PROCESS_STATUS.DONE
252
 	return {
249
 	return {
253
 		cardType: 'settlement',
250
 		cardType: 'settlement',
@@ -260,10 +257,10 @@ export function buildSettlementProcessPanel(stepStatus, options = {}) {
260
 			{ msgKey: 'b3', text: '一键打款到工人账户' }
257
 			{ msgKey: 'b3', text: '一键打款到工人账户' }
261
 		],
258
 		],
262
 		showActions: true,
259
 		showActions: true,
263
-		actionLocked,
260
+		actionLocked: actionDisabled,
264
 		actionDisabled,
261
 		actionDisabled,
265
 		viewOnly,
262
 		viewOnly,
266
-		lockHint: '请先完成任务发布',
263
+		lockHint: '当前步骤尚未开始',
267
 		primaryLabel: '查看结算单',
264
 		primaryLabel: '查看结算单',
268
 		primaryIcon: '◎',
265
 		primaryIcon: '◎',
269
 		primaryBtnClass: buildPrimaryBtnClass(actionDisabled),
266
 		primaryBtnClass: buildPrimaryBtnClass(actionDisabled),
@@ -274,13 +271,11 @@ export function buildSettlementProcessPanel(stepStatus, options = {}) {
274
 /**
271
 /**
275
  * 开票归档步骤面板
272
  * 开票归档步骤面板
276
  * @param {string} stepStatus done|active|pending
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
 	const processStatus = mapStepStatusToProcessStatus(stepStatus)
276
 	const processStatus = mapStepStatusToProcessStatus(stepStatus)
281
 	const statusMeta = buildProcessStatusMeta(processStatus)
277
 	const statusMeta = buildProcessStatusMeta(processStatus)
282
-	const actionLocked = options.actionLocked === true
283
-	const actionDisabled = isSequentialActionDisabled(processStatus, actionLocked)
278
+	const actionDisabled = isActionDisabledByApiStatus(processStatus)
284
 	const viewOnly = processStatus === PROCESS_STATUS.DONE
279
 	const viewOnly = processStatus === PROCESS_STATUS.DONE
285
 	return {
280
 	return {
286
 		cardType: 'invoice_archive',
281
 		cardType: 'invoice_archive',
@@ -293,10 +288,10 @@ export function buildInvoiceArchiveProcessPanel(stepStatus, options = {}) {
293
 			{ msgKey: 'b3', text: '导出合规证据链' }
288
 			{ msgKey: 'b3', text: '导出合规证据链' }
294
 		],
289
 		],
295
 		showActions: true,
290
 		showActions: true,
296
-		actionLocked,
291
+		actionLocked: actionDisabled,
297
 		actionDisabled,
292
 		actionDisabled,
298
 		viewOnly,
293
 		viewOnly,
299
-		lockHint: '请先完成结算打款',
294
+		lockHint: '当前步骤尚未开始',
300
 		primaryLabel: viewOnly ? '查看开票' : '去开票',
295
 		primaryLabel: viewOnly ? '查看开票' : '去开票',
301
 		primaryIcon: '📄',
296
 		primaryIcon: '📄',
302
 		primaryBtnClass: buildPrimaryBtnClass(actionDisabled),
297
 		primaryBtnClass: buildPrimaryBtnClass(actionDisabled),
@@ -315,8 +310,7 @@ export function buildInvoiceArchiveProcessPanel(stepStatus, options = {}) {
315
  *   qrToken?: string,
310
  *   qrToken?: string,
316
  *   registered?: number,
311
  *   registered?: number,
317
  *   total?: number,
312
  *   total?: number,
318
- *   highlight?: string,
319
- *   actionLocked?: boolean
313
+ *   highlight?: string
320
  * }} [qr]
314
  * }} [qr]
321
  */
315
  */
322
 export function buildRegistrationQrProcessPanel(stepStatus, qr = {}) {
316
 export function buildRegistrationQrProcessPanel(stepStatus, qr = {}) {
@@ -326,9 +320,8 @@ export function buildRegistrationQrProcessPanel(stepStatus, qr = {}) {
326
 	const total = qr.total != null ? Number(qr.total) : 0
320
 	const total = qr.total != null ? Number(qr.total) : 0
327
 	let progressText = qr.highlight || ''
321
 	let progressText = qr.highlight || ''
328
 	if (!progressText && total > 0) progressText = `已登记 ${registered}/${total}`
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
 	return {
325
 	return {
333
 		cardType: 'registration_qr',
326
 		cardType: 'registration_qr',
334
 		title: '人员登记',
327
 		title: '人员登记',
@@ -342,9 +335,9 @@ export function buildRegistrationQrProcessPanel(stepStatus, qr = {}) {
342
 		total,
335
 		total,
343
 		progressText,
336
 		progressText,
344
 		showActions: true,
337
 		showActions: true,
345
-		actionLocked,
338
+		actionLocked: false,
346
 		actionDisabled,
339
 		actionDisabled,
347
-		lockHint: '请先完成方案确认',
340
+		lockHint: '当前步骤尚未开始',
348
 		primaryLabel: '查看 / 分享',
341
 		primaryLabel: '查看 / 分享',
349
 		secondaryLabel: '查看进度',
342
 		secondaryLabel: '查看进度',
350
 		primaryBtnClass: buildPrimaryBtnClass(actionDisabled),
343
 		primaryBtnClass: buildPrimaryBtnClass(actionDisabled),
@@ -359,8 +352,7 @@ export function buildRegistrationQrProcessPanel(stepStatus, qr = {}) {
359
  * @param {{
352
  * @param {{
360
  *   registered?: number,
353
  *   registered?: number,
361
  *   total?: number,
354
  *   total?: number,
362
- *   highlight?: string,
363
- *   actionLocked?: boolean
355
+ *   highlight?: string
364
  * }} [options]
356
  * }} [options]
365
  */
357
  */
366
 export function buildWorkerRecruitmentProcessPanel(stepStatus, options = {}) {
358
 export function buildWorkerRecruitmentProcessPanel(stepStatus, options = {}) {
@@ -369,9 +361,8 @@ export function buildWorkerRecruitmentProcessPanel(stepStatus, options = {}) {
369
 	const registered = options.registered != null ? Number(options.registered) : 0
361
 	const registered = options.registered != null ? Number(options.registered) : 0
370
 	const total = options.total != null ? Number(options.total) : 0
362
 	const total = options.total != null ? Number(options.total) : 0
371
 	const progressText = `已登记 ${registered}/${total || 0}`
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
 	const viewOnly = processStatus === PROCESS_STATUS.DONE
366
 	const viewOnly = processStatus === PROCESS_STATUS.DONE
376
 	return {
367
 	return {
377
 		cardType: 'worker_recruitment',
368
 		cardType: 'worker_recruitment',
@@ -387,10 +378,10 @@ export function buildWorkerRecruitmentProcessPanel(stepStatus, options = {}) {
387
 		total,
378
 		total,
388
 		progressText,
379
 		progressText,
389
 		showActions: true,
380
 		showActions: true,
390
-		actionLocked,
381
+		actionLocked: false,
391
 		actionDisabled,
382
 		actionDisabled,
392
 		viewOnly,
383
 		viewOnly,
393
-		lockHint: '请先完成方案确认',
384
+		lockHint: '当前步骤尚未开始',
394
 		primaryLabel: viewOnly ? '查看招聘' : '去招聘',
385
 		primaryLabel: viewOnly ? '查看招聘' : '去招聘',
395
 		primaryIcon: '🔍',
386
 		primaryIcon: '🔍',
396
 		primaryBtnClass: buildPrimaryBtnClass(actionDisabled),
387
 		primaryBtnClass: buildPrimaryBtnClass(actionDisabled),
@@ -415,28 +406,20 @@ export function syncStepProcessPanel(step, stepStatus, options = {}) {
415
 			qrToken: prev.qrToken,
406
 			qrToken: prev.qrToken,
416
 			registered: prev.registered,
407
 			registered: prev.registered,
417
 			total: prev.total,
408
 			total: prev.total,
418
-			highlight: prev.progressText,
419
-			actionLocked: prev.actionLocked === true
409
+			highlight: prev.progressText
420
 		})
410
 		})
421
 	} else if (type === 'worker_recruitment') {
411
 	} else if (type === 'worker_recruitment') {
422
 		panel = buildWorkerRecruitmentProcessPanel(stepStatus || step.status, {
412
 		panel = buildWorkerRecruitmentProcessPanel(stepStatus || step.status, {
423
 			registered: prev.registered,
413
 			registered: prev.registered,
424
 			total: prev.total,
414
 			total: prev.total,
425
-			highlight: prev.progressText,
426
-			actionLocked: prev.actionLocked === true
415
+			highlight: prev.progressText
427
 		})
416
 		})
428
 	} else if (type === 'task_publish') {
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
 	} else if (type === 'settlement') {
419
 	} else if (type === 'settlement') {
433
-		panel = buildSettlementProcessPanel(stepStatus || step.status, {
434
-			actionLocked: prev.actionLocked === true
435
-		})
420
+		panel = buildSettlementProcessPanel(stepStatus || step.status)
436
 	} else if (type === 'invoice_archive') {
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
 	} else {
423
 	} else {
441
 		panel = buildEnterpriseCertProcessPanel(stepStatus || step.status, options)
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
 function pick(raw, camel, snake) {
7
 function pick(raw, camel, snake) {
6
 	if (!raw || typeof raw !== 'object') return null
8
 	if (!raw || typeof raw !== 'object') return null
7
 	if (raw[camel] != null && raw[camel] !== '') return raw[camel]
9
 	if (raw[camel] != null && raw[camel] !== '') return raw[camel]
@@ -24,23 +26,37 @@ export function syncRecruitGroupMeta(group) {
24
 	group.chevronText = group.expanded ? '▼' : '▶'
26
 	group.chevronText = group.expanded ? '▼' : '▶'
25
 }
27
 }
26
 
28
 
27
-/** 搜索条件(多工种逗号分隔) */
29
+/**
30
+ * 搜索条件
31
+ * 需求人数:工种*人数(如 搬运*10人,分拣*5人)
32
+ * 工作天数:工种*{x}天/月(月结用月,其余用天)
33
+ */
28
 export function buildRecruitCriteria(workerGroups = []) {
34
 export function buildRecruitCriteria(workerGroups = []) {
29
 	const groups = Array.isArray(workerGroups) ? workerGroups.filter((g) => g && g.workType) : []
35
 	const groups = Array.isArray(workerGroups) ? workerGroups.filter((g) => g && g.workType) : []
30
 	const criteriaTypes = groups.map((g) => g.workType).filter(Boolean)
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
 	const locations = []
39
 	const locations = []
33
-	const daysSet = []
34
 	groups.forEach((g) => {
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
 	return {
55
 	return {
40
 		jobTypeText: criteriaTypes.join(',') || '—',
56
 		jobTypeText: criteriaTypes.join(',') || '—',
41
-		needCountText: totalNeed > 0 ? `${totalNeed}人` : '—',
57
+		needCountText: needParts.length ? needParts.join(',') : '—',
42
 		locationText: locations.join(',') || '—',
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
 	isEnterpriseCertSkipped,
3
 	isEnterpriseCertSkipped,
4
 	isRegistrationStep,
4
 	isRegistrationStep,
5
 	isWorkerRecruitmentStep,
5
 	isWorkerRecruitmentStep,
6
-	isStaffingStep,
7
-	buildWorkerRecruitmentProcessPanel,
8
 	isTaskPublishStep,
6
 	isTaskPublishStep,
9
 	isSettlementStep,
7
 	isSettlementStep,
10
 	isInvoiceArchiveStep,
8
 	isInvoiceArchiveStep,
11
 	buildEnterpriseCertProcessPanel,
9
 	buildEnterpriseCertProcessPanel,
12
 	buildRegistrationQrProcessPanel,
10
 	buildRegistrationQrProcessPanel,
11
+	buildWorkerRecruitmentProcessPanel,
13
 	buildTaskPublishProcessPanel,
12
 	buildTaskPublishProcessPanel,
14
 	buildSettlementProcessPanel,
13
 	buildSettlementProcessPanel,
15
 	buildInvoiceArchiveProcessPanel
14
 	buildInvoiceArchiveProcessPanel
@@ -233,100 +232,17 @@ function planConfirmedOrParallelReady(steps) {
233
 	return list.some((s) => isPlanConfirmedStep(s) && s.status === 'done')
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
  * workflow-progress / registration-batch 详情 → 聊天气泡进度卡片
244
  * workflow-progress / registration-batch 详情 → 聊天气泡进度卡片
329
- * 总进度按办理步骤(排除草稿节点)计算;不展示登记人数进度
245
+ * 步骤状态与可否操作一律以流程接口 status 为准(pending / active / done)
330
  * @param {object} raw API 原始响应或已 map 的 detail
246
  * @param {object} raw API 原始响应或已 map 的 detail
331
  * @param {number|string} msgKey
247
  * @param {number|string} msgKey
332
  * @param {{ enterpriseRegistered?: boolean }} [options]
248
  * @param {{ enterpriseRegistered?: boolean }} [options]
@@ -338,29 +254,10 @@ export function buildWorkflowChatCardMessage(raw, msgKey, options = {}) {
338
 	const registered = options.enterpriseRegistered === true
254
 	const registered = options.enterpriseRegistered === true
339
 	const certSkipped = !registered && isEnterpriseCertSkipped()
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
 		return Object.assign({}, s, { status })
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
 	const qrCodeUrl = detail.qrCodeUrl || ''
262
 	const qrCodeUrl = detail.qrCodeUrl || ''
366
 	const qrToken = detail.qrToken || ''
263
 	const qrToken = detail.qrToken || ''
@@ -379,7 +276,6 @@ export function buildWorkflowChatCardMessage(raw, msgKey, options = {}) {
379
 		const isTask = isTaskPublishStep(s)
276
 		const isTask = isTaskPublishStep(s)
380
 		const isSettle = isSettlementStep(s)
277
 		const isSettle = isSettlementStep(s)
381
 		const isInvoice = isInvoiceArchiveStep(s)
278
 		const isInvoice = isInvoiceArchiveStep(s)
382
-		const actionLocked = s._actionLocked === true
383
 		// 步骤条展示名统一
279
 		// 步骤条展示名统一
384
 		let title = s.title || `步骤${i + 1}`
280
 		let title = s.title || `步骤${i + 1}`
385
 		if (isReg) title = '人员登记'
281
 		if (isReg) title = '人员登记'
@@ -415,22 +311,20 @@ export function buildWorkflowChatCardMessage(raw, msgKey, options = {}) {
415
 				qrToken,
311
 				qrToken,
416
 				registered: progressCurrent,
312
 				registered: progressCurrent,
417
 				total: progressTotal,
313
 				total: progressTotal,
418
-				highlight: s.highlight || '',
419
-				actionLocked
314
+				highlight: s.highlight || ''
420
 			})
315
 			})
421
 		} else if (isRecruit) {
316
 		} else if (isRecruit) {
422
 			processPanel = buildWorkerRecruitmentProcessPanel(status, {
317
 			processPanel = buildWorkerRecruitmentProcessPanel(status, {
423
 				registered: progressCurrent,
318
 				registered: progressCurrent,
424
 				total: progressTotal,
319
 				total: progressTotal,
425
-				highlight: s.highlight || '',
426
-				actionLocked
320
+				highlight: s.highlight || ''
427
 			})
321
 			})
428
 		} else if (isTask) {
322
 		} else if (isTask) {
429
-			processPanel = buildTaskPublishProcessPanel(status, { actionLocked })
323
+			processPanel = buildTaskPublishProcessPanel(status)
430
 		} else if (isSettle) {
324
 		} else if (isSettle) {
431
-			processPanel = buildSettlementProcessPanel(status, { actionLocked })
325
+			processPanel = buildSettlementProcessPanel(status)
432
 		} else if (isInvoice) {
326
 		} else if (isInvoice) {
433
-			processPanel = buildInvoiceArchiveProcessPanel(status, { actionLocked })
327
+			processPanel = buildInvoiceArchiveProcessPanel(status)
434
 		}
328
 		}
435
 
329
 
436
 		let processMod = ''
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
 import { formatMoney } from '@/utils/progress-action.js'
5
 import { formatMoney } from '@/utils/progress-action.js'
5
 import { normalizeWorkerGroups } from '@/utils/draft.js'
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
 	const totalOut = num(raw.totalOutflow != null ? raw.totalOutflow : raw.total_outflow)
48
 	const totalOut = num(raw.totalOutflow != null ? raw.totalOutflow : raw.total_outflow)
50
 	const netTotal = num(raw.netAmount != null ? raw.netAmount : raw.net_amount)
49
 	const netTotal = num(raw.netAmount != null ? raw.netAmount : raw.net_amount)
51
 	const workerCount = num(raw.workerCount != null ? raw.workerCount : raw.worker_count)
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
 export function mapSettlementSummary(raw = {}, ctx = {}) {
107
 export function mapSettlementSummary(raw = {}, ctx = {}) {
109
 	const fees = mapFees(raw)
108
 	const fees = mapFees(raw)
@@ -113,10 +112,7 @@ export function mapSettlementSummary(raw = {}, ctx = {}) {
113
 	const total = raw.totalOutflow != null ? raw.totalOutflow : raw.total_outflow
112
 	const total = raw.totalOutflow != null ? raw.totalOutflow : raw.total_outflow
114
 	const status = raw.settlementStatus || raw.settlement_status || ''
113
 	const status = raw.settlementStatus || raw.settlement_status || ''
115
 	const workerCount = raw.workerCount != null ? raw.workerCount : raw.worker_count
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
 	const enrichedRaw = Object.assign({}, raw, {
117
 	const enrichedRaw = Object.assign({}, raw, {
122
 		title: ctx.title || raw.title,
118
 		title: ctx.title || raw.title,