Compare commits

...

18 Commits

Author SHA1 Message Date
yuhaiming
ab7ec17ae0 优化 2026-07-28 10:19:36 +08:00
yuhaiming
f5dc2e2c3a feat: add app management pages 2026-07-01 10:18:30 +08:00
疯狂的狮子Li
6bc3c618fe !262 发布 v5.6.0-v2.6.0 版本 新年第一版
Merge pull request !262 from 疯狂的狮子Li/dev
2026-03-24 03:49:37 +00:00
疯狂的狮子Li
0076f5f6f7 🦁🦁🦁发布 v5.6.0-v2.6.0 版本 新年第一版 2026-03-24 11:47:41 +08:00
疯狂的狮子Li
ef5ea98a03 update 替代有问题的插件 2026-03-19 15:55:39 +08:00
gssong
73f2374c72 add 增加流程实例权限 2026-03-19 10:27:06 +08:00
gssong
9dcb392220 Merge remote-tracking branch 'origin/dev' into dev 2026-03-19 09:58:02 +08:00
gssong
54636ac14f add 补充流程定义权限 2026-03-19 09:57:56 +08:00
Lau
51a852caea update 修改vben5前端仓库地址 2026-03-13 14:25:48 +00:00
疯狂的狮子Li
767b00c257 Revert "update 优化 将logininfor规范化为loginInfo"
This reverts commit c9f9fbed49.
2026-03-13 15:05:10 +08:00
疯狂的狮子Li
c9f9fbed49 update 优化 将logininfor规范化为loginInfo 2026-03-13 14:58:55 +08:00
疯狂的狮子Li
5c9c940588 !257 发布 5.5.3-2.5.3 版本 提前祝大家新年快乐
Merge pull request !257 from 疯狂的狮子Li/dev
2026-01-23 06:04:29 +00:00
疯狂的狮子Li
dfd1dc29d1 !256 fix 修复 代码漏改问题
Merge pull request !256 from 疯狂的狮子Li/dev
2025-12-23 05:56:54 +00:00
疯狂的狮子Li
b411505b19 !255 发布 5.5.2-2.5.2 版本 2025年最后一版
Merge pull request !255 from 疯狂的狮子Li/dev
2025-12-23 01:41:45 +00:00
疯狂的狮子Li
52ea8895d6 !246 发布 5.5.1-2.5.1 日常依赖升级bug修复
Merge pull request !246 from 疯狂的狮子Li/dev
2025-10-28 03:22:49 +00:00
疯狂的狮子Li
1b46739799 !241 发布 5.5.0-2.5.0 喜迎国庆
Merge pull request !241 from 疯狂的狮子Li/dev
2025-09-22 03:18:41 +00:00
疯狂的狮子Li
b000788785 !222 发布 5.4.1-2.4.1 小步迭代修复问题
Merge pull request !222 from 疯狂的狮子Li/dev
2025-07-01 01:13:49 +00:00
疯狂的狮子Li
2dc094c1db !212 发布 5.4.0-2.4.0 正式版
Merge pull request !212 from 疯狂的狮子Li/dev
2025-05-29 03:18:30 +00:00
62 changed files with 5522 additions and 414 deletions

View File

@@ -1,6 +1,6 @@
# 页面标题 # 页面标题
VITE_APP_TITLE = RuoYi-Vue-Plus多租户管理系统 VITE_APP_TITLE = 驭鲲智防系统
VITE_APP_LOGO_TITLE = RuoYi-Vue-Plus VITE_APP_LOGO_TITLE = 驭鲲智防系统
# 开发环境配置 # 开发环境配置
VITE_APP_ENV = 'development' VITE_APP_ENV = 'development'
@@ -12,19 +12,19 @@ VITE_APP_BASE_API = '/dev-api'
VITE_APP_CONTEXT_PATH = '/' VITE_APP_CONTEXT_PATH = '/'
# 监控地址 # 监控地址
VITE_APP_MONITOR_ADMIN = 'http://localhost:9090/admin/applications' VITE_APP_MONITOR_ADMIN = 'http://localhost:9001/admin/applications'
# SnailJob 控制台地址 # SnailJob 控制台地址
VITE_APP_SNAILJOB_ADMIN = 'http://localhost:8800/snail-job' VITE_APP_SNAILJOB_ADMIN = 'http://localhost:9002/snail-job'
VITE_APP_PORT = 80 VITE_APP_PORT = 80
# 接口加密功能开关(如需关闭 后端也必须对应关闭) # 接口加密功能开关(如需关闭 后端也必须对应关闭)
VITE_APP_ENCRYPT = true VITE_APP_ENCRYPT = true
# 接口加密传输 RSA 公钥与后端解密私钥对应 如更换需前后端一同更换 # 接口加密传输 RSA 公钥与后端解密私钥对应 如更换需前后端一同更换
VITE_APP_RSA_PUBLIC_KEY = 'MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ==' VITE_APP_RSA_PUBLIC_KEY = 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtVIQm+1AWMfkT1uVOyugKQwBeHtgpulABOrVfaL7Xzc1/rKSdLdjYZLZnnOU+rdE1jqz20MXDBozK22spH/j53d9nFLftHkMCRUlfc0MWEqtSla+2MdzElIHEbq4CSizZOcT8QXCRw7V2OuDqnC7XRQmnpA5Ks1WaKH5V0aoKWztjsqZV6vVWPqyDC94mvu/Q/9e6KVfkqAzR+afuo9OoBFewDGQ6jfqyu1TxoGZrfSr5lXOVeADf0CVfWCDze5y4f7er4JjPHWA2+yv8Scd9bqVJ4Q+/yR2Gd516Mi11OgBKc8GdkuA4/kfmXc7n91SMjgQP6DlTVuPc/6KnB/pyQIDAQAB'
# 接口响应解密 RSA 私钥与后端加密公钥对应 如更换需前后端一同更换 # 接口响应解密 RSA 私钥与后端加密公钥对应 如更换需前后端一同更换
VITE_APP_RSA_PRIVATE_KEY = 'MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAmc3CuPiGL/LcIIm7zryCEIbl1SPzBkr75E2VMtxegyZ1lYRD+7TZGAPkvIsBcaMs6Nsy0L78n2qh+lIZMpLH8wIDAQABAkEAk82Mhz0tlv6IVCyIcw/s3f0E+WLmtPFyR9/WtV3Y5aaejUkU60JpX4m5xNR2VaqOLTZAYjW8Wy0aXr3zYIhhQQIhAMfqR9oFdYw1J9SsNc+CrhugAvKTi0+BF6VoL6psWhvbAiEAxPPNTmrkmrXwdm/pQQu3UOQmc2vCZ5tiKpW10CgJi8kCIFGkL6utxw93Ncj4exE/gPLvKcT+1Emnoox+O9kRXss5AiAMtYLJDaLEzPrAWcZeeSgSIzbL+ecokmFKSDDcRske6QIgSMkHedwND1olF8vlKsJUGK3BcdtM8w4Xq7BpSBwsloE=' 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'
# 客户端id # 客户端id
VITE_APP_CLIENT_ID = 'e5cd7e4891bf95d1d19206ce24a7b32e' VITE_APP_CLIENT_ID = 'e5cd7e4891bf95d1d19206ce24a7b32e'
@@ -34,3 +34,4 @@ VITE_APP_WEBSOCKET = false
# sse 开关 # sse 开关
VITE_APP_SSE = true VITE_APP_SSE = true

View File

@@ -1,6 +1,6 @@
# 页面标题 # 页面标题
VITE_APP_TITLE = RuoYi-Vue-Plus多租户管理系统 VITE_APP_TITLE = 驭鲲智防系统
VITE_APP_LOGO_TITLE = RuoYi-Vue-Plus VITE_APP_LOGO_TITLE = 驭鲲智防系统
# 生产环境配置 # 生产环境配置
VITE_APP_ENV = 'production' VITE_APP_ENV = 'production'
@@ -23,11 +23,12 @@ VITE_BUILD_COMPRESS = gzip
VITE_APP_PORT = 80 VITE_APP_PORT = 80
# 接口加密功能开关(如需关闭 后端也必须对应关闭) # 接口加密功能开关(如需关闭 后端也必须对应关闭)
# defensive_machine 后端 api-decrypt.enabled = true
VITE_APP_ENCRYPT = true VITE_APP_ENCRYPT = true
# 接口加密传输 RSA 公钥与后端解密私钥对应 如更换需前后端一同更换 # 接口加密传输 RSA 公钥与后端解密私钥对应 如更换需前后端一同更换
VITE_APP_RSA_PUBLIC_KEY = 'MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ==' VITE_APP_RSA_PUBLIC_KEY = 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtVIQm+1AWMfkT1uVOyugKQwBeHtgpulABOrVfaL7Xzc1/rKSdLdjYZLZnnOU+rdE1jqz20MXDBozK22spH/j53d9nFLftHkMCRUlfc0MWEqtSla+2MdzElIHEbq4CSizZOcT8QXCRw7V2OuDqnC7XRQmnpA5Ks1WaKH5V0aoKWztjsqZV6vVWPqyDC94mvu/Q/9e6KVfkqAzR+afuo9OoBFewDGQ6jfqyu1TxoGZrfSr5lXOVeADf0CVfWCDze5y4f7er4JjPHWA2+yv8Scd9bqVJ4Q+/yR2Gd516Mi11OgBKc8GdkuA4/kfmXc7n91SMjgQP6DlTVuPc/6KnB/pyQIDAQAB'
# 接口响应解密 RSA 私钥与后端加密公钥对应 如更换需前后端一同更换 # 接口响应解密 RSA 私钥与后端加密公钥对应 如更换需前后端一同更换
VITE_APP_RSA_PRIVATE_KEY = 'MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAmc3CuPiGL/LcIIm7zryCEIbl1SPzBkr75E2VMtxegyZ1lYRD+7TZGAPkvIsBcaMs6Nsy0L78n2qh+lIZMpLH8wIDAQABAkEAk82Mhz0tlv6IVCyIcw/s3f0E+WLmtPFyR9/WtV3Y5aaejUkU60JpX4m5xNR2VaqOLTZAYjW8Wy0aXr3zYIhhQQIhAMfqR9oFdYw1J9SsNc+CrhugAvKTi0+BF6VoL6psWhvbAiEAxPPNTmrkmrXwdm/pQQu3UOQmc2vCZ5tiKpW10CgJi8kCIFGkL6utxw93Ncj4exE/gPLvKcT+1Emnoox+O9kRXss5AiAMtYLJDaLEzPrAWcZeeSgSIzbL+ecokmFKSDDcRske6QIgSMkHedwND1olF8vlKsJUGK3BcdtM8w4Xq7BpSBwsloE=' 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'
# 客户端id # 客户端id
VITE_APP_CLIENT_ID = 'e5cd7e4891bf95d1d19206ce24a7b32e' VITE_APP_CLIENT_ID = 'e5cd7e4891bf95d1d19206ce24a7b32e'

View File

