Compare commits
47
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
690d329371 | ||
|
|
6a63a7cb8f | ||
|
|
f90aefc8e3 | ||
|
|
c1fe0783fb | ||
|
|
3de71aa99b | ||
|
|
8fe3e2ef8c | ||
|
|
13afb22060 | ||
|
|
dbbabc56eb | ||
|
|
5e3f151ce7 | ||
|
|
ab647c67ba | ||
|
|
e8ccc7d3ba | ||
|
|
b685ff5dc0 | ||
|
|
9c8b0f900a | ||
|
|
1af6005a55 | ||
|
|
fea12895c1 | ||
|
|
599fed01b5 | ||
|
|
e0d1b2b769 | ||
|
|
71452fcfd2 | ||
|
|
eee4c10155 | ||
|
|
f611c15a6e | ||
|
|
941031e1b4 | ||
|
|
59f8fad4ca | ||
|
|
f4d0c7d97a | ||
|
|
e37ff9f8d1 | ||
|
|
c81f097188 | ||
|
|
05804f41a9 | ||
|
|
29c051350b | ||
|
|
8c2d539f34 | ||
|
|
693aba9968 | ||
|
|
21a0bd36b6 | ||
|
|
796a1b9a7b | ||
|
|
74b9e80b0d | ||
|
|
1516effb69 | ||
|
|
edd91b53b7 | ||
|
|
7c885c0b4b | ||
|
|
31ca694d48 | ||
|
|
f41719f3c4 | ||
|
|
468cb8670f | ||
|
|
830f9088f4 | ||
|
|
c3ff58fd94 | ||
|
|
1b39e0086d | ||
|
|
c50cafe44e | ||
|
|
99e4ea8369 | ||
|
|
2f423c2d33 | ||
|
|
d2d0eb6cae | ||
|
|
17be4e6445 | ||
|
|
23750b4fde |
@@ -1,7 +0,0 @@
|
|||||||
# 正式环境 - base
|
|
||||||
VITE_BASE_HOST=http://base.yunhaixingtong.com
|
|
||||||
VITE_SSO_HOST=http://sso.yunhaixingtong.com
|
|
||||||
VITE_SERVICE_HOST=http://base.yunhaixingtong.com/notice
|
|
||||||
VITE_ZIYUANKU_HOST=https://base.bestsch.com/BschZyk
|
|
||||||
VITE_ZHUANXIANG_HOST=https://base.bestsch.com/BschZncpd
|
|
||||||
VITE_IS_MOCK=false
|
|
||||||
@@ -1,8 +1,13 @@
|
|||||||
# 开发环境 (dev):API 走 /api 同源相对路径(vite proxy 转发到本地后端 8070),
|
# 开发环境 (dev):仅 vite dev --mode dev 加载;build 产物不读任何 .env.*
|
||||||
|
# (.env.base/.env.ld/.env.prod 已删,线上由 /api/env-config?target=front 运行时下发)。
|
||||||
|
# ⚠️ VITE_* 仅本地开发生效。
|
||||||
|
# API 走 /api 同源相对路径(vite proxy 转发到本地后端 8101),
|
||||||
# 规避后端 CAS 过滤器不回 CORS 头导致的跨域拦截;SSO/基础平台用真实环境
|
# 规避后端 CAS 过滤器不回 CORS 头导致的跨域拦截;SSO/基础平台用真实环境
|
||||||
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
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
# 正式环境 - ld
|
|
||||||
VITE_BASE_HOST=https://ld.bestsch.com
|
|
||||||
VITE_SSO_HOST=http://sso.yunhaixingtong.com
|
|
||||||
VITE_SERVICE_HOST=https://ld.yunhaixingtong.com/notice
|
|
||||||
VITE_ZIYUANKU_HOST=https://ld.bestsch.com/BschZyk
|
|
||||||
VITE_ZHUANXIANG_HOST=https://ld.bestsch.com/BschZncpd
|
|
||||||
VITE_IS_MOCK=false
|
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
# 本地纯前端调试模式:后端 SSO/base 不可达时启用内置 mock
|
# 本地纯前端调试模式:仅 vite dev --mode mock 加载(build 产物不读任何 .env.*,
|
||||||
|
# 线上由 /api/env-config?target=front 运行时下发)。后端 SSO/base 不可达时启用内置 mock
|
||||||
# 启动:npm run dev:mock
|
# 启动:npm run dev:mock
|
||||||
VITE_BASE_HOST=
|
VITE_BASE_HOST=
|
||||||
VITE_SSO_HOST=
|
VITE_SSO_HOST=
|
||||||
VITE_SERVICE_HOST=http://localhost:8070
|
VITE_SERVICE_HOST=http://localhost:8101
|
||||||
VITE_ZIYUANKU_HOST=
|
VITE_ZIYUANKU_HOST=
|
||||||
VITE_ZHUANXIANG_HOST=
|
VITE_ZHUANXIANG_HOST=
|
||||||
VITE_IS_MOCK=true
|
VITE_IS_MOCK=true
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
# 生产环境 - prod(vite build --mode prod 加载此文件)
|
|
||||||
VITE_BASE_HOST=http://base.yunhaixingtong.com
|
|
||||||
VITE_SSO_HOST=http://sso.yunhaixingtong.com
|
|
||||||
VITE_SERVICE_HOST=http://base.yunhaixingtong.com/notice
|
|
||||||
VITE_ZIYUANKU_HOST=https://base.bestsch.com/BschZyk
|
|
||||||
VITE_ZHUANXIANG_HOST=https://base.bestsch.com/BschZncpd
|
|
||||||
VITE_IS_MOCK=false
|
|
||||||
@@ -38,10 +38,16 @@ jobs:
|
|||||||
- name: 构建前端(node 容器)
|
- name: 构建前端(node 容器)
|
||||||
run: |
|
run: |
|
||||||
set -e
|
set -e
|
||||||
docker rm -f frontbuild 2>/dev/null || true
|
# 构建容器名必须全局唯一:runner 挂的是宿主 docker.sock,容器名在整个 daemon 上是共享
|
||||||
docker run -d --name frontbuild -w /build node:20 sleep 3600
|
# 命名空间。旧写法固定名 frontbuild 会在多仓/多条 workflow 并发时互相 docker rm -f 掉
|
||||||
docker cp "${PWD}/." frontbuild:/build/
|
# 对方正在构建的容器,表现为无编译报错的 exit 137(SIGKILL,2026-09-10 三仓同推即撞)。
|
||||||
docker exec frontbuild sh -c '
|
# GITHUB_RUN_ID 实例级唯一,再拼时间戳兜底(手动重跑/未注入时也不撞名)。
|
||||||
|
BUILD_CONTAINER="frontbuild-${GITHUB_RUN_ID:-0}-$(date +%s)"
|
||||||
|
echo "BUILD_CONTAINER=${BUILD_CONTAINER}"
|
||||||
|
docker rm -f "$BUILD_CONTAINER" 2>/dev/null || true
|
||||||
|
docker run -d --name "$BUILD_CONTAINER" -w /build node:20 sleep 3600
|
||||||
|
docker cp "${PWD}/." "$BUILD_CONTAINER":/build/
|
||||||
|
docker exec "$BUILD_CONTAINER" sh -c '
|
||||||
set -e
|
set -e
|
||||||
cd /build
|
cd /build
|
||||||
if [ -f .npmrc ]; then sed -i "/SASS_BINARY_PATH/d" .npmrc; fi
|
if [ -f .npmrc ]; then sed -i "/SASS_BINARY_PATH/d" .npmrc; fi
|
||||||
@@ -49,13 +55,15 @@ jobs:
|
|||||||
npm run build
|
npm run build
|
||||||
'
|
'
|
||||||
echo "=== dist 产物 ==="
|
echo "=== dist 产物 ==="
|
||||||
docker exec frontbuild sh -c 'ls -la /build/dist/ | head -20'
|
docker exec "$BUILD_CONTAINER" sh -c 'ls -la /build/dist/ | head -20'
|
||||||
# 从容器 cp dist 到宿主工作目录
|
# 从容器 cp dist 到宿主工作目录
|
||||||
docker cp frontbuild:/build/dist ./dist
|
docker cp "$BUILD_CONTAINER":/build/dist ./dist
|
||||||
docker rm -f frontbuild
|
docker rm -f "$BUILD_CONTAINER"
|
||||||
echo "BUILD CHECK OK"
|
echo "BUILD CHECK OK"
|
||||||
|
|
||||||
# 打包 dist → zip(进入 dist 目录打包,zip 根目录直接是 html/css/js 文件)
|
# 打包 dist → zip(vite outDir 已简化为直出 dist/,无中间层;进入 dist 打包,
|
||||||
|
# zip 根目录直接是 index.html/css/js。此前 outDir=dist/base 时曾误打 dist/ 整层,
|
||||||
|
# zip 根只有 base/ 目录,projman 前端发布校验报「zip 根目录缺少 html 文件」)
|
||||||
- name: 打包产物
|
- name: 打包产物
|
||||||
run: |
|
run: |
|
||||||
TAG=$(echo "$GITHUB_SHA" | cut -c1-8)
|
TAG=$(echo "$GITHUB_SHA" | cut -c1-8)
|
||||||
|
|||||||
+46
@@ -5,6 +5,52 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="../../favicon.ico" />
|
<link rel="icon" type="image/svg+xml" href="../../favicon.ico" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<!-- 运行时配置引导:在任何应用模块执行前同步拉取 /<contextPath>/api/env-config
|
||||||
|
(后端免登录端点,projman 配置管理在线维护),替代生产构建环境变量。
|
||||||
|
必须同步:host.config.ts 在模块求值期就读 window.__APP_CONFIG__;失败/本地开发
|
||||||
|
(根路径,段数不足 2)则不设置:本地走 .env.dev/.env.mock(VITE_*),线上走同域推导兜底。
|
||||||
|
target = URL 第 2 段目录名:本应用产物部署于 /{contextPath}/front/,故为 front,
|
||||||
|
对应后端配置键 yh.env-config.source.front(环境变量 YH_ENV_CONFIG_SOURCE_FRONT)。
|
||||||
|
注意:同步 XHR 不支持 timeout 属性(赋值会抛 InvalidAccessError),依赖内网快速失败 -->
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
try {
|
||||||
|
var seg = location.pathname.split("/").filter(Boolean);
|
||||||
|
if (seg.length < 2) return; // 本地开发(根路径):走 .env.dev / .env.mock
|
||||||
|
var xhr = new XMLHttpRequest();
|
||||||
|
xhr.open("GET", "/" + seg[0] + "/api/env-config?target=" + encodeURIComponent(seg[1]), false);
|
||||||
|
xhr.send(null);
|
||||||
|
if (xhr.status === 200) {
|
||||||
|
var conf = JSON.parse(xhr.responseText);
|
||||||
|
if (conf && typeof conf === "object") window.__APP_CONFIG__ = conf;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
/* 拉取失败保持未设置,走同域推导兜底 */
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
<!-- 主题通道引导(A 类免插件接入,方案 §11):唯一接入点,不装依赖、不改 vite.config 也能吃主题。
|
||||||
|
origin 零写死,优先级四档:
|
||||||
|
__APP_CONFIG__.staticHost(专用通道覆盖,projman env-config 在线配)
|
||||||
|
> __APP_CONFIG__.baseHost(换域名改配置即生效)
|
||||||
|
> %VITE_THEME_HOST%(仅 dev 注入:主题通道专用域,需通道已开 CORS)
|
||||||
|
> 同源相对 "/static/…"(生产常态:通道与应用同域;本地 dev 由 vite /static 代理兜到通道)
|
||||||
|
为何不直接复用 .env.dev 的 VITE_BASE_HOST:那会让 dev 跨源直连线上域,而通道 Caddy 至今没有
|
||||||
|
Access-Control-Allow-Origin(方案 §10.4 现网无该头)→ manifest 是 fetch 跨源,会被拦成降级。
|
||||||
|
运维加上那行头之后,在 .env.dev 补 VITE_THEME_HOST=http://base.yunhaixingtong.com 即切直连,零改码。
|
||||||
|
document.write 保证 parser-blocking:init 的档位挂载与 FOUC 门先于首帧(同旧插件 initBootstrap 机制,
|
||||||
|
且 vite build 不解析内联脚本里的字符串,规避 "/static/…" 被当本仓资源解析报错;build 产物不读
|
||||||
|
.env.*,占位符原样留字面量 → 判首字符 % 置空,产物零环境值烧入)。
|
||||||
|
挂载/FOUC/CSS 排序全部由通道 init 脚本承担,逻辑升级全走 /static 通道,无需重新构建本应用 -->
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
var c = window.__APP_CONFIG__ || {};
|
||||||
|
var themeHost = "%VITE_THEME_HOST%";
|
||||||
|
if (themeHost.charAt(0) === "%") themeHost = "";
|
||||||
|
var host = String(c.staticHost || c.baseHost || themeHost).replace(/\/+$/, "");
|
||||||
|
document.write('<script src="' + host + '/static/js/yh-theme-init.js"><\/script>');
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
<title><%- title %></title>
|
<title><%- title %></title>
|
||||||
<link rel="stylesheet" href="./libs/ztree/zTreeStyle.css" />
|
<link rel="stylesheet" href="./libs/ztree/zTreeStyle.css" />
|
||||||
<script src="./libs/jquery.min.js"></script>
|
<script src="./libs/jquery.min.js"></script>
|
||||||
|
|||||||
Generated
+2590
-2800
File diff suppressed because it is too large
Load Diff
@@ -41,8 +41,6 @@
|
|||||||
"@unocss/preset-uno": "^66.6.0",
|
"@unocss/preset-uno": "^66.6.0",
|
||||||
"@vitejs/plugin-vue": "^5.0.4",
|
"@vitejs/plugin-vue": "^5.0.4",
|
||||||
"fast-glob": "^3.3.2",
|
"fast-glob": "^3.3.2",
|
||||||
"node-sass": "^6.0.1",
|
|
||||||
"sass-loader": "^10.0.1",
|
|
||||||
"typescript": "^5.2.2",
|
"typescript": "^5.2.2",
|
||||||
"unocss": "^66.6.0",
|
"unocss": "^66.6.0",
|
||||||
"unplugin-auto-import": "0.18.3",
|
"unplugin-auto-import": "0.18.3",
|
||||||
|
|||||||
+9
-2
@@ -62,7 +62,7 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error("加载用户信息失败", error);
|
console.error("加载用户信息失败", error);
|
||||||
if (error !== "redirect") {
|
if (error?.message !== "redirect") {
|
||||||
Message.error("载入用户信息失败,请检查后端服务是否正常!");
|
Message.error("载入用户信息失败,请检查后端服务是否正常!");
|
||||||
}
|
}
|
||||||
// 降级:使用默认值("/" 即已收到页,无需跳转)
|
// 降级:使用默认值("/" 即已收到页,无需跳转)
|
||||||
@@ -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>
|
||||||
@@ -101,4 +102,10 @@ body {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
/* 表格操作列(operatingHandle 渲染的 Tooltip 包裹层)横排且随单元格居中 */
|
||||||
|
.ivu-table-cell > .ivu-tooltip {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
margin: 0 2px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import { Method } from "alova";
|
||||||
|
import { alovaInstance } from "@/utils/alova/alova-iview";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用内审批接口封装(第三步 T-C4,"我的待审核")。
|
||||||
|
* 走本应用后端代理 /api/notice/approval/**(operator=CAS 登录态,前端不传身份);
|
||||||
|
* 后端经网关调 flow-center,契约=BasePageResult{total,content,...}。
|
||||||
|
*/
|
||||||
|
|
||||||
|
const APPROVAL = "/api/notice/approval";
|
||||||
|
|
||||||
|
function get<T = any>(url: string, params?: Record<string, any>): Method<T> {
|
||||||
|
return new Method<T>("GET", alovaInstance, url, { params });
|
||||||
|
}
|
||||||
|
|
||||||
|
function postJSON<T = any>(url: string, data?: Record<string, any>): Method<T> {
|
||||||
|
return new Method<T>(
|
||||||
|
"POST",
|
||||||
|
alovaInstance,
|
||||||
|
url,
|
||||||
|
{ headers: { "Content-Type": "application/json;charset=UTF-8" } },
|
||||||
|
data ? JSON.stringify(data) : undefined
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApprovalTodoTask {
|
||||||
|
taskId: number;
|
||||||
|
instanceId: number;
|
||||||
|
instanceVersion: number | null;
|
||||||
|
bizType: string;
|
||||||
|
bizId: string;
|
||||||
|
bizTitle: string;
|
||||||
|
bizSnapshot: string | null;
|
||||||
|
nodeId: string;
|
||||||
|
nodeName: string;
|
||||||
|
assigneeId: number;
|
||||||
|
assigneeName: string;
|
||||||
|
arriveTime: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApprovalInstance {
|
||||||
|
instanceId: number;
|
||||||
|
status: number;
|
||||||
|
version: number;
|
||||||
|
bizType: string;
|
||||||
|
bizId: string;
|
||||||
|
bizTitle: string;
|
||||||
|
bizUrl: string | null;
|
||||||
|
submitterName: string;
|
||||||
|
submitTime: string;
|
||||||
|
finishTime: string | null;
|
||||||
|
currentNodes: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApprovalPage<T> {
|
||||||
|
total: number;
|
||||||
|
content: T[];
|
||||||
|
pageIndex: number;
|
||||||
|
pageSize: number;
|
||||||
|
totalPages: number;
|
||||||
|
hasNext: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const approvalApi = {
|
||||||
|
/** 我的待审核(bizType=notice 固定在后端) */
|
||||||
|
todo(page = 1, size = 10): Method<ApprovalPage<ApprovalTodoTask>> {
|
||||||
|
return get(`${APPROVAL}/Todo`, { page, size });
|
||||||
|
},
|
||||||
|
/** 办件视角(status 可空;10 审批中/20 完成/30 拒绝/40 撤回) */
|
||||||
|
instances(status?: number, page = 1, size = 10): Method<ApprovalPage<ApprovalInstance>> {
|
||||||
|
return get(`${APPROVAL}/Instances`, status ? { status, page, size } : { page, size });
|
||||||
|
},
|
||||||
|
/** 通过(opinion 可选) */
|
||||||
|
approve(taskId: number, opinion?: string): Method<void> {
|
||||||
|
return postJSON(`${APPROVAL}/Approve?taskId=${taskId}`, opinion ? { opinion } : {});
|
||||||
|
},
|
||||||
|
/** 拒绝(opinion 必填) */
|
||||||
|
reject(taskId: number, opinion: string): Method<void> {
|
||||||
|
return postJSON(`${APPROVAL}/Reject?taskId=${taskId}`, { opinion });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default approvalApi;
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { Method } from "alova";
|
||||||
|
import { alovaInstance } from "@/utils/alova/alova-iview";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用内「抄送给我的」接口封装(2026-09-24)。
|
||||||
|
* 走本应用后端 /api/notice/cc/My(身份=CAS 登录态,前端不传身份);
|
||||||
|
* 数据源是 flow.cc.sent 回调落的 notice_cc 表,点击行跳通知详情。
|
||||||
|
*/
|
||||||
|
|
||||||
|
function get<T = any>(url: string, params?: Record<string, any>): Method<T> {
|
||||||
|
return new Method<T>("GET", alovaInstance, url, { params });
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CcItem {
|
||||||
|
id: number;
|
||||||
|
noticeId: number;
|
||||||
|
flowInstanceId: number | null;
|
||||||
|
nodeId: string | null;
|
||||||
|
nodeName: string | null;
|
||||||
|
sentTime: string;
|
||||||
|
title: string | null;
|
||||||
|
deptName: string | null;
|
||||||
|
publisherName: string | null;
|
||||||
|
noticeStatus: number | null;
|
||||||
|
auditStatus: number | null;
|
||||||
|
auditStatusDesc: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CcPage {
|
||||||
|
total: number;
|
||||||
|
content: CcItem[];
|
||||||
|
pageIndex: number;
|
||||||
|
pageSize: number;
|
||||||
|
totalPages: number;
|
||||||
|
hasNext: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ccApi = {
|
||||||
|
/** 抄送给我的(分页,sent_time 倒序) */
|
||||||
|
my(page = 1, size = 10): Method<CcPage> {
|
||||||
|
return get(`/api/notice/cc/My`, { page, size });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ccApi;
|
||||||
@@ -15,6 +15,9 @@ import type {
|
|||||||
NoticeRequest,
|
NoticeRequest,
|
||||||
NoticeQuery,
|
NoticeQuery,
|
||||||
DictOption,
|
DictOption,
|
||||||
|
FlowOption,
|
||||||
|
FlowTraceVo,
|
||||||
|
FlowBindingVo,
|
||||||
DeptManageItem,
|
DeptManageItem,
|
||||||
ContactGroupPage,
|
ContactGroupPage,
|
||||||
ContactUserPage,
|
ContactUserPage,
|
||||||
@@ -107,6 +110,36 @@ 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 });
|
||||||
|
}
|
||||||
|
/** 撤回审核(2026-09-17:后端经 SDK 托管件中转,仅创建人本人+审批中;替代原先前端直调 flow-center) */
|
||||||
|
flowWithdraw(serid: number) {
|
||||||
|
return postForm<void>(`${NOTICE}/flow/Withdraw`, { serid });
|
||||||
|
}
|
||||||
|
/** 办件进度嵌入页地址(Drawer iframe 直嵌;pageUrl 含短时 token 现取现发,勿缓存复用) */
|
||||||
|
flowEmbed(serid: number) {
|
||||||
|
return get<{ pageUrl: string }>(`${NOTICE}/flow/Embed`, { serid });
|
||||||
|
}
|
||||||
|
/** 应用级流程绑定(应用设置 > 流程绑定;发通知页据此判断「提交审核」还是「发送」) */
|
||||||
|
getFlowBinding() {
|
||||||
|
return get<FlowBindingVo>(`${NOTICE}/flow/Binding`);
|
||||||
|
}
|
||||||
|
/** 保存/解除绑定(flowGuid 空=解绑;仅本校管理员) */
|
||||||
|
saveFlowBinding(flowGuid: string) {
|
||||||
|
return postForm<FlowBindingVo>(`${NOTICE}/flow/Binding/Save`, { flowGuid });
|
||||||
|
}
|
||||||
|
|
||||||
// ========== 收件箱 ==========
|
// ========== 收件箱 ==========
|
||||||
/** 已收通知列表 */
|
/** 已收通知列表 */
|
||||||
list(params: NoticeQuery) {
|
list(params: NoticeQuery) {
|
||||||
@@ -214,11 +247,6 @@ class SettingApi {
|
|||||||
sortDeptDic(ids: string) {
|
sortDeptDic(ids: string) {
|
||||||
return postForm<void>(`${DICT}/Sort`, { ids });
|
return postForm<void>(`${DICT}/Sort`, { ids });
|
||||||
}
|
}
|
||||||
/** 同步基础平台部门 */
|
|
||||||
getDeptSync() {
|
|
||||||
return get<void>(`${DICT}/Sync`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========== 自定义组管理 ==========
|
// ========== 自定义组管理 ==========
|
||||||
/** 获取联系组列表 */
|
/** 获取联系组列表 */
|
||||||
getGroup(page: number, count: number) {
|
getGroup(page: number, count: number) {
|
||||||
|
|||||||
@@ -1,22 +1,69 @@
|
|||||||
import { BaseConf } from "@/types/Basics";
|
import { BaseConf } from "@/types/Basics";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 通过 Vite 环境变量管理配置,不再依赖 window.conf / config.js
|
* 配置优先级(高 → 低):
|
||||||
* 环境变量定义在项目根目录的 .env.dev / .env.base / .env.ld 中
|
* ① window.__APP_CONFIG__:线上运行时配置,index.html 引导脚本在应用模块加载前同步拉取
|
||||||
|
* /<contextPath>/api/env-config?target=<部署目录名>(本应用部署于 /{contextPath}/front/,
|
||||||
|
* 故为 front,对应后端键 yh.env-config.source.front,projman 配置管理维护)写入
|
||||||
|
* ② import.meta.env.VITE_*:仅本地开发/演示(vite dev --mode dev 读 .env.dev、--mode mock 读 .env.mock);
|
||||||
|
* build 产物不读任何 .env.*(.env.base/.env.ld/.env.prod 已删),VITE_* 恒为 undefined,
|
||||||
|
* 本层自动跳过,产物不含任何环境值
|
||||||
|
* ③ 同域推导兜底:env-config 未配置/拉取失败时框架至少能渲染并调通同源 API;
|
||||||
|
* ssoHost 为空 → 登出跳转不可用——与后端未就绪状态一致,可接受。
|
||||||
*/
|
*/
|
||||||
const env = import.meta.env;
|
const env = import.meta.env;
|
||||||
|
|
||||||
|
// ③ 同域推导兜底:context 段取当前路径首段(/{contextPath}/front/ → contextPath,即后端上下文),
|
||||||
|
// 与服务端 context-path 对齐(本地无、base/prod /notice);本地 dev/mock 在根路径运行(无段),
|
||||||
|
// serviceHost 留空走 vite 同源代理(规避 CAS 过滤器不回 CORS 头)
|
||||||
|
const seg = window.location.pathname.split("/").filter(Boolean);
|
||||||
|
const origin = window.location.origin;
|
||||||
|
|
||||||
let conf: BaseConf = {
|
let conf: BaseConf = {
|
||||||
|
baseHost: origin,
|
||||||
|
ssoHost: "",
|
||||||
|
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: {
|
||||||
|
ziyuankuHost: "",
|
||||||
|
zhuanxiangHost: "",
|
||||||
|
},
|
||||||
|
isMock: false,
|
||||||
|
isDebug: env.DEV,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 逐键覆盖:只覆盖来源里实际出现且非空的键,未出现的键沿用底层值
|
||||||
|
const applyConf = (target: any, source: any) => {
|
||||||
|
if (!source || typeof source !== "object") return;
|
||||||
|
Object.keys(source).forEach(key => {
|
||||||
|
if (source[key] !== undefined && source[key] !== null && source[key] !== "") {
|
||||||
|
(target as any)[key] = source[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// ② 本地开发/演示(build 产物中恒 undefined → 自动跳过)
|
||||||
|
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,
|
||||||
},
|
},
|
||||||
isMock: env.VITE_IS_MOCK === "true",
|
isMock: env.VITE_IS_MOCK === "true",
|
||||||
isDebug: env.DEV,
|
});
|
||||||
};
|
|
||||||
|
// ① 线上运行时配置(最高优先,覆盖前面所有);relayConf 为嵌套对象,按子键合并,
|
||||||
|
// 运行时 JSON 只配部分资源库地址时未配的键沿用兜底值
|
||||||
|
const runtime = (window as any).__APP_CONFIG__;
|
||||||
|
applyConf(conf, runtime);
|
||||||
|
if (runtime && typeof runtime === "object" && runtime.relayConf) {
|
||||||
|
applyConf(conf.relayConf, runtime.relayConf);
|
||||||
|
}
|
||||||
|
|
||||||
// HTTPS 自动升级:当前页面是 https 时,将所有 http:// 配置项升级为 https://
|
// HTTPS 自动升级:当前页面是 https 时,将所有 http:// 配置项升级为 https://
|
||||||
var ishttps = "https:" == document.location.protocol ? true : false;
|
var ishttps = "https:" == document.location.protocol ? true : false;
|
||||||
|
|||||||
@@ -9,8 +9,10 @@ export function getMenuList(): Promise<IModule[]> {
|
|||||||
{ id: 1, modId: 1, name: "已收到", page: "/" },
|
{ id: 1, modId: 1, name: "已收到", page: "/" },
|
||||||
{ id: 2, modId: 2, name: "已发送", page: "/list" },
|
{ id: 2, modId: 2, name: "已发送", page: "/list" },
|
||||||
{ id: 3, modId: 3, name: "全部通知", page: "/noticelist" },
|
{ id: 3, modId: 3, name: "全部通知", page: "/noticelist" },
|
||||||
|
{ id: 89, modId: 89, name: "待审核", page: "/approval" },
|
||||||
{ id: 4, modId: 4, name: "通知统计", page: "/stats" },
|
{ id: 4, modId: 4, name: "通知统计", page: "/stats" },
|
||||||
{ id: 21, modId: 21, name: "应用设置", page: "/setting" },
|
{ id: 21, modId: 21, name: "应用设置", page: "/setting" },
|
||||||
|
{ id: 91, modId: 91, name: "抄送给我的", page: "/cc" },
|
||||||
];
|
];
|
||||||
return Promise.resolve(menu);
|
return Promise.resolve(menu);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><path d="M859.977 336.128H608.251V171.489c0-41.159-33.318-74.836-74.039-74.836h-44.421c-40.729 0-74.044 33.677-74.044 74.836v164.639H164.023c-16.289 0-29.616 13.473-29.616 29.936v149.669c0 16.469 13.326 29.942 29.616 29.942h14.803v231.241c0 65.859-14.803 120.493-14.803 120.493 0 16.459 13.324 29.938 29.611 29.938h636.729c16.291 0 29.615-13.481 29.615-29.938 0 0-14.81-56.882-14.81-123.486V545.675h14.81c16.287 0 29.616-13.477 29.616-29.938V366.064c-0.001-16.469-13.323-29.936-29.617-29.936zM682.282 897.403V699.089c0-5.989-5.18-11.225-11.103-11.225h-7.407c-5.917 0-11.103 5.236-11.103 11.225v198.314H534.212V699.089c0-5.989-5.181-11.225-11.116-11.225H515.7c-5.918 0-11.106 5.236-11.106 11.225v198.314H378.731V699.089c0-5.989-5.186-11.225-11.111-11.225h-7.399c-5.918 0-11.106 5.236-11.106 11.225v198.314H193.634s14.812-53.884 14.812-120.487V545.675h607.113v231.241c0 66.606 14.804 120.487 14.804 120.487H682.282z m177.695-396.634c0 8.233-6.66 14.968-14.81 14.968H178.826c-8.145 0-14.807-6.735-14.807-14.968V381.028c0-8.235 6.662-14.964 14.807-14.964H445.36V178.971c0-29.184 22.956-52.382 51.835-52.382h29.61c28.876 0 51.828 23.199 51.828 52.382v187.093h266.535c8.15 0 14.81 6.729 14.81 14.964v119.741z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,7 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
|
||||||
|
<path fill="#4aaff0" d="M512,585 C415,455 330,398 215,395 C185,395 160,415 160,450 L160,770 C160,815 190,838 240,845 C340,856 430,872 512,888 C594,872 684,856 784,845 C834,838 864,815 864,770 L864,450 C864,415 839,395 809,395 C694,398 609,455 512,585 Z"/>
|
||||||
|
<path fill="#4aaff0" d="M218,312 C335,292 462,352 514,462 C452,404 334,354 218,378 Z"/>
|
||||||
|
<circle fill="#4aaff0" cx="218" cy="345" r="33"/>
|
||||||
|
<path fill="#80d976" d="M508,462 C455,310 525,145 700,105 C758,250 668,425 508,462 Z"/>
|
||||||
|
<path fill="none" stroke="#4aaff0" stroke-width="44" stroke-linecap="round" d="M210,935 Q285.5,881 361,935 T512,935 T663,935 T814,935"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 701 B |
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* 通知公告本地 Mock 数据(vite-plugin-mock,dev 下 VITE_IS_MOCK=true 生效)。
|
* 通知公告本地 Mock 数据(vite-plugin-mock,dev 下 VITE_IS_MOCK=true 生效)。
|
||||||
*
|
*
|
||||||
* 背景:本地无法访问 sso.yunhaixingtong.com / base.yunhaixingtong.com,
|
* 背景:本地无法访问真实环境的 SSO / 基础平台域名,
|
||||||
* 后端 CAS 过滤器会拦截 /api/* 请求,导致前端无法联调。
|
* 后端 CAS 过滤器会拦截 /api/* 请求,导致前端无法联调。
|
||||||
* 该文件按后端 NoticeApiController / DictController / ContactsController /
|
* 该文件按后端 NoticeApiController / DictController / ContactsController /
|
||||||
* FileController 的真实响应契约({code, msg, data})提供内存数据,
|
* FileController 的真实响应契约({code, msg, data})提供内存数据,
|
||||||
@@ -252,6 +252,33 @@ function fileUrl(id: number) {
|
|||||||
|
|
||||||
/* ============================ Mock 定义 ============================ */
|
/* ============================ Mock 定义 ============================ */
|
||||||
|
|
||||||
|
/* ---------- 我的待审核(flow-center 代理)内存数据(刷新/改本文件即重置为初始 6 条) ---------- */
|
||||||
|
const approvalTodos = [
|
||||||
|
{ taskId: 5001, instanceId: 8001, instanceVersion: 1, bizType: "notice", bizId: "1", bizTitle: "关于开展2026年秋季学期教学检查的通知", bizSnapshot: JSON.stringify({ summary: "定于下周开展秋季学期教学检查,请各教研组提前准备教案、听课记录。" }), nodeId: "n_dept", nodeName: "部门审核", assigneeId: 186532, assigneeName: "朱皓东", arriveTime: "2026-09-23T09:12:00" },
|
||||||
|
{ taskId: 5002, instanceId: 8002, instanceVersion: 1, bizType: "notice", bizId: "2", bizTitle: "校园网络安全培训通知", bizSnapshot: JSON.stringify({ summary: "本周五下午 15:00 在报告厅开展网络安全培训,请全体教职工参加。" }), nodeId: "n_school", nodeName: "校级审核", assigneeId: 186532, assigneeName: "朱皓东", arriveTime: "2026-09-23T10:40:00" },
|
||||||
|
{ taskId: 5003, instanceId: 8003, instanceVersion: 2, bizType: "notice", bizId: "5", bizTitle: "期中考试考务工作安排", bizSnapshot: JSON.stringify({ summary: "期中考试定于第 10 周,请各年级上报监考安排。" }), nodeId: "n_dept", nodeName: "部门审核", assigneeId: 186532, assigneeName: "朱皓东", arriveTime: "2026-09-22T15:05:00" },
|
||||||
|
{ taskId: 5004, instanceId: 8004, instanceVersion: 1, bizType: "notice", bizId: "7", bizTitle: "教师职称评审材料公示", bizSnapshot: JSON.stringify({ summary: "本年度职称评审材料已汇总,公示期 5 个工作日。" }), nodeId: "n_office", nodeName: "办公室复核", assigneeId: 186532, assigneeName: "朱皓东", arriveTime: "2026-09-22T08:30:00" },
|
||||||
|
{ taskId: 5005, instanceId: 8005, instanceVersion: 1, bizType: "notice", bizId: "9", bizTitle: "2027 年寒假放假通知", bizSnapshot: JSON.stringify({ summary: "寒假自 1 月 20 日起,下学期 2 月 15 日报到。" }), nodeId: "n_dept", nodeName: "部门审核", assigneeId: 186532, assigneeName: "朱皓东", arriveTime: "2026-09-21T16:20:00" },
|
||||||
|
{ taskId: 5006, instanceId: 8006, instanceVersion: 1, bizType: "notice", bizId: "12", bizTitle: "校园运动会报名通知", bizSnapshot: JSON.stringify({ summary: "秋季运动会报名截止本周五,请各班级汇总上报。" }), nodeId: "n_school", nodeName: "校级审核", assigneeId: 186532, assigneeName: "朱皓东", arriveTime: "2026-09-21T11:00:00" },
|
||||||
|
];
|
||||||
|
const approvalInstances = [
|
||||||
|
{ instanceId: 8001, status: 10, version: 1, bizType: "notice", bizId: "1", bizTitle: "关于开展2026年秋季学期教学检查的通知", bizUrl: null, submitterName: "刘文娟", submitTime: "2026-09-20T09:00:00", finishTime: null, currentNodes: "部门审核" },
|
||||||
|
{ instanceId: 8002, status: 20, version: 1, bizType: "notice", bizId: "2", bizTitle: "校园网络安全培训通知", bizUrl: null, submitterName: "黄瑶", submitTime: "2026-09-19T10:00:00", finishTime: "2026-09-21T14:00:00", currentNodes: "" },
|
||||||
|
{ instanceId: 8003, status: 30, version: 2, bizType: "notice", bizId: "5", bizTitle: "期中考试考务工作安排", bizUrl: null, submitterName: "张明", submitTime: "2026-09-18T08:30:00", finishTime: "2026-09-19T09:10:00", currentNodes: "" },
|
||||||
|
{ instanceId: 8004, status: 40, version: 1, bizType: "notice", bizId: "7", bizTitle: "教师职称评审材料公示", bizUrl: null, submitterName: "李强", submitTime: "2026-09-17T16:00:00", finishTime: "2026-09-17T16:30:00", currentNodes: "" },
|
||||||
|
{ instanceId: 8005, status: 10, version: 1, bizType: "notice", bizId: "9", bizTitle: "2027 年寒假放假通知", bizUrl: null, submitterName: "王芳", submitTime: "2026-09-16T09:00:00", finishTime: null, currentNodes: "校级审核" },
|
||||||
|
{ instanceId: 8006, status: 20, version: 1, bizType: "notice", bizId: "12", bizTitle: "校园运动会报名通知", bizUrl: null, submitterName: "陈垠", submitTime: "2026-09-15T10:00:00", finishTime: "2026-09-16T11:00:00", currentNodes: "" },
|
||||||
|
];
|
||||||
|
/** 组装 flow-center BasePageResult{total,content,pageIndex,pageSize,totalPages,hasNext} */
|
||||||
|
function approvalPage<T>(all: T[], query: any) {
|
||||||
|
const pageIndex = Number(query?.page || 1);
|
||||||
|
const pageSize = Number(query?.size || 10);
|
||||||
|
const start = (pageIndex - 1) * pageSize;
|
||||||
|
const total = all.length;
|
||||||
|
const totalPages = Math.max(1, Math.ceil(total / pageSize));
|
||||||
|
return { total, content: all.slice(start, start + pageSize), pageIndex, pageSize, totalPages, hasNext: pageIndex < totalPages };
|
||||||
|
}
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
/* ---------- 布局 / 用户 ---------- */
|
/* ---------- 布局 / 用户 ---------- */
|
||||||
{
|
{
|
||||||
@@ -271,11 +298,15 @@ export default [
|
|||||||
method: "get",
|
method: "get",
|
||||||
response: () =>
|
response: () =>
|
||||||
ok([
|
ok([
|
||||||
{ id: 1, modId: 1, name: "已收到", parentId: 0, page: "/", orderNumber: 1, usable: true, children: [] },
|
// id/modId 对齐平台模块管理的「配置 ID」(对照 /base/GetUserMod 现行数据);
|
||||||
{ id: 2, modId: 2, name: "已发送", parentId: 0, page: "/list", orderNumber: 2, usable: true, children: [] },
|
// icon:YschIcons 图标名,layout 渲染为 iconfont icon-<名称>
|
||||||
{ id: 3, modId: 3, name: "全部通知", parentId: 0, page: "/noticelist", orderNumber: 3, usable: true, children: [] },
|
{ id: 1, modId: 1, name: "已收到", parentId: 0, page: "/", icon: "sjx", orderNumber: 1, usable: true, children: [] },
|
||||||
{ id: 4, modId: 4, name: "通知统计", parentId: 0, page: "/stats", orderNumber: 4, usable: true, children: [] },
|
{ id: 2, modId: 2, name: "已发送", parentId: 0, page: "/list", icon: "fasong", orderNumber: 2, usable: true, children: [] },
|
||||||
{ id: 21, modId: 21, name: "应用设置", parentId: 0, page: "/setting", orderNumber: 5, usable: true, children: [] },
|
{ id: 3, modId: 3, name: "全部通知", parentId: 0, page: "/noticelist", icon: "liebiao", orderNumber: 3, usable: true, children: [] },
|
||||||
|
{ id: 89, modId: 89, name: "待审核", parentId: 0, page: "/approval", icon: "shenpi", orderNumber: 4, usable: true, children: [] },
|
||||||
|
{ id: 4, modId: 4, name: "通知统计", parentId: 0, page: "/stats", icon: "tongji", orderNumber: 5, usable: true, children: [] },
|
||||||
|
{ id: 21, modId: 21, name: "应用设置", parentId: 0, page: "/setting", icon: "shezhi", orderNumber: 6, usable: true, children: [] },
|
||||||
|
{ id: 91, modId: 91, name: "抄送给我的", parentId: 0, page: "/cc", icon: "", orderNumber: 7, usable: true, children: [] },
|
||||||
]),
|
]),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -721,4 +752,38 @@ export default [
|
|||||||
}))
|
}))
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/* ---------- 我的待审核(NoticeApproval → /api/notice/approval/**) ---------- */
|
||||||
|
{
|
||||||
|
url: "/api/notice/approval/Todo",
|
||||||
|
method: "get",
|
||||||
|
response: ({ query }: any) => ok(approvalPage(approvalTodos, query)),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: "/api/notice/approval/Instances",
|
||||||
|
method: "get",
|
||||||
|
response: ({ query }: any) => {
|
||||||
|
const status = Number(query?.status || 0);
|
||||||
|
const list = status ? approvalInstances.filter((i) => i.status === status) : approvalInstances;
|
||||||
|
return ok(approvalPage(list, query));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: "/api/notice/approval/Approve",
|
||||||
|
method: "post",
|
||||||
|
response: ({ query }: any) => {
|
||||||
|
const idx = approvalTodos.findIndex((t) => t.taskId === Number(query?.taskId));
|
||||||
|
if (idx >= 0) approvalTodos.splice(idx, 1);
|
||||||
|
return ok(null);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: "/api/notice/approval/Reject",
|
||||||
|
method: "post",
|
||||||
|
response: ({ query }: any) => {
|
||||||
|
const idx = approvalTodos.findIndex((t) => t.taskId === Number(query?.taskId));
|
||||||
|
if (idx >= 0) approvalTodos.splice(idx, 1);
|
||||||
|
return ok(null);
|
||||||
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,110 +0,0 @@
|
|||||||
/*
|
|
||||||
* 规范 iconfont 图标本地实现(bsch 应用页规范 §2 操作列 / 布局头尾图标)。
|
|
||||||
* 平台 base.layout.beta 主题不含 iconfont 字体文件,且新旧 BschHome 均已不再提供
|
|
||||||
* icon-browse 一类的字形,这里以 SVG mask + currentColor 实现同名类:
|
|
||||||
* 颜色跟随文字色,天然支持 .operating-btn:hover 圆形反色效果。
|
|
||||||
* 后续若接入平台真实 iconfont,可整文件移除。
|
|
||||||
*/
|
|
||||||
.iconfont {
|
|
||||||
font-family: inherit;
|
|
||||||
font-style: normal;
|
|
||||||
font-size: 16px;
|
|
||||||
line-height: 1;
|
|
||||||
display: inline-block;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
.iconfont::before {
|
|
||||||
content: "" !important;
|
|
||||||
display: block;
|
|
||||||
width: 1em;
|
|
||||||
height: 1em;
|
|
||||||
background-color: currentColor;
|
|
||||||
-webkit-mask-position: center;
|
|
||||||
mask-position: center;
|
|
||||||
-webkit-mask-repeat: no-repeat;
|
|
||||||
mask-repeat: no-repeat;
|
|
||||||
-webkit-mask-size: contain;
|
|
||||||
mask-size: contain;
|
|
||||||
}
|
|
||||||
.icon-browse::before {
|
|
||||||
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1024 1024'%3E%3Cpath fill-rule='evenodd' d='M512 256C299 256 121 361 32 512c89 151 267 256 480 256s391-105 480-256c-89-151-267-256-480-256zm0 416c-88 0-160-72-160-160s72-160 160-160 160 72 160 160-72 160-160 160z'/%3E%3Ccircle cx='512' cy='512' r='80'/%3E%3C/svg%3E");
|
|
||||||
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1024 1024'%3E%3Cpath fill-rule='evenodd' d='M512 256C299 256 121 361 32 512c89 151 267 256 480 256s391-105 480-256c-89-151-267-256-480-256zm0 416c-88 0-160-72-160-160s72-160 160-160 160 72 160 160-72 160-160 160z'/%3E%3Ccircle cx='512' cy='512' r='80'/%3E%3C/svg%3E");
|
|
||||||
}
|
|
||||||
.icon-edit::before {
|
|
||||||
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1024 1024'%3E%3Cpath d='M768 128c18 0 35 7 49 21l58 58c27 27 27 71 0 98L448 833l-200 54c-16 4-31-11-27-27l54-200L803 149c14-14 31-21 49-21zM128 896h768v48H128z'/%3E%3C/svg%3E");
|
|
||||||
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1024 1024'%3E%3Cpath d='M768 128c18 0 35 7 49 21l58 58c27 27 27 71 0 98L448 833l-200 54c-16 4-31-11-27-27l54-200L803 149c14-14 31-21 49-21zM128 896h768v48H128z'/%3E%3C/svg%3E");
|
|
||||||
}
|
|
||||||
.icon-ashbin::before {
|
|
||||||
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1024 1024'%3E%3Cpath fill-rule='evenodd' d='M416 96h192l32 64h160c18 0 32 14 32 32s-14 32-32 32H224c-18 0-32-14-32-32s14-32 32-32h160l32-64zM224 288h576l-36 520c-3 46-41 82-88 82H348c-47 0-85-36-88-82l-36-520zm176 64c-18 0-32 14-32 32v352c0 18 14 32 32 32s32-14 32-32V384c0-18-14-32-32-32zm224 0c-18 0-32 14-32 32v352c0 18 14 32 32 32s32-14 32-32V384c0-18-14-32-32-32z'/%3E%3C/svg%3E");
|
|
||||||
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1024 1024'%3E%3Cpath fill-rule='evenodd' d='M416 96h192l32 64h160c18 0 32 14 32 32s-14 32-32 32H224c-18 0-32-14-32-32s14-32 32-32h160l32-64zM224 288h576l-36 520c-3 46-41 82-88 82H348c-47 0-85-36-88-82l-36-520zm176 64c-18 0-32 14-32 32v352c0 18 14 32 32 32s32-14 32-32V384c0-18-14-32-32-32zm224 0c-18 0-32 14-32 32v352c0 18 14 32 32 32s32-14 32-32V384c0-18-14-32-32-32z'/%3E%3C/svg%3E");
|
|
||||||
}
|
|
||||||
.icon-user-filling::before {
|
|
||||||
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1024 1024'%3E%3Cpath d='M512 128a176 176 0 1 1 0 352 176 176 0 0 1 0-352zm0 416c190 0 320 104 320 224v32c0 26-21 48-48 48H240c-27 0-48-22-48-48v-32c0-120 130-224 320-224z'/%3E%3C/svg%3E");
|
|
||||||
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1024 1024'%3E%3Cpath d='M512 128a176 176 0 1 1 0 352 176 176 0 0 1 0-352zm0 416c190 0 320 104 320 224v32c0 26-21 48-48 48H240c-27 0-48-22-48-48v-32c0-120 130-224 320-224z'/%3E%3C/svg%3E");
|
|
||||||
}
|
|
||||||
.icon-attachment::before {
|
|
||||||
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1024 1024'%3E%3Cpath fill-rule='evenodd' d='M640 64H256c-35 0-64 29-64 64v768c0 35 29 64 64 64h512c35 0 64-29 64-64V256L640 64zm-16 112l144 144h-144V176zM320 448h384v48H320v-48zm0 128h384v48H320v-48zm0 128h384v48H320v-48z'/%3E%3C/svg%3E");
|
|
||||||
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1024 1024'%3E%3Cpath fill-rule='evenodd' d='M640 64H256c-35 0-64 29-64 64v768c0 35 29 64 64 64h512c35 0 64-29 64-64V256L640 64zm-16 112l144 144h-144V176zM320 448h384v48H320v-48zm0 128h384v48H320v-48zm0 128h384v48H320v-48z'/%3E%3C/svg%3E");
|
|
||||||
}
|
|
||||||
.icon-apps::before {
|
|
||||||
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1024 1024'%3E%3Cpath d='M224 128h224c18 0 32 14 32 32v224c0 18-14 32-32 32H224c-18 0-32-14-32-32V160c0-18 14-32 32-32zm352 0h224c18 0 32 14 32 32v224c0 18-14 32-32 32H576c-18 0-32-14-32-32V160c0-18 14-32 32-32zM224 480h224c18 0 32 14 32 32v224c0 18-14 32-32 32H224c-18 0-32-14-32-32V512c0-18 14-32 32-32zm352 0h224c18 0 32 14 32 32v224c0 18-14 32-32 32H576c-18 0-32-14-32-32V512c0-18 14-32 32-32z'/%3E%3C/svg%3E");
|
|
||||||
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1024 1024'%3E%3Cpath d='M224 128h224c18 0 32 14 32 32v224c0 18-14 32-32 32H224c-18 0-32-14-32-32V160c0-18 14-32 32-32zm352 0h224c18 0 32 14 32 32v224c0 18-14 32-32 32H576c-18 0-32-14-32-32V160c0-18 14-32 32-32zM224 480h224c18 0 32 14 32 32v224c0 18-14 32-32 32H224c-18 0-32-14-32-32V512c0-18 14-32 32-32zm352 0h224c18 0 32 14 32 32v224c0 18-14 32-32 32H576c-18 0-32-14-32-32V512c0-18 14-32 32-32z'/%3E%3C/svg%3E");
|
|
||||||
}
|
|
||||||
.icon-logout::before {
|
|
||||||
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1024 1024'%3E%3Cpath d='M544 96c18 0 32 14 32 32v96h-64v-64H256v704h256v-64h64v96c0 18-14 32-32 32H224c-18 0-32-14-32-32V128c0-18 14-32 32-32h320zm208 416L600 664l-45-45 75-75H448v-64h182l-75-75 45-45 152 152z'/%3E%3C/svg%3E");
|
|
||||||
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1024 1024'%3E%3Cpath d='M544 96c18 0 32 14 32 32v96h-64v-64H256v704h256v-64h64v96c0 18-14 32-32 32H224c-18 0-32-14-32-32V128c0-18 14-32 32-32h320zm208 416L600 664l-45-45 75-75H448v-64h182l-75-75 45-45 152 152z'/%3E%3C/svg%3E");
|
|
||||||
}
|
|
||||||
.icon-category::before {
|
|
||||||
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1024 1024'%3E%3Cpath d='M512 96l448 384h-112v352c0 18-14 32-32 32H608V608H416v256H208c-18 0-32-14-32-32V480H64l448-384z'/%3E%3C/svg%3E");
|
|
||||||
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1024 1024'%3E%3Cpath d='M512 96l448 384h-112v352c0 18-14 32-32 32H608V608H416v256H208c-18 0-32-14-32-32V480H64l448-384z'/%3E%3C/svg%3E");
|
|
||||||
}
|
|
||||||
/* 停用(圆圈斜杠),字形取自 bsch iconfont icon-stop(\e6e0) */
|
|
||||||
.icon-stop::before {
|
|
||||||
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1024 1024'%3E%3Cpath d='M512 74.666667C270.933333 74.666667 74.666667 270.933333 74.666667 512S270.933333 949.333333 512 949.333333 949.333333 753.066667 949.333333 512 753.066667 74.666667 512 74.666667zM885.333333 512c0 85.333333-29.866667 164.266667-78.933333 228.266667l-533.333333-514.133334c64-55.466667 149.333333-87.466667 238.933333-87.466666 204.8 0 373.333333 168.533333 373.333333 373.333333z m-746.666666 0c0-91.733333 34.133333-174.933333 87.466666-241.066667l535.466667 516.266667c-66.133333 59.733333-153.6 98.133333-251.733333 98.133333-202.666667 0-371.2-168.533333-371.2-373.333333z'/%3E%3C/svg%3E");
|
|
||||||
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1024 1024'%3E%3Cpath d='M512 74.666667C270.933333 74.666667 74.666667 270.933333 74.666667 512S270.933333 949.333333 512 949.333333 949.333333 753.066667 949.333333 512 753.066667 74.666667 512 74.666667zM885.333333 512c0 85.333333-29.866667 164.266667-78.933333 228.266667l-533.333333-514.133334c64-55.466667 149.333333-87.466667 238.933333-87.466666 204.8 0 373.333333 168.533333 373.333333 373.333333z m-746.666666 0c0-91.733333 34.133333-174.933333 87.466666-241.066667l535.466667 516.266667c-66.133333 59.733333-153.6 98.133333-251.733333 98.133333-202.666667 0-371.2-168.533333-371.2-373.333333z'/%3E%3C/svg%3E");
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ============ 通知模块图标(2026-08-31 与 oldfront 对齐,仅保留用到的) ============
|
|
||||||
* 附件类型字形(word/excel/ppt/pdf1/txt/tupian1/yasuo/weizhi)取自 oldfront/src/assets/js/iconfont/iconfont.js 同名 symbol,
|
|
||||||
* icon-saoba(一键已读)/icon-print(打印)平台原字体已失传,按同规格重绘。
|
|
||||||
*/
|
|
||||||
.icon-saoba::before {
|
|
||||||
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1024 1024'%3E%3Cpath d='M832 64L960 192 640 512 512 384 832 64zM512 384L640 512C560 704 448 848 256 960 192 992 128 976 96 928 64 880 80 816 128 768 256 608 384 480 512 384z'/%3E%3C/svg%3E");
|
|
||||||
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1024 1024'%3E%3Cpath d='M832 64L960 192 640 512 512 384 832 64zM512 384L640 512C560 704 448 848 256 960 192 992 128 976 96 928 64 880 80 816 128 768 256 608 384 480 512 384z'/%3E%3C/svg%3E");
|
|
||||||
}
|
|
||||||
.icon-print::before {
|
|
||||||
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1024 1024'%3E%3Cpath d='M352 64h320c18 0 32 14 32 32v160H320V96c0-18 14-32 32-32zM224 320h576c35 0 64 29 64 64v224c0 35-29 64-64 64h-96v-96c0-18-14-32-32-32H384c-18 0-32 14-32 32v96h-96c-35 0-64-29-64-64V384c0-35 29-64 64-64zM352 512h96v64c0 18 14 32 32 32h64c18 0 32-14 32-32v-64h96v352c0 18-14 32-32 32H352c-18 0-32-14-32-32V544c0-18 14-32 32-32z'/%3E%3C/svg%3E");
|
|
||||||
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1024 1024'%3E%3Cpath d='M352 64h320c18 0 32 14 32 32v160H320V96c0-18 14-32 32-32zM224 320h576c35 0 64 29 64 64v224c0 35-29 64-64 64h-96v-96c0-18-14-32-32-32H384c-18 0-32 14-32 32v96h-96c-35 0-64-29-64-64V384c0-35 29-64 64-64zM352 512h96v64c0 18 14 32 32 32h64c18 0 32-14 32-32v-64h96v352c0 18-14 32-32 32H352c-18 0-32-14-32-32V544c0-18 14-32 32-32z'/%3E%3C/svg%3E");
|
|
||||||
}
|
|
||||||
.icon-word::before {
|
|
||||||
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1024 1024'%3E%3Cpath d='M234.496 106.496h409.6l204.8 204.8v563.2c0 28.278784-22.921216 51.2-51.2 51.2H234.496c-28.278784 0-51.2-22.921216-51.2-51.2v-716.8c0-28.278784 22.921216-51.2 51.2-51.2z M644.096 106.496l204.8 204.8h-153.6c-28.278784 0-51.2-22.921216-51.2-51.2V106.496z M329.728 428.032l81.158144 282.95168h48.254976l54.833152-212.762624h2.195456l54.833152 212.762624h48.254976L700.416 428.032h-52.641792l-52.641792 214.953984h-2.195456L538.103808 428.032h-48.254976l-52.641792 214.953984h-2.195456L382.369792 428.032z'/%3E%3C/svg%3E");
|
|
||||||
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1024 1024'%3E%3Cpath d='M234.496 106.496h409.6l204.8 204.8v563.2c0 28.278784-22.921216 51.2-51.2 51.2H234.496c-28.278784 0-51.2-22.921216-51.2-51.2v-716.8c0-28.278784 22.921216-51.2 51.2-51.2z M644.096 106.496l204.8 204.8h-153.6c-28.278784 0-51.2-22.921216-51.2-51.2V106.496z M329.728 428.032l81.158144 282.95168h48.254976l54.833152-212.762624h2.195456l54.833152 212.762624h48.254976L700.416 428.032h-52.641792l-52.641792 214.953984h-2.195456L538.103808 428.032h-48.254976l-52.641792 214.953984h-2.195456L382.369792 428.032z'/%3E%3C/svg%3E");
|
|
||||||
}
|
|
||||||
.icon-excel::before {
|
|
||||||
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1024 1024'%3E%3Cpath d='M246.784 102.4h409.6l204.8 204.8v563.2c0 28.278784-22.921216 51.2-51.2 51.2H246.784c-28.278784 0-51.2-22.921216-51.2-51.2v-716.8c0-28.278784 22.921216-51.2 51.2-51.2z M438.9888 716.8l91.725824-131.657728L622.915584 716.8h43.708416l-114.704384-159.62112L658.366464 409.6h-43.708416l-83.943424 119.607296L447.1808 409.6H403.488768l105.590784 147.57888L395.264 716.8h43.7248z M656.384 102.4l204.8 204.8h-153.6c-28.278784 0-51.2-22.921216-51.2-51.2V102.4z'/%3E%3C/svg%3E");
|
|
||||||
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1024 1024'%3E%3Cpath d='M246.784 102.4h409.6l204.8 204.8v563.2c0 28.278784-22.921216 51.2-51.2 51.2H246.784c-28.278784 0-51.2-22.921216-51.2-51.2v-716.8c0-28.278784 22.921216-51.2 51.2-51.2z M438.9888 716.8l91.725824-131.657728L622.915584 716.8h43.708416l-114.704384-159.62112L658.366464 409.6h-43.708416l-83.943424 119.607296L447.1808 409.6H403.488768l105.590784 147.57888L395.264 716.8h43.7248z M656.384 102.4l204.8 204.8h-153.6c-28.278784 0-51.2-22.921216-51.2-51.2V102.4z'/%3E%3C/svg%3E");
|
|
||||||
}
|
|
||||||
.icon-ppt::before {
|
|
||||||
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1024 1024'%3E%3Cpath d='M234.496 106.496h409.6l204.8 204.8v563.2c0 28.278784-22.921216 51.2-51.2 51.2H234.496c-28.278784 0-51.2-22.921216-51.2-51.2v-716.8c0-28.278784 22.921216-51.2 51.2-51.2z M428.335104 745.472v-125.636608h89.501696c70.234112 0 105.7792-30.547968 105.7792-91.213824 0-60.235776-35.119104-90.353664-105.34912-90.353664H393.216v307.2l35.119104 0.004096z m87.793664-156.18048h-87.793664v-120.471552h87.793664c24.408064 0 42.3936 4.734976 54.382592 15.065088 11.993088 9.03168 17.985536 24.088576 17.985536 44.7488 0 20.647936-5.988352 35.704832-17.563648 45.604864-11.984896 9.89184-29.970432 15.0528-54.808576 15.0528h0.004096z M644.096 106.496l204.8 204.8h-153.6c-28.278784 0-51.2-22.921216-51.2-51.2V106.496z'/%3E%3C/svg%3E");
|
|
||||||
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1024 1024'%3E%3Cpath d='M234.496 106.496h409.6l204.8 204.8v563.2c0 28.278784-22.921216 51.2-51.2 51.2H234.496c-28.278784 0-51.2-22.921216-51.2-51.2v-716.8c0-28.278784 22.921216-51.2 51.2-51.2z M428.335104 745.472v-125.636608h89.501696c70.234112 0 105.7792-30.547968 105.7792-91.213824 0-60.235776-35.119104-90.353664-105.34912-90.353664H393.216v307.2l35.119104 0.004096z m87.793664-156.18048h-87.793664v-120.471552h87.793664c24.408064 0 42.3936 4.734976 54.382592 15.065088 11.993088 9.03168 17.985536 24.088576 17.985536 44.7488 0 20.647936-5.988352 35.704832-17.563648 45.604864-11.984896 9.89184-29.970432 15.0528-54.808576 15.0528h0.004096z M644.096 106.496l204.8 204.8h-153.6c-28.278784 0-51.2-22.921216-51.2-51.2V106.496z'/%3E%3C/svg%3E");
|
|
||||||
}
|
|
||||||
.icon-pdf1::before {
|
|
||||||
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1024 1024'%3E%3Cpath d='M230.4 122.88h409.6l204.8 204.8v563.2c0 28.278784-22.921216 51.2-51.2 51.2H230.4c-28.278784 0-51.2-22.921216-51.2-51.2v-716.8c0-28.278784 22.921216-51.2 51.2-51.2z M645.042176 674.586624c-27.643904-2.052096-54.255616-12.316672-75.75552-30.797824-41.971712 9.23648-81.903616 22.581248-121.831424 39.010304-31.735808 56.459264-61.423616 85.200896-87.01952 85.200896-5.12 0-11.259904-1.024-15.355904-4.108288-11.264-5.128192-17.403904-16.42496-17.403904-27.713536 0-9.240576 2.048-34.902016 99.303424-76.996608 22.528-41.0624 39.923712-83.1488 54.259712-127.287296-12.288-24.63744-38.899712-85.204992-20.48-115.99872 6.144-11.292672 18.432-17.453056 31.744-16.42496 10.235904 0 20.471808 5.132288 26.615808 13.340672 13.312 18.477056 12.288 57.48736-5.12 114.97472a309.497856 309.497856 0 0 0 63.471616 82.1248c21.504-4.112384 42.995712-7.18848 64.495616-7.18848 48.115712 1.024 55.283712 23.609344 54.259712 36.958208 0.008192 34.906112-33.779712 34.906112-51.183616 34.906112z m-286.646272 63.639552l3.072-1.024c14.331904-5.128192 25.587712-15.396864 33.779712-28.745728-15.36 6.160384-27.643904 16.42496-36.851712 29.773824v-0.004096z m136.155136-307.95776h-3.072c-1.024 0-3.072 0-4.096 1.024-4.096 17.44896-1.024 35.926016 6.144 52.350976 6.144-17.444864 6.144-35.926016 1.024-53.374976z m7.168 148.84864l-1.024 2.048-1.024-1.024c-9.216 23.61344-19.456 47.222784-30.715904 69.808128l2.048-1.024v2.048a594.771968 594.771968 0 0 1 69.615616-20.529152l-1.024-1.024h3.072c-15.355904-15.405056-29.687808-32.854016-40.947712-50.302976z m139.227136 54.407168c-9.216 0-17.403904 0-26.619904 2.048 10.24 5.136384 20.48 7.18848 30.715904 8.216576 7.168 1.024 14.336 0 20.475904-2.052096 0-3.084288-4.096-8.21248-24.571904-8.21248z M640 122.88l204.8 204.8h-153.6c-28.278784 0-51.2-22.921216-51.2-51.2V122.88z'/%3E%3C/svg%3E");
|
|
||||||
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1024 1024'%3E%3Cpath d='M230.4 122.88h409.6l204.8 204.8v563.2c0 28.278784-22.921216 51.2-51.2 51.2H230.4c-28.278784 0-51.2-22.921216-51.2-51.2v-716.8c0-28.278784 22.921216-51.2 51.2-51.2z M645.042176 674.586624c-27.643904-2.052096-54.255616-12.316672-75.75552-30.797824-41.971712 9.23648-81.903616 22.581248-121.831424 39.010304-31.735808 56.459264-61.423616 85.200896-87.01952 85.200896-5.12 0-11.259904-1.024-15.355904-4.108288-11.264-5.128192-17.403904-16.42496-17.403904-27.713536 0-9.240576 2.048-34.902016 99.303424-76.996608 22.528-41.0624 39.923712-83.1488 54.259712-127.287296-12.288-24.63744-38.899712-85.204992-20.48-115.99872 6.144-11.292672 18.432-17.453056 31.744-16.42496 10.235904 0 20.471808 5.132288 26.615808 13.340672 13.312 18.477056 12.288 57.48736-5.12 114.97472a309.497856 309.497856 0 0 0 63.471616 82.1248c21.504-4.112384 42.995712-7.18848 64.495616-7.18848 48.115712 1.024 55.283712 23.609344 54.259712 36.958208 0.008192 34.906112-33.779712 34.906112-51.183616 34.906112z m-286.646272 63.639552l3.072-1.024c14.331904-5.128192 25.587712-15.396864 33.779712-28.745728-15.36 6.160384-27.643904 16.42496-36.851712 29.773824v-0.004096z m136.155136-307.95776h-3.072c-1.024 0-3.072 0-4.096 1.024-4.096 17.44896-1.024 35.926016 6.144 52.350976 6.144-17.444864 6.144-35.926016 1.024-53.374976z m7.168 148.84864l-1.024 2.048-1.024-1.024c-9.216 23.61344-19.456 47.222784-30.715904 69.808128l2.048-1.024v2.048a594.771968 594.771968 0 0 1 69.615616-20.529152l-1.024-1.024h3.072c-15.355904-15.405056-29.687808-32.854016-40.947712-50.302976z m139.227136 54.407168c-9.216 0-17.403904 0-26.619904 2.048 10.24 5.136384 20.48 7.18848 30.715904 8.216576 7.168 1.024 14.336 0 20.475904-2.052096 0-3.084288-4.096-8.21248-24.571904-8.21248z M640 122.88l204.8 204.8h-153.6c-28.278784 0-51.2-22.921216-51.2-51.2V122.88z'/%3E%3C/svg%3E");
|
|
||||||
}
|
|
||||||
.icon-txt::before {
|
|
||||||
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1024 1024'%3E%3Cpath d='M238.592 126.976h409.6l204.8 204.8v563.2c0 28.278784-22.921216 51.2-51.2 51.2H238.592c-28.278784 0-51.2-22.921216-51.2-51.2v-716.8c0-28.278784 22.921216-51.2 51.2-51.2z M540.11904 741.376v-276.652032h108.07296V434.176H397.312v30.547968h108.07296V741.376h34.73408z M648.192 126.976l204.8 204.8h-153.6c-28.278784 0-51.2-22.921216-51.2-51.2V126.976z'/%3E%3C/svg%3E");
|
|
||||||
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1024 1024'%3E%3Cpath d='M238.592 126.976h409.6l204.8 204.8v563.2c0 28.278784-22.921216 51.2-51.2 51.2H238.592c-28.278784 0-51.2-22.921216-51.2-51.2v-716.8c0-28.278784 22.921216-51.2 51.2-51.2z M540.11904 741.376v-276.652032h108.07296V434.176H397.312v30.547968h108.07296V741.376h34.73408z M648.192 126.976l204.8 204.8h-153.6c-28.278784 0-51.2-22.921216-51.2-51.2V126.976z'/%3E%3C/svg%3E");
|
|
||||||
}
|
|
||||||
.icon-tupian1::before {
|
|
||||||
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1024 1024'%3E%3Cpath d='M250.88 122.88h409.6l204.8 204.8v563.2c0 28.278784-22.921216 51.2-51.2 51.2H250.88c-28.278784 0-51.2-22.921216-51.2-51.2v-716.8c0-28.278784 22.921216-51.2 51.2-51.2z M329.703424 716.53376l74.645504-109.71136a11.603968 11.603968 0 0 1 17.73568-1.748992l49.332224 48.3328 108.2368-169.652224a11.599872 11.599872 0 0 1 19.84512 0.446464l134.483968 233.0624a11.599872 11.599872 0 0 1-10.05568 17.412096H339.304448a11.599872 11.599872 0 0 1-9.60512-18.14528l0.004096 0.004096z M352.055296 468.115456c-0.004096 32.31744 26.189824 58.519552 58.507264 58.523648 32.31744 0.004096 58.519552-26.189824 58.523648-58.507264v-0.016384c0.004096-32.31744-26.189824-58.519552-58.507264-58.523648s-58.519552 26.189824-58.523648 58.507264v0.016384z M660.48 122.88l204.8 204.8h-153.6c-28.278784 0-51.2-22.921216-51.2-51.2V122.88z'/%3E%3C/svg%3E");
|
|
||||||
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1024 1024'%3E%3Cpath d='M250.88 122.88h409.6l204.8 204.8v563.2c0 28.278784-22.921216 51.2-51.2 51.2H250.88c-28.278784 0-51.2-22.921216-51.2-51.2v-716.8c0-28.278784 22.921216-51.2 51.2-51.2z M329.703424 716.53376l74.645504-109.71136a11.603968 11.603968 0 0 1 17.73568-1.748992l49.332224 48.3328 108.2368-169.652224a11.599872 11.599872 0 0 1 19.84512 0.446464l134.483968 233.0624a11.599872 11.599872 0 0 1-10.05568 17.412096H339.304448a11.599872 11.599872 0 0 1-9.60512-18.14528l0.004096 0.004096z M352.055296 468.115456c-0.004096 32.31744 26.189824 58.519552 58.507264 58.523648 32.31744 0.004096 58.519552-26.189824 58.523648-58.507264v-0.016384c0.004096-32.31744-26.189824-58.519552-58.507264-58.523648s-58.519552 26.189824-58.523648 58.507264v0.016384z M660.48 122.88l204.8 204.8h-153.6c-28.278784 0-51.2-22.921216-51.2-51.2V122.88z'/%3E%3C/svg%3E");
|
|
||||||
}
|
|
||||||
.icon-yasuo::before {
|
|
||||||
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1024 1024'%3E%3Cpath d='M250.88 126.976h409.6l204.8 204.8v563.2c0 28.278784-22.921216 51.2-51.2 51.2H250.88c-28.278784 0-51.2-22.921216-51.2-51.2v-716.8c0-28.278784 22.921216-51.2 51.2-51.2z M588.8 630.378496v111.24736a27.783168 27.783168 0 0 1-8.167424 19.664896 27.89376 27.89376 0 0 1-19.70176 8.146944h-56.905728a27.89376 27.89376 0 0 1-19.70176-8.142848 27.795456 27.795456 0 0 1-8.167424-19.677184V630.3744h112.644096z m-27.869184 63.73376h-56.905728v55.857152h56.905728v-55.857152z m26.939392-174.98112v55.857152h-55.967744v-55.857152h55.967744zM532.598784 126.976v56.32h56.201216V239.616h-56.209408v52.146176h56.209408v56.32h-56.201216v55.627776h56.201216v55.627776h-56.201216v55.62368H476.16v-55.627776h56.201216V403.705856H476.16v-55.62368h56.201216v-56.32H476.16v-56.32h56.201216V183.296H476.16V126.976h56.438784z M660.48 126.976l204.8 204.8h-153.6c-28.278784 0-51.2-22.921216-51.2-51.2V126.976z'/%3E%3C/svg%3E");
|
|
||||||
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1024 1024'%3E%3Cpath d='M250.88 126.976h409.6l204.8 204.8v563.2c0 28.278784-22.921216 51.2-51.2 51.2H250.88c-28.278784 0-51.2-22.921216-51.2-51.2v-716.8c0-28.278784 22.921216-51.2 51.2-51.2z M588.8 630.378496v111.24736a27.783168 27.783168 0 0 1-8.167424 19.664896 27.89376 27.89376 0 0 1-19.70176 8.146944h-56.905728a27.89376 27.89376 0 0 1-19.70176-8.142848 27.795456 27.795456 0 0 1-8.167424-19.677184V630.3744h112.644096z m-27.869184 63.73376h-56.905728v55.857152h56.905728v-55.857152z m26.939392-174.98112v55.857152h-55.967744v-55.857152h55.967744zM532.598784 126.976v56.32h56.201216V239.616h-56.209408v52.146176h56.209408v56.32h-56.201216v55.627776h56.201216v55.627776h-56.201216v55.62368H476.16v-55.627776h56.201216V403.705856H476.16v-55.62368h56.201216v-56.32H476.16v-56.32h56.201216V183.296H476.16V126.976h56.438784z M660.48 126.976l204.8 204.8h-153.6c-28.278784 0-51.2-22.921216-51.2-51.2V126.976z'/%3E%3C/svg%3E");
|
|
||||||
}
|
|
||||||
.icon-weizhi::before {
|
|
||||||
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1024 1024'%3E%3Cpath d='M242.688 102.4h409.6l204.8 204.8v563.2c0 28.278784-22.921216 51.2-51.2 51.2H242.688c-28.278784 0-51.2-22.921216-51.2-51.2v-716.8c0-28.278784 22.921216-51.2 51.2-51.2z M652.288 102.4l204.8 204.8h-153.6c-28.278784 0-51.2-22.921216-51.2-51.2V102.4z M421.888 501.76c0-20.48 4.096-36.864 8.192-47.104s12.288-20.48 22.528-28.672c10.24-10.24 20.48-16.384 34.816-20.48 12.288-4.096 24.576-6.144 38.912-6.144 28.672 0 51.2 8.192 71.68 24.576 18.432 16.384 28.672 36.864 28.672 63.488 0 12.288-4.096 22.528-8.192 32.768s-14.336 22.528-30.72 36.864-22.528 24.576-28.672 30.72-8.192 14.336-12.288 22.528-2.048 14.336-2.048 34.816h-40.96c0-20.48 2.048-30.72 4.096-40.96s6.144-18.432 12.288-26.624 12.288-18.432 26.624-32.768 24.576-24.576 28.672-30.72 8.192-16.384 8.192-30.72-6.144-24.576-16.384-34.816-24.576-16.384-43.008-16.384c-40.96 0-61.44 28.672-61.44 69.632h-40.96z m122.88 225.28h-40.96v-40.96h40.96v40.96z'/%3E%3C/svg%3E");
|
|
||||||
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1024 1024'%3E%3Cpath d='M242.688 102.4h409.6l204.8 204.8v563.2c0 28.278784-22.921216 51.2-51.2 51.2H242.688c-28.278784 0-51.2-22.921216-51.2-51.2v-716.8c0-28.278784 22.921216-51.2 51.2-51.2z M652.288 102.4l204.8 204.8h-153.6c-28.278784 0-51.2-22.921216-51.2-51.2V102.4z M421.888 501.76c0-20.48 4.096-36.864 8.192-47.104s12.288-20.48 22.528-28.672c10.24-10.24 20.48-16.384 34.816-20.48 12.288-4.096 24.576-6.144 38.912-6.144 28.672 0 51.2 8.192 71.68 24.576 18.432 16.384 28.672 36.864 28.672 63.488 0 12.288-4.096 22.528-8.192 32.768s-14.336 22.528-30.72 36.864-22.528 24.576-28.672 30.72-8.192 14.336-12.288 22.528-2.048 14.336-2.048 34.816h-40.96c0-20.48 2.048-30.72 4.096-40.96s6.144-18.432 12.288-26.624 12.288-18.432 26.624-32.768 24.576-24.576 28.672-30.72 8.192-16.384 8.192-30.72-6.144-24.576-16.384-34.816-24.576-16.384-43.008-16.384c-40.96 0-61.44 28.672-61.44 69.632h-40.96z m122.88 225.28h-40.96v-40.96h40.96v40.96z'/%3E%3C/svg%3E");
|
|
||||||
}
|
|
||||||
File diff suppressed because one or more lines are too long
@@ -27,7 +27,7 @@
|
|||||||
content: "";
|
content: "";
|
||||||
display: block;
|
display: block;
|
||||||
width: 3px;
|
width: 3px;
|
||||||
background: #5979ea;
|
background: var(--brand, #577afb);
|
||||||
height: 15px;
|
height: 15px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
h4.second-title {
|
h4.second-title {
|
||||||
@extend .base-subtitle;
|
@extend .base-subtitle;
|
||||||
&::before {
|
&::before {
|
||||||
background: #5979ea;
|
background: var(--brand, #577afb);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,7 +62,7 @@ h4.subtitle {
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
li {
|
li {
|
||||||
height: 42px;
|
height: 42px;
|
||||||
border: 1px solid #ebeef5;
|
border: 1px solid var(--line, #e9edf5);
|
||||||
border-top: none;
|
border-top: none;
|
||||||
position: relative;
|
position: relative;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -87,8 +87,8 @@ h4.subtitle {
|
|||||||
}
|
}
|
||||||
li.active {
|
li.active {
|
||||||
// background: #f3f3f3;
|
// background: #f3f3f3;
|
||||||
color: #7892ed;
|
color: var(--brand-2, #7b8ffd);
|
||||||
border-right: 3px solid #5979ea;
|
border-right: 3px solid var(--brand, #577afb);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -107,7 +107,7 @@ h4.subtitle {
|
|||||||
background-position: top center;
|
background-position: top center;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
border: none;
|
border: none;
|
||||||
color: #577afb;
|
color: var(--brand, #577afb);
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,3 +115,16 @@ h4.subtitle {
|
|||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
margin-bottom: 2px;
|
margin-bottom: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 修复表格幻影省略号(2026-10-10):.ivu-table-cell 基础样式自带 text-overflow:ellipsis,
|
||||||
|
tooltip 列(.ivu-tooltip 行内块包装 + width:100%)与操作列(图标外包 Tooltip)未溢出也常显「...」。
|
||||||
|
恢复组件预期的块级/flex 渲染层级即可;内层 .ivu-table-cell-tooltip-content 的真省略号与
|
||||||
|
悬浮 tooltip 不受影响(已在 dev:mock 实测:短内容无省略号、长标题正常截断+悬浮提示)。 */
|
||||||
|
/* 两条同为 3 类特异性(0,3,0):后者居后胜出 → tooltip 列(.ivu-table-cell-tooltip)最终 flex
|
||||||
|
(组件预期布局),操作列裸 .ivu-tooltip 包装取 block。 */
|
||||||
|
.ivu-table .ivu-table-cell > .ivu-tooltip {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.ivu-table .ivu-table-cell > .ivu-table-cell-tooltip {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
color: $color-warning !important;
|
color: $color-warning !important;
|
||||||
}
|
}
|
||||||
.text-primary {
|
.text-primary {
|
||||||
color: $color-primary !important;
|
color: var(--brand, #577afb) !important;
|
||||||
}
|
}
|
||||||
.text-normal {
|
.text-normal {
|
||||||
color: #404042 !important;
|
color: #404042 !important;
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
/*scss变量*/
|
/*scss变量*/
|
||||||
|
|
||||||
// 主题色
|
// 主题色(品牌主色不在此定义:由平台主题令牌 var(--brand) 经中央静态通道下发,
|
||||||
|
// 本文件只留业务语义色——成功/警告/错误不随主题档位变化)
|
||||||
$color-error: #e8684a;
|
$color-error: #e8684a;
|
||||||
$color-success: #45b972;
|
$color-success: #45b972;
|
||||||
$color-warning: #e9b954;
|
$color-warning: #e9b954;
|
||||||
$color-primary: #5979ea;
|
|
||||||
|
|
||||||
// 文字色
|
// 文字色
|
||||||
$color-black: #404042;
|
$color-black: #404042;
|
||||||
|
|||||||
@@ -1,22 +1,24 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="notice-upload">
|
<div class="notice-upload">
|
||||||
<!-- 自定义上传附件(自旧前端 oldfront/src/components/uploadFile.vue 适配) -->
|
<!-- 自定义上传附件(自旧前端 oldfront/src/components/uploadFile.vue 适配) -->
|
||||||
<Upload
|
<div class="upload-row">
|
||||||
:on-success="encloSuccess"
|
<Upload
|
||||||
:on-remove="encloRemove"
|
:on-success="encloSuccess"
|
||||||
:default-file-list="defaultList"
|
:on-remove="encloRemove"
|
||||||
:action="uploadFileUrl"
|
:default-file-list="defaultList"
|
||||||
:accept="fileType"
|
:action="uploadFileUrl"
|
||||||
:multiple="isMultiple"
|
:accept="fileType"
|
||||||
:before-upload="handleUpload"
|
:multiple="isMultiple"
|
||||||
:format="formatArr"
|
:before-upload="handleUpload"
|
||||||
:max-size="maxSize"
|
:format="formatArr"
|
||||||
:on-format-error="handleFormatError"
|
:max-size="maxSize"
|
||||||
:on-exceeded-size="handleExceeded"
|
:on-format-error="handleFormatError"
|
||||||
>
|
:on-exceeded-size="handleExceeded"
|
||||||
<Button><Icon type="ios-cloud-upload-outline"></Icon>选择文件</Button>
|
>
|
||||||
</Upload>
|
<Button><Icon type="ios-cloud-upload-outline"></Icon>选择文件</Button>
|
||||||
<span class="upload-tip">提示:附件大小不超过{{ maxFileSize }}</span>
|
</Upload>
|
||||||
|
<span class="upload-tip">提示:附件大小不超过{{ maxFileSize }}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -87,17 +89,30 @@ export default {
|
|||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.notice-upload {
|
.notice-upload {
|
||||||
display: flex;
|
display: block;
|
||||||
align-items: center;
|
.upload-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start; /* 提示语与按钮首行对齐(列表换行后不再垂直居中整块) */
|
||||||
|
}
|
||||||
:deep(.ivu-upload) {
|
:deep(.ivu-upload) {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
flex-wrap: wrap; /* 允许内部列表换行 */
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
/* 文件列表整体另起一行,每项独占一行,不再与按钮挤在一起 */
|
||||||
|
:deep(.ivu-upload-list) {
|
||||||
|
display: block;
|
||||||
|
width: 100%; /* 文件列表整体独占下一行 */
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
:deep(.ivu-upload-list-file) {
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
.upload-tip {
|
.upload-tip {
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
color: #f90;
|
color: #f90;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 1;
|
line-height: 32px; /* 与默认 Button 等高,紧贴按钮右侧 */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<Table :columns="columns" :data="list" :height="tableH" :loading="loading" no-data-text="暂无数据"
|
<div class="table-region" ref="regionRef">
|
||||||
class="link-table table-bottom-box" @on-selection-change="selectChange">
|
<Table :columns="columns" :data="list" :height="tableH" :loading="loading" no-data-text="暂无数据"
|
||||||
<template #loading>
|
class="link-table table-bottom-box" @on-selection-change="selectChange">
|
||||||
<div class="table-spin-load">加载中</div>
|
<template #loading>
|
||||||
</template>
|
<div class="table-spin-load">加载中</div>
|
||||||
<template v-for="slotName in tableSlotNames" :key="slotName" v-slot:[slotName]="slotProps">
|
</template>
|
||||||
<slot :name="slotName" v-bind="slotProps" />
|
<template v-for="slotName in tableSlotNames" :key="slotName" v-slot:[slotName]="slotProps">
|
||||||
</template>
|
<slot :name="slotName" v-bind="slotProps" />
|
||||||
</Table>
|
</template>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="page-panel">
|
<div class="page-panel">
|
||||||
<div class="fl ml-15 align-middle">
|
<div class="fl ml-15 align-middle">
|
||||||
@@ -22,7 +24,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref, useSlots } from 'vue'
|
import { computed, ref, useSlots } from 'vue'
|
||||||
import Utils from '@/utils'
|
import { useElementHeight } from '@/utils/hook/useElementHeight'
|
||||||
import { OperatorEnum } from '@/types'
|
import { OperatorEnum } from '@/types'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -44,7 +46,10 @@ const emit = defineEmits<{
|
|||||||
}>()
|
}>()
|
||||||
const slots = useSlots()
|
const slots = useSlots()
|
||||||
|
|
||||||
const tableH = ref(props.tableHeight ?? Utils.useTableHeight())
|
const regionRef = ref<HTMLElement | null>(null)
|
||||||
|
const measuredH = useElementHeight(regionRef)
|
||||||
|
// 显式传 tableHeight 则尊重之;否则实测 .table-region(flex:1)高度喂固定表头,首帧未测出前用 undefined(auto)
|
||||||
|
const tableH = computed(() => props.tableHeight ?? (measuredH.value || undefined))
|
||||||
const tableSlotNames = computed(() => Object.keys(slots).filter(name => !['default', 'loading'].includes(name)))
|
const tableSlotNames = computed(() => Object.keys(slots).filter(name => !['default', 'loading'].includes(name)))
|
||||||
|
|
||||||
const selectList = ref<number[]>([])
|
const selectList = ref<number[]>([])
|
||||||
|
|||||||
@@ -0,0 +1,274 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/* 管理端配色切换器(yh-theme 定稿 07-seam-pure 顶栏「配色」胶囊,方案 §6.5)。
|
||||||
|
* 状态唯一来源:utils/theme(localStorage 持久化 + <html> 档位类名),与门户皮肤包正交。 */
|
||||||
|
import { onBeforeUnmount, onMounted, ref } from "vue";
|
||||||
|
import {
|
||||||
|
loadThemeState,
|
||||||
|
saveThemeState,
|
||||||
|
applyThemeState,
|
||||||
|
type ThemeName,
|
||||||
|
type ThemeState,
|
||||||
|
} from "@/utils/theme";
|
||||||
|
|
||||||
|
const THEME_META: { name: ThemeName; label: string; color: string }[] = [
|
||||||
|
{ name: "blue", label: "原版靛蓝", color: "#577afb" },
|
||||||
|
{ name: "ocean", label: "云海蓝", color: "#2f7ce0" },
|
||||||
|
{ name: "ink", label: "石墨墨", color: "#3d4d63" },
|
||||||
|
{ name: "violet", label: "靛紫", color: "#6d5ae0" },
|
||||||
|
{ name: "teal", label: "松石绿", color: "#0d9488" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const wrapEl = ref<HTMLElement | null>(null);
|
||||||
|
const open = ref(false);
|
||||||
|
const state = ref<ThemeState>(loadThemeState());
|
||||||
|
/* 挂载即把存储态落到 <html>(与 main.ts initTheme 同源幂等;组件晚于引导挂载时也自洽) */
|
||||||
|
applyThemeState(state.value);
|
||||||
|
|
||||||
|
function commit(patch: Partial<ThemeState>) {
|
||||||
|
state.value = { ...state.value, ...patch };
|
||||||
|
applyThemeState(state.value);
|
||||||
|
saveThemeState(state.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle(ev: MouseEvent) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
open.value = !open.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 切换器外任意点击即收起(弹层内部点击由 wrap 判定放过) */
|
||||||
|
function onDocClick(ev: MouseEvent) {
|
||||||
|
if (open.value && wrapEl.value && !wrapEl.value.contains(ev.target as Node)) {
|
||||||
|
open.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => document.addEventListener("click", onDocClick, true));
|
||||||
|
onBeforeUnmount(() => document.removeEventListener("click", onDocClick, true));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div ref="wrapEl" class="theme-wrap">
|
||||||
|
<button type="button" class="theme-btn" :class="{ on: open }" @click="toggle">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M12 3a9 9 0 1 0 0 18c1.2 0 1.9-.9 1.9-1.8 0-.6-.3-1-.5-1.4-.2-.4-.4-.8-.4-1.3 0-.9.8-1.7 1.8-1.7H17a4 4 0 0 0 4-4c0-4.3-4-7.8-9-7.8z"/>
|
||||||
|
<circle cx="7.6" cy="11.2" r="1.1"/><circle cx="11" cy="7.6" r="1.1"/><circle cx="15.4" cy="8.6" r="1.1"/>
|
||||||
|
</svg>
|
||||||
|
<span>配色</span>
|
||||||
|
<svg class="caret" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M5.5 9.5l6.5 6.5 6.5-6.5"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="theme-pop" :class="{ open }">
|
||||||
|
<p class="tp-head">主题色</p>
|
||||||
|
<div class="tp-grid">
|
||||||
|
<button
|
||||||
|
v-for="t in THEME_META"
|
||||||
|
:key="t.name"
|
||||||
|
type="button"
|
||||||
|
class="theme-opt"
|
||||||
|
:class="{ active: state.theme === t.name }"
|
||||||
|
@click="commit({ theme: t.name })"
|
||||||
|
>
|
||||||
|
<i :style="{ background: t.color }"></i>{{ t.label }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="tp-sep"></div>
|
||||||
|
<p class="tp-head">侧栏样式</p>
|
||||||
|
<label class="tp-row">
|
||||||
|
<span>侧栏跟随顶栏色</span>
|
||||||
|
<span class="sw">
|
||||||
|
<input type="checkbox" :checked="state.sideFollow" @change="commit({ sideFollow: ($event.target as HTMLInputElement).checked })" />
|
||||||
|
<i></i>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<label class="tp-row" style="margin-top: 9px">
|
||||||
|
<span>侧栏深色(关=浅色)</span>
|
||||||
|
<span class="sw">
|
||||||
|
<input type="checkbox" :checked="state.sideDeep" @change="commit({ sideDeep: ($event.target as HTMLInputElement).checked })" />
|
||||||
|
<i></i>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<div class="tp-sep"></div>
|
||||||
|
<label class="tp-row">
|
||||||
|
<span>顶部过渡色(渐变)</span>
|
||||||
|
<span class="sw">
|
||||||
|
<input type="checkbox" :checked="state.gradOn" @change="commit({ gradOn: ($event.target as HTMLInputElement).checked })" />
|
||||||
|
<i></i>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* 皮肤规格照搬定稿 07-seam-pure.html .theme-wrap/.theme-btn/.theme-pop 段 */
|
||||||
|
.theme-wrap {
|
||||||
|
position: relative;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.theme-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
height: 30px;
|
||||||
|
padding: 0 11px;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.34);
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(255, 255, 255, 0.14);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 12.5px;
|
||||||
|
transition: background 0.16s, border-color 0.16s;
|
||||||
|
}
|
||||||
|
.theme-btn:hover,
|
||||||
|
.theme-btn.on {
|
||||||
|
background: rgba(255, 255, 255, 0.26);
|
||||||
|
border-color: rgba(255, 255, 255, 0.6);
|
||||||
|
}
|
||||||
|
.theme-btn svg {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
display: block;
|
||||||
|
fill: none;
|
||||||
|
stroke: currentColor;
|
||||||
|
stroke-width: 1.7;
|
||||||
|
stroke-linecap: round;
|
||||||
|
stroke-linejoin: round;
|
||||||
|
}
|
||||||
|
.theme-btn svg circle {
|
||||||
|
fill: currentColor;
|
||||||
|
stroke: none;
|
||||||
|
}
|
||||||
|
.theme-btn .caret {
|
||||||
|
width: 11px;
|
||||||
|
height: 11px;
|
||||||
|
opacity: 0.85;
|
||||||
|
transition: transform 0.18s;
|
||||||
|
}
|
||||||
|
.theme-btn.on .caret {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
.theme-pop {
|
||||||
|
position: absolute;
|
||||||
|
top: calc(100% + 6px);
|
||||||
|
right: 0;
|
||||||
|
width: 224px;
|
||||||
|
background: #fff;
|
||||||
|
color: var(--text, #1e2635);
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid var(--line, #e9edf5);
|
||||||
|
padding: 12px;
|
||||||
|
box-shadow: 0 12px 34px -10px rgba(16, 24, 40, 0.28), 0 2px 6px rgba(16, 24, 40, 0.08);
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
transform: translateY(-6px);
|
||||||
|
transition: 0.18s;
|
||||||
|
z-index: 40;
|
||||||
|
}
|
||||||
|
.theme-pop.open {
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
.tp-head {
|
||||||
|
font-size: 11.5px;
|
||||||
|
color: var(--text-3, #98a1b2);
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
margin: 0 2px 8px;
|
||||||
|
}
|
||||||
|
.tp-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.theme-opt {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 7px;
|
||||||
|
height: 32px;
|
||||||
|
padding: 0 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: left;
|
||||||
|
border: 1px solid var(--line, #e9edf5);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fff;
|
||||||
|
color: var(--text-2, #5a6478);
|
||||||
|
font-size: 12px;
|
||||||
|
transition: 0.15s;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
.theme-opt:hover {
|
||||||
|
border-color: #cfd7e6;
|
||||||
|
background: #fafbfe;
|
||||||
|
}
|
||||||
|
.theme-opt i {
|
||||||
|
width: 13px;
|
||||||
|
height: 13px;
|
||||||
|
border-radius: 50%;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
.theme-opt.active {
|
||||||
|
border-color: var(--brand, #577afb);
|
||||||
|
color: var(--brand, #577afb);
|
||||||
|
background: var(--brand-soft, #eef2ff);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.tp-sep {
|
||||||
|
height: 1px;
|
||||||
|
background: var(--line, #e9edf5);
|
||||||
|
margin: 12px 0 10px;
|
||||||
|
}
|
||||||
|
.tp-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 12.5px;
|
||||||
|
color: var(--text-2, #5a6478);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0 2px;
|
||||||
|
}
|
||||||
|
.sw {
|
||||||
|
position: relative;
|
||||||
|
width: 38px;
|
||||||
|
height: 21px;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
.sw input {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
opacity: 0;
|
||||||
|
margin: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
.sw i {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #d7dce6;
|
||||||
|
transition: background 0.2s;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.sw i::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 2.5px;
|
||||||
|
left: 2.5px;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 1px 3px rgba(16, 24, 40, 0.28);
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
.sw input:checked + i {
|
||||||
|
background: var(--brand, #577afb);
|
||||||
|
}
|
||||||
|
.sw input:checked + i::after {
|
||||||
|
transform: translateX(17px);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
/* 默认头像三态组件(YschTheme 公共源,scripts/sync-apps.mjs 下发三端 src/components/UserAvatar.vue):
|
||||||
|
头像 URL 非空 → <img>;否则 style==="text" 且能取到字 → 首字;否则 → 剪影(纯 CSS/SVG,零资源文件)。
|
||||||
|
取值链/取字契约见《设置页修复与默认头像落地方案》§10.1/§10.2;样式全在本文件 <style scoped>(唯一 CSS 源,
|
||||||
|
不进通道 icons.src.css——通道 CSS 对 UserHome 不可达,内联分发才能消双份漂移,见同方案 §10.4)。
|
||||||
|
根元素即 .yh-avatar 圆:尺寸内联必须挂圆本身(挂外层 span 会被 flex 拉成椭圆,§12 踩坑)。 */
|
||||||
|
import { computed, onBeforeUnmount, onMounted, ref } from "vue";
|
||||||
|
import { pickAvatarText, resolveAvatarStyle } from "@/utils/avatar";
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
/** 头像 URL:非空直接 <img>(三态第一优先) */
|
||||||
|
src?: string | null;
|
||||||
|
/** 取字来源(首字态用):经 pickAvatarText 判定,取不到字回落剪影 */
|
||||||
|
name?: string | null;
|
||||||
|
/** 直径 px;font-size = round(size * 0.44) */
|
||||||
|
size?: number;
|
||||||
|
/** 配色场景:light=页面浅底(品牌实底+白内容);brand=header 品牌色底(剪影 S1 玻璃底 / 首字 S2 白底反色,§10.3 分档) */
|
||||||
|
placement?: "light" | "brand";
|
||||||
|
}>(),
|
||||||
|
{ src: null, name: null, size: 28, placement: "light" },
|
||||||
|
);
|
||||||
|
|
||||||
|
const text = computed(() => (props.src ? "" : pickAvatarText(props.name)));
|
||||||
|
/* 外观偏好异步补齐(yh-theme-init 在挂载后拉服务端真源收敛 localStorage 键)会在
|
||||||
|
document 上派发 yh-appearance-updated;tick 自增让 style 依赖失效重读键,三态即时切换(无需刷新)。 */
|
||||||
|
const appearanceTick = ref(0);
|
||||||
|
const onAppearanceUpdated = () => { appearanceTick.value += 1; };
|
||||||
|
onMounted(() => document.addEventListener("yh-appearance-updated", onAppearanceUpdated));
|
||||||
|
onBeforeUnmount(() => document.removeEventListener("yh-appearance-updated", onAppearanceUpdated));
|
||||||
|
const style = computed(() => {
|
||||||
|
appearanceTick.value;
|
||||||
|
return resolveAvatarStyle();
|
||||||
|
});
|
||||||
|
/** 首字态 = style==="text" 且能取到字(契约 §10.2:二者缺一即剪影,取到字但档位是剪影时绝不能出字) */
|
||||||
|
const showText = computed(() => style.value === "text" && !!text.value);
|
||||||
|
|
||||||
|
const rootClass = computed(() => [
|
||||||
|
"yh-avatar",
|
||||||
|
props.placement === "brand" ? "yh-avatar--brand" : "yh-avatar--light",
|
||||||
|
{ "is-text": showText.value },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const rootStyle = computed(() => ({
|
||||||
|
width: `${props.size}px`,
|
||||||
|
height: `${props.size}px`,
|
||||||
|
fontSize: `${Math.round(props.size * 0.44)}px`,
|
||||||
|
}));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<span :class="rootClass" :style="rootStyle">
|
||||||
|
<img v-if="src" class="yh-avatar__img" :src="src" alt="" />
|
||||||
|
<template v-else-if="showText">{{ text }}</template>
|
||||||
|
<svg v-else class="yh-avatar__sil" viewBox="0 0 1024 1024" aria-hidden="true">
|
||||||
|
<circle cx="512" cy="399" r="174" />
|
||||||
|
<ellipse cx="512" cy="946" rx="420" ry="240" />
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.yh-avatar {
|
||||||
|
position: relative;
|
||||||
|
border-radius: 50%;
|
||||||
|
overflow: hidden;
|
||||||
|
display: inline-grid;
|
||||||
|
place-items: center;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
.yh-avatar__img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
/* 剪影 SVG:容器圆 overflow:hidden 裁切肩部溢出(禁用 clipPath);overflow:visible 放行 viewBox 外形状 */
|
||||||
|
.yh-avatar__sil {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: visible;
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
/* 页面内(浅底)——品牌实底 + 白内容 */
|
||||||
|
.yh-avatar--light {
|
||||||
|
background: var(--brand, #577afb);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
/* header(品牌色底)——与页面内反向:剪影档 = S1 半透明白,首字档 = S2 纯白底 + 品牌色字 */
|
||||||
|
.yh-avatar--brand {
|
||||||
|
background: rgba(255, 255, 255, 0.22);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.yh-avatar--brand.is-text {
|
||||||
|
background: #fff;
|
||||||
|
color: var(--brand, #577afb);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
+79
-40
@@ -1,8 +1,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onBeforeMount, watch, computed } from "vue";
|
import { ref, onBeforeMount, onBeforeUnmount, onMounted, watch, computed } 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 { resolveMenuPage } from "@/utils/menuRoute";
|
import { resolveMenuPage } from "@/utils/menuRoute";
|
||||||
|
import ThemeSwitcher from "@/components/ThemeSwitcher.vue";
|
||||||
|
import UserAvatar from "@/components/UserAvatar.vue";
|
||||||
|
import defaultLogo from "@/assets/image/yunhai-logo.svg";
|
||||||
|
|
||||||
const props = defineProps<{ layoutData: Record<string, any> }>();
|
const props = defineProps<{ layoutData: Record<string, any> }>();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
@@ -22,6 +25,27 @@ const modList = computed(() => props.layoutData?.modList || []);
|
|||||||
|
|
||||||
const pathNavStyle = computed(() => (isAlone.value ? { left: "0", width: "100%" } : null));
|
const pathNavStyle = computed(() => (isAlone.value ? { left: "0", width: "100%" } : null));
|
||||||
|
|
||||||
|
// 顶栏 Logo:localStorage["yh-logo-url"](平台键,yh-theme-init 异步补齐段写入)优先,本仓资产兜底——绝无空 src/裂图。
|
||||||
|
// 刷新:mounted 读一次 + 监听 yh-appearance-updated(服务端偏好到达后即时切换,无需刷新页面)。
|
||||||
|
const logoUrl = ref<string>(defaultLogo);
|
||||||
|
const readLogoUrl = () => {
|
||||||
|
let v: string | null = null;
|
||||||
|
try {
|
||||||
|
v = localStorage.getItem("yh-logo-url");
|
||||||
|
} catch {
|
||||||
|
v = null;
|
||||||
|
}
|
||||||
|
logoUrl.value = v || defaultLogo;
|
||||||
|
};
|
||||||
|
const onLogoError = () => {
|
||||||
|
if (logoUrl.value !== defaultLogo) logoUrl.value = defaultLogo;
|
||||||
|
};
|
||||||
|
onMounted(() => {
|
||||||
|
readLogoUrl();
|
||||||
|
document.addEventListener("yh-appearance-updated", readLogoUrl);
|
||||||
|
});
|
||||||
|
onBeforeUnmount(() => document.removeEventListener("yh-appearance-updated", readLogoUrl));
|
||||||
|
|
||||||
onBeforeMount(() => {
|
onBeforeMount(() => {
|
||||||
routeChange(route);
|
routeChange(route);
|
||||||
});
|
});
|
||||||
@@ -94,15 +118,21 @@ const routeChange = (to: any) => {
|
|||||||
navPath.value = [];
|
navPath.value = [];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** 统一解析菜单项目标:容忍平台配置的尾空格/旧 /notice 前缀/外链,返回内部路由 path 或外链 URL。
|
||||||
|
* 模板高亮与点击都走这里,避免「raw page(带尾空格)vs 解析后 path」不一致导致点不动/不高亮。 */
|
||||||
|
const resolveItemPage = (item: any) =>
|
||||||
|
resolveMenuPage(item?.page, item?.modId ?? item?.id, item?.name);
|
||||||
|
const pageOf = (item: any): string => resolveItemPage(item).path;
|
||||||
|
|
||||||
const linkMenu = (item: any) => {
|
const linkMenu = (item: any) => {
|
||||||
const page: string = item?.page || "";
|
const { path: page, external } = resolveItemPage(item);
|
||||||
if (!page) {
|
if (!page) {
|
||||||
// 解析不出有效页面:显式提示,而不是静默不动或被重定向吞到默认页
|
// 解析不出有效页面:显式提示,而不是静默不动或被重定向吞到默认页
|
||||||
Message.warning(`菜单「${item?.name || "未命名"}」未配置有效页面路径,请检查基础平台模块设置`);
|
Message.warning(`菜单「${item?.name || "未命名"}」未配置有效页面路径,请检查基础平台模块设置`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// 平台配置的外部链接模块:整页跳转
|
// 平台配置的外部链接模块:整页跳转
|
||||||
if (item?.external) {
|
if (external) {
|
||||||
window.location.href = page;
|
window.location.href = page;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -114,7 +144,8 @@ const linkMenu = (item: any) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const linkUserInfo = () => {
|
const linkUserInfo = () => {
|
||||||
window.location.href = baseHost.value + "/v/userinfo";
|
// 头像/姓名点击统一跳 UserHome 设置页「个人信息」pane(默认头像轮 2026-09-30,原 /v/userinfo 已下线语义)
|
||||||
|
window.location.href = baseHost.value + "/userhome/front/index.html#/settings?pane=profile";
|
||||||
};
|
};
|
||||||
const logout = () => {
|
const logout = () => {
|
||||||
let url = window.location.href;
|
let url = window.location.href;
|
||||||
@@ -128,31 +159,16 @@ const childChange = (index: number) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 根据菜单项映射图标:优先按解析后的 page 判断(平台模块 id 不可依赖),
|
/** 菜单图标:读取用户中心模块管理配置的 YschIcons 图标名(GetUserMod 的 icon 字段,如 list → icon-list)。
|
||||||
* 兜底保留旧版 modId 对照 */
|
* icons.css(ysch-icons 掩码版,基类 iconfont,类名 icon-<名称>)现由中央静态通道下发;
|
||||||
const getMenuIcon = (item?: any): string => {
|
* 平台未配置图标时统一兜底 icon-list(2026-10-09 定稿:菜单项恒有图标,保整列缩进/行高一致) */
|
||||||
const page: string = item?.page || "";
|
const getMenuIcon = (item?: any): string =>
|
||||||
if (page === "/list/new") return "ios-create";
|
`icon-${String(item?.icon || "").trim() || "list"}`;
|
||||||
if (page === "/list") return "ios-paper-plane";
|
|
||||||
if (page === "/" || page === "/accept") return "ios-mail";
|
|
||||||
if (page === "/noticelist") return "ios-list";
|
|
||||||
if (page === "/stats") return "ios-stats";
|
|
||||||
if (page === "/setting") return "ios-settings";
|
|
||||||
switch (item?.modId ?? item?.id) {
|
|
||||||
case 1: return "ios-mail";
|
|
||||||
case 2: return "ios-paper-plane";
|
|
||||||
case 3: return "ios-list";
|
|
||||||
case 4: return "ios-stats";
|
|
||||||
case 5: return "ios-document";
|
|
||||||
case 21: return "ios-settings";
|
|
||||||
case 22: return "ios-create";
|
|
||||||
default: return "";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
function linkHomePage() {
|
function linkHomePage() {
|
||||||
if (baseHost.value) {
|
if (baseHost.value) {
|
||||||
window.location.href = baseHost.value + "/BschHome/front/index.html";
|
// 旧 BschHome 已下线(线上 404),首页统一指向用户中心 userhome
|
||||||
|
window.location.href = baseHost.value + "/userhome/front/index.html";
|
||||||
} else {
|
} else {
|
||||||
window.location.href = "/";
|
window.location.href = "/";
|
||||||
}
|
}
|
||||||
@@ -162,16 +178,13 @@ function linkHomePage() {
|
|||||||
<template>
|
<template>
|
||||||
<div class="layout-wrap">
|
<div class="layout-wrap">
|
||||||
<header class="header-container fix">
|
<header class="header-container fix">
|
||||||
<div class="fl align-middle icon-box">
|
<!-- 云海 logo 徽标(结构与 Cms SchoolLayout 对齐,皮肤见通道 base.layout.css .logo-badge) -->
|
||||||
<div title="常用应用" :class="['icon-btn', isPanel ? 'active' : '']" @click="isPanel = !isPanel">
|
<span class="logo-badge fl"><img :src="logoUrl" alt="云海" @error="onLogoError" /></span>
|
||||||
<i class="iconfont icon-apps"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="delimiter fl align-middle"></div>
|
<div class="delimiter fl align-middle"></div>
|
||||||
<h4 class="fl align-middle">{{ appName }}</h4>
|
<h4 class="fl align-middle">{{ appName }}</h4>
|
||||||
<div class="school-bg"></div>
|
<div class="school-bg"></div>
|
||||||
<div class="header-aside fr">
|
<div class="header-aside fr">
|
||||||
<img :src="userInfo.avatar" class="fl align-middle" @click="linkUserInfo" />
|
<UserAvatar class="fl align-middle" :src="userInfo.avatar" :name="userInfo.name" :size="28" placement="brand" @click="linkUserInfo" />
|
||||||
<a class="name fl align-middle text-ellipsis" href="javascript:void(0);" @click="linkUserInfo">
|
<a class="name fl align-middle text-ellipsis" href="javascript:void(0);" @click="linkUserInfo">
|
||||||
{{ userInfo.name }}
|
{{ userInfo.name }}
|
||||||
</a>
|
</a>
|
||||||
@@ -180,6 +193,8 @@ function linkHomePage() {
|
|||||||
<i class="iconfont icon-logout"></i>
|
<i class="iconfont icon-logout"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- 配色切换器已移除(oapi 代理轮 2026-10-08 拍板):设置入口收敛在 UserHome 设置中心,
|
||||||
|
本地切换会被服务端生效值收敛打回,不留半残入口 -->
|
||||||
</header>
|
</header>
|
||||||
<div class="left-menu" v-if="!isAlone">
|
<div class="left-menu" v-if="!isAlone">
|
||||||
<div class="menu-top-box"></div>
|
<div class="menu-top-box"></div>
|
||||||
@@ -189,14 +204,14 @@ function linkHomePage() {
|
|||||||
<li>
|
<li>
|
||||||
<a href="javascript:;" @click="childChange(index)">
|
<a href="javascript:;" @click="childChange(index)">
|
||||||
<i :class="item.childState ? 'arrow-down' : 'arrow-up'"></i>
|
<i :class="item.childState ? 'arrow-down' : 'arrow-up'"></i>
|
||||||
<Icon v-if="getMenuIcon(item)" :type="getMenuIcon(item)" />
|
<i :class="['iconfont', getMenuIcon(item)]"></i>
|
||||||
{{ item.name }}
|
{{ item.name }}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<transition name="fade_child_ul">
|
<transition name="fade_child_ul">
|
||||||
<div class="child-item-wrap" v-show="item.childState">
|
<div class="child-item-wrap" v-show="item.childState">
|
||||||
<li
|
<li
|
||||||
:class="citem.page == currPath ? 'active child-item' : 'child-item'"
|
:class="pageOf(citem) == currPath ? 'active child-item' : 'child-item'"
|
||||||
v-for="citem in item.children"
|
v-for="citem in item.children"
|
||||||
:key="citem.id"
|
:key="citem.id"
|
||||||
>
|
>
|
||||||
@@ -207,9 +222,9 @@ function linkHomePage() {
|
|||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
</template>
|
</template>
|
||||||
<li v-else :class="item.page == currPath ? 'active' : ''">
|
<li v-else :class="pageOf(item) == currPath ? 'active' : ''">
|
||||||
<a href="javascript:;" @click="linkMenu(item)">
|
<a href="javascript:;" @click="linkMenu(item)">
|
||||||
<Icon v-if="getMenuIcon(item)" :type="getMenuIcon(item)" />
|
<i :class="['iconfont', getMenuIcon(item)]"></i>
|
||||||
{{ item.name }}
|
{{ item.name }}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -222,8 +237,10 @@ function linkHomePage() {
|
|||||||
<Breadcrumb separator=">">
|
<Breadcrumb separator=">">
|
||||||
<a href="javascript:;" @click="linkHomePage">
|
<a href="javascript:;" @click="linkHomePage">
|
||||||
<BreadcrumbItem>
|
<BreadcrumbItem>
|
||||||
<i class="iconfont icon-category"></i>
|
<span class="crumb-home">
|
||||||
首页
|
<i class="iconfont icon-shouye"></i>
|
||||||
|
首页
|
||||||
|
</span>
|
||||||
</BreadcrumbItem>
|
</BreadcrumbItem>
|
||||||
</a>
|
</a>
|
||||||
<BreadcrumbItem v-if="!isAlone" to="/">{{ appName }}</BreadcrumbItem>
|
<BreadcrumbItem v-if="!isAlone" to="/">{{ appName }}</BreadcrumbItem>
|
||||||
@@ -250,7 +267,7 @@ function linkHomePage() {
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div class="link-panel">
|
<div class="link-panel">
|
||||||
<a :href="baseHost + '/BschHome/front/index.html#/apps'" target="_blank" rel="noopener noreferrer">
|
<a :href="baseHost + '/userhome/front/index.html#/apps'" target="_blank" rel="noopener noreferrer">
|
||||||
全部应用
|
全部应用
|
||||||
<i>→</i>
|
<i>→</i>
|
||||||
</a>
|
</a>
|
||||||
@@ -261,5 +278,27 @@ function linkHomePage() {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
/* 仅覆盖/补充必要的动态样式,其余样式由全局 layout.css 提供 */
|
/* 仅覆盖/补充必要的动态样式,其余样式由中央静态通道的 base.layout.css(骨架 + 皮肤)提供 */
|
||||||
|
/* 菜单图标(通道下发的 ysch-icons 掩码类)与文字间距 */
|
||||||
|
.left-menu a .iconfont {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
/* 顶栏已由通道皮肤改为 flex 垂直居中(skin.src.css .header-container),
|
||||||
|
这里不再用 float 时代的 13px 顶偏移补偿,仅留右侧间距;徽标大小/圆底/阴影属皮肤 */
|
||||||
|
.header-container .logo-badge {
|
||||||
|
margin: 0 10px 0 0;
|
||||||
|
}
|
||||||
|
/* 面包屑「首页」垂直居中:骨架遗留的 .path-nav .icon-category{position:relative;top:1px}
|
||||||
|
是给老 iconfont 字体字形做的基线补偿,换成 ysch mask 的 1em 方块后反而把图标顶偏;
|
||||||
|
这里用 inline-flex 真居中并关掉该补偿(不动通道产物,覆盖收在本组件内) */
|
||||||
|
/* 面包屑「首页」与后续级目的文字对齐(2026-10-09 定稿):此前 .crumb-home 用 inline-flex,
|
||||||
|
行内级盒的基线取首项——16px 无字图标(其基线=方块底缘),整盒被顶高约 4px,首页文字
|
||||||
|
比相邻级目高。改回普通行内流:图标(.iconfont 基类 inline-block,基线=底缘)以 -0.125em
|
||||||
|
基线偏移做光学居中,文字与相邻级目共用同一行盒基线,天然对齐 */
|
||||||
|
.crumb-home .iconfont {
|
||||||
|
position: static;
|
||||||
|
top: 0;
|
||||||
|
margin-right: 4px;
|
||||||
|
vertical-align: -0.125em;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
+2
-4
@@ -13,10 +13,8 @@ import mittBus from "@/utils/mitt";
|
|||||||
import "view-ui-plus/dist/styles/viewuiplus.css";
|
import "view-ui-plus/dist/styles/viewuiplus.css";
|
||||||
|
|
||||||
import "@/assets/styles/main.scss";
|
import "@/assets/styles/main.scss";
|
||||||
// 本地化的布局/主题样式(原远程 base.layout.beta / _iview_override / default-theme 合并)
|
// 布局/主题/图标(base.layout.css + iview.override.css + icons.css)由中央静态通道下发:
|
||||||
import "@/assets/styles/layout.css";
|
// index.html 的 yh-theme-init.js 在 DOMContentLoaded 后按 manifest 版本追加 <link>,天然排在本文件组件库样式之后
|
||||||
// 规范 iconfont 图标本地实现(icon-browse / icon-edit / icon-ashbin 等,平台主题不含字体)
|
|
||||||
import "@/assets/styles/icons.css";
|
|
||||||
//导入自定义指令
|
//导入自定义指令
|
||||||
import directive from "@/utils/directive/index";
|
import directive from "@/utils/directive/index";
|
||||||
|
|
||||||
|
|||||||
+36
-10
@@ -6,12 +6,15 @@ import type { RouteRecordRaw } from "vue-router";
|
|||||||
* - /list/new?serid= (编辑)
|
* - /list/new?serid= (编辑)
|
||||||
* - /list/new?fid= (转发)
|
* - /list/new?fid= (转发)
|
||||||
* - /list/new?tempid=(模板引用)
|
* - /list/new?tempid=(模板引用)
|
||||||
* - /details?serid=&type= type: 0=编辑视角(不标读) 1=普通查看(标读)
|
* - /list/detail?serid=&type=、/noticelist/detail?serid=&type=
|
||||||
|
* type: 0=编辑视角(不标读) 1=普通查看(标读)
|
||||||
*
|
*
|
||||||
* modId 是菜单 ↔ 路由的稳定契约(对照后端 GetUserMod 现行数据):
|
* modId 是菜单 ↔ 路由的稳定契约(严格对照后端 /base/GetUserMod 现行返回的 modId):
|
||||||
* 1=已收到 2=已发送 3=全部通知 4=通知统计 21=应用设置
|
* 1=已收到 2=已发送 3=全部通知 4=通知统计 21=应用设置 89=待审核 91=抄送给我的
|
||||||
* 发通知为「已发送」的深层子页(modId 22 不在菜单树,meta.parentId=2 关联高亮),
|
* (后两条平台已挂一级菜单,page 分别为 /approval、/cc)。
|
||||||
* 与 UserBase「应用管理>应用类型管理」范式一致。
|
* 深层子页不在菜单树,靠 meta.parentId 关联高亮父菜单:/list/new=22(父2)、
|
||||||
|
* /list/detail=23(父2)、/noticelist/detail=24(父3)。深层页 modId 不得与任何
|
||||||
|
* 平台一级模块 ID(含 89/91)重复。
|
||||||
*/
|
*/
|
||||||
const routes: RouteRecordRaw[] = [
|
const routes: RouteRecordRaw[] = [
|
||||||
// 已收通知(收件箱):后端菜单 page 配置为 "/",即应用入口页
|
// 已收通知(收件箱):后端菜单 page 配置为 "/",即应用入口页
|
||||||
@@ -43,12 +46,19 @@ const routes: RouteRecordRaw[] = [
|
|||||||
component: () => import("@/views/notice/NoticeList.vue"),
|
component: () => import("@/views/notice/NoticeList.vue"),
|
||||||
meta: { title: "全部通知", modId: 3 },
|
meta: { title: "全部通知", modId: 3 },
|
||||||
},
|
},
|
||||||
// 通知详情(已发/已收共用,type 区分是否标读)
|
// 通知详情(同一组件挂两个路由,面包屑/菜单高亮由 meta.parentId 确定父节点:
|
||||||
|
// 已发送下 /list/detail,全部通知下 /noticelist/detail;type 区分是否标读)
|
||||||
{
|
{
|
||||||
path: "/details",
|
path: "/list/detail",
|
||||||
name: "notice_details",
|
name: "notice_list_detail",
|
||||||
component: () => import("@/views/notice/NoticeDetails.vue"),
|
component: () => import("@/views/notice/NoticeDetails.vue"),
|
||||||
meta: { title: "通知详情", modId: 5 },
|
meta: { title: "通知详情", modId: 23, parentId: 2 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/noticelist/detail",
|
||||||
|
name: "notice_manage_detail",
|
||||||
|
component: () => import("@/views/notice/NoticeDetails.vue"),
|
||||||
|
meta: { title: "通知详情", modId: 24, parentId: 3 },
|
||||||
},
|
},
|
||||||
// 通知统计(旧版行为:整页跳转外部统计系统)
|
// 通知统计(旧版行为:整页跳转外部统计系统)
|
||||||
{
|
{
|
||||||
@@ -64,9 +74,25 @@ const routes: RouteRecordRaw[] = [
|
|||||||
component: () => import("@/views/notice/NoticeSetting.vue"),
|
component: () => import("@/views/notice/NoticeSetting.vue"),
|
||||||
meta: { title: "应用设置", modId: 21 },
|
meta: { title: "应用设置", modId: 21 },
|
||||||
},
|
},
|
||||||
|
// 待审核(应用内审批;平台模块管理已挂一级菜单,配置 ID = 89,path = /approval)
|
||||||
|
{
|
||||||
|
path: "/approval",
|
||||||
|
name: "notice_approval",
|
||||||
|
component: () => import("@/views/notice/NoticeApproval.vue"),
|
||||||
|
meta: { title: "我的待审核", modId: 89 },
|
||||||
|
},
|
||||||
|
|
||||||
|
// 抄送给我的(流程 NOTIFY 知会;平台模块管理已挂一级菜单,配置 ID = 91,path = /cc)
|
||||||
|
{
|
||||||
|
path: "/cc",
|
||||||
|
name: "notice_cc",
|
||||||
|
component: () => import("@/views/notice/NoticeCc.vue"),
|
||||||
|
meta: { title: "抄送给我的", modId: 91 },
|
||||||
|
},
|
||||||
|
|
||||||
/* ---- 旧版路径兼容重定向(书签、残留的平台模块配置) ---- */
|
/* ---- 旧版路径兼容重定向(书签、残留的平台模块配置) ---- */
|
||||||
{ path: "/accept", redirect: "/" },
|
{ path: "/accept", redirect: "/" },
|
||||||
|
{ path: "/details", redirect: "/list/detail" },
|
||||||
{ path: "/newnotice", redirect: "/list/new" },
|
{ path: "/newnotice", redirect: "/list/new" },
|
||||||
{ path: "/edit", redirect: "/list/new" },
|
{ path: "/edit", redirect: "/list/new" },
|
||||||
{ path: "/manage", redirect: "/noticelist" },
|
{ path: "/manage", redirect: "/noticelist" },
|
||||||
@@ -76,7 +102,7 @@ const routes: RouteRecordRaw[] = [
|
|||||||
{ path: "/notice/notice_accept", redirect: "/accept" },
|
{ path: "/notice/notice_accept", redirect: "/accept" },
|
||||||
{ path: "/notice/notice_list", redirect: "/list" },
|
{ path: "/notice/notice_list", redirect: "/list" },
|
||||||
{ path: "/notice/manage", redirect: "/noticelist" },
|
{ path: "/notice/manage", redirect: "/noticelist" },
|
||||||
{ path: "/notice/notice_details", redirect: "/details" },
|
{ path: "/notice/notice_details", redirect: "/list/detail" },
|
||||||
{ path: "/notice/statistics", redirect: "/stats" },
|
{ path: "/notice/statistics", redirect: "/stats" },
|
||||||
{ path: "/notice/setting", redirect: "/setting" },
|
{ path: "/notice/setting", redirect: "/setting" },
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -64,10 +64,81 @@ export interface NoticeVo {
|
|||||||
smsFlag: number;
|
smsFlag: number;
|
||||||
/** 接收人列表(JSON) */
|
/** 接收人列表(JSON) */
|
||||||
receiveList: string | null;
|
receiveList: string | null;
|
||||||
|
/** 附件ID串("1,2,3";后端 Lombok 序列化名为 upFile,对齐旧 .NET UpFile 语义) */
|
||||||
|
upFile?: string | null;
|
||||||
|
/** @deprecated 旧 .NET 接口的 PascalCase 附件字段,仅作读取兼容 */
|
||||||
|
UpFile?: string | null;
|
||||||
createBy: string | null;
|
createBy: string | null;
|
||||||
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/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;
|
||||||
|
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;
|
||||||
|
/** 结构化步骤(2026-09-17 接入:steps[0] 固定"发起申请",同节点多任务每人一行) */
|
||||||
|
steps?: FlowProgressStepVo[] | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 流程步骤(SDK FlowProgressStep 同构:后端 /flow/Trace 的 steps 元素) */
|
||||||
|
export interface FlowProgressStepVo {
|
||||||
|
nodeId: string;
|
||||||
|
nodeName: string | null;
|
||||||
|
label: string | null;
|
||||||
|
active: boolean;
|
||||||
|
assignees?: {
|
||||||
|
userName?: string | null;
|
||||||
|
status?: number | null;
|
||||||
|
statusLabel?: string | null;
|
||||||
|
opinion?: string | null;
|
||||||
|
handleTime?: string | null;
|
||||||
|
}[] | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================ 通知详情 ============================
|
// ============================ 通知详情 ============================
|
||||||
@@ -76,6 +147,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 +270,7 @@ export interface NoticeRequest {
|
|||||||
smsFlag?: number;
|
smsFlag?: number;
|
||||||
/** 转发源通知ID */
|
/** 转发源通知ID */
|
||||||
sourceId?: number;
|
sourceId?: number;
|
||||||
|
// 审核流程不由前端指定:绑定在「应用设置 > 流程绑定」,后端按「已绑定 + 通知对象=全部用户」判定
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 通知列表查询参数(对齐 NoticeQuery.java + BasePageQuery.java) */
|
/** 通知列表查询参数(对齐 NoticeQuery.java + BasePageQuery.java) */
|
||||||
@@ -202,6 +279,11 @@ export interface NoticeQuery {
|
|||||||
pageSize?: number;
|
pageSize?: number;
|
||||||
keyword?: string;
|
keyword?: string;
|
||||||
status?: number;
|
status?: number;
|
||||||
|
/**
|
||||||
|
* 审核状态筛选(列表「状态」下拉):0 待提交 / 10 审批中 / 20 已通过 / 30 已驳回 / 40 已撤回;
|
||||||
|
* -1 = 无需审核(直发通知);不传不过滤
|
||||||
|
*/
|
||||||
|
auditStatus?: number;
|
||||||
type?: number;
|
type?: number;
|
||||||
/** 列表范围(管理端):"user" 只看自己发布的,不传=全部 */
|
/** 列表范围(管理端):"user" 只看自己发布的,不传=全部 */
|
||||||
scope?: string;
|
scope?: string;
|
||||||
|
|||||||
@@ -58,9 +58,11 @@ function errorHandle<T>(res: BschResponse<T>, status: number): T {
|
|||||||
case 302:
|
case 302:
|
||||||
console.log(res);
|
console.log(res);
|
||||||
// 后端 CasSsoApiRedirectFilter 已在 service 中拼好 ;redirectToReferer=<hex>,
|
// 后端 CasSsoApiRedirectFilter 已在 service 中拼好 ;redirectToReferer=<hex>,
|
||||||
// 直接跳转即可;登录后由 CasSsoRefererRedirectFilter 解码 302 回前端页
|
// 直接跳转即可;登录后由 CasSsoRefererRedirectFilter 解码 302 回前端页。
|
||||||
|
// 跳转后返回永不 resolve 的挂起 Promise:调用方 await 悬停,
|
||||||
|
// 全局 onBizError 与各页面的 catch 均不再触发(不弹 redirect 报错)
|
||||||
window.location.href = res.urlToRedirectTo!;
|
window.location.href = res.urlToRedirectTo!;
|
||||||
throw new Error("redirect");
|
return new Promise(() => {}) as T;
|
||||||
case 4:
|
case 4:
|
||||||
throw new Error(res.msg || "参数错误!");
|
throw new Error(res.msg || "参数错误!");
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
/**
|
||||||
|
* 默认头像样式(首字 / 剪影)公共源(YschTheme,scripts/sync-apps.mjs 下发三端 src/utils/avatar.ts)。
|
||||||
|
* 取值链与主题同构(theme-core.js resolveEffectiveThemeState 的合成链写法):
|
||||||
|
* URL ?yh-avatar=text|silhouette(仅预览/排障覆盖:只影响读、绝不写回)
|
||||||
|
* > localStorage["yh-avatar"](用户自选,纯字符串)
|
||||||
|
* > localStorage["yh-avatar-sch-default"](学校默认,UserHome 登录后写入)
|
||||||
|
* > "text"(系统默认)
|
||||||
|
* 值域恒为 "text" | "silhouette",键缺失/值非法均静默回落下一级,不报错不阻塞渲染。
|
||||||
|
* 渲染三态与取字规则的唯一契约见 UserHome《设置页修复与默认头像落地方案》§10.1/§10.2。
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type AvatarStyle = "text" | "silhouette";
|
||||||
|
|
||||||
|
interface StorageLike {
|
||||||
|
getItem(k: string): string | null;
|
||||||
|
setItem(k: string, v: string): void;
|
||||||
|
removeItem(k: string): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AVATAR_STORAGE_KEY = "yh-avatar";
|
||||||
|
|
||||||
|
/** 学校默认头像存储键(UserHome 登录时写入;值格式与用户键相同,仅无"用户自选"语义) */
|
||||||
|
export const SCHOOL_DEFAULT_AVATAR_STORAGE_KEY = "yh-avatar-sch-default";
|
||||||
|
|
||||||
|
export const AVATAR_STYLES: readonly AvatarStyle[] = ["text", "silhouette"];
|
||||||
|
|
||||||
|
export const DEFAULT_AVATAR_STYLE: AvatarStyle = "text";
|
||||||
|
|
||||||
|
/** 值合法才采用(用户键/学校键同一判据;脏数据不生效) */
|
||||||
|
function readAvatarStyleOrNull(raw: string | null): AvatarStyle | null {
|
||||||
|
return raw === "text" || raw === "silhouette" ? raw : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** URL 覆盖参数(?yh-avatar=text|silhouette):非法值忽略,且只影响"读哪一档"、绝不写回 localStorage */
|
||||||
|
function readAvatarOverride(): AvatarStyle | null {
|
||||||
|
try {
|
||||||
|
const search = (globalThis.location && globalThis.location.search) || "";
|
||||||
|
const m = new RegExp("[?&]yh-avatar=([^&]*)").exec(search);
|
||||||
|
return m ? readAvatarStyleOrNull(decodeURIComponent(m[1])) : null;
|
||||||
|
} catch {
|
||||||
|
/* 无 location 环境(测试/SSR):无覆盖 */
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生效样式合成(与 resolveEffectiveThemeState 同构):
|
||||||
|
* URL 覆盖 > localStorage["yh-avatar"](用户自选)> localStorage["yh-avatar-sch-default"](学校默认)
|
||||||
|
* > DEFAULT_AVATAR_STYLE。
|
||||||
|
*/
|
||||||
|
export function resolveAvatarStyle(storage: StorageLike | undefined = globalThis.localStorage): AvatarStyle {
|
||||||
|
let style: AvatarStyle | null = null;
|
||||||
|
if (storage) {
|
||||||
|
style = readAvatarStyleOrNull(storage.getItem(AVATAR_STORAGE_KEY));
|
||||||
|
if (!style) style = readAvatarStyleOrNull(storage.getItem(SCHOOL_DEFAULT_AVATAR_STORAGE_KEY));
|
||||||
|
}
|
||||||
|
if (!style) style = DEFAULT_AVATAR_STYLE;
|
||||||
|
return readAvatarOverride() || style;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 保存用户自选(直写 localStorage,无响应式;调用方操作后自行刷新 UI) */
|
||||||
|
export function saveAvatarStyle(style: AvatarStyle, storage: StorageLike | undefined = globalThis.localStorage): void {
|
||||||
|
try {
|
||||||
|
storage?.setItem(AVATAR_STORAGE_KEY, style);
|
||||||
|
} catch {
|
||||||
|
/* 隐私模式/配额失败静默:仅丢持久化,不丢本次会话生效 */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 跟随学校默认 = 删用户自选键(之后回落学校默认/系统默认) */
|
||||||
|
export function clearAvatarStyle(storage: StorageLike | undefined = globalThis.localStorage): void {
|
||||||
|
try {
|
||||||
|
storage?.removeItem(AVATAR_STORAGE_KEY);
|
||||||
|
} catch {
|
||||||
|
/* 隐私模式静默 */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取字(顺序判定,先命中先返回):含 "@" 先截 "@" 前段(邮箱取首字母);
|
||||||
|
* 首字符必须用展开 [...s][0](禁用 charAt(0),避免代理对截半出乱码)。
|
||||||
|
* ASCII 字母转大写;纯数字回落 ""(剪影);符号/emoji/空白回落 ""(剪影);其余(CJK 等)原样。
|
||||||
|
*/
|
||||||
|
export function pickAvatarText(name: string | null | undefined): string {
|
||||||
|
let s = String(name ?? "");
|
||||||
|
const at = s.indexOf("@");
|
||||||
|
if (at > -1) s = s.slice(0, at);
|
||||||
|
const ch = [...s.trim()][0];
|
||||||
|
if (ch === undefined) return "";
|
||||||
|
if (/^[A-Za-z]$/.test(ch)) return ch.toUpperCase();
|
||||||
|
if (/^[0-9]$/.test(ch)) return "";
|
||||||
|
if (/^[\p{P}\p{S}\s]$/u.test(ch)) return "";
|
||||||
|
return ch;
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
/**
|
||||||
|
* 流程示意图 PNG 显示口径(2026-10-10 用户反馈:嵌入页图过大,同 flow-center/front 同名工具):
|
||||||
|
* 服务端按 SCALE=2 出图(视网膜清晰度),按自然尺寸渲染等于整体放大一倍。
|
||||||
|
* 须按逻辑尺寸(自然宽一半)显示;超宽长图收缩到容器宽。
|
||||||
|
* 用法:<img ref="diagramEl" :src="..." @load="onDiagramLoad">(图均为打开时挂载,@load 必触发)。
|
||||||
|
*/
|
||||||
|
export function fitDiagram(el: HTMLImageElement | null | undefined): void {
|
||||||
|
if (!el || !el.naturalWidth) return;
|
||||||
|
const want = el.naturalWidth / 2;
|
||||||
|
const cap = (el.parentElement?.clientWidth || 0) - 8;
|
||||||
|
el.style.width = Math.round(Math.min(want, cap > 120 ? cap : want)) + "px";
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { ref, onMounted, onBeforeUnmount, watch, type Ref } from "vue";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 实测某容器元素的可用高度(px),随尺寸变化实时更新。
|
||||||
|
*
|
||||||
|
* 用途:表格页把 <Table> 放进 flex:1 的 .table-region 里,用本组合式量出该区域
|
||||||
|
* 的真实高度喂给 view-ui 的 `:height`(固定表头需要一个数值高度),从而彻底替代
|
||||||
|
* 旧的 useTableHeight()「视口高 - 一堆写死常量」的算法——加/删工具条、改顶栏高
|
||||||
|
* 都不再需要同步改任何魔法数字。
|
||||||
|
*
|
||||||
|
* @param el 目标容器 ref(模板里 ref 绑定的那个元素)
|
||||||
|
* @returns 响应式高度(px);挂载测量前为 0,调用方可 `:height="h || undefined"` 兜底
|
||||||
|
*/
|
||||||
|
export function useElementHeight(el: Ref<HTMLElement | null>): Ref<number> {
|
||||||
|
const height = ref(0);
|
||||||
|
let ro: ResizeObserver | null = null;
|
||||||
|
|
||||||
|
const measure = () => {
|
||||||
|
if (el.value) height.value = Math.floor(el.value.clientHeight);
|
||||||
|
};
|
||||||
|
|
||||||
|
const observe = () => {
|
||||||
|
ro?.disconnect();
|
||||||
|
if (!el.value || typeof ResizeObserver === "undefined") return;
|
||||||
|
measure();
|
||||||
|
ro = new ResizeObserver(measure);
|
||||||
|
ro.observe(el.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(observe);
|
||||||
|
// v-if / v-show 切换导致容器重建时,重新挂观察
|
||||||
|
watch(el, observe);
|
||||||
|
onBeforeUnmount(() => ro?.disconnect());
|
||||||
|
|
||||||
|
return height;
|
||||||
|
}
|
||||||
+1
-1
@@ -100,7 +100,7 @@ export default class Utils {
|
|||||||
"div",
|
"div",
|
||||||
{
|
{
|
||||||
class: `operating-btn iconfont ${iconClass}`,
|
class: `operating-btn iconfont ${iconClass}`,
|
||||||
onClick: callback,
|
onClick: disabled ? null : callback,
|
||||||
},
|
},
|
||||||
null,
|
null,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -0,0 +1,151 @@
|
|||||||
|
/**
|
||||||
|
* 本文件由 YschTheme scripts/gen-theme.mjs 从 assets-src/pc/theme-core.js 生成(版本 20261009-1655-488a3c3),勿手改。
|
||||||
|
* 分发方式:YschTheme `npm run sync` 拷进各端 src/utils/theme.ts(与 ThemeSwitcher.vue 配套,MD5 对齐报告)。
|
||||||
|
* 运行时档位类名由 /static/js/yh-theme-init.js 在 <head> 内先行挂好,本模块只负责读写与切换。
|
||||||
|
*/
|
||||||
|
export type ThemeName = "blue" | "ocean" | "ink" | "violet" | "teal";
|
||||||
|
|
||||||
|
/** 定稿默认档:blue + side-deep + grad-on + 侧栏跟随(D1 拍板 deep) */
|
||||||
|
export interface ThemeState {
|
||||||
|
theme: ThemeName;
|
||||||
|
/** 侧栏深色(false=浅色档) */
|
||||||
|
sideDeep: boolean;
|
||||||
|
/** 顶栏渐变(联动侧栏竖向渐变) */
|
||||||
|
gradOn: boolean;
|
||||||
|
/** 侧栏颜色跟随顶栏品牌色(false=中性灰 side-plain) */
|
||||||
|
sideFollow: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StorageLike {
|
||||||
|
getItem(k: string): string | null;
|
||||||
|
setItem(k: string, v: string): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const THEME_NAMES: readonly ThemeName[] = ["blue", "ocean", "ink", "violet", "teal"];
|
||||||
|
|
||||||
|
export const THEME_STORAGE_KEY = "yh-theme";
|
||||||
|
|
||||||
|
/* 学校默认档存储键(UserHome 登录时写入;值格式与 yh-theme 完全相同,仅无"用户自选"语义) */
|
||||||
|
export const SCHOOL_DEFAULT_STORAGE_KEY = "yh-theme-sch-default";
|
||||||
|
|
||||||
|
export const DEFAULT_THEME_STATE: ThemeState = {
|
||||||
|
theme: "blue",
|
||||||
|
sideDeep: true,
|
||||||
|
gradOn: true,
|
||||||
|
sideFollow: true
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 宽松解析:任一字段非法/缺失即回退默认档(脏数据不阻塞启动) */
|
||||||
|
export function parseThemeState(raw: string | null): ThemeState {
|
||||||
|
if (!raw) return { ...DEFAULT_THEME_STATE };
|
||||||
|
try {
|
||||||
|
const o = JSON.parse(raw);
|
||||||
|
if (!o || typeof o !== "object") return { ...DEFAULT_THEME_STATE };
|
||||||
|
return {
|
||||||
|
theme: THEME_NAMES.includes(o.theme) ? o.theme : DEFAULT_THEME_STATE.theme,
|
||||||
|
sideDeep: typeof o.sideDeep === "boolean" ? o.sideDeep : DEFAULT_THEME_STATE.sideDeep,
|
||||||
|
gradOn: typeof o.gradOn === "boolean" ? o.gradOn : DEFAULT_THEME_STATE.gradOn,
|
||||||
|
sideFollow: typeof o.sideFollow === "boolean" ? o.sideFollow : DEFAULT_THEME_STATE.sideFollow
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return { ...DEFAULT_THEME_STATE };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 严格读取:四字段全部合法才返回状态,否则 null(合成链"值非法→回落下一级"用;用户键仍走宽松解析保现状) */
|
||||||
|
function readThemeStateOrNull(raw: string | null): ThemeState | null {
|
||||||
|
if (!raw) return null;
|
||||||
|
try {
|
||||||
|
const o = JSON.parse(raw);
|
||||||
|
if (!o || typeof o !== "object") return null;
|
||||||
|
if (!THEME_NAMES.includes(o.theme)) return null;
|
||||||
|
if (typeof o.sideDeep !== "boolean" || typeof o.gradOn !== "boolean" || typeof o.sideFollow !== "boolean") return null;
|
||||||
|
return { theme: o.theme, sideDeep: o.sideDeep, gradOn: o.gradOn, sideFollow: o.sideFollow };
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** URL 覆盖参数(?yh-theme= / ?yh-side=deep|light / ?yh-grad=on|off):仅预览/排障用,
|
||||||
|
非法值忽略(对应字段不覆盖),且只影响"读哪一档"、绝不写回 localStorage。 */
|
||||||
|
function readThemeOverrides(): { theme?: ThemeName; sideDeep?: boolean; gradOn?: boolean } | null {
|
||||||
|
let out = null;
|
||||||
|
try {
|
||||||
|
const search = (globalThis.location && globalThis.location.search) || "";
|
||||||
|
const get = (name) => {
|
||||||
|
const m = new RegExp("[?&]" + name + "=([^&]*)").exec(search);
|
||||||
|
return m ? decodeURIComponent(m[1]) : null;
|
||||||
|
};
|
||||||
|
const t = get("yh-theme");
|
||||||
|
if (t && THEME_NAMES.includes(t)) {
|
||||||
|
out = out || {};
|
||||||
|
out.theme = t;
|
||||||
|
}
|
||||||
|
const s = get("yh-side");
|
||||||
|
if (s === "deep" || s === "light") {
|
||||||
|
out = out || {};
|
||||||
|
out.sideDeep = s === "deep";
|
||||||
|
}
|
||||||
|
const g = get("yh-grad");
|
||||||
|
if (g === "on" || g === "off") {
|
||||||
|
out = out || {};
|
||||||
|
out.gradOn = g === "on";
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
/* 无 location 环境(测试/SSR):无覆盖 */
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生效档合成(唯一实现,init 启动段与 UserHome theme.ts 共用):
|
||||||
|
* URL 覆盖 > localStorage["yh-theme"](用户自选,存在即用——宽松修补,与历史行为一致)
|
||||||
|
* > localStorage["yh-theme-sch-default"](学校默认,整体合法才生效)
|
||||||
|
* > DEFAULT_THEME_STATE。
|
||||||
|
* 键不存在/解析失败/值非法均静默回落,不报错不阻塞首帧。
|
||||||
|
*/
|
||||||
|
export function resolveEffectiveThemeState(storage: StorageLike | undefined = globalThis.localStorage): ThemeState {
|
||||||
|
let state = null;
|
||||||
|
if (storage) {
|
||||||
|
const userRaw = storage.getItem(THEME_STORAGE_KEY);
|
||||||
|
if (userRaw) state = parseThemeState(userRaw);
|
||||||
|
if (!state) state = readThemeStateOrNull(storage.getItem(SCHOOL_DEFAULT_STORAGE_KEY));
|
||||||
|
}
|
||||||
|
if (!state) state = { ...DEFAULT_THEME_STATE };
|
||||||
|
const ov = readThemeOverrides();
|
||||||
|
if (ov) {
|
||||||
|
if (ov.theme) state = { ...state, theme: ov.theme };
|
||||||
|
if (ov.sideDeep != null) state = { ...state, sideDeep: ov.sideDeep };
|
||||||
|
if (ov.gradOn != null) state = { ...state, gradOn: ov.gradOn };
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadThemeState(storage: StorageLike | undefined = globalThis.localStorage): ThemeState {
|
||||||
|
if (!storage) return { ...DEFAULT_THEME_STATE };
|
||||||
|
return parseThemeState(storage.getItem(THEME_STORAGE_KEY));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function saveThemeState(state: ThemeState, storage: StorageLike | undefined = globalThis.localStorage): void {
|
||||||
|
try {
|
||||||
|
storage?.setItem(THEME_STORAGE_KEY, JSON.stringify(state));
|
||||||
|
} catch {
|
||||||
|
/* 隐私模式/配额失败静默:仅丢持久化,不丢本次会话生效 */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 把状态落到根元素档位类名(幂等:先清后挂),返回传入的 state 便于链式 */
|
||||||
|
export function applyThemeState(state: ThemeState, root: HTMLElement = document.documentElement): ThemeState {
|
||||||
|
root.dataset.theme = state.theme;
|
||||||
|
root.classList.toggle("side-deep", state.sideDeep);
|
||||||
|
root.classList.toggle("side-light", !state.sideDeep);
|
||||||
|
root.classList.toggle("grad-on", state.gradOn);
|
||||||
|
root.classList.toggle("grad-off", !state.gradOn);
|
||||||
|
root.classList.toggle("side-plain", !state.sideFollow);
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 启动引导:各端 main.ts(或本仓 init 脚本的启动段)于挂载前调用,消除首帧闪色 */
|
||||||
|
export function initTheme(): ThemeState {
|
||||||
|
return applyThemeState(loadThemeState());
|
||||||
|
}
|
||||||
+159
-12
@@ -2,14 +2,15 @@
|
|||||||
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, FlowBindingVo } 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";
|
||||||
|
import { fitDiagram } from "@/utils/diagramImg";
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -26,6 +27,29 @@ const form = reactive({
|
|||||||
content: "",
|
content: "",
|
||||||
isShortMsg: false,
|
isShortMsg: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/* ============ 先审后发(应用设置 > 流程绑定,按学校生效) ============
|
||||||
|
规则(后端为单一事实源):已绑定 + 通知对象=全部用户 → 必须走审核,只显示「提交审核」,
|
||||||
|
隐藏「发送」与「定时发送」;指定人员与未绑定 → 原直发链路不变。 */
|
||||||
|
const flowBinding = ref<FlowBindingVo>({ bound: false, flowGuid: null, flowName: null, flowEnabled: true });
|
||||||
|
const noticeFlowGuid = ref(""); // 编辑回显:该通知自身的流程快照(决定沿用哪个流程重审)
|
||||||
|
const auditStatus = ref<number | null>(null); // 编辑回显的审核态
|
||||||
|
const auditResult = ref<{ result?: string; opinion?: string; rejectNode?: string } | null>(null);
|
||||||
|
|
||||||
|
/** 通知对象是否「全部用户」(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<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 +76,33 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/* 流程绑定(决定按钮显隐;流程中心未接入时 bound 恒为 false → 原直发界面) */
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
const view = await noticeApi.getFlowBinding().send();
|
||||||
|
if (view) flowBinding.value = view;
|
||||||
|
} catch {
|
||||||
|
flowBinding.value = { bound: false, flowGuid: null, flowName: null, flowEnabled: false };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 流程图 Drawer(后端 DiagramDef 中转 PNG:同源免 token;与设置页同款,修复旧深链 404) */
|
||||||
|
const flowDiagramVisible = ref(false);
|
||||||
|
const flowDiagramEl = ref<HTMLImageElement | null>(null);
|
||||||
|
const flowDiagramSrc = computed(() =>
|
||||||
|
flowDiagramVisible.value && auditFlowGuid.value
|
||||||
|
? `${config.serviceHost}/api/notice/flow/DiagramDef?flowGuid=${encodeURIComponent(auditFlowGuid.value)}`
|
||||||
|
: "",
|
||||||
|
);
|
||||||
|
function openFlowDiagram() {
|
||||||
|
if (!auditFlowGuid.value) return;
|
||||||
|
flowDiagramVisible.value = true;
|
||||||
|
}
|
||||||
|
function onFlowDiagramError() {
|
||||||
|
flowDiagramVisible.value = false;
|
||||||
|
Message.error("流程图加载失败(流程可能未发布或绑定已解除)");
|
||||||
|
}
|
||||||
|
|
||||||
/* ============ 富文本编辑器(wangeditor v5 for vue3) ============ */
|
/* ============ 富文本编辑器(wangeditor v5 for vue3) ============ */
|
||||||
const editorRef = shallowRef();
|
const editorRef = shallowRef();
|
||||||
const editorLoading = ref(true);
|
const editorLoading = ref(true);
|
||||||
@@ -134,6 +185,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;
|
||||||
|
// 审核绑定回显(驳回/撤回态显示意见与重提按钮)
|
||||||
|
noticeFlowGuid.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 +232,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, //是否发送短信
|
||||||
|
// 审核流程不再由前端指定:后端按「应用设置绑定 + 通知对象=全部用户」判定
|
||||||
};
|
};
|
||||||
if (isTime) {
|
if (isTime) {
|
||||||
//定时发送
|
//定时发送
|
||||||
@@ -204,21 +260,41 @@ async function doAdd(_type?: string) {
|
|||||||
Message.info("请选择大于当前的时间!");
|
Message.info("请选择大于当前的时间!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
data.timingSendTime = dateFormat(new Date(timeForm.date1), "yyyy/MM/dd HH:mm:ss");
|
data.timingSendTime = dateFormat(new Date(timeForm.date1), "yyyy-MM-dd HH:mm:ss");
|
||||||
}
|
}
|
||||||
|
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
try {
|
try {
|
||||||
if (!isEdit.value) {
|
if (!isEdit.value) {
|
||||||
await sendCreate(data);
|
await sendCreate(data);
|
||||||
Message.success("发送成功");
|
Message.success(needAudit.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 (needAudit.value && auditStatus.value === 20) {
|
||||||
router.push("/list");
|
Modal.confirm({
|
||||||
|
title: "重新送审确认",
|
||||||
|
content: "该通知已发布,修改后将重新走审批流程,期间对用户不可见,确认继续?",
|
||||||
|
onOk: async () => {
|
||||||
|
await saveEditAndLeave(data);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 绑定生效后把存量通知(此前直发、无审核轨道)改为全部用户:保存即将转入待审核
|
||||||
|
if (needAudit.value && !noticeFlowGuid.value) {
|
||||||
|
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 +302,27 @@ async function doAdd(_type?: string) {
|
|||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 编辑保存 + 离开(§3.6 抽出:确认分支复用) */
|
||||||
|
async function saveEditAndLeave(data: NoticeRequest) {
|
||||||
|
await sendEdit(data);
|
||||||
|
// 编辑驳回/撤回态:后端 edit 内已自动重提(复用同办件 R4);待提交态由重提按钮处理
|
||||||
|
Message.success(needAudit.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>
|
||||||
@@ -249,6 +346,20 @@ async function doAdd(_type?: string) {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
</Form>
|
</Form>
|
||||||
|
|
||||||
|
<!-- 绑定了审核流程且通知对象=全部用户:告知需要审核(流程在「应用设置 > 流程绑定」维护) -->
|
||||||
|
<Alert v-if="needAudit" type="info" show-icon class="audit-alert">
|
||||||
|
本应用已绑定审核流程{{ auditFlowName ? `《${auditFlowName}》` : "" }}:发送全部用户需审核通过后发布
|
||||||
|
<template v-if="auditFlowGuid"> <Button type="text" size="small" @click="openFlowDiagram">查看流程图</Button></template>
|
||||||
|
<template v-if="isAllTarget"> 如需直接发送,可将通知对象改为「指定用户」</template>
|
||||||
|
</Alert>
|
||||||
|
|
||||||
|
<!-- 驳回/撤回意见提示(编辑驳回/撤回态) -->
|
||||||
|
<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,8 +373,19 @@ 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()">
|
||||||
<Button v-if="!isEdit" style="margin-left: 10px" @click="isModal.timiSend = true">定时发送</Button>
|
{{ isEdit ? (needAudit ? "保存并重新提交审核" : "保存") : needAudit ? "提交审核" : "发送" }}
|
||||||
|
</Button>
|
||||||
|
<!-- 待提交态单独重提(提交流程中心失败后的幂等重推入口) -->
|
||||||
|
<Button
|
||||||
|
v-if="isEdit && noticeFlowGuid && auditStatus === 0"
|
||||||
|
style="margin-left: 10px"
|
||||||
|
:loading="isLoading"
|
||||||
|
@click="doResubmit"
|
||||||
|
>重新提交审核</Button
|
||||||
|
>
|
||||||
|
<!-- 定时发送与审核互斥:走审核时隐藏(后端同步清空定时时间兜底) -->
|
||||||
|
<Button v-if="!isEdit && !needAudit" style="margin-left: 10px" @click="isModal.timiSend = true">定时发送</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 定时发送 -->
|
<!-- 定时发送 -->
|
||||||
@@ -297,11 +419,22 @@ async function doAdd(_type?: string) {
|
|||||||
<Button @click="isModal.timiSend = false" style="margin-left: 8px">取消</Button>
|
<Button @click="isModal.timiSend = false" style="margin-left: 8px">取消</Button>
|
||||||
</template>
|
</template>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
<!-- 流程图 Drawer(后端 DiagramDef 同源 PNG;竖版长图容器内纵向滚动) -->
|
||||||
|
<Drawer v-model="flowDiagramVisible" title="流程示意图" :width="860" :transfer="false"
|
||||||
|
:styles="{ body: { padding: '0' } }">
|
||||||
|
<div class="flow-diagram-frame">
|
||||||
|
<img ref="flowDiagramEl" v-if="flowDiagramSrc" :src="flowDiagramSrc" alt="流程示意图" @load="fitDiagram(flowDiagramEl)" @error="onFlowDiagramError" />
|
||||||
|
</div>
|
||||||
|
</Drawer>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
.audit-alert {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
/* 页面超高时的滚动由 App.vue 的 .main-wrapper 统一承接 */
|
/* 页面超高时的滚动由 App.vue 的 .main-wrapper 统一承接 */
|
||||||
.myform {
|
.myform {
|
||||||
:deep(.ivu-form-item) {
|
:deep(.ivu-form-item) {
|
||||||
@@ -309,11 +442,11 @@ async function doAdd(_type?: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.editor-wrap {
|
.editor-wrap {
|
||||||
border: 1px solid #dcdee2;
|
border: 1px solid var(--line);
|
||||||
border-radius: 4px;
|
border-radius: var(--r-s);
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
.editor-toolbar {
|
.editor-toolbar {
|
||||||
border-bottom: 1px solid #e8e8e8;
|
border-bottom: 1px solid var(--line);
|
||||||
}
|
}
|
||||||
.editor-content {
|
.editor-content {
|
||||||
:deep(.w-e-text-container) {
|
:deep(.w-e-text-container) {
|
||||||
@@ -325,4 +458,18 @@ async function doAdd(_type?: string) {
|
|||||||
.bottom-btn {
|
.bottom-btn {
|
||||||
margin-top: 6px;
|
margin-top: 6px;
|
||||||
}
|
}
|
||||||
|
/* 流程图 Drawer:浅底色 + 纵向滚动(竖版长图),图片撑满可读宽度 */
|
||||||
|
.flow-diagram-frame {
|
||||||
|
height: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
background: #f5f7fa;
|
||||||
|
padding: 12px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.flow-diagram-frame img {
|
||||||
|
display: block;
|
||||||
|
margin: 0 auto;
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { Message, Tooltip } from "view-ui-plus";
|
|||||||
import { noticeApi } from "@/api/module/notice";
|
import { noticeApi } from "@/api/module/notice";
|
||||||
import type { NoticeDetailVo, NoticeQuery, NoticeVo, NoticePageResult } from "@/types/notice";
|
import type { NoticeDetailVo, NoticeQuery, NoticeVo, NoticePageResult } from "@/types/notice";
|
||||||
import FilePreviewModal from "@/components/FilePreviewModal.vue";
|
import FilePreviewModal from "@/components/FilePreviewModal.vue";
|
||||||
|
import SvgIcon from "@/components/SvgIcon.vue";
|
||||||
import Utils from "@/utils";
|
import Utils from "@/utils";
|
||||||
|
|
||||||
/* ============ 左侧列表 ============ */
|
/* ============ 左侧列表 ============ */
|
||||||
@@ -111,7 +112,7 @@ function openPreview(item: { name?: string; filename?: string; fileid?: number;
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 附件图标(按扩展名,图标名对齐旧版 matchSuffix → icon-word/excel/ppt/pdf1/txt/tupian1/yasuo/weizhi) */
|
/* 附件图标(按扩展名,图标名对齐旧版 matchSuffix → icon-word/excel/ppt/pdf1/txt/tupian/yasuo/weizhi) */
|
||||||
function getFileIcon(name: string) {
|
function getFileIcon(name: string) {
|
||||||
const suffix = String(name || "").split(".").pop()?.toLowerCase() || "";
|
const suffix = String(name || "").split(".").pop()?.toLowerCase() || "";
|
||||||
if (["doc", "docx", "wps"].includes(suffix)) return "icon-word";
|
if (["doc", "docx", "wps"].includes(suffix)) return "icon-word";
|
||||||
@@ -119,7 +120,8 @@ function getFileIcon(name: string) {
|
|||||||
if (["ppt", "pptx"].includes(suffix)) return "icon-ppt";
|
if (["ppt", "pptx"].includes(suffix)) return "icon-ppt";
|
||||||
if (["pdf"].includes(suffix)) return "icon-pdf1";
|
if (["pdf"].includes(suffix)) return "icon-pdf1";
|
||||||
if (["txt"].includes(suffix)) return "icon-txt";
|
if (["txt"].includes(suffix)) return "icon-txt";
|
||||||
if (["jpg", "jpeg", "png", "gif", "bmp"].includes(suffix)) return "icon-tupian1";
|
// 平台 icons.css 的 icon-tupian 是线性「图片」图标;icon-tupian1 是旧彩色图转单色,看不出是图片
|
||||||
|
if (["jpg", "jpeg", "png", "gif", "bmp"].includes(suffix)) return "icon-tupian";
|
||||||
if (["zip", "rar"].includes(suffix)) return "icon-yasuo";
|
if (["zip", "rar"].includes(suffix)) return "icon-yasuo";
|
||||||
return "icon-weizhi";
|
return "icon-weizhi";
|
||||||
}
|
}
|
||||||
@@ -163,7 +165,7 @@ function shortDate(date: string) {
|
|||||||
/* 时间显示(列表 MM/dd HH:mm,同旧版) */</script>
|
/* 时间显示(列表 MM/dd HH:mm,同旧版) */</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="page-wrapper">
|
<div class="page-wrapper yh-fill">
|
||||||
<div class="main-container">
|
<div class="main-container">
|
||||||
<div class="accept-wrap">
|
<div class="accept-wrap">
|
||||||
<!-- 左侧通知列表 -->
|
<!-- 左侧通知列表 -->
|
||||||
@@ -171,7 +173,7 @@ function shortDate(date: string) {
|
|||||||
<div class="total">
|
<div class="total">
|
||||||
<span>未读<span class="num">{{ page.unreadCount || 0 }}</span>条</span>
|
<span>未读<span class="num">{{ page.unreadCount || 0 }}</span>条</span>
|
||||||
<Tooltip content="一键已读" placement="top" transfer>
|
<Tooltip content="一键已读" placement="top" transfer>
|
||||||
<i class="iconfont icon-saoba clear-icon" @click="allRead"></i>
|
<SvgIcon class="clear-icon" name="read-all" :size="18" color="var(--text-3)" hover-color="var(--brand)" @click="allRead" />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
<div class="search-box">
|
<div class="search-box">
|
||||||
@@ -211,11 +213,11 @@ function shortDate(date: string) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="title-content">
|
<div class="title-content">
|
||||||
<p>来源:{{ detail.DeptName }}</p>
|
<p><span class="meta-label">来源</span>:{{ detail.DeptName }}</p>
|
||||||
<p>发布人:{{ detail.TeaName }}</p>
|
<p><span class="meta-label">发布人</span>:{{ detail.TeaName }}</p>
|
||||||
<p>发布时间:{{ detail.EditDate }}</p>
|
<p><span class="meta-label">发布时间</span>:{{ detail.EditDate }}</p>
|
||||||
<p>
|
<p>
|
||||||
<span>附件:{{ (detail.ChaoSend || []).length }}个</span>
|
<span class="meta-label">附件</span>:{{ (detail.ChaoSend || []).length }}个
|
||||||
<span class="to-file" v-if="detail.ChaoSend && detail.ChaoSend.length > 0" @click="downAll">全部下载</span>
|
<span class="to-file" v-if="detail.ChaoSend && detail.ChaoSend.length > 0" @click="downAll">全部下载</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -262,188 +264,280 @@ function shortDate(date: string) {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
/* 根容器 page-wrapper > main-container:内容区可视高度 = 100vh - 112(头部+面包屑) - 24(容器底padding) */
|
/* 已收到页 · 对齐原型 07-seam-pure.html 的 .nt-split(灰底上并排两张白卡)。
|
||||||
.accept-wrap {
|
通道把 .main-container 画成单张白卡,这里压平它、让两张卡直接坐灰底;
|
||||||
display: flex;
|
外层 .page-wrapper.yh-fill 由通道转 flex 纵向铺满,卡片 flex:1 撑满、四周留白 14/18/24 全站一致。
|
||||||
gap: 15px;
|
所有颜色/圆角/阴影一律走主题令牌,禁止硬编码。 */
|
||||||
height: calc(100vh - 136px);
|
.page-wrapper .main-container {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
padding: 0;
|
||||||
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
.accept-wrap {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 296px minmax(0, 1fr);
|
||||||
|
gap: 14px;
|
||||||
|
align-items: stretch;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 左:列表卡(原型 .nt-list) ---------- */
|
||||||
.title-list {
|
.title-list {
|
||||||
width: 360px;
|
|
||||||
flex: none;
|
|
||||||
background: #fff;
|
|
||||||
border: 1px solid #ebeef4;
|
|
||||||
border-radius: 3px;
|
|
||||||
padding: 14px 0;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
min-height: 0;
|
||||||
|
background: var(--panel);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: var(--r);
|
||||||
|
box-shadow: var(--sh);
|
||||||
.total {
|
.total {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0 15px 10px;
|
padding: 13px 16px 9px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-2);
|
||||||
.num {
|
.num {
|
||||||
padding: 0 5px;
|
padding: 0 2px;
|
||||||
color: #5c9edc;
|
color: var(--brand);
|
||||||
|
font-size: 15px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
.clear-icon {
|
.clear-icon {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 18px;
|
|
||||||
color: #999;
|
|
||||||
&:hover {
|
|
||||||
color: #2d8cf0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.search-box {
|
.search-box {
|
||||||
padding: 0 15px;
|
padding: 0 12px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
/* 让 iview 搜索框吃原型灰底胶囊观感(浅改,不动结构) */
|
||||||
|
:deep(.ivu-input-wrapper) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
:deep(.ivu-input) {
|
||||||
|
background: #f7f9fc;
|
||||||
|
border-color: transparent;
|
||||||
|
border-radius: var(--r-s);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
:deep(.ivu-input:hover) {
|
||||||
|
background: #f1f4f9;
|
||||||
|
border-color: transparent;
|
||||||
|
}
|
||||||
|
:deep(.ivu-input-wrapper-focused .ivu-input) {
|
||||||
|
background: #fff;
|
||||||
|
border-color: var(--brand);
|
||||||
|
box-shadow: 0 0 0 3px var(--brand-a);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.notice-items {
|
.notice-items {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
margin-top: 12px;
|
padding: 0 8px 12px;
|
||||||
.list_item {
|
.list_item {
|
||||||
padding: 10px 15px;
|
padding: 10px 12px;
|
||||||
cursor: pointer;
|
margin-bottom: 2px;
|
||||||
|
border-radius: var(--r-s);
|
||||||
border-left: 3px solid transparent;
|
border-left: 3px solid transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s, border-color 0.15s;
|
||||||
.tt {
|
.tt {
|
||||||
|
font-size: 13px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #272829;
|
color: var(--text);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
.other {
|
.other {
|
||||||
margin-top: 6px;
|
margin-top: 5px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
font-size: 12px;
|
font-size: 11.5px;
|
||||||
color: #999;
|
color: var(--text-3);
|
||||||
}
|
}
|
||||||
&:hover {
|
&:hover {
|
||||||
background: #f6f9ff;
|
background: #f7f9fc;
|
||||||
}
|
}
|
||||||
&.active {
|
&.active {
|
||||||
background: #ecf3ff;
|
background: var(--brand-soft);
|
||||||
border-left-color: #2d8cf0;
|
border-left-color: var(--brand);
|
||||||
|
.tt {
|
||||||
|
color: var(--brand);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
/* 已读弱化 */
|
/* 已读弱化 */
|
||||||
&.readed .tt {
|
&.readed .tt {
|
||||||
color: #8a94a6;
|
color: var(--text-2);
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.empty {
|
.empty {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: #999;
|
color: var(--text-3);
|
||||||
padding: 40px 0;
|
padding: 40px 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---------- 右:详情卡(原型 .nt-detail) ---------- */
|
||||||
.detail-content {
|
.detail-content {
|
||||||
flex: 1;
|
|
||||||
background: #fff;
|
|
||||||
border: 1px solid #ebeef4;
|
|
||||||
border-radius: 3px;
|
|
||||||
padding: 16px 20px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
min-height: 0;
|
||||||
|
background: var(--panel);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: var(--r);
|
||||||
|
box-shadow: var(--sh);
|
||||||
|
padding: 18px 22px 24px;
|
||||||
&.empty-detail {
|
&.empty-detail {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
color: #999;
|
color: var(--text-3);
|
||||||
}
|
}
|
||||||
.title-header {
|
.title-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: flex-start;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
.tt {
|
.tt {
|
||||||
font-size: 18px;
|
font-size: 17px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #272829;
|
letter-spacing: 0.2px;
|
||||||
|
color: var(--text);
|
||||||
}
|
}
|
||||||
.ops {
|
.ops {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 16px;
|
gap: 14px;
|
||||||
.op {
|
.op {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: #515a6e;
|
color: var(--text-2);
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 4px;
|
gap: 5px;
|
||||||
font-size: 13px;
|
font-size: 12.5px;
|
||||||
|
transition: color 0.15s;
|
||||||
&:hover {
|
&:hover {
|
||||||
color: #2d8cf0;
|
color: var(--brand);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.title-content {
|
.title-content {
|
||||||
margin-top: 10px;
|
margin-top: 14px;
|
||||||
padding: 8px 0;
|
padding-bottom: 16px;
|
||||||
border-bottom: 1px dashed #e8e8e8;
|
border-bottom: 1px dashed var(--line);
|
||||||
font-size: 13px;
|
display: grid;
|
||||||
color: #808695;
|
gap: 9px;
|
||||||
|
font-size: 12.5px;
|
||||||
|
color: var(--text-2);
|
||||||
p {
|
p {
|
||||||
margin: 4px 0;
|
margin: 0;
|
||||||
|
}
|
||||||
|
/* 标签两端撑开:冒号纵向对齐、值从同一列起(宽度取「发布时间」4 字) */
|
||||||
|
.meta-label {
|
||||||
|
display: inline-block;
|
||||||
|
width: 4em;
|
||||||
|
text-align: justify;
|
||||||
|
text-align-last: justify;
|
||||||
}
|
}
|
||||||
.to-file {
|
.to-file {
|
||||||
margin-left: 10px;
|
margin-left: 8px;
|
||||||
color: #2d8cf0;
|
color: var(--brand);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.notice-content {
|
.notice-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding-top: 10px;
|
padding-top: 14px;
|
||||||
|
.ql-editor {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
.file {
|
.file {
|
||||||
border-top: 1px solid #f0f0f0;
|
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: var(--r);
|
||||||
|
overflow: hidden;
|
||||||
.file-title {
|
.file-title {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
background: #f7f7f7;
|
align-items: center;
|
||||||
padding: 8px 12px;
|
padding: 9px 14px;
|
||||||
font-size: 13px;
|
background: #f7f9fc;
|
||||||
|
border-bottom: 1px solid var(--line);
|
||||||
|
font-size: 12.5px;
|
||||||
|
color: var(--text-2);
|
||||||
.all-down {
|
.all-down {
|
||||||
color: #2d8cf0;
|
color: var(--brand);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.file-list {
|
.file-list {
|
||||||
padding: 10px 12px;
|
padding: 2px 0;
|
||||||
.file-item {
|
.file-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
padding: 8px 6px;
|
padding: 11px 14px;
|
||||||
border-bottom: 1px dashed #f0f0f0;
|
transition: background 0.15s;
|
||||||
|
&:hover {
|
||||||
|
background: #fafbfe;
|
||||||
|
}
|
||||||
|
.left {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
.file-type-icon {
|
.file-type-icon {
|
||||||
font-size: 26px;
|
width: 30px;
|
||||||
color: #515a6e;
|
height: 30px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--brand-soft);
|
||||||
|
color: var(--brand);
|
||||||
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
.center {
|
.center {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: hidden;
|
min-width: 0;
|
||||||
.name {
|
.name {
|
||||||
color: #272829;
|
font-size: 12.5px;
|
||||||
font-size: 13px;
|
color: var(--text);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
.size {
|
.size {
|
||||||
font-size: 12px;
|
font-size: 11.5px;
|
||||||
color: #999;
|
color: var(--text-3);
|
||||||
|
margin-top: 2px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.right a {
|
.right {
|
||||||
margin-right: 8px;
|
display: flex;
|
||||||
color: #2d8cf0;
|
gap: 14px;
|
||||||
font-size: 13px;
|
flex: 0 0 auto;
|
||||||
|
a {
|
||||||
|
color: var(--brand);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12.5px;
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,273 @@
|
|||||||
|
<template>
|
||||||
|
<div class="page-wrapper yh-fill">
|
||||||
|
<div class="main-container yh-tabs-card">
|
||||||
|
<Tabs class="tabs-panel" v-model="tab" @on-click="load">
|
||||||
|
<TabPane label="我的待审核" name="todo" />
|
||||||
|
<TabPane label="办件视角" name="instances" />
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
<!-- 待审核列表 -->
|
||||||
|
<div class="tab-body" v-if="tab === 'todo'">
|
||||||
|
<div class="table-region" ref="todoRegionRef">
|
||||||
|
<Table :columns="todoCols" :data="todoRows" :height="todoH || undefined" :loading="loading" no-data-text="暂无待审核通知">
|
||||||
|
<template #opinion="{ row }">
|
||||||
|
<span class="opinion">{{ row._opinion }}</span>
|
||||||
|
</template>
|
||||||
|
<template #action="{ row }">
|
||||||
|
<span class="operating-link" @click="openEmbed(row)">查看详情</span>
|
||||||
|
<span class="operating-link" @click="approve(row)">通过</span>
|
||||||
|
<span class="operating-link text-error" @click="openReject(row)">驳回</span>
|
||||||
|
</template>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
<div class="page-panel" v-if="todoPage.total > todoPage.pageSize">
|
||||||
|
<Page class="page-box fr" :current="todoPage.pageIndex" :page-size="todoPage.pageSize"
|
||||||
|
:total="todoPage.total" show-total @on-change="loadTodo" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 办件视角 -->
|
||||||
|
<div class="tab-body" v-else>
|
||||||
|
<div class="filter-bar">
|
||||||
|
<RadioGroup v-model="instStatus" type="button" @on-change="loadInstances(1)">
|
||||||
|
<Radio :label="0">全部</Radio>
|
||||||
|
<Radio :label="10">审批中</Radio>
|
||||||
|
<Radio :label="20">已完成</Radio>
|
||||||
|
<Radio :label="30">已拒绝</Radio>
|
||||||
|
<Radio :label="40">已撤回</Radio>
|
||||||
|
</RadioGroup>
|
||||||
|
</div>
|
||||||
|
<div class="table-region" ref="instRegionRef">
|
||||||
|
<Table :columns="instCols" :data="instRows" :height="instH || undefined" :loading="loading" no-data-text="暂无办件">
|
||||||
|
<template #status="{ row }">
|
||||||
|
<Tag :color="instStatusColor(row.status)">{{ instStatusText(row.status) }}</Tag>
|
||||||
|
</template>
|
||||||
|
<template #instAction="{ row }">
|
||||||
|
<Button size="small" @click="openEmbed(row)">审批记录</Button>
|
||||||
|
</template>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
<div class="page-panel" v-if="instPage.total > instPage.pageSize">
|
||||||
|
<Page class="page-box fr" :current="instPage.pageIndex" :page-size="instPage.pageSize"
|
||||||
|
:total="instPage.total" show-total @on-change="loadInstances" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 驳回弹窗:意见必填 -->
|
||||||
|
<Modal
|
||||||
|
v-model="rejectVisible"
|
||||||
|
title="驳回审核"
|
||||||
|
:loading="acting"
|
||||||
|
@on-ok="doReject"
|
||||||
|
@on-cancel="rejectTaskId = null"
|
||||||
|
>
|
||||||
|
<p style="margin-bottom: 8px">
|
||||||
|
驳回「<b>{{ rejectTitle }}</b
|
||||||
|
>」,必须填写意见:
|
||||||
|
</p>
|
||||||
|
<Input v-model="rejectOpinion" type="textarea" :rows="3" placeholder="驳回意见(必填)" />
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<!-- 审批记录 Drawer(flow-center 嵌入页 iframe 撑满:状态/基本信息/流程示意图/轨迹/审批流水/表单六块) -->
|
||||||
|
<Drawer v-model="embedVisible" title="审批记录" :width="740" :transfer="false" :styles="{ body: { padding: '0' } }">
|
||||||
|
<div class="embed-frame-wrap">
|
||||||
|
<iframe v-if="embedPageUrl" :src="embedPageUrl" title="办件进度"></iframe>
|
||||||
|
</div>
|
||||||
|
</Drawer>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted, ref } from "vue";
|
||||||
|
import { Message } from "view-ui-plus";
|
||||||
|
import { useElementHeight } from "@/utils/hook/useElementHeight";
|
||||||
|
import approvalApi, {
|
||||||
|
type ApprovalInstance,
|
||||||
|
type ApprovalTodoTask,
|
||||||
|
} from "@/api/module/approval";
|
||||||
|
import { noticeApi } from "@/api/module/notice";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 我的待审核(应用内审批,第三步):待办列表 + 办件视角,通过/驳回走本应用后端代理
|
||||||
|
* (operator=登录态,对端按任务办理人复核;非办理人会收到业务错提示)。
|
||||||
|
* bizSnapshot 是发起时透传的 JSON 摘要,解析失败只展示标题。
|
||||||
|
*/
|
||||||
|
const tab = ref<"todo" | "instances">("todo");
|
||||||
|
const loading = ref(false);
|
||||||
|
const acting = ref(false);
|
||||||
|
|
||||||
|
/* 两个表格高度:实测各自 .table-region(flex:1),替代写死常量 */
|
||||||
|
const todoRegionRef = ref<HTMLElement | null>(null);
|
||||||
|
const instRegionRef = ref<HTMLElement | null>(null);
|
||||||
|
const todoH = useElementHeight(todoRegionRef);
|
||||||
|
const instH = useElementHeight(instRegionRef);
|
||||||
|
|
||||||
|
const todoRows = ref<(ApprovalTodoTask & { _summary: string })[]>([]);
|
||||||
|
const todoPage = ref({ total: 0, pageIndex: 1, pageSize: 10 });
|
||||||
|
|
||||||
|
const instRows = ref<ApprovalInstance[]>([]);
|
||||||
|
const instPage = ref({ total: 0, pageIndex: 1, pageSize: 10 });
|
||||||
|
const instStatus = ref(0);
|
||||||
|
|
||||||
|
const rejectVisible = ref(false);
|
||||||
|
const rejectTaskId = ref<number | null>(null);
|
||||||
|
const rejectTitle = ref("");
|
||||||
|
const rejectOpinion = ref("");
|
||||||
|
|
||||||
|
const todoCols = [
|
||||||
|
{ title: "通知", key: "bizTitle", minWidth: 220, ellipsis: true, tooltip: true },
|
||||||
|
{ title: "摘要", key: "_summary", minWidth: 200, ellipsis: true, tooltip: true },
|
||||||
|
{ title: "当前节点", key: "nodeName", width: 110 },
|
||||||
|
{ title: "到达时间", key: "arriveTime", width: 170, render: fmtTime },
|
||||||
|
{ title: "操作", slot: "action", width: 190, align: "center" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const instCols = [
|
||||||
|
{ title: "通知", key: "bizTitle", minWidth: 240, ellipsis: true, tooltip: true },
|
||||||
|
{ title: "状态", slot: "status", width: 100 },
|
||||||
|
{ title: "提交人", key: "submitterName", width: 100 },
|
||||||
|
{ title: "提交时间", key: "submitTime", width: 170, render: fmtTime },
|
||||||
|
{ title: "办结时间", key: "finishTime", width: 170, render: fmtTime },
|
||||||
|
{ title: "操作", slot: "instAction", width: 100 },
|
||||||
|
];
|
||||||
|
|
||||||
|
/* 审批记录 Drawer(嵌入页 iframe):pageUrl 含 10 分钟短时 token,每次打开现取,
|
||||||
|
* 不缓存、不落 storage(既有安全纪律);业务错(尚未进入流程/无权限 403)原文提示,不打断列表 */
|
||||||
|
const embedVisible = ref(false);
|
||||||
|
const embedPageUrl = ref("");
|
||||||
|
|
||||||
|
async function openEmbed(row: ApprovalInstance | ApprovalTodoTask) {
|
||||||
|
try {
|
||||||
|
const res = await noticeApi.flowEmbed(Number(row.bizId)).send();
|
||||||
|
embedPageUrl.value = res?.pageUrl || "";
|
||||||
|
if (!embedPageUrl.value) {
|
||||||
|
Message.error("审批记录地址获取失败");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
embedVisible.value = true;
|
||||||
|
} catch (e: any) {
|
||||||
|
Message.error(e?.message || "审批记录打开失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function fmtTime(h: any, p: any) {
|
||||||
|
return h(
|
||||||
|
"span",
|
||||||
|
{},
|
||||||
|
String(p.row[p.column.key] || "").replace("T", " ").slice(0, 19) || "—"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseSummary(task: ApprovalTodoTask): string {
|
||||||
|
if (!task.bizSnapshot) return "—";
|
||||||
|
try {
|
||||||
|
const snap = JSON.parse(task.bizSnapshot);
|
||||||
|
return snap.summary || snap.title || "—";
|
||||||
|
} catch {
|
||||||
|
return "—";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadTodo(page = 1) {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const res = await approvalApi.todo(page, todoPage.value.pageSize).send();
|
||||||
|
todoPage.value = res;
|
||||||
|
todoRows.value = (res.content || []).map((t) => ({ ...t, _summary: parseSummary(t) }));
|
||||||
|
} catch (e: any) {
|
||||||
|
Message.error(e?.message || "待审核列表加载失败");
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadInstances(page = 1) {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const res = await approvalApi
|
||||||
|
.instances(instStatus.value || undefined, page, instPage.value.pageSize)
|
||||||
|
.send();
|
||||||
|
instPage.value = res;
|
||||||
|
instRows.value = res.content || [];
|
||||||
|
} catch (e: any) {
|
||||||
|
Message.error(e?.message || "办件列表加载失败");
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function load() {
|
||||||
|
if (tab.value === "todo") {
|
||||||
|
loadTodo(1);
|
||||||
|
} else {
|
||||||
|
loadInstances(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function approve(row: ApprovalTodoTask) {
|
||||||
|
if (acting.value) return; // 链接式操作无 :loading 反馈,用 acting 拦截连点重复提交
|
||||||
|
acting.value = true;
|
||||||
|
try {
|
||||||
|
await approvalApi.approve(row.taskId).send();
|
||||||
|
Message.success("已通过");
|
||||||
|
loadTodo(todoPage.value.pageIndex);
|
||||||
|
} catch (e: any) {
|
||||||
|
// 非办理人/已被办理等业务错,后端信封 msg 直接展示
|
||||||
|
Message.error(e?.message || "操作失败");
|
||||||
|
} finally {
|
||||||
|
acting.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openReject(row: ApprovalTodoTask) {
|
||||||
|
rejectTaskId.value = row.taskId;
|
||||||
|
rejectTitle.value = row.bizTitle;
|
||||||
|
rejectOpinion.value = "";
|
||||||
|
rejectVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doReject() {
|
||||||
|
if (!rejectOpinion.value.trim()) {
|
||||||
|
Message.warning("驳回必须填写意见");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
acting.value = true;
|
||||||
|
try {
|
||||||
|
await approvalApi.reject(rejectTaskId.value!, rejectOpinion.value.trim()).send();
|
||||||
|
Message.success("已驳回");
|
||||||
|
rejectVisible.value = false;
|
||||||
|
loadTodo(todoPage.value.pageIndex);
|
||||||
|
} catch (e: any) {
|
||||||
|
Message.error(e?.message || "操作失败");
|
||||||
|
} finally {
|
||||||
|
acting.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function instStatusText(s: number) {
|
||||||
|
return ({ 10: "审批中", 20: "已完成", 30: "已拒绝", 40: "已撤回" } as Record<number, string>)[s] || String(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
function instStatusColor(s: number) {
|
||||||
|
return ({ 10: "blue", 20: "green", 30: "red", 40: "default" } as Record<number, string>)[s] || "default";
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(load);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.filter-bar {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
/* 审批记录 Drawer:iframe 撑满,嵌入页内部自己滚动 */
|
||||||
|
.embed-frame-wrap {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.embed-frame-wrap iframe {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
<template>
|
||||||
|
<div class="page-wrapper yh-fill">
|
||||||
|
<div class="main-container">
|
||||||
|
<TablePageAlovaNew ref="listRef" :data-api="fetchPage" :columns="cols">
|
||||||
|
<template #title="{ row }">
|
||||||
|
<a v-if="row.title" class="notice-link" @click="openDetail(row)">{{ row.title }}</a>
|
||||||
|
<span v-else class="notice-gone">通知已删除</span>
|
||||||
|
</template>
|
||||||
|
<template #status="{ row }">
|
||||||
|
<Tag v-if="row.auditStatusDesc && row.auditStatusDesc !== '通知已删除'"
|
||||||
|
:color="statusColor(row.auditStatus)">{{ row.auditStatusDesc }}</Tag>
|
||||||
|
<Tag v-else-if="row.noticeStatus === 1" color="green">已发布</Tag>
|
||||||
|
<Tag v-else>—</Tag>
|
||||||
|
</template>
|
||||||
|
</TablePageAlovaNew>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from "vue";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
import TablePageAlovaNew from "@/components/TablePageAlovaNew.vue";
|
||||||
|
import ccApi, { type CcItem } from "@/api/module/cc";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 抄送给我的(2026-09-24):流程 NOTIFY 节点激活即抄送(审批过程中就出现在列表里,
|
||||||
|
* 此时通知未发布——被抄送人可见由 NoticeFlowGuard 第 4 条放行)。点击标题跳通知详情;
|
||||||
|
* 编辑视角(type=0)不标读:未发布通知的阅读统计无意义。
|
||||||
|
*/
|
||||||
|
const router = useRouter();
|
||||||
|
const listRef = ref<InstanceType<typeof TablePageAlovaNew> | null>(null);
|
||||||
|
// usePagination 的 handler 必须返回 Method(组件内部统一 send 并吃全局响应拦截),返回 Promise 会卡在 loading
|
||||||
|
const fetchPage = (p: { pageIndex: number; pageSize: number }) => ccApi.my(p.pageIndex, p.pageSize);
|
||||||
|
|
||||||
|
const cols = [
|
||||||
|
{ title: "通知标题", slot: "title", minWidth: 260, ellipsis: true, tooltip: true },
|
||||||
|
{ title: "来源部门", key: "deptName", width: 140 },
|
||||||
|
{ title: "抄送环节", key: "nodeName", width: 140 },
|
||||||
|
{ title: "抄送时间", key: "sentTime", width: 170, render: fmtTime },
|
||||||
|
{ title: "状态", slot: "status", width: 100 },
|
||||||
|
];
|
||||||
|
|
||||||
|
function fmtTime(h: any, p: any) {
|
||||||
|
return h(
|
||||||
|
"span",
|
||||||
|
{},
|
||||||
|
String(p.row[p.column.key] || "").replace("T", " ").slice(0, 19) || "—"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function statusColor(auditStatus: number | null) {
|
||||||
|
return ({ 0: "default", 10: "blue", 20: "green", 30: "red", 40: "orange" } as Record<number, string>)[
|
||||||
|
auditStatus ?? -1
|
||||||
|
] || "default";
|
||||||
|
}
|
||||||
|
|
||||||
|
function openDetail(row: CcItem) {
|
||||||
|
router.push({ path: "/list/detail", query: { serid: row.noticeId, type: 0 } });
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.notice-link {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notice-gone {
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -3,9 +3,11 @@ 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 { fitDiagram } from "@/utils/diagramImg";
|
||||||
|
import config from "@/assets/config/host.config";
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -16,12 +18,98 @@ const viewType = computed(() => {
|
|||||||
const t = Number(route.query.type);
|
const t = Number(route.query.type);
|
||||||
return t === 0 ? 0 : 1;
|
return t === 0 ? 0 : 1;
|
||||||
});
|
});
|
||||||
const fromPath = computed(() => (route.query.from as string) || "/accept");
|
|
||||||
/* 上一/下一篇的序列类型:管理视角走已发(type=0),查看走已收(type=1) */
|
/* 上一/下一篇的序列类型:管理视角走已发(type=0),查看走已收(type=1) */
|
||||||
const navType = computed(() => (viewType.value === 0 ? "0" : "1"));
|
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; // 进度不可用时只展示本地镜像
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 撤回:走本应用后端(2026-09-17 接入:后端经 SDK 托管件中转并复核创建人,
|
||||||
|
* 不再前端直调 flow-center——跨应用 CAS 票据 + CORS 双重外溢且"谁能撤"落在前端)。
|
||||||
|
* 催办:仍是"人"的动作,走 flow-center 用户面(同主域 CAS 免二次登录)。
|
||||||
|
*/
|
||||||
|
async function flowAction(action: "withdraw" | "urge") {
|
||||||
|
const instanceId = detail.value?.flowInstanceId;
|
||||||
|
if (!instanceId) return;
|
||||||
|
flowActing.value = true;
|
||||||
|
try {
|
||||||
|
if (action === "withdraw") {
|
||||||
|
await noticeApi.flowWithdraw(serid.value).send();
|
||||||
|
Message.success("已撤回审核,可编辑后重新提交");
|
||||||
|
await load();
|
||||||
|
await loadTrace();
|
||||||
|
} else {
|
||||||
|
if (!config.flowHost) return;
|
||||||
|
const resp = await fetch(`${config.flowHost}/api/flow/instance/${instanceId}/urge`, {
|
||||||
|
method: "POST",
|
||||||
|
credentials: "include",
|
||||||
|
});
|
||||||
|
const res = await resp.json().catch(() => null);
|
||||||
|
if (resp.ok && res && res.code === 0) {
|
||||||
|
Message.success("已发送催办提醒");
|
||||||
|
} else {
|
||||||
|
Message.error(res?.msg || "催办失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
Message.error(
|
||||||
|
action === "withdraw"
|
||||||
|
? "撤回失败(已有人办理时不可撤回):" + (e?.msg || e?.message || e)
|
||||||
|
: "流程中心不可达:" + (e?.message || e),
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
flowActing.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 流程图弹窗(2026-09-17:后端同源出图 /flow/Diagram,img 免 token 免跨域,不再深链 flow-center) */
|
||||||
|
const diagramVisible = ref(false);
|
||||||
|
const diagramEl = ref<HTMLImageElement | null>(null);
|
||||||
|
function openFlowDiagram() {
|
||||||
|
if (!detail.value?.flowGuid || !detail.value?.flowInstanceId) return;
|
||||||
|
diagramVisible.value = true;
|
||||||
|
}
|
||||||
|
const diagramSrc = computed(() =>
|
||||||
|
diagramVisible.value && detail.value?.flowInstanceId
|
||||||
|
? `${config.serviceHost}/api/notice/flow/Diagram?serid=${serid.value}`
|
||||||
|
: "",
|
||||||
|
);
|
||||||
|
function onDiagramError() {
|
||||||
|
diagramVisible.value = false;
|
||||||
|
Message.error("流程图加载失败(可能已过期,请刷新页面重试)");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 办件进度嵌入页(iframe 免签嵌入 flow-center 统一进度页):每次打开现取 pageUrl——
|
||||||
|
* token 10 分钟短时效,不缓存、不落 localStorage 复用;嵌入页内部自己滚动 */
|
||||||
|
const embedVisible = ref(false);
|
||||||
|
const embedPageUrl = ref("");
|
||||||
|
async function openFlowEmbed() {
|
||||||
|
if (!detail.value?.flowInstanceId) return;
|
||||||
|
embedVisible.value = true;
|
||||||
|
embedPageUrl.value = "";
|
||||||
|
try {
|
||||||
|
const res = await noticeApi.flowEmbed(serid.value).send();
|
||||||
|
embedPageUrl.value = res?.pageUrl || "";
|
||||||
|
} catch (e: any) {
|
||||||
|
embedVisible.value = false;
|
||||||
|
Message.error(e?.msg || e?.message || "嵌入页地址获取失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const readGroups = ref<ReadGroup[]>([]);
|
const readGroups = ref<ReadGroup[]>([]);
|
||||||
const showRead = ref(false);
|
const showRead = ref(false);
|
||||||
const prevId = ref(0);
|
const prevId = ref(0);
|
||||||
@@ -29,6 +117,7 @@ const nextId = ref(0);
|
|||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await load();
|
await load();
|
||||||
|
loadTrace(); // 详情加载后并行取审批进度(未绑流程接口自身返回 bound=false)
|
||||||
});
|
});
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
@@ -39,6 +128,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 {
|
||||||
@@ -98,6 +188,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);
|
||||||
@@ -110,7 +209,7 @@ function downAll() {
|
|||||||
function displayName(item: { name?: string; filename?: string }) {
|
function displayName(item: { name?: string; filename?: string }) {
|
||||||
return item.name || item.filename || "";
|
return item.name || item.filename || "";
|
||||||
}
|
}
|
||||||
/* 附件图标(按扩展名,图标名对齐旧版 matchSuffix → icon-word/excel/ppt/pdf1/txt/tupian1/yasuo/weizhi) */
|
/* 附件图标(按扩展名,图标名对齐旧版 matchSuffix → icon-word/excel/ppt/pdf1/txt/tupian/yasuo/weizhi) */
|
||||||
function getFileIcon(name: string) {
|
function getFileIcon(name: string) {
|
||||||
const suffix = String(name || "").split(".").pop()?.toLowerCase() || "";
|
const suffix = String(name || "").split(".").pop()?.toLowerCase() || "";
|
||||||
if (["doc", "docx", "wps"].includes(suffix)) return "icon-word";
|
if (["doc", "docx", "wps"].includes(suffix)) return "icon-word";
|
||||||
@@ -118,7 +217,8 @@ function getFileIcon(name: string) {
|
|||||||
if (["ppt", "pptx"].includes(suffix)) return "icon-ppt";
|
if (["ppt", "pptx"].includes(suffix)) return "icon-ppt";
|
||||||
if (["pdf"].includes(suffix)) return "icon-pdf1";
|
if (["pdf"].includes(suffix)) return "icon-pdf1";
|
||||||
if (["txt"].includes(suffix)) return "icon-txt";
|
if (["txt"].includes(suffix)) return "icon-txt";
|
||||||
if (["jpg", "jpeg", "png", "gif", "bmp"].includes(suffix)) return "icon-tupian1";
|
// 平台 icons.css 的 icon-tupian 是线性「图片」图标;icon-tupian1 是旧彩色图转单色,看不出是图片
|
||||||
|
if (["jpg", "jpeg", "png", "gif", "bmp"].includes(suffix)) return "icon-tupian";
|
||||||
if (["zip", "rar"].includes(suffix)) return "icon-yasuo";
|
if (["zip", "rar"].includes(suffix)) return "icon-yasuo";
|
||||||
return "icon-weizhi";
|
return "icon-weizhi";
|
||||||
}
|
}
|
||||||
@@ -140,16 +240,12 @@ function openPreview(item: { name?: string; filename?: string; fileid?: number;
|
|||||||
fileName: displayName(item),
|
fileName: displayName(item),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
function back() {
|
|
||||||
router.push(fromPath.value);
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="page-wrapper">
|
<div class="page-wrapper">
|
||||||
<div class="main-container" v-if="detail">
|
<div class="main-container" v-if="detail">
|
||||||
<div class="ops-top">
|
<div class="ops-top">
|
||||||
<Button icon="ios-arrow-back" @click="back">返回</Button>
|
|
||||||
<div class="nav">
|
<div class="nav">
|
||||||
<Button size="small" :disabled="!prevId" @click="goNav(prevId)">上一篇</Button>
|
<Button size="small" :disabled="!prevId" @click="goNav(prevId)">上一篇</Button>
|
||||||
<Button size="small" :disabled="!nextId" @click="goNav(nextId)">下一篇</Button>
|
<Button size="small" :disabled="!nextId" @click="goNav(nextId)">下一篇</Button>
|
||||||
@@ -181,6 +277,88 @@ function back() {
|
|||||||
<!-- 查看视角:普通标题 -->
|
<!-- 查看视角:普通标题 -->
|
||||||
<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"
|
||||||
|
size="small"
|
||||||
|
style="margin-left: 8px"
|
||||||
|
@click="openFlowEmbed"
|
||||||
|
>审批进度</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>
|
||||||
|
<!-- 结构化步骤时间线(2026-09-17 接入 SDK progress;steps[0] 固定"发起申请",同节点多任务每人一行) -->
|
||||||
|
<div v-if="flowTrace.steps && flowTrace.steps.length" class="flow-timeline">
|
||||||
|
<div
|
||||||
|
v-for="(s, i) in flowTrace.steps"
|
||||||
|
:key="'fs' + i"
|
||||||
|
class="flow-step"
|
||||||
|
:class="{ 'flow-step-active': s.active }"
|
||||||
|
>
|
||||||
|
<span class="flow-step-node">{{ s.label || s.nodeName || `节点${i + 1}` }}</span>
|
||||||
|
<template v-if="s.assignees && s.assignees.length">
|
||||||
|
<span v-for="(a, j) in s.assignees" :key="j" class="flow-step-user">
|
||||||
|
{{ a.userName || "" }}
|
||||||
|
<em v-if="a.statusLabel" class="flow-step-status">{{ a.statusLabel }}</em>
|
||||||
|
<em v-if="a.opinion" class="flow-step-op">({{ a.opinion }})</em>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 节点时间线(traceJson 解析,steps 不可用时的兜底) -->
|
||||||
|
<div v-else-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>
|
||||||
|
|
||||||
|
<!-- 流程图弹窗(后端同源出图,免 token 免跨域) -->
|
||||||
|
<Modal v-model="diagramVisible" title="流程示意图" :width="860" footer-hide>
|
||||||
|
<div class="diagram-wrap">
|
||||||
|
<img v-if="diagramSrc" ref="diagramEl" :src="diagramSrc" alt="流程示意图" @load="fitDiagram(diagramEl)" @error="onDiagramError" />
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<!-- 办件进度嵌入页(Drawer 内 iframe 撑满,嵌入页内部滚动;token 每次打开现取) -->
|
||||||
|
<Drawer v-model="embedVisible" title="审批进度" :width="740" :transfer="false" :styles="{ body: { padding: '0' } }">
|
||||||
|
<div class="embed-frame-wrap">
|
||||||
|
<iframe v-if="embedPageUrl" :src="embedPageUrl" title="办件进度"></iframe>
|
||||||
|
</div>
|
||||||
|
</Drawer>
|
||||||
|
|
||||||
<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>
|
||||||
@@ -201,9 +379,12 @@ function back() {
|
|||||||
</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>
|
||||||
|
|
||||||
@@ -238,6 +419,19 @@ function back() {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
/* 高度模型(2026-10-10 定稿):卡片撑满视口(与全站其他页一致),但必须能"随长文生长"。
|
||||||
|
不能直接用通道 .yh-fill:其卡片是 flex:1(basis:0)+ min-height:420px,卡片被钉在容器高,
|
||||||
|
而 .main-container 带 overflow:hidden(base.layout.css:222)→ 长文被裁掉且滚不到(最小复现已验证)。
|
||||||
|
故此处 page-wrapper 转 flex 纵向(全局已给 min-height:100%),卡片 flex:1 0 auto(basis:auto)
|
||||||
|
→ 短文撑满、长文随内容长高、滚动留在 main-wrapper 整页滚。 */
|
||||||
|
.page-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.page-wrapper > .main-container {
|
||||||
|
flex: 1 0 auto;
|
||||||
|
min-height: 420px;
|
||||||
|
}
|
||||||
.ops-top {
|
.ops-top {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@@ -249,16 +443,110 @@ function back() {
|
|||||||
}
|
}
|
||||||
.notice-title2 {
|
.notice-title2 {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text);
|
||||||
margin: 0 0 12px;
|
margin: 0 0 12px;
|
||||||
}
|
}
|
||||||
|
.flow-card {
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: var(--r-s);
|
||||||
|
padding: 10px 14px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
background: #f7f9fc;
|
||||||
|
}
|
||||||
|
.flow-card-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.flow-title {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
.flow-nodes {
|
||||||
|
color: var(--text-3);
|
||||||
|
}
|
||||||
|
.flow-ops {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
.flow-opinion {
|
||||||
|
margin-top: 8px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
background: var(--panel);
|
||||||
|
border-radius: var(--r-s);
|
||||||
|
color: var(--red);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.flow-timeline {
|
||||||
|
margin-top: 8px;
|
||||||
|
background: var(--panel);
|
||||||
|
border-radius: var(--r-s);
|
||||||
|
padding: 6px 10px;
|
||||||
|
}
|
||||||
|
.flow-step {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-2);
|
||||||
|
padding: 3px 0;
|
||||||
|
border-bottom: 1px dashed var(--line);
|
||||||
|
}
|
||||||
|
.flow-step:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
.flow-step-node {
|
||||||
|
min-width: 120px;
|
||||||
|
}
|
||||||
|
.flow-step-user {
|
||||||
|
min-width: 90px;
|
||||||
|
color: var(--brand);
|
||||||
|
}
|
||||||
|
.flow-step-time {
|
||||||
|
color: var(--text-3);
|
||||||
|
}
|
||||||
|
.flow-step-op {
|
||||||
|
color: var(--red);
|
||||||
|
}
|
||||||
|
.flow-step-active .flow-step-node {
|
||||||
|
color: var(--brand);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.flow-step-status {
|
||||||
|
font-style: normal;
|
||||||
|
color: var(--text-3);
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
.flow-step .flow-step-op {
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
.diagram-wrap {
|
||||||
|
text-align: center;
|
||||||
|
min-height: 200px;
|
||||||
|
}
|
||||||
|
.diagram-wrap img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
/* 嵌入页 iframe 撑满 Drawer body(body padding 已由 :styles 置 0),嵌入页内部滚动 */
|
||||||
|
.embed-frame-wrap {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.embed-frame-wrap iframe {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
.meta {
|
.meta {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 18px;
|
gap: 18px;
|
||||||
padding: 8px 0;
|
padding: 8px 0;
|
||||||
border-bottom: 1px dashed #e8e8e8;
|
border-bottom: 1px dashed var(--line);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: #808695;
|
color: var(--text-3);
|
||||||
}
|
}
|
||||||
/* 管理视角统计条 */
|
/* 管理视角统计条 */
|
||||||
.stat-bar {
|
.stat-bar {
|
||||||
@@ -267,7 +555,7 @@ function back() {
|
|||||||
gap: 24px;
|
gap: 24px;
|
||||||
padding: 4px 0;
|
padding: 4px 0;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: #17233d;
|
color: var(--text);
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
.progress-box {
|
.progress-box {
|
||||||
@@ -277,7 +565,7 @@ function back() {
|
|||||||
width: 300px;
|
width: 300px;
|
||||||
}
|
}
|
||||||
.percent-label {
|
.percent-label {
|
||||||
color: #f60;
|
color: var(--brand);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
@@ -288,18 +576,15 @@ function back() {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.stat-nums:hover .item {
|
|
||||||
color: #2d8cf0;
|
|
||||||
}
|
|
||||||
.stat-nums .item {
|
.stat-nums .item {
|
||||||
color: #8a94a6;
|
color: var(--text-3);
|
||||||
}
|
}
|
||||||
.stat-nums b {
|
.stat-nums b {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
margin: 0 2px;
|
margin: 0 2px;
|
||||||
}
|
}
|
||||||
.stat-nums .blue {
|
.stat-nums .blue {
|
||||||
color: #5c9edc;
|
color: var(--brand);
|
||||||
}
|
}
|
||||||
.stat-nums .orange {
|
.stat-nums .orange {
|
||||||
color: #f60;
|
color: #f60;
|
||||||
@@ -310,66 +595,70 @@ function back() {
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
.divider {
|
.divider {
|
||||||
border-bottom: 1px dashed #e8e8e8;
|
border-bottom: 1px dashed var(--line);
|
||||||
margin: 8px 0;
|
margin: 8px 0;
|
||||||
}
|
}
|
||||||
.content {
|
.content {
|
||||||
padding: 16px 0;
|
padding: 16px 0;
|
||||||
|
/* 正文区最小高度:短通知别让附件/按钮紧贴一行字(旧版用 clientHeight-356 铺满视口,
|
||||||
|
会把附件顶出首屏;这里只留"呼吸区",剩余空白落在按钮下方的卡片底部) */
|
||||||
|
min-height: 200px;
|
||||||
line-height: 1.8;
|
line-height: 1.8;
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
|
color: var(--text);
|
||||||
}
|
}
|
||||||
.files {
|
.files {
|
||||||
border-top: 1px dashed #e8e8e8;
|
border-top: 1px dashed var(--line);
|
||||||
padding-top: 12px;
|
padding-top: 12px;
|
||||||
}
|
}
|
||||||
.files-title {
|
.files-title {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: #808695;
|
color: var(--text-3);
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
.down-all {
|
.down-all {
|
||||||
margin-left: 12px;
|
margin-left: 12px;
|
||||||
color: #2d8cf0;
|
color: var(--brand);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.file-item {
|
.file-item {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
color: #515a6e;
|
color: var(--text-2);
|
||||||
margin: 0 16px 8px 0;
|
margin: 0 16px 8px 0;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
.file-type-icon {
|
.file-type-icon {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
color: #515a6e;
|
color: var(--text-2);
|
||||||
}
|
}
|
||||||
.file-name {
|
.file-name {
|
||||||
max-width: 320px;
|
max-width: 320px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
.file-size {
|
.file-size {
|
||||||
color: #999;
|
color: var(--text-3);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
.file-op {
|
.file-op {
|
||||||
color: #2d8cf0;
|
color: var(--brand);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.bottom-btns {
|
.bottom-btns {
|
||||||
border-top: 1px dashed #e8e8e8;
|
border-top: 1px dashed var(--line);
|
||||||
padding-top: 14px;
|
padding-top: 14px;
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
/* 阅读情况弹窗 */
|
/* 阅读情况弹窗 */
|
||||||
.read-tip {
|
.read-tip {
|
||||||
background: #e8f4ff;
|
background: var(--brand-soft);
|
||||||
border: 1px solid #d3e9ff;
|
border: 1px solid var(--line);
|
||||||
color: #515a6e;
|
color: var(--text-2);
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
border-radius: 4px;
|
border-radius: var(--r-s);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
@@ -386,7 +675,7 @@ function back() {
|
|||||||
flex: none;
|
flex: none;
|
||||||
width: 130px;
|
width: 130px;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
color: #515a6e;
|
color: var(--text-2);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
line-height: 22px;
|
line-height: 22px;
|
||||||
}
|
}
|
||||||
@@ -402,13 +691,13 @@ function back() {
|
|||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
.green {
|
.green {
|
||||||
color: #19be6b;
|
color: var(--green);
|
||||||
}
|
}
|
||||||
.red {
|
.red {
|
||||||
color: #ed4014;
|
color: var(--red);
|
||||||
}
|
}
|
||||||
.empty {
|
.empty {
|
||||||
color: #999;
|
color: var(--text-3);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted, watch, h } from "vue";
|
import { ref, computed, onMounted, watch, h, resolveComponent } 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 } from "view-ui-plus";
|
||||||
@@ -7,6 +7,7 @@ import { noticeApi } from "@/api/module/notice";
|
|||||||
import type { NoticeVo, NoticeQuery } from "@/types/notice";
|
import type { NoticeVo, NoticeQuery } from "@/types/notice";
|
||||||
import MyConfirm from "@/components/MyConfirm";
|
import MyConfirm from "@/components/MyConfirm";
|
||||||
import Utils from "@/utils";
|
import Utils from "@/utils";
|
||||||
|
import { useElementHeight } from "@/utils/hook/useElementHeight";
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -17,13 +18,25 @@ const isManage = computed(() => route.path === "/noticelist");
|
|||||||
/* ============ 搜索条件(规范 §1:日期 fl + 下拉 fl + 关键字 fr 宽200 即搜) ============ */
|
/* ============ 搜索条件(规范 §1:日期 fl + 下拉 fl + 关键字 fr 宽200 即搜) ============ */
|
||||||
const keyword = ref("");
|
const keyword = ref("");
|
||||||
const deptType = ref("");
|
const deptType = ref("");
|
||||||
|
const auditType = ref<number | "">(""); // 状态筛选(audit_status;-1=无需审核)
|
||||||
const startDt = ref("");
|
const startDt = ref("");
|
||||||
const endDt = ref("");
|
const endDt = ref("");
|
||||||
const { data: deptOptions } = useRequest(noticeApi.getSysDic());
|
const { data: deptOptions } = useRequest(noticeApi.getSysDic());
|
||||||
|
|
||||||
function fmtDate(d: Date) {
|
/** 状态筛选选项(与列表「审核状态」列取值一一对应;-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 | string) {
|
||||||
|
const dt = new Date(d);
|
||||||
const pad = (n: number) => String(n).padStart(2, "0");
|
const pad = (n: number) => String(n).padStart(2, "0");
|
||||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
return `${dt.getFullYear()}-${pad(dt.getMonth() + 1)}-${pad(dt.getDate())}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ============ 分页(规范 §3:customPageCount 页长记忆) ============ */
|
/* ============ 分页(规范 §3:customPageCount 页长记忆) ============ */
|
||||||
@@ -43,8 +56,10 @@ async function run() {
|
|||||||
pageSize: pageSize.value,
|
pageSize: pageSize.value,
|
||||||
orderByTime: "1",
|
orderByTime: "1",
|
||||||
};
|
};
|
||||||
if (startDt.value) params.startTime = startDt.value;
|
// DatePicker 选过日期后 v-model 值是 Date 对象,后端 LocalDate 只认 yyyy-MM-dd → 统一格式化
|
||||||
if (endDt.value) params.endTime = endDt.value;
|
if (startDt.value) params.startTime = fmtDate(new Date(startDt.value));
|
||||||
|
if (endDt.value) params.endTime = fmtDate(new Date(endDt.value));
|
||||||
|
if (auditType.value !== "" && auditType.value !== null) params.auditStatus = Number(auditType.value);
|
||||||
if (!isManage.value) {
|
if (!isManage.value) {
|
||||||
params.scope = "user";
|
params.scope = "user";
|
||||||
}
|
}
|
||||||
@@ -90,6 +105,7 @@ watch(
|
|||||||
pageNum.value = 1;
|
pageNum.value = 1;
|
||||||
keyword.value = "";
|
keyword.value = "";
|
||||||
deptType.value = "";
|
deptType.value = "";
|
||||||
|
auditType.value = "";
|
||||||
if (!isManage.value) {
|
if (!isManage.value) {
|
||||||
const start = new Date();
|
const start = new Date();
|
||||||
start.setMonth(start.getMonth() - 1);
|
start.setMonth(start.getMonth() - 1);
|
||||||
@@ -105,7 +121,9 @@ watch(
|
|||||||
|
|
||||||
/* ============ 操作(规范 §2:operating-btn iconfont;§6:删除 myConfirm) ============ */
|
/* ============ 操作(规范 §2:operating-btn iconfont;§6:删除 myConfirm) ============ */
|
||||||
function openDetail(row: NoticeVo) {
|
function openDetail(row: NoticeVo) {
|
||||||
router.push({ path: "/details", query: { serid: row.id, type: 0, from: route.path } });
|
// 详情路由随入口:已发送 → /list/detail(高亮已发送),全部通知 → /noticelist/detail(高亮全部通知)
|
||||||
|
const detailPath = route.path.startsWith("/noticelist") ? "/noticelist/detail" : "/list/detail";
|
||||||
|
router.push({ path: detailPath, query: { serid: row.id, type: 0 } });
|
||||||
}
|
}
|
||||||
|
|
||||||
function del(row: NoticeVo) {
|
function del(row: NoticeVo) {
|
||||||
@@ -128,9 +146,10 @@ function del(row: NoticeVo) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* 附件下载:逐个触发(间隔 500ms,同旧版) */
|
/* 附件下载:逐个触发(间隔 500ms,同旧版) */
|
||||||
function downloadFiles(row: NoticeVo & { UpFile?: string }) {
|
function downloadFiles(row: NoticeVo) {
|
||||||
if (!row.UpFile) return;
|
const upFile = row.upFile ?? row.UpFile;
|
||||||
row.UpFile.split(",").forEach((id: string, i: number) => {
|
if (!upFile) return;
|
||||||
|
upFile.split(",").forEach((id: string, i: number) => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.location.href = noticeApi.fileDownUrl(Number(id));
|
window.location.href = noticeApi.fileDownUrl(Number(id));
|
||||||
}, i * 500);
|
}, i * 500);
|
||||||
@@ -139,7 +158,7 @@ function downloadFiles(row: NoticeVo & { UpFile?: string }) {
|
|||||||
|
|
||||||
/* 表格列(对照旧版 notice_list.vue;操作列图标式 width 180 fixed right) */
|
/* 表格列(对照旧版 notice_list.vue;操作列图标式 width 180 fixed right) */
|
||||||
const columns = [
|
const columns = [
|
||||||
{ title: "序号", type: "index", width: 60, align: "center" },
|
{ title: "序号", type: "index", width: 90, align: "center" },
|
||||||
{ title: "标题", key: "title", align: "left", tooltip: true },
|
{ title: "标题", key: "title", align: "left", tooltip: true },
|
||||||
{ title: "来源部门", key: "deptName", width: 130, align: "left" },
|
{ title: "来源部门", key: "deptName", width: 130, align: "left" },
|
||||||
{ title: "发布人", key: "publisherName", width: 110, align: "left", tooltip: true },
|
{ title: "发布人", key: "publisherName", width: 110, align: "left", tooltip: true },
|
||||||
@@ -149,10 +168,22 @@ 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: "已读人数", key: "readCount", width: 90, align: "left" },
|
{
|
||||||
|
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:var(--text-3);" }, "—");
|
||||||
|
// 色系对齐流程侧:待提交灰 / 审批中橙 / 已通过绿 / 已驳回红 / 已撤回灰
|
||||||
|
const colors: Record<number, string> = { 0: "default", 10: "warning", 20: "success", 30: "error", 40: "default" };
|
||||||
|
return h(resolveComponent("Tag"), { color: colors[st] || "default" }, () => p.row.auditStatusDesc || "—");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ title: "已读人数", key: "readCount", width: 110, align: "left" },
|
||||||
{
|
{
|
||||||
title: "未读人数",
|
title: "未读人数",
|
||||||
width: 90,
|
width: 110,
|
||||||
align: "left",
|
align: "left",
|
||||||
render: (h: any, p: any) => h("span", {}, (p.row.allowCount || 0) - (p.row.readCount || 0)),
|
render: (h: any, p: any) => h("span", {}, (p.row.allowCount || 0) - (p.row.readCount || 0)),
|
||||||
},
|
},
|
||||||
@@ -161,27 +192,33 @@ const columns = [
|
|||||||
width: 70,
|
width: 70,
|
||||||
align: "center",
|
align: "center",
|
||||||
render: (h: any, p: any) =>
|
render: (h: any, p: any) =>
|
||||||
p.row.UpFile
|
(p.row.upFile ?? p.row.UpFile)
|
||||||
? Utils.operatingHandle(h, "附件下载", "icon-attachment", () => downloadFiles(p.row))
|
? Utils.operatingHandle(h, "附件下载", "icon-attachment", () => downloadFiles(p.row))
|
||||||
: h("span", { style: "color:#999;" }, "无"),
|
: h("span", { style: "color:var(--text-3);" }, "无"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "操作",
|
title: "操作",
|
||||||
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 delDisabled = p.row.auditStatus === 10;
|
||||||
],
|
return [
|
||||||
|
Utils.operatingHandle(h, "查看通知", "icon-browse", () => openDetail(p.row)),
|
||||||
|
Utils.operatingHandle(h, "删除通知", "icon-ashbin", () => del(p.row), delDisabled),
|
||||||
|
];
|
||||||
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const tableHeight = computed(() => Utils.useTableHeight({ isSearch: true, isOperat: true, isPage: true }));
|
/* 表格高度:不再用 useTableHeight 猜常量,改为实测 .table-region(flex:1)真实高度喂给固定表头 */
|
||||||
|
const tableRegionRef = ref<HTMLElement | null>(null);
|
||||||
|
const tableH = useElementHeight(tableRegionRef);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="page-wrapper">
|
<div class="page-wrapper yh-fill">
|
||||||
<div class="main-container">
|
<div class="main-container">
|
||||||
<!-- 1. 操作按钮区 -->
|
<!-- 1. 操作按钮区 -->
|
||||||
<div class="fix mb-12">
|
<div class="fix mb-12">
|
||||||
@@ -198,6 +235,15 @@ const tableHeight = computed(() => Utils.useTableHeight({ isSearch: true, isOper
|
|||||||
<Select v-model="deptType" class="search-wrap_sel ml-12" clearable placeholder="来源部门" @on-change="doSearch">
|
<Select v-model="deptType" class="search-wrap_sel ml-12" clearable placeholder="来源部门" @on-change="doSearch">
|
||||||
<Option v-for="d in deptOptions" :key="d.k" :value="d.k">{{ d.v }}</Option>
|
<Option v-for="d in deptOptions" :key="d.k" :value="d.k">{{ d.v }}</Option>
|
||||||
</Select>
|
</Select>
|
||||||
|
<Select
|
||||||
|
v-model="auditType"
|
||||||
|
class="search-wrap_sel ml-12"
|
||||||
|
clearable
|
||||||
|
placeholder="状态"
|
||||||
|
@on-change="doSearch"
|
||||||
|
>
|
||||||
|
<Option v-for="s in auditStatusOptions" :key="s.value" :value="s.value">{{ s.label }}</Option>
|
||||||
|
</Select>
|
||||||
<Input
|
<Input
|
||||||
class="fr"
|
class="fr"
|
||||||
style="width: 200px"
|
style="width: 200px"
|
||||||
@@ -211,14 +257,16 @@ const tableHeight = computed(() => Utils.useTableHeight({ isSearch: true, isOper
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 3. 表格 + 分页 -->
|
<!-- 3. 表格 + 分页 -->
|
||||||
<Table
|
<div class="table-region" ref="tableRegionRef">
|
||||||
:columns="columns"
|
<Table
|
||||||
:data="page.content"
|
:columns="columns"
|
||||||
class="link-table table-bottom-box"
|
:data="page.content"
|
||||||
:height="tableHeight"
|
class="link-table table-bottom-box"
|
||||||
:loading="loading"
|
:height="tableH || undefined"
|
||||||
no-data-text="暂无数据"
|
:loading="loading"
|
||||||
></Table>
|
no-data-text="暂无数据"
|
||||||
|
></Table>
|
||||||
|
</div>
|
||||||
<div class="page-panel">
|
<div class="page-panel">
|
||||||
<div class="fl ml-15 align-middle"></div>
|
<div class="fl ml-15 align-middle"></div>
|
||||||
<Page
|
<Page
|
||||||
|
|||||||
@@ -5,11 +5,14 @@ import draggable from "vuedraggable";
|
|||||||
import MyConfirm from "@/components/MyConfirm";
|
import MyConfirm from "@/components/MyConfirm";
|
||||||
import UserCheckModal from "@/components/UserCheckModal.vue";
|
import UserCheckModal from "@/components/UserCheckModal.vue";
|
||||||
import TypeSetBox from "@/components/TypeSetBox.vue";
|
import TypeSetBox from "@/components/TypeSetBox.vue";
|
||||||
import { settingApi, baseApi } from "@/api/module/notice";
|
import { settingApi, baseApi, noticeApi } from "@/api/module/notice";
|
||||||
import type { DeptManageItem, ContactGroupPage, ContactUserPage, SchoolTreeNode } from "@/types/notice";
|
import type { DeptManageItem, ContactGroupPage, ContactUserPage, SchoolTreeNode, FlowOption } from "@/types/notice";
|
||||||
|
import config from "@/assets/config/host.config";
|
||||||
import Utils from "@/utils";
|
import Utils from "@/utils";
|
||||||
|
import { fitDiagram } from "@/utils/diagramImg";
|
||||||
|
import { useElementHeight } from "@/utils/hook/useElementHeight";
|
||||||
|
|
||||||
/* ============ Tab 切换(规范 §1:tabs-wrapper + Tabs type=card,切换只 v-show 内容区) ============ */
|
/* ============ Tab 切换(单卡片 yh-tabs-card 内含线性 Tabs,切换只 v-show 对应 tab-body) ============ */
|
||||||
const currentType = ref(1);
|
const currentType = ref(1);
|
||||||
|
|
||||||
function tabChange(name: string) {
|
function tabChange(name: string) {
|
||||||
@@ -18,9 +21,98 @@ function tabChange(name: string) {
|
|||||||
getDeptList();
|
getDeptList();
|
||||||
} else if (currentType.value === 3) {
|
} else if (currentType.value === 3) {
|
||||||
getGroupList();
|
getGroupList();
|
||||||
|
} else if (currentType.value === 2) {
|
||||||
|
loadFlowBinding();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ============ 流程绑定(应用设置 > 流程绑定) ============ */
|
||||||
|
|
||||||
|
const flowOptions = ref<FlowOption[]>([]);
|
||||||
|
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();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 流程图 Drawer(后端 DiagramDef 中转 PNG:同源免 token 免跨域。修复旧深链 404——
|
||||||
|
* 生产前端静态挂 /flow-center/front*,Caddy 无 /flow-center/ 根路由,window.open 深链必 404) */
|
||||||
|
const flowDiagramVisible = ref(false);
|
||||||
|
const flowDiagramEl = ref<HTMLImageElement | null>(null);
|
||||||
|
const flowDiagramGuid = ref(""); // 打开抽屉时定格的流程 GUID(绑定态=boundFlowGuid,预览态=selectedGuid)
|
||||||
|
const flowDiagramSrc = computed(() =>
|
||||||
|
flowDiagramVisible.value && flowDiagramGuid.value
|
||||||
|
? `${config.serviceHost}/api/notice/flow/DiagramDef?flowGuid=${encodeURIComponent(flowDiagramGuid.value)}`
|
||||||
|
: "",
|
||||||
|
);
|
||||||
|
function openFlowDiagram() {
|
||||||
|
if (!boundFlowGuid.value) return;
|
||||||
|
flowDiagramGuid.value = boundFlowGuid.value;
|
||||||
|
flowDiagramVisible.value = true;
|
||||||
|
}
|
||||||
|
/** 预览下拉当前选中(未必已保存)的流程 */
|
||||||
|
function previewSelectedFlow() {
|
||||||
|
if (!selectedGuid.value) return;
|
||||||
|
flowDiagramGuid.value = selectedGuid.value;
|
||||||
|
flowDiagramVisible.value = true;
|
||||||
|
}
|
||||||
|
function onFlowDiagramError() {
|
||||||
|
flowDiagramVisible.value = false;
|
||||||
|
Message.error("流程图加载失败(流程可能未发布或绑定已解除)");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 当前绑定流程的节点摘要(下拉里回显) */
|
||||||
|
const boundFlowSummary = computed(() => {
|
||||||
|
const hit = flowOptions.value.find((f) => f.flowGuid === boundFlowGuid.value);
|
||||||
|
return hit?.nodeSummary || "—";
|
||||||
|
});
|
||||||
|
|
||||||
/* ============ 发文部门(直接复用 UserBase 同源 TypeSetBox 卡片组件) ============ */
|
/* ============ 发文部门(直接复用 UserBase 同源 TypeSetBox 卡片组件) ============ */
|
||||||
interface DeptItem {
|
interface DeptItem {
|
||||||
SerID: number;
|
SerID: number;
|
||||||
@@ -144,16 +236,6 @@ async function deptSortSave() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function syncDept() {
|
|
||||||
try {
|
|
||||||
await settingApi.getDeptSync().send();
|
|
||||||
Message.success("同步成功");
|
|
||||||
await getDeptList();
|
|
||||||
} catch (e: any) {
|
|
||||||
Message.error(e.message || "同步失败");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ============ 自定义组(规范 §5 联系组管理页形态) ============ */
|
/* ============ 自定义组(规范 §5 联系组管理页形态) ============ */
|
||||||
interface GroupItem {
|
interface GroupItem {
|
||||||
serid: number;
|
serid: number;
|
||||||
@@ -306,6 +388,8 @@ async function settingUserSubmit(users: SchoolTreeNode[]) {
|
|||||||
try {
|
try {
|
||||||
await settingApi.setContactsTea(setUserGid.value, ids).send();
|
await settingApi.setContactsTea(setUserGid.value, ids).send();
|
||||||
Message.success("设置成功");
|
Message.success("设置成功");
|
||||||
|
// 刷新组列表,组员数即时更新
|
||||||
|
await getGroupList();
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
Message.error(e.message || "设置失败");
|
Message.error(e.message || "设置失败");
|
||||||
}
|
}
|
||||||
@@ -324,7 +408,6 @@ const groupColumns = [
|
|||||||
title: "操作",
|
title: "操作",
|
||||||
width: 250,
|
width: 250,
|
||||||
align: "center",
|
align: "center",
|
||||||
fixed: "right",
|
|
||||||
render: (h: any, params: any) => {
|
render: (h: any, params: any) => {
|
||||||
const row: GroupItem = params.row;
|
const row: GroupItem = params.row;
|
||||||
return [
|
return [
|
||||||
@@ -337,22 +420,25 @@ const groupColumns = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const groupTableHeight = computed(() => Utils.useTableHeight({ isTabs: true, isSearch: true, isPage: true }));
|
/* 自定义组表格高度:实测 .table-region(flex:1),替代 useTableHeight 常量减法 */
|
||||||
|
const groupRegionRef = ref<HTMLElement | null>(null);
|
||||||
|
const groupH = useElementHeight(groupRegionRef);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="tabs-wrapper">
|
<div class="page-wrapper yh-fill">
|
||||||
<Tabs class="tabs-panel" type="card" :value="String(currentType)" @on-click="tabChange">
|
<div class="main-container yh-tabs-card">
|
||||||
|
<Tabs class="tabs-panel" :value="String(currentType)" @on-click="tabChange">
|
||||||
<TabPane label="发文部门" name="1"></TabPane>
|
<TabPane label="发文部门" name="1"></TabPane>
|
||||||
<TabPane label="自定义组" name="3"></TabPane>
|
<TabPane label="自定义组" name="3"></TabPane>
|
||||||
|
<TabPane label="流程绑定" name="2"></TabPane>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
<!-- 发文部门 -->
|
<!-- 发文部门 -->
|
||||||
<div class="main-container" v-show="currentType === 1">
|
<div class="tab-body" v-show="currentType === 1">
|
||||||
<Alert show-icon>设置说明:拖动卡片调整部门排序;停用的部门不出现在「通知发送」的部门下拉中</Alert>
|
<Alert show-icon>设置说明:拖动卡片调整部门排序;停用的部门不出现在「通知发送」的部门下拉中</Alert>
|
||||||
<div class="search-wrap fix">
|
<div class="search-wrap fix">
|
||||||
<Button class="fl" type="primary" @click="addDeptShow">新增部门</Button>
|
<Button class="fl" type="primary" @click="addDeptShow">新增部门</Button>
|
||||||
<Button class="fr" @click="syncDept">同步基础平台部门</Button>
|
|
||||||
</div>
|
</div>
|
||||||
<draggable
|
<draggable
|
||||||
class="type_set-wrap fix"
|
class="type_set-wrap fix"
|
||||||
@@ -377,17 +463,19 @@ const groupTableHeight = computed(() => Utils.useTableHeight({ isTabs: true, isS
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 自定义组 -->
|
<!-- 自定义组 -->
|
||||||
<div class="main-container" v-show="currentType === 3">
|
<div class="tab-body" v-show="currentType === 3">
|
||||||
<div class="search-wrap fix">
|
<div class="search-wrap fix">
|
||||||
<Button class="fl" type="primary" @click="addGroupShow">新建联系组</Button>
|
<Button class="fl" type="primary" @click="addGroupShow">新建联系组</Button>
|
||||||
</div>
|
</div>
|
||||||
<Table
|
<div class="table-region" ref="groupRegionRef">
|
||||||
:columns="groupColumns"
|
<Table
|
||||||
:data="groupData"
|
:columns="groupColumns"
|
||||||
class="link-table table-bottom-box"
|
:data="groupData"
|
||||||
:height="groupTableHeight"
|
class="link-table table-bottom-box"
|
||||||
no-data-text="暂无数据"
|
:height="groupH || undefined"
|
||||||
></Table>
|
no-data-text="暂无数据"
|
||||||
|
></Table>
|
||||||
|
</div>
|
||||||
<div class="page-panel">
|
<div class="page-panel">
|
||||||
<div class="fl ml-15 align-middle"></div>
|
<div class="fl ml-15 align-middle"></div>
|
||||||
<Page
|
<Page
|
||||||
@@ -417,6 +505,60 @@ const groupTableHeight = computed(() => Utils.useTableHeight({ isTabs: true, isS
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 流程绑定(应用设置 > 流程绑定) -->
|
||||||
|
<div class="tab-body" v-show="currentType === 2">
|
||||||
|
<Alert show-icon type="warning">
|
||||||
|
绑定说明:绑定后「发送全部用户」的通知必须审核通过才发布(发通知页只显示「提交审核」按钮,
|
||||||
|
隐藏发送与定时发送);「发送指定人员」不受影响,仍直接发布。未绑定则保持现有发送方式。
|
||||||
|
</Alert>
|
||||||
|
<div v-if="!flowEnabled" class="flow-binding-empty">流程中心未接入,暂不支持流程绑定。</div>
|
||||||
|
<template v-else>
|
||||||
|
<div class="flow-binding-form">
|
||||||
|
<span class="label">审核流程:</span>
|
||||||
|
<Select
|
||||||
|
v-model="selectedGuid"
|
||||||
|
clearable
|
||||||
|
:disabled="!flowCanEdit"
|
||||||
|
placeholder="不绑定,全部直发"
|
||||||
|
style="width: 360px"
|
||||||
|
>
|
||||||
|
<Option value="">不绑定,全部直发</Option>
|
||||||
|
<Option v-for="f in flowOptions" :key="f.flowGuid" :value="f.flowGuid">
|
||||||
|
{{ f.name }}({{ f.nodeSummary || "—" }})
|
||||||
|
</Option>
|
||||||
|
</Select>
|
||||||
|
<template v-if="flowCanEdit">
|
||||||
|
<Button type="primary" :loading="bindingSaving" @click="saveFlowBinding">保存绑定</Button>
|
||||||
|
<Button v-if="boundFlowGuid" @click="unbindFlow">解除绑定</Button>
|
||||||
|
<Button v-if="selectedGuid" @click="previewSelectedFlow">查看流程图</Button>
|
||||||
|
</template>
|
||||||
|
<span v-else class="readonly-hint">仅本校管理员可修改流程绑定</span>
|
||||||
|
</div>
|
||||||
|
<div class="flow-binding-state">
|
||||||
|
当前状态:
|
||||||
|
<template v-if="boundFlowGuid">
|
||||||
|
已绑定
|
||||||
|
<Tag color="blue">{{ boundFlowName || boundFlowGuid }}</Tag>
|
||||||
|
<span class="node-summary">{{ boundFlowSummary }}</span>
|
||||||
|
<Button type="text" size="small" @click="openFlowDiagram">查看流程图</Button>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<Tag>未绑定</Tag>
|
||||||
|
<span class="node-summary">发送全部用户与指定人员均直接发布</span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 流程图 Drawer(后端 DiagramDef 同源 PNG;竖版长图容器内纵向滚动) -->
|
||||||
|
<Drawer v-model="flowDiagramVisible" title="流程示意图" :width="860" :transfer="false"
|
||||||
|
:styles="{ body: { padding: '0' } }">
|
||||||
|
<div class="flow-diagram-frame">
|
||||||
|
<img ref="flowDiagramEl" v-if="flowDiagramSrc" :src="flowDiagramSrc" alt="流程示意图" @load="fitDiagram(flowDiagramEl)" @error="onFlowDiagramError" />
|
||||||
|
</div>
|
||||||
|
</Drawer>
|
||||||
|
|
||||||
<!-- 新增发文部门 Modal(编辑走 TypeSetBox 内联编辑) -->
|
<!-- 新增发文部门 Modal(编辑走 TypeSetBox 内联编辑) -->
|
||||||
<Modal v-model="deptModal" :mask-closable="false" width="450px" title="新增部门">
|
<Modal v-model="deptModal" :mask-closable="false" width="450px" title="新增部门">
|
||||||
<Form ref="deptFormRef" :model="deptForm" :rules="deptRule" :label-width="110">
|
<Form ref="deptFormRef" :model="deptForm" :rules="deptRule" :label-width="110">
|
||||||
@@ -489,4 +631,45 @@ const groupTableHeight = computed(() => Utils.useTableHeight({ isTabs: true, isS
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
/* 流程绑定面板 */
|
||||||
|
.flow-binding-form {
|
||||||
|
margin: 16px 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.flow-binding-form .label {
|
||||||
|
color: var(--text-2);
|
||||||
|
}
|
||||||
|
.flow-binding-state {
|
||||||
|
color: var(--text-2);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.flow-binding-state .node-summary {
|
||||||
|
color: var(--text-3);
|
||||||
|
}
|
||||||
|
.readonly-hint {
|
||||||
|
color: var(--text-3);
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
.flow-binding-empty {
|
||||||
|
margin-top: 16px;
|
||||||
|
color: var(--text-3);
|
||||||
|
}
|
||||||
|
/* 流程图 Drawer:浅底色 + 纵向滚动(竖版长图),图片撑满可读宽度 */
|
||||||
|
.flow-diagram-frame {
|
||||||
|
height: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
background: #f5f7fa;
|
||||||
|
padding: 12px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.flow-diagram-frame img {
|
||||||
|
display: block;
|
||||||
|
margin: 0 auto;
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ import { Icon } from "view-ui-plus";
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="page-wrapper">
|
<div class="page-wrapper yh-fill">
|
||||||
<div class="main-container stats-dev">
|
<div class="main-container stats-dev">
|
||||||
<Icon type="ios-stats" size="56" color="#c5c8ce" />
|
<Icon type="ios-stats" size="56" color="var(--text-3)" />
|
||||||
<p class="tip">通知统计功能开发中,敬请期待……</p>
|
<p class="tip">通知统计功能开发中,敬请期待……</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -21,8 +21,7 @@ import { Icon } from "view-ui-plus";
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
min-height: calc(100vh - 136px);
|
color: var(--text-3);
|
||||||
color: #999;
|
|
||||||
}
|
}
|
||||||
.tip {
|
.tip {
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
|
|||||||
+19
-6
@@ -12,9 +12,11 @@ import presetUno from "@unocss/preset-uno";
|
|||||||
import presetRemToPx from "@unocss/preset-rem-to-px";
|
import presetRemToPx from "@unocss/preset-rem-to-px";
|
||||||
|
|
||||||
export default defineConfig(({ command, mode }: ConfigEnv): UserConfig => {
|
export default defineConfig(({ command, mode }: ConfigEnv): UserConfig => {
|
||||||
// 构建输出目录:base -> dist/base, ld -> dist/ld, 默认 dist/base
|
// 构建产物直出 dist/(env-config 模式,与 Cms/UserBase 同风格):CI zip 打包取 dist 根,
|
||||||
const subDir = mode === "ld" ? "ld" : "base";
|
// 不再有 dist/base 中间层(此前 outDir=dist/base 时 zip 打 dist 整层,zip 根只有 base/
|
||||||
const outDir = command === "build" ? `dist/${subDir}` : "dist";
|
// 目录,projman 前端发布校验报「zip 根目录缺少 html 文件」);dev(serve) 不写产物,
|
||||||
|
// 两者共用该值无冲突
|
||||||
|
const outDir = "dist";
|
||||||
|
|
||||||
return {
|
return {
|
||||||
plugins: [
|
plugins: [
|
||||||
@@ -71,7 +73,6 @@ export default defineConfig(({ command, mode }: ConfigEnv): UserConfig => {
|
|||||||
"flex-center": "flex items-center justify-center",
|
"flex-center": "flex items-center justify-center",
|
||||||
// 常用组件
|
// 常用组件
|
||||||
btn: "px-16px py-8px rounded-4px cursor-pointer transition-all duration-200",
|
btn: "px-16px py-8px rounded-4px cursor-pointer transition-all duration-200",
|
||||||
"btn-primary": "bg-blue-600 text-white hover:bg-blue-700",
|
|
||||||
card: "bg-white rounded-8px shadow-md p-16px",
|
card: "bg-white rounded-8px shadow-md p-16px",
|
||||||
// 文本截断
|
// 文本截断
|
||||||
"text-ellipsis": "truncate",
|
"text-ellipsis": "truncate",
|
||||||
@@ -88,6 +89,9 @@ export default defineConfig(({ command, mode }: ConfigEnv): UserConfig => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
// 主题接入已免插件化(A 类,YschTheme《管理端主题远程化与公共静态通道方案》§11):
|
||||||
|
// 唯一接入点 = index.html 一段同步 loader,挂载/FOUC/CSS 排序全由通道 init 承担;
|
||||||
|
// 下面的 /static 代理只是 dev 联调便利(prod 同源用不到),删除不影响线上主题
|
||||||
],
|
],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
@@ -101,7 +105,16 @@ export default defineConfig(({ command, mode }: ConfigEnv): UserConfig => {
|
|||||||
// dev 正式模式:serviceHost 为空(同源相对路径 /api/...),经此代理转发到本地后端
|
// dev 正式模式:serviceHost 为空(同源相对路径 /api/...),经此代理转发到本地后端
|
||||||
// (后端 CAS 过滤器对 /api/* 不回 CORS 头,浏览器直连 8070 会被拦,必须同源代理)
|
// (后端 CAS 过滤器对 /api/* 不回 CORS 头,浏览器直连 8070 会被拦,必须同源代理)
|
||||||
"/api": {
|
"/api": {
|
||||||
target: "http://localhost:8070",
|
target: "http://localhost:8101",
|
||||||
|
changeOrigin: true,
|
||||||
|
},
|
||||||
|
// 主题通道本地联调:dev 下 loader 走同源相对(见 index.html 注释的优先级四档),
|
||||||
|
// manifest/CSS 经此代理到中央通道;changeOrigin 会把 Host 改写成目标域,Caddy 按 Host 匹配站点,
|
||||||
|
// 无需再手工补 Host 头(这是旧 vite 插件里那段 headers.Host 的等价替代)。
|
||||||
|
// 联调本地通道源:YschTheme 侧 npm run serve:static(默认 9527),再用
|
||||||
|
// YH_THEME_PROXY_TARGET=http://localhost:9527 覆盖本 target
|
||||||
|
"/static": {
|
||||||
|
target: process.env.YH_THEME_PROXY_TARGET || "http://base.yunhaixingtong.com",
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -117,7 +130,7 @@ export default defineConfig(({ command, mode }: ConfigEnv): UserConfig => {
|
|||||||
base: "./",
|
base: "./",
|
||||||
build: {
|
build: {
|
||||||
target: "modules", //设置最终构建的浏览器兼容目标 //es2015(编译成es5) | modules
|
target: "modules", //设置最终构建的浏览器兼容目标 //es2015(编译成es5) | modules
|
||||||
outDir, // 构建输出目录,根据环境变量区分 base/ld
|
outDir, // 构建输出目录(恒 dist 根,见文件头说明)
|
||||||
assetsDir: "assets", // 静态资源得存放路径文件名 assets
|
assetsDir: "assets", // 静态资源得存放路径文件名 assets
|
||||||
sourcemap: false, //构建后是否生成 source map 文件
|
sourcemap: false, //构建后是否生成 source map 文件
|
||||||
minify: "esbuild", // 项目压缩 :boolean | 'terser' | 'esbuild'
|
minify: "esbuild", // 项目压缩 :boolean | 'terser' | 'esbuild'
|
||||||
|
|||||||
Reference in New Issue
Block a user