feat(前端): 重构数字人交互界面并添加全屏功能

重构前端界面布局,将控制面板改为浮动式设计,添加全屏显示功能。主要变更包括:
1. 重新设计界面布局,使用浮动面板替代固定面板
2. 实现数字人显示区域的全屏功能
3. 优化SDK容器缩放逻辑
4. 添加面板切换和状态管理功能
5. 更新样式以适应新的交互设计
This commit is contained in:
2026-04-30 09:54:24 +08:00
parent 1db57cd18c
commit ad676cf0b8
8 changed files with 846 additions and 507 deletions

View File

@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDIemmkD9Xbna4D
WydBTk+72YFgbaX16lTwWEaohZlUlROojevKKVSLUW4LnkHfB5AN0S+Kk8mv0vqq
17v8H+qp+p3141kdUtGuPK/Rjli5KahgVNhRkz56JDj2MkDQoP7i69lg4Nxu33+D
E6ZtMafmqgadlHLy4somPcKW2uqFHHRznXucJThGwVxKYyo5ouct86eGg5x5Ov0T
hE/9XdGkP9QJK6RgLzgoiLLAAlXvYhp/TpoGIuHRTabFsSl0vrDAtZxgsq3M5OUw
ltfUz98sgo+aZp7pev6tg1xlyJavclv02HScm0m6GLO/+legil4B5BylZZw179ye
UTn4JtYvAgMBAAECggEADOtKjhgd8kDQ7myidN4DKGCMKra8lUJyrJxbs7XZG2Je
xnCceCyKqYG0kD4a3so1Ev1FTYfxkG/0CsXLp1c2y//9y7mVzA+pIH8CEerugvXE
YOGgbdbwL0NqgkvbWANDLN3OMwtUJgPA5IvQuhFGayICbbqbxiHHeiH/vX31BmFu
NJTYH9BA/56qbMyruMvdo0WOSFgyYmTogBddTVGT4l1+xjKB1hHIggEOxcj+Kcu/
aPMh6W9a843nIt/AUkLztbpD49Jp2u9B+uHUexsp+vgGebHmn1+x5+QOjpr/4BzA
pa41ZdSZwpu10UzWDnXmmrVXuti0fhz8GdYGoD0OiQKBgQD6KiOkoRaUELwvENjV
ybMOs7rCrz3AzvvXwXl9ncP4e25zs0MO3Fdr+RnssashAQl/eMrpWbqXCJ5L8ocV
QdinicNRbiI67J7LIQYCKmoF0qHEioXZGljHCdDOi6mARgaRIXGzrfwTjYpt93BJ
ZZjAJnVemmITK6uCo3JdWdFSeQKBgQDNJ5IBzvwLL+/AkQJ4O6mNmJjsuh+0XLHi
50gAg/QkUJPFD9NIETXuAoSs+HEwoMNNOxvClNkQKuXhKeaVST5IXBNI+OLk4tAX
7kZYgmUB3WoqMRQyxi+HpcK8Vz9yEZYtcjltHaj9U+W758Pz4XDPPZRPVZynwNBT
SWQatz4D5wKBgQDS/EFNctMhE7AzHclTHabXqnvEAJpbh/ZddH6rlCiY5Dk2eaT/
3pQYq2HS/9Ftp3tEODMidZvqD8m6EhUdt4grXQQPJIzYF4KhnPt9ThpXSNtIcPAM
N61SSRQDb0UIu4aEVqmXuhBUyVZMApmAPaCDxqvMmvZGZtli98jXtI3XOQKBgHry
uAH1df+OXvTc8snzl42QYjWc5dtFoiYFzZThf7MzxxtWfJy4E8XsJG81BUDLV8A1
KFHYuq2Bx/rFDbdyCa174OLepbveYi+1UNtCah5P/4wBlcVIGLwGj0ooT5GhEItz
5Q6Ls/b+DLQsFbdW6DCLCxLwj83JNBB171RDdRsJAoGAS+fWOqOVhOiaNWfswJmz
ELOZyqrPx6cvtaaIupf1U4nxyGDZqoNhUkC8TK1rvURtbYCBeh7nMlbz/HIX2Xf1
1Za+lehJzHeNrjw2+mGo8VNkJyfkNJvIyQ6ELggrQLg65uETU8F23mhqY63k2Vdd
/69T6WWhQXpy6e6MyHT5P8Y=
-----END PRIVATE KEY-----

View File

@@ -0,0 +1,17 @@
-----BEGIN CERTIFICATE-----
MIICyTCCAbGgAwIBAgIJA1E28eTat1aCMA0GCSqGSIb3DQEBCwUAMBQxEjAQBgNV
BAMTCWxvY2FsaG9zdDAeFw0yNjA0MzAwMDM5NTJaFw0yNzA0MzAwMDM5NTJaMBQx
EjAQBgNVBAMTCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC
ggEBAMh6aaQP1dudrgNbJ0FOT7vZgWBtpfXqVPBYRqiFmVSVE6iN68opVItRbgue
Qd8HkA3RL4qTya/S+qrXu/wf6qn6nfXjWR1S0a48r9GOWLkpqGBU2FGTPnokOPYy
QNCg/uLr2WDg3G7ff4MTpm0xp+aqBp2UcvLiyiY9wpba6oUcdHOde5wlOEbBXEpj
Kjmi5y3zp4aDnHk6/ROET/1d0aQ/1AkrpGAvOCiIssACVe9iGn9OmgYi4dFNpsWx
KXS+sMC1nGCyrczk5TCW19TP3yyCj5pmnul6/q2DXGXIlq9yW/TYdJybSboYs7/6
V6CKXgHkHKVlnDXv3J5ROfgm1i8CAwEAAaMeMBwwGgYDVR0RBBMwEYIJbG9jYWxo
b3N0hwR/AAABMA0GCSqGSIb3DQEBCwUAA4IBAQBOUidP5/w56yRWMSSkRAqAHmmC
HKd/Q/Q2Haro+vXsyeWGhsGaxaWNo99YQ17K9RDPKPXSiaRebQf5gTKEt/VFMttI
HLzJM7/X4RK+wRRs1alD+4UBxI+dISS51BAYQEMc1ib9tzqInsW6ItvBEWuIIXyt
Itwqsvkw9lH8ClgrrgLADw3mzWfFXOaadEJxjshcSNgg6V1JcC4v0D/Nve2P/pLW
KSQYbudCn4AOpNPgChNckH9yrMU8mELtl1hmO1V35vYsJ4q6BDkmoT5aE6L+p3dC
+z6A49MEZ/W/n1Pgt0nXniSmUfCnzYz3ERVx7i7dZozOAk9g463UD3aazuSg
-----END CERTIFICATE-----

