Compare commits

..

2 Commits

Author SHA1 Message Date
yuhaiming
257d984003 Merge branch 'ts' into merge-main-tmp
# Conflicts:
#	.gitignore
#	README.md
2026-07-01 14:34:23 +08:00
9de68cb01a Initial commit 2026-07-01 09:57:08 +08:00
38 changed files with 563 additions and 1844 deletions

View File

@@ -1,6 +1,6 @@
# 页面标题 # 页面标题
VITE_APP_TITLE = 驭鲲智防系统 VITE_APP_TITLE = 防御机管理系统
VITE_APP_LOGO_TITLE = 驭鲲智防系统 VITE_APP_LOGO_TITLE = 防御机管理系统
# 开发环境配置 # 开发环境配置
VITE_APP_ENV = 'development' VITE_APP_ENV = 'development'
@@ -20,11 +20,13 @@ VITE_APP_SNAILJOB_ADMIN = 'http://localhost:9002/snail-job'
VITE_APP_PORT = 80 VITE_APP_PORT = 80
# 接口加密功能开关(如需关闭 后端也必须对应关闭) # 接口加密功能开关(如需关闭 后端也必须对应关闭)
VITE_APP_ENCRYPT = true VITE_APP_ENCRYPT = false
# 接口加密传输 RSA 公钥与后端解密私钥对应 如更换需前后端一同更换 # 接口加密传输 RSA 公钥与后端解密私钥对应 如更换需前后端一同更换
VITE_APP_RSA_PUBLIC_KEY = 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtVIQm+1AWMfkT1uVOyugKQwBeHtgpulABOrVfaL7Xzc1/rKSdLdjYZLZnnOU+rdE1jqz20MXDBozK22spH/j53d9nFLftHkMCRUlfc0MWEqtSla+2MdzElIHEbq4CSizZOcT8QXCRw7V2OuDqnC7XRQmnpA5Ks1WaKH5V0aoKWztjsqZV6vVWPqyDC94mvu/Q/9e6KVfkqAzR+afuo9OoBFewDGQ6jfqyu1TxoGZrfSr5lXOVeADf0CVfWCDze5y4f7er4JjPHWA2+yv8Scd9bqVJ4Q+/yR2Gd516Mi11OgBKc8GdkuA4/kfmXc7n91SMjgQP6DlTVuPc/6KnB/pyQIDAQAB' #VITE_APP_RSA_PUBLIC_KEY = 'MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ=='
VITE_APP_RSA_PUBLIC_KEY = 'MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAJJA9FRt24dkoEqTRBdm9yx6MQ5+f+rDIrLdCAXFUtpGcsT+kea/iGzk50OfSzSYxQYF8g/iksanB9XN0Ab6anECAwEAAQ=='
# 接口响应解密 RSA 私钥与后端加密公钥对应 如更换需前后端一同更换 # 接口响应解密 RSA 私钥与后端加密公钥对应 如更换需前后端一同更换
VITE_APP_RSA_PRIVATE_KEY = 'MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDYg1dC9XDMRtKJ5IFj+Hd3bq7+r2YL1y8FM9HD111PN/YVy4K6O67cRA/TLG7+tl6eYiA73f5lzPgv1riOwBPSCmvVopxq2KiJs30uEnug0qGp6l2cwQOmjitFAQF8dOtAl4m6EM/jdXCZYbQrthJ6n3tR+vGj2IrGSJEjPrj3VRljK9I7Zw0Be0rhJn/vZw2CA4GoVfE/3gqa5bpl+3SFxMP7TwF5uZueCNqLaPnd7jK5jB1SUghI7+pq897Ng4EG6kdOGsiQe5kbHWkSPTmPEObMwgLJ18i/XCVg+LUkTDJOC/MAfB+WGcKryNHrhcbhB6+XstpoGT8JlOPOgh0BAgMBAAECggEBALZG7MH8bDguL7XTYHNPjRRJZJ4aAGlbgcR5edHMkEvPKyfyK16qPG5IBqKcN93Mnx/sMIL2Q+RkiVWNCdd0MbuU9m/m9JSnTkyPhYWyHc6pRV0NaD496Nrhud/gFuY2cI/yhArXeI1gI6mdrddW83u0pFfCUojEFyETmsz3UzU1LMkVbwCXNbZ1b3GfcVnONMn8T/7CMlRPlKyOuE0OPnTut4wddAybe5BKDQYnGzuosUdO+Q+2K3hqaTD6Bb33D4ATHjvR3Vt9itE07BiaBFjZkE2odP4x3fSsnc7Cw3A7zHQivpfgqcoBcGUaMcN1FdgpaF2mbe9/eHn/lYCpevkCgYEA4FoKq3fPOITAtA6bjwCHXvIQUjv4vQ0BZtt7OX7zwpRwqOhJfN0ZRkd7NMUqYnB10N9NgPRz0kKC6qff5duN+zkYIzjHrlm5lzdIEAouovRlrGXST4n8gAYteh5D5Or9wRXj7USQoaNyiWKB9A/07ortJvul09sPjJU7IVfzzXMCgYEA9w45Gh2bAvYvm2bWieKZRhcV6Pwy9R8587qmKLC7V/0gfVojBHx7xTmTEfKCGrt3fK4jB5v/2Dar8gDF+RM7g01u0YuFJt17jeFr/4o5KMimfXhoEulDbgjfvhxQOrtkrF5iHpKxiEXqIPiH+B7155PAnE43011AAJCBSylmTrsCgYEAkxr3PA9HFKwXHvklDtMt8BeQlBs2sd9BOAxZ9A1GECP86wPEi9b9p5NfOe6+J+XNmrOQwimHeCqcZPjGWpVnt35sUUv9wlia1Igu/DVw9vCBalUpXXYA1oE2eIg3xHZBBMYxuXXnz9S4WVT6GOoNlAwMDC+dQBi3TVrcdrSQ6/kCgYBA254ICh+ovmKvJGdMGY3thZ/94z+pdEIthyGZ6xOzvMMrxV8ODXQcycmfW4/mXrK2q6yMkdqvs2KejK9sfS0RgmGGZ19UXa7TB4vnsSziVRLIO7TuyggmufOrIBm74XhDfB+8MPykbt5RO43OiKo72mElZ69mMMPdohIfXkX6gwKBgH4L9qJRhNunWSfHO4/8dST+2meSV+Cpqf0b/9cV6v5FMS22m+szzW9XoBXnI9dbAqiEDlCHCLw34pRlC3OzBNlbT6HzNmYXLko2Ttg9thiDq7WS5tV+EHJRP6kLabYGq0Aghtq+6LIwwogbHzhRi8Q52o3CNmM9lThNWZIkzaBe' #VITE_APP_RSA_PRIVATE_KEY = 'MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAmc3CuPiGL/LcIIm7zryCEIbl1SPzBkr75E2VMtxegyZ1lYRD+7TZGAPkvIsBcaMs6Nsy0L78n2qh+lIZMpLH8wIDAQABAkEAk82Mhz0tlv6IVCyIcw/s3f0E+WLmtPFyR9/WtV3Y5aaejUkU60JpX4m5xNR2VaqOLTZAYjW8Wy0aXr3zYIhhQQIhAMfqR9oFdYw1J9SsNc+CrhugAvKTi0+BF6VoL6psWhvbAiEAxPPNTmrkmrXwdm/pQQu3UOQmc2vCZ5tiKpW10CgJi8kCIFGkL6utxw93Ncj4exE/gPLvKcT+1Emnoox+O9kRXss5AiAMtYLJDaLEzPrAWcZeeSgSIzbL+ecokmFKSDDcRske6QIgSMkHedwND1olF8vlKsJUGK3BcdtM8w4Xq7BpSBwsloE='
VITE_APP_RSA_PRIVATE_KEY = 'MIIBOgIBAAJBAIHPXmO+vf9u33iXv97aYeuPsJ7Ut+GdBjpT7JOIEE1rIEIfaKK268uPwAHW+/fNOKQiTBuunI4PAiL7CAQoVeECAwEAAQJACS6S3aP9/9W8MY/XqZAcQbOjPvOK94bKNvv9YvN1O785/2U5z7hUHvF8O8qIttlMNqhJ60JO9x58EUE51/dZ1QIhAISwFmIah9HoOLR5M+sBsUb5dIAm1mg0hj7T/5LyMTO7AiEA+nKebopGVz7LVkX53m29975r3T+DYu9n24eIqNwTDRMCIHn6xxj4+tNlgHuo0KnCrI5q1UbabVDuuFsZ2WHldIXxAiEAx6BA5H9XHgexcC0Kw97ZgvXHaG1NvrhxIizgIyh4Dv0CIB+VslCqTQTr9idyfVuURAiDdzg/9tm07GhWLDn6I6+E'
# 客户端id # 客户端id
VITE_APP_CLIENT_ID = 'e5cd7e4891bf95d1d19206ce24a7b32e' VITE_APP_CLIENT_ID = 'e5cd7e4891bf95d1d19206ce24a7b32e'
@@ -34,4 +36,3 @@ VITE_APP_WEBSOCKET = false
# sse 开关 # sse 开关
VITE_APP_SSE = true VITE_APP_SSE = true

View File

@@ -1,6 +1,6 @@
# 页面标题 # 页面标题
VITE_APP_TITLE = 驭鲲智防系统 VITE_APP_TITLE = 防御机管理系统
VITE_APP_LOGO_TITLE = 驭鲲智防系统 VITE_APP_LOGO_TITLE = 防御机管理系统
# 生产环境配置 # 生产环境配置
VITE_APP_ENV = 'production' VITE_APP_ENV = 'production'
@@ -23,12 +23,12 @@ VITE_BUILD_COMPRESS = gzip
VITE_APP_PORT = 80 VITE_APP_PORT = 80
# 接口加密功能开关(如需关闭 后端也必须对应关闭) # 接口加密功能开关(如需关闭 后端也必须对应关闭)
# defensive_machine 后端 api-decrypt.enabled = true # defensive_machine 后端 api-decrypt.enabled = false
VITE_APP_ENCRYPT = true VITE_APP_ENCRYPT = false
# 接口加密传输 RSA 公钥与后端解密私钥对应 如更换需前后端一同更换 # 接口加密传输 RSA 公钥与后端解密私钥对应 如更换需前后端一同更换
VITE_APP_RSA_PUBLIC_KEY = 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtVIQm+1AWMfkT1uVOyugKQwBeHtgpulABOrVfaL7Xzc1/rKSdLdjYZLZnnOU+rdE1jqz20MXDBozK22spH/j53d9nFLftHkMCRUlfc0MWEqtSla+2MdzElIHEbq4CSizZOcT8QXCRw7V2OuDqnC7XRQmnpA5Ks1WaKH5V0aoKWztjsqZV6vVWPqyDC94mvu/Q/9e6KVfkqAzR+afuo9OoBFewDGQ6jfqyu1TxoGZrfSr5lXOVeADf0CVfWCDze5y4f7er4JjPHWA2+yv8Scd9bqVJ4Q+/yR2Gd516Mi11OgBKc8GdkuA4/kfmXc7n91SMjgQP6DlTVuPc/6KnB/pyQIDAQAB' VITE_APP_RSA_PUBLIC_KEY = 'MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ=='
# 接口响应解密 RSA 私钥与后端加密公钥对应 如更换需前后端一同更换 # 接口响应解密 RSA 私钥与后端加密公钥对应 如更换需前后端一同更换
VITE_APP_RSA_PRIVATE_KEY = 'MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDYg1dC9XDMRtKJ5IFj+Hd3bq7+r2YL1y8FM9HD111PN/YVy4K6O67cRA/TLG7+tl6eYiA73f5lzPgv1riOwBPSCmvVopxq2KiJs30uEnug0qGp6l2cwQOmjitFAQF8dOtAl4m6EM/jdXCZYbQrthJ6n3tR+vGj2IrGSJEjPrj3VRljK9I7Zw0Be0rhJn/vZw2CA4GoVfE/3gqa5bpl+3SFxMP7TwF5uZueCNqLaPnd7jK5jB1SUghI7+pq897Ng4EG6kdOGsiQe5kbHWkSPTmPEObMwgLJ18i/XCVg+LUkTDJOC/MAfB+WGcKryNHrhcbhB6+XstpoGT8JlOPOgh0BAgMBAAECggEBALZG7MH8bDguL7XTYHNPjRRJZJ4aAGlbgcR5edHMkEvPKyfyK16qPG5IBqKcN93Mnx/sMIL2Q+RkiVWNCdd0MbuU9m/m9JSnTkyPhYWyHc6pRV0NaD496Nrhud/gFuY2cI/yhArXeI1gI6mdrddW83u0pFfCUojEFyETmsz3UzU1LMkVbwCXNbZ1b3GfcVnONMn8T/7CMlRPlKyOuE0OPnTut4wddAybe5BKDQYnGzuosUdO+Q+2K3hqaTD6Bb33D4ATHjvR3Vt9itE07BiaBFjZkE2odP4x3fSsnc7Cw3A7zHQivpfgqcoBcGUaMcN1FdgpaF2mbe9/eHn/lYCpevkCgYEA4FoKq3fPOITAtA6bjwCHXvIQUjv4vQ0BZtt7OX7zwpRwqOhJfN0ZRkd7NMUqYnB10N9NgPRz0kKC6qff5duN+zkYIzjHrlm5lzdIEAouovRlrGXST4n8gAYteh5D5Or9wRXj7USQoaNyiWKB9A/07ortJvul09sPjJU7IVfzzXMCgYEA9w45Gh2bAvYvm2bWieKZRhcV6Pwy9R8587qmKLC7V/0gfVojBHx7xTmTEfKCGrt3fK4jB5v/2Dar8gDF+RM7g01u0YuFJt17jeFr/4o5KMimfXhoEulDbgjfvhxQOrtkrF5iHpKxiEXqIPiH+B7155PAnE43011AAJCBSylmTrsCgYEAkxr3PA9HFKwXHvklDtMt8BeQlBs2sd9BOAxZ9A1GECP86wPEi9b9p5NfOe6+J+XNmrOQwimHeCqcZPjGWpVnt35sUUv9wlia1Igu/DVw9vCBalUpXXYA1oE2eIg3xHZBBMYxuXXnz9S4WVT6GOoNlAwMDC+dQBi3TVrcdrSQ6/kCgYBA254ICh+ovmKvJGdMGY3thZ/94z+pdEIthyGZ6xOzvMMrxV8ODXQcycmfW4/mXrK2q6yMkdqvs2KejK9sfS0RgmGGZ19UXa7TB4vnsSziVRLIO7TuyggmufOrIBm74XhDfB+8MPykbt5RO43OiKo72mElZ69mMMPdohIfXkX6gwKBgH4L9qJRhNunWSfHO4/8dST+2meSV+Cpqf0b/9cV6v5FMS22m+szzW9XoBXnI9dbAqiEDlCHCLw34pRlC3OzBNlbT6HzNmYXLko2Ttg9thiDq7WS5tV+EHJRP6kLabYGq0Aghtq+6LIwwogbHzhRi8Q52o3CNmM9lThNWZIkzaBe' VITE_APP_RSA_PRIVATE_KEY = 'MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAmc3CuPiGL/LcIIm7zryCEIbl1SPzBkr75E2VMtxegyZ1lYRD+7TZGAPkvIsBcaMs6Nsy0L78n2qh+lIZMpLH8wIDAQABAkEAk82Mhz0tlv6IVCyIcw/s3f0E+WLmtPFyR9/WtV3Y5aaejUkU60JpX4m5xNR2VaqOLTZAYjW8Wy0aXr3zYIhhQQIhAMfqR9oFdYw1J9SsNc+CrhugAvKTi0+BF6VoL6psWhvbAiEAxPPNTmrkmrXwdm/pQQu3UOQmc2vCZ5tiKpW10CgJi8kCIFGkL6utxw93Ncj4exE/gPLvKcT+1Emnoox+O9kRXss5AiAMtYLJDaLEzPrAWcZeeSgSIzbL+ecokmFKSDDcRske6QIgSMkHedwND1olF8vlKsJUGK3BcdtM8w4Xq7BpSBwsloE='
# 客户端id # 客户端id
VITE_APP_CLIENT_ID = 'e5cd7e4891bf95d1d19206ce24a7b32e' VITE_APP_CLIENT_ID = 'e5cd7e4891bf95d1d19206ce24a7b32e'

28
.gitignore vendored
View File

@@ -1,3 +1,30 @@
# ---> Java
# Compiled class file
*.class
# Log file
*.log
**/*.log
# BlueJ files
*.ctxt
# Mobile Tools for Java (J2ME)
.mtj.tmp/
# Package Files #
*.jar
*.war
*.nar
*.ear
*.zip
*.tar.gz
*.rar
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
replay_pid*
.DS_Store .DS_Store
.history .history
node_modules/ node_modules/
@@ -5,7 +32,6 @@ dist/
npm-debug.log* npm-debug.log*
yarn-debug.log* yarn-debug.log*
yarn-error.log* yarn-error.log*
**/*.log
tests/**/coverage/ tests/**/coverage/
tests/e2e/reports tests/e2e/reports

View File

