@@ -6,6 +6,8 @@
|
|||||||
VITE_BASE_HOST=http://base.yunhaixingtong.com
|
VITE_BASE_HOST=http://base.yunhaixingtong.com
|
||||||
VITE_SSO_HOST=http://sso.yunhaixingtong.com
|
VITE_SSO_HOST=http://sso.yunhaixingtong.com
|
||||||
VITE_SERVICE_HOST=
|
VITE_SERVICE_HOST=
|
||||||
|
# 流程中心本地直连(详情页撤回/催办直调;生产同主域自动推导 /flow-center)
|
||||||
|
VITE_FLOW_HOST=http://localhost:8085
|
||||||
VITE_ZIYUANKU_HOST=https://base.bestsch.com/BschZyk
|
VITE_ZIYUANKU_HOST=https://base.bestsch.com/BschZyk
|
||||||
VITE_ZHUANXIANG_HOST=https://base.bestsch.com/BschZncpd
|
VITE_ZHUANXIANG_HOST=https://base.bestsch.com/BschZncpd
|
||||||
VITE_IS_MOCK=false
|
VITE_IS_MOCK=false
|
||||||
|
|||||||
+2
-1
@@ -78,7 +78,8 @@ onMounted(async () => {
|
|||||||
<layout v-if="isLogin" :layoutData="layoutData" />
|
<layout v-if="isLogin" :layoutData="layoutData" />
|
||||||
<div v-if="isLogin" class="base-wrap-main" :class="{ 'alone-page': layoutData.isAlone }">
|
<div v-if="isLogin" class="base-wrap-main" :class="{ 'alone-page': layoutData.isAlone }">
|
||||||
<div class="main-wrapper">
|
<div class="main-wrapper">
|
||||||
<router-view />
|
<!-- key=fullPath:详情页仅 query 变化(id/serid)时强制重渲染,避免显示上一条数据 -->
|
||||||
|
<router-view :key="$route.fullPath" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ import type {
|
|||||||
NoticeRequest,
|
NoticeRequest,
|
||||||
NoticeQuery,
|
NoticeQuery,
|
||||||
DictOption,
|
DictOption,
|
||||||
|
FlowOption,
|
||||||
|
FlowTraceVo,
|
||||||
DeptManageItem,
|
DeptManageItem,
|
||||||
ContactGroupPage,
|
ContactGroupPage,
|
||||||
ContactUserPage,
|
ContactUserPage,
|
||||||
@@ -107,6 +109,20 @@ class BaseApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class NoticeApi {
|
class NoticeApi {
|
||||||
|
// ========== 先审后发(流程中心托管) ==========
|
||||||
|
/** 审核流程下拉选项(流程中心未接入时返回空数组,页面隐藏选择器) */
|
||||||
|
getFlowOptions(schId?: number) {
|
||||||
|
return get<FlowOption[]>(`${NOTICE}/flow/Options`, schId ? { schId } : undefined);
|
||||||
|
}
|
||||||
|
/** 提交/重新提交审核(幂等:bizId=notice.id;审批中会被拒绝) */
|
||||||
|
trySubmitFlow(serid: number) {
|
||||||
|
return postForm<void>(`${NOTICE}/flow/TrySubmit`, { serid });
|
||||||
|
}
|
||||||
|
/** 审批进度(详情页进度卡片;未绑流程返回 bound=false) */
|
||||||
|
flowTrace(serid: number) {
|
||||||
|
return get<FlowTraceVo>(`${NOTICE}/flow/Trace`, { serid });
|
||||||
|
}
|
||||||
|
|
||||||
// ========== 收件箱 ==========
|
// ========== 收件箱 ==========
|
||||||
/** 已收通知列表 */
|
/** 已收通知列表 */
|
||||||
list(params: NoticeQuery) {
|
list(params: NoticeQuery) {
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ let conf: BaseConf = {
|
|||||||
baseHost: origin,
|
baseHost: origin,
|
||||||
ssoHost: "",
|
ssoHost: "",
|
||||||
serviceHost: seg.length >= 2 ? `${origin}/${seg[0]}` : "",
|
serviceHost: seg.length >= 2 ? `${origin}/${seg[0]}` : "",
|
||||||
|
// 流程中心基址(Notice 详情页撤回/催办直调 /api/flow/*):生产同主域 Caddy 路由 /flow-center/;
|
||||||
|
// 本地 dev 由 VITE_FLOW_HOST 指向 http://localhost:8085(flow-center CORS 已收 8077)
|
||||||
|
flowHost: seg.length >= 2 ? `${origin}/flow-center` : "",
|
||||||
relayConf: {
|
relayConf: {
|
||||||
ziyuankuHost: "",
|
ziyuankuHost: "",
|
||||||
zhuanxiangHost: "",
|
zhuanxiangHost: "",
|
||||||
@@ -46,6 +49,7 @@ applyConf(conf, {
|
|||||||
baseHost: env.VITE_BASE_HOST,
|
baseHost: env.VITE_BASE_HOST,
|
||||||
ssoHost: env.VITE_SSO_HOST,
|
ssoHost: env.VITE_SSO_HOST,
|
||||||
serviceHost: env.VITE_IS_MOCK === "true" ? "" : env.VITE_SERVICE_HOST,
|
serviceHost: env.VITE_IS_MOCK === "true" ? "" : env.VITE_SERVICE_HOST,
|
||||||
|
flowHost: env.VITE_FLOW_HOST,
|
||||||
relayConf: {
|
relayConf: {
|
||||||
ziyuankuHost: env.VITE_ZIYUANKU_HOST,
|
ziyuankuHost: env.VITE_ZIYUANKU_HOST,
|
||||||
zhuanxiangHost: env.VITE_ZHUANXIANG_HOST,
|
zhuanxiangHost: env.VITE_ZHUANXIANG_HOST,
|
||||||
|
|||||||
@@ -68,6 +68,44 @@ export interface NoticeVo {
|
|||||||
createTime: string | null;
|
createTime: string | null;
|
||||||
updateBy: string | null;
|
updateBy: string | null;
|
||||||
updateTime: string | null;
|
updateTime: string | null;
|
||||||
|
/** 绑定的审核流程 GUID(null=直发未接流程中心) */
|
||||||
|
flowGuid?: string | null;
|
||||||
|
/** 审核状态:0 待提交 / 10 审批中 / 20 已通过 / 30 已驳回 / 40 已撤回(null=未绑流程) */
|
||||||
|
auditStatus?: number | null;
|
||||||
|
/** 审核状态文案 */
|
||||||
|
auditStatusDesc?: string | null;
|
||||||
|
/** 流程中心办件 id */
|
||||||
|
flowInstanceId?: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================ 先审后发(流程中心) ============================
|
||||||
|
|
||||||
|
/** 审核流程下拉选项(对齐 FlowModels.FlowSummaryVo) */
|
||||||
|
export interface FlowOption {
|
||||||
|
flowGuid: string;
|
||||||
|
name: string;
|
||||||
|
version: number;
|
||||||
|
nodeSummary: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 审批进度(GET /api/notice/flow/Trace) */
|
||||||
|
export interface FlowTraceVo {
|
||||||
|
bound: boolean;
|
||||||
|
flowGuid: string | null;
|
||||||
|
flowInstanceId: number | null;
|
||||||
|
auditStatus: number | null;
|
||||||
|
auditStatusDesc: string | null;
|
||||||
|
status: number | null;
|
||||||
|
flowResult?: { result?: string; opinion?: string; rejectNode?: string } | null;
|
||||||
|
instanceStatus?: number | null;
|
||||||
|
version?: number | null;
|
||||||
|
nodeSummary?: string | null;
|
||||||
|
currentNodes?: string | null;
|
||||||
|
assigneeIds?: number[] | null;
|
||||||
|
submitTime?: string | null;
|
||||||
|
finishTime?: string | null;
|
||||||
|
/** 办理轨迹(traceJson 解析后的数组) */
|
||||||
|
trace?: any[] | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================ 通知详情 ============================
|
// ============================ 通知详情 ============================
|
||||||
@@ -76,6 +114,11 @@ export interface NoticeVo {
|
|||||||
export interface NoticeDetailVo {
|
export interface NoticeDetailVo {
|
||||||
serid: number;
|
serid: number;
|
||||||
title: string;
|
title: string;
|
||||||
|
flowGuid?: string | null;
|
||||||
|
auditStatus?: number | null;
|
||||||
|
auditStatusDesc?: string | null;
|
||||||
|
flowInstanceId?: number | null;
|
||||||
|
flowResult?: { result?: string; opinion?: string; rejectNode?: string } | null;
|
||||||
/** 通知内容(旧字段名 Contents) */
|
/** 通知内容(旧字段名 Contents) */
|
||||||
Contents: string;
|
Contents: string;
|
||||||
/** 发布部门ID(旧字段名 DeptID) */
|
/** 发布部门ID(旧字段名 DeptID) */
|
||||||
@@ -194,6 +237,8 @@ export interface NoticeRequest {
|
|||||||
smsFlag?: number;
|
smsFlag?: number;
|
||||||
/** 转发源通知ID */
|
/** 转发源通知ID */
|
||||||
sourceId?: number;
|
sourceId?: number;
|
||||||
|
/** 审核流程 GUID(非空=提交审核;空=直发) */
|
||||||
|
flowGuid?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 通知列表查询参数(对齐 NoticeQuery.java + BasePageQuery.java) */
|
/** 通知列表查询参数(对齐 NoticeQuery.java + BasePageQuery.java) */
|
||||||
|
|||||||
@@ -2,11 +2,11 @@
|
|||||||
import { ref, reactive, shallowRef, computed, onMounted, onBeforeUnmount } from "vue";
|
import { ref, reactive, shallowRef, computed, onMounted, onBeforeUnmount } from "vue";
|
||||||
import { useRoute, useRouter } from "vue-router";
|
import { useRoute, useRouter } from "vue-router";
|
||||||
import { useRequest } from "alova/client";
|
import { useRequest } from "alova/client";
|
||||||
import { Message } from "view-ui-plus";
|
import { Message, Modal } from "view-ui-plus";
|
||||||
import { Editor, Toolbar } from "@wangeditor/editor-for-vue";
|
import { Editor, Toolbar } from "@wangeditor/editor-for-vue";
|
||||||
import "@wangeditor/editor/dist/css/style.css";
|
import "@wangeditor/editor/dist/css/style.css";
|
||||||
import { noticeApi } from "@/api/module/notice";
|
import { noticeApi } from "@/api/module/notice";
|
||||||
import type { NoticeDetailVo, NoticeRequest, UpFileVo } from "@/types/notice";
|
import type { NoticeDetailVo, NoticeRequest, UpFileVo, FlowOption } from "@/types/notice";
|
||||||
import UserCheckInput from "@/components/UserCheckInput.vue";
|
import UserCheckInput from "@/components/UserCheckInput.vue";
|
||||||
import NoticeUpload from "@/components/NoticeUpload.vue";
|
import NoticeUpload from "@/components/NoticeUpload.vue";
|
||||||
import config from "@/assets/config/host.config";
|
import config from "@/assets/config/host.config";
|
||||||
@@ -26,6 +26,13 @@ const form = reactive({
|
|||||||
content: "",
|
content: "",
|
||||||
isShortMsg: false,
|
isShortMsg: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/* ============ 先审后发(审核流程绑定) ============ */
|
||||||
|
const flowOptions = ref<FlowOption[]>([]);
|
||||||
|
const flowGuid = ref(""); // 空=直发(原链路)
|
||||||
|
const auditStatus = ref<number | null>(null); // 编辑回显的审核态
|
||||||
|
const auditResult = ref<{ result?: string; opinion?: string; rejectNode?: string } | null>(null);
|
||||||
|
const flowEnabled = computed(() => flowOptions.value.length > 0); // 流程中心不可用→隐藏选择器走直发
|
||||||
const fileids = ref<number[]>([]);
|
const fileids = ref<number[]>([]);
|
||||||
const editFileList = ref<{ name: string; fileid: number }[]>([]);
|
const editFileList = ref<{ name: string; fileid: number }[]>([]);
|
||||||
const isSms = ref(false);
|
const isSms = ref(false);
|
||||||
@@ -52,6 +59,20 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/* 审核流程下拉(流程中心不可用/未配置 → 空数组,选择器隐藏、提交按钮走"发送") */
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
flowOptions.value = (await noticeApi.getFlowOptions().send()) || [];
|
||||||
|
} catch {
|
||||||
|
flowOptions.value = [];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 流程图:flow-center 示意图深链(Notice 指定到人场景用人员视图) */
|
||||||
|
function openFlowDiagram() {
|
||||||
|
window.open(`${config.flowHost}/#/diagram?flowGuid=${flowGuid.value}&mode=user`, "_blank");
|
||||||
|
}
|
||||||
|
|
||||||
/* ============ 富文本编辑器(wangeditor v5 for vue3) ============ */
|
/* ============ 富文本编辑器(wangeditor v5 for vue3) ============ */
|
||||||
const editorRef = shallowRef();
|
const editorRef = shallowRef();
|
||||||
const editorLoading = ref(true);
|
const editorLoading = ref(true);
|
||||||
@@ -134,6 +155,10 @@ onMounted(async () => {
|
|||||||
form.dept = detail.DeptID || "";
|
form.dept = detail.DeptID || "";
|
||||||
form.rdoname = detail.tlist === "all" || !Array.isArray(detail.tlist) ? "all" : detail.tlist.join(",");
|
form.rdoname = detail.tlist === "all" || !Array.isArray(detail.tlist) ? "all" : detail.tlist.join(",");
|
||||||
if (detail.DsfsTime) timeForm.date1 = detail.DsfsTime;
|
if (detail.DsfsTime) timeForm.date1 = detail.DsfsTime;
|
||||||
|
// 审核绑定回显(驳回/撤回态显示意见与重提按钮)
|
||||||
|
flowGuid.value = detail.flowGuid || "";
|
||||||
|
auditStatus.value = detail.auditStatus ?? null;
|
||||||
|
auditResult.value = detail.flowResult || null;
|
||||||
} else {
|
} else {
|
||||||
form.title = detail.title || "";
|
form.title = detail.title || "";
|
||||||
}
|
}
|
||||||
@@ -177,6 +202,7 @@ async function doAdd(_type?: string) {
|
|||||||
deptId: form.dept,
|
deptId: form.dept,
|
||||||
fileIds: fileids.value.join(","), //附件
|
fileIds: fileids.value.join(","), //附件
|
||||||
smsFlag: form.isShortMsg ? 1 : 0, //是否发送短信
|
smsFlag: form.isShortMsg ? 1 : 0, //是否发送短信
|
||||||
|
flowGuid: flowGuid.value || undefined, //绑流程→提交审核;空→直发
|
||||||
};
|
};
|
||||||
if (isTime) {
|
if (isTime) {
|
||||||
//定时发送
|
//定时发送
|
||||||
@@ -211,14 +237,23 @@ async function doAdd(_type?: string) {
|
|||||||
try {
|
try {
|
||||||
if (!isEdit.value) {
|
if (!isEdit.value) {
|
||||||
await sendCreate(data);
|
await sendCreate(data);
|
||||||
Message.success("发送成功");
|
Message.success(flowGuid.value ? "已提交审核,审核通过后自动发布" : "发送成功");
|
||||||
router.push("/list");
|
router.push("/list");
|
||||||
} else {
|
} else {
|
||||||
data.sourceId = serid.value;
|
data.sourceId = serid.value;
|
||||||
if (data.title.indexOf("[修订稿]") == -1) data.title = "[修订稿]" + data.title;
|
if (data.title.indexOf("[修订稿]") == -1) data.title = "[修订稿]" + data.title;
|
||||||
await sendEdit(data);
|
// §3.6:已发布(audit=20)编辑告知——修改后重新走审批,期间对用户不可见
|
||||||
Message.success("保存成功");
|
if (flowGuid.value && auditStatus.value === 20) {
|
||||||
router.push("/list");
|
Modal.confirm({
|
||||||
|
title: "重新送审确认",
|
||||||
|
content: "该通知已发布,修改后将重新走审批流程,期间对用户不可见,确认继续?",
|
||||||
|
onOk: async () => {
|
||||||
|
await saveEditAndLeave(data);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await saveEditAndLeave(data);
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
Message.error(e.message || "保存失败");
|
Message.error(e.message || "保存失败");
|
||||||
@@ -226,6 +261,28 @@ async function doAdd(_type?: string) {
|
|||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 编辑保存 + 离开(§3.6 抽出:audit=20 先 confirm 再走这里) */
|
||||||
|
async function saveEditAndLeave(data: NoticeRequest) {
|
||||||
|
await sendEdit(data);
|
||||||
|
// 编辑驳回/撤回态:后端 edit 内已自动重提(复用同办件 R4);待提交态由重提按钮处理
|
||||||
|
Message.success(flowGuid.value ? "已保存并提交审核" : "保存成功");
|
||||||
|
router.push("/list");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 单独重提审核(待提交/驳回/撤回态;审批中按钮隐藏) */
|
||||||
|
async function doResubmit() {
|
||||||
|
isLoading.value = true;
|
||||||
|
try {
|
||||||
|
await noticeApi.trySubmitFlow(serid.value).send();
|
||||||
|
Message.success("已提交审核");
|
||||||
|
router.push("/list");
|
||||||
|
} catch (e: any) {
|
||||||
|
Message.error(e.message || "提交审核失败");
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -247,8 +304,26 @@ async function doAdd(_type?: string) {
|
|||||||
<FormItem label="添加附件:">
|
<FormItem label="添加附件:">
|
||||||
<NoticeUpload v-model:fileids="fileids" :defaultList="editFileList"></NoticeUpload>
|
<NoticeUpload v-model:fileids="fileids" :defaultList="editFileList"></NoticeUpload>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
<FormItem v-if="flowEnabled" label="审核流程:">
|
||||||
|
<div class="flow-select">
|
||||||
|
<Select v-model="flowGuid" clearable placeholder="不审核,直接发布">
|
||||||
|
<Option value="">不审核,直接发布</Option>
|
||||||
|
<Option v-for="f in flowOptions" :key="f.flowGuid" :value="f.flowGuid">
|
||||||
|
{{ f.name }}({{ f.nodeSummary || "—" }})
|
||||||
|
</Option>
|
||||||
|
</Select>
|
||||||
|
<Button v-if="flowGuid" size="small" @click="openFlowDiagram">流程图</Button>
|
||||||
|
</div>
|
||||||
|
</FormItem>
|
||||||
</Form>
|
</Form>
|
||||||
|
|
||||||
|
<!-- 驳回/撤回意见提示(编辑驳回/撤回态) -->
|
||||||
|
<Alert v-if="isEdit && (auditStatus === 30 || auditStatus === 40)" type="warning" show-icon class="audit-alert">
|
||||||
|
审核{{ auditStatus === 30 ? "已驳回" : "已撤回" }}
|
||||||
|
<template v-if="auditResult?.opinion"> 办理意见:{{ auditResult.opinion }}<template v-if="auditResult?.rejectNode">({{ auditResult.rejectNode }})</template></template>
|
||||||
|
修改保存后将自动重新提交审核
|
||||||
|
</Alert>
|
||||||
|
|
||||||
<!-- 富文本编辑器 -->
|
<!-- 富文本编辑器 -->
|
||||||
<div class="editor-wrap">
|
<div class="editor-wrap">
|
||||||
<Toolbar class="editor-toolbar" :editor="editorRef" :defaultConfig="toolbarConfig" mode="default" />
|
<Toolbar class="editor-toolbar" :editor="editorRef" :defaultConfig="toolbarConfig" mode="default" />
|
||||||
@@ -262,7 +337,17 @@ async function doAdd(_type?: string) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bottom-btn">
|
<div class="bottom-btn">
|
||||||
<Button type="primary" :loading="isLoading" @click="doAdd()">{{ isEdit ? "保存" : "发送" }}</Button>
|
<Button type="primary" :loading="isLoading" @click="doAdd()">
|
||||||
|
{{ isEdit ? (flowGuid ? "保存并重新提交审核" : "保存") : flowGuid ? "提交审核" : "发送" }}
|
||||||
|
</Button>
|
||||||
|
<!-- 待提交态单独重提(提交流程中心失败后的幂等重推入口) -->
|
||||||
|
<Button
|
||||||
|
v-if="isEdit && flowGuid && auditStatus === 0"
|
||||||
|
style="margin-left: 10px"
|
||||||
|
:loading="isLoading"
|
||||||
|
@click="doResubmit"
|
||||||
|
>重新提交审核</Button
|
||||||
|
>
|
||||||
<Button v-if="!isEdit" style="margin-left: 10px" @click="isModal.timiSend = true">定时发送</Button>
|
<Button v-if="!isEdit" style="margin-left: 10px" @click="isModal.timiSend = true">定时发送</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -302,6 +387,14 @@ async function doAdd(_type?: string) {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
.audit-alert {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.flow-select {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
/* 页面超高时的滚动由 App.vue 的 .main-wrapper 统一承接 */
|
/* 页面超高时的滚动由 App.vue 的 .main-wrapper 统一承接 */
|
||||||
.myform {
|
.myform {
|
||||||
:deep(.ivu-form-item) {
|
:deep(.ivu-form-item) {
|
||||||
|
|||||||
@@ -3,9 +3,10 @@ import { ref, computed, onMounted } from "vue";
|
|||||||
import { useRoute, useRouter } from "vue-router";
|
import { useRoute, useRouter } from "vue-router";
|
||||||
import { Message } from "view-ui-plus";
|
import { Message } from "view-ui-plus";
|
||||||
import { noticeApi } from "@/api/module/notice";
|
import { noticeApi } from "@/api/module/notice";
|
||||||
import type { NoticeDetailVo, NextPrevVo, ReadGroup } from "@/types/notice";
|
import type { NoticeDetailVo, NextPrevVo, ReadGroup, FlowTraceVo } from "@/types/notice";
|
||||||
import FilePreviewModal from "@/components/FilePreviewModal.vue";
|
import FilePreviewModal from "@/components/FilePreviewModal.vue";
|
||||||
import Utils from "@/utils";
|
import Utils from "@/utils";
|
||||||
|
import config from "@/assets/config/host.config";
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -21,6 +22,50 @@ const navType = computed(() => (viewType.value === 0 ? "0" : "1"));
|
|||||||
|
|
||||||
const detail = ref<NoticeDetailVo | null>(null);
|
const detail = ref<NoticeDetailVo | null>(null);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
|
||||||
|
/* ============ 先审后发:审批进度卡片 + 撤回/催办 ============ */
|
||||||
|
const flowTrace = ref<FlowTraceVo | null>(null);
|
||||||
|
const flowActing = ref(false);
|
||||||
|
|
||||||
|
async function loadTrace() {
|
||||||
|
if (!detail.value?.flowGuid) return;
|
||||||
|
try {
|
||||||
|
flowTrace.value = await noticeApi.flowTrace(serid.value).send();
|
||||||
|
} catch {
|
||||||
|
flowTrace.value = null; // 进度不可用时只展示本地镜像
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 撤回/催办直调 flow-center 用户面(同主域 CAS 免二次登录;本地 dev 走 flowHost CORS) */
|
||||||
|
async function flowAction(action: "withdraw" | "urge") {
|
||||||
|
const instanceId = detail.value?.flowInstanceId;
|
||||||
|
if (!instanceId || !config.flowHost) return;
|
||||||
|
flowActing.value = true;
|
||||||
|
try {
|
||||||
|
const resp = await fetch(`${config.flowHost}/api/flow/instance/${instanceId}/${action}`, {
|
||||||
|
method: "POST",
|
||||||
|
credentials: "include",
|
||||||
|
});
|
||||||
|
const res = await resp.json().catch(() => null);
|
||||||
|
if (resp.ok && res && res.code === 0) {
|
||||||
|
Message.success(action === "withdraw" ? "已撤回审核,可编辑后重新提交" : "已发送催办提醒");
|
||||||
|
load(); // 撤回后状态刷新
|
||||||
|
loadTrace();
|
||||||
|
} else {
|
||||||
|
Message.error(res?.msg || (action === "withdraw" ? "撤回失败(已有人办理时不可撤回)" : "催办失败"));
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
Message.error("流程中心不可达:" + (e.message || e));
|
||||||
|
} finally {
|
||||||
|
flowActing.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 查看流程图:flow-center 示意图深链(绑流程时;Notice 指定到人场景用人员视图) */
|
||||||
|
function openFlowDiagram() {
|
||||||
|
if (!detail.value?.flowGuid || !config.flowHost) return;
|
||||||
|
window.open(`${config.flowHost}/#/diagram?flowGuid=${detail.value.flowGuid}&mode=user`, "_blank");
|
||||||
|
}
|
||||||
const readGroups = ref<ReadGroup[]>([]);
|
const readGroups = ref<ReadGroup[]>([]);
|
||||||
const showRead = ref(false);
|
const showRead = ref(false);
|
||||||
const prevId = ref(0);
|
const prevId = ref(0);
|
||||||
@@ -28,6 +73,7 @@ const nextId = ref(0);
|
|||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await load();
|
await load();
|
||||||
|
loadTrace(); // 详情加载后并行取审批进度(未绑流程接口自身返回 bound=false)
|
||||||
});
|
});
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
@@ -38,6 +84,7 @@ async function load() {
|
|||||||
const nav: NextPrevVo = await noticeApi.getNextPrevNotice(serid.value, navType.value).send();
|
const nav: NextPrevVo = await noticeApi.getNextPrevNotice(serid.value, navType.value).send();
|
||||||
prevId.value = nav.prevId || 0;
|
prevId.value = nav.prevId || 0;
|
||||||
nextId.value = nav.nextId || 0;
|
nextId.value = nav.nextId || 0;
|
||||||
|
loadTrace();
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
Message.error(e.message || "加载通知失败");
|
Message.error(e.message || "加载通知失败");
|
||||||
} finally {
|
} finally {
|
||||||
@@ -97,6 +144,15 @@ function onPrint() {
|
|||||||
win.print();
|
win.print();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 审核状态 Tag 颜色 */
|
||||||
|
function auditColor(auditStatus?: number | null) {
|
||||||
|
if (auditStatus === 20) return "green";
|
||||||
|
if (auditStatus === 30) return "red";
|
||||||
|
if (auditStatus === 40) return "orange";
|
||||||
|
if (auditStatus === 0) return "default";
|
||||||
|
return "blue"; // 审批中
|
||||||
|
}
|
||||||
|
|
||||||
function downFile(id: number) {
|
function downFile(id: number) {
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
window.location.href = noticeApi.fileDownUrl(id);
|
window.location.href = noticeApi.fileDownUrl(id);
|
||||||
@@ -176,6 +232,49 @@ function openPreview(item: { name?: string; filename?: string; fileid?: number;
|
|||||||
<!-- 查看视角:普通标题 -->
|
<!-- 查看视角:普通标题 -->
|
||||||
<h2 v-else class="notice-title2">{{ detail.title }}</h2>
|
<h2 v-else class="notice-title2">{{ detail.title }}</h2>
|
||||||
|
|
||||||
|
<!-- 审批进度卡片(绑流程通知;提交人/管理员/办理人可见) -->
|
||||||
|
<div v-if="flowTrace && flowTrace.bound" class="flow-card">
|
||||||
|
<div class="flow-card-head">
|
||||||
|
<span class="flow-title">审批进度</span>
|
||||||
|
<Tag :color="auditColor(flowTrace.auditStatus)">{{ flowTrace.auditStatusDesc || "—" }}</Tag>
|
||||||
|
<span v-if="flowTrace.nodeSummary" class="flow-nodes">流程:{{ flowTrace.nodeSummary }}</span>
|
||||||
|
<span v-if="flowTrace.currentNodes" class="flow-nodes">当前节点:{{ flowTrace.currentNodes }}</span>
|
||||||
|
<div class="flow-ops">
|
||||||
|
<Button v-if="detail.flowGuid" size="small" @click="openFlowDiagram">查看流程图</Button>
|
||||||
|
<Button
|
||||||
|
v-if="detail.flowInstanceId && flowTrace.auditStatus === 10"
|
||||||
|
size="small"
|
||||||
|
:loading="flowActing"
|
||||||
|
style="margin-left: 8px"
|
||||||
|
@click="flowAction('withdraw')"
|
||||||
|
>撤回审核</Button
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
v-if="detail.flowInstanceId && flowTrace.auditStatus === 10"
|
||||||
|
size="small"
|
||||||
|
:loading="flowActing"
|
||||||
|
style="margin-left: 8px"
|
||||||
|
@click="flowAction('urge')"
|
||||||
|
>催办</Button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 驳回/撤回意见 -->
|
||||||
|
<div v-if="flowTrace.flowResult && flowTrace.flowResult.opinion" class="flow-opinion">
|
||||||
|
办理意见:{{ flowTrace.flowResult.opinion }}
|
||||||
|
<template v-if="flowTrace.flowResult.rejectNode">({{ flowTrace.flowResult.rejectNode }})</template>
|
||||||
|
</div>
|
||||||
|
<!-- 节点时间线(traceJson 解析) -->
|
||||||
|
<div v-if="flowTrace.trace && flowTrace.trace.length" class="flow-timeline">
|
||||||
|
<div v-for="(t, i) in flowTrace.trace" :key="i" class="flow-step">
|
||||||
|
<span class="flow-step-node">{{ t.nodeName || t.node || t.taskName || `节点${i + 1}` }}</span>
|
||||||
|
<span class="flow-step-user">{{ t.userName || t.assigneeName || t.handlerName || "" }}</span>
|
||||||
|
<span class="flow-step-time">{{ t.handleTime || t.time || t.createTime || "" }}</span>
|
||||||
|
<span class="flow-step-op" v-if="t.opinion">{{ t.opinion }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-if="viewType !== 0" class="meta">
|
<div v-if="viewType !== 0" class="meta">
|
||||||
<span>发文单位:{{ detail.DeptName || "—" }}</span>
|
<span>发文单位:{{ detail.DeptName || "—" }}</span>
|
||||||
<span>发布人:{{ detail.TeaName || "—" }}</span>
|
<span>发布人:{{ detail.TeaName || "—" }}</span>
|
||||||
@@ -196,9 +295,12 @@ function openPreview(item: { name?: string; filename?: string; fileid?: number;
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 管理视角底部操作(同旧版:编辑 / 打印) -->
|
<!-- 管理视角底部操作(同旧版:编辑 / 打印;审批中隐藏编辑——后端 403 死按钮,BP-R5-1) -->
|
||||||
<div v-if="viewType === 0" class="bottom-btns">
|
<div v-if="viewType === 0" class="bottom-btns">
|
||||||
<Button @click="onEdit">编辑</Button>
|
<Button v-if="detail.auditStatus !== 10" @click="onEdit">编辑</Button>
|
||||||
|
<Tooltip v-else content="审批中,请先撤回再编辑" placement="top" transfer>
|
||||||
|
<Button disabled>编辑</Button>
|
||||||
|
</Tooltip>
|
||||||
<Button style="margin-left: 10px" @click="onPrint">打印</Button>
|
<Button style="margin-left: 10px" @click="onPrint">打印</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -246,6 +348,68 @@ function openPreview(item: { name?: string; filename?: string; fileid?: number;
|
|||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
margin: 0 0 12px;
|
margin: 0 0 12px;
|
||||||
}
|
}
|
||||||
|
.flow-card {
|
||||||
|
border: 1px solid #e8eaec;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
background: #f8f8f9;
|
||||||
|
}
|
||||||
|
.flow-card-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.flow-title {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #17233d;
|
||||||
|
}
|
||||||
|
.flow-nodes {
|
||||||
|
color: #808695;
|
||||||
|
}
|
||||||
|
.flow-ops {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
.flow-opinion {
|
||||||
|
margin-top: 8px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #ed4014;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.flow-timeline {
|
||||||
|
margin-top: 8px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
}
|
||||||
|
.flow-step {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #515a6e;
|
||||||
|
padding: 3px 0;
|
||||||
|
border-bottom: 1px dashed #f0f0f0;
|
||||||
|
}
|
||||||
|
.flow-step:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
.flow-step-node {
|
||||||
|
min-width: 120px;
|
||||||
|
}
|
||||||
|
.flow-step-user {
|
||||||
|
min-width: 90px;
|
||||||
|
color: #2d8cf0;
|
||||||
|
}
|
||||||
|
.flow-step-time {
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
.flow-step-op {
|
||||||
|
color: #ed4014;
|
||||||
|
}
|
||||||
.meta {
|
.meta {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|||||||
@@ -151,6 +151,17 @@ const columns = [
|
|||||||
align: "left",
|
align: "left",
|
||||||
render: (h: any, p: any) => h("span", {}, String(p.row.createTime || "").slice(0, 16)),
|
render: (h: any, p: any) => h("span", {}, String(p.row.createTime || "").slice(0, 16)),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "审核状态",
|
||||||
|
width: 100,
|
||||||
|
align: "center",
|
||||||
|
render: (h: any, p: any) => {
|
||||||
|
const st = p.row.auditStatus;
|
||||||
|
if (st === null || st === undefined) return h("span", { style: "color:#999;" }, "—");
|
||||||
|
const colors: Record<number, string> = { 0: "default", 10: "blue", 20: "green", 30: "red", 40: "orange" };
|
||||||
|
return h("Tag", { color: colors[st] || "default" }, () => p.row.auditStatusDesc || "—");
|
||||||
|
},
|
||||||
|
},
|
||||||
{ title: "已读人数", key: "readCount", width: 110, align: "left" },
|
{ title: "已读人数", key: "readCount", width: 110, align: "left" },
|
||||||
{
|
{
|
||||||
title: "未读人数",
|
title: "未读人数",
|
||||||
@@ -172,10 +183,14 @@ const columns = [
|
|||||||
width: 180,
|
width: 180,
|
||||||
align: "center",
|
align: "center",
|
||||||
fixed: "right",
|
fixed: "right",
|
||||||
render: (h: any, p: any) => [
|
render: (h: any, p: any) => {
|
||||||
Utils.operatingHandle(h, "查看通知", "icon-browse", () => openDetail(p.row)),
|
// BP-R5-1:审批中(audit=10)禁止删除(后端 403)——不再渲染死按钮,审批通过/驳回/撤回后恢复
|
||||||
Utils.operatingHandle(h, "删除通知", "icon-ashbin", () => del(p.row)),
|
const ops = [Utils.operatingHandle(h, "查看通知", "icon-browse", () => openDetail(p.row))];
|
||||||
],
|
if (p.row.auditStatus !== 10) {
|
||||||
|
ops.push(Utils.operatingHandle(h, "删除通知", "icon-ashbin", () => del(p.row)));
|
||||||
|
}
|
||||||
|
return ops;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user