View File

@@ -5,142 +5,153 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>华为交互数字人 - HwICSUiSdk 演示</title>
<link rel="stylesheet" href="./styles/main.css">
<link rel="stylesheet" href="./hwICSUiSdk/HwICSUiSdk.css">
</head>
<body>
<div class="app-container">
<!-- 头部 -->
<header class="header">
<div class="logo">
<span class="logo-icon">🎭</span>
<h1>华为交互数字人</h1>
</div>
<div class="status-bar">
<span id="connectionStatus" class="status disconnected">未连接</span>
<span id="sdkVersion" class="version"></span>
</div>
</header>
<!-- 主内容区 -->
<main class="main-content">
<!-- 配置面板 -->
<section class="config-panel">
<h2>📋 连接配置</h2>
<!-- 基本配置 -->
<div class="config-section">
<h3>基本配置</h3>
<div class="form-group">
<label for="serverAddress">服务器地址</label>
<input type="text" id="serverAddress" placeholder="请输入服务器地址" value="metastudio-api.cn-north-4.myhuaweicloud.com">
</div>
<div class="form-group">
<label for="robotId">机器人ID</label>
<input type="text" id="robotId" placeholder="请输入机器人ID" value="2c9084bf9c9cc1c0019db2c4ca2a5c89">
</div>
</div>
<!-- 授权码配置 -->
<div class="config-section">
<h3>授权码</h3>
<div class="form-group">
<label for="onceCode">一次性授权码</label>
<div class="input-with-button">
<input type="text" id="onceCode" placeholder="请输入一次性授权码或点击获取" value="" readonly>
<button id="getOnceCodeBtn" class="btn btn-secondary">🔑 获取授权码</button>
</div>
<small class="field-hint">有效期5分钟,只能使用一次</small>
</div>
</div>
<div class="form-actions main-actions">
<button id="initBtn" class="btn btn-primary">初始化 SDK</button>
<button id="destroyBtn" class="btn btn-danger" disabled>销毁 SDK</button>
</div>
</section>
<!-- 数字人显示区 -->
<section class="digital-human-container">
<h2>🎬 数字人显示</h2>
<div id="digitalHumanBox" class="digital-human-box">
<div class="placeholder">
<span class="placeholder-icon">🤖</span>
<p>请先初始化 SDK 并创建数字人</p>
</div>
</div>
<!-- 控制按钮 -->
<div class="control-panel">
<div class="control-group">
<h3>对话控制</h3>
<div class="button-row">
<button id="startChatBtn" class="btn btn-success" disabled>
<span class="icon">▶️</span> 开始对话
</button>
<button id="stopChatBtn" class="btn btn-warning" disabled>
<span class="icon">⏹️</span> 停止对话
</button>
</div>
</div>
<div class="control-group">
<h3>语音交互</h3>
<div class="button-row">
<button id="startSpeakBtn" class="btn btn-info" disabled>
<span class="icon">🎤</span> 开始说话
</button>
<button id="stopSpeakBtn" class="btn btn-secondary" disabled>
<span class="icon">🛑</span> 停止说话
</button>
</div>
</div>
<div class="control-group">
<h3>文本交互</h3>
<div class="text-input-row">
<input type="text" id="textInput" placeholder="输入文本消息..." disabled>
<button id="sendTextBtn" class="btn btn-primary" disabled>
<span class="icon">📤</span> 发送
</button>
</div>
</div>
<div class="control-group">
<h3>其他控制</h3>
<div class="button-row">
<button id="interruptBtn" class="btn btn-warning" disabled>
<span class="icon">⚡</span> 打断说话
</button>
<button id="toggleAudioBtn" class="btn btn-secondary" disabled>
<span class="icon">🔊</span> 静音/取消静音
</button>
</div>
</div>
</div>
</section>
<!-- 日志面板 -->
<section class="log-panel">
<h2>📜 事件日志</h2>
<div class="log-toolbar">
<button id="clearLogBtn" class="btn btn-small">清空日志</button>
<select id="logLevel">
<option value="debug">Debug</option>
<option value="info" selected>Info</option>
<option value="warn">Warn</option>
<option value="error">Error</option>
</select>
</div>
<div id="logContainer" class="log-container"></div>
</section>
</main>
<!-- 底部 -->
<footer class="footer">
<p>Powered by HwICSUiSdk | 华为交互数字人解决方案</p>
</footer>
<!-- 数字人全视口容器 -->
<div id="digitalHumanBox" class="digital-human-box">
<div class="fullscreen-exit-hint">按 ESC 退出全屏</div>
<div class="placeholder">
<span class="placeholder-icon">🤖</span>
<p>点击右下角 ⚙️ 按钮初始化 SDK</p>
</div>
</div>
<!-- 浮动控制栏 -->
<div class="floating-toolbar">
<button id="toggleConfigBtn" class="toolbar-btn" title="连接配置">
⚙️
</button>
<button id="toggleControlBtn" class="toolbar-btn" title="对话控制">
🎮
</button>
<button id="toggleLogBtn" class="toolbar-btn" title="事件日志">
📜
</button>
<button id="fullscreenBtn" class="toolbar-btn" title="全屏显示">
⛶
</button>
</div>
<!-- 浮动面板:连接配置 -->
<div id="configPanel" class="floating-panel config-panel">
<div class="panel-header">
<h2>📋 连接配置</h2>
<button class="panel-close" data-panel="configPanel">×</button>
</div>
<div class="panel-content">
<div class="config-section">
<h3>基本配置</h3>
<div class="form-group">
<label for="serverAddress">服务器地址</label>
<input type="text" id="serverAddress" placeholder="请输入服务器地址" value="metastudio-api.cn-north-4.myhuaweicloud.com">
</div>
<div class="form-group">
<label for="robotId">机器人ID</label>
<input type="text" id="robotId" placeholder="请输入机器人ID" value="2c9084bf9c9cc1c0019db2c4ca2a5c89">
</div>
</div>
<div class="config-section">
<h3>授权码</h3>
<div class="form-group">
<label for="onceCode">一次性授权码</label>
<div class="input-with-button">
<input type="text" id="onceCode" placeholder="请输入一次性授权码或点击获取" value="" readonly>
<button id="getOnceCodeBtn" class="btn btn-secondary">🔑 获取</button>
</div>
<small class="field-hint">有效期5分钟,只能使用一次</small>
</div>
</div>
<div class="form-actions">
<button id="initBtn" class="btn btn-primary">初始化 SDK</button>
<button id="destroyBtn" class="btn btn-danger" disabled>销毁 SDK</button>
</div>
</div>
</div>
<!-- 浮动面板:对话控制 -->
<div id="controlPanel" class="floating-panel control-panel">
<div class="panel-header">
<h2>🎮 对话控制</h2>
<button class="panel-close" data-panel="controlPanel">×</button>
</div>
<div class="panel-content">
<div class="control-group">
<h3>对话控制</h3>
<div class="button-row">
<button id="startChatBtn" class="btn btn-success" disabled>
<span class="icon">▶️</span> 开始对话
</button>
<button id="stopChatBtn" class="btn btn-warning" disabled>
<span class="icon">⏹️</span> 停止对话
</button>
</div>
</div>
<div class="control-group">
<h3>语音交互</h3>
<div class="button-row">
<button id="startSpeakBtn" class="btn btn-info" disabled>
<span class="icon">🎤</span> 开始说话
</button>
<button id="stopSpeakBtn" class="btn btn-secondary" disabled>
<span class="icon">🛑</span> 停止说话
</button>
</div>
</div>
<div class="control-group">
<h3>文本交互</h3>
<div class="text-input-row">
<input type="text" id="textInput" placeholder="输入文本消息..." disabled>
<button id="sendTextBtn" class="btn btn-primary" disabled>
<span class="icon">📤</span> 发送
</button>
</div>
</div>
<div class="control-group">
<h3>其他控制</h3>
<div class="button-row">
<button id="interruptBtn" class="btn btn-warning" disabled>
<span class="icon">⚡</span> 打断说话
</button>
<button id="toggleAudioBtn" class="btn btn-secondary" disabled>
<span class="icon">🔊</span> 静音
</button>
</div>
</div>
</div>
</div>
<!-- 浮动面板:事件日志 -->
<div id="logPanel" class="floating-panel log-panel">
<div class="panel-header">
<h2>📜 事件日志</h2>
<div class="log-toolbar">
<button id="clearLogBtn" class="btn btn-small btn-secondary">清空</button>
<select id="logLevel">
<option value="debug">Debug</option>
<option value="info" selected>Info</option>
<option value="warn">Warn</option>
<option value="error">Error</option>
</select>
</div>
<button class="panel-close" data-panel="logPanel">×</button>
</div>
<div class="panel-content">
<div id="logContainer" class="log-container"></div>
</div>
</div>
<!-- 状态栏 -->
<div class="status-bar-floating">
<span id="connectionStatus" class="status disconnected">未连接</span>
<span id="sdkVersion" class="version"></span>
</div>
<!-- SDK 引入 -->
<script type="module" src="./scripts/main.js"></script>
</body>
</html>

