feat(通知公告): 发布表单接入模块页同款「通知对象」与「正文」控件
build / build (push) Successful in 3m28s

按用户要求,把 NoticeFront 里这两块 UI 原样搬到助手表单(不再自制近似控件):

- 正文 → wangEditor v5(@wangeditor/editor + @wangeditor/editor-for-vue)。
  提交给后端的是 **HTML 字符串**,与模块页 NoticeRequest.content 一致;
  插图上传走 /api/file/UploadImage、预览走 /api/file/Preview?id=(地址由后端下发,
  统一由编排层拼模块前缀)。异步 chunk 加载,避免把 800KB 编辑器塞进首屏。
- 通知对象 → 1:1 搬运模块页 components/UserCheckInput.vue + UserCheckModal.vue:
  radio「全部用户 / 指定用户」+ 只读回显 + +/清空 + 选人弹窗(左 zTree 组织树勾选级联 /
  右已选列表 / 全选 / 删除选中 / 按名字搜索回勾);v-model 值是字符串("all" 或 id 逗号串)。
- 全局依赖 jQuery + zTree 按模块页的方式由 index.html 全局加载(public/libs/),
  不用 npm 引入 —— 保证加载顺序(zTree 必须在 jQuery 之后)与样式/图标与模块页完全一致。
  已确认模块页的 lodash 实际未被使用,故不引入。

FormCard 新增两个 widget 分支,每一处都对齐模块页口径:

- richtext:初值取字符串、提交原 HTML,只读摘要用剥标签后的纯文本;
- userpicker:初值数组 join / 字符串原样(空则 allToken)、提交字符串;
  ★ 跳过下拉字典的「拍平 + 取值对齐」—— 那条路会把人员 id 串判成「认不出的值」清掉,
    等于把模型/用户已经选好的人悄悄抹掉;
- 两者都独占整行(自适应多列会把单选组挤到换行)。

