refactor(home): de-productize hero visual to abstract data-driven graphic

- 移除「睿新·经营驾驶舱」产品 UI 外壳(窗口头/侧边栏/表格/产品名)
- Hero 视觉改为抽象数据可视化(KPI + 趋势图),眉标「数据驱动 · 让转型可量化」
- 对齐 IHG/字节式品牌矩阵定位:能力表达而非产品展示
- 首页单测 6/6 + 视觉基线三端更新,type-check / lint 0 errors
This commit is contained in:
2026-08-19 16:34:11 +08:00
parent 6573ab449e
commit cbdc7dd0c8
11 changed files with 58 additions and 228 deletions
@@ -86,11 +86,11 @@ jest.mock('lucide-react', () => {
});
describe('HomeContentV14 - 首页 Hero 优化', () => {
it('renders an SVG product visual with an interface disclaimer', () => {
it('renders an abstract data-driven visual without product UI shell', () => {
render(<HomeContentV14 />);
expect(screen.getByTestId('hero-product-visual')).toBeInTheDocument();
expect(screen.getByText('产品界面示意')).toBeInTheDocument();
expect(screen.getByText('数据驱动 · 让转型可量化')).toBeInTheDocument();
});
it('keeps a single primary CTA and demotes the secondary action to a text link', () => {
+56 -226
View File
@@ -1,26 +1,7 @@
import {
LayoutDashboard,
BarChart3,
Package,
Users,
Settings,
Search,
Bell,
TrendingUp,
AlertCircle,
ArrowUpRight,
ArrowDownRight,
} from 'lucide-react';
import { TrendingUp, ArrowUpRight, ArrowDownRight } from 'lucide-react';
import { cn } from '@/lib/utils';
const NAV_ITEMS = [
{ label: '经营驾驶舱', icon: LayoutDashboard, active: true },
{ label: '财务管理', icon: BarChart3 },
{ label: '供应链', icon: Package },
{ label: '客户管理', icon: Users },
{ label: '系统设置', icon: Settings },
];
// 抽象数据可视化示意(示例数据)——作为品牌「数据驱动」能力的视觉表达,不含具体产品 UI 外壳
const KPIS = [
{ label: '营收同比', value: '+32.6%', trend: 'up', note: '较上期' },
{ label: '订单履约率', value: '98.2%', trend: 'up', note: '目标 95%' },
@@ -28,18 +9,6 @@ const KPIS = [
{ label: '客户新增', value: '+18.4%', trend: 'up', note: '本月' },
] as const;
const ALERTS = [
{ title: '华东区库存预警', desc: '3 个 SKU 低于安全库存', level: 'high' },
{ title: '大客户合同待审批', desc: '2 份合同等待审批', level: 'medium' },
{ title: '本月目标完成 82%', desc: '距离目标还有 18%', level: 'normal' },
];
const TABLE_ROWS = [
{ name: '成都某制造集团', amount: '¥ 1,280,000', status: '已签约', date: '今日' },
{ name: '华南零售连锁', amount: '¥ 860,000', status: '洽谈中', date: '昨日' },
{ name: '华东物流企业', amount: '¥ 420,000', status: '方案确认', date: '07-18' },
];
function TrendBadge({ trend, label }: { trend: 'up' | 'down'; label: string }) {
const Icon = trend === 'up' ? ArrowUpRight : ArrowDownRight;
return (
@@ -50,209 +19,70 @@ function TrendBadge({ trend, label }: { trend: 'up' | 'down'; label: string }) {
);
}
function StatusBadge({ status }: { status: string }) {
const tone =
status === '已签约'
? 'bg-success-bg text-success'
: status === '洽谈中'
? 'bg-brand-bg text-brand'
: 'bg-bg-secondary text-text-secondary';
return (
<span className={cn('inline-flex rounded px-1.5 py-0.5 text-[10px] font-semibold', tone)}>
{status}
</span>
);
}
export function HeroProductVisual({ className }: { className?: string }) {
return (
<div
data-testid="hero-product-visual"
role="img"
aria-label="睿新经营驾驶舱界面示意(示例数据)"
aria-label="数据驱动可视化示意(示例数据)"
className={cn('relative', className)}
>
<div aria-hidden="true" className="overflow-hidden rounded-xl border border-border-primary bg-white shadow-xl">
{/* 应用窗口头 */}
<div className="flex items-center justify-between border-b border-border-primary bg-bg-secondary px-4 py-2.5">
<div className="flex items-center gap-3">
<div className="flex gap-1.5" aria-hidden="true">
<span className="h-2.5 w-2.5 rounded-full bg-border-secondary" />
<span className="h-2.5 w-2.5 rounded-full bg-border-secondary" />
<span className="h-2.5 w-2.5 rounded-full bg-brand/60" />
</div>
<span className="text-sm font-semibold text-ink"> · </span>
</div>
<span className="border border-border-primary bg-white px-2 py-0.5 text-[11px] font-medium text-text-muted">
</span>
</div>
<div className="flex">
{/* 侧边导航(桌面) */}
<aside className="hidden w-40 shrink-0 flex-col border-r border-border-primary bg-bg-secondary/60 p-3 lg:flex" aria-hidden="true">
<div className="mb-5 border-b border-border-primary px-2 pb-4">
<div className="text-sm font-black text-ink"></div>
<div className="text-[10px] font-medium tracking-[0.18em] text-text-muted">NOVALON</div>
</div>
<nav className="space-y-1">
{NAV_ITEMS.map((item) => {
const Icon = item.icon;
return (
<div
key={item.label}
className={cn(
'flex items-center gap-2 rounded px-2 py-1.5 text-xs font-medium',
item.active
? 'bg-brand-bg text-brand'
: 'text-text-secondary'
)}
>
<Icon className="h-3.5 w-3.5" />
{item.label}
</div>
);
})}
</nav>
<div className="mt-auto rounded border border-border-primary bg-white p-2.5">
<div className="text-[10px] font-semibold text-ink"></div>
<div className="mt-0.5 text-[10px] leading-snug text-text-muted"> · </div>
</div>
</aside>
{/* 主内容区 */}
<div className="min-w-0 flex-1 p-3 sm:p-4">
{/* 顶部操作栏 */}
<div className="mb-3 flex items-center justify-between gap-3">
<div className="flex min-w-0 items-center gap-2 rounded border border-border-primary bg-bg-secondary px-2.5 py-1.5 text-text-muted">
<Search className="h-3.5 w-3.5 shrink-0" />
<span className="truncate text-xs">SKU</span>
</div>
<div className="flex items-center gap-2">
<div className="relative flex h-7 w-7 items-center justify-center rounded border border-border-primary bg-white text-text-secondary">
<Bell className="h-3.5 w-3.5" />
<span className="absolute -right-0.5 -top-0.5 h-2 w-2 rounded-full bg-brand" />
</div>
<div className="flex h-7 w-7 items-center justify-center rounded-full bg-ink text-[10px] font-bold text-white">
</div>
</div>
</div>
{/* KPI 卡 */}
<div className="grid grid-cols-2 gap-2 sm:gap-2.5 lg:grid-cols-4">
{KPIS.map((kpi) => (
<div key={kpi.label} className="border border-border-primary bg-bg-secondary p-2.5">
<div className="flex items-center justify-between gap-2">
<span className="truncate text-[11px] font-medium text-text-muted">{kpi.label}</span>
<TrendingUp className="h-3.5 w-3.5 shrink-0 text-brand" />
</div>
<div className="mt-1.5 text-lg font-black tabular-nums leading-none text-ink sm:text-xl">
{kpi.value}
</div>
<TrendBadge trend={kpi.trend} label={kpi.note} />
</div>
))}
</div>
{/* 图表 + 预警 */}
<div className="mt-2.5 grid grid-cols-1 gap-2.5 sm:mt-3 lg:grid-cols-3">
<div className="border border-border-primary p-3 lg:col-span-2">
<div className="mb-3 flex items-center justify-between">
<div>
<div className="text-xs font-semibold text-ink"></div>
<div className="text-[10px] text-text-muted"> · 12 </div>
</div>
<div className="flex gap-1">
{['月', '季', '年'].map((period, i) => (
<span
key={period}
className={cn(
'rounded px-1.5 py-0.5 text-[10px] font-medium',
i === 0 ? 'bg-brand text-white' : 'bg-bg-secondary text-text-muted'
)}
>
{period}
</span>
))}
</div>
</div>
<svg
viewBox="0 0 360 120"
className="h-24 w-full sm:h-28"
preserveAspectRatio="none"
aria-hidden="true"
>
<defs>
<linearGradient id="hero-visual-fill" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="var(--color-brand)" stopOpacity="0.16" />
<stop offset="100%" stopColor="var(--color-brand)" stopOpacity="0" />
</linearGradient>
</defs>
{[24, 56, 88].map((y) => (
<line key={y} x1="0" y1={y} x2="360" y2={y} stroke="var(--color-border-light)" strokeWidth="1" />
))}
<path
d="M0,104 C36,96 56,80 88,82 C120,84 144,60 176,58 C208,56 232,38 264,40 C296,42 320,24 360,20 L360,120 L0,120 Z"
fill="url(#hero-visual-fill)"
/>
<path
d="M0,104 C36,96 56,80 88,82 C120,84 144,60 176,58 C208,56 232,38 264,40 C296,42 320,24 360,20"
fill="none"
stroke="var(--color-brand)"
strokeWidth="2.5"
strokeLinecap="round"
/>
<circle cx="360" cy="20" r="4" fill="var(--color-brand)" />
</svg>
</div>
<div className="border border-border-primary p-3">
<div className="mb-3 flex items-center gap-1.5">
<AlertCircle className="h-3.5 w-3.5 text-brand" />
<span className="text-xs font-semibold text-ink"></span>
</div>
<ul className="space-y-2.5">
{ALERTS.map((alert) => (
<li key={alert.title} className="flex items-start gap-2">
<span
className={cn(
'mt-1 h-1.5 w-1.5 shrink-0 rounded-full',
alert.level === 'high' ? 'bg-brand' : 'bg-border-secondary'
)}
aria-hidden="true"
/>
<div className="min-w-0">
<div className="truncate text-xs font-medium text-ink">{alert.title}</div>
<div className="truncate text-[10px] text-text-muted">{alert.desc}</div>
</div>
</li>
))}
</ul>
</div>
</div>
{/* 数据表格 */}
<div className="mt-2.5 hidden overflow-hidden border border-border-primary 2xl:block">
<div className="flex items-center justify-between border-b border-border-primary bg-bg-secondary px-3 py-2">
<span className="text-xs font-semibold text-ink"></span>
<span className="text-[10px] text-text-muted"></span>
</div>
<div className="divide-y divide-border-light">
{TABLE_ROWS.map((row) => (
<div key={row.name} className="grid grid-cols-[1fr_auto_auto] items-center gap-3 px-3 py-2">
<span className="truncate text-xs font-medium text-ink">{row.name}</span>
<span className="text-xs font-semibold tabular-nums text-text-secondary">{row.amount}</span>
<div className="flex items-center gap-2">
<StatusBadge status={row.status} />
<span className="text-[10px] text-text-muted">{row.date}</span>
</div>
</div>
))}
</div>
{/* 眉标:能力定位,非产品名 */}
<div className="mb-4 inline-flex items-center gap-2 text-xs font-bold tracking-[0.2em] uppercase text-brand">
<span className="h-1.5 w-1.5 rounded-full bg-brand" aria-hidden="true" />
·
</div>
{/* KPI 卡片:抽象数据可视化 */}
<div className="grid grid-cols-2 gap-3">
{KPIS.map((kpi) => (
<div key={kpi.label} className="border border-border-primary bg-bg-secondary p-4">
<div className="mb-2 flex items-center justify-between gap-2">
<span className="text-[11px] font-medium text-text-muted">{kpi.label}</span>
<TrendingUp className="h-3.5 w-3.5 text-brand" />
</div>
<div className="text-xl font-black tabular-nums leading-none text-ink sm:text-2xl">
{kpi.value}
</div>
<TrendBadge trend={kpi.trend} label={kpi.note} />
</div>
))}
</div>
{/* 趋势图:品牌色曲线 + 渐变填充 */}
<div className="mt-3 border border-border-primary bg-white p-4">
<div className="mb-3 flex items-center justify-between">
<span className="text-xs font-semibold text-ink"></span>
<span className="text-[10px] text-text-muted"> · 12 </span>
</div>
<svg
viewBox="0 0 360 120"
className="h-24 w-full sm:h-28"
preserveAspectRatio="none"
aria-hidden="true"
>
<defs>
<linearGradient id="hero-visual-fill" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="var(--color-brand)" stopOpacity="0.16" />
<stop offset="100%" stopColor="var(--color-brand)" stopOpacity="0" />
</linearGradient>
</defs>
{[24, 56, 88].map((y) => (
<line key={y} x1="0" y1={y} x2="360" y2={y} stroke="var(--color-border-light)" strokeWidth="1" />
))}
<path
d="M0,104 C36,96 56,80 88,82 C120,84 144,60 176,58 C208,56 232,38 264,40 C296,42 320,24 360,20 L360,120 L0,120 Z"
fill="url(#hero-visual-fill)"
/>
<path
d="M0,104 C36,96 56,80 88,82 C120,84 144,60 176,58 C208,56 232,38 264,40 C296,42 320,24 360,20"
fill="none"
stroke="var(--color-brand)"
strokeWidth="2.5"
strokeLinecap="round"
/>
<circle cx="360" cy="20" r="4" fill="var(--color-brand)" />
</svg>
</div>
</div>
);