| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984 |
- <template>
- <view class="fe-bottom-sheet fe-bottom-sheet--task-publish" :class="{ 'fe-bottom-sheet--active': active }">
- <view class="fe-sheet-handle" />
- <view class="fe-sheet-header">
- <view class="fe-sheet-title">✈ 任务发布</view>
- <view class="fe-sheet-close" @tap="onClose">✕</view>
- </view>
- <view v-if="sourceTitle" class="fe-sheet-source tp-source">🔗 {{ sourceTitle }}</view>
- <view v-if="loading" class="tp-empty">
- <text class="tp-empty__text">加载任务信息中...</text>
- </view>
- <view v-else-if="loadError" class="tp-empty">
- <text class="tp-empty__text">{{ loadError }}</text>
- <view class="fe-btn fe-btn--primary fe-btn--sm" @tap="loadData">重试</view>
- </view>
- <template v-else>
- <scroll-view scroll-y class="fe-sheet-body tp-body" :show-scrollbar="false">
- <view class="fe-sheet-body__inner">
- <view class="tp-hero">
- <view class="tp-hero__icon">
- <text class="tp-hero__icon-text">✈</text>
- </view>
- <text class="tp-hero__title">{{ heroTitle }}</text>
- <text class="tp-hero__desc">{{ statusDesc }}</text>
- </view>
- <!-- 任务信息:多工种逗号拼接,单卡展示 -->
- <view class="tp-section">
- <text class="tp-section__title">任务信息</text>
- <view class="tp-card">
- <view class="tp-grid">
- <view class="tp-grid__cell">
- <text class="tp-grid__label">岗位类型</text>
- <text class="tp-grid__value">{{ taskInfoSummary.jobTypeText }}</text>
- </view>
- <view class="tp-grid__cell">
- <text class="tp-grid__label">用工人数</text>
- <text class="tp-grid__value">{{ taskInfoSummary.workerCountText }}</text>
- </view>
- <view class="tp-grid__cell">
- <text class="tp-grid__label">工作地点</text>
- <text class="tp-grid__value">{{ taskInfoSummary.locationText }}</text>
- </view>
- <view class="tp-grid__cell">
- <text class="tp-grid__label">{{ taskInfoSummary.durationLabel }}</text>
- <text class="tp-grid__value">{{ taskInfoSummary.durationText }}</text>
- </view>
- </view>
- </view>
- </view>
- <!-- 工作时间:按工种多组 -->
- <view class="tp-section">
- <text class="tp-section__title">工作时间</text>
- <view
- v-for="(g, gi) in groups"
- :key="g.timeKey"
- class="tp-card"
- >
- <text v-if="groups.length > 1" class="tp-card__badge">{{ g.workType || ('工种 ' + (gi + 1)) }}</text>
- <view class="tp-field">
- <text class="tp-field__label">开始时间 <text v-if="!readonly" class="tp-required">*</text></text>
- <view class="tp-datetime-row">
- <picker
- v-if="!readonly"
- class="tp-datetime-row__item"
- mode="date"
- :value="g.startDate"
- @change="onStartDateByIndex(gi, $event)"
- >
- <view class="tp-input" :class="{ 'tp-input--placeholder': !g.startDate }">
- <text class="tp-input__icon">📅</text>
- <text class="tp-input__text">{{ g.startDateDisplay }}</text>
- </view>
- </picker>
- <view v-else class="tp-datetime-row__item">
- <view class="tp-input tp-input--readonly">
- <text class="tp-input__icon">📅</text>
- <text class="tp-input__text">{{ g.startDateDisplay }}</text>
- </view>
- </view>
- <picker
- v-if="!readonly"
- class="tp-datetime-row__item"
- mode="time"
- :value="g.startTime || '08:00'"
- @change="onStartTimeByIndex(gi, $event)"
- >
- <view class="tp-input" :class="{ 'tp-input--placeholder': !g.startTime }">
- <text class="tp-input__icon">⏱</text>
- <text class="tp-input__text">{{ g.startTimeDisplay }}</text>
- </view>
- </picker>
- <view v-else class="tp-datetime-row__item">
- <view class="tp-input tp-input--readonly">
- <text class="tp-input__icon">⏱</text>
- <text class="tp-input__text">{{ g.startTimeDisplay }}</text>
- </view>
- </view>
- </view>
- </view>
- <view class="tp-field">
- <text class="tp-field__label">结束时间 <text v-if="!readonly" class="tp-required">*</text></text>
- <view class="tp-datetime-row">
- <picker
- v-if="!readonly"
- class="tp-datetime-row__item"
- mode="date"
- :value="g.endDate"
- @change="onEndDateByIndex(gi, $event)"
- >
- <view class="tp-input" :class="{ 'tp-input--placeholder': !g.endDate }">
- <text class="tp-input__icon">📅</text>
- <text class="tp-input__text">{{ g.endDateDisplay }}</text>
- </view>
- </picker>
- <view v-else class="tp-datetime-row__item">
- <view class="tp-input tp-input--readonly">
- <text class="tp-input__icon">📅</text>
- <text class="tp-input__text">{{ g.endDateDisplay }}</text>
- </view>
- </view>
- <picker
- v-if="!readonly"
- class="tp-datetime-row__item"
- mode="time"
- :value="g.endTime || '18:00'"
- @change="onEndTimeByIndex(gi, $event)"
- >
- <view class="tp-input" :class="{ 'tp-input--placeholder': !g.endTime }">
- <text class="tp-input__icon">⏱</text>
- <text class="tp-input__text">{{ g.endTimeDisplay }}</text>
- </view>
- </picker>
- <view v-else class="tp-datetime-row__item">
- <view class="tp-input tp-input--readonly">
- <text class="tp-input__icon">⏱</text>
- <text class="tp-input__text">{{ g.endTimeDisplay }}</text>
- </view>
- </view>
- </view>
- </view>
- </view>
- </view>
- <view v-if="readonly" class="tp-gate-tip tp-gate-tip--ok">
- <text class="tp-gate-tip__text tp-gate-tip__text--ok">任务已发布,当前仅可查看</text>
- </view>
- <view v-else-if="!canPublish" class="tp-gate-tip">
- <text class="tp-gate-tip__text">{{ gateTip }}</text>
- </view>
- <view v-else-if="!scheduleComplete" class="tp-gate-tip">
- <text class="tp-gate-tip__text">请为每个工种选择开始/结束的日期与时间</text>
- </view>
- </view>
- </scroll-view>
- <view class="fe-sheet-footer">
- <template v-if="readonly">
- <view class="fe-btn fe-btn--primary fe-btn--lg fe-btn--full" @tap="onClose">关闭</view>
- </template>
- <template v-else>
- <view class="fe-btn fe-btn--secondary fe-btn--lg" @tap="onClose">取消</view>
- <view
- v-if="confirmDisabled"
- class="fe-btn fe-btn--primary fe-btn--lg fe-btn--disabled-look"
- >{{ confirmLabel }}</view>
- <view
- v-else
- class="fe-btn fe-btn--primary fe-btn--lg"
- @tap="onConfirm"
- >{{ confirmLabel }}</view>
- </template>
- </view>
- </template>
- </view>
- </template>
- <script>
- import { showToast } from '@/common/chat-data.js'
- import { getDraftDetail, getDraftWorkflowProgress } from '@/api/draft.js'
- import { getOrderPublishTask, publishOrderTask } from '@/api/order.js'
- import {
- normalizeWorkerGroups,
- formatWorkDurationUnit,
- splitDateTimeParts,
- formatDateTimeDisplay,
- normalizeDateYmd,
- mapDraftDetailToCard
- } from '@/utils/draft.js'
- import { isTaskPublishStep } from '@/utils/process-card.js'
- import { mapRegistrationBatchDetail } from '@/utils/registration-batch.js'
- /** 规范化 HH:mm */
- function normalizeTimeHm(timeStr) {
- if (timeStr == null || timeStr === '') return ''
- const m = String(timeStr).trim().match(/^(\d{1,2}):(\d{2})/)
- if (!m) return ''
- const hh = String(Math.min(23, Math.max(0, Number(m[1])))).padStart(2, '0')
- return `${hh}:${m[2]}`
- }
- /** 拼成接口常用日期时间:YYYY-MM-DD HH:mm:ss(日期、时间均必填) */
- function joinDateTimePayload(dateStr, timeStr) {
- const d = normalizeDateYmd(dateStr)
- const t = normalizeTimeHm(timeStr)
- if (!d || !t) return ''
- return `${d} ${t}:00`
- }
- function pad2(n) {
- return String(n).padStart(2, '0')
- }
- function formatLocalYmd(d) {
- if (!d || Number.isNaN(d.getTime())) return ''
- return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`
- }
- function parseLocalYmd(ymd) {
- const s = normalizeDateYmd(ymd)
- if (!s) return null
- const d = new Date(`${s.replace(/-/g, '/')} 00:00:00`)
- return Number.isNaN(d.getTime()) ? null : d
- }
- /** 默认开始日期:明天 */
- function tomorrowYmd() {
- const d = new Date()
- d.setHours(0, 0, 0, 0)
- d.setDate(d.getDate() + 1)
- return formatLocalYmd(d)
- }
- /**
- * 按工期算结束日期(含首日):
- * - 天:开始日 + (工期天数 - 1)
- * - 月:开始日加工期月数后再减 1 天
- */
- function calcEndDateByDuration(startYmd, workDays, settlementMode) {
- const start = parseLocalYmd(startYmd)
- if (!start) return ''
- const n = Number(workDays)
- const count = !Number.isNaN(n) && n > 0 ? Math.floor(n) : 1
- const unit = formatWorkDurationUnit(settlementMode)
- const end = new Date(start.getTime())
- if (unit === '月') {
- end.setMonth(end.getMonth() + count)
- end.setDate(end.getDate() - 1)
- } else {
- end.setDate(end.getDate() + (count - 1))
- }
- return formatLocalYmd(end)
- }
- function modalConfirm(title, content) {
- return new Promise((resolve) => {
- uni.showModal({
- title: title || '提示',
- content: content || '',
- success: (res) => resolve(!!(res && res.confirm)),
- fail: () => resolve(false)
- })
- })
- }
- export default {
- name: 'FeTaskPublishSheet',
- props: {
- active: { type: Boolean, default: false },
- draftId: { type: [Number, String], default: null },
- orderId: { type: [Number, String], default: null },
- /** 流程已完成:仅查看,禁止发布 */
- readonly: { type: Boolean, default: false }
- },
- data() {
- return {
- loading: false,
- loadError: '',
- publishing: false,
- sourceTitle: '',
- groups: [],
- statusDesc: '请确认任务信息后发布',
- orderIdInner: null,
- /** 流程接口任务发布步骤:pending | active | done */
- taskStepStatus: 'pending',
- /** 已登记 < 计划人数,发布时需 allow_understaffed */
- understaffed: false,
- registeredCount: 0,
- plannedCount: 0
- }
- },
- computed: {
- heroTitle() {
- return this.readonly ? '任务发布详情' : '确认发布任务'
- },
- canPublish() {
- return this.taskStepStatus === 'active'
- },
- scheduleComplete() {
- const list = this.groups || []
- if (!list.length) return false
- for (let i = 0; i < list.length; i++) {
- const g = list[i]
- if (!normalizeDateYmd(g.startDate) || !normalizeTimeHm(g.startTime)) return false
- if (!normalizeDateYmd(g.endDate) || !normalizeTimeHm(g.endTime)) return false
- }
- return true
- },
- gateTip() {
- if (this.taskStepStatus === 'done') return '任务已发布'
- if (this.taskStepStatus !== 'active') return '当前步骤尚未开始,请等待流程推进到任务发布'
- return ''
- },
- confirmDisabled() {
- return this.readonly || !this.canPublish || !this.scheduleComplete || this.publishing || this.loading
- },
- confirmLabel() {
- if (this.readonly) return '已发布'
- if (this.publishing) return '发布中...'
- return '确认发布'
- },
- /** 任务信息汇总:多工种逗号拼接,单卡展示 */
- taskInfoSummary() {
- const list = this.groups || []
- if (!list.length) {
- return {
- jobTypeText: '—',
- workerCountText: '—',
- locationText: '—',
- durationLabel: '工作天数',
- durationText: '—'
- }
- }
- const types = []
- const counts = []
- const locations = []
- const durations = []
- let hasMonth = false
- let hasDay = false
- for (let i = 0; i < list.length; i++) {
- const g = list[i]
- const type = String(g.workType || '').trim() || '待确认'
- types.push(type)
- if (g.workerCount != null && g.workerCount !== '') {
- counts.push(`${type}*${g.workerCount}人`)
- }
- const loc = String(g.workLocation || '').trim()
- if (loc && locations.indexOf(loc) < 0) locations.push(loc)
- const unit = formatWorkDurationUnit(g.settlementMode)
- if (unit === '月') hasMonth = true
- else hasDay = true
- if (g.workDays != null && g.workDays !== '') {
- durations.push(`${type}*${g.workDays}${unit}`)
- }
- }
- let durationLabel = '工作天数'
- if (hasMonth && !hasDay) durationLabel = '工作月数'
- else if (hasMonth && hasDay) durationLabel = '工作工期'
- return {
- jobTypeText: types.join(',') || '—',
- workerCountText: counts.length ? counts.join(',') : '—',
- locationText: locations.join(',') || '—',
- durationLabel,
- durationText: durations.length ? durations.join(',') : '—'
- }
- }
- },
- watch: {
- active(val) {
- if (val) this.loadData()
- },
- draftId() {
- if (this.active) this.loadData()
- },
- orderId() {
- if (this.active) this.loadData()
- },
- readonly() {
- if (this.active) this.loadData()
- }
- },
- methods: {
- onClose() {
- this.$emit('close')
- },
- pickField(raw, camel, snake) {
- if (!raw || typeof raw !== 'object') return null
- if (raw[camel] != null && raw[camel] !== '') return raw[camel]
- if (raw[snake] != null && raw[snake] !== '') return raw[snake]
- return null
- },
- /** GET publish-task → 工种行 */
- mapPublishDetailWorkItems(raw) {
- const items = (raw && (raw.workItems || raw.work_items)) || []
- const list = Array.isArray(items) ? items : []
- const groups = list.map((item, idx) => ({
- id: item.id != null ? item.id : null,
- groupId: this.pickField(item, 'groupNo', 'group_no') || `g_${idx}`,
- workType: this.pickField(item, 'jobType', 'job_type') || '待确认',
- workLocation: this.pickField(item, 'workLocation', 'work_location') || '待确认',
- workerCount: this.pickField(item, 'workerCount', 'worker_count'),
- workDays: this.pickField(item, 'workDays', 'work_days'),
- settlementMode: this.pickField(item, 'settlementCycle', 'settlement_cycle') || '',
- workStartDate: this.pickField(item, 'workStartDate', 'work_start_date') || '',
- workEndDate: this.pickField(item, 'workEndDate', 'work_end_date') || ''
- }))
- return this.buildGroupRows(groups, { viewMode: true })
- },
- buildGroupRows(groups, options = {}) {
- const viewMode = options.viewMode === true
- const emptyDate = viewMode ? '—' : '选择日期'
- const emptyTime = viewMode ? '—' : '选择时间'
- const list = Array.isArray(groups) ? groups : []
- return list.map((g, idx) => {
- const startParts = splitDateTimeParts(g.workStartDate)
- const endParts = splitDateTimeParts(g.workEndDate)
- const unit = formatWorkDurationUnit(g.settlementMode)
- const days = g.workDays
- let startDate = normalizeDateYmd(startParts.date)
- let endDate = normalizeDateYmd(endParts.date)
- // 接口返回若含时间,统一赋成 HH:mm;编辑态无时间时用默认
- let startTime = startParts.time ? normalizeTimeHm(startParts.time) : ''
- let endTime = endParts.time ? normalizeTimeHm(endParts.time) : ''
- if (!viewMode) {
- if (!startDate) startDate = tomorrowYmd()
- if (!startTime) startTime = '08:00'
- if (!endTime) endTime = '18:00'
- if (!endDate) endDate = calcEndDateByDuration(startDate, days, g.settlementMode)
- }
- return {
- rowKey: g.groupId || `info_${idx}`,
- timeKey: `time_${g.groupId || idx}`,
- workItemId: g.id != null ? g.id : null,
- groupId: g.groupId || `g_${idx}`,
- workType: g.workType || '待确认',
- workLocation: g.workLocation || '待确认',
- workerCount: g.workerCount,
- workerCountText: g.workerCount != null ? `${g.workerCount}人` : '—',
- durationLabel: unit === '月' ? '工作月数' : '工作天数',
- durationText: days != null ? `${days}${unit}` : '—',
- workDays: days,
- settlementMode: g.settlementMode || '',
- startDate,
- startTime,
- endDate,
- endTime,
- startDateDisplay: startDate || emptyDate,
- endDateDisplay: endDate || emptyDate,
- startTimeDisplay: startTime || emptyTime,
- endTimeDisplay: endTime || emptyTime,
- startDisplay: formatDateTimeDisplay(startDate, startTime),
- endDisplay: formatDateTimeDisplay(endDate, endTime)
- }
- })
- },
- async resolveOrderIdFromDraft() {
- if (this.orderId != null && this.orderId !== '') return this.orderId
- if (this.draftId == null || this.draftId === '') return null
- try {
- const draftRaw = await getDraftDetail(this.draftId, { showError: false })
- if (!draftRaw) return null
- return draftRaw.orderId != null && draftRaw.orderId !== ''
- ? draftRaw.orderId
- : (draftRaw.order_id != null && draftRaw.order_id !== '' ? draftRaw.order_id : null)
- } catch (e) {
- return null
- }
- },
- /** 已完成「查看发布」:GET /orders/{id}/publish-task */
- async loadPublishDetail() {
- this.loading = true
- this.loadError = ''
- this.understaffed = false
- try {
- const orderId = await this.resolveOrderIdFromDraft()
- this.orderIdInner = orderId
- if (orderId == null || orderId === '') {
- this.loadError = '缺少订单信息'
- this.groups = []
- return
- }
- const raw = await getOrderPublishTask(orderId, { showError: false })
- const title = this.pickField(raw, 'title', 'title') || '用工任务'
- const orderNo = this.pickField(raw, 'orderNo', 'order_no') || ''
- this.sourceTitle = orderNo ? `${title} ${orderNo}` : title
- this.groups = this.mapPublishDetailWorkItems(raw || {})
- const planned = Number(this.pickField(raw, 'plannedCount', 'planned_count') || 0)
- const registered = Number(this.pickField(raw, 'registeredCount', 'registered_count') || 0)
- this.plannedCount = planned
- this.registeredCount = registered
- this.understaffed = planned > 0 && registered < planned
- this.registrationDone = true
- const msg = (raw && raw.message) || ''
- if (msg) {
- this.statusDesc = msg
- } else if (planned > 0) {
- this.statusDesc = `已登记 ${registered}/${planned},任务已发布`
- } else {
- this.statusDesc = '任务已发布,当前仅可查看'
- }
- } catch (e) {
- this.loadError = (e && (e.msg || e.message)) || '任务发布详情加载失败'
- this.groups = []
- showToast(this.loadError)
- } finally {
- this.loading = false
- }
- },
- syncGroupDisplay(gi) {
- const g = this.groups[gi]
- if (!g) return
- const startDate = normalizeDateYmd(g.startDate)
- const endDate = normalizeDateYmd(g.endDate)
- const startTime = normalizeTimeHm(g.startTime)
- const endTime = normalizeTimeHm(g.endTime)
- this.$set(g, 'startDate', startDate)
- this.$set(g, 'endDate', endDate)
- this.$set(g, 'startTime', startTime)
- this.$set(g, 'endTime', endTime)
- this.$set(g, 'startDateDisplay', startDate || '选择日期')
- this.$set(g, 'endDateDisplay', endDate || '选择日期')
- this.$set(g, 'startTimeDisplay', startTime || '选择时间')
- this.$set(g, 'endTimeDisplay', endTime || '选择时间')
- this.$set(g, 'startDisplay', formatDateTimeDisplay(startDate, startTime))
- this.$set(g, 'endDisplay', formatDateTimeDisplay(endDate, endTime))
- },
- onStartDateByIndex(gi, e) {
- if (this.readonly) return
- const v = normalizeDateYmd(e && e.detail ? e.detail.value : '')
- const g = this.groups[gi]
- if (!g) return
- this.$set(g, 'startDate', v)
- // 改开始日时,按工期重算结束日
- const end = calcEndDateByDuration(v, g.workDays, g.settlementMode)
- if (end) this.$set(g, 'endDate', end)
- if (!normalizeTimeHm(g.startTime)) this.$set(g, 'startTime', '08:00')
- if (!normalizeTimeHm(g.endTime)) this.$set(g, 'endTime', '18:00')
- this.syncGroupDisplay(gi)
- },
- onStartTimeByIndex(gi, e) {
- if (this.readonly) return
- const v = normalizeTimeHm(e && e.detail ? e.detail.value : '')
- const g = this.groups[gi]
- if (!g) return
- this.$set(g, 'startTime', v)
- this.syncGroupDisplay(gi)
- },
- onEndDateByIndex(gi, e) {
- if (this.readonly) return
- const v = normalizeDateYmd(e && e.detail ? e.detail.value : '')
- const g = this.groups[gi]
- if (!g) return
- this.$set(g, 'endDate', v)
- this.syncGroupDisplay(gi)
- },
- onEndTimeByIndex(gi, e) {
- if (this.readonly) return
- const v = normalizeTimeHm(e && e.detail ? e.detail.value : '')
- const g = this.groups[gi]
- if (!g) return
- this.$set(g, 'endTime', v)
- this.syncGroupDisplay(gi)
- },
- validateSchedule() {
- const list = this.groups || []
- if (!list.length) return '暂无工种信息,无法发布'
- for (let i = 0; i < list.length; i++) {
- const g = list[i]
- const prefix = list.length > 1 ? `「${g.workType || ('工种' + (i + 1))}」` : ''
- if (!normalizeDateYmd(g.startDate)) return `${prefix}请选择开始日期`
- if (!normalizeTimeHm(g.startTime)) return `${prefix}请选择开始时间`
- if (!normalizeDateYmd(g.endDate)) return `${prefix}请选择结束日期`
- if (!normalizeTimeHm(g.endTime)) return `${prefix}请选择结束时间`
- const start = joinDateTimePayload(g.startDate, g.startTime)
- const end = joinDateTimePayload(g.endDate, g.endTime)
- if (start && end && start > end) return `${prefix}结束时间不能早于开始时间`
- }
- return ''
- },
- resolveTaskStepStatus(workflowRaw) {
- const detail = mapRegistrationBatchDetail(workflowRaw || {})
- const steps = detail.steps || []
- const task = steps.find((s) => isTaskPublishStep(s))
- if (!task) return 'pending'
- const s = String(task.status || 'pending').toLowerCase()
- if (s === 'done' || s === 'completed' || s === 'finish' || s === 'finished') return 'done'
- if (s === 'active' || s === 'current' || s === 'in_progress' || s === 'processing') return 'active'
- return 'pending'
- },
- buildStatusDesc(workflowRaw) {
- const detail = mapRegistrationBatchDetail(workflowRaw || {})
- const current = (detail.progress && detail.progress.current) || 0
- const total = (detail.progress && detail.progress.total) || 0
- if (this.canPublish) {
- if (total > 0 && current >= total) return `${total}人已全部登记,可发布任务`
- if (total > 0 && current < total) {
- return `已登记 ${current}/${total},未招满发布需确认缺编`
- }
- return '流程已到任务发布,可确认发布'
- }
- if (this.taskStepStatus === 'done') return '任务已发布'
- if (total > 0) return `已登记 ${current}/${total},等待流程推进到任务发布`
- return '等待流程推进到任务发布'
- },
- syncStaffingFromWorkflow(workflowRaw) {
- const detail = mapRegistrationBatchDetail(workflowRaw || {})
- const current = (detail.progress && detail.progress.current) || 0
- const total = (detail.progress && detail.progress.total) || 0
- this.registeredCount = current
- this.plannedCount = total
- this.understaffed = total > 0 && current < total
- },
- /**
- * TaskPublishRequest:
- * { work_items: [{ id?, group_no?, job_type?, work_start_date, work_end_date }], allow_understaffed? }
- * work_start_date / work_end_date:YYYY-MM-DD HH:mm:ss(日期+时间拼成一个字段)
- */
- buildPublishPayload(allowUnderstaffed = false) {
- const workItems = (this.groups || []).map((g) => {
- const start = joinDateTimePayload(g.startDate, g.startTime)
- const end = joinDateTimePayload(g.endDate, g.endTime)
- const item = {
- work_start_date: start,
- work_end_date: end
- }
- if (g.workItemId != null && g.workItemId !== '') {
- item.id = Number(g.workItemId)
- }
- if (g.groupId) {
- item.group_no = String(g.groupId)
- }
- if (g.workType && g.workType !== '待确认') {
- item.job_type = String(g.workType)
- }
- return item
- })
- return {
- work_items: workItems,
- allow_understaffed: !!allowUnderstaffed
- }
- },
- async loadData() {
- // 已完成「查看发布」走任务发布详情接口
- if (this.readonly) {
- await this.loadPublishDetail()
- return
- }
- if (this.draftId == null || this.draftId === '') {
- this.loadError = '缺少草稿信息'
- this.groups = []
- return
- }
- this.loading = true
- this.loadError = ''
- this.understaffed = false
- this.registeredCount = 0
- this.plannedCount = 0
- try {
- const draftRaw = await getDraftDetail(this.draftId, { showError: false })
- const card = mapDraftDetailToCard(draftRaw || {})
- const groups = normalizeWorkerGroups(draftRaw || {})
- this.groups = this.buildGroupRows(groups.length ? groups : (card.workerGroups || []))
- const batchNo = (draftRaw && (draftRaw.draftNo || draftRaw.draft_no)) || card.draftNo || ''
- const title = (card && card.title) || (draftRaw && draftRaw.title) || '用工任务'
- this.sourceTitle = batchNo ? `${title} ${batchNo}` : title
- // 订单 ID 仅从草稿详情 order_id / orderId 取值
- const orderId = draftRaw && (draftRaw.orderId != null && draftRaw.orderId !== ''
- ? draftRaw.orderId
- : (draftRaw.order_id != null && draftRaw.order_id !== '' ? draftRaw.order_id : null))
- this.orderIdInner = orderId
- let workflowRaw = null
- try {
- workflowRaw = await getDraftWorkflowProgress(this.draftId, { showError: false })
- } catch (e) {
- workflowRaw = null
- }
- this.syncStaffingFromWorkflow(workflowRaw)
- this.taskStepStatus = this.resolveTaskStepStatus(workflowRaw)
- this.statusDesc = this.buildStatusDesc(workflowRaw)
- } catch (e) {
- this.loadError = (e && (e.msg || e.message)) || '任务信息加载失败'
- this.groups = []
- showToast(this.loadError)
- } finally {
- this.loading = false
- }
- },
- async onConfirm() {
- if (this.readonly) {
- showToast('任务已发布,仅可查看')
- return
- }
- if (this.confirmDisabled) return
- if (!this.canPublish) {
- showToast(this.gateTip || '暂不可发布')
- return
- }
- const orderId = this.orderIdInner
- if (orderId == null || orderId === '') {
- showToast('缺少订单信息,暂无法发布')
- return
- }
- const scheduleErr = this.validateSchedule()
- if (scheduleErr) {
- showToast(scheduleErr)
- return
- }
- let allowUnderstaffed = false
- if (this.understaffed) {
- const tip = `已登记 ${this.registeredCount}/${this.plannedCount},未招满。是否缺编发布?`
- const ok = await modalConfirm('确认缺编发布', tip)
- if (!ok) return
- allowUnderstaffed = true
- } else {
- const ok = await modalConfirm('确认发布', '确认后将发布用工任务,是否继续?')
- if (!ok) return
- }
- this.publishing = true
- try {
- const res = await publishOrderTask(
- orderId,
- this.buildPublishPayload(allowUnderstaffed),
- { showError: true }
- )
- showToast((res && (res.message || res.msg)) || '任务已发布')
- const nextOrderId = (res && (res.orderId != null ? res.orderId : res.order_id)) || orderId
- this.$emit('success', {
- orderId: nextOrderId,
- draftId: this.draftId,
- result: res
- })
- this.onClose()
- } catch (e) {
- if (e && e.__handled) return
- } finally {
- this.publishing = false
- }
- }
- }
- }
- </script>
- <style lang="scss" scoped>
- .fe-bottom-sheet--task-publish {
- max-height: 88vh;
- }
- .fe-sheet-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- }
- .fe-sheet-close {
- width: 56rpx;
- height: 56rpx;
- border-radius: 50%;
- background: $fe-gray-100;
- color: $fe-muted;
- font-size: 28rpx;
- display: flex;
- align-items: center;
- justify-content: center;
- }
- .tp-source {
- padding: 0 40rpx 8rpx;
- }
- .tp-body {
- max-height: 58vh;
- }
- .tp-empty {
- padding: 80rpx 32rpx;
- text-align: center;
- }
- .tp-empty__text {
- display: block;
- font-size: 28rpx;
- color: #94A3B8;
- margin-bottom: 24rpx;
- }
- .tp-hero {
- display: flex;
- flex-direction: column;
- align-items: center;
- padding: 12rpx 0 28rpx;
- }
- .tp-hero__icon {
- width: 112rpx;
- height: 112rpx;
- border-radius: 50%;
- background: #FEF3C7;
- display: flex;
- align-items: center;
- justify-content: center;
- margin-bottom: 20rpx;
- }
- .tp-hero__icon-text {
- font-size: 52rpx;
- line-height: 1;
- }
- .tp-hero__title {
- font-size: 34rpx;
- font-weight: 700;
- color: $fe-foreground;
- margin-bottom: 8rpx;
- }
- .tp-hero__desc {
- font-size: 24rpx;
- color: $fe-muted;
- text-align: center;
- line-height: 1.5;
- padding: 0 24rpx;
- }
- .tp-section {
- margin-bottom: 28rpx;
- }
- .tp-section__title {
- display: block;
- font-size: 28rpx;
- font-weight: 700;
- color: $fe-foreground;
- margin-bottom: 16rpx;
- }
- .tp-card {
- background: $fe-gray-50;
- border-radius: $fe-radius-md;
- padding: 24rpx;
- margin-bottom: 16rpx;
- border: 1rpx solid $fe-border-light;
- box-sizing: border-box;
- }
- .tp-card__badge {
- display: block;
- font-size: 22rpx;
- font-weight: 600;
- color: $fe-primary;
- margin-bottom: 16rpx;
- }
- .tp-grid {
- display: flex;
- flex-wrap: wrap;
- }
- .tp-grid__cell {
- width: 50%;
- box-sizing: border-box;
- padding: 10rpx 12rpx 10rpx 0;
- }
- .tp-grid__label {
- display: block;
- font-size: 22rpx;
- color: $fe-muted;
- margin-bottom: 6rpx;
- }
- .tp-grid__value {
- display: block;
- font-size: 28rpx;
- font-weight: 700;
- color: $fe-foreground;
- word-break: break-all;
- }
- .tp-field {
- margin-bottom: 20rpx;
- }
- .tp-field:last-child {
- margin-bottom: 0;
- }
- .tp-field__label {
- display: block;
- font-size: 24rpx;
- color: $fe-muted;
- margin-bottom: 10rpx;
- }
- .tp-required {
- color: #EF4444;
- font-weight: 700;
- }
- .tp-datetime-row {
- display: flex;
- align-items: stretch;
- gap: 16rpx;
- width: 100%;
- box-sizing: border-box;
- }
- .tp-datetime-row__item {
- flex: 1;
- min-width: 0;
- width: 0;
- }
- .tp-input {
- display: flex;
- align-items: center;
- gap: 8rpx;
- min-height: 80rpx;
- padding: 0 16rpx;
- background: $fe-surface;
- border: 1rpx solid $fe-border;
- border-radius: $fe-radius-md;
- box-sizing: border-box;
- }
- .tp-input__icon {
- font-size: 28rpx;
- line-height: 1;
- flex-shrink: 0;
- }
- .tp-input__text {
- flex: 1;
- min-width: 0;
- font-size: 26rpx;
- font-weight: 600;
- color: $fe-foreground;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- }
- .tp-input--placeholder .tp-input__text {
- color: #94A3B8;
- font-weight: 500;
- }
- .tp-gate-tip {
- padding: 16rpx 20rpx;
- background: rgba(245, 158, 11, 0.1);
- border-radius: $fe-radius-md;
- margin-bottom: 12rpx;
- }
- .tp-gate-tip--ok {
- background: rgba(16, 185, 129, 0.1);
- }
- .tp-gate-tip__text {
- font-size: 24rpx;
- color: #B45309;
- line-height: 1.5;
- }
- .tp-gate-tip__text--ok {
- color: #047857;
- }
- .tp-input--readonly {
- background: $fe-gray-100;
- }
- .fe-btn--disabled-look {
- opacity: 0.42;
- pointer-events: none;
- }
- </style>
|