Compare commits

...
Author SHA1 Message Date
chenxing 690d329371 fix(notice): 通知功能修订12项全量落地——已发送幻影省略号/翻页日期格式化/附件upFile字段/审核状态Tag徽标对齐流程色系/审批中禁用删除/待审核操作列operating-link+查看详情/流程绑定预览选中流程图/一键已读改SvgIcon(read-all.svg)/图片附件icon-tupian/详情标签两端对齐/上传附件换行布局/抄送页接入TablePageAlovaNew(usePagination须返回Method)
build / build (push) Successful in 2m37s
2026-10-10 15:16:32 +08:00
chenxing 6a63a7cb8f feat(notice): 审批记录/审批进度 Drawer 收窄至 740,适配 flow-center 嵌入页 v2.4
build / build (push) Successful in 2m53s
- 两处内嵌 Drawer :width 860→740;「流程示意图」Modal 保持 860 不动
- 同批带上:审批页操作列改链接式(查看详情/通过/驳回)+ acting 防连点;
  详情页附件图片图标改 icon-tupian、正文高度模型(page-wrapper 转 flex 纵向)
2026-10-10 14:55:22 +08:00
chenxing f90aefc8e3 fix(diagram): 流程示意图 PNG 按逻辑尺寸显示(SCALE=2 图按自然尺寸渲染整体放大一倍)——详情 Modal 与两处流程图 Drawer 统一走 fitDiagram
build / build (push) Successful in 4m10s
2026-10-09 22:10:21 +08:00
chenxing c1fe0783fb Merge remote-tracking branch 'origin/main' into feat/flow-embed
build / build (push) Successful in 4m13s
2026-10-09 20:56:41 +08:00
chenxing 3de71aa99b feat(notice): 办件视角加操作列「审批记录」——Drawer iframe 嵌 flow-center 进度页(token 每次现取不缓存),业务错原文提示不打断列表 2026-10-09 20:15:34 +08:00
chenxing 8fe3e2ef8c feat(notice): 查看流程图改 Drawer(后端 DiagramDef 同源 PNG 竖版直嵌+纵向滚动)——替换 window.open 深链 404 坏链(生产 /flow-center/front* 无根路由);解绑隐藏入口、未发布走错误文案 2026-10-09 20:04:38 +08:00
chenxing 13afb22060 feat(layout): 菜单图标兜底 icon-list(平台未配置时恒有图标保缩进一致)+ 面包屑首页图标回归行内基线流(-0.125em 光学居中,修 inline-flex 首项无字图标基线把整盒顶高 4px 的错位)+ 通道版本戳跟版
build / build (push) Successful in 4m8s
2026-10-09 17:09:58 +08:00
chenxing dbbabc56eb feat(layout): 菜单图标兜底 icon-list(平台未配置时恒有图标保缩进一致)+ 面包屑首页图标回归行内基线流(-0.125em 光学居中,修 inline-flex 首项无字图标基线把整盒顶高 4px 的错位)+ 通道版本戳跟版 2026-10-09 17:08:28 +08:00
chenxing 5e3f151ce7 revert(顶栏): 移除 LOGO 文字显示(用户反馈子应用不展示,仅门户展示)(LOGO文字与主题JSON化增补)
build / build (push) Successful in 2m29s
2026-10-09 12:30:01 +08:00
chenxing ab647c67ba revert(顶栏): 移除 LOGO 文字显示(用户反馈子应用不展示,仅门户展示)(LOGO文字与主题JSON化增补) 2026-10-09 12:29:57 +08:00
chenxing e8ccc7d3ba fix(顶栏): LOGO 文字字号对齐应用名层级(19px/700)(LOGO文字与主题JSON化增补)
build / build (push) Successful in 3m1s
2026-10-09 12:04:01 +08:00
chenxing b685ff5dc0 fix(顶栏): LOGO 文字字号对齐应用名层级(19px/700)(LOGO文字与主题JSON化增补) 2026-10-09 11:56:42 +08:00
chenxing 9c8b0f900a chore(theme): theme.ts 同步通道 1038 版本戳(LOGO文字与主题JSON化) 2026-10-09 10:49:42 +08:00
chenxing 1af6005a55 feat(顶栏): LOGO 徽标旁显示平台文字(yh-logo-text 键接入);theme.ts 同步通道新版本戳(LOGO文字与主题JSON化)
build / build (push) Successful in 3m5s
2026-10-09 10:44:02 +08:00
chenxing fea12895c1 feat(顶栏): LOGO 徽标旁显示平台文字(yh-logo-text 键接入);theme.ts 同步通道新版本戳(LOGO文字与主题JSON化) 2026-10-09 10:35:45 +08:00
chenxing 599fed01b5 chore(theme): YschTheme sync 件更新(版本 20261009-0054-5732ee2,信封兼容) 2026-10-09 00:54:49 +08:00
chenxing e0d1b2b769 chore(theme): YschTheme sync 件更新(版本 20261009-0036-19f025a,serviceHost 前缀修复) 2026-10-09 00:36:34 +08:00
chenxing 71452fcfd2 chore(theme): YschTheme sync 件更新(版本 20261008-2326-f7fb534,oapi 代理轮)
build / build (push) Successful in 2m36s
2026-10-09 00:23:46 +08:00
chenxing eee4c10155 refactor(theme): 移除顶栏配色切换器入口(oapi 代理轮拍板:设置收敛 UserHome)
本地切换会被服务端生效值收敛打回,不留半残入口;ThemeSwitcher 资产保留(通道 sync 件)
2026-10-09 00:23:45 +08:00
chenxing f611c15a6e chore(theme): YschTheme sync 件更新(版本 20261008-2326-f7fb534,oapi 代理轮) 2026-10-09 00:23:28 +08:00
chenxing 941031e1b4 refactor(theme): 移除顶栏配色切换器入口(oapi 代理轮拍板:设置收敛 UserHome)
本地切换会被服务端生效值收敛打回,不留半残入口;ThemeSwitcher 资产保留(通道 sync 件)
2026-10-08 23:27:35 +08:00
chenxing 59f8fad4ca 外观偏好异步补齐C:YschTheme sync 件更新(UserAvatar 监听 yh-appearance-updated 重读键 + theme.ts 版本戳 20261008-1831-d9db725)
build / build (push) Successful in 2m38s
2026-10-08 18:52:14 +08:00
chenxing f4d0c7d97a 外观偏好异步补齐C:顶栏 Logo 接平台键 yh-logo-url(服务端学校 Logo 异步生效)
- .logo-badge img 改动态 src:localStorage["yh-logo-url"](yh-theme-init 异步补齐段写入)优先,本仓 yunhai-logo.svg 兜底,@error 回落兜底——绝无空 src/裂图
- mounted 读一次 + 监听 yh-appearance-updated 即时切换(无需刷新)
2026-10-08 18:51:51 +08:00
chenxing e37ff9f8d1 feat(avatar): 默认头像轮接入(YschTheme sync 件 + 顶栏三态头像)
- src/utils/avatar.ts + src/components/UserAvatar.vue:YschTheme 默认头像轮 sync 下发公共源(取值链 URL > yh-avatar > yh-avatar-sch-default > text)
- layout.vue:顶栏 img 头像换 UserAvatar 三态组件(placement=brand,size=28);头像/姓名点击统一跳 UserHome 设置页「个人信息」pane(原 /v/userinfo 已下线语义)
- theme.ts/ThemeSwitcher.vue:sync 刷新至 20261006-2013-75c1d10(theme.ts 仅版本戳;ThemeSwitcher 与源仓逐字节一致)
- auto-imports.d.ts:UserAvatar 组件自动登记
当前分支 feat/flow-embed(avatar 轮独立于流程嵌入工作,随分支推送时机一并处理)
2026-10-06 20:25:37 +08:00
chenxing c81f097188 feat(notice): 接入流程进度嵌入页(守卫端点 + Drawer iframe) 2026-10-03 16:59:48 +08:00
chenxing 05804f41a9 theme: 切 A 类免插件接入(YschTheme 方案 §11),删 vendored vite 插件
build / build (push) Successful in 4m19s
- 接入点收敛为 index.html <head> 内一段同步 loader(env-config 引导之后、title/入口之前),
  document.write 保 parser-blocking;通道 init 已吸收 FOUC 门 + manifest 预取 + DCL 挂 <link>
  + href 去重交棒,主题逻辑升级全走 /static,不再需要重新构建本应用
- 域名四档:staticHost > baseHost > VITE_THEME_HOST(dev 跨源直连专用,需通道 CORS)> 同源相对。
  不复用 VITE_BASE_HOST:它与 SSO/API 共用、.env.dev 里本就非空,复用会让 dev 立刻跨源被 CORS 拦;
  build 产物不读 .env,占位符留字面量 → 置空 → 同源(产物零环境值烧入,与 2f423c2 口径一致)
- 删 plugins/vite-plugin-theme(-420 行 vendor)+ package.json/lock 的 file: 依赖,
  根治 CI 容器解析不到 file:../../YschTheme 那类 npm ci 秒失败
- vite.config:保留 dev /static 代理(联调便利,changeOrigin 顺带改写 Host,等价旧插件的
  headers.Host 补偿),注释端口纠错 5174 → 9527;theme.ts 同步到通道版本 20260929-1112-1b1724d
- 已验:npm run build 通过,dist/index.html 注入段存活且无 B 类痕迹,
  YschTheme npm run smoke 断言 3 张主题表按序挂载、FOUC 门先于首帧、manifest 只拉一次
2026-09-29 11:38:31 +08:00
chenxing 29c051350b fix(layout): 菜单点击/高亮统一走 resolveMenuPage,修 /approval 尾空格导致要点多次+不高亮
build / build (push) Successful in 2m35s
- 之前模板与 linkMenu 用 raw item.page,后端 GetUserMod 待审核 page="/approval "(带尾空格)
  与 routeChange 解析后的 "/approval" 不一致 → router.push 带空格需多次才命中、active 比对不等不高亮。
