feat(theme): 暗黑模式 Phase 2 批次 3 — product-detail/standalone/detail-hero token 化

- product-detail-content-v3.tsx: 9 处 bg-white → bg-bg-primary
  (Hero / features / benefits / process / cases / certifications / CTA)
- standalone/[id]/client.tsx: 5 处 bg-white → bg-bg-primary
- detail-hero.tsx: 浅色 variant bg-white → bg-bg-primary(共享组件)
- detail.test.tsx: 新增常驻断言——light variant 必须用主题 token,禁止硬编码 bg-white

验证(evidence-first):
- type-check 0 / lint 0 errors
- jest 128 suites / 1624 passed(含新增断言)
- 视觉回归 22 passed(浅色基线零 diff)
- computed-style 一次性验证 4 passed: /products/erp + /products/crm 的 dark 反色
  (html/body/header → #0A0E14、section 层次自动反色)+ light 零深色残留;脚本已删

Phase 2 剩余批次: 批次 4 admin 全量(user 决策: admin 也参与)
This commit is contained in:
2026-08-31 18:53:47 +08:00
parent 80e3c48c5d
commit dcc2073e1e
4 changed files with 31 additions and 16 deletions
@@ -26,7 +26,7 @@ function DetailHero({ product }: DetailHeroProps) {
const productIcon = PRODUCT_ICONS[product.title] || <Package className="w-8 h-8" />;
return (
<section className="relative min-h-[80vh] flex items-center overflow-hidden bg-white pt-24">
<section className="relative min-h-[80vh] flex items-center overflow-hidden bg-bg-primary pt-24">
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10 w-full py-20 sm:py-24 md:py-28 lg:py-32">
<motion.div
initial={{ opacity: 0, y: 30 }}
@@ -135,7 +135,7 @@ function FeaturesSection({ features }: FeaturesSectionProps) {
{features.map((feature, idx) => (
<div
key={idx}
className="group relative p-8 bg-white hover:bg-bg-secondary transition-all duration-500"
className="group relative p-8 bg-bg-primary hover:bg-bg-secondary transition-all duration-500"
>
<div className="flex items-center gap-5">
<div className="w-10 h-10 rounded-xl bg-brand-soft flex items-center justify-center text-brand shrink-0">
@@ -157,7 +157,7 @@ interface BenefitsSectionProps {
function BenefitsSection({ benefits }: BenefitsSectionProps) {
return (
<section className="relative py-20 sm:py-28 md:py-32 lg:py-40 overflow-hidden bg-white">
<section className="relative py-20 sm:py-28 md:py-32 lg:py-40 overflow-hidden bg-bg-primary">
<div className="absolute top-0 left-0 right-0 h-px bg-border-primary" />
<div className="absolute bottom-0 left-0 right-0 h-px bg-border-primary" />
@@ -178,7 +178,7 @@ function BenefitsSection({ benefits }: BenefitsSectionProps) {
{benefits.map((benefit, idx) => (
<div
key={idx}
className="group relative p-8 bg-white hover:bg-bg-secondary transition-all duration-500"
className="group relative p-8 bg-bg-primary hover:bg-bg-secondary transition-all duration-500"
>
<div className="flex items-center gap-6">
<div className="w-12 h-12 rounded-2xl bg-brand-soft/50 flex items-center justify-center text-brand shrink-0">
@@ -231,7 +231,7 @@ function ProcessSection({ process }: ProcessSectionProps) {
{process.map((step, idx) => (
<div
key={idx}
className="group relative text-center p-8 bg-white hover:bg-bg-secondary transition-all duration-500"
className="group relative text-center p-8 bg-bg-primary hover:bg-bg-secondary transition-all duration-500"
>
<div className="relative z-10 w-20 h-20 rounded-3xl bg-bg-secondary border border-border-primary group-hover:border-brand/50 flex items-center justify-center mx-auto mb-8 transition-all duration-500">
<span className="text-3xl font-bold text-brand">{idx + 1}</span>
@@ -256,7 +256,7 @@ function CaseStudiesSection({ caseStudies }: CaseStudiesSectionProps) {
if (!caseStudies || caseStudies.length === 0) return null;
return (
<section className="relative py-20 sm:py-28 md:py-32 lg:py-40 overflow-hidden bg-white">
<section className="relative py-20 sm:py-28 md:py-32 lg:py-40 overflow-hidden bg-bg-primary">
<div className="absolute top-0 left-0 right-0 h-px bg-border-primary" />
<div className="absolute bottom-0 left-0 right-0 h-px bg-border-primary" />
@@ -281,7 +281,7 @@ function CaseStudiesSection({ caseStudies }: CaseStudiesSectionProps) {
{caseStudies.map((study) => (
<div
key={study.client}
className="group relative block overflow-hidden bg-white transition-all duration-500 hover:bg-bg-secondary"
className="group relative block overflow-hidden bg-bg-primary transition-all duration-500 hover:bg-bg-secondary"
>
<div className="aspect-[4/3] bg-gradient-to-br from-bg-secondary to-bg-tertiary relative overflow-hidden">
<div className="absolute inset-0 flex items-center justify-center">
@@ -350,7 +350,7 @@ function CertificationsSection({ certifications }: CertificationsSectionProps) {
{certifications.map((cert, idx) => (
<div
key={idx}
className="group flex items-center gap-4 px-8 py-5 bg-white hover:bg-bg-secondary transition-all duration-500"
className="group flex items-center gap-4 px-8 py-5 bg-bg-primary hover:bg-bg-secondary transition-all duration-500"
>
<div className="w-12 h-12 rounded-xl bg-brand-soft flex items-center justify-center text-brand">
<Shield className="w-6 h-6" />
@@ -374,7 +374,7 @@ interface CTASectionProps {
function CTASection({ product }: CTASectionProps) {
return (
<section className="relative py-36 lg:py-44 overflow-hidden bg-white">
<section className="relative py-36 lg:py-44 overflow-hidden bg-bg-primary">
<div className="absolute top-0 left-0 right-0 h-px bg-border-primary" />
<div className="absolute bottom-0 left-0 right-0 h-px bg-border-primary" />
@@ -68,7 +68,7 @@ export function StandaloneProductClient({ item }: StandaloneProductClientProps)
/>
{status === 'coming-soon' ? (
<section className="bg-white py-16 lg:py-20">
<section className="bg-bg-primary py-16 lg:py-20">
<div className="container-wide">
<motion.div
initial={{ opacity: 0, y: 20 }}
@@ -99,7 +99,7 @@ export function StandaloneProductClient({ item }: StandaloneProductClientProps)
transition={{ duration: 0.3, delay: index * 0.08 }}
className="rounded-lg border border-border-primary bg-bg-secondary p-4"
>
<div className="mb-2 flex h-8 w-8 items-center justify-center rounded-lg bg-white text-text-muted">
<div className="mb-2 flex h-8 w-8 items-center justify-center rounded-lg bg-bg-primary text-text-muted">
<Icon className="h-4 w-4" />
</div>
<h3 className="mb-1 text-sm font-semibold text-ink">
@@ -129,7 +129,7 @@ export function StandaloneProductClient({ item }: StandaloneProductClientProps)
</section>
) : (
<>
<section className="bg-white py-16 lg:py-20">
<section className="bg-bg-primary py-16 lg:py-20">
<div className="container-wide">
<div className="grid grid-cols-1 gap-12 lg:grid-cols-2">
<motion.div
@@ -192,7 +192,7 @@ export function StandaloneProductClient({ item }: StandaloneProductClientProps)
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.3, delay: index * 0.05 }}
className="rounded-lg border border-border-primary bg-white p-5"
className="rounded-lg border border-border-primary bg-bg-primary p-5"
>
<div className="mb-2 text-sm text-text-muted">{param.name}</div>
<div className="mb-2 text-2xl font-bold text-brand">{param.value}</div>
@@ -207,7 +207,7 @@ export function StandaloneProductClient({ item }: StandaloneProductClientProps)
)}
{complianceCertifications.length > 0 && (
<section className="bg-white py-16 lg:py-20">
<section className="bg-bg-primary py-16 lg:py-20">
<div className="container-wide">
<motion.div
initial={{ opacity: 0, y: 20 }}
@@ -238,7 +238,7 @@ export function StandaloneProductClient({ item }: StandaloneProductClientProps)
className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 rounded-lg border border-border-primary bg-bg-secondary p-5"
>
<div className="flex items-start gap-4">
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-white text-brand">
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-bg-primary text-brand">
<FileCheck className="h-5 w-5" />
</div>
<div>
+1 -1
View File
@@ -32,7 +32,7 @@ export function DetailHero({
const bgClass = isBrand
? 'bg-brand-section text-white'
: 'bg-white text-ink';
: 'bg-bg-primary text-ink';
return (
<section className={`relative min-h-[700px] flex items-center overflow-hidden ${bgClass}`}>
+15
View File
@@ -383,6 +383,21 @@ describe('DetailHero', () => {
const section = container.querySelector('section');
expect(section).toBeInTheDocument();
});
it('should use theme-aware bg token (not hard-coded bg-white) for light variant', async () => {
const { DetailHero } = await import('./detail-hero');
const { container } = render(
<DetailHero
theme={mockHeroTheme}
title="ERP"
subtitle="Expert"
/>
);
// 暗黑模式:浅色底色必须走 CSS 变量 token,dark 下才能随主题反色
const section = container.querySelector('section');
expect(section?.className).toContain('bg-bg-primary');
expect(section?.className).not.toContain('bg-white');
});
});
describe('ProductValueSection', () => {