refactor(web-admin): wire start page to OaStartFormDialog.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zlwuandCursor committed 2026-08-06 14:47:02 +08:00
1 parent 0dcc05dca7
commit cf009de58a
1 file changed
+27 -588
+27 -588
View File
@@ -84,166 +84,35 @@
</section>
</div>
<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"
>
<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-else-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>
<OaStartFormDialog
ref="startDialogRef"
@saved="onDialogSaved"
@submitted="onDialogSubmitted"
/>
</section>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, ref, watch } from 'vue'
import { computed, onMounted, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { Check, Close, Document, Promotion, Search } from '@element-plus/icons-vue'
import { Promotion, Search } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import { fetchUserList } from '@/api/admin'
import {
fetchCreateOaInstanceDraft,
fetchOaFlowPreview,
fetchOaGroupList,
fetchOaInstanceDetail,
fetchOaInstanceDraftDetail,
fetchOaTemplateDetail,
fetchOaTemplates,
fetchStartOaInstance,
fetchSubmitOaInstanceDraft,
fetchUpdateOaInstanceDraft,
type OaFlowPreviewStep
} from '@/api/oaApproval'
import { fetchOaGroupList, fetchOaTemplates } from '@/api/oaApproval'
import OaAppIcon from '@/components/oa-approval/OaAppIcon.vue'
import OaFlowPreview from '@/components/oa-approval/OaFlowPreview.vue'
import { MAX_ADMIN_LIST_PAGE_SIZE } from '@/utils/fetch-paged-list'
import OaStartFormDialog from '@/components/oa-approval/OaStartFormDialog.vue'
import { refreshOaMenuBadges } from '@/utils/oa-menu-badge'
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, ensureOvertimeKitRuleProps } 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 { useUserStore } from '@/store/modules/user'
defineOptions({ name: 'OaApprovalStart' })
const router = useRouter()
const route = useRoute()
const userStore = useUserStore()
const loading = ref(false)
const templates = ref<any[]>([])
const groupOptions = ref<Array<{ id: string; groupName: string }>>([])
const searchKeyword = ref('')
const activeGroupId = ref('')
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
/** 避免时长自动回写再次触发手改判定 */
let leaveKitSyncing = false
let leaveKitPrevSnapshot: Record<string, unknown> = {}
let tripKitSyncing = false
let tripKitPrevSnapshot: Record<string, unknown> = {}
let confirmationKitSyncing = false
let confirmationKitPrevSnapshot: Record<string, unknown> = {}
const confirmationDateManualFields = computed(() =>
listManualConfirmationDateFields({ rule: formRule.value }, formData.value)
)
const startDialogRef = ref<InstanceType<typeof OaStartFormDialog> | null>(null)
const displayGroups = computed(() => {
const kw = (searchKeyword.value || '').trim().toLowerCase()
@@ -252,7 +121,11 @@
list = list.filter((a) => a.groupId === activeGroupId.value)
}
if (kw) {
list = list.filter((a) => String(a.appName || '').toLowerCase().includes(kw))
list = list.filter((a) =>
String(a.appName || '')
.toLowerCase()
.includes(kw)
)
}
const bucket = new Map<string, { id: string; name: string; apps: any[] }>()
@@ -283,433 +156,18 @@
return sections
})
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 onPick(row: any) {
await startDialogRef.value?.openForApp(row)
}
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 = []
}
function onDialogSaved() {
void refreshOaMenuBadges()
}
async function loadFlowPreview(appId: string) {
previewLoading.value = true
try {
const preview = await fetchOaFlowPreview(appId, {
formData: { ...formData.value },
initiatorSelectByNode: { ...initiatorSelectByNode.value }
})
flowResolved.value = preview?.resolved !== false
flowPendingMessage.value =
preview?.message || '必填信息填写后,流程将自动显示'
flowSteps.value = flowResolved.value ? preview?.steps || [] : []
} catch {
flowSteps.value = []
flowResolved.value = true
} finally {
previewLoading.value = false
}
}
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 = {}
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 }
const flow = detail?.flowTreeJson || {}
const root = flow.root || flow
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 = ''
}
async function openDraft(draftId: string) {
const detail = await fetchOaInstanceDraftDetail(draftId)
const appId = String(detail?.appId || '').trim()
if (!appId) {
ElMessage.error('草稿缺少审批模板')
return
}
let row = templates.value.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 }
const sel = detail?.initiatorSelectByNode
initiatorSelectByNode.value =
sel && typeof sel === 'object' && !Array.isArray(sel) ? { ...sel } : {}
await loadFlowPreview(appId)
}
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
}
void refreshOaMenuBadges()
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
})
}
watch(
formData,
() => {
syncLeaveKitDurations()
syncTripKitDurations()
syncConfirmationKitDates()
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
})
}
closeDialog()
void refreshOaMenuBadges()
if (data?.id) {
router.push({ path: `/system-manage/oa-approval/instances/${data.id}` })
}
} finally {
submitting.value = false
function onDialogSubmitted(data: any) {
void refreshOaMenuBadges()
if (data?.id) {
router.push({ path: `/system-manage/oa-approval/instances/${data.id}` })
}
}
@@ -724,30 +182,17 @@
const qCopyFrom = String(route.query.copyFromInstanceId || '').trim()
if (qDraftId) {
try {
await openDraft(qDraftId)
await startDialogRef.value?.openForDraft(qDraftId, templates.value)
} catch {
ElMessage.error('无法打开草稿')
}
} else if (qAppId) {
const row = templates.value.find((t: any) => t.id === qAppId)
if (row) {
await onPick(row)
if (qCopyFrom) {
try {
const old = await fetchOaInstanceDetail(qCopyFrom)
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
)
} catch {
/* 预填失败仍可手工填 */
}
await startDialogRef.value?.openForAppWithCopy(row, qCopyFrom)
} else {
await startDialogRef.value?.openForApp(row)
}
}
}
@@ -972,12 +417,6 @@
}
}
.oa-start__tip {
margin: 8px 0 0;
color: var(--mint-text-muted, #94a3b8);
font-size: 13px;
}
@media (max-width: 640px) {
.oa-app-card {
width: 100%;