858 lines
27 KiB
Vue
858 lines
27 KiB
Vue
<!-- 发起审批 / 草稿编辑共用弹窗 -->
|
||
<template>
|
||
<el-dialog
|
||
v-model="dlgVisible"
|
||
:title="picked?.appName || '发起审批'"
|
||
width="920px"
|
||
class="dialog-form-layout"
|
||
align-center
|
||
append-to-body
|
||
modal-class="protocol-dialog-mask"
|
||
destroy-on-close
|
||
:close-on-click-modal="false"
|
||
@closed="onDialogClosed"
|
||
>
|
||
<div v-loading="detailLoading">
|
||
<form-create
|
||
v-if="formRule.length"
|
||
v-model="formData"
|
||
v-model:api="fapi"
|
||
:rule="formRule"
|
||
:option="formOption"
|
||
/>
|
||
<p v-if="confirmationDateManualFields.length" class="oa-start__tip oa-start__confirm-relink">
|
||
<el-button link type="primary" @click="restoreConfirmationDateAuto">
|
||
恢复转正日期自动计算
|
||
</el-button>
|
||
</p>
|
||
<p v-if="leaveDurationManualFields.length" class="oa-start__tip">
|
||
<el-button link type="primary" @click="restoreLeaveAuto">
|
||
<el-icon><RefreshRight /></el-icon>
|
||
恢复请假时长自动计算
|
||
</el-button>
|
||
</p>
|
||
<p v-if="overtimeDurationManualFields.length" class="oa-start__tip">
|
||
<el-button link type="primary" @click="restoreOvertimeAuto">
|
||
<el-icon><RefreshRight /></el-icon>
|
||
恢复加班时长自动计算
|
||
</el-button>
|
||
</p>
|
||
<p v-if="tripDaysManualFields.length" class="oa-start__tip">
|
||
<el-button link type="primary" @click="restoreTripDaysAuto">
|
||
<el-icon><RefreshRight /></el-icon>
|
||
恢复出差天数自动计算
|
||
</el-button>
|
||
</p>
|
||
<p v-if="!formRule.length && !detailLoading" class="oa-start__tip">
|
||
该模板暂无表单字段,仍可直接提交。
|
||
</p>
|
||
<OaFlowPreview
|
||
v-if="!detailLoading"
|
||
v-model:select-by-node="initiatorSelectByNode"
|
||
:steps="flowSteps"
|
||
:loading="previewLoading"
|
||
:resolved="flowResolved"
|
||
:pending-message="flowPendingMessage"
|
||
:user-options="userOptions"
|
||
@select-change="scheduleFlowPreview"
|
||
/>
|
||
</div>
|
||
<template #footer>
|
||
<el-button class="btn-ghost" @click="closeDialog">
|
||
<el-icon><Close /></el-icon>
|
||
取消
|
||
</el-button>
|
||
<el-button class="btn-ghost" :loading="savingDraft" @click="saveDraft">
|
||
<el-icon><Document /></el-icon>
|
||
{{ editingDraftId ? '保存草稿' : '暂存草稿' }}
|
||
</el-button>
|
||
<el-button class="btn-main" type="primary" :loading="submitting" @click="submit">
|
||
<el-icon><Check /></el-icon>
|
||
提交
|
||
</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { computed, ref, watch } from 'vue'
|
||
import { Check, Close, Document, RefreshRight } from '@element-plus/icons-vue'
|
||
import { ElMessage } from 'element-plus'
|
||
import { fetchUserList } from '@/api/admin'
|
||
import { fetchCnHolidayRange } from '@/api/cnHoliday'
|
||
import {
|
||
fetchCreateOaInstanceDraft,
|
||
fetchOaFlowPreview,
|
||
fetchOaInstanceDetail,
|
||
fetchOaInstanceDraftDetail,
|
||
fetchOaTemplateDetail,
|
||
fetchStartOaInstance,
|
||
fetchSubmitOaInstanceDraft,
|
||
fetchUpdateOaInstanceDraft,
|
||
type OaFlowPreviewStep
|
||
} from '@/api/oaApproval'
|
||
import OaFlowPreview from '@/components/oa-approval/OaFlowPreview.vue'
|
||
import { MAX_ADMIN_LIST_PAGE_SIZE } from '@/utils/fetch-paged-list'
|
||
import { OA_FC_DEFAULT_GUTTER, ensureOaUploadRuleProps } from '@/utils/oa-form-create-upload'
|
||
import {
|
||
applyLeaveKitDurationsToFormData,
|
||
ensureLeaveKitRuleProps,
|
||
LEAVE_MANUAL_PREFIX,
|
||
listLeaveKitBindings,
|
||
markLeaveDurationManual
|
||
} from '@/utils/oa-leave-kit-runtime'
|
||
import {
|
||
applyOvertimeKitDefaults,
|
||
applyOvertimeKitDurationsToFormData,
|
||
ensureOvertimeKitRuleProps,
|
||
listOvertimeKitBindings,
|
||
markOvertimeDurationManual,
|
||
OVERTIME_MANUAL_PREFIX
|
||
} from '@/utils/oa-overtime-kit-runtime'
|
||
import { applyResignationKitDefaults } from '@/utils/oa-resignation-kit-runtime'
|
||
import {
|
||
applyConfirmationKitDateLinkage,
|
||
applyConfirmationKitDefaults,
|
||
clearConfirmationDateManualAndRelink,
|
||
CONFIRMATION_MANUAL_PREFIX,
|
||
ensureConfirmationKitVisibility,
|
||
listConfirmationKitBindings,
|
||
listManualConfirmationDateFields,
|
||
markConfirmationDateManual
|
||
} from '@/utils/oa-confirmation-kit-runtime'
|
||
import {
|
||
applyOwnerFieldDefaults,
|
||
ensureOwnerFieldRuleProps
|
||
} from '@/utils/oa-owner-field-defaults'
|
||
import {
|
||
applyTripKitDaysToFormData,
|
||
listTripKits,
|
||
TRIP_MANUAL_PREFIX
|
||
} from '@/utils/oa-trip-kit-runtime'
|
||
import {
|
||
buildFlowPreviewRequestKey,
|
||
collectConditionFields,
|
||
shouldShowFlowPreviewSpinner,
|
||
shouldSkipFlowPreviewRequest
|
||
} from '@/utils/oa-flow-preview-refresh'
|
||
import { useUserStore } from '@/store/modules/user'
|
||
|
||
defineOptions({ name: 'OaStartFormDialog' })
|
||
|
||
const emit = defineEmits<{
|
||
saved: []
|
||
submitted: [instance: any]
|
||
closed: []
|
||
}>()
|
||
|
||
const userStore = useUserStore()
|
||
const dlgVisible = ref(false)
|
||
const detailLoading = ref(false)
|
||
const picked = ref<any>(null)
|
||
const formData = ref<Record<string, any>>({})
|
||
const formRule = ref<any[]>([])
|
||
const formOption = ref<Record<string, any>>({
|
||
submitBtn: false,
|
||
resetBtn: false,
|
||
row: { gutter: OA_FC_DEFAULT_GUTTER }
|
||
})
|
||
const fapi = ref<any>(null)
|
||
const submitting = ref(false)
|
||
const savingDraft = ref(false)
|
||
const editingDraftId = ref('')
|
||
const needInitiatorSelect = ref(false)
|
||
const initiatorSelectByNode = ref<Record<string, string[]>>({})
|
||
const userOptions = ref<Array<{ id: string; nickName: string; userNo: string }>>([])
|
||
const flowSteps = ref<OaFlowPreviewStep[]>([])
|
||
const flowResolved = ref(true)
|
||
const flowPendingMessage = ref('必填信息填写后,流程将自动显示')
|
||
const previewLoading = ref(false)
|
||
let previewTimer: ReturnType<typeof setTimeout> | null = null
|
||
/** 流程条件字段:仅这些 formData 变化才重拉预览 */
|
||
let flowConditionFields: string[] = []
|
||
let lastFlowPreviewRequestKey: string | null = null
|
||
let inFlightFlowPreviewKey: string | null = null
|
||
let hasCompletedFlowPreview = false
|
||
let flowPreviewReqSeq = 0
|
||
/** 避免时长自动回写再次触发手改判定 */
|
||
let leaveKitSyncing = false
|
||
let leaveKitPrevSnapshot: Record<string, unknown> = {}
|
||
let tripKitSyncing = false
|
||
let tripKitPrevSnapshot: Record<string, unknown> = {}
|
||
let confirmationKitSyncing = false
|
||
let confirmationKitPrevSnapshot: Record<string, unknown> = {}
|
||
let overtimeKitSyncing = false
|
||
let overtimeKitPrevSnapshot: Record<string, unknown> = {}
|
||
const overtimeRestMap: Record<string, boolean> = {}
|
||
|
||
const confirmationDateManualFields = computed(() =>
|
||
listManualConfirmationDateFields({ rule: formRule.value }, formData.value)
|
||
)
|
||
|
||
const leaveDurationManualFields = computed(() => {
|
||
const fields: string[] = []
|
||
for (const kit of listLeaveKitBindings({ rule: formRule.value })) {
|
||
if (formData.value[LEAVE_MANUAL_PREFIX + kit.durationField]) {
|
||
fields.push(kit.durationField)
|
||
}
|
||
}
|
||
return fields
|
||
})
|
||
|
||
const overtimeDurationManualFields = computed(() => {
|
||
const fields: string[] = []
|
||
for (const kit of listOvertimeKitBindings({ rule: formRule.value })) {
|
||
if (formData.value[OVERTIME_MANUAL_PREFIX + kit.durationField]) {
|
||
fields.push(kit.durationField)
|
||
}
|
||
}
|
||
return fields
|
||
})
|
||
|
||
const tripDaysManualFields = computed(() => {
|
||
const fields: string[] = []
|
||
for (const kit of listTripKits({ rule: formRule.value })) {
|
||
if (formData.value[TRIP_MANUAL_PREFIX + kit.daysField]) {
|
||
fields.push(kit.daysField)
|
||
}
|
||
}
|
||
return fields
|
||
})
|
||
|
||
function flowNeedsInitiatorSelect(node: any): boolean {
|
||
if (!node || typeof node !== 'object') return false
|
||
const cfg = node.approverConfig || {}
|
||
if ((cfg.assigneeType || '') === 'initiator_select') return true
|
||
if (node.child && flowNeedsInitiatorSelect(node.child)) return true
|
||
for (const g of node.conditionGroups || []) {
|
||
if (g?.child && flowNeedsInitiatorSelect(g.child)) return true
|
||
}
|
||
return false
|
||
}
|
||
|
||
async function loadUsers() {
|
||
try {
|
||
const pageSize = MAX_ADMIN_LIST_PAGE_SIZE
|
||
const raw: any[] = []
|
||
let page = 1
|
||
for (;;) {
|
||
const res = await fetchUserList({ page, pageSize, status: '1' })
|
||
const list = res?.list || []
|
||
raw.push(...list)
|
||
if (list.length < pageSize) break
|
||
if (typeof res?.total === 'number' && raw.length >= res.total) break
|
||
page += 1
|
||
}
|
||
userOptions.value = raw.map((u) => ({
|
||
id: u.id,
|
||
nickName: u.nickName || '',
|
||
userNo: u.userNo || ''
|
||
}))
|
||
} catch {
|
||
userOptions.value = []
|
||
}
|
||
}
|
||
|
||
async function loadFlowPreview(appId: string) {
|
||
if (!appId) return
|
||
const requestKey = buildFlowPreviewRequestKey({
|
||
formData: formData.value,
|
||
initiatorSelectByNode: initiatorSelectByNode.value,
|
||
conditionFields: flowConditionFields
|
||
})
|
||
if (
|
||
shouldSkipFlowPreviewRequest(lastFlowPreviewRequestKey, requestKey, {
|
||
inFlightKey: inFlightFlowPreviewKey
|
||
})
|
||
) {
|
||
return
|
||
}
|
||
const seq = ++flowPreviewReqSeq
|
||
inFlightFlowPreviewKey = requestKey
|
||
const showSpinner = shouldShowFlowPreviewSpinner({
|
||
hasCompletedPreview: hasCompletedFlowPreview
|
||
})
|
||
if (showSpinner) previewLoading.value = true
|
||
try {
|
||
const preview = await fetchOaFlowPreview(appId, {
|
||
formData: { ...formData.value },
|
||
initiatorSelectByNode: { ...initiatorSelectByNode.value }
|
||
})
|
||
if (seq !== flowPreviewReqSeq) return
|
||
lastFlowPreviewRequestKey = requestKey
|
||
flowResolved.value = preview?.resolved !== false
|
||
flowPendingMessage.value = preview?.message || '必填信息填写后,流程将自动显示'
|
||
flowSteps.value = flowResolved.value ? preview?.steps || [] : []
|
||
} catch {
|
||
if (seq !== flowPreviewReqSeq) return
|
||
flowSteps.value = []
|
||
flowResolved.value = true
|
||
} finally {
|
||
if (seq === flowPreviewReqSeq) {
|
||
inFlightFlowPreviewKey = null
|
||
hasCompletedFlowPreview = true
|
||
previewLoading.value = false
|
||
}
|
||
}
|
||
}
|
||
|
||
function resetFlowPreviewState(flowRoot: unknown) {
|
||
flowConditionFields = collectConditionFields(flowRoot)
|
||
lastFlowPreviewRequestKey = null
|
||
inFlightFlowPreviewKey = null
|
||
hasCompletedFlowPreview = false
|
||
flowPreviewReqSeq += 1
|
||
}
|
||
|
||
function scheduleFlowPreview() {
|
||
if (!dlgVisible.value || !picked.value?.id) return
|
||
if (previewTimer) clearTimeout(previewTimer)
|
||
previewTimer = setTimeout(() => {
|
||
loadFlowPreview(picked.value.id)
|
||
}, 300)
|
||
}
|
||
|
||
async function onPick(t: any) {
|
||
editingDraftId.value = ''
|
||
picked.value = t
|
||
formData.value = {}
|
||
formRule.value = []
|
||
leaveKitPrevSnapshot = {}
|
||
tripKitPrevSnapshot = {}
|
||
confirmationKitPrevSnapshot = {}
|
||
overtimeKitPrevSnapshot = {}
|
||
needInitiatorSelect.value = false
|
||
initiatorSelectByNode.value = {}
|
||
flowSteps.value = []
|
||
flowResolved.value = false
|
||
flowPendingMessage.value = '必填信息填写后,流程将自动显示'
|
||
dlgVisible.value = true
|
||
detailLoading.value = true
|
||
previewLoading.value = true
|
||
try {
|
||
const detail = await fetchOaTemplateDetail(t.id)
|
||
const schema = detail?.formSchemaJson || {}
|
||
const rawRule = Array.isArray(schema.rule) ? schema.rule : Array.isArray(schema) ? schema : []
|
||
formRule.value = ensureConfirmationKitVisibility(
|
||
ensureOwnerFieldRuleProps(
|
||
ensureOvertimeKitRuleProps(ensureLeaveKitRuleProps(ensureOaUploadRuleProps(rawRule)))
|
||
)
|
||
)
|
||
const savedGutter = Number(schema.option?.row?.gutter)
|
||
formOption.value = {
|
||
...(schema.option || {}),
|
||
form: {
|
||
...(schema.option?.form || {}),
|
||
labelPosition: 'top'
|
||
},
|
||
// 顶层字段 col.span 布局用;栅格组件 fcRow 由 ensureOaUploadRuleProps 处理
|
||
row: {
|
||
...(schema.option?.row || {}),
|
||
gutter: savedGutter > 0 ? savedGutter : OA_FC_DEFAULT_GUTTER
|
||
},
|
||
submitBtn: false,
|
||
resetBtn: false
|
||
}
|
||
let data = await applyOwnerFieldDefaults(
|
||
formRule.value,
|
||
{},
|
||
(userStore.info || userStore.getUserInfo) as any
|
||
)
|
||
data = await applyConfirmationKitDefaults(
|
||
{ rule: formRule.value },
|
||
data,
|
||
(userStore.info || userStore.getUserInfo) as any
|
||
)
|
||
data = applyResignationKitDefaults(
|
||
{ rule: formRule.value },
|
||
data,
|
||
(userStore.info || userStore.getUserInfo) as any
|
||
)
|
||
data = applyOvertimeKitDefaults(
|
||
{ rule: formRule.value },
|
||
data,
|
||
(userStore.info || userStore.getUserInfo) as any
|
||
)
|
||
const linked = applyConfirmationKitDateLinkage({ rule: formRule.value }, data)
|
||
formData.value = linked.data
|
||
confirmationKitPrevSnapshot = { ...linked.data }
|
||
overtimeKitPrevSnapshot = { ...linked.data }
|
||
void syncOvertimeKitDurations()
|
||
const flow = detail?.flowTreeJson || {}
|
||
const root = flow.root || flow
|
||
resetFlowPreviewState(root)
|
||
needInitiatorSelect.value = flowNeedsInitiatorSelect(root)
|
||
if (needInitiatorSelect.value && !userOptions.value.length) {
|
||
await loadUsers()
|
||
}
|
||
await loadFlowPreview(t.id)
|
||
} catch {
|
||
formRule.value = []
|
||
flowSteps.value = []
|
||
flowResolved.value = true
|
||
previewLoading.value = false
|
||
} finally {
|
||
detailLoading.value = false
|
||
}
|
||
}
|
||
|
||
function closeDialog() {
|
||
dlgVisible.value = false
|
||
editingDraftId.value = ''
|
||
}
|
||
|
||
function onDialogClosed() {
|
||
editingDraftId.value = ''
|
||
emit('closed')
|
||
}
|
||
|
||
async function openForApp(appRow: any) {
|
||
await onPick(appRow)
|
||
}
|
||
|
||
async function openForDraft(draftId: string, templatesOptional?: any[]) {
|
||
const detail = await fetchOaInstanceDraftDetail(draftId)
|
||
const appId = String(detail?.appId || '').trim()
|
||
if (!appId) {
|
||
ElMessage.error('草稿缺少审批模板')
|
||
return
|
||
}
|
||
const list = templatesOptional || []
|
||
let row = list.find((t: any) => t.id === appId)
|
||
if (!row) {
|
||
row = {
|
||
id: appId,
|
||
appName: detail?.appName || '发起审批'
|
||
}
|
||
}
|
||
await onPick(row)
|
||
editingDraftId.value = draftId
|
||
const saved = { ...(detail?.formData || {}) }
|
||
formData.value = await applyOwnerFieldDefaults(
|
||
formRule.value,
|
||
saved,
|
||
(userStore.info || userStore.getUserInfo) as any
|
||
)
|
||
formData.value = applyOvertimeKitDefaults(
|
||
{ rule: formRule.value },
|
||
formData.value,
|
||
(userStore.info || userStore.getUserInfo) as any
|
||
)
|
||
const linked = applyConfirmationKitDateLinkage({ rule: formRule.value }, formData.value)
|
||
formData.value = linked.data
|
||
confirmationKitPrevSnapshot = { ...linked.data }
|
||
leaveKitPrevSnapshot = { ...formData.value }
|
||
tripKitPrevSnapshot = { ...formData.value }
|
||
overtimeKitPrevSnapshot = { ...formData.value }
|
||
void syncOvertimeKitDurations()
|
||
const sel = detail?.initiatorSelectByNode
|
||
initiatorSelectByNode.value =
|
||
sel && typeof sel === 'object' && !Array.isArray(sel) ? { ...sel } : {}
|
||
await loadFlowPreview(appId)
|
||
}
|
||
|
||
async function openForAppWithCopy(appRow: any, copyFromInstanceId: string) {
|
||
await openForApp(appRow)
|
||
const copyId = String(copyFromInstanceId || '').trim()
|
||
if (!copyId) return
|
||
try {
|
||
const old = await fetchOaInstanceDetail(copyId)
|
||
formData.value = await applyOwnerFieldDefaults(
|
||
formRule.value,
|
||
{ ...(old?.formDataJson || {}) },
|
||
(userStore.info || userStore.getUserInfo) as any
|
||
)
|
||
formData.value = applyOvertimeKitDefaults(
|
||
{ rule: formRule.value },
|
||
formData.value,
|
||
(userStore.info || userStore.getUserInfo) as any
|
||
)
|
||
overtimeKitPrevSnapshot = { ...formData.value }
|
||
void syncOvertimeKitDurations()
|
||
} catch {
|
||
/* 预填失败仍可手工填 */
|
||
}
|
||
}
|
||
|
||
async function saveDraft() {
|
||
if (!picked.value?.id) return
|
||
savingDraft.value = true
|
||
try {
|
||
const payload = {
|
||
formData: { ...formData.value },
|
||
initiatorSelectByNode: { ...initiatorSelectByNode.value },
|
||
title: picked.value.appName || ''
|
||
}
|
||
if (editingDraftId.value) {
|
||
await fetchUpdateOaInstanceDraft(editingDraftId.value, payload)
|
||
} else {
|
||
const created: any = await fetchCreateOaInstanceDraft({
|
||
appId: picked.value.id,
|
||
...payload
|
||
})
|
||
if (created?.id) editingDraftId.value = created.id
|
||
}
|
||
emit('saved')
|
||
closeDialog()
|
||
} finally {
|
||
savingDraft.value = false
|
||
}
|
||
}
|
||
|
||
function syncLeaveKitDurations() {
|
||
if (leaveKitSyncing) return
|
||
if (!formRule.value.length) return
|
||
const schema = { rule: formRule.value }
|
||
const bindings = listLeaveKitBindings(schema)
|
||
if (!bindings.length) return
|
||
|
||
const curr = { ...formData.value }
|
||
for (const kit of bindings) {
|
||
const prevDur = leaveKitPrevSnapshot[kit.durationField]
|
||
const currDur = curr[kit.durationField]
|
||
const startChanged = leaveKitPrevSnapshot[kit.startField] !== curr[kit.startField]
|
||
const endChanged = leaveKitPrevSnapshot[kit.endField] !== curr[kit.endField]
|
||
const durChanged = prevDur !== currDur
|
||
if (durChanged && !startChanged && !endChanged && currDur !== undefined && currDur !== '') {
|
||
Object.assign(curr, markLeaveDurationManual(curr, kit.durationField, true))
|
||
}
|
||
}
|
||
|
||
const { data, changed } = applyLeaveKitDurationsToFormData(schema, curr)
|
||
leaveKitPrevSnapshot = { ...data }
|
||
const manualKeysChanged = bindings.some(
|
||
(k) =>
|
||
!!data[LEAVE_MANUAL_PREFIX + k.durationField] !==
|
||
!!formData.value[LEAVE_MANUAL_PREFIX + k.durationField]
|
||
)
|
||
const durationChanged = bindings.some(
|
||
(k) => data[k.durationField] !== formData.value[k.durationField]
|
||
)
|
||
if (changed || manualKeysChanged || durationChanged) {
|
||
leaveKitSyncing = true
|
||
formData.value = data
|
||
queueMicrotask(() => {
|
||
leaveKitSyncing = false
|
||
})
|
||
}
|
||
}
|
||
|
||
function syncTripKitDurations() {
|
||
if (tripKitSyncing) return
|
||
if (!formRule.value.length) return
|
||
const schema = { rule: formRule.value }
|
||
const kits = listTripKits(schema)
|
||
if (!kits.length) return
|
||
|
||
const curr = { ...formData.value }
|
||
for (const kit of kits) {
|
||
const prevDays = tripKitPrevSnapshot[kit.daysField]
|
||
const currDays = curr[kit.daysField]
|
||
const daysChanged = prevDays !== currDays
|
||
const listChanged =
|
||
JSON.stringify(tripKitPrevSnapshot[kit.listField]) !== JSON.stringify(curr[kit.listField])
|
||
if (daysChanged && !listChanged && currDays !== undefined && currDays !== '') {
|
||
curr[TRIP_MANUAL_PREFIX + kit.daysField] = true
|
||
}
|
||
}
|
||
|
||
const { data, changed } = applyTripKitDaysToFormData(schema, curr)
|
||
tripKitPrevSnapshot = { ...data }
|
||
const manualKeysChanged = kits.some(
|
||
(k) =>
|
||
!!data[TRIP_MANUAL_PREFIX + k.daysField] !==
|
||
!!formData.value[TRIP_MANUAL_PREFIX + k.daysField]
|
||
)
|
||
const daysFieldChanged = kits.some((k) => data[k.daysField] !== formData.value[k.daysField])
|
||
const listFieldChanged = kits.some(
|
||
(k) => JSON.stringify(data[k.listField]) !== JSON.stringify(formData.value[k.listField])
|
||
)
|
||
if (changed || manualKeysChanged || daysFieldChanged || listFieldChanged) {
|
||
tripKitSyncing = true
|
||
formData.value = data
|
||
queueMicrotask(() => {
|
||
tripKitSyncing = false
|
||
})
|
||
}
|
||
}
|
||
|
||
function syncConfirmationKitDates() {
|
||
if (confirmationKitSyncing) return
|
||
if (!formRule.value.length) return
|
||
const schema = { rule: formRule.value }
|
||
const kits = listConfirmationKitBindings(schema)
|
||
if (!kits.length) return
|
||
|
||
let curr = { ...formData.value }
|
||
for (const kit of kits) {
|
||
const confField = kit.confirmationDateField
|
||
const hireField = kit.hireDateField
|
||
const probField = kit.probationMonthsField
|
||
if (!confField || !hireField || !probField) continue
|
||
const confChanged = confirmationKitPrevSnapshot[confField] !== curr[confField]
|
||
const hireChanged = confirmationKitPrevSnapshot[hireField] !== curr[hireField]
|
||
const probChanged = confirmationKitPrevSnapshot[probField] !== curr[probField]
|
||
if (
|
||
confChanged &&
|
||
!hireChanged &&
|
||
!probChanged &&
|
||
curr[confField] !== undefined &&
|
||
curr[confField] !== ''
|
||
) {
|
||
curr = markConfirmationDateManual(curr, confField, true)
|
||
}
|
||
}
|
||
|
||
const { data, changed } = applyConfirmationKitDateLinkage(schema, curr)
|
||
confirmationKitPrevSnapshot = { ...data }
|
||
const anyDiff =
|
||
changed ||
|
||
kits.some((k) => {
|
||
const f = k.confirmationDateField
|
||
if (!f) return false
|
||
return (
|
||
data[f] !== formData.value[f] ||
|
||
!!data[CONFIRMATION_MANUAL_PREFIX + f] !==
|
||
!!formData.value[CONFIRMATION_MANUAL_PREFIX + f]
|
||
)
|
||
})
|
||
if (anyDiff) {
|
||
confirmationKitSyncing = true
|
||
formData.value = data
|
||
queueMicrotask(() => {
|
||
confirmationKitSyncing = false
|
||
})
|
||
}
|
||
}
|
||
|
||
function restoreConfirmationDateAuto() {
|
||
const schema = { rule: formRule.value }
|
||
let data = { ...formData.value }
|
||
for (const field of listManualConfirmationDateFields(schema, data)) {
|
||
data = clearConfirmationDateManualAndRelink(schema, data, field)
|
||
}
|
||
confirmationKitSyncing = true
|
||
formData.value = data
|
||
confirmationKitPrevSnapshot = { ...data }
|
||
queueMicrotask(() => {
|
||
confirmationKitSyncing = false
|
||
})
|
||
}
|
||
|
||
async function ensureOvertimeHolidayRange(startYmd: string, endYmd: string) {
|
||
const start = String(startYmd || '').trim().slice(0, 10)
|
||
const end = String(endYmd || '').trim().slice(0, 10)
|
||
if (!start || !end) return
|
||
let lo = start
|
||
let hi = end
|
||
if (lo > hi) {
|
||
const t = lo
|
||
lo = hi
|
||
hi = t
|
||
}
|
||
const begin = new Date(lo + 'T00:00:00')
|
||
const finish = new Date(hi + 'T00:00:00')
|
||
if (Number.isNaN(begin.getTime()) || Number.isNaN(finish.getTime())) return
|
||
|
||
let allPresent = true
|
||
for (let d = new Date(begin); d <= finish; d.setDate(d.getDate() + 1)) {
|
||
const y = d.getFullYear()
|
||
const m = String(d.getMonth() + 1).padStart(2, '0')
|
||
const day = String(d.getDate()).padStart(2, '0')
|
||
const key = `${y}-${m}-${day}`
|
||
if (!(key in overtimeRestMap)) {
|
||
allPresent = false
|
||
break
|
||
}
|
||
}
|
||
if (allPresent) return
|
||
|
||
try {
|
||
const res = await fetchCnHolidayRange(lo, hi)
|
||
const days = res?.days || []
|
||
for (const day of days) {
|
||
const key = String(day?.dayDate || '').trim().slice(0, 10)
|
||
if (!key) continue
|
||
overtimeRestMap[key] = !!day.isRestDay
|
||
}
|
||
} catch {
|
||
/* 失败保留已有 map,计算回退周末规则 */
|
||
}
|
||
}
|
||
|
||
function isRestDayResolver(ymd: string): boolean {
|
||
if (ymd in overtimeRestMap) return overtimeRestMap[ymd]
|
||
const d = new Date(ymd + 'T00:00:00')
|
||
const w = d.getDay()
|
||
return w === 0 || w === 6
|
||
}
|
||
|
||
async function syncOvertimeKitDurations() {
|
||
if (overtimeKitSyncing) return
|
||
if (!formRule.value.length) return
|
||
const schema = { rule: formRule.value }
|
||
const bindings = listOvertimeKitBindings(schema)
|
||
if (!bindings.length) return
|
||
|
||
overtimeKitSyncing = true
|
||
try {
|
||
let minYmd = ''
|
||
let maxYmd = ''
|
||
for (const kit of bindings) {
|
||
for (const field of [kit.startField, kit.endField]) {
|
||
const ymd = String(formData.value[field] || '')
|
||
.trim()
|
||
.slice(0, 10)
|
||
if (ymd.length < 10) continue
|
||
if (!minYmd || ymd < minYmd) minYmd = ymd
|
||
if (!maxYmd || ymd > maxYmd) maxYmd = ymd
|
||
}
|
||
}
|
||
if (minYmd && maxYmd) {
|
||
await ensureOvertimeHolidayRange(minYmd, maxYmd)
|
||
}
|
||
|
||
const curr = { ...formData.value }
|
||
for (const kit of bindings) {
|
||
const prevDur = overtimeKitPrevSnapshot[kit.durationField]
|
||
const currDur = curr[kit.durationField]
|
||
const startChanged = overtimeKitPrevSnapshot[kit.startField] !== curr[kit.startField]
|
||
const endChanged = overtimeKitPrevSnapshot[kit.endField] !== curr[kit.endField]
|
||
const durChanged = prevDur !== currDur
|
||
if (
|
||
durChanged &&
|
||
!startChanged &&
|
||
!endChanged &&
|
||
currDur !== undefined &&
|
||
currDur !== ''
|
||
) {
|
||
Object.assign(curr, markOvertimeDurationManual(curr, kit.durationField, true))
|
||
}
|
||
}
|
||
|
||
const { data, changed } = applyOvertimeKitDurationsToFormData(
|
||
schema,
|
||
curr,
|
||
isRestDayResolver
|
||
)
|
||
overtimeKitPrevSnapshot = { ...data }
|
||
const manualKeysChanged = bindings.some(
|
||
(k) =>
|
||
!!data[OVERTIME_MANUAL_PREFIX + k.durationField] !==
|
||
!!formData.value[OVERTIME_MANUAL_PREFIX + k.durationField]
|
||
)
|
||
const durationChanged = bindings.some(
|
||
(k) => data[k.durationField] !== formData.value[k.durationField]
|
||
)
|
||
if (changed || manualKeysChanged || durationChanged) {
|
||
formData.value = data
|
||
}
|
||
} finally {
|
||
queueMicrotask(() => {
|
||
overtimeKitSyncing = false
|
||
})
|
||
}
|
||
}
|
||
|
||
function restoreOvertimeAuto() {
|
||
let data = { ...formData.value }
|
||
for (const kit of listOvertimeKitBindings({ rule: formRule.value })) {
|
||
data = markOvertimeDurationManual(data, kit.durationField, false)
|
||
}
|
||
formData.value = data
|
||
void syncOvertimeKitDurations()
|
||
}
|
||
|
||
function restoreLeaveAuto() {
|
||
let data = { ...formData.value }
|
||
for (const kit of listLeaveKitBindings({ rule: formRule.value })) {
|
||
data = markLeaveDurationManual(data, kit.durationField, false)
|
||
}
|
||
formData.value = data
|
||
syncLeaveKitDurations()
|
||
}
|
||
|
||
function restoreTripDaysAuto() {
|
||
let data = { ...formData.value }
|
||
for (const kit of listTripKits({ rule: formRule.value })) {
|
||
delete data[TRIP_MANUAL_PREFIX + kit.daysField]
|
||
}
|
||
formData.value = data
|
||
syncTripKitDurations()
|
||
}
|
||
|
||
watch(
|
||
formData,
|
||
() => {
|
||
syncLeaveKitDurations()
|
||
syncTripKitDurations()
|
||
syncConfirmationKitDates()
|
||
void syncOvertimeKitDurations()
|
||
scheduleFlowPreview()
|
||
},
|
||
{ deep: true }
|
||
)
|
||
|
||
async function submit() {
|
||
if (!picked.value?.id) return
|
||
for (const s of flowSteps.value) {
|
||
if (s.type === 'approval') {
|
||
const types =
|
||
Array.isArray(s.assigneeTypes) && s.assigneeTypes.length
|
||
? s.assigneeTypes
|
||
: s.assigneeType
|
||
? [s.assigneeType]
|
||
: []
|
||
if (types.includes('initiator_select')) {
|
||
const ids = initiatorSelectByNode.value[s.id] || []
|
||
if (!ids.length) {
|
||
ElMessage.warning(`请为「${s.name || '审批人'}」选择审批人`)
|
||
return
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if (fapi.value?.validate) {
|
||
try {
|
||
await fapi.value.validate()
|
||
} catch {
|
||
ElMessage.warning('请完善表单必填项')
|
||
return
|
||
}
|
||
}
|
||
submitting.value = true
|
||
try {
|
||
const payload = {
|
||
formData: { ...formData.value },
|
||
initiatorSelectByNode: { ...initiatorSelectByNode.value }
|
||
}
|
||
let data: any
|
||
if (editingDraftId.value) {
|
||
data = await fetchSubmitOaInstanceDraft(editingDraftId.value, {
|
||
...payload,
|
||
title: picked.value.appName || ''
|
||
})
|
||
} else {
|
||
data = await fetchStartOaInstance({
|
||
appId: picked.value.id,
|
||
...payload
|
||
})
|
||
}
|
||
emit('submitted', data)
|
||
closeDialog()
|
||
} finally {
|
||
submitting.value = false
|
||
}
|
||
}
|
||
|
||
defineExpose({ openForApp, openForDraft, openForAppWithCopy })
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.oa-start__tip {
|
||
margin: 8px 0 0;
|
||
color: var(--mint-text-muted, #94a3b8);
|
||
font-size: 13px;
|
||
}
|
||
</style>
|