feat: 添加面包屑导航组件并优化页面布局

refactor: 重构页面结构和导航逻辑

fix: 修复移动端菜单导航和滚动行为

perf: 优化图片加载性能和资源请求

test: 添加端到端测试和性能测试用例

docs: 更新.gitignore文件

chore: 更新依赖和配置

style: 优化代码格式和类型安全

ci: 调整Playwright测试超时时间

build: 更新Next.js配置和构建选项
This commit is contained in:
张翔
2026-02-28 09:09:04 +08:00
parent 9d01e0982f
commit 9451814ca4
60 changed files with 4078 additions and 148 deletions
+15 -14
View File
@@ -4,7 +4,6 @@ import { PerformanceMetrics, PerformanceThresholds } from '../types';
export class PerformanceMonitor {
private page: Page;
private metrics: PerformanceMetrics;
private startTime: number;
constructor(page: Page) {
this.page = page;
@@ -16,12 +15,9 @@ export class PerformanceMonitor {
cumulativeLayoutShift: 0,
firstInputDelay: 0,
};
this.startTime = 0;
}
async startMonitoring(): Promise<void> {
this.startTime = Date.now();
await this.page.evaluate(() => {
window.performance.clearResourceTimings();
});
@@ -86,7 +82,7 @@ export class PerformanceMonitor {
const entries = list.getEntries();
const longTasks = entries.filter((e) => e.duration > 50);
if (longTasks.length > 0) {
resolve(longTasks[0].startTime);
resolve(longTasks[0]?.startTime || 0);
}
});
observer.observe({ entryTypes: ['longtask'] });
@@ -103,7 +99,8 @@ export class PerformanceMonitor {
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
if (entries.length > 0) {
resolve(entries[0].processingStart - entries[0].startTime);
const entry = entries[0] as any;
resolve((entry?.processingStart || 0) - (entry?.startTime || 0));
}
});
observer.observe({ entryTypes: ['first-input'] });
@@ -176,7 +173,7 @@ export class PerformanceMonitor {
const entries = list.getEntries();
const longTasks = entries.filter((e) => e.duration > 50);
if (longTasks.length > 0) {
resolve(longTasks[0].startTime);
resolve(longTasks[0]?.startTime || 0);
}
});
observer.observe({ entryTypes: ['longtask'] });
@@ -196,7 +193,8 @@ export class PerformanceMonitor {
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
if (entries.length > 0) {
resolve(entries[0].processingStart - entries[0].startTime);
const entry = entries[0] as any;
resolve((entry?.processingStart || 0) - (entry?.startTime || 0));
}
});
observer.observe({ entryTypes: ['first-input'] });
@@ -211,12 +209,15 @@ export class PerformanceMonitor {
async measureResourceTiming(): Promise<any[]> {
const resources = await this.page.evaluate(() => {
return performance.getEntriesByType('resource').map((r) => ({
name: r.name,
duration: r.duration,
size: (r as any).transferSize,
type: r.initiatorType,
}));
return performance.getEntriesByType('resource').map((r) => {
const resource = r as any;
return {
name: resource.name,
duration: resource.duration,
size: resource.transferSize,
type: resource.initiatorType,
};
});
});
return resources;
}
+13 -13
View File
@@ -7,32 +7,32 @@ export class TestDataGenerator {
private static readonly SUBJECTS = ['产品咨询', '技术支持', '商务合作', '其他', '意见反馈'];
static generateName(): string {
const first = this.FIRST_NAMES[Math.floor(Math.random() * this.FIRST_NAMES.length)];
const last = this.LAST_NAMES[Math.floor(Math.random() * this.LAST_NAMES.length)];
const first = this.FIRST_NAMES[Math.floor(Math.random() * this.FIRST_NAMES.length)]!;
const last = this.LAST_NAMES[Math.floor(Math.random() * this.LAST_NAMES.length)]!;
return `${first}${last}`;
}
static generateEmail(name?: string): string {
const username = name || this.generateName();
const domains = ['example.com', 'test.com', 'demo.com'];
const domain = domains[Math.floor(Math.random() * domains.length)];
const domain = domains[Math.floor(Math.random() * domains.length)]!;
return `${username}@${domain}`;
}
static generatePhone(): string {
const prefix = ['138', '139', '136', '137', '158', '159'][Math.floor(Math.random() * 6)];
const prefix = ['138', '139', '136', '137', '158', '159'][Math.floor(Math.random() * 6)]!;
const middle = Math.floor(Math.random() * 9000 + 1000);
const suffix = Math.floor(Math.random() * 9000 + 1000);
return `${prefix}${middle}${suffix}`;
}
static generateCompany(): string {
const prefix = ['创新', '未来', '智慧', '科技', '数字'][Math.floor(Math.random() * 5)];
const suffix = this.COMPANIES[Math.floor(Math.random() * this.COMPANIES.length)];
const prefix = ['创新', '未来', '智慧', '科技', '数字'][Math.floor(Math.random() * 5)]!;
const suffix = this.COMPANIES[Math.floor(Math.random() * this.COMPANIES.length)]!;
return `${prefix}${suffix}`;
}
static generateMessage(minLength: number = 10, maxLength: number = 100): string {
static generateMessage(): string {
const messages = [
'您好,我对贵公司的产品很感兴趣,希望能了解更多信息。',
'请问贵公司是否有相关的技术支持服务?',
@@ -43,11 +43,11 @@ export class TestDataGenerator {
'我们公司正在评估相关技术方案,希望能了解贵公司的解决方案。',
'您好,我想咨询一下贵公司的产品定制服务。',
];
return messages[Math.floor(Math.random() * messages.length)];
return messages[Math.floor(Math.random() * messages.length)]!;
}
static generateSubject(): string {
return this.SUBJECTS[Math.floor(Math.random() * this.SUBJECTS.length)];
return this.SUBJECTS[Math.floor(Math.random() * this.SUBJECTS.length)]!;
}
static generateContactFormData(): ContactFormData {
@@ -79,7 +79,7 @@ export class TestDataGenerator {
'user@domain',
'user domain.com',
];
return invalidEmails[Math.floor(Math.random() * invalidEmails.length)];
return invalidEmails[Math.floor(Math.random() * invalidEmails.length)]!;
}
static generateInvalidPhone(): string {
@@ -89,7 +89,7 @@ export class TestDataGenerator {
'abcdefghijk',
'123-456-7890',
];
return invalidPhones[Math.floor(Math.random() * invalidPhones.length)];
return invalidPhones[Math.floor(Math.random() * invalidPhones.length)]!;
}
static generateShortMessage(): string {
@@ -136,13 +136,13 @@ export class TestDataGenerator {
'https://demo.com/path',
'http://example.com/page?param=value',
];
return urls[Math.floor(Math.random() * urls.length)];
return urls[Math.floor(Math.random() * urls.length)]!;
}
static generateDate(): string {
const date = new Date();
date.setDate(date.getDate() + Math.floor(Math.random() * 30));
return date.toISOString().split('T')[0];
return date.toISOString().split('T')[0]!;
}
static generateTime(): string {
+8 -4
View File
@@ -76,7 +76,11 @@ export const tabletDevices = Object.entries(devices)
.map(([key, config]) => ({ key, ...config }));
export const getDevice = (key: string): DeviceConfig => {
return devices[key] || devices['desktop-1280x720'];
const device = devices[key];
if (!device) {
return devices['desktop-1280x720']!;
}
return device;
};
export const getAllDevices = (): DeviceConfig[] => {
@@ -84,15 +88,15 @@ export const getAllDevices = (): DeviceConfig[] => {
};
export const getDesktopDevices = (): DeviceConfig[] => {
return desktopDevices.map(d => devices[d.key]);
return desktopDevices.map(d => devices[d.key]!);
};
export const getMobileDevices = (): DeviceConfig[] => {
return mobileDevices.map(d => devices[d.key]);
return mobileDevices.map(d => devices[d.key]!);
};
export const getTabletDevices = (): DeviceConfig[] => {
return tabletDevices.map(d => devices[d.key]);
return tabletDevices.map(d => devices[d.key]!);
};
export const getBreakpoints = () => {