feat: 语音气泡浅蓝 + 页眉图标展示全部视觉状态 + 简化自动问候

- /voice 页右下角气泡文本改浅蓝色(最新 #4cc2ff / 历史 #6fb5ea)
- GlobalVision 广播 dpm:vision-status(status/cameraOn/gesture/faces)
- 页眉图标四态:默认灰(探索) → 识别中浅蓝(摄像头开/检测/面向) → 手势绿(惊喜) → 异常红
- 自动调用语音助手问候语简化为「您好呀,需要我的帮助吗?」
This commit is contained in:
Pine
2026-08-18 06:24:26 +08:00
parent 75498027f3
commit fbf2eccad2
5 changed files with 49 additions and 7 deletions
+12
View File
@@ -31,6 +31,18 @@ export default function GlobalVision() {
window.dispatchEvent(new CustomEvent('dpm:hand-track', { detail: { hands: vision.hands } }));
}, [vision.hands]);
// 视觉识别状态广播:dpm:vision-status(页眉图标展示全部视觉相关状态)
useEffect(() => {
window.dispatchEvent(new CustomEvent('dpm:vision-status', {
detail: {
status: vision.status, // off|loading|detecting|facing|triggered|silent|error
cameraOn: vision.cameraOn,
gesture: vision.gesture, // raise 等最近一次手势
faces: vision.faces,
},
}));
}, [vision.status, vision.cameraOn, vision.gesture, vision.faces]);
return (
<>
{/* 摄像头驱动(隐藏,仅供全局抽帧识别) */}
+15 -3
View File
@@ -24,6 +24,7 @@ export default function PageHeader() {
const [now, setNow] = useState(new Date());
const [aboutOpen, setAboutOpen] = useState(false);
const [gestureHit, setGestureHit] = useState(false); // 识别到手势 → 惊喜图标 3s
const [vision, setVision] = useState({ status: 'off', cameraOn: false, gesture: null }); // 视觉识别状态
const pageIdx = usePageIndex();
const gestureTimer = useRef(null);
@@ -32,7 +33,7 @@ export default function PageHeader() {
return () => clearInterval(iv);
}, []);
// 全局手势事件 → 右上角状态图标(默认灰色探索 → 识别到手势变绿色惊喜)
// 全局手势事件 → 惊喜图标 3s
useEffect(() => {
const onGesture = (e) => {
if (!e.detail || !e.detail.type) return;
@@ -47,6 +48,17 @@ export default function PageHeader() {
};
}, []);
// 视觉识别状态(GlobalVision 广播):off/loading/detecting/facing/triggered/silent/error
useEffect(() => {
const onStatus = (e) => setVision(e.detail || {});
window.addEventListener('dpm:vision-status', onStatus);
return () => window.removeEventListener('dpm:vision-status', onStatus);
}, []);
// 图标状态:手势(绿) > 视觉识别中(浅蓝) > 默认(灰)
const visionActive = vision.cameraOn && vision.status !== 'error' && vision.status !== 'off';
const visionError = vision.status === 'error';
const dateStr = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
const week = ['日', '一', '二', '三', '四', '五', '六'][now.getDay()];
const timeStr = now.toLocaleTimeString('zh-CN', { hour12: false });
@@ -101,9 +113,9 @@ export default function PageHeader() {
{status}
</div>
<button
className={`bd-about-btn${gestureHit ? ' gesture-hit' : ''}`}
className={`bd-about-btn${gestureHit ? ' gesture-hit' : visionActive ? ' vision-active' : visionError ? ' vision-error' : ''}`}
type="button"
title={gestureHit ? '识别到手势' : '关于 PineSound'}
title={gestureHit ? '识别到手势' : visionActive ? `视觉识别中(${vision.status}` : visionError ? '视觉识别异常' : '关于 PineSound'}
aria-label="关于 PineSound"
onClick={() => setAboutOpen(true)}
>
+1 -1
View File
@@ -133,7 +133,7 @@ export default function VoiceAssistant() {
const live = LIVE_STATES.has(state);
// ── 自动问候生命周期状态(须在 start 之前定义:start 依赖数组引用 sendGreeting)──
const GREETING_TEXT = '你好!欢迎来到园区,我是智能语音助手,请问有什么可以帮你的吗?';
const GREETING_TEXT = '您好呀,需要我的帮助吗?';
const GREETING_WAIT_MS = 25000; // 问候播完后等待用户回答的超时
const voiceStateRef = useRef(state);
voiceStateRef.current = state;
+3 -3
View File
@@ -159,7 +159,7 @@
/* ============ 语音气泡:持续展示 · 最新大字 · 历史小字 ============ */
.bubble {
color: #1d2c44; /* 浅色背景适配(原版深色文字在浅色下不可见 */
color: #4cc2ff; /* 浅蓝色文本(视觉识别对话风格 */
}
.bubble .bubble-role {
font-size: 10px;
@@ -172,13 +172,13 @@
font-size: 21px;
font-weight: 600;
line-height: 1.45;
color: #1d2c44;
color: #4cc2ff;
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.6);
}
/* 历史消息:小字,向上排列 */
.bubble.history .bubble-body {
font-size: 12.5px;
line-height: 1.5;
color: #4a5b76;
color: #6fb5ea;
opacity: 0.88;
}
+18
View File
@@ -2599,6 +2599,24 @@
0%, 100% { box-shadow: 0 0 8px rgba(23, 178, 106, 0.3); transform: scale(1); }
50% { box-shadow: 0 0 18px rgba(23, 178, 106, 0.6); transform: scale(1.08); }
}
/* 视觉识别中:浅蓝色(摄像头开启 / 检测中 / 面向 / 触发) */
.bd-about-btn.vision-active {
color: var(--bd-cyan, #4cc2ff);
border-color: rgba(76, 194, 255, 0.5);
background: rgba(76, 194, 255, 0.1);
box-shadow: 0 0 10px rgba(76, 194, 255, 0.35);
animation: bdVisionPulse 2s ease-in-out infinite;
}
@keyframes bdVisionPulse {
0%, 100% { box-shadow: 0 0 6px rgba(76, 194, 255, 0.25); }
50% { box-shadow: 0 0 14px rgba(76, 194, 255, 0.5); }
}
/* 视觉识别异常:红色 */
.bd-about-btn.vision-error {
color: var(--bd-red, #f04438);
border-color: rgba(240, 68, 56, 0.5);
background: rgba(240, 68, 56, 0.08);
}
.about-overlay {
position: fixed;