- {[...COMPANY_NAMES, ...COMPANY_NAMES].map((c, i) => (
+ {[...d.companies, ...d.companies].map((c, i) => (
{c}
@@ -457,30 +402,23 @@ function ProjectsPanel({ d }) {
/* ---------- 带动就业(组视图 · 中密度) ---------- */
function JobsPanel({ d }) {
+ const mixTotal = d.industryMix.reduce((a, b) => a + b.value, 0);
return (
- 累计{fmtInt(d.jobs.total)} 人
- 毕业生1,680 人
- 实习506 人
- {/* 今日新增{d.jobs.todayNew} 人 */}
+ 累计带动就业{fmtInt(d.jobs.total)} 人
+ 统计时间2026年7月
+ 来源运行情况统计表
} />
-
行业就业分布
-
- {INDUSTRY.slice(0, 6).map((it, i) => {
- const max = Math.max(...INDUSTRY.map((x) => x.value));
- return (
-
-
-
{it.value}
-
{it.name}
-
- );
- })}
+
园区企业行业比例 ({mixTotal} 家)
+
+
+
累计带动就业{fmtInt(d.jobs.total)}人
+
累计生产经营总额{d.revenue.total}万元
+
累计上缴税利{d.revenue.tax}万元
+
可容纳企业{d.projects.capacity}个
);
@@ -490,22 +428,34 @@ function JobsPanel({ d }) {
function RevenuePanel({ d }) {
return (
-
- 累计营收{fmtWan(d.revenue.total)} 元
- 今日营收+{d.revenue.today} 万
- 企业估值5.2 亿元
- 环比+{d.revenue.growth}%
+ 累计生产经营总额{fmtWan(d.revenue.total)}元
+ 累计税利{fmtWan(d.revenue.tax)}元
+ 统计时间2026年7月
} />
-
-
+
+
累计生产经营总额{d.revenue.total}万元
+
累计上缴税利总额{d.revenue.tax}万元
+
带动就业{fmtInt(d.jobs.total)}人
+
实际入驻企业{d.projects.inPark}家
+
累计投入运营资金{d.projects.invested}万元
+
可容纳企业{d.projects.capacity}个
+
园区面积{d.park.area}㎡
+
成立时间{d.park.founded}年
- 大赛奖项 47 项
- 重点研发 18 项
- 高新技术企业 32 家
+ 免费办公空间 ≥20 ㎡
+ 一次性创业补贴 最高10万
+ 担保贷款贴息 最高50万
+ 科技计划经费 最高30万
+
+
+ 单位负责人 李云鹏
+ {/* 填表人 李禹贤
+ 填报时间 2026-07-31 */}
+ 报名电话 {d.park.phone}
);
@@ -568,17 +518,28 @@ function MiddleRotator({ d }) {
);
}
-/* ---------- 实时动态(纯列表,外层自行包裹卡片) ---------- */
-function LiveFeed({ feed, limit = 7 }) {
+/* ---------- 实时动态(3 列 × 2 行,每几秒整体轮换加载) ---------- */
+function LiveFeed({ feed, limit = 6 }) {
+ const [page, setPage] = useState(0);
+ const chunks = [];
+ for (let i = 0; i < feed.length; i += limit) chunks.push(feed.slice(i, i + limit));
+ useEffect(() => {
+ if (chunks.length <= 1) return;
+ const iv = setInterval(() => setPage((p) => (p + 1) % chunks.length), 5000);
+ return () => clearInterval(iv);
+ }, [chunks.length]);
+ const chunk = chunks[page % chunks.length] || [];
return (
- {feed.slice(0, limit).map((e) => (
-
-
- {e.text}
- {e.time}
-
- ))}
+
+ {chunk.map((e) => (
+
+
+ {e.text}
+ {e.time}
+
+ ))}
+
);
}
diff --git a/src/pages/VoiceAssistant.jsx b/src/pages/VoiceAssistant.jsx
index 9a4c305..419da1b 100644
--- a/src/pages/VoiceAssistant.jsx
+++ b/src/pages/VoiceAssistant.jsx
@@ -8,8 +8,6 @@
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 ParkOverviewStrip from '../components/ParkOverviewStrip';
-import { useParkSim } from '../utils/parkData';
import { API_BASE } from '../config';
import '../styles/datascreen.css';
import './voice-original.css';
@@ -97,14 +95,14 @@ const icons = { micOn:
{vision.status === 'error' && !vision.cameraOn && (
-
⚠️ {visionErrorText}
- )}
- {vision.status === 'facing' && (
-
- 检测到 {vision.faces} 人,正对摄像头 {(vision.dwellMs / 1000).toFixed(1)}s,停留 {VISION_DWELL_MS / 1000}s 后自动问候
-
+
⚠️ {visionErrorText}
)}
);
diff --git a/src/pages/voice-overrides.css b/src/pages/voice-overrides.css
index 272536e..d4ccd8b 100644
--- a/src/pages/voice-overrides.css
+++ b/src/pages/voice-overrides.css
@@ -57,14 +57,14 @@
.voice-cam-toggle:hover { border-color: #2f6bff; }
.voice-cam-toggle.on { background: #2f6bff; color: #fff; border-color: #2f6bff; }
-/* 底部左侧摄像头实时窗口(放大,实时画面) */
+/* 底部左侧摄像头实时窗口(缩小,页脚已移除贴底) */
.voice-cam-pip {
position: fixed;
left: 18px;
- bottom: 62px; /* 上移避开底部园区概览条(bd-strip 约 44px 高 + 间隙) */
+ bottom: 16px;
z-index: 50;
- width: 380px;
- border-radius: 14px;
+ width: 250px;
+ border-radius: 12px;
overflow: hidden;
border: 1px solid rgba(47, 107, 255, 0.45);
box-shadow: 0 10px 28px rgba(29, 79, 216, 0.22);
@@ -72,7 +72,7 @@
}
.voice-cam-pip video {
width: 100%;
- height: 216px;
+ height: 140px;
object-fit: cover;
display: block;
transform: scaleX(-1); /* 镜像自拍视角 */
@@ -82,8 +82,8 @@
align-items: center;
justify-content: space-between;
gap: 8px;
- padding: 7px 10px;
- font-size: 12px;
+ padding: 5px 9px;
+ font-size: 11px;
color: #e6e8eb;
background: rgba(0, 0, 0, 0.6);
}
@@ -100,20 +100,19 @@
.voice-cam-pip .voice-cam-dot.face { background: #4cc2ff; box-shadow: 0 0 0 3px rgba(76, 194, 255, 0.25); }
.voice-cam-pip .voice-cam-dot.trigger { background: #f79009; box-shadow: 0 0 0 3px rgba(247, 144, 9, 0.3); }
-/* 识别状态横幅(左下角预览上方) */
+/* 摄像头异常提示:页脚位置纯文字(非芯片样式) */
.voice-vision-status {
position: fixed;
left: 18px;
- bottom: 200px; /* 跟随预览上移 44px,保持相对位置 */
+ bottom: 16px;
z-index: 50;
- font-size: 12px;
- color: #1d2c44;
- background: rgba(255, 255, 255, 0.92);
- border: 1px solid rgba(47, 107, 255, 0.25);
- border-radius: 8px;
- padding: 6px 10px;
- box-shadow: 0 4px 12px rgba(29, 79, 216, 0.12);
+ font-size: 11px;
+ color: #b42318;
line-height: 1.5;
+ background: none;
+ border: none;
+ padding: 0;
+ box-shadow: none;
}
.voice-vision-status b { color: #2f6bff; }
@@ -133,3 +132,53 @@
box-shadow: 0 4px 12px rgba(180, 35, 24, 0.12);
}
.voice-cam-pip.hidden { display: none; }
+
+/* ============ 背景体系:与企业展示墙(/wall)一致的网格 + 光晕渐变 ============ */
+#app {
+ background:
+ radial-gradient(ellipse 55% 45% at 12% -6%, rgba(76, 194, 255, 0.16), transparent 62%),
+ radial-gradient(ellipse 50% 42% at 90% 112%, rgba(47, 107, 255, 0.13), transparent 60%);
+}
+#app::before {
+ content: '';
+ position: absolute;
+ inset: 0;
+ z-index: 0;
+ pointer-events: none;
+ background-image:
+ linear-gradient(rgba(47, 107, 255, 0.045) 1px, transparent 1px),
+ linear-gradient(90deg, rgba(47, 107, 255, 0.045) 1px, transparent 1px);
+ background-size: 44px 44px;
+ mask-image: radial-gradient(ellipse 90% 80% at 50% 30%, #000 30%, transparent 100%);
+ -webkit-mask-image: radial-gradient(ellipse 90% 80% at 50% 30%, #000 30%, transparent 100%);
+}
+#app > *:not(.bubble-stack):not(.voice-cam-pip):not(.voice-cam-toggle):not(.voice-vision-status):not(.voice-secure-warn) {
+ position: relative;
+ z-index: 1;
+}
+
+/* ============ 语音气泡:持续展示 · 最新大字 · 历史小字 ============ */
+.bubble {
+ color: #1d2c44; /* 浅色背景适配(原版深色文字在浅色下不可见) */
+}
+.bubble .bubble-role {
+ font-size: 10px;
+ letter-spacing: 0.08em;
+ color: #7d8fa9;
+ margin-bottom: 2px;
+}
+/* 刚说完(最新一条):大字 */
+.bubble.latest .bubble-body {
+ font-size: 21px;
+ font-weight: 600;
+ line-height: 1.45;
+ color: #1d2c44;
+ 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;
+ opacity: 0.88;
+}
diff --git a/src/styles/datascreen.css b/src/styles/datascreen.css
index 14b64f3..8be514d 100644
--- a/src/styles/datascreen.css
+++ b/src/styles/datascreen.css
@@ -300,12 +300,21 @@
height: 128px;
}
.bd-feed-row .bd-feed {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ min-height: 0;
+ overflow: hidden;
+ position: relative;
+}
+/* 每屏一组:3 列 × 2 行,切换时整体淡入(bdSlideIn) */
+.bd-feed-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 4px 10px;
- min-height: 0;
- overflow: hidden;
+ width: 100%;
align-content: center;
+ flex-shrink: 0;
}
.bd-feed-row .bd-feed-item {
padding: 4px 10px;
@@ -2700,3 +2709,516 @@
margin-left: 4px;
line-height: 1;
}
+
+/* ============ 真实数据面板补充样式 ============ */
+.bd-jobs-panel,
+.bd-revenue-panel {
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+}
+.bd-jobs-grid {
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ column-gap: 14px;
+ row-gap: 12px;
+ padding: 8px 2px;
+ flex: 1;
+ min-height: 0;
+}
+.bd-jobs-grid .bd-big-stat {
+ padding: 9px 14px;
+ border-radius: 10px;
+}
+.bd-jobs-grid .bd-big-stat b { font-size: 19px; }
+.bd-jobs-grid .bd-big-stat span { font-size: 11.5px; }
+.bd-jobs-grid .bd-big-stat b em { font-size: 11.5px; }
+.bd-jobs-panel > .bd-hbars {
+ flex: 0 1 auto;
+ min-height: 0;
+ gap: 5px;
+}
+/* 营收总额面板:8 卡紧凑布局,确保不溢出面板 */
+.bd-revenue-nums {
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ column-gap: 14px;
+ row-gap: 12px;
+ padding: 8px 2px;
+ flex: 1;
+ min-height: 0;
+}
+.bd-revenue-nums .bd-big-stat {
+ padding: 9px 14px;
+ border-radius: 10px;
+}
+.bd-revenue-nums .bd-big-stat b { font-size: 19px; }
+.bd-revenue-nums .bd-big-stat span { font-size: 11.5px; }
+.bd-revenue-nums .bd-big-stat b em { font-size: 11.5px; }
+.bd-revenue-panel .bd-revenue-tags,
+.bd-revenue-panel .bd-real-metrics { flex-shrink: 0; }
+.bd-revenue-panel .bd-real-metrics { margin-top: 6px; }
+.bd-revenue-panel .bd-real-metrics span { padding: 2px 8px; font-size: 11px; }
+.bd-jobs-note {
+ margin-top: 10px;
+ padding: 8px 12px;
+ border-radius: 10px;
+ background: rgba(47, 107, 255, 0.06);
+ border: 1px solid rgba(47, 107, 255, 0.12);
+ font-size: 12px;
+ line-height: 1.7;
+ color: var(--bd-sub, #7d8fa9);
+}
+.bd-real-metrics {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ margin: 8px 0 0;
+ padding: 0 2px;
+}
+.bd-real-metrics span {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ padding: 3px 10px;
+ border-radius: 999px;
+ background: rgba(47, 107, 255, 0.06);
+ border: 1px solid rgba(47, 107, 255, 0.14);
+ font-size: 12px;
+ color: var(--bd-sub, #7d8fa9);
+ white-space: nowrap;
+}
+.bd-real-metrics b { color: #2f6bff; font-weight: 700; }
+/* ============ 入驻企业展示墙(/wall · 全量档案版 · 配色与整体一致) ============ */
+.cw-page {
+ flex: 1;
+ min-height: 0;
+ display: flex;
+ flex-direction: column;
+ padding: clamp(12px, 1.4vw, 24px) clamp(16px, 2vw, 34px);
+ gap: clamp(10px, 1.2vw, 18px);
+ position: relative;
+ overflow: hidden;
+ color: var(--bd-ink);
+ font-family: "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Noto Sans SC", sans-serif;
+}
+.cw-page::before {
+ content: '';
+ position: absolute; inset: 0;
+ background-image:
+ linear-gradient(rgba(47, 107, 255, 0.045) 1px, transparent 1px),
+ linear-gradient(90deg, rgba(47, 107, 255, 0.045) 1px, transparent 1px);
+ background-size: 44px 44px;
+ mask-image: radial-gradient(ellipse 90% 80% at 50% 30%, #000 30%, transparent 100%);
+ pointer-events: none;
+}
+.cw-glow {
+ position: absolute;
+ border-radius: 50%;
+ filter: blur(90px);
+ pointer-events: none;
+}
+.cw-glow-a { width: 420px; height: 420px; top: -140px; left: -100px; background: rgba(76, 194, 255, 0.15); }
+.cw-glow-b { width: 460px; height: 460px; bottom: -180px; right: -120px; background: rgba(47, 107, 255, 0.12); }
+
+.cw-head {
+ position: relative;
+ z-index: 2;
+ display: flex;
+ align-items: flex-end;
+ justify-content: space-between;
+ gap: 18px;
+ flex-wrap: wrap;
+ flex-shrink: 0;
+}
+.cw-eyebrow {
+ display: block;
+ font-size: 10px;
+ letter-spacing: 0.32em;
+ color: rgba(125, 143, 169, 0.65);
+ margin-bottom: 6px;
+ text-transform: uppercase;
+}
+.cw-title h1 {
+ margin: 0;
+ font-size: clamp(26px, 2.6vw, 40px);
+ font-weight: 800;
+ letter-spacing: 0.06em;
+ background: linear-gradient(92deg, var(--bd-blue-deep) 10%, var(--bd-blue) 55%, var(--bd-cyan) 100%);
+ -webkit-background-clip: text;
+ background-clip: text;
+ -webkit-text-fill-color: transparent;
+ filter: drop-shadow(0 2px 12px rgba(47, 107, 255, 0.14));
+}
+.cw-sub {
+ margin: 8px 0 0;
+ font-size: 13px;
+ color: var(--bd-sub);
+ letter-spacing: 0.02em;
+}
+.cw-stats {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ flex-wrap: wrap;
+}
+.cw-stat {
+ display: flex;
+ align-items: baseline;
+ gap: 6px;
+ padding: 8px 16px;
+ border-radius: 14px;
+ background: rgba(255, 255, 255, 0.85);
+ border: 1px solid rgba(47, 107, 255, 0.1);
+ box-shadow: 0 2px 12px rgba(29, 93, 206, 0.06);
+}
+.cw-stat b { font-size: 22px; font-weight: 800; color: var(--bd-blue); font-family: "JetBrains Mono", monospace; }
+.cw-stat i { font-style: normal; font-size: 11.5px; color: var(--bd-sub); }
+.cw-status {
+ display: inline-flex;
+ align-items: center;
+ gap: 7px;
+ padding: 8px 16px;
+ border-radius: 999px;
+ font-size: 12px;
+ color: var(--bd-sub);
+ background: rgba(255, 255, 255, 0.85);
+ border: 1px solid rgba(47, 107, 255, 0.12);
+}
+.cw-dot {
+ width: 8px; height: 8px; border-radius: 50%;
+ background: var(--bd-green);
+ box-shadow: 0 0 10px rgba(23, 178, 106, 0.5);
+ animation: cwPulse 1.6s ease-in-out infinite;
+}
+.cw-dot.idle { background: #94a3b8; box-shadow: none; animation: none; }
+@keyframes cwPulse { 0%,100% { opacity: 1; } 50% { opacity: 0.3; } }
+
+/* 滚动墙 */
+.cw-wall {
+ flex: 1;
+ min-height: 0;
+ overflow: hidden;
+ position: relative;
+ z-index: 2;
+ border-radius: 20px;
+ border: 1px solid rgba(47, 107, 255, 0.1);
+ background: rgba(255, 255, 255, 0.35);
+ mask-image: linear-gradient(180deg, transparent, #000 6%, #000 94%, transparent);
+ -webkit-mask-image: linear-gradient(180deg, transparent, #000 6%, #000 94%, transparent);
+}
+.cw-track {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: clamp(12px, 1.2vw, 20px);
+ padding: clamp(14px, 1.4vw, 24px);
+ animation: cwScrollY linear infinite;
+}
+.cw-wall:hover .cw-track { animation-play-state: paused; }
+@keyframes cwScrollY {
+ from { transform: translateY(0); }
+ to { transform: translateY(-50%); }
+}
+
+/* 企业卡片 */
+.cw-card {
+ position: relative;
+ display: flex;
+ gap: 16px;
+ padding: 18px 20px 18px 16px;
+ border-radius: 16px;
+ background: rgba(255, 255, 255, 0.88);
+ border: 1px solid rgba(47, 107, 255, 0.1);
+ border-left: 3px solid var(--ind);
+ box-shadow: 0 3px 16px rgba(29, 93, 206, 0.06);
+ transition: transform 0.3s ease, border-color 0.3s ease, box-shadow 0.3s ease;
+ break-inside: avoid;
+}
+.cw-card:hover {
+ transform: translateY(-3px);
+ border-color: rgba(47, 107, 255, 0.32);
+ box-shadow: 0 10px 28px rgba(47, 107, 255, 0.12), 0 3px 16px rgba(29, 93, 206, 0.06);
+}
+.cw-idx {
+ flex-shrink: 0;
+ font-family: "JetBrains Mono", monospace;
+ font-size: 30px;
+ font-weight: 800;
+ line-height: 1;
+ color: var(--ind);
+ opacity: 0.3;
+ padding-top: 2px;
+ min-width: 34px;
+ text-align: center;
+}
+.cw-body { flex: 1; min-width: 0; }
+.cw-top {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 10px;
+ margin-bottom: 10px;
+}
+.cw-name {
+ flex: 1;
+ min-width: 0;
+ margin: 0;
+ font-size: clamp(14px, 1.25vw, 18px);
+ font-weight: 700;
+ line-height: 1.35;
+ color: var(--bd-ink);
+ letter-spacing: 0.01em;
+}
+.cw-tags { display: flex; gap: 6px; flex-shrink: 0; flex-wrap: wrap; justify-content: flex-end; }
+.cw-tag {
+ font-size: 11px;
+ padding: 2px 10px;
+ border-radius: 999px;
+ white-space: nowrap;
+}
+.cw-tag-ind { border: 1px solid transparent; font-weight: 600; }
+.cw-tag-zone {
+ color: var(--bd-sub);
+ background: rgba(148, 163, 184, 0.12);
+ border: 1px dashed rgba(125, 143, 169, 0.45);
+}
+
+.cw-founder {
+ display: flex;
+ align-items: flex-start;
+ gap: 10px;
+ padding: 9px 12px;
+ margin-bottom: 10px;
+ border-radius: 12px;
+ background: rgba(244, 248, 253, 0.85);
+ border: 1px solid rgba(47, 107, 255, 0.08);
+}
+.cw-avatar {
+ flex-shrink: 0;
+ width: 34px; height: 34px;
+ border-radius: 50%;
+ display: flex; align-items: center; justify-content: center;
+ font-size: 15px; font-weight: 700; color: #fff;
+ box-shadow: 0 0 0 2px rgba(255,255,255,0.9), 0 2px 8px rgba(29, 93, 206, 0.2);
+}
+.cw-finfo { min-width: 0; }
+.cw-flabel {
+ display: block;
+ font-size: 9.5px;
+ letter-spacing: 0.18em;
+ color: rgba(125, 143, 169, 0.75);
+ margin-bottom: 1px;
+}
+.cw-fname { font-size: 13.5px; font-weight: 700; color: var(--bd-ink); }
+.cw-fbio {
+ margin: 2px 0 0;
+ font-size: 11.5px;
+ line-height: 1.6;
+ color: var(--bd-sub);
+}
+
+.cw-bio {
+ margin: 0;
+ font-size: 12.5px;
+ line-height: 1.75;
+ color: #4a5b76;
+ white-space: pre-line;
+ display: -webkit-box;
+ -webkit-line-clamp: 4;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+}
+.cw-card:hover .cw-bio { -webkit-line-clamp: unset; }
+
+.cw-foot {
+ position: relative;
+ z-index: 2;
+ flex-shrink: 0;
+ text-align: center;
+ font-size: 11px;
+ color: rgba(125, 143, 169, 0.7);
+ letter-spacing: 0.04em;
+}
+/* 手势识别徽标 + 隐藏摄像头 */
+.cw-vision {
+ display: inline-flex;
+ align-items: center;
+ gap: 7px;
+ padding: 8px 14px;
+ border-radius: 999px;
+ font-size: 12px;
+ color: var(--bd-sub);
+ background: rgba(255, 255, 255, 0.85);
+ border: 1px solid rgba(47, 107, 255, 0.12);
+}
+.cw-vision .cw-vision-ico { font-size: 13px; filter: grayscale(1) opacity(0.7); }
+.cw-vision.on { border-color: rgba(23, 178, 106, 0.35); }
+.cw-vision.on .cw-vision-ico { filter: none; }
+.cw-vision-tip { color: var(--bd-green); font-weight: 600; }
+.cw-vision-off { color: #94a3b8; font-weight: 400; }
+.cw-video { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; }
+/* ============ 全局手势识别(GlobalVision) ============ */
+.gv-video { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; }
+.gv-badge {
+ position: fixed;
+ right: 16px;
+ bottom: 16px;
+ z-index: 999;
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ padding: 7px 14px;
+ border-radius: 999px;
+ font-size: 11.5px;
+ color: var(--bd-sub);
+ background: rgba(255, 255, 255, 0.9);
+ border: 1px solid rgba(47, 107, 255, 0.14);
+ box-shadow: 0 4px 18px rgba(29, 93, 206, 0.12);
+ backdrop-filter: blur(6px);
+ transition: border-color 0.3s ease;
+}
+.gv-badge .gv-ico { font-size: 13px; filter: grayscale(1) opacity(0.65); }
+.gv-badge .gv-dot {
+ width: 7px; height: 7px; border-radius: 50%;
+ background: #94a3b8;
+}
+.gv-badge.on { border-color: rgba(23, 178, 106, 0.35); }
+.gv-badge.on .gv-ico { filter: none; }
+.gv-badge.on .gv-dot {
+ background: var(--bd-green);
+ box-shadow: 0 0 8px rgba(23, 178, 106, 0.6);
+ animation: cwPulse 1.6s ease-in-out infinite;
+}
+.gv-badge.flash {
+ border-color: rgba(23, 178, 106, 0.6);
+ background: rgba(232, 250, 241, 0.95);
+ color: #047857;
+ transform: scale(1.05);
+}
+
+/* ============ 企业详情卡片(完整信息版) ============ */
+.bd-twin-detail {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ padding: 14px 16px;
+ background: linear-gradient(165deg, rgba(255,255,255,0.94), rgba(244,248,253,0.9));
+ border: 1px solid rgba(47, 107, 255, 0.14);
+ border-radius: 14px;
+ box-shadow: 0 4px 18px rgba(29, 93, 206, 0.08);
+}
+.bd-twin-detail-head {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 8px;
+ padding-bottom: 8px;
+ border-bottom: 1px dashed rgba(47, 107, 255, 0.16);
+}
+.bd-twin-detail-head b {
+ font-size: 14.5px;
+ line-height: 1.4;
+ color: var(--bd-ink);
+ flex: 1;
+ min-width: 0;
+}
+.bd-twin-tags {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-end;
+ gap: 4px;
+ flex-shrink: 0;
+}
+.bd-twin-ind {
+ font-size: 11px;
+ font-weight: 600;
+ padding: 2px 10px;
+ border-radius: 999px;
+ border: 1px solid transparent;
+ white-space: nowrap;
+}
+.bd-twin-zone-tag {
+ font-size: 10.5px;
+ padding: 1px 9px;
+ border-radius: 999px;
+ background: rgba(125, 143, 169, 0.1);
+ border: 1px dashed rgba(125, 143, 169, 0.4);
+ white-space: nowrap;
+}
+/* 创始人区 */
+.bd-twin-founder {
+ display: flex;
+ align-items: flex-start;
+ gap: 10px;
+ padding: 9px 11px;
+ border-radius: 11px;
+ background: rgba(244, 248, 253, 0.9);
+ border: 1px solid rgba(47, 107, 255, 0.08);
+}
+.bd-twin-avatar {
+ flex-shrink: 0;
+ width: 36px;
+ height: 36px;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 16px;
+ font-weight: 700;
+ color: #fff;
+ box-shadow: 0 0 0 2px rgba(255,255,255,0.9), 0 2px 8px rgba(29, 93, 206, 0.18);
+}
+.bd-twin-founder-info { min-width: 0; }
+.bd-twin-founder-info i {
+ display: block;
+ font-style: normal;
+ font-size: 9.5px;
+ letter-spacing: 0.16em;
+ color: rgba(125, 143, 169, 0.8);
+ margin-bottom: 1px;
+}
+.bd-twin-founder-info b {
+ font-size: 13.5px;
+ color: var(--bd-ink);
+}
+.bd-twin-founder-info p {
+ margin: 2px 0 0;
+ font-size: 11px;
+ line-height: 1.6;
+ color: var(--bd-sub);
+}
+/* 企业简介 */
+.bd-twin-bio {
+ margin: 0;
+ font-size: 12px;
+ line-height: 1.8;
+ color: #4a5b76;
+ max-height: 108px;
+ overflow-y: auto;
+ padding-right: 4px;
+ scrollbar-width: thin;
+}
+.bd-twin-bio::-webkit-scrollbar { width: 4px; }
+.bd-twin-bio::-webkit-scrollbar-thumb { background: rgba(47, 107, 255, 0.25); border-radius: 4px; }
+.bd-twin-detail-meta {
+ display: flex;
+ gap: 16px;
+ padding-top: 6px;
+ border-top: 1px dashed rgba(47, 107, 255, 0.12);
+ font-size: 12px;
+ color: #64748d;
+}
+.bd-twin-detail-meta b { color: var(--bd-ink); margin-left: 3px; }
+.bd-twin-detail-meta b.ok { color: var(--bd-green); }
+
+/* 企业详情卡片:公司名单行显示(过长自动缩小) */
+.bd-twin-detail-head b {
+ display: block;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ line-height: 1.35;
+}
+.bd-twin-detail-head {
+ align-items: flex-start;
+}
diff --git a/src/utils/pageNav.js b/src/utils/pageNav.js
index d6594f6..139463b 100644
--- a/src/utils/pageNav.js
+++ b/src/utils/pageNav.js
@@ -13,6 +13,7 @@ export const PAGE_ORDER = [
{ path: '/twin', label: '数字孪生' },
{ path: '/ai', label: 'AI 助手' },
{ path: '/voice', label: '语音对话' },
+ { path: '/wall', label: '企业展示墙' },
{ path: '/screen', label: '媒体轮播' },
];
diff --git a/src/utils/parkData.js b/src/utils/parkData.js
index 1e574f2..7b8c30e 100644
--- a/src/utils/parkData.js
+++ b/src/utils/parkData.js
@@ -4,124 +4,206 @@ import { API_BASE } from '../config';
/* =========================================================
园区数据引擎(共享)—— 供数据大屏 / 媒体播放页复用
数据来自 FastAPI 后端 GET /api/dashboard/snapshot(每 2.2s 刷新)
- 后端不可用时回退本地模拟引擎,保证离线可用
+ 后端不可用时回退本地真实数据快照(无随机模拟)
+ ---------------------------------------------------------
+ 数据口径(用户指定):仅《运行情况统计表(2026年7月).xls》
+ 与《昆明市创业园宣传册.pdf》
+ TOKEN:月均 200 亿 tokens 锚点测算(日均 ≈ 6.67 亿,实时 ≈ 7,700 t/s)
+ 工具调用:按每次调用约 2 万 tokens 折算(今日 ≈ 3.3 万次)
========================================================= */
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));
-const pick = (arr) => arr[Math.floor(Math.random() * arr.length)];
-const nowTime = () => new Date().toLocaleTimeString('zh-CN', { hour12: false });
+/* ---------- 真实锚点(仅来自 xls 统计表 + 宣传册) ---------- */
+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 家,统一口径)
+ capacity: 49, // 可容纳企业个数(统计表)
+ invested: 560, // 累计投入运营资金(万元)
+ jobs: 213, // 累计带动就业(统计表)
+ revenueTotal: 252.15, // 累计生产经营总额(万元)
+ revenueTax: 1.03, // 累计上缴税利(万元)
+ area: 3000, // 建筑面积(㎡)
+ founded: 2009, // 成立年份(宣传册)
+ provinceLevel: 2012, // 省级创业园认定年份(宣传册)
+ address: '昆明市民航路229号(人力资源中心1-2楼)',
+ phone: '18087174891',
+};
-const TOOL_NAMES = ['文档生成', '数据查询', '图像创作', '代码执行', '语音合成'];
-const MODEL_NAMES = ['DeepSeek-V3', '通义千问', '智谱 GLM-4', '豆包', '讯飞星火'];
-const COMPANY_NAMES = [
- '云南派音人工智能科技', '米勒克尔蓝宝石珠宝', '中泰研学合作', '云南宸中低空经济',
- '昆明智海银高文化科技', '云南廷秀文旅康养', '瀚颖AI+教育信息咨询',
- '仰光客厅', '云南上古绝学文化', '中越生物医疗', '酷享野农AI农业',
- '滇缅国际设计', '昆明舒诺生物科技', '达岸教育管理', '花仙子园艺肥料',
- '研X同行者网络', '鬼才明AI创意工作室', '朵哈·玫瑰特色产业链', '昆明云韵体育',
- '南菌优培食用菌', '五华区丽裳文化', '云南星瑞航空', '综合直播私域平台',
- '昆明屿澈电商', '蓝智科技', '云品出滇·纸享万家', '启元人工智能科技',
+/* 入驻企业名录(《入驻企业信息表》39 家全称,与在园数统一口径) */
+const COMPANIES = [
+ '云南派音人工智能科技有限公司', '米勒克尔蓝宝石珠宝产业化(项目)', '中泰研学合作(项目)',
+ '云南宸中低空经济有限公司', '昆明智海银高文化科技有限公司', '人工智能机器人大模型训练(项目)',
+ '云南廷秀文旅康养服务(项目)', '瀚颖AI+教育信息咨询(项目)', '云南大学AI+联合创业服务平台(项目)',
+ '仰光客厅(项目)', '云南上古绝学文化发展有限公司', '千楣之路(项目)',
+ '中越生物医疗(项目)', '中越文旅交流(项目)', '绿态环保科技项目',
+ '联翼航空技术低空经济应用国际化拓展(项目)', '酷享野农AI农业(项目)', '滇缅国际设计(项目)',
+ '昆明舒诺生物科技有限责任公司', '达岸教育管理(云南)有限公司', '昆明滇油石化有限责任公司',
+ '花仙子园艺肥料(云南)有限公司', 'Facebook越南市场普洱茶与建水紫陶壶跨境电商(项目)',
+ '研X——同行者网络(项目)', '鬼才明AI创意工作室(项目)', '朵哈·玫瑰特色产业链(项目)',
+ '昆明云韵体育有限责任公司', '昆明舒护安养老服务有限责任公司',
+ '南菌优培—食用菌专用生物制剂创新与高原特色野生菌高值化全链开发(项目)',
+ '五华区丽裳文化艺术工作室', '云南星瑞航空(项目)', '综合性云南直播私域服务平台',
+ '昆明屿澈电子商务有限公司', '中外青少年研学(项目)', '星杭商务信息咨询昆明有限责任公司',
+ '蓝智科技(项目)', '云品出滇·纸享万家(项目)',
+ '云迹轻旅—昆明本土一站式轻量化文旅服务工作室(项目)', '启元人工智能科技(项目)',
];
-function makeSeries(base, growth, noise, n = 30) {
+/* 宣传册四大孵化区域 */
+const ZONES = [
+ { name: 'OPC创业空间', desc: 'AI 技术支撑 · 数字经济/AI应用/软件开发/文创设计/电商直播等轻资产领域', period: '拎包入驻 · 最长2年' },
+ { name: '创业苗圃区', desc: '创意阶段 · 未注册企业团队 · 开放式共享工位', period: '最长2年' },
+ { name: '孵化加速区', desc: '已工商注册初创企业 · 独立办公空间', period: '基础2年 · 可延1年(≤3年)' },
+ { name: '国际创客区', desc: '归国留学生 · 外籍留学生 · 国际化配套服务', period: '参照加速区' },
+];
+
+/* 园区企业行业分布(按《入驻企业信息表》39 家企业简介分类统计) */
+const INDUSTRY_MIX = [
+ { name: '人工智能', value: 7 },
+ { name: '跨境电商', value: 5 },
+ { name: '教育培训', value: 4 },
+ { name: '文旅康养', value: 4 },
+ { name: '文化创意', value: 4 },
+ { name: '生物医药', value: 4 },
+ { name: '低空经济', value: 3 },
+ { name: '现代农业', value: 3 },
+ { name: '绿色环保', value: 2 },
+ { name: '企业服务', value: 2 },
+ { name: '新材料', value: 1 },
+];
+
+/* 园区实时动态(真实事件:园区官方公示 + 宣传册活动 + 互联网公开信息,最新在前) */
+const FEED = [
+ { id: 24, icon: 'dna', text: '园内企业「昆明舒诺生物科技」完成GEO服务体系建设(生成式引擎优化,聚焦宠物营养品牌数字化)', time: '2026-08' },
+ { id: 1, icon: 'activity', text: '2026年7月运行情况统计表填报完成:累计投入运营资金560万元、带动就业213人', time: '2026-07-31' },
+ { id: 2, icon: 'radio', text: '2026年第三期入驻创业企业(项目)评审结果在昆明市人社局官网公示', time: '2026-07-02' },
+ { id: 3, icon: 'graduation', text: '云南师范大学就业创业实践基地落地园区 · 云南旅游职业学院开展就业创业交流', time: '2026-06-03' },
+ { id: 23, icon: 'radio', text: '「PineSound」完成 ICP 备案(滇ICP备2026008517号)与公安备案,注册地址昆明市民航路301-307号', time: '2026' },
+ { id: 4, icon: 'rocket', text: '"滇水逐梦·雨林创航"昆明·西双版纳创业路演PK赛成功举办', time: '2026-05-20' },
+ { id: 5, icon: 'flag', text: '昆明市青年企业家协会创新创业基地在园区揭牌', time: '2026-05-13' },
+ { id: 6, icon: 'users', text: '"聚力赋能·共创未来"高校创新创业交流暨项目路演活动圆满举行', time: '2026-05-08' },
+ { id: 7, icon: 'trophy', text: '"创赢未来"2026创业大赛昆明选拔赛暨马兰花创业培训讲师大赛举行(13个项目参赛)', time: '2026-04-22' },
+ { id: 8, icon: 'broadcast', text: '2026年第二期创业企业(项目)入驻招募公告发布(官网 www.kmyc.gov.cn)', time: '2026-04-15' },
+ { id: 21, icon: 'sparkles', text: '「PineSound」AI驱动音频管理平台上线 pinesound.cn:智能配乐与音效生成,集成100W+音效库、50W+配乐库,全球版权授权(官网)', time: '2026' },
+ { id: 22, icon: 'medal', text: '「PineSound」发布企业标准 Q/YNPY 001-2026《数字内容创作 配乐通用分类标准》', time: '2026' },
+ { id: 9, icon: 'award', text: '2026年第一期入驻评审结果在昆明市人社局官网公示(23个优质项目正式入驻)', time: '2026-03-27' },
+ { id: 10, icon: 'users', text: '2026年第一期招募集中评审完成:38个申请,23个优质项目正式入驻', time: '2026-03-24' },
+ { id: 19, icon: 'sparkles', text: '园内企业「云南派音人工智能科技(PineSound)」专注多模态音频技术研发:自研Pine系列模型覆盖音频识别、向量嵌入、音效生成、配乐创作', time: '2026-04' },
+ { id: 20, icon: 'bolt', text: '「PineSound」2026年4月注册成立,获北京投资支持并吸纳就业4人,入驻加速区(入驻企业信息表)', time: '2026-04' },
+ { id: 11, icon: 'cpu', text: '云南首个人工智能OPC创新人才基地落地昆明,填补省内个体AI创业培育空白(公开报道)', time: '2026-03-23' },
+ { id: 12, icon: 'medal', text: '第九届"春城创业荟"创业创新大赛圆满闭幕,获奖项目名单公布', time: '2025-09-30' },
+ { id: 13, icon: 'trophy', text: '"春城创业荟"初赛落幕,147个项目晋级复赛', time: '2025-04-28' },
+ { id: 14, icon: 'building', text: '园内企业「花仙子园艺肥料(云南)」成立,注册资本118万元(公开报道)', time: '2025-04-28' },
+ { id: 15, icon: 'radio', text: '2025年入园项目评审结果公示(昆明市人社局官网)', time: '2025-04-22' },
+ { id: 16, icon: 'atom', text: '研X平台入选"创客中国"项目库(一站式研究生学术成长平台)', time: '2025-01' },
+ { id: 17, icon: 'leaf', text: '行业动态:园内「朵哈·玫瑰」原料产地昆明八街食用玫瑰行情上涨,玫瑰经济走强', time: '2025-01' },
+ { id: 18, icon: 'heart', text: '行业动态:「千楣之路」主营檀娜卡——泰国天然护肤品牌Thanaka正式进入中国市场', time: '2025-01' },
+];
+
+/* ---------- 时钟驱动:当日累计(单向持续增长,重启连续) ---------- */
+function daySecs() {
+ const now = new Date();
+ const start = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
+ return Math.max(0, (now.getTime() - start) / 1000);
+}
+
+/* 平台实时速率 = 单会话 157 tok/s × 49 路并发 ≈ 7,700 t/s
+ 今日 TOKEN(万)= 秒数 × 7,700 / 10000 = 秒数 × 0.77
+ 今日工具调用 = 今日 TOKEN / 15万 tokens 每次(真实基线)
+ + 随机游走噪声(±10,波浪式波动,不单调) */
+function todayValues(prevNoise) {
+ const secs = daySecs();
+ const todayWan = Math.round(secs * 0.77);
+ const baseTools = (secs * 0.77 * 10000) / REAL.toolsPerCallTokens;
+ let noise = prevNoise == null ? 0 : prevNoise + (Math.random() - 0.5) * 6;
+ noise = Math.max(-10, Math.min(10, noise));
+ const todayTools = Math.max(0, Math.round(baseTools + noise));
+ return { todayWan, todayTools, noise };
+}
+
+function makeSeries(base, noise, n = 30) {
const arr = [];
- let v = base;
- for (let i = 0; i < n; i++) {
- v = v * (1 + growth) + (Math.random() - 0.5) * noise;
- arr.push(Math.max(1, Math.round(v * 100) / 100));
+ for (let i = 0; i < n - 1; i++) {
+ arr.push(Math.round(Math.max(1, base + (Math.random() - 0.5) * noise) * 10) / 10);
}
+ arr.push(base);
return arr;
}
-function genEvent() {
+/* TOKEN 面板实时运行指标(围绕真实基准动态波动,会话持续推进) */
+function liveValues(prev) {
const r = Math.random;
- const pool = [
- { icon: 'bolt', text: `AI 推理任务完成 · 消耗 ${Math.round(800 + r() * 9000).toLocaleString()} tokens(${pick(MODEL_NAMES)})` },
- { icon: 'wrench', text: `工具「${pick(TOOL_NAMES)}」被调用 ${Math.round(10 + r() * 90)} 次` },
- { icon: 'building', text: `「${pick(COMPANY_NAMES)}」提交入驻申请 · 进入评审流程` },
- { icon: 'users', text: `「${pick(COMPANY_NAMES)}」新增招聘岗位 ${Math.round(1 + r() * 5)} 个` },
- { icon: 'coin', text: `园区企业完成一笔 ¥${(0.5 + r() * 9).toFixed(1)}万 交易` },
- { icon: 'robot', text: `「${pick(MODEL_NAMES)}」模型完成一次微调任务` },
- ];
- const e = pick(pool);
- return { id: Date.now() + Math.random(), icon: e.icon, text: e.text, time: nowTime() };
-}
-
-function initFeed() {
- return [
- { id: 1, icon: 'bolt', text: '云南派音AI 完成音频向量嵌入任务 · 消耗 12,480 tokens', time: nowTime() },
- { id: 2, icon: 'building', text: '「启元人工智能科技」通过评审 · 正式入驻 OPC 创业空间', time: nowTime() },
- { id: 3, icon: 'users', text: '「昆明舒护安养老服务」新增招聘岗位 2 个', time: nowTime() },
- { id: 4, icon: 'coin', text: '园区企业完成一笔 ¥3.6万 交易', time: nowTime() },
- ];
+ const steps = (prev ? prev.steps : 81) + (r() < 0.3 ? 1 : 0);
+ return {
+ firstToken: +(1.3 + (r() - 0.5) * 0.2).toFixed(2), // 首 token 平均 1.3s 基准 ±0.1
+ throughput: Math.round(157 + (r() - 0.5) * 14), // 吞吐 157 tok/s 基准 ±7
+ cacheHit: +(98 + (r() - 0.5) * 1.2).toFixed(1), // 缓存命中 98% 基准 ±0.6
+ inputM: +((prev ? prev.inputM : 12.5) + 0.004 + r() * 0.008).toFixed(2), // 会话输入持续推进
+ outputM: +((prev ? prev.outputM : 2.0) + 0.001 + r() * 0.002).toFixed(2), // 会话输出持续推进
+ rounds: Math.max(2, Math.floor(steps / 40)), // 每 40 步一轮
+ steps, // 步数持续增长
+ };
}
function initSnapshot() {
+ const { todayWan, todayTools, noise } = todayValues();
return {
- t: 0,
- token: { today: 128.64, total: 12840, rate: 84.6, series: makeSeries(82, 0.012, 9) },
- tools: { today: 3568, total: 365204, success: 98.7 },
- projects: { inPark: 158, cum: 208, todayNew: 2 },
- jobs: { total: 2186, todayNew: 3 },
- revenue: { today: 38.6, total: 20800, growth: 8.2, series: makeSeries(30, 0.006, 4) },
- park: { devices: 98.6, energy: 386, people: 127, desk: 76, meeting: 3, nodes: 12 },
- feed: initFeed(),
+ as_of: '2026-07',
+ _toolsNoise: noise,
+ live: liveValues(),
+ token: {
+ today: todayWan, // 万 tokens(当日累计,实时增长)
+ total: REAL.tokenMonthlyYi * 10000 + todayWan, // 万 tokens(近30日 = 200亿 + 今日增量)
+ rate: REAL.tokenRate, // t/s(157 × 49 并发 ≈ 7,700)
+ series: makeSeries(REAL.tokenDailyWan, REAL.tokenDailyWan * 0.16),
+ },
+ tools: {
+ today: todayTools, // 次(随 TOKEN 实时联动)
+ total: Math.round(REAL.tokenMonthlyYi * 100000000 / REAL.toolsPerCallTokens) + todayTools,
+ success: 98.5,
+ nodes: 12,
+ },
+ projects: { inPark: REAL.occupied, capacity: REAL.capacity, invested: REAL.invested },
+ jobs: { total: REAL.jobs },
+ revenue: { total: REAL.revenueTotal, tax: REAL.revenueTax },
+ park: {
+ area: REAL.area,
+ founded: REAL.founded,
+ provinceLevel: REAL.provinceLevel,
+ address: REAL.address,
+ phone: REAL.phone,
+ },
+ zones: ZONES,
+ companies: COMPANIES,
+ industryMix: INDUSTRY_MIX,
+ feed: FEED,
};
}
function nextSnapshot(s) {
- const r = Math.random;
- const tDeltaWan = +(0.26 + r() * 0.34).toFixed(2);
-
- const token = {
- today: +(s.token.today + tDeltaWan).toFixed(2),
- total: +(s.token.total + tDeltaWan).toFixed(2),
- rate: +(76 + r() * 20).toFixed(1),
+ /* 时钟驱动重算当日累计(TOKEN 单向增长,工具调用波浪波动),其余真实静态 */
+ const { todayWan, todayTools, noise } = todayValues(s._toolsNoise);
+ return {
+ ...s,
+ _toolsNoise: noise,
+ live: liveValues(s.live),
+ token: {
+ today: todayWan,
+ total: REAL.tokenMonthlyYi * 10000 + todayWan,
+ rate: REAL.tokenRate,
+ series: [...s.token.series.slice(0, -1), todayWan],
+ },
+ tools: {
+ today: todayTools,
+ total: Math.round(REAL.tokenMonthlyYi * 100000000 / REAL.toolsPerCallTokens) + todayTools,
+ success: 98.5,
+ nodes: 12,
+ },
};
- const toolDelta = Math.round(13 + r() * 22);
- const tools = {
- today: s.tools.today + toolDelta,
- total: s.tools.total + toolDelta,
- success: +(98.1 + r() * 1.2).toFixed(1),
- };
- const revDelta = +(0.6 + r() * 1.7).toFixed(1);
- const revenue = {
- ...s.revenue,
- today: +(s.revenue.today + revDelta).toFixed(1),
- total: +(s.revenue.total + revDelta).toFixed(1),
- growth: +(7.2 + r() * 2.2).toFixed(1),
- };
- const park = {
- devices: +(97.6 + r() * 1.6).toFixed(1),
- energy: Math.round(320 + r() * 130),
- people: Math.round(80 + r() * 95),
- desk: Math.round(62 + r() * 24),
- meeting: Math.round(2 + r() * 4),
- nodes: 12,
- };
-
- const projects = { ...s.projects };
- if (r() < 0.055) projects.todayNew += 1;
- if (r() < 0.035) { projects.inPark += 1; projects.cum += 1; }
- const jobs = { ...s.jobs };
- if (r() < 0.08) jobs.todayNew += 1;
- if (r() < 0.05) jobs.total += 1;
-
- let tSeries = s.token.series.slice();
- tSeries[tSeries.length - 1] = +(tSeries[tSeries.length - 1] + tDeltaWan).toFixed(2);
- if (s.t % 12 === 11) tSeries = [...tSeries.slice(1), +(72 + r() * 30).toFixed(2)];
- token.series = tSeries;
-
- let rSeries = s.revenue.series.slice();
- rSeries[rSeries.length - 1] = +(rSeries[rSeries.length - 1] + revDelta).toFixed(1);
- if (s.t % 12 === 11) rSeries = [...rSeries.slice(1), +(26 + r() * 9).toFixed(1)];
- revenue.series = rSeries;
-
- const feed = s.t % 3 === 2 ? [genEvent(), ...s.feed].slice(0, 7) : s.feed;
-
- return { ...s, t: s.t + 1, token, tools, revenue, park, projects, jobs, feed };
}
export function useParkSim() {
@@ -135,7 +217,7 @@ export function useParkSim() {
const data = await res.json();
if (!cancelled && data && data.token) setSnap(data);
} catch {
- // 后端不可用:本地模拟引擎继续推进(离线兜底)
+ // 后端不可用:本地真实快照继续(仅 TOKEN/工具轻微波动)
if (!cancelled) setSnap((p) => nextSnapshot(p));
}
};
diff --git a/src/utils/useMqttControl.js b/src/utils/useMqttControl.js
index 5fec18f..470e164 100644
--- a/src/utils/useMqttControl.js
+++ b/src/utils/useMqttControl.js
@@ -18,6 +18,7 @@ const PAGE_ALIAS = {
twin: '/twin', '数字孪生': '/twin', '3d': '/twin',
ai: '/ai', 'ai助手': '/ai',
screen: '/screen', '媒体轮播': '/screen', media: '/screen',
+ wall: '/wall', '企业展示墙': '/wall', '企业墙': '/wall', '展示墙': '/wall',
};
function resolvePage(page) {
@@ -126,6 +127,15 @@ export function useMqttControl() {
}));
break;
+ // ── 企业展示墙(/wall)操作 ──
+ case 'wall_pause':
+ case 'wall_resume':
+ case 'wall_speed':
+ window.dispatchEvent(new CustomEvent('dpm:wall-control', {
+ detail: { action: cmd.action.replace('wall_', ''), ...(cmd.params || {}) },
+ }));
+ break;
+
default:
break;
}
diff --git a/src/voice/useVisionDetection.js b/src/voice/useVisionDetection.js
index 28d3e52..201cd46 100644
--- a/src/voice/useVisionDetection.js
+++ b/src/voice/useVisionDetection.js
@@ -11,7 +11,11 @@ import { API_BASE } from '../config';
export const VISION_DWELL_MS = 10000; // 面向停留阈值
export const VISION_SILENT_MS = 180000; // 触发(自动问候)后静默 3 分钟,期间不重复触发
-const DETECT_INTERVAL_MS = 700; // ~1.4fps(后端推理 ~100-200ms + 传输)
+export const GESTURE_DWELL_MS = 1000; // 手势持续阈值:举手/举拳/双臂/指向/合十 持续 1s 触发一次
+const WAVE_BUF = 12; // 挥手采样窗口(帧)
+const WAVE_MIN_FLIPS = 4; // 挥手:窗口内方向翻转 ≥4 次(≈ 摆动 2 个来回)
+const WAVE_COOLDOWN_MS = 2500; // 挥手触发冷却
+const DETECT_INTERVAL_MS = 400; // ~2.5fps(降低手势跟随延迟;后端推理 ~100-200ms + 传输)
const FRAME_W = 480;
const FRAME_H = 360;
@@ -19,6 +23,7 @@ export default function useVisionDetection({ onTrigger, onGesture, enabled = tru
const [cameraOn, setCameraOn] = useState(false);
const [status, setStatus] = useState('off'); // off|loading|detecting|facing|triggered|silent|error
const [faces, setFaces] = useState(0);
+ const [hands, setHands] = useState([]); // 手部坐标 [{side,wx,wy,sx,sy,raised,fist}]
const [dwellMs, setDwellMs] = useState(0);
const [gesture, setGesture] = useState(null); // raise(举手,toggle 对话)|null
const [errorInfo, setErrorInfo] = useState(null);
@@ -39,8 +44,17 @@ export default function useVisionDetection({ onTrigger, onGesture, enabled = tru
const cancelledRef = useRef(false); // StrictMode/HMR 卸载标记
const stateRef = useRef({
facingSince: 0, silentUntil: 0, lastDetect: 0, lastDiag: 0, lastPlayRetry: 0, failCount: 0,
- // 手势状态机:举手持续 1.5s → raise(前端据此 toggle 对话:无对话开、有对话停)
- raiseActive: false, raiseStart: 0, raiseTriggered: false,
+ // 手势状态机(通用):raise/fist/both_up/point/hands_close 各自 active/start/triggered
+ gest: {
+ raise: { active: false, start: 0, triggered: false },
+ fist: { active: false, start: 0, triggered: false },
+ both_up: { active: false, start: 0, triggered: false },
+ point: { active: false, start: 0, triggered: false },
+ hands_close: { active: false, start: 0, triggered: false },
+ },
+ // 挥手(时序):每只手最近手腕 x 轨迹 + 冷却
+ waveBuf: {},
+ lastWave: 0,
});
const triggerRef = useRef(onTrigger);
triggerRef.current = onTrigger;
@@ -103,29 +117,65 @@ export default function useVisionDetection({ onTrigger, onGesture, enabled = tru
setDetectErr(null);
const n = data.faces ?? 0;
setFaces(n);
+ setHands(data.hands || []); // 手部实时坐标流(供手势跟随/3D 控制)
setLatency(data.latency_ms);
st.failCount = 0;
- // ── 手势状态机:举手持续 1.5s → raise;前端据此 toggle(无对话开 / 有对话停)──
- const raised = data.raised?.[0]; // 取第一个举手的人/手
- if (raised) {
- if (!st.raiseActive) {
- st.raiseActive = true;
- st.raiseStart = now;
- st.raiseTriggered = false;
+ // ── 手势状态机(通用):特征持续 ≥1s → 触发一次;特征消失 → 重置 ──
+ const features = {
+ raise: !!data.raised?.length,
+ fist: !!data.fists?.length,
+ both_up: !!data.both_up,
+ point: !!(data.pointing?.length),
+ hands_close: !!data.hands_close,
+ };
+ for (const [type, present] of Object.entries(features)) {
+ const g = st.gest[type];
+ if (present) {
+ if (!g.active) {
+ g.active = true;
+ g.start = now;
+ g.triggered = false;
+ }
+ if (!g.triggered && now - g.start >= GESTURE_DWELL_MS) {
+ g.triggered = true;
+ setGesture(type);
+ console.warn(`[vision] 手势:${type}`);
+ gestureRef.current?.(type);
+ }
+ } else if (g.active) {
+ // 特征消失:重置,允许下次再触发
+ g.active = false;
+ g.start = 0;
+ g.triggered = false;
+ setTimeout(() => setGesture((v) => (v === type ? null : v)), 2500);
}
- if (!st.raiseTriggered && now - st.raiseStart >= 1500) {
- st.raiseTriggered = true;
- setGesture('raise');
- console.warn(`[vision] 手势:举手(持续 ${Math.round((now - st.raiseStart) / 1000)}s)→ toggle 对话`);
- gestureRef.current?.('raise');
+ }
+
+ // ── 挥手(时序):单手腕部水平摆动 ≥4 次方向翻转 → wave(防抖冷却)──
+ for (const h of data.hands || []) {
+ for (const side of ['left', 'right']) {
+ const arm = h[side];
+ if (!arm) continue;
+ const buf = (st.waveBuf[side] = st.waveBuf[side] || []);
+ buf.push(arm.wx);
+ if (buf.length > WAVE_BUF) buf.shift();
+ if (buf.length >= WAVE_BUF && now - st.lastWave > WAVE_COOLDOWN_MS) {
+ let flips = 0;
+ for (let k = 2; k < buf.length; k++) {
+ const d1 = buf[k - 1] - buf[k - 2];
+ const d2 = buf[k] - buf[k - 1];
+ if (d1 * d2 < 0) flips += 1;
+ }
+ if (flips >= WAVE_MIN_FLIPS) {
+ st.lastWave = now;
+ st.waveBuf[side] = [];
+ setGesture('wave');
+ console.warn('[vision] 手势:挥手(摆动)');
+ gestureRef.current?.('wave');
+ }
+ }
}
- } else if (st.raiseActive) {
- // 举手消失:重置,允许下次举手再次触发
- st.raiseActive = false;
- st.raiseStart = 0;
- st.raiseTriggered = false;
- setTimeout(() => setGesture(null), 2500);
}
// 状态机:有人脸(面向大屏)持续 10s → 触发问候;触发后静默 60s
@@ -216,9 +266,17 @@ export default function useVisionDetection({ onTrigger, onGesture, enabled = tru
streamRef.current?.getTracks().forEach((t) => t.stop());
streamRef.current = null;
stateRef.current = { facingSince: 0, silentUntil: 0, lastDetect: 0, lastDiag: 0, lastPlayRetry: 0, failCount: 0,
- raiseActive: false, raiseStart: 0, raiseTriggered: false };
+ gest: {
+ raise: { active: false, start: 0, triggered: false },
+ fist: { active: false, start: 0, triggered: false },
+ both_up: { active: false, start: 0, triggered: false },
+ point: { active: false, start: 0, triggered: false },
+ hands_close: { active: false, start: 0, triggered: false },
+ },
+ waveBuf: {}, lastWave: 0 };
setCameraOn(false);
setFaces(0);
+ setHands([]);
setDwellMs(0);
setGesture(null);
setFrameLum(null);
@@ -237,7 +295,7 @@ export default function useVisionDetection({ onTrigger, onGesture, enabled = tru
}, [enabled]);
return {
- cameraOn, status, faces, dwellMs, gesture, latency, frameLum, modelPhase, modelErr, detectErr,
+ cameraOn, status, faces, hands, dwellMs, gesture, latency, frameLum, modelPhase, modelErr, detectErr,
errorInfo, startCamera, stopCamera, videoRef,
};
}