feat(mobile-oa-h5): implement oa-start page with form and flow preview
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
7ea11ffdcd
commit
07eeb58d15
4 files changed
+761
-7
No files matched your search
@@ -0,0 +1,364 @@
|
||||
<template>
|
||||
<div class="oa-flow-preview">
|
||||
<div class="oa-flow-preview__title">审批流程</div>
|
||||
<van-loading v-if="loading" vertical class="oa-flow-preview__loading">加载中…</van-loading>
|
||||
<div v-else-if="!resolved" class="oa-flow-preview__pending">
|
||||
{{ pendingMessage || '必填信息填写后,流程将自动显示' }}
|
||||
</div>
|
||||
<div v-else class="oa-flow-preview__chain">
|
||||
<div v-if="!steps.length" class="oa-flow-preview__empty">暂无流程配置</div>
|
||||
<div v-for="(s, idx) in steps" :key="s.id || idx" class="oa-flow-step">
|
||||
<div class="oa-flow-rail">
|
||||
<div class="oa-flow-dot" :class="{ end: s.type === 'end' }" />
|
||||
<div v-if="idx < steps.length - 1" class="oa-flow-line" />
|
||||
</div>
|
||||
<div class="oa-flow-card">
|
||||
<div class="oa-flow-card__name-row">
|
||||
<span class="oa-flow-card__name">{{ s.name || '-' }}</span>
|
||||
<span v-if="signBadge(s)" class="oa-flow-badge" :class="signBadge(s)">
|
||||
{{ signBadgeText(s) }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="s.meta" class="oa-flow-card__meta">{{ s.meta }}</div>
|
||||
<div v-if="s.resolveError" class="oa-flow-card__err">{{ s.resolveError }}</div>
|
||||
|
||||
<template v-if="isInitiatorSelect(s)">
|
||||
<div class="oa-flow-card__people">
|
||||
<span
|
||||
v-for="p in displayPeople(s)"
|
||||
:key="p.id"
|
||||
class="oa-person-chip"
|
||||
>
|
||||
<span class="oa-person-chip__dot">{{ avatarChar(p.nickName) }}</span>
|
||||
{{ p.nickName || p.id }}
|
||||
<button
|
||||
type="button"
|
||||
class="oa-person-chip__x"
|
||||
@click="removeUser(s.id, p.id)"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</span>
|
||||
<span v-if="!displayPeople(s).length" class="oa-flow-card__placeholder">
|
||||
{{ s.type === 'cc' ? '请选择抄送人' : '请选择审批人' }}
|
||||
</span>
|
||||
</div>
|
||||
<van-button size="small" plain type="primary" class="oa-flow-add" @click="openPicker(s)">
|
||||
添加
|
||||
</van-button>
|
||||
</template>
|
||||
<div v-else-if="(s.people || []).length" class="oa-flow-card__people">
|
||||
<span v-for="p in s.people" :key="p.id" class="oa-person-chip">
|
||||
<span class="oa-person-chip__dot">{{ avatarChar(p.nickName) }}</span>
|
||||
{{ p.nickName || p.id }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<van-popup v-model:show="pickerVisible" position="bottom" round :style="{ height: '70%' }">
|
||||
<div class="oa-flow-picker">
|
||||
<div class="oa-flow-picker__hd">
|
||||
<span @click="pickerVisible = false">取消</span>
|
||||
<strong>{{ pickerTitle }}</strong>
|
||||
<span class="oa-flow-picker__ok" @click="confirmPicker">确定</span>
|
||||
</div>
|
||||
<van-search
|
||||
v-model="pickerKeyword"
|
||||
shape="round"
|
||||
label-align="left"
|
||||
input-align="left"
|
||||
placeholder="搜索姓名或工号"
|
||||
/>
|
||||
<div class="oa-flow-picker__list">
|
||||
<van-checkbox-group v-model="pickerIds">
|
||||
<van-cell
|
||||
v-for="u in filteredUsers"
|
||||
:key="u.id"
|
||||
clickable
|
||||
:title="u.nickName || u.id"
|
||||
:label="u.userNo || undefined"
|
||||
@click="togglePicker(u.id)"
|
||||
>
|
||||
<template #icon>
|
||||
<van-checkbox class="oa-flow-picker__check" :name="u.id" @click.stop />
|
||||
</template>
|
||||
</van-cell>
|
||||
</van-checkbox-group>
|
||||
<van-empty v-if="!filteredUsers.length" description="暂无人员" />
|
||||
</div>
|
||||
</div>
|
||||
</van-popup>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
steps: { type: Array, default: () => [] },
|
||||
resolved: { type: Boolean, default: true },
|
||||
pendingMessage: { type: String, default: '' },
|
||||
loading: { type: Boolean, default: false },
|
||||
initiatorSelectByNode: { type: Object, default: () => ({}) },
|
||||
userOptions: { type: Array, default: () => [] }
|
||||
})
|
||||
const emit = defineEmits(['update:initiatorSelectByNode'])
|
||||
|
||||
const pickerVisible = ref(false)
|
||||
const pickerNodeId = ref('')
|
||||
const pickerTitle = ref('选择人员')
|
||||
const pickerIds = ref([])
|
||||
const pickerKeyword = ref('')
|
||||
|
||||
const filteredUsers = computed(() => {
|
||||
const q = pickerKeyword.value.trim().toLowerCase()
|
||||
const list = props.userOptions || []
|
||||
if (!q) return list
|
||||
return list.filter(
|
||||
(u) =>
|
||||
String(u.nickName || '')
|
||||
.toLowerCase()
|
||||
.includes(q) ||
|
||||
String(u.userNo || '')
|
||||
.toLowerCase()
|
||||
.includes(q) ||
|
||||
String(u.id || '')
|
||||
.toLowerCase()
|
||||
.includes(q)
|
||||
)
|
||||
})
|
||||
|
||||
function isInitiatorSelect(s) {
|
||||
const types =
|
||||
Array.isArray(s.assigneeTypes) && s.assigneeTypes.length
|
||||
? s.assigneeTypes
|
||||
: s.assigneeType
|
||||
? [s.assigneeType]
|
||||
: []
|
||||
return types.includes('initiator_select')
|
||||
}
|
||||
|
||||
function signBadge(s) {
|
||||
const m = String(s.signMode || '').toLowerCase()
|
||||
if (m === 'and' || m === 'all' || m === '会签') return 'and'
|
||||
if (m === 'or' || m === 'any' || m === '或签') return 'or'
|
||||
return ''
|
||||
}
|
||||
|
||||
function signBadgeText(s) {
|
||||
const b = signBadge(s)
|
||||
if (b === 'and') return '会签'
|
||||
if (b === 'or') return '或签'
|
||||
return ''
|
||||
}
|
||||
|
||||
function avatarChar(name) {
|
||||
const n = String(name || '').trim()
|
||||
return n ? n.slice(0, 1) : '?'
|
||||
}
|
||||
|
||||
function displayPeople(s) {
|
||||
const ids = props.initiatorSelectByNode?.[s.id] || []
|
||||
const map = new Map((props.userOptions || []).map((u) => [u.id, u]))
|
||||
return ids
|
||||
.map((id) => map.get(id) || { id, nickName: id })
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
function setNodeIds(nodeId, ids) {
|
||||
emit('update:initiatorSelectByNode', {
|
||||
...(props.initiatorSelectByNode || {}),
|
||||
[nodeId]: [...ids]
|
||||
})
|
||||
}
|
||||
|
||||
function removeUser(nodeId, userId) {
|
||||
const cur = props.initiatorSelectByNode?.[nodeId] || []
|
||||
setNodeIds(
|
||||
nodeId,
|
||||
cur.filter((id) => id !== userId)
|
||||
)
|
||||
}
|
||||
|
||||
function openPicker(s) {
|
||||
pickerNodeId.value = s.id
|
||||
pickerTitle.value = s.type === 'cc' ? '选择抄送人' : `选择${s.name || '审批人'}`
|
||||
pickerIds.value = [...(props.initiatorSelectByNode?.[s.id] || [])]
|
||||
pickerKeyword.value = ''
|
||||
pickerVisible.value = true
|
||||
}
|
||||
|
||||
function togglePicker(id) {
|
||||
const set = new Set(pickerIds.value)
|
||||
if (set.has(id)) set.delete(id)
|
||||
else set.add(id)
|
||||
pickerIds.value = [...set]
|
||||
}
|
||||
|
||||
function confirmPicker() {
|
||||
if (pickerNodeId.value) setNodeIds(pickerNodeId.value, pickerIds.value)
|
||||
pickerVisible.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.oa-flow-preview {
|
||||
margin-top: 16px;
|
||||
padding: 14px 12px 8px;
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--border, #dcebe4);
|
||||
}
|
||||
.oa-flow-preview__title {
|
||||
margin-bottom: 12px;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary, #1f2a26);
|
||||
}
|
||||
.oa-flow-preview__loading,
|
||||
.oa-flow-preview__pending,
|
||||
.oa-flow-preview__empty {
|
||||
padding: 12px 4px 16px;
|
||||
color: var(--text-muted, #6b7b74);
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.oa-flow-step {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
min-height: 56px;
|
||||
}
|
||||
.oa-flow-rail {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 16px;
|
||||
}
|
||||
.oa-flow-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
margin-top: 6px;
|
||||
border-radius: 50%;
|
||||
background: var(--brand-green, #0a9f6d);
|
||||
}
|
||||
.oa-flow-dot.end {
|
||||
background: #94a3b8;
|
||||
}
|
||||
.oa-flow-line {
|
||||
flex: 1;
|
||||
width: 2px;
|
||||
margin: 4px 0;
|
||||
background: #d1fae5;
|
||||
}
|
||||
.oa-flow-card {
|
||||
flex: 1;
|
||||
padding: 0 0 16px;
|
||||
min-width: 0;
|
||||
}
|
||||
.oa-flow-card__name-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
}
|
||||
.oa-flow-card__name {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary, #1f2a26);
|
||||
}
|
||||
.oa-flow-badge {
|
||||
padding: 1px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
}
|
||||
.oa-flow-badge.and {
|
||||
color: #087f57;
|
||||
background: #e9f9f2;
|
||||
}
|
||||
.oa-flow-badge.or {
|
||||
color: #b45309;
|
||||
background: #fff7e8;
|
||||
}
|
||||
.oa-flow-card__meta,
|
||||
.oa-flow-card__placeholder {
|
||||
margin-top: 4px;
|
||||
color: var(--text-muted, #6b7b74);
|
||||
font-size: 12px;
|
||||
}
|
||||
.oa-flow-card__err {
|
||||
margin-top: 4px;
|
||||
color: #d14343;
|
||||
font-size: 12px;
|
||||
}
|
||||
.oa-flow-card__people {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.oa-person-chip {
|
||||
display: inline-flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
padding: 4px 8px;
|
||||
border-radius: 999px;
|
||||
background: #f5f8f7;
|
||||
font-size: 12px;
|
||||
color: var(--text-primary, #1f2a26);
|
||||
}
|
||||
.oa-person-chip__dot {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 50%;
|
||||
background: var(--brand-green, #0a9f6d);
|
||||
color: #fff;
|
||||
font-size: 11px;
|
||||
}
|
||||
.oa-person-chip__x {
|
||||
margin-left: 2px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: #94a3b8;
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
}
|
||||
.oa-flow-add {
|
||||
margin-top: 8px;
|
||||
}
|
||||
.oa-flow-picker {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background: #fff;
|
||||
}
|
||||
.oa-flow-picker__hd {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 14px 16px;
|
||||
border-bottom: 1px solid var(--border, #dcebe4);
|
||||
}
|
||||
.oa-flow-picker__ok {
|
||||
color: var(--brand-green, #0a9f6d);
|
||||
font-weight: 600;
|
||||
}
|
||||
.oa-flow-picker__list {
|
||||
flex: 1;
|
||||
padding: 12px;
|
||||
overflow: auto;
|
||||
}
|
||||
.oa-flow-picker__list .van-cell {
|
||||
margin-bottom: 10px;
|
||||
padding: 12px 14px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
.oa-flow-picker__check {
|
||||
margin-right: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,45 @@
|
||||
import { oaFormOption } from './form-option.js'
|
||||
|
||||
function walkRules(rules, fn) {
|
||||
const list = Array.isArray(rules) ? rules : []
|
||||
for (const r of list) {
|
||||
if (!r || typeof r !== 'object') continue
|
||||
fn(r)
|
||||
if (Array.isArray(r.children)) walkRules(r.children, fn)
|
||||
}
|
||||
}
|
||||
|
||||
/** 与 Web ensureOaUploadRuleProps 同思路的最小版 */
|
||||
export function ensureOaUploadRuleProps(rules) {
|
||||
const next = JSON.parse(JSON.stringify(rules || []))
|
||||
walkRules(next, (r) => {
|
||||
if (r.type === 'upload' || r.type === 'fcUpload') {
|
||||
r.props = { ...(r.props || {}) }
|
||||
if (r.props.limit == null) r.props.limit = 9
|
||||
}
|
||||
})
|
||||
return next
|
||||
}
|
||||
|
||||
export function prepareMobileFormSchema(formSchemaJson) {
|
||||
const schema = formSchemaJson || {}
|
||||
const rawRule = Array.isArray(schema.rule)
|
||||
? schema.rule
|
||||
: Array.isArray(schema)
|
||||
? schema
|
||||
: []
|
||||
const rule = ensureOaUploadRuleProps(rawRule)
|
||||
const option = {
|
||||
...oaFormOption,
|
||||
...(schema.option || {}),
|
||||
form: {
|
||||
...(schema.option?.form || {}),
|
||||
labelAlign: 'top',
|
||||
inputAlign: 'left',
|
||||
colon: false
|
||||
},
|
||||
submitBtn: false,
|
||||
resetBtn: false
|
||||
}
|
||||
return { rule, option }
|
||||
}
|
||||
@@ -1,16 +1,15 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import FormSink from './views/FormSink.vue'
|
||||
import OaStart from './views/OaStart.vue'
|
||||
|
||||
export default createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes: [
|
||||
{ path: '/', redirect: '/oa-form-sink' },
|
||||
{ path: '/oa-form-sink', component: FormSink },
|
||||
{
|
||||
path: '/oa-start',
|
||||
component: {
|
||||
template: '<div style="padding:16px">oa-start 占位(阶段二)</div>'
|
||||
}
|
||||
}
|
||||
path: '/',
|
||||
redirect: (to) => ({ path: '/oa-form-sink', query: to.query, hash: to.hash })
|
||||
},
|
||||
{ path: '/oa-form-sink', component: FormSink },
|
||||
{ path: '/oa-start', component: OaStart }
|
||||
]
|
||||
})
|
||||
@@ -0,0 +1,346 @@
|
||||
<template>
|
||||
<div class="oa-start">
|
||||
<header class="oa-start__hd">{{ appMeta.appName || '发起审批' }}</header>
|
||||
<div v-if="detailLoading" class="oa-start__loading">
|
||||
<van-loading vertical>加载模板…</van-loading>
|
||||
</div>
|
||||
<div v-else class="oa-start__body">
|
||||
<form-create-mobile
|
||||
v-model="formData"
|
||||
v-model:api="fapi"
|
||||
:rule="formRule"
|
||||
:option="formOption"
|
||||
/>
|
||||
<OaFlowPreview
|
||||
v-model:initiator-select-by-node="initiatorSelectByNode"
|
||||
:steps="flowSteps"
|
||||
:resolved="flowResolved"
|
||||
:pending-message="flowPendingMessage"
|
||||
:loading="previewLoading"
|
||||
:user-options="userOptions"
|
||||
/>
|
||||
</div>
|
||||
<footer class="oa-start__ft">
|
||||
<van-button class="oa-start__btn" @click="onCancel">取消</van-button>
|
||||
<van-button class="oa-start__btn" :loading="savingDraft" @click="saveDraft">
|
||||
暂存草稿
|
||||
</van-button>
|
||||
<van-button
|
||||
class="oa-start__btn"
|
||||
type="primary"
|
||||
:loading="submitting"
|
||||
@click="submit"
|
||||
>
|
||||
提交
|
||||
</van-button>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { showToast } from 'vant'
|
||||
import {
|
||||
fetchOaTemplateDetail,
|
||||
fetchOaFlowPreview,
|
||||
fetchCreateOaInstanceDraft,
|
||||
fetchUpdateOaInstanceDraft,
|
||||
fetchOaInstanceDraftDetail,
|
||||
fetchSubmitOaInstanceDraft,
|
||||
fetchStartOaInstance,
|
||||
fetchUserList
|
||||
} from '../api/oa.js'
|
||||
import { prepareMobileFormSchema } from '../oa/schema-prep.js'
|
||||
import OaFlowPreview from '../oa/components/OaFlowPreview.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const detailLoading = ref(true)
|
||||
const previewLoading = ref(false)
|
||||
const savingDraft = ref(false)
|
||||
const submitting = ref(false)
|
||||
const appMeta = ref({ id: '', appName: '' })
|
||||
const editingDraftId = ref('')
|
||||
const formData = ref({})
|
||||
const formRule = ref([])
|
||||
const formOption = ref({})
|
||||
const fapi = ref(null)
|
||||
const flowSteps = ref([])
|
||||
const flowResolved = ref(false)
|
||||
const flowPendingMessage = ref('必填信息填写后,流程将自动显示')
|
||||
const initiatorSelectByNode = ref({})
|
||||
const userOptions = ref([])
|
||||
|
||||
let previewTimer = null
|
||||
let skipPreviewWatch = false
|
||||
|
||||
function closeToMini(payload) {
|
||||
try {
|
||||
if (typeof wx !== 'undefined' && wx.miniProgram?.navigateBack) {
|
||||
wx.miniProgram.postMessage({ data: payload || { action: 'oa-start-close' } })
|
||||
wx.miniProgram.navigateBack()
|
||||
return
|
||||
}
|
||||
} catch (_) {
|
||||
/* ignore */
|
||||
}
|
||||
showToast((payload && payload.message) || '已完成(非小程序环境)')
|
||||
}
|
||||
|
||||
async function loadUsers() {
|
||||
try {
|
||||
const pageSize = 100
|
||||
const raw = []
|
||||
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
|
||||
if (page > 50) break
|
||||
}
|
||||
userOptions.value = raw.map((u) => ({
|
||||
id: String(u.id || ''),
|
||||
nickName: u.nickName || '',
|
||||
userNo: u.userNo || ''
|
||||
})).filter((u) => u.id)
|
||||
} catch {
|
||||
userOptions.value = []
|
||||
}
|
||||
}
|
||||
|
||||
function stepsNeedInitiatorSelect(steps) {
|
||||
return (steps || []).some((s) => {
|
||||
const types =
|
||||
Array.isArray(s.assigneeTypes) && s.assigneeTypes.length
|
||||
? s.assigneeTypes
|
||||
: s.assigneeType
|
||||
? [s.assigneeType]
|
||||
: []
|
||||
return types.includes('initiator_select')
|
||||
})
|
||||
}
|
||||
|
||||
async function loadFlowPreview() {
|
||||
if (!appMeta.value?.id) return
|
||||
previewLoading.value = true
|
||||
try {
|
||||
const preview = await fetchOaFlowPreview(appMeta.value.id, {
|
||||
formData: { ...formData.value },
|
||||
initiatorSelectByNode: { ...initiatorSelectByNode.value }
|
||||
})
|
||||
flowResolved.value = preview?.resolved !== false
|
||||
flowPendingMessage.value =
|
||||
preview?.message || '必填信息填写后,流程将自动显示'
|
||||
flowSteps.value = flowResolved.value ? preview?.steps || [] : []
|
||||
if (stepsNeedInitiatorSelect(flowSteps.value) && !userOptions.value.length) {
|
||||
await loadUsers()
|
||||
}
|
||||
} catch (e) {
|
||||
showToast(e?.message || '审批流预览失败')
|
||||
flowSteps.value = []
|
||||
flowResolved.value = true
|
||||
} finally {
|
||||
previewLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleFlowPreview() {
|
||||
if (!appMeta.value?.id || detailLoading.value) return
|
||||
if (previewTimer) clearTimeout(previewTimer)
|
||||
previewTimer = setTimeout(() => {
|
||||
loadFlowPreview()
|
||||
}, 300)
|
||||
}
|
||||
|
||||
async function loadByAppId(appId) {
|
||||
const detail = await fetchOaTemplateDetail(appId)
|
||||
appMeta.value = {
|
||||
id: appId,
|
||||
appName: detail?.appName || detail?.name || '发起审批'
|
||||
}
|
||||
const prepared = prepareMobileFormSchema(detail?.formSchemaJson)
|
||||
formRule.value = prepared.rule
|
||||
formOption.value = prepared.option
|
||||
await loadFlowPreview()
|
||||
}
|
||||
|
||||
async function bootstrap() {
|
||||
detailLoading.value = true
|
||||
skipPreviewWatch = true
|
||||
try {
|
||||
const appId = String(route.query.appId || '').trim()
|
||||
const draftId = String(route.query.draftId || '').trim()
|
||||
if (draftId) {
|
||||
const d = await fetchOaInstanceDraftDetail(draftId)
|
||||
editingDraftId.value = draftId
|
||||
const aid = String(d?.appId || '').trim()
|
||||
if (!aid) throw new Error('草稿缺少 appId')
|
||||
formData.value = d.formData && typeof d.formData === 'object' ? { ...d.formData } : {}
|
||||
initiatorSelectByNode.value =
|
||||
d.initiatorSelectByNode && typeof d.initiatorSelectByNode === 'object'
|
||||
? { ...d.initiatorSelectByNode }
|
||||
: {}
|
||||
await loadByAppId(aid)
|
||||
} else if (appId) {
|
||||
formData.value = {}
|
||||
initiatorSelectByNode.value = {}
|
||||
editingDraftId.value = ''
|
||||
await loadByAppId(appId)
|
||||
} else {
|
||||
showToast('缺少 appId')
|
||||
}
|
||||
} catch (e) {
|
||||
showToast(e?.message || '加载失败')
|
||||
} finally {
|
||||
detailLoading.value = false
|
||||
skipPreviewWatch = false
|
||||
}
|
||||
}
|
||||
|
||||
function onCancel() {
|
||||
closeToMini({ action: 'oa-start-cancel' })
|
||||
}
|
||||
|
||||
async function saveDraft() {
|
||||
if (!appMeta.value?.id) return
|
||||
savingDraft.value = true
|
||||
try {
|
||||
const payload = {
|
||||
formData: { ...formData.value },
|
||||
initiatorSelectByNode: { ...initiatorSelectByNode.value },
|
||||
title: appMeta.value.appName || ''
|
||||
}
|
||||
if (editingDraftId.value) {
|
||||
await fetchUpdateOaInstanceDraft(editingDraftId.value, payload)
|
||||
} else {
|
||||
const created = await fetchCreateOaInstanceDraft({
|
||||
appId: appMeta.value.id,
|
||||
...payload
|
||||
})
|
||||
if (created?.id) editingDraftId.value = created.id
|
||||
}
|
||||
showToast('草稿已保存')
|
||||
closeToMini({ action: 'oa-start-draft', draftId: editingDraftId.value })
|
||||
} catch (e) {
|
||||
showToast(e?.message || '保存草稿失败')
|
||||
} finally {
|
||||
savingDraft.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
if (!appMeta.value?.id) return
|
||||
for (const s of flowSteps.value) {
|
||||
if (s.type !== 'approval') continue
|
||||
const types =
|
||||
Array.isArray(s.assigneeTypes) && s.assigneeTypes.length
|
||||
? s.assigneeTypes
|
||||
: s.assigneeType
|
||||
? [s.assigneeType]
|
||||
: []
|
||||
if (!types.includes('initiator_select')) continue
|
||||
const ids = initiatorSelectByNode.value[s.id] || []
|
||||
if (!ids.length) {
|
||||
showToast(`请为「${s.name || '审批人'}」选择审批人`)
|
||||
return
|
||||
}
|
||||
}
|
||||
if (fapi.value?.validate) {
|
||||
try {
|
||||
await fapi.value.validate()
|
||||
} catch {
|
||||
showToast('请完善表单必填项')
|
||||
return
|
||||
}
|
||||
}
|
||||
submitting.value = true
|
||||
try {
|
||||
const payload = {
|
||||
formData: { ...formData.value },
|
||||
initiatorSelectByNode: { ...initiatorSelectByNode.value }
|
||||
}
|
||||
let data
|
||||
if (editingDraftId.value) {
|
||||
data = await fetchSubmitOaInstanceDraft(editingDraftId.value, {
|
||||
...payload,
|
||||
title: appMeta.value.appName || ''
|
||||
})
|
||||
} else {
|
||||
data = await fetchStartOaInstance({
|
||||
appId: appMeta.value.id,
|
||||
...payload
|
||||
})
|
||||
}
|
||||
showToast('提交成功')
|
||||
closeToMini({
|
||||
action: 'oa-start-submitted',
|
||||
instanceId: data?.id,
|
||||
message: '提交成功'
|
||||
})
|
||||
} catch (e) {
|
||||
showToast(e?.message || '提交失败')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
[formData, initiatorSelectByNode],
|
||||
() => {
|
||||
if (skipPreviewWatch) return
|
||||
scheduleFlowPreview()
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
bootstrap()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (previewTimer) clearTimeout(previewTimer)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.oa-start {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--bg-page, #f5f8f7);
|
||||
}
|
||||
.oa-start__hd {
|
||||
padding: 14px 16px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
background: linear-gradient(135deg, var(--brand-green, #0a9f6d), var(--brand-green-dark, #087f57));
|
||||
}
|
||||
.oa-start__loading {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 48px 16px;
|
||||
}
|
||||
.oa-start__body {
|
||||
flex: 1;
|
||||
padding: 12px 12px 96px;
|
||||
}
|
||||
.oa-start__ft {
|
||||
position: fixed;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 10px 12px calc(10px + env(safe-area-inset-bottom));
|
||||
background: #fff;
|
||||
border-top: 1px solid var(--border, #dcebe4);
|
||||
}
|
||||
.oa-start__btn {
|
||||
flex: 1;
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user