From 830f9088f43deab55ce604070e5536b377c55df1 Mon Sep 17 00:00:00 2001 From: chenxing Date: Wed, 16 Sep 2026 01:21:46 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=80=9A=E7=9F=A5=E5=89=8D=E7=AB=AF?= =?UTF-8?q?=E5=AD=98=E9=87=8F=EF=BC=88=E5=AE=A1=E6=89=B9=E7=8A=B6=E6=80=81?= =?UTF-8?q?/=E6=B6=88=E6=81=AF=E4=B8=AD=E5=BF=83/=E8=AF=A6=E6=83=85?= =?UTF-8?q?=E6=B5=81=E7=A8=8B=E5=85=A5=E5=8F=A3=E7=AD=89=202026-09-13/14?= =?UTF-8?q?=20=E6=94=B9=E5=8A=A8=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/module/notice.ts | 9 ++ src/assets/mock/noticeapi.ts | 11 ++- src/components/layout.vue | 24 +---- src/types/notice.ts | 20 +++- src/views/notice/NewNotice.vue | 95 +++++++++++-------- src/views/notice/NoticeList.vue | 22 +++++ src/views/notice/NoticeSetting.vue | 145 ++++++++++++++++++++++++++++- 7 files changed, 261 insertions(+), 65 deletions(-) diff --git a/src/api/module/notice.ts b/src/api/module/notice.ts index aa57c1f..0874f5a 100644 --- a/src/api/module/notice.ts +++ b/src/api/module/notice.ts @@ -17,6 +17,7 @@ import type { DictOption, FlowOption, FlowTraceVo, + FlowBindingVo, DeptManageItem, ContactGroupPage, ContactUserPage, @@ -122,6 +123,14 @@ class NoticeApi { flowTrace(serid: number) { return get(`${NOTICE}/flow/Trace`, { serid }); } + /** 应用级流程绑定(应用设置 > 流程绑定;发通知页据此判断「提交审核」还是「发送」) */ + getFlowBinding() { + return get(`${NOTICE}/flow/Binding`); + } + /** 保存/解除绑定(flowGuid 空=解绑;仅本校管理员) */ + saveFlowBinding(flowGuid: string) { + return postForm(`${NOTICE}/flow/Binding/Save`, { flowGuid }); + } // ========== 收件箱 ========== /** 已收通知列表 */ diff --git a/src/assets/mock/noticeapi.ts b/src/assets/mock/noticeapi.ts index 243fa87..ab8adba 100644 --- a/src/assets/mock/noticeapi.ts +++ b/src/assets/mock/noticeapi.ts @@ -271,11 +271,12 @@ export default [ method: "get", response: () => ok([ - { id: 1, modId: 1, name: "已收到", parentId: 0, page: "/", orderNumber: 1, usable: true, children: [] }, - { id: 2, modId: 2, name: "已发送", parentId: 0, page: "/list", orderNumber: 2, usable: true, children: [] }, - { id: 3, modId: 3, name: "全部通知", parentId: 0, page: "/noticelist", orderNumber: 3, usable: true, children: [] }, - { id: 4, modId: 4, name: "通知统计", parentId: 0, page: "/stats", orderNumber: 4, usable: true, children: [] }, - { id: 21, modId: 21, name: "应用设置", parentId: 0, page: "/setting", orderNumber: 5, usable: true, children: [] }, + // icon:用户中心模块管理配置的 YschIcons 图标名,layout 渲染为 iconfont icon-<名称> + { id: 1, modId: 1, name: "已收到", parentId: 0, page: "/", icon: "sjx", orderNumber: 1, usable: true, children: [] }, + { id: 2, modId: 2, name: "已发送", parentId: 0, page: "/list", icon: "fasong", orderNumber: 2, usable: true, children: [] }, + { id: 3, modId: 3, name: "全部通知", parentId: 0, page: "/noticelist", icon: "liebiao", orderNumber: 3, usable: true, children: [] }, + { id: 4, modId: 4, name: "通知统计", parentId: 0, page: "/stats", icon: "tongji", orderNumber: 4, usable: true, children: [] }, + { id: 21, modId: 21, name: "应用设置", parentId: 0, page: "/setting", icon: "shezhi", orderNumber: 5, usable: true, children: [] }, ]), }, { diff --git a/src/components/layout.vue b/src/components/layout.vue index ca359b2..c0abef5 100644 --- a/src/components/layout.vue +++ b/src/components/layout.vue @@ -128,26 +128,12 @@ const childChange = (index: number) => { } }; -/** 根据菜单项映射图标:优先按解析后的 page 判断(平台模块 id 不可依赖), - * 兜底保留旧版 modId 对照。图标为本地 bsch-icons(iconfont 掩码类,见 styles/icons.css) */ +/** 菜单图标:读取用户中心模块管理配置的 YschIcons 图标名(GetUserMod 的 icon 字段,如 list → icon-list)。 + * 本地 icons.css 即 ysch-icons 掩码版(基类 iconfont,类名 icon-<名称>); + * 平台约定留空不下发图标,此时返回空串由模板 v-if 不渲染 */ const getMenuIcon = (item?: any): string => { - const page: string = item?.page || ""; - if (page === "/list/new") return "icon-xiugaijl"; - if (page === "/list") return "icon-fasong"; - if (page === "/" || page === "/accept") return "icon-sjx"; - if (page === "/noticelist") return "icon-liebiao"; - if (page === "/stats") return "icon-tongji"; - if (page === "/setting") return "icon-shezhi"; - switch (item?.modId ?? item?.id) { - case 1: return "icon-xiugaijl"; - case 2: return "icon-fasong"; - case 3: return "icon-sjx"; - case 4: return "icon-liebiao"; - case 5: return "icon-wenzhang"; - case 21: return "icon-shezhi"; - case 22: return "icon-xiugaijl"; - default: return ""; - } + const icon = String(item?.icon || "").trim(); + return icon ? `icon-${icon}` : ""; }; function linkHomePage() { diff --git a/src/types/notice.ts b/src/types/notice.ts index 508192d..4b48f40 100644 --- a/src/types/notice.ts +++ b/src/types/notice.ts @@ -88,6 +88,18 @@ export interface FlowOption { nodeSummary: string | null; } +/** 应用级流程绑定视图(GET /api/notice/flow/Binding;应用设置 > 流程绑定 + 发通知页按钮显隐) */ +export interface FlowBindingVo { + /** 本校是否已绑定审核流程(false=全部直发,现有功能不变) */ + bound: boolean; + flowGuid: string | null; + flowName: string | null; + /** 流程中心是否接入(false 时设置页隐藏绑定入口) */ + flowEnabled: boolean; + /** 当前用户是否可维护绑定(本校管理员;false 时设置页只读展示) */ + canEdit?: boolean; +} + /** 审批进度(GET /api/notice/flow/Trace) */ export interface FlowTraceVo { bound: boolean; @@ -237,8 +249,7 @@ export interface NoticeRequest { smsFlag?: number; /** 转发源通知ID */ sourceId?: number; - /** 审核流程 GUID(非空=提交审核;空=直发) */ - flowGuid?: string; + // 审核流程不由前端指定:绑定在「应用设置 > 流程绑定」,后端按「已绑定 + 通知对象=全部用户」判定 } /** 通知列表查询参数(对齐 NoticeQuery.java + BasePageQuery.java) */ @@ -247,6 +258,11 @@ export interface NoticeQuery { pageSize?: number; keyword?: string; status?: number; + /** + * 审核状态筛选(列表「状态」下拉):0 待提交 / 10 审批中 / 20 已通过 / 30 已驳回 / 40 已撤回; + * -1 = 无需审核(直发通知);不传不过滤 + */ + auditStatus?: number; type?: number; /** 列表范围(管理端):"user" 只看自己发布的,不传=全部 */ scope?: string; diff --git a/src/views/notice/NewNotice.vue b/src/views/notice/NewNotice.vue index 14a04ec..34084bf 100644 --- a/src/views/notice/NewNotice.vue +++ b/src/views/notice/NewNotice.vue @@ -6,7 +6,7 @@ import { Message, Modal } from "view-ui-plus"; import { Editor, Toolbar } from "@wangeditor/editor-for-vue"; import "@wangeditor/editor/dist/css/style.css"; import { noticeApi } from "@/api/module/notice"; -import type { NoticeDetailVo, NoticeRequest, UpFileVo, FlowOption } from "@/types/notice"; +import type { NoticeDetailVo, NoticeRequest, UpFileVo, FlowBindingVo } from "@/types/notice"; import UserCheckInput from "@/components/UserCheckInput.vue"; import NoticeUpload from "@/components/NoticeUpload.vue"; import config from "@/assets/config/host.config"; @@ -27,12 +27,28 @@ const form = reactive({ isShortMsg: false, }); -/* ============ 先审后发(审核流程绑定) ============ */ -const flowOptions = ref([]); -const flowGuid = ref(""); // 空=直发(原链路) +/* ============ 先审后发(应用设置 > 流程绑定,按学校生效) ============ + 规则(后端为单一事实源):已绑定 + 通知对象=全部用户 → 必须走审核,只显示「提交审核」, + 隐藏「发送」与「定时发送」;指定人员与未绑定 → 原直发链路不变。 */ +const flowBinding = ref({ bound: false, flowGuid: null, flowName: null, flowEnabled: true }); +const noticeFlowGuid = ref(""); // 编辑回显:该通知自身的流程快照(决定沿用哪个流程重审) const auditStatus = ref(null); // 编辑回显的审核态 const auditResult = ref<{ result?: string; opinion?: string; rejectNode?: string } | null>(null); -const flowEnabled = computed(() => flowOptions.value.length > 0); // 流程中心不可用→隐藏选择器走直发 + +/** 通知对象是否「全部用户」(UserCheckInput 全部态 v-model 值为 "all") */ +const isAllTarget = computed(() => { + const v = (form.rdoname || "").trim().toLowerCase(); + return v === "" || v === "all"; +}); +/** 是否走审核(与后端 publish/edit 判定同式:已绑定 && 全部用户) */ +const needAudit = computed(() => flowBinding.value.bound && isAllTarget.value); +/** 提示条展示的流程:优先该通知自身快照,其次应用绑定 */ +const auditFlowGuid = computed(() => noticeFlowGuid.value || flowBinding.value.flowGuid || ""); +const auditFlowName = computed(() => { + if (!noticeFlowGuid.value) return flowBinding.value.flowName || ""; + return noticeFlowGuid.value === flowBinding.value.flowGuid ? flowBinding.value.flowName || "" : ""; +}); + const fileids = ref([]); const editFileList = ref<{ name: string; fileid: number }[]>([]); const isSms = ref(false); @@ -59,18 +75,21 @@ onMounted(async () => { } }); -/* 审核流程下拉(流程中心不可用/未配置 → 空数组,选择器隐藏、提交按钮走"发送") */ +/* 流程绑定(决定按钮显隐;流程中心未接入时 bound 恒为 false → 原直发界面) */ onMounted(async () => { try { - flowOptions.value = (await noticeApi.getFlowOptions().send()) || []; + const view = await noticeApi.getFlowBinding().send(); + if (view) flowBinding.value = view; } catch { - flowOptions.value = []; + flowBinding.value = { bound: false, flowGuid: null, flowName: null, flowEnabled: false }; } }); /** 流程图:flow-center 示意图深链(Notice 指定到人场景用人员视图) */ function openFlowDiagram() { - window.open(`${config.flowHost}/#/diagram?flowGuid=${flowGuid.value}&mode=user`, "_blank"); + if (auditFlowGuid.value) { + window.open(`${config.flowHost}/#/diagram?flowGuid=${auditFlowGuid.value}&mode=user`, "_blank"); + } } /* ============ 富文本编辑器(wangeditor v5 for vue3) ============ */ @@ -156,7 +175,7 @@ onMounted(async () => { form.rdoname = detail.tlist === "all" || !Array.isArray(detail.tlist) ? "all" : detail.tlist.join(","); if (detail.DsfsTime) timeForm.date1 = detail.DsfsTime; // 审核绑定回显(驳回/撤回态显示意见与重提按钮) - flowGuid.value = detail.flowGuid || ""; + noticeFlowGuid.value = detail.flowGuid || ""; auditStatus.value = detail.auditStatus ?? null; auditResult.value = detail.flowResult || null; } else { @@ -202,7 +221,7 @@ async function doAdd(_type?: string) { deptId: form.dept, fileIds: fileids.value.join(","), //附件 smsFlag: form.isShortMsg ? 1 : 0, //是否发送短信 - flowGuid: flowGuid.value || undefined, //绑流程→提交审核;空→直发 + // 审核流程不再由前端指定:后端按「应用设置绑定 + 通知对象=全部用户」判定 }; if (isTime) { //定时发送 @@ -237,13 +256,13 @@ async function doAdd(_type?: string) { try { if (!isEdit.value) { await sendCreate(data); - Message.success(flowGuid.value ? "已提交审核,审核通过后自动发布" : "发送成功"); + Message.success(needAudit.value ? "已提交审核,审核通过后自动发布" : "发送成功"); router.push("/list"); } else { data.sourceId = serid.value; if (data.title.indexOf("[修订稿]") == -1) data.title = "[修订稿]" + data.title; - // §3.6:已发布(audit=20)编辑告知——修改后重新走审批,期间对用户不可见 - if (flowGuid.value && auditStatus.value === 20) { + // §3.6:审核轨道内已发布(audit=20)编辑告知——修改后重新走审批,期间对用户不可见 + if (needAudit.value && auditStatus.value === 20) { Modal.confirm({ title: "重新送审确认", content: "该通知已发布,修改后将重新走审批流程,期间对用户不可见,确认继续?", @@ -253,6 +272,17 @@ async function doAdd(_type?: string) { }); return; } + // 绑定生效后把存量通知(此前直发、无审核轨道)改为全部用户:保存即将转入待审核 + if (needAudit.value && !noticeFlowGuid.value) { + Modal.confirm({ + title: "转为待审核确认", + content: "本应用已绑定审核流程,发送全部用户需审核通过后发布。保存后该通知将进入待审核状态,审核期间对用户不可见,确认继续?", + onOk: async () => { + await saveEditAndLeave(data); + }, + }); + return; + } await saveEditAndLeave(data); } } catch (e: any) { @@ -262,16 +292,15 @@ async function doAdd(_type?: string) { } } -/* 编辑保存 + 离开(§3.6 抽出:audit=20 先 confirm 再走这里) */ +/* 编辑保存 + 离开(§3.6 抽出:确认分支复用) */ async function saveEditAndLeave(data: NoticeRequest) { await sendEdit(data); // 编辑驳回/撤回态:后端 edit 内已自动重提(复用同办件 R4);待提交态由重提按钮处理 - Message.success(flowGuid.value ? "已保存并提交审核" : "保存成功"); + Message.success(needAudit.value ? "已保存并提交审核" : "保存成功"); router.push("/list"); } -/* 单独重提审核(待提交/驳回/撤回态;审批中按钮隐藏) */ -async function doResubmit() { +/* 单独重提审核(待提交/驳回/撤回态;审批中按钮隐藏) */async function doResubmit() { isLoading.value = true; try { await noticeApi.trySubmitFlow(serid.value).send(); @@ -304,19 +333,15 @@ async function doResubmit() { - -
- - -
-
+ + + 本应用已绑定审核流程{{ auditFlowName ? `《${auditFlowName}》` : "" }}:发送全部用户需审核通过后发布 + + + + 审核{{ auditStatus === 30 ? "已驳回" : "已撤回" }} @@ -338,17 +363,18 @@ async function doResubmit() {
- + +
@@ -390,11 +416,6 @@ async function doResubmit() { .audit-alert { margin-bottom: 12px; } -.flow-select { - display: flex; - gap: 8px; - align-items: center; -} /* 页面超高时的滚动由 App.vue 的 .main-wrapper 统一承接 */ .myform { :deep(.ivu-form-item) { diff --git a/src/views/notice/NoticeList.vue b/src/views/notice/NoticeList.vue index 9a5197e..f7bbbf7 100644 --- a/src/views/notice/NoticeList.vue +++ b/src/views/notice/NoticeList.vue @@ -17,10 +17,21 @@ const isManage = computed(() => route.path === "/noticelist"); /* ============ 搜索条件(规范 §1:日期 fl + 下拉 fl + 关键字 fr 宽200 即搜) ============ */ const keyword = ref(""); const deptType = ref(""); +const auditType = ref(""); // 状态筛选(audit_status;-1=无需审核) const startDt = ref(""); const endDt = ref(""); const { data: deptOptions } = useRequest(noticeApi.getSysDic()); +/** 状态筛选选项(与列表「审核状态」列取值一一对应;-1 = 未走审核的直发通知) */ +const auditStatusOptions = [ + { value: 0, label: "待提交" }, + { value: 10, label: "审批中" }, + { value: 20, label: "已通过" }, + { value: 30, label: "已驳回" }, + { value: 40, label: "已撤回" }, + { value: -1, label: "无需审核" }, +]; + function fmtDate(d: Date) { const pad = (n: number) => String(n).padStart(2, "0"); return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`; @@ -45,6 +56,7 @@ async function run() { }; if (startDt.value) params.startTime = startDt.value; if (endDt.value) params.endTime = endDt.value; + if (auditType.value !== "" && auditType.value !== null) params.auditStatus = Number(auditType.value); if (!isManage.value) { params.scope = "user"; } @@ -90,6 +102,7 @@ watch( pageNum.value = 1; keyword.value = ""; deptType.value = ""; + auditType.value = ""; if (!isManage.value) { const start = new Date(); start.setMonth(start.getMonth() - 1); @@ -215,6 +228,15 @@ const tableHeight = computed(() => Utils.useTableHeight({ isSearch: true, isOper + 流程绑定) ============ */ + +const flowOptions = ref([]); +const flowEnabled = ref(true); +const flowCanEdit = ref(false); // 仅本校管理员可维护绑定 +const boundFlowGuid = ref(""); // 已保存的绑定(提交前的编辑值另用 selectedGuid) +const boundFlowName = ref(""); +const selectedGuid = ref(""); +const bindingSaving = ref(false); + +async function loadFlowBinding() { + try { + const view = await noticeApi.getFlowBinding().send(); + flowEnabled.value = view?.flowEnabled !== false; + flowCanEdit.value = view?.canEdit === true; + boundFlowGuid.value = view?.flowGuid || ""; + boundFlowName.value = view?.flowName || ""; + selectedGuid.value = boundFlowGuid.value; + } catch (e: any) { + Message.error(e.message || "获取流程绑定失败"); + } + try { + flowOptions.value = (await noticeApi.getFlowOptions().send()) || []; + } catch { + flowOptions.value = []; + } +} + +async function saveFlowBinding() { + bindingSaving.value = true; + try { + const view = await noticeApi.saveFlowBinding(selectedGuid.value).send(); + boundFlowGuid.value = view?.flowGuid || ""; + boundFlowName.value = view?.flowName || ""; + selectedGuid.value = boundFlowGuid.value; + Message.success(boundFlowGuid.value ? "绑定成功" : "已解除绑定"); + } catch (e: any) { + Message.error(e.message || "保存失败"); + } finally { + bindingSaving.value = false; + } +} + +function unbindFlow() { + MyConfirm({ + title: "解除绑定", + msg: "确定解除流程绑定吗?", + info: "解除后发送全部用户将不再需要审核,直接发布", + async onOk() { + selectedGuid.value = ""; + await saveFlowBinding(); + }, + }); +} + +/** 流程图(flow-center 示意图深链,与发通知页同一入口) */ +function openFlowDiagram(guid: string) { + if (guid) window.open(`${config.flowHost}/#/diagram?flowGuid=${guid}`, "_blank"); +} + +/** 当前绑定流程的节点摘要(下拉里回显) */ +const boundFlowSummary = computed(() => { + const hit = flowOptions.value.find((f) => f.flowGuid === boundFlowGuid.value); + return hit?.nodeSummary || "—"; +}); + /* ============ 发文部门(直接复用 UserBase 同源 TypeSetBox 卡片组件) ============ */ interface DeptItem { SerID: number; @@ -336,6 +405,7 @@ const groupTableHeight = computed(() => Utils.useTableHeight({ isTabs: true, isS + @@ -407,6 +477,50 @@ const groupTableHeight = computed(() => Utils.useTableHeight({ isTabs: true, isS + +
+ + 绑定说明:绑定后「发送全部用户」的通知必须审核通过才发布(发通知页只显示「提交审核」按钮, + 隐藏发送与定时发送);「发送指定人员」不受影响,仍直接发布。未绑定则保持现有发送方式。 + +
流程中心未接入,暂不支持流程绑定。
+ +
+
@@ -479,4 +593,31 @@ const groupTableHeight = computed(() => Utils.useTableHeight({ isTabs: true, isS display: flex; justify-content: flex-end; } +/* 流程绑定面板 */ +.flow-binding-form { + margin: 16px 0; + display: flex; + align-items: center; + gap: 10px; +} +.flow-binding-form .label { + color: #515a6e; +} +.flow-binding-state { + color: #515a6e; + display: flex; + align-items: center; + gap: 6px; +} +.flow-binding-state .node-summary { + color: #808695; +} +.readonly-hint { + color: #808695; + margin-left: 8px; +} +.flow-binding-empty { + margin-top: 16px; + color: #808695; +}