feat(design): Vibe design optimization — Hero, Bento grids, trust layer, narrative

升级全站体验:首页 Hero 改为产品视觉 + 单一 CTA 转化布局;产品矩阵/服务/方案改
为 Bento 非对称网格(ERP 2x2 大卡 + BI 1x2);补齐信任层(可验证信号 + 来源标注)
与「问题→方法→结果」章节式叙事;新增 hero-product-visual 与 bento-grid 可复用组件;
统一动效与品牌视觉资产。新增对应单测、UJ-11 用户旅程测试并更新视觉回归基线快照。
This commit is contained in:
2026-08-19 14:43:27 +08:00
parent f3f4e78c51
commit 713186d552
35 changed files with 1576 additions and 79 deletions
+197
View File
@@ -0,0 +1,197 @@
import { test, expect, type Page } from '@playwright/test';
/**
* UJ-11: 首页转化旅程(设计优化专项)
*
* 覆盖首页 Vibe Design 优化后的关键转化路径,作为封版验收依据:
* UJ-11a: 首屏说服链路 — Hero 产品视觉 + 单一 CTA + 数据条 → 信任层(可验证信号)→ 叙事区 → 转化 CTA
* UJ-11b: 信任验证旅程 — 数据来源标注 + 首批客户共创标签 → 联系转化
* UJ-11c: 移动端首屏转化旅程 — 390px 下 CTA 首屏可见、无横向滚动
*
* 关联设计优化计划:docs/plans/2026-08-18-vibe-design-optimization-plan.md
* 关联 critique.impeccable/critique/
*/
test.setTimeout(90000);
// ==================== 辅助函数 ====================
async function closeCookieBanner(page: Page) {
const acceptAllButton = page.locator('button:has-text("接受所有")').first();
if ((await acceptAllButton.count()) > 0 && (await acceptAllButton.isVisible())) {
await acceptAllButton.click();
await page.waitForTimeout(500);
}
}
/**
* 断言页面无横向滚动(scrollWidth <= clientWidth + 1px 容差)
*/
async function expectNoHorizontalOverflow(page: Page) {
const overflow = await page.evaluate(() => {
const doc = document.documentElement;
return { scrollWidth: doc.scrollWidth, clientWidth: doc.clientWidth };
});
expect(overflow.scrollWidth).toBeLessThanOrEqual(overflow.clientWidth + 1);
}
// ==================== UJ-11a: 首屏说服链路 ====================
test.describe('UJ-11a: 首屏说服链路(Hero → 信任 → 叙事 → CTA', () => {
test('@journey @critical 首页首屏产品视觉、单一 CTA、数据条渲染正常', async ({ page }) => {
await page.goto('/', { waitUntil: 'domcontentloaded', timeout: 30000 });
await page.waitForTimeout(2500);
await closeCookieBanner(page);
// === L1 Hero:产品视觉 Mockup 可见(设计优化核心交付) ===
const productVisual = page.locator('[data-testid="hero-product-visual"]').first();
await expect(productVisual).toBeVisible({ timeout: 15000 });
// 诚实标签「产品界面示意」「示例数据」存在(零编造原则)
const visualText = await productVisual.textContent();
expect(visualText).toContain('产品界面示意');
expect(visualText).toContain('示例数据');
// 产品视觉含可识别产品模块命名
expect(visualText).toContain('经营驾驶舱');
// === 单一主 CTA ===
const primaryCta = page.locator('[data-testid="hero-primary-cta"]').first();
await expect(primaryCta).toBeVisible();
await expect(primaryCta).toHaveAttribute('href', '/contact');
// 次级 CTA 降级为文字链接
const secondaryLink = page.locator('[data-testid="hero-secondary-link"]').first();
await expect(secondaryLink).toBeVisible();
// === 主标题可见 ===
const heroTitle = page.locator('h1').first();
await expect(heroTitle).toBeVisible();
// 无横向滚动
await expectNoHorizontalOverflow(page);
});
test('@journey @critical 首页信任层展示可验证信号与来源标注', async ({ page }) => {
await page.goto('/', { waitUntil: 'domcontentloaded', timeout: 30000 });
await page.waitForTimeout(2500);
await closeCookieBanner(page);
// 滚动到信任层
const trustSection = page.locator('[data-testid="trust-section"]').first();
await trustSection.scrollIntoViewIfNeeded();
await expect(trustSection).toBeVisible({ timeout: 10000 });
const trustText = await trustSection.textContent();
// 可验证信任信号存在(私有化部署 / 本地化 / 全流程 / 共创)
expect(trustText).toContain('100%');
expect(trustText).toContain('成都');
expect(trustText).toContain('全流程');
expect(trustText).toContain('共创');
// 数据来源标注存在(零编造,可追溯)
expect(trustText).toContain('数据来源');
});
test('@journey @critical 首页叙事区按 01/02/03 章节呈现问题→方法→结果', async ({ page }) => {
await page.goto('/', { waitUntil: 'domcontentloaded', timeout: 30000 });
await page.waitForTimeout(2500);
await closeCookieBanner(page);
const narrativeSection = page.locator('[data-testid="narrative-section"]').first();
await narrativeSection.scrollIntoViewIfNeeded();
await expect(narrativeSection).toBeVisible({ timeout: 10000 });
const narrativeText = await narrativeSection.textContent();
// 三幕章节编号存在
expect(narrativeText).toContain('01');
expect(narrativeText).toContain('02');
expect(narrativeText).toContain('03');
// 叙事主题存在
expect(narrativeText).toContain('诊断现状');
expect(narrativeText).toContain('设计路径');
expect(narrativeText).toContain('交付成果');
// 每章保留详情入口
const narrativeLinks = narrativeSection.locator('a');
expect(await narrativeLinks.count()).toBeGreaterThanOrEqual(3);
});
});
// ==================== UJ-11b: 信任验证旅程 ====================
test.describe('UJ-11b: 信任验证旅程(共创标签 → 转化)', () => {
test('@journey @critical 首页首批客户共创标签引导至联系转化', async ({ page }) => {
await page.goto('/', { waitUntil: 'domcontentloaded', timeout: 30000 });
await page.waitForTimeout(2500);
await closeCookieBanner(page);
// 首批客户共创横幅存在(无虚构客户,如实标注)
const earlyAccessBanner = page.locator('[data-testid="early-access-banner"]').first();
await earlyAccessBanner.scrollIntoViewIfNeeded();
await expect(earlyAccessBanner).toBeVisible({ timeout: 10000 });
const bannerText = await earlyAccessBanner.textContent();
expect(bannerText).toContain('首批客户共创中');
// 引导至联系转化
const cta = earlyAccessBanner.locator('a[href="/contact"]').first();
await expect(cta).toBeVisible();
await expect(cta).toHaveAttribute('href', '/contact');
});
test('@journey @critical 空案例区呈现共创内容块而非裸空态', async ({ page }) => {
await page.goto('/', { waitUntil: 'domcontentloaded', timeout: 30000 });
await page.waitForTimeout(2500);
await closeCookieBanner(page);
const bodyText = await page.locator('body').textContent();
// 无「暂无案例数据」裸空态
expect(bodyText).not.toContain('暂无案例数据');
// 出现「共创进行时」前瞻内容块
expect(bodyText).toContain('共创进行时');
// 共创块包含转化 CTA
expect(bodyText).toContain('成为首批共创客户');
});
});
// ==================== UJ-11c: 移动端首屏转化旅程 ====================
test.describe('UJ-11c: 移动端首屏转化旅程', () => {
test('@mobile @journey @critical 移动端 CTA 首屏可见且无横向滚动', async ({ page }) => {
// 模拟移动端视口
await page.setViewportSize({ width: 390, height: 844 });
await page.goto('/', { waitUntil: 'domcontentloaded', timeout: 30000 });
await page.waitForTimeout(2500);
await closeCookieBanner(page);
// 无横向滚动
await expectNoHorizontalOverflow(page);
// CTA 首屏可达(在当前视口内可见)
const primaryCta = page.locator('[data-testid="hero-primary-cta"]').first();
await expect(primaryCta).toBeVisible({ timeout: 15000 });
// 产品视觉渲染正常
const productVisual = page.locator('[data-testid="hero-product-visual"]').first();
await expect(productVisual).toBeVisible();
});
test('@mobile @journey @critical 移动端信任层与叙事区单列堆叠无溢出', async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 });
await page.goto('/', { waitUntil: 'domcontentloaded', timeout: 30000 });
await page.waitForTimeout(2500);
await closeCookieBanner(page);
// 信任层
const trustSection = page.locator('[data-testid="trust-section"]').first();
await trustSection.scrollIntoViewIfNeeded();
await expect(trustSection).toBeVisible();
const trustText = await trustSection.textContent();
expect(trustText).toContain('数据来源');
// 叙事区
const narrativeSection = page.locator('[data-testid="narrative-section"]').first();
await narrativeSection.scrollIntoViewIfNeeded();
await expect(narrativeSection).toBeVisible();
expect(await narrativeSection.textContent()).toContain('01');
// 全程无横向滚动
await expectNoHorizontalOverflow(page);
});
});
Binary file not shown.

Before

Width:  |  Height:  |  Size: 594 KiB

After

Width:  |  Height:  |  Size: 482 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 305 KiB

After

Width:  |  Height:  |  Size: 305 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 625 KiB

After

Width:  |  Height:  |  Size: 751 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 957 KiB

After

Width:  |  Height:  |  Size: 953 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 519 KiB

After

Width:  |  Height:  |  Size: 520 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 717 KiB

After

Width:  |  Height:  |  Size: 606 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 782 KiB

After

Width:  |  Height:  |  Size: 816 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 525 KiB

After

Width:  |  Height:  |  Size: 475 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 619 KiB

After

Width:  |  Height:  |  Size: 631 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 609 KiB

After

Width:  |  Height:  |  Size: 610 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 489 KiB

After

Width:  |  Height:  |  Size: 497 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 610 KiB

After

Width:  |  Height:  |  Size: 598 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 625 KiB

After

Width:  |  Height:  |  Size: 751 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 625 KiB

After

Width:  |  Height:  |  Size: 751 KiB