18
frontend/package.json Normal file
View File

@@ -0,0 +1,18 @@
{
"name": "frontend",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "node server.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"type": "commonjs",
"dependencies": {
"selfsigned": "^5.5.0",
"serve-handler": "^6.1.7"
}
}

View File

@@ -1,18 +1,25 @@
const API_BASE = 'http://localhost:3001';
export async function fetchOnceCode() {
console.log('正在调用后端 API:', `${API_BASE}/api/once-code`);
const response = await fetch(`${API_BASE}/api/once-code`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
},
body: '{}'
});
console.log('后端响应状态:', response.status);
if (!response.ok) {
const error = await response.json();
console.error('后端返回错误:', error);
throw new Error(error.error_msg || '获取授权码失败');
}
const data = await response.json();
console.log('后端返回数据:', data);
return data.once_code;
}

View File

@@ -1,18 +1,41 @@
/**
* 华为交互数字人 - HwICSUiSdk 集成示例
*
* 功能说明:
* 1. SDK 初始化和销毁
* 2. 数字人创建和显示
* 3. 语音/文本对话交互
* 4. 事件监听和日志记录
*/
// 导入 HwICSUiSdk
import { HwICSUiSdk, ICSEventKey, InteractionMode, ConfigKey } from '../hwICSUiSdk/HwICSUiSdk.esm.js';
import { HwICSUiSdk } from '../hwICSUiSdk/HwICSUiSdk.esm.js';
import { fetchOnceCode } from './api.js';
// ===== 全局状态 =====
const ICSEventKey = {
ERROR: 'error',
JOB_INFO_CHANGE: 'jobInfoChange',
SPEAKING_START: 'speakingStart',
SPEAKING_STOP: 'speakingStop',
SPEECH_RECOGNIZED: 'speechRecognized',
SEMANTIC_RECOGNIZED: 'semanticRecognized',
ENTER_SLEEP: 'enterSleep',
ENTER_ACTIVE: 'enterActive',
LANGUAGE_INFO_CHANGE: 'languageInfoChange',
JOB_END_MENTION: 'jobEndMention',
JOB_END: 'jobEnd'
};
const InteractionMode = {
AUDIO: 'AUDIO',
TEXT: 'TEXT'
};
const ConfigKey = {
ENABLE_CAPTION: 'enableCaption',
ENABLE_CHAT_BTN: 'enableChatBtn',
ENABLE_HOT_ISSUES: 'enableHotIssues',
ENABLE_WEAK_ERROR_INFO: 'enableWeakErrorInfo',
SHOW_LANGUAGE_UI: 'showLanguageUI',
ENABLE_DEVICE_SETTING: 'enableDeviceSetting',
ENABLE_NETWORK_CHECK: 'enableNetworkCheck',
AUTO_PLAY_REMOTE_VIDEO: 'autoPlayRemoteVideo',
DEFAULT_MUTE_REMOTE_AUDIO: 'defaultMuteRemoteAudio'
};
const state = {
isInitialized: false,
isChatting: false,
@@ -22,14 +45,11 @@ const state = {
currentInteractionMode: InteractionMode.AUDIO
};
// ===== DOM 元素引用 =====
const elements = {
// 配置输入
serverAddress: document.getElementById('serverAddress'),
robotId: document.getElementById('robotId'),
onceCode: document.getElementById('onceCode'),
// 按钮
initBtn: document.getElementById('initBtn'),
destroyBtn: document.getElementById('destroyBtn'),
getOnceCodeBtn: document.getElementById('getOnceCodeBtn'),
@@ -41,19 +61,31 @@ const elements = {
interruptBtn: document.getElementById('interruptBtn'),
toggleAudioBtn: document.getElementById('toggleAudioBtn'),
clearLogBtn: document.getElementById('clearLogBtn'),
fullscreenBtn: document.getElementById('fullscreenBtn'),
toggleConfigBtn: document.getElementById('toggleConfigBtn'),
toggleControlBtn: document.getElementById('toggleControlBtn'),
toggleLogBtn: document.getElementById('toggleLogBtn'),
// 输入框
textInput: document.getElementById('textInput'),
logLevel: document.getElementById('logLevel'),
// 显示区域
digitalHumanBox: document.getElementById('digitalHumanBox'),
logContainer: document.getElementById('logContainer'),
connectionStatus: document.getElementById('connectionStatus'),
sdkVersion: document.getElementById('sdkVersion')
};
// ===== 日志工具 =====
const fullscreenState = {
isFullscreen: false,
sdkInitialized: false
};
const panelState = {
configPanel: false,
controlPanel: false,
logPanel: false
};
const logger = {
log(level, message, data = null) {
const timestamp = new Date().toLocaleTimeString('zh-CN', { hour12: false });
@@ -81,7 +113,6 @@ const logger = {
error(message, data) { this.log('ERROR', message, data); }
};
// ===== SDK 事件监听配置 =====
const eventListeners = {
[ICSEventKey.ERROR]: (error) => {
logger.error('SDK 错误', error);
@@ -138,7 +169,6 @@ const eventListeners = {
}
};
// ===== UI 状态更新 =====
function updateUIState() {
const { isInitialized, isChatting, isSpeaking } = state;
@@ -175,7 +205,39 @@ function updateConnectionStatus(connected) {
}
}
// ===== SDK 操作 =====
function togglePanel(panelId) {
const panel = document.getElementById(panelId);
console.log('切换面板:', panelId, '找到元素:', !!panel);
if (!panel) return;
const isVisible = panel.classList.contains('visible');
console.log('当前可见:', isVisible);
Object.keys(panelState).forEach(key => {
const el = document.getElementById(key);
if (el) el.classList.remove('visible');
panelState[key] = false;
});
document.querySelectorAll('.toolbar-btn').forEach(btn => btn.classList.remove('active'));
if (!isVisible) {
panel.classList.add('visible');
panelState[panelId] = true;
console.log('面板已显示:', panelId);
const btnMap = {
configPanel: 'toggleConfigBtn',
controlPanel: 'toggleControlBtn',
logPanel: 'toggleLogBtn'
};
const btnId = btnMap[panelId];
if (btnId && elements[btnId]) {
elements[btnId].classList.add('active');
}
}
}
async function initializeSDK() {
try {
@@ -221,10 +283,10 @@ async function initializeSDK() {
[ConfigKey.ENABLE_CAPTION]: true,
[ConfigKey.ENABLE_CHAT_BTN]: true,
[ConfigKey.ENABLE_HOT_ISSUES]: true,
[ConfigKey.ENABLE_WEAK_ERROR_INFO]: true,
[ConfigKey.SHOW_LANGUAGE_UI]: true,
[ConfigKey.ENABLE_DEVICE_SETTING]: true,
[ConfigKey.ENABLE_NETWORK_CHECK]: true,
[ConfigKey.ENABLE_WEAK_ERROR_INFO]: false,
[ConfigKey.SHOW_LANGUAGE_UI]: false,
[ConfigKey.ENABLE_DEVICE_SETTING]: false,
[ConfigKey.ENABLE_NETWORK_CHECK]: false,
[ConfigKey.AUTO_PLAY_REMOTE_VIDEO]: true,
[ConfigKey.DEFAULT_MUTE_REMOTE_AUDIO]: false
},
@@ -234,6 +296,7 @@ async function initializeSDK() {
await HwICSUiSdk.create(createParam);
state.isInitialized = true;
fullscreenState.sdkInitialized = true;
updateConnectionStatus(true);
updateUIState();
@@ -241,6 +304,9 @@ async function initializeSDK() {
logger.info('SDK 初始化成功', { version: HwICSUiSdk.version });
rebindDynamicButtons();
setTimeout(calculateScale, 500);
} catch (error) {
logger.error('SDK 初始化失败', error);
state.isInitialized = false;
@@ -258,14 +324,16 @@ async function destroySDK() {
state.isChatting = false;
state.isSpeaking = false;
state.jobInfo = null;
fullscreenState.sdkInitialized = false;
updateConnectionStatus(false);
updateUIState();
elements.digitalHumanBox.innerHTML = `
<div class="fullscreen-exit-hint">按 ESC 退出全屏</div>
<div class="placeholder">
<span class="placeholder-icon">🤖</span>
<p>请先初始化 SDK 并创建数字人</p>
<p>点击右下角 ⚙️ 按钮初始化 SDK</p>
</div>
`;
@@ -436,7 +504,7 @@ async function handleGetOnceCode() {
logger.error('获取授权码失败', error.message);
} finally {
elements.getOnceCodeBtn.disabled = false;
elements.getOnceCodeBtn.textContent = '🔑 获取授权码';
elements.getOnceCodeBtn.textContent = '🔑 获取';
}
}
@@ -445,7 +513,67 @@ function clearLogs() {
logger.info('日志已清空');
}
// ===== 事件绑定 =====
function calculateScale() {
const container = elements.digitalHumanBox;
const sdkContainer = container.querySelector('.digital-intelligent-interaction');
if (!sdkContainer) return;
const containerWidth = container.clientWidth;
const containerHeight = container.clientHeight;
const sdkWidth = 1920;
const sdkHeight = 1080;
const scaleX = containerWidth / sdkWidth;
const scaleY = containerHeight / sdkHeight;
const scale = Math.max(scaleX, scaleY);
sdkContainer.style.transform = `scale(${scale})`;
sdkContainer.style.transformOrigin = 'center center';
}
function toggleFullscreen() {
if (!fullscreenState.sdkInitialized) {
logger.warn('请先初始化 SDK');
return;
}
fullscreenState.isFullscreen = !fullscreenState.isFullscreen;
if (fullscreenState.isFullscreen) {
elements.digitalHumanBox.classList.add('fullscreen');
elements.fullscreenBtn.innerHTML = '⛶';
elements.fullscreenBtn.title = '退出全屏';
logger.info('进入全屏模式');
} else {
elements.digitalHumanBox.classList.remove('fullscreen');
elements.fullscreenBtn.innerHTML = '⛶';
elements.fullscreenBtn.title = '全屏显示';
logger.info('退出全屏模式');
}
setTimeout(calculateScale, 100);
}
function handleFullscreenKeydown(e) {
if (e.key === 'Escape' && fullscreenState.isFullscreen) {
toggleFullscreen();
}
}
function initScaling() {
const resizeObserver = new ResizeObserver(() => {
if (fullscreenState.sdkInitialized) {
calculateScale();
}
});
resizeObserver.observe(elements.digitalHumanBox);
window.addEventListener('keydown', handleFullscreenKeydown);
}
function bindEvents() {
elements.initBtn.addEventListener('click', initializeSDK);
elements.destroyBtn.addEventListener('click', destroySDK);
@@ -475,6 +603,18 @@ function bindEvents() {
}
});
elements.toggleConfigBtn.addEventListener('click', () => togglePanel('configPanel'));
elements.toggleControlBtn.addEventListener('click', () => togglePanel('controlPanel'));
elements.toggleLogBtn.addEventListener('click', () => togglePanel('logPanel'));
elements.fullscreenBtn.addEventListener('click', toggleFullscreen);
document.querySelectorAll('.panel-close').forEach(btn => {
btn.addEventListener('click', () => {
const panelId = btn.dataset.panel;
if (panelId) togglePanel(panelId);
});
});
window.addEventListener('beforeunload', () => {
if (state.isInitialized) {
HwICSUiSdk.destroy();
@@ -482,13 +622,21 @@ function bindEvents() {
});
}
// ===== 初始化 =====
function rebindDynamicButtons() {
const fullscreenBtn = document.getElementById('fullscreenBtn');
if (fullscreenBtn) {
fullscreenBtn.removeEventListener('click', toggleFullscreen);
fullscreenBtn.addEventListener('click', toggleFullscreen);
}
}
function init() {
logger.info('应用初始化完成');
logger.info('HwICSUiSdk 版本', { version: HwICSUiSdk.version, buildTime: HwICSUiSdk.buildTime });
bindEvents();
updateUIState();
initScaling();
}
init();

68
frontend/server.js Normal file
View File

@@ -0,0 +1,68 @@
const https = require('https');
const fs = require('fs');
const path = require('path');
const selfsigned = require('selfsigned');
const handler = require('serve-handler');
const PORT = 8443;
const CERT_DIR = path.join(__dirname, 'cert');
// 确保证书目录存在
if (!fs.existsSync(CERT_DIR)) {
fs.mkdirSync(CERT_DIR, { recursive: true });
}
const CERT_PATH = path.join(CERT_DIR, 'localhost.pem');
const KEY_PATH = path.join(CERT_DIR, 'localhost-key.pem');
async function startServer() {
let cert, key;
if (fs.existsSync(CERT_PATH) && fs.existsSync(KEY_PATH)) {
console.log('使用已生成的自签名证书...');
cert = fs.readFileSync(CERT_PATH);
key = fs.readFileSync(KEY_PATH);
} else {
console.log('正在生成自签名证书...');
const attrs = [{ name: 'commonName', value: 'localhost' }];
const pems = await selfsigned.generate(attrs, {
keySize: 2048,
algorithm: 'sha256',
extensions: [
{
name: 'subjectAltName',
altNames: [
{ type: 2, value: 'localhost' },
{ type: 7, ip: '127.0.0.1' }
]
}
]
});
fs.writeFileSync(CERT_PATH, pems.cert);
fs.writeFileSync(KEY_PATH, pems.private);
cert = pems.cert;
key = pems.private;
console.log('自签名证书已生成并保存');
}
// 创建 HTTPS 服务器
const server = https.createServer({ key, cert }, (req, res) => {
return handler(req, res, {
public: __dirname,
rewrites: [
{ source: '**', destination: '/index.html' }
]
});
});
server.listen(PORT, () => {
console.log(`前端 HTTPS 服务已启动: https://localhost:${PORT}`);
console.log('注意:浏览器会提示证书不安全,请点击"高级"->"继续访问"');
});
}
startServer().catch(err => {
console.error('启动失败:', err);
process.exit(1);
});

View File

@@ -1,18 +1,15 @@
/* ===== CSS 变量定义 ===== */
:root {
/* 主色调 - 科技蓝 */
--primary-color: #2563eb;
--primary-dark: #1d4ed8;
--primary-light: #3b82f6;
--primary-gradient: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%);
/* 辅助色 */
--success-color: #10b981;
--warning-color: #f59e0b;
--danger-color: #ef4444;
--info-color: #06b6d4;
/* 中性色 */
--bg-primary: #0f172a;
--bg-secondary: #1e293b;
--bg-tertiary: #334155;
@@ -21,26 +18,22 @@
--text-muted: #64748b;
--border-color: #475569;
/* 阴影 */
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.3);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.4);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.5);
--shadow-glow: 0 0 20px rgba(37, 99, 235, 0.3);
/* 间距 */
--spacing-xs: 0.25rem;
--spacing-sm: 0.5rem;
--spacing-md: 1rem;
--spacing-lg: 1.5rem;
--spacing-xl: 2rem;
/* 圆角 */
--radius-sm: 0.375rem;
--radius-md: 0.5rem;
--radius-lg: 0.75rem;
--radius-xl: 1rem;
/* 字体 */
--font-family: 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif;
}
@@ -56,128 +49,207 @@ body {
background: var(--bg-primary);
color: var(--text-primary);
min-height: 100vh;
line-height: 1.6;
overflow: hidden;
}
/* ===== 应用容器 ===== */
.app-container {
min-height: 100vh;
display: flex;
flex-direction: column;
}
/* ===== 头部 ===== */
.header {
background: var(--bg-secondary);
border-bottom: 1px solid var(--border-color);
padding: var(--spacing-md) var(--spacing-xl);
display: flex;
justify-content: space-between;
align-items: center;
position: sticky;
/* ===== 数字人全视口容器 ===== */
.digital-human-box {
position: fixed;
top: 0;
z-index: 100;
box-shadow: var(--shadow-md);
}
.logo {
left: 0;
width: 100vw;
height: 100vh;
background: var(--bg-primary);
display: flex;
align-items: center;
gap: var(--spacing-md);
justify-content: center;
z-index: 1;
}
.logo-icon {
font-size: 2rem;
.digital-human-box.active {
box-shadow: inset 0 0 50px rgba(37, 99, 235, 0.1);
}
.digital-human-box::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background:
radial-gradient(circle at 20% 80%, rgba(37, 99, 235, 0.05) 0%, transparent 50%),
radial-gradient(circle at 80% 20%, rgba(6, 182, 212, 0.05) 0%, transparent 50%);
pointer-events: none;
z-index: 0;
}
.placeholder {
text-align: center;
color: var(--text-muted);
z-index: 1;
}
.placeholder-icon {
font-size: 5rem;
display: block;
margin-bottom: var(--spacing-md);
opacity: 0.5;
animation: float 3s ease-in-out infinite;
}
@keyframes float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-5px); }
50% { transform: translateY(-10px); }
}
.logo h1 {
font-size: 1.5rem;
font-weight: 600;
background: linear-gradient(135deg, #60a5fa 0%, #3b82f6 50%, #2563eb 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
.placeholder p {
font-size: 1rem;
opacity: 0.7;
}
.status-bar {
display: flex;
align-items: center;
gap: var(--spacing-lg);
}
.status {
padding: var(--spacing-xs) var(--spacing-md);
border-radius: var(--radius-full);
.fullscreen-exit-hint {
position: absolute;
bottom: var(--spacing-lg);
left: 50%;
transform: translateX(-50%);
background: rgba(30, 41, 59, 0.8);
backdrop-filter: blur(10px);
padding: var(--spacing-sm) var(--spacing-md);
border-radius: var(--radius-md);
color: var(--text-secondary);
font-size: 0.875rem;
font-weight: 500;
opacity: 0;
transition: opacity 0.3s ease;
pointer-events: none;
z-index: 10;
}
.digital-human-box.fullscreen:hover .fullscreen-exit-hint {
opacity: 1;
}
/* ===== 浮动控制栏 ===== */
.floating-toolbar {
position: fixed;
bottom: var(--spacing-xl);
right: var(--spacing-xl);
display: flex;
align-items: center;
gap: var(--spacing-xs);
flex-direction: column;
gap: var(--spacing-sm);
z-index: 1000;
}
.status::before {
content: '';
width: 8px;
height: 8px;
border-radius: 50%;
background: currentColor;
}
.status.connected {
background: rgba(16, 185, 129, 0.2);
color: var(--success-color);
}
.status.disconnected {
background: rgba(239, 68, 68, 0.2);
color: var(--danger-color);
}
.version {
color: var(--text-muted);
font-size: 0.875rem;
}
/* ===== 主内容区 ===== */
.main-content {
flex: 1;
padding: var(--spacing-xl);
display: grid;
grid-template-columns: 320px 1fr 380px;
gap: var(--spacing-xl);
max-width: 1600px;
margin: 0 auto;
width: 100%;
}
/* ===== 面板通用样式 ===== */
section {
background: var(--bg-secondary);
.toolbar-btn {
width: 50px;
height: 50px;
border-radius: var(--radius-lg);
padding: var(--spacing-lg);
background: var(--bg-secondary);
border: 1px solid var(--border-color);
color: var(--text-primary);
font-size: 1.5rem;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
box-shadow: var(--shadow-md);
}
section h2 {
font-size: 1.125rem;
font-weight: 600;
margin-bottom: var(--spacing-lg);
color: var(--text-primary);
display: flex;
align-items: center;
gap: var(--spacing-sm);
.toolbar-btn:hover {
background: var(--primary-color);
border-color: var(--primary-color);
transform: scale(1.1);
box-shadow: var(--shadow-glow);
}
/* ===== 配置面板 ===== */
.toolbar-btn:active {
transform: scale(0.95);
}
.toolbar-btn.active {
background: var(--primary-color);
border-color: var(--primary-color);
}
/* ===== 浮动面板通用样式 ===== */
.floating-panel {
position: fixed;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: var(--radius-xl);
box-shadow: var(--shadow-lg);
z-index: 999;
display: none;
flex-direction: column;
max-height: 80vh;
overflow: hidden;
animation: panelSlideIn 0.2s ease-out;
}
@keyframes panelSlideIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.floating-panel.visible {
display: flex;
}
.panel-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--spacing-md) var(--spacing-lg);
border-bottom: 1px solid var(--border-color);
background: var(--bg-tertiary);
}
.panel-header h2 {
font-size: 1rem;
font-weight: 600;
color: var(--text-primary);
margin: 0;
}
.panel-close {
width: 30px;
height: 30px;
border-radius: var(--radius-md);
background: transparent;
border: none;
color: var(--text-secondary);
font-size: 1.5rem;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
.panel-close:hover {
background: var(--danger-color);
color: white;
}
.panel-content {
padding: var(--spacing-lg);
overflow-y: auto;
flex: 1;
}
/* ===== 连接配置面板 ===== */
.config-panel {
height: fit-content;
bottom: calc(var(--spacing-xl) + 60px);
right: var(--spacing-xl);
width: 360px;
max-height: 70vh;
}
.config-section {
@@ -238,16 +310,6 @@ section h2 {
cursor: default;
}
.input-with-status {
position: relative;
display: flex;
align-items: center;
}
.input-with-status input {
padding-right: 80px;
}
.input-with-button {
display: flex;
gap: var(--spacing-sm);
@@ -257,30 +319,6 @@ section h2 {
flex: 1;
}
.field-status {
position: absolute;
right: var(--spacing-sm);
font-size: 0.75rem;
padding: 2px 8px;
border-radius: var(--radius-sm);
font-weight: 500;
}
.field-status.success {
background: rgba(16, 185, 129, 0.2);
color: var(--success-color);
}
.field-status.error {
background: rgba(239, 68, 68, 0.2);
color: var(--danger-color);
}
.field-status.loading {
background: rgba(6, 182, 212, 0.2);
color: var(--info-color);
}
.field-hint {
display: block;
margin-top: var(--spacing-xs);
@@ -291,13 +329,194 @@ section h2 {
.form-actions {
display: flex;
gap: var(--spacing-sm);
margin-top: var(--spacing-md);
margin-top: var(--spacing-lg);
}
.form-actions.main-actions {
margin-top: var(--spacing-xl);
padding-top: var(--spacing-lg);
border-top: 2px solid var(--border-color);
/* ===== 对话控制面板 ===== */
.control-panel {
bottom: calc(var(--spacing-xl) + 60px);
right: var(--spacing-xl);
width: 360px;
}
.control-group {
margin-bottom: var(--spacing-lg);
}
.control-group:last-child {
margin-bottom: 0;
}
.control-group h3 {
font-size: 0.875rem;
font-weight: 600;
color: var(--text-secondary);
margin-bottom: var(--spacing-sm);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.button-row {
display: flex;
gap: var(--spacing-sm);
flex-wrap: wrap;
}
.button-row .btn {
flex: 1;
min-width: 100px;
}
.text-input-row {
display: flex;
gap: var(--spacing-sm);
}
.text-input-row input {
flex: 1;
padding: var(--spacing-sm) var(--spacing-md);
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
color: var(--text-primary);
font-size: 0.875rem;
}
.text-input-row input:focus {
outline: none;
border-color: var(--primary-color);
}
.text-input-row input:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* ===== 日志面板 ===== */
.log-panel {
bottom: calc(var(--spacing-xl) + 60px);
right: var(--spacing-xl);
width: 450px;
max-height: 60vh;
}
.log-toolbar {
display: flex;
gap: var(--spacing-sm);
align-items: center;
}
.log-toolbar select {
padding: var(--spacing-xs) var(--spacing-sm);
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
color: var(--text-primary);
font-size: 0.875rem;
}
.log-container {
overflow-y: auto;
background: var(--bg-primary);
border-radius: var(--radius-md);
padding: var(--spacing-md);
font-family: 'Consolas', 'Monaco', monospace;
font-size: 0.8125rem;
line-height: 1.5;
max-height: 400px;
}
.log-entry {
padding: var(--spacing-xs) 0;
border-bottom: 1px solid var(--bg-tertiary);
display: flex;
gap: var(--spacing-sm);
animation: slideIn 0.2s ease-out;
}
.log-entry:last-child {
border-bottom: none;
}
.log-time {
color: var(--text-muted);
white-space: nowrap;
}
.log-level {
font-weight: 600;
min-width: 50px;
}
.log-level.debug {
color: var(--text-muted);
}
.log-level.info {
color: var(--info-color);
}
.log-level.warn {
color: var(--warning-color);
}
.log-level.error {
color: var(--danger-color);
}
.log-message {
color: var(--text-primary);
word-break: break-all;
}
/* ===== 状态栏 ===== */
.status-bar-floating {
position: fixed;
top: var(--spacing-lg);
left: var(--spacing-lg);
display: flex;
align-items: center;
gap: var(--spacing-md);
z-index: 10000;
background: var(--bg-secondary);
padding: var(--spacing-sm) var(--spacing-md);
border-radius: var(--radius-lg);
border: 1px solid var(--border-color);
box-shadow: var(--shadow-md);
backdrop-filter: blur(10px);
}
.status {
padding: var(--spacing-xs) var(--spacing-md);
border-radius: var(--radius-full);
font-size: 0.875rem;
font-weight: 500;
display: flex;
align-items: center;
gap: var(--spacing-xs);
}
.status::before {
content: '';
width: 8px;
height: 8px;
border-radius: 50%;
background: currentColor;
}
.status.connected {
background: rgba(16, 185, 129, 0.2);
color: var(--success-color);
}
.status.disconnected {
background: rgba(239, 68, 68, 0.2);
color: var(--danger-color);
}
.version {
color: var(--text-muted);
font-size: 0.875rem;
}
/* ===== 按钮样式 ===== */
@@ -374,233 +593,60 @@ section h2 {
font-size: 0.75rem;
}
/* ===== 数字人显示区 ===== */
.digital-human-container {
display: flex;
flex-direction: column;
/* ===== 全屏模式 ===== */
.digital-human-box.fullscreen {
z-index: 9999 !important;
}
.digital-human-box {
flex: 1;
min-height: 400px;
background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
border-radius: var(--radius-lg);
border: 2px dashed var(--border-color);
display: flex;
align-items: center;
justify-content: center;
position: relative;
overflow: hidden;
margin-bottom: var(--spacing-lg);
.digital-human-box.fullscreen .fullscreen-exit-hint {
opacity: 1;
}
.digital-human-box.active {
border-style: solid;
border-color: var(--primary-color);
box-shadow: var(--shadow-glow);
}
.digital-human-box::before {
content: '';
/* ===== SDK 响应式缩放 ===== */
.digital-human-box .sdk-scaling-wrapper {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background:
radial-gradient(circle at 20% 80%, rgba(37, 99, 235, 0.1) 0%, transparent 50%),
radial-gradient(circle at 80% 20%, rgba(6, 182, 212, 0.1) 0%, transparent 50%);
pointer-events: none;
}
.placeholder {
text-align: center;
color: var(--text-muted);
}
.placeholder-icon {
font-size: 4rem;
display: block;
margin-bottom: var(--spacing-md);
opacity: 0.5;
}
/* ===== 控制面板 ===== */
.control-panel {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
gap: var(--spacing-lg);
}
.control-group h3 {
font-size: 0.875rem;
font-weight: 600;
color: var(--text-secondary);
margin-bottom: var(--spacing-sm);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.button-row {
display: flex;
gap: var(--spacing-sm);
flex-wrap: wrap;
}
.button-row .btn {
flex: 1;
min-width: 100px;
}
.text-input-row {
display: flex;
gap: var(--spacing-sm);
}
.text-input-row input {
flex: 1;
padding: var(--spacing-sm) var(--spacing-md);
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
color: var(--text-primary);
font-size: 0.875rem;
}
.text-input-row input:focus {
outline: none;
border-color: var(--primary-color);
}
.text-input-row input:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* ===== 日志面板 ===== */
.log-panel {
display: flex;
flex-direction: column;
max-height: calc(100vh - 200px);
}
.log-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-md);
justify-content: center;
overflow: hidden;
z-index: 1;
}
.log-toolbar select {
padding: var(--spacing-xs) var(--spacing-sm);
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
color: var(--text-primary);
font-size: 0.875rem;
.digital-human-box .sdk-scaling-wrapper .sdk-inner {
transform-origin: center center;
width: 100%;
height: 100%;
}
.log-container {
flex: 1;
overflow-y: auto;
background: var(--bg-primary);
border-radius: var(--radius-md);
padding: var(--spacing-md);
font-family: 'Consolas', 'Monaco', monospace;
font-size: 0.8125rem;
line-height: 1.5;
.digital-human-box .sdk-scaling-wrapper .sdk-inner .digital-intelligent-interaction {
width: 100% !important;
height: 100% !important;
}
.log-entry {
padding: var(--spacing-xs) 0;
border-bottom: 1px solid var(--bg-tertiary);
display: flex;
gap: var(--spacing-sm);
/* ===== 覆盖 SDK 字体大小 ===== */
.digital-intelligent-interaction {
font-size: 14px !important;
}
.log-entry:last-child {
border-bottom: none;
.digital-intelligent-interaction .ai-chat-btn .ai-chat-btn-content {
font-size: 16px !important;
}
.log-time {
color: var(--text-muted);
white-space: nowrap;
.digital-intelligent-interaction .message-item .rich-text-card .content {
font-size: 16px !important;
line-height: 24px !important;
}
.log-level {
font-weight: 600;
min-width: 50px;
.digital-intelligent-interaction .stop-chat-btn.normal .stop-chat-btn-content {
font-size: 16px !important;
}
.log-level.debug {
color: var(--text-muted);
}
.log-level.info {
color: var(--info-color);
}
.log-level.warn {
color: var(--warning-color);
}
.log-level.error {
color: var(--danger-color);
}
.log-message {
color: var(--text-primary);
word-break: break-all;
}
/* ===== 底部 ===== */
.footer {
background: var(--bg-secondary);
border-top: 1px solid var(--border-color);
padding: var(--spacing-md) var(--spacing-xl);
text-align: center;
color: var(--text-muted);
font-size: 0.875rem;
}
/* ===== 响应式设计 ===== */
@media (max-width: 1200px) {
.main-content {
grid-template-columns: 1fr;
max-width: 800px;
}
.digital-human-box {
min-height: 300px;
}
.log-panel {
max-height: 400px;
}
}
@media (max-width: 640px) {
.header {
flex-direction: column;
gap: var(--spacing-md);
text-align: center;
}
.main-content {
padding: var(--spacing-md);
}
.form-actions {
flex-direction: column;
}
.button-row {
flex-direction: column;
}
.text-input-row {
flex-direction: column;
}
.digital-intelligent-interaction .toast-container .toast-content .toast-text {
font-size: 12px !important;
}
/* ===== 滚动条样式 ===== */
@@ -647,7 +693,3 @@ section h2 {
transform: translateY(0);
}
}
.log-entry {
animation: slideIn 0.2s ease-out;
}