- linkMenu 与 active-class 改用 resolveMenuPage 解析后的 path(顺带修复外链 item.external 之前从 raw 取不到)。
- 不改数据;根治建议:平台模块管理把该 page 的尾空格去掉。
2026-09-24 15:38:36 +08:00
chenxing 8c2d539f34 fix(menu): modId 按真实 /base/GetUserMod 校正(1/2/3/4/21/89/91),撤销上版误值
build / build (push) Successful in 2m59s
- 上版(693aba9)照 31ca694 提交说明的假设值 1000/…/2100,与后端实际返回不符。
  真实 modId:已收到1 已发送2 全部通知3 通知统计4 应用设置21 待审核89 抄送91。
- 深层页回到 parentId 2/2/3(发通知22 / 详情23 / 详情24),修正上版 parentId:2000/3000
  造成的父菜单高亮失效。
- config/menu.ts、mock GetUserMod 同步真实值并补「抄送给我的」91。
- 说明:/approval 导航本就靠 page 精确匹配(后端 page "/approval " 尾空格已被 resolveMenuPage trim),
  本次主要修 modId 契约与详情撞车/高亮,不改变可点击性。
2026-09-24 15:15:46 +08:00
chenxing 693aba9968 fix(menu): modId 契约全量对齐后端 /base/GetUserMod,解 /approval 与 /list/detail 撞车
build / build (push) Successful in 2m41s
- routes.ts:已收到1000/已发送2000/全部通知3000/通知统计4000/应用设置5000;
  深层页按 UserBase 范式编码 20001/20002/30001;/approval 独立 2100
  (原与通知详情同为 23,ROUTE_BY_MODID 取先注册者 → 待审核会被误解析到通知详情)。
