diff --git a/proxy.conf.json b/proxy.conf.json index bc25667f..51b028c6 100644 --- a/proxy.conf.json +++ b/proxy.conf.json @@ -63,12 +63,18 @@ "ws": true, "changeOrigin": true }, - "/kael": { + "/facelive": { "target": "http://localhost:5173", "secure": false, "ws": true, "changeOrigin": true }, + "/kael": { + "target": "http://localhost:5172", + "secure": false, + "ws": true, + "changeOrigin": true + }, "/ui/": { "target": "http://localhost:9528", "secure": false, diff --git a/src/app/elements/chat/chat.component.ts b/src/app/elements/chat/chat.component.ts index 0b523062..eba97899 100644 --- a/src/app/elements/chat/chat.component.ts +++ b/src/app/elements/chat/chat.component.ts @@ -139,6 +139,7 @@ export class ElementChatComponent implements OnInit, OnDestroy { elements.forEach(element => { element.addEventListener('mousedown', (event: MouseEvent) => { + if (event.button !== 0) return; event.preventDefault(); startDrag(event); }, false); diff --git a/src/app/elements/connect/connect-dialog/advanced-option/advanced-option.component.ts b/src/app/elements/connect/connect-dialog/advanced-option/advanced-option.component.ts index a80910f5..72ef70c4 100644 --- a/src/app/elements/connect/connect-dialog/advanced-option/advanced-option.component.ts +++ b/src/app/elements/connect/connect-dialog/advanced-option/advanced-option.component.ts @@ -120,6 +120,20 @@ export class ElementAdvancedOptionComponent implements OnChanges, OnInit { } return true; } + }, + { + type: 'select', + field: 'rdp_connection_speed', + label: 'RDP connection speed', + hidden: () => { + return this.connectMethod && this.connectMethod.component !== 'razor'; + }, + value: 'auto', + options: [ + {label: 'Auto', value: 'auto'}, + {label: 'Low Speed Broadband (256 Kbps - 2 Mbps)', value: 'low_speed_broadband'}, + {label: 'High-speed broadband (2 Mbps – 10 Mbps )', value: 'high_speed_broadband'}, + ] } ]; } diff --git a/src/app/elements/connect/connect-dialog/connect-dialog.component.html b/src/app/elements/connect/connect-dialog/connect-dialog.component.html index 426e7d91..534140ad 100644 --- a/src/app/elements/connect/connect-dialog/connect-dialog.component.html +++ b/src/app/elements/connect/connect-dialog/connect-dialog.component.html @@ -73,7 +73,7 @@

{{ "Connect" | translate }} - {{ asset.name | truncatechars:30 }}

type="submit" > {{ "CONNECT"| translate }} - + ({{ 'Current online' | translate }}: {{ onlineNum === null ? '- ' : onlineNum }}) diff --git a/src/app/elements/connect/connect-dialog/connect-dialog.component.ts b/src/app/elements/connect/connect-dialog/connect-dialog.component.ts index e519b286..0e7d0542 100644 --- a/src/app/elements/connect/connect-dialog/connect-dialog.component.ts +++ b/src/app/elements/connect/connect-dialog/connect-dialog.component.ts @@ -25,6 +25,7 @@ export class ElementConnectDialogComponent implements OnInit { public accountSelected: Account = null; public connectOption: Object; public outputData: ConnectData = new ConnectData(); + public viewAssetOnlineSessionInfo: boolean = true; public manualAuthInfo: AuthInfo = new AuthInfo(); public connectMethod: ConnectMethod = new ConnectMethod('Null', '', 'null', 'null'); public preConnectData: ConnectData = new ConnectData(); @@ -92,6 +93,7 @@ export class ElementConnectDialogComponent implements OnInit { this.connectMethod = connectMethods[0]; } } + this.viewAssetOnlineSessionInfo = this._settingSvc.globalSetting.VIEW_ASSET_ONLINE_SESSION_INFO } onProtocolChange(protocol) { @@ -100,6 +102,9 @@ export class ElementConnectDialogComponent implements OnInit { } getOnlineNum() { + if (!this.viewAssetOnlineSessionInfo) { + return; + } if (this.protocol.name !== 'rdp') { return; } diff --git a/src/app/elements/content/send-command-with-variable-dialog/send-command-with-variable-dialog.component.ts b/src/app/elements/content/send-command-with-variable-dialog/send-command-with-variable-dialog.component.ts index 91754a55..bc55f8da 100644 --- a/src/app/elements/content/send-command-with-variable-dialog/send-command-with-variable-dialog.component.ts +++ b/src/app/elements/content/send-command-with-variable-dialog/send-command-with-variable-dialog.component.ts @@ -19,7 +19,7 @@ export class ElementSendCommandWithVariableDialogComponent implements OnInit { } async getVariableFormMeta() { const adhoc = this.data.command.id - const url=`/api/v1/ops/variable/form_data/?t=${new Date().getTime()}&adhoc=${adhoc}` + const url=`/api/v1/ops/variables/form-data/?t=${new Date().getTime()}&adhoc=${adhoc}` const res: any = await this._http.options(url).toPromise(); this.formConfig = res.actions.GET; this.command = this.data.command; diff --git a/src/app/elements/elements.component.ts b/src/app/elements/elements.component.ts index 7a16e911..e44e0469 100644 --- a/src/app/elements/elements.component.ts +++ b/src/app/elements/elements.component.ts @@ -31,11 +31,14 @@ import {ElementConnectorGuideComponent} from '@app/elements/content/content-wind import {ElementCommandDialogComponent} from '@app/elements/content/command-dialog/command-dialog.component'; import {ElementSendCommandDialogComponent} from '@app/elements/content/send-command-dialog/send-command-dialog.component'; // tslint:disable-next-line:max-line-length -import {ElementSendCommandWithVariableDialogComponent} from '@app/elements/content/send-command-with-variable-dialog/send-command-with-variable-dialog.component'; +import { + ElementSendCommandWithVariableDialogComponent +} from '@app/elements/content/send-command-with-variable-dialog/send-command-with-variable-dialog.component'; import {DynamicFormComponent} from '@app/elements/content/variable-dynamic-form/variable-dynamic-form.component'; import {ElementChatComponent} from '@app/elements/chat/chat.component'; import {ElementsPartsComponent} from './replay/parts/parts.component'; import {ElementConnectorNecComponent} from '@app/elements/content/content-window/nec/nec.component'; +import {ElementFaceMonitorComponent} from '@app/elements/face-monitor/face-monitor.component'; export const ElementComponents = [ @@ -51,6 +54,7 @@ export const ElementComponents = [ ElementNavComponent, ElementChatComponent, ElementIframeComponent, + ElementFaceMonitorComponent, ElementDialogAlertComponent, ElementAssetTreeComponent, ElementConnectorKokoComponent, diff --git a/src/app/elements/face-monitor/face-monitor.component.html b/src/app/elements/face-monitor/face-monitor.component.html new file mode 100644 index 00000000..a299ff7b --- /dev/null +++ b/src/app/elements/face-monitor/face-monitor.component.html @@ -0,0 +1,22 @@ +
+
+ ● [{{ monitoringTabCount }}]个会话正在被监控中 +
+ + +
+
+
+ +
+
diff --git a/src/app/elements/face-monitor/face-monitor.component.scss b/src/app/elements/face-monitor/face-monitor.component.scss new file mode 100644 index 00000000..d2bc3eb9 --- /dev/null +++ b/src/app/elements/face-monitor/face-monitor.component.scss @@ -0,0 +1,50 @@ +.box { + position: absolute; + width: 400px; + height: 460px; + bottom: 0; + right: 0; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); + display: flex; + flex-direction: column; + overflow: hidden; + z-index: 99999; +} + +.box.minimized { + height: 40px; +} + +.box-header { + height: 40px; + background-color: #3a3433; + color: white; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 10px; +} + +.box-title { + font-weight: bold; +} + +.box-controls button { + background: none; + border: none; + color: white; + font-size: 14px; + margin-left: 5px; + cursor: pointer; +} + +.box-controls button:hover { + text-decoration: underline; +} + +.box-content { + flex: 1; + padding: 8px; + background-color: #2f2a2a; + overflow: auto; +} diff --git a/src/app/elements/face-monitor/face-monitor.component.ts b/src/app/elements/face-monitor/face-monitor.component.ts new file mode 100644 index 00000000..a8626cc6 --- /dev/null +++ b/src/app/elements/face-monitor/face-monitor.component.ts @@ -0,0 +1,47 @@ +import {Component, OnInit} from '@angular/core'; +import {FaceService} from '@app/services/face'; +import {DomSanitizer, SafeResourceUrl} from '@angular/platform-browser'; + +@Component({ + selector: 'app-face-monitor', + templateUrl: './face-monitor.component.html', + styleUrls: ['./face-monitor.component.scss'] +}) +export class ElementFaceMonitorComponent implements OnInit { + + constructor( + private faceMonitorService: FaceService, + private sanitizer: DomSanitizer + ) { + this.faceMonitorService.isVisible$.subscribe(status => { + this.isVisible = status; + }); + this.faceMonitorService.monitoringTabCount$.subscribe(count => { + if (count > 0) { + this.ready = true; + } + this.monitoringTabCount = count; + if (this.ready && this.monitoringTabCount === 0) { + this.isVisible = false; + } + }); + } + + public faceMonitorUrl: SafeResourceUrl; + public isMinimized = false; + public isVisible = true; + public monitoringTabCount = 0; + public ready = false; + + ngOnInit() { + this.faceMonitorUrl = this.sanitizer.bypassSecurityTrustResourceUrl('/facelive/monitor?token=' + this.faceMonitorService.getToken()); + } + + minimizeBox() { + this.isMinimized = !this.isMinimized; + } + + closeBox() { + this.isVisible = false; + } +} diff --git a/src/app/elements/iframe/iframe.component.ts b/src/app/elements/iframe/iframe.component.ts index c87af9df..64bcc0a2 100644 --- a/src/app/elements/iframe/iframe.component.ts +++ b/src/app/elements/iframe/iframe.component.ts @@ -5,6 +5,7 @@ import {MatDialog} from '@angular/material'; import {Subject} from 'rxjs'; import {debounceTime} from 'rxjs/operators'; import {environment} from '@src/environments/environment'; +import {FaceService} from '@app/services/face'; @Component({ selector: 'elements-iframe', @@ -32,6 +33,7 @@ export class ElementIframeComponent implements OnInit, AfterViewInit, OnDestroy private _http: HttpService, private _dialog: MatDialog, public viewSrv: ViewService, + private faceService: FaceService ) { } @@ -67,9 +69,15 @@ export class ElementIframeComponent implements OnInit, AfterViewInit, OnDestroy break; case 'CLOSE': this.view.connected = false; + if (this.view.connectToken.face_monitor_token) { + this.faceService.removeMonitoringTab(this.view.id); + } break; case 'CONNECTED': this.view.connected = true; + if (this.view.connectToken.face_monitor_token) { + this.faceService.addMonitoringTab(this.view.id); + } break; case 'CLICK': document.body.click(); diff --git a/src/app/elements/nav/nav.component.ts b/src/app/elements/nav/nav.component.ts index e638bb22..e205703c 100644 --- a/src/app/elements/nav/nav.component.ts +++ b/src/app/elements/nav/nav.component.ts @@ -46,10 +46,6 @@ export class ElementNavComponent implements OnInit { this.viewIds = this._viewSrv.viewIds; } - refreshNav() { - this.navs = this.getNav(); - } - getNav() { return [ { @@ -97,36 +93,7 @@ export class ElementNavComponent implements OnInit { { id: 'Language', name: 'Language', - children: [ - { - id: 'English', - click: () => { - this._i18n.use('en'); - }, - name: 'English' - }, - { - id: 'Chinese', - click: () => { - this._i18n.use('zh'); - }, - name: '中文' - }, - { - id: 'Chinese-hant', - click: () => { - this._i18n.use('zh_hant'); - }, - name: '中文(繁體)' - }, - { - id: 'Japanese', - click: () => { - this._i18n.use('ja'); - }, - name: '日本語' - } - ] + children: this.getLanguageOptions(), }, { id: 'Setting', @@ -232,6 +199,24 @@ export class ElementNavComponent implements OnInit { ]; } + getLanguageOptions() { + const langOptions = []; + this._settingSvc.afterInited().then((state) => { + const languages = this._settingSvc.globalSetting.LANGUAGES; + for (const langObj of languages) { + langOptions.push({ + id: langObj.code, + click: () => { + this._i18n.use(langObj.code); + window.location.reload(); + }, + name: langObj.name + }); + } + }); + return langOptions; + } + onJumpUi() { window.open('/ui/', '_blank'); } diff --git a/src/app/elements/replay/guacamole/guacamole.component.scss b/src/app/elements/replay/guacamole/guacamole.component.scss index 3d9fa668..73d680bb 100644 --- a/src/app/elements/replay/guacamole/guacamole.component.scss +++ b/src/app/elements/replay/guacamole/guacamole.component.scss @@ -286,3 +286,88 @@ input[type="range"]:focus::-ms-fill-upper { background: #5AF; } } + +@media screen and (max-width: 768px) { + #player { + .controls { + height: auto; + min-height: 35px; + padding: 8px; + flex-wrap: wrap; + + // 进度条容器 + span[class*="range"] { + order: -1; + width: 100%; + padding: 4px 0; + } + + // 基础控制按钮和时间信息保持在同一行 + #play-pause, + #position, + #duration { + margin: 4px; + } + + // 资产信息换行显示 + #user, + #asset, + #system_user, + #date_start, + #download { + width: 100%; + padding: 4px 0 !important; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + } + + #display { + height: auto; + min-height: 200px; + padding: 0; + + // 隐藏左侧命令列表 + as-split-area[order="0"] { + display: none; + } + + // 视频区域自适应 + as-split-area[order="1"] { + width: 100% !important; + max-width: 100% !important; + min-width: 100% !important; + } + + #screen { + width: 100%; + height: calc(100vh - 200px); // 减去控制栏和其他元素的高度 + display: flex; + align-items: center; + justify-content: center; + + // 确保视频内容居中显示 + > * { + max-width: 100%; + max-height: 100%; + margin: auto; + } + } + + // 优化 seeking 提示 + .seek-notification { + display: flex; + align-items: center; + justify-content: center; + + p { + display: flex; + flex-direction: column; + gap: 10px; + align-items: center; + } + } + } + } +} diff --git a/src/app/elements/replay/guacamole/guacamole.component.ts b/src/app/elements/replay/guacamole/guacamole.component.ts index 07e50770..d13f7458 100644 --- a/src/app/elements/replay/guacamole/guacamole.component.ts +++ b/src/app/elements/replay/guacamole/guacamole.component.ts @@ -43,6 +43,7 @@ export class ElementReplayGuacamoleComponent implements OnInit, OnChanges { rangeHideClass = 'hideCursor'; lastDuration: number = 0; interval: number; + initializedCommand: boolean = false; constructor(private _http: HttpService, private _translate: TranslateService) {} @@ -59,24 +60,36 @@ export class ElementReplayGuacamoleComponent implements OnInit, OnChanges { initialize() { if (!this.replay || !this.replay.src) { - alert('Not found replay'); - return; + return alert('Not found replay'); } + this.commands = []; + + const date = new Date(Date.parse(this.replay.date_start)); this.startTime = this.toSafeLocalDateStr(date); this.startTimeStamp = Date.parse(this.replay.date_start); this.playerRef = document.getElementById('player'); this.displayRef = document.getElementById('display'); this.screenRef = document.getElementById('screen'); + const tunnel = new Guacamole.StaticHTTPTunnel(this.replay.src); this.recording = new Guacamole.SessionRecording(tunnel); this.recordingDisplay = this.recording.getDisplay(); const recordingElement = this.recordingDisplay.getElement(); + recordingElement.style.margin = '0 auto'; + this.screenRef.appendChild(recordingElement); + this.initRecording(); - this.getCommands(this.page); + + if (!this.initializedCommand) { + this.getCommands(this.page); + + this.initializedCommand = true; + } + this._translate.get('LeftInfo').subscribe((res: string) => { this.leftInfo = res; }); @@ -88,6 +101,10 @@ export class ElementReplayGuacamoleComponent implements OnInit, OnChanges { .subscribe(() => { this.recordingDisplay.scale(this.getPropScale()); }); + + if (this.isMobile()) { + this.initTouchEvents(); + } } initRecording() { @@ -103,8 +120,8 @@ export class ElementReplayGuacamoleComponent implements OnInit, OnChanges { this.recording.onprogress = (millis) => { if (millis >= this.max) { - this.duration = formatTime(millis); - this.max = millis; + this.duration = formatTime(millis); + this.max = millis; } if (this.firstLoad) { this.recording.play(); @@ -228,7 +245,7 @@ export class ElementReplayGuacamoleComponent implements OnInit, OnChanges { this.recording.seek(this.percent, () => { this.playerRef.className = ''; - this.setDisableStatusSiderElement(false); + this.setDisableStatusSiderElement(false); } ); @@ -269,13 +286,16 @@ export class ElementReplayGuacamoleComponent implements OnInit, OnChanges { if (!this.startTimeStamp) { return; } + this._http.getCommandsData(this.replay.id, page) .subscribe( data => { const results = data.results; - results.forEach(element => { + + results.forEach((element: any) => { element.atime = formatTime(element.timestamp * 1000 - this.startTimeStamp); }); + this.commands = this.commands.concat(results); }, err => { @@ -293,4 +313,58 @@ export class ElementReplayGuacamoleComponent implements OnInit, OnChanges { this.percent = time <= 0 ? 0 : time; this.runFrom(); } + + private isMobile(): boolean { + return window.innerWidth < 768; + } + + private initTouchEvents() { + const screen = document.getElementById('screen'); + if (!screen) return; + + let touchStartX = 0; + let touchStartY = 0; + let touchStartTime = 0; + + screen.addEventListener('touchstart', (e: TouchEvent) => { + touchStartX = e.touches[0].clientX; + touchStartY = e.touches[0].clientY; + touchStartTime = Date.now(); + }); + + screen.addEventListener('touchmove', (e: TouchEvent) => { + // 防止页面滚动 + if (Math.abs(e.touches[0].clientY - touchStartY) > 10) { + e.preventDefault(); + } + }); + + screen.addEventListener('touchend', (e: TouchEvent) => { + const touchEndX = e.changedTouches[0].clientX; + const touchEndY = e.changedTouches[0].clientY; + const touchEndTime = Date.now(); + + const deltaX = touchEndX - touchStartX; + const deltaY = touchEndY - touchStartY; + const deltaTime = touchEndTime - touchStartTime; + + // 点击判定 + if (Math.abs(deltaX) < 10 && Math.abs(deltaY) < 10 && deltaTime < 200) { + this.toggle(); + } + + // 左右滑动判定 + if (Math.abs(deltaX) > 50 && Math.abs(deltaY) < 30) { + const seekTime = 5000; // 5秒 + if (deltaX > 0) { + // 向右滑动,前进 + this.percent = Math.min(this.percent + seekTime, this.max); + } else { + // 向左滑动,后退 + this.percent = Math.max(this.percent - seekTime, 0); + } + this.runFrom(); + } + }); + } } diff --git a/src/app/model.ts b/src/app/model.ts index add34bd3..d427db16 100644 --- a/src/app/model.ts +++ b/src/app/model.ts @@ -99,7 +99,7 @@ export class ConnectEvt { export class Nav { id: string; name: string; - children?: Array