feat: 新增 AI 智能助手对话页面(/ai)

- 布局沿用数字孪生页接口:左侧 AI 对话区 + 右侧企业分布面板
- 新增共享 ParkSidePanel 组件(分区/企业名录/详情/实时动态),
  数字孪生与 AI 助手页右侧完全一致
- 新增 AiChatPanel 对话组件:欢迎语 + 消息流 + 快捷提问 + 输入发送,
  内置园区知识库模拟回复(入驻/政策/场地/AI 赋能/分区/企业/融资/就业/导师)
- 路由与导航循环加入 /ai(数据大屏 → 数字孪生 → AI 助手 → 媒体轮播)
- usePageNav 忽略输入框/文本域按键,避免输入方向键误切页面
- 新增 send/eraser/headset/circle-dot 图标,全部遵守 bd- 浅色科技 UI 规范
This commit is contained in:
Pine
2026-08-17 20:26:16 +08:00
parent ef009dad05
commit 7da9749953
9 changed files with 619 additions and 144 deletions
+185
View File
@@ -0,0 +1,185 @@
import { useState, useRef, useEffect, useCallback } from 'react';
import Icon from './Icons';
/* =========================================================
AI 智能助手对话面板(共享)—— 遵循 bd- 浅色科技 UI 规范
左侧对话区:消息流 + 快捷提问 + 输入发送(模拟园区知识库回复)
========================================================= */
/* ---------- 快捷提问 ---------- */
const QUICK_QUESTIONS = [
'如何申请入驻园区?',
'园区有哪些政策扶持?',
'办公场地怎么收费?',
'AI 赋能服务有哪些?',
'三个分区有什么区别?',
];
/* ---------- 园区知识库(模拟回复) ---------- */
const WELCOME = '您好,我是 OPC 园区 AI 智能助手。\n\n我可以为您解答入驻流程、政策扶持、场地服务、AI 赋能、企业名录等问题。您也可以点击下方快捷提问,或直接输入问题。';
const RULES = [
{
match: /入驻|申请|入孵|入园|如何加入|怎样加入/,
reply: '入驻流程如下:\n\n1. 提交入驻申请(线上表单或到园区服务中心现场办理)\n2. 项目评审(园区组织专家对项目可行性、团队背景进行评估)\n3. 签订入驻协议,办理入驻手续\n4. 安排办公场地,对接创业导师与孵化服务\n\n目前园区在园项目 158 家,累计孵化企业 208 余家。如需协助,可直接联系园区服务中心。',
},
{
match: /政策|补贴|优惠|扶持|减免|奖励|资助/,
reply: '园区政策服务覆盖「七位一体」中的政策维度:\n\n· 场地租金减免:符合条件的大学生创业项目可享受场地优惠\n· 创业补贴:协助对接省、市两级创业补贴与一次性创业资助\n· 税费优惠:对接小微企业所得税减免、增值税优惠等政策\n· 项目申报:指导高企认定(目前在园高新技术企业 32 家)、科技型中小企业备案\n\n更多政策细节可咨询园区政策服务窗口。',
},
{
match: /场地|办公室|工位|租金|收费|面积|多少钱|价格/,
reply: '园区总建筑面积 1.2 万㎡,按功能划分为三个区域:\n\n· 加速区(A1–A12):成长型项目办公室,共 11 间独立办公室 + 1 间园区管理办公室\n· 国际区(I1–I7):面向国际合作的办公空间(7 间)\n· 成长区(G1–G8):初创团队成长空间(8 间)\n\n租金按创业项目阶段与政策给予差异化优惠,具体可到园区服务中心咨询,或询问「入驻流程」。',
},
{
match: /AI|模型|算力|tokens|token|工具|智能|赋能|大模型/,
reply: '园区 AI 赋能服务是「七位一体」的核心特色:\n\n· 大模型推理:接入 DeepSeek-V3、通义千问、智谱 GLM-4、豆包、讯飞星火等多模型\n· 工具调用:文档生成、数据查询、图像创作、代码执行、语音合成等智能工具\n· 算力与成本:实时提供 token 消耗统计与速率监控,帮助企业控制 AI 使用成本\n\n今日园区 AI 推理任务持续运行,多模型负载均衡调度,服务节点 12/12 在线。',
},
{
match: /加速区|国际区|成长区|分区|区域|区别|布局/,
reply: '园区按企业成长阶段划分三个区域:\n\n· 加速区(蓝色):面向已具备产品与市场验证的成长型项目,提供独立办公室(A1–A12),目前在驻 9 家企业\n· 国际区(青色):面向国际合作与跨境业务项目(I1–I7),在驻 7 家\n· 成长区(绿色):面向初创团队的共享成长空间(G1–G8),在驻 8 家\n\n园区管理办公室位于加速区 M 房间,提供一站式服务。',
},
{
match: /企业|公司|名录|名单|入驻企业|有哪些企业|查.*企业/,
reply: '园区目前有 39 家入驻企业,分布在三个区域:\n\n· 加速区:云南派音人工智能科技、米勒克尔蓝宝石珠宝、云南宸中低空经济等 9 家\n· 国际区:仰光客厅、中越生物医疗、酷享野农AI农业等 7 家\n· 成长区:Facebook越南跨境电商、昆明云韵体育、云南星瑞航空等 8 家\n\n您也可以直接查看右侧「企业分布」面板,按分区浏览企业名录与房间号。',
},
{
match: /融资|投资|资金|贷款|估值|资本/,
reply: '园区融资服务包含:\n\n· 融资对接:定期组织路演与投资机构对接活动\n· 孵化估值:累计孵化企业估值约 5.2 亿元\n· 金融支持:协助对接银行创业贷款、政府创业担保贷款\n· 财务辅导:提供融资方案设计与商业计划书辅导\n\n有融资需求的企业可向园区服务中心提交商业计划书。',
},
{
match: /就业|招聘|岗位|人才|用工/,
reply: '园区累计带动就业 2,186 人(其中高校毕业生 1,680 人,实习岗位 506 个)。\n\n· 企业招聘:在驻企业持续发布岗位(今日新增多个岗位)\n· 人才对接:与在昆高校共建实习实训基地\n· 创业带动:园区鼓励以创业带动就业,多个项目已形成稳定用工\n\n具体招聘岗位可关注园区实时动态与入驻企业名录。',
},
{
match: /导师|培训|课程|辅导|孵化服务|孵化器/,
reply: '园区配备创业导师 58 位,提供全周期孵化服务:\n\n· 一对一辅导:创业导师定期坐诊,覆盖商业模式、法律财税、市场营销\n· 培训课程:创业训练营、行业专题沙龙、政策解读会\n· 资源对接:技术、供应链、渠道等产业资源链接\n· 公共服务:工商注册、知识产权、法律咨询一站式代办',
},
{
match: /你好|您好|hi|hello|嗨|在吗|hello/,
reply: '您好!我是 OPC 园区 AI 智能助手。\n\n可以问我:入驻流程、政策扶持、场地收费、AI 赋能、企业名录、融资服务等问题,很高兴为您服务。',
},
{
match: /谢谢|感谢|好的|明白|ok/,
reply: '不客气,随时为您服务。您也可以点击右侧「企业分布」查看入驻企业,或继续向我提问。',
},
];
const DEFAULT_REPLY = '园区提供「空间 + 孵化 + 融资 + 政策 + 资源 + AI 赋能 + 综合服务」七位一体服务,在园项目 158 家,累计带动就业 2,186 人,累计营收 2.08 亿元。\n\n您可以这样问我:\n· 如何申请入驻园区?\n· 园区有哪些政策扶持?\n· AI 赋能服务有哪些?';
function getReply(input) {
const text = String(input || '').trim();
for (const rule of RULES) {
if (rule.match.test(text)) return rule.reply;
}
return DEFAULT_REPLY;
}
/* ---------- 消息 ---------- */
let msgId = 0;
const nextId = () => ++msgId;
export default function AiChatPanel() {
const [msgs, setMsgs] = useState(() => [
{ id: nextId(), role: 'ai', text: WELCOME },
]);
const [input, setInput] = useState('');
const [typing, setTyping] = useState(false);
const scrollRef = useRef(null);
const timerRef = useRef(null);
// 新消息自动滚动到底部
useEffect(() => {
const el = scrollRef.current;
if (el) el.scrollTop = el.scrollHeight;
}, [msgs, typing]);
useEffect(() => () => clearTimeout(timerRef.current), []);
const send = useCallback((raw) => {
const text = String(raw || '').trim();
if (!text || typing) return;
setInput('');
setMsgs((prev) => [...prev, { id: nextId(), role: 'me', text }]);
setTyping(true);
const reply = getReply(text);
timerRef.current = setTimeout(() => {
setMsgs((prev) => [...prev, { id: nextId(), role: 'ai', text: reply }]);
setTyping(false);
}, 700 + Math.random() * 600);
}, [typing]);
const clear = useCallback(() => {
clearTimeout(timerRef.current);
setTyping(false);
setMsgs([{ id: nextId(), role: 'ai', text: WELCOME }]);
}, []);
return (
<section className="bd-chat">
{/* 头部 */}
<div className="bd-chat-head">
<span className="bd-chat-logo"><Icon name="robot" size={17} /></span>
<div className="bd-chat-titles">
<b>AI 智能助手</b>
<span>OPC 园区知识库 · 随时为您解答</span>
</div>
<span className="bd-chat-model"><i />DeepSeek-V3</span>
<button className="bd-chat-clear" title="清空对话" onClick={clear}>
<Icon name="eraser" size={15} />
</button>
</div>
{/* 消息区 */}
<div className="bd-chat-msgs" ref={scrollRef}>
{msgs.map((m) => (
<div className={`bd-msg ${m.role}`} key={m.id}>
{m.role === 'ai' && (
<span className="bd-msg-avatar"><Icon name="robot" size={13} /></span>
)}
<div className="bd-msg-bubble">{m.text}</div>
</div>
))}
{typing && (
<div className="bd-msg ai">
<span className="bd-msg-avatar"><Icon name="robot" size={13} /></span>
<div className="bd-msg-bubble bd-typing"><i /><i /><i /></div>
</div>
)}
</div>
{/* 快捷提问 */}
<div className="bd-chat-quick">
{QUICK_QUESTIONS.map((q) => (
<button key={q} onClick={() => send(q)}>
<Icon name="sparkles" size={12} />
{q}
</button>
))}
</div>
{/* 输入区 */}
<div className="bd-chat-input">
<input
value={input}
placeholder="输入你的问题,如:如何申请入驻园区?"
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.nativeEvent.isComposing) {
e.preventDefault();
send(input);
}
}}
/>
<button
className="bd-chat-send"
disabled={!input.trim() || typing}
onClick={() => send(input)}
title="发送"
>
<Icon name="send" size={16} />
</button>
</div>
</section>
);
}
+7
View File
@@ -10,6 +10,7 @@ import {
Heart, Pill, Dna, Microscope, Atom, MapPin, Flag, Award, Medal,
History, Drone, Plane, Rocket, Satellite, Wallet, UserPlus, Activity,
Volume, VolumeOff, ChevronLeft, ChevronRight, PlayerPlay, PlayerPause,
Send, Trash, Eraser, Headset, CircleDot,
} from '@appica/icons-react';
/* 名称 → 图标组件映射(推荐的调用方式:icon="bolt" */
@@ -73,6 +74,11 @@ const NAME_MAP = {
'player-pause': PlayerPause,
play: PlayerPlay,
pause: PlayerPause,
send: Send,
trash: Trash,
eraser: Eraser,
headset: Headset,
'circle-dot': CircleDot,
};
/**
@@ -105,4 +111,5 @@ export {
Heart, Pill, Dna, Microscope, Atom, MapPin, Flag, Award, Medal,
History, Drone, Plane, Rocket, Satellite, Wallet, UserPlus, Activity,
Volume, VolumeOff, ChevronLeft, ChevronRight, PlayerPlay, PlayerPause,
Send, Trash, Eraser, Headset, CircleDot,
};
+1
View File
@@ -12,6 +12,7 @@ import { Avatar, AvatarImage, AvatarFallback } from '@appica/ui-react/avatar'
const STATUS_BY_PATH = {
'/': '数据实时',
'/twin': '3D 实时',
'/ai': 'AI 助手',
'/screen': '媒体播放',
};
+151
View File
@@ -0,0 +1,151 @@
import { useState, useEffect } from 'react';
import Icon from './Icons';
/* =========================================================
园区企业分布侧栏(共享)—— 数字孪生 / AI 智能助手页右侧保持一致
分区切换 + 企业名录 + 企业详情 + 园区实时动态
========================================================= */
/* ---------- 企业数据(名称 / 区域 / 房间号) ---------- */
export const COMPANIES = [
{ name: '云南派音人工智能科技', zone: '加速区', room: 'A1', color: '#4c8dff' },
{ name: '米勒克尔蓝宝石珠宝', zone: '加速区', room: 'A2', color: '#4c8dff' },
{ name: '中泰研学合作', zone: '加速区', room: 'A3', color: '#4c8dff' },
{ name: '云南宸中低空经济', zone: '加速区', room: 'A4', color: '#4c8dff' },
{ name: '昆明智海银高文化科技', zone: '加速区', room: 'A5', color: '#4c8dff' },
{ name: 'AI机器人大模型训练', zone: '加速区', room: 'A6', color: '#4c8dff' },
{ name: '云南廷秀文旅康养', zone: '加速区', room: 'A7', color: '#4c8dff' },
{ name: '瀚颖AI+教育信息咨询', zone: '加速区', room: 'A8', color: '#4c8dff' },
{ name: '云南大学AI+创业平台', zone: '加速区', room: 'A9', color: '#4c8dff' },
{ name: '仰光客厅', zone: '国际区', room: 'I1', color: '#22d3ee' },
{ name: '云南上古绝学文化', zone: '国际区', room: 'I2', color: '#22d3ee' },
{ name: '中越生物医疗', zone: '国际区', room: 'I3', color: '#22d3ee' },
{ name: '酷享野农AI农业', zone: '国际区', room: 'I4', color: '#22d3ee' },
{ name: '滇缅国际设计', zone: '国际区', room: 'I5', color: '#22d3ee' },
{ name: '昆明舒诺生物科技', zone: '国际区', room: 'I6', color: '#22d3ee' },
{ name: '达岸教育管理', zone: '国际区', room: 'I7', color: '#22d3ee' },
{ name: 'Facebook越南跨境电商', zone: '成长区', room: 'G1', color: '#34d399' },
{ name: '研X同行者网络', zone: '成长区', room: 'G2', color: '#34d399' },
{ name: '朵哈·玫瑰特色产业链', zone: '成长区', room: 'G3', color: '#34d399' },
{ name: '昆明云韵体育', zone: '成长区', room: 'G4', color: '#34d399' },
{ name: '南菌优培食用菌', zone: '成长区', room: 'G5', color: '#34d399' },
{ name: '五华区丽裳文化', zone: '成长区', room: 'G6', color: '#34d399' },
{ name: '云南星瑞航空', zone: '成长区', room: 'G7', color: '#34d399' },
{ name: '综合直播私域平台', zone: '成长区', room: 'G8', color: '#34d399' },
{ name: '园区管理办公室', zone: '园区管理', room: 'M', color: '#fbbf24' },
];
export const ZONES = ['加速区', '国际区', '成长区'];
export const ZONE_COLORS = { 加速区: '#4c8dff', 国际区: '#22d3ee', 成长区: '#34d399' };
/* ---------- 工具 ---------- */
const nowTime = () => new Date().toLocaleTimeString('zh-CN', { hour12: false });
const pick = (arr) => arr[Math.floor(Math.random() * arr.length)];
const TOOL_NAMES = ['文档生成', '数据查询', '图像创作', '代码执行', '语音合成'];
const MODEL_NAMES = ['DeepSeek-V3', '通义千问', '智谱 GLM-4', '豆包', '讯飞星火'];
const COMPANY_NAMES = COMPANIES.map((c) => c.name);
function genEvent() {
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() },
];
}
export default function ParkSidePanel() {
const [feed, setFeed] = useState(initFeed);
const [activeZone, setActiveZone] = useState('加速区');
const [selected, setSelected] = useState(null);
// 实时动态滚动
useEffect(() => {
const iv = setInterval(() => {
setFeed((prev) => [genEvent(), ...prev].slice(0, 6));
}, 2600);
return () => clearInterval(iv);
}, []);
const list = COMPANIES.filter((c) => c.zone === activeZone);
return (
<aside className="bd-twin-side">
{/* 分区切换 */}
<div className="bd-twin-zones">
{ZONES.map((z) => (
<button
key={z}
className={`bd-twin-zone${activeZone === z ? ' active' : ''}`}
style={activeZone === z ? { borderColor: ZONE_COLORS[z], color: ZONE_COLORS[z] } : undefined}
onClick={() => { setActiveZone(z); setSelected(null); }}
>
{z}
<em>{COMPANIES.filter((c) => c.zone === z).length} </em>
</button>
))}
</div>
{/* 企业列表 */}
<div className="bd-twin-list">
{list.map((c) => (
<button
key={c.room}
className={`bd-twin-item${selected && selected.room === c.room ? ' active' : ''}`}
onClick={() => setSelected(c)}
>
<span className="bd-twin-room" style={{ color: c.color, borderColor: c.color }}>{c.room}</span>
<span className="bd-twin-name">{c.name}</span>
</button>
))}
</div>
{/* 选中企业详情 */}
{selected && (
<div className="bd-twin-detail">
<div className="bd-twin-detail-head">
<b>{selected.name}</b>
<span style={{ color: ZONE_COLORS[activeZone] }}>{selected.zone}</span>
</div>
<div className="bd-twin-detail-meta">
<span>房间 <b>{selected.room}</b></span>
<span>状态 <b className="ok">在驻</b></span>
</div>
</div>
)}
{/* 实时动态 */}
<div className="bd-card bd-feed-card bd-twin-feed">
<div className="bd-card-head">
<div className="bd-title">园区实时动态</div>
<span className="bd-pill-tag red"> LIVE</span>
</div>
<div className="bd-feed">
{feed.map((e) => (
<div className="bd-feed-item" key={e.id}>
<span className="bd-feed-ico"><Icon name={e.icon} size={15} /></span>
<span className="bd-feed-text">{e.text}</span>
<span className="bd-feed-time">{e.time}</span>
</div>
))}
</div>
</div>
</aside>
);
}