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 @@
+
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