feat(products): enable externalUrl redirect and external-site badge

- product 模型补 externalUrl 字段(CMS 可编辑),企业套装详情页支持外链跳转(复用 standalone 模板)
- 矩阵卡 externalUrl 产品加「外部站点」徽标(对齐 Google / 字节 / 腾讯对标)
- 清理 3 个无引用 AoyagiReisho 字体文件(性能优化)
- 测试 17/17 + 视觉基线三端更新,type-check / lint 0 errors
This commit is contained in:
2026-08-19 16:49:30 +08:00
parent cbdc7dd0c8
commit 5ed82101e0
11 changed files with 34 additions and 4 deletions
+7 -1
View File
@@ -1,5 +1,5 @@
import { Metadata } from 'next';
import { notFound } from 'next/navigation';
import { notFound, redirect } from 'next/navigation';
import { COMPANY_INFO } from '@/lib/constants';
import { getPublishedItemBySlug, getAllPublishedSlugs } from '@/lib/cms/data-server';
import type { Product } from '@/lib/constants/products';
@@ -36,6 +36,12 @@ export default async function ProductDetailPage({ params }: { params: Promise<{
notFound();
}
// 若配置 externalUrl,则跳转到独立子域名产品站(对齐品牌矩阵「成熟产品外链」机制)
const externalUrl = item.data.externalUrl as string | undefined;
if (externalUrl) {
redirect(externalUrl);
}
const product = { ...item.data, title: item.data.title || item.title } as unknown as Product;
return <ProductDetailContentV3 product={JSON.parse(JSON.stringify(product))} />;
}
@@ -77,6 +77,7 @@ jest.mock('lucide-react', () => {
};
return {
ArrowRight: mockIcon('arrow-right'),
ArrowUpRight: mockIcon('arrow-up-right'),
Package: mockIcon('package'),
Cpu: mockIcon('cpu'),
TrendingUp: mockIcon('trendingup'),
@@ -188,6 +189,7 @@ const mockSpecializedProducts: Product[] = [
image: '/novavis.png',
heroThemeId: 'novavis',
bundle: 'standalone',
externalUrl: 'https://novavis.p.novalon.cn',
scenario: '面向执法机关的离线智能数据分析与资金关系图谱平台',
metrics: [
{ value: '100万+', label: '秒级处理' },
@@ -253,6 +255,13 @@ describe('ProductsContentV3 - 产品列表页集成测试', () => {
expect(screen.getAllByText('NovaVis 数据可视化平台').length).toBeGreaterThanOrEqual(1);
});
it('should show external-site badge for products with externalUrl', async () => {
const { default: ProductsContentV3 } = await import('./products-content-v3');
render(<ProductsContentV3 products={allMockProducts} />);
expect(screen.getAllByText('外部站点').length).toBeGreaterThanOrEqual(1);
});
it('should render product capabilities on cards', async () => {
const { default: ProductsContentV3 } = await import('./products-content-v3');
render(<ProductsContentV3 products={allMockProducts} />);
@@ -7,7 +7,7 @@ import { EASE_OUT } from '@/components/ui/page-decoration';
import { Button } from '@/components/ui/button';
import { StaticLink } from '@/components/ui/static-link';
import { BentoItem } from '@/components/sections/bento-grid';
import { ArrowRight, Package, Cpu, TrendingUp, Users, Settings, FileText } from 'lucide-react';
import { ArrowRight, ArrowUpRight, Package, Cpu, TrendingUp, Users, Settings, FileText } from 'lucide-react';
import type { Product } from '@/lib/constants/products';
import { useReducedMotion } from '@/hooks/use-reduced-motion';
import { cn } from '@/lib/utils';
@@ -176,7 +176,15 @@ function ProductCard({ product, size = 'small' }: { product: Product; size?: 'la
{product.title}
</h3>
</div>
<span className="px-2 py-1 text-xs bg-brand/10 text-brand" aria-hidden="true">{bundleLabel}</span>
<div className="flex items-center gap-2 shrink-0">
<span className="px-2 py-1 text-xs bg-brand/10 text-brand" aria-hidden="true">{bundleLabel}</span>
{isExternal && (
<span className="inline-flex items-center gap-1 px-2 py-1 text-xs border border-border-primary text-text-secondary">
<ArrowUpRight className="w-3 h-3" aria-hidden="true" />
</span>
)}
</div>
</div>
<p className={cn('text-text-secondary mb-4 leading-relaxed flex-1', isLarge ? 'text-base' : 'text-sm')}>
Binary file not shown.
Binary file not shown.
+6
View File
@@ -291,6 +291,12 @@ const productFields: FieldDefinition[] = [
{ label: '已发布', value: '已发布' },
],
},
{
name: 'externalUrl',
type: 'text',
label: '外部跳转链接',
description: '若设置,访问该产品详情页时将跳转到此 URL(如独立子域名产品)',
},
{
name: 'overview',
type: 'textarea',