feat(hall): 大厅卡片信息密度 + 我的办事五分区 + 提交后状态告知
- Hall.vue:承诺时限/跑动次数、四态角标、卡片点击进指南 - ApplyMy.vue:待我审批/我发起的/已办/抄送/收藏 五分区 - Apply.vue:提交成功页(办件号 + 预计完成) - ApplyDetail.vue:进度条 + 已历时 + 催办占位 + 评价占位
This commit is contained in:
+201
-13
@@ -8,11 +8,10 @@
|
||||
* 于是明明提交成功也弹失败提示。现在按 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";
|
||||
@@ -33,8 +32,49 @@ const siteCode = ref("");
|
||||
const formData = reactive<Record<string, any>>({});
|
||||
const dynFormRef = ref<InstanceType<typeof DynamicForm> | null>(null); // DynamicForm.validate()
|
||||
|
||||
/** 提交后的状态告知(P0-5):成功页区块取代一次性 Message,用户看得到"下一步与预计完成" */
|
||||
const done = ref<{
|
||||
applyId: number;
|
||||
itemTitle: string;
|
||||
headline: string;
|
||||
tip: string;
|
||||
tone: "success" | "warning";
|
||||
currentNode: string;
|
||||
promise: string;
|
||||
eta: string;
|
||||
} | null>(null);
|
||||
|
||||
const schemaJson = computed(() => (item.value?.schemaJson as string) || "");
|
||||
|
||||
/** 需跑现场次数(0=零跑动):/api/apply/item 未下发该字段时整段隐藏,不编造 */
|
||||
const runText = computed(() => {
|
||||
const n = item.value?.runCount;
|
||||
if (n === null || n === undefined || n === "") return "";
|
||||
const num = Number(n);
|
||||
if (Number.isNaN(num)) return "";
|
||||
return num === 0 ? "零跑动" : `需跑 ${num} 次`;
|
||||
});
|
||||
|
||||
/**
|
||||
* 承诺时限 → 天数:取文案里最大的数字("1-3个工作日" 按上限 3 估,不把承诺说短)。
|
||||
* 解析不出("当日办结""次周内")返回 null,由调用方回落为原文展示。
|
||||
*/
|
||||
function parsePromiseDays(text: any): number | null {
|
||||
const nums = String(text || "").match(/\d+/g);
|
||||
if (!nums || !nums.length) return null;
|
||||
const n = Math.max(...nums.map(Number));
|
||||
return n > 0 && n <= 365 ? n : null;
|
||||
}
|
||||
|
||||
const pad2 = (n: number) => String(n).padStart(2, "0");
|
||||
|
||||
/** 提交日 + N 天(按自然日加,工作日口径待后端确认,见交付说明) */
|
||||
function addDays(base: Date, n: number): string {
|
||||
const d = new Date(base.getTime());
|
||||
d.setDate(d.getDate() + n);
|
||||
return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`;
|
||||
}
|
||||
|
||||
async function load() {
|
||||
siteCode.value = resolveSite(route.query.siteCode);
|
||||
// CMS 指南页来路(T4):校验通过即记入 session,App.vue 顶栏渲染"返回门户"
|
||||
@@ -100,6 +140,27 @@ async function awaitFlowStart(applyId: number, first: any) {
|
||||
return latest;
|
||||
}
|
||||
|
||||
/** 后端时间为 `yyyy-MM-dd HH:mm:ss` / ISO 两种形态,统一转 Date;解析失败返回 null */
|
||||
function toDate(s?: string | null): Date | null {
|
||||
if (!s) return null;
|
||||
const d = new Date(String(s).trim().replace(" ", "T"));
|
||||
return Number.isNaN(d.getTime()) ? null : d;
|
||||
}
|
||||
|
||||
/** 当前节点:进度接口里 active 节点的名称;进度不可见时留空(该行整体不显示) */
|
||||
async function currentNodeName(applyId: number): Promise<string> {
|
||||
try {
|
||||
const p: any = await applyApi.progress(applyId).send();
|
||||
const names = (p?.steps || [])
|
||||
.filter((st: any) => st.active)
|
||||
.map((st: any) => st.nodeName || st.label)
|
||||
.filter(Boolean);
|
||||
return Array.from(new Set(names)).join("、");
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
if (!item.value) return;
|
||||
// 前端先按 schema 校验(字段下方内联红字 + 滚到第一个错误),不再把"必填项未填写"
|
||||
@@ -116,21 +177,41 @@ async function submit() {
|
||||
})
|
||||
.send();
|
||||
|
||||
// 分流语义与提示文案保持不变(BP-R4-1),只是把结果落成成功页区块而非一闪而过的 Message
|
||||
let latest = apply;
|
||||
let headline = "已提交,进入审批流程";
|
||||
let tip = "已进入审批流程,可在办件详情查看进度";
|
||||
let tone: "success" | "warning" = "success";
|
||||
// ① 流程中心未接入:不会翻转,如实告知(不说"暂不可用",因为压根没接)
|
||||
if (apply.flowEnabled === false) {
|
||||
Message.warning("流程中心未接入,申请已保存为待提交;请联系管理员开通后重试");
|
||||
headline = "申请已保存为待提交";
|
||||
tip = "流程中心未接入,请联系管理员开通后在「我的办事」重试提交";
|
||||
tone = "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("申请已保存为待提交:流程中心响应较慢,可在「我的办件」中重试");
|
||||
latest = (await awaitFlowStart(apply.id, apply)) || apply;
|
||||
if (!(latest && latest.flowStatus && latest.flowStatus !== 0)) {
|
||||
headline = "申请已保存为待提交";
|
||||
tip = "流程中心响应较慢,可在「我的办事」中重试提交";
|
||||
tone = "warning";
|
||||
}
|
||||
} else {
|
||||
Message.success("已提交,进入审批流程");
|
||||
}
|
||||
router.replace(`/apply-detail?id=${apply.id}`);
|
||||
|
||||
const promise = String(item.value?.promiseTimeLimit || "");
|
||||
const days = parsePromiseDays(promise);
|
||||
const submittedAt = toDate(latest?.submitTime || latest?.createTime) || new Date();
|
||||
done.value = {
|
||||
applyId: Number(apply.id),
|
||||
itemTitle: String(item.value?.title || ""),
|
||||
headline,
|
||||
tip,
|
||||
tone,
|
||||
currentNode: tone === "success" ? await currentNodeName(apply.id) : "",
|
||||
promise,
|
||||
// 解析不出数字时不猜日期:该行直接显示承诺时限原文
|
||||
eta: days ? addDays(submittedAt, days) : promise,
|
||||
};
|
||||
window.scrollTo(0, 0);
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
@@ -141,7 +222,38 @@ onMounted(load);
|
||||
|
||||
<template>
|
||||
<div class="page">
|
||||
<Card :bordered="false" dis-hover v-if="item">
|
||||
<!-- 提交后的状态告知(P0-5):办件号 + 当前节点 + 承诺时限 + 预计完成,替代一闪而过的 Message -->
|
||||
<Card :bordered="false" dis-hover v-if="done">
|
||||
<div class="done">
|
||||
<Icon :type="done?.tone === 'success' ? 'md-checkmark-circle' : 'md-alert-circle'" :size="46"
|
||||
:class="['done-ico', done?.tone]" />
|
||||
<div class="done-headline">{{ done?.headline }}</div>
|
||||
<div class="done-tip sh-muted">{{ done?.tip }}</div>
|
||||
<div class="done-kv">
|
||||
<div class="dk" v-if="done?.itemTitle">
|
||||
<span class="k">办理事项</span><span class="v">{{ done?.itemTitle }}</span>
|
||||
</div>
|
||||
<div class="dk">
|
||||
<span class="k">办件号</span><span class="v b">{{ done?.applyId }}</span>
|
||||
</div>
|
||||
<div class="dk" v-if="done?.currentNode">
|
||||
<span class="k">当前节点</span><span class="v">{{ done?.currentNode }}</span>
|
||||
</div>
|
||||
<div class="dk" v-if="done?.promise">
|
||||
<span class="k">承诺时限</span><span class="v">{{ done?.promise }}</span>
|
||||
</div>
|
||||
<div class="dk" v-if="done?.eta">
|
||||
<span class="k">预计完成</span><span class="v b">{{ done?.eta }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="done-acts">
|
||||
<Button class="sh-btn-primary" type="primary"
|
||||
@click="router.push(`/apply-detail?id=${done?.applyId}`)">查看办件详情</Button>
|
||||
<Button @click="router.push('/hall')">再办一件</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<Card :bordered="false" dis-hover v-else-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>
|
||||
@@ -151,6 +263,14 @@ onMounted(load);
|
||||
<span v-if="item.contactPhone">咨询电话:{{ item.contactPhone }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 事项上下文条:填表前先把"谁办 / 多久 / 跑几次"摆在表单上方(字段缺失即不渲染) -->
|
||||
<div class="ctx-bar" v-if="item.deptName || item.promiseTimeLimit || runText">
|
||||
<span class="ctx-title">{{ item.title }}</span>
|
||||
<span class="sh-tag" v-if="item.deptName">{{ item.deptName }}</span>
|
||||
<span class="sh-tag sh-tag-primary" v-if="item.promiseTimeLimit">承诺 {{ item.promiseTimeLimit }}</span>
|
||||
<span class="sh-tag sh-tag-success" v-if="runText">{{ runText }}</span>
|
||||
<span class="sh-tag" v-if="item.handleTimeLimit">办理时限 {{ item.handleTimeLimit }}</span>
|
||||
</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>
|
||||
@@ -158,7 +278,7 @@ onMounted(load);
|
||||
<Button class="sh-btn-primary" type="primary" :loading="submitting" @click="submit">
|
||||
提交申请
|
||||
</Button>
|
||||
<Button @click="router.push('/my')">我的办件</Button>
|
||||
<Button @click="router.push('/my')">我的办事</Button>
|
||||
<Button @click="router.push('/hall')">返回大厅</Button>
|
||||
</div>
|
||||
</Card>
|
||||
@@ -178,6 +298,74 @@ onMounted(load);
|
||||
.summary {
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
.ctx-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
padding: 10px 12px;
|
||||
margin-bottom: 6px;
|
||||
border: 1px solid var(--sh-border);
|
||||
border-left: 3px solid var(--sh-primary);
|
||||
border-radius: calc(var(--sh-radius) - 4px);
|
||||
background: color-mix(in srgb, var(--sh-primary) 4%, #fff);
|
||||
}
|
||||
.ctx-title {
|
||||
font-weight: 600;
|
||||
}
|
||||
.done {
|
||||
text-align: center;
|
||||
padding: 24px 0 10px;
|
||||
}
|
||||
.done-ico {
|
||||
line-height: 1;
|
||||
}
|
||||
.done-ico.success {
|
||||
color: var(--sh-success);
|
||||
}
|
||||
.done-ico.warning {
|
||||
color: var(--sh-accent);
|
||||
}
|
||||
.done-headline {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin: 10px 0 4px;
|
||||
}
|
||||
.done-tip {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.done-kv {
|
||||
display: inline-block;
|
||||
text-align: left;
|
||||
min-width: 340px;
|
||||
max-width: 100%;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
.dk {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 6px 0;
|
||||
border-bottom: 1px dashed var(--sh-border);
|
||||
font-size: 14px;
|
||||
}
|
||||
.dk .k {
|
||||
width: 88px;
|
||||
color: var(--sh-text-weak);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.dk .v {
|
||||
flex: 1;
|
||||
word-break: break-all;
|
||||
}
|
||||
.dk .v.b {
|
||||
color: var(--sh-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
.done-acts {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
justify-content: center;
|
||||
}
|
||||
.meta {
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
|
||||
+206
-2
@@ -124,6 +124,7 @@ const SNAPSHOT_LABEL: Record<string, string> = {
|
||||
summary: "事项说明",
|
||||
deptName: "办理部门",
|
||||
handleTimeLimit: "办理时限",
|
||||
promiseTimeLimit: "承诺时限",
|
||||
contactPhone: "咨询电话",
|
||||
};
|
||||
|
||||
@@ -138,6 +139,78 @@ function parse(json?: string): Record<string, any> {
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 头部进度条(P0-5):节点全部取自 progress.steps,有几步画几步,不硬凑五格 =====
|
||||
const stepList = computed(() => progress.value?.steps || []);
|
||||
const activeIdx = computed(() => stepList.value.findIndex(st => st.active));
|
||||
|
||||
function stepStateAt(i: number): "done" | "now" | "pending" {
|
||||
const act = activeIdx.value;
|
||||
if (act >= 0) return i < act ? "done" : i === act ? "now" : "pending";
|
||||
// 无活动节点(终态或未推进):有办理时间的节点算已完成
|
||||
return (stepList.value[i]?.assignees || []).some(a => !!a.handleTime) ? "done" : "pending";
|
||||
}
|
||||
|
||||
const barSteps = computed(() =>
|
||||
stepList.value.map((st, i) => ({ name: st.nodeName || st.label || "—", state: stepStateAt(i) }))
|
||||
);
|
||||
|
||||
/** 后端时间两种形态(`yyyy-MM-dd HH:mm:ss` / ISO),解析失败返回 null(不显示该行,不猜) */
|
||||
function toDate(s?: string | null): Date | null {
|
||||
if (!s) return null;
|
||||
const d = new Date(String(s).trim().replace(" ", "T"));
|
||||
return Number.isNaN(d.getTime()) ? null : d;
|
||||
}
|
||||
|
||||
/** 已历时天数:提交 → 办结(在途则 → 现在),自然日向下取整 */
|
||||
const elapsedDays = computed<number | null>(() => {
|
||||
const from = toDate(apply.value?.submitTime);
|
||||
if (!from) return null;
|
||||
const to = toDate(apply.value?.finishTime) || new Date();
|
||||
return Math.max(0, Math.floor((to.getTime() - from.getTime()) / 86400000));
|
||||
});
|
||||
|
||||
/** 承诺时限文案/天数(快照未带 promiseTimeLimit 时回落办理时限;解析不出数字就不算余量) */
|
||||
const promiseText = computed(() =>
|
||||
String(snapshot.value?.promiseTimeLimit || snapshot.value?.handleTimeLimit || "")
|
||||
);
|
||||
const promiseDays = computed<number | null>(() => {
|
||||
const nums = promiseText.value.match(/\d+/g);
|
||||
if (!nums || !nums.length) return null;
|
||||
const n = Math.max(...nums.map(Number));
|
||||
return n > 0 && n <= 365 ? n : null;
|
||||
});
|
||||
const remainDays = computed<number | null>(() => {
|
||||
if (elapsedDays.value === null || promiseDays.value === null) return null;
|
||||
return promiseDays.value - elapsedDays.value;
|
||||
});
|
||||
|
||||
/** 一句话时长告知:"当前节点已历时 N 天,时限剩 M 天"(余量解析不出则只报已历时) */
|
||||
const clockText = computed(() => {
|
||||
const elapsed = elapsedDays.value;
|
||||
if (elapsed === null) return "";
|
||||
const base = `${activeIdx.value >= 0 ? "当前节点" : "本办件"}已历时 ${elapsed} 天`;
|
||||
const rest = remainDays.value;
|
||||
if (rest === null) return base;
|
||||
return rest >= 0 ? `${base},时限剩 ${rest} 天` : `${base},已超承诺时限 ${-rest} 天`;
|
||||
});
|
||||
const clockOver = computed(() => remainDays.value !== null && remainDays.value < 0);
|
||||
|
||||
/** 催办(P1-3 本期占位):只确认 + 记提示,不接后端;限频文案先行告知 */
|
||||
function urge() {
|
||||
Modal.confirm({
|
||||
title: "催办确认",
|
||||
content: `确认催办「${snapshot.itemTitle || apply.value?.itemTitle || "该办件"}」?系统会提醒当前审批人(同一办件 10 分钟内只能催一次)。`,
|
||||
onOk: () => {
|
||||
Message.success("已记录催办");
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** 服务评价(P1-6 本期占位) */
|
||||
function evaluate() {
|
||||
Message.info("评价功能即将上线");
|
||||
}
|
||||
|
||||
/** 附件/图片字段值 = /api/file/upload 返回体数组({fileName,fileUrl}),渲染为可点链接 */
|
||||
function fileList(v: any): { fileName: string; fileUrl: string }[] {
|
||||
if (!Array.isArray(v)) return [];
|
||||
@@ -274,6 +347,25 @@ onMounted(load);
|
||||
<span class="k">办结时间</span>
|
||||
<span class="v">{{ apply.finishTime }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 横向进度条:步骤与审批轨迹同源 -->
|
||||
<div class="progress" v-if="barSteps.length">
|
||||
<div v-for="(st, i) in barSteps" :key="i" class="pstep" :class="st.state">
|
||||
<div class="dot">{{ st.state === "done" ? "✓" : i + 1 }}</div>
|
||||
<div class="pstep-name">{{ st.name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clock-row" v-if="clockText">
|
||||
<div class="clock-text">
|
||||
<span :class="{ over: clockOver }">{{ clockText }}</span>
|
||||
<span class="sh-muted" v-if="promiseText">({{ promiseText }})</span>
|
||||
</div>
|
||||
<div class="clock-act" v-if="apply.flowStatus === 10">
|
||||
<Button size="small" @click="urge">催办</Button>
|
||||
<span class="sh-muted">同一办件 10 分钟内只能催一次</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sh-card" v-if="apply.formDataJson">
|
||||
@@ -312,7 +404,7 @@ onMounted(load);
|
||||
<div class="sh-card">
|
||||
<div class="section-title">审批进度</div>
|
||||
<div v-if="!progress || !progress.steps || !progress.steps.length" class="sh-muted">
|
||||
{{ apply.flowInstanceId ? "加载中或暂无可展示的轨迹" : "尚未进入审批流程(可在「我的办件」重试提交)" }}
|
||||
{{ apply.flowInstanceId ? "加载中或暂无可展示的轨迹" : "尚未进入审批流程(可在「我的办事」重试提交)" }}
|
||||
</div>
|
||||
<div class="timeline" v-else>
|
||||
<div class="step" v-for="(step, idx) in progress.steps" :key="idx">
|
||||
@@ -336,8 +428,17 @@ onMounted(load);
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 服务评价(P1-6 本期占位,后端未接) -->
|
||||
<div class="sh-card">
|
||||
<div class="section-title">服务评价</div>
|
||||
<p class="sh-muted">
|
||||
办结后可对本次服务评价;评价进入管理端「差评回访」清单,差评 100% 回访。
|
||||
</p>
|
||||
<Button class="sh-btn-primary" type="primary" @click="evaluate">去评价</Button>
|
||||
</div>
|
||||
|
||||
<div class="sh-card actions">
|
||||
<Button @click="router.push('/my')">返回我的办件</Button>
|
||||
<Button @click="router.push('/my')">返回我的办事</Button>
|
||||
<Button @click="router.push('/hall')">办事大厅</Button>
|
||||
<Button v-if="apply.flowInstanceId" @click="openDiagram">查看流程图</Button>
|
||||
<Button v-if="apply.flowStatus === 0" :loading="acting" @click="retry">重试提交</Button>
|
||||
@@ -378,6 +479,109 @@ onMounted(load);
|
||||
padding-left: 8px;
|
||||
border-left: 3px solid var(--sh-primary);
|
||||
}
|
||||
/* 头部进度条:已完成绿 / 当前蓝 / 待办灰,连线随已完成段着色(配色全部由令牌混合) */
|
||||
.progress {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin: 18px 0 4px;
|
||||
}
|
||||
.pstep {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
font-size: 12.5px;
|
||||
color: var(--sh-text-weak);
|
||||
}
|
||||
.pstep .dot {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 50%;
|
||||
background: var(--sh-border);
|
||||
color: var(--sh-text-weak);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 auto 6px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
border: 2px solid #fff;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.pstep-name {
|
||||
padding: 0 4px;
|
||||
word-break: break-all;
|
||||
}
|
||||
.pstep::before,
|
||||
.pstep::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 11px;
|
||||
height: 2px;
|
||||
background: var(--sh-border);
|
||||
}
|
||||
.pstep::before {
|
||||
left: 0;
|
||||
right: 50%;
|
||||
}
|
||||
.pstep::after {
|
||||
left: 50%;
|
||||
right: 0;
|
||||
}
|
||||
.pstep:first-child::before {
|
||||
display: none;
|
||||
}
|
||||
.pstep:last-child::after {
|
||||
display: none;
|
||||
}
|
||||
.pstep.done {
|
||||
color: var(--sh-success);
|
||||
}
|
||||
.pstep.done .dot {
|
||||
background: var(--sh-success);
|
||||
color: #fff;
|
||||
}
|
||||
.pstep.done::before,
|
||||
.pstep.done::after,
|
||||
.pstep.now::before {
|
||||
background: var(--sh-success);
|
||||
}
|
||||
.pstep.now {
|
||||
color: var(--sh-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
.pstep.now .dot {
|
||||
background: var(--sh-primary);
|
||||
color: #fff;
|
||||
box-shadow: 0 0 0 5px color-mix(in srgb, var(--sh-primary) 15%, transparent);
|
||||
}
|
||||
.clock-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 16px;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid var(--sh-border);
|
||||
border-radius: calc(var(--sh-radius) - 4px);
|
||||
background: color-mix(in srgb, var(--sh-accent) 8%, #fff);
|
||||
font-size: 13px;
|
||||
}
|
||||
.clock-text {
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
}
|
||||
.clock-text .over {
|
||||
color: var(--sh-danger);
|
||||
font-weight: 600;
|
||||
}
|
||||
.clock-act {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.group-title {
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
|
||||
+279
-47
@@ -1,22 +1,55 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 我的办件:镜像状态列表 + 撤回/重试/重提入口。
|
||||
* 撤回走本应用后端 /api/apply/{id}/withdraw(不再直调 flow-center:跨应用票据与 CORS 都不再需要,
|
||||
* "谁能撤"由后端 + 流程中心按 submitterId 双重把关)。
|
||||
* 我的办事(/my,P0-4 聚合页):待我审批 / 我发起的 / 已办 / 抄送我的 / 我的收藏 五分区一页。
|
||||
* 分区数量与列表同源:todo/done/cc 走流程中心代理(/api/apply/approval/{Todo,Done,Cc}),
|
||||
* my 走本地办件(/api/apply/my);收藏本期未接后端,恒为 0。
|
||||
* 三个审批分区直接复用 FlowApprovalList(不重写组件),宿主仍按 P1-09 负责换址代际与 bizId 校验。
|
||||
*
|
||||
* 我发起的 = 原"我的办件"能力,撤回/重试/重提语义未变:
|
||||
* 撤回走本应用后端 /api/apply/{id}/withdraw("谁能撤"由后端 + 流程中心按 submitterId 双重把关)。
|
||||
*/
|
||||
import { onMounted, ref } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { Message, Modal } from "view-ui-plus";
|
||||
import { applyApi } from "@/api/module/svc";
|
||||
import { applyApi, approvalApi } from "@/api/module/svc";
|
||||
import svcFlowApprovalApi from "@/api/module/approvalAdapter";
|
||||
import FlowApprovalList from "@/components/flow-approval/FlowApprovalList.vue";
|
||||
import FlowRecordDrawer from "@/components/flow-approval/FlowRecordDrawer.vue";
|
||||
import FlowTaskEmbed from "@/components/flow-approval/FlowTaskEmbed.vue";
|
||||
import type { ApplyVo } from "@/types/svc";
|
||||
|
||||
type Section = "todo" | "my" | "done" | "cc" | "fav";
|
||||
type ApprovalSection = "todo" | "done" | "cc";
|
||||
|
||||
const SECTIONS: Array<{ key: Section; label: string }> = [
|
||||
{ key: "todo", label: "待我审批" },
|
||||
{ key: "my", label: "我发起的" },
|
||||
{ key: "done", label: "已办" },
|
||||
{ key: "cc", label: "抄送我的" },
|
||||
{ key: "fav", label: "我的收藏" },
|
||||
];
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
/** 分区用 tab 态切换(不改路由;路由表由并行任务 B 维护),query 仅用于外部深链 */
|
||||
const querySection = String(route.query.section || route.query.tab || "") as Section;
|
||||
const section = ref<Section>(SECTIONS.some(s => s.key === querySection) ? querySection : "my");
|
||||
|
||||
const counts = ref<Record<Section, number>>({ todo: 0, my: 0, done: 0, cc: 0, fav: 0 });
|
||||
|
||||
const faTabs = computed<ApprovalSection[]>(() => {
|
||||
const s = section.value;
|
||||
return s === "todo" || s === "done" || s === "cc" ? [s] : [];
|
||||
});
|
||||
|
||||
const loading = ref(false);
|
||||
const list = ref<ApplyVo[]>([]);
|
||||
const total = ref(0);
|
||||
const page = ref(1);
|
||||
const flowStatus = ref<number | undefined>(undefined);
|
||||
const actingId = ref<number>(0);
|
||||
const approvalRef = ref<InstanceType<typeof FlowApprovalList> | null>(null);
|
||||
|
||||
const STATUS: Record<number, { text: string; cls: string }> = {
|
||||
0: { text: "待提交", cls: "sh-tag" },
|
||||
@@ -26,6 +59,21 @@ const STATUS: Record<number, { text: string; cls: string }> = {
|
||||
40: { text: "已撤回", cls: "sh-tag sh-tag-warning" },
|
||||
};
|
||||
|
||||
/** 五个分区各取 total(pageSize=1 只为数数);未登录/后端拒绝时该分区记 0,不阻塞其他分区 */
|
||||
async function loadCounts() {
|
||||
const jobs: Array<[Section, () => Promise<any>]> = [
|
||||
["todo", () => approvalApi.todo(1, 1).send()],
|
||||
["my", () => applyApi.my({ page: 1, pageSize: 1 }).send()],
|
||||
["done", () => approvalApi.done(1, 1).send()],
|
||||
["cc", () => approvalApi.cc(1, 1).send()],
|
||||
];
|
||||
const settled = await Promise.allSettled(jobs.map(([, run]) => run()));
|
||||
settled.forEach((r, i) => {
|
||||
const next = r.status === "fulfilled" ? Number(r.value?.total) || 0 : 0;
|
||||
counts.value[jobs[i][0]] = next;
|
||||
});
|
||||
}
|
||||
|
||||
async function load() {
|
||||
loading.value = true;
|
||||
try {
|
||||
@@ -39,13 +87,32 @@ async function load() {
|
||||
}
|
||||
}
|
||||
|
||||
function switchSection(key: Section) {
|
||||
if (section.value === key) return;
|
||||
section.value = key;
|
||||
if (key === "my") {
|
||||
page.value = 1;
|
||||
load();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 业务编号(原型 P0-4 行内展示)。本期后端未下发正式编号(P1-5),
|
||||
* 先按 `SVC-提交日-办件号` 占位;提交时间缺失时退回显示"办件号 {id}",不编造日期。
|
||||
*/
|
||||
function bizNo(row: ApplyVo): string {
|
||||
const day = String(row.submitTime || row.createTime || "").slice(0, 10).replace(/-/g, "");
|
||||
if (!/^\d{8}$/.test(day)) return `办件号 ${row.id}`;
|
||||
return `SVC-${day}-${String(row.id).padStart(6, "0")}`;
|
||||
}
|
||||
|
||||
/** 待提交态重试(后端复用同一 bizId 再次代发起,流程中心 uk 幂等) */
|
||||
async function retry(row: ApplyVo) {
|
||||
actingId.value = row.id;
|
||||
try {
|
||||
await applyApi.retry(row.id).send();
|
||||
Message.success("已重新提交");
|
||||
load();
|
||||
refresh();
|
||||
} catch (e: any) {
|
||||
Message.error(e?.message || "重试失败");
|
||||
} finally {
|
||||
@@ -63,7 +130,7 @@ async function withdraw(row: ApplyVo) {
|
||||
try {
|
||||
await applyApi.withdraw(row.id).send();
|
||||
Message.success("已撤回,可重新提交");
|
||||
load();
|
||||
refresh();
|
||||
} catch (e: any) {
|
||||
// 典型原因:"已有任务被办理,不能撤回"
|
||||
Message.error(e?.message || "撤回失败");
|
||||
@@ -84,7 +151,7 @@ async function resubmit(row: ApplyVo) {
|
||||
try {
|
||||
await applyApi.resubmit(row.id).send();
|
||||
Message.success("已重新提交,进入审批流程");
|
||||
load();
|
||||
refresh();
|
||||
} catch (e: any) {
|
||||
Message.error(e?.message || "重新提交失败");
|
||||
} finally {
|
||||
@@ -94,56 +161,221 @@ async function resubmit(row: ApplyVo) {
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(load);
|
||||
/** 写动作后列表与分区计数一起收敛 */
|
||||
function refresh() {
|
||||
if (section.value === "my") load();
|
||||
loadCounts();
|
||||
}
|
||||
|
||||
// ===== FlowApprovalList 宿主接线(与 MyApprovals 同一纪律,不改组件本体) =====
|
||||
const recordVisible = ref(false);
|
||||
const recordBizId = ref("");
|
||||
|
||||
function openRecord(bizId: string) {
|
||||
recordBizId.value = bizId;
|
||||
recordVisible.value = true;
|
||||
}
|
||||
|
||||
const taskVisible = ref(false);
|
||||
const taskUrl = ref("");
|
||||
const taskTaskId = ref<number | string>("");
|
||||
const taskBizId = ref<string | number>("");
|
||||
/** 换址请求代际号:关闭/换件时 +1,晚到响应按代际丢弃(P1-09) */
|
||||
let taskReqEpoch = 0;
|
||||
|
||||
async function openTask(payload: { bizId: string; taskId: number }) {
|
||||
const epoch = ++taskReqEpoch;
|
||||
taskBizId.value = payload.bizId;
|
||||
taskTaskId.value = payload.taskId;
|
||||
taskUrl.value = "";
|
||||
taskVisible.value = true;
|
||||
try {
|
||||
const res: any = await svcFlowApprovalApi.taskPageUrl(payload.bizId, payload.taskId);
|
||||
if (epoch !== taskReqEpoch) return; // 已关闭/已换件:废弃
|
||||
const url = res?.pageUrl || "";
|
||||
if (!url) {
|
||||
Message.error("任务页地址获取失败");
|
||||
taskVisible.value = false;
|
||||
return;
|
||||
}
|
||||
taskUrl.value = url;
|
||||
} catch (e: any) {
|
||||
if (epoch !== taskReqEpoch) return;
|
||||
Message.error(e?.msg || e?.message || "任务页地址获取失败");
|
||||
taskVisible.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function onTaskClose() {
|
||||
taskReqEpoch++;
|
||||
taskUrl.value = "";
|
||||
taskTaskId.value = "";
|
||||
taskBizId.value = "";
|
||||
}
|
||||
|
||||
function onHandled() {
|
||||
approvalRef.value?.load();
|
||||
loadCounts();
|
||||
}
|
||||
|
||||
/** open-biz:只用与当前办件上下文一致的 bizId 构造 /apply-detail?id=(P1-09) */
|
||||
function onOpenBiz(payload: { bizId?: string | null }) {
|
||||
const msgBizId = payload.bizId != null ? String(payload.bizId) : "";
|
||||
const ctxBizId = String(taskBizId.value || "");
|
||||
if (!msgBizId && !ctxBizId) return;
|
||||
if (msgBizId && ctxBizId && msgBizId !== ctxBizId) return;
|
||||
const applyId = Number(msgBizId || ctxBizId);
|
||||
if (!Number.isInteger(applyId) || applyId <= 0) return;
|
||||
taskVisible.value = false;
|
||||
router.push(`/apply-detail?id=${applyId}`);
|
||||
}
|
||||
|
||||
/** 查看详情(只用本行 id 构造路由) */
|
||||
function openDetail(applyId: number | string) {
|
||||
const id = Number(applyId);
|
||||
if (!Number.isInteger(id) || id <= 0) return;
|
||||
router.push(`/apply-detail?id=${id}`);
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
loadCounts();
|
||||
if (section.value === "my") await load();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page">
|
||||
<div class="sh-card bar">
|
||||
<Select v-model="flowStatus" clearable placeholder="全部状态" style="width: 150px"
|
||||
@on-change="() => { page = 1; load(); }">
|
||||
<Option v-for="(v, k) in STATUS" :key="k" :value="Number(k)">{{ v.text }}</Option>
|
||||
</Select>
|
||||
<span class="sh-muted">共 {{ total }} 条</span>
|
||||
<Button class="sh-btn-primary" type="primary" size="small" @click="router.push('/hall')">
|
||||
去办事大厅
|
||||
</Button>
|
||||
<div class="page mine-page">
|
||||
<div class="page-head">
|
||||
<h2 class="page-title">我的办事</h2>
|
||||
<span class="page-sub">「待我审批/已办/抄送我的」为流程中心数据(按本应用过滤);「我发起的」为本地办件</span>
|
||||
<Button size="small" @click="router.push('/hall')">去办事大厅</Button>
|
||||
</div>
|
||||
|
||||
<div class="sh-card" v-for="row in list" :key="row.id">
|
||||
<div class="row-head">
|
||||
<span class="row-title" @click="router.push(`/apply-detail?id=${row.id}`)">
|
||||
{{ row.itemTitle }}
|
||||
</span>
|
||||
<span :class="STATUS[row.flowStatus]?.cls || 'sh-tag'">
|
||||
{{ STATUS[row.flowStatus]?.text || row.flowStatus }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="row-meta sh-muted">
|
||||
<span>办件号 {{ row.id }}</span>
|
||||
<span>提交时间 {{ row.submitTime || "—" }}</span>
|
||||
<span v-if="row.flowInstanceId">流程实例 {{ row.flowInstanceId }}</span>
|
||||
</div>
|
||||
<div class="row-actions">
|
||||
<Button size="small" type="text" @click="router.push(`/apply-detail?id=${row.id}`)">详情</Button>
|
||||
<Button size="small" type="text" v-if="row.flowStatus === 0"
|
||||
:loading="actingId === row.id" @click="retry(row)">重试提交</Button>
|
||||
<Button size="small" type="text" v-if="row.flowStatus === 10"
|
||||
:loading="actingId === row.id" @click="withdraw(row)">撤回</Button>
|
||||
<Button size="small" type="text" v-if="row.flowStatus === 30 || row.flowStatus === 40"
|
||||
:loading="actingId === row.id" @click="resubmit(row)">重新提交</Button>
|
||||
</div>
|
||||
<div class="mine-tabs">
|
||||
<button v-for="s in SECTIONS" :key="s.key" type="button" class="mine-tab"
|
||||
:class="{ on: section === s.key }" @click="switchSection(s.key)">
|
||||
<div class="n">{{ counts[s.key] }}</div>
|
||||
<div class="l">{{ s.label }}</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="sh-card empty" v-if="!list.length && !loading">还没有办件,去办事大厅选一个事项开始吧</div>
|
||||
<!-- 我发起的:本地办件(状态筛选只作用于本分区) -->
|
||||
<template v-if="section === 'my'">
|
||||
<div class="sh-card bar">
|
||||
<Select v-model="flowStatus" clearable placeholder="全部状态" style="width: 150px"
|
||||
@on-change="() => { page = 1; load(); }">
|
||||
<Option v-for="(v, k) in STATUS" :key="k" :value="Number(k)">{{ v.text }}</Option>
|
||||
</Select>
|
||||
<span class="sh-muted">共 {{ total }} 条</span>
|
||||
</div>
|
||||
|
||||
<Page class="pager" v-if="total > 10" :total="total" :current="page" :page-size="10" show-total
|
||||
@on-change="(p: number) => { page = p; load(); }" />
|
||||
<Spin v-if="loading" fix></Spin>
|
||||
<div class="sh-card" v-for="row in list" :key="row.id">
|
||||
<div class="row-head">
|
||||
<span class="row-title" @click="openDetail(row.id)">
|
||||
{{ row.itemTitle }}
|
||||
</span>
|
||||
<span :class="STATUS[row.flowStatus]?.cls || 'sh-tag'">
|
||||
{{ STATUS[row.flowStatus]?.text || row.flowStatus }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="row-meta sh-muted">
|
||||
<span :title="bizNo(row).startsWith('SVC-') ? '占位编号,正式业务编号随后端 P1-5 下发' : ''">
|
||||
业务编号 {{ bizNo(row) }}
|
||||
</span>
|
||||
<span>提交时间 {{ row.submitTime || "—" }}</span>
|
||||
<span v-if="row.flowInstanceId">流程实例 {{ row.flowInstanceId }}</span>
|
||||
</div>
|
||||
<div class="row-actions">
|
||||
<Button size="small" type="text" @click="openDetail(row.id)">详情</Button>
|
||||
<Button size="small" type="text" v-if="row.flowStatus === 0"
|
||||
:loading="actingId === row.id" @click="retry(row)">重试提交</Button>
|
||||
<Button size="small" type="text" v-if="row.flowStatus === 10"
|
||||
:loading="actingId === row.id" @click="withdraw(row)">撤回</Button>
|
||||
<Button size="small" type="text" v-if="row.flowStatus === 30 || row.flowStatus === 40"
|
||||
:loading="actingId === row.id" @click="resubmit(row)">重新提交</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sh-card empty" v-if="!list.length && !loading">还没有办件,去办事大厅选一个事项开始吧</div>
|
||||
|
||||
<Page class="pager" v-if="total > 10" :total="total" :current="page" :page-size="10" show-total
|
||||
@on-change="(p: number) => { page = p; load(); }" />
|
||||
<Spin v-if="loading" fix></Spin>
|
||||
</template>
|
||||
|
||||
<!-- 待我审批 / 已办 / 抄送我的:复用审批列表组件(单分区页签,切换即重挂载自取数据) -->
|
||||
<FlowApprovalList v-else-if="faTabs.length" ref="approvalRef" :key="section" :api="svcFlowApprovalApi"
|
||||
:tabs="faTabs" @open-record="openRecord" @open-task="openTask" @handled="onHandled"
|
||||
@open-detail="openDetail" />
|
||||
|
||||
<div class="sh-card empty" v-else>
|
||||
收藏功能本期未接入后端(P1-2),先在事项卡片上查看,稍后可在这里管理
|
||||
</div>
|
||||
|
||||
<FlowRecordDrawer v-model="recordVisible" :api="svcFlowApprovalApi" :biz-id="recordBizId" />
|
||||
<FlowTaskEmbed v-model="taskVisible" :page-url="taskUrl" :task-id="taskTaskId" :biz-id="taskBizId"
|
||||
@handled="onHandled" @open-biz="onOpenBiz" @close="onTaskClose" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.mine-page {
|
||||
max-width: 1080px;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.page-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.page-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
.page-sub {
|
||||
font-size: 13px;
|
||||
color: var(--sh-text-weak);
|
||||
margin-right: auto;
|
||||
}
|
||||
.mine-tabs {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
.mine-tab {
|
||||
background: #fff;
|
||||
border: 1px solid var(--sh-border);
|
||||
border-radius: var(--sh-radius);
|
||||
padding: 10px 16px;
|
||||
min-width: 112px;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
transition: 0.15s;
|
||||
}
|
||||
.mine-tab:hover {
|
||||
border-color: var(--sh-primary);
|
||||
}
|
||||
.mine-tab.on {
|
||||
border-color: var(--sh-primary);
|
||||
background: color-mix(in srgb, var(--sh-primary) 8%, #fff);
|
||||
}
|
||||
.mine-tab .n {
|
||||
font-size: 19px;
|
||||
font-weight: 700;
|
||||
color: var(--sh-primary);
|
||||
line-height: 1.2;
|
||||
}
|
||||
.mine-tab .l {
|
||||
font-size: 12px;
|
||||
color: var(--sh-text-weak);
|
||||
}
|
||||
.bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
+87
-14
@@ -2,9 +2,11 @@
|
||||
/**
|
||||
* 办事大厅(事项大厅):学生/教师/访客的办理入口。
|
||||
* 数据源 /api/hall/items(后端复用门户 oapi 的已发布事项口径),支持关键词 + 服务对象/分类/部门筛选。
|
||||
* 卡片只承载"多久办完 / 要不要跑现场 / 能不能在线办"三件事,点击一律先进事项指南(/item),
|
||||
* 不在大厅直连表单——外链事项例外,其入口在站外,直接新窗口。
|
||||
* 列表布局由皮肤包决定(宿主只给 .sh-hall-grid 钩子,见 src/themes/**)。
|
||||
*/
|
||||
import { onMounted, reactive, ref } from "vue";
|
||||
import { computed, onMounted, reactive, ref } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { hallApi } from "@/api/module/svc";
|
||||
import { useSite } from "@/utils/site";
|
||||
@@ -87,12 +89,30 @@ function search() {
|
||||
load();
|
||||
}
|
||||
|
||||
/** 进入在线办理(携带 siteCode/itemCode,与 CMS 指南页按钮同一形态) */
|
||||
function apply(item: HallItem) {
|
||||
router.push({
|
||||
path: "/apply",
|
||||
query: { siteCode: siteCode.value, itemCode: item.itemCode, title: item.title },
|
||||
});
|
||||
/** 右上角状态角标(契约 §5 三态口径;「暂未开放」本期不做——列表只返回已发布事项) */
|
||||
function badgeOf(item: HallItem): { text: string; cls: string } {
|
||||
if (item.applyUrl) return { text: "外链办理", cls: "corner-link" };
|
||||
if (item.onlineFlag === 1) return { text: "可在线办理", cls: "corner-ok" };
|
||||
return { text: "仅线下办理", cls: "corner-offline" };
|
||||
}
|
||||
|
||||
/** 跑现场次数:0=零跑动(契约 §2.1),未配置则不显示 */
|
||||
function runOf(item: HallItem): string {
|
||||
if (item.runCount === null || item.runCount === undefined) return "";
|
||||
return item.runCount === 0 ? "零跑动" : `跑 ${item.runCount} 次`;
|
||||
}
|
||||
|
||||
const cards = computed(() => list.value.map(item => ({ ...item, badge: badgeOf(item) })));
|
||||
|
||||
/** 进事项指南(指南页与 /item 路由由并行任务 B 提供;卡片不再直连表单,避免绕过指南) */
|
||||
function openGuide(item: HallItem) {
|
||||
router.push({ path: "/item", query: { siteCode: siteCode.value, itemCode: item.itemCode } });
|
||||
}
|
||||
|
||||
/** 外链事项:新窗口打开办理入口(applyUrl 已由后端白名单校验) */
|
||||
function openApplyUrl(item: HallItem) {
|
||||
if (!item.applyUrl) return;
|
||||
window.open(item.applyUrl, "_blank", "noopener");
|
||||
}
|
||||
|
||||
/** 封面地址不可达时隐藏占位,避免裂图图标 */
|
||||
@@ -143,7 +163,8 @@ onMounted(async () => {
|
||||
<Spin v-if="loading" fix></Spin>
|
||||
|
||||
<div class="sh-hall-grid" v-if="list.length">
|
||||
<div class="item-card" v-for="item in list" :key="item.id">
|
||||
<div class="item-card" v-for="item in cards" :key="item.id" @click="openGuide(item)">
|
||||
<span class="corner" :class="item.badge.cls">{{ item.badge.text }}</span>
|
||||
<img
|
||||
v-if="item.coverImage"
|
||||
:src="item.coverImage"
|
||||
@@ -157,10 +178,11 @@ onMounted(async () => {
|
||||
<span class="sh-tag" v-if="item.categoryName">{{ item.categoryName }}</span>
|
||||
<span class="sh-tag" v-if="item.deptName">{{ item.deptName }}</span>
|
||||
</div>
|
||||
<div class="item-actions">
|
||||
<Button size="small" class="sh-btn-primary" type="primary" @click="apply(item)">
|
||||
在线办理
|
||||
</Button>
|
||||
<div class="item-foot">
|
||||
<span class="promise" v-if="item.promiseTimeLimit">承诺 {{ item.promiseTimeLimit }}</span>
|
||||
<span class="run" v-if="runOf(item)">· {{ runOf(item) }}</span>
|
||||
<span class="go" v-if="item.applyUrl" @click.stop="openApplyUrl(item)">前往办理 ↗</span>
|
||||
<span class="go" v-else>查看指南 ›</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -232,6 +254,8 @@ onMounted(async () => {
|
||||
}
|
||||
}
|
||||
.item-card {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
background: #fff;
|
||||
border: 1px solid var(--sh-border);
|
||||
border-radius: var(--sh-radius);
|
||||
@@ -271,8 +295,57 @@ onMounted(async () => {
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.item-actions {
|
||||
text-align: right;
|
||||
/* 状态角标:三态口径(外链 / 可在线 / 仅线下),配色全部由令牌混合 */
|
||||
.corner {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
z-index: 2;
|
||||
padding: 2px 8px;
|
||||
border-radius: 7px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
line-height: 16px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.corner-ok {
|
||||
color: var(--sh-success);
|
||||
background: color-mix(in srgb, var(--sh-success) 12%, #fff);
|
||||
}
|
||||
.corner-link {
|
||||
color: var(--sh-primary);
|
||||
background: color-mix(in srgb, var(--sh-primary) 12%, #fff);
|
||||
}
|
||||
.corner-offline {
|
||||
color: var(--sh-text-weak);
|
||||
background: var(--sh-page-bg);
|
||||
}
|
||||
/* 卡片底部:承诺时限 + 跑动次数(信息密度),右侧动作文案 */
|
||||
.item-foot {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 10px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px dashed var(--sh-border);
|
||||
font-size: 13px;
|
||||
}
|
||||
.item-foot .promise {
|
||||
color: var(--sh-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
.item-foot .run {
|
||||
color: var(--sh-text-weak);
|
||||
}
|
||||
.item-foot .go {
|
||||
margin-left: auto;
|
||||
color: var(--sh-primary);
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.item-foot .go:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
.empty {
|
||||
text-align: center;
|
||||
|
||||
@@ -7,17 +7,29 @@
|
||||
* - open-task 换址请求带代际号——关闭/换件时废弃在途响应并清 taskUrl/选中 ID;
|
||||
* - open-biz 只用与当前办件上下文一致的 bizId 构造 /apply-detail?id=,不照抄任意 URL。
|
||||
*/
|
||||
import { onBeforeUnmount, ref } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { computed, onBeforeUnmount, ref } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { Message } from "view-ui-plus";
|
||||
import FlowApprovalList from "@/components/flow-approval/FlowApprovalList.vue";
|
||||
import FlowRecordDrawer from "@/components/flow-approval/FlowRecordDrawer.vue";
|
||||
import FlowTaskEmbed from "@/components/flow-approval/FlowTaskEmbed.vue";
|
||||
import svcFlowApprovalApi from "@/api/module/approvalAdapter";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const listRef = ref<InstanceType<typeof FlowApprovalList> | null>(null);
|
||||
|
||||
/**
|
||||
* ApplyMy(我的办事)分区深链:?tab=todo|my|done|cc 决定默认页签。
|
||||
* FlowApprovalList 的初始页签取 tabs[0],故把请求的页签排到首位即可,不改组件本体。
|
||||
*/
|
||||
const TAB_ORDER = ["todo", "my", "done", "cc"] as const;
|
||||
const tabs = computed(() => {
|
||||
const want = String(route.query.tab || "");
|
||||
const head = TAB_ORDER.find(t => t === want) || "todo";
|
||||
return [head, ...TAB_ORDER.filter(t => t !== head)];
|
||||
});
|
||||
|
||||
// ===== 审批记录抽屉 =====
|
||||
const recordVisible = ref(false);
|
||||
const recordBizId = ref("");
|
||||
@@ -102,7 +114,7 @@ onBeforeUnmount(() => {
|
||||
<h2 class="page-title">我的审批</h2>
|
||||
<span class="page-sub">「待我审批/我已审批/抄送我的」为流程中心数据(按本应用过滤);「我提交的」为本地办件</span>
|
||||
</div>
|
||||
<FlowApprovalList ref="listRef" :api="svcFlowApprovalApi" :tabs="['todo', 'my', 'done', 'cc']"
|
||||
<FlowApprovalList ref="listRef" :api="svcFlowApprovalApi" :tabs="tabs"
|
||||
@open-record="openRecord" @open-task="openTask" @handled="onHandled"
|
||||
@open-detail="openDetail" />
|
||||
|
||||
|
||||
Reference in New Issue
Block a user