feat: 图标系统与共享组件

- Icons.jsx:Appica SVG 图标统一封装(零 emoji,名称映射)
- PageHeader.jsx:两页共享页眉(标题/副标题/时钟/导航)
- CompanySearch.jsx:入驻企业 Autocomplete 搜索(Appica)
This commit is contained in:
Pine
2026-08-17 18:37:21 +08:00
parent 0d20e1413f
commit cffc1e7a29
3 changed files with 260 additions and 0 deletions
+102
View File
@@ -0,0 +1,102 @@
import { useState } from 'react';
import {
Autocomplete,
AutocompleteInput,
AutocompleteContent,
AutocompleteEmpty,
AutocompleteList,
AutocompleteItem,
} from '@appica/ui-react/autocomplete';
import { BuildingSkyscraper, BuildingCommunity, UsersGroup, Rocket, Search } from '@appica/icons-react';
/* =========================================================
入驻企业搜索(Appica Autocomplete
按名称搜索企业,显示区域/行业/所在房间
========================================================= */
const ZONE_META = {
'加速区': { icon: Rocket, color: '#4c8dff' },
'国际区': { icon: BuildingCommunity, color: '#22d3ee' },
'成长区': { icon: UsersGroup, color: '#34d399' },
'OPC 空间': { icon: BuildingSkyscraper, color: '#fbbf24' },
'孵化加速区': { icon: Rocket, color: '#4c8dff' },
'国际创客区': { icon: BuildingCommunity, color: '#22d3ee' },
'成长苗圃区': { icon: UsersGroup, color: '#34d399' },
};
export default function CompanySearch({ companies }) {
// value 存字符串(企业名),onValueChange 收到的是 item 对象 → 取 name
const [value, setValue] = useState('');
const selected = companies.find((c) => c.name === value);
return (
<div className="bd-search">
<Autocomplete
items={companies}
value={value}
onValueChange={(v) => setValue(typeof v === 'string' ? v : (v && v.name) || '')}
itemToStringValue={(item) => (item && item.name) || ''}
clearable
icon
size="md"
>
<AutocompleteInput
placeholder="搜索入驻企业…"
aria-label="搜索入驻企业"
startSlot={selected
? (() => {
const meta = ZONE_META[selected.zone];
const Ico = meta ? meta.icon : BuildingSkyscraper;
return <Ico size={16} style={{ color: meta ? meta.color : '#8aa0c0' }} aria-hidden />;
})()
: <Search size={16} aria-hidden />}
/>
<AutocompleteContent
side="bottom"
align="end"
sideOffset={6}
className="bd-search-pop"
positionerProps={{ className: 'bd-search-pos' }}
>
<AutocompleteEmpty>未找到匹配企业</AutocompleteEmpty>
<AutocompleteList>
{(item) => {
const meta = ZONE_META[item.zone];
const ZoneIco = meta ? meta.icon : BuildingSkyscraper;
return (
<AutocompleteItem key={item.name} value={item}>
<span className="bd-search-item">
<span className="bd-search-zone" style={{ color: meta ? meta.color : '#8aa0c0' }}>
<ZoneIco size={15} data-icon="start" aria-hidden />
</span>
<span className="bd-search-info">
<b>{item.name}</b>
<em>{item.zone} · {item.industry}</em>
</span>
<span className="bd-search-room">{item.room}</span>
</span>
</AutocompleteItem>
);
}}
</AutocompleteList>
</AutocompleteContent>
</Autocomplete>
{selected && (
<div className="bd-search-result">
<div className="bd-search-result-head">
<span className="bd-search-result-name">{selected.name}</span>
<span className="bd-search-result-zone" style={{ color: (ZONE_META[selected.zone] || {}).color }}>
{selected.zone}
</span>
</div>
<div className="bd-search-result-meta">
<span>行业 <b>{selected.industry}</b></span>
<span>房间 <b>{selected.room}</b></span>
<span>状态 <b className="ok">在驻</b></span>
</div>
</div>
)}
</div>
);
}
+98
View File
@@ -0,0 +1,98 @@
/* =========================================================
图标库:Appica Icons@appica/icons-react
禁止使用表情图标,统一从这里引用 SVG 图标
========================================================= */
import {
Bolt, Wrench, BuildingSkyscraper, Users, UsersGroup, Coin, Radio, Trophy,
Flask, BuildingFactory, Diamond, GraduationCap, ChartLine, ChartArea,
Clock, Robot, Broadcast, Message, Briefcase, Flame, Leaf, Star,
Check, Gauge, Cpu, Server, Database, Sparkles, Bulb, Settings,
Heart, Pill, Dna, Microscope, Atom, MapPin, Flag, Award, Medal,
History, Drone, Plane, Rocket, Satellite, Wallet, UserPlus, Activity,
} from '@appica/icons-react';
/* 名称 → 图标组件映射(推荐的调用方式:icon="bolt" */
const NAME_MAP = {
bolt: Bolt,
wrench: Wrench,
building: BuildingSkyscraper,
'building-skyscraper': BuildingSkyscraper,
'building-community': UsersGroup,
users: Users,
'users-group': UsersGroup,
coin: Coin,
radio: Radio,
trophy: Trophy,
flask: Flask,
factory: BuildingFactory,
'building-factory': BuildingFactory,
diamond: Diamond,
graduation: GraduationCap,
'graduation-cap': GraduationCap,
'chart-line': ChartLine,
'chart-area': ChartArea,
clock: Clock,
robot: Robot,
broadcast: Broadcast,
message: Message,
briefcase: Briefcase,
flame: Flame,
leaf: Leaf,
star: Star,
check: Check,
gauge: Gauge,
cpu: Cpu,
server: Server,
database: Database,
sparkles: Sparkles,
bulb: Bulb,
settings: Settings,
heart: Heart,
pill: Pill,
dna: Dna,
microscope: Microscope,
atom: Atom,
'map-pin': MapPin,
flag: Flag,
award: Award,
medal: Medal,
history: History,
drone: Drone,
plane: Plane,
rocket: Rocket,
satellite: Satellite,
wallet: Wallet,
'user-plus': UserPlus,
activity: Activity,
};
/**
* Icon —— 统一图标组件
* @param {string} name 图标标识(emoji 兼容 或 名称,如 "bolt" / "chart-line"
* @param {number|string} size
* @param {number} strokeWidth
* @param {string} className
*/
export default function Icon({ name, size = 18, strokeWidth = 1.6, className = '', ...rest }) {
const Comp = NAME_MAP[name] || null;
if (!Comp) return null;
return (
<Comp
size={size}
strokeWidth={strokeWidth}
className={`appica-ico${className ? ` ${className}` : ''}`}
aria-hidden="true"
{...rest}
/>
);
}
/* 独立导出的常用图标(组件内直接引用) */
export {
Bolt, Wrench, BuildingSkyscraper, Users, Coin, Radio, Trophy,
Flask, BuildingFactory, Diamond, GraduationCap, ChartLine, ChartArea,
Clock, Robot, Broadcast, Message, Briefcase, Flame, Leaf, Star,
Check, Gauge, Cpu, Server, Database, Sparkles, Bulb, Settings,
Heart, Pill, Dna, Microscope, Atom, MapPin, Flag, Award, Medal,
History, Drone, Plane, Rocket, Satellite, Wallet, UserPlus, Activity,
};
+60
View File
@@ -0,0 +1,60 @@
import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import Icon from './Icons';
/* =========================================================
共享页眉 —— 两个大屏页面(数据大屏 / 数字孪生)统一风格
仅通过 props 区分:导航目标、状态文案
========================================================= */
export default function PageHeader({ nav, status = '数据实时' }) {
const navigate = useNavigate();
const [now, setNow] = useState(new Date());
useEffect(() => {
const iv = setInterval(() => setNow(new Date()), 1000);
return () => clearInterval(iv);
}, []);
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 });
return (
<header className="bd-header">
<div className="bd-header-left">
<span className="bd-logo">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinejoin="round">
<path d="M3 21V9l6-5 6 5v12" />
<path d="M9 21v-6h6v6" />
<path d="M15 9.5V21" />
</svg>
</span>
<div className="bd-header-titles">
<h1 className="bd-header-title">昆明市大学生创业园 <span className="bd-header-title-accent">· OPC 智能园区数字运营中心</span></h1>
<div className="bd-header-sub">云南省首家政府主办大学生创业孵化园区 · 空间+孵化+融资+政策+资源+AI赋能+综合服务 七位一体</div>
</div>
</div>
<div className="bd-header-right">
{nav && (
<button
className="bd-nav-hint"
onClick={() => navigate(nav.to)}
title={nav.title}
>
<Icon name={nav.icon || 'chevron-right'} size={14} />
{nav.label}
</button>
)}
<div className="bd-live">
<span className="bd-live-dot" />
{status}
</div>
<div className="bd-clock">
<div className="bd-clock-time">{timeStr}</div>
<div className="bd-clock-date">{dateStr} {week}</div>
</div>
</div>
</header>
);
}