@@ -1,83 +1,3 @@
## 平台简介 # dm-ui
- 本仓库为前端技术栈 [Vue3](https://v3.cn.vuejs.org) + [TS](https://www.typescriptlang.org/) + [Element Plus](https://element-plus.org/zh-CN) + [Vite](https://cn.vitejs.dev) 版本。 防御机后台管理ui
- 成员项目: 基于 vben5(ant-design-vue) 的前端项目 [ruoyi-plus-vben5](https://github.com/imdap/ruoyi-plus-vben5)
- 成员项目: 基于soybean 的前端项目 [ruoyi-plus-soybean](https://gitee.com/xlsea/ruoyi-plus-soybean)
## 配套后端代码仓库地址
| 介绍 | 项目名 | 项目地址 |
|------------|:-----------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 🔥 分布式集群框架 | RuoYi-Vue-Plus | - [Gitee](https://gitee.com/dromara/RuoYi-Vue-Plus)<br> - [GitHub](https://github.com/dromara/RuoYi-Vue-Plus)<br> - [GitCode](https://gitcode.com/dromara/RuoYi-Vue-Plus) |
| 🔥 微服务框架 | RuoYi-Cloud-Plus | - [Gitee](https://gitee.com/dromara/RuoYi-Cloud-Plus)<br>- [GitHub](https://github.com/dromara/RuoYi-Cloud-Plus)<br> - [GitCode](https://gitcode.com/dromara/RuoYi-Cloud-Plus) |
## 分支说明
- ts分支(稳定发布主分支 生产可用)
- dev分支(开发分支 开发过程中使用)
## 前端运行
```bash
# 安装依赖
npm install --registry=https://registry.npmmirror.com
# 启动服务
npm run dev
# 构建生产环境
npm run build:prod
# 前端访问地址 http://localhost:80
```
## 本框架与RuoYi的业务差异
| 业务 | 功能说明 | 本框架 | RuoYi |
| ------------ | ------------------------------------------------------------- | ------ | ----------------------------- |
| 租户管理 | 系统内租户的管理 如:租户套餐、过期时间、用户数量、企业信息等 | 支持 | 无 |
| 租户套餐管理 | 系统内租户所能使用的套餐管理 如:套餐内所包含的菜单等 | 支持 | 无 |
| 用户管理 | 用户的管理配置 如:新增用户、分配用户所属部门、角色、岗位等 | 支持 | 支持 |
| 部门管理 | 配置系统组织机构(公司、部门、小组) 树结构展现支持数据权限 | 支持 | 支持 |
| 岗位管理 | 配置系统用户所属担任职务 | 支持 | 支持 |
| 菜单管理 | 配置系统菜单、操作权限、按钮权限标识等 | 支持 | 支持 |
| 角色管理 | 角色菜单权限分配、设置角色按机构进行数据范围权限划分 | 支持 | 支持 |
| 字典管理 | 对系统中经常使用的一些较为固定的数据进行维护 | 支持 | 支持 |
| 参数管理 | 对系统动态配置常用参数 | 支持 | 支持 |
| 通知公告 | 系统通知公告信息发布维护 | 支持 | 支持 |
| 操作日志 | 系统正常操作日志记录和查询 系统异常信息日志记录和查询 | 支持 | 支持 |
| 登录日志 | 系统登录日志记录查询包含登录异常 | 支持 | 支持 |
| 文件管理 | 系统文件展示、上传、下载、删除等管理 | 支持 | 无 |
| 文件配置管理 | 系统文件上传、下载所需要的配置信息动态添加、修改、删除等管理 | 支持 | 无 |
| 在线用户管理 | 已登录系统的在线用户信息监控与强制踢出操作 | 支持 | 支持 |
| 定时任务 | 运行报表、任务管理(添加、修改、删除)、日志管理、执行器管理等 | 支持 | 仅支持任务与日志管理 |
| 代码生成 | 多数据源前后端代码的生成java、html、xml、sql支持CRUD下载 | 支持 | 仅支持单数据源 |
| 系统接口 | 根据业务代码自动生成相关的api接口文档 | 支持 | 支持 |
| 服务监控 | 监视集群系统CPU、内存、磁盘、堆栈、在线日志、Spring相关配置等 | 支持 | 仅支持单机CPU、内存、磁盘监控 |
| 缓存监控 | 对系统的缓存信息查询,命令统计等。 | 支持 | 支持 |
| 在线构建器 | 拖动表单元素生成相应的HTML代码。 | 支持 | 支持 |
| 使用案例 | 系统的一些功能案例 | 支持 | 不支持 |
## 演示图例
| | |
| ---------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
| ![输入图片说明](https://foruda.gitee.com/images/1680077524361362822/270bb429_1766278.png '屏幕截图') | ![输入图片说明](https://foruda.gitee.com/images/1680077619939771291/989bf9b6_1766278.png '屏幕截图') |
| ![输入图片说明](https://foruda.gitee.com/images/1680077681751513929/1c27c5bd_1766278.png '屏幕截图') | ![输入图片说明](https://foruda.gitee.com/images/1680077721559267315/74d63e23_1766278.png '屏幕截图') |
| ![输入图片说明](https://foruda.gitee.com/images/1680077765638904515/1b75d4a6_1766278.png '屏幕截图') | ![输入图片说明](https://foruda.gitee.com/images/1680078026375951297/eded7a4b_1766278.png '屏幕截图') |
| ![输入图片说明](https://foruda.gitee.com/images/1680078237104531207/0eb1b6a7_1766278.png '屏幕截图') | ![输入图片说明](https://foruda.gitee.com/images/1680078254306078709/5931e22f_1766278.png '屏幕截图') |
| ![输入图片说明](https://foruda.gitee.com/images/1680078287971528493/0b9af60a_1766278.png '屏幕截图') | ![输入图片说明](https://foruda.gitee.com/images/1680078308138770249/8d3b6696_1766278.png '屏幕截图') |
| ![输入图片说明](https://foruda.gitee.com/images/1680078352553634393/db5ef880_1766278.png '屏幕截图') | ![输入图片说明](https://foruda.gitee.com/images/1680078378238393374/601e4357_1766278.png '屏幕截图') |
| ![输入图片说明](https://foruda.gitee.com/images/1680078414983206024/2aae27c1_1766278.png '屏幕截图') | ![输入图片说明](https://foruda.gitee.com/images/1680078446738419874/ecce7d59_1766278.png '屏幕截图') |
| ![输入图片说明](https://foruda.gitee.com/images/1680078475971341775/149e8634_1766278.png '屏幕截图') | ![输入图片说明](https://foruda.gitee.com/images/1680078491666717143/3fadece7_1766278.png '屏幕截图') |
| ![输入图片说明](https://foruda.gitee.com/images/1680078558863188826/fb8ced2a_1766278.png '屏幕截图') | ![输入图片说明](https://foruda.gitee.com/images/1680078574561685461/ae68a0b2_1766278.png '屏幕截图') |
| ![输入图片说明](https://foruda.gitee.com/images/1680078594932772013/9d8bfec6_1766278.png '屏幕截图') | ![输入图片说明](https://foruda.gitee.com/images/1680078626493093532/fcfe4ff6_1766278.png '屏幕截图') |
| ![输入图片说明](https://foruda.gitee.com/images/1680078643608812515/0295bd4f_1766278.png '屏幕截图') | ![输入图片说明](https://foruda.gitee.com/images/1680078685196286463/d7612c81_1766278.png '屏幕截图') |
| ![输入图片说明](https://foruda.gitee.com/images/1680078703877318597/56fce0bc_1766278.png '屏幕截图') | ![输入图片说明](https://foruda.gitee.com/images/1680078716586545643/b6dbd68f_1766278.png '屏幕截图') |
| ![输入图片说明](https://foruda.gitee.com/images/1680078734103217688/eb1e6aa6_1766278.png '屏幕截图') | ![输入图片说明](https://foruda.gitee.com/images/1680078759131415480/73c525d8_1766278.png '屏幕截图') |
| ![输入图片说明](https://foruda.gitee.com/images/1680078779416197879/75e3ed02_1766278.png '屏幕截图') | ![输入图片说明](https://foruda.gitee.com/images/1680078802329118061/77e10915_1766278.png '屏幕截图') |
| ![输入图片说明](https://foruda.gitee.com/images/1680078893627848351/34a1c342_1766278.png '屏幕截图') | ![输入图片说明](https://foruda.gitee.com/images/1680078928175016986/f126ec4a_1766278.png '屏幕截图') |
| ![输入图片说明](https://foruda.gitee.com/images/1680078941718318363/b68a0f72_1766278.png '屏幕截图') | ![输入图片说明](https://foruda.gitee.com/images/1680078963175518631/3bb769a1_1766278.png '屏幕截图') |
| ![输入图片说明](https://foruda.gitee.com/images/1680078982294090567/b31c343d_1766278.png '屏幕截图') | ![输入图片说明](https://foruda.gitee.com/images/1680079000642440444/77ca82a9_1766278.png '屏幕截图') |
| ![输入图片说明](https://foruda.gitee.com/images/1680079020995074177/03b7d52e_1766278.png '屏幕截图') | ![输入图片说明](https://foruda.gitee.com/images/1680079039367822173/76811806_1766278.png '屏幕截图') |
| ![输入图片说明](https://foruda.gitee.com/images/1680079274333484664/4dfdc7c0_1766278.png '屏幕截图') | ![输入图片说明](https://foruda.gitee.com/images/1680079290467458224/d6715fcf_1766278.png '屏幕截图') |

View File

@@ -5,7 +5,7 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" /> <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta name="renderer" content="webkit" /> <meta name="renderer" content="webkit" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" /> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <link rel="icon" href="/favicon.ico" />
<title>%VITE_APP_TITLE%</title> <title>%VITE_APP_TITLE%</title>
<!--[if lt IE 11 <!--[if lt IE 11
]><script> ]><script>

View File

@@ -35,10 +35,8 @@
"image-conversion": "2.1.1", "image-conversion": "2.1.1",
"js-cookie": "3.0.5", "js-cookie": "3.0.5",
"jsencrypt": "3.5.4", "jsencrypt": "3.5.4",
"jszip": "^3.10.1",
"nprogress": "0.2.0", "nprogress": "0.2.0",
"pinia": "3.0.4", "pinia": "3.0.4",
"qrcode": "^1.5.4",
"screenfull": "6.0.2", "screenfull": "6.0.2",
"vue": "3.5.30", "vue": "3.5.30",
"vue-cropper": "1.1.4", "vue-cropper": "1.1.4",
@@ -57,7 +55,6 @@
"@types/js-cookie": "3.0.6", "@types/js-cookie": "3.0.6",
"@types/node": "^25.4.0", "@types/node": "^25.4.0",
"@types/nprogress": "0.2.3", "@types/nprogress": "0.2.3",
"@types/qrcode": "^1.5.6",
"@unocss/preset-attributify": "66.6.6", "@unocss/preset-attributify": "66.6.6",
"@unocss/preset-icons": "66.6.6", "@unocss/preset-icons": "66.6.6",
"@unocss/preset-uno": "66.6.6", "@unocss/preset-uno": "66.6.6",

View File

@@ -4,11 +4,6 @@ export interface DeviceVO {
*/ */
id: string | number; id: string | number;
/**
* 绑定用户 ID
*/
userId?: string | number;
/** /**
* 设备编号 * 设备编号
*/ */
@@ -24,11 +19,6 @@ export interface DeviceVO {
*/ */
status: string; status: string;
/**
* 后端按请求语言返回的状态展示文案
*/
statusLabel?: string;
/** /**
* WiFi名称 * WiFi名称
*/ */
@@ -108,6 +98,7 @@ export interface DeviceVO {
* 识别模式 0-蓝牙 1-蓝牙+人脸 * 识别模式 0-蓝牙 1-蓝牙+人脸
*/ */
patternPerception?: string; patternPerception?: string;
} }
export interface DeviceForm extends BaseEntity { export interface DeviceForm extends BaseEntity {
@@ -116,11 +107,6 @@ export interface DeviceForm extends BaseEntity {
*/ */
id?: string | number; id?: string | number;
/**
* 绑定用户 ID
*/
userId?: string | number;
/** /**
* 设备编号 * 设备编号
*/ */

View File

@@ -14,11 +14,6 @@ export interface MessageVO {
*/ */
categoryName: string; categoryName: string;
/**
* 后端按请求语言返回的告警类型展示文案
*/
categoryLabel?: string;
/** /**
* 图片地址 * 图片地址
*/ */
@@ -32,6 +27,7 @@ export interface MessageVO {
* 视频地址 * 视频地址
*/ */
video: string | number; video: string | number;
} }
export interface MessageForm extends BaseEntity { export interface MessageForm extends BaseEntity {
@@ -59,9 +55,11 @@ export interface MessageForm extends BaseEntity {
* 视频地址 * 视频地址
*/ */
video?: string | number; video?: string | number;
} }
export interface MessageQuery extends PageQuery { export interface MessageQuery extends PageQuery {
/** /**
* 类型 * 类型
*/ */

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.8 KiB

After

Width:  |  Height:  |  Size: 7.9 KiB

View File

@@ -2,10 +2,8 @@
<el-breadcrumb class="app-breadcrumb" separator="/"> <el-breadcrumb class="app-breadcrumb" separator="/">
<transition-group name="breadcrumb"> <transition-group name="breadcrumb">
<el-breadcrumb-item v-for="(item, index) in levelList" :key="item.path"> <el-breadcrumb-item v-for="(item, index) in levelList" :key="item.path">
<span v-if="item.redirect === 'noRedirect' || index == levelList.length - 1" class="no-redirect"> <span v-if="item.redirect === 'noRedirect' || index == levelList.length - 1" class="no-redirect">{{ item.meta?.title }}</span>
{{ translateRouteTitle(item.meta?.title as string) }} <a v-else @click.prevent="handleLink(item)">{{ item.meta?.title }}</a>
</span>
<a v-else @click.prevent="handleLink(item)">{{ translateRouteTitle(item.meta?.title as string) }}</a>
</el-breadcrumb-item> </el-breadcrumb-item>
</transition-group> </transition-group>
</el-breadcrumb> </el-breadcrumb>
@@ -14,7 +12,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { RouteLocationMatched } from 'vue-router'; import { RouteLocationMatched } from 'vue-router';
import { usePermissionStore } from '@/store/modules/permission'; import { usePermissionStore } from '@/store/modules/permission';
import { translateRouteTitle } from '@/utils/i18n';
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();

View File

@@ -24,17 +24,23 @@
</el-upload> </el-upload>
<!-- 上传提示 --> <!-- 上传提示 -->
<div v-if="showTip" class="el-upload__tip"> <div v-if="showTip" class="el-upload__tip">
{{ $t('upload.tip', { size: fileSize, types: fileType.join('/') }) }} 请上传
<template v-if="fileSize">
大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b>
</template>
<template v-if="fileType">
格式为 <b style="color: #f56c6c">{{ fileType.join('/') }}</b>
</template>
的文件
</div> </div>
<el-dialog v-model="dialogVisible" :title="$t('upload.preview')" width="800px" append-to-body> <el-dialog v-model="dialogVisible" title="预览" width="800px" append-to-body>
<img :src="dialogImageUrl" style="display: block; max-width: 100%; margin: 0 auto" /> <img :src="dialogImageUrl" style="display: block; max-width: 100%; margin: 0 auto" />
</el-dialog> </el-dialog>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useI18n } from 'vue-i18n';
import { listByIds, delOss } from '@/api/system/oss'; import { listByIds, delOss } from '@/api/system/oss';
import { OssVO } from '@/api/system/oss/types'; import { OssVO } from '@/api/system/oss/types';
import { propTypes } from '@/utils/propTypes'; import { propTypes } from '@/utils/propTypes';
@@ -67,7 +73,6 @@ const props = defineProps({
}); });
const { proxy } = getCurrentInstance() as ComponentInternalInstance; const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { t } = useI18n();
const emit = defineEmits(['update:modelValue']); const emit = defineEmits(['update:modelValue']);
const number = ref(0); const number = ref(0);
const uploadList = ref<any[]>([]); const uploadList = ref<any[]>([]);
@@ -135,35 +140,35 @@ const handleBeforeUpload = (file: any) => {
isImg = file.type.indexOf('image') > -1; isImg = file.type.indexOf('image') > -1;
} }
if (!isImg) { if (!isImg) {
proxy?.$modal.msgError(t('upload.invalidFormat', { types: props.fileType.join('/') })); proxy?.$modal.msgError(`文件格式不正确, 请上传${props.fileType.join('/')}图片格式文件!`);
return false; return false;
} }
if (file.name.includes(',')) { if (file.name.includes(',')) {
proxy?.$modal.msgError(t('upload.invalidFileName')); proxy?.$modal.msgError('文件名不正确,不能包含英文逗号!');
return false; return false;
} }
if (props.fileSize) { if (props.fileSize) {
const isLt = file.size / 1024 / 1024 < props.fileSize; const isLt = file.size / 1024 / 1024 < props.fileSize;
if (!isLt) { if (!isLt) {
proxy?.$modal.msgError(t('upload.tooLarge', { size: props.fileSize })); proxy?.$modal.msgError(`上传头像图片大小不能超过 ${props.fileSize} MB!`);
return false; return false;
} }
} }
//压缩图片,开启压缩并且大于指定的压缩大小时才压缩 //压缩图片,开启压缩并且大于指定的压缩大小时才压缩
if (props.compressSupport && file.size / 1024 > props.compressTargetSize) { if (props.compressSupport && file.size / 1024 > props.compressTargetSize) {
proxy?.$modal.loading(t('upload.uploading')); proxy?.$modal.loading('正在上传图片,请稍候...');
number.value++; number.value++;
return compressAccurately(file, props.compressTargetSize); return compressAccurately(file, props.compressTargetSize);
} else { } else {
proxy?.$modal.loading(t('upload.uploading')); proxy?.$modal.loading('正在上传图片,请稍候...');
number.value++; number.value++;
} }
}; };
// 文件个数超出 // 文件个数超出
const handleExceed = () => { const handleExceed = () => {
proxy?.$modal.msgError(t('upload.tooMany', { limit: props.limit })); proxy?.$modal.msgError(`上传文件数量不能超过 ${props.limit} 个!`);
}; };
// 上传成功回调 // 上传成功回调
@@ -206,7 +211,7 @@ const uploadedSuccessfully = () => {
// 上传失败 // 上传失败
const handleUploadError = () => { const handleUploadError = () => {
proxy?.$modal.msgError(t('upload.failed')); proxy?.$modal.msgError('上传图片失败');
proxy?.$modal.closeLoading(); proxy?.$modal.closeLoading();
}; };

View File

@@ -5,12 +5,8 @@
</div> </div>
<template #dropdown> <template #dropdown>
<el-dropdown-menu> <el-dropdown-menu>
<el-dropdown-item :disabled="appStore.language === LanguageEnum.zh_CN" :command="LanguageEnum.zh_CN"> <el-dropdown-item :disabled="appStore.language === 'zh_CN'" command="zh_CN"> 中文 </el-dropdown-item>
{{ t('language.chinese') }} <el-dropdown-item :disabled="appStore.language === 'en_US'" command="en_US"> English </el-dropdown-item>
</el-dropdown-item>
<el-dropdown-item :disabled="appStore.language === LanguageEnum.en_US" :command="LanguageEnum.en_US">
{{ t('language.english') }}
</el-dropdown-item>
</el-dropdown-menu> </el-dropdown-menu>
</template> </template>
</el-dropdown> </el-dropdown>
@@ -20,19 +16,18 @@
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useAppStore } from '@/store/modules/app'; import { useAppStore } from '@/store/modules/app';
import SvgIcon from '@/components/SvgIcon/index.vue'; import SvgIcon from '@/components/SvgIcon/index.vue';
import VXETable from 'vxe-table';
import { getVxeLanguage, LanguageEnum } from '@/enums/LanguageEnum';
import { useDynamicTitle } from '@/utils/dynamicTitle';
const appStore = useAppStore(); const appStore = useAppStore();
const { locale, t } = useI18n(); const { locale } = useI18n();
const handleLanguageChange = (lang: LanguageEnum) => { const message: any = {
zh_CN: '切换语言成功!',
en_US: 'Switch Language Successful!'
};
const handleLanguageChange = (lang: any) => {
locale.value = lang; locale.value = lang;
appStore.changeLanguage(lang); appStore.changeLanguage(lang);
VXETable.setLanguage(getVxeLanguage(lang)); ElMessage.success(message[lang] || '切换语言成功!');
useDynamicTitle();
ElMessage.success(t('language.switched'));
}; };
</script> </script>

View File

@@ -1,22 +1,16 @@
<template> <template>
<div class="top-right-btn" :style="style"> <div class="top-right-btn" :style="style">
<el-row> <el-row>
<el-tooltip <el-tooltip v-if="search" class="item" effect="dark" :content="showSearch ? '隐藏搜索' : '显示搜索'" placement="top">
v-if="search"
class="item"
effect="dark"
:content="showSearch ? $t('toolbar.hideSearch') : $t('toolbar.showSearch')"
placement="top"
>
<el-button circle icon="Search" @click="toggleSearch()" /> <el-button circle icon="Search" @click="toggleSearch()" />
</el-tooltip> </el-tooltip>
<el-tooltip class="item" effect="dark" :content="$t('toolbar.refresh')" placement="top"> <el-tooltip class="item" effect="dark" content="刷新" placement="top">
<el-button circle icon="Refresh" @click="refresh()" /> <el-button circle icon="Refresh" @click="refresh()" />
</el-tooltip> </el-tooltip>
<el-tooltip v-if="columns" class="item" effect="dark" :content="$t('toolbar.columns')" placement="top"> <el-tooltip v-if="columns" class="item" effect="dark" content="显示/隐藏列" placement="top">
<div class="show-btn"> <div class="show-btn">
<el-popover placement="bottom" trigger="click"> <el-popover placement="bottom" trigger="click">
<div class="tree-header">{{ $t('toolbar.columns') }}</div> <div class="tree-header">显示/隐藏列</div>
<el-tree <el-tree
ref="columnRef" ref="columnRef"
:data="columns" :data="columns"

View File

@@ -16,17 +16,15 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useI18n } from 'vue-i18n';
import { useAppStore } from '@/store/modules/app'; import { useAppStore } from '@/store/modules/app';
const appStore = useAppStore(); const appStore = useAppStore();
const size = computed(() => appStore.size); const size = computed(() => appStore.size);
const { t } = useI18n(); const sizeOptions = ref([
const sizeOptions = computed(() => [ { label: '较大', value: 'large' },
{ label: t('size.large'), value: 'large' }, { label: '默认', value: 'default' },
{ label: t('size.default'), value: 'default' }, { label: '稍小', value: 'small' }
{ label: t('size.small'), value: 'small' }
]); ]);
const handleSetSize = (size: 'large' | 'default' | 'small') => { const handleSetSize = (size: 'large' | 'default' | 'small') => {

View File

@@ -3,16 +3,16 @@
<template v-for="(item, index) in topMenus"> <template v-for="(item, index) in topMenus">
<el-menu-item v-if="index < visibleNumber" :key="index" :style="{ '--theme': theme }" :index="item.path" <el-menu-item v-if="index < visibleNumber" :key="index" :style="{ '--theme': theme }" :index="item.path"
><svg-icon v-if="item.meta && item.meta.icon && item.meta.icon !== '#'" :icon-class="item.meta ? item.meta.icon : ''" /> ><svg-icon v-if="item.meta && item.meta.icon && item.meta.icon !== '#'" :icon-class="item.meta ? item.meta.icon : ''" />
{{ translateRouteTitle(item.meta?.title as string) }}</el-menu-item {{ item.meta?.title }}</el-menu-item
> >
</template> </template>
<!-- 顶部菜单超出数量折叠 --> <!-- 顶部菜单超出数量折叠 -->
<el-sub-menu v-if="topMenus.length > visibleNumber" :style="{ '--theme': theme }" index="more"> <el-sub-menu v-if="topMenus.length > visibleNumber" :style="{ '--theme': theme }" index="more">
<template #title>{{ $t('navbar.moreMenu') }}</template> <template #title>更多菜单</template>
<template v-for="(item, index) in topMenus"> <template v-for="(item, index) in topMenus">
<el-menu-item v-if="index >= visibleNumber" :key="index" :index="item.path" <el-menu-item v-if="index >= visibleNumber" :key="index" :index="item.path"
><svg-icon :icon-class="item.meta ? item.meta.icon : ''" /> {{ translateRouteTitle(item.meta?.title as string) }}</el-menu-item ><svg-icon :icon-class="item.meta ? item.meta.icon : ''" /> {{ item.meta?.title }}</el-menu-item
> >
</template> </template>
</el-sub-menu> </el-sub-menu>
@@ -26,7 +26,6 @@ import { useAppStore } from '@/store/modules/app';
import { useSettingsStore } from '@/store/modules/settings'; import { useSettingsStore } from '@/store/modules/settings';
import { usePermissionStore } from '@/store/modules/permission'; import { usePermissionStore } from '@/store/modules/permission';
import { RouteRecordRaw } from 'vue-router'; import { RouteRecordRaw } from 'vue-router';
import { translateRouteTitle } from '@/utils/i18n';
// 顶部栏初始数 // 顶部栏初始数
const visibleNumber = ref<number>(-1); const visibleNumber = ref<number>(-1);

View File

@@ -3,15 +3,3 @@ export enum LanguageEnum {
en_US = 'en_US' en_US = 'en_US'
} }
export const isSupportedLanguage = (language: string | null | undefined): language is LanguageEnum => {
return Object.values(LanguageEnum).includes(language as LanguageEnum);
};
export const getHtmlLanguage = (language: LanguageEnum): string => {
return language === LanguageEnum.en_US ? 'en-US' : 'zh-CN';
};
export const getVxeLanguage = (language: LanguageEnum): 'en-US' | 'zh-CN' => {
return language === LanguageEnum.en_US ? 'en-US' : 'zh-CN';
};

View File

@@ -1,84 +1,9 @@
export default { export default {
app: {
title: 'Reinkun Intelligent Defense System'
},
// 路由国际化 // 路由国际化
route: { route: {
dashboard: 'Dashboard', dashboard: 'Dashboard',
document: 'Document' document: 'Document'
}, },
routeTitles: {
: 'Dashboard',
: 'Profile',
: 'Defense Devices',
: 'System',
: 'Tenants',
: 'Monitoring',
: 'Tools',
: 'Demos',
: 'Users',
: 'Roles',
: 'Menus',
: 'Departments',
: 'Positions',
: 'Dictionaries',
: 'Parameters',
: 'Notices',
: 'Logs',
线: 'Online Users',
: 'Cache',
: 'Code Generator',
: 'Tenant Plans',
: 'Clients',
: 'Files',
: 'Job Scheduler',
: 'Operation Logs',
: 'Login Logs',
: 'Devices',
: 'Devices',
: 'Alerts',
: 'Alert Messages',
: 'Alert Messages',
: 'Messages',
APP用户管理: 'App Users',
APP用户信息: 'App Users',
: 'Families',
: 'Family Members',
: 'Family Devices',
: 'Device Users',
: 'Device Member Links',
: 'Device Tokens'
},
common: {
search: 'Search',
reset: 'Reset',
add: 'Add',
edit: 'Edit',
delete: 'Delete',
export: 'Export',
confirm: 'Confirm',
cancel: 'Cancel',
close: 'Close',
save: 'Save',
operation: 'Actions',
index: '#',
status: 'Status',
pleaseSelect: 'Please select',
pleaseInput: 'Please enter',
operationSuccess: 'Operation successful',
deleteSuccess: 'Deleted successfully',
required: 'This field is required',
systemPrompt: 'System Notification'
},
home: {
welcome: 'Welcome to Reinkun Intelligent Defense System',
slogan: 'Intelligent Defense · Reliable Protection'
},
language: {
chinese: '中文',
english: 'English',
switched: 'Language switched to English'
},
// 登录页面国际化 // 登录页面国际化
login: { login: {
selectPlaceholder: 'Please select/enter a company name', selectPlaceholder: 'Please select/enter a company name',
@@ -148,7 +73,6 @@ export default {
navbar: { navbar: {
full: 'Full Screen', full: 'Full Screen',
language: 'Language', language: 'Language',
search: 'Search',
dashboard: 'Dashboard', dashboard: 'Dashboard',
document: 'Document', document: 'Document',
message: 'Message', message: 'Message',
@@ -156,144 +80,6 @@ export default {
selectTenant: 'Select Tenant', selectTenant: 'Select Tenant',
layoutSetting: 'Layout Setting', layoutSetting: 'Layout Setting',
personalCenter: 'Personal Center', personalCenter: 'Personal Center',
logout: 'Logout', logout: 'Logout'
logoutConfirm: 'Are you sure you want to sign out?',
prompt: 'Confirmation',
confirm: 'Confirm',
cancel: 'Cancel',
moreMenu: 'More'
},
size: {
large: 'Large',
default: 'Default',
small: 'Small'
},
tagsView: {
refresh: 'Refresh',
closeCurrent: 'Close Current',
closeOthers: 'Close Others',
closeLeft: 'Close Left',
closeRight: 'Close Right',
closeAll: 'Close All'
},
settings: {
navigation: 'Navigation',
leftMenu: 'Left Menu',
mixedMenu: 'Mixed Menu',
topMenu: 'Top Menu',
themeStyle: 'Theme',
themeColor: 'Theme Color',
darkMode: 'Dark Mode',
systemLayout: 'Layout',
enableTagsView: 'Enable Tags View',
showTagsIcon: 'Show Tag Icons',
fixedHeader: 'Fixed Header',
showLogo: 'Show Logo',
dynamicTitle: 'Dynamic Title',
save: 'Save Settings',
reset: 'Reset Settings',
saving: 'Saving locally, please wait...',
resetting: 'Clearing settings and reloading, please wait...'
},
notice: {
title: 'Notices',
readAll: 'Mark All Read',
read: 'Read',
unread: 'Unread',
empty: 'No messages',
viewMore: 'View More'
},
toolbar: {
hideSearch: 'Hide Search',
showSearch: 'Show Search',
refresh: 'Refresh',
columns: 'Show/Hide Columns'
},
upload: {
tip: 'Upload files up to {size} MB in {types} format',
preview: 'Preview',
invalidFormat: 'Invalid file format. Upload an image in {types} format.',
invalidFileName: 'The file name cannot contain commas.',
tooLarge: 'The image must be smaller than {size} MB.',
uploading: 'Uploading image, please wait...',
tooMany: 'You can upload up to {limit} files.',
failed: 'Image upload failed'
},
device: {
deviceNo: 'Device No.',
deviceName: 'Device Name',
owner: 'Owner',
wifiName: 'Wi-Fi Name',
wifiPassword: 'Wi-Fi Password',
model: 'Device Model',
modelShort: 'Model',
serialNumber: 'Serial Number',
serialNumberShort: 'Serial No.',
firmwareVersion: 'Firmware',
perceptionMode: 'Recognition Mode',
workMode: 'Work Mode',
light: 'Light',
lightEnabled: 'Enable Light',
alert: 'Alert',
alertPlayback: 'Alert Playback',
alertContent: 'Alert Content',
strikeRange: 'Strike Range',
strikeInterval: 'Strike Interval',
containerCount: 'Container Count',
strikePosition: 'Strike Position',
deploymentStart: 'Deployment Start',
deploymentEnd: 'Deployment End',
selectTime: 'Select a time',
qrCode: 'QR Code',
batchDownloadQr: 'Download QR Codes',
saveImage: 'Save Image',
addTitle: 'Add Device',
editTitle: 'Edit Device',
deleteConfirm: 'Delete the selected device record(s) {ids}?',
selectOwner: 'Please select an owner',
noData: 'No device data available',
qrGenerationFailed: 'Failed to generate QR code',
batchGenerationFailed: 'Batch generation failed',
batchPackaged: 'Packaged {count} device QR code(s)',
status: {
offline: 'Offline',
online: 'Online',
fault: 'Fault',
expired: 'Expired'
},
perception: {
bluetooth: 'Bluetooth',
bluetoothFace: 'Bluetooth + Face'
},
work: {
automatic: 'Automatic',
semiAutomatic: 'Semi-automatic'
},
lightState: {
off: 'Off',
on: 'On'
},
audioState: {
off: 'Disabled',
on: 'Enabled'
}
},
message: {
category: 'Alert Type',
description: 'Description',
deviceId: 'Device ID',
deviceName: 'Device Name',
image: 'Images',
video: 'Video',
videoUrl: 'Video URL',
videoPlayback: 'Video Playback',
addTitle: 'Add Alert Message',
editTitle: 'Edit Alert Message',
deleteConfirm: 'Delete the selected alert message(s) {ids}?',
categoryType: {
intrusion: 'Intrusion Detected',
deviceFault: 'Device Fault',
expiration: 'Expiration Reminder'
}
} }
}; };

View File

@@ -1,7 +1,7 @@
// 自定义国际化配置 // 自定义国际化配置
import { createI18n } from 'vue-i18n'; import { createI18n } from 'vue-i18n';
import { getHtmlLanguage, isSupportedLanguage, LanguageEnum } from '@/enums/LanguageEnum'; import { LanguageEnum } from '@/enums/LanguageEnum';
import zh_CN from '@/lang/zh_CN'; import zh_CN from '@/lang/zh_CN';
import en_US from '@/lang/en_US'; import en_US from '@/lang/en_US';
@@ -10,19 +10,11 @@ import en_US from '@/lang/en_US';
* @returns zh-cn|en ... * @returns zh-cn|en ...
*/ */
export const getLanguage = (): LanguageEnum => { export const getLanguage = (): LanguageEnum => {
const storedLanguage = localStorage.getItem('language'); const language = useStorage<LanguageEnum>('language', LanguageEnum.zh_CN);
if (isSupportedLanguage(storedLanguage)) { if (language.value) {
return storedLanguage; return language.value;
} }
if (storedLanguage !== null) { return LanguageEnum.zh_CN;
localStorage.removeItem('language');
}
return navigator.language.toLowerCase().startsWith('zh') ? LanguageEnum.zh_CN : LanguageEnum.en_US;
};
export const setDocumentLanguage = (language: LanguageEnum): void => {
document.documentElement.lang = getHtmlLanguage(language);
}; };
const i18n = createI18n({ const i18n = createI18n({
@@ -30,15 +22,12 @@ const i18n = createI18n({
allowComposition: true, allowComposition: true,
legacy: false, legacy: false,
locale: getLanguage(), locale: getLanguage(),
fallbackLocale: LanguageEnum.zh_CN,
messages: { messages: {
[LanguageEnum.zh_CN]: zh_CN, zh_CN: zh_CN,
[LanguageEnum.en_US]: en_US en_US: en_US
} }
}); });
setDocumentLanguage(i18n.global.locale.value);
export default i18n; export default i18n;
export type LanguageType = typeof zh_CN; export type LanguageType = typeof zh_CN;

View File

@@ -1,84 +1,9 @@
export default { export default {
app: {
title: '驭鲲智防系统'
},
// 路由国际化 // 路由国际化
route: { route: {
dashboard: '首页', dashboard: '首页',
document: '项目文档' document: '项目文档'
}, },
routeTitles: {
: '首页',
: '个人中心',
: '防御机管理',
: '系统管理',
: '租户管理',
: '系统监控',
: '系统工具',
: '测试菜单',
: '用户管理',
: '角色管理',
: '菜单管理',
: '部门管理',
: '岗位管理',
: '字典管理',
: '参数设置',
: '通知公告',
: '日志管理',
线: '在线用户',
: '缓存监控',
: '代码生成',
: '租户套餐管理',
: '客户端管理',
: '文件管理',
: '任务调度中心',
: '操作日志',
: '登录日志',
: '设备管理',
: '设备信息',
: '告警管理',
: '告警消息',
: '告警消息列',
: '消息管理',
APP用户管理: 'APP用户管理',
APP用户信息: 'APP用户信息',
: '家庭管理',
: '家庭成员',
: '家庭设备管理',
: '设备用户管理',
: '设备成员关联',
: '设备令牌管理'
},
common: {
search: '搜索',
reset: '重置',
add: '新增',
edit: '修改',
delete: '删除',
export: '导出',
confirm: '确定',
cancel: '取消',
close: '关闭',
save: '保存',
operation: '操作',
index: '序号',
status: '状态',
pleaseSelect: '请选择',
pleaseInput: '请输入',
operationSuccess: '操作成功',
deleteSuccess: '删除成功',
required: '不能为空',
systemPrompt: '系统提示'
},
home: {
welcome: '欢迎使用驭鲲智防系统',
slogan: '智能防御 · 安全守护'
},
language: {
chinese: '中文',
english: 'English',
switched: '语言已切换为中文'
},
// 登录页面国际化 // 登录页面国际化
login: { login: {
selectPlaceholder: '请选择/输入公司名称', selectPlaceholder: '请选择/输入公司名称',
@@ -148,7 +73,6 @@ export default {
navbar: { navbar: {
full: '全屏', full: '全屏',
language: '语言', language: '语言',
search: '搜索',
dashboard: '首页', dashboard: '首页',
document: '项目文档', document: '项目文档',
message: '消息', message: '消息',
@@ -156,144 +80,6 @@ export default {
selectTenant: '选择租户', selectTenant: '选择租户',
layoutSetting: '布局设置', layoutSetting: '布局设置',
personalCenter: '个人中心', personalCenter: '个人中心',
logout: '退出登录', logout: '退出登录'
logoutConfirm: '确定注销并退出系统吗?',
prompt: '提示',
confirm: '确定',
cancel: '取消',
moreMenu: '更多菜单'
},
size: {
large: '较大',
default: '默认',
small: '稍小'
},
tagsView: {
refresh: '刷新页面',
closeCurrent: '关闭当前',
closeOthers: '关闭其他',
closeLeft: '关闭左侧',
closeRight: '关闭右侧',
closeAll: '全部关闭'
},
settings: {
navigation: '菜单导航设置',
leftMenu: '左侧菜单',
mixedMenu: '混合菜单',
topMenu: '顶部菜单',
themeStyle: '主题风格设置',
themeColor: '主题颜色',
darkMode: '深色模式',
systemLayout: '系统布局配置',
enableTagsView: '开启 Tags-Views',
showTagsIcon: '显示页签图标',
fixedHeader: '固定 Header',
showLogo: '显示 Logo',
dynamicTitle: '动态标题',
save: '保存配置',
reset: '重置配置',
saving: '正在保存到本地,请稍候...',
resetting: '正在清除设置缓存并刷新,请稍候...'
},
notice: {
title: '通知公告',
readAll: '全部已读',
read: '已读',
unread: '未读',
empty: '消息为空',
viewMore: '查看更多'
},
toolbar: {
hideSearch: '隐藏搜索',
showSearch: '显示搜索',
refresh: '刷新',
columns: '显示/隐藏列'
},
upload: {
tip: '请上传大小不超过 {size} MB、格式为 {types} 的文件',
preview: '预览',
invalidFormat: '文件格式不正确,请上传 {types} 格式的图片!',
invalidFileName: '文件名不正确,不能包含英文逗号!',
tooLarge: '上传图片大小不能超过 {size} MB',
uploading: '正在上传图片,请稍候...',
tooMany: '上传文件数量不能超过 {limit} 个!',
failed: '上传图片失败'
},
device: {
deviceNo: '设备编号',
deviceName: '设备名称',
owner: '所属用户',
wifiName: 'WiFi名称',
wifiPassword: 'WiFi密码',
model: '设备型号',
modelShort: '型号',
serialNumber: '设备序列号',
serialNumberShort: '序列号',
firmwareVersion: '固件版本',
perceptionMode: '识别模式',
workMode: '工作模式',
light: '强光',
lightEnabled: '强光开启',
alert: '告警',
alertPlayback: '告警播放',
alertContent: '告警内容',
strikeRange: '打击距离',
strikeInterval: '打击间隔',
containerCount: '容器数量',
strikePosition: '打击位置',
deploymentStart: '布放开始时间',
deploymentEnd: '布放结束时间',
selectTime: '选择时间',
qrCode: '二维码',
batchDownloadQr: '批量下载二维码',
saveImage: '保存图片',
addTitle: '添加设备信息',
editTitle: '修改设备信息',
deleteConfirm: '是否确认删除设备信息编号为“{ids}”的数据项?',
selectOwner: '请选择所属用户',
noData: '当前没有设备数据',
qrGenerationFailed: '二维码生成失败',
batchGenerationFailed: '批量生成失败',
batchPackaged: '已打包 {count} 个设备二维码',
status: {
offline: '离线',
online: '在线',
fault: '故障',
expired: '到期'
},
perception: {
bluetooth: '蓝牙',
bluetoothFace: '蓝牙+人脸'
},
work: {
automatic: '自动',
semiAutomatic: '半自动'
},
lightState: {
off: '不开启',
on: '开启'
},
audioState: {
off: '不播放',
on: '播放'
}
},
message: {
category: '告警类型',
description: '告警说明',
deviceId: '设备ID',
deviceName: '设备名',
image: '图片',
video: '视频',
videoUrl: '视频地址',
videoPlayback: '视频播放',
addTitle: '添加告警消息',
editTitle: '修改告警消息',
deleteConfirm: '是否确认删除告警消息编号为“{ids}”的数据项?',
categoryType: {
intrusion: '有人入侵',
deviceFault: '设备故障',
expiration: '到期提醒'
}
} }
}; };

View File

@@ -27,7 +27,7 @@
</el-select> </el-select>
<search-menu ref="searchMenuRef" /> <search-menu ref="searchMenuRef" />
<el-tooltip :content="proxy.$t('navbar.search')" effect="dark" placement="bottom"> <el-tooltip content="搜索" effect="dark" placement="bottom">
<div class="right-menu-item hover-effect" @click="openSearchMenu"> <div class="right-menu-item hover-effect" @click="openSearchMenu">
<svg-icon class-name="search-icon" icon-class="search" /> <svg-icon class-name="search-icon" icon-class="search" />
</div> </div>
@@ -47,6 +47,14 @@
</el-popover> </el-popover>
</div> </div>
</el-tooltip> </el-tooltip>
<el-tooltip content="Github" effect="dark" placement="bottom">
<ruo-yi-git id="ruoyi-git" class="right-menu-item hover-effect" />
</el-tooltip>
<el-tooltip :content="proxy.$t('navbar.document')" effect="dark" placement="bottom">
<ruo-yi-doc id="ruoyi-doc" class="right-menu-item hover-effect" />
</el-tooltip>
<el-tooltip :content="proxy.$t('navbar.full')" effect="dark" placement="bottom"> <el-tooltip :content="proxy.$t('navbar.full')" effect="dark" placement="bottom">
<screenfull id="screenfull" class="right-menu-item hover-effect" /> <screenfull id="screenfull" class="right-menu-item hover-effect" />
</el-tooltip> </el-tooltip>
@@ -97,8 +105,8 @@ import notice from './notice/index.vue';
import router from '@/router'; import router from '@/router';
import { ElMessageBoxOptions } from 'element-plus/es/components/message-box/src/message-box.type'; import { ElMessageBoxOptions } from 'element-plus/es/components/message-box/src/message-box.type';
import { NavTypeEnum } from '@/enums/NavTypeEnum'; import { NavTypeEnum } from '@/enums/NavTypeEnum';
import Logo from '@/layout/components/Sidebar/Logo.vue'; import Logo from "@/layout/components/Sidebar/Logo.vue";
import TopBar from './TopBar/index.vue'; import TopBar from './TopBar'
const appStore = useAppStore(); const appStore = useAppStore();
const userStore = useUserStore(); const userStore = useUserStore();
@@ -162,9 +170,9 @@ const toggleSideBar = () => {
}; };
const logout = async () => { const logout = async () => {
await ElMessageBox.confirm(proxy.$t('navbar.logoutConfirm'), proxy.$t('navbar.prompt'), { await ElMessageBox.confirm('确定注销并退出系统吗?', '提示', {
confirmButtonText: proxy.$t('navbar.confirm'), confirmButtonText: '确定',
cancelButtonText: proxy.$t('navbar.cancel'), cancelButtonText: '取消',
type: 'warning' type: 'warning'
} as ElMessageBoxOptions); } as ElMessageBoxOptions);
userStore.logout().then(() => { userStore.logout().then(() => {
@@ -274,6 +282,7 @@ watch(
margin-left: 8px; margin-left: 8px;
} }
.errLog-container { .errLog-container {
display: inline-block; display: inline-block;
vertical-align: top; vertical-align: top;

View File

@@ -1,8 +1,8 @@
<template> <template>
<el-drawer v-model="showSettings" :with-header="false" direction="rtl" size="300px" close-on-click-modal> <el-drawer v-model="showSettings" :with-header="false" direction="rtl" size="300px" close-on-click-modal>
<h3 class="drawer-title">{{ $t('settings.navigation') }}</h3> <h3 class="drawer-title">菜单导航设置</h3>
<div class="nav-wrap"> <div class="nav-wrap">
<el-tooltip :content="$t('settings.leftMenu')" placement="bottom"> <el-tooltip content="左侧菜单" placement="bottom">
<div <div
class="item left" class="item left"
@click="handleNavType(NavTypeEnum.LEFT)" @click="handleNavType(NavTypeEnum.LEFT)"
@@ -13,7 +13,7 @@
</div> </div>
</el-tooltip> </el-tooltip>
<el-tooltip :content="$t('settings.mixedMenu')" placement="bottom"> <el-tooltip content="混合菜单" placement="bottom">
<div <div
class="item mix" class="item mix"
@click="handleNavType(NavTypeEnum.MIX)" @click="handleNavType(NavTypeEnum.MIX)"
@@ -23,7 +23,7 @@
<b></b><b></b> <b></b><b></b>
</div> </div>
</el-tooltip> </el-tooltip>
<el-tooltip :content="$t('settings.topMenu')" placement="bottom"> <el-tooltip content="顶部菜单" placement="bottom">
<div <div
class="item top" class="item top"
@click="handleNavType(NavTypeEnum.TOP)" @click="handleNavType(NavTypeEnum.TOP)"
@@ -35,14 +35,14 @@
</el-tooltip> </el-tooltip>
</div> </div>
<h3 class="drawer-title">{{ $t('settings.themeStyle') }}</h3> <h3 class="drawer-title">主题风格设置</h3>
<div class="setting-drawer-block-checbox"> <div class="setting-drawer-block-checbox">
<div class="setting-drawer-block-checbox-item" @click="handleTheme(SideThemeEnum.DARK)"> <div class="setting-drawer-block-checbox-item" @click="handleTheme(SideThemeEnum.DARK)">
<img src="@/assets/images/dark.svg" alt="dark" /> <img src="@/assets/images/dark.svg" alt="dark" />
<div v-if="sideTheme === 'theme-dark'" class="setting-drawer-block-checbox-selectIcon" style="display: block"> <div v-if="sideTheme === 'theme-dark'" class="setting-drawer-block-checbox-selectIcon" style="display: block">
<i aria-label="check" class="anticon anticon-check"> <i aria-label="图标: check" class="anticon anticon-check">
<svg viewBox="64 64 896 896" data-icon="check" width="1em" height="1em" :fill="theme" aria-hidden="true" focusable="false"> <svg viewBox="64 64 896 896" data-icon="check" width="1em" height="1em" :fill="theme" aria-hidden="true" focusable="false" class>
<path <path
d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 0 0-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z" d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 0 0-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z"
/> />
@@ -53,8 +53,8 @@
<div class="setting-drawer-block-checbox-item" @click="handleTheme(SideThemeEnum.LIGHT)"> <div class="setting-drawer-block-checbox-item" @click="handleTheme(SideThemeEnum.LIGHT)">
<img src="@/assets/images/light.svg" alt="light" /> <img src="@/assets/images/light.svg" alt="light" />
<div v-if="sideTheme === 'theme-light'" class="setting-drawer-block-checbox-selectIcon" style="display: block"> <div v-if="sideTheme === 'theme-light'" class="setting-drawer-block-checbox-selectIcon" style="display: block">
<i aria-label="check" class="anticon anticon-check"> <i aria-label="图标: check" class="anticon anticon-check">
<svg viewBox="64 64 896 896" data-icon="check" width="1em" height="1em" :fill="theme" aria-hidden="true" focusable="false"> <svg viewBox="64 64 896 896" data-icon="check" width="1em" height="1em" :fill="theme" aria-hidden="true" focusable="false" class>
<path <path
d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 0 0-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z" d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 0 0-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z"
/> />
@@ -64,13 +64,13 @@
</div> </div>
</div> </div>
<div class="drawer-item"> <div class="drawer-item">
<span>{{ $t('settings.themeColor') }}</span> <span>主题颜色</span>
<span class="comp-style"> <span class="comp-style">
<el-color-picker v-model="theme" :predefine="predefineColors" @change="themeChange" /> <el-color-picker v-model="theme" :predefine="predefineColors" @change="themeChange" />
</span> </span>
</div> </div>
<div class="drawer-item"> <div class="drawer-item">
<span>{{ $t('settings.darkMode') }}</span> <span>深色模式</span>
<span class="comp-style"> <span class="comp-style">
<el-switch v-model="isDark" class="drawer-switch" @change="toggleDark" /> <el-switch v-model="isDark" class="drawer-switch" @change="toggleDark" />
</span> </span>
@@ -78,38 +78,38 @@
<el-divider /> <el-divider />
<h3 class="drawer-title">{{ $t('settings.systemLayout') }}</h3> <h3 class="drawer-title">系统布局配置</h3>
<div class="drawer-item"> <div class="drawer-item">
<span>{{ $t('settings.enableTagsView') }}</span> <span>开启 Tags-Views</span>
<span class="comp-style"> <span class="comp-style">
<el-switch v-model="settingsStore.tagsView" class="drawer-switch" /> <el-switch v-model="settingsStore.tagsView" class="drawer-switch" />
</span> </span>
</div> </div>
<div class="drawer-item"> <div class="drawer-item">
<span>{{ $t('settings.showTagsIcon') }}</span> <span>显示页签图标</span>
<span class="comp-style"> <span class="comp-style">
<el-switch v-model="settingsStore.tagsIcon" :disabled="!settingsStore.tagsView" class="drawer-switch" /> <el-switch v-model="settingsStore.tagsIcon" :disabled="!settingsStore.tagsView" class="drawer-switch" />
</span> </span>
</div> </div>
<div class="drawer-item"> <div class="drawer-item">
<span>{{ $t('settings.fixedHeader') }}</span> <span>固定 Header</span>
<span class="comp-style"> <span class="comp-style">
<el-switch v-model="settingsStore.fixedHeader" class="drawer-switch" /> <el-switch v-model="settingsStore.fixedHeader" class="drawer-switch" />
</span> </span>
</div> </div>
<div class="drawer-item"> <div class="drawer-item">
<span>{{ $t('settings.showLogo') }}</span> <span>显示 Logo</span>
<span class="comp-style"> <span class="comp-style">
<el-switch v-model="settingsStore.sidebarLogo" class="drawer-switch" /> <el-switch v-model="settingsStore.sidebarLogo" class="drawer-switch" />
</span> </span>
</div> </div>
<div class="drawer-item"> <div class="drawer-item">
<span>{{ $t('settings.dynamicTitle') }}</span> <span>动态标题</span>
<span class="comp-style"> <span class="comp-style">
<el-switch v-model="settingsStore.dynamicTitle" class="drawer-switch" @change="dynamicTitleChange" /> <el-switch v-model="settingsStore.dynamicTitle" class="drawer-switch" @change="dynamicTitleChange" />
</span> </span>
@@ -117,13 +117,12 @@
<el-divider /> <el-divider />
<el-button type="primary" plain icon="DocumentAdd" @click="saveSetting">{{ $t('settings.save') }}</el-button> <el-button type="primary" plain icon="DocumentAdd" @click="saveSetting">保存配置</el-button>
<el-button plain icon="Refresh" @click="resetSetting">{{ $t('settings.reset') }}</el-button> <el-button plain icon="Refresh" @click="resetSetting">重置配置</el-button>
</el-drawer> </el-drawer>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useI18n } from 'vue-i18n';
import { useDynamicTitle } from '@/utils/dynamicTitle'; import { useDynamicTitle } from '@/utils/dynamicTitle';
import { useAppStore } from '@/store/modules/app'; import { useAppStore } from '@/store/modules/app';
import { useSettingsStore } from '@/store/modules/settings'; import { useSettingsStore } from '@/store/modules/settings';
@@ -137,7 +136,6 @@ const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const appStore = useAppStore(); const appStore = useAppStore();
const settingsStore = useSettingsStore(); const settingsStore = useSettingsStore();
const permissionStore = usePermissionStore(); const permissionStore = usePermissionStore();
const { t } = useI18n();
const showSettings = ref(false); const showSettings = ref(false);
const theme = ref(settingsStore.theme); const theme = ref(settingsStore.theme);
@@ -163,15 +161,15 @@ const toggleDark = () => useToggle(isDark);
/** 菜单导航设置 */ /** 菜单导航设置 */
watch( watch(
navType, () => navType,
(val) => { (val: string) => {
if (val === NavTypeEnum.TOP) { if (val.value === NavTypeEnum.TOP) {
appStore.toggleSideBarHide(true); appStore.toggleSideBarHide(true);
permissionStore.setSidebarRouters(permissionStore.defaultRoutes as any); permissionStore.setSidebarRouters(permissionStore.defaultRoutes as any);
} else if (val === NavTypeEnum.LEFT) { } else if (val.value === NavTypeEnum.LEFT) {
appStore.toggleSideBarHide(false); appStore.toggleSideBarHide(false);
permissionStore.setSidebarRouters(permissionStore.defaultRoutes as any); permissionStore.setSidebarRouters(permissionStore.defaultRoutes as any);
} else if (val === NavTypeEnum.MIX) { } else if (val.value === NavTypeEnum.MIX) {
appStore.toggleSideBarHide(false); appStore.toggleSideBarHide(false);
} }
}, },
@@ -202,7 +200,7 @@ const handleTheme = (val: string) => {
settingsStore.sideTheme = val; settingsStore.sideTheme = val;
}; };
const saveSetting = () => { const saveSetting = () => {
proxy?.$modal.loading(t('settings.saving')); proxy?.$modal.loading('正在保存到本地,请稍候...');
const settings = useStorage<LayoutSetting>('layout-setting', defaultSettings); const settings = useStorage<LayoutSetting>('layout-setting', defaultSettings);
settings.value.tagsView = storeSettings.value.tagsView; settings.value.tagsView = storeSettings.value.tagsView;
settings.value.tagsIcon = storeSettings.value.tagsIcon; settings.value.tagsIcon = storeSettings.value.tagsIcon;
@@ -217,7 +215,7 @@ const saveSetting = () => {
}, 1000); }, 1000);
}; };
const resetSetting = () => { const resetSetting = () => {
proxy?.$modal.loading(t('settings.resetting')); proxy?.$modal.loading('正在清除设置缓存并刷新,请稍候...');
useStorage<any>('layout-setting', null).value = null; useStorage<any>('layout-setting', null).value = null;
setTimeout('window.location.reload()', 1000); setTimeout('window.location.reload()', 1000);
}; };

View File

@@ -5,9 +5,7 @@
<el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{ 'submenu-title-noDropdown': !isNest }"> <el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{ 'submenu-title-noDropdown': !isNest }">
<svg-icon :icon-class="onlyOneChild.meta.icon || (item.meta && item.meta.icon)" /> <svg-icon :icon-class="onlyOneChild.meta.icon || (item.meta && item.meta.icon)" />
<template #title> <template #title>
<span class="menu-title" :title="hasTitle(translateRouteTitle(onlyOneChild.meta.title))"> <span class="menu-title" :title="hasTitle(onlyOneChild.meta.title)">{{ onlyOneChild.meta.title }}</span>
{{ translateRouteTitle(onlyOneChild.meta.title) }}
</span>
</template> </template>
</el-menu-item> </el-menu-item>
</app-link> </app-link>
@@ -16,7 +14,7 @@
<el-sub-menu v-else ref="subMenu" :index="resolvePath(item.path)" teleported> <el-sub-menu v-else ref="subMenu" :index="resolvePath(item.path)" teleported>
<template v-if="item.meta" #title> <template v-if="item.meta" #title>
<svg-icon :icon-class="item.meta ? item.meta.icon : ''" /> <svg-icon :icon-class="item.meta ? item.meta.icon : ''" />
<span class="menu-title" :title="hasTitle(translateRouteTitle(item.meta?.title))">{{ translateRouteTitle(item.meta?.title) }}</span> <span class="menu-title" :title="hasTitle(item.meta?.title)">{{ item.meta?.title }}</span>
</template> </template>
<sidebar-item <sidebar-item
@@ -36,7 +34,6 @@ import { isExternal } from '@/utils/validate';
import AppLink from './Link.vue'; import AppLink from './Link.vue';
import { getNormalPath } from '@/utils/ruoyi'; import { getNormalPath } from '@/utils/ruoyi';
import { RouteRecordRaw } from 'vue-router'; import { RouteRecordRaw } from 'vue-router';
import { translateRouteTitle } from '@/utils/i18n';
const props = defineProps({ const props = defineProps({
item: { item: {

View File

@@ -6,28 +6,26 @@
:key="tag.path" :key="tag.path"
:data-path="tag.path" :data-path="tag.path"
:class="{ 'active': isActive(tag), 'has-icon': tagsIcon }" :class="{ 'active': isActive(tag), 'has-icon': tagsIcon }"
:to="{ path: tag.path ? tag.path : '', query: tag.query }" :to="{ path: tag.path ? tag.path : '', query: tag.query, fullPath: tag.fullPath ? tag.fullPath : '' }"
class="tags-view-item" class="tags-view-item"
:style="activeStyle(tag)" :style="activeStyle(tag)"
@click.middle="!isAffix(tag) ? closeSelectedTag(tag) : ''" @click.middle="!isAffix(tag) ? closeSelectedTag(tag) : ''"
@contextmenu.prevent="openMenu(tag, $event)" @contextmenu.prevent="openMenu(tag, $event)"
> >
<svg-icon v-if="tagsIcon && tag.meta && tag.meta.icon && tag.meta.icon !== '#'" :icon-class="tag.meta.icon"/> <svg-icon v-if="tagsIcon && tag.meta && tag.meta.icon && tag.meta.icon !== '#'" :icon-class="tag.meta.icon"/>
<span class="tags-view-item-title">{{ translateRouteTitle(tag.title as string) }}</span> <span class="tags-view-item-title">{{ tag.title }}</span>
<span v-if="!isAffix(tag)" @click.prevent.stop="closeSelectedTag(tag)"> <span v-if="!isAffix(tag)" @click.prevent.stop="closeSelectedTag(tag)">
<close class="el-icon-close" style="width: 1em; height: 1em; vertical-align: middle" /> <close class="el-icon-close" style="width: 1em; height: 1em; vertical-align: middle" />
</span> </span>
</router-link> </router-link>
</scroll-pane> </scroll-pane>
<ul v-show="visible" :style="{ left: left + 'px', top: top + 'px' }" class="contextmenu"> <ul v-show="visible" :style="{ left: left + 'px', top: top + 'px' }" class="contextmenu">
<li @click="refreshSelectedTag(selectedTag)"><refresh-right style="width: 1em; height: 1em" /> {{ $t('tagsView.refresh') }}</li> <li @click="refreshSelectedTag(selectedTag)"><refresh-right style="width: 1em; height: 1em" /> 刷新页面</li>
<li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)"> <li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)"><close style="width: 1em; height: 1em" /> 关闭当前</li>
<close style="width: 1em; height: 1em" /> {{ $t('tagsView.closeCurrent') }} <li @click="closeOthersTags"><circle-close style="width: 1em; height: 1em" /> 关闭其他</li>
</li> <li v-if="!isFirstView()" @click="closeLeftTags"><back style="width: 1em; height: 1em" /> 关闭左侧</li>
<li @click="closeOthersTags"><circle-close style="width: 1em; height: 1em" /> {{ $t('tagsView.closeOthers') }}</li> <li v-if="!isLastView()" @click="closeRightTags"><right style="width: 1em; height: 1em" /> 关闭右侧</li>
<li v-if="!isFirstView()" @click="closeLeftTags"><back style="width: 1em; height: 1em" /> {{ $t('tagsView.closeLeft') }}</li> <li @click="closeAllTags(selectedTag)"><circle-close style="width: 1em; height: 1em" /> 全部关闭</li>
<li v-if="!isLastView()" @click="closeRightTags"><right style="width: 1em; height: 1em" /> {{ $t('tagsView.closeRight') }}</li>
<li @click="closeAllTags(selectedTag)"><circle-close style="width: 1em; height: 1em" /> {{ $t('tagsView.closeAll') }}</li>
</ul> </ul>
</div> </div>
</template> </template>
@@ -39,7 +37,6 @@ import { useSettingsStore } from '@/store/modules/settings';
import { usePermissionStore } from '@/store/modules/permission'; import { usePermissionStore } from '@/store/modules/permission';
import { useTagsViewStore } from '@/store/modules/tagsView'; import { useTagsViewStore } from '@/store/modules/tagsView';
import { RouteRecordRaw, RouteLocationNormalized } from 'vue-router'; import { RouteRecordRaw, RouteLocationNormalized } from 'vue-router';
import { translateRouteTitle } from '@/utils/i18n';
const visible = ref(false); const visible = ref(false);
const top = ref(0); const top = ref(0);
@@ -55,7 +52,7 @@ const router = useRouter();
const visitedViews = computed(() => useTagsViewStore().getVisitedViews()); const visitedViews = computed(() => useTagsViewStore().getVisitedViews());
const routes = computed(() => usePermissionStore().getRoutes()); const routes = computed(() => usePermissionStore().getRoutes());
const theme = computed(() => useSettingsStore().theme); const theme = computed(() => useSettingsStore().theme);
const tagsIcon = computed(() => useSettingsStore().tagsIcon); const tagsIcon = computed(() => useSettingsStore().tagsIcon)
watch(route, () => { watch(route, () => {
addTags(); addTags();
@@ -183,8 +180,7 @@ const closeLeftTags = () => {
}); });
}; };
const closeOthersTags = () => { const closeOthersTags = () => {
if (!selectedTag.value) return; router.push(selectedTag.value).catch(() => {});
router.push({ path: selectedTag.value.path, query: selectedTag.value.query }).catch(() => {});
proxy?.$tab.closeOtherPage(selectedTag.value).then(() => { proxy?.$tab.closeOtherPage(selectedTag.value).then(() => {
moveToCurrentTag(); moveToCurrentTag();
}); });
@@ -265,11 +261,7 @@ onMounted(() => {
margin-left: 5px; margin-left: 5px;
margin-top: 4px; margin-top: 4px;
border-radius: var(--app-radius-md); border-radius: var(--app-radius-md);
transition: transition: box-shadow 0.2s ease, transform 0.2s ease, border-color 0.2s ease, color 0.2s ease;
box-shadow 0.2s ease,
transform 0.2s ease,
border-color 0.2s ease,
color 0.2s ease;
&:hover { &:hover {
color: var(--el-color-primary); color: var(--el-color-primary);
border-color: var(--el-color-primary-light-5); border-color: var(--el-color-primary-light-5);

View File

@@ -4,69 +4,66 @@
<el-sub-menu index="more" class="el-sub-menu__hide-arrow" v-if="moreRoutes.length > 0"> <el-sub-menu index="more" class="el-sub-menu__hide-arrow" v-if="moreRoutes.length > 0">
<template #title> <template #title>
<span>{{ $t('navbar.moreMenu') }}</span> <span>更多菜单</span>
</template> </template>
<sidebar-item :key="route.path + index" v-for="(route, index) in moreRoutes" :item="route" :base-path="route.path" /> <sidebar-item :key="route.path + index" v-for="(route, index) in moreRoutes" :item="route" :base-path="route.path" />
</el-sub-menu> </el-sub-menu>
</el-menu> </el-menu>
</template> </template>
<script setup lang="ts"> <script setup>
import SidebarItem from '../Sidebar/SidebarItem.vue'; import SidebarItem from '../Sidebar/SidebarItem'
import { useAppStore } from '@/store/modules/app'; import {useAppStore} from '@/store/modules/app'
import { useSettingsStore } from '@/store/modules/settings'; import {useSettingsStore} from '@/store/modules/settings'
import { usePermissionStore } from '@/store/modules/permission'; import {usePermissionStore} from '@/store/modules/permission'
import type { RouteRecordRaw } from 'vue-router';
const route = useRoute(); const route = useRoute()
const appStore = useAppStore(); const appStore = useAppStore()
const settingsStore = useSettingsStore(); const settingsStore = useSettingsStore()
const permissionStore = usePermissionStore(); const permissionStore = usePermissionStore()
const sidebarRouters = computed(() => permissionStore.sidebarRouters); const sidebarRouters = computed(() => permissionStore.sidebarRouters)
const theme = computed(() => settingsStore.theme); const theme = computed(() => settingsStore.theme)
const device = computed(() => appStore.device); const device = computed(() => appStore.device)
const activeMenu = computed(() => { const activeMenu = computed(() => {
const { meta, path } = route; const { meta, path } = route
if (meta.activeMenu) { if (meta.activeMenu) {
return meta.activeMenu; return meta.activeMenu
} }
return path; return path
}); })
const visibleNumber = ref(5); const visibleNumber = ref(5)
const topMenus = computed<RouteRecordRaw[]>(() => { const topMenus = computed(() => {
return permissionStore.sidebarRouters.filter((f) => !f.hidden).slice(0, visibleNumber.value) as RouteRecordRaw[]; return permissionStore.sidebarRouters.filter((f) => !f.hidden).slice(0, visibleNumber.value)
}); })
const moreRoutes = computed<RouteRecordRaw[]>(() => { const moreRoutes = computed(() => {
return permissionStore.sidebarRouters return permissionStore.sidebarRouters.filter((f) => !f.hidden).slice(visibleNumber.value, sidebarRouters.value.length - visibleNumber.value)
.filter((f) => !f.hidden) })
.slice(visibleNumber.value, sidebarRouters.value.length - visibleNumber.value) as RouteRecordRaw[];
});
function setVisibleNumber() { function setVisibleNumber() {
let width = document.body.getBoundingClientRect().width; let width = document.body.getBoundingClientRect().width
if (width >= 1000) { if (width >= 1000) {
width -= 500; width -= 500
} }
visibleNumber.value = Math.floor(width / 3 / 85); visibleNumber.value = parseInt(width / 3 / 85)
} }
onMounted(() => { onMounted(() => {
window.addEventListener('resize', setVisibleNumber); window.addEventListener('resize', setVisibleNumber)
}); })
onBeforeUnmount(() => { onBeforeUnmount(() => {
window.removeEventListener('resize', setVisibleNumber); window.removeEventListener('resize', setVisibleNumber)
}); })
onMounted(() => { onMounted(() => {
setVisibleNumber(); setVisibleNumber()
}); })
</script> </script>
<style lang="scss"> <style lang="scss">
/* menu item */ /* menu item */
#app .topbar-menu.el-menu--horizontal .el-sub-menu__title, #app .topbar-menu.el-menu--horizontal .el-sub-menu__title, #app .topbar-menu.el-menu--horizontal .el-menu-item {
#app .topbar-menu.el-menu--horizontal .el-menu-item {
padding: 0 10px !important; padding: 0 10px !important;
} }
@@ -79,10 +76,7 @@ onMounted(() => {
margin: 0 10px !important; margin: 0 10px !important;
} }
.el-sub-menu.is-active .svg-icon, .el-sub-menu.is-active .svg-icon, .el-menu-item.is-active .svg-icon + span, .el-sub-menu.is-active .svg-icon + span, .el-sub-menu.is-active .el-sub-menu__title span {
.el-menu-item.is-active .svg-icon + span,
.el-sub-menu.is-active .svg-icon + span,
.el-sub-menu.is-active .el-sub-menu__title span {
color: v-bind(theme); color: v-bind(theme);
} }
@@ -103,8 +97,7 @@ onMounted(() => {
} }
/* menu__title el-menu-item */ /* menu__title el-menu-item */
.topbar-menu.el-menu--horizontal .el-sub-menu__title, .topbar-menu.el-menu--horizontal .el-sub-menu__title, .topbar-menu.el-menu--horizontal .el-menu-item {
.topbar-menu.el-menu--horizontal .el-menu-item {
height: 60px; height: 60px;
} }
</style> </style>

View File

@@ -1,12 +1,12 @@
<template> <template>
<div class="layout-search-dialog"> <div class="layout-search-dialog">
<el-dialog v-model="state.isShowSearch" destroy-on-close :show-close="false" :close-on-click-modal="true" @closed="closeSearch"> <el-dialog v-model="state.isShowSearch" destroy-on-close :show-close="false">
<template #footer> <template #footer>
<el-autocomplete <el-autocomplete
ref="layoutMenuAutocompleteRef" ref="layoutMenuAutocompleteRef"
v-model="state.menuQuery" v-model="state.menuQuery"
:fetch-suggestions="menuSearch" :fetch-suggestions="menuSearch"
:placeholder="$t('navbar.search')" placeholder="搜索"
:fit-input-width="true" :fit-input-width="true"
@select="onHandleSelect" @select="onHandleSelect"
> >
@@ -30,7 +30,6 @@ import { getNormalPath } from '@/utils/ruoyi';
import { isHttp } from '@/utils/validate'; import { isHttp } from '@/utils/validate';
import { usePermissionStore } from '@/store/modules/permission'; import { usePermissionStore } from '@/store/modules/permission';
import { RouteRecordRaw } from 'vue-router'; import { RouteRecordRaw } from 'vue-router';
import { translateRouteTitle } from '@/utils/i18n';
type Router = Array<{ type Router = Array<{
path: string; path: string;
icon: string; icon: string;
@@ -65,17 +64,7 @@ const openSearch = () => {
// 搜索弹窗关闭 // 搜索弹窗关闭
const closeSearch = () => { const closeSearch = () => {
state.isShowSearch = false; state.isShowSearch = false;
state.menuQuery = '';
}; };
// ESC 关闭搜索
onMounted(() => {
document.addEventListener('keydown', (e: KeyboardEvent) => {
if (e.key === 'Escape' && state.isShowSearch) {
closeSearch();
}
});
});
// 菜单搜索数据过滤 // 菜单搜索数据过滤
const menuSearch = (queryString: string, cb: (options: any[]) => void) => { const menuSearch = (queryString: string, cb: (options: any[]) => void) => {
const options = state.menuList.filter((item) => { const options = state.menuList.filter((item) => {
@@ -98,7 +87,7 @@ const generateRoutes = (routes: RouteRecordRaw[], basePath = '', prefixTitle: st
title: [...prefixTitle] title: [...prefixTitle]
}; };
if (r.meta && r.meta.title) { if (r.meta && r.meta.title) {
data.title = [...data.title, translateRouteTitle(r.meta.title as string)]; data.title = [...data.title, r.meta.title];
if (r.redirect !== 'noRedirect') { if (r.redirect !== 'noRedirect') {
// only push the routes with title // only push the routes with title
// special case: need to exclude parent router without redirect // special case: need to exclude parent router without redirect

View File

@@ -1,8 +1,8 @@
<template> <template>
<div v-loading="state.loading" class="layout-navbars-breadcrumb-user-news"> <div v-loading="state.loading" class="layout-navbars-breadcrumb-user-news">
<div class="head-box"> <div class="head-box">
<div class="head-box-title">{{ $t('notice.title') }}</div> <div class="head-box-title">通知公告</div>
<div class="head-box-btn" @click="readAll">{{ $t('notice.readAll') }}</div> <div class="head-box-btn" @click="readAll">全部已读</div>
</div> </div>
<div v-loading="state.loading" class="content-box"> <div v-loading="state.loading" class="content-box">
<template v-if="newsList.length > 0"> <template v-if="newsList.length > 0">
@@ -13,13 +13,13 @@
<div class="content-box-time">{{ v.time }}</div> <div class="content-box-time">{{ v.time }}</div>
</div> </div>
<!-- 已读/未读 --> <!-- 已读/未读 -->
<span v-if="v.read" class="el-tag el-tag--success el-tag--mini read">{{ $t('notice.read') }}</span> <span v-if="v.read" class="el-tag el-tag--success el-tag--mini read">已读</span>
<span v-else class="el-tag el-tag--danger el-tag--mini read">{{ $t('notice.unread') }}</span> <span v-else class="el-tag el-tag--danger el-tag--mini read">未读</span>
</div> </div>
</template> </template>
<el-empty v-else :description="$t('notice.empty')"></el-empty> <el-empty v-else :description="'消息为空'"></el-empty>
</div> </div>
<div v-if="newsList.length > 0" class="foot-box" @click="onGoToGiteeClick">{{ $t('notice.viewMore') }}</div> <div v-if="newsList.length > 0" class="foot-box" @click="onGoToGiteeClick">前往gitee</div>
</div> </div>
</template> </template>

View File

@@ -33,13 +33,7 @@ import i18n from '@/lang/index';
// vxeTable // vxeTable
import VXETable from 'vxe-table'; import VXETable from 'vxe-table';
import vxeZhCN from 'vxe-table/es/locale/lang/zh-CN';
import vxeEnUS from 'vxe-table/es/locale/lang/en-US';
import 'vxe-table/lib/style.css'; import 'vxe-table/lib/style.css';
import { getVxeLanguage } from '@/enums/LanguageEnum';
VXETable.setI18n('zh-CN', vxeZhCN);
VXETable.setI18n('en-US', vxeEnUS);
VXETable.setLanguage(getVxeLanguage(i18n.global.locale.value));
VXETable.setConfig({ VXETable.setConfig({
zIndex: 999999 zIndex: 999999
}); });
@@ -50,8 +44,6 @@ ElDialog.props.closeOnClickModal.default = false;
const app = createApp(App); const app = createApp(App);
document.title = i18n.global.t('app.title');
app.use(HighLight); app.use(HighLight);
app.use(ElementIcons); app.use(ElementIcons);
app.use(router); app.use(router);

View File

@@ -3,8 +3,6 @@ import enUS from 'element-plus/es/locale/lang/en';
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { useStorage } from '@vueuse/core'; import { useStorage } from '@vueuse/core';
import { ref, reactive, computed } from 'vue'; import { ref, reactive, computed } from 'vue';
import { getLanguage, setDocumentLanguage } from '@/lang';
import { LanguageEnum } from '@/enums/LanguageEnum';
export const useAppStore = defineStore('app', () => { export const useAppStore = defineStore('app', () => {
const sidebarStatus = useStorage('sidebarStatus', '1'); const sidebarStatus = useStorage('sidebarStatus', '1');
@@ -17,10 +15,10 @@ export const useAppStore = defineStore('app', () => {
const size = useStorage<'large' | 'default' | 'small'>('size', 'default'); const size = useStorage<'large' | 'default' | 'small'>('size', 'default');
// 语言 // 语言
const language = useStorage<LanguageEnum>('language', getLanguage()); const language = useStorage('language', 'zh_CN');
const languageObj: Record<LanguageEnum, typeof zhCN> = { const languageObj: any = {
[LanguageEnum.en_US]: enUS, en_US: enUS,
[LanguageEnum.zh_CN]: zhCN zh_CN: zhCN
}; };
const locale = computed(() => { const locale = computed(() => {
return languageObj[language.value]; return languageObj[language.value];
@@ -55,9 +53,8 @@ export const useAppStore = defineStore('app', () => {
sidebar.hide = status; sidebar.hide = status;
}; };
const changeLanguage = (val: LanguageEnum): void => { const changeLanguage = (val: string): void => {
language.value = val; language.value = val;
setDocumentLanguage(val);
}; };
return { return {

View File

@@ -1,17 +1,14 @@
import defaultSettings from '@/settings'; import defaultSettings from '@/settings';
import { useSettingsStore } from '@/store/modules/settings'; import { useSettingsStore } from '@/store/modules/settings';
import i18n from '@/lang';
import { translateRouteTitle } from '@/utils/i18n';
/** /**
* 动态修改标题 * 动态修改标题
*/ */
export const useDynamicTitle = () => { export const useDynamicTitle = () => {
const settingsStore = useSettingsStore(); const settingsStore = useSettingsStore();
const appTitle = i18n.global.t('app.title');
if (settingsStore.dynamicTitle) { if (settingsStore.dynamicTitle) {
document.title = translateRouteTitle(settingsStore.title) + ' - ' + appTitle; document.title = settingsStore.title + ' - ' + import.meta.env.VITE_APP_TITLE;
} else { } else {
document.title = appTitle || (defaultSettings.title as string); document.title = defaultSettings.title as string;
} }
}; };

View File

@@ -6,20 +6,11 @@ import i18n from '@/lang/index';
* @param title 路由名称 * @param title 路由名称
* @returns {string} * @returns {string}
*/ */
export const translateRouteTitle = (title?: string): string => { export const translateRouteTitle = (title: string): string => {
const normalizedTitle = title?.trim(); const hasKey = i18n.global.te('route.' + title);
if (!normalizedTitle) { if (hasKey) {
return ''; const translatedTitle = i18n.global.t('route.' + title);
return translatedTitle;
} }
return title;
const titleKey = 'routeTitles.' + normalizedTitle;
if (i18n.global.te(titleKey)) {
return i18n.global.t(titleKey);
}
const routeKey = 'route.' + normalizedTitle;
if (i18n.global.te(routeKey)) {
return i18n.global.t(routeKey);
}
return normalizedTitle;
}; };

View File

@@ -19,8 +19,7 @@ export const isRelogin = { show: false };
export const globalHeaders = () => { export const globalHeaders = () => {
return { return {
Authorization: 'Bearer ' + getToken(), Authorization: 'Bearer ' + getToken(),
clientid: import.meta.env.VITE_APP_CLIENT_ID, clientid: import.meta.env.VITE_APP_CLIENT_ID
'Content-Language': getLanguage()
}; };
}; };
@@ -46,7 +45,7 @@ service.interceptors.request.use(
// 是否需要防止数据重复提交 // 是否需要防止数据重复提交
const isRepeatSubmit = config.headers?.repeatSubmit === false; const isRepeatSubmit = config.headers?.repeatSubmit === false;
// 是否需要加密 // 是否需要加密
const isEncrypt = config.headers?.isEncrypt === true || config.headers?.isEncrypt === 'true'; const isEncrypt = config.headers?.isEncrypt === 'true';
if (getToken() && !isToken) { if (getToken() && !isToken) {
config.headers['Authorization'] = 'Bearer ' + getToken(); // 让每个请求携带自定义token 请根据实际情况自行修改 config.headers['Authorization'] = 'Bearer ' + getToken(); // 让每个请求携带自定义token 请根据实际情况自行修改

View File

@@ -4,35 +4,72 @@
<div v-show="showSearch" class="mb-[10px]"> <div v-show="showSearch" class="mb-[10px]">
<el-card shadow="hover"> <el-card shadow="hover">
<el-form ref="queryFormRef" :model="queryParams" :inline="true"> <el-form ref="queryFormRef" :model="queryParams" :inline="true">
<el-form-item :label="$t('device.deviceNo')" prop="deviceNo"> <el-form-item label="设备编号" prop="deviceNo">
<el-input <el-input v-model="queryParams.deviceNo" placeholder="请输入设备编号" clearable @keyup.enter="handleQuery" />
v-model="queryParams.deviceNo"
:placeholder="$t('device.deviceNo')"
clearable
style="width: 200px"
@keyup.enter="handleQuery"
/>
</el-form-item> </el-form-item>
<el-form-item :label="$t('device.deviceName')" prop="deviceName"> <el-form-item label="设备名称" prop="deviceName">
<el-input <el-input v-model="queryParams.deviceName" placeholder="请输入设备名称" clearable @keyup.enter="handleQuery" />
v-model="queryParams.deviceName"
:placeholder="$t('device.deviceName')"
clearable
style="width: 200px"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item :label="$t('common.status')" prop="status">
<el-select v-model="queryParams.status" :placeholder="$t('common.pleaseSelect')" clearable style="width: 140px" @change="handleQuery">
<el-option :label="$t('device.status.online')" value="1" />
<el-option :label="$t('device.status.offline')" value="0" />
<el-option :label="$t('device.status.fault')" value="2" />
<el-option :label="$t('device.status.expired')" value="3" />
</el-select>
</el-form-item> </el-form-item>
<!-- <el-form-item label="WiFi名称" prop="wifiName">-->
<!-- <el-input v-model="queryParams.wifiName" placeholder="请输入WiFi名称" clearable @keyup.enter="handleQuery" />-->
<!-- </el-form-item>-->
<!-- <el-form-item label="WiFi密码" prop="wifiPassword">-->
<!-- <el-input v-model="queryParams.wifiPassword" placeholder="请输入WiFi密码" clearable @keyup.enter="handleQuery" />-->
<!-- </el-form-item>-->
<!-- <el-form-item label="强光开启" prop="light">-->
<!-- <el-select v-model="queryParams.light" placeholder="请选择" clearable @change="handleQuery">-->
<!-- <el-option label="不开启" value="0" />-->
<!-- <el-option label="开启" value="1" />-->
<!-- </el-select>-->
<!-- </el-form-item>-->
<!-- <el-form-item label="告警播放" prop="alertAudio">-->
<!-- <el-select v-model="queryParams.alertAudio" placeholder="请选择" clearable @change="handleQuery">-->
<!-- <el-option label="不播放" value="0" />-->
<!-- <el-option label="播放" value="1" />-->
<!-- </el-select>-->
<!-- </el-form-item>-->
<!-- <el-form-item label="识别模式" prop="patternPerception">-->
<!-- <el-select v-model="queryParams.patternPerception" placeholder="请选择" clearable @change="handleQuery">-->
<!-- <el-option label="蓝牙" value="0" />-->
<!-- <el-option label="蓝牙+人脸" value="1" />-->
<!-- </el-select>-->
<!-- </el-form-item>-->
<!-- <el-form-item label="告警内容" prop="audioContet">-->
<!-- <el-input v-model="queryParams.audioContet" placeholder="请输入告警内容" clearable @keyup.enter="handleQuery" />-->
<!-- </el-form-item>-->
<!-- <el-form-item label="打击距离" prop="strikeRange">-->
<!-- <el-input v-model="queryParams.strikeRange" placeholder="请输入打击距离" clearable @keyup.enter="handleQuery" />-->
<!-- </el-form-item>-->
<!-- <el-form-item label="打击间隔" prop="strikeInterval">-->
<!-- <el-input v-model="queryParams.strikeInterval" placeholder="请输入打击间隔" clearable @keyup.enter="handleQuery" />-->
<!-- </el-form-item>-->
<!-- <el-form-item label="容器数量" prop="container">-->
<!-- <el-input v-model="queryParams.container" placeholder="请输入容器数量" clearable @keyup.enter="handleQuery" />-->
<!-- </el-form-item>-->
<!-- <el-form-item label="打击位置" prop="strikePosition">-->
<!-- <el-input v-model="queryParams.strikePosition" placeholder="请输入打击位置" clearable @keyup.enter="handleQuery" />-->
<!-- </el-form-item>-->
<!-- <el-form-item label="自动布放开始时间" prop="startTime">-->
<!-- <el-date-picker clearable v-model="queryParams.startTime" type="date" value-format="YYYY-MM-DD" placeholder="请选择自动布放开始时间" />-->
<!-- </el-form-item>-->
<!-- <el-form-item label="自动布放结束时间" prop="endTime">-->
<!-- <el-date-picker clearable v-model="queryParams.endTime" type="date" value-format="YYYY-MM-DD" placeholder="请选择自动布放结束时间" />-->
<!-- </el-form-item>-->
<!-- <el-form-item label="工作模式 0-自动 1-半自动" prop="workMode">-->
<!-- <el-input v-model="queryParams.workMode" placeholder="请输入工作模式 0-自动 1-半自动" clearable @keyup.enter="handleQuery" />-->
<!-- </el-form-item>-->
<!-- <el-form-item label="设备型号" prop="deviceEm">-->
<!-- <el-input v-model="queryParams.deviceEm" placeholder="请输入设备型号" clearable @keyup.enter="handleQuery" />-->
<!-- </el-form-item>-->
<!-- <el-form-item label="设备序列号" prop="deviceSn">-->
<!-- <el-input v-model="queryParams.deviceSn" placeholder="请输入设备序列号" clearable @keyup.enter="handleQuery" />-->
<!-- </el-form-item>-->
<!-- <el-form-item label="固件版本" prop="fwVer">-->
<!-- <el-input v-model="queryParams.fwVer" placeholder="请输入固件版本" clearable @keyup.enter="handleQuery" />-->
<!-- </el-form-item>-->
<el-form-item> <el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">{{ $t('common.search') }}</el-button> <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">{{ $t('common.reset') }}</el-button> <el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item> </el-form-item>
</el-form> </el-form>
</el-card> </el-card>
@@ -42,93 +79,97 @@
<el-card shadow="never"> <el-card shadow="never">
<template #header> <template #header>
<el-row :gutter="10" class="mb8"> <el-row :gutter="10" class="mb8">
<!-- <el-col :span="1.5">-->
<!-- <el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['app:device:add']">新增</el-button>-->
<!-- </el-col>-->
<!-- <el-col :span="1.5">-->
<!-- <el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['app:device:edit']">修改</el-button>-->
<!-- </el-col>-->
<el-col :span="1.5"> <el-col :span="1.5">
<el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['app:device:add']">{{ $t('common.add') }}</el-button> <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['app:device:remove']"
</el-col> >删除</el-button
<el-col :span="1.5">
<el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['app:device:remove']">{{
$t('common.delete')
}}</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['app:device:export']">{{
$t('common.export')
}}</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="success"
plain
icon="PictureFilled"
:loading="batchQrLoading"
:disabled="deviceList.length === 0"
@click="batchDownloadQrCodes"
>{{ $t('device.batchDownloadQr') }}</el-button
> >
</el-col> </el-col>
<el-col :span="1.5">
<el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['app:device:export']">导出</el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar> <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row> </el-row>
</template> </template>
<el-table v-loading="loading" border :data="deviceList" @selection-change="handleSelectionChange"> <el-table v-loading="loading" border :data="deviceList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="50" align="center" fixed="left" /> <el-table-column type="selection" width="55" align="center" />
<el-table-column :label="$t('common.index')" type="index" width="60" align="center" /> <el-table-column label="" align="center" prop="id" v-if="true" />
<el-table-column :label="$t('device.deviceNo')" align="center" prop="deviceNo" min-width="140" show-overflow-tooltip /> <el-table-column label="设备编号" align="center" prop="deviceNo" />
<el-table-column :label="$t('device.deviceName')" align="center" prop="deviceName" min-width="120" show-overflow-tooltip /> <el-table-column label="设备名称" align="center" prop="deviceName" />
<el-table-column :label="$t('common.status')" align="center" prop="status" width="80"> <el-table-column label="状态" align="center" prop="status" width="100">
<template #default="scope"> <template #default="scope">
<el-tag :type="statusTagType(scope.row.status)" size="small"> <el-tag :type="statusTagType(scope.row.status)">
{{ statusLabel(scope.row.status, scope.row.statusLabel) }} {{ statusLabel(scope.row.status) }}
</el-tag> </el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column :label="$t('device.light')" align="center" prop="light" width="70"> <el-table-column label="WiFi名称" align="center" prop="wifiName" />
<el-table-column label="WiFi密码" align="center" prop="wifiPassword" />
<el-table-column label="强光开启" align="center" prop="light" width="100">
<template #default="scope"> <template #default="scope">
<el-tag :type="lightTagType(scope.row.light)" size="small"> <el-tag :type="lightTagType(scope.row.light)">
{{ lightLabel(scope.row.light) }} {{ lightLabel(scope.row.light) }}
</el-tag> </el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column :label="$t('device.alert')" align="center" prop="alertAudio" width="70"> <el-table-column label="告警播放" align="center" prop="alertAudio" width="100">
<template #default="scope"> <template #default="scope">
<el-tag :type="alertAudioTagType(scope.row.alertAudio)" size="small"> <el-tag :type="alertAudioTagType(scope.row.alertAudio)">
{{ alertAudioLabel(scope.row.alertAudio) }} {{ alertAudioLabel(scope.row.alertAudio) }}
</el-tag> </el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column :label="$t('device.perceptionMode')" align="center" prop="patternPerception" width="120"> <el-table-column label="识别模式" align="center" prop="patternPerception" width="120">
<template #default="scope"> <template #default="scope">
<el-tag :type="scope.row.patternPerception === '1' ? 'primary' : 'info'" size="small"> <el-tag :type="scope.row.patternPerception === '1' ? 'primary' : 'info'">
{{ patternPerceptionLabel(scope.row.patternPerception) }} {{ patternPerceptionLabel(scope.row.patternPerception) }}
</el-tag> </el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column :label="$t('device.alertContent')" align="center" prop="audioContet" min-width="140" show-overflow-tooltip> <el-table-column label="告警内容" align="center" prop="audioContet" min-width="120">
<template #default="scope"> <template #default="scope">
<span v-if="scope.row.audioContet">{{ scope.row.audioContet }}</span> <el-tooltip v-if="scope.row.audioContet" :content="scope.row.audioContet" placement="top" :show-after="200">
<span class="text-ellipsis">{{ scope.row.audioContet }}</span>
</el-tooltip>
<span v-else>-</span> <span v-else>-</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column :label="$t('device.modelShort')" align="center" prop="deviceEm" width="110" show-overflow-tooltip /> <el-table-column label="打击距离" align="center" prop="strikeRange" />
<el-table-column :label="$t('device.serialNumberShort')" align="center" prop="deviceSn" width="130" show-overflow-tooltip /> <el-table-column label="打击间隔" align="center" prop="strikeInterval" />
<el-table-column :label="$t('device.firmwareVersion')" align="center" prop="fwVer" width="100" /> <el-table-column label="容器数量" align="center" prop="container" />
<el-table-column :label="$t('device.qrCode')" align="center" width="90"> <el-table-column label="打击位置" align="center" prop="strikePosition" />
<el-table-column label="自动布放开始时间" align="center" prop="startTime" width="180">
<template #default="scope"> <template #default="scope">
<img <span>{{ parseTime(scope.row.startTime, '{y}-{m}-{d}') }}</span>
v-if="qrCodeMap[scope.row.id]"
:src="qrCodeMap[scope.row.id]"
@click="previewQrCode(scope.row)"
style="width: 60px; height: 60px; cursor: pointer; vertical-align: middle; object-fit: contain"
/>
<span v-else style="color: #ccc">-</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column :label="$t('common.operation')" align="center" width="100" fixed="right"> <el-table-column label="自动布放结束时间" align="center" prop="endTime" width="180">
<template #default="scope">
<span>{{ parseTime(scope.row.endTime, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
<el-table-column label="工作模式" align="center" prop="workMode" width="100">
<template #default="scope">
<el-tag :type="workModeTagType(scope.row.workMode)">
{{ workModeLabel(scope.row.workMode) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="设备型号" align="center" prop="deviceEm" />
<el-table-column label="设备序列号" align="center" prop="deviceSn" />
<el-table-column label="固件版本" align="center" prop="fwVer" />
<el-table-column label="操作" align="center" fixed="right" class-name="small-padding fixed-width">
<template #default="scope"> <template #default="scope">
<!-- <el-tooltip content="修改" placement="top">--> <!-- <el-tooltip content="修改" placement="top">-->
<!-- <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['app:device:edit']"></el-button>--> <!-- <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['app:device:edit']"></el-button>-->
<!-- </el-tooltip>--> <!-- </el-tooltip>-->
<el-tooltip :content="$t('common.delete')" placement="top"> <el-tooltip content="删除" placement="top">
<el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['app:device:remove']"></el-button> <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['app:device:remove']"></el-button>
</el-tooltip> </el-tooltip>
</template> </template>
@@ -137,230 +178,137 @@
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" /> <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
</el-card> </el-card>
<!-- 添加 / 修改设备 --> <!-- 添加或修改设备信息
<el-dialog :title="dialog.title" v-model="dialog.visible" width="700px" append-to-body> 对话框 -->
<el-form ref="deviceFormRef" :model="form" :rules="rules" label-width="110px" label-position="right"> <el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
<el-row :gutter="20"> <el-form ref="deviceFormRef" :model="form" :rules="rules" label-width="80px">
<el-col :span="12"> <el-form-item label="设备编号" prop="deviceNo">
<el-form-item :label="$t('device.deviceNo')" prop="deviceNo"> <el-input v-model="form.deviceNo" placeholder="请输入设备编号" />
<el-input v-model="form.deviceNo" :placeholder="$t('device.deviceNo')" />
</el-form-item> </el-form-item>
</el-col> <el-form-item label="设备名称" prop="deviceName">
<el-col :span="12"> <el-input v-model="form.deviceName" placeholder="请输入设备名称" />
<el-form-item :label="$t('device.deviceName')" prop="deviceName">
<el-input v-model="form.deviceName" :placeholder="$t('device.deviceName')" />
</el-form-item> </el-form-item>
</el-col> <el-form-item label="WiFi名称" prop="wifiName">
<el-col :span="12"> <el-input v-model="form.wifiName" placeholder="请输入WiFi名称" />
<el-form-item :label="$t('device.owner')" prop="userId">
<el-select
v-model="form.userId"
:placeholder="$t('device.selectOwner')"
filterable
clearable
:loading="appUserLoading"
style="width: 100%"
>
<el-option
v-for="user in appUserList"
:key="user.userId"
:label="`${user.nickName || user.userName} (${user.userName})`"
:value="user.userId"
/>
</el-select>
</el-form-item> </el-form-item>
</el-col> <el-form-item label="WiFi密码" prop="wifiPassword">
<el-col :span="12"> <el-input v-model="form.wifiPassword" placeholder="请输入WiFi密码" />
<el-form-item :label="$t('device.wifiName')" prop="wifiName">
<el-input v-model="form.wifiName" :placeholder="$t('device.wifiName')" />
</el-form-item> </el-form-item>
</el-col> <el-form-item label="强光开启" prop="light">
<el-col :span="12">
<el-form-item :label="$t('device.wifiPassword')" prop="wifiPassword">
<el-input v-model="form.wifiPassword" :placeholder="$t('device.wifiPassword')" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item :label="$t('device.model')" prop="deviceEm">
<el-input v-model="form.deviceEm" :placeholder="$t('device.model')" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item :label="$t('device.serialNumber')" prop="deviceSn">
<el-input v-model="form.deviceSn" :placeholder="$t('device.serialNumber')" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item :label="$t('device.firmwareVersion')" prop="fwVer">
<el-input v-model="form.fwVer" :placeholder="$t('device.firmwareVersion')" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item :label="$t('device.perceptionMode')" prop="patternPerception">
<el-select v-model="form.patternPerception" :placeholder="$t('common.pleaseSelect')">
<el-option :label="$t('device.perception.bluetooth')" value="0" />
<el-option :label="$t('device.perception.bluetoothFace')" value="1" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item :label="$t('device.workMode')" prop="workMode">
<el-select v-model="form.workMode" :placeholder="$t('common.pleaseSelect')">
<el-option :label="$t('device.work.automatic')" value="0" />
<el-option :label="$t('device.work.semiAutomatic')" value="1" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item :label="$t('device.lightEnabled')" prop="light">
<el-switch v-model="form.light" active-value="1" inactive-value="0" /> <el-switch v-model="form.light" active-value="1" inactive-value="0" />
</el-form-item> </el-form-item>
</el-col> <el-form-item label="告警播放" prop="alertAudio">
<el-col :span="12">
<el-form-item :label="$t('device.alertPlayback')" prop="alertAudio">
<el-switch v-model="form.alertAudio" active-value="1" inactive-value="0" /> <el-switch v-model="form.alertAudio" active-value="1" inactive-value="0" />
</el-form-item> </el-form-item>
</el-col> <el-form-item label="识别模式" prop="patternPerception">
<el-col :span="24"> <el-select v-model="form.patternPerception" placeholder="请选择识别模式">
<el-form-item :label="$t('device.alertContent')" prop="audioContet"> <el-option label="蓝牙" value="0" />
<el-input v-model="form.audioContet" :placeholder="$t('device.alertContent')" type="textarea" :rows="2" /> <el-option label="蓝牙+人脸" value="1" />
</el-select>
</el-form-item> </el-form-item>
</el-col> <el-form-item label="告警内容" prop="audioContet">
<el-col :span="8"> <el-input v-model="form.audioContet" placeholder="请输入告警内容" />
<el-form-item :label="$t('device.strikeRange')" prop="strikeRange">
<el-input v-model="form.strikeRange" :placeholder="$t('common.pleaseInput')" />
</el-form-item> </el-form-item>
</el-col> <el-form-item label="打击距离" prop="strikeRange">
<el-col :span="8"> <el-input v-model="form.strikeRange" placeholder="请输入打击距离" />
<el-form-item :label="$t('device.strikeInterval')" prop="strikeInterval">
<el-input v-model="form.strikeInterval" :placeholder="$t('common.pleaseInput')" />
</el-form-item> </el-form-item>
</el-col> <el-form-item label="打击间隔" prop="strikeInterval">
<el-col :span="8"> <el-input v-model="form.strikeInterval" placeholder="请输入打击间隔" />
<el-form-item :label="$t('device.containerCount')" prop="container">
<el-input v-model="form.container" :placeholder="$t('common.pleaseInput')" />
</el-form-item> </el-form-item>
</el-col> <el-form-item label="容器数量" prop="container">
<el-col :span="12"> <el-input v-model="form.container" placeholder="请输入容器数量" />
<el-form-item :label="$t('device.strikePosition')" prop="strikePosition">
<el-input v-model="form.strikePosition" :placeholder="$t('common.pleaseInput')" />
</el-form-item> </el-form-item>
</el-col> <el-form-item label="打击位置" prop="strikePosition">
<el-col :span="12"> <el-input v-model="form.strikePosition" placeholder="请输入打击位置" />
<el-form-item :label="$t('device.deploymentStart')" prop="startTime">
<el-date-picker
v-model="form.startTime"
type="datetime"
value-format="YYYY-MM-DD HH:mm:ss"
:placeholder="$t('device.selectTime')"
style="width: 100%"
/>
</el-form-item> </el-form-item>
</el-col> <el-form-item label="自动布放开始时间" prop="startTime">
<el-col :span="12"> <el-date-picker clearable v-model="form.startTime" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" placeholder="请选择自动布放开始时间">
<el-form-item :label="$t('device.deploymentEnd')" prop="endTime"> </el-date-picker>
<el-date-picker </el-form-item>
v-model="form.endTime" <el-form-item label="自动布放结束时间" prop="endTime">
type="datetime" <el-date-picker clearable v-model="form.endTime" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" placeholder="请选择自动布放结束时间">
value-format="YYYY-MM-DD HH:mm:ss" </el-date-picker>
:placeholder="$t('device.selectTime')" </el-form-item>
style="width: 100%" <el-form-item label="工作模式 0-自动 1-半自动" prop="workMode">
/> <el-input v-model="form.workMode" placeholder="请输入工作模式 0-自动 1-半自动" />
</el-form-item>
<el-form-item label="设备型号" prop="deviceEm">
<el-input v-model="form.deviceEm" placeholder="请输入设备型号" />
</el-form-item>
<el-form-item label="设备序列号" prop="deviceSn">
<el-input v-model="form.deviceSn" placeholder="请输入设备序列号" />
</el-form-item>
<el-form-item label="固件版本" prop="fwVer">
<el-input v-model="form.fwVer" placeholder="请输入固件版本" />
</el-form-item> </el-form-item>
</el-col>
</el-row>
</el-form> </el-form>
<template #footer> <template #footer>
<el-button :loading="buttonLoading" type="primary" @click="submitForm">{{ $t('common.confirm') }}</el-button> <div class="dialog-footer">
<el-button @click="cancel">{{ $t('common.cancel') }}</el-button> <el-button :loading="buttonLoading" type="primary" @click="submitForm"> </el-button>
</template> <el-button @click="cancel"> </el-button>
</el-dialog>
<!-- 二维码预览弹窗 -->
<el-dialog v-model="qrPreviewVisible" :title="$t('device.qrCode')" width="420px" append-to-body :close-on-click-modal="true">
<div style="text-align: center; padding: 10px 0">
<img v-if="qrPreviewSrc" :src="qrPreviewSrc" style="width: 340px; height: auto; display: inline-block" />
</div> </div>
<template #footer>
<el-button type="primary" :disabled="!qrPreviewSrc" @click="saveQrPreview">{{ $t('device.saveImage') }}</el-button>
<el-button @click="qrPreviewVisible = false">{{ $t('common.close') }}</el-button>
</template> </template>
</el-dialog> </el-dialog>
</div> </div>
</template> </template>
<script setup name="Device" lang="ts"> <script setup name="Device" lang="ts">
import { useI18n } from 'vue-i18n';
import { listDevice, getDevice, delDevice, addDevice, updateDevice } from '@/api/app/device'; import { listDevice, getDevice, delDevice, addDevice, updateDevice } from '@/api/app/device';
import { DeviceVO, DeviceQuery, DeviceForm } from '@/api/app/device/types'; import { DeviceVO, DeviceQuery, DeviceForm } from '@/api/app/device/types';
import { listUser, getUser } from '@/api/app/user';
import { UserVO } from '@/api/app/user/types';
import QRCode from 'qrcode';
import JSZip from 'jszip';
const { proxy } = getCurrentInstance() as ComponentInternalInstance; const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { t } = useI18n();
type TagType = 'primary' | 'success' | 'warning' | 'danger' | 'info';
/** 状态标签类型映射 */ /** 状态标签类型映射 */
const statusTagType = (status: string): TagType => { const statusTagType = (status: string): string => {
const map: Record<string, TagType> = { '0': 'info', '1': 'success', '2': 'danger', '3': 'warning' }; const map: Record<string, string> = { '0': 'info', '1': 'success', '2': 'danger', '3': 'warning' };
return map[status] || 'info'; return map[status] || 'info';
}; };
/** 优先显示后端按请求语言返回的状态标签 */ /** 状态中文标签 */
const statusLabel = (status: string, localizedLabel?: string): string => { const statusLabel = (status: string): string => {
if (localizedLabel) { const map: Record<string, string> = { '0': '离线', '1': '在线', '2': '故障', '3': '到期' };
return localizedLabel;
}
const map: Record<string, string> = {
'0': t('device.status.offline'),
'1': t('device.status.online'),
'2': t('device.status.fault'),
'3': t('device.status.expired')
};
return map[status] || status || '-'; return map[status] || status || '-';
}; };
/** 识别模式中文标签 */ /** 识别模式中文标签 */
const patternPerceptionLabel = (mode: string): string => { const patternPerceptionLabel = (mode: string): string => {
const map: Record<string, string> = { '0': t('device.perception.bluetooth'), '1': t('device.perception.bluetoothFace') }; const map: Record<string, string> = { '0': '蓝牙', '1': '蓝牙+人脸' };
return map[mode] || mode || '-'; return map[mode] || mode || '-';
}; };
/** 强光开启标签类型映射 */ /** 强光开启标签类型映射 */
const lightTagType = (light: string): TagType => { const lightTagType = (light: string): string => {
const map: Record<string, TagType> = { '0': 'info', '1': 'success' }; const map: Record<string, string> = { '0': 'info', '1': 'success' };
return map[light] || 'info'; return map[light] || 'info';
}; };
/** 强光开启中文标签 */ /** 强光开启中文标签 */
const lightLabel = (light: string): string => { const lightLabel = (light: string): string => {
const map: Record<string, string> = { '0': t('device.lightState.off'), '1': t('device.lightState.on') }; const map: Record<string, string> = { '0': '不开启', '1': '开启' };
return map[light] || light || '-'; return map[light] || light || '-';
}; };
/** 告警播放标签类型映射 */ /** 告警播放标签类型映射 */
const alertAudioTagType = (alertAudio: string): TagType => { const alertAudioTagType = (alertAudio: string): string => {
const map: Record<string, TagType> = { '0': 'info', '1': 'success' }; const map: Record<string, string> = { '0': 'info', '1': 'success' };
return map[alertAudio] || 'info'; return map[alertAudio] || 'info';
}; };
/** 告警播放中文标签 */ /** 告警播放中文标签 */
const alertAudioLabel = (alertAudio: string): string => { const alertAudioLabel = (alertAudio: string): string => {
const map: Record<string, string> = { '0': t('device.audioState.off'), '1': t('device.audioState.on') }; const map: Record<string, string> = { '0': '不播放', '1': '播放' };
return map[alertAudio] || alertAudio || '-'; return map[alertAudio] || alertAudio || '-';
}; };
/** 工作模式标签类型映射 */ /** 工作模式标签类型映射 */
const workModeTagType = (mode: string): TagType => { const workModeTagType = (mode: string): string => {
const map: Record<string, TagType> = { '0': 'success', '1': 'primary' }; const map: Record<string, string> = { '0': 'success', '1': 'primary' };
return map[mode] || 'info'; return map[mode] || 'info';
}; };
/** 工作模式中文标签 */ /** 工作模式中文标签 */
const workModeLabel = (mode: string): string => { const workModeLabel = (mode: string): string => {
const map: Record<string, string> = { '0': t('device.work.automatic'), '1': t('device.work.semiAutomatic') }; const map: Record<string, string> = { '0': '自动', '1': '半自动' };
return map[mode] || mode || '-'; return map[mode] || mode || '-';
}; };
@@ -372,8 +320,6 @@ const ids = ref<Array<string | number>>([]);
const single = ref(true); const single = ref(true);
const multiple = ref(true); const multiple = ref(true);
const total = ref(0); const total = ref(0);
const appUserList = ref<UserVO[]>([]);
const appUserLoading = ref(false);
const queryFormRef = ref<ElFormInstance>(); const queryFormRef = ref<ElFormInstance>();
const deviceFormRef = ref<ElFormInstance>(); const deviceFormRef = ref<ElFormInstance>();
@@ -383,149 +329,8 @@ const dialog = reactive<DialogOption>({
title: '' title: ''
}); });
// 二维码内联展示 — 用 ref 包装 plain object避免 reactive 动态 key 追踪问题
const qrCodeMap = ref<Record<string | number, string>>({});
/** 生成一张合成二维码(二维码 + 设备编号)→ data URL */
const makeCompositeQr = async (deviceNo: string): Promise<string> => {
const qrDataUrl = await QRCode.toDataURL(deviceNo, {
width: 300,
margin: 2,
color: { dark: '#000000', light: '#ffffff' }
});
return new Promise((resolve, reject) => {
const qrImg = new Image();
qrImg.onload = () => {
const canvas = document.createElement('canvas');
canvas.width = 340;
canvas.height = 310;
const ctx = canvas.getContext('2d')!;
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, canvas.width, canvas.height);
const qrSize = 280;
const qrX = (canvas.width - qrSize) / 2;
const qrY = 5;
ctx.drawImage(qrImg, qrX, qrY, qrSize, qrSize);
ctx.fillStyle = '#333333';
ctx.font = 'bold 22px sans-serif';
ctx.textAlign = 'center';
ctx.fillText(deviceNo, canvas.width / 2, qrY + qrSize + 6);
resolve(canvas.toDataURL('image/png'));
};
qrImg.onerror = () => reject(new Error(t('device.qrGenerationFailed')));
qrImg.src = qrDataUrl;
});
};
/** 生成所有设备的合成二维码 */
const generateQrCodes = async (list: DeviceVO[]) => {
const map = { ...qrCodeMap.value };
let changed = false;
for (const item of list) {
if (map[item.id]) continue;
try {
map[item.id] = await makeCompositeQr(item.deviceNo);
changed = true;
} catch {
// 生成失败,静默跳过
}
}
if (changed) qrCodeMap.value = map;
};
// 二维码预览
const qrPreviewVisible = ref(false);
const qrPreviewSrc = ref('');
const qrPreviewDeviceNo = ref('');
const previewQrCode = async (row: DeviceVO) => {
qrPreviewDeviceNo.value = row.deviceNo;
qrPreviewVisible.value = true;
// 如果有已生成的合成图直接用,否则实时生成
if (qrCodeMap.value[row.id]) {
qrPreviewSrc.value = qrCodeMap.value[row.id];
} else {
try {
qrPreviewSrc.value = await makeCompositeQr(row.deviceNo);
} catch {
proxy?.$modal.msgError(t('device.qrGenerationFailed'));
}
}
};
const saveQrPreview = () => {
if (!qrPreviewSrc.value) return;
const link = document.createElement('a');
link.download = `device_qr_${qrPreviewDeviceNo.value}.png`;
link.href = qrPreviewSrc.value;
link.click();
};
// 批量下载二维码
const batchQrLoading = ref(false);
const selectedDevices = ref<DeviceVO[]>([]);
const sanitizeQrFileName = (deviceNo: string | undefined, id: string | number) => {
const name = (deviceNo || '').replace(/[\\/:*?"<>|]/g, '_').trim();
return name || `device_${id}`;
};
const batchDownloadQrCodes = async () => {
const targetDevices = selectedDevices.value.length > 0 ? selectedDevices.value : deviceList.value;
if (targetDevices.length === 0) {
proxy?.$modal.msgWarning(t('device.noData'));
return;
}
batchQrLoading.value = true;
try {
// 确保所有二维码都已生成
const generateQrItem = async (item: DeviceVO): Promise<string> => {
if (qrCodeMap.value[item.id]) return qrCodeMap.value[item.id];
return makeCompositeQr(item.deviceNo);
};
const zip = new JSZip();
const usedNames = new Set<string>();
for (const item of targetDevices) {
const dataUrl = await generateQrItem(item);
// data:image/png;base64,xxxx → 提取 base64 部分
const base64 = dataUrl.split(',')[1];
if (base64) {
const baseName = sanitizeQrFileName(item.deviceNo, item.id);
let fileName = `${baseName}.png`;
let suffix = 1;
while (usedNames.has(fileName)) {
fileName = `${baseName}_${suffix++}.png`;
}
usedNames.add(fileName);
zip.file(fileName, base64, { base64: true });
}
}
const blob = await zip.generateAsync({ type: 'blob' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.download = `device_qr_batch_${new Date().toISOString().slice(0, 10)}.zip`;
link.href = url;
link.click();
window.setTimeout(() => URL.revokeObjectURL(url), 1000);
proxy?.$modal.msgSuccess(t('device.batchPackaged', { count: targetDevices.length }));
} catch {
proxy?.$modal.msgError(t('device.batchGenerationFailed'));
} finally {
batchQrLoading.value = false;
}
};
// WiFi密码显隐
const showPwd = reactive<Record<string | number, boolean>>({});
const togglePwd = (id: string | number) => {
showPwd[id] = !showPwd[id];
};
const initFormData: DeviceForm = { const initFormData: DeviceForm = {
id: undefined, id: undefined,
userId: undefined,
deviceNo: undefined, deviceNo: undefined,
deviceName: undefined, deviceName: undefined,
status: undefined, status: undefined,
@@ -572,14 +377,12 @@ const data = reactive<PageData<DeviceForm, DeviceQuery>>({
patternPerception: undefined, patternPerception: undefined,
params: {} params: {}
}, },
rules: {} rules: {
id: [{ required: true, message: '不能为空', trigger: 'blur' }]
}
}); });
const { queryParams, form } = toRefs(data); const { queryParams, form, rules } = toRefs(data);
const rules = computed(() => ({
id: [{ required: true, message: t('common.required'), trigger: 'blur' }],
userId: [{ required: true, message: t('device.selectOwner'), trigger: 'change' }]
}));
/** 查询设备信息 /** 查询设备信息
列表 */ 列表 */
@@ -588,9 +391,7 @@ const getList = async () => {
const res = await listDevice(queryParams.value); const res = await listDevice(queryParams.value);
deviceList.value = res.rows; deviceList.value = res.rows;
total.value = res.total; total.value = res.total;
selectedDevices.value = [];
loading.value = false; loading.value = false;
generateQrCodes(res.rows);
}; };
/** 取消按钮 */ /** 取消按钮 */
@@ -617,26 +418,8 @@ const resetQuery = () => {
handleQuery(); handleQuery();
}; };
const loadAppUsers = async () => {
if (appUserList.value.length > 0) return;
appUserLoading.value = true;
try {
const res = await listUser({ pageNum: 1, pageSize: 500, status: '0' });
appUserList.value = res.rows || [];
} finally {
appUserLoading.value = false;
}
};
const ensureAppUserOption = async (userId?: string | number) => {
if (userId === undefined || userId === null || appUserList.value.some((user) => user.userId == userId)) return;
const res = await getUser(userId);
if (res.data) appUserList.value.push(res.data);
};
/** 多选框选中数据 */ /** 多选框选中数据 */
const handleSelectionChange = (selection: DeviceVO[]) => { const handleSelectionChange = (selection: DeviceVO[]) => {
selectedDevices.value = selection;
ids.value = selection.map((item) => item.id); ids.value = selection.map((item) => item.id);
single.value = selection.length != 1; single.value = selection.length != 1;
multiple.value = !selection.length; multiple.value = !selection.length;
@@ -645,21 +428,18 @@ const handleSelectionChange = (selection: DeviceVO[]) => {
/** 新增按钮操作 */ /** 新增按钮操作 */
const handleAdd = () => { const handleAdd = () => {
reset(); reset();
loadAppUsers();
dialog.visible = true; dialog.visible = true;
dialog.title = t('device.addTitle'); dialog.title = '添加设备信息';
}; };
/** 修改按钮操作 */ /** 修改按钮操作 */
const handleUpdate = async (row?: DeviceVO) => { const handleUpdate = async (row?: DeviceVO) => {
reset(); reset();
await loadAppUsers();
const _id = row?.id || ids.value[0]; const _id = row?.id || ids.value[0];
const res = await getDevice(_id); const res = await getDevice(_id);
Object.assign(form.value, res.data); Object.assign(form.value, res.data);
await ensureAppUserOption(form.value.userId);
dialog.visible = true; dialog.visible = true;
dialog.title = t('device.editTitle'); dialog.title = '修改设备信息';
}; };
/** 提交按钮 */ /** 提交按钮 */
@@ -672,7 +452,7 @@ const submitForm = () => {
} else { } else {
await addDevice(form.value).finally(() => (buttonLoading.value = false)); await addDevice(form.value).finally(() => (buttonLoading.value = false));
} }
proxy?.$modal.msgSuccess(t('common.operationSuccess')); proxy?.$modal.msgSuccess('操作成功');
dialog.visible = false; dialog.visible = false;
await getList(); await getList();
} }
@@ -682,9 +462,9 @@ const submitForm = () => {
/** 删除按钮操作 */ /** 删除按钮操作 */
const handleDelete = async (row?: DeviceVO) => { const handleDelete = async (row?: DeviceVO) => {
const _ids = row?.id || ids.value; const _ids = row?.id || ids.value;
await proxy?.$modal.confirm(t('device.deleteConfirm', { ids: String(_ids) })).finally(() => (loading.value = false)); await proxy?.$modal.confirm('是否确认删除设备信息编号为"' + _ids + '"的数据项?').finally(() => (loading.value = false));
await delDevice(_ids); await delDevice(_ids);
proxy?.$modal.msgSuccess(t('common.deleteSuccess')); proxy?.$modal.msgSuccess('删除成功');
await getList(); await getList();
}; };
@@ -704,4 +484,14 @@ onMounted(() => {
}); });
</script> </script>
<style scoped></style> <style scoped>
.text-ellipsis {
display: inline-block;
max-width: 120px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
vertical-align: bottom;
cursor: default;
}
</style>

View File

@@ -4,23 +4,20 @@
<div v-show="showSearch" class="mb-[10px]"> <div v-show="showSearch" class="mb-[10px]">
<el-card shadow="hover"> <el-card shadow="hover">
<el-form ref="queryFormRef" :model="queryParams" :inline="true"> <el-form ref="queryFormRef" :model="queryParams" :inline="true">
<el-form-item :label="$t('message.category')" prop="category"> <el-form-item label="告警类型" prop="category">
<el-select v-model="queryParams.category" :placeholder="$t('common.pleaseSelect')" clearable style="width: 160px" @change="handleQuery"> <el-select v-model="queryParams.category" placeholder="请选择告警类型" clearable @change="handleQuery">
<el-option v-for="item in categoryOptions" :key="item.value" :label="item.label" :value="item.value" /> <el-option v-for="item in categoryOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item :label="$t('message.description')" prop="categoryName"> <!-- <el-form-item label="告警说明" prop="categoryName">-->
<el-input <!-- <el-input v-model="queryParams.categoryName" placeholder="请输入告警说明" clearable @keyup.enter="handleQuery" />-->
v-model="queryParams.categoryName" <!-- </el-form-item>-->
:placeholder="$t('common.pleaseInput')" <!-- <el-form-item label="视频地址" prop="video">
clearable <el-input v-model="queryParams.video" placeholder="请输入视频地址" clearable @keyup.enter="handleQuery" />
style="width: 200px" </el-form-item>-->
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item> <el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">{{ $t('common.search') }}</el-button> <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">{{ $t('common.reset') }}</el-button> <el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item> </el-form-item>
</el-form> </el-form>
</el-card> </el-card>
@@ -30,73 +27,58 @@
<el-card shadow="never"> <el-card shadow="never">
<template #header> <template #header>
<el-row :gutter="10" class="mb8"> <el-row :gutter="10" class="mb8">
<!-- <el-col :span="1.5">-->
<!-- <el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['app:message:add']">新增</el-button>-->
<!-- </el-col>-->
<!-- <el-col :span="1.5">-->
<!-- <el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['app:message:edit']">修改</el-button>-->
<!-- </el-col>-->
<el-col :span="1.5"> <el-col :span="1.5">
<el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['app:message:remove']">{{ <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['app:message:remove']"
$t('common.delete') >删除</el-button
}}</el-button> >
</el-col> </el-col>
<el-col :span="1.5"> <el-col :span="1.5">
<el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['app:message:export']">{{ <el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['app:message:export']">导出</el-button>
$t('common.export')
}}</el-button>
</el-col> </el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar> <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row> </el-row>
</template> </template>
<el-table v-loading="loading" border :data="messageList" @selection-change="handleSelectionChange"> <el-table v-loading="loading" border :data="messageList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="45" align="center" /> <el-table-column type="selection" width="55" align="center" />
<el-table-column type="index" :label="$t('common.index')" width="55" align="center" /> <el-table-column label="ID" align="center" prop="id" v-if="true" />
<el-table-column :label="$t('message.deviceId')" align="center" prop="deviceId" width="160" show-overflow-tooltip /> <el-table-column label="设备ID" align="center" prop="deviceId" v-if="true" />
<el-table-column :label="$t('message.deviceName')" align="center" prop="deviceName" width="160" show-overflow-tooltip /> <el-table-column label="设备名" align="center" prop="deviceName" v-if="true" />
<el-table-column :label="$t('message.category')" align="center" prop="category" width="130"> <el-table-column label="告警类型" align="center" prop="category" width="120">
<template #default="scope"> <template #default="scope">
<el-tag :type="categoryTagType(scope.row.category)" size="small"> <el-tag :type="categoryTagType(scope.row.category)">
{{ categoryLabel(scope.row.category, scope.row.categoryLabel) }} {{ categoryLabel(scope.row.category) }}
</el-tag> </el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column :label="$t('message.description')" align="center" prop="categoryName" width="130" show-overflow-tooltip /> <el-table-column label="告警说明" align="center" prop="categoryName" />
<el-table-column :label="$t('message.image')" align="center" width="200"> <el-table-column label="图片地址" align="center" prop="imageUrl" width="100">
<template #default="scope"> <template #default="scope">
<template v-if="scope.row.imageUrl"> <image-preview :src="scope.row.imageUrl" :width="50" :height="50" />
<el-image
v-for="(url, idx) in (scope.row.imageUrl || '').split(',').filter(Boolean).slice(0, 3)"
:key="idx"
:src="url"
:preview-src-list="(scope.row.imageUrl || '').split(',').filter(Boolean)"
:initial-index="Number(idx)"
fit="cover"
style="width: 48px; height: 48px; margin: 2px; border-radius: 4px; cursor: pointer"
/>
<el-tag
v-if="(scope.row.imageUrl || '').split(',').filter(Boolean).length > 3"
size="small"
type="info"
style="vertical-align: middle; margin-left: 4px"
>
+{{ (scope.row.imageUrl || '').split(',').filter(Boolean).length - 3 }}
</el-tag>
</template>
<span v-else class="text-gray-400 text-xs">-</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column :label="$t('message.video')" align="center" width="120"> <el-table-column label="视频地址" align="center" prop="video" min-width="120">
<template #default="scope"> <template #default="scope">
<template v-if="scope.row.video"> <template v-if="scope.row.video">
<el-button link type="primary" size="small" @click="handlePlayVideo(scope.row.video)"> <el-button link type="primary" @click="handlePlayVideo(scope.row.video)">
<el-icon><VideoPlay /></el-icon> <el-icon><VideoPlay /></el-icon> 播放
</el-button>
<el-button link type="success" size="small" @click="openVideoUrl(scope.row.video)" style="margin-left: 0">
<el-icon><Link /></el-icon>
</el-button> </el-button>
</template> </template>
<span v-else class="text-gray-400 text-xs">-</span> <span v-else class="text-gray-400 text-xs">-</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column :label="$t('common.operation')" align="center" width="90" fixed="right"> <el-table-column label="操作" align="center" fixed="right" class-name="small-padding fixed-width">
<template #default="scope"> <template #default="scope">
<el-tooltip :content="$t('common.delete')" placement="top"> <!-- <el-tooltip content="修改" placement="top">-->
<!-- <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['app:message:edit']"></el-button>-->
<!-- </el-tooltip>-->
<el-tooltip content="删除" placement="top">
<el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['app:message:remove']"></el-button> <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['app:message:remove']"></el-button>
</el-tooltip> </el-tooltip>
</template> </template>
@@ -105,49 +87,34 @@
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" /> <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
</el-card> </el-card>
<!-- 添加 / 修改告警消息 --> <!-- 添加修改告警消息列对话框 -->
<el-dialog :title="dialog.title" v-model="dialog.visible" width="620px" append-to-body> <el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
<el-form ref="messageFormRef" :model="form" :rules="rules" label-width="90px" label-position="right"> <el-form ref="messageFormRef" :model="form" :rules="rules" label-width="80px">
<el-row :gutter="20"> <el-form-item label="告警类型" prop="category">
<el-col :span="12"> <el-select v-model="form.category" placeholder="请选择告警类型">
<el-form-item :label="$t('message.category')" prop="category">
<el-select v-model="form.category" :placeholder="$t('common.pleaseSelect')">
<el-option v-for="item in categoryOptions" :key="item.value" :label="item.label" :value="item.value" /> <el-option v-for="item in categoryOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select> </el-select>
</el-form-item> </el-form-item>
</el-col> <el-form-item label="告警说明" prop="categoryName">
<el-col :span="12"> <el-input v-model="form.categoryName" placeholder="请输入告警说明" />
<el-form-item :label="$t('message.description')" prop="categoryName">
<el-input v-model="form.categoryName" :placeholder="$t('common.pleaseInput')" />
</el-form-item> </el-form-item>
</el-col> <el-form-item label="图片地址" prop="image">
<el-col :span="12">
<el-form-item :label="$t('message.image')" prop="image">
<image-upload v-model="form.image" /> <image-upload v-model="form.image" />
</el-form-item> </el-form-item>
</el-col> <el-form-item label="视频地址" prop="video">
<el-col :span="12"> <el-input v-model="form.video" placeholder="请输入视频地址" />
<el-form-item :label="$t('message.videoUrl')" prop="video">
<el-input v-model="form.video" :placeholder="$t('common.pleaseInput')" />
</el-form-item> </el-form-item>
</el-col>
</el-row>
</el-form> </el-form>
<template #footer> <template #footer>
<el-button :loading="buttonLoading" type="primary" @click="submitForm">{{ $t('common.confirm') }}</el-button> <div class="dialog-footer">
<el-button @click="cancel">{{ $t('common.cancel') }}</el-button> <el-button :loading="buttonLoading" type="primary" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
</div>
</template> </template>
</el-dialog> </el-dialog>
<!-- 视频播放 --> <!-- 视频播放弹窗 -->
<el-dialog <el-dialog title="视频播放" v-model="videoDialog.visible" width="720px" append-to-body :close-on-click-modal="false" @close="destroyPlayer">
v-model="videoDialog.visible"
:title="$t('message.videoPlayback')"
width="720px"
append-to-body
:close-on-click-modal="false"
@close="destroyPlayer"
>
<div ref="videoPlayerRef" class="video-player-wrap"></div> <div ref="videoPlayerRef" class="video-player-wrap"></div>
</el-dialog> </el-dialog>
</div> </div>
@@ -155,16 +122,13 @@
<script setup name="Message" lang="ts"> <script setup name="Message" lang="ts">
import { nextTick } from 'vue'; import { nextTick } from 'vue';
import { useI18n } from 'vue-i18n';
import { LanguageEnum } from '@/enums/LanguageEnum';
import { listMessage, getMessage, delMessage, addMessage, updateMessage } from '@/api/app/message'; import { listMessage, getMessage, delMessage, addMessage, updateMessage } from '@/api/app/message';
import { MessageVO, MessageQuery, MessageForm } from '@/api/app/message/types'; import { MessageVO, MessageQuery, MessageForm } from '@/api/app/message/types';
import { VideoPlay, Link } from '@element-plus/icons-vue'; import { VideoPlay } from '@element-plus/icons-vue';
import Player from 'xgplayer'; import Player from 'xgplayer';
import 'xgplayer-flv'; import 'xgplayer-flv';
const { proxy } = getCurrentInstance() as ComponentInternalInstance; const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { t, locale } = useI18n();
/** 视频播放器容器 ref */ /** 视频播放器容器 ref */
const videoPlayerRef = ref<HTMLDivElement | null>(null); const videoPlayerRef = ref<HTMLDivElement | null>(null);
@@ -177,12 +141,7 @@ const videoDialog = reactive<{ visible: boolean; url: string }>({
url: '' url: ''
}); });
/** 新窗口打开视频 */ /** 点击播放按钮 */
const openVideoUrl = (url: string) => {
window.open(url, '_blank', 'noopener,noreferrer');
};
/** 弹窗播放视频 */
const handlePlayVideo = (url: string) => { const handlePlayVideo = (url: string) => {
videoDialog.url = url; videoDialog.url = url;
videoDialog.visible = true; videoDialog.visible = true;
@@ -203,7 +162,6 @@ const initPlayer = (url: string) => {
playsinline: true, playsinline: true,
height: '100%', height: '100%',
width: '100%', width: '100%',
lang: locale.value === LanguageEnum.en_US ? 'en' : 'zh-cn',
flv: { flv: {
mediaDataSource: { mediaDataSource: {
isLive: true, isLive: true,
@@ -223,24 +181,21 @@ const destroyPlayer = () => {
}; };
/** 告警类型选项 */ /** 告警类型选项 */
const categoryOptions = computed(() => [ const categoryOptions = [
{ value: 1, label: t('message.categoryType.intrusion') }, { value: 1, label: '有人入侵' },
{ value: 2, label: t('message.categoryType.deviceFault') }, { value: 2, label: '设备故障' },
{ value: 3, label: t('message.categoryType.expiration') } { value: 3, label: '到期提醒' }
]); ];
/** 优先显示后端按请求语言返回的告警类型标签 */ /** 根据 category 值返回中文标签 */
const categoryLabel = (category: number | string, localizedLabel?: string): string => { const categoryLabel = (category: number): string => {
if (localizedLabel) { return categoryOptions.find((item) => item.value === category)?.label || category?.toString() || '-';
return localizedLabel;
}
return categoryOptions.value.find((item) => item.value == category)?.label || '-';
}; };
/** 根据 category 值返回 el-tag 类型 */ /** 根据 category 值返回 el-tag 类型 */
const categoryTagType = (category: number | string): 'danger' | 'warning' | 'info' => { const categoryTagType = (category: number): string => {
const map: Record<string, 'danger' | 'warning' | 'info'> = { '1': 'danger', '2': 'warning', '3': 'info' }; const map: Record<number, string> = { 1: 'danger', 2: 'warning', 3: 'info' };
return map[String(category)] || 'info'; return map[category] || 'info';
}; };
const messageList = ref<MessageVO[]>([]); const messageList = ref<MessageVO[]>([]);
@@ -278,13 +233,12 @@ const data = reactive<PageData<MessageForm, MessageQuery>>({
video: undefined, video: undefined,
params: {} params: {}
}, },
rules: {} rules: {
id: [{ required: true, message: '不能为空', trigger: 'blur' }]
}
}); });
const { queryParams, form } = toRefs(data); const { queryParams, form, rules } = toRefs(data);
const rules = computed(() => ({
id: [{ required: true, message: t('common.required'), trigger: 'blur' }]
}));
/** 查询告警消息列列表 */ /** 查询告警消息列列表 */
const getList = async () => { const getList = async () => {
@@ -330,7 +284,7 @@ const handleSelectionChange = (selection: MessageVO[]) => {
const handleAdd = () => { const handleAdd = () => {
reset(); reset();
dialog.visible = true; dialog.visible = true;
dialog.title = t('message.addTitle'); dialog.title = '添加告警消息列';
}; };
/** 修改按钮操作 */ /** 修改按钮操作 */
@@ -340,7 +294,7 @@ const handleUpdate = async (row?: MessageVO) => {
const res = await getMessage(_id); const res = await getMessage(_id);
Object.assign(form.value, res.data); Object.assign(form.value, res.data);
dialog.visible = true; dialog.visible = true;
dialog.title = t('message.editTitle'); dialog.title = '修改告警消息列';
}; };
/** 提交按钮 */ /** 提交按钮 */
@@ -353,7 +307,7 @@ const submitForm = () => {
} else { } else {
await addMessage(form.value).finally(() => (buttonLoading.value = false)); await addMessage(form.value).finally(() => (buttonLoading.value = false));
} }
proxy?.$modal.msgSuccess(t('common.operationSuccess')); proxy?.$modal.msgSuccess('操作成功');
dialog.visible = false; dialog.visible = false;
await getList(); await getList();
} }
@@ -363,9 +317,9 @@ const submitForm = () => {
/** 删除按钮操作 */ /** 删除按钮操作 */
const handleDelete = async (row?: MessageVO) => { const handleDelete = async (row?: MessageVO) => {
const _ids = row?.id || ids.value; const _ids = row?.id || ids.value;
await proxy?.$modal.confirm(t('message.deleteConfirm', { ids: String(_ids) })).finally(() => (loading.value = false)); await proxy?.$modal.confirm('是否确认删除告警消息列编号为"' + _ids + '"的数据项?').finally(() => (loading.value = false));
await delMessage(_ids); await delMessage(_ids);
proxy?.$modal.msgSuccess(t('common.deleteSuccess')); proxy?.$modal.msgSuccess('删除成功');
await getList(); await getList();
}; };

View File

@@ -193,32 +193,10 @@ import { UserVO, UserQuery, UserForm } from '@/api/app/user/types';
const { proxy } = getCurrentInstance() as ComponentInternalInstance; const { proxy } = getCurrentInstance() as ComponentInternalInstance;
/** 解析后端存储的 JSON 数组,并兼容旧的逗号分隔格式 */ /** 将逗号分隔的人脸地址拆分为数组 */
const faceUrls = (faceAddr?: string | string[]): string[] => { const faceUrls = (faceAddr: string): string[] => {
const normalizeUrls = (urls: unknown[]): string[] => if (!faceAddr) return [];
urls.filter((url): url is string => typeof url === 'string').map((url) => url.trim()).filter(Boolean); return faceAddr.split(',').map(u => u.trim()).filter(Boolean);
if (Array.isArray(faceAddr)) {
return normalizeUrls(faceAddr);
}
if (!faceAddr?.trim()) {
return [];
}
const value = faceAddr.trim();
try {
const parsed: unknown = JSON.parse(value);
if (Array.isArray(parsed)) {
return normalizeUrls(parsed);
}
if (typeof parsed === 'string' && parsed.trim()) {
return [parsed.trim()];
}
} catch {
// 兼容历史数据url1,url2
}
return value.split(',').map((url) => url.trim()).filter(Boolean);
}; };
const userList = ref<UserVO[]>([]); const userList = ref<UserVO[]>([]);

View File

@@ -1,13 +1,14 @@
<template> <template>
<div class="app-container home"> <div class="app-container home">
<div class="welcome-wrapper"> <div class="welcome-wrapper">
<h1 class="welcome-title">{{ $t('home.welcome') }}</h1> <h1 class="welcome-title">欢迎使用智能防御机管理系统</h1>
<p class="welcome-desc">{{ $t('home.slogan') }}</p> <p class="welcome-desc">智能防御 · 安全守护</p>
</div> </div>
</div> </div>
</template> </template>
<script setup name="Index" lang="ts"></script> <script setup name="Index" lang="ts">
</script>
<style lang="scss" scoped> <style lang="scss" scoped>
.home { .home {

View File

@@ -1,116 +1,5 @@
<template> <template>
<div class="login"> <div class="login">
<!-- 左侧品牌区新增驭鲲智防 · REINKUN 智能安防动画 -->
<aside class="brand">
<div class="sweep"></div>
<header class="brand-head">
<svg width="40" height="40" viewBox="0 0 40 40" fill="none">
<rect x="2" y="2" width="36" height="36" rx="10" stroke="rgba(125,211,252,.4)" stroke-width="1.5" fill="rgba(56,189,248,.08)" />
<circle cx="20" cy="20" r="11" stroke="#38bdf8" stroke-width="2" />
<path d="M20 13.5c3 3.8 4.8 6.1 4.8 8.6a4.8 4.8 0 1 1-9.6 0c0-2.5 1.8-4.8 4.8-8.6z" fill="url(#lg)" />
<defs>
<linearGradient id="lg" x1="20" y1="13" x2="20" y2="27" gradientUnits="userSpaceOnUse">
<stop stop-color="#38bdf8" />
<stop offset="1" stop-color="#22d3ee" />
</linearGradient>
</defs>
</svg>
<div class="brand-name">驭鲲智防<small>REINKUN SMART DEFENSE</small></div>
</header>
<!-- 产品场景动画AI 识别 锁定 水枪驱离 -->
<div class="scene">
<div class="live"><i></i>LIVE · 01 号机位</div>
<div class="status">
<span class="s1">AI 巡检中 · 人形检测</span>
<span class="s2">入侵目标已锁定</span>
<span class="s3">水枪驱离 · 喷射中</span>
</div>
<svg viewBox="0 0 520 320">
<defs>
<radialGradient id="lensGlow" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="#7dd3fc" />
<stop offset="100%" stop-color="#0ea5e9" />
</radialGradient>
</defs>
<!-- 地面参考线 -->
<line x1="40" y1="270" x2="480" y2="270" stroke="rgba(148,163,184,.18)" stroke-width="1.5" stroke-dasharray="2 8" />
<!-- 雷达波纹 -->
<circle class="ring" cx="220" cy="158" r="34" fill="none" stroke="#38bdf8" stroke-width="1.2" />
<circle class="ring r2" cx="220" cy="158" r="34" fill="none" stroke="#38bdf8" stroke-width="1.2" />
<circle class="ring r3" cx="220" cy="158" r="34" fill="none" stroke="#22d3ee" stroke-width="1" />
<!-- 摄像头吊装 -->
<g transform="translate(150,140) rotate(12)">
<rect x="-4" y="-64" width="8" height="46" rx="3.5" fill="#1e293b" stroke="rgba(148,163,184,.25)" />
<circle cx="0" cy="-14" r="7" fill="#334155" />
<rect x="-8" y="-14" width="88" height="36" rx="11" fill="#1e293b" stroke="rgba(148,163,184,.28)" />
<rect x="10" y="-20" width="34" height="7" rx="3.5" fill="#334155" />
<circle class="led" cx="6" cy="4" r="2.6" fill="#ef4444" />
<rect x="66" y="-10" width="14" height="28" rx="6" fill="#0f172a" />
<circle cx="73" cy="4" r="7.5" fill="#0b1220" stroke="url(#lensGlow)" stroke-width="2" />
<circle cx="73" cy="4" r="2.4" fill="#7dd3fc" />
<!-- 水枪喷头 -->
<rect x="76" y="11" width="10" height="6" rx="2.5" fill="#0ea5e9" />
</g>
<!-- 水柱SMIL 同步到 6s 节奏33%66% 喷射窗口 -->
<g>
<circle r="3.6" fill="#7dd3fc">
<animateMotion dur="6s" repeatCount="indefinite" path="M 232 172 Q 322 118 424 208" keyPoints="0;0;1;1" keyTimes="0;0.34;0.60;1" calcMode="linear" />
<animate attributeName="opacity" dur="6s" repeatCount="indefinite" values="0;0;1;1;0;0" keyTimes="0;0.32;0.34;0.60;0.63;1" />
</circle>
<circle r="3" fill="#a5f3fc">
<animateMotion dur="6s" repeatCount="indefinite" path="M 232 172 Q 322 118 424 208" keyPoints="0;0;1;1" keyTimes="0;0.37;0.63;1" calcMode="linear" />
<animate attributeName="opacity" dur="6s" repeatCount="indefinite" values="0;0;1;1;0;0" keyTimes="0;0.35;0.37;0.63;0.66;1" />
</circle>
<circle r="3.4" fill="#67e8f9">
<animateMotion dur="6s" repeatCount="indefinite" path="M 232 172 Q 322 118 424 208" keyPoints="0;0;1;1" keyTimes="0;0.40;0.66;1" calcMode="linear" />
<animate attributeName="opacity" dur="6s" repeatCount="indefinite" values="0;0;1;1;0;0" keyTimes="0;0.38;0.40;0.66;0.69;1" />
</circle>
<circle r="2.6" fill="#bae6fd">
<animateMotion dur="6s" repeatCount="indefinite" path="M 232 172 Q 322 118 424 208" keyPoints="0;0;1;1" keyTimes="0;0.43;0.69;1" calcMode="linear" />
<animate attributeName="opacity" dur="6s" repeatCount="indefinite" values="0;0;1;1;0;0" keyTimes="0;0.41;0.43;0.69;0.72;1" />
</circle>
<circle r="3.1" fill="#7dd3fc">
<animateMotion dur="6s" repeatCount="indefinite" path="M 232 172 Q 322 118 424 208" keyPoints="0;0;1;1" keyTimes="0;0.46;0.72;1" calcMode="linear" />
<animate attributeName="opacity" dur="6s" repeatCount="indefinite" values="0;0;1;1;0;0" keyTimes="0;0.44;0.46;0.72;0.75;1" />
</circle>
<!-- 落点水花 -->
<ellipse cx="424" cy="210" rx="4" ry="2.5" fill="none" stroke="#7dd3fc" stroke-width="1.6">
<animate attributeName="rx" dur="6s" repeatCount="indefinite" values="3;3;16;16" keyTimes="0;0.56;0.75;1" />
<animate attributeName="ry" dur="6s" repeatCount="indefinite" values="2;2;8;8" keyTimes="0;0.56;0.75;1" />
<animate attributeName="opacity" dur="6s" repeatCount="indefinite" values="0;0;.9;0;0" keyTimes="0;0.56;0.6;0.75;1" />
</ellipse>
</g>
<!-- 入侵者剪影 + 检测框 -->
<g class="figure" style="transform-origin: 424px 268px">
<g fill="#3f4f66">
<circle cx="424" cy="206" r="11" />
<path d="M410 220 q14 -9 28 0 l4 34 h-10 l-3 -16 -5 0 -3 16 h-10 z" />
</g>
<g class="alert" fill="#ef4444">
<circle cx="424" cy="206" r="11" />
<path d="M410 220 q14 -9 28 0 l4 34 h-10 l-3 -16 -5 0 -3 16 h-10 z" />
</g>
</g>
<g class="det">
<rect x="398" y="188" width="52" height="74" fill="rgba(248,113,113,.06)" stroke="none" />
<path d="M398 200 v-12 h12 M438 188 h12 v12 M398 250 v12 h12 M438 262 h12 v-12" stroke="#f87171" stroke-width="2.4" fill="none" stroke-linecap="round" />
<rect x="392" y="168" width="64" height="15" rx="4" fill="rgba(248,113,113,.92)" />
<text x="424" y="179" text-anchor="middle" font-size="9.5" fill="#fff">入侵目标 98.6%</text>
</g>
</svg>
</div>
</aside>
<!-- 右侧登录表单原有内容未改动 -->
<div class="login-main">
<el-form ref="loginRef" :model="loginForm" :rules="loginRules" class="login-form"> <el-form ref="loginRef" :model="loginForm" :rules="loginRules" class="login-form">
<div class="title-box"> <div class="title-box">
<h3 class="title">{{ title }}</h3> <h3 class="title">{{ title }}</h3>
@@ -182,10 +71,9 @@
</div> </div>
</el-form-item> </el-form-item>
</el-form> </el-form>
<!-- 底部仅右侧表单区域内显示避免跨背景看不清 --> <!-- 底部 -->
<div class="el-login-footer"> <div class="el-login-footer">
<span>© 2026 驭鲲智能科技湖北有限公司 · 鄂ICP备2026000000号</span> <!-- <span>Copyright © 2018-2026 疯狂的狮子Li All Rights Reserved.</span>-->
</div>
</div> </div>
</div> </div>
</template> </template>
@@ -201,10 +89,10 @@ import { useI18n } from 'vue-i18n';
const { proxy } = getCurrentInstance() as ComponentInternalInstance; const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const title = import.meta.env.VITE_APP_TITLE;
const userStore = useUserStore(); const userStore = useUserStore();
const router = useRouter(); const router = useRouter();
const { t } = useI18n(); const { t } = useI18n();
const title = computed(() => t('app.title'));
const loginForm = ref<LoginData>({ const loginForm = ref<LoginData>({
tenantId: '000000', tenantId: '000000',
@@ -217,12 +105,12 @@ const loginForm = ref<LoginData>({
uuid: '' uuid: ''
} as LoginData); } as LoginData);
const loginRules = computed<ElFormRules>(() => ({ const loginRules: ElFormRules = {
tenantId: [{ required: true, trigger: 'blur', message: t('login.rule.tenantId.required') }], tenantId: [{ required: true, trigger: 'blur', message: t('login.rule.tenantId.required') }],
username: [{ required: true, trigger: 'blur', message: t('login.rule.username.required') }], username: [{ required: true, trigger: 'blur', message: t('login.rule.username.required') }],
password: [{ required: true, trigger: 'blur', message: t('login.rule.password.required') }], password: [{ required: true, trigger: 'blur', message: t('login.rule.password.required') }],
code: [{ required: true, trigger: 'change', message: t('login.rule.code.required') }] code: [{ required: true, trigger: 'change', message: t('login.rule.code.required') }]
})); };
const codeUrl = ref(''); const codeUrl = ref('');
const loading = ref(false); const loading = ref(false);
@@ -348,315 +236,13 @@ onMounted(() => {
<style lang="scss" scoped> <style lang="scss" scoped>
.login { .login {
display: flex;
height: 100%;
background: #ffffff;
}
.login-main {
position: relative;
flex: 1;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
} height: 100%;
background-image: url('../assets/images/login-background.jpg');
/* ============ 左侧品牌区 ============ */ background-size: cover;
.brand { background-position: center;
position: relative;
flex: 0 0 70%;
overflow: hidden;
display: flex;
flex-direction: column;
padding: 44px 48px 32px;
color: #e2e8f0;
background:
radial-gradient(900px 500px at 30% 20%, #10264a 0%, transparent 60%),
linear-gradient(160deg, #0b1b33 0%, #081426 55%, #071122 100%);
}
.brand::before {
/* 网格 */
content: '';
position: absolute;
inset: 0;
background-image:
linear-gradient(rgba(148, 163, 184, 0.055) 1px, transparent 1px),
linear-gradient(90deg, rgba(148, 163, 184, 0.055) 1px, transparent 1px);
background-size: 44px 44px;
mask-image: radial-gradient(75% 70% at 50% 42%, #000 30%, transparent 100%);
}
.sweep {
/* 雷达扫描 */
position: absolute;
left: 50%;
top: 44%;
width: 680px;
height: 680px;
transform: translate(-50%, -50%);
border-radius: 50%;
background: conic-gradient(from 0deg, rgba(34, 211, 238, 0.14), rgba(34, 211, 238, 0.04) 70deg, transparent 90deg);
animation: sweep 9s linear infinite;
pointer-events: none;
}
@keyframes sweep {
to {
transform: translate(-50%, -50%) rotate(360deg);
}
}
.brand-head {
position: relative;
display: flex;
align-items: center;
gap: 12px;
z-index: 2;
}
.brand-name {
font-size: 19px;
font-weight: 700;
letter-spacing: 0.5px;
small {
display: block;
font-size: 10px;
font-weight: 500;
letter-spacing: 2.5px;
color: #7dd3fc;
margin-top: 2px;
}
}
.scene {
position: relative;
z-index: 1;
margin: auto;
width: 100%;
max-width: 640px;
svg {
display: block;
width: 100%;
height: auto;
}
.live {
position: absolute;
top: 6px;
left: 4px;
display: flex;
align-items: center;
gap: 6px;
font-size: 11px;
letter-spacing: 1.5px;
color: #fca5a5;
i {
width: 7px;
height: 7px;
border-radius: 50%;
background: #ef4444;
animation: blink 1.4s infinite;
}
}
}
@keyframes blink {
50% {
opacity: 0.25;
}
}
.status {
position: absolute;
top: 0;
right: 0;
height: 30px;
min-width: 170px;
span {
position: absolute;
right: 0;
top: 0;
white-space: nowrap;
font-size: 12px;
padding: 5px 12px;
border-radius: 999px;
background: rgba(8, 20, 38, 0.72);
border: 1px solid rgba(125, 211, 252, 0.22);
opacity: 0;
}
.s1 {
color: #7dd3fc;
animation: st1 6s infinite;
}
.s2 {
color: #fca5a5;
border-color: rgba(248, 113, 113, 0.35);
animation: st2 6s infinite;
}
.s3 {
color: #67e8f9;
animation: st3 6s infinite;
}
}
@keyframes st1 {
0%,
22% {
opacity: 1;
}
26%,
100% {
opacity: 0;
}
}
@keyframes st2 {
0%,
24% {
opacity: 0;
}
28%,
44% {
opacity: 1;
}
48%,
100% {
opacity: 0;
}
}
@keyframes st3 {
0%,
46% {
opacity: 0;
}
50%,
74% {
opacity: 1;
}
78%,
100% {
opacity: 0;
}
}
.ring {
transform-box: fill-box;
transform-origin: center;
animation: ring 3s ease-out infinite;
opacity: 0;
&.r2 {
animation-delay: 1s;
}
&.r3 {
animation-delay: 2s;
}
}
@keyframes ring {
0% {
transform: scale(0.35);
opacity: 0.55;
}
80%,
100% {
transform: scale(2.6);
opacity: 0;
}
}
.led {
animation: blink 1.6s infinite;
}
.det {
opacity: 0;
animation: det 6s infinite;
}
@keyframes det {
0%,
22% {
opacity: 0;
}
26%,
76% {
opacity: 1;
}
80%,
100% {
opacity: 0;
}
}
.figure {
animation: recoil 6s infinite;
.alert {
opacity: 0;
animation: alert 6s infinite;
}
}
@keyframes recoil {
0%,
40% {
transform: translate(0, 0) rotate(0deg);
}
44% {
transform: translate(4px, 0) rotate(1.5deg);
}
48% {
transform: translate(-5px, 0) rotate(-2deg);
}
52% {
transform: translate(6px, 0) rotate(2deg);
}
56% {
transform: translate(-4px, 0) rotate(-1.5deg);
}
60% {
transform: translate(5px, 0) rotate(1.5deg);
}
64% {
transform: translate(-2px, 0) rotate(-0.5deg);
}
70%,
100% {
transform: translate(0, 0) rotate(0deg);
}
}
@keyframes alert {
0%,
32% {
opacity: 0;
}
36%,
66% {
opacity: 0.9;
}
72%,
100% {
opacity: 0;
}
}
/* 窄屏隐藏品牌区,表单占满 */
@media (max-width: 900px) {
.brand {
display: none;
}
} }
.title-box { .title-box {
@@ -756,12 +342,11 @@ onMounted(() => {
.el-login-footer { .el-login-footer {
height: 40px; height: 40px;
line-height: 40px; line-height: 40px;
position: absolute; position: fixed;
bottom: 0; bottom: 0;
left: 0;
width: 100%; width: 100%;
text-align: center; text-align: center;
color: rgba(100, 116, 139, 0.85); color: rgba(255, 255, 255, 0.75);
font-family: Arial, serif; font-family: Arial, serif;
font-size: 12px; font-size: 12px;
letter-spacing: 1px; letter-spacing: 1px;
@@ -773,10 +358,6 @@ onMounted(() => {
} }
:global(html.dark) { :global(html.dark) {
.login {
background: #0b1220;
}
.login-form { .login-form {
background: rgba(17, 24, 39, 0.9); background: rgba(17, 24, 39, 0.9);
border-color: rgba(148, 163, 184, 0.2); border-color: rgba(148, 163, 184, 0.2);

View File

@@ -67,7 +67,7 @@
</el-form> </el-form>
<!-- 底部 --> <!-- 底部 -->
<div class="el-register-footer"> <div class="el-register-footer">
<span>Copyright © 2018-2026. All Rights Reserved.</span> <span>Copyright © 2018-2026 疯狂的狮子Li All Rights Reserved.</span>
</div> </div>
</div> </div>
</template> </template>
@@ -80,10 +80,10 @@ import { useI18n } from 'vue-i18n';
const { proxy } = getCurrentInstance() as ComponentInternalInstance; const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const title = import.meta.env.VITE_APP_TITLE;
const router = useRouter(); const router = useRouter();
const { t } = useI18n(); const { t } = useI18n();
const title = computed(() => t('app.title'));
const registerForm = ref<RegisterForm>({ const registerForm = ref<RegisterForm>({
tenantId: '', tenantId: '',
@@ -106,7 +106,7 @@ const equalToPassword = (rule: any, value: string, callback: any) => {
} }
}; };
const registerRules = computed<ElFormRules>(() => ({ const registerRules: ElFormRules = {
tenantId: [{ required: true, trigger: 'blur', message: t('register.rule.tenantId.required') }], tenantId: [{ required: true, trigger: 'blur', message: t('register.rule.tenantId.required') }],
username: [ username: [
{ required: true, trigger: 'blur', message: t('register.rule.username.required') }, { required: true, trigger: 'blur', message: t('register.rule.username.required') },
@@ -122,7 +122,7 @@ const registerRules = computed<ElFormRules>(() => ({
{ required: true, validator: equalToPassword, trigger: 'blur' } { required: true, validator: equalToPassword, trigger: 'blur' }
], ],
code: [{ required: true, trigger: 'change', message: t('register.rule.code.required') }] code: [{ required: true, trigger: 'change', message: t('register.rule.code.required') }]
})); };
const codeUrl = ref(''); const codeUrl = ref('');
const loading = ref(false); const loading = ref(false);
const captchaEnabled = ref(true); const captchaEnabled = ref(true);
@@ -137,7 +137,8 @@ const handleRegister = () => {
const [err] = await to(register(registerForm.value)); const [err] = await to(register(registerForm.value));
if (!err) { if (!err) {
const username = registerForm.value.username; const username = registerForm.value.username;
await ElMessageBox.alert('<span style="color: red; ">' + t('register.registerSuccess', { username }) + '</span>', t('common.systemPrompt'), { await ElMessageBox.alert('<span style="color: red; ">' + t('register.registerSuccess', { username }) + '</font>', '系统提示', {
app: undefined,
dangerouslyUseHTMLString: true, dangerouslyUseHTMLString: true,
type: 'success' type: 'success'
}); });

View File

@@ -24,14 +24,9 @@ export default defineConfig(({ mode, command }) => {
open: true, open: true,
proxy: { proxy: {
[env.VITE_APP_BASE_API]: { [env.VITE_APP_BASE_API]: {
target: 'http://localhost:8001', target: 'http://localhost:8080',
changeOrigin: true, changeOrigin: true,
ws: true, ws: true,
configure: (proxy) => {
proxy.on('proxyReq', (proxyReq) => {
proxyReq.removeHeader('origin');
});
},
rewrite: (path) => path.replace(new RegExp('^' + env.VITE_APP_BASE_API), '') rewrite: (path) => path.replace(new RegExp('^' + env.VITE_APP_BASE_API), '')
} }
} }