feat(前端): 重构数字人交互界面并添加全屏功能
重构前端界面布局,将控制面板改为浮动式设计,添加全屏显示功能。主要变更包括: 1. 重新设计界面布局,使用浮动面板替代固定面板 2. 实现数字人显示区域的全屏功能 3. 优化SDK容器缩放逻辑 4. 添加面板切换和状态管理功能 5. 更新样式以适应新的交互设计
This commit is contained in:
28
frontend/cert/localhost-key.pem
Normal file
28
frontend/cert/localhost-key.pem
Normal 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-----
|
||||
17
frontend/cert/localhost.pem
Normal file
17
frontend/cert/localhost.pem
Normal 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-----
|
||||
@@ -5,28 +5,41 @@
|
||||
<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 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 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">
|
||||
<!-- 浮动控制栏 -->
|
||||
<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">
|
||||
@@ -39,37 +52,32 @@
|
||||
</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>
|
||||
<button id="getOnceCodeBtn" class="btn btn-secondary">🔑 获取</button>
|
||||
</div>
|
||||
<small class="field-hint">有效期5分钟,只能使用一次</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-actions main-actions">
|
||||
<div class="form-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 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">
|
||||
@@ -111,18 +119,19 @@
|
||||
<span class="icon">⚡</span> 打断说话
|
||||
</button>
|
||||
<button id="toggleAudioBtn" class="btn btn-secondary" disabled>
|
||||
<span class="icon">🔊</span> 静音/取消静音
|
||||
<span class="icon">🔊</span> 静音
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- 日志面板 -->
|
||||
<section class="log-panel">
|
||||
<!-- 浮动面板:事件日志 -->
|
||||
<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">清空日志</button>
|
||||
<button id="clearLogBtn" class="btn btn-small btn-secondary">清空</button>
|
||||
<select id="logLevel">
|
||||
<option value="debug">Debug</option>
|
||||
<option value="info" selected>Info</option>
|
||||
@@ -130,17 +139,19 @@
|
||||
<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>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<!-- 底部 -->
|
||||
<footer class="footer">
|
||||
<p>Powered by HwICSUiSdk | 华为交互数字人解决方案</p>
|
||||
</footer>
|
||||
</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
18
frontend/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
68
frontend/server.js
Normal file
68
frontend/server.js
Normal 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);
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user