20 changed files with 2171 additions and 0 deletions
@ -0,0 +1,379 @@ |
|||
# EnforcementCode-NOS-2.0 项目说明(非现场执法) |
|||
|
|||
## 背景与术语约定 |
|||
|
|||
| 术语 | 含义 | 维护团队 / 仓库 | |
|||
| ---- | ---- | ----------------- | |
|||
| **现场执法** | 执法人员到现场对企业进行检查、处罚等业务,**当前团队正在开发的执法码主体项目** | 现有团队 · 现场执法主仓库 | |
|||
| **非现场执法(NOS, Non On-Site)** | 不走到现场也能完成的执法监管场景(企业自查、远程抽查、数据上报等),**新进团队准备开发的业务**;本仓库即为此部分代码 | 新进团队 · 本仓库(EnforcementCode-NOS-2.0) | |
|||
|
|||
### 为什么单独建仓库开发 |
|||
|
|||
* 现场执法仓库由现有团队持续迭代。 |
|||
* 先在独立仓库中完成后端模块与前端 H5 工程,可以避免两边代码冲突、快速迭代。初步开发完成后,再合并入现场执法的代码库里边。 |
|||
* 后端在开发阶段复用现场执法的启动工程([`zdxt-admin-web-server`](zdxt-web-server/zdxt-admin-web-server))、公共 jar(`zdxt-common-*` / `zdxt-system`)以及部分现场执法的表(以视图方式引入),保证技术栈、权限体系、Sa-Token 认证与主项目一致。 |
|||
|
|||
### 最终合入路径 |
|||
|
|||
非现场执法开发完成后,**代码与数据库均需合入现场执法**: |
|||
|
|||
* **代码**:[`zdxt-modules/zdxt-enforcement-nos`](zdxt-modules/zdxt-enforcement-nos) 主体拷入现场执法后端仓库的 `zdxt-modules/`下;三个前端工程按照企业端,执法端,监督端,手工合并入拷入现场执法的的三端下。 |
|||
* **数据库**:开发阶段使用 227 上的 `zfjd2.0-nos` 独立库(以视图方式复用现场执法部分表);**部署时合并为一个库**,现场与非现场使用同一个数据库。为避免表名冲突,本仓库所有新增表以 `nos_` 前缀区分。 |
|||
|
|||
仓库定位:在不污染现场执法主仓库的前提下,先行独立完成非现场执法相关的后端模块与三个前端 H5 工程的开发,再统一合并。 |
|||
|
|||
--- |
|||
|
|||
## 一、代码仓库 |
|||
|
|||
``` |
|||
http://146.56.199.25:3000/zhuibobo/EnforcementCode-NOS-2.0.git |
|||
``` |
|||
|
|||
--- |
|||
|
|||
## 二、第三方 Jar 安装 |
|||
|
|||
* 使用 [`Libs/install-to-maven.ps1`](Libs/install-to-maven.ps1) 将 `Libs/` 目录下的 `zdxt-common-*.jar`、`zdxt-system-*.jar` 等 jar 包注册到本地 Maven 仓库。 |
|||
* 后续如有 jar 包更新(版本不变),**重新执行脚本即可**,执行脚本时会直接覆盖本地仓库中的同版本 jar。覆盖后建议在 IDE 中刷新 Maven(IDEA:右键项目 → Maven → Reload Project),确保 IDE 加载到最新的 jar。如果无效清理idea工具的项目缓存。 |
|||
|
|||
### Windows 下使用方法 |
|||
|
|||
**前置条件**:已安装 Maven,且 `mvn` 命令已加入系统 PATH。 |
|||
|
|||
1. 打开 **PowerShell**(Win + R → 输入 `powershell` → 回车)。 |
|||
2. 进入 `Libs` 目录并执行脚本: |
|||
```powershell |
|||
cd D:\EnforcementCodeProject\CodeV2\EnforcementCode-NOS-2.0\Libs |
|||
.\install-to-maven.ps1 |
|||
``` |
|||
3. 如果提示 **"无法加载文件...因为在此系统上禁止运行脚本"**,先执行以下命令放开执行策略,再重新运行脚本: |
|||
```powershell |
|||
Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned |
|||
``` |
|||
4. 脚本会逐个安装 jar,安装成功显示绿色 `OK`,失败显示红色 `FAILED`,全部完成后输出 `All done.`。 |
|||
|
|||
--- |
|||
|
|||
## 三、数据库 |
|||
|
|||
### 1. 开发数据库 |
|||
|
|||
* 开发阶段统一使用 `227` 服务器上的数据库 `zfjd2.0-nos` 作为开发库。 |
|||
* 通过 **视图(VIEW)** 的方式从现场执法库引入以下表,仅做数据读取的功能,如需要修改其中的表,或者添加新的表,与现场执法团队进行讨论: |
|||
``` |
|||
efcode_virtual_unit |
|||
srz_dept |
|||
srz_user |
|||
sys_client |
|||
sys_dept |
|||
sys_menu |
|||
sys_post |
|||
sys_role |
|||
sys_user |
|||
sys_user_post |
|||
sys_user_role |
|||
``` |
|||
* 如果现场执法团队需要使用非现场执法团队建立的表,同样通过 **视图(VIEW)** 的方式从非现场执法库引入,仅做数据读取的功能,如需要修改其中的表,或者添加新的表,与非现场执法团队进行讨论: |
|||
* 部署测试时,**现场执法**与**非现场执法**两个库会合并为同一个库。 |
|||
|
|||
### 2. 表结构维护 |
|||
|
|||
* 所有 DDL/DML 全部以 SQL 形式存放在 [`script/sql/nos`](script/sql/nos) 目录下,方便后续脚本化重建数据库。 |
|||
* **命名规范**:非现场执法的所有表必须以 `nos_` 开头,方便后续与现场执法表区分。 |
|||
* **注释规范**:所有表和字段必须添加中文 `comment` 说明。 |
|||
* **租户排除**:所有 `nos_` 开头的表都需加入 [`application.yml`](zdxt-web-server/zdxt-admin-web-server/src/main/resources/application.yml) 的多租户排除表配置中: |
|||
```yaml |
|||
# 多租户配置 |
|||
tenant: |
|||
# 是否开启 |
|||
enable: false |
|||
# 排除表 |
|||
excludes: |
|||
- sys_menu |
|||
- sys_tenant |
|||
- sys_tenant_package |
|||
# 在此追加 nos_ 开头的表名 |
|||
``` |
|||
|
|||
### 3. 公共字段(所有 nos_ 开头表必须包含) |
|||
|
|||
每张非现场表必须包含以下统一字段,以满足审计、租户、逻辑删除等通用能力: |
|||
|
|||
| 字段名 | 类型 | 说明 | |
|||
| -------------- | -------------- |--------------------| |
|||
| `id` | varchar(50) | 主键 | |
|||
| `create_dept` | varchar(100) | 创建部门Id | |
|||
| `create_by` | varchar(50) | 创建人Id | |
|||
| `create_time` | datetime | 创建时间 | |
|||
| `update_by` | varchar(50) | 更新人Id | |
|||
| `update_time` | datetime | 更新时间 | |
|||
| `is_delete` | char(1) | 逻辑删除:`0` 否 / `1` 是 | |
|||
| `bureau_dept_id` | varchar(100) | 执法单位ID | |
|||
| `dept_id` | varchar(100) | 部门ID | |
|||
| `organ_id` | varchar(100) | 组织单位ID | |
|||
> 实体类继承 `TenantEntity` / `BaseEntity` 即可自动包含上述字段。 |
|||
``` |
|||
* bureau_dept_id:执法单位ID(当前用户所在的执法单位的id)=DataFilterUtil.getUserBureauDeptId |
|||
* dept_id:部门ID(当前用户所在的部门id)=DataFilterUtil.getUserDeptId |
|||
* organ_id:组织单位ID (当前用户所在的组织单位id)=DataFilterUtil.getUserOrganId |
|||
* 对应的名称作为可选项,自行决定加不加 |
|||
``` |
|||
--- |
|||
|
|||
## 四、后端 |
|||
|
|||
### 1. 模块位置 |
|||
|
|||
* 所有非现场执法的后端代码统一写入: |
|||
* [`zdxt-modules/zdxt-enforcement-nos`](zdxt-modules/zdxt-enforcement-nos) |
|||
* 该模块由主工程 [`zdxt-web-server/zdxt-admin-web-server`](zdxt-web-server/zdxt-admin-web-server) 引入并打包启动。 |
|||
|
|||
### 2. 后端模块目录规范 |
|||
|
|||
模块根包:`com.zdxt.enforcementnos`,各子目录职责如下: |
|||
建表后可以使用代码生成工具,与现场执法团队沟通,将表通过视图引入到现场执法库中或者将sql提供给现场执法团队直接在数据库中创建,然后再生成代码 |
|||
``` |
|||
zdxt-modules/zdxt-enforcement-nos/src/main/java/com/zdxt/enforcementnos/ |
|||
├── Main.java # 模块入口占位类(仅用于包扫描错误提示,不包含 main 方法) |
|||
├── common/ # 模块内部公共类集合(仅 nos 业务复用) |
|||
│ ├── commonenum/ # 业务枚举:状态、类型、扣分项等 |
|||
│ ├── config/ # 模块内业务配置类(例如自定义 ConfigurationProperties、常量配置) |
|||
│ └── util/ # 业务工具类(状态转换、字段补齐等,区别于全局 zdxt-common-core 工具) |
|||
├── config/ # Spring 配置类(@Configuration):Bean 装配、MyBatis-Plus、拦截器等 |
|||
├── controller/ # REST 接口层,路由前缀统一 `/enforcementcode/xxx` |
|||
│ ├── app/ # 移动端(H5)接口 |
|||
│ │ ├── enforcement/ # 执法端接口 |
|||
│ │ └── enterprise/ # 企业端接口 |
|||
│ ├── web/ # PC 管理端接口 |
|||
│ │ └── supervision/ # 监督端接口 |
|||
│ └── TestEnfDemoController.java |
|||
├── domain/ # 领域对象 |
|||
│ ├── TestEnfDemo.java # 实体(PO),客户端与服务端交互时,不要直接传递数据库对应的实体,对应数据库 `nos_xxx` 表,继承 TenantEntity / BaseEntity |
|||
│ ├── bo/ # 业务入参对象,服务端传递给客户端的实体对象(Business Object):新增/修改/查询参数 |
|||
│ └── vo/ # 出参对象,客户端传递给服务器的实体对象(View Object):返回给前端的结构 |
|||
├── mapper/ # MyBatis-Plus Mapper 接口,继承 BaseMapperPlus |
|||
│ └── TestEnfDemoMapper.java |
|||
├── service/ # 业务服务接口(`I*Service`) |
|||
│ ├── ITestEnfDemoService.java |
|||
│ └── impl/ # 服务实现类(`*ServiceImpl`) |
|||
│ └── TestEnfDemoServiceImpl.java |
|||
├── exception/ # 模块专属业务异常类(继承 ServiceException) |
|||
├── handler/ # 处理器:全局异常处理、枚举字典处理、数据权限处理等 |
|||
├── helper/ # 业务辅助类(复杂逻辑抽取,供 service 调用,区别于 util 是带业务上下文的) |
|||
├── listener/ # 事件监听:Spring Event、MQ消费、MyBatis-Plus 填充、Excel 导入监听等 |
|||
├── quartz/ # 定时任务(Quartz Job),接入公共模块 zdxt-common-quartz |
|||
├── runner/ # ApplicationRunner / CommandLineRunner:启动后需要执行一次的初始化逻辑 |
|||
└── util/ # 顶层通用工具类(不依赖 Spring 上下文) |
|||
``` |
|||
|
|||
#### 分层写入规定 |
|||
|
|||
| 场景 | 应放入的包 | |
|||
| -------------------------- | --------------------------------------------- | |
|||
| 接收 HTTP 请求 | `controller` | |
|||
| 表字段映射 | `domain` (`TestEnfDemo.java`) | |
|||
| 接收前端入参 | `domain.bo` | |
|||
| 返回前端出参 | `domain.vo` | |
|||
| 数据库访问 | `mapper` | |
|||
| 业务逻辑接口 | `service` | |
|||
| 业务逻辑实现 | `service.impl` | |
|||
| 复杂业务子步骤抽取 | `helper` | |
|||
| Bean 装配 / 拦截器注册 | `config` | |
|||
| 业务枚举、常量 | `common.commonenum`、`common.config` | |
|||
| 不依赖 Spring 的工具 | `util`(顶层) | |
|||
| 依赖 Spring 的业务工具 | `common.util` | |
|||
| 全局异常 / 字典填充 | `handler` | |
|||
| 事件监听 | `listener` | |
|||
| 定时任务 | `quartz` | |
|||
| 启动后一次性执行逻辑 | `runner` | |
|||
| 业务级异常 | `exception` | |
|||
|
|||
> 约定:不要在 `zdxt-enforcement-nos` 中创建与主工程同名的包名,使用默认的包名com.zdxt.enforcementnos。 |
|||
|
|||
### 3. 服务端口与上下文路径 |
|||
|
|||
* 启动 profile:`application.yml,dev,dev-nos-ep`(即 [`application-dev-nos-ep.yml`](zdxt-web-server/zdxt-admin-web-server/src/main/resources/application-dev-nos-ep.yml)) |
|||
* 端口:`18005` |
|||
* `context-path`:`/enf-ep` |
|||
* 因此后端实际接收路径形如:`http://127.0.0.1:18005/enf-ep/enforcementcode/enfDemo/list` |
|||
|
|||
### 4. 接入方法(接口设计原则) |
|||
|
|||
* 现场执法对外提供给非现场执法调用的接口,所需的参数 **必须通过接口参数传递**,不要凭借 token 去查询用户/部门等数据。 |
|||
* 非现场执法自身的接口,**只验证 token 的有效性**,暂时不再做权限码(`@SaCheckPermission`)等其他权限校验,等后续合并阶段再统一处理。 |
|||
* 如果非现场执法前端需要调用现场执法的后接口,通过token进行调用. |
|||
* 如果非现场执法后端需要调用现场执法后端的接口(尽量避免),在前端进入后,先请求一次后端,将 token 提交给后端,后端自行存入当前 session,本次 session 中凭该 token 调用后端接口。 |
|||
|
|||
### 5. clientid 必须一致 |
|||
|
|||
后端 Sa-Token 在登录时会把请求 Header 中的 `clientid` 与 token 绑定,之后每次请求都会校验。**前端 axios 拦截器中固定写入的 `clientid` 必须与后端 `sys_client` 表中已注册的指纹完全一致**,否则会出现: |
|||
|
|||
``` |
|||
认证失败:客户端ID与Token不匹配 |
|||
``` |
|||
|
|||
参考管理端 `e5cd7e4891bf95d1d19206ce24a7b32e`(开发环境),三个前端 H5 工程建议在 `.env.*` 中配置 `VITE_APP_CLIENT_ID`,并在 `http.js` 中通过 `import.meta.env.VITE_APP_CLIENT_ID` 读取。 |
|||
|
|||
--- |
|||
|
|||
## 五、前端 |
|||
|
|||
### 1. 三个独立工程 |
|||
|
|||
非现场执法前端拆分为 **3 个独立的 Vue 3 + Vite H5 工程**,相互不依赖: |
|||
|
|||
| 角色 | 工程路径 | 端口 | |
|||
| ------------ | ------------------------------------------------------------------------------------------- |-------| |
|||
| 企业端 | [`zdxt-efcode-nos-enterprise-app`](zdxt-web-client/zdxt-efcode-nos-enterprise-app) | 12001 | |
|||
| 执法端 | [`zdxt-efcode-nos-enforcement-app`](zdxt-web-client/zdxt-efcode-nos-enforcement-app) | 12002 | |
|||
| 监督端 | [`zdxt-admin-nos-plus-ui`](zdxt-web-client/zdxt-efcode-nos-supervision-app) | 12005 | |
|||
|
|||
> 实际端口以各工程 `vite-config/index.js` 与 `vite.config.js` 中 `server.port` 为准。 |
|||
|
|||
### 2. 非现场执法代码统一放入 `nos/` 子目录 |
|||
|
|||
为了后续合入现场执法时能一眼辨识哪些是本期非现场执法新增代码,**三个前端工程内所有非现场执法新增的文件必须放入名为 `nos` 的子目录**。现有的 `nos/` 目录清单如下: |
|||
|
|||
#### `zdxt-admin-nos-plus-ui`(PC 管理端,参考用) |
|||
``` |
|||
src/views/efcode/nos/ |
|||
``` |
|||
|
|||
#### `zdxt-efcode-nos-enforcement-app`(执法端 H5) |
|||
``` |
|||
src/api/nos/ |
|||
src/assets/nos/ |
|||
src/components/nos/ |
|||
src/router/nos/ |
|||
src/views/enforcement/nos/ |
|||
``` |
|||
|
|||
#### `zdxt-efcode-nos-enterprise-app`(企业端 H5) |
|||
``` |
|||
src/api/nos/ |
|||
src/assets/nos/ |
|||
src/components/nos/ |
|||
src/router/nos/ |
|||
src/store/nos/ |
|||
src/util/nos/ |
|||
src/views/enterprise/nos/ |
|||
``` |
|||
|
|||
> 约定:凡是本期非现场执法新写的 `*.vue / *.js / *.ts / 图片 / less` 等,都必须放在上述对应目录下的 `nos/` 里,不得混入现场执法原有目录。合并时只需拷入主仓库对应目录的 `nos/` 下。 |
|||
|
|||
### 3. 启动命令 |
|||
|
|||
```bash |
|||
cd zdxt-web-client/zdxt-efcode-nos-enforcement-app |
|||
npm install |
|||
npm run dev # 开发模式(加载 vite-env/.env.enforcement-dev) |
|||
npm run build # 生产打包(加载 vite-env/.env.enforcement-prod) |
|||
``` |
|||
|
|||
### 4. 请求前缀约定 |
|||
|
|||
| 前缀 | 含义 | 转发目标 | |
|||
| ---------- | ----------------------------- | ------------------------------------- | |
|||
| `/api` | 现场执法接口 | 现场执法后台 | |
|||
| `/nosapi` | 非现场执法接口 | 现场执法后台(合并部署后) / 本地后端 | |
|||
|
|||
* **开发期**通过 Vite 代理转发;**生产期**通过 NG(Nginx)代理转发,前缀保持一致。 |
|||
* `/nosapi` 代理到 `http://127.0.0.1:18005` 时,**rewrite 必须补上后端 `context-path`**: |
|||
```js |
|||
'/nosapi': { |
|||
target: 'http://127.0.0.1:18005', |
|||
changeOrigin: true, |
|||
// 必须 rewrite 到 /enf-ep,否则 404 |
|||
rewrite: (path) => path.replace(/^\/nosapi/, '/enf-ep'), |
|||
} |
|||
``` |
|||
|
|||
### 5. 开发期登录流程 |
|||
|
|||
```mermaid |
|||
graph TD |
|||
A[开发时 App 登录页面] -->|Vite 代理转发登录请求| B["现场执法后台 /api 前缀请求(现场执法)"] |
|||
B -->|返回访问 Token| C[后续接口访问] |
|||
C --> D["/api 前缀请求(现场执法)"] |
|||
C --> E["/nosapi 前缀请求(非现场执法)"] |
|||
D -->|Vite 代理转发| F[现场执法后台] |
|||
E -->|Vite 代理转发| F |
|||
``` |
|||
|
|||
### 6. 部署期免登录流程 |
|||
|
|||
从主页面进入非现场页面时,会携带 token,凭借 token 可以调用监督码后台接口获取相关信息: |
|||
|
|||
```mermaid |
|||
graph TD |
|||
A[从主页面进入非现场的页面时] -->|携带 token 进入| B["路由守卫拦截,获取 token,存入 $globalStore"] |
|||
B -->|返回访问 Token| C[后续接口访问] |
|||
C --> D["/api 前缀请求(现场执法)"] |
|||
C --> E["/nosapi 前缀请求(非现场执法)"] |
|||
D -->|NG 代理转发| F[现场执法后台] |
|||
E -->|NG 代理转发| F |
|||
``` |
|||
|
|||
--- |
|||
|
|||
## 六、代码生成 |
|||
|
|||
* 将设计好的表 **创建到执法码二期的库** 中,使用现有的代码生成模块即可生成统一风格的 CRUD 代码,便于后续维护。 |
|||
* **包名设置**:在代码生成模块中导入表后,点击「编辑」→「生成信息」,将包名修改为: |
|||
``` |
|||
com.zdxt.enforcementnos |
|||
``` |
|||
这样生成的代码与现有 `zdxt-modules/zdxt-enforcement-nos` 的目录结构一致,可直接拷贝过来使用。 |
|||
|
|||
--- |
|||
|
|||
## 七、合入主仓库的步骤 |
|||
|
|||
### 1. 前端 |
|||
|
|||
3 个前端工程独立合并,每个都按以下步骤来一次: |
|||
|
|||
* [`zdxt-efcode-nos-enterprise-app`](zdxt-web-client/zdxt-efcode-nos-enterprise-app) |
|||
* [`zdxt-efcode-nos-enforcement-app`](zdxt-web-client/zdxt-efcode-nos-enforcement-app) |
|||
* [`zdxt-efcode-nos-supervision-app`](zdxt-web-client/zdxt-efcode-nos-supervision-app) |
|||
|
|||
操作要点: |
|||
|
|||
1. 把整个工程目录拷贝到现场执法仓库的对应位置。 |
|||
2. 同步 `vite-env/.env.*`、`vite-config/index.js` 中的代理与端口配置。 |
|||
3. 把 `clientid` 改为现场执法环境对应的指纹值。 |
|||
4. 检查路由前缀(`/enforcement`、`/enterprise`、`/supervision`)是否与现场执法已有路由冲突。 |
|||
|
|||
### 2. 后端 |
|||
|
|||
* 将 [`zdxt-modules/zdxt-enforcement-nos`](zdxt-modules/zdxt-enforcement-nos) 模块整体拷贝到现场执法的 `zdxt-modules` 目录下。 |
|||
* 在现场执法的 `zdxt-web-server/zdxt-admin-web-server/pom.xml` 中加入对该模块的依赖。 |
|||
* 把 `nos_` 开头的表 SQL([`script/sql/nos`](script/sql/nos))合入现场执法的数据库初始化脚本。 |
|||
* 把 `application.yml` 中的租户排除表条目合入。 |
|||
* 验证 Sa-Token 全局拦截器对 `/enforcementcode/**` 路径的放行规则。 |
|||
|
|||
### 3. 合并验证清单 |
|||
|
|||
- [ ] 三端 H5 可独立启动并成功登录 |
|||
- [ ] `/api` 与 `/nosapi` 接口均能正常返回 |
|||
- [ ] 后端 `nos_` 表无与现场执法表的命名冲突 |
|||
- [ ] 多租户字段在 `nos_` 表上不会被误注入 |
|||
- [ ] `clientid` 与 `sys_client` 表数据一致 |
|||
|
|||
--- |
|||
|
|||
## 八、目录速查 |
|||
|
|||
``` |
|||
EnforcementCode-NOS-2.0 |
|||
├── Libs/ # 第三方私有 jar |
|||
├── script/sql/nos/ # 非现场表 SQL |
|||
├── zdxt-modules/zdxt-enforcement-nos/ # 后端业务模块 |
|||
├── zdxt-web-server/zdxt-admin-web-server/ # 后端启动工程(端口 18005, ctx /enf-ep) |
|||
└── zdxt-web-client/ |
|||
├── zdxt-admin-nos-plus-ui/ # 管理端(PC,参考用) |
|||
├── zdxt-efcode-nos-enterprise-app/ # 企业端 H5 |
|||
├── zdxt-efcode-nos-enforcement-app/ # 执法端 H5 |
|||
└── zdxt-efcode-nos-supervision-app/ # 监督端 H5 |
|||
``` |
|||
@ -0,0 +1,4 @@ |
|||
package com.zdxt.enforcementnos.controller.app.enforcement; |
|||
|
|||
public class c1 { |
|||
} |
|||
@ -0,0 +1,4 @@ |
|||
package com.zdxt.enforcementnos.controller.app.enterprise; |
|||
|
|||
public class c1 { |
|||
} |
|||
@ -0,0 +1,4 @@ |
|||
package com.zdxt.enforcementnos.controller.web.supervision; |
|||
|
|||
public class c1 { |
|||
} |
|||
@ -0,0 +1,59 @@ |
|||
import request from '@/utils/request'; |
|||
import { AxiosPromise } from 'axios'; |
|||
import { CacheVO } from './types'; |
|||
|
|||
// 查询缓存详细
|
|||
export function getCache(): AxiosPromise<CacheVO> { |
|||
return request({ |
|||
url: '/monitor/cache', |
|||
method: 'get' |
|||
}); |
|||
} |
|||
|
|||
// 查询缓存名称列表
|
|||
export function listCacheName() { |
|||
return request({ |
|||
url: '/monitor/cache/getNames', |
|||
method: 'get' |
|||
}); |
|||
} |
|||
|
|||
// 查询缓存键名列表
|
|||
export function listCacheKey(cacheName: string) { |
|||
return request({ |
|||
url: '/monitor/cache/getKeys/' + cacheName, |
|||
method: 'get' |
|||
}); |
|||
} |
|||
|
|||
// 查询缓存内容
|
|||
export function getCacheValue(cacheName: string, cacheKey: string) { |
|||
return request({ |
|||
url: '/monitor/cache/getValue/' + cacheName + '/' + cacheKey, |
|||
method: 'get' |
|||
}); |
|||
} |
|||
|
|||
// 清理指定名称缓存
|
|||
export function clearCacheName(cacheName: string) { |
|||
return request({ |
|||
url: '/monitor/cache/clearCacheName/' + cacheName, |
|||
method: 'delete' |
|||
}); |
|||
} |
|||
|
|||
// 清理指定键名缓存
|
|||
export function clearCacheKey(cacheName: string, cacheKey: string) { |
|||
return request({ |
|||
url: '/monitor/cache/clearCacheKey/' + cacheName + '/' + cacheKey, |
|||
method: 'delete' |
|||
}); |
|||
} |
|||
|
|||
// 清理全部缓存
|
|||
export function clearCacheAll() { |
|||
return request({ |
|||
url: '/monitor/cache/clearCacheAll', |
|||
method: 'delete' |
|||
}); |
|||
} |
|||
@ -0,0 +1,7 @@ |
|||
export interface CacheVO { |
|||
commandStats: Array<{ name: string; value: string }>; |
|||
|
|||
dbSize: number; |
|||
|
|||
info: { [key: string]: string }; |
|||
} |
|||
@ -0,0 +1,36 @@ |
|||
import request from '@/utils/request'; |
|||
import { LoginInfoQuery, LoginInfoVO } from './types'; |
|||
import { AxiosPromise } from 'axios'; |
|||
|
|||
// 查询登录日志列表
|
|||
export function list(query: LoginInfoQuery): AxiosPromise<LoginInfoVO[]> { |
|||
return request({ |
|||
url: '/monitor/logininfor/list', |
|||
method: 'get', |
|||
params: query |
|||
}); |
|||
} |
|||
|
|||
// 删除登录日志
|
|||
export function delLoginInfo(infoId: string | number | Array<string | number>) { |
|||
return request({ |
|||
url: '/monitor/logininfor/' + infoId, |
|||
method: 'delete' |
|||
}); |
|||
} |
|||
|
|||
// 解锁用户登录状态
|
|||
export function unlockLoginInfo(userName: string | Array<string>) { |
|||
return request({ |
|||
url: '/monitor/logininfor/unlock/' + userName, |
|||
method: 'get' |
|||
}); |
|||
} |
|||
|
|||
// 清空登录日志
|
|||
export function cleanLoginInfo() { |
|||
return request({ |
|||
url: '/monitor/logininfor/clean', |
|||
method: 'delete' |
|||
}); |
|||
} |
|||
@ -0,0 +1,20 @@ |
|||
export interface LoginInfoVO { |
|||
infoId: string | number; |
|||
tenantId: string | number; |
|||
userName: string; |
|||
status: string; |
|||
ipaddr: string; |
|||
loginLocation: string; |
|||
browser: string; |
|||
os: string; |
|||
msg: string; |
|||
loginTime: string; |
|||
} |
|||
|
|||
export interface LoginInfoQuery extends PageQuery { |
|||
ipaddr: string; |
|||
userName: string; |
|||
status: string; |
|||
orderByColumn: string; |
|||
isAsc: string; |
|||
} |
|||
@ -0,0 +1,36 @@ |
|||
import request from '@/utils/request'; |
|||
import { OnlineQuery, OnlineVO } from './types'; |
|||
import { AxiosPromise } from 'axios'; |
|||
|
|||
// 查询在线用户列表
|
|||
export function list(query: OnlineQuery): AxiosPromise<OnlineVO[]> { |
|||
return request({ |
|||
url: '/monitor/online/list', |
|||
method: 'get', |
|||
params: query |
|||
}); |
|||
} |
|||
|
|||
// 强退用户
|
|||
export function forceLogout(tokenId: string) { |
|||
return request({ |
|||
url: '/monitor/online/' + tokenId, |
|||
method: 'delete' |
|||
}); |
|||
} |
|||
|
|||
// 获取当前用户登录在线设备
|
|||
export function getOnline() { |
|||
return request({ |
|||
url: '/monitor/online', |
|||
method: 'get' |
|||
}); |
|||
} |
|||
|
|||
// 删除当前在线设备
|
|||
export function delOnline(tokenId: string) { |
|||
return request({ |
|||
url: '/monitor/online/myself/' + tokenId, |
|||
method: 'delete' |
|||
}); |
|||
} |
|||
@ -0,0 +1,15 @@ |
|||
export interface OnlineQuery extends PageQuery { |
|||
ipaddr: string; |
|||
userName: string; |
|||
} |
|||
|
|||
export interface OnlineVO extends BaseEntity { |
|||
tokenId: string; |
|||
deptName: string; |
|||
userName: string; |
|||
ipaddr: string; |
|||
loginLocation: string; |
|||
browser: string; |
|||
os: string; |
|||
loginTime: number; |
|||
} |
|||
@ -0,0 +1,28 @@ |
|||
import request from '@/utils/request'; |
|||
import { OperLogQuery, OperLogVO } from './types'; |
|||
import { AxiosPromise } from 'axios'; |
|||
|
|||
// 查询操作日志列表
|
|||
export function list(query: OperLogQuery): AxiosPromise<OperLogVO[]> { |
|||
return request({ |
|||
url: '/monitor/operlog/list', |
|||
method: 'get', |
|||
params: query |
|||
}); |
|||
} |
|||
|
|||
// 删除操作日志
|
|||
export function delOperlog(operId: string | number | Array<string | number>) { |
|||
return request({ |
|||
url: '/monitor/operlog/' + operId, |
|||
method: 'delete' |
|||
}); |
|||
} |
|||
|
|||
// 清空操作日志
|
|||
export function cleanOperlog() { |
|||
return request({ |
|||
url: '/monitor/operlog/clean', |
|||
method: 'delete' |
|||
}); |
|||
} |
|||
@ -0,0 +1,53 @@ |
|||
export interface OperLogQuery extends PageQuery { |
|||
operIp: string; |
|||
title: string; |
|||
operName: string; |
|||
businessType: string; |
|||
status: string; |
|||
orderByColumn: string; |
|||
isAsc: string; |
|||
} |
|||
|
|||
export interface OperLogVO extends BaseEntity { |
|||
operId: string | number; |
|||
tenantId: string; |
|||
title: string; |
|||
businessType: number; |
|||
businessTypes: number[] | undefined; |
|||
method: string; |
|||
requestMethod: string; |
|||
operatorType: number; |
|||
operName: string; |
|||
deptName: string; |
|||
operUrl: string; |
|||
operIp: string; |
|||
operLocation: string; |
|||
operParam: string; |
|||
jsonResult: string; |
|||
status: number; |
|||
errorMsg: string; |
|||
operTime: string; |
|||
costTime: number; |
|||
} |
|||
|
|||
export interface OperLogForm { |
|||
operId: number | string | undefined; |
|||
tenantId: string | number | undefined; |
|||
title: string; |
|||
businessType: number; |
|||
businessTypes: number[] | undefined; |
|||
method: string; |
|||
requestMethod: string; |
|||
operatorType: number; |
|||
operName: string; |
|||
deptName: string; |
|||
operUrl: string; |
|||
operIp: string; |
|||
operLocation: string; |
|||
operParam: string; |
|||
jsonResult: string; |
|||
status: number; |
|||
errorMsg: string; |
|||
operTime: string; |
|||
costTime: number; |
|||
} |
|||
@ -0,0 +1,146 @@ |
|||
<template> |
|||
<div class="p-2"> |
|||
<div class="panel"> |
|||
<h4 class="panel-title">基本信息</h4> |
|||
<el-form :model="form" :inline="true"> |
|||
<el-row :gutter="10"> |
|||
<el-col :span="2.5"> |
|||
<el-form-item label="用户昵称" prop="nickName"> |
|||
<el-input v-model="form.nickName" disabled /> |
|||
</el-form-item> |
|||
</el-col> |
|||
<el-col :span="2.5"> |
|||
<el-form-item label="登录账号" prop="userName"> |
|||
<el-input v-model="form.userName" disabled /> |
|||
</el-form-item> |
|||
</el-col> |
|||
</el-row> |
|||
</el-form> |
|||
</div> |
|||
<div class="panel"> |
|||
<h4 class="panel-title">角色信息</h4> |
|||
<div> |
|||
<el-table |
|||
ref="tableRef" |
|||
v-loading="loading" |
|||
border |
|||
:row-key="getRowKey" |
|||
:data="roles.slice((pageNum - 1) * pageSize, pageNum * pageSize)" |
|||
@row-click="clickRow" |
|||
@selection-change="handleSelectionChange" |
|||
> |
|||
<el-table-column label="序号" width="55" type="index" align="center"> |
|||
<template #default="scope"> |
|||
<span>{{ (pageNum - 1) * pageSize + scope.$index + 1 }}</span> |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column type="selection" :reserve-selection="true" :selectable="checkSelectable" width="55"></el-table-column> |
|||
<el-table-column label="角色编号" align="center" prop="roleId" /> |
|||
<el-table-column label="角色名称" align="center" prop="roleName" /> |
|||
<el-table-column label="权限字符" align="center" prop="roleKey" /> |
|||
<el-table-column label="创建时间" align="center" prop="createTime" width="180"> |
|||
<template #default="scope"> |
|||
<span>{{ proxy.parseTime(scope.row.createTime) }}</span> |
|||
</template> |
|||
</el-table-column> |
|||
</el-table> |
|||
<pagination v-show="total > 0" v-model:page="pageNum" v-model:limit="pageSize" :total="total" /> |
|||
<div style="text-align: center; margin-left: -120px; margin-top: 30px"> |
|||
<el-button type="primary" @click="submitForm()">提交</el-button> |
|||
<el-button @click="close()">返回</el-button> |
|||
</div> |
|||
<div></div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<script setup name="AuthRole" lang="ts"> |
|||
import { RoleVO } from '@/api/system/role/types'; |
|||
import { getAuthRole, updateAuthRole } from '@/api/system/user'; |
|||
import { UserForm } from '@/api/system/user/types'; |
|||
import { RouteLocationNormalized } from 'vue-router'; |
|||
import { parseTime } from '@/utils/ruoyi'; |
|||
|
|||
const route = useRoute(); |
|||
const { proxy } = getCurrentInstance() as ComponentInternalInstance; |
|||
|
|||
const loading = ref(true); |
|||
const total = ref(0); |
|||
const pageNum = ref(1); |
|||
const pageSize = ref(10); |
|||
const roleIds = ref<Array<string | number>>([]); |
|||
const roles = ref<RoleVO[]>([]); |
|||
const form = ref<Partial<UserForm>>({ |
|||
nickName: undefined, |
|||
userName: '', |
|||
userId: undefined |
|||
}); |
|||
|
|||
const tableRef = ref<ElTableInstance>(); |
|||
|
|||
/** 单击选中行数据 */ |
|||
const clickRow = (row: RoleVO) => { |
|||
if (checkSelectable(row)) { |
|||
row.flag = !row.flag; |
|||
tableRef.value?.toggleRowSelection(row, row.flag); |
|||
} |
|||
}; |
|||
/** 多选框选中数据 */ |
|||
const handleSelectionChange = (selection: RoleVO[]) => { |
|||
roleIds.value = selection.map((item) => item.roleId); |
|||
}; |
|||
/** 保存选中的数据编号 */ |
|||
const getRowKey = (row: RoleVO): string => { |
|||
return String(row.roleId); |
|||
}; |
|||
/** 检查角色状态 */ |
|||
const checkSelectable = (row: RoleVO): boolean => { |
|||
return row.status === '0'; |
|||
}; |
|||
/** 关闭按钮 */ |
|||
const close = () => { |
|||
const obj: RouteLocationNormalized = { |
|||
fullPath: '', |
|||
hash: '', |
|||
matched: [], |
|||
meta: undefined, |
|||
name: undefined, |
|||
params: undefined, |
|||
query: undefined, |
|||
redirectedFrom: undefined, |
|||
path: '/system/user' |
|||
}; |
|||
proxy?.$tab.closeOpenPage(obj); |
|||
}; |
|||
/** 提交按钮 */ |
|||
const submitForm = async () => { |
|||
const userId = form.value.userId; |
|||
const rIds = roleIds.value.join(','); |
|||
await updateAuthRole({ userId: userId as string, roleIds: rIds }); |
|||
proxy?.$modal.msgSuccess('授权成功'); |
|||
close(); |
|||
}; |
|||
|
|||
const getList = async () => { |
|||
const userId = route.params && route.params.userId; |
|||
if (userId) { |
|||
loading.value = true; |
|||
const res = await getAuthRole(userId as string); |
|||
Object.assign(form.value, res.data.user); |
|||
Object.assign(roles.value, res.data.roles); |
|||
total.value = roles.value.length; |
|||
await nextTick(() => { |
|||
roles.value.forEach((row) => { |
|||
if (row?.flag) { |
|||
tableRef.value?.toggleRowSelection(row, true); |
|||
} |
|||
}); |
|||
}); |
|||
loading.value = false; |
|||
} |
|||
}; |
|||
onMounted(() => { |
|||
getList(); |
|||
}); |
|||
</script> |
|||
@ -0,0 +1,732 @@ |
|||
<template> |
|||
<div class="p-2"> |
|||
<el-row :gutter="20"> |
|||
<!-- 部门树 --> |
|||
<el-col :lg="4" :xs="24" style=""> |
|||
<el-card shadow="hover" style="position: sticky; top: 0;"> |
|||
<el-input v-model="deptName" placeholder="请输入部门名称" prefix-icon="Search" clearable/> |
|||
<div style="max-height: calc(100vh - 180px); overflow-y: auto; overflow-x: hidden;"> |
|||
<el-tree |
|||
ref="deptTreeRef" |
|||
class="mt-2" |
|||
node-key="id" |
|||
:data="deptOptions" |
|||
:props="{ label: 'label', children: 'children' } as any" |
|||
:expand-on-click-node="false" |
|||
:filter-node-method="filterNode" |
|||
highlight-current |
|||
:default-expanded-keys="deptExpandedKeys" |
|||
@node-click="handleNodeClick" |
|||
/> |
|||
</div> |
|||
</el-card> |
|||
</el-col> |
|||
<el-col :lg="20" :xs="24"> |
|||
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" |
|||
:leave-active-class="proxy?.animate.searchAnimate.leave"> |
|||
<div v-show="showSearch" class="mb-[10px]"> |
|||
<el-card shadow="hover"> |
|||
<el-form ref="queryFormRef" :model="queryParams" :inline="true"> |
|||
<el-form-item label="用户名称" prop="userName"> |
|||
<el-input v-model="queryParams.userName" placeholder="请输入用户名称" clearable |
|||
@keyup.enter="handleQuery"/> |
|||
</el-form-item> |
|||
<el-form-item label="用户昵称" prop="nickName"> |
|||
<el-input v-model="queryParams.nickName" placeholder="请输入用户昵称" clearable |
|||
@keyup.enter="handleQuery"/> |
|||
</el-form-item> |
|||
<el-form-item label="手机号码" prop="phonenumber"> |
|||
<el-input v-model="queryParams.phonenumber" placeholder="请输入手机号码" clearable |
|||
@keyup.enter="handleQuery"/> |
|||
</el-form-item> |
|||
|
|||
<el-form-item label="状态" prop="status"> |
|||
<el-select v-model="queryParams.status" placeholder="用户状态" clearable> |
|||
<el-option v-for="dict in sys_normal_disable" :key="dict.value" :label="dict.label" |
|||
:value="dict.value"/> |
|||
</el-select> |
|||
</el-form-item> |
|||
<el-form-item label="创建时间" style="width: 308px"> |
|||
<el-date-picker |
|||
v-model="dateRange" |
|||
value-format="YYYY-MM-DD HH:mm:ss" |
|||
type="daterange" |
|||
range-separator="-" |
|||
start-placeholder="开始日期" |
|||
end-placeholder="结束日期" |
|||
:default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]" |
|||
></el-date-picker> |
|||
</el-form-item> |
|||
<el-form-item> |
|||
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button> |
|||
<el-button icon="Refresh" @click="resetQuery">重置</el-button> |
|||
</el-form-item> |
|||
</el-form> |
|||
</el-card> |
|||
</div> |
|||
</transition> |
|||
|
|||
<el-card shadow="hover"> |
|||
<template #header> |
|||
<el-row :gutter="10"> |
|||
<el-col :span="1.5"> |
|||
<el-button v-has-permi="['system:user:add']" type="primary" plain icon="Plus" @click="handleAdd()"> |
|||
新增 |
|||
</el-button> |
|||
</el-col> |
|||
<el-col :span="1.5"> |
|||
<el-button v-has-permi="['system:user:edit']" type="success" plain :disabled="single" icon="Edit" |
|||
@click="handleUpdate()"> |
|||
修改 |
|||
</el-button> |
|||
</el-col> |
|||
<el-col :span="1.5"> |
|||
<el-button v-has-permi="['system:user:remove']" type="danger" plain :disabled="multiple" icon="Delete" |
|||
@click="handleDelete()"> |
|||
删除 |
|||
</el-button> |
|||
</el-col> |
|||
<el-col :span="1.5"> |
|||
<el-dropdown class="mt-[1px]"> |
|||
<el-button plain type="info"> |
|||
更多 |
|||
<el-icon class="el-icon--right"> |
|||
<arrow-down/> |
|||
</el-icon |
|||
> |
|||
</el-button> |
|||
<template #dropdown> |
|||
<el-dropdown-menu> |
|||
<el-dropdown-item icon="Download" @click="importTemplate">下载模板</el-dropdown-item> |
|||
<!-- 注意 由于el-dropdown-item标签是延迟加载的 所以v-has-permi自定义标签不生效 需要使用v-if调用方法执行 --> |
|||
<el-dropdown-item v-if="checkPermi(['system:user:import'])" icon="Top" @click="handleImport"> |
|||
导入数据 |
|||
</el-dropdown-item> |
|||
<el-dropdown-item v-if="checkPermi(['system:user:export'])" icon="Download" @click="handleExport"> |
|||
导出数据 |
|||
</el-dropdown-item> |
|||
</el-dropdown-menu> |
|||
</template> |
|||
</el-dropdown> |
|||
</el-col> |
|||
<right-toolbar v-model:show-search="showSearch" :columns="columns" :search="true" |
|||
@query-table="getList"></right-toolbar> |
|||
</el-row> |
|||
</template> |
|||
|
|||
<el-table v-loading="loading" border :data="userList" @selection-change="handleSelectionChange"> |
|||
<el-table-column type="selection" width="50" align="center"/> |
|||
<el-table-column v-if="columns[0].visible" key="userId" label="用户编号" align="center" prop="userId"/> |
|||
<el-table-column v-if="columns[1].visible" key="userName" label="用户名称" align="center" prop="userName" |
|||
:show-overflow-tooltip="true"/> |
|||
<el-table-column v-if="columns[2].visible" key="nickName" label="用户昵称" align="center" prop="nickName" |
|||
:show-overflow-tooltip="true"/> |
|||
<el-table-column v-if="columns[3].visible" key="deptName" label="部门" align="center" prop="deptName" |
|||
:show-overflow-tooltip="true"/> |
|||
<el-table-column v-if="columns[4].visible" key="phonenumber" label="手机号码" align="center" |
|||
prop="phonenumber" width="120"/> |
|||
<el-table-column v-if="columns[5].visible" key="status" label="状态" align="center"> |
|||
<template #default="scope"> |
|||
<el-switch v-model="scope.row.status" active-value="0" inactive-value="1" |
|||
@change="handleStatusChange(scope.row)"></el-switch> |
|||
</template> |
|||
</el-table-column> |
|||
|
|||
<el-table-column v-if="columns[6].visible" label="创建时间" align="center" prop="createTime" width="160"> |
|||
<template #default="scope"> |
|||
<span>{{ scope.row.createTime }}</span> |
|||
</template> |
|||
</el-table-column> |
|||
|
|||
<el-table-column label="操作" fixed="right" width="180" class-name="small-padding fixed-width"> |
|||
<template #default="scope"> |
|||
<el-tooltip v-if="scope.row.userId !== 1" content="修改" placement="top"> |
|||
<el-button v-hasPermi="['system:user:edit']" link type="primary" icon="Edit" |
|||
@click="handleUpdate(scope.row)"></el-button> |
|||
</el-tooltip> |
|||
<el-tooltip v-if="scope.row.userId !== 1" content="删除" placement="top"> |
|||
<el-button v-hasPermi="['system:user:remove']" link type="primary" icon="Delete" |
|||
@click="handleDelete(scope.row)"></el-button> |
|||
</el-tooltip> |
|||
|
|||
<el-tooltip v-if="scope.row.userId !== 1" content="重置密码" placement="top"> |
|||
<el-button v-hasPermi="['system:user:resetPwd']" link type="primary" icon="Key" |
|||
@click="handleResetPwd(scope.row)"></el-button> |
|||
</el-tooltip> |
|||
|
|||
<el-tooltip v-if="scope.row.userId !== 1" content="分配角色" placement="top"> |
|||
<el-button v-hasPermi="['system:user:edit']" link type="primary" icon="CircleCheck" |
|||
@click="handleAuthRole(scope.row)"></el-button> |
|||
</el-tooltip> |
|||
</template> |
|||
</el-table-column> |
|||
</el-table> |
|||
|
|||
<pagination |
|||
v-show="total > 0" |
|||
v-model:page="queryParams.pageNum" |
|||
v-model:limit="queryParams.pageSize" |
|||
:total="total" |
|||
@pagination="getList" |
|||
/> |
|||
</el-card> |
|||
</el-col> |
|||
</el-row> |
|||
|
|||
<!-- 添加或修改用户配置对话框 --> |
|||
<el-dialog ref="formDialogRef" v-model="dialog.visible" :title="dialog.title" width="600px" append-to-body |
|||
@close="closeDialog"> |
|||
<el-form ref="userFormRef" :model="form" :rules="rules" label-width="80px"> |
|||
<el-row> |
|||
<el-col :span="12"> |
|||
<el-form-item label="用户昵称" prop="nickName"> |
|||
<el-input v-model="form.nickName" placeholder="请输入用户昵称" maxlength="30"/> |
|||
</el-form-item> |
|||
</el-col> |
|||
<el-col :span="12" v-if="form.userId == null || form.userId != useUserStore().userId"> |
|||
<el-form-item label="归属部门" prop="deptId"> |
|||
<el-tree-select |
|||
v-model="form.deptId" |
|||
:data="enabledDeptOptions" |
|||
:props="{ value: 'id', label: 'label', children: 'children' } as any" |
|||
value-key="id" |
|||
placeholder="请选择归属部门" |
|||
check-strictly |
|||
@change="handleDeptChange" |
|||
/> |
|||
</el-form-item> |
|||
</el-col> |
|||
</el-row> |
|||
<el-row> |
|||
<el-col :span="12"> |
|||
<el-form-item label="手机号码" prop="phonenumber"> |
|||
<el-input v-model="form.phonenumber" placeholder="请输入手机号码" maxlength="11"/> |
|||
</el-form-item> |
|||
</el-col> |
|||
<el-col :span="12"> |
|||
<el-form-item label="邮箱" prop="email"> |
|||
<el-input v-model="form.email" placeholder="请输入邮箱" maxlength="50"/> |
|||
</el-form-item> |
|||
</el-col> |
|||
</el-row> |
|||
<el-row> |
|||
<el-col :span="12"> |
|||
<el-form-item v-if="form.userId == undefined" label="用户名称" prop="userName"> |
|||
<el-input v-model="form.userName" placeholder="请输入用户名称" maxlength="30"/> |
|||
</el-form-item> |
|||
</el-col> |
|||
<el-col :span="12"> |
|||
<el-form-item v-if="form.userId == undefined" label="用户密码" prop="password"> |
|||
<el-input v-model="form.password" placeholder="请输入用户密码" type="password" maxlength="20" |
|||
show-password/> |
|||
</el-form-item> |
|||
</el-col> |
|||
</el-row> |
|||
<el-row> |
|||
<el-col :span="12"> |
|||
<el-form-item label="用户性别"> |
|||
<el-select v-model="form.sex" placeholder="请选择"> |
|||
<el-option v-for="dict in sys_user_sex" :key="dict.value" :label="dict.label" |
|||
:value="dict.value"></el-option> |
|||
</el-select> |
|||
</el-form-item> |
|||
</el-col> |
|||
<el-col :span="12"> |
|||
<el-form-item label="状态"> |
|||
<el-radio-group v-model="form.status"> |
|||
<el-radio v-for="dict in sys_normal_disable" :key="dict.value" :value="dict.value">{{ |
|||
dict.label |
|||
}} |
|||
</el-radio> |
|||
</el-radio-group> |
|||
</el-form-item> |
|||
</el-col> |
|||
</el-row> |
|||
<el-row> |
|||
<el-col :span="12" v-if="form.userId == null || form.userId != useUserStore().userId"> |
|||
<el-form-item label="岗位"> |
|||
<el-select v-model="form.postIds" multiple placeholder="请选择"> |
|||
<el-option |
|||
v-for="item in postOptions" |
|||
:key="item.postId" |
|||
:label="item.postName" |
|||
:value="item.postId" |
|||
:disabled="item.status == '1'" |
|||
></el-option> |
|||
</el-select> |
|||
</el-form-item> |
|||
</el-col> |
|||
<el-col :span="12" v-if="form.userId == null || form.userId != useUserStore().userId"> |
|||
<el-form-item label="角色" prop="roleIds"> |
|||
<el-select v-model="form.roleIds" filterable multiple placeholder="请选择"> |
|||
<el-option |
|||
v-for="item in roleOptions" |
|||
:key="item.roleId" |
|||
:label="item.roleName" |
|||
:value="item.roleId" |
|||
:disabled="item.status == '1'" |
|||
></el-option> |
|||
</el-select> |
|||
</el-form-item> |
|||
</el-col> |
|||
</el-row> |
|||
<el-row> |
|||
<el-col :span="24"> |
|||
<el-form-item label="备注"> |
|||
<el-input v-model="form.remark" type="textarea" placeholder="请输入内容"></el-input> |
|||
</el-form-item> |
|||
</el-col> |
|||
</el-row> |
|||
</el-form> |
|||
<template #footer> |
|||
<div class="dialog-footer"> |
|||
<el-button type="primary" @click="submitForm">确 定</el-button> |
|||
<el-button @click="cancel()">取 消</el-button> |
|||
</div> |
|||
</template> |
|||
</el-dialog> |
|||
|
|||
<!-- 用户导入对话框 --> |
|||
<el-dialog v-model="upload.open" :title="upload.title" width="400px" append-to-body> |
|||
<el-upload |
|||
ref="uploadRef" |
|||
:limit="1" |
|||
accept=".xlsx, .xls" |
|||
:headers="upload.headers" |
|||
:action="upload.url + '?updateSupport=' + upload.updateSupport" |
|||
:disabled="upload.isUploading" |
|||
:on-progress="handleFileUploadProgress" |
|||
:on-success="handleFileSuccess" |
|||
:auto-upload="false" |
|||
drag |
|||
> |
|||
<el-icon class="el-icon--upload"> |
|||
<i-ep-upload-filled/> |
|||
</el-icon> |
|||
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div> |
|||
<template #tip> |
|||
<div class="text-center el-upload__tip"> |
|||
<div class="el-upload__tip"> |
|||
<el-checkbox v-model="upload.updateSupport"/> |
|||
是否更新已经存在的用户数据 |
|||
</div> |
|||
<span>仅允许导入xls、xlsx格式文件。</span> |
|||
<el-link type="primary" :underline="false" style="font-size: 12px; vertical-align: baseline" |
|||
@click="importTemplate">下载模板 |
|||
</el-link> |
|||
</div> |
|||
</template> |
|||
</el-upload> |
|||
<template #footer> |
|||
<div class="dialog-footer"> |
|||
<el-button type="primary" @click="submitFileForm">确 定</el-button> |
|||
<el-button @click="upload.open = false">取 消</el-button> |
|||
</div> |
|||
</template> |
|||
</el-dialog> |
|||
</div> |
|||
</template> |
|||
|
|||
<script setup name="User" lang="ts"> |
|||
import api from '@/api/system/user'; |
|||
import {UserForm, UserQuery, UserVO} from '@/api/system/user/types'; |
|||
import {DeptTreeVO, DeptVO} from '@/api/system/dept/types'; |
|||
import {RoleVO} from '@/api/system/role/types'; |
|||
import {PostVO} from '@/api/system/post/types'; |
|||
import {globalHeaders} from '@/utils/request'; |
|||
import {to} from 'await-to-js'; |
|||
import {optionselect} from '@/api/system/post'; |
|||
import {checkPermi} from '@/utils/permission'; |
|||
import {useUserStore} from '@/store/modules/user'; |
|||
|
|||
const router = useRouter(); |
|||
const {proxy} = getCurrentInstance() as ComponentInternalInstance; |
|||
const {sys_normal_disable, sys_user_sex} = toRefs<any>(proxy?.useDict('sys_normal_disable', 'sys_user_sex')); |
|||
const userList = ref<UserVO[]>(); |
|||
const loading = ref(true); |
|||
const showSearch = ref(true); |
|||
const ids = ref<Array<number | string>>([]); |
|||
const single = ref(true); |
|||
const multiple = ref(true); |
|||
const total = ref(0); |
|||
const dateRange = ref<[DateModelType, DateModelType]>(['', '']); |
|||
const deptName = ref(''); |
|||
const deptOptions = ref<DeptTreeVO[]>([]); |
|||
const enabledDeptOptions = ref<DeptTreeVO[]>([]); |
|||
const initPassword = ref<string>(''); |
|||
const postOptions = ref<PostVO[]>([]); |
|||
const roleOptions = ref<RoleVO[]>([]); |
|||
/*** 用户导入参数 */ |
|||
const upload = reactive<ImportOption>({ |
|||
// 是否显示弹出层(用户导入) |
|||
open: false, |
|||
// 弹出层标题(用户导入) |
|||
title: '', |
|||
// 是否禁用上传 |
|||
isUploading: false, |
|||
// 是否更新已经存在的用户数据 |
|||
updateSupport: 0, |
|||
// 设置上传的请求头部 |
|||
headers: globalHeaders(), |
|||
// 上传的地址 |
|||
url: import.meta.env.VITE_APP_BASE_API + '/system/user/importData' |
|||
}); |
|||
// 列显隐信息 |
|||
const columns = ref<FieldOption[]>([ |
|||
{key: 0, label: `用户编号`, visible: false, children: []}, |
|||
{key: 1, label: `用户名称`, visible: true, children: []}, |
|||
{key: 2, label: `用户昵称`, visible: true, children: []}, |
|||
{key: 3, label: `部门`, visible: true, children: []}, |
|||
{key: 4, label: `手机号码`, visible: true, children: []}, |
|||
{key: 5, label: `状态`, visible: true, children: []}, |
|||
{key: 6, label: `创建时间`, visible: true, children: []} |
|||
]); |
|||
|
|||
const deptTreeRef = ref<ElTreeInstance>(); |
|||
const deptExpandedKeys = ref<(string | number)[]>([]); |
|||
|
|||
/** 收集前 N 层节点的 id,用于默认展开 */ |
|||
const collectExpandKeys = (nodes: DeptTreeVO[], currentLevel: number, maxLevel: number) => { |
|||
if (currentLevel > maxLevel) return; |
|||
for (const node of nodes) { |
|||
deptExpandedKeys.value.push(node.id); |
|||
if (node.children?.length) { |
|||
collectExpandKeys(node.children, currentLevel + 1, maxLevel); |
|||
} |
|||
} |
|||
}; |
|||
const queryFormRef = ref<ElFormInstance>(); |
|||
const userFormRef = ref<ElFormInstance>(); |
|||
const uploadRef = ref<ElUploadInstance>(); |
|||
const formDialogRef = ref<ElDialogInstance>(); |
|||
|
|||
const dialog = reactive<DialogOption>({ |
|||
visible: false, |
|||
title: '' |
|||
}); |
|||
|
|||
const initFormData: UserForm = { |
|||
userId: undefined, |
|||
deptId: undefined, |
|||
userName: '', |
|||
nickName: undefined, |
|||
password: '', |
|||
phonenumber: undefined, |
|||
email: undefined, |
|||
sex: undefined, |
|||
status: '0', |
|||
remark: '', |
|||
postIds: [], |
|||
roleIds: [] |
|||
}; |
|||
|
|||
const initData: PageData<UserForm, UserQuery> = { |
|||
form: {...initFormData}, |
|||
queryParams: { |
|||
pageNum: 1, |
|||
pageSize: 10, |
|||
userName: '', |
|||
phonenumber: '', |
|||
status: '', |
|||
deptId: '', |
|||
roleId: '' |
|||
}, |
|||
rules: { |
|||
userName: [ |
|||
{required: true, message: '用户名称不能为空', trigger: 'blur'}, |
|||
{ |
|||
min: 2, |
|||
max: 20, |
|||
message: '用户名称长度必须介于 2 和 20 之间', |
|||
trigger: 'blur' |
|||
} |
|||
], |
|||
nickName: [{required: true, message: '用户昵称不能为空', trigger: 'blur'}], |
|||
password: [ |
|||
{required: true, message: '用户密码不能为空', trigger: 'blur'}, |
|||
{ |
|||
min: 5, |
|||
max: 20, |
|||
message: '用户密码长度必须介于 5 和 20 之间', |
|||
trigger: 'blur' |
|||
}, |
|||
{pattern: /^[^<>"'|\\]+$/, message: '不能包含非法字符:< > " \' \\ |', trigger: 'blur'} |
|||
], |
|||
email: [ |
|||
{ |
|||
type: 'email', |
|||
message: '请输入正确的邮箱地址', |
|||
trigger: ['blur', 'change'] |
|||
} |
|||
], |
|||
phonenumber: [ |
|||
{ |
|||
pattern: /^1[3456789][0-9]\d{8}$/, |
|||
message: '请输入正确的手机号码', |
|||
trigger: 'blur' |
|||
} |
|||
], |
|||
roleIds: [{required: true, message: '用户角色不能为空', trigger: 'blur'}] |
|||
} |
|||
}; |
|||
const data = reactive<PageData<UserForm, UserQuery>>(initData); |
|||
|
|||
const {queryParams, form, rules} = toRefs<PageData<UserForm, UserQuery>>(data); |
|||
|
|||
/** 通过条件过滤节点 */ |
|||
const filterNode = (value: string, data: any) => { |
|||
if (!value) return true; |
|||
return data.label.indexOf(value) !== -1; |
|||
}; |
|||
/** 根据名称筛选部门树 */ |
|||
watchEffect( |
|||
() => { |
|||
deptTreeRef.value?.filter(deptName.value); |
|||
}, |
|||
{ |
|||
flush: 'post' // watchEffect会在DOM挂载或者更新之前就会触发,此属性控制在DOM元素更新后运行 |
|||
} |
|||
); |
|||
|
|||
/** 查询用户列表 */ |
|||
const getList = async () => { |
|||
loading.value = true; |
|||
const res = await api.listUser(proxy?.addDateRange(queryParams.value, dateRange.value)); |
|||
loading.value = false; |
|||
userList.value = res.rows; |
|||
total.value = res.total; |
|||
}; |
|||
|
|||
/** 查询部门下拉树结构 */ |
|||
const getDeptTree = async () => { |
|||
const res = await api.deptTreeSelect(); |
|||
deptOptions.value = res.data; |
|||
enabledDeptOptions.value = filterDisabledDept(res.data); |
|||
deptExpandedKeys.value = []; |
|||
collectExpandKeys(res.data, 1, 2); |
|||
}; |
|||
|
|||
/** 过滤禁用的部门 */ |
|||
const filterDisabledDept = (deptList: DeptTreeVO[]) => { |
|||
return deptList.filter((dept) => { |
|||
if (dept.disabled) { |
|||
return false; |
|||
} |
|||
if (dept.children && dept.children.length) { |
|||
dept.children = filterDisabledDept(dept.children); |
|||
} |
|||
return true; |
|||
}); |
|||
}; |
|||
|
|||
/** 节点单击事件 */ |
|||
const handleNodeClick = (data: DeptVO) => { |
|||
queryParams.value.deptId = data.id; |
|||
handleQuery(); |
|||
}; |
|||
|
|||
/** 搜索按钮操作 */ |
|||
const handleQuery = () => { |
|||
queryParams.value.pageNum = 1; |
|||
getList(); |
|||
}; |
|||
/** 重置按钮操作 */ |
|||
const resetQuery = () => { |
|||
dateRange.value = ['', '']; |
|||
queryFormRef.value?.resetFields(); |
|||
queryParams.value.pageNum = 1; |
|||
queryParams.value.deptId = undefined; |
|||
deptTreeRef.value?.setCurrentKey(undefined); |
|||
handleQuery(); |
|||
}; |
|||
|
|||
/** 删除按钮操作 */ |
|||
const handleDelete = async (row?: UserVO) => { |
|||
const userIds = row?.userId || ids.value; |
|||
const [err] = await to(proxy?.$modal.confirm('是否确认删除用户编号为"' + userIds + '"的数据项?') as any); |
|||
if (!err) { |
|||
await api.delUser(userIds); |
|||
await getList(); |
|||
proxy?.$modal.msgSuccess('删除成功'); |
|||
} |
|||
}; |
|||
|
|||
/** 用户状态修改 */ |
|||
const handleStatusChange = async (row: UserVO) => { |
|||
const text = row.status === '0' ? '启用' : '停用'; |
|||
try { |
|||
await proxy?.$modal.confirm('确认要"' + text + '""' + row.userName + '"用户吗?'); |
|||
await api.changeUserStatus(row.userId, row.status); |
|||
proxy?.$modal.msgSuccess(text + '成功'); |
|||
} catch (err) { |
|||
row.status = row.status === '0' ? '1' : '0'; |
|||
} |
|||
}; |
|||
/** 跳转角色分配 */ |
|||
const handleAuthRole = (row: UserVO) => { |
|||
const userId = row.userId; |
|||
router.push('/system/user-auth/role/' + userId); |
|||
}; |
|||
|
|||
/** 重置密码按钮操作 */ |
|||
const handleResetPwd = async (row: UserVO) => { |
|||
const [err, res] = await to( |
|||
ElMessageBox.prompt('请输入"' + row.userName + '"的新密码', '提示', { |
|||
confirmButtonText: '确定', |
|||
cancelButtonText: '取消', |
|||
closeOnClickModal: false, |
|||
inputPattern: /^.{5,20}$/, |
|||
inputErrorMessage: '用户密码长度必须介于 5 和 20 之间', |
|||
inputValidator: (value) => { |
|||
if (/<|>|"|'|\||\\/.test(value)) { |
|||
return '不能包含非法字符:< > " \' \\ |'; |
|||
} |
|||
} |
|||
}) |
|||
); |
|||
if (!err && res) { |
|||
await api.resetUserPwd(row.userId, res.value); |
|||
proxy?.$modal.msgSuccess('修改成功,新密码是:' + res.value); |
|||
} |
|||
}; |
|||
|
|||
/** 选择条数 */ |
|||
const handleSelectionChange = (selection: UserVO[]) => { |
|||
ids.value = selection.map((item) => item.userId); |
|||
single.value = selection.length != 1; |
|||
multiple.value = !selection.length; |
|||
}; |
|||
|
|||
/** 导入按钮操作 */ |
|||
const handleImport = () => { |
|||
upload.title = '用户导入'; |
|||
upload.open = true; |
|||
}; |
|||
/** 导出按钮操作 */ |
|||
const handleExport = () => { |
|||
proxy?.download( |
|||
'system/user/export', |
|||
{ |
|||
...queryParams.value |
|||
}, |
|||
`user_${new Date().getTime()}.xlsx` |
|||
); |
|||
}; |
|||
/** 下载模板操作 */ |
|||
const importTemplate = () => { |
|||
proxy?.download('system/user/importTemplate', {}, `user_template_${new Date().getTime()}.xlsx`); |
|||
}; |
|||
|
|||
/**文件上传中处理 */ |
|||
const handleFileUploadProgress = () => { |
|||
upload.isUploading = true; |
|||
}; |
|||
/** 文件上传成功处理 */ |
|||
const handleFileSuccess = (response: any, file: UploadFile) => { |
|||
upload.open = false; |
|||
upload.isUploading = false; |
|||
uploadRef.value?.handleRemove(file); |
|||
ElMessageBox.alert("<div style='overflow: auto;overflow-x: hidden;max-height: 70vh;padding: 10px 20px 0;'>" + response.msg + '</div>', '导入结果', { |
|||
dangerouslyUseHTMLString: true |
|||
}); |
|||
getList(); |
|||
}; |
|||
|
|||
/** 提交上传文件 */ |
|||
function submitFileForm() { |
|||
uploadRef.value?.submit(); |
|||
} |
|||
|
|||
/** 重置操作表单 */ |
|||
const reset = () => { |
|||
form.value = {...initFormData}; |
|||
userFormRef.value?.resetFields(); |
|||
}; |
|||
/** 取消按钮 */ |
|||
const cancel = () => { |
|||
dialog.visible = false; |
|||
reset(); |
|||
}; |
|||
|
|||
/** 新增按钮操作 */ |
|||
const handleAdd = async () => { |
|||
reset(); |
|||
const {data} = await api.getUser(); |
|||
dialog.visible = true; |
|||
dialog.title = '新增用户'; |
|||
postOptions.value = data.posts; |
|||
roleOptions.value = data.roles; |
|||
form.value.password = initPassword.value.toString(); |
|||
}; |
|||
|
|||
/** 修改按钮操作 */ |
|||
const handleUpdate = async (row?: UserForm) => { |
|||
reset(); |
|||
const userId = row?.userId || ids.value[0]; |
|||
const {data} = await api.getUser(userId); |
|||
dialog.visible = true; |
|||
dialog.title = '修改用户'; |
|||
Object.assign(form.value, data.user); |
|||
postOptions.value = data.posts; |
|||
roleOptions.value = Array.from( |
|||
new Map([...data.roles, ...data.user.roles].map(role => [role.roleId, role])).values() |
|||
); |
|||
form.value.postIds = data.postIds; |
|||
form.value.roleIds = data.roleIds; |
|||
form.value.password = ''; |
|||
}; |
|||
|
|||
/** 提交按钮 */ |
|||
const submitForm = () => { |
|||
userFormRef.value?.validate(async (valid: boolean) => { |
|||
if (valid) { |
|||
if (form.value.userId) { |
|||
// 自己编辑自己的情况下 不允许编辑角色部门岗位 |
|||
if (form.value.userId == useUserStore().userId) { |
|||
form.value.roleIds = null; |
|||
form.value.deptId = null; |
|||
form.value.postIds = null; |
|||
} |
|||
await api.updateUser(form.value); |
|||
} else { |
|||
await api.addUser(form.value); |
|||
} |
|||
proxy?.$modal.msgSuccess('操作成功'); |
|||
dialog.visible = false; |
|||
await getList(); |
|||
} |
|||
}); |
|||
}; |
|||
|
|||
/** |
|||
* 关闭用户弹窗 |
|||
*/ |
|||
const closeDialog = () => { |
|||
dialog.visible = false; |
|||
resetForm(); |
|||
}; |
|||
|
|||
/** |
|||
* 重置表单 |
|||
*/ |
|||
const resetForm = () => { |
|||
userFormRef.value?.resetFields(); |
|||
userFormRef.value?.clearValidate(); |
|||
|
|||
form.value.id = undefined; |
|||
form.value.status = '1'; |
|||
}; |
|||
onMounted(() => { |
|||
getDeptTree(); // 初始化部门数据 |
|||
getList(); // 初始化列表数据 |
|||
proxy?.getConfigKey('sys.user.initPassword').then((response) => { |
|||
initPassword.value = response.data; |
|||
}); |
|||
}); |
|||
|
|||
async function handleDeptChange(value: number | string) { |
|||
const response = await optionselect(value); |
|||
postOptions.value = response.data; |
|||
form.value.postIds = []; |
|||
} |
|||
</script> |
|||
@ -0,0 +1,122 @@ |
|||
<template> |
|||
<div class="p-2"> |
|||
<el-row :gutter="20"> |
|||
<el-col :span="6" :xs="24"> |
|||
<el-card class="box-card"> |
|||
<template #header> |
|||
<div class="clearfix"> |
|||
<span>个人信息</span> |
|||
</div> |
|||
</template> |
|||
<div> |
|||
<div class="text-center"> |
|||
<userAvatar /> |
|||
</div> |
|||
<ul class="list-group list-group-striped"> |
|||
<li class="list-group-item"> |
|||
<svg-icon icon-class="user" />用户名称 |
|||
<div class="pull-right">{{ state.user.userName }}</div> |
|||
</li> |
|||
<li class="list-group-item"> |
|||
<svg-icon icon-class="phone" />手机号码 |
|||
<div class="pull-right">{{ state.user.phonenumber }}</div> |
|||
</li> |
|||
<li class="list-group-item"> |
|||
<svg-icon icon-class="email" />用户邮箱 |
|||
<div class="pull-right">{{ state.user.email }}</div> |
|||
</li> |
|||
<li class="list-group-item"> |
|||
<svg-icon icon-class="tree" />所属部门 |
|||
<div v-if="state.user.deptName" class="pull-right">{{ state.user.deptName }} / {{ state.postGroup }}</div> |
|||
</li> |
|||
<li class="list-group-item"> |
|||
<svg-icon icon-class="peoples" />所属角色 |
|||
<div class="pull-right">{{ state.roleGroup }}</div> |
|||
</li> |
|||
<li class="list-group-item"> |
|||
<svg-icon icon-class="date" />创建日期 |
|||
<div class="pull-right">{{ state.user.createTime }}</div> |
|||
</li> |
|||
</ul> |
|||
</div> |
|||
</el-card> |
|||
</el-col> |
|||
<el-col :span="18" :xs="24"> |
|||
<el-card> |
|||
<template #header> |
|||
<div class="clearfix"> |
|||
<span>基本资料</span> |
|||
</div> |
|||
</template> |
|||
<el-tabs v-model="activeTab"> |
|||
<el-tab-pane label="基本资料" name="userinfo"> |
|||
<userInfo :user="userForm" /> |
|||
</el-tab-pane> |
|||
<el-tab-pane label="修改密码" name="resetPwd"> |
|||
<resetPwd /> |
|||
</el-tab-pane> |
|||
<el-tab-pane label="第三方应用" name="thirdParty"> |
|||
<thirdParty :auths="state.auths" /> |
|||
</el-tab-pane> |
|||
<el-tab-pane label="在线设备" name="onlineDevice"> |
|||
<onlineDevice :devices="state.devices" /> |
|||
</el-tab-pane> |
|||
</el-tabs> |
|||
</el-card> |
|||
</el-col> |
|||
</el-row> |
|||
</div> |
|||
</template> |
|||
|
|||
<script setup name="Profile" lang="ts"> |
|||
import UserAvatar from './userAvatar.vue'; |
|||
import UserInfo from './userInfo.vue'; |
|||
import ResetPwd from './resetPwd.vue'; |
|||
import ThirdParty from './thirdParty.vue'; |
|||
import OnlineDevice from './onlineDevice.vue'; |
|||
import { getAuthList } from '@/api/system/social/auth'; |
|||
import { getUserProfile } from '@/api/system/user'; |
|||
import { getOnline } from '@/api/monitor/online'; |
|||
import { UserVO } from '@/api/system/user/types'; |
|||
|
|||
const activeTab = ref('userinfo'); |
|||
interface State { |
|||
user: Partial<UserVO>; |
|||
roleGroup: string; |
|||
postGroup: string; |
|||
auths: any; |
|||
devices: any; |
|||
} |
|||
const state = ref<State>({ |
|||
user: {}, |
|||
roleGroup: '', |
|||
postGroup: '', |
|||
auths: [], |
|||
devices: [] |
|||
}); |
|||
|
|||
const userForm = ref({}); |
|||
|
|||
const getUser = async () => { |
|||
const res = await getUserProfile(); |
|||
state.value.user = res.data.user; |
|||
userForm.value = { ...res.data.user }; |
|||
state.value.roleGroup = res.data.roleGroup; |
|||
state.value.postGroup = res.data.postGroup; |
|||
}; |
|||
|
|||
const getAuths = async () => { |
|||
const res = await getAuthList(); |
|||
state.value.auths = res.data; |
|||
}; |
|||
const getOnlines = async () => { |
|||
const res = await getOnline(); |
|||
state.value.devices = res.rows; |
|||
}; |
|||
|
|||
onMounted(() => { |
|||
getUser(); |
|||
getAuths(); |
|||
getOnlines(); |
|||
}); |
|||
</script> |
|||
@ -0,0 +1,57 @@ |
|||
<template> |
|||
<div> |
|||
<el-table :data="devices" border style="width: 100%; height: 100%; font-size: 14px"> |
|||
<el-table-column label="设备类型" align="center"> |
|||
<template #default="scope"> |
|||
<dict-tag :options="sys_device_type" :value="scope.row.deviceType" /> |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column label="主机" align="center" prop="ipaddr" :show-overflow-tooltip="true" /> |
|||
<el-table-column label="登录地点" align="center" prop="loginLocation" :show-overflow-tooltip="true" /> |
|||
<el-table-column label="操作系统" align="center" prop="os" :show-overflow-tooltip="true" /> |
|||
<el-table-column label="浏览器" align="center" prop="browser" :show-overflow-tooltip="true" /> |
|||
<el-table-column label="登录时间" align="center" prop="loginTime" width="180"> |
|||
<template #default="scope"> |
|||
<span>{{ proxy.parseTime(scope.row.loginTime) }}</span> |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width"> |
|||
<template #default="scope"> |
|||
<el-tooltip content="删除" placement="top"> |
|||
<el-button link type="primary" icon="Delete" @click="handldDelOnline(scope.row)"> </el-button> |
|||
</el-tooltip> |
|||
</template> |
|||
</el-table-column> |
|||
</el-table> |
|||
</div> |
|||
</template> |
|||
|
|||
<script setup name="Online" lang="ts"> |
|||
import { delOnline } from '@/api/monitor/online'; |
|||
import { propTypes } from '@/utils/propTypes'; |
|||
|
|||
const { proxy } = getCurrentInstance() as ComponentInternalInstance; |
|||
const { sys_device_type } = toRefs<any>(proxy?.useDict('sys_device_type')); |
|||
|
|||
const props = defineProps({ |
|||
devices: propTypes.any.isRequired |
|||
}); |
|||
const devices = computed(() => props.devices); |
|||
|
|||
/** 删除按钮操作 */ |
|||
const handldDelOnline = (row: any) => { |
|||
ElMessageBox.confirm('删除设备后,在该设备登录需要重新进行验证') |
|||
.then(() => { |
|||
return delOnline(row.tokenId); |
|||
}) |
|||
.then((res: any) => { |
|||
if (res.code === 200) { |
|||
proxy?.$modal.msgSuccess('删除成功'); |
|||
proxy?.$tab.refreshPage(); |
|||
} else { |
|||
proxy?.$modal.msgError(res.msg); |
|||
} |
|||
}) |
|||
.catch(() => {}); |
|||
}; |
|||
</script> |
|||
@ -0,0 +1,73 @@ |
|||
<template> |
|||
<el-form ref="pwdRef" :model="user" :rules="rules" label-width="80px"> |
|||
<el-form-item label="旧密码" prop="oldPassword"> |
|||
<el-input v-model="user.oldPassword" placeholder="请输入旧密码" type="password" show-password /> |
|||
</el-form-item> |
|||
<el-form-item label="新密码" prop="newPassword"> |
|||
<el-input v-model="user.newPassword" placeholder="请输入新密码" type="password" show-password /> |
|||
</el-form-item> |
|||
<el-form-item label="确认密码" prop="confirmPassword"> |
|||
<el-input v-model="user.confirmPassword" placeholder="请确认新密码" type="password" show-password /> |
|||
</el-form-item> |
|||
<el-form-item> |
|||
<el-button type="primary" @click="submit">保存</el-button> |
|||
<el-button type="danger" @click="close">关闭</el-button> |
|||
</el-form-item> |
|||
</el-form> |
|||
</template> |
|||
|
|||
<script setup lang="ts"> |
|||
import { updateUserPwd } from '@/api/system/user'; |
|||
import type { ResetPwdForm } from '@/api/system/user/types'; |
|||
|
|||
const { proxy } = getCurrentInstance() as ComponentInternalInstance; |
|||
const pwdRef = ref<ElFormInstance>(); |
|||
const user = ref<ResetPwdForm>({ |
|||
oldPassword: '', |
|||
newPassword: '', |
|||
confirmPassword: '' |
|||
}); |
|||
|
|||
const equalToPassword = (rule: any, value: string, callback: any) => { |
|||
if (user.value.newPassword !== value) { |
|||
callback(new Error('两次输入的密码不一致')); |
|||
} else { |
|||
callback(); |
|||
} |
|||
}; |
|||
const rules = ref({ |
|||
oldPassword: [{ required: true, message: '旧密码不能为空', trigger: 'blur' }], |
|||
newPassword: [ |
|||
{ required: true, message: '新密码不能为空', trigger: 'blur' }, |
|||
{ |
|||
min: 6, |
|||
max: 20, |
|||
message: '长度在 6 到 20 个字符', |
|||
trigger: 'blur' |
|||
}, |
|||
{ pattern: /^[^<>"'|\\]+$/, message: '不能包含非法字符:< > " \' \\ |', trigger: 'blur' } |
|||
], |
|||
confirmPassword: [ |
|||
{ required: true, message: '确认密码不能为空', trigger: 'blur' }, |
|||
{ |
|||
required: true, |
|||
validator: equalToPassword, |
|||
trigger: 'blur' |
|||
} |
|||
] |
|||
}); |
|||
|
|||
/** 提交按钮 */ |
|||
const submit = () => { |
|||
pwdRef.value?.validate(async (valid: boolean) => { |
|||
if (valid) { |
|||
await updateUserPwd(user.value.oldPassword, user.value.newPassword); |
|||
proxy?.$modal.msgSuccess('修改成功'); |
|||
} |
|||
}); |
|||
}; |
|||
/** 关闭按钮 */ |
|||
const close = () => { |
|||
proxy?.$tab.closePage(); |
|||
}; |
|||
</script> |
|||
@ -0,0 +1,145 @@ |
|||
<template> |
|||
<div> |
|||
<el-table :data="auths" border style="width: 100%; height: 100%; font-size: 14px"> |
|||
<el-table-column label="序号" width="50" type="index" /> |
|||
<el-table-column label="绑定账号平台" width="140" align="center" prop="source" show-overflow-tooltip /> |
|||
<el-table-column label="头像" width="120" align="center" prop="avatar"> |
|||
<template #default="scope"> |
|||
<img :src="scope.row.avatar" style="width: 45px; height: 45px" /> |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column label="系统账号" width="180" align="center" prop="userName" :show-overflow-tooltip="true" /> |
|||
<el-table-column label="绑定时间" width="180" align="center" prop="createTime" /> |
|||
<el-table-column label="操作" width="80" align="center" class-name="small-padding fixed-width"> |
|||
<template #default="scope"> |
|||
<el-button size="small" type="text" @click="unlockAuth(scope.row)">解绑</el-button> |
|||
</template> |
|||
</el-table-column> |
|||
</el-table> |
|||
|
|||
<div id="git-user-binding"> |
|||
<h4 class="provider-desc">你可以绑定以下第三方帐号</h4> |
|||
<div id="authlist" class="user-bind"> |
|||
<a class="third-app" href="#" title="使用 微信 账号授权登录" @click="authUrl('wechat')"> |
|||
<div class="git-other-login-icon"> |
|||
<svg-icon icon-class="wechat" /> |
|||
</div> |
|||
<span class="app-name">WeiXin</span> |
|||
</a> |
|||
<a class="third-app" href="#" title="使用 MaxKey 账号授权登录" @click="authUrl('maxkey')"> |
|||
<div class="git-other-login-icon"> |
|||
<svg-icon icon-class="maxkey" /> |
|||
</div> |
|||
<span class="app-name">MaxKey</span> |
|||
</a> |
|||
<a class="third-app" href="#" title="使用 TopIam 账号授权登录" @click="authUrl('topiam')"> |
|||
<div class="git-other-login-icon"> |
|||
<svg-icon icon-class="topiam" /> |
|||
</div> |
|||
<span class="app-name">TopIam</span> |
|||
</a> |
|||
<a class="third-app" href="#" title="使用 Gitee 账号授权登录" @click="authUrl('gitee')"> |
|||
<div class="git-other-login-icon"> |
|||
<svg-icon icon-class="gitee" /> |
|||
</div> |
|||
<span class="app-name">Gitee</span> |
|||
</a> |
|||
<a class="third-app" href="#" title="使用 GitHub 账号授权登录" @click="authUrl('github')"> |
|||
<div class="git-other-login-icon"> |
|||
<svg-icon icon-class="github" /> |
|||
</div> |
|||
<span class="app-name">Github</span> |
|||
</a> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<script setup lang="ts"> |
|||
import { authUnlock, authRouterUrl } from '@/api/system/social/auth'; |
|||
import { propTypes } from '@/utils/propTypes'; |
|||
import { useUserStore } from '@/store/modules/user'; |
|||
|
|||
const { proxy } = getCurrentInstance() as ComponentInternalInstance; |
|||
|
|||
const props = defineProps({ |
|||
auths: propTypes.any.isRequired |
|||
}); |
|||
const auths = computed(() => props.auths); |
|||
|
|||
const unlockAuth = (row: any) => { |
|||
ElMessageBox.confirm('您确定要解除"' + row.source + '"的账号绑定吗?') |
|||
.then(() => { |
|||
return authUnlock(row.id); |
|||
}) |
|||
.then((res: any) => { |
|||
if (res.code === 200) { |
|||
proxy?.$modal.msgSuccess('解绑成功'); |
|||
proxy?.$tab.refreshPage(); |
|||
} else { |
|||
proxy?.$modal.msgError(res.msg); |
|||
} |
|||
}) |
|||
.catch(() => {}); |
|||
}; |
|||
|
|||
const authUrl = (source: string) => { |
|||
authRouterUrl(source, useUserStore().tenantId).then((res: any) => { |
|||
if (res.code === 200) { |
|||
window.location.href = res.data; |
|||
} else { |
|||
proxy?.$modal.msgError(res.msg); |
|||
} |
|||
}); |
|||
}; |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
.user-bind .third-app { |
|||
display: -webkit-box; |
|||
display: -ms-flexbox; |
|||
display: flex; |
|||
-webkit-box-orient: vertical; |
|||
-webkit-box-direction: normal; |
|||
-ms-flex-direction: column; |
|||
flex-direction: column; |
|||
-webkit-box-align: center; |
|||
-ms-flex-align: center; |
|||
align-items: center; |
|||
min-width: 80px; |
|||
float: left; |
|||
} |
|||
|
|||
.user-bind { |
|||
font-size: 1rem; |
|||
text-align: start; |
|||
height: 50px; |
|||
margin-top: 10px; |
|||
} |
|||
|
|||
.git-other-login-icon > img { |
|||
height: 32px; |
|||
} |
|||
|
|||
a { |
|||
text-decoration: none; |
|||
cursor: pointer; |
|||
color: #005980; |
|||
} |
|||
|
|||
.provider-desc { |
|||
font-family: |
|||
-apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Liberation Sans', |
|||
'PingFang SC', 'Microsoft YaHei', 'Hiragino Sans GB', 'Wenquanyi Micro Hei', 'WenQuanYi Zen Hei', 'ST Heiti', SimHei, SimSun, |
|||
'WenQuanYi Zen Hei Sharp', sans-serif; |
|||
font-size: 1.071rem; |
|||
} |
|||
|
|||
td > img { |
|||
height: 20px; |
|||
width: 20px; |
|||
display: inline-block; |
|||
border-radius: 50%; |
|||
margin-right: 5px; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,182 @@ |
|||
<template> |
|||
<div class="user-info-head" @click="editCropper()"> |
|||
<img :src="options.img" title="点击上传头像" class="img-circle img-lg" /> |
|||
<el-dialog v-model="open" :title="title" width="800px" append-to-body @opened="modalOpened" @close="closeDialog"> |
|||
<el-row> |
|||
<el-col :xs="24" :md="12" :style="{ height: '350px' }"> |
|||
<vue-cropper |
|||
v-if="visible" |
|||
ref="cropper" |
|||
:img="options.img" |
|||
:info="true" |
|||
:auto-crop="options.autoCrop" |
|||
:auto-crop-width="options.autoCropWidth" |
|||
:auto-crop-height="options.autoCropHeight" |
|||
:fixed-box="options.fixedBox" |
|||
:output-type="options.outputType" |
|||
@real-time="realTime" |
|||
/> |
|||
</el-col> |
|||
<el-col :xs="24" :md="12" :style="{ height: '350px' }"> |
|||
<div class="avatar-upload-preview"> |
|||
<img :src="options.previews.url" :style="options.previews.img" /> |
|||
</div> |
|||
</el-col> |
|||
</el-row> |
|||
<br /> |
|||
<el-row> |
|||
<el-col :lg="2" :md="2"> |
|||
<el-upload action="#" :http-request="requestUpload" :show-file-list="false" :before-upload="beforeUpload"> |
|||
<el-button> |
|||
选择 |
|||
<el-icon class="el-icon--right"> |
|||
<Upload /> |
|||
</el-icon> |
|||
</el-button> |
|||
</el-upload> |
|||
</el-col> |
|||
<el-col :lg="{ span: 1, offset: 2 }" :md="2"> |
|||
<el-button icon="Plus" @click="changeScale(1)"></el-button> |
|||
</el-col> |
|||
<el-col :lg="{ span: 1, offset: 1 }" :md="2"> |
|||
<el-button icon="Minus" @click="changeScale(-1)"></el-button> |
|||
</el-col> |
|||
<el-col :lg="{ span: 1, offset: 1 }" :md="2"> |
|||
<el-button icon="RefreshLeft" @click="rotateLeft()"></el-button> |
|||
</el-col> |
|||
<el-col :lg="{ span: 1, offset: 1 }" :md="2"> |
|||
<el-button icon="RefreshRight" @click="rotateRight()"></el-button> |
|||
</el-col> |
|||
<el-col :lg="{ span: 2, offset: 6 }" :md="2"> |
|||
<el-button type="primary" @click="uploadImg()">提 交</el-button> |
|||
</el-col> |
|||
</el-row> |
|||
</el-dialog> |
|||
</div> |
|||
</template> |
|||
|
|||
<script setup lang="ts"> |
|||
import 'vue-cropper/dist/index.css'; |
|||
import { VueCropper } from 'vue-cropper'; |
|||
import { uploadAvatar } from '@/api/system/user'; |
|||
import { useUserStore } from '@/store/modules/user'; |
|||
import { UploadRawFile } from 'element-plus'; |
|||
|
|||
interface Options { |
|||
img: string | any; // 裁剪图片的地址 |
|||
autoCrop: boolean; // 是否默认生成截图框 |
|||
autoCropWidth: number; // 默认生成截图框宽度 |
|||
autoCropHeight: number; // 默认生成截图框高度 |
|||
fixedBox: boolean; // 固定截图框大小 不允许改变 |
|||
fileName: string; |
|||
previews: any; // 预览数据 |
|||
outputType: string; |
|||
visible: boolean; |
|||
} |
|||
|
|||
const userStore = useUserStore(); |
|||
const { proxy } = getCurrentInstance() as ComponentInternalInstance; |
|||
|
|||
const open = ref(false); |
|||
const visible = ref(false); |
|||
const title = ref('修改头像'); |
|||
|
|||
const cropper = ref<any>({}); |
|||
//图片裁剪数据 |
|||
const options = reactive<Options>({ |
|||
img: userStore.avatar, |
|||
autoCrop: true, |
|||
autoCropWidth: 200, |
|||
autoCropHeight: 200, |
|||
fixedBox: true, |
|||
outputType: 'png', |
|||
fileName: '', |
|||
previews: {}, |
|||
visible: false |
|||
}); |
|||
|
|||
/** 编辑头像 */ |
|||
const editCropper = () => { |
|||
open.value = true; |
|||
}; |
|||
/** 打开弹出层结束时的回调 */ |
|||
const modalOpened = () => { |
|||
visible.value = true; |
|||
}; |
|||
/** 覆盖默认上传行为 */ |
|||
const requestUpload = (): any => {}; |
|||
/** 向左旋转 */ |
|||
const rotateLeft = () => { |
|||
cropper.value.rotateLeft(); |
|||
}; |
|||
/** 向右旋转 */ |
|||
const rotateRight = () => { |
|||
cropper.value.rotateRight(); |
|||
}; |
|||
/** 图片缩放 */ |
|||
const changeScale = (num: number) => { |
|||
num = num || 1; |
|||
cropper.value.changeScale(num); |
|||
}; |
|||
/** 上传预处理 */ |
|||
const beforeUpload = (file: UploadRawFile): any => { |
|||
if (file.type.indexOf('image/') == -1) { |
|||
proxy?.$modal.msgError('文件格式错误,请上传图片类型,如:JPG,PNG后缀的文件。'); |
|||
} else { |
|||
const reader = new FileReader(); |
|||
reader.readAsDataURL(file); |
|||
reader.onload = () => { |
|||
options.img = reader.result; |
|||
options.fileName = file.name; |
|||
}; |
|||
} |
|||
}; |
|||
/** 上传图片 */ |
|||
const uploadImg = async () => { |
|||
cropper.value.getCropBlob(async (data: any) => { |
|||
const formData = new FormData(); |
|||
formData.append('avatarfile', data, options.fileName); |
|||
const res = await uploadAvatar(formData); |
|||
open.value = false; |
|||
options.img = res.data.imgUrl; |
|||
userStore.setAvatar(options.img); |
|||
proxy?.$modal.msgSuccess('修改成功'); |
|||
visible.value = false; |
|||
}); |
|||
}; |
|||
/** 实时预览 */ |
|||
const realTime = (data: any) => { |
|||
options.previews = data; |
|||
}; |
|||
/** 关闭窗口 */ |
|||
const closeDialog = () => { |
|||
options.img = userStore.avatar; |
|||
options.visible = false; |
|||
}; |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
.user-info-head { |
|||
position: relative; |
|||
display: inline-block; |
|||
height: 120px; |
|||
} |
|||
|
|||
.user-info-head:hover:after { |
|||
content: '+'; |
|||
position: absolute; |
|||
left: 0; |
|||
right: 0; |
|||
top: 0; |
|||
bottom: 0; |
|||
color: #eee; |
|||
background: rgba(0, 0, 0, 0.5); |
|||
font-size: 24px; |
|||
font-style: normal; |
|||
-webkit-font-smoothing: antialiased; |
|||
-moz-osx-font-smoothing: grayscale; |
|||
cursor: pointer; |
|||
line-height: 110px; |
|||
border-radius: 50%; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,69 @@ |
|||
<template> |
|||
<el-form ref="userRef" :model="userForm" :rules="rules" label-width="80px"> |
|||
<el-form-item label="用户昵称" prop="nickName"> |
|||
<el-input v-model="userForm.nickName" maxlength="30" /> |
|||
</el-form-item> |
|||
<el-form-item label="手机号码" prop="phonenumber"> |
|||
<el-input v-model="userForm.phonenumber" maxlength="11" /> |
|||
</el-form-item> |
|||
<el-form-item label="邮箱" prop="email"> |
|||
<el-input v-model="userForm.email" maxlength="50" /> |
|||
</el-form-item> |
|||
<el-form-item label="性别"> |
|||
<el-radio-group v-model="userForm.sex"> |
|||
<el-radio value="0">男</el-radio> |
|||
<el-radio value="1">女</el-radio> |
|||
</el-radio-group> |
|||
</el-form-item> |
|||
<el-form-item> |
|||
<el-button type="primary" @click="submit">保存</el-button> |
|||
<el-button type="danger" @click="close">关闭</el-button> |
|||
</el-form-item> |
|||
</el-form> |
|||
</template> |
|||
|
|||
<script setup lang="ts"> |
|||
import { updateUserProfile } from '@/api/system/user'; |
|||
import { propTypes } from '@/utils/propTypes'; |
|||
|
|||
const props = defineProps({ |
|||
user: propTypes.any.isRequired |
|||
}); |
|||
const userForm = computed(() => props.user); |
|||
const { proxy } = getCurrentInstance() as ComponentInternalInstance; |
|||
const userRef = ref<ElFormInstance>(); |
|||
const rule: ElFormRules = { |
|||
nickName: [{ required: true, message: '用户昵称不能为空', trigger: 'blur' }], |
|||
email: [ |
|||
{ required: true, message: '邮箱地址不能为空', trigger: 'blur' }, |
|||
{ |
|||
type: 'email', |
|||
message: '请输入正确的邮箱地址', |
|||
trigger: ['blur', 'change'] |
|||
} |
|||
], |
|||
phonenumber: [ |
|||
{ |
|||
required: true, |
|||
message: '手机号码不能为空', |
|||
trigger: 'blur' |
|||
}, |
|||
{ pattern: /^1[3456789][0-9]\d{8}$/, message: '请输入正确的手机号码', trigger: 'blur' } |
|||
] |
|||
}; |
|||
const rules = ref<ElFormRules>(rule); |
|||
|
|||
/** 提交按钮 */ |
|||
const submit = () => { |
|||
userRef.value?.validate(async (valid: boolean) => { |
|||
if (valid) { |
|||
await updateUserProfile(props.user); |
|||
proxy?.$modal.msgSuccess('修改成功'); |
|||
} |
|||
}); |
|||
}; |
|||
/** 关闭按钮 */ |
|||
const close = () => { |
|||
proxy?.$tab.closePage(); |
|||
}; |
|||
</script> |
|||
Loading…
Reference in new issue