204 lines
8.3 KiB
Vue
204 lines
8.3 KiB
Vue
<script setup lang="ts">
|
||
/**
|
||
* 在线办理(发起页):CMS 指南页"我要办理"跳入 → /#/apply?siteCode=&itemCode=
|
||
*
|
||
* 提交反馈纪律(BP-R4-1 修复,务必保留语义):
|
||
* 后端代发起是"落库(PENDING=0) → AFTER_COMMIT 异步出站 → 回调推进 10"。所以**提交成功的那一刻
|
||
* flowStatus 必然是 0**,随后秒级翻转。原实现把 flowStatus===0 一律当"流程中心暂不可用",
|
||
* 于是明明提交成功也弹失败提示。现在按 flowEnabled 分流 + 短轮询收敛:
|
||
* flowEnabled=false → 流程中心未接入,真的不会翻转:如实告知"已保存为待提交";
|
||
* flowEnabled=true → 排队中:轮询详情等状态翻转(10=审批中),翻转即报成功;
|
||
* 超时未翻转才提示"已保存为待提交,可在我的办件重试"(此时多半真失败了)。
|
||
*/
|
||
import { computed, onMounted, reactive, ref } from "vue";
|
||
import { useRoute, useRouter } from "vue-router";
|
||
import { Message } from "view-ui-plus";
|
||
import { applyApi } from "@/api/module/svc";
|
||
import type { ApplyAttachmentItem } from "@/types/svc";
|
||
import { useSite } from "@/utils/site";
|
||
import DynamicForm from "@/components/DynamicForm.vue";
|
||
const route = useRoute();
|
||
const router = useRouter();
|
||
const { resolveSite, resolveFrom, rememberItem } = useSite();
|
||
|
||
/** 轮询参数:代发起 + 回调一般在 1s 内完成,给到 ~9s 足够,避免用户盯着"待提交"发懵 */
|
||
const POLL_TIMES = 12;
|
||
const POLL_INTERVAL_MS = 750;
|
||
|
||
const loading = ref(false);
|
||
const submitting = ref(false);
|
||
const item = ref<Record<string, any> | null>(null);
|
||
const loadError = ref("");
|
||
const siteCode = ref("");
|
||
const formData = reactive<Record<string, any>>({});
|
||
const dynFormRef = ref<InstanceType<typeof DynamicForm> | null>(null); // DynamicForm.validate()
|
||
|
||
const schemaJson = computed(() => (item.value?.schemaJson as string) || "");
|
||
|
||
async function load() {
|
||
siteCode.value = resolveSite(route.query.siteCode);
|
||
// CMS 指南页来路(T4):校验通过即记入 session,App.vue 顶栏渲染"返回门户"
|
||
resolveFrom(route.query.from);
|
||
const itemCode = String(route.query.itemCode || "");
|
||
if (!siteCode.value || !itemCode) {
|
||
loadError.value = "缺少 siteCode/itemCode 参数";
|
||
return;
|
||
}
|
||
rememberItem(itemCode);
|
||
loading.value = true;
|
||
try {
|
||
item.value = (await applyApi.item(siteCode.value, itemCode).send()) as any;
|
||
loadError.value = "";
|
||
} catch (e: any) {
|
||
// 区分加载失败与业务性不可办理:网络/服务异常不当成"事项不可办理"
|
||
loadError.value = String(e?.message || e || "网络或服务异常");
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
}
|
||
|
||
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
|
||
|
||
/**
|
||
* 从 formData 提取附件行(upload/image 字段值为 /api/file/upload 返回体数组,
|
||
* 每项含附件行 id):后端 submit 依据 attachments 做办件材料绑定(bindApply),
|
||
* 不传则附件行永远未落 topic,表现为「附件传了但提交后无效」。
|
||
*/
|
||
function collectAttachments(): ApplyAttachmentItem[] {
|
||
const out: ApplyAttachmentItem[] = [];
|
||
try {
|
||
const groups = JSON.parse(schemaJson.value || "{}").groups || [];
|
||
for (const g of groups) {
|
||
for (const f of g.fields || []) {
|
||
if (f.type !== "upload" && f.type !== "image") continue;
|
||
const list = formData[f.key];
|
||
for (const it of Array.isArray(list) ? list : []) {
|
||
if (it && it.id != null) out.push({ fieldKey: f.key, attachmentId: it.id });
|
||
}
|
||
}
|
||
}
|
||
} catch {
|
||
// schema 解析失败时只丢附件绑定,不阻断提交
|
||
}
|
||
return out;
|
||
}
|
||
|
||
/** 轮询办件状态直到离开 PENDING;超时返回最后一次看到的办件(可能仍是待提交) */
|
||
async function awaitFlowStart(applyId: number, first: any) {
|
||
let latest = first;
|
||
for (let i = 0; i < POLL_TIMES; i++) {
|
||
if (latest && latest.flowStatus !== 0 && latest.flowStatus !== undefined && latest.flowStatus !== null) {
|
||
return latest;
|
||
}
|
||
await sleep(POLL_INTERVAL_MS);
|
||
try {
|
||
latest = (await applyApi.detail(applyId).send()) as any;
|
||
} catch {
|
||
// 轮询期间的偶发失败不打断流程,继续下一轮
|
||
}
|
||
}
|
||
return latest;
|
||
}
|
||
|
||
async function submit() {
|
||
if (!item.value) return;
|
||
// 前端先按 schema 校验(字段下方内联红字 + 滚到第一个错误),不再把"必填项未填写"
|
||
// 这类校验留给后端弹 Message——后端强校验仍保留作兜底
|
||
if (dynFormRef.value && !dynFormRef.value.validate()) return;
|
||
submitting.value = true;
|
||
try {
|
||
const apply: any = await applyApi
|
||
.submit({
|
||
siteCode: siteCode.value,
|
||
itemCode: String(route.query.itemCode),
|
||
formData: Object.keys(formData).length ? JSON.stringify(formData) : undefined,
|
||
attachments: collectAttachments(),
|
||
})
|
||
.send();
|
||
|
||
// ① 流程中心未接入:不会翻转,如实告知(不说"暂不可用",因为压根没接)
|
||
if (apply.flowEnabled === false) {
|
||
Message.warning("流程中心未接入,申请已保存为待提交;请联系管理员开通后重试");
|
||
} else if (apply.flowStatus === 0) {
|
||
// ② 排队中:等状态翻转(成功即成功提示,不再误报)
|
||
const latest = await awaitFlowStart(apply.id, apply);
|
||
if (latest && latest.flowStatus && latest.flowStatus !== 0) {
|
||
Message.success("已提交,进入审批流程");
|
||
} else {
|
||
Message.warning("申请已保存为待提交:流程中心响应较慢,可在「我的办件」中重试");
|
||
}
|
||
} else {
|
||
Message.success("已提交,进入审批流程");
|
||
}
|
||
router.replace(`/apply-detail?id=${apply.id}`);
|
||
} finally {
|
||
submitting.value = false;
|
||
}
|
||
}
|
||
|
||
onMounted(load);
|
||
</script>
|
||
|
||
<template>
|
||
<div class="page">
|
||
<Card :bordered="false" dis-hover v-if="item">
|
||
<template #title>在线办理 · {{ item.title }}</template>
|
||
<div class="summary" v-if="item.summary || item.deptName || item.handleTimeLimit">
|
||
<div v-if="item.summary">{{ item.summary }}</div>
|
||
<div class="meta">
|
||
<span v-if="item.deptName">办理部门:{{ item.deptName }}</span>
|
||
<span v-if="item.handleTimeLimit">办理时限:{{ item.handleTimeLimit }}</span>
|
||
<span v-if="item.contactPhone">咨询电话:{{ item.contactPhone }}</span>
|
||
</div>
|
||
</div>
|
||
<Divider v-if="schemaJson" orientation="left">申请表单</Divider>
|
||
<DynamicForm v-if="schemaJson" ref="dynFormRef" :schema-json="schemaJson" :form-data="formData" />
|
||
<div v-else class="no-form">该事项无需填写表单,提交后由办理部门联系确认。</div>
|
||
<div class="actions">
|
||
<Button class="sh-btn-primary" type="primary" :loading="submitting" @click="submit">
|
||
提交申请
|
||
</Button>
|
||
<Button @click="router.push('/my')">我的办件</Button>
|
||
<Button @click="router.push('/hall')">返回大厅</Button>
|
||
</div>
|
||
</Card>
|
||
<Card :bordered="false" dis-hover v-else>
|
||
<div class="state-tip">
|
||
<template v-if="loading">加载中…</template>
|
||
<template v-else-if="loadError">
|
||
事项加载失败:{{ loadError }}
|
||
</template>
|
||
<template v-else>事项不可办理(未发布或未开通在线办理)</template>
|
||
</div>
|
||
</Card>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.summary {
|
||
padding-bottom: 12px;
|
||
}
|
||
.meta {
|
||
margin-top: 8px;
|
||
display: flex;
|
||
gap: 16px;
|
||
color: var(--sh-text-weak);
|
||
font-size: 13px;
|
||
flex-wrap: wrap;
|
||
}
|
||
.no-form {
|
||
color: var(--sh-text-weak);
|
||
padding: 12px 0;
|
||
}
|
||
.actions {
|
||
margin-top: 16px;
|
||
display: flex;
|
||
gap: 8px;
|
||
}
|
||
.state-tip {
|
||
text-align: center;
|
||
padding: 40px;
|
||
color: var(--sh-text-weak);
|
||
}
|
||
</style>
|