This commit is contained in:
zxr
2026-08-24 22:06:19 +08:00
71 changed files with 2970812 additions and 71 deletions

View File

@@ -8,16 +8,17 @@ VITE_APP_TITLE=标准管理系统
VITE_APP_DESCRIPTION="default standard template"
VITE_USE_MOCK=false
# API 基础URL
VITE_API_BASE_URL=https://ops-api.apinb.com
# 开发环境走 Vite 同源代理,避免依赖本机 80/443 nginx
# VITE_API_BASE_URL=
# 开发环境通过 Vite 同源代理访问 API避免浏览器跨域
VITE_API_BASE_URL=/api
VITE_API_PROXY_TARGET=https://ops-api.apinb.com
# 海康 WebSDK 开发代理(按实际摄像头/NVR 地址填写,不要提交账号密码)
VITE_HIKVISION_PROXY_TARGET=http://192.168.1.101:80
VITE_HIKVISION_WS_PROXY_TARGET=
# Logs 本地调试地址(仅 logs 模块使用)
VITE_LOGS_API_BASE_URL=http://127.0.0.1:12440
# 应用版本
VITE_APP_VERSION=1.0.0

File diff suppressed because it is too large Load Diff

View File

@@ -1,22 +1,51 @@
import { mergeConfig } from 'vite'
import type { IncomingMessage } from 'node:http'
import { loadEnv, mergeConfig, type ConfigEnv, type ProxyOptions } from 'vite'
// import eslint from 'vite-plugin-eslint'
import baseConfig from './vite.config.base'
const HIKVISION_PROXY_TIMEOUT = 8000
const proxyTarget = (port: number) => ({
target: `http://127.0.0.1:${port}`,
changeOrigin: true,
})
export default mergeConfig(
{
mode: 'development',
server: {
open: true,
host: '0.0.0.0',
fs: {
strict: true,
/** 读取海康 WebSDK 写入的代理目标 Cookie。 */
function getProxyCookie(request: IncomingMessage, name: string): string {
const cookie = request.headers.cookie || ''
const matched = cookie.match(new RegExp(`(?:^|;\\s*)${name}=([^;]+)`))
if (!matched) return ''
try {
return decodeURIComponent(matched[1])
} catch (_error) {
return ''
}
}
/** 从 SDK Cookie 解析 WebSocket 端口,并限制只能代理到已配置的录像机。 */
function resolveHikvisionWebSocketTarget(request: IncomingMessage, allowedHost: string, fallbackTarget: string): string {
const secureAddress = getProxyCookie(request, 'webVideoCtrlProxyWss')
const address = secureAddress || getProxyCookie(request, 'webVideoCtrlProxyWs')
if (!address) return fallbackTarget
try {
const target = new URL(`${secureAddress ? 'wss' : 'ws'}://${address}`)
return target.hostname.toLowerCase() === allowedHost.toLowerCase() ? target.toString() : fallbackTarget
} catch (_error) {
return fallbackTarget
}
}
/** 创建开发环境 Vite 配置。 */
export default ({ mode }: ConfigEnv) => {
const env = loadEnv(mode, process.cwd(), '')
const apiProxyTarget = env.VITE_API_PROXY_TARGET || 'https://ops-api.apinb.com'
const proxy: Record<string, ProxyOptions> = {
'/api': {
target: apiProxyTarget,
changeOrigin: true,
ws: true,
rewrite: (path) => path.replace(/^\/api/, ''),
},
proxy: {
'/rbac2': proxyTarget(10001),
'/Alert': proxyTarget(12427),
'/alert': proxyTarget(12427),
@@ -40,7 +69,55 @@ export default mergeConfig(
'/mgt': proxyTarget(12436),
'/Visual': proxyTarget(12438),
'/visual': proxyTarget(12438),
}
if (env.VITE_HIKVISION_PROXY_TARGET) {
const hikvisionHttpTarget = new URL(env.VITE_HIKVISION_PROXY_TARGET)
const hikvisionWebSocketTarget =
env.VITE_HIKVISION_WS_PROXY_TARGET || `${hikvisionHttpTarget.protocol === 'https:' ? 'wss' : 'ws'}://${hikvisionHttpTarget.host}`
proxy['/ISAPI'] = {
target: env.VITE_HIKVISION_PROXY_TARGET,
changeOrigin: true,
secure: false,
timeout: HIKVISION_PROXY_TIMEOUT,
proxyTimeout: HIKVISION_PROXY_TIMEOUT,
}
proxy['/SDK'] = {
target: env.VITE_HIKVISION_PROXY_TARGET,
changeOrigin: true,
secure: false,
timeout: HIKVISION_PROXY_TIMEOUT,
proxyTimeout: HIKVISION_PROXY_TIMEOUT,
}
proxy['/webSocketVideoCtrlProxy'] = {
target: hikvisionWebSocketTarget,
changeOrigin: true,
secure: false,
ws: true,
timeout: HIKVISION_PROXY_TIMEOUT,
proxyTimeout: HIKVISION_PROXY_TIMEOUT,
router: env.VITE_HIKVISION_WS_PROXY_TARGET
? undefined
: (request) => resolveHikvisionWebSocketTarget(request, hikvisionHttpTarget.hostname, hikvisionWebSocketTarget),
rewrite: (path) => path.replace(/^\/webSocketVideoCtrlProxy/, '/'),
}
}
return mergeConfig(
{
mode: 'development',
server: {
open: true,
host: '0.0.0.0',
headers: {
'Cross-Origin-Embedder-Policy': 'require-corp',
'Cross-Origin-Opener-Policy': 'same-origin',
'Cross-Origin-Resource-Policy': 'cross-origin',
},
fs: {
strict: true,
},
proxy,
},
plugins: [
// eslint({
@@ -51,4 +128,5 @@ export default mergeConfig(
],
},
baseConfig
)
)
}

15673
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -19,6 +19,7 @@
"dependencies": {
"@arco-design/web-vue": "^2.57.0",
"@tabler/icons-vue": "^3.40.0",
"@tweenjs/tween.js": "18.6.0",
"@vue-flow/background": "^1.3.2",
"@vue-flow/controls": "^1.1.3",
"@vue-flow/core": "^1.48.2",
@@ -33,6 +34,7 @@
"pinia": "^3.0.4",
"query-string": "^9.3.1",
"sortablejs": "^1.15.7",
"three": "0.115.0",
"uuid": "^13.0.0",
"vue": "^3.5.29",
"vue-echarts": "^8.0.1",
@@ -72,7 +74,6 @@
"rollup": "^4.59.0",
"rollup-plugin-visualizer": "^7.0.1",
"stylelint": "^17.4.0",
"stylelint-config-prettier": "^9.0.5",
"stylelint-config-rational-order": "^0.1.2",
"stylelint-config-recommended-vue": "^1.6.1",
"stylelint-config-standard": "^40.0.0",

30
pnpm-lock.yaml generated
View File

@@ -13,6 +13,9 @@ importers:
'@tabler/icons-vue':
specifier: ^3.40.0
version: 3.40.0(vue@3.5.29(typescript@5.9.3))
'@tweenjs/tween.js':
specifier: 18.6.0
version: 18.6.0
'@vue-flow/background':
specifier: ^1.3.2
version: 1.3.2(@vue-flow/core@1.48.2(vue@3.5.29(typescript@5.9.3)))(vue@3.5.29(typescript@5.9.3))
@@ -55,6 +58,9 @@ importers:
sortablejs:
specifier: ^1.15.7
version: 1.15.7
three:
specifier: 0.115.0
version: 0.115.0
uuid:
specifier: ^13.0.0
version: 13.0.0
@@ -167,9 +173,6 @@ importers:
stylelint:
specifier: ^17.4.0
version: 17.4.0(typescript@5.9.3)
stylelint-config-prettier:
specifier: ^9.0.5
version: 9.0.5(stylelint@17.4.0(typescript@5.9.3))
stylelint-config-rational-order:
specifier: ^0.1.2
version: 0.1.2
@@ -205,6 +208,9 @@ importers:
version: 3.2.5(typescript@5.9.3)
packages:
'@tweenjs/tween.js@18.6.0':
resolution: { integrity: sha512-z45HU0G0e/SenbvGdAlTpUR5Hur5zwZXQcqfI+f7EnVHdeb2oMI2rQghEePu7uXuvBC0nuKWG5YtZ1nWbuvqzQ== }
'@arco-design/color@0.4.0':
resolution: { integrity: sha512-s7p9MSwJgHeL8DwcATaXvWT3m2SigKpxx4JA1BGPHL4gfvaQsmQfrLBDpjOJFJuJ2jG2dMt3R3P8Pm9E65q18g== }
@@ -3968,13 +3974,6 @@ packages:
postcss-html: ^1.0.0
stylelint: '>=14.0.0'
stylelint-config-prettier@9.0.5:
resolution: { integrity: sha512-U44lELgLZhbAD/xy/vncZ2Pq8sh2TnpiPvo38Ifg9+zeioR+LAkHu0i6YORIOxFafZoVg0xqQwex6e6F25S5XA== }
engines: { node: '>= 12' }
hasBin: true
peerDependencies:
stylelint: '>= 11.x < 15'
stylelint-config-rational-order@0.1.2:
resolution: { integrity: sha512-Qo7ZQaihCwTqijfZg4sbdQQHtugOX/B1/fYh018EiDZHW+lkqH9uHOnsDwDPGZrYJuB6CoyI7MZh2ecw2dOkew== }
@@ -4110,6 +4109,9 @@ packages:
trough@1.0.5:
resolution: { integrity: sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA== }
three@0.115.0:
resolution: { integrity: sha512-mAV2Ky3RdcbdSbR9capI+tKLvRldWYxd4151PZTT/o7+U2jh9Is3a4KmnYwzyUAhB2ZA3pXSgCd2DOY4Tj5kow== }
ts-api-utils@2.4.0:
resolution: { integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA== }
engines: { node: '>=18.12' }
@@ -4502,6 +4504,8 @@ packages:
resolution: { integrity: sha512-41dFXEEXuJpNecuUQq6JlbybmnHaqqpGlbH1yxnA5V9MMP4SbohSVZsJIwz+zdjQXSSlR1Vc34EgH1zxyTDvhg== }
snapshots:
'@tweenjs/tween.js@18.6.0': {}
'@arco-design/color@0.4.0':
dependencies:
color: 3.2.1
@@ -8473,10 +8477,6 @@ snapshots:
postcss-html: 1.8.1
stylelint: 17.4.0(typescript@5.9.3)
stylelint-config-prettier@9.0.5(stylelint@17.4.0(typescript@5.9.3)):
dependencies:
stylelint: 17.4.0(typescript@5.9.3)
stylelint-config-rational-order@0.1.2:
dependencies:
stylelint: 9.10.1
@@ -8667,6 +8667,8 @@ snapshots:
text-table@0.2.0: {}
three@0.115.0: {}
tiny-emitter@2.1.0: {}
tinyexec@1.0.2: {}

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 469 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 276 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 285 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 612 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 766 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 603 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 840 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 451 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

View File

@@ -0,0 +1,962 @@
# 3ds Max Wavefront OBJ Exporter v0.97b - (c)2007 guruware
# <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ļ<EFBFBD>:04.08.2026 13:21:15
newmtl Mat3d66_10994759_12_33214
Ns 10.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 0.5880 0.5880 0.5880
Kd 0.3686 0.3686 0.3686
Ks 0.0000 0.0000 0.0000
Ke 0.0000 0.0000 0.0000
newmtl Mat3d66_10994759_11_76789
Ns 10.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 1.0000 1.0000 1.0000
Kd 1.0000 1.0000 1.0000
Ks 0.0000 0.0000 0.0000
Ke 0.0000 0.0000 0.0000
map_Ka maps\Map__107_Noise.tga
map_Kd maps\Map__107_Noise.tga
map_bump -bm 0.2000 maps\Map__106_Noise.tga
newmtl Mat3d66_10994759_30_54055
Ns 10.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 0.5880 0.5880 0.5880
Kd 0.6667 0.0000 0.0000
Ks 0.0000 0.0000 0.0000
Ke 0.0000 0.0000 0.0000
map_Ka maps\3d66_17938251_color_VR-<2D><>ɫ.tga
map_Kd maps\3d66_17938251_color_VR-<2D><>ɫ.tga
map_bump -bm 0.0500 maps\3d66_17938251_color_VR-<2D><>ɫ.tga
newmtl Mat3d66_11807706_26_30152
Ns 10.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 0.5880 0.5880 0.5880
Kd 0.0196 0.0196 0.0196
Ks 0.0000 0.0000 0.0000
Ke 0.0000 0.0000 0.0000
map_bump maps\3d66Model-11807706-files-51.png
bump maps\3d66Model-11807706-files-51.png
newmtl Mat3d66_10994759_29_37910
Ns 10.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 0.5880 0.5880 0.5880
Kd 0.6078 0.0000 0.0000
Ks 0.0000 0.0000 0.0000
Ke 0.0000 0.0000 0.0000
newmtl Mat3d66_10994759_22_2dad4012
Ns 10.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 0.5880 0.5880 0.5880
Kd 0.1373 0.1373 0.1373
Ks 0.0000 0.0000 0.0000
Ke 0.0000 0.0000 0.0000
newmtl Mat3d66_10994759_34_15870
Ns 10.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 0.5880 0.5880 0.5880
Kd 0.6392 0.6392 0.6392
Ks 0.0000 0.0000 0.0000
Ke 0.0000 0.0000 0.0000
map_Ka maps\3d66Model-10994759-files-1.jpg
map_Kd maps\3d66Model-10994759-files-1.jpg
newmtl Mat3d66_10994759_22_24012
Ns 10.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 0.5880 0.5880 0.5880
Kd 0.0039 0.0039 0.0039
Ks 0.0000 0.0000 0.0000
Ke 0.0000 0.0000 0.0000
newmtl Mat3d66_10994759_37_59834
Ns 10.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 0.5880 0.5880 0.5880
Kd 0.0000 0.0000 0.0000
Ks 0.0000 0.0000 0.0000
Ke 0.0000 0.0000 0.0000
newmtl wire_198225087
Ns 32
d 1
Tr 0
Tf 1 1 1
illum 2
Ka 0.7765 0.8824 0.3412
Kd 0.7765 0.8824 0.3412
Ks 0.3500 0.3500 0.3500
newmtl Mat3d66_10994759_35_21635
Ns 10.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 0.5880 0.5880 0.5880
Kd 0.0588 0.0588 0.0588
Ks 0.0000 0.0000 0.0000
Ke 0.0000 0.0000 0.0000
newmtl wire_115115115
Ns 32
d 1
Tr 0
Tf 1 1 1
illum 2
Ka 0.4510 0.4510 0.4510
Kd 0.4510 0.4510 0.4510
Ks 0.3500 0.3500 0.3500
newmtl Mat3d66_10994759_25_25429
Ns 10.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 0.5880 0.5880 0.5880
Kd 0.8549 0.9098 0.9412
Ks 0.0000 0.0000 0.0000
Ke 0.0000 0.0000 0.0000
map_Ka maps\3d66Model-10994759-files-13.jpg
map_Kd maps\3d66Model-10994759-files-13.jpg
map_bump maps\3d66Model-10994759-files-13.jpg
bump maps\3d66Model-10994759-files-13.jpg
newmtl wire_000000000
Ns 32
d 1
Tr 0
Tf 1 1 1
illum 2
Ka 0.0000 0.0000 0.0000
Kd 0.0000 0.0000 0.0000
Ks 0.3500 0.3500 0.3500
newmtl 3d66_VRayMtl_23563919_021
Ns 10.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 0.5880 0.5880 0.5880
Kd 0.7529 0.7647 0.7294
Ks 0.0000 0.0000 0.0000
Ke 0.0000 0.0000 0.0000
newmtl 3d66_VRayMtl_23563919_022
Ns 10.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 0.5880 0.5880 0.5880
Kd 0.5961 0.6039 0.5765
Ks 0.0000 0.0000 0.0000
Ke 0.0000 0.0000 0.0000
newmtl 3d66_VRayMtl_23563919_018
Ns 10.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 0.5880 0.5880 0.5880
Kd 0.6314 0.0392 0.0392
Ks 0.0000 0.0000 0.0000
Ke 0.0000 0.0000 0.0000
newmtl 3d66_VRayMtl_23563919_019
Ns 10.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 0.5880 0.5880 0.5880
Kd 0.0000 0.0000 0.0000
Ks 0.0000 0.0000 0.0000
Ke 0.0000 0.0000 0.0000
newmtl 3d66_VRayMtl_23563919_020
Ns 10.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 0.5880 0.5880 0.5880
Kd 0.0275 0.0275 0.0275
Ks 0.0000 0.0000 0.0000
Ke 0.0000 0.0000 0.0000
newmtl <20><>ҵ<EFBFBD><D2B5><EFBFBD>Ұ<EFBFBD>ɫ<EFBFBD>߹<EFBFBD><DFB9><EFBFBD><E2BBAC>ƺ<EFBFBD><C6BA>_Ϳ<5F><CDBF>_VR
Ns 10.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 0.5880 0.5880 0.5880
Kd 0.3922 0.3922 0.3922
Ks 0.0000 0.0000 0.0000
Ke 0.0000 0.0000 0.0000
map_Ka maps\3d66Mat-14871359-maps-3.jpg
map_Kd maps\3d66Mat-14871359-maps-3.jpg
map_bump -bm 0.0150 maps\<5C><>ͼ__123_Noise.tga
newmtl <20>ִ<EFBFBD><D6B4><EFBFBD>ɫ<EFBFBD>ƹ<EFBFBD><C6B9><EFBFBD><E2BBAC><EFBFBD><EFBFBD>_<EFBFBD><5F>_VR
Ns 10.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 0.5880 0.5880 0.5880
Kd 0.0196 0.0196 0.0196
Ks 0.0000 0.0000 0.0000
Ke 0.0000 0.0000 0.0000
newmtl Mat3d66_11807706_4_39763
Ns 10.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 0.5880 0.5880 0.5880
Kd 0.0196 0.0196 0.0196
Ks 0.0000 0.0000 0.0000
Ke 0.0000 0.0000 0.0000
newmtl Mat3d66_11807706_5_89594
Ns 41.0000
Ni 1.5000
d 0.8000
Tr 0.2000
Tf 0.8000 0.8000 0.8000
illum 2
Ka 0.9725 0.0000 0.0000
Kd 0.9725 0.0000 0.0000
Ks 0.3960 0.3960 0.3960
Ke 0.0000 0.0000 0.0000
newmtl wire_225198087
Ns 32
d 1
Tr 0
Tf 1 1 1
illum 2
Ka 0.8824 0.7765 0.3412
Kd 0.8824 0.7765 0.3412
Ks 0.3500 0.3500 0.3500
newmtl Mat3d66_11807706_7_95731
Ns 10.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 0.5880 0.5880 0.5880
Kd 0.5333 0.5294 0.5255
Ks 0.0000 0.0000 0.0000
Ke 0.0000 0.0000 0.0000
newmtl Mat3d66_11807706_8_21117
Ns 76.0000
Ni 1.5000
d 0.4000
Tr 0.6000
Tf 0.4000 0.4000 0.4000
illum 2
Ka 0.1608 0.1569 0.1843
Kd 0.1608 0.1569 0.1843
Ks 0.7920 0.7920 0.7920
Ke 0.0000 0.0000 0.0000
map_refl maps\Map__614327418_VR-<2D><>ͼ.tga
newmtl Mat3d66_11807706_9_83349
Ns 10.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 0.5880 0.5880 0.5880
Kd 0.0863 0.0745 0.0667
Ks 0.0000 0.0000 0.0000
Ke 0.0000 0.0000 0.0000
newmtl Mat3d66_11807706_10_61673
Ns 76.0000
Ni 1.5000
d 0.0500
Tr 0.9500
Tf 0.0500 0.0500 0.0500
illum 2
Ka 0.1608 0.1569 0.1843
Kd 0.1608 0.1569 0.1843
Ks 0.7920 0.7920 0.7920
Ke 0.0000 0.0000 0.0000
map_refl maps\Map__614327418_VR-<2D><>ͼ.tga
newmtl Mat3d66_11807706_11_67368
Ns 10.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 0.5880 0.5880 0.5880
Kd 0.0863 0.0745 0.0667
Ks 0.0000 0.0000 0.0000
Ke 0.0000 0.0000 0.0000
map_Ka maps\3d66Model-11807706-files-3.jpg
map_Kd maps\3d66Model-11807706-files-3.jpg
newmtl 22___Default
Ns 10.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 0.5880 0.5880 0.5880
Kd 0.0039 0.0039 0.0039
Ks 0.0000 0.0000 0.0000
Ke 0.0000 0.0000 0.0000
newmtl Mat3d66_11807706_29_27585
Ns 38.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 0.5882 0.5882 0.5882
Kd 0.5882 0.5882 0.5882
Ks 0.1800 0.1800 0.1800
Ke 0.0706 0.0706 0.0706
map_Ka maps\3d66Model-11807706-files-60.jpg
map_Kd maps\3d66Model-11807706-files-60.jpg
map_bump maps\3d66Model-11807706-files-60.jpg
bump maps\3d66Model-11807706-files-60.jpg
newmtl Mat3d66_11807706_120_47020
Ns 10.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 0.5880 0.5880 0.5880
Kd 0.5000 0.5000 0.5000
Ks 0.0000 0.0000 0.0000
Ke 0.0000 0.0000 0.0000
map_Ka maps\Map__1_<31><5F>ɫУ<C9AB><D0A3>.tga
map_Kd maps\Map__1_<31><5F>ɫУ<C9AB><D0A3>.tga
map_bump maps\Map__1_<31><5F>ɫУ<C9AB><D0A3>.tga
newmtl wire_010140195
Ns 32
d 1
Tr 0
Tf 1 1 1
illum 2
Ka 0.0392 0.5490 0.7647
Kd 0.0392 0.5490 0.7647
Ks 0.3500 0.3500 0.3500
newmtl Mat3d66_11807706_28_26251
Ns 10.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 0.5880 0.5880 0.5880
Kd 0.8941 0.8941 0.8941
Ks 0.0000 0.0000 0.0000
Ke 0.0000 0.0000 0.0000
newmtl Mat3d66_15185da96_31_5088
Ns 10.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 0.5880 0.5880 0.5880
Kd 0.0275 0.0275 0.0235
Ks 0.0000 0.0000 0.0000
Ke 0.0000 0.0000 0.0000
newmtl Mat3d66_10994759_49_69999
Ns 10.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 0.5880 0.5880 0.5880
Kd 0.1373 0.1373 0.1373
Ks 0.0000 0.0000 0.0000
Ke 0.0000 0.0000 0.0000
newmtl Mat3d66_10994759_31_26985
Ns 10.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 0.5880 0.5880 0.5880
Kd 0.0392 0.0392 0.0392
Ks 0.0000 0.0000 0.0000
Ke 0.0000 0.0000 0.0000
map_bump maps\3d66Model-10994759-files-15.JPG
bump maps\3d66Model-10994759-files-15.JPG
newmtl 24___Default
Ns 10.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 0.5880 0.5880 0.5880
Kd 0.5000 0.5000 0.5000
Ks 0.0000 0.0000 0.0000
Ke 0.0000 0.0000 0.0000
map_Ka maps\t7.jpg
map_Kd maps\t7.jpg
newmtl 3d66_VRayMtl_20092954_028
Ns 10.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 0.5880 0.5880 0.5880
Kd 0.9216 0.9216 0.9216
Ks 0.0000 0.0000 0.0000
Ke 0.0000 0.0000 0.0000
newmtl 3d66_VRayMtl_20092954_034
Ns 10.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 0.5880 0.5880 0.5880
Kd 0.1176 0.1176 0.1176
Ks 0.0000 0.0000 0.0000
Ke 0.0000 0.0000 0.0000
newmtl Mat3d66_7566050_1_94391
Ns 10.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 0.5880 0.5880 0.5880
Kd 0.9020 0.9020 0.9020
Ks 0.0000 0.0000 0.0000
Ke 0.0000 0.0000 0.0000
newmtl Mat3d66_1518596_31_5088
Ns 10.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 0.5880 0.5880 0.5880
Kd 0.6471 0.6235 0.6000
Ks 0.0000 0.0000 0.0000
Ke 0.0000 0.0000 0.0000
newmtl 3d66_VRayMtl_23605226_030
Ns 10.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 0.5880 0.5880 0.5880
Kd 0.5882 0.5882 0.5882
Ks 0.0000 0.0000 0.0000
Ke 0.0000 0.0000 0.0000
newmtl 3d66_VRayMtl_23605226_031
Ns 10.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 0.5880 0.5880 0.5880
Kd 0.5000 0.5000 0.5000
Ks 0.0000 0.0000 0.0000
Ke 0.0000 0.0000 0.0000
map_Ka maps\3d66Model-23605226-files-005.png
map_Kd maps\3d66Model-23605226-files-005.png
map_bump maps\3d66_VRayNormalMap_23605226_023_VR-<2D><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ͼ.tga
newmtl Mat3d66_7859414_4_42689
Ns 10.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 0.5880 0.5880 0.5880
Kd 0.3529 0.3529 0.3529
Ks 0.0000 0.0000 0.0000
Ke 0.0000 0.0000 0.0000
newmtl Mat3d66_7859414_3_41489
Ns 10.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 0.5880 0.5880 0.5880
Kd 0.8627 0.8549 0.8471
Ks 0.0000 0.0000 0.0000
Ke 0.0000 0.0000 0.0000
newmtl Mat3d66_7859414_7_21754
Ns 10.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 0.5880 0.5880 0.5880
Kd 0.1765 0.1765 0.1765
Ks 0.0000 0.0000 0.0000
Ke 0.0000 0.0000 0.0000
newmtl Mat3d66_7859414_5_93165
Ns 10.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 0.5880 0.5880 0.5880
Kd 0.8235 0.8196 0.8157
Ks 0.0000 0.0000 0.0000
Ke 0.0000 0.0000 0.0000
newmtl wire_135006006
Ns 32
d 1
Tr 0
Tf 1 1 1
illum 2
Ka 0.5294 0.0235 0.0235
Kd 0.5294 0.0235 0.0235
Ks 0.3500 0.3500 0.3500
newmtl 3d66_VRayMtl_22568095_046
Ns 10.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 0.5880 0.5880 0.5880
Kd 0.0000 0.0000 0.0000
Ks 0.0000 0.0000 0.0000
Ke 0.0000 0.0000 0.0000
newmtl 3d66_VRayMtl_22568095_047
Ns 10.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 0.5880 0.5880 0.5880
Kd 0.0275 0.0275 0.0275
Ks 0.0000 0.0000 0.0000
Ke 0.0000 0.0000 0.0000
newmtl 3d66_VRayMtl_22568095_045
Ns 10.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 0.5880 0.5880 0.5880
Kd 0.6314 0.0392 0.0392
Ks 0.0000 0.0000 0.0000
Ke 0.0000 0.0000 0.0000
newmtl 3d66_VRayMtl_22568095_048
Ns 10.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 0.5880 0.5880 0.5880
Kd 0.7529 0.7647 0.7294
Ks 0.0000 0.0000 0.0000
Ke 0.0000 0.0000 0.0000
newmtl 3d66_VRayMtl_22568095_049
Ns 10.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 0.5880 0.5880 0.5880
Kd 0.5961 0.6039 0.5765
Ks 0.0000 0.0000 0.0000
Ke 0.0000 0.0000 0.0000
newmtl 3d66_VRayMtl_22568095_052
Ns 10.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 0.5880 0.5880 0.5880
Kd 0.0510 0.0510 0.0510
Ks 0.0000 0.0000 0.0000
Ke 0.0000 0.0000 0.0000
newmtl 3d66_VRayMtl_22568095_069
Ns 10.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 0.5880 0.5880 0.5880
Kd 1.0000 1.0000 1.0000
Ks 0.0000 0.0000 0.0000
Ke 0.0000 0.0000 0.0000
newmtl 3d66_VRayMtl_22568095_070
Ns 10.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 0.5880 0.5880 0.5880
Kd 0.7176 0.7882 0.8510
Ks 0.0000 0.0000 0.0000
Ke 0.0000 0.0000 0.0000
newmtl 3d66_VRayMtl_22568095_071
Ns 10.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 0.5880 0.5880 0.5880
Kd 0.0000 0.0000 0.0000
Ks 0.0000 0.0000 0.0000
Ke 0.0000 0.0000 0.0000
newmtl 3d66_VRayMtl_22568095_074
Ns 10.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 0.5880 0.5880 0.5880
Kd 0.5000 0.5000 0.5000
Ks 0.0000 0.0000 0.0000
Ke 0.0000 0.0000 0.0000
map_Ka maps\3d66_Bricks_22568095_021_Tiles.tga
map_Kd maps\3d66_Bricks_22568095_021_Tiles.tga
newmtl 3d66_VRayMtl_22568095_025
Ns 10.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 0.5880 0.5880 0.5880
Kd 0.0902 0.0902 0.0902
Ks 0.0000 0.0000 0.0000
Ke 0.0000 0.0000 0.0000
newmtl 3d66_Standardmaterial_22568095_004
Ns 10.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 0.5882 0.5882 0.5882
Kd 0.5882 0.5882 0.5882
Ks 0.0000 0.0000 0.0000
Ke 0.0000 0.0000 0.0000
newmtl 3d66_VRayMtl_22568095_066
Ns 10.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 0.5880 0.5880 0.5880
Kd 0.0392 0.0392 0.0392
Ks 0.0000 0.0000 0.0000
Ke 0.0000 0.0000 0.0000
newmtl 17___Default
Ns 10.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 0.5880 0.5880 0.5880
Kd 0.5000 0.5000 0.5000
Ks 0.0000 0.0000 0.0000
Ke 0.0000 0.0000 0.0000
map_Ka maps\t3.jpg
map_Kd maps\t3.jpg
newmtl 18___Default
Ns 10.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 0.5880 0.5880 0.5880
Kd 0.5000 0.5000 0.5000
Ks 0.0000 0.0000 0.0000
Ke 0.0000 0.0000 0.0000
map_Ka maps\t5.jpg
map_Kd maps\t5.jpg
newmtl 16___Default
Ns 10.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 0.5880 0.5880 0.5880
Kd 0.5000 0.5000 0.5000
Ks 0.0000 0.0000 0.0000
Ke 0.0000 0.0000 0.0000
map_Ka maps\t2.jpg
map_Kd maps\t2.jpg
newmtl 15___Default
Ns 10.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 0.5880 0.5880 0.5880
Kd 0.5000 0.5000 0.5000
Ks 0.0000 0.0000 0.0000
Ke 0.0000 0.0000 0.0000
map_Ka maps\t1.jpg
map_Kd maps\t1.jpg
newmtl Mat3d66_14662737_11_91260
Ns 10.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 0.5880 0.5880 0.5880
Kd 1.0000 1.0000 1.0000
Ks 0.0000 0.0000 0.0000
Ke 0.0000 0.0000 0.0000
map_Ka maps\3d66Model-14662737-files-5.jpg
map_Kd maps\3d66Model-14662737-files-5.jpg
newmtl Mat3d66_14662737_12_48868
Ns 10.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 0.5880 0.5880 0.5880
Kd 1.0000 1.0000 1.0000
Ks 0.0000 0.0000 0.0000
Ke 0.0000 0.0000 0.0000
map_Ka maps\3d66Model-14662737-files-5.jpg
map_Kd maps\3d66Model-14662737-files-5.jpg
newmtl wire_154185229
Ns 32
d 1
Tr 0
Tf 1 1 1
illum 2
Ka 0.6039 0.7255 0.8980
Kd 0.6039 0.7255 0.8980
Ks 0.3500 0.3500 0.3500
newmtl Glass
Ns 10.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 0.5880 0.5880 0.5880
Kd 0.0000 0.0000 0.0000
Ks 0.0000 0.0000 0.0000
Ke 0.0000 0.0000 0.0000
newmtl Mat3d66_8099876_13_28564
Ns 10.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 0.5880 0.5880 0.5880
Kd 0.4667 0.4510 0.4353
Ks 0.0000 0.0000 0.0000
Ke 0.0000 0.0000 0.0000
newmtl Material__53
Ns 10.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 0.5880 0.5880 0.5880
Kd 0.0000 0.0000 0.0000
Ks 0.0000 0.0000 0.0000
Ke 0.0000 0.0000 0.0000
newmtl Material__52
Ns 10.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 0.5880 0.5880 0.5880
Kd 0.1451 0.1451 0.1451
Ks 0.0000 0.0000 0.0000
Ke 0.0000 0.0000 0.0000
map_Ka maps\3d66Model-8099876-files-4.jpg
map_Kd maps\3d66Model-8099876-files-4.jpg
newmtl Material__109
Ns 10.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 0.5880 0.5880 0.5880
Kd 0.0000 0.0000 0.0000
Ks 0.0000 0.0000 0.0000
Ke 0.0000 0.0000 0.0000
newmtl <20><><EFBFBD>ݧ<EFBFBD><DDA7>ާڧߧڧ<DFA7>_<EFBFBD><5F><EFBFBD><EFBFBD><EFBFBD>ݧڧ<DDA7><DAA7><EFBFBD><EFBFBD>ӧѧߧߧ<DFA7><DFA7><EFBFBD>
Ns 10.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 0.5880 0.5880 0.5880
Kd 0.1872 0.1872 0.1872
Ks 0.0000 0.0000 0.0000
Ke 0.0000 0.0000 0.0000
newmtl <20><><EFBFBD><EFBFBD><EFBFBD>ݧ<EFBFBD>
Ns 10.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 0.5880 0.5880 0.5880
Kd 0.7529 0.7529 0.7529
Ks 0.0000 0.0000 0.0000
Ke 0.0000 0.0000 0.0000
map_d maps\Map__19_Falloff.tga
newmtl wire_154215229
Ns 32
d 1
Tr 0
Tf 1 1 1
illum 2
Ka 0.6039 0.8431 0.8980
Kd 0.6039 0.8431 0.8980
Ks 0.3500 0.3500 0.3500

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,621 @@
(function() {
var Nr = 10;
// convert two-dimensional indicies to one-dim array indices
var I00 = 0;
var I01 = 1;
var I02 = 2;
var I03 = 3;
var I10 = 4;
var I11 = 5;
var I12 = 6;
var I13 = 7;
var I20 = 8;
var I21 = 9;
var I22 = 10;
var I23 = 11;
var I30 = 12;
var I31 = 13;
var I32 = 14;
var I33 = 15;
// S-Box substitution table
var S_enc = new Array(
0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5,
0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76,
0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0,
0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0,
0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc,
0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15,
0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a,
0x07, 0x12, 0x80, 0xe2, 0xeb, 0x27, 0xb2, 0x75,
0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0,
0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84,
0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b,
0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf,
0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85,
0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8,
0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5,
0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2,
0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17,
0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73,
0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88,
0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb,
0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c,
0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79,
0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9,
0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08,
0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6,
0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a,
0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e,
0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e,
0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94,
0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf,
0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68,
0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb, 0x16);
// inverse S-Box for decryptions
var S_dec = new Array(
0x52, 0x09, 0x6a, 0xd5, 0x30, 0x36, 0xa5, 0x38,
0xbf, 0x40, 0xa3, 0x9e, 0x81, 0xf3, 0xd7, 0xfb,
0x7c, 0xe3, 0x39, 0x82, 0x9b, 0x2f, 0xff, 0x87,
0x34, 0x8e, 0x43, 0x44, 0xc4, 0xde, 0xe9, 0xcb,
0x54, 0x7b, 0x94, 0x32, 0xa6, 0xc2, 0x23, 0x3d,
0xee, 0x4c, 0x95, 0x0b, 0x42, 0xfa, 0xc3, 0x4e,
0x08, 0x2e, 0xa1, 0x66, 0x28, 0xd9, 0x24, 0xb2,
0x76, 0x5b, 0xa2, 0x49, 0x6d, 0x8b, 0xd1, 0x25,
0x72, 0xf8, 0xf6, 0x64, 0x86, 0x68, 0x98, 0x16,
0xd4, 0xa4, 0x5c, 0xcc, 0x5d, 0x65, 0xb6, 0x92,
0x6c, 0x70, 0x48, 0x50, 0xfd, 0xed, 0xb9, 0xda,
0x5e, 0x15, 0x46, 0x57, 0xa7, 0x8d, 0x9d, 0x84,
0x90, 0xd8, 0xab, 0x00, 0x8c, 0xbc, 0xd3, 0x0a,
0xf7, 0xe4, 0x58, 0x05, 0xb8, 0xb3, 0x45, 0x06,
0xd0, 0x2c, 0x1e, 0x8f, 0xca, 0x3f, 0x0f, 0x02,
0xc1, 0xaf, 0xbd, 0x03, 0x01, 0x13, 0x8a, 0x6b,
0x3a, 0x91, 0x11, 0x41, 0x4f, 0x67, 0xdc, 0xea,
0x97, 0xf2, 0xcf, 0xce, 0xf0, 0xb4, 0xe6, 0x73,
0x96, 0xac, 0x74, 0x22, 0xe7, 0xad, 0x35, 0x85,
0xe2, 0xf9, 0x37, 0xe8, 0x1c, 0x75, 0xdf, 0x6e,
0x47, 0xf1, 0x1a, 0x71, 0x1d, 0x29, 0xc5, 0x89,
0x6f, 0xb7, 0x62, 0x0e, 0xaa, 0x18, 0xbe, 0x1b,
0xfc, 0x56, 0x3e, 0x4b, 0xc6, 0xd2, 0x79, 0x20,
0x9a, 0xdb, 0xc0, 0xfe, 0x78, 0xcd, 0x5a, 0xf4,
0x1f, 0xdd, 0xa8, 0x33, 0x88, 0x07, 0xc7, 0x31,
0xb1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xec, 0x5f,
0x60, 0x51, 0x7f, 0xa9, 0x19, 0xb5, 0x4a, 0x0d,
0x2d, 0xe5, 0x7a, 0x9f, 0x93, 0xc9, 0x9c, 0xef,
0xa0, 0xe0, 0x3b, 0x4d, 0xae, 0x2a, 0xf5, 0xb0,
0xc8, 0xeb, 0xbb, 0x3c, 0x83, 0x53, 0x99, 0x61,
0x17, 0x2b, 0x04, 0x7e, 0xba, 0x77, 0xd6, 0x26,
0xe1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0c, 0x7d);
function cvt_hex8 (val) {
var vh = (val>>>4)&0x0f;
return vh.toString(16) + (val&0x0f).toString(16);
}
function cvt_byte (str) {
// get the first hex digit
var val1 = str.charCodeAt(0);
// do some error checking
if (val1 >= 48 && val1 <= 57) {
// have a valid digit 0-9
val1 -= 48;
} else if (val1 >= 65 && val1 <= 70) {
// have a valid digit A-F
val1 -= 55;
} else if (val1 >= 97 && val1 <= 102) {
// have a valid digit A-F
val1 -= 87;
} else {
// not 0-9 or A-F, complain
console.log( str.charAt(1)+" is not a valid hex digit" );
return -1;
}
// get the second hex digit
var val2 = str.charCodeAt(1);
// do some error checking
if ( val2 >= 48 && val2 <= 57 ) {
// have a valid digit 0-9
val2 -= 48;
} else if ( val2 >= 65 && val2 <= 70 ) {
// have a valid digit A-F
val2 -= 55;
} else if ( val2 >= 97 && val2 <= 102 ) {
// have a valid digit A-F
val2 -= 87;
} else {
// not 0-9 or A-F, complain
console.log( str.charAt(2)+" is not a valid hex digit" );
return -1;
}
// all is ok, return the value
return val1*16 + val2;
}
// conversion function for non-constant subscripts
// assume subscript range 0..3
function I(x,y) {
return (x*4) + y;
}
// remove spaces from input
function remove_spaces(instr) {
var i;
var outstr = "";
for(i=0; i<instr.length; i++) {
if ( instr.charAt(i) != " " )
// not a space, include it
outstr += instr.charAt(i);
}
return outstr;
}
// get the message to encrypt/decrypt or the key
// return as a 16-byte array
function get_value(str, isASCII) {
var dbyte = new Array(16);
var i;
var val; // one hex digit
if (isASCII) {
// check length of data
if (str.length > 16) {
console.log("is too long, using the first 16 ASCII characters" );
}
// have ASCII data
// 16 characters?
if (str.length >= 16) {
// 16 or more characters
for(i=0; i<16; i++) {
dbyte[i] = str.charCodeAt(i);
}
} else {
// less than 16 characters - fill with NULLs
for(i=0; i<str.length; i++) {
dbyte[i] = str.charCodeAt(i);
}
for( i=str.length; i<16; i++) {
dbyte[i] = 0;
}
}
} else {
// have hex data - remove any spaces they used, then convert
//str = remove_spaces(str);
// check length of data
if ( str.length != 32 ) {
//console.log("\tget_value:\tstr = " + str + "\tisASCII = " + isASCII); //isASCII = false
console.log("length wrong: Is " + str.length + " hex digits, but must be 128 bits (32 hex digits)");
dbyte[0] = -1;
return dbyte;
}
for( i=0; i<16; i++ ) {
// isolate and convert this substring
dbyte[i] = cvt_byte( str.substr(i*2,2) );
if( dbyte[i] < 0 ) {
// have an error
dbyte[0] = -1;
return dbyte;
}
}
}
// return successful conversion
return dbyte;
}
//do the AES GF(2**8) multiplication
// do this by the shift-and-"add" approach
function aes_mul(a, b) {
var res = 0;
while(a > 0) {
if((a&1) != 0)
res = res ^ b; // "add" to the result
a >>>= 1; // shift a to get next higher-order bit
b <<= 1; // shift multiplier also
}
// now reduce it modulo x**8 + x**4 + x**3 + x + 1
var hbit = 0x10000; // bit to test if we need to take action
var modulus = 0x11b00; // modulus - XOR by this to change value
while(hbit >= 0x100) {
if ((res & hbit) != 0) {
res ^= modulus; // XOR with the modulus
}
// prepare for the next loop
hbit >>= 1;
modulus >>= 1;
}
return res;
}
// apply the S-box substitution to the key expansion
function SubWord(word_ary) {
var i;
for(i=0; i<16; i++) {
word_ary[i] = S_enc[word_ary[i]];
}
return word_ary;
}
// rotate the bytes in a word
function RotWord(word_ary) {
return new Array(word_ary[1], word_ary[2], word_ary[3], word_ary[0]);
}
// calculate the first item Rcon[i] = { x^(i-1), 0, 0, 0 }
// note we only return the first item
function Rcon(exp) {
var val = 2;
var result = 1;
// remember to calculate x^(exp-1)
exp--;
// process the exponent using normal shift and multiply
while ( exp > 0 )
{
if ( (exp & 1) != 0 )
result = aes_mul( result, val );
// square the value
val = aes_mul( val, val );
// move to the next bit
exp >>= 1;
}
return result;
}
// round key generation
// return a byte array with the expanded key information
function key_expand( key )
{
var temp = new Array(4);
var i, j;
var w = new Array(4*(Nr+1));
// copy initial key stuff
for( i=0; i<16; i++ )
{
w[i] = key[i];
}
// generate rest of key schedule using 32-bit words
i = 4;
while ( i < 4*(Nr+1)) // blocksize * ( rounds + 1 )
{
// copy word W[i-1] to temp
for( j=0; j<4; j++ )
temp[j] = w[(i-1)*4+j];
if ( i % 4 == 0)
{
// temp = SubWord(RotWord(temp)) ^ Rcon[i/4];
temp = RotWord( temp );
temp = SubWord( temp );
temp[0] ^= Rcon( i>>>2 );
}
// word = word ^ temp
for( j=0; j<4; j++ )
w[i*4+j] = w[(i-4)*4+j] ^ temp[j];
i++;
}
return w;
}
// do S-Box substitution
function SubBytes(state, Sbox)
{
var i;
for( i=0; i<16; i++ )
state[i] = Sbox[ state[i] ];
return state;
}
// shift each row as appropriate
function ShiftRows(state)
{
var t0, t1, t2, t3;
// top row (row 0) isn't shifted
// next row (row 1) rotated left 1 place
t0 = state[I10];
t1 = state[I11];
t2 = state[I12];
t3 = state[I13];
state[I10] = t1;
state[I11] = t2;
state[I12] = t3;
state[I13] = t0;
// next row (row 2) rotated left 2 places
t0 = state[I20];
t1 = state[I21];
t2 = state[I22];
t3 = state[I23];
state[I20] = t2;
state[I21] = t3;
state[I22] = t0;
state[I23] = t1;
// bottom row (row 3) rotated left 3 places
t0 = state[I30];
t1 = state[I31];
t2 = state[I32];
t3 = state[I33];
state[I30] = t3;
state[I31] = t0;
state[I32] = t1;
state[I33] = t2;
return state;
}
// inverset shift each row as appropriate
function InvShiftRows(state)
{
var t0, t1, t2, t3;
// top row (row 0) isn't shifted
// next row (row 1) rotated left 1 place
t0 = state[I10];
t1 = state[I11];
t2 = state[I12];
t3 = state[I13];
state[I10] = t3;
state[I11] = t0;
state[I12] = t1;
state[I13] = t2;
// next row (row 2) rotated left 2 places
t0 = state[I20];
t1 = state[I21];
t2 = state[I22];
t3 = state[I23];
state[I20] = t2;
state[I21] = t3;
state[I22] = t0;
state[I23] = t1;
// bottom row (row 3) rotated left 3 places
t0 = state[I30];
t1 = state[I31];
t2 = state[I32];
t3 = state[I33];
state[I30] = t1;
state[I31] = t2;
state[I32] = t3;
state[I33] = t0;
return state;
}
// process column info
function MixColumns(state)
{
var col;
var c0, c1, c2, c3;
for( col=0; col<4; col++ )
{
c0 = state[I(0,col)];
c1 = state[I(1,col)];
c2 = state[I(2,col)];
c3 = state[I(3,col)];
// do mixing, and put back into array
state[I(0,col)] = aes_mul(2,c0) ^ aes_mul(3,c1) ^ c2 ^ c3;
state[I(1,col)] = c0 ^ aes_mul(2,c1) ^ aes_mul(3,c2) ^ c3;
state[I(2,col)] = c0 ^ c1 ^ aes_mul(2,c2) ^ aes_mul(3,c3);
state[I(3,col)] = aes_mul(3,c0) ^ c1 ^ c2 ^ aes_mul(2,c3);
}
return state;
}
// inverse process column info
function InvMixColumns(state)
{
var col;
var c0, c1, c2, c3;
for( col=0; col<4; col++ )
{
c0 = state[I(0,col)];
c1 = state[I(1,col)];
c2 = state[I(2,col)];
c3 = state[I(3,col)];
// do inverse mixing, and put back into array
state[I(0,col)] = aes_mul(0x0e,c0) ^ aes_mul(0x0b,c1)
^ aes_mul(0x0d,c2) ^ aes_mul(0x09,c3);
state[I(1,col)] = aes_mul(0x09,c0) ^ aes_mul(0x0e,c1)
^ aes_mul(0x0b,c2) ^ aes_mul(0x0d,c3);
state[I(2,col)] = aes_mul(0x0d,c0) ^ aes_mul(0x09,c1)
^ aes_mul(0x0e,c2) ^ aes_mul(0x0b,c3);
state[I(3,col)] = aes_mul(0x0b,c0) ^ aes_mul(0x0d,c1)
^ aes_mul(0x09,c2) ^ aes_mul(0x0e,c3);
}
return state;
}
// insert subkey information
function AddRoundKey( state, w, base )
{
var col;
for( col=0; col<4; col++ )
{
state[I(0,col)] ^= w[base+col*4];
state[I(1,col)] ^= w[base+col*4+1];
state[I(2,col)] ^= w[base+col*4+2];
state[I(3,col)] ^= w[base+col*4+3];
}
return state;
}
// return a transposed array
function transpose( msg )
{
var row, col;
var state = new Array( 16 );
for( row=0; row<4; row++ )
for( col=0; col<4; col++ )
state[I(row,col)] = msg[I(col,row)];
return state;
}
// final AES state
var AES_output = new Array(16);
// format AES output
// -- uses the global array DES_output
function format_AES_output(bASCII)
{
var i;
var bits;
var str="";
// what type of data do we have to work with?
if (bASCII)
{
// convert each set of bits back to ASCII
for( i=0; i<16; i++ )
str += String.fromCharCode( AES_output[i] );
}
else
{
// output hexdecimal data (insert spaces)
str = cvt_hex8( AES_output[0] );
for( i=1; i<16; i++ )
{
str += "" + cvt_hex8( AES_output[i] );
}
}
return str;
}
// do encrytion
function aes_encrypt(str, key, bASCII)
{
//console.log(" aes_encrypt:\tstr = " + str + "\tkey = " + key + "\t bASCII = " + bASCII);
var w = new Array( 4*(Nr+1) ); // subkey information
var state = new Array( 16 ); // working state
var round;
//accumulated_output_info = "";
// get the message from the user
// also check if it is ASCII or hex
var msg = get_value(str, bASCII);
// problems??
if ( msg[0] < 0 )
{
return;
}
// get the key from the user
var key = get_value(key, false);
// problems??
if ( key[0] < 0 )
{
return;
}
// expand the key
w = key_expand( key );
// initial state = message in columns (transposed from what we input)
state = transpose( msg );
// display the round key - Transpose due to the way it is stored/used
state = AddRoundKey(state, w, 0);
for( round=1; round<Nr; round++ )
{
state = SubBytes(state, S_enc);
state = ShiftRows(state);
state = MixColumns(state);
// display the round key - Transpose due to the way it is stored/used
// note here the spec uses 32-bit words, we are using bytes, so an extra *4
state = AddRoundKey(state, w, round*4*4);
}
SubBytes(state, S_enc);
ShiftRows(state);
AddRoundKey(state, w, Nr*4*4);
// process output
AES_output = transpose( state );
var szOutput = format_AES_output(!bASCII);
return szOutput;
}
// do decryption
function aes_decrypt(str, key, bASCII)
{
//console.log(" aes_decrypt:\tstr = " + str + "\tkey = " + key + "\tbASCII = " + bASCII);
var w = new Array( 4*(Nr+1) ); // subkey information
var state = new Array( 16 ); // working state
var round;
//accumulated_output_info = "";
// get the message from the user
// also check if it is ASCII or hex
var msg = get_value(str, bASCII);
// problems??
if ( msg[0] < 0 )
{
return;
}
// get the key from the user
var key = get_value(key, false);
// problems??
if ( key[0] < 0 )
{
return;
}
// expand the key
w = key_expand( key );
// initial state = message
state = transpose( msg );
// display the round key - Transpose due to the way it is stored/used
state = AddRoundKey(state, w, Nr*4*4);
for( round=Nr-1; round>=1; round-- )
{
state = InvShiftRows(state);
state = SubBytes(state, S_dec);
// display the round key - Transpose due to the way it is stored/used
// note here the spec uses 32-bit words, we are using bytes, so an extra *4
state = AddRoundKey(state, w, round*4*4);
state = InvMixColumns(state);
}
InvShiftRows(state);
SubBytes(state, S_dec);
AddRoundKey(state, w, 0);
// process output
AES_output = transpose( state );
var szOutput = format_AES_output(!bASCII);
return szOutput;
}
window.aes_encrypt = aes_encrypt;
window.aes_decrypt = aes_decrypt;
window.console = window.console || {
log: function() {}
};
}());

View File

@@ -0,0 +1,106 @@
var dbits,canary=244837814094590,j_lm=(canary&16777215)==15715070;function BigInteger(a,b,c){a!=null&&("number"==typeof a?this.fromNumber(a,b,c):b==null&&"string"!=typeof a?this.fromString(a,256):this.fromString(a,b))}function nbi(){return new BigInteger(null)}function am1(a,b,c,d,e,g){for(;--g>=0;){var h=b*this[a++]+c[d]+e,e=Math.floor(h/67108864);c[d++]=h&67108863}return e}
function am2(a,b,c,d,e,g){var h=b&32767;for(b>>=15;--g>=0;){var f=this[a]&32767,o=this[a++]>>15,p=b*f+o*h,f=h*f+((p&32767)<<15)+c[d]+(e&1073741823),e=(f>>>30)+(p>>>15)+b*o+(e>>>30);c[d++]=f&1073741823}return e}function am3(a,b,c,d,e,g){var h=b&16383;for(b>>=14;--g>=0;){var f=this[a]&16383,o=this[a++]>>14,p=b*f+o*h,f=h*f+((p&16383)<<14)+c[d]+e,e=(f>>28)+(p>>14)+b*o;c[d++]=f&268435455}return e}
j_lm&&navigator.appName=="Microsoft Internet Explorer"?(BigInteger.prototype.am=am2,dbits=30):j_lm&&navigator.appName!="Netscape"?(BigInteger.prototype.am=am1,dbits=26):(BigInteger.prototype.am=am3,dbits=28);BigInteger.prototype.DB=dbits;BigInteger.prototype.DM=(1<<dbits)-1;BigInteger.prototype.DV=1<<dbits;var BI_FP=52;BigInteger.prototype.FV=Math.pow(2,BI_FP);BigInteger.prototype.F1=BI_FP-dbits;BigInteger.prototype.F2=2*dbits-BI_FP;var BI_RM="0123456789abcdefghijklmnopqrstuvwxyz",BI_RC=[],rr,vv;
rr="0".charCodeAt(0);for(vv=0;vv<=9;++vv)BI_RC[rr++]=vv;rr="a".charCodeAt(0);for(vv=10;vv<36;++vv)BI_RC[rr++]=vv;rr="A".charCodeAt(0);for(vv=10;vv<36;++vv)BI_RC[rr++]=vv;function int2char(a){return BI_RM.charAt(a)}function intAt(a,b){var c=BI_RC[a.charCodeAt(b)];return c==null?-1:c}function bnpCopyTo(a){for(var b=this.t-1;b>=0;--b)a[b]=this[b];a.t=this.t;a.s=this.s}function bnpFromInt(a){this.t=1;this.s=a<0?-1:0;a>0?this[0]=a:a<-1?this[0]=a+DV:this.t=0}
function nbv(a){var b=nbi();b.fromInt(a);return b}
function bnpFromString(a,b){var c;if(b==16)c=4;else if(b==8)c=3;else if(b==256)c=8;else if(b==2)c=1;else if(b==32)c=5;else if(b==4)c=2;else{this.fromRadix(a,b);return}this.s=this.t=0;for(var d=a.length,e=!1,g=0;--d>=0;){var h=c==8?a[d]&255:intAt(a,d);h<0?a.charAt(d)=="-"&&(e=!0):(e=!1,g==0?this[this.t++]=h:g+c>this.DB?(this[this.t-1]|=(h&(1<<this.DB-g)-1)<<g,this[this.t++]=h>>this.DB-g):this[this.t-1]|=h<<g,g+=c,g>=this.DB&&(g-=this.DB))}if(c==8&&(a[0]&128)!=0)this.s=-1,g>0&&(this[this.t-1]|=(1<<
this.DB-g)-1<<g);this.clamp();e&&BigInteger.ZERO.subTo(this,this)}function bnpClamp(){for(var a=this.s&this.DM;this.t>0&&this[this.t-1]==a;)--this.t}
function bnToString(a){if(this.s<0)return"-"+this.negate().toString(a);if(a==16)a=4;else if(a==8)a=3;else if(a==2)a=1;else if(a==32)a=5;else if(a==64)a=6;else if(a==4)a=2;else return this.toRadix(a);var b=(1<<a)-1,c,d=!1,e="",g=this.t,h=this.DB-g*this.DB%a;if(g-- >0){if(h<this.DB&&(c=this[g]>>h)>0)d=!0,e=int2char(c);for(;g>=0;)h<a?(c=(this[g]&(1<<h)-1)<<a-h,c|=this[--g]>>(h+=this.DB-a)):(c=this[g]>>(h-=a)&b,h<=0&&(h+=this.DB,--g)),c>0&&(d=!0),d&&(e+=int2char(c))}return d?e:"0"}
function bnNegate(){var a=nbi();BigInteger.ZERO.subTo(this,a);return a}function bnAbs(){return this.s<0?this.negate():this}function bnCompareTo(a){var b=this.s-a.s;if(b!=0)return b;var c=this.t,b=c-a.t;if(b!=0)return b;for(;--c>=0;)if((b=this[c]-a[c])!=0)return b;return 0}function nbits(a){var b=1,c;if((c=a>>>16)!=0)a=c,b+=16;if((c=a>>8)!=0)a=c,b+=8;if((c=a>>4)!=0)a=c,b+=4;if((c=a>>2)!=0)a=c,b+=2;a>>1!=0&&(b+=1);return b}
function bnBitLength(){return this.t<=0?0:this.DB*(this.t-1)+nbits(this[this.t-1]^this.s&this.DM)}function bnpDLShiftTo(a,b){var c;for(c=this.t-1;c>=0;--c)b[c+a]=this[c];for(c=a-1;c>=0;--c)b[c]=0;b.t=this.t+a;b.s=this.s}function bnpDRShiftTo(a,b){for(var c=a;c<this.t;++c)b[c-a]=this[c];b.t=Math.max(this.t-a,0);b.s=this.s}
function bnpLShiftTo(a,b){var c=a%this.DB,d=this.DB-c,e=(1<<d)-1,g=Math.floor(a/this.DB),h=this.s<<c&this.DM,f;for(f=this.t-1;f>=0;--f)b[f+g+1]=this[f]>>d|h,h=(this[f]&e)<<c;for(f=g-1;f>=0;--f)b[f]=0;b[g]=h;b.t=this.t+g+1;b.s=this.s;b.clamp()}
function bnpRShiftTo(a,b){b.s=this.s;var c=Math.floor(a/this.DB);if(c>=this.t)b.t=0;else{var d=a%this.DB,e=this.DB-d,g=(1<<d)-1;b[0]=this[c]>>d;for(var h=c+1;h<this.t;++h)b[h-c-1]|=(this[h]&g)<<e,b[h-c]=this[h]>>d;d>0&&(b[this.t-c-1]|=(this.s&g)<<e);b.t=this.t-c;b.clamp()}}
function bnpSubTo(a,b){for(var c=0,d=0,e=Math.min(a.t,this.t);c<e;)d+=this[c]-a[c],b[c++]=d&this.DM,d>>=this.DB;if(a.t<this.t){for(d-=a.s;c<this.t;)d+=this[c],b[c++]=d&this.DM,d>>=this.DB;d+=this.s}else{for(d+=this.s;c<a.t;)d-=a[c],b[c++]=d&this.DM,d>>=this.DB;d-=a.s}b.s=d<0?-1:0;d<-1?b[c++]=this.DV+d:d>0&&(b[c++]=d);b.t=c;b.clamp()}
function bnpMultiplyTo(a,b){var c=this.abs(),d=a.abs(),e=c.t;for(b.t=e+d.t;--e>=0;)b[e]=0;for(e=0;e<d.t;++e)b[e+c.t]=c.am(0,d[e],b,e,0,c.t);b.s=0;b.clamp();this.s!=a.s&&BigInteger.ZERO.subTo(b,b)}function bnpSquareTo(a){for(var b=this.abs(),c=a.t=2*b.t;--c>=0;)a[c]=0;for(c=0;c<b.t-1;++c){var d=b.am(c,b[c],a,2*c,0,1);if((a[c+b.t]+=b.am(c+1,2*b[c],a,2*c+1,d,b.t-c-1))>=b.DV)a[c+b.t]-=b.DV,a[c+b.t+1]=1}a.t>0&&(a[a.t-1]+=b.am(c,b[c],a,2*c,0,1));a.s=0;a.clamp()}
function bnpDivRemTo(a,b,c){var d=a.abs();if(!(d.t<=0)){var e=this.abs();if(e.t<d.t)b!=null&&b.fromInt(0),c!=null&&this.copyTo(c);else{c==null&&(c=nbi());var g=nbi(),h=this.s,a=a.s,f=this.DB-nbits(d[d.t-1]);f>0?(d.lShiftTo(f,g),e.lShiftTo(f,c)):(d.copyTo(g),e.copyTo(c));d=g.t;e=g[d-1];if(e!=0){var o=e*(1<<this.F1)+(d>1?g[d-2]>>this.F2:0),p=this.FV/o,o=(1<<this.F1)/o,q=1<<this.F2,n=c.t,k=n-d,j=b==null?nbi():b;g.dlShiftTo(k,j);c.compareTo(j)>=0&&(c[c.t++]=1,c.subTo(j,c));BigInteger.ONE.dlShiftTo(d,
j);for(j.subTo(g,g);g.t<d;)g[g.t++]=0;for(;--k>=0;){var l=c[--n]==e?this.DM:Math.floor(c[n]*p+(c[n-1]+q)*o);if((c[n]+=g.am(0,l,c,k,0,d))<l){g.dlShiftTo(k,j);for(c.subTo(j,c);c[n]<--l;)c.subTo(j,c)}}b!=null&&(c.drShiftTo(d,b),h!=a&&BigInteger.ZERO.subTo(b,b));c.t=d;c.clamp();f>0&&c.rShiftTo(f,c);h<0&&BigInteger.ZERO.subTo(c,c)}}}}function bnMod(a){var b=nbi();this.abs().divRemTo(a,null,b);this.s<0&&b.compareTo(BigInteger.ZERO)>0&&a.subTo(b,b);return b}function Classic(a){this.m=a}
function cConvert(a){return a.s<0||a.compareTo(this.m)>=0?a.mod(this.m):a}function cRevert(a){return a}function cReduce(a){a.divRemTo(this.m,null,a)}function cMulTo(a,b,c){a.multiplyTo(b,c);this.reduce(c)}function cSqrTo(a,b){a.squareTo(b);this.reduce(b)}Classic.prototype.convert=cConvert;Classic.prototype.revert=cRevert;Classic.prototype.reduce=cReduce;Classic.prototype.mulTo=cMulTo;Classic.prototype.sqrTo=cSqrTo;
function bnpInvDigit(){if(this.t<1)return 0;var a=this[0];if((a&1)==0)return 0;var b=a&3,b=b*(2-(a&15)*b)&15,b=b*(2-(a&255)*b)&255,b=b*(2-((a&65535)*b&65535))&65535,b=b*(2-a*b%this.DV)%this.DV;return b>0?this.DV-b:-b}function Montgomery(a){this.m=a;this.mp=a.invDigit();this.mpl=this.mp&32767;this.mph=this.mp>>15;this.um=(1<<a.DB-15)-1;this.mt2=2*a.t}
function montConvert(a){var b=nbi();a.abs().dlShiftTo(this.m.t,b);b.divRemTo(this.m,null,b);a.s<0&&b.compareTo(BigInteger.ZERO)>0&&this.m.subTo(b,b);return b}function montRevert(a){var b=nbi();a.copyTo(b);this.reduce(b);return b}
function montReduce(a){for(;a.t<=this.mt2;)a[a.t++]=0;for(var b=0;b<this.m.t;++b){var c=a[b]&32767,d=c*this.mpl+((c*this.mph+(a[b]>>15)*this.mpl&this.um)<<15)&a.DM,c=b+this.m.t;for(a[c]+=this.m.am(0,d,a,b,0,this.m.t);a[c]>=a.DV;)a[c]-=a.DV,a[++c]++}a.clamp();a.drShiftTo(this.m.t,a);a.compareTo(this.m)>=0&&a.subTo(this.m,a)}function montSqrTo(a,b){a.squareTo(b);this.reduce(b)}function montMulTo(a,b,c){a.multiplyTo(b,c);this.reduce(c)}Montgomery.prototype.convert=montConvert;
Montgomery.prototype.revert=montRevert;Montgomery.prototype.reduce=montReduce;Montgomery.prototype.mulTo=montMulTo;Montgomery.prototype.sqrTo=montSqrTo;function bnpIsEven(){return(this.t>0?this[0]&1:this.s)==0}function bnpExp(a,b){if(a>4294967295||a<1)return BigInteger.ONE;var c=nbi(),d=nbi(),e=b.convert(this),g=nbits(a)-1;for(e.copyTo(c);--g>=0;)if(b.sqrTo(c,d),(a&1<<g)>0)b.mulTo(d,e,c);else var h=c,c=d,d=h;return b.revert(c)}
function bnModPowInt(a,b){var c;c=a<256||b.isEven()?new Classic(b):new Montgomery(b);return this.exp(a,c)}BigInteger.prototype.copyTo=bnpCopyTo;BigInteger.prototype.fromInt=bnpFromInt;BigInteger.prototype.fromString=bnpFromString;BigInteger.prototype.clamp=bnpClamp;BigInteger.prototype.dlShiftTo=bnpDLShiftTo;BigInteger.prototype.drShiftTo=bnpDRShiftTo;BigInteger.prototype.lShiftTo=bnpLShiftTo;BigInteger.prototype.rShiftTo=bnpRShiftTo;BigInteger.prototype.subTo=bnpSubTo;
BigInteger.prototype.multiplyTo=bnpMultiplyTo;BigInteger.prototype.squareTo=bnpSquareTo;BigInteger.prototype.divRemTo=bnpDivRemTo;BigInteger.prototype.invDigit=bnpInvDigit;BigInteger.prototype.isEven=bnpIsEven;BigInteger.prototype.exp=bnpExp;BigInteger.prototype.toString=bnToString;BigInteger.prototype.negate=bnNegate;BigInteger.prototype.abs=bnAbs;BigInteger.prototype.compareTo=bnCompareTo;BigInteger.prototype.bitLength=bnBitLength;BigInteger.prototype.mod=bnMod;BigInteger.prototype.modPowInt=bnModPowInt;
BigInteger.ZERO=nbv(0);BigInteger.ONE=nbv(1);function bnClone(){var a=nbi();this.copyTo(a);return a}function bnIntValue(){if(this.s<0)if(this.t==1)return this[0]-this.DV;else{if(this.t==0)return-1}else if(this.t==1)return this[0];else if(this.t==0)return 0;return(this[1]&(1<<32-this.DB)-1)<<this.DB|this[0]}function bnByteValue(){return this.t==0?this.s:this[0]<<24>>24}function bnShortValue(){return this.t==0?this.s:this[0]<<16>>16}
function bnpChunkSize(a){return Math.floor(Math.LN2*this.DB/Math.log(a))}function bnSigNum(){return this.s<0?-1:this.t<=0||this.t==1&&this[0]<=0?0:1}function bnpToRadix(a){a==null&&(a=10);if(this.signum()==0||a<2||a>36)return"0";var b=this.chunkSize(a),b=Math.pow(a,b),c=nbv(b),d=nbi(),e=nbi(),g="";for(this.divRemTo(c,d,e);d.signum()>0;)g=(b+e.intValue()).toString(a).substr(1)+g,d.divRemTo(c,d,e);return e.intValue().toString(a)+g}
function bnpFromRadix(a,b){this.fromInt(0);b==null&&(b=10);for(var c=this.chunkSize(b),d=Math.pow(b,c),e=!1,g=0,h=0,f=0;f<a.length;++f){var o=intAt(a,f);o<0?a.charAt(f)=="-"&&this.signum()==0&&(e=!0):(h=b*h+o,++g>=c&&(this.dMultiply(d),this.dAddOffset(h,0),h=g=0))}g>0&&(this.dMultiply(Math.pow(b,g)),this.dAddOffset(h,0));e&&BigInteger.ZERO.subTo(this,this)}
function bnpFromNumber(a,b,c){if("number"==typeof b)if(a<2)this.fromInt(1);else{this.fromNumber(a,c);this.testBit(a-1)||this.bitwiseTo(BigInteger.ONE.shiftLeft(a-1),op_or,this);for(this.isEven()&&this.dAddOffset(1,0);!this.isProbablePrime(b);)this.dAddOffset(2,0),this.bitLength()>a&&this.subTo(BigInteger.ONE.shiftLeft(a-1),this)}else{var c=[],d=a&7;c.length=(a>>3)+1;b.nextBytes(c);d>0?c[0]&=(1<<d)-1:c[0]=0;this.fromString(c,256)}}
function bnToByteArray(){var a=this.t,b=[];b[0]=this.s;var c=this.DB-a*this.DB%8,d,e=0;if(a-- >0){if(c<this.DB&&(d=this[a]>>c)!=(this.s&this.DM)>>c)b[e++]=d|this.s<<this.DB-c;for(;a>=0;)if(c<8?(d=(this[a]&(1<<c)-1)<<8-c,d|=this[--a]>>(c+=this.DB-8)):(d=this[a]>>(c-=8)&255,c<=0&&(c+=this.DB,--a)),(d&128)!=0&&(d|=-256),e==0&&(this.s&128)!=(d&128)&&++e,e>0||d!=this.s)b[e++]=d}return b}function bnEquals(a){return this.compareTo(a)==0}function bnMin(a){return this.compareTo(a)<0?this:a}
function bnMax(a){return this.compareTo(a)>0?this:a}function bnpBitwiseTo(a,b,c){var d,e,g=Math.min(a.t,this.t);for(d=0;d<g;++d)c[d]=b(this[d],a[d]);if(a.t<this.t){e=a.s&this.DM;for(d=g;d<this.t;++d)c[d]=b(this[d],e);c.t=this.t}else{e=this.s&this.DM;for(d=g;d<a.t;++d)c[d]=b(e,a[d]);c.t=a.t}c.s=b(this.s,a.s);c.clamp()}function op_and(a,b){return a&b}function bnAnd(a){var b=nbi();this.bitwiseTo(a,op_and,b);return b}function op_or(a,b){return a|b}
function bnOr(a){var b=nbi();this.bitwiseTo(a,op_or,b);return b}function op_xor(a,b){return a^b}function bnXor(a){var b=nbi();this.bitwiseTo(a,op_xor,b);return b}function op_andnot(a,b){return a&~b}function bnAndNot(a){var b=nbi();this.bitwiseTo(a,op_andnot,b);return b}function bnNot(){for(var a=nbi(),b=0;b<this.t;++b)a[b]=this.DM&~this[b];a.t=this.t;a.s=~this.s;return a}function bnShiftLeft(a){var b=nbi();a<0?this.rShiftTo(-a,b):this.lShiftTo(a,b);return b}
function bnShiftRight(a){var b=nbi();a<0?this.lShiftTo(-a,b):this.rShiftTo(a,b);return b}function lbit(a){if(a==0)return-1;var b=0;(a&65535)==0&&(a>>=16,b+=16);(a&255)==0&&(a>>=8,b+=8);(a&15)==0&&(a>>=4,b+=4);(a&3)==0&&(a>>=2,b+=2);(a&1)==0&&++b;return b}function bnGetLowestSetBit(){for(var a=0;a<this.t;++a)if(this[a]!=0)return a*this.DB+lbit(this[a]);return this.s<0?this.t*this.DB:-1}function cbit(a){for(var b=0;a!=0;)a&=a-1,++b;return b}
function bnBitCount(){for(var a=0,b=this.s&this.DM,c=0;c<this.t;++c)a+=cbit(this[c]^b);return a}function bnTestBit(a){var b=Math.floor(a/this.DB);return b>=this.t?this.s!=0:(this[b]&1<<a%this.DB)!=0}function bnpChangeBit(a,b){var c=BigInteger.ONE.shiftLeft(a);this.bitwiseTo(c,b,c);return c}function bnSetBit(a){return this.changeBit(a,op_or)}function bnClearBit(a){return this.changeBit(a,op_andnot)}function bnFlipBit(a){return this.changeBit(a,op_xor)}
function bnpAddTo(a,b){for(var c=0,d=0,e=Math.min(a.t,this.t);c<e;)d+=this[c]+a[c],b[c++]=d&this.DM,d>>=this.DB;if(a.t<this.t){for(d+=a.s;c<this.t;)d+=this[c],b[c++]=d&this.DM,d>>=this.DB;d+=this.s}else{for(d+=this.s;c<a.t;)d+=a[c],b[c++]=d&this.DM,d>>=this.DB;d+=a.s}b.s=d<0?-1:0;d>0?b[c++]=d:d<-1&&(b[c++]=this.DV+d);b.t=c;b.clamp()}function bnAdd(a){var b=nbi();this.addTo(a,b);return b}function bnSubtract(a){var b=nbi();this.subTo(a,b);return b}
function bnMultiply(a){var b=nbi();this.multiplyTo(a,b);return b}function bnSquare(){var a=nbi();this.squareTo(a);return a}function bnDivide(a){var b=nbi();this.divRemTo(a,b,null);return b}function bnRemainder(a){var b=nbi();this.divRemTo(a,null,b);return b}function bnDivideAndRemainder(a){var b=nbi(),c=nbi();this.divRemTo(a,b,c);return[b,c]}function bnpDMultiply(a){this[this.t]=this.am(0,a-1,this,0,0,this.t);++this.t;this.clamp()}
function bnpDAddOffset(a,b){if(a!=0){for(;this.t<=b;)this[this.t++]=0;for(this[b]+=a;this[b]>=this.DV;)this[b]-=this.DV,++b>=this.t&&(this[this.t++]=0),++this[b]}}function NullExp(){}function nNop(a){return a}function nMulTo(a,b,c){a.multiplyTo(b,c)}function nSqrTo(a,b){a.squareTo(b)}NullExp.prototype.convert=nNop;NullExp.prototype.revert=nNop;NullExp.prototype.mulTo=nMulTo;NullExp.prototype.sqrTo=nSqrTo;function bnPow(a){return this.exp(a,new NullExp)}
function bnpMultiplyLowerTo(a,b,c){var d=Math.min(this.t+a.t,b);c.s=0;for(c.t=d;d>0;)c[--d]=0;var e;for(e=c.t-this.t;d<e;++d)c[d+this.t]=this.am(0,a[d],c,d,0,this.t);for(e=Math.min(a.t,b);d<e;++d)this.am(0,a[d],c,d,0,b-d);c.clamp()}function bnpMultiplyUpperTo(a,b,c){--b;var d=c.t=this.t+a.t-b;for(c.s=0;--d>=0;)c[d]=0;for(d=Math.max(b-this.t,0);d<a.t;++d)c[this.t+d-b]=this.am(b-d,a[d],c,0,0,this.t+d-b);c.clamp();c.drShiftTo(1,c)}
function Barrett(a){this.r2=nbi();this.q3=nbi();BigInteger.ONE.dlShiftTo(2*a.t,this.r2);this.mu=this.r2.divide(a);this.m=a}function barrettConvert(a){if(a.s<0||a.t>2*this.m.t)return a.mod(this.m);else if(a.compareTo(this.m)<0)return a;else{var b=nbi();a.copyTo(b);this.reduce(b);return b}}function barrettRevert(a){return a}
function barrettReduce(a){a.drShiftTo(this.m.t-1,this.r2);if(a.t>this.m.t+1)a.t=this.m.t+1,a.clamp();this.mu.multiplyUpperTo(this.r2,this.m.t+1,this.q3);for(this.m.multiplyLowerTo(this.q3,this.m.t+1,this.r2);a.compareTo(this.r2)<0;)a.dAddOffset(1,this.m.t+1);for(a.subTo(this.r2,a);a.compareTo(this.m)>=0;)a.subTo(this.m,a)}function barrettSqrTo(a,b){a.squareTo(b);this.reduce(b)}function barrettMulTo(a,b,c){a.multiplyTo(b,c);this.reduce(c)}Barrett.prototype.convert=barrettConvert;
Barrett.prototype.revert=barrettRevert;Barrett.prototype.reduce=barrettReduce;Barrett.prototype.mulTo=barrettMulTo;Barrett.prototype.sqrTo=barrettSqrTo;
function bnModPow(a,b){var c=a.bitLength(),d,e=nbv(1),g;if(c<=0)return e;else d=c<18?1:c<48?3:c<144?4:c<768?5:6;g=c<8?new Classic(b):b.isEven()?new Barrett(b):new Montgomery(b);var h=[],f=3,o=d-1,p=(1<<d)-1;h[1]=g.convert(this);if(d>1){c=nbi();for(g.sqrTo(h[1],c);f<=p;)h[f]=nbi(),g.mulTo(c,h[f-2],h[f]),f+=2}for(var q=a.t-1,n,k=!0,j=nbi(),c=nbits(a[q])-1;q>=0;){c>=o?n=a[q]>>c-o&p:(n=(a[q]&(1<<c+1)-1)<<o-c,q>0&&(n|=a[q-1]>>this.DB+c-o));for(f=d;(n&1)==0;)n>>=1,--f;if((c-=f)<0)c+=this.DB,--q;if(k)h[n].copyTo(e),
k=!1;else{for(;f>1;)g.sqrTo(e,j),g.sqrTo(j,e),f-=2;f>0?g.sqrTo(e,j):(f=e,e=j,j=f);g.mulTo(j,h[n],e)}for(;q>=0&&(a[q]&1<<c)==0;)g.sqrTo(e,j),f=e,e=j,j=f,--c<0&&(c=this.DB-1,--q)}return g.revert(e)}
function bnGCD(a){var b=this.s<0?this.negate():this.clone(),a=a.s<0?a.negate():a.clone();if(b.compareTo(a)<0)var c=b,b=a,a=c;var c=b.getLowestSetBit(),d=a.getLowestSetBit();if(d<0)return b;c<d&&(d=c);d>0&&(b.rShiftTo(d,b),a.rShiftTo(d,a));for(;b.signum()>0;)(c=b.getLowestSetBit())>0&&b.rShiftTo(c,b),(c=a.getLowestSetBit())>0&&a.rShiftTo(c,a),b.compareTo(a)>=0?(b.subTo(a,b),b.rShiftTo(1,b)):(a.subTo(b,a),a.rShiftTo(1,a));d>0&&a.lShiftTo(d,a);return a}
function bnpModInt(a){if(a<=0)return 0;var b=this.DV%a,c=this.s<0?a-1:0;if(this.t>0)if(b==0)c=this[0]%a;else for(var d=this.t-1;d>=0;--d)c=(b*c+this[d])%a;return c}
function bnModInverse(a){var b=a.isEven();if(this.isEven()&&b||a.signum()==0)return BigInteger.ZERO;for(var c=a.clone(),d=this.clone(),e=nbv(1),g=nbv(0),h=nbv(0),f=nbv(1);c.signum()!=0;){for(;c.isEven();){c.rShiftTo(1,c);if(b){if(!e.isEven()||!g.isEven())e.addTo(this,e),g.subTo(a,g);e.rShiftTo(1,e)}else g.isEven()||g.subTo(a,g);g.rShiftTo(1,g)}for(;d.isEven();){d.rShiftTo(1,d);if(b){if(!h.isEven()||!f.isEven())h.addTo(this,h),f.subTo(a,f);h.rShiftTo(1,h)}else f.isEven()||f.subTo(a,f);f.rShiftTo(1,
f)}c.compareTo(d)>=0?(c.subTo(d,c),b&&e.subTo(h,e),g.subTo(f,g)):(d.subTo(c,d),b&&h.subTo(e,h),f.subTo(g,f))}if(d.compareTo(BigInteger.ONE)!=0)return BigInteger.ZERO;if(f.compareTo(a)>=0)return f.subtract(a);if(f.signum()<0)f.addTo(a,f);else return f;return f.signum()<0?f.add(a):f}
var lowprimes=[2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59,61,67,71,73,79,83,89,97,101,103,107,109,113,127,131,137,139,149,151,157,163,167,173,179,181,191,193,197,199,211,223,227,229,233,239,241,251,257,263,269,271,277,281,283,293,307,311,313,317,331,337,347,349,353,359,367,373,379,383,389,397,401,409,419,421,431,433,439,443,449,457,461,463,467,479,487,491,499,503,509,521,523,541,547,557,563,569,571,577,587,593,599,601,607,613,617,619,631,641,643,647,653,659,661,673,677,683,691,701,709,719,727,
733,739,743,751,757,761,769,773,787,797,809,811,821,823,827,829,839,853,857,859,863,877,881,883,887,907,911,919,929,937,941,947,953,967,971,977,983,991,997],lplim=67108864/lowprimes[lowprimes.length-1];
function bnIsProbablePrime(a){var b,c=this.abs();if(c.t==1&&c[0]<=lowprimes[lowprimes.length-1]){for(b=0;b<lowprimes.length;++b)if(c[0]==lowprimes[b])return!0;return!1}if(c.isEven())return!1;for(b=1;b<lowprimes.length;){for(var d=lowprimes[b],e=b+1;e<lowprimes.length&&d<lplim;)d*=lowprimes[e++];for(d=c.modInt(d);b<e;)if(d%lowprimes[b++]==0)return!1}return c.millerRabin(a)}
function bnpMillerRabin(a){var b=this.subtract(BigInteger.ONE),c=b.getLowestSetBit();if(c<=0)return!1;var d=b.shiftRight(c),a=a+1>>1;if(a>lowprimes.length)a=lowprimes.length;for(var e=nbi(),g=0;g<a;++g){e.fromInt(lowprimes[Math.floor(Math.random()*lowprimes.length)]);var h=e.modPow(d,this);if(h.compareTo(BigInteger.ONE)!=0&&h.compareTo(b)!=0){for(var f=1;f++<c&&h.compareTo(b)!=0;)if(h=h.modPowInt(2,this),h.compareTo(BigInteger.ONE)==0)return!1;if(h.compareTo(b)!=0)return!1}}return!0}
BigInteger.prototype.chunkSize=bnpChunkSize;BigInteger.prototype.toRadix=bnpToRadix;BigInteger.prototype.fromRadix=bnpFromRadix;BigInteger.prototype.fromNumber=bnpFromNumber;BigInteger.prototype.bitwiseTo=bnpBitwiseTo;BigInteger.prototype.changeBit=bnpChangeBit;BigInteger.prototype.addTo=bnpAddTo;BigInteger.prototype.dMultiply=bnpDMultiply;BigInteger.prototype.dAddOffset=bnpDAddOffset;BigInteger.prototype.multiplyLowerTo=bnpMultiplyLowerTo;BigInteger.prototype.multiplyUpperTo=bnpMultiplyUpperTo;
BigInteger.prototype.modInt=bnpModInt;BigInteger.prototype.millerRabin=bnpMillerRabin;BigInteger.prototype.clone=bnClone;BigInteger.prototype.intValue=bnIntValue;BigInteger.prototype.byteValue=bnByteValue;BigInteger.prototype.shortValue=bnShortValue;BigInteger.prototype.signum=bnSigNum;BigInteger.prototype.toByteArray=bnToByteArray;BigInteger.prototype.equals=bnEquals;BigInteger.prototype.min=bnMin;BigInteger.prototype.max=bnMax;BigInteger.prototype.and=bnAnd;BigInteger.prototype.or=bnOr;
BigInteger.prototype.xor=bnXor;BigInteger.prototype.andNot=bnAndNot;BigInteger.prototype.not=bnNot;BigInteger.prototype.shiftLeft=bnShiftLeft;BigInteger.prototype.shiftRight=bnShiftRight;BigInteger.prototype.getLowestSetBit=bnGetLowestSetBit;BigInteger.prototype.bitCount=bnBitCount;BigInteger.prototype.testBit=bnTestBit;BigInteger.prototype.setBit=bnSetBit;BigInteger.prototype.clearBit=bnClearBit;BigInteger.prototype.flipBit=bnFlipBit;BigInteger.prototype.add=bnAdd;BigInteger.prototype.subtract=bnSubtract;
BigInteger.prototype.multiply=bnMultiply;BigInteger.prototype.divide=bnDivide;BigInteger.prototype.remainder=bnRemainder;BigInteger.prototype.divideAndRemainder=bnDivideAndRemainder;BigInteger.prototype.modPow=bnModPow;BigInteger.prototype.modInverse=bnModInverse;BigInteger.prototype.pow=bnPow;BigInteger.prototype.gcd=bnGCD;BigInteger.prototype.isProbablePrime=bnIsProbablePrime;BigInteger.prototype.square=bnSquare;
(function(a,b,c,d,e,g,h){function f(a){var b,d,e=this,g=a.length,f=0,h=e.i=e.j=e.m=0;e.S=[];e.c=[];for(g||(a=[g++]);f<c;)e.S[f]=f++;for(f=0;f<c;f++)b=e.S[f],h=h+b+a[f%g]&c-1,d=e.S[h],e.S[f]=d,e.S[h]=b;e.g=function(a){var b=e.S,d=e.i+1&c-1,g=b[d],f=e.j+g&c-1,h=b[f];b[d]=h;b[f]=g;for(var k=b[g+h&c-1];--a;)d=d+1&c-1,g=b[d],f=f+g&c-1,h=b[f],b[d]=h,b[f]=g,k=k*c+b[g+h&c-1];e.i=d;e.j=f;return k};e.g(c)}function o(a,b,c,d,e){c=[];e=typeof a;if(b&&e=="object")for(d in a)if(d.indexOf("S")<5)try{c.push(o(a[d],
b-1))}catch(g){}return c.length?c:a+(e!="string"?"\x00":"")}function p(a,b,d,e){a+="";for(e=d=0;e<a.length;e++){var g=b,f=e&c-1,h=(d^=b[e&c-1]*19)+a.charCodeAt(e);g[f]=h&c-1}a="";for(e in b)a+=String.fromCharCode(b[e]);return a}b.seedrandom=function(q,n){var k=[],j,q=p(o(n?[q,a]:arguments.length?q:[(new Date).getTime(),a,window],3),k);j=new f(k);p(j.S,a);b.random=function(){for(var a=j.g(d),b=h,f=0;a<e;)a=(a+f)*c,b*=c,f=j.g(1);for(;a>=g;)a/=2,b/=2,f>>>=1;return(a+f)/b};return q};h=b.pow(c,d);e=b.pow(2,
e);g=e*2;p(b.random(),a)})([],Math,256,6,52);function SeededRandom(){}function SRnextBytes(a){var b;for(b=0;b<a.length;b++)a[b]=Math.floor(Math.random()*256)}SeededRandom.prototype.nextBytes=SRnextBytes;function Arcfour(){this.j=this.i=0;this.S=[]}function ARC4init(a){var b,c,d;for(b=0;b<256;++b)this.S[b]=b;for(b=c=0;b<256;++b)c=c+this.S[b]+a[b%a.length]&255,d=this.S[b],this.S[b]=this.S[c],this.S[c]=d;this.j=this.i=0}
function ARC4next(){var a;this.i=this.i+1&255;this.j=this.j+this.S[this.i]&255;a=this.S[this.i];this.S[this.i]=this.S[this.j];this.S[this.j]=a;return this.S[a+this.S[this.i]&255]}Arcfour.prototype.init=ARC4init;Arcfour.prototype.next=ARC4next;function prng_newstate(){return new Arcfour}var rng_psize=256,rng_state,rng_pool,rng_pptr;
function rng_seed_int(a){rng_pool[rng_pptr++]^=a&255;rng_pool[rng_pptr++]^=a>>8&255;rng_pool[rng_pptr++]^=a>>16&255;rng_pool[rng_pptr++]^=a>>24&255;rng_pptr>=rng_psize&&(rng_pptr-=rng_psize)}function rng_seed_time(){rng_seed_int((new Date).getTime())}
if(rng_pool==null){rng_pool=[];rng_pptr=0;var t;if(navigator.appName=="Netscape"&&navigator.appVersion<"5"&&window.crypto){var z=window.crypto.random(32);for(t=0;t<z.length;++t)rng_pool[rng_pptr++]=z.charCodeAt(t)&255}for(;rng_pptr<rng_psize;)t=Math.floor(65536*Math.random()),rng_pool[rng_pptr++]=t>>>8,rng_pool[rng_pptr++]=t&255;rng_pptr=0;rng_seed_time()}
function rng_get_byte(){if(rng_state==null){rng_seed_time();rng_state=prng_newstate();rng_state.init(rng_pool);for(rng_pptr=0;rng_pptr<rng_pool.length;++rng_pptr)rng_pool[rng_pptr]=0;rng_pptr=0}return rng_state.next()}function rng_get_bytes(a){var b;for(b=0;b<a.length;++b)a[b]=rng_get_byte()}function SecureRandom(){}SecureRandom.prototype.nextBytes=rng_get_bytes;
function SHA256(a){function b(a,b){var c=(a&65535)+(b&65535);return(a>>16)+(b>>16)+(c>>16)<<16|c&65535}function c(a,b){return a>>>b|a<<32-b}a=function(a){for(var a=a.replace(/\r\n/g,"\n"),b="",c=0;c<a.length;c++){var h=a.charCodeAt(c);h<128?b+=String.fromCharCode(h):(h>127&&h<2048?b+=String.fromCharCode(h>>6|192):(b+=String.fromCharCode(h>>12|224),b+=String.fromCharCode(h>>6&63|128)),b+=String.fromCharCode(h&63|128))}return b}(a);return function(a){for(var b="",c=0;c<a.length*4;c++)b+="0123456789abcdef".charAt(a[c>>
2]>>(3-c%4)*8+4&15)+"0123456789abcdef".charAt(a[c>>2]>>(3-c%4)*8&15);return b}(function(a,e){var g=[1116352408,1899447441,3049323471,3921009573,961987163,1508970993,2453635748,2870763221,3624381080,310598401,607225278,1426881987,1925078388,2162078206,2614888103,3248222580,3835390401,4022224774,264347078,604807628,770255983,1249150122,1555081692,1996064986,2554220882,2821834349,2952996808,3210313671,3336571891,3584528711,113926993,338241895,666307205,773529912,1294757372,1396182291,1695183700,1986661051,
2177026350,2456956037,2730485921,2820302411,3259730800,3345764771,3516065817,3600352804,4094571909,275423344,430227734,506948616,659060556,883997877,958139571,1322822218,1537002063,1747873779,1955562222,2024104815,2227730452,2361852424,2428436474,2756734187,3204031479,3329325298],h=[1779033703,3144134277,1013904242,2773480762,1359893119,2600822924,528734635,1541459225],f=Array(64),o,p,q,n,k,j,l,m,s,r,u,w;a[e>>5]|=128<<24-e%32;a[(e+64>>9<<4)+15]=e;for(s=0;s<a.length;s+=16){o=h[0];p=h[1];q=h[2];n=h[3];
k=h[4];j=h[5];l=h[6];m=h[7];for(r=0;r<64;r++)f[r]=r<16?a[r+s]:b(b(b(c(f[r-2],17)^c(f[r-2],19)^f[r-2]>>>10,f[r-7]),c(f[r-15],7)^c(f[r-15],18)^f[r-15]>>>3),f[r-16]),u=b(b(b(b(m,c(k,6)^c(k,11)^c(k,25)),k&j^~k&l),g[r]),f[r]),w=b(c(o,2)^c(o,13)^c(o,22),o&p^o&q^p&q),m=l,l=j,j=k,k=b(n,u),n=q,q=p,p=o,o=b(u,w);h[0]=b(o,h[0]);h[1]=b(p,h[1]);h[2]=b(q,h[2]);h[3]=b(n,h[3]);h[4]=b(k,h[4]);h[5]=b(j,h[5]);h[6]=b(l,h[6]);h[7]=b(m,h[7])}return h}(function(a){for(var b=[],c=0;c<a.length*8;c+=8)b[c>>5]|=(a.charCodeAt(c/
8)&255)<<24-c%32;return b}(a),a.length*8))}var sha256={hex:function(a){return SHA256(a)}};
function SHA1(a){function b(a,b){return a<<b|a>>>32-b}function c(a){var b="",c,d;for(c=7;c>=0;c--)d=a>>>c*4&15,b+=d.toString(16);return b}var d,e,g=Array(80),h=1732584193,f=4023233417,o=2562383102,p=271733878,q=3285377520,n,k,j,l,m,a=function(a){for(var a=a.replace(/\r\n/g,"\n"),b="",c=0;c<a.length;c++){var d=a.charCodeAt(c);d<128?b+=String.fromCharCode(d):(d>127&&d<2048?b+=String.fromCharCode(d>>6|192):(b+=String.fromCharCode(d>>12|224),b+=String.fromCharCode(d>>6&63|128)),b+=String.fromCharCode(d&
63|128))}return b}(a);n=a.length;var s=[];for(d=0;d<n-3;d+=4)e=a.charCodeAt(d)<<24|a.charCodeAt(d+1)<<16|a.charCodeAt(d+2)<<8|a.charCodeAt(d+3),s.push(e);switch(n%4){case 0:d=2147483648;break;case 1:d=a.charCodeAt(n-1)<<24|8388608;break;case 2:d=a.charCodeAt(n-2)<<24|a.charCodeAt(n-1)<<16|32768;break;case 3:d=a.charCodeAt(n-3)<<24|a.charCodeAt(n-2)<<16|a.charCodeAt(n-1)<<8|128}for(s.push(d);s.length%16!=14;)s.push(0);s.push(n>>>29);s.push(n<<3&4294967295);for(a=0;a<s.length;a+=16){for(d=0;d<16;d++)g[d]=
s[a+d];for(d=16;d<=79;d++)g[d]=b(g[d-3]^g[d-8]^g[d-14]^g[d-16],1);e=h;n=f;k=o;j=p;l=q;for(d=0;d<=19;d++)m=b(e,5)+(n&k|~n&j)+l+g[d]+1518500249&4294967295,l=j,j=k,k=b(n,30),n=e,e=m;for(d=20;d<=39;d++)m=b(e,5)+(n^k^j)+l+g[d]+1859775393&4294967295,l=j,j=k,k=b(n,30),n=e,e=m;for(d=40;d<=59;d++)m=b(e,5)+(n&k|n&j|k&j)+l+g[d]+2400959708&4294967295,l=j,j=k,k=b(n,30),n=e,e=m;for(d=60;d<=79;d++)m=b(e,5)+(n^k^j)+l+g[d]+3395469782&4294967295,l=j,j=k,k=b(n,30),n=e,e=m;h=h+e&4294967295;f=f+n&4294967295;o=o+k&4294967295;
p=p+j&4294967295;q=q+l&4294967295}m=c(h)+c(f)+c(o)+c(p)+c(q);return m.toLowerCase()}
var sha1={hex:function(a){return SHA1(a)}},MD5=function(a){function b(a,b){var c,d,e,f,g;e=a&2147483648;f=b&2147483648;c=a&1073741824;d=b&1073741824;g=(a&1073741823)+(b&1073741823);return c&d?g^2147483648^e^f:c|d?g&1073741824?g^3221225472^e^f:g^1073741824^e^f:g^e^f}function c(a,c,d,e,f,g,h){a=b(a,b(b(c&d|~c&e,f),h));return b(a<<g|a>>>32-g,c)}function d(a,c,d,e,f,g,h){a=b(a,b(b(c&e|d&~e,f),h));return b(a<<g|a>>>32-g,c)}function e(a,c,d,e,f,g,h){a=b(a,b(b(c^d^e,f),h));return b(a<<g|a>>>32-g,c)}function g(a,
c,d,e,f,g,h){a=b(a,b(b(d^(c|~e),f),h));return b(a<<g|a>>>32-g,c)}function h(a){var b="",c="",d;for(d=0;d<=3;d++)c=a>>>d*8&255,c="0"+c.toString(16),b+=c.substr(c.length-2,2);return b}var f=[],o,p,q,n,k,j,l,m,a=function(a){for(var a=a.replace(/\r\n/g,"\n"),b="",c=0;c<a.length;c++){var d=a.charCodeAt(c);d<128?b+=String.fromCharCode(d):(d>127&&d<2048?b+=String.fromCharCode(d>>6|192):(b+=String.fromCharCode(d>>12|224),b+=String.fromCharCode(d>>6&63|128)),b+=String.fromCharCode(d&63|128))}return b}(a),
f=function(a){var b,c=a.length;b=c+8;for(var d=((b-b%64)/64+1)*16,e=Array(d-1),f=0,g=0;g<c;)b=(g-g%4)/4,f=g%4*8,e[b]|=a.charCodeAt(g)<<f,g++;e[(g-g%4)/4]|=128<<g%4*8;e[d-2]=c<<3;e[d-1]=c>>>29;return e}(a);k=1732584193;j=4023233417;l=2562383102;m=271733878;for(a=0;a<f.length;a+=16)o=k,p=j,q=l,n=m,k=c(k,j,l,m,f[a+0],7,3614090360),m=c(m,k,j,l,f[a+1],12,3905402710),l=c(l,m,k,j,f[a+2],17,606105819),j=c(j,l,m,k,f[a+3],22,3250441966),k=c(k,j,l,m,f[a+4],7,4118548399),m=c(m,k,j,l,f[a+5],12,1200080426),l=c(l,
m,k,j,f[a+6],17,2821735955),j=c(j,l,m,k,f[a+7],22,4249261313),k=c(k,j,l,m,f[a+8],7,1770035416),m=c(m,k,j,l,f[a+9],12,2336552879),l=c(l,m,k,j,f[a+10],17,4294925233),j=c(j,l,m,k,f[a+11],22,2304563134),k=c(k,j,l,m,f[a+12],7,1804603682),m=c(m,k,j,l,f[a+13],12,4254626195),l=c(l,m,k,j,f[a+14],17,2792965006),j=c(j,l,m,k,f[a+15],22,1236535329),k=d(k,j,l,m,f[a+1],5,4129170786),m=d(m,k,j,l,f[a+6],9,3225465664),l=d(l,m,k,j,f[a+11],14,643717713),j=d(j,l,m,k,f[a+0],20,3921069994),k=d(k,j,l,m,f[a+5],5,3593408605),
m=d(m,k,j,l,f[a+10],9,38016083),l=d(l,m,k,j,f[a+15],14,3634488961),j=d(j,l,m,k,f[a+4],20,3889429448),k=d(k,j,l,m,f[a+9],5,568446438),m=d(m,k,j,l,f[a+14],9,3275163606),l=d(l,m,k,j,f[a+3],14,4107603335),j=d(j,l,m,k,f[a+8],20,1163531501),k=d(k,j,l,m,f[a+13],5,2850285829),m=d(m,k,j,l,f[a+2],9,4243563512),l=d(l,m,k,j,f[a+7],14,1735328473),j=d(j,l,m,k,f[a+12],20,2368359562),k=e(k,j,l,m,f[a+5],4,4294588738),m=e(m,k,j,l,f[a+8],11,2272392833),l=e(l,m,k,j,f[a+11],16,1839030562),j=e(j,l,m,k,f[a+14],23,4259657740),
k=e(k,j,l,m,f[a+1],4,2763975236),m=e(m,k,j,l,f[a+4],11,1272893353),l=e(l,m,k,j,f[a+7],16,4139469664),j=e(j,l,m,k,f[a+10],23,3200236656),k=e(k,j,l,m,f[a+13],4,681279174),m=e(m,k,j,l,f[a+0],11,3936430074),l=e(l,m,k,j,f[a+3],16,3572445317),j=e(j,l,m,k,f[a+6],23,76029189),k=e(k,j,l,m,f[a+9],4,3654602809),m=e(m,k,j,l,f[a+12],11,3873151461),l=e(l,m,k,j,f[a+15],16,530742520),j=e(j,l,m,k,f[a+2],23,3299628645),k=g(k,j,l,m,f[a+0],6,4096336452),m=g(m,k,j,l,f[a+7],10,1126891415),l=g(l,m,k,j,f[a+14],15,2878612391),
j=g(j,l,m,k,f[a+5],21,4237533241),k=g(k,j,l,m,f[a+12],6,1700485571),m=g(m,k,j,l,f[a+3],10,2399980690),l=g(l,m,k,j,f[a+10],15,4293915773),j=g(j,l,m,k,f[a+1],21,2240044497),k=g(k,j,l,m,f[a+8],6,1873313359),m=g(m,k,j,l,f[a+15],10,4264355552),l=g(l,m,k,j,f[a+6],15,2734768916),j=g(j,l,m,k,f[a+13],21,1309151649),k=g(k,j,l,m,f[a+4],6,4149444226),m=g(m,k,j,l,f[a+11],10,3174756917),l=g(l,m,k,j,f[a+2],15,718787259),j=g(j,l,m,k,f[a+9],21,3951481745),k=b(k,o),j=b(j,p),l=b(l,q),m=b(m,n);return(h(k)+h(j)+h(l)+
h(m)).toLowerCase()};function parseBigInt(a,b){return new BigInteger(a,b)}function linebrk(a,b){for(var c="",d=0;d+b<a.length;)c+=a.substring(d,d+b)+"\n",d+=b;return c+a.substring(d,a.length)}function byte2Hex(a){return a<16?"0"+a.toString(16):a.toString(16)}
function pkcs1pad2(a,b){if(b<a.length+11)throw"Message too long for RSA (n="+b+", l="+a.length+")";for(var c=[],d=a.length-1;d>=0&&b>0;){var e=a.charCodeAt(d--);e<128?c[--b]=e:e>127&&e<2048?(c[--b]=e&63|128,c[--b]=e>>6|192):(c[--b]=e&63|128,c[--b]=e>>6&63|128,c[--b]=e>>12|224)}c[--b]=0;d=new SecureRandom;for(e=[];b>2;){for(e[0]=0;e[0]==0;)d.nextBytes(e);c[--b]=e[0]}c[--b]=2;c[--b]=0;return new BigInteger(c)}
function RSAKey(){this.n=null;this.e=0;this.coeff=this.dmq1=this.dmp1=this.q=this.p=this.d=null}function RSASetPublic(a,b){a!=null&&b!=null&&a.length>0&&b.length>0?(this.n=parseBigInt(a,16),this.e=parseInt(b,16)):alert("Invalid RSA public key")}function RSADoPublic(a){return a.modPowInt(this.e,this.n)}function RSAEncrypt(a){a=pkcs1pad2(a,this.n.bitLength()+7>>3);if(a==null)return null;a=this.doPublic(a);if(a==null)return null;a=a.toString(16);return(a.length&1)==0?a:"0"+a}
RSAKey.prototype.doPublic=RSADoPublic;RSAKey.prototype.setPublic=RSASetPublic;RSAKey.prototype.encrypt=RSAEncrypt;function pkcs1unpad2(a,b){for(var c=a.toByteArray(),d=0;d<c.length&&c[d]==0;)++d;if(c.length-d!=b-1||c[d]!=2)return null;for(++d;c[d]!=0;)if(++d>=c.length)return null;for(var e="";++d<c.length;){var g=c[d]&255;g<128?e+=String.fromCharCode(g):g>191&&g<224?(e+=String.fromCharCode((g&31)<<6|c[d+1]&63),++d):(e+=String.fromCharCode((g&15)<<12|(c[d+1]&63)<<6|c[d+2]&63),d+=2)}return e}
function RSASetPrivate(a,b,c){a!=null&&b!=null&&a.length>0&&b.length>0?(this.n=parseBigInt(a,16),this.e=parseInt(b,16),this.d=parseBigInt(c,16)):alert("Invalid RSA private key")}
function RSASetPrivateEx(a,b,c,d,e,g,h,f){a!=null&&b!=null&&a.length>0&&b.length>0?(this.n=parseBigInt(a,16),this.e=parseInt(b,16),this.d=parseBigInt(c,16),this.p=parseBigInt(d,16),this.q=parseBigInt(e,16),this.dmp1=parseBigInt(g,16),this.dmq1=parseBigInt(h,16),this.coeff=parseBigInt(f,16)):alert("Invalid RSA private key")}
function RSAGenerate(a,b){var c=new SeededRandom,d=a>>1;this.e=parseInt(b,16);for(var e=new BigInteger(b,16);;){for(;;)if(this.p=new BigInteger(a-d,1,c),this.p.subtract(BigInteger.ONE).gcd(e).compareTo(BigInteger.ONE)==0&&this.p.isProbablePrime(10))break;for(;;)if(this.q=new BigInteger(d,1,c),this.q.subtract(BigInteger.ONE).gcd(e).compareTo(BigInteger.ONE)==0&&this.q.isProbablePrime(10))break;if(this.p.compareTo(this.q)<=0){var g=this.p;this.p=this.q;this.q=g}var g=this.p.subtract(BigInteger.ONE),
h=this.q.subtract(BigInteger.ONE),f=g.multiply(h);if(f.gcd(e).compareTo(BigInteger.ONE)==0){this.n=this.p.multiply(this.q);this.d=e.modInverse(f);this.dmp1=this.d.mod(g);this.dmq1=this.d.mod(h);this.coeff=this.q.modInverse(this.p);break}}}
function RSADoPrivate(a){if(this.p==null||this.q==null)return a.modPow(this.d,this.n);for(var b=a.mod(this.p).modPow(this.dmp1,this.p),a=a.mod(this.q).modPow(this.dmq1,this.q);b.compareTo(a)<0;)b=b.add(this.p);return b.subtract(a).multiply(this.coeff).mod(this.p).multiply(this.q).add(a)}function RSADecrypt(a){a=this.doPrivate(parseBigInt(a,16));return a==null?null:pkcs1unpad2(a,this.n.bitLength()+7>>3)}RSAKey.prototype.doPrivate=RSADoPrivate;RSAKey.prototype.setPrivate=RSASetPrivate;
RSAKey.prototype.setPrivateEx=RSASetPrivateEx;RSAKey.prototype.generate=RSAGenerate;RSAKey.prototype.decrypt=RSADecrypt;var _RSASIGN_DIHEAD=[];_RSASIGN_DIHEAD.sha1="3021300906052b0e03021a05000414";_RSASIGN_DIHEAD.sha256="3031300d060960864801650304020105000420";var _RSASIGN_HASHHEXFUNC=[];_RSASIGN_HASHHEXFUNC.sha1=sha1.hex;_RSASIGN_HASHHEXFUNC.sha256=sha256.hex;
function _rsasign_getHexPaddedDigestInfoForString(a,b,c){b/=4;for(var a=(0,_RSASIGN_HASHHEXFUNC[c])(a),c="00"+_RSASIGN_DIHEAD[c]+a,a="",b=b-4-c.length,d=0;d<b;d+=2)a+="ff";return sPaddedMessageHex="0001"+a+c}function _rsasign_signString(a,b){var c=_rsasign_getHexPaddedDigestInfoForString(a,this.n.bitLength(),b);return this.doPrivate(parseBigInt(c,16)).toString(16)}
function _rsasign_signStringWithSHA1(a){a=_rsasign_getHexPaddedDigestInfoForString(a,this.n.bitLength(),"sha1");return this.doPrivate(parseBigInt(a,16)).toString(16)}function _rsasign_signStringWithSHA256(a){a=_rsasign_getHexPaddedDigestInfoForString(a,this.n.bitLength(),"sha256");return this.doPrivate(parseBigInt(a,16)).toString(16)}function _rsasign_getDecryptSignatureBI(a,b,c){var d=new RSAKey;d.setPublic(b,c);return d.doPublic(a)}
function _rsasign_getHexDigestInfoFromSig(a,b,c){return _rsasign_getDecryptSignatureBI(a,b,c).toString(16).replace(/^1f+00/,"")}function _rsasign_getAlgNameAndHashFromHexDisgestInfo(a){for(var b in _RSASIGN_DIHEAD){var c=_RSASIGN_DIHEAD[b],d=c.length;if(a.substring(0,d)==c)return[b,a.substring(d)]}return[]}
function _rsasign_verifySignatureWithArgs(a,b,c,d){b=_rsasign_getHexDigestInfoFromSig(b,c,d);c=_rsasign_getAlgNameAndHashFromHexDisgestInfo(b);if(c.length==0)return!1;b=c[1];a=(0,_RSASIGN_HASHHEXFUNC[c[0]])(a);return b==a}function _rsasign_verifyHexSignatureForMessage(a,b){var c=parseBigInt(a,16);return _rsasign_verifySignatureWithArgs(b,c,this.n.toString(16),this.e.toString(16))}
function _rsasign_verifyString(a,b){var b=b.replace(/[ \n]+/g,""),c=this.doPublic(parseBigInt(b,16)).toString(16).replace(/^1f+00/,""),d=_rsasign_getAlgNameAndHashFromHexDisgestInfo(c);if(d.length==0)return!1;c=d[1];d=(0,_RSASIGN_HASHHEXFUNC[d[0]])(a);return c==d}RSAKey.prototype.signString=_rsasign_signString;RSAKey.prototype.signStringWithSHA1=_rsasign_signStringWithSHA1;RSAKey.prototype.signStringWithSHA256=_rsasign_signStringWithSHA256;RSAKey.prototype.verifyString=_rsasign_verifyString;
RSAKey.prototype.verifyHexSignatureForMessage=_rsasign_verifyHexSignatureForMessage;
var aes=function(){var a={Sbox:[99,124,119,123,242,107,111,197,48,1,103,43,254,215,171,118,202,130,201,125,250,89,71,240,173,212,162,175,156,164,114,192,183,253,147,38,54,63,247,204,52,165,229,241,113,216,49,21,4,199,35,195,24,150,5,154,7,18,128,226,235,39,178,117,9,131,44,26,27,110,90,160,82,59,214,179,41,227,47,132,83,209,0,237,32,252,177,91,106,203,190,57,74,76,88,207,208,239,170,251,67,77,51,133,69,249,2,127,80,60,159,168,81,163,64,143,146,157,56,245,188,182,218,33,16,255,243,210,205,12,19,236,
95,151,68,23,196,167,126,61,100,93,25,115,96,129,79,220,34,42,144,136,70,238,184,20,222,94,11,219,224,50,58,10,73,6,36,92,194,211,172,98,145,149,228,121,231,200,55,109,141,213,78,169,108,86,244,234,101,122,174,8,186,120,37,46,28,166,180,198,232,221,116,31,75,189,139,138,112,62,181,102,72,3,246,14,97,53,87,185,134,193,29,158,225,248,152,17,105,217,142,148,155,30,135,233,206,85,40,223,140,161,137,13,191,230,66,104,65,153,45,15,176,84,187,22],ShiftRowTab:[0,5,10,15,4,9,14,3,8,13,2,7,12,1,6,11]};a.Init=
function(){a.Sbox_Inv=Array(256);for(var b=0;b<256;b++)a.Sbox_Inv[a.Sbox[b]]=b;a.ShiftRowTab_Inv=Array(16);for(b=0;b<16;b++)a.ShiftRowTab_Inv[a.ShiftRowTab[b]]=b;a.xtime=Array(256);for(b=0;b<128;b++)a.xtime[b]=b<<1,a.xtime[128+b]=b<<1^27};a.Done=function(){delete a.Sbox_Inv;delete a.ShiftRowTab_Inv;delete a.xtime};a.ExpandKey=function(b){var c=b.length,d,e=1;switch(c){case 16:d=176;break;case 24:d=208;break;case 32:d=240;break;default:alert("my.ExpandKey: Only key lengths of 16, 24 or 32 bytes allowed!")}for(var g=
c;g<d;g+=4){var h=b.slice(g-4,g);if(g%c==0){if(h=[a.Sbox[h[1]]^e,a.Sbox[h[2]],a.Sbox[h[3]],a.Sbox[h[0]]],(e<<=1)>=256)e^=283}else c>24&&g%c==16&&(h=[a.Sbox[h[0]],a.Sbox[h[1]],a.Sbox[h[2]],a.Sbox[h[3]]]);for(var f=0;f<4;f++)b[g+f]=b[g+f-c]^h[f]}};a.Encrypt=function(b,c){var d=c.length;a.AddRoundKey(b,c.slice(0,16));for(var e=16;e<d-16;e+=16)a.SubBytes(b,a.Sbox),a.ShiftRows(b,a.ShiftRowTab),a.MixColumns(b),a.AddRoundKey(b,c.slice(e,e+16));a.SubBytes(b,a.Sbox);a.ShiftRows(b,a.ShiftRowTab);a.AddRoundKey(b,
c.slice(e,d))};a.Decrypt=function(b,c){var d=c.length;a.AddRoundKey(b,c.slice(d-16,d));a.ShiftRows(b,a.ShiftRowTab_Inv);a.SubBytes(b,a.Sbox_Inv);for(d-=32;d>=16;d-=16)a.AddRoundKey(b,c.slice(d,d+16)),a.MixColumns_Inv(b),a.ShiftRows(b,a.ShiftRowTab_Inv),a.SubBytes(b,a.Sbox_Inv);a.AddRoundKey(b,c.slice(0,16))};a.SubBytes=function(a,c){for(var d=0;d<16;d++)a[d]=c[a[d]]};a.AddRoundKey=function(a,c){for(var d=0;d<16;d++)a[d]^=c[d]};a.ShiftRows=function(a,c){for(var d=[].concat(a),e=0;e<16;e++)a[e]=d[c[e]]};
a.MixColumns=function(b){for(var c=0;c<16;c+=4){var d=b[c+0],e=b[c+1],g=b[c+2],h=b[c+3],f=d^e^g^h;b[c+0]^=f^a.xtime[d^e];b[c+1]^=f^a.xtime[e^g];b[c+2]^=f^a.xtime[g^h];b[c+3]^=f^a.xtime[h^d]}};a.MixColumns_Inv=function(b){for(var c=0;c<16;c+=4){var d=b[c+0],e=b[c+1],g=b[c+2],h=b[c+3],f=d^e^g^h,o=a.xtime[f],p=a.xtime[a.xtime[o^d^g]]^f;f^=a.xtime[a.xtime[o^e^h]];b[c+0]^=p^a.xtime[d^e];b[c+1]^=f^a.xtime[e^g];b[c+2]^=p^a.xtime[g^h];b[c+3]^=f^a.xtime[h^d]}};return a}(),cryptico=function(){var a={};aes.Init();
a.b256to64=function(a){var c,d,e,g="",h=0,f=0,o=a.length;for(e=0;e<o;e++)d=a.charCodeAt(e),f==0?(g+="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".charAt(d>>2&63),c=(d&3)<<4):f==1?(g+="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".charAt(c|d>>4&15),c=(d&15)<<2):f==2&&(g+="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".charAt(c|d>>6&3),h+=1,g+="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".charAt(d&63)),h+=1,f+=1,f==3&&
(f=0);f>0&&(g+="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".charAt(c),g+="=");f==1&&(g+="=");return g};a.b64to256=function(a){var c,d,e="",g=0,h=0,f=a.length;for(d=0;d<f;d++)c="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".indexOf(a.charAt(d)),c>=0&&(g&&(e+=String.fromCharCode(h|c>>6-g&255)),g=g+2&7,h=c<<g&255);return e};a.b16to64=function(a){var c,d,e="";a.length%2==1&&(a="0"+a);for(c=0;c+3<=a.length;c+=3)d=parseInt(a.substring(c,c+3),16),e+="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".charAt(d>>
6)+"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".charAt(d&63);c+1==a.length?(d=parseInt(a.substring(c,c+1),16),e+="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".charAt(d<<2)):c+2==a.length&&(d=parseInt(a.substring(c,c+2),16),e+="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".charAt(d>>2)+"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".charAt((d&3)<<4));for(;(e.length&3)>0;)e+="=";return e};a.b64to16=function(a){var c="",
d,e=0,g;for(d=0;d<a.length;++d){if(a.charAt(d)=="=")break;v="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".indexOf(a.charAt(d));v<0||(e==0?(c+=int2char(v>>2),g=v&3,e=1):e==1?(c+=int2char(g<<2|v>>4),g=v&15,e=2):e==2?(c+=int2char(g),c+=int2char(v>>2),g=v&3,e=3):(c+=int2char(g<<2|v>>4),c+=int2char(v&15),e=0))}e==1&&(c+=int2char(g<<2));return c};a.string2bytes=function(a){for(var c=[],d=0;d<a.length;d++)c.push(a.charCodeAt(d));return c};a.bytes2string=function(a){for(var c="",d=0;d<
a.length;d++)c+=String.fromCharCode(a[d]);return c};a.blockXOR=function(a,c){for(var d=Array(16),e=0;e<16;e++)d[e]=a[e]^c[e];return d};a.blockIV=function(){var a=new SecureRandom,c=Array(16);a.nextBytes(c);return c};a.pad16=function(a){var c=a.slice(0),d=(16-a.length%16)%16;for(i=a.length;i<a.length+d;i++)c.push(0);return c};a.depad=function(a){for(a=a.slice(0);a[a.length-1]==0;)a=a.slice(0,a.length-1);return a};a.encryptAESCBC=function(b,c){var d=c.slice(0);aes.ExpandKey(d);for(var e=a.string2bytes(b),
e=a.pad16(e),g=a.blockIV(),h=0;h<e.length/16;h++){var f=e.slice(h*16,h*16+16),o=g.slice(h*16,h*16+16),f=a.blockXOR(o,f);aes.Encrypt(f,d);g=g.concat(f)}d=a.bytes2string(g);return a.b256to64(d)};a.decryptAESCBC=function(b,c){var d=c.slice(0);aes.ExpandKey(d);for(var b=a.b64to256(b),e=a.string2bytes(b),g=[],h=1;h<e.length/16;h++){var f=e.slice(h*16,h*16+16),o=e.slice((h-1)*16,(h-1)*16+16);aes.Decrypt(f,d);f=a.blockXOR(o,f);g=g.concat(f)}g=a.depad(g);return a.bytes2string(g)};a.wrap60=function(a){for(var c=
"",d=0;d<a.length;d++)d%60==0&&d!=0&&(c+="\n"),c+=a[d];return c};a.generateAESKey=function(){var a=Array(16);(new SecureRandom).nextBytes(a);return a};a.generateRSAKey=function(a,c){Math.seedrandom(sha256.hex(a));var d=new RSAKey;d.generate(c,"10001");return d};a.publicKeyString=function(b){return pubkey=b.n.toString(16)};a.publicKeyID=function(a){return MD5(a)};a.publicKeyFromString=function(b){var b=b.split("|")[0],c=new RSAKey;c.setPublic(b,"10001");return c};a.encrypt=function(b,
c,d){var e="";try{var h=a.publicKeyFromString(c);e+=h.encrypt(b)+"?"}catch(f){return{status:"Invalid public key"}};return{status:"success",cipher:e}};a.decrypt=function(b,c){var d=b.split("?"),e=c.decrypt(d[0]);return{status:"success",plaintext:e,signature:"unsigned"}};return a}();

View File

@@ -0,0 +1,35 @@
/*
CryptoJS v3.1.2
code.google.com/p/crypto-js
(c) 2009-2013 by Jeff Mott. All rights reserved.
code.google.com/p/crypto-js/wiki/License
*/
var CryptoJS=CryptoJS||function(u,p){var d={},l=d.lib={},s=function(){},t=l.Base={extend:function(a){s.prototype=this;var c=new s;a&&c.mixIn(a);c.hasOwnProperty("init")||(c.init=function(){c.$super.init.apply(this,arguments)});c.init.prototype=c;c.$super=this;return c},create:function(){var a=this.extend();a.init.apply(a,arguments);return a},init:function(){},mixIn:function(a){for(var c in a)a.hasOwnProperty(c)&&(this[c]=a[c]);a.hasOwnProperty("toString")&&(this.toString=a.toString)},clone:function(){return this.init.prototype.extend(this)}},
r=l.WordArray=t.extend({init:function(a,c){a=this.words=a||[];this.sigBytes=c!=p?c:4*a.length},toString:function(a){return(a||v).stringify(this)},concat:function(a){var c=this.words,e=a.words,j=this.sigBytes;a=a.sigBytes;this.clamp();if(j%4)for(var k=0;k<a;k++)c[j+k>>>2]|=(e[k>>>2]>>>24-8*(k%4)&255)<<24-8*((j+k)%4);else if(65535<e.length)for(k=0;k<a;k+=4)c[j+k>>>2]=e[k>>>2];else c.push.apply(c,e);this.sigBytes+=a;return this},clamp:function(){var a=this.words,c=this.sigBytes;a[c>>>2]&=4294967295<<
32-8*(c%4);a.length=u.ceil(c/4)},clone:function(){var a=t.clone.call(this);a.words=this.words.slice(0);return a},random:function(a){for(var c=[],e=0;e<a;e+=4)c.push(4294967296*u.random()|0);return new r.init(c,a)}}),w=d.enc={},v=w.Hex={stringify:function(a){var c=a.words;a=a.sigBytes;for(var e=[],j=0;j<a;j++){var k=c[j>>>2]>>>24-8*(j%4)&255;e.push((k>>>4).toString(16));e.push((k&15).toString(16))}return e.join("")},parse:function(a){for(var c=a.length,e=[],j=0;j<c;j+=2)e[j>>>3]|=parseInt(a.substr(j,
2),16)<<24-4*(j%8);return new r.init(e,c/2)}},b=w.Latin1={stringify:function(a){var c=a.words;a=a.sigBytes;for(var e=[],j=0;j<a;j++)e.push(String.fromCharCode(c[j>>>2]>>>24-8*(j%4)&255));return e.join("")},parse:function(a){for(var c=a.length,e=[],j=0;j<c;j++)e[j>>>2]|=(a.charCodeAt(j)&255)<<24-8*(j%4);return new r.init(e,c)}},x=w.Utf8={stringify:function(a){try{return decodeURIComponent(escape(b.stringify(a)))}catch(c){throw Error("Malformed UTF-8 data");}},parse:function(a){return b.parse(unescape(encodeURIComponent(a)))}},
q=l.BufferedBlockAlgorithm=t.extend({reset:function(){this._data=new r.init;this._nDataBytes=0},_append:function(a){"string"==typeof a&&(a=x.parse(a));this._data.concat(a);this._nDataBytes+=a.sigBytes},_process:function(a){var c=this._data,e=c.words,j=c.sigBytes,k=this.blockSize,b=j/(4*k),b=a?u.ceil(b):u.max((b|0)-this._minBufferSize,0);a=b*k;j=u.min(4*a,j);if(a){for(var q=0;q<a;q+=k)this._doProcessBlock(e,q);q=e.splice(0,a);c.sigBytes-=j}return new r.init(q,j)},clone:function(){var a=t.clone.call(this);
a._data=this._data.clone();return a},_minBufferSize:0});l.Hasher=q.extend({cfg:t.extend(),init:function(a){this.cfg=this.cfg.extend(a);this.reset()},reset:function(){q.reset.call(this);this._doReset()},update:function(a){this._append(a);this._process();return this},finalize:function(a){a&&this._append(a);return this._doFinalize()},blockSize:16,_createHelper:function(a){return function(b,e){return(new a.init(e)).finalize(b)}},_createHmacHelper:function(a){return function(b,e){return(new n.HMAC.init(a,
e)).finalize(b)}}});var n=d.algo={};return d}(Math);
(function(){var u=CryptoJS,p=u.lib.WordArray;u.enc.Base64={stringify:function(d){var l=d.words,p=d.sigBytes,t=this._map;d.clamp();d=[];for(var r=0;r<p;r+=3)for(var w=(l[r>>>2]>>>24-8*(r%4)&255)<<16|(l[r+1>>>2]>>>24-8*((r+1)%4)&255)<<8|l[r+2>>>2]>>>24-8*((r+2)%4)&255,v=0;4>v&&r+0.75*v<p;v++)d.push(t.charAt(w>>>6*(3-v)&63));if(l=t.charAt(64))for(;d.length%4;)d.push(l);return d.join("")},parse:function(d){var l=d.length,s=this._map,t=s.charAt(64);t&&(t=d.indexOf(t),-1!=t&&(l=t));for(var t=[],r=0,w=0;w<
l;w++)if(w%4){var v=s.indexOf(d.charAt(w-1))<<2*(w%4),b=s.indexOf(d.charAt(w))>>>6-2*(w%4);t[r>>>2]|=(v|b)<<24-8*(r%4);r++}return p.create(t,r)},_map:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="}})();
(function(u){function p(b,n,a,c,e,j,k){b=b+(n&a|~n&c)+e+k;return(b<<j|b>>>32-j)+n}function d(b,n,a,c,e,j,k){b=b+(n&c|a&~c)+e+k;return(b<<j|b>>>32-j)+n}function l(b,n,a,c,e,j,k){b=b+(n^a^c)+e+k;return(b<<j|b>>>32-j)+n}function s(b,n,a,c,e,j,k){b=b+(a^(n|~c))+e+k;return(b<<j|b>>>32-j)+n}for(var t=CryptoJS,r=t.lib,w=r.WordArray,v=r.Hasher,r=t.algo,b=[],x=0;64>x;x++)b[x]=4294967296*u.abs(u.sin(x+1))|0;r=r.MD5=v.extend({_doReset:function(){this._hash=new w.init([1732584193,4023233417,2562383102,271733878])},
_doProcessBlock:function(q,n){for(var a=0;16>a;a++){var c=n+a,e=q[c];q[c]=(e<<8|e>>>24)&16711935|(e<<24|e>>>8)&4278255360}var a=this._hash.words,c=q[n+0],e=q[n+1],j=q[n+2],k=q[n+3],z=q[n+4],r=q[n+5],t=q[n+6],w=q[n+7],v=q[n+8],A=q[n+9],B=q[n+10],C=q[n+11],u=q[n+12],D=q[n+13],E=q[n+14],x=q[n+15],f=a[0],m=a[1],g=a[2],h=a[3],f=p(f,m,g,h,c,7,b[0]),h=p(h,f,m,g,e,12,b[1]),g=p(g,h,f,m,j,17,b[2]),m=p(m,g,h,f,k,22,b[3]),f=p(f,m,g,h,z,7,b[4]),h=p(h,f,m,g,r,12,b[5]),g=p(g,h,f,m,t,17,b[6]),m=p(m,g,h,f,w,22,b[7]),
f=p(f,m,g,h,v,7,b[8]),h=p(h,f,m,g,A,12,b[9]),g=p(g,h,f,m,B,17,b[10]),m=p(m,g,h,f,C,22,b[11]),f=p(f,m,g,h,u,7,b[12]),h=p(h,f,m,g,D,12,b[13]),g=p(g,h,f,m,E,17,b[14]),m=p(m,g,h,f,x,22,b[15]),f=d(f,m,g,h,e,5,b[16]),h=d(h,f,m,g,t,9,b[17]),g=d(g,h,f,m,C,14,b[18]),m=d(m,g,h,f,c,20,b[19]),f=d(f,m,g,h,r,5,b[20]),h=d(h,f,m,g,B,9,b[21]),g=d(g,h,f,m,x,14,b[22]),m=d(m,g,h,f,z,20,b[23]),f=d(f,m,g,h,A,5,b[24]),h=d(h,f,m,g,E,9,b[25]),g=d(g,h,f,m,k,14,b[26]),m=d(m,g,h,f,v,20,b[27]),f=d(f,m,g,h,D,5,b[28]),h=d(h,f,
m,g,j,9,b[29]),g=d(g,h,f,m,w,14,b[30]),m=d(m,g,h,f,u,20,b[31]),f=l(f,m,g,h,r,4,b[32]),h=l(h,f,m,g,v,11,b[33]),g=l(g,h,f,m,C,16,b[34]),m=l(m,g,h,f,E,23,b[35]),f=l(f,m,g,h,e,4,b[36]),h=l(h,f,m,g,z,11,b[37]),g=l(g,h,f,m,w,16,b[38]),m=l(m,g,h,f,B,23,b[39]),f=l(f,m,g,h,D,4,b[40]),h=l(h,f,m,g,c,11,b[41]),g=l(g,h,f,m,k,16,b[42]),m=l(m,g,h,f,t,23,b[43]),f=l(f,m,g,h,A,4,b[44]),h=l(h,f,m,g,u,11,b[45]),g=l(g,h,f,m,x,16,b[46]),m=l(m,g,h,f,j,23,b[47]),f=s(f,m,g,h,c,6,b[48]),h=s(h,f,m,g,w,10,b[49]),g=s(g,h,f,m,
E,15,b[50]),m=s(m,g,h,f,r,21,b[51]),f=s(f,m,g,h,u,6,b[52]),h=s(h,f,m,g,k,10,b[53]),g=s(g,h,f,m,B,15,b[54]),m=s(m,g,h,f,e,21,b[55]),f=s(f,m,g,h,v,6,b[56]),h=s(h,f,m,g,x,10,b[57]),g=s(g,h,f,m,t,15,b[58]),m=s(m,g,h,f,D,21,b[59]),f=s(f,m,g,h,z,6,b[60]),h=s(h,f,m,g,C,10,b[61]),g=s(g,h,f,m,j,15,b[62]),m=s(m,g,h,f,A,21,b[63]);a[0]=a[0]+f|0;a[1]=a[1]+m|0;a[2]=a[2]+g|0;a[3]=a[3]+h|0},_doFinalize:function(){var b=this._data,n=b.words,a=8*this._nDataBytes,c=8*b.sigBytes;n[c>>>5]|=128<<24-c%32;var e=u.floor(a/
4294967296);n[(c+64>>>9<<4)+15]=(e<<8|e>>>24)&16711935|(e<<24|e>>>8)&4278255360;n[(c+64>>>9<<4)+14]=(a<<8|a>>>24)&16711935|(a<<24|a>>>8)&4278255360;b.sigBytes=4*(n.length+1);this._process();b=this._hash;n=b.words;for(a=0;4>a;a++)c=n[a],n[a]=(c<<8|c>>>24)&16711935|(c<<24|c>>>8)&4278255360;return b},clone:function(){var b=v.clone.call(this);b._hash=this._hash.clone();return b}});t.MD5=v._createHelper(r);t.HmacMD5=v._createHmacHelper(r)})(Math);
(function(){var u=CryptoJS,p=u.lib,d=p.Base,l=p.WordArray,p=u.algo,s=p.EvpKDF=d.extend({cfg:d.extend({keySize:4,hasher:p.MD5,iterations:1}),init:function(d){this.cfg=this.cfg.extend(d)},compute:function(d,r){for(var p=this.cfg,s=p.hasher.create(),b=l.create(),u=b.words,q=p.keySize,p=p.iterations;u.length<q;){n&&s.update(n);var n=s.update(d).finalize(r);s.reset();for(var a=1;a<p;a++)n=s.finalize(n),s.reset();b.concat(n)}b.sigBytes=4*q;return b}});u.EvpKDF=function(d,l,p){return s.create(p).compute(d,
l)}})();
CryptoJS.lib.Cipher||function(u){var p=CryptoJS,d=p.lib,l=d.Base,s=d.WordArray,t=d.BufferedBlockAlgorithm,r=p.enc.Base64,w=p.algo.EvpKDF,v=d.Cipher=t.extend({cfg:l.extend(),createEncryptor:function(e,a){return this.create(this._ENC_XFORM_MODE,e,a)},createDecryptor:function(e,a){return this.create(this._DEC_XFORM_MODE,e,a)},init:function(e,a,b){this.cfg=this.cfg.extend(b);this._xformMode=e;this._key=a;this.reset()},reset:function(){t.reset.call(this);this._doReset()},process:function(e){this._append(e);return this._process()},
finalize:function(e){e&&this._append(e);return this._doFinalize()},keySize:4,ivSize:4,_ENC_XFORM_MODE:1,_DEC_XFORM_MODE:2,_createHelper:function(e){return{encrypt:function(b,k,d){return("string"==typeof k?c:a).encrypt(e,b,k,d)},decrypt:function(b,k,d){return("string"==typeof k?c:a).decrypt(e,b,k,d)}}}});d.StreamCipher=v.extend({_doFinalize:function(){return this._process(!0)},blockSize:1});var b=p.mode={},x=function(e,a,b){var c=this._iv;c?this._iv=u:c=this._prevBlock;for(var d=0;d<b;d++)e[a+d]^=
c[d]},q=(d.BlockCipherMode=l.extend({createEncryptor:function(e,a){return this.Encryptor.create(e,a)},createDecryptor:function(e,a){return this.Decryptor.create(e,a)},init:function(e,a){this._cipher=e;this._iv=a}})).extend();q.Encryptor=q.extend({processBlock:function(e,a){var b=this._cipher,c=b.blockSize;x.call(this,e,a,c);b.encryptBlock(e,a);this._prevBlock=e.slice(a,a+c)}});q.Decryptor=q.extend({processBlock:function(e,a){var b=this._cipher,c=b.blockSize,d=e.slice(a,a+c);b.decryptBlock(e,a);x.call(this,
e,a,c);this._prevBlock=d}});b=b.CBC=q;q=(p.pad={}).Pkcs7={pad:function(a,b){for(var c=4*b,c=c-a.sigBytes%c,d=c<<24|c<<16|c<<8|c,l=[],n=0;n<c;n+=4)l.push(d);c=s.create(l,c);a.concat(c)},unpad:function(a){a.sigBytes-=a.words[a.sigBytes-1>>>2]&255}};d.BlockCipher=v.extend({cfg:v.cfg.extend({mode:b,padding:q}),reset:function(){v.reset.call(this);var a=this.cfg,b=a.iv,a=a.mode;if(this._xformMode==this._ENC_XFORM_MODE)var c=a.createEncryptor;else c=a.createDecryptor,this._minBufferSize=1;this._mode=c.call(a,
this,b&&b.words)},_doProcessBlock:function(a,b){this._mode.processBlock(a,b)},_doFinalize:function(){var a=this.cfg.padding;if(this._xformMode==this._ENC_XFORM_MODE){a.pad(this._data,this.blockSize);var b=this._process(!0)}else b=this._process(!0),a.unpad(b);return b},blockSize:4});var n=d.CipherParams=l.extend({init:function(a){this.mixIn(a)},toString:function(a){return(a||this.formatter).stringify(this)}}),b=(p.format={}).OpenSSL={stringify:function(a){var b=a.ciphertext;a=a.salt;return(a?s.create([1398893684,
1701076831]).concat(a).concat(b):b).toString(r)},parse:function(a){a=r.parse(a);var b=a.words;if(1398893684==b[0]&&1701076831==b[1]){var c=s.create(b.slice(2,4));b.splice(0,4);a.sigBytes-=16}return n.create({ciphertext:a,salt:c})}},a=d.SerializableCipher=l.extend({cfg:l.extend({format:b}),encrypt:function(a,b,c,d){d=this.cfg.extend(d);var l=a.createEncryptor(c,d);b=l.finalize(b);l=l.cfg;return n.create({ciphertext:b,key:c,iv:l.iv,algorithm:a,mode:l.mode,padding:l.padding,blockSize:a.blockSize,formatter:d.format})},
decrypt:function(a,b,c,d){d=this.cfg.extend(d);b=this._parse(b,d.format);return a.createDecryptor(c,d).finalize(b.ciphertext)},_parse:function(a,b){return"string"==typeof a?b.parse(a,this):a}}),p=(p.kdf={}).OpenSSL={execute:function(a,b,c,d){d||(d=s.random(8));a=w.create({keySize:b+c}).compute(a,d);c=s.create(a.words.slice(b),4*c);a.sigBytes=4*b;return n.create({key:a,iv:c,salt:d})}},c=d.PasswordBasedCipher=a.extend({cfg:a.cfg.extend({kdf:p}),encrypt:function(b,c,d,l){l=this.cfg.extend(l);d=l.kdf.execute(d,
b.keySize,b.ivSize);l.iv=d.iv;b=a.encrypt.call(this,b,c,d.key,l);b.mixIn(d);return b},decrypt:function(b,c,d,l){l=this.cfg.extend(l);c=this._parse(c,l.format);d=l.kdf.execute(d,b.keySize,b.ivSize,c.salt);l.iv=d.iv;return a.decrypt.call(this,b,c,d.key,l)}})}();
(function(){for(var u=CryptoJS,p=u.lib.BlockCipher,d=u.algo,l=[],s=[],t=[],r=[],w=[],v=[],b=[],x=[],q=[],n=[],a=[],c=0;256>c;c++)a[c]=128>c?c<<1:c<<1^283;for(var e=0,j=0,c=0;256>c;c++){var k=j^j<<1^j<<2^j<<3^j<<4,k=k>>>8^k&255^99;l[e]=k;s[k]=e;var z=a[e],F=a[z],G=a[F],y=257*a[k]^16843008*k;t[e]=y<<24|y>>>8;r[e]=y<<16|y>>>16;w[e]=y<<8|y>>>24;v[e]=y;y=16843009*G^65537*F^257*z^16843008*e;b[k]=y<<24|y>>>8;x[k]=y<<16|y>>>16;q[k]=y<<8|y>>>24;n[k]=y;e?(e=z^a[a[a[G^z]]],j^=a[a[j]]):e=j=1}var H=[0,1,2,4,8,
16,32,64,128,27,54],d=d.AES=p.extend({_doReset:function(){for(var a=this._key,c=a.words,d=a.sigBytes/4,a=4*((this._nRounds=d+6)+1),e=this._keySchedule=[],j=0;j<a;j++)if(j<d)e[j]=c[j];else{var k=e[j-1];j%d?6<d&&4==j%d&&(k=l[k>>>24]<<24|l[k>>>16&255]<<16|l[k>>>8&255]<<8|l[k&255]):(k=k<<8|k>>>24,k=l[k>>>24]<<24|l[k>>>16&255]<<16|l[k>>>8&255]<<8|l[k&255],k^=H[j/d|0]<<24);e[j]=e[j-d]^k}c=this._invKeySchedule=[];for(d=0;d<a;d++)j=a-d,k=d%4?e[j]:e[j-4],c[d]=4>d||4>=j?k:b[l[k>>>24]]^x[l[k>>>16&255]]^q[l[k>>>
8&255]]^n[l[k&255]]},encryptBlock:function(a,b){this._doCryptBlock(a,b,this._keySchedule,t,r,w,v,l)},decryptBlock:function(a,c){var d=a[c+1];a[c+1]=a[c+3];a[c+3]=d;this._doCryptBlock(a,c,this._invKeySchedule,b,x,q,n,s);d=a[c+1];a[c+1]=a[c+3];a[c+3]=d},_doCryptBlock:function(a,b,c,d,e,j,l,f){for(var m=this._nRounds,g=a[b]^c[0],h=a[b+1]^c[1],k=a[b+2]^c[2],n=a[b+3]^c[3],p=4,r=1;r<m;r++)var q=d[g>>>24]^e[h>>>16&255]^j[k>>>8&255]^l[n&255]^c[p++],s=d[h>>>24]^e[k>>>16&255]^j[n>>>8&255]^l[g&255]^c[p++],t=
d[k>>>24]^e[n>>>16&255]^j[g>>>8&255]^l[h&255]^c[p++],n=d[n>>>24]^e[g>>>16&255]^j[h>>>8&255]^l[k&255]^c[p++],g=q,h=s,k=t;q=(f[g>>>24]<<24|f[h>>>16&255]<<16|f[k>>>8&255]<<8|f[n&255])^c[p++];s=(f[h>>>24]<<24|f[k>>>16&255]<<16|f[n>>>8&255]<<8|f[g&255])^c[p++];t=(f[k>>>24]<<24|f[n>>>16&255]<<16|f[g>>>8&255]<<8|f[h&255])^c[p++];n=(f[n>>>24]<<24|f[g>>>16&255]<<16|f[h>>>8&255]<<8|f[k&255])^c[p++];a[b]=q;a[b+1]=s;a[b+2]=t;a[b+3]=n},keySize:8});u.AES=p._createHelper(d)})();

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
"use strict";var Module={};var initializedJS=false;function threadPrintErr(){var text=Array.prototype.slice.call(arguments).join(" ");console.error(text)}function threadAlert(){var text=Array.prototype.slice.call(arguments).join(" ");postMessage({cmd:"alert",text:text,threadId:Module["_pthread_self"]()})}var err=threadPrintErr;self.alert=threadAlert;Module["instantiateWasm"]=(info,receiveInstance)=>{var module=Module["wasmModule"];Module["wasmModule"]=null;var instance=new WebAssembly.Instance(module,info);return receiveInstance(instance)};self.onunhandledrejection=e=>{throw e.reason??e};function handleMessage(e){try{if(e.data.cmd==="load"){let messageQueue=[];self.onmessage=e=>messageQueue.push(e);self.startWorker=instance=>{Module=instance;postMessage({"cmd":"loaded"});for(let msg of messageQueue){handleMessage(msg)}self.onmessage=handleMessage};Module["wasmModule"]=e.data.wasmModule;for(const handler of e.data.handlers){Module[handler]=(...args)=>{postMessage({cmd:"callHandler",handler:handler,args:args})}}Module["wasmMemory"]=e.data.wasmMemory;Module["buffer"]=Module["wasmMemory"].buffer;Module["ENVIRONMENT_IS_PTHREAD"]=true;if(typeof e.data.urlOrBlob=="string"){importScripts(e.data.urlOrBlob)}else{var objectUrl=URL.createObjectURL(e.data.urlOrBlob);importScripts(objectUrl);URL.revokeObjectURL(objectUrl)}JSPlayerModule(Module)}else if(e.data.cmd==="run"){Module["__emscripten_thread_init"](e.data.pthread_ptr,0,0,1);Module["__emscripten_thread_mailbox_await"](e.data.pthread_ptr);Module["establishStackSpace"]();Module["PThread"].receiveObjectTransfer(e.data);Module["PThread"].threadInitTLS();if(!initializedJS){Module["__embind_initialize_bindings"]();initializedJS=true}try{Module["invokeEntryPoint"](e.data.start_routine,e.data.arg)}catch(ex){if(ex!="unwind"){throw ex}}}else if(e.data.cmd==="cancel"){if(Module["_pthread_self"]()){Module["__emscripten_thread_exit"](-1)}}else if(e.data.target==="setimmediate"){}else if(e.data.cmd==="checkMailbox"){if(initializedJS){Module["checkMailbox"]()}}else if(e.data.cmd){err("worker.js received unknown command "+e.data.cmd);err(e.data)}}catch(ex){if(Module["__emscripten_thread_crashed"]){Module["__emscripten_thread_crashed"]()}throw ex}}self.onmessage=handleMessage;

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
"use strict";var Module={};var initializedJS=false;function threadPrintErr(){var text=Array.prototype.slice.call(arguments).join(" ");console.error(text)}function threadAlert(){var text=Array.prototype.slice.call(arguments).join(" ");postMessage({cmd:"alert",text:text,threadId:Module["_pthread_self"]()})}var err=threadPrintErr;self.alert=threadAlert;Module["instantiateWasm"]=(info,receiveInstance)=>{var module=Module["wasmModule"];Module["wasmModule"]=null;var instance=new WebAssembly.Instance(module,info);return receiveInstance(instance)};self.onunhandledrejection=e=>{throw e.reason??e};function handleMessage(e){try{if(e.data.cmd==="load"){let messageQueue=[];self.onmessage=e=>messageQueue.push(e);self.startWorker=instance=>{Module=instance;postMessage({"cmd":"loaded"});for(let msg of messageQueue){handleMessage(msg)}self.onmessage=handleMessage};Module["wasmModule"]=e.data.wasmModule;for(const handler of e.data.handlers){Module[handler]=(...args)=>{postMessage({cmd:"callHandler",handler:handler,args:args})}}Module["wasmMemory"]=e.data.wasmMemory;Module["buffer"]=Module["wasmMemory"].buffer;Module["ENVIRONMENT_IS_PTHREAD"]=true;if(typeof e.data.urlOrBlob=="string"){importScripts(e.data.urlOrBlob)}else{var objectUrl=URL.createObjectURL(e.data.urlOrBlob);importScripts(objectUrl);URL.revokeObjectURL(objectUrl)}JSPlayerModule(Module)}else if(e.data.cmd==="run"){Module["__emscripten_thread_init"](e.data.pthread_ptr,0,0,1);Module["__emscripten_thread_mailbox_await"](e.data.pthread_ptr);Module["establishStackSpace"]();Module["PThread"].receiveObjectTransfer(e.data);Module["PThread"].threadInitTLS();if(!initializedJS){Module["__embind_initialize_bindings"]();initializedJS=true}try{Module["invokeEntryPoint"](e.data.start_routine,e.data.arg)}catch(ex){if(ex!="unwind"){throw ex}}}else if(e.data.cmd==="cancel"){if(Module["_pthread_self"]()){Module["__emscripten_thread_exit"](-1)}}else if(e.data.target==="setimmediate"){}else if(e.data.cmd==="checkMailbox"){if(initializedJS){Module["checkMailbox"]()}}else if(e.data.cmd){err("worker.js received unknown command "+e.data.cmd);err(e.data)}}catch(ex){if(Module["__emscripten_thread_crashed"]){Module["__emscripten_thread_crashed"]()}throw ex}}self.onmessage=handleMessage;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@@ -0,0 +1,240 @@
importScripts('libSystemTransform.js');
const RECORDRTP = 0; //录制一份未经过转封装的码流原始数据,用于定位问题
let dataType = 1;
// 字母字符串转byte数组
function stringToBytes (str) {
var ch;
var st;
var re = [];
for (var i = 0; i < str.length; i++) {
ch = str.charCodeAt(i); // get char
st = []; // set up "stack"
do {
st.push(ch & 0xFF); // push byte to stack
ch = ch >> 8; // shift value down by 1 byte
}
while (ch);
// add stack contents to result
// done because chars have "wrong" endianness
re = re.concat(st.reverse());
}
// return an array of bytes
return re;
}
// 转封装库回调函数
self.STCallBack = function (fileIndex, indexLen, data, dataLen) {
//stFrameInfo的类型见DETAIL_FRAME_INFO
let stFrameInfo = Module._GetDetialFrameInfo();
let nIsMp4Index = stFrameInfo.nIsMp4Index;
//console.log("FrameType is " , stFrameInfo);
//console.log("nIsMp4Index is " + nIsMp4Index);
//debugger
var pData = null;
pData = new Uint8Array(dataLen);
pData.set(Module.HEAPU8.subarray(data, data + dataLen));
if (dataType === 1) {
postMessage({ type: "outputData",
buf: pData.buffer,
dType: 1,
frameInfo: stFrameInfo }, [pData.buffer]);
dataType = 2;
} else {
if (nIsMp4Index) {
postMessage({ type: "outputData",
buf: pData.buffer,
dType: 6,
frameInfo: stFrameInfo }, [pData.buffer]); //6索引类型
} else {
postMessage({ type: "outputData",
buf: pData.buffer,
dType: 2,
frameInfo: stFrameInfo }, [pData.buffer]); //2:码流
}
}
//stFrameInfo的类型见DETAIL_FRAME_INFO
//let stFrameInfo = Module._GetDetialFrameInfo();
//let stFrameType = stFrameInfo.nFrameType;
//let nFrameNum = stFrameInfo.nFrameNum;
//let nTimeStamp = stFrameInfo.nTimeStamp;
//let nIsMp4Index = stFrameInfo.nIsMp4Index;
//console.log("FrameType is " + stFrameType);
//console.log("nIsMp4Index is " + nIsMp4Index);
};
// self.Module = { memoryInitializerRequest: loadMemInitFile(), TOTAL_MEMORY: 128*1024*1024 };
// importScripts('SystemTransform.js');
self.Module['onRuntimeInitialized'] = function () {
postMessage({type: "loaded"});
};
onmessage = function (e) {
var data = e.data;
if ("create" === data.type) {
if (RECORDRTP) {
postMessage({ type: "created" });
postMessage({ type: "outputData",
buf: data.buf,
dType: 1 }, [data.buf]);
} else {
var iHeadLen = data.len;
var pHead = Module._malloc(iHeadLen);
if (pHead === null) {
console.log("inputdata malloc failed!!!");
return -1;
}
var iTransType = data.packType;//目标格式
var iRet = 0;
var buf = new Uint8Array(data.buf);
//PS流(只有ps支持探测)并且编码格式异常正常是265和26411位和10位 不可能全是0全0 并且是ps就探测策略的情况下使用探测策略
if (buf[9] === 0 && buf[8] === 2 && buf[11] === 0 && buf[10] === 0) {
iRet = Module._CreatHandle(0, iTransType, iHeadLen); //用探测的策略
} else {
self.writeArrayToMemory(buf, pHead);
iRet = Module._CreatHandle(pHead, iTransType, iHeadLen);
//-2147483645代表的是参数错误此种情况大概率发生在头信息错误例如大华设备的情况此时用 无头探测的策略
//其他情况 按海康标准处理流程,不要做任何特殊处理
if (iRet == -2147483645) {
iRet = Module._CreatHandle(0, iTransType, iHeadLen); //失败了,用探测的策略再试一次
}
}
if (iRet != 0) {
if (iRet == -2147483647) {
postMessage({ type: "outputData",
dType: 1501 }); //标记为格式不支持
} else {
postMessage({ type: "outputData",
dType: 1501 }); //转封装创建失败,也同样提示码流格式不支持,如果后续要细化再区分
}
console.log("_CreatHandle failed!" + iRet);
} else {
if (data.options && typeof data.options.pKeyData !== "undefined" && data.options.pKeyData !== null) {
if ((2 === iTransType && "" === data.options.pKeyData)) {
//转ps的时候如果密码是空是允许的即使码流加密了导出加密后的码流就行
//此时不要设置密码否则反而会提示密码错误
} else {
var secretInfo = data.options;
var keyLen = secretInfo.nKeyLen;
var pKeyData = Module._malloc(keyLen);
if (pKeyData === null) {
console.log("setEncryptKey malloc failed!!!");
return -1;
}
var nKeySize = secretInfo.pKeyData.length;
var bufData = stringToBytes(secretInfo.pKeyData);
let inputData = new Uint8Array(bufData);
Module.writeArrayToMemory(inputData, pKeyData);
inputData = null;
iRet = Module._SysTransSetEncryptKey(secretInfo.nKeyType, pKeyData, keyLen, nKeySize);
if (iRet != 0) {
console.log("_SysTransSetEncryptKey failed!");
}
if (pKeyData != null) {
Module._free(pKeyData);
pKeyData = null;
}
}
}
//带samplingParam参数代表需要 用到 音频替换功能
if (data.options && typeof data.options.samplingParam !== "undefined") {
var oParam = data.options.samplingParam;
var nCapacityType = 1; //写死1 代表 剔除音频
var nType = 3; //写死3 代表 修改输出目标的海康头配置,内部包含视频参数、音频参数
var nAudioEnable = 1; //音频参数修改使能开关0=不启用1=启用
var nAudioFormat = oParam.iAudioType; //音频编码类型 对应关系参考海康媒体头规范PCM 0x7001 G711_U 0x7110 G711_A 0x7111 AAC 0x2001
var nAudioChannels = oParam.iChannel; //音频通道数直接设置为
var nAudioBitsPerSample = oParam.iAudioBitWidth; //音频位样率
var nAudioSamplesrate = oParam.iAudioSamplingRate; //音频采样率
var nAudioBitrate = oParam.iAudioBitRate; //音频比特率
iRet = Module._SysTransConfig(nCapacityType, nType, nAudioEnable, nAudioFormat,
nAudioChannels, nAudioBitsPerSample, nAudioSamplesrate, nAudioBitrate);
if (iRet != 0) {
console.log("_SysTransConfig Failed:" + iRet);
}
} else {
iRet = Module._SysTransConfig(128, 0, 0, 0, 0, 0, 0, 0); //nCapacityType = 0x00000080 代表开启私有信息回调 解决转mp4后私有信息丢失问题
if (iRet != 0) {
console.log("_SysTransConfig Failed:" + iRet);
}
}
iRet = Module._SysTransRegisterDataCallBack();
if (iRet != 0) {
console.log("_SysTransRegisterDataCallBack Failed:" + iRet);
}
iRet = Module._SysTransStart(null, null);
if (iRet != 0) {
console.log("_SysTransStart Failed:" + iRet);
}
postMessage({type: "created"});
}
if (pHead != null) {
Module._free(pHead);
pHead = null;
}
}
} else if ("inputData" === data.type) {
if (RECORDRTP) {
var aFileData = new Uint8Array(data.buf); // 拷贝一份
var iBufferLen = aFileData.length;
var szBufferLen = iBufferLen.toString(16);
if (szBufferLen.length === 1) {
szBufferLen = "000" + szBufferLen;
} else if (szBufferLen.length === 2) {
szBufferLen = "00" + szBufferLen;
} else if (szBufferLen.length === 3) {
szBufferLen = "0" + szBufferLen;
}
var aData = [0, 0, parseInt(szBufferLen.substring(0, 2), 16), parseInt(szBufferLen.substring(2, 4), 16)];
for (var iIndex = 0, iDataLength = aFileData.length; iIndex < iDataLength; iIndex++) {
aData[iIndex + 4] = aFileData[iIndex];
}
var dataUint8 = new Uint8Array(aData);
postMessage({type: "outputData",
buf: dataUint8.buffer,
dType: 2});
} else {
let inputMode = 0; //代表输入原始数据
if (data.samplingParam) {
iRet = Module._SysTransInputAudioPara(5, data.samplingParam.iChannel, data.samplingParam.iAudioBitWidth,
data.samplingParam.iAudioSamplingRate, data.samplingParam.iTimeStamp, data.samplingParam.iAudioBitRate); //参数含义和_SysTransConfig类似
if (iRet != 0) {
console.log("_SysTransInputAudioPara Failed:" + iRet);
}
inputMode = 2; //输入替换的音频
}
var pInputDataBuf = Module._malloc(data.len);
var idataLen = data.len;
self.writeArrayToMemory(new Uint8Array(data.buf), pInputDataBuf);
// 输入数据每次最多2m
let pp = Module._SysTransInputData(inputMode, pInputDataBuf, idataLen);
if (pp == -2147483627) {
//-2147483627 对应十六进制的80000015
postMessage({ type: "outputData",
dType: 1500 }); //标记为密码错误
} else if (pp == -2147483647) {
postMessage({ type: "outputData",
dType: 1501 }); //标记为格式不支持
} else if (pp != 0) {
console.log("InputData Failed:" + pp);
}
Module._free(pInputDataBuf);
}
} else if ("release" === data.type) {
var iRet = Module._SysTransStop();
if (iRet != 0) {
console.log("_SysTransStop failed!");
}
Module._SysTransRelease();
if (iRet != 0) {
console.log("_SysTransRelease failed!");
}
close();
}
};

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,370 @@
import { request } from '@/api/request'
/** 3D 机房接口标准响应。 */
interface ThreeMachineRoomResponse<T> {
code: number
message?: string
details: T
}
/** 告警级别。 */
export interface ThreeMachineRoomSeverity {
id?: number
name?: string
code?: string
color?: string
priority?: number
}
/** 三维坐标和旋转。 */
export interface ThreeMachineRoomTransform {
position_x: number
position_y: number
position_z: number
rotation_x?: number
rotation_y?: number
rotation_z?: number
}
/** 独立设备三维尺寸。 */
export interface ThreeMachineRoomSize {
width: number
height: number
depth: number
}
/** 设备绑定的监控资源。 */
export interface ThreeMachineRoomBinding {
resource_uid: string
resource_category?: string
service_identity?: string
display_name?: string
business_system_id?: number | null
}
/** 监控指标。 */
export interface ThreeMachineRoomMetric {
name: string
value?: string | number | null
unit?: string
type?: string
timestamp?: string
}
/** 设备资源运行数据。 */
export interface ThreeMachineRoomResource extends ThreeMachineRoomBinding {
status?: string
metrics_at?: string
metrics?: ThreeMachineRoomMetric[]
}
/** 活动告警。 */
export interface ThreeMachineRoomAlert {
id?: number
alert_name?: string
summary?: string
severity?: ThreeMachineRoomSeverity
status?: string
starts_at?: string
last_seen_at?: string
updated_at?: string
}
/** 按监控资源分组的活动告警。 */
export interface ThreeMachineRoomAlertGroup {
resource_uid: string
active_count?: number
highest_severity?: ThreeMachineRoomSeverity
alerts?: ThreeMachineRoomAlert[]
}
/** 机柜 U 位。 */
export interface ThreeMachineRoomUnit {
id?: number
unit_number: number
status: 'available' | 'occupied' | 'reserved' | 'disabled'
asset_id?: number | null
}
/** 机柜中的资产设备。 */
export interface ThreeMachineRoomDevice {
asset_id: number
asset_code?: string
asset_name?: string
category_id?: number | null
category_code?: string
category_name?: string
placement_type?: 'rack' | 'room' | 'unplaced'
rack_id?: number | null
unit_start?: number | null
unit_end?: number | null
occupied_units?: number
power_consumption?: number
transform?: ThreeMachineRoomTransform
size?: ThreeMachineRoomSize
bindings?: ThreeMachineRoomBinding[]
}
/** 3D 场景机柜。 */
export interface ThreeMachineRoomRack {
id: number
code?: string
name?: string
row?: number
column?: number
height?: number
width_mm?: number
depth_mm?: number
status?: string
transform?: ThreeMachineRoomTransform
utilization_rate?: number
units?: ThreeMachineRoomUnit[]
devices?: ThreeMachineRoomDevice[]
}
/** 3D 机房场景数据。 */
export interface ThreeMachineRoomScene {
room: {
id: number
datacenter_id?: number
floor_id?: number
name?: string
code?: string
scene_length?: number
scene_width?: number
scene_height?: number
layout_version?: number
[key: string]: unknown
}
racks: ThreeMachineRoomRack[]
room_devices?: ThreeMachineRoomDevice[]
summary?: {
rack_count?: number
unit_count?: number
rack_device_count?: number
room_device_count?: number
device_count?: number
}
[key: string]: unknown
}
/** 设备状态信号。 */
export interface ThreeMachineRoomSignal {
asset_id: number
status?: 'normal' | 'warning' | 'abnormal' | string
active_alert_count?: number
highest_alert_severity?: ThreeMachineRoomSeverity
resources?: ThreeMachineRoomResource[]
alerts?: ThreeMachineRoomAlertGroup[]
[key: string]: unknown
}
/** 机房状态信号集合。 */
export interface ThreeMachineRoomSignals {
signals: ThreeMachineRoomSignal[]
[key: string]: unknown
}
/** 设备可观测数据。 */
export interface ThreeMachineRoomObservability {
device?: ThreeMachineRoomDevice
resources?: ThreeMachineRoomResource[]
alerts?: ThreeMachineRoomAlertGroup[]
[key: string]: unknown
}
/** 3D 场景全量导出数据。 */
export interface ThreeMachineRoomExport {
datacenters: Array<{
id: number
name?: string
code?: string
status?: string
latitude?: string
longitude?: string
rooms?: ThreeMachineRoomScene[]
}>
summary?: {
datacenter_count?: number
room_count?: number
rack_count?: number
unit_count?: number
device_count?: number
}
}
/** 机柜布局保存项。 */
export interface ThreeMachineRoomRackLayout {
rack_id: number
row?: number
column?: number
position_x: number
position_y: number
position_z: number
rotation_y: number
}
/** 机房 3D 初始化配置。 */
export interface ThreeMachineRoomConfigData {
expectedVersion: number
sceneLength: number
sceneWidth: number
sceneHeight: number
racks: ThreeMachineRoomRackLayout[]
}
/** 独立设备放置参数。 */
export interface RoomDevicePlacement {
expectedVersion: number
positionX: number
positionY: number
positionZ: number
rotationX: number
rotationY: number
rotationZ: number
sceneWidth: number
sceneHeight: number
sceneDepth: number
}
/** 设备机柜上架参数。 */
export interface RackDevicePlacement {
expectedVersion: number
rackId: number
startUnit: number
occupiedUnits: number
powerConsumption?: number
}
/** 校验响应并返回 3D 机房业务数据。 */
function getResponseDetails<T>(response: ThreeMachineRoomResponse<T>): T {
if (!response || typeof response !== 'object') {
throw new Error('接口返回格式错误')
}
if (response.code !== 0) {
throw new Error(response.message || '接口调用失败')
}
return response.details
}
/** 获取指定机房的 3D 场景。 */
export async function fetchThreeMachineRoomScene(roomId: number): Promise<ThreeMachineRoomScene> {
const response = await request.get<ThreeMachineRoomResponse<ThreeMachineRoomScene>>(
`/Assets/v1/three-d/rooms/${encodeURIComponent(roomId)}/scene`
)
return getResponseDetails(response)
}
/** 获取权限范围内的全部 3D 场景。 */
export async function exportThreeMachineRoomScenes(): Promise<ThreeMachineRoomExport> {
const response = await request.get<ThreeMachineRoomResponse<ThreeMachineRoomExport>>('/Assets/v1/three-d/export')
return getResponseDetails(response)
}
/** 保存机房尺寸和初始机柜布局。 */
export async function saveThreeMachineRoomConfig(
roomId: number,
config: ThreeMachineRoomConfigData
): Promise<{ room_id: number; layout_version: number }> {
const response = await request.put<ThreeMachineRoomResponse<{ room_id: number; layout_version: number }>>(
`/Assets/v1/three-d/rooms/${encodeURIComponent(roomId)}/config`,
{
expected_version: config.expectedVersion,
scene_length: config.sceneLength,
scene_width: config.sceneWidth,
scene_height: config.sceneHeight,
racks: config.racks,
}
)
return getResponseDetails(response)
}
/** 批量保存机柜布局。 */
export async function saveThreeMachineRoomRackLayout(
roomId: number,
expectedVersion: number,
racks: ThreeMachineRoomRackLayout[]
): Promise<{ room_id: number; layout_version: number }> {
const response = await request.put<ThreeMachineRoomResponse<{ room_id: number; layout_version: number }>>(
`/Assets/v1/three-d/rooms/${encodeURIComponent(roomId)}/layout`,
{
expected_version: expectedVersion,
racks,
}
)
return getResponseDetails(response)
}
/** 获取指定机房的设备状态与告警。 */
export async function fetchThreeMachineRoomSignals(roomId: number): Promise<ThreeMachineRoomSignals> {
const response = await request.get<ThreeMachineRoomResponse<ThreeMachineRoomSignals>>(
`/Assets/v1/three-d/rooms/${encodeURIComponent(roomId)}/signals`
)
return getResponseDetails(response)
}
/** 获取设备可观测指标与告警详情。 */
export async function fetchThreeMachineRoomDeviceObservability(assetId: number): Promise<ThreeMachineRoomObservability> {
const response = await request.get<ThreeMachineRoomResponse<ThreeMachineRoomObservability>>(
`/Assets/v1/three-d/devices/${encodeURIComponent(assetId)}/observability`
)
return getResponseDetails(response)
}
/** 保存设备机柜及 U 位。 */
export async function saveThreeMachineRoomRackPlacement(
roomId: number,
assetId: number,
placement: RackDevicePlacement
): Promise<{ layout_version: number }> {
const response = await request.put<ThreeMachineRoomResponse<{ layout_version: number }>>(
`/Assets/v1/three-d/rooms/${encodeURIComponent(roomId)}/devices/${encodeURIComponent(assetId)}/placement`,
{
expected_version: placement.expectedVersion,
placement_type: 'rack',
rack_id: placement.rackId,
start_unit: placement.startUnit,
occupied_units: placement.occupiedUnits,
power_consumption: placement.powerConsumption || 0,
}
)
return getResponseDetails(response)
}
/** 将设备放置在机房场景中。 */
export async function saveThreeMachineRoomDevicePlacement(
roomId: number,
assetId: number,
placement: RoomDevicePlacement
): Promise<{ room_id: number; asset_id: number; placement_type: 'room'; layout_version: number }> {
const response = await request.put<
ThreeMachineRoomResponse<{ room_id: number; asset_id: number; placement_type: 'room'; layout_version: number }>
>(`/Assets/v1/three-d/rooms/${encodeURIComponent(roomId)}/devices/${encodeURIComponent(assetId)}/placement`, {
expected_version: placement.expectedVersion,
placement_type: 'room',
position_x: placement.positionX,
position_y: placement.positionY,
position_z: placement.positionZ,
rotation_x: placement.rotationX,
rotation_y: placement.rotationY,
rotation_z: placement.rotationZ,
scene_width: placement.sceneWidth,
scene_height: placement.sceneHeight,
scene_depth: placement.sceneDepth,
})
return getResponseDetails(response)
}
/** 解除设备在机柜或机房中的位置。 */
export async function removeThreeMachineRoomDevicePlacement(
roomId: number,
assetId: number,
expectedVersion: number
): Promise<{ room_id: number; asset_id: number; placement_type: 'unplaced'; layout_version: number }> {
const response = await request.delete<
ThreeMachineRoomResponse<{ room_id: number; asset_id: number; placement_type: 'unplaced'; layout_version: number }>
>(`/Assets/v1/three-d/rooms/${encodeURIComponent(roomId)}/devices/${encodeURIComponent(assetId)}/placement`, {
data: { expected_version: expectedVersion },
})
return getResponseDetails(response)
}

View File

@@ -100,6 +100,20 @@ const OPS: AppRouteRecordRaw = {
roles: ['*'],
},
},
{
path: 'datacenter/three-machine-room/:room_id?',
alias: ['/datacenter/three-machine-room/:room_id?'],
name: 'ThreeMachineRoom',
component: () => import('@/views/ops/pages/datacenter/three-machine-room/index.vue'),
meta: {
locale: '3D 机房',
requiresAuth: true,
roles: ['*'],
hideInMenu: true,
ignoreCache: true,
is_full: true,
},
},
{
path: 'governance',
alias: ['/governance'],

View File

@@ -0,0 +1,282 @@
/** 海康摄像头实时预览参数。 */
export interface HikvisionPreviewConfig {
host: string
protocol: 1 | 2
httpPort: number
rtspPort: number
webSocketPort?: number
username: string
password: string
channelId: number
streamType: number
proxyEnabled: boolean
secretKey?: string
}
/** 海康播放器运行事件。 */
export interface HikvisionPlayerCallbacks {
onError?: (message: string) => void
onPerformanceLack?: () => void
}
const SDK_SCRIPT_PATHS = [
'jsPlugin/jquery.min.js',
'encryption/AES.js',
'encryption/cryptico.min.js',
'encryption/crypto-3.1.2.min.js',
'webVideoCtrl.js',
]
const PLAYER_ERROR_MESSAGES: Record<number, string> = {
1001: '码流传输异常',
1003: '取流失败,连接被动断开',
1006: '视频编码格式不支持,仅支持 H.264/H.265',
1007: '网络异常导致 WebSocket 断开',
1008: '首帧等待超时',
1011: '视频数据接收异常,请检查设备编码配置',
1012: '浏览器播放资源不足',
1015: '播放地址获取失败',
1017: '设备认证失败',
}
let scriptsPromise: Promise<void> | null = null
let initializationPromise: Promise<void> | null = null
let initializedContainerId = ''
let activeDeviceIdentify = ''
let runtimeCallbacks: HikvisionPlayerCallbacks = {}
let previewOperation = 0
/** 取得 WebSDK 静态资源的部署地址。 */
function getSdkAssetUrl(relativePath: string): string {
const applicationDirectory = new URL('.', window.location.href)
return new URL(`vendor/hikvision/${relativePath}`, applicationDirectory).toString()
}
/** 动态加载一个 SDK 脚本。 */
function loadScript(relativePath: string): Promise<void> {
const source = getSdkAssetUrl(relativePath)
const existing = Array.from(document.scripts).find((script) => script.src === source)
if (existing?.dataset.loaded === 'true') return Promise.resolve()
return new Promise((resolve, reject) => {
const script = existing || document.createElement('script')
const handleLoad = () => {
script.dataset.loaded = 'true'
resolve()
}
const handleError = () => reject(new Error(`海康 WebSDK 资源加载失败:${relativePath}`))
script.addEventListener('load', handleLoad, { once: true })
script.addEventListener('error', handleError, { once: true })
if (!existing) {
script.src = source
script.async = false
script.dataset.hikvisionSdk = 'true'
if (relativePath === 'webVideoCtrl.js') script.id = 'videonode'
document.head.appendChild(script)
}
})
}
/** 按官方 Demo 顺序加载 WebSDK 依赖。 */
async function loadSdkScripts(): Promise<void> {
if (window.WebVideoCtrl) return
if (!scriptsPromise) {
scriptsPromise = SDK_SCRIPT_PATHS.reduce((promise, path) => promise.then(() => loadScript(path)), Promise.resolve()).catch((error) => {
scriptsPromise = null
throw error
})
}
await scriptsPromise
if (!window.WebVideoCtrl) throw new Error('海康 WebSDK 未正确挂载到页面')
}
/** 获取已加载的海康控制器。 */
function getController(): HikvisionWebVideoCtrl {
if (!window.WebVideoCtrl) throw new Error('海康 WebSDK 尚未初始化')
return window.WebVideoCtrl
}
/** 将 SDK 状态码转换为可读错误。 */
function createSdkError(action: string, status?: number): Error {
if (status === 401) return new Error(`${action}失败:用户名或密码错误`)
if (status === 403) return new Error(`${action}失败:设备不支持 WebSocket 取流或当前账号无权限`)
return new Error(status ? `${action}失败(状态码 ${status}` : `${action}失败`)
}
/** 初始化单窗口无插件播放器。 */
export async function initializeHikvisionPlayer(containerId: string, callbacks: HikvisionPlayerCallbacks = {}): Promise<void> {
runtimeCallbacks = callbacks
if (initializedContainerId === containerId) return
if (initializationPromise) return initializationPromise
initializationPromise = (async () => {
await loadSdkScripts()
const controller = getController()
if (!controller.I_SupportNoPlugin()) throw new Error('当前浏览器不支持海康无插件播放器,请升级 Chrome、Edge 或 Firefox')
if (!document.getElementById(containerId)) throw new Error('摄像头播放器容器尚未创建')
await new Promise<void>((resolve, reject) => {
let settled = false
const timeout = window.setTimeout(() => {
if (!settled) reject(new Error('海康播放器初始化超时'))
}, 20000)
const finish = (error?: Error) => {
if (settled) return
settled = true
window.clearTimeout(timeout)
error ? reject(error) : resolve()
}
controller.I_InitPlugin('100%', '100%', {
bWndFull: true,
iPackageType: 2,
iWndowType: 1,
bNoPlugin: true,
cbInitPluginComplete: () => {
const result = controller.I_InsertOBJECTPlugin(containerId)
finish(result === 0 ? undefined : new Error('海康播放器挂载失败'))
},
cbPluginErrorHandler: (_windowIndex, errorCode) => {
runtimeCallbacks.onError?.(PLAYER_ERROR_MESSAGES[errorCode] || `播放器异常(错误码 ${errorCode}`)
},
cbPerformanceLack: () => runtimeCallbacks.onPerformanceLack?.(),
cbSecretKeyError: () => runtimeCallbacks.onError?.('码流加密密钥错误'),
})
})
initializedContainerId = containerId
})().catch((error) => {
initializationPromise = null
throw error
})
return initializationPromise
}
/** 停止当前窗口中的实时预览。 */
async function stopWindow(): Promise<void> {
const controller = getController()
if (!controller.I_GetWindowStatus(0)) return
await new Promise<void>((resolve) => {
const timeout = window.setTimeout(resolve, 3000)
controller.I_Stop({
iIndex: 0,
success: () => {
window.clearTimeout(timeout)
resolve()
},
error: () => {
window.clearTimeout(timeout)
resolve()
},
})
})
}
/** 停止预览并注销当前设备。 */
export async function stopHikvisionPreview(): Promise<void> {
previewOperation += 1
if (!window.WebVideoCtrl || !initializedContainerId) return
await stopWindow()
if (activeDeviceIdentify) {
getController().I_Logout(activeDeviceIdentify)
activeDeviceIdentify = ''
}
}
/** 登录摄像头并开始实时预览。 */
export async function startHikvisionPreview(config: HikvisionPreviewConfig): Promise<void> {
const controller = getController()
await stopHikvisionPreview()
const operation = ++previewOperation
const deviceIdentify = `${config.host}_${config.httpPort}`
await new Promise<void>((resolve, reject) => {
let settled = false
const timeout = window.setTimeout(() => {
settled = true
reject(new Error('摄像头登录超时'))
}, 20000)
const finish = (error?: Error) => {
if (settled) return
settled = true
window.clearTimeout(timeout)
error ? reject(error) : resolve()
}
const result = controller.I_Login(config.host, config.protocol, String(config.httpPort), config.username, config.password, {
success: () => {
if (settled || operation !== previewOperation) {
controller.I_Logout(deviceIdentify)
if (settled) return
finish(new Error('摄像头播放已取消'))
return
}
finish()
},
error: (status) => finish(createSdkError('摄像头登录', status)),
})
if (result === -1) finish()
})
if (operation !== previewOperation) {
controller.I_Logout(deviceIdentify)
throw new Error('摄像头播放已取消')
}
activeDeviceIdentify = deviceIdentify
try {
await new Promise<void>((resolve, reject) => {
let settled = false
const timeout = window.setTimeout(() => {
settled = true
reject(new Error('摄像头取流超时'))
}, 20000)
const finish = (error?: Error) => {
if (settled) return
settled = true
window.clearTimeout(timeout)
error ? reject(error) : resolve()
}
controller.I_StartRealPlay(deviceIdentify, {
iWndIndex: 0,
iRtspPort: config.rtspPort,
iWSPort: config.webSocketPort,
iStreamType: config.streamType,
iChannelID: config.channelId,
bZeroChannel: false,
bProxy: config.proxyEnabled,
success: () => {
if (settled || operation !== previewOperation) {
void stopWindow().then(() => controller.I_Logout(deviceIdentify))
if (settled) return
finish(new Error('摄像头播放已取消'))
return
}
finish()
},
error: (status) => finish(createSdkError('摄像头取流', status)),
})
})
if (operation !== previewOperation) throw new Error('摄像头播放已取消')
if (config.secretKey) await controller.I_SetSecretKey(config.secretKey, 0)
} catch (error) {
await stopHikvisionPreview()
throw error
}
}
/** 调整播放器画布尺寸。 */
export function resizeHikvisionPlayer(width: number, height: number): void {
if (!window.WebVideoCtrl || !initializedContainerId || width <= 0 || height <= 0) return
getController().I_Resize(Math.floor(width), Math.floor(height))
}
/** 销毁播放器 Worker仅在离开 3D 页面时调用。 */
export async function destroyHikvisionPlayer(): Promise<void> {
if (!window.WebVideoCtrl || !initializedContainerId) return
await stopHikvisionPreview()
getController().I_DestroyWorker()
initializedContainerId = ''
initializationPromise = null
runtimeCallbacks = {}
}

3
src/types/env.d.ts vendored
View File

@@ -2,6 +2,9 @@
interface ImportMetaEnv {
readonly VITE_API_BASE_URL?: string
readonly VITE_API_PROXY_TARGET?: string
readonly VITE_HIKVISION_PROXY_TARGET?: string
readonly VITE_HIKVISION_WS_PROXY_TARGET?: string
readonly VITE_LOGS_API_BASE_URL?: string
// 在这里可以继续补充其他 VITE_ 前缀的环境变量
}

55
src/types/hikvision-web-sdk.d.ts vendored Normal file
View File

@@ -0,0 +1,55 @@
/** 海康 WebSDK 通用异步回调。 */
interface HikvisionSdkCallbacks {
success?: (xmlDoc?: Document) => void
error?: (status?: number, xmlDoc?: Document) => void
}
/** 海康 WebSDK 初始化参数。 */
interface HikvisionPluginOptions {
bWndFull: boolean
iPackageType: number
iWndowType: number
bNoPlugin: boolean
cbInitPluginComplete: () => void
cbPluginErrorHandler: (windowIndex: number, errorCode: number, error?: unknown) => void
cbPerformanceLack: () => void
cbSecretKeyError: (windowIndex: number) => void
}
/** 海康实时预览参数。 */
interface HikvisionRealPlayOptions extends HikvisionSdkCallbacks {
iWndIndex: number
iRtspPort: number
iStreamType: number
iChannelID: number
iWSPort?: number
bZeroChannel: boolean
bProxy: boolean
}
/** 海康 WebSDK 暴露到 window 的控制器。 */
interface HikvisionWebVideoCtrl {
I_SupportNoPlugin: () => boolean
I_InitPlugin: (width: string, height: string, options: HikvisionPluginOptions) => void
I_InsertOBJECTPlugin: (containerId: string) => number
I_Login: (
host: string,
protocol: number,
port: string,
username: string,
password: string,
callbacks: HikvisionSdkCallbacks
) => number | void
I_Logout: (deviceIdentify: string) => number
I_StartRealPlay: (deviceIdentify: string, options: HikvisionRealPlayOptions) => void
I_GetWindowStatus: (windowIndex: number) => { szDeviceIdentify?: string } | null
I_Stop: (options: HikvisionSdkCallbacks & { iIndex?: number }) => void
I_StopAll: () => Promise<unknown> | void
I_Resize: (width: number, height: number) => void
I_DestroyWorker: () => void
I_SetSecretKey: (secretKey: string, windowIndex: number) => Promise<unknown>
}
interface Window {
WebVideoCtrl?: HikvisionWebVideoCtrl
}

View File

@@ -47,7 +47,7 @@
<a-button type="text" size="small" @click="handleDetail(record)">详情</a-button>
<a-button type="text" size="small" @click="handleRacks(record)">机柜</a-button>
<a-button type="text" size="small" @click="handleEdit(record)">编辑</a-button>
<a-button type="text" size="small" @click="handleThreeDRoom(record)">3D机房</a-button>
<a-button type="text" size="small" @click="handleOpenThreeMachineRoom(record)">3D机房</a-button>
<a-button type="text" size="small" status="danger" @click="handleDelete(record)">删除</a-button>
</template>
</search-table>
@@ -90,7 +90,6 @@ const statusMap: Record<string, { text: string; color: string }> = {
maintenance: { text: '维护中', color: 'gold' },
offline: { text: '已下线', color: 'red' },
}
const loading = ref(false)
const tableData = ref<any[]>([])
const formModel = ref({
@@ -295,8 +294,12 @@ const handleRacks = (record: any) => {
})
}
const handleThreeDRoom = (record: any) => {
router.push(`/datacenter/room-3d/${record.id}`)
/** 打开当前机房的 3D 数字孪生场景。 */
const handleOpenThreeMachineRoom = (record: any) => {
router.push({
name: 'ThreeMachineRoom',
params: { room_id: record.id },
})
}
const handleDelete = async (record: any) => {

View File

@@ -0,0 +1,635 @@
<template>
<teleport to="body">
<aside
v-show="props.visible"
ref="floatingLayer"
class="camera-preview-floating"
:class="{ 'is-dragging': dragging }"
:style="floatingLayerStyle"
role="region"
:aria-label="floatingTitle"
>
<header class="camera-preview-floating__header" @pointerdown="startDragging">
<span class="camera-preview-floating__drag-mark" aria-hidden="true"></span>
<span class="camera-preview-floating__title" :title="floatingTitle">{{ floatingTitle }}</span>
<div class="camera-preview-floating__actions" @pointerdown.stop>
<button
type="button"
class="camera-preview-floating__action"
:disabled="initializing || starting"
title="开始播放"
aria-label="开始播放"
@click.stop="startPreview"
>
{{ starting ? '…' : '▶' }}
</button>
<button
type="button"
class="camera-preview-floating__action"
:disabled="!playing"
title="停止播放"
aria-label="停止播放"
@click.stop="stopPreview"
>
</button>
<button
type="button"
class="camera-preview-floating__action"
title="设备管理"
aria-label="设备管理"
@click.stop="openDeviceManagement"
>
</button>
<button
type="button"
class="camera-preview-floating__action"
title="关闭视频浮层"
aria-label="关闭视频浮层"
@click.stop="closePreviewLayer"
>
×
</button>
</div>
</header>
<div ref="playerWrapper" class="camera-preview-floating__player">
<div :id="PLAYER_CONTAINER_ID" class="camera-preview__canvas"></div>
<div v-if="initializing" class="camera-preview-floating__message">
<a-spin :size="22" />
<span>播放器初始化中</span>
</div>
<div
v-else-if="!playing"
class="camera-preview-floating__message"
:class="{ 'is-error': Boolean(errorMessage) }"
:title="errorMessage || developmentProxyTip"
>
{{ errorMessage || developmentProxyTip || '点击顶部播放按钮连接摄像头' }}
</div>
<div v-else class="camera-preview-floating__live">
<i></i>
实时
</div>
</div>
</aside>
</teleport>
</template>
<script lang="ts" setup>
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
import { Message } from '@arco-design/web-vue'
import { fetchAssetDetail } from '@/api/ops/asset'
import type { ThreeMachineRoomDevice } from '@/api/ops/three-machine-room'
import {
destroyHikvisionPlayer,
initializeHikvisionPlayer,
resizeHikvisionPlayer,
startHikvisionPreview,
stopHikvisionPreview,
type HikvisionPreviewConfig,
} from '@/services/hikvisionWebSdk'
/** 摄像头预览浮层属性。 */
interface Props {
visible: boolean
device: ThreeMachineRoomDevice | null
}
/** 摄像头预览浮层事件。 */
interface Emits {
(event: 'update:visible', value: boolean): void
(event: 'manage'): void
}
/** 摄像头连接配置。 */
interface CameraConnectionForm {
host: string
protocol: 1 | 2
httpPort: number
rtspPort: number
webSocketPort?: number
username: string
password: string
channelId: number
streamType: number
proxyEnabled: boolean
secretKey?: string
}
const PLAYER_CONTAINER_ID = 'hikvision-camera-preview-player'
const FIXED_CAMERA_HOST = '192.168.1.101'
const DEFAULT_CAMERA_USERNAME = 'admin'
const DEFAULT_CAMERA_PASSWORD = 'Xzrmyy@12'
const FLOATING_LAYER_WIDTH = 200
const FLOATING_LAYER_HEIGHT = 200
const FLOATING_LAYER_MARGIN = 16
const FLOATING_LAYER_DEFAULT_TOP = 88
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
const floatingLayer = ref<HTMLElement>()
const playerWrapper = ref<HTMLElement>()
const initializing = ref(false)
const starting = ref(false)
const playing = ref(false)
const dragging = ref(false)
const errorMessage = ref('')
const connection = reactive<CameraConnectionForm>(createDefaultConnection())
const floatingLayerPosition = reactive({
x: Math.max(0, window.innerWidth - FLOATING_LAYER_WIDTH - FLOATING_LAYER_MARGIN),
y: Math.min(FLOATING_LAYER_DEFAULT_TOP, Math.max(0, window.innerHeight - FLOATING_LAYER_HEIGHT)),
})
const developmentProxyConfigured = Boolean(import.meta.env.VITE_HIKVISION_PROXY_TARGET)
let resizeObserver: ResizeObserver | null = null
let openSequence = 0
let playbackSequence = 0
let dragPointerId: number | null = null
let dragOffsetX = 0
let dragOffsetY = 0
const floatingTitle = computed(() => props.device?.asset_name || props.device?.asset_code || '摄像头预览')
const floatingLayerStyle = computed(() => ({
transform: `translate3d(${floatingLayerPosition.x}px, ${floatingLayerPosition.y}px, 0)`,
}))
const developmentProxyTip = computed(() => {
if (!import.meta.env.DEV || !connection.proxyEnabled || developmentProxyConfigured) return ''
return '开发环境尚未配置海康代理;请配置 VITE_HIKVISION_PROXY_TARGET或切换为直连。'
})
/** 创建安全的默认连接参数。 */
function createDefaultConnection(): CameraConnectionForm {
return {
host: FIXED_CAMERA_HOST,
protocol: window.location.protocol === 'https:' ? 2 : 1,
httpPort: window.location.protocol === 'https:' ? 443 : 80,
rtspPort: 554,
webSocketPort: undefined,
username: DEFAULT_CAMERA_USERNAME,
password: DEFAULT_CAMERA_PASSWORD,
channelId: 1,
streamType: 2,
proxyEnabled: true,
secretKey: undefined,
}
}
/** 将未知值转换为对象。 */
function asRecord(value: unknown): Record<string, unknown> | null {
return value && typeof value === 'object' && !Array.isArray(value) ? (value as Record<string, unknown>) : null
}
/** 从若干配置对象中读取第一个有效字段。 */
function readConfigValue(records: Record<string, unknown>[], keys: string[]): unknown {
for (const record of records) {
for (const key of keys) {
const value = record[key]
if (value !== undefined && value !== null && value !== '') return value
}
}
return undefined
}
/** 将未知值转换为合法端口或正整数。 */
function toPositiveInteger(value: unknown, fallback?: number): number | undefined {
const parsed = Number(value)
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback
}
/** 将未知值转换为布尔配置。 */
function toBoolean(value: unknown, fallback: boolean): boolean {
if (typeof value === 'boolean') return value
if (typeof value === 'number') return value !== 0
if (typeof value === 'string') {
if (['true', '1', 'yes', 'on'].includes(value.toLowerCase())) return true
if (['false', '0', 'no', 'off'].includes(value.toLowerCase())) return false
}
return fallback
}
/** 校验浮层设备与固定开发代理是否为同一目标,避免凭据被发往错误设备。 */
function matchesDevelopmentProxyTarget(): boolean {
if (!import.meta.env.DEV || !connection.proxyEnabled || !developmentProxyConfigured) return true
try {
const httpTarget = new URL(import.meta.env.VITE_HIKVISION_PROXY_TARGET!)
const httpPort = Number(httpTarget.port) || (httpTarget.protocol === 'https:' ? 443 : 80)
const httpMatches = httpTarget.hostname.toLowerCase() === connection.host.toLowerCase() && httpPort === connection.httpPort
if (!httpMatches || !import.meta.env.VITE_HIKVISION_WS_PROXY_TARGET) return httpMatches
const webSocketTarget = new URL(import.meta.env.VITE_HIKVISION_WS_PROXY_TARGET)
return webSocketTarget.hostname.toLowerCase() === connection.host.toLowerCase()
} catch (_error) {
return false
}
}
/** 解析资产 source_address 中的地址、通道与码流。 */
function parseSourceAddress(sourceAddress: unknown): Record<string, unknown> {
if (typeof sourceAddress !== 'string' || !sourceAddress.trim()) return {}
const source = sourceAddress.trim()
if (source.startsWith('{')) {
try {
return asRecord(JSON.parse(source)) || {}
} catch (_error) {
return { host: source }
}
}
try {
const sourceUrl = new URL(source.includes('://') ? source : `http://${source}`)
const channelMatch = sourceUrl.pathname.match(/\/Streaming\/Channels\/(\d+)/i)
const trackMatch = sourceUrl.pathname.match(/\/Streaming\/tracks\/(\d+)/i)
const legacyChannelMatch = sourceUrl.pathname.match(/\/h264\/ch(\d+)\/(main|sub)\/av_stream/i)
const channelCode = Number(channelMatch?.[1] || trackMatch?.[1] || 0)
const protocol = sourceUrl.protocol === 'https:' ? 2 : 1
return {
host: sourceUrl.hostname,
protocol,
http_port: ['http:', 'https:'].includes(sourceUrl.protocol) ? Number(sourceUrl.port) || (protocol === 2 ? 443 : 80) : undefined,
rtsp_port: sourceUrl.protocol === 'rtsp:' ? Number(sourceUrl.port) || 554 : undefined,
username: sourceUrl.username ? decodeURIComponent(sourceUrl.username) : undefined,
password: sourceUrl.password ? decodeURIComponent(sourceUrl.password) : undefined,
channel_id: channelCode >= 100 ? Math.floor(channelCode / 100) : toPositiveInteger(legacyChannelMatch?.[1]),
stream_type:
channelCode >= 100 ? channelCode % 100 : legacyChannelMatch?.[2]?.toLowerCase() === 'main' ? 1 : legacyChannelMatch ? 2 : undefined,
}
} catch (_error) {
return { host: source }
}
}
/** 将资产详情映射为 WebSDK 连接参数。 */
function applyAssetCameraConfig(asset: Record<string, unknown>): void {
const sourceConfig = parseSourceAddress(asset.source_address)
const nestedConfig =
asRecord(asset.camera_config) || asRecord(asset.video_config) || asRecord(asset.stream_config) || asRecord(asset.hikvision_config)
const records = [nestedConfig, asset, sourceConfig].filter((item): item is Record<string, unknown> => Boolean(item))
const protocolValue = readConfigValue(records, ['camera_protocol', 'protocol', 'http_protocol'])
const normalizedProtocol = String(protocolValue).toLowerCase()
const protocol: 1 | 2 = protocolValue === 2 || normalizedProtocol === '2' || normalizedProtocol === 'https' ? 2 : 1
connection.host = FIXED_CAMERA_HOST
connection.protocol = protocol
connection.httpPort = toPositiveInteger(
readConfigValue(records, ['camera_http_port', 'http_port', 'https_port', 'port']),
protocol === 2 ? 443 : 80
)!
connection.rtspPort = toPositiveInteger(readConfigValue(records, ['camera_rtsp_port', 'rtsp_port']), 554)!
connection.webSocketPort = toPositiveInteger(readConfigValue(records, ['camera_ws_port', 'websocket_port', 'ws_port']))
connection.username = String(readConfigValue(records, ['camera_username', 'username', 'user']) || DEFAULT_CAMERA_USERNAME)
connection.password = String(readConfigValue(records, ['camera_password', 'password']) || DEFAULT_CAMERA_PASSWORD)
connection.channelId = toPositiveInteger(readConfigValue(records, ['camera_channel_id', 'channel_id', 'channel']), 1)!
connection.streamType = toPositiveInteger(readConfigValue(records, ['camera_stream_type', 'stream_type']), 2)!
connection.proxyEnabled = toBoolean(readConfigValue(records, ['camera_proxy_enabled', 'proxy_enabled', 'use_proxy']), true)
connection.secretKey = String(readConfigValue(records, ['camera_secret_key', 'secret_key']) || '') || undefined
}
/** 获取资产详情中的摄像头连接配置。 */
async function loadAssetCameraConfig(): Promise<void> {
if (!props.device?.asset_id) return
const response = (await fetchAssetDetail(props.device.asset_id)) as unknown
const responseRecord = asRecord(response)
if (!responseRecord || Number(responseRecord.code) !== 0) {
throw new Error(String(responseRecord?.message || '摄像头资产详情获取失败'))
}
const details = asRecord(responseRecord.details)
if (!details) throw new Error('摄像头资产详情格式错误')
applyAssetCameraConfig(details)
}
/** 加载资产配置;失败时保留前端预置参数,不阻断摄像头播放。 */
async function loadOptionalAssetCameraConfig(): Promise<void> {
try {
await loadAssetCameraConfig()
} catch (error) {
console.warn('摄像头资产配置获取失败,使用前端预置参数:', error)
Message.warning('摄像头资产配置获取失败,已使用前端预置参数')
}
}
/** 将视频浮层限制在浏览器可视区域内。 */
function clampFloatingLayerPosition(): void {
const maxX = Math.max(0, window.innerWidth - FLOATING_LAYER_WIDTH)
const maxY = Math.max(0, window.innerHeight - FLOATING_LAYER_HEIGHT)
floatingLayerPosition.x = Math.min(Math.max(0, floatingLayerPosition.x), maxX)
floatingLayerPosition.y = Math.min(Math.max(0, floatingLayerPosition.y), maxY)
}
/** 拖动摄像头浮层。 */
function handleDrag(event: PointerEvent): void {
if (!dragging.value || event.pointerId !== dragPointerId) return
floatingLayerPosition.x = event.clientX - dragOffsetX
floatingLayerPosition.y = event.clientY - dragOffsetY
clampFloatingLayerPosition()
}
/** 结束摄像头浮层拖动。 */
function stopDragging(): void {
if (!dragging.value) return
dragging.value = false
dragPointerId = null
window.removeEventListener('pointermove', handleDrag)
window.removeEventListener('pointerup', stopDragging)
window.removeEventListener('pointercancel', stopDragging)
}
/** 从标题栏开始拖动摄像头浮层。 */
function startDragging(event: PointerEvent): void {
if (event.button !== 0 || !floatingLayer.value) return
const rect = floatingLayer.value.getBoundingClientRect()
dragging.value = true
dragPointerId = event.pointerId
dragOffsetX = event.clientX - rect.left
dragOffsetY = event.clientY - rect.top
window.addEventListener('pointermove', handleDrag)
window.addEventListener('pointerup', stopDragging)
window.addEventListener('pointercancel', stopDragging)
event.preventDefault()
}
/** 浏览器尺寸变化时保持浮层可见,并同步播放器大小。 */
function handleViewportResize(): void {
clampFloatingLayerPosition()
resizePlayer()
}
/** 打开浮层并准备连接参数,不在页面加载阶段连接摄像头。 */
async function openPreview(): Promise<void> {
const sequence = ++openSequence
Object.assign(connection, createDefaultConnection())
errorMessage.value = ''
playing.value = false
initializing.value = false
await loadOptionalAssetCameraConfig()
if (sequence !== openSequence || !props.visible) return
await nextTick()
clampFloatingLayerPosition()
resizePlayer()
}
/** 校验参数并开始实时预览。 */
async function startPreview(): Promise<void> {
const sequence = ++playbackSequence
errorMessage.value = ''
if (!connection.host || !connection.username || !connection.password) {
errorMessage.value = '请填写摄像头地址、用户名和密码'
return
}
if (import.meta.env.DEV && connection.proxyEnabled && !developmentProxyConfigured) {
errorMessage.value = '开发环境同源代理尚未配置,请先填写海康代理目标,或切换为直连'
return
}
if (!matchesDevelopmentProxyTarget()) {
errorMessage.value = '当前摄像头地址与固定开发代理目标不一致,请修改代理配置后重启开发服务器'
return
}
starting.value = true
try {
initializing.value = true
await nextTick()
await new Promise<void>((resolve) => window.requestAnimationFrame(() => resolve()))
await initializeHikvisionPlayer(PLAYER_CONTAINER_ID, {
onError: (message) => {
errorMessage.value = message
playing.value = false
},
onPerformanceLack: () => Message.warning('当前浏览器性能不足,建议关闭其他视频窗口或使用子码流'),
})
if (sequence !== playbackSequence || !props.visible) return
initializing.value = false
observePlayerSize()
const config: HikvisionPreviewConfig = { ...connection }
await startHikvisionPreview(config)
if (sequence !== playbackSequence || !props.visible) {
await stopHikvisionPreview()
return
}
playing.value = true
resizePlayer()
} catch (error) {
if (sequence !== playbackSequence || !props.visible) return
playing.value = false
errorMessage.value = error instanceof Error ? error.message : '摄像头播放失败'
} finally {
initializing.value = false
starting.value = false
}
}
/** 停止当前摄像头预览。 */
async function stopPreview(): Promise<void> {
playbackSequence += 1
await stopHikvisionPreview()
playing.value = false
}
/** 根据浮层中的容器大小同步 SDK 画布。 */
function resizePlayer(): void {
const rect = playerWrapper.value?.getBoundingClientRect()
if (rect) resizeHikvisionPlayer(rect.width, rect.height)
}
/** 监听播放器容器尺寸变化。 */
function observePlayerSize(): void {
resizeObserver?.disconnect()
if (!playerWrapper.value) return
resizeObserver = new ResizeObserver(() => resizePlayer())
resizeObserver.observe(playerWrapper.value)
resizePlayer()
}
/** 从视频浮层切换到设备管理。 */
function openDeviceManagement(): void {
closePreviewLayer()
emit('manage')
}
/** 关闭视频浮层。 */
function closePreviewLayer(): void {
emit('update:visible', false)
}
/** 浮层关闭后清理播放会话和敏感字段。 */
function handleClosed(): void {
openSequence += 1
playbackSequence += 1
resizeObserver?.disconnect()
resizeObserver = null
void stopPreview()
connection.password = ''
connection.secretKey = undefined
errorMessage.value = ''
}
watch(
() => props.visible,
(visible) => {
if (visible) void openPreview()
else handleClosed()
},
{ immediate: true }
)
onMounted(() => {
window.addEventListener('resize', handleViewportResize)
clampFloatingLayerPosition()
})
onBeforeUnmount(() => {
openSequence += 1
playbackSequence += 1
stopDragging()
resizeObserver?.disconnect()
window.removeEventListener('resize', handleViewportResize)
void destroyHikvisionPlayer()
connection.password = ''
connection.secretKey = undefined
})
</script>
<style scoped lang="less">
.camera-preview-floating {
position: fixed;
z-index: 2001;
top: 0;
left: 0;
display: flex;
flex-direction: column;
box-sizing: border-box;
width: 200px;
height: 200px;
overflow: hidden;
border: 1px solid rgba(86, 151, 211, 0.72);
border-radius: 8px;
background: #030811;
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.48);
user-select: none;
will-change: transform;
&__header {
display: flex;
flex: 0 0 32px;
align-items: center;
gap: 4px;
padding: 0 4px 0 7px;
color: rgba(255, 255, 255, 0.9);
background: linear-gradient(90deg, #123f68, #0a223a);
cursor: grab;
touch-action: none;
}
&.is-dragging &__header {
cursor: grabbing;
}
&__drag-mark {
color: rgba(255, 255, 255, 0.52);
font-size: 14px;
}
&__title {
min-width: 0;
flex: 1;
overflow: hidden;
font-size: 12px;
font-weight: 600;
text-overflow: ellipsis;
white-space: nowrap;
}
&__actions {
display: flex;
flex: none;
align-items: center;
gap: 1px;
}
&__action {
display: inline-flex;
width: 22px;
height: 22px;
align-items: center;
justify-content: center;
padding: 0;
border: 0;
border-radius: 4px;
color: rgba(255, 255, 255, 0.82);
font-size: 12px;
line-height: 1;
background: transparent;
cursor: pointer;
&:hover:not(:disabled) {
color: #fff;
background: rgba(255, 255, 255, 0.16);
}
&:disabled {
color: rgba(255, 255, 255, 0.28);
cursor: not-allowed;
}
}
&__player {
position: relative;
min-height: 0;
flex: 1;
overflow: hidden;
background: #030811;
}
&__message {
position: absolute;
z-index: 2;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px;
padding: 14px;
color: rgba(255, 255, 255, 0.78);
font-size: 12px;
line-height: 1.5;
text-align: center;
background: radial-gradient(circle, rgba(19, 62, 99, 0.36), rgba(3, 8, 17, 0.82));
pointer-events: none;
&.is-error {
color: #ffb3b3;
}
}
&__live {
position: absolute;
z-index: 2;
top: 6px;
right: 7px;
display: flex;
align-items: center;
gap: 4px;
padding: 2px 5px;
border-radius: 8px;
color: rgba(255, 255, 255, 0.9);
font-size: 10px;
background: rgba(0, 0, 0, 0.46);
pointer-events: none;
i {
width: 5px;
height: 5px;
border-radius: 50%;
background: #38d996;
box-shadow: 0 0 6px #38d996;
}
}
}
.camera-preview__canvas {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}
</style>

View File

@@ -0,0 +1,382 @@
<template>
<a-modal v-model:visible="dialogVisible" :title="deviceDisplayName" :width="820" :footer="false" :mask-closable="false">
<a-spin :loading="detailsLoading" style="width: 100%">
<a-descriptions :column="3" bordered size="small">
<a-descriptions-item label="资产编码">{{ device?.asset_code || '-' }}</a-descriptions-item>
<a-descriptions-item label="设备分类">{{ device?.category_name || device?.category_code || '-' }}</a-descriptions-item>
<a-descriptions-item label="放置类型">{{ placementTypeText }}</a-descriptions-item>
<a-descriptions-item v-if="device?.placement_type === 'rack'" label="机柜 ID">{{ device.rack_id || '-' }}</a-descriptions-item>
<a-descriptions-item v-if="device?.placement_type === 'rack'" label="U 位">{{ unitRange }}</a-descriptions-item>
<a-descriptions-item label="功耗">{{ device?.power_consumption || 0 }} W</a-descriptions-item>
</a-descriptions>
<section class="management-section">
<div class="section-header">
<strong>实时状态与告警</strong>
<a-button size="mini" :loading="observabilityLoading" @click="loadObservability">刷新</a-button>
</div>
<a-alert v-if="observabilityError" type="warning" show-icon>{{ observabilityError }}</a-alert>
<div v-else class="observability-summary">
<a-statistic title="监控资源" :value="observabilityResources.length" />
<a-statistic title="告警分组" :value="observabilityAlerts.length" />
<span v-if="!observabilityResources.length" class="empty-tip">未绑定监控资源或暂无运行数据</span>
</div>
<div v-if="observabilityResources.length" class="runtime-list">
<div v-for="resource in observabilityResources" :key="resource.resource_uid" class="runtime-item">
<div class="runtime-item__header">
<strong>{{ resource.display_name || resource.resource_uid }}</strong>
<a-tag :color="getRuntimeStatusColor(resource.status)">{{ resource.status || 'unknown' }}</a-tag>
</div>
<div v-if="resource.metrics?.length" class="metric-list">
<span v-for="metric in resource.metrics" :key="`${resource.resource_uid}-${metric.name}`">
{{ metric.name }}{{ formatMetricValue(metric.value, metric.unit) }}
</span>
</div>
<span v-else class="empty-tip">暂无指标数据</span>
</div>
</div>
</section>
<section class="management-section">
<div class="section-header">
<strong>监控资源绑定</strong>
<a-button size="mini" :loading="bindingsLoading" @click="loadBindings">刷新</a-button>
</div>
<div class="binding-form">
<a-select v-model="selectedResourceUid" allow-search placeholder="请选择可绑定监控资源" :loading="resourceOptionsLoading">
<a-option v-for="option in resourceOptions" :key="option.value" :value="option.value">{{ option.label }}</a-option>
</a-select>
<a-input v-model="bindingDisplayName" placeholder="展示名称(可选)" />
<a-button type="primary" :loading="bindingSaving" :disabled="!selectedResourceUid" @click="bindResource">绑定</a-button>
</div>
<div v-if="bindings.length" class="binding-list">
<div v-for="binding in bindings" :key="binding.resource_uid" class="binding-item">
<div>
<strong>{{ binding.display_name || binding.resource_uid }}</strong>
<span>{{ binding.resource_uid }}</span>
</div>
<a-button size="mini" status="danger" @click="unbindResource(binding)">解除绑定</a-button>
</div>
</div>
<a-empty v-else description="暂未绑定监控资源" />
</section>
<div class="dialog-footer">
<a-button @click="dialogVisible = false">关闭</a-button>
<a-button status="danger" @click="confirmRemovePlacement">解除设备位置</a-button>
</div>
</a-spin>
</a-modal>
</template>
<script lang="ts" setup>
import { computed, ref, watch } from 'vue'
import { Message, Modal } from '@arco-design/web-vue'
import { fetchAssetResourceBindings, linkAssetResource, unlinkAssetResource } from '@/api/ops/asset'
import { fetchControlResourceOptions, type OptionItem } from '@/api/ops/dcControl'
import {
fetchThreeMachineRoomDeviceObservability,
removeThreeMachineRoomDevicePlacement,
type ThreeMachineRoomDevice,
type ThreeMachineRoomObservability,
} from '@/api/ops/three-machine-room'
/** 资产监控资源绑定。 */
interface AssetResourceBinding {
resource_uid: string
display_name?: string
resource_category?: string
service_identity?: string
}
/** 设备管理弹窗属性。 */
interface Props {
visible: boolean
roomId: number
device: ThreeMachineRoomDevice | null
layoutVersion: number
}
/** 设备管理弹窗事件。 */
interface Emits {
(event: 'update:visible', value: boolean): void
(event: 'placement-removed'): void
(event: 'observability-loaded', assetId: number, details: ThreeMachineRoomObservability): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
const dialogVisible = computed({
get: () => props.visible,
set: (value) => emit('update:visible', value),
})
const detailsLoading = ref(false)
const observabilityLoading = ref(false)
const bindingsLoading = ref(false)
const resourceOptionsLoading = ref(false)
const bindingSaving = ref(false)
const observability = ref<ThreeMachineRoomObservability | null>(null)
const observabilityError = ref('')
const bindings = ref<AssetResourceBinding[]>([])
const resourceOptions = ref<OptionItem[]>([])
const selectedResourceUid = ref<string>()
const bindingDisplayName = ref('')
const deviceDisplayName = computed(() => props.device?.asset_name || props.device?.asset_code || `设备 ${props.device?.asset_id || ''}`)
const placementTypeText = computed(() => (props.device?.placement_type === 'room' ? '机房独立设备' : '机柜设备'))
const unitRange = computed(() => {
const start = Number(props.device?.unit_start) || 0
const end = Number(props.device?.unit_end) || start + (Number(props.device?.occupied_units) || 1) - 1
return start > 0 ? `U${start}${end > start ? ` - U${end}` : ''}` : '-'
})
const observabilityResources = computed(() => (Array.isArray(observability.value?.resources) ? observability.value.resources : []))
const observabilityAlerts = computed(() => (Array.isArray(observability.value?.alerts) ? observability.value.alerts : []))
/** 获取资源运行状态标签颜色。 */
function getRuntimeStatusColor(status?: string): string {
if (['online', 'up', 'healthy', 'normal', 'running', 'success'].includes(status || '')) return 'green'
if (['warning', 'degraded'].includes(status || '')) return 'orange'
if (['offline', 'down', 'error', 'critical', 'unhealthy', 'failed'].includes(status || '')) return 'red'
return 'gray'
}
/** 格式化监控指标值。 */
function formatMetricValue(value?: string | number | null, unit?: string): string {
return value === null || value === undefined ? '--' : `${value}${unit || ''}`
}
/** 获取设备可观测详情。 */
async function loadObservability(): Promise<void> {
if (!props.device?.asset_id) return
observabilityLoading.value = true
observabilityError.value = ''
try {
const details = await fetchThreeMachineRoomDeviceObservability(props.device.asset_id)
observability.value = details
emit('observability-loaded', props.device.asset_id, details)
} catch (error) {
observability.value = null
observabilityError.value = error instanceof Error ? error.message : '设备实时信息获取失败'
} finally {
observabilityLoading.value = false
}
}
/** 获取资产当前的资源绑定。 */
async function loadBindings(): Promise<void> {
if (!props.device?.asset_id) return
bindingsLoading.value = true
try {
const response: any = await fetchAssetResourceBindings(props.device.asset_id)
if (response.code !== 0) throw new Error(response.message || '资源绑定获取失败')
bindings.value = Array.isArray(response.details) ? response.details : []
} catch (error) {
bindings.value = []
Message.error(error instanceof Error ? error.message : '资源绑定获取失败')
} finally {
bindingsLoading.value = false
}
}
/** 获取可绑定监控资源选项。 */
async function loadResourceOptions(): Promise<void> {
resourceOptionsLoading.value = true
try {
const response = await fetchControlResourceOptions(
props.device?.placement_type === 'room' ? { resource_category: 'room_device' } : undefined
)
if (response.code !== 0) throw new Error(response.message || '监控资源获取失败')
resourceOptions.value = response.details?.list || []
} catch (error) {
resourceOptions.value = []
Message.error(error instanceof Error ? error.message : '监控资源获取失败')
} finally {
resourceOptionsLoading.value = false
}
}
/** 绑定所选监控资源。 */
async function bindResource(): Promise<void> {
if (!props.device?.asset_id || !selectedResourceUid.value) return
bindingSaving.value = true
try {
const response: any = await linkAssetResource({
asset_id: props.device.asset_id,
resource_uid: selectedResourceUid.value,
display_name: bindingDisplayName.value.trim() || undefined,
})
if (response.code !== 0) throw new Error(response.message || '资源绑定失败')
selectedResourceUid.value = undefined
bindingDisplayName.value = ''
Message.success('监控资源绑定成功')
await Promise.all([loadBindings(), loadObservability()])
} catch (error) {
Message.error(error instanceof Error ? error.message : '资源绑定失败')
} finally {
bindingSaving.value = false
}
}
/** 解除指定监控资源绑定。 */
async function unbindResource(binding: AssetResourceBinding): Promise<void> {
if (!props.device?.asset_id) return
try {
const response: any = await unlinkAssetResource(props.device.asset_id, binding.resource_uid)
if (response.code !== 0) throw new Error(response.message || '解除绑定失败')
Message.success('监控资源已解除')
await Promise.all([loadBindings(), loadObservability()])
} catch (error) {
Message.error(error instanceof Error ? error.message : '解除绑定失败')
}
}
/** 确认并解除设备当前位置。 */
function confirmRemovePlacement(): void {
if (!props.device?.asset_id) return
Modal.confirm({
title: '确认解除设备位置',
content: `解除后将释放 ${placementTypeText.value === '机柜设备' ? '占用的 U 位和机柜功耗' : '机房空间位置'}`,
onBeforeOk: async () => {
try {
await removeThreeMachineRoomDevicePlacement(props.roomId, props.device!.asset_id, props.layoutVersion)
Message.success('设备位置已解除')
dialogVisible.value = false
emit('placement-removed')
return true
} catch (error) {
Message.error(error instanceof Error ? error.message : '设备位置解除失败')
return false
}
},
})
}
/** 打开弹窗时加载设备详情和资源数据。 */
async function loadDialogData(): Promise<void> {
detailsLoading.value = true
try {
await Promise.all([loadObservability(), loadBindings(), loadResourceOptions()])
} finally {
detailsLoading.value = false
}
}
watch(
() => props.visible,
(visible) => {
if (visible) {
void loadDialogData()
return
}
observability.value = null
observabilityError.value = ''
bindings.value = []
resourceOptions.value = []
selectedResourceUid.value = undefined
bindingDisplayName.value = ''
}
)
</script>
<style scoped lang="less">
.management-section {
margin-top: 20px;
padding-top: 16px;
border-top: 1px solid var(--color-border-2);
}
.section-header,
.dialog-footer,
.binding-item,
.binding-form,
.observability-summary {
display: flex;
align-items: center;
}
.section-header {
justify-content: space-between;
margin-bottom: 12px;
}
.observability-summary {
gap: 36px;
}
.runtime-list {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
margin-top: 12px;
}
.runtime-item {
min-width: 0;
padding: 10px 12px;
border: 1px solid var(--color-border-2);
border-radius: 4px;
&__header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
}
.metric-list {
display: flex;
flex-wrap: wrap;
gap: 4px 12px;
margin-top: 8px;
color: var(--color-text-2);
font-size: 12px;
}
.empty-tip,
.binding-item span {
color: var(--color-text-3);
font-size: 12px;
}
.binding-form {
gap: 10px;
> :first-child {
flex: 2;
}
> :nth-child(2) {
flex: 1;
}
}
.binding-list {
margin-top: 12px;
border: 1px solid var(--color-border-2);
border-radius: 4px;
}
.binding-item {
justify-content: space-between;
gap: 12px;
padding: 9px 12px;
border-bottom: 1px solid var(--color-border-1);
&:last-child {
border-bottom: 0;
}
> div {
display: flex;
flex-direction: column;
gap: 3px;
}
}
.dialog-footer {
justify-content: flex-end;
gap: 10px;
margin-top: 22px;
}
</style>

View File

@@ -0,0 +1,117 @@
<template>
<a-modal
v-model:visible="dialogVisible"
:title="`${rackDisplayName} · 调整布局`"
:width="620"
:mask-closable="false"
:on-before-ok="handleBeforeOk"
>
<a-alert type="info" show-icon>保存时后端会同时校验机柜边界机柜重叠及独立设备重叠</a-alert>
<a-form :model="formData" layout="vertical" class="rack-layout-form">
<a-grid :cols="2" :col-gap="16">
<a-grid-item>
<a-form-item label="业务行号"><a-input-number v-model="formData.row" :min="0" :precision="0" /></a-form-item>
</a-grid-item>
<a-grid-item>
<a-form-item label="业务列号"><a-input-number v-model="formData.column" :min="0" :precision="0" /></a-form-item>
</a-grid-item>
<a-grid-item>
<a-form-item label="X 坐标(米)" required><a-input-number v-model="formData.positionX" :min="0" :precision="2" /></a-form-item>
</a-grid-item>
<a-grid-item>
<a-form-item label="Y 坐标(米)" required><a-input-number v-model="formData.positionY" :min="0" :precision="2" /></a-form-item>
</a-grid-item>
<a-grid-item>
<a-form-item label="Z 坐标(米)" required><a-input-number v-model="formData.positionZ" :min="0" :precision="2" /></a-form-item>
</a-grid-item>
<a-grid-item>
<a-form-item label="绕 Y 轴旋转(度)" required>
<a-input-number v-model="formData.rotationY" :min="0" :max="359.99" :precision="2" />
</a-form-item>
</a-grid-item>
</a-grid>
</a-form>
</a-modal>
</template>
<script lang="ts" setup>
import { computed, reactive, watch } from 'vue'
import { Message } from '@arco-design/web-vue'
import { saveThreeMachineRoomRackLayout, type ThreeMachineRoomRack } from '@/api/ops/three-machine-room'
/** 机柜布局弹窗属性。 */
interface Props {
visible: boolean
roomId: number
rack: ThreeMachineRoomRack | null
layoutVersion: number
}
/** 机柜布局弹窗事件。 */
interface Emits {
(event: 'update:visible', value: boolean): void
(event: 'success'): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
const dialogVisible = computed({
get: () => props.visible,
set: (value) => emit('update:visible', value),
})
const rackDisplayName = computed(() => props.rack?.name || props.rack?.code || `机柜 ${props.rack?.id || ''}`)
const formData = reactive({ row: 0, column: 0, positionX: 0, positionY: 0, positionZ: 0, rotationY: 0 })
/** 使用当前机柜数据重置布局表单。 */
function resetForm(): void {
formData.row = Number(props.rack?.row) || 0
formData.column = Number(props.rack?.column) || 0
formData.positionX = Number(props.rack?.transform?.position_x) || 0
formData.positionY = Number(props.rack?.transform?.position_y) || 0
formData.positionZ = Number(props.rack?.transform?.position_z) || 0
formData.rotationY = Number(props.rack?.transform?.rotation_y) || 0
}
/** 校验并保存当前机柜布局。 */
async function handleBeforeOk(): Promise<boolean> {
if (!props.rack?.id || props.layoutVersion <= 0) {
Message.error('机柜或布局版本无效,请刷新场景后重试')
return false
}
try {
await saveThreeMachineRoomRackLayout(props.roomId, props.layoutVersion, [
{
rack_id: props.rack.id,
row: formData.row,
column: formData.column,
position_x: formData.positionX,
position_y: formData.positionY,
position_z: formData.positionZ,
rotation_y: formData.rotationY,
},
])
emit('success')
return true
} catch (error) {
Message.error(error instanceof Error ? error.message : '机柜布局保存失败')
return false
}
}
watch(
() => props.visible,
(visible) => {
if (visible) resetForm()
}
)
</script>
<style scoped lang="less">
.rack-layout-form {
margin-top: 18px;
:deep(.arco-input-number) {
width: 100%;
}
}
</style>

View File

@@ -0,0 +1,461 @@
<template>
<a-modal
v-model:visible="dialogVisible"
:title="`${rackDisplayName} · 设备上架`"
:width="880"
:mask-closable="false"
:on-before-ok="handleBeforeOk"
@cancel="handleCancel"
>
<div class="dialog-actions">
<a-button size="small" @click="handleEditLayout">调整机柜坐标</a-button>
</div>
<a-spin :loading="unitsLoading" style="width: 100%">
<div class="placement-layout">
<section class="unit-panel">
<div class="panel-header">
<div>
<strong>选择起始 U </strong>
<span>U 位从下向上递增</span>
</div>
<a-space size="small">
<a-tag color="green">可用 {{ unitSummary.available }}</a-tag>
<a-tag color="blue">占用 {{ unitSummary.occupied }}</a-tag>
<a-tag color="orange">预留 {{ unitSummary.reserved }}</a-tag>
<a-tag color="gray">禁用 {{ unitSummary.disabled }}</a-tag>
</a-space>
</div>
<div v-if="sortedUnits.length" class="unit-grid">
<button
v-for="unit in sortedUnits"
:key="unit.unit_number"
type="button"
class="unit-tile"
:class="[unit.status, { selected: selectedUnitNumbers.has(unit.unit_number) }]"
:disabled="unit.status !== 'available'"
:title="getUnitTitle(unit)"
@click="selectStartUnit(unit)"
>
<strong>U{{ unit.unit_number }}</strong>
<span>{{ getUnitStatusText(unit) }}</span>
</button>
</div>
<a-empty v-else description="该机柜暂无 U 位数据" />
</section>
<a-form ref="formRef" class="placement-form" :model="formData" :rules="rules" layout="vertical">
<a-form-item field="assetId" label="选择设备" required>
<a-select
v-model="formData.assetId"
allow-search
:filter-option="false"
:loading="assetsLoading"
placeholder="请选择未放置设备"
@search="handleAssetSearch"
@change="handleAssetChange"
>
<a-option v-for="asset in assetList" :key="asset.id" :value="asset.id">
{{ asset.assetName }}{{ asset.assetCode }}
</a-option>
</a-select>
</a-form-item>
<a-form-item field="startUnit" label="起始 U 位" required>
<a-input-number v-model="formData.startUnit" :min="1" :max="rackHeight" placeholder="请从左侧选择" style="width: 100%" />
</a-form-item>
<a-form-item field="occupiedUnits" label="占用 U 位数" required>
<a-input-number
v-model="formData.occupiedUnits"
:min="1"
:max="maxOccupiedUnits"
placeholder="请输入连续占用数量"
style="width: 100%"
/>
</a-form-item>
<a-form-item label="目标区间">
<a-alert :type="rangeAvailable ? 'success' : 'error'">
U{{ formData.startUnit }} - U{{ selectedEndUnit }}
{{ rangeAvailable ? ',区间可用' : ',包含不可用 U 位' }}
</a-alert>
</a-form-item>
<a-form-item field="powerConsumption" label="功耗W">
<a-input-number v-model="formData.powerConsumption" :min="0" placeholder="请输入功耗" style="width: 100%" />
</a-form-item>
</a-form>
</div>
</a-spin>
</a-modal>
</template>
<script lang="ts" setup>
import { computed, onBeforeUnmount, reactive, ref, watch } from 'vue'
import { Message } from '@arco-design/web-vue'
import { fetchAssetAll } from '@/api/ops/asset'
import { saveThreeMachineRoomRackPlacement, type ThreeMachineRoomRack } from '@/api/ops/three-machine-room'
import { fetchRackUnits, type RackUnitItem, type RackUnitStatus } from '@/api/ops/unit'
/** 待上架资产。 */
interface PlacementAsset {
id: number
assetCode: string
assetName: string
occupiedUnits: number
powerConsumption: number
}
/** 设备上架弹窗属性。 */
interface Props {
visible: boolean
roomId: number
rack: ThreeMachineRoomRack | null
layoutVersion: number
}
/** 设备上架弹窗事件。 */
interface Emits {
(event: 'update:visible', value: boolean): void
(event: 'success'): void
(event: 'edit-layout'): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
const dialogVisible = computed({
get: () => props.visible,
set: (value) => emit('update:visible', value),
})
const formRef = ref()
const unitsLoading = ref(false)
const assetsLoading = ref(false)
const units = ref<RackUnitItem[]>([])
const assetList = ref<PlacementAsset[]>([])
let assetSearchTimer: number | undefined
const formData = reactive({
assetId: undefined as number | undefined,
startUnit: 1,
occupiedUnits: 1,
powerConsumption: 0,
})
const rules = {
assetId: [{ required: true, message: '请选择设备' }],
startUnit: [{ required: true, message: '请选择起始 U 位' }],
occupiedUnits: [{ required: true, message: '请输入占用 U 位数' }],
}
const rackHeight = computed(() => Math.max(1, Number(props.rack?.height) || 42))
const rackDisplayName = computed(() => props.rack?.name || props.rack?.code || `机柜 ${props.rack?.id || ''}`)
const sortedUnits = computed(() => [...units.value].sort((left, right) => right.unit_number - left.unit_number))
const unitMap = computed(() => new Map(units.value.map((unit) => [unit.unit_number, unit])))
const maxOccupiedUnits = computed(() => Math.max(1, rackHeight.value - formData.startUnit + 1))
const selectedEndUnit = computed(() => formData.startUnit + formData.occupiedUnits - 1)
const selectedUnitNumbers = computed(() => {
const result = new Set<number>()
for (let unitNumber = formData.startUnit; unitNumber <= selectedEndUnit.value; unitNumber += 1) {
result.add(unitNumber)
}
return result
})
const rangeAvailable = computed(() => {
if (!units.value.length || selectedEndUnit.value > rackHeight.value) return false
return [...selectedUnitNumbers.value].every((unitNumber) => unitMap.value.get(unitNumber)?.status === 'available')
})
const unitSummary = computed(() =>
units.value.reduce(
(summary, unit) => {
summary[unit.status] += 1
return summary
},
{ available: 0, occupied: 0, reserved: 0, disabled: 0 } as Record<RackUnitStatus, number>
)
)
/** 获取 U 位状态文案。 */
function getUnitStatusText(unit: RackUnitItem): string {
const statusText: Record<RackUnitStatus, string> = {
available: '可用',
occupied: '占用',
reserved: '预留',
disabled: '禁用',
}
return statusText[unit.status]
}
/** 获取 U 位悬浮提示。 */
function getUnitTitle(unit: RackUnitItem): string {
if (unit.status === 'occupied') return `U${unit.unit_number} · ${unit.asset_name || unit.asset_code}`
if (unit.status === 'reserved') return `U${unit.unit_number} · ${unit.reserved_for}`
return `U${unit.unit_number} · ${getUnitStatusText(unit)}`
}
/** 选择设备并回填默认参数。 */
function handleAssetChange(value: unknown): void {
const asset = assetList.value.find((item) => String(item.id) === String(value))
if (!asset) return
formData.assetId = asset.id
formData.occupiedUnits = Math.min(Math.max(1, asset.occupiedUnits), maxOccupiedUnits.value)
formData.powerConsumption = asset.powerConsumption
}
/** 选择连续 U 位的起点。 */
function selectStartUnit(unit: RackUnitItem): void {
if (unit.status !== 'available') return
formData.startUnit = unit.unit_number
formData.occupiedUnits = Math.min(formData.occupiedUnits, maxOccupiedUnits.value)
}
/** 获取机柜最新 U 位状态。 */
async function loadRackUnits(): Promise<void> {
if (!props.rack?.id) return
unitsLoading.value = true
try {
const response = await fetchRackUnits(props.rack.id)
if (response.code !== 0) throw new Error(response.message || 'U 位获取失败')
units.value = response.details?.units || []
const firstAvailable = units.value
.filter((unit) => unit.status === 'available')
.sort((left, right) => left.unit_number - right.unit_number)[0]
formData.startUnit = firstAvailable?.unit_number || 1
} catch (error) {
units.value = []
Message.error(error instanceof Error ? error.message : 'U 位获取失败')
} finally {
unitsLoading.value = false
}
}
/** 获取未放置资产列表。 */
async function loadAssetList(keyword?: string): Promise<void> {
assetsLoading.value = true
try {
const response: any = await fetchAssetAll({ keyword: keyword || undefined })
if (response.code !== 0) throw new Error(response.message || '设备列表获取失败')
assetList.value = (Array.isArray(response.details) ? response.details : [])
.filter((item: any) => item.placement_type === 'unplaced')
.map((item: any) => ({
id: item.id,
assetCode: item.asset_code || String(item.id),
assetName: item.asset_name || item.name || `设备 ${item.id}`,
occupiedUnits: Number(item.occupied_units) || 1,
powerConsumption: Number(item.power_consumption) || 0,
}))
} catch (error) {
assetList.value = []
Message.error(error instanceof Error ? error.message : '设备列表获取失败')
} finally {
assetsLoading.value = false
}
}
/** 延迟搜索待上架设备。 */
function handleAssetSearch(keyword: string): void {
if (assetSearchTimer !== undefined) window.clearTimeout(assetSearchTimer)
assetSearchTimer = window.setTimeout(() => {
void loadAssetList(keyword.trim() || undefined)
}, 300)
}
/** 校验并保存设备机柜及 U 位。 */
async function handleBeforeOk(): Promise<boolean> {
try {
await formRef.value?.validate()
} catch {
return false
}
if (!props.rack?.id || !formData.assetId) {
Message.error('机柜或设备信息不完整')
return false
}
if (props.layoutVersion <= 0) {
Message.error('机房布局版本无效,请刷新场景后重试')
return false
}
if (!rangeAvailable.value) {
Message.error('请选择连续且可用的 U 位')
return false
}
try {
await saveThreeMachineRoomRackPlacement(props.roomId, formData.assetId, {
expectedVersion: props.layoutVersion,
rackId: props.rack.id,
startUnit: formData.startUnit,
occupiedUnits: formData.occupiedUnits,
powerConsumption: formData.powerConsumption,
})
emit('success')
return true
} catch (error) {
Message.error(error instanceof Error ? error.message : '设备上架失败')
return false
}
}
/** 重置弹窗数据。 */
function resetDialog(): void {
units.value = []
assetList.value = []
formData.assetId = undefined
formData.startUnit = 1
formData.occupiedUnits = 1
formData.powerConsumption = 0
formRef.value?.resetFields()
}
/** 取消设备上架。 */
function handleCancel(): void {
resetDialog()
}
/** 切换到机柜布局编辑弹窗。 */
function handleEditLayout(): void {
emit('update:visible', false)
emit('edit-layout')
}
watch(
() => props.visible,
(visible) => {
if (visible) {
void Promise.all([loadRackUnits(), loadAssetList()])
} else {
resetDialog()
}
}
)
watch(
() => formData.startUnit,
() => {
formData.occupiedUnits = Math.min(formData.occupiedUnits, maxOccupiedUnits.value)
}
)
onBeforeUnmount(() => {
if (assetSearchTimer !== undefined) window.clearTimeout(assetSearchTimer)
})
</script>
<style scoped lang="less">
.dialog-actions {
display: flex;
justify-content: flex-end;
margin-bottom: 10px;
}
.placement-layout {
display: grid;
grid-template-columns: minmax(0, 1fr) 280px;
gap: 20px;
min-height: 480px;
}
.unit-panel {
min-width: 0;
padding-right: 20px;
border-right: 1px solid var(--color-border-2);
}
.panel-header {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
gap: 10px;
margin-bottom: 14px;
> div:first-child {
display: flex;
flex-direction: column;
gap: 3px;
}
span {
color: var(--color-text-3);
font-size: 12px;
}
}
.unit-grid {
display: grid;
grid-template-columns: repeat(6, minmax(56px, 1fr));
gap: 8px;
max-height: 430px;
overflow-y: auto;
padding: 3px;
}
.unit-tile {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
padding: 7px 6px;
border: 1px solid transparent;
border-radius: 4px;
font-size: 12px;
cursor: pointer;
span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
&.available {
color: #006d22;
border-color: #7be188;
background: #e8ffea;
}
&.occupied {
color: #0e42d2;
border-color: #94bfff;
background: #e8f3ff;
}
&.reserved {
color: #b54708;
border-color: #ffb65d;
background: #fff7e8;
}
&.disabled {
color: #4e5969;
border-color: #c9cdd4;
background: #f2f3f5;
}
&.selected {
outline: 3px solid rgba(114, 46, 209, 0.72);
outline-offset: 1px;
}
&:disabled {
cursor: not-allowed;
}
}
.placement-form {
min-width: 0;
}
@media (max-width: 900px) {
.placement-layout {
grid-template-columns: 1fr;
}
.unit-panel {
padding-right: 0;
padding-bottom: 16px;
border-right: 0;
border-bottom: 1px solid var(--color-border-2);
}
}
</style>

View File

@@ -0,0 +1,223 @@
<template>
<a-modal
v-model:visible="dialogVisible"
title="放置机房独立设备"
:width="720"
:mask-closable="false"
:on-before-ok="handleBeforeOk"
@cancel="resetForm"
>
<a-alert type="info" show-icon>适用于空调摄像头智能插座等不占用机柜 U 位的设备</a-alert>
<a-form ref="formRef" :model="formData" :rules="rules" layout="vertical" class="device-placement-form">
<a-form-item field="assetId" label="选择设备" required>
<a-select
v-model="formData.assetId"
allow-search
:filter-option="false"
:loading="assetsLoading"
placeholder="请选择未放置设备"
@search="handleAssetSearch"
>
<a-option v-for="asset in assetList" :key="asset.id" :value="asset.id">{{ asset.name }}{{ asset.code }}</a-option>
</a-select>
</a-form-item>
<strong class="section-title">底面中心坐标</strong>
<a-grid :cols="3" :col-gap="16">
<a-grid-item>
<a-form-item label="X" required><a-input-number v-model="formData.positionX" :min="0" :precision="2" /></a-form-item>
</a-grid-item>
<a-grid-item>
<a-form-item label="Y" required><a-input-number v-model="formData.positionY" :min="0" :precision="2" /></a-form-item>
</a-grid-item>
<a-grid-item>
<a-form-item label="Z" required><a-input-number v-model="formData.positionZ" :min="0" :precision="2" /></a-form-item>
</a-grid-item>
</a-grid>
<strong class="section-title">旋转角度</strong>
<a-grid :cols="3" :col-gap="16">
<a-grid-item>
<a-form-item label="X"><a-input-number v-model="formData.rotationX" :min="0" :max="359.99" :precision="2" /></a-form-item>
</a-grid-item>
<a-grid-item>
<a-form-item label="Y"><a-input-number v-model="formData.rotationY" :min="0" :max="359.99" :precision="2" /></a-form-item>
</a-grid-item>
<a-grid-item>
<a-form-item label="Z"><a-input-number v-model="formData.rotationZ" :min="0" :max="359.99" :precision="2" /></a-form-item>
</a-grid-item>
</a-grid>
<strong class="section-title">设备尺寸</strong>
<a-grid :cols="3" :col-gap="16">
<a-grid-item>
<a-form-item label="宽度" required><a-input-number v-model="formData.sceneWidth" :min="0.01" :precision="2" /></a-form-item>
</a-grid-item>
<a-grid-item>
<a-form-item label="高度" required><a-input-number v-model="formData.sceneHeight" :min="0.01" :precision="2" /></a-form-item>
</a-grid-item>
<a-grid-item>
<a-form-item label="深度" required><a-input-number v-model="formData.sceneDepth" :min="0.01" :precision="2" /></a-form-item>
</a-grid-item>
</a-grid>
</a-form>
</a-modal>
</template>
<script lang="ts" setup>
import { computed, onBeforeUnmount, reactive, ref, watch } from 'vue'
import { Message } from '@arco-design/web-vue'
import { fetchAssetAll } from '@/api/ops/asset'
import { saveThreeMachineRoomDevicePlacement, type ThreeMachineRoomScene } from '@/api/ops/three-machine-room'
/** 待放置独立设备。 */
interface UnplacedAsset {
id: number
name: string
code: string
}
/** 独立设备放置弹窗属性。 */
interface Props {
visible: boolean
roomId: number
scene: ThreeMachineRoomScene | null
}
/** 独立设备放置弹窗事件。 */
interface Emits {
(event: 'update:visible', value: boolean): void
(event: 'success'): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
const dialogVisible = computed({
get: () => props.visible,
set: (value) => emit('update:visible', value),
})
const formRef = ref()
const assetsLoading = ref(false)
const assetList = ref<UnplacedAsset[]>([])
let assetSearchTimer: number | undefined
const formData = reactive({
assetId: undefined as number | undefined,
positionX: 0,
positionY: 0,
positionZ: 0,
rotationX: 0,
rotationY: 0,
rotationZ: 0,
sceneWidth: 1,
sceneHeight: 1,
sceneDepth: 1,
})
const rules = { assetId: [{ required: true, message: '请选择设备' }] }
/** 获取未放置资产列表。 */
async function loadAssetList(keyword?: string): Promise<void> {
assetsLoading.value = true
try {
const response: any = await fetchAssetAll({ keyword: keyword || undefined })
if (response.code !== 0) throw new Error(response.message || '设备列表获取失败')
assetList.value = (Array.isArray(response.details) ? response.details : [])
.filter((item: any) => item.placement_type === 'unplaced')
.map((item: any) => ({
id: item.id,
name: item.asset_name || item.name || `设备 ${item.id}`,
code: item.asset_code || String(item.id),
}))
} catch (error) {
assetList.value = []
Message.error(error instanceof Error ? error.message : '设备列表获取失败')
} finally {
assetsLoading.value = false
}
}
/** 延迟搜索未放置设备。 */
function handleAssetSearch(keyword: string): void {
if (assetSearchTimer !== undefined) window.clearTimeout(assetSearchTimer)
assetSearchTimer = window.setTimeout(() => void loadAssetList(keyword.trim() || undefined), 300)
}
/** 使用机房尺寸重置放置参数。 */
function resetForm(): void {
formData.assetId = undefined
formData.positionX = (Number(props.scene?.room?.scene_length) || 0) / 2
formData.positionY = 0
formData.positionZ = (Number(props.scene?.room?.scene_width) || 0) / 2
formData.rotationX = 0
formData.rotationY = 0
formData.rotationZ = 0
formData.sceneWidth = 1
formData.sceneHeight = 1
formData.sceneDepth = 1
formRef.value?.resetFields()
}
/** 校验并保存独立设备位置。 */
async function handleBeforeOk(): Promise<boolean> {
try {
await formRef.value?.validate()
} catch {
return false
}
const layoutVersion = Number(props.scene?.room?.layout_version) || 0
if (!formData.assetId || layoutVersion <= 0) {
Message.error('设备或布局版本无效,请刷新场景后重试')
return false
}
if (formData.sceneWidth <= 0 || formData.sceneHeight <= 0 || formData.sceneDepth <= 0) {
Message.error('设备尺寸必须大于 0')
return false
}
try {
await saveThreeMachineRoomDevicePlacement(props.roomId, formData.assetId, {
expectedVersion: layoutVersion,
positionX: formData.positionX,
positionY: formData.positionY,
positionZ: formData.positionZ,
rotationX: formData.rotationX,
rotationY: formData.rotationY,
rotationZ: formData.rotationZ,
sceneWidth: formData.sceneWidth,
sceneHeight: formData.sceneHeight,
sceneDepth: formData.sceneDepth,
})
emit('success')
return true
} catch (error) {
Message.error(error instanceof Error ? error.message : '独立设备放置失败')
return false
}
}
watch(
() => props.visible,
(visible) => {
if (!visible) return
resetForm()
void loadAssetList()
}
)
onBeforeUnmount(() => {
if (assetSearchTimer !== undefined) window.clearTimeout(assetSearchTimer)
})
</script>
<style scoped lang="less">
.device-placement-form {
margin-top: 18px;
:deep(.arco-input-number) {
width: 100%;
}
}
.section-title {
display: block;
margin: 4px 0 10px;
}
</style>

View File

@@ -0,0 +1,217 @@
<template>
<a-modal
v-model:visible="dialogVisible"
title="3D 场景配置"
:width="960"
:mask-closable="false"
:on-before-ok="handleBeforeOk"
@cancel="resetForm"
>
<a-alert type="info" show-icon>首次配置会提交当前机房的全部机柜位置后端将统一校验边界和空间重叠</a-alert>
<a-form :model="formData" layout="vertical" class="scene-config-form">
<a-grid :cols="3" :col-gap="16">
<a-grid-item>
<a-form-item label="机房长度(米)" required>
<a-input-number v-model="formData.sceneLength" :min="0.01" :precision="2" style="width: 100%" />
</a-form-item>
</a-grid-item>
<a-grid-item>
<a-form-item label="机房宽度(米)" required>
<a-input-number v-model="formData.sceneWidth" :min="0.01" :precision="2" style="width: 100%" />
</a-form-item>
</a-grid-item>
<a-grid-item>
<a-form-item label="机房高度(米)" required>
<a-input-number v-model="formData.sceneHeight" :min="0.01" :precision="2" style="width: 100%" />
</a-form-item>
</a-grid-item>
</a-grid>
</a-form>
<div class="rack-layout-header">
<strong>机柜位置{{ rackLayouts.length }}</strong>
<span>坐标为机柜底面中心机柜仅支持绕 Y 轴旋转</span>
</div>
<div class="rack-layout-list">
<div v-for="rack in rackLayouts" :key="rack.rack_id" class="rack-layout-row">
<div class="rack-name">
<strong>{{ rack.name }}</strong>
<span>{{ rack.code }}</span>
</div>
<label>
<a-input-number v-model="rack.row" :min="0" :precision="0" />
</label>
<label>
<a-input-number v-model="rack.column" :min="0" :precision="0" />
</label>
<label>
X
<a-input-number v-model="rack.position_x" :min="0" :precision="2" />
</label>
<label>
Y
<a-input-number v-model="rack.position_y" :min="0" :precision="2" />
</label>
<label>
Z
<a-input-number v-model="rack.position_z" :min="0" :precision="2" />
</label>
<label>
旋转
<a-input-number v-model="rack.rotation_y" :min="0" :max="359.99" :precision="2" />
</label>
</div>
</div>
</a-modal>
</template>
<script lang="ts" setup>
import { computed, reactive, ref, watch } from 'vue'
import { Message } from '@arco-design/web-vue'
import { saveThreeMachineRoomConfig, type ThreeMachineRoomRackLayout, type ThreeMachineRoomScene } from '@/api/ops/three-machine-room'
/** 场景配置弹窗属性。 */
interface Props {
visible: boolean
roomId: number
scene: ThreeMachineRoomScene | null
}
/** 场景配置弹窗事件。 */
interface Emits {
(event: 'update:visible', value: boolean): void
(event: 'success'): void
}
/** 带展示字段的机柜布局项。 */
interface EditableRackLayout extends ThreeMachineRoomRackLayout {
name: string
code: string
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
const dialogVisible = computed({
get: () => props.visible,
set: (value) => emit('update:visible', value),
})
const formData = reactive({
sceneLength: 0,
sceneWidth: 0,
sceneHeight: 0,
})
const rackLayouts = ref<EditableRackLayout[]>([])
/** 使用当前场景重置配置表单。 */
function resetForm(): void {
const room = props.scene?.room
formData.sceneLength = Number(room?.scene_length) || 0
formData.sceneWidth = Number(room?.scene_width) || 0
formData.sceneHeight = Number(room?.scene_height) || 0
rackLayouts.value = (props.scene?.racks || []).map((rack) => ({
rack_id: rack.id,
name: rack.name || `机柜 ${rack.id}`,
code: rack.code || '',
row: Number(rack.row) || 0,
column: Number(rack.column) || 0,
position_x: Number(rack.transform?.position_x) || 0,
position_y: Number(rack.transform?.position_y) || 0,
position_z: Number(rack.transform?.position_z) || 0,
rotation_y: Number(rack.transform?.rotation_y) || 0,
}))
}
/** 校验并保存完整场景配置。 */
async function handleBeforeOk(): Promise<boolean> {
const layoutVersion = Number(props.scene?.room?.layout_version) || 0
if (layoutVersion <= 0) {
Message.error('机房布局版本无效,请刷新场景后重试')
return false
}
if (formData.sceneLength <= 0 || formData.sceneWidth <= 0 || formData.sceneHeight <= 0) {
Message.error('机房长、宽、高必须大于 0')
return false
}
try {
await saveThreeMachineRoomConfig(props.roomId, {
expectedVersion: layoutVersion,
sceneLength: formData.sceneLength,
sceneWidth: formData.sceneWidth,
sceneHeight: formData.sceneHeight,
racks: rackLayouts.value.map(({ name, code, ...rack }) => rack),
})
emit('success')
return true
} catch (error) {
Message.error(error instanceof Error ? error.message : '场景配置保存失败')
return false
}
}
watch(
() => props.visible,
(visible) => {
if (visible) resetForm()
}
)
</script>
<style scoped lang="less">
.scene-config-form {
margin-top: 18px;
}
.rack-layout-header {
display: flex;
align-items: center;
justify-content: space-between;
margin: 4px 0 12px;
span {
color: var(--color-text-3);
font-size: 12px;
}
}
.rack-layout-list {
max-height: 420px;
overflow-y: auto;
border: 1px solid var(--color-border-2);
border-radius: 4px;
}
.rack-layout-row {
display: grid;
grid-template-columns: minmax(120px, 1.5fr) repeat(6, minmax(82px, 1fr));
align-items: end;
gap: 10px;
padding: 10px 12px;
border-bottom: 1px solid var(--color-border-1);
&:last-child {
border-bottom: 0;
}
label,
.rack-name {
display: flex;
flex-direction: column;
gap: 4px;
color: var(--color-text-3);
font-size: 12px;
}
.rack-name strong {
overflow: hidden;
color: var(--color-text-1);
text-overflow: ellipsis;
white-space: nowrap;
}
}
</style>

View File

@@ -0,0 +1,761 @@
<template>
<div
id="three-machine-room-canvas"
class="three-machine-room-page"
:style="sceneStyle"
:data-room-id="roomId"
:data-mapped-rack-count="sceneSummary.mappedRackCount"
:data-rack-device-count="sceneSummary.equipmentCount"
:data-active-alert-count="signalSummary.activeAlertCount"
>
<div class="scene-toolbar">
<a-space wrap :size="8">
<a-button size="small" @click="goBack">返回机房列表</a-button>
<a-button size="small" :loading="sceneLoading" @click="refreshScene">刷新数据</a-button>
<a-button size="small" :disabled="usingMockData || !roomScene" @click="sceneConfigVisible = true">场景配置</a-button>
<a-button size="small" :disabled="usingMockData || !roomScene" @click="roomDevicePlacementVisible = true">放置独立设备</a-button>
<a-button size="small" :loading="exportLoading" @click="exportScenes">导出场景</a-button>
<a-button size="small" :disabled="!modelReady" @click="toggleWalls">
{{ wallTransparent ? '恢复实体外墙' : '外墙透明' }}
</a-button>
<a-button size="small" :disabled="!modelReady" @click="toggleExteriorDoors">
{{ exteriorDoorsOpen ? '关闭外墙门' : '打开外墙门' }}
</a-button>
<a-button size="small" :disabled="!modelReady" @click="toggleCabinetDoors">
{{ cabinetDoorsOpen ? '关闭全部机柜门' : '打开全部机柜门' }}
</a-button>
<a-button size="small" :disabled="!modelReady" @click="toggleServers">
{{ serversVisible ? '隐藏服务器设备' : '显示服务器设备' }}
</a-button>
<a-button size="small" :disabled="!modelReady" @click="toggleAisleView">
{{ insideAisle ? '返回总览' : '进入右侧过道' }}
</a-button>
</a-space>
<div class="scene-toolbar__meta">
<span class="room-name">{{ roomName }}</span>
<span
class="signal-summary"
:class="{
'has-warning': signalSummary.warningCount > 0,
'has-abnormal': signalSummary.abnormalCount > 0,
}"
>
<i class="signal-summary__dot"></i>
活动告警 {{ signalSummary.activeAlertCount }}
</span>
<span class="operation-tip">
{{ insideAisle ? '滚轮前后移动,拖拽转向;点击设备查看实时信息' : '点击机柜上架设备;双击门板开关' }}
</span>
</div>
</div>
<div v-if="usingMockData" class="mock-data-notice">接口数据暂不可用当前展示演示机房数据</div>
<div v-if="sceneLoading || modelLoading" class="scene-loading">
<a-spin :size="36" />
<strong>{{ loadingText }}</strong>
<a-progress v-if="modelLoading && modelProgress > 0" class="scene-loading__progress" :percent="modelProgress / 100" />
</div>
<rack-placement-dialog
v-model:visible="rackPlacementVisible"
:room-id="roomId"
:rack="selectedRack"
:layout-version="layoutVersion"
@success="handleRackPlacementSuccess"
@edit-layout="handleOpenRackLayout"
/>
<rack-layout-dialog
v-model:visible="rackLayoutVisible"
:room-id="roomId"
:rack="selectedRack"
:layout-version="layoutVersion"
@success="handleRackLayoutSuccess"
/>
<scene-config-dialog v-model:visible="sceneConfigVisible" :room-id="roomId" :scene="roomScene" @success="handleSceneConfigSuccess" />
<room-device-placement-dialog
v-model:visible="roomDevicePlacementVisible"
:room-id="roomId"
:scene="roomScene"
@success="handleRoomDevicePlacementSuccess"
/>
<camera-preview-dialog v-model:visible="cameraPreviewVisible" :device="selectedDevice" @manage="handleOpenDeviceManagement" />
<device-management-dialog
v-model:visible="deviceManagementVisible"
:room-id="roomId"
:device="selectedDevice"
:layout-version="layoutVersion"
@observability-loaded="handleObservabilityLoaded"
@placement-removed="handlePlacementRemoved"
/>
</div>
</template>
<script lang="ts" setup>
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref } from 'vue'
import { Message } from '@arco-design/web-vue'
import { useRoute, useRouter } from 'vue-router'
import CameraPreviewDialog from './components/CameraPreviewDialog.vue'
import DeviceManagementDialog from './components/DeviceManagementDialog.vue'
import RackLayoutDialog from './components/RackLayoutDialog.vue'
import RackPlacementDialog from './components/RackPlacementDialog.vue'
import RoomDevicePlacementDialog from './components/RoomDevicePlacementDialog.vue'
import SceneConfigDialog from './components/SceneConfigDialog.vue'
import {
exportThreeMachineRoomScenes,
fetchThreeMachineRoomDeviceObservability,
fetchThreeMachineRoomScene,
fetchThreeMachineRoomSignals,
type ThreeMachineRoomDevice,
type ThreeMachineRoomObservability,
type ThreeMachineRoomRack,
type ThreeMachineRoomScene,
} from '@/api/ops/three-machine-room'
import { getMockDeviceObservability, MOCK_ROOM_SCENE, MOCK_ROOM_SIGNALS } from './scene/MockThreeMachineRoom'
import ThreeMap from './scene/ThreeMap'
import { ThreeData } from './scene/ThreeData'
/** 场景渲染摘要。 */
interface SceneSummary {
rackCount: number
mappedRackCount: number
equipmentCount: number
}
/** 设备信号摘要。 */
interface SignalSummary {
signalCount: number
activeAlertCount: number
warningCount: number
abnormalCount: number
}
const route = useRoute()
const router = useRouter()
const assetBasePath = `${import.meta.env.BASE_URL}three-machine-room/`
let machineRoomMap: any = null
let signalTimer: number | undefined
const sceneLoading = ref(false)
const modelLoading = ref(true)
const modelReady = ref(false)
const modelProgress = ref(0)
const signalsLoading = ref(false)
const usingMockData = ref(false)
const wallTransparent = ref(true)
const exteriorDoorsOpen = ref(false)
const cabinetDoorsOpen = ref(false)
const serversVisible = ref(true)
const insideAisle = ref(false)
const roomScene = ref<ThreeMachineRoomScene | null>(null)
const selectedRack = ref<ThreeMachineRoomRack | null>(null)
const selectedDevice = ref<ThreeMachineRoomDevice | null>(null)
const rackPlacementVisible = ref(false)
const rackLayoutVisible = ref(false)
const sceneConfigVisible = ref(false)
const roomDevicePlacementVisible = ref(false)
const cameraPreviewVisible = ref(true)
const deviceManagementVisible = ref(false)
const exportLoading = ref(false)
const sceneSummary = reactive<SceneSummary>({
rackCount: 0,
mappedRackCount: 0,
equipmentCount: 0,
})
const signalSummary = reactive<SignalSummary>({
signalCount: 0,
activeAlertCount: 0,
warningCount: 0,
abnormalCount: 0,
})
/** 将路由参数转换为合法机房 ID。 */
function resolveRoomId(): number {
const routeValue = route.params.room_id || route.query.room_id || route.query.roomId || 1
const value = Array.isArray(routeValue) ? routeValue[0] : routeValue
const parsedId = Number(value)
return Number.isInteger(parsedId) && parsedId > 0 ? parsedId : 1
}
const roomId = resolveRoomId()
const roomName = computed(() => roomScene.value?.room?.name || `机房 ${roomId}`)
const layoutVersion = computed(() => Number(roomScene.value?.room?.layout_version) || 0)
const loadingText = computed(() => {
if (modelLoading.value) {
return modelProgress.value > 0 ? `3D 模型加载中 ${modelProgress.value}%` : '3D 模型加载中'
}
return '机房数据加载中'
})
const sceneStyle = computed(() => ({
backgroundImage: `radial-gradient(circle at center, rgba(0, 63, 112, 0.35), rgba(0, 12, 63, 0.92)), url("${assetBasePath}rack/homebg.png")`,
}))
/** 从未知异常中提取可展示文案。 */
function getErrorMessage(error: unknown, fallback: string): string {
return error instanceof Error && error.message ? error.message : fallback
}
/** 停止设备状态轮询。 */
function stopSignalPolling(): void {
if (signalTimer !== undefined) {
window.clearInterval(signalTimer)
signalTimer = undefined
}
}
/** 启动设备状态轮询。 */
function startSignalPolling(): void {
stopSignalPolling()
signalTimer = window.setInterval(() => {
void loadRoomSignals(true)
}, 30000)
}
/** 拉取并应用机房设备状态。 */
async function loadRoomSignals(silent = false): Promise<void> {
if (!machineRoomMap || signalsLoading.value) return
if (usingMockData.value) {
Object.assign(signalSummary, machineRoomMap.setRoomSignals(MOCK_ROOM_SIGNALS))
return
}
signalsLoading.value = true
try {
const details = await fetchThreeMachineRoomSignals(roomId)
Object.assign(signalSummary, machineRoomMap.setRoomSignals(details))
} catch (error) {
if (!silent) {
Message.warning(getErrorMessage(error, '设备状态及告警获取失败'))
} else {
console.warn('设备状态及告警获取失败:', error)
}
} finally {
signalsLoading.value = false
}
}
/** 拉取场景数据,失败时回退到源项目的演示数据。 */
async function loadRoomScene(): Promise<boolean> {
sceneLoading.value = true
try {
const scene = await fetchThreeMachineRoomScene(roomId)
if (!scene?.room) {
throw new Error('机房详情缺少 room 数据')
}
usingMockData.value = false
roomScene.value = scene
machineRoomMap?.setRoomScene(scene)
const roomConfigured = [scene.room.scene_length, scene.room.scene_width, scene.room.scene_height].every((value) => Number(value) > 0)
if (!roomConfigured) {
sceneConfigVisible.value = true
Message.info('该机房尚未完成 3D 初始化,请先配置机房尺寸和机柜位置')
}
await loadRoomSignals(true)
return true
} catch (error) {
usingMockData.value = true
roomScene.value = MOCK_ROOM_SCENE as ThreeMachineRoomScene
machineRoomMap?.setRoomScene(MOCK_ROOM_SCENE)
if (machineRoomMap) {
Object.assign(signalSummary, machineRoomMap.setRoomSignals(MOCK_ROOM_SIGNALS))
}
Message.warning(getErrorMessage(error, '接口数据暂不可用,当前展示演示机房数据'))
return false
} finally {
sceneLoading.value = false
}
}
/** 加载所选设备的实时指标。 */
async function loadDeviceObservability(device: ThreeMachineRoomDevice): Promise<void> {
const assetId = Number(device?.asset_id)
if (!Number.isInteger(assetId) || assetId <= 0 || !machineRoomMap) return
const mockDetails = usingMockData.value ? getMockDeviceObservability(assetId) : null
if (mockDetails) {
machineRoomMap.setDeviceObservability(assetId, mockDetails)
return
}
machineRoomMap.setDeviceObservabilityLoading(assetId)
try {
const details = await fetchThreeMachineRoomDeviceObservability(assetId)
machineRoomMap?.setDeviceObservability(assetId, details)
} catch (error) {
machineRoomMap?.setDeviceObservabilityError(assetId, error)
Message.warning(getErrorMessage(error, '设备实时信息获取失败'))
}
}
/** 打开所选机柜的设备上架弹窗。 */
function handleRackSelected(rack: ThreeMachineRoomRack): void {
if (usingMockData.value) {
Message.warning('演示数据不支持设备上架')
return
}
selectedRack.value = rack
rackPlacementVisible.value = true
}
/** 保存上架结果后重新加载 3D 场景。 */
async function handleRackPlacementSuccess(): Promise<void> {
rackPlacementVisible.value = false
Message.success('设备上架成功,正在重新加载 3D 场景')
await refreshScene()
selectedRack.value = null
}
/** 从设备上架弹窗切换到机柜布局弹窗。 */
function handleOpenRackLayout(): void {
rackPlacementVisible.value = false
rackLayoutVisible.value = true
}
/** 保存机柜布局后重新加载 3D 场景。 */
async function handleRackLayoutSuccess(): Promise<void> {
rackLayoutVisible.value = false
Message.success('机柜布局保存成功,正在重新加载 3D 场景')
await refreshScene()
selectedRack.value = null
}
/** 保存场景配置后重新加载 3D 场景。 */
async function handleSceneConfigSuccess(): Promise<void> {
sceneConfigVisible.value = false
Message.success('场景配置保存成功,正在重新加载 3D 场景')
await refreshScene()
}
/** 保存独立设备位置后重新加载 3D 场景。 */
async function handleRoomDevicePlacementSuccess(): Promise<void> {
roomDevicePlacementVisible.value = false
Message.success('独立设备放置成功,正在重新加载 3D 场景')
await refreshScene()
}
/** 判断 3D 设备是否为摄像头。 */
function isCameraDevice(device: ThreeMachineRoomDevice): boolean {
const categoryCode = String(device.category_code || '').toLowerCase()
const searchableText = [categoryCode, device.category_name, device.asset_name, device.asset_code].filter(Boolean).join(' ').toLowerCase()
return ['camera', 'cctv', '摄像', '监控'].some((keyword) => searchableText.includes(keyword)) || categoryCode === 'ipc'
}
/** 点击摄像头时打开预览,其他设备进入详情管理。 */
function handleDeviceSelected(device: ThreeMachineRoomDevice): void {
selectedDevice.value = device
if (isCameraDevice(device)) {
cameraPreviewVisible.value = true
return
}
if (usingMockData.value) {
void loadDeviceObservability(device)
Message.warning('演示数据不支持资源绑定和位置管理')
return
}
deviceManagementVisible.value = true
}
/** 从摄像头预览切换到设备详情管理。 */
function handleOpenDeviceManagement(): void {
cameraPreviewVisible.value = false
deviceManagementVisible.value = true
}
/** 将弹窗加载的可观测数据同步到 3D 设备标牌。 */
function handleObservabilityLoaded(assetId: number, details: ThreeMachineRoomObservability): void {
machineRoomMap?.setDeviceObservability(assetId, details)
}
/** 解除设备位置后重新加载 3D 场景。 */
async function handlePlacementRemoved(): Promise<void> {
deviceManagementVisible.value = false
await refreshScene()
selectedDevice.value = null
}
/** 导出当前权限范围内的全部 3D 场景 JSON。 */
async function exportScenes(): Promise<void> {
exportLoading.value = true
try {
const scenes = await exportThreeMachineRoomScenes()
const blob = new Blob([JSON.stringify(scenes, null, 2)], { type: 'application/json;charset=utf-8' })
const url = URL.createObjectURL(blob)
const anchor = document.createElement('a')
anchor.href = url
anchor.download = `three-machine-room-scenes-${new Date().toISOString().slice(0, 10)}.json`
document.body.appendChild(anchor)
anchor.click()
anchor.remove()
URL.revokeObjectURL(url)
Message.success('3D 场景导出成功')
} catch (error) {
Message.error(getErrorMessage(error, '3D 场景导出失败'))
} finally {
exportLoading.value = false
}
}
/** 初始化 Three.js 场景。 */
async function initializeMachineRoom(): Promise<void> {
await nextTick()
machineRoomMap = new ThreeMap(
{
domID: 'three-machine-room-canvas',
assetBasePath,
onRoomSceneApplied: (summary: SceneSummary) => Object.assign(sceneSummary, summary),
onDeviceSelected: (device: ThreeMachineRoomDevice) => handleDeviceSelected(device),
onRackSelected: (rack: ThreeMachineRoomRack) => handleRackSelected(rack),
onModelLoading: (loading: boolean) => {
modelLoading.value = loading
},
onModelProgress: (progress: number) => {
modelProgress.value = progress
},
onModelReady: () => {
modelReady.value = true
modelLoading.value = false
},
onModelError: () => {
modelReady.value = false
Message.error('3D 机房模型加载失败')
},
},
ThreeData
)
machineRoomMap.init()
const connected = await loadRoomScene()
if (connected) startSignalPolling()
}
/** 刷新机房场景与状态数据。 */
async function refreshScene(): Promise<void> {
const connected = await loadRoomScene()
if (connected) startSignalPolling()
else stopSignalPolling()
}
/** 返回机房管理页面。 */
function goBack(): void {
void router.push('/datacenter/room')
}
/** 切换外墙透明状态。 */
function toggleWalls(): void {
if (machineRoomMap) wallTransparent.value = machineRoomMap.setWallsTransparent(!wallTransparent.value)
}
/** 切换全部外墙门。 */
function toggleExteriorDoors(): void {
if (machineRoomMap) exteriorDoorsOpen.value = machineRoomMap.toggleExteriorDoors()
}
/** 切换全部机柜门。 */
function toggleCabinetDoors(): void {
if (machineRoomMap) cabinetDoorsOpen.value = machineRoomMap.toggleCabinetDoors()
}
/** 切换服务器设备可见性。 */
function toggleServers(): void {
if (machineRoomMap) serversVisible.value = machineRoomMap.setServerEquipmentVisible(!serversVisible.value)
}
/** 切换总览与过道视角。 */
function toggleAisleView(): void {
if (!machineRoomMap) return
const changed = insideAisle.value ? machineRoomMap.setOverviewView() : machineRoomMap.setAisleView()
if (changed) {
insideAisle.value = !insideAisle.value
} else {
Message.warning('模型仍在加载,请稍后再试')
}
}
onMounted(() => {
void initializeMachineRoom()
})
onBeforeUnmount(() => {
stopSignalPolling()
machineRoomMap?.destroy()
machineRoomMap = null
})
</script>
<script lang="ts">
export default {
name: 'ThreeMachineRoom',
}
</script>
<style lang="less">
.three-machine-room-page {
position: relative;
width: 100%;
height: 100vh;
overflow: hidden;
background-color: #000c3f;
background-position: center;
background-repeat: no-repeat;
background-size: cover;
color: #d9f1ff;
> canvas {
display: block;
}
.scene-toolbar {
position: absolute;
z-index: 10;
top: 18px;
right: 18px;
left: 18px;
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 10px;
padding: 10px 12px;
border: 1px solid rgba(90, 216, 255, 0.28);
border-radius: 8px;
background: rgba(4, 28, 58, 0.82);
box-shadow: 0 8px 28px rgba(0, 0, 0, 0.28);
backdrop-filter: blur(8px);
}
.scene-toolbar__meta {
display: flex;
align-items: center;
gap: 10px;
}
.room-name {
color: #e9f9ff;
font-weight: 600;
}
.operation-tip {
color: #b7d8e8;
font-size: 12px;
white-space: nowrap;
}
.signal-summary {
display: inline-flex;
align-items: center;
height: 26px;
padding: 0 9px;
border: 1px solid rgba(74, 211, 255, 0.35);
border-radius: 14px;
color: #a9d9ee;
background: rgba(2, 35, 65, 0.78);
font-size: 12px;
white-space: nowrap;
&__dot {
width: 7px;
height: 7px;
margin-right: 6px;
border-radius: 50%;
background: #20e6b2;
box-shadow: 0 0 8px #20e6b2;
}
&.has-warning {
border-color: rgba(255, 180, 41, 0.55);
color: #ffd27a;
.signal-summary__dot {
background: #ffb429;
box-shadow: 0 0 9px #ffb429;
}
}
&.has-abnormal {
border-color: rgba(255, 77, 103, 0.62);
color: #ff9aa9;
.signal-summary__dot {
background: #ff4d67;
box-shadow: 0 0 10px #ff4d67;
}
}
}
.mock-data-notice {
position: absolute;
z-index: 9;
right: 20px;
bottom: 20px;
padding: 8px 12px;
border: 1px solid rgba(255, 180, 41, 0.5);
border-radius: 4px;
background: rgba(92, 54, 2, 0.82);
color: #ffd27a;
font-size: 12px;
}
.scene-loading {
position: absolute;
z-index: 20;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 14px;
background: rgba(0, 10, 38, 0.72);
&__progress {
width: 280px;
}
}
}
.rack-device-callout-layer {
position: absolute;
z-index: 6;
inset: 0;
overflow: hidden;
pointer-events: none;
}
.rack-device-callout {
--device-status-color: #62a7c8;
position: absolute;
top: 0;
left: 0;
width: 0;
height: 0;
color: #d9f5ff;
pointer-events: none;
&__card {
position: absolute;
bottom: 64px;
left: 0;
width: 270px;
transform: translateX(-50%);
overflow: hidden;
border: 1px solid color-mix(in srgb, var(--device-status-color) 62%, #1bd7ff);
border-radius: 3px;
background: linear-gradient(100deg, rgba(2, 73, 102, 0.96), rgba(4, 22, 49, 0.96) 58%, rgba(11, 30, 62, 0.96));
box-shadow:
0 8px 26px rgba(0, 7, 24, 0.55),
0 0 18px color-mix(in srgb, var(--device-status-color) 22%, transparent);
cursor: pointer;
pointer-events: auto;
}
&__header {
display: flex;
align-items: center;
justify-content: space-between;
min-height: 42px;
padding: 0 13px;
border-bottom: 1px solid rgba(62, 210, 255, 0.16);
background: linear-gradient(90deg, rgba(2, 104, 137, 0.68), rgba(14, 45, 79, 0.3));
}
&__title {
max-width: 155px;
overflow: hidden;
color: #def8ff;
font-size: 15px;
font-weight: 600;
text-overflow: ellipsis;
white-space: nowrap;
}
&__status {
font-size: 11px;
font-weight: 600;
white-space: nowrap;
}
&__body {
padding: 8px 13px 9px;
}
&__row {
display: grid;
grid-template-columns: 8px minmax(76px, 1fr) auto;
align-items: center;
min-height: 25px;
column-gap: 7px;
font-size: 12px;
}
&__dot {
width: 5px;
height: 5px;
border-radius: 50%;
background: #18dcff;
box-shadow: 0 0 7px #18dcff;
}
&__label {
overflow: hidden;
color: #a9c8d8;
text-overflow: ellipsis;
white-space: nowrap;
}
&__value {
max-width: 105px;
overflow: hidden;
color: #eefaff;
font-weight: 600;
text-align: right;
text-overflow: ellipsis;
white-space: nowrap;
&.is-warning {
color: var(--device-status-color);
}
}
&__leader {
position: absolute;
bottom: 0;
left: -1px;
width: 2px;
height: 64px;
background: linear-gradient(to bottom, rgba(31, 221, 255, 0.6), var(--device-status-color));
box-shadow: 0 0 7px var(--device-status-color);
}
&__marker {
position: absolute;
top: -5px;
left: -5px;
width: 10px;
height: 10px;
border: 2px solid var(--device-status-color);
border-radius: 50%;
background: rgba(5, 25, 51, 0.92);
box-shadow: 0 0 10px var(--device-status-color);
}
&.is-selected &__card {
border-color: var(--device-status-color);
box-shadow:
0 10px 30px rgba(0, 7, 24, 0.62),
0 0 24px color-mix(in srgb, var(--device-status-color) 35%, transparent);
}
&.is-below &__card {
top: 64px;
bottom: auto;
}
&.is-below &__leader {
top: 0;
bottom: auto;
}
}
</style>

View File

@@ -0,0 +1,168 @@
const CRITICAL_SEVERITY = {
id: 1,
name: '严重',
color: '#FF4D67',
}
const WARNING_SEVERITY = {
id: 2,
name: '预警',
color: '#FFB429',
}
function createRack(index) {
const numberLabel = index < 10 ? '0' + index : String(index)
return {
id: 9000 + index,
code: 'R' + numberLabel,
name: '演示机柜-' + numberLabel,
row: Math.floor((index - 1) / 6) + 1,
column: ((index - 1) % 6) + 1,
height: 42,
devices: [],
}
}
const racks = Array.from({ length: 12 }, (item, index) => createRack(index + 1))
racks[9].devices.push({
asset_id: 990001,
asset_code: 'SRV-DEMO-01',
asset_name: '应用服务器-01',
category_code: 'server',
unit_start: 12,
unit_end: 13,
occupied_units: 2,
power_consumption: 680,
})
racks[11].devices.push({
asset_id: 990002,
asset_code: 'UPS-DEMO-01',
asset_name: 'UPS 电源-01',
category_code: 'ups',
unit_start: 20,
unit_end: 23,
occupied_units: 4,
power_consumption: 1250,
})
racks[0].devices.push({
asset_id: 990003,
asset_code: 'NET-DEMO-02',
asset_name: '核心交换机-02',
category_code: 'network_switch',
unit_start: 30,
unit_end: 31,
occupied_units: 2,
power_consumption: 420,
callout_position: 'below',
})
export const MOCK_ROOM_SCENE = {
room: {
id: 1,
name: '三维机房演示数据',
layout_version: 1,
},
racks: racks,
}
export const MOCK_ROOM_SIGNALS = {
signals: [
{
asset_id: 990001,
status: 'abnormal',
active_alert_count: 1,
highest_alert_severity: CRITICAL_SEVERITY,
alerts: [
{
alerts: [
{
alert_name: '机柜温度过高',
status: 'firing',
severity: CRITICAL_SEVERITY,
},
],
},
],
},
{
asset_id: 990002,
status: 'warning',
active_alert_count: 1,
highest_alert_severity: WARNING_SEVERITY,
alerts: [
{
alerts: [
{
alert_name: 'UPS 输入电压异常',
status: 'firing',
severity: WARNING_SEVERITY,
},
],
},
],
},
{
asset_id: 990003,
status: 'warning',
active_alert_count: 1,
highest_alert_severity: WARNING_SEVERITY,
alerts: [
{
alerts: [
{
alert_name: '端口流量超过阈值',
status: 'firing',
severity: WARNING_SEVERITY,
},
],
},
],
},
],
}
const MOCK_DEVICE_OBSERVABILITY = {
990001: {
resources: [
{
metrics: [
{ name: 'temperature', value: 38.6, unit: '°C' },
{ name: 'cpu_usage', value: 86.2, unit: '%' },
{ name: 'memory_usage', value: 78.4, unit: '%' },
],
},
],
alerts: MOCK_ROOM_SIGNALS.signals[0].alerts,
},
990002: {
resources: [
{
metrics: [
{ name: 'input_voltage', value: 185, unit: 'V' },
{ name: 'output_voltage', value: 220, unit: 'V' },
{ name: 'power', value: 1250, unit: 'W' },
],
},
],
alerts: MOCK_ROOM_SIGNALS.signals[1].alerts,
},
990003: {
resources: [
{
metrics: [
{ name: 'temperature', value: 31.8, unit: '°C' },
{ name: 'power', value: 420, unit: 'W' },
{ name: 'voltage', value: 12.1, unit: 'V' },
],
},
],
alerts: MOCK_ROOM_SIGNALS.signals[2].alerts,
},
}
export function getMockDeviceObservability(assetId) {
return MOCK_DEVICE_OBSERVABILITY[String(assetId)] || null
}

View File

@@ -0,0 +1,33 @@
/** 3D 机房模型场景配置。 */
export const ThreeData = {
objects: [
{
uuid: '',
name: 'importedModel',
objType: 'objModel',
filePath: 'qd/quding.obj',
mtlPath: 'qd/quding.mtl',
textEncoding: 'gb18030',
autoScale: true,
targetSize: 1800,
centerModel: true,
alignToFloor: true,
fitCamera: true,
cameraDirection: [1.1, 1.8, 2],
cameraPadding: 1.05,
aisleView: {
xRatio: 0.25,
startZRatio: 0.68,
targetZRatio: 0.28,
eyeHeightRatio: 0.46,
fov: 58,
},
isolateScene: true,
x: 0,
y: 45,
z: 0,
},
],
events: {},
btns: [],
}

File diff suppressed because it is too large Load Diff

View File

@@ -16,5 +16,6 @@
},
"lib": ["es2020", "dom"]
},
"include": ["src/**/*", "src/**/*.vue", "src/types/vue-i18n.d.ts"]
"include": ["src/**/*", "src/**/*.vue", "src/types/vue-i18n.d.ts"],
"exclude": ["src/WebSDK_noPlugin_V3.4.0_251202_20251204103656"]
}