Compare commits
1 Commits
257d984003
...
ts
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ab7ec17ae0 |
@@ -1,6 +1,6 @@
|
||||
# 页面标题
|
||||
VITE_APP_TITLE = 防御机管理系统
|
||||
VITE_APP_LOGO_TITLE = 防御机管理系统
|
||||
VITE_APP_TITLE = 驭鲲智防系统
|
||||
VITE_APP_LOGO_TITLE = 驭鲲智防系统
|
||||
|
||||
# 开发环境配置
|
||||
VITE_APP_ENV = 'development'
|
||||
@@ -20,13 +20,11 @@ VITE_APP_SNAILJOB_ADMIN = 'http://localhost:9002/snail-job'
|
||||
VITE_APP_PORT = 80
|
||||
|
||||
# 接口加密功能开关(如需关闭 后端也必须对应关闭)
|
||||
VITE_APP_ENCRYPT = false
|
||||
VITE_APP_ENCRYPT = true
|
||||
# 接口加密传输 RSA 公钥与后端解密私钥对应 如更换需前后端一同更换
|
||||
#VITE_APP_RSA_PUBLIC_KEY = 'MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ=='
|
||||
VITE_APP_RSA_PUBLIC_KEY = 'MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAJJA9FRt24dkoEqTRBdm9yx6MQ5+f+rDIrLdCAXFUtpGcsT+kea/iGzk50OfSzSYxQYF8g/iksanB9XN0Ab6anECAwEAAQ=='
|
||||
VITE_APP_RSA_PUBLIC_KEY = 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtVIQm+1AWMfkT1uVOyugKQwBeHtgpulABOrVfaL7Xzc1/rKSdLdjYZLZnnOU+rdE1jqz20MXDBozK22spH/j53d9nFLftHkMCRUlfc0MWEqtSla+2MdzElIHEbq4CSizZOcT8QXCRw7V2OuDqnC7XRQmnpA5Ks1WaKH5V0aoKWztjsqZV6vVWPqyDC94mvu/Q/9e6KVfkqAzR+afuo9OoBFewDGQ6jfqyu1TxoGZrfSr5lXOVeADf0CVfWCDze5y4f7er4JjPHWA2+yv8Scd9bqVJ4Q+/yR2Gd516Mi11OgBKc8GdkuA4/kfmXc7n91SMjgQP6DlTVuPc/6KnB/pyQIDAQAB'
|
||||
# 接口响应解密 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 = 'MIIBOgIBAAJBAIHPXmO+vf9u33iXv97aYeuPsJ7Ut+GdBjpT7JOIEE1rIEIfaKK268uPwAHW+/fNOKQiTBuunI4PAiL7CAQoVeECAwEAAQJACS6S3aP9/9W8MY/XqZAcQbOjPvOK94bKNvv9YvN1O785/2U5z7hUHvF8O8qIttlMNqhJ60JO9x58EUE51/dZ1QIhAISwFmIah9HoOLR5M+sBsUb5dIAm1mg0hj7T/5LyMTO7AiEA+nKebopGVz7LVkX53m29975r3T+DYu9n24eIqNwTDRMCIHn6xxj4+tNlgHuo0KnCrI5q1UbabVDuuFsZ2WHldIXxAiEAx6BA5H9XHgexcC0Kw97ZgvXHaG1NvrhxIizgIyh4Dv0CIB+VslCqTQTr9idyfVuURAiDdzg/9tm07GhWLDn6I6+E'
|
||||
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
|
||||
VITE_APP_CLIENT_ID = 'e5cd7e4891bf95d1d19206ce24a7b32e'
|
||||
@@ -36,3 +34,4 @@ VITE_APP_WEBSOCKET = false
|
||||
|
||||
# sse 开关
|
||||
VITE_APP_SSE = true
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 页面标题
|
||||
VITE_APP_TITLE = 防御机管理系统
|
||||
VITE_APP_LOGO_TITLE = 防御机管理系统
|
||||
VITE_APP_TITLE = 驭鲲智防系统
|
||||
VITE_APP_LOGO_TITLE = 驭鲲智防系统
|
||||
|
||||
# 生产环境配置
|
||||
VITE_APP_ENV = 'production'
|
||||
@@ -23,12 +23,12 @@ VITE_BUILD_COMPRESS = gzip
|
||||
VITE_APP_PORT = 80
|
||||
|
||||
# 接口加密功能开关(如需关闭 后端也必须对应关闭)
|
||||
# defensive_machine 后端 api-decrypt.enabled = false
|
||||
VITE_APP_ENCRYPT = false
|
||||
# defensive_machine 后端 api-decrypt.enabled = true
|
||||
VITE_APP_ENCRYPT = true
|
||||
# 接口加密传输 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 私钥与后端加密公钥对应 如更换需前后端一同更换
|
||||
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
|
||||
VITE_APP_CLIENT_ID = 'e5cd7e4891bf95d1d19206ce24a7b32e'
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
||||
<meta name="renderer" content="webkit" />
|
||||
<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>
|
||||
<!--[if lt IE 11
|
||||
]><script>
|
||||
|
||||
@@ -35,8 +35,10 @@
|
||||
"image-conversion": "2.1.1",
|
||||
"js-cookie": "3.0.5",
|
||||
"jsencrypt": "3.5.4",
|
||||
"jszip": "^3.10.1",
|
||||
"nprogress": "0.2.0",
|
||||
"pinia": "3.0.4",
|
||||
"qrcode": "^1.5.4",
|
||||
"screenfull": "6.0.2",
|
||||
"vue": "3.5.30",
|
||||
"vue-cropper": "1.1.4",
|
||||
@@ -55,6 +57,7 @@
|
||||
"@types/js-cookie": "3.0.6",
|
||||
"@types/node": "^25.4.0",
|
||||
"@types/nprogress": "0.2.3",
|
||||
"@types/qrcode": "^1.5.6",
|
||||
"@unocss/preset-attributify": "66.6.6",
|
||||
"@unocss/preset-icons": "66.6.6",
|
||||
"@unocss/preset-uno": "66.6.6",
|
||||
|
||||
@@ -4,6 +4,11 @@ export interface DeviceVO {
|
||||
*/
|
||||
id: string | number;
|
||||
|
||||
/**
|
||||
* 绑定用户 ID
|
||||
*/
|
||||
userId?: string | number;
|
||||
|
||||
/**
|
||||
* 设备编号
|
||||
*/
|
||||
@@ -19,6 +24,11 @@ export interface DeviceVO {
|
||||
*/
|
||||
status: string;
|
||||
|
||||
/**
|
||||
* 后端按请求语言返回的状态展示文案
|
||||
*/
|
||||
statusLabel?: string;
|
||||
|
||||
/**
|
||||
* WiFi名称
|
||||
*/
|
||||
@@ -98,7 +108,6 @@ export interface DeviceVO {
|
||||
* 识别模式 0-蓝牙 1-蓝牙+人脸
|
||||
*/
|
||||
patternPerception?: string;
|
||||
|
||||
}
|
||||
|
||||
export interface DeviceForm extends BaseEntity {
|
||||
@@ -107,6 +116,11 @@ export interface DeviceForm extends BaseEntity {
|
||||
*/
|
||||
id?: string | number;
|
||||
|
||||
/**
|
||||
* 绑定用户 ID
|
||||
*/
|
||||
userId?: string | number;
|
||||
|
||||
/**
|
||||
* 设备编号
|
||||
*/
|
||||
|
||||
@@ -14,6 +14,11 @@ export interface MessageVO {
|
||||
*/
|
||||
categoryName: string;
|
||||
|
||||
/**
|
||||
* 后端按请求语言返回的告警类型展示文案
|
||||
*/
|
||||
categoryLabel?: string;
|
||||
|
||||
/**
|
||||
* 图片地址
|
||||
*/
|
||||
@@ -27,7 +32,6 @@ export interface MessageVO {
|
||||
* 视频地址
|
||||
*/
|
||||
video: string | number;
|
||||
|
||||
}
|
||||
|
||||
export interface MessageForm extends BaseEntity {
|
||||
@@ -55,11 +59,9 @@ export interface MessageForm extends BaseEntity {
|
||||
* 视频地址
|
||||
*/
|
||||
video?: string | number;
|
||||
|
||||
}
|
||||
|
||||
export interface MessageQuery extends PageQuery {
|
||||
|
||||
/**
|
||||
* 类型
|
||||
*/
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 7.9 KiB After Width: | Height: | Size: 8.8 KiB |
@@ -2,8 +2,10 @@
|
||||
<el-breadcrumb class="app-breadcrumb" separator="/">
|
||||
<transition-group name="breadcrumb">
|
||||
<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>
|
||||
<a v-else @click.prevent="handleLink(item)">{{ item.meta?.title }}</a>
|
||||
<span v-if="item.redirect === 'noRedirect' || index == levelList.length - 1" class="no-redirect">
|
||||
{{ translateRouteTitle(item.meta?.title as string) }}
|
||||
</span>
|
||||
<a v-else @click.prevent="handleLink(item)">{{ translateRouteTitle(item.meta?.title as string) }}</a>
|
||||
</el-breadcrumb-item>
|
||||
</transition-group>
|
||||
</el-breadcrumb>
|
||||
@@ -12,6 +14,7 @@
|
||||
<script setup lang="ts">
|
||||
import { RouteLocationMatched } from 'vue-router';
|
||||
import { usePermissionStore } from '@/store/modules/permission';
|
||||
import { translateRouteTitle } from '@/utils/i18n';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
@@ -24,23 +24,17 @@
|
||||
</el-upload>
|
||||
<!-- 上传提示 -->
|
||||
<div v-if="showTip" class="el-upload__tip">
|
||||
请上传
|
||||
<template v-if="fileSize">
|
||||
大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b>
|
||||
</template>
|
||||
<template v-if="fileType">
|
||||
格式为 <b style="color: #f56c6c">{{ fileType.join('/') }}</b>
|
||||
</template>
|
||||
的文件
|
||||
{{ $t('upload.tip', { size: fileSize, types: fileType.join('/') }) }}
|
||||
</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" />
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { listByIds, delOss } from '@/api/system/oss';
|
||||
import { OssVO } from '@/api/system/oss/types';
|
||||
import { propTypes } from '@/utils/propTypes';
|
||||
@@ -73,6 +67,7 @@ const props = defineProps({
|
||||
});
|
||||
|
||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||
const { t } = useI18n();
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
const number = ref(0);
|
||||
const uploadList = ref<any[]>([]);
|
||||
@@ -140,35 +135,35 @@ const handleBeforeUpload = (file: any) => {
|
||||
isImg = file.type.indexOf('image') > -1;
|
||||
}
|
||||
if (!isImg) {
|
||||
proxy?.$modal.msgError(`文件格式不正确, 请上传${props.fileType.join('/')}图片格式文件!`);
|
||||
proxy?.$modal.msgError(t('upload.invalidFormat', { types: props.fileType.join('/') }));
|
||||
return false;
|
||||
}
|
||||
if (file.name.includes(',')) {
|
||||
proxy?.$modal.msgError('文件名不正确,不能包含英文逗号!');
|
||||
proxy?.$modal.msgError(t('upload.invalidFileName'));
|
||||
return false;
|
||||
}
|
||||
if (props.fileSize) {
|
||||
const isLt = file.size / 1024 / 1024 < props.fileSize;
|
||||
if (!isLt) {
|
||||
proxy?.$modal.msgError(`上传头像图片大小不能超过 ${props.fileSize} MB!`);
|
||||
proxy?.$modal.msgError(t('upload.tooLarge', { size: props.fileSize }));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
//压缩图片,开启压缩并且大于指定的压缩大小时才压缩
|
||||
if (props.compressSupport && file.size / 1024 > props.compressTargetSize) {
|
||||
proxy?.$modal.loading('正在上传图片,请稍候...');
|
||||
proxy?.$modal.loading(t('upload.uploading'));
|
||||
number.value++;
|
||||
return compressAccurately(file, props.compressTargetSize);
|
||||
} else {
|
||||
proxy?.$modal.loading('正在上传图片,请稍候...');
|
||||
proxy?.$modal.loading(t('upload.uploading'));
|
||||
number.value++;
|
||||
}
|
||||
};
|
||||
|
||||
// 文件个数超出
|
||||
const handleExceed = () => {
|
||||
proxy?.$modal.msgError(`上传文件数量不能超过 ${props.limit} 个!`);
|
||||
proxy?.$modal.msgError(t('upload.tooMany', { limit: props.limit }));
|
||||
};
|
||||
|
||||
// 上传成功回调
|
||||
@@ -211,7 +206,7 @@ const uploadedSuccessfully = () => {
|
||||
|
||||
// 上传失败
|
||||
const handleUploadError = () => {
|
||||
proxy?.$modal.msgError('上传图片失败');
|
||||
proxy?.$modal.msgError(t('upload.failed'));
|
||||
proxy?.$modal.closeLoading();
|
||||
};
|
||||
|
||||
|
||||
@@ -5,8 +5,12 @@
|
||||
</div>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item :disabled="appStore.language === 'zh_CN'" command="zh_CN"> 中文 </el-dropdown-item>
|
||||
<el-dropdown-item :disabled="appStore.language === 'en_US'" command="en_US"> English </el-dropdown-item>
|
||||
<el-dropdown-item :disabled="appStore.language === LanguageEnum.zh_CN" :command="LanguageEnum.zh_CN">
|
||||
{{ 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>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
@@ -16,18 +20,19 @@
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useAppStore } from '@/store/modules/app';
|
||||
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 { locale } = useI18n();
|
||||
const { locale, t } = useI18n();
|
||||
|
||||
const message: any = {
|
||||
zh_CN: '切换语言成功!',
|
||||
en_US: 'Switch Language Successful!'
|
||||
};
|
||||
const handleLanguageChange = (lang: any) => {
|
||||
const handleLanguageChange = (lang: LanguageEnum) => {
|
||||
locale.value = lang;
|
||||
appStore.changeLanguage(lang);
|
||||
ElMessage.success(message[lang] || '切换语言成功!');
|
||||
VXETable.setLanguage(getVxeLanguage(lang));
|
||||
useDynamicTitle();
|
||||
ElMessage.success(t('language.switched'));
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,16 +1,22 @@
|
||||
<template>
|
||||
<div class="top-right-btn" :style="style">
|
||||
<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-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-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">
|
||||
<el-popover placement="bottom" trigger="click">
|
||||
<div class="tree-header">显示/隐藏列</div>
|
||||
<div class="tree-header">{{ $t('toolbar.columns') }}</div>
|
||||
<el-tree
|
||||
ref="columnRef"
|
||||
:data="columns"
|
||||
|
||||
@@ -16,15 +16,17 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useAppStore } from '@/store/modules/app';
|
||||
|
||||
const appStore = useAppStore();
|
||||
const size = computed(() => appStore.size);
|
||||
|
||||
const sizeOptions = ref([
|
||||
{ label: '较大', value: 'large' },
|
||||
{ label: '默认', value: 'default' },
|
||||
{ label: '稍小', value: 'small' }
|
||||
const { t } = useI18n();
|
||||
const sizeOptions = computed(() => [
|
||||
{ label: t('size.large'), value: 'large' },
|
||||
{ label: t('size.default'), value: 'default' },
|
||||
{ label: t('size.small'), value: 'small' }
|
||||
]);
|
||||
|
||||
const handleSetSize = (size: 'large' | 'default' | 'small') => {
|
||||
|
||||
@@ -3,16 +3,16 @@
|
||||
<template v-for="(item, index) in topMenus">
|
||||
<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 : ''" />
|
||||
{{ item.meta?.title }}</el-menu-item
|
||||
{{ translateRouteTitle(item.meta?.title as string) }}</el-menu-item
|
||||
>
|
||||
</template>
|
||||
|
||||
<!-- 顶部菜单超出数量折叠 -->
|
||||
<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">
|
||||
<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>
|
||||
</el-sub-menu>
|
||||
@@ -26,6 +26,7 @@ import { useAppStore } from '@/store/modules/app';
|
||||
import { useSettingsStore } from '@/store/modules/settings';
|
||||
import { usePermissionStore } from '@/store/modules/permission';
|
||||
import { RouteRecordRaw } from 'vue-router';
|
||||
import { translateRouteTitle } from '@/utils/i18n';
|
||||
|
||||
// 顶部栏初始数
|
||||
const visibleNumber = ref<number>(-1);
|
||||
|
||||
@@ -3,3 +3,15 @@ export enum LanguageEnum {
|
||||
|
||||
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';
|
||||
};
|
||||
|
||||
@@ -1,9 +1,84 @@
|
||||
export default {
|
||||
app: {
|
||||
title: 'Reinkun Intelligent Defense System'
|
||||
},
|
||||
// 路由国际化
|
||||
route: {
|
||||
dashboard: 'Dashboard',
|
||||
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: {
|
||||
selectPlaceholder: 'Please select/enter a company name',
|
||||
@@ -73,6 +148,7 @@ export default {
|
||||
navbar: {
|
||||
full: 'Full Screen',
|
||||
language: 'Language',
|
||||
search: 'Search',
|
||||
dashboard: 'Dashboard',
|
||||
document: 'Document',
|
||||
message: 'Message',
|
||||
@@ -80,6 +156,144 @@ export default {
|
||||
selectTenant: 'Select Tenant',
|
||||
layoutSetting: 'Layout Setting',
|
||||
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'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// 自定义国际化配置
|
||||
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 en_US from '@/lang/en_US';
|
||||
|
||||
@@ -10,11 +10,19 @@ import en_US from '@/lang/en_US';
|
||||
* @returns zh-cn|en ...
|
||||
*/
|
||||
export const getLanguage = (): LanguageEnum => {
|
||||
const language = useStorage<LanguageEnum>('language', LanguageEnum.zh_CN);
|
||||
if (language.value) {
|
||||
return language.value;
|
||||
const storedLanguage = localStorage.getItem('language');
|
||||
if (isSupportedLanguage(storedLanguage)) {
|
||||
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({
|
||||
@@ -22,12 +30,15 @@ const i18n = createI18n({
|
||||
allowComposition: true,
|
||||
legacy: false,
|
||||
locale: getLanguage(),
|
||||
fallbackLocale: LanguageEnum.zh_CN,
|
||||
messages: {
|
||||
zh_CN: zh_CN,
|
||||
en_US: en_US
|
||||
[LanguageEnum.zh_CN]: zh_CN,
|
||||
[LanguageEnum.en_US]: en_US
|
||||
}
|
||||
});
|
||||
|
||||
setDocumentLanguage(i18n.global.locale.value);
|
||||
|
||||
export default i18n;
|
||||
|
||||
export type LanguageType = typeof zh_CN;
|
||||
|
||||
@@ -1,9 +1,84 @@
|
||||
export default {
|
||||
app: {
|
||||
title: '驭鲲智防系统'
|
||||
},
|
||||
// 路由国际化
|
||||
route: {
|
||||
dashboard: '首页',
|
||||
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: {
|
||||
selectPlaceholder: '请选择/输入公司名称',
|
||||
@@ -73,6 +148,7 @@ export default {
|
||||
navbar: {
|
||||
full: '全屏',
|
||||
language: '语言',
|
||||
search: '搜索',
|
||||
dashboard: '首页',
|
||||
document: '项目文档',
|
||||
message: '消息',
|
||||
@@ -80,6 +156,144 @@ export default {
|
||||
selectTenant: '选择租户',
|
||||
layoutSetting: '布局设置',
|
||||
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: '到期提醒'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
</el-select>
|
||||
|
||||
<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">
|
||||
<svg-icon class-name="search-icon" icon-class="search" />
|
||||
</div>
|
||||
@@ -47,14 +47,6 @@
|
||||
</el-popover>
|
||||
</div>
|
||||
</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">
|
||||
<screenfull id="screenfull" class="right-menu-item hover-effect" />
|
||||
</el-tooltip>
|
||||
@@ -105,8 +97,8 @@ import notice from './notice/index.vue';
|
||||
import router from '@/router';
|
||||
import { ElMessageBoxOptions } from 'element-plus/es/components/message-box/src/message-box.type';
|
||||
import { NavTypeEnum } from '@/enums/NavTypeEnum';
|
||||
import Logo from "@/layout/components/Sidebar/Logo.vue";
|
||||
import TopBar from './TopBar'
|
||||
import Logo from '@/layout/components/Sidebar/Logo.vue';
|
||||
import TopBar from './TopBar/index.vue';
|
||||
|
||||
const appStore = useAppStore();
|
||||
const userStore = useUserStore();
|
||||
@@ -170,9 +162,9 @@ const toggleSideBar = () => {
|
||||
};
|
||||
|
||||
const logout = async () => {
|
||||
await ElMessageBox.confirm('确定注销并退出系统吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
await ElMessageBox.confirm(proxy.$t('navbar.logoutConfirm'), proxy.$t('navbar.prompt'), {
|
||||
confirmButtonText: proxy.$t('navbar.confirm'),
|
||||
cancelButtonText: proxy.$t('navbar.cancel'),
|
||||
type: 'warning'
|
||||
} as ElMessageBoxOptions);
|
||||
userStore.logout().then(() => {
|
||||
@@ -282,7 +274,6 @@ watch(
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
|
||||
.errLog-container {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<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">
|
||||
<el-tooltip content="左侧菜单" placement="bottom">
|
||||
<el-tooltip :content="$t('settings.leftMenu')" placement="bottom">
|
||||
<div
|
||||
class="item left"
|
||||
@click="handleNavType(NavTypeEnum.LEFT)"
|
||||
@@ -13,7 +13,7 @@
|
||||
</div>
|
||||
</el-tooltip>
|
||||
|
||||
<el-tooltip content="混合菜单" placement="bottom">
|
||||
<el-tooltip :content="$t('settings.mixedMenu')" placement="bottom">
|
||||
<div
|
||||
class="item mix"
|
||||
@click="handleNavType(NavTypeEnum.MIX)"
|
||||
@@ -23,7 +23,7 @@
|
||||
<b></b><b></b>
|
||||
</div>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="顶部菜单" placement="bottom">
|
||||
<el-tooltip :content="$t('settings.topMenu')" placement="bottom">
|
||||
<div
|
||||
class="item top"
|
||||
@click="handleNavType(NavTypeEnum.TOP)"
|
||||
@@ -35,14 +35,14 @@
|
||||
</el-tooltip>
|
||||
</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-item" @click="handleTheme(SideThemeEnum.DARK)">
|
||||
<img src="@/assets/images/dark.svg" alt="dark" />
|
||||
<div v-if="sideTheme === 'theme-dark'" class="setting-drawer-block-checbox-selectIcon" style="display: block">
|
||||
<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>
|
||||
<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">
|
||||
<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"
|
||||
/>
|
||||
@@ -53,8 +53,8 @@
|
||||
<div class="setting-drawer-block-checbox-item" @click="handleTheme(SideThemeEnum.LIGHT)">
|
||||
<img src="@/assets/images/light.svg" alt="light" />
|
||||
<div v-if="sideTheme === 'theme-light'" class="setting-drawer-block-checbox-selectIcon" style="display: block">
|
||||
<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>
|
||||
<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">
|
||||
<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"
|
||||
/>
|
||||
@@ -64,13 +64,13 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="drawer-item">
|
||||
<span>主题颜色</span>
|
||||
<span>{{ $t('settings.themeColor') }}</span>
|
||||
<span class="comp-style">
|
||||
<el-color-picker v-model="theme" :predefine="predefineColors" @change="themeChange" />
|
||||
</span>
|
||||
</div>
|
||||
<div class="drawer-item">
|
||||
<span>深色模式</span>
|
||||
<span>{{ $t('settings.darkMode') }}</span>
|
||||
<span class="comp-style">
|
||||
<el-switch v-model="isDark" class="drawer-switch" @change="toggleDark" />
|
||||
</span>
|
||||
@@ -78,38 +78,38 @@
|
||||
|
||||
<el-divider />
|
||||
|
||||
<h3 class="drawer-title">系统布局配置</h3>
|
||||
<h3 class="drawer-title">{{ $t('settings.systemLayout') }}</h3>
|
||||
|
||||
<div class="drawer-item">
|
||||
<span>开启 Tags-Views</span>
|
||||
<span>{{ $t('settings.enableTagsView') }}</span>
|
||||
<span class="comp-style">
|
||||
<el-switch v-model="settingsStore.tagsView" class="drawer-switch" />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="drawer-item">
|
||||
<span>显示页签图标</span>
|
||||
<span>{{ $t('settings.showTagsIcon') }}</span>
|
||||
<span class="comp-style">
|
||||
<el-switch v-model="settingsStore.tagsIcon" :disabled="!settingsStore.tagsView" class="drawer-switch" />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="drawer-item">
|
||||
<span>固定 Header</span>
|
||||
<span>{{ $t('settings.fixedHeader') }}</span>
|
||||
<span class="comp-style">
|
||||
<el-switch v-model="settingsStore.fixedHeader" class="drawer-switch" />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="drawer-item">
|
||||
<span>显示 Logo</span>
|
||||
<span>{{ $t('settings.showLogo') }}</span>
|
||||
<span class="comp-style">
|
||||
<el-switch v-model="settingsStore.sidebarLogo" class="drawer-switch" />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="drawer-item">
|
||||
<span>动态标题</span>
|
||||
<span>{{ $t('settings.dynamicTitle') }}</span>
|
||||
<span class="comp-style">
|
||||
<el-switch v-model="settingsStore.dynamicTitle" class="drawer-switch" @change="dynamicTitleChange" />
|
||||
</span>
|
||||
@@ -117,12 +117,13 @@
|
||||
|
||||
<el-divider />
|
||||
|
||||
<el-button type="primary" plain icon="DocumentAdd" @click="saveSetting">保存配置</el-button>
|
||||
<el-button plain icon="Refresh" @click="resetSetting">重置配置</el-button>
|
||||
<el-button type="primary" plain icon="DocumentAdd" @click="saveSetting">{{ $t('settings.save') }}</el-button>
|
||||
<el-button plain icon="Refresh" @click="resetSetting">{{ $t('settings.reset') }}</el-button>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useDynamicTitle } from '@/utils/dynamicTitle';
|
||||
import { useAppStore } from '@/store/modules/app';
|
||||
import { useSettingsStore } from '@/store/modules/settings';
|
||||
@@ -136,6 +137,7 @@ const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||
const appStore = useAppStore();
|
||||
const settingsStore = useSettingsStore();
|
||||
const permissionStore = usePermissionStore();
|
||||
const { t } = useI18n();
|
||||
|
||||
const showSettings = ref(false);
|
||||
const theme = ref(settingsStore.theme);
|
||||
@@ -161,15 +163,15 @@ const toggleDark = () => useToggle(isDark);
|
||||
|
||||
/** 菜单导航设置 */
|
||||
watch(
|
||||
() => navType,
|
||||
(val: string) => {
|
||||
if (val.value === NavTypeEnum.TOP) {
|
||||
navType,
|
||||
(val) => {
|
||||
if (val === NavTypeEnum.TOP) {
|
||||
appStore.toggleSideBarHide(true);
|
||||
permissionStore.setSidebarRouters(permissionStore.defaultRoutes as any);
|
||||
} else if (val.value === NavTypeEnum.LEFT) {
|
||||
} else if (val === NavTypeEnum.LEFT) {
|
||||
appStore.toggleSideBarHide(false);
|
||||
permissionStore.setSidebarRouters(permissionStore.defaultRoutes as any);
|
||||
} else if (val.value === NavTypeEnum.MIX) {
|
||||
} else if (val === NavTypeEnum.MIX) {
|
||||
appStore.toggleSideBarHide(false);
|
||||
}
|
||||
},
|
||||
@@ -200,7 +202,7 @@ const handleTheme = (val: string) => {
|
||||
settingsStore.sideTheme = val;
|
||||
};
|
||||
const saveSetting = () => {
|
||||
proxy?.$modal.loading('正在保存到本地,请稍候...');
|
||||
proxy?.$modal.loading(t('settings.saving'));
|
||||
const settings = useStorage<LayoutSetting>('layout-setting', defaultSettings);
|
||||
settings.value.tagsView = storeSettings.value.tagsView;
|
||||
settings.value.tagsIcon = storeSettings.value.tagsIcon;
|
||||
@@ -215,7 +217,7 @@ const saveSetting = () => {
|
||||
}, 1000);
|
||||
};
|
||||
const resetSetting = () => {
|
||||
proxy?.$modal.loading('正在清除设置缓存并刷新,请稍候...');
|
||||
proxy?.$modal.loading(t('settings.resetting'));
|
||||
useStorage<any>('layout-setting', null).value = null;
|
||||
setTimeout('window.location.reload()', 1000);
|
||||
};
|
||||
|
||||
@@ -5,7 +5,9 @@
|
||||
<el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{ 'submenu-title-noDropdown': !isNest }">
|
||||
<svg-icon :icon-class="onlyOneChild.meta.icon || (item.meta && item.meta.icon)" />
|
||||
<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>
|
||||
</el-menu-item>
|
||||
</app-link>
|
||||
@@ -14,7 +16,7 @@
|
||||
<el-sub-menu v-else ref="subMenu" :index="resolvePath(item.path)" teleported>
|
||||
<template v-if="item.meta" #title>
|
||||
<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>
|
||||
|
||||
<sidebar-item
|
||||
@@ -34,6 +36,7 @@ import { isExternal } from '@/utils/validate';
|
||||
import AppLink from './Link.vue';
|
||||
import { getNormalPath } from '@/utils/ruoyi';
|
||||
import { RouteRecordRaw } from 'vue-router';
|
||||
import { translateRouteTitle } from '@/utils/i18n';
|
||||
|
||||
const props = defineProps({
|
||||
item: {
|
||||
|
||||
@@ -6,26 +6,28 @@
|
||||
:key="tag.path"
|
||||
:data-path="tag.path"
|
||||
: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"
|
||||
:style="activeStyle(tag)"
|
||||
@click.middle="!isAffix(tag) ? closeSelectedTag(tag) : ''"
|
||||
@contextmenu.prevent="openMenu(tag, $event)"
|
||||
>
|
||||
<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>
|
||||
<svg-icon v-if="tagsIcon && tag.meta && tag.meta.icon && tag.meta.icon !== '#'" :icon-class="tag.meta.icon" />
|
||||
<span class="tags-view-item-title">{{ translateRouteTitle(tag.title as string) }}</span>
|
||||
<span v-if="!isAffix(tag)" @click.prevent.stop="closeSelectedTag(tag)">
|
||||
<close class="el-icon-close" style="width: 1em; height: 1em; vertical-align: middle" />
|
||||
</span>
|
||||
</router-link>
|
||||
</scroll-pane>
|
||||
<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 v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)"><close style="width: 1em; height: 1em" /> 关闭当前</li>
|
||||
<li @click="closeOthersTags"><circle-close style="width: 1em; height: 1em" /> 关闭其他</li>
|
||||
<li v-if="!isFirstView()" @click="closeLeftTags"><back style="width: 1em; height: 1em" /> 关闭左侧</li>
|
||||
<li v-if="!isLastView()" @click="closeRightTags"><right style="width: 1em; height: 1em" /> 关闭右侧</li>
|
||||
<li @click="closeAllTags(selectedTag)"><circle-close 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" /> {{ $t('tagsView.closeCurrent') }}
|
||||
</li>
|
||||
<li @click="closeOthersTags"><circle-close style="width: 1em; height: 1em" /> {{ $t('tagsView.closeOthers') }}</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>
|
||||
</div>
|
||||
</template>
|
||||
@@ -37,6 +39,7 @@ import { useSettingsStore } from '@/store/modules/settings';
|
||||
import { usePermissionStore } from '@/store/modules/permission';
|
||||
import { useTagsViewStore } from '@/store/modules/tagsView';
|
||||
import { RouteRecordRaw, RouteLocationNormalized } from 'vue-router';
|
||||
import { translateRouteTitle } from '@/utils/i18n';
|
||||
|
||||
const visible = ref(false);
|
||||
const top = ref(0);
|
||||
@@ -52,7 +55,7 @@ const router = useRouter();
|
||||
const visitedViews = computed(() => useTagsViewStore().getVisitedViews());
|
||||
const routes = computed(() => usePermissionStore().getRoutes());
|
||||
const theme = computed(() => useSettingsStore().theme);
|
||||
const tagsIcon = computed(() => useSettingsStore().tagsIcon)
|
||||
const tagsIcon = computed(() => useSettingsStore().tagsIcon);
|
||||
|
||||
watch(route, () => {
|
||||
addTags();
|
||||
@@ -180,7 +183,8 @@ const closeLeftTags = () => {
|
||||
});
|
||||
};
|
||||
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(() => {
|
||||
moveToCurrentTag();
|
||||
});
|
||||
@@ -261,7 +265,11 @@ onMounted(() => {
|
||||
margin-left: 5px;
|
||||
margin-top: 4px;
|
||||
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 {
|
||||
color: var(--el-color-primary);
|
||||
border-color: var(--el-color-primary-light-5);
|
||||
|
||||
@@ -4,66 +4,69 @@
|
||||
|
||||
<el-sub-menu index="more" class="el-sub-menu__hide-arrow" v-if="moreRoutes.length > 0">
|
||||
<template #title>
|
||||
<span>更多菜单</span>
|
||||
<span>{{ $t('navbar.moreMenu') }}</span>
|
||||
</template>
|
||||
<sidebar-item :key="route.path + index" v-for="(route, index) in moreRoutes" :item="route" :base-path="route.path" />
|
||||
</el-sub-menu>
|
||||
</el-menu>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import SidebarItem from '../Sidebar/SidebarItem'
|
||||
import {useAppStore} from '@/store/modules/app'
|
||||
import {useSettingsStore} from '@/store/modules/settings'
|
||||
import {usePermissionStore} from '@/store/modules/permission'
|
||||
<script setup lang="ts">
|
||||
import SidebarItem from '../Sidebar/SidebarItem.vue';
|
||||
import { useAppStore } from '@/store/modules/app';
|
||||
import { useSettingsStore } from '@/store/modules/settings';
|
||||
import { usePermissionStore } from '@/store/modules/permission';
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
|
||||
const route = useRoute()
|
||||
const appStore = useAppStore()
|
||||
const settingsStore = useSettingsStore()
|
||||
const permissionStore = usePermissionStore()
|
||||
const route = useRoute();
|
||||
const appStore = useAppStore();
|
||||
const settingsStore = useSettingsStore();
|
||||
const permissionStore = usePermissionStore();
|
||||
|
||||
const sidebarRouters = computed(() => permissionStore.sidebarRouters)
|
||||
const theme = computed(() => settingsStore.theme)
|
||||
const device = computed(() => appStore.device)
|
||||
const sidebarRouters = computed(() => permissionStore.sidebarRouters);
|
||||
const theme = computed(() => settingsStore.theme);
|
||||
const device = computed(() => appStore.device);
|
||||
const activeMenu = computed(() => {
|
||||
const { meta, path } = route
|
||||
const { meta, path } = route;
|
||||
if (meta.activeMenu) {
|
||||
return meta.activeMenu
|
||||
return meta.activeMenu;
|
||||
}
|
||||
return path
|
||||
})
|
||||
return path;
|
||||
});
|
||||
|
||||
const visibleNumber = ref(5)
|
||||
const topMenus = computed(() => {
|
||||
return permissionStore.sidebarRouters.filter((f) => !f.hidden).slice(0, visibleNumber.value)
|
||||
})
|
||||
const moreRoutes = computed(() => {
|
||||
return permissionStore.sidebarRouters.filter((f) => !f.hidden).slice(visibleNumber.value, sidebarRouters.value.length - visibleNumber.value)
|
||||
})
|
||||
const visibleNumber = ref(5);
|
||||
const topMenus = computed<RouteRecordRaw[]>(() => {
|
||||
return permissionStore.sidebarRouters.filter((f) => !f.hidden).slice(0, visibleNumber.value) as RouteRecordRaw[];
|
||||
});
|
||||
const moreRoutes = computed<RouteRecordRaw[]>(() => {
|
||||
return permissionStore.sidebarRouters
|
||||
.filter((f) => !f.hidden)
|
||||
.slice(visibleNumber.value, sidebarRouters.value.length - visibleNumber.value) as RouteRecordRaw[];
|
||||
});
|
||||
function setVisibleNumber() {
|
||||
let width = document.body.getBoundingClientRect().width
|
||||
let width = document.body.getBoundingClientRect().width;
|
||||
if (width >= 1000) {
|
||||
width -= 500
|
||||
width -= 500;
|
||||
}
|
||||
visibleNumber.value = parseInt(width / 3 / 85)
|
||||
visibleNumber.value = Math.floor(width / 3 / 85);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('resize', setVisibleNumber)
|
||||
})
|
||||
window.addEventListener('resize', setVisibleNumber);
|
||||
});
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', setVisibleNumber)
|
||||
})
|
||||
window.removeEventListener('resize', setVisibleNumber);
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
setVisibleNumber()
|
||||
})
|
||||
setVisibleNumber();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
@@ -76,7 +79,10 @@ onMounted(() => {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -85,7 +91,7 @@ onMounted(() => {
|
||||
float: left;
|
||||
line-height: 50px !important;
|
||||
color: #303133 !important;
|
||||
margin: 0 15px -3px!important;
|
||||
margin: 0 15px -3px !important;
|
||||
}
|
||||
|
||||
/* topbar more arrow */
|
||||
@@ -97,7 +103,8 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
/* 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;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<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>
|
||||
<el-autocomplete
|
||||
ref="layoutMenuAutocompleteRef"
|
||||
v-model="state.menuQuery"
|
||||
:fetch-suggestions="menuSearch"
|
||||
placeholder="搜索"
|
||||
:placeholder="$t('navbar.search')"
|
||||
:fit-input-width="true"
|
||||
@select="onHandleSelect"
|
||||
>
|
||||
@@ -30,6 +30,7 @@ import { getNormalPath } from '@/utils/ruoyi';
|
||||
import { isHttp } from '@/utils/validate';
|
||||
import { usePermissionStore } from '@/store/modules/permission';
|
||||
import { RouteRecordRaw } from 'vue-router';
|
||||
import { translateRouteTitle } from '@/utils/i18n';
|
||||
type Router = Array<{
|
||||
path: string;
|
||||
icon: string;
|
||||
@@ -64,7 +65,17 @@ const openSearch = () => {
|
||||
// 搜索弹窗关闭
|
||||
const closeSearch = () => {
|
||||
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 options = state.menuList.filter((item) => {
|
||||
@@ -87,7 +98,7 @@ const generateRoutes = (routes: RouteRecordRaw[], basePath = '', prefixTitle: st
|
||||
title: [...prefixTitle]
|
||||
};
|
||||
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') {
|
||||
// only push the routes with title
|
||||
// special case: need to exclude parent router without redirect
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div v-loading="state.loading" class="layout-navbars-breadcrumb-user-news">
|
||||
<div class="head-box">
|
||||
<div class="head-box-title">通知公告</div>
|
||||
<div class="head-box-btn" @click="readAll">全部已读</div>
|
||||
<div class="head-box-title">{{ $t('notice.title') }}</div>
|
||||
<div class="head-box-btn" @click="readAll">{{ $t('notice.readAll') }}</div>
|
||||
</div>
|
||||
<div v-loading="state.loading" class="content-box">
|
||||
<template v-if="newsList.length > 0">
|
||||
@@ -13,13 +13,13 @@
|
||||
<div class="content-box-time">{{ v.time }}</div>
|
||||
</div>
|
||||
<!-- 已读/未读 -->
|
||||
<span v-if="v.read" class="el-tag el-tag--success el-tag--mini read">已读</span>
|
||||
<span v-else class="el-tag el-tag--danger el-tag--mini read">未读</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">{{ $t('notice.unread') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<el-empty v-else :description="'消息为空'"></el-empty>
|
||||
<el-empty v-else :description="$t('notice.empty')"></el-empty>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -33,7 +33,13 @@ import i18n from '@/lang/index';
|
||||
|
||||
// vxeTable
|
||||
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 { getVxeLanguage } from '@/enums/LanguageEnum';
|
||||
VXETable.setI18n('zh-CN', vxeZhCN);
|
||||
VXETable.setI18n('en-US', vxeEnUS);
|
||||
VXETable.setLanguage(getVxeLanguage(i18n.global.locale.value));
|
||||
VXETable.setConfig({
|
||||
zIndex: 999999
|
||||
});
|
||||
@@ -44,6 +50,8 @@ ElDialog.props.closeOnClickModal.default = false;
|
||||
|
||||
const app = createApp(App);
|
||||
|
||||
document.title = i18n.global.t('app.title');
|
||||
|
||||
app.use(HighLight);
|
||||
app.use(ElementIcons);
|
||||
app.use(router);
|
||||
|
||||
@@ -3,6 +3,8 @@ import enUS from 'element-plus/es/locale/lang/en';
|
||||
import { defineStore } from 'pinia';
|
||||
import { useStorage } from '@vueuse/core';
|
||||
import { ref, reactive, computed } from 'vue';
|
||||
import { getLanguage, setDocumentLanguage } from '@/lang';
|
||||
import { LanguageEnum } from '@/enums/LanguageEnum';
|
||||
|
||||
export const useAppStore = defineStore('app', () => {
|
||||
const sidebarStatus = useStorage('sidebarStatus', '1');
|
||||
@@ -15,10 +17,10 @@ export const useAppStore = defineStore('app', () => {
|
||||
const size = useStorage<'large' | 'default' | 'small'>('size', 'default');
|
||||
|
||||
// 语言
|
||||
const language = useStorage('language', 'zh_CN');
|
||||
const languageObj: any = {
|
||||
en_US: enUS,
|
||||
zh_CN: zhCN
|
||||
const language = useStorage<LanguageEnum>('language', getLanguage());
|
||||
const languageObj: Record<LanguageEnum, typeof zhCN> = {
|
||||
[LanguageEnum.en_US]: enUS,
|
||||
[LanguageEnum.zh_CN]: zhCN
|
||||
};
|
||||
const locale = computed(() => {
|
||||
return languageObj[language.value];
|
||||
@@ -53,8 +55,9 @@ export const useAppStore = defineStore('app', () => {
|
||||
sidebar.hide = status;
|
||||
};
|
||||
|
||||
const changeLanguage = (val: string): void => {
|
||||
const changeLanguage = (val: LanguageEnum): void => {
|
||||
language.value = val;
|
||||
setDocumentLanguage(val);
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import defaultSettings from '@/settings';
|
||||
import { useSettingsStore } from '@/store/modules/settings';
|
||||
import i18n from '@/lang';
|
||||
import { translateRouteTitle } from '@/utils/i18n';
|
||||
|
||||
/**
|
||||
* 动态修改标题
|
||||
*/
|
||||
export const useDynamicTitle = () => {
|
||||
const settingsStore = useSettingsStore();
|
||||
const appTitle = i18n.global.t('app.title');
|
||||
if (settingsStore.dynamicTitle) {
|
||||
document.title = settingsStore.title + ' - ' + import.meta.env.VITE_APP_TITLE;
|
||||
document.title = translateRouteTitle(settingsStore.title) + ' - ' + appTitle;
|
||||
} else {
|
||||
document.title = defaultSettings.title as string;
|
||||
document.title = appTitle || (defaultSettings.title as string);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -6,11 +6,20 @@ import i18n from '@/lang/index';
|
||||
* @param title 路由名称
|
||||
* @returns {string}
|
||||
*/
|
||||
export const translateRouteTitle = (title: string): string => {
|
||||
const hasKey = i18n.global.te('route.' + title);
|
||||
if (hasKey) {
|
||||
const translatedTitle = i18n.global.t('route.' + title);
|
||||
return translatedTitle;
|
||||
export const translateRouteTitle = (title?: string): string => {
|
||||
const normalizedTitle = title?.trim();
|
||||
if (!normalizedTitle) {
|
||||
return '';
|
||||
}
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -19,7 +19,8 @@ export const isRelogin = { show: false };
|
||||
export const globalHeaders = () => {
|
||||
return {
|
||||
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 isEncrypt = config.headers?.isEncrypt === 'true';
|
||||
const isEncrypt = config.headers?.isEncrypt === true || config.headers?.isEncrypt === 'true';
|
||||
|
||||
if (getToken() && !isToken) {
|
||||
config.headers['Authorization'] = 'Bearer ' + getToken(); // 让每个请求携带自定义token 请根据实际情况自行修改
|
||||
|
||||
@@ -4,72 +4,35 @@
|
||||
<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 :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="设备名称" prop="deviceName">
|
||||
<el-input v-model="queryParams.deviceName" placeholder="请输入设备名称" clearable @keyup.enter="handleQuery" />
|
||||
<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 label="WiFi名称" prop="wifiName">-->
|
||||
<!-- <el-input v-model="queryParams.wifiName" placeholder="请输入WiFi名称" clearable @keyup.enter="handleQuery" />-->
|
||||
<!-- </el-form-item>-->
|
||||
<!-- <el-form-item label="WiFi密码" prop="wifiPassword">-->
|
||||
<!-- <el-input v-model="queryParams.wifiPassword" placeholder="请输入WiFi密码" clearable @keyup.enter="handleQuery" />-->
|
||||
<!-- </el-form-item>-->
|
||||
<!-- <el-form-item label="强光开启" prop="light">-->
|
||||
<!-- <el-select v-model="queryParams.light" placeholder="请选择" clearable @change="handleQuery">-->
|
||||
<!-- <el-option label="不开启" value="0" />-->
|
||||
<!-- <el-option label="开启" value="1" />-->
|
||||
<!-- </el-select>-->
|
||||
<!-- </el-form-item>-->
|
||||
<!-- <el-form-item label="告警播放" prop="alertAudio">-->
|
||||
<!-- <el-select v-model="queryParams.alertAudio" placeholder="请选择" clearable @change="handleQuery">-->
|
||||
<!-- <el-option label="不播放" value="0" />-->
|
||||
<!-- <el-option label="播放" value="1" />-->
|
||||
<!-- </el-select>-->
|
||||
<!-- </el-form-item>-->
|
||||
<!-- <el-form-item label="识别模式" prop="patternPerception">-->
|
||||
<!-- <el-select v-model="queryParams.patternPerception" placeholder="请选择" clearable @change="handleQuery">-->
|
||||
<!-- <el-option label="蓝牙" value="0" />-->
|
||||
<!-- <el-option label="蓝牙+人脸" value="1" />-->
|
||||
<!-- </el-select>-->
|
||||
<!-- </el-form-item>-->
|
||||
<!-- <el-form-item label="告警内容" prop="audioContet">-->
|
||||
<!-- <el-input v-model="queryParams.audioContet" placeholder="请输入告警内容" clearable @keyup.enter="handleQuery" />-->
|
||||
<!-- </el-form-item>-->
|
||||
<!-- <el-form-item label="打击距离" prop="strikeRange">-->
|
||||
<!-- <el-input v-model="queryParams.strikeRange" placeholder="请输入打击距离" clearable @keyup.enter="handleQuery" />-->
|
||||
<!-- </el-form-item>-->
|
||||
<!-- <el-form-item label="打击间隔" prop="strikeInterval">-->
|
||||
<!-- <el-input v-model="queryParams.strikeInterval" placeholder="请输入打击间隔" clearable @keyup.enter="handleQuery" />-->
|
||||
<!-- </el-form-item>-->
|
||||
<!-- <el-form-item label="容器数量" prop="container">-->
|
||||
<!-- <el-input v-model="queryParams.container" placeholder="请输入容器数量" clearable @keyup.enter="handleQuery" />-->
|
||||
<!-- </el-form-item>-->
|
||||
<!-- <el-form-item label="打击位置" prop="strikePosition">-->
|
||||
<!-- <el-input v-model="queryParams.strikePosition" placeholder="请输入打击位置" clearable @keyup.enter="handleQuery" />-->
|
||||
<!-- </el-form-item>-->
|
||||
<!-- <el-form-item label="自动布放开始时间" prop="startTime">-->
|
||||
<!-- <el-date-picker clearable v-model="queryParams.startTime" type="date" value-format="YYYY-MM-DD" placeholder="请选择自动布放开始时间" />-->
|
||||
<!-- </el-form-item>-->
|
||||
<!-- <el-form-item label="自动布放结束时间" prop="endTime">-->
|
||||
<!-- <el-date-picker clearable v-model="queryParams.endTime" type="date" value-format="YYYY-MM-DD" placeholder="请选择自动布放结束时间" />-->
|
||||
<!-- </el-form-item>-->
|
||||
<!-- <el-form-item label="工作模式 0-自动 1-半自动" prop="workMode">-->
|
||||
<!-- <el-input v-model="queryParams.workMode" placeholder="请输入工作模式 0-自动 1-半自动" clearable @keyup.enter="handleQuery" />-->
|
||||
<!-- </el-form-item>-->
|
||||
<!-- <el-form-item label="设备型号" prop="deviceEm">-->
|
||||
<!-- <el-input v-model="queryParams.deviceEm" placeholder="请输入设备型号" clearable @keyup.enter="handleQuery" />-->
|
||||
<!-- </el-form-item>-->
|
||||
<!-- <el-form-item label="设备序列号" prop="deviceSn">-->
|
||||
<!-- <el-input v-model="queryParams.deviceSn" placeholder="请输入设备序列号" clearable @keyup.enter="handleQuery" />-->
|
||||
<!-- </el-form-item>-->
|
||||
<!-- <el-form-item label="固件版本" prop="fwVer">-->
|
||||
<!-- <el-input v-model="queryParams.fwVer" placeholder="请输入固件版本" clearable @keyup.enter="handleQuery" />-->
|
||||
<!-- </el-form-item>-->
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
|
||||
<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>
|
||||
@@ -79,97 +42,93 @@
|
||||
<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']">新增</el-button>-->
|
||||
<!-- </el-col>-->
|
||||
<!-- <el-col :span="1.5">-->
|
||||
<!-- <el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['app:device:edit']">修改</el-button>-->
|
||||
<!-- </el-col>-->
|
||||
<el-col :span="1.5">
|
||||
<el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['app:device:remove']"
|
||||
>删除</el-button
|
||||
>
|
||||
<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="warning" plain icon="Download" @click="handleExport" v-hasPermi="['app:device:export']">导出</el-button>
|
||||
<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="55" align="center" />
|
||||
<el-table-column label="" align="center" prop="id" 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" prop="status" width="100">
|
||||
<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)">
|
||||
{{ statusLabel(scope.row.status) }}
|
||||
<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="WiFi名称" align="center" prop="wifiName" />
|
||||
<el-table-column label="WiFi密码" align="center" prop="wifiPassword" />
|
||||
<el-table-column label="强光开启" align="center" prop="light" width="100">
|
||||
<el-table-column :label="$t('device.light')" align="center" prop="light" width="70">
|
||||
<template #default="scope">
|
||||
<el-tag :type="lightTagType(scope.row.light)">
|
||||
<el-tag :type="lightTagType(scope.row.light)" size="small">
|
||||
{{ lightLabel(scope.row.light) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="告警播放" align="center" prop="alertAudio" width="100">
|
||||
<el-table-column :label="$t('device.alert')" align="center" prop="alertAudio" width="70">
|
||||
<template #default="scope">
|
||||
<el-tag :type="alertAudioTagType(scope.row.alertAudio)">
|
||||
<el-tag :type="alertAudioTagType(scope.row.alertAudio)" size="small">
|
||||
{{ alertAudioLabel(scope.row.alertAudio) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="识别模式" align="center" prop="patternPerception" width="120">
|
||||
<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'">
|
||||
<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="告警内容" align="center" prop="audioContet" min-width="120">
|
||||
<el-table-column :label="$t('device.alertContent')" align="center" prop="audioContet" min-width="140" show-overflow-tooltip>
|
||||
<template #default="scope">
|
||||
<el-tooltip v-if="scope.row.audioContet" :content="scope.row.audioContet" placement="top" :show-after="200">
|
||||
<span class="text-ellipsis">{{ scope.row.audioContet }}</span>
|
||||
</el-tooltip>
|
||||
<span v-if="scope.row.audioContet">{{ scope.row.audioContet }}</span>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="打击距离" align="center" prop="strikeRange" />
|
||||
<el-table-column label="打击间隔" align="center" prop="strikeInterval" />
|
||||
<el-table-column label="容器数量" align="center" prop="container" />
|
||||
<el-table-column label="打击位置" align="center" prop="strikePosition" />
|
||||
<el-table-column label="自动布放开始时间" align="center" prop="startTime" width="180">
|
||||
<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">
|
||||
<span>{{ parseTime(scope.row.startTime, '{y}-{m}-{d}') }}</span>
|
||||
<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="自动布放结束时间" align="center" prop="endTime" width="180">
|
||||
<el-table-column :label="$t('common.operation')" align="center" width="100" fixed="right">
|
||||
<template #default="scope">
|
||||
<span>{{ parseTime(scope.row.endTime, '{y}-{m}-{d}') }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="工作模式" align="center" prop="workMode" width="100">
|
||||
<template #default="scope">
|
||||
<el-tag :type="workModeTagType(scope.row.workMode)">
|
||||
{{ workModeLabel(scope.row.workMode) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="设备型号" align="center" prop="deviceEm" />
|
||||
<el-table-column label="设备序列号" align="center" prop="deviceSn" />
|
||||
<el-table-column label="固件版本" align="center" prop="fwVer" />
|
||||
<el-table-column label="操作" align="center" fixed="right" class-name="small-padding fixed-width">
|
||||
<template #default="scope">
|
||||
<!-- <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="删除" placement="top">
|
||||
<!-- <el-tooltip content="修改" placement="top">-->
|
||||
<!-- <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['app:device:edit']"></el-button>-->
|
||||
<!-- </el-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>
|
||||
@@ -178,137 +137,230 @@
|
||||
|
||||
<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="deviceFormRef" :model="form" :rules="rules" label-width="80px">
|
||||
<el-form-item label="设备编号" prop="deviceNo">
|
||||
<el-input v-model="form.deviceNo" placeholder="请输入设备编号" />
|
||||
<!-- 添加 / 修改设备 -->
|
||||
<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-form-item label="设备名称" prop="deviceName">
|
||||
<el-input v-model="form.deviceName" placeholder="请输入设备名称" />
|
||||
</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-form-item label="WiFi名称" prop="wifiName">
|
||||
<el-input v-model="form.wifiName" placeholder="请输入WiFi名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="WiFi密码" prop="wifiPassword">
|
||||
<el-input v-model="form.wifiPassword" placeholder="请输入WiFi密码" />
|
||||
</el-form-item>
|
||||
<el-form-item label="强光开启" prop="light">
|
||||
<el-switch v-model="form.light" active-value="1" inactive-value="0" />
|
||||
</el-form-item>
|
||||
<el-form-item label="告警播放" prop="alertAudio">
|
||||
<el-switch v-model="form.alertAudio" active-value="1" inactive-value="0" />
|
||||
</el-form-item>
|
||||
<el-form-item label="识别模式" prop="patternPerception">
|
||||
<el-select v-model="form.patternPerception" placeholder="请选择识别模式">
|
||||
<el-option label="蓝牙" value="0" />
|
||||
<el-option label="蓝牙+人脸" value="1" />
|
||||
</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-form-item label="告警内容" prop="audioContet">
|
||||
<el-input v-model="form.audioContet" placeholder="请输入告警内容" />
|
||||
</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-form-item label="打击距离" prop="strikeRange">
|
||||
<el-input v-model="form.strikeRange" placeholder="请输入打击距离" />
|
||||
</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-form-item label="打击间隔" prop="strikeInterval">
|
||||
<el-input v-model="form.strikeInterval" placeholder="请输入打击间隔" />
|
||||
</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-form-item label="容器数量" prop="container">
|
||||
<el-input v-model="form.container" placeholder="请输入容器数量" />
|
||||
</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-form-item label="打击位置" prop="strikePosition">
|
||||
<el-input v-model="form.strikePosition" placeholder="请输入打击位置" />
|
||||
</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-form-item label="自动布放开始时间" prop="startTime">
|
||||
<el-date-picker clearable v-model="form.startTime" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" placeholder="请选择自动布放开始时间">
|
||||
</el-date-picker>
|
||||
</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-form-item label="自动布放结束时间" prop="endTime">
|
||||
<el-date-picker clearable v-model="form.endTime" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" placeholder="请选择自动布放结束时间">
|
||||
</el-date-picker>
|
||||
</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-form-item label="工作模式 0-自动 1-半自动" prop="workMode">
|
||||
<el-input v-model="form.workMode" placeholder="请输入工作模式 0-自动 1-半自动" />
|
||||
</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-form-item label="设备型号" prop="deviceEm">
|
||||
<el-input v-model="form.deviceEm" placeholder="请输入设备型号" />
|
||||
</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-form-item label="设备序列号" prop="deviceSn">
|
||||
<el-input v-model="form.deviceSn" placeholder="请输入设备序列号" />
|
||||
</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-form-item label="固件版本" prop="fwVer">
|
||||
<el-input v-model="form.fwVer" placeholder="请输入固件版本" />
|
||||
</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>
|
||||
<div class="dialog-footer">
|
||||
<el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
|
||||
<el-button @click="cancel">取 消</el-button>
|
||||
<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): string => {
|
||||
const map: Record<string, string> = { '0': 'info', '1': 'success', '2': 'danger', '3': 'warning' };
|
||||
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): string => {
|
||||
const map: Record<string, string> = { '0': '离线', '1': '在线', '2': '故障', '3': '到期' };
|
||||
/** 优先显示后端按请求语言返回的状态标签 */
|
||||
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': '蓝牙', '1': '蓝牙+人脸' };
|
||||
const map: Record<string, string> = { '0': t('device.perception.bluetooth'), '1': t('device.perception.bluetoothFace') };
|
||||
return map[mode] || mode || '-';
|
||||
};
|
||||
|
||||
/** 强光开启标签类型映射 */
|
||||
const lightTagType = (light: string): string => {
|
||||
const map: Record<string, string> = { '0': 'info', '1': 'success' };
|
||||
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': '不开启', '1': '开启' };
|
||||
const map: Record<string, string> = { '0': t('device.lightState.off'), '1': t('device.lightState.on') };
|
||||
return map[light] || light || '-';
|
||||
};
|
||||
|
||||
/** 告警播放标签类型映射 */
|
||||
const alertAudioTagType = (alertAudio: string): string => {
|
||||
const map: Record<string, string> = { '0': 'info', '1': 'success' };
|
||||
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': '不播放', '1': '播放' };
|
||||
const map: Record<string, string> = { '0': t('device.audioState.off'), '1': t('device.audioState.on') };
|
||||
return map[alertAudio] || alertAudio || '-';
|
||||
};
|
||||
|
||||
/** 工作模式标签类型映射 */
|
||||
const workModeTagType = (mode: string): string => {
|
||||
const map: Record<string, string> = { '0': 'success', '1': 'primary' };
|
||||
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': '自动', '1': '半自动' };
|
||||
const map: Record<string, string> = { '0': t('device.work.automatic'), '1': t('device.work.semiAutomatic') };
|
||||
return map[mode] || mode || '-';
|
||||
};
|
||||
|
||||
@@ -320,6 +372,8 @@ 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>();
|
||||
@@ -329,8 +383,149 @@ const dialog = reactive<DialogOption>({
|
||||
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,
|
||||
@@ -377,12 +572,14 @@ const data = reactive<PageData<DeviceForm, DeviceQuery>>({
|
||||
patternPerception: undefined,
|
||||
params: {}
|
||||
},
|
||||
rules: {
|
||||
id: [{ required: true, message: '不能为空', trigger: 'blur' }]
|
||||
}
|
||||
rules: {}
|
||||
});
|
||||
|
||||
const { queryParams, form, rules } = toRefs(data);
|
||||
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' }]
|
||||
}));
|
||||
|
||||
/** 查询设备信息
|
||||
列表 */
|
||||
@@ -391,7 +588,9 @@ const getList = async () => {
|
||||
const res = await listDevice(queryParams.value);
|
||||
deviceList.value = res.rows;
|
||||
total.value = res.total;
|
||||
selectedDevices.value = [];
|
||||
loading.value = false;
|
||||
generateQrCodes(res.rows);
|
||||
};
|
||||
|
||||
/** 取消按钮 */
|
||||
@@ -418,8 +617,26 @@ const resetQuery = () => {
|
||||
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;
|
||||
@@ -428,18 +645,21 @@ const handleSelectionChange = (selection: DeviceVO[]) => {
|
||||
/** 新增按钮操作 */
|
||||
const handleAdd = () => {
|
||||
reset();
|
||||
loadAppUsers();
|
||||
dialog.visible = true;
|
||||
dialog.title = '添加设备信息';
|
||||
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 = '修改设备信息';
|
||||
dialog.title = t('device.editTitle');
|
||||
};
|
||||
|
||||
/** 提交按钮 */
|
||||
@@ -452,7 +672,7 @@ const submitForm = () => {
|
||||
} else {
|
||||
await addDevice(form.value).finally(() => (buttonLoading.value = false));
|
||||
}
|
||||
proxy?.$modal.msgSuccess('操作成功');
|
||||
proxy?.$modal.msgSuccess(t('common.operationSuccess'));
|
||||
dialog.visible = false;
|
||||
await getList();
|
||||
}
|
||||
@@ -462,9 +682,9 @@ const submitForm = () => {
|
||||
/** 删除按钮操作 */
|
||||
const handleDelete = async (row?: DeviceVO) => {
|
||||
const _ids = row?.id || ids.value;
|
||||
await proxy?.$modal.confirm('是否确认删除设备信息编号为"' + _ids + '"的数据项?').finally(() => (loading.value = false));
|
||||
await proxy?.$modal.confirm(t('device.deleteConfirm', { ids: String(_ids) })).finally(() => (loading.value = false));
|
||||
await delDevice(_ids);
|
||||
proxy?.$modal.msgSuccess('删除成功');
|
||||
proxy?.$modal.msgSuccess(t('common.deleteSuccess'));
|
||||
await getList();
|
||||
};
|
||||
|
||||
@@ -484,14 +704,4 @@ onMounted(() => {
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.text-ellipsis {
|
||||
display: inline-block;
|
||||
max-width: 120px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
vertical-align: bottom;
|
||||
cursor: default;
|
||||
}
|
||||
</style>
|
||||
<style scoped></style>
|
||||
|
||||
@@ -4,20 +4,23 @@
|
||||
<div v-show="showSearch" class="mb-[10px]">
|
||||
<el-card shadow="hover">
|
||||
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
|
||||
<el-form-item label="告警类型" prop="category">
|
||||
<el-select v-model="queryParams.category" placeholder="请选择告警类型" clearable @change="handleQuery">
|
||||
<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="告警说明" prop="categoryName">-->
|
||||
<!-- <el-input v-model="queryParams.categoryName" placeholder="请输入告警说明" clearable @keyup.enter="handleQuery" />-->
|
||||
<!-- </el-form-item>-->
|
||||
<!-- <el-form-item label="视频地址" prop="video">
|
||||
<el-input v-model="queryParams.video" placeholder="请输入视频地址" clearable @keyup.enter="handleQuery" />
|
||||
</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">搜索</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
|
||||
<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>
|
||||
@@ -27,58 +30,73 @@
|
||||
<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:message:add']">新增</el-button>-->
|
||||
<!-- </el-col>-->
|
||||
<!-- <el-col :span="1.5">-->
|
||||
<!-- <el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['app:message:edit']">修改</el-button>-->
|
||||
<!-- </el-col>-->
|
||||
<el-col :span="1.5">
|
||||
<el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['app:message:remove']"
|
||||
>删除</el-button
|
||||
>
|
||||
<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']">导出</el-button>
|
||||
<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="55" align="center" />
|
||||
<el-table-column label="ID" align="center" prop="id" v-if="true" />
|
||||
<el-table-column label="设备ID" align="center" prop="deviceId" v-if="true" />
|
||||
<el-table-column label="设备名" align="center" prop="deviceName" v-if="true" />
|
||||
<el-table-column label="告警类型" align="center" prop="category" width="120">
|
||||
<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)">
|
||||
{{ categoryLabel(scope.row.category) }}
|
||||
<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="告警说明" align="center" prop="categoryName" />
|
||||
<el-table-column label="图片地址" align="center" prop="imageUrl" width="100">
|
||||
<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">
|
||||
<image-preview :src="scope.row.imageUrl" :width="50" :height="50" />
|
||||
<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="视频地址" align="center" prop="video" min-width="120">
|
||||
<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" @click="handlePlayVideo(scope.row.video)">
|
||||
<el-icon><VideoPlay /></el-icon> 播放
|
||||
<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="操作" align="center" fixed="right" class-name="small-padding fixed-width">
|
||||
<el-table-column :label="$t('common.operation')" align="center" width="90" fixed="right">
|
||||
<template #default="scope">
|
||||
<!-- <el-tooltip content="修改" placement="top">-->
|
||||
<!-- <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['app:message:edit']"></el-button>-->
|
||||
<!-- </el-tooltip>-->
|
||||
<el-tooltip content="删除" placement="top">
|
||||
<el-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>
|
||||
@@ -87,34 +105,49 @@
|
||||
|
||||
<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="messageFormRef" :model="form" :rules="rules" label-width="80px">
|
||||
<el-form-item label="告警类型" prop="category">
|
||||
<el-select v-model="form.category" placeholder="请选择告警类型">
|
||||
<!-- 添加 / 修改告警消息 -->
|
||||
<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-form-item label="告警说明" prop="categoryName">
|
||||
<el-input v-model="form.categoryName" placeholder="请输入告警说明" />
|
||||
</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-form-item label="图片地址" prop="image">
|
||||
</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-form-item label="视频地址" prop="video">
|
||||
<el-input v-model="form.video" placeholder="请输入视频地址" />
|
||||
</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>
|
||||
<div class="dialog-footer">
|
||||
<el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
|
||||
<el-button @click="cancel">取 消</el-button>
|
||||
</div>
|
||||
<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 title="视频播放" v-model="videoDialog.visible" width="720px" append-to-body :close-on-click-modal="false" @close="destroyPlayer">
|
||||
<!-- 视频播放 -->
|
||||
<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>
|
||||
@@ -122,13 +155,16 @@
|
||||
|
||||
<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 } from '@element-plus/icons-vue';
|
||||
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);
|
||||
@@ -141,7 +177,12 @@ const videoDialog = reactive<{ visible: boolean; url: string }>({
|
||||
url: ''
|
||||
});
|
||||
|
||||
/** 点击播放按钮 */
|
||||
/** 新窗口打开视频 */
|
||||
const openVideoUrl = (url: string) => {
|
||||
window.open(url, '_blank', 'noopener,noreferrer');
|
||||
};
|
||||
|
||||
/** 弹窗播放视频 */
|
||||
const handlePlayVideo = (url: string) => {
|
||||
videoDialog.url = url;
|
||||
videoDialog.visible = true;
|
||||
@@ -162,6 +203,7 @@ const initPlayer = (url: string) => {
|
||||
playsinline: true,
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
lang: locale.value === LanguageEnum.en_US ? 'en' : 'zh-cn',
|
||||
flv: {
|
||||
mediaDataSource: {
|
||||
isLive: true,
|
||||
@@ -181,21 +223,24 @@ const destroyPlayer = () => {
|
||||
};
|
||||
|
||||
/** 告警类型选项 */
|
||||
const categoryOptions = [
|
||||
{ value: 1, label: '有人入侵' },
|
||||
{ value: 2, label: '设备故障' },
|
||||
{ value: 3, label: '到期提醒' }
|
||||
];
|
||||
const categoryOptions = computed(() => [
|
||||
{ value: 1, label: t('message.categoryType.intrusion') },
|
||||
{ value: 2, label: t('message.categoryType.deviceFault') },
|
||||
{ value: 3, label: t('message.categoryType.expiration') }
|
||||
]);
|
||||
|
||||
/** 根据 category 值返回中文标签 */
|
||||
const categoryLabel = (category: number): string => {
|
||||
return categoryOptions.find((item) => item.value === category)?.label || category?.toString() || '-';
|
||||
/** 优先显示后端按请求语言返回的告警类型标签 */
|
||||
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 => {
|
||||
const map: Record<number, string> = { 1: 'danger', 2: 'warning', 3: 'info' };
|
||||
return map[category] || 'info';
|
||||
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[]>([]);
|
||||
@@ -233,12 +278,13 @@ const data = reactive<PageData<MessageForm, MessageQuery>>({
|
||||
video: undefined,
|
||||
params: {}
|
||||
},
|
||||
rules: {
|
||||
id: [{ required: true, message: '不能为空', trigger: 'blur' }]
|
||||
}
|
||||
rules: {}
|
||||
});
|
||||
|
||||
const { queryParams, form, rules } = toRefs(data);
|
||||
const { queryParams, form } = toRefs(data);
|
||||
const rules = computed(() => ({
|
||||
id: [{ required: true, message: t('common.required'), trigger: 'blur' }]
|
||||
}));
|
||||
|
||||
/** 查询告警消息列列表 */
|
||||
const getList = async () => {
|
||||
@@ -284,7 +330,7 @@ const handleSelectionChange = (selection: MessageVO[]) => {
|
||||
const handleAdd = () => {
|
||||
reset();
|
||||
dialog.visible = true;
|
||||
dialog.title = '添加告警消息列';
|
||||
dialog.title = t('message.addTitle');
|
||||
};
|
||||
|
||||
/** 修改按钮操作 */
|
||||
@@ -294,7 +340,7 @@ const handleUpdate = async (row?: MessageVO) => {
|
||||
const res = await getMessage(_id);
|
||||
Object.assign(form.value, res.data);
|
||||
dialog.visible = true;
|
||||
dialog.title = '修改告警消息列';
|
||||
dialog.title = t('message.editTitle');
|
||||
};
|
||||
|
||||
/** 提交按钮 */
|
||||
@@ -307,7 +353,7 @@ const submitForm = () => {
|
||||
} else {
|
||||
await addMessage(form.value).finally(() => (buttonLoading.value = false));
|
||||
}
|
||||
proxy?.$modal.msgSuccess('操作成功');
|
||||
proxy?.$modal.msgSuccess(t('common.operationSuccess'));
|
||||
dialog.visible = false;
|
||||
await getList();
|
||||
}
|
||||
@@ -317,9 +363,9 @@ const submitForm = () => {
|
||||
/** 删除按钮操作 */
|
||||
const handleDelete = async (row?: MessageVO) => {
|
||||
const _ids = row?.id || ids.value;
|
||||
await proxy?.$modal.confirm('是否确认删除告警消息列编号为"' + _ids + '"的数据项?').finally(() => (loading.value = false));
|
||||
await proxy?.$modal.confirm(t('message.deleteConfirm', { ids: String(_ids) })).finally(() => (loading.value = false));
|
||||
await delMessage(_ids);
|
||||
proxy?.$modal.msgSuccess('删除成功');
|
||||
proxy?.$modal.msgSuccess(t('common.deleteSuccess'));
|
||||
await getList();
|
||||
};
|
||||
|
||||
|
||||
@@ -193,10 +193,32 @@ import { UserVO, UserQuery, UserForm } from '@/api/app/user/types';
|
||||
|
||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||
|
||||
/** 将逗号分隔的人脸地址拆分为数组 */
|
||||
const faceUrls = (faceAddr: string): string[] => {
|
||||
if (!faceAddr) return [];
|
||||
return faceAddr.split(',').map(u => u.trim()).filter(Boolean);
|
||||
/** 解析后端存储的 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[]>([]);
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
<template>
|
||||
<div class="app-container home">
|
||||
<div class="welcome-wrapper">
|
||||
<h1 class="welcome-title">欢迎使用智能防御机管理系统</h1>
|
||||
<p class="welcome-desc">智能防御 · 安全守护</p>
|
||||
<h1 class="welcome-title">{{ $t('home.welcome') }}</h1>
|
||||
<p class="welcome-desc">{{ $t('home.slogan') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="Index" lang="ts">
|
||||
</script>
|
||||
<script setup name="Index" lang="ts"></script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.home {
|
||||
|
||||
@@ -1,5 +1,116 @@
|
||||
<template>
|
||||
<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">
|
||||
<div class="title-box">
|
||||
<h3 class="title">{{ title }}</h3>
|
||||
@@ -44,23 +155,23 @@
|
||||
</div>
|
||||
</el-form-item>
|
||||
<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-button circle :title="proxy.$t('login.social.wechat')" @click="doSocialLogin('wechat')">-->
|
||||
<!-- <svg-icon icon-class="wechat" />-->
|
||||
<!-- </el-button>-->
|
||||
<!-- <el-button circle :title="proxy.$t('login.social.maxkey')" @click="doSocialLogin('maxkey')">-->
|
||||
<!-- <svg-icon icon-class="maxkey" />-->
|
||||
<!-- </el-button>-->
|
||||
<!-- <el-button circle :title="proxy.$t('login.social.topiam')" @click="doSocialLogin('topiam')">-->
|
||||
<!-- <svg-icon icon-class="topiam" />-->
|
||||
<!-- </el-button>-->
|
||||
<!-- <el-button circle :title="proxy.$t('login.social.gitee')" @click="doSocialLogin('gitee')">-->
|
||||
<!-- <svg-icon icon-class="gitee" />-->
|
||||
<!-- </el-button>-->
|
||||
<!-- <el-button circle :title="proxy.$t('login.social.github')" @click="doSocialLogin('github')">-->
|
||||
<!-- <svg-icon icon-class="github" />-->
|
||||
<!-- </el-button>-->
|
||||
<!-- </el-form-item>-->
|
||||
<!-- <el-form-item style="float: right">-->
|
||||
<!-- <el-button circle :title="proxy.$t('login.social.wechat')" @click="doSocialLogin('wechat')">-->
|
||||
<!-- <svg-icon icon-class="wechat" />-->
|
||||
<!-- </el-button>-->
|
||||
<!-- <el-button circle :title="proxy.$t('login.social.maxkey')" @click="doSocialLogin('maxkey')">-->
|
||||
<!-- <svg-icon icon-class="maxkey" />-->
|
||||
<!-- </el-button>-->
|
||||
<!-- <el-button circle :title="proxy.$t('login.social.topiam')" @click="doSocialLogin('topiam')">-->
|
||||
<!-- <svg-icon icon-class="topiam" />-->
|
||||
<!-- </el-button>-->
|
||||
<!-- <el-button circle :title="proxy.$t('login.social.gitee')" @click="doSocialLogin('gitee')">-->
|
||||
<!-- <svg-icon icon-class="gitee" />-->
|
||||
<!-- </el-button>-->
|
||||
<!-- <el-button circle :title="proxy.$t('login.social.github')" @click="doSocialLogin('github')">-->
|
||||
<!-- <svg-icon icon-class="github" />-->
|
||||
<!-- </el-button>-->
|
||||
<!-- </el-form-item>-->
|
||||
<el-form-item style="width: 100%">
|
||||
<el-button :loading="loading" size="large" type="primary" style="width: 100%" @click.prevent="handleLogin">
|
||||
<span v-if="!loading">{{ proxy.$t('login.login') }}</span>
|
||||
@@ -71,9 +182,10 @@
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<!-- 底部 -->
|
||||
<!-- 底部(仅右侧表单区域内显示,避免跨背景看不清) -->
|
||||
<div class="el-login-footer">
|
||||
<!-- <span>Copyright © 2018-2026 疯狂的狮子Li All Rights Reserved.</span>-->
|
||||
<span>© 2026 驭鲲智能科技(湖北)有限公司 · 鄂ICP备2026000000号</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -89,10 +201,10 @@ import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||
|
||||
const title = import.meta.env.VITE_APP_TITLE;
|
||||
const userStore = useUserStore();
|
||||
const router = useRouter();
|
||||
const { t } = useI18n();
|
||||
const title = computed(() => t('app.title'));
|
||||
|
||||
const loginForm = ref<LoginData>({
|
||||
tenantId: '000000',
|
||||
@@ -105,12 +217,12 @@ const loginForm = ref<LoginData>({
|
||||
uuid: ''
|
||||
} as LoginData);
|
||||
|
||||
const loginRules: ElFormRules = {
|
||||
const loginRules = computed<ElFormRules>(() => ({
|
||||
tenantId: [{ required: true, trigger: 'blur', message: t('login.rule.tenantId.required') }],
|
||||
username: [{ required: true, trigger: 'blur', message: t('login.rule.username.required') }],
|
||||
password: [{ required: true, trigger: 'blur', message: t('login.rule.password.required') }],
|
||||
code: [{ required: true, trigger: 'change', message: t('login.rule.code.required') }]
|
||||
};
|
||||
}));
|
||||
|
||||
const codeUrl = ref('');
|
||||
const loading = ref(false);
|
||||
@@ -236,13 +348,315 @@ onMounted(() => {
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.login {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.login-main {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: 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 {
|
||||
@@ -342,11 +756,12 @@ onMounted(() => {
|
||||
.el-login-footer {
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
position: fixed;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
color: rgba(100, 116, 139, 0.85);
|
||||
font-family: Arial, serif;
|
||||
font-size: 12px;
|
||||
letter-spacing: 1px;
|
||||
@@ -358,6 +773,10 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
:global(html.dark) {
|
||||
.login {
|
||||
background: #0b1220;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
background: rgba(17, 24, 39, 0.9);
|
||||
border-color: rgba(148, 163, 184, 0.2);
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
</el-form>
|
||||
<!-- 底部 -->
|
||||
<div class="el-register-footer">
|
||||
<span>Copyright © 2018-2026 疯狂的狮子Li All Rights Reserved.</span>
|
||||
<span>Copyright © 2018-2026. All Rights Reserved.</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -80,10 +80,10 @@ import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||
|
||||
const title = import.meta.env.VITE_APP_TITLE;
|
||||
const router = useRouter();
|
||||
|
||||
const { t } = useI18n();
|
||||
const title = computed(() => t('app.title'));
|
||||
|
||||
const registerForm = ref<RegisterForm>({
|
||||
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') }],
|
||||
username: [
|
||||
{ required: true, trigger: 'blur', message: t('register.rule.username.required') },
|
||||
@@ -122,7 +122,7 @@ const registerRules: ElFormRules = {
|
||||
{ required: true, validator: equalToPassword, trigger: 'blur' }
|
||||
],
|
||||
code: [{ required: true, trigger: 'change', message: t('register.rule.code.required') }]
|
||||
};
|
||||
}));
|
||||
const codeUrl = ref('');
|
||||
const loading = ref(false);
|
||||
const captchaEnabled = ref(true);
|
||||
@@ -137,8 +137,7 @@ const handleRegister = () => {
|
||||
const [err] = await to(register(registerForm.value));
|
||||
if (!err) {
|
||||
const username = registerForm.value.username;
|
||||
await ElMessageBox.alert('<span style="color: red; ">' + t('register.registerSuccess', { username }) + '</font>', '系统提示', {
|
||||
app: undefined,
|
||||
await ElMessageBox.alert('<span style="color: red; ">' + t('register.registerSuccess', { username }) + '</span>', t('common.systemPrompt'), {
|
||||
dangerouslyUseHTMLString: true,
|
||||
type: 'success'
|
||||
});
|
||||
|
||||
@@ -24,9 +24,14 @@ export default defineConfig(({ mode, command }) => {
|
||||
open: true,
|
||||
proxy: {
|
||||
[env.VITE_APP_BASE_API]: {
|
||||
target: 'http://localhost:8080',
|
||||
target: 'http://localhost:8001',
|
||||
changeOrigin: true,
|
||||
ws: true,
|
||||
configure: (proxy) => {
|
||||
proxy.on('proxyReq', (proxyReq) => {
|
||||
proxyReq.removeHeader('origin');
|
||||
});
|
||||
},
|
||||
rewrite: (path) => path.replace(new RegExp('^' + env.VITE_APP_BASE_API), '')
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user