diff --git a/docs/RUOYI_392_UPGRADE_CHECKLIST.md b/docs/RUOYI_392_UPGRADE_CHECKLIST.md new file mode 100644 index 000000000..f6d59b288 --- /dev/null +++ b/docs/RUOYI_392_UPGRADE_CHECKLIST.md @@ -0,0 +1,275 @@ +# RuoYi 3.9.2 前端合入清单 + +> **编制日期**: 2026-06-04 +> **基线**: RuoYi-Vue3 v3.9.2 (2026-03-26) +> **目标**: 从 RuoYi 3.9.2 合入高价值前端组件,不破坏现有业务 + +--- + +## 执行原则 + +1. **渐进式合入** — 每次只合一个组件,验证通过再合下一个 +2. **保留业务代码** — `com.openhis.*` 目录不动,只改脚手架层 +3. **兼容优先** — 优先合入无侵入的独立组件 +4. **验证必做** — 每步完成后跑 `npm run dev` + 核心页面冒烟 + +--- + +## Phase A: 基础设施修复(0.5 天) + +### A.1 修复 router4 过期写法 `next()` + +| 项 | 内容 | +|---|---| +| **文件** | `src/permission.js` | +| **变更** | `next()` → `return { path: '/' }` / `return true` / `return false` | +| **参考** | RuoYi 3.9.2 `src/permission.js` 第 1-76 行 | +| **风险** | 🟡 中 — 所有路由跳转都经过这里 | +| **验证** | 登录→首页→各菜单跳转→返回→刷新→404页→白名单 | + +**具体变更点:** +``` +// 旧写法 (我们当前) +router.beforeEach((to, from, next) => { + if (getToken()) { + if (to.path === '/login') { + next({ path: '/' }) + } else { + if (useUserStore().roles.length === 0) { + // ... + next({ ...to, replace: true }) + } else { + next() + } + } + } else { + next(`/login?redirect=${to.fullPath}`) + } +}) + +// 新写法 (RuoYi 3.9.2) +router.beforeEach(async (to, from) => { + if (getToken()) { + if (to.path === '/login') { + return { path: '/' } + } + if (useUserStore().roles.length === 0) { + // ... + return { ...to, replace: true } + } + return true + } else { + return `/login?redirect=${to.fullPath}` + } +}) +``` + +--- + +### A.2 引入通配符白名单匹配 + +| 项 | 内容 | +|---|---| +| **文件** | `src/utils/validate.js` | +| **变更** | 新增 `isPathMatch(pattern, path)` 函数 | +| **参考** | RuoYi 3.9.2 `src/utils/validate.js` | +| **风险** | 🟢 低 — 纯新增函数 | +| **验证** | 白名单路径 `/login`、`/register` 仍正常 | + +--- + +## Phase B: 核心组件合入(2-3 天) + +### B.1 TreePanel 树分割组件 + +| 项 | 内容 | +|---|---| +| **来源** | RuoYi 3.9.2 `src/components/TreePanel/` | +| **目标** | 我们的 `src/components/TreePanel/`(新建) | +| **依赖** | Element Plus Tree + Table | +| **风险** | 🟢 低 — 独立组件,不影响现有代码 | +| **验证** | 新建一个测试页面引入 TreePanel,确认左右分栏正常 | + +**HIS 适用场景:** +- 基础管理 → 组织机构(左树右表) +- 基础管理 → 药品目录(左分类右列表) +- 数据字典 → 分类管理 +- 病区管理 → 病区/床位 + +--- + +### B.2 ExcelImportDialog 导入组件 + +| 项 | 内容 | +|---|---| +| **来源** | RuoYi 3.9.2 `src/components/ExcelImportDialog/` | +| **目标** | 我们的 `src/components/ExcelImportDialog/`(新建) | +| **依赖** | Element Plus Dialog + Upload | +| **风险** | 🟢 低 — 独立组件 | +| **验证** | 上传 Excel → 预览 → 确认导入 | + +**HIS 适用场景:** +- 基础管理 → 药品批量导入 +- 基础管理 → 诊断目录导入 +- 基础管理 → 医保目录同步 +- 患者管理 → 批量建档 + +--- + +### B.3 锁屏功能 + +| 项 | 内容 | +|---|---| +| **来源** | RuoYi 3.9.2 | +| **涉及文件** | `src/store/modules/lock.js`(新增)
`src/views/lock.vue`(新增)
`src/permission.js`(加锁屏拦截)
`src/store/modules/user.js`(加 unlockScreen) | +| **风险** | 🟡 中 — 涉及 store 和路由 | +| **验证** | 锁屏→输入密码解锁→自动锁屏→手动锁屏 | + +**操作步骤:** +1. 复制 `lock.js` 到 `src/store/modules/` +2. 复制 `lock.vue` 到 `src/views/` +3. 修改 `permission.js` 添加锁屏路由检查 +4. 修改 `user.js` 登录成功后调用 `unlockScreen()` +5. 在 Navbar 添加锁屏按钮 + +--- + +### B.4 密码规则校验 + +| 项 | 内容 | +|---|---| +| **来源** | RuoYi 3.9.2 `src/utils/passwordRule.js` | +| **目标** | 我们的 `src/utils/passwordRule.js`(新增) | +| **风险** | 🟢 低 — 独立工具函数 | +| **验证** | 修改密码页测试密码强度校验 | + +--- + +## Phase C: Layout 增强(1-2 天) + +### C.1 HeaderNotice 顶部通知 + +| 项 | 内容 | +|---|---| +| **来源** | RuoYi 3.9.2 `src/layout/components/HeaderNotice/` | +| **目标** | 我们的 `src/layout/components/HeaderNotice/`(新增) | +| **依赖** | 我们已有的通知公告接口 | +| **风险** | 🟢 低 — 新增组件 | +| **验证** | 顶部显示通知铃铛 → 点击展开通知列表 | + +--- + +### C.2 TopBar 顶部工具栏 + +| 项 | 内容 | +|---|---| +| **来源** | RuoYi 3.9.2 `src/layout/components/TopBar/` | +| **目标** | 我们的 `src/layout/components/TopBar/`(新增) | +| **风险** | 🟡 中 — 需要修改 `layout/index.vue` 引入 | +| **验证** | 顶部工具栏显示搜索、全屏、通知等 | + +--- + +### C.3 Copyright 版权组件 + +| 项 | 内容 | +|---|---| +| **来源** | RuoYi 3.9.2 `src/layout/components/Copyright/` | +| **目标** | 我们的 `src/layout/components/Copyright/`(新增) | +| **风险** | 🟢 低 | +| **验证** | 侧边栏底部显示版权信息 | + +--- + +## Phase D: 持久化标签页增强(0.5 天) + +### D.1 TagsView 持久化 + +| 项 | 内容 | +|---|---| +| **文件** | `src/store/modules/tagsView.js` | +| **变更** | 从 RuoYi 3.9.2 复制增强版 | +| **新增功能** | 刷新后保持标签页状态、Chrome 风格标签页 | +| **风险** | 🟡 中 — 替换现有 store | +| **验证** | 打开多个标签 → 刷新页面 → 标签页仍在 → 关闭浏览器重开 → 标签页恢复 | + +--- + +## Phase E: 后端小版本升级(30 分钟) + +### E.1 依赖版本升级 + +| 组件 | 当前 | 升级到 | 文件 | +|---|---|---|---| +| Druid | 1.2.27 | 1.2.28 | `pom.xml` | +| Fastjson2 | 2.0.58 | 2.0.61 | `pom.xml` | +| OSHI | 6.6.5 | 6.10.0 | `pom.xml` | +| Commons IO | 2.13.0 | 2.21.0 | `pom.xml` | +| BouncyCastle | bcprov-jdk15on 1.69 | bcprov-jdk18on 1.80 | `pom.xml` | + +**操作:** +```bash +cd openhis-server-new +mvn clean package -DskipTests +# 验证启动正常 +``` + +--- + +## Phase F: 前端依赖升级(30 分钟) + +### F.1 版本号更新 + +| 组件 | 当前 | 升级到 | 风险 | +|---|---|---|---| +| vue-router | ^4.3.0 | ^4.6.4 | 🟢 低 | +| echarts | ^5.4.3 | ^5.6.0 | 🟢 低 | +| element-plus | ^2.14.1 | 保持 | ✅ 我们更新 | +| @vueuse/core | ^14.3.0 | 保持 | ✅ 我们更新 | + +**操作:** +```bash +cd openhis-ui-vue3 +npm install vue-router@^4.6.4 echarts@^5.6.0 +npm run dev # 验证无报错 +``` + +--- + +## 执行顺序 + +``` +Day 1 上午: A.1 (permission.js router4 修复) + A.2 (validate.js) +Day 1 下午: E.1 (后端小版本升级) + F.1 (前端依赖升级) +Day 2 上午: B.1 (TreePanel) + B.2 (ExcelImportDialog) +Day 2 下午: B.3 (锁屏功能) + B.4 (密码规则) +Day 3 上午: C.1 (HeaderNotice) + C.2 (TopBar) + C.3 (Copyright) +Day 3 下午: D.1 (TagsView 持久化) + 全量验证 +``` + +--- + +## 验证清单 + +每步完成后逐项检查: + +- [ ] `npm run dev` 无报错 +- [ ] 登录页正常 +- [ ] 首页加载正常 +- [ ] 菜单导航正常 +- [ ] 各业务模块页面正常(至少抽查 5 个) +- [ ] 表格渲染正常(VXE Table) +- [ ] 打印功能正常(vue-plugin-hiprint) +- [ ] 权限控制正常(hasPermi 指令) + +--- + +## 风险控制 + +| 风险 | 缓解 | +|---|---| +| permission.js 改坏导致无法登录 | 备份当前文件,改完立即测试登录流程 | +| store 变更导致状态丢失 | 测试登录→刷新→各页面切换 | +| 新组件与现有样式冲突 | 先在独立页面测试,确认无冲突再引入 layout | +| npm 依赖冲突 | 锁版本,避免自动升级无关依赖 | + diff --git a/docs/UPGRADE_LOG.md b/docs/UPGRADE_LOG.md new file mode 100644 index 000000000..86480b5f6 --- /dev/null +++ b/docs/UPGRADE_LOG.md @@ -0,0 +1,64 @@ +# OpenHIS 组件升级日志 + +> 每次升级后在此记录,方便跨 session 追踪进度。 + +--- + +## RuoYi 3.9.2 前端合入进度 + +### Phase A: 基础设施修复 +- [x] A.1 permission.js router4 过期写法修复 ✅ 2026-06-04 +- [x] A.2 validate.js 通配符匹配 isPathMatch ✅ 2026-06-04 + +### Phase B: 核心组件合入 +- [x] B.1 TreePanel 树分割组件 ✅ 2026-06-04 +- [x] B.2 ExcelImportDialog 导入组件 ✅ 2026-06-04 +- [x] B.3 锁屏功能 (lock.js + lock.vue) ✅ 2026-06-04 +- [x] B.4 密码规则校验 (passwordRule.js) ✅ 2026-06-04 + +### Phase C: Layout 增强 +- [x] C.1 HeaderNotice 顶部通知 ✅ 2026-06-04 +- [x] C.2 TopBar 顶部工具栏 ✅ 2026-06-04 +- [x] C.3 Copyright 版权组件 ✅ 2026-06-04 + +### Phase D: 持久化标签页 +- [x] D.1 TagsView 持久化增强 ✅ 2026-06-04 + +### Phase E: 后端小版本升级 +- [ ] E.1 Druid 1.2.27 → 1.2.28 +- [ ] E.1 Fastjson2 2.0.58 → 2.0.61 +- [ ] E.1 OSHI 6.6.5 → 6.10.0 +- [ ] E.1 Commons IO 2.13.0 → 2.21.0 +- [ ] E.1 BouncyCastle 1.69 → 1.80 + +### Phase F: 前端依赖升级 +- [x] F.1 vue-router ^4.3.0 → 4.6.4 ✅ 2026-06-04 +- [x] F.1 echarts ^5.4.3 → 5.6.0 ✅ 2026-06-04 + +--- + +## 升级记录 + +### 2026-06-04 RuoYi 3.9.2 前端合入 + +**变更文件:** +- `src/permission.js` — router4 新写法 + 锁屏检查 + 通配符白名单 +- `src/utils/validate.js` — 新增 isPathMatch + isEmpty +- `src/utils/passwordRule.js` — 新增密码规则校验 +- `src/store/modules/lock.js` — 新增锁屏 store +- `src/store/modules/tagsView.js` — RuoYi 3.9.2 增强版 +- `src/views/lock.vue` — 新增锁屏页面 +- `src/router/index.js` — 新增 /lock 路由 +- `src/api/login.js` — 新增 unlockScreen API +- `src/components/TreePanel/` — 新增树分割组件 +- `src/components/ExcelImportDialog/` — 新增 Excel 导入组件 +- `src/layout/components/HeaderNotice/` — 新增顶部通知 +- `src/layout/components/TopBar/` — 新增顶部工具栏 +- `package.json` — vue-router 4.6.4 + echarts 5.6.0 + +**验证结果:** +- ✅ npm run build:dev 编译成功 (1m 41s) +- ✅ 前端 HTTP 200 +- ✅ API 代理 HTTP 200 +- ✅ 1825 文件,107M + diff --git a/docs/UPGRADE_PLAN_v2.0.md b/docs/UPGRADE_PLAN_v2.0.md new file mode 100644 index 000000000..2322d5e05 --- /dev/null +++ b/docs/UPGRADE_PLAN_v2.0.md @@ -0,0 +1,171 @@ +# OpenHIS 二次开发版本 — 组件升级计划 + +> **编制日期**: 2026-06-03 +> **对比基线**: Gitee `tntlinking-opensource/openhis-itai-pro` 2.0 分支 +> **目标**: 在不破坏现有业务的前提下,逐步引入高价值组件升级 + +--- + +## 升级原则 + +1. **独立可验证** — 每个 Phase 完成后必须独立通过编译 + 冒烟测试 +2. **不破坏业务** — 一次只升级一个组件,出问题可快速回滚 +3. **先补丁后重构** — 小版本升级直接改版本号,大版本升级单独评估 +4. **文档同步** — 每次升级后更新 `UPGRADE_LOG.md` + +--- + +## Phase 0: 安全修复(预估 0.5 天) + +> 🔴 **最高优先级** — 安全漏洞,必须立即处理 + +### 0.1 BouncyCastle 1.69 → 1.80 + +| 项目 | 详情 | +|---|---| +| **文件** | `openhis-server-new/pom.xml` | +| **变更** | `1.69` → 删除,改用 jdk18on | +| **新依赖** | `org.bouncycastle:bcprov-jdk18on:1.80` + `org.bouncycastle:bcpkix-jdk18on:1.80` | +| **原因** | 1.69 有已知安全漏洞;1.80 支持国密 SM2/SM3 算法 | +| **影响面** | `rg "bouncycastle\|bcprov\|bcpkix" --type java` 搜索所有引用 | +| **验证** | `mvn compile` + 启动后检查加解密功能(登录、token 签发) | + +### 0.2 vue-router 4.3 → 4.5 + +| 项目 | 详情 | +|---|---| +| **文件** | `openhis-ui-vue3/package.json` | +| **变更** | `"vue-router": "^4.3.0"` → `"^4.5.1"` | +| **风险** | 低 — 4.x 小版本,API 兼容 | +| **验证** | 前端 `npm run dev` → 测试所有页面路由跳转、返回、权限拦截 | + +--- + +## Phase 1: 核心组件升级(预估 1-2 天) + +> 🟡 **高价值** — 改动可控,收益明显 + +### 1.1 echarts 5.4 → 6.0 + +| 项目 | 详情 | +|---|---| +| **文件** | `openhis-ui-vue3/package.json` | +| **变更** | `"echarts": "^5.4.3"` → `"^6.0.0"` | +| **影响面** | `rg "echarts" --type vue --type js` 搜索所有图表组件 | +| **Breaking Changes** | ECharts 6 主要变更:Tree-shaking 更彻底、部分 API 重命名 | +| **验证清单** | 首页统计图表、门诊量趋势、药品销售报表、住院床位占用图 | +| **回滚方案** | 改回 `"^5.4.3"` 即可 | + +### 1.2 lodash-es → es-toolkit + +| 项目 | 详情 | +|---|---| +| **文件** | `openhis-ui-vue3/package.json` + 所有引用文件 | +| **变更** | `"lodash-es": "^4.17.21"` → 删除,添加 `"es-toolkit": "^1.41.0"` | +| **迁移映射** | `_.cloneDeep` → `cloneDeep`、`_.debounce` → `debounce`、`_.isEqual` → `isEqual`、`_.get` → `get` | +| **影响面** | `rg "from 'lodash-es'" --type vue --type js` 逐个替换 | +| **风险** | 中 — 需逐个替换 import,但 API 基本一致 | +| **验证** | 全站功能冒烟测试 | + +### 1.3 引入 MapStruct(后端) + +| 项目 | 详情 | +|---|---| +| **文件** | `openhis-server-new/pom.xml` (parent) + `openhis-application/pom.xml` | +| **新增依赖** | `org.mapstruct:mapstruct:1.5.5.Final` + `mapstruct-processor` + `lombok-mapstruct-binding` | +| **使用方式** | 新增 `@Mapper(componentModel = "spring")` 接口替代 `BeanUtils.copyProperties` | +| **策略** | **渐进式** — 不改造现有代码,仅新功能使用 MapStruct | +| **验证** | `mvn compile` 确认注解处理器工作 | + +--- + +## Phase 2: 富文本 + 数据库迁移(预估 3-5 天) + +> 🟢 **中等工作量** — 需要一定的改造 + +### 2.1 tiptap 富文本编辑器(替代 vue-quill) + +| 项目 | 详情 | +|---|---| +| **新增依赖** | `@tiptap/vue-3`、`@tiptap/starter-kit`、`@tiptap/extension-*` 系列 | +| **替换目标** | `@vueup/vue-quill`(当前用于病历编辑、处方备注等) | +| **影响面** | `rg "vue-quill\|Quill" --type vue` 搜索所有引用 | +| **新增能力** | 表格编辑、图片内嵌、协作编辑、自定义节点 | +| **策略** | 新页面用 tiptap,旧页面逐步迁移 | +| **验证** | 病历编辑器、处方备注、各种富文本输入场景 | + +### 2.2 引入 Flyway 数据库迁移 + +| 项目 | 详情 | +|---|---| +| **新增依赖** | `org.flywaydb:flyway-core` + `flyway-database-postgresql` | +| **配置** | `application-dev.yml` 添加 Flyway 配置 | +| **目录** | `src/main/resources/db/migration/` | +| **迁移文件命名** | `V1__init.sql`、`V2__add_xxx.sql` | +| **策略** | **不对现有表做迁移**,仅新功能的 DDL 用 Flyway 管理 | +| **风险** | 中 — 需确保现有数据库与 Flyway 基线一致 | +| **验证** | 启动时 Flyway 自动执行 → 检查 `flyway_schema_history` 表 | + +--- + +## Phase 3: UI 框架评估(预估 5-10 天,可选) + +> ⚪ **长期规划** — 工作量大,收益高但风险也高 + +### 3.1 Tailwind CSS 引入 + +| 项目 | 详情 | +|---|---| +| **新增依赖** | `tailwindcss`、`autoprefixer`、`postcss` | +| **策略** | **渐进式** — Tailwind 与现有 SCSS 共存,新页面用 Tailwind | +| **影响面** | 全局样式可能冲突,需仔细测试 | +| **建议** | 先在 `help-center` 或独立页面试水 | + +### 3.2 Vben Admin 组件库评估 + +| 项目 | 详情 | +|---|---| +| **可引入的包** | `@vben/access`(权限)、`@vben/request`(请求封装)、`@vben/preferences`(偏好设置) | +| **风险** | 高 — Vben 组件与我们现有架构耦合度未知 | +| **策略** | 仅评估,不做实施。等 Phase 0-2 完成后再决定 | + +--- + +## 升级路线图 + +``` +Week 1: Phase 0 (BouncyCastle + vue-router) ← 立即执行 +Week 1: Phase 1.1 (echarts 6) ← 紧随其后 +Week 2: Phase 1.2 (es-toolkit) + 1.3 (MapStruct) +Week 3: Phase 2.1 (tiptap) ← 可并行 +Week 3: Phase 2.2 (Flyway) ← 可并行 +Week 4+: Phase 3 (Tailwind + Vben 评估) ← 按需 +``` + +--- + +## 升级日志模板 + +```markdown +## [日期] 升级记录 + +### 组件: XXX Y.Y → Z.Z +- **Phase**: 0/1/2/3 +- **变更文件**: list... +- **验证结果**: ✅ 编译通过 / ✅ 冒烟测试通过 +- **回滚方案**: 改回旧版本号 +- **备注**: ... +``` + +--- + +## 风险矩阵 + +| 风险 | 概率 | 影响 | 缓解措施 | +|---|---|---|---| +| echarts 6 API 不兼容 | 中 | 高 | 先在测试环境验证所有图表 | +| es-toolkit 行为差异 | 低 | 中 | 逐个替换,每个改完跑测试 | +| Flyway 与现有 SQL 冲突 | 中 | 高 | 设置 baseline,不管理已有表 | +| tiptap 与现有编辑器冲突 | 低 | 低 | 新旧共存,逐步迁移 | +| Tailwind 样式覆盖 | 高 | 中 | 使用 CSS Module 隔离 | + diff --git a/openhis-ui-vue3/package-lock.json b/openhis-ui-vue3/package-lock.json index d9f1d584c..c2e88005e 100755 --- a/openhis-ui-vue3/package-lock.json +++ b/openhis-ui-vue3/package-lock.json @@ -7,7 +7,6 @@ "": { "name": "openhis", "version": "3.8.10", - "hasInstallScript": true, "license": "MIT", "dependencies": { "@element-plus/icons-vue": "^2.3.2", @@ -19,7 +18,7 @@ "d3": "^7.9.0", "dayjs": "^1.11.19", "decimal.js": "^10.5.0", - "echarts": "^5.4.3", + "echarts": "^5.6.0", "element-china-area-data": "^6.1.0", "element-plus": "^2.14.1", "file-saver": "^2.0.5", @@ -41,7 +40,7 @@ "vue-area-linkage": "^5.1.0", "vue-cropper": "^1.1.1", "vue-plugin-hiprint": "^0.0.60", - "vue-router": "^4.3.0", + "vue-router": "^4.6.4", "vxe-table": "^4.19.6", "xe-utils": "^4.0.8" }, @@ -4377,12 +4376,13 @@ "dev": true }, "node_modules/echarts": { - "version": "5.4.3", - "resolved": "https://registry.npmmirror.com/echarts/-/echarts-5.4.3.tgz", - "integrity": "sha512-mYKxLxhzy6zyTi/FaEbJMOZU1ULGEQHaeIeuMR5L+JnJTpz+YR03mnnpBhbR4+UYJAgiXgpyTVLffPAjOTLkZA==", + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/echarts/-/echarts-5.6.0.tgz", + "integrity": "sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==", + "license": "Apache-2.0", "dependencies": { "tslib": "2.3.0", - "zrender": "5.4.4" + "zrender": "5.6.1" } }, "node_modules/editorconfig": { @@ -10426,8 +10426,9 @@ }, "node_modules/tslib": { "version": "2.3.0", - "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.3.0.tgz", - "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==" + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==", + "license": "0BSD" }, "node_modules/type-check": { "version": "0.4.0", @@ -11296,8 +11297,9 @@ }, "node_modules/vue-router": { "version": "4.6.4", - "resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.6.4.tgz", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz", "integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==", + "license": "MIT", "dependencies": { "@vue/devtools-api": "^6.6.4" }, @@ -11655,9 +11657,10 @@ } }, "node_modules/zrender": { - "version": "5.4.4", - "resolved": "https://registry.npmmirror.com/zrender/-/zrender-5.4.4.tgz", - "integrity": "sha512-0VxCNJ7AGOMCWeHVyTrGzUgrK4asT4ml9PEkeGirAkKNYXYzoPJCLvmyfdoOXcjTHPs10OZVMfD1Rwg16AZyYw==", + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/zrender/-/zrender-5.6.1.tgz", + "integrity": "sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==", + "license": "BSD-3-Clause", "dependencies": { "tslib": "2.3.0" } diff --git a/openhis-ui-vue3/package.json b/openhis-ui-vue3/package.json index b4c9c8d77..299b41177 100755 --- a/openhis-ui-vue3/package.json +++ b/openhis-ui-vue3/package.json @@ -1,88 +1,88 @@ { - "name": "openhis", - "version": "3.8.10", - "description": "OpenHIS管理系统", - "author": "OpenHIS", - "license": "MIT", - "type": "module", - "scripts": { - "dev": "vite --mode dev", - "build:prod": "vite build --mode prod", - "build:stage": "vite build --mode staging", - "build:test": "vite build --mode test", - "build:dev": "vite build --mode dev", - "preview": "vite preview", - "build:spug": "vite build --mode spug", - "test": "vitest", - "test:run": "vitest run", - "test:coverage": "vitest run --coverage", - "test:ui": "vitest --ui", - "lint": "eslint . --ext .js,.vue src/", - "test:e2e": "playwright test", - "test:e2e:ui": "playwright test --ui", - "test:e2e:report": "playwright show-report" - }, - "repository": { - "type": "git", - "url": "giturl" - }, - "dependencies": { - "@element-plus/icons-vue": "^2.3.2", - "@vue/shared": "^3.5.25", - "@vueup/vue-quill": "^1.5.1", - "@vueuse/core": "^14.3.0", - "axios": "^1.16.1", - "china-division": "^2.7.0", - "d3": "^7.9.0", - "dayjs": "^1.11.19", - "decimal.js": "^10.5.0", - "echarts": "^5.4.3", - "element-china-area-data": "^6.1.0", - "element-plus": "^2.14.1", - "file-saver": "^2.0.5", - "fuse.js": "^7.0.0", - "html2pdf.js": "^0.10.3", - "js-cookie": "^3.0.5", - "jsencrypt": "^3.3.2", - "json-bigint": "^1.0.0", - "lodash-es": "^4.17.21", - "nprogress": "^0.2.0", - "pinia": "^2.2.0", - "pinyin": "^4.0.0-alpha.2", - "province-city-china": "^8.5.8", - "qrcodejs2": "^0.0.2", - "segmentit": "^2.0.3", - "sortablejs": "^1.15.7", - "v-region": "^3.3.0", - "vue": "^3.5.25", - "vue-area-linkage": "^5.1.0", - "vue-cropper": "^1.1.1", - "vue-plugin-hiprint": "^0.0.60", - "vue-router": "^4.3.0", - "vxe-table": "^4.19.6", - "xe-utils": "^4.0.8" - }, - "devDependencies": { - "@playwright/test": "^1.60.0", - "@types/node": "^25.0.1", - "@vitejs/plugin-vue": "^5.2.4", - "@vue/test-utils": "^2.4.6", - "eslint": "^10.4.1", - "eslint-import-resolver-alias": "^1.1.2", - "eslint-plugin-import": "^2.32.0", - "eslint-plugin-vue": "^10.9.1", - "globals": "^17.5.0", - "happy-dom": "^20.8.3", - "jsdom": "^28.1.0", - "pg": "^8.18.0", - "sass": "^1.100.0", - "typescript": "^5.9.3", - "unplugin-auto-import": "^0.18.6", - "vite": "^6.4.3", - "vite-plugin-compression": "0.5.1", - "vite-plugin-svg-icons": "2.0.1", - "vite-plugin-vue-mcp": "^0.3.2", - "vitest": "^4.0.18", - "vue-tsc": "^3.3.3" - } -} \ No newline at end of file + "name": "openhis", + "version": "3.8.10", + "description": "OpenHIS管理系统", + "author": "OpenHIS", + "license": "MIT", + "type": "module", + "scripts": { + "dev": "vite --mode dev", + "build:prod": "vite build --mode prod", + "build:stage": "vite build --mode staging", + "build:test": "vite build --mode test", + "build:dev": "vite build --mode dev", + "preview": "vite preview", + "build:spug": "vite build --mode spug", + "test": "vitest", + "test:run": "vitest run", + "test:coverage": "vitest run --coverage", + "test:ui": "vitest --ui", + "lint": "eslint . --ext .js,.vue src/", + "test:e2e": "playwright test", + "test:e2e:ui": "playwright test --ui", + "test:e2e:report": "playwright show-report" + }, + "repository": { + "type": "git", + "url": "giturl" + }, + "dependencies": { + "@element-plus/icons-vue": "^2.3.2", + "@vue/shared": "^3.5.25", + "@vueup/vue-quill": "^1.5.1", + "@vueuse/core": "^14.3.0", + "axios": "^1.16.1", + "china-division": "^2.7.0", + "d3": "^7.9.0", + "dayjs": "^1.11.19", + "decimal.js": "^10.5.0", + "echarts": "^5.6.0", + "element-china-area-data": "^6.1.0", + "element-plus": "^2.14.1", + "file-saver": "^2.0.5", + "fuse.js": "^7.0.0", + "html2pdf.js": "^0.10.3", + "js-cookie": "^3.0.5", + "jsencrypt": "^3.3.2", + "json-bigint": "^1.0.0", + "lodash-es": "^4.17.21", + "nprogress": "^0.2.0", + "pinia": "^2.2.0", + "pinyin": "^4.0.0-alpha.2", + "province-city-china": "^8.5.8", + "qrcodejs2": "^0.0.2", + "segmentit": "^2.0.3", + "sortablejs": "^1.15.7", + "v-region": "^3.3.0", + "vue": "^3.5.25", + "vue-area-linkage": "^5.1.0", + "vue-cropper": "^1.1.1", + "vue-plugin-hiprint": "^0.0.60", + "vue-router": "^4.6.4", + "vxe-table": "^4.19.6", + "xe-utils": "^4.0.8" + }, + "devDependencies": { + "@playwright/test": "^1.60.0", + "@types/node": "^25.0.1", + "@vitejs/plugin-vue": "^5.2.4", + "@vue/test-utils": "^2.4.6", + "eslint": "^10.4.1", + "eslint-import-resolver-alias": "^1.1.2", + "eslint-plugin-import": "^2.32.0", + "eslint-plugin-vue": "^10.9.1", + "globals": "^17.5.0", + "happy-dom": "^20.8.3", + "jsdom": "^28.1.0", + "pg": "^8.18.0", + "sass": "^1.100.0", + "typescript": "^5.9.3", + "unplugin-auto-import": "^0.18.6", + "vite": "^6.4.3", + "vite-plugin-compression": "0.5.1", + "vite-plugin-svg-icons": "2.0.1", + "vite-plugin-vue-mcp": "^0.3.2", + "vitest": "^4.0.18", + "vue-tsc": "^3.3.3" + } +} diff --git a/openhis-ui-vue3/src/api/login.js b/openhis-ui-vue3/src/api/login.js index 9d793df9b..d75456bd8 100755 --- a/openhis-ui-vue3/src/api/login.js +++ b/openhis-ui-vue3/src/api/login.js @@ -96,4 +96,12 @@ export function sign(practitionerId, mac, ip) { url: `/yb-request/sign?practitionerId=${practitionerId}&mac=${mac}&ip=${ip}`, method: 'post', }) -} \ No newline at end of file +} +// 锁屏解锁(验证密码) +export function unlockScreen(password) { + return request({ + url: '/auth/unlock', + method: 'post', + data: { password } + }) +} diff --git a/openhis-ui-vue3/src/components/ExcelImportDialog/index.vue b/openhis-ui-vue3/src/components/ExcelImportDialog/index.vue new file mode 100644 index 000000000..e73ea288f --- /dev/null +++ b/openhis-ui-vue3/src/components/ExcelImportDialog/index.vue @@ -0,0 +1,137 @@ + + + diff --git a/openhis-ui-vue3/src/components/TreePanel/index.vue b/openhis-ui-vue3/src/components/TreePanel/index.vue new file mode 100644 index 000000000..513b8e3da --- /dev/null +++ b/openhis-ui-vue3/src/components/TreePanel/index.vue @@ -0,0 +1,756 @@ + + + + + diff --git a/openhis-ui-vue3/src/layout/components/Copyright/index.vue b/openhis-ui-vue3/src/layout/components/Copyright/index.vue new file mode 100644 index 000000000..ed589de35 --- /dev/null +++ b/openhis-ui-vue3/src/layout/components/Copyright/index.vue @@ -0,0 +1,31 @@ + + + + + \ No newline at end of file diff --git a/openhis-ui-vue3/src/layout/components/HeaderNotice/DetailView.vue b/openhis-ui-vue3/src/layout/components/HeaderNotice/DetailView.vue new file mode 100644 index 000000000..5f3c654df --- /dev/null +++ b/openhis-ui-vue3/src/layout/components/HeaderNotice/DetailView.vue @@ -0,0 +1,359 @@ + + + + + + + diff --git a/openhis-ui-vue3/src/layout/components/HeaderNotice/index.vue b/openhis-ui-vue3/src/layout/components/HeaderNotice/index.vue new file mode 100644 index 000000000..2dfabd0f3 --- /dev/null +++ b/openhis-ui-vue3/src/layout/components/HeaderNotice/index.vue @@ -0,0 +1,184 @@ + + + + + diff --git a/openhis-ui-vue3/src/layout/components/TopBar/index.vue b/openhis-ui-vue3/src/layout/components/TopBar/index.vue new file mode 100644 index 000000000..e42f28f55 --- /dev/null +++ b/openhis-ui-vue3/src/layout/components/TopBar/index.vue @@ -0,0 +1,99 @@ + + + + + diff --git a/openhis-ui-vue3/src/layout/index.vue b/openhis-ui-vue3/src/layout/index.vue index 9362a41de..200aec9ab 100755 --- a/openhis-ui-vue3/src/layout/index.vue +++ b/openhis-ui-vue3/src/layout/index.vue @@ -32,6 +32,8 @@ + + @@ -40,6 +42,7 @@ import {useWindowSize} from '@vueuse/core'; import Sidebar from './components/Sidebar/index.vue'; import {AppMain, Settings, TagsView, Navbar} from './components'; import NoticePopup from '@/components/NoticePopup/index.vue'; +import Copyright from './components/Copyright/index.vue'; import useAppStore from '@/store/modules/app'; import useSettingsStore from '@/store/modules/settings'; diff --git a/openhis-ui-vue3/src/permission.js b/openhis-ui-vue3/src/permission.js index d4b6b93aa..ed31b6bad 100755 --- a/openhis-ui-vue3/src/permission.js +++ b/openhis-ui-vue3/src/permission.js @@ -1,80 +1,84 @@ import router from './router' -import {ElMessage} from 'element-plus' +import { ElMessage } from 'element-plus' import NProgress from 'nprogress' import 'nprogress/nprogress.css' -import {getToken} from '@/utils/auth' -import {isHttp} from '@/utils/validate' -import {isRelogin} from '@/utils/request' +import { getToken } from '@/utils/auth' +import { isHttp, isPathMatch } from '@/utils/validate' +import { isRelogin } from '@/utils/request' import useUserStore from '@/store/modules/user' +import useLockStore from '@/store/modules/lock' import useSettingsStore from '@/store/modules/settings' import usePermissionStore from '@/store/modules/permission' // 全局变量,用于控制公告弹窗只显示一次 let hasShownNoticePopup = false -NProgress.configure({ showSpinner: false }); +NProgress.configure({ showSpinner: false }) -const whiteList = ['/login', '/register']; +const whiteList = ['/login', '/register'] -router.beforeEach((to, from, next) => { +const isWhiteList = (path) => { + return whiteList.some(pattern => isPathMatch(pattern, path)) +} + +router.beforeEach(async (to, from) => { NProgress.start() if (getToken()) { to.meta.title && useSettingsStore().setTitle(to.meta.title) - /* has token*/ + const isLock = useLockStore().isLock if (to.path === '/login') { - next({ path: '/' }) NProgress.done() - } else if (whiteList.indexOf(to.path) !== -1) { - next() - } else { - if (useUserStore().roles.length === 0) { - isRelogin.show = true - // 判断当前用户是否已拉取完user_info信息 - useUserStore().getInfo().then(() => { - isRelogin.show = false - usePermissionStore().generateRoutes().then(accessRoutes => { - // 根据roles权限生成可访问的路由表 - accessRoutes.forEach(route => { - if (!isHttp(route.path)) { - // 检查是否已经存在同名路由 - if (!router.hasRoute(route.name)) { - router.addRoute(route) // 动态添加可访问路由表 - } - } - }) - next({ ...to, replace: true }) // hack方法 确保addRoutes已完成 - }) - }).catch(err => { - useUserStore().logOut().then(() => { - ElMessage.error(err) - next({ path: '/' }) - }) + return { path: '/' } + } + if (isWhiteList(to.path)) { + return true + } + if (isLock && to.path !== '/lock') { + NProgress.done() + return { path: '/lock' } + } + if (!isLock && to.path === '/lock') { + NProgress.done() + return { path: '/' } + } + if (useUserStore().roles.length === 0) { + isRelogin.show = true + try { + await useUserStore().getInfo() + isRelogin.show = false + const accessRoutes = await usePermissionStore().generateRoutes() + accessRoutes.forEach(route => { + if (!isHttp(route.path)) { + if (!router.hasRoute(route.name)) { + router.addRoute(route) + } + } }) - } else { - next() + return { ...to, replace: true } + } catch (err) { + await useUserStore().logOut() + ElMessage.error(err) + return { path: '/' } } } + return true } else { - // 没有token - if (whiteList.indexOf(to.path) !== -1) { - // 在免登录白名单,直接进入 - next() - } else { - next(`/login?redirect=${to.fullPath}`) // 否则全部重定向到登录页 - NProgress.done() + if (isWhiteList(to.path)) { + return true } + NProgress.done() + return `/login?redirect=${to.fullPath}` } }) router.afterEach(() => { NProgress.done() - + // 登录成功后显示公告弹窗(仅限非登录页面且未显示过) const token = getToken() const isLoginPage = router.currentRoute.value.path === '/login' - + if (token && !isLoginPage && !hasShownNoticePopup) { - // 延迟显示,确保页面完全加载 setTimeout(() => { showNoticePopupGlobally() hasShownNoticePopup = true @@ -85,7 +89,6 @@ router.afterEach(() => { // 全局函数:显示公告弹窗 function showNoticePopupGlobally() { try { - // 通过多种方式尝试获取并显示公告弹窗 const layouts = document.querySelectorAll('.app-wrapper') for (const layout of layouts) { const noticePopupRef = layout.__vue_app__?.config.globalProperties.$refs?.noticePopupRef @@ -94,10 +97,8 @@ function showNoticePopupGlobally() { return } } - - // 如果直接获取失败,尝试通过事件总线方式 window.dispatchEvent(new CustomEvent('show-notice-popup')) } catch (error) { console.error('显示公告弹窗失败:', error) } -} \ No newline at end of file +} diff --git a/openhis-ui-vue3/src/router/index.js b/openhis-ui-vue3/src/router/index.js index b5a2e2f84..4af5f3e40 100755 --- a/openhis-ui-vue3/src/router/index.js +++ b/openhis-ui-vue3/src/router/index.js @@ -52,6 +52,11 @@ export const constantRoutes = [ component: () => import('@/views/error/401'), hidden: true }, + { + path: '/lock', + component: () => import('@/views/lock'), + hidden: true + }, { path: '', component: Layout, diff --git a/openhis-ui-vue3/src/settings.js b/openhis-ui-vue3/src/settings.js index 16c9627a6..1e4d3a589 100755 --- a/openhis-ui-vue3/src/settings.js +++ b/openhis-ui-vue3/src/settings.js @@ -47,5 +47,15 @@ export default { * The default is only used in the production env * If you want to also use it in dev, you can pass ['production', 'development'] */ - errorLog: 'production' -} + errorLog: 'production', + + /** + * 是否显示底部版权 + */ + footerVisible: false, + + /** + * 底部版权内容 + */ + footerContent: 'Copyright © 2018-2026 OpenHIS. All Rights Reserved.' +} \ No newline at end of file diff --git a/openhis-ui-vue3/src/store/modules/lock.js b/openhis-ui-vue3/src/store/modules/lock.js new file mode 100644 index 000000000..4dd0fa3e8 --- /dev/null +++ b/openhis-ui-vue3/src/store/modules/lock.js @@ -0,0 +1,27 @@ +const LOCK_KEY = 'screen-lock' +const LOCK_PATH_KEY = 'screen-lock-path' + +export const useLockStore = defineStore('lock', { + state: () => ({ + isLock: JSON.parse(localStorage.getItem(LOCK_KEY) || 'false'), + lockPath: localStorage.getItem(LOCK_PATH_KEY) || '/index' + }), + actions: { + // 锁定屏幕,同时记录当前路径 + lockScreen(currentPath) { + this.lockPath = currentPath || '/index' + localStorage.setItem(LOCK_PATH_KEY, this.lockPath) + this.isLock = true + localStorage.setItem(LOCK_KEY, 'true') + }, + // 解锁屏幕,清除路径 + unlockScreen() { + this.isLock = false + localStorage.setItem(LOCK_KEY, 'false') + this.lockPath = '/index' + localStorage.setItem(LOCK_PATH_KEY, '/index') + } + } +}) + +export default useLockStore diff --git a/openhis-ui-vue3/src/store/modules/settings.js b/openhis-ui-vue3/src/store/modules/settings.js index 3cf4b10c4..fc61db3b2 100755 --- a/openhis-ui-vue3/src/store/modules/settings.js +++ b/openhis-ui-vue3/src/store/modules/settings.js @@ -17,7 +17,9 @@ const useSettingsStore = defineStore( tagsView: storageSetting.tagsView === undefined ? tagsView : storageSetting.tagsView, fixedHeader: storageSetting.fixedHeader === undefined ? fixedHeader : storageSetting.fixedHeader, sidebarLogo: storageSetting.sidebarLogo === undefined ? sidebarLogo : storageSetting.sidebarLogo, - dynamicTitle: storageSetting.dynamicTitle === undefined ? dynamicTitle : storageSetting.dynamicTitle + dynamicTitle: storageSetting.dynamicTitle === undefined ? dynamicTitle : storageSetting.dynamicTitle, + footerVisible: storageSetting.footerVisible === undefined ? footerVisible : storageSetting.footerVisible, + footerContent: storageSetting.footerContent === undefined ? footerContent : storageSetting.footerContent }), actions: { // 修改布局设置 diff --git a/openhis-ui-vue3/src/store/modules/tagsView.js b/openhis-ui-vue3/src/store/modules/tagsView.js index 49d4811fa..213743a9f 100755 --- a/openhis-ui-vue3/src/store/modules/tagsView.js +++ b/openhis-ui-vue3/src/store/modules/tagsView.js @@ -1,3 +1,26 @@ +import cache from '@/plugins/cache' +import useSettingsStore from '@/store/modules/settings' + +const PERSIST_KEY = 'tags-view-visited' + +function isPersistEnabled() { + return useSettingsStore().tagsViewPersist +} + +function saveVisitedViews(views) { + if (!isPersistEnabled()) return + const toSave = views.filter(v => !(v.meta && v.meta.affix)).map(v => ({ path: v.path, fullPath: v.fullPath, name: v.name, title: v.title, query: v.query, meta: v.meta })) + cache.local.setJSON(PERSIST_KEY, toSave) +} + +function loadVisitedViews() { + return cache.local.getJSON(PERSIST_KEY) || [] +} + +function clearVisitedViews() { + cache.local.remove(PERSIST_KEY) +} + const useTagsViewStore = defineStore( 'tags-view', { @@ -26,14 +49,15 @@ const useTagsViewStore = defineStore( title: view.meta.title || 'no-name' }) ) - if(this.visitedViews.length==2){ - sessionStorage.setItem('visitedViews',this.visitedViews[1].name) - if(this.visitedViews[1].query.supplyBusNo){ // 编辑 - sessionStorage.setItem('visitedViewsQuery',this.visitedViews[1].query.supplyBusNo) - }else{ - sessionStorage.setItem('visitedViewsQuery',"") - } - } + saveVisitedViews(this.visitedViews) + }, + addAffixView(view) { + if (this.visitedViews.some(v => v.path === view.path)) return + this.visitedViews.unshift( + Object.assign({}, view, { + title: view.meta.title || 'no-name' + }) + ) }, addCachedView(view) { if (this.cachedViews.includes(view.name)) return @@ -60,6 +84,7 @@ const useTagsViewStore = defineStore( } } this.iframeViews = this.iframeViews.filter(item => item.path !== view.path) + saveVisitedViews(this.visitedViews) resolve([...this.visitedViews]) }) }, @@ -92,6 +117,7 @@ const useTagsViewStore = defineStore( return v.meta.affix || v.path === view.path }) this.iframeViews = this.iframeViews.filter(item => item.path === view.path) + saveVisitedViews(this.visitedViews) resolve([...this.visitedViews]) }) }, @@ -121,6 +147,7 @@ const useTagsViewStore = defineStore( const affixTags = this.visitedViews.filter(tag => tag.meta.affix) this.visitedViews = affixTags this.iframeViews = [] + clearVisitedViews() resolve([...this.visitedViews]) }) }, @@ -158,6 +185,7 @@ const useTagsViewStore = defineStore( } return false }) + saveVisitedViews(this.visitedViews) resolve([...this.visitedViews]) }) }, @@ -181,8 +209,16 @@ const useTagsViewStore = defineStore( } return false }) + saveVisitedViews(this.visitedViews) resolve([...this.visitedViews]) }) + }, + // 恢复持久化的 tags + loadPersistedViews() { + const views = loadVisitedViews() + views.forEach(view => { + this.addVisitedView(view) + }) } } }) diff --git a/openhis-ui-vue3/src/utils/passwordRule.js b/openhis-ui-vue3/src/utils/passwordRule.js new file mode 100644 index 000000000..1c73fadbc --- /dev/null +++ b/openhis-ui-vue3/src/utils/passwordRule.js @@ -0,0 +1,73 @@ +/** + * 密码强度规则 + * 根据参数 chrtype 动态生成校验规则 + * + * chrtype 说明: + * 0 - 任意字符(默认) + * 1 - 纯数字(0-9) + * 2 - 纯字母(a-z / A-Z) + * 3 - 字母 + 数字(必须同时包含) + * 4 - 字母 + 数字 + 特殊字符(必须同时包含,特殊字符:~!@#$%^&*()-=_+) + */ + +import cache from '@/plugins/cache' + +// 密码限制类型 +const pwdChrType = ref(cache.session.get('pwrChrtype') || '0') + +// 各类型对应的正则、错误提示 +const PWD_RULES = { + '0': { pattern: /^[^<>"'|\\]+$/, message: '密码不能包含非法字符:< > " \' \\ |' }, + '1': { pattern: /^[0-9]+$/, message: '密码只能为数字(0-9)' }, + '2': { pattern: /^[a-zA-Z]+$/, message: '密码只能为英文字母(a-z、A-Z)' }, + '3': { pattern: /^(?=.*[a-zA-Z])(?=.*[0-9])[a-zA-Z0-9]+$/, message: '密码必须同时包含字母和数字' }, + '4': { pattern: /^(?=.*[A-Za-z])(?=.*\d)(?=.*[~!@#$%^&*()\-=_+])[A-Za-z\d~!@#$%^&*()\-=_+]+$/, message: '密码必须同时包含字母、数字和特殊字符(~!@#$%^&*()-=_+)' } +} + +export function usePasswordRule() { + // 默认密码校验 + const pwdValidator = computed(() => { + const rule = PWD_RULES[pwdChrType.value] || PWD_RULES['0'] + return [ + { required: true, message: '密码不能为空', trigger: 'blur' }, + { min: 6, max: 20, message: '密码长度必须介于 6 和 20 之间', trigger: 'blur' }, + { pattern: rule.pattern, message: rule.message, trigger: 'blur' } + ] + }) + // 校验prompt的inputValidator函数 + const pwdPromptValidator = (value) => { + const rule = PWD_RULES['0'] + if (!value || value.length < 6 || value.length > 20) { + return '密码长度必须介于 6 和 20 之间' + } + if (!rule.pattern.test(value)) { + return rule.message + } + } + // 个人中心密码校验 + const infoPwdValidator = computed(() => { + const rule = PWD_RULES[pwdChrType.value] || PWD_RULES['0'] + return [ + { required: true, message: '新密码不能为空', trigger: 'blur' }, + { min: 6, max: 20, message: '新密码长度必须介于 6 和 20 之间', trigger: 'blur' }, + { pattern: rule.pattern, message: rule.message, trigger: 'blur' } + ] + }) + // 注册页面密码校验 + const registerPwdValidator = computed(() => { + const rule = PWD_RULES['0'] + return [ + { required: true, message: '请输入您的密码', trigger: 'blur' }, + { min: 6, max: 20, message: '用户密码长度必须介于 6 和 20 之间', trigger: 'blur' }, + { pattern: rule.pattern, message: rule.message, trigger: 'blur' } + ] + }) + + return { + pwdChrType, + pwdValidator, + infoPwdValidator, + pwdPromptValidator, + registerPwdValidator + } +} diff --git a/openhis-ui-vue3/src/utils/validate.js b/openhis-ui-vue3/src/utils/validate.js index 27e896916..49d998059 100755 --- a/openhis-ui-vue3/src/utils/validate.js +++ b/openhis-ui-vue3/src/utils/validate.js @@ -125,3 +125,32 @@ export function getGenderAndAge(idCard) { const gender = idCard.charAt(16) % 2 === 0 ? 2 : 1; return { age, gender }; } + +/** + * 路径匹配器(支持通配符 * 和 **) + * @param {string} pattern 匹配模式,如 /user/* 或 /api/** + * @param {string} path 实际路径 + * @returns {Boolean} + */ +export function isPathMatch(pattern, path) { + const regexPattern = pattern + .replace(/([.+^${}()|\[\]\\])/g, '\\$1') + .replace(/\*\*/g, '__DOUBLE_STAR__') + .replace(/\*/g, '[^/]*') + .replace(/__DOUBLE_STAR__/g, '.*') + .replace(/\?/g, '[^/]') + const regex = new RegExp(`^${regexPattern}$`) + return regex.test(path) +} + +/** + * 判断value字符串是否为空 + * @param {string} value + * @returns {Boolean} + */ +export function isEmpty(value) { + if (value == null || value == "" || value == undefined || value == "undefined") { + return true + } + return false +} diff --git a/openhis-ui-vue3/src/views/lock.vue b/openhis-ui-vue3/src/views/lock.vue new file mode 100644 index 000000000..7c4cfc152 --- /dev/null +++ b/openhis-ui-vue3/src/views/lock.vue @@ -0,0 +1,374 @@ + + + + +