验证:.scratch/verify-notice-publish-widgets.mjs 真机 38/38(含正文真实打字、
两个数据源页签、勾选回显、以及提交体 receiveList 必须是字符串 "101"、content 必须是 <p>…)。
npm run build 通过(RichTextEditor 单独成 chunk)。
This commit is contained in:
lijing
2026-10-14 15:19:08 +08:00
parent 3dc6cc561a
commit 6610623cf6
19 changed files with 1960 additions and 3 deletions
+8
View File
@@ -26,6 +26,14 @@
})();
</script>
<title><%- title %></title>
<!-- 业务模块组件的全局依赖(照搬 NoticeFront/index.html 的加载方式)。
★ zTree 是 jQuery 插件,**必须**在 jquery 之后、且在应用脚本之前加载:
组件里的 `$.fn.zTree` 是在方法调用期求值的,晚一步就会 `undefined.init`。
为什么不用 npm 引入:模块前端本来就是用 public/libs 下的静态文件全局加载的,
搬过来保持同一种加载方式,行为与模块页完全一致(含 zTree 自带样式与图标)。 -->
<link rel="stylesheet" href="./libs/ztree/zTreeStyle.css" />
<script src="./libs/jquery.min.js"></script>
<script src="./libs/ztree/jquery.ztree.all.min.js"></script>
<%- injectScript %>
</head>
+548
View File
@@ -8,6 +8,8 @@
"name": "agent-front",
"version": "1.0.0",
"dependencies": {
"@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^5.1.12",
"alova": "^3.4.0",
"lodash": "^4.17.21",
"mitt": "^3.0.1",
@@ -112,6 +114,15 @@
"node": ">=6.0.0"
}
},
"node_modules/@babel/runtime": {
"version": "7.29.10",
"resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.29.10.tgz",
"integrity": "sha512-NpugTzbKgGrNe45Ui32cN8vCFFMxVtP/9rtfwv0nP5FRDAjvYBxJcLVpYA9mr1jxP6cnIT7x9LUzuDTbWYYNRA==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/types": {
"version": "7.29.8",
"license": "MIT",
@@ -1338,11 +1349,23 @@
"win32"
]
},
"node_modules/@transloadit/prettier-bytes": {
"version": "0.0.7",
"resolved": "https://registry.npmmirror.com/@transloadit/prettier-bytes/-/prettier-bytes-0.0.7.tgz",
"integrity": "sha512-VeJbUb0wEKbcwaSlj5n+LscBl9IPgLPkHVGBkh00cztv6X4L/TJXK58LzFuBKX7/GAfiGhIwH67YTLTlzvIzBA==",
"license": "MIT"
},
"node_modules/@types/estree": {
"version": "1.0.9",
"dev": true,
"license": "MIT"
},
"node_modules/@types/event-emitter": {
"version": "0.3.5",
"resolved": "https://registry.npmmirror.com/@types/event-emitter/-/event-emitter-0.3.5.tgz",
"integrity": "sha512-zx2/Gg0Eg7gwEiOIIh5w9TrhKKTeQh7CPCOPNc0el4pLSwzebA8SmnHwZs2dWlLONvyulykSwGSQxQHLhjGLvQ==",
"license": "MIT"
},
"node_modules/@types/lodash": {
"version": "4.17.26",
"dev": true,
@@ -1680,6 +1703,61 @@
"vite": "^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0 || ^8.0.0-0"
}
},
"node_modules/@uppy/companion-client": {
"version": "2.2.2",
"resolved": "https://registry.npmmirror.com/@uppy/companion-client/-/companion-client-2.2.2.tgz",
"integrity": "sha512-5mTp2iq97/mYSisMaBtFRry6PTgZA6SIL7LePteOV5x0/DxKfrZW3DEiQERJmYpHzy7k8johpm2gHnEKto56Og==",
"license": "MIT",
"dependencies": {
"@uppy/utils": "^4.1.2",
"namespace-emitter": "^2.0.1"
}
},
"node_modules/@uppy/core": {
"version": "2.3.4",
"resolved": "https://registry.npmmirror.com/@uppy/core/-/core-2.3.4.tgz",
"integrity": "sha512-iWAqppC8FD8mMVqewavCz+TNaet6HPXitmGXpGGREGrakZ4FeuWytVdrelydzTdXx6vVKkOmI2FLztGg73sENQ==",
"license": "MIT",
"dependencies": {
"@transloadit/prettier-bytes": "0.0.7",
"@uppy/store-default": "^2.1.1",
"@uppy/utils": "^4.1.3",
"lodash.throttle": "^4.1.1",
"mime-match": "^1.0.2",
"namespace-emitter": "^2.0.1",
"nanoid": "^3.1.25",
"preact": "^10.5.13"
}
},
"node_modules/@uppy/store-default": {
"version": "2.1.1",
"resolved": "https://registry.npmmirror.com/@uppy/store-default/-/store-default-2.1.1.tgz",
"integrity": "sha512-xnpTxvot2SeAwGwbvmJ899ASk5tYXhmZzD/aCFsXePh/v8rNvR2pKlcQUH7cF/y4baUGq3FHO/daKCok/mpKqQ==",
"license": "MIT"
},
"node_modules/@uppy/utils": {
"version": "4.1.3",
"resolved": "https://registry.npmmirror.com/@uppy/utils/-/utils-4.1.3.tgz",
"integrity": "sha512-nTuMvwWYobnJcytDO3t+D6IkVq/Qs4Xv3vyoEZ+Iaf8gegZP+rEyoaFT2CK5XLRMienPyqRqNbIfRuFaOWSIFw==",
"license": "MIT",
"dependencies": {
"lodash.throttle": "^4.1.1"
}
},
"node_modules/@uppy/xhr-upload": {
"version": "2.1.3",
"resolved": "https://registry.npmmirror.com/@uppy/xhr-upload/-/xhr-upload-2.1.3.tgz",
"integrity": "sha512-YWOQ6myBVPs+mhNjfdWsQyMRWUlrDLMoaG7nvf/G6Y3GKZf8AyjFDjvvJ49XWQ+DaZOftGkHmF1uh/DBeGivJQ==",
"license": "MIT",
"dependencies": {
"@uppy/companion-client": "^2.2.2",
"@uppy/utils": "^4.1.2",
"nanoid": "^3.1.25"
},
"peerDependencies": {
"@uppy/core": "^2.3.3"
}
},
"node_modules/@vitejs/plugin-vue": {
"version": "5.2.4",
"resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz",
@@ -1879,6 +1957,165 @@
"version": "3.5.43",
"license": "MIT"
},
"node_modules/@wangeditor/basic-modules": {
"version": "1.1.7",
"resolved": "https://registry.npmmirror.com/@wangeditor/basic-modules/-/basic-modules-1.1.7.tgz",
"integrity": "sha512-cY9CPkLJaqF05STqfpZKWG4LpxTMeGSIIF1fHvfm/mz+JXatCagjdkbxdikOuKYlxDdeqvOeBmsUBItufDLXZg==",
"license": "MIT",
"dependencies": {
"is-url": "^1.2.4"
},
"peerDependencies": {
"@wangeditor/core": "1.x",
"dom7": "^3.0.0",
"lodash.throttle": "^4.1.1",
"nanoid": "^3.2.0",
"slate": "^0.72.0",
"snabbdom": "^3.1.0"
}
},
"node_modules/@wangeditor/code-highlight": {
"version": "1.0.3",
"resolved": "https://registry.npmmirror.com/@wangeditor/code-highlight/-/code-highlight-1.0.3.tgz",
"integrity": "sha512-iazHwO14XpCuIWJNTQTikqUhGKyqj+dUNWJ9288Oym9M2xMVHvnsOmDU2sgUDWVy+pOLojReMPgXCsvvNlOOhw==",
"license": "MIT",
"dependencies": {
"prismjs": "^1.23.0"
},
"peerDependencies": {
"@wangeditor/core": "1.x",
"dom7": "^3.0.0",
"slate": "^0.72.0",
"snabbdom": "^3.1.0"
}
},
"node_modules/@wangeditor/core": {
"version": "1.1.19",
"resolved": "https://registry.npmmirror.com/@wangeditor/core/-/core-1.1.19.tgz",
"integrity": "sha512-KevkB47+7GhVszyYF2pKGKtCSj/YzmClsD03C3zTt+9SR2XWT5T0e3yQqg8baZpcMvkjs1D8Dv4fk8ok/UaS2Q==",
"license": "MIT",
"dependencies": {
"@types/event-emitter": "^0.3.3",
"event-emitter": "^0.3.5",
"html-void-elements": "^2.0.0",
"i18next": "^20.4.0",
"scroll-into-view-if-needed": "^2.2.28",
"slate-history": "^0.66.0"
},
"peerDependencies": {
"@uppy/core": "^2.1.1",
"@uppy/xhr-upload": "^2.0.3",
"dom7": "^3.0.0",
"is-hotkey": "^0.2.0",
"lodash.camelcase": "^4.3.0",
"lodash.clonedeep": "^4.5.0",
"lodash.debounce": "^4.0.8",
"lodash.foreach": "^4.5.0",
"lodash.isequal": "^4.5.0",
"lodash.throttle": "^4.1.1",
"lodash.toarray": "^4.4.0",
"nanoid": "^3.2.0",
"slate": "^0.72.0",
"snabbdom": "^3.1.0"
}
},
"node_modules/@wangeditor/editor": {
"version": "5.1.23",
"resolved": "https://registry.npmmirror.com/@wangeditor/editor/-/editor-5.1.23.tgz",
"integrity": "sha512-0RxfeVTuK1tktUaPROnCoFfaHVJpRAIE2zdS0mpP+vq1axVQpLjM8+fCvKzqYIkH0Pg+C+44hJpe3VVroSkEuQ==",
"license": "MIT",
"dependencies": {
"@uppy/core": "^2.1.1",
"@uppy/xhr-upload": "^2.0.3",
"@wangeditor/basic-modules": "^1.1.7",
"@wangeditor/code-highlight": "^1.0.3",
"@wangeditor/core": "^1.1.19",
"@wangeditor/list-module": "^1.0.5",
"@wangeditor/table-module": "^1.1.4",
"@wangeditor/upload-image-module": "^1.0.2",
"@wangeditor/video-module": "^1.1.4",
"dom7": "^3.0.0",
"is-hotkey": "^0.2.0",
"lodash.camelcase": "^4.3.0",
"lodash.clonedeep": "^4.5.0",
"lodash.debounce": "^4.0.8",
"lodash.foreach": "^4.5.0",
"lodash.isequal": "^4.5.0",
"lodash.throttle": "^4.1.1",
"lodash.toarray": "^4.4.0",
"nanoid": "^3.2.0",
"slate": "^0.72.0",
"snabbdom": "^3.1.0"
}
},
"node_modules/@wangeditor/editor-for-vue": {
"version": "5.1.12",
"resolved": "https://registry.npmmirror.com/@wangeditor/editor-for-vue/-/editor-for-vue-5.1.12.tgz",
"integrity": "sha512-0Ds3D8I+xnpNWezAeO7HmPRgTfUxHLMd9JKcIw+QzvSmhC5xUHbpCcLU+KLmeBKTR/zffnS5GQo6qi3GhTMJWQ==",
"license": "MIT",
"peerDependencies": {
"@wangeditor/editor": ">=5.1.0",
"vue": "^3.0.5"
}
},
"node_modules/@wangeditor/list-module": {
"version": "1.0.5",
"resolved": "https://registry.npmmirror.com/@wangeditor/list-module/-/list-module-1.0.5.tgz",
"integrity": "sha512-uDuYTP6DVhcYf7mF1pTlmNn5jOb4QtcVhYwSSAkyg09zqxI1qBqsfUnveeDeDqIuptSJhkh81cyxi+MF8sEPOQ==",
"license": "MIT",
"peerDependencies": {
"@wangeditor/core": "1.x",
"dom7": "^3.0.0",
"slate": "^0.72.0",
"snabbdom": "^3.1.0"
}
},
"node_modules/@wangeditor/table-module": {
"version": "1.1.4",
"resolved": "https://registry.npmmirror.com/@wangeditor/table-module/-/table-module-1.1.4.tgz",
"integrity": "sha512-5saanU9xuEocxaemGdNi9t8MCDSucnykEC6jtuiT72kt+/Hhh4nERYx1J20OPsTCCdVr7hIyQenFD1iSRkIQ6w==",
"license": "MIT",
"peerDependencies": {
"@wangeditor/core": "1.x",
"dom7": "^3.0.0",
"lodash.isequal": "^4.5.0",
"lodash.throttle": "^4.1.1",
"nanoid": "^3.2.0",
"slate": "^0.72.0",
"snabbdom": "^3.1.0"
}
},
"node_modules/@wangeditor/upload-image-module": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/@wangeditor/upload-image-module/-/upload-image-module-1.0.2.tgz",
"integrity": "sha512-z81lk/v71OwPDYeQDxj6cVr81aDP90aFuywb8nPD6eQeECtOymrqRODjpO6VGvCVxVck8nUxBHtbxKtjgcwyiA==",
"license": "MIT",
"peerDependencies": {
"@uppy/core": "^2.0.3",
"@uppy/xhr-upload": "^2.0.3",
"@wangeditor/basic-modules": "1.x",
"@wangeditor/core": "1.x",
"dom7": "^3.0.0",
"lodash.foreach": "^4.5.0",
"slate": "^0.72.0",
"snabbdom": "^3.1.0"
}
},
"node_modules/@wangeditor/video-module": {
"version": "1.1.4",
"resolved": "https://registry.npmmirror.com/@wangeditor/video-module/-/video-module-1.1.4.tgz",
"integrity": "sha512-ZdodDPqKQrgx3IwWu4ZiQmXI8EXZ3hm2/fM6E3t5dB8tCaIGWQZhmqd6P5knfkRAd3z2+YRSRbxOGfoRSp/rLg==",
"license": "MIT",
"peerDependencies": {
"@uppy/core": "^2.1.4",
"@uppy/xhr-upload": "^2.0.7",
"@wangeditor/core": "1.x",
"dom7": "^3.0.0",
"nanoid": "^3.2.0",
"slate": "^0.72.0",
"snabbdom": "^3.1.0"
}
},
"node_modules/acorn": {
"version": "8.19.0",
"dev": true,
@@ -2456,6 +2693,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/compute-scroll-into-view": {
"version": "1.0.20",
"resolved": "https://registry.npmmirror.com/compute-scroll-into-view/-/compute-scroll-into-view-1.0.20.tgz",
"integrity": "sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg==",
"license": "MIT"
},
"node_modules/computeds": {
"version": "0.0.1",
"dev": true,
@@ -2606,6 +2849,19 @@
"version": "3.2.3",
"license": "MIT"
},
"node_modules/d": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/d/-/d-1.0.2.tgz",
"integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==",
"license": "ISC",
"dependencies": {
"es5-ext": "^0.10.64",
"type": "^2.7.2"
},
"engines": {
"node": ">=0.12"
}
},
"node_modules/data-view-buffer": {
"version": "1.0.2",
"dev": true,
@@ -2813,6 +3069,15 @@
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
}
},
"node_modules/dom7": {
"version": "3.0.0",
"resolved": "https://registry.npmmirror.com/dom7/-/dom7-3.0.0.tgz",
"integrity": "sha512-oNlcUdHsC4zb7Msx7JN3K0Nro1dzJ48knvBOnDPKJ2GV9wl1i5vydJZUSyOfrkKFDZEud/jBsTk92S/VGSAe/g==",
"license": "MIT",
"dependencies": {
"ssr-window": "^3.0.0-alpha.1"
}
},
"node_modules/domelementtype": {
"version": "2.3.0",
"dev": true,
@@ -3078,11 +3343,51 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/es5-ext": {
"version": "0.10.64",
"resolved": "https://registry.npmmirror.com/es5-ext/-/es5-ext-0.10.64.tgz",
"integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==",
"hasInstallScript": true,
"license": "ISC",
"dependencies": {
"es6-iterator": "^2.0.3",
"es6-symbol": "^3.1.3",
"esniff": "^2.0.1",
"next-tick": "^1.1.0"
},
"engines": {
"node": ">=0.10"
}
},
"node_modules/es6-iterator": {
"version": "2.0.3",
"resolved": "https://registry.npmmirror.com/es6-iterator/-/es6-iterator-2.0.3.tgz",
"integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==",
"license": "MIT",
"dependencies": {
"d": "1",
"es5-ext": "^0.10.35",
"es6-symbol": "^3.1.1"
}
},
"node_modules/es6-promise": {
"version": "3.3.1",
"dev": true,
"license": "MIT"
},
"node_modules/es6-symbol": {
"version": "3.1.4",
"resolved": "https://registry.npmmirror.com/es6-symbol/-/es6-symbol-3.1.4.tgz",
"integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==",
"license": "ISC",
"dependencies": {
"d": "^1.0.2",
"ext": "^1.7.0"
},
"engines": {
"node": ">=0.12"
}
},
"node_modules/esbuild": {
"version": "0.25.12",
"dev": true,
@@ -3499,6 +3804,21 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/esniff": {
"version": "2.0.1",
"resolved": "https://registry.npmmirror.com/esniff/-/esniff-2.0.1.tgz",
"integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==",
"license": "ISC",
"dependencies": {
"d": "^1.0.1",
"es5-ext": "^0.10.62",
"event-emitter": "^0.3.5",
"type": "^2.7.2"
},
"engines": {
"node": ">=0.10"
}
},
"node_modules/estree-walker": {
"version": "3.0.3",
"dev": true,
@@ -3515,6 +3835,16 @@
"node": ">= 0.6"
}
},
"node_modules/event-emitter": {
"version": "0.3.5",
"resolved": "https://registry.npmmirror.com/event-emitter/-/event-emitter-0.3.5.tgz",
"integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==",
"license": "MIT",
"dependencies": {
"d": "1",
"es5-ext": "~0.10.14"
}
},
"node_modules/expand-brackets": {
"version": "2.1.4",
"dev": true,
@@ -3573,6 +3903,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/ext": {
"version": "1.7.0",
"resolved": "https://registry.npmmirror.com/ext/-/ext-1.7.0.tgz",
"integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==",
"license": "ISC",
"dependencies": {
"type": "^2.7.2"
}
},
"node_modules/extend-shallow": {
"version": "2.0.1",
"dev": true,
@@ -4190,6 +4529,16 @@
"node": ">= 12"
}
},
"node_modules/html-void-elements": {
"version": "2.0.1",
"resolved": "https://registry.npmmirror.com/html-void-elements/-/html-void-elements-2.0.1.tgz",
"integrity": "sha512-0quDb7s97CfemeJAnW9wC0hw78MtW7NU3hqtCD75g2vFlDLt36llsYD7uB7SUzojLMP24N5IatXf7ylGXiGG9A==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/htmlparser2": {
"version": "3.10.1",
"dev": true,
@@ -4263,6 +4612,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/i18next": {
"version": "20.6.1",
"resolved": "https://registry.npmmirror.com/i18next/-/i18next-20.6.1.tgz",
"integrity": "sha512-yCMYTMEJ9ihCwEQQ3phLo7I/Pwycf8uAx+sRHwwk5U9Aui/IZYgQRyMqXafQOw5QQ7DM1Z+WyEXWIqSuJHhG2A==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.12.0"
}
},
"node_modules/ieee754": {
"version": "1.2.1",
"dev": true,
@@ -4293,6 +4651,16 @@
"node": ">=0.10.0"
}
},
"node_modules/immer": {
"version": "9.0.21",
"resolved": "https://registry.npmmirror.com/immer/-/immer-9.0.21.tgz",
"integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/immer"
}
},
"node_modules/immutable": {
"version": "5.1.9",
"license": "MIT"
@@ -4564,6 +4932,12 @@
"node": ">=0.10.0"
}
},
"node_modules/is-hotkey": {
"version": "0.2.0",
"resolved": "https://registry.npmmirror.com/is-hotkey/-/is-hotkey-0.2.0.tgz",
"integrity": "sha512-UknnZK4RakDmTgz4PI1wIph5yxSs/mvChWs9ifnlXsKuXgWmOkY/hAE0H/k2MIqH0RlRye0i1oC07MCRSD28Mw==",
"license": "MIT"
},
"node_modules/is-interactive": {
"version": "1.0.0",
"dev": true,
@@ -4742,6 +5116,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-url": {
"version": "1.2.4",
"resolved": "https://registry.npmmirror.com/is-url/-/is-url-1.2.4.tgz",
"integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==",
"license": "MIT"
},
"node_modules/is-weakmap": {
"version": "2.0.2",
"dev": true,
@@ -4946,14 +5326,51 @@
"version": "4.18.1",
"license": "MIT"
},
"node_modules/lodash.camelcase": {
"version": "4.3.0",
"resolved": "https://registry.npmmirror.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
"integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==",
"license": "MIT"
},
"node_modules/lodash.chunk": {
"version": "4.2.0",
"license": "MIT"
},
"node_modules/lodash.clonedeep": {
"version": "4.5.0",
"resolved": "https://registry.npmmirror.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
"integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==",
"license": "MIT"
},
"node_modules/lodash.debounce": {
"version": "4.0.8",
"resolved": "https://registry.npmmirror.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
"integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",
"license": "MIT"
},
"node_modules/lodash.foreach": {
"version": "4.5.0",
"resolved": "https://registry.npmmirror.com/lodash.foreach/-/lodash.foreach-4.5.0.tgz",
"integrity": "sha512-aEXTF4d+m05rVOAUG3z4vZZ4xVexLKZGF0lIxuHZ1Hplpk/3B6Z1+/ICICYRLm7c41Z2xiejbkCkJoTlypoXhQ==",
"license": "MIT"
},
"node_modules/lodash.isequal": {
"version": "4.5.0",
"resolved": "https://registry.npmmirror.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
"integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==",
"deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.",
"license": "MIT"
},
"node_modules/lodash.throttle": {
"version": "4.1.1",
"license": "MIT"
},
"node_modules/lodash.toarray": {
"version": "4.4.0",
"resolved": "https://registry.npmmirror.com/lodash.toarray/-/lodash.toarray-4.4.0.tgz",
"integrity": "sha512-QyffEA3i5dma5q2490+SgCvDN0pXLmRGSyAANuVi0HQ01Pkfr9fuoKQW8wm1wGBnJITs/mS7wQvS6VshUEBFCw==",
"license": "MIT"
},
"node_modules/log-symbols": {
"version": "4.1.0",
"dev": true,
@@ -5089,6 +5506,15 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/mime-match": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/mime-match/-/mime-match-1.0.2.tgz",
"integrity": "sha512-VXp/ugGDVh3eCLOBCiHZMYWQaTNUHv2IJrut+yXA6+JbLPXHglHwfS/5A5L0ll+jkCY7fIzRJcH6OIunF+c6Cg==",
"license": "ISC",
"dependencies": {
"wildcard": "^1.1.0"
}
},
"node_modules/mimic-fn": {
"version": "2.1.0",
"dev": true,
@@ -5177,6 +5603,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/namespace-emitter": {
"version": "2.0.1",
"resolved": "https://registry.npmmirror.com/namespace-emitter/-/namespace-emitter-2.0.1.tgz",
"integrity": "sha512-N/sMKHniSDJBjfrkbS/tpkPj4RAbvW3mr8UAzvlMHyun93XEm83IAvhWtJVHo+RHn/oO8Job5YN4b+wRjSVp5g==",
"license": "MIT"
},
"node_modules/nanoid": {
"version": "3.3.20",
"funding": [
@@ -5270,6 +5702,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/next-tick": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/next-tick/-/next-tick-1.1.0.tgz",
"integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==",
"license": "ISC"
},
"node_modules/no-case": {
"version": "3.0.4",
"dev": true,
@@ -5913,6 +6351,24 @@
"posthtml-render": "^1.0.6"
}
},
"node_modules/preact": {
"version": "10.29.8",
"resolved": "https://registry.npmmirror.com/preact/-/preact-10.29.8.tgz",
"integrity": "sha512-ej2aVZ+vZ8WO7tvlQWRM9N63A0KzF9q4mWJfDUHgYaIofWY9hu74QdnQrjoPMmZi2/nZ5gN0bJCQF49xQqx09Q==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/preact"
},
"peerDependencies": {
"preact-render-to-string": ">=5"
},
"peerDependenciesMeta": {
"preact-render-to-string": {
"optional": true
}
}
},
"node_modules/prettier": {
"version": "3.9.9",
"dev": true,
@@ -5927,6 +6383,15 @@
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/prismjs": {
"version": "1.30.0",
"resolved": "https://registry.npmmirror.com/prismjs/-/prismjs-1.30.0.tgz",
"integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/quansync": {
"version": "1.0.0",
"dev": true,
@@ -6371,6 +6836,15 @@
"node": ">=11.0.0"
}
},
"node_modules/scroll-into-view-if-needed": {
"version": "2.2.31",
"resolved": "https://registry.npmmirror.com/scroll-into-view-if-needed/-/scroll-into-view-if-needed-2.2.31.tgz",
"integrity": "sha512-dGCXy99wZQivjmjIqihaBQNjryrz5rueJY7eHfTdyWEiR4ttYpsajb14rn9s5d4DY4EcY6+4+U/maARBXJedkA==",
"license": "MIT",
"dependencies": {
"compute-scroll-into-view": "^1.0.20"
}
},
"node_modules/scule": {
"version": "1.3.0",
"dev": true,
@@ -6594,6 +7068,56 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/slate": {
"version": "0.72.8",
"resolved": "https://registry.npmmirror.com/slate/-/slate-0.72.8.tgz",
"integrity": "sha512-/nJwTswQgnRurpK+bGJFH1oM7naD5qDmHd89JyiKNT2oOKD8marW0QSBtuFnwEbL5aGCS8AmrhXQgNOsn4osAw==",
"license": "MIT",
"dependencies": {
"immer": "^9.0.6",
"is-plain-object": "^5.0.0",
"tiny-warning": "^1.0.3"
}
},
"node_modules/slate-history": {
"version": "0.66.0",
"resolved": "https://registry.npmmirror.com/slate-history/-/slate-history-0.66.0.tgz",
"integrity": "sha512-6MWpxGQZiMvSINlCbMW43E2YBSVMCMCIwQfBzGssjWw4kb0qfvj0pIdblWNRQZD0hR6WHP+dHHgGSeVdMWzfng==",
"license": "MIT",
"dependencies": {
"is-plain-object": "^5.0.0"
},
"peerDependencies": {
"slate": ">=0.65.3"
}
},
"node_modules/slate-history/node_modules/is-plain-object": {
"version": "5.1.0",
"resolved": "https://registry.npmmirror.com/is-plain-object/-/is-plain-object-5.1.0.tgz",
"integrity": "sha512-bUi/yjmtKYcRVUtWRGr0UA6xEFh2I6zWUwMrUXB3s7bmYCaZ8a+0ZsTRkrawh/mzlSD1Y0Ph8bp/U+TvBpWDNw==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/slate/node_modules/is-plain-object": {
"version": "5.1.0",
"resolved": "https://registry.npmmirror.com/is-plain-object/-/is-plain-object-5.1.0.tgz",
"integrity": "sha512-bUi/yjmtKYcRVUtWRGr0UA6xEFh2I6zWUwMrUXB3s7bmYCaZ8a+0ZsTRkrawh/mzlSD1Y0Ph8bp/U+TvBpWDNw==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/snabbdom": {
"version": "3.6.4",
"resolved": "https://registry.npmmirror.com/snabbdom/-/snabbdom-3.6.4.tgz",
"integrity": "sha512-VmxEfuw1/Y/eFj5VtMhYnukExpYiPkNzoo3+N3qwAOUDMl8wXgbli5ebR+j0knE3lZ/0eYskLxNcX64uy10N9w==",
"license": "MIT",
"engines": {
"node": ">=12.17.0"
}
},
"node_modules/snapdragon": {
"version": "0.8.2",
"dev": true,
@@ -6785,6 +7309,12 @@
"node": ">=20.16.0"
}
},
"node_modules/ssr-window": {
"version": "3.0.0",
"resolved": "https://registry.npmmirror.com/ssr-window/-/ssr-window-3.0.0.tgz",
"integrity": "sha512-q+8UfWDg9Itrg0yWK7oe5p/XRCJpJF9OBtXfOPgSJl+u3Xd5KI328RUEvUqSMVM9CiQUEf1QdBzJMkYGErj9QA==",
"license": "MIT"
},
"node_modules/stable": {
"version": "0.1.8",
"dev": true,
@@ -7264,6 +7794,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/tiny-warning": {
"version": "1.0.3",
"resolved": "https://registry.npmmirror.com/tiny-warning/-/tiny-warning-1.0.3.tgz",
"integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==",
"license": "MIT"
},
"node_modules/tinycolor2": {
"version": "1.6.0",
"license": "MIT"
@@ -7409,6 +7945,12 @@
"dev": true,
"license": "0BSD"
},
"node_modules/type": {
"version": "2.7.3",
"resolved": "https://registry.npmmirror.com/type/-/type-2.7.3.tgz",
"integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==",
"license": "ISC"
},
"node_modules/type-level-regexp": {
"version": "0.1.17",
"dev": true,
@@ -8369,6 +8911,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/wildcard": {
"version": "1.1.2",
"resolved": "https://registry.npmmirror.com/wildcard/-/wildcard-1.1.2.tgz",
"integrity": "sha512-DXukZJxpHA8LuotRwL0pP1+rS6CS7FF2qStDDE1C7DDg2rLud2PXRMuEDYIPhgEezwnlHNL4c+N6MfMTjCGTng==",
"license": "MIT"
},
"node_modules/wordwrap": {
"version": "1.0.0",
"dev": true,
+2
View File
@@ -12,6 +12,8 @@
"api:gen": "alova gen -f"
},
"dependencies": {
"@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^5.1.12",
"alova": "^3.4.0",
"lodash": "^4.17.21",
"mitt": "^3.0.1",
+2
View File
File diff suppressed because one or more lines are too long
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 933 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

File diff suppressed because one or more lines are too long
+378
View File
@@ -0,0 +1,378 @@
/*-------------------------------------
zTree Style
version: 3.4
author: Hunter.z
email: hunter.z@263.net
website: http://code.google.com/p/jquerytree/
-------------------------------------*/
.ztree * {
padding: 0;
margin: 0;
font-size: 14px;
font-family: Verdana, Arial, Helvetica, AppleGothic, sans-serif
}
.ztree {
margin: 0;
padding: 5px;
color: #333
}
.ztree li {
padding: 0;
margin: 0;
list-style: none;
line-height: 17px;
text-align: left;
white-space: nowrap;
outline: 0
}
.ztree li ul {
margin: 0;
padding: 0 0 0 18px
}
.ztree li ul.line {
background: url(./img/line_conn.png) 0 0 repeat-y;
}
.ztree li a {
padding-right: 3px;
margin: 0;
cursor: pointer;
height: 21px;
color: #333;
background-color: transparent;
text-decoration: none;
vertical-align: top;
display: inline-block
}
.ztree li a:hover {
text-decoration: underline
}
.ztree li a.curSelectedNode {
padding-top: 0px;
background-color: #e5e5e5;
color: black;
height: 21px;
opacity: 0.8;
}
.ztree li a.curSelectedNode_Edit {
padding-top: 0px;
background-color: #e5e5e5;
color: black;
height: 21px;
border: 1px #666 solid;
opacity: 0.8;
}
.ztree li a.tmpTargetNode_inner {
padding-top: 0px;
background-color: #aaa;
color: white;
height: 21px;
border: 1px #666 solid;
opacity: 0.8;
filter: alpha(opacity=80)
}
.ztree li a.tmpTargetNode_prev {}
.ztree li a.tmpTargetNode_next {}
.ztree li a input.rename {
height: 14px;
width: 80px;
padding: 0;
margin: 0;
font-size: 12px;
border: 1px #585956 solid;
*border: 0px
}
.ztree li span {
line-height: 21px;
margin-right: 2px
}
.ztree li span.button {
line-height: 0;
margin: 0;
padding: 0;
width: 21px;
height: 21px;
display: inline-block;
vertical-align: middle;
border: 0 none;
cursor: pointer;
outline: none;
background-color: transparent;
background-repeat: no-repeat;
background-attachment: scroll;
background-image: url("./img/bootstrap.png");
}
.ztree li span.button.chk {
width: 13px;
height: 13px;
margin: 0 2px;
cursor: auto
}
.ztree li span.button.chk.checkbox_false_full {
background-position: -5px -5px;
}
.ztree li span.button.chk.checkbox_false_full_focus {
background-position: -5px -26px;
}
.ztree li span.button.chk.checkbox_false_part {
background-position: -5px -48px;
}
.ztree li span.button.chk.checkbox_false_part_focus {
background-position: -5px -68px;
}
.ztree li span.button.chk.checkbox_false_disable {
background-position: -5px -89px;
}
.ztree li span.button.chk.checkbox_true_full {
background-position: -26px -5px;
}
.ztree li span.button.chk.checkbox_true_full_focus {
background-position: -26px -26px;
}
.ztree li span.button.chk.checkbox_true_part {
background-position: -26px -48px;
}
.ztree li span.button.chk.checkbox_true_part_focus {
background-position: -26px -68px;
}
.ztree li span.button.chk.checkbox_true_disable {
background-position: -26px -89px;
}
.ztree li span.button.chk.radio_false_full {
background-position: -47px -5px;
}
.ztree li span.button.chk.radio_false_full_focus {
background-position: -47px -26px;
}
.ztree li span.button.chk.radio_false_part {
background-position: -47px -47px;
}
.ztree li span.button.chk.radio_false_part_focus {
background-position: -47px -68px;
}
.ztree li span.button.chk.radio_false_disable {
background-position: -47px -89px;
}
.ztree li span.button.chk.radio_true_full {
background-position: -68px -5px;
}
.ztree li span.button.chk.radio_true_full_focus {
background-position: -68px -26px;
}
.ztree li span.button.chk.radio_true_part {
background-position: -68px -47px;
}
.ztree li span.button.chk.radio_true_part_focus {
background-position: -68px -68px;
}
.ztree li span.button.chk.radio_true_disable {
background-position: -68px -89px;
}
.ztree li span.button.switch {
width: 21px;
height: 21px
}
.ztree li span.button.root_open {
background-position: -105px -63px
}
.ztree li span.button.root_close {
background-position: -126px -63px
}
.ztree li span.button.roots_open {
background-position: -105px 0;
}
.ztree li span.button.roots_close {
background-position: -126px 0;
}
.ztree li span.button.center_open {
background-position: -105px -21px;
}
.ztree li span.button.center_close {
background-position: -126px -21px;
}
.ztree li span.button.bottom_open {
background-position: -105px -42px;
}
.ztree li span.button.bottom_close {
background-position: -126px -42px;
}
.ztree li span.button.noline_open {
background-position: -105px -84px;
}
.ztree li span.button.noline_close {
background-position: -126px -84px;
}
.ztree li span.button.root_docu {
background-position: -105px -63px;
}
.ztree li span.button.roots_docu {
background-position: -84px 0;
}
.ztree li span.button.center_docu {
background-position: -84px -21px;
}
.ztree li span.button.bottom_docu {
background-position: -84px -42px;
}
.ztree li span.button.noline_docu {
background: none;
}
.ztree li span.button.ico_open {
margin-right: 2px;
background-position: -147px -21px;
vertical-align: top;
*vertical-align: middle
}
.ztree li span.button.ico_close {
margin-right: 2px;
margin-right: 2px;
background-position: -147px 0;
vertical-align: top;
*vertical-align: middle
}
.ztree li span.button.ico_docu {
margin-right: 2px;
background-position: -147px -43px;
vertical-align: top;
*vertical-align: middle
}
.ztree li span.button.edit {
margin-left: 2px;
margin-right: -1px;
background-position: -189px -21px;
vertical-align: top;
*vertical-align: middle
}
.ztree li span.button.edit:hover {
background-position: -168px -21px;
}
.ztree li span.button.remove {
margin-left: 2px;
margin-right: -1px;
background-position: -189px -42px;
vertical-align: top;
*vertical-align: middle
}
.ztree li span.button.remove:hover {
background-position: -168px -42px;
}
.ztree li span.button.add {
margin-left: 2px;
margin-right: -1px;
background-position: -189px 0;
vertical-align: top;
*vertical-align: middle
}
.ztree li span.button.add:hover {
background-position: -168px 0;
}
ul.tmpTargetzTree {
background-color: #FFE6B0;
opacity: 0.8;
filter: alpha(opacity=80)
}
span.tmpzTreeMove_arrow {
width: 16px;
height: 21px;
display: inline-block;
padding: 0;
margin: 2px 0 0 1px;
border: 0 none;
position: absolute;
background-color: transparent;
background-repeat: no-repeat;
background-attachment: scroll;
background-position: -168px -84px;
background-image: url("./img/bootstrap.png");
z-index: 10000;
}
span.tmpzTreeMove_prev {
background: red;
}
ul.ztree.zTreeDragUL {
margin: 0;
padding: 0;
position: absolute;
width: auto;
height: auto;
overflow: hidden;
background-color: #cfcfcf;
border: 1px #00B83F dotted;
opacity: 0.8;
filter: alpha(opacity=80)
}
.ztreeMask {
z-index: 10000;
background-color: #cfcfcf;
opacity: 0.0;
filter: alpha(opacity=0);
position: absolute
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 254 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 524 B

+121 -2
View File
@@ -13,11 +13,20 @@
* 注意:view-ui-plus 组件在模板里必须用 PascalCase(Select / DatePicker / Input / Option),
* 写成 i-select / i-date-picker 不会被解析。
*/
import { reactive, computed, ref, watch } from "vue";
import { reactive, computed, ref, watch, defineAsyncComponent } from "vue";
import { Message } from "view-ui-plus";
import { callModuleApi, parseOptionItems, isLoginRedirect, warmModuleSession } from "@/utils/module-session";
import UserCheckInput from "@/components/UserCheckInput.vue";
import type { FormField, FormRequest } from "@/types/agent";
/**
* 富文本正文编辑器(wangEditor)——**异步加载**。
*
* <p>`@wangeditor/*` 是重量级依赖(编辑器 + 工具栏 + 样式)。整个助手只有「通知公告」
* 的发布表单用得上,静态引入等于让所有用户都背上这份体积,所以拆包按需加载。
*/
const RichTextEditor = defineAsyncComponent(() => import("@/components/RichTextEditor.vue"));
const props = defineProps<{
form: FormRequest;
submitted?: boolean;
@@ -410,6 +419,28 @@ function approverNames(f: FormField, ids: any[]): string {
return ids.map(id => opts.find(o => String(o.value) === String(id))?.label || String(id)).join("、");
}
/* ==================== 通知对象(widget=userpicker,搬运自通知公告模块页) ==================== */
/**
* 「通知对象」的显示文本(按字段名)。
*
* <p>为什么要在外面存一份:控件内部只持有 id 串(`"101,102"`),提交后只读摘要里
* 直接摆一串 id 是没法看的。控件在选中/清空时会 `@names` 回传它自己的可读文本
* (如「张三、李四」),这里接住用于摘要。
*/
const pickerText = reactive<Record<string, string>>({});
/** 选人弹窗的页签(后端下发;没有时退回 optionCall 的单页签) */
function pickerSourcesOf(f: FormField) {
return f.pickerSources && f.pickerSources.length ? f.pickerSources : [];
}
/**
* 本表单是否是「编辑」场景 —— 由是否有 `sourceId`(要改的那条记录 id)判断。
* <p>编辑态下「全部用户」要显示成「现有用户」(模块页 `:isEdit` 同款口径):
* 定向发送过的通知不能再改成全校广播。
*/
const isEditForm = computed(() => (props.form?.fields || []).some(f => f.name === "sourceId"));
/* ==================== 模块页同款呈现:自动选中 / 单选项隐藏 / 快捷时段 / 逐天时段 ==================== */
/** widget=switch 的勾选态(真正提交的是 switchOn / switchOff 映射后的值) */
@@ -598,7 +629,7 @@ function widgetOf(f: FormField) {
* 附件与审批流更是必须整行。时间类字段保持并排(「开始 ~ 结束」与模块页一致)。
*/
function isWide(f: FormField) {
if (["flow", "radio", "switch", "textarea", "file"].includes(f.widget)) return true;
if (["flow", "radio", "switch", "textarea", "file", "richtext", "userpicker"].includes(f.widget)) return true;
// 「连续天数」是「连续请假」按天展开的从属字段,跟着独占一行
return (props.form?.fields || []).some(x => x.dailyExpand?.count === f.name);
}
@@ -677,6 +708,11 @@ async function reloadOptions(f: FormField) {
function syncOptions() {
for (const f of props.form?.fields || []) {
if (!f.optionCall) continue;
// ★ userpicker(通知对象)跳过:它的 optionCall 是给**选人弹窗**用的原始组织树,
// 弹窗自己取、自己用嵌套结构。这里再按「下拉字典」取一遍会把树拍平成叶子,
// 而且 alignToOptions 会把「具体人员 id 串」判成「认不出的值」直接清掉 ——
// 那等于把模型已经识别出来的人悄悄抹掉。
if (f.widget === "userpicker") continue;
const sig = depSignature(f);
if (depSig[f.name] === sig) continue;
const hadSig = depSig[f.name] !== undefined;
@@ -718,6 +754,7 @@ function initValues() {
Object.keys(switchOn).forEach(k => delete switchOn[k]);
Object.keys(dailyRows).forEach(k => delete dailyRows[k]);
Object.keys(hiddenSingle).forEach(k => delete hiddenSingle[k]);
Object.keys(pickerText).forEach(k => delete pickerText[k]);
// 栏目动态字段:换表单就整体丢弃(取值、区划树缓存都不跨表单复用)
extraFields.value = [];
extraState.value = "idle";
@@ -748,6 +785,20 @@ function initValues() {
values[f.name] = alignToOptions(f, arr.length ? arr : f.allToken ? [f.allToken] : []);
} else if (f.widget === "file") {
values[f.name] = Array.isArray(seed) ? [...seed] : [];
} else if (f.widget === "userpicker") {
// 通知对象:v-model 是**字符串**(`"all"` 或用户 id 逗号串),不是数组 ——
// 虽然声明成 array(wire 上要逗号串),但模块页 form.rdoname 初值就是 "all"。
// 模型已经点出具体人时可能是数组,join 成模块要的形态。
const token = f.allToken || "all";
const raw = Array.isArray(seed) ? seed.join(",") : seed === undefined || seed === null ? "" : String(seed);
values[f.name] = raw || token;
pickerText[f.name] =
raw && raw !== token
? `已选 ${raw.split(",").filter(x => String(x).trim() !== "").length} 人`
: f.allTokenLabel || "全部用户";
} else if (f.widget === "richtext") {
// 正文:HTML 字符串(与模块页 NoticeRequest.content 一致)
values[f.name] = seed === undefined || seed === null ? "" : String(seed);
} else if (f.widget === "select" || f.widget === "number" || f.widget === "radio") {
// ★ 对齐选项取值:模型按 schema 给的是数字(integer)或名称(string),
// 这里统一成「选项本身的值」,否则预填了也选不上,用户以为要再选一次
@@ -876,6 +927,22 @@ function labelOfName(name: string): string {
return f?.label || NAME_LABEL[name] || name;
}
/** 富文本 HTML → 摘要里可读的纯文本(只用于展示,提交的仍是原 HTML) */
function plainTextOfHtml(html: string): string {
const text = String(html || "")
.replace(/<br\s*\/?>/gi, " ")
.replace(/<\/(p|div|li|h[1-6])>/gi, " ")
.replace(/<[^>]*>/g, "")
.replace(/&nbsp;/gi, " ")
.replace(/&lt;/gi, "<")
.replace(/&gt;/gi, ">")
.replace(/&amp;/gi, "&")
.replace(/\s+/g, " ")
.trim();
if (!text) return "(空)";
return text.length > 60 ? `${text.slice(0, 60)}…` : text;
}
/**
* 动态下拉的状态提示:等待上级字段 / 正在读取 / 取不到(可手填编号)。
* 有了它,用户不会对着一个空下拉框猜「为什么点不开」。
@@ -959,6 +1026,15 @@ function onSubmit() {
});
}
}
// 「通知对象」:值是**字符串**(`"all"` 或 id 逗号串),不能走下面的数组/空值分支 ——
// 空值在这里补回「全部」(模块页 form.rdoname 从不留空)。
if (f.widget === "userpicker") {
const token = f.allToken || "all";
const s = values[f.name] === undefined || values[f.name] === null ? "" : String(values[f.name]).trim();
args[f.name] = s || token;
rows.push({ label: f.label, text: pickerText[f.name] || s || f.allTokenLabel || "全部用户" });
continue;
}
const v = values[f.name];
// 数组类字段即使是空数组也要提交(模块页就是恒带 fileIds:"" / attachments:[]),
// 否则「没传」与「传空」在 wire 上不一致,后端拿到的可能是 null 而不是空集合。
@@ -989,6 +1065,13 @@ function onSubmit() {
rows.push({ label: f.label, text: objs.map(o => o.fileName).join("、") || `${objs.length} 个附件` });
continue;
}
// 富文本正文:提交的是**完整 HTML**(与模块页 NoticeRequest.content 一致);
// 但摘要里不能摆原始标签,剥成纯文本给人看。
if (f.widget === "richtext") {
args[f.name] = String(v);
rows.push({ label: f.label, text: plainTextOfHtml(String(v)) });
continue;
}
const normalized = normalize(f, v);
args[f.name] = normalized;
const opt = fieldOptions(f).find(o => String(o.value) === String(normalized));
@@ -1161,6 +1244,32 @@ function reset() {
<span v-else class="file-tip">{{ f.placeholder || "可选,支持图片 / 文档 / 压缩包" }}</span>
</div>
<!-- 通知对象:搬自通知公告模块页的 UserCheckInput(radio 全部/指定 + 选人弹窗,
左组织树 / 右已选列表 / 全选 / 按名字搜索回勾)。值是 "all" 或 id 逗号串。 -->
<div v-else-if="widgetOf(f) === 'userpicker'" class="field__picker">
<UserCheckInput
v-model="values[f.name]"
:sources="pickerSourcesOf(f)"
:fallback-call="f.optionCall"
:all-token="f.allToken || 'all'"
:all-label="f.allTokenLabel || '全部用户'"
:is-edit="isEditForm"
:disabled="disabled"
@names="(t: string) => (pickerText[f.name] = t)"
/>
</div>
<!-- 富文本正文:搬自通知公告模块页的 wangEditor(提交的是 HTML 字符串) -->
<div v-else-if="widgetOf(f) === 'richtext'" class="field__richtext">
<RichTextEditor
v-model="values[f.name]"
:upload-path="f.richTextUploadPath"
:preview-path="f.richTextPreviewPath"
:placeholder="f.placeholder || '请输入通知内容……'"
:disabled="disabled"
/>
</div>
<div v-else-if="widgetOf(f) === 'flow'" class="field__flow">
<Checkbox v-model="flowEnabled[f.name]" :disabled="disabled">
选择审批人后走审批流程(不勾选 = 直接发布)
@@ -1469,6 +1578,16 @@ function reset() {
width: 100%;
}
/* 通知对象(搬自模块页):radio + 只读回显 + +/清空 一行排开 */
.field__picker {
width: 100%;
}
/* 富文本正文:编辑器自己带边框,这里只管占满整行 */
.field__richtext {
width: 100%;
}
.field__label {
display: block;
margin-bottom: 6px;
+193
View File
@@ -0,0 +1,193 @@
<script setup lang="ts">
/**
* 富文本正文编辑器 —— 通知公告模块页(`NoticeFront/src/views/notice/NewNotice.vue`)
* 里那块 wangEditor 的封装,供助手的「发布通知」表单复用。
*
* <p>为什么是**封装**而不是把 wangEditor 直接写进 FormCard:
* 编辑器要自己管实例(创建 / 销毁 / 只读态)与图片上传,塞进表单组件会把 FormCard
* 已经够长的逻辑再撑大一截;而且 `@wangeditor/*` 是重量级依赖,单独成一个组件后
* FormCard 可以**异步加载**它,只有出现富文本字段的表单才付这份体积。
*
* <p>契约:提交给后端的 `content` 是 **HTML 字符串**,与模块页
* `NoticeRequest.content` 完全一致(模块页也是 `v-model="form.content"`)。
*
* <p>图片上传的两点对齐(两处都容易踩):
* <ol>
* <li>接口是 {@code /api/file/UploadImage}(**不是**附件的 {@code /api/base/FileUpload}),
* 返回 {@code {code,data:[{id,fileTrueName,fileName}]}};</li>
* <li>插进正文的 `src` 必须用 {@code /api/file/Preview?id=<id>} —— inline 输出图片。
* {@code /api/file/Down} 是 attachment,放进 {@code <img src>} 显示不出来。</li>
* </ol>
* 两个地址都由后端下发(已带模块前缀),这里不拼前缀。
*
* <p>上传同样带**用户自己的登录态**:模块接口只认模块容器的 CAS 会话,服务端代调必 302。
* 遇 {@code code=302} 先预热模块会话再重试一次(与附件上传、下拉代取同一条链路)。
*/
import { ref, shallowRef, onBeforeUnmount, watch } from "vue";
import { Editor, Toolbar } from "@wangeditor/editor-for-vue";
import "@wangeditor/editor/dist/css/style.css";
import { Message } from "view-ui-plus";
import { isLoginRedirect, warmModuleSession } from "@/utils/module-session";
const props = withDefaults(
defineProps<{
modelValue?: string;
/** 正文内图片的上传地址(后端下发,**已带模块前缀**) */
uploadPath?: string;
/** 正文内图片的预览地址模板(后端下发,含 `{id}` 占位) */
previewPath?: string;
placeholder?: string;
disabled?: boolean;
}>(),
{ modelValue: "", uploadPath: "", previewPath: "", placeholder: "请输入通知内容……", disabled: false }
);
const emit = defineEmits<{ (e: "update:modelValue", html: string): void }>();
/** wangEditor 实例(必须 shallowRef —— 深度响应式会把编辑器内部状态搞坏) */
const editorRef = shallowRef<any>(null);
/**
* 工具栏与模块页一致:去掉「上传视频」和「全屏」。
* <p>去掉视频是本模块的既有选择;全屏在对话面板里会把整个助手界面顶飞。
*/
const toolbarConfig = { excludeKeys: ["group-video", "fullScreen"] };
/** 图片预览地址(把后端给的模板里的 `{id}` 换掉) */
function previewUrl(id: any): string {
return String(props.previewPath || "").split("{id}").join(String(id));
}
/**
* 正文图片上传。
* <p>注意 {@code customUpload} 里**不能**用 `this`,且必须同步调用 `insertFn` —— 这是 wangEditor 的约定。
*/
async function customUpload(file: File, insertFn: (url: string, alt: string, href: string) => void) {
if (!props.uploadPath) {
Message.error("正文图片上传地址缺失,请稍后重试");
return;
}
if (!props.previewPath) {
Message.error("正文图片预览地址缺失,请稍后重试");
return;
}
const doSend = async () => {
const fd = new FormData();
fd.append("file", file);
const res = await fetch(`${location.origin}${props.uploadPath}`, {
method: "POST",
headers: { yschapirequest: "1" },
body: fd,
credentials: "same-origin",
});
return { status: res.status, body: await res.text() };
};
try {
let r = await doSend();
if (isLoginRedirect(r.status, r.body)) {
await warmModuleSession(props.uploadPath);
r = await doSend();
}
const j = JSON.parse(r.body);
const data = j && typeof j === "object" ? j.data : null;
const first = Array.isArray(data) ? data[0] : data;
if (Number(j?.code) === 0 && first) {
const id = first.id ?? first.fileid ?? first.fileId;
if (id === undefined || id === null) {
Message.error("图片上传失败:没拿回文件编号");
return;
}
const url = previewUrl(id);
// 三项都要给:url 是图片地址,href 是点击图片要跳的地址(模块页也指向自己)
insertFn(url, first.fileTrueName || first.fileName || file.name || "", url);
} else {
Message.error(j?.msg || "图片上传失败");
}
} catch {
Message.error(`「${file.name}」上传失败`);
}
}
const editorConfig = {
placeholder: props.placeholder,
MENU_CONF: {
uploadImage: { customUpload },
},
};
function handleCreated(editor: any) {
editorRef.value = editor;
// 首屏就是只读(历史消息里的表单)时立刻置灰
if (props.disabled) editor.disable();
}
watch(
() => props.disabled,
v => {
const editor = editorRef.value;
if (!editor) return;
if (v) editor.disable();
else editor.enable();
}
);
onBeforeUnmount(() => {
const editor = editorRef.value;
editorRef.value = null;
try {
editor?.destroy();
} catch {
/* 吞掉:切换会话时组件与编辑器可能已被回收,重复 destroy 不该打断页面 */
}
});
</script>
<template>
<div class="rt-editor" :class="{ 'rt-editor--readonly': disabled }">
<Toolbar class="rt-editor__toolbar" :editor="editorRef" :defaultConfig="toolbarConfig" mode="default" />
<Editor
class="rt-editor__content"
:model-value="modelValue"
:defaultConfig="editorConfig"
mode="default"
@onCreated="handleCreated"
@update:modelValue="(html: string) => emit('update:modelValue', html)"
/>
</div>
</template>
<style scoped>
.rt-editor {
width: 100%;
border: 1px solid var(--agent-border-strong, #dcdee2);
border-radius: 8px;
overflow: hidden;
background: #fff;
/* 工具栏的弹出层要盖在下面的字段上 */
position: relative;
z-index: 10;
}
.rt-editor--readonly {
opacity: 0.9;
}
.rt-editor__toolbar {
border-bottom: 1px solid var(--agent-border, #e8e8e8);
}
.rt-editor__content {
overflow-y: hidden;
}
/* 正文编辑区高度:模块页是 320px,对话面板里略矮一点,避免表单把整屏占满 */
.rt-editor__content :deep(.w-e-text-container) {
height: 240px !important;
}
/* wangEditor 的工具栏按钮自带行高,压一压免得两行按钮把小面板撑高 */
.rt-editor__toolbar :deep(.w-e-bar) {
padding: 2px 0;
}
</style>
+241
View File
@@ -0,0 +1,241 @@
<template>
<!-- 通知对象选择:全部用户 / 指定用户(自业务模块 NoticeFront 的 components/UserCheckInput.vue 照搬) -->
<div class="check-user-wrap">
<div class="check-user-box">
<RadioGroup v-model="isCheckUserAll" class="radio" @on-change="userAllCheck">
<Radio :disabled="disabled || editDisabled" :label="0">{{ allText }}</Radio>
<Radio :disabled="disabled" :label="1">指定用户</Radio>
</RadioGroup>
<Input
type="text"
v-model="userNameStr"
readonly
:disabled="disabled || !isCheckUserAll"
placeholder="选择指定的用户"
class="input"
></Input>
<Button icon="md-add" :disabled="disabled || !isCheckUserAll" @click="checkUserShow"></Button>
<Button icon="md-trash" :disabled="disabled || !isCheckUserAll" @click="resetUser"></Button>
</div>
<UserCheckModal ref="userCheckModal" :sources="tabs" :userids="tempUserIds" @on-submit="submitUser" />
</div>
</template>
<script lang="ts">
/**
* 「通知对象」输入控件 —— 自业务模块 NoticeFront 的 `components/UserCheckInput.vue` **照搬**。
*
* <p>v-model 值是**字符串**:`"all"` 或用户 id 逗号串(与模块页 `form.rdoname` 一致);
* 切到「指定用户」会直接弹出选人弹窗(左组织树 / 右已选列表)。
* 提交时由后端按 `joinSeparator` 拼串,这里原样给出字符串即可。
*
* <p>与本体的**唯一区别是数据从哪儿来**:模块里是 alova 直连自己的 `/api/*`,
* 这里改为后端下发的 `sources[].call`(`FormField.pickerSources`),
* 由 `callModuleApi` 带**用户登录态**去取 —— 模块接口只认模块容器的 CAS 会话,服务端代调必 302。
* 取回的是**原始嵌套树**,不做拍平(拍平会丢层级,树就画不出来)。
*/
import UserCheckModal from "@/components/UserCheckModal.vue";
import { callModuleApi } from "@/utils/module-session";
import type { BrowserToolCall } from "@/types/agent";
/** 后端下发的数据源页签(每个 call 都已带模块前缀) */
interface PickerSource {
name: string;
call: BrowserToolCall;
}
/**
* 把模块返回的原始响应解成**组织树数组**。
*
* <p>与 `parseOptionItems` 分开写:那个是给下拉用的,会把树**拍平成叶子**
* (下拉里当然不该出现「年级」这一层)。选人弹窗要的恰恰是层级本身,
* 所以这里只做「剥掉 {code,msg,data} 外壳」,不动结构。
*/
function parseTree(body?: string): any[] {
if (!body) return [];
let payload: any;
try {
payload = JSON.parse(body);
} catch {
return [];
}
if (payload && typeof payload === "object" && !Array.isArray(payload) && "code" in payload) {
if (Number(payload.code) !== 0) return [];
payload = payload.data;
}
if (Array.isArray(payload)) return payload;
// 少数版本把数组包在 data.list / data.records 里,容错取第一个数组
if (payload && typeof payload === "object") {
const arr = Object.values(payload).find(v => Array.isArray(v));
if (arr) return arr as any[];
}
return [];
}
export default {
name: "UserCheckInput",
components: { UserCheckModal },
props: {
/** 当前值:`"all"` 或用户 id 逗号串 */
modelValue: {
type: String,
default: "all",
},
/** 选人弹窗的数据源页签(后端下发的 pickerSources,已带模块前缀) */
sources: {
type: Array as () => PickerSource[],
default: () => [],
},
/** 兜底数据源(后端只下发了 optionCall 时用;页签名固定「选择用户」) */
fallbackCall: {
type: Object as () => BrowserToolCall | undefined,
default: undefined,
},
/** 「全部」哨兵值(后端下发,默认 all) */
allToken: {
type: String,
default: "all",
},
/** 「全部」选项的显示名(后端下发,默认「全部用户」) */
allLabel: {
type: String,
default: "全部用户",
},
/** 编辑态:把「全部用户」显示成「现有用户」(模块页同款) */
isEdit: {
type: Boolean,
default: false,
},
disabled: {
type: Boolean,
default: false,
},
},
emits: ["update:modelValue", "names"],
data() {
return {
isCheckUserAll: 0 as number,
tempUserIds: [] as any[],
userNameStr: "",
editDisabled: false,
};
},
computed: {
/** 弹窗的页签:{ name, loader }(loader 返回原始组织树) */
tabs(): { name: string; loader: () => Promise<any[]> }[] {
const srcs: PickerSource[] = this.sources || [];
if (srcs.length) {
return srcs.map(s => ({
name: s.name,
loader: async () => parseTree((await callModuleApi(s.call)).body),
}));
}
const fb = this.fallbackCall;
if (fb) {
return [{ name: "选择用户", loader: async () => parseTree((await callModuleApi(fb)).body) }];
}
return [];
},
allText(): string {
return this.isEdit ? "现有用户" : this.allLabel;
},
},
created() {
this.syncFromValue();
},
methods: {
/** 按当前 v-model 值同步界面(初值与外部改动共用一份口径) */
syncFromValue() {
const v = this.modelValue;
if (v === this.allToken || !v) {
this.isCheckUserAll = 0;
this.userNameStr = v === this.allToken ? this.allText : "";
} else {
this.isCheckUserAll = 1;
this.tempUserIds = parseIntList(v);
}
},
checkUserShow() {
(this.$refs.userCheckModal as any)?.show();
},
resetUser() {
// 清空指定用户 → 回到「全部用户」
this.tempUserIds = [];
this.userNameStr = "";
this.$emit("update:modelValue", this.allToken);
this.$emit("names", this.allText);
},
userAllCheck(type: number) {
const tempIds = parseIntList(this.modelValue);
if (type == 0) {
if (this.isEdit) {
this.tempUserIds = tempIds;
this.userNameStr = "现有用户";
this.$emit("update:modelValue", String(tempIds));
this.$emit("names", "现有用户");
} else {
this.tempUserIds = [];
this.userNameStr = this.allLabel;
this.$emit("update:modelValue", this.allToken);
this.$emit("names", this.allLabel);
}
} else {
if (this.isEdit) {
this.editDisabled = true;
}
this.tempUserIds = tempIds;
this.userNameStr = "";
this.checkUserShow();
}
},
submitUser(arr: any[]) {
// 确认用户
this.userNameStr = arr.map((x: any) => x.name).join("、");
const ids = arr.map((x: any) => x.id).join(",");
this.$emit("update:modelValue", ids || this.allToken);
this.$emit("names", this.userNameStr || this.allText);
if (!arr.length) {
// 一个人都没选 → 回到「全部用户」(模块页同款)
this.isCheckUserAll = 0;
this.userNameStr = this.allLabel;
this.$emit("update:modelValue", this.allToken);
this.$emit("names", this.allLabel);
}
},
},
watch: {
modelValue() {
this.syncFromValue();
},
},
};
/** `"101,102"` → `[101,102]`(非数字段丢掉,与模块页 `split().map(parseInt).filter` 同口径) */
function parseIntList(v: any): number[] {
return String(v || "")
.split(",")
.map(x => parseInt(x))
.filter(x => !isNaN(x));
}
</script>
<style scoped>
.check-user-wrap {
width: 100%;
}
.check-user-box {
display: flex;
align-items: center;
gap: 8px;
}
.check-user-box .radio {
width: 180px;
flex: none;
font-size: 12px;
vertical-align: middle;
line-height: 32px;
}
.check-user-box .input {
flex: 1;
}
</style>
+437
View File
@@ -0,0 +1,437 @@
<template>
<Modal v-model="modal" title="选择用户" width="720px" :mask-closable="false" :closable="false">
<Tabs v-if="sources.length > 1" :key="tabKey" type="card" :value="String(tabIndex)" @on-click="tabChange">
<TabPane v-for="(item, index) in sources" :key="index" :label="item.name" :name="index + ''"></TabPane>
</Tabs>
<AutoComplete
@on-search="handleSearch"
@on-select="handleSelect"
placeholder="输入名字搜索"
v-model="searchTreeName"
:clearable="true"
class="auto-box"
>
<Option v-for="item in searchResult" :key="item.id" :value="item.name">
<span>{{ item.name }}</span>
<span class="fr info">{{ item.group }}</span>
</Option>
</AutoComplete>
<div class="tree-wrap fix">
<div class="tree-panel fl">
<div class="tree-box">
<div :id="ztreeDomId" class="ztree"></div>
</div>
</div>
<div class="arrow-box fl">
<svg
t="1626916477711"
class="icon"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
width="36"
height="36"
>
<path
d="M138.935 256.078v511.844h63.42V256.078h-63.42z m126.842 0v511.844h127.215V256.078H265.777zM885.064 512L583.254 64.323v191.755H456.414v511.844h126.842v191.755L885.064 512z"
fill="#2d8cf0"
></path>
</svg>
</div>
<div class="user-panel fl">
<div class="title-box fix">
<Checkbox @on-change="checkAllUser" v-model="isCheckUserAll">全选</Checkbox>
<div class="fr">{{ tempUserList.length }}</div>
<a href="javascript:;" @click="delCheckUser" v-if="tempUserList.findIndex((x: any) => x.isCheck) > -1"
>删除选中</a
>
</div>
<ul class="user-box">
<li
v-for="item in tempUserList"
:key="item.id"
:class="{ active: item.isCheck }"
@click="checkUserOne(item)"
>
{{ item.name }}
<span class="info">{{ item.group }}</span>
</li>
</ul>
</div>
</div>
<template #footer>
<Button type="primary" @click="submit">提交</Button>
<Button @click="modal = false" style="margin-left: 8px">取消</Button>
</template>
</Modal>
</template>
<script lang="ts">
/**
* 选择用户弹窗 —— 自业务模块 NoticeFront 的 `components/UserCheckModal.vue` 照搬。
*
* <p>为什么整块搬过来而不是自己重写:这个弹窗的交互(左组织树勾选级联 / 右已选列表 /
* 全选 / 删除选中 / 按名字搜索回勾)细节很多,自己重写很难做到「和系统里看到的一样」。
*
* <p>与本体的**唯一区别是数据从哪儿来**:模块里是 alova 客户端直连自己的 `/api/*`,
* 这里改成由父组件传进来的 `sources[].loader` —— 助手侧统一走
* `callModuleApi`(带用户登录态 + 遇 302 先预热模块会话再重试),
* 因为助手的服务端**无法**代持模块会话。
*
* <p>依赖:jQuery + zTree 由 `index.html` 全局加载(模块页也是这个加载方式)。
* zTree 的数据预处理、`id` 形如 `"1,301"`(原 id, 父 id)等约定**原样保留**,
* 否则回显与去重都会错。
*/
import treeIconGroup from "@/assets/image/tree-icon-group.png";
import treeIconItem from "@/assets/image/tree-icon-item.png";
import { Message } from "view-ui-plus";
interface SelectSource {
name: string;
loader: () => Promise<any[]>;
}
/** 全局 jQuery(由 index.html 的 script 提供)。取用时机放在方法内,避免模块求值早于脚本加载。 */
function jq(): any {
return (window as any).$;
}
export default {
name: "UserCheckModal",
props: {
/** 数据源 tab:{ name, loader },loader 返回 ztree 树数据(children 嵌套,叶子=人员) */
sources: {
type: Array as () => SelectSource[],
default: () => [],
},
/** 已选用户 id(打开时自动勾选) */
userids: {
type: Array as () => number[],
default: () => [],
},
isDisabled: {
type: Boolean,
default: false,
},
},
emits: ["update:userids", "on-submit"],
data() {
return {
ztreeDomId: "user_check_tree_" + Math.floor(Math.random() * 1e8),
modal: false,
tabIndex: 0,
/** 每次打开弹窗自增,强制重建 Tabs,避免上次 tab 高亮残留与数据错位 */
tabKey: 0,
/** 树加载序号:tab 快速切换时只渲染最后一次请求的结果 */
treeSeq: 0,
isCheckUserAll: false,
cacheMap: new Map(),
currentTree: [] as any[],
searchTreeName: "",
searchResult: [] as any[],
tempUserList: [] as any[],
};
},
computed: {
/** 当前树扁平化(含父级名称,供搜索展示) */
currentTreeFlatten(): any[] {
const flatten = (data: any[], pid: number, pname: string): any[] =>
data.reduce(
(arr: any[], { id, name, children = [], type }: any) =>
arr.concat([{ id, name, pid, type, group: pname }], flatten(children, id, name)),
[]
);
return flatten(this.currentTree, 0, "");
},
},
methods: {
show() {
this.modal = true;
// 重置到第一个 tab:显式赋值 + key 重建 Tabs,保证高亮与数据一致
this.tabIndex = 0;
this.tabKey++;
this.changeEmpty();
this.initTree();
},
tabChange(index: string) {
const idx = Number(index);
// 严格比较:v-model 场景下 iview 会把值写成字符串 name,
// 宽松相等("1" == 1)会误判"未切换"导致 tab 永远不生效
if (idx !== 0 && this.tabIndex === idx) return;
this.tabIndex = idx;
this.changeEmpty();
this.initTree();
},
handleSearch(query: string) {
let arr: any[] = [];
const searchArr = this.currentTreeFlatten.filter(
(x: any) => x.type == 2 && x.name.indexOf(query) != -1
);
for (const item of searchArr) {
!arr.map(x => this.getSplitId(x.id)).includes(this.getSplitId(item.id)) && arr.push(item);
}
this.searchResult = arr;
},
handleSelect(query: string) {
if (query.indexOf(",") != -1) query = query.split(",")[0];
const treeObj = jq().fn.zTree.getZTreeObj(this.ztreeDomId);
const node = treeObj.getNodeByParam("name", query, null);
if (!!node) treeObj.checkNode(node, true, true, true);
},
/**
* ztree 数据预处理:拼接 pid 保证同人多组时 id 可区分;叶子标记 type=2。
* 组/用户判定优先用接口 isParent(组=文件夹),缺失时按有无子节点兜底;
* 并设置组/用户区分图标。
*/
formatTree(tree: any[], level = 0, pid: number = 0) {
tree.forEach((element: any) => {
element.open = level == 0;
element.pid = pid;
element.id = element.id + "," + pid;
element.checked = false;
if (typeof element.isParent !== "undefined") {
element.type = element.isParent ? 1 : 2;
} else {
element.type = element.children && element.children.length > 0 ? 1 : 2;
}
element.icon = element.type == 1 ? treeIconGroup : treeIconItem;
if (typeof element.children !== "undefined" && element.children.length > 0) {
element.children = this.formatTree(element.children, level + 1, this.getSplitId(element.id));
}
});
return tree;
},
initTree() {
const _this = this;
// 竞态守卫:慢请求返回时若已切换 tab,丢弃结果
const seq = ++this.treeSeq;
const cacheKey = this.tabIndex;
const source = this.sources[this.tabIndex];
if (!source) return;
const cached = this.cacheMap.get(cacheKey);
if (cached) {
this.createTree(cached);
return;
}
source
.loader()
.then((data: any[]) => {
if (seq !== _this.treeSeq) return;
_this.cacheMap.set(cacheKey, data || []);
_this.createTree(data || []);
})
.catch((e: any) => {
if (seq === _this.treeSeq) Message.error(e?.message || "加载组织数据失败");
});
},
createTree(rawData: any[]) {
const _this = this;
const jtree = JSON.parse(JSON.stringify(rawData));
const ftree = _this.formatTree(jtree);
_this.currentTree = ftree;
const treeSetting: any = {
view: {
dblClickExpand: false,
showIcon: true,
},
check: {
enable: true,
chkboxType: { Y: "ps", N: "ps" },
},
callback: {},
};
/** 勾选变化后同步右侧已选列表:收集勾选节点子树内的所有叶子 */
function formatUser(treeNode: any, checked: boolean) {
const recursionCheckIds = (tree: any): any[] => {
let arrIds: any[] = [];
for (const key in tree) {
const element = tree[key];
if (element.type == 2 && !element.chkDisabled) {
arrIds.push({ id: element.id, tid: element.tId });
}
if (typeof element.children !== "undefined" && element.children) {
arrIds = [...arrIds, ...recursionCheckIds(element.children)];
}
}
return arrIds;
};
const checkIds = recursionCheckIds([treeNode]);
const checkUserList = _this.currentTreeFlatten.filter((x: any) => checkIds.map(x => x.id).includes(x.id));
const checkUserArr: any[] = [];
for (const element of checkUserList) {
const findObj = checkIds.find((x: any) => x.id == element.id);
if (
!!findObj &&
checkUserArr.findIndex((x: any) => x.id == element.id) === -1 &&
_this.tempUserList.findIndex((x: any) => x.id == element.id) === -1
) {
checkUserArr.push({ ...element, isCheck: false, tid: findObj.tid });
}
}
if (checked) {
_this.tempUserList = _this.tempUserList.concat(checkUserArr);
} else {
const ids = checkUserList.map((x: any) => x.id);
_this.tempUserList = _this.tempUserList.filter((x: any) => !ids.includes(x.id));
}
}
treeSetting.callback.onClick = (event: any, treeId: string, treeNode: any) => {
const check = treeNode.checked;
const treeObj = jq().fn.zTree.getZTreeObj(treeId);
treeObj.checkNode(treeNode, !check, true);
formatUser(treeNode, treeNode.checked);
};
treeSetting.callback.onCheck = (event: any, treeId: string, treeNode: any) => {
formatUser(treeNode, treeNode.checked);
};
const treeObj = jq().fn.zTree.init(jq()("#" + _this.ztreeDomId), treeSetting, ftree);
// 回显:已选用户打勾(callbackFlag=true 会触发 onCheck,同步右侧列表)
const nodes = treeObj.getNodesByFilter((x: any) => _this.userids.includes(_this.getSplitId(x.id)));
const arr: any[] = [];
nodes.forEach((element: any) => {
const splitId = _this.getSplitId(element.id); // 重复 id 只选择第一个
if (!arr.includes(splitId)) treeObj.checkNode(element, true, true, true);
if (_this.isDisabled) treeObj.setChkDisabled(element, true);
arr.push(splitId);
});
},
checkAllUser(check: boolean) {
this.tempUserList.forEach((element: any) => {
element.isCheck = check;
});
},
checkUserOne(item: any) {
item.isCheck = !item.isCheck;
const tlist = this.tempUserList;
this.isCheckUserAll = tlist.length > 0 && tlist.filter((x: any) => x.isCheck).length == tlist.length;
},
delCheckUser() {
const _this = this;
const tlist = _this.tempUserList;
const delArr = tlist.filter((item: any) => item.isCheck);
const treeObj = jq().fn.zTree.getZTreeObj(_this.ztreeDomId);
delArr.forEach((element: any) => {
const node = treeObj.getNodeByTId(element.tid);
treeObj.checkNode(node, false, true, true);
});
_this.isCheckUserAll = false;
},
changeEmpty() {
this.tempUserList = [];
this.searchTreeName = "";
this.searchResult = [];
},
submit() {
const _this = this;
let idsArr = this.tempUserList.map((x: any) => _this.getSplitId(x.id));
idsArr = Array.from(new Set(idsArr));
this.$emit("update:userids", idsArr);
function delKey(obj: any, ...args: string[]) {
args.forEach(v => delete obj[v]);
return obj;
}
const objArr: any[] = [];
idsArr.forEach(element => {
let findObj = _this.tempUserList.find((x: any) => _this.getSplitId(x.id) == element);
findObj = delKey({ ...findObj }, "id", "isCheck", "tid", "pid", "type");
objArr.push({ ...findObj, id: element });
});
this.$emit("on-submit", objArr);
this.modal = false;
},
/** 树节点 id 形如 "1,301"(原id,父id),取真实 id */
getSplitId(strId: any) {
if (typeof strId === "string" && strId.indexOf(",") != -1) return parseInt(strId.split(",")[0]);
return strId;
},
},
};
</script>
<style scoped>
.tree-wrap {
height: 430px;
position: relative;
}
.tree-panel {
border: 1px solid #eee;
width: 280px;
height: 430px;
overflow: hidden;
}
.tree-box {
padding: 4px 0;
height: 428px;
width: auto;
overflow-y: auto;
border: none;
}
.user-panel {
border: 1px solid #eee;
width: 280px;
.title-box {
background: #f7f7f7;
height: 34px;
line-height: 34px;
padding: 0 12px;
a {
text-decoration: none;
float: right;
margin-right: 20px;
outline: none;
}
}
.user-box {
height: 396px;
overflow-y: auto;
li {
line-height: 32px;
padding-left: 12px;
cursor: pointer;
.info {
float: right;
margin-right: 12px;
font-size: 12px;
color: gray;
}
}
li:hover {
background: #f3f3f3;
}
li.active {
background: #e4edf6;
}
}
}
.arrow-box {
width: 128px;
text-align: center;
position: relative;
top: 50%;
margin-top: -17px;
}
.auto-box {
width: 200px;
margin-bottom: 10px;
:deep(.ivu-select-item .info) {
font-size: 12px;
color: gray;
position: relative;
top: 2px;
}
}
:deep(.ztree li) {
overflow: hidden;
}
:deep(.ztree li a) {
width: 100%;
}
:deep(.ztree li a:hover) {
text-decoration: none;
}
:deep(.ivu-auto-complete .ivu-select-dropdown-list) {
max-height: 310px;
overflow-y: auto;
}
</style>
+27 -1
View File
@@ -52,7 +52,18 @@ export interface FormField {
/** 勾选框(对齐模块页的 Checkbox,如「连续请假」) */
| "switch"
/** 级联选择(栏目动态字段的 province_city:省市区,值存编码数组) */
| "cascader";
| "cascader"
/**
* 富文本正文(wangEditor,对齐通知公告模块页的编辑器)。
* 提交给后端的是 **HTML 字符串**,与模块页 NoticeRequest.content 一致。
*/
| "richtext"
/**
* 「通知对象」自绘控件(模块页 components/UserCheckInput.vue 的同款):
* radio「全部用户 / 指定用户」+ 只读回显 + +/清空 + 选人弹窗(左组织树右已选列表)。
* v-model 值是 **字符串**:`"all"` 或用户 id 逗号串。
*/
| "userpicker";
type?: string;
required: boolean;
/** 字段说明(含义与格式) */
@@ -71,6 +82,21 @@ export interface FormField {
uploadPath?: string;
/** widget=file:是否允许多个附件 */
multiple?: boolean;
/**
* widget=richtext:正文里「插入图片」的上传地址(已带模块前缀的绝对相对路径)。
* 与 uploadPath 不是同一个接口(正文图片走 /api/file/UploadImage)。
*/
richTextUploadPath?: string;
/** widget=richtext:正文内图片的预览地址模板(已带模块前缀,含 {id} 占位) */
richTextPreviewPath?: string;
/**
* widget=userpicker:选人弹窗的**数据源页签**(后端下发,每个 call 都已带模块前缀)。
*
* <p>模块页的弹窗有两个页签(「基础平台」组织树 / 「自定义组」);
* 未下发时前端退回 `optionCall`(单页签)。取回的是**原始嵌套树**,不做拍平 ——
* 拍平会丢掉层级,树就画不出来。
*/
pickerSources?: { name: string; call: BrowserToolCall }[];
/** 「全部」哨兵值(如 ALL):作为多选的第一项,可选中代表「全部班级 / 全部宿舍」 */
allToken?: string;
/** 「全部」选项的显示名 */