- config/menu.ts、mock GetUserMod:id/modId 同步对齐。
- 不动 NoticeApproval.vue 主题改动;/cc 维持 25(与平台一致,未纳入本次对齐)。
2026-09-24 14:22:11 +08:00
chenxing 21a0bd36b6 fix(ci): vendor @yunhai/vite-plugin-theme 进仓,修 CI npm ci 解析不到 file:../../YschTheme
build / build (push) Successful in 2m34s
- CI 只 clone NoticeFront,file:../../YschTheme/packages/vite-plugin 在容器里不存在 → npm ci 秒失败(run#13)。
- 改为仓内 plugins/vite-plugin-theme(拷构建产物,dist→lib 规避 .gitignore 的 dist 规则;剥离 devDeps 防嵌套安装)。
- package.json 消费改 file:./plugins/vite-plugin-theme,重生成 lock(已清 extraneous 孤儿)。
- 本地 npm run build 通过。待 Nexus npm 仓就绪后再迁回版本号消费(方案 B)。
2026-09-24 12:22:09 +08:00
chenxing 796a1b9a7b chore(theme): 同步 theme.ts 到已发布通道版本 20260924-1142-e34cbc4
build / build (push) Failing after 10s
YschTheme 通道已 --publish 上线(base.yunhaixingtong.com 实测 manifest=1142);
经 npm run sync 刷新 src/utils/theme.ts 版本戳。
2026-09-24 11:51:46 +08:00
chenxing 74b9e80b0d theme: 以 feature/theme-remote-p0 内容整体替换 main(主题通道化试点,非 merge)
- 本地 icons.css/layout.css 下线改走中央静态通道;新增 ThemeSwitcher.vue + useElementHeight.ts;
  layout/main.ts/vite/menu 及各页适配对齐 feature 试点。
- 已知回退:本轮按 feature 覆盖,冲掉了 main 上 31ca694(/approval 可达性+modId 对齐) 与
  1516eff(cc useTableHeight) 两处功能修复,待后续单独补回。
- 回滚锚点:archive/main-before-theme-20260924 = 替换前 main(1516eff)。
2026-09-24 11:33:13 +08:00
chenxing 1516effb69 fix(cc): 抄送页高度改走 Utils.useTableHeight——main 分支无 theme 试点分支的 useElementHeight 钩子
build / build (push) Successful in 2m41s
2026-09-24 09:33:36 +08:00
chenxing edd91b53b7 feat(flow): 应用内「抄送给我的」——notice_cc 列表页(路由 /cc)、API 封装、跳通知详情(type=0 不标读)
数据源 /api/notice/cc/My(flw_cc 落表经 flow.cc.sent 回调);未发布通知的详情可见由后端 NoticeFlowGuard 第 4 条(被抄送人)放行
2026-09-24 09:29:21 +08:00
36 changed files with 2349 additions and 1493 deletions
+22
View File
@@ -29,6 +29,28 @@
}
})();
</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>
<link rel="stylesheet" href="./libs/ztree/zTreeStyle.css" />
<script src="./libs/jquery.min.js"></script>
+777
View File
@@ -75,6 +75,431 @@
"alova": "dist/bin/cli.js"
}
},
"node_modules/@alova/wormhole/node_modules/@esbuild/aix-ppc64": {
"version": "0.25.12",
"resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
"integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@alova/wormhole/node_modules/@esbuild/android-arm": {
"version": "0.25.12",
"resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
"integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@alova/wormhole/node_modules/@esbuild/android-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
"integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@alova/wormhole/node_modules/@esbuild/android-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
"integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@alova/wormhole/node_modules/@esbuild/darwin-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
"integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@alova/wormhole/node_modules/@esbuild/darwin-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
"integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@alova/wormhole/node_modules/@esbuild/freebsd-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
"integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@alova/wormhole/node_modules/@esbuild/freebsd-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
"integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@alova/wormhole/node_modules/@esbuild/linux-arm": {
"version": "0.25.12",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
"integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@alova/wormhole/node_modules/@esbuild/linux-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
"integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@alova/wormhole/node_modules/@esbuild/linux-ia32": {
"version": "0.25.12",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
"integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@alova/wormhole/node_modules/@esbuild/linux-loong64": {
"version": "0.25.12",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
"integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@alova/wormhole/node_modules/@esbuild/linux-mips64el": {
"version": "0.25.12",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
"integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@alova/wormhole/node_modules/@esbuild/linux-ppc64": {
"version": "0.25.12",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
"integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@alova/wormhole/node_modules/@esbuild/linux-riscv64": {
"version": "0.25.12",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
"integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@alova/wormhole/node_modules/@esbuild/linux-s390x": {
"version": "0.25.12",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
"integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@alova/wormhole/node_modules/@esbuild/linux-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
"integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@alova/wormhole/node_modules/@esbuild/netbsd-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
"integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@alova/wormhole/node_modules/@esbuild/netbsd-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
"integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@alova/wormhole/node_modules/@esbuild/openbsd-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
"integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@alova/wormhole/node_modules/@esbuild/openbsd-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
"integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@alova/wormhole/node_modules/@esbuild/openharmony-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmmirror.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
"integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openharmony"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@alova/wormhole/node_modules/@esbuild/sunos-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
"integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@alova/wormhole/node_modules/@esbuild/win32-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
"integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@alova/wormhole/node_modules/@esbuild/win32-ia32": {
"version": "0.25.12",
"resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
"integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@alova/wormhole/node_modules/@esbuild/win32-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
@@ -10766,6 +11191,358 @@
"dev": true,
"license": "MIT"
},
"node_modules/vite/node_modules/@esbuild/aix-ppc64": {
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
"integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
"cpu": [
"ppc64"
],
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=12"
}
},
"node_modules/vite/node_modules/@esbuild/android-arm": {
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
"integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/vite/node_modules/@esbuild/android-arm64": {
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
"integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/vite/node_modules/@esbuild/android-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
"integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/vite/node_modules/@esbuild/darwin-arm64": {
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
"integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=12"
}
},
"node_modules/vite/node_modules/@esbuild/darwin-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
"integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=12"
}
},
"node_modules/vite/node_modules/@esbuild/freebsd-arm64": {
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
"integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/vite/node_modules/@esbuild/freebsd-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
"integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/vite/node_modules/@esbuild/linux-arm": {
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
"integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/vite/node_modules/@esbuild/linux-arm64": {
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
"integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/vite/node_modules/@esbuild/linux-ia32": {
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
"integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
"cpu": [
"ia32"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/vite/node_modules/@esbuild/linux-loong64": {
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
"integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
"cpu": [
"loong64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/vite/node_modules/@esbuild/linux-mips64el": {
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
"integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
"cpu": [
"mips64el"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/vite/node_modules/@esbuild/linux-ppc64": {
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
"integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
"cpu": [
"ppc64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/vite/node_modules/@esbuild/linux-riscv64": {
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
"integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
"cpu": [
"riscv64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/vite/node_modules/@esbuild/linux-s390x": {
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
"integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
"cpu": [
"s390x"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/vite/node_modules/@esbuild/linux-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
"integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/vite/node_modules/@esbuild/netbsd-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
"integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/vite/node_modules/@esbuild/openbsd-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
"integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/vite/node_modules/@esbuild/sunos-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
"integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=12"
}
},
"node_modules/vite/node_modules/@esbuild/win32-arm64": {
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
"integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/vite/node_modules/@esbuild/win32-ia32": {
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
"integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
"cpu": [
"ia32"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/vite/node_modules/esbuild": {
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.21.5.tgz",
+45
View File
@@ -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;
+4
View File
@@ -127,6 +127,10 @@ class NoticeApi {
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`);
+7 -6
View File
@@ -6,12 +6,13 @@ import type { IModule } from "@/store";
*/
export function getMenuList(): Promise<IModule[]> {
const menu: IModule[] = [
{ id: 1000, modId: 1000, name: "已收到", page: "/" },
{ id: 2000, modId: 2000, name: "已发送", page: "/list" },
{ id: 3000, modId: 3000, name: "全部通知", page: "/noticelist" },
{ id: 2100, modId: 2100, name: "待审核", page: "/approval" },
{ id: 4000, modId: 4000, name: "通知统计", page: "/stats" },
{ id: 5000, modId: 5000, name: "应用设置", page: "/setting" },
{ id: 1, modId: 1, name: "已收到", page: "/" },
{ id: 2, modId: 2, name: "已发送", page: "/list" },
{ id: 3, modId: 3, name: "全部通知", page: "/noticelist" },
{ id: 89, modId: 89, name: "待审核", page: "/approval" },
{ id: 4, modId: 4, name: "通知统计", page: "/stats" },
{ id: 21, modId: 21, name: "应用设置", page: "/setting" },
{ id: 91, modId: 91, name: "抄送给我的", page: "/cc" },
];
return Promise.resolve(menu);
}
+1
View File
@@ -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

+7
View File
@@ -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

+70 -8
View File
@@ -252,6 +252,33 @@ function fileUrl(id: number) {
/* ============================ 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 [
/* ---------- 布局 / 用户 ---------- */
{
@@ -271,14 +298,15 @@ export default [
method: "get",
response: () =>
ok([
// icon:用户中心模块管理配置的 YschIcons 图标名,layout 渲染为 iconfont icon-<名称>
// id/modId 与平台模块管理的「配置 ID」一致(对照 /base/GetUserMod 现行数据)
{ id: 1000, modId: 1000, name: "已收到", parentId: 0, page: "/", icon: "sjx", orderNumber: 1, usable: true, children: [] },
{ id: 2000, modId: 2000, name: "已发送", parentId: 0, page: "/list", icon: "fasong", orderNumber: 2, usable: true, children: [] },
{ id: 3000, modId: 3000, name: "全部通知", parentId: 0, page: "/noticelist", icon: "liebiao", orderNumber: 3, usable: true, children: [] },
{ id: 2100, modId: 2100, name: "待审核", parentId: 0, page: "/approval", icon: "", orderNumber: 4, usable: true, children: [] },
{ id: 4000, modId: 4000, name: "通知统计", parentId: 0, page: "/stats", icon: "tongji", orderNumber: 5, usable: true, children: [] },
{ id: 5000, modId: 5000, name: "应用设置", parentId: 0, page: "/setting", icon: "shezhi", orderNumber: 6, usable: true, children: [] },
// id/modId 对齐平台模块管理的「配置 ID」(对照 /base/GetUserMod 现行数据);
// icon:YschIcons 图标名,layout 渲染为 iconfont icon-<名称>
{ id: 1, modId: 1, name: "已收到", parentId: 0, page: "/", icon: "sjx", orderNumber: 1, usable: true, children: [] },
{ id: 2, modId: 2, name: "已发送", parentId: 0, page: "/list", icon: "fasong", orderNumber: 2, usable: true, children: [] },
{ id: 3, modId: 3, name: "全部通知", parentId: 0, page: "/noticelist", icon: "liebiao", orderNumber: 3, usable: true, children: [] },
{ id: 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: [] },
]),
},
{
@@ -724,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);
},
},
];
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+19 -6
View File
@@ -27,7 +27,7 @@
content: "";
display: block;
width: 3px;
background: #5979ea;
background: var(--brand, #577afb);
height: 15px;
border-radius: 4px;
position: absolute;
@@ -39,7 +39,7 @@
h4.second-title {
@extend .base-subtitle;
&::before {
background: #5979ea;
background: var(--brand, #577afb);
}
}
@@ -62,7 +62,7 @@ h4.subtitle {
overflow-y: auto;
li {
height: 42px;
border: 1px solid #ebeef5;
border: 1px solid var(--line, #e9edf5);
border-top: none;
position: relative;
cursor: pointer;
@@ -87,8 +87,8 @@ h4.subtitle {
}
li.active {
// background: #f3f3f3;
color: #7892ed;
border-right: 3px solid #5979ea;
color: var(--brand-2, #7b8ffd);
border-right: 3px solid var(--brand, #577afb);
}
}
}
@@ -107,7 +107,7 @@ h4.subtitle {
background-position: top center;
background-repeat: no-repeat;
border: none;
color: #577afb;
color: var(--brand, #577afb);
margin: 0 auto;
}
@@ -115,3 +115,16 @@ h4.subtitle {
margin-right: 4px;
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;
}
+1 -1
View File
@@ -14,7 +14,7 @@
color: $color-warning !important;
}
.text-primary {
color: $color-primary !important;
color: var(--brand, #577afb) !important;
}
.text-normal {
color: #404042 !important;
+2 -2
View File
@@ -1,10 +1,10 @@
/*scss变量*/
// 主题色
// 主题色(品牌主色不在此定义:由平台主题令牌 var(--brand) 经中央静态通道下发,
// 本文件只留业务语义色——成功/警告/错误不随主题档位变化)
$color-error: #e8684a;
$color-success: #45b972;
$color-warning: #e9b954;
$color-primary: #5979ea;
// 文字色
$color-black: #404042;
+5 -3
View File
@@ -92,16 +92,18 @@ export default {
display: block;
.upload-row {
display: flex;
align-items: center;
align-items: flex-start; /* 提示语与按钮首行对齐(列表换行后不再垂直居中整块) */
}
:deep(.ivu-upload) {
display: inline-flex;
flex-wrap: wrap; /* 允许内部列表换行 */
align-items: center;
}
/* 文件列表整体另起一行,每项独占一行,不再与按钮挤在一起 */
:deep(.ivu-upload-list) {
display: block;
margin-top: 8px;
width: 100%; /* 文件列表整体独占下一行 */
margin-top: 4px;
}
:deep(.ivu-upload-list-file) {
padding: 4px 0;
@@ -110,7 +112,7 @@ export default {
margin-left: 10px;
color: #f90;
font-size: 12px;
line-height: 1;
line-height: 32px; /* 与默认 Button 等高,紧贴按钮右侧 */
}
}
</style>
+16 -11
View File
@@ -1,13 +1,15 @@
<template>
<Table :columns="columns" :data="list" :height="tableH" :loading="loading" no-data-text="暂无数据"
class="link-table table-bottom-box" @on-selection-change="selectChange">
<template #loading>
<div class="table-spin-load">加载中</div>
</template>
<template v-for="slotName in tableSlotNames" :key="slotName" v-slot:[slotName]="slotProps">
<slot :name="slotName" v-bind="slotProps" />
</template>
</Table>
<div class="table-region" ref="regionRef">
<Table :columns="columns" :data="list" :height="tableH" :loading="loading" no-data-text="暂无数据"
class="link-table table-bottom-box" @on-selection-change="selectChange">
<template #loading>
<div class="table-spin-load">加载中</div>
</template>
<template v-for="slotName in tableSlotNames" :key="slotName" v-slot:[slotName]="slotProps">
<slot :name="slotName" v-bind="slotProps" />
</template>
</Table>
</div>
<div class="page-panel">
<div class="fl ml-15 align-middle">
@@ -22,7 +24,7 @@
<script setup lang="ts">
import { computed, ref, useSlots } from 'vue'
import Utils from '@/utils'
import { useElementHeight } from '@/utils/hook/useElementHeight'
import { OperatorEnum } from '@/types'
interface Props {
@@ -44,7 +46,10 @@ const emit = defineEmits<{
}>()
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 selectList = ref<number[]>([])
+274
View File
@@ -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>
+102
View File
@@ -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>
+64 -30
View File
@@ -1,8 +1,11 @@
<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 { Message } from "view-ui-plus";
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 route = useRoute();
@@ -22,6 +25,27 @@ const modList = computed(() => props.layoutData?.modList || []);
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(() => {
routeChange(route);
});
@@ -94,15 +118,21 @@ const routeChange = (to: any) => {
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 page: string = item?.page || "";
const { path: page, external } = resolveItemPage(item);
if (!page) {
// 解析不出有效页面:显式提示,而不是静默不动或被重定向吞到默认页
Message.warning(`菜单「${item?.name || "未命名"}」未配置有效页面路径,请检查基础平台模块设置`);
return;
}
// 平台配置的外部链接模块:整页跳转
if (item?.external) {
if (external) {
window.location.href = page;
return;
}
@@ -114,7 +144,8 @@ const linkMenu = (item: any) => {
};
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 = () => {
let url = window.location.href;
@@ -129,12 +160,10 @@ const childChange = (index: number) => {
};
/** 菜单图标:读取用户中心模块管理配置的 YschIcons 图标名(GetUserMod 的 icon 字段,如 list → icon-list)。
* 本地 icons.css 即 ysch-icons 掩码版(基类 iconfont,类名 icon-<名称>);
* 平台约定留空不下发图标,此时返回空串由模板 v-if 不渲染 */
const getMenuIcon = (item?: any): string => {
const icon = String(item?.icon || "").trim();
return icon ? `icon-${icon}` : "";
};
* icons.css(ysch-icons 掩码版,基类 iconfont,类名 icon-<名称>)现由中央静态通道下发;
* 平台未配置图标时统一兜底 icon-list(2026-10-09 定稿:菜单项恒有图标,保整列缩进/行高一致) */
const getMenuIcon = (item?: any): string =>
`icon-${String(item?.icon || "").trim() || "list"}`;
function linkHomePage() {
if (baseHost.value) {
@@ -149,16 +178,13 @@ function linkHomePage() {
<template>
<div class="layout-wrap">
<header class="header-container fix">
<div class="fl align-middle icon-box">
<div title="首页" class="icon-btn" @click="linkHomePage">
<i class="iconfont icon-shouye"></i>
</div>
</div>
<!-- 云海 logo 徽标(结构与 Cms SchoolLayout 对齐,皮肤见通道 base.layout.css .logo-badge) -->
<span class="logo-badge fl"><img :src="logoUrl" alt="云海" @error="onLogoError" /></span>
<div class="delimiter fl align-middle"></div>
<h4 class="fl align-middle">{{ appName }}</h4>
<div class="school-bg"></div>
<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">
{{ userInfo.name }}
</a>
@@ -167,6 +193,8 @@ function linkHomePage() {
<i class="iconfont icon-logout"></i>
</a>
</div>
<!-- 配色切换器已移除(oapi 代理轮 2026-10-08 拍板):设置入口收敛在 UserHome 设置中心,
本地切换会被服务端生效值收敛打回,不留半残入口 -->
</header>
<div class="left-menu" v-if="!isAlone">
<div class="menu-top-box"></div>
@@ -176,14 +204,14 @@ function linkHomePage() {
<li>
<a href="javascript:;" @click="childChange(index)">
<i :class="item.childState ? 'arrow-down' : 'arrow-up'"></i>
<i v-if="getMenuIcon(item)" :class="['iconfont', getMenuIcon(item)]"></i>
<i :class="['iconfont', getMenuIcon(item)]"></i>
{{ item.name }}
</a>
</li>
<transition name="fade_child_ul">
<div class="child-item-wrap" v-show="item.childState">
<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"
:key="citem.id"
>
@@ -194,9 +222,9 @@ function linkHomePage() {
</div>
</transition>
</template>
<li v-else :class="item.page == currPath ? 'active' : ''">
<li v-else :class="pageOf(item) == currPath ? 'active' : ''">
<a href="javascript:;" @click="linkMenu(item)">
<i v-if="getMenuIcon(item)" :class="['iconfont', getMenuIcon(item)]"></i>
<i :class="['iconfont', getMenuIcon(item)]"></i>
{{ item.name }}
</a>
</li>
@@ -210,7 +238,7 @@ function linkHomePage() {
<a href="javascript:;" @click="linkHomePage">
<BreadcrumbItem>
<span class="crumb-home">
<i class="iconfont icon-category"></i>
<i class="iconfont icon-shouye"></i>
首页
</span>
</BreadcrumbItem>
@@ -250,21 +278,27 @@ function linkHomePage() {
</template>
<style scoped>
/* 仅覆盖/补充必要的动态样式,其余样式由全局 layout.css 提供 */
/* 菜单图标(本地 bsch-icons 掩码类)与文字间距 */
/* 仅覆盖/补充必要的动态样式,其余样式由中央静态通道的 base.layout.css(骨架 + 皮肤)提供 */
/* 菜单图标(通道下发的 ysch-icons 掩码类)与文字间距 */
.left-menu a .iconfont {
margin-right: 8px;
}
/* 面包屑「首页」垂直居中:layout.css 遗留的 .path-nav .icon-category{position:relative;top:1px}
是给老 iconfont 字体字形做的基线补偿,换成 ysch mask 的 1em 方块后反而把图标顶偏;
这里用 inline-flex 真居中并关掉该补偿(不动压缩产物 layout.css,覆盖收在本组件内) */
.crumb-home {
display: inline-flex;
align-items: center;
gap: 4px;
/* 顶栏已由通道皮肤改为 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>
+2 -4
View File
@@ -13,10 +13,8 @@ import mittBus from "@/utils/mitt";
import "view-ui-plus/dist/styles/viewuiplus.css";
import "@/assets/styles/main.scss";
// 本地化的布局/主题样式(原远程 base.layout.beta / _iview_override / default-theme 合并)
import "@/assets/styles/layout.css";
// 规范 iconfont 图标本地实现(icon-browse / icon-edit / icon-ashbin 等,平台主题不含字体)
import "@/assets/styles/icons.css";
// 布局/主题/图标(base.layout.css + iview.override.css + icons.css)由中央静态通道下发:
// index.html 的 yh-theme-init.js 在 DOMContentLoaded 后按 manifest 版本追加 <link>,天然排在本文件组件库样式之后
//导入自定义指令
import directive from "@/utils/directive/index";
+24 -16
View File
@@ -9,12 +9,12 @@ import type { RouteRecordRaw } from "vue-router";
* - /list/detail?serid=&type=、/noticelist/detail?serid=&type=
* type: 0=编辑视角(不标读) 1=普通查看(标读)
*
* modId 是菜单 ↔ 路由的稳定契约(对照后端 /base/GetUserMod 现行数据,即用户中心
* 模块管理里的配置 ID):
* 1000=已收到 2000=已发送 3000=全部通知 2100=待审核 4000=通知统计 5000=应用设置
* 深层子页不在菜单树,编码沿用 UserBase 范式「父模块 ×10 + 序号」并靠 meta.parentId
* 关联高亮:/list/new=20001、/list/detail=20002(父 2000),/noticelist/detail=30001(父 3000)。
* 深层页编码不得与任何平台一级模块 ID 重复。
* modId 是菜单 ↔ 路由的稳定契约(严格对照后端 /base/GetUserMod 现行返回的 modId):
* 1=已收到 2=已发送 3=全部通知 4=通知统计 21=应用设置 89=待审核 91=抄送给我的
* (后两条平台已挂一级菜单,page 分别为 /approval、/cc)。
* 深层子页不在菜单树,靠 meta.parentId 关联高亮父菜单:/list/new=22(父2)、
* /list/detail=23(父2)、/noticelist/detail=24(父3)。深层页 modId 不得与任何
* 平台一级模块 ID(含 89/91)重复。
*/
const routes: RouteRecordRaw[] = [
// 已收通知(收件箱):后端菜单 page 配置为 "/",即应用入口页
@@ -22,14 +22,14 @@ const routes: RouteRecordRaw[] = [
path: "/",
name: "notice_accept",
component: () => import("@/views/notice/NoticeAccept.vue"),
meta: { title: "已收到", modId: 1000 },
meta: { title: "已收到", modId: 1 },
},
// 已发送
{
path: "/list",
name: "notice_list",
component: () => import("@/views/notice/NoticeList.vue"),
meta: { title: "已发送", modId: 2000 },
meta: { title: "已发送", modId: 2 },
},
// 发通知(新建/编辑/转发/模板):已发送的深层子页,不在菜单里,
// layout.vue 按.meta.parentId 推导面包屑「已发送>发通知」并高亮父菜单
@@ -37,14 +37,14 @@ const routes: RouteRecordRaw[] = [
path: "/list/new",
name: "new_notice",
component: () => import("@/views/notice/NewNotice.vue"),
meta: { title: "发通知", modId: 20001, parentId: 2000 },
meta: { title: "发通知", modId: 22, parentId: 2 },
},
// 全部通知(管理端)
{
path: "/noticelist",
name: "notice_manage",
component: () => import("@/views/notice/NoticeList.vue"),
meta: { title: "全部通知", modId: 3000 },
meta: { title: "全部通知", modId: 3 },
},
// 通知详情(同一组件挂两个路由,面包屑/菜单高亮由 meta.parentId 确定父节点:
// 已发送下 /list/detail,全部通知下 /noticelist/detail;type 区分是否标读)
@@ -52,34 +52,42 @@ const routes: RouteRecordRaw[] = [
path: "/list/detail",
name: "notice_list_detail",
component: () => import("@/views/notice/NoticeDetails.vue"),
meta: { title: "通知详情", modId: 20002, parentId: 2000 },
meta: { title: "通知详情", modId: 23, parentId: 2 },
},
{
path: "/noticelist/detail",
name: "notice_manage_detail",
component: () => import("@/views/notice/NoticeDetails.vue"),
meta: { title: "通知详情", modId: 30001, parentId: 3000 },
meta: { title: "通知详情", modId: 24, parentId: 3 },
},
// 通知统计(旧版行为:整页跳转外部统计系统)
{
path: "/stats",
name: "notice_stats",
component: () => import("@/views/notice/NoticeStats.vue"),
meta: { title: "通知统计", modId: 4000 },
meta: { title: "通知统计", modId: 4 },
},
// 应用设置(发文部门 + 自定义组)
{
path: "/setting",
name: "notice_setting",
component: () => import("@/views/notice/NoticeSetting.vue"),
meta: { title: "应用设置", modId: 5000 },
meta: { title: "应用设置", modId: 21 },
},
// 待审核(应用内审批;平台模块管理已挂一级菜单,配置 ID = 2100,path = /approval)
// 待审核(应用内审批;平台模块管理已挂一级菜单,配置 ID = 89,path = /approval)
{
path: "/approval",
name: "notice_approval",
component: () => import("@/views/notice/NoticeApproval.vue"),
meta: { title: "我的待审核", modId: 2100 },
meta: { title: "我的待审核", modId: 89 },
},
// 抄送给我的(流程 NOTIFY 知会;平台模块管理已挂一级菜单,配置 ID = 91,path = /cc)
{
path: "/cc",
name: "notice_cc",
component: () => import("@/views/notice/NoticeCc.vue"),
meta: { title: "抄送给我的", modId: 91 },
},
/* ---- 旧版路径兼容重定向(书签、残留的平台模块配置) ---- */
+4
View File
@@ -64,6 +64,10 @@ export interface NoticeVo {
smsFlag: number;
/** 接收人列表(JSON) */
receiveList: string | null;
/** 附件ID串("1,2,3";后端 Lombok 序列化名为 upFile,对齐旧 .NET UpFile 语义) */
upFile?: string | null;
/** @deprecated 旧 .NET 接口的 PascalCase 附件字段,仅作读取兼容 */
UpFile?: string | null;
createBy: string | null;
createTime: string | null;
updateBy: string | null;
+94
View File
@@ -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;
}
+12
View File
@@ -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";
}
+36
View File
@@ -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
View File
@@ -100,7 +100,7 @@ export default class Utils {
"div",
{
class: `operating-btn iconfont ${iconClass}`,
onClick: callback,
onClick: disabled ? null : callback,
},
null,
),
+73 -1
View File
@@ -1,5 +1,5 @@
/**
* 本文件由 YschTheme scripts/gen-theme.mjs 从 assets-src/pc/theme-core.js 生成(版本 20260923-1454-9619fbf),勿手改。
* 本文件由 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> 内先行挂好,本模块只负责读写与切换。
*/
@@ -25,6 +25,9 @@ export const THEME_NAMES: readonly ThemeName[] = ["blue", "ocean", "ink", "viole
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,
@@ -49,6 +52,75 @@ export function parseThemeState(raw: string | null): ThemeState {
}
}
/** 严格读取:四字段全部合法才返回状态,否则 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));
+40 -7
View File
@@ -10,6 +10,7 @@ import type { NoticeDetailVo, NoticeRequest, UpFileVo, FlowBindingVo } from "@/t
import UserCheckInput from "@/components/UserCheckInput.vue";
import NoticeUpload from "@/components/NoticeUpload.vue";
import config from "@/assets/config/host.config";
import { fitDiagram } from "@/utils/diagramImg";
const route = useRoute();
const router = useRouter();
@@ -85,11 +86,21 @@ onMounted(async () => {
}
});
/** 流程图:flow-center 示意图深链(Notice 指定到人场景用人员视图) */
/** 流程图 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) {
window.open(`${config.flowHost}/#/diagram?flowGuid=${auditFlowGuid.value}&mode=user`, "_blank");
}
if (!auditFlowGuid.value) return;
flowDiagramVisible.value = true;
}
function onFlowDiagramError() {
flowDiagramVisible.value = false;
Message.error("流程图加载失败(流程可能未发布或绑定已解除)");
}
/* ============ 富文本编辑器(wangeditor v5 for vue3) ============ */
@@ -408,6 +419,14 @@ async function saveEditAndLeave(data: NoticeRequest) {
<Button @click="isModal.timiSend = false" style="margin-left: 8px">取消</Button>
</template>
</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>
</template>
@@ -423,11 +442,11 @@ async function saveEditAndLeave(data: NoticeRequest) {
}
}
.editor-wrap {
border: 1px solid #dcdee2;
border-radius: 4px;
border: 1px solid var(--line);
border-radius: var(--r-s);
z-index: 100;
.editor-toolbar {
border-bottom: 1px solid #e8e8e8;
border-bottom: 1px solid var(--line);
}
.editor-content {
:deep(.w-e-text-container) {
@@ -439,4 +458,18 @@ async function saveEditAndLeave(data: NoticeRequest) {
.bottom-btn {
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>
+176 -82
View File
@@ -5,6 +5,7 @@ import { Message, Tooltip } from "view-ui-plus";
import { noticeApi } from "@/api/module/notice";
import type { NoticeDetailVo, NoticeQuery, NoticeVo, NoticePageResult } from "@/types/notice";
import FilePreviewModal from "@/components/FilePreviewModal.vue";
import SvgIcon from "@/components/SvgIcon.vue";
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) {
const suffix = String(name || "").split(".").pop()?.toLowerCase() || "";
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 (["pdf"].includes(suffix)) return "icon-pdf1";
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";
return "icon-weizhi";
}
@@ -163,7 +165,7 @@ function shortDate(date: string) {
/* 时间显示(列表 MM/dd HH:mm,同旧版) */</script>
<template>
<div class="page-wrapper">
<div class="page-wrapper yh-fill">
<div class="main-container">
<div class="accept-wrap">
<!-- 左侧通知列表 -->
@@ -171,7 +173,7 @@ function shortDate(date: string) {
<div class="total">
<span>未读<span class="num">{{ page.unreadCount || 0 }}</span>条</span>
<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>
</div>
<div class="search-box">
@@ -211,11 +213,11 @@ function shortDate(date: string) {
</div>
</div>
<div class="title-content">
<p>来源:{{ detail.DeptName }}</p>
<p>发布人:{{ detail.TeaName }}</p>
<p>发布时间:{{ detail.EditDate }}</p>
<p><span class="meta-label">来源</span>:{{ detail.DeptName }}</p>
<p><span class="meta-label">发布人</span>:{{ detail.TeaName }}</p>
<p><span class="meta-label">发布时间</span>:{{ detail.EditDate }}</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>
</p>
</div>
@@ -262,188 +264,280 @@ function shortDate(date: string) {
</template>
<style lang="scss" scoped>
/* 根容器 page-wrapper > main-container:内容区可视高度 = 100vh - 112(头部+面包屑) - 24(容器底padding) */
.accept-wrap {
display: flex;
gap: 15px;
height: calc(100vh - 136px);
/* 已收到页 · 对齐原型 07-seam-pure.html 的 .nt-split(灰底上并排两张白卡)。
通道把 .main-container 画成单张白卡,这里压平它、让两张卡直接坐灰底;
外层 .page-wrapper.yh-fill 由通道转 flex 纵向铺满,卡片 flex:1 撑满、四周留白 14/18/24 全站一致。
所有颜色/圆角/阴影一律走主题令牌,禁止硬编码。 */
.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 {
width: 360px;
flex: none;
background: #fff;
border: 1px solid #ebeef4;
border-radius: 3px;
padding: 14px 0;
display: flex;
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 {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 15px 10px;
padding: 13px 16px 9px;
font-size: 13px;
color: var(--text-2);
.num {
padding: 0 5px;
color: #5c9edc;
padding: 0 2px;
color: var(--brand);
font-size: 15px;
font-weight: 600;
}
.clear-icon {
cursor: pointer;
font-size: 18px;
color: #999;
&:hover {
color: #2d8cf0;
}
}
}
.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 {
flex: 1;
min-height: 0;
overflow-y: auto;
margin-top: 12px;
padding: 0 8px 12px;
.list_item {
padding: 10px 15px;
cursor: pointer;
padding: 10px 12px;
margin-bottom: 2px;
border-radius: var(--r-s);
border-left: 3px solid transparent;
cursor: pointer;
transition: background 0.15s, border-color 0.15s;
.tt {
font-size: 13px;
font-weight: 600;
color: #272829;
color: var(--text);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.other {
margin-top: 6px;
margin-top: 5px;
display: flex;
justify-content: space-between;
font-size: 12px;
color: #999;
font-size: 11.5px;
color: var(--text-3);
}
&:hover {
background: #f6f9ff;
background: #f7f9fc;
}
&.active {
background: #ecf3ff;
border-left-color: #2d8cf0;
background: var(--brand-soft);
border-left-color: var(--brand);
.tt {
color: var(--brand);
}
}
/* 已读弱化 */
&.readed .tt {
color: #8a94a6;
color: var(--text-2);
font-weight: 400;
}
}
.empty {
text-align: center;
color: #999;
color: var(--text-3);
padding: 40px 0;
}
}
}
/* ---------- 右:详情卡(原型 .nt-detail) ---------- */
.detail-content {
flex: 1;
background: #fff;
border: 1px solid #ebeef4;
border-radius: 3px;
padding: 16px 20px;
display: flex;
flex-direction: column;
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 {
align-items: center;
justify-content: center;
color: #999;
color: var(--text-3);
}
.title-header {
display: flex;
align-items: center;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
.tt {
font-size: 18px;
font-size: 17px;
font-weight: 600;
color: #272829;
letter-spacing: 0.2px;
color: var(--text);
}
.ops {
display: flex;
gap: 16px;
gap: 14px;
.op {
cursor: pointer;
color: #515a6e;
color: var(--text-2);
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 13px;
gap: 5px;
font-size: 12.5px;
transition: color 0.15s;
&:hover {
color: #2d8cf0;
color: var(--brand);
}
}
}
}
.title-content {
margin-top: 10px;
padding: 8px 0;
border-bottom: 1px dashed #e8e8e8;
font-size: 13px;
color: #808695;
margin-top: 14px;
padding-bottom: 16px;
border-bottom: 1px dashed var(--line);
display: grid;
gap: 9px;
font-size: 12.5px;
color: var(--text-2);
p {
margin: 4px 0;
margin: 0;
}
/* 标签两端撑开:冒号纵向对齐、值从同一列起(宽度取「发布时间」4 字) */
.meta-label {
display: inline-block;
width: 4em;
text-align: justify;
text-align-last: justify;
}
.to-file {
margin-left: 10px;
color: #2d8cf0;
margin-left: 8px;
color: var(--brand);
cursor: pointer;
&:hover {
text-decoration: underline;
}
}
}
.notice-content {
flex: 1;
min-height: 0;
overflow-y: auto;
padding-top: 10px;
padding-top: 14px;
.ql-editor {
font-size: 14px;
color: var(--text);
}
.file {
border-top: 1px solid #f0f0f0;
margin-top: 16px;
border: 1px solid var(--line);
border-radius: var(--r);
overflow: hidden;
.file-title {
display: flex;
justify-content: space-between;
background: #f7f7f7;
padding: 8px 12px;
font-size: 13px;
align-items: center;
padding: 9px 14px;
background: #f7f9fc;
border-bottom: 1px solid var(--line);
font-size: 12.5px;
color: var(--text-2);
.all-down {
color: #2d8cf0;
color: var(--brand);
cursor: pointer;
&:hover {
text-decoration: underline;
}
}
}
.file-list {
padding: 10px 12px;
padding: 2px 0;
.file-item {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 6px;
border-bottom: 1px dashed #f0f0f0;
padding: 11px 14px;
transition: background 0.15s;
&:hover {
background: #fafbfe;
}
.left {
flex: 0 0 auto;
}
.file-type-icon {
font-size: 26px;
color: #515a6e;
width: 30px;
height: 30px;
display: grid;
place-items: center;
border-radius: 8px;
background: var(--brand-soft);
color: var(--brand);
font-size: 16px;
}
.center {
flex: 1;
overflow: hidden;
min-width: 0;
.name {
color: #272829;
font-size: 13px;
font-size: 12.5px;
color: var(--text);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.size {
font-size: 12px;
color: #999;
font-size: 11.5px;
color: var(--text-3);
margin-top: 2px;
}
}
.right a {
margin-right: 8px;
color: #2d8cf0;
font-size: 13px;
.right {
display: flex;
gap: 14px;
flex: 0 0 auto;
a {
color: var(--brand);
cursor: pointer;
font-size: 12.5px;
&:hover {
text-decoration: underline;
}
}
}
}
}
+161 -187
View File
@@ -1,77 +1,162 @@
<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 { ref, reactive, computed, onMounted, nextTick } from "vue";
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 Utils from "@/utils";
import { noticeApi } from "@/api/module/notice";
/**
* 我的待审核(应用内审批,第三步):待办列表 + 办件视角。
* 通过/驳回走本应用后端代理(operator=登录态,对端按任务办理人复核;非办理人会收到业务错提示)。
* bizSnapshot 是发起时透传的 JSON 摘要,解析失败只展示标题。
*
* 规范:bsch应用页规范 §1(tabs-wrapper + tabs-panel type=card,切换只 v-show)、
* §2(操作列 operatingHandle 文字链式)、§3(customPageCount 页长记忆)、§8(必填红星 + 统一提示)。
* 我的待审核(应用内审批,第三步):待办列表 + 办件视角,通过/驳回走本应用后端代理
* (operator=登录态,对端按任务办理人复核;非办理人会收到业务错提示)。
* bizSnapshot 是发起时透传的 JSON 摘要,解析失败只展示标题。
*/
const tab = ref<"todo" | "instances">("todo");
const loading = ref(false);
const acting = ref(false);
/* ============ 分页(规范 §3:customPageCount 页长记忆) ============ */
const defaultSize = parseInt(localStorage.getItem("customPageCount") || "10");
const todoRows = ref<(ApprovalTodoTask & { _summary: string })[]>([]);
const todoPage = ref({ total: 0, pageIndex: 1, pageSize: defaultSize });
/* 两个表格高度:实测各自 .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 instRows = ref<(ApprovalInstance & { cellClassName?: Record<string, string> })[]>([]);
const instPage = ref({ total: 0, pageIndex: 1, pageSize: defaultSize });
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 tableHeight = computed(() => Utils.useTableHeight({ isTabs: true, isSearch: true, isPage: true }));
/* ============ 驳回弹窗(规范 §1/§8:必填红星 + 统一提示"请填写红色星号内容!") ============ */
const rejectVisible = ref(false);
const rejectTaskId = ref<number | null>(null);
const rejectTitle = ref("");
const rejectFormRef = ref();
const rejectForm = reactive({ opinion: "" });
const rejectRule = {
opinion: [{ required: true, message: "请输入驳回意见", trigger: "blur" }],
};
const rejectOpinion = ref("");
const todoCols = [
{ title: "通知", key: "bizTitle", minWidth: 220, align: "left", ellipsis: true, tooltip: true },
{ title: "摘要", key: "_summary", minWidth: 200, align: "left", ellipsis: true, tooltip: true },
{ title: "当前节点", key: "nodeName", width: 110, align: "center" },
{ title: "到达时间", key: "arriveTime", width: 170, align: "center", render: fmtTime },
{
title: "操作",
width: 180,
align: "center",
fixed: "right",
render: (h: any, p: any) => [
Utils.operatingTextHandle(h, "通过", "", () => approve(p.row)),
Utils.operatingTextHandle(h, "驳回", "text-error", () => openReject(p.row)),
],
},
{ 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, align: "left", ellipsis: true, tooltip: true },
{
title: "状态",
width: 100,
align: "center",
render: (h: any, p: any) => h("span", {}, instStatusText(p.row.status)),
},
{ title: "提交人", key: "submitterName", width: 110, align: "center" },
{ title: "提交时间", key: "submitTime", width: 170, align: "center", render: fmtTime },
{ title: "办结时间", key: "finishTime", width: 170, align: "center", render: fmtTime },
{ 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) || "—");
return h(
"span",
{},
String(p.row[p.column.key] || "").replace("T", " ").slice(0, 19) || "—"
);
}
function parseSummary(task: ApprovalTodoTask): string {
@@ -84,17 +169,6 @@ function parseSummary(task: ApprovalTodoTask): string {
}
}
/* ============ Tab(规范 §1:切换只 v-show,两列表分页状态各自独立) ============ */
function tabChange(name: string) {
tab.value = name as "todo" | "instances";
load();
}
function load() {
if (tab.value === "todo") loadTodo(1);
else loadInstances(1);
}
async function loadTodo(page = 1) {
loading.value = true;
try {
@@ -115,11 +189,7 @@ async function loadInstances(page = 1) {
.instances(instStatus.value || undefined, page, instPage.value.pageSize)
.send();
instPage.value = res;
instRows.value = (res.content || []).map((r) => ({
...r,
// 状态列主题色(规范 §7):view-ui-plus 的 cellClassName 挂在行上,按列 key 取
cellClassName: { status: instStatusClass(r.status) },
}));
instRows.value = res.content || [];
} catch (e: any) {
Message.error(e?.message || "办件列表加载失败");
} finally {
@@ -127,35 +197,23 @@ async function loadInstances(page = 1) {
}
}
function todoPageUpdate(p: number) {
todoPage.value.pageIndex = p;
loadTodo(p);
}
function todoPageSizeUpdate(size: number) {
todoPage.value.pageIndex = 1;
todoPage.value.pageSize = size;
localStorage.setItem("customPageCount", String(size));
loadTodo(1);
}
function instPageUpdate(p: number) {
instPage.value.pageIndex = p;
loadInstances(p);
}
function instPageSizeUpdate(size: number) {
instPage.value.pageIndex = 1;
instPage.value.pageSize = size;
localStorage.setItem("customPageCount", String(size));
loadInstances(1);
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 直接展示
// 非办理人/已被办理等业务错,后端信封 msg 直接展示
Message.error(e?.message || "操作失败");
} finally {
acting.value = false;
@@ -165,20 +223,18 @@ async function approve(row: ApprovalTodoTask) {
function openReject(row: ApprovalTodoTask) {
rejectTaskId.value = row.taskId;
rejectTitle.value = row.bizTitle;
rejectForm.opinion = "";
rejectOpinion.value = "";
rejectVisible.value = true;
nextTick(() => rejectFormRef.value?.resetFields());
}
async function doReject() {
const valid = await rejectFormRef.value?.validate();
if (!valid) {
Message.info("请填写红色星号内容!");
if (!rejectOpinion.value.trim()) {
Message.warning("驳回必须填写意见");
return;
}
acting.value = true;
try {
await approvalApi.reject(rejectTaskId.value!, rejectForm.opinion.trim()).send();
await approvalApi.reject(rejectTaskId.value!, rejectOpinion.value.trim()).send();
Message.success("已驳回");
rejectVisible.value = false;
loadTodo(todoPage.value.pageIndex);
@@ -189,111 +245,29 @@ async function doReject() {
}
}
/* 状态文案 / 主题状态色(规范 §7:cellClassName 挂 .status-color-N) */
function instStatusText(s: number) {
return ({ 10: "审批中", 20: "已完成", 30: "已拒绝", 40: "已撤回" } as Record<number, string>)[s] || String(s);
}
function instStatusClass(s: number) {
return ({ 10: "status-color-2", 20: "status-color-3", 30: "status-color-4", 40: "status-color-1" } as Record<number, string>)[s] || "";
function instStatusColor(s: number) {
return ({ 10: "blue", 20: "green", 30: "red", 40: "default" } as Record<number, string>)[s] || "default";
}
onMounted(load);
</script>
<template>
<div class="tabs-wrapper">
<Tabs class="tabs-panel" type="card" :value="tab" @on-click="tabChange">
<TabPane label="我的待审核" name="todo"></TabPane>
<TabPane label="办件视角" name="instances"></TabPane>
</Tabs>
<!-- 我的待审核 -->
<div class="main-container" v-show="tab === 'todo'">
<div class="search-wrap fix">
<Button class="fl" @click="loadTodo(1)">刷新</Button>
</div>
<Table
:columns="todoCols"
:data="todoRows"
class="link-table table-bottom-box"
:height="tableHeight"
:loading="loading"
no-data-text="暂无待审核通知"
></Table>
<div class="page-panel">
<div class="fl ml-15 align-middle"></div>
<Page
class="page-box fr"
:total="todoPage.total"
:current="todoPage.pageIndex"
:page-size="todoPage.pageSize"
:page-size-opts="[10, 20, 30, 40]"
@on-page-size-change="todoPageSizeUpdate"
@on-change="todoPageUpdate"
placement="top"
show-total
show-sizer
></Page>
</div>
</div>
<!-- 办件视角 -->
<div class="main-container" v-show="tab === 'instances'">
<div class="search-wrap fix">
<RadioGroup v-model="instStatus" class="fl" 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>
<Table
:columns="instCols"
:data="instRows"
class="link-table table-bottom-box"
:height="tableHeight"
:loading="loading"
no-data-text="暂无办件"
></Table>
<div class="page-panel">
<div class="fl ml-15 align-middle"></div>
<Page
class="page-box fr"
:total="instPage.total"
:current="instPage.pageIndex"
:page-size="instPage.pageSize"
:page-size-opts="[10, 20, 30, 40]"
@on-page-size-change="instPageSizeUpdate"
@on-change="instPageUpdate"
placement="top"
show-total
show-sizer
></Page>
</div>
</div>
<!-- 驳回弹窗:意见必填 -->
<Modal v-model="rejectVisible" :mask-closable="false" width="450px" title="驳回审核">
<Form ref="rejectFormRef" :model="rejectForm" :rules="rejectRule" :label-width="90">
<FormItem label="通知:">
<span class="reject-title">{{ rejectTitle || "—" }}</span>
</FormItem>
<FormItem label="驳回意见:" prop="opinion">
<Input type="textarea" :rows="3" placeholder="请输入驳回意见" v-model="rejectForm.opinion" />
</FormItem>
</Form>
<template #footer>
<Button type="primary" :loading="acting" @click="doReject">提交</Button>
<Button @click="rejectVisible = false">取消</Button>
</template>
</Modal>
</div>
</template>
<style lang="scss" scoped>
.reject-title {
color: #515a6e;
word-break: break-all;
<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>
+71
View File
@@ -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>
+111 -46
View File
@@ -6,6 +6,7 @@ import { noticeApi } from "@/api/module/notice";
import type { NoticeDetailVo, NextPrevVo, ReadGroup, FlowTraceVo } from "@/types/notice";
import FilePreviewModal from "@/components/FilePreviewModal.vue";
import Utils from "@/utils";
import { fitDiagram } from "@/utils/diagramImg";
import config from "@/assets/config/host.config";
const route = useRoute();
@@ -77,6 +78,7 @@ async function flowAction(action: "withdraw" | "urge") {
/* 流程图弹窗(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;
@@ -90,6 +92,24 @@ 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 showRead = ref(false);
const prevId = ref(0);
@@ -189,7 +209,7 @@ function downAll() {
function displayName(item: { name?: string; filename?: string }) {
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) {
const suffix = String(name || "").split(".").pop()?.toLowerCase() || "";
if (["doc", "docx", "wps"].includes(suffix)) return "icon-word";
@@ -197,7 +217,8 @@ function getFileIcon(name: string) {
if (["ppt", "pptx"].includes(suffix)) return "icon-ppt";
if (["pdf"].includes(suffix)) return "icon-pdf1";
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";
return "icon-weizhi";
}
@@ -265,6 +286,13 @@ function openPreview(item: { name?: string; filename?: string; fileid?: number;
<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"
@@ -320,10 +348,17 @@ function openPreview(item: { name?: string; filename?: string; fileid?: number;
<!-- 流程图弹窗(后端同源出图,免 token 免跨域) -->
<Modal v-model="diagramVisible" title="流程示意图" :width="860" footer-hide>
<div class="diagram-wrap">
<img v-if="diagramSrc" :src="diagramSrc" alt="流程示意图" @error="onDiagramError" />
<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">
<span>发文单位:{{ detail.DeptName || "—" }}</span>
<span>发布人:{{ detail.TeaName || "—" }}</span>
@@ -384,6 +419,19 @@ function openPreview(item: { name?: string; filename?: string; fileid?: number;
</template>
<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 {
display: flex;
justify-content: space-between;
@@ -395,14 +443,16 @@ function openPreview(item: { name?: string; filename?: string; fileid?: number;
}
.notice-title2 {
font-size: 20px;
font-weight: 600;
color: var(--text);
margin: 0 0 12px;
}
.flow-card {
border: 1px solid #e8eaec;
border-radius: 4px;
border: 1px solid var(--line);
border-radius: var(--r-s);
padding: 10px 14px;
margin-bottom: 14px;
background: #f8f8f9;
background: #f7f9fc;
}
.flow-card-head {
display: flex;
@@ -413,10 +463,10 @@ function openPreview(item: { name?: string; filename?: string; fileid?: number;
}
.flow-title {
font-weight: 600;
color: #17233d;
color: var(--text);
}
.flow-nodes {
color: #808695;
color: var(--text-3);
}
.flow-ops {
margin-left: auto;
@@ -424,24 +474,24 @@ function openPreview(item: { name?: string; filename?: string; fileid?: number;
.flow-opinion {
margin-top: 8px;
padding: 6px 10px;
background: #fff;
border-radius: 4px;
color: #ed4014;
background: var(--panel);
border-radius: var(--r-s);
color: var(--red);
font-size: 13px;
}
.flow-timeline {
margin-top: 8px;
background: #fff;
border-radius: 4px;
background: var(--panel);
border-radius: var(--r-s);
padding: 6px 10px;
}
.flow-step {
display: flex;
gap: 12px;
font-size: 13px;
color: #515a6e;
color: var(--text-2);
padding: 3px 0;
border-bottom: 1px dashed #f0f0f0;
border-bottom: 1px dashed var(--line);
}
.flow-step:last-child {
border-bottom: none;
@@ -451,21 +501,21 @@ function openPreview(item: { name?: string; filename?: string; fileid?: number;
}
.flow-step-user {
min-width: 90px;
color: #2d8cf0;
color: var(--brand);
}
.flow-step-time {
color: #999;
color: var(--text-3);
}
.flow-step-op {
color: #ed4014;
color: var(--red);
}
.flow-step-active .flow-step-node {
color: #2d8cf0;
color: var(--brand);
font-weight: 600;
}
.flow-step-status {
font-style: normal;
color: #808695;
color: var(--text-3);
margin-left: 4px;
}
.flow-step .flow-step-op {
@@ -477,15 +527,26 @@ function openPreview(item: { name?: string; filename?: string; fileid?: number;
}
.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 {
display: flex;
flex-wrap: wrap;
gap: 18px;
padding: 8px 0;
border-bottom: 1px dashed #e8e8e8;
border-bottom: 1px dashed var(--line);
font-size: 13px;
color: #808695;
color: var(--text-3);
}
/* 管理视角统计条 */
.stat-bar {
@@ -494,7 +555,7 @@ function openPreview(item: { name?: string; filename?: string; fileid?: number;
gap: 24px;
padding: 4px 0;
font-size: 13px;
color: #17233d;
color: var(--text);
flex-wrap: wrap;
}
.progress-box {
@@ -504,7 +565,7 @@ function openPreview(item: { name?: string; filename?: string; fileid?: number;
width: 300px;
}
.percent-label {
color: #f60;
color: var(--brand);
white-space: nowrap;
font-size: 13px;
}
@@ -516,14 +577,14 @@ function openPreview(item: { name?: string; filename?: string; fileid?: number;
cursor: pointer;
}
.stat-nums .item {
color: #8a94a6;
color: var(--text-3);
}
.stat-nums b {
font-size: 20px;
margin: 0 2px;
}
.stat-nums .blue {
color: #5c9edc;
color: var(--brand);
}
.stat-nums .orange {
color: #f60;
@@ -534,66 +595,70 @@ function openPreview(item: { name?: string; filename?: string; fileid?: number;
flex-wrap: wrap;
}
.divider {
border-bottom: 1px dashed #e8e8e8;
border-bottom: 1px dashed var(--line);
margin: 8px 0;
}
.content {
padding: 16px 0;
/* 正文区最小高度:短通知别让附件/按钮紧贴一行字(旧版用 clientHeight-356 铺满视口,
会把附件顶出首屏;这里只留"呼吸区",剩余空白落在按钮下方的卡片底部) */
min-height: 200px;
line-height: 1.8;
word-break: break-all;
color: var(--text);
}
.files {
border-top: 1px dashed #e8e8e8;
border-top: 1px dashed var(--line);
padding-top: 12px;
}
.files-title {
font-size: 13px;
color: #808695;
color: var(--text-3);
margin-bottom: 8px;
}
.down-all {
margin-left: 12px;
color: #2d8cf0;
color: var(--brand);
cursor: pointer;
}
.file-item {
display: inline-flex;
align-items: center;
gap: 6px;
color: #515a6e;
color: var(--text-2);
margin: 0 16px 8px 0;
font-size: 13px;
.file-type-icon {
font-size: 16px;
color: #515a6e;
color: var(--text-2);
}
.file-name {
max-width: 320px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.file-size {
color: #999;
color: var(--text-3);
font-size: 12px;
}
.file-op {
color: #2d8cf0;
color: var(--brand);
cursor: pointer;
}
}
.bottom-btns {
border-top: 1px dashed #e8e8e8;
border-top: 1px dashed var(--line);
padding-top: 14px;
margin-top: 8px;
}
/* 阅读情况弹窗 */
.read-tip {
background: #e8f4ff;
border: 1px solid #d3e9ff;
color: #515a6e;
background: var(--brand-soft);
border: 1px solid var(--line);
color: var(--text-2);
padding: 8px 12px;
border-radius: 4px;
border-radius: var(--r-s);
font-size: 13px;
margin-bottom: 12px;
}
@@ -610,7 +675,7 @@ function openPreview(item: { name?: string; filename?: string; fileid?: number;
flex: none;
width: 130px;
text-align: right;
color: #515a6e;
color: var(--text-2);
font-size: 13px;
line-height: 22px;
}
@@ -626,13 +691,13 @@ function openPreview(item: { name?: string; filename?: string; fileid?: number;
cursor: default;
}
.green {
color: #19be6b;
color: var(--green);
}
.red {
color: #ed4014;
color: var(--red);
}
.empty {
color: #999;
color: var(--text-3);
font-size: 13px;
}
</style>
+38 -29
View File
@@ -1,5 +1,5 @@
<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 { useRequest } from "alova/client";
import { Message } from "view-ui-plus";
@@ -7,6 +7,7 @@ import { noticeApi } from "@/api/module/notice";
import type { NoticeVo, NoticeQuery } from "@/types/notice";
import MyConfirm from "@/components/MyConfirm";
import Utils from "@/utils";
import { useElementHeight } from "@/utils/hook/useElementHeight";
const route = useRoute();
const router = useRouter();
@@ -32,9 +33,10 @@ const auditStatusOptions = [
{ value: -1, label: "无需审核" },
];
function fmtDate(d: Date) {
function fmtDate(d: Date | string) {
const dt = new Date(d);
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 页长记忆) ============ */
@@ -54,8 +56,9 @@ async function run() {
pageSize: pageSize.value,
orderByTime: "1",
};
if (startDt.value) params.startTime = startDt.value;
if (endDt.value) params.endTime = endDt.value;
// DatePicker 选过日期后 v-model 值是 Date 对象,后端 LocalDate 只认 yyyy-MM-dd → 统一格式化
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) {
params.scope = "user";
@@ -143,9 +146,10 @@ function del(row: NoticeVo) {
}
/* 附件下载:逐个触发(间隔 500ms,同旧版) */
function downloadFiles(row: NoticeVo & { UpFile?: string }) {
if (!row.UpFile) return;
row.UpFile.split(",").forEach((id: string, i: number) => {
function downloadFiles(row: NoticeVo) {
const upFile = row.upFile ?? row.UpFile;
if (!upFile) return;
upFile.split(",").forEach((id: string, i: number) => {
setTimeout(() => {
window.location.href = noticeApi.fileDownUrl(Number(id));
}, i * 500);
@@ -170,9 +174,10 @@ const columns = [
align: "center",
render: (h: any, p: any) => {
const st = p.row.auditStatus;
if (st === null || st === undefined) return h("span", { style: "color:#999;" }, "—");
const colors: Record<number, string> = { 0: "default", 10: "blue", 20: "green", 30: "red", 40: "orange" };
return h("Tag", { color: colors[st] || "default" }, () => p.row.auditStatusDesc || "—");
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" },
@@ -187,9 +192,9 @@ const columns = [
width: 70,
align: "center",
render: (h: any, p: any) =>
p.row.UpFile
(p.row.upFile ?? p.row.UpFile)
? Utils.operatingHandle(h, "附件下载", "icon-attachment", () => downloadFiles(p.row))
: h("span", { style: "color:#999;" }, "无"),
: h("span", { style: "color:var(--text-3);" }, "无"),
},
{
title: "操作",
@@ -197,21 +202,23 @@ const columns = [
align: "center",
fixed: "right",
render: (h: any, p: any) => {
// BP-R5-1:审批中(audit=10)禁止删除(后端 403)——不再渲染死按钮,审批通过/驳回/撤回后恢复
const ops = [Utils.operatingHandle(h, "查看通知", "icon-browse", () => openDetail(p.row))];
if (p.row.auditStatus !== 10) {
ops.push(Utils.operatingHandle(h, "删除通知", "icon-ashbin", () => del(p.row)));
}
return ops;
// BP-R5-1:审批中(audit=10)禁止删除(后端 403)——渲染禁用态而非隐藏,保持操作列对齐
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>
<template>
<div class="page-wrapper">
<div class="page-wrapper yh-fill">
<div class="main-container">
<!-- 1. 操作按钮区 -->
<div class="fix mb-12">
@@ -250,14 +257,16 @@ const tableHeight = computed(() => Utils.useTableHeight({ isSearch: true, isOper
</div>
<!-- 3. 表格 + 分页 -->
<Table
:columns="columns"
:data="page.content"
class="link-table table-bottom-box"
:height="tableHeight"
:loading="loading"
no-data-text="暂无数据"
></Table>
<div class="table-region" ref="tableRegionRef">
<Table
:columns="columns"
:data="page.content"
class="link-table table-bottom-box"
:height="tableH || undefined"
:loading="loading"
no-data-text="暂无数据"
></Table>
</div>
<div class="page-panel">
<div class="fl ml-15 align-middle"></div>
<Page
+75 -23
View File
@@ -9,8 +9,10 @@ import { settingApi, baseApi, noticeApi } from "@/api/module/notice";
import type { DeptManageItem, ContactGroupPage, ContactUserPage, SchoolTreeNode, FlowOption } from "@/types/notice";
import config from "@/assets/config/host.config";
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);
function tabChange(name: string) {
@@ -79,9 +81,30 @@ function unbindFlow() {
});
}
/** 流程图(flow-center 示意图深链,与发通知页同一入口) */
function openFlowDiagram(guid: string) {
if (guid) window.open(`${config.flowHost}/#/diagram?flowGuid=${guid}`, "_blank");
/** 流程图 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("流程图加载失败(流程可能未发布或绑定已解除)");
}
/** 当前绑定流程的节点摘要(下拉里回显) */
@@ -397,19 +420,22 @@ 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>
<template>
<div class="tabs-wrapper">
<Tabs class="tabs-panel" type="card" :value="String(currentType)" @on-click="tabChange">
<div class="page-wrapper yh-fill">
<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="3"></TabPane>
<TabPane label="流程绑定" name="2"></TabPane>
</Tabs>
<!-- 发文部门 -->
<div class="main-container" v-show="currentType === 1">
<div class="tab-body" v-show="currentType === 1">
<Alert show-icon>设置说明:拖动卡片调整部门排序;停用的部门不出现在「通知发送」的部门下拉中</Alert>
<div class="search-wrap fix">
<Button class="fl" type="primary" @click="addDeptShow">新增部门</Button>
@@ -437,17 +463,19 @@ const groupTableHeight = computed(() => Utils.useTableHeight({ isTabs: true, isS
</div>
<!-- 自定义组 -->
<div class="main-container" v-show="currentType === 3">
<div class="tab-body" v-show="currentType === 3">
<div class="search-wrap fix">
<Button class="fl" type="primary" @click="addGroupShow">新建联系组</Button>
</div>
<Table
:columns="groupColumns"
:data="groupData"
class="link-table table-bottom-box"
:height="groupTableHeight"
no-data-text="暂无数据"
></Table>
<div class="table-region" ref="groupRegionRef">
<Table
:columns="groupColumns"
:data="groupData"
class="link-table table-bottom-box"
:height="groupH || undefined"
no-data-text="暂无数据"
></Table>
</div>
<div class="page-panel">
<div class="fl ml-15 align-middle"></div>
<Page
@@ -478,7 +506,7 @@ const groupTableHeight = computed(() => Utils.useTableHeight({ isTabs: true, isS
</div>
<!-- 流程绑定(应用设置 > 流程绑定) -->
<div class="main-container" v-show="currentType === 2">
<div class="tab-body" v-show="currentType === 2">
<Alert show-icon type="warning">
绑定说明:绑定后「发送全部用户」的通知必须审核通过才发布(发通知页只显示「提交审核」按钮,
隐藏发送与定时发送);「发送指定人员」不受影响,仍直接发布。未绑定则保持现有发送方式。
@@ -502,6 +530,7 @@ const groupTableHeight = computed(() => Utils.useTableHeight({ isTabs: true, isS
<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>
@@ -511,7 +540,7 @@ const groupTableHeight = computed(() => Utils.useTableHeight({ isTabs: true, isS
已绑定
<Tag color="blue">{{ boundFlowName || boundFlowGuid }}</Tag>
<span class="node-summary">{{ boundFlowSummary }}</span>
<Button type="text" size="small" @click="openFlowDiagram(boundFlowGuid)">查看流程图</Button>
<Button type="text" size="small" @click="openFlowDiagram">查看流程图</Button>
</template>
<template v-else>
<Tag>未绑定</Tag>
@@ -520,6 +549,15 @@ const groupTableHeight = computed(() => Utils.useTableHeight({ isTabs: true, isS
</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 v-model="deptModal" :mask-closable="false" width="450px" title="新增部门">
@@ -601,23 +639,37 @@ const groupTableHeight = computed(() => Utils.useTableHeight({ isTabs: true, isS
gap: 10px;
}
.flow-binding-form .label {
color: #515a6e;
color: var(--text-2);
}
.flow-binding-state {
color: #515a6e;
color: var(--text-2);
display: flex;
align-items: center;
gap: 6px;
}
.flow-binding-state .node-summary {
color: #808695;
color: var(--text-3);
}
.readonly-hint {
color: #808695;
color: var(--text-3);
margin-left: 8px;
}
.flow-binding-empty {
margin-top: 16px;
color: #808695;
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>
+3 -4
View File
@@ -7,9 +7,9 @@ import { Icon } from "view-ui-plus";
</script>
<template>
<div class="page-wrapper">
<div class="page-wrapper yh-fill">
<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>
</div>
</div>
@@ -21,8 +21,7 @@ import { Icon } from "view-ui-plus";
flex-direction: column;
align-items: center;
justify-content: center;
min-height: calc(100vh - 136px);
color: #999;
color: var(--text-3);
}
.tip {
margin-top: 16px;
+12 -1
View File
@@ -73,7 +73,6 @@ export default defineConfig(({ command, mode }: ConfigEnv): UserConfig => {
"flex-center": "flex items-center justify-center",
// 常用组件
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",
// 文本截断
"text-ellipsis": "truncate",
@@ -90,6 +89,9 @@ export default defineConfig(({ command, mode }: ConfigEnv): UserConfig => {
},
},
}),
// 主题接入已免插件化(A 类,YschTheme《管理端主题远程化与公共静态通道方案》§11):
// 唯一接入点 = index.html 一段同步 loader,挂载/FOUC/CSS 排序全由通道 init 承担;
// 下面的 /static 代理只是 dev 联调便利(prod 同源用不到),删除不影响线上主题
],
resolve: {
alias: {
@@ -106,6 +108,15 @@ export default defineConfig(({ command, mode }: ConfigEnv): UserConfig => {
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,
},
},
},
// css预处理器