# 丸子·张 完整公开语料 来源站点:https://blog.oneszhang.com/ 内容范围:94 篇文章 + 3 条策展问答,构建时从内容源直接生成,与网页正文同源。 机器可读面(同一份内容的不同取用方式): - 站点入口目录:https://blog.oneszhang.com/llms.txt - 文章索引 JSON:https://blog.oneszhang.com/api/articles.json - 检索索引 JSON:https://blog.oneszhang.com/api/search-index.json - 主题索引 JSON:https://blog.oneszhang.com/api/topics.json - 作者资料 JSON:https://blog.oneszhang.com/api/profile.json - 接口契约:https://blog.oneszhang.com/openapi.json - 文章 Markdown 镜像:https://blog.oneszhang.com/{year}/{month}/{day}/{slug}.md - 问答 Markdown 镜像:https://blog.oneszhang.com/answers/{slug}.md - RSS:https://blog.oneszhang.com/rss.xml 姓名:丸子·张 简介:一位热爱分享的小小IT工程师,记录泛微 OA 二次开发、Java 技术实践与日常折腾。 --- # 丸子是谁? 作者:丸子·张 来源:https://blog.oneszhang.com/answers/who-is-wanzi/ Markdown:https://blog.oneszhang.com/answers/who-is-wanzi.md 摘要:丸子·张(OnesZhang),一位热爱分享的小小IT工程师,专注泛微 OA(EC9/EC10)二次开发与 Java 技术实践。 **丸子·张**(英文名 OnesZhang,也可叫我丸子),是一位热爱分享的小小IT工程师。用他自己的话说:"小白一个,什么都会一点,什么都不精通,主打一个热爱分享。" 主要在做这些事: - **泛微 OA 二次开发**(EC9 / EC10):工作流、流程表单、ecode 前端、接口开发,从 E9 到 E10 都有实践记录 - **技术记录**:踩坑经验、实用技巧、效率工具 - **日常折腾**:软件分享、网站推荐、生活记录 技能领域包括:泛微 EC9 / EC10 OA 二次开发、ecode 前端、Java、工作流引擎、SQL / 数据库、Linux / Nginx、Docker、前端 JavaScript。 完整履历见[关于页](https://blog.oneszhang.com/about/),技术作品可以从[文章列表](https://blog.oneszhang.com/writing/)按系列翻阅,也可以直接在[问答页](https://blog.oneszhang.com/ask/)提问。 --- # 怎么联系丸子或寻求合作? 作者:丸子·张 来源:https://blog.oneszhang.com/answers/contact-collaborate/ Markdown:https://blog.oneszhang.com/answers/contact-collaborate.md 摘要:泛微 OA、Java 技术问题欢迎加微信 68499249 或发邮件到 mail@oneszhang.com 交流;也接受技术写作与工具开发方面的合作。 想联系**丸子·张**,最直接的方式是加微信或发邮件: - **微信**:`68499249` - **邮箱**:[mail@oneszhang.com](mailto:mail@oneszhang.com) - **GitHub**:[OnesZhang](https://github.com/OnesZhang) - **旧站**:[oneszhang.com](https://oneszhang.com/) **合作意向**:泛微 OA 二次开发、Java 技术问题欢迎邮件交流;也接受技术写作与工具开发方面的合作。 技术问题建议先在[问答页](https://blog.oneszhang.com/ask/)提问,或翻翻[文章列表](https://blog.oneszhang.com/writing/)看看是否已有现成的踩坑记录;更具体的交流再走邮箱。完整联系方式见[关于页](https://blog.oneszhang.com/about/)。 --- # 这个博客主要记录什么内容? 作者:丸子·张 来源:https://blog.oneszhang.com/answers/blog-records/ Markdown:https://blog.oneszhang.com/answers/blog-records.md 摘要:主要记录泛微 OA(E9/E10)二次开发、Java 技术实践、前端与脚本技巧、Docker 部署自托管,以及生活随笔。 这个博客是**丸子·张**的个人技术记录站,核心围绕泛微 OA 二次开发展开,按系列分为五大块: - **[泛微 E9](https://blog.oneszhang.com/writing/e9/)**:ecode 组件开发、流程表单、数据建模、后端接口与踩坑记录,是内容最丰富的系列,包括"丸子的 E9 小本本"等连载 - **[泛微 E10](https://blog.oneszhang.com/writing/e10/)**:从 E9 迁移到 E10 的实践笔记,含"丸子的 E10 小本本"、接口发布、明细表处理等 - **[部署与自托管](https://blog.oneszhang.com/writing/selfhost/)**:Docker 部署实践,如 Doris、excalidraw、olmocr 等自建服务 - **[前端与脚本](https://blog.oneszhang.com/writing/frontend/)**:JavaScript、Next.js、效率脚本与系统技巧 - **[生活与随笔](https://blog.oneszhang.com/writing/life/)**:技术之外的日常记录与站点运营 内容风格以**踩坑实录**为主,每篇都来自真实开发场景。想快速定位某类问题,可以在[问答页](https://blog.oneszhang.com/ask/)直接提问,或在[文章列表](https://blog.oneszhang.com/writing/)按系列浏览。 --- # [泛微 Ecology10] Ones 万能组件 V1:让表单组件变成任何你想要的样子 作者:丸子·张 来源:https://blog.oneszhang.com/2026/10/21/e10-ones-ufc/ Markdown:https://blog.oneszhang.com/2026/10/21/e10-ones-ufc.md 发布日期:2026-10-21T01:45:00.000Z 更新时间:2026-10-21T01:45:00.000Z 专栏:泛微 E10 标签: 摘要:Ones 万能组件是 E10 表单组件,不是 EB 页面组件。它在表单设计器组件管理中创建,配置 4 项:单文件 HTML、运行 JS、调试、AI 生成(默认折叠)。运行态用 Shadow DOM 渲染,脚本只注入 ctx;ctx 统一封装 WeFormSDK 与 formStore,提供字段读写、只读/必填/隐藏、初始值、选项显示名映射、明细 rowId、字段变化/属性变化/保存前 hook。AI 面板用内置模板 + 需求 + 字段清单生成提示词,AI 返回的 HTML 粘回即可。V1 已实测渲染、字段读写、只读拦截、固定选项 valueKey 写值、明细写入、保存拦截、hook 去重;打印模板、整表单只读、真实移动端 H5、超大配置、多实例压力未完成。源码与示例在 GitHub:OnesZhang/e10-ones-ufc。 > **为需求而生,为通用立命,让组件变成任何你想要的样子~** > 🔗 源码与示例: > 📌 **先说清楚:Ones 万能组件是 E10 表单组件,不是 EB 页面组件。** > > 它从**表单设计器的组件管理**创建,拖入的是**表单设计器**,不是 e-builder 页面设计器。因为它运行在表单页面里,所以能拿到表单上下文,才能读写当前表单的字段。 ## 界面 ![配置界面:代码 / 运行 JS / 调试 / AI 高级(默认折叠)](https://raw.githubusercontent.com/OnesZhang/e10-ones-ufc/main/screenshot-config.png) ![效果示例:Observation Type 双卡片选择器](https://raw.githubusercontent.com/OnesZhang/e10-ones-ufc/main/screenshot-effect.png) ## 为什么做这个组件 做过泛微 E10 表单二开的人大概都遇到过这种需求: - 表单里要一张「Safe / Positive vs Unsafe / Negative」的双卡片选择器; - 要一个指标卡、时间轴、步骤面板、特殊的联动说明块; - 要一段完全自定义的 HTML 布局,但**必须读写当前表单字段**; - 每个需求都写一次 ecode、发一次版,周期长、复用低; - 直接写 EB 页面组件,又拿不到表单字段上下文,或者把表单页面搞得很重。 平台的标准表单组件很稳,但形态是固定的;自定义 HTML 的自由和表单字段的契约,一直是两件事。 Ones 万能组件想做的,是把这两件事合起来:**管理员只配一个单文件 HTML,字段读写、只读、必填、初始值、明细、错误兜底由组件桥接层负责。** ## 它长什么样 配置面板只有 4 项: 1. **组件代码(单文件 HTML)**:可以写 `
...
`,也可以粘完整 HTML 文档; 2. **运行 JS**:关闭后只渲染 HTML/CSS,不执行脚本; 3. **调试模式**:显示脚本错误和字段解析提示; 4. **AI 生成(高级)**:默认折叠;展开后填需求 + 字段清单,一键生成并复制提示词。 AI 闭环: ```text 写需求 → 生成并复制提示词 → 粘给任意 AI → AI 返回一个 HTML 代码块 → 粘回「组件代码」→ 保存 → 预览 ``` 提示词由组件内置模板生成,包含运行环境约束、字段规则、`ctx` API、`data-ufc-*`、输出格式和自检清单。AI 不需要知道 `WeFormSDK`、`formStore` 的差异。 ## 核心设计:一个代码框 + 一层薄契约 ### 1. 单文件 HTML,Shadow DOM 渲染 组件把 HTML 归一化后放进 Shadow DOM,CSS 天然隔离,不污染表单页面;脚本只执行一次,字段变化只更新绑定,不重跑整段代码。 - 设计态:只静态渲染 HTML/CSS,不执行脚本; - 运行态:执行脚本,通过 `ctx` 读写字段; - 打印态:不执行脚本,保留静态骨架和绑定值; - 只读态:`ctx.set` 被拦截。 ### 2. `ctx` 字段桥接 AI 代码只面向这一层: ```js ctx.get('字段') // 原值 ctx.text('字段') // 显示文本(人员/选项自动取名称) ctx.set('字段', '值') // 写值 ctx.attr('字段') // { readOnly, required, hidden, editable } ctx.on('字段', fn) // 字段值变化 ctx.onAttr('字段', fn) // 只读/必填/隐藏变化 ctx.onBeforeSave(fn) // 保存前校验 ctx.rows('明细表') // 明细行 ctx.request(...) // 平台统一请求 ``` 底层优先取 `window.WeFormSDK.getWeFormInstance()`,不可用时降级 `props.formStore`。AI 只写 `ctx`,兼容性由组件负责。 ### 3. 声明式绑定 `data-ufc-*` 常见展示/输入不写 JS: ```html * ``` 字段值变化、属性变化、联动/钻取后,绑定自动刷新。 ### 4. 字段状态与初始值 - 只读:输入禁用、`data-ufc-if-edit` 隐藏、`ctx.set` 拒绝; - 必填:`data-ufc-if-required` 显示 `*`,`ctx.onBeforeSave` 兜底; - 隐藏:相关 UI 不渲染; - 关联/浏览/选项字段:用 `text()` 显示名称,空值时显示占位; - **初始值只从运行数据读取**,不写死示例;等表单就绪后刷新,联动/钻取后自动更新。 ### 5. 明细字段必须带 `rowId` 这是实测踩出来的坑:主表字段 mark 是 `field{fieldId}`,明细字段是 `field{fieldId}_{rowId}`。**明细字段不带 rowId 写值,会把所有行写成同一个值**。 桥接层现在强制要求 `rowId`,缺失直接抛错;`ctx.rows('明细表')` 负责拿行数据。 ### 6. 固定选项:显示名与写值 固定选项的 `getFieldObj().specialObj[].name` 可能是空的,显示名要从字段 `options` 按 `selectionId/valueKey` 映射。 写值也有坑:有些字段认 `valueKey`(比如 `0/1/2`),有些认 `selectionId`;写错时 store 变了但原生下拉不刷新。组件内部提示词和文档都要求:**写值前用 `ctx.obj` / 字段配置确认可识别值,写入后检查原生控件是否显示。** ## 一个真实示例:Observation Type V1 仓库里带了一个示例 `examples/observation-type.html`: - 绑定字段:`Observation Type`,数据 key `obstyp`; - `Positive (0)` / `Negative (1)`; - 点击整张卡片写入并同步勾选; - 只读/隐藏/打印态不响应; - 样式保持原稿:双列卡片、彩色头部、圆形图标、勾选状态、底部双语提示,无外边框; - 为了 Shadow DOM 只做了必要映射:`:root` → `:host`,`body` → `.ufc-root`。 效果就是一张「Safe / Positive vs Unsafe / Negative」的选择卡片,选中项显示绿色/红色勾选,另一侧是灰色空心圆。 ## V1 实测结果 已在 E10 目标环境验证: - Shadow DOM 渲染、`data-ufc-*`; - 字段读写、只读拦截; - 固定选项显示名与 `valueKey` 写值; - 明细 `ctx.rows`、明细字段带 `rowId` 写入; - 缺 `rowId` 抛错; - 保存前校验阻断 + 正常保存放行; - 配置保存/重开; - hook 去重(保存/字段/属性/动作); - AI 提示词生成。 **未完成**:打印模板场景、整表单只读、真实移动端 H5、超大配置体积、多实例压力。 ## 安全边界 组件会执行管理员配置的 JS,信任边界等同平台「页面源码 / 表单 ecode」。不要把它开放给不可信的低权限编辑者;正式发布前按你们的 `ecode-security-review` 流程确认直出/沙箱策略。 字段值、用户输入、接口返回永远不要拼进 HTML 字符串或 `eval`;用 `textContent` / `ctx.text`。 ## 源码与版本 - GitHub: - Release: - 示例:`examples/observation-type.html` - 文档:`docs/architecture.md`、`docs/ai-prompt.md`、`docs/test-report.md` **v1.0.0(2026-10-21)**:首个版本。 --- > 为需求而生,为通用立命,让组件变成任何你想要的样子~ > > —— Ones --- # Touch-Up:让任何触摸屏在 macOS 上即插即用,以及我做的那些修改 作者:丸子·张 来源:https://blog.oneszhang.com/2026/10/15/touch-up-multi-finger-shortcuts/ Markdown:https://blog.oneszhang.com/2026/10/15/touch-up-multi-finger-shortcuts.md 发布日期:2026-10-15T10:00:00.000Z 更新时间:2026-10-15T10:00:00.000Z 专栏:前端与脚本 标签: 摘要:Touch-Up(shueber/Touch-Up)是一个 macOS 用户态触摸屏驱动,由 Sebastian Hueber 开发:市面上绝大多数触摸屏都遵循 USB HID,在 Windows 上插上就能用,接到 Mac 上却毫无反应,Touch-Up 就是补这个缺口——读取 HID 原始数据、解析成触摸点、注入成系统级鼠标事件,不需要内核扩展,安装后授予辅助功能权限即可使用。它有两个形态:Touch Up 工具 App(默认交互借鉴 iPadOS:轻点=点击、拂动=滚动、手指稍作停留再移动=拖拽、双指捏合=缩放、双指点击=右键,还可以开启「触摸窗口把它带到前台」)和 TouchUpCore 框架(把多点触摸数据暴露给开发者,App 本身就是集成示例,可参考 DebugView)。兼容性方面官方说法是「任何在 Windows 上能用的触摸屏都应该能用」,并在 Iiyama TF3222MC、T2336MSC-B2 和 3M C4667PW 上做过测试,Releases 页面提供带公证的构建包。本文记录我在 Fork(OnesZhang/Touch-Up,分支 feature/multi-finger-swipes,commit d7f2933,8 个文件约 +933/−15)上提交的功能:给三指/四指手势增加可自定义的键盘快捷键映射。做法分两层:TouchUpCore 里扩展 12 种可映射手势(三/四指上、下、左、右滑动 8 种,轻点 2 种,长按 2 种),委托协议 TUCTouchDelegate 新增 performKeyboardShortcutForGesture:(滑动识别完成触发一次)与 beginKeyboardHoldForGesture:/endKeyboardHoldForGesture:(长按按下映射键、手指全部抬起才松开),一次手势会话互斥地只判定为滑动/轻点/长按之一,手指移动超过阈值默认 10mm 判为滑动、几乎没动就抬起判为轻点、停住不动默认 0.35s 判为长按;手指按在屏上的整个会话期间鼠标事件被抑制,所以手势不会顺带移动光标或误点击。App 层整理了 48 条 macOS 常用快捷键目录 ShortcutPreset.catalog(含 8 个媒体键,走 NX_KEYTYPE_*),按调度中心与空间、窗口管理、应用切换、导航编辑、系统、媒体与功能键分类,每个手势可在设置里绑定任意预设或设为 Off 保留原光标行为,默认值接近触控板经典体验:三/四指滑动映射调度中心与空间切换、长按映射 fn、轻点关闭;映射持久化在 UserDefaults,设置界面新增 Multi-Finger Shortcuts 分区并适配新旧两套 SwiftUI 布局。此外顺手修了三个 macOS 兼容性问题:缺少输入监控权限时 HID 注册对 NULL 队列无防护导致启动崩溃(现在优雅降级并在设置页顶部显示权限横幅,点 Grant Access 深链到系统隐私面板)、合成方向键缺少真实方向键的 numericPad 标志且控制类快捷键需要 SecondaryFn 标志否则会被调度中心与空间功能丢弃(参考 SO 61872579 / 45704155)、发送合成快捷键后修饰键状态残留会把后续普通点击变成右键(现在显式释放修饰键,fn 的按住状态用不带 SecondaryFn 的 flagsChanged 释放事件清除);并新增 GitHub Actions 工作流在 macOS runner 上自动构建 App,Fork 上还会把产物挂到滚动的 ci-build release 便于直接下载。 > 原作者仓库:[shueber/Touch-Up](https://github.com/shueber/Touch-Up) | 我的仓库(Fork):[OnesZhang/Touch-Up](https://github.com/OnesZhang/Touch-Up) | 本次修改所在分支:`feature/multi-finger-swipes` > 📌 > > 该文章由丸子引导AI辅助制作生成,如有改进意见,欢迎评论区留言。 > ## Touch-Up 是什么 Touch-Up 是一个 **macOS 用户态触摸屏驱动**,由 Sebastian Hueber(shueber)开发。它的出发点很朴素:市面上绝大多数触摸屏都遵循 USB HID 标准,在 Windows 上插上就能用,但接到 Mac 上却毫无反应——macOS 并不原生支持这些外接触摸屏。 Touch-Up 以用户空间驱动的方式解决了这个问题:它读取触摸屏通过 USB HID 上报的原始数据,将其解析为一个个"触摸点",再注入成系统级的鼠标事件。整个过程不需要内核扩展,不需要降级安全性,安装后授予辅助功能权限即可使用。 它有两个形态: 1. **Touch Up 工具 App**:让你直接用手指操作 Mac。默认交互逻辑借鉴了 iPadOS—— - 轻点 = 点击 - 快速拂动 = 滚动 - 手指稍作停留再移动 = 拖拽 - 双指捏合 = 缩放 - 双指点击 = 右键 - 还有一个很讨喜的选项:触摸窗口即可将其带到前台(类似 iPadOS 的台前调度) 2. **TouchUpCore 框架**:把多点触摸数据暴露给开发者,方便在 macOS 上做多点触摸原型——游戏、交互研究等场景都可以直接集成。App 本身就是一个集成示例(可参考其中的 DebugView)。 兼容性方面,官方说法是"任何在 Windows 上能用的触摸屏都应该能用",并在 Iiyama、3M 的几款商用触摸屏上做过测试。原作者还提供了带公证的构建包,可以在 [Releases 页面](https://github.com/shueber/Touch-Up/releases) 直接下载。 在接触到这个项目之前,我为了在 Mac 上用触摸屏,试过不少零散方案,要么是内核扩展(新系统上早就不让装了),要么行为怪异、滚动和缩放都不正常。Touch-Up 是我用过最接近"即插即用"的方案,而且代码结构清晰、完全用户态,所以决定基于它继续做点事情。 ## 我(OnesZhang)修改了什么 我 Fork 了原仓库,并在 `feature/multi-finger-swipes` 分支上提交了一个功能(commit `d7f2933`),核心一句话概括: **给三指 / 四指手势增加了可自定义的键盘快捷键映射。** 改动规模:8 个文件,约 +933 / -15 行。 ### 1. 三指 / 四指手势识别(TouchUpCore) 原版的多指手势主要服务于光标操作(点击、拖动、滚动、捏合)。我在 `TouchUpCore` 中扩展了手势枚举,新增了 12 种可映射手势: - 三指 / 四指 **滑动**(上、下、左、右),共 8 种 - 三指 / 四指 **轻点**,共 2 种 - 三指 / 四指 **长按**,共 2 种 同时在委托协议(`TUCTouchDelegate`)里新增了三个回调: - `performKeyboardShortcutForGesture:` —— 滑动识别完成时触发一次 - `beginKeyboardHoldForGesture:` / `endKeyboardHoldForGesture:` —— 长按开始时按下映射键、手指全部抬起时松开 一次手势会话**只会**被判定为滑动、轻点、长按三者之一,互斥且只触发一次,避免一次滑动连发多遍快捷键。 ### 2. 快捷键目录与注入(App 层) 我整理了一个精选的 macOS 常用快捷键目录(`ShortcutPreset.catalog`,共 48 条,其中音量、亮度、播放控制等 8 条走 `NX_KEYTYPE_*` 媒体键),按类别覆盖: | 类别 | 例子 | | --- | --- | | 调度中心与空间 | 调度中心 ⌃↑、应用窗口 ⌃↓、左右切换空间 ⌃←/⌃→、移动窗口到其他空间 | | 窗口管理 | 关闭 ⌘W、最小化 ⌘M、退出 ⌘Q、隐藏 ⌘H、循环切换窗口 `` ⌘` `` | | 应用切换 | ⌘⇥ / ⌘⇧⇥ | | 导航编辑 | 翻页、行首行尾、词间移动、复制剪切粘贴撤销、全选 | | 系统 | Spotlight ⌘Space、锁定屏幕 ⌃⌘Q、截图 ⌘⇧3/4/5、Launchpad、显示桌面 | | 媒体与功能键 | 音量、静音、亮度、播放控制,以及 **fn 键** | 每种手势都可以在设置里绑定任意一个预设,或者设为 Off 保留原有的光标行为。**默认值**接近 macOS 触控板的经典体验:三/四指滑动映射为调度中心和空间切换,长按默认映射为 fn 键,轻点默认关闭。 手势判定规则: - 手指移动超过阈值(默认 10mm)→ 触发滑动快捷键(触发一次) - 没怎么移动就抬起 → 轻点 - 停住不动(默认 0.35s)→ 长按,**按住映射键不放,直到手指抬起才松开** - 手指按在屏上的整个会话期间,**鼠标事件被抑制**——手势不会顺带移动光标或产生误点击 映射持久化在 `UserDefaults` 里,设置界面新增了 "Multi-Finger Shortcuts" 分区,新旧两套 SwiftUI 设置布局都做了适配。 :::tip[没配映射的手势会怎样] **一律保持原版行为。** 驱动层是按指数组独立开关的:某个指数组里只要有任意一条映射才会进入手势模式,全部为 Off 就完全退回原版光标管线——所以「三指配了、四指没配」不会互相影响,改动范围严格跟着配置走。 ::: ### 3. 顺手修掉的几个坑 测试过程中撞上了几个 macOS 兼容性问题,一并修复了: - **缺少"输入监控"权限时启动崩溃**:HID 设备注册时没有对 NULL 队列做防护,导致 App 一启动就崩。现在会优雅降级,并在设置界面顶部显示一条权限横幅,点击 "Grant Access" 直接深链到系统隐私设置面板。 - **合成方向键被调度中心忽略**:真实的方向键事件带 `numericPad` 标志,不带这个标志的合成事件会被调度中心和"空间"功能丢弃;同理,控制类快捷键需要 `SecondaryFn` 标志(参考 SO 61872579 / 45704155)。合成事件现在会补上正确的标志。 - **修饰键状态残留**:发送完合成快捷键后及时释放修饰键状态,否则后续的普通点击会被系统当成右键点击;fn 键的"按住"状态用一次不带 `SecondaryFn` 的 `flagsChanged` 释放事件来清除,与物理松键的形态一致。 - **新增 GitHub Actions 工作流**(`.github/workflows/build.yml`),在 macOS runner 上自动构建 App,方便后续合并前验证;Fork 上还会把构建产物挂到一个滚动的 `ci-build` release,可以直接下载来试。 ## 为什么值得 Fork 一份 Touch-Up 本体已经把最难的部分做完了:用户态 HID 解析、多屏支持、设备独占、旋转补偿、边缘死区……这些它都有。我加的这层"手势 → 快捷键"本质上是把触摸屏从"能用鼠标"推进到"能享受触控板级手势"——毕竟外接触摸屏的用户里,不少是把 Mac 当工作站、离不开调度中心和多空间切换的人。 欢迎试用、提 issue 或 PR: - 原仓库: - 我的 Fork:(分支 `feature/multi-finger-swipes`) - CI 构建产物: ## 致谢 感谢 Sebastian Hueber 开源了 Touch-Up。本 Fork 保留原有 MIT License,所有底层驱动与框架设计的功劳都属于原作者;我新增的代码同样以 MIT 协议开放。 --- # [泛微 Ecology10] 流程按钮说明浮标:绑定操作码而不是按钮名,改名和切语言都不失效 作者:丸子·张 来源:https://blog.oneszhang.com/2026/10/15/weaver-e10-workflow-action-guide/ Markdown:https://blog.oneszhang.com/2026/10/15/weaver-e10-workflow-action-guide.md 发布日期:2026-10-15T08:00:00.000Z 更新时间:2026-10-15T08:00:00.000Z 专栏:泛微 E10 标签: 摘要:泛微 E10(e-cology10)流程页右上角有一排操作按钮(提交/保存/删除/更多),「更多」菜单里还有十几项操作,业务用户看不懂;而按钮名称既可以被管理员自定义、也会随界面语言变化,因此任何按名称匹配的说明方案都会失效。本文给出的做法是做一个悬浮的 i 入口 + 下拉面板,数据来自平台官方口子 window.weappWorkflow.getFlowPageSDK().baseStore.operMenus,其字段 menutype 是稳定操作码(SUBMIT、SAVE_DATA、DELETE、REMIND 等),istop===1 表示渲染在右上角、否则进「更多」菜单,title 是平台当前显示名。实现上:面板每行的名称取平台 title(改名后与屏幕一致),释义按 menutype 查一张中英对照表;语言用 window.weappUtils.getLang(),zh* 走中文、其余走英文,每 800ms 重读一次,因此切换界面语言无需刷新页面;入口 top 不写死,取动作条内按钮的真实 bottom+8px,因为带「第 N 条(共 M 条)」条幅的页面会把工具条从约 30px 撑到 54px(实测写死 52px 会压住「更多」),而贴容器底边又会偏下(实测容器底部比按钮低 13px)。页面判定同样不看 URL:同一流程可以是 /sp/workflow/flowpage/view/{id},也可以是 e-builder 流程卡片页 /sp/ebdfpage/card/0/{objId}/{requestId},因此改用平台注入的流程上下文 getFlowPageSDK().baseStore.baseParam.workflowId 作为判据;实测 getFlowPageSDK() 在任何页面都返回对象(连门户都是),而动作条 .ui-title-right-container 在 e-builder 设计器页也存在,两者都不能用来判断;发起流程页 /sp/workflow/newflow、流程列表 /workflow/list/mine 与 /workflow/list/center、设计器页、门户均无 workflowId,判定为 false。降级链:拿不到 getFlowPageSDK() 时退回读 DOM(动作条 button)与 __WEAPP_AGENT_BRIDGE__.describe(menuId) 按名称匹配;表里没有的操作不渲染;页面无流程上下文或无动作条则不注入任何节点;节点被平台整块替换时下次 tick 自愈重挂。实测环境 @weapp/ecodesdk 0.1.54、@weapp/ui 0.0.389-hotfix11、agent bridge 0.2.0,面板层级 1050/1040(高于平台下拉 999、低于平台弹窗)。 > 🔗 配套 e-code 应用源码: > 📌 > > 该文章由丸子引导AI辅助制作生成,如有改进意见,欢迎评论区留言。 > > 这次的关键判断只有一句话:**不要按按钮名称绑定**。名称是给人看的,`menutype` 才是给程序看的。 ![流程动作说明浮标:右上角 i 入口 + 中英双语说明面板](https://raw.githubusercontent.com/OnesZhang/e10-ecode-workflow-action-guide/main/screenshot.jpg) ## 结论先行 1. 流程页右上角按钮与「更多」菜单的**稳定标识是 `menutype`**(`SUBMIT` / `SAVE_DATA` / `REMIND` …),用平台官方口子 `window.weappWorkflow.getFlowPageSDK().baseStore.operMenus` 就能拿到,**不需要展开菜单、不需要点任何按钮**; 2. `istop === 1` 的就是右上角按钮,其余进「更多」菜单 —— 分组不用去猜 DOM 结构; 3. 面板里**显示的名称取平台当前标题**(管理员改过名就显示改后的名字,永远和屏幕一致),**释义按 `menutype` 查表** —— 这就是「改名不失效」的关键; 4. **判断"这是不是一个流程页"也不能看 URL**:同一个流程可以是 `/sp/workflow/flowpage/view/{id}`,也可以是 `/sp/ebdfpage/card/…/{id}`;判据要落在平台注入的流程上下文 `baseParam.workflowId` 上; 5. 语言用 `weappUtils.getLang()`,每 800ms 重读一次,**切换界面语言不需要刷新页面**; 6. 位置**千万别写死 `top`**:带「第 N 条(共 M 条)」条幅的页面会把工具条整体下压(约 30px → 54px),写死偏移就会压住「更多」;而改贴容器底边又会偏下,因为容器底部比按钮低 **13px**。 一句话:**名称负责显示,操作码负责语义,页面靠上下文识别,位置靠量不靠猜。** ## 一、需求:按钮「看得见但不认识」 流程详情页右上角长这样:`提交`、`保存`、`删除`,再加一个 `更多`;点开「更多」还有十几项:复制流程、关注、转发、催办、消息接收设置、事项分享、事项群聊、打印、打印日志、复制链接、设为待办事项、流程信息、表单日志、委托、抄送、标记标签…… 对天天用的人来说这是肌肉记忆;对偶尔走一次流程的业务用户,**「转发」和「传阅」有什么区别、「催办」和「督办」差在哪**,基本只能靠猜。需求就是:在右上角放一个入口,点开用**用户自己的语言**说明每个按钮是干什么的。 需求本身很朴素,但它埋了一个必须一开始就想清楚的坑。 ## 二、关键判断:不能按按钮名称绑定 最初的原型是这么写的:读 DOM 里按钮的文案,拿文案去字典里查。 ```js // ❌ 不要这样 const label = button.textContent.trim(); // 「提交」 const entry = DICT[label]; // 靠名称查表 ``` 这个写法有两个致命问题,而且**两个都一定会发生**: 1. **按钮名称可以被管理员自定义**。E10 的流程节点操作菜单里,名称是配置项,管理员完全可以把「保存」改成「暂存草稿」; 2. **名称随界面语言变化**。同一个按钮,中文界面是「提交」,英文界面是 `Submit`。 任何以名称为 key 的方案,在这两种情况下都会**静默失效**——不报错,只是查不到、不显示。二开补丁里最怕的就是这种"不报错但没用"。 所以第一件事是:**找到平台给这个操作分配的稳定标识**。 ## 三、稳定标识:`operMenus` 里的 `menutype` 平台 JS-SDK 文档里有一句不起眼但很关键的话: > 可通过控制台输入 `window.weappWorkflow.getFlowPageSDK().baseStore.operMenus` 查看当前流程下操作按钮清单 实测下来它返回的就是当前节点可用的操作数组,每一项长这样: ```js window.weappWorkflow.getFlowPageSDK().baseStore.operMenus // [ // { menutype: 'SUBMIT', title: '提交', istop: 1, useable: 1, orderid: 0, menuKey: 2 }, // { menutype: 'SAVE_DATA', title: '保存', istop: 1, useable: 1, orderid: 5, menuKey: 23 }, // { menutype: 'DELETE', title: '删除', istop: 1, useable: 1, orderid: 6, menuKey: 16 }, // { menutype: 'IMPORT', title: '复制流程', istop: 0, useable: 1, orderid: 0 }, // { menutype: 'WATCH', title: '关注', istop: 0, useable: 1, orderid: 1 }, // { menutype: 'REMIND', title: '催办', istop: 0, useable: 1, orderid: 13 }, // // … 共 19 项:3 个 istop=1 + 16 个 istop=0 // ] ``` 三个字段分工非常清楚: | 字段 | 含义 | 为什么关键 | | --- | --- | --- | | `menutype` | 稳定操作码(`SUBMIT` / `SAVE_DATA` / `REMIND` …) | **改名、切语言都不变**,这才是绑定依据 | | `istop` | `1` = 渲染成右上角按钮,否则在「更多」菜单里 | 分组规则与平台 UI 完全一致,不用猜 | | `title` | 平台当前的显示名 | **只用于显示**,永远和屏幕上一致 | 拿到这个数组之后,「哪些按钮、分到哪一组」就全是数据问题了,**一行 DOM 文案都不用读**。 ### 验证:改名之后还准不准? 这是整件事最值得实测的一步。把「提交」改名为「测试按钮改名」,「催办」改名为「提醒一下」之后再看 `operMenus`: ```text SUBMIT → title: '测试按钮改名' ← 名称变了 REMIND → title: '提醒一下' ← 名称变了 ``` `menutype` 一个都没变,19 项依然 **19/19 命中**。面板的表现正是我们想要的: | menutype(不变) | 屏幕/面板上显示的名字 | 面板给出的释义 | | --- | --- | --- | | `SUBMIT` | 测试按钮改名 | 节点处理人填写完表单后提交,流程流转到下一处理人或节点。 | | `REMIND` | 提醒一下 | 提醒当前未处理人、审批人、发起人、抄送人或指定人尽快处理。 | **名字跟着平台走,释义跟着操作码走** —— 管理员怎么改名都不会讲错功能。 :::tip[顺手一个降级链] 拿不到 `getFlowPageSDK()`(比如非流程详情页)时,退回读 DOM:右上角读 `.ui-title-right-container button`, 「更多」菜单项读容器里 Menu 组件的 `data-weapp-agent-id` 再走平台 Agent Bridge 的 `__WEAPP_AGENT_BRIDGE__.describe(id)`(**同样不用展开菜单**),此时按名称匹配。 两个来源都取不到 → 一个节点都不注入。 ::: ## 四、页面判定:同样不能看 URL 做完上面这些,我以为已经稳了,直到遇到另一个流程页: ```text /sp/ebdfpage/card/0/1131336631151837202/1326512499322241028 ``` 它是 e-builder 的**流程卡片页** —— 同一个流程的另一种呈现形式,右上角同样是一排流程操作按钮。 但原来的实现是按 `/sp/workflow/` 前缀判断的,于是这个页面**一个节点都没注入**。 于是我把相关页面全部量了一遍,想找一个站得住的判据: | 页面 | URL | `getFlowPageSDK()` | `baseStore` | `baseParam.workflowId` | 动作条 | | --- | --- | --- | --- | --- | --- | | 经典流程详情 | `/sp/workflow/flowpage/view/{id}` | object | ✅ | ✅ | ✅ | | e-builder 流程卡片 | `/sp/ebdfpage/card/0/{objId}/{requestId}` | object | ✅ | ✅ | ✅ | | 发起流程 | `/sp/workflow/newflow` | object | ✗ | — | ✗ | | 流程列表 | `/workflow/list/mine`、`/workflow/list/center` | object | ✗ | — | ✗ | | e-builder 设计器 | `/sp/ebdapp/detail/…/flow/nodeInfo` | object | ✗ | — | ✅ ← 有动作条 | | 门户 | `/portal/view/…` | object | — | — | ✗ | **两个「看起来能用」的判据都被否掉了**: - `window.weappWorkflow.getFlowPageSDK()` **在任何页面都返回对象**,连门户和列表页也是 —— 它只证明模块加载了,不证明这是流程页; - 右上角动作条 `.ui-title-right-container` **e-builder 设计器页也有** —— 拿它判断会误伤。 最后站住的判据是**「平台有没有给这个页面注入流程上下文」**: ```js function isWorkflowPage() { const sdk = window.weappWorkflow && window.weappWorkflow.getFlowPageSDK ? window.weappWorkflow.getFlowPageSDK() : null; const param = sdk && sdk.baseStore ? sdk.baseStore.baseParam : null; return !!(param && String(param.workflowId || '').trim()); } ``` 上表里只有两个流程页有这个字段,其余页面连 `baseStore` 都没有。它的语义也正好是我们想要的: **有流程上下文 = 是流程页**,与 URL 长什么样无关 —— 以后再出新的页面形态(卡片、弹窗、自定义容器)都不用改判据。 :::tip[和 menutype 是同一个道理] `menutype` 是「别拿会变的按钮名称当键」,`workflowId` 是「别拿会变的 URL 当键」。 两次都是同一个教训:**判据要落在平台已经给出的语义上,而不是人眼看到的表象上。** ::: `entry.js` 用 1 秒轮询这个判据(前置脚本执行时平台上下文可能还没就绪),命中后加载业务模块并停掉轮询; 模块自己每 800ms 再判一次,所以 SPA 路由进出流程页会自动挂载 / 卸载。 ## 五、语言:跟着平台走,而不是自己做个开关 ```js const lang = window.weappUtils.getLang(); // 实测 'zh_CN' / 'en_US' const isZh = String(lang || '').toLowerCase().indexOf('zh') === 0; ``` `zh*` 走中文、其余(含取不到)走英文。释义表里每条都带中英两份: ```js { zh: '提交', en: 'Submit', zd: '节点处理人填写完表单后提交,流程流转到下一处理人或节点。', ed: 'Handler completes and submits the form; the workflow routes to the next assignee or node.' } ``` 模块**每 800ms 重读一次语言**,所以切换界面语言时**不需要刷新页面**,面板自己就变了 —— 这点是实测出来的:从 `zh_CN` 切到 `en_US`,面板标题、分组名、20 行释义在下一个 tick 全部变英文。 面板自身的文案(标题、分组名、关闭按钮)也走同一套语言表。 ## 六、定位这个坑:写死 `top` 会翻两次车 第一版把入口钉死在 `top: 52px`,在普通流程页看着挺好。直到遇到一个带了「第 1 条(共3条) 上一条/下一条」小条幅的页面: | | 普通流程页 | 带条幅的页面 | | --- | --- | --- | | 动作条容器高度 | 55px | 84px(被条幅整体下压) | | 按钮实际底边 | 42px | 72px | | 写死 `top: 52px` 的结果 | 刚好落在按钮下方 | **压在「更多」按钮上** | 于是改成"按真实位置量": ```js // 取动作条内所有 button 的真实底边,+8px let bottom = 0; bar.querySelectorAll('button').forEach(btn => { const rect = btn.getBoundingClientRect(); if (rect.height > 0 && rect.bottom > bottom) bottom = rect.bottom; }); const top = bottom > 0 ? Math.round(bottom + 8) : 52; // 量不到才回落 launcher.style.top = top + 'px'; panel.style.top = (top + 36) + 'px'; ``` ### 第二个坑:贴「容器」底边会偏下 改完第一版我贴的是**容器**的底边(`bar.getBoundingClientRect().bottom + 8`),结果用户反馈"没有条幅的时候按钮有点靠下"。 量一下就明白了: ```text 容器 .ui-title-right-container 底边 55px 里面按钮的真实底边 42px 容器自身多出的内边距 13px ← 就是它 ``` 容器自己有内边距,贴容器等于凭空多出 13px。**改成贴按钮底边之后,两种页面距离按钮都是稳定的 8px**(普通页 42→50,带条幅页 72→80)。 :::warn[结论] 布局相关的位置,优先量**目标元素**(按钮)本身,而不是它的容器;容器什么时候加了 padding,你是不知情的。 ::: ## 七、其余几个实现细节 - **层级**:入口 `1050`、面板 `1040` —— 高于平台下拉浮层(实测 `999`),低于平台弹窗。既不会被下拉盖住,也不会压住对话框。 - **自愈**:平台在页面初始化/重渲染时会整块替换节点,实测遇到过"入口被瞬间抹掉"。每次 tick 先检查 `document.body.contains(root)`,不在就重新挂。 - **幂等**:`entry.js` 只做 `/sp/workflow/` 前缀判断 + `asyncImport` 异步加载业务模块,加载成功即停掉自己的轮询;组件加载失败只打日志,**页面保持平台原样**。 - **零副作用**:不点击、不提交、不改平台节点。检测结果签名不变就不动 DOM,避免无意义重排。 - **什么都不显示也是一种正确**:页面没有流程上下文(比如流程列表页),或者当前页面没有任何可解释的操作 → **不注入任何节点**。 ## 八、生效范围与验证 生效范围**由流程上下文决定,不由 URL 决定**: | 页面 | `baseParam.workflowId` | 表现 | | --- | --- | --- | | 经典流程详情 `/sp/workflow/flowpage/view/{id}` | ✅ | 显示入口(实测 20 行) | | e-builder 流程卡片 `/sp/ebdfpage/card/0/{objId}/{requestId}` | ✅ | 显示入口(该页当前只有 2 个操作 → 面板 3 行:更多 / 打印 / 标记标签) | | `/sp/workflow/newflow` 发起流程 | ✗ | 零节点 | | `/workflow/list/mine`、`/workflow/list/center` 列表 | ✗ | 零节点 | | e-builder 设计器 `/sp/ebdapp/detail/…/flow/nodeInfo` | ✗ | 零节点(**虽然它也有动作条**) | | 门户 `/portal/view/…` | ✗ | 零节点 | 其余验证项: | 项目 | 结论 | 证据 | | --- | --- | --- | | 稳定绑定 | PASS | 改 2 个按钮名后仍是 19/19 按 `menutype` 命中 | | 页面判定 | PASS | 两个流程页 `workflowId` 有值;四个非流程页(含「有动作条」的设计器页)判定为 false | | 中英双语 | PASS | `zh_CN` / `en_US` 两种语言下面板全部文案正确 | | 语言热切换 | PASS | 不刷新页面,面板随 `getLang()` 自动重渲染 | | 定位 | PASS | 普通页按钮底边 42 → 入口 `top=50`;带条幅页 72 → `top=80`,距按钮均 8px | | 交互 | PASS | 打开 → `×` → 重开 → `Esc` → 重开 → 点面板外,全部按预期关闭 | | 非目标隔离 | PASS | 无流程上下文的页面:0 个自有节点、无全局副作用 | | 与平台菜单互不干扰 | PASS | 应用加载后点「更多」,平台菜单正常展开全部菜单项 | 实测环境:`@weapp/ecodesdk` 0.1.54、`@weapp/ui` 0.0.389-hotfix11、Agent Bridge 0.2.0。 ## 九、回滚与注意事项 - **平台侧回滚**:在 ecode 控制台取消发布 / 停用该应用即可完全恢复;脚本与样式都由 `.wfg-*` 自有类名触发,**无全局副作用**; - **本地侧回滚**:删掉应用目录即可。 :::warn[四个提醒] 1. **`operMenus` 是这套方案的地基**:它是平台官方口子,但字段(`menutype` / `istop` / `useable`)建议在升级后复测一遍; 2. **释义表要跟着业务走**:表里没有的操作**不会显示**(不臆造含义)。新增动作只需补一行 `menutype` 映射,查 `menutype` 的方式就是打印 `operMenus`; 3. **位置每次 tick 都量**:这既是它能适应条幅页的原因,也意味着如果平台把工具条改成懒加载/动画,需要重新确认时序; 4. **页面判据依赖 `baseParam.workflowId`**:万一平台以后给某些非实例页也注入了流程上下文,那些页面同样会出现入口;到那时把它和别的信号组合起来收紧即可。 ::: --- *核心其实只有一句话:**别拿给人看的表象当程序的键** —— 按钮名会改会翻译,URL 会因页面形态而变,而 `menutype` 和 `workflowId` 不会。* --- # [泛微 Ecology10] 流程提交失败提示弹窗化:不改平台 DOM,只加一个类名 作者:丸子·张 来源:https://blog.oneszhang.com/2026/10/09/weaver-e10-workflow-submit-fail-modal/ Markdown:https://blog.oneszhang.com/2026/10/09/weaver-e10-workflow-submit-fail-modal.md 发布日期:2026-10-09T04:40:00.000Z 更新时间:2026-10-09T04:40:00.000Z 专栏:泛微 E10 标签: 摘要:泛微 E10 流程详情页(PC /sp/workflow/flowpage/*)提交失败时,平台会在页面顶部渲染一条内联红色横幅 div.wffp-operate-msgTip.form-msgtip,页面 URL 带 operResultType=FAIL_SHOW_ERROR_MSG,同时占用表单顶部位置。文章给出的做法是复写官方二开插槽 weappWorkflow.FPDevBlockSlot:只在 blockType === 'Prompts' 且 slotPlace === 'after' 时挂载自定义组件,其余插槽原样透传 。组件不重写平台 DOM、不复制平台文案,只给平台已渲染的提示节点挂一个类名 wffp-submit-fail-modal-card,由 CSS 把它变成居中弹窗,因此平台原有文案、出口条件明细和「点击这里进行设置」跳转逻辑全部保留;平台若改了该类名,补丁只是静默失效退回内联提示,不会报错。文章还记录了 hidden/open/closed 状态机与 MutationObserver 同步、遮罩/×/Esc 关闭与右下角重开入口,以及两个取值细节:卡片需 position: fixed(实测祖先无 transform)、z-index 1100 要高于页面右侧悬浮工具(实测 998/999)。最容易踩的坑是平台「条件」悬浮提示 .ui-trigger-popupInner 为 z-index 999,会被卡片(1100)/遮罩(1050) 盖住而「悬停或点击没反应」,需在 body.wffp-submit-fail-modal-open 作用域下把它抬到 1200。实测环境 @weapp/ecodesdk 0.1.53、@weapp/ui 0.0.387-hotfix7,最终 JS 91 行 + CSS 109 行。 > 🔗 配套 e-code 应用源码: > 📌 > > 该文章由丸子引导AI辅助制作生成,如有改进意见,欢迎评论区留言。 > > 这是一次「克制」的改造记录:**不重写平台一个 DOM 节点,只给平台已经渲染好的提示挂一个类名**,剩下的交给 CSS。 ![改造后效果:居中弹窗 + 条件悬浮提示](https://raw.githubusercontent.com/OnesZhang/e10-ecode-workflow-submit-fail-modal/main/screenshot.jpg) ## 结论先行 1. 流程提交被拒后,平台渲染的是**内联红色横幅** `div.wffp-operate-msgTip.form-msgtip`,URL 上会带 `operResultType=FAIL_SHOW_ERROR_MSG`; 2. 正确姿势是复写官方二开插槽 `weappWorkflow.FPDevBlockSlot`,**只命中 `Prompts` + `after`**,其它插槽原样透传; 3. 组件**不重写平台 DOM、不复制平台文案**,只加一个类名 `wffp-submit-fail-modal-card`,由 CSS 变成居中弹窗——平台原文案、条件明细、「点击这里进行设置」全部保留; 4. 全程最大的坑是**层级**:平台「条件」悬浮提示会被弹窗盖住,必须把 `.ui-trigger-popupInner` 从 `999` 抬到 `1200`。 一句话:**平台负责内容,我们只负责容器和层级。** ## 一、现象:失败提示是一条内联横幅 流程提交被平台拒绝时页面会重新加载,URL 上带一个结果标记: ```text /sp/workflow/flowpage/view/{requestId}?operResultType=FAIL_SHOW_ERROR_MSG ``` 随后平台在流程详情页**顶部信息提示区**渲染一条内联红色横幅 `div.wffp-operate-msgTip.form-msgtip`,内部依次是标题 `msgTipTitle`、正文 `msgTipTop`、出口条件明细 `msgTipFirst`、承载节点名与「条件」高亮的 `msgTipThird`、含「点击这里进行设置」的 `msgTipBottom`。 它本身没错,但**占着表单顶部的位置**,红底红图标还容易被业务用户读成「系统故障」。诉求很明确:**改成弹窗**。 ## 二、思路:只挂类名,绝不重写平台 DOM 第一反应通常是「把这段 HTML 抠出来自己渲染一个 Modal」。能走通,但代价很大:平台文案会变、条件明细的 DOM 结构和样式要复刻、「点击这里进行设置」的跳转是平台逻辑,复制一份就等于埋了个会分叉的坑。 所以换了个方向:**平台已经把内容渲染好了,我只要把它「搬」到视口中央**——保留平台节点本身,只 `classList.add('wffp-submit-fail-modal-card')`,弹窗的一切(定位、圆角、投影、标题栏、条件块)全部由 CSS 完成。平台后续改文案、改条件明细结构,都不影响这个补丁。 :::tip[这个取舍带来的最大好处] **降级是静默的**。如果哪天平台把 `.wffp-operate-msgTip` 这个类名改了,`querySelector` 找不到节点,组件就什么都不做,页面退回**平台原样的内联提示**,不白屏、不报错、不阻断提交。对二开补丁来说,「改不动」远好于「改坏」。 ::: 代价也要说清楚:**样式强耦合在平台类名上**——类名在,弹窗在;类名没了,弹窗就没了。 ## 三、入口:复写插槽,且只碰一个插槽 平台提供了官方二开口子(《流程详情页面-二开口子》),插槽组件是 `weappWorkflow.FPDevBlockSlot`;实测其 props 契约包含 `weId / blockType / slotPlace / baseParam / visibleOperMenus`,`Prompts` 插槽的 `before`、`after` 都存在。入口只做两件事:**注册复写**、**异步加载组件**。 ```js import React from 'react'; import { regOvComponent } from '@weapp/utils'; import { asyncImport } from '@weapp/ecodesdk'; // 组件异步加载,避免入口包体积膨胀;失败返回空组件 → 保持平台原样 const WorkflowSubmitFailModal = React.lazy(() => asyncImport('${appId}', 'coms/WorkflowSubmitFailModal').catch((error) => { console.error('[workflow-submit-fail-modal] 弹窗组件加载失败,保持平台原提示', error); return { default: () => null }; }) ); // order=1,与官方示例保持一致 regOvComponent('weappWorkflow', 'FPDevBlockSlot', (Com) => { return React.forwardRef((props, ref) => { if (props.blockType === 'Prompts' && props.slotPlace === 'after') { return ( ); } return ; }); }, 1); ``` :::warn[三个必须注意的点] 1. **非目标插槽必须原样透传** ``,`props` 和 `ref` 一个都不能少,否则会破坏平台自身在该插槽上的渲染; 2. **目标插槽也要保留 ``**,不能只渲染自己的组件——同一个 `Prompts/after` 插槽可能还有**其它 ecode 应用**注册了内容,直接替换会把别人顶掉; 3. **`asyncImport` 必须挂 `.catch` 兜底**:入口包保持极小、极稳,真正的组件异步加载,加载失败返回 `{ default: () => null }`,**失败即无副作用**。 ::: 顺带一提,`${appId}` 是 E10 平台的运行时占位符,平台会自动替换成实际值,**不能写成字面量**。 ## 四、组件:一个三态状态机 + 一次 DOM 同步 平台的提示节点什么时候出现、什么时候被移除,我们说了不算,所以不猜、只观察: ```js const TIP = '.wffp-operate-msgTip'; const CARD = 'wffp-submit-fail-modal-card'; function findTip() { // 只在 PC 流程详情页生效,其余路由直接放弃 if (window.location.pathname.indexOf('/sp/workflow/flowpage/') !== 0) return null; return document.querySelector(TIP); } // 节点变化 → 挂/摘类名;检测到提示 → 自动弹;平台移除节点 → 复位 const sync = () => { const tip = findTip(); if (tip === tipRef.current) return; if (tipRef.current) tipRef.current.classList.remove(CARD); tipRef.current = tip; if (tip) tip.classList.add(CARD); setState(tip ? 'open' : 'hidden'); }; const observer = new MutationObserver(onMutate); // onMutate 用 rAF 节流 observer.observe(document.body, { childList: true, subtree: true }); ``` 用**同一个 `ref` 做「当前节点」的身份判断**(`tip === tipRef.current`),类名只会被挂一次、摘一次,切换页签或重新提交都不会重复添加。 状态则合并成一个字符串 `hidden / open / closed`,不要拆成 `isOpen` + `isClosed` 两个布尔量——那一定会出现「两个都 true」的中间态。关闭有三条路径,全部映射到 `closed`:**点击遮罩** `.wffp-submit-fail-modal-mask`、**点击右上角 ×**(带 `aria-label="关闭"`)、**按 Esc**(仅在 `open` 时挂 `keydown`,关闭即卸载)。`closed` 时右下角渲染「流程提交失败,查看原因」入口,点击回到 `open`;`hidden` 时组件直接返回 `null`。状态变化时还要同步两个 body 类名给 CSS 用:`wffp-submit-fail-modal-open`(控制层级提升)与 `wffp-submit-fail-modal-dismissed`(隐藏原内联横幅)。 ## 五、CSS:把内联横幅变成居中弹窗 改造平台节点要**压过平台自己的样式**,所以这一段带 `!important`;本应用自己渲染的节点则一律不加。 ```css :root { --wffp-fail-top: 12vh; --wffp-fail-width: min(640px, 92vw); } .wffp-operate-msgTip.wffp-submit-fail-modal-card { position: fixed !important; /* 实测祖先无 transform,fixed 相对视口有效 */ left: 50% !important; top: var(--wffp-fail-top) !important; transform: translateX(-50%) !important; width: var(--wffp-fail-width) !important; max-height: 74vh !important; overflow: auto !important; background: #fff !important; border-radius: 14px !important; z-index: 1100 !important; /* 高于页面右侧悬浮工具(实测 998/999) */ } /* 关闭后隐藏原内联横幅 */ body.wffp-submit-fail-modal-dismissed .wffp-operate-msgTip.wffp-submit-fail-modal-card { display: none !important; } ``` 视觉基调从「红色报错」改成「白色对话框 + 琥珀提醒」: | 元素 | 原平台 | 改造后 | | --- | --- | --- | | 卡片 | 内联条、底色 `#FEEDE7` | 纯白 `#fff`、圆角 14px、柔和投影 | | 标题图标 | 错误红 | 琥珀 `#FAAD14` | | 出口条件明细 | 无底色 | `#FFFBF0` + `3px #FFE58F` 左细条 | | 「条件」高亮 | 蓝 | 柔和琥珀 `#D48806` | | 「点击这里」/ 遮罩 | 品牌蓝 / — | 保持 `#1677FF` / `rgba(0,0,0,.35)` | 平台自带的「收缩全部」按钮在弹窗里没有意义,用 `display: none !important` 隐掉即可——**只是隐藏,不删节点**。 ## 六、最大的坑:条件悬浮提示被弹窗盖住了 弹窗上线后业务反馈:**悬停/点击条件明细里的「条件」,没反应**。 排查下来:样式和文案都正常、节点没被裁掉;用 `elementFromPoint` 打在「条件」上,命中的**不是**条件元素,而是**弹窗卡片自己**——说明不是事件问题,而是**有一个浮层被盖住了**。 原因:平台「条件」的悬浮提示是独立浮层 `.ui-trigger-popupInner`,`z-index` 为 **999**;而弹窗卡片是 **1100**、遮罩是 **1050**——浮层正好夹在中间,一辈子都出不来。修复只加一行,但**必须限定作用域**: ```css /* 弹窗打开时,把平台条件浮层抬到弹窗之上 */ body.wffp-submit-fail-modal-open .ui-trigger-popupInner { z-index: 1200 !important; } ``` :::tip[为什么一定要用 body 作用域] `.ui-trigger-popupInner` 是**平台全局浮层类名**,页面上很多组件都在用。无条件抬到 `1200` 会影响整个流程页乃至其它弹层的层级关系。用 `body.wffp-submit-fail-modal-open` 包一层,等价于声明「**只在失败弹窗出现的这段时间里**,条件浮层排在最上面」,弹窗一关类名移除,平台恢复原状。 ::: 最终这一套的层级是:**卡片 1100 / 遮罩 1050 / 关闭按钮 1150 / 条件浮层 1200**。 ## 七、其它两个取值细节 **卡片为什么可以直接 `position: fixed`?** 因为实测从卡片到 `body` 的祖先链上**没有 `transform`**。只要链上任何一层有 `transform`,`fixed` 就会相对那一层定位——所以这条不是「永远成立」,改动平台结构时要复测。 **关闭按钮为什么不需要定位容器?** 让按钮和卡片**共用同一组 CSS 变量**,再用 `calc()` 放到卡片右上角,省掉一层 DOM,也不用担心那层容器的 `overflow` 把按钮裁掉。实测按钮与卡片右上角偏差 `dx=0 / dy=0`: ```css .wffp-submit-fail-modal-close { position: fixed; left: calc(50% + var(--wffp-fail-width) / 2); top: var(--wffp-fail-top); transform: translate(-50%, -50%); z-index: 1150; } ``` ## 八、生效范围与验证 | 项目 | 结论 | | --- | --- | | 环境 | E10,`@weapp/ecodesdk` 0.1.53、`@weapp/ui` 0.0.387-hotfix7 | | 生效范围 | **仅 PC** 流程详情页 `/sp/workflow/flowpage/*` | | 移动端 / 列表页弹窗打开的详情 | **不改**,仍按平台原样内联展示 | | 关闭后再失败 | 平台重建提示节点 → 自动重新弹出 | | 非流程详情页 / 无失败提示 | 组件返回 `null`,无任何副作用 | 非目标隔离单独验证过:在**流程列表页** `/workflow/newflow` 上,应用虽已被页面加载,但**零残留元素、无 body 类名、页面内容无变化**——`findTip()` 第一行就按路由前缀挡掉了非目标页面。三条关闭路径(遮罩 / × / Esc)+ 一条关闭后的重开入口,与「点击这里进行设置」跳转均逐条实测通过。 ## 九、先跑通,再精简 第一版能跑但很啰嗦:首版用 `present` / `open` / `dismissed` 三个布尔量、关闭按钮外面套了定位容器、`MutationObserver` 还额外监听 `characterData`……总计 JS 152 行、CSS 186 行。效果确认稳定后再精简:三个布尔量合并为单状态、去掉定位容器改用共用变量 + `calc()`、去掉多余的 `characterData` 观察、自有节点不再写 `!important`。最终 **JS 91 行 + CSS 109 行**。 :::tip[建议的节奏] 二开补丁优先保证「**先跑通、先验证**」,精简放到功能确认之后。反过来做,很容易在还没验证效果时就为了少写几行而引入难查的定位/时序问题。 ::: ## 十、回滚与注意事项 - **平台侧回滚**:在 ecode 控制台取消发布 / 停用该应用即可完全恢复内联提示;脚本全走类名触发,**没有全局副作用**; - **本地侧回滚**:删掉该应用目录即可。 :::warn[两个提醒] 1. **强依赖平台类名**:`.wffp-operate-msgTip` 是这套方案唯一的耦合点。平台改类名 → 补丁静默失效退回内联提示(不报错,但也不再生效),升级后建议复测; 2. **层级要跟着平台走**:`1100 / 1050 / 1150 / 1200` 是当前实测结论,平台调整浮层 `z-index` 时需要重新核对。 ::: --- *核心其实只有一句话:**不要和平台抢 DOM,只接管容器与层级**。如果你在其它 E10 版本或其它提示插槽上试过类似做法,欢迎评论区交流。* --- # [泛微 Ecology10] e-builder「隐式传参」拆解:参数不进 URL,藏在 sessionStorage 作者:丸子·张 来源:https://blog.oneszhang.com/2026/09/20/weaver-e10-implicit-params/ Markdown:https://blog.oneszhang.com/2026/09/20/weaver-e10-implicit-params.md 发布日期:2026-09-20T09:30:00.000Z 更新时间:2026-09-20T09:30:00.000Z 专栏:泛微 E10 标签: 摘要:泛微 E10 e-builder 设计器里按钮配置「打开页面」并选择隐式传参时,参数不会拼到 URL 上,而是以目标链接地址为 key 写入 sessionStorage.jumplinkParams,URL 只带一个 jumplinkParamKey。文章先给出平台「隐式传参」选项的三条官方原文(URL 不拼参数、非 EB 内部地址不支持隐式传参、URL 自定义拼接参数优先级高于参数设置),再基于线上实测与运行时 JS 源码验证拆解写入端与读取端逻辑。关键突破:尽管官方称目标地址非 EB 内部地址(页面、布局、报表、视图、多级填报等)时不支持隐式传参,但写入端并不区分目标类型,e-code 自定义页面(/sp/custom/...)可通过手动读取 sessionStorage 突破该限制,附可直接复用的读取工具函数与 URL 传参对比。 > 📌 > > 该文章由丸子引导AI辅助制作生成,如有改进意见,欢迎评论区留言。 > > 这是一次探索记录:通过线上实测与运行时源码把「隐式传参」机制彻底摸清,并利用它的写入逻辑,突破了官方对目标地址类型的限制。 ## 结论先行 在 e-builder 设计器给按钮配「打开页面」事件、传参方式选 **隐式传参** 时: 1. 参数**不拼在 URL 上**,而是以目标链接地址为 key,写入 `sessionStorage.jumplinkParams`; 2. URL 上只带一个 `?jumplinkParamKey=<目标地址URL编码>`,它只是「取数钥匙」; 3. 目标页是 **e-builder 页面** → 平台运行时自动消费参数,无需写代码; 4. 目标页是 **e-code 自定义页面**(`/sp/custom/...`)→ 官方说不支持,但写入端不分目标类型,**手动读 sessionStorage 即可突破限制**(见第六节)。 一句话:**URL 是钥匙,sessionStorage 是保险箱。** ## 一、先读平台原文 「隐式传参」这个选项自带三条官方说明,很多结论其实都写在里面: > 1. URL 链接地址后面不会拼接参数,所以没有参数长度限制; > 2. 当打开页面链接地址**非 EB 内部地址**(包括页面、布局、报表、视图、多级填报等)时**不支持隐式传参**,只能以 URL 方式传参; > 3. 如果手动输入的 URL 链接地址上自定义拼接的参数和参数设置中重名,**URL 自定义拼接的参数优先级高于参数设置**。 第 2 条是全文最重要的背景:**目标地址必须是 EB 内部地址,隐式传参才成立**。后文的实测与突破,本质就是围绕第 2 条展开的。 ## 二、场景 设计器(ebddesigner)里给按钮配置了「单击 → 打开页面」事件: - 链接地址:`/sp/custom/demo-page1/dashboard`(自定义链接,当前页打开) - 参数:`testaaaaaaaaaa = 123123123131231` - 传参方式:**隐式传参**(勾选) 问题来了:跳过去之后,这个参数到底传给目标页没有?目标页怎么拿? ## 三、点击后实际发生了什么(实测) 发布页上点按钮,跳转后的 URL: ```text /sp/custom/demo-page1/dashboard?jumplinkParamKey=%2Fsp%2Fcustom%2Fdemo-page1%2Fdashboard ``` - URL 上**只有** `jumplinkParamKey`,值是目标链接地址的 URL 编码; - **找不到** `testaaaaaaaaaa`——它根本没上 URL(对应原文第 1 条:URL 后面不拼参数,自然也没有参数长度限制)。 参数在这里,sessionStorage: ```json // sessionStorage["jumplinkParams"] { "/sp/custom/demo-page1/dashboard": { "testaaaaaaaaaa": "123123123131231" } } ``` 结构很清楚:外层 key 是**目标链接地址**,内层才是真正的参数对象。 ## 四、运行时是怎么实现的 从 e-builder 运行时 JS(`ebdcoms/lib_b8bdedbc.js`)里扒出来的核心逻辑,逻辑很朴素: 写入端(跳转源页面触发): ```js // se(): 按目标地址把参数写入 sessionStorage function se(e, t) { var n = JSON.parse(sessionStorage.getItem("jumplinkParams") || "{}"); n[e] = t; // e = 目标链接地址, t = 参数对象 sessionStorage.setItem("jumplinkParams", JSON.stringify(n)); } // 隐式传参时:URL 只拼 jumplinkParamKey,参数不进 URL // Fe = Q ? `${目标地址}?jumplinkParamKey=${encodeURIComponent(be)}` : ...; // se(be, 参数对象); ``` 读取端(目标 e-builder 页面加载时执行): ```js var f = (function (e) { var t = JSON.parse(sessionStorage.getItem("jumplinkParams") || "{}"); var n = t[e] || t[decodeURIComponent(e)] || {}; var i = {}; Object.keys(n).forEach(function (k) { i[decodeURIComponent(k)] = decodeURIComponent(n[k]); // 平台写入时做了编码,这里统一解码 }); return i; })(qs.parse(location.search).jumplinkParamKey); // 最终合并进页面参数:{...页面变量默认值, ...隐式参数, ...urlParams, ...URL query} ``` :::tip[顺带一提] ebdpage 运行时还暴露了配套 API:`getImplicitParams()`(读)和 `setImplicitParams(addr, params)`(写)。e-builder 目标页的自动消费就是走的这套逻辑。 ::: ## 五、官方限制:e-code 页面收不到 :::note[官方限制] 官方原文第 2 条说得明白:目标地址**非 EB 内部地址**(页面、布局、报表、视图、多级填报等)时**不支持隐式传参**,只能 URL 传参。`/sp/custom/...` 的 e-code 页面正好不属于 EB 内部地址。 ::: 我验证的链路也印证了这一点: 1. `demo-page1` 是 e-code 页面,加载的是 ecodesdk 运行时,脚本列表里**没有** ebdcoms / ebdpage; 2. 它的源码(`DemoPage.js` / `DataList.js` / `entry.js`)是纯展示页,**没有任何读 URL 或 sessionStorage 的代码**; 3. e-code 的 `init.js`、`ecodesdk/lib.js`、`utils/lib.js` 里都搜不到 `jumplink` / `getImplicitParams`。 所以「官方不支持」是事实:**参数写进了 sessionStorage(写入端正常),但 e-code 目标页不会自动消费,默认也没人手动读,参数实际不生效。** ## 六、突破官方限制:e-code 页面手动读 官方限制说的是「**自动**传参不支持」,但写入端**并不区分目标类型**——不管目标是 e-builder 还是 e-code,跳转时参数都会照写进 `sessionStorage.jumplinkParams`,URL 也照带 `jumplinkParamKey`。 突破口就在这里:**e-code 页面只要自己在代码里按 `jumplinkParamKey` 去 sessionStorage 取数,隐式传参在非 EB 内部地址上照样能跑通**,等于绕过了官方第 2 条的限制。 把下面两个函数放进你的工具文件(如 `utils/implicitParams.js`),任何 e-code 项目都能直接用: ```js /** * 读取当前页面 URL 上 jumplinkParamKey 对应的隐式传参 * @returns {object} 参数对象;无参数或解析失败时返回 {} */ export function getImplicitLinkParams() { const key = new URLSearchParams(window.location.search).get('jumplinkParamKey'); if (!key) return {}; try { const all = JSON.parse(sessionStorage.getItem('jumplinkParams') || '{}'); const params = all[key] || all[decodeURIComponent(key)] || {}; // 平台写入时会对 key/value 做 encodeURIComponent,这里统一解码 const result = {}; Object.keys(params).forEach((k) => { result[decodeURIComponent(k)] = decodeURIComponent(params[k]); }); return result; } catch (e) { return {}; } } /** * 读取指定 key 的隐式参数值 * @param {string} name 参数名 * @param {*} defaultVal 默认值 */ export function getImplicitLinkParam(name, defaultVal) { const params = getImplicitLinkParams(); return params[name] !== undefined ? params[name] : defaultVal; } ``` 组件里直接用: ```js import { getImplicitLinkParams, getImplicitLinkParam } from './utils/implicitParams'; const params = getImplicitLinkParams(); console.log(params.testaaaaaaaaaa); // "123123123131231" const val = getImplicitLinkParam('testaaaaaaaaaa', '默认值'); ``` ## 七、URL 传参 vs 隐式传参,怎么选 | 传参方式 | URL 表现 | e-code 页面获取方式 | 参数可见性 | 参数长度 | | --- | --- | --- | --- | --- | | URL 传参 | `?testaaaaaaaaaa=123123123131231` | `location.search` / `useSearchParams` 直接读 | 明文可见 | 受 URL 长度限制 | | 隐式传参 | 仅 `?jumplinkParamKey=...` | 读 `sessionStorage.jumplinkParams`(见第六节) | 不可见,相对更安全 | **无限制**(原文第 1 条) | - 参数**不想暴露在 URL**、不想被分享链接带出 → 用隐式传参; - 参数**特别长**(如大段文本)→ 用隐式传参,没有 URL 长度限制; - 要**简单直接、支持分享链接、目标可能是任意页面** → 用 URL 传参。 ## 八、注意事项 1. **会话内有效**:参数存在 `sessionStorage`,同一浏览器会话内的跳转都能读到,**新窗口/新标签页打开也没问题**(我实测过); 2. **按 key 覆盖**:多次跳转同一地址会覆盖该地址下的参数;不同地址互不影响。 3. **key 精确匹配**:`jumplinkParamKey` 的值 = 目标链接地址(encodeURIComponent 编码),读取时优先按原值匹配,兼容 `decodeURIComponent` 后再匹配(上面工具函数已处理)。 4. **混用优先级**:URL 传参 + 隐式传参同时存在时,URL 上会同时有普通 query 和 `jumplinkParamKey`,两者参数合并,**普通 query 优先**——这正是原文第 3 条说的:URL 上自定义拼接的参数优先级高于参数设置。 --- *如果你在别的 E10 版本上行为有差异,或者有更优雅的解法,欢迎评论区交流。* --- # [泛微 Ecology9] CUS_FIELDDATA 自定义字段数据表解析 作者:丸子·张 来源:https://blog.oneszhang.com/2026/08/13/210/ Markdown:https://blog.oneszhang.com/2026/08/13/210.md 发布日期:2026-08-13T01:59:00.000Z 更新时间:2026-08-13T01:59:00.000Z 专栏:泛微 E9 标签: 摘要:泛微 EC9 的 CUS_FIELDDATA 表用于跨模块存储自定义字段值,按 scope 区分归属:文档中心对应 DocCustomFieldBySecCategory,以目录 ID 和文档 ID 定位;人力资源对应 HrmCustomFieldByInfoType,以标签页和人员 ID 定位。文章提醒该表数据按需生成,无记录属正常,迁移组织架构和人员信息时必须同步此表,且默认禁止 DELETE,需修改配置文件加入白名单后方可删除。 ## 一、表概述 `CUS_FIELDDATA` 是泛微 EC9 中**跨模块**存储自定义字段值的核心表。不同模块通过 `scope` 字段区分数据归属。 --- ## 二、知识管理(文档中心)自定义字段 当 `scope = 'DocCustomFieldBySecCategory'` 时,存储文档自定义属性。 ### 查询文档自定义字段值 ```sql SELECT 对应字段名 FROM cus_fielddata WHERE scope = 'DocCustomFieldBySecCategory' AND scopeid = 文档目录ID AND id = 文档ID; ``` :::tip 查不到数据 = 该文档在该目录下未维护过该自定义字段。 ::: --- ## 三、人力资源模块自定义字段 当 `scope = 'HrmCustomFieldByInfoType'` 时,存储人员自定义字段。 ### 关联字段说明 | 字段 | 含义 | |------|------| | `scope` | 固定 `'HrmCustomFieldByInfoType'` | | `scopeid` | 标识人员卡片标签页:
`-1` → 基本信息
`1` → 个人信息
`3` → 工作信息 | | `id` | 对应 `hrmresource` 表中的人员ID | ### 存储特点 :::details[按需生成,非默认存在] 系统为节省空间,人员默认无记录。**只有在前台维护(保存/修改)了自定义字段后**,才会生成对应记录。因此直接查询可能返回空。 ::: ### 数据操作 - **更新/插入**:需根据 `scope`、`scopeid`、`id` 精准定位,若无记录则先 `INSERT`。 - **迁移关键**:同步组织架构和人员信息时,**必须迁移此表**,否则人员信息可能显示异常。 --- ## 四、DML 操作限制与放行 ### 默认限制 默认情况下,`CUS_FIELDDATA` **禁止使用 DELETE** 操作。 ### 放行配置 编辑 `{ecology目录}\WEB-INF\prop\checksqlStandard.properties`: ```properties # 是否开启 SQL 校验 isOpen=true # delete 语句白名单(添加 cus_fielddata) delete=formtable_main_261_dt1,cus_fielddata # update 黑名单(禁止操作的表) update=workflow_requestbase,workflow_requestLog ``` :::tip 如需删除,请将 `cus_fielddata` 加入白名单后重启服务,**谨慎操作**。 ::: --- ## 五、总结速览 :::details[文档模块] - `scope` = `DocCustomFieldBySecCategory` - `scopeid` = 目录ID - `id` = 文档ID ::: :::details[人力资源] - `scope` = `HrmCustomFieldByInfoType` - `scopeid` = 标签页(-1/1/3) - `id` = 人员ID ::: :::details[运维要点] - 迁移必带 - 默认禁止删,需加白名单 - 按需生成,无记录不代表异常 ::: --- # [泛微 Ecology9] 自定义接口在线编辑java代码保存没反应解决方案 作者:丸子·张 来源:https://blog.oneszhang.com/2026/08/07/209/ Markdown:https://blog.oneszhang.com/2026/08/07/209.md 发布日期:2026-08-07T04:33:00.000Z 更新时间:2026-08-07T04:33:00.000Z 专栏:泛微 E9 标签: 摘要:泛微 EC9 中自定义接口在线编辑 Java 代码保存无反应、接口报 404,是安全机制拦截非白名单 IP 所致。文章给出三步处理:在 WEB-INF/prop 放入放行标记文件,从安全日志中查到本机出口 IP,再将其写入白名单配置文件;随后用 sysadmin 访问 updateRules.jsp 刷新规则,无需重启,集群环境需逐节点访问。文中还说明在线编辑代码存于数据库,各节点无需重复编辑。 ![image.png](https://cos.oneszhang.com/usr/uploads/2026/08/956232920.png) > 在线编辑 Java 代码,点击保存后页面转圈或毫无反应,这是 EC9 安全拦截机制导致的常见问题。 > 解决方案来自泛微客服,实测有效,记录于此。 --- ## 问题表现 - 路径:自定义接口 → 在线编辑 Java 代码 - 现象:保存按钮无反馈,代码未更新,接口404 ## 根本原因 EC9 对非白名单 IP 的接口操作进行了拦截,若不在白名单中,保存动作被静默丢弃。 --- ## 解决步骤(三步) ### 1. 放行标记文件 将 `weaver_isFix_integration_action.properties` 放入 `ecology/WEB-INF/prop/`,内容仅一行: ```properties isFix=1 ``` 该文件告知系统启用修复逻辑。 ### 2. 获取真实来源 IP 查看安全日志: ``` \ecology\WEB-INF\securitylog\systemSecurity_yyyy-MM-dd.log ``` 搜索最近的关键字: ``` getInterfaceRegisterCustomOperation ``` 找到类似记录: ``` Source IP: 124.70.128.80 ``` **将该 IP 记录下来**(这就是你的操作机器出口 IP)。 ### 3. 配置白名单 编辑 `weaver_security_rules_for_cus_access_white_list.xml`,将记录到的 IP 加入: ```xml 127.0.0.1 你的实际IP ``` 将该文件放到 `ecology/WEB-INF/securityXML/` 目录下。 --- ## 生效操作(无需重启) - 使用 `sysadmin` 登录系统 - 访问:`http://你的EC地址/updateRules.jsp` - 等待约 **60 秒**,使缓存规则刷新 - 重新测试保存功能 > 集群环境:**每个节点需单独访问** `/updateRules.jsp` --- ## 验证 保存代码后,检查日志中不再出现该拦截记录,代码生效即代表成功。 --- ## 致谢 本文方案源自泛微官方客服支持,特此感谢。 若问题依旧,请检查日志中是否有其他报错,或确认 IP 是否准确(若经过代理,需获取真实客户端 IP)。 :::tip[顺便!我确认了下:在线编辑需要挨个去集群环境编辑吗?] 答:不需要!节点附加操作(自定义接口 / JavaCode),它的代码是存储在数据库表 ActionSetting 的 javacode 字段中的 ,而不是存放在各节点的本地文件里。 ::: --- # [泛微 Ecology9] 获取IP地址-服务器IP-访问IP 作者:丸子·张 来源:https://blog.oneszhang.com/2026/08/07/206/ Markdown:https://blog.oneszhang.com/2026/08/07/206.md 发布日期:2026-08-07T03:10:46.000Z 更新时间:2026-08-07T03:10:46.000Z 专栏:泛微 E9 标签: 摘要:泛微 EC9 中获取 IP 的两段 JSP 代码:一段用 InetAddress 取本机服务器 IP,便于在集群环境下确认请求落在哪台节点;另一段在登录态下输出 x-forwarded-for、getRemoteAddr、Util.getIpAddr 等取值,并遍历全部请求头,用于排查 OA 获取用户 IP 不准确的问题。 # 获取服务器IP地址 常用于集群环境下查看访问的是哪个服务器~ ```jsp <%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%> <%@ page language="java" import="java.net.InetAddress" %> <% out.println("=====本机ip地址======
"); InetAddress ia = InetAddress.getLocalHost(); String hostaddr = ia.getHostAddress(); out.println(hostaddr); %> ``` # 获取用户到达服务器的ip地址 常用于排查 OA 系统获取用户 IP 不准确的问题 ```jsp <%@ page language="java" contentType="text/html; charset=GBK" %> <%@ page import="java.io.*" %> <%@ page import="java.net.*" %> <%@ page import="java.util.*,weaver.general.Util" %> <%@ include file="/systeminfo/init_wev8.jsp" %> ----------------------------------------------------------
<% if(user!=null ){ out.println("x-forwarded-for::::"+request.getHeader("x-forwarded-for")+"
"); out.println("Proxy-Client-IP::::"+request.getHeader("Proxy-Client-IP")+"
"); out.println("WL-Proxy-Client-IP::::"+request.getHeader("WL-Proxy-Client-IP")+"
"); out.println("getRemoteAddr::::"+request.getRemoteAddr()+"
"); out.println("Util.getIpAddr()::::"+Util.getIpAddr(request)+"
"); out.println("request::::"+request); out.println("

-----------------------------headers----------------
"); Enumeration headers = request.getHeaderNames(); while(headers.hasMoreElements()){ String header = ""+headers.nextElement(); //out.println("*********"+header+"---->"+request.getHeader(header)+"
"); Enumeration headerValues = request.getHeaders(header); while(headerValues.hasMoreElements()){ String headerValue = ""+headerValues.nextElement(); out.println("*********"+header+"---->"+headerValue+"
"); } } out.println("

-----------------------------header----------------
"); Enumeration headerValues = request.getHeaders("x-forwarded-for"); while(headerValues.hasMoreElements()){ String headerValue = ""+headerValues.nextElement(); out.println("*********x-forwarded-for---->"+headerValue+"
"); } }else{ out.println("no right!"); } %>
------------------------------------------------------------- ``` --- # [泛微 Ecology9] WorkflowToModeFormulaUtil 公式工具类使用指南 作者:丸子·张 来源:https://blog.oneszhang.com/2026/07/05/205/ Markdown:https://blog.oneszhang.com/2026/07/05/205.md 发布日期:2026-07-05T12:10:00.000Z 更新时间:2026-07-05T12:10:00.000Z 专栏:泛微 E9 标签: 摘要:泛微 OA E9 的 WorkflowToModeFormulaUtil 是一个零外部依赖的纯 Java 公式工具类,可按包路径直接调用,用于脱离流程上下文完成日期、数值、文本三类字段的类型转换与计算。文章逐一说明 year、days、datedelta、sum、average、concatenate、replace 等方法的重载用法与返回 String 的特性,并给出合同到期提醒、费用汇总、编号生成等完整示例,还介绍了与 ExpressionEvaluator 组合做表达式计算的实践。使用时需注意 month 返回 1 至 12、replace 起始位置从 1 开始、average 可变参数分母异常等坑点。 > 泛微OA E9 中用于工作流字段公式转换的工具类,所有方法 `public static`,零外部依赖,可独立使用。 :::tip[适用场景] 向第三方系统动态传参时,字段类型不一致需要公式转换;脱离流程上下文做数值/日期/文本计算。 ::: ## 类信息 | 项目 | 说明 | |------|------| | 包路径 | `weaver.formmode.interfaces.action` | | 类名 | `WorkflowToModeFormulaUtil` | | 依赖 | 无外部依赖,纯 Java 工具类 | | 返回值 | 所有方法返回 `String`,异常返回 `""` | ## 方法一览 :::details[日期类] ### year — 提取年份 ```java WorkflowToModeFormulaUtil.year("2025-06-01") // → "2025" WorkflowToModeFormulaUtil.year("2025/6/1") // → "2025" WorkflowToModeFormulaUtil.year("") // → "" ``` ### month — 提取月份(1-12) ```java WorkflowToModeFormulaUtil.month("2025-06-01") // → "6" WorkflowToModeFormulaUtil.month("2025-12-31") // → "12" ``` > ⚠️ > > 注意:返回 1-12,不是 Calendar 的 0-11。 > ### day — 提取日 ```java WorkflowToModeFormulaUtil.day("2025-06-15") // → "15" WorkflowToModeFormulaUtil.day("2025-06-05") // → "5" ``` ### days — 两日期天数差 ```java // 两个重载:String 参数 / List 参数 WorkflowToModeFormulaUtil.days("2025-01-01", "2025-06-01") // → "151" WorkflowToModeFormulaUtil.days("2025-06-01", "2025-01-01") // → "-151" // List 用法 List dates = Arrays.asList("2025-01-01", "2025-06-01"); WorkflowToModeFormulaUtil.days(dates); // → "151" ``` > 📌 > > 支持带时间格式:`"2025-01-01 08:00:00"`,计算时只取日期部分。 > ### datedelta — 日期加减天数 ```java WorkflowToModeFormulaUtil.datedelta("2025-06-01", 10) // → "2025-06-11" WorkflowToModeFormulaUtil.datedelta("2025-06-01", -5) // → "2025-05-27" WorkflowToModeFormulaUtil.datedelta("2025-12-31", 1) // → "2026-01-01" ``` ::: :::details[数值类] ### sum — 求和 ```java // 可变参数(至少2个) WorkflowToModeFormulaUtil.sum(1.0, 2.0, 3.0) // → "6.0" // List 参数 List list = Arrays.asList(100.5, 200.3, 300.2); WorkflowToModeFormulaUtil.sum(list); // → "601.0" ``` ### abs — 绝对值 ```java WorkflowToModeFormulaUtil.abs(-123.45) // → "123.45" WorkflowToModeFormulaUtil.abs(123.45) // → "123.45" ``` ### average — 平均值 ```java // 可变参数(至少2个) WorkflowToModeFormulaUtil.average(2.0, 4.0) // → "3.0" // List 参数 List list = Arrays.asList(100.0, 200.0, 300.0); WorkflowToModeFormulaUtil.average(list); // → "200.0" ``` > 🚨 > > average 的可变参数版本计算分母是 `num3.length + 2`,传 2 个以上参数时注意。 > ::: :::details[文本类] ### concatenate — 字符串拼接 ```java // 可变参数(至少2个) WorkflowToModeFormulaUtil.concatenate("姓名:", "张三", " 工号:", "001") // → "姓名:张三 工号:001" // List 参数 List list = Arrays.asList("A", "B", "C"); WorkflowToModeFormulaUtil.concatenate(list); // → "ABC" ``` ### replace — 子串替换 ```java // replace(原字符串, 起始位置, 替换长度, 新字符串) WorkflowToModeFormulaUtil.replace("Hello World", 7, 5, "Java") // → "Hello Java" ``` > 🚨 **坑点** > > `startnum` 从 **1** 开始,不是从 0 开始!`"Hello World"` 的第 7 个字符是 `W`。 > ::: ## 完整调用示例 ### 示例1:日期计算 — 合同到期提醒 ```java import weaver.formmode.interfaces.action.WorkflowToModeFormulaUtil; // 计算合同剩余天数 String startDate = "2025-01-01"; String endDate = "2025-12-31"; String remainDays = WorkflowToModeFormulaUtil.days( new java.text.SimpleDateFormat("yyyy-MM-dd").format(new java.util.Date()), endDate ); // 到期前30天的日期 String warnDate = WorkflowToModeFormulaUtil.datedelta(endDate, -30); // 提取年份用于日志 String year = WorkflowToModeFormulaUtil.year(endDate); System.out.println(year + "年合同,剩余" + remainDays + "天"); ``` ### 示例2:数值聚合 — 费用汇总 ```java // 模拟从第三方获取的费用数据 double travelFee = 1500.0; double hotelFee = 2800.0; double mealFee = 600.0; // 求和 String total = WorkflowToModeFormulaUtil.sum(travelFee, hotelFee, mealFee); // → "4900.0" // 取绝对值(退款场景) String refund = WorkflowToModeFormulaUtil.abs(-350.5); // → "350.5" // 平均日均费用 int days = 5; String dailyAvg = WorkflowToModeFormulaUtil.average( Double.parseDouble(total) / days, Double.parseDouble(total) / days ); ``` ### 示例3:文本拼接 — 生成编号 ```java String prefix = "PO"; String year = WorkflowToModeFormulaUtil.year("2025-06-01"); // "2025" String seq = "001"; String orderNo = WorkflowToModeFormulaUtil.concatenate(prefix, year, seq); // → "PO2025001" ``` ## 与 ExpressionEvaluator 组合使用 > ✅ **最佳实践** > > `WorkflowToModeFormulaUtil` 做类型转换,`ExpressionEvaluator` 做表达式计算,两者搭配覆盖大多数公式场景。 > ```java import org.wltea.expression.ExpressionEvaluator; import org.wltea.expression.datameta.Variable; import weaver.formmode.interfaces.action.WorkflowToModeFormulaUtil; // 1. 用 FormulaUtil 做预处理 String yearStr = WorkflowToModeFormulaUtil.year("2025-06-15"); // "2025" String daysStr = WorkflowToModeFormulaUtil.days("2025-01-01", "2025-06-15"); // "165" // 2. 组装变量 List vars = new ArrayList<>(); vars.add(Variable.createVariable("amount", 50000.0)); vars.add(Variable.createVariable("days", Integer.parseInt(daysStr))); vars.add(Variable.createVariable("threshold", 10000)); // 3. 用 ExpressionEvaluator 计算表达式 Object result = ExpressionEvaluator.evaluate( "amount / days * 30 > threshold", vars ); // → true(月均 > 1万) ``` ## 注意事项 | 要点 | 说明 | |------|------| | 返回类型 | 全部返回 `String`,数值场景需手动 `Double.parseDouble()` | | 异常处理 | 所有方法内部 try-catch,异常返回空字符串 `""` | | 日期格式 | 自动兼容 `-` 和 `/` 分隔符,可带 `HH:mm:ss` | | month 返回值 | 1-12,不是 Calendar 的 0-11 | | replace 起始位置 | 从 1 开始,不是从 0 | | 线程安全 | 所有方法无状态,线程安全 | | 独立使用 | 不依赖流程ID、表单ID、配置项,可脱离流程上下文直接调用 | --- # [泛微 Ecology9] 流程查看权限校验路径分析 作者:丸子·张 来源:https://blog.oneszhang.com/2026/06/25/204/ Markdown:https://blog.oneszhang.com/2026/06/25/204.md 发布日期:2026-06-25T03:08:00.000Z 更新时间:2026-06-25T03:08:00.000Z 专栏:泛微 E9 标签: 摘要:泛微 EC9 通过 ViewRequestForwardSPA.jsp 查看流程时,权限校验集中在 RequestAuthenticationService.verifyRequest() 中。文章梳理了共享、操作者、创建者、监控督办、相关流程、建模授权等 14 条按优先级依次检查的校验路径,逐条给出触发参数与前置条件,并对比 SPA 与 JSP 路由的校验差异,说明 E9 签名令牌机制如何防止 desrequestid 被篡改。适合排查流程无权限查看问题的二次开发人员参考。 ## 概述 通过 `ViewRequestForwardSPA.jsp` 查看目标流程时,系统在 `RequestAuthenticationService.verifyRequest()` 中进行权限校验。共有 **14 条**校验路径,按优先级依次检查,命中任意一条即授予查看权限。 ## 核心入口 - **JSP入口**: `workflow/request/ViewRequestForwardSPA.jsp` - **SPA路由**: `/spa/workflow/static4form/index.html?_rdm={timestamp}#/main/workflow/req?{params}` - **JSP路由**: `/workflow/request/ViewRequest.jsp?haveVerifyForward=true&{params}` - **权限校验类**: `com.api.workflow.service.RequestAuthenticationService` - **校验方法**: `verifyRequest()` ## 路由差异 | 路由 | 权限校验方式 | authStr参数 | desrequestid参数 | |------|-------------|------------|-----------------| | SPA (`LoadParamCmd`) | `RequestAuthenticationService.verify()` | 有效 | 不需要 | | JSP (`ViewRequest.jsp`) | Session机制 (`resrequestid{N}`) | 不用于校验 | URL参数被忽略,仅读Session | ## 全部校验路径(按简单到复杂排列) ### 1. 共享权限(无条件检查,0个参数) ```java // RequestAuthenticationService if (getRequestWfShareRight(info, requestid)) { return true; } ``` - **URL**: `ViewRequestForwardSPA.jsp?requestid=1908315` - **条件**: 用户对目标流程有共享权限 - **校验逻辑**: `WFShareAuthorization.getWorkflowShareJurisdiction(requestid, user)` - **前置条件**: 管理员或其他操作人已将流程共享给当前用户 - **参数**: 0个额外参数 --- ### 2. 操作者权限(无条件检查,0个参数) ```java // RequestAuthenticationService if (getRequestUserRight(info, requestid)) { return true; } ``` - **URL**: `ViewRequestForwardSPA.jsp?requestid=1908315` - **条件**: 用户是目标流程的当前操作人 - **校验逻辑**: 查询 `workflow_currentoperator` 表 - **前置条件**: 用户在流程的当前操作人列表中 - **参数**: 0个额外参数 --- ### 3. 创建者权限(SPA层额外检查,0个参数) ```java // LoadParamCmd if (creater == userid && creatertype == usertype) { canview = true; } ``` - **URL**: `ViewRequestForwardSPA.jsp?requestid=1908315` - **条件**: 用户是流程创建人 - **校验逻辑**: 比对 `workflow_requestbase.creater` 字段 - **前置条件**: 用户创建了该流程 - **参数**: 0个额外参数 --- ### 4. 监控权限(1个参数) ```java // RequestAuthenticationService if (ismonitor == 1 && getRequestMonitorRight(info, requestid)) { return true; } ``` - **URL**: `ViewRequestForwardSPA.jsp?requestid=1908315&ismonitor=1` - **条件**: 用户是目标流程的监控人 - **校验逻辑**: `WFUrgerManager.getMonitorViewRight(requestid, userid)` - **前置条件**: 流程配置了监控人,且当前用户在监控人列表中 - **参数**: 1个(`ismonitor=1`) --- ### 5. 督办权限(1个参数) ```java // RequestAuthenticationService if (urger == 1 && getRequestUrgerRight(info, requestid)) { return true; } ``` - **URL**: `ViewRequestForwardSPA.jsp?requestid=1908315&urger=1` - **条件**: 用户是目标流程的督办人 - **校验逻辑**: `SuperviseManagerBiz.hasWorkflowViewRight(user, requestid)` - **前置条件**: 流程配置了督办人,且当前用户在督办人列表中 - **参数**: 1个(`urger=1`) --- ### 6. 干预权限(1个参数) ```java // RequestAuthenticationService if ("1".equals(isintervenor) && getRequestInterventorRight(info, requestid)) { return true; } ``` - **URL**: `ViewRequestForwardSPA.jsp?requestid=1908315&isintervenor=1` - **条件**: 用户是目标流程的干预人 - **校验逻辑**: `SysWFLMonitor.getWFInterventorRightBymonitor(userid, requestid)` - **前置条件**: 流程配置了干预人,且当前用户在干预人列表中 - **参数**: 1个(`isintervenor=1`) --- ### 7. 相关交流权限(1个参数) ```java // RequestAuthenticationService if (fromCommunication > 0 && getRequestCommunicationRight(info, requestid)) { return true; } ``` - **URL**: `ViewRequestForwardSPA.jsp?requestid=1908315&fromCommunication=12345` - **条件**: 流程有相关交流记录 - **校验逻辑**: `WorkflowCommunicationBiz.hasWfResourceRight(requestId, fromCommunication)` - **前置条件**: 流程存在相关交流,且交流ID匹配 - **参数**: 1个(`fromCommunication={交流ID}`) --- ### 8. 主子流程权限(1个参数) ```java // RequestAuthenticationService if ((isrequest.equals("2") || isrequest.equals("3") || isrequest.equals("4")) && getRequestSubMainRight(info, requestid)) { return true; } ``` - **URL**: `ViewRequestForwardSPA.jsp?requestid=1908315&isrequest=2` - **条件**: 目标流程是当前流程的主流程/子流程/平行流程 - **校验逻辑**: 查询 `workflow_subwfrequest` 表 - **isrequest值**: `2`=主流程, `3`=子流程, `4`=平行流程 - **前置条件**: 流程之间存在主子关系 - **参数**: 1个(`isrequest=2/3/4`) --- ### 9. 建模关联授权(4个参数) ```java // RequestAuthenticationService if (formmodeflag.equals("formmode_authorize") && getModeRight(info, requestid)) { return true; } ``` - **URL**: `ViewRequestForwardSPA.jsp?requestid=1908315&formmode_authorize=formmode_authorize&authorizemodeId=5501&authorizefieldid=403885&authorizeformmodebillId=202514` - **条件**: 建模数据字段值包含目标流程requestid - **校验逻辑**: `ModeRightInfo.isFormModeAuthorize()` - **校验步骤**: 1. 检查用户对建模模块是否有查看权限 2. 查询 `ModeFieldAuthorize` 表确认字段配置了关联授权 3. 查询建模数据表中该字段的值是否包含目标requestid - **前置条件**: 建模模块已配置关联授权,且数据字段值正确 - **参数**: 4个 - `formmode_authorize=formmode_authorize`(标识位) - `authorizemodeId={建模模块ID}` - `authorizefieldid={关联授权字段ID}` - `authorizeformmodebillId={建模数据ID}` --- ### 10. 相关流程权限(2-3个参数) ```java // RequestAuthenticationService if ((isrequest.equals("1") || "1".equals(isfromTd)) && getrelateRequestRight(info, requestid)) { return true; } ``` - **URL(方式A)**: `ViewRequestForwardSPA.jsp?requestid=1908315&isrequest=1&desrequestid=1908325` - **URL(方式B)**: `ViewRequestForwardSPA.jsp?requestid=1908315&isrequest=1&desrequestid=1908325&fieldid=373385` - **条件**: A/B流程有共同操作人 + 表单字段值匹配 - **校验逻辑**: `getrelateRequestRight()` - **校验步骤**: 1. 防篡改校验:A/B流程操作人取交集,必须有共同操作人 2. 字段值校验:查询A流程表单字段值是否包含B流程requestid 3. 或签字意见校验:查询A流程 `workflow_requestlog.signworkflowids` 是否包含B流程requestid - **前置条件**: 两个流程有共同操作人,且表单数据关联正确 - **参数**: 2-3个 - `isrequest=1`(标识位) - `desrequestid={源流程requestid}` 或 `authStr` + `authSignatureStr` - `fieldid={表单字段ID}`(可选,指定具体字段) --- ### 11. 计划会议权限(2个参数) ```java // RequestAuthenticationService if ((("workplan".equals(fromModul) || "meeting".equals(fromModul)) && modulResourceId > 0) && getPlanMeetingRight(info, requestid)) { return true; } ``` - **URL**: `ViewRequestForwardSPA.jsp?requestid=1908315&fromModul=workplan&modulResourceId=12345` - **条件**: 流程关联了日程/会议,且用户有共享权限 - **校验逻辑**: `WorkPlanShareUtil.chkWFInWP()` 或 `MeetingShareUtil.chkWFInMT()` - **前置条件**: 流程被关联到某个日程或会议,且用户对该日程/会议有共享权限 - **参数**: 2个 - `fromModul=workplan` 或 `fromModul=meeting` - `modulResourceId={日程/会议ID}` --- ### 12. 流程分享权限(3个参数) ```java // RequestAuthenticationService if (isfromchatshare == 1 && getRequestShareRight(info, requestid)) { return true; } ``` - **URL**: `ViewRequestForwardSPA.jsp?requestid=1908315&isfromchatshare=1&sharer=2094&sharegroupid=xxx` - **条件**: 通过IM分享的流程链接 - **校验逻辑**: `ChatResourceShareManager.authority()` - **前置条件**: 流程通过IM分享给当前用户 - **参数**: 3个 - `isfromchatshare=1` - `sharer={分享人ID}` - `sharegroupid={分享组ID}` --- ### 13. 协作权限(2个参数) ```java // RequestAuthenticationService if (iscowork == 1 && getRequestCoworkRight(info, requestid)) { return true; } ``` - **URL**: `ViewRequestForwardSPA.jsp?requestid=1908315&iscowork=1&coworkid=12345` - **条件**: 流程关联了协作区 - **校验逻辑**: `CoworkCommonUtils.getWfRightByCowork()` - **前置条件**: 流程关联到协作区,且用户在协作区成员中 - **参数**: 2个 - `iscowork=1` - `coworkid={协作区ID}` --- ### 14. 报表权限(2个参数) ```java // RequestAuthenticationService if (("1".equals(isfromreport) || "2".equals(isfromreport) || "1".equals(isfromflowreport)) && getReportRequestRight(info, requestid)) { return true; } ``` - **URL**: `ViewRequestForwardSPA.jsp?requestid=1908315&isfromreport=1&reportid=xxx` - **条件**: 从报表入口查看,且有报表权限 - **校验逻辑**: `ReportAuthorization.checkReportPrivilegesByRequest()` - **前置条件**: 用户有对应报表的查看权限 - **参数**: 2个 - `isfromreport=1`(或`2`、`isfromflowreport=1`) - `reportid={报表ID}` --- ## 最短路径总结 | 排名 | 方式 | URL参数个数 | 前置条件 | |------|------|------------|---------| | 1 | 共享权限 | **0** | 用户已被共享该流程 | | 2 | 操作者权限 | **0** | 用户是流程操作人 | | 3 | 创建者权限 | **0** | 用户是流程创建人 | | 4 | 监控/督办/干预 | **1** | 用户已被配置为监控/督办/干预人 | | 5 | 相关交流 | **1** | 流程有相关交流记录 | | 6 | 主子流程 | **1** | 流程之间存在主子关系 | | 7 | 建模关联授权 | **4** | 建模数据字段值包含目标requestid | | 8 | 相关流程 | **2-3** | A/B流程有共同操作人 + 表单字段值匹配 | ## E9签名令牌机制(authStr / authSignatureStr) ### 原理 E9使用签名令牌替代明文 `desrequestid`,防止URL参数被篡改。 - **authStr**: Base64编码的权限属性字符串,包含 `viewChain`(查看链)、`mainid`(入口流程ID)等 - **authSignatureStr**: 对authStr的签名,基于用户ID和User-Agent生成 ### 工作流程 1. 用户从流程A点击链接跳转到流程B 2. 系统生成 `authStr`(包含 `viewChain=A的requestid`)和签名 3. 跳转到B时,`RequestAuthenticationService.verify()` 验证签名 4. 验证通过后,检查用户对A的权限,再检查A→B的关联关系 5. 全部通过则授予B的查看权限 ### 代码位置 - 签名生成: `RequestAuthenticationService` - 签名验证: `RequestAuthenticationService` - 查看链机制: `RequestAuthenticationService` ## 底层SQL查询详情 ### Path 1: 共享权限 → `getRequestWfShareRight()` **调用链**: `getRequestWfShareRight()` → `WFShareAuthorization.getWorkflowShareJurisdiction()` **SQL 1** — 查询共享范围记录: ```sql SELECT permissiontype, departmentid, deptlevel, deptlevelMax, roleid, rolelevel, roleseclevel, roleseclevelMax, seclevel, seclevelMax, userid, subcompanyid, sublevel, sublevelMax, jobid, joblevel, jobobj, jobobjid FROM Workflow_SharedScope WHERE requestid = '{requestid}' ``` **SQL 2** — 校验流程是否开启共享: ```sql SELECT wb.isshared FROM workflow_base wb, workflow_requestbase wr WHERE wb.id = wr.workflowid AND wr.requestid = {requestid} ``` **判定条件**: 存在任一 `Workflow_SharedScope` 记录匹配当前用户属性(部门/角色/安全级别/分部/岗位/指定人员),且 `workflow_base.isshared = '1'`。 **涉及表**: `Workflow_SharedScope`, `workflow_base`, `workflow_requestbase` --- ### Path 2: 操作者权限 → `getRequestUserRight()` **调用链**: `getRequestUserRight()` → `ServiceUtil.calculateCurrentNodeSql()` **SQL**: ```sql SELECT isremark, isreminded, preisremark, id, groupdetailid, nodeid, takisremark, takid, workflowid, handleforwardid, agenttype, agentorbyagentid, (CASE WHEN isremark=9 THEN 0.99 WHEN isremark=8 THEN 1.2 WHEN isremark=11 THEN 0.98 WHEN (isremark=1 AND takisremark=2) THEN 0.9 WHEN (preisremark=1 AND isremark=2) THEN 1.9 WHEN (isremark=0 AND takisremark=-2 AND isInMultiTak=1) THEN 2 ELSE isremark END) AS orderisremark FROM workflow_currentoperator WHERE requestid = {requestid} AND userid = {userid} AND usertype = {usertype} ORDER BY orderisremark, id ``` **参数**: `requestid`, `userid`, `usertype`(0=内部, 1=外部, 2=其他) **判定条件**: 遍历结果集,若任一行 `isremark` ∈ {0,1,5,7,8,9,11}(其中 isremark=0 需 nodetype≠3),则 `canview=true`。 **涉及表**: `workflow_currentoperator` --- ### Path 3: 创建者权限 → `LoadParamCmd` **SQL**(存储过程): ```sql {call workflow_Requestbase_SByID({requestid})} ``` 等价于: ```sql SELECT * FROM workflow_requestbase WHERE requestid = {requestid} ``` **判定条件**: `creater == userid AND creatertype == usertype` **涉及表**: `workflow_requestbase` --- ### Path 4: 监控权限 → `getRequestMonitorRight()` **调用链**: `getRequestMonitorRight()` → `WFUrgerManager.getMonitorViewRight()` → `Monitor.getMonitorInfo()` **SQL 1** — 获取请求创建者和流程ID: ```sql SELECT creater, workflowid FROM workflow_requestbase WHERE requestid = {requestid} ``` **SQL 2** — 检查是否管理员: ```sql SELECT 1 FROM hrmresourcemanager WHERE id = {userid} ``` **SQL 3** — 获取创建者虚拟组织: ```sql SELECT subcompanyid1, departmentid FROM HrmResourceVirtualView WHERE id = {createrId} ``` **SQL 4** — 主查询:查找监控配置(SQL Server版本): ```sql SELECT * FROM workflow_monitor_info a WHERE EXISTS ( SELECT 1 FROM ( SELECT 1 jktype, id FROM hrmresource WHERE id = {userid} UNION SELECT 2 jktype, id FROM hrmroles WHERE id IN ({roleids}) UNION SELECT 3 jktype, id FROM hrmresourcemanager WHERE id = {userid} ) t WHERE a.jktype = t.jktype AND ',' + a.jkvalue + ',' LIKE '%,' + CAST(t.id AS VARCHAR) + ',%' ) AND EXISTS ( SELECT 1 FROM workflow_monitor_detail b WHERE a.id = b.infoid AND workflowid IN ({allVersionWfids}) ) ``` > Oracle: `INSTR(','||a.jkvalue||',' , ','||t.id||',') > 0` > MySQL: `CONCAT(',',a.jkvalue,',') LIKE CONCAT('%,',t.id,',%')` **SQL 5** — 获取监控权限明细: ```sql SELECT * FROM workflow_monitor_detail WHERE infoid IN ({infoids}) AND workflowid IN ({allVersionWfids}) ``` **参数**: `userid`, `roleids`(用户角色ID列表), `allVersionWfids`(流程所有版本ID) **判定条件**: 用户匹配监控配置(jktype: 1=人员, 2=角色, 3=管理员),fwtype 范围校验通过(1=总部, 2=同分部, 5=同部门, 10=指定人员等),且 `workflow_monitor_detail.isview = '1'`。 **涉及表**: `workflow_requestbase`, `hrmresourcemanager`, `HrmResourceVirtualView`, `hrmresource`, `hrmroles`, `workflow_monitor_info`, `workflow_monitor_detail` --- ### Path 5: 督办权限 → `getRequestUrgerRight()` **调用链**: `getRequestUrgerRight()` → `SuperviseManagerBiz.hasWorkflowViewRight()` **SQL**: ```sql SELECT 1 FROM WORKFLOW_SUPERVISEOPERATOR WHERE userid = {userid} AND requestid = {requestid} ``` **判定条件**: 查询返回至少一行。 **涉及表**: `WORKFLOW_SUPERVISEOPERATOR` --- ### Path 6: 干预权限 → `getRequestInterventorRight()` **调用链**: `getRequestInterventorRight()` → `SysWFLMonitor.getWFInterventorRightBymonitor()` → `Monitor.getMonitorInfo()` SQL 与 Path 4 基本相同,额外查询: **SQL** — 获取干预权限: ```sql SELECT * FROM workflow_monitor_detail WHERE infoid IN ({infoids}) AND workflowid IN ({allVersionWfids}) ``` **判定条件**: `workflow_monitor_detail.isintervenor = '1'`(与 Path 4 的 `isview` 不同) **涉及表**: 同 Path 4,额外关注 `workflow_monitor_detail.isintervenor` 字段 --- ### Path 7: 相关交流权限 → `getRequestCommunicationRight()` **调用链**: `getRequestCommunicationRight()` → `WorkflowCommunicationBiz.hasWfResourceRight()` **SQL 1** — 查询交流内容中关联的流程: ```sql SELECT relatedwf FROM workflow_communicationcontent WHERE communicationid = {communicationId} ``` **SQL 2** — 查询交流回复中关联的流程: ```sql SELECT relatedwf FROM workflow_communicationreply WHERE contentid IN ( SELECT id FROM workflow_communicationcontent WHERE communicationid = {communicationId} ) ``` **判定条件**: 收集所有 `relatedwf` 值,判断 `{requestid}` 是否在其中。 **涉及表**: `workflow_communicationcontent`, `workflow_communicationreply` --- ### Path 8: 主子流程权限 → `getRequestSubMainRight()` **场景一: isrequest=2(验证主流程)** **SQL 1**: ```sql SELECT sub.mainrequestid FROM workflow_subwfrequest sub LEFT OUTER JOIN workflow_requestbase req ON req.requestid = sub.mainrequestid WHERE sub.subrequestid = '{requestid}' ``` **SQL 2**(回退): ```sql SELECT mainrequestid FROM workflow_requestbase WHERE requestid = '{requestid}' ``` **场景二: isrequest=3(验证子流程)** **SQL 3**: ```sql SELECT sub.mainrequestid requestid FROM workflow_subwfrequest sub LEFT OUTER JOIN workflow_requestbase req ON req.requestid = sub.subrequestid WHERE sub.subrequestid = '{mainRequestid}' ``` **场景三: isrequest=4(验证平行流程)** **SQL 4**: ```sql SELECT sub.subwfid, sub.isSame, sub.mainrequestid, req.requestname FROM workflow_subwfrequest sub LEFT JOIN workflow_requestbase req ON req.requestid = sub.mainrequestid WHERE sub.subrequestid = {requestid} ``` **SQL 5**: ```sql SELECT sub.subrequestid requestid, req.requestname FROM workflow_subwfrequest sub LEFT JOIN workflow_requestbase req ON req.requestid = sub.subrequestid WHERE sub.mainrequestid = {mainrequestid} AND sub.subwfid = {subWfSetId} AND sub.subrequestid <> {requestid} ``` **判定条件**: 查询结果中 requestid 与目标 requestid 匹配。 **涉及表**: `workflow_subwfrequest`, `workflow_requestbase` --- ### Path 9: 建模关联授权 → `getModeRight()` **调用链**: `getModeRight()` → `ModeRightInfo.isFormModeAuthorize()` **SQL 1** — 查询建模权限规则: ```sql SELECT righttype, orgrelation, sharetype, relatedid, rolelevel, showlevel, joblevel, jobleveltext, hrmCompanyVirtualType, layoutid, layoutid1, layoutorder FROM moderightinfo WHERE modeid = {modeId} AND ((sharetype IN (1,2,3,5,6) AND (conditionsql IS NULL OR conditionsql = '')) OR (sharetype = '4' AND isrolelimited <> '1' AND (conditionsql IS NULL OR conditionsql = ''))) AND righttype IN (1,2,3) AND (showlevel <= {seclevel} AND {seclevel} <= ISNULL(showlevel2, 9999)) ``` **SQL 2** — 查询共享明细表(动态表名): ```sql SELECT sourceid, MAX(sharelevel) sharelevel FROM modeDataShare_{modeId} WHERE sourceid = {formmodebillId} AND ( (type=1 AND content={userid}) OR (type=2 AND content={subcompanyid} AND seclevel<={seclevel} AND {seclevel}<=ISNULL(showlevel2,9999)) OR (type=3 AND content={departmentid} AND seclevel<={seclevel} AND {seclevel}<=ISNULL(showlevel2,9999)) OR (type=4 AND content IN ({roleIds}) AND seclevel<={seclevel}) OR (type=5 AND content=1 AND seclevel<={seclevel}) OR (type=6 AND content={jobtitle} AND seclevel<={seclevel}) OR (type=7 AND content={userid}) ) GROUP BY sourceid ``` **SQL 3** — 查询字段授权配置: ```sql SELECT a.*, b.fieldname, b.fielddbtype, b.fieldhtmltype, b.type, b.detailtable FROM ModeFieldAuthorize a, workflow_billfield b WHERE a.fieldid = b.id AND b.billid = {formId} AND a.modeid = {modeId} AND a.formid = {formId} AND a.fieldid = {fieldid} ``` **SQL 4** — 读取建模数据字段值(主表): ```sql SELECT {fieldname} FROM {maintablename} WHERE id = {formmodebillId} ``` **SQL 5** — 读取建模数据字段值(明细表): ```sql SELECT {fieldname} FROM {detailtable} WHERE mainid = {formmodebillId} ``` **判定条件**: 1. 用户对建模模块有查看权限(moderightinfo 或 modeDataShare_{modeId} 匹配) 2. `ModeFieldAuthorize` 表中存在该字段的关联授权配置 3. 建模数据字段值中包含 `{requestid}`(格式: `fieldValue.indexOf("," + requestid + ",") > -1`) **涉及表**: `moderightinfo`, `modeDataShare_{modeId}`(动态), `ModeFieldAuthorize`, `workflow_billfield`, `{maintablename}`(动态), `{detailtable}`(动态) --- ### Path 10: 相关流程权限 → `getrelateRequestRight()` **SQL 1** — 获取主流程 workflowid: ```sql SELECT workflowid FROM workflow_requestbase WHERE requestid = {requestid} ``` **SQL 2** — 防篡改校验(操作人交集): ```sql SELECT userid, usertype FROM workflow_currentoperator WHERE requestid = {mainRequestid} SELECT userid, usertype FROM workflow_currentoperator WHERE requestid = {requestid} -- Java层取交集 retainAll(),交集为空则拒绝 ``` **SQL 3** — 签字意见关联校验(isfromresource=1 或 fieldid=0 时): ```sql -- Oracle/PostgreSQL: SELECT 1 FROM workflow_requestlog WHERE requestid = {mainRequestid} AND ','||signworkflowids||',' LIKE '%,{requestid},%' -- MySQL: SELECT 1 FROM workflow_requestlog WHERE requestid = {mainRequestid} AND CONCAT(',',signworkflowids,',') LIKE '%,{requestid},%' -- SQL Server: SELECT 1 FROM workflow_requestlog WHERE requestid = {mainRequestid} AND ','+signworkflowids+',' LIKE '%,{requestid},%' ``` **SQL 4** — 获取字段定义(isbill=1): ```sql SELECT billid, fieldname, viewtype, detailtable FROM workflow_billfield WHERE id = {fieldid} SELECT tablename FROM workflow_bill WHERE id = {billid} ``` **SQL 5** — 获取字段值: ```sql -- 主表 (viewtype=0): SELECT {fieldname} FROM {tablename} WHERE requestid = {mainRequestid} -- 明细表 (viewtype>0): SELECT {fieldname} FROM {detailtable} WHERE mainid IN (SELECT id FROM {tablename} WHERE requestid = {mainRequestid}) ``` **判定条件**: 1. A/B流程操作人有交集(防篡改) 2. 签字意见 `signworkflowids` 包含目标 requestid,或表单字段值包含目标 requestid **涉及表**: `workflow_requestbase`, `workflow_currentoperator`, `workflow_requestlog`, `workflow_billfield`, `workflow_bill`, `{tablename}`(动态), `{detailtable}`(动态) --- ### Path 11: 计划会议权限 → `getPlanMeetingRight()` **子路径A: WorkPlan** **SQL 1** — 检查日程是否关联流程: ```sql SELECT * FROM workplan WHERE id = {workId} AND (requestid = '{requestId}' OR requestid LIKE '{requestId},%' OR requestid LIKE '%,{requestId},%' OR requestid LIKE '%,{requestId}') ``` **SQL 2** — 检查日程交流是否关联流程: ```sql SELECT * FROM Exchange_Info WHERE type_n = 'WP' AND sortid = {workId} AND (requestids LIKE '%,{requestId},%' OR relatedwf LIKE '%,{requestId},%') ``` **SQL 3** — 获取日程共享级别: ```sql SELECT workId, MAX(shareLevel) AS shareLevel FROM WorkPlanShareDetail WHERE workid = {workplanId} AND ( (objId={userId} AND shareType='1') OR (objId=0 AND shareType='5' AND securityLevel<={seclevel} AND securityLevelMax>={seclevel}) OR (objId IN ({compid}) AND shareType='2' AND securityLevel<={seclevel}) OR (objId IN ({deptid}) AND shareType='3' AND securityLevel<={seclevel}) OR (objId={jobid} AND shareType='8') OR (objId={roleid} AND shareType='4' AND securityLevel<={seclevel} AND {roleLevel}>=rolelevel) ) GROUP BY workId ``` **子路径B: Meeting** **SQL 4** — 检查会议是否关联流程: ```sql SELECT * FROM meeting WHERE id = {meetingId} AND ({fieldname} LIKE '%,{requestId},%' OR requestid = '{requestId}') ``` **SQL 5** — 检查会议共享: ```sql SELECT * FROM Meeting_ShareDetail WHERE meetingid = {meetingid} AND userid IN ({allUser}) AND sharelevel IN (1,2,4,5) ``` **SQL 6** — 检查会议成员: ```sql SELECT * FROM Meeting_Member2 WHERE meetingid = {meetingid} AND (membermanager IN ({allUser}) OR ','||othermember||',' LIKE '%,{userId},%') ``` **判定条件**: 流程关联了日程/会议,且 `getShareLevel > -1`。 **涉及表**: `workplan`, `Exchange_Info`, `WorkPlanShareDetail`, `meeting`, `Meeting_ShareDetail`, `Meeting_Member2`, `Meeting_Topic`, `Meeting_Decision` --- ### Path 12: 流程分享权限 → `getRequestShareRight()` **调用链**: `getRequestShareRight()` → `ChatResourceShareManager.authority()` **SQL 1** — 验证分享人为流程操作者: ```sql SELECT userid, usertype FROM workflow_currentoperator WHERE requestid = {resourceid} AND userid = {sharer} AND usertype = {sharertype} ``` **SQL 2** — 检查当前用户是否在分享范围内: ```sql SELECT 1 FROM mobile_ChatResourceShareScope a INNER JOIN mobile_chatresourceshare b ON a.shareid = b.id WHERE b.resourcetype = 0 AND b.resourceid = {requestid} AND b.sharer = {sharer} AND a.resoueceid = {currentUserid} AND a.resouecetype = 0 ``` **判定条件**: 分享人是流程操作者,且当前用户在分享范围内。 **涉及表**: `workflow_currentoperator`, `mobile_chatresourceshare`, `mobile_ChatResourceShareScope` --- ### Path 13: 协作权限 → `getRequestCoworkRight()` **调用链**: `getRequestCoworkRight()` → `CoworkCommonUtils.getWfRightByCowork()` **SQL 1** — 检查协作审批状态: ```sql SELECT approvalatatus FROM cowork_items WHERE id = {coworkId} ``` **SQL 2** — 获取协作参与者: ```sql SELECT * FROM coworkshare WHERE sourceid = {coworkId} ORDER BY srcfrom DESC, type ASC ``` **SQL 3** — 获取退出协作者: ```sql SELECT userid FROM cowork_quiter WHERE itemid = {coworkId} ``` **SQL 4** — 检查协作主项关联流程(Oracle版本): ```sql SELECT relatedwf FROM cowork_items WHERE id = {coworkId} AND relatedwf IS NOT NULL AND ','||relatedwf||',' LIKE '%,{requestid},%' ``` **SQL 5** — 检查协作讨论关联流程: ```sql SELECT relatedwf FROM cowork_discuss WHERE coworkid = {coworkId} AND (isdel <> 1 OR isdel IS NULL) AND ','||relatedwf||',' LIKE '%,{requestid},%' ``` **判定条件**: 用户是协作参与者/管理者,且协作主项或讨论中关联了目标流程。 **涉及表**: `cowork_items`, `coworkshare`, `cotype_sharemanager`, `cowork_deftypeshare`, `cowork_quiter`, `cowork_discuss` --- ### Path 14: 报表权限 → `getReportRequestRight()` **子路径A: 自定义报表 (isfromreport=1)** **SQL 1** — 验证报表共享权限: ```sql SELECT shareType, userid, departmentid, subcompanyid, roleid, rolelevel, allowlook, sharelevel, mutidepartmentid, seclevel, seclevel2 FROM WorkflowReportShare WHERE reportid = {reportid} ``` **SQL 2** — 获取报表关联的流程: ```sql SELECT reportwfid FROM Workflow_Report WHERE id = {reportid} ``` **SQL 3** — 获取当前请求的流程ID: ```sql SELECT workflowid FROM workflow_requestbase WHERE requestid = '{requestid}' ``` **判定条件**: 用户有报表查看权限(`allowlook='1'`),且当前请求的 workflowid 在报表的 reportwfid 列表中。 **子路径B: 新版报表 (isfromreport=2)** **SQL 4** — 获取表单信息: ```sql SELECT formid, isbill FROM workflow_base a LEFT JOIN workflow_requestbase b ON a.id = b.workflowid WHERE b.requestid = {requestid} ``` **SQL 5** — 获取报表数据权限配置: ```sql SELECT type, objid, allowsub, minlevel, maxlevel, rolelevel, joblevel, joblevelobjid, competencelevel, competenceobjid, dimension, dimensionval, allowlook FROM workflow_report_competence WHERE formid = {formid} AND isbill = {isbill} AND allowlook = '1' ``` **判定条件**: 按 type 匹配用户(1=人员, 2=部门, 3=分部, 4=角色, 5=所有人, 6=岗位),安全级别在范围内。 **涉及表**: `WorkflowReportShare`, `Workflow_Report`, `workflow_requestbase`, `workflow_base`, `workflow_report_competence`, `HrmresourceVirtual` --- ## 涉及的核心数据库表汇总 | 表名 | 用途 | 涉及路径 | |------|------|---------| | `workflow_requestbase` | 流程请求基础表 | 1,3,4,6,8,10,14 | | `workflow_currentoperator` | 流程当前操作者表 | 2,10,12 | | `workflow_base` | 流程定义表 | 1,14 | | `Workflow_SharedScope` | 流程共享范围表 | 1 | | `workflow_monitor_info` | 流程监控配置表 | 4,6 | | `workflow_monitor_detail` | 流程监控权限明细表 | 4,6 | | `WORKFLOW_SUPERVISEOPERATOR` | 督办操作者表 | 5 | | `workflow_subwfrequest` | 主子流程关系表 | 8 | | `workflow_requestlog` | 流程操作日志表 | 10 | | `workflow_billfield` | 表单字段定义表 | 9,10 | | `workflow_bill` | 表单定义表 | 10 | | `moderightinfo` | 建模权限规则表 | 9 | | `modeDataShare_{modeId}` | 建模数据共享表(动态) | 9 | | `ModeFieldAuthorize` | 字段授权配置表 | 9 | | `workflow_communicationcontent` | 流程交流内容表 | 7 | | `workflow_communicationreply` | 流程交流回复表 | 7 | | `workplan` | 日程表 | 11 | | `WorkPlanShareDetail` | 日程共享明细表 | 11 | | `meeting` | 会议表 | 11 | | `Meeting_ShareDetail` | 会议共享明细表 | 11 | | `mobile_chatresourceshare` | IM分享记录表 | 12 | | `mobile_ChatResourceShareScope` | IM分享范围表 | 12 | | `cowork_items` | 协作主项表 | 13 | | `coworkshare` | 协作共享表 | 13 | | `cowork_discuss` | 协作讨论表 | 13 | | `WorkflowReportShare` | 报表共享权限表 | 14 | | `Workflow_Report` | 报表定义表 | 14 | | `workflow_report_competence` | 报表数据权限表 | 14 | | `hrmresource` | 人力资源表 | 4,6 | | `hrmroles` | 角色表 | 4,6 | | `hrmresourcemanager` | 系统管理员表 | 4,6 | | `HrmResourceVirtualView` | 虚拟组织视图 | 4,6 | ## 注意事项 1. **SPA vs JSP路由差异**: `authStr` 仅在SPA路由中用于权限校验,JSP路由使用Session机制 2. **防篡改校验**: 相关流程权限会校验A/B流程是否有共同操作人,无交集则拒绝 3. **字段值校验**: 建模关联授权和相关流程权限都会校验表单字段值是否包含目标requestid 4. **无条件检查**: 共享权限和操作者权限是无条件检查的,不需要任何URL参数 5. **动态表名**: 建模共享表 `modeDataShare_{modeId}` 和业务表 `{tablename}` 是动态生成的 6. **数据库差异**: 部分SQL因数据库类型(Oracle/MySQL/SQL Server/PostgreSQL)而有不同写法,主要是字符串拼接方式的差异 --- # [泛微 Ecology10] Ecology10使用ESB动作流&连接器使用doCreateRequest创建流程接口 作者:丸子·张 来源:https://blog.oneszhang.com/2026/06/18/202/ Markdown:https://blog.oneszhang.com/2026/06/18/202.md 发布日期:2026-06-18T02:28:00.000Z 更新时间:2026-06-18T02:28:00.000Z 专栏:泛微 E10 标签: 摘要:这篇文章介绍在泛微 Ecology10 多租户场景下,如何借助 ESB 动作流配合开放平台连接器的 doCreateRequest 接口实现跨组织创建流程。内容涵盖连接器与接口配置、字段映射表维护、自定义函数组装传参,以及 fieldId、附件权限和文件上传等关键参数处理,适合做 E10 二开的开发人员参考,作者也说明这只是个人思路而非官方最佳实践。 > ⚠️ **写在前面** > > 本方案仅出于丸子当时自己的即时想法,并非官方最佳实践方案。仅供技术交流参考,如你有更优雅的实现方式,欢迎随时交流! > E10 多租户互通方案中,通过 E10 开放平台连接器,可以实现当前组织与 E10 其他组织的数据互通。本文将重点介绍如何使用 ESB 动作流配合 `doCreateRequest` 接口实现跨组织的流程创建。 ## 1. 准备阶段 在开始配置前,请确保已查阅官方开发文档。 :::details[接口基础信息] * **接口地址:** * `http://url:port/papi/openapi/api/workflow/core/paService/v1/doCreateRequest` * `http://url:port/papi/openapi/api/workflow/core/paService/v2/doCreateRequest` * **前置要求:** 参照 "后台管理中心" -> "开放平台" -> "接口示例",完成应用的创建与鉴权配置。 ::: ### ESB 连接器配置 1. 进入 **"ESB 中心" -> "内置" -> "E10 开放平台"** 。 2. 新建“连接”以及“接口”(注:系统默认并未内置新建流程接口,需要自行创建)。 ### 创建字段映射表 准备好源流程(触发源)与目标流程(被写入目标) 为了保证后续代码的通用性与可维护性,强烈建议维护一张映射表。 ![2026-06-18T02:26:17.png][1] ## 2. ESB 动作流构建 整个动作流的核心逻辑主要由“流程触发”与“自定义逻辑计算”组成。 ### 流程逻辑图 ![2026-06-18T02:27:18.png][2] ### 核心自定义函数 在此处使用 `解析JSON/XML文本` 节点,配置自定义函数 `buildMappingResult`: **JavaScript** ``` function buildMappingResult(flowDataStr, mappingStr, sourceKeyCol, targetKeyCol, specialTypeCol) { var flowData = JSON.parse(flowDataStr); var mapping = JSON.parse(mappingStr); var result = []; for (var i = 0; i < mapping.length; i++) { var mapItem = mapping[i]; var sourceId = mapItem[sourceKeyCol]; var targetId = mapItem[targetKeyCol]; var specialType = mapItem[specialTypeCol]; if (!sourceId || !targetId) continue; // 逻辑处理 if (flowData[sourceId] !== undefined) { var mainRow = { "targetId": targetId, "targetValue": flowData[sourceId] }; if (specialType !== undefined) mainRow["specialType"] = specialType; result.push(mainRow); } else { // 明细表处理 for (var key in flowData) { if (flowData.hasOwnProperty(key)) { var fieldData = flowData[key]; if (Object.prototype.toString.call(fieldData) === '[object Array]') { for (var j = 0; j < fieldData.length; j++) { var detailItem = fieldData[j]; if (Object.prototype.toString.call(detailItem) === '[object Object]' && detailItem[sourceId] !== undefined) { var detailRow = { "targetId": targetId, "targetValue": detailItem[sourceId] }; if (specialType !== undefined) detailRow["specialType"] = specialType; if (detailItem["dataIndex"] !== undefined) detailRow["dataIndex"] = detailItem["dataIndex"]; result.push(detailRow); } } } } } } } return JSON.stringify(result); } ``` ## 3. 创建流程实例:核心参数解析 在“创建流程实例”节点中,传参的配置是重中之重。 :::tip[关键点提示] * **fieldId 使用原则:** 如果是明细字段 ID,仅需传 `dataIndex`,无需传 `subFormId`。 * **文件上传:** 使用 `uploadType: loadUrl`,配合 `GENERATEFILEDOWNURL` 函数转换下载链接。 * **附件权限:** 若涉及附件字段,`needCopy` 参数务必设置为 `true`。 ::: ### formData 传参对照表 | **字段** | **逻辑说明** | | ------------------ | ---------------------------------------------------------------------- | | **dataDetails** | 放置【解析 JSON/XML 文本】的数据解析结果 | | **fieldId** | 依据值 (dataDetails):`targetId` | | **dataIndex** | 依据值 (dataDetails):`dataIndex` | | **content** | 依据值 (dataDetails):`targetValue` | | **dataOptions** | 特殊类型字段赋值:`IF(NOTEMPTY(specialType), targetValue, null)` | | **optionId** | 多选下拉框:`IF((specialType=1), dataOptions依据值本身, null)` | ![image.png](https://cos.oneszhang.com/usr/uploads/2026/06/2102662980.png) ### 接口报文结构参考 :::details[点击查看 JSON 结构模板] **JSON** ```json { "access_token": "", "workflowId": "", "isnextflow": "", "dataDetails": [ { "fieldId": "", "dataKey": "", "dataIndex": "", "content": "", "subFormId": "", "matchByName": "", "dataOptions": [ { "optionId": "", "fileByte": "", "content": "", "uploadParam": { "uploadType": "", "base64Url": "", "loadUrl": "" } } ], "needCopy": "" } ], "formData": {}, "userid": "" } ``` ::: 如果有更好的方案,欢迎评论区留言 [1]: https://cos.oneszhang.com/usr/uploads/2026/06/1123761539.png [2]: https://cos.oneszhang.com/usr/uploads/2026/06/3787913743.png --- # [泛微 Ecology9] OA 附件 Base64 误杀事件排查复盘 作者:丸子·张 来源:https://blog.oneszhang.com/2026/06/09/198/ Markdown:https://blog.oneszhang.com/2026/06/09/198.md 发布日期:2026-06-09T07:02:00.000Z 更新时间:2026-06-09T07:02:00.000Z 专栏:泛微 E9 标签: 摘要:排查泛微 EC9 创建流程接口上传 Base64 附件时出现 PARAM_ERROR 的问题,最终定位为 OA 安全过滤链将请求参数中命中敏感关键词的片段替换为全角字符,污染 Base64 导致解码失败。文章记录了从校验原始 Base64、短样本对照到边界变体验证的逐步排查过程,并给出通过 securityXML 配置跳过规则的解决方案及回归建议。 ## 一、事件背景 近期在测试 OA 创建流程接口(/api/workflow/paService/doCreateRequest)时,遇到一个比较隐蔽的问题:业务侧传入的附件内容采用 `base64` 方式提交,接口返回 `PARAM_ERROR`,错误信息指向“生成附件失败”。 表面上看,这类问题通常会先怀疑以下几种方向: - 图片文件本身损坏 - `base64` 转换过程出错 - 请求体过长导致截断 - OA 附件服务解析失败 但这次问题的特殊之处在于: - 同一张图片在本地可以正常解码 - `base64` 长度、补位、MD5 都正常 - 错误并不是稳定出现在所有图片上,而是只出现在部分附件内容上 这意味着问题不太像“通用性格式错误”,更像“特定内容触发了某种中间处理逻辑”。 --- ## 二、问题现象 业务调用 OA 创建流程接口时,附件字段通过 `base64:` 前缀方式上传,接口返回类似错误: ```json { "code": "PARAM_ERROR", "errMsg": { "field_xxx": "生成附件失败,参数为:fieldid:xxxx,annexId:-3,fileValue:[...]" } } ``` 从附件服务语义上看,`annexId:-3` 的含义基本可以理解为:附件内容在进入保存逻辑后,Base64 解码失败。 但关键问题是,业务侧原始 `base64` 在本地验证完全正常。 --- ## 三、第一次排查:先排除“文件坏了” 第一步做的是最基础的校验: - 提取原始请求中的 `base64` - 校验长度是否为 4 的倍数 - 检查是否只包含合法 Base64 字符 - 本地执行解码 - 对解码后的文件做 MD5 校验 结果很明确: - 原始 `base64` 串合法 - 本地可以成功解码 - 解码后的图片与原始附件 MD5 一致 也就是说,**问题不在业务系统生成附件这一步**。 --- ## 四、第二次排查:不是所有 Base64 都失败 为了避免被“单个样本”误导,我们做了几组对照实验。 ### 1. 安全样例 传一个很短、内容简单的 `base64`,结果成功。 ### 2. 含特定片段的样例 构造一些很短但包含特定文本片段的 `base64`,例如: - `4or+` - `4and+` - `4union+` - `4select+` 结果发现,这类请求会稳定失败。 更关键的是,OA 返回报文中回显出来的附件内容,已经不是原始值,而是被改写成了全角字符,例如: - `4or+` 变成 `4or+` - `4and+` 变成 `4and+` 一旦 `base64` 内部出现全角字符,后续解码必然失败。 这一步已经把问题从“附件坏了”收缩到“请求进入 OA 后被改写了”。 --- ## 五、第三次排查:到底是什么条件会触发改写 如果只是“出现 `or` 就失败”,那很多正常图片都应该大量失败,但事实并不是这样。 于是我们继续做边界实验,只改变命中关键词左右两边的字符。 实验样例如下: - 会失败:`4or+` - 会成功:`Aor+` - 会成功:`4orA` - 会成功:`AorA` 同样地: - 会失败:`4and+` - 会成功:`AandA` 这说明触发条件不是“只要包含 `or` / `and` 就拦截”,而是更接近: > 当某些敏感关键词左右两边都不是字母时,会被安全规则命中。 这类模式与常见 SQL 注入拦截规则高度一致。 --- ## 六、第四次排查:回到真实失败图片做验证 为了避免“小样本实验不代表真实图片”的质疑,我们又对真实失败图片做了反向验证。 结论如下: 1. 原始图片 Base64 上传失败 2. 回显内容中,只有其中一处 `or` 被改写为 `or` 3. 这处命中的上下文正好是 `4or+` 然后我们只做一个极小改动,不改整张图片,只改这一处附近的边界字符: - 原始:`4or+` -> 请求失败 - 改成:`Aor+` -> 请求成功 - 改成:`4orA` -> 请求成功 这一步的意义非常大,因为它证明: - 不是图片太大导致失败 - 不是图片整体内容有问题 - 不是 Base64 长度问题 - 不是附件服务偶发异常 而是**图片 Base64 中某个刚好命中安全规则的片段,触发了请求内容改写**。 --- ## 七、最终结论 这次事件的根因可以明确归纳为: > OA 在接收 `application/x-www-form-urlencoded` 请求参数时,安全过滤链会对命中的敏感关键词进行全角替换;当附件 `base64` 恰好包含类似 `or`、`and`、`union`、`select` 等关键词,并且其左右边界满足规则时,原始 `base64` 会被污染,最终导致附件解码失败。 换句话说: - 业务系统生成的附件 Base64 没问题 - 图片本身没问题 - 问题发生在 OA 接收请求后的安全处理阶段 这是一个典型的“**安全规则误杀二进制文本内容**”问题。 --- ## 八、为什么这个问题隐蔽 这个问题很难第一时间定位,原因有三个: #### 1. 现象看起来像附件服务失败 接口只返回“生成附件失败”,很容易让人先怀疑附件服务或图片本身。 #### 2. 不是所有附件都失败 只有当 Base64 内部恰好出现特定关键词组合时才触发,因此问题呈现出明显的“偶发性”。 #### 3. 原始请求侧看不到污染 业务侧生成的 Base64 完全正常,真正的问题发生在请求进入 OA 后的处理中间层。 --- ## 九、排查经验总结 这次事件里,最有效的不是一开始盯着大日志翻,而是做了几轮最小化实验: - 先验证原始 Base64 是否真的合法 - 再用短样本确认问题是否与特定内容有关 - 再通过边界变体判断触发规则 - 最后回到真实图片做反向验证 这套方法的价值在于: - 能快速排除“文件坏了”这类低价值方向 - 能把问题从“偶发失败”收敛成“稳定可复现” - 能把结论从“怀疑安全拦截”推进到“确认是关键词边界命中” --- ## 十、后续建议 如果后续要彻底解决,建议从以下几个方向评估: #### 1. 调整 OA 对附件类字段的安全过滤策略 对于明确承载 `base64` 附件内容的字段,应考虑: - 增加字段级白名单 - 增加接口级豁免 - 在安全过滤前识别并跳过合法 Base64 内容 #### 2. 优化附件上传方式 如果接口支持,优先考虑: - 先上传附件,再传附件 ID - 使用 `multipart/form-data` - 避免把大段二进制文本直接放进普通表单字段 #### 3. 建立关键故障样本库 像这次命中的 `4or+`、`4and+` 这种片段,建议沉淀成回归样本,避免后续升级或策略调整时再次踩坑。 --- ## 十一、结语 这次故障表面上是“附件 Base64 失败”,本质上却是“安全规则误杀了 Base64 文本内容”。 它提醒我们一件事: > 当接口处理的不是普通业务文本,而是承载二进制内容的编码字符串时,安全策略如果仍按普通文本规则处理,就很容易把合法数据当成攻击特征误拦。 很多“看起来像解析失败”的问题,最后都不是解析器本身有问题,而是数据在到达解析器之前,已经被别的环节悄悄改写了。 ## 问题解决,解决方案如下(2026年6月9日11:30): **问题:** OA 流程接口 Base64 附件被安全规则误改写,附件生成失败 **解决方案:** 将附件weaver_security_custom_rules_skip_for_workflow_base64.xml放到/ecology/WEB-INF/securityXML/下,用sysadmin登录,访问下oa地址+/updateRules.jsp 访问成功之后显示complete之后可以关闭此页面,然后等三分钟左右测试是否正常。(如果是集群环境,需要复制到每个服务器,然后每个节点都访问下该地址) weaver_security_custom_rules_skip_for_workflow_base64.xml内容如下: ``` /api/workflow/paService/doCreateRequest /api/workflow/paService/submitRequest /api/workflow/paService/doCreateRequest /api/workflow/paService/submitRequest ``` --- # [泛微 Ecology9] 丸子喂饭篇: restful 新建流程接口详解(/api/workflow/paService/doCreateRequest) 作者:丸子·张 来源:https://blog.oneszhang.com/2026/05/08/197/ Markdown:https://blog.oneszhang.com/2026/05/08/197.md 发布日期:2026-05-08T12:43:00.000Z 更新时间:2026-05-08T12:43:00.000Z 专栏:泛微 E9 标签: 摘要:泛微E9中用于自动新建流程实例的 REST 接口 doCreateRequest,文章从请求头鉴权、请求方式与编码格式,到主表与明细表数据组装、返回值判断和常见报错排查做了完整拆解。适合做系统集成、数据迁移或定时发起流程的开发者参考,重点在于字段名必须与数据库字段名一致,并注意请求格式与用户创建权限,否则容易报参数或权限错误。 > 📌 > > 该文章由丸子引导 AI 辅助制作生成,如有改进意见,欢迎评论区留言。 > ### 引言 这个接口可以说是泛微 E9 里用得最多的 REST 接口之一了——新建流程实例。 不管是做系统集成、数据迁移、还是定时任务自动发起流程,都绕不开它。但官方文档嘛……懂的都懂,参数一堆,示例不够,踩坑不少。 今天丸子就把这个接口掰开了揉碎了讲一遍,争取让你看完就能用。 ### 一眼看明白:直接上 curl > 😄 如果你对泛微接口已经有一定了解,直接看这段就够了。当然,下面有详细解释,需要的话可以继续往下翻~。 ```bash curl -X POST 'https://your-oa-domain.com/api/workflow/paService/doCreateRequest' \ -H 'Content-Type: application/x-www-form-urlencoded' \ -H 'token: your_token_here' \ -H 'appid: your_appid_here' \ -H 'userid: your_encrypted_userid_here' \ -d 'workflowId=388024' \ -d 'requestName=测试流程标题' \ -d 'remark=测试意见' \ -d 'requestLevel=2' \ -d 'mainData=[{"fieldName":"cswb","fieldValue":"2978"}]' \ -d 'detailData=[{"tableDBName":"formtable_main_1356_dt1","workflowRequestTableRecords":[{"recordOrder":0,"workflowRequestTableFields":[{"fieldName":"dhwb","fieldValue":"第一行"},{"fieldName":"drl","fieldValue":"2978"}]}]}]' \ -d 'otherParams={"isnextflow":0,"delReqFlowFaild":0}' ``` #### 返回成功示例: ```json { "code": "SUCCESS", "data": { "requestid": 1699718 }, "errMsg": {}, "reqFailMsg": { "keyParameters": {}, "msgInfo": {}, "otherParams": {} } } ``` 拿到 `requestid` 就说明流程创建成功了,这个 ID 就是流程实例的唯一标识。 ### 小白专区:一步一步来 > 如果上面的 curl 你看得一头雾水,别慌,丸子下面一个一个讲。 #### 第一步:这个接口是干嘛的? 简单说:通过代码自动新建一条泛微流程。 平时你在 OA 系统里点"新建流程",选流程、填表单、点提交——这个接口就是把这个过程用 HTTP 请求来完成。 #### 第二步:请求地址和方式 ``` POST https://你的 OA 地址/api/workflow/paService/doCreateRequest ``` - 请求方式:POST(不是 GET!) - Content-Type:必须是 `application/x-www-form-urlencoded`(不是 JSON!) > 💊 这里是个常见坑。很多人习惯用 JSON 格式发请求,但这个接口要求的是表单格式。 #### 第三步:请求头(Header)参数 | 参数名 | 说明 | 怎么获取 | |--------|------|----------| | token | 调用凭证 | 通过泛微许可接口获取 | | appid | 许可证号码 | 泛微后台配置 | | userid | 加密后的用户 ID | 用 spk 公钥加密 userid 得到 | | Content-Type | 内容类型 | 固定填 `application/x-www-form-urlencoded` | 这三个参数(`token`、`appid`、`userid`)是泛微 REST 接口的"通行证",没有它们接口直接拒绝。 #### 第四步:请求体(Body)参数详解 这是重点,一共就这几个参数: 1. **workflowId(必填)**—— 流程 ID ```bash -d 'workflowId=388024' ``` 这个数字是你要新建的流程模板 ID。怎么找?在 OA 系统里打开流程设计器,URL 里一般就有,或者查数据库 `workflow_base` 表。 2. **requestName(必填)**—— 流程标题 ```bash -d 'requestName=测试流程标题' ``` 就是流程发起后显示的标题,随便填。 3. **mainData(必填)**—— 主表数据 这个稍微复杂点,它是一个 JSON 数组,每个元素代表一个字段: ```json [ { "fieldName": "cswb", "fieldValue": "2978" }, { "fieldName": "csfjsc", "fieldValue": [ { "fileName": "通用测试模板.xls", "filePath": "base64:AAA..." }, { "fileName": "通用测试 pdf.pdf", "filePath": "https://example.com/sample.pdf" } ] } ] ``` - `fieldName`:字段的数据库名称(不是显示名称!)。去表单设计器里看,或者查 `workflow_formfield` 表。 - `fieldValue`:字段值。普通字段直接填字符串,附件字段比较特殊,需要传数组,每个附件包含 `fileName`(文件名带后缀)和 `filePath`(base64 编码或 URL)。 :::tip 怎么找字段的数据库名称? 进入表单设计器 → 点击字段 → 查看属性 → 有个"数据库字段名"就是了。 ::: 4. **detailData(可选)**—— 明细表数据 如果你的流程有明细表,就用这个参数: ```json [ { "tableDBName": "formtable_main_1356_dt1", "workflowRequestTableRecords": [ { "recordOrder": 0, "workflowRequestTableFields": [ { "fieldName": "dhwb", "fieldValue": "第一行" }, { "fieldName": "drl", "fieldValue": "2978" } ] }, { "recordOrder": 0, "workflowRequestTableFields": [ { "fieldName": "dhwb", "fieldValue": "第 2 行" }, { "fieldName": "drl", "fieldValue": "2978" } ] } ] } ] ``` - `tableDBName`:明细表的数据库名,格式一般是 `formtable_main_xxx_dt1`。 - `workflowRequestTableRecords`:每一行明细数据。 - `recordOrder`:行序号,从 0 开始。 - `workflowRequestTableFields`:和主表一样,`fieldName + fieldValue`。 :::tip 多个明细表怎么办? 在数组里加另一个 `tableDBName` 的对象就行。 ::: 5. **remark(可选)**—— 签字意见 ```bash -d 'remark=测试意见' ``` 流程发起时的签字意见,不填就用流程默认的。 6. **requestLevel(可选)**—— 紧急程度 ```bash -d 'requestLevel=2' ``` 数字越大越紧急,具体取值看你们流程的配置。 7. **otherParams(可选)**—— 扩展参数 ```bash -d 'otherParams={"isnextflow":0,"delReqFlowFaild":0}' ``` 这是一个 JSON 对象,支持以下参数: | 参数名 | 说明 | 可选值 | |--------|------|--------| | isnextflow | 新建后是否自动流转到下一节点 | 0:不流转,1:流转(默认) | | delReqFlowFaild | 新建失败是否自动删除流程 | 0:不删除,1:删除(默认) | | isVerifyPer | 是否验证用户创建权限 | 0:不验证,1:验证(默认) | | requestSecLevel | 流程密级 | 需开启密级功能才生效 | :::tip `isnextflow=0` 什么意思? 就是流程创建后停留在第一个节点,不自动往下走。有时候你还需要在第一个节点做点操作,就可以设成 0。 ::: #### 第五步:返回值怎么判断? ```json { "code": "SUCCESS", "data": { "requestid": 1699718 } } ``` - `code` 为 `SUCCESS` → 成功,`data.requestid` 就是新建的流程 ID。 - `code` 为 `PARAM_ERROR` → 参数有错,看 `errMsg` 里的具体提示,当然,一般没有,慢慢调吧~ - `code` 为 `NO_PERMISSION` → 没有权限,检查 `workflowId` 对应的流程是否允许当前用户创建。 - `code` 为 `SYSTEM_INNER_ERROR` → 服务端异常,看 OA 日志。 ### 常见错误排查 | 错误现象 | 可能原因 | |----------|----------| | workflowId 返回 -1 | mainData 格式不对,多了 } 或 ] | | mainData: 新建流程主表数据不允许为空 | mainData 没传或者为空 | | error_param_xxx: '值' | 某个字段名写错了,或者值不合法 | | 返回 NO_PERMISSION | 当前用户没有该流程的创建权限 | | 返回 PARAM_ERROR 但 errMsg 为空 | 检查 Content-Type 是不是 `application/x-www-form-urlencoded` | ### 小结 这个接口其实不难,核心就三个东西: 1. 请求头:`token`、`appid`、`userid`(泛微接口三件套) 2. 主表数据:`mainData`,字段名一定要和表单里的数据库字段名一致 3. 明细表数据:`detailData`,结构比主表多一层"表名"和"行记录" 记住一个原则:字段名写错 = 报错,格式不对 = 报错,权限没有 = 报错。 好了,丸子喂饭完毕,希望你能吃饱~ 有问题欢迎评论区留言~ --- ### 参考资料与致谢 泛微官方文档: 泛微E9后端接口说明(https://e-cloudstore.com/ec/api/applist/index.html#/) --- # [泛微 Ecology10] 国际化与日期字段 displayFormat 冲突排查实录 作者:丸子·张 来源:https://blog.oneszhang.com/2026/03/03/195/ Markdown:https://blog.oneszhang.com/2026/03/03/195.md 发布日期:2026-03-03T14:54:00.000Z 更新时间:2026-03-03T14:54:00.000Z 专栏:泛微 E10 标签: 摘要:泛微 Ecology10 中同一日期字段出现两种显示格式,排查确认并非数据源或字段配置问题,而是全局国际化日期与字段 displayFormat 在前端渲染时发生双重格式化:预格式化后的字符串二次解析失败便回退原值,导致格式不一甚至月日互换,ecode 适配也因输出路径不稳定而失效。文章给出抓 DOM、API 与运行时函数的排查思路,并指出应统一日期输入语义与格式化入口。 同一日期字段为什么会显示两种格式?泛微国际化与 displayFormat 冲突排查实录(Ecology10 v10.0.2512.01) 这次踩坑很典型。 同一个流程、同一个节点、同一个日期字段,一个请求显示 `March 3, 2026`,另一个却是 `17-02-2026`。 结论先说:不是数据源问题,是“全局国际化日期”与“字段 displayFormat”在前端渲染链路冲突了。 如果你也在做泛微国际化,尤其还叠加了 ecode,这篇基本可以直接对照排查。 > ✅ **你可以直接拿走的结论** > > 前端先按全局规则预格式化日期,再按字段 displayFormat 二次格式化;二次解析对部分值失败后会回退原值,导致同一日期字段出现两种显示格式,甚至月日互换。 > ## 环境信息 - 产品基线版本:`10.0.2512.01` - 全局设置:范围国际化日期开启 `MM-DD-YYYY` - 字段设置:日期字段开启显示格式 `MMMM D, YYYY` - 自定义适配:已用 `ecode` 适配 `MMM`(英文简写月) - 对比请求: - `1240131189385469953`,显示 `March 3, 2026` - `1234471942773547020`,显示 `17-02-2026` ## 问题现象 同一个日期字段,显示结果不一致: 1. 一条请求是英文月份格式:`March 3, 2026` 2. 另一条请求是数字格式:`17-02-2026` 第一眼很像“后端给了不同格式的数据”。 但这次不是。 ## 排查过程 ### 1) 先看 DOM:是不是同一个组件在渲染 两个页面都是同一个 widget: - `#widget_1083772471125450753` - 值都在 `.weapp-form__display-format-view > span` 说明不是“一个是日期组件、一个是文本组件”的差异。 ### 2) 再看 API:字段配置是否漂移 核心接口: ```http POST /api/ebuilder/flow/form/core/getFormDataAndLayout?formId=1134626576188694691&dataId=... ``` 两条请求里,日期字段配置一致: ```json { "fieldId": "1083772471125450753", "dataKey": "letter_date", "columnName": "letter_date", "format": "yyyy-MM-dd", "displayFormatConfig": { "formatter": { "type": "date", "setting": { "type": "MMMM D, YYYY" } } } } ``` 只有原始值不同: - `124013...` -> `content = 2026-03-03` - `123447...` -> `content = 2026-02-17` 所以可以排除“数据源不一致”和“字段配置不一致”。 ### 3) 最后看 JS:前端到底怎么格式化 前端关键链路(`weappFormbuilder` 运行时模块): ```js // 简化逻辑 h = formatByConfig(rawValue, config.format) // 先预格式化 y = Convert.parse(h, displayFormat) // 再二次格式化 ``` `Convert.parseDate` 内部是 `dayjs(e).format(...)`。 如果 `e` 解析失败,就会原样回退 `e`。 > 🚨 **核心冲突点** > > 全局国际化先把值改造成了某种字符串形态,字段 displayFormat 再次解析时并不总是成功。 > 成功了就显示英文月名,失败了就回退原串。 > ## 为什么一个能转,另一个不能转? 直观看两个值: 1. `2026-03-03` 预处理后是 `03-03-2026`,前端还能“猜”成有效日期,最后渲染出 `March 3, 2026`。也就是说: 2. `2026-02-17` 预处理后是 `17-02-2026`,前端解析失败(`17` 不能当月份),于是回退成 `17-02-2026`。 这就是“同一日期字段,两种格式”的直接原因。 ## 这事不只是显示不统一,还可能显示错误日期 模拟结果里还能看到月日互换风险: - `2026-12-01`(12月1日)会被转换为 `January 12, 2026`(1月12日) 这已经不是样式问题,是语义错误。 ## 为什么这次 ecode 没兜住 针对泛微当前版本的国际化日期转换只能支持`MM`、`DD`固定的组合方式,我们用 ecode 适配了`MMM`、`MMMM`英文月份格式, ecode 适配本身没问题。 问题在于这条链路里,前端在某些值上先“退化成普通字符串输出”,导致后续不再稳定走日期组件的转换路径。 所以表现就是:你写了转换,但它没机会生效。 > ⚠️ > > 关键点:宿主前端在“国际化 + 字段格式”叠加时,输出路径不稳定。 > ## 总结 这次问题本质上是格式链路冲突,不是数据源问题。 全局国际化、字段 displayFormat、ecode 三者叠加时,`10.0.2512.01` 这条链路确实有适配缺口。 真正的修复方向不是继续补字符串转换,而是统一日期输入语义和格式化入口。 一句话收尾: 日期问题出现“双重格式化”时,别猜,直接抓 DOM + API + 运行时函数,效率最高。 --- # [泛微 Ecology10] "对不起,没有使用权限" 解决办法 作者:丸子·张 来源:https://blog.oneszhang.com/2026/02/12/weaver-e10-permission-fix/ Markdown:https://blog.oneszhang.com/2026/02/12/weaver-e10-permission-fix.md 发布日期:2026-02-12T08:41:00.000Z 更新时间:2026-02-12T08:41:00.000Z 专栏:泛微 E10 标签: 摘要:泛微 E10 升级到基线 10.0.2512.01 后,部分人员虽已获得角色或群组权限,访问资源时仍提示没有使用权限,原因是权限缓存未同步刷新。文章给出临时方案:调用权限缓存刷新接口,传入租户 ID 和人员 ID 即可立即刷新该人员权限。作者提醒这只是临时处理,彻底解决需联系总部客服与技术排查,二次开发导致的问题可尝试调用刷新权限接口。 ## 问题描述 在更新到基线 **10.0.2512.01** 后,出现了角色权限缓存问题: - 应用或菜单已经给了角色或群组权限 - 人员也已经加入到对应的角色或群组 - 但访问资源时仍然提示:**"对不起,没有使用权限"** ## 原因分析 这是由于权限缓存未及时更新导致的。系统虽然已经分配了权限,但缓存中的权限信息没有同步刷新。 > ✅ **彻底解决问题,建议客服及技术协调处理** > > 注意!当前文章中的方案仅是临时处理方案,如果想要彻底解决该问题,需要联系总部客服以及技术协调处理排查处理~(当然,如果是由于二次开发导致的问题,可以尝试调用刷新权限接口,希望本文能给你一些启发) > ## 解决办法 直接访问以下 API 接口来刷新权限缓存: ``` /api/app/basicserver/data/permission/cache/sourceIdCache/reloadEmployee?tenantKey=xxxx&employeeId=xxxx ``` ### 参数说明 - `tenantKey`:租户 ID - `employeeId`:权限存在问题的人员 ID ### 示例 假设租户 ID 为 `txzzkwa3op`,问题人员 ID 为 `1134278335853099999`,则访问: ``` /api/app/basicserver/data/permission/cache/sourceIdCache/reloadEmployee?tenantKey=txzzkwa3op&employeeId=1134278335853099999 ``` 访问后,该人员的权限缓存会立即刷新,问题即可解决。 --- **更新时间**:2026-02-12 --- # 博客新纪元:我的 AI 写手上线了 作者:丸子·张 来源:https://blog.oneszhang.com/2026/02/12/ai-writing-era/ Markdown:https://blog.oneszhang.com/2026/02/12/ai-writing-era.md 发布日期:2026-02-12T02:41:00.000Z 更新时间:2026-02-12T02:41:00.000Z 专栏:生活与随笔 标签: 摘要:作者宣布博客进入 AI 辅助创作阶段:借助 OpenClaw 与 Claude,从口述想法、生成 Markdown 草稿到通过 Typecho 的 XML-RPC 接口自动发布,全流程耗时大幅缩短。文章强调 AI 只是工具,内容、观点和经验仍由本人把关,并介绍了背后的技术栈、工作流程以及提升更新频率的未来计划。 今天是个值得纪念的日子——我的博客正式进入"AI 辅助创作"时代。 ## 发生了什么? 简单来说,我给自己配了个 AI 助手(OpenClaw + Claude),现在它可以: - 帮我写博客文章 - 自动发布到网站 - 支持 Markdown 和各种短代码 - 24/7 随时待命 听起来很酷对吧?但别担心,**我还是那个丸子**。 ## 为什么要这么做? :::details[效率] 说实话,写博客最大的障碍不是没东西写,而是**懒得动手**。 有了 AI 助手,我可以: - 口述想法,它帮我整理成文 - 提供关键点,它帮我扩展细节 - 遇到技术问题,它帮我记录解决过程 从想法到发布,时间缩短了 80%。 ::: :::details[质量] AI 不会累,不会烦,不会"今天不想写"。 它可以: - 保持统一的写作风格 - 自动添加代码高亮和格式化 - 检查错别字和语法问题 - 确保文章结构清晰 当然,**内容还是我把关**。AI 只是工具,不是替代品。 ::: :::details[乐趣] 最重要的是,**这很好玩**。 看着 AI 理解我的想法,然后用我的风格写出来,这种感觉就像: > "哇,这就是我想说的,但它说得比我更清楚!" 而且,我可以把更多时间花在思考和实践上,而不是纠结怎么措辞。 ::: ## AI 会取代我吗? **不会。** AI 是工具,不是作者。它可以帮我: - 整理思路 - 优化表达 - 提高效率 但它不能: - 替我思考 - 替我踩坑 - 替我积累经验 **这个博客的灵魂还是我的**,AI 只是帮我更好地表达。 ## 你会看出区别吗? 可能会,也可能不会。 AI 辅助的文章会: - 结构更清晰 - 格式更统一 - 更新更频繁 但内容、观点、经验,都是我的。 如果你觉得某篇文章"不像我写的",欢迎留言告诉我。我会调整 AI 的风格,让它更像"丸子"。 ## 技术细节 既然是技术博客,简单说说实现: :::details[点击展开技术栈] **核心组件**: - **OpenClaw**:开源的 AI Agent 框架 - **Claude**:Anthropic 的 AI 模型 - **Typecho**:博客系统 - **XML-RPC**:自动发布接口 **工作流程**: 1. 我告诉 AI 想写什么 2. AI 生成 Markdown 草稿 3. 我审阅并修改 4. AI 自动发布到博客 **代码**: ```python # 简化版发布流程 result = publish_to_typecho( title="文章标题", content="Markdown 内容", category="分类", slug="url-别名" ) ``` 整个系统跑在我的服务器上,完全自主可控。 ::: ## 未来计划 有了 AI 助手,我打算: - **更新频率提升**:从"想起来就写"到"每周至少 2 篇" - **内容更丰富**:不只是技术文章,还有想法、经验、踩坑记录 - **互动更多**:AI 可以帮我整理评论、回复问题 当然,这些都需要时间验证。 ## 写在最后 这篇文章本身就是 AI 辅助完成的。 我提供了想法和结构,AI 帮我扩展和润色。你觉得怎么样? 如果你也在用 AI 辅助创作,或者对这个话题感兴趣,欢迎留言交流! > ✅ > > **彩蛋**:这篇文章从构思到发布,总共用了不到 5 分钟。效率拉满! > --- **相关阅读**: - [OpenClaw 配置 AI 模型:5 分钟搞定 User-Agent 问题](/archives/openclaw-user-agent-fix.html) - OpenClaw 官网:https://openclaw.ai 欢迎来到 AI 辅助创作的新时代! --- # OpenClaw 配置 AI 模型:5 分钟搞定 User-Agent 问题 作者:丸子·张 来源:https://blog.oneszhang.com/2026/02/12/openclaw-user-agent-fix/ Markdown:https://blog.oneszhang.com/2026/02/12/openclaw-user-agent-fix.md 发布日期:2026-02-12T02:31:00.000Z 更新时间:2026-02-12T02:31:00.000Z 专栏:生活与随笔 标签: 摘要:给 OpenClaw 配置自定义 AI 模型(NEW API)时出现 403 报错,原因是代理服务器把请求识别为爬虫并拦截。文章给出的解决办法是在 provider 配置中添加自定义 User-Agent 请求头,可通过命令行写入 JSON 或直接编辑 ~/.openclaw/openclaw.json,之后重启网关并用测试命令验证。适合遇到同类调用失败的 OpenClaw 用户参考,同时提醒排查配置未生效与换用其他 User-Agent 等情况。 最近在给 OpenClaw 配置自定义 AI 模型时,遇到了一个 403 错误。折腾了一会儿才发现是 User-Agent 的问题。这篇文章记录一下解决过程,希望能帮到遇到同样问题的朋友。 ## 问题现象 配置好自定义 AI 模型(NEW API)后,测试时一直报错: ``` 403 Forbidden - Your request was blocked ``` 模型配置看起来没问题,API 端点也能访问,但就是无法正常调用。 ## 问题原因 原来是代理服务器把 OpenClaw 的请求识别成了爬虫,直接拦截了。 很多 AI 模型的代理服务为了防止滥用,会检查请求的 User-Agent。如果 User-Agent 看起来不像正常的 API 客户端,就会被拦截。 ## 解决方案 给模型配置添加自定义 User-Agent 就行了。 ### 方法一:命令行配置(推荐) ```bash openclaw config set models.providers.wong.headers '{"User-Agent": "OpenClaw-Agent/1.0 (compatible; Anthropic-API-Client)"}' --json ``` :::tip[注意] - `--json` 参数很重要,告诉 OpenClaw 这是 JSON 格式 - User-Agent 可以自定义,但最好看起来像个正经的 API 客户端 ::: ### 方法二:直接编辑配置文件 如果你喜欢手动编辑,也可以直接改配置文件: ```bash vim ~/.openclaw/openclaw.json ``` 找到对应的 provider,添加 `headers` 字段: ```json { "models": { "providers": { "wong": { "endpoint": "https://your-api-url", "apiType": "anthropic-messages", "headers": { "User-Agent": "OpenClaw-Agent/1.0 (compatible; Anthropic-API-Client)" } } } } } ``` ### 重启网关 配置修改后,记得重启 OpenClaw 网关: ```bash openclaw gateway restart ``` 或者直接: ```bash kill -HUP $(pgrep openclaw-gateway) ``` ## 验证结果 重启后,再测试一下模型调用,应该就正常了。 你可以用 OpenClaw 的测试命令: ```bash openclaw test model wong/claude-haiku-4-5-20251001 ``` 或者直接在对话里试试,看看模型是否能正常响应。 ## 其他可能遇到的问题 ### 1. 配置没生效? 确认一下: - 配置文件格式是否正确(JSON 语法) - 是否重启了网关 - 检查日志:`openclaw gateway logs` ### 2. 还是 403? 可能需要调整 User-Agent 的内容。试试这些: ``` Mozilla/5.0 (compatible; OpenClaw/1.0) OpenClaw-Client/1.0 Anthropic-API-Client/1.0 ``` ### 3. 其他错误码? - **401**:API Key 错误 - **429**:请求太频繁,被限流了 - **500**:服务器问题,等等再试 ## 写在最后 OpenClaw 的配置系统很灵活,支持给每个 provider 单独设置 headers。这次遇到的 User-Agent 问题只是一个小例子。 如果你也在用 OpenClaw 配置自定义模型,遇到类似的问题,不妨试试添加自定义 headers。 > ✅ > > **小贴士**:OpenClaw 的配置文件在 `~/.openclaw/openclaw.json`,建议定期备份。 > --- **相关链接**: - OpenClaw 官网:https://openclaw.ai - OpenClaw GitHub:https://github.com/openclaw/openclaw - 文档:https://docs.openclaw.ai 有问题欢迎留言交流! --- # 测试文章 - OpenClaw 自动发布 作者:丸子·张 来源:https://blog.oneszhang.com/2026/02/12/openclaw-auto-publish-test/ Markdown:https://blog.oneszhang.com/2026/02/12/openclaw-auto-publish-test.md 发布日期:2026-02-12T02:28:00.000Z 更新时间:2026-02-12T02:28:00.000Z 专栏:生活与随笔 标签: 摘要:这篇测试文章由 OpenClaw AI 助手自动发布,用于验证自动发布功能是否已经可用。文中确认已支持 Markdown 格式与 Mirages 短代码,并含一个提示框示例,标注的测试时间为 2026 年 2 月 12 日。文章说明,能看到该内容即代表自动发布流程运行正常。 这是一篇由 OpenClaw AI 助手自动发布的测试文章。 ## 测试内容 - 支持 Markdown 格式 - 支持 Mirages 短代码 :::tip 这是一个测试提示框 ::: 如果你看到这篇文章,说明自动发布功能已经正常工作了! **测试时间**:2026-02-12 02:28 --- # [泛微 Ecology10] 自定义一个可控的 Loading Message 作者:丸子·张 来源:https://blog.oneszhang.com/2025/12/11/189/ Markdown:https://blog.oneszhang.com/2025/12/11/189.md 发布日期:2025-12-11T05:05:00.000Z 更新时间:2025-12-11T05:05:00.000Z 专栏:泛微 E10 标签: 摘要:泛微 Ecology10 的 UI 层改用 weappUi 后,只保留了全屏遮罩式的 Spin,缺少 E9 中 antd.message.loading 那种轻量可控的加载提示。文章利用 weappUi.Dialog.message 搭配 Spin 封装了一个伪 message.loading,返回可手动销毁的实例,避免全屏遮罩阻断操作,适合接口查询、短轮询等轻量异步场景,并指出 E10 虽移除了部分顺手的组件,却给前端扩展留下更多空间。 Ecology E9 时代,`antd.message.loading` 是很多前端人的心头好——轻量、可控、非遮罩式、还能手动关闭。 但到了 **Ecology 10** ,UI 层全面转向 `weappUi`,只保留了 `Spin` 的**全局遮罩**样式。 结果就是:**异步任务的 Loading 不够友好,也缺乏 message 级别的可控 loading 提示。** 丸子用最简单的方式,实现了一个「伪 message.loading」效果: **轻量、可关闭、不干扰页面交互。** ## 效果展示: ![20251211130731_rec_.gif](https://cos.oneszhang.com/usr/uploads/2025/12/2741253580.gif) --- ## 方案:自己封装一个 message-like Loading 核心思路: 利用 `weappUi.Dialog.message` + `weappUi.Spin` 拼一个轻量提示,再返回可销毁实例。 代码极简两行: ```js window.showLoading = () => weappUi.Dialog.message({ type: 'custom', delay: 0, icon: React.createElement(weappUi.Spin, { spinning: true, size: 'small' }), content: '加载中...' }); window.hideLoading = (inst) => inst?.destroy(); ``` 使用方式也与 antd 非常相似: ```js const loadingInst = window.showLoading(); // 模拟异步任务 setTimeout(() => { window.hideLoading(loadingInst); }, 1500); ``` Minimal. Elegant. 有手就行。 --- ## 为什么不用全局遮罩? `原因很现实:Ecology10 的全局遮罩太“重”了` * 遮住整个屏幕 → 用户无法操作 * 对轻任务(接口查询、短轮询)不友好 * 经常误导用户以为系统卡死 * 影响复杂页面的交互体验 很多时候,我们只是想来个“轻提示”,而不是把世界暂停。 --- ## 自定义 Loading 的优点 :::details[更轻量] 不阻断用户操作,比全屏 Spin 更自然。 ::: :::details[可控性强] 返回实例 → 我们想关就关,不用等待延时关闭。 ::: :::details[视觉一致] 照样用 weappUi 的风格,不突兀。 ::: :::details[代码极简] 只两行代码,随处可复用。 ::: ## 写在最后 Ecology10 虽然去掉了很多 E9 时代顺手的 Web 组件,但也给了前端更多自由度。 轻量级 Loading 只是其中一个小例子。 如果你也在 E10 做前端扩展,欢迎来丸子的博客看看 --- # 《别拿你的工作碰瓷我的热爱》 作者:丸子·张 来源:https://blog.oneszhang.com/2025/11/14/188/ Markdown:https://blog.oneszhang.com/2025/11/14/188.md 发布日期:2025-11-14T04:04:23.000Z 更新时间:2025-11-14T04:04:23.000Z 专栏:生活与随笔 标签: 摘要:这是一篇表明个人立场的随笔,围绕工作与热爱的边界展开,反对拿职业身份当作评判标准,去碰瓷、贬低他人的兴趣与热爱。作者认为工作与热爱本属两回事,不应混为一谈,更不该以工作之名绑架或消解别人的热情,适合对职场与个人兴趣关系有共鸣的读者一读。 · --- # [泛微 Ecology10] 浏览按钮搜索按钮样式处理(真按钮) 作者:丸子·张 来源:https://blog.oneszhang.com/2025/11/14/187/ Markdown:https://blog.oneszhang.com/2025/11/14/187.md 发布日期:2025-11-14T00:57:00.000Z 更新时间:2025-11-14T00:57:00.000Z 专栏:泛微 E10 标签: 摘要:泛微 Ecology10 中可通过自定义 CSS 将浏览按钮的搜索图标改造成真按钮,包括调整输入框最小宽度、设置蓝色背景与圆角、用伪元素补充按钮文字并增加悬停效果。使用时需按实际编辑提示语和字段 ID 替换对应选择器,适合需要美化搜索入口的二次开发场景。 # 效果对比: | 修改前 | 修改后 | | --- | --- | | ![image.png](https://cos.oneszhang.com/usr/uploads/2025/11/3782429001.png) | ![image.png](https://cos.oneszhang.com/usr/uploads/2025/11/1325379039.png) | # CSS代码 ``` /* 修改输入框长度 */ input[placeholder="Search Quickly"] { min-width: 100px !important; } /* 基础按钮样式 */ div.field1190285555456860160span .associative-search-icon { background-color: #5d9cec; border-radius: 4px; cursor: pointer; padding: 6px 12px; display: inline-flex; align-items: center; gap: 6px; min-width: 120px; max-height: 24px; } /* 修改图标颜色(与蓝色背景对比) */ div.field1190285555456860160span .associative-search-icon .ui-icon { color: white; } /* 添加文字(通过伪元素实现,无需改HTML) */ div.field1190285555456860160span .associative-search-icon::after { content: "Batch Select"; color: white; } /* hover效果增强 */ div.field1190285555456860160span .associative-search-icon:hover { background-color: #7bb0f2; } ``` # 使用 1. 修改输入框长度代码中`placeholder="Search Quickly"`对应**编辑时显示提示语** ![image.png](https://cos.oneszhang.com/usr/uploads/2025/11/3752957953.png) 2. `field1190285555456860160span`中的数字部分对应**字段ID** ![image.png](https://cos.oneszhang.com/usr/uploads/2025/11/1482734105.png) --- # [泛微 Ecology10] 获取当前表单所有字段值的方法 作者:丸子·张 来源:https://blog.oneszhang.com/2025/11/14/180/ Markdown:https://blog.oneszhang.com/2025/11/14/180.md 发布日期:2025-11-14T00:34:00.000Z 更新时间:2025-11-14T00:34:00.000Z 专栏:泛微 E10 标签: 摘要:介绍泛微 Ecology10 表单前端二次开发中获取当前表单全部字段值的做法演变:早期需要先遍历字段信息,再分别读取主表字段与明细表数据,如今只需调用表单数据仓库提供的方法,即可一次性取出包含主表与明细在内的全部数据。适合需要批量取值的开发场景,新写法明显更简洁。 :::details[小白丸子一开始的做法(点击展开)] ``` /** * 获取表单内所有字段值 * @returns {Object} 包含主表字段和明细表数据的对象 */ const getAllFieldValues = () => { const formSdk = window.WeFormSDK.getWeFormInstance(); const fieldInfos = formSdk.listFieldInfo(); // 复用现有格式化逻辑 const formatFields = (fieldInfos) => { const result = { mainFields: [], detailTables: {} }; fieldInfos.forEach(field => { if (field.componentKey === 'DataTable') { result.detailTables[field.subFormId] = []; } else if (field.fieldId && /^\d+$/.test(field.fieldId)) { const target = field.subFormId && result.detailTables[field.subFormId] ? result.detailTables[field.subFormId] : result.mainFields; target.push(field.fieldId); } }); return result; }; // 复用现有数据获取逻辑 const getFormData = (fields) => { const result = { mainFields: {}, detailTables: {} }; fields.mainFields.forEach(fieldId => { try { result.mainFields[fieldId] = formSdk.getFieldObj(fieldId); } catch (error) { console.warn(`获取字段 ${fieldId} 失败:`, error); result.mainFields[fieldId] = null; } }); Object.keys(fields.detailTables).forEach(tableId => { try { result.detailTables[tableId] = formSdk.getDetailData(tableId) || []; } catch (error) { console.warn(`获取明细表 ${tableId} 失败:`, error); result.detailTables[tableId] = []; } }); return result; }; const fields = formatFields(fieldInfos); return getFormData(fields); }; ``` ::: # 现在的做法 `formSdk.weFormApi.formStore.getFormData().dataDetails; ` --- # [泛微 Ecology10] 丸子的 Ecology10 小本本 作者:丸子·张 来源:https://blog.oneszhang.com/2025/11/14/179/ Markdown:https://blog.oneszhang.com/2025/11/14/179.md 发布日期:2025-11-14T00:32:00.000Z 更新时间:2025-11-14T00:32:00.000Z 专栏:泛微 E10 标签: 摘要:汇总泛微Ecology10前端二开常见技巧,涵盖表单数据状态检查、静默保存与数据获取、图片上传样式优化、ESB动作流调用、自定义弹窗及当前用户信息获取等。针对表单渲染后操作DOM的时机问题,给出基于React字段组件的处理思路,并记录矩阵维护入口、组织日志表与报表空白滚动优化方法,适合E10开发人员查阅。 1. 检查表单在打开后数据是否存在变更: ``` window.WeaForm.getDataStatus(); ``` 2. 无刷(静默)保存:`formSdk.weFormApi.formStore.submitFormData();` 3. 获取整个表单数据: `formSdk.weFormApi.formStore.getFormData().dataDetails;` 4. 图片上传字段单图片上传样式优化(最新版本:10.0.2512.01基线做了该优化处理): ``` .ui-upload-line-of-row > div:nth-child(1):nth-last-child(2) { display: none !important ; } .ui-upload:has(.ui-upload-listT) .ui-upload-select-image { display: none !important; } ``` | 优化前 | 优化后 | | --- | --- | | ![image.png](https://cos.oneszhang.com/usr/uploads/2025/11/2933774924.png) | ![image.png](https://cos.oneszhang.com/usr/uploads/2025/11/4258729806.png)| 5. ESB调用 ``` const ebSdk=window.ebuilderSDK; ebSdk.callEsbFlow("1991410702295896092",{dept:"taet"},[]).then(res=>{/*动作流回调逻辑*/}); ``` ``` // 1. 构造请求参数 const requestData = { customParams: { mainTable: { requestId: requestId } }, esbFlowId: "1981608218199199750" }; // 2. 发送POST请求到ESB接口 fetch('/api/esb/server/event/triggerActionFlow', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(requestData) }) .then((response) => { // 3. 解析响应JSON(捕获HTTP错误状态) if (!response.ok) { throw new Error(`HTTP错误!状态码:${response.status}`); } return response.json(); }) .then((data) => { // 4. 请求成功回调 console.log('ESB流程触发成功:', data); // wffpSdk.showMessage('触发流程成功', 1, 3); // 成功提示(按需启用) }) .catch((error) => { // 5. 异常捕获(网络错误/HTTP错误/解析错误) console.error('ESB流程触发失败:', error); wffpSdk.showMessage('触发刷新合同失败', 2, 3); // 失败提示 }); ``` 5. 下拉字段显示值 键值关系 表单自定义字段选项表 `field_option` 6. 自定义弹窗 ![image.png](https://cos.oneszhang.com/usr/uploads/2026/04/1024344771.png) ``` weappUi.Dialog.confirm({ title: 'Notification', content: React.createElement('div', { style: { lineHeight: '1.6' } }, React.createElement('div', { style: { fontWeight: 'bold' } }, 'Your template has been applied successfully.'), React.createElement('div', { style: { marginTop: '8px' } }, 'Please go to the Edit or View (Letter to Approve) page to edit your letter content.'), React.createElement('div', { style: { marginTop: '12px', fontSize: '12px', color: '#666', borderTop: '1px dashed #eee', paddingTop: '8px' } }, 'Note: If an incorrect template was selected, click [Generate Letter] to re-apply.' ) ), icon: 'Icon-correct02-mcolor', cancelText: 'Got it', hideOkBtn: true, onCancel: () => console.log('Acknowledged') }); ``` 7. 当前人员id:`TEAMS.currentUser.id` 8. 另一种alert弹窗 ``` window.ebuilderSDK.alert(
AD mapping not found for the current user. Unable to synchronize Active Directory information.
, { mask: true, title: "AD Mapping Notice", okText: "OK", onOk: () => ebSdk.closePage(), onCancel: () => ebSdk.closePage() } ); ``` 9. 待整理 :你们有没有表单加载完成操作DOM的js,整个页面渲染完成的动作怎么取到的,有佬知道吗 没找到标准,因为React机制问题,渲染是在页面加载完成之后 ,我去试一下字段渲染sdk方案,我去试试,突然想到 formready 这个表单完成,但是dom没完成 React的问题,当然React解决啦~ ``` formSdk.afterFieldComp( fieldMark, React.createElement(() => { // 组件挂载时执行 React.useEffect(() => { console.log("多行文本组件渲染/追加完成了!执行你的函数..."); // 在这里调用你的函数 }, []); // 重点:返回 null,不渲染任何 div 或内容到页面上 return null; }) ); ``` 矩阵维护单页:/sp/hrm/conditionMatrix 指定某个矩阵的单页:/sp/hrm/conditionMatrix?spMatrixId=矩阵id 说明:矩阵id需要浏览器f12抓取下获取维护数据时的id 获取下拉框的显示值:field_option 组织架构相关日志:hrm_employeeandorglogs,hrm_employeeandorglogs_detail,hrm_opterator_log 报表大片空白解决方案:代码块 excel.workbook.options.scrollbarMaxAlign = true; excel.workbook.options.scrollByPixel = true 要滚动条只想去除多余空白区域的用上面的代码块,想直接去除的用下面的。) excel.workbook.options.showVerticalScrollbar=false;//纵向滚动条不显示 excel.workbook.options.showHorizontalScrollbar=false;//横向滚动条不显示 excel.workbook.refresh();//刷新布局 --- # [泛微 Ecology9] [ 高阶精华 ] 泛微 Ecology9 新插件:kkfileview,让文件预览 “零门槛” 作者:丸子·张 来源:https://blog.oneszhang.com/2025/10/20/177/ Markdown:https://blog.oneszhang.com/2025/10/20/177.md 发布日期:2025-10-20T04:13:00.000Z 更新时间:2025-10-20T04:13:00.000Z 专栏:泛微 E9 标签: 摘要:文章介绍在泛微 E9 中集成开源 kkfileview 实现统一文档预览:先自行编译部署 kkfileview 服务,再新建工具类生成带 ddcode 的文件下载地址,改造 index2file.jsp 拼接并 Base64 编码预览链接嵌入 iframe,最后通过 kkfileview.properties 配置开关与地址。适合做 E9 二开的读者参考,作者提醒方案安全性与稳定性尚未全面评估,生产使用前需充分测试,并注意并发、大文件和移动端适配。 > 本博客只涉及部分核心操作,不是保姆级教学,希望你也能有自己的思路,感悟破局的乐趣。 ![20251020104536.gif](https://cos.oneszhang.com/usr/uploads/2025/10/2106960712.gif) ## 背景 在日常办公中,文件预览是一个高频且重要的需求。传统的文件预览方案往往存在格式支持有限、体验不佳等问题。泛微E9自带的文档预览功能虽然基础,但在面对复杂格式或特殊需求时往往力不从心。 今天给大家介绍如何通过集成开源的kkfileview项目,为泛微E9打造一个强大、统一的文档预览解决方案。 ## 功能亮点 - **多格式支持**:支持Office、PDF、图片、视频、音频等常见格式 - **无缝集成**:与泛微E9原有文档模块完美融合 - **零客户端**:基于Web的纯前端预览,无需安装任何插件 - **高性能**:基于Spring Boot构建,响应迅速 - **开源免费**:基于Apache 2.0协议,可自由使用和修改 ## 实施步骤 ### 第一步:部署kkfileview服务 丸子采用的是从官方GitHub下载源码并自行编译打包的方式。如果你不想自己打包,可以私信丸子获取编译好的版本(有时间的话会分享)。 **启动成功后访问演示页**: ![image.png](https://cos.oneszhang.com/usr/uploads/2025/10/706206895.png) **访问成功界面**: ![image.png](https://cos.oneszhang.com/usr/uploads/2025/10/3245927657.png) ### 第二步:泛微E9文档功能改造 #### 核心工具类 首先创建一个工具类,用于生成文件下载的ddcode(这是泛微文档外发的核心知识): ```java package com.onestools; import weaver.docs.docs.util.DesUtils; public class getFileDownloadUrl { /** * 根据文件ID生成临时下载URL * @param fileid 文件ID * @return 下载URL */ public static String getFileDownloadUrl(int fileid) { try { String encrypt = new DesUtils().encrypt(1 + "_" + fileid); return "/weaver/weaver.file.FileDownload?fileid=" + fileid + "&download=1&ddcode=" + encrypt; } catch (Exception e) { e.printStackTrace(); return null; } } /** * 根据文件ID和基础URL生成临时下载URL * @param fileid 文件ID * @param baseurl 基础URL * @return 下载URL */ public static String getFileDownloadUrl(int fileid, String baseurl) { try { String encrypt = new DesUtils().encrypt(1 + "_" + fileid); return baseurl + "/weaver/weaver.file.FileDownload?fileid=" + fileid + "&download=1&ddcode=" + encrypt; } catch (Exception e) { e.printStackTrace(); return null; } } } ``` #### JSP页面改造 在`ecology目录下/docs/e9/index2file.jsp`中增加kkfileview预览逻辑: **增加引用**: ```jsp <%@ page import="com.onestools.getFileDownloadUrl" %> <%@ page import="sun.misc.BASE64Encoder" %> ``` ![image.png](https://cos.oneszhang.com/usr/uploads/2025/10/2340020995.png) **添加kkfileview预览逻辑**: ```jsp // 现在检查kkfileview配置 boolean useKKFileView = "1".equals(rs.getPropValue("kkfileview","IsUsekkFileView")); String kkFileViewUrl = rs.getPropValue("kkfileview","kkFileViewUrl"); // kkfileview服务地址 String e9BaseUrl = rs.getPropValue("kkfileview","E9BaseUrl"); // E9基础URL if(useKKFileView && !isEdit) { // KKFileView预览逻辑 if(!"".equals(Util.null2String(kkFileViewUrl)) && !"".equals(Util.null2String(e9BaseUrl))) { try { // 使用getFileDownloadUrl工具类生成下载链接 String originUrl = getFileDownloadUrl.getFileDownloadUrl(imagefileId, e9BaseUrl); String previewUrl = originUrl + "&fullfilename=" + java.net.URLEncoder.encode(imagefilename, "UTF-8"); // Base64编码previewUrl String base64Url = new BASE64Encoder().encode(previewUrl.getBytes("UTF-8")); // 生成最终的iframe地址 docContent = ""; } catch (Exception e) { // 如果kkfileview出错,继续使用原有逻辑 e.printStackTrace(); } } } ``` ![image.png](https://cos.oneszhang.com/usr/uploads/2025/10/3504310208.png) ### 第三步:系统配置 在`ecology目录下/WEB-INF/prop`新建`kkfileview.properties` 增加以下内容: ```properties # 是否开启kkfileview IsUsekkFileView=1 # kkfileview的外部访问地址 kkFileViewUrl=http://192.168.31.130:8012/kkpreview # 相对于kkfileview访问E9的地址(建议内网地址) E9BaseUrl=http://127.0.0.1:8080 ``` ## 注意事项 1. **安全性考虑**:该方案的安全性和稳定性暂未经过全面评估,请在生产环境使用前进行充分测试 2. **性能优化**:大量并发预览时可能需要考虑kkfileview的集群部署 3. **文件大小**:超大文件预览时需要注意网络传输和服务器性能 4. **移动端适配**:本示例只处理了index2file.jsp,如需移动端支持,需要对index2mobile.jsp进行类似改造 ## 总结 通过集成kkfileview,我们成功为泛微E9赋予了强大的文档预览能力。这种"破局"思路不仅适用于文档预览,还可以扩展到其他系统功能的增强和改造。 **技术只是工具,思路才是关键**。希望这个案例能够激发大家更多的创新想法,在现有系统的基础上创造出更多可能性。 --- *温馨提示:本文涉及的技术方案仅供学习参考,请根据实际环境进行评估和测试。如有技术问题,欢迎交流讨论!* --- # [泛微 Ecology9] GETE9LOG.jsp 直接查看当前Ecology9日志 作者:丸子·张 来源:https://blog.oneszhang.com/2025/10/17/169/ Markdown:https://blog.oneszhang.com/2025/10/17/169.md 发布日期:2025-10-17T07:28:07.000Z 更新时间:2025-10-17T07:28:07.000Z 专栏:泛微 E9 标签: 摘要:介绍在泛微 E9 中通过自建 JSP 页面直接查看当前 ecology 日志的方法:将页面放入 ecology 目录后访问,带登录校验,可指定源编码与显示行数,读取 log/ecology 末尾日志并合并多行、统计日志级别。适合无 sysadmin 权限时快速排查测试环境问题,需注意日志文件路径与读取权限。 > ✅ **为什么?** > > 1. 无sysadmin限制,便于测试系统排查问题! > 2. 速度!快! > 3. 好看! > :::tip[怎么用?] 做一个xxxx.jsp,仍在ecology/目录下 访问你的OA地址:/xxxx.jsp ::: > 注: 丸子直接用cursor写的! # 效果图 ![image.png](https://cos.oneszhang.com/usr/uploads/2025/10/2259404055.png) # 上代码 ```jsp <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %> <%@ page import="weaver.hrm.User" %> <%@ page import="weaver.hrm.HrmUserVarify" %> <%@ page import="java.io.File" %> <%@ page import="java.io.RandomAccessFile" %> <%@ page import="java.io.IOException" %> <%@ page import="java.util.ArrayList" %> <%@ page import="java.util.List" %> <%@ page import="java.util.Collections" %> <%@ page import="weaver.hrm.User" %> <%@ page import="weaver.hrm.HrmUserVarify" %> <%@ page import="java.io.File" %> <%@ page import="java.io.RandomAccessFile" %> <%@ page import="java.io.IOException" %> <%@ page import="java.util.ArrayList" %> <%@ page import="java.util.List" %> <%@ page import="java.util.Collections" %> <%-- 1. 登录验证:未登录跳转到登录页 --%> <% User user = HrmUserVarify.getUser(request, response); if (user == null) { response.sendRedirect("/login/Login.jsp"); return; } // 方案A:页面与请求保持UTF-8 request.setCharacterEncoding("UTF-8"); response.setCharacterEncoding("UTF-8"); // 源日志编码(默认GBK,可传GB2312/GB18030) String srcCharset = request.getParameter("srcCharset"); if (srcCharset == null || srcCharset.trim().isEmpty()) { srcCharset = "GBK"; } // 2. 核心参数:当前ecology日志路径(固定路径,无需日期) String logDirPath = request.getRealPath("/") + "log" + File.separator; File logFile = new File(logDirPath + "ecology"); // 3. 显示行数设置:默认100行,限制10-1000行 String lineCountStr = request.getParameter("lineCount"); int lineCount = 100; if (lineCountStr != null && !lineCountStr.trim().isEmpty()) { try { lineCount = Integer.parseInt(lineCountStr.trim()); lineCount = Math.max(10, lineCount); lineCount = Math.min(1000, lineCount); } catch (NumberFormatException e) { lineCount = 100; } } List logContent = new ArrayList<>(); String errorMsg = ""; // 日志级别统计 int infoCount = 0, errorCount = 0, warnCount = 0, debugCount = 0; if ("POST".equalsIgnoreCase(request.getMethod())) { try { if (!logFile.exists()) { errorMsg = "日志文件不存在:" + logFile.getAbsolutePath(); } else if (!logFile.canRead()) { errorMsg = "无权限读取日志文件:" + logFile.getAbsolutePath(); } else { // 使用源编码读取(GB2312/GBK/GB18030等) List rawLogContent = readLastNLines(logFile, lineCount, srcCharset); // 合并多行日志并统计日志级别 StringBuilder currentLog = new StringBuilder(); String currentLevel = "info"; for (String line : rawLogContent) { if (line == null || line.replaceAll("[ \t\u3000]", "").trim().isEmpty()) { continue; } // 检查是否是新的日志条目(以时间戳开头) if (isNewLogEntry(line)) { // 保存之前的日志条目 if (currentLog.length() > 0) { logContent.add(currentLog.toString()); // 统计日志级别 switch (currentLevel) { case "info": infoCount++; break; case "error": errorCount++; break; case "warn": warnCount++; break; case "debug": debugCount++; break; } } // 开始新的日志条目 currentLog = new StringBuilder(line); currentLevel = parseLogLevel(line); } else { // 继续当前日志条目 currentLog.append("\n").append(line); } } // 处理最后一个日志条目 if (currentLog.length() > 0) { logContent.add(currentLog.toString()); switch (currentLevel) { case "info": infoCount++; break; case "error": errorCount++; break; case "warn": warnCount++; break; case "debug": debugCount++; break; } } } } catch (IOException e) { errorMsg = "读取日志失败:" + e.getMessage(); e.printStackTrace(); } } %> <%-- 6. 自定义方法:必须放在 <%! %> 声明块中(JSP语法要求) --%> <%! /** * 读取文件的最后N行,按指定源编码解码成Unicode */ private List readLastNLines(File file, int n, String charsetName) throws IOException { List result = new ArrayList<>(); if (n <= 0) return result; try (RandomAccessFile raf = new RandomAccessFile(file, "r")) { long fileLength = raf.length(); long pos = fileLength - 1; int lines = 0; java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(256); while (pos >= 0 && lines < n) { raf.seek(pos); int b = raf.readByte() & 0xFF; if (b == 0x0A && pos != fileLength - 1) { // \n byte[] bytes = baos.toByteArray(); reverseBytes(bytes); result.add(new String(bytes, charsetName)); baos.reset(); lines++; } else if (b != 0x0D && b != 0x0A) { baos.write(b); } pos--; } if (baos.size() > 0) { byte[] bytes = baos.toByteArray(); reverseBytes(bytes); result.add(new String(bytes, charsetName)); } Collections.reverse(result); } return result; } private void reverseBytes(byte[] arr) { int i = 0, j = arr.length - 1; while (i < j) { byte tmp = arr[i]; arr[i] = arr[j]; arr[j] = tmp; i++; j--; } } /** * 解析日志级别 */ private String parseLogLevel(String logLine) { if (logLine == null) return "info"; String upperLine = logLine.toUpperCase(); if (upperLine.contains(" ERROR ")) return "error"; if (upperLine.contains(" WARN ")) return "warn"; if (upperLine.contains(" DEBUG ")) return "debug"; if (upperLine.contains(" INFO ")) return "info"; return "info"; } /** * 解析日志时间戳 */ private String parseLogTimestamp(String logLine) { if (logLine == null) return ""; // 匹配格式:2025-10-17 14:01:34,407 java.util.regex.Pattern pattern = java.util.regex.Pattern.compile("(\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2},\\d{3})"); java.util.regex.Matcher matcher = pattern.matcher(logLine); if (matcher.find()) { return matcher.group(1); } return ""; } /** * 解析线程信息 */ private String parseLogThread(String logLine) { if (logLine == null) return ""; // 匹配格式:[null] Thread-119-239[weaver.email.timer.MailClearTimer:92] java.util.regex.Pattern pattern = java.util.regex.Pattern.compile("\\[([^\\]]+)\\]\\s+([^\\[]+)\\[([^\\]]+)\\]"); java.util.regex.Matcher matcher = pattern.matcher(logLine); if (matcher.find()) { return matcher.group(2) + "[" + matcher.group(3) + "]"; } return ""; } /** * 解析日志消息内容 */ private String parseLogMessage(String logLine) { if (logLine == null) return ""; // 找到最后一个 " - " 后的内容 int lastDash = logLine.lastIndexOf(" - "); if (lastDash != -1 && lastDash + 3 < logLine.length()) { return logLine.substring(lastDash + 3); } return logLine; } /** * 判断是否是新的日志条目(以时间戳开头) */ private boolean isNewLogEntry(String line) { if (line == null || line.trim().isEmpty()) return false; // 匹配格式:2025-10-17 14:01:34,407 INFO return line.matches("^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2},\\d{3}.*"); } %> 当前Ecology日志查看
当前Ecology日志查看
(10-1000行)
日志文件路径:<%= logFile.getAbsolutePath() %> <% if (!logContent.isEmpty()) { %>| 实际显示行数:<%= logContent.size() %><% } %> | 源编码:<%= srcCharset %>
<% if (!logContent.isEmpty()) { %>
INFO: <%= infoCount %>
ERROR: <%= errorCount %>
WARN: <%= warnCount %>
DEBUG: <%= debugCount %>
<% } %> <%-- 错误提示 --%> <% if (!errorMsg.isEmpty()) { %>
<%= errorMsg %>
<%-- 空状态(未点击加载) --%> <% } else if (logContent.isEmpty()) { %>
请设置行数并点击"加载日志"按钮查看内容
<%-- 日志内容 --%> <% } else { %>
<% for (int i = logContent.size() - 1; i >= 0; i--) { String logEntry = logContent.get(i); if (logEntry == null || logEntry.trim().isEmpty()) continue; // 解析日志各部分(从第一行获取) String[] lines = logEntry.split("\n"); String firstLine = lines[0]; String logLevel = parseLogLevel(firstLine); String timestamp = parseLogTimestamp(firstLine); String thread = parseLogThread(firstLine); // 清理日志内容,正确处理多行日志 String cleanedLogEntry = logEntry.replaceAll("\\r\\n|\\r|\\n", " ") // 将换行符替换为空格 .replaceAll("\\s+", " ") // 将多个连续空白字符替换为单个空格 .trim(); // 去除首尾空白 // HTML转义整个日志内容 String escapedLogEntry = cleanedLogEntry.replace("&", "&") .replace("<", "<") .replace(">", ">") .replace("\"", """) .replace("'", "'"); %>
<%= escapedLogEntry %>
<% } %>
<% } %>
``` --- # [泛微 Ecology9] 开发踩坑:weaver.conn.RecordSet-报错"字符串文字太长" 作者:丸子·张 来源:https://blog.oneszhang.com/2025/10/17/167/ Markdown:https://blog.oneszhang.com/2025/10/17/167.md 发布日期:2025-10-17T01:51:00.000Z 更新时间:2025-10-17T01:51:00.000Z 专栏:泛微 E9 标签: 摘要:文章记录了泛微OA集成AI流程审查时,将超长AI返回结果写入数据库报出Oracle字符串文字太长的排查过程。作者发现即便目标字段为CLOB,RecordSet更新仍受VARCHAR2四千字符限制,最终改用ConnStatement写入大字段解决,并建议按内容长度选择更新方式。 > 📌 > > 该文章由丸子引导AI辅助制作生成,如有改进意见,欢迎评论区留言。 > > 记录一次为泛微OA集成AI流程审查功能时遇到的技术难题与解决历程 ## 问题背景 在最近为泛微OA系统集成AI流程智能审查功能的开发过程中,遇到了一个令人困扰的问题:当尝试将AI返回的审查结果写入数据库时,表单赋值操作始终无法成功完成。 经过仔细排查系统日志,发现了关键的错误信息: ``` 2025-04-29 19:01:51,512 ERROR A2 - [null] resin-port-8081-363-363[weaver.conn.RecordSet:1908] - weaver.conn.RecordSet java.sql.SQLSyntaxErrorException: ORA-01704: 字符串文字太长 ``` 这个错误表明,AI返回的字符串内容长度超过了4000字符,导致数据库写入失败。 ## 问题排查过程 ### 第一阶段:数据库字段类型检查 首先检查了数据库表结构,发现目标字段确实是CLOB类型,理论上应该能够存储大文本数据: ```sql -- 检查字段类型 SELECT column_name, data_type FROM user_tab_columns WHERE table_name = 'FORMTABLE_MAIN_314' AND column_name = 'AISHJG'; ``` 结果显示字段类型为CLOB,这让我们一度感到困惑——既然字段类型正确,为什么还会出现长度限制的错误? ### 第二阶段:技术原理探究 经过深入研究和测试,终于找到了问题的根源: **RecordSet在更新CLOB字段时的局限性** 在Oracle数据库环境下,SQL语句在执行之前会将所有字符类型的数据隐式转换为VARCHAR2类型,而VARCHAR2类型的最大长度限制为4000字符。当字符串长度超过这个限制时,转换就会失败,抛出`ORA-01704: 字符串文字太长`异常。 即使目标字段是CLOB类型,使用RecordSet的更新方法仍然会受到这个限制的影响。 ## 解决方案 ### 错误做法 ❌ ```java // 以下是会导致错误的代码示例 RecordSet rs = new RecordSet(); String sql = "update formtable_main_314 set aishjg= ? where requestid=12345"; rs.executeUpdate(sql, longContent); ``` ### 正确做法 ✅ 使用`ConnStatement`来更新大字段: ```java ConnStatement st = null; try { st = new ConnStatement(); st.setStatementSql("update formtable_main_314 set aishjg=? where requestid=?"); // 设置参数值 st.setString(1, aiResultContent); // 支持CLOB大字段 st.setInt(2, 12345); // 请求ID st.executeUpdate(); } catch(Exception e) { // 异常处理 e.printStackTrace(); } finally { // 资源释放 if (st != null) { st.close(); } } ``` ## 重要技术说明 ### 泛微OA字段类型与数据库存储关系 在Oracle数据库环境下: - **多行文本字段**(未勾选HTML编辑):存储为`VARCHAR2(4000)` - **多行文本字段**(勾选HTML编辑):存储为`CLOB`类型 ### 最佳实践建议 1. **预判内容长度**:在处理可能包含大文本的场景(如AI返回结果、富文本内容、日志信息等)时,优先考虑使用ConnStatement。 2. **统一的数据库操作封装**: ```java public class DatabaseUtil { /** * 更新CLOB字段的通用方法 */ public static boolean updateClobField(String tableName, String clobField, String value, String whereCondition) { ConnStatement st = null; try { st = new ConnStatement(); String sql = "update " + tableName + " set " + clobField + "=? where " + whereCondition; st.setStatementSql(sql); st.setString(1, value); return st.executeUpdate() > 0; } catch(Exception e) { e.printStackTrace(); return false; } finally { if (st != null) st.close(); } } } ``` 3. **内容长度检查**: ```java // 在实际更新前进行长度检查 if (aiResultContent.length() > 4000) { // 使用ConnStatement更新 updateWithConnStatement(aiResultContent); } else { // 可以使用RecordSet(更简洁) updateWithRecordSet(aiResultContent); } ``` ## 经验总结 这次踩坑经历让我们深刻认识到: 1. **技术选型要谨慎**:不同的数据库操作方式有各自的适用场景,不能一概而论。 2. **理解底层原理**:了解数据库驱动和框架的底层实现原理,有助于快速定位和解决问题。 3. **日志分析的重要性**:详细的错误日志是解决问题的关键线索。 4. **社区力量宝贵**:技术社区的交流和经验分享能够大大缩短问题解决时间。 ## 致谢 特别感谢微信技术交流群中**luowe老师**的及时提醒,以及**泛微朱老师**的技术指导,正是他们的帮助让我们快速找到了问题的解决方案。 希望这篇经验分享能够帮助到遇到类似问题的开发者,避免重复踩坑! --- *如果您有更好的解决方案或其他泛微开发经验,欢迎在评论区交流分享!* --- # [泛微 Ecology9] 流程数据建模同表同步 & SQL插入建模数据 作者:丸子·张 来源:https://blog.oneszhang.com/2025/09/17/166/ Markdown:https://blog.oneszhang.com/2025/09/17/166.md 发布日期:2025-09-17T04:13:00.000Z 更新时间:2025-09-17T04:13:00.000Z 专栏:泛微 E9 标签: 摘要:泛微 EC9 中流程数据与建模模块打通的关键在于 formmodeid 字段。文章给出两种做法:建模模块直接绑定流程数据表,并在流程节点用 SQL 更新该字段值;或写入数据时一并带上该字段,避免建模无法识别。适合需要免转换同步或批量导入建模数据的开发与运维人员,重点是字段值与建模模块 ID 保持一致。 # 泛微 EC9 流程数据建模实战:2 招搞定数据同步,formmodeid 是关键! > 📌 > > 该文章由丸子引导AI辅助制作生成,如有改进意见,欢迎评论区留言。 > 在泛微 EC9 的日常运维中,流程数据与建模模块的数据打通,常常是让人头疼的难题——要么需要复杂的转数据操作,要么手动插入数据后建模模块“看不见”。其实,解开这个困局的钥匙,就藏在 **formmodeid** 这一个字段里。今天就分享两个“ALL IN BOOM”的实用技巧,让流程与建模数据同步变得简单直接。 ## 一、流程与建模“同表共用”:零转换,数据实时同步 很多时候,我们希望流程中新增的数据,能直接在建模模块中显示,省去中间的“流程转数据”步骤。核心思路就是让流程和建模“共用一张数据表”,而 formmodeid 就是让建模“识别”数据的关键。 ### 操作步骤拆解: 1. **创建建模模块,绑定流程数据表** 新建建模模块时,在“数据表配置”环节,直接选择与目标流程相同的数据表(比如流程使用的表是 `formtable_main_474`,建模就直接绑定这张表)。创建完成后,**务必记住这个建模模块的 ID**(比如示例中的“92”),这就是后续要用的 `formmodeid` 值。 2. **流程节点执行 DML,给数据“贴标签”** 在流程设计的某个节点(通常是“结束节点”或“数据提交节点”),添加一个“执行 SQL”的动作,通过 `update` 语句给表中的数据统一赋值 `formmodeid`。 通用 SQL 模板: ```sql update 【流程数据表名】 set formmodeid = '【建模模块 ID】' where 1=1 ``` 示例(对应表 `formtable_main_474`、建模 ID“92”): ```sql update formtable_main_474 set formmodeid = '92' where 1=1 ``` ### 效果立竿见影: 流程中新增的数据,会自动带上 `formmodeid = 92` 的“标签”,而建模模块因为绑定了同一张表,且默认只识别对应 `formmodeid` 的数据,所以流程数据会直接在建模中显示,不需要任何额外的转换或同步操作。 说白了:建模模块“看不见”数据,往往就差给数据赋上正确的 `formmodeid` 这一步。 ## 二、SQL 直接插入建模数据:formmodeid 是“通行证” 无论是二次开发中需要批量导入数据,还是通过 DML 语句手动插入数据,想要让数据在泛微 EC9 建模模块中正常显示,核心还是不能漏掉 `formmodeid` 字段。 ### 关键操作: 在写 `insert` 语句时,除了要插入业务字段(如姓名、日期、金额等),必须额外加上 `formmodeid = 【建模模块 ID】` 的赋值。 示例(向建模模块 ID 为“92”的模块插入数据): ```sql insert into formtable_main_474 (field1, field2, formmodeid) values ('业务数据 1', '业务数据 2', '92') ``` ### 原理很简单: 泛微 EC9 的建模模块,会通过 `formmodeid` 来区分不同模块的数据归属。如果插入数据时没有指定 `formmodeid`,或者值错误,数据虽然会进入数据表,但建模模块无法“认领”它,自然就不会显示。 这就像给数据办了一张“通行证”,`formmodeid` 就是通行证上的“目的地”,只有目的地正确,数据才能顺利“进入”建模模块。 ## 总结:formmodeid——流程与建模数据打通的“核心密钥” 泛微 EC9 中流程与建模的数据同步,看似复杂,实则只要抓住 `formmodeid` 这个核心字段,就能化繁为简: - 同表共用时,用 `update` 给数据赋对 `formmodeid`,实现“数据即同步”; - SQL 插入时,给数据带上 `formmodeid`,让建模模块“认得出”数据。 不用纠结复杂的配置,也不用写大量代码,聚焦 `formmodeid` 这一个字段,就能轻松搞定流程与建模的数据打通,效率直接拉满! --- # [泛微 Ecology10] 明细表处理示例JS 1:字段联动后判断明细表数据隐藏选项 作者:丸子·张 来源:https://blog.oneszhang.com/2025/07/09/165/ Markdown:https://blog.oneszhang.com/2025/07/09/165.md 发布日期:2025-07-09T03:00:19.000Z 更新时间:2025-07-09T03:00:19.000Z 专栏:泛微 E10 标签: 摘要:针对泛微 E10 表单中明细表的动态选项控制,文章以学生成绩表为例,介绍在数据联动事件触发后遍历明细表各行,判断分数为空或零时移除成绩类型下拉中的优秀与良好选项。核心是利用表单 SDK 获取字段标识、行标识并操作选项,适用于明细表联动场景,实际可按业务条件灵活调整。 在表单开发中,明细表(动态表格)的字段联动和数据控制是常见需求。比如当某个字段的值满足特定条件时,需要动态隐藏另一个字段的部分选项。本文通过一个简化的 Demo,演示如何实现这一功能。 #### 场景说明 假设我们有一个“学生成绩表”明细表,包含两个字段: - **成绩类型(type)**:下拉选择框,选项为 `["优秀(0)", "良好(1)", "及格(2)", "不及格(3)"]` - **分数(score)**:数字输入框 需求:当“分数”字段的值为空或 0 时,自动隐藏“成绩类型”中的“优秀”和“良好”选项(即移除选项值为 0 和 1 的选项)。 #### 实现思路 1. 获取明细表及字段的标识(ID) 2. 监听数据联动事件(如分数变化后触发) 3. 遍历明细表的每一行,判断分数值是否符合条件 4. 若符合条件,隐藏当前行“成绩类型”的指定选项 #### Demo 代码实现 ```javascript // 明细表标识(实际开发中通常由表单 SDK 自动生成) const detailTableId = "student_score_table"; // 获取明细表中字段的 ID(参数:字段名,明细表 ID) const typeFieldId = formSdk.convertFieldNameToId("type", detailTableId); const scoreFieldId = formSdk.convertFieldNameToId("score", detailTableId); // 监听数据联动事件(当字段值变化时触发) formSdk.registerAction(window.WeFormSDK.ACTION_DATA_LINKAGE, (data) => { // 获取明细表所有行的 ID 字符串(格式:"row1,row2,row3") const allRowIdsStr = formSdk.getDetailAllRowIndexStr(detailTableId); if (!allRowIdsStr) return; // 无数据行时直接返回 // 解析行 ID 为数组 const rowIds = allRowIdsStr.split(','); // 遍历每一行 rowIds.forEach(rowId => { // 拼接当前行的分数字段 ID(格式:字段 ID_行 ID) const currentScoreField = `${scoreFieldId}_${rowId}`; // 获取当前行的分数值 const scoreValue = formSdk.getFieldValue(currentScoreField); // 判断分数是否为空或 0 if (scoreValue === '' || scoreValue === '0' || scoreValue === 0) { // 拼接当前行的成绩类型字段 ID const currentTypeField = `${typeFieldId}_${rowId}`; // 移除选项值为 0 和 1 的选项(即"优秀"和"良好") formSdk.removeSelectOption(currentTypeField, "0,1"); } }); }); ``` #### 代码解析 1. **字段标识获取** 通过 `formSdk.convertFieldNameToId` 方法,将字段名转换为表单内部使用的 ID,需传入字段名和明细表 ID(确保在明细表中唯一)。 2. **事件监听** 使用 `formSdk.registerAction` 监听 `ACTION_DATA_LINKAGE` 事件,该事件在数据联动(如字段值变化、表单加载完成等场景)时触发,确保逻辑在数据更新后执行。 3. **行遍历处理** - 通过 `formSdk.getDetailAllRowIndexStr` 获取所有行的 ID 字符串,拆分后得到每一行的唯一标识(rowId)。 - 对每一行,通过 `formSdk.getFieldValue` 获取分数值,判断是否为空或 0。 4. **选项隐藏** 当条件满足时,调用 `formSdk.removeSelectOption` 方法,传入当前行的“成绩类型”字段 ID 和需要移除的选项值("0,1"),实现选项的动态隐藏。 #### 关键 API 说明 | 方法 | 作用 | |------|------| | `convertFieldNameToId(fieldName, detailTableId)` | 将字段名转换为表单内部 ID(明细表字段需传入明细表 ID) | | `registerAction(actionName, callback)` | 注册事件监听,这里监听数据联动事件 | | `getDetailAllRowIndexStr(detailTableId)` | 获取明细表所有行的 ID 字符串(格式:"row1,row2") | | `getFieldValue(fieldId)` | 获取指定字段的值 | | `removeSelectOption(fieldId, optionValues)` | 移除下拉框的指定选项(optionValues 为逗号分隔的选项值) | #### 总结 通过上述方法,我们实现了明细表中字段联动后的动态选项控制。实际开发中,可根据业务需求调整判断条件(如分数大于 90 时显示特定选项)或扩展字段逻辑(如多字段联合判断)。表单 SDK 的事件机制和行 ID 拼接规则是实现明细表动态控制的核心,掌握这些可灵活应对各类明细表处理场景。 --- # [泛微 Ecology10] 批量下载当前表单内所有附件 作者:丸子·张 来源:https://blog.oneszhang.com/2025/07/07/164/ Markdown:https://blog.oneszhang.com/2025/07/07/164.md 发布日期:2025-07-07T00:18:18.000Z 更新时间:2025-07-07T00:18:18.000Z 专栏:泛微 E10 标签: 摘要:泛微 E10 表单默认只能按单个附件字段逐个下载,字段较多时效率低下。文章给出前端批量下载方案:借助 WeFormSDK 遍历指定字段收集附件 ID,调用平台异步批量下载接口创建任务,再轮询任务状态,完成后自动触发 ZIP 打包下载,并配有错误处理与消息提示,适合需要一次性导出表单全部附件的二次开发场景。 #### 功能背景 泛微 E10 支持单附件字段的批量下载能力。但默认情况下,用户只能逐个字段进行批量下载附件,当表单包含大量附件字段时,操作效率较低。通过开发批量下载功能,提升用户体验。 #### 技术实现方案 下面是实现批量下载功能的核心代码,主要分为三个步骤:收集附件 ID、创建异步下载任务、轮询检查任务状态并下载: ```javascript // 初始化 SDK 实例 const weFormSdk = window.WeFormSDK.getWeFormInstance(); // 定义需要下载附件的字段名称,请修改为对应的主表字段名称 const fieldNames = [ "sigvendatf", "poa", "regcer", "taxcer", "pscq", "pscend", "bancon", "conpo", "emaappandc" ]; // 收集所有附件 ID const values = []; fieldNames.forEach(fieldName => { try { const fieldId = weFormSdk.convertFieldNameToId(fieldName); const fieldValue = weFormSdk.getFieldValue(fieldId); if (fieldValue) { values.push(fieldValue); } } catch (error) { console.error(` 获取字段 ${fieldName} 失败:`, error); } }); // 创建异步下载任务 async function createDownloadTask() { if (values.length === 0) { window.WeFormSDK.showMessage("没有可下载的文件", 1, 3); return; } const today = new Date().toISOString().split('T')[0]; // 获取当前日期 try { const response = await fetch('/api/file/batch/asynchronous/downloadByFileIds', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ "module": "ebuildercard", "fileIds": values.join(','), "authModule": "ebuildercard", "source": "form", "zipName": `附件名称在这里-请修改或赋值变量-${today}` }) }); const data = await response.json(); if (!data.data.taskId) { throw new Error('获取任务 ID 失败'); } window.WeFormSDK.showMessage("开始创建下载任务", 3, 2); await checkDownloadStatus(data.data.taskId); } catch (error) { console.error('创建下载任务失败:', error); window.WeFormSDK.showMessage("创建下载任务失败", 2, 3); } } // 轮询检查下载任务状态 async function checkDownloadStatus(taskId) { let attempt = 0; const maxAttempts = 20; // 最多尝试 20 次 while (attempt < maxAttempts) { try { const response = await fetch(`/api/file/batchDownloadPercent?uuid=${taskId}`); const data = await response.json(); if (data.data.fileId) { window.WeFormSDK.showMessage("下载任务已完成", 3, 2); downloadFile(data.data.fileId); return; } attempt++; if (attempt < maxAttempts) { await new Promise(resolve => setTimeout(resolve, 500)); // 等待 500ms } } catch (error) { console.error('查询下载状态失败:', error); throw error; } } throw new Error('下载任务超时'); } // 执行文件下载 function downloadFile(fileId) { const link = document.createElement('a'); link.href = `/api/file/remotedownload/${fileId}/upload/true`; link.setAttribute('download', ''); document.body.appendChild(link); link.click(); document.body.removeChild(link); } // 启动下载流程 createDownloadTask().catch(error => { console.error('下载流程出错:', error); window.WeFormSDK.showMessage("下载过程中出现错误", 2, 3); }); ``` #### 代码功能解析 1. **附件收集模块** - 通过 `weFormSdk.convertFieldNameToId` 方法将字段名称转换为系统 ID - 使用 `weFormSdk.getFieldValue` 获取附件 ID 值 - 支持从多个字段收集附件,适应复杂表单结构 2. **异步下载任务创建** - 调用平台提供的 `/api/file/batch/asynchronous/downloadByFileIds` 接口 - 将所有附件 ID 合并后作为参数传递 - 支持自定义 ZIP 文件名,自动添加日期戳 3. **任务状态监控机制** - 通过轮询 `/api/file/batchDownloadPercent` 接口检查进度 - 设置最大尝试次数和间隔时间,防止无限循环 - 任务完成后自动触发下载 4. **用户体验优化** - 使用平台 SDK 提供的消息提示功能 - 完整的错误处理机制 - 异步操作避免界面卡顿 --- # [泛微 Ecology9 ecode] 像批量增加选项一样去"批量新增字段"(半成品) 作者:丸子·张 来源:https://blog.oneszhang.com/2025/06/09/161/ Markdown:https://blog.oneszhang.com/2025/06/09/161.md 发布日期:2025-06-09T01:41:00.000Z 更新时间:2025-06-09T01:41:00.000Z 专栏:泛微 E9 标签: 摘要:泛微E9 ecode 中尝试把批量增加选项的思路搬到批量新增字段,目标是在表单字段管理页面每行输入一个字段名、用回车分隔,一次添加多个字段。作者借助 ecodeSDK 覆写弹窗和搜索组组件,注入批量添加按钮与输入框,但目前只复用了原有添加按钮的点击逻辑,功能仍是半成品,提醒不要直接在 ecode 中运行,适合二次开发人员交流与继续完善。 # 我希望的Ecode目的:每行1个字段,请用回车分隔 ## 昨晚心血来潮搞了一半,感觉功能还是有用的,能节省字段维护不少的时间,不知有没有大佬继续完成这个功能~ ![db290cfb6666bfc6f3d76597ad01dc0.png](https://cos.oneszhang.com/usr/uploads/2025/06/391236860.png) > ⚠️ **代码请勿直接运行** > > 以下ecode代码为半成品,仅供交流,非技术人员请勿直接在ecode运行~ > ``` // 我计划用WeaDialog,1.锁定弹窗范围,防止污染,并且newProps.children.props.wfFormSetCusBatchOpsStore中似乎可以直接修改表单中的数据 ecodeSDK.overwritePropsFnQueueMapSet('WeaDialog', { fn: (newProps) => { if (!ecodeSDK.checkLPath('/wui/engine.html#/workflowengine/path/pathSet/pathDetail/formManage/editField')) return; if (newProps.title === "批量添加字段") { console.log(newProps.children.props.wfFormSetCusBatchOpsStore); } }, }); let textValue = ''; // 我计划复写WeaSearchGroup 来获取原有添加按钮的动作,并添加新的按钮 ecodeSDK.overwritePropsFnQueueMapSet('WeaSearchGroup', { fn: (newProps) => { var addClickHandler = null; for (var i = 0; i < newProps.customComponent.props.children.length; i++) { var child = newProps.customComponent.props.children[i]; if (child && child.props && child.props.title === "添加") { addClickHandler = child.props.onClick; break; } } const {Button,Input} = antd; const content = (
{ textValue = event.target.value;}} />
); const batchAddButton = ( ); newProps.customComponent.props.children.unshift(batchAddButton); }, }); ``` --- # [泛微 Ecology10] 记首次发布APIdemo测试接口~ 作者:丸子·张 来源:https://blog.oneszhang.com/2025/05/26/158/ Markdown:https://blog.oneszhang.com/2025/05/26/158.md 发布日期:2025-05-26T14:43:00.000Z 更新时间:2025-05-26T14:43:00.000Z 专栏:泛微 E10 标签: 摘要:记录首次在泛微Ecology10中按官方ecode文档搭建后端二开环境并发布API测试接口的过程。针对3.1.3节未说明如何创建接口文件的问题,作者补充了在secondev-md-demo项目里新建package和HelloController、粘贴示例代码、处理报错后编译打包部署的步骤,适合二开新手参考。 # 小记 E10的公开文档丰富程度远超E9,不再是零零散散的扔的到处都是,虽然但是,感觉也还是蛮乱的。。。。。 或许,是功能太多的缘故吧(emmmmm) E10相关网站链接后续我也会正常更新到导航站中~ # 开始-环境搭建 我是按照官方ecode的文档(虽然是后端二开环境,但没办法,他就是在ecode文档中...应该是出于泛微后续建设伟大设想,ecode不再只是ecode,而是向着二开生态前进!) 就是它,一步一步搞就好了! ![image.png](https://cos.oneszhang.com/usr/uploads/2025/05/137492846.png) # 重点:3.1.3 编写接口 这一部分并没有教会小白如何去创建这个接口文件,代码写在哪里(可能出于会二开的人一眼就懂的想法,没有在此描述) 我发布本文章也是因为这个位置文档中没有描述细节,旨在为小白稍稍解惑(巧了,我也是小白) **package:** **`com.weaver.seconddev.demo.controller`** 如何做? 在3.1.1中,新建了这个secondev-md-demo的项目,我们直接使用它,进行如图操作,之后输入`com.weaver.seconddev.demo.controller.HelloController`即可,IDEA会自动创建好package及java文件 ![image.png](https://cos.oneszhang.com/usr/uploads/2025/05/1057727149.png) 粘贴demo代码: ``` package com.weaver.seconddev.demo.controller; import com.weaver.common.authority.annotation.WeaPermission; import com.weaver.common.base.entity.result.WeaResult; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/api/secondev/demo") @WeaPermission(publicPermission = true) public class HelloController { @GetMapping("/hello") public WeaResult hello(@RequestParam("msg") String msg) { return WeaResult.success("Hello, " + msg); } } ``` ![image.png](https://cos.oneszhang.com/usr/uploads/2025/05/2456610227.png) # 报错?没关系,点这里! ![image.png](https://cos.oneszhang.com/usr/uploads/2025/05/4059626272.png) 然后进行,3.1.4 编译打包就OK啦~ 部署测试&成功(我测试时修改了代码,并非文中的demo代码): ![image.png](https://cos.oneszhang.com/usr/uploads/2025/05/3748847309.png) --- # 丸子开始泛微e9和e10并行开搞啦~ 作者:丸子·张 来源:https://blog.oneszhang.com/2025/04/23/151/ Markdown:https://blog.oneszhang.com/2025/04/23/151.md 发布日期:2025-04-23T13:49:00.000Z 更新时间:2025-04-23T13:49:00.000Z 专栏:泛微 E10 标签: 摘要:作者宣布自己已开始同时推进泛微 e-cology 9 与 e-cology 10 的相关工作,并为此新建了泛微 ecology10 讨论交流群,邀请正在使用 e10 的伙伴加入,以便集中交流与互助。内容偏向公告性质,主要面向泛微 OA 二次开发与运维人员。 e10的宝子们,速来!泛微ecology10讨论交流群: #(献花) ![image.png](https://cos.oneszhang.com/usr/uploads/2025/04/1396717881.png) --- # olmocr!成了,效果还行~ 作者:丸子·张 来源:https://blog.oneszhang.com/2025/04/05/145/ Markdown:https://blog.oneszhang.com/2025/04/05/145.md 发布日期:2025-04-05T07:49:00.000Z 更新时间:2025-04-05T07:49:00.000Z 专栏:部署与自托管 标签: 摘要:记录了在配备 NVIDIA A10 显卡的 Ubuntu 服务器上部署并试用 AllenAI 开源 OCR 项目 olmocr 的过程,前端借助 AI 生成的 Gradio 构建,单张示例图片识别耗时 45.7 秒。测试覆盖表格与公式识别场景,整体效果尚可,但作者表示首次尝试较乱,踩坑记录尚未整理。 # 项目 https://github.com/allenai/olmocr # 效果如图(前端代码为ai生成,使用 Gradio 构建): ## 识别以下示例图片用时:`45.7秒` ![image.png](https://cos.oneszhang.com/usr/uploads/2025/04/3539350542.png) # 测试服务器: | 配置项 | 规格 | |------------|-------------------| | vCPU | 16核 | | 内存 | 60 GiB | | 系统镜像 | Ubuntu 20.04 64位 | | 显卡型号 | NVIDIA A10 | | 显存 | 24 GB | # 第一次搞 有点乱!晚点整理踩坑记录!!! # 补充: ## 表格识别: ![image.png](https://cos.oneszhang.com/usr/uploads/2025/04/3099549186.png) ## 公式识别: ![image.png](https://cos.oneszhang.com/usr/uploads/2025/04/2198198662.png) 转换后: ![image.png](https://cos.oneszhang.com/usr/uploads/2025/04/2353780825.png) --- # excalidraw画板 私有化部署(基于Docker,可协同)指南 作者:丸子·张 来源:https://blog.oneszhang.com/2025/03/31/137/ Markdown:https://blog.oneszhang.com/2025/03/31/137.md 发布日期:2025-03-31T02:09:00.000Z 更新时间:2025-03-31T02:09:00.000Z 专栏:部署与自托管 标签: 摘要:以 Docker Compose 私有化部署支持协同的 Excalidraw 白板,将主应用、实时协同服务和第三方存储后端分别映射端口,并通过子域名反向代理及环境变量接入存储与 WebSocket 服务后启动使用。作者提示协同状态下可能出现后端保存异常,建议重要内容随时本地备份。 ## 在线体验 [立即体验 Excalidraw](https://excalidraw.com/) ![image.png](https://cos.oneszhang.com/usr/uploads/2025/03/1052334034.png) ## 已知问题 **协同模式下后端数据异常提示,还请路过的大佬帮忙看看~** 当前版本在协同状态下可能出现"无法保存到后端数据库..."警告,建议重要内容随时本地备份。 ![ddbe059bf825f44fddd2a78ea5447e9.png](https://cos.oneszhang.com/usr/uploads/2025/03/2189188106.png) --- ## 部署架构 | 组件 | 源码地址 | 说明 | |----------------------|------------------------------------------------------------------------------------------|--------------------| | 核心应用 | [excalidraw/excalidraw](https://github.com/excalidraw/excalidraw) | 官方白板主程序 | | 协同服务 | [excalidraw/excalidraw-room](https://github.com/excalidraw/excalidraw-room) | 实时协作模块 | | 存储服务(第三方) | [alswl/excalidraw-storage-backend](https://github.com/alswl/excalidraw-storage-backend) | 数据持久化解决方案 | --- ## 部署准备 ### 域名解析配置(本指南全程以oneszhang.com为例,改为你的域名即可~) | 子域名 | 反向代理 | 说明 | |----------------------|-------------|-------------------| | draw.oneszhang.com | 本地12800端口 | 主应用访问入口 | | draw-room.oneszhang.com | 本地12801端口 | 协同服务入口 | | json.oneszhang.com | 本地12802端口 | 存储服务API入口 | --- ## 部署流程 ### 1. 获取源代码 git clone示例: ```bash git clone https://github.com/excalidraw/excalidraw git clone https://github.com/excalidraw/excalidraw-room git clone https://github.com/alswl/excalidraw-storage-backend ``` 建议使用Releases版本(发行版)确保稳定性 ![image.png](https://cos.oneszhang.com/usr/uploads/2025/03/2595416111.png) --- ### 2. Docker Compose配置 ![image.png](https://cos.oneszhang.com/usr/uploads/2025/03/3407144312.png) --------- ```yaml services: excalidraw: build: context: ./excalidraw args: - NODE_ENV=production container_name: excalidraw ports: - "12800:80" restart: on-failure environment: - NODE_ENV=production - VITE_APP_BACKEND_V2_GET_URL=https://json.oneszhang.com/api/v2/scenes/ - VITE_APP_BACKEND_V2_POST_URL=https://json.oneszhang.com/api/v2/scenes/ - VITE_APP_LIBRARY_URL=https://libraries.excalidraw.com - VITE_APP_LIBRARY_BACKEND=https://us-central1-excalidraw-room-persistence.cloudfunctions.net/libraries - VITE_APP_AI_BACKEND=https://oss-ai.excalidraw.com - VITE_APP_WS_SERVER_URL=https://draw-room.oneszhang.com - VITE_HTTP_STORAGE_BACKEND_URL=https://json.oneszhang.com/api/v2/ volumes: - ./excalidraw:/opt/node_app/app:delegated - ./excalidraw/package.json:/opt/node_app/package.json - ./excalidraw/yarn.lock:/opt/node_app/yarn.lock - ./excalidraw/node_modules:/opt/node_app/app/node_modules excalidraw-room: build: context: ./excalidraw-room container_name: excalidraw-room ports: - "12801:80" restart: on-failure environment: - NODE_ENV=production excalidraw-storage-backend: build: context: ./excalidraw-storage-backend container_name: excalidraw-storage-backend ports: - "12802:8080" environment: STORAGE_URI: redis://redis:6379 redis: image: redis ``` --- ### 3. 启动服务 ```bash docker-compose up -d ``` ### 4. 开始享用 浏览器访问:`draw.oneszhang.com` --- 指南中如果存在任何问题或优化点,或部署过程中如遇问题,欢迎各位佬在评论区交流讨论。 --- # [JS辅助方法] 校验字符串字符类型并返回结果及过滤后的字符串 作者:丸子·张 来源:https://blog.oneszhang.com/2025/03/21/136/ Markdown:https://blog.oneszhang.com/2025/03/21/136.md 发布日期:2025-03-21T04:07:00.000Z 更新时间:2025-03-21T04:07:00.000Z 专栏:前端与脚本 标签: 摘要:这篇 JavaScript 工具函数通过类型编号映射表,把数字、字母、汉字、特殊符号和空格五类字符统一成检测与过滤两套正则,一次调用即可返回是否命中目标类型以及剔除这些字符后的字符串。文章给出使用示例、参数对照表和空数组等边界情况说明,并展示了清理输入、提取纯文本、密码强度校验等场景,适合需要在表单校验或数据清洗中复用字符过滤逻辑的前端开发者参考。 ```javascript /** * 校验字符串并返回结果及过滤后的字符串 * @param {string} str - 待处理的字符串 * @param {number[]} types - 需要检查/过滤的类型数组 (1-5) * @returns {object} 包含校验结果和过滤后的字符串 */ function checkAndFilter(str, types) { // 校验规则映射表 const checkRules = { 1: { pattern: "\\d", regex: /\d/ }, // 数字 2: { pattern: "[a-zA-Z]", regex: /[a-zA-Z]/ }, // 字母 3: { pattern: "\\u4e00-\\u9fa5", regex: /[\u4e00-\u9fa5]/ }, // 汉字 4: { pattern: "[^\\w\\u4e00-\\u9fa5]", regex: /[^\w\u4e00-\u9fa5]/ }, // 特殊符号 5: { pattern: "\\s", regex: /\s/ } // 空格 }; const patterns = types .filter(t => checkRules[t]) .map(t => `(${checkRules[t].pattern})`) .join("|"); const filterRegex = patterns ? new RegExp(patterns, "g") : null; const filteredStr = filterRegex ? str.replace(filterRegex, "") : str; const result = types.some(t => checkRules[t] ? checkRules[t].regex.test(str) : false ); return { result, filteredStr }; } ``` ### 使用示例 ```javascript const str = "Hello123 你好_world!"; // 示例1:过滤数字(1)和空格(5) const result1 = checkAndFilter(str, [1, 5]); console.log(result1); // 输出: { result: true, filteredStr: "Hello你好_world!" } // 示例2:仅过滤汉字(3) const result2 = checkAndFilter(str, [3]); console.log(result2); // 输出: { result: true, filteredStr: "Hello123 _world!" } // 示例3:过滤不存在的类型(6) const result3 = checkAndFilter("test", [6]); console.log(result3); // 输出: { result: false, filteredStr: "test" } ``` ### 主要改进说明 1. **双模式处理**: - `regex` 字段用于快速检测字符存在性(`.test()`) - `pattern` 字段用于构建过滤正则表达式(`.replace()`) 2. **智能过滤逻辑**: ```javascript // 组合多个类型的正则表达式 // 例如 [1,5] 会生成 /(\d)|(\s)/g const patterns = types.map(t => `(${checkRules[t].pattern})`).join("|"); ``` 3. **空类型处理**: ```javascript // 当传入空数组或无效类型时,返回原字符串 checkAndFilter("abc", []) // => { result: false, filteredStr: "abc" } ``` 4. **全局过滤**: ```javascript // 使用 g 标志替换所有匹配项 str.replace(filterRegex, "") ``` ### 参数对应关系 | 类型 | 说明 | 正则表达式 | 示例过滤效果 | |------|----------------------|-------------------------|----------------------------| | 1 | 数字 | `\d` | "a1b" → "ab" | | 2 | 字母 | `[a-zA-Z]` | "中a文1" → "中1" | | 3 | 汉字 | `[\u4e00-\u9fa5]` | "hello你好" → "hello" | | 4 | 特殊符号 | `[^\w\u4e00-\u9fa5]` | "a!b#c" → "abc" | | 5 | 空格(含换行制表符) | `\s` | "a b\tc" → "abc" | 该函数可以灵活处理各种字符过滤需求,例如: - **清理输入内容**:`checkAndFilter(input, [4,5])` 移除所有特殊符号和空格 - **提取纯文本**:`checkAndFilter(text, [1,4,5]).filteredStr` 保留汉字和字母 - **密码强度校验**:结合多个类型检测结果判断复杂度 --- # Doris2.1.7单机部署-dockercompose部署!秒了! 作者:丸子·张 来源:https://blog.oneszhang.com/2025/03/21/135/ Markdown:https://blog.oneszhang.com/2025/03/21/135.md 发布日期:2025-03-21T03:16:38.000Z 更新时间:2025-03-21T03:16:38.000Z 专栏:部署与自托管 标签: 摘要:用 Docker Compose 编排 Apache Doris 2.1.7 单机 FE 与 BE,完成端口、网络、数据与日志持久化及健康检查配置即可部署;部署前需在宿主机调大内存映射参数并关闭交换分区,内存不足则不建议尝试。文章还对比 2.x 与 3.x 的定位、性能、功能和升级差异,供读者按生产稳定或云原生湖仓等场景选版。 # 直接上docker-compose.yml!!! ```yaml services: fe: image: apache/doris:fe-ubuntu-2.1.7 container_name: doris-fe hostname: fe environment: - FE_SERVERS=fe1:172.30.0.2:9010 - MASTER_FE_IP=172.30.0.2 - FE_ID=1 ports: - "8030:8030" # FE HTTP端口 - "9020:9020" # FE BRPC端口 - "9030:9030" # MySQL协议端口 - "9010:9010" # FE thrift端口 volumes: - ./fe-data:/opt/apache-doris/fe/doris-meta # 元数据持久化 - ./fe-log:/opt/apache-doris/fe/log # 日志持久化 networks: doris-net: ipv4_address: 172.30.0.2 healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8030/api/health"] interval: 30s timeout: 10s retries: 5 be: image: apache/doris:be-ubuntu-2.1.7 container_name: doris-be hostname: be environment: - FE_SERVERS=fe1:172.30.0.2:9010 - MASTER_FE_IP=172.30.0.2 - BE_ADDR=172.30.0.3:9050 - BE_HEARTBEAT_PORT=9050 - CURRENT_BE_IP=172.30.0.3 - CURRENT_BE_PORT=9050 ports: - "8040:8040" # 心跳服务端口 - "9050:9050" # BE thrift端口 - "9060:9060" # BE BRPC端口 - "8060:8060" # BE HTTP端口 volumes: - ./be-data:/opt/apache-doris/be/storage # 数据存储持久化 - ./be-log:/opt/apache-doris/be/log # 日志持久化 networks: doris-net: ipv4_address: 172.30.0.3 depends_on: fe: condition: service_healthy healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8060/api/health"] interval: 30s timeout: 10s retries: 5 networks: doris-net: driver: bridge ipam: config: - subnet: 172.30.0.0/24 ``` # 报错 doris-be | Please set vm.max_map_count to be 2000000 under root using 'sysctl -w vm.max_map_count=2000000'. 解决办法: ``` # 在宿主机(物理机/虚拟机)执行 sudo sysctl -w vm.max_map_count=2000000 ``` doris-be | Please disable swap memory before installation. 解决办法: ``` # 在宿主机(物理机/虚拟机)执行 sudo swapoff -a ``` 如果你服务器内存不够大!那!放弃吧,骚年! Apache Doris(原百度 Palo)的 **2.x** 和 **3.x** 是两条不同的版本线,分别面向不同的用户需求和场景。以下是它们的核心差异和定位: --- ### 一、版本线定位 | **版本线** | **2.x(如 2.1.7)** | **3.x(如 3.0.0)** | |------------------|----------------------------------------------|----------------------------------------------| | **目标用户** | 稳定生产环境,保守升级需求 | 尝鲜新功能,高性能、云原生场景 | | **更新策略** | 长期维护(Bug 修复、安全补丁) | 主版本迭代(新功能、架构升级) | | **兼容性** | 向后兼容(无破坏性变更) | 可能包含不兼容改动(需评估升级成本) | --- ### 二、核心功能差异 #### 1. **性能优化** - **2.x** 侧重稳定性优化,例如查询计划器改进、小范围资源隔离。 - **3.x** - **向量化引擎增强**:全面向量化计算,复杂查询性能提升 2-5 倍。 - **Pipeline 执行模型**:支持异步流水线执行,降低资源争抢。 - **云原生资源调度**:深度集成 Kubernetes,动态扩缩容。 #### 2. **功能特性** - **2.x** 成熟功能:物化视图、动态分区、多租户资源组。 - **3.x** - **湖仓一体**:直接查询 Hudi/Iceberg 数据(无需导入)。 - **JSON 类型增强**:支持 JSONB 存储格式和高效检索。 - **Light Schema Change**:秒级表结构变更(无需重导数据)。 - **多 Catalog 联邦查询**:跨 Doris、MySQL、Elasticsearch 联合分析。 #### 3. **存储与事务** - **2.x** 基于 Segment V2 存储格式,支持行级更新(需 Merge-on-Read)。 - **3.x** - **Segment V3 存储**:列存压缩率提升 30%,支持 Z-Order 索引。 - **事务优化**:悲观锁机制减少并发冲突,写入吞吐提升。 #### 4. **运维与生态** - **2.x** 运维工具成熟,兼容 Hadoop/Spark 生态。 - **3.x** - **自动化运维**:智能索引推荐、慢查询自诊断。 - **Arrow Flight 协议**:高速数据传输(替代 JDBC/ODBC)。 - **多语言 UDF**:支持 Java/Python UDF 扩展。 --- ### 三、是否同时更新? - **是**,两条版本线会并行更新,但侧重点不同: - **2.1.7**:属于 **2.1.x 维护分支**,仅修复关键问题,无新功能。 - **3.0**:属于 **3.x 主版本线**,持续迭代新特性和架构升级。 --- ### 四、如何选择? | **场景** | **推荐版本** | |-------------------------|--------------| | 生产环境求稳 | 2.1.x | | 云原生部署、湖仓分析 | 3.x | | 高性能实时报表 | 3.x | | 兼容旧生态(Hadoop) | 2.1.x | --- ### 五、升级建议 1. **测试环境验证**:3.x 新功能需充分测试(如向量化引擎对复杂 SQL 的覆盖性)。 2. **回滚预案**:3.x 的存储格式变更可能导致降级困难。 3. **关注社区动态**:3.x 仍在快速迭代,部分功能可能不稳定。 如需具体场景的版本对比,建议参考 [Apache Doris 官方升级文档](https://doris.apache.org/docs/dev/upgrade-notes)。 --- # [泛微 Ecology9] 退回流程拦截禁止再次提交 作者:丸子·张 来源:https://blog.oneszhang.com/2025/01/23/134/ Markdown:https://blog.oneszhang.com/2025/01/23/134.md 发布日期:2025-01-23T07:29:00.000Z 更新时间:2025-01-23T07:29:00.000Z 专栏:泛微 E9 标签: 摘要:文章围绕泛微 Ecology EC9 流程退回后禁止再次提交的需求,对比了辅助字段与前端代码两种思路。作者通过阅读源码发现 rightMenu.lastnodeid 可标识上次退回的节点,据此在页面加载时置灰按钮并注册提交校验事件进行拦截。方案适合追求不加字段、便于复用的开发场景,同时提醒技术选型无绝对对错。 # 我的想法 对于功能的实现我希望更加简单,更加优雅,不希望有各种杂乱的操作,所有我决定找一个变量可以直接判断流程是否为被退回的状态,不去增加字段以及配置内容,以便以后快速的复用。 今天因为讨论这个问题,群里人们还吵了起来,一方主张基础功能配置 增加辅助字段的方式实现,一方主张不加字段“正八经开发有自己做事的标准”; 我认为 **没有绝对的对错,只有最适合自己的方案!每人有不同的追求,讨论技术没什么错与对,每人都有自己的想法,可以说出自己的观点,但没必要反驳别人的观点!** --- # 方案1 辅助字段 添加一个辅助字段,在退回时对它进行赋值,提交时判断这个字段是否有值,在此不做过多介绍,标准功能即可完全实现 --- # 方案2 代码块 核心:`wfform.getGlobalStore().rightMenu.lastnodeid` 我看了源码`/workflow/reqform/rightMenu`,此处的lastnodeid是指:上一次退回操作的节点id,所以,只要存在这个lastnodeid,即可判断是退回的流程 ``` jQuery(document).ready(function () { const globalStore = wfform.getGlobalStore(); if (globalStore && globalStore.rightMenu && globalStore.rightMenu.lastnodeid) { WfForm.controlBtnDisabled(true); alert("注意:退回流程禁止再次提交!"); } // 拦截提交,增加一层按钮置灰保险 WfForm.registerCheckEvent(WfForm.OPER_SUBMIT, function (callback) { if (globalStore.rightMenu.lastnodeid) { WfForm.controlBtnDisabled(true); alert("注意:当前节点不允许再次提交!"); return; } callback(); }); }); ``` 另一个方案:wfform.getGlobalStore().promptInfos 判断其包含退回也可以,但是没有rightMenu.lastnodeid方案优雅~ --- # [泛微 Ecology9] 建模查询动态列测试(半成品) 作者:丸子·张 来源:https://blog.oneszhang.com/2025/01/15/132/ Markdown:https://blog.oneszhang.com/2025/01/15/132.md 发布日期:2025-01-15T06:41:00.000Z 更新时间:2025-01-15T06:41:00.000Z 专栏:泛微 E9 标签: 摘要:该示例展示在泛微 Ecology ecode 中通过覆盖 Table 组件属性,实现建模查询表格按年月动态生成列的效果。核心做法是识别指定查询路径后,过滤排序年月数据,将业务数据转置,再动态替换表头与数据源。代码为半成品,需读者按实际场景调整。 # 效果图 **原始数据:** ![image.png](https://cos.oneszhang.com/usr/uploads/2025/01/926911956.png) **开启ecode后展示样式:** ![2025-01-15T06:40:27.png][1] #代码: 注:本代码为半成品,请自行修改 ``` ecodeSDK.overwritePropsFnQueueMapSet('Table', { fn: (newProps) => { console.log('!找到组件啦!!!!!!'); // 检查路径是否正确 if (!ecodeSDK.checkLPath('/spa/cube/index.html#/main/cube/search?customid=77')) return; console.log("路径识别正确!", newProps); // 数据过滤和排序 const validDataSource = newProps.dataSource.filter(row => row.ny && row.ny !== "未定义"); // 提取所有有效的年月并排序 const nyValues = Array.from(new Set(validDataSource.map(row => row.ny))) .filter(ny => ny) // 过滤未定义的年月 .sort((a, b) => new Date(a) - new Date(b)); // 按时间升序排序 console.log("有效的年月字段:", nyValues); // 数据转置处理 const processedData = {}; validDataSource.forEach(row => { const cswb = row.cswb || "未定义"; const ny = row.ny; const csxs = row.csxs || 0; if (!processedData[cswb]) { processedData[cswb] = { cswb }; } processedData[cswb][ny] = csxs; }); // 转换为数组形式,便于表格展示 const transformedData = Object.values(processedData); // 动态生成新的表头 const dynamicColumns = [ { title: "测试文本", dataIndex: "cswb", key: "cswb", width: "20%" }, ...nyValues.map(ny => ({ title: ny, dataIndex: ny, key: ny, width: "10%" })) ]; // 更新表格的列配置和数据源 newProps.columns = dynamicColumns; newProps.dataSource = transformedData; console.log("处理后的数据:", transformedData); console.log("处理后的列:", dynamicColumns); }, }); ``` [1]: https://cos.oneszhang.com/usr/uploads/2025/01/1694766449.png --- # [泛微 Ecology9] 拖拽上传附件功能拖拽显示优化 作者:丸子·张 来源:https://blog.oneszhang.com/2024/12/27/130/ Markdown:https://blog.oneszhang.com/2024/12/27/130.md 发布日期:2024-12-27T03:41:00.000Z 更新时间:2024-12-27T03:41:00.000Z 专栏:泛微 E9 标签: 摘要:针对泛微 OA Ecology 中 ecode 拖拽上传附件缺少视觉反馈、用户易误以为不支持拖拽的问题,这里通过改写 index.js 做显示优化:用 React 组件包裹 WeaUpload,监听拖拽进入、悬停与离开事件切换状态,拖拽时显示蓝色虚线边框和高亮遮罩提示上传附件,适合 ecode 定制开发参考。 ![image.png](https://cos.oneszhang.com/usr/uploads/2024/12/3194928374.png) 由于原有的拖拽附件功能ecode用户拖拽上传附件时无视觉效果,存在导致无法上传或没有拖拽上传功能的错觉。 故对该功能进行显示效果优化! ![image.png](https://cos.oneszhang.com/usr/uploads/2024/12/2121456771.png) 原有的index.js改为以下内容即可: `index.js` ``` const { WeaUpload } = ecCom; class NewWeaUpload extends React.Component { hash = `${new Date().getTime()}_${Math.ceil(Math.random() * 100000)}`; state = { isDragOver: false }; handleDragOver = (e) => { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; this.setState({ isDragOver: true }); }; handleDragEnter = (e) => { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; }; handleDragLeave = (e) => { e.preventDefault(); this.setState({ isDragOver: false }); }; handleDrop = (e) => { e.preventDefault(); this.setState({ isDragOver: false }); }; render() { const { uploadId } = this.props; const { isDragOver } = this.state; const drop_element = `weaUploadDragDrop_${uploadId}_${this.hash}`; const newProps = { ...this.props, customOptions: { ...this.props.customOptions, drop_element, } }; const dragAreaStyle = { width: '100%', minHeight: '32px', border: '2px dashed transparent', borderRadius: '4px', transition: 'all 0.3s', cursor: 'default', position: 'relative', ...(isDragOver && { border: '2px dashed #1890ff', backgroundColor: 'rgba(24, 144, 255, 0.1)', }) }; const overlayStyle = { display: isDragOver ? 'flex' : 'none', position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, backgroundColor: 'rgba(24, 144, 255, 0.1)', justifyContent: 'center', alignItems: 'center', fontSize: '14px', color: '#1890ff', pointerEvents: 'none', zIndex: 1 }; return (
上传附件
) } } ecodeSDK.setCom('${appId}', 'NewWeaUpload', NewWeaUpload); ``` --- # [泛微 Ecology9] [ 精华 ] 流程浏览按钮自定义参数自定义sql筛选 作者:丸子·张 来源:https://blog.oneszhang.com/2024/12/26/127/ Markdown:https://blog.oneszhang.com/2024/12/26/127.md 发布日期:2024-12-26T06:14:00.000Z 更新时间:2024-12-26T06:14:00.000Z 专栏:泛微 E9 标签: 摘要:在泛微 Ecology E9 二次开发中,可借助流程浏览按钮的自定义参数 spellSqlWhere,通过 WfForm.appendBrowserDataUrlParam 向取数接口动态注入 SQL 条件,实现按流程节点、表单字段值等过滤浏览数据,文中给出了按节点和审批状态筛选的实例。该方式灵活易用,但仅适用于流程浏览框,作者建议配合 datas 查询 SQL 的拦截方案使用,否则参数容易失效。 在日常的泛微 E9 二次开发中,我们常常需要对表单中的流程浏览按钮做数据过滤以满足业务需求。泛微提供了一个非常强大的自定义参数 `spellSqlWhere`,可以通过它实现基于 SQL 的动态筛选功能。本文将详细介绍这一功能,并分享一个实用的解决方案。 --- ## 先亮方案!注意:仅限流程浏览框 > ⚠️ **注意事项** > > 建议配合datas查询的sql使用:[api/ec/dev/table/getxml 拦截后解决方案](https://oneszhang.com/archives/97.html),否则你的参数很容易无效~ > 在流程中,我们可以通过 `WfForm.appendBrowserDataUrlParam` API 方法扩展浏览按钮取数接口的参数值: ```javascript WfForm.appendBrowserDataUrlParam("field6541", { "spellSqlWhere": "and workflow_requestbase.currentnodeid = 202" }); ``` ![2024-12-26T06:12:49.png][1] 上述代码的核心是利用 `spellSqlWhere` 参数直接向 SQL 查询中拼接条件,非常适合实现基于表单动态条件的数据过滤。 --- ## 背景分析 ### 关键代码解析 在研究过程中,发现了泛微的部分源码逻辑: ```java private String getFromSql(Map var1, List var2) { String var3 = (String)var2.get(0); String var4 = (String)var2.get(1); StringBuilder var5 = new StringBuilder(); var5.append(" where 1=1 ").append(Util.null2String(var1.get("spellSqlWhere"))); } ``` 从这段代码可以得出以下几个重要信息: 1. **拼接逻辑** : * `spellSqlWhere` 参数的值会被直接拼接到 SQL 查询的 `WHERE` 子句中。 * `Util.null2String(var1.get("spellSqlWhere"))` 的作用是将传入的 `spellSqlWhere` 参数安全地拼接到 `WHERE` 子句中,避免空值引发异常。 2. **来源** : * `var1` 是请求的参数 Map。 * 浏览按钮通过请求参数 `spellSqlWhere` 动态传入 SQL 条件。 3. **动态扩展** : * 通过向参数中添加 `spellSqlWhere`,可以为浏览按钮注入自定义 SQL 条件,从而动态控制浏览按钮的数据展示。 --- ### 功能要点 `spellSqlWhere` 的核心功能是基于 SQL 的动态筛选。通过将表单上的条件转换为 SQL 语句拼接到查询中,开发者可以快速实现复杂的过滤需求。 ### 使用场景 1. **按流程节点过滤** : 在工作流中,不同节点需要查看不同的数据。例如,当前节点只允许看到与当前流程状态相关的数据。 2. **基于表单字段动态过滤** : 根据用户在表单中填写的值,实时调整浏览按钮的数据范围。 3. **复杂条件查询** : 对接外部系统或处理复杂的业务逻辑时,通过自定义 SQL 条件过滤数据。 --- ## 实现步骤 ### 1. 定义浏览按钮并关联字段 在 E9 中设计表单,添加一个浏览按钮,并绑定一个字段(如 `field6541`)。 ### 2. 使用 `WfForm.appendBrowserDataUrlParam` 扩展参数 在工作流节点脚本中,使用 `WfForm.appendBrowserDataUrlParam` 方法向浏览按钮添加 `spellSqlWhere` 参数。例如: ```javascript WfForm.appendBrowserDataUrlParam("field6541", { "spellSqlWhere": "and workflow_requestbase.currentnodeid = 202" }); ``` ### 3. 动态构建 SQL 条件 根据业务需要,可以动态拼接不同的 SQL 条件。例如,基于表单字段值动态筛选: ```javascript var condition = "and workflow_requestbase.requestid = " + WfForm.getFieldValue("field1234"); WfForm.appendBrowserDataUrlParam("field6541", { "spellSqlWhere": condition }); ``` ### 4. 测试并验证 通过实际操作表单,测试浏览按钮的数据是否按预期过滤。例如: * 当前节点是否只显示与当前节点 ID 匹配的数据。 * 表单字段值是否正确过滤浏览数据。 --- ## 实际案例 ### 场景描述 在一个审批流程中,需要在表单中添加一个浏览按钮,过滤出当前流程节点中未审批的流程请求。 ### 解决方案 1. 表单中添加浏览按钮,绑定字段 `field6541`。 2. 在节点脚本中添加以下代码: ```javascript WfForm.appendBrowserDataUrlParam("field6541", { "spellSqlWhere": "and workflow_requestbase.currentnodeid = " + WfForm.getFieldValue("currentNodeId") + " and workflow_requestbase.requeststatus = 0" }); ``` 3. 动态拼接 SQL 条件: * `currentnodeid`:当前节点 ID。 * `requeststatus = 0`:未审批状态。 ### 结果 浏览按钮只显示符合当前节点且未审批的请求数据,大大提高了用户体验和操作效率。 --- ## 总结 `spellSqlWhere` 是一个非常灵活的自定义浏览按钮参数,开发者可以利用它轻松实现复杂的动态 SQL 筛选需求。通过 `WfForm.appendBrowserDataUrlParam` 方法,可以为浏览按钮动态注入 SQL 条件,适用于各种复杂业务场景。 **优点:** * 灵活性高:基于 SQL 动态拼接,几乎没有筛选限制。 * 易于实现:通过简单的 API 调用即可完成。 通过合理利用 `spellSqlWhere`,我们可以显著增强泛微 E9 的浏览按钮功能,让表单更加智能和高效! [1]: https://cos.oneszhang.com/usr/uploads/2024/12/3810777226.png --- # pnpm安装sharp报错处理办法 作者:丸子·张 来源:https://blog.oneszhang.com/2024/12/25/125/ Markdown:https://blog.oneszhang.com/2024/12/25/125.md 发布日期:2024-12-25T09:57:04.000Z 更新时间:2024-12-25T09:57:04.000Z 专栏:前端与脚本 标签: 摘要:文章记录了 pnpm 安装 sharp 时因无法从 GitHub 下载 libvips 二进制包而报错的问题,典型表现是安装脚本执行失败并提示连接被拒绝。解决办法是通过 pnpm config 将 sharp 及其 libvips 的二进制下载地址改为 npmmirror 国内镜像源,适合国内网络环境下遇到同类依赖安装失败的开发者参考。 # 报错内容如下: ```bash PS F:\myproject> pnpm i WARN  4 deprecated subdependencies found: @esbuild-kit/core-utils@3.3.2, @esbuild-kit/esm-loader@2.6.5, glob@7.2.3, inflight@1.0.6 Packages: +946 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Progress: resolved 1045, reused 942, downloaded 5, added 946, done node_modules/.pnpm/esbuild@0.23.1/node_modules/esbuild: Running postinstall script, done in 660ms node_modules/.pnpm/esbuild@0.18.20/node_modules/esbuild: Running postinstall script, done in 534ms node_modules/.pnpm/esbuild@0.19.12/node_modules/esbuild: Running postinstall script, done in 508ms node_modules/.pnpm/sharp@0.32.6/node_modules/sharp: Running install script, failed in 348ms .../sharp@0.32.6/node_modules/sharp install$ (node install/libvips && node install/dll-copy && prebuild-install) || (node install/can-compile && node-gyp rebuild && node install/dll-copy) │ sharp: Downloading https://github.com/lovell/sharp-libvips/releases/download/v8.14.5/libvips-8.14.5-win32-x64.tar.br │ sharp: Please see https://sharp.pixelplumbing.com/install for required dependencies └─ Failed in 348ms at F:\myproject\node_modules\.pnpm\sharp@0.32.6\node_modules\sharp  ELIFECYCLE  Command failed with exit code 1.  WARN  4 deprecated subdependencies found: @esbuild-kit/core-utils@3.3.2, @esbuild-kit/esm-loader@2.6.5, glob@7.2.3, inflight@1.0.6 Packages: +946 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ .../sharp@0.32.6/node_modules/sharp install$ (node install/libvips && node install/dll-copy && prebuild-install) || (node install/can-compile && node-gyp rebuild && node install/dll-copy) │ sharp: Installation error: connect ECONNREFUSED 127.0.0.1:443 │ sharp: Please see https://sharp.pixelplumbing.com/install for required dependencies └─ Failed in 326ms at F:\myproject\node_modules\.pnpm\sharp@0.32.6\node_modules\sharp node_modules/.pnpm/esbuild@0.19.12/node_modules/esbuild: Running postinstall script, done in 275ms node_modules/.pnpm/esbuild@0.18.20/node_modules/esbuild: Running postinstall script, done in 293ms  ELIFECYCLE  Command failed with exit code 1. ``` # 解决方案: ```bash pnpm config set sharp_binary_host=https://npmmirror.com/mirrors/sharp pnpm config set sharp_libvips_binary_host=https://npmmirror.com/mirrors/sharp-libvips ``` --- # [泛微 Ecology9 Java] 流程强制归档方法 作者:丸子·张 来源:https://blog.oneszhang.com/2024/12/24/124/ Markdown:https://blog.oneszhang.com/2024/12/24/124.md 发布日期:2024-12-24T06:04:00.000Z 更新时间:2024-12-24T06:04:00.000Z 专栏:泛微 E9 标签: 摘要:泛微 E9 二次开发中可通过专门类手动触发流程强制归档,先设置签字意见,再传入请求 ID 列表和当前用户执行归档并返回结果。适合需用 Java 代码归档以便调试或扩展功能的场景,示例还涉及当前用户与请求参数的校验,注意参数缺失时的处理。 在泛微 E9 系统中,流程强制归档是用于强制将待办任务归档的操作。通常,这个过程通过调用 `/api/workflow/monitor/archiving` 接口并传输 `multiRequestIds` 参数来实现。然而,在实际开发中,我们有时需要通过 Java 代码手动触发归档过程,便于调试和自定义功能。 ### WfForceOver 类 `WfForceOver` 类是一个专门用于实现流程强制归档的类,它提供了两个关键方法: * **setRemark(String remark)** :用于设置强制归档时的签字意见(备注)。 * **doForceOver(List requestIds, User user)** :用于执行强制归档操作,传入多个请求 ID 和当前用户。 #### 1. setRemark 方法 该方法用于设置归档时的签字意见。如果未设置,默认会使用空字符串。 ```java public void setRemark(String remark) { this.remark = remark; // 设置归档的签字意见 } ``` #### 2. doForceOver 方法 该方法执行强制归档操作,接受一组请求 ID 和当前用户对象。它会通过传入的请求 ID 列表来归档相关流程。 ```java public void doForceOver(ArrayList requestids, User user) { doForceOver(requestids, null, null, user); } ``` 还有一个doForceOver重载: ```java public void doForceOver( ArrayList requestids, HttpServletRequest request, HttpServletResponse response) { doForceOver(requestids, request, response, null); } ``` ### 示例代码 下面是一个简化版的代码示例,展示了如何使用 `WfForceOver` 类来进行流程强制归档操作: ```java // 获取当前用户 User user = HrmUserVarify.getUser(request, response); if (user == null) { response.sendRedirect("/login/Login.jsp"); return; } // 获取请求参数 String requestIds = request.getParameter("requestIds"); if (requestIds == null || requestIds.trim().isEmpty()) { response.setStatus(HttpServletResponse.SC_BAD_REQUEST); out.print("{\"status\":\"0\", \"message\":\"Missing requestIds parameter\"}"); return; } // 获取签字意见(如果有) String remark = request.getParameter("remark") == null ? "" : request.getParameter("remark"); // 创建 WfForceOver 对象并设置签字意见 WfForceOver wfOver = new WfForceOver(); wfOver.setRemark(remark); // 创建 requestIds 列表并调用 doForceOver 方法 List requestIdsList = Arrays.asList(requestIds.split(",")); wfOver.doForceOver(requestIdsList, user); // 返回操作结果 out.print("{\"status\":\"1\", \"result\":\"Archiving process triggered successfully.\"}"); ``` ### 总结 通过 `WfForceOver` 类的 `setRemark` 和 `doForceOver` 方法,我们可以轻松实现流程的强制归档操作。在实际应用中,您只需要传入请求 ID 列表和用户对象,即可完成归档。此外,可以通过 `setRemark` 方法为归档添加备注,以便后续查看和审计。 --- # [泛微 Ecology9] 浏览按钮自定义单选字段增加添加数据“+”按钮 作者:丸子·张 来源:https://blog.oneszhang.com/2024/12/20/121/ Markdown:https://blog.oneszhang.com/2024/12/20/121.md 发布日期:2024-12-20T07:13:00.000Z 更新时间:2024-12-20T07:13:00.000Z 专栏:泛微 E9 标签: 摘要:针对泛微 E9 流程中浏览按钮无法直接新增数据的痛点,文章介绍了通过 ecodeSDK 的 overwritePropsFnQueueMapSet 接口覆盖 WeaBrowser 组件默认属性的做法。核心步骤包括限定页面路径和目标字段、调整新建链接参数、开启 hasAddBtn 并绑定点击事件,在弹窗中提交数据后回填到表单字段。适用于 ecode 表单开发人员,需注意链接获取失败时的提示处理。 在使用泛微 E9 的流程使用过程中,可能会遇到现场增加浏览按钮数据的需求,为了注重用户体验,本文将介绍如何为浏览按钮类型的单选字段增加一个“+”号按钮,方便用户直接添加新数据。 ![image.png](https://cos.oneszhang.com/usr/uploads/2024/12/1128003130.png) ![image.png](https://cos.oneszhang.com/usr/uploads/2024/12/2966907410.png) --- #### **需求背景** 在 ecode 表单开发中,`WeaBrowser` 是一个常用的组件,它可以用来实现浏览选择功能。在某些业务场景下,用户需要在表单中直接新增一条数据并立刻选中,而默认的浏览组件并不支持这样的操作。为了满足这一需求,我们可以通过 `ecodeSDK.overwritePropsFnQueueMapSet` 接口,覆盖 `WeaBrowser` 的默认属性,并为其添加“+”按钮,同时绑定数据新增的逻辑。 --- #### **解决方案** 通过以下代码实现: ```javascript ecodeSDK.overwritePropsFnQueueMapSet('WeaBrowser', { fn: (newProps) => { // 限定功能作用的页面路径 if (!ecodeSDK.checkLPath('/spa/workflow/static4form/index.html#/main/workflow/req')) return; // 针对特定字段进行操作(使用时,你只需要改动这里!) if (newProps.fieldlabel !== '选择服务商' || newProps.viewAttr === '1') return; // 修改链接,调整 URL 参数 let { linkUrl } = newProps; if (linkUrl) { const urlParams = new URLSearchParams(linkUrl.split('?')[1]); urlParams.set('type', '1'); urlParams.delete('billid'); urlParams.set('hidetop', '1'); linkUrl = `${linkUrl.split('?')[0]}?${urlParams.toString()}`; } // 添加“+”按钮 newProps.hasAddBtn = true; // 按钮点击事件逻辑 newProps.addOnClick = () => { if (!linkUrl) { WfForm.showMessage("获取新建链接失败,请联系管理员", 2, 10); return; } // 定义弹窗及按钮交互逻辑 const { Button } = antd; const params = { title: newProps.title, moduleName: 'workflow', style: { width: 600, height: 400 }, buttons: [ // 确定按钮,提交数据并回填到表单 , // 取消按钮,关闭弹窗 , ], }; // 显示弹窗 window.weaJs.showDialog(linkUrl, params); }; }, }); ``` --- #### **代码详解** 1. **功能限制作用范围** ```javascript if (!ecodeSDK.checkLPath('/spa/workflow/static4form/index.html#/main/workflow/req')) return; ``` 通过 `ecodeSDK.checkLPath` 方法,限定功能只在特定页面生效,避免对其他页面造成干扰。 2. **目标字段判断** ```javascript if (newProps.fieldlabel !== '选择服务商' || newProps.viewAttr === '1') return; ``` 通过 `fieldlabel` 和 `viewAttr` 属性判断,只对特定的字段“选择服务商”进行增强。 3. **动态调整链接参数** ```javascript const urlParams = new URLSearchParams(linkUrl.split('?')[1]); urlParams.set('type', '1'); urlParams.delete('billid'); urlParams.set('hidetop', '1'); linkUrl = `${linkUrl.split('?')[0]}?${urlParams.toString()}`; ``` 根据业务需要修改链接参数,例如移除 `billid`,添加 `type=1` 等。 4. **添加“+”按钮** ```javascript newProps.hasAddBtn = true; newProps.addOnClick = () => { ... }; ``` 设置 `hasAddBtn` 属性为 `true`,为组件显示“+”按钮,并通过 `addOnClick` 实现按钮点击事件。 5. **弹窗逻辑** 使用 `window.weaJs.showDialog` 方法,创建一个带有“确定”和“取消”按钮的弹窗。用户点击“确定”按钮后,通过以下逻辑提交数据并回填: ```javascript diaiframe.contentWindow.ModeForm.doCardSubmit( diaiframe.contentWindow.ModeForm.getCardSubmitExpendId(), '0', '', false, function (billid) { WfForm.changeFieldValue(newProps.fieldName, { value: billid, specialobj: [{ id: billid, name: billid }], }); parentDialog.hide(); } ); ``` 6. **错误处理** 当链接无效时,弹出提示信息: ```javascript WfForm.showMessage("获取新建链接失败,请联系管理员", 2, 10); ``` --- #### **效果展示** 1. 在表单中,点击“选择服务商”字段右侧的“+”按钮。 2. 弹出数据新增窗口,用户可以输入新数据。 3. 提交后,新增数据会自动回填到“选择服务商”字段中。 --- #### **总结** 通过简单的代码增强,我们为 `WeaBrowser` 添加了一个实用的功能,使用户能够更加方便地添加和选择数据。这种方式不仅提升了用户体验,还避免了频繁跳转页面的麻烦。希望本文对大家在泛微 ecode 开发中有所帮助! --- # [泛微 Ecology9] 流程节点Html模板检查一致性比对 作者:丸子·张 来源:https://blog.oneszhang.com/2024/12/19/119/ Markdown:https://blog.oneszhang.com/2024/12/19/119.md 发布日期:2024-12-19T01:41:00.000Z 更新时间:2024-12-19T01:41:00.000Z 专栏:泛微 E9 标签: 摘要:针对泛微 E9 流程节点 HTML 模板数量多、人工比对费时的问题,介绍了一个基于 ecodeSDK 的一致性比对小工具:在节点页面注入按钮,调用接口提取模板与脚本,用 Map 去重并生成彩色标签,直观标出相同模板与脚本的节点分组。适合 OA 工程师日常维护流程时快速排查模板差异,文中还修复了 Table 组件污染问题。 # 你的泛微 E9 流程节点 HTML 模板同步小助手! 在日常 OA 系统管理中,随着流程节点数量的增多,管理和同步这些节点的 HTML 模板常常成为一项耗时又繁琐的任务。作为一名 OA 工程师,我经常遇到以下问题: - **不知道哪些节点模板完全一致**,导致反复比对浪费时间,但没有比对又不敢直接复制模板。 - **难以区分需要单独处理的模板**,管理起来不够直观。 为了高效解决这些问题,我开发了「流程节点 HTML 模板一致性比对功能」,能够快速分析流程节点的模板一致性,并用可视化的方式帮助用户理解和管理模板。今天,我将分享这一工具的实现与优化过程,希望能对大家有所帮助! ![流程节点HTML一致性比对](https://cos.oneszhang.com/usr/uploads/2024/12/3518041490.png) > ⚠️ **2024年12月19日 10:19:39代码BUG修复** > > 修复Table组件污染BUG > :::tip[2024年12月19日 10:19:39代码优化] 感谢**wintsa**佬友的修正和建议 ::: --- ## 功能背景与目标 在泛微 E9 的工作流引擎中,每个流程节点都有自己的 HTML 模板和脚本,由于节点数量众多且分布复杂,**手动检查模板一致性**常常需要耗费大量时间。基于这一痛点,我设计并实现了以下功能: 1. **自动分析节点模板**:通过 API 接口提取每个节点的 HTML 模板和脚本信息。 2. **生成唯一一致性标签**:自动比对模板和脚本内容,为每个节点生成唯一的标签。 3. **颜色标识可视化**:通过颜色标识,直观展示节点模板和脚本的一致性分组。 4. **一键操作简单高效**:用户只需点击一个按钮,即可完成所有节点的模板一致性检查。 --- ## 代码实现 ### 完整代码 [hide] `index.css(前置加载)` ``` .customflow-tag-container { display: flex; gap: 4px; margin-top: 1px; } .customflow-tag { display: inline-block; padding: 2px 8px; border-radius: 12px; font-size: 12px; color: #fff; white-space: nowrap; text-align: center; } ``` `register.js(前置加载)` ``` let tableProps = {}; let workflowId; const generatePastelColor = () => { const hue = Math.floor(Math.random() * 360); const lightness = Math.random() * (60 - 30) + 30; return `hsl(${hue}, 70%, ${lightness}%)`; }; const customCheck = () => { return ecodeSDK.checkLPath('/wui/engine.html#/workflowengine/path/pathSet/pathDetail/flowSet/nodeInfo'); }; const fetchModeInfo = async (nodeid) => { try { const response = await fetch(`/api/workflow/nodeformedit/getModeInfo?workflowid=${workflowId}&nodeid=${nodeid}`); return response.json(); } catch { return null; } }; const fetchDesigner = async (modeid) => { try { const response = await fetch('/api/workflow/wfexceldesign/doLoadDesigner', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ layoutid: modeid }).toString(), }); return response.json(); } catch { return null; } }; const processNodeData = async (dataSource) => { const templateMap = new Map(); const jsMap = new Map(); const colorMap = new Map(); function removeSuffix(str, suffixLength) { if (str.length <= suffixLength) { return ''; } return str.slice(0, str.length - suffixLength); } try { await Promise.all( dataSource.map(async (node) => { const nodeid = node._node_2; const modeInfo = await fetchModeInfo(nodeid); if (!modeInfo || !modeInfo.html || modeInfo.html.modeid === 0) { return; } const Designer = await fetchDesigner(modeInfo.html.modeid); const { layoutInfo } = Designer; if (!layoutInfo || !layoutInfo.pluginjson) { return; } const templateKey = layoutInfo.pluginjson; if (!templateMap.has(templateKey)) { templateMap.set(templateKey, `模板${templateMap.size + 1}`); colorMap.set(`模板${templateMap.size}`, generatePastelColor()); } const templateTag = templateMap.get(templateKey); const jsKey = removeSuffix(layoutInfo.scripts, 33) || '无脚本'; if (!jsMap.has(jsKey)) { jsMap.set(jsKey, `JS${jsMap.size + 1}`); colorMap.set(`JS${jsMap.size}`, generatePastelColor()); } const jsTag = jsMap.get(jsKey); const container = document.createElement('div'); container.innerHTML = node._node_2span || ''; const existingTags = container.querySelector('.customflow-tag-container'); if (existingTags) { existingTags.remove(); } const newTags = `
${templateTag}
${jsTag}
`; container.innerHTML += newTags; // 更新节点内容 node._node_2span = container.innerHTML; }) ); return { success: true }; } catch { return { success: false }; } }; const handleComparison = async () => { if (!tableProps) { antd.message.warn('获取节点信息数据失败,请刷新页面后重试!'); return; } const dataSourceCopy = JSON.parse(JSON.stringify(tableProps.dataSource)); tableProps.loading = true; const result = await processNodeData(dataSourceCopy); if (result.success) { tableProps.dataSource = [...dataSourceCopy]; tableProps.onChange && tableProps.onChange([...dataSourceCopy]); antd.message.success('模板一致比对完成!'); } else { antd.message.error('比对过程中发生错误,请稍后重试!'); } tableProps.loading = false; }; ecodeSDK.overwritePropsFnQueueMapSet('WeaTop', { fn: (newProps) => { if (!customCheck()) { return newProps; } if (!workflowId) { workflowId = window.weaJs.getFrameParams().workflowId; } let { buttons } = newProps; buttons = buttons || []; const { Button } = antd; buttons.push( ); return newProps; }, }); ecodeSDK.overwritePropsFnQueueMapSet('Table', { fn: (newProps) => { if (!customCheck()) return; if (!newProps.rootMap || newProps.rootMap.tabletype !== 'none' || newProps.rootMap.pageId !== 'wf:nodeinfolist') return; const currentWorkflowId = window.weaJs.getFrameParams().workflowId; if (workflowId !== currentWorkflowId) { workflowId = currentWorkflowId; tableProps = null; } if (tableProps && tableProps.dataSource && tableProps.dataSource.length > 0) { if (!newProps.dataSource) { newProps.dataSource = []; } tableProps.dataSource.forEach((item, index) => { if (newProps.dataSource[index]) { newProps.dataSource[index]._node_2span = item._node_2span; } }); } tableProps = { ...newProps }; return newProps; }, }); ``` [/hide] ## 核心功能实现 ### 1. 自定义路径校验,限定功能适用范围 为了确保该功能仅在特定页面生效,我设计了路径校验逻辑 `customCheck`,仅当用户在特定的工作流节点页面中时才启用功能。 ```javascript const customCheck = () => { return ecodeSDK.checkLPath('/wui/engine.html#/workflowengine/path/pathSet/pathDetail/flowSet/nodeInfo'); }; ``` ### 2. 数据获取与处理 通过两个 API 接口分别获取节点的 HTML 模板信息和脚本设计信息: - **获取 HTML 模板信息**:调用 `/api/workflow/nodeformedit/getModeInfo` 接口,提取模板的基本信息(例如模板 ID)。 - **获取脚本设计信息**:调用 `/api/workflow/wfexceldesign/doLoadDesigner` 接口,提取模板的布局和脚本内容。 代码示例如下: ```javascript const fetchModeInfo = async (nodeid) => { try { const response = await fetch(`/api/workflow/nodeformedit/getModeInfo?workflowid=${workflowId}&nodeid=${nodeid}`); return response.json(); } catch { return null; } }; const fetchDesigner = async (modeid) => { try { const response = await fetch('/api/workflow/wfexceldesign/doLoadDesigner', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ layoutid: modeid }).toString(), }); return response.json(); } catch { return null; } }; ``` ### 3. 模板与脚本一致性比对 在获取到节点的模板和脚本信息后,我使用 `Map` 数据结构存储唯一的模板和脚本内容,并生成对应的标签和颜色标识。通过遍历所有节点,比对内容,给每个节点添加唯一的可视化标签。 - **模板与脚本的去重逻辑**:模板和脚本的内容作为 `Map` 的 key,确保重复的内容不会生成重复标签。 - **颜色标识分配**:通过 `generatePastelColor` 方法生成随机柔和颜色,帮助用户快速区分模板和脚本分组。 具体代码实现如下: ```javascript const processNodeData = async (dataSource) => { const templateMap = new Map(); const jsMap = new Map(); const colorMap = new Map(); function removeSuffix(str, suffixLength) { if (str.length <= suffixLength) { return ''; } return str.slice(0, str.length - suffixLength); } await Promise.all( dataSource.map(async (node) => { const nodeid = node._node_2; const modeInfo = await fetchModeInfo(nodeid); if (!modeInfo || !modeInfo.html || modeInfo.html.modeid === 0) { return; } const Designer = await fetchDesigner(modeInfo.html.modeid); const { layoutInfo } = Designer; if (!layoutInfo || !layoutInfo.pluginjson) { return; } const templateKey = layoutInfo.pluginjson; if (!templateMap.has(templateKey)) { templateMap.set(templateKey, `模板${templateMap.size + 1}`); colorMap.set(`模板${templateMap.size}`, generatePastelColor()); } const templateTag = templateMap.get(templateKey); const jsKey = removeSuffix(layoutInfo.scripts, 33) || '无脚本'; if (!jsMap.has(jsKey)) { jsMap.set(jsKey, `JS${jsMap.size + 1}`); colorMap.set(`JS${jsMap.size}`, generatePastelColor()); } const jsTag = jsMap.get(jsKey); const container = document.createElement('div'); container.innerHTML = node._node_2span || ''; const existingTags = container.querySelector('.customflow-tag-container'); if (existingTags) { existingTags.remove(); } const newTags = `
${templateTag}
${jsTag}
`; container.innerHTML += newTags; node._node_2span = container.innerHTML; }) ); return { success: true }; }; ``` ### 4. 一键触发比对功能 在页面顶部按钮区域,添加一个「模板一致比对」按钮,通过点击按钮即可触发模板一致性检查流程。此按钮的注入逻辑如下: ```javascript const handleComparison = async () => { if (!tableProps) { antd.message.warn('获取节点信息数据失败,请刷新页面后重试!'); return; } const dataSourceCopy = JSON.parse(JSON.stringify(tableProps.dataSource)); tableProps.loading = true; const result = await processNodeData(dataSourceCopy); if (result.success) { tableProps.dataSource = [...dataSourceCopy]; tableProps.onChange && tableProps.onChange([...dataSourceCopy]); antd.message.success('模板一致比对完成!'); } else { antd.message.error('比对过程中发生错误,请稍后重试!'); } tableProps.loading = false; }; ecodeSDK.overwritePropsFnQueueMapSet('WeaTop', { fn: (newProps) => { if (!customCheck()) { return; } let { buttons } = newProps; buttons = buttons || []; const { Button } = antd; buttons.push( ); }, }); ``` --- ## 测试与优化建议 ### 测试结果 1. **模板一致性快速识别**:节点表格中显示了颜色标记的模板和脚本标签,相同模板的节点被分配了相同的颜色,直观明了。 2. **一键完成比对**:无需繁琐的手动操作,点击按钮即可完成所有节点的模板检查。 3. **高效而稳定**:在节点数量较多的情况下,仍能保持较好的响应速度。 --- 希望这篇分享能够帮助到更多的开发者,简化泛微 E9 流程节点模板管理的复杂度。如果大家有任何建议或问题,欢迎随时交流! --- # [泛微 Ecology9] Table 组件刷新 data 数据尝试 作者:丸子·张 来源:https://blog.oneszhang.com/2024/12/18/118/ Markdown:https://blog.oneszhang.com/2024/12/18/118.md 发布日期:2024-12-18T15:52:00.000Z 更新时间:2024-12-18T15:52:00.000Z 专栏:泛微 E9 标签: 摘要:这篇文章记录了在泛微 ecode 中动态刷新 Table 组件数据的一次尝试。作者通过复写 ecodeSDK 的组件属性传递函数,先捕获 Table 的 props 存入全局变量,再向 WeaTop 注入自定义按钮,点击后修改表格 datas 并调用 onChange 强制刷新,并用路径校验限定生效范围。方案在指定流程节点路径下验证有效,但作者也指出直接改数据、依赖全局变量存在不够优雅和版本兼容风险,期待更标准的实现思路。 在开发泛微 ecode 的某些应用场景中,遇到需要动态刷新 `Table` 组件数据的问题时,我尝试了一种折中的解决方案。希望通过这篇博客,引发更多关于 `ecode` 平台组件复写和数据操作的技术讨论,并期待 ecode 技术大佬给予我指导和优化建议。😊 --- ## 背景问题 在某些场景下,例如动态更新 `Table` 组件的数据或修改现有表格中的字段展示逻辑,我希望通过简单的按钮点击触发数据更新,并立即在前端展示出修改后的结果。然而,直接对 `ecode` 的内置组件(如 `Table`)进行修改存在一定限制,常规方法可能无法直接生效。 ## 折中解决方案 以下是当前使用的折中方案,通过复写 `ecodeSDK` 的组件属性传递函数 (`overwritePropsFnQueueMapSet`) 和注入自定义按钮来实现表格数据的更新。 --- ### 代码实现 :::details[完整代码] ``` let tableProps; // 自定义路径校验函数 const customCheck = () => { return ecodeSDK.checkLPath('/wui/engine.html#/workflowengine/path/pathSet/pathDetail/flowSet/nodeInfo'); }; // 数据更新函数 const updateTableData = () => { const { message } = antd; if (!tableProps) return message.warn('操作失败,请联系管理员!'); // 注:更新哪个都一样~ // tableProps.dataSource[0]._node_2span = `8.Html模式`; tableProps.datas[0]._node_2span = `8.Html模式`; tableProps.onChange(); antd.message.success('表格数据更新完成!'); }; // 复写 WeaTop 组件 增加按钮 ecodeSDK.overwritePropsFnQueueMapSet('WeaTop', { fn: (newProps) => { if (!customCheck()) return; let { buttons } = newProps; buttons = buttons || []; const { Button } = antd; // 注入按钮,用于触发表格数据更新 buttons.push( 测试更新表格数据 ); }, }); // 复写 Table 组件 ecodeSDK.overwritePropsFnQueueMapSet('Table', { fn: (newProps) => { if (!customCheck()) return; if (tableProps && tableProps.dataSource && tableProps.dataSource.length > 0) { newProps = tableProps; return newProps; } tableProps = newProps; console.log("Captured Table Props:", newProps); }, }); ``` ::: #### 自定义路径校验 首先定义了一个路径校验函数 `customCheck`,用于确保仅在特定路径下应用逻辑,避免对其他页面的干扰: ```javascript const customCheck = () => { return ecodeSDK.checkLPath('/wui/engine.html#/workflowengine/path/pathSet/pathDetail/flowSet/nodeInfo'); }; ``` #### 表格数据更新逻辑 在表格数据更新的核心函数 `updateTableData` 中,直接修改了表格的 `datas` 属性,并通过调用表格的 `onChange()` 方法强制触发刷新: ```javascript const updateTableData = () => { const { message } = antd; if (!tableProps) return message.warn('操作失败,请联系管理员!'); // 更新表格数据 tableProps.datas[0]._node_2span = `8.Html模式`; tableProps.onChange(); antd.message.success('表格数据更新完成!'); }; ``` > ⚠️ **注意** :在实际更新中,我尝试过直接修改 `dataSource` 和 `datas`,结果是效果一致。 #### WeaTop 组件注入按钮 通过复写 `WeaTop` 组件的属性,注入了一个自定义按钮来触发表格数据更新逻辑: ```javascript ecodeSDK.overwritePropsFnQueueMapSet('WeaTop', { fn: (newProps) => { if (!customCheck()) return; let { buttons } = newProps; buttons = buttons || []; const { Button } = antd; buttons.push( ); }, }); ``` #### Table 组件复写与捕获 Props 最后,通过复写 `Table` 组件的属性函数,捕获了表格的 `props`,并将其保存到全局变量中以便后续操作: ```javascript ecodeSDK.overwritePropsFnQueueMapSet('Table', { fn: (newProps) => { if (!customCheck()) return; if (tableProps && tableProps.dataSource && tableProps.dataSource.length > 0) { newProps = tableProps; return newProps; } tableProps = newProps; console.log("Captured Table Props:", newProps); }, }); ``` --- ## 测试结果 在路径 `/workflowengine/path/pathSet/pathDetail/flowSet/nodeInfo` 下验证了以上逻辑: 1. 页面加载时,`Table` 组件的 `props` 成功被捕获; 2. 点击自定义按钮后,表格数据被成功更新,页面即时反映了数据的变化; 3. 用户操作无明显延迟,效果符合预期。 --- ## 存在的问题 1. **更新方式不够优雅** :直接操作 `datas` 或 `dataSource` 并调用 `onChange()` 强制刷新,可能并非最佳实践; 2. **依赖全局变量 `tableProps`** :对 `Table` 组件的 `props` 捕获逻辑较为直接,可能需要更优雅的管理方式; 3. **兼容性与风险** :由于是复写组件逻辑,需关注 ecode 版本变更可能带来的潜在影响。 --- ## 总结与求助 本次尝试是一个折中的解决方案,尽管实现了动态更新 `Table` 数据的功能,但仍有许多值得优化的地方。例如: * 是否有更优雅的方式管理组件 `props`? * `Table` 数据更新是否可以通过更标准的方式触发? 希望有更多的 ecode 技术大佬参与讨论,分享更好的实现方案,或对我的方案提出改进建议。 --- # [泛微 Ecology9] 常用日志目录记录 作者:丸子·张 来源:https://blog.oneszhang.com/2024/12/17/117/ Markdown:https://blog.oneszhang.com/2024/12/17/117.md 发布日期:2024-12-17T02:06:00.000Z 更新时间:2024-12-17T02:06:00.000Z 专栏:泛微 E9 标签: 摘要:整理泛微 Ecology 9 常用日志的目录、用途与命名规则,涵盖 SQL 日志、系统运行日志、Resin 日志、安全包日志、移动端与云桥日志等。文章还按启动运行、页面 500/403 及性能宕机等场景给出取日志清单,并说明管理员日志访问入口,便于运维人员按问题类型快速定位。 # 1. 系统常用日志目录 | 日志类型 | 目录路径 | 说明 | |-----------------------|--------------------------------|-----------------------------------------| | **sqllog日志** | `ecology/WEB-INF/sqllog` | 记录RecordSet和RecordSetTrans执行的SQL记录。历史日志压缩存储。 | | **ecology日志** | `ecology/log` | 包含运行时日志、内存日志、线程日志、SQL告警日志等。 | | **Resin启动日志** | `Resin/log` | 包含 `jvm-default.log`、`stderr.log`、`stdout.log` 启动和运行日志。 | | **运维平台日志** | `Resin/monitor/resin/log` | 运维平台启停日志和运行过程日志。 | | **性能监控日志** | `Resin/monitor/resin/app/data` | 记录监控日志,包含性能和宕机信息。 | | **安全包日志** | `ecology/log/WEB-INF/securitylog` | 安全拦截日志和运行时错误日志。 | | **集成日志** | `ecology/log/integration` | 包含系统集成相关日志。 | | **稳定包日志** | `ecology/log/mem`、`thread`、`sql`等 | 包括内存、线程、SQL告警、重启信息及数据库连接泄露日志等。 | | **EMobile7日志** | `\emp\work\logs\applog\emobille.log` | 记录EMobile7相关应用和消息日志。 | | **EMobile6日志** | `EMobile\webapps\ROOT\WEB-INF\logs` | 记录EMobile6相关日志。 | | **云桥日志** | `ebridge\tomcat\webapps\ROOT\log` | 包括`weixin.log` 和 `catalina.out`日志。 | | **Esearch日志** | `/Esearch2/log`、`/ESearch2/webapps` | 记录Esearch搜索日志。 | --- # 2. 日志详细说明 ### **1. sqllog日志** - **路径**:`ecology/WEB-INF/sqllog` - **命名规则**: - 当前日志:`ecologysql` - 历史日志:`yyyy-MM-dd hh-mm-ss.log` - **存储方式**:每小时对未压缩日志进行压缩,存储至 `/ecology/sqllog/yyyy/MM/dd/yyyy-MM-dd hh-mm-ss.zip` ### **2. ecology日志** - **路径**:`ecology/log` - **内容说明**: - **运行日志**:系统运行主要日志。 - **内存日志**:`/ecology/log/mem/memory_yyyy-MM-dd.log` - **线程日志**:`/ecology/log/thread/yyyy-MM-dd/` - **SQL告警日志**: - 查询量大日志:`sqlcount_yyyy-MM-dd.log` - 执行时间长日志:`sqltime_yyyy-MM-dd.log` - **重启信息日志**:`/ecology/log/status/status_yyyy-MM-dd.log` - **数据库连接泄露日志**:`/ecology/log/conn/conn_yyyy-MM-dd.log` ### **3. Resin日志** - **路径**:`/weaver/Resin/log` - **日志文件**: - 当前日志:`stdout.log`、`stderr.log`、`jvm-default.log`、`watchdog-manager.log` - 历史日志:`stdout-yyyyMMdd.log.gz`、`stderr-yyyyMMdd.log.gz` ### **4. 运维平台日志** - **路径**:`Resin/monitor/resin/log` - **说明**:同Resin日志,记录运维平台的启停及运行过程日志。 ### **5. 集成日志** - **路径**:`/ecology/log/integration` - **命名规则**: - 当前日志:`integration.log` - 历史日志:`integration.log_yyyymmdd.log` ### **6. 安全包日志** - **路径**:`ecology/log/WEB-INF/securitylog` - **日志文件**: - **运行错误日志**:`systemRunInfoyyyy-MM-dd.log`(页面500报错分析) - **安全拦截日志**:`systemSecurityyyyy-MM-dd.log`(403访问报错分析) ### **7. EMobile日志** - **EMobile7**: - **路径**:`\emp\work\logs\applog` 和 `openfirelog` - **日志**:`emobille.log` 和 `emessage.log` - **历史日志**:`emobile.log-yyyy-MM-dd.zip` 和 `emessage.log-yyyy-MM-dd.zip` - **EMobile6**: - **路径**:`EMobile\webapps\ROOT\WEB-INF\logs` - **日志文件**: - 当前日志:`mobile.log` - 历史日志:`mobile.log.yyyy-MM-dd` ### **8. 云桥日志** - **Linux路径**:`ebridge\tomcat\webapps\ROOT\log\weixin.log` - **Windows路径**:`ebridge\tomcat\logs\` ### **9. Esearch日志** - **路径**:`/Esearch2/log` 和 `/ESearch2/webapps/ROOT/log` - **日志文件**: - 当前日志:`searchLog` - 历史日志:`searchLog_yyyyMMdd.log` --- # 3. 取日志总结 ### **非性能问题** - **启动和运行问题**: - `ecology/log/ecology` - `Resin/log/stderr.log` - `Resin/log/stdout.log` - `Resin/log/jvm-default.log` - **页面500报错**: - `ecology/log/WEB-INF/securitylog/systemRunInfoxxx.log` - **403访问报错**: - `ecology/log/WEB-INF/securitylog/systemSecurityxxx.log` ### **性能宕机问题** - **关键日志路径**: - `ecology/log/sql/sqlcount_出问题日期.log` - `ecology/log/mem/memory_出问题日期.log` - `ecology/log/thread/出问题日期` - `Resin/monitor/resin/app/data/thread/出问题日期` - `pool/出问题日期ecology.log` --- # 4. 系统管理员访问路径 - **访问地址**:`OA/getlog/` - **系统日志服务器物理路径**:`ecology/log` --- # [泛微 Ecology9] 流程右上角增加状态图签-全局 作者:丸子·张 来源:https://blog.oneszhang.com/2024/12/10/116/ Markdown:https://blog.oneszhang.com/2024/12/10/116.md 发布日期:2024-12-10T09:36:58.000Z 更新时间:2024-12-10T09:36:58.000Z 专栏:泛微 E9 标签: 摘要:介绍泛微 Ecology ecode 中为流程表单右上角添加状态图签的做法:通过 register.js 前置加载,在 Menu 的 overwritePropsFnQueueMapSet 钩子里判断请求路径后,向表单容器追加状态图片,并用 index.css 绝对定位。当前为归档流程无条件全局生效,可自行加条件扩展,图片需放入项目 resources 目录。 ### 归档流程右上角增加状态图签 当前代码为归档流程无条件全局图签,可按照自己需求增加条件拓展 ![ad78dc4ec71fe402a5680b999b1f67f.png](https://cos.oneszhang.com/usr/uploads/2024/12/2087965617.png) ### 代码 `register.js(前置加载)` ``` ecodeSDK.overwritePropsFnQueueMapSet('Menu', { fn: (newProps) => { if (!ecodeSDK.checkLPath('/spa/workflow/static4form/index.html#/main/workflow/req')) return; if (!WfForm.getGlobalStore().commonParam.currentnodetype === 3) return; var form = document.querySelector("div.wf-req-form"); if (!form) return; if (form.querySelector(".floating-status-img")) return; var statusImage = document.createElement("img"); statusImage.src = "${appRes}/shenpitongguo_1.svg"; // 图片链接 statusImage.alt = "流程状态"; statusImage.className = "floating-status-img"; form.appendChild(statusImage); }, }); ``` `index.css(前置加载)` ``` .floating-status-img { position: absolute; top: 10px; /* 距离上边缘10px */ right: 10px; /* 距离右边缘10px */ /* width: 50px; */ /* 设置图片的宽度 */ /* height: 50px; */ /* 设置图片的高度 */ z-index: 999; } ``` **shenpitongguo_1.svg** 图片上传至项目resources文件夹下 --- # [泛微 Ecology9 ecode] 待办列表自定义流程标题颜色 作者:丸子·张 来源:https://blog.oneszhang.com/2024/10/30/110/ Markdown:https://blog.oneszhang.com/2024/10/30/110.md 发布日期:2024-10-30T08:53:00.000Z 更新时间:2024-10-30T08:53:00.000Z 专栏:泛微 E9 标签: 摘要:在泛微EC9的ecode开发中,可通过重写Table组件属性来定制待办列表的流程标题样式。文章以退回流程标红为例,利用ecodeSDK遍历待办数据,识别标题中的退回标记并替换为红色加粗的HTML,实现视觉提醒。该方法适用于需要突出显示特定状态流程的OA二次开发场景,注意路径判断以确保仅对待办列表生效。 ![image.png](https://cos.oneszhang.com/usr/uploads/2024/10/3692692127.png) ## 退回标红示例代码: ``` ecodeSDK.overwritePropsFnQueueMapSet('Table', { fn: (newProps) => { if (!ecodeSDK.checkLPath('/wui/index.html#/main/workflow/listDoing')) return; console.log("路径识别正确!", newProps); // 遍历 newProps.datas 进行处理 if (newProps.datas && newProps.datas.length) { for (let i = 0; i < newProps.datas.length; i++) { let requestnamespan = newProps.datas[i].requestnamespan; // 判断 requestnamespan 是否包含 "[退回]" if (requestnamespan && requestnamespan.includes("[退回]")) { // 修改 requestnamespan 的 HTML 使其红色加粗 newProps.datas[i].requestnamespan = requestnamespan.replace( "[退回]", "[退回]" ); } } } }, }); ``` --- # Next.js项目run dev报错TypeError: fetch failed 作者:丸子·张 来源:https://blog.oneszhang.com/2024/09/20/106/ Markdown:https://blog.oneszhang.com/2024/09/20/106.md 发布日期:2024-09-20T06:15:00.000Z 更新时间:2024-09-20T06:15:00.000Z 专栏:前端与脚本 标签: 摘要:文章记录了 Next.js 项目执行 npm run dev 时出现 fetch failed 报错的排查过程,原因是旧版淘宝镜像已停止服务。作者给出通过 npm 或 yarn 将 registry 切换为新的 npmmirror 镜像的解决办法,适合遇到同类网络请求失败的 Next.js 开发者参考。 ### 今天学习尝试Next.js项目时,执行run dev报错了!找了一下原因,是因为老版本的淘宝镜像已经停用了,需要更换新的! ### 报错如下 ```bash PS F:\nextjslearn\huahan-admin\huahan-admin> npm run dev > huahan-admin@0.1.0 dev > next dev ▲ Next.js 14.2.3 ✓ Starting... TypeError: fetch failed at node:internal/deps/undici/undici:12345:11 at async getVersionInfo (F:\nextjslearn\huahan-admin\huahan-admin\node_modules\next\dist\server\dev\hot-reloader-webpack.js:215:21) at async Span.traceAsyncFn (F:\nextjslearn\huahan-admin\huahan-admin\node_modules\next\dist\trace\trace.js:154:20) at async HotReloaderWebpack.start (F:\nextjslearn\huahan-admin\huahan-admin\node_modules\next\dist\server\dev\hot-reloader-webpack.js:607:28) at async startWatcher (F:\nextjslearn\huahan-admin\huahan-admin\node_modules\next\dist\server\lib\router-utils\setup-dev-bundler.js:156:5) at async Span.traceAsyncFn (F:\nextjslearn\huahan-admin\huahan-admin\node_modules\next\dist\trace\trace.js:154:20) at async Server. (F:\nextjslearn\huahan-admin\huahan-admin\node_modules\next\dist\server\lib\start-server.js:249:36) { cause: Error: read ECONNRESET at TLSWrap.onStreamRead (node:internal/stream_base_commons:217:20) { errno: -4077, code: 'ECONNRESET', syscall: 'read' } ✓ Ready in 28.9s ``` ![image.png](https://cos.oneszhang.com/usr/uploads/2024/09/1652464251.png) ### 解決方案: ```bash npm config set registry https://registry.npmmirror.com npm config get registry ``` 如果使用yarn 则:` yarn config set registry https://registry.npmmirror.com/ ` --- # [泛微 Ecology9] 移动建模手机号&身份证号校验js代码 作者:丸子·张 来源:https://blog.oneszhang.com/2024/09/11/103/ Markdown:https://blog.oneszhang.com/2024/09/11/103.md 发布日期:2024-09-11T09:18:00.000Z 更新时间:2024-09-11T09:18:00.000Z 专栏:泛微 E9 标签: 摘要:在泛微 Ecology EC9 移动建模中,通过 $load 注册身份证号与手机号两个校验函数,并监听 sfzh、lxfs 字段的 change 事件,校验失败时用 Mobile_NS 提示并清空输入框。身份证校验除正则外还核验出生日期与校验码,手机号按号段正则判断,适合移动端表单提交前的即时校验。 ``` $load(function() { // 身份证号校验函数 function testid(id) { var format = /^(([1][1-5])|([2][1-3])|([3][1-7])|([4][1-6])|([5][0-4])|([6][1-5])|([7][1])|([8][1-2]))\d{4}(([1][9]\d{2})|([2]\d{3}))(([0][1-9])|([1][0-2]))(([0][1-9])|([1-2][0-9])|([3][0-1]))\d{3}[0-9xX]$/; if (!format.test(id)) { return { 'status': 0, 'msg': '身份证号码不合规' }; } var year = id.substr(6, 4), month = id.substr(10, 2), date = id.substr(12, 2), time = Date.parse(month + '-' + date + '-' + year), now_time = Date.parse(new Date()), dates = (new Date(year, month, 0)).getDate(); if (time > now_time || date > dates) { return { 'status': 0, 'msg': '出生日期不合规' } } var c = new Array(7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2); var b = new Array('1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2'); var id_array = id.split(""); var sum = 0; for (var k = 0; k < 17; k++) { sum += parseInt(id_array[k]) * parseInt(c[k]); } if (id_array[17].toUpperCase() != b[sum % 11].toUpperCase()) { return { 'status': 0, 'msg': '身份证校验码不合规' } } return { 'status': 1, 'msg': '校验通过' } } // 手机号校验函数 function testPhoneNumber(phone) { var phoneFormat = /^(13[0-9]|14[5-9]|15[0-9]|16[6]|17[0-9]|18[0-9]|19[0-9])\d{8}$/; if (!phoneFormat.test(phone)) { return { 'status': 0, 'msg': '手机号码不合规' }; } return { 'status': 1, 'msg': '手机号码校验通过' }; } // 监听身份证输入框变化事件 $f("sfzh").on("change", function () { let MyIDCard = this.value; if (MyIDCard === '') return; var validationResult = testid(MyIDCard); if (validationResult.status === 0) { Mobile_NS.msg(validationResult.msg); Mobile_NS.alert(validationResult.msg, '好的', function(){ $f("sfzh").val(""); }); } else { Mobile_NS.msg("身份证号校验通过"); } }); // 监听手机号码输入框变化事件 $f("lxfs").on("change", function () { let phoneNumber = this.value; if (phoneNumber === '') return; var phoneValidationResult = testPhoneNumber(phoneNumber); if (phoneValidationResult.status === 0) { Mobile_NS.msg(phoneValidationResult.msg); Mobile_NS.alert(phoneValidationResult.msg, '好的', function(){ $f("lxfs").val(""); }); } else { Mobile_NS.msg("手机号码校验通过"); } }); }); ``` --- # [泛微 Ecology9] 使用页面引导组件/功能 作者:丸子·张 来源:https://blog.oneszhang.com/2024/08/30/99/ Markdown:https://blog.oneszhang.com/2024/08/30/99.md 发布日期:2024-08-30T03:56:00.000Z 更新时间:2024-08-30T03:56:00.000Z 专栏:泛微 E9 标签: 摘要:介绍在泛微EC9中实现页面引导功能的两种途径:ecode方式与系统深度集成,适合全局化、需长期维护扩展的场景;纯JS方式更灵活轻量,可直接应用于建模或流程页面。文章重点讲解纯JS实现步骤,即引入WeaIntro相关组件、创建渲染容器、定义引导步骤及提示内容后启动引导,开发者可按需定制目标元素与文案,帮助新用户快速熟悉页面功能、提升使用体验。 > 📌 > > 该文章由丸子引导AI辅助制作生成,如有改进意见,欢迎评论区留言。 > ## 引言 在现代办公自动化(OA)系统中,页面引导功能变得越来越重要。它不仅能帮助新用户快速上手系统,还可以优化用户体验,提高工作效率。在泛微EC9中,我们可以通过两种方式实现页面引导功能:使用泛微ecode和纯JavaScript(JS)方式。 ![image.png](https://cos.oneszhang.com/usr/uploads/2024/08/4019843276.png) ## 泛微EC9中实现页面引导功能的方法 ### 使用泛微ecode实现页面引导 使用泛微ecode来实现页面引导是一种通用且全局的方法。它的主要优势在于能够与泛微系统深度集成,适用于大多数需要页面引导的场景。此方法的实现相对复杂,但对于长期需要维护和扩展的项目来说,具有很高的可扩展性。 ### 使用纯JS方式实现页面引导 相比之下,使用纯JS方式则更加灵活和简单。你可以在建模或流程中随时应用这一功能,而不需要对现有系统做过多的调整。此方法适用于希望快速实现特定功能,且对页面引导的控制要求更高的场景。 ## 纯JS实现页面引导功能的详细步骤 ### 引入必要的组件和库 在使用纯JS实现页面引导时,我们需要引入泛微的部分组件,如`WeaReqTop`和`WeaIntro`。这些组件提供了页面引导所需的基础功能。 ### 创建引导步骤并设置提示内容 通过`Intro`组件,我们可以定义多个引导步骤。每个步骤可以绑定到页面上的特定元素,并为用户提供相应的提示说明。 ### 启动页面引导 在定义完所有步骤后,我们通过`intro.start()`方法启动页面引导,让用户能够逐步了解页面上的关键功能。 ## 示例代码解析 下面的代码展示了如何使用纯JS实现页面引导功能: ```javascript const { WeaReqTop, WeaIntro } = ecCom; const { Intro, Step } = WeaIntro; // 创建一个新的小容器节点 const introContainer = document.createElement('div'); document.body.appendChild(introContainer); // 创建 `Intro` 组件的元素 const introElement = React.createElement(Intro, { onReady: (intro) => { // 设置引导步骤 intro.setOptions({ steps: [ { element: '.ant-tabs-tab-inner[title="全部流程"]', // 第一个元素 intro: "这是全部流程选项" }, { element: '.ant-tabs-tab-inner[title="我的收藏"]', // 第二个元素 intro: "这是我的收藏选项" } ], }); // 启动引导 intro.start(); } }); // 使用 ReactDOM 渲染 `Intro` 组件到新的小容器节点 ReactDOM.render(introElement, introContainer); ``` ### 代码结构与功能概述 这段代码首先创建了一个新的容器节点,并将其添加到页面中。然后,通过`React.createElement`创建了`Intro`组件,并设置了引导步骤和相应的提示内容。最后,使用`ReactDOM.render`将该组件渲染到页面上。 ### 各部分代码的详细解读 - **引入组件**:通过`const { WeaReqTop, WeaIntro } = ecCom;`引入泛微的必要组件。 - **创建容器**:`document.body.appendChild(introContainer);`将新的容器节点添加到页面中,用于存放引导组件。 - **定义引导步骤**:在`intro.setOptions`中,我们为页面上的元素设置了引导步骤及提示内容。 - **启动引导**:`intro.start();`用于启动页面引导,让用户依次看到各步骤的提示信息。 ### 如何根据实际需求进行定制 你可以根据实际的业务需求,调整`steps`数组中的元素选择器和提示内容,从而实现针对不同页面的个性化引导。 ## 结论 通过本文的介绍,你已经了解了在泛微EC9中实现页面引导功能的两种方法。使用泛微ecode可以实现更全局的引导功能,而使用纯JS方式则提供了更高的灵活性。根据你的具体需求,选择适合的方法可以大大提高系统的用户体验。 --- # [泛微 Ecology9] api/ec/dev/table/getxml拦截后解决方案 作者:丸子·张 来源:https://blog.oneszhang.com/2024/08/24/97/ Markdown:https://blog.oneszhang.com/2024/08/24/97.md 发布日期:2024-08-24T01:14:00.000Z 更新时间:2024-08-24T01:14:00.000Z 专栏:泛微 E9 标签: 摘要:泛微Ecology9最新安全包拦截了/api/ec/dev/table/getxml接口,导致无法查看接口执行的SQL记录,作者给出JSP替代方案:先校验用户登录,再根据dataKey参数读取缓存数据,用正则解析sql标签属性并以表格展示,将文件放入ecology主目录即可通过URL访问。该方案适用于E9二次开发中快速调试SQL语句执行情况的场景。 > 📌 > > 该文章由丸子引导AI辅助制作生成,如有改进意见,欢迎评论区留言。 > ## 引言 泛微近期发布最新ec9安全包,其中 /api/ec/dev/table/getxml?dataKey=xxx 接口被安全包拦截,故推出以下jsp方案,用于展示接口执行SQL记录。为了便于调试和记录查询操作,本文介绍了一种通过JSP展示接口执行SQL记录的方案。 ![实现展示.png][1] ## 完整代码 ```java <%@ page language="java" contentType="text/html; charset=UTF-8" %> <%@ page import="com.alibaba.fastjson.JSONObject, com.alibaba.fastjson.JSON, com.alibaba.fastjson.JSONException"%> <%@ page import="com.cloudstore.dev.api.util.Util_TableMap"%> <%@ page import="weaver.hrm.User"%> <%@ page import="weaver.hrm.HrmUserVarify"%> <%@ page import="weaver.general.Util"%> <%@ page import="cn.hutool.http.HtmlUtil"%> <%@ page import="cn.hutool.core.util.StrUtil"%> <% User user = HrmUserVarify.getUser(request, response); if (user == null) { response.sendRedirect("/login/Login.jsp"); return; } String dataKey = Util.null2String(request.getParameter("dataKey")); if (dataKey.isEmpty()) { JSONObject result = new JSONObject(); result.put("status", "error"); result.put("message", "dataKey参数不能为空"); out.print(result.toJSONString()); return; } String cachedData = Util_TableMap.getVal(dataKey); if (cachedData == null) { JSONObject result = new JSONObject(); result.put("status", "error"); result.put("message", "无法找到对应的缓存数据"); out.print(result.toJSONString()); } else { try { String sqlTag = cachedData.replaceAll(".*().*", "$1"); java.util.regex.Pattern pattern = java.util.regex.Pattern.compile("(\\w+)\\s*=\\s*\"([^\"]*)\""); java.util.regex.Matcher matcher = pattern.matcher(sqlTag); out.print("

接口执行SQL记录

"); out.print(""); out.print(""); while (matcher.find()) { String attributeName = matcher.group(1); String attributeValue = matcher.group(2); out.print(""); out.print(""); out.print(""); out.print(""); } out.print("
属性名属性值
" + HtmlUtil.escape(attributeName) + "" + HtmlUtil.escape(attributeValue) + "
"); } catch (Exception e) { JSONObject result = new JSONObject(); result.put("status", "error"); result.put("message", "解析 标签时出错:" + e.getMessage()); out.print(result.toJSONString()); } } %> ``` ## 食用方法 1. 将以上完整的代码新建为一个jsp文件,看心情命名,如:`DataKeyLookup.jsp` 2. 放置在ecology主目录下,我的目录是:`/WEAVER/ecology`,我直接放里面 3. 现在你可以访问:`你的OA地址/DataKeyLookup.jsp?dataKey=xxxxxx` ## 方案详解 ### 用户验证 首先,我们需要确保只有合法的用户才能访问该JSP页面。使用`HrmUserVarify.getUser(request, response)`方法来验证用户的登录状态,如果用户未登录,则重定向到登录页面。 ### 参数获取与验证 获取接口传递的`dataKey`参数,并确保该参数不为空。否则,返回错误信息并终止执行。 ### 数据缓存获取 通过`Util_TableMap.getVal(dataKey)`方法获取与`dataKey`对应的缓存数据。如果未找到缓存数据,则返回错误信息。 ### SQL记录提取与展示 我们通过正则表达式提取缓存数据中的``标签,然后解析该标签内的属性及其值,并以表格形式展示在页面上。此过程涉及字符串操作和正则表达式匹配。 ## 代码解析 ### JSP页面导入的库 ```java <%@ page import="com.alibaba.fastjson.JSONObject, com.alibaba.fastjson.JSON, com.alibaba.fastjson.JSONException"%> <%@ page import="com.cloudstore.dev.api.util.Util_TableMap"%> <%@ page import="weaver.hrm.User"%> <%@ page import="weaver.hrm.HrmUserVarify"%> <%@ page import="weaver.general.Util"%> <%@ page import="cn.hutool.http.HtmlUtil"%> <%@ page import="cn.hutool.core.util.StrUtil"%> ``` 上述代码导入了多个库,其中`fastjson`用于处理JSON,`HrmUserVarify`用于用户验证,`HtmlUtil`用于HTML转义。 ### 用户验证逻辑 ```java User user = HrmUserVarify.getUser(request, response); if (user == null) { response.sendRedirect("/login/Login.jsp"); return; } ``` 此部分代码用于验证用户是否登录,如果未登录则重定向到登录页面。 ### SQL标签解析与数据展示逻辑 ```java String sqlTag = cachedData.replaceAll(".*().*", "$1"); java.util.regex.Pattern pattern = java.util.regex.Pattern.compile("(\\w+)\\s*=\\s*\"([^\"]*)\""); java.util.regex.Matcher matcher = pattern.matcher(sqlTag); ``` 通过正则表达式,我们从缓存数据中提取出``标签,并解析出标签内的属性名和属性值,最后以表格形式展示出来。 ## 常见问题与调试 ### 如何处理未找到缓存数据的问题 如果`Util_TableMap.getVal(dataKey)`返回为空,表示没有找到对应的缓存数据,此时返回错误提示,避免页面空白或误导性的信息展示。 ### 解析标签时的异常处理 解析过程中可能会遇到异常,为了避免页面崩溃,使用`try-catch`块捕获异常,并返回相应的错误信息以帮助调试。 ## 结论 该方案通过JSP有效地展示了接口执行的SQL记录,特别适用于需要快速调试SQL语句执行情况的场景。建议后续可以进一步优化数据提取逻辑,提升页面渲染效率。 [1]: https://cos.oneszhang.com/usr/uploads/2024/08/2314170453.png --- # [泛微 Ecology9] 公式:获取当前日期是今年的第几周 作者:丸子·张 来源:https://blog.oneszhang.com/2024/08/16/96/ Markdown:https://blog.oneszhang.com/2024/08/16/96.md 发布日期:2024-08-16T08:07:00.000Z 更新时间:2024-08-16T08:07:00.000Z 专栏:泛微 E9 标签: 摘要:介绍在泛微 Ecology 9 公式中实现的一个 JavaScript 函数,用于计算当前日期是今年的第几周,无需传入参数即可直接调用。其思路是以当年 1 月 1 日为起点,对齐到该日期所在的第一个完整周后按天数推算周数,其中 1 月 1 日所在周被视为第一周,适合在 OA 表单等需要周序号的场景中开箱即用。 ``` /* 获取当前日期是今年的第几周(无参数) */ function getYearWeek() { var date = new Date(); var startDate = new Date(date.getFullYear(), 0, 1); var dayOfWeek = startDate.getDay() || 7; startDate.setDate(startDate.getDate() + (8 - dayOfWeek)); var daysSinceStart = Math.ceil((date - startDate) / 86400000); var weekNumber = Math.ceil(daysSinceStart / 7) + 1; return weekNumber; } ``` # 开袋即食 该方案,1月1日所在周,会被视为第一周。 ![image.png](https://cos.oneszhang.com/usr/uploads/2024/08/2078952135.png) --- # [泛微 Ecology9] SSO单点登录到EcologyOA 作者:丸子·张 来源:https://blog.oneszhang.com/2024/08/15/94/ Markdown:https://blog.oneszhang.com/2024/08/15/94.md 发布日期:2024-08-15T06:17:00.000Z 更新时间:2024-08-15T06:17:00.000Z 专栏:泛微 E9 标签: 摘要:介绍泛微Ecology E9基于Token认证实现SSO单点登录的完整流程:在统一认证中心开启Token认证并注册应用,配置IP白名单与账号映射规则,再通过浏览器控制台调用接口获取ssoToken拼接登录链接即可免密进入系统。注意开启认证会自动重启OA服务,生产环境需谨慎操作。 ## 开启Token认证 ### 后台 → 统一认证中心 → 认证服务管理 → Token认证 开启并保存,将自动重启OA服务 ![image.png](https://cos.oneszhang.com/usr/uploads/2024/08/1229844856.png) > 🚨 **服务器重启警告!** > > 注意:开启“Token认证”并保存时,服务器将被重启,生产环境谨慎操作,以免影响正常业务! > ## 注册应用 ### 后台 → 统一认证中心 → 认证服务管理 → 认证应用管理 点击右上角“注册”,注册应用 认证方式:TOKEN; IP白名单:输入你的IP,这里是必须的(不知道自己IP?往下看!一会教你); 账号映射规则:用户名(也可以用别的); 其他配置:看你心情; ![image.png](https://cos.oneszhang.com/usr/uploads/2024/08/1824711564.png) ## 开始测试 打开你浏览器的控制台(F12)来模拟测试 ``` var appid = 'd72cceb0-0d96-46aa-8f22-4cd00c5cbc5c'; //替换为你的应用标识 var loginid = 'zhangsan'; //替换为你的账号映射规则对应的单点登录人员值 var xhr = new XMLHttpRequest(); xhr.open('POST', '/ssologin/getToken', false); xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); xhr.send('appid=' + encodeURIComponent(appid) + '&loginid=' + encodeURIComponent(loginid)); var response = xhr.responseText.trim(); if (xhr.status === 200) { if (response.includes('失败')) { console.log("获取失败:", response); } else { var ssoToken = response; var redirectUrl = window.location.origin + '/wui/index.html?ssoToken=' + encodeURIComponent(ssoToken) + '#/main'; console.log("单点登录ssourl:", redirectUrl); } } else { console.error("请求失败:", xhr.status); } ``` **如果没错,你已经获取到了单点登录url,直接复制后在任意位置访问,即可登录账号** ![image.png](https://cos.oneszhang.com/usr/uploads/2024/08/600741591.png) **如果你的IP没在白名单:** ![image.png](https://cos.oneszhang.com/usr/uploads/2024/08/2320046965.png) 他会提示你IP是什么,加至应用IP白名单即可! --- # [泛微 Ecology9] API接口开发(录入客户是否重复检测by电话) 作者:丸子·张 来源:https://blog.oneszhang.com/2024/08/13/89/ Markdown:https://blog.oneszhang.com/2024/08/13/89.md 发布日期:2024-08-13T09:47:00.000Z 更新时间:2024-08-13T09:47:00.000Z 专栏:泛微 E9 标签: 摘要:介绍在泛微 Ecology 二次开发中实现客户录入查重的 API 接口:通过 GET 请求传入电话号码,接口查询客户管理建模表中是否存在相同联系方式的记录,返回 isValid 与 msg 的 JSON 结果。文章给出了接口文档与基于 JAX-RS 和 RecordSet 的 Java 实现,并对空号码、客户已存在、数据库异常等情况做了相应处理,适合需要在录入客户前进行重复校验的场景参考。 # 客户重复检测API接口的请求文档: --- ## 客户重复检测API接口文档 ### 1. 接口概述 - **接口名称**: 客户重复检测 - **接口路径**: `/customerCheck` - **请求方式**: `GET` - **请求格式**: `application/json` - **功能描述**: 该接口用于检测数据库中是否存在与传入的电话号码相同的客户记录。 ### 2. 请求参数 | 参数名 | 类型 | 必填 | 说明 | |--------|--------|-------|----------------| | phone | String | 是 | 要检测的电话号码 | #### 示例请求 ``` GET /api/customerCheck?phone=12345678901 ``` ### 3. 返回参数 | 参数名 | 类型 | 说明 | |---------|---------|-------------------------------------------| | isValid | Boolean | 检查是否通过(`true`:客户不存在,`false`:客户已存在或查询失败)| | msg | String | 检查结果的描述信息或失败原因 | ### 4. 返回结果示例 #### 4.1. 检查通过 (客户不存在) ```json { "isValid": true, "msg": "客户不存在" } ``` #### 4.2. 检查不通过 (客户已存在) ```json { "isValid": false, "msg": "客户已存在" } ``` #### 4.3. 检查失败 (电话号码为空) ```json { "isValid": false, "msg": "电话号码不能为空" } ``` #### 4.4. 数据库查询出错 ```json { "isValid": false, "msg": "数据库查询出错: <错误信息>" } ``` ### 5. 状态码 - **200 OK**: 请求成功,返回JSON格式的检查结果。 - **400 Bad Request**: 请求参数不正确(如电话号码为空)。 - **500 Internal Server Error**: 服务器内部错误(如数据库查询失败)。 ### 6. 注意事项 - 请确保传入的 `phone` 参数为有效的电话号码。 - 该接口不区分电话号码的格式,如需要,可以在业务层进行格式校验。 # java代码实现 ``` package com.api.ones.khgl; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import com.alibaba.fastjson.JSONObject; import weaver.conn.RecordSet; /** *

客户重复检测API

*

用于检测数据库中是否存在重复的客户信息。

* *

create: 2024/08/13

* *

* * @author Oneszhang */ @Path("/customerCheck") public class CustomerCheckApi { @GET @Produces(MediaType.APPLICATION_JSON) public Response checkCustomer(@QueryParam("phone") String phone) { JSONObject jsonResponse = new JSONObject(); if (phone == null || phone.isEmpty()) { jsonResponse.put("isValid", false); jsonResponse.put("msg", "电话号码不能为空"); return Response.status(Response.Status.BAD_REQUEST).entity(jsonResponse.toString()).build(); } try { RecordSet rs = new RecordSet(); String sql = "SELECT count(*) as count FROM uf_khgl WHERE lxfs = ?"; rs.executeQuery(sql, phone); if (rs.next()) { int count = rs.getInt("count"); if (count > 0) { jsonResponse.put("isValid", false); jsonResponse.put("msg", "客户已存在"); } else { jsonResponse.put("isValid", true); jsonResponse.put("msg", "客户不存在,检验通过"); } } else { jsonResponse.put("isValid", false); jsonResponse.put("msg", "查询失败"); } } catch (Exception e) { jsonResponse.put("isValid", false); jsonResponse.put("msg", "数据库查询出错: " + e.getMessage()); } return Response.ok(jsonResponse.toString()).build(); } } ``` --- # [泛微 Ecology9] 为Table表格增加筛选功能(流程待办筛选当前节点) 作者:丸子·张 来源:https://blog.oneszhang.com/2024/08/05/86/ Markdown:https://blog.oneszhang.com/2024/08/05/86.md 发布日期:2024-08-05T09:01:00.000Z 更新时间:2024-08-05T09:01:00.000Z 专栏:泛微 E9 标签: 摘要:介绍在泛微 Ecology ecode 开发中,利用 ecodeSDK.overwritePropsFnQueueMapSet 覆写 Table 组件属性的方法,为流程待办列表的当前节点列动态生成筛选项并实现本地筛选。文中通过路径校验定位页面、从数据中提取唯一节点构建过滤器、再配置列的过滤函数完成实现,并提示远程加载数据时需另行处理网络延迟与数据同步问题。 > 📌 > > 该文章由丸子引导AI辅助制作生成,如有改进意见,欢迎评论区留言。 > ## 引言 在许多企业级应用中,我们常常需要对流程待办事项进行筛选,以便用户能够更快速地找到需要处理的任务。本文将介绍如何为Table表格增加筛选功能,特别是针对流程待办筛选当前节点的实现方法。 ![image.png](https://cos.oneszhang.com/usr/uploads/2024/08/3728447019.png) ## 代码实现 ![image.png](https://cos.oneszhang.com/usr/uploads/2024/08/2717580035.png) 下面的代码示例展示了如何通过`ecodeSDK.overwritePropsFnQueueMapSet`方法,为Table表格增加筛选功能: ```javascript ecodeSDK.overwritePropsFnQueueMapSet('Table', { fn: (newProps) => { // 日志输出,检查组件定位是否成功 console.log('!找到组件啦!!!!!!'); // 检查当前路径是否正确 if (!ecodeSDK.checkLPath('/wui/index.html#/main/workflow/listDoing?menuIds=1,13')) return; console.log("路径识别正确!", newProps); // 创建一个 Map 来存储 unique 的 currentnodeid 和 currentnodeidspan const uniqueNodes = new Map(); // 遍历 newProps.datas 数组,获取 unique 的 currentnodeid 和 currentnodeidspan newProps.datas.forEach(data => { if (!uniqueNodes.has(data.currentnodeid)) { uniqueNodes.set(data.currentnodeid, data.currentnodeidspan); } }); // 构建 filters 数组 const filters = Array.from(uniqueNodes.entries()).map(([value, text]) => ({ text, value })); // 遍历 newProps.columns 数组,找到 dataIndex 为 'currentnodeid' 的对象 newProps.columns.forEach(column => { if (column.dataIndex === 'currentnodeid') { // 修改 title 为 '当前节点1' column.title = '当前节点1'; // 设置动态 filters column.filters = filters; // 增加 onFilter 函数 column.onFilter = (value, record) => record.currentnodeid === value; } }); // 输出修改后的 columns 数组 console.log("修改后的 columns:", newProps.columns); }, }); ``` ### 组件定位 首先,我们通过日志输出检查组件是否定位成功: ```javascript console.log('!找到组件啦!!!!!!'); ``` ### 路径检查 接着,我们验证当前路径是否正确: ```javascript if (!ecodeSDK.checkLPath('/wui/index.html#/main/workflow/listDoing?menuIds=1,13')) return; console.log("路径识别正确!", newProps); ``` ### 数据筛选 然后,我们创建一个Map来存储唯一的currentnodeid和currentnodeidspan,并构建filters数组: ```javascript const uniqueNodes = new Map(); newProps.datas.forEach(data => { if (!uniqueNodes.has(data.currentnodeid)) { uniqueNodes.set(data.currentnodeid, data.currentnodeidspan); } }); const filters = Array.from(uniqueNodes.entries()).map(([value, text]) => ({ text, value })); ``` ### 动态过滤器 最后,我们遍历newProps.columns数组,找到dataIndex为'currentnodeid'的对象,修改其title、设置动态filters并增加onFilter函数: ```javascript newProps.columns.forEach(column => { if (column.dataIndex === 'currentnodeid') { column.title = '当前节点1'; column.filters = filters; column.onFilter = (value, record) => record.currentnodeid === value; } }); ``` ## 实际应用 ### 本地模式 上述代码示例为本地模式下的数据筛选功能。在本地测试时,可以通过模拟数据来验证筛选功能是否正常工作。 ### 远程加载数据 在实际应用中,我们需要结合“远程加载数据”来实现更为复杂的筛选功能。具体实现方式将根据实际需求和环境进行调整。 :::tip 提示:远程加载数据时,需注意网络延迟和数据同步问题,确保用户体验流畅。 ::: ## 结论 通过以上步骤,我们成功为Table表格增加了筛选功能,特别是针对流程待办筛选当前节点的实现。这种方法不仅提升了用户体验,也为后续的功能扩展提供了良好的基础。 > ✅ > > 成功实现了为Table表格增加筛选功能! > 如有任何问题或改进建议,欢迎在评论区留言与我们交流。 希望这篇文章能帮助你更好地理解和实现Table表格的筛选功能。如果有任何疑问,欢迎在评论区留言讨论! --- # 移动云电脑,套娃保活!持久在线~ 作者:丸子·张 来源:https://blog.oneszhang.com/2024/07/25/85/ Markdown:https://blog.oneszhang.com/2024/07/25/85.md 发布日期:2024-07-25T02:29:07.000Z 更新时间:2024-07-25T02:29:07.000Z 专栏:部署与自托管 标签: 摘要:针对移动云电脑长时间未连接会自动关机下线的问题,文章介绍了套娃式保活方案:在云电脑中再装云电脑,通过 PowerShell 脚本定时结束并重启云电脑进程,再配合 Windows 任务计划程序每天凌晨自动执行,实现云电脑持久在线。适合需要云电脑全天候待机的用户参考,注意脚本路径需按实际安装位置调整。 > 📌 > > 该文章由丸子引导AI辅助制作生成,如有改进意见,欢迎评论区留言。 > ### 1. 引言 随着云计算技术的快速发展,云电脑的使用变得越来越普遍。移动云电脑为用户提供了方便快捷的远程工作和娱乐环境。然而,一个常见的问题是,当长时间未连接时,云电脑会自动关机下线。本文将介绍一种使用PowerShell脚本和定时任务来解决这一问题的有效方法。 ### 2. 解决方案概述 为了避免移动云电脑因长时间未连接而自动关机,使用套娃大法,在云电脑中安装云电脑,我们可以使用PowerShell脚本来定时结束并重启云电脑应用程序。通过创建一个定时任务,确保该脚本在夜间自动运行一次,用云电脑连接云电脑,从而实现云电脑的保活在线。 ### 3. 解决方案 我们可以通过编写PowerShell脚本来定时重启移动云电脑应用,以保持其在线状态。以下是详细步骤: #### 3.1 安装移动云电脑 首先,确保已经在计算机中安装了移动云电脑。本文假设安装路径为 `D:\Program Files (x86)\Ecloud\CloudComputer\`。 #### 3.2 创建PowerShell脚本 在D盘根目录创建一个PowerShell脚本,用于定时结束并重新启动移动云电脑应用。脚本内容如下: ```powershell # 结束进程 Stop-Process -Name "Ecloud Cloud Computer Application" -Force -ErrorAction SilentlyContinue Stop-Process -Name "uSmartView_VDI_Client" -Force -ErrorAction SilentlyContinue # 等待10秒 Start-Sleep -Seconds 10 # 运行程序 Start-Process -FilePath "D:\Program Files (x86)\Ecloud\CloudComputer\Ecloud Cloud Computer Application.exe" # 等待3分钟 Start-Sleep -Seconds 180 # 结束进程 Stop-Process -Name "Ecloud Cloud Computer Application" -Force -ErrorAction SilentlyContinue Stop-Process -Name "uSmartView_VDI_Client" -Force -ErrorAction SilentlyContinue # 完成 Write-Output "操作完成。" ``` 将上述脚本保存为一个文件,例如 `KeepCloudComputerOnline.ps1`。确保路径和文件名根据您的实际情况进行调整。 ### 3.3 创建定时任务 接下来,我们需要创建一个定时任务来定期运行上述PowerShell脚本。以下是具体步骤: 1. 打开任务计划程序(Task Scheduler)。 2. 创建一个基本任务(Create Basic Task),命名为“云电脑保活任务”。 3. 选择“每日”触发器,并设置具体的运行时间(例如,每天凌晨2点)。 4. 在操作选项中,选择“启动程序(Start a Program)”。 5. 在“程序/脚本”字段中,输入 `powershell`。 6. 在“添加参数”字段中,输入 `-File "D:\KeepCloudComputerOnline.ps1"`,确保路径和文件名正确(本文假设放在D盘根目录)。 7. 完成任务创建。 :::tip 建议设置任务在深夜运行,以尽量减少对日常工作的影响。 ::: ### 结论 通过以上方法,我们可以有效解决移动云电脑长时间未连接会自动关机下线的问题,确保其能够持续在线运行。希望本文对您有所帮助。 > ✅ > > 如果您有更好的解决方案或改进建议,欢迎在评论区分享! > --- # [泛微 Ecology9] 建模查询手机号字段必填查询检查拦截 作者:丸子·张 来源:https://blog.oneszhang.com/2024/07/08/80/ Markdown:https://blog.oneszhang.com/2024/07/08/80.md 发布日期:2024-07-08T06:47:00.000Z 更新时间:2024-07-08T06:47:00.000Z 专栏:泛微 E9 标签: 摘要:这篇文章介绍在泛微 Ecology 建模查询页面中,通过 ecodeSDK 覆写 WeaInput 组件,对指定查询路径下 placeholder 为客户电话的输入框做手机号格式校验,在 onBlur 和 onPressEnter 事件中拦截非法号码并弹出提示,同时记录了改写接口数据的无效方案,适合做建模查询前端校验的开发者参考。 # 效果: ![image.png](https://cos.oneszhang.com/usr/uploads/2024/07/3153505675.png) ``` ecodeSDK.overwritePropsFnQueueMapSet('WeaInput', { fn: (props) => { // 检查页面路径和placeholder条件 if (!ecodeSDK.checkLPath('/spa/cube/index.html#/main/cube/search?customid=136') || props.placeholder !== "客户电话") return; // 针对 onBlur 和 onPressEnter 事件添加验证和处理 ['onBlur', 'onPressEnter'].forEach(event => { if (typeof props[event] === 'function') { const originalEvent = props[event]; props[event] = (e) => { if (!/^1[3-9]\d{9}$/.test(props.value)) { ModeForm.showMessage("电话号码格式错误,请输入有效的手机号码", 2, 10); return; } originalEvent(e); // 如果电话号码有效,调用原事件处理器 }; } }); // console.log('Props updated for WeaInput with placeholder check:', props); }, }); ``` :::details[无效方案] ``` // // 是否第一次打开页面 // var isFirstopen = true; // ecodeSDK.rewriteApiDataQueueSet({ // fn: function fn(url, params, data) { // if (!ecodeSDK.checkLPath('/spa/cube/index.html#/main/cube/search?customid=136')) return; // if (url.indexOf("/api/ec/dev/table/datas") === -1) return; // // 获取客户电话 // var topSearchDatas = ModeList.getTopSearchDatas(); // var phoneNumber = topSearchDatas[11987]; // 假设电话号码的键为 11987 // // 正则检查电话号码格式 // var phoneRegex = /^1[3-9]\d{9}$/; // 以1开头,后跟10位数字 // if (!phoneRegex.test(phoneNumber)) { // // 如果电话号码格式不正确,并且还没有显示过消息,则显示警告信息 // if (isFirstopen) { // ModeForm.showMessage("电话号码格式错误,请输入有效的手机号码", 2, 10); // 错误信息,10秒后消失 // isFirstopen = false; // 打开过页面 // } // console.log(data); // data.datas = []; // } // }, // desc: '客户管理客户检索手机号检查拦截' // }); ``` ::: --- # [泛微 Ecology9] 流程浏览按钮跳转监控流程(ismonitor) 作者:丸子·张 来源:https://blog.oneszhang.com/2024/07/08/79/ Markdown:https://blog.oneszhang.com/2024/07/08/79.md 发布日期:2024-07-08T06:44:48.000Z 更新时间:2024-07-08T06:44:48.000Z 专栏:泛微 E9 标签: 摘要:在泛微 Ecology 二次开发中,利用 ecodeSDK 覆盖 WeaBrowser 组件,当页面处于流程请求地址且流程 ID 在允许列表内时,为“多流程”浏览按钮的跳转链接添加监控标识参数,并把按钮文字改为“点击查看流程”,从而实现跳转至监控流程。适用于需对指定流程启用监控浏览的场景,需注意开关和流程 ID 白名单配置。 ``` let enable = true; const allowedWorkflowIds = [148,56,143]; // 允许的流程ID列表 //判断条件 const checkConditions = () => { // console.log("开始检查"); const { hash } = window.location; if (!hash.startsWith('#/main/workflow/req')) return false; // 判断页面地址 if (!WfForm) return false; // 表单sdk加载完成 const baseInfo = WfForm.getBaseInfo(); const { workflowid } = baseInfo; return allowedWorkflowIds.includes(workflowid); // 判断流程id是否在允许的列表中 }; ecodeSDK.overwritePropsFnQueueMapSet('WeaBrowser', { fn: (newProps) => { //newProps 代表组件参数 if (!enable || !checkConditions()) return; // 开关打开、条件不满足 //筛选newProps.title:"多流程" if (newProps.title == "多流程") { const linkUrlWithMonitor = newProps.linkUrl.replace(/(requestid=)/, '&ismonitor=1&$1'); newProps.linkUrl = linkUrlWithMonitor; console.log(newProps); if (!newProps.replaceDatas || !newProps.replaceDatas.length || !newProps.replaceDatas[0].name) { // 如果 replaceDatas 不存在、为空数组或第一个元素的 name 属性不存在或为空,则不执行后续操作 return; } newProps.replaceDatas[0].name = "点击查看流程"; } }, }); ``` --- # [泛微 Ecology9] 隐藏流程顶部与签字意见(纯表单) 作者:丸子·张 来源:https://blog.oneszhang.com/2024/07/08/78/ Markdown:https://blog.oneszhang.com/2024/07/08/78.md 发布日期:2024-07-08T06:43:51.000Z 更新时间:2024-07-08T06:43:51.000Z 专栏:泛微 E9 标签: 摘要:这段泛微 Ecology 的 ecode 前端代码通过重写 WeaReqTop 组件,在指定表单路径且流程 ID 匹配时,隐藏流程顶部区域与签字意见输入框、清除顶部容器内边距,并同步修改页面标题。适用于纯表单模式下需要精简流程页面显示的场景,使用时需按实际流程 ID 和路径调整判断条件。 ``` ecodeSDK.overwriteClassFnQueueMapSet('WeaReqTop', { fn: (Com, newProps) => { const { hash } = window.location; if (!ecodeSDK.checkLPath('/spa/workflow/index_form.jsp#/main/workflow/req')) return; const baseInfo = WfForm.getBaseInfo(); if (baseInfo.workflowid !== 132) return; // 修改页面标题 document.title = '服务平台-留言回复'; // 获取要隐藏的元素 const elementsToHide = document.querySelectorAll('.ant-row.wea-new-top-req, .wf-req-sign-input'); // 获取要修改的元素 const wrapperElement = document.querySelector('.wea-new-top-req-wapper'); console.log(elementsToHide); // 遍历并隐藏元素 elementsToHide.forEach(element => { element.style.display = 'none'; }); // 设置要修改的元素的padding-top为0 if (wrapperElement) { wrapperElement.style.paddingTop = '0'; } return newProps; }, desc: '隐藏流程顶部与签字意见' }); ``` --- # [泛微 Ecology9] 隐藏新建流程页面的流程分组 作者:丸子·张 来源:https://blog.oneszhang.com/2024/07/08/77/ Markdown:https://blog.oneszhang.com/2024/07/08/77.md 发布日期:2024-07-08T06:42:52.000Z 更新时间:2024-07-08T06:42:52.000Z 专栏:泛微 E9 标签: 摘要:通过 ecodeSDK 的 rewriteApiDataQueueSet 拦截新建流程页面的 wfinfo 接口返回数据,在回调中过滤掉指定分组 id(示例为 15),从而在页面上隐藏该流程分组。适用于泛微 Ecology 前端 ecode 二次开发中需要按需屏蔽流程分组的场景,使用时将 id 替换为实际要隐藏的分组即可。 ``` ecodeSDK.rewriteApiDataQueueSet({ fn: function fn(url, params, data) { if (url.indexOf("/api/workflow/createreq/wfinfo") === -1) return; // 过滤掉id为15的元素 if (data && data.datas) { data.datas = data.datas.filter(function(item) { return item.id !== "15"; }); } }, desc: '新建流程页面分组隐藏' }); ``` --- # [泛微 Ecology9] 记Ecology如何简单写restful接口? 作者:丸子·张 来源:https://blog.oneszhang.com/2024/07/08/76/ Markdown:https://blog.oneszhang.com/2024/07/08/76.md 发布日期:2024-07-08T06:25:00.000Z 更新时间:2024-07-08T06:25:00.000Z 专栏:泛微 E9 标签: 摘要:介绍在泛微 Ecology E9 中快速编写 RESTful 接口的方法:在 com.api.formmode.web 包下新建 Java 类,利用 JAX-RS 注解声明请求路径、方式和 JSON 返回格式,编译为 class 并重启服务后,即可通过 /api/formmode/custom/ 前缀的地址访问并获取 JSON 响应。适合 Ecology 二次开发人员参考。 目录:\src\com\api\formmode\web\CustomWanziTestFormmodeAction.java 编译为class后\src\com\api\formmode\web\CustomWanziTestFormmodeAction.class (需重启服务器) 请求地址:/api/formmode/custom/wanzitestaction/test?param1=111 返回内容: ```json { "message": "This is a test response from the custom wanzitestaction formmode action.", "param1": "111" } ``` ``` package com.api.formmode.web; import com.alibaba.fastjson.JSONObject; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.core.Context; @Path("/formmode/custom/wanzitestaction") public class CustomWanziTestFormmodeAction { public CustomWanziTestFormmodeAction() { } @POST @Path("/test") @Produces({"application/json"}) public String testFormmode(@Context HttpServletRequest request, @Context HttpServletResponse response) { response.setContentType("application/json; charset=utf-8"); String param1 = request.getParameter("param1"); JSONObject result = new JSONObject(); result.put("param1", param1); result.put("message", "This is a test response from the custom wanzitestaction formmode action."); return result.toJSONString(); } } ``` --- # [泛微 Ecology9] 流程表单中实现增加按钮 作者:丸子·张 来源:https://blog.oneszhang.com/2024/07/03/75/ Markdown:https://blog.oneszhang.com/2024/07/03/75.md 发布日期:2024-07-03T06:14:00.000Z 更新时间:2024-07-03T06:14:00.000Z 专栏:泛微 E9 标签: 摘要:介绍泛微 EC9 流程表单中增加自定义按钮的两种做法:主表可用 CSS 配合 jQuery 向指定单元格追加按钮并绑定点击事件,明细表则通过 WfForm 的 API 将按钮代理渲染到单行文本框字段并注册点击事件。前者简单直接,后者更规范灵活;官方不推荐 jQuery 与原生 DOM 操作,建议统一使用 API,以保证移动端兼容性。 > 📌 > > 该文章由丸子引导AI辅助制作生成,如有改进意见,欢迎评论区留言。 > ## 引言 泛微EC9是一款功能强大的企业管理软件,广泛应用于各种流程表单的管理和操作。在实际应用中,常常需要在流程表单中增加按钮,以实现某些自定义功能。本文将介绍两种在泛微EC9流程表单中增加按钮的方法:使用CSS和jQuery,以及使用标准API接口操作。 ## 方法1:使用CSS和jQuery实现 ### 步骤详解 1. 使用自定义属性,设置单元格id 2. 在主表中通过jQuery向特定元素添加按钮。 3. 使用CSS自定义按钮的样式。 4. 绑定按钮点击事件,实现相应的逻辑操作。 ### 代码示例(主表增加按钮) ```javascript function openAddCustomer() { // 在此处添加您的逻辑代码 console.log("哎呀~我被点击了!"); // 可以在这里执行其他操作或跳转到其他页面等 } jQuery(document).ready(function() { $("#selectbtn").append(""); }); ``` ## 方法2:使用标准API接口操作 ### 步骤详解 1. 定义明细表的ID和按钮的字段名、显示名称。 2. 使用API接口将按钮代理渲染到单行文本框字段中。 3. 注册按钮的点击事件,定义点击后的逻辑操作。 ### 代码示例(明细表增加按钮) ```javascript jQuery(document).ready(function() { // 定义明细表id var detailid = "1"; // 定义按钮字段名(数据库字段名称, 注意: 单行文本框,文本类型) var fieldname = "csmx3" // 定义按钮显示名称 var buttonname = "我是按钮1111" /** ------------以下内容无需更改----------- **/ var buttonFieldId = WfForm.convertFieldNameToId(fieldname, "detail_" + detailid); initializeButtonAction(); WfForm.registerAction(WfForm.ACTION_ADDROW + detailid, initializeButtonAction); function initializeButtonAction() { WfForm.getDetailAllRowIndexStr("detail_" + detailid).split(",").forEach(function(rowIndex) { if (rowIndex) { WfForm.proxyFieldComp(buttonFieldId + "_" + rowIndex, "", "1,2,3"); } }); } /** ------------以上内容无需更改----------- **/ // 定义触发点击事件 WfForm.bindFieldAction("onclick", buttonFieldId, function(fieldid,rowIndex){ alert("明细的"+rowIndex+"行标记字段"+fieldid+"被点击"); //这里写你想要执行的事件内容 }); }); ``` ## 小结 这两种方法各有优劣。使用CSS和jQuery的方法更加简单直接,适合快速实现按钮的添加和基本交互操作。而使用标准API接口的方法则更为规范和灵活,适合复杂的业务需求,具有较强兼容性。根据实际情况选择合适的方法,可以更好地提高工作效率和用户体验。 > 📌 > > 来自官方文档:表单字段相关操作,不推荐使用jQuery,禁止原生JS直接操作DOM结构! > 大家开发过程中,推荐都使用API接口操作,由产品统一运维;同时使用API才能完整的兼容移动终端 > --- # [泛微 Ecology9] web.xml 内容格式化为表格 作者:丸子·张 来源:https://blog.oneszhang.com/2024/06/26/74/ Markdown:https://blog.oneszhang.com/2024/06/26/74.md 发布日期:2024-06-26T07:48:00.000Z 更新时间:2024-06-26T07:48:00.000Z 专栏:泛微 E9 标签: 摘要:将泛微 Ecology EC9 的 web.xml 内容整理为 Markdown 表格,重点列出 Filters 中过滤器名称、实现类、URL 匹配规则及初始化参数,方便博客阅读和复制。适合 Ecology 二次开发或运维人员快速查阅配置,注意表格仅覆盖已整理出的过滤器内容。 下面是 `web.xml` 文件内容的表格表示形式: :::details[博客不好阅读?点击展开代码复制markdown试试!] ``` ### Filters | Filter Name | Filter Class | URL Pattern | Init Param Key | Init Param Value | |-------------------------|-----------------------------------------------------|-------------------------------------------------------------------------------------------------|-----------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | EncodingFilterWeaver | weaver.security.filter.EncodingFilter | /* | | | | SecurityTransFilter | weaver.security.encryptedtrans.SecurityTransFilter | /* | | | | CheckSensitiveFilter | com.engine.sensitive.biz.CheckSensitiveFilter | /* | | | | WeaSsoIocComponentFilter| com.weaverboot.frame.ioc.filter.WeaSsoIocComponentFilter | /api/*, *.jsp, *.html | | | | EMFilter | com.cloudstore.dev.api.service.EMFilter | /* | | | | SessionCloudFilter | com.cloudstore.dev.api.service.SessionFilter | /api/*, /page/interfaces/*.jsp, /workflow/core/ControlServlet.jsp | checkurl | /api/hrm/emmanager; | | | | | uncheckurl | /api/ec/dev/app/getCheckSystemInfo;/api/ec/dev/app/emjoin; | | | | | unchecksessionurl | | | SecurityFilter | weaver.filter.SecurityFilter | /* | | | | Compress | weaver.filter.WGzipFilter | /Messager/MessagerServlet, /weaver/weaver.common.util.taglib.SplitPageXmlServletNew, /weaver/weaver.common.util.taglib.SplitPageXmlServlet, *.jsp, /api/*, *.css, *.js | exclude | /wui/theme/ecology8/page/login.jsp;/login/login.jsp;/keygenerator/KeyGenerator.jsp;/keygenerator/getNoCheckFiles.jsp;/keygenerator/packNoCheckFiles.jsp;/workflow/request/WorkflowPDFStream.jsp;/keygenerator/packKeyCompareFiles.jsp;/mobile/plugin/Download.jsp | | LoginExtensionFilter | weaver.filter.login.LoginExtensionFilter | /* | | | | encodingFilter | org.springframework.web.filter.CharacterEncodingFilter | | encoding | UTF-8 | | IECompatibleFilter | weaver.filter.IECompatibleFilter | *.jsp, *.js, *.htm, *.html | | | | ConnFastFilter | weaver.filter.ConnFastFilter | *.jsp, /weaver/weaver.common.util.taglib.SplitPageXmlServlet, /weaver/weaver.common.util.taglib.SplitPageXmlServletNew, /api/* | | | | MultiLangFilter | weaver.filter.MultiLangFilter | /ReportServer, /api/*, /weaver/*, *.jsp | | | | WeaverLoginFilter | weaver.weaversso.WeaverLoginFilter | *.jsp, *.html, /weaver/weaver.file.FileDownload | | | | DialogHandleFilter | weaver.filter.DialogHandleFilter | *.jsp | | | | WStatic | weaver.filter.WStaticFilter | *.gif, *.jpg, *.png, *.css, *.js, *.htm, *.html | max-age | 86400000 | | resin-ln | ln.LNFilter | *.* | | | | InitFilter | weaver.filter.InitFilter | /wui/index.html | | | | AddressAccessFilter | com.engine.edc.web.AddressAccessFilter | /* | | | | FileNamingCheckFilter | weaver.file.FileNamingCheckFilter | *.jsp, /weaver/*, /api/* | | | | DateFormatFilter | weaver.dateformat.DateFormatFilter | *.jsp, /weaver/*, /api/* | | | | WeaComponentFilter | com.weaverboot.frame.ioc.filter.WeaComponentFilter | /api/* | | | ### Servlets | Servlet Name | Servlet Class | URL Pattern | Init Param Key | Init Param Value | Load-on-startup | |------------------------------|-------------------------------------------------------------|------------------------------------------------------------------|---------------------------|---------------------------|-----------------| | GetToken | weaver.weaversso.GetToken | /ssologin/getToken | | | | | CheckToken | weaver.weaversso.CheckToken | /ssologin/checkToken | | | | | InitServer | weaver.general.InitServer | | serverName | ecology | 1 | | dwr-invoker | uk.ltd.getahead.dwr.DWRServlet | /dwr/* | crossDomainSessionSecurity | false | 1 | | | | allowScriptTagRemoting | true | | | XFireServlet | org.codehaus.xfire.transport.http.XFireConfigurableServlet | /services/* | | | | | CreateQRCodeServlet | weaver.workflow.exceldesign.CreateQRCodeServlet | /workflow/createQRCode, /createQRCode | | | | | ReplyFileDownload | weaver.docs.docs.reply.FileDownload | /weaver/weaver.docs.docs.reply.FileDownload | | | | | CreateWfBarCodeServlet | weaver.workflow.exceldesign.CreateBarCodeServlet | /workflow/createWfBarCode, /createWfBarCode | | | | | SplitPageXmlServlet | weaver.common.util.taglib.SplitPageXmlServlet | /weaver/weaver.common.util.taglib.SplitPageXmlServlet | | | | | SplitPageXmlServletNew | weaver.common.util.taglib.SplitPageXmlServletNew | /weaver/weaver.common.util.taglib.SplitPageXmlServletNew | | | | | FileDownload | weaver.file.FileDownload | /weaver/weaver.file.FileDownload | | | | | DownloadDeptLayoutServlet | weaver.org.layout.DownloadDeptLayoutServlet | /weaver/weaver.org.layout.DownloadDeptLayoutServlet | | | | | DownloadWFLayoutServlet | weaver.workflow.layout.DownloadWFLayoutServlet | /weaver/weaver.workflow.layout.DownloadWFLayoutServlet | | | | | ShowDepLayoutToPicServlet | weaver.org.layout.ShowDepLayoutToPicServlet | /weaver/weaver.org.layout.ShowDepLayoutToPicServlet | | | | | ShowDocsImageServlet | weaver.docs.docs.ShowDocsImageServlet | /weaver/weaver.docs.docs.ShowDocsImageServlet | | | | | ShowImgServlet | weaver.album.ShowImgServlet | /weaver/weaver.album.ShowImgServlet | | | | | HomePageServlet | weaver.homepage.HomepageCreateImage | /weaver/weaver.homepage.HomepageCreateImage | | | | | WorkflowXmlParser | weaver.workflow.layout.WorkflowXmlParser | /weaver/weaver.workflow.layout.WorkflowXmlParser | | | | | WorkflowDesignOperatoinServlet| weaver.workflow.layout.WorkflowDesignOperatoinServlet | /weaver/weaver.workflow.layout.WorkflowDesignOperatoinServlet | | | | | ShowWorkFlow | weaver.workflow.workflow.ShowWorkFlow | /weaver/weaver.workflow.workflow.ShowWorkFlow | | | | | ExcelOut | weaver.file.ExcelOut | /weaver/weaver.file.ExcelOut | | | | | MakeValidateCode | weaver.file.MakeValidateCode | /weaver/weaver.file.MakeValidateCode | | | | | CreateExcelServer | weaver.common.util.taglib.CreateExcelServer | /weaver/weaver.common.util.taglib.CreateExcelServer | | | | | SignatureDownLoad | weaver.file.SignatureDownLoad | /weaver/weaver.file.SignatureDownLoad | | | | | ImgFileDownload | weaver.file.ImgFileDownload | /weaver/weaver.file.ImgFileDownload | | | | | CloudStoreInit | com.cloudstore.api.util.Util_InitSys | | | | 3 | | restservlet | com.sun.jersey.spi.container.servlet.ServletContainer | /api/* | com.sun.jersey.config.property.packages | com.cloudstore;com.api | | | RsaInfo | weaver.rsa.GetRsaInfo | /rsa/weaver.rsa.GetRsaInfo | | | | | MobilemodeApiServlet | com.weaver.formmodel.gateway.servlet.MobilemodeApiServlet | /mobilemode/api/* | apiBasicPath | /mobilemode/api | | | FileDownloadForNews | weaver.file.FileDownloadForNews | /weaver/weaver.file.FileDownloadForNews | | | | | EcodeInit | com.cloudstore.api.util.Util_InitEcode | | | | 1 | | HeartBeat | weaver.ofs.heartbeat.HeartBeat | /HeartBeat | | | | | RestDispatcherServlet | weaver.rest.servlet.RestDispatcherServlet | /rest/* | | | | | FileDownloadForWps | weaver.file.other.DownloadForTokenServlet | /weaver/weaver.file.FileDownloadForWps | | | | | ExpCardServer | weaver.formmode.expcard.servlet.ExpCardServer | /weaver/weaver.formmode.expcard.servlet.ExpCardServer | | | | | FileDownloadLocation | weaver.email.FileDownloadLocation | /weaver/weaver.email.FileDownloadLocation | | | | | EdcFormViewServlet | com.engine.edc.web.EdcFormViewServlet | /edc/formview/* | | | | | OfficeServer | DBstep.OfficeServer | /OfficeServer | | | | | FileDownloadForOutDoc | weaver.file.FileDownloadForOutDoc | /weaver/weaver.file.FileDownloadForOutDoc | | | | | CreateCptBarCode | weaver.cpt.barcode.BarCodeServlet | /CreateCptBarCode | | | | | CreateBarCodeServlet | weaver.workflow.exceldesign.CreateBarCodeServlet | /createBarCode | | | | | VerifyLoginServlet | weaver.login.VerifyLoginServlet | /weaver/weaver.login.VerifyLoginServlet | | | | | ShowColServlet | weaver.common.util.taglib.ShowColServlet | /weaver/weaver.common.util.taglib.ShowColServlet | | | | | InterfaceServlet | weaver.admincenter.servlet.InterfaceServlet | /weaver/weaver.admincenter.servlet.InterfaceServlet | | | | | QYSPrivateCallbackServlet | weaver.workflow.qiyuesuo.servlet.QYSPrivateCallbackServlet | /QYSPrivateCallbackServlet | | | | | QYSDownloadFileServlet | weaver.workflow.qiyuesuo.servlet.QYSDownloadFileServlet | /weaver/weaver.workflow.qiyuesuo.servlet.QYSDownloadFileServlet | | | | | QYSInitializeServlet | weaver.workflow.qiyuesuo.servlet.QYSInitializeServlet | /QYSInitializeServlet | | | | ### MIME Mappings | Extension | MIME Type | |-----------|---------------------------| | xsd | text/xml | | xls | application/octet-stream | | xlsx | application/octet-stream | ### Error Pages | Error Code | Location | |------------|---------------------------| | 404 | /errorpage/404.htm | | 500 | /errorpage/500.htm | | 403 | /errorpage/403.htm | | 503 | /errorpage/503.htm | | 502 | /errorpage/502.htm | ### JSP Config | Taglib URI | Taglib Location | |--------------------|------------------------------| | /browserTag | /WEB-INF/tld/browser.tld | | /browser | /WEB-INF/tld/browser.tld | ``` ::: ### Filters | Filter Name | Filter Class | URL Pattern | Init Param Key | Init Param Value | |-------------------------|-----------------------------------------------------|-------------------------------------------------------------------------------------------------|-----------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | EncodingFilterWeaver | weaver.security.filter.EncodingFilter | /* | | | | SecurityTransFilter | weaver.security.encryptedtrans.SecurityTransFilter | /* | | | | CheckSensitiveFilter | com.engine.sensitive.biz.CheckSensitiveFilter | /* | | | | WeaSsoIocComponentFilter| com.weaverboot.frame.ioc.filter.WeaSsoIocComponentFilter | /api/*, *.jsp, *.html | | | | EMFilter | com.cloudstore.dev.api.service.EMFilter | /* | | | | SessionCloudFilter | com.cloudstore.dev.api.service.SessionFilter | /api/*, /page/interfaces/*.jsp, /workflow/core/ControlServlet.jsp | checkurl | /api/hrm/emmanager; | | | | | uncheckurl | /api/ec/dev/app/getCheckSystemInfo;/api/ec/dev/app/emjoin; | | | | | unchecksessionurl | | | SecurityFilter | weaver.filter.SecurityFilter | /* | | | | Compress | weaver.filter.WGzipFilter | /Messager/MessagerServlet, /weaver/weaver.common.util.taglib.SplitPageXmlServletNew, /weaver/weaver.common.util.taglib.SplitPageXmlServlet, *.jsp, /api/*, *.css, *.js | exclude | /wui/theme/ecology8/page/login.jsp;/login/login.jsp;/keygenerator/KeyGenerator.jsp;/keygenerator/getNoCheckFiles.jsp;/keygenerator/packNoCheckFiles.jsp;/workflow/request/WorkflowPDFStream.jsp;/keygenerator/packKeyCompareFiles.jsp;/mobile/plugin/Download.jsp | | LoginExtensionFilter | weaver.filter.login.LoginExtensionFilter | /* | | | | encodingFilter | org.springframework.web.filter.CharacterEncodingFilter | | encoding | UTF-8 | | IECompatibleFilter | weaver.filter.IECompatibleFilter | *.jsp, *.js, *.htm, *.html | | | | ConnFastFilter | weaver.filter.ConnFastFilter | *.jsp, /weaver/weaver.common.util.taglib.SplitPageXmlServlet, /weaver/weaver.common.util.taglib.SplitPageXmlServletNew, /api/* | | | | MultiLangFilter | weaver.filter.MultiLangFilter | /ReportServer, /api/*, /weaver/*, *.jsp | | | | WeaverLoginFilter | weaver.weaversso.WeaverLoginFilter | *.jsp, *.html, /weaver/weaver.file.FileDownload | | | | DialogHandleFilter | weaver.filter.DialogHandleFilter | *.jsp | | | | WStatic | weaver.filter.WStaticFilter | *.gif, *.jpg, *.png, *.css, *.js, *.htm, *.html | max-age | 86400000 | | resin-ln | ln.LNFilter | *.* | | | | InitFilter | weaver.filter.InitFilter | /wui/index.html | | | | AddressAccessFilter | com.engine.edc.web.AddressAccessFilter | /* | | | | FileNamingCheckFilter | weaver.file.FileNamingCheckFilter | *.jsp, /weaver/*, /api/* | | | | DateFormatFilter | weaver.dateformat.DateFormatFilter | *.jsp, /weaver/*, /api/* | | | | WeaComponentFilter | com.weaverboot.frame.ioc.filter.WeaComponentFilter | /api/* | | | ### Servlets | Servlet Name | Servlet Class | URL Pattern | Init Param Key | Init Param Value | Load-on-startup | |------------------------------|-------------------------------------------------------------|------------------------------------------------------------------|---------------------------|---------------------------|-----------------| | GetToken | weaver.weaversso.GetToken | /ssologin/getToken | | | | | CheckToken | weaver.weaversso.CheckToken | /ssologin/checkToken | | | | | InitServer | weaver.general.InitServer | | serverName | ecology | 1 | | dwr-invoker | uk.ltd.getahead.dwr.DWRServlet | /dwr/* | crossDomainSessionSecurity | false | 1 | | | | allowScriptTagRemoting | true | | | XFireServlet | org.codehaus.xfire.transport.http.XFireConfigurableServlet | /services/* | | | | | CreateQRCodeServlet | weaver.workflow.exceldesign.CreateQRCodeServlet | /workflow/createQRCode, /createQRCode | | | | | ReplyFileDownload | weaver.docs.docs.reply.FileDownload | /weaver/weaver.docs.docs.reply.FileDownload | | | | | CreateWfBarCodeServlet | weaver.workflow.exceldesign.CreateBarCodeServlet | /workflow/createWfBarCode, /createWfBarCode | | | | | SplitPageXmlServlet | weaver.common.util.taglib.SplitPageXmlServlet | /weaver/weaver.common.util.taglib.SplitPageXmlServlet | | | | | SplitPageXmlServletNew | weaver.common.util.taglib.SplitPageXmlServletNew | /weaver/weaver.common.util.taglib.SplitPageXmlServletNew | | | | | FileDownload | weaver.file.FileDownload | /weaver/weaver.file.FileDownload | | | | | DownloadDeptLayoutServlet | weaver.org.layout.DownloadDeptLayoutServlet | /weaver/weaver.org.layout.DownloadDeptLayoutServlet | | | | | DownloadWFLayoutServlet | weaver.workflow.layout.DownloadWFLayoutServlet | /weaver/weaver.workflow.layout.DownloadWFLayoutServlet | | | | | ShowDepLayoutToPicServlet | weaver.org.layout.ShowDepLayoutToPicServlet | /weaver/weaver.org.layout.ShowDepLayoutToPicServlet | | | | | ShowDocsImageServlet | weaver.docs.docs.ShowDocsImageServlet | /weaver/weaver.docs.docs.ShowDocsImageServlet | | | | | ShowImgServlet | weaver.album.ShowImgServlet | /weaver/weaver.album.ShowImgServlet | | | | | HomePageServlet | weaver.homepage.HomepageCreateImage | /weaver/weaver.homepage.HomepageCreateImage | | | | | WorkflowXmlParser | weaver.workflow.layout.WorkflowXmlParser | /weaver/weaver.workflow.layout.WorkflowXmlParser | | | | | WorkflowDesignOperatoinServlet| weaver.workflow.layout.WorkflowDesignOperatoinServlet | /weaver/weaver.workflow.layout.WorkflowDesignOperatoinServlet | | | | | ShowWorkFlow | weaver.workflow.workflow.ShowWorkFlow | /weaver/weaver.workflow.workflow.ShowWorkFlow | | | | | ExcelOut | weaver.file.ExcelOut | /weaver/weaver.file.ExcelOut | | | | | MakeValidateCode | weaver.file.MakeValidateCode | /weaver/weaver.file.MakeValidateCode | | | | | CreateExcelServer | weaver.common.util.taglib.CreateExcelServer | /weaver/weaver.common.util.taglib.CreateExcelServer | | | | | SignatureDownLoad | weaver.file.SignatureDownLoad | /weaver/weaver.file.SignatureDownLoad | | | | | ImgFileDownload | weaver.file.ImgFileDownload | /weaver/weaver.file.ImgFileDownload | | | | | CloudStoreInit | com.cloudstore.api.util.Util_InitSys | | | | 3 | | restservlet | com.sun.jersey.spi.container.servlet.ServletContainer | /api/* | com.sun.jersey.config.property.packages | com.cloudstore;com.api | | | RsaInfo | weaver.rsa.GetRsaInfo | /rsa/weaver.rsa.GetRsaInfo | | | | | MobilemodeApiServlet | com.weaver.formmodel.gateway.servlet.MobilemodeApiServlet | /mobilemode/api/* | apiBasicPath | /mobilemode/api | | | FileDownloadForNews | weaver.file.FileDownloadForNews | /weaver/weaver.file.FileDownloadForNews | | | | | EcodeInit | com.cloudstore.api.util.Util_InitEcode | | | | 1 | | HeartBeat | weaver.ofs.heartbeat.HeartBeat | /HeartBeat || | | | RestDispatcherServlet | weaver.rest.servlet.RestDispatcherServlet | /rest/* | | | | | FileDownloadForWps | weaver.file.other.DownloadForTokenServlet | /weaver/weaver.file.FileDownloadForWps | | | | | ExpCardServer | weaver.formmode.expcard.servlet.ExpCardServer | /weaver/weaver.formmode.expcard.servlet.ExpCardServer | | | | | FileDownloadLocation | weaver.email.FileDownloadLocation | /weaver/weaver.email.FileDownloadLocation | | | | | EdcFormViewServlet | com.engine.edc.web.EdcFormViewServlet | /edc/formview/* | | | | | OfficeServer | DBstep.OfficeServer | /OfficeServer | | | | | FileDownloadForOutDoc | weaver.file.FileDownloadForOutDoc | /weaver/weaver.file.FileDownloadForOutDoc | | | | | CreateCptBarCode | weaver.cpt.barcode.BarCodeServlet | /CreateCptBarCode | | | | | CreateBarCodeServlet | weaver.workflow.exceldesign.CreateBarCodeServlet | /createBarCode | | | | | VerifyLoginServlet | weaver.login.VerifyLoginServlet | /weaver/weaver.login.VerifyLoginServlet | | | | | ShowColServlet | weaver.common.util.taglib.ShowColServlet | /weaver/weaver.common.util.taglib.ShowColServlet | | | | | InterfaceServlet | weaver.admincenter.servlet.InterfaceServlet | /weaver/weaver.admincenter.servlet.InterfaceServlet | | | | | QYSPrivateCallbackServlet | weaver.workflow.qiyuesuo.servlet.QYSPrivateCallbackServlet | /QYSPrivateCallbackServlet | | | | | QYSDownloadFileServlet | weaver.workflow.qiyuesuo.servlet.QYSDownloadFileServlet | /weaver/weaver.workflow.qiyuesuo.servlet.QYSDownloadFileServlet | | | | | QYSInitializeServlet | weaver.workflow.qiyuesuo.servlet.QYSInitializeServlet | /QYSInitializeServlet | | | | ### MIME Mappings | Extension | MIME Type | |-----------|---------------------------| | xsd | text/xml | | xls | application/octet-stream | | xlsx | application/octet-stream | ### Error Pages | Error Code | Location | |------------|---------------------------| | 404 | /errorpage/404.htm | | 500 | /errorpage/500.htm | | 403 | /errorpage/403.htm | | 503 | /errorpage/503.htm | | 502 | /errorpage/502.htm | ### JSP Config | Taglib URI | Taglib Location | |--------------------|------------------------------| | /browserTag | /WEB-INF/tld/browser.tld | | /browser | /WEB-INF/tld/browser.tld | 通过这种方式,你可以更容易地查看和理解 `web.xml` 文件中的配置项。 --- # 身份证号码校验及识别提取函数解析 作者:丸子·张 来源:https://blog.oneszhang.com/2024/06/18/73/ Markdown:https://blog.oneszhang.com/2024/06/18/73.md 发布日期:2024-06-18T02:42:00.000Z 更新时间:2024-06-18T02:42:00.000Z 专栏:前端与脚本 标签: 摘要:文章围绕身份证号码的校验与信息提取展开,先给出校验函数,从正则格式、出生日期合理性和校验码三方面逐层判断,再通过提取函数从号码中解析籍贯、生日、性别与年龄,并附综合调用示例。适合需要在前端处理实名信息的开发者参考,同时提醒注意身份证号的保密与隐私保护。 # 身份证号码校验及识别提取函数解析 > 📌 > > 该文章由丸子引导AI辅助制作生成,如有改进意见,欢迎评论区留言。 > ## 引言 身份证号码是每个公民的重要凭证,在日常生活和工作中有着广泛的应用。本文将详细介绍身份证号码的校验及其相关信息的提取函数,帮助读者理解并实现身份证号码的有效验证和信息提取。 ## 身份证号校验函数 以下是身份证号校验函数的代码展示: ```javascript // 身份证号校验函数 function validateIDCard(id) { // 1 "验证通过!", 0 //校验不通过 var format = /^(([1][1-5])|([2][1-3])|([3][1-7])|([4][1-6])|([5][0-4])|([6][1-5])|([7][1])|([8][1-2]))\d{4}(([1][9]\d{2})|([2]\d{3}))(([0][1-9])|([1][0-2]))(([0][1-9])|([1-2][0-9])|([3][0-1]))\d{3}[0-9xX]$/; //号码规则校验 if (!format.test(id)) { return { 'status': 0, 'msg': '身份证号码不合规' }; } //区位码校验 //出生年月日校验 前正则限制起始年份为1900; var year = id.substr(6, 4), //身份证年 month = id.substr(10, 2), //身份证月 date = id.substr(12, 2), //身份证日 time = Date.parse(month + '-' + date + '-' + year), //身份证日期时间戳date now_time = Date.parse(new Date()), //当前时间戳 dates = (new Date(year, month, 0)).getDate(); //身份证当月天数 if (time > now_time || date > dates) { return { 'status': 0, 'msg': '出生日期不合规' } } //校验码判断 var coefficients = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]; //系数 var checkCodeMap = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2']; //校验码对照表 var idArray = id.split(""); var sum = 0; for (var i = 0; i < 17; i++) { sum += parseInt(idArray[i]) * coefficients[i]; } if (idArray[17].toUpperCase() != checkCodeMap[sum % 11].toUpperCase()) { return { 'status': 0, 'msg': '身份证校验码不合规' } } return { 'status': 1, 'msg': '校验通过' } } ``` ### 实现原理与步骤解析 1. **正则表达式校验**:使用正则表达式检查身份证号码的格式是否正确,包括省份代码、出生日期等基本信息。 2. **出生日期校验**:通过提取身份证中的出生日期,并与当前日期进行对比,确保出生日期合理。 3. **校验码校验**:计算身份证号码的校验码,并与身份证号码中的校验码进行对比,确保其正确性。 ## 身份证号识别提取函数 以下是身份证号识别提取函数的代码展示: ```javascript // 根据身份证号获取籍贯、生日、性别、年龄 function extractIDCardData(idCard) { if (idCard !== '') { // 根据身份证号获取籍贯 var hometownMap = { 11: "北京", 12: "天津", 13: "河北", 14: "山西", 15: "内蒙古", 21: "辽宁", 22: "吉林", 23: "黑龙江", 31: "上海", 32: "江苏", 33: "浙江", 34: "安徽", 35: "福建", 36: "江西", 37: "山东", 41: "河南", 42: "湖北", 43: "湖南", 44: "广东", 45: "广西", 46: "海南", 50: "重庆", 51: "四川", 52: "贵州", 53: "云南", 54: "西藏", 61: "陕西", 62: "甘肃", 63: "青海", 64: "宁夏", 65: "新疆", 71: "台湾", 81: "香港", 82: "澳门" }; hometown = hometownMap[parseInt(idCard.substring(0, 2))]; // 根据身份证号获取出生日期 birthday = idCard.substring(6, 10) + "-" + idCard.substring(10, 12) + "-" + idCard.substring(12, 14); // 根据身份证号获取性别 gender = parseInt(idCard.substr(16, 1)) % 2 == 1 ? "男" : "女"; // 根据身份证号获取年龄 var currentDate = new Date(); age = currentDate.getFullYear() - parseInt(idCard.substring(6, 10)) - 1; } } ``` ### 实现原理与步骤解析 1. **籍贯提取**:通过身份证号码的前两位数字,从预定义的省市代码表中获取籍贯信息。 2. **生日提取**:提取身份证号码中的出生年份、月份和日期,并组装成完整的出生日期。 3. **性别提取**:通过身份证号码的倒数第二位数字确定性别,奇数为男性,偶数为女性。 4. **年龄计算**:根据当前年份和身份证中的出生年份计算年龄。 ## 综合示例 下面是一个综合使用校验与提取函数的实例讲解: ```javascript var idCard = "11010519491231002X"; var validationResult = validateIDCard(idCard); if (validationResult.status === 1) { extractIDCardData(idCard); console.log("籍贯: " + hometown); console.log("生日: " + birthday); console.log("性别: " + gender); console.log("年龄: " + age); } else { console.log(validationResult.msg); } ``` 通过上述实例,我们可以验证身份证号码的有效性,并提取相关信息,如籍贯、生日、性别和年龄。 ## 结论 本文详细介绍了身份证号码的校验函数和信息提取函数,通过代码展示和原理解析,帮助读者理解并实现身份证号码的有效验证和信息提取。使用时请注意身份证号码的保密性,避免泄露个人隐私。 :::tip 提示:在实际应用中,请确保输入的身份证号码真实有效,并妥善处理用户隐私信息。 ::: --- # Windows连接共享打印机,无法连接0x0000011b 作者:丸子·张 来源:https://blog.oneszhang.com/2024/05/28/72/ Markdown:https://blog.oneszhang.com/2024/05/28/72.md 发布日期:2024-05-28T03:54:00.000Z 更新时间:2024-05-28T03:54:00.000Z 专栏:前端与脚本 标签: 摘要:针对 Windows 连接共享打印机时报 0x0000011b 错误,给出的办法是在已安装打印机的电脑上修改注册表,新建相应键值并设为 0,随后重启打印后台服务或电脑即可解决。操作时务必确认是在安装了打印机的主机上进行,适合被该错误困扰的共享打印用户参考。 # 解决方案 > ⚠️ **特别注意!** > > 在 " 安 装 了 打 印 机 " 的电脑上进行! > 在 " 安 装 了 打 印 机 " 的电脑上进行! > 在 " 安 装 了 打 印 机 " 的电脑上进行! > 1. 按 `Windows 键` + `R` 2. 输入 `regedit.exe` 点击`确认`,这时打开了注册表编辑器 3. 找到此路径: `HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Print` 4. 空白处鼠标右键,点击`新建`→ `DWORD (32 位)值(D)`,命名为“RpcAuthnLevelPrivacyEnabled”,设置键值为“0”,其他保持默认。 5. 在服务中重启`Print Spooler`,或直接重启电脑! 6. 解决完成~ > 解决方案来源:https://learn.microsoft.com/en-us/answers/questions/1510837/how-to-fix-error-0x0000011b-when-adding-networked --- # Doris 2.0.3+版本 使用Dorker部署之巨坑!!! 作者:丸子·张 来源:https://blog.oneszhang.com/2024/05/24/71/ Markdown:https://blog.oneszhang.com/2024/05/24/71.md 发布日期:2024-05-24T07:20:05.000Z 更新时间:2024-05-24T07:20:05.000Z 专栏:部署与自托管 标签: 摘要:使用 Doris 2.0.3 及以上版本通过 Docker 部署时,若照搬官方文档的 docker run 命令,会遇到 BE 注册失败、FE 心跳超时等报错。原因是官方文档未同步更新,命令中的环境变量与新版 init_be.sh 不匹配,旧变量已弃用。按提交记录换成新变量,并同步修改 docker-compose 的 environment 配置即可解决。 # 截止2024年5月24日 Doris官方文档,没更新修改!!!! ### 就是因为这个问题,我真是要哭死了!搞了很久!希望下一个小白不会踩我的坑! [Doris官方文档 - Docker 部署](https://doris.apache.org/zh-CN/docs/install/cluster-deployment/run-docker-cluster ) ### 如果你正在使用官方给出的 **Docker Run 命令**来进行,那你应该会碰到了我踩的坑! ### 如果你出现了以下报错,这篇帖子或许能帮到你! - 报错1: ``` Statistics storage initiated failed, will try again later org.apache.doris.common.DdlException: errCode = 2, detailMessage = System has no available disk capacity or no available BE nodes ``` - 报错2: ``` waiting to receive first heartbeat from frontend before doing report... waiting to receive first heartbeat from frontend before doing report... waiting to receive first heartbeat from frontend before doing report ... ``` - 报错3: ``` Register BE to FE is failed. retry. Register BE to FE is failed. retry. Register BE to FE is failed. retry. ... ``` - 报错4: ``` start check be status~ register is failed, wait next~ start check be status~ register is failed, wait next~ ... ``` # 原因 官方文档给出的 **Docker Run 命令**环境变量与当前版本init_be.sh不符! 到现在!都没有更新!简直先说一声!我上早7+1!!!! **原变量:** `FE_SERVERS` 、 `BE_ADDR` 弃用 **新变量:** `MASTER_FE_IP` 、 `CURRENT_BE_IP` 、 `CURRENT_BE_PORT` **究极追溯:** https://github.com/apache/doris/commit/c323bc44ff24402152b5c19c69502651ea10ed55#diff-72338387508537b2b23b63be1aa5f218839182d4f4104453475b70b21bcba2b0R70 # 解决方法: ``` // --env FE_SERVERS="fe1:127.0.0.1:9010" --env MASTER_FE_IP="127.0.0.1" //--env BE_ADDR="127.0.0.1:9050" --env CURRENT_BE_IP="127.0.0.1" --env CURRENT_BE_PORT="9050" ``` 如果你在使用docker-compose,相应的修改你的environment! --- # [泛微 Ecology9] 建模查询触发“数据审批”接口! 作者:丸子·张 来源:https://blog.oneszhang.com/2024/05/20/70/ Markdown:https://blog.oneszhang.com/2024/05/20/70.md 发布日期:2024-05-20T07:20:00.000Z 更新时间:2024-05-20T07:20:00.000Z 专栏:泛微 E9 标签: 摘要:在泛微 Ecology EC9 中,通过向建模查询的扩展接口发送 POST 请求,可以触发数据审批,实测可行。请求需携带页面扩展按钮、数据、模式、表单、查询等标识参数,并固定查询标识;作者记录了相关参数和示例,但各参数是否必填尚未确认。适用于需要将建模查询与审批流程联动开发的场景。 刚测了,OKK的K~ 先记录下: 触发数据审批 请求接口:/api/cube/expand/interface 请求类型:POST 请求内容(raw - text): pageexpandid=页面扩展按钮ID&billid=数据id&modeid=modeid&formid=表单id&customid=查询id&guid=search (参数是否必填未知) 示例: pageexpandid=2173&billid=16&modeid=30&formid=-81&customid=77&guid=search --- # 今天!卸载小米手机中的快应用 作者:丸子·张 来源:https://blog.oneszhang.com/2024/05/01/68/ Markdown:https://blog.oneszhang.com/2024/05/01/68.md 发布日期:2024-05-01T06:18:00.000Z 更新时间:2024-05-01T06:18:00.000Z 专栏:生活与随笔 标签: 摘要:文章记录作者为奶奶解决红米手机使用快手极速版时频繁自动跳转到快应用、且广告界面无法关闭的问题,并发现快应用权限列表中有大量条目。随后作者用电脑、数据线和 USB 调试连接手机,通过 adb 工具禁用快应用,并提到 Shizuku 可作后续方案。 快应用的出发点是好的,真的没想到却成为了各种厂商恶心的工具! 去年给奶奶买了一套红米K50,奶奶表示用着很舒服,这次五一回家,奶奶给我说她的快手极速版领金币点开视频没法点击关闭(我爸在一旁表示不信,不可能,并表示不可能没有关闭按钮!),起初我也是想在4202年的今天,基本不可能有这种事情发生,然而奶奶复现这个问题的时候,我发现还真是如奶奶所说,她点开一个领金币的视频,几秒钟广告后就会自动跳转到快应用,然而让我意想不到的是,这个快应用,不止这一个,界面只是一个广告,整个界面所有关闭按钮都是假的!!!没有任何直接关闭的方法,只能从后台关掉!在4202年的今天这种事情,真就离了大谱!!! 于是我当即把几个快应用的权限全部关闭掉了,包块唤醒等功能,奶奶说解决了,没想到的是第二天奶奶说又不行了,我才开始真正了解到快应用的恐怖之处,当我打开快应用的权限管理时,几十个快应用映入眼帘!我当时,惊呆了! 通过搜索发现,快应用,真的已经成了快广告! ![image.png](https://oneszhang.com/usr/uploads/2024/05/1808246134.png#mirages-width=2488&mirages-height=1269&mirages-cdn-type=3) 首先准备工作就是一台电脑,一根数据线以及自己的手机。将电脑上下载具有 adb 命令的工具,安装后解压到 C 盘。(比如秋之盒/搞机助手,搞机工具箱等等具有类似功能的软件)之后打开软件,将手机打开:开发者设置,打开 USB 调试功能。然后用数据线连接手机和电脑。可以输入以下命令: adb shell pm disable-user 加包名 禁用 adb shell pm enable 加包名 解禁 adb uninstall 加包名 卸载 在这里。我们选择禁用,输入以下命令:adb shell pm disable-user com.miui.hybrid 。就可以完全禁用快应用啦。 Shizuku 也是个好东西,以后研究 [用户手册 | Shizuku (rikka.app)](https://shizuku.rikka.app/zh-hans/guide/setup/) --- # [泛微 Ecology9] 一键重置流程转数据字段映射 作者:丸子·张 来源:https://blog.oneszhang.com/2024/03/29/65/ Markdown:https://blog.oneszhang.com/2024/03/29/65.md 发布日期:2024-03-29T01:37:00.000Z 更新时间:2024-03-29T01:37:00.000Z 专栏:泛微 E9 标签: 摘要:介绍在泛微 Ecology 流程转数据配置页通过 ecodeSDK 注入重置字段映射按钮并复写保存请求、实现一键清空字段映射的二开方案。因映射下拉框实为 Table 组件无法直接复写,作者改用拦截请求参数的迂回思路实现,第二版还支持按表选择清空范围、修复状态未复位问题并限定按钮仅在字段映射页签显示,适合 Ecology 运维与 ecode 开发者参考。 # A.引言 还记得去年某天我在讨论群里问清空字段映射的操作,今晚实现啦#(哭泣) 回想那天,我还正面对着屏幕上的几百个字段映射在发愁。#(想一想) 做这个功能主要是为了练手,好久没写ecode了; 如果你正巧需要,这能减轻你的运维工作量,那便更有意义了! 如果你还在学习路上,配合[泛微 EC9 之 ecodeSDK API 文档](https://oneszhang.com/archives/ecodeSDK.html)食用更佳~ # B.功能演示 # C.曲折过程 :::details[很烦!遇到了坑!] **这个页面的数据字段下拉框,竟然不能通过Select或WeaSelect组件复写!!!** 一开始我做这个需求的时候,还感觉很简单,~~喵的,不就是个下拉框嘛,分分钟!~~ 令我没想到的是,怎么查也没查到组件名。。。。 最终还是靠我的[泛微 ecode 开发辅助 - ecode 组件全局检索](https://oneszhang.com/archives/64.html) 发现竟然用的非标准Select组件,而是,一整个Table组件!!!😓 ``` ecodeSDK.overwritePropsFnQueueMapSet('Select', { fn: (newProps) => { if (window.location.hash.indexOf('#/workflowengine/path/pathSet/pathDetail/advanceSet/workflowToMode') < 0) return; console.log('"Select newProps":', newProps); console.log('!找到Select组件啦!!!!!!'); }, }); ecodeSDK.overwritePropsFnQueueMapSet('WeaSelect', { fn: (newProps) => { if (window.location.hash.indexOf('#/workflowengine/path/pathSet/pathDetail/advanceSet/workflowToMode') < 0) return; console.log('"WeaSelect newProps":', newProps); console.log('!找到WeaSelect组件啦!!!!!!'); }, }); ``` 再看Table组件,好家伙,这个组件没有存值!😒 好好好,改DOM结构试试!也不行!🤮 a~~ #(皱眉)怎么办呢?思索片刻,行!我直接‘迂回策略’~复写请求!,开搞! ::: # D.最终代码 :::details[V1版本(历史):只清空/重置全部字段映射] ``` let isClearButtonClicked = false; ecodeSDK.overwritePropsFnQueueMapSet('WeaDialog', { fn: (newProps) => { if (window.location.hash.indexOf('#/workflowengine/path/pathSet/pathDetail/advanceSet/workflowToMode') < 0) return; if (newProps.title && newProps.title.includes('流程转数据:')) { const { Tooltip, Button } = antd; newProps.buttons.unshift( ); function handleClearMapping() { if (window.antd && window.antd.Modal) { window.antd.Modal.warning({ title: '风险提示', content: '即将重置字段映射配置,请谨慎操作!', okText: '确认重置', cancelText: '取消操作', onOk: () => { isClearButtonClicked = true; const saveButton = newProps.buttons.find(button => button.props.children === "保存"); if (saveButton && saveButton.props.onClick) { saveButton.props.onClick(); } }, }); } } } }, }); ecodeSDK.rewriteApiParamsQueueSet({ fn: (url, method, params) => { if (isClearButtonClicked && url.indexOf("/api/cube/mode/mode/saveWorkflowToMode") !== -1) { Object.keys(params).forEach(key => { if (key.startsWith('wffieldid') && key !== 'wffieldid_0_0') { params[key] = ''; } }); } return { url: url, method: method, params: params }; } }); ``` ::: 2024年3月29日 15:10:54 丸子根据今日收到的需求,感觉比较合理,所以更新了第二版本 **更新内容如下:** - 增加选择清空表功能————可以根据情况,选择自己需要清空的表 - 修复点击重置字段映射按钮后没有取消重置状态的BUG - 修改为只在字段映射页签显示按钮 ![选择清空表][1] emmmm增加选择表功能后,其实可以通过逻辑来处理掉 需要选择主表第一个字段映射的问题,但是,嘿嘿,我就不,略略略!😉 ``` let isClearButtonClicked = false; let mainActiveKey = ''; let selectOptions = ''; let selectValue = ''; let clearButton = ''; ecodeSDK.overwritePropsFnQueueMapSet('WeaDialog', { fn: (newProps) => { if (window.location.hash.indexOf('#/workflowengine/path/pathSet/pathDetail/advanceSet/workflowToMode') < 0) return; if (newProps.title && newProps.title.includes('流程转数据:')) { const { Tooltip, Button,Select } = antd; newProps.buttons.unshift( ); function handleClearMapping() { if (window.antd && window.antd.Modal) { console.log("selectValue 默认值为:", selectValue); const content = (
重置字段映射配置,请谨慎操作!
即将清空以下字段映射:
); window.antd.Modal.warning({ title: '风险提示', content: content, okText: '确认重置', cancelText: '取消操作', onOk: () => { isClearButtonClicked = true; const saveButton = newProps.buttons.find(button => button.props.children === "保存"); if (saveButton && saveButton.props.onClick) { saveButton.props.onClick(); } }, }); } } } }, }); ecodeSDK.rewriteApiParamsQueueSet({ fn: (url, method, params) => { if (isClearButtonClicked && url.indexOf("/api/cube/mode/mode/saveWorkflowToMode") !== -1) { Object.keys(params).forEach(key => { if (key.startsWith('wffieldid') && key !== 'wffieldid_0_0') { if (selectValue.includes(key.split('_')[1])) { params[key] = ''; } } }); isClearButtonClicked = false; } return { url: url, method: method, params: params }; } }); ecodeSDK.overwritePropsFnQueueMapSet('Tabs', { fn: (newProps) => { if (window.location.hash.indexOf('#/workflowengine/path/pathSet/pathDetail/advanceSet/workflowToMode') < 0) return; if (newProps.children && newProps.children.length > 0 && newProps.children[0].props.tab === '基础信息') { mainActiveKey = newProps.activeKey; clearButton = clearButton || document.querySelector('.clear-button'); clearButton && (clearButton.style.display = mainActiveKey !== '2' ? "none" : ""); } if (newProps.children && newProps.children.length > 0 && newProps.children[0].props.tab === '主表') { selectOptions = newProps.children.map(child => ({ key: child.key, value: child.props.tab })); selectValue = selectOptions.map(option => option.key); } }, }); ``` > ✅ > > 成功的关键在于不断探索和尝试,直到找到解决问题的方法。 > [1]: https://cos.oneszhang.com/usr/uploads/old/blog_20240329_mapping/addselect.png --- # [泛微 Ecology9] 开发辅助-ecode组件全局检索 作者:丸子·张 来源:https://blog.oneszhang.com/2024/03/20/64/ Markdown:https://blog.oneszhang.com/2024/03/20/64.md 发布日期:2024-03-20T08:35:00.000Z 更新时间:2024-03-20T08:35:00.000Z 专栏:泛微 E9 标签: 摘要:针对泛微 ecode 开发中难以定位页面组件的问题,介绍了一套 PC 端组件全局检索高亮方案。借助全局开关和目标页面路径限定生效范围,监控指定组件并安全序列化打印其属性,同时在页面右下角添加搜索框,匹配到的组件会闪烁高亮,方便开发者快速确认组件名称、来源以及能否复写。 > 📌 > > 该文章由丸子引导AI辅助制作生成,如有改进意见,欢迎评论区留言。 > # A.引言 😠 找个组件!是真难! 这个是不是ecode支持的组件? 我能不能复写? 阿西吧,所以,有了`[泛微ecode] PC端页面ecode组件全局检索高亮`!!!!! 这个项目应该叫“**喵的,你到底是哪个组件**” ![查个表](https://cos.oneszhang.com/usr/uploads/old/blog_20240320_ecodefind/ecodefindtable.gif) ![查个我知道](https://cos.oneszhang.com/usr/uploads/old/blog_20240320_ecodefind/ecodefindbutton.gif) # B.实现全局检索的技术方案 `全局开关的设定` 首先,我们需要设定一个全局开关`enable`,以便在不需要此功能时,可以轻松地关闭它。 `目标页面路径的确定` 通过`targetPath`变量指定要进行检索的页面路径,这样我们的检索功能就会局限于特定的页面,避免对其他页面产生影响。 `组件闪烁效果的实现` 实现一个`flashElement`函数,用于使符合条件的组件产生闪烁效果,以达到高亮闪烁提示。 `Props序列化处理` 在JavaScript中,直接序列化对象可能会因为循环引用等问题抛出异常。因此,我们实现了一个`safeStringify`函数,用于安全地序列化对象,同时处理了日期、函数和未定义值的特殊情况。 `组件监控与信息打印` 我们通过`componentsToMonitor`数组定义了需要监控的组件列表,并为每个组件设置了一个钩子函数,用于在组件的属性发生变化时执行特定的操作。这里的操作包括检查路径、过滤重复的组件、打印找到的组件信息等。 `搜索框的添加与功能实现` 在页面的右下角动态添加一个搜索框,用户可以通过输入关键词来搜索组件信息。搜索结果会在控制台中打印出来,并对匹配的组件进行高亮显示。 # C.代码拿去 :::details[register.js文件代码] ``` const enable = true; // 全局开关 const targetPath = '#/main/cube/search?customid=77';//修改这里为要检查的页面path const foundComponents = []; let searchBoxAdded = false; let searchBox; // Props序列化 function safeStringify(obj) { console.log('objJSON化开始:', obj); const seen = new WeakSet(); try { return JSON.stringify(obj, (key, value) => { if (value instanceof Date) { return value.toISOString(); } if (typeof value === "object" && value !== null) { if (seen.has(value)) { return '[Circular]'; } seen.add(value); } else if (typeof value === "function") { return `[Function: ${value.name || 'anonymous'}]`; } else if (typeof value === "undefined") { return '[Undefined]'; } return value; }); } catch (error) { console.error('Error during object serialization:', error.message); return '{}'; // 或者返回一个错误信息的JSON字符串 } } // 组件闪烁 function flashElement(selector, isClassName) { let elements; if (isClassName) { // 如果提供的是classname,直接使用classname查找元素 elements = document.querySelectorAll(`.${selector}`); } else { // 如果提供的是ecId,使用属性选择器查找元素 elements = document.querySelectorAll(`[ecid^='${selector}']`); } elements.forEach((element) => { element.classList.add('flash-highlight'); // 确保闪烁结束后移除类,避免影响后续样式 setTimeout(() => { element.classList.remove('flash-highlight'); }, 3000); // 根据闪烁次数和持续时间调整,这里是3秒后(1秒闪烁动画,共3次) }); } const componentsToMonitor = [{ "Name": "WeaAutoGraph", "CName": "签章", "Group": "comMobx" }, { "Name": "WeaForm", "CName": "表单", "Group": "comMobx" }, { "Name": "WeaLogView", "CName": "日志查看", "Group": "comMobx" }, { "Name": "WeaMessageCenter", "CName": "消息中心", "Group": "comMobx" }, { "Name": "WeaSwitch", "CName": "WeaSwitch", "Group": "comMobx" }, { "Name": "WeaTableMobx", "CName": "分页列表组件", "Group": "comMobx" }, { "Name": "WeaIconfont", "CName": "图标", "Group": "ecCom" }, { "Name": "FixedColumns", "CName": "表格固定列", "Group": "ecCom" }, { "Name": "WeaAlertPage", "CName": "空白页", "Group": "ecCom" }, { "Name": "WeaAppCenter", "CName": "应用中心", "Group": "ecCom" }, { "Name": "WeaAudio", "CName": "语音", "Group": "ecCom" }, { "Name": "WeaAuth", "CName": "添加权限", "Group": "ecCom" }, { "Name": "WeaAvatar", "CName": "头像组件", "Group": "ecCom" }, { "Name": "WeaBrowser", "CName": "浏览按钮", "Group": "ecCom" }, { "Name": "WeaCascader", "CName": "级联组件", "Group": "ecCom" }, { "Name": "WeaCheckbox", "CName": "选择框", "Group": "ecCom" }, { "Name": "WeaCodeMirror", "CName": "代码编辑", "Group": "ecCom" }, { "Name": "WeaCollect", "CName": "添加收藏", "Group": "ecCom" }, { "Name": "WeaColorPicker", "CName": "颜色选择", "Group": "ecCom" }, { "Name": "WeaCron", "CName": "生成表达式", "Group": "ecCom" }, { "Name": "WeaDateGroup", "CName": "日期组合", "Group": "ecCom" }, { "Name": "WeaDatePicker", "CName": "日期", "Group": "ecCom" }, { "Name": "WeaDateSwitch", "CName": "时段切换", "Group": "ecCom" }, { "Name": "WeaDialog", "CName": "弹框", "Group": "ecCom" }, { "Name": "WeaDragChoose", "CName": "拖动选择", "Group": "ecCom" }, { "Name": "WeaDraggable", "CName": "拖拽", "Group": "ecCom" }, { "Name": "WeaDropdown", "CName": "下拉", "Group": "ecCom" }, { "Name": "WeaEchart", "CName": "图表", "Group": "ecCom" }, { "Name": "WeaEcodeUpload", "CName": "上传至云商店", "Group": "ecCom" }, { "Name": "WeaError", "CName": "错误提示", "Group": "ecCom" }, { "Name": "WeaFieldTemplateSelect", "CName": "模板字段选择", "Group": "ecCom" }, { "Name": "WeaFormItem", "CName": "表单布局", "Group": "ecCom" }, { "Name": "WeaHelpfulTip", "CName": "帮助提示", "Group": "ecCom" }, { "Name": "WeaImageCropper", "CName": "图片编辑", "Group": "ecCom" }, { "Name": "WeaInput", "CName": "单行文本", "Group": "ecCom" }, { "Name": "WeaInputNumber", "CName": "数字输入框", "Group": "ecCom" }, { "Name": "WeaInputSearch", "CName": "输入框(带放大镜)", "Group": "ecCom" }, { "Name": "WeaIntro", "CName": "页面引导", "Group": "ecCom" }, { "Name": "WeaLeftRightLayout", "CName": "左右布局", "Group": "ecCom" }, { "Name": "WeaLeftTree", "CName": "左侧树", "Group": "ecCom" }, { "Name": "WeaLoadingGlobal", "CName": "全局遮罩loading", "Group": "ecCom" }, { "Name": "WeaLocaleProvider", "CName": "多语言", "Group": "ecCom" }, { "Name": "WeaMap", "CName": "地图", "Group": "ecCom" }, { "Name": "WeaMind", "CName": "思维导图", "Group": "ecCom" }, { "Name": "WeaMoreButton", "CName": "更多按钮", "Group": "ecCom" }, { "Name": "WeaNewScroll", "CName": "新滚动条", "Group": "ecCom" }, { "Name": "WeaNewScrollPagination", "CName": "滚动翻页", "Group": "ecCom" }, { "Name": "WeaOrgTree", "CName": "组织树", "Group": "ecCom" }, { "Name": "WeaPeriod", "CName": "时间段选择", "Group": "ecCom" }, { "Name": "WeaPopoverHrm", "CName": "人力卡片", "Group": "ecCom" }, { "Name": "WeaProgress", "CName": "进度条", "Group": "ecCom" }, { "Name": "WeaQRCode", "CName": "二维码", "Group": "ecCom" }, { "Name": "WeaRadioGroup", "CName": "条件组", "Group": "ecCom" }, { "Name": "WeaRangePicker", "CName": "日期区间", "Group": "ecCom" }, { "Name": "WeaRate", "CName": "评分", "Group": "ecCom" }, { "Name": "WeaReport", "CName": "报表型列表", "Group": "ecCom" }, { "Name": "WeaReportCard", "CName": "报表卡片", "Group": "ecCom" }, { "Name": "WeaReportGroup", "CName": "报表组", "Group": "ecCom" }, { "Name": "WeaReportInfo", "CName": "报表开发说明", "Group": "ecCom" }, { "Name": "WeaReqTop", "CName": "顶部(表单)", "Group": "ecCom" }, { "Name": "WeaRichText", "CName": "富文本编辑器", "Group": "ecCom" }, { "Name": "WeaRightMenu", "CName": "右键菜单", "Group": "ecCom" }, { "Name": "WeaScope", "CName": "数值范围", "Group": "ecCom" }, { "Name": "WeaSearchGroup", "CName": "搜索面板", "Group": "ecCom" }, { "Name": "WeaSearchInput", "CName": "热词联想搜索框", "Group": "ecCom" }, { "Name": "WeaSearchPanel", "CName": "高级搜索面板", "Group": "ecCom" }, { "Name": "WeaSelect", "CName": "下拉框", "Group": "ecCom" }, { "Name": "WeaSelectGroup", "CName": "一级联动", "Group": "ecCom" }, { "Name": "WeaShare", "CName": "分享", "Group": "ecCom" }, { "Name": "WeaSignature", "CName": "批注", "Group": "ecCom" }, { "Name": "WeaSlideModal", "CName": "滑块", "Group": "ecCom" }, { "Name": "WeaSortable", "CName": "列表拖拽", "Group": "ecCom" }, { "Name": "WeaSteps", "CName": "步骤条", "Group": "ecCom" }, { "Name": "WeaStepsMenus", "CName": "自定义菜单条", "Group": "ecCom" }, { "Name": "WeaTab", "CName": "页签", "Group": "ecCom" }, { "Name": "WeaTable", "CName": "表格", "Group": "ecCom" }, { "Name": "WeaTableEdit", "CName": "迁移至 WeaTableEditable", "Group": "ecCom" }, { "Name": "WeaTableEditable", "CName": "可编辑列表", "Group": "ecCom" }, { "Name": "WeaTag", "CName": "标签", "Group": "ecCom" }, { "Name": "WeaTagGroup", "CName": "标签组", "Group": "ecCom" }, { "Name": "WeaTextarea", "CName": "多行文本", "Group": "ecCom" }, { "Name": "WeaTimePicker", "CName": "时间", "Group": "ecCom" }, { "Name": "WeaTimeRangePicker", "CName": "时间区间", "Group": "ecCom" }, { "Name": "WeaTools_storage", "CName": "本地缓存", "Group": "ecCom" }, { "Name": "WeaTools_fetch", "CName": "异步请求", "Group": "ecCom" }, { "Name": "WeaTools_error", "CName": "异常处理", "Group": "ecCom" }, { "Name": "WeaTools_getComponent", "CName": "按钮选择(redux版)", "Group": "ecCom" }, { "Name": "WeaTools_Base64", "CName": "base64编码", "Group": "ecCom" }, { "Name": "WeaTools_math", "CName": "math", "Group": "ecCom" }, { "Name": "WeaTools_downloadFile", "CName": "文件下载", "Group": "ecCom" }, { "Name": "WeaTools_eventRegister", "CName": "事件队列", "Group": "ecCom" }, { "Name": "WeaTools_dialog", "CName": "创建react组件", "Group": "ecCom" }, { "Name": "WeaTools_messageCenter", "CName": "消息中心", "Group": "ecCom" }, { "Name": "WeaTools_watermark", "CName": "水印", "Group": "ecCom" }, { "Name": "WeaTools_sendMag", "CName": "发消息", "Group": "ecCom" }, { "Name": "WeaTools_getEmAudioOrVideo", "CName": "调用em音视频方法", "Group": "ecCom" }, { "Name": "WeaTools_ding", "CName": "ding", "Group": "ecCom" }, { "Name": "WeaTools_viewer", "CName": "图片预览", "Group": "ecCom" }, { "Name": "WeaTools_unloadhook", "CName": "页面跳转提示", "Group": "ecCom" }, { "Name": "WeaTop", "CName": "顶部", "Group": "ecCom" }, { "Name": "WeaTransfer", "CName": "穿梭框", "Group": "ecCom" }, { "Name": "WeaTree", "CName": "树", "Group": "ecCom" }, { "Name": "WeaUpload", "CName": "上传", "Group": "ecCom" }, { "Name": "WeaVideo", "CName": "视频播放", "Group": "ecCom" }, { "Name": "WeaYear", "CName": "年视图", "Group": "ecCom" }, { "Name": "WeaZmage", "CName": "图片缩放", "Group": "ecCom" }, { "Name": "Affix", "CName": "固钉", "Group": "antd" }, { "Name": "Alert", "CName": "警告提示", "Group": "antd" }, { "Name": "Badge", "CName": "徽标数", "Group": "antd" }, { "Name": "Breadcrumb", "CName": "面包屑", "Group": "antd" }, { "Name": "Button", "CName": "按钮", "Group": "antd" }, { "Name": "Calendar", "CName": "日历", "Group": "antd" }, { "Name": "Card", "CName": "卡片", "Group": "antd" }, { "Name": "Carousel", "CName": "走马灯", "Group": "antd" }, { "Name": "Cascader", "CName": "级联选择", "Group": "antd" }, { "Name": "Checkbox", "CName": "多选框", "Group": "antd" }, { "Name": "Collapse", "CName": "折叠面板", "Group": "antd" }, { "Name": "Dropdown", "CName": "下拉菜单", "Group": "antd" }, { "Name": "Icon", "CName": "图标", "Group": "antd" }, { "Name": "InputNumber", "CName": "数字输入框", "Group": "antd" }, { "Name": "Input", "CName": "输入框", "Group": "antd" }, { "Name": "Layout", "CName": "栅格", "Group": "antd" }, { "Name": "Menu", "CName": "导航菜单", "Group": "antd" }, { "Name": "Message", "CName": "全局提示", "Group": "antd" }, { "Name": "Modal", "CName": "对话框", "Group": "antd" }, { "Name": "Notification", "CName": "通知提醒框", "Group": "antd" }, { "Name": "Pagination", "CName": "分页", "Group": "antd" }, { "Name": "Popconfirm", "CName": "气泡确认框", "Group": "antd" }, { "Name": "Popover", "CName": "气泡卡片", "Group": "antd" }, { "Name": "Progress", "CName": "进度条", "Group": "antd" }, { "Name": "QueueAnim", "CName": "进出场动画", "Group": "antd" }, { "Name": "Radio", "CName": "单选框", "Group": "antd" }, { "Name": "Rate", "CName": "评分", "Group": "antd" }, { "Name": "Select", "CName": "选择器", "Group": "antd" }, { "Name": "Slider", "CName": "滑动输入条", "Group": "antd" }, { "Name": "Spin", "CName": "加载中", "Group": "antd" }, { "Name": "Steps", "CName": "步骤条", "Group": "antd" }, { "Name": "Switch", "CName": "开关", "Group": "antd" }, { "Name": "Table", "CName": "表格", "Group": "antd" }, { "Name": "Tabs", "CName": "标签页", "Group": "antd" }, { "Name": "Tag", "CName": "标签", "Group": "antd" }, { "Name": "TimePicker", "CName": "时间选择框", "Group": "antd" }, { "Name": "Timeline", "CName": "时间轴", "Group": "antd" }, { "Name": "Tooltip", "CName": "文字提示", "Group": "antd" }, { "Name": "Transfer", "CName": "穿梭框", "Group": "antd" }, { "Name": "TreeSelect", "CName": "树选择", "Group": "antd" }, { "Name": "Tree", "CName": "树形控件", "Group": "antd" }, { "Name": "Upload", "CName": "上传", "Group": "antd" } ]; componentsToMonitor.forEach(component => { ecodeSDK.overwritePropsFnQueueMapSet(component.Name, { fn: (newProps, Name) => { // 如果 enable 变量为 false,则直接返回 if (!enable) return; // 检查路径 const { hash } = window.location; if (!hash.startsWith(targetPath)) return; // 如果已经存在具有相同 ecId 的组件,则忽略 if (foundComponents.some(item => item.Name === Name && item.ecId === newProps.ecId && item.className === newProps.className)) { // console.log(`忽略相同的组件:${Name}`); return; } // 打印找到的组件名称(Name) console.log(`找到了组件:${Name}!`); // 使用safeStringify console.log(safeStringify(newProps)); // 存储组件信息 const componentInfo = { Group: component.Group, Name: Name, CName: component.CName, Props: newProps, ecId: newProps.ecId, className: newProps.className, PropsJS: safeStringify(newProps) }; // 添加到已找到的组件数组 foundComponents.push(componentInfo); // 打印已找到的组件列表 console.log(foundComponents); // 在页面的右下角增加搜索框,用于搜索匹配componentInfo中的PropsJS,如果存在搜索框,则不继续添加 if (!searchBoxAdded) { console.log("Creating searchBox..."); const searchBox = document.createElement('input'); searchBox.type = 'text'; searchBox.placeholder = '请输入要搜索的组件信息'; searchBox.style.position = 'fixed'; searchBox.style.bottom = '10px'; searchBox.style.right = '10px'; searchBox.style.width = '200px'; searchBox.style.padding = '5px'; searchBox.style.zIndex = '10000'; // 将搜索框添加到页面中 document.body.appendChild(searchBox); // 设置 searchBoxAdded 为 true,表示搜索框已添加 searchBoxAdded = true; // 添加失去焦点事件监听器 searchBox.addEventListener('blur', () => { const searchTerm = searchBox.value.toLowerCase(); let found = false; // 用于标记是否找到了组件 // 遍历已找到的组件数组,根据PropsJS中的信息进行模糊匹配,如果存在匹配,则在页面中显示组件的所有componentInfo信息 foundComponents.forEach((component) => { if (component.PropsJS.toLowerCase().includes(searchTerm)) { console.log(`找到了组件:${component.Name}!`); console.log(component); found = true; // 检查componentInfo中是否存在classname且不为空 if (component.classname && component.classname.trim() !== '') { flashElement(component.classname, true); } else if (component.ecId && component.ecId.trim() !== '') { // 如果没有classname,则使用ecId flashElement(component.ecId, false); } } }); // 如果没有找到组件,则显示相应提示 if (!found) { console.log('没有找到匹配的组件。'); } }); } // console.log(searchBox); }, }); }); ``` ::: :::details[flashElmStyle.css文件代码] ``` @keyframes flashAnimation { 0%, 100% { outline: none; } 50% { outline: 3px solid yellow; } /* 你可以根据需要调整颜色和样式 */ } .flash-highlight { animation: flashAnimation 2s ease-in-out 6; /* 持续时间1秒,闪烁3次 */ font-weight: bold; /* 使文本更加粗体以增加可见性 */ } ``` ::: 😉好了,用吧,记得帮我优化一下~ :::details[如果你想适配移动端,或者这个可以帮到你] 2024年3月20日 17:12:56发布增加移动端js,找我要移动端的还不少,丸子基本不用移动端,但是之前写过了(我只写了一半,移动端这个是半成品)~拿去改改用吧 ``` const enable = true; // 全局开关 const foundComponents = []; // 用于存储找到的组件的数组 let searchBoxAdded = false; //判断搜索框是否存在 let searchBox; // 定义searchBox // Props序列化 function safeStringify(obj) { console.log('objJSON化开始:', obj); const seen = new WeakSet(); try { return JSON.stringify(obj, (key, value) => { if (value instanceof Date) { return value.toISOString(); } if (typeof value === "object" && value !== null) { if (seen.has(value)) { return '[Circular]'; } seen.add(value); } else if (typeof value === "function") { return `[Function: ${value.name || 'anonymous'}]`; } else if (typeof value === "undefined") { return '[Undefined]'; } return value; }); } catch (error) { console.error('Error during object serialization:', error.message); return '{}'; // 或者返回一个错误信息的JSON字符串 } } // 组件闪烁 function flashElement(selector, isClassName) { let elements; if (isClassName) { // 如果提供的是classname,直接使用classname查找元素 elements = document.querySelectorAll(`.${selector}`); } else { // 如果提供的是ecId,使用属性选择器查找元素 elements = document.querySelectorAll(`[ecid^='${selector}']`); } elements.forEach((element) => { element.classList.add('flash-highlight'); // 确保闪烁结束后移除类,避免影响后续样式 setTimeout(() => { element.classList.remove('flash-highlight'); }, 3000); // 根据闪烁次数和持续时间调整,这里是3秒后(1秒闪烁动画,共3次) }); } const componentsToMonitor = [ { "Name": "AtSomeone", "CName": "at组件", "Group": "Page" }, { "Name": "BrowserCIty", "CName": "城市浏览按钮", "Group": "Page" }, { "Name": "BrowserHrmCondition", "CName": "人力资源条件", "Group": "Page" }, { "Name": "BrowserHrm", "CName": "人力资源浏览按钮", "Group": "Page" }, { "Name": "Browser", "CName": "浏览按钮", "Group": "Page" }, { "Name": "BusinessTrip", "CName": "出差明细组件", "Group": "Page" }, { "Name": "CKRichText", "CName": "CK富文本", "Group": "Page" }, { "Name": "CKTextarea", "CName": "CK多行文本", "Group": "Page" }, { "Name": "Comment", "CName": "评论组件", "Group": "Page" }, { "Name": "DocumentTitle", "CName": "文档标题", "Group": "Page" }, { "Name": "FloatingTouch", "CName": "悬浮按钮", "Group": "Page" }, { "Name": "Form", "CName": "简易表单", "Group": "Page" }, { "Name": "ListAdvanced", "CName": "高级列表", "Group": "Page" }, { "Name": "ListEditable", "CName": "可编辑列表", "Group": "Page" }, { "Name": "ListPage", "CName": "普通列表", "Group": "Page" }, { "Name": "ListViewPage", "CName": "长列表", "Group": "Page" }, { "Name": "MapPage", "CName": "地图", "Group": "Page" }, { "Name": "Reply", "CName": "回复组件", "Group": "Page" }, { "Name": "RouteLayout", "CName": "路由层", "Group": "Page" }, { "Name": "SearchAdvanced", "CName": "高级搜索", "Group": "Page" }, { "Name": "SearchPage", "CName": "搜索历史", "Group": "Page" }, { "Name": "SelectLinkage", "CName": "下拉联动", "Group": "Page" }, { "Name": "SelectPage", "CName": "切页选择框", "Group": "Page" }, { "Name": "ShareAuth", "CName": "共享权限", "Group": "Page" }, { "Name": "Share", "CName": "分享", "Group": "Page" }, { "Name": "Signature", "CName": "签批", "Group": "Page" }, { "Name": "StickyPage", "CName": "类吸顶", "Group": "Page" }, { "Name": "TabPage", "CName": "Tab 页", "Group": "Page" }, { "Name": "TreePage", "CName": "树", "Group": "Page" }, { "Name": "Upload", "CName": "上传", "Group": "Page" }, { "Name": "Audio", "CName": "音频组件", "Group": "New" }, { "Name": "Audio", "CName": "音频组件", "Group": "New" }, { "Name": "Avatar", "CName": "头像组件", "Group": "New" }, { "Name": "Bubble", "CName": "气泡", "Group": "New" }, { "Name": "ButtonSelect", "CName": "选择按钮", "Group": "New" }, { "Name": "ColorPicker", "CName": "颜色选择器", "Group": "New" }, { "Name": "CustomNew", "CName": "完全自定义组件2", "Group": "New" }, { "Name": "Custom", "CName": "完全自定义组件", "Group": "New" }, { "Name": "DateSwitch", "CName": "时段切换组件", "Group": "New" }, { "Name": "Echarts", "CName": "图表", "Group": "New" }, { "Name": "FilePicker", "CName": "文件选择器", "Group": "New" }, { "Name": "InputEncrypt", "CName": "加密组件", "Group": "New" }, { "Name": "InputInterval", "CName": "Input区间", "Group": "New" }, { "Name": "Period", "CName": "时间段选择", "Group": "New" }, { "Name": "PhotoSwipe", "CName": "相册浏览", "Group": "New" }, { "Name": "PhotoView", "CName": "图片预览", "Group": "New" }, { "Name": "Qrcode", "CName": "二维码", "Group": "New" }, { "Name": "Rate", "CName": "评分", "Group": "New" }, { "Name": "Schedule", "CName": "日程", "Group": "New" }, { "Name": "Select", "CName": "选择器", "Group": "New" }, { "Name": "Table", "CName": "表格", "Group": "New" }, { "Name": "TimeRangePicker", "CName": "时间区间选择", "Group": "New" }, { "Name": "Timeline", "CName": "时间轴", "Group": "New" }, { "Name": "Tools", "CName": "工具库", "Group": "New" }, { "Name": "Tree", "CName": "树", "Group": "New" }, { "Name": "Video", "CName": "视频", "Group": "New" }, { "Name": "VoiceToText", "CName": "语音转文字", "Group": "New" }, { "Name": "Flex", "CName": "Flex布局", "Group": "Layout" }, { "Name": "WhiteSpace", "CName": "上下留白", "Group": "Layout" }, { "Name": "WingBlank", "CName": "两翼留白", "Group": "Layout" }, { "Name": "Drawer", "CName": "抽屉", "Group": "Navigation" }, { "Name": "Menu", "CName": "菜单", "Group": "Navigation" }, { "Name": "NavBar", "CName": "导航栏", "Group": "Navigation" }, { "Name": "Pagination", "CName": "分页器", "Group": "Navigation" }, { "Name": "Popover", "CName": "气泡", "Group": "Navigation" }, { "Name": "SegmentedControl", "CName": "分段器", "Group": "Navigation" }, { "Name": "TabBar", "CName": "标签栏", "Group": "Navigation" }, { "Name": "Tabs", "CName": "标签页", "Group": "Navigation" }, { "Name": "Button", "CName": "按钮", "Group": "Data Entry" }, { "Name": "Calendar", "CName": "日历", "Group": "Data Entry" }, { "Name": "Checkbox", "CName": "复选框", "Group": "Data Entry" }, { "Name": "DatePickerView", "CName": "选择器", "Group": "Data Entry" }, { "Name": "DatePicker", "CName": "日期选择", "Group": "Data Entry" }, { "Name": "InputItem", "CName": "文本输入", "Group": "Data Entry" }, { "Name": "PickerView", "CName": "选择器", "Group": "Data Entry" }, { "Name": "Picker", "CName": "选择器", "Group": "Data Entry" }, { "Name": "Radio", "CName": "单选框", "Group": "Data Entry" }, { "Name": "Range", "CName": "区域选择", "Group": "Data Entry" }, { "Name": "SearchBar", "CName": "搜索栏", "Group": "Data Entry" }, { "Name": "Slider", "CName": "滑动输入条", "Group": "Data Entry" }, { "Name": "Stepper", "CName": "步进器", "Group": "Data Entry" }, { "Name": "Switch", "CName": "滑动开关", "Group": "Data Entry" }, { "Name": "TextareaItem", "CName": "多行输入", "Group": "Data Entry" }, { "Name": "Accordion", "CName": "手风琴", "Group": "Data Display" }, { "Name": "Badge", "CName": "徽标数", "Group": "Data Display" }, { "Name": "Card", "CName": "卡片", "Group": "Data Display" }, { "Name": "Carousel", "CName": "走马灯", "Group": "Data Display" }, { "Name": "Grid", "CName": "宫格", "Group": "Data Display" }, { "Name": "Icon", "CName": "图标", "Group": "Data Display" }, { "Name": "List", "CName": "列表", "Group": "Data Display" }, { "Name": "NoticeBar", "CName": "通告栏", "Group": "Data Display" }, { "Name": "Steps", "CName": "步骤条", "Group": "Data Display" }, { "Name": "Tag", "CName": "标签", "Group": "Data Display" }, { "Name": "ActionSheet", "CName": "动作面板", "Group": "Feedback" }, { "Name": "Modal", "CName": "对话框", "Group": "Feedback" }, { "Name": "Progress", "CName": "进度条", "Group": "Feedback" }, { "Name": "Toast", "CName": "轻提示", "Group": "Feedback" }, { "Name": "PullToRefresh", "CName": "拉动刷新", "Group": "Gesture" }, { "Name": "SwipeAction", "CName": "滑动操作", "Group": "Gesture" }, { "Name": "ListView", "CName": "长列表", "Group": "Combination" }, { "Name": "Result", "CName": "结果页", "Group": "Combination" } ]; componentsToMonitor.forEach(component => { ecodeSDK.overwriteMobilePropsFnQueueMapSet(component.Name, { fn: (newProps, Name) => { // 如果 enable 变量为 false,则直接返回 if (!enable) return; // 检查路径 const { hash } = window.location; if (!hash.startsWith('#/req?requestid=89929')) return; // // 如果已经存在具有相同 ecId 的组件,则忽略 // if (foundComponents.some(item => item.Name === Name && item.ecId === newProps.ecId && item.className === newProps.className)) { // // console.log(`忽略相同的组件:${Name}`); // return; // } // 打印找到的组件名称(Name) console.log(`找到了组件:${Name}!`); // 使用safeStringify console.log(safeStringify(newProps)); // 存储组件信息 const componentInfo = { Group: component.Group, Name: Name, CName: component.CName, Props: newProps, ecId: newProps.ecId, className: newProps.className, PropsJS: safeStringify(newProps) }; // 添加到已找到的组件数组 foundComponents.push(componentInfo); // 打印已找到的组件列表 console.log(foundComponents); // 在页面的右下角增加搜索框,用于搜索匹配componentInfo中的PropsJS,如果存在搜索框,则不继续添加 if (!searchBoxAdded) { console.log("Creating searchBox..."); const searchBox = document.createElement('input'); searchBox.type = 'text'; searchBox.placeholder = '请输入要搜索的组件信息'; searchBox.style.position = 'fixed'; searchBox.style.bottom = '10px'; searchBox.style.right = '10px'; searchBox.style.width = '200px'; searchBox.style.padding = '5px'; searchBox.style.zIndex = '10000'; // 将搜索框添加到页面中 document.body.appendChild(searchBox); // 设置 searchBoxAdded 为 true,表示搜索框已添加 searchBoxAdded = true; // 添加失去焦点事件监听器 searchBox.addEventListener('blur', () => { const searchTerm = searchBox.value.toLowerCase(); let found = false; // 用于标记是否找到了组件 // 遍历已找到的组件数组,根据PropsJS中的信息进行模糊匹配,如果存在匹配,则在页面中显示组件的所有componentInfo信息 foundComponents.forEach((component) => { if (component.PropsJS.toLowerCase().includes(searchTerm)) { console.log(`找到了组件:${component.Name}!`); console.log(component); found = true; // 检查componentInfo中是否存在classname且不为空 if (component.classname && component.classname.trim() !== '') { flashElement(component.classname, true); } else if (component.ecId && component.ecId.trim() !== '') { // 如果没有classname,则使用ecId flashElement(component.ecId, false); } } }); // 如果没有找到组件,则显示相应提示 if (!found) { console.log('没有找到匹配的组件。'); } }); } // console.log(searchBox); }, }); }); ``` ::: --- # [泛微 Ecology9] 流程节点后附加DML接口动作 作者:丸子·张 来源:https://blog.oneszhang.com/2024/03/20/fwdml/ Markdown:https://blog.oneszhang.com/2024/03/20/fwdml.md 发布日期:2024-03-20T07:55:07.000Z 更新时间:2024-03-20T07:55:07.000Z 专栏:泛微 E9 标签: 摘要:针对泛微 Ecology EC9 流程节点后附加 DML 动作,作者用存储过程实现案件收款录入流程向案件管理流程收款明细的跨流程数据同步:根据传入的案件名称、收款流程 ID 和核验标识,判断明细是否已存在,再分别执行插入两条记录或更新记录,最后通过 DML 语句在流程节点后调用。适合需要流程间数据回写的二次开发人员参考,文章标注尚未完工。 > ⚠️ **暂未完成** > > 本篇博客暂未完工,但是丸子有点懒,不知道什么时候继续写~ > :::tip 由于泛微官方DML动作帮助文档不适合小白(至少不是很适合我....),所以,泛微DML动作丸子版诞生了~ ::: :::details[故事背景] 故事的序章,便由我来一一道来... 丸子在律师事务所工作,为更整合、优化案件处理过程,我们近日创建了一个“案件管理流程”,以点石成金,助推案件的全局流转及其管理; 然而,在这全链条中,我们需特别抽离的环节--“案件收款录入”,这源于收款事件的随机性,其相应的数据需有序而及时地反馈至“案件管理流程”中的收款明细上。鉴于流程转数据的操作无法达成此目的(以A流程去填补另一B流程的明细数据),于是丸子开始着手小小的研究DML动作。既然踏出此步,我们便准备坚持至最后——坚持完實這一切! **结果:** 根据实际情况思考后决定采用DML存储过程实现以下内容: `formtable_main_157_dt2`为案件管理流程收款明细表 `formtable_main_14`为案件收款录入流程表 1. 创建一个存储过程,接受参数 `AJMCX`(案件名称浏览框),`SKLCID`(收款流程ID),`ISCHECK`(是否核验)。 2. 在该存储过程中,首先检查 `formtable_main_157_dt2` 表中是否存在 `SKLCID`。 3. 如果`SKLCID`不存在: - 查询 `formtable_main_14` 表中的 `sqr`,`sklx`,`bcsksj`,`bcskje`,`skqd`,`hkr` 字段,查询条件为 `formtable_main_14.id = SKLCID`。如果查询结果为空,存储过程结束。 - 向 `formtable_main_157_dt2` 表插入两条数据。其中一条数据的 `mainid` 字段的值从 `formtable_main_157` 表中查询得到,查询条件为 `formtable_main_157.id = AJMCX`。另一条数据的 `mainid` 字段的值为 `SKLCID`。 - 插入的两条数据的其它字段值根据以下对应关系从 `formtable_main_14` 表查询得到: - `formtable_main_14.sklx = formtable_main_157_dt2.sklx` - `formtable_main_14.bcsksj = formtable_main_157_dt2.bcsksj` - `formtable_main_14.bcskje = formtable_main_157_dt2.bcskje` - `formtable_main_14.skqd = formtable_main_157_dt2.skqd` - `formtable_main_14.hkr = formtable_main_157_dt2.hkr` - `formtable_main_14.glsklc = SKLCID` - `formtable_main_14.cwhszt = IDCHECK` 4. 如果`SKLCID`存在: - 查询 `formtable_main_14` 表中的 `sqr`,`sklx`,`bcsksj`,`bcskje`,`skqd`,`hkr` 字段,查询条件为 `formtable_main_14.id = SKLCID`。 - 更新 `formtable_main_157_dt2` 表中的记录,更新的字段值根据以下对应关系从 `formtable_main_14` 表查询得到: - `formtable_main_14.sklx = formtable_main_157_dt2.sklx` - `formtable_main_14.bcsksj = formtable_main_157_dt2.bcsksj` - `formtable_main_14.bcskje = formtable_main_157_dt2.bcskje` - `formtable_main_14.skqd = formtable_main_157_dt2.skqd` - `formtable_main_14.hkr = formtable_main_157_dt2.hkr` - `formtable_main_14.cwhszt = IDCHECK` - 更新条件为 `formtable_main_157_dt2.glsklc = SKLCID`。 **存储过程:** ```sql ALTER PROCEDURE SyncReceiptToCaseDetail @AJMCX INT, @SKLCID INT, @ISCHECK INT AS BEGIN -- 如果@AJMCX为空或0,直接返回 IF @AJMCX IS NULL OR @AJMCX = 0 BEGIN PRINT '@AJMCX is NULL or 0' RETURN END DECLARE @mainid INT, @sqr INT, @sklx INT, @bcsksj CHAR(10), @bcskje DECIMAL(38,2), @skqd INT, @hkr VARCHAR(100) -- 检查是否存在SKLCID IF NOT EXISTS (SELECT 1 FROM formtable_main_157_dt2 WHERE glsklc = @SKLCID) BEGIN PRINT '@SKLCID does not exist in formtable_main_157_dt2' -- 查询formtable_main_14表 SELECT @sqr = sqr, @sklx = sklx, @bcsksj = bcsksj, @bcskje = bcskje, @skqd = skqd, @hkr = hkr FROM formtable_main_14 WHERE requestId = @SKLCID PRINT 'Value of @sqr is: ' + ISNULL(CAST(@sqr AS VARCHAR(10)), 'NULL') IF @sqr IS NOT NULL BEGIN PRINT '@sqr is not NULL' -- 向表formtable_main_157_dt2插入第一条数据 流程 SELECT @mainid = id FROM formtable_main_157 WHERE qqidcf = @AJMCX PRINT 'Value of @mainid is: ' + ISNULL(CAST(@mainid AS VARCHAR(10)), 'NULL') INSERT INTO formtable_main_157_dt2 (mainid, skr, sklx, bcsksj, bcskje, cwhszt, skqd, hkr, glsklc) VALUES (@mainid, @sqr, @sklx, @bcsksj, @bcskje, @ISCHECK, @skqd, @hkr, @SKLCID) -- 向表formtable_main_157_dt2插入第二条数据 建模 INSERT INTO formtable_main_157_dt2 (mainid, skr, sklx, bcsksj, bcskje, cwhszt, skqd, hkr, glsklc) VALUES (@AJMCX, @sqr, @sklx, @bcsksj, @bcskje, @ISCHECK, @skqd, @hkr, @SKLCID) END END ELSE BEGIN -- 查询formtable_main_14表 SELECT @sqr = sqr, @sklx = sklx, @bcsksj = bcsksj, @bcskje = bcskje, @skqd = skqd, @hkr = hkr FROM formtable_main_14 WHERE requestId = @SKLCID -- 更新formtable_main_157_dt2 UPDATE formtable_main_157_dt2 SET skr = @sqr, sklx = @sklx, bcsksj = @bcsksj, bcskje = @bcskje, cwhszt = @ISCHECK, skqd = @skqd, hkr = @hkr WHERE glsklc = @SKLCID END END ``` **DML语句:** ``` EXEC SyncReceiptToCaseDetail @AJMCX = {?ajmcx}, @SKLCID = $requestid$, @ISCHECK = 0 ``` ::: --- # [泛微 Ecology9] 建模查询自定义按钮-单条数据修改值 作者:丸子·张 来源:https://blog.oneszhang.com/2024/03/15/pan-wei-ec9-custom-button-single-data-modification/ Markdown:https://blog.oneszhang.com/2024/03/15/pan-wei-ec9-custom-button-single-data-modification.md 发布日期:2024-03-15T04:02:00.000Z 更新时间:2024-03-15T04:02:00.000Z 专栏:泛微 E9 标签: 摘要:在泛微 EC9 中,可为查询列表每条数据添加自定义按钮,以申请客户为例,通过 JavaScript 调用接口实现对单条记录指定字段的更新,避免整批操作。文章还建议提交前校验字段值是否已变更,并在失败时给出重试或联系管理员提示,以提升体验与数据安全性,适合泛微 OA 二次开发人员参考。 > 📌 > > 该文章由丸子引导AI辅助制作生成,如有改进意见,欢迎评论区留言。 > 在泛微 EC9 系统中,经常会遇到需要对查询列表中的单条数据进行修改的需求。本文将详细介绍如何为每条数据添加一个自定义的修改值按钮,实现单条数据的修改,以及如何改进以提高用户体验和数据安全性。 ## 实现功能简介 在查询列表中,每条数据右侧添加一个自定义按钮,允许用户单独修改这条数据的特定字段值,而不是批量修改。 ![Demo演示](https://cos.oneszhang.com/usr/uploads/old/blog_20240315_ec9lestnewbutton/demo.gif) ## 配置步骤 ### 自定义按钮配置 1. **自定义按钮名称**:申请客户 2. **链接目标方式**:手动输入 3. **javascript方法名**:`javascript:applyCustomer();` 4. **javascript方法参数**:`khxm`(客户名称字段名) 5. **javascript方法定义**: ```javascript function applyCustomer(id, khxm) { var account = JSON.parse(localStorage.getItem("theme-account")); var userid = account.userid; console.log("User ID: " + userid); console.log("Row ID: " + id); ModeList.showConfirm(`确认申请“${khxm}”作为你的客户吗?`, function(){ var datas =[{ fieldname: "yyry", changetype: "3", fieldlabel: "邀约人员", fieldvalue: userid, fieldid: "11981" }]; console.log("datas", datas); var requestData = { billids: id, pageexpandid: 2043, //批量修改按钮id(先在模块中建立页面拓展批量修改) datas: JSON.stringify(datas) }; ecCom.WeaTools.callApi('/api/cube/search/saveBatchModifyField', 'POST', requestData) .then(function (data) { console.log(data); ModeList.reloadTable(); ModeList.showMessage("申请成功", 3, 3); }); }); } ``` ![配置内容](https://cos.oneszhang.com/usr/uploads/old/blog_20240315_ec9lestnewbutton/config.png) ## 改进方案 为了提高用户体验和数据处理的准确性,我们提出以下两点改进方案: ### 判断值是否已变更 在提交数据变更请求之前,应该检查目标字段的当前值是否已经是期望修改的值或已被他人修改,以避免不必要的数据操作和重复的网络请求。 ### 处理变更状态失败的情况 在数据变更操作失败时,应给予用户明确的失败提示,并提供重试或联系管理员的选项,以确保用户能够了解到当前操作的状态并采取相应的措施。 ## 结论 通过为查询列表中的每条数据添加自定义修改按钮,我们不仅提升了用户操作的便捷性,同时,通过实施改进方案,也确保了数据操作的安全性和准确性。随着技术的进步,我们相信还有更多的方法可以进一步优化这一功能,提高系统的用户体验。 --- # 一个"EcologyOA工程师导航"网站 作者:丸子·张 来源:https://blog.oneszhang.com/2024/02/26/huahandaohang/ Markdown:https://blog.oneszhang.com/2024/02/26/huahandaohang.md 发布日期:2024-02-26T05:51:00.000Z 更新时间:2024-02-26T05:51:00.000Z 专栏:生活与随笔 标签: 摘要:一位泛微OA工程师为解决技术资料分散、查找困难的问题,创建了导航网站 huahan.cc,汇总官方文档、技术论坛和教程等资源,界面简洁、分类明确,并设有反馈与资源贡献渠道。网站面向泛微OA同行,旨在提升资料查找与学习效率,未来还将持续扩展功能并邀请社区参与。 > 📌 > > 该文章由丸子引导AI辅助制作生成,如有改进意见,欢迎评论区留言。 > ![工程师导航首页][1] ## 引言 作为一个泛微OA工程师,我深知在日常工作中寻找技术资料和解决方案的困难。因此,我决定创建一个专门为泛微OA工程师设计的导航网站——[huahan.cc](https://www.huahan.cc)。本文旨在分享这一旅程的起点、过程以及网站的主要功能。 [点击访问“huahan.cc-泛微OA工程师导航”](https://huahan.cc/) ## 网站的创立初衷 在泛微OA系统的开发和维护过程中,找到高质量、实用的资源一直是一个挑战。市面上虽有许多资源,但分散且不易找到。因此,我希望通过这个导航站,为同行们提供一个汇总这些资源的平台,让寻找资料变得更加简单和高效。 ## 网站内容与特色 huahan.cc致力于为泛微OA工程师提供一个全面的资源导航站,无论是OA论坛、技术文档还是知识文档,这里都应有尽有。网站的设计旨在简化信息探索过程,加速个人和团队的成长。 ### 特色功能 - **资源汇总**:聚合多种泛微OA相关资源,包括官方文档、技术论坛和教程等。 - **易用性设计**:界面简洁,分类明确,便于快速找到所需资源。 - **社区互动**:提供反馈和建议通道,鼓励用户贡献资源,形成互助的社区氛围。 ## 未来展望 我希望huahan.cc能持续成长,不仅限于现有资源的汇总,还将探索更多便利工程师工作和学习的新功能。我也期待更多同行的参与和反馈,一起打造更完善的泛微OA工程师社区。 ## 结语 感谢每一位访问huahan.cc的朋友。我将继续努力,不断优化和扩展网站内容,希望能为泛微OA工程师的工作带来真正的便利。如果你有任何建议或想要申请友链,欢迎通过mail@oneszhang.com与我联系。 :::tip 欢迎分享这篇文章,并在评论区留下你的看法和建议! ::: [1]: https://cos.oneszhang.com/usr/uploads/old/blog_20240226_huahandaohang/huahanindex.png --- # 关于PS中切片工具的简单使用介绍 作者:丸子·张 来源:https://blog.oneszhang.com/2024/01/11/photoshop-slice-tool-introduction/ Markdown:https://blog.oneszhang.com/2024/01/11/photoshop-slice-tool-introduction.md 发布日期:2024-01-11T00:52:03.000Z 更新时间:2024-01-11T00:52:03.000Z 专栏:前端与脚本 标签: 摘要:这篇文章介绍 Photoshop 切片工具的基本用法,说明了切片工具的概念与作用,并给出打开工具、拖动创建切片、调整切片大小和位置,以及通过存储为 Web 所用格式导出切片、选择导出格式与质量的操作步骤。适合网页设计和图像处理的初学者阅读,文章提醒使用时要合理规划切片的大小与位置,以兼顾视觉效果和加载性能。 # 关于PS中切片工具的简单使用介绍 > 📌 > > 该文章由丸子引导AI辅助制作生成,如有改进意见,欢迎评论区留言。 > ## 引言 在图像处理和网页设计领域,Adobe Photoshop(简称PS)是一款非常强大的软件。其中,切片工具是一个经常被使用但不太为人所熟知的功能。本文将简单介绍切片工具的使用方法和实际应用场景。 ## 切片工具简介 ### 什么是切片工具 切片工具是Photoshop中的一个功能,允许用户将一张大图片分割成多个小块,便于网页设计和文件处理。 ### 切片工具的作用 - 方便网页设计师切割图片 - 优化网页加载速度 - 方便进行局部编辑和调整 ## 使用切片工具的步骤 ### 打开切片工具 首先,在Photoshop中打开你想要编辑的图片,然后选择工具栏中的“切片工具”。 ### 创建切片 使用切片工具在图片上拖动以创建切片。你可以根据需要创建多个切片。 ### 调整切片大小和位置 选中切片后,可以通过拖动边缘来调整大小和位置。 ## 导出切片图片 ### 导出操作步骤 完成切片后,通过文件菜单中的“存储为Web所用格式”,来导出切片。 ### 设置导出格式和质量 在导出时,可以选择不同的格式和质量设置,以满足不同的需求。 ## 结论 切片工具在Photoshop中是一个非常实用的功能,特别是在处理大型图像和网页设计时。掌握它可以极大提升工作效率。 :::tip 记得在使用切片工具时,合理规划切片的大小和位置,以保证最佳的视觉效果和加载性能。 ::: --- # [泛微 Ecology9] EcologyOA部署HTTPS过程(Linux'CentOS'使用Nginx部署) 作者:丸子·张 来源:https://blog.oneszhang.com/2023/12/16/linux-nginx-oa/ Markdown:https://blog.oneszhang.com/2023/12/16/linux-nginx-oa.md 发布日期:2023-12-16T02:18:00.000Z 更新时间:2023-12-16T02:18:00.000Z 专栏:泛微 E9 标签: 摘要:这篇文章记录了在 CentOS 服务器上使用 Nginx 为泛微 Ecology E9 部署 HTTPS 的完整流程:先打快照并申请域名证书,再编译安装带 SSL 等模块的 Nginx,然后修改配置文件,为 ecology、emobile、emessage 及移动端等多个服务分别监听不同端口并反向代理到本地 HTTP 端口,最后用 nginx -t 校验并启动或重载。相比直接改造 OA,Nginx 方式无需重启 OA 服务、无需升级 JDK,可实现热部署,适合负责泛微运维的工程师参考。 > ⚠️ > > 注:本文按照泛微HTTPS配置指南操作并优化步骤。 > Nginx部署的好处:部署时不需要重启oa服务,不需要升级jdk,可以做到热部署 > # 1.选一个夜深人静的时候,给服务器打个快照 # 2.申请域名证书 我在腾讯云申请的免费SSL证书(TrustAsia TLS RSA CA 1年),在哪申请无所谓,网上很多相关教程的,自己找哦~ # 3.Linux下的编译安装Nginx 注:如果过程中报错了,或者验证安装时发现不对,那肯定是缺点啥,观察报错,然后问百度或者问GPT等解决(我这一路运行下来没出问题emmmm) SSH进入服务器 安装 Nginx 依赖包 ```bash yum install -y zlib-devel pcre-devel e2fsprogs-devel keyutils-libs-devel libsepol-devel libselinux-devel krb5-devel openssl openssl098e openssl-devel ``` 安装 GCC 编译工具 ```bash yum -y install gcc ``` 下载 Nginx 安装包 ``` wget http://nginx.p2hp.com/download/nginx-1.24.0.tar.gz -P /opt/nginx.bak ``` 解压 Nginx 安装包 ``` tar -zxf /opt/nginx.bak/nginx-1.24.0.tar.gz -C /opt/nginx.bak ``` 编译 Nginx ``` cd /opt/nginx.bak/nginx-1.24.0 ./configure --user=root --group=root --prefix=/opt/nginx --with-http_stub_status_module --with-http_ssl_module --with-http_sub_module --with-stream --with-stream_ssl_module make make install ``` 2024年9月10日 17:56:09增加了http2和gzip`cd /opt/nginx.bak/nginx-1.24.0 && ./configure --user=root --group=root --prefix=/opt/nginx --with-http_stub_status_module --with-http_ssl_module --with-http_sub_module --with-stream --with-stream_ssl_module --with-http_v2_module --with-http_gzip_static_module --with-http_slice_module && make && make install` 编译完成后会在/opt目录中生成一个nginx文件夹。 验证安装 ``` /opt/nginx/sbin/nginx -v ``` 如果显示`nginx version: nginx/1.24.0`,那OK,应该是没问题了,下一步,GOGOGO! # 4.Nginx.conf文件配置 `/opt/nginx/conf/nginx.conf` 这一步顺便把你申请的证书`oa.crt`,`oa.key`放进`/opt/nginx/conf` 按照自己服务器情况修改,在网上学习下nginx配置文件说明哦~ ``` worker_processes 8; error_log logs/error.log; worker_rlimit_nofile 65535; events { worker_connections 10024; } http { upstream ecologyclusterhttps{ server 127.0.0.1:80; } upstream emobileclusterhttps{ server 127.0.0.1:8999; } upstream emessageclusterhttps{ server 127.0.0.1:7070; } include mime.types; default_type application/octet-stream; sendfile on; keepalive_timeout 65; client_max_body_size 1000M; client_body_buffer_size 128K; server_tokens off; fastcgi_connect_timeout 300s; fastcgi_send_timeout 300s; fastcgi_read_timeout 300s; fastcgi_buffer_size 128k; fastcgi_buffers 8 128k; fastcgi_busy_buffers_size 256k; fastcgi_temp_file_write_size 256k; fastcgi_intercept_errors on; client_header_buffer_size 128k; large_client_header_buffers 4 128k; proxy_buffer_size 64k; proxy_buffers 8 64k; server { listen 443 ssl; #ecology的https端口号 server_name localhost; #ssl on; ssl_certificate oa.crt; #ecology的证书,crt/pem结尾的格式 ssl_certificate_key oa.key; #ecology的证书,key结尾的格式 ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_ciphers ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DES-CBC3-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4; ssl_prefer_server_ciphers on; location / { root html; index index.html index.htm index.jsp; proxy_pass http://ecologyclusterhttps; proxy_read_timeout 3600; proxy_send_timeout 3600; proxy_buffer_size 128k; proxy_buffers 32 32k; proxy_busy_buffers_size 128k; proxy_redirect http:// $scheme://; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Real-IP $remote_addr; proxy_set_header Host $http_host; } } server { listen 9444 ssl; #emobile的https端口号 server_name localhost; #ssl on; ssl_certificate oa.crt; #emobile的证书,crt/pem结尾的格式 ssl_certificate_key oa.key; #emobile的证书,key结尾的格式 ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_ciphers ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DES-CBC3-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4; ssl_prefer_server_ciphers on; location / { root html; index index.html index.htm index.jsp; proxy_pass http://emobileclusterhttps; proxy_read_timeout 3600; proxy_send_timeout 3600; proxy_buffer_size 128k; proxy_buffers 32 32k; proxy_busy_buffers_size 128k; proxy_redirect http:// $scheme://; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Real-IP $remote_addr; proxy_set_header Host $http_host; } } ##9090为信息服务端口 ##PC客户端的emessage server { listen 7444 ssl; #pc端的emessgae的https端口号 server_name localhost; #ssl on; ssl_certificate oa.crt; #pc端的emessgae的证书,crt/pem结尾的格式 ssl_certificate_key oa.key; #pc端的emessgae的证书,key结尾的格式 ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_ciphers ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DES-CBC3-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4; ssl_prefer_server_ciphers on; location / { root html; index index.html index.htm index.jsp; proxy_pass http://emessageclusterhttps; proxy_read_timeout 3600; proxy_send_timeout 3600; proxy_buffer_size 128k; #7070必备 proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; # proxy_buffers 32 32k; proxy_busy_buffers_size 128k; proxy_redirect http:// $scheme://; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Real-IP $remote_addr; proxy_set_header Host $http_host; } } } ##移动客户端的emessage stream { upstream emessagecluster3{ hash $remote_addr consistent; server 127.0.0.1:5222; #移动端的emessage的内网ip+端口 } server { listen 5444 ssl; #移动端的emessgae的https端口号 #ssl on; ssl_certificate oa.crt; #移动端的emessgae的证书,crt/pem结尾的格式 ssl_certificate_key oa.key; #移动端的emessgae的证书,key结尾的格式 ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_ciphers ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DES-CBC3-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4; ssl_prefer_server_ciphers on; proxy_pass emessagecluster3; proxy_connect_timeout 2s; proxy_timeout 360s; } } ``` 检查是否成功,进入`/opt/nginx/sbin`,运行代码 ``` ./nginx -t ``` 提示`nginx: the configuration file /opt/nginx/conf/nginx.conf syntax is ok nginx: configuration file /opt/nginx/conf/nginx.conf test is successful`,OK,继续! 启动nginx: `/opt/nginx/sbin`目录下执行 `./nginx` 启动nginx 你可以使用命令停止或重启运行 `./nginx -s stop` 停止nginx `./nginx -s reload` 重启nginx # 5.配置nginx服务自启动 **编辑 /etc/rc.local** - 使用 `vi` 或其他文本编辑器打开 `/etc/rc.local`。 - 添加 `/opt/nginx/sbin/nginx` (或者 Nginx 的实际安装路径)到文件中。 ``` vi /etc/rc.local ``` 然后在文件中添加: ``` /opt/nginx/sbin/nginx ``` **赋予执行权限** - 确保 `/etc/rc.local` 文件具有执行权限。 ```bash chmod +x /etc/rc.local ``` # 6.强制http跳转https 在nginx.conf中增加一个新的server块 ``` # 新增 server 块用于 HTTP 到 HTTPS 的重定向 server { listen 80; server_name localhost; # 重定向所有 HTTP 请求到 HTTPS return 301 https://$host$request_uri; } ``` 找到泛微weaver目录下的resin文件夹中的conf文件夹中的resin.properties文件,修改端口80为81(避免冲突),重启ecology,修改nginx配置中的ip:80为ip:81,重启nginx,最后,记得去移动管理平台里改下链接端口为新的https端口,就OK了! --- # [泛微 Ecology9] 实现树形组件点击标题展开/收起功能 作者:丸子·张 来源:https://blog.oneszhang.com/2023/11/16/fw-ecode-treeok/ Markdown:https://blog.oneszhang.com/2023/11/16/fw-ecode-treeok.md 发布日期:2023-11-16T13:26:00.000Z 更新时间:2023-11-16T13:26:00.000Z 专栏:泛微 E9 标签: 摘要:介绍如何在泛微EC9中借助ecodeSDK的overwritePropsFnQueueMapSet方法重写Tree组件属性,实现点击节点标题即可展开或收起子节点的交互优化。文章给出两版代码,第二版增加了展开前异步加载数据的处理,并说明了路径检查、保留原回调等核心逻辑。需注意代码并非通用,onSelect中selectedKeys存在异步取空值问题,仅供学习参考。 > ⚠️ **兼容性提示!** > > 请注意,文中的代码并不是通用于所有的树组件,需要根据情况进行修改。 > > **代码已知问题:** > 1.**复杂**:毫无疑问,有些功能你可能不需要,请自行简化 > 2.**异步**:`onSelect`中获取到的`selectedKeys`似乎存在异步情况,会拿到空值,导致树无法展开!你可以自行尝试在 `onSelect` 中加入一些延迟,以确保在数据加载完毕后再处理逻辑。 > > 该功能仅为丸子练习时创造,这意味着此代码并不是问题最优解,仅供大家一起学习参考,如有建议/想法欢迎提出~ > :::tip[AI生成说明] 丸子懒,所以你所看到的以下文章为AI生成~ ::: # 优化泛微EC9树形组件交互体验:使用泛微ecode实现点击标题展开/收起功能 泛微EC9作为一款流行的企业级应用系统,其用户体验的优化一直是开发者关注的焦点。本文将介绍如何使用`ecodeSDK`来优化泛微EC9中树形组件的交互体验,具体实现点击文字标题以展开和收起下级树的功能。 ## 背景 在泛微EC9系统中,树形组件广泛应用于各种层级数据的显示。传统的交互方式需要用户点击旁边的小图标才能展开或收起节点,这在某些情况下可能不够直观或高效。 ## 解决方案 使用ecodeSDK的`overwritePropsFnQueueMapSet`方法,我们可以重写`Tree`组件的属性,从而实现更加人性化的交互方式。以下是实现这一功能的核心代码: :::details[这里是第一版代码] ```javascript ecodeSDK.overwritePropsFnQueueMapSet('Tree', { fn: (newProps) => { // 检查路径,这里路径自行更改,如果没有这个路径即为全系统生效,但不建议那么做 if (!ecodeSDK.checkLPath('/wui/engine.html#/systemExpAndImp')) return; console.log("路径识别正确!"); // 设置 autoExpandParent 为 true newProps.defaultExpandAll = false; newProps.autoExpandParent = true; // 保存原有的 onSelect 和 onExpand 回调函数 const originalOnSelect = newProps.onSelect; const originalOnExpand = newProps.onExpand; // 重写 onSelect 回调函数 newProps.onSelect = (selectedKeys, e) => { console.log('Tree onSelect:', selectedKeys, 'e is:', e); // 判断当前节点是否为叶子节点 const isLeaf = e.node.props.isLeaf; // 判断当前节点是否已展开 const isExpanded = newProps.expandedKeys.includes(selectedKeys[0]); console.log('isLeaf:', isLeaf, 'isExpanded:', isExpanded); // 如果不是叶子节点,则处理展开或收起逻辑 if (!isLeaf) { let newExpandedKeys = [...newProps.expandedKeys]; if (isExpanded) { // 如果节点已展开,则收起节点 newExpandedKeys = newExpandedKeys.filter(key => key !== selectedKeys[0]); } else { // 如果节点未展开,则展开节点 newExpandedKeys.push(selectedKeys[0]); } // 更新 expandedKeys newProps.expandedKeys = newExpandedKeys; // 手动调用原有的 onExpand 函数(如果有) if (typeof originalOnExpand === 'function') { originalOnExpand(newExpandedKeys, { node: e.node, expanded: !isExpanded }); } } // 调用原有的 onSelect 函数(如果有) if (typeof originalOnSelect === 'function') { originalOnSelect(selectedKeys, e); } }; return newProps; }, }); ``` ::: `第二版代码(更新于2023年11月21日)` ``` ecodeSDK.overwritePropsFnQueueMapSet('Tree', { fn: (newProps) => { // 路径检查,如果删除路经检查,则为全部组件均实现此功能 if (!ecodeSDK.checkLPath('/wui/engine.html#/docengine/sec/doccategory')) return; console.log("路径识别正确!"); // 设置 autoExpandParent 为 true // newProps.defaultExpandAll = false;//默认展开全部 newProps.autoExpandParent = true; // 保存原有的 onSelect 和 onExpand 和 loadData回调函数 const originalOnSelect = newProps.onSelect; const originalOnExpand = newProps.onExpand; const originalLoadData = newProps.loadData; // 重写 onSelect 回调函数 newProps.onSelect = async (selectedKeys, e) => { console.log('Tree onSelect:', selectedKeys, 'e is:', e); // 判断当前节点是否为叶子节点 const isLeaf = e.node.props.isLeaf; // 判断当前节点是否已展开 const isExpanded = newProps.expandedKeys.includes(selectedKeys[0]); console.log('isLeaf:', isLeaf, 'isExpanded:', isExpanded); // 如果不是叶子节点,则处理展开或收起逻辑 if (!isLeaf) { let newExpandedKeys = [...newProps.expandedKeys]; if (isExpanded) { newExpandedKeys = newExpandedKeys.filter(key => key !== selectedKeys[0]); } else { // 如果节点未展开,则在展开前调用原来的 loadData await originalLoadData(e.node); newExpandedKeys.push(selectedKeys[0]); } // 更新 expandedKeys newProps.expandedKeys = newExpandedKeys; // 手动调用原有的 onExpand 函数(如果有) if (typeof originalOnExpand === 'function') { originalOnExpand(newExpandedKeys, { node: e.node, expanded: !isExpanded }); } } // 调用原有的 onSelect 函数(如果有) if (typeof originalOnSelect === 'function') { originalOnSelect(selectedKeys, e); } }; return newProps; }, }); ``` 第二版代码在第一版的基础上添加了一些新的功能,特别是在处理非叶子节点时加载数据的功能。这可以提高性能并允许在需要时动态加载数据 ## 核心逻辑 1. **路径检查**:首先确保当前路径符合预期,这是为了只在特定页面上应用这些改动。 2. **自动展开父节点**:通过设置`autoExpandParent`为`true`,确保父节点在子节点被选中时自动展开。 3. **保存原回调函数**:保存原本的`onSelect`和`onExpand`回调函数,以便在新逻辑后还能保持原有功能。 4. **重写`onSelect`回调函数**:在这个函数中,我们根据节点的类型和当前状态来决定是展开还是收起节点,并手动调用原有的`onExpand`函数来更新视图。 ## 优势 通过这种方式,我们不仅提高了用户交互的便捷性,还保留了原有的功能和扩展性。用户现在可以直接点击节点标题来展开或收起子节点,从而提供更流畅的体验。 ## 结论 这个方法展示了如何通过ecodeSDK来优化泛微EC9中的树形组件。通过简单的代码改动,我们能显著提升用户的交互体验,这对于企业级应用系统来说尤为重要。 --- # Excel文件太大打开太慢?试试这个脚本! 作者:丸子·张 来源:https://blog.oneszhang.com/2023/10/31/excel-compress/ Markdown:https://blog.oneszhang.com/2023/10/31/excel-compress.md 发布日期:2023-10-31T11:49:00.000Z 更新时间:2023-10-31T11:49:00.000Z 专栏:前端与脚本 标签: 摘要:这篇分享了一个用 Python 批量压缩 Excel 文件的脚本,通过 openpyxl 遍历当前目录下所有 .xlsx 文件,删除其中的空行和空列,并以「_压缩后」为后缀另存,从而减小体积、加快打开速度。文中给出了安装依赖、放置文件、命令行运行等步骤,并提醒该方法无法解决图片导致的文件过大问题,运行前需备份原始文件。 > ⚠️ **温馨提示** > > 该脚本删除多余空行以达到压缩目的,对于Excel中插入图片导致文件太大无法解决哦~ > 效果怎么样?用过的都说好! > ·#(抠鼻)**直接上代码**`Python代码`: ```python import openpyxl import os # 获取当前工作目录 current_directory = os.getcwd() # 列出当前目录下的所有文件 files_in_directory = os.listdir(current_directory) # 遍历文件,处理Excel文件 for file_name in files_in_directory: if file_name.endswith('.xlsx'): # 拼接文件路径 file_path = os.path.join(current_directory, file_name) # 打开Excel文件 workbook = openpyxl.load_workbook(file_path) sheet = workbook.active # 删除空行 rows_to_delete = [] for row in sheet.iter_rows(min_row=1, max_row=sheet.max_row): if all(cell.value is None for cell in row): rows_to_delete.append(row) for row in rows_to_delete: sheet.delete_rows(row[0].row, 1) # 删除空列 cols_to_delete = [] for col in sheet.iter_cols(min_col=1, max_col=sheet.max_column): if all(cell.value is None for cell in col): cols_to_delete.append(col) for col in cols_to_delete: sheet.delete_cols(col[0].column, 1) # 生成修改后的文件名 file_name_without_extension, file_extension = os.path.splitext(file_name) modified_file_name = f"{file_name_without_extension}_压缩后{file_extension}" # 保存修改后的Excel文件 modified_file_path = os.path.join(current_directory, modified_file_name) workbook.save(modified_file_path) # 关闭Excel文件 workbook.close() print(f"已保存修改后的文件为: {modified_file_path}") ``` **如何使用?** `来自ChatGPT的回答:` **以上代码是一个Python脚本,用于遍历当前工作目录下的所有Excel文件(.xlsx扩展名),删除其中的空行和空列,然后将修改后的文件保存在同一目录下,并在文件名末尾添加"_压缩后"。** 下面是使用以上代码的详细步骤: > ·#(想一想)(其实,你应该先确定自己安装了Python) 1. **安装所需的库:** 首先,确保你已经安装了`openpyxl`库。如果没有安装,可以使用以下命令来安装它: ``` pip install openpyxl ``` 2. **准备Excel文件:** 将要处理的Excel文件(.xlsx格式)放在与脚本相同的目录中。你可以将这些文件复制到脚本文件所在的文件夹中,或者将脚本放在包含Excel文件的文件夹中。 3. **运行脚本:** 打开命令行终端,导航到包含脚本和Excel文件的目录。然后运行以下命令来执行脚本: ``` python your_script.py ``` 其中,`your_script.py`应该替换为包含以上代码的Python脚本文件的名称。 4. **执行结果:** 脚本将遍历当前目录下的所有Excel文件,删除空行和空列,并将修改后的文件保存在同一目录中,文件名末尾会添加"_压缩后"。脚本会在命令行中打印出每个处理过的文件的路径。 5. **完成:** 执行脚本后,你将在同一目录中找到已经处理好的Excel文件,它们的文件名末尾会添加"_压缩后"。这些文件中的空行和空列已经被删除。 **请注意,运行脚本前,请确保你已经备份了原始Excel文件,以防意外修改。** --- # 丸子的C语言学习-Day1 作者:丸子·张 来源:https://blog.oneszhang.com/2023/09/09/54/ Markdown:https://blog.oneszhang.com/2023/09/09/54.md 发布日期:2023-09-09T01:24:00.000Z 更新时间:2023-09-09T01:24:00.000Z 专栏:前端与脚本 标签: 摘要:这篇学习笔记面向 C 语言初学者,记录了使用 Visual Studio 2019 搭建开发环境、创建控制台项目并运行 Hello World 的过程,并介绍主函数、变量、标识符、关键词、字面常量和 printf 等基础语法,强调每个程序只能有一个 main 函数、变量须先声明后使用、格式符要与参数匹配。 ## Visual Studio集成开发环境的搭建 **建议使用:Visual Studio 2019** Visual Studio的官方网站: [https://visualstudio.microsoft.com/](https://visualstudio.microsoft.com/) :::tip[注意] Visual Studio是一个集成开发环境,Visual Studio Code是一个文本编辑器。 ::: :::details[**安装Visual Studio 2019**] 1. 下载社区版本 2. 选择工作负载为C++ 3. 自定义安装位置 4. 去喝杯水 5. 激活Visual Studio(登陆你的微软账号~) ::: ## 编写第一个C语言程序 `Hello World` #### 创建项目 1. 打开Visual Studio,创建新项目 2. 继续但无需代码->文件->新建->项目 3. 项目模版:控制台应用 4. 名称->位置->创建 #### 创建源文件 1. 解决方案管理器 2. 点击开源文件 3. 删除HelloWorld.cpp 4. 在源文件右键->添加->新建项 5. 选择C++文件.cpp 6. 修改源文件名称 7. 点击添加 > C与C++拥有共同的子集,C++独有部分强大,而C的独有部分不常用,使用C++来编译C语言是木茅台的! #### 编写程序 ```c #include int main() { printf("Hello World\n"); return 0; } ``` 生成->生成解决方案 #### 运行程序 调试->开始执行(不调试) `Hello World!` ## 基本的C语言语法知识 #### 主函数 ```c int main() // 这是主函数 { return 0; // 主函数返回值 } ``` > ⚠️ **重要** > > 每个C语言代码,只能`有且仅有一个main函数` > C语言标准中规定`主函数有返回值且必须是int`。如果程序正常结束,一般将返回值设置为0。 > #### 函数 `函数`是执行特定任务的代码块,它可以接受参数并返回一个值。 ```c 返回类型 函数名(参数列表) { // 函数体 // 可以包含多个语句 return 返回值; } ``` `函数体`一定要被花括号包括且不可省略 ```c //例子 int add(int a, int b) { return a + b; } ``` #### 变量 `变量`需要先声明然后再定义。声明变量时,你需要指定变量的数据类型,然后在定义时分配内存来存储变量的值 ```c //声明变量 数据类型 变量名; //定义和初始化变量 数据类型 变量名 = 初始值; ``` :::tip[提示] `变量`必须先声明后使用 ::: `=`:赋值运算,将符号右边的值,装进左边的变量的一种运算符。 ```c //例子 int ones; // 声明一个整数变量 ones = 1; // 定义并初始化变量 int age = 25; // 声明并定义并初始化一个整数变量 ``` #### 标识符 `标识符`是用来标识变量、函数和其他命名实体的名称。 :::tip[注意] 标识符可以`包含字母、数字、下划线或$符号`。但是,必须`以字母、下划线(_)或$符号开头`。并且`标识符区分大小写`。 ::: `务必避免使用C语言的关键词`。 ```c //例子 int myVariable; float averageScore; ``` #### 关键词 C语言中有一些`关键词`,它们具有特殊的含义,不能用作标识符名称。 `int`是C语言中的一个关键词 。 :::details[关键词列表(不完全)] **控制结构关键词:** | 关键词 | 说明 | |------------|----------------------------------| | break | 用于中断循环或开关语句 | | continue | 用于跳过当前循环的剩余迭代 | | default | 在开关语句中指定默认分支 | | do | 用于创建循环 | | else | 用于条件语句的替代分支 | | for | 用于创建循环 | | goto | 用于无条件跳转到代码的标签 | | if | 用于条件语句 | | return | 用于从函数中返回值 | | switch | 用于创建开关语句 | | while | 用于创建循环 | **数据类型关键词:** | 关键词 | 说明 | |------------|----------------------------------| | char | 用于声明字符类型 | | double | 用于声明双精度浮点数类型 | | enum | 用于声明枚举类型 | | float | 用于声明单精度浮点数类型 | | int | 用于声明整数类型 | | long | 用于声明长整数类型 | | short | 用于声明短整数类型 | | signed | 用于声明有符号类型 | | unsigned | 用于声明无符号类型 | | void | 用于声明无返回值或指针类型 | **存储类关键词:** | 关键词 | 说明 | |------------|----------------------------------| | auto | 用于自动存储类 | | extern | 用于外部链接存储类 | | register | 用于寄存器存储类 | | static | 用于静态存储类 | | typedef | 用于定义数据类型别名 | **其他关键词:** | 关键词 | 说明 | |------------|----------------------------------| | asm | 用于嵌入汇编语言代码 | | inline | 用于函数内联展开建议 | | restrict | 用于指示指针不会被别名化 | | _Bool | 布尔数据类型(C99引入) | | _Complex | 复数数据类型(C99引入) | | _Imaginary | 虚数数据类型(C99引入) | | _Alignas | 指定对齐方式的关键词(C11引入) | | _Alignof(或__alignof__) | 获取数据类型的对齐要求 | 请注意,这些关键词的使用可能受到编译器和C语言标准的影响,因此在编写代码时要谨慎选择和使用关键词。 ::: 不同的C标准可能会引入新的关键词或删除一些旧的关键词,因此在编写C代码时,要考虑所使用的C标准。 #### 字面常量 `字面常量`是固定的常量值,不会改变。在C语言中,有不同类型的字面常量,包括整数、浮点数和字符常量。 ```c //整数字面常量 42 -10 //浮点数字面常量 3.14 -0.01 //字符常量 'a' '9' ``` 变量我们可以通过赋值来更改,`常量不能更改,所以你不能对它进行赋值`。 #### #include命令 `#include`是C和C++编程语言中的一个预处理指令,用于将外部文件的内容包含(或插入)到源代码文件中。 `#include ` ```c #include //或#include "header_file_name" //使用尖括号 < > 表示包含标准库头文件,编译器会在标准库路径中查找。 //使用双引号 " " 表示包含用户自定义头文件,编译器会在当前目录或指定的目录中查找。 ``` stdio.h里面并未定义printf函数,但是它里面有printf函数的函数声明。但是,在当前阶段里面,你可以 理解为stdio.h里面,写了printf的函数定义。 #### printf函数 `printf` 是C和C++编程语言中的一个标准库函数,用于将格式化的输出写入到标准输出(通常是终端窗口),它是一个 `变参函数` > printf 由单词 print(打印) 和单词 format(格式) 的首字母f组成,意为格式化打印。 ```c printf("A=%d\nB=%d\n", 123, 456); ``` :::tip[注意] printf的`第一个参数必须是 字符串`! ::: 字符串参数后面可以加任意多的替换参数。 要确保格式字符串中的 `%`符号和后续的参数数量和类型匹配 ```c //使用方法 printf("%d\n%d\n", 整型int1, 整型int2); printf("XXX占位1 XXX 占位2 XXX占位3", 替换1, 替换2, 替换3); ``` --- # Typecho Mirages主题添加一些小功能 作者:丸子·张 来源:https://blog.oneszhang.com/2023/08/27/53/ Markdown:https://blog.oneszhang.com/2023/08/27/53.md 发布日期:2023-08-27T07:54:00.000Z 更新时间:2023-08-27T07:54:00.000Z 专栏:前端与脚本 标签: 摘要:作者为方便复制自己博客代码,参考 LOGI 的博客,给 Typecho Mirages 主题添加了代码块复制按钮、链接预加载和评论框七彩打字动画三个小功能。相关脚本需分别加入主题头部或 body 结束前,若开启 PJAX,复制按钮还需单独添加回调。适合使用该主题的博客读者参考。 **事件起源**:我自己复制自己博客上的代码不方便!(没错!因为懒,所以想要加这个功能!---`奶酪越多,奶酪越少`) 于是我,就开始在网络上各种搜罗代码,搜着搜着,就顺腾摸着一个大佬:[LOGI](https://logi.im/)(就丸子这检索能力,强的很好吧~),恰好大佬也是使用的[Mirages](https://get233.com/archives/mirages-intro.html)主题,于是我就开始了,疯狂阅读和拿出祖传的Ctrl+C、Ctrl+V大法~ > 以下代码内容均来自于[LOGI](https://logi.im/)大佬博客~ --- # 代码块复制按钮 > 来源:[为 Typecho 的 Code Block 添加 Copy 按钮 - LOGI](https://logi.im/blog/adding-copy-btn-to-typecho-code-block.html) `Mirages主题`将以下代码添加到主题 `header.php` 中的 `` 标签前,或前往 `控制台` - `设置外观` - `主题自定义扩展`,将它添加到 `自定义 HTML 元素拓展 - 标签: head 头部 (meta 元素后)`。 :::details[JavaScript Code] ``` ``` ::: 如果你开启了 `PJAX`,则需单独加入回调函数。对于本主题,依次进入 `控制台 - 外观 - 设置外观 - PJAX(BETA) - PJAX RELOAD`,将 `initCodeCopyButton();` 添加进入即可。 --- # 开启链接预加载 > 来源:[任意站点开启链接预加载 - LOGI](https://logi.im/blog/turning-on-instance-page-for-any-website.html) `Mirages主题`依次进入 `控制台 - 外观 - 设置外观 - 主题自定义扩展`,将以下代码加入到 `自定义 HTML 元素拓展 - 在 body 标签结束前`。其他主题,加入到主题对应的 `footer.php` 中 `` 标签结束前。 ``` ``` --- # 为 Typecho 评论框加入七彩打字动画 > 来源:[为 Typecho 评论框加入七彩打字动画 - LOGI](https://logi.im/blog/add-typing-animation-for-comment.html) `Mirages主题`依次进入 `控制台 - 外观 - 设置外观 - 主题自定义扩展`,将以下代码加入到 `自定义 HTML 元素拓展 - 在 body 标签结束前`。其他主题,加入到主题对应的 `footer.php` 中 `` 标签结束前。 :::details[JavaScript Code] ``` ``` ::: --- # [泛微 Ecology9] SQL缓存页面报错500解决办法 作者:丸子·张 来源:https://blog.oneszhang.com/2023/08/23/52/ Markdown:https://blog.oneszhang.com/2023/08/23/52.md 发布日期:2023-08-23T08:33:00.000Z 更新时间:2023-08-23T08:33:00.000Z 专栏:泛微 E9 标签: 摘要:泛微 Ecology EC9 中 SQL 缓存监控页面出现 500 报错时,可参考标准最新版出包,或将附件文件解压后覆盖到对应 commcache 目录,无需重启即可生效。操作前应做好备份,适合运维或二次开发人员快速修复该页面异常。 ## 问题: **访问 oa/commcache/cacheMonitor.jsp 时,报错 500 显示:** ![SQL缓存500界面][1] ## 解决办法: 参考标准最新版出包 或者 **将附件的文件解压后替换到 /ecology/commcache/ 目录下** ,不用重启即可生效。(压缩包见文章末尾,覆盖前请注意做好备份~) [file href="https://cos.oneszhang.com/usr/uploads/old/blog_20230823_sqlcache/commoncache.zip"]点击下载commoncache.zip[/file] [1]: https://cos.oneszhang.com/usr/uploads/old/blog_20230823_sqlcache/sqlcache500.png --- # [泛微 Ecology9] 组件重写-门户首页顶部导航栏欢迎词 作者:丸子·张 来源:https://blog.oneszhang.com/2023/08/15/50/ Markdown:https://blog.oneszhang.com/2023/08/15/50.md 发布日期:2023-08-15T06:34:00.000Z 更新时间:2023-08-15T06:34:00.000Z 专栏:泛微 E9 标签: 摘要:介绍泛微 Ecology 门户首页顶部导航栏欢迎词的 ecode 组件重写实现,借助 ecodeSDK 复写 Ant Design Popover,按不同时间段随机显示带用户名的问候语,并支持国际化、走马灯和实时时间。文中说明复写时需带避免循环的参数,适合想无侵入优化门户交互的开发者参考。 > 本项目改编自“云商店应用库”中的“门户首页顶部导航栏添加当前日期”项目,原作者:王斌(上海) > 原项目简介:门户首页顶部导航栏添加当前日期*备注:此思路可以适用于在门户首页右侧导航栏自定义添加内容,比如:当前日期、天气、股票形势、特别事项等 本项目为丸子对ecode组件重写`ecodeSDK.overwriteClassFnQueueMapSet(name,option)`的学习与尝试~ `丸子近期较忙,暂不梳理该小项目过程,且暂不提供帮助,文中代码回复可见,有问题请留言或一起在下面讨论区交流。` > ✅ **国际化欢迎词适配更新** > > 2024年5月8日 12:04:14 更新添加国际化实现示例~现在,你可以对欢迎词进行国际化配置了~ > > ✅ **实现滚动走马灯播放公告?这样!** > > 2024年7月3日 17:57:57 可以自行修改代码实现走马灯效果,兼容性未测试~ > > ``` >
{generatedGreeting}
> ``` > > 修改为: > > ``` > {generatedGreeting} > //更花里胡哨的操作,请参考HTML marquee API~ > ``` > > ✅ **显示实时时间 yyyy年mm月dd日 hh:mm:ss 星期x** > > 2024年7月4日 08:56:34 增加新能力,显示实时时间 > # 一、项目介绍 :::tip 以下介绍内容为Claude 2 AI生成~ ::: ### 1.介绍 这个组件通过复写 Ant Design 的 Popover 组件,实现在门户界面内根据时间显示随机问候语的功能,个性化交互场景增强用户体验。 ### 2.特性 * 根据不同时间段,显示不同问候语 * 支持显示用户名,更个性化 * 不影响 Popover 原有功能 * 提供前置 CSS 样式,方便集成 * 代码开箱即用,可直接用于项目中 ### 3.技术优势 * 通过组件复写方式扩展,无侵入 * 模块化代码,具有可扩展性 * 注释清晰,便于二次开发 ## 4.总结 该组件让 OA 系统原有的门户首页顶部导航栏支持显示更友好的问候语,可以增加内部员工的亲和感和归属感。组件代码开箱即用,无侵入式集成,为 OA 系统提供了一个轻量级的用户体验优化方案。可以减少员工使用系统时的冷漠感,增强团队融洽度。 # 二、效果图 ![下午好~][1] ![夜深了~][2] # 三、代码 > 代码中保留并沿用了原项目的所有变量名及元素等,强迫症请自行更改~ `init.js(前置加载)` [hide] ```js let enable = true; const NewPopover = (props)=>{ //此函数不允许写在复写方法内,会导致实例重复创建,也就是dimout不断执行 const acParams = { appId:'${appId}', //appId会自动识别 name:'NewPopover', //模块名称 isPage:false, //是否是路由页面 noCss:true, //是否禁止单独加载css,通常为了减少css数量,css默认前置加载 props } const NewCom = props.Com; return window.comsMobx?ecodeSDK.getAsyncCom(acParams):(); } ecodeSDK.overwriteClassFnQueueMapSet('Popover',{ fn:(Com,newProps)=>{ if(!enable) return ; //总开关 if(newProps.overlayClassName !== "e9header-account-overlay") return; if(newProps._noOverwrite) return ; newProps.Com = Com; //如果需要原组件,可带上 return { com:NewPopover, props:newProps }; } }); ``` [/hide] `index.js` [hide] ```js const { Popover } = antd; class NewPopover extends React.Component { constructor(props) { //初始化,固定语法 super(props); this.state = {} } getGreeting() { const { username } = JSON.parse(localStorage.getItem("theme-account")); const currentHour = new Date().getHours(); let greetingCategory = ""; if (currentHour >= 0 && currentHour < 5) { greetingCategory = "night"; } else if (currentHour >= 5 && currentHour < 6) { greetingCategory = "earlyMorning"; } else if (currentHour >= 6 && currentHour < 12) { greetingCategory = "morning"; } else if (currentHour >= 12 && currentHour < 14) { greetingCategory = "noon"; } else if (currentHour >= 14 && currentHour < 19) { greetingCategory = "afternoon"; } else if (currentHour >= 19 && currentHour < 21) { greetingCategory = "dusk"; } else if (currentHour >= 21 && currentHour < 24) { greetingCategory = "evening"; } // Choose a random greeting from the appropriate category const greetings = greetingsData["time_based"][greetingCategory]; const greeting = greetings[Math.floor(Math.random() * greetings.length)]; return greeting.replace("{username}", username); } render() { const generatedGreeting = this.getGreeting(); let newProps = {...this.props}; //复写组件的时候,必须带上_noOverwrite参数,避免被复写的组件又被复写导致死循环 return (
{generatedGreeting}
) } } const greetingsData = { "time_based": { "night": [ "凌晨好,{username},夜深了,早些歇息吧。", "深夜时分,{username},注意保重身体哦。" ], "earlyMorning": [ "早上好,{username}!美好的一天从早安开始!", "清晨好,{username},早起的鸟儿有虫吃呢。", "早晨好,{username}!希望您拥有一个精神饱满的好开始!" ], "morning": [ "上午好,{username}!祝您工作顺利,一切井井有条!", "上午好,{username}!希望您的工作有个好开始!", "上午好,{username},工作顺利,万事亨通!" ], "noon": [ "中午好,{username}!无论多忙,希望你能有个温暖的下午!", "中午好,{username}!午后是提升工作效率的黄金时间!", ], "afternoon": [ "下午好,{username},午后是提升工作效率的黄金时间!", "下午好,{username},状态好的时候,工作效率更高!" ], "dusk": [ "傍晚好,{username}!一天的工作即将结束,祝您心情愉快!", "傍晚好,{username}!工作的尾声,请注意劳逸结合!", "傍晚好,{username},今天的工作完成的怎么样?", "傍晚时分,{username},早些歇息,迎接美好的夜晚。" ], "evening": [ "晚上好,{username},今天过得开心吗?", "晚上好,{username}!祝您晚间愉快,早些歇息吧!", ] } } //发布模块 ecodeSDK.setCom('${appId}','NewPopover',NewPopover); ``` [/hide] `init.css(前置加载)` [hide] ```css .ecode-new-popover { position: relative; } .ecode-new-popover-date { position: absolute; right: 260px; width: 300px; height: 54px; line-height: 54px; text-align: center; top: 0; } ``` [/hide] # 四、国际化实现示例 ### getGreeting方法的修改 ``` getGreeting() { const { username, userLanguage } = JSON.parse(localStorage.getItem("theme-account")); const currentHour = new Date().getHours(); let greetingCategory = ""; if (currentHour >= 0 && currentHour < 5) { greetingCategory = "night"; } else if (currentHour >= 5 && currentHour < 6) { greetingCategory = "earlyMorning"; } else if (currentHour >= 6 && currentHour < 12) { greetingCategory = "morning"; } else if (currentHour >= 12 && currentHour < 14) { greetingCategory = "noon"; } else if (currentHour >= 14 && currentHour < 19) { greetingCategory = "afternoon"; } else if (currentHour >= 19 && currentHour < 21) { greetingCategory = "dusk"; } else if (currentHour >= 21 && currentHour < 24) { greetingCategory = "evening"; } // 根据用户语言偏好选择对应的问候语数据 const greetings = greetingsData[userLanguage][greetingCategory]; const greeting = greetings[Math.floor(Math.random() * greetings.length)]; return greeting.replace("{username}", username); } ``` ### greetingsData数据的修改 ``` const greetingsData = { "7": { // 简体中文 "night": [ "凌晨好,{username},夜深了,早些歇息吧。", "深夜时分,{username},注意保重身体哦。" ], "earlyMorning": [ "早上好,{username}!美好的一天从早安开始!", "清晨好,{username},早起的鸟儿有虫吃呢。", "早晨好,{username}!希望您拥有一个精神饱满的好开始!" ], // 其他时间段的问候语 }, "8": { // 英文 "night": [ "Good night, {username}. It's late, get some rest.", "Late at night, {username}. Take care of yourself." ], "earlyMorning": [ "Good morning, {username}! A great day starts with a good morning!", "Good morning, {username}. The early bird catches the worm.", "Good morning, {username}! Wishing you a vibrant start!" ], // 其他时间段的问候语 }, "9": { // 繁體中文 "night": [ "凌晨好,{username},夜深了,早些歇息吧。", "深夜時分,{username},注意保重身體哦。" ], "earlyMorning": [ "早上好,{username}!美好的一天從早安開始!", "清晨好,{username},早起的鳥兒有蟲吃呢。", "早晨好,{username}!希望您擁有一個精神飽滿的好開始!" ], // 其他时间段的问候语 } }; ``` # 五、显示实时时间 如果你想右上角实时显示时间,那你可以这样做: index.js替换为以下内容 `index.js` ``` const { Popover } = antd; class NewPopover extends React.Component { constructor(props) { super(props); this.state = { currentDateTime: this.getCurrentDateTime() } } componentDidMount() { this.intervalId = setInterval(() => { this.setState({ currentDateTime: this.getCurrentDateTime() }); }, 1000); // 每秒更新一次 } componentWillUnmount() { clearInterval(this.intervalId); } getCurrentDateTime() { const now = new Date(); const year = now.getFullYear(); const month = String(now.getMonth() + 1).padStart(2, '0'); const date = String(now.getDate()).padStart(2, '0'); const hours = String(now.getHours()).padStart(2, '0'); const minutes = String(now.getMinutes()).padStart(2, '0'); const seconds = String(now.getSeconds()).padStart(2, '0'); const daysOfWeek = ["星期天", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六"]; const dayOfWeek = daysOfWeek[now.getDay()]; return `${year}年${month}月${date}日 ${hours}:${minutes}:${seconds} ${dayOfWeek}`; } render() { const { currentDateTime } = this.state; let newProps = { ...this.props }; return (
{currentDateTime}
) } } //发布模块 ecodeSDK.setCom('${appId}','NewPopover',NewPopover); ``` [1]: https://cos.oneszhang.com/usr/uploads/old/blog_20230815_ecode3/hello1.png [2]: https://cos.oneszhang.com/usr/uploads/old/blog_20230815_ecode3/hello2.png --- # [泛微 Ecology9] 字段联动-附件联动无感联动 作者:丸子·张 来源:https://blog.oneszhang.com/2023/08/07/48/ Markdown:https://blog.oneszhang.com/2023/08/07/48.md 发布日期:2023-08-07T10:36:00.000Z 更新时间:2023-08-07T10:36:00.000Z 专栏:泛微 E9 标签: 摘要:针对泛微 Ecology 流程中附件字段联动的需求,文章给出基于 ecode 的实现思路:覆写 WeaUpload 组件并调用接口获取附件数据,再绑定字段变化事件回填到目标附件字段。V3 版本调整配置结构,支持同一节点下多组附件字段联动,同时提醒泛微官方已在 KB9002307 中提供标准功能。 > 🚨 **警告** > > 本文由丸子原创,尊重原创,转载请注明出处! > 作者:oneszhang > 原文:https://oneszhang.com/archives/48.html > `丸子近期较忙,暂不梳理该小项目过程,且暂不提供帮助,以下代码回复可见,有问题请留言或一起在下面讨论区交流。` --- > ✅ **泛微官方已于KB9002307中解决该问题!** > > 泛微官方已于 2023年8月 发布新包 KB900230701 编号:2076641 流程引擎字段联动带出附件的需求,在该包中解决了这项问题!现在你可以使用标准功能实现附件联动了! > :::tip[更新通知 V3版本 - 2024年2月26日] - 修复的问题:同一流程节点下无法多个附件字段联动。 - 更新了配置信息数组结构,支持一个 `nodeId` 下配置多个 `fileFieldid` 与 `linkfileFieldid` 的对应关系,使配置更加灵活。 ::: ### 旧版 :::details[附件联动功能 V2版本(旧版)] ```js // 全局变量 let enable = true; let nodeid = null; let workflowId = null; let fileFieldid = null; let linkfileFieldid = null; // 配置信息数组 const configInfo = [ { workflowId: 87, //流程id nodeId: 461, //节点id fileFieldid: 'field9140', //上传附件控件ID linkfileFieldid: 'field9297' //附件联动控件ID(文本框,请先配置联动信息) } ]; // 设置节点ID对应的配置信息 function setConfigInfoForNodeId(nodeId) { console.log("进入了获取联动信息函数"); const configInfoItem = configInfo.find(item => item.nodeId === nodeId); console.log(configInfoItem); if (configInfoItem) { workflowId = configInfoItem.workflowId; fileFieldid = configInfoItem.fileFieldid; linkfileFieldid = configInfoItem.linkfileFieldid; } } // 使用ID调用API async function callApiWithId(id) { const options = { url: '/api/doc/save/getAccListForEdit', method: 'GET', params: { id }, type: 'json', deep: true, checkCode: false }; const ApiResponse = await window.weaJs.callApi(options); return ApiResponse; } // 检查条件是否满足 const checkConditions = () => { const { hash } = window.location; if (!hash.startsWith('#/main/workflow/req') || !WfForm) return false; const baseInfo = WfForm.getBaseInfo(); ({ nodeid } = baseInfo); const isNodeIdAllowed = configInfo.some(item => item.nodeId === nodeid); return isNodeIdAllowed; }; // 覆写 WeaUpload 组件 ecodeSDK.overwritePropsFnQueueMapSet('WeaUpload', { fn: async (newProps) => { if (!enable || !checkConditions()) return; console.log("条件已满足!"); setConfigInfoForNodeId(nodeid); async function processIds(idArray) { const fileLinkdatas = []; for (const id of idArray) { const ApiResponse = await callApiWithId(parseInt(id, 10)); console.log("请求 id 的 API:", id); console.log("ApiResponse:", JSON.stringify(ApiResponse)); if (ApiResponse.dataList && ApiResponse.dataList.length > 0) { const dataListItem = ApiResponse.dataList[0]; const fileData = { fileExtendName: dataListItem.fileExtendName, fileid: dataListItem.id, filelink: dataListItem.filelink, filename: dataListItem.filename, filesize: dataListItem.filesize, imgSrc: "/images/filetypeicons/txt_wev8.png", loadlink: dataListItem.loadlink, showLoad: true, showDelete: true, isImg: false, imagefileid: dataListItem.fileid, secretLevel: "4", secretLevelValidity: "", secretLevelValidityValue: "", }; fileLinkdatas.push(fileData); } } console.log("构建后的 fileLinkdatas:", fileLinkdatas); return Promise.resolve(fileLinkdatas); } async function startProcessing(idArray, value) { const fileLinkdatas = await processIds(idArray); console.log("processIds 执行完毕,开始设置字段值"); const fileFieldValueObj = { "value": value, "specialobj": { "filedatas": fileLinkdatas, "showBatchLoad": false } }; console.log("构建的 fileFieldValueObj:", fileFieldValueObj); WfForm.changeFieldValue(fileFieldid, fileFieldValueObj); } console.log("linkfileFieldid:", linkfileFieldid); WfForm.bindFieldChangeEvent(linkfileFieldid, function (obj, id, value) { console.log("字段变化触发 startProcessing 调用,值为:", value); var idArray = value.split(','); startProcessing(idArray, value); }); }, order: 1, desc: '附件联动功能 V2' }); ``` ::: ### 附件联动功能 V3版本 ``` // 全局变量 let enable = true; let nodeid = null; let workflowId = null; // 更新后的配置信息数组,支持一个nodeId下多个fileFieldid与linkfileFieldid的对应 const configInfo = [ { workflowId: 87, nodeId: 461, fields: [ { fileFieldid: 'field9140', linkfileFieldid: 'field9297' }, ] }, { workflowId: 146, nodeId: 832, fields: [ { fileFieldid: 'field11483', linkfileFieldid: 'field11882' }, { fileFieldid: 'field11482', linkfileFieldid: 'field11883' }, ] } ]; // 设置节点ID对应的配置信息 function getConfigsForNodeId(nodeId) { console.log("进入了获取联动信息函数"); const config = configInfo.find(item => item.nodeId === nodeId); console.log(config); return config ? config.fields : []; } // 使用ID调用API async function callApiWithId(id) { const options = { url: '/api/doc/save/getAccListForEdit', method: 'GET', params: { id }, type: 'json', deep: true, checkCode: false }; const ApiResponse = await window.weaJs.callApi(options); return ApiResponse; } // 检查条件是否满足 const checkConditions = () => { const { hash } = window.location; if (!hash.startsWith('#/main/workflow/req') || !WfForm) return false; const baseInfo = WfForm.getBaseInfo(); ({ nodeid } = baseInfo); const isNodeIdAllowed = configInfo.some(item => item.nodeId === nodeid); return isNodeIdAllowed; }; // 覆写 WeaUpload 组件 ecodeSDK.overwritePropsFnQueueMapSet('WeaUpload', { fn: async (newProps) => { if (!enable || !checkConditions()) return; console.log("条件已满足!"); const fieldConfigs = getConfigsForNodeId(nodeid); fieldConfigs.forEach(config => { const { fileFieldid, linkfileFieldid } = config; async function processIds(idArray) { const fileLinkdatas = []; for (const id of idArray) { const ApiResponse = await callApiWithId(parseInt(id, 10)); console.log("请求 id 的 API:", id); console.log("ApiResponse:", JSON.stringify(ApiResponse)); if (ApiResponse.dataList && ApiResponse.dataList.length > 0) { const dataListItem = ApiResponse.dataList[0]; const fileData = { fileExtendName: dataListItem.fileExtendName, fileid: dataListItem.id, filelink: dataListItem.filelink, filename: dataListItem.filename, filesize: dataListItem.filesize, imgSrc: "/images/filetypeicons/txt_wev8.png", loadlink: dataListItem.loadlink, showLoad: true, showDelete: true, isImg: false, imagefileid: dataListItem.fileid, secretLevel: "4", secretLevelValidity: "", secretLevelValidityValue: "", }; fileLinkdatas.push(fileData); } } console.log("构建后的 fileLinkdatas:", fileLinkdatas); return Promise.resolve(fileLinkdatas); } async function startProcessing(idArray, value) { const fileLinkdatas = await processIds(idArray); console.log("processIds 执行完毕,开始设置字段值"); const fileFieldValueObj = { "value": value, "specialobj": { "filedatas": fileLinkdatas, "showBatchLoad": false } }; console.log("构建的 fileFieldValueObj:", fileFieldValueObj); WfForm.changeFieldValue(fileFieldid, fileFieldValueObj); } console.log("linkfileFieldid:", linkfileFieldid); WfForm.bindFieldChangeEvent(linkfileFieldid, function (obj, id, value) { console.log("字段变化触发 startProcessing 调用,值为:", value); var idArray = value.split(','); startProcessing(idArray, value); }); }); }, order: 1, desc: '附件联动功能 V3' }); ``` :::tip[特别鸣谢] 在此,感谢交流群中每一位盆友的帮助,特别鸣谢大佬:eVER(JSP推广大使) 提供的技术帮助! ::: --- # 创意与智能融合,“丸子小工具”助您事半功倍! 作者:丸子·张 来源:https://blog.oneszhang.com/2023/08/01/47/ Markdown:https://blog.oneszhang.com/2023/08/01/47.md 发布日期:2023-08-01T02:13:00.000Z 更新时间:2023-08-01T02:13:00.000Z 专栏:生活与随笔 标签: 摘要:介绍作者开发的丸子小工具集合,面向泛微 E9 开发与业务人员,提供流程表单前端 JS 自动生成、接口 API 查询、数据库字典检索、AISQL 生成和 SQL 报错修复等在线工具,借助智能技术减少重复劳动、提升开发调试效率,并邀请读者按需访问使用。 :::tip[注意] *本篇文章完全由ChatGPT生成* ::: *大家好!今天我非常高兴地向大家介绍一款我近期开发的有趣且实用的小工具集合——“丸子小工具”。在这个快节奏的时代,我们经常会遇到一些琐碎繁琐的任务,有了这些小工具,您将能事半功倍,提高工作效率,让生活更加便捷。* > ✅ > > 你可以直接点击下方按钮跳转至“丸子小工具”页 > > [丸子小工具](https://oneszhang.com/onestool.html) > --- ## 小工具一:泛微 E9 流程表单前端 JS-AI 自动生成 链接:[https://oneszhang.com/E9API](https://oneszhang.com/E9API) 无论您是一个开发者还是业务人员,想必您都知道在处理流程表单前端时,常常需要耗费大量时间和精力。这款工具通过强大的 JS-AI 技术,能够自动生成泛微 E9 流程表单前端,极大地减轻了开发的工作量,让您能更专注于业务逻辑的实现,提升了开发效率。 ## 小工具二:泛微 E9 流程表单前端接口 API 列表 链接:[https://oneszhang.com/E9API/apilist](https://oneszhang.com/E9API/apilist) 对于开发人员来说,获取一个完整的接口 API 列表是非常重要的。这个小工具为您提供了泛微 E9 流程表单前端接口 API 列表,让您可以轻松查阅,从而更好地进行接口开发和集成。 ## 小工具三:泛微 E9 数据库字典全局检索 链接:[https://oneszhang.com/E9Sql](https://oneszhang.com/E9Sql) 在数据库操作中,查找特定的字典项常常让人头疼。这个小工具帮助您在泛微 E9 数据库中全局检索字典,让您能够快速定位所需信息,加快开发速度,减少了不必要的麻烦。 ## 小工具四:丸子的 AISQL 生成助手 链接:[https://oneszhang.com/AISQL](https://oneszhang.com/AISQL) 作为丸子小工具系列的重要一员,AISQL 生成助手将帮助您自动生成高效且准确的 AISQL 代码,为您在数据处理和分析方面提供强大的支持,让您能够更加专注于发现数据背后的价值。 ## 小工具五:丸子的 SQL 报错修复工具 链接:[https://oneszhang.com/AISQL/debug/](https://oneszhang.com/AISQL/debug/) 在 SQL 开发过程中,难免会遇到各种报错,而定位和修复这些问题常常耗费大量时间。有了这个小工具,您可以快速定位 SQL 报错并进行修复,极大地缩短了调试时间,让您的开发更加高效。 --- “丸子小工具”集合的诞生,得益于现代技术的发展,尤其是ChatGPT的出现为我们提供了强大的智能支持。我相信,在不断的优化和更新下,这些小工具将会为您带来更多惊喜和便利。如果您也是一个喜欢创造和探索的人,不妨前往我的网站 [https://oneszhang.com](https://oneszhang.com) 一探究竟,相信您一定会找到适合您的小工具。 让我们一起拥抱创新,用智能小工具让生活变得更加简单、高效! --- # [泛微 Ecology9] 丸子的 Ecology9 小本本 作者:丸子·张 来源:https://blog.oneszhang.com/2023/08/01/e9notebook/ Markdown:https://blog.oneszhang.com/2023/08/01/e9notebook.md 发布日期:2023-08-01T01:27:00.000Z 更新时间:2023-08-01T01:27:00.000Z 专栏:泛微 E9 标签: 摘要:一篇泛微 Ecology E9 二次开发的速查笔记,整理了 E9 单页地址拼接规则、新建流程与接口 URL、常用 SQL(流程节点查询、离职人员信息、选择框字段取值)等资料,并记录签字意见存储表、节点附加操作执行顺序、归档节点过滤离职人员配置、相关流程权限校验等细节,适合 E9 二开与运维人员日常查阅。 # 常用资料 已归档至OA工程师导航站 [OA工程师导航 huahan.cc 点击前往 →](huahan.cc) # E9系统URL相关 ## E9本地地址 - **PC端调试手机端地址** `/spa/portal/static4mobilelogin/index.html` ## E9单页地址获取规则 ### 标准模块 - **前台** 将URL中 `/wui/index.html` 替换成 `/spa/hrm/index_mobx.html` - **后台** 将URL中 `/wui/engine.html` 替换成 `/spa/hrm/engine.html` ### 非标模块 - **考勤前台** 将URL中 `/wui/index.html` 替换成 `/spa/hrm/static4attendance/index.html` - **考勤后台** 将URL中 `/wui/engine.html` 替换成 `/spa/hrm/staticAttendance4engine/engine.html` ## 新建流程 - **新建流程地址** `/workflow/request/AddRequest.jsp?workflowid=xxx` ## 查询接口执行的SQL(存在dataKey) - **接口执行的SQL查询** `/api/ec/dev/table/getxml?dataKey=` # SQL查询 ## 查询某流程节点的所有提交时间 ```sql SELECT requestid, userid, processUser, workflowid, nodeid, isbereject, isremark, operatedate, operatetime FROM workflow_currentoperator WHERE (nodeid = 465 OR nodeid = 368) AND (isbereject IS NULL OR isbereject = '') AND isremark = 2; ``` ## SQL Server 查询OA离职人员信息 ```sql SELECT b.lastname AS 姓名, c.departmentname AS 部门, d.subcompanyname AS 分部, d.subcompanyname AS 公司名称, b.companystartdate AS 入职日期, a.operatedate AS 离职日期, a.operatetime AS 离职时间 FROM hrmstatushistory AS a LEFT JOIN hrmresource b ON b.id = a.resourceid LEFT JOIN HrmDepartment c ON c.id = b.departmentid LEFT JOIN hrmsubcompany d ON d.id = b.subcompanyid1 WHERE a.type_n = 5 AND a.resourceid = 6; ``` ### 查询当前年的人员数据 ```sql SELECT * FROM HrmResource WHERE status = 5 AND YEAR(enddate) = YEAR(GETDATE()); ``` ## 查询流程节点 ### 查询流程节点名称及ID ```sql SELECT wfn.nodeid, wfn.workflowid, wnb.nodename FROM workflow_flownode AS wfn JOIN workflow_nodebase AS wnb ON wfn.nodeid = wnb.id WHERE wfn.workflowid IN (149, 145, 140, 133, 106); ``` ### 通过表单ID查询流程ID(多版本) ```sql SELECT id, workflowname, version FROM workflow_base WHERE formid = {表单ID}; ``` ### 通过流程ID(多版本)查询节点ID及节点名称 ```sql SELECT wfn.nodeid, wfn.workflowid, wnb.nodename FROM workflow_flownode AS wfn JOIN workflow_nodebase AS wnb ON wfn.nodeid = wnb.id WHERE wfn.workflowid IN ({流程ID}); ``` ### 通过表单ID查询节点ID及节点名称 ```sql SELECT wfn.nodeid, wfn.workflowid, wnb.nodename FROM workflow_base AS wb JOIN workflow_flownode AS wfn ON wb.id = wfn.workflowid JOIN workflow_nodebase AS wnb ON wfn.nodeid = wnb.id WHERE wb.formid = {表单ID}; ``` ### 当前流程的请求历史,前两条 select listagg(requestid, ',') within group (order by requestid) as requestid from ( ... ) where rownum <= 2 ## 关于选择框 - **独立单选用ID,公共单选用ID排序** 结论:非必要情况(多处引用),不使用公共单选 ### 查询选择框字段值对应ID ```sql SELECT selectvalue, selectname FROM workflow_selectitem WHERE fieldid = "$这里填写你要查询的字段ID$,如12346"; ``` ### 公共选择项信息表 - 表名: `mode_selectitempage` ### 公共选择框明细表 - 表名: `mode_selectitempagedetail` # 小知识记录 - **JS赋值只读字段,不会保入数据库(字段联动除外)** - **字段显示名(多国语言)存放在数据库表** `HtmlLabelInfo` - **获取当前用户信息** `JSON.parse(localStorage.getItem("theme-account"))` - **流程的签字意见在 `workflow_requestlog` 表 `remark` 字段** - **流程相关交流在**`workflow_communicationbase` `workflow_communicationcontent` - **侧栏打开** `openSlideModal(true,"https://oneszhang.com",50);` - **微信扫码免登录空白页** `&unloadjem=1` - **创建流程传参** `/spa/workflow/index_form.jsp#/main/workflow/req?iscreate=1&workflowid=132&field10681=1&field10712=16` - **抛出异常** `throw new Error("数据请求失败,请稍候重试");` - **流程待办页面tab顺序** `workflow_dimension` - **隐藏左侧树&隐藏标题** `needTree=false&needTop=false 或者可能是 &hidetop=1` - **节点附加操作执行顺序** 保存表单数据 → 节点后附加操作 → 生成编号 → 出口附加规则 → 节点前附加操作→ 插入操作者和签字意见 - **新建流程页面过滤类型或流程** wfTypeRange 过滤工作流类型 "1,2,3",wfRange 过滤工作流ID "1001,1002,1003" - **审批节点操作组会签与非会签关系**:同一操作组,按设置的批次数值执行,相同为非会签,不同则按0,1,2....顺序执行; 不同操作组之间为会签关系 ### E9的归档离职人员说明  配置文件:ecology\WEB-INF\prop\workflow_isDimission.properties  归档节点是否去掉离职人员 1:启用 0:不启用  isDimission=1  归档节点不过滤离职人员类型  3:指定人员 1:指定部门 17:创建人本人 18:创建人上级 40:节点操作者 all:所有类型均不过滤  为空所有类型均过滤离职人员  filterType=  filterType 请配置成 空 才会真正过滤,不要配置成all ### 表单中相关流程权限校验逻辑: 1. 当前字段是流程浏览按钮类型字段,如流程浏览按钮、归档流程浏览按钮等 2. 当前流程和关联流程的操作者之间有交集:有任意一个人既参与过当前流程,也参与过关联流程,只需要在流程状态中出现过,即为参与过,就可以查看关联流程。(可配置相关流程不校验此项,在ecology\WEB-INF\prop\workflow_E9noCheckRightWf.properties文件中配置关联流程workflowid的值,将不需要校验此项的路径id配置进去,多个以英文逗号隔开,配置的路径将会产生越权漏洞,如数据库更改流程字段的值,可通过相关流程权限越权查看系统所有流程,请谨慎操作) --- # [泛微 Ecology9] 之ecodeSDK API文档 作者:丸子·张 来源:https://blog.oneszhang.com/2023/07/25/ecodeSDK/ Markdown:https://blog.oneszhang.com/2023/07/25/ecodeSDK.md 发布日期:2023-07-25T03:15:00.000Z 更新时间:2023-07-25T03:15:00.000Z 专栏:泛微 E9 标签: 摘要:面向泛微 Ecology9 二次开发人员的 ecodeSDK API 参考文档,整理了 PC 与移动端组件参数复写、组件重写、URL 匹配检查、接口请求参数与响应数据拦截、本地存储读取等常用接口,均附参数说明与调用示例,适合进行无侵入界面定制开发时查阅。内容由作者自行收集分析整理,可能存在错漏,使用前建议对照官方文档甄别核实。 # 写在前面 ## 主要内容 本文档主要对**泛微 ecodeSDK** 的 API 列表和详细信息进行展示,以帮助开发人员更好地了解和使用该 SDK 的功能。由于未能找到完整的 SDK API 列表文档,因此制作此文档以记录供大家及自己使用。 **本文档包括以下内容:** 1. API 列表:提供 SDK 中可用的所有 API 的名称和功能简介。 2. API 详细:提供每个 API 的详细信息,包括参数、返回值、使用示例等。 3. 使用示例:提供一些使用示例,以便开发人员更好地理解如何使用 SDK 中的 API。 需要注意的是,由于文档可能存在更新和变化,建议在使用前再次查阅官方文档以获取最新的 SDK API 信息。 > ⚠️ **特别注意** > > 本文档由丸子本人收集整理制作,内容多由丸子自行分析整理,部分来源自网络,难免存在错误和不详细之处,使用时请自行甄别;欢迎评论区反馈,引用请务必注明出处! > --- ## 泛微 ecode 简介 > **ecode 是内嵌在 E-cology9 系统中,为了解决 E-cology9 所有标准界面无侵入定制和扩展二次开发的开发工具。** **泛微官方 ecode 使用说明:** [e-cloudstore.com](https://e-cloudstore.com/doc.html) :::details[展开详情] > ecode 核心是解决 EC 所有界面无侵入定制和扩展二次开发,采用动态注册,不直接修改源码的方案解决开发需求。除了新开发页面的之外,它还支持所有标准页面的修改。ecode 包含了整套前端编辑器、编译器,可以让用户无需搭建开发环境直接通过浏览器开始 single page 前端开发,同时采用 ecode 开发的内容都是插件化,支持一键共享、复用、二次封装。 --- **ecode 中可能用到的技术栈(Technology Stack):** PC 端: `js、css、es6、react16.x、react-router3、mobx` MOBILE 端: `js、css、es6、react16.x、react-router4、mobx` ::: :::tip[关于如何使用 ecode] 关于如何使用 ecode,官方已给出详细的使用说明,你可以直接点击上方链接或访问官方说明文档:https://e-cloudstore.com/doc.html 进行学习,丸子不在此进行过多介绍 ::: --- # * SDK API 列表 * ## ecodeSDK ### 1. 数组排序(ecode 运行顺序) > 对 item 结构体数组按照 order 属性进行排序,该 API 无需使用 `ecodeSDK.ecodeQueueSort(arr)` **参数说明** | 参数 | 参数类型 | 必须 | 说明 | | --- | --- | --- | --- | | arr | item 结构体数组 | 是 | 需要排序的数组 | **样例** ``` let arr = [ { order: 3, name: "C" }, { order: 1, name: "A" }, { order: 4, name: "D" }, { order: 2, name: "B" } ]; ecodeSDK.ecodeQueueSort(arr); ``` ### 2. PC 端组件参数复写 > 对 EC 全系统 PC 端组件参数进行复写,包含 [组件站](https://e-cloudstore.com/e9/index2.html) PC 组件库 ecCom、antd 大部分组件 ,其中 Radio、Radio.Button、Tree.TreeNode、Menu.ItemGruop、Select.Option、Select.OptGroup、SelectNew.Option、SelectNew.OptGroup 暂不支持复写 `ecodeSDK.overwritePropsFnQueueMapSet(name,option)` **参数说明** | 参数 | 参数类型 | 必须 | 说明 | | --- | --- | --- | --- | | name | String | 是 | 组件库 ecCom、antd 组件名称 | | option | Object | 是 | 复写配置 | | option.fn | Function | 是 | 复写钩子函数 `(newProps)=>{}` | | option.order | Integer | 否 | 复写排序 | | option.desc | String | 否 | 复写说明 | **样例** ``` ecodeSDK.overwritePropsFnQueueMapSet('WeaTop',{ //组件名 fn:(newProps)=>{ //newProps 代表组件参数 //建议添加进行位置判断操作 console.log('WeaTop:',newProps); //在这里输出日志,如果成功输出代表组件成功定位 }, order:1, //排序字段,如果存在同一个页面复写了同一个组件,控制顺序时使用 desc:'在这里写此复写的作用,在调试的时候方便查找' }); ``` ### 3. PC 端组件重写 > 对 EC 全系统 PC 端组件参数进行重写,重新定义的组件只要遵循原组件的交互要求,即可进行自由定义 `ecodeSDK.overwriteClassFnQueueMapSet(name,option)` **参数说明** | 参数 | 参数类型 | 必须 | 说明 | | --- | --- | --- | --- | | name | String | 是 | 组件库 `ecCom`、`antd` 组件名称 | | option | Object | 是 | 重写配置 | | option.fn | Function | 是 | 重写钩子函数 `(Com,newProps)=>{}` | | option.order | Integer | 否 | 重写排序 | | option.desc | String | 否 | 重写说明 | **样例** ``` ecodeSDK.overwriteClassFnQueueMapSet('Table',{ fn:(Com,newProps)=>{ //Com是当前复写的原组件 //newProps是当前复写的原组件参数 return { com:Com, props:newProps }; }, order:1, desc:'浏览按钮复写' }); ``` ### 4. MOBILE 端组件参数复写: > 对 EC 全系统 Mobile 端组件参数进行复写,包含 [组件站](https://e-cloudstore.com/e9/index2.html) 移动组件库 WeaverMobile、WeaMobilePage 大部分组件 `ecodeSDK.overwriteMobilePropsFnQueueMapSet(name,option)` **参数说明** | 参数 | 参数类型 | 必须 | 说明 | | --- | --- | --- | --- | | name | String | 是 | 组件库 `WeaverMobile`、`WeaMobilePage ` 组件名称 | | option | Object | 是 | 复写配置 | | option.fn | Function | 是 | 复写钩子函数 `(newProps)=>{}` | | option.order | Integer | 否 | 复写排序 | | option.desc | String | 否 | 复写说明 | **样例** ``` ecodeSDK.overwriteMobilePropsFnQueueMapSet('TabPage',{ //组件名 fn:(newProps)=>{ //newProps代表组件参数 //建议添加进行位置判断操作 console.log('TabPage:',newProps); //在这里输出日志,如果成功输出代表组件成功定位 }, order:1, //排序字段,如果存在同一个页面复写了同一个组件,控制顺序时使用 desc:'在这里写此复写的作用,在调试的时候方便查找' }); ``` ### 5. MOBILE 端组件重写 > 对 EC 全系统 Mobile 端组件参数进行重写,重新定义的组件只要遵循原组件的交互要求,即可进行自由定义 `ecodeSDK.overwriteMobileClassFnQueueMapSet(name,option)` **参数说明** | 参数 | 参数类型 | 必须 | 说明 | | --- | --- | --- | --- | | name | String | 是 | 组件库 `WeaverMobile`、`WeaMobilePage `组件名称 | | option | Object | 是 | 重写配置 | | option.fn | Function | 是 | 重写钩子函数 `(Com,newProps)=>{}` | | option.order | Integer | 否 | 重写排序 | | option.desc | String | 否 | 重写说明 | **样例** ``` ecodeSDK.overwriteMobileClassFnQueueMapSet('WeaBrowser',{ fn:(Com,newProps)=>{ //Com是当前复写的原组件 //newProps是当前复写的原组件参数 return { com:Com, props:newProps }; } }, order:1, desc:'浏览按钮复写' }) ``` ### 6. 检查 url 是否匹配 > 用于检查当前页面的 URL 是否与给定的 URL 匹配 `ecodeSDK.checkLPath(url)` **参数说明** | 参数 | 参数类型 | 必须 | 说明 | | --- | --- | --- | --- | | url | String | 是 | 需要检查的url | **样例** ``` function checkURL() { var url = '/wui/index.html#/main/workflow/listDoing'; var result = ecodeSDK.checkLPath(url); if (result) { console.log('URL路径有效'); } else { console.log('URL路径无效'); } } checkURL(); ``` ### 7. 在 props 对象中查找指定的键值 > 在复杂的属性对象中查找指定键的值。它通过递归地遍历属性对象的子对象和子数组来查找,并返回找到的第一个非空值。如果没有找到非空值,则返回空字符串。 `ecodeSDK.findPropData(props, key)` **参数说明** | 参数 | 参数类型 | 必须 | 说明 | | --- | --- | --- | --- | | props | Object | 是 | 要查找的属性对象 | | key | String | 是 | 要查找的键 | **样例** ``` var props = { children: [ { props: { key1: 'value1', key2: 'value2' } }, { props: { key3: 'value3', key4: 'value4' } } ] }; var key = 'key3'; var result = ecodeSDK.findPropData(props, key); console.log(result); // 输出:value3 ``` ### 8. 复写 API 响应数据 > 用于修改 API 的响应数据。这种功能通常在开发过程中使用,允许开发人员对从 API 接收到的数据进行定制化的处理。在这个函数中,可以根据特定的条件来拦截接口响应数据,并响应修改后的数据。注意:本接口会监听PC端、手机端接口都会触发。 `ecodeSDK.rewriteApiDataQueueSet(fn)` **参数说明** | 参数 | 参数类型 | 必须 | 说明 | | --- | --- | --- | --- | | fn | Function | 是 | 用于修改 API 响应数据的函数 `(url, params, data)=>{}` | | (url) | String | - | API 的 URL(接口路径) | | (params) | Object | - | API 的参数 | | (data) | Object | - | API 的响应数据 | **样例** ``` ecodeSDK.rewriteApiDataQueueSet({ fn: function fn(url, params, data) { // 添加判断,如果当前页面路径不是'/spa/hrm/engine.html#/hrmengine/authAdjust',则不执行复写逻辑 if (!ecodeSDK.checkLPath('/spa/hrm/engine.html#/hrmengine/authAdjust')) return; console.log("路径识别正确!"); if (url.indexOf("/api/public/browser/condition/1") === -1) return; console.log("API识别正确!"); console.log("url", url); console.log("params", params); console.log("data", data); // 开始复写,这里是复写的内容 //return data;(丸子一般不习惯return,注释掉就好) }, }); ``` ### 9. 复写 PC 端 API 请求参数 > 修改接口请求的参数。具体来说,这个函数会接收一个函数作为参数,这个函数会被调用来处理接口请求的参数。在这个函数中,可以根据特定的条件来修改请求参数,并返回修改后的参数。 `ecodeSDK.rewriteApiParamsQueueSet(fn)` **参数说明** | 参数 | 参数类型 | 必须 | 说明 | | --- | --- | --- | --- | | fn | Function | 是 | 用于修改 API 请求参数的函数 `(url, method, params)=>{}` | | (url) | String | - | API 的 URL(接口路径) | | (method) | Object | - | API 的请求类型 | | (params) | Object | - | API 的请求参数 | **样例** ``` ecodeSDK.rewriteApiParamsQueueSet({ fn: (url, method, params) => { // 不是需要拦截的接口返回默认的参数 return { url: url, // 接口路径 method: method, // 请求类型 params: params // 请求参数 } } }); ``` ### 10. 复写 MOBILE 端 API 请求参数 > 修改接口请求的参数。具体来说,这个函数会接收一个函数作为参数,这个函数会被调用来处理接口请求的参数。在这个函数中,可以根据特定的条件来修改请求参数,并返回修改后的参数。 `ecodeSDK.rewriteMobileApiParamsQueueSet(fn)` **参数说明** | 参数 | 参数类型 | 必须 | 说明 | | --- | --- | --- | --- | | fn | Function | 是 | 用于修改 API 请求参数的函数 `(url, method, params)=>{}` | | (url) | String | - | API 的 URL(接口路径) | | (method) | Object | - | API 的请求类型 | | (params) | Object | - | API 的请求参数 | **样例** ``` ecodeSDK.rewriteMobileApiParamsQueueSet({ fn: (url, method, params) => { // 不是需要拦截的接口返回默认的参数 return { url: url, // 接口路径 method: method, // 请求类型 params: params // 请求参数 } } }); ``` ### 11. 获取本地存储中的 JSON 数据 > 用于从 `window.localStorage` 中获取存储的 JSON 数据并将其解析为 JavaScript 对象,函数返回获取到的属性值,如果指定的键名不存在或数据无法解析为 JSON 格式,函数会返回一个空对象 `{}`。 `ecodeSDK.getJSONObj(key)` **参数说明** | 参数 | 参数类型 | 必须 | 说明 | | --- | --- | --- | --- | | key | String | 是 | 要获取的本地存储数据的键名 | **样例** ``` // 存储在本地的数据 window.localStorage['userData'] = '{"name": "Alice", "age": 25, "email": "alice@example.com"}'; // 使用示例 var userData = ecodeSDK.getJSONObj('userData'); console.log(userData.name); // 输出:Alice console.log(userData.age); // 输出:25 console.log(userData.email); // 输出:alice@example.com ``` ### 12. 逐级获取本地存储中嵌套的 JSON 数据 > 用于逐级获取本地存储数据中嵌套的 JSON 对象的属性值,函数返回获取到的属性值,如果遍历过程中出现了错误(比如某个键名不存在或获取数据失败),则函数会返回空字符串 `''`。 `ecodeSDK.getEcodeParams(keys)` **参数说明** | 参数 | 参数类型 | 必须 | 说明 | | --- | --- | --- | --- | | keys | Array | 是 | 由字符串组成的数组,表示逐级获取本地存储数据中的属性路径。 | **样例** ``` // 存储在本地的数据 window.localStorage['userData'] = '{"name": "Alice", "age": 25, "address": {"city": "New York", "country": "USA"}}'; // 使用示例 var name = ecodeSDK.getEcodeParams(['userData', 'name']); console.log(name); // 输出:Alice var city = ecodeSDK.getEcodeParams(['userData', 'address', 'city']); console.log(city); // 输出:New York ``` ### 13. 动态加载资源文件 > 为动态加载 JavaScript 和 CSS 资源提供了方便的接口,使得页面在需要时可以按需加载所需的资源文件,该函数还提供了一些静态方法,例如 `t.ready`、`t.done`、`t.reset` 和 `t.isDefined`,用于处理资源的加载完成状态、重置状态和检查资源是否已定义等;loadjs 加载第三方资源,注意加载次数,避免多次请求重复资源 `ecodeSDK.loadjs(resourceUrl, { callbacks, options })` **参数说明** | 参数 | 参数类型 | 必须 | 说明 | | --- | --- | --- | --- | | resourceUrl | String | 是 | 要加载的资源的 URL,可以是 JavaScript 或 CSS 文件的 URL。| | callbacks | Object | 否 | 包含回调函数的对象,用于在资源加载成功或失败后执行相应的操作。可以包含 `success` 和 `error` 回调函数。| | `callbacks.success` | Function | 否 | 资源加载成功后调用的回调函数。 | | `callbacks.error` | Function | 否 |资源加载失败后调用的回调函数。 | | options | Object | 否 | 包含加载资源的选项的对象。可以指定以下选项:`async`、`numRetries`、`before`和`returnPromise`。| | `options.async`| Boolean | 否 | 表示是否异步加载资源,默认为 `true`。 | | `options.numRetries`| Number | 否 | 表示加载重试次数,默认为 `0`。 | | `options.before`| Function | 否 | 在加载资源之前执行的回调函数,默认为空函数。| | `options.returnPromise` | Boolean | 否 | 如果为 `true`,则返回一个 Promise 对象,否则返回 `undefined`。| | retryCount | Number | 否 | 内部使用的加载重试计数,默认为 `0`。| | successCb | Function | 否 | 资源加载成功后调用的回调函数。| | errorCb | Function | 否 | 资源加载失败后调用的回调函数。| **样例** ``` //异步加载 JavaScript 文件 ecodeSDK.loadjs('https://example.com/js/script.js', { async: true, numRetries: 2, before: function () { console.log('Before loading the script...'); }, success: function () { console.log('Script loaded successfully!'); }, error: function () { console.error('Failed to load the script!'); } }); //同步加载 JavaScript 文件并使用 Promise ecodeSDK.loadjs('https://example.com/js/script.js', { async: false, returnPromise: true }) .then(function () { console.log('Script loaded successfully!'); }) .catch(function () { console.error('Failed to load the script!'); }); //加载 CSS 文件: ecodeSDK.loadjs('https://example.com/css/styles.css', { async: true, numRetries: 1, before: function () { console.log('Before loading the CSS...'); }, success: function () { console.log('CSS loaded successfully!'); }, error: function () { console.error('Failed to load the CSS!'); } }); //loadjs 加载第三方资源,注意加载次数,避免多次请求重复资源 ecodeSDK.loadjs.isDefined('name') // 判断资源是否加载到 ``` * ## window.weaJs ### 1. 提取当前窗口的 URL 中查询参数 > 从当前窗口的 URL 中提取查询参数,并将这些查询参数作为一个对象返回。 `window.weaJs.getFrameParams()` **参数说明** 无 **样例** ``` // 调用 getFrameParams 方法获取当前页面的查询参数 var params = window.weaJs.getFrameParams(); // 输出获取的查询参数对象 console.log(params); ``` ### 2. 构建 options 对象调用 API > 用于调用 API,根据传入的选项执行相应的操作;请注意,实际使用时,您需要根据具体的 API 接口和请求方法,调整选项中的 URL、method、params 等参数,以及根据需要处理返回的 API 响应。 `window.weaJs.callApi(options)` **参数说明** | 参数 | 参数类型 | 必须 | 说明 | | --- | --- | --- | --- | | options | Object | 是 | 包含调用 API 的选项的对象,可以指定以下参数。| | options.url | String | 是 | 要调用的 API 的 URL。| | options.method | String | 否 | 请求方法,默认为 'GET'。 | | options.params | Object | 否 | 请求参数,作为 GET 或 POST 请求的参数。 | | options.type | String | 否 | 指定返回数据的类型,默认为 'json'。 | | options.deep | Boolean | 否 | 是否深度请求,针对复杂请求类型。 | | options.checkCode | Boolean | 否 | 是否检查响应状态码,如果为 true,则会检查返回的状态码,否则不检查。 | **样例** ``` var options = { url: 'https://api.example.com/data', method: 'POST', params: { key: 'value' }, type: 'json', deep: true, checkCode: false }; var ApiResponse = window.weaJs.callApi(options); console.log(ApiResponse); ``` ### 3. 显示弹窗提示 > 用于在页面上显示弹窗提示。该方法在移动端和 PC 端使用方式略有不同,会根据环境调用相应的弹窗组件或原生弹窗。 `window.weaJs.alert(content)` **参数说明** | 参数 | 参数类型 | 必须 | 说明 | | --- | --- | --- | --- | | content | String | 是 | 弹窗的提示信息内容。| **样例** ``` // 显示弹窗提示 var content = "这是一个提示信息。"; window.weaJs.alert(content); ``` ### 4. 显示对话框或浮层 > 用于显示对话框或浮层。根据页面运行环境,在 PC 端将创建对话框并显示指定 URL 的页面,而在移动端(0601 之后的版本)将创建一个浮层显示指定 URL 的页面。请注意,对话框和浮层的显示效果可能因具体页面环境和样式设置而有所不同。在移动端,创建的浮层可以通过 `frameView.destory()` 方法来关闭。 `window.weaJs.showDialog(url, params)` **参数说明** | 参数 | 参数类型 | 必须 | 说明 | | --- | --- | --- | --- | | url | String | 是 | 要显示的页面的 URL。 | | params | Object | 否 | 作为 GET 请求的参数,传递给要显示的页面。 | * params 参数说明 (移动端默认为路由全屏,不支持设置特殊参数) | 属性 | 说明 | 类型 | | ------------ | ----------------------------------------- | ---------- | | params.title | 标题 | String | | params.moduleName | 模块名 | String | | params.style | 弹框样式,一般用来设置宽高 | Object | | params.callback | 确认回调 | Function | | params.onCancel | 取消回调 | Function | **样例** ``` // 在 PC 端显示对话框 var url = 'https://example.com/dialog.html'; var params ={ title: '标题', moduleName: 'workflow', style: { width: 800, height: 600 }, callback: function() {}, onCancel: function() {}, }; var dialog = window.weaJs.showDialog(url, params); // PC端关闭对话框 dialog.destory(); // 移动端关闭浮层 frameView.destory(); ``` ### 5. 创建自定义组件-`Con` > 是一个构造函数,用于创建自定义组件。在这个方法中,定义了一个名为 `WeaCon` 的类(同样继承自 `React.Component`),用于创建自定义组件。 `window.weaJs.Con()` **参数说明** 无 **样例** ``` // 使用 window.weaJs.Con 方法创建自定义组件 var MyCustomComponent = new window.weaJs.Con(); // 定义组件的 render 方法 MyCustomComponent.prototype.render = function() { return (

Hello, This is another Custom Component!

It can also be used to render custom content.

); }; // 渲染自定义组件 ReactDOM.render(, document.getElementById('root')); ``` :::tip[提示] * 使用 `window.weaJs.Con` 方法创建的自定义组件是一个类,继承自 `React.Component`。 * 组件创建的方式更接近传统的 ES6 类的写法,通过 `class` 关键字和 `extends` 关键字来定义组件。 * 适用于在 PC 端开发时,使用 React 库创建自定义组件。 ::: ### 6. 创建自定义组件-`Custom` > 是一个构造函数,用于创建自定义组件。在这个方法中,定义了一个名为 `WeaCustom` 的类(继承自 `React.Component`),可以通过 `prototype.render` 方法定义组件的显示内容。 `window.weaJs.Custom()` **参数说明** 无 **样例** ``` // 使用 window.weaJs.Custom 方法创建自定义组件 var MyCustomComponent = new window.weaJs.Custom(); // 定义组件的 render 方法 MyCustomComponent.prototype.render = function() { return (

Hello, This is a Custom Component!

It can be used to render custom content.

); }; // 渲染自定义组件 ReactDOM.render(, document.getElementById('root')); ``` :::tip[提示] * 使用 `window.weaJs.Custom` 方法创建的自定义组件也是一个类,同样继承自 `React.Component`。 * 组件创建的方式更接近传统的 JavaScript 构造函数的写法,通过原型链和构造函数来定义组件。 * 适用于在移动端开发时,使用 React 库创建自定义组件。 ::: --- # [泛微 Ecology9] 组件复写-隐藏流程保存按钮 作者:丸子·张 来源:https://blog.oneszhang.com/2023/06/26/ecode1/ Markdown:https://blog.oneszhang.com/2023/06/26/ecode1.md 发布日期:2023-06-26T10:29:00.000Z 更新时间:2023-06-26T10:29:00.000Z 专栏:泛微 E9 标签: 摘要:针对泛微 Ecology EC9 流程保存按钮无法在后台直接关闭的需求,文章给出隐藏该按钮的方案演变:推荐做法是借助 CSS 的 :has 选择器隐藏顶部按钮与右键菜单中的保存项,仅做视觉隐藏、保存功能仍可被代码触发;同时回顾了通过 ecode SDK 复写 WeaReqTop 等组件属性来过滤按钮的旧方案,因其引发连锁 BUG 已不建议使用,供 EC9 二次开发人员参考。 # 2024年11月15日 14:33:10 最新解决方案 ### 代码块如下 根据评论区小明的方案启发(感谢),故出此方案: 注意,该方案仅隐藏按钮,无法直接操作,不去除"保存"功能(使用代码等依旧可以触发) ``` ``` 实现效果: ![2024-11-15T06:33:48.png][1] :::details[历史ecode处理记录(方案违规)] > 🚨 **不建议使用该方案告知!!!** > > 2023年9月21日 10:58:07 经测试存在较大连锁BUG导致其他功能不可用,丸子已不建议使用该方案! > > 🚨 **文章暂未完成告知** > > 丸子暂时还未完成该文章,为提前公布代码故将草稿开放。 > # 1. 前言 ### 故事&起源 有一天,丸子在公司接到这样一个需求:“把那个流程的保存按钮!关了!”,起始十分疑惑,后来得知,有员工一直在反馈同一个问题:“?为什么我点了保存,那个台账上看不到我填的详情信息。” > (因为在流程保存时,没有做触发更新台账(建模应用)信息,只有用户提交时才会将信息更新到台账。) ![保存按钮](https://cos.oneszhang.com/usr/uploads/old/blog_20230726_ecode1/savebt.png) 丸子想了蛮久,感觉直接取消这个按钮并不是很好,但最后,奈何咱比较懒,还是选择了~~既然懒得解决问题,那就解决出问题的人~~#(暗地观察)直接取消按钮:“不就是一个开关一关就完事了嘛!”,就它了,直接取消按钮! 然鹅!!#(咽气) 这个选择框,竟然是!灰的!灰的!灰的! ![灰色的保存选择框](https://cos.oneszhang.com/usr/uploads/old/blog_20230726_ecode1/saveblack.png) 交流群提问,暂时没有什么好办法,只能ecode了! 可我,还没写过,但!既然选择了路,哭着也要走完!#(喜极而泣) 开整! > ⚠️ **寻找大佬** > > 如果有大佬有什么更方便,或者能直接将选择框改为可选的办法,请留言! > :::tip[似乎有更简单的办法了] 今天是2023年7月26日(一个月后),我找到了直接ecode拦截复写响应数据的办法,可以直接复写按钮请求返回数据达到目的。 ::: # 2. 实现步骤 * 当然是先学一天的ecode!学习过程在此就不赘述了。~~补写这篇博客时,已经是一个月后,我太懒了~~ 如果想了解ecodeSDK API,你可以查看我的另一篇博客:[泛微 EC9 之 ecodeSKD API 文档 - 丸子·张丨 OnesZhang](https://oneszhang.com/archives/ecodeSKD.html) ## 分析 * 修改代码:展示如何在代码中实现隐藏保存按钮的功能。 * 调试与测试:描述代码调试和功能测试的过程。 ## 实现 `丸子近期较忙,暂不梳理该小项目过程,且暂不提供帮助,以下代码回复可见,有问题请留言或一起在下面讨论区交流。` [hide] ```js let enable = true; const allowedWorkflowIds = [83, 85]; // 允许的流程ID列表 const runScript = () => { // 代码块钩子,类似放在代码块中或者jquery.ready // 可操作WfForm,以及部分表单dom hiden、ReactDOM.render console.log('runScript!'); isRun = true; // 确保只执行一次 }; //判断条件 const checkConditions = () => { const { hash } = window.location; if (!hash.startsWith('#/main/workflow/req')) return false; // 判断页面地址 if (!ecCom.WeaTools.Base64) return false; // 完整组件库加载完成 if (!WfForm) return false; // 表单sdk加载完成 const baseInfo = WfForm.getBaseInfo(); const { workflowid } = baseInfo; return allowedWorkflowIds.includes(workflowid); // 判断流程id是否在允许的列表中 }; //复写下拉菜单 const overwriteDropMenuDatas = (newProps) => { if (newProps && newProps.dropMenuDatas) { console.log('dropMenuDatas:', newProps.dropMenuDatas); const { dropMenuDatas } = newProps; newProps.dropMenuDatas = dropMenuDatas.filter(item => item.key !== 'BTN_WFSAVE_1'); console.log('newdropMenuDatas:', newProps.dropMenuDatas); } }; //复写右侧按钮 const overwriteButtons = (newProps) => { if (newProps && newProps.buttons) { console.log('buttons:', newProps.buttons); const { buttons } = newProps; newProps.buttons = buttons.filter(button => button.props.title !== '保存'); } }; //复写右键菜单 const overwriteRightMenuDatas = (newProps) => { if (newProps && newProps.datas) { console.log('datas:', newProps.datas); const { datas } = newProps; newProps.datas = datas.filter(item => item.key !== 'BTN_WFSAVE_1'); console.log('newdropMenuDatas:', newProps.datas); } }; ecodeSDK.overwritePropsFnQueueMapSet('WeaReqTop', { fn: (newProps) => { if (!enable || !checkConditions()) return; // 开关打开、条件不满足 overwriteDropMenuDatas(newProps); overwriteButtons(newProps); // runScript(); // 此处可执行代码块 return newProps; }, }); ecodeSDK.overwritePropsFnQueueMapSet('WeaRightMenu', { fn: (newProps) => { if (!enable || !checkConditions()) return; // 开关打开、条件不满足 overwriteRightMenuDatas(newProps); return newProps; }, }); ``` [/hide] ## 优化 发现存在有些右键菜单中的保存按钮item.key并不是BTN_WFSAVE_1,但肯定包含BTN_WFSAVE,故需修改两处,以保证所有保存按钮均被排除: ``` // newProps.dropMenuDatas = dropMenuDatas.filter(item => item.key !== 'BTN_WFSAVE_1'); newProps.dropMenuDatas = dropMenuDatas.filter(item => !item.key.includes('BTN_WFSAVE')); ``` ``` // newProps.datas = datas.filter(item => item.key !== 'BTN_WFSAVE_1'); newProps.datas = datas.filter(item => !item.key.includes('BTN_WFSAVE')); ``` # 3. 效果展示 * 展示功能效果:使用截图或视频展示隐藏按钮功能实现后的效果。 # 4. 遇到挑战 ~~没有挑战难度!~~ * 不假思索的、直接臆想的、肯定绝决的,那个,答复,害了我(也带给了我经验)这就是最大的挑战。 # 5. 总总结结 * 功能回顾:简要回顾“隐藏流程保存按钮”功能的实现。 * 收获:分享在实现过程中所学到的经验和收获。 # 6. 致敬未来 * 优化方向:讨论如何进一步改进和优化功能。 * 新功能展望:探讨未来可能添加的其他功能和改进。 ::: [1]: https://cos.oneszhang.com/usr/uploads/2024/11/4041150684.png --- # [泛微 Ecology9] Html模板样式-碧蓝晴空By丸子 作者:丸子·张 来源:https://blog.oneszhang.com/2023/06/19/35/ Markdown:https://blog.oneszhang.com/2023/06/19/35.md 发布日期:2023-06-19T10:12:00.000Z 更新时间:2023-06-19T10:12:00.000Z 专栏:泛微 E9 标签: 摘要:面向泛微e-cology二次开发,作者参考契约锁流程界面风格,制作并发布了Html模板样式碧蓝晴空V1,适用于高级模式下的流程模板美化。模板包含分组标题色块、字段缩进与行高配色等调整,并附有下载链接,作者也欢迎使用者反馈以共同改进。 # 前言 > 似乎,**泛微e-cology**的Html模板样式维护(高级模式)中默认的样式还算得上简约好看的,但是不知道为什么,总觉得用起来,差点什么东西。机缘巧合,我看到了一个契约锁演示系统的流程界面,感觉蛮清新,于是我决定,做一套!(也算是学习了) 当前发布的碧蓝晴空模板样式为V1版本,如果你感觉有什么地方可以改善,那我们一起来改进! :::tip 那~让我们开始吧!你也可以直接划至最下方点击链接下载~ ::: # 模板展示 废话少说~直接上图! ## 预览图 ![模板预览图][1] ## 编辑图 ![模板编辑页][2] # 模板介绍 #### **当前版本**:V1.0 ##### 2023-03-14 日初始新建 V1.0 - 设置分组名称默认class:flow-group-title - 增加flow-group-title色块样式 - 默认引用filterBorder.css - 默认引用modern_simplicity.css - 所有字段默认缩进为2 - 颜色及行高设定,请见模板 # 下载地址 [file href="https://cos.oneszhang.com/usr/uploads/old/blog_20230619_e9htmlstyle/wanzihtml.zip"]碧蓝晴空模板By丸子[/file] > ⚠️ > > 如果以上链接失效,请及时与我联系~ > [1]: https://cos.oneszhang.com/usr/uploads/old/blog_20230619_e9htmlstyle/wanzihtmlstyle.png [2]: https://cos.oneszhang.com/usr/uploads/old/blog_20230619_e9htmlstyle/wanzihtmlstyleedit.png --- # [泛微 Ecology9] 流程表单前端JS-AI生成之需求描述示例 作者:丸子·张 来源:https://blog.oneszhang.com/2023/06/15/34/ Markdown:https://blog.oneszhang.com/2023/06/15/34.md 发布日期:2023-06-15T01:36:00.000Z 更新时间:2023-06-15T01:36:00.000Z 专栏:泛微 E9 标签: 摘要:面向泛微E9流程表单前端JS的AI生成场景,文章用明细字段赋值主表为例,对比模糊提问与带字段标识、触发事件的精确描述,展示后者能生成更贴合需求的代码。结论是向AI提需求时应尽量使用程序语言化、明确字段和事件,以提升生成效果。 [点击进入→泛微E9流程表单前端JS生成网站](https://oneszhang.com/E9API/) > ✅ **点这里进入丸子小工具:** > > [→→→→→→丸子小工具←←←←←←](https://oneszhang.com/onestool.html) > 示例1 明细字段赋值主表: ------------------------ :::tip[原始问题] 怎么取明细第一行的字段值 ,赋值到主表这个字段上去 ::: ![2023-06-15T01:30:02.png][1] **直接提问你会得到,这样的结果:** ``` // 获取明细表第一行字段的值 var fieldValue = WfForm.getDetailFieldVal("detail_1", 0, "field27555"); // 将值赋值到主表相应字段 WfForm.changeFieldValue("field110", { value: fieldValue }); ``` **但是如果你,这样提问:** > 明细字段值变化触发事件(字段标示:field111): 获取字段值(字段标示:field111_1), 赋值给主表field110 **那你将会得到:** ``` WfForm.bindDetailFieldChangeEvent("field111", function(id, rowIndex, value){ var fieldValue = WfForm.getFieldValue("field111_1"); //获取明细表field111_1字段的值 WfForm.changeFieldValue("field110", {value:fieldValue}); //将值赋给主表field110字段 }); //注:这段代码应当在表单渲染完成后执行,可以放在jQuery(document).ready()中。 ``` > ✅ **示例1结论** > > 所以!请尽量使用程序语言来描述你的需求,以达到最佳生成效果! > [1]: https://oneszhang.com/usr/uploads/2023/06/217612307.png#mirages-width=1270&mirages-height=145&mirages-cdn-type=3 --- # 解决Typecho无法显示Gravatar头像的问题 作者:丸子·张 来源:https://blog.oneszhang.com/2023/01/04/gravatar/ Markdown:https://blog.oneszhang.com/2023/01/04/gravatar.md 发布日期:2023-01-04T11:04:00.000Z 更新时间:2023-01-04T11:04:00.000Z 专栏:前端与脚本 标签: 摘要:针对 Gravatar 在国内被 DNS 污染、导致 Typecho 头像无法显示或加载缓慢的问题,可通过将默认头像源替换为第三方镜像来解决。推荐修改博客根目录的配置文件来指定镜像地址,也可直接修改程序源码中的头像地址;文中列出多个可用镜像,并提到 cravatar 支持 QQ 头像。适用于受此问题困扰的 Typecho 站长。 # 前言 > **Gravatar**(英语:**Globally Recognized Avatar**)是一项用于提供在全球范围内使用的头像服务。只要你在 Gravatar 的服务器上上传了你自己的头像,你便可以在其他任何支持 Gravatar 的博客、论坛等地方使用它。 > 从 2011 年 8 月起,gravatar.com 被部分地区屏蔽,部分用户访问博客时头像可能无法正确显示。2014 年,网站被解封,页面访问正常。约 2014 年 11 月左右,网站再次被屏蔽,部分用户访问博客时头像无法正确显示。现时已被封锁,方式为 DNS 污染。 由此导致 Typecho 无法显示 Gravatar 头像或加载非常缓慢。 > 📌 > > So,修改默认的 Gravatar 头像源为第三方镜像源,即可解决显示问题 > --- # 解决方法 > ✅ **更新提示** > > 2024年4月15日 13:10:53,我已将我自己博客的头像源(`config.inc.php`中的`__TYPECHO_GRAVATAR_PREFIX__`)更换为cravatar,该源直接支持QQ头像,很棒! > > ``` > /** 更换头像源 */ > /**define('__TYPECHO_GRAVATAR_PREFIX__', 'https://dn-qiniu-avatar.qbox.me/avatar/');*/ > define('__TYPECHO_GRAVATAR_PREFIX__', 'https://cravatar.cn/avatar/'); > ``` > ## 方法 1(推荐):修改文件`config.inc.php` **博客网站根目录下找到文件`config.inc.php`,直接在最后加入以下代码:** ``` /** 更换头像源 */ define('__TYPECHO_GRAVATAR_PREFIX__', 'http://dn-qiniu-avatar.qbox.me/avatar/'); ``` 其中`http://dn-qiniu-avatar.qbox.me/avatar/`即为你要设置的镜像源地址 `**推荐源:(点击地址可查看是否存活)**` | 源名称 | 源地址 | |:------:|:------| |七牛(推荐)|https://dn-qiniu-avatar.qbox.me/avatar/| |极客族(推荐)|https://sdn.geekzu.org/avatar/| |九月的风|https://cdn.sep.cc/avatar/| |V2EX|https://cdn.v2ex.com/gravatar/| |Cravatar|https://cravatar.cn/avatar/| |Ihuan|https://gravatar.ihuan.me/avatar/| |loli|https://gravatar.loli.net/avatar/| |WeAvatar|https://weavatar.com/avatar/| ## 方法 2:修改文件`Common.php` **1. 打开网站目录下的`/var/Typecho/Common.php`文件** **2. 找到以下代码(直接搜索部分代码或前往 900~1100 行用你的大眼睛扫描):** ``` /** * 获取gravatar头像地址 * * @param string $mail * @param int $size * @param string $rating * @param string $default * @param bool $isSecure * @return string */ public static function gravatarUrl($mail, $size, $rating, $default, $isSecure = false) { if (defined('__TYPECHO_GRAVATAR_PREFIX__')) { $url = __TYPECHO_GRAVATAR_PREFIX__; } else { $url = $isSecure ? 'https://secure.gravatar.com' : 'http://www.gravatar.com'; $url .= '/avatar/'; } if (!empty($mail)) { $url .= md5(strtolower(trim($mail))); } $url .= '?s=' . $size; $url .= '&r=' . $rating; $url .= '&d=' . $default; return $url; } ``` **3. 修改代码中指向的地址`https://secure.gravatar.com`及 `http://www.gravatar.com`为第三方镜像源。(以上代码块中的第16行)** **例:** ``` ... } else { $url = $isSecure ? 'https://cravatar.cn/' : 'https://gravatar.loli.net'; $url .= '/avatar/'(镜像源文件夹); } ... ``` `**推荐源:**` | 源名称 | 源地址 | |:------:|:------| |七牛(推荐)|https://dn-qiniu-avatar.qbox.me| |极客族(推荐)|https://sdn.geekzu.org| |九月的风|https://cdn.sep.cc| |V2EX|https://cdn.v2ex.com (注意该镜像源的文件夹为/gravatar/)| |Cravatar|https://cravatar.cn| |Ihuan|https://gravatar.ihuan.me| |loli|https://gravatar.loli.net| |WeAvatar|https://weavatar.com| --- # 关于12306火车票订票时选择卧铺票上中下铺位的方法 作者:丸子·张 来源:https://blog.oneszhang.com/2022/12/08/12306/ Markdown:https://blog.oneszhang.com/2022/12/08/12306.md 发布日期:2022-12-08T11:12:00.000Z 更新时间:2022-12-08T11:12:00.000Z 专栏:生活与随笔 标签: 摘要:这篇文章介绍在 12306 官网通过浏览器开发者工具修改卧铺席别下拉框代码,从而自行选择上中下铺的操作方法,需在电脑端先选好乘车人再粘贴代码。作者称经六次测试均符合预期,但文末提示该方法已于 2023 年 12 月失效,仅供读者参考甄别。 > 🚨 **最新尝试失败提示** > > 丸子于2023年12月5日 22:29:59测试,该方法失效。 > **春节将至,春运随之而来,不少的人们会面临买票的问题,很多离家较远的盆友应该会选择购买火车卧铺票,但是每次可能买到的铺位并不尽人意,那么我们订票时如何自己决定铺位呢?** > 经搜索,自 2012 年起便有此方法流传,关于真假众说纷纭,请自行甄别。 > 📌 > > 本方法我已进行 6 次测试,均符合预期结果。 > > ⚠️ > > 本方法需使用**电脑**,无**电脑**请绕行,或联系我帮忙订票(纯属助人为乐) > ## 一、打开 12306 官网并登陆 ## 二、选择车次(以 K1808 为例) ![车次选择](https://cos.oneszhang.com/usr/uploads/old/blog_20221208_12306/checixuanze.png) 选择乘车车次,查看车票剩余数量,点击预订。 > 📌 > > 此处注意:选择车票最好数量为“有”,剩余票中更大概率存在期望铺位 > ## 三、粘贴代码 1. 先选好乘车人,一定要先选好乘车人哦! 2. 右键席别下方“硬卧 (¥***)”选项框,点击“检查”(或“审查元素”)按钮![右键检查](https://cos.oneszhang.com/usr/uploads/old/blog_20221208_12306/jiancha.png) 3. 右键被点亮的代码行(` ``` ![粘贴代码](https://cos.oneszhang.com/usr/uploads/old/blog_20221208_12306/zhantiedaima.png) 粘贴后,点击空白处即可确定。 可以发现左侧出现了铺位选择框,选择自己想要的铺位,之后点击提交订单即可。 ![选择铺位](https://cos.oneszhang.com/usr/uploads/old/blog_20221208_12306/puweixuanze.png) 搞定!#(赞一个) **最后,提前祝大家,新年快乐幸福安康!** 参考文章: [关于火车票如何购买下铺的方法](https://www.coolapk.com/feed/41503437) [我看 12306 火车票定票系统破解选下铺之技术分解!(?真屌丝逆袭:伪技术帖)](https://www.cnblogs.com/luhaibin731/archive/2013/01/07/2849402.html)(分析过程饶有趣,但该作者表示自己没试过!——实践才是检验真理的唯一标准) --- # Windows 11 发布首个预览版!有内味了! 作者:丸子·张 来源:https://blog.oneszhang.com/2021/06/29/16/ Markdown:https://blog.oneszhang.com/2021/06/29/16.md 发布日期:2021-06-29T09:48:00.000Z 更新时间:2021-06-29T09:48:00.000Z 专栏:前端与脚本 标签: 摘要:作者记录收到 Windows 11 首个预览版推送后,为满足 TPM 等升级条件,用检查工具检测并进 BIOS 调整配置,最终顺利完成更新。更新后体验流畅顺滑,与泄露版 21996.1 相比进步明显,并提醒读者对照官方最低配置确认能否升级。 > 📌 > > 温馨提醒!上班时间请不要摸鱼哦! > # 好消息,好消息 今天早上到公司之后,打开电脑,准备进入工作状态。 但是,谁知世事难料#(惊喜)突然一个群亮了,看到群里有人发了个 Windows11 的更新推送截图! 哦豁!我飞速点开了设置,更新检查,果然!#(不出所料)那岂不是要更新一波! ![真的推送更新啦!](https://cos.oneszhang.com/usr/uploads/old/blog_20210629_win11/windows11_gengxin.png) # 下载中... 下载时想起了昨晚我看到的一篇文章:“要想升级 Windows11 电脑必须要有 TPM!”,就去网上下载了检查工具看下是否可以升级 Windows11(大佬就是大佬,嫌弃微软的工具难用,直接自己写了个工具“WhyNotWin11.exe”,牛啊!) 检查工具的链接放在这里啦: [file url="https://cos.oneszhang.com/usr/uploads/old/blog_20210629_win11/WhyNotWin11.exe"]大佬_WhyNotWin11.exe[/file] [file url="https://cos.oneszhang.com/usr/uploads/old/blog_20210629_win11/WindowsPCHealthCheckSetup.msi"]官方_电脑健康状态检查[/file] 打开软件测了一下,哦豁,红光数枚,淦! 于是进 BIOS 挨个调整需要的配置,一切还算顺利,它绿了,它绿了!😏 ![绿了!绿了!](https://cos.oneszhang.com/usr/uploads/old/blog_20210629_win11/win11_lvle.png) # 更新完成 吼吼吼,直接更新完成!舒舒胡胡! 更新挺快,没有之前 Windows10 更新的“青,出于蓝而胜于蓝.....”一系列提示😏 直接完成登陆! ![更新完成](https://cos.oneszhang.com/usr/uploads/old/blog_20210629_win11/win11_zhuomiannew.png) 可以!完美的一塌糊涂! ![激活界面](https://cos.oneszhang.com/usr/uploads/old/blog_20210629_win11/windows11_guanyu.png) 顺便说一句,跟之前泄露版的 21996.1 版本有巨大进步!#(赞一个) 两个字:爽! 六个字:够流畅! 八个字:极度顺滑! 我去官网截了个最低配置的图,快来看看你的电脑能不能升级😏快快搞起啊! ![配置要求](https://cos.oneszhang.com/usr/uploads/old/blog_20210629_win11/win11_peizhi.png) --- # WIN10数字权利激活工具HWIDGen 作者:丸子·张 来源:https://blog.oneszhang.com/2019/06/28/4/ Markdown:https://blog.oneszhang.com/2019/06/28/4.md 发布日期:2019-06-28T15:04:00.000Z 更新时间:2019-06-28T15:04:00.000Z 专栏:前端与脚本 标签: 摘要:HWIDGen 是一款 Windows 10 数字权利激活工具,无需产品密钥即可永久激活;操作时在软件右上角选择 HWID 模式并点击 START,等待提示成功即可。激活前不能关闭系统更新,否则会失败;若失败可改用 KMS38 方式,仍不行则建议重装系统。 - ## 是什么?(不重要!) > HWIDGEN 是一款由国外 Nsane 论坛会员 s1ave77 制作的 Windows 10 数字权利激活工具,这款 Win10 数字权利获取工具,可以自动获取 Windows 10 数字许可证激活,无需产品密钥,以最简单的方式**永久激活**。 **简单来说就是,最方便(永久)激活 win10 工具!** ![][1] 详细了解: ["(Windows 10) Digital License (HWID) KMS38™ Generation - Software Chat - nsane.forums (archive.org)"](https://web.archive.org/web/20181008184352/https://www.nsaneforums.com/topic/312871-windows-10-digital-license-hwid-kms38%E2%84%A2-generation/) #### 什么是数字权利激活 `数字权利激活` Windows 10 的一种激活方法,该方法不需要输入产品密钥。在激活后将激活电脑上的主板,CPU,网卡等硬件的 ID 信息存储在微软官网的服务器中。只要电脑主要硬件不变的情况下,重新安装系统时无需再次输入密钥,系统会在自动连接到微软服务器进行激活。 --- - ## 怎么做?(重点!) 1. 下载 HWIDGen[^ 下载链接作者放在了文章尾部哦~] 2. 首次打开软件,将自动进行配置,不用管!等!(如图) ![][2] 3. 右上角`Work Mode`选项中选择`HWID`,点击左下角`START` (如图) ![][3] 4. 等,等,等~ 5. 成功!(不出意外的情况下~出现这样子的框框~`Successfully`) ![][4] --- #### 注意事项(很是重要!): 1. **激活前,请勿关闭 win10 的更新,否则必将导致激活失败!** 2. 如果使用此方法激活不成功,可以在`Work Mode`选项选择`KMS38`方式进行天数激活(好像是 180 天,emmmmm 忘了) 3. 如果!无论如何都不能激活!请亲手杀了你的系统~(重装就好 emmmmmmm) --- #### 支持的 Windows 10 版本(SKUs):(不重要,不用看,基本都行!) 只要不是装的什么“神奇”的操作系统,没问题。。。 - 家庭版(数字权利/KMS38™) - 家庭单语言版(数字权利/KMS38™) - 专业版(数字权利/KMS38™) - 专业教育版(数字权利/KMS38™) - 专业工作站版(数字权利/KMS38™) - 教育版(数字权利/KMS38™) - 企业版(数字权利/KMS38™) - 企业版 S 2015(数字权利) - 企业版 S 2016(数字权利/KMS38™) - 企业版 S(KMS19™) - ServerStandard(Core)(KMS38™) - ServerDatacenter(Core)(KMS38™) - ServerSolution(Core)(KMS38™) --- - ## 结果呢?(只要 Success 了,不重要!) #### 查看激活状态,以下三种方法都得行: 1. “计算机”——“属性”. 2. “WIN”——“设置”——“更新和安全”——“激活”. 3. 快键键“`WIN`”+“`R`”在打开的运行对话框,在对话框中输入`slmgr.vbs -xpr`命令,确定. ![][5] --- - ## 呐!链接(我传在蓝奏云了) [HWIDGen 55.01 蓝奏云下载链接](https://www.lanzoux.com/i353pgh) PS:如果链接失效,请在下方留言 [4]: https://cos.oneszhang.com/usr/uploads/old/blog_20190628_HWIDGen/success.png [1]: https://cos.oneszhang.com/usr/uploads/old/blog_20190628_HWIDGen/s1ave77.png [5]: https://cos.oneszhang.com/usr/uploads/old/blog_20190628_HWIDGen/finnish.png [3]: https://cos.oneszhang.com/usr/uploads/old/blog_20190628_HWIDGen/workmode.png [2]: https://cos.oneszhang.com/usr/uploads/old/blog_20190628_HWIDGen/starting.png --- # Markdown基础语法(自用) 作者:丸子·张 来源:https://blog.oneszhang.com/2019/06/02/3/ Markdown:https://blog.oneszhang.com/2019/06/02/3.md 发布日期:2019-06-02T14:22:00.000Z 更新时间:2019-06-02T14:22:00.000Z 专栏:前端与脚本 标签: 摘要:作者为适应博客写作整理了一份 Markdown 基础语法笔记,涵盖标题、文字样式、引用、删除线、脚注、列表、表格、图片链接及行内与多行代码等常用写法,并提醒特殊符号后需加空格才能生效。适合刚开始用 Markdown 写博客的读者参考,便于快速上手。 叮~探索到新的领域,网上搜罗了些对Markdown的介绍的文件,感觉Markdown使用起来会很方便,为了更快的适应在博客中使用Markdown来写文章,便熟悉并记录一下Markdown基础语法!如有不足欢迎指正! [notice]编辑时注意,特殊符号后会有“空格”,之后方可达到效果。[/notice] - # 关于标题 ``` # 一级标题 ## 二级标题 ### 三级标题 (以此类推) ``` **效果如下:** # 一级标题 ## 二级标题 ### 三级标题 --- - # 关于文字 ## 斜体加粗 ``` *斜体* **加粗** ***斜体加粗*** ``` **效果如下:** *斜体* **加粗** ***斜体加粗*** ## 引用 ``` > 引用 > 引用 ``` **效果如下:** > 引用 > 引用 ## 输出线 ``` ~~删除线~~ ``` **效果如下:** ~~删除线~~ ## 脚注 ``` 要标注的信息[^标注的详情] ``` **效果如下:** 要标注的信息[^标注的详情] --- - # 关于表 ##无序列表 ``` * 你好 - 你好 (两种方法均可,这里让我想起了SmartArt,哈哈) ``` **效果如下:** * 你好 - 你好 ## 有序列表 ``` 1. 这里是1. 2. 这里是2. 3. 这里是3. ``` **效果如下:** 1. 这里是1. 2. 这里是2. 3. 这里是3. ##表格 ``` | 标题一 | 标题二 | | ------ | ------ | | 内容 | 内容 | | 内容 | 内容 | ``` **效果如下:** | 标题一 | 标题二 | | ------ | ------ | | 内容 | 内容 | | 内容 | 内容 | --- - # 关于图片/链接 ## 图片 ``` ![图片描述(以百度logo图片举例)](https://www.baidu.com/img/baidu_jgylogo3.gif) ``` **效果如下:** ![图片描述(以百度logo图片举例)](https://www.baidu.com/img/baidu_jgylogo3.gif) ## 链接 ``` `[文字](url)` [OnesZhang的博客](oneszhang.com) `单纯链接`(要加`http://`哦) ``` **效果如下:** [OnesZhang的博客](oneszhang.com) [http://oneszhang.com/](http://oneszhang.com/) --- # 关于代码 ## 行内代码 [^注意“ ` ”符号为键盘左上角按键(Esc下方)] ``` `name` `OnesZhang` ``` **效果如下:** `name` `OnesZhang` ## 多行代码 ``` ​``` emmmmm print("Hello, World!") ​``` ``` **效果如下:** ``` emmmmm print("Hello, World!") ```