@@ -1,7 +1,7 @@
## 平台简介 ## 平台简介
- 本仓库为前端技术栈 [Vue3](https://v3.cn.vuejs.org) + [TS](https://www.typescriptlang.org/) + [Element Plus](https://element-plus.org/zh-CN) + [Vite](https://cn.vitejs.dev) 版本。 - 本仓库为前端技术栈 [Vue3](https://v3.cn.vuejs.org) + [TS](https://www.typescriptlang.org/) + [Element Plus](https://element-plus.org/zh-CN) + [Vite](https://cn.vitejs.dev) 版本。
- 成员项目: 基于 vben5(ant-design-vue) 的前端项目 [ruoyi-plus-vben5](https://gitee.com/dapppp/ruoyi-plus-vben5) - 成员项目: 基于 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) - 成员项目: 基于soybean 的前端项目 [ruoyi-plus-soybean](https://gitee.com/xlsea/ruoyi-plus-soybean)
## 配套后端代码仓库地址 ## 配套后端代码仓库地址

BIN
dm-ui-dist.zip Normal file

Binary file not shown.

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" href="/favicon.ico" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<title>%VITE_APP_TITLE%</title> <title>%VITE_APP_TITLE%</title>
<!--[if lt IE 11 <!--[if lt IE 11
]><script> ]><script>

View File

@@ -1,7 +1,7 @@
{ {
"$schema": "https://json.schemastore.org/package", "$schema": "https://json.schemastore.org/package",
"name": "ruoyi-vue-plus", "name": "ruoyi-vue-plus",
"version": "5.5.3-2.5.3", "version": "5.6.0-2.6.0",
"description": "RuoYi-Vue-Plus多租户管理系统", "description": "RuoYi-Vue-Plus多租户管理系统",
"author": "LionLi", "author": "LionLi",
"license": "MIT", "license": "MIT",
@@ -35,8 +35,10 @@
"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",
@@ -44,7 +46,9 @@
"vue-json-pretty": "2.6.0", "vue-json-pretty": "2.6.0",
"vue-router": "5.0.3", "vue-router": "5.0.3",
"vue-types": "6.0.0", "vue-types": "6.0.0",
"vxe-table": "4.18.1" "vxe-table": "4.18.1",
"xgplayer": "^3.0.24",
"xgplayer-flv": "^3.0.24"
}, },
"devDependencies": { "devDependencies": {
"@iconify/json": "^2.2.448", "@iconify/json": "^2.2.448",
@@ -53,6 +57,7 @@
"@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",
@@ -74,7 +79,6 @@
"unplugin-vue-components": "31.0.0", "unplugin-vue-components": "31.0.0",
"unplugin-vue-setup-extend-plus": "1.0.1", "unplugin-vue-setup-extend-plus": "1.0.1",
"vite": "7.3.1", "vite": "7.3.1",
"vite-plugin-compression": "0.5.1",
"vite-plugin-svg-icons-ng": "^1.5.2", "vite-plugin-svg-icons-ng": "^1.5.2",
"vite-plugin-vue-devtools": "8.0.7", "vite-plugin-vue-devtools": "8.0.7",
"vitest": "4.0.18", "vitest": "4.0.18",

View File

@@ -0,0 +1,90 @@
# Put this server block in the BaoTa site's Nginx config.
# Replace server_name with your domain, or keep _ for IP-based access.
server {
listen 8000;
server_name _;
root /www/wwwroot/dm-ui/dist;
index index.html;
client_max_body_size 20m;
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss image/svg+xml;
location / {
try_files $uri $uri/ /index.html;
}
location ^~ /assets/ {
try_files $uri =404;
expires 30d;
add_header Cache-Control "public, immutable";
}
location ^~ /prod-api/resource/sse {
rewrite ^/prod-api/(.*)$ /$1 break;
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
location ^~ /prod-api/resource/websocket {
rewrite ^/prod-api/(.*)$ /$1 break;
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
location ^~ /prod-api/ {
rewrite ^/prod-api/(.*)$ /$1 break;
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
}
location = /admin {
return 301 /admin/;
}
location ^~ /admin/ {
proxy_pass http://127.0.0.1:9001/admin/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location = /snail-job {
return 301 /snail-job/;
}
location ^~ /snail-job/ {
proxy_pass http://127.0.0.1:9002/snail-job/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}

View File

@@ -0,0 +1,68 @@
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
import { DeviceVO, DeviceForm, DeviceQuery } from '@/api/app/device/types';
/**
* 查询设备信息
列表
* @param query
* @returns {*}
*/
export const listDevice = (query?: DeviceQuery): AxiosPromise<DeviceVO[]> => {
return request({
url: '/app/device/list',
method: 'get',
params: query
});
};
/**
* 查询设备信息
详细
* @param id
*/
export const getDevice = (id: string | number): AxiosPromise<DeviceVO> => {
return request({
url: '/app/device/' + id,
method: 'get'
});
};
/**
* 新增设备信息
* @param data
*/
export const addDevice = (data: DeviceForm) => {
return request({
url: '/app/device',
method: 'post',
data: data
});
};
/**
* 修改设备信息
* @param data
*/
export const updateDevice = (data: DeviceForm) => {
return request({
url: '/app/device',
method: 'put',
data: data
});
};
/**
* 删除设备信息
* @param id
*/
export const delDevice = (id: string | number | Array<string | number>) => {
return request({
url: '/app/device/' + id,
method: 'delete'
});
};

320
src/api/app/device/types.ts Normal file
View File

@@ -0,0 +1,320 @@
export interface DeviceVO {
/**
*
*/
id: string | number;
/**
* 绑定用户 ID
*/
userId?: string | number;
/**
* 设备编号
*/
deviceNo: string;
/**
* 设备名称
*/
deviceName: string;
/**
* 状态 1-在线 0-离线 2-故障 3-到期
*/
status: string;
/**
* 后端按请求语言返回的状态展示文案
*/
statusLabel?: string;
/**
* WiFi名称
*/
wifiName: string;
/**
* WiFi密码
*/
wifiPassword: string;
/**
* 强光开启
*/
light: string;
/**
* 告警播放
*/
alertAudio: string;
/**
* 告警内容
*/
audioContet: string;
/**
* 打击距离
*/
strikeRange: string;
/**
* 打击间隔
*/
strikeInterval: string;
/**
* 容器数量
*/
container: string;
/**
* 打击位置
*/
strikePosition: string;
/**
* 自动布放开始时间
*/
startTime: string;
/**
* 自动布放结束时间
*/
endTime: string;
/**
* 工作模式 0-自动 1-半自动
*/
workMode: string;
/**
* 设备型号
*/
deviceEm: string;
/**
* 设备序列号
*/
deviceSn: string;
/**
* 固件版本
*/
fwVer: string;
/**
* 识别模式 0-蓝牙 1-蓝牙+人脸
*/
patternPerception?: string;
}
export interface DeviceForm extends BaseEntity {
/**
*
*/
id?: string | number;
/**
* 绑定用户 ID
*/
userId?: string | number;
/**
* 设备编号
*/
deviceNo?: string;
/**
* 设备名称
*/
deviceName?: string;
/**
* 状态 1-在线 0-离线 2-故障 3-到期
*/
status?: string;
/**
* WiFi名称
*/
wifiName?: string;
/**
* WiFi密码
*/
wifiPassword?: string;
/**
* 强光开启
*/
light?: string;
/**
* 告警播放
*/
alertAudio?: string;
/**
* 告警内容
*/
audioContet?: string;
/**
* 打击距离
*/
strikeRange?: string;
/**
* 打击间隔
*/
strikeInterval?: string;
/**
* 容器数量
*/
container?: string;
/**
* 打击位置
*/
strikePosition?: string;
/**
* 自动布放开始时间
*/
startTime?: string;
/**
* 自动布放结束时间
*/
endTime?: string;
/**
* 工作模式 0-自动 1-半自动
*/
workMode?: string;
/**
* 设备型号
*/
deviceEm?: string;
/**
* 设备序列号
*/
deviceSn?: string;
/**
* 固件版本
*/
fwVer?: string;
/**
* 识别模式 0-蓝牙 1-蓝牙+人脸
*/
patternPerception?: string;
}
export interface DeviceQuery extends PageQuery {
/**
* 设备编号
*/
deviceNo?: string;
/**
* 设备名称
*/
deviceName?: string;
/**
* 状态 1-在线 0-离线 2-故障 3-到期
*/
status?: string;
/**
* WiFi名称
*/
wifiName?: string;
/**
* WiFi密码
*/
wifiPassword?: string;
/**
* 强光开启
*/
light?: string;
/**
* 告警播放
*/
alertAudio?: string;
/**
* 告警内容
*/
audioContet?: string;
/**
* 打击距离
*/
strikeRange?: string;
/**
* 打击间隔
*/
strikeInterval?: string;
/**
* 容器数量
*/
container?: string;
/**
* 打击位置
*/
strikePosition?: string;
/**
* 自动布放开始时间
*/
startTime?: string;
/**
* 自动布放结束时间
*/
endTime?: string;
/**
* 工作模式 0-自动 1-半自动
*/
workMode?: string;
/**
* 设备型号
*/
deviceEm?: string;
/**
* 设备序列号
*/
deviceSn?: string;
/**
* 固件版本
*/
fwVer?: string;
/**
* 识别模式 0-蓝牙 1-蓝牙+人脸
*/
patternPerception?: string;
/**
* 日期范围参数
*/
params?: any;
}

View File

@@ -0,0 +1,63 @@
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
import { DeviceUserVO, DeviceUserForm, DeviceUserQuery } from '@/api/app/deviceUser/types';
/**
* 查询设备成员关联列表
* @param query
* @returns {*}
*/
export const listDeviceUser = (query?: DeviceUserQuery): AxiosPromise<DeviceUserVO[]> => {
return request({
url: '/app/deviceUser/list',
method: 'get',
params: query
});
};
/**
* 查询设备成员关联详细
* @param deviceId
*/
export const getDeviceUser = (deviceId: string | number): AxiosPromise<DeviceUserVO> => {
return request({
url: '/app/deviceUser/' + deviceId,
method: 'get'
});
};
/**
* 新增设备成员关联
* @param data
*/
export const addDeviceUser = (data: DeviceUserForm) => {
return request({
url: '/app/deviceUser',
method: 'post',
data: data
});
};
/**
* 修改设备成员关联
* @param data
*/
export const updateDeviceUser = (data: DeviceUserForm) => {
return request({
url: '/app/deviceUser',
method: 'put',
data: data
});
};
/**
* 删除设备成员关联
* @param deviceId
*/
export const delDeviceUser = (deviceId: string | number | Array<string | number>) => {
return request({
url: '/app/deviceUser/' + deviceId,
method: 'delete'
});
};

View File

@@ -0,0 +1,78 @@
export interface DeviceUserVO {
/**
*
*/
deviceId: string | number;
/**
* 设备编号
*/
deviceNo: string;
/**
* 设备名称
*/
deviceName: string;
/**
* 用户ID
*/
userId: string | number;
/**
* 用户昵称
*/
nickName: string;
}
export interface DeviceUserForm extends BaseEntity {
/**
*
*/
deviceId?: string | number;
/**
* 设备编号
*/
deviceNo?: string;
/**
* 设备名称
*/
deviceName?: string;
/**
* 用户ID
*/
userId?: string | number;
/**
* 用户昵称
*/
nickName?: string;
}
export interface DeviceUserQuery extends PageQuery {
/**
* 设备编号
*/
deviceNo?: string;
/**
* 设备名称
*/
deviceName?: string;
/**
* 用户昵称
*/
nickName?: string;
/**
* 日期范围参数
*/
params?: any;
}

View File

@@ -0,0 +1,80 @@
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
import { FamilyVO, FamilyForm, FamilyQuery } from '@/api/app/family/types';
/**
* 查询家庭成员列表
* @param query
* @returns {*}
*/
export const listFamily = (query?: FamilyQuery): AxiosPromise<FamilyVO[]> => {
return request({
url: '/app/family/list',
method: 'get',
params: query
});
};
/**
* 查询家庭成员详细
* @param id
*/
export const getFamily = (id: string | number): AxiosPromise<FamilyVO> => {
return request({
url: '/app/family/' + id,
method: 'get'
});
};
/**
* 新增家庭成员
* @param data
*/
export const addFamily = (data: FamilyForm) => {
return request({
url: '/app/family',
method: 'post',
data: data
});
};
/**
* 修改家庭成员
* @param data
*/
export const updateFamily = (data: FamilyForm) => {
return request({
url: '/app/family',
method: 'put',
data: data
});
};
/**
* 删除家庭成员
* @param id
*/
export const delFamily = (id: string | number | Array<string | number>) => {
return request({
url: '/app/family/' + id,
method: 'delete'
});
};
/**
* 家庭成员状态修改
* @param id 家庭成员ID
* @param status 状态
*/
export const changeFamilyStatus = (id: string | number, status: string) => {
const data = {
id,
status
};
return request({
url: '/app/family/changeStatus',
method: 'put',
data: data
});
};

View File

@@ -0,0 +1,83 @@
export interface FamilyVO {
/**
*
*/
id: string | number;
/**
* 家长id
*/
parentId: string | number;
/**
* 家长昵称
*/
parentName: string;
/**
* 成员id
*/
userId: string | number;
/**
* 账号状态0正常 1停用
*/
status: string;
}
export interface FamilyForm extends BaseEntity {
/**
*
*/
id?: string | number;
/**
* 家长id
*/
parentId?: string | number;
/**
* 家长昵称
*/
parentName?: string;
/**
* 成员id
*/
userId?: string | number;
/**
* 账号状态0正常 1停用
*/
status?: string;
}
export interface FamilyQuery extends PageQuery {
/**
* 家长id
*/
parentId?: string | number;
/**
* 家长昵称
*/
parentName?: string;
/**
* 成员id
*/
userId?: string | number;
/**
* 账号状态0正常 1停用
*/
status?: string;
/**
* 日期范围参数
*/
params?: any;
}

View File

@@ -0,0 +1,63 @@
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
import { FamilyDeviceVO, FamilyDeviceForm, FamilyDeviceQuery } from '@/api/app/familyDevice/types';
/**
* 查询家庭设备关联列表
* @param query
* @returns {*}
*/
export const listFamilyDevice = (query?: FamilyDeviceQuery): AxiosPromise<FamilyDeviceVO[]> => {
return request({
url: '/app/familyDevice/list',
method: 'get',
params: query
});
};
/**
* 查询家庭设备关联详细
* @param userId
*/
export const getFamilyDevice = (userId: string | number): AxiosPromise<FamilyDeviceVO> => {
return request({
url: '/app/familyDevice/' + userId,
method: 'get'
});
};
/**
* 新增家庭设备关联
* @param data
*/
export const addFamilyDevice = (data: FamilyDeviceForm) => {
return request({
url: '/app/familyDevice',
method: 'post',
data: data
});
};
/**
* 修改家庭设备关联
* @param data
*/
export const updateFamilyDevice = (data: FamilyDeviceForm) => {
return request({
url: '/app/familyDevice',
method: 'put',
data: data
});
};
/**
* 删除家庭设备关联
* @param userId
*/
export const delFamilyDevice = (userId: string | number | Array<string | number>) => {
return request({
url: '/app/familyDevice/' + userId,
method: 'delete'
});
};

View File

@@ -0,0 +1,78 @@
export interface FamilyDeviceVO {
/**
* 用户ID
*/
userId: string | number;
/**
* 用户昵称
*/
nickName: string;
/**
*
*/
deviceId: string | number;
/**
* 设备编号
*/
deviceNo: string;
/**
* 设备名称
*/
deviceName: string;
}
export interface FamilyDeviceForm extends BaseEntity {
/**
* 用户ID
*/
userId?: string | number;
/**
* 用户昵称
*/
nickName?: string;
/**
*
*/
deviceId?: string | number;
/**
* 设备编号
*/
deviceNo?: string;
/**
* 设备名称
*/
deviceName?: string;
}
export interface FamilyDeviceQuery extends PageQuery {
/**
* 用户昵称
*/
nickName?: string;
/**
* 设备编号
*/
deviceNo?: string;
/**
* 设备名称
*/
deviceName?: string;
/**
* 日期范围参数
*/
params?: any;
}

View File

@@ -0,0 +1,63 @@
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
import { MessageVO, MessageForm, MessageQuery } from '@/api/app/message/types';
/**
* 查询告警消息列列表
* @param query
* @returns {*}
*/
export const listMessage = (query?: MessageQuery): AxiosPromise<MessageVO[]> => {
return request({
url: '/app/message/list',
method: 'get',
params: query
});
};
/**
* 查询告警消息列详细
* @param id
*/
export const getMessage = (id: string | number): AxiosPromise<MessageVO> => {
return request({
url: '/app/message/' + id,
method: 'get'
});
};
/**
* 新增告警消息列
* @param data
*/
export const addMessage = (data: MessageForm) => {
return request({
url: '/app/message',
method: 'post',
data: data
});
};
/**
* 修改告警消息列
* @param data
*/
export const updateMessage = (data: MessageForm) => {
return request({
url: '/app/message',
method: 'put',
data: data
});
};
/**
* 删除告警消息列
* @param id
*/
export const delMessage = (id: string | number | Array<string | number>) => {
return request({
url: '/app/message/' + id,
method: 'delete'
});
};

View File

@@ -0,0 +1,89 @@
export interface MessageVO {
/**
*
*/
id: string | number;
/**
* 类型
*/
category: number;
/**
* 告警类型
*/
categoryName: string;
/**
* 后端按请求语言返回的告警类型展示文案
*/
categoryLabel?: string;
/**
* 图片地址
*/
image: string;
/**
* 图片地址Url
*/
imageUrl: string;
/**
* 视频地址
*/
video: string | number;
}
export interface MessageForm extends BaseEntity {
/**
*
*/
id?: string | number;
/**
* 类型
*/
category?: number;
/**
* 告警类型
*/
categoryName?: string;
/**
* 图片地址
*/
image?: string;
/**
* 视频地址
*/
video?: string | number;
}
export interface MessageQuery extends PageQuery {
/**
* 类型
*/
category?: number;
/**
* 告警类型
*/
categoryName?: string;
/**
* 图片地址
*/
image?: string;
/**
* 视频地址
*/
video?: string | number;
/**
* 日期范围参数
*/
params?: any;
}

101
src/api/app/user/index.ts Normal file
View File

@@ -0,0 +1,101 @@
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
import { UserVO, UserForm, UserQuery } from '@/api/app/user/types';
/**
* 查询app用户信息列表
* @param query
* @returns {*}
*/
export const listUser = (query?: UserQuery): AxiosPromise<UserVO[]> => {
return request({
url: '/app/user/list',
method: 'get',
params: query
});
};
/**
* 查询app用户信息详细
* @param userId
*/
export const getUser = (userId: string | number): AxiosPromise<UserVO> => {
return request({
url: '/app/user/' + userId,
method: 'get'
});
};
/**
* 新增app用户信息
* @param data
*/
export const addUser = (data: UserForm) => {
return request({
url: '/app/user',
method: 'post',
data: data
});
};
/**
* 修改app用户信息
* @param data
*/
export const updateUser = (data: UserForm) => {
return request({
url: '/app/user',
method: 'put',
data: data
});
};
/**
* 删除app用户信息
* @param userId
*/
export const delUser = (userId: string | number | Array<string | number>) => {
return request({
url: '/app/user/' + userId,
method: 'delete'
});
};
/**
* 用户状态修改
* @param userId 用户ID
* @param status 用户状态
*/
export const changeUserStatus = (userId: string | number, status: string) => {
const data = {
userId,
status
};
return request({
url: '/app/user/changeStatus',
method: 'put',
data: data
});
};
/**
* 重置用户密码
* @param userId 用户ID
* @param password 新密码
*/
export const resetUserPwd = (userId: string | number, password: string) => {
const data = {
userId,
password
};
return request({
url: '/app/user/resetPwd',
method: 'put',
headers: {
isEncrypt: true,
repeatSubmit: false
},
data: data
});
};

273
src/api/app/user/types.ts Normal file
View File

@@ -0,0 +1,273 @@
export interface UserVO {
/**
* 用户ID
*/
userId: string | number;
/**
* 用户账号
*/
userName: string;
/**
* 用户昵称
*/
nickName: string;
/**
* 身份证号
*/
idCard: string | number;
/**
* 用户类型sys_user系统用户
*/
userType: string;
/**
* 用户邮箱
*/
email: string;
/**
* 手机号码
*/
phonenumber: string;
/**
* 用户性别0男 1女 2未知
*/
sex: string;
/**
* 头像地址
*/
avatar: number;
/**
* 密码
*/
password: string;
/**
* 账号状态0正常 1停用
*/
status: string;
/**
* 人脸
*/
faceAddr: string;
/**
* 手机mac地址
*/
mac: string;
/**
* 邀请码
*/
inviteUsed: string;
/**
* 邀请码创建时间
*/
inviteTime: string;
/**
* 最后登录IP
*/
loginIp: string;
/**
* 最后登录时间
*/
loginDate: string;
/**
* 备注
*/
remark: string;
}
export interface UserForm extends BaseEntity {
/**
* 用户ID
*/
userId?: string | number;
/**
* 用户账号
*/
userName?: string;
/**
* 用户昵称
*/
nickName?: string;
/**
* 身份证号
*/
idCard?: string | number;
/**
* 用户类型sys_user系统用户
*/
userType?: string;
/**
* 用户邮箱
*/
email?: string;
/**
* 手机号码
*/
phonenumber?: string;
/**
* 用户性别0男 1女 2未知
*/
sex?: string;
/**
* 头像地址
*/
avatar?: number;
/**
* 密码
*/
password?: string;
/**
* 账号状态0正常 1停用
*/
status?: string;
/**
* 人脸
*/
faceAddr?: string;
/**
* 手机mac地址
*/
mac?: string;
/**
* 邀请码
*/
inviteUsed?: string;
/**
* 邀请码创建时间
*/
inviteTime?: string;
/**
* 最后登录IP
*/
loginIp?: string;
/**
* 最后登录时间
*/
loginDate?: string;
/**
* 备注
*/
remark?: string;
}
export interface UserQuery extends PageQuery {
/**
* 用户账号
*/
userName?: string;
/**
* 用户昵称
*/
nickName?: string;
/**
* 身份证号
*/
idCard?: string | number;
/**
* 用户类型sys_user系统用户
*/
userType?: string;
/**
* 用户邮箱
*/
email?: string;
/**
* 手机号码
*/
phonenumber?: string;
/**
* 用户性别0男 1女 2未知
*/
sex?: string;
/**
* 头像地址
*/
avatar?: number;
/**
* 密码
*/
password?: string;
/**
* 账号状态0正常 1停用
*/
status?: string;
/**
* 人脸
*/
faceAddr?: string;
/**
* 手机mac地址
*/
mac?: string;
/**
* 邀请码
*/
inviteUsed?: string;
/**
* 邀请码创建时间
*/
inviteTime?: string;
/**
* 最后登录IP
*/
loginIp?: string;
/**
* 最后登录时间
*/
loginDate?: string;
/**
* 日期范围参数
*/
params?: any;
}

View File

@@ -0,0 +1,63 @@
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
import { UserdeviceTokenVO, UserdeviceTokenForm, UserdeviceTokenQuery } from '@/api/app/userdeviceToken/types';
/**
* 查询推送用户手机token列表
* @param query
* @returns {*}
*/
export const listUserdeviceToken = (query?: UserdeviceTokenQuery): AxiosPromise<UserdeviceTokenVO[]> => {
return request({
url: '/app/userdeviceToken/list',
method: 'get',
params: query
});
};
/**
* 查询推送用户手机token详细
* @param id
*/
export const getUserdeviceToken = (id: string | number): AxiosPromise<UserdeviceTokenVO> => {
return request({
url: '/app/userdeviceToken/' + id,
method: 'get'
});
};
/**
* 新增推送用户手机token
* @param data
*/
export const addUserdeviceToken = (data: UserdeviceTokenForm) => {
return request({
url: '/app/userdeviceToken',
method: 'post',
data: data
});
};
/**
* 修改推送用户手机token
* @param data
*/
export const updateUserdeviceToken = (data: UserdeviceTokenForm) => {
return request({
url: '/app/userdeviceToken',
method: 'put',
data: data
});
};
/**
* 删除推送用户手机token
* @param id
*/
export const delUserdeviceToken = (id: string | number | Array<string | number>) => {
return request({
url: '/app/userdeviceToken/' + id,
method: 'delete'
});
};

View File

@@ -0,0 +1,83 @@
export interface UserdeviceTokenVO {
/**
*
*/
id: string | number;
/**
* 账号用户id
*/
userId: string | number;
/**
* 设备token
*/
token: string;
/**
* 平台 iOS/Android
*/
platform: string;
/**
*
*/
createdTime: string;
}
export interface UserdeviceTokenForm extends BaseEntity {
/**
*
*/
id?: string | number;
/**
* 账号用户id
*/
userId?: string | number;
/**
* 设备token
*/
token?: string;
/**
* 平台 iOS/Android
*/
platform?: string;
/**
*
*/
createdTime?: string;
}
export interface UserdeviceTokenQuery extends PageQuery {
/**
* 账号用户id
*/
userId?: string | number;
/**
* 设备token
*/
token?: string;
/**
* 平台 iOS/Android
*/
platform?: string;
/**
*
*/
createdTime?: string;
/**
* 日期范围参数
*/
params?: any;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

After

Width:  |  Height:  |  Size: 8.8 KiB

View File

@@ -2,8 +2,10 @@
<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">{{ item.meta?.title }}</span> <span v-if="item.redirect === 'noRedirect' || index == levelList.length - 1" class="no-redirect">
<a v-else @click.prevent="handleLink(item)">{{ item.meta?.title }}</a> {{ translateRouteTitle(item.meta?.title as string) }}
</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>
@@ -12,6 +14,7 @@
<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,23 +24,17 @@
</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="预览" width="800px" append-to-body> <el-dialog v-model="dialogVisible" :title="$t('upload.preview')" 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';
@@ -73,6 +67,7 @@ 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[]>([]);
@@ -140,35 +135,35 @@ const handleBeforeUpload = (file: any) => {
isImg = file.type.indexOf('image') > -1; isImg = file.type.indexOf('image') > -1;
} }
if (!isImg) { if (!isImg) {
proxy?.$modal.msgError(`文件格式不正确, 请上传${props.fileType.join('/')}图片格式文件!`); proxy?.$modal.msgError(t('upload.invalidFormat', { types: props.fileType.join('/') }));
return false; return false;
} }
if (file.name.includes(',')) { if (file.name.includes(',')) {
proxy?.$modal.msgError('文件名不正确,不能包含英文逗号!'); proxy?.$modal.msgError(t('upload.invalidFileName'));
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(`上传头像图片大小不能超过 ${props.fileSize} MB!`); proxy?.$modal.msgError(t('upload.tooLarge', { size: props.fileSize }));
return false; return false;
} }
} }
//压缩图片,开启压缩并且大于指定的压缩大小时才压缩 //压缩图片,开启压缩并且大于指定的压缩大小时才压缩
if (props.compressSupport && file.size / 1024 > props.compressTargetSize) { if (props.compressSupport && file.size / 1024 > props.compressTargetSize) {
proxy?.$modal.loading('正在上传图片,请稍候...'); proxy?.$modal.loading(t('upload.uploading'));
number.value++; number.value++;
return compressAccurately(file, props.compressTargetSize); return compressAccurately(file, props.compressTargetSize);
} else { } else {
proxy?.$modal.loading('正在上传图片,请稍候...'); proxy?.$modal.loading(t('upload.uploading'));
number.value++; number.value++;
} }
}; };
// 文件个数超出 // 文件个数超出
const handleExceed = () => { const handleExceed = () => {
proxy?.$modal.msgError(`上传文件数量不能超过 ${props.limit} 个!`); proxy?.$modal.msgError(t('upload.tooMany', { limit: props.limit }));
}; };
// 上传成功回调 // 上传成功回调
@@ -211,7 +206,7 @@ const uploadedSuccessfully = () => {
// 上传失败 // 上传失败
const handleUploadError = () => { const handleUploadError = () => {
proxy?.$modal.msgError('上传图片失败'); proxy?.$modal.msgError(t('upload.failed'));
proxy?.$modal.closeLoading(); proxy?.$modal.closeLoading();
}; };

View File

@@ -5,8 +5,12 @@
</div> </div>
<template #dropdown> <template #dropdown>
<el-dropdown-menu> <el-dropdown-menu>
<el-dropdown-item :disabled="appStore.language === 'zh_CN'" command="zh_CN"> 中文 </el-dropdown-item> <el-dropdown-item :disabled="appStore.language === LanguageEnum.zh_CN" :command="LanguageEnum.zh_CN">
<el-dropdown-item :disabled="appStore.language === 'en_US'" command="en_US"> English </el-dropdown-item> {{ t('language.chinese') }}
</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>
@@ -16,18 +20,19 @@
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 } = useI18n(); const { locale, t } = useI18n();
const message: any = { const handleLanguageChange = (lang: LanguageEnum) => {
zh_CN: '切换语言成功!',
en_US: 'Switch Language Successful!'
};
const handleLanguageChange = (lang: any) => {
locale.value = lang; locale.value = lang;
appStore.changeLanguage(lang); appStore.changeLanguage(lang);
ElMessage.success(message[lang] || '切换语言成功!'); VXETable.setLanguage(getVxeLanguage(lang));
useDynamicTitle();
ElMessage.success(t('language.switched'));
}; };
</script> </script>

View File

@@ -1,16 +1,22 @@
<template> <template>
<div class="top-right-btn" :style="style"> <div class="top-right-btn" :style="style">
<el-row> <el-row>
<el-tooltip v-if="search" class="item" effect="dark" :content="showSearch ? '隐藏搜索' : '显示搜索'" placement="top"> <el-tooltip
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="刷新" placement="top"> <el-tooltip class="item" effect="dark" :content="$t('toolbar.refresh')" 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="显示/隐藏列" placement="top"> <el-tooltip v-if="columns" class="item" effect="dark" :content="$t('toolbar.columns')" 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">显示/隐藏列</div> <div class="tree-header">{{ $t('toolbar.columns') }}</div>
<el-tree <el-tree
ref="columnRef" ref="columnRef"
:data="columns" :data="columns"

View File

@@ -144,7 +144,7 @@ const computedIds = (data) => {
const getList = () => { const getList = () => {
loading.value = true; loading.value = true;
api.listRole(proxy?.addDateRange(queryParams.value, dateRange.value)).then((res) => { api.listRole(proxy?.addDateRange(queryParams.value, dateRange.value)).then((res) => {
roleList.value = res.rows; roleList.value = res.data;
total.value = res.total; total.value = res.total;
loading.value = false; loading.value = false;
}); });

View File

@@ -16,15 +16,17 @@
</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 sizeOptions = ref([ const { t } = useI18n();
{ label: '较大', value: 'large' }, const sizeOptions = computed(() => [
{ label: '默认', value: 'default' }, { label: t('size.large'), value: 'large' },
{ label: '稍小', value: 'small' } { label: t('size.default'), value: 'default' },
{ 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 : ''" />
{{ item.meta?.title }}</el-menu-item {{ translateRouteTitle(item.meta?.title as string) }}</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>更多菜单</template> <template #title>{{ $t('navbar.moreMenu') }}</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 : ''" /> {{ item.meta?.title }}</el-menu-item ><svg-icon :icon-class="item.meta ? item.meta.icon : ''" /> {{ translateRouteTitle(item.meta?.title as string) }}</el-menu-item
> >
</template> </template>
</el-sub-menu> </el-sub-menu>
@@ -26,6 +26,7 @@ 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,3 +3,15 @@ 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,9 +1,84 @@
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',
@@ -73,6 +148,7 @@ 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',
@@ -80,6 +156,144 @@ 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 { LanguageEnum } from '@/enums/LanguageEnum'; import { getHtmlLanguage, isSupportedLanguage, 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,11 +10,19 @@ import en_US from '@/lang/en_US';
* @returns zh-cn|en ... * @returns zh-cn|en ...
*/ */
export const getLanguage = (): LanguageEnum => { export const getLanguage = (): LanguageEnum => {
const language = useStorage<LanguageEnum>('language', LanguageEnum.zh_CN); const storedLanguage = localStorage.getItem('language');
if (language.value) { if (isSupportedLanguage(storedLanguage)) {
return language.value; return storedLanguage;
} }
return LanguageEnum.zh_CN; if (storedLanguage !== null) {
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({
@@ -22,12 +30,15 @@ const i18n = createI18n({
allowComposition: true, allowComposition: true,
legacy: false, legacy: false,
locale: getLanguage(), locale: getLanguage(),
fallbackLocale: LanguageEnum.zh_CN,
messages: { messages: {
zh_CN: zh_CN, [LanguageEnum.zh_CN]: zh_CN,
en_US: en_US [LanguageEnum.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,9 +1,84 @@
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: '请选择/输入公司名称',
@@ -73,6 +148,7 @@ export default {
navbar: { navbar: {
full: '全屏', full: '全屏',
language: '语言', language: '语言',
search: '搜索',
dashboard: '首页', dashboard: '首页',
document: '项目文档', document: '项目文档',
message: '消息', message: '消息',
@@ -80,6 +156,144 @@ 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="搜索" effect="dark" placement="bottom"> <el-tooltip :content="proxy.$t('navbar.search')" 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,14 +47,6 @@
</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>
@@ -105,8 +97,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' import TopBar from './TopBar/index.vue';
const appStore = useAppStore(); const appStore = useAppStore();
const userStore = useUserStore(); const userStore = useUserStore();
@@ -170,9 +162,9 @@ const toggleSideBar = () => {
}; };
const logout = async () => { const logout = async () => {
await ElMessageBox.confirm('确定注销并退出系统吗?', '提示', { await ElMessageBox.confirm(proxy.$t('navbar.logoutConfirm'), proxy.$t('navbar.prompt'), {
confirmButtonText: '确定', confirmButtonText: proxy.$t('navbar.confirm'),
cancelButtonText: '取消', cancelButtonText: proxy.$t('navbar.cancel'),
type: 'warning' type: 'warning'
} as ElMessageBoxOptions); } as ElMessageBoxOptions);
userStore.logout().then(() => { userStore.logout().then(() => {
@@ -282,7 +274,6 @@ 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">菜单导航设置</h3> <h3 class="drawer-title">{{ $t('settings.navigation') }}</h3>
<div class="nav-wrap"> <div class="nav-wrap">
<el-tooltip content="左侧菜单" placement="bottom"> <el-tooltip :content="$t('settings.leftMenu')" 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="混合菜单" placement="bottom"> <el-tooltip :content="$t('settings.mixedMenu')" 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="顶部菜单" placement="bottom"> <el-tooltip :content="$t('settings.topMenu')" 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">主题风格设置</h3> <h3 class="drawer-title">{{ $t('settings.themeStyle') }}</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" class> <svg viewBox="64 64 896 896" data-icon="check" width="1em" height="1em" :fill="theme" aria-hidden="true" focusable="false">
<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" class> <svg viewBox="64 64 896 896" data-icon="check" width="1em" height="1em" :fill="theme" aria-hidden="true" focusable="false">
<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>主题颜色</span> <span>{{ $t('settings.themeColor') }}</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>深色模式</span> <span>{{ $t('settings.darkMode') }}</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">系统布局配置</h3> <h3 class="drawer-title">{{ $t('settings.systemLayout') }}</h3>
<div class="drawer-item"> <div class="drawer-item">
<span>开启 Tags-Views</span> <span>{{ $t('settings.enableTagsView') }}</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>显示页签图标</span> <span>{{ $t('settings.showTagsIcon') }}</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>固定 Header</span> <span>{{ $t('settings.fixedHeader') }}</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>显示 Logo</span> <span>{{ $t('settings.showLogo') }}</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>动态标题</span> <span>{{ $t('settings.dynamicTitle') }}</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,12 +117,13 @@
<el-divider /> <el-divider />
<el-button type="primary" plain icon="DocumentAdd" @click="saveSetting">保存配置</el-button> <el-button type="primary" plain icon="DocumentAdd" @click="saveSetting">{{ $t('settings.save') }}</el-button>
<el-button plain icon="Refresh" @click="resetSetting">重置配置</el-button> <el-button plain icon="Refresh" @click="resetSetting">{{ $t('settings.reset') }}</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';
@@ -136,6 +137,7 @@ 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);
@@ -161,15 +163,15 @@ const toggleDark = () => useToggle(isDark);
/** 菜单导航设置 */ /** 菜单导航设置 */
watch( watch(
() => navType, navType,
(val: string) => { (val) => {
if (val.value === NavTypeEnum.TOP) { if (val === NavTypeEnum.TOP) {
appStore.toggleSideBarHide(true); appStore.toggleSideBarHide(true);
permissionStore.setSidebarRouters(permissionStore.defaultRoutes as any); permissionStore.setSidebarRouters(permissionStore.defaultRoutes as any);
} else if (val.value === NavTypeEnum.LEFT) { } else if (val === NavTypeEnum.LEFT) {
appStore.toggleSideBarHide(false); appStore.toggleSideBarHide(false);
permissionStore.setSidebarRouters(permissionStore.defaultRoutes as any); permissionStore.setSidebarRouters(permissionStore.defaultRoutes as any);
} else if (val.value === NavTypeEnum.MIX) { } else if (val === NavTypeEnum.MIX) {
appStore.toggleSideBarHide(false); appStore.toggleSideBarHide(false);
} }
}, },
@@ -200,7 +202,7 @@ const handleTheme = (val: string) => {
settingsStore.sideTheme = val; settingsStore.sideTheme = val;
}; };
const saveSetting = () => { const saveSetting = () => {
proxy?.$modal.loading('正在保存到本地,请稍候...'); proxy?.$modal.loading(t('settings.saving'));
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;
@@ -215,7 +217,7 @@ const saveSetting = () => {
}, 1000); }, 1000);
}; };
const resetSetting = () => { const resetSetting = () => {
proxy?.$modal.loading('正在清除设置缓存并刷新,请稍候...'); proxy?.$modal.loading(t('settings.resetting'));
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,7 +5,9 @@
<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(onlyOneChild.meta.title)">{{ onlyOneChild.meta.title }}</span> <span class="menu-title" :title="hasTitle(translateRouteTitle(onlyOneChild.meta.title))">
{{ translateRouteTitle(onlyOneChild.meta.title) }}
</span>
</template> </template>
</el-menu-item> </el-menu-item>
</app-link> </app-link>
@@ -14,7 +16,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(item.meta?.title)">{{ item.meta?.title }}</span> <span class="menu-title" :title="hasTitle(translateRouteTitle(item.meta?.title))">{{ translateRouteTitle(item.meta?.title) }}</span>
</template> </template>
<sidebar-item <sidebar-item
@@ -34,6 +36,7 @@ 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,26 +6,28 @@
: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, fullPath: tag.fullPath ? tag.fullPath : '' }" :to="{ path: tag.path ? tag.path : '', query: tag.query }"
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">{{ tag.title }}</span> <span class="tags-view-item-title">{{ translateRouteTitle(tag.title as string) }}</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" /> 刷新页面</li> <li @click="refreshSelectedTag(selectedTag)"><refresh-right style="width: 1em; height: 1em" /> {{ $t('tagsView.refresh') }}</li>
<li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)"><close style="width: 1em; height: 1em" /> 关闭当前</li> <li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)">
<li @click="closeOthersTags"><circle-close style="width: 1em; height: 1em" /> 关闭其他</li> <close style="width: 1em; height: 1em" /> {{ $t('tagsView.closeCurrent') }}
<li v-if="!isFirstView()" @click="closeLeftTags"><back style="width: 1em; height: 1em" /> 关闭左侧</li> </li>
<li v-if="!isLastView()" @click="closeRightTags"><right style="width: 1em; height: 1em" /> 关闭右侧</li> <li @click="closeOthersTags"><circle-close style="width: 1em; height: 1em" /> {{ $t('tagsView.closeOthers') }}</li>
<li @click="closeAllTags(selectedTag)"><circle-close style="width: 1em; height: 1em" /> 全部关闭</li> <li v-if="!isFirstView()" @click="closeLeftTags"><back style="width: 1em; height: 1em" /> {{ $t('tagsView.closeLeft') }}</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>
@@ -37,6 +39,7 @@ 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);
@@ -52,7 +55,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();
@@ -180,7 +183,8 @@ const closeLeftTags = () => {
}); });
}; };
const closeOthersTags = () => { const closeOthersTags = () => {
router.push(selectedTag.value).catch(() => {}); if (!selectedTag.value) return;
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();
}); });
@@ -261,7 +265,11 @@ 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: box-shadow 0.2s ease, transform 0.2s ease, border-color 0.2s ease, color 0.2s ease; transition:
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,66 +4,69 @@
<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>更多菜单</span> <span>{{ $t('navbar.moreMenu') }}</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> <script setup lang="ts">
import SidebarItem from '../Sidebar/SidebarItem' import SidebarItem from '../Sidebar/SidebarItem.vue';
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(() => { const topMenus = computed<RouteRecordRaw[]>(() => {
return permissionStore.sidebarRouters.filter((f) => !f.hidden).slice(0, visibleNumber.value) return permissionStore.sidebarRouters.filter((f) => !f.hidden).slice(0, visibleNumber.value) as RouteRecordRaw[];
}) });
const moreRoutes = computed(() => { const moreRoutes = computed<RouteRecordRaw[]>(() => {
return permissionStore.sidebarRouters.filter((f) => !f.hidden).slice(visibleNumber.value, sidebarRouters.value.length - visibleNumber.value) return permissionStore.sidebarRouters
}) .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 = parseInt(width / 3 / 85) visibleNumber.value = Math.floor(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-menu-item { #app .topbar-menu.el-menu--horizontal .el-sub-menu__title,
#app .topbar-menu.el-menu--horizontal .el-menu-item {
padding: 0 10px !important; padding: 0 10px !important;
} }
@@ -76,7 +79,10 @@ onMounted(() => {
margin: 0 10px !important; margin: 0 10px !important;
} }
.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-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 {
color: v-bind(theme); color: v-bind(theme);
} }
@@ -97,7 +103,8 @@ 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-menu-item { .topbar-menu.el-menu--horizontal .el-sub-menu__title,
.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"> <el-dialog v-model="state.isShowSearch" destroy-on-close :show-close="false" :close-on-click-modal="true" @closed="closeSearch">
<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="搜索" :placeholder="$t('navbar.search')"
:fit-input-width="true" :fit-input-width="true"
@select="onHandleSelect" @select="onHandleSelect"
> >
@@ -30,6 +30,7 @@ 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;
@@ -64,7 +65,17 @@ 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) => {
@@ -87,7 +98,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, r.meta.title]; data.title = [...data.title, translateRouteTitle(r.meta.title as string)];
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">通知公告</div> <div class="head-box-title">{{ $t('notice.title') }}</div>
<div class="head-box-btn" @click="readAll">全部已读</div> <div class="head-box-btn" @click="readAll">{{ $t('notice.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">已读</span> <span v-if="v.read" class="el-tag el-tag--success el-tag--mini read">{{ $t('notice.read') }}</span>
<span v-else class="el-tag el-tag--danger el-tag--mini read">未读</span> <span v-else class="el-tag el-tag--danger el-tag--mini read">{{ $t('notice.unread') }}</span>
</div> </div>
</template> </template>
<el-empty v-else :description="'消息为空'"></el-empty> <el-empty v-else :description="$t('notice.empty')"></el-empty>
</div> </div>
<div v-if="newsList.length > 0" class="foot-box" @click="onGoToGiteeClick">前往gitee</div> <div v-if="newsList.length > 0" class="foot-box" @click="onGoToGiteeClick">{{ $t('notice.viewMore') }}</div>
</div> </div>
</template> </template>

View File

@@ -33,7 +33,13 @@ 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
}); });
@@ -44,6 +50,8 @@ 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,6 +3,8 @@ 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');
@@ -15,10 +17,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('language', 'zh_CN'); const language = useStorage<LanguageEnum>('language', getLanguage());
const languageObj: any = { const languageObj: Record<LanguageEnum, typeof zhCN> = {
en_US: enUS, [LanguageEnum.en_US]: enUS,
zh_CN: zhCN [LanguageEnum.zh_CN]: zhCN
}; };
const locale = computed(() => { const locale = computed(() => {
return languageObj[language.value]; return languageObj[language.value];
@@ -53,8 +55,9 @@ export const useAppStore = defineStore('app', () => {
sidebar.hide = status; sidebar.hide = status;
}; };
const changeLanguage = (val: string): void => { const changeLanguage = (val: LanguageEnum): void => {
language.value = val; language.value = val;
setDocumentLanguage(val);
}; };
return { return {

View File

@@ -1,14 +1,17 @@
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 = settingsStore.title + ' - ' + import.meta.env.VITE_APP_TITLE; document.title = translateRouteTitle(settingsStore.title) + ' - ' + appTitle;
} else { } else {
document.title = defaultSettings.title as string; document.title = appTitle || (defaultSettings.title as string);
} }
}; };

View File

@@ -6,11 +6,20 @@ 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 hasKey = i18n.global.te('route.' + title); const normalizedTitle = title?.trim();
if (hasKey) { if (!normalizedTitle) {
const translatedTitle = i18n.global.t('route.' + title); return '';
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,7 +19,8 @@ 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()
}; };
}; };
@@ -45,7 +46,7 @@ service.interceptors.request.use(
// 是否需要防止数据重复提交 // 是否需要防止数据重复提交
const isRepeatSubmit = config.headers?.repeatSubmit === false; const isRepeatSubmit = config.headers?.repeatSubmit === false;
// 是否需要加密 // 是否需要加密
const isEncrypt = config.headers?.isEncrypt === 'true'; const isEncrypt = config.headers?.isEncrypt === true || config.headers?.isEncrypt === 'true';
if (getToken() && !isToken) { if (getToken() && !isToken) {
config.headers['Authorization'] = 'Bearer ' + getToken(); // 让每个请求携带自定义token 请根据实际情况自行修改 config.headers['Authorization'] = 'Bearer ' + getToken(); // 让每个请求携带自定义token 请根据实际情况自行修改

View File

@@ -0,0 +1,45 @@
# 2026-06-04
## user/index.vue - 人脸列多图片展示
- 将人脸列从单张 `image-preview` 改为多张 `el-image` 缩略图布局
- 新增 `faceUrls()` 工具函数,将逗号分隔的 `faceAddr` 拆分为数组
- 每张缩略图 40x40flex 横排gap 4px
- 通过 `preview-src-list` + `initial-index` 实现点击任意一张可轮播预览全部
- 添加 hover 放大动效和图片加载失败的占位 icon
## user/index.vue - 用户账号列展示优化
- "用户账号"列改为优先展示手机号、其次邮箱、最后 userName 的降级逻辑
- 模板表达式:`phonenumber || email || userName || '-'`
## user/index.vue - 仿 sysuser 添加重置密码
- API: `d:\code\plus-ui\src\api\app\user\index.ts` 新增 `resetUserPwd(userId, password)``PUT /app/user/resetPwd`
- View: 操作列新增"重置密码"按钮Key 图标),权限 `app:user:resetPwd`
- `handleResetPwd`: ElMessageBox.prompt 弹窗输入新密码,校验 5-20 位、禁非法字符 `<>"'|\`
- 成功提示包含新密码明文
## user/index.vue - 账号状态改为 el-switch 开关
- API: `d:\code\plus-ui\src\api\app\user\index.ts` 新增 `changeUserStatus(userId, status)``PUT /app/user/changeStatus`
- 状态列从纯文本改为 `el-switch``active-value="0"`(启用)、`inactive-value="1"`(停用)
- `handleStatusChange`: 切换前弹确认框,操作失败时回滚开关状态
## message/index.vue - 视频地址改为在线播放
- 视频地址列从纯文本改为"播放"按钮(`VideoPlay` 图标 + 点击触发 `handlePlayVideo`
- 新增视频播放弹窗:`<video>` 标签 + `controls` + `autoplay`,宽度 100%,最大高度 60vh
- `videoDialog`: `{ visible: boolean; url: string }`,关闭弹窗时清空 url
- 无视频时显示 `-`
## message/index.vue - 视频地址改为在线播放ZLMediaKit HTTP-FLV
- 安装 `xgplayer` + `xgplayer-flv` 依赖npm install --save
- 视频地址列从纯文本改为"播放"按钮(`VideoPlay` 图标)
- 点击按钮弹出视频播放弹窗720px 宽),内嵌 xgplayer 实例
- `initPlayer(url)`: 创建 xgplayer配置 `flv.mediaDataSource.isLive=true``type=flv`
- `destroyPlayer()`: 弹窗关闭时销毁播放器,防止内存泄漏
- `nextTick` 延迟初始化确保 DOM 渲染完毕
- 播放器容器 CSS: 60vh 高度、黑色背景、圆角、overflow hidden
## family/index.vue - 状态改为 el-switch 开关
- API: `d:\code\plus-ui\src\api\app\family\index.ts` 新增 `changeFamilyStatus(id, status)``PUT /app/family/changeStatus`
- 移除重复的纯文本"账号状态"列,保留 el-switch 列
- `handleStatusChange`: 修复从错误引用(`phonenumber/email/userName``changeUserStatus`)改为 FamilyVO 字段(`parentName/id`)和 `changeFamilyStatus`
- 操作失败时自动回滚开关状态

View File

@@ -0,0 +1,32 @@
# 2026-06-05
## device/index.vue - 设备信息页面4个字段展示改造
### 改动概述
按用户要求修改设备信息页面的4个字段展示方式
| 字段 | 改造前 | 改造后 |
|---|---|---|
| `status` | 纯数字 (0/1/2/3) | el-tag 彩色标签0=离线(info)、1=在线(success)、2=故障(danger)、3=到期(warning) |
| `light` | 纯数字输入框 → el-switch → **el-tag 标签** | el-tag 彩色标签0=不开启(info)、1=开启(success),移除开关交互 |
| `alertAudio` | 纯数字输入框 → el-switch → **el-tag 标签** | el-tag 彩色标签0=不播放(info)、1=播放(success),移除开关交互 |
| `recognizeMode` | 不存在 | 新增字段el-tag 展示0=蓝牙(info)、1=蓝牙+人脸(primary) |
| `workMode` | 纯数字文本 | el-tag 彩色标签0=自动(success)、1=半自动(primary) |
### 涉及文件
1. **`src/api/app/device/types.ts`**
- `DeviceVO` 新增 `recognizeMode?: string` 字段
- `DeviceForm` 新增 `recognizeMode?: string` 字段
- `DeviceQuery` 新增 `recognizeMode?: string` 字段
2. **`src/views/app/device/index.vue`**
- 查询表单:`light`/`alertAudio` 改为 el-select 下拉选择,`recognizeMode` 新增查询条件
- 表格列:`status`/`light`/`alertAudio`/`recognizeMode` 改为标签/开关展示
- 弹窗表单:`light`/`alertAudio` 改为 el-switch`recognizeMode` 新增表单字段
- Script新增 `statusTagType()``statusLabel()``patternPerceptionLabel()``lightTagType()``lightLabel()``alertAudioTagType()``alertAudioLabel()` 函数;移除 `handleLightChange()``handleAlertAudioChange()` 开关 handler
### 注意事项
- `light`/`alertAudio` 最终改为纯展示el-tag 标签,无交互),与 `status` 列展示风格统一
- `workMode` 列:纯数字文本 → el-tag 彩色标签0=自动 success / 1=半自动 primary
- `audioContet` 列:纯文本 → 超长截断max-width 120px ellipsis+ el-tooltip 悬停显示完整内容show-after 200ms
- `patternPerception` 字段名统一修正DeviceVO 中原错误写为 `recognizeMode`,已统一改为 `patternPerception`

View File

@@ -0,0 +1,707 @@
<template>
<div class="p-2">
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
<div v-show="showSearch" class="mb-[10px]">
<el-card shadow="hover">
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
<el-form-item :label="$t('device.deviceNo')" prop="deviceNo">
<el-input
v-model="queryParams.deviceNo"
:placeholder="$t('device.deviceNo')"
clearable
style="width: 200px"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item :label="$t('device.deviceName')" prop="deviceName">
<el-input
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-button type="primary" icon="Search" @click="handleQuery">{{ $t('common.search') }}</el-button>
<el-button icon="Refresh" @click="resetQuery">{{ $t('common.reset') }}</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</transition>
<el-card shadow="never">
<template #header>
<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']">{{ $t('common.add') }}</el-button>
</el-col>
<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>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
</template>
<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 :label="$t('common.index')" type="index" width="60" align="center" />
<el-table-column :label="$t('device.deviceNo')" align="center" prop="deviceNo" min-width="140" show-overflow-tooltip />
<el-table-column :label="$t('device.deviceName')" align="center" prop="deviceName" min-width="120" show-overflow-tooltip />
<el-table-column :label="$t('common.status')" align="center" prop="status" width="80">
<template #default="scope">
<el-tag :type="statusTagType(scope.row.status)" size="small">
{{ statusLabel(scope.row.status, scope.row.statusLabel) }}
</el-tag>
</template>
</el-table-column>
<el-table-column :label="$t('device.light')" align="center" prop="light" width="70">
<template #default="scope">
<el-tag :type="lightTagType(scope.row.light)" size="small">
{{ lightLabel(scope.row.light) }}
</el-tag>
</template>
</el-table-column>
<el-table-column :label="$t('device.alert')" align="center" prop="alertAudio" width="70">
<template #default="scope">
<el-tag :type="alertAudioTagType(scope.row.alertAudio)" size="small">
{{ alertAudioLabel(scope.row.alertAudio) }}
</el-tag>
</template>
</el-table-column>
<el-table-column :label="$t('device.perceptionMode')" align="center" prop="patternPerception" width="120">
<template #default="scope">
<el-tag :type="scope.row.patternPerception === '1' ? 'primary' : 'info'" size="small">
{{ patternPerceptionLabel(scope.row.patternPerception) }}
</el-tag>
</template>
</el-table-column>
<el-table-column :label="$t('device.alertContent')" align="center" prop="audioContet" min-width="140" show-overflow-tooltip>
<template #default="scope">
<span v-if="scope.row.audioContet">{{ scope.row.audioContet }}</span>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column :label="$t('device.modelShort')" align="center" prop="deviceEm" width="110" show-overflow-tooltip />
<el-table-column :label="$t('device.serialNumberShort')" align="center" prop="deviceSn" width="130" show-overflow-tooltip />
<el-table-column :label="$t('device.firmwareVersion')" align="center" prop="fwVer" width="100" />
<el-table-column :label="$t('device.qrCode')" align="center" width="90">
<template #default="scope">
<img
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>
</el-table-column>
<el-table-column :label="$t('common.operation')" align="center" width="100" fixed="right">
<template #default="scope">
<!-- <el-tooltip content="修改" placement="top">-->
<!-- <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['app:device:edit']"></el-button>-->
<!-- </el-tooltip>-->
<el-tooltip :content="$t('common.delete')" placement="top">
<el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['app:device:remove']"></el-button>
</el-tooltip>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
</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-row :gutter="20">
<el-col :span="12">
<el-form-item :label="$t('device.deviceNo')" prop="deviceNo">
<el-input v-model="form.deviceNo" :placeholder="$t('device.deviceNo')" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item :label="$t('device.deviceName')" prop="deviceName">
<el-input v-model="form.deviceName" :placeholder="$t('device.deviceName')" />
</el-form-item>
</el-col>
<el-col :span="12">
<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-col>
<el-col :span="12">
<el-form-item :label="$t('device.wifiName')" prop="wifiName">
<el-input v-model="form.wifiName" :placeholder="$t('device.wifiName')" />
</el-form-item>
</el-col>
<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-form-item>
</el-col>
<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-form-item>
</el-col>
<el-col :span="24">
<el-form-item :label="$t('device.alertContent')" prop="audioContet">
<el-input v-model="form.audioContet" :placeholder="$t('device.alertContent')" type="textarea" :rows="2" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item :label="$t('device.strikeRange')" prop="strikeRange">
<el-input v-model="form.strikeRange" :placeholder="$t('common.pleaseInput')" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item :label="$t('device.strikeInterval')" prop="strikeInterval">
<el-input v-model="form.strikeInterval" :placeholder="$t('common.pleaseInput')" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item :label="$t('device.containerCount')" prop="container">
<el-input v-model="form.container" :placeholder="$t('common.pleaseInput')" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item :label="$t('device.strikePosition')" prop="strikePosition">
<el-input v-model="form.strikePosition" :placeholder="$t('common.pleaseInput')" />
</el-form-item>
</el-col>
<el-col :span="12">
<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-col>
<el-col :span="12">
<el-form-item :label="$t('device.deploymentEnd')" prop="endTime">
<el-date-picker
v-model="form.endTime"
type="datetime"
value-format="YYYY-MM-DD HH:mm:ss"
:placeholder="$t('device.selectTime')"
style="width: 100%"
/>
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<el-button :loading="buttonLoading" type="primary" @click="submitForm">{{ $t('common.confirm') }}</el-button>
<el-button @click="cancel">{{ $t('common.cancel') }}</el-button>
</template>
</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>
<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>
</el-dialog>
</div>
</template>
<script setup name="Device" lang="ts">
import { useI18n } from 'vue-i18n';
import { listDevice, getDevice, delDevice, addDevice, updateDevice } from '@/api/app/device';
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 { t } = useI18n();
type TagType = 'primary' | 'success' | 'warning' | 'danger' | 'info';
/** 状态标签类型映射 */
const statusTagType = (status: string): TagType => {
const map: Record<string, TagType> = { '0': 'info', '1': 'success', '2': 'danger', '3': 'warning' };
return map[status] || 'info';
};
/** 优先显示后端按请求语言返回的状态标签 */
const statusLabel = (status: string, localizedLabel?: string): string => {
if (localizedLabel) {
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 || '-';
};
/** 识别模式中文标签 */
const patternPerceptionLabel = (mode: string): string => {
const map: Record<string, string> = { '0': t('device.perception.bluetooth'), '1': t('device.perception.bluetoothFace') };
return map[mode] || mode || '-';
};
/** 强光开启标签类型映射 */
const lightTagType = (light: string): TagType => {
const map: Record<string, TagType> = { '0': 'info', '1': 'success' };
return map[light] || 'info';
};
/** 强光开启中文标签 */
const lightLabel = (light: string): string => {
const map: Record<string, string> = { '0': t('device.lightState.off'), '1': t('device.lightState.on') };
return map[light] || light || '-';
};
/** 告警播放标签类型映射 */
const alertAudioTagType = (alertAudio: string): TagType => {
const map: Record<string, TagType> = { '0': 'info', '1': 'success' };
return map[alertAudio] || 'info';
};
/** 告警播放中文标签 */
const alertAudioLabel = (alertAudio: string): string => {
const map: Record<string, string> = { '0': t('device.audioState.off'), '1': t('device.audioState.on') };
return map[alertAudio] || alertAudio || '-';
};
/** 工作模式标签类型映射 */
const workModeTagType = (mode: string): TagType => {
const map: Record<string, TagType> = { '0': 'success', '1': 'primary' };
return map[mode] || 'info';
};
/** 工作模式中文标签 */
const workModeLabel = (mode: string): string => {
const map: Record<string, string> = { '0': t('device.work.automatic'), '1': t('device.work.semiAutomatic') };
return map[mode] || mode || '-';
};
const deviceList = ref<DeviceVO[]>([]);
const buttonLoading = ref(false);
const loading = ref(true);
const showSearch = ref(true);
const ids = ref<Array<string | number>>([]);
const single = ref(true);
const multiple = ref(true);
const total = ref(0);
const appUserList = ref<UserVO[]>([]);
const appUserLoading = ref(false);
const queryFormRef = ref<ElFormInstance>();
const deviceFormRef = ref<ElFormInstance>();
const dialog = reactive<DialogOption>({
visible: false,
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 = {
id: undefined,
userId: undefined,
deviceNo: undefined,
deviceName: undefined,
status: undefined,
wifiName: undefined,
wifiPassword: undefined,
light: undefined,
alertAudio: undefined,
audioContet: undefined,
strikeRange: undefined,
strikeInterval: undefined,
container: undefined,
strikePosition: undefined,
startTime: undefined,
endTime: undefined,
workMode: undefined,
deviceEm: undefined,
deviceSn: undefined,
fwVer: undefined,
patternPerception: undefined
};
const data = reactive<PageData<DeviceForm, DeviceQuery>>({
form: { ...initFormData },
queryParams: {
pageNum: 1,
pageSize: 10,
deviceNo: undefined,
deviceName: undefined,
status: undefined,
wifiName: undefined,
wifiPassword: undefined,
light: undefined,
alertAudio: undefined,
audioContet: undefined,
strikeRange: undefined,
strikeInterval: undefined,
container: undefined,
strikePosition: undefined,
startTime: undefined,
endTime: undefined,
workMode: undefined,
deviceEm: undefined,
deviceSn: undefined,
fwVer: undefined,
patternPerception: undefined,
params: {}
},
rules: {}
});
const { queryParams, form } = toRefs(data);
const rules = computed(() => ({
id: [{ required: true, message: t('common.required'), trigger: 'blur' }],
userId: [{ required: true, message: t('device.selectOwner'), trigger: 'change' }]
}));
/** 查询设备信息
列表 */
const getList = async () => {
loading.value = true;
const res = await listDevice(queryParams.value);
deviceList.value = res.rows;
total.value = res.total;
selectedDevices.value = [];
loading.value = false;
generateQrCodes(res.rows);
};
/** 取消按钮 */
const cancel = () => {
reset();
dialog.visible = false;
};
/** 表单重置 */
const reset = () => {
form.value = { ...initFormData };
deviceFormRef.value?.resetFields();
};
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.value.pageNum = 1;
getList();
};
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value?.resetFields();
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[]) => {
selectedDevices.value = selection;
ids.value = selection.map((item) => item.id);
single.value = selection.length != 1;
multiple.value = !selection.length;
};
/** 新增按钮操作 */
const handleAdd = () => {
reset();
loadAppUsers();
dialog.visible = true;
dialog.title = t('device.addTitle');
};
/** 修改按钮操作 */
const handleUpdate = async (row?: DeviceVO) => {
reset();
await loadAppUsers();
const _id = row?.id || ids.value[0];
const res = await getDevice(_id);
Object.assign(form.value, res.data);
await ensureAppUserOption(form.value.userId);
dialog.visible = true;
dialog.title = t('device.editTitle');
};
/** 提交按钮 */
const submitForm = () => {
deviceFormRef.value?.validate(async (valid: boolean) => {
if (valid) {
buttonLoading.value = true;
if (form.value.id) {
await updateDevice(form.value).finally(() => (buttonLoading.value = false));
} else {
await addDevice(form.value).finally(() => (buttonLoading.value = false));
}
proxy?.$modal.msgSuccess(t('common.operationSuccess'));
dialog.visible = false;
await getList();
}
});
};
/** 删除按钮操作 */
const handleDelete = async (row?: DeviceVO) => {
const _ids = row?.id || ids.value;
await proxy?.$modal.confirm(t('device.deleteConfirm', { ids: String(_ids) })).finally(() => (loading.value = false));
await delDevice(_ids);
proxy?.$modal.msgSuccess(t('common.deleteSuccess'));
await getList();
};
/** 导出按钮操作 */
const handleExport = () => {
proxy?.download(
'app/device/export',
{
...queryParams.value
},
`device_${new Date().getTime()}.xlsx`
);
};
onMounted(() => {
getList();
});
</script>
<style scoped></style>

View File

@@ -0,0 +1,237 @@
<template>
<div class="p-2">
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
<div v-show="showSearch" class="mb-[10px]">
<el-card shadow="hover">
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
<el-form-item label="设备编号" prop="deviceNo">
<el-input v-model="queryParams.deviceNo" placeholder="请输入设备编号" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="设备名称" prop="deviceName">
<el-input v-model="queryParams.deviceName" placeholder="请输入设备名称" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="用户昵称" prop="nickName">
<el-input v-model="queryParams.nickName" placeholder="请输入用户昵称" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</transition>
<el-card shadow="never">
<!-- <template #header>-->
<!-- <el-row :gutter="10" class="mb8">-->
<!-- <el-col :span="1.5">-->
<!-- <el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['app:deviceUser:add']">新增</el-button>-->
<!-- </el-col>-->
<!-- <el-col :span="1.5">-->
<!-- <el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['app:deviceUser:edit']">修改</el-button>-->
<!-- </el-col>-->
<!-- <el-col :span="1.5">-->
<!-- <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['app:deviceUser:remove']">删除</el-button>-->
<!-- </el-col>-->
<!-- <el-col :span="1.5">-->
<!-- <el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['app:deviceUser:export']">导出</el-button>-->
<!-- </el-col>-->
<!-- <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>-->
<!-- </el-row>-->
<!-- </template>-->
<el-table v-loading="loading" border :data="deviceUserList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="设备ID" align="center" prop="deviceId" v-if="true" />
<el-table-column label="设备编号" align="center" prop="deviceNo" />
<el-table-column label="设备名称" align="center" prop="deviceName" />
<el-table-column label="用户ID" align="center" prop="userId" v-if="true" />
<el-table-column label="用户昵称" align="center" prop="nickName" />
<el-table-column label="用户身份证号" align="center" prop="idCard" />
<el-table-column label="用户手机号" align="center" prop="phonenumber" />
<el-table-column label="用户MAC地址" align="center" prop="mac" />
<!-- <el-table-column label="操作" align="center" fixed="right" class-name="small-padding fixed-width">-->
<!-- <template #default="scope">-->
<!-- <el-tooltip content="修改" placement="top">-->
<!-- <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['app:deviceUser:edit']"></el-button>-->
<!-- </el-tooltip>-->
<!-- <el-tooltip content="删除" placement="top">-->
<!-- <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['app:deviceUser:remove']"></el-button>-->
<!-- </el-tooltip>-->
<!-- </template>-->
<!-- </el-table-column>-->
</el-table>
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
</el-card>
<!-- 添加或修改设备成员关联对话框 -->
<el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
<el-form ref="deviceUserFormRef" :model="form" :rules="rules" label-width="80px">
<el-form-item label="设备编号" prop="deviceNo">
<el-input v-model="form.deviceNo" placeholder="请输入设备编号" />
</el-form-item>
<el-form-item label="设备名称" prop="deviceName">
<el-input v-model="form.deviceName" placeholder="请输入设备名称" />
</el-form-item>
<el-form-item label="用户昵称" prop="nickName">
<el-input v-model="form.nickName" placeholder="请输入用户昵称" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button :loading="buttonLoading" type="primary" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup name="DeviceUser" lang="ts">
import { listDeviceUser, getDeviceUser, delDeviceUser, addDeviceUser, updateDeviceUser } from '@/api/app/deviceUser';
import { DeviceUserVO, DeviceUserQuery, DeviceUserForm } from '@/api/app/deviceUser/types';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const deviceUserList = ref<DeviceUserVO[]>([]);
const buttonLoading = ref(false);
const loading = ref(true);
const showSearch = ref(true);
const ids = ref<Array<string | number>>([]);
const single = ref(true);
const multiple = ref(true);
const total = ref(0);
const queryFormRef = ref<ElFormInstance>();
const deviceUserFormRef = ref<ElFormInstance>();
const dialog = reactive<DialogOption>({
visible: false,
title: ''
});
const initFormData: DeviceUserForm = {
deviceId: undefined,
deviceNo: undefined,
deviceName: undefined,
userId: undefined,
nickName: undefined
};
const data = reactive<PageData<DeviceUserForm, DeviceUserQuery>>({
form: { ...initFormData },
queryParams: {
pageNum: 1,
pageSize: 10,
deviceNo: undefined,
deviceName: undefined,
nickName: undefined,
params: {}
},
rules: {
deviceId: [{ required: true, message: '不能为空', trigger: 'blur' }],
userId: [{ required: true, message: '用户ID不能为空', trigger: 'blur' }],
nickName: [{ required: true, message: '用户昵称不能为空', trigger: 'blur' }]
}
});
const { queryParams, form, rules } = toRefs(data);
/** 查询设备成员关联列表 */
const getList = async () => {
loading.value = true;
const res = await listDeviceUser(queryParams.value);
deviceUserList.value = res.rows;
total.value = res.total;
loading.value = false;
};
/** 取消按钮 */
const cancel = () => {
reset();
dialog.visible = false;
};
/** 表单重置 */
const reset = () => {
form.value = { ...initFormData };
deviceUserFormRef.value?.resetFields();
};
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.value.pageNum = 1;
getList();
};
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value?.resetFields();
handleQuery();
};
/** 多选框选中数据 */
const handleSelectionChange = (selection: DeviceUserVO[]) => {
ids.value = selection.map((item) => item.deviceId);
single.value = selection.length != 1;
multiple.value = !selection.length;
};
/** 新增按钮操作 */
const handleAdd = () => {
reset();
dialog.visible = true;
dialog.title = '添加设备成员关联';
};
/** 修改按钮操作 */
const handleUpdate = async (row?: DeviceUserVO) => {
reset();
const _deviceId = row?.deviceId || ids.value[0];
const res = await getDeviceUser(_deviceId);
Object.assign(form.value, res.data);
dialog.visible = true;
dialog.title = '修改设备成员关联';
};
/** 提交按钮 */
const submitForm = () => {
deviceUserFormRef.value?.validate(async (valid: boolean) => {
if (valid) {
buttonLoading.value = true;
if (form.value.deviceId) {
await updateDeviceUser(form.value).finally(() => (buttonLoading.value = false));
} else {
await addDeviceUser(form.value).finally(() => (buttonLoading.value = false));
}
proxy?.$modal.msgSuccess('操作成功');
dialog.visible = false;
await getList();
}
});
};
/** 删除按钮操作 */
const handleDelete = async (row?: DeviceUserVO) => {
const _deviceIds = row?.deviceId || ids.value;
await proxy?.$modal.confirm('是否确认删除设备成员关联编号为"' + _deviceIds + '"的数据项?').finally(() => (loading.value = false));
await delDeviceUser(_deviceIds);
proxy?.$modal.msgSuccess('删除成功');
await getList();
};
/** 导出按钮操作 */
const handleExport = () => {
proxy?.download(
'app/deviceUser/export',
{
...queryParams.value
},
`deviceUser_${new Date().getTime()}.xlsx`
);
};
onMounted(() => {
getList();
});
</script>

View File

@@ -0,0 +1,255 @@
<template>
<div class="p-2">
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
<div v-show="showSearch" class="mb-[10px]">
<el-card shadow="hover">
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
<el-form-item label="家长id" prop="parentId">
<el-input v-model="queryParams.parentId" placeholder="请输入家长id" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="家长昵称" prop="parentName">
<el-input v-model="queryParams.parentName" placeholder="请输入家长昵称" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="成员id" prop="userId">
<el-input v-model="queryParams.userId" placeholder="请输入成员id" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</transition>
<el-card shadow="never">
<!-- <template #header>-->
<!-- <el-row :gutter="10" class="mb8">-->
<!-- <el-col :span="1.5">-->
<!-- <el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['app:family:add']">新增</el-button>-->
<!-- </el-col>-->
<!-- <el-col :span="1.5">-->
<!-- <el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['app:family:edit']">修改</el-button>-->
<!-- </el-col>-->
<!-- <el-col :span="1.5">-->
<!-- <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['app:family:remove']">删除</el-button>-->
<!-- </el-col>-->
<!-- <el-col :span="1.5">-->
<!-- <el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['app:family:export']">导出</el-button>-->
<!-- </el-col>-->
<!-- <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>-->
<!-- </el-row>-->
<!-- </template>-->
<el-table v-loading="loading" border :data="familyList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="ID" align="center" prop="id" v-if="true" />
<el-table-column label="家长id" align="center" prop="parentId" />
<el-table-column label="家长昵称" align="center" prop="parentName" />
<el-table-column label="成员id" align="center" prop="userId" />
<el-table-column label="用户昵称" align="center" prop="nickName" />
<el-table-column label="用户身份证号" align="center" prop="idCard" />
<el-table-column label="用户手机号" align="center" prop="phonenumber" />
<el-table-column label="用户MAC地址" align="center" prop="mac" />
<el-table-column label="账号状态" align="center">
<template #default="scope">
<el-switch v-model="scope.row.status" active-value="0" inactive-value="1" @change="handleStatusChange(scope.row)"></el-switch>
</template>
</el-table-column>
<el-table-column label="操作" align="center" fixed="right" class-name="small-padding fixed-width">
<!-- <template #default="scope">-->
<!-- <el-tooltip content="修改" placement="top">-->
<!-- <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['app:family:edit']"></el-button>-->
<!-- </el-tooltip>-->
<!-- <el-tooltip content="删除" placement="top">-->
<!-- <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['app:family:remove']"></el-button>-->
<!-- </el-tooltip>-->
<!-- </template>-->
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
</el-card>
<!-- 添加或修改家庭成员对话框 -->
<el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
<el-form ref="familyFormRef" :model="form" :rules="rules" label-width="80px">
<el-form-item label="家长id" prop="parentId">
<el-input v-model="form.parentId" placeholder="请输入家长id" />
</el-form-item>
<el-form-item label="家长昵称" prop="parentName">
<el-input v-model="form.parentName" placeholder="请输入家长昵称" />
</el-form-item>
<el-form-item label="成员id" prop="userId">
<el-input v-model="form.userId" placeholder="请输入成员id" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button :loading="buttonLoading" type="primary" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup name="Family" lang="ts">
import { listFamily, getFamily, delFamily, addFamily, updateFamily, changeFamilyStatus } from '@/api/app/family';
import { FamilyVO, FamilyQuery, FamilyForm } from '@/api/app/family/types';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const familyList = ref<FamilyVO[]>([]);
const buttonLoading = ref(false);
const loading = ref(true);
const showSearch = ref(true);
const ids = ref<Array<string | number>>([]);
const single = ref(true);
const multiple = ref(true);
const total = ref(0);
const queryFormRef = ref<ElFormInstance>();
const familyFormRef = ref<ElFormInstance>();
const dialog = reactive<DialogOption>({
visible: false,
title: ''
});
const initFormData: FamilyForm = {
id: undefined,
parentId: undefined,
parentName: undefined,
userId: undefined,
status: undefined
};
const data = reactive<PageData<FamilyForm, FamilyQuery>>({
form: { ...initFormData },
queryParams: {
pageNum: 1,
pageSize: 10,
parentId: undefined,
parentName: undefined,
userId: undefined,
status: undefined,
params: {}
},
rules: {
id: [{ required: true, message: '不能为空', trigger: 'blur' }],
parentId: [{ required: true, message: '家长id不能为空', trigger: 'blur' }],
userId: [{ required: true, message: '成员id不能为空', trigger: 'blur' }]
}
});
const { queryParams, form, rules } = toRefs(data);
/** 查询家庭成员列表 */
const getList = async () => {
loading.value = true;
const res = await listFamily(queryParams.value);
familyList.value = res.rows;
total.value = res.total;
loading.value = false;
};
/** 取消按钮 */
const cancel = () => {
reset();
dialog.visible = false;
};
/** 表单重置 */
const reset = () => {
form.value = { ...initFormData };
familyFormRef.value?.resetFields();
};
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.value.pageNum = 1;
getList();
};
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value?.resetFields();
handleQuery();
};
/** 多选框选中数据 */
const handleSelectionChange = (selection: FamilyVO[]) => {
ids.value = selection.map((item) => item.id);
single.value = selection.length != 1;
multiple.value = !selection.length;
};
/** 新增按钮操作 */
const handleAdd = () => {
reset();
dialog.visible = true;
dialog.title = '添加家庭成员';
};
/** 修改按钮操作 */
const handleUpdate = async (row?: FamilyVO) => {
reset();
const _id = row?.id || ids.value[0];
const res = await getFamily(_id);
Object.assign(form.value, res.data);
dialog.visible = true;
dialog.title = '修改家庭成员';
};
/** 提交按钮 */
const submitForm = () => {
familyFormRef.value?.validate(async (valid: boolean) => {
if (valid) {
buttonLoading.value = true;
if (form.value.id) {
await updateFamily(form.value).finally(() => (buttonLoading.value = false));
} else {
await addFamily(form.value).finally(() => (buttonLoading.value = false));
}
proxy?.$modal.msgSuccess('操作成功');
dialog.visible = false;
await getList();
}
});
};
/** 删除按钮操作 */
const handleDelete = async (row?: FamilyVO) => {
const _ids = row?.id || ids.value;
await proxy?.$modal.confirm('是否确认删除家庭成员编号为"' + _ids + '"的数据项?').finally(() => (loading.value = false));
await delFamily(_ids);
proxy?.$modal.msgSuccess('删除成功');
await getList();
};
/** 导出按钮操作 */
const handleExport = () => {
proxy?.download(
'app/family/export',
{
...queryParams.value
},
`family_${new Date().getTime()}.xlsx`
);
};
/** 用户状态修改 */
const handleStatusChange = async (row: FamilyVO) => {
const text = row.status === '0' ? '启用' : '停用';
try {
await proxy?.$modal.confirm('确认要"' + text + '""' + (row.parentName || row.id) + '"家庭吗?');
await changeFamilyStatus(row.id, row.status);
proxy?.$modal.msgSuccess(text + '成功');
} catch {
row.status = row.status === '0' ? '1' : '0';
}
};
onMounted(() => {
getList();
});
</script>

View File

@@ -0,0 +1,237 @@
<template>
<div class="p-2">
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
<div v-show="showSearch" class="mb-[10px]">
<el-card shadow="hover">
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
<el-form-item label="用户昵称" prop="nickName">
<el-input v-model="queryParams.nickName" placeholder="请输入用户昵称" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="设备编号" prop="deviceNo">
<el-input v-model="queryParams.deviceNo" placeholder="请输入设备编号" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="设备名称" prop="deviceName">
<el-input v-model="queryParams.deviceName" placeholder="请输入设备名称" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</transition>
<el-card shadow="never">
<template #header>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['app:familyDevice:add']">新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['app:familyDevice:edit']">修改</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['app:familyDevice:remove']">删除</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['app:familyDevice:export']">导出</el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
</template>
<el-table v-loading="loading" border :data="familyDeviceList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="用户ID" align="center" prop="userId" v-if="true" />
<el-table-column label="用户昵称" align="center" prop="nickName" />
<el-table-column label="" align="center" prop="deviceId" v-if="true" />
<el-table-column label="设备编号" align="center" prop="deviceNo" />
<el-table-column label="设备名称" align="center" prop="deviceName" />
<el-table-column label="操作" align="center" fixed="right" class-name="small-padding fixed-width">
<template #default="scope">
<el-tooltip content="修改" placement="top">
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['app:familyDevice:edit']"></el-button>
</el-tooltip>
<el-tooltip content="删除" placement="top">
<el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['app:familyDevice:remove']"></el-button>
</el-tooltip>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
</el-card>
<!-- 添加或修改家庭设备关联对话框 -->
<el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
<el-form ref="familyDeviceFormRef" :model="form" :rules="rules" label-width="80px">
<el-form-item label="用户昵称" prop="nickName">
<el-input v-model="form.nickName" placeholder="请输入用户昵称" />
</el-form-item>
<el-form-item label="设备编号" prop="deviceNo">
<el-input v-model="form.deviceNo" placeholder="请输入设备编号" />
</el-form-item>
<el-form-item label="设备名称" prop="deviceName">
<el-input v-model="form.deviceName" placeholder="请输入设备名称" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button :loading="buttonLoading" type="primary" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup name="FamilyDevice" lang="ts">
import { listFamilyDevice, getFamilyDevice, delFamilyDevice, addFamilyDevice, updateFamilyDevice } from '@/api/app/familyDevice';
import { FamilyDeviceVO, FamilyDeviceQuery, FamilyDeviceForm } from '@/api/app/familyDevice/types';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const familyDeviceList = ref<FamilyDeviceVO[]>([]);
const buttonLoading = ref(false);
const loading = ref(true);
const showSearch = ref(true);
const ids = ref<Array<string | number>>([]);
const single = ref(true);
const multiple = ref(true);
const total = ref(0);
const queryFormRef = ref<ElFormInstance>();
const familyDeviceFormRef = ref<ElFormInstance>();
const dialog = reactive<DialogOption>({
visible: false,
title: ''
});
const initFormData: FamilyDeviceForm = {
userId: undefined,
nickName: undefined,
deviceId: undefined,
deviceNo: undefined,
deviceName: undefined,
}
const data = reactive<PageData<FamilyDeviceForm, FamilyDeviceQuery>>({
form: {...initFormData},
queryParams: {
pageNum: 1,
pageSize: 10,
nickName: undefined,
deviceNo: undefined,
deviceName: undefined,
params: {
}
},
rules: {
userId: [
{ required: true, message: "用户ID不能为空", trigger: "blur" }
],
nickName: [
{ required: true, message: "用户昵称不能为空", trigger: "blur" }
],
deviceId: [
{ required: true, message: "不能为空", trigger: "blur" }
],
}
});
const { queryParams, form, rules } = toRefs(data);
/** 查询家庭设备关联列表 */
const getList = async () => {
loading.value = true;
const res = await listFamilyDevice(queryParams.value);
familyDeviceList.value = res.rows;
total.value = res.total;
loading.value = false;
}
/** 取消按钮 */
const cancel = () => {
reset();
dialog.visible = false;
}
/** 表单重置 */
const reset = () => {
form.value = {...initFormData};
familyDeviceFormRef.value?.resetFields();
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.value.pageNum = 1;
getList();
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value?.resetFields();
handleQuery();
}
/** 多选框选中数据 */
const handleSelectionChange = (selection: FamilyDeviceVO[]) => {
ids.value = selection.map(item => item.userId);
single.value = selection.length != 1;
multiple.value = !selection.length;
}
/** 新增按钮操作 */
const handleAdd = () => {
reset();
dialog.visible = true;
dialog.title = "添加家庭设备关联";
}
/** 修改按钮操作 */
const handleUpdate = async (row?: FamilyDeviceVO) => {
reset();
const _userId = row?.userId || ids.value[0]
const res = await getFamilyDevice(_userId);
Object.assign(form.value, res.data);
dialog.visible = true;
dialog.title = "修改家庭设备关联";
}
/** 提交按钮 */
const submitForm = () => {
familyDeviceFormRef.value?.validate(async (valid: boolean) => {
if (valid) {
buttonLoading.value = true;
if (form.value.userId) {
await updateFamilyDevice(form.value).finally(() => buttonLoading.value = false);
} else {
await addFamilyDevice(form.value).finally(() => buttonLoading.value = false);
}
proxy?.$modal.msgSuccess("操作成功");
dialog.visible = false;
await getList();
}
});
}
/** 删除按钮操作 */
const handleDelete = async (row?: FamilyDeviceVO) => {
const _userIds = row?.userId || ids.value;
await proxy?.$modal.confirm('是否确认删除家庭设备关联编号为"' + _userIds + '"的数据项?').finally(() => loading.value = false);
await delFamilyDevice(_userIds);
proxy?.$modal.msgSuccess("删除成功");
await getList();
}
/** 导出按钮操作 */
const handleExport = () => {
proxy?.download('app/familyDevice/export', {
...queryParams.value
}, `familyDevice_${new Date().getTime()}.xlsx`)
}
onMounted(() => {
getList();
});
</script>

View File

@@ -0,0 +1,399 @@
<template>
<div class="p-2">
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
<div v-show="showSearch" class="mb-[10px]">
<el-card shadow="hover">
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
<el-form-item :label="$t('message.category')" prop="category">
<el-select v-model="queryParams.category" :placeholder="$t('common.pleaseSelect')" clearable style="width: 160px" @change="handleQuery">
<el-option v-for="item in categoryOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item :label="$t('message.description')" prop="categoryName">
<el-input
v-model="queryParams.categoryName"
:placeholder="$t('common.pleaseInput')"
clearable
style="width: 200px"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">{{ $t('common.search') }}</el-button>
<el-button icon="Refresh" @click="resetQuery">{{ $t('common.reset') }}</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</transition>
<el-card shadow="never">
<template #header>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['app:message: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:message:export']">{{
$t('common.export')
}}</el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
</template>
<el-table v-loading="loading" border :data="messageList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="45" align="center" />
<el-table-column type="index" :label="$t('common.index')" width="55" align="center" />
<el-table-column :label="$t('message.deviceId')" align="center" prop="deviceId" width="160" show-overflow-tooltip />
<el-table-column :label="$t('message.deviceName')" align="center" prop="deviceName" width="160" show-overflow-tooltip />
<el-table-column :label="$t('message.category')" align="center" prop="category" width="130">
<template #default="scope">
<el-tag :type="categoryTagType(scope.row.category)" size="small">
{{ categoryLabel(scope.row.category, scope.row.categoryLabel) }}
</el-tag>
</template>
</el-table-column>
<el-table-column :label="$t('message.description')" align="center" prop="categoryName" width="130" show-overflow-tooltip />
<el-table-column :label="$t('message.image')" align="center" width="200">
<template #default="scope">
<template v-if="scope.row.imageUrl">
<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>
</el-table-column>
<el-table-column :label="$t('message.video')" align="center" width="120">
<template #default="scope">
<template v-if="scope.row.video">
<el-button link type="primary" size="small" @click="handlePlayVideo(scope.row.video)">
<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>
</template>
<span v-else class="text-gray-400 text-xs">-</span>
</template>
</el-table-column>
<el-table-column :label="$t('common.operation')" align="center" width="90" fixed="right">
<template #default="scope">
<el-tooltip :content="$t('common.delete')" placement="top">
<el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['app:message:remove']"></el-button>
</el-tooltip>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
</el-card>
<!-- 添加 / 修改告警消息 -->
<el-dialog :title="dialog.title" v-model="dialog.visible" width="620px" append-to-body>
<el-form ref="messageFormRef" :model="form" :rules="rules" label-width="90px" label-position="right">
<el-row :gutter="20">
<el-col :span="12">
<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-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item :label="$t('message.description')" prop="categoryName">
<el-input v-model="form.categoryName" :placeholder="$t('common.pleaseInput')" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item :label="$t('message.image')" prop="image">
<image-upload v-model="form.image" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item :label="$t('message.videoUrl')" prop="video">
<el-input v-model="form.video" :placeholder="$t('common.pleaseInput')" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<el-button :loading="buttonLoading" type="primary" @click="submitForm">{{ $t('common.confirm') }}</el-button>
<el-button @click="cancel">{{ $t('common.cancel') }}</el-button>
</template>
</el-dialog>
<!-- 视频播放 -->
<el-dialog
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>
</el-dialog>
</div>
</template>
<script setup name="Message" lang="ts">
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 { MessageVO, MessageQuery, MessageForm } from '@/api/app/message/types';
import { VideoPlay, Link } from '@element-plus/icons-vue';
import Player from 'xgplayer';
import 'xgplayer-flv';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { t, locale } = useI18n();
/** 视频播放器容器 ref */
const videoPlayerRef = ref<HTMLDivElement | null>(null);
/** xgplayer 实例 */
let player: InstanceType<typeof Player> | null = null;
/** 视频播放弹窗状态 */
const videoDialog = reactive<{ visible: boolean; url: string }>({
visible: false,
url: ''
});
/** 新窗口打开视频 */
const openVideoUrl = (url: string) => {
window.open(url, '_blank', 'noopener,noreferrer');
};
/** 弹窗播放视频 */
const handlePlayVideo = (url: string) => {
videoDialog.url = url;
videoDialog.visible = true;
nextTick(() => {
initPlayer(url);
});
};
/** 初始化 xgplayer 播放 FLV 流 */
const initPlayer = (url: string) => {
destroyPlayer();
if (!videoPlayerRef.value) return;
player = new Player({
el: videoPlayerRef.value,
url,
videoInit: true,
autoplay: true,
playsinline: true,
height: '100%',
width: '100%',
lang: locale.value === LanguageEnum.en_US ? 'en' : 'zh-cn',
flv: {
mediaDataSource: {
isLive: true,
type: 'flv',
url
}
}
});
};
/** 销毁播放器 */
const destroyPlayer = () => {
if (player) {
player.destroy();
player = null;
}
};
/** 告警类型选项 */
const categoryOptions = computed(() => [
{ value: 1, label: t('message.categoryType.intrusion') },
{ value: 2, label: t('message.categoryType.deviceFault') },
{ value: 3, label: t('message.categoryType.expiration') }
]);
/** 优先显示后端按请求语言返回的告警类型标签 */
const categoryLabel = (category: number | string, localizedLabel?: string): string => {
if (localizedLabel) {
return localizedLabel;
}
return categoryOptions.value.find((item) => item.value == category)?.label || '-';
};
/** 根据 category 值返回 el-tag 类型 */
const categoryTagType = (category: number | string): 'danger' | 'warning' | 'info' => {
const map: Record<string, 'danger' | 'warning' | 'info'> = { '1': 'danger', '2': 'warning', '3': 'info' };
return map[String(category)] || 'info';
};
const messageList = ref<MessageVO[]>([]);
const buttonLoading = ref(false);
const loading = ref(true);
const showSearch = ref(true);
const ids = ref<Array<string | number>>([]);
const single = ref(true);
const multiple = ref(true);
const total = ref(0);
const queryFormRef = ref<ElFormInstance>();
const messageFormRef = ref<ElFormInstance>();
const dialog = reactive<DialogOption>({
visible: false,
title: ''
});
const initFormData: MessageForm = {
id: undefined,
category: undefined,
categoryName: undefined,
image: undefined,
video: undefined
};
const data = reactive<PageData<MessageForm, MessageQuery>>({
form: { ...initFormData },
queryParams: {
pageNum: 1,
pageSize: 10,
category: undefined,
categoryName: undefined,
image: undefined,
video: undefined,
params: {}
},
rules: {}
});
const { queryParams, form } = toRefs(data);
const rules = computed(() => ({
id: [{ required: true, message: t('common.required'), trigger: 'blur' }]
}));
/** 查询告警消息列列表 */
const getList = async () => {
loading.value = true;
const res = await listMessage(queryParams.value);
messageList.value = res.rows;
total.value = res.total;
loading.value = false;
};
/** 取消按钮 */
const cancel = () => {
reset();
dialog.visible = false;
};
/** 表单重置 */
const reset = () => {
form.value = { ...initFormData };
messageFormRef.value?.resetFields();
};
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.value.pageNum = 1;
getList();
};
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value?.resetFields();
handleQuery();
};
/** 多选框选中数据 */
const handleSelectionChange = (selection: MessageVO[]) => {
ids.value = selection.map((item) => item.id);
single.value = selection.length != 1;
multiple.value = !selection.length;
};
/** 新增按钮操作 */
const handleAdd = () => {
reset();
dialog.visible = true;
dialog.title = t('message.addTitle');
};
/** 修改按钮操作 */
const handleUpdate = async (row?: MessageVO) => {
reset();
const _id = row?.id || ids.value[0];
const res = await getMessage(_id);
Object.assign(form.value, res.data);
dialog.visible = true;
dialog.title = t('message.editTitle');
};
/** 提交按钮 */
const submitForm = () => {
messageFormRef.value?.validate(async (valid: boolean) => {
if (valid) {
buttonLoading.value = true;
if (form.value.id) {
await updateMessage(form.value).finally(() => (buttonLoading.value = false));
} else {
await addMessage(form.value).finally(() => (buttonLoading.value = false));
}
proxy?.$modal.msgSuccess(t('common.operationSuccess'));
dialog.visible = false;
await getList();
}
});
};
/** 删除按钮操作 */
const handleDelete = async (row?: MessageVO) => {
const _ids = row?.id || ids.value;
await proxy?.$modal.confirm(t('message.deleteConfirm', { ids: String(_ids) })).finally(() => (loading.value = false));
await delMessage(_ids);
proxy?.$modal.msgSuccess(t('common.deleteSuccess'));
await getList();
};
/** 导出按钮操作 */
const handleExport = () => {
proxy?.download(
'app/message/export',
{
...queryParams.value
},
`message_${new Date().getTime()}.xlsx`
);
};
onMounted(() => {
getList();
});
</script>
<style scoped>
.video-player-wrap {
width: 100%;
height: 60vh;
display: flex;
align-items: center;
justify-content: center;
background: #000;
border-radius: 8px;
overflow: hidden;
}
</style>

View File

@@ -0,0 +1,454 @@
<template>
<div class="p-2">
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
<div v-show="showSearch" class="mb-[10px]">
<el-card shadow="hover">
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
<el-form-item label="用户账号" prop="userName">
<el-input v-model="queryParams.userName" placeholder="请输入用户账号" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="用户昵称" prop="nickName">
<el-input v-model="queryParams.nickName" placeholder="请输入用户昵称" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="身份证号" prop="idCard">
<el-input v-model="queryParams.idCard" placeholder="请输入身份证号" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="用户邮箱" prop="email">
<el-input v-model="queryParams.email" placeholder="请输入用户邮箱" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="手机号码" prop="phonenumber">
<el-input v-model="queryParams.phonenumber" placeholder="请输入手机号码" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</transition>
<el-card shadow="never">
<!-- <template #header>-->
<!-- <el-row :gutter="10" class="mb8">-->
<!-- <el-col :span="1.5">-->
<!-- <el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['app:user:add']">新增</el-button>-->
<!-- </el-col>-->
<!-- <el-col :span="1.5">-->
<!-- <el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['app:user:edit']">修改</el-button>-->
<!-- </el-col>-->
<!-- <el-col :span="1.5">-->
<!-- <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['app:user:remove']"-->
<!-- >删除</el-button-->
<!-- >-->
<!-- </el-col>-->
<!-- <el-col :span="1.5">-->
<!-- <el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['app:user:export']">导出</el-button>-->
<!-- </el-col>-->
<!-- <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>-->
<!-- </el-row>-->
<!-- </template>-->
<el-table v-loading="loading" border :data="userList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="用户ID" align="center" prop="userId" v-if="true" />
<el-table-column label="用户账号" align="center" prop="userName">
<template #default="scope">
{{ scope.row.phonenumber || scope.row.email || scope.row.userName || '-' }}
</template>
</el-table-column>
<el-table-column label="用户昵称" align="center" prop="nickName" />
<el-table-column label="身份证号" align="center" prop="idCard" />
<!-- <el-table-column label="用户类型" align="center" prop="userType" />-->
<el-table-column label="用户邮箱" align="center" prop="email" />
<el-table-column label="手机号码" align="center" prop="phonenumber" />
<el-table-column label="用户性别" align="center" prop="sex" />
<!-- <el-table-column label="头像地址" align="center" prop="avatar" />-->
<el-table-column label="头像地址" align="center" prop="avatar" width="100">
<template #default="scope">
<image-preview :src="scope.row.avatar" :width="50" :height="50" />
</template>
</el-table-column>
<!-- <el-table-column label="密码" align="center" prop="password" />-->
<el-table-column label="账号状态" align="center">
<template #default="scope">
<el-switch v-model="scope.row.status" active-value="0" inactive-value="1" @change="handleStatusChange(scope.row)"></el-switch>
</template>
</el-table-column>
<el-table-column label="人脸" align="center" prop="faceAddr" width="180">
<template #default="scope">
<div class="face-image-list" v-if="scope.row.faceAddr">
<el-image
v-for="(url, idx) in faceUrls(scope.row.faceAddr)"
:key="idx"
:src="url"
:preview-src-list="faceUrls(scope.row.faceAddr)"
:initial-index="idx"
fit="cover"
style="width:40px;height:40px"
preview-teleported
>
<template #error>
<div class="face-error-slot">
<el-icon><PictureFilled /></el-icon>
</div>
</template>
</el-image>
</div>
<span v-else class="text-gray-400 text-xs">-</span>
</template>
</el-table-column>
<el-table-column label="手机mac地址" align="center" prop="mac" />
<!-- <el-table-column label="邀请码" align="center" prop="inviteUsed" />
<el-table-column label="邀请码创建时间" align="center" prop="inviteTime" width="180">
<template #default="scope">
<span>{{ parseTime(scope.row.inviteTime, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>-->
<el-table-column label="最后登录IP" align="center" prop="loginIp" />
<el-table-column label="最后登录时间" align="center" prop="loginDate" width="180">
<template #default="scope">
<span>{{ parseTime(scope.row.loginDate, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
<!-- <el-table-column label="备注" align="center" prop="remark" />-->
<el-table-column label="操作" align="center" fixed="right" class-name="small-padding fixed-width">
<template #default="scope">
<!-- <el-tooltip content="修改" placement="top">-->
<!-- <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['app:user:edit']"></el-button>-->
<!-- </el-tooltip>-->
<el-tooltip content="删除" placement="top">
<el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['app:user:remove']"></el-button>
</el-tooltip>
<el-tooltip content="重置密码" placement="top">
<el-button link type="primary" icon="Key" @click="handleResetPwd(scope.row)" v-hasPermi="['app:user:resetPwd']"></el-button>
</el-tooltip>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
</el-card>
<!-- 添加或修改app用户信息对话框 -->
<el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
<el-form ref="userFormRef" :model="form" :rules="rules" label-width="80px">
<el-form-item label="用户账号" prop="userName">
<el-input v-model="form.userName" placeholder="请输入用户账号" />
</el-form-item>
<el-form-item label="用户昵称" prop="nickName">
<el-input v-model="form.nickName" placeholder="请输入用户昵称" />
</el-form-item>
<el-form-item label="身份证号" prop="idCard">
<el-input v-model="form.idCard" placeholder="请输入身份证号" />
</el-form-item>
<el-form-item label="用户邮箱" prop="email">
<el-input v-model="form.email" placeholder="请输入用户邮箱" />
</el-form-item>
<el-form-item label="手机号码" prop="phonenumber">
<el-input v-model="form.phonenumber" placeholder="请输入手机号码" />
</el-form-item>
<el-form-item label="头像地址" prop="avatar">
<el-input v-model="form.avatar" placeholder="请输入头像地址" />
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input v-model="form.password" placeholder="请输入密码" />
</el-form-item>
<el-form-item label="人脸" prop="faceAddr">
<el-input v-model="form.faceAddr" placeholder="请输入人脸" />
</el-form-item>
<el-form-item label="手机mac地址" prop="mac">
<el-input v-model="form.mac" placeholder="请输入手机mac地址" />
</el-form-item>
<el-form-item label="邀请码" prop="inviteUsed">
<el-input v-model="form.inviteUsed" placeholder="请输入邀请码" />
</el-form-item>
<el-form-item label="邀请码创建时间" prop="inviteTime">
<el-date-picker clearable v-model="form.inviteTime" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" placeholder="请选择邀请码创建时间">
</el-date-picker>
</el-form-item>
<el-form-item label="最后登录IP" prop="loginIp">
<el-input v-model="form.loginIp" placeholder="请输入最后登录IP" />
</el-form-item>
<el-form-item label="最后登录时间" prop="loginDate">
<el-date-picker clearable v-model="form.loginDate" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" placeholder="请选择最后登录时间">
</el-date-picker>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
<el-button @click="cancel">取 消</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup name="User" lang="ts">
import { listUser, getUser, delUser, addUser, updateUser, resetUserPwd, changeUserStatus } from '@/api/app/user';
import { UserVO, UserQuery, UserForm } from '@/api/app/user/types';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
/** 解析后端存储的 JSON 数组,并兼容旧的逗号分隔格式 */
const faceUrls = (faceAddr?: string | string[]): string[] => {
const normalizeUrls = (urls: unknown[]): string[] =>
urls.filter((url): url is string => typeof url === 'string').map((url) => url.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 buttonLoading = ref(false);
const loading = ref(true);
const showSearch = ref(true);
const ids = ref<Array<string | number>>([]);
const single = ref(true);
const multiple = ref(true);
const total = ref(0);
const queryFormRef = ref<ElFormInstance>();
const userFormRef = ref<ElFormInstance>();
const dialog = reactive<DialogOption>({
visible: false,
title: ''
});
const initFormData: UserForm = {
userId: undefined,
userName: undefined,
nickName: undefined,
idCard: undefined,
userType: undefined,
email: undefined,
phonenumber: undefined,
sex: undefined,
avatar: undefined,
password: undefined,
status: undefined,
faceAddr: undefined,
mac: undefined,
inviteUsed: undefined,
inviteTime: undefined,
loginIp: undefined,
loginDate: undefined,
remark: undefined
};
const data = reactive<PageData<UserForm, UserQuery>>({
form: { ...initFormData },
queryParams: {
pageNum: 1,
pageSize: 10,
userName: undefined,
nickName: undefined,
idCard: undefined,
userType: undefined,
email: undefined,
phonenumber: undefined,
sex: undefined,
avatar: undefined,
password: undefined,
status: undefined,
faceAddr: undefined,
mac: undefined,
inviteUsed: undefined,
inviteTime: undefined,
loginIp: undefined,
loginDate: undefined,
params: {}
},
rules: {
userId: [{ required: true, message: '用户ID不能为空', trigger: 'blur' }],
userName: [{ required: true, message: '用户账号不能为空', trigger: 'blur' }],
nickName: [{ required: true, message: '用户昵称不能为空', trigger: 'blur' }]
}
});
const { queryParams, form, rules } = toRefs(data);
/** 查询app用户信息列表 */
const getList = async () => {
loading.value = true;
const res = await listUser(queryParams.value);
userList.value = res.rows;
total.value = res.total;
loading.value = false;
};
/** 取消按钮 */
const cancel = () => {
reset();
dialog.visible = false;
};
/** 表单重置 */
const reset = () => {
form.value = { ...initFormData };
userFormRef.value?.resetFields();
};
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.value.pageNum = 1;
getList();
};
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value?.resetFields();
handleQuery();
};
/** 多选框选中数据 */
const handleSelectionChange = (selection: UserVO[]) => {
ids.value = selection.map((item) => item.userId);
single.value = selection.length != 1;
multiple.value = !selection.length;
};
/** 新增按钮操作 */
const handleAdd = () => {
reset();
dialog.visible = true;
dialog.title = '添加app用户信息';
};
/** 修改按钮操作 */
const handleUpdate = async (row?: UserVO) => {
reset();
const _userId = row?.userId || ids.value[0];
const res = await getUser(_userId);
Object.assign(form.value, res.data);
dialog.visible = true;
dialog.title = '修改app用户信息';
};
/** 提交按钮 */
const submitForm = () => {
userFormRef.value?.validate(async (valid: boolean) => {
if (valid) {
buttonLoading.value = true;
if (form.value.userId) {
await updateUser(form.value).finally(() => (buttonLoading.value = false));
} else {
await addUser(form.value).finally(() => (buttonLoading.value = false));
}
proxy?.$modal.msgSuccess('操作成功');
dialog.visible = false;
await getList();
}
});
};
/** 删除按钮操作 */
const handleDelete = async (row?: UserVO) => {
const _userIds = row?.userId || ids.value;
await proxy?.$modal.confirm('是否确认删除app用户信息编号为"' + _userIds + '"的数据项?').finally(() => (loading.value = false));
await delUser(_userIds);
proxy?.$modal.msgSuccess('删除成功');
await getList();
};
/** 导出按钮操作 */
const handleExport = () => {
proxy?.download(
'app/user/export',
{
...queryParams.value
},
`user_${new Date().getTime()}.xlsx`
);
};
/** 用户状态修改 */
const handleStatusChange = async (row: UserVO) => {
const text = row.status === '0' ? '启用' : '停用';
try {
await proxy?.$modal.confirm('确认要"' + text + '""' + (row.phonenumber || row.email || row.userName) + '"用户吗?');
await changeUserStatus(row.userId, row.status);
proxy?.$modal.msgSuccess(text + '成功');
} catch {
row.status = row.status === '0' ? '1' : '0';
}
};
/** 重置密码按钮操作 */
const handleResetPwd = async (row: UserVO) => {
const res = await ElMessageBox.prompt('请输入"' + (row.phonenumber || row.email || row.userName) + '"的新密码', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
closeOnClickModal: false,
inputPattern: /^.{5,20}$/,
inputErrorMessage: '用户密码长度必须介于 5 和 20 之间',
inputValidator: (value) => {
if (/<|>|"|'|\||\\/.test(value)) {
return '不能包含非法字符:< > " \' \\ |';
}
}
});
if (res) {
await resetUserPwd(row.userId, res.value);
proxy?.$modal.msgSuccess('修改成功新密码是' + res.value);
}
};
onMounted(() => {
getList();
});
</script>
<style scoped>
.face-image-list {
display: flex;
flex-wrap: wrap;
gap: 4px;
justify-content: center;
align-items: center;
}
.face-image-list .el-image {
border-radius: 4px;
box-shadow: 0 0 3px 1px #ddd;
cursor: pointer;
transition: transform 0.2s;
}
.face-image-list .el-image:hover {
transform: scale(1.15);
}
.face-error-slot {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
color: #c0c4cc;
font-size: 20px;
background: #f5f7fa;
}
</style>

View File

@@ -0,0 +1,252 @@
<template>
<div class="p-2">
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
<div v-show="showSearch" class="mb-[10px]">
<el-card shadow="hover">
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
<el-form-item label="账号用户id" prop="userId">
<el-input v-model="queryParams.userId" placeholder="请输入账号用户id" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="设备token" prop="token">
<el-input v-model="queryParams.token" placeholder="请输入设备token" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="平台 iOS/Android" prop="platform">
<el-input v-model="queryParams.platform" placeholder="请输入平台 iOS/Android" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="" prop="createdTime">
<el-date-picker clearable
v-model="queryParams.createdTime"
type="date"
value-format="YYYY-MM-DD"
placeholder="请选择"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</transition>
<el-card shadow="never">
<template #header>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['app:userdeviceToken:add']">新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['app:userdeviceToken:edit']">修改</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['app:userdeviceToken:remove']">删除</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['app:userdeviceToken:export']">导出</el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
</template>
<el-table v-loading="loading" border :data="userdeviceTokenList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="" align="center" prop="id" v-if="true" />
<el-table-column label="账号用户id" align="center" prop="userId" />
<el-table-column label="设备token" align="center" prop="token" />
<el-table-column label="平台 iOS/Android" align="center" prop="platform" />
<el-table-column label="" align="center" prop="createdTime" width="180">
<template #default="scope">
<span>{{ parseTime(scope.row.createdTime, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" fixed="right" class-name="small-padding fixed-width">
<template #default="scope">
<el-tooltip content="修改" placement="top">
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['app:userdeviceToken:edit']"></el-button>
</el-tooltip>
<el-tooltip content="删除" placement="top">
<el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['app:userdeviceToken:remove']"></el-button>
</el-tooltip>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
</el-card>
<!-- 添加或修改推送用户手机token对话框 -->
<el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
<el-form ref="userdeviceTokenFormRef" :model="form" :rules="rules" label-width="80px">
<el-form-item label="账号用户id" prop="userId">
<el-input v-model="form.userId" placeholder="请输入账号用户id" />
</el-form-item>
<el-form-item label="设备token" prop="token">
<el-input v-model="form.token" placeholder="请输入设备token" />
</el-form-item>
<el-form-item label="平台 iOS/Android" prop="platform">
<el-input v-model="form.platform" placeholder="请输入平台 iOS/Android" />
</el-form-item>
<el-form-item label="" prop="createdTime">
<el-date-picker clearable
v-model="form.createdTime"
type="datetime"
value-format="YYYY-MM-DD HH:mm:ss"
placeholder="请选择">
</el-date-picker>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button :loading="buttonLoading" type="primary" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup name="UserdeviceToken" lang="ts">
import { listUserdeviceToken, getUserdeviceToken, delUserdeviceToken, addUserdeviceToken, updateUserdeviceToken } from '@/api/app/userdeviceToken';
import { UserdeviceTokenVO, UserdeviceTokenQuery, UserdeviceTokenForm } from '@/api/app/userdeviceToken/types';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const userdeviceTokenList = ref<UserdeviceTokenVO[]>([]);
const buttonLoading = ref(false);
const loading = ref(true);
const showSearch = ref(true);
const ids = ref<Array<string | number>>([]);
const single = ref(true);
const multiple = ref(true);
const total = ref(0);
const queryFormRef = ref<ElFormInstance>();
const userdeviceTokenFormRef = ref<ElFormInstance>();
const dialog = reactive<DialogOption>({
visible: false,
title: ''
});
const initFormData: UserdeviceTokenForm = {
id: undefined,
userId: undefined,
token: undefined,
platform: undefined,
createdTime: undefined,
}
const data = reactive<PageData<UserdeviceTokenForm, UserdeviceTokenQuery>>({
form: {...initFormData},
queryParams: {
pageNum: 1,
pageSize: 10,
userId: undefined,
token: undefined,
platform: undefined,
createdTime: undefined,
params: {
}
},
rules: {
id: [
{ required: true, message: "不能为空", trigger: "blur" }
],
}
});
const { queryParams, form, rules } = toRefs(data);
/** 查询推送用户手机token列表 */
const getList = async () => {
loading.value = true;
const res = await listUserdeviceToken(queryParams.value);
userdeviceTokenList.value = res.rows;
total.value = res.total;
loading.value = false;
}
/** 取消按钮 */
const cancel = () => {
reset();
dialog.visible = false;
}
/** 表单重置 */
const reset = () => {
form.value = {...initFormData};
userdeviceTokenFormRef.value?.resetFields();
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.value.pageNum = 1;
getList();
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value?.resetFields();
handleQuery();
}
/** 多选框选中数据 */
const handleSelectionChange = (selection: UserdeviceTokenVO[]) => {
ids.value = selection.map(item => item.id);
single.value = selection.length != 1;
multiple.value = !selection.length;
}
/** 新增按钮操作 */
const handleAdd = () => {
reset();
dialog.visible = true;
dialog.title = "添加推送用户手机token";
}
/** 修改按钮操作 */
const handleUpdate = async (row?: UserdeviceTokenVO) => {
reset();
const _id = row?.id || ids.value[0]
const res = await getUserdeviceToken(_id);
Object.assign(form.value, res.data);
dialog.visible = true;
dialog.title = "修改推送用户手机token";
}
/** 提交按钮 */
const submitForm = () => {
userdeviceTokenFormRef.value?.validate(async (valid: boolean) => {
if (valid) {
buttonLoading.value = true;
if (form.value.id) {
await updateUserdeviceToken(form.value).finally(() => buttonLoading.value = false);
} else {
await addUserdeviceToken(form.value).finally(() => buttonLoading.value = false);
}
proxy?.$modal.msgSuccess("操作成功");
dialog.visible = false;
await getList();
}
});
}
/** 删除按钮操作 */
const handleDelete = async (row?: UserdeviceTokenVO) => {
const _ids = row?.id || ids.value;
await proxy?.$modal.confirm('是否确认删除推送用户手机token编号为"' + _ids + '"的数据项?').finally(() => loading.value = false);
await delUserdeviceToken(_ids);
proxy?.$modal.msgSuccess("删除成功");
await getList();
}
/** 导出按钮操作 */
const handleExport = () => {
proxy?.download('app/userdeviceToken/export', {
...queryParams.value
}, `userdeviceToken_${new Date().getTime()}.xlsx`)
}
onMounted(() => {
getList();
});
</script>

View File

@@ -1,165 +1,37 @@
<template> <template>
<div class="app-container home"> <div class="app-container home">
<el-row :gutter="20"> <div class="welcome-wrapper">
<el-col :sm="24" :lg="12" style="padding-left: 20px"> <h1 class="welcome-title">{{ $t('home.welcome') }}</h1>
<h2>RuoYi-Vue-Plus多租户管理系统</h2> <p class="welcome-desc">{{ $t('home.slogan') }}</p>
<p> </div>
RuoYi-Vue-Plus 是基于 RuoYi-Vue 针对 分布式集群 场景升级(不兼容原框架)
<br />
* 前端开发框架 Vue3TSElement Plus<br />
* 后端开发框架 Spring Boot<br />
* 容器框架 Undertow 基于 Netty 的高性能容器<br />
* 权限认证框架 Sa-Token 支持多终端认证系统<br />
* 关系数据库 MySQL 适配 8.X 最低 5.7<br />
* 缓存数据库 Redis 适配 6.X 最低 4.X<br />
* 数据库框架 Mybatis-Plus 快速 CRUD 增加开发效率<br />
* 数据库框架 p6spy 更强劲的 SQL 分析<br />
* 多数据源框架 dynamic-datasource 支持主从与多种类数据库异构<br />
* 序列化框架 Jackson 统一使用 jackson 高效可靠<br />
* Redis客户端 Redisson 性能强劲API丰富<br />
* 分布式限流 Redisson 全局请求IP集群ID 多种限流<br />
* 分布式锁 Lock4j 注解锁工具锁 多种多样<br />
* 分布式幂等 Lock4j 基于分布式锁实现<br />
* 分布式链路追踪 SkyWalking 支持链路追踪网格分析度量聚合可视化<br />
* 分布式任务调度 SnailJob 高性能 高可靠 易扩展<br />
* 文件存储 Minio 本地存储<br />
* 文件存储 七牛阿里腾讯 云存储<br />
* 监控框架 SpringBoot-Admin 全方位服务监控<br />
* 校验框架 Validation 增强接口安全性 严谨性<br />
* Excel框架 FastExcel(原Alibaba EasyExcel) 性能优异 扩展性强<br />
* 文档框架 SpringDocjavadoc 无注解零入侵基于java注释<br />
* 工具类框架 HutoolLombok 减少代码冗余 增加安全性<br />
* 代码生成器 适配MPSpringDoc规范化代码 一键生成前后端代码<br />
* 部署方式 Docker 容器编排 一键部署业务集群<br />
* 国际化 SpringMessage Spring标准国际化方案<br />
</p>
<p><b>当前版本:</b> <span>v5.5.3</span></p>
<p>
<el-tag type="danger">&yen;免费开源</el-tag>
</p>
<p>
<el-button type="primary" icon="Cloudy" plain @click="goTarget('https://gitee.com/dromara/RuoYi-Vue-Plus')">访问码云</el-button>
<el-button type="primary" icon="Cloudy" plain @click="goTarget('https://github.com/dromara/RuoYi-Vue-Plus')">访问GitHub</el-button>
<el-button type="primary" icon="Cloudy" plain @click="goTarget('https://plus-doc.dromara.org/#/ruoyi-vue-plus/changlog')"
>更新日志</el-button
>
</p>
</el-col>
<el-col :sm="24" :lg="12" style="padding-left: 20px">
<h2>RuoYi-Cloud-Plus多租户微服务管理系统</h2>
<p>
RuoYi-Cloud-Plus 微服务通用权限管理系统 重写 RuoYi-Cloud 全方位升级(不兼容原框架)
<br />
* 前端开发框架 Vue3TSElement UI<br />
* 后端开发框架 Spring Boot<br />
* 微服务开发框架 Spring CloudSpring Cloud Alibaba<br />
* 容器框架 Undertow 基于 XNIO 的高性能容器<br />
* 权限认证框架 Sa-TokenJwt 支持多终端认证系统<br />
* 关系数据库 MySQL 适配 8.X 最低 5.7<br />
* 关系数据库 Oracle 适配 11g 12c<br />
* 关系数据库 PostgreSQL 适配 13 14<br />
* 关系数据库 SQLServer 适配 2017 2019<br />
* 缓存数据库 Redis 适配 6.X 最低 5.X<br />
* 分布式注册中心 Alibaba Nacos 采用2.X 基于GRPC通信高性能<br />
* 分布式配置中心 Alibaba Nacos 采用2.X 基于GRPC通信高性能<br />
* 服务网关 Spring Cloud Gateway 响应式高性能网关<br />
* 负载均衡 Spring Cloud Loadbalancer 负载均衡处理<br />
* RPC远程调用 Apache Dubbo 原生态使用体验高性能<br />
* 分布式限流熔断 Alibaba Sentinel 无侵入高扩展<br />
* 分布式事务 Alibaba Seata 无侵入高扩展 支持 四种模式<br />
* 分布式消息队列 Apache Kafka 高性能高速度<br />
* 分布式消息队列 Apache RocketMQ 高可用功能多样<br />
* 分布式消息队列 RabbitMQ 支持各种扩展插件功能多样性<br />
* 分布式搜索引擎 ElasticSearch 业界知名<br />
* 分布式链路追踪 Apache SkyWalking 链路追踪网格分析度量聚合可视化<br />
* 分布式日志中心 ELK 业界成熟解决方案<br />
* 分布式监控 PrometheusGrafana 全方位性能监控<br />
* 其余与 Vue 版本一致<br />
</p>
<p><b>当前版本:</b> <span>v2.5.3</span></p>
<p>
<el-tag type="danger">&yen;免费开源</el-tag>
</p>
<p>
<el-button type="primary" icon="Cloudy" plain @click="goTarget('https://gitee.com/dromara/RuoYi-Cloud-Plus')">访问码云</el-button>
<el-button type="primary" icon="Cloudy" plain @click="goTarget('https://github.com/dromara/RuoYi-Cloud-Plus')">访问GitHub</el-button>
<el-button type="primary" icon="Cloudy" plain @click="goTarget('https://plus-doc.dromara.org/#/ruoyi-cloud-plus/changlog')"
>更新日志</el-button
>
</p>
</el-col>
</el-row>
<el-divider />
</div> </div>
</template> </template>
<script setup name="Index" lang="ts"> <script setup name="Index" lang="ts"></script>
const goTarget = (url: string) => {
window.open(url, '__blank');
};
</script>
<style lang="scss" scoped> <style lang="scss" scoped>
.home { .home {
blockquote { display: flex;
padding: 10px 20px; align-items: center;
margin: 0 0 20px; justify-content: center;
font-size: 17.5px; min-height: calc(100vh - 120px);
border-left: 5px solid #eee;
}
hr {
margin-top: 20px;
margin-bottom: 20px;
border: 0;
border-top: 1px solid #eee;
}
.col-item {
margin-bottom: 20px;
} }
ul { .welcome-wrapper {
padding: 0; text-align: center;
margin: 0;
.welcome-title {
font-size: 32px;
font-weight: 600;
color: #303133;
margin-bottom: 16px;
letter-spacing: 2px;
} }
font-family: 'open sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; .welcome-desc {
font-size: 13px; font-size: 16px;
color: #676a6c; color: #909399;
overflow-x: hidden; letter-spacing: 4px;
ul {
list-style-type: none;
}
h4 {
margin-top: 0px;
}
h2 {
margin-top: 10px;
font-size: 26px;
font-weight: 100;
}
p {
margin-top: 10px;
b {
font-weight: 700;
}
}
.update-log {
ol {
display: block;
list-style-type: decimal;
margin-block-start: 1em;
margin-block-end: 1em;
margin-inline-start: 0;
margin-inline-end: 0;
padding-inline-start: 40px;
}
} }
} }
</style> </style>

View File

@@ -1,5 +1,116 @@
<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>
@@ -44,23 +155,23 @@
</div> </div>
</el-form-item> </el-form-item>
<el-checkbox v-model="loginForm.rememberMe" style="margin: 0 0 25px 0">{{ proxy.$t('login.rememberPassword') }}</el-checkbox> <el-checkbox v-model="loginForm.rememberMe" style="margin: 0 0 25px 0">{{ proxy.$t('login.rememberPassword') }}</el-checkbox>
<el-form-item style="float: right"> <!-- <el-form-item style="float: right">-->
<el-button circle :title="proxy.$t('login.social.wechat')" @click="doSocialLogin('wechat')"> <!-- <el-button circle :title="proxy.$t('login.social.wechat')" @click="doSocialLogin('wechat')">-->
<svg-icon icon-class="wechat" /> <!-- <svg-icon icon-class="wechat" />-->
</el-button> <!-- </el-button>-->
<el-button circle :title="proxy.$t('login.social.maxkey')" @click="doSocialLogin('maxkey')"> <!-- <el-button circle :title="proxy.$t('login.social.maxkey')" @click="doSocialLogin('maxkey')">-->
<svg-icon icon-class="maxkey" /> <!-- <svg-icon icon-class="maxkey" />-->
</el-button> <!-- </el-button>-->
<el-button circle :title="proxy.$t('login.social.topiam')" @click="doSocialLogin('topiam')"> <!-- <el-button circle :title="proxy.$t('login.social.topiam')" @click="doSocialLogin('topiam')">-->
<svg-icon icon-class="topiam" /> <!-- <svg-icon icon-class="topiam" />-->
</el-button> <!-- </el-button>-->
<el-button circle :title="proxy.$t('login.social.gitee')" @click="doSocialLogin('gitee')"> <!-- <el-button circle :title="proxy.$t('login.social.gitee')" @click="doSocialLogin('gitee')">-->
<svg-icon icon-class="gitee" /> <!-- <svg-icon icon-class="gitee" />-->
</el-button> <!-- </el-button>-->
<el-button circle :title="proxy.$t('login.social.github')" @click="doSocialLogin('github')"> <!-- <el-button circle :title="proxy.$t('login.social.github')" @click="doSocialLogin('github')">-->
<svg-icon icon-class="github" /> <!-- <svg-icon icon-class="github" />-->
</el-button> <!-- </el-button>-->
</el-form-item> <!-- </el-form-item>-->
<el-form-item style="width: 100%"> <el-form-item style="width: 100%">
<el-button :loading="loading" size="large" type="primary" style="width: 100%" @click.prevent="handleLogin"> <el-button :loading="loading" size="large" type="primary" style="width: 100%" @click.prevent="handleLogin">
<span v-if="!loading">{{ proxy.$t('login.login') }}</span> <span v-if="!loading">{{ proxy.$t('login.login') }}</span>
@@ -71,9 +182,10 @@
</div> </div>
</el-form-item> </el-form-item>
</el-form> </el-form>
<!-- 底部 --> <!-- 底部仅右侧表单区域内显示避免跨背景看不清 -->
<div class="el-login-footer"> <div class="el-login-footer">
<span>Copyright © 2018-2026 疯狂的狮子Li All Rights Reserved.</span> <span>© 2026 驭鲲智能科技湖北有限公司 · 鄂ICP备2026000000号</span>
</div>
</div> </div>
</div> </div>
</template> </template>
@@ -89,26 +201,28 @@ 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',
username: 'admin', username: '',
password: 'admin123', // username: 'admin',
// password: 'admin123',
password: '',
rememberMe: false, rememberMe: false,
code: '', code: '',
uuid: '' uuid: ''
} as LoginData); } as LoginData);
const loginRules: ElFormRules = { const loginRules = computed<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);
@@ -234,13 +348,315 @@ 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; /* ============ 左侧品牌区 ============ */
background-position: center; .brand {
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 {
@@ -340,11 +756,12 @@ onMounted(() => {
.el-login-footer { .el-login-footer {
height: 40px; height: 40px;
line-height: 40px; line-height: 40px;
position: fixed; position: absolute;
bottom: 0; bottom: 0;
left: 0;
width: 100%; width: 100%;
text-align: center; text-align: center;
color: rgba(255, 255, 255, 0.75); color: rgba(100, 116, 139, 0.85);
font-family: Arial, serif; font-family: Arial, serif;
font-size: 12px; font-size: 12px;
letter-spacing: 1px; letter-spacing: 1px;
@@ -356,6 +773,10 @@ 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 疯狂的狮子Li All Rights Reserved.</span> <span>Copyright © 2018-2026. 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: ElFormRules = { const registerRules = computed<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: 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,8 +137,7 @@ 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 }) + '</font>', '系统提示', { await ElMessageBox.alert('<span style="color: red; ">' + t('register.registerSuccess', { username }) + '</span>', t('common.systemPrompt'), {
app: undefined,
dangerouslyUseHTMLString: true, dangerouslyUseHTMLString: true,
type: 'success' type: 'success'
}); });

View File

@@ -42,19 +42,19 @@
<template #header> <template #header>
<el-row :gutter="10" class="mb8"> <el-row :gutter="10" class="mb8">
<el-col :span="1.5"> <el-col :span="1.5">
<el-button type="primary" icon="Plus" @click="handleAdd()">添加</el-button> <el-button v-hasPermi="['workflow:definition:add']" type="primary" icon="Plus" @click="handleAdd()">添加</el-button>
</el-col> </el-col>
<el-col :span="1.5"> <el-col :span="1.5">
<el-button type="success" icon="Edit" :disabled="single" @click="handleUpdate()">修改</el-button> <el-button v-hasPermi="['workflow:definition:edit']" type="success" icon="Edit" :disabled="single" @click="handleUpdate()">修改</el-button>
</el-col> </el-col>
<el-col :span="1.5"> <el-col :span="1.5">
<el-button type="danger" icon="Delete" :disabled="multiple" @click="handleDelete()">删除</el-button> <el-button v-hasPermi="['workflow:definition:remove']" type="danger" icon="Delete" :disabled="multiple" @click="handleDelete()">删除</el-button>
</el-col> </el-col>
<el-col :span="1.5"> <el-col :span="1.5">
<el-button type="primary" icon="UploadFilled" @click="uploadDialog.visible = true">部署流程文件</el-button> <el-button v-hasPermi="['workflow:definition:import']" type="primary" icon="UploadFilled" @click="uploadDialog.visible = true">部署流程文件</el-button>
</el-col> </el-col>
<el-col :span="1.5"> <el-col :span="1.5">
<el-button type="warning" icon="Download" :disabled="single" @click="handleExportDef">导出</el-button> <el-button v-hasPermi="['workflow:definition:export']" type="warning" icon="Download" :disabled="single" @click="handleExportDef">导出</el-button>
</el-col> </el-col>
<right-toolbar v-model:show-search="showSearch" @query-table="handleQuery"></right-toolbar> <right-toolbar v-model:show-search="showSearch" @query-table="handleQuery"></right-toolbar>
</el-row> </el-row>
@@ -74,6 +74,7 @@
<el-table-column align="center" prop="activityStatus" label="激活状态" width="130"> <el-table-column align="center" prop="activityStatus" label="激活状态" width="130">
<template #default="scope"> <template #default="scope">
<el-switch <el-switch
v-hasPermi="['workflow:definition:active']"
v-model="scope.row.activityStatus" v-model="scope.row.activityStatus"
:active-value="1" :active-value="1"
:inactive-value="0" :inactive-value="0"
@@ -92,21 +93,21 @@
<template #default="scope"> <template #default="scope">
<el-row :gutter="10" class="mb8"> <el-row :gutter="10" class="mb8">
<el-col :span="1.5"> <el-col :span="1.5">
<el-button link type="primary" size="small" icon="Delete" @click="handleDelete(scope.row)">删除流程</el-button> <el-button v-hasPermi="['workflow:definition:remove']" link type="primary" size="small" icon="Delete" @click="handleDelete(scope.row)">删除流程</el-button>
</el-col> </el-col>
<el-col :span="1.5"> <el-col :span="1.5">
<el-button link type="primary" size="small" icon="CopyDocument" @click="handleCopyDef(scope.row)">复制流程</el-button> <el-button v-hasPermi="['workflow:definition:copy']" link type="primary" size="small" icon="CopyDocument" @click="handleCopyDef(scope.row)">复制流程</el-button>
</el-col> </el-col>
</el-row> </el-row>
<el-row :gutter="10" class="mb8"> <el-row :gutter="10" class="mb8">
<el-col :span="1.5"> <el-col :span="1.5">
<el-button link type="primary" v-if="scope.row.isPublish === 0" icon="Pointer" size="small" @click="design(scope.row)" <el-button v-hasPermi="['workflow:definition:query']" link type="primary" v-if="scope.row.isPublish === 0" icon="Pointer" size="small" @click="design(scope.row)"
>流程设计</el-button >流程设计</el-button
> >
<el-button link type="primary" v-else icon="View" size="small" @click="designView(scope.row)">查看流程</el-button> <el-button v-hasPermi="['workflow:definition:query']" link type="primary" v-else icon="View" size="small" @click="designView(scope.row)">查看流程</el-button>
</el-col> </el-col>
<el-col v-if="scope.row.isPublish !== 1" :span="1.5"> <el-col v-if="scope.row.isPublish !== 1" :span="1.5">
<el-button link type="primary" size="small" icon="CircleCheck" @click="handlePublish(scope.row)">发布流程</el-button> <el-button v-hasPermi="['workflow:definition:publish']" link type="primary" size="small" icon="CircleCheck" @click="handlePublish(scope.row)">发布流程</el-button>
</el-col> </el-col>
</el-row> </el-row>
</template> </template>

View File

@@ -50,7 +50,7 @@
<template #header> <template #header>
<el-row :gutter="10" class="mb8"> <el-row :gutter="10" class="mb8">
<el-col :span="1.5"> <el-col :span="1.5">
<el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete">删除</el-button> <el-button v-hasPermi="['workflow:instance:remove']" type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete">删除</el-button>
</el-col> </el-col>
<right-toolbar v-model:show-search="showSearch" @query-table="handleQuery"></right-toolbar> <right-toolbar v-model:show-search="showSearch" @query-table="handleQuery"></right-toolbar>
</el-row> </el-row>
@@ -75,7 +75,7 @@
<el-table-column align="center" prop="version" label="版本号" width="90"> <el-table-column align="center" prop="version" label="版本号" width="90">
<template #default="scope"> v{{ scope.row.version }}.0</template> <template #default="scope"> v{{ scope.row.version }}.0</template>
</el-table-column> </el-table-column>
<el-table-column v-if="tab === 'running'" align="center" prop="isSuspended" label="状态" min-width="70"> <el-table-column v-if="tab === 'running'" v-hasPermi="['workflow:instance:active']" align="center" prop="isSuspended" label="状态" min-width="70">
<template #default="scope"> <template #default="scope">
<el-tag v-if="!scope.row.isSuspended" type="success">激活</el-tag> <el-tag v-if="!scope.row.isSuspended" type="success">激活</el-tag>
<el-tag v-else type="danger">挂起</el-tag> <el-tag v-else type="danger">挂起</el-tag>
@@ -104,15 +104,15 @@
</el-popover> </el-popover>
</el-col> </el-col>
<el-col :span="1.5"> <el-col :span="1.5">
<el-button type="danger" size="small" icon="Delete" @click="handleDelete(scope.row)">删除 </el-button> <el-button v-hasPermi="['workflow:instance:remove']" type="danger" size="small" icon="Delete" @click="handleDelete(scope.row)">删除 </el-button>
</el-col> </el-col>
</el-row> </el-row>
<el-row :gutter="10" class="mb8"> <el-row :gutter="10" class="mb8">
<el-col :span="1.5"> <el-col :span="1.5">
<el-button type="primary" size="small" icon="View" @click="handleView(scope.row)">查看</el-button> <el-button v-hasPermi="['workflow:instance:query']" type="primary" size="small" icon="View" @click="handleView(scope.row)">查看</el-button>
</el-col> </el-col>
<el-col :span="1.5"> <el-col :span="1.5">
<el-button type="primary" size="small" icon="Document" @click="handleInstanceVariable(scope.row)"> 变量 </el-button> <el-button v-hasPermi="['workflow:instance:variableQuery']" type="primary" size="small" icon="Document" @click="handleInstanceVariable(scope.row)"> 变量 </el-button>
</el-col> </el-col>
</el-row> </el-row>
</template> </template>
@@ -138,7 +138,7 @@
<el-table-column align="center" prop="version" label="版本号" width="90"> <el-table-column align="center" prop="version" label="版本号" width="90">
<template #default="scope"> v{{ scope.row.version }}.0</template> <template #default="scope"> v{{ scope.row.version }}.0</template>
</el-table-column> </el-table-column>
<el-table-column align="center" prop="suspensionState" label="状态" min-width="70"> <el-table-column v-hasPermi="['workflow:instance:active']" align="center" prop="suspensionState" label="状态" min-width="70">
<template #default="scope"> <template #default="scope">
<el-tag v-if="scope.row.suspensionState == 1" type="success">激活</el-tag> <el-tag v-if="scope.row.suspensionState == 1" type="success">激活</el-tag>
<el-tag v-else type="danger">挂起</el-tag> <el-tag v-else type="danger">挂起</el-tag>
@@ -170,7 +170,7 @@
<el-input v-model="form.value" placeholder="请输入变量值" /> <el-input v-model="form.value" placeholder="请输入变量值" />
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button type="primary" @click="handleVariable(ruleFormRef)">确认</el-button> <el-button v-hasPermi="['workflow:instance:variable']" type="primary" @click="handleVariable(ruleFormRef)">确认</el-button>
</el-form-item> </el-form-item>
</el-form> </el-form>
</el-card> </el-card>

View File

@@ -24,9 +24,14 @@ export default defineConfig(({ mode, command }) => {
open: true, open: true,
proxy: { proxy: {
[env.VITE_APP_BASE_API]: { [env.VITE_APP_BASE_API]: {
target: 'http://localhost:8080', target: 'http://localhost:8001',
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), '')
} }
} }

View File

@@ -1,28 +1,110 @@
import compression from 'vite-plugin-compression'; import { promises as fs } from 'fs';
import path from 'path';
import zlib from 'zlib';
import { promisify } from 'util';
import type { Plugin, ResolvedConfig } from 'vite';
export default (env: any) => { const gzip = promisify(zlib.gzip);
const { VITE_BUILD_COMPRESS } = env; const brotliCompress = promisify(zlib.brotliCompress);
const plugin: any[] = []; const compressibleFileRE = /\.(js|mjs|json|css|html)$/i;
if (VITE_BUILD_COMPRESS) { const defaultThreshold = 1025;
const compressList = VITE_BUILD_COMPRESS.split(',');
if (compressList.includes('gzip')) { type CompressionKind = 'gzip' | 'brotli';
// http://doc.ruoyi.vip/ruoyi-vue/other/faq.html#使用gzip解压缩静态文件
plugin.push( const compressionHandlers: Record<CompressionKind, { ext: string; compress: (content: Buffer) => Promise<Buffer> }> = {
compression({ gzip: {
ext: '.gz', ext: '.gz',
deleteOriginFile: false compress: (content) => gzip(content, { level: zlib.constants.Z_BEST_COMPRESSION })
}) },
); brotli: {
}
if (compressList.includes('brotli')) {
plugin.push(
compression({
ext: '.br', ext: '.br',
algorithm: 'brotliCompress', compress: (content) =>
deleteOriginFile: false brotliCompress(content, {
params: {
[zlib.constants.BROTLI_PARAM_QUALITY]: zlib.constants.BROTLI_MAX_QUALITY,
[zlib.constants.BROTLI_PARAM_MODE]: zlib.constants.BROTLI_MODE_TEXT
}
})
}
};
async function collectFiles(rootDir: string): Promise<string[]> {
const entries = await fs.readdir(rootDir, { withFileTypes: true });
const files = await Promise.all(
entries.map(async (entry) => {
const fullPath = path.join(rootDir, entry.name);
if (entry.isDirectory()) {
return collectFiles(fullPath);
}
return compressibleFileRE.test(entry.name) ? [fullPath] : [];
}) })
); );
return files.flat();
} }
function createCompressionPlugin(kind: CompressionKind): Plugin {
const handler = compressionHandlers[kind];
let config: ResolvedConfig | undefined;
return {
name: `local:compression:${kind}`,
apply: 'build',
enforce: 'post',
configResolved(resolvedConfig) {
config = resolvedConfig;
},
async closeBundle() {
const outputDir = path.resolve(process.cwd(), config?.build.outDir ?? 'dist');
const files = await collectFiles(outputDir);
const compressedEntries: Array<{ file: string; originalKb: string; compressedKb: string }> = [];
await Promise.all(
files.map(async (filePath) => {
const stat = await fs.stat(filePath);
if (stat.size < defaultThreshold) {
return;
}
const content = await fs.readFile(filePath);
const compressed = await handler.compress(content);
const outputFile = `${filePath}${handler.ext}`;
await fs.writeFile(outputFile, compressed);
compressedEntries.push({
file: path.relative(outputDir, outputFile).replaceAll('\\', '/'),
originalKb: (stat.size / 1024).toFixed(2),
compressedKb: (compressed.byteLength / 1024).toFixed(2)
});
})
);
if (!compressedEntries.length) {
return;
}
compressedEntries.sort((a, b) => a.file.localeCompare(b.file));
config?.logger.info(`\n[compression:${kind}] generated ${compressedEntries.length} files`);
for (const entry of compressedEntries) {
config?.logger.info(`${path.basename(outputDir)}/${entry.file} ${entry.originalKb}kb -> ${entry.compressedKb}kb`);
}
config?.logger.info('');
} }
return plugin; };
}
export default (env: Record<string, string>) => {
const { VITE_BUILD_COMPRESS } = env;
const plugins: Plugin[] = [];
if (!VITE_BUILD_COMPRESS) {
return plugins;
}
const compressionList = VITE_BUILD_COMPRESS.split(',').map((item) => item.trim()) as CompressionKind[];
if (compressionList.includes('gzip')) {
plugins.push(createCompressionPlugin('gzip'));
}
if (compressionList.includes('brotli')) {
plugins.push(createCompressionPlugin('brotli'));
}
return plugins;
}; };