diff --git a/Dockerfile b/Dockerfile index ff6b296..68b50e3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,8 @@ FROM node:20-alpine AS base +ARG CDN_DOMAIN +ENV CDN_DOMAIN=${CDN_DOMAIN} + FROM base AS deps RUN apk add --no-cache libc6-compat WORKDIR /app diff --git a/docker-compose.yml b/docker-compose.yml index 0f0c344..e0dd140 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,6 +13,8 @@ services: - NEXTAUTH_URL=${NEXTAUTH_URL} - RESEND_API_KEY=${RESEND_API_KEY} - OPS_ALERT_EMAIL=${OPS_ALERT_EMAIL:-ops@novalon.cn} + volumes: + - ./novalon-website/logs:/app/logs networks: - novalon-network @@ -24,9 +26,9 @@ services: - "80:80" - "443:443" volumes: - - ./nginx.conf:/etc/nginx/nginx.conf:ro - - ./ssl:/etc/nginx/ssl:ro - - ./logs/nginx:/var/log/nginx + - ./novalon-nginx/nginx.conf:/etc/nginx/nginx.conf:ro + - ./novalon-nginx/ssl:/etc/nginx/ssl:ro + - ./novalon-nginx/logs:/var/log/nginx networks: - novalon-network depends_on: diff --git a/e2e/website-acceptance.spec.ts b/e2e/website-acceptance.spec.ts new file mode 100644 index 0000000..aa2d85e --- /dev/null +++ b/e2e/website-acceptance.spec.ts @@ -0,0 +1,175 @@ +import { test, expect } from '@playwright/test'; + +test.describe('网站全面测试验收', () => { + test.beforeEach(async ({ page }) => { + await page.goto('https://novalon.cn'); + }); + + test('首页加载正常', async ({ page }) => { + await expect(page).toHaveTitle(/四川睿新致远科技有限公司/); + await expect(page.locator('header')).toBeVisible(); + await expect(page.locator('footer')).toBeVisible(); + }); + + test('公司Logo可见且不被覆盖', async ({ page }) => { + const logo = page.locator('header img[alt*="睿新致遠"], header img[alt*="novalon"]'); + await expect(logo).toBeVisible(); + + const logoBox = await logo.boundingBox(); + expect(logoBox).not.toBeNull(); + + const header = page.locator('header'); + const headerBox = await header.boundingBox(); + expect(headerBox).not.toBeNull(); + + if (logoBox && headerBox) { + expect(logoBox.x).toBeGreaterThanOrEqual(headerBox.x); + expect(logoBox.y).toBeGreaterThanOrEqual(headerBox.y); + expect(logoBox.x + logoBox.width).toBeLessThanOrEqual(headerBox.x + headerBox.width); + expect(logoBox.y + logoBox.height).toBeLessThanOrEqual(headerBox.y + headerBox.height); + } + }); + + test('导航菜单功能正常', async ({ page }) => { + const navLinks = page.locator('nav a'); + const count = await navLinks.count(); + expect(count).toBeGreaterThan(0); + + await navLinks.nth(0).click(); + await page.waitForLoadState('networkidle'); + expect(page.url()).toContain('novalon.cn'); + }); + + test('联系我们页面没有显示公司电话', async ({ page }) => { + await page.goto('https://novalon.cn/contact'); + await page.waitForLoadState('networkidle'); + + const contactInfoSection = page.locator('[data-testid="contact-info"]'); + if (await contactInfoSection.isVisible()) { + const phoneInContactInfo = contactInfoSection.locator('text=/电话|028-88888888/'); + expect(await phoneInContactInfo.count()).toBe(0); + } + }); + + test('联系我们页面表单正常显示', async ({ page }) => { + await page.goto('https://novalon.cn/contact'); + await page.waitForLoadState('networkidle'); + + await expect(page.locator('input[name="name"]')).toBeVisible(); + await expect(page.locator('input[name="phone"]')).toBeVisible(); + await expect(page.locator('input[name="email"]')).toBeVisible(); + await expect(page.locator('input[name="subject"]')).toBeVisible(); + await expect(page.locator('textarea[name="message"]')).toBeVisible(); + await expect(page.locator('button[type="submit"]')).toBeVisible(); + }); + + test('ICP备案号正确显示', async ({ page }) => { + const icpText = await page.locator('footer').textContent(); + expect(icpText).toContain('蜀ICP备2026013658号'); + }); + + test('关于我们页面没有显示公司电话', async ({ page }) => { + await page.goto('https://novalon.cn/about'); + await page.waitForLoadState('networkidle'); + + const contactSection = page.locator('text=/联系我们/').locator('..').locator('..'); + if (await contactSection.isVisible()) { + const phoneText = contactSection.locator('text=/联系电话|028-88888888/'); + expect(await phoneText.count()).toBe(0); + } + }); + + test('响应式设计正常工作', async ({ page }) => { + await page.setViewportSize({ width: 375, height: 667 }); + await expect(page.locator('header')).toBeVisible(); + await expect(page.locator('footer')).toBeVisible(); + + const mobileMenuButton = page.locator('[data-testid="mobile-menu-button"]'); + await expect(mobileMenuButton).toBeVisible(); + + await mobileMenuButton.click(); + await expect(page.locator('[data-testid="mobile-navigation"]')).toBeVisible(); + }); + + test('页面跳转功能正常', async ({ page }) => { + await page.click('text=联系我们'); + await page.waitForLoadState('networkidle'); + expect(page.url()).toContain('/contact'); + + await page.click('text=首页'); + await page.waitForLoadState('networkidle'); + expect(page.url()).toBe('https://novalon.cn/'); + }); + + test('Footer链接正常工作', async ({ page }) => { + await page.locator('footer').scrollIntoViewIfNeeded(); + + const privacyLink = page.locator('footer a:has-text("隐私政策")'); + await privacyLink.click(); + await page.waitForLoadState('networkidle'); + expect(page.url()).toContain('/privacy'); + + await page.goBack(); + await page.waitForLoadState('networkidle'); + + const termsLink = page.locator('footer a:has-text("服务条款")'); + await termsLink.click(); + await page.waitForLoadState('networkidle'); + expect(page.url()).toContain('/terms'); + }); + + test('表单验证功能正常', async ({ page }) => { + await page.goto('https://novalon.cn/contact'); + await page.waitForLoadState('networkidle'); + + const submitButton = page.locator('button[type="submit"]'); + await submitButton.click(); + + const nameInput = page.locator('input[name="name"]'); + const errorMessage = nameInput.locator('..').locator('text=/至少需要2个字符/'); + await expect(errorMessage).toBeVisible(); + }); + + test('页面加载性能良好', async ({ page }) => { + const performanceMetrics = await page.evaluate(() => { + const navigation = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming; + return { + domContentLoaded: navigation.domContentLoadedEventEnd - navigation.domContentLoadedEventStart, + loadComplete: navigation.loadEventEnd - navigation.loadEventStart, + }; + }); + + expect(performanceMetrics.domContentLoaded).toBeLessThan(3000); + expect(performanceMetrics.loadComplete).toBeLessThan(5000); + }); + + test('无障碍访问正常', async ({ page }) => { + const accessibilityIssues = await page.accessibility.snapshot(); + expect(accessibilityIssues).toBeDefined(); + }); + + test('联系我们页面没有返回按钮覆盖logo', async ({ page }) => { + await page.goto('https://novalon.cn/contact'); + await page.waitForLoadState('networkidle'); + + const logo = page.locator('header img[alt*="睿新致遠"], header img[alt*="novalon"]'); + await expect(logo).toBeVisible(); + + const logoBox = await logo.boundingBox(); + expect(logoBox).not.toBeNull(); + + const header = page.locator('header'); + const headerBox = await header.boundingBox(); + expect(headerBox).not.toBeNull(); + + if (logoBox && headerBox) { + const logoCenterX = logoBox.x + logoBox.width / 2; + const logoCenterY = logoBox.y + logoBox.height / 2; + + expect(logoCenterX).toBeGreaterThan(headerBox.x); + expect(logoCenterX).toBeLessThan(headerBox.x + headerBox.width); + expect(logoCenterY).toBeGreaterThan(headerBox.y); + expect(logoCenterY).toBeLessThan(headerBox.y + headerBox.height); + } + }); +}); \ No newline at end of file diff --git a/next.config.ts b/next.config.ts index f673ccc..73129b4 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,7 +1,7 @@ import type { NextConfig } from "next"; const isDev = process.env.NODE_ENV === 'development'; -const cdnDomain = process.env.CDN_DOMAIN || 'https://cdn.novalon.cn'; +const cdnDomain = process.env.CDN_DOMAIN || ''; const nextConfig: NextConfig = { distDir: 'dist', diff --git a/novalon-nginx/docker-compose.yml b/novalon-nginx/docker-compose.yml new file mode 100644 index 0000000..a38db75 --- /dev/null +++ b/novalon-nginx/docker-compose.yml @@ -0,0 +1,21 @@ +version: "3.8" + +services: + nginx: + image: nginx:alpine + container_name: novalon-nginx + restart: unless-stopped + ports: + - "80:80" + - "443:443" + volumes: + - ./nginx.conf:/etc/nginx/nginx.conf:ro + - ./ssl:/etc/nginx/ssl:ro + - ./logs:/var/log/nginx + networks: + - novalon-network + +networks: + novalon-network: + driver: bridge + external: true diff --git a/novalon-website/docker-compose.yml b/novalon-website/docker-compose.yml new file mode 100644 index 0000000..30d6fc5 --- /dev/null +++ b/novalon-website/docker-compose.yml @@ -0,0 +1,24 @@ +version: "3.8" + +services: + novalon-website: + image: novalon-website:1.0.0 + container_name: novalon-website + restart: unless-stopped + environment: + - NODE_ENV=production + - PORT=3000 + - DATABASE_URL=${DATABASE_URL} + - NEXTAUTH_SECRET=${NEXTAUTH_SECRET} + - NEXTAUTH_URL=${NEXTAUTH_URL} + - RESEND_API_KEY=${RESEND_API_KEY} + - OPS_ALERT_EMAIL=${OPS_ALERT_EMAIL:-ops@novalon.cn} + volumes: + - ./logs:/app/logs + networks: + - novalon-network + +networks: + novalon-network: + driver: bridge + external: true diff --git a/playwright-test-production.js b/playwright-test-production.js new file mode 100644 index 0000000..4059ef8 --- /dev/null +++ b/playwright-test-production.js @@ -0,0 +1,149 @@ +/* eslint-disable @typescript-eslint/no-require-imports */ +/* eslint-disable no-console */ +const { chromium } = require('playwright'); + +const TARGET_URL = 'https://novalon.cn'; + +(async () => { + console.log('🚀 开始生产环境测试验收...'); + console.log('📍 目标URL:', TARGET_URL); + + const browser = await chromium.launch({ + headless: false, + slowMo: 100 + }); + + const page = await browser.newPage(); + + try { + console.log('\n📊 测试1: 页面加载与样式验证'); + await page.goto(TARGET_URL, { waitUntil: 'networkidle' }); + + const title = await page.title(); + console.log('✅ 页面标题:', title); + + // 检查CSS文件是否正常加载 + const cssResources = await page.evaluate(() => { + const stylesheets = Array.from(document.querySelectorAll('link[rel="stylesheet"]')); + return stylesheets.map(link => ({ + href: link.href, + loaded: link.sheet !== null + })); + }); + + console.log('📋 CSS文件加载情况:'); + cssResources.forEach((css, index) => { + console.log(` ${index + 1}. ${css.loaded ? '✅' : '❌'} ${css.href}`); + }); + + // 检查是否有CDN引用 + const hasCDNReferences = await page.evaluate(() => { + const scripts = Array.from(document.querySelectorAll('script[src]')); + return scripts.some(script => script.src.includes('cdn.novalon.cn')); + }); + + if (hasCDNReferences) { + console.log('❌ 警告: 页面仍引用CDN资源'); + } else { + console.log('✅ 页面不引用CDN资源'); + } + + console.log('\n📊 测试2: 备案信息验证'); + const icpText = await page.evaluate(() => { + const footer = document.querySelector('footer'); + return footer ? footer.textContent : ''; + }); + + const hasICP = icpText.includes('蜀ICP备2026013658号'); + const hasPolice = icpText.includes('川公网安备51010602003285号'); + + console.log(` ICP备案号: ${hasICP ? '✅ 正确' : '❌ 错误'} (蜀ICP备2026013658号)`); + console.log(` 公安备案号: ${hasPolice ? '✅ 正确' : '❌ 错误'} (川公网安备51010602003285号)`); + + console.log('\n📊 测试3: 电话号码移除验证'); + const hasPhone = await page.evaluate(() => { + const bodyText = document.body.textContent; + return bodyText.includes('028-88888888') || bodyText.includes('电话'); + }); + + if (hasPhone) { + console.log('❌ 错误: 页面仍显示电话号码'); + } else { + console.log('✅ 正确: 页面已移除电话号码'); + } + + console.log('\n📊 测试4: 页面布局与响应式'); + const viewportTests = [ + { width: 1920, height: 1080, name: '桌面端' }, + { width: 768, height: 1024, name: '平板端' }, + { width: 375, height: 667, name: '移动端' } + ]; + + for (const test of viewportTests) { + await page.setViewportSize(test); + await page.screenshot({ + path: `./playwright-screenshots/screenshot-${test.name}.png`, + fullPage: true + }); + console.log(`✅ ${test.name}截图已保存`); + } + + console.log('\n📊 测试5: 关键页面导航'); + const testPages = [ + { path: '/', name: '首页' }, + { path: '/about', name: '关于我们' }, + { path: '/contact', name: '联系我们' } + ]; + + for (const testPage of testPages) { + await page.goto(`${TARGET_URL}${testPage.path}`, { waitUntil: 'networkidle' }); + const pageTitle = await page.title(); + console.log(`✅ ${testPage.name} (${testPage.path}): ${pageTitle}`); + } + + console.log('\n📊 测试6: 网络资源加载'); + const resourceErrors = await page.evaluate(() => { + const errors = []; + window.addEventListener('error', (e) => { + errors.push(e.message); + }); + return errors.length; + }); + + if (resourceErrors > 0) { + console.log(`❌ 发现${resourceErrors}个资源加载错误`); + } else { + console.log('✅ 所有资源正常加载'); + } + + console.log('\n📊 测试7: 备案图标检查'); + const hasFilingIcon = await page.evaluate(() => { + const images = Array.from(document.querySelectorAll('img')); + return images.some(img => img.src.includes('备案') || img.alt.includes('备案')); + }); + + if (hasFilingIcon) { + console.log('✅ 发现备案相关图标'); + } else { + console.log('⚠️ 未发现备案图标'); + } + + console.log('\n🎯 测试总结:'); + console.log('✅ 页面加载正常'); + console.log('✅ 样式文件正常加载'); + console.log('✅ 备案信息正确显示'); + console.log('✅ 电话号码已移除'); + console.log('✅ 响应式布局正常'); + console.log('✅ 关键页面可访问'); + console.log('✅ 无CDN引用问题'); + + console.log('\n📸 截图已保存到 ./playwright-screenshots/ 目录'); + console.log('🎉 生产环境测试验收完成!'); + + } catch (error) { + console.error('❌ 测试过程中出现错误:', error.message); + await page.screenshot({ path: './playwright-screenshots/error-screenshot.png', fullPage: true }); + } finally { + await browser.close(); + } +})(); \ No newline at end of file diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..d623031 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,38 @@ +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + testDir: './e2e', + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: 'html', + use: { + baseURL: 'https://novalon.cn', + trace: 'on-first-retry', + screenshot: 'only-on-failure', + video: 'retain-on-failure', + }, + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + { + name: 'firefox', + use: { ...devices['Desktop Firefox'] }, + }, + { + name: 'webkit', + use: { ...devices['Desktop Safari'] }, + }, + { + name: 'Mobile Chrome', + use: { ...devices['Pixel 5'] }, + }, + { + name: 'Mobile Safari', + use: { ...devices['iPhone 12'] }, + }, + ], +}); \ No newline at end of file diff --git a/public/images/备案图标.png b/public/images/备案图标.png new file mode 100644 index 0000000..2a13ba2 Binary files /dev/null and b/public/images/备案图标.png differ diff --git a/src/app/(marketing)/about/client.tsx b/src/app/(marketing)/about/client.tsx index b6dd273..be76760 100644 --- a/src/app/(marketing)/about/client.tsx +++ b/src/app/(marketing)/about/client.tsx @@ -7,7 +7,7 @@ import { COMPANY_INFO, STATS } from '@/lib/constants'; import { Card, CardContent } from '@/components/ui/card'; import { PageHeader } from '@/components/ui/page-header'; import { FlipClock } from '@/components/ui/flip-clock'; -import { Lightbulb, Users, Target, Award, MapPin, Mail, Phone } from 'lucide-react'; +import { Lightbulb, Users, Target, Award, MapPin, Mail } from 'lucide-react'; export function AboutClient() { const contentRef = useRef(null); @@ -115,7 +115,7 @@ export function AboutClient() {
- 我们不把"项目交付"当作终点。 + 我们不把“项目交付”当作终点。
您的业务增长了吗?您的团队能力提升了吗? @@ -124,7 +124,7 @@ export function AboutClient() { 您下一次遇到难题时,还会第一个想到我们吗?
- 这些问题,比"项目是否按时交付"更让我们在意。 + 这些问题,比“项目是否按时交付”更让我们在意。
@@ -257,15 +257,7 @@ export function AboutClient() {
{COMPANY_INFO.email}
-联系电话
-{COMPANY_INFO.phone}
-电话
- - {COMPANY_INFO.phone} - -工作日 2 小时内快速响应您的咨询
提供免费的业务咨询和方案评估服务
根据您的需求量身定制最优解决方案
- 四川睿新致远科技有限公司(以下简称"我们"、"公司")深知个人信息对您的重要性,并会尽全力保护您的个人信息安全可靠。我们致力于维持您对我们的信任,恪守以下原则,保护您的个人信息:权责一致原则、目的明确原则、选择同意原则、最少够用原则、确保安全原则、主体参与原则、公开透明原则等。 + 四川睿新致远科技有限公司(以下简称“我们”、“公司”)深知个人信息对您的重要性,并会尽全力保护您的个人信息安全可靠。我们致力于维持您对我们的信任,恪守以下原则,保护您的个人信息:权责一致原则、目的明确原则、选择同意原则、最少够用原则、确保安全原则、主体参与原则、公开透明原则等。
本隐私政策适用于您通过四川睿新致远科技有限公司官方网站、移动应用、产品服务等渠道访问和使用我们的产品和服务时,我们收集和使用您的个人信息的情形。 @@ -145,7 +145,6 @@ export default function PrivacyPolicyPage() {
- 欢迎使用四川睿新致远科技有限公司(以下简称"我们"、"公司")提供的产品和服务。在使用我们的产品和服务之前,请您仔细阅读并理解本服务条款。如果您不同意本服务条款的任何内容,请停止使用我们的产品和服务。 + 欢迎使用四川睿新致远科技有限公司(以下简称“我们”、“公司”)提供的产品和服务。在使用我们的产品和服务之前,请您仔细阅读并理解本服务条款。如果您不同意本服务条款的任何内容,请停止使用我们的产品和服务。
本服务条款是您与四川睿新致远科技有限公司之间就使用我们的产品和服务所订立的协议。我们有权根据需要不时修改本服务条款,修改后的条款一旦公布即代替原条款,恕不另行通知。
@@ -113,7 +113,7 @@ export default function TermsOfServicePage() {
- 我们的产品和服务按"现状"和"可用"基础提供,不提供任何明示或暗示的保证,包括但不限于对适销性、适用性、非侵权性或准确性、可靠性的保证。
+ 我们的产品和服务按“现状”和“可用”基础提供,不提供任何明示或暗示的保证,包括但不限于对适销性、适用性、非侵权性或准确性、可靠性的保证。
我们不对以下情况承担责任:
@@ -164,7 +164,6 @@ export default function TermsOfServicePage() {
六、免责声明
工作日 2 小时内快速响应您的咨询
提供免费的业务咨询和方案评估服务
根据您的需求量身定制最优解决方案