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
+8 -8
View File
@@ -1,6 +1,6 @@
import { useState, useRef, useEffect, useCallback } from 'react';
import Icon from './Icons';
import { API_BASE } from '../config';
import { getApiBase as API_BASE } from '../config';
import { getMqttStatus } from '../utils/mqtt';
import { renderMarkdown } from '../voice/markdown.jsx';
@@ -19,12 +19,12 @@ const QUICK_QUESTIONS = [
'切换到数字孪生页面',
];
/* 离线兜底回复(后端不可用时) */
/* 离线兜底回复(后端不可用时 · 口径按《2026年7月统计表》/《附件1省级材料》/《宣传册》 */
const FALLBACK_REPLIES = [
'园区提供「空间 + 孵化 + 融资 + 政策 + 资源 + AI 赋能 + 综合服务」七位一体服务,在园项目 158 家,累计带动就业 2,186 人,累计营收 2.08 亿元。',
'入驻流程:提交申请 → 项目评审 → 签订协议 → 安排场地并对接导师服务。在园项目 158 家,累计孵化 208 余家。',
'园区政策覆盖场地租金减免、创业补贴、税费优惠与高企认定申报辅导(在园高企 32 家)。',
'园区按成长阶段分三个区域:加速区(A1–A12)、国际区(I1–I7)、成长区(G1–G8),共 39 家入驻企业。',
'园区提供「空间 + 孵化 + 融资 + 政策 + 资源 + AI 赋能 + 综合服务」七位一体服务。按官方统计(2026年7月):实际入驻企业 38 家,当年累计带动就业 213 人,当年生产经营总额 252.15 万元。',
'入驻流程(按《入驻管理规程》):提出申请(入驻申请表、创业计划书、身份证、学历证明等材料)→ 项目初审 → 入驻评审(导师库抽 3 名专家、路演答辩、100 分制 80 分通过)→ 评审公示(市人社局官网)→ 签订《入驻孵化协议》、安排场地工位。',
'园区政策服务覆盖:免费办公空间、孵化辅导、融资对接、政策申报、资源整合、AI 专项培训、工商注册与法律咨询等一站式服务;每年原则上组织两次入驻。',
'园区按发展阶段设四大孵化区域(《宣传册》):OPC创业空间(AI轻资产、最长2年)、创业苗圃区(未注册团队、最长2年)、孵化加速区(已注册企业、基础2年可延1年)、国际创客区(留学生、参照加速区)。',
];
const pick = (arr) => arr[Math.floor(Math.random() * arr.length)];
@@ -118,7 +118,7 @@ export default function AiChatPanel() {
.filter((m) => m.role === 'me' || (m.role === 'ai' && !/我是「问问小园」/.test(m.text)))
.map((m) => ({ role: m.role === 'me' ? 'user' : 'assistant', content: m.text }));
history.push({ role: 'user', content: text });
const res = await fetch(`${API_BASE}/api/ai/chat`, {
const res = await fetch(`${API_BASE()}/api/ai/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ messages: history }),
@@ -183,7 +183,7 @@ export default function AiChatPanel() {
const form = new FormData();
form.append('file', blob, `recording.${fmt === 'mp4' ? 'm4a' : fmt}`);
form.append('format', fmt === 'mp4' ? 'm4a' : fmt);
const res = await fetch(`${API_BASE}/api/ai/asr`, { method: 'POST', body: form });
const res = await fetch(`${API_BASE()}/api/ai/asr`, { method: 'POST', body: form });
const data = await res.json();
const text = (data && data.text || '').trim();
if (text) {
+4 -4
View File
@@ -1,6 +1,6 @@
import { useState, useEffect, useCallback, useRef } from 'react';
import Icon from './Icons';
import { API_BASE } from '../config';
import { getApiBase as API_BASE } from '../config';
/* =========================================================
DpmOverlays —— 后端/AI 指令驱动的全局覆盖层
@@ -16,17 +16,17 @@ function CardContent({ card }) {
useEffect(() => {
let alive = true;
if (type === 'companies') {
fetch(`${API_BASE}/api/park/companies`)
fetch(`${API_BASE()}/api/park/companies`)
.then((r) => r.json())
.then((d) => alive && setData(d.companies || []))
.catch(() => alive && setData([]));
} else if (type === 'zones') {
fetch(`${API_BASE}/api/park/zones`)
fetch(`${API_BASE()}/api/park/zones`)
.then((r) => r.json())
.then((d) => alive && setData(d.zones || []))
.catch(() => alive && setData([]));
} else if (type === 'overview') {
fetch(`${API_BASE}/api/dashboard/snapshot`)
fetch(`${API_BASE()}/api/dashboard/snapshot`)
.then((r) => r.json())
.then((d) => alive && setData(d))
.catch(() => alive && setData(null));
+2 -2
View File
@@ -1,6 +1,6 @@
import { useState, useEffect } from 'react';
import Icon from './Icons';
import { API_BASE } from '../config';
import { getApiBase as API_BASE } from '../config';
/* =========================================================
AI 助手 · 预设问题面板(右侧)
@@ -17,7 +17,7 @@ export default function PromptPanel({ onSend }) {
let cancelled = false;
(async () => {
try {
const res = await fetch(`${API_BASE}/api/ai/questions`, { cache: 'no-store' });
const res = await fetch(`${API_BASE()}/api/ai/questions`, { cache: 'no-store' });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
if (!cancelled) {
+34 -8
View File
@@ -1,20 +1,46 @@
/* =========================================================
运行配置 —— 后端地址 / MQTT Broker
优先级:window.__DPM_*__ 全局变量 > Vite 环境变量(.env.local> 内置默认
部署时在项目根目录 .env.local 配置(已 gitignore):
VITE_API_BASE=http://192.168.x.x:18003
VITE_MQTT_URL=ws://192.168.x.x:8083/mqtt
VITE_MQTT_USERNAME=...
VITE_MQTT_PASSWORD=...
优先级:window.__DPM_*__ 全局变量(运行时可被 /api/config 引导覆盖)
> Vite 环境变量(.env.local,构建时)> 内置默认
内置默认(打包部署):Tauri 内 WebView 的 hostname 为 tauri.localhost
后端与 MQTT Broker 若与展播程序同机部署,回退到 127.0.0.1;
浏览器开发/局域网访问时回退到当前主机 hostname。
========================================================= */
const env = import.meta.env || {};
export const API_BASE = window.__DPM_API__ || env.VITE_API_BASE || 'http://192.168.1.9:10085';
export const MQTT_URL = window.__DPM_MQTT__ || env.VITE_MQTT_URL || 'ws://192.168.1.3:8083/mqtt';
/* 运行期解析(惰性读取 window.__DPM_*__,供 main.jsx bootstrap 覆盖后再生效) */
function resolveHost() {
const h = window.location.hostname || '127.0.0.1';
// Tauri 打包 WebViewtauri.localhost / localhost)→ 服务同机部署用 127.0.0.1
if (h === 'tauri.localhost' || h === 'localhost' || h === '127.0.0.1' || h.includes('tauri')) {
return '127.0.0.1';
}
return h; // 浏览器开发/局域网:当前主机 IP
}
export const API_BASE = window.__DPM_API__ || env.VITE_API_BASE || `http://${resolveHost()}:10085`;
export const MQTT_URL = window.__DPM_MQTT__ || env.VITE_MQTT_URL || `ws://${resolveHost()}:8083/mqtt`;
export const MQTT_USERNAME = window.__DPM_MQTT_USER__ || env.VITE_MQTT_USERNAME || '';
export const MQTT_PASSWORD = window.__DPM_MQTT_PASS__ || env.VITE_MQTT_PASSWORD || '';
/** 惰性读取 API 基址(允许 bootstrap 从后端 /api/config 覆盖后再生效) */
export function getApiBase() {
return window.__DPM_API__ || API_BASE;
}
/** 惰性读取 MQTT 地址(允许 bootstrap 覆盖后再生效) */
export function getMqttUrl() {
return window.__DPM_MQTT__ || MQTT_URL;
}
export function getMqttCredentials() {
return {
username: window.__DPM_MQTT_USER__ || MQTT_USERNAME,
password: window.__DPM_MQTT_PASS__ || MQTT_PASSWORD,
};
}
export const MQTT_TOPIC_COMMAND = 'opc/display/command';
export const MQTT_TOPIC_TICK = 'opc/dashboard/tick';
export const MQTT_TOPIC_HEARTBEAT = 'opc/display/heartbeat';
+33 -7
View File
@@ -2,12 +2,38 @@ import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import { ThemeProvider } from "@appica/ui-react/providers/theme-provider";
import { API_BASE } from "./config";
import "./styles/global.css";
ReactDOM.createRoot(document.getElementById("root")).render(
<React.StrictMode>
<ThemeProvider>
<App />
</ThemeProvider>
</React.StrictMode>,
);
/* =========================================================
启动引导(打包部署关键):
渲染前先从后端 GET /api/config 拉取运行时配置
(MQTT 地址/账号、API 基址),写入 window.__DPM_*__
之后 api.js / mqtt.js 均以惰性读取方式生效。
后端不可达时静默使用内置默认(Tauri 打包回退 127.0.0.1)。
========================================================= */
async function bootstrapRuntimeConfig() {
try {
const res = await fetch(`${API_BASE()}/api/config`, { cache: 'no-store' });
if (!res.ok) return;
const cfg = await res.json();
// 仅覆盖 MQTT 连接信息(后端 .env 为准);API 基址以前端实际可达地址为准
if (cfg.mqtt_url) window.__DPM_MQTT__ = cfg.mqtt_url;
if (cfg.mqtt_username) window.__DPM_MQTT_USER__ = cfg.mqtt_username;
if (cfg.mqtt_password) window.__DPM_MQTT_PASS__ = cfg.mqtt_password;
// eslint-disable-next-line no-console
if (cfg.mqtt_url) console.info(`[bootstrap] MQTT -> ${cfg.mqtt_url}`);
} catch {
/* 后端不可达:使用内置默认地址 */
}
}
bootstrapRuntimeConfig().then(() => {
ReactDOM.createRoot(document.getElementById("root")).render(
<React.StrictMode>
<ThemeProvider>
<App />
</ThemeProvider>
</React.StrictMode>,
);
});
+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({
+12 -10
View File
@@ -1,10 +1,12 @@
// API 基地址 —— 独立 FastAPI 后端(配置见 src/config.js,可用 window.__DPM_API__ 覆盖)
import { API_BASE as BASE } from '../config';
import { getApiBase } from '../config';
export const API_BASE = BASE;
/* 惰性读取 API 基址:允许 main.jsx 引导阶段从后端 /api/config 覆盖(打包部署时生效) */
export const API_BASE = getApiBase();
const base = () => getApiBase();
async function request(url, options = {}) {
const res = await fetch(`${BASE}${url}`, {
const res = await fetch(`${base()}${url}`, {
headers: { 'Content-Type': 'application/json' },
...options,
});
@@ -19,7 +21,7 @@ function formEncode(data) {
// ============ Auth ============
export async function login(username, password) {
const res = await fetch(`${BASE}/api/login`, {
const res = await fetch(`${base()}/api/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: formEncode({ username, password }),
@@ -29,7 +31,7 @@ export async function login(username, password) {
// ============ Settings ============
export async function getSettings() {
const res = await fetch(`${BASE}/api/settings`);
const res = await fetch(`${base()}/api/settings`);
return res.json();
}
@@ -42,7 +44,7 @@ export async function updateSettings(data) {
// ============ Media Files ============
export async function getMediaFiles() {
const res = await fetch(`${BASE}/media`);
const res = await fetch(`${base()}/media`);
return res.json();
}
@@ -50,7 +52,7 @@ export async function uploadFiles(files) {
for (const file of files) {
const fd = new FormData();
fd.append('file', file);
const res = await fetch(`${BASE}/upload`, { method: 'POST', body: fd });
const res = await fetch(`${base()}/upload`, { method: 'POST', body: fd });
if (!res.ok) throw new Error(`上传失败 (${res.status})`);
}
}
@@ -60,7 +62,7 @@ export async function uploadFilesWithProgress(files, onProgress) {
const file = files[i];
await new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('POST', `${BASE}/upload`);
xhr.open('POST', `${base()}/upload`);
xhr.upload.onprogress = (e) => {
if (e.lengthComputable) {
@@ -109,7 +111,7 @@ export async function deleteMedia(path) {
// ============ Playlist ============
export async function getPlaylist() {
const res = await fetch(`${BASE}/api/playlist`);
const res = await fetch(`${base()}/api/playlist`);
return res.json();
}
@@ -136,7 +138,7 @@ export async function sendControl(action) {
}
export async function getState() {
const res = await fetch(`${BASE}/api/state`);
const res = await fetch(`${base()}/api/state`);
return res.json();
}
+19 -6
View File
@@ -1,6 +1,6 @@
import { useState, useEffect } from 'react';
import mqtt from 'mqtt';
import { MQTT_URL, MQTT_TOPIC_COMMAND, MQTT_TOPIC_TICK, MQTT_USERNAME, MQTT_PASSWORD } from '../config';
import { getMqttUrl, getMqttCredentials, MQTT_TOPIC_COMMAND, MQTT_TOPIC_TICK } from '../config';
/* =========================================================
MQTT 客户端(mqtt.js)—— 后端控制通道
@@ -17,18 +17,31 @@ function notifyStatus() {
window.dispatchEvent(new CustomEvent('dpm:mqtt-status', { detail: { status } }));
}
/* 每个连接生成全局唯一 client_id(独立客户端 ID 铁律):
优先 crypto.randomUUIDWebView2/现代浏览器),回退时间戳+双段高熵随机 */
function makeClientId() {
try {
if (window.crypto && typeof window.crypto.randomUUID === 'function') {
return `dpm-screen-${crypto.randomUUID().replace(/-/g, '')}`;
}
} catch { /* 继续回退 */ }
return `dpm-screen-${Date.now().toString(36)}-${Math.random().toString(16).slice(2, 12)}-${Math.random().toString(16).slice(2, 10)}`;
}
export function connectMqtt() {
if (client) return;
try {
// 每个页面会话生成唯一 client_id(时间戳 + 高熵随机),避免多屏/多实例竞争
clientId = `dpm-screen-${Date.now().toString(36)}-${Math.random().toString(16).slice(2, 12)}`;
client = mqtt.connect(MQTT_URL, {
// 独立客户端 ID:避免多屏/多实例/多标签页同 ID 被 broker 互相踢下线
clientId = makeClientId();
// 连接时惰性读取 MQTT 地址/账号:打包部署时 main.jsx 引导已从后端 /api/config 覆盖
const { username, password } = getMqttCredentials();
client = mqtt.connect(getMqttUrl(), {
reconnectPeriod: 3000,
connectTimeout: 8000,
clientId,
clean: true,
username: MQTT_USERNAME || undefined,
password: MQTT_PASSWORD || undefined,
username: username || undefined,
password: password || undefined,
});
client.on('connect', () => {
status = 'connected';
+13 -13
View File
@@ -1,5 +1,5 @@
import { useState, useEffect } from 'react';
import { API_BASE } from '../config';
import { getApiBase as API_BASE } from '../config';
/* =========================================================
园区数据引擎(共享)—— 供数据大屏 / 媒体播放页复用
@@ -15,19 +15,19 @@ import { API_BASE } from '../config';
export const fmtInt = (n) => Math.round(n).toLocaleString('zh-CN');
export const fmtWan = (n) => (n >= 10000 ? (n / 10000).toFixed(2) + '亿' : n >= 100 ? n.toFixed(1) + '万' : n.toFixed(1));
/* ---------- 真实锚点(仅来自 xls 统计表 + 宣传册) ---------- */
/* ---------- 真实锚点(官方统计《2026年7月运行情况统计表》+ 附件1省级材料 + 宣传册) ---------- */
const REAL = {
tokenMonthlyYi: 200, // 月均 200 亿 tokens真实业务锚点
tokenDailyWan: 66666.7, // 日均 6.67 亿 tokens
tokenRate: 7700, // 实时 t/s
toolsPerCallTokens: 150000, // 平均每次工具调用消耗 tokens(真实测算:12.5M输入 ÷ 81步 ≈ 15.4万
occupied: 39, // 实际入驻企业数(信息表名单 39 家,统一口径
tokenMonthlyYi: 200, // 月均 200 亿 tokensAI 平台业务锚点,非政府统计口径
tokenDailyWan: 66666.7, // 日均 6.67 亿 tokensAI 平台锚点)
tokenRate: 7700, // 实时 t/sAI 平台锚点)
toolsPerCallTokens: 150000, // 平均每次工具调用消耗 tokens(AI 平台测算
occupied: 38, // 实际入驻企业数(官方口径:《2026年7月运行情况统计表》38 家
capacity: 49, // 可容纳企业个数(统计表)
invested: 560, // 累计投入运营资金(万元)
jobs: 213, // 累计带动就业(统计表)
revenueTotal: 252.15, // 累计生产经营总额(万元)
revenueTax: 1.03, // 累计上缴税利(万元)
area: 3000, // 建筑面积(㎡)
invested: 560, // 开园以来累计投入运营资金(万元,统计表
jobs: 213, // 带动就业人数(统计表,当年累计口径
revenueTotal: 252.15, // 生产经营总额(万元,统计表,当年累计口径
revenueTax: 1.03, // 上缴税利总额(万元,统计表,当年累计口径
area: 3000, // 建筑面积(㎡,统计表+宣传册
founded: 2009, // 成立年份(宣传册)
provinceLevel: 2012, // 省级创业园认定年份(宣传册)
address: '昆明市民航路229号(人力资源中心1-2楼)',
@@ -212,7 +212,7 @@ export function useParkSim() {
let cancelled = false;
const load = async () => {
try {
const res = await fetch(`${API_BASE}/api/dashboard/snapshot`, { cache: 'no-store' });
const res = await fetch(`${API_BASE()}/api/dashboard/snapshot`, { cache: 'no-store' });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
if (!cancelled && data && data.token) setSnap(data);
+2 -2
View File
@@ -7,7 +7,7 @@
- 帧经局域网传到后端(大屏机同机部署则不出设备);画面预览仍本地
========================================================= */
import { useCallback, useEffect, useRef, useState } from 'react';
import { API_BASE } from '../config';
import { getApiBase as API_BASE } from '../config';
export const VISION_DWELL_MS = 10000; // 面向停留阈值
export const VISION_SILENT_MS = 180000; // 触发(自动问候)后静默 3 分钟,期间不重复触发
@@ -107,7 +107,7 @@ export default function useVisionDetection({ onTrigger, onGesture, enabled = tru
// JPEG base64 → 后端 YOLO
const b64 = canvasRef.current.toDataURL('image/jpeg', 0.6).split(',')[1];
setModelPhase('ok');
const res = await fetch(`${API_BASE}/api/vision/frame`, {
const res = await fetch(`${API_BASE()}/api/vision/frame`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ image: b64 }),