feat: 添加后端部署和API基本检索的Docker Compose配置

- 引入新的`docker-compose.yml`文件,便于后端和MQTT代理(EMQX)的部署。
- 更新多个组件中的API基本检索,使用函数进行动态解析。
- 加强了多个组件中API调用的错误处理和日志记录。
- 优化了AI聊天面板离线场景的回退响应。
- 更新DataScreen和MediaScreen组件中的数据显示和统计,以反映准确的指标。
- 重构MQTT连接逻辑,以支持动态凭证和客户端ID。
This commit is contained in:
Pine
2026-08-18 06:50:50 +08:00
parent fbf2eccad2
commit 18d28be943
23 changed files with 621 additions and 142 deletions
+10 -7
View File
@@ -8,15 +8,18 @@ import { useParkSim } from '../utils/parkData';
昆明市大学生创业园 · OPC 智能园区数字运营中心
—— 数据大屏(真实数据展示)
---------------------------------------------------------
数据口径(用户指定,仅两个权威来源):
· 《昆明市大学生创业园运行情况统计表(2026年7月).xls》
实际入驻 39 家 / 可容纳 49 个 / 累计投入 560 万 /
带动就业 213 人 / 累计生产经营总额 252.15 万 / 累计税利 1.03 万 / 面积 3000㎡
数据口径(标准资料,多个权威来源):
· 《昆明市大学生创业园运行情况统计表(2026年7月).xls》(官方统计)
实际入驻 38 家 / 可容纳 49 个 / 累计投入 560 万 /
当年累计带动就业 213 人 / 当年生产经营总额 252.15 万 / 当年税利 1.03 万 / 面积 3000㎡
· 《附件1.省级创业孵化载体相关情况简介材料》(省级绩效累计口径,截至2026年5月)
累计孵化 181 家 / 在孵 31 家 / 累计就业 1,529 人 / 累计经营收入 21,911.14 万
· 《昆明市创业园宣传册.pdf》
2009 年成立 / 2012 年省级创业园 / 民航路 229 号 / 四大孵化区域 / 28 家企业名录
测算口径(用户确认):
2009 年成立 / 2012 年省级创业园 / 民航路 229 号 / 四大孵化区域
· 《入驻企业信息表》39 家名录(含创始人/简介/场地安排)
测算口径(AI 平台业务锚点,非政府统计口径):
· TOKEN:月均 200 亿 tokens → 日均 ≈ 6.67 亿,实时 ≈ 7,700 t/s,近30日累计 200 亿
· 工具调用:按每次调用约 2 万 tokens 折算(今日 ≈ 3.3 万次)
· 工具调用:按每次调用约 15 万 tokens 折算
TOKEN/工具为基于真实锚点的测算演示,其余为真实统计值
========================================================= */
+62 -13
View File
@@ -1,6 +1,7 @@
import { useState, useEffect, useRef, useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import * as api from '../utils/api';
import { getApiBase } from '../config';
import { API_BASE } from '../utils/api';
import { getCurrentWindow } from '@tauri-apps/api/window';
import { enable, disable } from '@tauri-apps/plugin-autostart';
@@ -58,7 +59,8 @@ export default function MediaScreen() {
useEffect(() => { soundBlockedRef.current = soundBlocked; }, [soundBlocked]);
const getUrl = useCallback((item) => {
return item.source === 'url' ? item.relative_path : `${API_BASE}/file/${item.relative_path}`;
// 惰性读取 API 基址:打包部署引导覆盖后媒体文件地址同步生效
return item.source === 'url' ? item.relative_path : `${getApiBase()}/file/${item.relative_path}`;
}, []);
// ============ 核心播放控制 ============
@@ -106,14 +108,19 @@ export default function MediaScreen() {
const url = getUrl(item);
video.src = url;
video.load();
// 打包部署:WebView2 已加 --autoplay-policy=no-user-gesture-required
// 这里仍先尝试带声音播放;仅真实自动播放策略拦截(NotAllowedError)才进入静音待解锁
video.muted = soundBlocked;
video.volume = volume;
const p = video.play();
if (p !== undefined) {
p.catch(() => {
setSoundBlocked(true);
video.muted = true;
video.play().catch(() => {});
p.catch((err) => {
if (err && (err.name === 'NotAllowedError' || /autoplay|gesture/i.test(String(err.message || '')))) {
setSoundBlocked(true);
video.muted = true;
video.play().catch(() => {});
}
// 其他错误(网络/解码等)不强制静音,等重试
});
}
} else {
@@ -132,15 +139,54 @@ export default function MediaScreen() {
if (!item || item.type !== 'video') return;
const video = videoRef.current;
if (video && video.paused && video.src) {
video.play().catch(() => {
setSoundBlocked(true);
video.muted = true;
video.play().catch(() => {});
video.play().catch((err) => {
if (err && (err.name === 'NotAllowedError' || /autoplay|gesture/i.test(String(err.message || '')))) {
setSoundBlocked(true);
video.muted = true;
video.play().catch(() => {});
}
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [paused]);
// ============ 声音自动恢复(打包部署无需人工点击) ============
// 1) 视频真正开始播放(playing)→ 解除静音标记
useEffect(() => {
const video = videoRef.current;
if (!video) return;
const onPlaying = () => {
setSoundBlocked(false);
if (video.muted && !soundBlockedRef.current) {
video.muted = false;
}
};
video.addEventListener('playing', onPlaying);
return () => video.removeEventListener('playing', onPlaying);
}, [mediaKey, loaded]);
// 2) 若因自动播放策略被静音,每 3 秒自动尝试解静音播放(无需用户点击)
useEffect(() => {
if (!loaded) return undefined;
const iv = setInterval(() => {
if (!soundBlockedRef.current) return;
const video = videoRef.current;
if (!video || !video.src) return;
video.muted = false;
const p = video.play();
if (p !== undefined) {
p.then(() => {
setSoundBlocked(false);
const hint = document.getElementById('soundHint');
if (hint) hint.style.display = 'none';
}).catch(() => {
video.muted = true;
});
}
}, 3000);
return () => clearInterval(iv);
}, [loaded]);
// ============ 事件监听 ============
useEffect(() => {
const video = videoRef.current;
@@ -176,9 +222,12 @@ export default function MediaScreen() {
testPlay.then(() => {
setSoundBlocked(false);
video.pause();
}).catch(() => {
setSoundBlocked(true);
video.muted = true;
}).catch((err) => {
if (err && (err.name === 'NotAllowedError' || /autoplay|gesture/i.test(String(err.message || '')))) {
setSoundBlocked(true);
video.muted = true;
}
// 其他错误不强制静音(自动重试由 3s 定时器接管)
});
}
}
@@ -188,7 +237,7 @@ export default function MediaScreen() {
}, []);
useEffect(() => {
const es = new EventSource(`${API_BASE}/api/events`);
const es = new EventSource(`${getApiBase()}/api/events`);
es.onmessage = (e) => {
try {
const msg = JSON.parse(e.data);
+5 -5
View File
@@ -8,7 +8,7 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { S2sWsRealtimeClient } from '../voice/s2s-ws-client.js';
import useVisionDetection, { VISION_DWELL_MS } from '../voice/useVisionDetection.js';
import { API_BASE } from '../config';
import { getApiBase as API_BASE } from '../config';
import '../styles/datascreen.css';
import './voice-original.css';
import './voice-overrides.css';
@@ -222,7 +222,7 @@ export default function VoiceAssistant() {
pushMessage('assistant', `🔧 调用工具:${name}`, false);
let output;
try {
const res = await fetch(`${API_BASE}/api/tools/exec`, {
const res = await fetch(`${API_BASE()}/api/tools/exec`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, args }),
@@ -303,7 +303,7 @@ export default function VoiceAssistant() {
// 基础提示词:从后端拉取(配置 + 园区知识库已由服务端组装);失败用极简兜底
let instructions = FALLBACK_INSTRUCTIONS;
try {
const insRes = await fetch(`${API_BASE}/api/s2s/instructions`, { cache: 'no-store' });
const insRes = await fetch(`${API_BASE()}/api/s2s/instructions`, { cache: 'no-store' });
const insData = await insRes.json();
if (insData?.ok && insData.instructions) instructions = insData.instructions;
} catch { /* 后端不可用不影响对话 */ }
@@ -452,7 +452,7 @@ export default function VoiceAssistant() {
} else {
void start(); // start 连接成功后检查 pendingGreetingRef 再发问候
}
fetch(`${API_BASE}/api/vision/event`, {
fetch(`${API_BASE()}/api/vision/event`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ event: 'triggered', faces: vision.faces, dwell_ms: VISION_DWELL_MS }),
@@ -466,7 +466,7 @@ export default function VoiceAssistant() {
// 摄像头识别状态变化 → 上报后端(详细日志定位)
useEffect(() => {
if (!vision.status || vision.status === 'off' || vision.status === 'loading') return;
fetch(`${API_BASE}/api/vision/event`, {
fetch(`${API_BASE()}/api/vision/event`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({