chore: sync marketing pages, CMS extensions, tests and project docs

同步工作区剩余变更,主要包括:
- 营销页面组件与布局持续优化(about/news/services/solutions/team 等)
- 详情页四层叙事组件、布局组件、UI 组件调整
- CMS 数据模型、API 路由、权限、工作流、站内通知、媒体管理扩展
- 新增/补充单元测试与 E2E 测试(cms-workflow.spec.ts 等)
- ESLint 9 迁移、jest/tsconfig 配置更新、依赖调整
- 新增 ADR、CMS 评估文档、Release Review / Acceptance 报告
- 移除水墨装饰组件与大体积未使用字体文件
This commit is contained in:
张翔
2026-07-25 08:04:01 +08:00
parent e35090b914
commit 10404dbb36
208 changed files with 18107 additions and 8330 deletions
+32 -33
View File
@@ -1,7 +1,7 @@
'use client';
import { ArrowRight, Lock, TrendingUp, Shield } from 'lucide-react';
import { InkGlowCard } from '@/components/ui/ink-glow-card';
import { Card } from '@/components/ui/card';
import type { LucideIcon } from 'lucide-react';
interface ChallengeCardProps {
@@ -28,39 +28,38 @@ export function ChallengeCard({ title, description, scenario, href, index }: Cha
const IconComponent = config.icon;
return (
<InkGlowCard
index={index}
href={href}
>
<div className="p-6 md:p-8">
<div className="flex items-start justify-between mb-5">
<div
className="w-11 h-11 rounded-xl flex items-center justify-center"
style={{ backgroundColor: `rgba(var(--color-brand-rgb), ${config.iconBgOpacity})` }}
>
<IconComponent
className="w-5 h-5 text-[var(--color-brand)]"
strokeWidth={1.8}
/>
<a href={href}>
<Card className="group relative overflow-hidden transition-all duration-300 hover:-translate-y-1 hover:shadow-lg">
<div className="p-6 md:p-8">
<div className="flex items-start justify-between mb-5">
<div
className="w-11 h-11 rounded-xl flex items-center justify-center"
style={{ backgroundColor: `rgba(var(--color-brand-rgb), ${config.iconBgOpacity})` }}
>
<IconComponent
className="w-5 h-5 text-[var(--color-brand)]"
strokeWidth={1.8}
/>
</div>
<span className="text-xs font-mono tracking-widest text-[var(--color-text-subtle)]">
{String(index + 1).padStart(2, '0')}
</span>
</div>
<h3 className="text-xl font-semibold mb-3 leading-tight tracking-tight text-[var(--color-text-primary)]">
{title}
</h3>
<p className="text-sm text-[var(--color-text-muted)] leading-relaxed mb-6 min-h-[3.5rem]">
{description}
</p>
<div className="flex items-center gap-2 text-sm font-medium text-[var(--color-brand)]">
<span>了解方案</span>
<ArrowRight className="w-4 h-4" />
</div>
<span className="text-xs font-mono tracking-widest text-[var(--color-text-subtle)]">
{String(index + 1).padStart(2, '0')}
</span>
</div>
<h3 className="text-xl font-semibold mb-3 leading-tight tracking-tight text-[var(--color-text-primary)]">
{title}
</h3>
<p className="text-sm text-[var(--color-text-muted)] leading-relaxed mb-6 min-h-[3.5rem]">
{description}
</p>
<div className="flex items-center gap-2 text-sm font-medium text-[var(--color-brand)]">
<span>了解方案</span>
<ArrowRight className="w-4 h-4" />
</div>
</div>
</InkGlowCard>
</Card>
</a>
);
}
-295
View File
@@ -1,295 +0,0 @@
'use client';
import { useEffect, useRef, useMemo, useSyncExternalStore } from 'react';
import { motion, useScroll, useTransform } from 'framer-motion';
import { useReducedMotion } from '@/hooks/use-reduced-motion';
const PARTICLE_COUNT = 12;
interface Particle {
x: number;
y: number;
size: number;
duration: number;
delay: number;
opacity: number;
}
function generateParticles(): Particle[] {
const particles: Particle[] = [];
for (let i = 0; i < PARTICLE_COUNT; i++) {
particles.push({
x: Math.random() * 100,
y: Math.random() * 100,
size: Math.max(1.5, Math.random() * 3.5 + 0.5),
duration: 14 + Math.random() * 20,
delay: Math.random() * -20,
opacity: 0.15 + Math.random() * 0.35,
});
}
return particles;
}
function ParallaxLayer({ children }: { children: React.ReactNode }) {
const containerRef = useRef<HTMLDivElement>(null);
const { scrollYProgress } = useScroll({
target: containerRef,
offset: ['start start', 'end start'],
});
const y = useTransform(scrollYProgress, [0, 1], [0, 120]);
const opacity = useTransform(scrollYProgress, [0, 0.6, 1], [1, 0.6, 0]);
return (
<motion.div
ref={containerRef}
className="pointer-events-none absolute inset-0 overflow-hidden"
style={{ y, opacity }}
aria-hidden="true"
>
{children}
</motion.div>
);
}
function InkCanvas({ shouldReduceMotion }: { shouldReduceMotion: boolean }) {
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
if (shouldReduceMotion || !canvasRef.current) {
return undefined;
}
const canvas = canvasRef.current;
const rawCtx = canvas.getContext('2d');
if (!rawCtx) {
return undefined;
}
const ctx = rawCtx;
let animFrameId: number;
let time = 0;
function resize() {
if (!canvasRef.current) {
return;
}
const dpr = window.devicePixelRatio || 1;
const rect = canvasRef.current.getBoundingClientRect();
canvas.width = rect.width * dpr;
canvas.height = rect.height * dpr;
ctx.scale(dpr, dpr);
}
resize();
window.addEventListener('resize', resize);
const inkBlobs = [
{ cx: 0.18, cy: 0.35, r: 0.28, speed: 0.0004, phase: 0 },
{ cx: 0.82, cy: 0.22, r: 0.22, speed: 0.0003, phase: 2 },
{ cx: 0.55, cy: 0.78, r: 0.32, speed: 0.00035, phase: 4 },
];
function draw() {
const w = canvas.getBoundingClientRect().width;
const h = canvas.getBoundingClientRect().height;
ctx.clearRect(0, 0, w, h);
inkBlobs.forEach((blob) => {
const pulse = Math.sin(time * blob.speed + blob.phase) * 0.08 + 1;
const rx = blob.r * w * pulse;
const ry = blob.r * h * pulse;
const gx = blob.cx * w + Math.sin(time * 0.0006 + blob.phase) * w * 0.03;
const gy = blob.cy * h + Math.cos(time * 0.0005 + blob.phase) * h * 0.03;
const grad = ctx.createRadialGradient(gx, gy, 0, gx, gy, Math.max(rx, ry));
grad.addColorStop(0, 'rgba(196, 30, 58, 0.045)');
grad.addColorStop(0.5, 'rgba(196, 30, 58, 0.02)');
grad.addColorStop(1, 'rgba(196, 30, 58, 0)');
ctx.fillStyle = grad;
ctx.fillRect(0, 0, w, h);
});
const particleGrad = ctx.createRadialGradient(w * 0.5, h * 0.4, 0, w * 0.5, h * 0.4, w * 0.6);
particleGrad.addColorStop(0, 'rgba(28, 28, 28, 0.025)');
particleGrad.addColorStop(1, 'rgba(28, 28, 28, 0)');
ctx.fillStyle = particleGrad;
ctx.fillRect(0, 0, w, h);
time += 16;
animFrameId = requestAnimationFrame(draw);
}
animFrameId = requestAnimationFrame(draw);
return () => {
cancelAnimationFrame(animFrameId);
window.removeEventListener('resize', resize);
};
}, [shouldReduceMotion]);
return (
<canvas
ref={canvasRef}
className="absolute inset-0 w-full h-full"
style={{ opacity: 0.8 }}
/>
);
}
function InkParticles({ particles }: { particles: Particle[] }) {
return (
<svg className="absolute inset-0 w-full h-full" xmlns="http://www.w3.org/2000/svg">
<defs>
<filter id="ink-glow">
<feGaussianBlur stdDeviation="40" result="blur" />
<feColorMatrix
type="matrix"
values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1.2 0"
in="blur"
result="glow"
/>
<feMerge>
<feMergeNode in="glow" />
<feMergeNode in="SourceGraphic" />
</feMerge>
</filter>
</defs>
<circle
cx="15%"
cy="40%"
r="18%"
fill="rgba(196, 30, 58, 0.03)"
filter="url(#ink-glow)"
style={{
transformOrigin: '15% 40%',
animation: 'inkBreatheA 10s ease-in-out infinite',
}}
/>
<circle
cx="85%"
cy="25%"
r="14%"
fill="rgba(28, 28, 28, 0.025)"
filter="url(#ink-glow)"
style={{
transformOrigin: '85% 25%',
animation: 'inkBreatheB 13s ease-in-out infinite',
}}
/>
<circle
cx="50%"
cy="85%"
r="22%"
fill="rgba(196, 30, 58, 0.025)"
filter="url(#ink-glow)"
style={{
transformOrigin: '50% 85%',
animation: 'inkBreatheC 11s ease-in-out infinite',
}}
/>
{particles.map((p, i) => (
<circle
key={i}
cx={`${p.x}%`}
cy={`${p.y}%`}
r={p.size}
fill={`rgba(196, 30, 58, ${p.opacity})`}
style={{
transformOrigin: `${p.x}% ${p.y}%`,
animation: `particleFloat ${p.duration}s ease-in-out ${p.delay}s infinite`,
}}
/>
))}
<style>{`
@keyframes inkBreatheA {
0%, 100% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.15); opacity: 0.7; }
}
@keyframes inkBreatheB {
0%, 100% { transform: scale(1); opacity: 0.9; }
50% { transform: scale(1.2); opacity: 0.5; }
}
@keyframes inkBreatheC {
0%, 100% { transform: scale(1); opacity: 0.8; }
50% { transform: scale(1.12); opacity: 0.5; }
}
@keyframes particleFloat {
0%, 100% {
transform: translate(0, 0) scale(1);
opacity: var(--base-opacity, 0.3);
}
25% {
transform: translate(12px, -20px) scale(1.1);
opacity: calc(var(--base-opacity, 0.3) * 1.5);
}
50% {
transform: translate(-8px, -35px) scale(0.9);
opacity: var(--base-opacity, 0.3);
}
75% {
transform: translate(15px, -15px) scale(1.05);
opacity: calc(var(--base-opacity, 0.3) * 1.2);
}
}
`}</style>
</svg>
);
}
function useIsMounted() {
return useSyncExternalStore(
(callback) => {
window.addEventListener('resize', callback);
return () => window.removeEventListener('resize', callback);
},
() => true,
() => false
);
}
export function HeroInkBackground() {
const shouldReduceMotion = useReducedMotion();
const mounted = useIsMounted();
const particles = useMemo(() => {
if (!mounted) {return [];}
return generateParticles();
}, [mounted]);
if (shouldReduceMotion) {
return (
<div
className="pointer-events-none absolute inset-0"
style={{
background:
'radial-gradient(ellipse at 18% 35%, rgba(196, 30, 58, 0.05) 0%, transparent 50%), radial-gradient(ellipse at 82% 22%, rgba(28, 28, 28, 0.04) 0%, transparent 45%), radial-gradient(ellipse at 55% 78%, rgba(196, 30, 58, 0.03) 0%, transparent 40%)',
}}
aria-hidden="true"
/>
);
}
if (!mounted) {
return (
<div
className="pointer-events-none absolute inset-0 relative"
style={{
background:
'radial-gradient(ellipse at 18% 35%, rgba(196, 30, 58, 0.05) 0%, transparent 50%), radial-gradient(ellipse at 82% 22%, rgba(28, 28, 28, 0.04) 0%, transparent 45%)',
}}
aria-hidden="true"
/>
);
}
return (
<div className="pointer-events-none absolute inset-0 relative" aria-hidden="true">
<ParallaxLayer>
<InkCanvas shouldReduceMotion={shouldReduceMotion} />
<InkParticles particles={particles} />
</ParallaxLayer>
</div>
);
}
-3
View File
@@ -183,7 +183,6 @@ export { PageTransition, StaggerChildren } from './page-transition';
export {
EASE_OUT,
GrainOverlay,
FloatingInkParticles,
SectionLabel,
} from './page-decoration';
export {
@@ -192,11 +191,9 @@ export {
GradientDivider,
BrandStamp,
} from './brand-visuals';
export { InkGlowCard } from './ink-glow-card';
export { ChallengeCard } from './challenge-card';
export { ProductCard as UiProductCard } from './product-card';
export { DetailSwipeNav } from './detail-swipe-nav';
export { HeroInkBackground } from './hero-ink-background';
export { FlipClock } from './flip-clock';
export { AnimatedCounter } from './animated-counter';
export { MetricCard } from './metric-card';
-106
View File
@@ -1,106 +0,0 @@
'use client';
import { useRef, useState, useCallback, type ReactNode } from 'react';
import { motion } from 'framer-motion';
import { StaticLink } from '@/components/ui/static-link';
interface InkGlowCardProps {
children: ReactNode;
index?: number;
className?: string;
href?: string;
onClick?: () => void;
}
const DEFAULT_ACCENT_RGB = '196, 30, 58';
export function InkGlowCard({
children,
index = 0,
className = '',
href,
onClick,
}: InkGlowCardProps) {
const cardRef = useRef<HTMLDivElement>(null);
const [mousePos, setMousePos] = useState({ x: 0, y: 0 });
const [isHovered, setIsHovered] = useState(false);
const handleMouseMove = useCallback((e: React.MouseEvent) => {
if (!cardRef.current) {return;}
const rect = cardRef.current.getBoundingClientRect();
setMousePos({
x: e.clientX - rect.left,
y: e.clientY - rect.top,
});
}, []);
const content = (
<>
<div
className="absolute inset-0 pointer-events-none transition-opacity duration-500"
style={{
opacity: isHovered ? 1 : 0,
background: `radial-gradient(400px circle at ${mousePos.x}px ${mousePos.y}px, rgba(${DEFAULT_ACCENT_RGB}, 0.04), transparent 40%)`,
}}
/>
<div className="relative z-10">{children}</div>
</>
);
return (
<motion.div
ref={cardRef}
initial={{ opacity: 0, scale: 0.96, y: 16 }}
whileInView={{ opacity: 1, scale: 1, y: 0 }}
viewport={{ once: true, margin: '-60px' }}
transition={{
duration: 0.5,
delay: index * 0.08,
ease: [0.16, 1, 0.3, 1],
}}
className={`relative ink-glow-border rounded-2xl ${className}`}
style={
{
'--glow-start': 'var(--color-brand)',
'--glow-end': 'var(--color-warning)',
} as React.CSSProperties
}
>
{href ? (
<StaticLink
href={href}
className="relative block rounded-2xl bg-[var(--color-bg-primary)] overflow-hidden transition-all duration-500"
style={{
boxShadow: isHovered
? `0 20px 40px rgba(0,0,0,0.1), 0 0 0 1px rgba(${DEFAULT_ACCENT_RGB}, 0.12)`
: '0 1px 3px rgba(0,0,0,0.04), 0 0 0 1px rgba(0,0,0,0.06)',
transform: isHovered ? 'translateY(-6px)' : 'translateY(0)',
}}
onMouseMove={handleMouseMove}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
{content}
</StaticLink>
) : (
<div
className="relative rounded-2xl bg-[var(--color-bg-primary)] overflow-hidden transition-all duration-500"
style={{
boxShadow: isHovered
? `0 20px 40px rgba(0,0,0,0.1), 0 0 0 1px rgba(${DEFAULT_ACCENT_RGB}, 0.12)`
: '0 1px 3px rgba(0,0,0,0.04), 0 0 0 1px rgba(0,0,0,0.06)',
transform: isHovered ? 'translateY(-6px)' : 'translateY(0)',
}}
onMouseMove={handleMouseMove}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
onClick={onClick}
role={onClick ? 'button' : undefined}
tabIndex={onClick ? 0 : undefined}
>
{content}
</div>
)}
</motion.div>
);
}
-101
View File
@@ -1,101 +0,0 @@
'use client';
import { type ReactNode, useRef } from 'react';
import { useMouseGlow } from '@/hooks/use-mouse-glow';
import { cn } from '@/lib/utils';
interface InkCardProps {
children: ReactNode;
className?: string;
/** Enable ink-glow border on hover */
glow?: boolean;
/** Enable mouse follow glow */
mouseFollow?: boolean;
/** Card padding variant */
padding?: 'none' | 'sm' | 'md' | 'lg';
/** Interactive - adds hover elevation */
interactive?: boolean;
onClick?: () => void;
href?: string;
}
const paddingMap = {
none: '',
sm: 'p-4 sm:p-5',
md: 'p-5 sm:p-6 md:p-7',
lg: 'p-6 sm:p-8 md:p-10',
} as const;
/**
* 水墨雅致统一卡片组件
* 融合 ink-glow-border + mouse-follow-glow + 宣纸底色
*/
export function InkCard({
children,
className,
glow = true,
mouseFollow = true,
padding = 'md',
interactive = true,
onClick,
href,
}: InkCardProps) {
const { glowStyle, handlers } = useMouseGlow({ radius: 350, opacity: 0.05 });
const anchorRef = useRef<HTMLAnchorElement>(null);
const divRef = useRef<HTMLDivElement>(null);
const cardClasses = cn(
'block relative rounded-2xl',
'bg-[var(--color-bg-primary)]',
'border border-[var(--color-border-primary)]',
glow && 'ink-glow-border',
interactive && 'transition-all duration-300 hover:border-[rgba(var(--color-brand-rgb),0.2)] hover:shadow-lg',
paddingMap[padding],
className,
);
const content = (
<>
{/* Mouse follow glow overlay */}
{mouseFollow && <div style={glowStyle} />}
{/* Card content */}
<div className="relative z-[1]">{children}</div>
</>
);
if (href) {
const isExternal = href.startsWith('http://') || href.startsWith('https://');
const handleLinkClick = !isExternal
? (e: React.MouseEvent<HTMLAnchorElement>) => {
// 阻止 Next.js 客户端路由拦截(output: 'export' 模式下 RSC payload 不存在)
e.preventDefault();
window.location.href = href;
}
: undefined;
return (
<a
ref={anchorRef}
href={href}
className={cardClasses}
onClick={handleLinkClick}
{...handlers}
style={glow ? { '--glow-start': 'var(--color-text-muted)', '--glow-end': 'var(--color-border-primary)' } as React.CSSProperties : undefined}
>
{content}
</a>
);
}
return (
<div
ref={divRef}
className={cardClasses}
onClick={onClick}
{...handlers}
style={glow ? { '--glow-start': 'var(--color-text-muted)', '--glow-end': 'var(--color-border-primary)' } as React.CSSProperties : undefined}
>
{content}
</div>
);
}
@@ -1,58 +0,0 @@
'use client';
interface InkWashBackgroundProps {
/** Background variant */
variant?: 'hero' | 'section' | 'subtle';
className?: string;
}
/**
* 水墨晕散背景组件
* 用于 Hero 和各大 section 的水墨晕散效果
*/
export function InkWashBackground({ variant = 'section', className }: InkWashBackgroundProps) {
if (variant === 'hero') {
return (
<div className={`absolute inset-0 overflow-hidden pointer-events-none ${className ?? ''}`} aria-hidden="true">
{/* 主墨晕 */}
<div
className="absolute top-[10%] left-[15%] w-[600px] h-[600px] rounded-full"
style={{
background: 'radial-gradient(circle, rgba(var(--color-brand-rgb), 0.04) 0%, transparent 60%)',
}}
/>
{/* 品牌红点缀 */}
<div
className="absolute bottom-[20%] right-[10%] w-[400px] h-[400px] rounded-full"
style={{
background: 'radial-gradient(circle, rgba(var(--color-brand-rgb), 0.025) 0%, transparent 55%)',
}}
/>
{/* 宣纸纹理 */}
<div className="absolute inset-0 bg-paper-texture opacity-50" />
</div>
);
}
if (variant === 'subtle') {
return (
<div className={`absolute inset-0 overflow-hidden pointer-events-none ${className ?? ''}`} aria-hidden="true">
<div className="absolute inset-0 bg-paper-texture opacity-30" />
</div>
);
}
// Default: section
return (
<div className={`absolute inset-0 overflow-hidden pointer-events-none ${className ?? ''}`} aria-hidden="true">
<div
className="absolute top-0 left-1/2 -translate-x-1/2 w-[800px] h-[400px]"
style={{
background: 'radial-gradient(ellipse at 50% 0%, rgba(var(--color-brand-rgb), 0.025) 0%, transparent 65%)',
}}
/>
<div className="absolute inset-0 bg-paper-texture opacity-30" />
</div>
);
}
-2
View File
@@ -1,2 +0,0 @@
export { InkCard } from './InkCard';
export { InkWashBackground } from './InkWashBackground';
-53
View File
@@ -1,20 +1,9 @@
'use client';
import { motion } from 'framer-motion';
import { cn } from '@/lib/utils';
import { useReducedMotion } from '@/hooks/use-reduced-motion';
export const EASE_OUT = [0.22, 1, 0.36, 1] as const;
const DEFAULT_PARTICLES = [
{ x: 15, y: 32, scale: 0.8, duration: 10, delay: 0, size: 3 },
{ x: 82, y: 58, scale: 0.6, duration: 12, delay: 1.5, size: 2 },
{ x: 45, y: 42, scale: 1.0, duration: 9, delay: 0.8, size: 4 },
{ x: 75, y: 25, scale: 0.7, duration: 11, delay: 2.2, size: 2.5 },
{ x: 25, y: 75, scale: 0.9, duration: 8.5, delay: 1.0, size: 3.5 },
{ x: 58, y: 65, scale: 0.55, duration: 13, delay: 2.8, size: 2 },
];
export function GrainOverlay({ opacity = 0.025 }: { opacity?: number }) {
return (
<div
@@ -29,48 +18,6 @@ export function GrainOverlay({ opacity = 0.025 }: { opacity?: number }) {
);
}
export function FloatingInkParticles({ particles = DEFAULT_PARTICLES }: { particles?: typeof DEFAULT_PARTICLES }) {
const shouldReduceMotion = useReducedMotion();
return (
<div className="absolute inset-0 pointer-events-none">
{particles.map((particle, i) => (
<motion.div
key={i}
className="absolute rounded-full bg-white/[0.03]"
style={{
width: `${particle.size}px`,
height: `${particle.size}px`,
filter: 'blur(1px)',
}}
initial={{
x: `${particle.x}%`,
y: `${particle.y}%`,
scale: particle.scale,
opacity: 0,
}}
animate={
shouldReduceMotion
? { opacity: 0.3 }
: {
y: [`${particle.y + 12}%`, `${particle.y - 12}%`],
opacity: [0, 0.35, 0],
x: [`${particle.x - 2}%`, `${particle.x + 2}%`],
}
}
transition={{
duration: particle.duration,
repeat: Infinity,
repeatType: 'reverse',
delay: particle.delay,
ease: 'easeInOut' as const,
}}
/>
))}
</div>
);
}
export function SectionLabel({
children,
className = '',
+56 -61
View File
@@ -1,7 +1,7 @@
'use client';
import { ArrowUpRight, Database, Users, BarChart3, FileText, Truck, Building2 } from 'lucide-react';
import { InkCard } from '@/components/ui/ink-wash/InkCard';
import { Card } from '@/components/ui/card';
import type { LucideIcon } from 'lucide-react';
interface ProductCardProps {
@@ -45,72 +45,67 @@ export function ProductCard({ title, description, href, index, status }: Product
const statusStyle = status ? statusConfig[status] : null;
return (
<InkCard
href={href}
padding="md" // 使用统一的 padding 规范 (p-5 sm:p-6 md:p-7)
glow={true}
mouseFollow={true}
interactive={true}
className="group h-full"
>
{/* 产品图标 */}
<div className="flex items-start justify-between mb-4">
<div
className="w-11 h-11 rounded-xl flex items-center justify-center flex-shrink-0"
style={{ backgroundColor: config.color.bg }}
>
<IconComponent
className="w-5 h-5"
style={{ color: config.color.text }}
strokeWidth={1.8}
/>
</div>
<a href={href}>
<Card className="group h-full relative overflow-hidden transition-all duration-300 hover:-translate-y-1 hover:shadow-lg">
{/* 产品图标 */}
<div className="flex items-start justify-between mb-4">
<div
className="w-11 h-11 rounded-xl flex items-center justify-center flex-shrink-0"
style={{ backgroundColor: config.color.bg }}
>
<IconComponent
className="w-5 h-5"
style={{ color: config.color.text }}
strokeWidth={1.8}
/>
</div>
{/* 状态标签 + 序号 */}
<div className="flex items-center gap-2 flex-shrink-0">
{status && statusStyle && (
<span
className="text-[10px] font-medium px-2 py-0.5 rounded-full border whitespace-nowrap"
style={{
backgroundColor: statusStyle.bg,
color: statusStyle.text,
borderColor: statusStyle.border,
}}
>
{status}
{/* 状态标签 + 序号 */}
<div className="flex items-center gap-2 flex-shrink-0">
{status && statusStyle && (
<span
className="text-[10px] font-medium px-2 py-0.5 rounded-full border whitespace-nowrap"
style={{
backgroundColor: statusStyle.bg,
color: statusStyle.text,
borderColor: statusStyle.border,
}}
>
{status}
</span>
)}
<span className="text-xs font-mono tracking-widest text-[var(--color-text-subtle)]">
{String(index + 1).padStart(2, '0')}
</span>
)}
<span className="text-xs font-mono tracking-widest text-[var(--color-text-subtle)]">
{String(index + 1).padStart(2, '0')}
</span>
</div>
</div>
</div>
{/* 标题 */}
<h3 className="text-base font-semibold mb-2 leading-snug tracking-tight text-[var(--color-text-primary)] group-hover:text-[var(--color-brand)] transition-colors">
{title}
</h3>
{/* 标题 */}
<h3 className="text-base font-semibold mb-2 leading-snug tracking-tight text-[var(--color-text-primary)] group-hover:text-[var(--color-brand)] transition-colors">
{title}
</h3>
{/* 描述 */}
<p className="text-sm text-[var(--color-text-muted)] leading-relaxed line-clamp-3 mb-4">
{description}
</p>
{/* 描述 */}
<p className="text-sm text-[var(--color-text-muted)] leading-relaxed line-clamp-3 mb-4">
{description}
</p>
{/* 研发中/内测中 提示 */}
{(status === '研发中' || status === '内测中') && (
<div className="flex items-center gap-1.5 text-xs text-[var(--color-brand)]/70 mb-4 px-3 py-2 rounded-lg bg-[var(--color-brand-bg)]/50">
<svg className="w-3.5 h-3.5 animate-spin" viewBox="0 0 24 24" fill="none">
<circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeDasharray="32 16" />
</svg>
<span>{status === '研发中' ? '正在积极开发中,欢迎提前交流需求' : '即将上线,欢迎预约内测体验'}</span>
{/* 研发中/内测中 提示 */}
{(status === '研发中' || status === '内测中') && (
<div className="flex items-center gap-1.5 text-xs text-[var(--color-brand)]/70 mb-4 px-3 py-2 rounded-lg bg-[var(--color-brand-bg)]/50">
<svg className="w-3.5 h-3.5 animate-spin" viewBox="0 0 24 24" fill="none">
<circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeDasharray="32 16" />
</svg>
<span>{status === '研发中' ? '正在积极开发中,欢迎提前交流需求' : '即将上线,欢迎预约内测体验'}</span>
</div>
)}
{/* CTA 链接 */}
<div className="flex items-center gap-1.5 text-sm font-medium text-[var(--color-text-subtle)] group-hover:text-[var(--color-brand)] transition-colors min-h-[44px] mt-auto">
<span>{status === '研发中' ? '了解规划' : status === '内测中' ? '申请内测' : '了解更多'}</span>
<ArrowUpRight className="w-4 h-4" strokeWidth={2} />
</div>
)}
{/* CTA 链接 */}
<div className="flex items-center gap-1.5 text-sm font-medium text-[var(--color-text-subtle)] group-hover:text-[var(--color-brand)] transition-colors min-h-[44px] mt-auto">
<span>{status === '研发中' ? '了解规划' : status === '内测中' ? '申请内测' : '了解更多'}</span>
<ArrowUpRight className="w-4 h-4" strokeWidth={2} />
</div>
</InkCard>
</Card>
</a>
);
}