feat(前端): 重构数字人交互界面并添加全屏功能
重构前端界面布局,将控制面板改为浮动式设计,添加全屏显示功能。主要变更包括: 1. 重新设计界面布局,使用浮动面板替代固定面板 2. 实现数字人显示区域的全屏功能 3. 优化SDK容器缩放逻辑 4. 添加面板切换和状态管理功能 5. 更新样式以适应新的交互设计
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user