Files
svc-hall-hall/src/views/Apply.vue
T

204 lines
8.3 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>