feat(cms): 添加 CMS 内容管理系统与 Admin 管理后台

- 新增 Prisma + SQLite 数据库模型 (Category, Content, Media, User 等)
- 新增 Admin 管理后台 (认证、内容管理、媒体管理)
- 新增 CMS API 路由 (CRUD, 草稿/发布, 重新验证)
- 新增 CMS 内容版本的历史归档页面
- 新增 components/cms 内容渲染组件
- 新增 components/admin 管理后台 UI 组件
- 更新 Contact API 路由
This commit is contained in:
张翔
2026-07-07 06:53:58 +08:00
parent 829d83522c
commit b5245f9aa2
87 changed files with 25659 additions and 0 deletions
@@ -0,0 +1,126 @@
-- CreateTable
CREATE TABLE "User" (
"id" TEXT NOT NULL PRIMARY KEY,
"username" TEXT NOT NULL,
"password" TEXT NOT NULL,
"nickname" TEXT NOT NULL DEFAULT '',
"avatar" TEXT NOT NULL DEFAULT '',
"email" TEXT NOT NULL DEFAULT '',
"phone" TEXT NOT NULL DEFAULT '',
"status" INTEGER NOT NULL DEFAULT 1,
"role" TEXT NOT NULL DEFAULT 'admin',
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL
);
-- CreateTable
CREATE TABLE "ContentModel" (
"id" TEXT NOT NULL PRIMARY KEY,
"code" TEXT NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT NOT NULL DEFAULT '',
"fields" TEXT NOT NULL,
"isPageType" BOOLEAN NOT NULL DEFAULT false,
"urlPattern" TEXT NOT NULL DEFAULT '',
"hasVersions" BOOLEAN NOT NULL DEFAULT false,
"hasDraft" BOOLEAN NOT NULL DEFAULT false,
"icon" TEXT NOT NULL DEFAULT '',
"sortOrder" INTEGER NOT NULL DEFAULT 0,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL
);
-- CreateTable
CREATE TABLE "ContentItem" (
"id" TEXT NOT NULL PRIMARY KEY,
"modelId" TEXT NOT NULL,
"modelCode" TEXT NOT NULL,
"title" TEXT NOT NULL,
"slug" TEXT NOT NULL DEFAULT '',
"status" TEXT NOT NULL DEFAULT 'draft',
"data" TEXT NOT NULL,
"version" INTEGER NOT NULL DEFAULT 1,
"sortOrder" INTEGER NOT NULL DEFAULT 0,
"publishedAt" DATETIME,
"createdBy" TEXT NOT NULL DEFAULT 'system',
"updatedBy" TEXT NOT NULL DEFAULT 'system',
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "ContentItem_modelId_fkey" FOREIGN KEY ("modelId") REFERENCES "ContentModel" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "ContentZone" (
"id" TEXT NOT NULL PRIMARY KEY,
"code" TEXT NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT NOT NULL DEFAULT '',
"pageCode" TEXT NOT NULL DEFAULT '',
"zoneKey" TEXT NOT NULL DEFAULT '',
"allowedModels" TEXT NOT NULL DEFAULT '[]',
"items" TEXT NOT NULL DEFAULT '[]',
"settings" TEXT NOT NULL DEFAULT '{}',
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL
);
-- CreateTable
CREATE TABLE "MediaAsset" (
"id" TEXT NOT NULL PRIMARY KEY,
"name" TEXT NOT NULL,
"path" TEXT NOT NULL,
"url" TEXT NOT NULL,
"mimeType" TEXT NOT NULL,
"size" INTEGER NOT NULL DEFAULT 0,
"width" INTEGER NOT NULL DEFAULT 0,
"height" INTEGER NOT NULL DEFAULT 0,
"alt" TEXT NOT NULL DEFAULT '',
"storageType" TEXT NOT NULL DEFAULT 'local',
"createdBy" TEXT NOT NULL DEFAULT 'system',
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL
);
-- CreateTable
CREATE TABLE "AuditLog" (
"id" TEXT NOT NULL PRIMARY KEY,
"module" TEXT NOT NULL,
"targetId" TEXT NOT NULL,
"action" TEXT NOT NULL,
"operator" TEXT NOT NULL,
"beforeData" TEXT NOT NULL DEFAULT '{}',
"afterData" TEXT NOT NULL DEFAULT '{}',
"ip" TEXT NOT NULL DEFAULT '',
"userAgent" TEXT NOT NULL DEFAULT '',
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- CreateIndex
CREATE UNIQUE INDEX "User_username_key" ON "User"("username");
-- CreateIndex
CREATE UNIQUE INDEX "ContentModel_code_key" ON "ContentModel"("code");
-- CreateIndex
CREATE INDEX "ContentItem_modelCode_idx" ON "ContentItem"("modelCode");
-- CreateIndex
CREATE INDEX "ContentItem_status_idx" ON "ContentItem"("status");
-- CreateIndex
CREATE UNIQUE INDEX "ContentItem_modelCode_slug_key" ON "ContentItem"("modelCode", "slug");
-- CreateIndex
CREATE UNIQUE INDEX "ContentZone_code_key" ON "ContentZone"("code");
-- CreateIndex
CREATE INDEX "ContentZone_pageCode_idx" ON "ContentZone"("pageCode");
-- CreateIndex
CREATE INDEX "AuditLog_module_idx" ON "AuditLog"("module");
-- CreateIndex
CREATE INDEX "AuditLog_targetId_idx" ON "AuditLog"("targetId");
-- CreateIndex
CREATE INDEX "AuditLog_createdAt_idx" ON "AuditLog"("createdAt");
+3
View File
@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (e.g., Git)
provider = "sqlite"
+125
View File
@@ -0,0 +1,125 @@
generator client {
provider = "prisma-client"
output = "../src/generated/prisma"
}
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
// ============ 用户与认证 ============
model User {
id String @id @default(cuid())
username String @unique
password String
nickname String @default("")
avatar String @default("")
email String @default("")
phone String @default("")
status Int @default(1) // 1=正常, 0=禁用
role String @default("admin") // admin | editor
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
// ============ 内容模型定义 ============
model ContentModel {
id String @id @default(cuid())
code String @unique
name String
description String @default("")
fields String // JSON: FieldDefinition[]
isPageType Boolean @default(false)
urlPattern String @default("")
hasVersions Boolean @default(false)
hasDraft Boolean @default(false)
icon String @default("")
sortOrder Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
items ContentItem[]
}
// ============ 内容条目 ============
model ContentItem {
id String @id @default(cuid())
modelId String
modelCode String
title String
slug String @default("")
status String @default("draft") // draft | published | archived
data String // JSON: Record<string, unknown>
version Int @default(1)
sortOrder Int @default(0)
publishedAt DateTime?
createdBy String @default("system")
updatedBy String @default("system")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
model ContentModel @relation(fields: [modelId], references: [id])
@@unique([modelCode, slug])
@@index([modelCode])
@@index([status])
}
// ============ 内容区域 ============
model ContentZone {
id String @id @default(cuid())
code String @unique
name String
description String @default("")
pageCode String @default("")
zoneKey String @default("")
allowedModels String @default("[]") // JSON: string[]
items String @default("[]") // JSON: ContentZoneItem[]
settings String @default("{}") // JSON: ContentZoneSettings
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([pageCode])
}
// ============ 媒体资源 ============
model MediaAsset {
id String @id @default(cuid())
name String
path String
url String
mimeType String
size Int @default(0)
width Int @default(0)
height Int @default(0)
alt String @default("")
storageType String @default("local") // local | oss | s3
createdBy String @default("system")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
// ============ 操作日志 ============
model AuditLog {
id String @id @default(cuid())
module String // model | item | zone | theme | media
targetId String
action String // create | update | delete | publish | unpublish
operator String
beforeData String @default("{}") // JSON
afterData String @default("{}") // JSON
ip String @default("")
userAgent String @default("")
createdAt DateTime @default(now())
@@index([module])
@@index([targetId])
@@index([createdAt])
}
+601
View File
@@ -0,0 +1,601 @@
import 'dotenv/config';
import { PrismaClient } from '../src/generated/prisma/client';
import { hashPassword } from '../src/lib/auth';
import { CONTENT_TYPE_CONFIGS } from '../src/lib/cms/content-types';
import { CASE_STUDIES } from '../src/lib/constants/cases';
import { NEWS } from '../src/lib/constants/news';
import { SERVICES } from '../src/lib/constants/services';
import { PRODUCTS } from '../src/lib/constants/products';
import { SOLUTIONS } from '../src/lib/constants/solutions';
import { STATS } from '../src/lib/constants/stats';
const prisma = new PrismaClient();
async function getModelId(code: string): Promise<string> {
const model = await prisma.contentModel.findUnique({ where: { code } });
if (!model) throw new Error(`模型 ${code} 未找到`);
return model.id;
}
async function seedItems(
modelCode: string,
items: Record<string, unknown>[],
options: { slugField?: string; titleField?: string } = {},
) {
const modelId = await getModelId(modelCode);
const slugField = options.slugField || 'slug';
const titleField = options.titleField || 'title';
for (let i = 0; i < items.length; i++) {
const item = items[i]!;
const slug = String(item[slugField] || item.id || `${modelCode}-${i}`);
const title = String(item[titleField] || `未命名 ${i + 1}`);
const { id, slug: _s, title: _t, ...data } = item as Record<string, unknown>;
await prisma.contentItem.upsert({
where: { modelCode_slug: { modelCode, slug } },
update: {
title,
data: JSON.stringify(data),
sortOrder: i,
status: 'published',
publishedAt: new Date(),
},
create: {
modelId,
modelCode,
title,
slug,
data: JSON.stringify(data),
sortOrder: i,
status: 'published',
publishedAt: new Date(),
createdBy: 'system',
updatedBy: 'system',
},
});
}
}
async function main() {
console.log('🌱 开始数据库初始化...');
// ============ 1. 创建管理员用户 ============
console.log('📝 创建管理员用户...');
const adminPassword = await hashPassword('admin123');
await prisma.user.upsert({
where: { username: 'admin' },
update: {},
create: {
username: 'admin',
password: adminPassword,
nickname: '管理员',
role: 'admin',
status: 1,
},
});
console.log(' ✅ 管理员: admin / admin123');
// ============ 2. 创建内容模型 ============
console.log('📝 创建内容模型...');
const modelConfigs = [
...Object.values(CONTENT_TYPE_CONFIGS),
{
model: {
code: 'case-study',
name: '案例研究',
description: '客户成功案例与实践分享',
isPageType: true,
urlPattern: '/cases/{slug}',
hasVersions: true,
hasDraft: true,
icon: 'briefcase',
fields: [
{ name: 'client', type: 'text', label: '客户名称', required: true },
{ name: 'industry', type: 'text', label: '所属行业', required: true },
{ name: 'companySize', type: 'text', label: '公司规模', required: true },
{ name: 'subtitle', type: 'text', label: '副标题', required: true },
{ name: 'challenge', type: 'textarea', label: '挑战', required: true },
{ name: 'solution', type: 'textarea', label: '解决方案', required: true },
{ name: 'result', type: 'textarea', label: '实施成果', required: true },
{ name: 'color', type: 'text', label: '主题色', defaultValue: 'brand' },
{ name: 'featured', type: 'boolean', label: '精选案例', defaultValue: false },
],
},
},
// === about-page ===
{
model: {
code: 'about-page',
name: '关于我们',
description: '公司介绍、核心价值、发展历程与资质认证信息',
isPageType: true,
urlPattern: '/about',
hasVersions: true,
hasDraft: true,
icon: 'building2',
fields: [
{ name: 'heroSubtitle', type: 'text', label: 'Hero 标签', required: true },
{ name: 'heroTitle', type: 'text', label: 'Hero 主标题', required: true },
{ name: 'heroDescription', type: 'textarea', label: 'Hero 描述', required: true },
{ name: 'whoWeAreTitle', type: 'text', label: '"我们是谁"标题' },
{ name: 'whoWeAreDescription', type: 'textarea', label: '"我们是谁"描述' },
{ name: 'promise', type: 'textarea', label: '承诺标语', required: true },
{ name: 'coreValues', type: 'array', label: '核心价值观', fields: [
{ name: 'number', type: 'text', label: '编号' },
{ name: 'title', type: 'text', label: '标题' },
{ name: 'description', type: 'textarea', label: '描述' },
]},
{ name: 'milestones', type: 'array', label: '发展历程', fields: [
{ name: 'year', type: 'text', label: '年份' },
{ name: 'title', type: 'text', label: '标题' },
{ name: 'description', type: 'textarea', label: '描述' },
{ name: 'highlight', type: 'text', label: '亮点' },
]},
{ name: 'keyMetrics', type: 'array', label: '关键指标', fields: [
{ name: 'value', type: 'text', label: '数值' },
{ name: 'label', type: 'text', label: '标签' },
{ name: 'sub', type: 'text', label: '副文本' },
]},
{ name: 'certifications', type: 'array', label: '资质认证', fields: [
{ name: 'name', type: 'text', label: '名称' },
{ name: 'desc', type: 'text', label: '描述' },
{ name: 'icon', type: 'text', label: '图标名称' },
]},
{ name: 'partners', type: 'array', label: '合作伙伴', fields: [
{ name: 'name', type: 'text', label: '名称' },
]},
{ name: 'ctaTitle', type: 'text', label: 'CTA 标题' },
{ name: 'ctaDescription', type: 'textarea', label: 'CTA 描述' },
],
},
},
// === team-page ===
{
model: {
code: 'team-page',
name: '团队介绍',
description: '团队实力、文化氛围、数据指标',
isPageType: true,
urlPattern: '/team',
hasVersions: true,
hasDraft: true,
icon: 'users',
fields: [
{ name: 'heroSubtitle', type: 'text', label: 'Hero 标签', required: true },
{ name: 'heroTitle', type: 'textarea', label: 'Hero 主标题', required: true },
{ name: 'heroDescription', type: 'textarea', label: 'Hero 描述', required: true },
{ name: 'stats', type: 'array', label: '统计指标', fields: [
{ name: 'value', type: 'number', label: '数值' },
{ name: 'suffix', type: 'text', label: '后缀' },
{ name: 'label', type: 'text', label: '标签' },
]},
{ name: 'strengths', type: 'array', label: '团队优势', fields: [
{ name: 'icon', type: 'text', label: '图标名称' },
{ name: 'number', type: 'text', label: '编号' },
{ name: 'title', type: 'text', label: '标题' },
{ name: 'description', type: 'textarea', label: '描述' },
]},
{ name: 'culture', type: 'array', label: '团队文化', fields: [
{ name: 'icon', type: 'text', label: '图标名称' },
{ name: 'number', type: 'text', label: '编号' },
{ name: 'title', type: 'text', label: '标题' },
{ name: 'description', type: 'textarea', label: '描述' },
]},
{ name: 'teamAboutTitle', type: 'text', label: '团队介绍标题' },
{ name: 'ctaTitle', type: 'text', label: 'CTA 标题' },
{ name: 'ctaDescription', type: 'textarea', label: 'CTA 描述' },
],
},
},
// === contact-page ===
{
model: {
code: 'contact-page',
name: '联系我们',
description: '联系信息、工作时间与服务承诺',
isPageType: true,
urlPattern: '/contact',
hasVersions: true,
hasDraft: true,
icon: 'phone',
fields: [
{ name: 'heroSubtitle', type: 'text', label: 'Hero 标签', required: true },
{ name: 'heroTitle', type: 'textarea', label: 'Hero 主标题', required: true },
{ name: 'heroDescription', type: 'textarea', label: 'Hero 描述', required: true },
{ name: 'companyName', type: 'text', label: '公司名称', required: true },
{ name: 'companyEmail', type: 'text', label: '联系邮箱', required: true },
{ name: 'companyAddress', type: 'text', label: '公司地址', required: true },
{ name: 'workHoursWeekday', type: 'text', label: '工作日描述' },
{ name: 'workHoursTime', type: 'text', label: '工作时间段' },
{ name: 'promises', type: 'array', label: '服务承诺', fields: [
{ name: 'text', type: 'textarea', label: '承诺内容' },
]},
{ name: 'ctaTitle', type: 'text', label: 'CTA 标题' },
{ name: 'ctaDescription', type: 'textarea', label: 'CTA 描述' },
],
},
},
// === legal-page ===
{
model: {
code: 'legal-page',
name: '法律条款',
description: '隐私政策和服务条款的页面内容',
isPageType: true,
urlPattern: '/legal/{slug}',
hasVersions: true,
hasDraft: true,
icon: 'file-text',
fields: [
{ name: 'pageTitle', type: 'text', label: '页面标题', required: true },
{ name: 'pageSubtitle', type: 'text', label: '页面副标题' },
{ name: 'sections', type: 'array', label: '内容章节', fields: [
{ name: 'title', type: 'text', label: '章节标题' },
{ name: 'content', type: 'textarea', label: '章节内容' },
]},
{ name: 'lastUpdated', type: 'text', label: '最后更新日期' },
],
},
},
];
for (let i = 0; i < modelConfigs.length; i++) {
const config = modelConfigs[i]!;
await prisma.contentModel.upsert({
where: { code: config.model.code },
update: {
name: config.model.name,
description: config.model.description,
fields: JSON.stringify(config.model.fields),
isPageType: config.model.isPageType,
urlPattern: config.model.urlPattern || '',
hasVersions: config.model.hasVersions,
hasDraft: config.model.hasDraft,
icon: config.model.icon || '',
sortOrder: i,
},
create: {
code: config.model.code,
name: config.model.name,
description: config.model.description,
fields: JSON.stringify(config.model.fields),
isPageType: config.model.isPageType,
urlPattern: config.model.urlPattern || '',
hasVersions: config.model.hasVersions,
hasDraft: config.model.hasDraft,
icon: config.model.icon || '',
sortOrder: i,
},
});
console.log(` ✅ 模型: ${config.model.name} (${config.model.code})`);
}
// ============ 3. 导入内容数据 ============
console.log('📝 导入案例研究...');
await seedItems('case-study', CASE_STUDIES as unknown as Record<string, unknown>[]);
console.log(`${CASE_STUDIES.length} 个案例`);
console.log('📝 导入新闻...');
await seedItems('news', NEWS as unknown as Record<string, unknown>[]);
console.log(`${NEWS.length} 条新闻`);
console.log('📝 导入服务...');
await seedItems('service', SERVICES as unknown as Record<string, unknown>[]);
console.log(`${SERVICES.length} 个服务`);
console.log('📝 导入产品...');
await seedItems('product', PRODUCTS as unknown as Record<string, unknown>[]);
console.log(`${PRODUCTS.length} 个产品`);
console.log('📝 导入解决方案...');
await seedItems('solution', SOLUTIONS as unknown as Record<string, unknown>[]);
console.log(`${SOLUTIONS.length} 个解决方案`);
// 数据指标需要特殊处理
console.log('📝 导入数据指标...');
const statList = Array.isArray(STATS) ? STATS : (STATS as { stats?: unknown[] }).stats || [];
const statModelId = await getModelId('stat-item');
for (let i = 0; i < statList.length; i++) {
const s = statList[i] as Record<string, unknown>;
const slug = `stat-${i}`;
const title = String(s.label || `指标 ${i + 1}`);
const { id, ...data } = s;
await prisma.contentItem.upsert({
where: { modelCode_slug: { modelCode: 'stat-item', slug } },
update: {
title,
data: JSON.stringify({ ...data, accentColor: s.color }),
sortOrder: i,
status: 'published',
publishedAt: new Date(),
},
create: {
modelId: statModelId,
modelCode: 'stat-item',
title,
slug,
data: JSON.stringify({ ...data, accentColor: s.color }),
sortOrder: i,
status: 'published',
publishedAt: new Date(),
createdBy: 'system',
updatedBy: 'system',
},
});
}
console.log(`${statList.length} 个数据指标`);
// ============ 4. 导入页面内容 ============
console.log('📝 导入关于页面...');
await seedItems('about-page', [
{
slug: 'default',
title: '关于我们',
heroSubtitle: 'About Us',
heroTitle: '12 年深耕\n500+ 企业\n的转型伙伴',
heroDescription: '我们不把项目完成当作终点。您的业务增长了吗?您的团队能力提升了吗?\n这才是我们真正在意的事。',
heroCtaText: '预约咨询',
heroCtaLink: '/contact',
heroSecondaryCtaText: '了解服务',
heroSecondaryCtaLink: '/services',
whoWeAreTitle: '我们是谁',
whoWeAreDescription: '我们坚持对行业趋势的深度研究,不追逐昙花一现的概念。每一次方案,都源于对您业务场景的洞察;每一次连接,都为了让技术真正服务于您的未来。',
promise: '不卖您用不上的技术,不说不懂业务的术语,不做路过就忘的一锤子买卖。',
coreValues: [
{ number: '01', title: '结果导向', description: '不以「项目上线」为终点,以「客户业务是否真正改善」为衡量标准。每一次交付,都追求可量化的价值。' },
{ number: '02', title: '长期主义', description: '不追逐风口,只做真正为客户创造价值的事。您的下一次难题,还愿意第一个想到我们——这才是我们追求的成功。' },
{ number: '03', title: '技术驱动', description: '用扎实的工程能力和行业经验赢得信任。技术不是炫技的工具,而是解决真实问题的手段。' },
],
milestones: [
{ year: '2014', title: '团队启航', description: '核心团队始于大型企业数字化转型项目,深耕 ERP、CRM 实施与定制开发。', highlight: '服务首批 20+ 企业客户' },
{ year: '2018', title: '业务扩展', description: '从单一实施服务向全链路咨询转型,建立战略咨询 + 技术落地的双轮驱动模式。', highlight: '客户突破 100 家' },
{ year: '2021', title: '产品化起步', description: '开始自研产品矩阵,从项目制向「产品+服务」双模式升级。', highlight: '3 款自研产品上线' },
{ year: '2023', title: '行业深耕', description: '聚焦制造、零售、医疗三大核心行业,建立行业解决方案库与最佳实践沉淀。', highlight: '客户续约率 95%+' },
{ year: '2026', title: '睿新致远成立', description: '四川睿新致远科技有限公司在成都正式成立,以全新品牌开启下一段征程。', highlight: '全新启航,步履不停' },
],
keyMetrics: [
{ value: '500+', label: '服务企业', sub: '六大行业覆盖' },
{ value: '200+', label: '专业顾问', sub: '平均行业经验 8 年' },
{ value: '98%', label: '客户满意度', sub: '97% 续约率' },
{ value: '12年', label: '行业深耕', sub: 'since 2014' },
],
certifications: [
{ name: 'ISO 27001', description: '信息安全管理体系认证', icon: 'Shield' },
{ name: 'ISO 9001', description: '质量管理体系认证', icon: 'Award' },
{ name: 'CMMI 3级', description: '软件能力成熟度认证', icon: 'Zap' },
{ name: '高新技术企业', description: '国家高新技术企业认定', icon: 'Trophy' },
],
partners: [
'SAP', 'Oracle', 'Salesforce', 'Microsoft',
'阿里云', '腾讯云', '华为云', '金蝶',
],
ctaTitle: '准备好开始了吗?',
ctaDescription: '无论您是想了解合作机会,还是希望深入探讨具体方案,都欢迎随时联系。',
},
]);
console.log(' ✅ 关于页面');
console.log('📝 导入团队页面...');
await seedItems('team-page', [
{
slug: 'default',
title: '团队介绍',
heroSubtitle: 'Our Team',
heroTitle: '既懂技术又懂业务的\n复合型团队',
heroDescription: '我们的成员不只是写代码的工程师——我们是能理解您业务场景、能和您一起想清楚问题的合作伙伴。',
heroCtaText: '与我们交流',
heroCtaLink: '/contact',
heroSecondaryCtaText: '了解公司',
heroSecondaryCtaLink: '/about',
stats: [
{ value: 12, suffix: '+', label: '年行业经验' },
{ value: 10, suffix: '+', label: '核心成员' },
{ value: 5, suffix: '+', label: '行业覆盖' },
],
strengths: [
{ icon: 'Shield', number: '01', title: '12 年+ 行业深耕', description: '核心团队长期从事技术咨询、企业数字化等领域,服务覆盖金融、制造、零售、政务、农业等多个行业。' },
{ icon: 'Building2', number: '02', title: '大型 IT 企业背景', description: '开发团队成员来自多个大型传统 IT 企业,具备扎实的工程能力、规范化的交付流程和严格的质量意识。' },
{ icon: 'Users', number: '03', title: '复合型技术团队', description: '既懂技术又懂业务,能够深入理解客户的真实场景和痛点,提供真正可落地的解决方案。' },
{ icon: 'Code', number: '04', title: '全栈技术能力', description: '从前端到后端、从云原生到数据智能、从移动端到物联网——应对各种复杂技术挑战。' },
{ icon: 'Target', number: '05', title: '结果导向交付', description: '不以"项目上线"为终点,以"客户业务是否真正改善"为衡量标准。每一个交付成果都追求可量化的业务价值。' },
],
culture: [
{ icon: 'Layers', number: '01', title: '扁平化协作', description: '没有冗长的汇报链条,每个人都有机会直接参与决策。信息透明,沟通高效。' },
{ icon: 'BookOpen', number: '02', title: '持续学习', description: '技术日新月异,我们保持对新技术的好奇。内部分享、技术沙龙、外部培训——学习是日常的一部分。' },
{ icon: 'Sparkles', number: '03', title: '客户成功', description: '我们的成就感来自客户的成功。客户的业务增长,就是我们最好的成绩单。' },
],
teamAboutTitle: '关于我们的团队',
teamAboutParagraphs: [
'我们的核心团队长期从事技术咨询、企业数字化等行业,拥有 12 年以上的深厚积累。',
'开发团队成员来自于多个大型传统 IT 企业,具备扎实的工程能力和规范化的交付经验。',
'我们相信,优秀的技术咨询不仅需要过硬的技术能力,更需要深入理解客户的业务场景和真实需求。每一位成员都是既懂技术又懂业务的复合型人才。',
],
strengthsSectionTitle: '我们的底气,从何而来',
strengthsSectionDescription: '不是自吹自擂,而是多年实战积累的真实能力',
cultureSectionTitle: '我们怎样工作',
cultureSectionDescription: '好的团队文化,最终体现在为客户创造的价值上',
ctaTitle: '想成为我们的一员?',
ctaDescription: '我们始终在寻找既懂技术又热爱业务的伙伴。如果您认同我们的理念,欢迎聊聊。',
ctaPrimaryText: '联系我们',
ctaPrimaryLink: '/contact',
ctaSecondaryText: '了解公司',
ctaSecondaryLink: '/about',
},
]);
console.log(' ✅ 团队页面');
console.log('📝 导入联系页面...');
await seedItems('contact-page', [
{
slug: 'default',
title: '联系我们',
heroSubtitle: 'Contact Us',
heroTitle: '随时欢迎\n聊一聊',
heroDescription: '工作日 2 小时内快速响应,首次咨询免费。\n没有销售压力,只是坐下来把问题理清楚。',
companyName: '四川睿新致远科技有限公司',
companyEmail: 'contact@novalon.cn',
companyAddress: '中国四川省成都市龙泉驿区幸福路12号',
workHoursWeekday: '周一至周五',
workHoursTime: '9:00 - 18:00',
promises: [
{ text: '工作日 2 小时内快速响应您的咨询' },
{ text: '提供免费的业务咨询和方案评估服务' },
{ text: '根据您的需求量身定制最优解决方案' },
],
formTitle: '发送消息',
ctaTitle: '还是想直接聊?',
ctaDescription: '表单填起来太麻烦?直接发邮件到 contact@novalon.cn,我们同样会快速回复。',
},
]);
console.log(' ✅ 联系页面');
console.log('📝 导入法律页面...');
await seedItems('legal-page', [
{
slug: 'default',
title: '法律条款',
pageSubtitle: '隐私政策与服务条款',
lastUpdated: '2026-02-26',
privacyPolicy: {
title: '隐私政策',
subtitle: '我们重视您的隐私,致力于保护您的个人信息安全',
lastUpdated: '2026-02-26',
sections: [
{ title: '引言', content: '四川睿新致远科技有限公司(以下简称"我们"、"公司")深知个人信息对您的重要性,并会尽全力保护您的个人信息安全可靠。我们致力于维持您对我们的信任,恪守以下原则,保护您的个人信息:权责一致原则、目的明确原则、选择同意原则、最少够用原则、确保安全原则、主体参与原则、公开透明原则等。本隐私政策适用于您通过四川睿新致远科技有限公司官方网站、移动应用、产品服务等渠道访问和使用我们的产品和服务时,我们收集和使用您的个人信息的情形。' },
{ title: '一、我们如何收集和使用您的个人信息', content: '我们会遵循正当、合法、必要的原则,仅为实现产品功能,向您提供服务之目的,收集和使用您的个人信息。\n\n1.1 我们收集的个人信息:\n• 账户信息:当您注册账户时,我们可能收集您的姓名、电子邮箱地址、手机号码、公司名称等。\n• 联系信息:当您通过联系表单或客服与我们沟通时,我们可能收集您的姓名、电子邮箱地址、手机号码、留言内容等。\n• 使用信息:我们可能收集您使用我们产品和服务的信息,包括访问时间、浏览记录、操作日志等。\n• 设备信息:我们可能收集您使用的设备信息,包括设备型号、操作系统、浏览器类型等。\n\n1.2 我们如何使用您的个人信息:\n• 提供产品和服务:向您提供我们的产品和服务,处理您的请求和订单。\n• 客户服务:与您联系,提供客户支持,回复您的咨询和反馈。\n• 改进产品和服务:分析使用情况,改进我们的产品和服务质量。\n• 安全保障:检测、预防和处理安全事件,保护您和我们的合法权益。\n• 法律合规:遵守法律法规要求,履行法律义务。' },
{ title: '二、我们如何共享、转让、公开披露您的个人信息', content: '2.1 共享:我们不会向其他任何公司、组织和个人分享您的个人信息,但以下情况除外:在获取明确同意的情况下分享;根据法律法规、法律程序、强制性的行政或司法要求所必须的情况进行提供;在涉及合并、收购或破产清算时,如涉及到个人信息转让,我们会要求新的持有您个人信息的公司、组织继续受本隐私政策的约束。\n\n2.2 转让:我们不会将您的个人信息转让给任何公司、组织和个人,但以下情况除外:在获取明确同意的情况下转让;根据适用的法律法规、法律程序、强制性的行政或司法要求所必须的情况进行提供;在涉及合并、收购或破产清算时。\n\n2.3 公开披露:我们仅会在以下情况下公开披露您的个人信息:获得您的明确同意;基于法律法规或法律程序的要求;在涉及合并、收购或破产清算时;为保护我们或他人的合法权益免受损害。' },
{ title: '三、我们如何保护您的个人信息', content: '我们已使用符合业界标准的安全防护措施保护您提供的个人信息,防止数据遭到未经授权访问、公开披露、使用、修改、损坏或丢失。我们会采取一切合理可行的措施,保护您的个人信息:\n• 使用加密技术确保数据传输和存储安全。\n• 限制访问权限,仅授权人员可访问个人信息。\n• 定期进行安全审计和风险评估。\n• 建立数据备份和恢复机制。\n• 制定应急响应预案,及时处理安全事件。' },
{ title: '四、您的权利', content: '按照中国相关的法律、法规、标准,以及其他国家、地区的通行做法,我们保障您对自己的个人信息行使以下权利:访问您的个人信息;更正您的个人信息;删除您的个人信息;改变您授权同意的范围;注销您的账户;获取您的个人信息副本。如您需要行使上述权利,请通过本隐私政策提供的联系方式与我们联系。' },
{ title: '五、未成年人保护', content: '我们非常重视对未成年人个人信息的保护。如果您是18周岁以下的未成年人,在使用我们的产品和服务前,应事先取得您家长或法定监护人的同意。如您是未成年人的监护人,当您对您所监护的未成年人的个人信息处理存在疑问时,请通过本隐私政策提供的联系方式与我们联系。' },
{ title: '六、隐私政策的更新', content: '我们可能适时更新本隐私政策的条款,该等更新构成本隐私政策的一部分。如该等更新造成您在本隐私政策下权利的实质减少,我们将在更新生效前通过在主页上显著位置提示或向您发送电子邮件或其他方式通知您,在该种情况下,若您继续使用我们的服务,即表示同意受经修订的本隐私政策的约束。' },
{ title: '七、Cookie 和网站分析工具', content: '7.1 Cookie 使用说明:我们使用 Cookie 和类似技术来提供、保护和改进我们的服务。Cookie 是存储在您设备上的小型文本文件,帮助我们识别您的设备、记住您的偏好设置。\n\nCookie类型及用途:\n• 必要Cookie:网站基本功能运行,会话期间,必需\n• 分析Cookie:了解网站使用情况改进服务,14个月,非必需\n• 营销Cookie:个性化广告(当前未使用),非必需\n\n7.2 Google Analytics 使用说明:我们使用 Google Analytics 4(由 Google LLC 提供)分析网站使用情况。收集数据包括:访问的页面和停留时间;设备类型、浏览器类型;地理位置(国家/城市级别,IP地址已匿名化);访问来源。\n已采取的保护措施:IP地址匿名化;数据保留期限设为14个月;禁用广告个性化功能;禁用Google信号;不与Google其他服务共享数据用于广告目的。\n\n7.3 您的选择:您可以在首次访问时选择接受或拒绝分析Cookie;可以点击"Cookie设置"按钮随时更改偏好;可以通过浏览器设置删除或阻止Cookie。\n\n7.4 数据删除请求:如您希望删除我们持有的您的个人数据,请通过 privacy@novalon.cn 联系我们。联系地址:中国四川省成都市龙泉驿区幸福路12号。' },
{ title: '八、如何联系我们', content: '如果您对本隐私政策有任何疑问、意见或建议,或需要行使您的权利,请通过以下方式与我们联系:\n\n公司名称:四川睿新致远科技有限公司\n联系邮箱:contact@novalon.cn\n隐私邮箱:privacy@novalon.cn\n联系地址:中国四川省成都市龙泉驿区幸福路12号' },
],
},
termsOfService: {
title: '服务条款',
subtitle: '请仔细阅读以下服务条款,使用我们的服务即表示您同意这些条款',
lastUpdated: '2026-02-26',
sections: [
{ title: '引言', content: '欢迎使用四川睿新致远科技有限公司(以下简称"我们"、"公司")提供的产品和服务。在使用我们的产品和服务之前,请您仔细阅读并理解本服务条款。如果您不同意本服务条款的任何内容,请停止使用我们的产品和服务。本服务条款是您与四川睿新致远科技有限公司之间就使用我们的产品和服务所订立的协议。我们有权根据需要不时修改本服务条款,修改后的条款一旦公布即代替原条款,恕不另行通知。' },
{ title: '一、服务内容', content: '我们提供的产品和服务包括但不限于:软件开发、云服务、数据分析、信息安全、企业级软件产品(如ERP、CRM、后台管理系统、BI等)等。具体服务内容以我们官方网站或产品文档为准。我们保留随时修改、暂停或终止部分或全部服务的权利,无需事先通知。对于因服务修改、暂停或终止而给您造成的任何损失,我们不承担任何责任,除非法律另有规定。' },
{ title: '二、用户注册与账户', content: '2.1 注册资格:您确认,在您完成注册程序或以其他方式实际使用本服务时,您应当是具备完全民事权利能力和完全民事行为能力的自然人、法人或其他组织。若您不具备前述主体资格,则您及您的监护人应承担因此而导致的一切后果,且我们有权注销或永久冻结您的账户。\n\n2.2 账户安全:您有责任维护您账户的安全性和保密性。您不得向任何第三方泄露您的账户信息,也不得与他人共享您的账户。如果您发现任何未经授权使用您账户的情况,应立即通知我们。您对您账户下发生的所有活动负责。我们对因您未能维护账户安全而造成的任何损失不承担责任。' },
{ title: '三、用户行为规范', content: '在使用我们的产品和服务时,您同意遵守以下行为规范:\n• 遵守所有适用的法律法规和本服务条款。\n• 不得利用我们的产品和服务进行任何违法或不当活动。\n• 不得干扰或破坏我们的产品和服务或与我们的产品和服务相连的服务器和网络。\n• 不得上传、传播或存储任何违法、有害、威胁、诽谤、骚扰、侵权或其他不当内容。\n• 不得侵犯他人的知识产权、隐私权或其他合法权益。\n• 不得进行任何形式的商业欺诈或诈骗活动。\n• 不得恶意收集或获取其他用户的信息。\n如果我们认定您违反了本服务条款或任何适用法律,我们有权在不事先通知的情况下,暂停或终止您的账户,并拒绝您现在或将来使用我们的产品和服务。' },
{ title: '四、知识产权', content: '我们的产品和服务中包含的所有内容,包括但不限于软件、设计、文字、图片、音频、视频、商标、服务标识等,均受著作权法、商标法、专利法或其他适用法律的保护。除非另有明确说明,我们拥有或持有我们产品和服务中所有内容的所有知识产权。您不得以任何形式复制、修改、传播、展示、执行、创作衍生作品、转让、出售或以其他方式使用这些内容,除非获得我们的明确书面许可。您在使用我们的产品和服务时产生的任何内容,您仍保留其知识产权,但您授予我们全球性、非独占性、免版税的许可,以使用、复制、修改、传播、展示和执行这些内容,仅限于提供和改进我们的产品和服务。' },
{ title: '五、服务费用与支付', content: '5.1 费用标准:我们的产品和服务可能需要支付费用。具体费用标准以我们官方网站或产品文档公布的价格为准。我们保留随时调整价格的权利,调整后的价格适用于调整后的新订单或续费。\n\n5.2 支付方式:我们接受多种支付方式,包括但不限于银行转账、支付宝、微信支付等。\n\n5.3 退款政策:除非另有明确说明,我们提供的产品和服务一经售出,不予退款。如因我们的原因导致产品或服务无法正常使用,我们将根据实际情况提供相应的补偿或解决方案。' },
{ title: '六、免责声明', content: '我们的产品和服务按"现状"和"可用"基础提供,不提供任何明示或暗示的保证,包括但不限于对适销性、适用性、非侵权性或准确性、可靠性的保证。\n\n我们不对以下情况承担责任:\n• 因您违反本服务条款或任何适用法律而导致的任何损失或损害。\n• 因不可抗力、网络故障、设备故障等不可控因素导致的服务中断或数据丢失。\n• 因第三方服务或内容导致的任何损失或损害。\n• 因您使用或无法使用我们的产品和服务而导致的任何间接、附带、特殊或后果性损害。' },
{ title: '七、服务终止', content: '您可以随时停止使用我们的产品和服务,并注销您的账户。我们也有权在不事先通知的情况下,因以下原因暂停或终止您的账户:\n• 您违反本服务条款或任何适用法律。\n• 我们出于安全、法律或商业考虑,认为有必要终止您的账户。\n• 您长时间未使用您的账户。\n• 我们停止提供相关的产品和服务。' },
{ title: '八、争议解决', content: '本服务条款的订立、执行、解释及争议解决均适用中华人民共和国法律。如就本服务条款发生任何争议,双方应首先通过友好协商解决;协商不成的,任何一方均可向公司所在地有管辖权的人民法院提起诉讼。' },
{ title: '九、其他条款', content: '• 本服务条款构成您与我们就使用我们的产品和服务所达成的完整协议,取代之前的所有口头或书面协议。\n• 如本服务条款的任何条款被认定为无效或不可执行,该条款应被限制或排除,以使其有效和可执行,其余条款继续有效。\n• 我们未行使或延迟行使本服务条款项下的任何权利或规定,不构成对该权利或规定的放弃。\n• 本服务条款的标题仅为方便而设,不影响本服务条款的解释。' },
{ title: '十、联系我们', content: '如果您对本服务条款有任何疑问、意见或建议,请通过以下方式与我们联系:\n\n公司名称:四川睿新致远科技有限公司\n联系邮箱:contact@novalon.cn\n联系地址:中国四川省成都市龙泉驿区幸福路12号' },
],
},
},
]);
console.log(' ✅ 法律页面');
// ============ 5. 创建默认内容区域 ============
console.log('📝 创建默认内容区域...');
const defaultZones = [
{
code: 'home-stats',
name: '首页 - 数据指标',
pageCode: 'home',
zoneKey: 'stats',
allowedModels: ['stat-item'],
items: statList.slice(0, 4).map((_: unknown, i: number) => ({
itemId: `stat-${i}`,
sortOrder: i,
variant: 'default',
})),
},
{
code: 'home-services',
name: '首页 - 服务展示',
pageCode: 'home',
zoneKey: 'services',
allowedModels: ['service'],
items: SERVICES.slice(0, 6).map((_: unknown, i: number) => {
const s = _ as Record<string, unknown>;
return { itemId: String(s.id || `service-${i}`), sortOrder: i, variant: 'default' };
}),
},
{
code: 'home-solutions',
name: '首页 - 解决方案',
pageCode: 'home',
zoneKey: 'solutions',
allowedModels: ['solution'],
items: SOLUTIONS.slice(0, 6).map((_: unknown, i: number) => {
const s = _ as Record<string, unknown>;
return { itemId: String(s.id || `solution-${i}`), sortOrder: i, variant: 'default' };
}),
},
{
code: 'home-cases',
name: '首页 - 精选案例',
pageCode: 'home',
zoneKey: 'cases',
allowedModels: ['case-study'],
items: CASE_STUDIES.filter((cs) => cs.featured).slice(0, 3).map((cs, i: number) => ({
itemId: cs.id,
sortOrder: i,
variant: 'default',
})),
},
{
code: 'home-news',
name: '首页 - 新闻动态',
pageCode: 'home',
zoneKey: 'news',
allowedModels: ['news'],
items: NEWS.slice(0, 3).map((_: unknown, i: number) => {
const n = _ as Record<string, unknown>;
return { itemId: String(n.id || `news-${i}`), sortOrder: i, variant: 'default' };
}),
},
];
for (const zone of defaultZones) {
await prisma.contentZone.upsert({
where: { code: zone.code },
update: {
name: zone.name,
pageCode: zone.pageCode,
zoneKey: zone.zoneKey,
allowedModels: JSON.stringify(zone.allowedModels),
items: JSON.stringify(zone.items),
},
create: {
code: zone.code,
name: zone.name,
pageCode: zone.pageCode,
zoneKey: zone.zoneKey,
allowedModels: JSON.stringify(zone.allowedModels),
items: JSON.stringify(zone.items),
},
});
console.log(` ✅ 区域: ${zone.name}`);
}
// ============ 统计 ============
const itemCount = await prisma.contentItem.count();
const modelCount = await prisma.contentModel.count();
const zoneCount = await prisma.contentZone.count();
console.log('\n🎉 数据库初始化完成!');
console.log(` 📊 内容模型: ${modelCount}`);
console.log(` 📄 内容条目: ${itemCount}`);
console.log(` 🗂️ 内容区域: ${zoneCount}`);
console.log(` 👤 管理员: admin / admin123`);
}
main()
.catch((e) => {
console.error('❌ 种子数据初始化失败:', e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});
@@ -0,0 +1,359 @@
'use client';
import { motion } from 'framer-motion';
import { ScrollReveal, StaggerReveal } from '@/components/ui/scroll-reveal';
import { Button } from '@/components/ui/button';
import { ArrowRight, Target, Heart, Lightbulb, CheckCircle2 } from 'lucide-react';
import { cn } from '@/lib/utils';
const EASE_OUT = [0.22, 1, 0.36, 1] as const;
function GrainOverlay() {
return (
<div
className="absolute inset-0 pointer-events-none opacity-[0.03] mix-blend-overlay"
style={{
backgroundImage:
"url(\"data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E\")",
}}
/>
);
}
function SectionLabel({ children, className = '' }: { children: React.ReactNode; className?: string }) {
return (
<div className={cn('flex items-center gap-5 mb-7', className)}>
<div className="w-14 h-px bg-brand" />
<span className="text-[11px] tracking-[0.4em] uppercase font-bold text-brand">
{children}
</span>
<div className="w-14 h-px bg-brand" />
</div>
);
}
const CORE_VALUES = [
{
icon: Target,
number: '01',
title: '结果导向',
description: '不以"项目上线"为终点,以"客户业务是否真正改善"为衡量标准。每一次交付,都追求可量化的价值。',
},
{
icon: Heart,
number: '02',
title: '长期主义',
description: '不追逐风口,只做真正为客户创造价值的事。您的下一次难题,还愿意第一个想到我们——这才是我们追求的成功。',
},
{
icon: Lightbulb,
number: '03',
title: '技术驱动',
description: '用扎实的工程能力和行业经验赢得信任。技术不是炫技的工具,而是解决真实问题的手段。',
},
];
const MILESTONES = [
{ date: '2026.01', title: '公司成立', description: '四川睿新致远科技有限公司在成都龙泉驿区正式成立' },
{ date: '2026.01', title: '团队组建', description: '核心团队到位,成员来自多个大型 IT 企业,具备扎实的工程能力和规范化交付经验' },
{ date: '2026.02', title: '业务启动', description: '推出企业数字化转型咨询与解决方案服务,开始接触首批客户' },
{ date: '2026.03', title: '产品研发', description: '自主研发 ERP、CRM 等核心产品,逐步构建产品矩阵' },
{ date: '2026.05', title: '研发推进', description: '多款产品进入核心功能开发阶段,同步开展早期用户体验计划' },
];
const PROMISES = [
'不卖您用不上的技术',
'不说不懂业务的术语',
'不做路过就忘的"一锤子买卖"',
];
export default function AboutContentV1() {
return (
<div className="min-h-screen bg-ink text-white overflow-x-hidden">
{/* Hero Section */}
<section className="relative pt-32 pb-24 lg:pt-40 lg:pb-32 overflow-hidden">
<GrainOverlay />
<div className="absolute inset-0 bg-gradient-to-b from-ink-light/50 to-ink pointer-events-none" />
<div className="max-w-container mx-auto px-6 lg:px-10 relative z-10">
<ScrollReveal>
<SectionLabel>
<div className="flex items-center gap-5">
<div className="w-14 h-px bg-brand" />
<span className="text-[11px] tracking-[0.4em] uppercase font-bold text-brand">
About Us
</span>
<div className="w-14 h-px bg-brand" />
</div>
</SectionLabel>
</ScrollReveal>
<ScrollReveal delay={0.1}>
<h1 className="mt-8 text-4xl md:text-5xl lg:text-6xl xl:text-7xl font-bold tracking-tight leading-[1.1]">
<span className="block mt-2 text-brand"></span>
</h1>
</ScrollReveal>
<ScrollReveal delay={0.2}>
<p className="mt-8 text-lg md:text-xl text-white/60 max-w-2xl leading-relaxed">
</p>
</ScrollReveal>
<ScrollReveal delay={0.3}>
<div className="mt-12 flex flex-wrap gap-4">
<Button size="lg" asChild>
<a href="/contact">
<ArrowRight className="w-4 h-4 ml-2" />
</a>
</Button>
<Button size="lg" variant="outline" asChild>
<a href="/team"></a>
</Button>
</div>
</ScrollReveal>
<ScrollReveal delay={0.4}>
<div className="mt-20 grid grid-cols-3 gap-8 md:gap-12 max-w-2xl">
<div className="text-center md:text-left">
<div className="text-4xl md:text-5xl font-bold text-brand">12+</div>
<div className="mt-2 text-sm text-white/50 tracking-wide"></div>
</div>
<div className="text-center">
<div className="text-4xl md:text-5xl font-bold text-white">6 </div>
<div className="mt-2 text-sm text-white/50 tracking-wide"></div>
</div>
<div className="text-center md:text-right">
<div className="text-4xl md:text-5xl font-bold text-white">10+</div>
<div className="mt-2 text-sm text-white/50 tracking-wide"></div>
</div>
</div>
</ScrollReveal>
</div>
</section>
{/* Brand Story Section */}
<section className="relative py-24 lg:py-32 overflow-hidden bg-ink-light">
<GrainOverlay />
<div className="max-w-container mx-auto px-6 lg:px-10 relative z-10">
<ScrollReveal className="max-w-3xl">
<SectionLabel>
<div className="flex items-center gap-5">
<div className="w-14 h-px bg-brand" />
<span className="text-[11px] tracking-[0.4em] uppercase font-bold text-brand">
Brand Story
</span>
<div className="w-14 h-px bg-brand" />
</div>
</SectionLabel>
<h2 className="mt-6 text-3xl md:text-4xl lg:text-5xl font-bold tracking-tight leading-tight">
</h2>
</ScrollReveal>
<div className="mt-16 grid md:grid-cols-2 gap-8">
<StaggerReveal staggerDelay={0.1} delayChildren={0.05}>
<motion.div
className="p-10 lg:p-12 border border-white/10 bg-ink/50 backdrop-blur-sm hover:border-brand/30 transition-all duration-500"
whileHover={{ y: -4 }}
transition={{ duration: 0.4, ease: EASE_OUT }}
>
<div className="text-brand font-mono text-sm tracking-widest mb-6">01 / </div>
<h3 className="text-2xl font-bold mb-6"></h3>
<div className="space-y-4 text-white/60 leading-relaxed">
<p></p>
<p></p>
<p></p>
</div>
</motion.div>
<motion.div
className="p-10 lg:p-12 border border-white/10 bg-ink/50 backdrop-blur-sm hover:border-brand/30 transition-all duration-500"
whileHover={{ y: -4 }}
transition={{ duration: 0.4, ease: EASE_OUT }}
>
<div className="text-brand font-mono text-sm tracking-widest mb-6">02 / </div>
<h3 className="text-2xl font-bold mb-6"></h3>
<div className="space-y-4 text-white/60 leading-relaxed">
<p>&ldquo;&rdquo;</p>
<p></p>
<p></p>
</div>
</motion.div>
</StaggerReveal>
</div>
<ScrollReveal delay={0.3}>
<div className="mt-16 p-10 lg:p-12 border border-brand/20 bg-brand/5">
<p className="text-lg font-medium mb-8"></p>
<div className="grid md:grid-cols-3 gap-6">
{PROMISES.map((promise) => (
<div key={promise} className="flex items-start gap-4">
<CheckCircle2 className="w-5 h-5 text-brand shrink-0 mt-0.5" />
<span className="text-white/70 leading-relaxed">{promise}</span>
</div>
))}
</div>
<p className="mt-10 text-xl md:text-2xl font-bold leading-relaxed">
<span className="text-brand"></span>
</p>
</div>
</ScrollReveal>
</div>
</section>
{/* Core Values Section */}
<section className="relative py-24 lg:py-32 overflow-hidden">
<GrainOverlay />
<div className="max-w-container mx-auto px-6 lg:px-10 relative z-10">
<ScrollReveal className="text-center max-w-3xl mx-auto mb-20">
<SectionLabel className="justify-center">
<div className="flex items-center gap-5">
<div className="w-14 h-px bg-brand" />
<span className="text-[11px] tracking-[0.4em] uppercase font-bold text-brand">
Core Values
</span>
<div className="w-14 h-px bg-brand" />
</div>
</SectionLabel>
<h2 className="mt-6 text-3xl md:text-4xl lg:text-5xl font-bold tracking-tight leading-tight">
</h2>
<p className="mt-6 text-lg text-white/60 leading-relaxed">
</p>
</ScrollReveal>
<div className="grid md:grid-cols-3 gap-8">
<StaggerReveal staggerDelay={0.1} delayChildren={0.05}>
{CORE_VALUES.map((value) => {
const Icon = value.icon;
return (
<motion.div
key={value.title}
className="group relative p-10 lg:p-12 border border-white/10 hover:border-brand/30 bg-ink-light/50 transition-all duration-500"
whileHover={{ y: -8 }}
transition={{ duration: 0.4, ease: EASE_OUT }}
>
<div className="absolute top-0 left-0 right-0 h-1 bg-brand scale-x-0 group-hover:scale-x-100 transition-transform duration-700 origin-left" />
<div className="flex items-center gap-4 mb-8">
<div className="w-14 h-14 flex items-center justify-center border border-brand/30 bg-brand/10 text-brand">
<Icon className="w-6 h-6" />
</div>
<span className="text-5xl font-bold text-white/10 font-mono">{value.number}</span>
</div>
<h3 className="text-2xl font-bold mb-4">{value.title}</h3>
<p className="text-white/60 leading-relaxed">{value.description}</p>
</motion.div>
);
})}
</StaggerReveal>
</div>
</div>
</section>
{/* Milestones Section */}
<section className="relative py-24 lg:py-32 overflow-hidden bg-ink-light">
<GrainOverlay />
<div className="max-w-container mx-auto px-6 lg:px-10 relative z-10">
<ScrollReveal className="text-center max-w-3xl mx-auto mb-20">
<SectionLabel className="justify-center">
<div className="flex items-center gap-5">
<div className="w-14 h-px bg-brand" />
<span className="text-[11px] tracking-[0.4em] uppercase font-bold text-brand">
Milestones
</span>
<div className="w-14 h-px bg-brand" />
</div>
</SectionLabel>
<h2 className="mt-6 text-3xl md:text-4xl lg:text-5xl font-bold tracking-tight leading-tight">
</h2>
</ScrollReveal>
<ScrollReveal delay={0.1}>
<div className="relative max-w-3xl mx-auto">
<div className="absolute left-6 md:left-[7rem] top-0 bottom-0 w-px bg-white/10" />
<div className="space-y-10">
{MILESTONES.map((milestone, idx) => {
const isLatest = idx === MILESTONES.length - 1;
return (
<motion.div
key={milestone.title + milestone.date}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-100px' }}
transition={{ duration: 0.5, delay: idx * 0.08, ease: EASE_OUT }}
className="relative flex items-start gap-6 md:gap-10"
>
<div className="w-12 md:w-24 shrink-0 text-right">
<span className="text-sm font-mono font-medium text-brand">{milestone.date}</span>
</div>
<div className="relative shrink-0">
<div className={`w-4 h-4 rounded-full border-2 border-ink-light mt-1.5 ${isLatest ? 'bg-brand' : 'bg-brand'}`} />
{isLatest && (
<div className="absolute inset-0 w-4 h-4 rounded-full bg-brand animate-ping opacity-30" />
)}
</div>
<div className="flex-1 pb-2">
<div className="flex items-center gap-3 mb-2">
<h3 className="text-xl font-bold">{milestone.title}</h3>
{isLatest && (
<span className="inline-flex items-center px-2.5 py-0.5 text-[10px] font-bold bg-brand/10 text-brand border border-brand/30 tracking-wide">
</span>
)}
</div>
<p className="text-white/60 leading-relaxed">{milestone.description}</p>
</div>
</motion.div>
);
})}
</div>
</div>
</ScrollReveal>
</div>
</section>
{/* CTA Section */}
<section className="relative py-24 lg:py-32 overflow-hidden">
<GrainOverlay />
<div className="absolute inset-0 bg-gradient-to-b from-ink to-ink-light pointer-events-none" />
<div className="max-w-container mx-auto px-6 lg:px-10 relative z-10">
<ScrollReveal className="text-center max-w-3xl mx-auto">
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold tracking-tight leading-tight">
</h2>
<p className="mt-6 text-lg text-white/60 leading-relaxed">
</p>
<div className="mt-12 flex flex-wrap justify-center gap-4">
<Button size="lg" asChild>
<a href="/contact">
<ArrowRight className="w-4 h-4 ml-2" />
</a>
</Button>
<Button size="lg" variant="outline" asChild>
<a href="/team"></a>
</Button>
</div>
</ScrollReveal>
</div>
</section>
</div>
);
}
@@ -0,0 +1,382 @@
'use client';
import { useRef, useState, useEffect } from 'react';
import { motion, useInView } from 'framer-motion';
import { ScrollReveal, StaggerReveal } from '@/components/ui/scroll-reveal';
import { ArrowRight, Target, Heart, Lightbulb, CheckCircle2 } from 'lucide-react';
import { GrainOverlay, FloatingInkParticles, SectionLabel, EASE_OUT } from '@/components/ui/page-decoration';
import { useReducedMotion } from '@/hooks/use-reduced-motion';
const CORE_VALUES = [
{
icon: Target,
number: '01',
title: '结果导向',
description: '不以"项目上线"为终点,以"客户业务是否真正改善"为衡量标准。每一次交付,都追求可量化的价值。',
},
{
icon: Heart,
number: '02',
title: '长期主义',
description: '不追逐风口,只做真正为客户创造价值的事。您的下一次难题,还愿意第一个想到我们——这才是我们追求的成功。',
},
{
icon: Lightbulb,
number: '03',
title: '技术驱动',
description: '用扎实的工程能力和行业经验赢得信任。技术不是炫技的工具,而是解决真实问题的手段。',
},
];
const MILESTONES = [
{ date: '2026.01', title: '公司成立', description: '四川睿新致远科技有限公司在成都龙泉驿区正式成立' },
{ date: '2026.01', title: '团队组建', description: '核心团队到位,成员来自多个大型 IT 企业,具备扎实的工程能力和规范化交付经验' },
{ date: '2026.02', title: '业务启动', description: '推出企业数字化转型咨询与解决方案服务,开始接触首批客户' },
{ date: '2026.03', title: '产品研发', description: '自主研发 ERP、CRM 等核心产品,逐步构建产品矩阵' },
{ date: '2026.05', title: '研发推进', description: '多款产品进入核心功能开发阶段,同步开展早期用户体验计划' },
];
const PROMISES = [
'不卖您用不上的技术',
'不说不懂业务的术语',
'不做路过就忘的"一锤子买卖"',
];
const STATS = [
{ value: 12, suffix: '+', label: '年行业深耕' },
{ value: 6, suffix: '款', label: '自研产品' },
{ value: 10, suffix: '+', label: '人核心团队' },
];
function useCountUp(target: number, start: boolean, duration: number = 2000) {
const [count, setCount] = useState(0);
const prefersReducedMotion = useReducedMotion();
useEffect(() => {
if (!start || prefersReducedMotion) {
setCount(target);
return;
}
const startTime = performance.now();
let animationId: number;
const animate = (currentTime: number) => {
const elapsed = currentTime - startTime;
const progress = Math.min(elapsed / duration, 1);
const easeOutQuart = 1 - Math.pow(1 - progress, 4);
setCount(Math.floor(target * easeOutQuart));
if (progress < 1) {
animationId = requestAnimationFrame(animate);
} else {
setCount(target);
}
};
animationId = requestAnimationFrame(animate);
return () => cancelAnimationFrame(animationId);
}, [target, start, duration, prefersReducedMotion]);
return count;
}
function StatItem({ value, suffix, label, start, delay }: { value: number; suffix: string; label: string; start: boolean; delay: number }) {
const count = useCountUp(value, start, 2000);
const shouldReduceMotion = useReducedMotion();
return (
<motion.div
initial={shouldReduceMotion ? {} : { opacity: 0, y: 20 }}
animate={start ? { opacity: 1, y: 0 } : {}}
transition={{ delay, duration: 0.6, ease: EASE_OUT }}
className="text-center"
>
<div className="text-4xl sm:text-5xl font-bold text-white tracking-tight leading-none mb-2">
{count}<span className="text-brand">{suffix}</span>
</div>
<div className="text-sm text-white/50 tracking-wide">{label}</div>
</motion.div>
);
}
function ValueCard({ value, index }: { value: typeof CORE_VALUES[0]; index: number }) {
const Icon = value.icon;
const shouldReduceMotion = useReducedMotion();
return (
<motion.div
className="group relative block overflow-hidden bg-ink-light/50 border border-white/[0.06]"
initial={shouldReduceMotion ? {} : { opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-80px' }}
transition={{ duration: 0.6, delay: index * 0.1, ease: EASE_OUT }}
>
<div className="absolute top-0 left-0 bottom-0 w-1 bg-brand" />
<div className="p-8 sm:p-10 md:p-12">
<div className="flex items-start justify-between mb-8">
<div className="w-14 h-14 flex items-center justify-center border border-brand/30 bg-brand/[0.08] text-brand">
<Icon className="w-6 h-6" />
</div>
<span className="text-5xl font-bold text-white/[0.06] font-mono">{value.number}</span>
</div>
<h3 className="text-2xl font-bold mb-4">{value.title}</h3>
<p className="text-white/60 leading-relaxed">{value.description}</p>
</div>
<div className="absolute bottom-0 left-0 right-0 h-px bg-gradient-to-r from-brand/40 via-brand/10 to-transparent scale-x-0 group-hover:scale-x-100 transition-transform duration-700 origin-left" />
</motion.div>
);
}
export default function AboutContentV2() {
const statsRef = useRef(null);
const isInView = useInView(statsRef, { once: true, margin: '-100px' });
return (
<div className="min-h-screen bg-ink text-white overflow-x-hidden">
{/* Hero Section */}
<section className="relative min-h-[85vh] flex items-center overflow-hidden bg-ink">
<GrainOverlay />
<FloatingInkParticles />
<div className="absolute inset-0 bg-gradient-to-b from-ink-light/30 via-transparent to-ink pointer-events-none" />
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10 w-full py-28 sm:py-36 md:py-44 lg:py-56">
<div className="max-w-4xl">
<ScrollReveal>
<SectionLabel>About Us</SectionLabel>
</ScrollReveal>
<ScrollReveal delay={0.1}>
<h1 className="text-4xl sm:text-5xl md:text-6xl lg:text-7xl xl:text-8xl font-bold leading-[0.92] tracking-tighter mb-10 sm:mb-12 md:mb-16">
<span className="block mt-4 text-brand"></span>
</h1>
</ScrollReveal>
<ScrollReveal delay={0.2}>
<p className="text-lg md:text-xl text-white/60 max-w-2xl leading-relaxed mb-12">
</p>
</ScrollReveal>
<ScrollReveal delay={0.3}>
<div className="flex flex-col sm:flex-row gap-4 sm:gap-6">
<a
href="/contact"
className="group inline-flex items-center justify-center gap-3 px-12 py-6 font-semibold text-base bg-brand text-white transition-all duration-300 hover:bg-brand/90"
>
<ArrowRight className="w-4 h-4 transition-transform duration-300 group-hover:translate-x-1" />
</a>
<a
href="/team"
className="inline-flex items-center justify-center gap-3 px-12 py-6 font-semibold text-base text-white border border-white/20 hover:border-white/40 hover:bg-white/[0.03] transition-all duration-300"
>
</a>
</div>
</ScrollReveal>
<div ref={statsRef} className="mt-20 grid grid-cols-3 gap-6 sm:gap-8 md:gap-12 max-w-2xl">
{STATS.map((stat, idx) => (
<StatItem
key={stat.label}
value={stat.value}
suffix={stat.suffix}
label={stat.label}
start={isInView}
delay={idx * 0.15}
/>
))}
</div>
</div>
</div>
</section>
{/* Brand Story Section */}
<section className="relative py-20 sm:py-28 md:py-36 lg:py-44 overflow-hidden bg-ink-light">
<GrainOverlay />
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10">
<ScrollReveal className="max-w-3xl mb-16 sm:mb-20">
<SectionLabel>Brand Story</SectionLabel>
<h2 className="text-3xl sm:text-4xl md:text-5xl font-bold tracking-tight leading-tight">
</h2>
</ScrollReveal>
<div className="grid md:grid-cols-2 gap-8">
<StaggerReveal staggerDelay={0.1} delayChildren={0.05}>
<motion.div
className="relative p-8 sm:p-10 lg:p-12 border border-white/[0.08] bg-ink/50 overflow-hidden group"
whileHover={{ y: -4 }}
transition={{ duration: 0.4, ease: EASE_OUT }}
>
<div className="absolute top-0 left-0 bottom-0 w-1 bg-brand/60" />
<div className="text-brand font-mono text-sm tracking-widest mb-6">01 / </div>
<h3 className="text-2xl font-bold mb-6"></h3>
<div className="space-y-4 text-white/60 leading-relaxed">
<p></p>
<p></p>
<p></p>
</div>
</motion.div>
<motion.div
className="relative p-8 sm:p-10 lg:p-12 border border-white/[0.08] bg-ink/50 overflow-hidden group"
whileHover={{ y: -4 }}
transition={{ duration: 0.4, ease: EASE_OUT }}
>
<div className="absolute top-0 left-0 bottom-0 w-1 bg-brand/60" />
<div className="text-brand font-mono text-sm tracking-widest mb-6">02 / </div>
<h3 className="text-2xl font-bold mb-6"></h3>
<div className="space-y-4 text-white/60 leading-relaxed">
<p></p>
<p></p>
<p></p>
</div>
</motion.div>
</StaggerReveal>
</div>
<ScrollReveal delay={0.3}>
<div className="mt-16 p-8 sm:p-10 lg:p-12 border border-brand/20 bg-brand/[0.03]">
<p className="text-lg font-medium mb-8"></p>
<div className="grid md:grid-cols-3 gap-6">
{PROMISES.map((promise) => (
<div key={promise} className="flex items-start gap-4">
<CheckCircle2 className="w-5 h-5 text-brand shrink-0 mt-0.5" />
<span className="text-white/70 leading-relaxed">{promise}</span>
</div>
))}
</div>
<p className="mt-10 text-xl md:text-2xl font-bold leading-relaxed">
<span className="text-brand"></span>
</p>
</div>
</ScrollReveal>
</div>
</section>
{/* Core Values Section */}
<section className="relative py-20 sm:py-28 md:py-36 lg:py-44 overflow-hidden">
<GrainOverlay />
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10">
<ScrollReveal className="text-center max-w-3xl mx-auto mb-16 sm:mb-20">
<SectionLabel className="justify-center">Core Values</SectionLabel>
<h2 className="text-3xl sm:text-4xl md:text-5xl font-bold tracking-tight leading-tight">
</h2>
<p className="mt-6 text-lg text-white/60 leading-relaxed">
</p>
</ScrollReveal>
<div className="grid md:grid-cols-3 gap-8">
{CORE_VALUES.map((value, idx) => (
<ValueCard key={value.title} value={value} index={idx} />
))}
</div>
</div>
</section>
{/* Milestones Section */}
<section className="relative py-20 sm:py-28 md:py-36 lg:py-44 overflow-hidden bg-ink-light">
<GrainOverlay />
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10">
<ScrollReveal className="text-center max-w-3xl mx-auto mb-16 sm:mb-20">
<SectionLabel className="justify-center">Milestones</SectionLabel>
<h2 className="text-3xl sm:text-4xl md:text-5xl font-bold tracking-tight leading-tight">
</h2>
</ScrollReveal>
<ScrollReveal delay={0.1}>
<div className="relative max-w-3xl mx-auto">
<div className="absolute left-6 sm:left-8 md:left-[7rem] top-0 bottom-0 w-px bg-white/10" />
<div className="space-y-10">
{MILESTONES.map((milestone, idx) => {
const isLatest = idx === MILESTONES.length - 1;
return (
<motion.div
key={milestone.title + milestone.date}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-100px' }}
transition={{ duration: 0.5, delay: idx * 0.08, ease: EASE_OUT }}
className="relative flex items-start gap-6 md:gap-10"
>
<div className="w-12 sm:w-16 md:w-24 shrink-0 text-right">
<span className="text-sm font-mono font-medium text-brand">{milestone.date}</span>
</div>
<div className="relative shrink-0">
<div className={`w-4 h-4 rounded-full border-2 border-ink-light mt-1.5 ${isLatest ? 'bg-brand' : 'bg-brand'}`} />
{isLatest && (
<div className="absolute inset-0 w-4 h-4 rounded-full bg-brand animate-ping opacity-30" />
)}
</div>
<div className="flex-1 pb-2">
<div className="flex items-center gap-3 mb-2">
<h3 className="text-xl font-bold">{milestone.title}</h3>
{isLatest && (
<span className="inline-flex items-center px-2.5 py-0.5 text-[10px] font-bold bg-brand/10 text-brand border border-brand/30 tracking-wide">
</span>
)}
</div>
<p className="text-white/60 leading-relaxed">{milestone.description}</p>
</div>
</motion.div>
);
})}
</div>
</div>
</ScrollReveal>
</div>
</section>
{/* CTA Section */}
<section className="relative py-20 sm:py-28 md:py-36 lg:py-44 overflow-hidden">
<GrainOverlay />
<FloatingInkParticles />
<div className="absolute inset-0 bg-gradient-to-b from-ink to-ink-light/50 pointer-events-none" />
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10">
<ScrollReveal className="text-center max-w-3xl mx-auto">
<h2 className="text-3xl sm:text-4xl md:text-5xl font-bold tracking-tight leading-tight">
</h2>
<p className="mt-6 text-lg text-white/60 leading-relaxed">
</p>
<div className="mt-12 flex flex-col sm:flex-row justify-center gap-4 sm:gap-6">
<a
href="/contact"
className="group inline-flex items-center justify-center gap-3 px-12 py-6 font-semibold text-base bg-brand text-white transition-all duration-300 hover:bg-brand/90"
>
<ArrowRight className="w-4 h-4 transition-transform duration-300 group-hover:translate-x-1" />
</a>
<a
href="/team"
className="inline-flex items-center justify-center gap-3 px-12 py-6 font-semibold text-base text-white border border-white/20 hover:border-white/40 hover:bg-white/[0.03] transition-all duration-300"
>
</a>
</div>
</ScrollReveal>
</div>
</section>
</div>
);
}
@@ -0,0 +1,443 @@
'use client';
import {
Trophy, Users, Briefcase, Award,
Target, Heart, Lightbulb, CheckCircle2,
Building2, Shield, Zap, TrendingUp,
} from 'lucide-react';
import { ScrollReveal, StaggerReveal } from '@/components/ui/scroll-reveal';
import { MetricCard } from '@/components/ui/metric-card';
import { MilestoneTimeline } from '@/components/ui/milestone-timeline';
import { GrainOverlay, FloatingInkParticles, SectionLabel } from '@/components/ui/page-decoration';
const CORE_VALUES = [
{
icon: Target,
number: '01',
title: '结果导向',
description: '不以「项目上线」为终点,以「客户业务是否真正改善」为衡量标准。每一次交付,都追求可量化的价值。',
},
{
icon: Heart,
number: '02',
title: '长期主义',
description: '不追逐风口,只做真正为客户创造价值的事。您的下一次难题,还愿意第一个想到我们——这才是我们追求的成功。',
},
{
icon: Lightbulb,
number: '03',
title: '技术驱动',
description: '用扎实的工程能力和行业经验赢得信任。技术不是炫技的工具,而是解决真实问题的手段。',
},
];
const MILESTONES = [
{
year: '2014',
title: '团队启航',
description: '核心团队始于大型企业数字化转型项目,深耕 ERP、CRM 实施与定制开发,积累了扎实的工程交付能力。',
highlight: '服务首批 20+ 企业客户',
},
{
year: '2018',
title: '业务扩展',
description: '从单一实施服务向全链路咨询转型,建立战略咨询 + 技术落地的双轮驱动模式。',
highlight: '客户突破 100 家',
},
{
year: '2021',
title: '产品化起步',
description: '开始自研产品矩阵,从项目制向「产品+服务」双模式升级,覆盖更多成长型企业需求。',
highlight: '3 款自研产品上线',
},
{
year: '2023',
title: '行业深耕',
description: '聚焦制造、零售、医疗三大核心行业,建立行业解决方案库与最佳实践沉淀。',
highlight: '客户续约率 95%+',
},
{
year: '2026',
title: '睿新致远成立',
description: '四川睿新致远科技有限公司在成都正式成立,以全新品牌开启下一段征程。',
highlight: '全新启航,步履不停',
},
];
const PROMISES = [
'不卖您用不上的技术',
'不说不懂业务的术语',
'不做路过就忘的「一锤子买卖」',
];
const KEY_METRICS = [
{
icon: <Building2 className="w-5 h-5" />,
label: '服务企业',
value: 500,
suffix: '+',
description: '覆盖制造、零售、医疗、教育等六大行业',
trend: { value: '23% YoY', direction: 'up' as const, label: '增长' },
accentColor: 'brand' as const,
},
{
icon: <Users className="w-5 h-5" />,
label: '专业顾问',
value: 200,
suffix: '+',
description: '10年以上资深顾问占比 40%,平均行业经验 8 年',
trend: { value: '年均 30+', direction: 'up' as const, label: '增长' },
accentColor: 'blue' as const,
},
{
icon: <Trophy className="w-5 h-5" />,
label: '客户满意度',
value: 98,
suffix: '%',
description: '80% 客户合作超过 3 年,续约率 97%',
trend: { value: '97% 续约率', direction: 'up' as const, label: '' },
accentColor: 'teal' as const,
},
{
icon: <Briefcase className="w-5 h-5" />,
label: '行业经验',
value: 12,
suffix: '年',
description: '跨越四轮技术变革周期,持续进化',
trend: { value: 'since 2014', direction: 'neutral' as const, label: '' },
accentColor: 'amber' as const,
},
];
const CERTIFICATIONS = [
{ name: 'ISO 27001', desc: '信息安全管理体系认证', icon: Shield },
{ name: 'ISO 9001', desc: '质量管理体系认证', icon: Award },
{ name: 'CMMI 3级', desc: '软件能力成熟度认证', icon: Zap },
{ name: '高新技术企业', desc: '国家高新技术企业认定', icon: Trophy },
];
const PARTNERS = [
'SAP', 'Oracle', 'Salesforce', 'Microsoft',
'阿里云', '腾讯云', '华为云', '金蝶',
];
function ValueCard({ value, index: _index }: { value: typeof CORE_VALUES[0]; index: number }) {
const Icon = value.icon;
return (
<div
className="group relative block overflow-hidden bg-ink-light/50 border border-white/[0.06]"
>
<div className="absolute top-0 left-0 bottom-0 w-1 bg-brand" />
<div className="p-8 sm:p-10 md:p-12">
<div className="flex items-start justify-between mb-8">
<div className="w-14 h-14 flex items-center justify-center border border-brand/30 bg-brand/[0.08] text-brand">
<Icon className="w-6 h-6" />
</div>
<span className="text-5xl font-bold text-white/[0.06] font-mono">{value.number}</span>
</div>
<h3 className="text-2xl font-bold mb-4">{value.title}</h3>
<p className="text-white/60 leading-relaxed">{value.description}</p>
</div>
<div className="absolute bottom-0 left-0 right-0 h-px bg-gradient-to-r from-brand/40 via-brand/10 to-transparent scale-x-0 group-hover:scale-x-100 transition-transform duration-700 origin-left" />
</div>
);
}
export default function AboutContentV3() {
return (
<div className="min-h-screen bg-ink text-white overflow-x-hidden" data-theme="dark">
{/* Hero Section */}
<section className="relative min-h-[85vh] flex items-center overflow-hidden bg-ink">
<GrainOverlay />
<FloatingInkParticles />
<div className="absolute inset-0 bg-gradient-to-b from-ink-light/30 via-transparent to-ink pointer-events-none" />
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10 w-full py-28 sm:py-36 md:py-44 lg:py-56">
<div className="max-w-4xl">
<ScrollReveal>
<SectionLabel>About Us</SectionLabel>
</ScrollReveal>
<ScrollReveal delay={0.1}>
<h1 className="text-4xl sm:text-5xl md:text-6xl lg:text-7xl xl:text-8xl font-bold leading-[0.92] tracking-tighter mb-10 sm:mb-12 md:mb-16">
<span className="block mt-4 text-brand"></span>
</h1>
</ScrollReveal>
<ScrollReveal delay={0.2}>
<p className="text-lg md:text-xl text-white/60 max-w-2xl leading-relaxed mb-12">
</p>
</ScrollReveal>
<ScrollReveal delay={0.3}>
<div className="flex flex-col sm:flex-row gap-4 sm:gap-6">
<a
href="/contact"
className="group inline-flex items-center justify-center gap-3 px-12 py-6 font-semibold text-base bg-brand text-white transition-all duration-300 hover:bg-brand/90 min-h-[52px] touch-manipulation"
>
<TrendingUp className="w-4 h-4 transition-transform duration-300 group-hover:translate-x-0.5 group-hover:-translate-y-0.5" />
</a>
<a
href="/services"
className="inline-flex items-center justify-center gap-3 px-12 py-6 font-semibold text-base text-white border border-white/20 hover:border-white/40 hover:bg-white/[0.03] transition-all duration-300 min-h-[52px] touch-manipulation"
>
</a>
</div>
</ScrollReveal>
</div>
</div>
</section>
{/* Key Metrics Section */}
<section className="relative py-20 sm:py-28 md:py-32 overflow-hidden bg-ink-light">
<GrainOverlay />
<div className="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-white/10 to-transparent" />
<div className="absolute bottom-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-white/10 to-transparent" />
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10">
<ScrollReveal className="text-center max-w-3xl mx-auto mb-12 sm:mb-16">
<SectionLabel className="justify-center">By The Numbers</SectionLabel>
<h2 className="text-3xl sm:text-4xl md:text-5xl font-bold tracking-tight leading-tight">
</h2>
<p className="mt-6 text-lg text-white/60 leading-relaxed">
</p>
</ScrollReveal>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 md:gap-6">
{KEY_METRICS.map((metric, i) => (
<ScrollReveal key={metric.label} delay={i * 0.08}>
<div className="h-full">
<MetricCard
icon={metric.icon}
label={metric.label}
value={metric.value}
suffix={metric.suffix}
description={metric.description}
trend={metric.trend}
accentColor={metric.accentColor}
theme="dark"
className="h-full"
/>
</div>
</ScrollReveal>
))}
</div>
</div>
</section>
{/* Brand Story Section */}
<section className="relative py-20 sm:py-28 md:py-36 lg:py-44 overflow-hidden bg-ink">
<GrainOverlay />
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10">
<div className="grid lg:grid-cols-12 gap-10 sm:gap-12 md:gap-16 items-start">
<ScrollReveal className="lg:col-span-5">
<SectionLabel>Brand Story</SectionLabel>
<h2 className="text-3xl sm:text-4xl md:text-5xl font-bold tracking-tight leading-tight mb-8">
<br />
</h2>
<p className="text-white/60 leading-relaxed text-lg mb-8">
</p>
<div className="flex items-center gap-4">
<div className="w-12 h-12 rounded-full bg-brand/10 border border-brand/30 flex items-center justify-center">
<Heart className="w-5 h-5 text-brand" />
</div>
<div>
<div className="font-semibold"></div>
<div className="text-sm text-white/50"></div>
</div>
</div>
</ScrollReveal>
<div className="lg:col-span-7">
<StaggerReveal staggerDelay={0.1} delayChildren={0.05}>
<div className="grid sm:grid-cols-2 gap-4 md:gap-6">
{PROMISES.map((promise, i) => (
<div
key={i}
className="relative p-6 sm:p-8 border border-white/[0.08] bg-ink-light/50 group hover:border-brand/30 hover:bg-ink-light transition-all duration-500"
>
<div className="absolute top-0 left-0 w-1 h-0 bg-brand group-hover:h-full transition-all duration-500" />
<div className="flex items-start gap-4">
<div className="w-8 h-8 rounded-full bg-brand/10 flex items-center justify-center shrink-0 mt-0.5">
<CheckCircle2 className="w-4 h-4 text-brand" />
</div>
<p className="text-white/80 leading-relaxed font-medium">{promise}</p>
</div>
</div>
))}
<div className="sm:col-span-2 p-6 sm:p-8 border border-brand/20 bg-brand/[0.03]">
<p className="text-xl md:text-2xl font-bold leading-relaxed">
<span className="text-brand"></span>
</p>
</div>
</div>
</StaggerReveal>
</div>
</div>
</div>
</section>
{/* Core Values Section */}
<section className="relative py-20 sm:py-28 md:py-36 lg:py-44 overflow-hidden bg-ink-light">
<GrainOverlay />
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10">
<ScrollReveal className="text-center max-w-3xl mx-auto mb-16 sm:mb-20">
<SectionLabel className="justify-center">Core Values</SectionLabel>
<h2 className="text-3xl sm:text-4xl md:text-5xl font-bold tracking-tight leading-tight">
</h2>
<p className="mt-6 text-lg text-white/60 leading-relaxed">
</p>
</ScrollReveal>
<div className="grid md:grid-cols-3 gap-6 md:gap-8">
{CORE_VALUES.map((value, idx) => (
<ScrollReveal key={value.title} delay={idx * 0.1}>
<ValueCard value={value} index={idx} />
</ScrollReveal>
))}
</div>
</div>
</section>
{/* Milestones Section */}
<section className="relative py-20 sm:py-28 md:py-36 lg:py-44 overflow-hidden bg-ink">
<GrainOverlay />
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10">
<ScrollReveal className="text-center max-w-3xl mx-auto mb-16 sm:mb-20">
<SectionLabel className="justify-center">Our Journey</SectionLabel>
<h2 className="text-3xl sm:text-4xl md:text-5xl font-bold tracking-tight leading-tight">
</h2>
<p className="mt-6 text-lg text-white/60 leading-relaxed">
2014 2026
</p>
</ScrollReveal>
<div className="max-w-5xl mx-auto">
<MilestoneTimeline
items={MILESTONES.map((m) => ({
year: m.year,
title: m.title,
description: m.description,
highlight: m.highlight,
}))}
variant="vertical"
theme="dark"
/>
</div>
</div>
</section>
{/* Certifications & Partners Section */}
<section className="relative py-20 sm:py-28 md:py-32 overflow-hidden bg-ink-light">
<GrainOverlay />
<div className="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-white/10 to-transparent" />
<div className="absolute bottom-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-white/10 to-transparent" />
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10">
<div className="grid lg:grid-cols-2 gap-12 md:gap-16">
<ScrollReveal>
<SectionLabel>Certifications</SectionLabel>
<h2 className="text-3xl sm:text-4xl font-bold tracking-tight leading-tight mb-8">
</h2>
<div className="grid grid-cols-2 gap-4">
{CERTIFICATIONS.map((cert) => {
const Icon = cert.icon;
return (
<div
key={cert.name}
className="p-5 sm:p-6 border border-white/[0.08] bg-ink/50 hover:border-brand/30 transition-all duration-300 group"
>
<div className="w-10 h-10 rounded-lg bg-brand/10 flex items-center justify-center mb-4 text-brand group-hover:scale-110 transition-transform duration-300">
<Icon className="w-5 h-5" />
</div>
<div className="font-semibold mb-1">{cert.name}</div>
<div className="text-sm text-white/50">{cert.desc}</div>
</div>
);
})}
</div>
</ScrollReveal>
<ScrollReveal delay={0.1}>
<SectionLabel>Partners</SectionLabel>
<h2 className="text-3xl sm:text-4xl font-bold tracking-tight leading-tight mb-8">
</h2>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
{PARTNERS.map((partner) => (
<div
key={partner}
className="aspect-[3/2] flex items-center justify-center border border-white/[0.08] bg-ink/30 hover:border-white/20 hover:bg-ink/50 transition-all duration-300 group"
>
<span className="text-sm font-bold text-white/40 group-hover:text-white/70 transition-colors duration-300">
{partner}
</span>
</div>
))}
</div>
<p className="mt-6 text-sm text-white/40 leading-relaxed">
</p>
</ScrollReveal>
</div>
</div>
</section>
{/* CTA Section */}
<section className="relative py-20 sm:py-28 md:py-36 lg:py-44 overflow-hidden">
<GrainOverlay />
<FloatingInkParticles />
<div className="absolute inset-0 bg-gradient-to-b from-ink to-ink-light/50 pointer-events-none" />
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10">
<ScrollReveal className="text-center max-w-3xl mx-auto">
<h2 className="text-3xl sm:text-4xl md:text-5xl font-bold tracking-tight leading-tight">
</h2>
<p className="mt-6 text-lg text-white/60 leading-relaxed">
</p>
<div className="mt-12 flex flex-col sm:flex-row justify-center gap-4 sm:gap-6">
<a
href="/contact"
className="group inline-flex items-center justify-center gap-3 px-12 py-6 font-semibold text-base bg-brand text-white transition-all duration-300 hover:bg-brand/90 min-h-[52px] touch-manipulation"
>
<TrendingUp className="w-4 h-4 transition-transform duration-300 group-hover:translate-x-0.5 group-hover:-translate-y-0.5" />
</a>
<a
href="/services"
className="inline-flex items-center justify-center gap-3 px-12 py-6 font-semibold text-base text-white border border-white/20 hover:border-white/40 hover:bg-white/[0.03] transition-all duration-300 min-h-[52px] touch-manipulation"
>
</a>
</div>
</ScrollReveal>
</div>
</section>
</div>
);
}
@@ -0,0 +1,420 @@
'use client';
import { motion } from 'framer-motion';
import {
Trophy,
Shield, Award, Zap,
ArrowUpRight,
type LucideIcon,
} from 'lucide-react';
const EASE_OUT = [0.22, 1, 0.36, 1] as const;
/** Icon name-to-component mapping for CMS string-based icons */
const ICON_MAP: Record<string, LucideIcon> = {
Shield,
Award,
Zap,
Trophy,
};
const DEFAULT_CERT_ICON_NAMES = ['Shield', 'Award', 'Zap', 'Trophy'] as const;
interface AboutData {
heroSubtitle?: string;
heroTitle?: string;
heroDescription?: string;
heroPrimaryCtaLabel?: string;
heroPrimaryCtaHref?: string;
heroSecondaryCtaLabel?: string;
heroSecondaryCtaHref?: string;
whoWeAreTitle?: string;
whoWeAreDescription?: string;
promiseText?: string;
coreValues?: { number: string; title: string; description: string }[];
milestones?: { year: string; title: string; description: string; highlight: string }[];
keyMetrics?: { value: string; label: string; sub: string }[];
certifications?: { name: string; description: string }[];
partners?: string[];
ctaTitle?: string;
ctaDescription?: string;
ctaPrimaryLabel?: string;
ctaPrimaryHref?: string;
ctaSecondaryLabel?: string;
ctaSecondaryHref?: string;
}
function HeroSection({ data }: { data: AboutData }) {
const titleLines = (data.heroTitle || '').split('\n');
return (
<section className="relative min-h-[85vh] flex items-center overflow-hidden bg-white">
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10 w-full py-28 sm:py-36 md:py-44 lg:py-56">
<div className="max-w-4xl">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.1, ease: EASE_OUT }}
className="text-sm font-mono tracking-[0.2em] text-text-muted uppercase mb-8"
>
{data.heroSubtitle || ''}
</motion.div>
<motion.h1
initial={{ opacity: 0, y: 40 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 1, delay: 0.2, ease: EASE_OUT }}
className="text-4xl sm:text-5xl md:text-6xl lg:text-7xl xl:text-8xl font-black text-ink leading-[0.88] tracking-tightest mb-10 sm:mb-12 md:mb-16"
>
{titleLines.map((line, i) => (
<div key={i} className={`block ${i > 0 ? 'mt-4 sm:mt-6' : ''}`}>
{i === 1 ? (
<span className="relative inline-block">
<span className="relative text-brand font-black">
{line}
<motion.span
className="absolute -bottom-2 left-0 w-full h-[3px] bg-brand"
style={{ transformOrigin: 'left' }}
initial={{ scaleX: 0 }}
animate={{ scaleX: 1 }}
transition={{ duration: 0.8, delay: 1.2, ease: EASE_OUT }}
/>
</span>
</span>
) : (
line
)}
</div>
))}
</motion.h1>
<motion.p
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.5, ease: EASE_OUT }}
className="text-lg md:text-xl text-text-secondary max-w-2xl leading-relaxed mb-12 sm:mb-16"
>
{data.heroDescription || ''}
</motion.p>
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.7, ease: EASE_OUT }}
className="flex flex-col sm:flex-row gap-4 sm:gap-6"
>
<a
href={data.heroPrimaryCtaHref || '#'}
className="group inline-flex items-center justify-center gap-3 px-10 py-5 font-semibold text-base bg-brand text-white transition-all duration-300 hover:bg-brand/90 min-h-[52px] touch-manipulation"
>
{data.heroPrimaryCtaLabel || ''}
<ArrowUpRight className="w-4 h-4 transition-transform duration-300 group-hover:translate-x-0.5 group-hover:-translate-y-0.5" />
</a>
<a
href={data.heroSecondaryCtaHref || '#'}
className="inline-flex items-center justify-center gap-3 px-10 py-5 font-semibold text-base text-ink border border-border-primary hover:border-border-secondary hover:bg-bg-secondary transition-all duration-300 min-h-[52px] touch-manipulation"
>
{data.heroSecondaryCtaLabel || ''}
</a>
</motion.div>
</div>
</div>
</section>
);
}
function MetricsStrip({ data }: { data: AboutData }) {
const metrics = data.keyMetrics ?? [];
if (metrics.length === 0) return null;
return (
<section className="relative py-16 sm:py-20 md:py-24 overflow-hidden bg-bg-secondary">
<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" />
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10">
<div className="grid grid-cols-2 lg:grid-cols-4 gap-8 md:gap-12">
{metrics.map((metric, i) => (
<motion.div
key={metric.label}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-80px' }}
transition={{ duration: 0.6, delay: i * 0.08, ease: EASE_OUT }}
className="text-center lg:text-left"
>
<div className={`text-3xl sm:text-4xl md:text-5xl font-black tracking-tight mb-2 ${i === 0 ? 'text-brand' : 'text-ink'}`}>
{metric.value}
</div>
<div className="text-sm font-medium text-text-secondary mb-1">{metric.label}</div>
<div className="text-xs text-text-muted">{metric.sub}</div>
</motion.div>
))}
</div>
</div>
</section>
);
}
function WhoWeAreSection({ data }: { data: AboutData }) {
const values = data.coreValues ?? [];
return (
<section className="relative py-20 sm:py-28 md:py-36 lg:py-44 overflow-hidden bg-white">
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10">
<div className="grid lg:grid-cols-12 gap-12 sm:gap-16 md:gap-20 items-start">
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-80px' }}
transition={{ duration: 0.8, ease: EASE_OUT }}
className="lg:col-span-5"
>
<div className="text-sm font-mono tracking-[0.2em] text-text-muted uppercase mb-6">
Who We Are
</div>
<h2 className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-extrabold text-ink tracking-tight leading-[0.95] mb-8">
{data.whoWeAreTitle || ''}
</h2>
<p className="text-text-secondary leading-relaxed text-lg mb-8">
{data.whoWeAreDescription || ''}
</p>
<div className="bg-brand/[0.04] border-t-2 border-brand p-6">
<p className="text-ink font-medium leading-relaxed">
{data.promiseText || ''}
</p>
</div>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-80px' }}
transition={{ duration: 0.8, delay: 0.15, ease: EASE_OUT }}
className="lg:col-span-7"
>
<div className="space-y-6 md:space-y-8">
{values.map((value) => (
<div
key={value.title}
className="group relative border-b border-border-primary pb-6 md:pb-8 last:border-0 last:pb-0"
>
<div className="flex items-start gap-6 md:gap-8">
<div className="text-4xl md:text-5xl font-black text-text-muted/30 font-mono shrink-0 leading-none pt-1">
{value.number}
</div>
<div className="flex-1">
<h3 className="text-xl md:text-2xl font-bold text-ink mb-3 group-hover:text-brand transition-colors duration-300">
{value.title}
</h3>
<p className="text-text-secondary leading-relaxed">{value.description}</p>
</div>
</div>
</div>
))}
</div>
</motion.div>
</div>
</div>
</section>
);
}
function MilestonesSection({ data }: { data: AboutData }) {
const milestones = data.milestones ?? [];
return (
<section className="relative py-20 sm:py-28 md:py-36 lg:py-44 overflow-hidden bg-bg-secondary">
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10">
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-80px' }}
transition={{ duration: 0.8, ease: EASE_OUT }}
className="max-w-3xl mb-16 sm:mb-20 md:mb-24"
>
<div className="text-sm font-mono tracking-[0.2em] text-text-muted uppercase mb-6">
Our Journey
</div>
<h2 className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-extrabold text-ink tracking-tight leading-[0.95]">
<br className="sm:hidden" />
</h2>
</motion.div>
<div className="max-w-4xl mx-auto space-y-0">
{milestones.map((milestone, idx) => (
<motion.div
key={milestone.year}
initial={{ opacity: 0, x: -20 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true, margin: '-60px' }}
transition={{ duration: 0.6, delay: idx * 0.08, ease: EASE_OUT }}
className="relative pl-8 sm:pl-12 md:pl-16 pb-10 sm:pb-12 md:pb-14 last:pb-0"
>
<div className="absolute left-0 top-2 bottom-0 w-px bg-border-primary last:hidden" />
<div className={`absolute left-0 top-2 w-3 h-3 rounded-full -translate-x-1/2 ${idx === 0 ? 'bg-brand' : 'bg-border-primary'}`} />
<div className="grid sm:grid-cols-12 gap-4 sm:gap-8">
<div className="sm:col-span-3">
<div className={`text-2xl sm:text-3xl md:text-4xl font-black tracking-tight ${idx === 0 ? 'text-brand' : 'text-ink'}`}>
{milestone.year}
</div>
</div>
<div className="sm:col-span-9">
<h3 className="text-xl md:text-2xl font-bold text-ink mb-2">{milestone.title}</h3>
<p className="text-text-secondary leading-relaxed mb-3">{milestone.description}</p>
<div className="text-sm font-medium text-ink">{milestone.highlight}</div>
</div>
</div>
</motion.div>
))}
</div>
</div>
</section>
);
}
function CertificationsSection({ data }: { data: AboutData }) {
const certifications = data.certifications ?? [];
const partners = data.partners ?? [];
if (certifications.length === 0 && partners.length === 0) return null;
return (
<section className="relative py-20 sm:py-28 md:py-32 overflow-hidden bg-white">
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10">
<div className="grid lg:grid-cols-2 gap-12 md:gap-16 lg:gap-20">
{certifications.length > 0 && (
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-80px' }}
transition={{ duration: 0.8, ease: EASE_OUT }}
>
<div className="text-sm font-mono tracking-[0.2em] text-text-muted uppercase mb-6">
Certifications
</div>
<h2 className="text-3xl sm:text-4xl md:text-5xl font-extrabold text-ink tracking-tight leading-[0.95] mb-10">
</h2>
<div className="grid grid-cols-2 gap-4 md:gap-6">
{certifications.map((cert, i) => {
const iconName = DEFAULT_CERT_ICON_NAMES[i % DEFAULT_CERT_ICON_NAMES.length] ?? 'Shield';
const Icon = ICON_MAP[iconName] ?? Shield;
return (
<div
key={cert.name}
className="p-5 sm:p-6 border border-border-primary bg-white hover:bg-bg-secondary hover:border-border-secondary transition-all duration-300"
>
<div className="w-10 h-10 flex items-center justify-center mb-4 text-text-secondary">
<Icon className="w-5 h-5" />
</div>
<div className="font-semibold text-ink mb-1">{cert.name}</div>
<div className="text-sm text-text-muted">{cert.description}</div>
</div>
);
})}
</div>
</motion.div>
)}
{partners.length > 0 && (
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-80px' }}
transition={{ duration: 0.8, delay: 0.15, ease: EASE_OUT }}
>
<div className="text-sm font-mono tracking-[0.2em] text-text-muted uppercase mb-6">
Partners
</div>
<h2 className="text-3xl sm:text-4xl md:text-5xl font-extrabold text-ink tracking-tight leading-[0.95] mb-10">
</h2>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3 md:gap-4">
{partners.map((partner) => (
<div
key={partner}
className="aspect-[3/2] flex items-center justify-center border border-border-primary bg-white hover:bg-bg-secondary hover:border-border-secondary transition-all duration-300 group"
>
<span className="text-sm font-bold text-text-muted group-hover:text-text-secondary transition-colors duration-300">
{partner}
</span>
</div>
))}
</div>
<p className="mt-6 text-sm text-text-muted leading-relaxed">
</p>
</motion.div>
)}
</div>
</div>
</section>
);
}
function CTASection({ data }: { data: AboutData }) {
return (
<section className="relative py-20 sm:py-28 md:py-36 lg:py-44 overflow-hidden bg-bg-secondary">
<div className="absolute top-0 left-0 right-0 h-px bg-border-primary" />
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10">
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-80px' }}
transition={{ duration: 0.8, ease: EASE_OUT }}
className="max-w-3xl"
>
<h2 className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-extrabold text-ink tracking-tight leading-[0.95] mb-8">
{data.ctaTitle || ''}
</h2>
<p className="text-lg text-text-secondary leading-relaxed mb-10">
{data.ctaDescription || ''}
</p>
<div className="flex flex-col sm:flex-row gap-4 sm:gap-6">
<a
href={data.ctaPrimaryHref || '#'}
className="group inline-flex items-center justify-center gap-3 px-10 py-5 font-semibold text-base bg-brand text-white transition-all duration-300 hover:bg-brand/90 min-h-[52px] touch-manipulation"
>
{data.ctaPrimaryLabel || ''}
<ArrowUpRight className="w-4 h-4 transition-transform duration-300 group-hover:translate-x-0.5 group-hover:-translate-y-0.5" />
</a>
<a
href={data.ctaSecondaryHref || '#'}
className="inline-flex items-center justify-center gap-3 px-10 py-5 font-semibold text-base text-ink border border-border-primary hover:border-border-secondary hover:bg-bg-secondary transition-all duration-300 min-h-[52px] touch-manipulation"
>
{data.ctaSecondaryLabel || ''}
</a>
</div>
</motion.div>
</div>
</section>
);
}
export default function AboutContentV4({ data }: { data: Record<string, unknown> }) {
const aboutData = data as AboutData;
// Empty state when no CMS data is available
if (!data || Object.keys(data).length === 0) {
return (
<div className="min-h-screen bg-white flex items-center justify-center">
<div className="text-text-muted text-lg"></div>
</div>
);
}
return (
<div className="min-h-screen bg-white text-ink overflow-x-hidden">
<HeroSection data={aboutData} />
<MetricsStrip data={aboutData} />
<WhoWeAreSection data={aboutData} />
<MilestonesSection data={aboutData} />
<CertificationsSection data={aboutData} />
<CTASection data={aboutData} />
</div>
);
}
@@ -0,0 +1,52 @@
'use client';
import { notFound } from 'next/navigation';
import CaseDetailPage from '@/components/sections/case-detail-page';
import { ScrollProgress } from '@/components/ui/scroll-progress';
import type { ContentItem } from '@/lib/cms/types';
import type { CaseStudyData } from '@/lib/cms/mock-data';
interface CaseDetailClientProps {
item: ContentItem;
}
/**
* 案例详情客户端组件(CMS 化版本)
* 数据源:CMS ContentItem(由 server 端从 mock-data.ts 同步预取)
* 类型系统:使用 CMS 类型 ContentItem,未来切换真实 API 时无需改业务代码
*/
export function CaseDetailClient({ item }: CaseDetailClientProps) {
if (!item) {
notFound();
}
const data = item.data as unknown as CaseStudyData;
const detailData = {
id: item.id,
client: data.client,
industry: data.industry,
title: item.title,
subtitle: data.subtitle,
challenge: data.challenge,
solution: data.solution,
result: data.result,
metrics: data.metrics,
timeline: data.timeline,
services: data.services,
testimonial: data.testimonial
? {
quote: data.testimonial.quote,
author: data.testimonial.author,
role: `${data.testimonial.role}${data.testimonial.company}`,
}
: undefined,
};
return (
<>
<ScrollProgress />
<CaseDetailPage data={detailData} />
</>
);
}
+40
View File
@@ -0,0 +1,40 @@
import { notFound } from 'next/navigation';
import { CaseDetailClient } from './client';
import { getPublishedItemBySlug, getAllPublishedSlugs } from '@/lib/cms/data-server';
import { COMPANY_INFO } from '@/lib/constants/company';
import type { CaseStudyData } from '@/lib/cms/mock-data';
import type { Metadata } from 'next';
interface CaseDetailPageProps {
params: Promise<{ slug: string }>;
}
export async function generateStaticParams() {
const slugs = await getAllPublishedSlugs('case-study');
return slugs.map((s) => ({ slug: s.slug }));
}
export async function generateMetadata({ params }: CaseDetailPageProps): Promise<Metadata> {
const { slug } = await params;
const item = await getPublishedItemBySlug('case-study', slug);
if (!item) {
return { title: `案例未找到 - ${COMPANY_INFO.displayName}` };
}
const data = item.data as unknown as CaseStudyData;
return {
title: `${item.title} - ${data.industry}案例 - ${COMPANY_INFO.displayName}`,
description: `${data.client}数字化转型案例:${data.challenge.substring(0, 80)}... ${data.metrics?.map((m: { value: string; label: string }) => `${m.value} ${m.label}`).join('')}`,
};
}
export default async function CaseDetailPage({ params }: CaseDetailPageProps) {
const { slug } = await params;
const item = await getPublishedItemBySlug('case-study', slug);
if (!item) { notFound(); }
return <CaseDetailClient item={item} />;
}
@@ -0,0 +1,320 @@
'use client';
import { useState, useMemo } from 'react';
import {
Building2, ArrowRight, TrendingUp, Target, Lightbulb,
Filter,
} from 'lucide-react';
import { ScrollReveal } from '@/components/ui/scroll-reveal';
import { Badge } from '@/components/ui/badge';
import { MetricCard } from '@/components/ui/metric-card';
import {
CASE_INDUSTRIES,
getCasesByIndustry,
type CaseStudy,
} from '@/lib/constants/cases';
import {
GrainOverlay,
FloatingInkParticles,
SectionLabel,
} from '@/components/ui/page-decoration';
import { cn } from '@/lib/utils';
const colorMap: Record<CaseStudy['color'], string> = {
brand: 'bg-brand text-brand',
blue: 'bg-accent-blue text-accent-blue',
teal: 'bg-accent-teal text-accent-teal',
amber: 'bg-accent-amber text-accent-amber',
purple: 'bg-accent-purple text-accent-purple',
};
function CaseCard({ caseItem }: { caseItem: CaseStudy; index?: number }) {
const colorStyle = colorMap[caseItem.color];
const [bgClass, textClass] = colorStyle.split(' ');
return (
<a
href={`/cases/${caseItem.slug}`}
className="group relative block overflow-hidden border border-white/[0.08] hover:border-white/20 bg-ink transition-all duration-500 hover:-translate-y-1 flex flex-col h-full"
>
<div className={cn(
'absolute top-0 left-0 right-0 h-1 scale-x-0 group-hover:scale-x-100 transition-transform duration-700 origin-left',
bgClass
)} />
<div className="p-6 sm:p-7 lg:p-8 flex flex-col flex-1">
<div className="flex items-center gap-3 mb-5">
<Badge variant="outline" className={cn('border-transparent', bgClass, 'text-white')}>
{caseItem.industry}
</Badge>
<span className="text-[10px] tracking-[0.25em] uppercase text-white/30 font-semibold">
{caseItem.companySize}
</span>
</div>
<h3 className="text-lg sm:text-xl font-bold text-white mb-4 leading-tight group-hover:text-brand transition-colors duration-300">
{caseItem.title}
</h3>
<div className="space-y-4 mb-6 flex-1">
<div>
<div className="flex items-center gap-2 mb-2">
<Target className={cn('w-3.5 h-3.5', textClass)} />
<span className={cn('text-[11px] tracking-wide uppercase font-bold', textClass)}></span>
</div>
<p className="text-sm text-white/50 leading-relaxed line-clamp-2 pl-5.5">
{caseItem.challenge}
</p>
</div>
<div>
<div className="flex items-center gap-2 mb-2">
<Lightbulb className={cn('w-3.5 h-3.5', textClass)} />
<span className={cn('text-[11px] tracking-wide uppercase font-bold', textClass)}></span>
</div>
<p className="text-sm text-white/50 leading-relaxed line-clamp-2 pl-5.5">
{caseItem.solution}
</p>
</div>
</div>
<div className="grid grid-cols-2 gap-4 pt-5 border-t border-white/[0.08] mb-5">
{caseItem.metrics.slice(0, 2).map((m, i) => (
<div key={i}>
<div className="text-2xl sm:text-3xl font-bold text-white mb-1 tabular-nums">
{m.value}
</div>
<div className="text-xs text-white/40">{m.label}</div>
</div>
))}
</div>
<div className="flex items-center justify-between pt-4 mt-auto border-t border-white/[0.06]">
<div className="flex items-center gap-2">
<Building2 className="w-4 h-4 text-white/30" />
<span className="text-xs text-white/40">{caseItem.client}</span>
</div>
<div className="flex items-center gap-1.5 text-brand text-sm font-medium">
<ArrowRight className="w-4 h-4 group-hover:translate-x-1 transition-transform duration-300" />
</div>
</div>
</div>
</a>
);
}
const PAGE_METRICS = [
{
icon: <Building2 className="w-5 h-5" />,
label: '服务客户',
value: 500,
suffix: '+',
description: '覆盖六大行业,500+企业客户的共同选择',
trend: { value: '23% YoY', direction: 'up' as const, label: '增长' },
accentColor: 'brand' as const,
},
{
icon: <TrendingUp className="w-5 h-5" />,
label: '成功率',
value: 98,
suffix: '%',
description: '项目一次性上线成功率,持续稳定交付',
trend: { value: '97%续约率', direction: 'up' as const, label: '' },
accentColor: 'teal' as const,
},
{
icon: <Target className="w-5 h-5" />,
label: '平均效率提升',
value: 35,
suffix: '%',
description: '客户核心业务流程效率平均提升幅度',
trend: { value: '基于 200+ 案例', direction: 'neutral' as const, label: '' },
accentColor: 'blue' as const,
},
{
icon: <Lightbulb className="w-5 h-5" />,
label: '行业覆盖',
value: 6,
suffix: '大',
description: '制造、零售、医疗、教育、金融、物流六大行业',
trend: { value: '持续扩展中', direction: 'up' as const, label: '' },
accentColor: 'amber' as const,
},
];
export default function CasesContentV1() {
const [selectedIndustry, setSelectedIndustry] = useState('all');
const filteredCases = useMemo(() => {
return getCasesByIndustry(selectedIndustry);
}, [selectedIndustry]);
return (
<div className="min-h-screen bg-ink text-white overflow-x-hidden" data-theme="dark">
{/* Hero Section */}
<section className="relative min-h-[70vh] flex items-center overflow-hidden bg-ink">
<GrainOverlay />
<FloatingInkParticles />
<div className="absolute inset-0 bg-gradient-to-b from-ink-light/30 via-transparent to-ink pointer-events-none" />
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10 w-full py-28 sm:py-36 md:py-40">
<div className="max-w-4xl">
<ScrollReveal>
<SectionLabel>Case Studies</SectionLabel>
</ScrollReveal>
<ScrollReveal delay={0.1}>
<h1 className="text-4xl sm:text-5xl md:text-6xl lg:text-7xl font-bold leading-[0.95] tracking-tighter mb-8 sm:mb-10">
<span className="block mt-3 text-brand"></span>
</h1>
</ScrollReveal>
<ScrollReveal delay={0.2}>
<p className="text-lg md:text-xl text-white/60 max-w-2xl leading-relaxed mb-10">
</p>
</ScrollReveal>
<ScrollReveal delay={0.3}>
<div className="flex flex-wrap gap-3">
<Badge variant="outline" className="border-brand/30 text-brand px-4 py-1.5 text-xs">
500+
</Badge>
<Badge variant="outline" className="border-white/20 text-white/70 px-4 py-1.5 text-xs">
98%
</Badge>
<Badge variant="outline" className="border-white/20 text-white/70 px-4 py-1.5 text-xs">
6
</Badge>
<Badge variant="outline" className="border-white/20 text-white/70 px-4 py-1.5 text-xs">
97%
</Badge>
</div>
</ScrollReveal>
</div>
</div>
</section>
{/* Key Metrics Section */}
<section className="relative py-16 sm:py-20 md:py-24 overflow-hidden bg-ink-light">
<div className="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-white/10 to-transparent" />
<div className="absolute bottom-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-white/10 to-transparent" />
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 md:gap-6">
{PAGE_METRICS.map((metric, i) => (
<ScrollReveal key={metric.label} delay={i * 0.08}>
<div className="h-full">
<MetricCard
icon={metric.icon}
label={metric.label}
value={metric.value}
suffix={metric.suffix}
description={metric.description}
trend={metric.trend}
accentColor={metric.accentColor}
theme="dark"
className="h-full"
/>
</div>
</ScrollReveal>
))}
</div>
</div>
</section>
{/* Cases List Section */}
<section className="relative py-20 sm:py-28 md:py-36 lg:py-44 overflow-hidden bg-ink">
<GrainOverlay />
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10">
<div className="flex flex-col lg:flex-row lg:items-end lg:justify-between gap-8 mb-12 sm:mb-16 md:mb-20">
<ScrollReveal className="lg:max-w-2xl">
<SectionLabel>All Cases</SectionLabel>
<h2 className="text-3xl sm:text-4xl md:text-5xl font-bold tracking-tight leading-tight mt-4">
</h2>
<p className="mt-6 text-lg text-white/60 leading-relaxed">
</p>
</ScrollReveal>
<ScrollReveal delay={0.1}>
<div className="flex items-center gap-2 flex-wrap">
<Filter className="w-4 h-4 text-white/40" />
{CASE_INDUSTRIES.map((industry) => (
<button
key={industry.id}
onClick={() => setSelectedIndustry(industry.id)}
className={cn(
'px-4 py-2 text-xs font-medium transition-all duration-300',
selectedIndustry === industry.id
? 'bg-brand text-white'
: 'bg-white/[0.05] text-white/60 hover:bg-white/[0.1] hover:text-white'
)}
>
{industry.label}
</button>
))}
</div>
</ScrollReveal>
</div>
{filteredCases.length > 0 ? (
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6 md:gap-8">
{filteredCases.map((caseItem, i) => (
<ScrollReveal key={caseItem.id} delay={i * 0.08}>
<div className="h-full">
<CaseCard caseItem={caseItem} index={i} />
</div>
</ScrollReveal>
))}
</div>
) : (
<div className="text-center py-20">
<p className="text-white/50 text-lg"></p>
</div>
)}
</div>
</section>
{/* CTA Section */}
<section className="relative py-20 sm:py-28 md:py-36 lg:py-44 overflow-hidden bg-ink-light">
<GrainOverlay />
<FloatingInkParticles />
<div className="absolute inset-0 bg-gradient-to-b from-ink-light/50 to-ink pointer-events-none" />
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10">
<ScrollReveal className="text-center max-w-3xl mx-auto">
<h2 className="text-3xl sm:text-4xl md:text-5xl font-bold tracking-tight leading-tight">
<span className="text-brand"></span>
</h2>
<p className="mt-6 text-lg text-white/60 leading-relaxed">
</p>
<div className="mt-12 flex flex-col sm:flex-row justify-center gap-4 sm:gap-6">
<a
href="/contact"
className="group inline-flex items-center justify-center gap-3 px-12 py-6 font-semibold text-base bg-brand text-white transition-all duration-300 hover:bg-brand/90 min-h-[52px] touch-manipulation"
>
<ArrowRight className="w-4 h-4 transition-transform duration-300 group-hover:translate-x-1" />
</a>
<a
href="/services"
className="inline-flex items-center justify-center gap-3 px-12 py-6 font-semibold text-base text-white border border-white/20 hover:border-white/40 hover:bg-white/[0.03] transition-all duration-300 min-h-[52px] touch-manipulation"
>
</a>
</div>
</ScrollReveal>
</div>
</section>
</div>
);
}
@@ -0,0 +1,316 @@
'use client';
import { useState, useMemo } from 'react';
import { motion } from 'framer-motion';
import { Building2, ArrowUpRight, Target, Lightbulb, Filter } from 'lucide-react';
import {
CASE_INDUSTRIES,
getCasesByIndustry,
type CaseStudy,
} from '@/lib/constants/cases';
import { cn } from '@/lib/utils';
const EASE_OUT = [0.22, 1, 0.36, 1] as const;
const PAGE_METRICS = [
{ value: '500+', label: '服务客户', sub: '覆盖六大行业' },
{ value: '98%', label: '项目成功率', sub: '持续稳定交付' },
{ value: '35%', label: '平均效率提升', sub: '核心业务流程' },
{ value: '6大', label: '行业覆盖', sub: '制造零售医疗等' },
];
function CaseCard({ caseItem, index }: { caseItem: CaseStudy; index: number }) {
return (
<motion.a
href={`/cases/${caseItem.slug}`}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-60px' }}
transition={{ duration: 0.6, delay: index * 0.06, ease: EASE_OUT }}
className="group relative block overflow-hidden border border-border-primary hover:border-border-secondary bg-white hover:bg-bg-secondary transition-all duration-500 flex flex-col h-full"
>
<div className="absolute top-0 left-0 h-0 w-[2px] group-hover:h-full transition-all duration-500 origin-top bg-brand" />
<div className="p-6 sm:p-7 lg:p-8 flex flex-col flex-1 relative z-10">
<div className="flex items-center gap-3 mb-5">
<span className="inline-block px-3 py-1 text-[11px] font-bold text-brand border border-brand/20 bg-brand/[0.05]">
{caseItem.industry}
</span>
<span className="text-[10px] tracking-[0.25em] uppercase text-text-muted font-semibold">
{caseItem.companySize}
</span>
</div>
<h3 className="text-lg sm:text-xl font-bold text-ink mb-4 leading-tight group-hover:text-brand transition-colors duration-300">
{caseItem.title}
</h3>
<div className="space-y-4 mb-6 flex-1">
<div>
<div className="flex items-center gap-2 mb-2">
<Target className="w-3.5 h-3.5 text-brand" />
<span className="text-[11px] tracking-wide uppercase font-bold text-brand"></span>
</div>
<p className="text-sm text-text-secondary leading-relaxed line-clamp-2 pl-5.5">
{caseItem.challenge}
</p>
</div>
<div>
<div className="flex items-center gap-2 mb-2">
<Lightbulb className="w-3.5 h-3.5 text-text-muted" />
<span className="text-[11px] tracking-wide uppercase font-bold text-text-secondary"></span>
</div>
<p className="text-sm text-text-secondary leading-relaxed line-clamp-2 pl-5.5">
{caseItem.solution}
</p>
</div>
</div>
<div className="grid grid-cols-2 gap-4 pt-5 border-t border-border-primary mb-5">
{caseItem.metrics.slice(0, 2).map((m, i) => (
<div key={i}>
<div className={`text-2xl sm:text-3xl font-black mb-1 tabular-nums tracking-tight ${
i === 0 ? 'text-brand' : 'text-ink'
}`}>
{m.value}
</div>
<div className="text-xs text-text-muted">{m.label}</div>
</div>
))}
</div>
<div className="flex items-center justify-between pt-4 mt-auto border-t border-border-primary">
<div className="flex items-center gap-2">
<Building2 className="w-4 h-4 text-text-muted" />
<span className="text-xs text-text-muted">{caseItem.client}</span>
</div>
<div className="flex items-center gap-1.5 text-brand text-sm font-semibold">
<ArrowUpRight className="w-4 h-4 group-hover:translate-x-0.5 group-hover:-translate-y-0.5 transition-transform duration-300" />
</div>
</div>
</div>
</motion.a>
);
}
export default function CasesContentV2({
cases: casesProp,
industries: industriesProp,
}: {
cases?: CaseStudy[];
industries?: { id: string; label: string }[];
}) {
const [selectedIndustry, setSelectedIndustry] = useState('all');
const industries = industriesProp ?? CASE_INDUSTRIES;
const filteredCases = useMemo(() => {
if (casesProp) return casesProp;
return getCasesByIndustry(selectedIndustry);
}, [selectedIndustry, casesProp]);
return (
<div className="min-h-screen bg-white text-ink overflow-x-hidden">
{/* Hero Section */}
<section className="relative min-h-[70vh] flex items-center overflow-hidden bg-white">
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10 w-full py-28 sm:py-36 md:py-40">
<div className="max-w-4xl">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.1, ease: EASE_OUT }}
className="text-sm font-mono tracking-[0.2em] text-text-muted uppercase mb-8"
>
Case Studies
</motion.div>
<motion.h1
initial={{ opacity: 0, y: 40 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 1, delay: 0.2, ease: EASE_OUT }}
className="text-4xl sm:text-5xl md:text-6xl lg:text-7xl xl:text-8xl font-black text-ink leading-[0.88] tracking-tightest mb-10 sm:mb-12 md:mb-16"
>
<div className="block"></div>
<div className="block mt-4 sm:mt-6">
<span className="relative inline-block">
<span className="relative text-brand font-black">
<motion.span
className="absolute -bottom-2 left-0 w-full h-[3px] bg-brand"
style={{ transformOrigin: 'left' }}
initial={{ scaleX: 0 }}
animate={{ scaleX: 1 }}
transition={{ duration: 0.8, delay: 1.2, ease: EASE_OUT }}
/>
</span>
</span>
</div>
</motion.h1>
<motion.p
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.5, ease: EASE_OUT }}
className="text-lg md:text-xl text-text-secondary max-w-2xl leading-relaxed mb-12"
>
<br className="hidden sm:block" />
</motion.p>
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.7, ease: EASE_OUT }}
className="flex flex-wrap gap-3"
>
<span className="px-4 py-2 text-xs font-bold text-brand border border-brand/20 bg-brand/[0.05]">
500+
</span>
<span className="px-4 py-2 text-xs font-medium text-text-secondary border border-border-primary">
98%
</span>
<span className="px-4 py-2 text-xs font-medium text-text-secondary border border-border-primary">
6
</span>
<span className="px-4 py-2 text-xs font-medium text-text-secondary border border-border-primary">
97%
</span>
</motion.div>
</div>
</div>
</section>
{/* Metrics Strip */}
<section className="relative py-16 sm:py-20 md:py-24 overflow-hidden bg-bg-secondary">
<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" />
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10">
<div className="grid grid-cols-2 lg:grid-cols-4 gap-8 md:gap-12">
{PAGE_METRICS.map((metric, i) => (
<motion.div
key={metric.label}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-80px' }}
transition={{ duration: 0.6, delay: i * 0.08, ease: EASE_OUT }}
className="text-center lg:text-left"
>
<div className={`text-3xl sm:text-4xl md:text-5xl font-black tracking-tight mb-2 ${
i === 0 ? 'text-brand' : 'text-ink'
}`}>
{metric.value}
</div>
<div className="text-sm font-medium text-text-secondary mb-1">{metric.label}</div>
<div className="text-xs text-text-muted">{metric.sub}</div>
</motion.div>
))}
</div>
</div>
</section>
{/* Cases List Section */}
<section className="relative py-20 sm:py-28 md:py-36 lg:py-44 overflow-hidden bg-white">
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10">
<div className="flex flex-col lg:flex-row lg:items-end lg:justify-between gap-8 mb-12 sm:mb-16 md:mb-20">
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-80px' }}
transition={{ duration: 0.8, ease: EASE_OUT }}
className="lg:max-w-2xl"
>
<div className="text-sm font-mono tracking-[0.2em] text-text-muted uppercase mb-6">
All Cases
</div>
<h2 className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-extrabold text-ink tracking-tight leading-[0.95]">
</h2>
<p className="mt-6 text-lg text-text-secondary leading-relaxed">
</p>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-80px' }}
transition={{ duration: 0.8, delay: 0.15, ease: EASE_OUT }}
>
<div className="flex items-center gap-2 flex-wrap">
<Filter className="w-4 h-4 text-text-muted" />
{industries.map((industry) => (
<button
key={industry.id}
onClick={() => setSelectedIndustry(industry.id)}
className={cn(
'px-4 py-2 text-xs font-medium transition-all duration-300',
selectedIndustry === industry.id
? 'bg-brand text-white'
: 'bg-bg-secondary text-text-secondary hover:bg-bg-tertiary hover:text-ink'
)}
>
{industry.label}
</button>
))}
</div>
</motion.div>
</div>
{filteredCases.length > 0 ? (
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-px bg-border-primary">
{filteredCases.map((caseItem, i) => (
<CaseCard key={caseItem.id} caseItem={caseItem} index={i} />
))}
</div>
) : (
<div className="text-center py-20">
<p className="text-text-secondary text-lg"></p>
</div>
)}
</div>
</section>
{/* CTA Section */}
<section className="relative py-20 sm:py-28 md:py-36 lg:py-44 overflow-hidden bg-bg-secondary">
<div className="absolute top-0 left-0 right-0 h-px bg-border-primary" />
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10">
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-80px' }}
transition={{ duration: 0.8, ease: EASE_OUT }}
className="max-w-3xl"
>
<h2 className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-extrabold text-ink tracking-tight leading-[0.95] mb-8">
<br className="sm:hidden" />
</h2>
<p className="text-lg text-text-secondary leading-relaxed mb-10">
<br className="hidden sm:block" />
</p>
<div className="flex flex-col sm:flex-row gap-4 sm:gap-6">
<a
href="/contact"
className="group inline-flex items-center justify-center gap-3 px-10 py-5 font-semibold text-base bg-brand text-white transition-all duration-300 hover:bg-brand/90 min-h-[52px] touch-manipulation"
>
<ArrowUpRight className="w-4 h-4 transition-transform duration-300 group-hover:translate-x-0.5 group-hover:-translate-y-0.5" />
</a>
<a
href="/services"
className="inline-flex items-center justify-center gap-3 px-10 py-5 font-semibold text-base text-ink border border-border-primary hover:border-border-secondary hover:bg-white transition-all duration-300 min-h-[52px] touch-manipulation"
>
</a>
</div>
</motion.div>
</div>
</section>
</div>
);
}
@@ -0,0 +1,315 @@
'use client';
import { useState, useMemo } from 'react';
import { motion } from 'framer-motion';
import { Building2, ArrowUpRight, Target, Lightbulb, Filter } from 'lucide-react';
import {
type CaseStudy,
} from '@/lib/constants/cases';
import { cn } from '@/lib/utils';
const EASE_OUT = [0.22, 1, 0.36, 1] as const;
const PAGE_METRICS = [
{ value: '500+', label: '服务客户', sub: '覆盖六大行业' },
{ value: '98%', label: '项目成功率', sub: '持续稳定交付' },
{ value: '35%', label: '平均效率提升', sub: '核心业务流程' },
{ value: '6大', label: '行业覆盖', sub: '制造零售医疗等' },
];
function CaseCard({ caseItem, index }: { caseItem: CaseStudy; index: number }) {
return (
<motion.a
href={`/cases/${caseItem.slug}`}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-60px' }}
transition={{ duration: 0.6, delay: index * 0.06, ease: EASE_OUT }}
className="group relative block overflow-hidden border border-border-primary hover:border-border-secondary bg-white hover:bg-bg-secondary transition-all duration-500 flex flex-col h-full"
>
<div className="absolute top-0 left-0 h-0 w-[2px] group-hover:h-full transition-all duration-500 origin-top bg-brand" />
<div className="p-6 sm:p-7 lg:p-8 flex flex-col flex-1 relative z-10">
<div className="flex items-center gap-3 mb-5">
<span className="inline-block px-3 py-1 text-[11px] font-bold text-brand border border-brand/20 bg-brand/[0.05]">
{caseItem.industry}
</span>
<span className="text-[10px] tracking-[0.25em] uppercase text-text-muted font-semibold">
{caseItem.companySize}
</span>
</div>
<h3 className="text-lg sm:text-xl font-bold text-ink mb-4 leading-tight group-hover:text-brand transition-colors duration-300">
{caseItem.title}
</h3>
<div className="space-y-4 mb-6 flex-1">
<div>
<div className="flex items-center gap-2 mb-2">
<Target className="w-3.5 h-3.5 text-brand" />
<span className="text-[11px] tracking-wide uppercase font-bold text-brand"></span>
</div>
<p className="text-sm text-text-secondary leading-relaxed line-clamp-2 pl-5.5">
{caseItem.challenge}
</p>
</div>
<div>
<div className="flex items-center gap-2 mb-2">
<Lightbulb className="w-3.5 h-3.5 text-text-muted" />
<span className="text-[11px] tracking-wide uppercase font-bold text-text-secondary"></span>
</div>
<p className="text-sm text-text-secondary leading-relaxed line-clamp-2 pl-5.5">
{caseItem.solution}
</p>
</div>
</div>
<div className="grid grid-cols-2 gap-4 pt-5 border-t border-border-primary mb-5">
{caseItem.metrics.slice(0, 2).map((m, i) => (
<div key={i}>
<div className={`text-2xl sm:text-3xl font-black mb-1 tabular-nums tracking-tight ${
i === 0 ? 'text-brand' : 'text-ink'
}`}>
{m.value}
</div>
<div className="text-xs text-text-muted">{m.label}</div>
</div>
))}
</div>
<div className="flex items-center justify-between pt-4 mt-auto border-t border-border-primary">
<div className="flex items-center gap-2">
<Building2 className="w-4 h-4 text-text-muted" />
<span className="text-xs text-text-muted">{caseItem.client}</span>
</div>
<div className="flex items-center gap-1.5 text-brand text-sm font-semibold">
<ArrowUpRight className="w-4 h-4 group-hover:translate-x-0.5 group-hover:-translate-y-0.5 transition-transform duration-300" />
</div>
</div>
</div>
</motion.a>
);
}
export default function CasesContentV3({
cases: casesProp,
industries: industriesProp,
}: {
cases?: CaseStudy[];
industries?: { id: string; label: string }[];
}) {
const [selectedIndustry, setSelectedIndustry] = useState('all');
const industries = industriesProp ?? [];
const filteredCases = useMemo(() => {
const allCases = casesProp ?? [];
if (selectedIndustry === 'all') return allCases;
return allCases.filter(c => c.industry === selectedIndustry);
}, [selectedIndustry, casesProp]);
return (
<div className="min-h-screen bg-white text-ink overflow-x-hidden">
{/* Hero Section */}
<section className="relative min-h-[70vh] flex items-center overflow-hidden bg-white">
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10 w-full py-28 sm:py-36 md:py-40">
<div className="max-w-4xl">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.1, ease: EASE_OUT }}
className="text-sm font-mono tracking-[0.2em] text-text-muted uppercase mb-8"
>
Case Studies
</motion.div>
<motion.h1
initial={{ opacity: 0, y: 40 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 1, delay: 0.2, ease: EASE_OUT }}
className="text-4xl sm:text-5xl md:text-6xl lg:text-7xl xl:text-8xl font-black text-ink leading-[0.88] tracking-tightest mb-10 sm:mb-12 md:mb-16"
>
<div className="block"></div>
<div className="block mt-4 sm:mt-6">
<span className="relative inline-block">
<span className="relative text-brand font-black">
<motion.span
className="absolute -bottom-2 left-0 w-full h-[3px] bg-brand"
style={{ transformOrigin: 'left' }}
initial={{ scaleX: 0 }}
animate={{ scaleX: 1 }}
transition={{ duration: 0.8, delay: 1.2, ease: EASE_OUT }}
/>
</span>
</span>
</div>
</motion.h1>
<motion.p
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.5, ease: EASE_OUT }}
className="text-lg md:text-xl text-text-secondary max-w-2xl leading-relaxed mb-12"
>
<br className="hidden sm:block" />
</motion.p>
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.7, ease: EASE_OUT }}
className="flex flex-wrap gap-3"
>
<span className="px-4 py-2 text-xs font-bold text-brand border border-brand/20 bg-brand/[0.05]">
500+
</span>
<span className="px-4 py-2 text-xs font-medium text-text-secondary border border-border-primary">
98%
</span>
<span className="px-4 py-2 text-xs font-medium text-text-secondary border border-border-primary">
6
</span>
<span className="px-4 py-2 text-xs font-medium text-text-secondary border border-border-primary">
97%
</span>
</motion.div>
</div>
</div>
</section>
{/* Metrics Strip */}
<section className="relative py-16 sm:py-20 md:py-24 overflow-hidden bg-bg-secondary">
<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" />
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10">
<div className="grid grid-cols-2 lg:grid-cols-4 gap-8 md:gap-12">
{PAGE_METRICS.map((metric, i) => (
<motion.div
key={metric.label}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-80px' }}
transition={{ duration: 0.6, delay: i * 0.08, ease: EASE_OUT }}
className="text-center lg:text-left"
>
<div className={`text-3xl sm:text-4xl md:text-5xl font-black tracking-tight mb-2 ${
i === 0 ? 'text-brand' : 'text-ink'
}`}>
{metric.value}
</div>
<div className="text-sm font-medium text-text-secondary mb-1">{metric.label}</div>
<div className="text-xs text-text-muted">{metric.sub}</div>
</motion.div>
))}
</div>
</div>
</section>
{/* Cases List Section */}
<section className="relative py-20 sm:py-28 md:py-36 lg:py-44 overflow-hidden bg-white">
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10">
<div className="flex flex-col lg:flex-row lg:items-end lg:justify-between gap-8 mb-12 sm:mb-16 md:mb-20">
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-80px' }}
transition={{ duration: 0.8, ease: EASE_OUT }}
className="lg:max-w-2xl"
>
<div className="text-sm font-mono tracking-[0.2em] text-text-muted uppercase mb-6">
All Cases
</div>
<h2 className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-extrabold text-ink tracking-tight leading-[0.95]">
</h2>
<p className="mt-6 text-lg text-text-secondary leading-relaxed">
</p>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-80px' }}
transition={{ duration: 0.8, delay: 0.15, ease: EASE_OUT }}
>
<div className="flex items-center gap-2 flex-wrap">
<Filter className="w-4 h-4 text-text-muted" />
{industries.map((industry) => (
<button
key={industry.id}
onClick={() => setSelectedIndustry(industry.id)}
className={cn(
'px-4 py-2 text-xs font-medium transition-all duration-300',
selectedIndustry === industry.id
? 'bg-brand text-white'
: 'bg-bg-secondary text-text-secondary hover:bg-bg-tertiary hover:text-ink'
)}
>
{industry.label}
</button>
))}
</div>
</motion.div>
</div>
{filteredCases.length > 0 ? (
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-px bg-border-primary">
{filteredCases.map((caseItem, i) => (
<CaseCard key={caseItem.id} caseItem={caseItem} index={i} />
))}
</div>
) : (
<div className="text-center py-20">
<p className="text-text-secondary text-lg"></p>
</div>
)}
</div>
</section>
{/* CTA Section */}
<section className="relative py-20 sm:py-28 md:py-36 lg:py-44 overflow-hidden bg-bg-secondary">
<div className="absolute top-0 left-0 right-0 h-px bg-border-primary" />
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10">
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-80px' }}
transition={{ duration: 0.8, ease: EASE_OUT }}
className="max-w-3xl"
>
<h2 className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-extrabold text-ink tracking-tight leading-[0.95] mb-8">
<br className="sm:hidden" />
</h2>
<p className="text-lg text-text-secondary leading-relaxed mb-10">
<br className="hidden sm:block" />
</p>
<div className="flex flex-col sm:flex-row gap-4 sm:gap-6">
<a
href="/contact"
className="group inline-flex items-center justify-center gap-3 px-10 py-5 font-semibold text-base bg-brand text-white transition-all duration-300 hover:bg-brand/90 min-h-[52px] touch-manipulation"
>
<ArrowUpRight className="w-4 h-4 transition-transform duration-300 group-hover:translate-x-0.5 group-hover:-translate-y-0.5" />
</a>
<a
href="/services"
className="inline-flex items-center justify-center gap-3 px-10 py-5 font-semibold text-base text-ink border border-border-primary hover:border-border-secondary hover:bg-white transition-all duration-300 min-h-[52px] touch-manipulation"
>
</a>
</div>
</motion.div>
</div>
</section>
</div>
);
}
+8
View File
@@ -0,0 +1,8 @@
'use client';
import type { CaseStudy } from '@/lib/constants/cases';
import CasesContentV3 from './cases-content-v3';
export function CasesClient({ cases }: { cases?: CaseStudy[] }) {
return <CasesContentV3 cases={cases} />;
}
+16
View File
@@ -0,0 +1,16 @@
import { Metadata } from 'next';
import { COMPANY_INFO } from '@/lib/constants';
import { getPublishedItems } from '@/lib/cms/data-server';
import type { CaseStudy } from '@/lib/constants/cases';
import { CasesClient } from './client';
export const metadata: Metadata = {
title: `客户案例 - ${COMPANY_INFO.displayName}`,
description: `500+企业客户的成功案例,覆盖制造、零售、医疗、教育、金融、物流六大行业。真实数据,可衡量的成果——见证数字化转型的价值。`,
};
export default async function CasesPage() {
const items = await getPublishedItems('case-study');
const cases = items.map((item) => ({ ...item.data, title: item.data.title || item.title } as unknown as CaseStudy));
return <CasesClient cases={cases} />;
}
+153
View File
@@ -0,0 +1,153 @@
'use client';
import { useState, useEffect } from 'react';
import { initCms } from '@/lib/cms';
import { ZoneManager } from '@/components/cms/ZoneManager';
import ContentEditor from '@/components/cms/ContentEditor';
type ViewMode = 'content' | 'zones';
export default function CmsStudioPage() {
const [ready, setReady] = useState(false);
const [view, setView] = useState<ViewMode>('zones');
useEffect(() => {
initCms();
setReady(true);
}, []);
if (!ready) {
return (
<div className="h-screen bg-ink flex items-center justify-center">
<div className="text-white/50">...</div>
</div>
);
}
return (
<div className="h-screen flex bg-ink text-white overflow-hidden" data-theme="dark">
<aside className="w-56 flex-shrink-0 border-r border-white/[0.08] bg-ink-light flex flex-col">
<div className="p-4 border-b border-white/[0.08]">
<div className="flex items-center gap-3">
<div className="w-9 h-9 rounded-xl bg-gradient-to-br from-brand to-brand/70 flex items-center justify-center text-white font-bold text-sm">
N
</div>
<div>
<div className="text-sm font-semibold text-white">Novalon CMS</div>
<div className="text-xs text-white/40"></div>
</div>
</div>
</div>
<nav className="flex-1 overflow-y-auto p-3 space-y-1">
<div className="px-3 py-2 text-xs text-white/40 font-medium uppercase tracking-wider">
</div>
<button
onClick={() => setView('zones')}
className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm transition-colors text-left ${
view === 'zones'
? 'bg-brand/20 text-brand font-medium'
: 'text-white/60 hover:bg-white/[0.05] hover:text-white'
}`}
>
<span className="text-lg">🎯</span>
<span></span>
</button>
<button
onClick={() => setView('content')}
className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm transition-colors text-left ${
view === 'content'
? 'bg-brand/20 text-brand font-medium'
: 'text-white/60 hover:bg-white/[0.05] hover:text-white'
}`}
>
<span className="text-lg">📄</span>
<span></span>
</button>
<div className="my-3 border-t border-white/[0.06]" />
<div className="px-3 py-2 text-xs text-white/40 font-medium uppercase tracking-wider">
</div>
<a
href="/"
target="_blank"
className="w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm text-white/60 hover:bg-white/[0.05] hover:text-white transition-colors"
>
<span className="text-lg">🏠</span>
<span></span>
</a>
<a
href="/cases"
target="_blank"
className="w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm text-white/60 hover:bg-white/[0.05] hover:text-white transition-colors"
>
<span className="text-lg">📁</span>
<span></span>
</a>
</nav>
<div className="p-3 border-t border-white/[0.08]">
<div className="px-3 py-2 rounded-lg bg-white/[0.03] text-xs text-white/40">
<div className="font-medium text-white/60 mb-1">💡 使</div>
<div className="leading-relaxed">
{view === 'zones'
? '拖拽内容排序,配置布局,底部实时预览效果'
: '选择内容类型,编辑字段,右侧预览渲染效果'}
</div>
</div>
</div>
</aside>
<main className="flex-1 flex flex-col min-w-0">
<header className="h-14 flex-shrink-0 border-b border-white/[0.08] bg-ink-light flex items-center justify-between px-6">
<div className="flex items-center gap-4">
<h1 className="text-base font-semibold text-white">
{view === 'zones' ? '内容区管理' : '内容管理'}
</h1>
<span className="text-xs text-white/40">
{view === 'zones' ? '配置页面内容区块' : '管理所有内容条目'}
</span>
</div>
<div className="flex items-center gap-2">
<div className="flex bg-white/[0.05] rounded-lg p-0.5">
<button
onClick={() => setView('zones')}
className={`px-3 py-1.5 text-xs font-medium rounded-md transition-colors ${
view === 'zones'
? 'bg-white/[0.1] text-white'
: 'text-white/50 hover:text-white'
}`}
>
</button>
<button
onClick={() => setView('content')}
className={`px-3 py-1.5 text-xs font-medium rounded-md transition-colors ${
view === 'content'
? 'bg-white/[0.1] text-white'
: 'text-white/50 hover:text-white'
}`}
>
</button>
</div>
<button className="px-4 py-1.5 bg-brand hover:bg-brand-hover text-white text-xs font-medium rounded-lg transition-colors">
</button>
</div>
</header>
<div className="flex-1 min-h-0 overflow-hidden">
{view === 'zones' ? <ZoneManager /> : <ContentEditor />}
</div>
</main>
</div>
);
}
@@ -0,0 +1,557 @@
'use client';
import { useState, Suspense } from 'react';
import { useSearchParams } from 'next/navigation';
import { z } from 'zod';
import { ScrollReveal } from '@/components/ui/scroll-reveal';
import { Button } from '@/components/ui/button';
import { LabeledInput as Input } from '@/components/ui/labeled-input';
import { LabeledTextarea as Textarea } from '@/components/ui/labeled-textarea';
import { Toast } from '@/components/ui/toast';
import {
Mail,
MapPin,
Send,
Loader2,
Clock,
HeadphonesIcon,
CheckCircle2,
HelpCircle,
MessageSquare,
ArrowRight,
} from 'lucide-react';
import { COMPANY_INFO } from '@/lib/constants';
import { trackContactForm, trackConversion } from '@/lib/analytics';
import { BreadcrumbSchema } from '@/components/seo/structured-data';
import { LegacyTooltip as Tooltip } from '@/components/ui/tooltip';
import { cn } from '@/lib/utils';
const contactFormSchema = z.object({
name: z.string().min(2, '姓名至少需要2个字符'),
phone: z.string().regex(/^1[3-9]\d{9}$/, '请输入有效的手机号码'),
email: z.string().email('请输入有效的邮箱地址'),
subject: z.string().min(2, '主题至少需要2个字符'),
message: z.string().min(10, '留言内容至少需要10个字符'),
});
type ContactFormData = z.infer<typeof contactFormSchema>;
interface FormErrors {
name?: string;
phone?: string;
email?: string;
subject?: string;
message?: string;
}
const FAQ_ITEMS = [
{
q: '你们的服务流程是怎样的?',
a: '通常分四步:需求沟通 → 方案评估 → 签约启动 → 敏捷交付。首次咨询免费,我们会在 2 个工作日内给出初步方案建议。',
},
{
q: '收费模式是怎样的?',
a: '根据项目规模和合作模式灵活定价。项目制按里程碑付款,订阅制按月结算,长期陪跑模式可协商年度合作方案。',
},
{
q: '数据安全如何保障?',
a: '我们严格遵守数据保护规范,签署保密协议,采用加密传输和权限隔离。项目结束后,客户数据按要求彻底清除。',
},
{
q: '项目周期一般多长?',
a: '小型项目 2-4 周,中型项目 1-3 个月,大型项目按阶段推进。我们会在方案评估阶段给出明确的时间线。',
},
];
function GrainOverlay() {
return (
<div
className="absolute inset-0 pointer-events-none opacity-[0.03] mix-blend-overlay"
style={{
backgroundImage:
"url(\"data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E\")",
}}
/>
);
}
function SectionLabel({ children, className = '' }: { children: React.ReactNode; className?: string }) {
return (
<div className={cn('flex items-center gap-5 mb-7', className)}>
<div className="w-14 h-px bg-brand" />
<span className="text-[11px] tracking-[0.4em] uppercase font-bold text-brand">
{children}
</span>
<div className="w-14 h-px bg-brand" />
</div>
);
}
function ContactFormContent() {
const searchParams = useSearchParams();
const isSuccessFromRedirect = searchParams.get('success') === 'true';
const [showToast, setShowToast] = useState(isSuccessFromRedirect);
const [toastMessage, setToastMessage] = useState(
isSuccessFromRedirect ? '表单提交成功!我们会尽快与您联系。' : ''
);
const [toastType, setToastType] = useState<'success' | 'error'>(
isSuccessFromRedirect ? 'success' : 'success'
);
const [isSubmitting, setIsSubmitting] = useState(false);
const [isSubmitted, setIsSubmitted] = useState(isSuccessFromRedirect);
const [formData, setFormData] = useState<ContactFormData>({
name: '',
phone: '',
email: '',
subject: '',
message: '',
});
const [errors, setErrors] = useState<FormErrors>({});
const validateField = (field: keyof ContactFormData, value: string) => {
try {
contactFormSchema.shape[field].parse(value);
setErrors((prev) => ({ ...prev, [field]: undefined }));
} catch (error) {
if (error instanceof z.ZodError) {
const fieldError = error.issues[0];
if (fieldError) {
setErrors((prev) => ({ ...prev, [field]: fieldError.message }));
}
}
}
};
const handleChange = (field: keyof ContactFormData, value: string) => {
setFormData((prev) => ({ ...prev, [field]: value }));
if (errors[field]) {
validateField(field, value);
}
};
const handleBlur = (field: keyof ContactFormData, value: string) => {
validateField(field, value);
};
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
const result = contactFormSchema.safeParse(formData);
if (!result.success) {
const fieldErrors: FormErrors = {};
result.error.issues.forEach((issue) => {
const field = issue.path[0] as keyof ContactFormData;
fieldErrors[field] = issue.message;
});
setErrors(fieldErrors);
setToastMessage(`请检查表单:${Object.keys(fieldErrors).length} 项内容需要修正`);
setToastType('error');
setShowToast(true);
const firstErrorField = document.querySelector('[data-testid*="-input"][aria-invalid="true"]') as HTMLElement;
firstErrorField?.scrollIntoView({ behavior: 'smooth', block: 'center' });
firstErrorField?.focus();
return;
}
setIsSubmitting(true);
try {
const formBody = new URLSearchParams();
formBody.append('name', formData.name);
formBody.append('phone', formData.phone);
formBody.append('email', formData.email);
formBody.append('subject', formData.subject);
formBody.append('message', formData.message);
const response = await fetch('/api/contact', {
method: 'POST',
body: formBody,
});
const data = await response.json();
if (response.ok && (data.success === 'true' || data.success === true)) {
trackContactForm({
name: formData.name,
email: formData.email,
company: formData.subject,
});
trackConversion('contact_form_submission');
setToastMessage('表单提交成功!我们会尽快与您联系。');
setToastType('success');
setShowToast(true);
setIsSubmitted(true);
setFormData({ name: '', phone: '', email: '', subject: '', message: '' });
setErrors({});
} else {
const errorMsg = data.message || '提交失败,请稍后重试或直接发送邮件联系我们。';
if (errorMsg.includes('HTML files') || errorMsg.includes('web server')) {
setToastMessage('表单服务需要在生产环境激活。部署后首次提交会发送确认邮件到 ' + COMPANY_INFO.email);
} else {
setToastMessage(errorMsg);
}
setToastType('error');
setShowToast(true);
}
} catch {
setToastMessage('网络错误,请稍后重试。');
setToastType('error');
setShowToast(true);
} finally {
setIsSubmitting(false);
}
}
return (
<div className="min-h-screen bg-ink text-white overflow-x-hidden">
<BreadcrumbSchema items={[{ name: '首页', href: '/' }, { name: '联系我们', href: '/contact' }]} />
{showToast && (
<Toast
message={toastMessage}
type={toastType}
duration={toastType === 'error' ? 0 : undefined}
onClose={() => setShowToast(false)}
/>
)}
{/* Hero Section */}
<section className="relative pt-32 pb-24 lg:pt-40 lg:pb-32 overflow-hidden">
<GrainOverlay />
<div className="absolute inset-0 bg-gradient-to-b from-ink-light/50 to-ink pointer-events-none" />
<div className="max-w-container mx-auto px-6 lg:px-10 relative z-10">
<ScrollReveal>
<SectionLabel>Contact Us</SectionLabel>
</ScrollReveal>
<ScrollReveal delay={0.1}>
<p className="text-lg text-white/60 mb-4">
2
</p>
</ScrollReveal>
<ScrollReveal delay={0.2}>
<h1 className="text-4xl md:text-5xl lg:text-6xl xl:text-7xl font-bold tracking-tight leading-[1.1]">
<span className="block mt-2 text-brand"></span>
</h1>
</ScrollReveal>
<ScrollReveal delay={0.3}>
<p className="mt-8 text-lg md:text-xl text-white/60 max-w-2xl leading-relaxed">
</p>
</ScrollReveal>
</div>
</section>
{/* Form + Info Section */}
<section className="relative py-24 lg:py-32 overflow-hidden bg-ink-light">
<GrainOverlay />
<div className="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-white/12 to-transparent" />
<div className="max-w-container mx-auto px-6 lg:px-10 relative z-10">
<div className="grid lg:grid-cols-5 gap-10 lg:gap-14">
{/* Left: Contact Info */}
<ScrollReveal className="lg:col-span-2 space-y-8">
<div>
<h2 className="text-xl font-semibold text-white mb-8"></h2>
<div className="space-y-6" data-testid="contact-info">
<div className="flex items-start gap-4 group" data-testid="email-info">
<div className="w-12 h-12 bg-brand/10 border border-brand/20 flex items-center justify-center shrink-0 group-hover:scale-105 transition-transform duration-300">
<Mail className="w-5 h-5 text-brand" />
</div>
<div>
<p className="text-sm text-white/50 mb-1"></p>
<a
href={`mailto:${COMPANY_INFO.email}`}
className="text-white hover:text-brand transition-colors duration-300"
data-testid="email-link"
>
{COMPANY_INFO.email}
</a>
</div>
</div>
<div className="flex items-start gap-4 group" data-testid="address-info">
<div className="w-12 h-12 bg-brand/10 border border-brand/20 flex items-center justify-center shrink-0 group-hover:scale-105 transition-transform duration-300">
<MapPin className="w-5 h-5 text-brand" />
</div>
<div>
<p className="text-sm text-white/50 mb-1"></p>
<p className="text-white" data-testid="address-text">
{COMPANY_INFO.address}
</p>
</div>
</div>
</div>
</div>
<div className="border border-white/10 bg-ink p-8" data-testid="work-hours-card">
<div className="flex items-center gap-3 mb-4">
<Clock className="w-5 h-5 text-brand" />
<h2 className="text-base font-semibold text-white"></h2>
</div>
<div className="flex justify-between" data-testid="work-hours-row">
<span className="text-white/60"></span>
<span className="text-brand font-medium">9:00 - 18:00</span>
</div>
</div>
<div className="border border-white/10 bg-ink p-8">
<div className="flex items-center gap-3 mb-4">
<HeadphonesIcon className="w-5 h-5 text-brand" />
<h2 className="text-base font-semibold text-white"></h2>
</div>
<div className="space-y-4">
<div className="flex items-start gap-3">
<div className="w-1.5 h-1.5 bg-brand rounded-full mt-2.5 shrink-0" />
<p className="text-white/60"> 2 </p>
</div>
<div className="flex items-start gap-3">
<div className="w-1.5 h-1.5 bg-brand rounded-full mt-2.5 shrink-0" />
<p className="text-white/60"></p>
</div>
<div className="flex items-start gap-3">
<div className="w-1.5 h-1.5 bg-brand rounded-full mt-2.5 shrink-0" />
<p className="text-white/60"></p>
</div>
</div>
</div>
</ScrollReveal>
{/* Right: Form */}
<ScrollReveal delay={0.1} className="lg:col-span-3">
<div className="border border-white/10 bg-ink p-8 lg:p-10 h-full relative overflow-hidden">
<div className="absolute top-0 left-0 right-0 h-1 bg-brand" />
<h2 className="text-xl font-semibold text-white mb-8"></h2>
{isSubmitted ? (
<div className="text-center py-16">
<div className="w-20 h-20 bg-brand rounded-full flex items-center justify-center mx-auto mb-6">
<CheckCircle2 className="w-10 h-10 text-white" />
</div>
<h4 className="text-2xl font-semibold text-white mb-3"></h4>
<p className="text-white/60"></p>
</div>
) : (
<form onSubmit={handleSubmit} className="space-y-6" noValidate>
<input
type="text"
name="website"
style={{ display: 'none' }}
tabIndex={-1}
autoComplete="off"
aria-hidden="true"
/>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
<div className="relative">
<Input
name="name"
data-testid="name-input"
label={
<span className="flex items-center gap-1.5">
<Tooltip content="用于正式沟通和方案报价">
<HelpCircle className="w-3.5 h-3.5 text-white/40 hover:text-brand cursor-help" />
</Tooltip>
</span>
}
id="name"
placeholder="请输入您的姓名"
required
value={formData.name}
onChange={(e) => handleChange('name', e.target.value)}
onBlur={(e) => handleBlur('name', e.target.value)}
error={errors.name}
className="bg-ink-light border-white/10 text-white placeholder:text-white/30 focus:border-brand/50"
/>
</div>
<div className="relative">
<Input
name="phone"
data-testid="phone-input"
label={
<span className="flex items-center gap-1.5">
<Tooltip content="接收项目进度通知和验证码,仅用于业务联系">
<HelpCircle className="w-3.5 h-3.5 text-white/40 hover:text-brand cursor-help" />
</Tooltip>
</span>
}
id="phone"
type="tel"
placeholder="请输入11位手机号"
required
value={formData.phone}
onChange={(e) => handleChange('phone', e.target.value)}
onBlur={(e) => handleBlur('phone', e.target.value)}
error={errors.phone}
className="bg-ink-light border-white/10 text-white placeholder:text-white/30 focus:border-brand/50"
/>
</div>
</div>
<Input
name="email"
data-testid="email-input"
label="邮箱"
id="email"
type="email"
placeholder="请输入您的邮箱"
required
value={formData.email}
onChange={(e) => handleChange('email', e.target.value)}
onBlur={(e) => handleBlur('email', e.target.value)}
error={errors.email}
className="bg-ink-light border-white/10 text-white placeholder:text-white/30 focus:border-brand/50"
/>
<Input
name="subject"
data-testid="subject-input"
label="主题"
id="subject"
placeholder="请输入消息主题"
required
value={formData.subject}
onChange={(e) => handleChange('subject', e.target.value)}
onBlur={(e) => handleBlur('subject', e.target.value)}
error={errors.subject}
className="bg-ink-light border-white/10 text-white placeholder:text-white/30 focus:border-brand/50"
/>
<Textarea
name="message"
data-testid="message-input"
label="留言内容"
id="message"
placeholder="请输入您想咨询的内容"
rows={5}
required
value={formData.message}
onChange={(e) => handleChange('message', e.target.value)}
onBlur={(e) => handleBlur('message', e.target.value)}
error={errors.message}
className="bg-ink-light border-white/10 text-white placeholder:text-white/30 focus:border-brand/50"
/>
<Button
type="submit"
data-testid="submit-button"
size="xl"
className="w-full h-14 disabled:opacity-70"
disabled={isSubmitting}
>
{isSubmitting ? (
<span className="flex items-center justify-center gap-2">
<Loader2 className="h-5 w-5 animate-spin" />
<span>...</span>
</span>
) : (
<>
<Send className="mr-2 h-5 w-5" />
</>
)}
</Button>
</form>
)}
</div>
</ScrollReveal>
</div>
</div>
</section>
{/* FAQ Section */}
<section className="relative py-24 lg:py-32 overflow-hidden bg-ink">
<GrainOverlay />
<div className="max-w-container mx-auto px-6 lg:px-10 relative z-10">
<ScrollReveal className="mb-16">
<SectionLabel>FAQ</SectionLabel>
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold text-white tracking-tight leading-tight">
<span className="text-brand"></span>
</h2>
</ScrollReveal>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 max-w-5xl">
{FAQ_ITEMS.map((faq, idx) => (
<ScrollReveal key={faq.q} delay={idx * 0.08}>
<div className="border border-white/10 bg-ink-light p-8 h-full hover:border-brand/30 transition-colors duration-300">
<div className="flex items-start gap-4">
<MessageSquare className="w-6 h-6 text-brand shrink-0 mt-0.5" strokeWidth={1.8} />
<div>
<h3 className="text-base font-semibold text-white mb-3">{faq.q}</h3>
<p className="text-sm text-white/60 leading-relaxed">{faq.a}</p>
</div>
</div>
</div>
</ScrollReveal>
))}
</div>
</div>
</section>
{/* CTA Section */}
<section className="relative py-36 lg:py-44 overflow-hidden bg-ink-light">
<GrainOverlay />
<div className="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-white/12 to-transparent" />
<div className="absolute inset-0 pointer-events-none">
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[600px] h-[600px] rounded-full opacity-[0.08] blur-3xl bg-brand" />
</div>
<div className="max-w-container mx-auto px-6 lg:px-10 relative z-10">
<ScrollReveal className="max-w-3xl mx-auto text-center">
<SectionLabel className="justify-center">
<div className="flex items-center gap-5">
<div className="w-14 h-px bg-brand" />
<span className="text-[11px] tracking-[0.4em] uppercase font-bold text-brand">
Get in Touch
</span>
<div className="w-14 h-px bg-brand" />
</div>
</SectionLabel>
<h2 className="text-3xl md:text-4xl lg:text-6xl font-bold text-white tracking-tight leading-tight mb-8">
</h2>
<p className="text-lg md:text-xl text-white/60 leading-relaxed mb-12">
{COMPANY_INFO.email}
</p>
<div className="flex flex-wrap gap-6 justify-center">
<Button size="xl" asChild>
<a href={`mailto:${COMPANY_INFO.email}`}>
<ArrowRight className="w-5 h-5 ml-2" />
</a>
</Button>
<Button size="xl" variant="outline" asChild>
<a href="/"></a>
</Button>
</div>
</ScrollReveal>
</div>
</section>
</div>
);
}
export default function ContactContentV1() {
return (
<Suspense
fallback={
<div className="min-h-screen bg-ink flex items-center justify-center">
<div className="animate-pulse text-white/50">...</div>
</div>
}
>
<ContactFormContent />
</Suspense>
);
}
@@ -0,0 +1,529 @@
'use client';
import { useState, Suspense } from 'react';
import { useSearchParams } from 'next/navigation';
import { z } from 'zod';
import { ScrollReveal } from '@/components/ui/scroll-reveal';
import { Button } from '@/components/ui/button';
import { LabeledInput as Input } from '@/components/ui/labeled-input';
import { LabeledTextarea as Textarea } from '@/components/ui/labeled-textarea';
import { Toast } from '@/components/ui/toast';
import {
Mail,
MapPin,
Send,
Loader2,
Clock,
HeadphonesIcon,
CheckCircle2,
HelpCircle,
MessageSquare,
ArrowRight,
} from 'lucide-react';
import { COMPANY_INFO } from '@/lib/constants';
import { trackContactForm, trackConversion } from '@/lib/analytics';
import { BreadcrumbSchema } from '@/components/seo/structured-data';
import { LegacyTooltip as Tooltip } from '@/components/ui/tooltip';
import { GrainOverlay, FloatingInkParticles, SectionLabel } from '@/components/ui/page-decoration';
const contactFormSchema = z.object({
name: z.string().min(2, '姓名至少需要2个字符'),
phone: z.string().regex(/^1[3-9]\d{9}$/, '请输入有效的手机号码'),
email: z.string().email('请输入有效的邮箱地址'),
subject: z.string().min(2, '主题至少需要2个字符'),
message: z.string().min(10, '留言内容至少需要10个字符'),
});
type ContactFormData = z.infer<typeof contactFormSchema>;
interface FormErrors {
name?: string;
phone?: string;
email?: string;
subject?: string;
message?: string;
}
const FAQ_ITEMS = [
{
q: '你们的服务流程是怎样的?',
a: '通常分四步:需求沟通 → 方案评估 → 签约启动 → 敏捷交付。首次咨询免费,我们会在 2 个工作日内给出初步方案建议。',
},
{
q: '收费模式是怎样的?',
a: '根据项目规模和合作模式灵活定价。项目制按里程碑付款,订阅制按月结算,长期陪跑模式可协商年度合作方案。',
},
{
q: '数据安全如何保障?',
a: '我们严格遵守数据保护规范,签署保密协议,采用加密传输和权限隔离。项目结束后,客户数据按要求彻底清除。',
},
{
q: '项目周期一般多长?',
a: '小型项目 2-4 周,中型项目 1-3 个月,大型项目按阶段推进。我们会在方案评估阶段给出明确的时间线。',
},
];
function ContactFormContent() {
const searchParams = useSearchParams();
const isSuccessFromRedirect = searchParams.get('success') === 'true';
const [showToast, setShowToast] = useState(isSuccessFromRedirect);
const [toastMessage, setToastMessage] = useState(
isSuccessFromRedirect ? '表单提交成功!我们会尽快与您联系。' : ''
);
const [toastType, setToastType] = useState<'success' | 'error'>(
isSuccessFromRedirect ? 'success' : 'success'
);
const [isSubmitting, setIsSubmitting] = useState(false);
const [isSubmitted, setIsSubmitted] = useState(isSuccessFromRedirect);
const [formData, setFormData] = useState<ContactFormData>({
name: '',
phone: '',
email: '',
subject: '',
message: '',
});
const [errors, setErrors] = useState<FormErrors>({});
const validateField = (field: keyof ContactFormData, value: string) => {
try {
contactFormSchema.shape[field].parse(value);
setErrors((prev) => ({ ...prev, [field]: undefined }));
} catch (error) {
if (error instanceof z.ZodError) {
const fieldError = error.issues[0];
if (fieldError) {
setErrors((prev) => ({ ...prev, [field]: fieldError.message }));
}
}
}
};
const handleChange = (field: keyof ContactFormData, value: string) => {
setFormData((prev) => ({ ...prev, [field]: value }));
if (errors[field]) {
validateField(field, value);
}
};
const handleBlur = (field: keyof ContactFormData, value: string) => {
validateField(field, value);
};
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
const result = contactFormSchema.safeParse(formData);
if (!result.success) {
const fieldErrors: FormErrors = {};
result.error.issues.forEach((issue) => {
const field = issue.path[0] as keyof ContactFormData;
fieldErrors[field] = issue.message;
});
setErrors(fieldErrors);
setToastMessage(`请检查表单:${Object.keys(fieldErrors).length} 项内容需要修正`);
setToastType('error');
setShowToast(true);
const firstErrorField = document.querySelector('[data-testid*="-input"][aria-invalid="true"]') as HTMLElement;
firstErrorField?.scrollIntoView({ behavior: 'smooth', block: 'center' });
firstErrorField?.focus();
return;
}
setIsSubmitting(true);
try {
const formBody = new URLSearchParams();
formBody.append('name', formData.name);
formBody.append('phone', formData.phone);
formBody.append('email', formData.email);
formBody.append('subject', formData.subject);
formBody.append('message', formData.message);
const response = await fetch('/api/contact', {
method: 'POST',
body: formBody,
});
const data = await response.json();
if (response.ok && (data.success === 'true' || data.success === true)) {
trackContactForm({
name: formData.name,
email: formData.email,
company: formData.subject,
});
trackConversion('contact_form_submission');
setToastMessage('表单提交成功!我们会尽快与您联系。');
setToastType('success');
setShowToast(true);
setIsSubmitted(true);
setFormData({ name: '', phone: '', email: '', subject: '', message: '' });
setErrors({});
} else {
const errorMsg = data.message || '提交失败,请稍后重试或直接发送邮件联系我们。';
if (errorMsg.includes('HTML files') || errorMsg.includes('web server')) {
setToastMessage('表单服务需要在生产环境激活。部署后首次提交会发送确认邮件到 ' + COMPANY_INFO.email);
} else {
setToastMessage(errorMsg);
}
setToastType('error');
setShowToast(true);
}
} catch {
setToastMessage('网络错误,请稍后重试。');
setToastType('error');
setShowToast(true);
} finally {
setIsSubmitting(false);
}
}
return (
<div className="min-h-screen bg-ink text-white overflow-x-hidden">
<BreadcrumbSchema items={[{ name: '首页', href: '/' }, { name: '联系我们', href: '/contact' }]} />
{showToast && (
<Toast
message={toastMessage}
type={toastType}
duration={toastType === 'error' ? 0 : undefined}
onClose={() => setShowToast(false)}
/>
)}
{/* Hero Section */}
<section className="relative min-h-[75vh] flex items-center overflow-hidden bg-ink">
<GrainOverlay />
<FloatingInkParticles />
<div className="absolute inset-0 bg-gradient-to-b from-ink-light/30 via-transparent to-ink pointer-events-none" />
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10 w-full py-28 sm:py-36 md:py-44 lg:py-56">
<div className="max-w-4xl">
<ScrollReveal>
<SectionLabel>Contact Us</SectionLabel>
</ScrollReveal>
<ScrollReveal delay={0.1}>
<p className="text-lg text-white/60 mb-4">
2
</p>
</ScrollReveal>
<ScrollReveal delay={0.2}>
<h1 className="text-4xl sm:text-5xl md:text-6xl lg:text-7xl xl:text-8xl font-bold leading-[0.92] tracking-tighter mb-10 sm:mb-12 md:mb-16">
<span className="block mt-4 text-brand"></span>
</h1>
</ScrollReveal>
<ScrollReveal delay={0.3}>
<p className="text-lg md:text-xl text-white/60 max-w-2xl leading-relaxed">
</p>
</ScrollReveal>
</div>
</div>
</section>
{/* Form + Info Section */}
<section className="relative py-20 sm:py-28 md:py-36 lg:py-44 overflow-hidden bg-ink-light">
<GrainOverlay />
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10">
<div className="grid lg:grid-cols-5 gap-10 lg:gap-14">
{/* Left: Contact Info */}
<ScrollReveal className="lg:col-span-2 space-y-8">
<div>
<h2 className="text-xl font-semibold text-white mb-8"></h2>
<div className="space-y-6" data-testid="contact-info">
<div className="flex items-start gap-4 group" data-testid="email-info">
<div className="w-12 h-12 bg-brand/[0.08] border border-brand/30 flex items-center justify-center shrink-0 group-hover:scale-105 transition-transform duration-300">
<Mail className="w-5 h-5 text-brand" />
</div>
<div>
<p className="text-sm text-white/50 mb-1"></p>
<a
href={`mailto:${COMPANY_INFO.email}`}
className="text-white hover:text-brand transition-colors duration-300"
data-testid="email-link"
>
{COMPANY_INFO.email}
</a>
</div>
</div>
<div className="flex items-start gap-4 group" data-testid="address-info">
<div className="w-12 h-12 bg-brand/[0.08] border border-brand/30 flex items-center justify-center shrink-0 group-hover:scale-105 transition-transform duration-300">
<MapPin className="w-5 h-5 text-brand" />
</div>
<div>
<p className="text-sm text-white/50 mb-1"></p>
<p className="text-white" data-testid="address-text">
{COMPANY_INFO.address}
</p>
</div>
</div>
</div>
</div>
<div className="relative border border-white/[0.08] bg-ink p-8 overflow-hidden">
<div className="absolute top-0 left-0 bottom-0 w-1 bg-brand/60" />
<div className="flex items-center gap-3 mb-4">
<Clock className="w-5 h-5 text-brand" />
<h2 className="text-base font-semibold text-white"></h2>
</div>
<div className="flex justify-between" data-testid="work-hours-row">
<span className="text-white/60"></span>
<span className="text-brand font-medium">9:00 - 18:00</span>
</div>
</div>
<div className="relative border border-white/[0.08] bg-ink p-8 overflow-hidden">
<div className="absolute top-0 left-0 bottom-0 w-1 bg-brand/60" />
<div className="flex items-center gap-3 mb-4">
<HeadphonesIcon className="w-5 h-5 text-brand" />
<h2 className="text-base font-semibold text-white"></h2>
</div>
<div className="space-y-4">
<div className="flex items-start gap-3">
<div className="w-1.5 h-1.5 bg-brand rounded-full mt-2.5 shrink-0" />
<p className="text-white/60"> 2 </p>
</div>
<div className="flex items-start gap-3">
<div className="w-1.5 h-1.5 bg-brand rounded-full mt-2.5 shrink-0" />
<p className="text-white/60"></p>
</div>
<div className="flex items-start gap-3">
<div className="w-1.5 h-1.5 bg-brand rounded-full mt-2.5 shrink-0" />
<p className="text-white/60"></p>
</div>
</div>
</div>
</ScrollReveal>
{/* Right: Form */}
<ScrollReveal delay={0.1} className="lg:col-span-3">
<div className="relative border border-white/[0.08] bg-ink p-6 sm:p-8 lg:p-10 h-full overflow-hidden">
<div className="absolute top-0 left-0 right-0 h-1 bg-brand" />
<h2 className="text-xl font-semibold text-white mb-8"></h2>
{isSubmitted ? (
<div className="text-center py-16">
<div className="w-20 h-20 bg-brand rounded-full flex items-center justify-center mx-auto mb-6">
<CheckCircle2 className="w-10 h-10 text-white" />
</div>
<h4 className="text-2xl font-semibold text-white mb-3"></h4>
<p className="text-white/60"></p>
</div>
) : (
<form onSubmit={handleSubmit} className="space-y-6" noValidate>
<input
type="text"
name="website"
style={{ display: 'none' }}
tabIndex={-1}
autoComplete="off"
aria-hidden="true"
aria-label="honeypot"
/>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
<div className="relative">
<Input
name="name"
data-testid="name-input"
label={
<span className="flex items-center gap-1.5">
<Tooltip content="用于正式沟通和方案报价">
<HelpCircle className="w-3.5 h-3.5 text-white/40 hover:text-brand cursor-help" />
</Tooltip>
</span>
}
id="name"
placeholder="请输入您的姓名"
required
value={formData.name}
onChange={(e) => handleChange('name', e.target.value)}
onBlur={(e) => handleBlur('name', e.target.value)}
error={errors.name}
className="bg-ink-light border-white/10 text-white placeholder:text-white/30 focus:border-brand/50"
/>
</div>
<div className="relative">
<Input
name="phone"
data-testid="phone-input"
label={
<span className="flex items-center gap-1.5">
<Tooltip content="接收项目进度通知和验证码,仅用于业务联系">
<HelpCircle className="w-3.5 h-3.5 text-white/40 hover:text-brand cursor-help" />
</Tooltip>
</span>
}
id="phone"
type="tel"
placeholder="请输入11位手机号"
required
value={formData.phone}
onChange={(e) => handleChange('phone', e.target.value)}
onBlur={(e) => handleBlur('phone', e.target.value)}
error={errors.phone}
className="bg-ink-light border-white/10 text-white placeholder:text-white/30 focus:border-brand/50"
/>
</div>
</div>
<Input
name="email"
data-testid="email-input"
label="邮箱"
id="email"
type="email"
placeholder="请输入您的邮箱"
required
value={formData.email}
onChange={(e) => handleChange('email', e.target.value)}
onBlur={(e) => handleBlur('email', e.target.value)}
error={errors.email}
className="bg-ink-light border-white/10 text-white placeholder:text-white/30 focus:border-brand/50"
/>
<Input
name="subject"
data-testid="subject-input"
label="主题"
id="subject"
placeholder="请输入消息主题"
required
value={formData.subject}
onChange={(e) => handleChange('subject', e.target.value)}
onBlur={(e) => handleBlur('subject', e.target.value)}
error={errors.subject}
className="bg-ink-light border-white/10 text-white placeholder:text-white/30 focus:border-brand/50"
/>
<Textarea
name="message"
data-testid="message-input"
label="留言内容"
id="message"
placeholder="请输入您想咨询的内容"
rows={5}
required
value={formData.message}
onChange={(e) => handleChange('message', e.target.value)}
onBlur={(e) => handleBlur('message', e.target.value)}
error={errors.message}
className="bg-ink-light border-white/10 text-white placeholder:text-white/30 focus:border-brand/50"
/>
<Button
type="submit"
data-testid="submit-button"
size="xl"
className="w-full h-14 disabled:opacity-70"
disabled={isSubmitting}
>
{isSubmitting ? (
<span className="flex items-center justify-center gap-2">
<Loader2 className="h-5 w-5 animate-spin" />
<span>...</span>
</span>
) : (
<>
<Send className="mr-2 h-5 w-5" />
</>
)}
</Button>
</form>
)}
</div>
</ScrollReveal>
</div>
</div>
</section>
{/* FAQ Section */}
<section className="relative py-20 sm:py-28 md:py-36 lg:py-44 overflow-hidden bg-ink">
<GrainOverlay />
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10">
<ScrollReveal className="mb-16">
<SectionLabel>FAQ</SectionLabel>
<h2 className="text-3xl sm:text-4xl md:text-5xl font-bold tracking-tight leading-tight">
<span className="text-brand"></span>
</h2>
</ScrollReveal>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 max-w-5xl">
{FAQ_ITEMS.map((faq, idx) => (
<ScrollReveal key={faq.q} delay={idx * 0.08}>
<div className="relative border border-white/[0.08] bg-ink-light p-8 h-full overflow-hidden group hover:border-brand/30 transition-colors duration-300">
<div className="absolute top-0 left-0 bottom-0 w-1 bg-brand/40 scale-y-0 group-hover:scale-y-100 transition-transform duration-500 origin-top" />
<div className="flex items-start gap-4">
<MessageSquare className="w-6 h-6 text-brand shrink-0 mt-0.5" strokeWidth={1.8} />
<div>
<h3 className="text-base font-semibold text-white mb-3">{faq.q}</h3>
<p className="text-sm text-white/60 leading-relaxed">{faq.a}</p>
</div>
</div>
</div>
</ScrollReveal>
))}
</div>
</div>
</section>
{/* CTA Section */}
<section className="relative py-20 sm:py-28 md:py-36 lg:py-44 overflow-hidden bg-ink-light">
<GrainOverlay />
<FloatingInkParticles />
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10">
<ScrollReveal className="max-w-3xl mx-auto text-center">
<SectionLabel className="justify-center">Get in Touch</SectionLabel>
<h2 className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-bold tracking-tight leading-tight mb-8">
</h2>
<p className="text-lg md:text-xl text-white/60 leading-relaxed mb-12">
{COMPANY_INFO.email}
</p>
<div className="flex flex-col sm:flex-row gap-4 sm:gap-6 justify-center">
<a
href={`mailto:${COMPANY_INFO.email}`}
className="group inline-flex items-center justify-center gap-3 px-12 py-6 font-semibold text-base bg-brand text-white transition-all duration-300 hover:bg-brand/90"
>
<ArrowRight className="w-5 h-5 transition-transform duration-300 group-hover:translate-x-1" />
</a>
<a
href="/"
className="inline-flex items-center justify-center gap-3 px-12 py-6 font-semibold text-base text-white border border-white/20 hover:border-white/40 hover:bg-white/[0.03] transition-all duration-300"
>
</a>
</div>
</ScrollReveal>
</div>
</section>
</div>
);
}
export default function ContactContentV2() {
return (
<Suspense
fallback={
<div className="min-h-screen bg-ink flex items-center justify-center">
<div className="animate-pulse text-white/50">...</div>
</div>
}
>
<ContactFormContent />
</Suspense>
);
}
@@ -0,0 +1,576 @@
'use client';
import { useState, Suspense } from 'react';
import { useSearchParams } from 'next/navigation';
import { z } from 'zod';
import { motion } from 'framer-motion';
import { Button } from '@/components/ui/button';
import { LabeledInput as Input } from '@/components/ui/labeled-input';
import { LabeledTextarea as Textarea } from '@/components/ui/labeled-textarea';
import { Toast } from '@/components/ui/toast';
import {
Mail,
MapPin,
Send,
Loader2,
Clock,
HeadphonesIcon,
CheckCircle2,
HelpCircle,
ArrowRight,
} from 'lucide-react';
import { trackContactForm, trackConversion } from '@/lib/analytics';
import { BreadcrumbSchema } from '@/components/seo/structured-data';
import { LegacyTooltip as Tooltip } from '@/components/ui/tooltip';
const EASE_OUT = [0.22, 1, 0.36, 1] as const;
const contactFormSchema = z.object({
name: z.string().min(2, '姓名至少需要2个字符'),
phone: z.string().regex(/^1[3-9]\d{9}$/, '请输入有效的手机号码'),
email: z.string().email('请输入有效的邮箱地址'),
subject: z.string().min(2, '主题至少需要2个字符'),
message: z.string().min(10, '留言内容至少需要10个字符'),
});
type ContactFormData = z.infer<typeof contactFormSchema>;
interface FormErrors {
name?: string;
phone?: string;
email?: string;
subject?: string;
message?: string;
}
interface ContactData {
heroSubtitle?: string;
heroTitle?: string;
heroDescription?: string;
contactTitle?: string;
emailLabel?: string;
addressLabel?: string;
email?: string;
address?: string;
workHoursTitle?: string;
dayLabel?: string;
hours?: string;
commitmentTitle?: string;
commitmentItems?: string[];
formTitle?: string;
successTitle?: string;
successMessage?: string;
submitLabel?: string;
submittingLabel?: string;
ctaSubtitle?: string;
ctaTitle?: string;
ctaDescription?: string;
ctaPrimaryLabel?: string;
ctaPrimaryHref?: string;
ctaSecondaryLabel?: string;
ctaSecondaryHref?: string;
}
function ContactFormContent({ data }: { data: ContactData }) {
const searchParams = useSearchParams();
const isSuccessFromRedirect = searchParams.get('success') === 'true';
const [showToast, setShowToast] = useState(isSuccessFromRedirect);
const [toastMessage, setToastMessage] = useState(
isSuccessFromRedirect ? '表单提交成功!我们会尽快与您联系。' : ''
);
const [toastType, setToastType] = useState<'success' | 'error'>(
isSuccessFromRedirect ? 'success' : 'success'
);
const [isSubmitting, setIsSubmitting] = useState(false);
const [isSubmitted, setIsSubmitted] = useState(isSuccessFromRedirect);
const [formData, setFormData] = useState<ContactFormData>({
name: '',
phone: '',
email: '',
subject: '',
message: '',
});
const [errors, setErrors] = useState<FormErrors>({});
const emailAddress = data.email ?? '';
const addressText = data.address ?? '';
const validateField = (field: keyof ContactFormData, value: string) => {
try {
contactFormSchema.shape[field].parse(value);
setErrors((prev) => ({ ...prev, [field]: undefined }));
} catch (error) {
if (error instanceof z.ZodError) {
const fieldError = error.issues[0];
if (fieldError) {
setErrors((prev) => ({ ...prev, [field]: fieldError.message }));
}
}
}
};
const handleChange = (field: keyof ContactFormData, value: string) => {
setFormData((prev) => ({ ...prev, [field]: value }));
if (errors[field]) {
validateField(field, value);
}
};
const handleBlur = (field: keyof ContactFormData, value: string) => {
validateField(field, value);
};
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
const result = contactFormSchema.safeParse(formData);
if (!result.success) {
const fieldErrors: FormErrors = {};
result.error.issues.forEach((issue) => {
const field = issue.path[0] as keyof ContactFormData;
if (!fieldErrors[field]) {
fieldErrors[field] = issue.message;
}
});
setErrors(fieldErrors);
setToastMessage(`请检查表单:${Object.keys(fieldErrors).length} 项内容需要修正`);
setToastType('error');
setShowToast(true);
// 滚动到表单区域,让用户看到错误提示
setTimeout(() => {
const formSection = document.getElementById('contact-form-section');
if (formSection) {
formSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
}, 100);
return;
}
setIsSubmitting(true);
try {
const formBody = new URLSearchParams();
formBody.append('name', formData.name);
formBody.append('phone', formData.phone);
formBody.append('email', formData.email);
formBody.append('subject', formData.subject);
formBody.append('message', formData.message);
const response = await fetch('/api/contact', {
method: 'POST',
body: formBody,
});
const resultData = await response.json();
if (response.ok && (resultData.success === 'true' || resultData.success === true)) {
trackContactForm({
name: formData.name,
email: formData.email,
company: formData.subject,
});
trackConversion('contact_form_submission');
setToastMessage(data.successMessage || '');
setToastType('success');
setShowToast(true);
setIsSubmitted(true);
setFormData({ name: '', phone: '', email: '', subject: '', message: '' });
setErrors({});
} else {
const errorMsg = resultData.message || '提交失败,请稍后重试或直接发送邮件联系我们。';
if (errorMsg.includes('HTML files') || errorMsg.includes('web server')) {
setToastMessage(`表单服务需要在生产环境激活。部署后首次提交会发送确认邮件到 ${emailAddress}`);
} else {
setToastMessage(errorMsg);
}
setToastType('error');
setShowToast(true);
}
} catch {
setToastMessage('网络错误,请稍后重试。');
setToastType('error');
setShowToast(true);
} finally {
setIsSubmitting(false);
}
}
const heroTitleLines = (data.heroTitle || '').split('\n');
const commitmentItems = data.commitmentItems ?? [];
return (
<div className="min-h-screen bg-white text-ink overflow-x-hidden">
<BreadcrumbSchema items={[{ name: '首页', href: '/' }, { name: '联系我们', href: '/contact' }]} />
{showToast && (
<Toast
message={toastMessage}
type={toastType}
duration={toastType === 'error' ? 0 : undefined}
onClose={() => setShowToast(false)}
/>
)}
{/* Hero Section */}
<section className="relative min-h-[80vh] flex items-center overflow-hidden bg-white">
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10 w-full py-28 sm:py-36 md:py-44 lg:py-56">
<div className="max-w-4xl">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.1, ease: EASE_OUT }}
className="text-sm font-mono tracking-[0.2em] text-text-muted uppercase mb-8"
>
{data.heroSubtitle || ''}
</motion.div>
<motion.h1
initial={{ opacity: 0, y: 40 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 1, delay: 0.2, ease: EASE_OUT }}
className="text-4xl sm:text-5xl md:text-6xl lg:text-7xl xl:text-8xl font-black text-ink leading-[0.88] tracking-tightest mb-10 sm:mb-12 md:mb-16"
>
{heroTitleLines.map((line, i) => (
<div key={i} className={`block ${i > 0 ? 'mt-4 sm:mt-6' : ''}`}>
{i === 1 ? (
<span className="relative inline-block">
<span className="relative text-brand font-black">
{line}
<motion.span
className="absolute -bottom-2 left-0 w-full h-[3px] bg-brand"
style={{ transformOrigin: 'left' }}
initial={{ scaleX: 0 }}
animate={{ scaleX: 1 }}
transition={{ duration: 0.8, delay: 1.2, ease: EASE_OUT }}
/>
</span>
</span>
) : (
line
)}
</div>
))}
</motion.h1>
<motion.p
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.5, ease: EASE_OUT }}
className="text-lg md:text-xl text-text-secondary max-w-2xl leading-relaxed mb-12 sm:mb-16"
>
{data.heroDescription || ''}
</motion.p>
</div>
</div>
</section>
{/* Form + Info Section */}
<section className="relative py-20 sm:py-28 md:py-36 lg:py-44 overflow-hidden bg-bg-secondary">
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10">
<div className="grid lg:grid-cols-5 gap-10 lg:gap-14">
{/* Left: Contact Info */}
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-60px' }}
transition={{ duration: 0.8, ease: EASE_OUT }}
className="lg:col-span-2 space-y-8"
>
<div>
<h2 className="text-xl font-bold text-ink mb-8">{data.contactTitle || ''}</h2>
<div className="space-y-6" data-testid="contact-info">
<div className="flex items-start gap-4" data-testid="email-info">
<div className="w-12 h-12 border border-border-primary bg-white flex items-center justify-center shrink-0">
<Mail className="w-5 h-5 text-text-secondary" />
</div>
<div>
<p className="text-sm text-text-muted mb-1">{data.emailLabel || ''}</p>
<a
href={`mailto:${emailAddress}`}
className="text-ink hover:text-brand transition-colors duration-300"
data-testid="email-link"
>
{emailAddress}
</a>
</div>
</div>
<div className="flex items-start gap-4" data-testid="address-info">
<div className="w-12 h-12 border border-border-primary bg-white flex items-center justify-center shrink-0">
<MapPin className="w-5 h-5 text-text-secondary" />
</div>
<div>
<p className="text-sm text-text-muted mb-1">{data.addressLabel || ''}</p>
<p className="text-ink" data-testid="address-text">
{addressText}
</p>
</div>
</div>
</div>
</div>
<div className="relative border border-border-primary bg-white p-8 overflow-hidden">
<div className="absolute top-0 left-0 bottom-0 w-0.5 bg-brand/60" />
<div className="flex items-center gap-3 mb-4">
<Clock className="w-5 h-5 text-brand" />
<h2 className="text-base font-bold text-ink">{data.workHoursTitle || ''}</h2>
</div>
<div className="flex justify-between" data-testid="work-hours-row">
<span className="text-text-secondary">{data.dayLabel || ''}</span>
<span className="text-brand font-medium">{data.hours || ''}</span>
</div>
</div>
<div className="relative border border-border-primary bg-white p-8 overflow-hidden">
<div className="absolute top-0 left-0 bottom-0 w-0.5 bg-brand/60" />
<div className="flex items-center gap-3 mb-4">
<HeadphonesIcon className="w-5 h-5 text-text-secondary" />
<h2 className="text-base font-bold text-ink">{data.commitmentTitle || ''}</h2>
</div>
<div className="space-y-4">
{commitmentItems.map((item, idx) => (
<div key={idx} className="flex items-start gap-3">
<div className="w-1 h-1 bg-text-muted rounded-full mt-2.5 shrink-0" />
<p className="text-text-secondary">{item}</p>
</div>
))}
</div>
</div>
</motion.div>
{/* Right: Form */}
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-60px' }}
transition={{ duration: 0.8, delay: 0.1, ease: EASE_OUT }}
className="lg:col-span-3"
>
<div className="relative border border-border-primary bg-white p-6 sm:p-8 lg:p-10 h-full overflow-hidden" id="contact-form-section">
<div className="absolute top-0 left-0 right-0 h-0.5 bg-brand" />
<h2 className="text-xl font-bold text-ink mb-8">{data.formTitle || ''}</h2>
{Object.keys(errors).length > 0 && (
<div className="mb-6 p-4 border border-brand/30 bg-brand/5" role="alert">
<p className="text-sm font-bold text-brand mb-2">
{Object.keys(errors).length}
</p>
<ul className="space-y-1">
{Object.entries(errors).map(([field, message]) => (
<li key={field} className="text-sm text-text-secondary flex items-start gap-2">
<span className="text-brand shrink-0 mt-0.5">&#8226;</span>
<span>{message}</span>
</li>
))}
</ul>
</div>
)}
{isSubmitted ? (
<div className="text-center py-16">
<div className="w-20 h-20 bg-brand rounded-full flex items-center justify-center mx-auto mb-6">
<CheckCircle2 className="w-10 h-10 text-white" />
</div>
<h4 className="text-2xl font-bold text-ink mb-3">{data.successTitle || ''}</h4>
<p className="text-text-secondary">{data.successMessage || ''}</p>
</div>
) : (
<form onSubmit={handleSubmit} className="space-y-6" noValidate>
<input
type="text"
name="website"
style={{ display: 'none' }}
tabIndex={-1}
autoComplete="off"
aria-hidden="true"
aria-label="honeypot"
/>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
<div className="relative">
<Input
name="name"
data-testid="name-input"
label={
<span className="flex items-center gap-1.5">
<Tooltip content="用于正式沟通和方案报价">
<HelpCircle className="w-3.5 h-3.5 text-text-muted hover:text-brand cursor-help" />
</Tooltip>
</span>
}
id="name"
placeholder="请输入您的姓名"
required
value={formData.name}
onChange={(e) => handleChange('name', e.target.value)}
onBlur={(e) => handleBlur('name', e.target.value)}
error={errors.name}
className="bg-white border-border-primary text-ink placeholder:text-text-muted focus:border-brand/50"
/>
</div>
<div className="relative">
<Input
name="phone"
data-testid="phone-input"
label={
<span className="flex items-center gap-1.5">
<Tooltip content="接收项目进度通知和验证码,仅用于业务联系">
<HelpCircle className="w-3.5 h-3.5 text-text-muted hover:text-brand cursor-help" />
</Tooltip>
</span>
}
id="phone"
type="tel"
placeholder="请输入11位手机号"
required
value={formData.phone}
onChange={(e) => handleChange('phone', e.target.value)}
onBlur={(e) => handleBlur('phone', e.target.value)}
error={errors.phone}
className="bg-white border-border-primary text-ink placeholder:text-text-muted focus:border-brand/50"
/>
</div>
</div>
<Input
name="email"
data-testid="email-input"
label="邮箱"
id="email"
type="email"
placeholder="请输入您的邮箱"
required
value={formData.email}
onChange={(e) => handleChange('email', e.target.value)}
onBlur={(e) => handleBlur('email', e.target.value)}
error={errors.email}
className="bg-white border-border-primary text-ink placeholder:text-text-muted focus:border-brand/50"
/>
<Input
name="subject"
data-testid="subject-input"
label="主题"
id="subject"
placeholder="请输入消息主题"
required
value={formData.subject}
onChange={(e) => handleChange('subject', e.target.value)}
onBlur={(e) => handleBlur('subject', e.target.value)}
error={errors.subject}
className="bg-white border-border-primary text-ink placeholder:text-text-muted focus:border-brand/50"
/>
<Textarea
name="message"
data-testid="message-input"
label="留言内容"
id="message"
placeholder="请输入您想咨询的内容"
rows={5}
required
value={formData.message}
onChange={(e) => handleChange('message', e.target.value)}
onBlur={(e) => handleBlur('message', e.target.value)}
error={errors.message}
className="bg-white border-border-primary text-ink placeholder:text-text-muted focus:border-brand/50"
/>
<Button
type="submit"
data-testid="submit-button"
size="xl"
className="w-full h-14 disabled:opacity-70"
disabled={isSubmitting}
>
{isSubmitting ? (
<span className="flex items-center justify-center gap-2">
<Loader2 className="h-5 w-5 animate-spin" />
<span>{data.submittingLabel || '...'}</span>
</span>
) : (
<>
<span>{data.submitLabel || '发送'}</span>
<Send className="h-4 w-4" />
</>
)}
</Button>
</form>
)}
</div>
</motion.div>
</div>
</div>
</section>
{/* CTA Section */}
<section className="relative py-20 sm:py-28 md:py-36 lg:py-44 overflow-hidden bg-white">
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10">
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-60px' }}
transition={{ duration: 0.8, ease: EASE_OUT }}
className="max-w-3xl mx-auto text-center"
>
<div className="text-sm font-mono tracking-[0.2em] text-text-muted uppercase mb-8">
{data.ctaSubtitle || ''}
</div>
<h2 className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-black text-ink tracking-tight leading-tight mb-8">
{data.ctaTitle || ''}
</h2>
<p className="text-lg md:text-xl text-text-secondary leading-relaxed mb-12">
{data.ctaDescription || ''}
</p>
<div className="flex flex-col sm:flex-row gap-4 sm:gap-6 justify-center">
<a
href={data.ctaPrimaryHref || '#'}
className="group inline-flex items-center justify-center gap-3 px-12 py-6 font-bold text-base bg-brand text-white transition-all duration-300 hover:bg-brand/90"
>
{data.ctaPrimaryLabel || ''}
<ArrowRight className="w-5 h-5 transition-transform duration-300 group-hover:translate-x-1" />
</a>
<a
href={data.ctaSecondaryHref || '#'}
className="inline-flex items-center justify-center gap-3 px-12 py-6 font-bold text-base text-ink border border-border-primary hover:border-border-secondary hover:bg-bg-secondary transition-all duration-300"
>
{data.ctaSecondaryLabel || ''}
</a>
</div>
</motion.div>
</div>
</section>
</div>
);
}
function EmptyState() {
return (
<div className="min-h-screen bg-white flex items-center justify-center">
<div className="text-text-muted text-lg"></div>
</div>
);
}
export default function ContactContentV3({ data }: { data: Record<string, unknown> }) {
const contactData = data as ContactData;
// Empty state when no CMS data is available
if (!data || Object.keys(data).length === 0) {
return <EmptyState />;
}
return (
<Suspense
fallback={
<div className="min-h-screen bg-white flex items-center justify-center">
<div className="animate-pulse text-text-muted">...</div>
</div>
}
>
<ContactFormContent data={contactData} />
</Suspense>
);
}
+195
View File
@@ -0,0 +1,195 @@
'use client';
import { useEffect, useState, useCallback, useMemo, createElement } from 'react';
import {
initCms,
getItemRenderer,
ContentZoneRenderer,
getContentZone,
getContentItems,
} from '@/lib/cms';
import { getMockHomeHeroBanner } from '@/lib/cms/mock-home';
import type { ContentItem, ContentZone } from '@/lib/cms';
function SectionHeading({ eyebrow, title, description, align = 'center' }: {
eyebrow?: string;
title: string;
description?: string;
align?: 'center' | 'left';
}) {
return (
<div className={`mb-12 md:mb-16 ${align === 'center' ? 'text-center' : ''}`}>
{eyebrow && (
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-brand/10 text-brand text-xs font-medium mb-4">
<span className="w-1.5 h-1.5 rounded-full bg-brand" />
{eyebrow}
</div>
)}
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold text-white tracking-tight">
{title}
</h2>
{description && (
<p
className="mt-4 text-lg text-white/50 max-w-2xl leading-relaxed"
style={{ marginInline: align === 'center' ? 'auto' : undefined }}
>
{description}
</p>
)}
</div>
);
}
export default function HomeContentCms() {
const [ready, setReady] = useState(false);
const [hero, setHero] = useState<ContentItem | null>(null);
const [statsZone, setStatsZone] = useState<ContentZone | null>(null);
const [servicesZone, setServicesZone] = useState<ContentZone | null>(null);
const [solutionsZone, setSolutionsZone] = useState<ContentZone | null>(null);
const [casesZone, setCasesZone] = useState<ContentZone | null>(null);
const [newsZone, setNewsZone] = useState<ContentZone | null>(null);
const loadData = useCallback(() => {
const heroItems = getContentItems('hero-banner');
setHero(heroItems.length > 0 ? heroItems[0]! : getMockHomeHeroBanner());
setStatsZone(getContentZone('home-stats'));
setServicesZone(getContentZone('home-services'));
setSolutionsZone(getContentZone('home-solutions'));
setCasesZone(getContentZone('home-cases'));
setNewsZone(getContentZone('home-news'));
}, []);
useEffect(() => {
initCms();
loadData();
setReady(true);
const handleStorage = (e: StorageEvent) => {
if (e.key === 'novalon-cms-data') {
loadData();
}
};
window.addEventListener('storage', handleStorage);
const handleCmsUpdate = () => loadData();
window.addEventListener('cms-updated', handleCmsUpdate);
return () => {
window.removeEventListener('storage', handleStorage);
window.removeEventListener('cms-updated', handleCmsUpdate);
};
}, [loadData]);
const HeroRenderer = useMemo(
() => (hero ? getItemRenderer(hero.modelCode) : null),
[hero]
);
if (!ready) {
return (
<div className="min-h-screen bg-ink flex items-center justify-center">
<div className="text-white/50">...</div>
</div>
);
}
return (
<div className="min-h-screen bg-ink text-white overflow-x-hidden" data-theme="dark">
{hero && HeroRenderer && createElement(HeroRenderer, { item: hero })}
<section className="relative py-16 sm:py-20 md:py-28 overflow-hidden bg-ink-light">
<div className="max-w-7xl mx-auto px-6 lg:px-8">
<SectionHeading
eyebrow="数据说话"
title="用数字证明实力"
description="我们的价值,体现在每一个可量化的成果中。"
/>
{statsZone && <ContentZoneRenderer zone={statsZone} />}
</div>
</section>
<section className="relative py-20 sm:py-28 md:py-36 overflow-hidden bg-ink">
<div className="max-w-7xl mx-auto px-6 lg:px-8">
<SectionHeading
eyebrow="核心服务"
title="端到端的数字化服务能力"
description="从战略咨询到技术实施,从平台建设到运营运维,覆盖企业数字化转型的全生命周期。"
/>
{servicesZone && <ContentZoneRenderer zone={servicesZone} />}
</div>
</section>
<section className="relative py-20 sm:py-28 md:py-36 overflow-hidden bg-ink-light">
<div className="max-w-7xl mx-auto px-6 lg:px-8">
<SectionHeading
eyebrow="解决方案"
title="行业场景化解决方案"
description="深入理解不同行业的业务特性,提供量身定制的数字化解决方案。"
/>
{solutionsZone && <ContentZoneRenderer zone={solutionsZone} />}
</div>
</section>
<section className="relative py-20 sm:py-28 md:py-36 overflow-hidden bg-ink">
<div className="max-w-7xl mx-auto px-6 lg:px-8">
<div className="flex flex-col md:flex-row md:items-end md:justify-between mb-12 md:mb-16">
<SectionHeading
eyebrow="客户案例"
title="成果验证 · 客户信赖"
description="每一个项目,都是我们与客户共同成长的见证。"
align="left"
/>
<a href="/cases" className="inline-flex items-center gap-2 text-brand font-medium hover:gap-3 transition-all">
<span></span>
</a>
</div>
{casesZone && <ContentZoneRenderer zone={casesZone} />}
</div>
</section>
<section className="relative py-20 sm:py-28 md:py-36 overflow-hidden bg-ink-light">
<div className="max-w-7xl mx-auto px-6 lg:px-8">
<div className="flex flex-col md:flex-row md:items-end md:justify-between mb-12 md:mb-16">
<SectionHeading
eyebrow="新闻动态"
title="最新动态"
description="了解公司最新动态、行业洞察和技术分享。"
align="left"
/>
<a href="/news" className="inline-flex items-center gap-2 text-brand font-medium hover:gap-3 transition-all">
<span></span>
</a>
</div>
{newsZone && <ContentZoneRenderer zone={newsZone} />}
</div>
</section>
<section className="relative py-20 sm:py-28 md:py-36 overflow-hidden bg-ink">
<div className="max-w-4xl mx-auto px-6 lg:px-8 text-center">
<SectionHeading
eyebrow="准备好开始了吗"
title="开启您的数字化转型之旅"
description="无论您处于数字化的哪个阶段,我们都能提供量身定制的解决方案。"
/>
<div className="mt-12 flex flex-wrap justify-center gap-4">
<a
href="/contact"
className="inline-flex items-center gap-2 px-8 py-4 bg-brand hover:bg-brand-hover text-white font-medium rounded-xl transition-all duration-300 hover:-translate-y-0.5 hover:shadow-lg hover:shadow-brand/20"
>
<span></span>
</a>
<a
href="/solutions"
className="inline-flex items-center gap-2 px-8 py-4 bg-white/[0.05] hover:bg-white/[0.1] text-white font-medium rounded-xl border border-white/[0.1] hover:border-white/[0.2] transition-all duration-300"
>
</a>
</div>
</div>
</section>
</div>
);
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+671
View File
@@ -0,0 +1,671 @@
'use client';
import { useRef, useState } from 'react';
import { motion, useScroll, useTransform } from 'framer-motion';
import { ScrollReveal, StaggerReveal } from '@/components/ui/scroll-reveal';
import { Badge } from '@/components/ui/badge';
import {
ArrowRight, ChevronDown,
Quote, CheckCircle2,
Zap, Award,
} from 'lucide-react';
import { cn } from '@/lib/utils';
import { SectionLabel, EASE_OUT } from '@/components/ui/page-decoration';
function MagneticButton({ children, href, variant = 'primary', className }: { children: React.ReactNode; href: string; variant?: 'primary' | 'secondary'; className?: string }) {
const ref = useRef<HTMLAnchorElement>(null);
const [position, setPosition] = useState({ x: 0, y: 0 });
const handleMouseMove = (e: React.MouseEvent) => {
if (!ref.current) return;
const rect = ref.current.getBoundingClientRect();
const x = (e.clientX - rect.left - rect.width / 2) * 0.15;
const y = (e.clientY - rect.top - rect.height / 2) * 0.15;
setPosition({ x, y });
};
const handleMouseLeave = () => {
setPosition({ x: 0, y: 0 });
};
return (
<motion.a
ref={ref}
href={href}
animate={{ x: position.x, y: position.y }}
transition={{ duration: 0.3, ease: EASE_OUT }}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
className={cn(
'group relative inline-flex items-center gap-3 px-12 py-6 font-semibold text-base overflow-hidden transition-transform duration-200',
variant === 'primary'
? 'bg-brand text-white'
: 'text-ink border border-border-primary hover:border-border-secondary hover:bg-bg-secondary',
className
)}
>
{variant === 'primary' && (
<div className="absolute inset-0 bg-black/20 translate-y-full group-hover:translate-y-0 transition-transform duration-500 ease-out" />
)}
<span className="relative z-10">{children}</span>
<ArrowRight className="w-5 h-5 relative z-10 transition-transform duration-500 group-hover:translate-x-2" />
</motion.a>
);
}
function HeroSection({ heroData }: { heroData?: Record<string, any> | null }) {
const containerRef = useRef<HTMLDivElement>(null);
const { scrollYProgress } = useScroll({
target: containerRef,
offset: ['start start', 'end start'],
});
const y = useTransform(scrollYProgress, [0, 1], [0, 160]);
const opacity = useTransform(scrollYProgress, [0, 0.7], [1, 0.15]);
const headingTop = heroData?.headingTop || '';
const headingBottom = heroData?.headingBottom || '';
const description = heroData?.description || '';
const ctaLabel = heroData?.ctaLabel || '';
const ctaHref = heroData?.ctaHref || '#';
const statValue = heroData?.statValue || '';
const statSubtext = heroData?.statSubtext || '';
if (!heroData) {
return (
<section className="relative min-h-screen flex items-center overflow-hidden bg-white">
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 text-center">
<p className="text-text-muted text-lg"> Hero </p>
</div>
</section>
);
}
return (
<section
ref={containerRef}
className="relative min-h-screen flex items-center overflow-hidden bg-white"
>
{/* 右侧抽象几何装饰 — 桌面端可见,暗示咨询框架 */}
<div className="absolute right-0 top-1/2 -translate-y-1/2 hidden lg:block pointer-events-none select-none" aria-hidden="true">
<div className="relative w-[480px] h-[480px]">
{/* 外圈:结构框架 */}
<motion.div
initial={{ opacity: 0, rotate: -8 }}
animate={{ opacity: 0.12, rotate: 0 }}
transition={{ duration: 1.5, delay: 0.3, ease: EASE_OUT }}
className="absolute top-0 right-0 w-64 h-64 border border-ink/20"
/>
<motion.div
initial={{ opacity: 0, rotate: 8 }}
animate={{ opacity: 0.08, rotate: 0 }}
transition={{ duration: 1.5, delay: 0.5, ease: EASE_OUT }}
className="absolute top-16 right-16 w-48 h-48 border border-ink/15"
/>
{/* 内圈:数据节点 */}
<motion.div
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 0.06, scale: 1 }}
transition={{ duration: 1.2, delay: 0.7, ease: EASE_OUT }}
className="absolute top-24 right-24 w-24 h-24 bg-brand"
/>
<motion.div
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 0.03, scale: 1 }}
transition={{ duration: 1.2, delay: 0.9, ease: EASE_OUT }}
className="absolute bottom-32 right-32 w-16 h-16 bg-ink"
/>
{/* 连线和节点 */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 0.06 }}
transition={{ duration: 1, delay: 1.1, ease: EASE_OUT }}
className="absolute top-32 right-12 w-20 h-px bg-ink/30"
/>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 0.04 }}
transition={{ duration: 1, delay: 1.3, ease: EASE_OUT }}
className="absolute top-48 right-0 w-28 h-px bg-ink/25"
/>
<motion.div
initial={{ opacity: 0, scale: 0 }}
animate={{ opacity: 0.1, scale: 1 }}
transition={{ duration: 0.8, delay: 1.5, ease: EASE_OUT }}
className="absolute top-32 right-12 w-1.5 h-1.5 bg-brand"
/>
<motion.div
initial={{ opacity: 0, scale: 0 }}
animate={{ opacity: 0.08, scale: 1 }}
transition={{ duration: 0.8, delay: 1.7, ease: EASE_OUT }}
className="absolute top-48 right-28 w-1.5 h-1.5 bg-ink"
/>
</div>
</div>
<div className="absolute bottom-0 left-0 w-full h-1/3 bg-gradient-to-t from-white to-transparent pointer-events-none" />
<motion.div
style={{ y, opacity }}
className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10 w-full py-32 sm:py-40 md:py-48 lg:py-56"
>
<div className="max-w-4xl">
<motion.h1
initial={{ opacity: 0, y: 60 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 1.2, delay: 0.1, ease: EASE_OUT }}
className="text-4xl sm:text-5xl md:text-6xl lg:text-7xl xl:text-8xl font-black text-ink leading-[0.92] tracking-tightest mb-10 sm:mb-12 md:mb-16"
>
{headingTop && (
<div className="block text-text-secondary font-bold text-2xl sm:text-3xl md:text-4xl lg:text-5xl tracking-tight mb-6 sm:mb-8">
{headingTop}
</div>
)}
<div className="block">
<span className="relative inline-block">
<span className="relative text-brand font-black">
{headingBottom}
<motion.span
className="absolute -bottom-3 left-0 w-full h-[3px] bg-brand"
style={{ transformOrigin: 'left' }}
initial={{ scaleX: 0 }}
animate={{ scaleX: 1 }}
transition={{ duration: 0.9, delay: 1.4, ease: EASE_OUT }}
/>
</span>
</span>
</div>
</motion.h1>
<motion.p
initial={{ opacity: 0, y: 40 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 1, delay: 0.5, ease: EASE_OUT }}
className="text-lg sm:text-xl text-text-secondary/80 max-w-2xl leading-relaxed mb-12 sm:mb-14 md:mb-16"
>
{description}
</motion.p>
<motion.div
initial={{ opacity: 0, y: 40 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 1, delay: 0.65, ease: EASE_OUT }}
className="flex flex-col sm:flex-row sm:items-center gap-6 sm:gap-10"
>
<MagneticButton href={ctaHref}>{ctaLabel || '预约咨询'}</MagneticButton>
{statValue && (
<div className="flex items-center gap-5 pl-4 border-l border-border-primary">
<div className="flex items-center gap-1.5">
{[0, 1, 2, 3].map((i) => (
<div key={i} className="w-2 h-2 rounded-full bg-brand" style={{ opacity: 1 - i * 0.22 }} />
))}
</div>
<div>
<div className="text-sm font-bold text-ink">{statValue}</div>
<div className="text-xs text-text-muted">{statSubtext}</div>
</div>
</div>
)}
</motion.div>
</div>
</motion.div>
<motion.div
animate={{ y: [0, 12, 0] }}
transition={{ duration: 2.5, repeat: Infinity, ease: 'easeInOut' }}
className="absolute left-4 sm:left-6 lg:left-10 flex flex-col items-start gap-4 bottom-[calc(4rem+env(safe-area-inset-bottom,0px))] md:bottom-16"
>
<ChevronDown className="w-5 h-5 text-text-muted" />
</motion.div>
</section>
);
}
function ServicesSection({ services }: { services?: Record<string, any>[] }) {
const hasValidData = services?.length && services[0]?.highlights;
const SERVICES = hasValidData ? services : [];
if (SERVICES.length === 0) {
return (
<section className="relative py-28 overflow-hidden bg-white">
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 text-center">
<p className="text-text-muted text-lg"></p>
</div>
</section>
);
}
return (
<section className="relative py-28 sm:py-36 md:py-44 lg:py-56 overflow-hidden bg-white">
<div className="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-border-primary to-transparent" />
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10">
<div className="max-w-3xl mb-20 sm:mb-28 md:mb-36">
<ScrollReveal>
<SectionLabel></SectionLabel>
<h2 className="font-sans text-2xl sm:text-3xl md:text-4xl lg:text-6xl font-black text-ink tracking-tight leading-[0.95] mt-6">
<br />
</h2>
</ScrollReveal>
</div>
<div className="grid md:grid-cols-3 gap-px bg-border-primary">
{/* 首张卡片:战略咨询 — 全宽突出 */}
{(() => {
const first = SERVICES[0];
if (!first) return null;
return (
<StaggerReveal className="md:col-span-3" staggerDelay={0.05}>
<a
href={first.href}
className="group relative block p-8 sm:p-10 md:p-12 lg:p-14 transition-all duration-500 hover:bg-bg-secondary overflow-hidden bg-white"
>
<div className="relative z-10 flex flex-col md:flex-row md:items-start gap-8 md:gap-16">
<div className="w-14 h-14 flex items-center justify-center bg-bg-secondary text-text-secondary group-hover:text-ink transition-colors duration-300 shrink-0">
{first.icon}
</div>
<div className="flex-1">
<div className="mb-6">
<h3 className="text-sm text-text-muted mb-3 font-medium">
{first.subtitle}
</h3>
<h3 className="text-xl lg:text-2xl font-bold text-ink transition-colors duration-300">
{first.title}
</h3>
</div>
<p className="text-text-secondary leading-relaxed mb-8 text-sm max-w-2xl">
{first.desc}
</p>
<div className="flex flex-wrap gap-2 mb-10">
{first.highlights.map((h: string, j: number) => (
<span
key={j}
className="px-3 py-1.5 text-xs text-text-muted border border-border-primary group-hover:border-border-secondary group-hover:text-text-secondary transition-all duration-300 font-medium"
>
{h}
</span>
))}
</div>
{(first as any).metrics && (
<div className="flex flex-wrap gap-10 mb-10 pb-10 border-b border-border-primary">
{(first as any).metrics.map((m: any, j: number) => (
<div key={j}>
<div className="text-3xl sm:text-4xl font-black text-brand tracking-tightest mb-1">
{m.value}
</div>
<div className="text-xs text-text-muted font-medium">
{m.label}
</div>
</div>
))}
</div>
)}
<div className="inline-flex items-center gap-2 text-sm font-semibold text-text-secondary group-hover:text-brand transition-colors duration-300">
<span></span>
<ArrowRight className="w-4 h-4 transition-transform duration-300 group-hover:translate-x-1" />
</div>
</div>
</div>
</a>
</StaggerReveal>
);
})()}
{/* 其余三张卡片:并排 */}
{SERVICES.slice(1).map((service, i) => (
<StaggerReveal key={i} staggerDelay={0.05}>
<a
href={service.href}
className="group relative block p-8 sm:p-10 md:p-12 lg:p-14 transition-all duration-500 hover:bg-bg-secondary overflow-hidden bg-white h-full flex flex-col"
>
<div className="relative z-10 flex flex-col flex-1">
<div className="flex items-start justify-between mb-10">
<div className="w-12 h-12 flex items-center justify-center bg-bg-secondary text-text-secondary group-hover:text-ink transition-colors duration-300">
{service.icon}
</div>
</div>
<div className="mb-6">
<h3 className="text-sm text-text-muted mb-3 font-medium">
{service.subtitle}
</h3>
<h3 className="text-xl lg:text-2xl font-bold text-ink transition-colors duration-300">
{service.title}
</h3>
</div>
<p className="text-text-secondary leading-relaxed mb-8 text-sm">
{service.desc}
</p>
<div className="flex flex-wrap gap-2 mb-8">
{service.highlights.map((h: string, j: number) => (
<span
key={j}
className="px-3 py-1.5 text-xs text-text-muted border border-border-primary group-hover:border-border-secondary group-hover:text-text-secondary transition-all duration-300 font-medium"
>
{h}
</span>
))}
</div>
{(service as any).metrics && (
<div className="flex gap-6 mb-8 pb-8 border-b border-border-primary">
{(service as any).metrics.map((m: any, j: number) => (
<div key={j}>
<div className="text-2xl sm:text-3xl font-black text-brand tracking-tightest mb-1">
{m.value}
</div>
<div className="text-[11px] text-text-muted font-medium">
{m.label}
</div>
</div>
))}
</div>
)}
<div className="mt-auto pt-2">
<div className="inline-flex items-center gap-2 text-sm font-semibold text-text-secondary group-hover:text-brand transition-colors duration-300">
<span></span>
<ArrowRight className="w-4 h-4 transition-transform duration-300 group-hover:translate-x-1" />
</div>
</div>
</div>
</a>
</StaggerReveal>
))}
</div>
</div>
</section>
);
}
function CasesSection({ cases }: { cases?: Record<string, any>[] }) {
const hasValidData = cases?.length && cases[0]?.metrics;
const CASES = hasValidData ? cases : [];
if (CASES.length === 0) {
return (
<section className="relative py-28 overflow-hidden bg-dark-bg">
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 text-center">
<p className="text-dark-text-muted text-lg"></p>
</div>
</section>
);
}
return (
<section className="relative py-28 sm:py-36 md:py-44 lg:py-56 overflow-hidden bg-dark-bg">
<div className="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-dark-border to-transparent" />
<div className="absolute inset-0 pointer-events-none">
<div className="absolute top-1/4 left-1/4 w-[400px] h-[400px] bg-brand/[0.03] rounded-full blur-[120px]" />
<div className="absolute bottom-1/3 right-1/4 w-[500px] h-[500px] bg-accent-blue/[0.02] rounded-full blur-[140px]" />
</div>
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10">
<div className="flex flex-col md:flex-row md:items-end md:justify-between gap-8 sm:gap-10 mb-20 sm:mb-28 md:mb-36">
<ScrollReveal className="md:max-w-3xl">
<div className="flex items-center gap-3 mb-6">
<div className="w-10 h-px bg-brand" />
<span className="text-[13px] tracking-[0.15em] font-medium text-brand">
</span>
</div>
<h2 className="font-sans text-2xl sm:text-3xl md:text-4xl lg:text-6xl font-black text-dark-text-primary tracking-tight leading-[0.95] mt-6">
<br />
</h2>
</ScrollReveal>
<ScrollReveal delay={0.1}>
<a
href="/cases"
className="group inline-flex items-center gap-3 text-dark-text-primary font-semibold text-sm"
>
<ArrowRight className="w-4 h-5 transition-transform duration-300 group-hover:translate-x-2 text-dark-text-muted group-hover:text-brand" />
</a>
</ScrollReveal>
</div>
<div className="space-y-16">
{CASES.map((caseItem, i) => (
<ScrollReveal key={i} delay={i * 0.15}>
<article className="group relative overflow-hidden border border-dark-border hover:border-dark-border/80 transition-all duration-500 bg-dark-bg-secondary">
<div className="relative z-10">
<div className="p-10 sm:p-12 lg:p-16 pb-0">
<div className="flex items-center gap-4 mb-10">
<Badge className="bg-dark-bg-tertiary text-dark-text-primary border-transparent">
{caseItem.industry}
</Badge>
</div>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-8 sm:gap-10 pb-12 border-b border-dark-border">
{caseItem.metrics.map((metric: Record<string, any>, j: number) => (
<div key={j}>
<motion.div
className={cn(
'text-5xl sm:text-6xl lg:text-7xl font-black mb-4 leading-none tracking-tightest',
j === 0 ? 'text-brand' : 'text-dark-text-primary'
)}
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
viewport={{ once: true }}
transition={{ duration: 0.4, delay: 0.1 * j }}
>
{metric.value}
</motion.div>
<div className="mb-4">
<div className="h-1 bg-dark-border rounded-full overflow-hidden">
<motion.div
className={cn(
'h-full rounded-full',
j === 0 ? 'bg-brand' : 'bg-dark-text-primary/60'
)}
initial={{ width: 0 }}
whileInView={{ width: `${(metric as any).progress || 50}%` }}
viewport={{ once: true }}
transition={{ duration: 1, delay: 0.3 + 0.1 * j, ease: 'easeOut' }}
/>
</div>
</div>
<div className="flex items-center gap-3">
<div className={cn('w-8 h-px', j === 0 ? 'bg-brand' : 'bg-dark-border')} />
<div className="text-dark-text-secondary font-medium text-sm">{metric.label}</div>
</div>
</div>
))}
</div>
</div>
<div className="px-10 sm:px-12 lg:px-16 pt-12">
<h3 className="text-xl lg:text-2xl font-bold mb-10 leading-tight text-dark-text-primary">
{caseItem.title}
</h3>
</div>
<div className="px-10 sm:px-12 lg:px-16 pb-12">
<div className="space-y-8">
<div>
<div className="flex items-center gap-3 mb-5">
<Award className="w-4 h-4 text-dark-text-muted" />
<span className="text-xs font-semibold text-dark-text-muted tracking-wide">
</span>
</div>
<p className="text-dark-text-secondary leading-relaxed text-sm">
{caseItem.challenge}
</p>
</div>
{(caseItem as any).insight && (
<div className="border-t-2 border-brand bg-brand/[0.04] p-5 pr-6">
<div className="flex items-center gap-3 mb-3">
<span className="text-xs font-bold text-brand tracking-wide">
</span>
</div>
<p className="text-dark-text-primary leading-relaxed text-sm font-medium">
{(caseItem as any).insight}
</p>
</div>
)}
<div>
<div className="flex items-center gap-3 mb-5">
<Zap className="w-4 h-4 text-dark-text-muted" />
<span className="text-xs font-semibold text-dark-text-muted tracking-wide">
</span>
</div>
<p className="text-dark-text-secondary leading-relaxed text-sm">
{caseItem.solution}
</p>
</div>
</div>
</div>
<div className="px-10 sm:px-12 lg:px-16 pb-12">
<div className="p-6 sm:p-8 bg-dark-bg-tertiary/50 border border-dark-border">
<div className="flex items-center gap-3 mb-4">
<CheckCircle2 className="w-4 h-4 text-brand" />
<span className="text-xs font-bold text-brand tracking-wide">
</span>
</div>
<p className="text-dark-text-primary leading-relaxed text-sm font-medium">
{caseItem.result}
</p>
</div>
</div>
<div className="px-10 sm:px-12 lg:px-16 pb-12 lg:pb-16 pt-10 border-t border-dark-border">
<div className="flex items-start gap-4">
<Quote className="w-8 h-8 shrink-0 mt-0.5 text-dark-text-muted" />
<div>
<p className="text-dark-text-secondary italic leading-relaxed mb-4 text-sm">
&ldquo;{caseItem.quote}&rdquo;
</p>
<p className="text-xs text-dark-text-muted">
{caseItem.author}{caseItem.company}
</p>
</div>
</div>
</div>
</div>
</article>
</ScrollReveal>
))}
</div>
</div>
</section>
);
}
function ApproachStrip({ stats }: { stats?: Record<string, any>[] }) {
const hasValidData = stats?.length && stats[0]?.value;
const APPROACH_STRIP = hasValidData ? stats : [];
if (APPROACH_STRIP.length === 0) {
return (
<section className="relative py-20 overflow-hidden bg-dark-bg-secondary">
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 text-center">
<p className="text-dark-text-muted text-lg"></p>
</div>
</section>
);
}
return (
<section
className="relative py-20 sm:py-24 md:py-28 lg:py-32 overflow-hidden bg-dark-bg-secondary"
>
<div className="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-dark-border to-transparent" />
<div className="absolute bottom-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-dark-border to-transparent" />
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10">
<ScrollReveal className="mb-10 sm:mb-12 md:mb-14">
<div className="flex items-center gap-3 mb-6">
<div className="w-10 h-px bg-brand" />
<span className="text-[13px] tracking-[0.15em] font-medium text-brand">
</span>
</div>
<h2 className="font-sans text-xl sm:text-2xl md:text-3xl lg:text-4xl font-black text-dark-text-primary tracking-tight leading-tight">
500+
</h2>
</ScrollReveal>
<div className="grid grid-cols-2 md:grid-cols-4 gap-8 md:gap-0">
{APPROACH_STRIP.map((item, i) => (
<div key={i} className="flex flex-col">
{i > 0 && (
<div className="hidden md:block absolute left-0 top-1/2 -translate-y-1/2 w-px h-16 bg-dark-border" />
)}
<div className="relative">
<motion.div
className="text-3xl sm:text-4xl lg:text-5xl font-black text-dark-text-primary tracking-tightest mb-2"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: 0.1 * i }}
>
{item.value}
</motion.div>
<div className="text-xs text-dark-text-muted tracking-wide mb-4">
{item.label}
</div>
<div className="h-1 bg-dark-border/50 rounded-full overflow-hidden max-w-[160px]">
<motion.div
className="h-full bg-brand rounded-full"
initial={{ width: 0 }}
whileInView={{ width: `${(item as any).progress || 80}%` }}
viewport={{ once: true }}
transition={{ duration: 1.2, delay: 0.2 + 0.1 * i, ease: 'easeOut' }}
/>
</div>
</div>
</div>
))}
</div>
</div>
</section>
);
}
function CTASection({ heroData }: { heroData?: Record<string, any> | null }) {
const ctaTitleLine1 = heroData?.ctaTitleLine1 || '';
const ctaTitleLine2 = heroData?.ctaTitleLine2 || '';
const ctaLabel = heroData?.ctaLabel || '';
const ctaHref = heroData?.ctaHref || '#';
return (
<section
className="relative py-32 sm:py-40 md:py-52 lg:py-60 overflow-hidden bg-dark-bg"
>
<div className="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-dark-border to-transparent" />
<div className="absolute inset-0 pointer-events-none">
<div className="absolute top-1/3 right-1/4 w-[500px] h-[500px] bg-brand/[0.04] rounded-full blur-[140px]" />
</div>
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10">
<ScrollReveal className="max-w-3xl">
<h2 className="font-sans text-2xl sm:text-3xl md:text-4xl lg:text-6xl xl:text-7xl font-black text-dark-text-primary tracking-tightest leading-[0.9] mb-12 sm:mb-16 md:mb-20">
{ctaTitleLine1 && <>{ctaTitleLine1}<br /></>}
<span className="text-brand">{ctaTitleLine2 || '真正转化为业务增长'}</span>
</h2>
<MagneticButton href={ctaHref}>{ctaLabel || '预约咨询'}</MagneticButton>
</ScrollReveal>
</div>
</section>
);
}
export default function HomeContentV13({ services, cases, stats, heroData }: {
services?: Record<string, any>[];
cases?: Record<string, any>[];
stats?: Record<string, any>[];
heroData?: Record<string, any> | null;
}) {
return (
<main>
<HeroSection heroData={heroData} />
<ServicesSection services={services} />
<CasesSection cases={cases} />
<ApproachStrip stats={stats} />
<CTASection heroData={heroData} />
</main>
);
}
+510
View File
@@ -0,0 +1,510 @@
'use client';
import { useRef } from 'react';
import { motion, useScroll, useTransform } from 'framer-motion';
import { StaticLink } from '@/components/ui/static-link';
import { SectionHeader } from '@/components/sections/section-header';
import { ScrollReveal } from '@/components/ui/scroll-reveal';
import { ProductCard } from '@/components/sections/product-card';
import { QuestionCard } from '@/components/sections/question-card';
import { CaseCard } from '@/components/sections/case-card';
import { InsightCard } from '@/components/sections/insight-card';
import { StatsBar } from '@/components/sections/stats-bar';
import { IndustryGrid } from '@/components/sections/industry-grid';
import { CTASection } from '@/components/sections/cta-section';
import { ScrollProgress } from '@/components/ui/scroll-progress';
import { useReducedMotion } from '@/hooks/use-reduced-motion';
import {
ArrowRight,
BarChart3, Cpu, Globe, Factory, ShoppingCart, Stethoscope,
GraduationCap, Building, Shield, Users,
Database, LineChart, Cloud, Brain,
} from 'lucide-react';
// ============================================================
// Design Tokens
// ============================================================
const INK = '#0F1419';
const CREAM = '#F3F0EB';
const EASE = [0.22, 1, 0.36, 1] as const;
const SECTION = 'py-20 md:py-36 lg:py-44';
const CONTAINER = 'max-w-[1280px] mx-auto px-5 sm:px-8 lg:px-16';
// ============================================================
// Data
// ============================================================
const PRODUCTS = [
{ icon: <Database className="w-5 h-5 sm:w-6 sm:h-6 text-[var(--color-brand)]" strokeWidth={1.8} />, title: 'ERP 管理系统', description: '覆盖财务、采购、销售、库存、生产全链路,支撑企业核心运营', href: '/products/erp', badge: '旗舰' },
{ icon: <Users className="w-5 h-5 sm:w-6 sm:h-6 text-[var(--color-brand)]" strokeWidth={1.8} />, title: 'CRM 客户管理', description: '从线索到回款的全生命周期客户关系管理', href: '/products/crm', badge: '旗舰' },
{ icon: <LineChart className="w-5 h-5 sm:w-6 sm:h-6 text-[var(--color-brand)]" strokeWidth={1.8} />, title: 'BI 数据平台', description: '让数据说话,从报表到预测辅助决策', href: '/products/bi' },
{ icon: <Globe className="w-5 h-5 sm:w-6 sm:h-6 text-[var(--color-brand)]" strokeWidth={1.8} />, title: 'CMS 内容平台', description: '建站、运营、分发一体化内容管理', href: '/products/cms' },
{ icon: <Brain className="w-5 h-5 sm:w-6 sm:h-6 text-[var(--color-brand)]" strokeWidth={1.8} />, title: 'AI 智能平台', description: '大模型驱动的企业级 AI 应用与自动化', href: '/products/ai', badge: '新品' },
{ icon: <Cloud className="w-5 h-5 sm:w-6 sm:h-6 text-[var(--color-brand)]" strokeWidth={1.8} />, title: '协同办公', description: '审批、公文、协作、知识管理一站式平台', href: '/products/oa' },
];
const STRATEGIC_QUESTIONS = [
{
question: '我们的数字化投入,是否真正带来了业务增长?',
description: '许多企业在数字化上投入巨大,却难以量化 ROI。我们帮助您建立可衡量的数字化价值评估体系,让每一分投入都可见、可追踪、可优化。',
href: '/services/consulting',
},
{
question: '核心系统如何选型、落地与集成,才能支撑业务持续增长?',
description: 'ERP、BI、CRM 等核心系统的评估、实施与无缝集成,确保技术架构稳健可扩展,避免"上线即落后"的困境。',
href: '/products',
},
{
question: '数据如何从成本中心转变为真正的企业资产?',
description: '数据治理、商业智能分析与信息安全体系,让数据驱动决策而非成为风险。',
href: '/solutions',
},
{
question: '面对 AI 浪潮,我们的企业准备好了吗?',
description: 'AI 不是选择题,而是必答题。我们从 AI 成熟度评估到场景落地,帮助您找到最适合的 AI 应用路径,避免盲目跟风。',
href: '/services/ai',
},
];
const CASE_STUDIES = [
{
industry: '智能制造',
title: '某大型制造企业 ERP 全面升级',
challenge: '原有系统无法支撑业务增长,数据孤岛严重,生产排程效率低下。',
solution: '从选型到全模块上线仅 30 天,打通财务、生产、供应链全链路数据。',
context: '原有系统无法支撑业务增长,数据孤岛严重,生产排程效率低下。从选型到全模块上线仅 30 天。',
impact: [{ value: '40%', label: '生产效率提升' }, { value: '30天', label: '核心系统上线' }],
quote: '睿新团队不仅完成了系统升级,更帮助我们建立了数据驱动的运营体系。',
author: 'CTO,某上市制造企业',
},
{
industry: '贸易零售',
title: '连锁零售全渠道数字化升级',
challenge: '线上线下数据割裂,库存管理混乱,会员体系无法统一。',
solution: '打通全渠道数据,重构供应链与库存管理,统一会员运营体系。',
context: '线上线下数据割裂,库存管理混乱,会员体系无法统一。打通全渠道数据,重构供应链与库存管理。',
impact: [{ value: '25%', label: '营收增长' }, { value: '50%', label: '库存周转优化' }],
quote: '从咨询到落地,睿新团队展现出了超出预期的专业能力和责任心。',
author: 'CEO,某区域零售龙头',
},
{
industry: '医疗健康',
title: '三甲医院数据合规与患者服务',
challenge: '在满足严格合规要求的同时优化患者服务体验,临床数据与运营数据割裂。',
solution: '建立统一数据中台,打通临床与运营数据,构建合规数据治理体系。',
context: '在满足严格合规要求的同时优化患者服务体验,打通临床数据与运营数据。',
impact: [{ value: '100%', label: '合规达标' }, { value: '60%', label: '患者满意度提升' }],
quote: '在医疗数据合规领域,睿新的专业度在行业中非常难得。',
author: '信息科主任,某三甲医院',
},
];
const INSIGHTS_FEATURED = [
{
tag: '白皮书',
title: '2026 中国企业数字化转型趋势报告',
desc: '基于 500+ 企业调研数据,深度解析制造、零售、医疗三大行业数字化成熟度与关键趋势。',
image: 'https://images.unsplash.com/photo-1460925895917-afdab827c52f?w=800&q=80',
},
{
tag: '案例研究',
title: '从数据孤岛到智能决策:一家制造企业的三年数字化之路',
desc: '深度复盘某上市制造企业如何通过分阶段数字化实现 40% 效率提升。',
image: 'https://images.unsplash.com/photo-1504384308090-c894fdcc538d?w=800&q=80',
},
];
const INSIGHTS_LIST = [
{ tag: '观点', title: 'AI 时代,中小企业如何避免"为了数字化而数字化"', desc: '从投入产出比出发,讨论中小企业在 AI 浪潮中的务实策略。', date: '2026-06-15' },
{ tag: '方法论', title: '数字化转型的四个阶段:评估、规划、实施、优化', desc: '我们总结的经过 500+ 项目验证的数字化转型方法论。', date: '2026-06-08' },
{ tag: '行业洞察', title: '制造业数字化转型的五大误区', desc: '避开常见陷阱,让数字化投入真正转化为业务价值。', date: '2026-05-28' },
];
const INDUSTRIES = [
{ icon: Factory, name: '智能制造', count: '120+ 案例' },
{ icon: ShoppingCart, name: '贸易零售', count: '80+ 案例' },
{ icon: Building, name: '金融业', count: '60+ 案例' },
{ icon: Stethoscope, name: '医疗健康', count: '40+ 案例' },
{ icon: Cpu, name: '汽车零部件', count: '45+ 案例' },
{ icon: BarChart3, name: '能源化工', count: '35+ 案例' },
{ icon: Shield, name: '物流供应链', count: '40+ 案例' },
{ icon: GraduationCap, name: '教育', count: '30+ 案例' },
];
const TRUST_STATS = [
{ number: '12', unit: '年', label: '行业深耕', desc: '核心团队平均从业经验 12 年以上' },
{ number: '500', unit: '+', label: '企业客户', desc: '覆盖制造、零售、医疗等多个行业' },
{ number: '98', unit: '%', label: '客户续约率', desc: '年度客户续约率行业领先' },
{ number: '150', unit: '+', label: '专业顾问', desc: '技术+业务复合型团队' },
{ number: '6', unit: '款', label: '自研产品', desc: '覆盖数据、协同、AI 等核心领域' },
{ number: '30', unit: '天', label: '平均交付周期', desc: '敏捷迭代,快速验证业务价值' },
];
// ============================================================
// Section 1: Hero — Deep Ink Background
// ============================================================
function HeroSection() {
const shouldReduceMotion = useReducedMotion();
const heroRef = useRef<HTMLDivElement>(null);
const { scrollYProgress } = useScroll({
target: heroRef,
offset: ['start start', 'end start'],
});
const opacity = useTransform(scrollYProgress, [0, 0.6], [1, 0]);
const scale = useTransform(scrollYProgress, [0, 0.6], [1, 0.95]);
const y = useTransform(scrollYProgress, [0, 0.6], [0, 30]);
const bgOrbY = useTransform(scrollYProgress, [0, 1], [0, 80]);
const gridY = useTransform(scrollYProgress, [0, 1], [0, -40]);
return (
<section
ref={heroRef}
className="relative min-h-[100svh] flex items-center overflow-hidden"
style={{ background: INK }}
>
{/* Atmosphere — subtle gradient orbs (parallax: slowest, moves down) */}
<motion.div
className="absolute inset-0 z-0 pointer-events-none"
style={{ y: shouldReduceMotion ? 0 : bgOrbY }}
>
<div className="absolute w-[600px] md:w-[900px] h-[600px] md:h-[900px] rounded-full blur-[120px] md:blur-[140px] opacity-[0.12]"
style={{
background: 'radial-gradient(circle, rgba(196,30,58,0.4), transparent 70%)',
top: '-30%', right: '-15%',
}}
/>
<div className="absolute w-[400px] md:w-[600px] h-[400px] md:h-[600px] rounded-full blur-[100px] md:blur-[120px] opacity-[0.06]"
style={{
background: 'radial-gradient(circle, rgba(10,14,20,0.3), transparent 70%)',
bottom: '-20%', left: '-10%',
}}
/>
</motion.div>
{/* Grid texture (parallax: medium speed, moves up) */}
<motion.div
className="absolute inset-0 z-[1] pointer-events-none opacity-[0.025]"
style={{
y: shouldReduceMotion ? 0 : gridY,
backgroundImage: `linear-gradient(rgba(255,255,255,0.5) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,0.5) 1px, transparent 1px)`,
backgroundSize: '60px 60px',
maskImage: 'radial-gradient(ellipse 70% 60% at 50% 40%, black 30%, transparent 70%)',
}}
/>
{/* Content */}
<motion.div
className="relative z-10 w-full max-w-[1280px] mx-auto px-5 sm:px-8 lg:px-16 py-32 md:py-40 lg:py-48"
style={{ opacity, scale, y }}
>
<div className="max-w-[720px]">
{/* Eyebrow */}
<motion.div
className="text-[10px] sm:text-[11px] tracking-[4px] sm:tracking-[5px] uppercase font-medium mb-6 md:mb-8"
style={{ color: 'var(--color-brand)' }}
initial={shouldReduceMotion ? {} : { opacity: 0, y: 16 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.1, ease: EASE }}
>
</motion.div>
{/* Title */}
<motion.h1
className="font-sans text-[clamp(36px,10vw,88px)] font-black leading-[1.04] tracking-[-1.5px] sm:tracking-[-2.5px] text-white mb-6 md:mb-8"
initial={shouldReduceMotion ? {} : { opacity: 0, y: 24 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.7, delay: 0.2, ease: EASE }}
>
<br />
<span style={{ color: 'var(--color-brand)' }}> 3 </span>
</motion.h1>
{/* Subtitle — answer-first: quantified results */}
<motion.div
className="flex flex-wrap gap-x-6 gap-y-2 mb-8 md:mb-10"
initial={shouldReduceMotion ? {} : { opacity: 0, y: 16 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.35, ease: EASE }}
>
<span className="text-[14px] sm:text-base font-medium" style={{ color: 'rgba(255,255,255,0.6)' }}>
<span style={{ color: 'var(--color-brand)', fontWeight: 700 }}>500+</span>
</span>
<span className="text-[14px] sm:text-base font-medium" style={{ color: 'rgba(255,255,255,0.6)' }}>
<span style={{ color: 'var(--color-brand)', fontWeight: 700 }}>2.8x</span> ROI
</span>
<span className="text-[14px] sm:text-base font-medium" style={{ color: 'rgba(255,255,255,0.6)' }}>
<span style={{ color: 'var(--color-brand)', fontWeight: 700 }}>40%</span>
</span>
</motion.div>
{/* Strategic question */}
<motion.p
className="font-sans text-[14px] sm:text-base italic mb-10 md:mb-12"
style={{ color: 'rgba(255,255,255,0.4)' }}
initial={shouldReduceMotion ? {} : { opacity: 0, y: 16 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.45, ease: EASE }}
>
</motion.p>
{/* CTAs */}
<motion.div
className="flex flex-col sm:flex-row gap-3 sm:gap-5 items-start sm:items-center mb-12 md:mb-20"
initial={shouldReduceMotion ? {} : { opacity: 0, y: 16 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.55, ease: EASE }}
>
<StaticLink
href="/contact"
className="min-h-[44px] min-w-[44px] active:scale-[0.98] transition-transform duration-150 inline-flex items-center justify-center gap-2.5 px-6 sm:px-8 py-3.5 text-sm font-medium text-white rounded-sm transition-all duration-300 hover:-translate-y-0.5"
style={{ background: 'var(--color-brand)' }}
>
<ArrowRight className="w-4 h-4" />
</StaticLink>
<StaticLink
href="/products"
className="min-h-[44px] inline-flex items-center gap-2 text-sm font-normal transition-all duration-300 border-b hover:border-white/25"
style={{ color: 'rgba(255,255,255,0.45)', borderColor: 'rgba(255,255,255,0.1)', padding: '12px 0' }}
>
</StaticLink>
</motion.div>
{/* Metrics row */}
<motion.div
className="flex gap-8 md:gap-16 flex-wrap"
initial={shouldReduceMotion ? {} : { opacity: 0, y: 16 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.7, ease: EASE }}
>
{[
{ value: '12', unit: '', label: '年行业深耕' },
{ value: '500', unit: '+', label: '企业客户信任' },
{ value: '98', unit: '%', label: '客户年度续约率' },
].map(item => (
<div key={item.label} className="flex flex-col">
<div className="font-sans tabular-nums text-[28px] sm:text-[32px] md:text-[36px] font-bold text-white leading-none mb-1 tracking-[-1px]">
{item.value}
<span style={{ color: 'var(--color-brand)', fontSize: '0.5em' }}>{item.unit}</span>
</div>
<div className="text-[10px] sm:text-[11px] tracking-[1px] text-white/25">
{item.label}
</div>
</div>
))}
</motion.div>
</div>
</motion.div>
{/* Scroll indicator — desktop only */}
<div className="absolute bottom-8 md:bottom-10 left-1/2 -translate-x-1/2 z-10 hidden md:flex flex-col items-center gap-2.5">
<div className="w-px h-10 relative overflow-hidden bg-white/[0.06]">
<motion.div
className="absolute top-0 left-0 w-full h-[20%] bg-white/20"
animate={{ y: [0, 30, 0] }}
transition={{ duration: 2.5, repeat: Infinity, ease: 'easeInOut' }}
/>
</div>
<span className="text-[9px] tracking-[4px] text-white/10">SCROLL</span>
</div>
</section>
);
}
// ============================================================
// Section 2: Product Matrix — Paper White Background
// ============================================================
function ProductMatrixSection() {
return (
<section className={SECTION} style={{ background: 'var(--color-bg-primary)' }}>
<div className={CONTAINER}>
<SectionHeader
label="Products"
title="覆盖数字化"
highlight="全生命周期"
desc="6 款自研产品,支撑企业核心业务场景"
/>
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-6">
{PRODUCTS.map((product, i) => (
<ScrollReveal key={i} delay={i * 0.06}>
<ProductCard {...product} />
</ScrollReveal>
))}
</div>
</div>
</section>
);
}
// ============================================================
// Section 3: Strategic Questions — Cream Background [Porsche-style]
// ============================================================
function StrategicQuestionsSection() {
return (
<section className={SECTION} style={{ background: CREAM }}>
<div className={CONTAINER}>
<SectionHeader
label="Strategic Questions"
title="先问对问题"
highlight="再做对事"
desc="在启动任何数字化项目之前,先找到正确的方向"
/>
<div>
{STRATEGIC_QUESTIONS.map((item, i) => (
<ScrollReveal key={i} delay={i * 0.08}>
<QuestionCard
question={item.question}
description={item.description}
href={item.href}
/>
</ScrollReveal>
))}
</div>
</div>
</section>
);
}
// ============================================================
// Section 4: Client Results — Deep Ink Background [Bain-style]
// ============================================================
function CaseStudiesSection() {
return (
<section className={SECTION} style={{ background: INK }}>
<div className={CONTAINER}>
<SectionHeader
label="Client Results"
title="500+ 企业的"
highlight="增长实践"
desc="不以项目上线为终点,以客户业务改善为衡量标准"
light
/>
<div className="grid lg:grid-cols-3 gap-px rounded-sm overflow-hidden"
style={{ background: 'rgba(255,255,255,0.03)' }}>
{CASE_STUDIES.map((cs, i) => (
<ScrollReveal key={i} delay={i * 0.1}>
<CaseCard {...cs} />
</ScrollReveal>
))}
</div>
<ScrollReveal delay={0.3}>
<div className="text-center mt-8 md:mt-12">
<StaticLink
href="/solutions"
className="min-h-[44px] inline-flex items-center gap-2 text-[13px] font-medium transition-colors duration-300 text-white/50 hover:text-white group"
>
<ArrowRight className="w-3.5 h-3.5 transition-transform duration-300 group-hover:translate-x-1" />
</StaticLink>
</div>
</ScrollReveal>
</div>
</section>
);
}
// ============================================================
// Section 5: Insights — Paper White Background [Accenture-style]
// ============================================================
function InsightsSection() {
return (
<section className={SECTION} style={{ background: 'var(--color-bg-primary)' }}>
<div className={CONTAINER}>
<SectionHeader
label="Insights"
title="把握数字化"
highlight="前沿趋势"
desc="基于 500+ 项目实践的研究成果与行业观点"
/>
{/* Featured insights — 2 large cards */}
<div className="grid md:grid-cols-2 gap-4 sm:gap-6 mb-6 md:mb-8">
{INSIGHTS_FEATURED.map((insight, i) => (
<ScrollReveal key={i} delay={i * 0.1}>
<InsightCard featured {...insight} />
</ScrollReveal>
))}
</div>
{/* Secondary insights — 3 small cards */}
<div className="grid sm:grid-cols-2 md:grid-cols-3 gap-px rounded-sm overflow-hidden"
style={{ background: 'var(--color-border-primary)' }}>
{INSIGHTS_LIST.map((insight, i) => (
<ScrollReveal key={i} delay={i * 0.06}>
<InsightCard {...insight} />
</ScrollReveal>
))}
</div>
</div>
</section>
);
}
// ============================================================
// Section 6: Industry Selector — Paper White Background
// ============================================================
function IndustrySelectorSection() {
return (
<section className={SECTION} style={{ background: CREAM }}>
<div className={CONTAINER}>
<SectionHeader
label="Industries"
title="8 大行业"
highlight="深度实践"
desc="深入行业场景,理解业务逻辑,提供真正落地的解决方案"
/>
<ScrollReveal delay={0.1}>
<p className="font-sans text-[18px] sm:text-[20px] md:text-[22px] font-semibold mb-8 md:mb-10 leading-relaxed text-[var(--color-text-primary)]">
<span style={{ color: 'var(--color-brand)' }}></span>
</p>
</ScrollReveal>
<ScrollReveal delay={0.15}>
<IndustryGrid items={INDUSTRIES} />
</ScrollReveal>
</div>
</section>
);
}
// ============================================================
// Section 7: Trust Proof — Paper White Background
// ============================================================
function TrustSection() {
return (
<section className={SECTION} style={{ background: 'var(--color-bg-primary)' }}>
<div className={CONTAINER}>
<SectionHeader
label="Trust"
title="98% 客户"
highlight="续约率"
desc="数字会说话,信任是合作的基础"
/>
<StatsBar items={TRUST_STATS} />
</div>
</section>
);
}
// ============================================================
// Main Export
// ============================================================
export default function HomeContent() {
return (
<main className="min-h-screen">
<ScrollProgress />
<HeroSection />
<ProductMatrixSection />
<StrategicQuestionsSection />
<CaseStudiesSection />
<InsightsSection />
<IndustrySelectorSection />
<TrustSection />
<CTASection />
</main>
);
}
@@ -0,0 +1,183 @@
'use client';
import { useState, useMemo, useEffect } from 'react';
import { initCms, getItemRenderer } from '@/lib/cms';
import type { ContentItem } from '@/lib/cms';
import { NEWS } from '@/lib/constants';
const categories = ['全部', '公司新闻', '研发动态'];
const ITEMS_PER_PAGE = 9;
function newsToContentItem(news: unknown, index: number): ContentItem {
const n = news as Record<string, unknown>;
return {
id: String(n.id),
modelId: 'news',
modelCode: 'news',
title: String(n.title),
slug: String(n.id),
status: 'published',
version: 1,
sortOrder: index,
data: n,
createdAt: '2024-01-01T00:00:00Z',
updatedAt: '2024-01-01T00:00:00Z',
publishedAt: '2024-01-01T00:00:00Z',
createdBy: 'system',
updatedBy: 'system',
};
}
export default function NewsContentCms() {
const [ready, setReady] = useState(false);
const [items, setItems] = useState<ContentItem[]>([]);
const [selectedCategory, setSelectedCategory] = useState('全部');
const [searchQuery, setSearchQuery] = useState('');
const [currentPage, setCurrentPage] = useState(1);
useEffect(() => {
initCms();
setItems(NEWS.map(newsToContentItem));
setReady(true);
}, []);
const filteredItems = useMemo(() => {
return items.filter((item) => {
const data = item.data as Record<string, unknown>;
const category = (data.category as string) || '';
const matchesCategory = selectedCategory === '全部' || category === selectedCategory;
const matchesSearch =
!searchQuery ||
item.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
((data.excerpt as string) || '').toLowerCase().includes(searchQuery.toLowerCase());
return matchesCategory && matchesSearch;
});
}, [items, selectedCategory, searchQuery]);
const totalPages = Math.ceil(filteredItems.length / ITEMS_PER_PAGE);
const paginatedItems = filteredItems.slice(
(currentPage - 1) * ITEMS_PER_PAGE,
currentPage * ITEMS_PER_PAGE
);
useEffect(() => {
setCurrentPage(1);
}, [selectedCategory, searchQuery]);
if (!ready) {
return (
<div className="min-h-screen bg-ink flex items-center justify-center">
<div className="text-white/50">...</div>
</div>
);
}
const NewsCardRenderer = getItemRenderer('news');
return (
<div className="min-h-screen bg-ink text-white overflow-x-hidden" data-theme="dark">
<section className="relative min-h-[50vh] flex items-center overflow-hidden bg-ink pt-24">
<div className="absolute inset-0">
<div className="absolute top-1/4 -left-32 w-96 h-96 rounded-full bg-brand/20 blur-[120px]" />
</div>
<div className="relative z-10 w-full max-w-7xl mx-auto px-6 lg:px-8 py-20">
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-white/[0.05] border border-white/[0.1] text-sm text-white/70 mb-6">
<span className="w-1.5 h-1.5 rounded-full bg-brand animate-pulse" />
</div>
<h1 className="text-4xl sm:text-5xl md:text-6xl font-bold text-white tracking-tight leading-[1.1]">
</h1>
<p className="mt-6 text-lg text-white/60 max-w-2xl leading-relaxed">
</p>
</div>
</section>
<section className="relative py-16 sm:py-20 md:py-28 overflow-hidden bg-ink-light">
<div className="max-w-7xl mx-auto px-6 lg:px-8">
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-6 mb-12">
<div className="flex flex-wrap gap-2">
{categories.map((cat) => (
<button
key={cat}
onClick={() => setSelectedCategory(cat)}
className={`px-5 py-2 text-sm font-medium rounded-xl transition-all ${
selectedCategory === cat
? 'bg-brand text-white'
: 'bg-white/[0.05] text-white/60 hover:bg-white/[0.1] hover:text-white border border-white/[0.08]'
}`}
>
{cat}
</button>
))}
</div>
<div className="relative w-full md:w-80">
<input
type="text"
placeholder="搜索新闻..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full pl-10 pr-4 py-2.5 bg-white/[0.05] border border-white/[0.1] rounded-xl text-white text-sm placeholder-white/30 focus:outline-none focus:border-brand/50 focus:ring-1 focus:ring-brand/30 transition-colors"
/>
<svg className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-white/40" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</div>
</div>
{paginatedItems.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 md:gap-8">
{paginatedItems.map((item, i) =>
NewsCardRenderer ? (
<NewsCardRenderer key={item.id} item={item} index={i} variant="featured" />
) : (
<div key={item.id} className="p-4 border border-white/10 rounded-lg">
{item.title}
</div>
)
)}
</div>
) : (
<div className="py-20 text-center">
<div className="text-5xl mb-4 opacity-30">📭</div>
<div className="text-white/50"></div>
</div>
)}
{totalPages > 1 && (
<div className="mt-16 flex items-center justify-center gap-2">
<button
onClick={() => setCurrentPage((p) => Math.max(1, p - 1))}
disabled={currentPage === 1}
className="p-2 rounded-lg bg-white/[0.05] text-white/60 hover:bg-white/[0.1] disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
>
</button>
{Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => (
<button
key={page}
onClick={() => setCurrentPage(page)}
className={`w-10 h-10 rounded-lg text-sm font-medium transition-colors ${
currentPage === page
? 'bg-brand text-white'
: 'bg-white/[0.05] text-white/60 hover:bg-white/[0.1]'
}`}
>
{page}
</button>
))}
<button
onClick={() => setCurrentPage((p) => Math.min(totalPages, p + 1))}
disabled={currentPage === totalPages}
className="p-2 rounded-lg bg-white/[0.05] text-white/60 hover:bg-white/[0.1] disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
>
</button>
</div>
)}
</div>
</section>
</div>
);
}
@@ -0,0 +1,297 @@
'use client';
import { useState, useMemo, ChangeEvent } from 'react';
import { motion } from 'framer-motion';
import { ScrollReveal, StaggerReveal } from '@/components/ui/scroll-reveal';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Input } from '@/components/ui/input';
import { Search, Calendar, ArrowRight, Newspaper, ChevronLeft, ChevronRight } from 'lucide-react';
import { NEWS, COMPANY_INFO } from '@/lib/constants';
import { cn } from '@/lib/utils';
const EASE_OUT = [0.22, 1, 0.36, 1] as const;
const categories = ['全部', '公司新闻', '研发动态'];
const ITEMS_PER_PAGE = 9;
function GrainOverlay() {
return (
<div
className="absolute inset-0 pointer-events-none opacity-[0.03] mix-blend-overlay"
style={{
backgroundImage:
"url(\"data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E\")",
}}
/>
);
}
function SectionLabel({ children, className = '' }: { children: React.ReactNode; className?: string }) {
return (
<div className={cn('flex items-center gap-5 mb-7', className)}>
<div className="w-14 h-px bg-brand" />
<span className="text-[11px] tracking-[0.4em] uppercase font-bold text-brand">
{children}
</span>
<div className="w-14 h-px bg-brand" />
</div>
);
}
function NewsCard({ newsItem, index }: { newsItem: typeof NEWS[0]; index: number }) {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: index * 0.08, ease: EASE_OUT }}
>
<a
href={`/news/${newsItem.id}`}
className="group relative block border border-white/10 hover:border-brand/40 bg-ink transition-all duration-600 hover:-translate-y-2 overflow-hidden h-full"
>
<div className="absolute top-0 left-0 right-0 h-1 bg-brand scale-x-0 group-hover:scale-x-100 transition-transform duration-700 origin-left" />
{newsItem.image ? (
<div className="aspect-video bg-ink-light overflow-hidden">
<img
src={newsItem.image}
alt={newsItem.title}
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-700"
/>
</div>
) : (
<div className="aspect-video bg-gradient-to-br from-brand/10 to-ink-light flex items-center justify-center">
<Newspaper className="w-12 h-12 text-brand/30" />
</div>
)}
<div className="p-6 lg:p-8">
<div className="flex items-center gap-3 mb-4">
<Badge variant="outline" className="text-xs border-brand/30 text-brand">
{newsItem.category}
</Badge>
<div className="flex items-center gap-1.5 text-xs text-white/50">
<Calendar className="w-3.5 h-3.5" />
{newsItem.date}
</div>
</div>
<h3 className="text-lg font-semibold text-white mb-3 line-clamp-2 group-hover:text-brand transition-colors duration-300 leading-snug">
{newsItem.title}
</h3>
<p className="text-sm text-white/50 line-clamp-3 mb-5 leading-relaxed">
{newsItem.excerpt}
</p>
<div className="flex items-center text-brand text-sm font-medium">
<ArrowRight className="ml-2 w-4 h-4 group-hover:translate-x-1 transition-transform duration-300" />
</div>
</div>
</a>
</motion.div>
);
}
export default function NewsContentV1() {
const [selectedCategory, setSelectedCategory] = useState('全部');
const [searchQuery, setSearchQuery] = useState('');
const [currentPage, setCurrentPage] = useState(1);
const filteredNews = useMemo(() => {
return NEWS.filter((newsItem) => {
const matchesCategory = selectedCategory === '全部' || newsItem.category === selectedCategory;
const matchesSearch =
newsItem.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
newsItem.excerpt.toLowerCase().includes(searchQuery.toLowerCase());
return matchesCategory && matchesSearch;
});
}, [selectedCategory, searchQuery]);
const totalPages = Math.ceil(filteredNews.length / ITEMS_PER_PAGE);
const paginatedNews = useMemo(() => {
const startIndex = (currentPage - 1) * ITEMS_PER_PAGE;
return filteredNews.slice(startIndex, startIndex + ITEMS_PER_PAGE);
}, [filteredNews, currentPage]);
const handlePageChange = (page: number) => {
setCurrentPage(page);
window.scrollTo({ top: 0, behavior: 'smooth' });
};
const handleCategoryChange = (category: string) => {
setSelectedCategory(category);
setCurrentPage(1);
};
const handleSearchChange = (e: ChangeEvent<HTMLInputElement>) => {
setSearchQuery(e.target.value);
setCurrentPage(1);
};
return (
<div className="min-h-screen bg-ink text-white overflow-x-hidden">
{/* Hero Section */}
<section className="relative pt-32 pb-24 lg:pt-40 lg:pb-32 overflow-hidden">
<GrainOverlay />
<div className="absolute inset-0 bg-gradient-to-b from-ink-light/50 to-ink pointer-events-none" />
<div className="max-w-container mx-auto px-6 lg:px-10 relative z-10">
<ScrollReveal>
<SectionLabel>News & Insights</SectionLabel>
</ScrollReveal>
<ScrollReveal delay={0.1}>
<h1 className="mt-8 text-4xl md:text-5xl lg:text-6xl xl:text-7xl font-bold tracking-tight leading-[1.1]">
<span className="block mt-2 text-brand"></span>
</h1>
</ScrollReveal>
<ScrollReveal delay={0.2}>
<p className="mt-8 text-lg md:text-xl text-white/60 max-w-2xl leading-relaxed">
{COMPANY_INFO.displayName}
</p>
</ScrollReveal>
</div>
</section>
{/* Filter + News List Section */}
<section className="relative py-24 lg:py-32 overflow-hidden bg-ink-light">
<GrainOverlay />
<div className="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-white/12 to-transparent" />
<div className="max-w-container mx-auto px-6 lg:px-10 relative z-10">
{/* Filters */}
<ScrollReveal className="mb-16 space-y-6">
<div className="flex flex-wrap gap-3">
{categories.map((category) => (
<Button
key={category}
variant={selectedCategory === category ? 'primary' : 'outline'}
onClick={() => handleCategoryChange(category)}
>
{category}
</Button>
))}
</div>
<div className="relative max-w-md">
<Search className="absolute left-4 top-1/2 transform -translate-y-1/2 text-white/40 w-5 h-5 pointer-events-none" />
<Input
type="text"
placeholder="搜索新闻..."
value={searchQuery}
onChange={handleSearchChange}
className="pl-12 h-12 bg-ink border-white/10 text-white placeholder:text-white/30 focus:border-brand/50"
/>
</div>
</ScrollReveal>
{/* News Grid */}
{paginatedNews.length === 0 ? (
<ScrollReveal>
<div className="text-center py-24">
<Newspaper className="w-16 h-16 text-white/20 mx-auto mb-6" />
<p className="text-xl text-white/50"></p>
<p className="mt-2 text-white/30"></p>
</div>
</ScrollReveal>
) : (
<>
<StaggerReveal
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 md:gap-8"
delayChildren={0.05}
>
{paginatedNews.map((newsItem, index) => (
<NewsCard key={newsItem.id} newsItem={newsItem} index={index} />
))}
</StaggerReveal>
{/* Pagination */}
{totalPages > 1 && (
<ScrollReveal>
<div className="flex justify-center items-center gap-2 mt-16">
<Button
variant="outline"
size="icon"
onClick={() => handlePageChange(currentPage - 1)}
disabled={currentPage === 1}
>
<ChevronLeft className="w-4 h-4" />
</Button>
{Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => (
<Button
key={page}
variant={currentPage === page ? 'primary' : 'outline'}
size="icon"
onClick={() => handlePageChange(page)}
>
{page}
</Button>
))}
<Button
variant="outline"
size="icon"
onClick={() => handlePageChange(currentPage + 1)}
disabled={currentPage === totalPages}
>
<ChevronRight className="w-4 h-4" />
</Button>
</div>
</ScrollReveal>
)}
</>
)}
</div>
</section>
{/* CTA Section */}
<section className="relative py-36 lg:py-44 overflow-hidden bg-ink">
<GrainOverlay />
<div className="absolute inset-0 pointer-events-none">
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[600px] h-[600px] rounded-full opacity-[0.08] blur-3xl bg-brand" />
</div>
<div className="max-w-container mx-auto px-6 lg:px-10 relative z-10">
<ScrollReveal className="max-w-3xl mx-auto text-center">
<SectionLabel className="justify-center">
<div className="flex items-center gap-5">
<div className="w-14 h-px bg-brand" />
<span className="text-[11px] tracking-[0.4em] uppercase font-bold text-brand">
Get in Touch
</span>
<div className="w-14 h-px bg-brand" />
</div>
</SectionLabel>
<h2 className="text-3xl md:text-4xl lg:text-6xl font-bold text-white tracking-tight leading-tight mb-8">
</h2>
<p className="text-lg md:text-xl text-white/60 leading-relaxed mb-12">
</p>
<div className="flex flex-wrap gap-6 justify-center">
<Button size="xl" asChild>
<a href="/contact">
<ArrowRight className="w-5 h-5 ml-2" />
</a>
</Button>
<Button size="xl" variant="outline" asChild>
<a href="/about"></a>
</Button>
</div>
</ScrollReveal>
</div>
</section>
</div>
);
}
@@ -0,0 +1,268 @@
'use client';
import { useState, useMemo, ChangeEvent } from 'react';
import { motion } from 'framer-motion';
import { ScrollReveal, StaggerReveal } from '@/components/ui/scroll-reveal';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Input } from '@/components/ui/input';
import { Search, Calendar, ArrowRight, Newspaper, ChevronLeft, ChevronRight } from 'lucide-react';
import { COMPANY_INFO } from '@/lib/constants';
import { type NewsItem } from '@/lib/constants/news';
import { getMockItems } from '@/lib/cms/mock-data';
// CMS 数据源:优先使用 prop,回退到 mock-data
function getNewsData(newsProp?: NewsItem[]): NewsItem[] {
if (newsProp?.length) return newsProp;
return getMockItems('news').map(
(item) => ({ ...item.data, title: item.data.title || item.title } as unknown as NewsItem),
);
}
import { SectionLabel, EASE_OUT } from '@/components/ui/page-decoration';
const categories = ['全部', '公司新闻', '研发动态'];
const ITEMS_PER_PAGE = 9;
function NewsCard({ newsItem, index }: { newsItem: NewsItem; index: number }) {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-80px' }}
transition={{ duration: 0.5, delay: index * 0.08, ease: EASE_OUT }}
>
<a
href={`/news/${newsItem.id}`}
className="group relative block border border-border-primary hover:border-brand/30 bg-white transition-all duration-500 hover:-translate-y-2 overflow-hidden h-full"
>
<div className="absolute top-0 left-0 bottom-0 w-1 bg-brand scale-y-0 group-hover:scale-y-100 transition-transform duration-500 origin-top" />
{newsItem.image ? (
<div className="aspect-video bg-bg-secondary overflow-hidden">
<img
src={newsItem.image}
alt={newsItem.title}
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-700"
/>
</div>
) : (
<div className="aspect-video bg-gradient-to-br from-brand/[0.08] to-bg-secondary flex items-center justify-center">
<Newspaper className="w-12 h-12 text-brand/30" />
</div>
)}
<div className="p-6 sm:p-7 lg:p-8">
<div className="flex items-center gap-3 mb-4">
<Badge variant="outline" className="text-xs border-brand/30 text-brand">
{newsItem.category}
</Badge>
<div className="flex items-center gap-1.5 text-xs text-text-muted">
<Calendar className="w-3.5 h-3.5" />
{newsItem.date}
</div>
</div>
<h3 className="text-lg sm:text-xl font-semibold text-ink mb-3 line-clamp-2 group-hover:text-brand transition-colors duration-300 leading-snug">
{newsItem.title}
</h3>
<p className="text-sm text-text-secondary line-clamp-3 mb-5 leading-relaxed">
{newsItem.excerpt}
</p>
<div className="flex items-center text-brand text-sm font-medium">
<ArrowRight className="ml-2 w-4 h-4 group-hover:translate-x-1 transition-transform duration-300" />
</div>
</div>
</a>
</motion.div>
);
}
export default function NewsContentV2({ news }: { news?: NewsItem[] }) {
const NEWS = getNewsData(news);
const [selectedCategory, setSelectedCategory] = useState('全部');
const [searchQuery, setSearchQuery] = useState('');
const [currentPage, setCurrentPage] = useState(1);
const filteredNews = useMemo(() => {
return NEWS.filter((newsItem) => {
const matchesCategory = selectedCategory === '全部' || newsItem.category === selectedCategory;
const matchesSearch =
newsItem.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
newsItem.excerpt.toLowerCase().includes(searchQuery.toLowerCase());
return matchesCategory && matchesSearch;
});
}, [selectedCategory, searchQuery]);
const totalPages = Math.ceil(filteredNews.length / ITEMS_PER_PAGE);
const paginatedNews = useMemo(() => {
const startIndex = (currentPage - 1) * ITEMS_PER_PAGE;
return filteredNews.slice(startIndex, startIndex + ITEMS_PER_PAGE);
}, [filteredNews, currentPage]);
const handlePageChange = (page: number) => {
setCurrentPage(page);
window.scrollTo({ top: 0, behavior: 'smooth' });
};
const handleCategoryChange = (category: string) => {
setSelectedCategory(category);
setCurrentPage(1);
};
const handleSearchChange = (e: ChangeEvent<HTMLInputElement>) => {
setSearchQuery(e.target.value);
setCurrentPage(1);
};
return (
<div className="min-h-screen bg-white text-ink overflow-x-hidden">
{/* Hero Section */}
<section className="relative min-h-[75vh] flex items-center overflow-hidden bg-white">
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10 w-full py-28 sm:py-36 md:py-44 lg:py-56">
<div className="max-w-4xl">
<ScrollReveal>
<SectionLabel>News &amp; Insights</SectionLabel>
</ScrollReveal>
<ScrollReveal delay={0.1}>
<h1 className="text-4xl sm:text-5xl md:text-6xl lg:text-7xl xl:text-8xl font-bold leading-[0.92] tracking-tighter mb-10 sm:mb-12 md:mb-16">
<span className="block mt-4 text-brand"></span>
</h1>
</ScrollReveal>
<ScrollReveal delay={0.2}>
<p className="text-lg md:text-xl text-text-secondary max-w-2xl leading-relaxed">
{COMPANY_INFO.displayName}
</p>
</ScrollReveal>
</div>
</div>
</section>
{/* Filter + News List Section */}
<section className="relative py-20 sm:py-28 md:py-36 lg:py-44 overflow-hidden bg-bg-secondary">
<div className="absolute top-0 left-0 right-0 h-px bg-border-primary" />
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10">
{/* Filters */}
<ScrollReveal className="mb-12 sm:mb-16 space-y-6">
<div className="flex flex-wrap gap-3">
{categories.map((category) => (
<Button
key={category}
variant={selectedCategory === category ? 'primary' : 'outline'}
onClick={() => handleCategoryChange(category)}
>
{category}
</Button>
))}
</div>
<div className="relative max-w-md">
<Search className="absolute left-4 top-1/2 transform -translate-y-1/2 text-text-muted w-5 h-5 pointer-events-none" />
<Input
type="text"
placeholder="搜索新闻..."
value={searchQuery}
onChange={handleSearchChange}
className="pl-12 h-12 bg-white border-border-primary text-ink placeholder:text-text-muted focus:border-brand/50"
/>
</div>
</ScrollReveal>
{/* News Grid */}
{paginatedNews.length === 0 ? (
<ScrollReveal>
<div className="text-center py-24">
<Newspaper className="w-16 h-16 text-text-muted/20 mx-auto mb-6" />
<p className="text-xl text-text-secondary"></p>
<p className="mt-2 text-text-muted"></p>
</div>
</ScrollReveal>
) : (
<>
<StaggerReveal
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 sm:gap-8"
delayChildren={0.05}
>
{paginatedNews.map((newsItem, index) => (
<NewsCard key={newsItem.id} newsItem={newsItem} index={index} />
))}
</StaggerReveal>
{/* Pagination */}
{totalPages > 1 && (
<ScrollReveal>
<div className="flex justify-center items-center gap-2 mt-16">
<Button
variant="outline"
size="icon"
onClick={() => handlePageChange(currentPage - 1)}
disabled={currentPage === 1}
>
<ChevronLeft className="w-4 h-4" />
</Button>
{Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => (
<Button
key={page}
variant={currentPage === page ? 'primary' : 'outline'}
size="icon"
onClick={() => handlePageChange(page)}
>
{page}
</Button>
))}
<Button
variant="outline"
size="icon"
onClick={() => handlePageChange(currentPage + 1)}
disabled={currentPage === totalPages}
>
<ChevronRight className="w-4 h-4" />
</Button>
</div>
</ScrollReveal>
)}
</>
)}
</div>
</section>
{/* CTA Section */}
<section className="relative py-20 sm:py-28 md:py-36 lg:py-44 overflow-hidden bg-white">
<div className="absolute top-0 left-0 right-0 h-px bg-border-primary" />
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10">
<ScrollReveal className="text-center max-w-3xl mx-auto">
<SectionLabel className="justify-center">Get in Touch</SectionLabel>
<h2 className="text-3xl sm:text-4xl md:text-5xl font-bold tracking-tight leading-tight">
</h2>
<p className="mt-6 text-lg text-text-secondary leading-relaxed">
</p>
<div className="mt-12 flex flex-col sm:flex-row justify-center gap-4 sm:gap-6">
<a
href="/contact"
className="group inline-flex items-center justify-center gap-3 px-12 py-6 font-semibold text-base bg-brand text-white transition-all duration-300 hover:bg-brand/90"
>
<ArrowRight className="w-4 h-4 transition-transform duration-300 group-hover:translate-x-1" />
</a>
<a
href="/about"
className="inline-flex items-center justify-center gap-3 px-12 py-6 font-semibold text-base text-ink border border-border-primary hover:border-border-secondary hover:bg-bg-secondary transition-all duration-300"
>
</a>
</div>
</ScrollReveal>
</div>
</section>
</div>
);
}
@@ -0,0 +1,259 @@
'use client';
import { useState, useMemo, ChangeEvent } from 'react';
import { motion } from 'framer-motion';
import { ScrollReveal, StaggerReveal } from '@/components/ui/scroll-reveal';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Input } from '@/components/ui/input';
import { Search, Calendar, ArrowRight, Newspaper, ChevronLeft, ChevronRight } from 'lucide-react';
import { COMPANY_INFO } from '@/lib/constants';
import { type NewsItem } from '@/lib/constants/news';
import { SectionLabel, EASE_OUT } from '@/components/ui/page-decoration';
const categories = ['全部', '公司新闻', '研发动态'];
const ITEMS_PER_PAGE = 9;
function NewsCard({ newsItem, index }: { newsItem: NewsItem; index: number }) {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-80px' }}
transition={{ duration: 0.5, delay: index * 0.08, ease: EASE_OUT }}
>
<a
href={`/news/${newsItem.id}`}
className="group relative block border border-border-primary hover:border-brand/30 bg-white transition-all duration-500 hover:-translate-y-2 overflow-hidden h-full"
>
<div className="absolute top-0 left-0 bottom-0 w-1 bg-brand scale-y-0 group-hover:scale-y-100 transition-transform duration-500 origin-top" />
{newsItem.image ? (
<div className="aspect-video bg-bg-secondary overflow-hidden">
<img
src={newsItem.image}
alt={newsItem.title}
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-700"
/>
</div>
) : (
<div className="aspect-video bg-gradient-to-br from-brand/[0.08] to-bg-secondary flex items-center justify-center">
<Newspaper className="w-12 h-12 text-brand/30" />
</div>
)}
<div className="p-6 sm:p-7 lg:p-8">
<div className="flex items-center gap-3 mb-4">
<Badge variant="outline" className="text-xs border-brand/30 text-brand">
{newsItem.category}
</Badge>
<div className="flex items-center gap-1.5 text-xs text-text-muted">
<Calendar className="w-3.5 h-3.5" />
{newsItem.date}
</div>
</div>
<h3 className="text-lg sm:text-xl font-semibold text-ink mb-3 line-clamp-2 group-hover:text-brand transition-colors duration-300 leading-snug">
{newsItem.title}
</h3>
<p className="text-sm text-text-secondary line-clamp-3 mb-5 leading-relaxed">
{newsItem.excerpt}
</p>
<div className="flex items-center text-brand text-sm font-medium">
<ArrowRight className="ml-2 w-4 h-4 group-hover:translate-x-1 transition-transform duration-300" />
</div>
</div>
</a>
</motion.div>
);
}
export default function NewsContentV3({ news }: { news?: NewsItem[] }) {
const NEWS = news ?? [];
const [selectedCategory, setSelectedCategory] = useState('全部');
const [searchQuery, setSearchQuery] = useState('');
const [currentPage, setCurrentPage] = useState(1);
const filteredNews = useMemo(() => {
return NEWS.filter((newsItem) => {
const matchesCategory = selectedCategory === '全部' || newsItem.category === selectedCategory;
const matchesSearch =
newsItem.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
newsItem.excerpt.toLowerCase().includes(searchQuery.toLowerCase());
return matchesCategory && matchesSearch;
});
}, [selectedCategory, searchQuery]);
const totalPages = Math.ceil(filteredNews.length / ITEMS_PER_PAGE);
const paginatedNews = useMemo(() => {
const startIndex = (currentPage - 1) * ITEMS_PER_PAGE;
return filteredNews.slice(startIndex, startIndex + ITEMS_PER_PAGE);
}, [filteredNews, currentPage]);
const handlePageChange = (page: number) => {
setCurrentPage(page);
window.scrollTo({ top: 0, behavior: 'smooth' });
};
const handleCategoryChange = (category: string) => {
setSelectedCategory(category);
setCurrentPage(1);
};
const handleSearchChange = (e: ChangeEvent<HTMLInputElement>) => {
setSearchQuery(e.target.value);
setCurrentPage(1);
};
return (
<div className="min-h-screen bg-white text-ink overflow-x-hidden">
{/* Hero Section */}
<section className="relative min-h-[75vh] flex items-center overflow-hidden bg-white">
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10 w-full py-28 sm:py-36 md:py-44 lg:py-56">
<div className="max-w-4xl">
<ScrollReveal>
<SectionLabel>News &amp; Insights</SectionLabel>
</ScrollReveal>
<ScrollReveal delay={0.1}>
<h1 className="text-4xl sm:text-5xl md:text-6xl lg:text-7xl xl:text-8xl font-bold leading-[0.92] tracking-tighter mb-10 sm:mb-12 md:mb-16">
<span className="block mt-4 text-brand"></span>
</h1>
</ScrollReveal>
<ScrollReveal delay={0.2}>
<p className="text-lg md:text-xl text-text-secondary max-w-2xl leading-relaxed">
{COMPANY_INFO.displayName}
</p>
</ScrollReveal>
</div>
</div>
</section>
{/* Filter + News List Section */}
<section className="relative py-20 sm:py-28 md:py-36 lg:py-44 overflow-hidden bg-bg-secondary">
<div className="absolute top-0 left-0 right-0 h-px bg-border-primary" />
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10">
{/* Filters */}
<ScrollReveal className="mb-12 sm:mb-16 space-y-6">
<div className="flex flex-wrap gap-3">
{categories.map((category) => (
<Button
key={category}
variant={selectedCategory === category ? 'primary' : 'outline'}
onClick={() => handleCategoryChange(category)}
>
{category}
</Button>
))}
</div>
<div className="relative max-w-md">
<Search className="absolute left-4 top-1/2 transform -translate-y-1/2 text-text-muted w-5 h-5 pointer-events-none" />
<Input
type="text"
placeholder="搜索新闻..."
value={searchQuery}
onChange={handleSearchChange}
className="pl-12 h-12 bg-white border-border-primary text-ink placeholder:text-text-muted focus:border-brand/50"
/>
</div>
</ScrollReveal>
{/* News Grid */}
{paginatedNews.length === 0 ? (
<ScrollReveal>
<div className="text-center py-24">
<Newspaper className="w-16 h-16 text-text-muted/20 mx-auto mb-6" />
<p className="text-xl text-text-secondary"></p>
<p className="mt-2 text-text-muted"></p>
</div>
</ScrollReveal>
) : (
<>
<StaggerReveal
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 sm:gap-8"
delayChildren={0.05}
>
{paginatedNews.map((newsItem, index) => (
<NewsCard key={newsItem.id} newsItem={newsItem} index={index} />
))}
</StaggerReveal>
{/* Pagination */}
{totalPages > 1 && (
<ScrollReveal>
<div className="flex justify-center items-center gap-2 mt-16">
<Button
variant="outline"
size="icon"
onClick={() => handlePageChange(currentPage - 1)}
disabled={currentPage === 1}
>
<ChevronLeft className="w-4 h-4" />
</Button>
{Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => (
<Button
key={page}
variant={currentPage === page ? 'primary' : 'outline'}
size="icon"
onClick={() => handlePageChange(page)}
>
{page}
</Button>
))}
<Button
variant="outline"
size="icon"
onClick={() => handlePageChange(currentPage + 1)}
disabled={currentPage === totalPages}
>
<ChevronRight className="w-4 h-4" />
</Button>
</div>
</ScrollReveal>
)}
</>
)}
</div>
</section>
{/* CTA Section */}
<section className="relative py-20 sm:py-28 md:py-36 lg:py-44 overflow-hidden bg-white">
<div className="absolute top-0 left-0 right-0 h-px bg-border-primary" />
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10">
<ScrollReveal className="text-center max-w-3xl mx-auto">
<SectionLabel className="justify-center">Get in Touch</SectionLabel>
<h2 className="text-3xl sm:text-4xl md:text-5xl font-bold tracking-tight leading-tight">
</h2>
<p className="mt-6 text-lg text-text-secondary leading-relaxed">
</p>
<div className="mt-12 flex flex-col sm:flex-row justify-center gap-4 sm:gap-6">
<a
href="/contact"
className="group inline-flex items-center justify-center gap-3 px-12 py-6 font-semibold text-base bg-brand text-white transition-all duration-300 hover:bg-brand/90"
>
<ArrowRight className="w-4 h-4 transition-transform duration-300 group-hover:translate-x-1" />
</a>
<a
href="/about"
className="inline-flex items-center justify-center gap-3 px-12 py-6 font-semibold text-base text-ink border border-border-primary hover:border-border-secondary hover:bg-bg-secondary transition-all duration-300"
>
</a>
</div>
</ScrollReveal>
</div>
</section>
</div>
);
}
@@ -0,0 +1,193 @@
'use client';
import { ScrollReveal } from '@/components/ui/scroll-reveal';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Calendar, ArrowLeft, Newspaper, ArrowRight } from 'lucide-react';
import { NEWS } from '@/lib/constants';
import { cn } from '@/lib/utils';
function GrainOverlay() {
return (
<div
className="absolute inset-0 pointer-events-none opacity-[0.03] mix-blend-overlay"
style={{
backgroundImage:
"url(\"data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E\")",
}}
/>
);
}
function SectionLabel({ children, className = '' }: { children: React.ReactNode; className?: string }) {
return (
<div className={cn('flex items-center gap-5 mb-7', className)}>
<div className="w-14 h-px bg-brand" />
<span className="text-[11px] tracking-[0.4em] uppercase font-bold text-brand">
{children}
</span>
<div className="w-14 h-px bg-brand" />
</div>
);
}
function RelatedNewsCard({ news }: { news: typeof NEWS[0] }) {
return (
<a
href={`/news/${news.id}`}
className="group relative block border border-white/10 hover:border-brand/40 bg-ink transition-all duration-600 hover:-translate-y-2 overflow-hidden h-full"
>
<div className="absolute top-0 left-0 right-0 h-1 bg-brand scale-x-0 group-hover:scale-x-100 transition-transform duration-700 origin-left" />
<div className="aspect-video bg-ink-light overflow-hidden">
{news.image ? (
<img
src={news.image}
alt={news.title}
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-700"
/>
) : (
<div className="w-full h-full bg-gradient-to-br from-brand/10 to-ink-light flex items-center justify-center">
<Newspaper className="w-10 h-10 text-brand/30" />
</div>
)}
</div>
<div className="p-5">
<Badge variant="outline" className="mb-3 text-xs border-brand/30 text-brand">
{news.category}
</Badge>
<h3 className="text-base font-semibold text-white mb-2 line-clamp-2 group-hover:text-brand transition-colors duration-300 leading-snug">
{news.title}
</h3>
<p className="text-sm text-white/50 line-clamp-2 leading-relaxed">{news.excerpt}</p>
</div>
</a>
);
}
interface NewsDetailContentV1Props {
news: typeof NEWS[0];
}
export default function NewsDetailContentV1({ news }: NewsDetailContentV1Props) {
const relatedNews = NEWS
.filter((n) => n.id !== news.id && n.category === news.category)
.slice(0, 3);
return (
<div className="min-h-screen bg-ink text-white overflow-x-hidden">
{/* Hero Section */}
<section className="relative pt-32 pb-16 lg:pt-40 lg:pb-20 overflow-hidden">
<GrainOverlay />
<div className="absolute inset-0 bg-gradient-to-b from-ink-light/50 to-ink pointer-events-none" />
<div className="max-w-container mx-auto px-6 lg:px-10 relative z-10">
<ScrollReveal>
<nav className="flex items-center gap-2 text-sm text-white/50 mb-10">
<a href="/news" className="hover:text-brand transition-colors duration-300">
</a>
<span>/</span>
<span className="text-white/80 line-clamp-1">{news.title}</span>
</nav>
</ScrollReveal>
<ScrollReveal delay={0.1} className="max-w-4xl">
<Badge variant="outline" className="mb-6 border-brand/30 text-brand text-sm">
{news.category}
</Badge>
<h1 className="text-3xl sm:text-4xl lg:text-5xl font-bold tracking-tight leading-tight">
{news.title}
</h1>
<div className="flex items-center gap-6 mt-8 text-white/50 text-sm">
<div className="flex items-center gap-2">
<Calendar className="w-4 h-4" />
{news.date}
</div>
</div>
<div className="mt-10 h-px bg-gradient-to-r from-brand/50 via-white/10 to-transparent" />
</ScrollReveal>
</div>
</section>
{/* Article Body Section */}
<section className="relative pb-24 lg:pb-32 overflow-hidden bg-ink">
<div className="max-w-container mx-auto px-6 lg:px-10">
<ScrollReveal className="max-w-4xl mx-auto">
{/* Featured Image */}
{news.image ? (
<div className="aspect-video overflow-hidden mb-12 border border-white/10">
<img
src={news.image}
alt={news.title}
className="w-full h-full object-cover"
/>
</div>
) : (
<div className="aspect-video bg-gradient-to-br from-brand/10 to-ink-light mb-12 flex items-center justify-center border border-white/10">
<Newspaper className="w-20 h-20 text-brand/20" />
</div>
)}
{/* Excerpt */}
<div className="mb-12 p-8 border-t-2 border-brand bg-brand/[0.04]">
<p className="text-lg text-white/80 leading-relaxed italic">
{news.excerpt}
</p>
</div>
{/* Article Content */}
<div className="prose prose-invert max-w-none">
<div className="text-white/80 text-base sm:text-lg leading-[1.9] whitespace-pre-line">
{news.content}
</div>
</div>
</ScrollReveal>
{/* Related News */}
{relatedNews.length > 0 && (
<div className="mt-24 pt-20 border-t border-white/10">
<ScrollReveal className="mb-16">
<SectionLabel>Related News</SectionLabel>
<h2 className="text-3xl md:text-4xl font-bold text-white tracking-tight leading-tight">
</h2>
</ScrollReveal>
<ScrollReveal delay={0.1}>
<div className="grid md:grid-cols-3 gap-6 md:gap-8">
{relatedNews.map((related) => (
<RelatedNewsCard key={related.id} news={related} />
))}
</div>
</ScrollReveal>
</div>
)}
{/* Navigation */}
<ScrollReveal>
<div className="mt-20 flex flex-wrap justify-center gap-4">
<Button size="lg" variant="outline" asChild>
<a href="/news">
<ArrowLeft className="w-4 h-4 mr-2" />
</a>
</Button>
<Button size="lg" asChild>
<a href="/contact">
<ArrowRight className="w-4 h-4 ml-2" />
</a>
</Button>
</div>
</ScrollReveal>
</div>
</section>
</div>
);
}
@@ -0,0 +1,170 @@
'use client';
import { ScrollReveal } from '@/components/ui/scroll-reveal';
import { Badge } from '@/components/ui/badge';
import { Calendar, ArrowLeft, Newspaper, ArrowRight } from 'lucide-react';
import { NEWS } from '@/lib/constants';
import { SectionLabel, EASE_OUT } from '@/components/ui/page-decoration';
import { motion } from 'framer-motion';
function RelatedNewsCard({ news, index }: { news: typeof NEWS[0]; index: number }) {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-80px' }}
transition={{ duration: 0.5, delay: index * 0.1, ease: EASE_OUT }}
>
<a
href={`/news/${news.id}`}
className="group relative block border border-border-primary hover:border-brand/30 bg-white transition-all duration-500 hover:-translate-y-2 overflow-hidden h-full"
>
<div className="absolute top-0 left-0 bottom-0 w-1 bg-brand scale-y-0 group-hover:scale-y-100 transition-transform duration-500 origin-top" />
<div className="aspect-video bg-bg-secondary overflow-hidden">
{news.image ? (
<img
src={news.image}
alt={news.title}
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-700"
/>
) : (
<div className="w-full h-full bg-gradient-to-br from-brand/[0.08] to-bg-secondary flex items-center justify-center">
<Newspaper className="w-10 h-10 text-brand/30" />
</div>
)}
</div>
<div className="p-5 sm:p-6">
<Badge variant="outline" className="mb-3 text-xs border-brand/30 text-brand">
{news.category}
</Badge>
<h3 className="text-base font-semibold text-ink mb-2 line-clamp-2 group-hover:text-brand transition-colors duration-300 leading-snug">
{news.title}
</h3>
<p className="text-sm text-text-secondary line-clamp-2 leading-relaxed">{news.excerpt}</p>
</div>
</a>
</motion.div>
);
}
interface NewsDetailContentV2Props {
news: typeof NEWS[0];
}
export default function NewsDetailContentV2({ news }: NewsDetailContentV2Props) {
const relatedNews = NEWS
.filter((n) => n.id !== news.id && n.category === news.category)
.slice(0, 3);
return (
<div className="min-h-screen bg-white text-ink overflow-x-hidden">
{/* Hero Section */}
<section className="relative pt-28 sm:pt-32 md:pt-40 pb-16 sm:pb-20 md:pb-24 overflow-hidden">
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10">
<ScrollReveal>
<nav className="flex items-center gap-2 text-sm text-text-muted mb-8 sm:mb-10">
<a href="/news" className="hover:text-brand transition-colors duration-300">
</a>
<span>/</span>
<span className="text-text-secondary line-clamp-1">{news.title}</span>
</nav>
</ScrollReveal>
<ScrollReveal delay={0.1} className="max-w-4xl">
<Badge variant="outline" className="mb-6 border-brand/30 text-brand text-sm">
{news.category}
</Badge>
<h1 className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-bold tracking-tight leading-tight">
{news.title}
</h1>
<div className="flex items-center gap-6 mt-8 text-text-muted text-sm">
<div className="flex items-center gap-2">
<Calendar className="w-4 h-4" />
{news.date}
</div>
</div>
<div className="mt-10 h-px bg-gradient-to-r from-brand/50 via-border-primary to-transparent" />
</ScrollReveal>
</div>
</section>
{/* Article Body Section */}
<section className="relative pb-20 sm:pb-28 md:pb-36 overflow-hidden bg-white">
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10">
<ScrollReveal className="max-w-4xl mx-auto">
{/* Featured Image */}
{news.image ? (
<div className="aspect-video overflow-hidden mb-10 sm:mb-12 border border-border-primary">
<img
src={news.image}
alt={news.title}
className="w-full h-full object-cover"
/>
</div>
) : (
<div className="aspect-video bg-gradient-to-br from-brand/[0.08] to-bg-secondary mb-10 sm:mb-12 flex items-center justify-center border border-border-primary">
<Newspaper className="w-20 h-20 text-brand/20" />
</div>
)}
{/* Excerpt */}
<div className="mb-10 sm:mb-12 p-6 sm:p-8 border-t-2 border-brand bg-brand/[0.04]">
<p className="text-base sm:text-lg text-text-secondary leading-relaxed italic">
{news.excerpt}
</p>
</div>
{/* Article Content */}
<div className="text-text-secondary text-base sm:text-lg leading-[1.9] whitespace-pre-line">
{news.content}
</div>
</ScrollReveal>
{/* Related News */}
{relatedNews.length > 0 && (
<div className="mt-20 sm:mt-24 pt-16 sm:pt-20 border-t border-border-primary">
<ScrollReveal className="mb-12 sm:mb-16">
<SectionLabel>Related News</SectionLabel>
<h2 className="text-2xl sm:text-3xl md:text-4xl font-bold tracking-tight leading-tight">
</h2>
</ScrollReveal>
<div className="grid md:grid-cols-3 gap-6 sm:gap-8">
{relatedNews.map((related, idx) => (
<RelatedNewsCard key={related.id} news={related} index={idx} />
))}
</div>
</div>
)}
{/* Navigation */}
<ScrollReveal>
<div className="mt-16 sm:mt-20 flex flex-col sm:flex-row justify-center gap-4">
<a
href="/news"
className="inline-flex items-center justify-center gap-2 px-8 py-4 font-medium text-base text-ink border border-border-primary hover:border-border-secondary hover:bg-bg-secondary transition-all duration-300"
>
<ArrowLeft className="w-4 h-4" />
</a>
<a
href="/contact"
className="group inline-flex items-center justify-center gap-2 px-8 py-4 font-medium text-base bg-brand text-white transition-all duration-300 hover:bg-brand/90"
>
<ArrowRight className="w-4 h-4 transition-transform duration-300 group-hover:translate-x-1" />
</a>
</div>
</ScrollReveal>
</div>
</section>
</div>
);
}
@@ -0,0 +1,168 @@
'use client';
import { ScrollReveal } from '@/components/ui/scroll-reveal';
import { Badge } from '@/components/ui/badge';
import { Calendar, ArrowLeft, Newspaper, ArrowRight } from 'lucide-react';
import { SectionLabel, EASE_OUT } from '@/components/ui/page-decoration';
import { motion } from 'framer-motion';
import type { NewsItem } from '@/lib/constants/news';
function RelatedNewsCard({ news, index }: { news: NewsItem; index: number }) {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-80px' }}
transition={{ duration: 0.5, delay: index * 0.1, ease: EASE_OUT }}
>
<a
href={`/news/${news.id}`}
className="group relative block border border-border-primary hover:border-brand/30 bg-white transition-all duration-500 hover:-translate-y-2 overflow-hidden h-full"
>
<div className="absolute top-0 left-0 bottom-0 w-1 bg-brand scale-y-0 group-hover:scale-y-100 transition-transform duration-500 origin-top" />
<div className="aspect-video bg-bg-secondary overflow-hidden">
{news.image ? (
<img
src={news.image}
alt={news.title}
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-700"
/>
) : (
<div className="w-full h-full bg-gradient-to-br from-brand/[0.08] to-bg-secondary flex items-center justify-center">
<Newspaper className="w-10 h-10 text-brand/30" />
</div>
)}
</div>
<div className="p-5 sm:p-6">
<Badge variant="outline" className="mb-3 text-xs border-brand/30 text-brand">
{news.category}
</Badge>
<h3 className="text-base font-semibold text-ink mb-2 line-clamp-2 group-hover:text-brand transition-colors duration-300 leading-snug">
{news.title}
</h3>
<p className="text-sm text-text-secondary line-clamp-2 leading-relaxed">{news.excerpt}</p>
</div>
</a>
</motion.div>
);
}
interface NewsDetailContentV3Props {
news: NewsItem;
relatedNews?: NewsItem[];
}
export default function NewsDetailContentV3({ news, relatedNews = [] }: NewsDetailContentV3Props) {
return (
<div className="min-h-screen bg-white text-ink overflow-x-hidden">
{/* Hero Section */}
<section className="relative pt-28 sm:pt-32 md:pt-40 pb-16 sm:pb-20 md:pb-24 overflow-hidden">
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10">
<ScrollReveal>
<nav className="flex items-center gap-2 text-sm text-text-muted mb-8 sm:mb-10">
<a href="/news" className="hover:text-brand transition-colors duration-300">
</a>
<span>/</span>
<span className="text-text-secondary line-clamp-1">{news.title}</span>
</nav>
</ScrollReveal>
<ScrollReveal delay={0.1} className="max-w-4xl">
<Badge variant="outline" className="mb-6 border-brand/30 text-brand text-sm">
{news.category}
</Badge>
<h1 className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-bold tracking-tight leading-tight">
{news.title}
</h1>
<div className="flex items-center gap-6 mt-8 text-text-muted text-sm">
<div className="flex items-center gap-2">
<Calendar className="w-4 h-4" />
{news.date}
</div>
</div>
<div className="mt-10 h-px bg-gradient-to-r from-brand/50 via-border-primary to-transparent" />
</ScrollReveal>
</div>
</section>
{/* Article Body Section */}
<section className="relative pb-20 sm:pb-28 md:pb-36 overflow-hidden bg-white">
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10">
<ScrollReveal className="max-w-4xl mx-auto">
{/* Featured Image */}
{news.image ? (
<div className="aspect-video overflow-hidden mb-10 sm:mb-12 border border-border-primary">
<img
src={news.image}
alt={news.title}
className="w-full h-full object-cover"
/>
</div>
) : (
<div className="aspect-video bg-gradient-to-br from-brand/[0.08] to-bg-secondary mb-10 sm:mb-12 flex items-center justify-center border border-border-primary">
<Newspaper className="w-20 h-20 text-brand/20" />
</div>
)}
{/* Excerpt */}
<div className="mb-10 sm:mb-12 p-6 sm:p-8 border-t-2 border-brand bg-brand/[0.04]">
<p className="text-base sm:text-lg text-text-secondary leading-relaxed italic">
{news.excerpt}
</p>
</div>
{/* Article Content */}
<div className="text-text-secondary text-base sm:text-lg leading-[1.9] whitespace-pre-line">
{news.content}
</div>
</ScrollReveal>
{/* Related News */}
{relatedNews.length > 0 && (
<div className="mt-20 sm:mt-24 pt-16 sm:pt-20 border-t border-border-primary">
<ScrollReveal className="mb-12 sm:mb-16">
<SectionLabel>Related News</SectionLabel>
<h2 className="text-2xl sm:text-3xl md:text-4xl font-bold tracking-tight leading-tight">
</h2>
</ScrollReveal>
<div className="grid md:grid-cols-3 gap-6 sm:gap-8">
{relatedNews.map((related, idx) => (
<RelatedNewsCard key={related.id} news={related} index={idx} />
))}
</div>
</div>
)}
{/* Navigation */}
<ScrollReveal>
<div className="mt-16 sm:mt-20 flex flex-col sm:flex-row justify-center gap-4">
<a
href="/news"
className="inline-flex items-center justify-center gap-2 px-8 py-4 font-medium text-base text-ink border border-border-primary hover:border-border-secondary hover:bg-bg-secondary transition-all duration-300"
>
<ArrowLeft className="w-4 h-4" />
</a>
<a
href="/contact"
className="group inline-flex items-center justify-center gap-2 px-8 py-4 font-medium text-base bg-brand text-white transition-all duration-300 hover:bg-brand/90"
>
<ArrowRight className="w-4 h-4 transition-transform duration-300 group-hover:translate-x-1" />
</a>
</div>
</ScrollReveal>
</div>
</section>
</div>
);
}
@@ -0,0 +1,451 @@
'use client';
import { ScrollReveal, StaggerReveal } from '@/components/ui/scroll-reveal';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import {
ArrowRight,
CheckCircle2,
Zap,
Shield,
TrendingUp,
Clock,
Database,
Users,
BarChart3,
Target,
Layers,
Settings,
} from 'lucide-react';
import { PRODUCTS } from '@/lib/constants';
import { cn } from '@/lib/utils';
function SectionLabel({ children, className = '' }: { children: React.ReactNode; className?: string }) {
return (
<div className={cn('flex items-center gap-5 mb-7', className)}>
<div className="w-14 h-px bg-brand" />
<span className="text-[11px] tracking-[0.4em] uppercase font-bold text-brand">
{children}
</span>
<div className="w-14 h-px bg-brand" />
</div>
);
}
const PAIN_POINTS = [
{
icon: Database,
title: '数据孤岛',
desc: '财务、采购、销售、库存各有系统,数据分散,对账耗时耗力',
},
{
icon: Clock,
title: '效率低下',
desc: '大量人工重复操作,流程不透明,审批周期长,决策缺乏数据支撑',
},
{
icon: Shield,
title: '安全隐患',
desc: '老旧系统维护困难,数据安全缺乏保障,合规风险日益严峻',
},
{
icon: TrendingUp,
title: '增长瓶颈',
desc: '系统跟不上业务发展,新需求响应慢,成为企业数字化转型的绊脚石',
},
];
const UPGRADE_BENEFITS = [
{
icon: Zap,
value: '40%+',
title: '运营效率提升',
desc: '流程自动化,减少人工重复操作,让团队聚焦高价值工作',
},
{
icon: Target,
value: '99.2%',
title: '数据准确性',
desc: '实时数据同步,消除信息孤岛,决策基于真实数据',
},
{
icon: Clock,
value: '70%',
title: '月结时间缩短',
desc: '从5天到1天,财务结账效率大幅提升',
},
{
icon: Shield,
value: '100%',
title: '合规安全保障',
desc: '等保三级+ISO27001+信创认证,数据安全无忧',
},
];
const UPGRADE_PROCESS = [
{
step: '01',
title: '现状诊断',
desc: '全面评估现有系统与业务流程,识别核心痛点与优化空间',
duration: '1-2 周',
},
{
step: '02',
title: '方案设计',
desc: '定制化升级方案,明确功能模块、数据迁移策略与实施路径',
duration: '2-3 周',
},
{
step: '03',
title: '系统部署',
desc: '私有化部署实施,数据安全迁移,系统配置与集成对接',
duration: '1-3 个月',
},
{
step: '04',
title: '培训上线',
desc: '分层用户培训,试运行验证,正式上线与持续优化',
duration: '2-4 周',
},
];
const FEATURE_MODULES = [
{ icon: BarChart3, title: '财务管理', desc: '总账、应收应付、成本核算、固定资产' },
{ icon: Users, title: '采购管理', desc: '供应商、采购订单、入库验收、采购分析' },
{ icon: Target, title: '销售管理', desc: '客户管理、销售订单、发货管理、销售分析' },
{ icon: Database, title: '库存管理', desc: '库存查询、出入库、库存预警、盘点管理' },
{ icon: Layers, title: '生产管理', desc: '生产计划、物料需求、生产订单、成本核算' },
{ icon: Settings, title: '报表分析', desc: '丰富报表模板,支持自定义报表与数据分析' },
];
export default function ErpUpgradeContentV1() {
const product = PRODUCTS.find((p) => p.id === 'erp')!;
return (
<div className="min-h-screen bg-white text-ink overflow-x-hidden">
{/* Hero Section */}
<section className="relative pt-32 pb-32 lg:pt-40 lg:pb-40 overflow-hidden">
<div className="max-w-container mx-auto px-6 lg:px-10 relative z-10">
<ScrollReveal>
<Badge variant="outline" className="mb-8 border-brand/30 text-brand text-sm px-4 py-2">
</Badge>
</ScrollReveal>
<ScrollReveal delay={0.1}>
<h1 className="text-4xl md:text-5xl lg:text-6xl xl:text-7xl font-bold tracking-tight leading-[1.1] max-w-4xl text-ink">
ERP系统
<br />
<span className="text-brand"></span>
</h1>
</ScrollReveal>
<ScrollReveal delay={0.2}>
<p className="mt-8 text-lg md:text-xl text-text-secondary max-w-2xl leading-relaxed">
ERP
</p>
</ScrollReveal>
<ScrollReveal delay={0.3}>
<div className="mt-12 flex flex-wrap gap-4">
<Button size="xl" asChild>
<a href="/contact">
<ArrowRight className="w-5 h-5 ml-2" />
</a>
</Button>
<Button size="xl" variant="outline" asChild>
<a href="#features"></a>
</Button>
</div>
</ScrollReveal>
</div>
</section>
{/* Pain Points Section */}
<section className="relative py-24 lg:py-32 overflow-hidden bg-bg-secondary">
<div className="absolute top-0 left-0 right-0 h-px bg-border-primary" />
<div className="max-w-container mx-auto px-6 lg:px-10 relative z-10">
<ScrollReveal className="max-w-3xl mb-16">
<SectionLabel>Pain Points</SectionLabel>
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold tracking-tight leading-tight text-ink">
<span className="text-brand"></span>
</h2>
</ScrollReveal>
<StaggerReveal
className="grid grid-cols-1 md:grid-cols-2 gap-6 md:gap-8"
delayChildren={0.08}
>
{PAIN_POINTS.map((item, index) => (
<div
key={index}
className="group relative border border-border-primary hover:border-brand/30 bg-white p-8 lg:p-10 transition-all duration-500 hover:-translate-y-1 overflow-hidden"
>
<div className="absolute top-0 left-0 right-0 h-1 bg-brand scale-x-0 group-hover:scale-x-100 transition-transform duration-700 origin-left" />
<div className="w-14 h-14 bg-brand/10 border border-brand/20 flex items-center justify-center mb-6">
<item.icon className="w-7 h-7 text-brand" />
</div>
<h3 className="text-xl font-semibold mb-3 text-ink">{item.title}</h3>
<p className="text-text-secondary leading-relaxed">{item.desc}</p>
</div>
))}
</StaggerReveal>
</div>
</section>
{/* Benefits Section */}
<section className="relative py-24 lg:py-32 overflow-hidden bg-white">
<div className="max-w-container mx-auto px-6 lg:px-10 relative z-10">
<ScrollReveal className="max-w-3xl mb-16">
<SectionLabel>Upgrade Benefits</SectionLabel>
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold tracking-tight leading-tight text-ink">
<span className="text-brand"></span>
</h2>
<p className="mt-6 text-lg text-text-secondary leading-relaxed">
</p>
</ScrollReveal>
<StaggerReveal
className="grid grid-cols-2 lg:grid-cols-4 gap-6 md:gap-8"
delayChildren={0.08}
>
{UPGRADE_BENEFITS.map((item, index) => (
<div
key={index}
className="text-center p-8 border border-border-primary bg-white hover:border-brand/30 hover:bg-bg-secondary transition-all duration-500 group"
>
<div className="w-12 h-12 mx-auto mb-6 bg-brand/10 border border-brand/20 flex items-center justify-center group-hover:scale-110 transition-transform duration-300">
<item.icon className="w-6 h-6 text-brand" />
</div>
<div className="text-4xl md:text-5xl font-bold text-brand mb-3">{item.value}</div>
<h4 className="text-base font-semibold mb-2 text-ink">{item.title}</h4>
<p className="text-sm text-text-muted leading-relaxed">{item.desc}</p>
</div>
))}
</StaggerReveal>
</div>
</section>
{/* Features Section */}
<section id="features" className="relative py-24 lg:py-32 overflow-hidden bg-bg-secondary">
<div className="absolute top-0 left-0 right-0 h-px bg-border-primary" />
<div className="max-w-container mx-auto px-6 lg:px-10 relative z-10">
<ScrollReveal className="max-w-3xl mb-16">
<SectionLabel>Core Modules</SectionLabel>
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold tracking-tight leading-tight text-ink">
<span className="text-brand"></span>
</h2>
</ScrollReveal>
<StaggerReveal
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 md:gap-8"
delayChildren={0.06}
>
{FEATURE_MODULES.map((item, index) => (
<div
key={index}
className="group relative border border-border-primary hover:border-brand/30 bg-white p-8 transition-all duration-500 hover:-translate-y-1 overflow-hidden"
>
<div className="absolute top-0 left-0 right-0 h-1 bg-brand scale-x-0 group-hover:scale-x-100 transition-transform duration-700 origin-left" />
<div className="flex items-start gap-4">
<div className="w-12 h-12 bg-brand/10 border border-brand/20 flex items-center justify-center shrink-0">
<item.icon className="w-6 h-6 text-brand" />
</div>
<div>
<h3 className="text-lg font-semibold mb-2 text-ink">{item.title}</h3>
<p className="text-text-secondary text-sm leading-relaxed">{item.desc}</p>
</div>
</div>
</div>
))}
</StaggerReveal>
</div>
</section>
{/* Process Section */}
<section className="relative py-24 lg:py-32 overflow-hidden bg-white">
<div className="max-w-container mx-auto px-6 lg:px-10 relative z-10">
<ScrollReveal className="max-w-3xl mb-16">
<SectionLabel>Upgrade Process</SectionLabel>
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold tracking-tight leading-tight text-ink">
<span className="text-brand"></span>
</h2>
<p className="mt-6 text-lg text-text-secondary leading-relaxed">
</p>
</ScrollReveal>
<div className="relative">
<div className="hidden lg:block absolute top-20 left-0 right-0 h-px bg-border-primary" />
<StaggerReveal
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 md:gap-8"
delayChildren={0.1}
>
{UPGRADE_PROCESS.map((item, index) => (
<div key={index} className="relative">
<div className="text-6xl font-bold text-brand/10 mb-4">{item.step}</div>
<h3 className="text-xl font-semibold mb-3 text-ink">{item.title}</h3>
<p className="text-text-secondary mb-4 leading-relaxed">{item.desc}</p>
<div className="inline-flex items-center gap-2 text-sm text-brand">
<Clock className="w-4 h-4" />
{item.duration}
</div>
</div>
))}
</StaggerReveal>
</div>
</div>
</section>
{/* Case Study Section */}
<section className="relative py-24 lg:py-32 overflow-hidden bg-bg-secondary">
<div className="absolute top-0 left-0 right-0 h-px bg-border-primary" />
<div className="max-w-container mx-auto px-6 lg:px-10 relative z-10">
<ScrollReveal className="max-w-3xl mb-16">
<SectionLabel>Case Study</SectionLabel>
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold tracking-tight leading-tight text-ink">
<span className="text-brand"></span>
</h2>
</ScrollReveal>
<ScrollReveal delay={0.1}>
<div className="border border-border-primary bg-white p-8 lg:p-12 relative overflow-hidden">
<div className="absolute top-0 left-0 right-0 h-1 bg-brand" />
<div className="grid lg:grid-cols-3 gap-8 lg:gap-12">
<div>
<Badge variant="outline" className="mb-4 border-brand/30 text-brand">
</Badge>
<h3 className="text-xl font-semibold mb-4 text-ink"></h3>
<p className="text-text-secondary leading-relaxed">
55000+ERP系统已使用10年
</p>
</div>
<div>
<h3 className="text-xl font-semibold mb-4 text-ink"></h3>
<ul className="space-y-3">
{['财务、采购、销售、库存数据分散', '月结耗时5天以上,数据准确性差', '多工厂协同困难,库存周转率低', '系统扩展性差,新需求响应慢'].map((item, i) => (
<li key={i} className="flex items-start gap-3">
<CheckCircle2 className="w-5 h-5 text-brand shrink-0 mt-0.5" />
<span className="text-text-secondary">{item}</span>
</li>
))}
</ul>
</div>
<div>
<h3 className="text-xl font-semibold mb-4 text-ink"></h3>
<ul className="space-y-3">
{['月结时间从5天缩短至1天', '库存准确率提升至99.2%', '库存周转率提升35%', '运营效率整体提升40%+'].map((item, i) => (
<li key={i} className="flex items-start gap-3">
<TrendingUp className="w-5 h-5 text-brand shrink-0 mt-0.5" />
<span className="text-text-secondary">{item}</span>
</li>
))}
</ul>
</div>
</div>
</div>
</ScrollReveal>
</div>
</section>
{/* Certifications Section */}
<section className="relative py-24 lg:py-32 overflow-hidden bg-white">
<div className="max-w-container mx-auto px-6 lg:px-10 relative z-10">
<ScrollReveal className="max-w-3xl mb-16 text-center mx-auto">
<SectionLabel className="justify-center">
<div className="flex items-center gap-5">
<div className="w-14 h-px bg-brand" />
<span className="text-[11px] tracking-[0.4em] uppercase font-bold text-brand">
Security & Compliance
</span>
<div className="w-14 h-px bg-brand" />
</div>
</SectionLabel>
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold tracking-tight leading-tight text-ink">
<span className="text-brand"></span>
</h2>
</ScrollReveal>
<StaggerReveal
className="flex flex-wrap justify-center gap-6 md:gap-10"
delayChildren={0.1}
>
{product.certifications.map((cert, index) => (
<div
key={index}
className="group text-center p-8 border border-border-primary bg-white hover:border-brand/30 hover:bg-bg-secondary transition-all duration-500 min-w-[200px]"
>
<div className="w-16 h-16 mx-auto mb-4 bg-brand/10 border border-brand/20 flex items-center justify-center group-hover:scale-110 transition-transform duration-300">
<Shield className="w-8 h-8 text-brand" />
</div>
<h4 className="text-base font-semibold mb-1 text-ink">{cert.name}</h4>
<p className="text-sm text-text-muted">{cert.issuer}</p>
</div>
))}
</StaggerReveal>
</div>
</section>
{/* CTA Section */}
<section className="relative py-36 lg:py-44 overflow-hidden bg-bg-secondary">
<div className="absolute top-0 left-0 right-0 h-px bg-border-primary" />
<div className="max-w-container mx-auto px-6 lg:px-10 relative z-10">
<ScrollReveal className="max-w-3xl mx-auto text-center">
<SectionLabel className="justify-center">
<div className="flex items-center gap-5">
<div className="w-14 h-px bg-brand" />
<span className="text-[11px] tracking-[0.4em] uppercase font-bold text-brand">
Get Started
</span>
<div className="w-14 h-px bg-brand" />
</div>
</SectionLabel>
<h2 className="text-3xl md:text-4xl lg:text-6xl font-bold text-ink tracking-tight leading-tight mb-8">
</h2>
<p className="text-lg md:text-xl text-text-secondary leading-relaxed mb-12">
+ ERP能为您带来什么
</p>
<div className="flex flex-wrap gap-6 justify-center">
<Button size="xl" asChild>
<a href="/contact">
<ArrowRight className="w-5 h-5 ml-2" />
</a>
</Button>
<Button size="xl" variant="outline" asChild>
<a href="/products/erp"></a>
</Button>
</div>
</ScrollReveal>
</div>
</section>
</div>
);
}
@@ -0,0 +1,454 @@
'use client';
import { ScrollReveal, StaggerReveal } from '@/components/ui/scroll-reveal';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import {
ArrowRight,
CheckCircle2,
Zap,
Shield,
TrendingUp,
Clock,
Database,
Users,
BarChart3,
Target,
Layers,
Settings,
} from 'lucide-react';
import { cn } from '@/lib/utils';
import type { ContentItem } from '@/lib/cms/types';
interface ErpUpgradeContentV2Props {
item: ContentItem;
}
function SectionLabel({ children, className = '' }: { children: React.ReactNode; className?: string }) {
return (
<div className={cn('flex items-center gap-5 mb-7', className)}>
<div className="w-14 h-px bg-brand" />
<span className="text-[11px] tracking-[0.4em] uppercase font-bold text-brand">
{children}
</span>
<div className="w-14 h-px bg-brand" />
</div>
);
}
const PAIN_POINTS = [
{
icon: Database,
title: '数据孤岛',
desc: '财务、采购、销售、库存各有系统,数据分散,对账耗时耗力',
},
{
icon: Clock,
title: '效率低下',
desc: '大量人工重复操作,流程不透明,审批周期长,决策缺乏数据支撑',
},
{
icon: Shield,
title: '安全隐患',
desc: '老旧系统维护困难,数据安全缺乏保障,合规风险日益严峻',
},
{
icon: TrendingUp,
title: '增长瓶颈',
desc: '系统跟不上业务发展,新需求响应慢,成为企业数字化转型的绊脚石',
},
];
const UPGRADE_BENEFITS = [
{
icon: Zap,
value: '40%+',
title: '运营效率提升',
desc: '流程自动化,减少人工重复操作,让团队聚焦高价值工作',
},
{
icon: Target,
value: '99.2%',
title: '数据准确性',
desc: '实时数据同步,消除信息孤岛,决策基于真实数据',
},
{
icon: Clock,
value: '70%',
title: '月结时间缩短',
desc: '从5天到1天,财务结账效率大幅提升',
},
{
icon: Shield,
value: '100%',
title: '合规安全保障',
desc: '等保三级+ISO27001+信创认证,数据安全无忧',
},
];
const UPGRADE_PROCESS = [
{
step: '01',
title: '现状诊断',
desc: '全面评估现有系统与业务流程,识别核心痛点与优化空间',
duration: '1-2 周',
},
{
step: '02',
title: '方案设计',
desc: '定制化升级方案,明确功能模块、数据迁移策略与实施路径',
duration: '2-3 周',
},
{
step: '03',
title: '系统部署',
desc: '私有化部署实施,数据安全迁移,系统配置与集成对接',
duration: '1-3 个月',
},
{
step: '04',
title: '培训上线',
desc: '分层用户培训,试运行验证,正式上线与持续优化',
duration: '2-4 周',
},
];
const FEATURE_MODULES = [
{ icon: BarChart3, title: '财务管理', desc: '总账、应收应付、成本核算、固定资产' },
{ icon: Users, title: '采购管理', desc: '供应商、采购订单、入库验收、采购分析' },
{ icon: Target, title: '销售管理', desc: '客户管理、销售订单、发货管理、销售分析' },
{ icon: Database, title: '库存管理', desc: '库存查询、出入库、库存预警、盘点管理' },
{ icon: Layers, title: '生产管理', desc: '生产计划、物料需求、生产订单、成本核算' },
{ icon: Settings, title: '报表分析', desc: '丰富报表模板,支持自定义报表与数据分析' },
];
export default function ErpUpgradeContentV2({ item }: ErpUpgradeContentV2Props) {
const data = item.data;
const certifications = (data.certifications ?? []) as Array<{ name: string; issuer: string; link?: string }>;
return (
<div className="min-h-screen bg-white text-ink overflow-x-hidden">
{/* Hero Section */}
<section className="relative pt-32 pb-32 lg:pt-40 lg:pb-40 overflow-hidden">
<div className="max-w-container mx-auto px-6 lg:px-10 relative z-10">
<ScrollReveal>
<Badge variant="outline" className="mb-8 border-brand/30 text-brand text-sm px-4 py-2">
</Badge>
</ScrollReveal>
<ScrollReveal delay={0.1}>
<h1 className="text-4xl md:text-5xl lg:text-6xl xl:text-7xl font-bold tracking-tight leading-[1.1] max-w-4xl text-ink">
ERP系统
<br />
<span className="text-brand"></span>
</h1>
</ScrollReveal>
<ScrollReveal delay={0.2}>
<p className="mt-8 text-lg md:text-xl text-text-secondary max-w-2xl leading-relaxed">
ERP
</p>
</ScrollReveal>
<ScrollReveal delay={0.3}>
<div className="mt-12 flex flex-wrap gap-4">
<Button size="xl" asChild>
<a href="/contact">
<ArrowRight className="w-5 h-5 ml-2" />
</a>
</Button>
<Button size="xl" variant="outline" asChild>
<a href="#features"></a>
</Button>
</div>
</ScrollReveal>
</div>
</section>
{/* Pain Points Section */}
<section className="relative py-24 lg:py-32 overflow-hidden bg-bg-secondary">
<div className="absolute top-0 left-0 right-0 h-px bg-border-primary" />
<div className="max-w-container mx-auto px-6 lg:px-10 relative z-10">
<ScrollReveal className="max-w-3xl mb-16">
<SectionLabel>Pain Points</SectionLabel>
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold tracking-tight leading-tight text-ink">
<span className="text-brand"></span>
</h2>
</ScrollReveal>
<StaggerReveal
className="grid grid-cols-1 md:grid-cols-2 gap-6 md:gap-8"
delayChildren={0.08}
>
{PAIN_POINTS.map((item, index) => (
<div
key={index}
className="group relative border border-border-primary hover:border-brand/30 bg-white p-8 lg:p-10 transition-all duration-500 hover:-translate-y-1 overflow-hidden"
>
<div className="absolute top-0 left-0 right-0 h-1 bg-brand scale-x-0 group-hover:scale-x-100 transition-transform duration-700 origin-left" />
<div className="w-14 h-14 bg-brand/10 border border-brand/20 flex items-center justify-center mb-6">
<item.icon className="w-7 h-7 text-brand" />
</div>
<h3 className="text-xl font-semibold mb-3 text-ink">{item.title}</h3>
<p className="text-text-secondary leading-relaxed">{item.desc}</p>
</div>
))}
</StaggerReveal>
</div>
</section>
{/* Benefits Section */}
<section className="relative py-24 lg:py-32 overflow-hidden bg-white">
<div className="max-w-container mx-auto px-6 lg:px-10 relative z-10">
<ScrollReveal className="max-w-3xl mb-16">
<SectionLabel>Upgrade Benefits</SectionLabel>
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold tracking-tight leading-tight text-ink">
<span className="text-brand"></span>
</h2>
<p className="mt-6 text-lg text-text-secondary leading-relaxed">
</p>
</ScrollReveal>
<StaggerReveal
className="grid grid-cols-2 lg:grid-cols-4 gap-6 md:gap-8"
delayChildren={0.08}
>
{UPGRADE_BENEFITS.map((item, index) => (
<div
key={index}
className="text-center p-8 border border-border-primary bg-white hover:border-brand/30 hover:bg-bg-secondary transition-all duration-500 group"
>
<div className="w-12 h-12 mx-auto mb-6 bg-brand/10 border border-brand/20 flex items-center justify-center group-hover:scale-110 transition-transform duration-300">
<item.icon className="w-6 h-6 text-brand" />
</div>
<div className="text-4xl md:text-5xl font-bold text-brand mb-3">{item.value}</div>
<h4 className="text-base font-semibold mb-2 text-ink">{item.title}</h4>
<p className="text-sm text-text-muted leading-relaxed">{item.desc}</p>
</div>
))}
</StaggerReveal>
</div>
</section>
{/* Features Section */}
<section id="features" className="relative py-24 lg:py-32 overflow-hidden bg-bg-secondary">
<div className="absolute top-0 left-0 right-0 h-px bg-border-primary" />
<div className="max-w-container mx-auto px-6 lg:px-10 relative z-10">
<ScrollReveal className="max-w-3xl mb-16">
<SectionLabel>Core Modules</SectionLabel>
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold tracking-tight leading-tight text-ink">
<span className="text-brand"></span>
</h2>
</ScrollReveal>
<StaggerReveal
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 md:gap-8"
delayChildren={0.06}
>
{FEATURE_MODULES.map((item, index) => (
<div
key={index}
className="group relative border border-border-primary hover:border-brand/30 bg-white p-8 transition-all duration-500 hover:-translate-y-1 overflow-hidden"
>
<div className="absolute top-0 left-0 right-0 h-1 bg-brand scale-x-0 group-hover:scale-x-100 transition-transform duration-700 origin-left" />
<div className="flex items-start gap-4">
<div className="w-12 h-12 bg-brand/10 border border-brand/20 flex items-center justify-center shrink-0">
<item.icon className="w-6 h-6 text-brand" />
</div>
<div>
<h3 className="text-lg font-semibold mb-2 text-ink">{item.title}</h3>
<p className="text-text-secondary text-sm leading-relaxed">{item.desc}</p>
</div>
</div>
</div>
))}
</StaggerReveal>
</div>
</section>
{/* Process Section */}
<section className="relative py-24 lg:py-32 overflow-hidden bg-white">
<div className="max-w-container mx-auto px-6 lg:px-10 relative z-10">
<ScrollReveal className="max-w-3xl mb-16">
<SectionLabel>Upgrade Process</SectionLabel>
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold tracking-tight leading-tight text-ink">
<span className="text-brand"></span>
</h2>
<p className="mt-6 text-lg text-text-secondary leading-relaxed">
</p>
</ScrollReveal>
<div className="relative">
<div className="hidden lg:block absolute top-20 left-0 right-0 h-px bg-border-primary" />
<StaggerReveal
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 md:gap-8"
delayChildren={0.1}
>
{UPGRADE_PROCESS.map((item, index) => (
<div key={index} className="relative">
<div className="text-6xl font-bold text-brand/10 mb-4">{item.step}</div>
<h3 className="text-xl font-semibold mb-3 text-ink">{item.title}</h3>
<p className="text-text-secondary mb-4 leading-relaxed">{item.desc}</p>
<div className="inline-flex items-center gap-2 text-sm text-brand">
<Clock className="w-4 h-4" />
{item.duration}
</div>
</div>
))}
</StaggerReveal>
</div>
</div>
</section>
{/* Case Study Section */}
<section className="relative py-24 lg:py-32 overflow-hidden bg-bg-secondary">
<div className="absolute top-0 left-0 right-0 h-px bg-border-primary" />
<div className="max-w-container mx-auto px-6 lg:px-10 relative z-10">
<ScrollReveal className="max-w-3xl mb-16">
<SectionLabel>Case Study</SectionLabel>
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold tracking-tight leading-tight text-ink">
<span className="text-brand"></span>
</h2>
</ScrollReveal>
<ScrollReveal delay={0.1}>
<div className="border border-border-primary bg-white p-8 lg:p-12 relative overflow-hidden">
<div className="absolute top-0 left-0 right-0 h-1 bg-brand" />
<div className="grid lg:grid-cols-3 gap-8 lg:gap-12">
<div>
<Badge variant="outline" className="mb-4 border-brand/30 text-brand">
</Badge>
<h3 className="text-xl font-semibold mb-4 text-ink"></h3>
<p className="text-text-secondary leading-relaxed">
55000+ERP系统已使用10年
</p>
</div>
<div>
<h3 className="text-xl font-semibold mb-4 text-ink"></h3>
<ul className="space-y-3">
{['财务、采购、销售、库存数据分散', '月结耗时5天以上,数据准确性差', '多工厂协同困难,库存周转率低', '系统扩展性差,新需求响应慢'].map((item, i) => (
<li key={i} className="flex items-start gap-3">
<CheckCircle2 className="w-5 h-5 text-brand shrink-0 mt-0.5" />
<span className="text-text-secondary">{item}</span>
</li>
))}
</ul>
</div>
<div>
<h3 className="text-xl font-semibold mb-4 text-ink"></h3>
<ul className="space-y-3">
{['月结时间从5天缩短至1天', '库存准确率提升至99.2%', '库存周转率提升35%', '运营效率整体提升40%+'].map((item, i) => (
<li key={i} className="flex items-start gap-3">
<TrendingUp className="w-5 h-5 text-brand shrink-0 mt-0.5" />
<span className="text-text-secondary">{item}</span>
</li>
))}
</ul>
</div>
</div>
</div>
</ScrollReveal>
</div>
</section>
{/* Certifications Section */}
<section className="relative py-24 lg:py-32 overflow-hidden bg-white">
<div className="max-w-container mx-auto px-6 lg:px-10 relative z-10">
<ScrollReveal className="max-w-3xl mb-16 text-center mx-auto">
<SectionLabel className="justify-center">
<div className="flex items-center gap-5">
<div className="w-14 h-px bg-brand" />
<span className="text-[11px] tracking-[0.4em] uppercase font-bold text-brand">
Security & Compliance
</span>
<div className="w-14 h-px bg-brand" />
</div>
</SectionLabel>
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold tracking-tight leading-tight text-ink">
<span className="text-brand"></span>
</h2>
</ScrollReveal>
<StaggerReveal
className="flex flex-wrap justify-center gap-6 md:gap-10"
delayChildren={0.1}
>
{certifications.map((cert, index) => (
<div
key={index}
className="group text-center p-8 border border-border-primary bg-white hover:border-brand/30 hover:bg-bg-secondary transition-all duration-500 min-w-[200px]"
>
<div className="w-16 h-16 mx-auto mb-4 bg-brand/10 border border-brand/20 flex items-center justify-center group-hover:scale-110 transition-transform duration-300">
<Shield className="w-8 h-8 text-brand" />
</div>
<h4 className="text-base font-semibold mb-1 text-ink">{cert.name}</h4>
<p className="text-sm text-text-muted">{cert.issuer}</p>
</div>
))}
</StaggerReveal>
</div>
</section>
{/* CTA Section */}
<section className="relative py-36 lg:py-44 overflow-hidden bg-bg-secondary">
<div className="absolute top-0 left-0 right-0 h-px bg-border-primary" />
<div className="max-w-container mx-auto px-6 lg:px-10 relative z-10">
<ScrollReveal className="max-w-3xl mx-auto text-center">
<SectionLabel className="justify-center">
<div className="flex items-center gap-5">
<div className="w-14 h-px bg-brand" />
<span className="text-[11px] tracking-[0.4em] uppercase font-bold text-brand">
Get Started
</span>
<div className="w-14 h-px bg-brand" />
</div>
</SectionLabel>
<h2 className="text-3xl md:text-4xl lg:text-6xl font-bold text-ink tracking-tight leading-tight mb-8">
</h2>
<p className="text-lg md:text-xl text-text-secondary leading-relaxed mb-12">
+ ERP能为您带来什么
</p>
<div className="flex flex-wrap gap-6 justify-center">
<Button size="xl" asChild>
<a href="/contact">
<ArrowRight className="w-5 h-5 ml-2" />
</a>
</Button>
<Button size="xl" variant="outline" asChild>
<a href="/products/erp"></a>
</Button>
</div>
</ScrollReveal>
</div>
</section>
</div>
);
}
@@ -0,0 +1,547 @@
'use client';
import { motion } from 'framer-motion';
import { ScrollReveal, StaggerReveal } from '@/components/ui/scroll-reveal';
import { Button } from '@/components/ui/button';
import { ArrowRight, Package, Check, Settings, BarChart3, Users, FileText, Cpu, Shield, TrendingUp } from 'lucide-react';
import { cn } from '@/lib/utils';
import type { Product } from '@/lib/constants/products';
const EASE_OUT = [0.22, 1, 0.36, 1] as const;
const PRODUCT_ICONS: Record<string, React.ReactNode> = {
'睿新ERP管理系统': <Package className="w-8 h-8" />,
'睿视商业智能分析平台': <BarChart3 className="w-8 h-8" />,
'睿新客户关系管理系统': <Users className="w-8 h-8" />,
'睿新协同办公平台': <Settings className="w-8 h-8" />,
'睿新内容管理系统': <FileText className="w-8 h-8" />,
'睿新供应链数字化平台': <Cpu className="w-8 h-8" />,
};
function GrainOverlay() {
return (
<div
className="absolute inset-0 pointer-events-none opacity-[0.03] mix-blend-overlay"
style={{
backgroundImage: `url("data:image/svg+xml,%3Csvg viewBox='0 0 512 512' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E")`,
backgroundRepeat: 'repeat',
backgroundSize: '300px 300px',
}}
/>
);
}
function SectionLabel({ children, className = '' }: { children: React.ReactNode; className?: string }) {
return (
<div className={cn('flex items-center gap-5 mb-7', className)}>
<div className="w-14 h-px bg-brand" />
<span className="text-[11px] tracking-[0.4em] uppercase font-bold text-brand">
{children}
</span>
</div>
);
}
interface DetailHeroProps {
product: Product;
}
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-ink pt-24">
<GrainOverlay />
<div className="absolute inset-0 pointer-events-none">
<div
className="absolute top-0 right-0 w-[50%] h-full"
style={{
background: 'linear-gradient(135deg, transparent 20%, rgba(196, 30, 58, 0.08) 100%)',
clipPath: 'polygon(30% 0, 100% 0, 100% 100%, 0% 100%)',
}}
/>
<div className="absolute bottom-0 left-0 w-full h-1/3 bg-gradient-to-t from-ink to-transparent" />
<div className="absolute top-32 right-[15%] w-[400px] h-[400px] rounded-full opacity-10 blur-3xl bg-brand" />
</div>
<div className="max-w-container mx-auto px-6 lg:px-10 relative z-10 w-full py-24 lg:py-32">
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, ease: EASE_OUT }}
className="flex items-center gap-4 mb-8"
>
<div className="w-16 h-16 rounded-2xl bg-brand-soft flex items-center justify-center text-brand">
{productIcon}
</div>
<div>
<span className="px-3 py-1.5 text-xs font-bold text-brand bg-brand-soft/50">
{product.category}
</span>
<div className="flex items-center gap-3 mt-2">
<span className="px-2.5 py-1 text-xs text-dark-text-muted border border-white/10">
{product.status}
</span>
<div className="flex gap-1.5">
{product.tags.slice(0, 2).map(tag => (
<span key={tag} className="px-2.5 py-1 text-xs text-dark-text-muted border border-white/10">
{tag}
</span>
))}
</div>
</div>
</div>
</motion.div>
<motion.h1
initial={{ opacity: 0, y: 50 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 1, delay: 0.15, ease: EASE_OUT }}
className="text-4xl sm:text-5xl md:text-6xl lg:text-7xl font-bold text-white leading-tight tracking-tighter mb-8 max-w-4xl"
>
{product.title}
</motion.h1>
<motion.p
initial={{ opacity: 0, y: 40 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.9, delay: 0.3, ease: EASE_OUT }}
className="text-xl lg:text-2xl text-dark-text-secondary max-w-3xl leading-relaxed mb-8"
>
{product.description}
</motion.p>
<motion.p
initial={{ opacity: 0, y: 40 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.9, delay: 0.4, ease: EASE_OUT }}
className="text-lg text-dark-text-muted max-w-3xl leading-relaxed mb-12"
>
{product.overview}
</motion.p>
<motion.div
initial={{ opacity: 0, y: 40 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.9, delay: 0.55, ease: EASE_OUT }}
className="flex flex-wrap gap-6"
>
<Button size="xl" asChild>
<a href="/contact">
<ArrowRight className="w-5 h-5 ml-2" />
</a>
</Button>
<Button size="xl" variant="outline" asChild>
<a href="/products"></a>
</Button>
</motion.div>
</div>
</section>
);
}
interface FeaturesSectionProps {
features: string[];
}
function FeaturesSection({ features }: FeaturesSectionProps) {
return (
<section className="relative py-32 lg:py-40 overflow-hidden bg-ink-light">
<GrainOverlay />
<div className="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-white/12 to-transparent" />
<div className="max-w-container mx-auto px-6 lg:px-10">
<ScrollReveal className="text-center max-w-3xl mx-auto mb-20">
<SectionLabel className="justify-center">
<div className="flex items-center gap-5">
<div className="w-14 h-px bg-brand" />
<span className="text-[11px] tracking-[0.4em] uppercase font-bold text-brand">
Core Features
</span>
<div className="w-14 h-px bg-brand" />
</div>
</SectionLabel>
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold text-white tracking-tight leading-tight">
</h2>
</ScrollReveal>
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
<StaggerReveal staggerDelay={0.08} delayChildren={0.05}>
{features.map((feature, idx) => (
<motion.div
key={idx}
className="group relative p-8 border border-white/10 bg-ink hover:border-brand/40 transition-all duration-500 hover:-translate-y-1"
>
<div className="absolute top-0 left-0 right-0 h-1 bg-brand scale-x-0 group-hover:scale-x-100 transition-transform duration-700 origin-left" />
<div className="flex items-start gap-5">
<div className="w-10 h-10 rounded-xl bg-brand-soft flex items-center justify-center text-brand shrink-0 mt-0.5">
<Check className="w-5 h-5" />
</div>
<p className="text-lg text-white font-medium leading-relaxed">{feature}</p>
</div>
</motion.div>
))}
</StaggerReveal>
</div>
</div>
</section>
);
}
interface BenefitsSectionProps {
benefits: string[];
}
function BenefitsSection({ benefits }: BenefitsSectionProps) {
return (
<section className="relative py-32 lg:py-40 overflow-hidden bg-ink">
<GrainOverlay />
<div className="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-white/12 to-transparent" />
<div className="max-w-container mx-auto px-6 lg:px-10">
<div className="grid lg:grid-cols-2 gap-20 items-start">
<ScrollReveal>
<SectionLabel>Benefits</SectionLabel>
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold text-white tracking-tight leading-tight mb-8">
</h2>
<p className="text-dark-text-secondary text-lg leading-relaxed">
</p>
</ScrollReveal>
<div className="space-y-5">
<StaggerReveal staggerDelay={0.1} delayChildren={0.05}>
{benefits.map((benefit, idx) => (
<motion.div
key={idx}
className="group relative p-8 border border-white/10 bg-ink-light hover:border-brand/30 transition-all duration-500"
whileHover={{ x: 12 }}
>
<div className="absolute left-0 top-0 bottom-0 w-1 bg-brand scale-y-0 group-hover:scale-y-100 transition-transform duration-500 origin-top" />
<div className="flex items-start gap-6">
<div className="w-12 h-12 rounded-2xl bg-brand-soft/50 flex items-center justify-center text-brand shrink-0">
<TrendingUp className="w-6 h-6" />
</div>
<p className="text-xl text-white font-semibold leading-relaxed">{benefit}</p>
</div>
</motion.div>
))}
</StaggerReveal>
</div>
</div>
</div>
</section>
);
}
interface ProcessSectionProps {
process: string[];
}
function ProcessSection({ process }: ProcessSectionProps) {
return (
<section className="relative py-32 lg:py-40 overflow-hidden bg-ink-light">
<GrainOverlay />
<div className="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-white/12 to-transparent" />
<div className="max-w-container mx-auto px-6 lg:px-10">
<ScrollReveal className="text-center max-w-3xl mx-auto mb-20">
<SectionLabel className="justify-center">
<div className="flex items-center gap-5">
<div className="w-14 h-px bg-brand" />
<span className="text-[11px] tracking-[0.4em] uppercase font-bold text-brand">
Implementation
</span>
<div className="w-14 h-px bg-brand" />
</div>
</SectionLabel>
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold text-white tracking-tight leading-tight">
</h2>
<p className="text-dark-text-secondary text-lg mt-6 leading-relaxed">
</p>
</ScrollReveal>
<div className="relative">
<div className="absolute top-1/2 left-0 right-0 h-px bg-gradient-to-r from-transparent via-white/20 to-transparent hidden lg:block" />
<div className="grid md:grid-cols-2 lg:grid-cols-4 gap-8">
<StaggerReveal staggerDelay={0.12} delayChildren={0.05}>
{process.map((step, idx) => (
<motion.div
key={idx}
className="group relative text-center"
whileHover={{ y: -8 }}
transition={{ type: 'spring', stiffness: 300, damping: 30 }}
>
<div className="relative z-10 w-20 h-20 rounded-3xl bg-ink border border-white/10 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>
</div>
<div className="p-6 border border-white/10 bg-ink group-hover:border-brand/30 transition-all duration-500">
<p className="text-lg text-white font-medium leading-relaxed">{step}</p>
</div>
</motion.div>
))}
</StaggerReveal>
</div>
</div>
</div>
</section>
);
}
interface DataProofsSectionProps {
dataProofs: Product['dataProofs'];
}
function DataProofsSection({ dataProofs }: DataProofsSectionProps) {
if (!dataProofs || dataProofs.length === 0) return null;
return (
<section className="relative py-32 lg:py-40 overflow-hidden bg-ink">
<GrainOverlay />
<div className="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-white/12 to-transparent" />
<div className="max-w-container mx-auto px-6 lg:px-10">
<ScrollReveal className="text-center max-w-3xl mx-auto mb-20">
<SectionLabel className="justify-center">
<div className="flex items-center gap-5">
<div className="w-14 h-px bg-brand" />
<span className="text-[11px] tracking-[0.4em] uppercase font-bold text-brand">
Proven Results
</span>
<div className="w-14 h-px bg-brand" />
</div>
</SectionLabel>
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold text-white tracking-tight leading-tight">
</h2>
</ScrollReveal>
<div className="grid md:grid-cols-3 gap-8">
<StaggerReveal staggerDelay={0.12} delayChildren={0.05}>
{dataProofs.map((proof, idx) => (
<motion.div
key={idx}
className="group relative p-10 text-center border border-white/10 bg-ink-light hover:border-brand/40 transition-all duration-500 hover:-translate-y-2"
>
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-1/3 h-1 bg-brand scale-x-0 group-hover:scale-x-100 transition-transform duration-700 origin-center" />
<div className="text-6xl lg:text-7xl font-bold text-brand mb-4 tracking-tighter">
{proof.value}
</div>
<div className="text-xl font-bold text-white mb-3">{proof.metric}</div>
<p className="text-dark-text-muted leading-relaxed">{proof.description}</p>
</motion.div>
))}
</StaggerReveal>
</div>
</div>
</section>
);
}
interface CaseStudiesSectionProps {
caseStudies: Product['caseStudies'];
}
function CaseStudiesSection({ caseStudies }: CaseStudiesSectionProps) {
if (!caseStudies || caseStudies.length === 0) return null;
return (
<section className="relative py-32 lg:py-40 overflow-hidden bg-ink-light">
<GrainOverlay />
<div className="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-white/12 to-transparent" />
<div className="max-w-container mx-auto px-6 lg:px-10">
<ScrollReveal className="text-center max-w-3xl mx-auto mb-20">
<SectionLabel className="justify-center">
<div className="flex items-center gap-5">
<div className="w-14 h-px bg-brand" />
<span className="text-[11px] tracking-[0.4em] uppercase font-bold text-brand">
Case Studies
</span>
<div className="w-14 h-px bg-brand" />
</div>
</SectionLabel>
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold text-white tracking-tight leading-tight">
</h2>
</ScrollReveal>
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
<StaggerReveal staggerDelay={0.1} delayChildren={0.05}>
{caseStudies.map((study) => (
<div
key={study.client}
className="group relative block overflow-hidden border border-white/10 hover:border-brand/40 bg-ink transition-all duration-500 hover:-translate-y-2"
>
<div className="aspect-[4/3] bg-gradient-to-br from-ink to-ink-light relative overflow-hidden">
<div className="absolute inset-0 flex items-center justify-center">
<span className="text-6xl font-bold text-white/[0.08]">
{study.client.charAt(0)}
</span>
</div>
<div className="absolute top-5 left-5">
<span className="px-3 py-1.5 text-xs font-bold text-brand bg-brand-soft/80 backdrop-blur-sm">
{study.industry}
</span>
</div>
</div>
<div className="p-8">
<h3 className="text-xl font-bold text-white mb-3 group-hover:translate-x-2 transition-transform duration-500">
{study.client}
</h3>
<p className="text-dark-text-secondary text-sm leading-relaxed mb-6">
{study.challenge}
</p>
<div className="pt-6 border-t border-white/10">
<div className="text-2xl font-bold text-brand mb-1">{study.result}</div>
<div className="text-xs text-dark-text-muted"></div>
</div>
</div>
</div>
))}
</StaggerReveal>
</div>
</div>
</section>
);
}
interface CertificationsSectionProps {
certifications: Product['certifications'];
}
function CertificationsSection({ certifications }: CertificationsSectionProps) {
if (!certifications || certifications.length === 0) return null;
return (
<section className="relative py-24 lg:py-32 overflow-hidden bg-ink">
<GrainOverlay />
<div className="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-white/12 to-transparent" />
<div className="max-w-container mx-auto px-6 lg:px-10">
<ScrollReveal className="text-center max-w-3xl mx-auto mb-16">
<SectionLabel className="justify-center">
<div className="flex items-center gap-5">
<div className="w-14 h-px bg-brand" />
<span className="text-[11px] tracking-[0.4em] uppercase font-bold text-brand">
Certifications
</span>
<div className="w-14 h-px bg-brand" />
</div>
</SectionLabel>
<h2 className="text-2xl md:text-3xl font-bold text-white tracking-tight">
</h2>
</ScrollReveal>
<div className="flex flex-wrap justify-center gap-8">
<StaggerReveal staggerDelay={0.08} delayChildren={0.05}>
{certifications.map((cert, idx) => (
<motion.div
key={idx}
className="group flex items-center gap-4 px-8 py-5 border border-white/10 bg-ink-light hover:border-brand/30 transition-all duration-500"
whileHover={{ y: -4 }}
>
<div className="w-12 h-12 rounded-xl bg-brand-soft flex items-center justify-center text-brand">
<Shield className="w-6 h-6" />
</div>
<div>
<div className="text-white font-bold">{cert.name}</div>
<div className="text-sm text-dark-text-muted">{cert.issuer}</div>
</div>
</motion.div>
))}
</StaggerReveal>
</div>
</div>
</section>
);
}
interface CTASectionProps {
product: Product;
}
function CTASection({ product }: CTASectionProps) {
return (
<section className="relative py-36 lg:py-44 overflow-hidden bg-ink-light">
<GrainOverlay />
<div className="absolute inset-0 pointer-events-none">
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[600px] h-[600px] rounded-full opacity-[0.08] blur-3xl bg-brand" />
</div>
<div className="max-w-container mx-auto px-6 lg:px-10 relative z-10">
<ScrollReveal className="max-w-3xl mx-auto text-center">
<SectionLabel className="justify-center">
<div className="flex items-center gap-5">
<div className="w-14 h-px bg-brand" />
<span className="text-[11px] tracking-[0.4em] uppercase font-bold text-brand">
Get Started
</span>
<div className="w-14 h-px bg-brand" />
</div>
</SectionLabel>
<h2 className="text-3xl md:text-4xl lg:text-6xl font-bold text-white tracking-tight leading-tight mb-8">
{product.title}
</h2>
<p className="text-xl text-dark-text-secondary leading-relaxed mb-12 max-w-2xl mx-auto">
使
</p>
<div className="flex flex-wrap gap-6 justify-center">
<Button size="xl" asChild>
<a href="/contact">
<ArrowRight className="w-5 h-5 ml-2" />
</a>
</Button>
<Button size="xl" variant="outline" asChild>
<a href="/products"></a>
</Button>
</div>
</ScrollReveal>
</div>
</section>
);
}
interface ProductDetailContentV1Props {
product: Product;
}
export default function ProductDetailContentV1({ product }: ProductDetailContentV1Props) {
return (
<main>
<DetailHero product={product} />
<FeaturesSection features={product.features} />
<BenefitsSection benefits={product.benefits} />
<ProcessSection process={product.process} />
<DataProofsSection dataProofs={product.dataProofs} />
<CaseStudiesSection caseStudies={product.caseStudies} />
<CertificationsSection certifications={product.certifications} />
<CTASection product={product} />
</main>
);
}
@@ -0,0 +1,468 @@
'use client';
import { motion } from 'framer-motion';
import { ScrollReveal, StaggerReveal } from '@/components/ui/scroll-reveal';
import { Button } from '@/components/ui/button';
import { ArrowRight, Package, Check, Settings, BarChart3, Users, FileText, Cpu, Shield, TrendingUp } from 'lucide-react';
import { SectionLabel, EASE_OUT } from '@/components/ui/page-decoration';
import type { Product } from '@/lib/constants/products';
import { MethodologyFramework, TechStackShowcase, FAQSection, DataProofSection } from '@/components/content/sections';
const PRODUCT_ICONS: Record<string, React.ReactNode> = {
'睿新ERP管理系统': <Package className="w-8 h-8" />,
'睿视商业智能分析平台': <BarChart3 className="w-8 h-8" />,
'睿新客户关系管理系统': <Users className="w-8 h-8" />,
'睿新协同办公平台': <Settings className="w-8 h-8" />,
'睿新内容管理系统': <FileText className="w-8 h-8" />,
'睿新供应链数字化平台': <Cpu className="w-8 h-8" />,
};
interface DetailHeroProps {
product: Product;
}
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">
<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 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, ease: EASE_OUT }}
className="flex items-center gap-4 mb-8"
>
<div className="w-16 h-16 rounded-2xl bg-brand-soft flex items-center justify-center text-brand">
{productIcon}
</div>
<div>
<span className="px-3 py-1.5 text-xs font-bold text-brand bg-brand-soft/50">
{product.category}
</span>
<div className="flex items-center gap-3 mt-2">
<span className="px-2.5 py-1 text-xs text-text-muted border border-border-primary">
{product.status}
</span>
<div className="flex gap-1.5">
{product.tags.slice(0, 2).map(tag => (
<span key={tag} className="px-2.5 py-1 text-xs text-text-muted border border-border-primary">
{tag}
</span>
))}
</div>
</div>
</div>
</motion.div>
<motion.h1
initial={{ opacity: 0, y: 50 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 1, delay: 0.15, ease: EASE_OUT }}
className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-bold text-ink leading-tight tracking-tighter mb-6 sm:mb-8 max-w-4xl"
>
{product.title}
</motion.h1>
<motion.p
initial={{ opacity: 0, y: 40 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.9, delay: 0.3, ease: EASE_OUT }}
className="text-xl lg:text-2xl text-text-secondary max-w-3xl leading-relaxed mb-8"
>
{product.description}
</motion.p>
<motion.p
initial={{ opacity: 0, y: 40 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.9, delay: 0.4, ease: EASE_OUT }}
className="text-lg text-text-muted max-w-3xl leading-relaxed mb-12"
>
{product.overview}
</motion.p>
<motion.div
initial={{ opacity: 0, y: 40 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.9, delay: 0.55, ease: EASE_OUT }}
className="flex flex-wrap gap-6"
>
<Button size="xl" asChild>
<a href="/contact">
<ArrowRight className="w-5 h-5 ml-2" />
</a>
</Button>
<Button size="xl" variant="outline" asChild>
<a href="/products"></a>
</Button>
</motion.div>
</div>
</section>
);
}
interface FeaturesSectionProps {
features: string[];
}
function FeaturesSection({ features }: FeaturesSectionProps) {
return (
<section className="relative py-20 sm:py-28 md:py-32 lg:py-40 overflow-hidden bg-bg-secondary">
<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" />
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10">
<ScrollReveal className="text-center max-w-3xl mx-auto mb-16 sm:mb-20 md:mb-20">
<SectionLabel className="justify-center">
<div className="flex items-center gap-5">
<div className="w-14 h-px bg-brand" />
<span className="text-[11px] tracking-[0.4em] uppercase font-bold text-brand">
Core Features
</span>
<div className="w-14 h-px bg-brand" />
</div>
</SectionLabel>
<h2 className="text-2xl sm:text-3xl md:text-4xl lg:text-5xl font-bold text-ink tracking-tight leading-tight">
</h2>
</ScrollReveal>
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-px bg-border-primary">
<StaggerReveal staggerDelay={0.08} delayChildren={0.05}>
{features.map((feature, idx) => (
<div
key={idx}
className="group relative p-8 bg-white hover:bg-bg-secondary transition-all duration-500"
>
<div className="absolute top-0 left-0 h-full w-1 scale-y-0 group-hover:scale-y-100 transition-transform duration-700 origin-top bg-brand" />
<div className="flex items-start gap-5">
<div className="w-10 h-10 rounded-xl bg-brand-soft flex items-center justify-center text-brand shrink-0 mt-0.5">
<Check className="w-5 h-5" />
</div>
<p className="text-lg text-ink font-medium leading-relaxed">{feature}</p>
</div>
</div>
))}
</StaggerReveal>
</div>
</div>
</section>
);
}
interface BenefitsSectionProps {
benefits: string[];
}
function BenefitsSection({ benefits }: BenefitsSectionProps) {
return (
<section className="relative py-20 sm:py-28 md:py-32 lg:py-40 overflow-hidden bg-white">
<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" />
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10">
<div className="grid lg:grid-cols-2 gap-20 items-start">
<ScrollReveal>
<SectionLabel>Benefits</SectionLabel>
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold text-ink tracking-tight leading-tight mb-8">
</h2>
<p className="text-text-secondary text-lg leading-relaxed">
</p>
</ScrollReveal>
<div className="space-y-px bg-border-primary">
<StaggerReveal staggerDelay={0.1} delayChildren={0.05}>
{benefits.map((benefit, idx) => (
<div
key={idx}
className="group relative p-8 bg-white hover:bg-bg-secondary transition-all duration-500"
>
<div className="absolute left-0 top-0 bottom-0 w-1 bg-brand scale-y-0 group-hover:scale-y-100 transition-transform duration-500 origin-top" />
<div className="flex items-start gap-6">
<div className="w-12 h-12 rounded-2xl bg-brand-soft/50 flex items-center justify-center text-brand shrink-0">
<TrendingUp className="w-6 h-6" />
</div>
<p className="text-xl text-ink font-semibold leading-relaxed">{benefit}</p>
</div>
</div>
))}
</StaggerReveal>
</div>
</div>
</div>
</section>
);
}
interface ProcessSectionProps {
process: string[];
}
function ProcessSection({ process }: ProcessSectionProps) {
return (
<section className="relative py-20 sm:py-28 md:py-32 lg:py-40 overflow-hidden bg-bg-secondary">
<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" />
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10">
<ScrollReveal className="text-center max-w-3xl mx-auto mb-16 sm:mb-20 md:mb-20">
<SectionLabel className="justify-center">
<div className="flex items-center gap-5">
<div className="w-14 h-px bg-brand" />
<span className="text-[11px] tracking-[0.4em] uppercase font-bold text-brand">
Implementation
</span>
<div className="w-14 h-px bg-brand" />
</div>
</SectionLabel>
<h2 className="text-2xl sm:text-3xl md:text-4xl lg:text-5xl font-bold text-ink tracking-tight leading-tight">
</h2>
<p className="text-text-secondary text-lg mt-6 leading-relaxed">
</p>
</ScrollReveal>
<div className="relative">
<div className="grid md:grid-cols-2 lg:grid-cols-4 gap-px bg-border-primary">
<StaggerReveal staggerDelay={0.12} delayChildren={0.05}>
{process.map((step, idx) => (
<div
key={idx}
className="group relative text-center p-8 bg-white 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>
</div>
<p className="text-lg text-ink font-medium leading-relaxed">{step}</p>
</div>
))}
</StaggerReveal>
</div>
</div>
</div>
</section>
);
}
interface CaseStudiesSectionProps {
caseStudies: Product['caseStudies'];
}
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">
<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" />
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10">
<ScrollReveal className="text-center max-w-3xl mx-auto mb-16 sm:mb-20 md:mb-20">
<SectionLabel className="justify-center">
<div className="flex items-center gap-5">
<div className="w-14 h-px bg-brand" />
<span className="text-[11px] tracking-[0.4em] uppercase font-bold text-brand">
Case Studies
</span>
<div className="w-14 h-px bg-brand" />
</div>
</SectionLabel>
<h2 className="text-2xl sm:text-3xl md:text-4xl lg:text-5xl font-bold text-ink tracking-tight leading-tight">
</h2>
</ScrollReveal>
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-px bg-border-primary">
<StaggerReveal staggerDelay={0.1} delayChildren={0.05}>
{caseStudies.map((study) => (
<div
key={study.client}
className="group relative block overflow-hidden bg-white 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">
<span className="text-6xl font-bold text-text-muted/10">
{study.client.charAt(0)}
</span>
</div>
<div className="absolute top-5 left-5">
<span className="px-3 py-1.5 text-xs font-bold text-brand bg-brand-soft/80 backdrop-blur-sm">
{study.industry}
</span>
</div>
</div>
<div className="p-8">
<h3 className="text-xl font-bold text-ink mb-3 group-hover:translate-x-2 transition-transform duration-500">
{study.client}
</h3>
<p className="text-text-secondary text-sm leading-relaxed mb-6">
{study.challenge}
</p>
<div className="pt-6 border-t border-border-primary">
<div className="text-2xl font-bold text-brand mb-1">{study.result}</div>
<div className="text-xs text-text-muted"></div>
</div>
</div>
</div>
))}
</StaggerReveal>
</div>
</div>
</section>
);
}
interface CertificationsSectionProps {
certifications: Product['certifications'];
}
function CertificationsSection({ certifications }: CertificationsSectionProps) {
if (!certifications || certifications.length === 0) return null;
return (
<section className="relative py-16 sm:py-20 md:py-24 lg:py-32 overflow-hidden bg-bg-secondary">
<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" />
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10">
<ScrollReveal className="text-center max-w-3xl mx-auto mb-16">
<SectionLabel className="justify-center">
<div className="flex items-center gap-5">
<div className="w-14 h-px bg-brand" />
<span className="text-[11px] tracking-[0.4em] uppercase font-bold text-brand">
Certifications
</span>
<div className="w-14 h-px bg-brand" />
</div>
</SectionLabel>
<h2 className="text-2xl md:text-3xl font-bold text-ink tracking-tight">
</h2>
</ScrollReveal>
<div className="flex flex-wrap justify-center gap-px bg-border-primary">
<StaggerReveal staggerDelay={0.08} delayChildren={0.05}>
{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"
>
<div className="w-12 h-12 rounded-xl bg-brand-soft flex items-center justify-center text-brand">
<Shield className="w-6 h-6" />
</div>
<div>
<div className="text-ink font-bold">{cert.name}</div>
<div className="text-sm text-text-muted">{cert.issuer}</div>
</div>
</div>
))}
</StaggerReveal>
</div>
</div>
</section>
);
}
interface CTASectionProps {
product: Product;
}
function CTASection({ product }: CTASectionProps) {
return (
<section className="relative py-36 lg:py-44 overflow-hidden bg-white">
<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" />
<div className="max-w-container mx-auto px-6 lg:px-10 relative z-10">
<ScrollReveal className="text-center max-w-3xl mx-auto">
<div className="flex justify-center mb-7">
<div className="flex items-center gap-5">
<div className="w-14 h-px bg-brand" />
<span className="text-[11px] tracking-[0.4em] uppercase font-bold text-brand">
Get Started
</span>
<div className="w-14 h-px bg-brand" />
</div>
</div>
<h2 className="text-4xl md:text-5xl lg:text-6xl font-bold text-ink tracking-tight leading-tight mb-8">
{product.title} <br />
<span className="text-brand"></span>
</h2>
<p className="text-xl text-text-secondary mb-12 max-w-2xl mx-auto leading-relaxed">
使
</p>
<div className="flex flex-wrap justify-center gap-6">
<Button size="xl" asChild>
<a href="/contact">
<ArrowRight className="w-5 h-5 ml-2" />
</a>
</Button>
<Button size="xl" variant="outline" asChild>
<a href="/products"></a>
</Button>
</div>
</ScrollReveal>
</div>
</section>
);
}
interface ProductDetailContentV2Props {
product: Product;
}
export default function ProductDetailContentV2({ product }: ProductDetailContentV2Props) {
return (
<main>
<DetailHero product={product} />
<FeaturesSection features={product.features} />
{product.methodology && product.methodology.length > 0 && (
<MethodologyFramework
title="产品方法论"
subtitle="经过实战验证的产品设计理念与实施框架"
layers={product.methodology}
/>
)}
<BenefitsSection benefits={product.benefits} />
<ProcessSection process={product.process} />
{product.techStack && product.techStack.length > 0 && (
<TechStackShowcase
title="技术架构与集成"
subtitle="开放、标准、可扩展的技术架构"
categories={product.techStack}
/>
)}
<DataProofSection
title="可衡量的成果"
subtitle="用数据说话,每一项指标都源自真实客户案例"
metrics={product.dataProofs.map((dp) => ({
value: dp.value,
label: dp.metric,
description: dp.description,
}))}
/>
<CaseStudiesSection caseStudies={product.caseStudies} />
<CertificationsSection certifications={product.certifications} />
{product.faqs && product.faqs.length > 0 && (
<FAQSection
title="常见问题"
subtitle="关于这款产品,客户最常问的问题"
items={product.faqs}
/>
)}
<CTASection product={product} />
</main>
);
}
@@ -0,0 +1,468 @@
'use client';
import { motion } from 'framer-motion';
import { ScrollReveal, StaggerReveal } from '@/components/ui/scroll-reveal';
import { Button } from '@/components/ui/button';
import { ArrowRight, Package, Check, Settings, BarChart3, Users, FileText, Cpu, Shield, TrendingUp } from 'lucide-react';
import { SectionLabel, EASE_OUT } from '@/components/ui/page-decoration';
import type { Product } from '@/lib/constants/products';
import { MethodologyFramework, TechStackShowcase, FAQSection, DataProofSection } from '@/components/content/sections';
const PRODUCT_ICONS: Record<string, React.ReactNode> = {
'睿新ERP管理系统': <Package className="w-8 h-8" />,
'睿视商业智能分析平台': <BarChart3 className="w-8 h-8" />,
'睿新客户关系管理系统': <Users className="w-8 h-8" />,
'睿新协同办公平台': <Settings className="w-8 h-8" />,
'睿新内容管理系统': <FileText className="w-8 h-8" />,
'睿新供应链数字化平台': <Cpu className="w-8 h-8" />,
};
interface DetailHeroProps {
product: Product;
}
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">
<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 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, ease: EASE_OUT }}
className="flex items-center gap-4 mb-8"
>
<div className="w-16 h-16 rounded-2xl bg-brand-soft flex items-center justify-center text-brand">
{productIcon}
</div>
<div>
<span className="px-3 py-1.5 text-xs font-bold text-brand bg-brand-soft/50">
{product.category}
</span>
<div className="flex items-center gap-3 mt-2">
<span className="px-2.5 py-1 text-xs text-text-muted border border-border-primary">
{product.status}
</span>
<div className="flex gap-1.5">
{product.tags.slice(0, 2).map(tag => (
<span key={tag} className="px-2.5 py-1 text-xs text-text-muted border border-border-primary">
{tag}
</span>
))}
</div>
</div>
</div>
</motion.div>
<motion.h1
initial={{ opacity: 0, y: 50 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 1, delay: 0.15, ease: EASE_OUT }}
className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-bold text-ink leading-tight tracking-tighter mb-6 sm:mb-8 max-w-4xl"
>
{product.title}
</motion.h1>
<motion.p
initial={{ opacity: 0, y: 40 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.9, delay: 0.3, ease: EASE_OUT }}
className="text-xl lg:text-2xl text-text-secondary max-w-3xl leading-relaxed mb-8"
>
{product.description}
</motion.p>
<motion.p
initial={{ opacity: 0, y: 40 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.9, delay: 0.4, ease: EASE_OUT }}
className="text-lg text-text-muted max-w-3xl leading-relaxed mb-12"
>
{product.overview}
</motion.p>
<motion.div
initial={{ opacity: 0, y: 40 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.9, delay: 0.55, ease: EASE_OUT }}
className="flex flex-wrap gap-6"
>
<Button size="xl" asChild>
<a href="/contact">
<ArrowRight className="w-5 h-5 ml-2" />
</a>
</Button>
<Button size="xl" variant="outline" asChild>
<a href="/products"></a>
</Button>
</motion.div>
</div>
</section>
);
}
interface FeaturesSectionProps {
features: string[];
}
function FeaturesSection({ features }: FeaturesSectionProps) {
return (
<section className="relative py-20 sm:py-28 md:py-32 lg:py-40 overflow-hidden bg-bg-secondary">
<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" />
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10">
<ScrollReveal className="text-center max-w-3xl mx-auto mb-16 sm:mb-20 md:mb-20">
<SectionLabel className="justify-center">
<div className="flex items-center gap-5">
<div className="w-14 h-px bg-brand" />
<span className="text-[11px] tracking-[0.4em] uppercase font-bold text-brand">
Core Features
</span>
<div className="w-14 h-px bg-brand" />
</div>
</SectionLabel>
<h2 className="text-2xl sm:text-3xl md:text-4xl lg:text-5xl font-bold text-ink tracking-tight leading-tight">
</h2>
</ScrollReveal>
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-px bg-border-primary">
<StaggerReveal staggerDelay={0.08} delayChildren={0.05}>
{features.map((feature, idx) => (
<div
key={idx}
className="group relative p-8 bg-white hover:bg-bg-secondary transition-all duration-500"
>
<div className="absolute top-0 left-0 h-full w-1 scale-y-0 group-hover:scale-y-100 transition-transform duration-700 origin-top bg-brand" />
<div className="flex items-start gap-5">
<div className="w-10 h-10 rounded-xl bg-brand-soft flex items-center justify-center text-brand shrink-0 mt-0.5">
<Check className="w-5 h-5" />
</div>
<p className="text-lg text-ink font-medium leading-relaxed">{feature}</p>
</div>
</div>
))}
</StaggerReveal>
</div>
</div>
</section>
);
}
interface BenefitsSectionProps {
benefits: string[];
}
function BenefitsSection({ benefits }: BenefitsSectionProps) {
return (
<section className="relative py-20 sm:py-28 md:py-32 lg:py-40 overflow-hidden bg-white">
<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" />
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10">
<div className="grid lg:grid-cols-2 gap-20 items-start">
<ScrollReveal>
<SectionLabel>Benefits</SectionLabel>
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold text-ink tracking-tight leading-tight mb-8">
</h2>
<p className="text-text-secondary text-lg leading-relaxed">
</p>
</ScrollReveal>
<div className="space-y-px bg-border-primary">
<StaggerReveal staggerDelay={0.1} delayChildren={0.05}>
{benefits.map((benefit, idx) => (
<div
key={idx}
className="group relative p-8 bg-white hover:bg-bg-secondary transition-all duration-500"
>
<div className="absolute left-0 top-0 bottom-0 w-1 bg-brand scale-y-0 group-hover:scale-y-100 transition-transform duration-500 origin-top" />
<div className="flex items-start gap-6">
<div className="w-12 h-12 rounded-2xl bg-brand-soft/50 flex items-center justify-center text-brand shrink-0">
<TrendingUp className="w-6 h-6" />
</div>
<p className="text-xl text-ink font-semibold leading-relaxed">{benefit}</p>
</div>
</div>
))}
</StaggerReveal>
</div>
</div>
</div>
</section>
);
}
interface ProcessSectionProps {
process: string[];
}
function ProcessSection({ process }: ProcessSectionProps) {
return (
<section className="relative py-20 sm:py-28 md:py-32 lg:py-40 overflow-hidden bg-bg-secondary">
<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" />
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10">
<ScrollReveal className="text-center max-w-3xl mx-auto mb-16 sm:mb-20 md:mb-20">
<SectionLabel className="justify-center">
<div className="flex items-center gap-5">
<div className="w-14 h-px bg-brand" />
<span className="text-[11px] tracking-[0.4em] uppercase font-bold text-brand">
Implementation
</span>
<div className="w-14 h-px bg-brand" />
</div>
</SectionLabel>
<h2 className="text-2xl sm:text-3xl md:text-4xl lg:text-5xl font-bold text-ink tracking-tight leading-tight">
</h2>
<p className="text-text-secondary text-lg mt-6 leading-relaxed">
</p>
</ScrollReveal>
<div className="relative">
<div className="grid md:grid-cols-2 lg:grid-cols-4 gap-px bg-border-primary">
<StaggerReveal staggerDelay={0.12} delayChildren={0.05}>
{process.map((step, idx) => (
<div
key={idx}
className="group relative text-center p-8 bg-white 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>
</div>
<p className="text-lg text-ink font-medium leading-relaxed">{step}</p>
</div>
))}
</StaggerReveal>
</div>
</div>
</div>
</section>
);
}
interface CaseStudiesSectionProps {
caseStudies: Product['caseStudies'];
}
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">
<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" />
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10">
<ScrollReveal className="text-center max-w-3xl mx-auto mb-16 sm:mb-20 md:mb-20">
<SectionLabel className="justify-center">
<div className="flex items-center gap-5">
<div className="w-14 h-px bg-brand" />
<span className="text-[11px] tracking-[0.4em] uppercase font-bold text-brand">
Case Studies
</span>
<div className="w-14 h-px bg-brand" />
</div>
</SectionLabel>
<h2 className="text-2xl sm:text-3xl md:text-4xl lg:text-5xl font-bold text-ink tracking-tight leading-tight">
</h2>
</ScrollReveal>
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-px bg-border-primary">
<StaggerReveal staggerDelay={0.1} delayChildren={0.05}>
{caseStudies.map((study) => (
<div
key={study.client}
className="group relative block overflow-hidden bg-white 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">
<span className="text-6xl font-bold text-text-muted/10">
{study.client.charAt(0)}
</span>
</div>
<div className="absolute top-5 left-5">
<span className="px-3 py-1.5 text-xs font-bold text-brand bg-brand-soft/80 backdrop-blur-sm">
{study.industry}
</span>
</div>
</div>
<div className="p-8">
<h3 className="text-xl font-bold text-ink mb-3 group-hover:translate-x-2 transition-transform duration-500">
{study.client}
</h3>
<p className="text-text-secondary text-sm leading-relaxed mb-6">
{study.challenge}
</p>
<div className="pt-6 border-t border-border-primary">
<div className="text-2xl font-bold text-brand mb-1">{study.result}</div>
<div className="text-xs text-text-muted"></div>
</div>
</div>
</div>
))}
</StaggerReveal>
</div>
</div>
</section>
);
}
interface CertificationsSectionProps {
certifications: Product['certifications'];
}
function CertificationsSection({ certifications }: CertificationsSectionProps) {
if (!certifications || certifications.length === 0) return null;
return (
<section className="relative py-16 sm:py-20 md:py-24 lg:py-32 overflow-hidden bg-bg-secondary">
<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" />
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10">
<ScrollReveal className="text-center max-w-3xl mx-auto mb-16">
<SectionLabel className="justify-center">
<div className="flex items-center gap-5">
<div className="w-14 h-px bg-brand" />
<span className="text-[11px] tracking-[0.4em] uppercase font-bold text-brand">
Certifications
</span>
<div className="w-14 h-px bg-brand" />
</div>
</SectionLabel>
<h2 className="text-2xl md:text-3xl font-bold text-ink tracking-tight">
</h2>
</ScrollReveal>
<div className="flex flex-wrap justify-center gap-px bg-border-primary">
<StaggerReveal staggerDelay={0.08} delayChildren={0.05}>
{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"
>
<div className="w-12 h-12 rounded-xl bg-brand-soft flex items-center justify-center text-brand">
<Shield className="w-6 h-6" />
</div>
<div>
<div className="text-ink font-bold">{cert.name}</div>
<div className="text-sm text-text-muted">{cert.issuer}</div>
</div>
</div>
))}
</StaggerReveal>
</div>
</div>
</section>
);
}
interface CTASectionProps {
product: Product;
}
function CTASection({ product }: CTASectionProps) {
return (
<section className="relative py-36 lg:py-44 overflow-hidden bg-white">
<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" />
<div className="max-w-container mx-auto px-6 lg:px-10 relative z-10">
<ScrollReveal className="text-center max-w-3xl mx-auto">
<div className="flex justify-center mb-7">
<div className="flex items-center gap-5">
<div className="w-14 h-px bg-brand" />
<span className="text-[11px] tracking-[0.4em] uppercase font-bold text-brand">
Get Started
</span>
<div className="w-14 h-px bg-brand" />
</div>
</div>
<h2 className="text-4xl md:text-5xl lg:text-6xl font-bold text-ink tracking-tight leading-tight mb-8">
{product.title} <br />
<span className="text-brand"></span>
</h2>
<p className="text-xl text-text-secondary mb-12 max-w-2xl mx-auto leading-relaxed">
使
</p>
<div className="flex flex-wrap justify-center gap-6">
<Button size="xl" asChild>
<a href="/contact">
<ArrowRight className="w-5 h-5 ml-2" />
</a>
</Button>
<Button size="xl" variant="outline" asChild>
<a href="/products"></a>
</Button>
</div>
</ScrollReveal>
</div>
</section>
);
}
interface ProductDetailContentV3Props {
product: Product;
}
export default function ProductDetailContentV3({ product }: ProductDetailContentV3Props) {
return (
<main>
<DetailHero product={product} />
<FeaturesSection features={product.features} />
{product.methodology && product.methodology.length > 0 && (
<MethodologyFramework
title="产品方法论"
subtitle="经过实战验证的产品设计理念与实施框架"
layers={product.methodology}
/>
)}
<BenefitsSection benefits={product.benefits} />
<ProcessSection process={product.process} />
{product.techStack && product.techStack.length > 0 && (
<TechStackShowcase
title="技术架构与集成"
subtitle="开放、标准、可扩展的技术架构"
categories={product.techStack}
/>
)}
<DataProofSection
title="可衡量的成果"
subtitle="用数据说话,每一项指标都源自真实客户案例"
metrics={product.dataProofs.map((dp) => ({
value: dp.value,
label: dp.metric,
description: dp.description,
}))}
/>
<CaseStudiesSection caseStudies={product.caseStudies} />
<CertificationsSection certifications={product.certifications} />
{product.faqs && product.faqs.length > 0 && (
<FAQSection
title="常见问题"
subtitle="关于这款产品,客户最常问的问题"
items={product.faqs}
/>
)}
<CTASection product={product} />
</main>
);
}
@@ -0,0 +1,120 @@
'use client';
import { useState, useMemo, useEffect } from 'react';
import { initCms, getItemRenderer } from '@/lib/cms';
import type { ContentItem } from '@/lib/cms';
import { PRODUCTS, PRODUCT_CATEGORIES } from '@/lib/constants';
function productToContentItem(product: unknown, index: number): ContentItem {
const p = product as Record<string, unknown>;
return {
id: String(p.id),
modelId: 'product',
modelCode: 'product',
title: String(p.title),
slug: String(p.id),
status: 'published',
version: 1,
sortOrder: index,
data: p,
createdAt: '2024-01-01T00:00:00Z',
updatedAt: '2024-01-01T00:00:00Z',
publishedAt: '2024-01-01T00:00:00Z',
createdBy: 'system',
updatedBy: 'system',
};
}
export default function ProductsContentCms() {
const [ready, setReady] = useState(false);
const [items, setItems] = useState<ContentItem[]>([]);
const [selectedCategory, setSelectedCategory] = useState<string>('all');
useEffect(() => {
initCms();
setItems(PRODUCTS.map(productToContentItem));
setReady(true);
}, []);
const filteredItems = useMemo(() => {
if (selectedCategory === 'all') return items;
return items.filter((item) => {
const data = item.data as Record<string, unknown>;
return data.categoryId === selectedCategory;
});
}, [items, selectedCategory]);
if (!ready) {
return (
<div className="min-h-screen bg-ink flex items-center justify-center">
<div className="text-white/50">...</div>
</div>
);
}
const ProductCardRenderer = getItemRenderer('product');
return (
<div className="min-h-screen bg-ink text-white overflow-x-hidden" data-theme="dark">
<section className="relative min-h-[50vh] flex items-center overflow-hidden bg-ink pt-24">
<div className="absolute inset-0">
<div className="absolute top-1/3 -right-32 w-96 h-96 rounded-full bg-blue-500/20 blur-[120px]" />
</div>
<div className="relative z-10 w-full max-w-7xl mx-auto px-6 lg:px-8 py-20">
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-white/[0.05] border border-white/[0.1] text-sm text-white/70 mb-6">
<span className="w-1.5 h-1.5 rounded-full bg-brand animate-pulse" />
</div>
<h1 className="text-4xl sm:text-5xl md:text-6xl font-bold text-white tracking-tight leading-[1.1]">
</h1>
<p className="mt-6 text-lg text-white/60 max-w-2xl leading-relaxed">
</p>
</div>
</section>
<section className="relative py-16 sm:py-20 md:py-28 overflow-hidden bg-ink-light">
<div className="max-w-7xl mx-auto px-6 lg:px-8">
<div className="flex flex-wrap gap-3 mb-12">
<button
onClick={() => setSelectedCategory('all')}
className={`px-5 py-2 text-sm font-medium rounded-xl transition-all ${
selectedCategory === 'all'
? 'bg-brand text-white'
: 'bg-white/[0.05] text-white/60 hover:bg-white/[0.1] hover:text-white border border-white/[0.08]'
}`}
>
</button>
{PRODUCT_CATEGORIES.map((cat) => (
<button
key={cat.id}
onClick={() => setSelectedCategory(cat.id)}
className={`px-5 py-2 text-sm font-medium rounded-xl transition-all ${
selectedCategory === cat.id
? 'bg-brand text-white'
: 'bg-white/[0.05] text-white/60 hover:bg-white/[0.1] hover:text-white border border-white/[0.08]'
}`}
>
{cat.title}
</button>
))}
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 md:gap-8">
{filteredItems.map((item, i) =>
ProductCardRenderer ? (
<ProductCardRenderer key={item.id} item={item} index={i} />
) : (
<div key={item.id} className="p-4 border border-white/10 rounded-lg">
{item.title}
</div>
)
)}
</div>
</div>
</section>
</div>
);
}
@@ -0,0 +1,428 @@
'use client';
import { motion } from 'framer-motion';
import { ScrollReveal, StaggerReveal } from '@/components/ui/scroll-reveal';
import { Button } from '@/components/ui/button';
import { ArrowRight, Package, Cpu, BarChart3, Users, Settings, FileText } from 'lucide-react';
import { cn } from '@/lib/utils';
import { PRODUCTS, PRODUCT_CATEGORIES, type Product } from '@/lib/constants/products';
const EASE_OUT = [0.22, 1, 0.36, 1] as const;
const PRODUCT_ICONS: Record<string, React.ReactNode> = {
'睿新ERP管理系统': <Package className="w-7 h-7" />,
'睿视商业智能分析平台': <BarChart3 className="w-7 h-7" />,
'睿新客户关系管理系统': <Users className="w-7 h-7" />,
'睿新协同办公平台': <Settings className="w-7 h-7" />,
'睿新内容管理系统': <FileText className="w-7 h-7" />,
'睿新供应链数字化平台': <Cpu className="w-7 h-7" />,
};
function GrainOverlay() {
return (
<div
className="absolute inset-0 pointer-events-none opacity-[0.03] mix-blend-overlay"
style={{
backgroundImage: `url("data:image/svg+xml,%3Csvg viewBox='0 0 512 512' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E")`,
backgroundRepeat: 'repeat',
backgroundSize: '300px 300px',
}}
/>
);
}
function SectionLabel({ children, className = '' }: { children: React.ReactNode; className?: string }) {
return (
<div className={cn('flex items-center gap-5 mb-7', className)}>
<div className="w-14 h-px bg-brand" />
<span className="text-[11px] tracking-[0.4em] uppercase font-bold text-brand">
{children}
</span>
</div>
);
}
function HeroSection() {
return (
<section className="relative min-h-[85vh] flex items-center overflow-hidden bg-ink">
<GrainOverlay />
<div className="absolute inset-0 pointer-events-none">
<div
className="absolute top-0 right-0 w-[45%] h-full"
style={{
background: 'linear-gradient(135deg, transparent 20%, rgba(196, 30, 58, 0.09) 100%)',
clipPath: 'polygon(30% 0, 100% 0, 100% 100%, 0% 100%)',
}}
/>
<div className="absolute bottom-0 left-0 w-full h-2/5 bg-gradient-to-t from-ink to-transparent" />
<div className="absolute top-24 right-[20%] w-[400px] h-[400px] rounded-full opacity-15 blur-3xl bg-brand" />
</div>
<div className="max-w-container mx-auto px-6 lg:px-10 relative z-10 w-full py-32 lg:py-40">
<div className="max-w-4xl">
<motion.div
initial={{ opacity: 0, x: -50 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 1.1, ease: EASE_OUT }}
className="mb-10"
>
<SectionLabel>Product Matrix</SectionLabel>
</motion.div>
<motion.h1
initial={{ opacity: 0, y: 70 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 1.2, delay: 0.15, ease: EASE_OUT }}
className="text-5xl sm:text-6xl md:text-7xl lg:text-8xl font-bold text-white leading-[0.85] tracking-tighter mb-12"
>
<div className="block"></div>
<div className="block mt-4">
<span className="relative">
<span className="text-brand"></span>
<motion.span
className="absolute -bottom-2 left-0 w-full h-1 bg-brand"
style={{ transformOrigin: 'left' }}
initial={{ scaleX: 0 }}
animate={{ scaleX: 1 }}
transition={{ duration: 0.8, delay: 1.2, ease: EASE_OUT }}
/>
</span>
</div>
<div className="block mt-4"></div>
</motion.h1>
<motion.p
initial={{ opacity: 0, y: 50 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 1, delay: 0.45, ease: EASE_OUT }}
className="text-xl lg:text-2xl text-dark-text-secondary max-w-2xl leading-relaxed mb-12"
>
6
</motion.p>
<motion.div
initial={{ opacity: 0, y: 50 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 1, delay: 0.6, ease: EASE_OUT }}
className="flex flex-wrap gap-6"
>
<Button size="xl" asChild>
<a href="/contact">
<ArrowRight className="w-5 h-5 ml-2" />
</a>
</Button>
<Button size="xl" variant="outline" asChild>
<a href="/solutions"></a>
</Button>
</motion.div>
</div>
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.9, ease: EASE_OUT }}
className="grid grid-cols-3 gap-12 mt-24 pt-16 border-t border-white/10 max-w-2xl"
>
<div>
<div className="text-5xl font-bold text-white mb-3">6<span className="text-brand">+</span></div>
<div className="text-sm text-dark-text-muted">线</div>
</div>
<div>
<div className="text-5xl font-bold text-white mb-3">100<span className="text-brand">%</span></div>
<div className="text-sm text-dark-text-muted"></div>
</div>
<div>
<div className="text-5xl font-bold text-white mb-3"><span className="text-brand"></span></div>
<div className="text-sm text-dark-text-muted"></div>
</div>
</motion.div>
</div>
</section>
);
}
function EnterpriseProductsSection() {
const enterpriseProducts = PRODUCTS.filter(p => p.categoryId === 'enterprise');
const category = PRODUCT_CATEGORIES.find(c => c.id === 'enterprise');
return (
<section className="relative py-36 lg:py-44 overflow-hidden bg-ink-light">
<GrainOverlay />
<div className="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-white/12 to-transparent" />
<div className="max-w-container mx-auto px-6 lg:px-10">
<div className="flex flex-col lg:flex-row lg:items-end lg:justify-between gap-12 mb-20">
<ScrollReveal className="lg:max-w-3xl">
<div className="flex items-center gap-5 mb-8">
<div className="w-16 h-16 rounded-2xl bg-brand-soft flex items-center justify-center text-brand">
<Package className="w-8 h-8" />
</div>
<div>
<SectionLabel>Enterprise Suite</SectionLabel>
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold text-white tracking-tight leading-tight">
</h2>
</div>
</div>
<p className="text-dark-text-secondary text-lg leading-relaxed max-w-2xl">
{category?.description}
</p>
</ScrollReveal>
</div>
<StaggerReveal
className="grid md:grid-cols-2 lg:grid-cols-3 gap-8"
staggerDelay={0.08}
delayChildren={0.05}
>
{enterpriseProducts.map((product) => (
<ProductCard key={product.id} product={product} />
))}
</StaggerReveal>
</div>
</section>
);
}
function SpecializedProductsSection() {
const specializedProducts = PRODUCTS.filter(p => p.categoryId === 'specialized');
const category = PRODUCT_CATEGORIES.find(c => c.id === 'specialized');
if (specializedProducts.length === 0) return null;
return (
<section className="relative py-36 lg:py-44 overflow-hidden bg-ink">
<GrainOverlay />
<div className="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-white/12 to-transparent" />
<div className="max-w-container mx-auto px-6 lg:px-10">
<div className="flex flex-col lg:flex-row lg:items-end lg:justify-between gap-12 mb-20">
<ScrollReveal className="lg:max-w-3xl">
<div className="flex items-center gap-5 mb-8">
<div className="w-16 h-16 rounded-2xl bg-brand-soft flex items-center justify-center text-brand">
<Cpu className="w-8 h-8" />
</div>
<div>
<SectionLabel>Specialized Products</SectionLabel>
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold text-white tracking-tight leading-tight">
</h2>
</div>
</div>
<p className="text-dark-text-secondary text-lg leading-relaxed max-w-2xl">
{category?.description}
</p>
</ScrollReveal>
</div>
<StaggerReveal
className="grid md:grid-cols-2 lg:grid-cols-3 gap-8"
staggerDelay={0.08}
delayChildren={0.05}
>
{specializedProducts.map((product) => (
<ProductCard key={product.id} product={product} />
))}
</StaggerReveal>
</div>
</section>
);
}
function ProductCard({ product }: { product: Product }) {
const productIcon = PRODUCT_ICONS[product.title] || <Package className="w-7 h-7" />;
return (
<a
href={`/products/${product.id}`}
className="group relative block border border-white/10 hover:border-brand/40 bg-ink transition-all duration-600 hover:-translate-y-2 overflow-hidden"
>
<div className="absolute top-0 left-0 right-0 h-1 bg-brand scale-x-0 group-hover:scale-x-100 transition-transform duration-700 origin-left" />
<div className="p-8 lg:p-10 relative z-10">
<div className="flex items-center justify-between mb-8">
<div className="w-14 h-14 rounded-2xl bg-brand-soft flex items-center justify-center text-brand group-hover:scale-110 transition-transform duration-500">
{productIcon}
</div>
<span className="px-3 py-1 text-[11px] font-bold text-brand bg-brand-soft/50">
{product.status}
</span>
</div>
<div className="flex flex-wrap gap-2 mb-6">
{product.tags.slice(0, 2).map(tag => (
<span key={tag} className="px-2.5 py-1 text-xs text-dark-text-muted border border-white/10">
{tag}
</span>
))}
</div>
<h3 className="text-xl lg:text-2xl font-bold text-white mb-4 group-hover:translate-x-2 transition-transform duration-500">
{product.title}
</h3>
<p className="text-dark-text-secondary leading-relaxed mb-8">
{product.description}
</p>
<motion.div
className="inline-flex items-center gap-3 text-sm font-bold text-brand"
whileHover={{ x: 8 }}
transition={{ type: 'spring', stiffness: 300, damping: 30 }}
>
<span></span>
<ArrowRight className="w-4 h-4" />
</motion.div>
</div>
</a>
);
}
function SuiteCombosSection() {
const SUITE_COMBOS = [
{ products: ['erp', 'bi'], label: 'ERP + BI 数据驱动组合', description: '打通业务数据,实现数据驱动决策' },
{ products: ['crm', 'bi'], label: 'CRM + BI 客户洞察组合', description: '深度客户洞察,提升销售转化' },
{ products: ['erp', 'sds'], label: 'ERP + SDS 供应链优化组合', description: '供应链全链路数字化,降本增效' },
{ products: ['cms', 'oa'], label: 'CMS + OA 协同运营组合', description: '内容管理与办公协同一体化' },
];
return (
<section className="relative py-36 lg:py-44 overflow-hidden bg-ink-light">
<GrainOverlay />
<div className="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-white/12 to-transparent" />
<div className="max-w-container mx-auto px-6 lg:px-10">
<ScrollReveal className="text-center max-w-3xl mx-auto mb-20">
<SectionLabel className="justify-center">
<div className="flex items-center gap-5">
<div className="w-14 h-px bg-brand" />
<span className="text-[11px] tracking-[0.4em] uppercase font-bold text-brand">
Recommended Combos
</span>
<div className="w-14 h-px bg-brand" />
</div>
</SectionLabel>
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold text-white tracking-tight leading-tight">
</h2>
<p className="text-dark-text-secondary text-lg mt-6 leading-relaxed">
1+1 2
</p>
</ScrollReveal>
<div className="grid md:grid-cols-2 gap-8">
<StaggerReveal staggerDelay={0.1} delayChildren={0.05}>
{SUITE_COMBOS.map((combo, idx) => {
const products = combo.products
.map(pid => PRODUCTS.find(p => p.id === pid))
.filter(Boolean);
return (
<motion.div
key={idx}
className="group relative p-10 border border-white/10 bg-ink hover:border-brand/40 transition-all duration-500 hover:-translate-y-2"
>
<div className="absolute top-0 left-0 right-0 h-1 bg-brand scale-x-0 group-hover:scale-x-100 transition-transform duration-700 origin-left" />
<div className="flex items-center gap-4 mb-6">
<div className="flex -space-x-3">
{products.map((p, pIdx) => p && (
<div
key={p.id}
className="w-12 h-12 rounded-xl bg-brand-soft border-2 border-ink flex items-center justify-center text-brand"
style={{ zIndex: products.length - pIdx }}
>
{PRODUCT_ICONS[p.title] || <Package className="w-5 h-5" />}
</div>
))}
</div>
<div className="text-xs font-bold text-brand tracking-widest uppercase">
{idx + 1}
</div>
</div>
<h3 className="text-2xl font-bold text-white mb-3">{combo.label}</h3>
<p className="text-dark-text-secondary leading-relaxed mb-6">{combo.description}</p>
<div className="flex flex-wrap gap-2">
{products.map(p => p && (
<span key={p.id} className="px-3 py-1.5 text-xs font-medium text-white bg-ink-light border border-white/10">
{p.title.replace('睿新', '').replace('睿视 ', '')}
</span>
))}
</div>
</motion.div>
);
})}
</StaggerReveal>
</div>
</div>
</section>
);
}
function CTASection() {
return (
<section className="relative py-36 lg:py-44 overflow-hidden bg-ink">
<GrainOverlay />
<div className="absolute inset-0 pointer-events-none">
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[600px] h-[600px] rounded-full opacity-[0.08] blur-3xl bg-brand" />
</div>
<div className="max-w-container mx-auto px-6 lg:px-10 relative z-10">
<ScrollReveal className="max-w-3xl mx-auto text-center">
<SectionLabel className="justify-center">
<div className="flex items-center gap-5">
<div className="w-14 h-px bg-brand" />
<span className="text-[11px] tracking-[0.4em] uppercase font-bold text-brand">
Get Started
</span>
<div className="w-14 h-px bg-brand" />
</div>
</SectionLabel>
<h2 className="text-3xl md:text-4xl lg:text-6xl font-bold text-white tracking-tight leading-tight mb-8">
</h2>
<p className="text-xl text-dark-text-secondary leading-relaxed mb-12 max-w-2xl mx-auto">
</p>
<div className="flex flex-wrap gap-6 justify-center">
<Button size="xl" asChild>
<a href="/contact">
<ArrowRight className="w-5 h-5 ml-2" />
</a>
</Button>
<Button size="xl" variant="outline" asChild>
<a href="/solutions"></a>
</Button>
</div>
</ScrollReveal>
</div>
</section>
);
}
export default function ProductsContentV1() {
return (
<main>
<HeroSection />
<EnterpriseProductsSection />
<SpecializedProductsSection />
<SuiteCombosSection />
<CTASection />
</main>
);
}
@@ -0,0 +1,484 @@
'use client';
import { useRef, useState, useEffect } from 'react';
import { motion, useInView } from 'framer-motion';
import { ScrollReveal, StaggerReveal } from '@/components/ui/scroll-reveal';
import { SectionLabel, EASE_OUT } from '@/components/ui/page-decoration';
import { Button } from '@/components/ui/button';
import { ArrowRight, Package, Cpu, BarChart3, Users, Settings, FileText, CheckCircle2 } from 'lucide-react';
import { PRODUCT_CATEGORIES, type Product } from '@/lib/constants/products';
import { getMockItems } from '@/lib/cms/mock-data';
// CMS 数据源:从 mock-data 读取产品数据
const FALLBACK_PRODUCTS: Product[] = getMockItems('product').map(
(item) => item.data as unknown as Product,
);
import { useReducedMotion } from '@/hooks/use-reduced-motion';
import { cn } from '@/lib/utils';
const EASE_SPRING = { type: 'spring', stiffness: 300, damping: 30 } as const;
const PRODUCT_ICONS: Record<string, React.ReactNode> = {
'睿新ERP管理系统': <Package className="w-7 h-7" />,
'睿视商业智能分析平台': <BarChart3 className="w-7 h-7" />,
'睿新客户关系管理系统': <Users className="w-7 h-7" />,
'睿新协同办公平台': <Settings className="w-7 h-7" />,
'睿新内容管理系统': <FileText className="w-7 h-7" />,
'睿新供应链数字化平台': <Cpu className="w-7 h-7" />,
};
function useCountUp(target: number, start: boolean, duration: number = 2000) {
const [count, setCount] = useState(0);
const prefersReducedMotion = useReducedMotion();
useEffect(() => {
if (!start || prefersReducedMotion) {
setCount(target);
return;
}
const startTime = performance.now();
let animationId: number;
const animate = (currentTime: number) => {
const elapsed = currentTime - startTime;
const progress = Math.min(elapsed / duration, 1);
const easeOutQuart = 1 - Math.pow(1 - progress, 4);
setCount(Math.floor(target * easeOutQuart));
if (progress < 1) {
animationId = requestAnimationFrame(animate);
} else {
setCount(target);
}
};
animationId = requestAnimationFrame(animate);
return () => cancelAnimationFrame(animationId);
}, [target, start, duration, prefersReducedMotion]);
return count;
}
function StatItem({ value, suffix, label, start, delay }: { value: number; suffix: string; label: string; start: boolean; delay: number }) {
const count = useCountUp(value, start, 2000);
const shouldReduceMotion = useReducedMotion();
return (
<motion.div
initial={shouldReduceMotion ? {} : { opacity: 0, y: 20 }}
animate={start ? { opacity: 1, y: 0 } : {}}
transition={{ delay, duration: 0.6, ease: EASE_OUT }}
>
<div className="text-4xl lg:text-5xl font-bold text-ink tracking-tight leading-none mb-2">
{count}{suffix}
</div>
<div className="text-sm text-text-muted">{label}</div>
</motion.div>
);
}
function HeroSection() {
const statsRef = useRef(null);
const isInView = useInView(statsRef, { once: true, margin: '-100px' });
return (
<section className="relative min-h-[85vh] flex items-center overflow-hidden bg-white">
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10 w-full py-24 sm:py-28 md:py-32 lg:py-40">
<div className="max-w-4xl">
<motion.div
initial={{ opacity: 0, x: -50 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 1.1, ease: EASE_OUT }}
className="mb-10"
>
<SectionLabel>Product Matrix</SectionLabel>
</motion.div>
<motion.h1
initial={{ opacity: 0, y: 70 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 1.2, delay: 0.15, ease: EASE_OUT }}
className="text-4xl sm:text-5xl md:text-6xl lg:text-7xl font-bold text-ink leading-[0.92] tracking-tighter mb-8 sm:mb-10 md:mb-12"
>
<div className="block"></div>
<div className="block mt-5">
<span className="relative inline-block">
<span className="text-brand font-extrabold"></span>
<motion.span
className="absolute -bottom-3 left-0 w-full h-[3px] bg-brand"
style={{ transformOrigin: 'left' }}
initial={{ scaleX: 0 }}
animate={{ scaleX: 1 }}
transition={{ duration: 0.8, delay: 1.2, ease: EASE_OUT }}
/>
</span>
</div>
<div className="block mt-5"></div>
</motion.h1>
<motion.p
initial={{ opacity: 0, y: 50 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 1, delay: 0.45, ease: EASE_OUT }}
className="text-lg sm:text-xl lg:text-2xl text-text-secondary max-w-2xl leading-relaxed mb-8 sm:mb-10 md:mb-12"
>
6
</motion.p>
<motion.div
initial={{ opacity: 0, y: 50 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 1, delay: 0.6, ease: EASE_OUT }}
className="flex flex-wrap gap-6"
>
<Button size="xl" asChild>
<a href="/contact">
<ArrowRight className="w-5 h-5 ml-2" />
</a>
</Button>
<Button size="xl" variant="outline" asChild>
<a href="/solutions"></a>
</Button>
</motion.div>
<div ref={statsRef} className="grid grid-cols-3 gap-12 mt-24 pt-16 border-t border-border-primary max-w-2xl">
<StatItem value={6} suffix="+" label="产品线" start={isInView} delay={0.1} />
<StatItem value={100} suffix="%" label="自研率" start={isInView} delay={0.2} />
<div>
<div className="text-4xl lg:text-5xl font-bold text-ink tracking-tight leading-none mb-2">
<span className="text-brand"></span>
</div>
<div className="text-sm text-text-muted"></div>
</div>
</div>
</div>
</div>
</section>
);
}
function ProductCard({ product, index }: { product: Product; index: number }) {
const productIcon = PRODUCT_ICONS[product.title] || <Package className="w-7 h-7" />;
const colorMap: Record<string, { color: string; bg: string; border: string }> = {
enterprise: {
color: '#C41E3A',
bg: 'bg-brand-soft',
border: 'group-hover:border-brand/30',
},
specialized: {
color: '#3b82f6',
bg: 'bg-accent-blue-soft',
border: 'group-hover:border-accent-blue/30',
},
};
const colors = colorMap[product.categoryId] ?? colorMap.enterprise!;
return (
<a
href={`/products/${product.id}`}
className="group relative block transition-all duration-600 hover:bg-bg-secondary overflow-hidden bg-white border border-border-primary hover:border-brand/30"
>
<div
className="absolute top-0 left-0 h-full w-1 scale-y-0 group-hover:scale-y-100 transition-transform duration-700 origin-top"
style={{ backgroundColor: colors.color }}
/>
<div className="relative z-10 p-8 lg:p-10">
<div className="flex items-start justify-between mb-8">
<motion.div
whileHover={{ scale: 1.08, y: -3 }}
transition={EASE_SPRING}
className={cn('w-14 h-14 flex items-center justify-center rounded-2xl', colors.bg)}
style={{ color: colors.color }}
>
{productIcon}
</motion.div>
<span className="text-7xl font-bold text-text-muted/10 group-hover:text-text-muted/20 transition-all duration-500 tracking-tighter leading-none">
{String(index + 1).padStart(2, '0')}
</span>
</div>
<div className="flex flex-wrap gap-2 mb-6">
{product.tags.slice(0, 2).map(tag => (
<span key={tag} className="px-2.5 py-1 text-xs text-text-muted border border-border-primary group-hover:border-border-secondary group-hover:text-text-secondary transition-all duration-300">
{tag}
</span>
))}
</div>
<h3 className="text-xl lg:text-2xl font-bold text-ink mb-4 group-hover:translate-x-1.5 transition-transform duration-500">
{product.title}
</h3>
<p className="text-text-secondary leading-relaxed mb-8 text-[15px]">
{product.description}
</p>
<div className="flex flex-wrap gap-2 mb-8">
{product.features.slice(0, 3).map((feature) => (
<span key={feature} className="flex items-center gap-1.5 text-xs text-text-muted">
<CheckCircle2 className="w-3.5 h-3.5 text-brand/70" />
{feature}
</span>
))}
</div>
<motion.div
className="inline-flex items-center gap-2.5 text-sm font-bold"
style={{ color: colors.color }}
whileHover={{ x: 6 }}
transition={EASE_SPRING}
>
<span></span>
<ArrowRight className="w-4 h-4" />
</motion.div>
</div>
</a>
);
}
function EnterpriseProductsSection({ products }: { products: Product[] }) {
const enterpriseProducts = products.filter(p => p.categoryId === 'enterprise');
const category = PRODUCT_CATEGORIES.find(c => c.id === 'enterprise');
return (
<section className="relative py-20 sm:py-28 md:py-36 lg:py-44 overflow-hidden bg-bg-secondary">
<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" />
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10">
<div className="flex flex-col lg:flex-row lg:items-end lg:justify-between gap-8 sm:gap-10 md:gap-12 mb-16 sm:mb-20 md:mb-20">
<ScrollReveal className="lg:max-w-3xl">
<div className="flex items-center gap-5 mb-8">
<div className="w-16 h-16 rounded-2xl bg-brand-soft flex items-center justify-center text-brand">
<Package className="w-8 h-8" />
</div>
<div>
<SectionLabel>Enterprise Suite</SectionLabel>
<h2 className="text-2xl sm:text-3xl md:text-4xl lg:text-5xl font-bold text-ink tracking-tight leading-tight">
</h2>
</div>
</div>
<p className="text-text-secondary text-lg leading-relaxed max-w-2xl">
{category?.description}
</p>
</ScrollReveal>
</div>
<StaggerReveal
className="grid md:grid-cols-2 lg:grid-cols-3 gap-px bg-border-primary"
staggerDelay={0.08}
delayChildren={0.05}
>
{enterpriseProducts.map((product, index) => (
<ProductCard key={product.id} product={product} index={index} />
))}
</StaggerReveal>
</div>
</section>
);
}
function SpecializedProductsSection({ products }: { products: Product[] }) {
const specializedProducts = products.filter(p => p.categoryId === 'specialized');
const category = PRODUCT_CATEGORIES.find(c => c.id === 'specialized');
if (specializedProducts.length === 0) return null;
return (
<section className="relative py-20 sm:py-28 md:py-36 lg:py-44 overflow-hidden bg-white">
<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" />
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10">
<div className="flex flex-col lg:flex-row lg:items-end lg:justify-between gap-8 sm:gap-10 md:gap-12 mb-16 sm:mb-20 md:mb-20">
<ScrollReveal className="lg:max-w-3xl">
<div className="flex items-center gap-5 mb-8">
<div className="w-16 h-16 rounded-2xl bg-accent-blue-soft flex items-center justify-center text-accent-blue">
<Cpu className="w-8 h-8" />
</div>
<div>
<SectionLabel>Specialized Products</SectionLabel>
<h2 className="text-2xl sm:text-3xl md:text-4xl lg:text-5xl font-bold text-ink tracking-tight leading-tight">
</h2>
</div>
</div>
<p className="text-text-secondary text-lg leading-relaxed max-w-2xl">
{category?.description}
</p>
</ScrollReveal>
</div>
<StaggerReveal
className="grid md:grid-cols-2 lg:grid-cols-3 gap-px bg-border-primary"
staggerDelay={0.08}
delayChildren={0.05}
>
{specializedProducts.map((product, index) => (
<ProductCard key={product.id} product={product} index={index} />
))}
</StaggerReveal>
</div>
</section>
);
}
function SuiteCombosSection({ products }: { products: Product[] }) {
const SUITE_COMBOS = [
{ products: ['erp', 'bi'], label: 'ERP + BI 数据驱动组合', description: '打通业务数据,实现数据驱动决策' },
{ products: ['crm', 'bi'], label: 'CRM + BI 客户洞察组合', description: '深度客户洞察,提升销售转化' },
{ products: ['erp', 'sds'], label: 'ERP + SDS 供应链优化组合', description: '供应链全链路数字化,降本增效' },
{ products: ['cms', 'oa'], label: 'CMS + OA 协同运营组合', description: '内容管理与办公协同一体化' },
];
return (
<section className="relative py-20 sm:py-28 md:py-36 lg:py-44 overflow-hidden bg-bg-secondary">
<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" />
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10">
<ScrollReveal className="text-center max-w-3xl mx-auto mb-16 sm:mb-20 md:mb-20">
<div className="flex justify-center mb-7">
<div className="flex items-center gap-5">
<div className="w-14 h-px bg-brand" />
<span className="text-[11px] tracking-[0.4em] uppercase font-bold text-brand">
Recommended Combos
</span>
<div className="w-14 h-px bg-brand" />
</div>
</div>
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold text-ink tracking-tight leading-tight">
</h2>
<p className="text-text-secondary text-lg mt-6 leading-relaxed">
1+1 2
</p>
</ScrollReveal>
<StaggerReveal
className="grid md:grid-cols-2 gap-px bg-border-primary"
staggerDelay={0.1}
delayChildren={0.05}
>
{SUITE_COMBOS.map((combo, idx) => {
const comboProducts = combo.products
.map(pid => products.find(p => p.id === pid))
.filter(Boolean);
return (
<a
key={idx}
href="/contact"
className="group relative p-10 lg:p-12 bg-white hover:bg-bg-secondary transition-all duration-500 overflow-hidden"
>
<div className="absolute top-0 left-0 right-0 h-1 bg-brand scale-x-0 group-hover:scale-x-100 transition-transform duration-700 origin-left" />
<div className="relative z-10">
<div className="flex items-center gap-4 mb-6">
<div className="flex -space-x-3">
{comboProducts.map((p, pIdx) => p && (
<div
key={pIdx}
className="w-10 h-10 rounded-xl bg-brand-soft flex items-center justify-center text-brand border-2 border-white"
style={{ zIndex: 10 - pIdx }}
>
{PRODUCT_ICONS[p.title] || <Package className="w-5 h-5" />}
</div>
))}
</div>
<span className="px-3 py-1 text-[11px] font-bold text-brand bg-brand-soft/50">
</span>
</div>
<h3 className="text-xl lg:text-2xl font-bold text-ink mb-3 group-hover:translate-x-1.5 transition-transform duration-500">
{combo.label}
</h3>
<p className="text-text-secondary leading-relaxed mb-6">
{combo.description}
</p>
<motion.div
className="inline-flex items-center gap-2.5 text-sm font-bold text-brand"
whileHover={{ x: 6 }}
transition={EASE_SPRING}
>
<span></span>
<ArrowRight className="w-4 h-4" />
</motion.div>
</div>
</a>
);
})}
</StaggerReveal>
</div>
</section>
);
}
function CTASection() {
return (
<section className="relative py-20 sm:py-28 md:py-36 lg:py-44 overflow-hidden bg-white">
<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" />
<div className="max-w-container mx-auto px-6 lg:px-10 relative z-10">
<ScrollReveal className="text-center max-w-3xl mx-auto">
<div className="flex justify-center mb-7">
<div className="flex items-center gap-5">
<div className="w-14 h-px bg-brand" />
<span className="text-[11px] tracking-[0.4em] uppercase font-bold text-brand">
Get Started
</span>
<div className="w-14 h-px bg-brand" />
</div>
</div>
<h2 className="text-2xl sm:text-3xl md:text-4xl lg:text-5xl font-bold text-ink tracking-tight leading-tight mb-6 sm:mb-8">
<br />
<span className="text-brand"></span>
</h2>
<p className="text-xl text-text-secondary mb-12 max-w-2xl mx-auto leading-relaxed">
</p>
<div className="flex flex-wrap justify-center gap-6">
<Button size="xl" asChild>
<a href="/contact">
<ArrowRight className="w-5 h-5 ml-2" />
</a>
</Button>
<Button size="xl" variant="outline" asChild>
<a href="/solutions"></a>
</Button>
</div>
</ScrollReveal>
</div>
</section>
);
}
export default function ProductsContentV2({ products: productsProp }: { products?: Product[] }) {
const products = productsProp ?? FALLBACK_PRODUCTS;
return (
<main>
<HeroSection />
<EnterpriseProductsSection products={products} />
<SpecializedProductsSection products={products} />
<SuiteCombosSection products={products} />
<CTASection />
</main>
);
}
@@ -0,0 +1,476 @@
'use client';
import { useRef, useState, useEffect } from 'react';
import { motion, useInView } from 'framer-motion';
import { ScrollReveal, StaggerReveal } from '@/components/ui/scroll-reveal';
import { SectionLabel, EASE_OUT } from '@/components/ui/page-decoration';
import { Button } from '@/components/ui/button';
import { ArrowRight, Package, Cpu, BarChart3, Users, Settings, FileText, CheckCircle2 } from 'lucide-react';
import type { Product } from '@/lib/constants/products';
import { useReducedMotion } from '@/hooks/use-reduced-motion';
import { cn } from '@/lib/utils';
const EASE_SPRING = { type: 'spring', stiffness: 300, damping: 30 } as const;
const PRODUCT_ICONS: Record<string, React.ReactNode> = {
'睿新ERP管理系统': <Package className="w-7 h-7" />,
'睿视商业智能分析平台': <BarChart3 className="w-7 h-7" />,
'睿新客户关系管理系统': <Users className="w-7 h-7" />,
'睿新协同办公平台': <Settings className="w-7 h-7" />,
'睿新内容管理系统': <FileText className="w-7 h-7" />,
'睿新供应链数字化平台': <Cpu className="w-7 h-7" />,
};
function useCountUp(target: number, start: boolean, duration: number = 2000) {
const [count, setCount] = useState(0);
const prefersReducedMotion = useReducedMotion();
useEffect(() => {
if (!start || prefersReducedMotion) {
setCount(target);
return;
}
const startTime = performance.now();
let animationId: number;
const animate = (currentTime: number) => {
const elapsed = currentTime - startTime;
const progress = Math.min(elapsed / duration, 1);
const easeOutQuart = 1 - Math.pow(1 - progress, 4);
setCount(Math.floor(target * easeOutQuart));
if (progress < 1) {
animationId = requestAnimationFrame(animate);
} else {
setCount(target);
}
};
animationId = requestAnimationFrame(animate);
return () => cancelAnimationFrame(animationId);
}, [target, start, duration, prefersReducedMotion]);
return count;
}
function StatItem({ value, suffix, label, start, delay }: { value: number; suffix: string; label: string; start: boolean; delay: number }) {
const count = useCountUp(value, start, 2000);
const shouldReduceMotion = useReducedMotion();
return (
<motion.div
initial={shouldReduceMotion ? {} : { opacity: 0, y: 20 }}
animate={start ? { opacity: 1, y: 0 } : {}}
transition={{ delay, duration: 0.6, ease: EASE_OUT }}
>
<div className="text-4xl lg:text-5xl font-bold text-ink tracking-tight leading-none mb-2">
{count}{suffix}
</div>
<div className="text-sm text-text-muted">{label}</div>
</motion.div>
);
}
function HeroSection() {
const statsRef = useRef(null);
const isInView = useInView(statsRef, { once: true, margin: '-100px' });
return (
<section className="relative min-h-[85vh] flex items-center overflow-hidden bg-white">
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10 w-full py-24 sm:py-28 md:py-32 lg:py-40">
<div className="max-w-4xl">
<motion.div
initial={{ opacity: 0, x: -50 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 1.1, ease: EASE_OUT }}
className="mb-10"
>
<SectionLabel>Product Matrix</SectionLabel>
</motion.div>
<motion.h1
initial={{ opacity: 0, y: 70 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 1.2, delay: 0.15, ease: EASE_OUT }}
className="text-4xl sm:text-5xl md:text-6xl lg:text-7xl font-bold text-ink leading-[0.92] tracking-tighter mb-8 sm:mb-10 md:mb-12"
>
<div className="block"></div>
<div className="block mt-5">
<span className="relative inline-block">
<span className="text-brand font-extrabold"></span>
<motion.span
className="absolute -bottom-3 left-0 w-full h-[3px] bg-brand"
style={{ transformOrigin: 'left' }}
initial={{ scaleX: 0 }}
animate={{ scaleX: 1 }}
transition={{ duration: 0.8, delay: 1.2, ease: EASE_OUT }}
/>
</span>
</div>
<div className="block mt-5"></div>
</motion.h1>
<motion.p
initial={{ opacity: 0, y: 50 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 1, delay: 0.45, ease: EASE_OUT }}
className="text-lg sm:text-xl lg:text-2xl text-text-secondary max-w-2xl leading-relaxed mb-8 sm:mb-10 md:mb-12"
>
6
</motion.p>
<motion.div
initial={{ opacity: 0, y: 50 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 1, delay: 0.6, ease: EASE_OUT }}
className="flex flex-wrap gap-6"
>
<Button size="xl" asChild>
<a href="/contact">
<ArrowRight className="w-5 h-5 ml-2" />
</a>
</Button>
<Button size="xl" variant="outline" asChild>
<a href="/solutions"></a>
</Button>
</motion.div>
<div ref={statsRef} className="grid grid-cols-3 gap-12 mt-24 pt-16 border-t border-border-primary max-w-2xl">
<StatItem value={6} suffix="+" label="产品线" start={isInView} delay={0.1} />
<StatItem value={100} suffix="%" label="自研率" start={isInView} delay={0.2} />
<div>
<div className="text-4xl lg:text-5xl font-bold text-ink tracking-tight leading-none mb-2">
<span className="text-brand"></span>
</div>
<div className="text-sm text-text-muted"></div>
</div>
</div>
</div>
</div>
</section>
);
}
function ProductCard({ product, index }: { product: Product; index: number }) {
const productIcon = PRODUCT_ICONS[product.title] || <Package className="w-7 h-7" />;
const colorMap: Record<string, { color: string; bg: string; border: string }> = {
enterprise: {
color: '#C41E3A',
bg: 'bg-brand-soft',
border: 'group-hover:border-brand/30',
},
specialized: {
color: '#3b82f6',
bg: 'bg-accent-blue-soft',
border: 'group-hover:border-accent-blue/30',
},
};
const colors = colorMap[product.categoryId] ?? colorMap.enterprise!;
return (
<a
href={`/products/${product.id}`}
className="group relative block transition-all duration-600 hover:bg-bg-secondary overflow-hidden bg-white border border-border-primary hover:border-brand/30"
>
<div
className="absolute top-0 left-0 h-full w-1 scale-y-0 group-hover:scale-y-100 transition-transform duration-700 origin-top"
style={{ backgroundColor: colors.color }}
/>
<div className="relative z-10 p-8 lg:p-10">
<div className="flex items-start justify-between mb-8">
<motion.div
whileHover={{ scale: 1.08, y: -3 }}
transition={EASE_SPRING}
className={cn('w-14 h-14 flex items-center justify-center rounded-2xl', colors.bg)}
style={{ color: colors.color }}
>
{productIcon}
</motion.div>
<span className="text-7xl font-bold text-text-muted/10 group-hover:text-text-muted/20 transition-all duration-500 tracking-tighter leading-none">
{String(index + 1).padStart(2, '0')}
</span>
</div>
<div className="flex flex-wrap gap-2 mb-6">
{product.tags.slice(0, 2).map(tag => (
<span key={tag} className="px-2.5 py-1 text-xs text-text-muted border border-border-primary group-hover:border-border-secondary group-hover:text-text-secondary transition-all duration-300">
{tag}
</span>
))}
</div>
<h3 className="text-xl lg:text-2xl font-bold text-ink mb-4 group-hover:translate-x-1.5 transition-transform duration-500">
{product.title}
</h3>
<p className="text-text-secondary leading-relaxed mb-8 text-[15px]">
{product.description}
</p>
<div className="flex flex-wrap gap-2 mb-8">
{product.features.slice(0, 3).map((feature) => (
<span key={feature} className="flex items-center gap-1.5 text-xs text-text-muted">
<CheckCircle2 className="w-3.5 h-3.5 text-brand/70" />
{feature}
</span>
))}
</div>
<motion.div
className="inline-flex items-center gap-2.5 text-sm font-bold"
style={{ color: colors.color }}
whileHover={{ x: 6 }}
transition={EASE_SPRING}
>
<span></span>
<ArrowRight className="w-4 h-4" />
</motion.div>
</div>
</a>
);
}
function EnterpriseProductsSection({ products }: { products: Product[] }) {
const enterpriseProducts = products.filter(p => p.categoryId === 'enterprise');
return (
<section className="relative py-20 sm:py-28 md:py-36 lg:py-44 overflow-hidden bg-bg-secondary">
<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" />
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10">
<div className="flex flex-col lg:flex-row lg:items-end lg:justify-between gap-8 sm:gap-10 md:gap-12 mb-16 sm:mb-20 md:mb-20">
<ScrollReveal className="lg:max-w-3xl">
<div className="flex items-center gap-5 mb-8">
<div className="w-16 h-16 rounded-2xl bg-brand-soft flex items-center justify-center text-brand">
<Package className="w-8 h-8" />
</div>
<div>
<SectionLabel>Enterprise Suite</SectionLabel>
<h2 className="text-2xl sm:text-3xl md:text-4xl lg:text-5xl font-bold text-ink tracking-tight leading-tight">
</h2>
</div>
</div>
<p className="text-text-secondary text-lg leading-relaxed max-w-2xl">
ERP到协同办公
</p>
</ScrollReveal>
</div>
<StaggerReveal
className="grid md:grid-cols-2 lg:grid-cols-3 gap-px bg-border-primary"
staggerDelay={0.08}
delayChildren={0.05}
>
{enterpriseProducts.map((product, index) => (
<ProductCard key={product.id} product={product} index={index} />
))}
</StaggerReveal>
</div>
</section>
);
}
function SpecializedProductsSection({ products }: { products: Product[] }) {
const specializedProducts = products.filter(p => p.categoryId === 'specialized');
if (specializedProducts.length === 0) return null;
return (
<section className="relative py-20 sm:py-28 md:py-36 lg:py-44 overflow-hidden bg-white">
<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" />
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10">
<div className="flex flex-col lg:flex-row lg:items-end lg:justify-between gap-8 sm:gap-10 md:gap-12 mb-16 sm:mb-20 md:mb-20">
<ScrollReveal className="lg:max-w-3xl">
<div className="flex items-center gap-5 mb-8">
<div className="w-16 h-16 rounded-2xl bg-accent-blue-soft flex items-center justify-center text-accent-blue">
<Cpu className="w-8 h-8" />
</div>
<div>
<SectionLabel>Specialized Products</SectionLabel>
<h2 className="text-2xl sm:text-3xl md:text-4xl lg:text-5xl font-bold text-ink tracking-tight leading-tight">
</h2>
</div>
</div>
<p className="text-text-secondary text-lg leading-relaxed max-w-2xl">
</p>
</ScrollReveal>
</div>
<StaggerReveal
className="grid md:grid-cols-2 lg:grid-cols-3 gap-px bg-border-primary"
staggerDelay={0.08}
delayChildren={0.05}
>
{specializedProducts.map((product, index) => (
<ProductCard key={product.id} product={product} index={index} />
))}
</StaggerReveal>
</div>
</section>
);
}
function SuiteCombosSection({ products }: { products: Product[] }) {
const SUITE_COMBOS = [
{ products: ['erp', 'bi'], label: 'ERP + BI 数据驱动组合', description: '打通业务数据,实现数据驱动决策' },
{ products: ['crm', 'bi'], label: 'CRM + BI 客户洞察组合', description: '深度客户洞察,提升销售转化' },
{ products: ['erp', 'sds'], label: 'ERP + SDS 供应链优化组合', description: '供应链全链路数字化,降本增效' },
{ products: ['cms', 'oa'], label: 'CMS + OA 协同运营组合', description: '内容管理与办公协同一体化' },
];
return (
<section className="relative py-20 sm:py-28 md:py-36 lg:py-44 overflow-hidden bg-bg-secondary">
<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" />
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10">
<ScrollReveal className="text-center max-w-3xl mx-auto mb-16 sm:mb-20 md:mb-20">
<div className="flex justify-center mb-7">
<div className="flex items-center gap-5">
<div className="w-14 h-px bg-brand" />
<span className="text-[11px] tracking-[0.4em] uppercase font-bold text-brand">
Recommended Combos
</span>
<div className="w-14 h-px bg-brand" />
</div>
</div>
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold text-ink tracking-tight leading-tight">
</h2>
<p className="text-text-secondary text-lg mt-6 leading-relaxed">
1+1 2
</p>
</ScrollReveal>
<StaggerReveal
className="grid md:grid-cols-2 gap-px bg-border-primary"
staggerDelay={0.1}
delayChildren={0.05}
>
{SUITE_COMBOS.map((combo, idx) => {
const comboProducts = combo.products
.map(pid => products.find(p => p.id === pid))
.filter(Boolean);
return (
<a
key={idx}
href="/contact"
className="group relative p-10 lg:p-12 bg-white hover:bg-bg-secondary transition-all duration-500 overflow-hidden"
>
<div className="absolute top-0 left-0 right-0 h-1 bg-brand scale-x-0 group-hover:scale-x-100 transition-transform duration-700 origin-left" />
<div className="relative z-10">
<div className="flex items-center gap-4 mb-6">
<div className="flex -space-x-3">
{comboProducts.map((p, pIdx) => p && (
<div
key={pIdx}
className="w-10 h-10 rounded-xl bg-brand-soft flex items-center justify-center text-brand border-2 border-white"
style={{ zIndex: 10 - pIdx }}
>
{PRODUCT_ICONS[p.title] || <Package className="w-5 h-5" />}
</div>
))}
</div>
<span className="px-3 py-1 text-[11px] font-bold text-brand bg-brand-soft/50">
</span>
</div>
<h3 className="text-xl lg:text-2xl font-bold text-ink mb-3 group-hover:translate-x-1.5 transition-transform duration-500">
{combo.label}
</h3>
<p className="text-text-secondary leading-relaxed mb-6">
{combo.description}
</p>
<motion.div
className="inline-flex items-center gap-2.5 text-sm font-bold text-brand"
whileHover={{ x: 6 }}
transition={EASE_SPRING}
>
<span></span>
<ArrowRight className="w-4 h-4" />
</motion.div>
</div>
</a>
);
})}
</StaggerReveal>
</div>
</section>
);
}
function CTASection() {
return (
<section className="relative py-20 sm:py-28 md:py-36 lg:py-44 overflow-hidden bg-white">
<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" />
<div className="max-w-container mx-auto px-6 lg:px-10 relative z-10">
<ScrollReveal className="text-center max-w-3xl mx-auto">
<div className="flex justify-center mb-7">
<div className="flex items-center gap-5">
<div className="w-14 h-px bg-brand" />
<span className="text-[11px] tracking-[0.4em] uppercase font-bold text-brand">
Get Started
</span>
<div className="w-14 h-px bg-brand" />
</div>
</div>
<h2 className="text-2xl sm:text-3xl md:text-4xl lg:text-5xl font-bold text-ink tracking-tight leading-tight mb-6 sm:mb-8">
<br />
<span className="text-brand"></span>
</h2>
<p className="text-xl text-text-secondary mb-12 max-w-2xl mx-auto leading-relaxed">
</p>
<div className="flex flex-wrap justify-center gap-6">
<Button size="xl" asChild>
<a href="/contact">
<ArrowRight className="w-5 h-5 ml-2" />
</a>
</Button>
<Button size="xl" variant="outline" asChild>
<a href="/solutions"></a>
</Button>
</div>
</ScrollReveal>
</div>
</section>
);
}
export default function ProductsContentV3({ products: productsProp }: { products?: Product[] }) {
const products = productsProp ?? [];
return (
<main>
<HeroSection />
<EnterpriseProductsSection products={products} />
<SpecializedProductsSection products={products} />
<SuiteCombosSection products={products} />
<CTASection />
</main>
);
}
@@ -0,0 +1,548 @@
'use client';
import { useRef } from 'react';
import { motion } from 'framer-motion';
import { ScrollReveal, StaggerReveal } from '@/components/ui/scroll-reveal';
import { Button } from '@/components/ui/button';
import { ArrowRight, CheckCircle2, Zap, Clock, Users, Award, TrendingUp, BarChart3, Code, Lightbulb, Puzzle } from 'lucide-react';
import { cn } from '@/lib/utils';
import type { Service, CaseStudy, DataProof } from '@/lib/constants/services';
const EASE_OUT = [0.22, 1, 0.36, 1] as const;
const ICON_MAP: Record<string, React.ReactNode> = {
Code: <Code className="w-7 h-7" />,
BarChart3: <BarChart3 className="w-7 h-7" />,
Lightbulb: <Lightbulb className="w-7 h-7" />,
Puzzle: <Puzzle className="w-7 h-7" />,
};
const FEATURE_ICONS = [Zap, Clock, Users, Award, TrendingUp];
function GrainOverlay() {
return (
<div
className="absolute inset-0 pointer-events-none opacity-[0.03] mix-blend-overlay"
style={{
backgroundImage: `url("data:image/svg+xml,%3Csvg viewBox='0 0 512 512' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E")`,
backgroundRepeat: 'repeat',
backgroundSize: '300px 300px',
}}
/>
);
}
function SectionLabel({ children, className = '' }: { children: React.ReactNode; className?: string }) {
return (
<div className={cn('flex items-center gap-5 mb-7', className)}>
<div className="w-14 h-px bg-brand" />
<span className="text-[11px] tracking-[0.4em] uppercase font-bold text-brand">
{children}
</span>
</div>
);
}
interface DetailHeroProps {
service: Service;
}
function DetailHero({ service }: DetailHeroProps) {
const containerRef = useRef<HTMLDivElement>(null);
return (
<section
ref={containerRef}
className="relative min-h-[75vh] flex items-center overflow-hidden bg-ink"
>
<GrainOverlay />
<div className="absolute inset-0 pointer-events-none">
<div
className="absolute top-0 right-0 w-[45%] h-full"
style={{
background: 'linear-gradient(135deg, transparent 20%, rgba(196, 30, 58, 0.09) 100%)',
clipPath: 'polygon(30% 0, 100% 0, 100% 100%, 0% 100%)',
}}
/>
<div className="absolute bottom-0 left-0 w-full h-2/5 bg-gradient-to-t from-ink to-transparent" />
<div className="absolute top-32 right-[25%] w-[400px] h-[400px] rounded-full opacity-12 blur-3xl bg-brand" />
</div>
<div className="max-w-container mx-auto px-6 lg:px-10 relative z-10 w-full py-32 lg:py-40">
<div className="max-w-4xl">
<motion.div
initial={{ opacity: 0, x: -50 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 1.1, ease: EASE_OUT }}
className="mb-10"
>
<SectionLabel>Professional Service</SectionLabel>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 60 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 1, delay: 0.15, ease: EASE_OUT }}
className="flex items-center gap-6 mb-10"
>
<div className="w-20 h-20 rounded-2xl bg-brand-soft flex items-center justify-center text-brand">
{ICON_MAP[service.icon] || <Code className="w-10 h-10" />}
</div>
<div>
<div className="text-dark-text-muted text-sm tracking-widest uppercase">
Service
</div>
</div>
</motion.div>
<motion.h1
initial={{ opacity: 0, y: 70 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 1.2, delay: 0.25, ease: EASE_OUT }}
className="text-5xl sm:text-6xl md:text-7xl lg:text-8xl font-bold text-white leading-[0.85] tracking-tighter mb-12"
>
{service.title}
</motion.h1>
<motion.p
initial={{ opacity: 0, y: 50 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 1, delay: 0.45, ease: EASE_OUT }}
className="text-xl lg:text-2xl text-dark-text-secondary max-w-3xl leading-relaxed mb-12"
>
{service.description}
</motion.p>
<motion.div
initial={{ opacity: 0, y: 50 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 1, delay: 0.6, ease: EASE_OUT }}
className="flex flex-wrap gap-6"
>
<Button size="xl" asChild>
<a href="/contact">
<ArrowRight className="w-5 h-5 ml-2" />
</a>
</Button>
<Button size="xl" variant="outline" asChild>
<a href="/cases"></a>
</Button>
</motion.div>
</div>
</div>
</section>
);
}
interface OverviewSectionProps {
service: Service;
}
function OverviewSection({ service }: OverviewSectionProps) {
return (
<section className="relative py-36 lg:py-44 overflow-hidden bg-ink-light">
<GrainOverlay />
<div className="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-white/12 to-transparent" />
<div className="max-w-container mx-auto px-6 lg:px-10">
<div className="grid lg:grid-cols-12 gap-16">
<ScrollReveal className="lg:col-span-4">
<SectionLabel>Overview</SectionLabel>
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold text-white tracking-tight leading-tight">
</h2>
</ScrollReveal>
<ScrollReveal delay={0.1} className="lg:col-span-8">
<p className="text-xl text-dark-text-secondary leading-relaxed">
{service.overview}
</p>
</ScrollReveal>
</div>
</div>
</section>
);
}
interface FeaturesSectionProps {
service: Service;
}
function FeaturesSection({ service }: FeaturesSectionProps) {
return (
<section className="relative py-36 lg:py-44 overflow-hidden bg-ink">
<GrainOverlay />
<div className="max-w-container mx-auto px-6 lg:px-10">
<ScrollReveal className="mb-20 max-w-3xl">
<SectionLabel>Core Capabilities</SectionLabel>
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold text-white tracking-tight leading-tight mb-8">
</h2>
<p className="text-dark-text-secondary leading-relaxed text-lg">
</p>
</ScrollReveal>
<StaggerReveal
className="grid md:grid-cols-2 lg:grid-cols-3 gap-px bg-white/10"
staggerDelay={0.08}
delayChildren={0.05}
>
{service.features.map((feature, index) => {
const Icon = FEATURE_ICONS[index % FEATURE_ICONS.length]!;
const [title, desc] = feature.split('');
return (
<div
key={index}
className="group relative p-10 lg:p-12 transition-all duration-500 hover:bg-white/[0.02] bg-ink"
>
<div className="absolute top-0 left-0 h-full w-[3px] scale-y-0 group-hover:scale-y-100 transition-transform duration-700 origin-top bg-brand" />
<div className="relative z-10">
<div className="w-14 h-14 rounded-xl bg-brand-soft flex items-center justify-center text-brand mb-8 group-hover:scale-110 transition-transform duration-500">
<Icon className="w-7 h-7" strokeWidth={1.8} />
</div>
<h3 className="text-xl font-bold text-white mb-4 group-hover:translate-x-2 transition-transform duration-500">
{desc ? title : feature.slice(0, 10)}
</h3>
<p className="text-dark-text-secondary leading-relaxed">
{desc || feature}
</p>
</div>
</div>
);
})}
</StaggerReveal>
</div>
</section>
);
}
interface BenefitsSectionProps {
service: Service;
}
function BenefitsSection({ service }: BenefitsSectionProps) {
return (
<section className="relative py-36 lg:py-44 overflow-hidden bg-ink-light">
<GrainOverlay />
<div className="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-white/12 to-transparent" />
<div className="max-w-container mx-auto px-6 lg:px-10">
<div className="flex flex-col lg:flex-row lg:items-end lg:justify-between gap-12 mb-20">
<ScrollReveal className="lg:max-w-3xl">
<SectionLabel>Why Choose Us</SectionLabel>
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold text-white tracking-tight leading-tight">
</h2>
</ScrollReveal>
<ScrollReveal delay={0.1}>
<p className="text-dark-text-secondary max-w-md leading-relaxed text-lg">
</p>
</ScrollReveal>
</div>
<StaggerReveal
className="grid md:grid-cols-2 gap-6"
staggerDelay={0.08}
delayChildren={0.05}
>
{service.benefits.map((benefit, index) => (
<div
key={index}
className="group flex items-start gap-5 p-8 lg:p-10 border border-white/10 hover:border-brand/40 bg-ink/50 transition-all duration-500 hover:-translate-y-1"
>
<CheckCircle2 className="w-7 h-7 text-brand shrink-0 mt-0.5 group-hover:scale-110 transition-transform duration-300" />
<p className="text-lg text-white leading-relaxed font-medium">
{benefit}
</p>
</div>
))}
</StaggerReveal>
</div>
</section>
);
}
interface ProcessSectionProps {
service: Service;
}
function ProcessSection({ service }: ProcessSectionProps) {
if (!service.process || service.process.length === 0) return null;
return (
<section className="relative py-36 lg:py-44 overflow-hidden bg-ink">
<GrainOverlay />
<div className="max-w-container mx-auto px-6 lg:px-10">
<ScrollReveal className="mb-24 max-w-3xl text-center mx-auto">
<SectionLabel className="justify-center">
<div className="flex items-center gap-5">
<div className="w-14 h-px bg-brand" />
<span className="text-[11px] tracking-[0.4em] uppercase font-bold text-brand">
Service Process
</span>
<div className="w-14 h-px bg-brand" />
</div>
</SectionLabel>
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold text-white tracking-tight leading-tight mb-8">
</h2>
<p className="text-dark-text-secondary leading-relaxed text-lg">
</p>
</ScrollReveal>
<div className="relative max-w-4xl mx-auto">
<StaggerReveal
className="space-y-6"
staggerDelay={0.12}
delayChildren={0.1}
>
{service.process.map((step, index) => {
const [title, desc] = step.split('');
return (
<div
key={index}
className="relative flex gap-8 group"
>
<div className="relative z-10 shrink-0">
<div className="w-20 h-20 rounded-2xl bg-ink-light border-2 border-white/10 flex items-center justify-center group-hover:border-brand group-hover:scale-110 transition-all duration-500">
<span className="text-2xl font-bold text-white group-hover:text-brand transition-colors">
{String(index + 1).padStart(2, '0')}
</span>
</div>
{index < service.process!.length - 1 && (
<div className="absolute top-20 left-1/2 -translate-x-1/2 w-px h-6 bg-white/10" />
)}
</div>
<div className="flex-1 py-5">
<h4 className="text-2xl font-bold text-white mb-3 group-hover:translate-x-2 transition-transform duration-500">
{desc ? title : step.slice(0, 15)}
</h4>
<p className="text-dark-text-secondary leading-relaxed text-lg">
{desc || step}
</p>
</div>
</div>
);
})}
</StaggerReveal>
</div>
</div>
</section>
);
}
interface DataProofsSectionProps {
dataProofs?: DataProof[];
}
function DataProofsSection({ dataProofs = [] }: DataProofsSectionProps) {
if (dataProofs.length === 0) return null;
return (
<section className="relative py-36 lg:py-44 overflow-hidden bg-ink-light">
<GrainOverlay />
<div className="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-white/12 to-transparent" />
<div className="max-w-container mx-auto px-6 lg:px-10">
<ScrollReveal className="mb-20 text-center max-w-3xl mx-auto">
<SectionLabel className="justify-center">
<div className="flex items-center gap-5">
<div className="w-14 h-px bg-brand" />
<span className="text-[11px] tracking-[0.4em] uppercase font-bold text-brand">
Data Speaks
</span>
<div className="w-14 h-px bg-brand" />
</div>
</SectionLabel>
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold text-white tracking-tight leading-tight mb-8">
</h2>
<p className="text-dark-text-secondary leading-relaxed text-lg">
使
</p>
</ScrollReveal>
<StaggerReveal
className="grid md:grid-cols-2 lg:grid-cols-3 gap-6 max-w-5xl mx-auto"
staggerDelay={0.1}
delayChildren={0.05}
>
{dataProofs.map((proof, index) => (
<div
key={proof.metric}
className="group relative p-10 lg:p-12 border border-white/10 hover:border-brand/40 bg-ink transition-all duration-500 hover:-translate-y-2"
>
{index === 0 && (
<div className="absolute top-0 left-0 right-0 h-1 bg-brand" />
)}
<div className="relative z-10">
<div className="text-5xl lg:text-6xl font-bold text-white mb-5 group-hover:text-brand transition-colors duration-500">
{proof.value}
</div>
<p className="text-xl font-bold text-white mb-3">{proof.metric}</p>
<p className="text-dark-text-muted leading-relaxed">{proof.description}</p>
</div>
</div>
))}
</StaggerReveal>
</div>
</section>
);
}
interface CaseStudiesSectionProps {
caseStudies?: CaseStudy[];
}
function CaseStudiesSection({ caseStudies = [] }: CaseStudiesSectionProps) {
if (caseStudies.length === 0) return null;
return (
<section className="relative py-36 lg:py-44 overflow-hidden bg-ink">
<GrainOverlay />
<div className="max-w-container mx-auto px-6 lg:px-10">
<div className="flex flex-col lg:flex-row lg:items-end lg:justify-between gap-12 mb-20">
<ScrollReveal className="lg:max-w-3xl">
<SectionLabel>Case Studies</SectionLabel>
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold text-white tracking-tight leading-tight">
</h2>
</ScrollReveal>
<ScrollReveal delay={0.1}>
<p className="text-dark-text-secondary max-w-md leading-relaxed text-lg">
</p>
</ScrollReveal>
</div>
<StaggerReveal
className="grid md:grid-cols-2 lg:grid-cols-3 gap-6"
staggerDelay={0.1}
delayChildren={0.05}
>
{caseStudies.map((study) => (
<div
key={study.client}
className="group relative block overflow-hidden border border-white/10 hover:border-brand/40 bg-ink-light transition-all duration-500 hover:-translate-y-2"
>
<div className="aspect-[4/3] bg-gradient-to-br from-ink to-ink-light relative overflow-hidden">
<div className="absolute inset-0 flex items-center justify-center">
<span className="text-6xl font-bold text-white/[0.08]">
{study.client.charAt(0)}
</span>
</div>
<div className="absolute top-5 left-5">
<span className="px-3 py-1.5 text-xs font-bold text-brand bg-brand-soft backdrop-blur-sm">
{study.industry}
</span>
</div>
</div>
<div className="p-8">
<h3 className="text-xl font-bold text-white mb-3 group-hover:translate-x-2 transition-transform duration-500">
{study.client}
</h3>
<p className="text-dark-text-secondary text-sm leading-relaxed mb-6">
{study.challenge}
</p>
<div className="pt-6 border-t border-white/10">
<div className="text-2xl font-bold text-brand mb-1">{study.result}</div>
<div className="text-xs text-dark-text-muted"></div>
</div>
</div>
</div>
))}
</StaggerReveal>
</div>
</section>
);
}
interface CTASectionProps {
service: Service;
}
function CTASection({ service }: CTASectionProps) {
return (
<section className="relative py-36 lg:py-44 overflow-hidden bg-ink-light">
<GrainOverlay />
<div className="absolute inset-0 pointer-events-none">
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[600px] h-[600px] rounded-full opacity-[0.08] blur-3xl bg-brand" />
</div>
<div className="max-w-container mx-auto px-6 lg:px-10 relative z-10">
<ScrollReveal className="max-w-3xl mx-auto text-center">
<SectionLabel className="justify-center">
<div className="flex items-center gap-5">
<div className="w-14 h-px bg-brand" />
<span className="text-[11px] tracking-[0.4em] uppercase font-bold text-brand">
Get Started
</span>
<div className="w-14 h-px bg-brand" />
</div>
</SectionLabel>
<h2 className="text-3xl md:text-4xl lg:text-6xl font-bold text-white tracking-tight leading-tight mb-8">
{service.title}
</h2>
<p className="text-xl text-dark-text-secondary leading-relaxed mb-12 max-w-2xl mx-auto">
</p>
<div className="flex flex-wrap gap-6 justify-center">
<Button size="xl" asChild>
<a href="/contact">
<ArrowRight className="w-5 h-5 ml-2" />
</a>
</Button>
<Button size="xl" variant="outline" asChild>
<a href="/services"></a>
</Button>
</div>
</ScrollReveal>
</div>
</section>
);
}
interface ServiceDetailContentV1Props {
service: Service;
}
export default function ServiceDetailContentV1({ service }: ServiceDetailContentV1Props) {
return (
<main>
<DetailHero service={service} />
<OverviewSection service={service} />
<FeaturesSection service={service} />
<BenefitsSection service={service} />
<ProcessSection service={service} />
<DataProofsSection dataProofs={service.dataProofs} />
<CaseStudiesSection caseStudies={service.caseStudies} />
<CTASection service={service} />
</main>
);
}
@@ -0,0 +1,555 @@
'use client';
import { motion } from 'framer-motion';
import { ScrollReveal, StaggerReveal } from '@/components/ui/scroll-reveal';
import { Button } from '@/components/ui/button';
import { ArrowRight, CheckCircle2, Zap, Clock, Users, Award, TrendingUp, BarChart3, Code, Lightbulb, Puzzle, Shield } from 'lucide-react';
import { GrainOverlay, FloatingInkParticles, SectionLabel, EASE_OUT } from '@/components/ui/page-decoration';
import type { Service, CaseStudy } from '@/lib/constants/services';
import { MethodologyFramework, TechStackShowcase, FAQSection, DataProofSection } from '@/components/content/sections';
const ICON_MAP: Record<string, React.ReactNode> = {
Code: <Code className="w-7 h-7" />,
BarChart3: <BarChart3 className="w-7 h-7" />,
Lightbulb: <Lightbulb className="w-7 h-7" />,
Puzzle: <Puzzle className="w-7 h-7" />,
};
const FEATURE_ICONS = [Zap, Clock, Users, Award, TrendingUp];
const SERVICE_DETAIL_PARTICLES = [
{ x: 12, y: 35, scale: 0.8, duration: 10, delay: 0, size: 3 },
{ x: 85, y: 55, scale: 0.6, duration: 12, delay: 1.5, size: 2 },
{ x: 48, y: 45, scale: 1.0, duration: 9, delay: 0.8, size: 4 },
{ x: 70, y: 28, scale: 0.7, duration: 11, delay: 2.2, size: 2.5 },
{ x: 30, y: 78, scale: 0.9, duration: 8.5, delay: 1.0, size: 3.5 },
{ x: 65, y: 68, scale: 0.55, duration: 13, delay: 2.8, size: 2 },
];
interface DetailHeroProps {
service: Service;
}
function DetailHero({ service }: DetailHeroProps) {
return (
<section className="relative min-h-[80vh] flex items-center overflow-hidden bg-ink pt-24">
<GrainOverlay />
<FloatingInkParticles particles={SERVICE_DETAIL_PARTICLES} />
<div className="absolute inset-0 pointer-events-none">
<div
className="absolute top-0 right-0 w-[50%] h-full"
style={{
background: 'linear-gradient(135deg, transparent 20%, rgba(196, 30, 58, 0.03) 100%)',
clipPath: 'polygon(30% 0, 100% 0, 100% 100%, 0% 100%)',
}}
/>
<div className="absolute bottom-0 left-0 w-full h-1/3 bg-gradient-to-t from-ink to-transparent" />
<div className="absolute top-32 right-[15%] w-[400px] h-[400px] rounded-full opacity-[0.03] blur-3xl bg-brand" />
</div>
<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 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, ease: EASE_OUT }}
className="mb-8"
>
<SectionLabel>Professional Service</SectionLabel>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 50 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 1, delay: 0.15, ease: EASE_OUT }}
className="flex items-center gap-6 mb-10"
>
<div className="w-20 h-20 rounded-2xl bg-brand-soft flex items-center justify-center text-brand">
{ICON_MAP[service.icon] || <Code className="w-10 h-10" />}
</div>
<div>
<div className="text-dark-text-muted text-sm tracking-widest uppercase">
Service
</div>
</div>
</motion.div>
<motion.h1
initial={{ opacity: 0, y: 60 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 1.2, delay: 0.25, ease: EASE_OUT }}
className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-bold text-white leading-tight tracking-tighter mb-8 sm:mb-10 max-w-4xl"
>
{service.title}
</motion.h1>
<motion.p
initial={{ opacity: 0, y: 40 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 1, delay: 0.45, ease: EASE_OUT }}
className="text-xl lg:text-2xl text-dark-text-secondary max-w-3xl leading-relaxed mb-12"
>
{service.description}
</motion.p>
<motion.div
initial={{ opacity: 0, y: 40 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 1, delay: 0.6, ease: EASE_OUT }}
className="flex flex-wrap gap-6"
>
<Button size="xl" asChild>
<a href="/contact">
<ArrowRight className="w-5 h-5 ml-2" />
</a>
</Button>
<Button size="xl" variant="outline" asChild>
<a href="/cases"></a>
</Button>
</motion.div>
</div>
</section>
);
}
interface OverviewSectionProps {
service: Service;
}
function OverviewSection({ service }: OverviewSectionProps) {
return (
<section className="relative py-20 sm:py-28 md:py-32 lg:py-40 overflow-hidden bg-ink-light">
<GrainOverlay />
<div className="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-white/10 to-transparent" />
<div className="absolute bottom-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-white/10 to-transparent" />
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10">
<div className="grid lg:grid-cols-12 gap-16">
<ScrollReveal className="lg:col-span-4">
<SectionLabel>Overview</SectionLabel>
<h2 className="text-2xl sm:text-3xl md:text-4xl lg:text-5xl font-bold text-white tracking-tight leading-tight">
</h2>
</ScrollReveal>
<ScrollReveal delay={0.1} className="lg:col-span-8">
<p className="text-xl text-dark-text-secondary leading-relaxed">
{service.overview}
</p>
</ScrollReveal>
</div>
</div>
</section>
);
}
interface FeaturesSectionProps {
service: Service;
}
function FeaturesSection({ service }: FeaturesSectionProps) {
return (
<section className="relative py-20 sm:py-28 md:py-32 lg:py-40 overflow-hidden bg-ink">
<GrainOverlay />
<div className="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-white/10 to-transparent" />
<div className="absolute bottom-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-white/10 to-transparent" />
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10">
<ScrollReveal className="mb-16 sm:mb-20 max-w-3xl">
<SectionLabel>Core Capabilities</SectionLabel>
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold text-white tracking-tight leading-tight mb-8">
</h2>
<p className="text-dark-text-secondary leading-relaxed text-lg">
</p>
</ScrollReveal>
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-px bg-white/10">
<StaggerReveal staggerDelay={0.08} delayChildren={0.05}>
{service.features.map((feature, index) => {
const Icon = FEATURE_ICONS[index % FEATURE_ICONS.length]!;
const [title, desc] = feature.split('');
return (
<div
key={index}
className="group relative p-10 lg:p-12 transition-all duration-500 hover:bg-white/[0.02] bg-ink-light"
>
<div className="absolute top-0 left-0 h-full w-1 scale-y-0 group-hover:scale-y-100 transition-transform duration-700 origin-top bg-brand" />
<div className="relative z-10">
<div className="w-14 h-14 rounded-xl bg-brand-soft flex items-center justify-center text-brand mb-8">
<Icon className="w-7 h-7" strokeWidth={1.8} />
</div>
<h3 className="text-xl font-bold text-white mb-4">
{desc ? title : feature.slice(0, 10)}
</h3>
<p className="text-dark-text-secondary leading-relaxed">
{desc || feature}
</p>
</div>
</div>
);
})}
</StaggerReveal>
</div>
</div>
</section>
);
}
interface BenefitsSectionProps {
service: Service;
}
function BenefitsSection({ service }: BenefitsSectionProps) {
return (
<section className="relative py-20 sm:py-28 md:py-32 lg:py-40 overflow-hidden bg-ink-light">
<GrainOverlay />
<div className="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-white/10 to-transparent" />
<div className="absolute bottom-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-white/10 to-transparent" />
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10">
<div className="flex flex-col lg:flex-row lg:items-end lg:justify-between gap-12 mb-20">
<ScrollReveal className="lg:max-w-3xl">
<SectionLabel>Why Choose Us</SectionLabel>
<h2 className="text-2xl sm:text-3xl md:text-4xl lg:text-5xl font-bold text-white tracking-tight leading-tight">
</h2>
</ScrollReveal>
<ScrollReveal delay={0.1}>
<p className="text-dark-text-secondary max-w-md leading-relaxed text-lg">
</p>
</ScrollReveal>
</div>
<div className="grid md:grid-cols-2 gap-px bg-white/10">
<StaggerReveal staggerDelay={0.08} delayChildren={0.05}>
{service.benefits.map((benefit, index) => (
<div
key={index}
className="group flex items-start gap-5 p-8 lg:p-10 bg-ink hover:bg-white/[0.02] transition-all duration-500"
>
<CheckCircle2 className="w-7 h-7 text-brand shrink-0 mt-0.5" />
<p className="text-lg text-white leading-relaxed font-medium">
{benefit}
</p>
</div>
))}
</StaggerReveal>
</div>
</div>
</section>
);
}
interface ProcessSectionProps {
service: Service;
}
function ProcessSection({ service }: ProcessSectionProps) {
if (!service.process || service.process.length === 0) return null;
return (
<section className="relative py-20 sm:py-28 md:py-32 lg:py-40 overflow-hidden bg-ink">
<GrainOverlay />
<div className="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-white/10 to-transparent" />
<div className="absolute bottom-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-white/10 to-transparent" />
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10">
<ScrollReveal className="mb-16 sm:mb-20 max-w-3xl text-center mx-auto">
<SectionLabel className="justify-center">
<div className="flex items-center gap-5">
<div className="w-14 h-px bg-brand" />
<span className="text-[11px] tracking-[0.4em] uppercase font-bold text-brand">
Service Process
</span>
<div className="w-14 h-px bg-brand" />
</div>
</SectionLabel>
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold text-white tracking-tight leading-tight mb-8">
</h2>
<p className="text-dark-text-secondary leading-relaxed text-lg">
</p>
</ScrollReveal>
<div className="relative max-w-4xl mx-auto">
<StaggerReveal
className="space-y-px bg-white/10"
staggerDelay={0.12}
delayChildren={0.1}
>
{service.process.map((step, index) => {
const [title, desc] = step.split('');
return (
<div
key={index}
className="relative flex gap-8 group p-8 bg-ink hover:bg-white/[0.02] transition-all duration-500"
>
<div className="relative z-10 shrink-0">
<div className="w-20 h-20 rounded-2xl bg-ink-light border border-white/10 flex items-center justify-center group-hover:border-brand/50 transition-all duration-500">
<span className="text-2xl font-bold text-white group-hover:text-brand transition-colors">
{String(index + 1).padStart(2, '0')}
</span>
</div>
</div>
<div className="flex-1 py-5">
<h4 className="text-2xl font-bold text-white mb-3">
{desc ? title : step.slice(0, 15)}
</h4>
<p className="text-dark-text-secondary leading-relaxed text-lg">
{desc || step}
</p>
</div>
</div>
);
})}
</StaggerReveal>
</div>
</div>
</section>
);
}
interface CaseStudiesSectionProps {
caseStudies?: CaseStudy[];
}
function CaseStudiesSection({ caseStudies = [] }: CaseStudiesSectionProps) {
if (caseStudies.length === 0) return null;
return (
<section className="relative py-20 sm:py-28 md:py-32 lg:py-40 overflow-hidden bg-ink">
<GrainOverlay />
<div className="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-white/10 to-transparent" />
<div className="absolute bottom-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-white/10 to-transparent" />
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10">
<div className="flex flex-col lg:flex-row lg:items-end lg:justify-between gap-12 mb-20">
<ScrollReveal className="lg:max-w-3xl">
<SectionLabel>Case Studies</SectionLabel>
<h2 className="text-2xl sm:text-3xl md:text-4xl lg:text-5xl font-bold text-white tracking-tight leading-tight">
</h2>
</ScrollReveal>
<ScrollReveal delay={0.1}>
<p className="text-dark-text-secondary max-w-md leading-relaxed text-lg">
</p>
</ScrollReveal>
</div>
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-px bg-white/10">
<StaggerReveal staggerDelay={0.1} delayChildren={0.05}>
{caseStudies.map((study) => (
<div
key={study.client}
className="group relative block overflow-hidden bg-ink-light hover:bg-white/[0.02] transition-all duration-500"
>
<div className="aspect-[4/3] bg-gradient-to-br from-ink to-ink-light relative overflow-hidden">
<div className="absolute inset-0 flex items-center justify-center">
<span className="text-6xl font-bold text-white/[0.08]">
{study.client.charAt(0)}
</span>
</div>
<div className="absolute top-5 left-5">
<span className="px-3 py-1.5 text-xs font-bold text-brand bg-brand-soft/80 backdrop-blur-sm">
{study.industry}
</span>
</div>
</div>
<div className="p-8">
<h3 className="text-xl font-bold text-white mb-3 group-hover:translate-x-2 transition-transform duration-500">
{study.client}
</h3>
<p className="text-dark-text-secondary text-sm leading-relaxed mb-6">
{study.challenge}
</p>
<div className="pt-6 border-t border-white/10">
<div className="text-2xl font-bold text-brand mb-1">{study.result}</div>
<div className="text-xs text-dark-text-muted"></div>
</div>
</div>
</div>
))}
</StaggerReveal>
</div>
</div>
</section>
);
}
interface CertificationsSectionProps {
certifications?: { name: string; issuer: string }[];
}
function CertificationsSection({ certifications = [] }: CertificationsSectionProps) {
if (certifications.length === 0) return null;
return (
<section className="relative py-24 lg:py-32 overflow-hidden bg-ink-light">
<GrainOverlay />
<div className="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-white/10 to-transparent" />
<div className="absolute bottom-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-white/10 to-transparent" />
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10">
<ScrollReveal className="text-center max-w-3xl mx-auto mb-16">
<SectionLabel className="justify-center">
<div className="flex items-center gap-5">
<div className="w-14 h-px bg-brand" />
<span className="text-[11px] tracking-[0.4em] uppercase font-bold text-brand">
Certifications
</span>
<div className="w-14 h-px bg-brand" />
</div>
</SectionLabel>
<h2 className="text-2xl md:text-3xl font-bold text-white tracking-tight">
</h2>
</ScrollReveal>
<div className="flex flex-wrap justify-center gap-px bg-white/10">
<StaggerReveal staggerDelay={0.08} delayChildren={0.05}>
{certifications.map((cert, idx) => (
<div
key={idx}
className="group flex items-center gap-4 px-8 py-5 bg-ink hover:bg-white/[0.02] 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" />
</div>
<div>
<div className="text-white font-bold">{cert.name}</div>
<div className="text-sm text-dark-text-muted">{cert.issuer}</div>
</div>
</div>
))}
</StaggerReveal>
</div>
</div>
</section>
);
}
interface CTASectionProps {
service: Service;
}
function CTASection({ service }: CTASectionProps) {
return (
<section className="relative py-36 lg:py-44 overflow-hidden bg-ink">
<GrainOverlay />
<FloatingInkParticles particles={SERVICE_DETAIL_PARTICLES} />
<div className="absolute inset-0 pointer-events-none">
<div className="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-brand/30 to-transparent" />
<div className="absolute bottom-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-brand/30 to-transparent" />
<div className="absolute top-1/4 right-[10%] w-[380px] h-[380px] rounded-full opacity-[0.03] blur-3xl bg-brand" />
<div className="absolute bottom-1/4 left-[10%] w-[320px] h-[320px] rounded-full opacity-[0.02] blur-3xl bg-teal-500" />
</div>
<div className="max-w-container mx-auto px-6 lg:px-10 relative z-10">
<ScrollReveal className="text-center max-w-3xl mx-auto">
<div className="flex justify-center mb-7">
<div className="flex items-center gap-5">
<div className="w-14 h-px bg-brand" />
<span className="text-[11px] tracking-[0.4em] uppercase font-bold text-brand">
Get Started
</span>
<div className="w-14 h-px bg-brand" />
</div>
</div>
<h2 className="text-4xl md:text-5xl lg:text-6xl font-bold text-white tracking-tight leading-tight mb-8">
<br />
<span className="text-brand">{service.title}</span>
</h2>
<p className="text-xl text-dark-text-secondary mb-12 max-w-2xl mx-auto leading-relaxed">
</p>
<div className="flex flex-wrap justify-center gap-6">
<Button size="xl" asChild>
<a href="/contact">
<ArrowRight className="w-5 h-5 ml-2" />
</a>
</Button>
<Button size="xl" variant="outline" asChild>
<a href="/services"></a>
</Button>
</div>
</ScrollReveal>
</div>
</section>
);
}
interface ServiceDetailContentV2Props {
service: Service;
}
export default function ServiceDetailContentV2({ service }: ServiceDetailContentV2Props) {
return (
<main>
<DetailHero service={service} />
<OverviewSection service={service} />
<FeaturesSection service={service} />
{service.methodology && service.methodology.length > 0 && (
<MethodologyFramework
title="我们的方法论"
subtitle="经过多年实战打磨的系统化方法,确保每个项目都有章可循、有据可依"
layers={service.methodology}
/>
)}
<BenefitsSection service={service} />
<ProcessSection service={service} />
{service.techStack && service.techStack.length > 0 && (
<TechStackShowcase
title="技术栈与工具"
subtitle="我们擅长的技术和工具,确保用最适合的方案解决问题"
categories={service.techStack}
/>
)}
<DataProofSection
title="可衡量的成果"
subtitle="用数据说话,每一项指标都源自真实项目积累"
metrics={service.dataProofs.map((dp) => ({
value: dp.value,
label: dp.metric,
description: dp.description,
}))}
/>
<CaseStudiesSection caseStudies={service.caseStudies} />
<CertificationsSection certifications={service.certifications} />
{service.faqs && service.faqs.length > 0 && (
<FAQSection
title="常见问题"
subtitle="关于这项服务,客户最常问的问题"
items={service.faqs}
/>
)}
<CTASection service={service} />
</main>
);
}
@@ -0,0 +1,502 @@
'use client';
import { motion, useScroll, useTransform } from 'framer-motion';
import { useRef } from 'react';
import { ScrollReveal, StaggerReveal } from '@/components/ui/scroll-reveal';
import { Button } from '@/components/ui/button';
import { SectionHeader } from '@/components/sections/section-header';
import { ServiceCard } from '@/components/sections/service-card';
import { ScrollProgress } from '@/components/ui/scroll-progress';
import { useReducedMotion } from '@/hooks/use-reduced-motion';
import { ArrowRight, CheckCircle2, Zap, Clock, Users, Award, TrendingUp, BarChart3, Code, Lightbulb, Puzzle, Shield } from 'lucide-react';
import type { Service, CaseStudy } from '@/lib/constants/services';
const ICON_MAP: Record<string, React.ReactNode> = {
Code: <Code className="w-7 h-7" />,
BarChart3: <BarChart3 className="w-7 h-7" />,
Lightbulb: <Lightbulb className="w-7 h-7" />,
Puzzle: <Puzzle className="w-7 h-7" />,
};
const FEATURE_ICONS = [Zap, Clock, Users, Award, TrendingUp, Shield];
const EASE = [0.22, 1, 0.36, 1] as const;
function DetailHero({ service }: { service: Service }) {
const shouldReduceMotion = useReducedMotion();
const heroRef = useRef<HTMLDivElement>(null);
const { scrollYProgress } = useScroll({
target: heroRef,
offset: ['start start', 'end start'],
});
const opacity = useTransform(scrollYProgress, [0, 0.6], [1, 0]);
const contentY = useTransform(scrollYProgress, [0, 1], [0, 60]);
const bgY = useTransform(scrollYProgress, [0, 1], [0, 100]);
return (
<section
ref={heroRef}
className="relative min-h-[90svh] flex items-center overflow-hidden pt-20"
style={{ background: 'var(--color-ink)' }}
>
{/* Background layers — parallax */}
<motion.div
className="absolute inset-0 pointer-events-none"
style={{ y: shouldReduceMotion ? 0 : bgY }}
>
{/* Brand glow */}
<div
className="absolute w-[600px] md:w-[800px] h-[600px] md:h-[800px] rounded-full blur-[120px] opacity-[0.08]"
style={{
background: 'radial-gradient(circle, var(--color-brand), transparent 70%)',
top: '-20%',
right: '-10%',
}}
/>
{/* Grid texture */}
<div
className="absolute inset-0 opacity-[0.02]"
style={{
backgroundImage: `linear-gradient(rgba(255,255,255,0.5) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,0.5) 1px, transparent 1px)`,
backgroundSize: '50px 50px',
maskImage: 'radial-gradient(ellipse 60% 50% at 50% 40%, black 20%, transparent 70%)',
}}
/>
</motion.div>
{/* Content */}
<motion.div
className="relative z-10 w-full max-w-[1280px] mx-auto px-5 sm:px-8 lg:px-16 py-24 md:py-32"
style={{ opacity, y: shouldReduceMotion ? 0 : contentY }}
>
<motion.div
className="flex items-center gap-3 mb-8"
initial={shouldReduceMotion ? {} : { opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, ease: EASE }}
>
<div
className="w-1 h-4 rounded-full"
style={{ backgroundColor: 'var(--color-brand)' }}
/>
<span
className="text-[10px] tracking-[4px] uppercase font-medium"
style={{ color: 'var(--color-brand)' }}
>
</span>
</motion.div>
<motion.div
className="flex items-center gap-5 mb-8"
initial={shouldReduceMotion ? {} : { opacity: 0, y: 24 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.7, delay: 0.1, ease: EASE }}
>
<div
className="w-16 h-16 rounded-2xl flex items-center justify-center"
style={{
backgroundColor: 'var(--color-brand-soft)',
color: 'var(--color-brand)',
}}
>
{ICON_MAP[service.icon] || <Code className="w-8 h-8" />}
</div>
</motion.div>
{/* Bain-style: conclusion-first title */}
<motion.h1
className="font-sans text-[clamp(36px,7vw,72px)] font-black leading-[1.05] tracking-[-1.5px] text-white mb-6 md:mb-8"
initial={shouldReduceMotion ? {} : { opacity: 0, y: 40 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.9, delay: 0.2, ease: EASE }}
>
{service.title}<br />
<span style={{ color: 'var(--color-brand)' }}>
{service.benefits?.[0]?.slice(0, 12) || '效率提升 40%'}
</span>
</motion.h1>
<motion.p
className="text-base sm:text-lg md:text-xl max-w-2xl leading-relaxed mb-8 md:mb-10 font-light"
style={{ color: 'rgba(255,255,255,0.5)' }}
initial={shouldReduceMotion ? {} : { opacity: 0, y: 24 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.35, ease: EASE }}
>
{service.description}
</motion.p>
{/* Quick stats — answer-first */}
<motion.div
className="grid grid-cols-3 gap-4 sm:gap-8 mb-10 md:mb-12 max-w-xl"
initial={shouldReduceMotion ? {} : { opacity: 0, y: 24 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.5, ease: EASE }}
>
{[
{ value: '12年', label: '行业深耕' },
{ value: '95%+', label: '准时交付' },
{ value: '98%', label: '客户续约' },
].map((stat, i) => (
<div key={i}>
<div
className="font-sans tabular-nums text-[28px] sm:text-[36px] font-bold leading-none mb-1.5"
style={{ color: 'var(--color-brand)' }}
>
{stat.value}
</div>
<div
className="text-[11px] sm:text-[12px]"
style={{ color: 'rgba(255,255,255,0.35)' }}
>
{stat.label}
</div>
</div>
))}
</motion.div>
<motion.div
className="flex flex-wrap gap-4"
initial={shouldReduceMotion ? {} : { opacity: 0, y: 24 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.65, ease: EASE }}
>
<Button size="lg" asChild>
<a href="/contact">
<ArrowRight className="w-4 h-4 ml-2" />
</a>
</Button>
<Button size="lg" variant="outline" asChild>
<a href="#cases"></a>
</Button>
</motion.div>
</motion.div>
</section>
);
}
function OverviewSection({ service }: { service: Service }) {
return (
<section className="relative py-20 sm:py-28 md:py-36 lg:py-44" style={{ background: 'var(--color-bg-primary)' }}>
<div className="max-w-[1280px] mx-auto px-5 sm:px-8 lg:px-16">
<div className="grid lg:grid-cols-12 gap-12 lg:gap-16 items-start">
<ScrollReveal className="lg:col-span-5">
<SectionHeader
label="Overview"
title="解决什么"
highlight="问题"
desc="每一项服务,都从一个具体的业务痛点出发"
/>
</ScrollReveal>
<ScrollReveal delay={0.1} className="lg:col-span-7">
<p
className="text-lg md:text-xl leading-relaxed mb-8"
style={{ color: 'var(--color-text-secondary)' }}
>
{service.overview}
</p>
<div className="flex flex-wrap gap-3">
{service.benefits?.slice(0, 4).map((benefit, i) => (
<div
key={i}
className="inline-flex items-center gap-2 px-4 py-2 rounded-full text-sm"
style={{
backgroundColor: 'var(--color-brand-soft)',
color: 'var(--color-brand)',
}}
>
<CheckCircle2 className="w-4 h-4 shrink-0" />
<span className="font-medium">{benefit.slice(0, 10)}</span>
</div>
))}
</div>
</ScrollReveal>
</div>
</div>
</section>
);
}
function FeaturesSection({ service }: { service: Service }) {
return (
<section className="relative py-20 sm:py-28 md:py-36 lg:py-44 overflow-hidden" style={{ background: 'var(--color-ink-light)' }}>
<div className="max-w-[1280px] mx-auto px-5 sm:px-8 lg:px-16">
<ScrollReveal className="mb-16 sm:mb-20 max-w-2xl">
<SectionHeader
label="Capabilities"
title="核心"
highlight="能力"
desc="每一项都经过 500+ 项目实战打磨"
light
/>
</ScrollReveal>
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-px" style={{ backgroundColor: 'rgba(255,255,255,0.06)' }}>
<StaggerReveal staggerDelay={0.08} delayChildren={0.05}>
{service.features.map((feature, index) => {
const Icon = FEATURE_ICONS[index % FEATURE_ICONS.length]!;
const [title, desc] = feature.split('');
return (
<div
key={index}
className="group relative p-8 sm:p-10 transition-all duration-500 hover:bg-white/[0.03]"
style={{ backgroundColor: 'var(--color-ink)' }}
>
<div className="absolute top-0 left-0 h-full w-[3px] origin-bottom scale-y-0 group-hover:scale-y-100 transition-transform duration-500" style={{ backgroundColor: 'var(--color-brand)' }} />
<div className="relative z-10">
<div
className="w-12 h-12 rounded-xl flex items-center justify-center mb-6"
style={{
backgroundColor: 'var(--color-brand-soft)',
color: 'var(--color-brand)',
}}
>
<Icon className="w-6 h-6" strokeWidth={1.8} />
</div>
<h3 className="text-lg font-bold mb-3 text-white">
{desc ? title : feature.slice(0, 10)}
</h3>
<p className="text-sm leading-relaxed" style={{ color: 'rgba(255,255,255,0.4)' }}>
{desc || feature}
</p>
</div>
</div>
);
})}
</StaggerReveal>
</div>
</div>
</section>
);
}
function ProcessSection({ service }: { service: Service }) {
if (!service.process || service.process.length === 0) return null;
return (
<section className="relative py-20 sm:py-28 md:py-36 lg:py-44" style={{ background: 'var(--color-bg-primary)' }}>
<div className="max-w-[1280px] mx-auto px-5 sm:px-8 lg:px-16">
<ScrollReveal className="mb-16 sm:mb-20 max-w-2xl text-center mx-auto">
<SectionHeader
label="Process"
title="服务"
highlight="流程"
desc="标准化流程,确保每个项目可控、可预期"
/>
</ScrollReveal>
<div className="grid md:grid-cols-2 lg:grid-cols-5 gap-0">
<StaggerReveal staggerDelay={0.1} delayChildren={0.05}>
{service.process.slice(0, 5).map((step, index) => {
const [title, desc] = step.split('');
return (
<div key={index} className="relative group p-6 sm:p-8">
<div
className="font-sans text-[56px] font-bold leading-none mb-4"
style={{
color: 'var(--color-brand)',
opacity: 0.15,
transition: 'opacity 0.4s',
}}
onMouseEnter={(e) => (e.currentTarget.style.opacity = '0.4')}
onMouseLeave={(e) => (e.currentTarget.style.opacity = '0.15')}
>
0{index + 1}
</div>
<h3 className="text-lg font-bold mb-2" style={{ color: 'var(--color-text-primary)' }}>
{desc ? title : step.slice(0, 6)}
</h3>
<p className="text-sm leading-relaxed" style={{ color: 'var(--color-text-muted)' }}>
{desc || step}
</p>
{index < service.process.length - 1 && index < 4 && (
<div
className="hidden lg:block absolute top-16 right-0 w-full h-px"
style={{
background: `linear-gradient(90deg, var(--color-brand), transparent)`,
opacity: 0.2,
}}
/>
)}
</div>
);
})}
</StaggerReveal>
</div>
</div>
</section>
);
}
function CaseStudiesSection({ caseStudies = [] }: { caseStudies?: CaseStudy[] }) {
if (caseStudies.length === 0) return null;
return (
<section id="cases" className="relative py-20 sm:py-28 md:py-36 lg:py-44 overflow-hidden" style={{ background: 'var(--color-ink-light)' }}>
<div className="max-w-[1280px] mx-auto px-5 sm:px-8 lg:px-16">
<ScrollReveal className="mb-16 sm:mb-20 max-w-2xl">
<SectionHeader
label="Case Studies"
title="500+ 企业的"
highlight="增长实践"
desc="用真实案例说话,每一个都是我们能力的证明"
light
/>
</ScrollReveal>
<div className="grid md:grid-cols-2 gap-6 lg:gap-8">
<StaggerReveal staggerDelay={0.1} delayChildren={0.05}>
{caseStudies.slice(0, 4).map((cs, i) => (
<div
key={i}
className="group relative p-8 sm:p-10 transition-all duration-500 hover:-translate-y-1"
style={{
backgroundColor: 'var(--color-ink)',
border: '1px solid rgba(255,255,255,0.06)',
}}
>
<div className="absolute top-0 left-0 right-0 h-[2px] scale-x-0 group-hover:scale-x-100 transition-transform duration-500 origin-left"
style={{
background: 'linear-gradient(90deg, var(--color-brand), transparent)',
}}
/>
<div className="relative z-10">
<div className="flex items-center gap-3 mb-5">
<span
className="text-[10px] tracking-[3px] uppercase font-medium"
style={{ color: 'var(--color-brand)' }}
>
{cs.industry}
</span>
<span style={{ color: 'rgba(255,255,255,0.1)' }}></span>
<span className="text-sm" style={{ color: 'rgba(255,255,255,0.5)' }}>
{cs.client}
</span>
</div>
<h3 className="text-xl sm:text-2xl font-bold mb-6 text-white leading-tight">
{cs.challenge.slice(0, 30)}
</h3>
<p className="text-sm leading-relaxed mb-6" style={{ color: 'rgba(255,255,255,0.45)' }}>
{cs.solution}
</p>
<div className="flex items-center gap-2 text-sm font-medium transition-all duration-300 group-hover:gap-3" style={{ color: 'var(--color-brand)' }}>
<span></span>
<ArrowRight className="w-4 h-4" />
</div>
</div>
</div>
))}
</StaggerReveal>
</div>
</div>
</section>
);
}
function FAQSection({ service }: { service: Service }) {
if (!service.faqs || service.faqs.length === 0) return null;
return (
<section className="relative py-20 sm:py-28 md:py-36 lg:py-44" style={{ background: 'var(--color-bg-primary)' }}>
<div className="max-w-[900px] mx-auto px-5 sm:px-8 lg:px-16">
<ScrollReveal className="mb-16 sm:mb-20 text-center">
<SectionHeader
label="FAQ"
title="常见"
highlight="问题"
desc="在开始之前,这些可能是您想知道的"
/>
</ScrollReveal>
<div className="space-y-4">
<StaggerReveal staggerDelay={0.06} delayChildren={0.05}>
{service.faqs.map((faq, i) => (
<div
key={i}
className="group p-6 sm:p-8 transition-all duration-300 cursor-pointer"
style={{
backgroundColor: 'var(--color-bg-secondary)',
borderBottom: '1px solid var(--color-border)',
}}
>
<h4 className="text-base sm:text-lg font-semibold mb-3" style={{ color: 'var(--color-text-primary)' }}>
{faq.question}
</h4>
<p className="text-sm leading-relaxed" style={{ color: 'var(--color-text-secondary)' }}>
{faq.answer}
</p>
</div>
))}
</StaggerReveal>
</div>
</div>
</section>
);
}
function RelatedServicesSection({ currentId }: { currentId: string }) {
const related = [
{ id: 'software', title: '软件开发', desc: '定制化全栈开发服务', icon: 'Code' },
{ id: 'data', title: '数据分析', desc: '数据驱动业务决策', icon: 'BarChart3' },
{ id: 'consulting', title: '咨询服务', desc: '数字化战略规划', icon: 'Lightbulb' },
].filter(s => s.id !== currentId);
return (
<section className="relative py-20 sm:py-28 md:py-36 lg:py-44 overflow-hidden" style={{ background: 'var(--color-ink-light)' }}>
<div className="max-w-[1280px] mx-auto px-5 sm:px-8 lg:px-16">
<ScrollReveal className="mb-16 sm:mb-20 max-w-2xl">
<SectionHeader
label="Related"
title="相关"
highlight="服务"
desc="您可能还需要这些服务"
light
/>
</ScrollReveal>
<div className="grid md:grid-cols-2 gap-6">
<StaggerReveal staggerDelay={0.1}>
{related.map((s, i) => (
<ServiceCard
key={s.id}
number={String(i + 1).padStart(2, '0')}
title={s.title}
description={s.desc}
href={`/services/${s.id}`}
color="brand"
/>
))}
</StaggerReveal>
</div>
</div>
</section>
);
}
export default function ServiceDetailContentV3({ service }: { service: Service }) {
return (
<main className="min-h-screen">
<ScrollProgress />
<DetailHero service={service} />
<OverviewSection service={service} />
<FeaturesSection service={service} />
<ProcessSection service={service} />
<CaseStudiesSection caseStudies={service.caseStudies} />
<FAQSection service={service} />
<RelatedServicesSection currentId={service.id} />
</main>
);
}
@@ -0,0 +1,394 @@
'use client';
import { motion } from 'framer-motion';
import { ArrowUpRight, CheckCircle2 } from 'lucide-react';
import type { Service, CaseStudy } from '@/lib/constants/services';
const EASE_OUT = [0.22, 1, 0.36, 1] as const;
const FEATURE_ICONS = ['⚡', '⏱', '👥', '🏆', '📈', '🛡'];
function DetailHero({ service }: { service: Service }) {
const firstBenefit = service.benefits?.[0] || '效率提升 40%';
return (
<section className="relative min-h-[85vh] flex items-center overflow-hidden bg-white">
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10 w-full py-28 sm:py-36 md:py-44 lg:py-56">
<div className="max-w-4xl">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.1, ease: EASE_OUT }}
className="text-sm font-mono tracking-[0.2em] text-text-muted uppercase mb-8"
>
Professional Services
</motion.div>
<motion.h1
initial={{ opacity: 0, y: 40 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 1, delay: 0.2, ease: EASE_OUT }}
className="text-4xl sm:text-5xl md:text-6xl lg:text-7xl xl:text-8xl font-black text-ink leading-[0.88] tracking-tightest mb-10 sm:mb-12 md:mb-16"
>
<div className="block">{service.title}</div>
<div className="block mt-4 sm:mt-6">
<span className="relative inline-block">
<span className="relative text-brand font-black">
{firstBenefit}
<motion.span
className="absolute -bottom-2 left-0 w-full h-[3px] bg-brand"
style={{ transformOrigin: 'left' }}
initial={{ scaleX: 0 }}
animate={{ scaleX: 1 }}
transition={{ duration: 0.8, delay: 1.2, ease: EASE_OUT }}
/>
</span>
</span>
</div>
</motion.h1>
<motion.p
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.5, ease: EASE_OUT }}
className="text-lg md:text-xl text-text-secondary max-w-2xl leading-relaxed mb-12 sm:mb-16"
>
{service.description}
</motion.p>
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.7, ease: EASE_OUT }}
className="grid grid-cols-3 gap-8 mb-12 sm:mb-16 max-w-xl"
>
<div>
<div className="text-3xl sm:text-4xl font-black text-brand tracking-tight leading-none mb-2">
12
</div>
<div className="text-sm text-text-muted"></div>
</div>
<div>
<div className="text-3xl sm:text-4xl font-black text-ink tracking-tight leading-none mb-2">
95%+
</div>
<div className="text-sm text-text-muted"></div>
</div>
<div>
<div className="text-3xl sm:text-4xl font-black text-ink tracking-tight leading-none mb-2">
98%
</div>
<div className="text-sm text-text-muted"></div>
</div>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.9, ease: EASE_OUT }}
className="flex flex-col sm:flex-row gap-4 sm:gap-6"
>
<a
href="/contact"
className="group inline-flex items-center justify-center gap-3 px-10 py-5 font-semibold text-base bg-brand text-white transition-all duration-300 hover:bg-brand/90 min-h-[52px] touch-manipulation"
>
<ArrowUpRight className="w-4 h-4 transition-transform duration-300 group-hover:translate-x-0.5 group-hover:-translate-y-0.5" />
</a>
<a
href="#cases"
className="inline-flex items-center justify-center gap-3 px-10 py-5 font-semibold text-base text-ink border border-border-primary hover:border-ink/30 transition-all duration-300 min-h-[52px] touch-manipulation"
>
</a>
</motion.div>
</div>
</div>
</section>
);
}
function OverviewSection({ service }: { service: Service }) {
return (
<section className="relative py-20 sm:py-28 md:py-36 lg:py-44 overflow-hidden bg-bg-secondary">
<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" />
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10">
<div className="grid lg:grid-cols-12 gap-12 sm:gap-16 md:gap-20 items-start">
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-80px' }}
transition={{ duration: 0.8, ease: EASE_OUT }}
className="lg:col-span-5"
>
<div className="text-sm font-mono tracking-[0.2em] text-text-muted uppercase mb-6">
Overview
</div>
<h2 className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-extrabold text-ink tracking-tight leading-[0.95]">
</h2>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-80px' }}
transition={{ duration: 0.8, delay: 0.15, ease: EASE_OUT }}
className="lg:col-span-7"
>
<p className="text-text-secondary leading-relaxed text-lg mb-8">
{service.overview}
</p>
<div className="flex flex-wrap gap-3">
{service.benefits?.slice(0, 4).map((benefit, i) => (
<div
key={i}
className="inline-flex items-center gap-2 px-4 py-2 text-sm border border-border-primary text-ink"
>
<CheckCircle2 className="w-4 h-4 shrink-0 text-brand" />
<span className="font-medium">{benefit}</span>
</div>
))}
</div>
</motion.div>
</div>
</div>
</section>
);
}
function FeaturesSection({ service }: { service: Service }) {
return (
<section className="relative py-20 sm:py-28 md:py-36 lg:py-44 overflow-hidden bg-white">
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10">
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-80px' }}
transition={{ duration: 0.8, ease: EASE_OUT }}
className="max-w-3xl mb-16 sm:mb-20 md:mb-24"
>
<div className="text-sm font-mono tracking-[0.2em] text-text-muted uppercase mb-6">
Capabilities
</div>
<h2 className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-extrabold text-ink tracking-tight leading-[0.95]">
</h2>
<p className="mt-6 text-lg text-text-secondary leading-relaxed">
500+
</p>
</motion.div>
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-px bg-border-primary">
{service.features.map((feature, index) => {
const [title, desc] = feature.split('');
const icon = FEATURE_ICONS[index % FEATURE_ICONS.length];
return (
<motion.div
key={index}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-60px' }}
transition={{ duration: 0.6, delay: index * 0.06, ease: EASE_OUT }}
className="group relative p-8 sm:p-10 transition-all duration-500 hover:bg-bg-secondary bg-white"
>
<div className="absolute top-0 left-0 h-0 w-[2px] group-hover:h-full transition-all duration-500 origin-top bg-brand" />
<div className="relative z-10">
<div className="text-3xl mb-6">{icon}</div>
<h3 className="text-lg sm:text-xl font-bold mb-3 text-ink group-hover:text-brand transition-colors duration-300">
{desc ? title : feature.slice(0, 10)}
</h3>
<p className="text-sm leading-relaxed text-text-secondary">
{desc || feature}
</p>
</div>
</motion.div>
);
})}
</div>
</div>
</section>
);
}
function ProcessSection({ service }: { service: Service }) {
if (!service.process || service.process.length === 0) return null;
return (
<section className="relative py-20 sm:py-28 md:py-36 lg:py-44 overflow-hidden bg-bg-secondary">
<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" />
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10">
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-80px' }}
transition={{ duration: 0.8, ease: EASE_OUT }}
className="max-w-3xl mb-16 sm:mb-20 md:mb-24"
>
<div className="text-sm font-mono tracking-[0.2em] text-text-muted uppercase mb-6">
Process
</div>
<h2 className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-extrabold text-ink tracking-tight leading-[0.95]">
</h2>
<p className="mt-6 text-lg text-text-secondary leading-relaxed">
</p>
</motion.div>
<div className="grid md:grid-cols-2 lg:grid-cols-5 gap-px bg-border-primary">
{service.process.slice(0, 5).map((step, index) => {
const [title, desc] = step.split('');
return (
<motion.div
key={index}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-60px' }}
transition={{ duration: 0.6, delay: index * 0.08, ease: EASE_OUT }}
className="relative group p-6 sm:p-8 bg-white"
>
<div className={`text-4xl sm:text-5xl font-black font-mono mb-6 ${
index === 0 ? 'text-brand' : 'text-ink/[0.06]'
}`}>
0{index + 1}
</div>
<h3 className="text-lg font-bold mb-2 text-ink group-hover:text-brand transition-colors duration-300">
{desc ? title : step.slice(0, 6)}
</h3>
<p className="text-sm leading-relaxed text-text-secondary">
{desc || step}
</p>
</motion.div>
);
})}
</div>
</div>
</section>
);
}
function CaseStudiesSection({ caseStudies = [] }: { caseStudies?: CaseStudy[] }) {
if (caseStudies.length === 0) return null;
return (
<section id="cases" className="relative py-20 sm:py-28 md:py-36 lg:py-44 overflow-hidden bg-white">
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10">
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-80px' }}
transition={{ duration: 0.8, ease: EASE_OUT }}
className="max-w-3xl mb-16 sm:mb-20 md:mb-24"
>
<div className="text-sm font-mono tracking-[0.2em] text-text-muted uppercase mb-6">
Case Studies
</div>
<h2 className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-extrabold text-ink tracking-tight leading-[0.95]">
<br className="sm:hidden" />
</h2>
</motion.div>
<div className="grid md:grid-cols-2 gap-px bg-border-primary">
{caseStudies.slice(0, 4).map((cs, i) => (
<motion.a
key={i}
href={`/cases/${cs.client.toLowerCase().replace(/\s+/g, '-')}`}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-60px' }}
transition={{ duration: 0.6, delay: i * 0.08, ease: EASE_OUT }}
className="group relative block p-8 sm:p-10 transition-all duration-500 hover:bg-bg-secondary bg-white border border-transparent hover:border-border-primary"
>
<div className="absolute top-0 left-0 h-0 w-[2px] group-hover:h-full transition-all duration-500 origin-top bg-brand" />
<div className="relative z-10">
<div className="flex items-center gap-3 mb-5">
<span className="text-[11px] tracking-[0.25em] uppercase font-medium text-brand">
{cs.industry}
</span>
<span className="text-border-primary"></span>
<span className="text-sm text-text-secondary">{cs.client}</span>
</div>
<h3 className="text-xl sm:text-2xl font-bold mb-6 text-ink leading-tight group-hover:text-brand transition-colors duration-300">
{cs.challenge.slice(0, 30)}
</h3>
<p className="text-sm leading-relaxed mb-6 text-text-secondary">
{cs.solution}
</p>
<div className="inline-flex items-center gap-2 text-sm font-semibold text-text-secondary group-hover:text-brand transition-colors duration-300">
<span></span>
<ArrowUpRight className="w-4 h-4 group-hover:translate-x-0.5 group-hover:-translate-y-0.5 transition-transform duration-300" />
</div>
</div>
</motion.a>
))}
</div>
</div>
</section>
);
}
function CTASection() {
return (
<section className="relative py-20 sm:py-28 md:py-36 lg:py-44 overflow-hidden bg-bg-secondary">
<div className="absolute top-0 left-0 right-0 h-px bg-border-primary" />
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10">
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-80px' }}
transition={{ duration: 0.8, ease: EASE_OUT }}
className="max-w-3xl"
>
<h2 className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-extrabold text-ink tracking-tight leading-[0.95] mb-8">
</h2>
<p className="text-lg text-text-secondary leading-relaxed mb-10">
<br className="hidden sm:block" />
</p>
<div className="flex flex-col sm:flex-row gap-4 sm:gap-6">
<a
href="/contact"
className="group inline-flex items-center justify-center gap-3 px-10 py-5 font-semibold text-base bg-brand text-white transition-all duration-300 hover:bg-brand/90 min-h-[52px] touch-manipulation"
>
<ArrowUpRight className="w-4 h-4 transition-transform duration-300 group-hover:translate-x-0.5 group-hover:-translate-y-0.5" />
</a>
<a
href="/services"
className="inline-flex items-center justify-center gap-3 px-10 py-5 font-semibold text-base text-ink border border-border-primary hover:border-ink/30 transition-all duration-300 min-h-[52px] touch-manipulation"
>
</a>
</div>
</motion.div>
</div>
</section>
);
}
export default function ServiceDetailContentV4({ service }: { service: Service }) {
return (
<main className="min-h-screen bg-white text-ink">
<DetailHero service={service} />
<OverviewSection service={service} />
<FeaturesSection service={service} />
<ProcessSection service={service} />
<CaseStudiesSection caseStudies={service.caseStudies} />
<CTASection />
</main>
);
}
@@ -0,0 +1,85 @@
'use client';
import { useState, useEffect } from 'react';
import { initCms, getItemRenderer } from '@/lib/cms';
import type { ContentItem } from '@/lib/cms';
import { SERVICES } from '@/lib/constants';
function serviceToContentItem(service: unknown, index: number): ContentItem {
const s = service as Record<string, unknown>;
return {
id: String(s.id),
modelId: 'service',
modelCode: 'service',
title: String(s.title),
slug: String(s.id),
status: 'published',
version: 1,
sortOrder: index,
data: s,
createdAt: '2024-01-01T00:00:00Z',
updatedAt: '2024-01-01T00:00:00Z',
publishedAt: '2024-01-01T00:00:00Z',
createdBy: 'system',
updatedBy: 'system',
};
}
export default function ServicesContentCms() {
const [ready, setReady] = useState(false);
const [items, setItems] = useState<ContentItem[]>([]);
useEffect(() => {
initCms();
setItems(SERVICES.map(serviceToContentItem));
setReady(true);
}, []);
if (!ready) {
return (
<div className="min-h-screen bg-ink flex items-center justify-center">
<div className="text-white/50">...</div>
</div>
);
}
const ServiceCardRenderer = getItemRenderer('service');
return (
<div className="min-h-screen bg-ink text-white overflow-x-hidden" data-theme="dark">
<section className="relative min-h-[50vh] flex items-center overflow-hidden bg-ink pt-24">
<div className="absolute inset-0">
<div className="absolute top-1/3 -left-32 w-96 h-96 rounded-full bg-teal-500/20 blur-[120px]" />
</div>
<div className="relative z-10 w-full max-w-7xl mx-auto px-6 lg:px-8 py-20">
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-white/[0.05] border border-white/[0.1] text-sm text-white/70 mb-6">
<span className="w-1.5 h-1.5 rounded-full bg-brand animate-pulse" />
</div>
<h1 className="text-4xl sm:text-5xl md:text-6xl font-bold text-white tracking-tight leading-[1.1]">
</h1>
<p className="mt-6 text-lg text-white/60 max-w-2xl leading-relaxed">
</p>
</div>
</section>
<section className="relative py-16 sm:py-20 md:py-28 overflow-hidden bg-ink-light">
<div className="max-w-7xl mx-auto px-6 lg:px-8">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 md:gap-8">
{items.map((item, i) =>
ServiceCardRenderer ? (
<ServiceCardRenderer key={item.id} item={item} index={i} />
) : (
<div key={item.id} className="p-4 border border-white/10 rounded-lg">
{item.title}
</div>
)
)}
</div>
</div>
</section>
</div>
);
}
@@ -0,0 +1,510 @@
'use client';
import { useRef } from 'react';
import { motion } from 'framer-motion';
import { ScrollReveal, StaggerReveal } from '@/components/ui/scroll-reveal';
import { Button } from '@/components/ui/button';
import { ArrowRight, Code, BarChart3, Lightbulb, Puzzle, ClipboardList, PencilRuler, Rocket, HeartHandshake, Briefcase, RefreshCcw, Handshake, CheckCircle2 } from 'lucide-react';
import { cn } from '@/lib/utils';
const EASE_OUT = [0.22, 1, 0.36, 1] as const;
const SERVICES = [
{
number: '01',
title: '软件开发',
subtitle: 'Software Development',
desc: '从需求分析到上线运维,全栈定制化开发。用扎实的工程能力交付高质量软件,确保每个项目都能创造真实业务价值。',
href: '/services/software',
icon: <Code className="w-7 h-7" />,
colorClass: 'text-brand',
bgClass: 'bg-brand-soft',
highlights: ['定制化开发', '全栈技术栈', '敏捷开发', '质量保证', '持续支持'],
metrics: [
{ value: '95%+', label: '准时交付率' },
{ value: 'A+', label: '代码质量' },
],
},
{
number: '02',
title: '数据分析',
subtitle: 'Data Analytics',
desc: '让数据从"存着"变成"用着"。多源数据整合、可视化看板、预测模型,为经营决策提供可量化的洞察支撑。',
href: '/services/data',
icon: <BarChart3 className="w-7 h-7" />,
colorClass: 'text-accent-blue',
bgClass: 'bg-accent-blue-soft',
highlights: ['数据整合', '可视化分析', '预测模型', '实时分析', '洞察挖掘'],
metrics: [
{ value: '100+', label: '分析模型' },
{ value: '80%+', label: '洞察转化率' },
],
},
{
number: '03',
title: '技术咨询',
subtitle: 'Tech Consulting',
desc: 'IT 战略规划、技术选型评估、数字化转型路线图——帮您理清方向,规避技术风险,减少试错成本。',
href: '/services/consulting',
icon: <Lightbulb className="w-7 h-7" />,
colorClass: 'text-accent-teal',
bgClass: 'bg-accent-teal-soft',
highlights: ['IT战略规划', '技术选型评估', '转型路线图', '架构评审', '团队建设'],
metrics: [
{ value: '90%+', label: '方案落地率' },
{ value: '50+', label: '咨询项目' },
],
},
{
number: '04',
title: '行业方案实施',
subtitle: 'Industry Solutions',
desc: '深耕制造、零售、金融、医疗等行业,提供从咨询到落地的端到端交付。确保方案不只是PPT。',
href: '/services/solutions',
icon: <Puzzle className="w-7 h-7" />,
colorClass: 'text-accent-amber',
bgClass: 'bg-accent-amber-soft',
highlights: ['行业深耕', '场景化方案', '快速交付', '生态整合', '持续迭代'],
metrics: [
{ value: '30+', label: '行业方案' },
{ value: '85%', label: '客户续约率' },
],
},
];
const SERVICE_PROCESS = [
{ icon: ClipboardList, step: '01', title: '需求分析', desc: '深入了解业务场景与真实痛点,而非凭空假设' },
{ icon: PencilRuler, step: '02', title: '方案设计', desc: '量身定制技术路线,拒绝千篇一律的模板' },
{ icon: Rocket, step: '03', title: '敏捷交付', desc: '快速迭代,每个冲刺周期都有可交付成果' },
{ icon: HeartHandshake, step: '04', title: '持续支持', desc: '上线不是终点,长期陪伴才是真正的承诺' },
];
const SERVICE_MODES = [
{
icon: Briefcase,
title: '项目制',
description: '针对明确的需求和交付目标,以项目为单位进行合作。适合有清晰边界的一次性建设类项目。',
highlight: '固定范围 · 固定周期',
},
{
icon: RefreshCcw,
title: '订阅制',
description: '按月或按年持续提供技术支持和迭代优化。适合需要长期维护、持续演进的产品和系统。',
highlight: '持续迭代 · 弹性调整',
},
{
icon: Handshake,
title: '长期陪跑',
description: '以合作伙伴身份深度参与您的数字化进程,从规划到执行全程陪伴。适合正在系统性转型的企业。',
highlight: '深度参与 · 共同成长',
},
];
function GrainOverlay() {
return (
<div
className="absolute inset-0 pointer-events-none opacity-[0.03] mix-blend-overlay"
style={{
backgroundImage: `url("data:image/svg+xml,%3Csvg viewBox='0 0 512 512' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E")`,
backgroundRepeat: 'repeat',
backgroundSize: '300px 300px',
}}
/>
);
}
function SectionLabel({ children, className = '' }: { children: React.ReactNode; className?: string }) {
return (
<div className={cn('flex items-center gap-5 mb-7', className)}>
<div className="w-14 h-px bg-brand" />
<span className="text-[11px] tracking-[0.4em] uppercase font-bold text-brand">
{children}
</span>
</div>
);
}
function HeroSection() {
const containerRef = useRef<HTMLDivElement>(null);
return (
<section
ref={containerRef}
className="relative min-h-[85vh] flex items-center overflow-hidden bg-ink"
>
<GrainOverlay />
<div className="absolute inset-0 pointer-events-none">
<div
className="absolute top-0 right-0 w-[42%] h-full"
style={{
background: 'linear-gradient(135deg, transparent 25%, rgba(196, 30, 58, 0.094) 100%)',
clipPath: 'polygon(35% 0, 100% 0, 100% 100%, 0% 100%)',
}}
/>
<div className="absolute bottom-0 left-0 w-full h-2/5 bg-gradient-to-t from-ink to-transparent" />
<div className="absolute top-24 right-[22%] w-[350px] h-[350px] rounded-full opacity-15 blur-3xl bg-brand" />
</div>
<div className="max-w-container mx-auto px-6 lg:px-10 relative z-10 w-full py-32 lg:py-40">
<div className="max-w-4xl">
<motion.div
initial={{ opacity: 0, x: -50 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 1.1, ease: EASE_OUT }}
className="mb-10"
>
<SectionLabel>Professional Services</SectionLabel>
</motion.div>
<motion.h1
initial={{ opacity: 0, y: 70 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 1.2, delay: 0.15, ease: EASE_OUT }}
className="text-5xl sm:text-6xl md:text-7xl lg:text-8xl font-bold text-white leading-[0.85] tracking-tighter mb-12"
>
<div className="block"></div>
<div className="block mt-4">
<span className="relative">
<span className="text-brand"></span>
<motion.span
className="absolute -bottom-2 left-0 w-full h-1 bg-brand"
style={{ transformOrigin: 'left' }}
initial={{ scaleX: 0 }}
animate={{ scaleX: 1 }}
transition={{ duration: 0.8, delay: 1.2, ease: EASE_OUT }}
/>
</span>
</div>
</motion.h1>
<motion.p
initial={{ opacity: 0, y: 50 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 1, delay: 0.45, ease: EASE_OUT }}
className="text-xl lg:text-2xl text-dark-text-secondary max-w-2xl leading-relaxed mb-12"
>
12
</motion.p>
<motion.div
initial={{ opacity: 0, y: 50 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 1, delay: 0.6, ease: EASE_OUT }}
className="flex flex-wrap gap-6"
>
<Button size="xl" asChild>
<a href="/contact">
<ArrowRight className="w-5 h-5 ml-2" />
</a>
</Button>
<Button size="xl" variant="outline" asChild>
<a href="/products"></a>
</Button>
</motion.div>
</div>
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.9, ease: EASE_OUT }}
className="grid grid-cols-3 gap-12 mt-24 pt-16 border-t border-white/10 max-w-2xl"
>
<div>
<div className="text-5xl font-bold text-white mb-3">4<span className="text-brand"></span></div>
<div className="text-sm text-dark-text-muted"></div>
</div>
<div>
<div className="text-5xl font-bold text-white mb-3">3<span className="text-brand"></span></div>
<div className="text-sm text-dark-text-muted"></div>
</div>
<div>
<div className="text-5xl font-bold text-white mb-3"><span className="text-brand"></span></div>
<div className="text-sm text-dark-text-muted"></div>
</div>
</motion.div>
</div>
</section>
);
}
function ServicesGridSection() {
return (
<section className="relative py-36 lg:py-44 overflow-hidden bg-ink-light">
<GrainOverlay />
<div className="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-white/12 to-transparent" />
<div className="max-w-container mx-auto px-6 lg:px-10">
<div className="flex flex-col lg:flex-row lg:items-end lg:justify-between gap-12 mb-28">
<ScrollReveal className="lg:max-w-3xl">
<SectionLabel>Service Capabilities</SectionLabel>
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold text-white tracking-tight leading-tight">
</h2>
</ScrollReveal>
<ScrollReveal delay={0.1}>
<p className="text-dark-text-secondary max-w-md leading-relaxed text-lg">
</p>
</ScrollReveal>
</div>
<StaggerReveal
className="grid md:grid-cols-2 gap-px bg-white/10"
staggerDelay={0.1}
delayChildren={0.05}
>
{SERVICES.map((service, i) => (
<a
key={i}
href={service.href}
className="group relative block p-12 lg:p-16 transition-all duration-600 hover:bg-white/[0.02] overflow-hidden bg-ink-light"
>
<div
className={cn(
'absolute top-0 left-0 h-full w-[3px] scale-y-0 group-hover:scale-y-100 transition-transform duration-700 origin-top',
)}
style={{ backgroundColor: 'currentColor' }}
/>
<div className="relative z-10">
<div className="flex items-start justify-between mb-12">
<motion.div
whileHover={{ scale: 1.1, y: -4 }}
transition={{ type: 'spring', stiffness: 300, damping: 30 }}
className={cn('w-16 h-16 flex items-center justify-center', service.bgClass, service.colorClass)}
>
{service.icon}
</motion.div>
<span className="text-7xl font-bold text-white/[0.04] group-hover:text-white/[0.08] transition-colors duration-500 tracking-tight">
{service.number}
</span>
</div>
<div className="mb-8">
<div className="text-[11px] tracking-[0.35em] uppercase text-dark-text-muted mb-3 font-semibold">
{service.subtitle}
</div>
<h3 className="text-2xl lg:text-3xl font-bold text-white group-hover:translate-x-2 transition-transform duration-500">
{service.title}
</h3>
</div>
<p className="text-dark-text-secondary leading-relaxed mb-10">
{service.desc}
</p>
<div className="flex flex-wrap gap-3 mb-12">
{service.highlights.map((h, j) => (
<span
key={j}
className="px-4 py-2 text-xs text-dark-text-muted border border-white/10 group-hover:border-white/20 group-hover:text-dark-text-secondary transition-all duration-300 font-medium"
>
{h}
</span>
))}
</div>
<div className="flex items-center gap-8 pt-8 border-t border-white/[0.06]">
{service.metrics.map((m, j) => (
<div key={j}>
<div className="text-2xl font-bold text-white">{m.value}</div>
<div className="text-xs text-dark-text-muted mt-1.5">{m.label}</div>
</div>
))}
</div>
<motion.div
className={cn('inline-flex items-center gap-3 text-sm font-bold mt-12', service.colorClass)}
whileHover={{ x: 8 }}
transition={{ type: 'spring', stiffness: 300, damping: 30 }}
>
<span></span>
<ArrowRight className="w-4 h-4" />
</motion.div>
</div>
</a>
))}
</StaggerReveal>
</div>
</section>
);
}
function ProcessSection() {
return (
<section className="relative py-36 lg:py-44 overflow-hidden bg-ink">
<GrainOverlay />
<div className="max-w-container mx-auto px-6 lg:px-10">
<ScrollReveal className="mb-28 max-w-3xl">
<SectionLabel>Our Process</SectionLabel>
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold text-white tracking-tight leading-tight mb-8">
</h2>
<p className="text-dark-text-secondary leading-relaxed text-lg">
</p>
</ScrollReveal>
<div className="relative">
<div className="absolute top-[115px] left-0 right-0 h-px hidden lg:block bg-brand/30" />
<StaggerReveal
className="grid md:grid-cols-2 lg:grid-cols-4 gap-px bg-white/10"
staggerDelay={0.12}
delayChildren={0.1}
>
{SERVICE_PROCESS.map((item) => {
const Icon = item.icon;
return (
<div
key={item.step}
className="relative h-full p-12 lg:p-14 border-t-2 border-transparent hover:border-brand transition-all duration-500 group bg-ink"
>
<div className="absolute -top-[2px] left-0 w-full flex justify-center hidden lg:flex">
<div className="w-5 h-5 rounded-full -mt-[9px] border-2 border-brand bg-ink" />
</div>
<div className="text-center">
<div className="mx-auto mb-8 w-20 h-20 rounded-2xl bg-ink-light border border-white/10 flex items-center justify-center relative">
<Icon className="w-8 h-8 text-white" strokeWidth={1.5} />
<span className="absolute -top-2 -right-2 w-7 h-7 rounded-full bg-brand text-white text-xs font-bold flex items-center justify-center">
{item.step}
</span>
</div>
<h3 className="text-lg font-bold text-white mb-3">{item.title}</h3>
<p className="text-sm text-dark-text-muted leading-relaxed">{item.desc}</p>
</div>
</div>
);
})}
</StaggerReveal>
</div>
</div>
</section>
);
}
function ModesSection() {
return (
<section className="relative py-36 lg:py-44 overflow-hidden bg-ink-light">
<GrainOverlay />
<div className="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-white/12 to-transparent" />
<div className="max-w-container mx-auto px-6 lg:px-10">
<div className="flex flex-col lg:flex-row lg:items-end lg:justify-between gap-12 mb-28">
<ScrollReveal className="lg:max-w-3xl">
<SectionLabel>Engagement Models</SectionLabel>
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold text-white tracking-tight leading-tight">
</h2>
</ScrollReveal>
<ScrollReveal delay={0.1}>
<p className="text-dark-text-secondary max-w-md leading-relaxed text-lg">
</p>
</ScrollReveal>
</div>
<StaggerReveal
className="grid md:grid-cols-3 gap-8"
staggerDelay={0.1}
delayChildren={0.05}
>
{SERVICE_MODES.map((mode) => {
const Icon = mode.icon;
return (
<div
key={mode.title}
className="group relative p-10 lg:p-12 border border-white/10 hover:border-brand/30 bg-ink transition-all duration-500 hover:-translate-y-1"
>
<div className="absolute top-0 left-0 right-0 h-1 bg-brand scale-x-0 group-hover:scale-x-100 transition-transform duration-700 origin-left" />
<div className="relative z-10">
<div className="w-14 h-14 rounded-xl bg-brand-soft flex items-center justify-center mb-8 text-brand">
<Icon className="w-7 h-7" strokeWidth={1.8} />
</div>
<h3 className="text-2xl font-bold text-white mb-5">{mode.title}</h3>
<p className="text-dark-text-secondary leading-relaxed mb-8">{mode.description}</p>
<div className="flex items-center gap-3 pt-6 border-t border-white/10">
<CheckCircle2 className="w-5 h-5 text-brand shrink-0" />
<span className="text-sm font-medium text-white">{mode.highlight}</span>
</div>
</div>
</div>
);
})}
</StaggerReveal>
</div>
</section>
);
}
function CTASection() {
return (
<section className="relative py-36 lg:py-44 overflow-hidden bg-ink">
<GrainOverlay />
<div className="absolute inset-0 pointer-events-none">
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[600px] h-[600px] rounded-full opacity-[0.08] blur-3xl bg-brand" />
</div>
<div className="max-w-container mx-auto px-6 lg:px-10 relative z-10">
<ScrollReveal className="max-w-3xl mx-auto text-center">
<SectionLabel className="justify-center">
<div className="flex items-center gap-5">
<div className="w-14 h-px bg-brand" />
<span className="text-[11px] tracking-[0.4em] uppercase font-bold text-brand">
Get Started
</span>
<div className="w-14 h-px bg-brand" />
</div>
</SectionLabel>
<h2 className="text-3xl md:text-4xl lg:text-6xl font-bold text-white tracking-tight leading-tight mb-8">
</h2>
<p className="text-xl text-dark-text-secondary leading-relaxed mb-12 max-w-2xl mx-auto">
</p>
<div className="flex flex-wrap gap-6 justify-center">
<Button size="xl" asChild>
<a href="/contact">
<ArrowRight className="w-5 h-5 ml-2" />
</a>
</Button>
<Button size="xl" variant="outline" asChild>
<a href="/team"></a>
</Button>
</div>
</ScrollReveal>
</div>
</section>
);
}
export default function ServicesContentV1() {
return (
<main>
<HeroSection />
<ServicesGridSection />
<ProcessSection />
<ModesSection />
<CTASection />
</main>
);
}
@@ -0,0 +1,529 @@
'use client';
import { motion } from 'framer-motion';
import { ScrollReveal, StaggerReveal } from '@/components/ui/scroll-reveal';
import { Button } from '@/components/ui/button';
import { ArrowRight, Code, BarChart3, Lightbulb, Puzzle, ClipboardList, PencilRuler, Rocket, HeartHandshake, Briefcase, RefreshCcw, Handshake, CheckCircle2 } from 'lucide-react';
import { GrainOverlay, FloatingInkParticles, SectionLabel, EASE_OUT } from '@/components/ui/page-decoration';
import { cn } from '@/lib/utils';
const EASE_SPRING = { type: 'spring', stiffness: 300, damping: 30 } as const;
const SERVICES_PARTICLES = [
{ x: 12, y: 35, scale: 0.8, duration: 10, delay: 0, size: 3 },
{ x: 85, y: 55, scale: 0.6, duration: 12, delay: 1.5, size: 2 },
{ x: 48, y: 45, scale: 1.0, duration: 9, delay: 0.8, size: 4 },
{ x: 70, y: 28, scale: 0.7, duration: 11, delay: 2.2, size: 2.5 },
{ x: 30, y: 78, scale: 0.9, duration: 8.5, delay: 1.0, size: 3.5 },
{ x: 65, y: 68, scale: 0.55, duration: 13, delay: 2.8, size: 2 },
];
const SERVICES = [
{
number: '01',
title: '软件开发',
subtitle: 'Software Development',
desc: '从需求分析到上线运维,全栈定制化开发。用扎实的工程能力交付高质量软件,确保每个项目都能创造真实业务价值。',
href: '/services/software',
icon: <Code className="w-7 h-7" />,
color: '#C41E3A',
colorClass: 'text-brand',
bgClass: 'bg-brand-soft',
highlights: ['定制化开发', '全栈技术栈', '敏捷开发', '质量保证', '持续支持'],
metrics: [
{ value: '95%+', label: '准时交付率' },
{ value: 'A+', label: '代码质量' },
],
},
{
number: '02',
title: '数据分析',
subtitle: 'Data Analytics',
desc: '让数据从"存着"变成"用着"。多源数据整合、可视化看板、预测模型,为经营决策提供可量化的洞察支撑。',
href: '/services/data',
icon: <BarChart3 className="w-7 h-7" />,
color: '#3b82f6',
colorClass: 'text-accent-blue',
bgClass: 'bg-accent-blue-soft',
highlights: ['数据整合', '可视化分析', '预测模型', '实时分析', '洞察挖掘'],
metrics: [
{ value: '100+', label: '分析模型' },
{ value: '80%+', label: '洞察转化率' },
],
},
{
number: '03',
title: '技术咨询',
subtitle: 'Tech Consulting',
desc: 'IT 战略规划、技术选型评估、数字化转型路线图——帮您理清方向,规避技术风险,减少试错成本。',
href: '/services/consulting',
icon: <Lightbulb className="w-7 h-7" />,
color: '#14b8a6',
colorClass: 'text-accent-teal',
bgClass: 'bg-accent-teal-soft',
highlights: ['IT战略规划', '技术选型评估', '转型路线图', '架构评审', '团队建设'],
metrics: [
{ value: '90%+', label: '方案落地率' },
{ value: '50+', label: '咨询项目' },
],
},
{
number: '04',
title: '行业方案实施',
subtitle: 'Industry Solutions',
desc: '深耕制造、零售、金融、医疗等行业,提供从咨询到落地的端到端交付。确保方案不只是PPT。',
href: '/services/solutions',
icon: <Puzzle className="w-7 h-7" />,
color: '#f59e0b',
colorClass: 'text-accent-amber',
bgClass: 'bg-accent-amber-soft',
highlights: ['行业深耕', '场景化方案', '快速交付', '生态整合', '持续迭代'],
metrics: [
{ value: '30+', label: '行业方案' },
{ value: '85%', label: '客户续约率' },
],
},
];
const SERVICE_PROCESS = [
{ icon: ClipboardList, step: '01', title: '需求分析', desc: '深入了解业务场景与真实痛点,而非凭空假设' },
{ icon: PencilRuler, step: '02', title: '方案设计', desc: '量身定制技术路线,拒绝千篇一律的模板' },
{ icon: Rocket, step: '03', title: '敏捷交付', desc: '快速迭代,每个冲刺周期都有可交付成果' },
{ icon: HeartHandshake, step: '04', title: '持续支持', desc: '上线不是终点,长期陪伴才是真正的承诺' },
];
const SERVICE_MODES = [
{
icon: Briefcase,
title: '项目制',
description: '针对明确的需求和交付目标,以项目为单位进行合作。适合有清晰边界的一次性建设类项目。',
highlight: '固定范围 · 固定周期',
color: '#C41E3A',
},
{
icon: RefreshCcw,
title: '订阅制',
description: '按月或按年持续提供技术支持和迭代优化。适合需要长期维护、持续演进的产品和系统。',
highlight: '持续迭代 · 弹性调整',
color: '#3b82f6',
},
{
icon: Handshake,
title: '长期陪跑',
description: '以合作伙伴身份深度参与您的数字化进程,从规划到执行全程陪伴。适合正在系统性转型的企业。',
highlight: '深度参与 · 共同成长',
color: '#14b8a6',
},
];
function HeroSection() {
return (
<section className="relative min-h-[85vh] flex items-center overflow-hidden bg-ink">
<GrainOverlay />
<FloatingInkParticles particles={SERVICES_PARTICLES} />
<div className="absolute inset-0 pointer-events-none">
<div
className="absolute top-0 right-0 w-[42%] h-full"
style={{
background: 'linear-gradient(145deg, transparent 25%, rgba(196, 30, 58, 0.03) 100%)',
clipPath: 'polygon(35% 0, 100% 0, 100% 100%, 0% 100%)',
}}
/>
<div
className="absolute bottom-0 left-0 w-[50%] h-full"
style={{
background: 'linear-gradient(220deg, transparent 35%, rgba(20, 184, 166, 0.02) 100%)',
clipPath: 'polygon(0 0, 65% 0, 100% 100%, 0 100%)',
}}
/>
<div className="absolute bottom-0 left-0 w-full h-2/5 bg-gradient-to-t from-ink to-transparent" />
<div className="absolute top-24 right-[22%] w-[360px] h-[360px] rounded-full opacity-[0.04] blur-3xl bg-brand" />
<div className="absolute bottom-28 left-[15%] w-[300px] h-[300px] rounded-full opacity-[0.025] blur-3xl bg-teal-500" />
</div>
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10 w-full py-24 sm:py-28 md:py-32 lg:py-40">
<div className="max-w-4xl">
<motion.div
initial={{ opacity: 0, x: -50 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 1.1, ease: EASE_OUT }}
className="mb-10"
>
<SectionLabel>Professional Services</SectionLabel>
</motion.div>
<motion.h1
initial={{ opacity: 0, y: 70 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 1.2, delay: 0.15, ease: EASE_OUT }}
className="text-5xl sm:text-6xl md:text-7xl lg:text-8xl font-bold text-white leading-[0.88] tracking-tighter mb-12"
>
<div className="block"></div>
<div className="block mt-5">
<span className="relative inline-block">
<span className="text-brand font-extrabold"></span>
<motion.span
className="absolute -bottom-3 left-0 w-full h-[3px] bg-brand"
style={{ transformOrigin: 'left' }}
initial={{ scaleX: 0 }}
animate={{ scaleX: 1 }}
transition={{ duration: 0.8, delay: 1.2, ease: EASE_OUT }}
/>
</span>
</div>
</motion.h1>
<motion.p
initial={{ opacity: 0, y: 50 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 1, delay: 0.45, ease: EASE_OUT }}
className="text-xl lg:text-2xl text-dark-text-secondary max-w-2xl leading-relaxed mb-12"
>
</motion.p>
<motion.div
initial={{ opacity: 0, y: 50 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 1, delay: 0.6, ease: EASE_OUT }}
className="flex flex-wrap gap-6"
>
<Button size="xl" asChild>
<a href="/contact">
<ArrowRight className="w-5 h-5 ml-2" />
</a>
</Button>
<Button size="xl" variant="outline" asChild>
<a href="/products"></a>
</Button>
</motion.div>
<div className="grid grid-cols-3 gap-12 mt-24 pt-16 border-t border-white/10 max-w-2xl">
<div>
<div className="text-4xl lg:text-5xl font-bold text-white tracking-tight leading-none mb-2">
4<span className="text-brand"></span>
</div>
<div className="text-sm text-dark-text-muted"></div>
</div>
<div>
<div className="text-4xl lg:text-5xl font-bold text-white tracking-tight leading-none mb-2">
3<span className="text-brand"></span>
</div>
<div className="text-sm text-dark-text-muted"></div>
</div>
<div>
<div className="text-4xl lg:text-5xl font-bold text-white tracking-tight leading-none mb-2">
<span className="text-brand"></span>
</div>
<div className="text-sm text-dark-text-muted"></div>
</div>
</div>
</div>
</div>
</section>
);
}
function ServicesGridSection() {
return (
<section className="relative py-20 sm:py-28 md:py-36 lg:py-44 overflow-hidden bg-ink-light">
<GrainOverlay />
<div className="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-white/10 to-transparent" />
<div className="absolute bottom-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-white/10 to-transparent" />
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10">
<div className="flex flex-col lg:flex-row lg:items-end lg:justify-between gap-8 sm:gap-10 md:gap-12 mb-16 sm:mb-20 md:mb-20">
<ScrollReveal className="lg:max-w-3xl">
<SectionLabel>Service Capabilities</SectionLabel>
<h2 className="text-2xl sm:text-3xl md:text-4xl lg:text-5xl font-bold text-white tracking-tight leading-tight">
</h2>
</ScrollReveal>
<ScrollReveal delay={0.1}>
<p className="text-dark-text-secondary max-w-md leading-relaxed text-lg">
</p>
</ScrollReveal>
</div>
<StaggerReveal
className="grid md:grid-cols-2 gap-px bg-white/10"
staggerDelay={0.1}
delayChildren={0.05}
>
{SERVICES.map((service, i) => (
<a
key={i}
href={service.href}
className="group relative block p-10 lg:p-12 transition-all duration-600 hover:bg-white/[0.02] overflow-hidden bg-ink border border-transparent hover:border-white/10"
>
<div
className="absolute top-0 left-0 h-full w-1 scale-y-0 group-hover:scale-y-100 transition-transform duration-700 origin-top"
style={{ backgroundColor: service.color }}
/>
<div className="absolute top-8 right-8 w-48 h-48 rounded-full opacity-0 group-hover:opacity-100 blur-3xl transition-opacity duration-700" style={{ backgroundColor: service.color + '06' }} />
<div className="relative z-10">
<div className="flex items-start justify-between mb-10">
<motion.div
whileHover={{ scale: 1.08, y: -3 }}
transition={EASE_SPRING}
className={cn('w-16 h-16 flex items-center justify-center rounded-2xl', service.bgClass, service.colorClass)}
>
{service.icon}
</motion.div>
<span className="text-7xl font-bold text-white/[0.05] group-hover:text-white/[0.1] transition-all duration-500 tracking-tighter leading-none">
{service.number}
</span>
</div>
<div className="mb-8">
<div className="text-[11px] tracking-[0.35em] uppercase text-dark-text-muted mb-3 font-semibold">
{service.subtitle}
</div>
<h3 className="text-2xl lg:text-3xl font-bold text-white group-hover:translate-x-1.5 transition-transform duration-500">
{service.title}
</h3>
</div>
<p className="text-dark-text-secondary leading-relaxed mb-8 text-[15px]">
{service.desc}
</p>
<div className="flex flex-wrap gap-2.5 mb-8">
{service.highlights.map((h, j) => (
<span
key={j}
className="px-3 py-1.5 text-xs text-dark-text-muted border border-white/10 group-hover:border-white/20 group-hover:text-dark-text-secondary transition-all duration-300 font-medium"
>
{h}
</span>
))}
</div>
<div className="flex items-center gap-8 pt-6 border-t border-white/[0.06] mb-8">
{service.metrics.map((m, j) => (
<div key={j}>
<div className="text-2xl font-bold text-white">{m.value}</div>
<div className="text-xs text-dark-text-muted mt-1.5">{m.label}</div>
</div>
))}
</div>
<motion.div
className={cn('inline-flex items-center gap-2.5 text-sm font-bold', service.colorClass)}
whileHover={{ x: 6 }}
transition={EASE_SPRING}
>
<span></span>
<ArrowRight className="w-4 h-4" />
</motion.div>
</div>
</a>
))}
</StaggerReveal>
</div>
</section>
);
}
function ProcessSection() {
return (
<section className="relative py-20 sm:py-28 md:py-36 lg:py-44 overflow-hidden bg-ink">
<GrainOverlay />
<div className="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-white/10 to-transparent" />
<div className="absolute bottom-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-white/10 to-transparent" />
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10">
<ScrollReveal className="mb-16 sm:mb-20 md:mb-20 max-w-3xl">
<SectionLabel>Our Process</SectionLabel>
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold text-white tracking-tight leading-tight mb-8">
</h2>
<p className="text-dark-text-secondary leading-relaxed text-lg">
</p>
</ScrollReveal>
<div className="relative">
<div className="absolute top-[115px] left-0 right-0 h-px hidden lg:block bg-brand/20" />
<StaggerReveal
className="grid md:grid-cols-2 lg:grid-cols-4 gap-px bg-white/10"
staggerDelay={0.12}
delayChildren={0.1}
>
{SERVICE_PROCESS.map((item) => {
const Icon = item.icon;
return (
<div
key={item.step}
className="relative h-full p-10 lg:p-12 border-t-2 border-transparent hover:border-brand transition-all duration-500 group bg-ink"
>
<div className="absolute -top-[2px] left-0 w-full flex justify-center hidden lg:flex">
<div className="w-5 h-5 rounded-full -mt-[9px] border-2 border-brand bg-ink" />
</div>
<div className="text-center">
<div className="mx-auto mb-8 w-20 h-20 rounded-2xl bg-ink-light border border-white/10 flex items-center justify-center relative">
<Icon className="w-8 h-8 text-white" strokeWidth={1.5} />
<span className="absolute -top-2 -right-2 w-7 h-7 rounded-full bg-brand text-white text-xs font-bold flex items-center justify-center">
{item.step}
</span>
</div>
<h3 className="text-lg font-bold text-white mb-3">{item.title}</h3>
<p className="text-sm text-dark-text-muted leading-relaxed">{item.desc}</p>
</div>
</div>
);
})}
</StaggerReveal>
</div>
</div>
</section>
);
}
function ModesSection() {
return (
<section className="relative py-20 sm:py-28 md:py-36 lg:py-44 overflow-hidden bg-ink-light">
<GrainOverlay />
<div className="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-white/10 to-transparent" />
<div className="absolute bottom-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-white/10 to-transparent" />
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10">
<div className="flex flex-col lg:flex-row lg:items-end lg:justify-between gap-8 sm:gap-10 md:gap-12 mb-16 sm:mb-20 md:mb-20">
<ScrollReveal className="lg:max-w-3xl">
<SectionLabel>Engagement Models</SectionLabel>
<h2 className="text-2xl sm:text-3xl md:text-4xl lg:text-5xl font-bold text-white tracking-tight leading-tight">
</h2>
</ScrollReveal>
<ScrollReveal delay={0.1}>
<p className="text-dark-text-secondary max-w-md leading-relaxed text-lg">
</p>
</ScrollReveal>
</div>
<StaggerReveal
className="grid md:grid-cols-3 gap-px bg-white/10"
staggerDelay={0.1}
delayChildren={0.05}
>
{SERVICE_MODES.map((mode) => {
const Icon = mode.icon;
return (
<div
key={mode.title}
className="group relative p-10 lg:p-12 bg-ink transition-all duration-500 hover:bg-white/[0.02]"
>
<div
className="absolute top-0 left-0 h-full w-1 scale-y-0 group-hover:scale-y-100 transition-transform duration-700 origin-top"
style={{ backgroundColor: mode.color }}
/>
<div className="absolute top-8 right-8 w-40 h-40 rounded-full opacity-0 group-hover:opacity-100 blur-3xl transition-opacity duration-700" style={{ backgroundColor: mode.color + '06' }} />
<div className="relative z-10">
<div
className="w-14 h-14 rounded-xl flex items-center justify-center mb-8"
style={{ backgroundColor: mode.color + '12', color: mode.color }}
>
<Icon className="w-7 h-7" strokeWidth={1.8} />
</div>
<h3 className="text-2xl font-bold text-white mb-5">{mode.title}</h3>
<p className="text-dark-text-secondary leading-relaxed mb-8 text-[15px]">{mode.description}</p>
<div className="flex items-center gap-3 pt-6 border-t border-white/10">
<CheckCircle2 className="w-5 h-5 shrink-0" style={{ color: mode.color }} />
<span className="text-sm font-medium text-white">{mode.highlight}</span>
</div>
</div>
</div>
);
})}
</StaggerReveal>
</div>
</section>
);
}
function CTASection() {
return (
<section className="relative py-20 sm:py-28 md:py-36 lg:py-44 overflow-hidden bg-ink">
<GrainOverlay />
<FloatingInkParticles />
<div className="absolute inset-0 pointer-events-none">
<div className="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-brand/30 to-transparent" />
<div className="absolute bottom-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-brand/30 to-transparent" />
<div className="absolute top-1/4 right-[10%] w-[380px] h-[380px] rounded-full opacity-[0.03] blur-3xl bg-brand" />
<div className="absolute bottom-1/4 left-[10%] w-[320px] h-[320px] rounded-full opacity-[0.02] blur-3xl bg-teal-500" />
</div>
<div className="max-w-container mx-auto px-6 lg:px-10 relative z-10">
<ScrollReveal className="text-center max-w-3xl mx-auto">
<div className="flex justify-center mb-7">
<div className="flex items-center gap-5">
<div className="w-14 h-px bg-brand" />
<span className="text-[11px] tracking-[0.4em] uppercase font-bold text-brand">
Get Started
</span>
<div className="w-14 h-px bg-brand" />
</div>
</div>
<h2 className="text-2xl sm:text-3xl md:text-4xl lg:text-5xl font-bold text-white tracking-tight leading-tight mb-6 sm:mb-8">
<br />
<span className="text-brand"></span>
</h2>
<p className="text-xl text-dark-text-secondary mb-12 max-w-2xl mx-auto leading-relaxed">
</p>
<div className="flex flex-wrap justify-center gap-6">
<Button size="xl" asChild>
<a href="/contact">
<ArrowRight className="w-5 h-5 ml-2" />
</a>
</Button>
<Button size="xl" variant="outline" asChild>
<a href="/team"></a>
</Button>
</div>
</ScrollReveal>
</div>
</section>
);
}
export default function ServicesContentV2() {
return (
<main>
<HeroSection />
<ServicesGridSection />
<ProcessSection />
<ModesSection />
<CTASection />
</main>
);
}
@@ -0,0 +1,410 @@
'use client';
import { motion } from 'framer-motion';
import { ArrowUpRight, Code, BarChart3, Lightbulb, Puzzle, type LucideIcon } from 'lucide-react';
import type { Service } from '@/lib/constants/services';
const EASE_OUT = [0.22, 1, 0.36, 1] as const;
/** Icon name-to-component mapping for CMS string-based icons */
const ICON_MAP: Record<string, LucideIcon> = {
Lightbulb,
Code,
BarChart3,
Puzzle,
};
/** UI metadata keyed by service ID — presentation-only fields not stored in CMS */
const SERVICE_UI_META: Record<string, { subtitle: string; metrics: { value: string; label: string }[] }> = {
consulting: { subtitle: 'Strategy Consulting', metrics: [{ value: '90%+', label: '方案落地率' }, { value: '50+', label: '咨询项目' }] },
software: { subtitle: 'Enterprise Software', metrics: [{ value: '95%+', label: '准时交付率' }, { value: 'A+', label: '代码质量' }] },
data: { subtitle: 'Technology Services', metrics: [{ value: '100+', label: '技术方案' }, { value: '24/7', label: '运维支持' }] },
solutions: { subtitle: 'AI Empowerment', metrics: [{ value: '30+', label: 'AI 场景' }, { value: '85%', label: '效率提升' }] },
};
function mapServicesToCards(services: Service[]) {
return services.map((svc, i) => {
const uiMeta = SERVICE_UI_META[svc.id];
return {
number: String(i + 1).padStart(2, '0'),
title: svc.title,
subtitle: uiMeta?.subtitle ?? '',
desc: svc.description,
href: `/services/${svc.id}`,
icon: ICON_MAP[svc.icon] ?? Lightbulb,
highlights: svc.features?.slice(0, 4) ?? [],
metrics: uiMeta?.metrics ?? [],
};
});
}
const SERVICE_PROCESS = [
{ step: '01', title: '需求分析', desc: '深入了解业务场景与真实痛点,而非凭空假设' },
{ step: '02', title: '方案设计', desc: '量身定制技术路线,拒绝千篇一律的模板' },
{ step: '03', title: '敏捷交付', desc: '快速迭代,每个冲刺周期都有可交付成果' },
{ step: '04', title: '持续支持', desc: '上线不是终点,长期陪伴才是真正的承诺' },
];
const SERVICE_MODES = [
{
title: '项目制',
description: '针对明确的需求和交付目标,以项目为单位进行合作。适合有清晰边界的一次性建设类项目。',
highlight: '固定范围 · 固定周期',
},
{
title: '订阅制',
description: '按月或按年持续提供技术支持和迭代优化。适合需要长期维护、持续演进的产品和系统。',
highlight: '持续迭代 · 弹性调整',
},
{
title: '长期陪跑',
description: '以合作伙伴身份深度参与您的数字化进程,从规划到执行全程陪伴。适合正在系统性转型的企业。',
highlight: '深度参与 · 共同成长',
},
];
function HeroSection() {
return (
<section className="relative min-h-[85vh] flex items-center overflow-hidden bg-white">
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10 w-full py-28 sm:py-36 md:py-44 lg:py-56">
<div className="max-w-4xl">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.1, ease: EASE_OUT }}
className="text-sm font-mono tracking-[0.2em] text-text-muted uppercase mb-8"
>
Professional Services
</motion.div>
<motion.h1
initial={{ opacity: 0, y: 40 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 1, delay: 0.2, ease: EASE_OUT }}
className="text-4xl sm:text-5xl md:text-6xl lg:text-7xl xl:text-8xl font-black text-ink leading-[0.88] tracking-tightest mb-10 sm:mb-12 md:mb-16"
>
<div className="block"></div>
<div className="block mt-4 sm:mt-6">
<span className="relative inline-block">
<span className="relative text-brand font-black">
<motion.span
className="absolute -bottom-2 left-0 w-full h-[3px] bg-brand"
style={{ transformOrigin: 'left' }}
initial={{ scaleX: 0 }}
animate={{ scaleX: 1 }}
transition={{ duration: 0.8, delay: 1.2, ease: EASE_OUT }}
/>
</span>
</span>
</div>
</motion.h1>
<motion.p
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.5, ease: EASE_OUT }}
className="text-lg md:text-xl text-text-secondary max-w-2xl leading-relaxed mb-12 sm:mb-16"
>
<br className="hidden sm:block" />
</motion.p>
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.7, ease: EASE_OUT }}
className="flex flex-col sm:flex-row gap-4 sm:gap-6"
>
<a
href="/contact"
className="group inline-flex items-center justify-center gap-3 px-10 py-5 font-semibold text-base bg-brand text-white transition-all duration-300 hover:bg-brand/90 min-h-[52px] touch-manipulation"
>
<ArrowUpRight className="w-4 h-4 transition-transform duration-300 group-hover:translate-x-0.5 group-hover:-translate-y-0.5" />
</a>
<a
href="/products"
className="inline-flex items-center justify-center gap-3 px-10 py-5 font-semibold text-base text-ink border border-border-primary hover:border-ink/30 transition-all duration-300 min-h-[52px] touch-manipulation"
>
</a>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.9, ease: EASE_OUT }}
className="grid grid-cols-3 gap-8 mt-20 pt-12 border-t border-border-primary max-w-xl"
>
<div>
<div className="text-3xl sm:text-4xl font-black text-ink tracking-tight leading-none mb-2">
4<span className="text-brand"></span>
</div>
<div className="text-sm text-text-muted"></div>
</div>
<div>
<div className="text-3xl sm:text-4xl font-black text-ink tracking-tight leading-none mb-2">
3<span className="text-brand"></span>
</div>
<div className="text-sm text-text-muted"></div>
</div>
<div>
<div className="text-3xl sm:text-4xl font-black text-ink tracking-tight leading-none mb-2">
<span className="text-brand"></span>
</div>
<div className="text-sm text-text-muted"></div>
</div>
</motion.div>
</div>
</div>
</section>
);
}
function ServicesGridSection({ services }: { services?: Service[] }) {
const displayServices = services?.length ? mapServicesToCards(services) : [];
if (displayServices.length === 0) {
return (
<section className="relative py-20 sm:py-28 md:py-36 lg:py-44 overflow-hidden bg-bg-secondary">
<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" />
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 text-center">
<p className="text-text-muted text-lg"></p>
</div>
</section>
);
}
return (
<section className="relative py-20 sm:py-28 md:py-36 lg:py-44 overflow-hidden bg-bg-secondary">
<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" />
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10">
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-80px' }}
transition={{ duration: 0.8, ease: EASE_OUT }}
className="max-w-3xl mb-16 sm:mb-20 md:mb-24"
>
<div className="text-sm font-mono tracking-[0.2em] text-text-muted uppercase mb-6">
Service Capabilities
</div>
<h2 className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-extrabold text-ink tracking-tight leading-[0.95]">
<br className="sm:hidden" />
</h2>
</motion.div>
<div className="grid md:grid-cols-2 gap-px bg-border-primary">
{displayServices.map((service, i) => {
const Icon = service.icon;
return (
<motion.a
key={i}
href={service.href}
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-60px' }}
transition={{ duration: 0.6, delay: i * 0.08, ease: EASE_OUT }}
className="group relative block p-8 sm:p-10 md:p-12 transition-all duration-500 hover:bg-bg-secondary bg-white border border-transparent hover:border-border-primary"
>
<div className="absolute top-0 left-0 h-0 w-[2px] group-hover:h-full transition-all duration-500 origin-top bg-brand" />
<div className="relative z-10">
<div className="flex items-start justify-between mb-8">
<div className={`w-12 h-12 flex items-center justify-center ${
i === 0 ? 'text-brand' : 'text-text-secondary'
}`}>
<Icon className="w-6 h-6" strokeWidth={1.5} />
</div>
<span className="text-5xl sm:text-6xl font-black text-ink/[0.04] tracking-tighter leading-none">
{service.number}
</span>
</div>
<div className="mb-6">
<div className="text-[11px] tracking-[0.3em] uppercase text-text-muted mb-3 font-semibold">
{service.subtitle}
</div>
<h3 className="text-xl sm:text-2xl md:text-3xl font-bold text-ink group-hover:text-brand transition-colors duration-300">
{service.title}
</h3>
</div>
<p className="text-text-secondary leading-relaxed mb-6 text-[15px]">
{service.desc}
</p>
<div className="flex flex-wrap gap-2 mb-6">
{service.highlights.map((h, j) => (
<span
key={j}
className="px-3 py-1 text-xs text-text-secondary border border-border-primary group-hover:border-ink/20 group-hover:text-ink transition-all duration-300 font-medium"
>
{h}
</span>
))}
</div>
<div className="flex items-center gap-8 pt-5 border-t border-border-primary mb-6">
{service.metrics.map((m, j) => (
<div key={j}>
<div className="text-xl font-bold text-ink">{m.value}</div>
<div className="text-xs text-text-muted mt-1">{m.label}</div>
</div>
))}
</div>
<div className="inline-flex items-center gap-2 text-sm font-semibold text-text-secondary group-hover:text-brand transition-colors duration-300">
<span></span>
<ArrowUpRight className="w-4 h-4 group-hover:translate-x-0.5 group-hover:-translate-y-0.5 transition-transform duration-300" />
</div>
</div>
</motion.a>
);
})}
</div>
</div>
</section>
);
}
function ProcessModesSection() {
return (
<section className="relative py-20 sm:py-28 md:py-36 lg:py-44 overflow-hidden bg-white">
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10">
<div className="grid lg:grid-cols-12 gap-12 sm:gap-16 md:gap-20">
{/* Process */}
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-80px' }}
transition={{ duration: 0.8, ease: EASE_OUT }}
className="lg:col-span-7"
>
<div className="text-sm font-mono tracking-[0.2em] text-text-muted uppercase mb-6">
Our Process
</div>
<h2 className="text-3xl sm:text-4xl md:text-5xl font-extrabold text-ink tracking-tight leading-[0.95] mb-10 sm:mb-14">
</h2>
<div className="space-y-0">
{SERVICE_PROCESS.map((item, idx) => (
<div
key={item.step}
className="relative pl-8 sm:pl-12 pb-8 sm:pb-10 last:pb-0"
>
<div className="absolute left-0 top-3 bottom-0 w-px bg-border-primary last:hidden" />
<div className={`absolute left-0 top-3 w-2.5 h-2.5 rounded-full -translate-x-1/2 ${
idx === 0 ? 'bg-brand' : 'bg-text-muted'
}`} />
<div className="flex items-start gap-6">
<div className="text-2xl sm:text-3xl font-black text-ink/[0.06] font-mono shrink-0 leading-none w-14">
{item.step}
</div>
<div className="flex-1">
<h3 className="text-lg sm:text-xl font-bold text-ink mb-2">{item.title}</h3>
<p className="text-text-secondary leading-relaxed text-sm sm:text-base">{item.desc}</p>
</div>
</div>
</div>
))}
</div>
</motion.div>
{/* Modes */}
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-80px' }}
transition={{ duration: 0.8, delay: 0.15, ease: EASE_OUT }}
className="lg:col-span-5"
>
<div className="text-sm font-mono tracking-[0.2em] text-text-muted uppercase mb-6">
Engagement Models
</div>
<h2 className="text-3xl sm:text-4xl md:text-5xl font-extrabold text-ink tracking-tight leading-[0.95] mb-10 sm:mb-14">
</h2>
<div className="space-y-4 sm:space-y-5">
{SERVICE_MODES.map((mode) => (
<div
key={mode.title}
className="group relative p-6 sm:p-7 border border-border-primary bg-white hover:bg-bg-secondary hover:border-ink/20 transition-all duration-300"
>
<div className="absolute top-0 left-0 w-1 h-0 bg-brand group-hover:h-full transition-all duration-500" />
<div className="relative z-10">
<h3 className="text-xl font-bold text-ink mb-3 group-hover:text-brand transition-colors duration-300">
{mode.title}
</h3>
<p className="text-text-secondary leading-relaxed text-sm sm:text-[15px] mb-4">{mode.description}</p>
<div className="text-sm font-medium text-ink">{mode.highlight}</div>
</div>
</div>
))}
</div>
</motion.div>
</div>
</div>
</section>
);
}
function CTASection() {
return (
<section className="relative py-20 sm:py-28 md:py-36 lg:py-44 overflow-hidden bg-bg-secondary">
<div className="absolute top-0 left-0 right-0 h-px bg-border-primary" />
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10">
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-80px' }}
transition={{ duration: 0.8, ease: EASE_OUT }}
className="max-w-3xl"
>
<h2 className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-extrabold text-ink tracking-tight leading-[0.95] mb-8">
</h2>
<p className="text-lg text-text-secondary leading-relaxed mb-10">
<br className="hidden sm:block" />
</p>
<div className="flex flex-col sm:flex-row gap-4 sm:gap-6">
<a
href="/contact"
className="group inline-flex items-center justify-center gap-3 px-10 py-5 font-semibold text-base bg-brand text-white transition-all duration-300 hover:bg-brand/90 min-h-[52px] touch-manipulation"
>
<ArrowUpRight className="w-4 h-4 transition-transform duration-300 group-hover:translate-x-0.5 group-hover:-translate-y-0.5" />
</a>
<a
href="/team"
className="inline-flex items-center justify-center gap-3 px-10 py-5 font-semibold text-base text-ink border border-border-primary hover:border-ink/30 transition-all duration-300 min-h-[52px] touch-manipulation"
>
</a>
</div>
</motion.div>
</div>
</section>
);
}
export default function ServicesContentV3({ services }: { services?: Service[] }) {
return (
<main className="bg-white text-ink">
<HeroSection />
<ServicesGridSection services={services} />
<ProcessModesSection />
<CTASection />
</main>
);
}
@@ -0,0 +1,455 @@
'use client';
import { motion } from 'framer-motion';
import { ScrollReveal, StaggerReveal } from '@/components/ui/scroll-reveal';
import { Button } from '@/components/ui/button';
import { ArrowRight, Factory, ShoppingCart, Heart, GraduationCap, Lightbulb, Settings, Users, TrendingUp, BookOpen, MessageSquare, Calendar, Smartphone, Package, BarChart3 } from 'lucide-react';
import { cn } from '@/lib/utils';
import type { Solution } from '@/lib/constants/solutions';
import { PRODUCTS } from '@/lib/constants/products';
const EASE_OUT = [0.22, 1, 0.36, 1] as const;
const INDUSTRY_ICONS: Record<string, React.ReactNode> = {
: <Factory className="w-8 h-8" />,
: <ShoppingCart className="w-8 h-8" />,
: <Heart className="w-8 h-8" />,
: <GraduationCap className="w-8 h-8" />,
};
const VALUE_ICONS: Record<string, React.ReactNode> = {
Factory: <Factory className="w-6 h-6" />,
Package: <Package className="w-6 h-6" />,
BarChart3: <BarChart3 className="w-6 h-6" />,
Users: <Users className="w-6 h-6" />,
TrendingUp: <TrendingUp className="w-6 h-6" />,
ShoppingCart: <ShoppingCart className="w-6 h-6" />,
GraduationCap: <GraduationCap className="w-6 h-6" />,
BookOpen: <BookOpen className="w-6 h-6" />,
MessageSquare: <MessageSquare className="w-6 h-6" />,
Heart: <Heart className="w-6 h-6" />,
Calendar: <Calendar className="w-6 h-6" />,
Smartphone: <Smartphone className="w-6 h-6" />,
};
function GrainOverlay() {
return (
<div
className="absolute inset-0 pointer-events-none opacity-[0.03] mix-blend-overlay"
style={{
backgroundImage: `url("data:image/svg+xml,%3Csvg viewBox='0 0 512 512' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E")`,
backgroundRepeat: 'repeat',
backgroundSize: '300px 300px',
}}
/>
);
}
function SectionLabel({ children, className = '' }: { children: React.ReactNode; className?: string }) {
return (
<div className={cn('flex items-center gap-5 mb-7', className)}>
<div className="w-14 h-px bg-brand" />
<span className="text-[11px] tracking-[0.4em] uppercase font-bold text-brand">
{children}
</span>
</div>
);
}
interface DetailHeroProps {
solution: Solution;
}
function DetailHero({ solution }: DetailHeroProps) {
const industryIcon = INDUSTRY_ICONS[solution.industry] || <Factory className="w-8 h-8" />;
return (
<section className="relative min-h-[80vh] flex items-center overflow-hidden bg-ink pt-24">
<GrainOverlay />
<div className="absolute inset-0 pointer-events-none">
<div
className="absolute top-0 right-0 w-[50%] h-full"
style={{
background: 'linear-gradient(135deg, transparent 20%, rgba(196, 30, 58, 0.08) 100%)',
clipPath: 'polygon(30% 0, 100% 0, 100% 100%, 0% 100%)',
}}
/>
<div className="absolute bottom-0 left-0 w-full h-1/3 bg-gradient-to-t from-ink to-transparent" />
<div className="absolute top-32 right-[15%] w-[400px] h-[400px] rounded-full opacity-10 blur-3xl bg-brand" />
</div>
<div className="max-w-container mx-auto px-6 lg:px-10 relative z-10 w-full py-24 lg:py-32">
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, ease: EASE_OUT }}
className="flex items-center gap-4 mb-8"
>
<div className="w-14 h-14 rounded-2xl bg-brand-soft flex items-center justify-center text-brand">
{industryIcon}
</div>
<div>
<span className="px-3 py-1.5 text-xs font-bold text-brand bg-brand-soft/50">
{solution.industry}
</span>
<div className="text-dark-text-muted text-sm mt-2">{solution.subtitle}</div>
</div>
</motion.div>
<motion.h1
initial={{ opacity: 0, y: 50 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 1, delay: 0.15, ease: EASE_OUT }}
className="text-4xl sm:text-5xl md:text-6xl lg:text-7xl font-bold text-white leading-tight tracking-tighter mb-8 max-w-4xl"
>
{solution.title}
</motion.h1>
<motion.p
initial={{ opacity: 0, y: 40 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.9, delay: 0.3, ease: EASE_OUT }}
className="text-xl lg:text-2xl text-dark-text-secondary max-w-3xl leading-relaxed mb-12"
>
{solution.description}
</motion.p>
<motion.div
initial={{ opacity: 0, y: 40 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.9, delay: 0.45, ease: EASE_OUT }}
className="flex flex-wrap gap-6"
>
<Button size="xl" asChild>
<a href="/contact">
<ArrowRight className="w-5 h-5 ml-2" />
</a>
</Button>
<Button size="xl" variant="outline" asChild>
<a href="/products"></a>
</Button>
</motion.div>
</div>
</section>
);
}
interface ChallengesSectionProps {
challenges: string[];
}
function ChallengesSection({ challenges }: ChallengesSectionProps) {
return (
<section className="relative py-32 lg:py-40 overflow-hidden bg-ink-light">
<GrainOverlay />
<div className="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-white/12 to-transparent" />
<div className="max-w-container mx-auto px-6 lg:px-10">
<div className="grid lg:grid-cols-2 gap-20 items-start">
<ScrollReveal>
<SectionLabel>Challenges</SectionLabel>
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold text-white tracking-tight leading-tight mb-8">
</h2>
<p className="text-dark-text-secondary text-lg leading-relaxed">
</p>
</ScrollReveal>
<div className="space-y-5">
<StaggerReveal staggerDelay={0.1} delayChildren={0.05}>
{challenges.map((challenge, idx) => (
<motion.div
key={idx}
className="group relative p-8 border border-white/10 bg-ink hover:border-brand/30 transition-all duration-500"
whileHover={{ x: 12 }}
>
<div className="absolute left-0 top-0 bottom-0 w-1 bg-brand scale-y-0 group-hover:scale-y-100 transition-transform duration-500 origin-top" />
<div className="flex items-start gap-6">
<div className="w-10 h-10 rounded-xl bg-brand-soft/50 flex items-center justify-center text-brand shrink-0 mt-0.5">
<span className="text-lg font-bold">{idx + 1}</span>
</div>
<p className="text-lg text-white leading-relaxed">{challenge}</p>
</div>
</motion.div>
))}
</StaggerReveal>
</div>
</div>
</div>
</section>
);
}
interface SolutionsSectionProps {
solutions: string[];
}
function SolutionsSection({ solutions }: SolutionsSectionProps) {
return (
<section className="relative py-32 lg:py-40 overflow-hidden bg-ink">
<GrainOverlay />
<div className="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-white/12 to-transparent" />
<div className="max-w-container mx-auto px-6 lg:px-10">
<ScrollReveal className="text-center max-w-3xl mx-auto mb-24">
<SectionLabel className="justify-center">
<div className="flex items-center gap-5">
<div className="w-14 h-px bg-brand" />
<span className="text-[11px] tracking-[0.4em] uppercase font-bold text-brand">
Solutions
</span>
<div className="w-14 h-px bg-brand" />
</div>
</SectionLabel>
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold text-white tracking-tight leading-tight">
</h2>
</ScrollReveal>
<div className="grid md:grid-cols-2 gap-8">
<StaggerReveal staggerDelay={0.1} delayChildren={0.05}>
{solutions.map((solution, idx) => (
<motion.div
key={idx}
className="group relative p-10 border border-white/10 bg-ink-light hover:border-brand/40 transition-all duration-500 hover:-translate-y-2"
>
<div className="absolute top-0 left-0 right-0 h-1 bg-brand scale-x-0 group-hover:scale-x-100 transition-transform duration-700 origin-left" />
<div className="flex items-start gap-6">
<div className="w-14 h-14 rounded-2xl bg-brand-soft flex items-center justify-center text-brand shrink-0">
<Lightbulb className="w-7 h-7" />
</div>
<div>
<div className="text-xs font-bold text-brand mb-3 tracking-widest uppercase">
{idx + 1}
</div>
<p className="text-xl text-white font-semibold leading-relaxed">{solution}</p>
</div>
</div>
</motion.div>
))}
</StaggerReveal>
</div>
</div>
</section>
);
}
interface ValuePropositionProps {
valueProposition: Solution['valueProposition'];
}
function ValuePropositionSection({ valueProposition }: ValuePropositionProps) {
return (
<section className="relative py-32 lg:py-40 overflow-hidden bg-ink-light">
<GrainOverlay />
<div className="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-white/12 to-transparent" />
<div className="max-w-container mx-auto px-6 lg:px-10">
<ScrollReveal className="text-center max-w-3xl mx-auto mb-24">
<SectionLabel className="justify-center">
<div className="flex items-center gap-5">
<div className="w-14 h-px bg-brand" />
<span className="text-[11px] tracking-[0.4em] uppercase font-bold text-brand">
Value Proposition
</span>
<div className="w-14 h-px bg-brand" />
</div>
</SectionLabel>
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold text-white tracking-tight leading-tight">
{valueProposition.headline}
</h2>
</ScrollReveal>
<div className="grid md:grid-cols-3 gap-8">
<StaggerReveal staggerDelay={0.12} delayChildren={0.05}>
{valueProposition.points.map((point, idx) => {
const icon = VALUE_ICONS[point.icon] || <Settings className="w-6 h-6" />;
return (
<motion.div
key={idx}
className="group relative p-10 text-center border border-white/10 bg-ink hover:border-brand/40 transition-all duration-500 hover:-translate-y-2"
>
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-1/3 h-1 bg-brand scale-x-0 group-hover:scale-x-100 transition-transform duration-700 origin-center" />
<div className="w-20 h-20 rounded-3xl bg-brand-soft flex items-center justify-center text-brand mx-auto mb-8 group-hover:scale-110 transition-transform duration-500">
{icon}
</div>
<h3 className="text-2xl font-bold text-white mb-4">{point.title}</h3>
<p className="text-dark-text-secondary leading-relaxed">{point.description}</p>
</motion.div>
);
})}
</StaggerReveal>
</div>
</div>
</section>
);
}
interface SuiteCombinationProps {
suiteCombination: Solution['suiteCombination'];
}
function SuiteCombinationSection({ suiteCombination }: SuiteCombinationProps) {
const primaryProducts = suiteCombination.primaryProducts
.map(pid => PRODUCTS.find(p => p.id === pid))
.filter(Boolean);
return (
<section className="relative py-32 lg:py-40 overflow-hidden bg-ink">
<GrainOverlay />
<div className="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-white/12 to-transparent" />
<div className="max-w-container mx-auto px-6 lg:px-10">
<ScrollReveal className="text-center max-w-3xl mx-auto mb-24">
<SectionLabel className="justify-center">
<div className="flex items-center gap-5">
<div className="w-14 h-px bg-brand" />
<span className="text-[11px] tracking-[0.4em] uppercase font-bold text-brand">
Suite Combination
</span>
<div className="w-14 h-px bg-brand" />
</div>
</SectionLabel>
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold text-white tracking-tight leading-tight">
</h2>
</ScrollReveal>
<div className="grid lg:grid-cols-5 gap-8 mb-16">
<StaggerReveal staggerDelay={0.1} delayChildren={0.05}>
{primaryProducts.map((product) => product && (
<motion.div
key={product.id}
className="group relative p-8 border border-white/10 bg-ink-light hover:border-brand/40 transition-all duration-500 hover:-translate-y-2 text-center lg:col-span-2"
>
<div className="absolute top-0 left-0 right-0 h-1 bg-brand scale-x-0 group-hover:scale-x-100 transition-transform duration-700 origin-left" />
<div className="w-16 h-16 rounded-2xl bg-brand-soft flex items-center justify-center text-brand mx-auto mb-6 group-hover:scale-110 transition-transform duration-500">
<Package className="w-8 h-8" />
</div>
<div className="text-xs font-bold text-brand mb-2 tracking-widest uppercase">
</div>
<h3 className="text-xl font-bold text-white mb-3">{product.title}</h3>
<p className="text-sm text-dark-text-muted leading-relaxed">{product.description}</p>
</motion.div>
))}
</StaggerReveal>
<ScrollReveal delay={0.2} className="flex items-center justify-center lg:col-span-1">
<div className="w-20 h-20 rounded-full bg-brand-soft/30 border-2 border-brand/30 flex items-center justify-center text-brand font-bold text-xl">
+
</div>
</ScrollReveal>
<ScrollReveal delay={0.3} className="lg:col-span-2">
<div className="group relative p-8 border border-brand/20 bg-brand-soft/10 hover:border-brand/40 transition-all duration-500 text-center h-full flex flex-col justify-center">
<div className="w-16 h-16 rounded-2xl bg-brand-soft flex items-center justify-center text-brand mx-auto mb-6">
<Users className="w-8 h-8" />
</div>
<div className="text-xs font-bold text-brand mb-2 tracking-widest uppercase">
</div>
<h3 className="text-xl font-bold text-white mb-3"></h3>
<p className="text-sm text-dark-text-secondary leading-relaxed">
{suiteCombination.complementaryServices.length}
</p>
</div>
</ScrollReveal>
</div>
<ScrollReveal>
<div className="p-10 border border-white/10 bg-ink-light text-center">
<div className="text-[11px] tracking-[0.4em] uppercase font-bold text-brand mb-4">
</div>
<p className="text-xl lg:text-2xl text-white leading-relaxed max-w-4xl mx-auto font-medium">
{suiteCombination.rationale}
</p>
</div>
</ScrollReveal>
</div>
</section>
);
}
interface CTASectionProps {
solution: Solution;
}
function CTASection({ solution }: CTASectionProps) {
return (
<section className="relative py-36 lg:py-44 overflow-hidden bg-ink-light">
<GrainOverlay />
<div className="absolute inset-0 pointer-events-none">
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[600px] h-[600px] rounded-full opacity-[0.08] blur-3xl bg-brand" />
</div>
<div className="max-w-container mx-auto px-6 lg:px-10 relative z-10">
<ScrollReveal className="max-w-3xl mx-auto text-center">
<SectionLabel className="justify-center">
<div className="flex items-center gap-5">
<div className="w-14 h-px bg-brand" />
<span className="text-[11px] tracking-[0.4em] uppercase font-bold text-brand">
Get Started
</span>
<div className="w-14 h-px bg-brand" />
</div>
</SectionLabel>
<h2 className="text-3xl md:text-4xl lg:text-6xl font-bold text-white tracking-tight leading-tight mb-8">
</h2>
<p className="text-xl text-dark-text-secondary leading-relaxed mb-12 max-w-2xl mx-auto">
{solution.title}
</p>
<div className="flex flex-wrap gap-6 justify-center">
<Button size="xl" asChild>
<a href="/contact">
<ArrowRight className="w-5 h-5 ml-2" />
</a>
</Button>
<Button size="xl" variant="outline" asChild>
<a href="/solutions"></a>
</Button>
</div>
</ScrollReveal>
</div>
</section>
);
}
interface SolutionDetailContentV1Props {
solution: Solution;
}
export default function SolutionDetailContentV1({ solution }: SolutionDetailContentV1Props) {
return (
<main>
<DetailHero solution={solution} />
<ChallengesSection challenges={solution.challenges} />
<SolutionsSection solutions={solution.solutions} />
<ValuePropositionSection valueProposition={solution.valueProposition} />
<SuiteCombinationSection suiteCombination={solution.suiteCombination} />
<CTASection solution={solution} />
</main>
);
}
@@ -0,0 +1,400 @@
'use client';
import { motion } from 'framer-motion';
import { ScrollReveal, StaggerReveal } from '@/components/ui/scroll-reveal';
import { Button } from '@/components/ui/button';
import { ArrowRight, Factory, ShoppingCart, Heart, GraduationCap, Lightbulb, Settings, Users, TrendingUp, Package, BarChart3, BookOpen, MessageSquare, Calendar, Smartphone } from 'lucide-react';
import { SectionLabel, EASE_OUT } from '@/components/ui/page-decoration';
import type { Solution } from '@/lib/constants/solutions';
import { PRODUCTS } from '@/lib/constants/products';
const INDUSTRY_ICONS: Record<string, React.ReactNode> = {
: <Factory className="w-8 h-8" />,
: <ShoppingCart className="w-8 h-8" />,
: <Heart className="w-8 h-8" />,
: <GraduationCap className="w-8 h-8" />,
};
const VALUE_ICONS: Record<string, React.ReactNode> = {
Factory: <Factory className="w-6 h-6" />,
Package: <Package className="w-6 h-6" />,
BarChart3: <BarChart3 className="w-6 h-6" />,
Users: <Users className="w-6 h-6" />,
TrendingUp: <TrendingUp className="w-6 h-6" />,
ShoppingCart: <ShoppingCart className="w-6 h-6" />,
GraduationCap: <GraduationCap className="w-6 h-6" />,
BookOpen: <BookOpen className="w-6 h-6" />,
MessageSquare: <MessageSquare className="w-6 h-6" />,
Heart: <Heart className="w-6 h-6" />,
Calendar: <Calendar className="w-6 h-6" />,
Smartphone: <Smartphone className="w-6 h-6" />,
};
interface DetailHeroProps {
solution: Solution;
}
function DetailHero({ solution }: DetailHeroProps) {
const industryIcon = INDUSTRY_ICONS[solution.industry] || <Factory className="w-8 h-8" />;
return (
<section className="relative min-h-[80vh] flex items-center overflow-hidden bg-white 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 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, ease: EASE_OUT }}
className="flex items-center gap-4 mb-8"
>
<div className="w-16 h-16 rounded-2xl bg-brand-soft flex items-center justify-center text-brand">
{industryIcon}
</div>
<div>
<span className="px-3 py-1.5 text-xs font-bold text-brand bg-brand-soft/50">
{solution.industry}
</span>
<div className="text-text-muted text-sm mt-2">{solution.subtitle}</div>
</div>
</motion.div>
<motion.h1
initial={{ opacity: 0, y: 50 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 1, delay: 0.15, ease: EASE_OUT }}
className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-bold text-ink leading-tight tracking-tighter mb-6 sm:mb-8 max-w-4xl"
>
{solution.title}
</motion.h1>
<motion.p
initial={{ opacity: 0, y: 40 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.9, delay: 0.3, ease: EASE_OUT }}
className="text-xl lg:text-2xl text-text-secondary max-w-3xl leading-relaxed mb-12"
>
{solution.description}
</motion.p>
<motion.div
initial={{ opacity: 0, y: 40 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.9, delay: 0.45, ease: EASE_OUT }}
className="flex flex-wrap gap-6"
>
<Button size="xl" asChild>
<a href="/contact">
<ArrowRight className="w-5 h-5 ml-2" />
</a>
</Button>
<Button size="xl" variant="outline" asChild>
<a href="/products"></a>
</Button>
</motion.div>
</div>
</section>
);
}
interface ChallengesSectionProps {
challenges: string[];
}
function ChallengesSection({ challenges }: ChallengesSectionProps) {
return (
<section className="relative py-20 sm:py-28 md:py-32 lg:py-40 overflow-hidden bg-bg-secondary">
<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" />
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10">
<div className="grid lg:grid-cols-2 gap-20 items-start">
<ScrollReveal>
<SectionLabel>Challenges</SectionLabel>
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold text-ink tracking-tight leading-tight mb-8">
</h2>
<p className="text-text-secondary text-lg leading-relaxed">
</p>
</ScrollReveal>
<div className="space-y-px bg-border-primary">
<StaggerReveal staggerDelay={0.1} delayChildren={0.05}>
{challenges.map((challenge, idx) => (
<div
key={idx}
className="group relative p-8 bg-white hover:bg-bg-secondary transition-all duration-500"
>
<div className="absolute left-0 top-0 bottom-0 w-1 bg-brand scale-y-0 group-hover:scale-y-100 transition-transform duration-500 origin-top" />
<div className="flex items-start gap-6">
<div className="w-10 h-10 rounded-xl bg-brand-soft/50 flex items-center justify-center text-brand shrink-0 mt-0.5">
<span className="text-lg font-bold">{idx + 1}</span>
</div>
<p className="text-lg text-ink leading-relaxed">{challenge}</p>
</div>
</div>
))}
</StaggerReveal>
</div>
</div>
</div>
</section>
);
}
interface SolutionsSectionProps {
solutions: string[];
}
function SolutionsSection({ solutions }: SolutionsSectionProps) {
return (
<section className="relative py-20 sm:py-28 md:py-32 lg:py-40 overflow-hidden bg-white">
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10">
<ScrollReveal className="text-center max-w-3xl mx-auto mb-16 sm:mb-20 md:mb-20">
<SectionLabel className="justify-center">
<div className="flex items-center gap-5">
<div className="w-14 h-px bg-brand" />
<span className="text-[11px] tracking-[0.4em] uppercase font-bold text-brand">
Solutions
</span>
<div className="w-14 h-px bg-brand" />
</div>
</SectionLabel>
<h2 className="text-2xl sm:text-3xl md:text-4xl lg:text-5xl font-bold text-ink tracking-tight leading-tight">
</h2>
</ScrollReveal>
<div className="grid md:grid-cols-2 gap-px bg-border-primary">
<StaggerReveal staggerDelay={0.1} delayChildren={0.05}>
{solutions.map((solution, idx) => (
<div
key={idx}
className="group relative p-10 bg-white hover:bg-bg-secondary transition-all duration-500"
>
<div className="absolute top-0 left-0 right-0 h-1 bg-brand scale-x-0 group-hover:scale-x-100 transition-transform duration-700 origin-left" />
<div className="flex items-start gap-6">
<div className="w-14 h-14 rounded-2xl bg-brand-soft flex items-center justify-center text-brand shrink-0">
<Lightbulb className="w-7 h-7" />
</div>
<div>
<div className="text-xs font-bold text-brand mb-3 tracking-widest uppercase">
{idx + 1}
</div>
<p className="text-xl text-ink font-semibold leading-relaxed">{solution}</p>
</div>
</div>
</div>
))}
</StaggerReveal>
</div>
</div>
</section>
);
}
interface ValuePropositionProps {
valueProposition: Solution['valueProposition'];
}
function ValuePropositionSection({ valueProposition }: ValuePropositionProps) {
return (
<section className="relative py-20 sm:py-28 md:py-32 lg:py-40 overflow-hidden bg-bg-secondary">
<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" />
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10">
<ScrollReveal className="text-center max-w-3xl mx-auto mb-16 sm:mb-20 md:mb-20">
<SectionLabel className="justify-center">
<div className="flex items-center gap-5">
<div className="w-14 h-px bg-brand" />
<span className="text-[11px] tracking-[0.4em] uppercase font-bold text-brand">
Value Proposition
</span>
<div className="w-14 h-px bg-brand" />
</div>
</SectionLabel>
<h2 className="text-2xl sm:text-3xl md:text-4xl lg:text-5xl font-bold text-ink tracking-tight leading-tight">
{valueProposition.headline}
</h2>
</ScrollReveal>
<div className="grid md:grid-cols-3 gap-px bg-border-primary">
<StaggerReveal staggerDelay={0.12} delayChildren={0.05}>
{valueProposition.points.map((point, idx) => {
const icon = VALUE_ICONS[point.icon] || <Settings className="w-6 h-6" />;
return (
<div
key={idx}
className="group relative p-10 text-center bg-white hover:bg-bg-secondary transition-all duration-500"
>
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-1/3 h-1 bg-brand scale-x-0 group-hover:scale-x-100 transition-transform duration-700 origin-center" />
<div className="w-20 h-20 rounded-3xl bg-brand-soft flex items-center justify-center text-brand mx-auto mb-8">
{icon}
</div>
<h3 className="text-2xl font-bold text-ink mb-4">{point.title}</h3>
<p className="text-text-secondary leading-relaxed">{point.description}</p>
</div>
);
})}
</StaggerReveal>
</div>
</div>
</section>
);
}
interface SuiteCombinationProps {
suiteCombination: Solution['suiteCombination'];
}
function SuiteCombinationSection({ suiteCombination }: SuiteCombinationProps) {
const primaryProducts = suiteCombination.primaryProducts
.map(pid => PRODUCTS.find(p => p.id === pid))
.filter(Boolean);
return (
<section className="relative py-20 sm:py-28 md:py-32 lg:py-40 overflow-hidden bg-white">
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10">
<ScrollReveal className="text-center max-w-3xl mx-auto mb-16 sm:mb-20 md:mb-20">
<SectionLabel className="justify-center">
<div className="flex items-center gap-5">
<div className="w-14 h-px bg-brand" />
<span className="text-[11px] tracking-[0.4em] uppercase font-bold text-brand">
Suite Combination
</span>
<div className="w-14 h-px bg-brand" />
</div>
</SectionLabel>
<h2 className="text-2xl sm:text-3xl md:text-4xl lg:text-5xl font-bold text-ink tracking-tight leading-tight">
</h2>
</ScrollReveal>
<div className="grid lg:grid-cols-5 gap-px bg-border-primary mb-px">
<StaggerReveal staggerDelay={0.1} delayChildren={0.05}>
{primaryProducts.map((product) => product && (
<div
key={product.id}
className="group relative p-8 bg-white hover:bg-bg-secondary transition-all duration-500 text-center lg:col-span-2"
>
<div className="absolute top-0 left-0 right-0 h-1 bg-brand scale-x-0 group-hover:scale-x-100 transition-transform duration-700 origin-left" />
<div className="w-16 h-16 rounded-2xl bg-brand-soft flex items-center justify-center text-brand mx-auto mb-6">
<Package className="w-8 h-8" />
</div>
<div className="text-xs font-bold text-brand mb-2 tracking-widest uppercase">
</div>
<h3 className="text-xl font-bold text-ink mb-3">{product.title}</h3>
<p className="text-sm text-text-muted leading-relaxed">{product.description}</p>
</div>
))}
</StaggerReveal>
<ScrollReveal delay={0.2} className="flex items-center justify-center lg:col-span-1 bg-white">
<div className="w-20 h-20 rounded-full bg-brand-soft/30 border-2 border-brand/30 flex items-center justify-center text-brand font-bold text-xl">
+
</div>
</ScrollReveal>
<ScrollReveal delay={0.3} className="lg:col-span-2">
<div className="group relative p-8 bg-brand-soft/10 hover:bg-bg-secondary transition-all duration-500 text-center h-full flex flex-col justify-center">
<div className="w-16 h-16 rounded-2xl bg-brand-soft flex items-center justify-center text-brand mx-auto mb-6">
<Users className="w-8 h-8" />
</div>
<div className="text-xs font-bold text-brand mb-2 tracking-widest uppercase">
</div>
<h3 className="text-xl font-bold text-ink mb-3"></h3>
<p className="text-sm text-text-secondary leading-relaxed">
{suiteCombination.complementaryServices.length}
</p>
</div>
</ScrollReveal>
</div>
<ScrollReveal>
<div className="p-10 bg-bg-secondary text-center border border-border-primary">
<div className="text-[11px] tracking-[0.4em] uppercase font-bold text-brand mb-4">
</div>
<p className="text-xl lg:text-2xl text-ink leading-relaxed max-w-4xl mx-auto font-medium">
{suiteCombination.rationale}
</p>
</div>
</ScrollReveal>
</div>
</section>
);
}
interface CTASectionProps {
solution: Solution;
}
function CTASection({ solution }: CTASectionProps) {
return (
<section className="relative py-36 lg:py-44 overflow-hidden bg-bg-secondary">
<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" />
<div className="max-w-container mx-auto px-6 lg:px-10 relative z-10">
<ScrollReveal className="text-center max-w-3xl mx-auto">
<div className="flex justify-center mb-7">
<div className="flex items-center gap-5">
<div className="w-14 h-px bg-brand" />
<span className="text-[11px] tracking-[0.4em] uppercase font-bold text-brand">
Get Started
</span>
<div className="w-14 h-px bg-brand" />
</div>
</div>
<h2 className="text-4xl md:text-5xl lg:text-6xl font-bold text-ink tracking-tight leading-tight mb-8">
<br />
<span className="text-brand"></span>
</h2>
<p className="text-xl text-text-secondary mb-12 max-w-2xl mx-auto leading-relaxed">
{solution.title}
</p>
<div className="flex flex-wrap justify-center gap-6">
<Button size="xl" asChild>
<a href="/contact">
<ArrowRight className="w-5 h-5 ml-2" />
</a>
</Button>
<Button size="xl" variant="outline" asChild>
<a href="/solutions"></a>
</Button>
</div>
</ScrollReveal>
</div>
</section>
);
}
interface SolutionDetailContentV2Props {
solution: Solution;
}
export default function SolutionDetailContentV2({ solution }: SolutionDetailContentV2Props) {
return (
<main>
<DetailHero solution={solution} />
<ChallengesSection challenges={solution.challenges} />
<SolutionsSection solutions={solution.solutions} />
<ValuePropositionSection valueProposition={solution.valueProposition} />
<SuiteCombinationSection suiteCombination={solution.suiteCombination} />
<CTASection solution={solution} />
</main>
);
}
@@ -0,0 +1,402 @@
'use client';
import { motion } from 'framer-motion';
import { ScrollReveal, StaggerReveal } from '@/components/ui/scroll-reveal';
import { Button } from '@/components/ui/button';
import { ArrowRight, Factory, ShoppingCart, Heart, GraduationCap, Lightbulb, Settings, Users, TrendingUp, Package, BarChart3, BookOpen, MessageSquare, Calendar, Smartphone } from 'lucide-react';
import { SectionLabel, EASE_OUT } from '@/components/ui/page-decoration';
import type { Solution } from '@/lib/constants/solutions';
import type { Product } from '@/lib/constants/products';
const INDUSTRY_ICONS: Record<string, React.ReactNode> = {
: <Factory className="w-8 h-8" />,
: <ShoppingCart className="w-8 h-8" />,
: <Heart className="w-8 h-8" />,
: <GraduationCap className="w-8 h-8" />,
};
const VALUE_ICONS: Record<string, React.ReactNode> = {
Factory: <Factory className="w-6 h-6" />,
Package: <Package className="w-6 h-6" />,
BarChart3: <BarChart3 className="w-6 h-6" />,
Users: <Users className="w-6 h-6" />,
TrendingUp: <TrendingUp className="w-6 h-6" />,
ShoppingCart: <ShoppingCart className="w-6 h-6" />,
GraduationCap: <GraduationCap className="w-6 h-6" />,
BookOpen: <BookOpen className="w-6 h-6" />,
MessageSquare: <MessageSquare className="w-6 h-6" />,
Heart: <Heart className="w-6 h-6" />,
Calendar: <Calendar className="w-6 h-6" />,
Smartphone: <Smartphone className="w-6 h-6" />,
};
interface DetailHeroProps {
solution: Solution;
}
function DetailHero({ solution }: DetailHeroProps) {
const industryIcon = INDUSTRY_ICONS[solution.industry] || <Factory className="w-8 h-8" />;
return (
<section className="relative min-h-[80vh] flex items-center overflow-hidden bg-white 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 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, ease: EASE_OUT }}
className="flex items-center gap-4 mb-8"
>
<div className="w-16 h-16 rounded-2xl bg-brand-soft flex items-center justify-center text-brand">
{industryIcon}
</div>
<div>
<span className="px-3 py-1.5 text-xs font-bold text-brand bg-brand-soft/50">
{solution.industry}
</span>
<div className="text-text-muted text-sm mt-2">{solution.subtitle}</div>
</div>
</motion.div>
<motion.h1
initial={{ opacity: 0, y: 50 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 1, delay: 0.15, ease: EASE_OUT }}
className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-bold text-ink leading-tight tracking-tighter mb-6 sm:mb-8 max-w-4xl"
>
{solution.title}
</motion.h1>
<motion.p
initial={{ opacity: 0, y: 40 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.9, delay: 0.3, ease: EASE_OUT }}
className="text-xl lg:text-2xl text-text-secondary max-w-3xl leading-relaxed mb-12"
>
{solution.description}
</motion.p>
<motion.div
initial={{ opacity: 0, y: 40 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.9, delay: 0.45, ease: EASE_OUT }}
className="flex flex-wrap gap-6"
>
<Button size="xl" asChild>
<a href="/contact">
<ArrowRight className="w-5 h-5 ml-2" />
</a>
</Button>
<Button size="xl" variant="outline" asChild>
<a href="/products"></a>
</Button>
</motion.div>
</div>
</section>
);
}
interface ChallengesSectionProps {
challenges: string[];
}
function ChallengesSection({ challenges }: ChallengesSectionProps) {
return (
<section className="relative py-20 sm:py-28 md:py-32 lg:py-40 overflow-hidden bg-bg-secondary">
<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" />
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10">
<div className="grid lg:grid-cols-2 gap-20 items-start">
<ScrollReveal>
<SectionLabel>Challenges</SectionLabel>
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold text-ink tracking-tight leading-tight mb-8">
</h2>
<p className="text-text-secondary text-lg leading-relaxed">
</p>
</ScrollReveal>
<div className="space-y-px bg-border-primary">
<StaggerReveal staggerDelay={0.1} delayChildren={0.05}>
{challenges.map((challenge, idx) => (
<div
key={idx}
className="group relative p-8 bg-white hover:bg-bg-secondary transition-all duration-500"
>
<div className="absolute left-0 top-0 bottom-0 w-1 bg-brand scale-y-0 group-hover:scale-y-100 transition-transform duration-500 origin-top" />
<div className="flex items-start gap-6">
<div className="w-10 h-10 rounded-xl bg-brand-soft/50 flex items-center justify-center text-brand shrink-0 mt-0.5">
<span className="text-lg font-bold">{idx + 1}</span>
</div>
<p className="text-lg text-ink leading-relaxed">{challenge}</p>
</div>
</div>
))}
</StaggerReveal>
</div>
</div>
</div>
</section>
);
}
interface SolutionsSectionProps {
solutions: string[];
}
function SolutionsSection({ solutions }: SolutionsSectionProps) {
return (
<section className="relative py-20 sm:py-28 md:py-32 lg:py-40 overflow-hidden bg-white">
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10">
<ScrollReveal className="text-center max-w-3xl mx-auto mb-16 sm:mb-20 md:mb-20">
<SectionLabel className="justify-center">
<div className="flex items-center gap-5">
<div className="w-14 h-px bg-brand" />
<span className="text-[11px] tracking-[0.4em] uppercase font-bold text-brand">
Solutions
</span>
<div className="w-14 h-px bg-brand" />
</div>
</SectionLabel>
<h2 className="text-2xl sm:text-3xl md:text-4xl lg:text-5xl font-bold text-ink tracking-tight leading-tight">
</h2>
</ScrollReveal>
<div className="grid md:grid-cols-2 gap-px bg-border-primary">
<StaggerReveal staggerDelay={0.1} delayChildren={0.05}>
{solutions.map((solution, idx) => (
<div
key={idx}
className="group relative p-10 bg-white hover:bg-bg-secondary transition-all duration-500"
>
<div className="absolute top-0 left-0 right-0 h-1 bg-brand scale-x-0 group-hover:scale-x-100 transition-transform duration-700 origin-left" />
<div className="flex items-start gap-6">
<div className="w-14 h-14 rounded-2xl bg-brand-soft flex items-center justify-center text-brand shrink-0">
<Lightbulb className="w-7 h-7" />
</div>
<div>
<div className="text-xs font-bold text-brand mb-3 tracking-widest uppercase">
{idx + 1}
</div>
<p className="text-xl text-ink font-semibold leading-relaxed">{solution}</p>
</div>
</div>
</div>
))}
</StaggerReveal>
</div>
</div>
</section>
);
}
interface ValuePropositionProps {
valueProposition: Solution['valueProposition'];
}
function ValuePropositionSection({ valueProposition }: ValuePropositionProps) {
return (
<section className="relative py-20 sm:py-28 md:py-32 lg:py-40 overflow-hidden bg-bg-secondary">
<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" />
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10">
<ScrollReveal className="text-center max-w-3xl mx-auto mb-16 sm:mb-20 md:mb-20">
<SectionLabel className="justify-center">
<div className="flex items-center gap-5">
<div className="w-14 h-px bg-brand" />
<span className="text-[11px] tracking-[0.4em] uppercase font-bold text-brand">
Value Proposition
</span>
<div className="w-14 h-px bg-brand" />
</div>
</SectionLabel>
<h2 className="text-2xl sm:text-3xl md:text-4xl lg:text-5xl font-bold text-ink tracking-tight leading-tight">
{valueProposition.headline}
</h2>
</ScrollReveal>
<div className="grid md:grid-cols-3 gap-px bg-border-primary">
<StaggerReveal staggerDelay={0.12} delayChildren={0.05}>
{valueProposition.points.map((point, idx) => {
const icon = VALUE_ICONS[point.icon] || <Settings className="w-6 h-6" />;
return (
<div
key={idx}
className="group relative p-10 text-center bg-white hover:bg-bg-secondary transition-all duration-500"
>
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-1/3 h-1 bg-brand scale-x-0 group-hover:scale-x-100 transition-transform duration-700 origin-center" />
<div className="w-20 h-20 rounded-3xl bg-brand-soft flex items-center justify-center text-brand mx-auto mb-8">
{icon}
</div>
<h3 className="text-2xl font-bold text-ink mb-4">{point.title}</h3>
<p className="text-text-secondary leading-relaxed">{point.description}</p>
</div>
);
})}
</StaggerReveal>
</div>
</div>
</section>
);
}
interface SuiteCombinationProps {
suiteCombination: Solution['suiteCombination'];
products: Product[];
}
function SuiteCombinationSection({ suiteCombination, products }: SuiteCombinationProps) {
const primaryProducts = suiteCombination.primaryProducts
.map(pid => products.find(p => p.id === pid))
.filter(Boolean);
return (
<section className="relative py-20 sm:py-28 md:py-32 lg:py-40 overflow-hidden bg-white">
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10">
<ScrollReveal className="text-center max-w-3xl mx-auto mb-16 sm:mb-20 md:mb-20">
<SectionLabel className="justify-center">
<div className="flex items-center gap-5">
<div className="w-14 h-px bg-brand" />
<span className="text-[11px] tracking-[0.4em] uppercase font-bold text-brand">
Suite Combination
</span>
<div className="w-14 h-px bg-brand" />
</div>
</SectionLabel>
<h2 className="text-2xl sm:text-3xl md:text-4xl lg:text-5xl font-bold text-ink tracking-tight leading-tight">
</h2>
</ScrollReveal>
<div className="grid lg:grid-cols-5 gap-px bg-border-primary mb-px">
<StaggerReveal staggerDelay={0.1} delayChildren={0.05}>
{primaryProducts.map((product) => product && (
<div
key={product.id}
className="group relative p-8 bg-white hover:bg-bg-secondary transition-all duration-500 text-center lg:col-span-2"
>
<div className="absolute top-0 left-0 right-0 h-1 bg-brand scale-x-0 group-hover:scale-x-100 transition-transform duration-700 origin-left" />
<div className="w-16 h-16 rounded-2xl bg-brand-soft flex items-center justify-center text-brand mx-auto mb-6">
<Package className="w-8 h-8" />
</div>
<div className="text-xs font-bold text-brand mb-2 tracking-widest uppercase">
</div>
<h3 className="text-xl font-bold text-ink mb-3">{product.title}</h3>
<p className="text-sm text-text-muted leading-relaxed">{product.description}</p>
</div>
))}
</StaggerReveal>
<ScrollReveal delay={0.2} className="flex items-center justify-center lg:col-span-1 bg-white">
<div className="w-20 h-20 rounded-full bg-brand-soft/30 border-2 border-brand/30 flex items-center justify-center text-brand font-bold text-xl">
+
</div>
</ScrollReveal>
<ScrollReveal delay={0.3} className="lg:col-span-2">
<div className="group relative p-8 bg-brand-soft/10 hover:bg-bg-secondary transition-all duration-500 text-center h-full flex flex-col justify-center">
<div className="w-16 h-16 rounded-2xl bg-brand-soft flex items-center justify-center text-brand mx-auto mb-6">
<Users className="w-8 h-8" />
</div>
<div className="text-xs font-bold text-brand mb-2 tracking-widest uppercase">
</div>
<h3 className="text-xl font-bold text-ink mb-3"></h3>
<p className="text-sm text-text-secondary leading-relaxed">
{suiteCombination.complementaryServices.length}
</p>
</div>
</ScrollReveal>
</div>
<ScrollReveal>
<div className="p-10 bg-bg-secondary text-center border border-border-primary">
<div className="text-[11px] tracking-[0.4em] uppercase font-bold text-brand mb-4">
</div>
<p className="text-xl lg:text-2xl text-ink leading-relaxed max-w-4xl mx-auto font-medium">
{suiteCombination.rationale}
</p>
</div>
</ScrollReveal>
</div>
</section>
);
}
interface CTASectionProps {
solution: Solution;
}
function CTASection({ solution }: CTASectionProps) {
return (
<section className="relative py-36 lg:py-44 overflow-hidden bg-bg-secondary">
<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" />
<div className="max-w-container mx-auto px-6 lg:px-10 relative z-10">
<ScrollReveal className="text-center max-w-3xl mx-auto">
<div className="flex justify-center mb-7">
<div className="flex items-center gap-5">
<div className="w-14 h-px bg-brand" />
<span className="text-[11px] tracking-[0.4em] uppercase font-bold text-brand">
Get Started
</span>
<div className="w-14 h-px bg-brand" />
</div>
</div>
<h2 className="text-4xl md:text-5xl lg:text-6xl font-bold text-ink tracking-tight leading-tight mb-8">
<br />
<span className="text-brand"></span>
</h2>
<p className="text-xl text-text-secondary mb-12 max-w-2xl mx-auto leading-relaxed">
{solution.title}
</p>
<div className="flex flex-wrap justify-center gap-6">
<Button size="xl" asChild>
<a href="/contact">
<ArrowRight className="w-5 h-5 ml-2" />
</a>
</Button>
<Button size="xl" variant="outline" asChild>
<a href="/solutions"></a>
</Button>
</div>
</ScrollReveal>
</div>
</section>
);
}
interface SolutionDetailContentV3Props {
solution: Solution;
products?: Product[];
}
export default function SolutionDetailContentV3({ solution, products = [] }: SolutionDetailContentV3Props) {
return (
<main>
<DetailHero solution={solution} />
<ChallengesSection challenges={solution.challenges} />
<SolutionsSection solutions={solution.solutions} />
<ValuePropositionSection valueProposition={solution.valueProposition} />
<SuiteCombinationSection suiteCombination={solution.suiteCombination} products={products} />
<CTASection solution={solution} />
</main>
);
}
@@ -0,0 +1,85 @@
'use client';
import { useState, useEffect } from 'react';
import { initCms, getItemRenderer } from '@/lib/cms';
import type { ContentItem } from '@/lib/cms';
import { SOLUTIONS } from '@/lib/constants/solutions';
function solutionToContentItem(solution: unknown, index: number): ContentItem {
const s = solution as Record<string, unknown>;
return {
id: String(s.id),
modelId: 'solution',
modelCode: 'solution',
title: String(s.title),
slug: String(s.id),
status: 'published',
version: 1,
sortOrder: index,
data: s,
createdAt: '2024-01-01T00:00:00Z',
updatedAt: '2024-01-01T00:00:00Z',
publishedAt: '2024-01-01T00:00:00Z',
createdBy: 'system',
updatedBy: 'system',
};
}
export default function SolutionsContentCms() {
const [ready, setReady] = useState(false);
const [items, setItems] = useState<ContentItem[]>([]);
useEffect(() => {
initCms();
setItems(SOLUTIONS.map(solutionToContentItem));
setReady(true);
}, []);
if (!ready) {
return (
<div className="min-h-screen bg-ink flex items-center justify-center">
<div className="text-white/50">...</div>
</div>
);
}
const SolutionCardRenderer = getItemRenderer('solution');
return (
<div className="min-h-screen bg-ink text-white overflow-x-hidden" data-theme="dark">
<section className="relative min-h-[50vh] flex items-center overflow-hidden bg-ink pt-24">
<div className="absolute inset-0">
<div className="absolute top-1/3 -right-32 w-96 h-96 rounded-full bg-purple-500/20 blur-[120px]" />
</div>
<div className="relative z-10 w-full max-w-7xl mx-auto px-6 lg:px-8 py-20">
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-white/[0.05] border border-white/[0.1] text-sm text-white/70 mb-6">
<span className="w-1.5 h-1.5 rounded-full bg-brand animate-pulse" />
</div>
<h1 className="text-4xl sm:text-5xl md:text-6xl font-bold text-white tracking-tight leading-[1.1]">
</h1>
<p className="mt-6 text-lg text-white/60 max-w-2xl leading-relaxed">
</p>
</div>
</section>
<section className="relative py-16 sm:py-20 md:py-28 overflow-hidden bg-ink-light">
<div className="max-w-7xl mx-auto px-6 lg:px-8">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 md:gap-8">
{items.map((item, i) =>
SolutionCardRenderer ? (
<SolutionCardRenderer key={item.id} item={item} index={i} />
) : (
<div key={item.id} className="p-4 border border-white/10 rounded-lg">
{item.title}
</div>
)
)}
</div>
</div>
</section>
</div>
);
}
@@ -0,0 +1,314 @@
'use client';
import { useRef } from 'react';
import { motion } from 'framer-motion';
import { ScrollReveal, StaggerReveal } from '@/components/ui/scroll-reveal';
import { Button } from '@/components/ui/button';
import { ArrowRight, Factory, ShoppingCart, Heart, GraduationCap } from 'lucide-react';
import { cn } from '@/lib/utils';
import { SOLUTIONS, type Solution } from '@/lib/constants/solutions';
import { PRODUCTS } from '@/lib/constants/products';
const EASE_OUT = [0.22, 1, 0.36, 1] as const;
const INDUSTRY_ICONS: Record<string, React.ReactNode> = {
: <Factory className="w-7 h-7" />,
: <ShoppingCart className="w-7 h-7" />,
: <Heart className="w-7 h-7" />,
: <GraduationCap className="w-7 h-7" />,
};
function GrainOverlay() {
return (
<div
className="absolute inset-0 pointer-events-none opacity-[0.03] mix-blend-overlay"
style={{
backgroundImage: `url("data:image/svg+xml,%3Csvg viewBox='0 0 512 512' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E")`,
backgroundRepeat: 'repeat',
backgroundSize: '300px 300px',
}}
/>
);
}
function SectionLabel({ children, className = '' }: { children: React.ReactNode; className?: string }) {
return (
<div className={cn('flex items-center gap-5 mb-7', className)}>
<div className="w-14 h-px bg-brand" />
<span className="text-[11px] tracking-[0.4em] uppercase font-bold text-brand">
{children}
</span>
</div>
);
}
function HeroSection() {
const containerRef = useRef<HTMLDivElement>(null);
return (
<section
ref={containerRef}
className="relative min-h-[85vh] flex items-center overflow-hidden bg-ink"
>
<GrainOverlay />
<div className="absolute inset-0 pointer-events-none">
<div
className="absolute top-0 right-0 w-[42%] h-full"
style={{
background: 'linear-gradient(135deg, transparent 25%, rgba(196, 30, 58, 0.094) 100%)',
clipPath: 'polygon(35% 0, 100% 0, 100% 100%, 0% 100%)',
}}
/>
<div className="absolute bottom-0 left-0 w-full h-2/5 bg-gradient-to-t from-ink to-transparent" />
<div className="absolute top-24 right-[22%] w-[350px] h-[350px] rounded-full opacity-15 blur-3xl bg-brand" />
</div>
<div className="max-w-container mx-auto px-6 lg:px-10 relative z-10 w-full py-32 lg:py-40">
<div className="max-w-4xl">
<motion.div
initial={{ opacity: 0, x: -50 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 1.1, ease: EASE_OUT }}
className="mb-10"
>
<SectionLabel>Industry Solutions</SectionLabel>
</motion.div>
<motion.h1
initial={{ opacity: 0, y: 70 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 1.2, delay: 0.15, ease: EASE_OUT }}
className="text-5xl sm:text-6xl md:text-7xl lg:text-8xl font-bold text-white leading-[0.85] tracking-tighter mb-12"
>
<div className="block"></div>
<div className="block mt-4">
<span className="relative">
<span className="text-brand"></span>
<motion.span
className="absolute -bottom-2 left-0 w-full h-1 bg-brand"
style={{ transformOrigin: 'left' }}
initial={{ scaleX: 0 }}
animate={{ scaleX: 1 }}
transition={{ duration: 0.8, delay: 1.2, ease: EASE_OUT }}
/>
</span>
</div>
</motion.h1>
<motion.p
initial={{ opacity: 0, y: 50 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 1, delay: 0.45, ease: EASE_OUT }}
className="text-xl lg:text-2xl text-dark-text-secondary max-w-2xl leading-relaxed mb-12"
>
+
</motion.p>
<motion.div
initial={{ opacity: 0, y: 50 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 1, delay: 0.6, ease: EASE_OUT }}
className="flex flex-wrap gap-6"
>
<Button size="xl" asChild>
<a href="/contact">
<ArrowRight className="w-5 h-5 ml-2" />
</a>
</Button>
<Button size="xl" variant="outline" asChild>
<a href="/products"></a>
</Button>
</motion.div>
</div>
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.9, ease: EASE_OUT }}
className="grid grid-cols-3 gap-12 mt-24 pt-16 border-t border-white/10 max-w-2xl"
>
<div>
<div className="text-5xl font-bold text-white mb-3">4<span className="text-brand">+</span></div>
<div className="text-sm text-dark-text-muted"></div>
</div>
<div>
<div className="text-5xl font-bold text-white mb-3"><span className="text-brand"></span></div>
<div className="text-sm text-dark-text-muted"></div>
</div>
<div>
<div className="text-5xl font-bold text-white mb-3"><span className="text-brand"></span></div>
<div className="text-sm text-dark-text-muted"></div>
</div>
</motion.div>
</div>
</section>
);
}
function SolutionsGridSection() {
return (
<section className="relative py-36 lg:py-44 overflow-hidden bg-ink-light">
<GrainOverlay />
<div className="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-white/12 to-transparent" />
<div className="max-w-container mx-auto px-6 lg:px-10">
<div className="flex flex-col lg:flex-row lg:items-end lg:justify-between gap-12 mb-28">
<ScrollReveal className="lg:max-w-3xl">
<SectionLabel>Industry Expertise</SectionLabel>
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold text-white tracking-tight leading-tight">
</h2>
</ScrollReveal>
<ScrollReveal delay={0.1}>
<p className="text-dark-text-secondary max-w-md leading-relaxed text-lg">
</p>
</ScrollReveal>
</div>
<StaggerReveal
className="grid md:grid-cols-2 gap-8"
staggerDelay={0.1}
delayChildren={0.05}
>
{SOLUTIONS.map((solution) => (
<SolutionCard key={solution.id} solution={solution} />
))}
</StaggerReveal>
</div>
</section>
);
}
function SolutionCard({ solution }: { solution: Solution }) {
const industryIcon = INDUSTRY_ICONS[solution.industry] || <Factory className="w-7 h-7" />;
return (
<a
href={`/solutions/${solution.id}`}
className="group relative block border border-white/10 hover:border-brand/40 bg-ink transition-all duration-600 hover:-translate-y-2 overflow-hidden"
>
<div className="absolute top-0 left-0 right-0 h-1 bg-brand scale-x-0 group-hover:scale-x-100 transition-transform duration-700 origin-left" />
<div className="p-10 lg:p-12 relative z-10">
<div className="flex items-start justify-between mb-8">
<div className="w-16 h-16 rounded-2xl bg-brand-soft flex items-center justify-center text-brand group-hover:scale-110 transition-transform duration-500">
{industryIcon}
</div>
<span className="px-4 py-2 text-xs font-bold text-brand bg-brand-soft/50 backdrop-blur-sm">
{solution.industry}
</span>
</div>
<div className="mb-6">
<div className="text-dark-text-muted text-sm tracking-widest uppercase mb-2">
{solution.subtitle}
</div>
<h3 className="text-2xl lg:text-3xl font-bold text-white group-hover:translate-x-2 transition-transform duration-500">
{solution.title}
</h3>
</div>
<p className="text-dark-text-secondary leading-relaxed mb-10">
{solution.description}
</p>
<div className="pt-8 border-t border-white/10">
<p className="text-[11px] text-dark-text-muted mb-4 uppercase tracking-widest font-bold">
</p>
<div className="flex flex-wrap gap-2 mb-6">
{solution.suiteCombination.primaryProducts.map(pid => {
const prod = PRODUCTS.find(p => p.id === pid);
return prod ? (
<span
key={pid}
className="px-3 py-1.5 text-xs font-medium text-white bg-ink-light border border-white/10 group-hover:border-white/20 transition-colors"
>
{prod.title.replace('睿新', '').replace('睿视 ', '')}
</span>
) : null;
})}
<span className="px-3 py-1.5 text-xs font-bold text-brand bg-brand-soft">
+
</span>
</div>
<p className="text-sm text-dark-text-muted leading-relaxed">
{solution.suiteCombination.rationale}
</p>
</div>
<motion.div
className="inline-flex items-center gap-3 text-sm font-bold text-brand mt-10"
whileHover={{ x: 8 }}
transition={{ type: 'spring', stiffness: 300, damping: 30 }}
>
<span></span>
<ArrowRight className="w-4 h-4" />
</motion.div>
</div>
</a>
);
}
function CTASection() {
return (
<section className="relative py-36 lg:py-44 overflow-hidden bg-ink">
<GrainOverlay />
<div className="absolute inset-0 pointer-events-none">
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[600px] h-[600px] rounded-full opacity-[0.08] blur-3xl bg-brand" />
</div>
<div className="max-w-container mx-auto px-6 lg:px-10 relative z-10">
<ScrollReveal className="max-w-3xl mx-auto text-center">
<SectionLabel className="justify-center">
<div className="flex items-center gap-5">
<div className="w-14 h-px bg-brand" />
<span className="text-[11px] tracking-[0.4em] uppercase font-bold text-brand">
Get Started
</span>
<div className="w-14 h-px bg-brand" />
</div>
</SectionLabel>
<h2 className="text-3xl md:text-4xl lg:text-6xl font-bold text-white tracking-tight leading-tight mb-8">
</h2>
<p className="text-xl text-dark-text-secondary leading-relaxed mb-12 max-w-2xl mx-auto">
</p>
<div className="flex flex-wrap gap-6 justify-center">
<Button size="xl" asChild>
<a href="/contact">
<ArrowRight className="w-5 h-5 ml-2" />
</a>
</Button>
<Button size="xl" variant="outline" asChild>
<a href="/products"></a>
</Button>
</div>
</ScrollReveal>
</div>
</section>
);
}
export default function SolutionsContentV1() {
return (
<main>
<HeroSection />
<SolutionsGridSection />
<CTASection />
</main>
);
}
@@ -0,0 +1,398 @@
'use client';
import { useRef, useState, useEffect } from 'react';
import { motion, useInView } from 'framer-motion';
import { ScrollReveal, StaggerReveal } from '@/components/ui/scroll-reveal';
import { GrainOverlay, FloatingInkParticles, SectionLabel, EASE_OUT } from '@/components/ui/page-decoration';
import { Button } from '@/components/ui/button';
import { ArrowRight, Factory, ShoppingCart, Heart, GraduationCap, Sparkles } from 'lucide-react';
import { type Solution } from '@/lib/constants/solutions';
import { PRODUCTS } from '@/lib/constants/products';
import { getMockItems } from '@/lib/cms/mock-data';
// CMS 数据源:从 mock-data 读取解决方案数据
const SOLUTIONS: Solution[] = getMockItems('solution').map(
(item) => item.data as unknown as Solution,
);
import { useReducedMotion } from '@/hooks/use-reduced-motion';
import { cn } from '@/lib/utils';
const EASE_SPRING = { type: 'spring', stiffness: 300, damping: 30 } as const;
const INDUSTRY_ICONS: Record<string, React.ReactNode> = {
: <Factory className="w-7 h-7" />,
: <ShoppingCart className="w-7 h-7" />,
: <Heart className="w-7 h-7" />,
: <GraduationCap className="w-7 h-7" />,
};
const INDUSTRY_COLORS: Record<string, { color: string; bg: string; soft: string }> = {
: { color: '#C41E3A', bg: 'bg-brand-soft', soft: 'rgba(196, 30, 58, 0.08)' },
: { color: '#3b82f6', bg: 'bg-accent-blue-soft', soft: 'rgba(59, 130, 246, 0.08)' },
: { color: '#14b8a6', bg: 'bg-accent-teal-soft', soft: 'rgba(20, 184, 166, 0.08)' },
: { color: '#f59e0b', bg: 'bg-accent-amber-soft', soft: 'rgba(245, 158, 11, 0.08)' },
};
function useCountUp(target: number, start: boolean, duration: number = 2000) {
const [count, setCount] = useState(0);
const prefersReducedMotion = useReducedMotion();
useEffect(() => {
if (!start || prefersReducedMotion) {
setCount(target);
return;
}
const startTime = performance.now();
let animationId: number;
const animate = (currentTime: number) => {
const elapsed = currentTime - startTime;
const progress = Math.min(elapsed / duration, 1);
const easeOutQuart = 1 - Math.pow(1 - progress, 4);
setCount(Math.floor(target * easeOutQuart));
if (progress < 1) {
animationId = requestAnimationFrame(animate);
} else {
setCount(target);
}
};
animationId = requestAnimationFrame(animate);
return () => cancelAnimationFrame(animationId);
}, [target, start, duration, prefersReducedMotion]);
return count;
}
const SOLUTION_PARTICLES = [
{ x: 18, y: 28, scale: 0.8, duration: 10, delay: 0, size: 3 },
{ x: 78, y: 62, scale: 0.6, duration: 12, delay: 1.5, size: 2 },
{ x: 50, y: 48, scale: 1.0, duration: 9, delay: 0.8, size: 4 },
{ x: 72, y: 30, scale: 0.7, duration: 11, delay: 2.2, size: 2.5 },
{ x: 28, y: 72, scale: 0.9, duration: 8.5, delay: 1.0, size: 3.5 },
{ x: 62, y: 70, scale: 0.55, duration: 13, delay: 2.8, size: 2 },
];
function HeroSection() {
const statsRef = useRef(null);
const isInView = useInView(statsRef, { once: true, margin: '-100px' });
const count4 = useCountUp(4, isInView, 1500);
return (
<section className="relative min-h-[85vh] flex items-center overflow-hidden bg-ink">
<GrainOverlay />
<FloatingInkParticles particles={SOLUTION_PARTICLES} />
<div className="absolute inset-0 pointer-events-none">
<div
className="absolute top-0 right-0 w-[42%] h-full"
style={{
background: 'linear-gradient(145deg, transparent 25%, rgba(196, 30, 58, 0.03) 100%)',
clipPath: 'polygon(35% 0, 100% 0, 100% 100%, 0% 100%)',
}}
/>
<div
className="absolute bottom-0 left-0 w-[50%] h-full"
style={{
background: 'linear-gradient(220deg, transparent 35%, rgba(20, 184, 166, 0.02) 100%)',
clipPath: 'polygon(0 0, 65% 0, 100% 100%, 0 100%)',
}}
/>
<div className="absolute bottom-0 left-0 w-full h-2/5 bg-gradient-to-t from-ink to-transparent" />
<div className="absolute top-24 right-[20%] w-[360px] h-[360px] rounded-full opacity-[0.04] blur-3xl bg-brand" />
<div className="absolute bottom-28 left-[18%] w-[300px] h-[300px] rounded-full opacity-[0.025] blur-3xl bg-teal-500" />
</div>
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10 w-full py-24 sm:py-28 md:py-32 lg:py-40">
<div className="max-w-4xl">
<motion.div
initial={{ opacity: 0, x: -50 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 1.1, ease: EASE_OUT }}
className="mb-10"
>
<SectionLabel>Industry Solutions</SectionLabel>
</motion.div>
<motion.h1
initial={{ opacity: 0, y: 70 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 1.2, delay: 0.15, ease: EASE_OUT }}
className="text-5xl sm:text-6xl md:text-7xl lg:text-8xl font-bold text-white leading-[0.88] tracking-tighter mb-12"
>
<div className="block"></div>
<div className="block mt-5">
<span className="relative inline-block">
<span className="text-brand font-extrabold"></span>
<motion.span
className="absolute -bottom-3 left-0 w-full h-[3px] bg-brand"
style={{ transformOrigin: 'left' }}
initial={{ scaleX: 0 }}
animate={{ scaleX: 1 }}
transition={{ duration: 0.8, delay: 1.2, ease: EASE_OUT }}
/>
</span>
</div>
</motion.h1>
<motion.p
initial={{ opacity: 0, y: 50 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 1, delay: 0.45, ease: EASE_OUT }}
className="text-xl lg:text-2xl text-dark-text-secondary max-w-2xl leading-relaxed mb-12"
>
+
</motion.p>
<motion.div
initial={{ opacity: 0, y: 50 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 1, delay: 0.6, ease: EASE_OUT }}
className="flex flex-wrap gap-6"
>
<Button size="xl" asChild>
<a href="/contact">
<ArrowRight className="w-5 h-5 ml-2" />
</a>
</Button>
<Button size="xl" variant="outline" asChild>
<a href="/products"></a>
</Button>
</motion.div>
<div ref={statsRef} className="grid grid-cols-3 gap-12 mt-24 pt-16 border-t border-white/10 max-w-2xl">
<div>
<div className="text-4xl lg:text-5xl font-bold text-white tracking-tight leading-none mb-2">
{count4}<span className="text-brand">+</span>
</div>
<div className="text-sm text-dark-text-muted"></div>
</div>
<div>
<div className="text-4xl lg:text-5xl font-bold text-white tracking-tight leading-none mb-2">
<span className="text-brand"></span>
</div>
<div className="text-sm text-dark-text-muted"></div>
</div>
<div>
<div className="text-4xl lg:text-5xl font-bold text-white tracking-tight leading-none mb-2">
<span className="text-brand"></span>
</div>
<div className="text-sm text-dark-text-muted"></div>
</div>
</div>
</div>
</div>
</section>
);
}
function SolutionCard({ solution, index }: { solution: Solution; index: number }) {
const industryIcon = INDUSTRY_ICONS[solution.industry] || <Factory className="w-7 h-7" />;
const colors = INDUSTRY_COLORS[solution.industry] ?? INDUSTRY_COLORS['制造业']!;
return (
<a
href={`/solutions/${solution.id}`}
className="group relative block transition-all duration-600 hover:bg-white/[0.02] overflow-hidden bg-ink border border-white/10 hover:border-white/20"
>
<div
className="absolute top-0 left-0 h-full w-1 scale-y-0 group-hover:scale-y-100 transition-transform duration-700 origin-top"
style={{ backgroundColor: colors.color }}
/>
<div className="absolute top-10 right-10 w-52 h-52 rounded-full opacity-0 group-hover:opacity-100 blur-3xl transition-opacity duration-700" style={{ backgroundColor: colors.color + '08' }} />
<div className="relative z-10 p-10 lg:p-12">
<div className="flex items-start justify-between mb-8">
<div
className={cn('w-16 h-16 flex items-center justify-center rounded-2xl', colors.bg)}
style={{ color: colors.color }}
>
<motion.div
whileHover={{ scale: 1.08, y: -3 }}
transition={EASE_SPRING}
>
{industryIcon}
</motion.div>
</div>
<span className="text-7xl font-bold text-white/[0.05] group-hover:text-white/[0.1] transition-all duration-500 tracking-tighter leading-none">
{String(index + 1).padStart(2, '0')}
</span>
</div>
<div className="mb-6">
<span
className="inline-block px-3.5 py-1.5 text-[11px] font-bold mb-4 backdrop-blur-sm rounded-sm"
style={{ backgroundColor: colors.soft, color: colors.color }}
>
{solution.industry}
</span>
<div className="text-dark-text-muted text-sm tracking-widest uppercase mb-2">
{solution.subtitle}
</div>
<h3 className="text-2xl lg:text-3xl font-bold text-white group-hover:translate-x-1.5 transition-transform duration-500 leading-tight">
{solution.title}
</h3>
</div>
<p className="text-dark-text-secondary leading-relaxed mb-8 text-[15px]">
{solution.description}
</p>
<div className="flex flex-col gap-2.5 mb-8">
{solution.challenges.slice(0, 2).map((challenge, idx) => (
<div key={idx} className="flex items-start gap-2.5 text-sm text-dark-text-muted">
<span className="text-brand mt-0.5 font-bold"></span>
<span>{challenge}</span>
</div>
))}
</div>
<div className="pt-8 border-t border-white/10">
<p className="text-[11px] text-dark-text-muted mb-4 uppercase tracking-widest font-bold flex items-center gap-2">
<Sparkles className="w-3.5 h-3.5" style={{ color: colors.color }} />
</p>
<div className="flex flex-wrap gap-2 mb-5">
{solution.suiteCombination.primaryProducts.map(pid => {
const prod = PRODUCTS.find(p => p.id === pid);
return prod ? (
<span
key={pid}
className="px-3 py-1.5 text-xs font-medium text-white bg-ink-light border border-white/10 group-hover:border-white/20 transition-colors"
>
{prod.title.replace('睿新', '').replace('睿视 ', '')}
</span>
) : null;
})}
<span
className="px-3 py-1.5 text-xs font-bold"
style={{ backgroundColor: colors.soft, color: colors.color }}
>
+
</span>
</div>
<p className="text-sm text-dark-text-muted leading-relaxed">
{solution.suiteCombination.rationale}
</p>
</div>
<motion.div
className="inline-flex items-center gap-2.5 text-sm font-bold mt-8"
style={{ color: colors.color }}
whileHover={{ x: 6 }}
transition={EASE_SPRING}
>
<span></span>
<ArrowRight className="w-4 h-4" />
</motion.div>
</div>
</a>
);
}
function SolutionsGridSection() {
return (
<section className="relative py-20 sm:py-28 md:py-36 lg:py-44 overflow-hidden bg-ink-light">
<GrainOverlay />
<div className="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-white/10 to-transparent" />
<div className="absolute bottom-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-white/10 to-transparent" />
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10">
<div className="flex flex-col lg:flex-row lg:items-end lg:justify-between gap-8 sm:gap-10 md:gap-12 mb-16 sm:mb-20 md:mb-20">
<ScrollReveal className="lg:max-w-3xl">
<SectionLabel>Industry Expertise</SectionLabel>
<h2 className="text-2xl sm:text-3xl md:text-4xl lg:text-5xl font-bold text-white tracking-tight leading-tight">
</h2>
</ScrollReveal>
<ScrollReveal delay={0.1}>
<p className="text-dark-text-secondary max-w-md leading-relaxed text-lg">
</p>
</ScrollReveal>
</div>
<StaggerReveal
className="grid md:grid-cols-2 gap-px bg-white/10"
staggerDelay={0.1}
delayChildren={0.05}
>
{SOLUTIONS.map((solution, index) => (
<SolutionCard key={solution.id} solution={solution} index={index} />
))}
</StaggerReveal>
</div>
</section>
);
}
function CTASection() {
return (
<section className="relative py-20 sm:py-28 md:py-36 lg:py-44 overflow-hidden bg-ink">
<GrainOverlay />
<FloatingInkParticles particles={SOLUTION_PARTICLES} />
<div className="absolute inset-0 pointer-events-none">
<div className="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-brand/30 to-transparent" />
<div className="absolute bottom-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-brand/30 to-transparent" />
<div className="absolute top-1/4 right-[10%] w-[380px] h-[380px] rounded-full opacity-[0.03] blur-3xl bg-brand" />
<div className="absolute bottom-1/4 left-[10%] w-[320px] h-[320px] rounded-full opacity-[0.02] blur-3xl bg-teal-500" />
</div>
<div className="max-w-container mx-auto px-6 lg:px-10 relative z-10">
<ScrollReveal className="text-center max-w-3xl mx-auto">
<div className="flex justify-center mb-7">
<div className="flex items-center gap-5">
<div className="w-14 h-px bg-brand" />
<span className="text-[11px] tracking-[0.4em] uppercase font-bold text-brand">
Get Started
</span>
<div className="w-14 h-px bg-brand" />
</div>
</div>
<h2 className="text-2xl sm:text-3xl md:text-4xl lg:text-5xl font-bold text-white tracking-tight leading-tight mb-6 sm:mb-8">
<br />
<span className="text-brand"></span>
</h2>
<p className="text-xl text-dark-text-secondary mb-12 max-w-2xl mx-auto leading-relaxed">
</p>
<div className="flex flex-wrap justify-center gap-6">
<Button size="xl" asChild>
<a href="/contact">
<ArrowRight className="w-5 h-5 ml-2" />
</a>
</Button>
<Button size="xl" variant="outline" asChild>
<a href="/products"></a>
</Button>
</div>
</ScrollReveal>
</div>
</section>
);
}
export default function SolutionsContentV2() {
return (
<main>
<HeroSection />
<SolutionsGridSection />
<CTASection />
</main>
);
}
@@ -0,0 +1,287 @@
'use client';
import { motion } from 'framer-motion';
import { ArrowUpRight, Factory, ShoppingCart, Heart, GraduationCap, Sparkles, Shield, Truck, type LucideIcon } from 'lucide-react';
import { type Solution } from '@/lib/constants/solutions';
import type { Product } from '@/lib/constants/products';
const EASE_OUT = [0.22, 1, 0.36, 1] as const;
const INDUSTRY_ICONS: Record<string, LucideIcon> = {
制造业: Factory,
贸易零售: ShoppingCart,
医疗健康: Heart,
教育培训: GraduationCap,
金融服务: Shield,
物流运输: Truck,
};
function HeroSection() {
return (
<section className="relative min-h-[85vh] flex items-center overflow-hidden bg-white">
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10 w-full py-28 sm:py-36 md:py-44 lg:py-56">
<div className="max-w-4xl">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.1, ease: EASE_OUT }}
className="text-sm font-mono tracking-[0.2em] text-text-muted uppercase mb-8"
>
Industry Solutions
</motion.div>
<motion.h1
initial={{ opacity: 0, y: 40 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 1, delay: 0.2, ease: EASE_OUT }}
className="text-4xl sm:text-5xl md:text-6xl lg:text-7xl xl:text-8xl font-black text-ink leading-[0.88] tracking-tightest mb-10 sm:mb-12 md:mb-16"
>
<div className="block"></div>
<div className="block mt-4 sm:mt-6">
<span className="relative inline-block">
<span className="relative text-brand font-black">
<motion.span
className="absolute -bottom-2 left-0 w-full h-[3px] bg-brand"
style={{ transformOrigin: 'left' }}
initial={{ scaleX: 0 }}
animate={{ scaleX: 1 }}
transition={{ duration: 0.8, delay: 1.2, ease: EASE_OUT }}
/>
</span>
</span>
</div>
</motion.h1>
<motion.p
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.5, ease: EASE_OUT }}
className="text-lg md:text-xl text-text-secondary max-w-2xl leading-relaxed mb-12 sm:mb-16"
>
+
<br className="hidden sm:block" />
</motion.p>
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.7, ease: EASE_OUT }}
className="flex flex-col sm:flex-row gap-4 sm:gap-6"
>
<a
href="/contact"
className="group inline-flex items-center justify-center gap-3 px-10 py-5 font-semibold text-base bg-brand text-white transition-all duration-300 hover:bg-brand/90 min-h-[52px] touch-manipulation"
>
<ArrowUpRight className="w-4 h-4 transition-transform duration-300 group-hover:translate-x-0.5 group-hover:-translate-y-0.5" />
</a>
<a
href="/products"
className="inline-flex items-center justify-center gap-3 px-10 py-5 font-semibold text-base text-ink border border-border-primary hover:border-ink/30 transition-all duration-300 min-h-[52px] touch-manipulation"
>
</a>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.9, ease: EASE_OUT }}
className="grid grid-cols-3 gap-8 mt-20 pt-12 border-t border-border-primary max-w-xl"
>
<div>
<div className="text-3xl sm:text-4xl font-black text-ink tracking-tight leading-none mb-2">
6<span className="text-brand"></span>
</div>
<div className="text-sm text-text-muted"></div>
</div>
<div>
<div className="text-3xl sm:text-4xl font-black text-ink tracking-tight leading-none mb-2">
<span className="text-brand"></span>
</div>
<div className="text-sm text-text-muted"></div>
</div>
<div>
<div className="text-3xl sm:text-4xl font-black text-ink tracking-tight leading-none mb-2">
<span className="text-brand"></span>
</div>
<div className="text-sm text-text-muted"></div>
</div>
</motion.div>
</div>
</div>
</section>
);
}
function SolutionCard({ solution, index, products }: { solution: Solution; index: number; products?: Product[] }) {
const productLookup = products ?? [];
const Icon = INDUSTRY_ICONS[solution.industry] || Factory;
return (
<motion.a
href={`/solutions/${solution.id}`}
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-60px' }}
transition={{ duration: 0.6, delay: index * 0.08, ease: EASE_OUT }}
className="group relative block transition-all duration-500 hover:bg-bg-secondary bg-white border border-border-primary hover:border-ink/20"
>
<div className="absolute top-0 left-0 h-0 w-[2px] group-hover:h-full transition-all duration-500 origin-top bg-brand" />
<div className="relative z-10 p-8 sm:p-10 md:p-12">
<div className="flex items-start justify-between mb-8">
<div className="w-12 h-12 flex items-center justify-center text-text-secondary group-hover:text-brand transition-colors duration-300">
<Icon className="w-6 h-6" strokeWidth={1.5} />
</div>
<span className="text-5xl sm:text-6xl font-black text-ink/[0.04] tracking-tighter leading-none">
{String(index + 1).padStart(2, '0')}
</span>
</div>
<div className="mb-6">
<span className="inline-block px-3 py-1 text-[11px] font-bold mb-4 text-brand border border-brand/20 bg-brand/[0.05]">
{solution.industry}
</span>
<div className="text-text-muted text-sm tracking-widest uppercase mb-2">
{solution.subtitle}
</div>
<h3 className="text-xl sm:text-2xl md:text-3xl font-bold text-ink group-hover:text-brand transition-colors duration-300 leading-tight">
{solution.title}
</h3>
</div>
<p className="text-text-secondary leading-relaxed mb-6 text-[15px]">
{solution.description}
</p>
<div className="flex flex-col gap-2 mb-6">
{solution.challenges.slice(0, 2).map((challenge, idx) => (
<div key={idx} className="flex items-start gap-2.5 text-sm text-text-secondary">
<span className="text-brand mt-0.5 font-bold"></span>
<span>{challenge}</span>
</div>
))}
</div>
<div className="pt-6 border-t border-border-primary">
<p className="text-[11px] text-text-muted mb-3 uppercase tracking-widest font-bold flex items-center gap-2">
<Sparkles className="w-3.5 h-3.5 text-brand" />
</p>
<div className="flex flex-wrap gap-2 mb-4">
{solution.suiteCombination.primaryProducts.map(pid => {
const prod = productLookup.find(p => p.id === pid);
return prod ? (
<span
key={pid}
className="px-3 py-1.5 text-xs font-medium text-ink bg-bg-secondary border border-border-primary"
>
{prod.title.replace('睿新', '').replace('睿视 ', '')}
</span>
) : null;
})}
<span className="px-3 py-1.5 text-xs font-bold text-brand border border-brand/20 bg-brand/[0.05]">
+
</span>
</div>
<p className="text-sm text-text-muted leading-relaxed">
{solution.suiteCombination.rationale}
</p>
</div>
<div className="inline-flex items-center gap-2 text-sm font-semibold text-text-secondary group-hover:text-brand transition-colors duration-300 mt-6">
<span></span>
<ArrowUpRight className="w-4 h-4 group-hover:translate-x-0.5 group-hover:-translate-y-0.5 transition-transform duration-300" />
</div>
</div>
</motion.a>
);
}
function SolutionsGridSection({ solutions, products }: { solutions: Solution[]; products?: Product[] }) {
return (
<section className="relative py-20 sm:py-28 md:py-36 lg:py-44 overflow-hidden bg-bg-secondary">
<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" />
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10">
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-80px' }}
transition={{ duration: 0.8, ease: EASE_OUT }}
className="max-w-3xl mb-16 sm:mb-20 md:mb-24"
>
<div className="text-sm font-mono tracking-[0.2em] text-text-muted uppercase mb-6">
Industry Expertise
</div>
<h2 className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-extrabold text-ink tracking-tight leading-[0.95]">
</h2>
<p className="mt-6 text-lg text-text-secondary leading-relaxed">
</p>
</motion.div>
<div className="grid md:grid-cols-2 gap-px bg-border-primary">
{solutions.map((solution, index) => (
<SolutionCard key={solution.id} solution={solution} index={index} products={products} />
))}
</div>
</div>
</section>
);
}
function CTASection() {
return (
<section className="relative py-20 sm:py-28 md:py-36 lg:py-44 overflow-hidden bg-white">
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10">
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-80px' }}
transition={{ duration: 0.8, ease: EASE_OUT }}
className="max-w-3xl"
>
<h2 className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-extrabold text-ink tracking-tight leading-[0.95] mb-8">
<br className="sm:hidden" />
</h2>
<p className="text-lg text-text-secondary leading-relaxed mb-10">
</p>
<div className="flex flex-col sm:flex-row gap-4 sm:gap-6">
<a
href="/contact"
className="group inline-flex items-center justify-center gap-3 px-10 py-5 font-semibold text-base bg-brand text-white transition-all duration-300 hover:bg-brand/90 min-h-[52px] touch-manipulation"
>
<ArrowUpRight className="w-4 h-4 transition-transform duration-300 group-hover:translate-x-0.5 group-hover:-translate-y-0.5" />
</a>
<a
href="/products"
className="inline-flex items-center justify-center gap-3 px-10 py-5 font-semibold text-base text-ink border border-border-primary hover:border-ink/30 transition-all duration-300 min-h-[52px] touch-manipulation"
>
</a>
</div>
</motion.div>
</div>
</section>
);
}
export default function SolutionsContentV3({ solutions: solutionsProp, products }: { solutions?: Solution[]; products?: Product[] }) {
const solutions = solutionsProp ?? [];
return (
<main className="bg-white text-ink">
<HeroSection />
<SolutionsGridSection solutions={solutions} products={products} />
<CTASection />
</main>
);
}
@@ -0,0 +1,317 @@
'use client';
import { motion } from 'framer-motion';
import { ScrollReveal, StaggerReveal } from '@/components/ui/scroll-reveal';
import { Button } from '@/components/ui/button';
import { ArrowRight, Shield, Building2, Users, Code, Target, Layers, BookOpen, Sparkles } from 'lucide-react';
import { cn } from '@/lib/utils';
const EASE_OUT = [0.22, 1, 0.36, 1] as const;
function GrainOverlay() {
return (
<div
className="absolute inset-0 pointer-events-none opacity-[0.03] mix-blend-overlay"
style={{
backgroundImage:
"url(\"data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E\")",
}}
/>
);
}
function SectionLabel({ children, className = '' }: { children: React.ReactNode; className?: string }) {
return (
<div className={cn('flex items-center gap-5 mb-7', className)}>
<div className="w-14 h-px bg-brand" />
<span className="text-[11px] tracking-[0.4em] uppercase font-bold text-brand">
{children}
</span>
<div className="w-14 h-px bg-brand" />
</div>
);
}
const TEAM_STRENGTHS = [
{
icon: Shield,
number: '01',
title: '12 年+ 行业深耕',
description: '核心团队长期从事技术咨询、企业数字化等领域,服务覆盖金融、制造、零售、政务、农业等多个行业。',
},
{
icon: Building2,
number: '02',
title: '大型 IT 企业背景',
description: '开发团队成员来自多个大型传统 IT 企业,具备扎实的工程能力、规范化的交付流程和严格的质量意识。',
},
{
icon: Users,
number: '03',
title: '复合型技术团队',
description: '既懂技术又懂业务,能够深入理解客户的真实场景和痛点,提供真正可落地的解决方案。',
},
{
icon: Code,
number: '04',
title: '全栈技术能力',
description: '从前端到后端、从云原生到数据智能、从移动端到物联网——应对各种复杂技术挑战。',
},
{
icon: Target,
number: '05',
title: '结果导向交付',
description: '不以"项目上线"为终点,以"客户业务是否真正改善"为衡量标准。每一个交付成果都追求可量化的业务价值。',
},
];
const TEAM_CULTURE = [
{
icon: Layers,
number: '01',
title: '扁平化协作',
description: '没有冗长的汇报链条,每个人都有机会直接参与决策。信息透明,沟通高效。',
},
{
icon: BookOpen,
number: '02',
title: '持续学习',
description: '技术日新月异,我们保持对新技术的好奇。内部分享、技术沙龙、外部培训——学习是日常的一部分。',
},
{
icon: Sparkles,
number: '03',
title: '客户成功',
description: '我们的成就感来自客户的成功。客户的业务增长,就是我们最好的成绩单。',
},
];
export default function TeamContentV1() {
return (
<div className="min-h-screen bg-ink text-white overflow-x-hidden">
{/* Hero Section */}
<section className="relative pt-32 pb-24 lg:pt-40 lg:pb-32 overflow-hidden">
<GrainOverlay />
<div className="absolute inset-0 bg-gradient-to-b from-ink-light/50 to-ink pointer-events-none" />
<div className="max-w-container mx-auto px-6 lg:px-10 relative z-10">
<ScrollReveal>
<SectionLabel>
<div className="flex items-center gap-5">
<div className="w-14 h-px bg-brand" />
<span className="text-[11px] tracking-[0.4em] uppercase font-bold text-brand">
Our Team
</span>
<div className="w-14 h-px bg-brand" />
</div>
</SectionLabel>
</ScrollReveal>
<ScrollReveal delay={0.1}>
<h1 className="mt-8 text-4xl md:text-5xl lg:text-6xl xl:text-7xl font-bold tracking-tight leading-[1.1]">
<span className="block mt-2 text-brand"></span>
</h1>
</ScrollReveal>
<ScrollReveal delay={0.2}>
<p className="mt-8 text-lg md:text-xl text-white/60 max-w-2xl leading-relaxed">
</p>
</ScrollReveal>
<ScrollReveal delay={0.3}>
<div className="mt-12 flex flex-wrap gap-4">
<Button size="lg" asChild>
<a href="/contact">
<ArrowRight className="w-4 h-4 ml-2" />
</a>
</Button>
<Button size="lg" variant="outline" asChild>
<a href="/about"></a>
</Button>
</div>
</ScrollReveal>
<ScrollReveal delay={0.4}>
<div className="mt-20 grid grid-cols-3 gap-8 md:gap-12 max-w-2xl">
<div className="text-center md:text-left">
<div className="text-4xl md:text-5xl font-bold text-brand">12+</div>
<div className="mt-2 text-sm text-white/50 tracking-wide"></div>
</div>
<div className="text-center">
<div className="text-4xl md:text-5xl font-bold text-white">10+</div>
<div className="mt-2 text-sm text-white/50 tracking-wide"></div>
</div>
<div className="text-center md:text-right">
<div className="text-4xl md:text-5xl font-bold text-white">5+</div>
<div className="mt-2 text-sm text-white/50 tracking-wide"></div>
</div>
</div>
</ScrollReveal>
</div>
</section>
{/* Team Strengths Section */}
<section className="relative py-24 lg:py-32 overflow-hidden bg-ink-light">
<GrainOverlay />
<div className="max-w-container mx-auto px-6 lg:px-10 relative z-10">
<ScrollReveal className="text-center max-w-3xl mx-auto mb-20">
<SectionLabel className="justify-center">
<div className="flex items-center gap-5">
<div className="w-14 h-px bg-brand" />
<span className="text-[11px] tracking-[0.4em] uppercase font-bold text-brand">
Team Strengths
</span>
<div className="w-14 h-px bg-brand" />
</div>
</SectionLabel>
<h2 className="mt-6 text-3xl md:text-4xl lg:text-5xl font-bold tracking-tight leading-tight">
</h2>
<p className="mt-6 text-lg text-white/60 leading-relaxed">
</p>
</ScrollReveal>
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
<StaggerReveal staggerDelay={0.08} delayChildren={0.05}>
{TEAM_STRENGTHS.map((strength) => {
const Icon = strength.icon;
return (
<motion.div
key={strength.title}
className="group relative p-8 lg:p-10 border border-white/10 hover:border-brand/30 bg-ink/50 transition-all duration-500"
whileHover={{ y: -6 }}
transition={{ duration: 0.4, ease: EASE_OUT }}
>
<div className="absolute top-0 left-0 right-0 h-1 bg-brand scale-x-0 group-hover:scale-x-100 transition-transform duration-700 origin-left" />
<div className="flex items-center gap-4 mb-6">
<div className="w-12 h-12 flex items-center justify-center border border-brand/30 bg-brand/10 text-brand">
<Icon className="w-5 h-5" />
</div>
<span className="text-4xl font-bold text-white/10 font-mono">{strength.number}</span>
</div>
<h3 className="text-xl font-bold mb-3">{strength.title}</h3>
<p className="text-white/60 text-sm leading-relaxed">{strength.description}</p>
</motion.div>
);
})}
</StaggerReveal>
</div>
</div>
</section>
{/* Team About Section */}
<section className="relative py-24 lg:py-32 overflow-hidden">
<GrainOverlay />
<div className="max-w-container mx-auto px-6 lg:px-10 relative z-10">
<ScrollReveal>
<div className="max-w-4xl mx-auto p-10 lg:p-16 border border-white/10 bg-ink-light/50">
<h2 className="text-2xl md:text-3xl font-bold mb-8 text-center"></h2>
<div className="space-y-6 max-w-3xl mx-auto text-center">
<p className="text-white/70 leading-relaxed text-lg">
<span className="text-brand font-semibold"></span><span className="text-brand font-semibold"></span> 12
</p>
<p className="text-white/70 leading-relaxed text-lg">
<span className="text-brand font-semibold"> IT </span>
</p>
<p className="text-white/70 leading-relaxed text-lg">
</p>
</div>
</div>
</ScrollReveal>
</div>
</section>
{/* Team Culture Section */}
<section className="relative py-24 lg:py-32 overflow-hidden bg-ink-light">
<GrainOverlay />
<div className="max-w-container mx-auto px-6 lg:px-10 relative z-10">
<ScrollReveal className="text-center max-w-3xl mx-auto mb-20">
<SectionLabel className="justify-center">
<div className="flex items-center gap-5">
<div className="w-14 h-px bg-brand" />
<span className="text-[11px] tracking-[0.4em] uppercase font-bold text-brand">
Team Culture
</span>
<div className="w-14 h-px bg-brand" />
</div>
</SectionLabel>
<h2 className="mt-6 text-3xl md:text-4xl lg:text-5xl font-bold tracking-tight leading-tight">
</h2>
<p className="mt-6 text-lg text-white/60 leading-relaxed">
</p>
</ScrollReveal>
<div className="grid md:grid-cols-3 gap-8">
<StaggerReveal staggerDelay={0.1} delayChildren={0.05}>
{TEAM_CULTURE.map((culture) => {
const Icon = culture.icon;
return (
<motion.div
key={culture.title}
className="group relative p-10 lg:p-12 border border-white/10 hover:border-brand/30 bg-ink/50 transition-all duration-500 text-center"
whileHover={{ y: -8 }}
transition={{ duration: 0.4, ease: EASE_OUT }}
>
<div className="absolute top-0 left-0 right-0 h-1 bg-brand scale-x-0 group-hover:scale-x-100 transition-transform duration-700 origin-left" />
<div className="w-16 h-16 mx-auto mb-6 flex items-center justify-center border border-brand/30 bg-brand/10 text-brand">
<Icon className="w-7 h-7" />
</div>
<span className="block text-6xl font-bold text-white/10 font-mono mb-4">{culture.number}</span>
<h3 className="text-2xl font-bold mb-4">{culture.title}</h3>
<p className="text-white/60 leading-relaxed">{culture.description}</p>
</motion.div>
);
})}
</StaggerReveal>
</div>
</div>
</section>
{/* CTA Section */}
<section className="relative py-24 lg:py-32 overflow-hidden">
<GrainOverlay />
<div className="absolute inset-0 bg-gradient-to-b from-ink to-ink-light pointer-events-none" />
<div className="max-w-container mx-auto px-6 lg:px-10 relative z-10">
<ScrollReveal className="text-center max-w-3xl mx-auto">
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold tracking-tight leading-tight">
</h2>
<p className="mt-6 text-lg text-white/60 leading-relaxed">
</p>
<div className="mt-12 flex flex-wrap justify-center gap-4">
<Button size="lg" asChild>
<a href="/contact">
<ArrowRight className="w-4 h-4 ml-2" />
</a>
</Button>
<Button size="lg" variant="outline" asChild>
<a href="/about"></a>
</Button>
</div>
</ScrollReveal>
</div>
</section>
</div>
);
}
@@ -0,0 +1,352 @@
'use client';
import { useRef, useState, useEffect } from 'react';
import { motion, useInView } from 'framer-motion';
import { ScrollReveal } from '@/components/ui/scroll-reveal';
import { ArrowRight, Shield, Building2, Users, Code, Target, Layers, BookOpen, Sparkles } from 'lucide-react';
import { SectionLabel, EASE_OUT } from '@/components/ui/page-decoration';
import { useReducedMotion } from '@/hooks/use-reduced-motion';
const TEAM_STRENGTHS = [
{
icon: Shield,
number: '01',
title: '12 年+ 行业深耕',
description: '核心团队长期从事技术咨询、企业数字化等领域,服务覆盖金融、制造、零售、政务、农业等多个行业。',
},
{
icon: Building2,
number: '02',
title: '大型 IT 企业背景',
description: '开发团队成员来自多个大型传统 IT 企业,具备扎实的工程能力、规范化的交付流程和严格的质量意识。',
},
{
icon: Users,
number: '03',
title: '复合型技术团队',
description: '既懂技术又懂业务,能够深入理解客户的真实场景和痛点,提供真正可落地的解决方案。',
},
{
icon: Code,
number: '04',
title: '全栈技术能力',
description: '从前端到后端、从云原生到数据智能、从移动端到物联网——应对各种复杂技术挑战。',
},
{
icon: Target,
number: '05',
title: '结果导向交付',
description: '不以"项目上线"为终点,以"客户业务是否真正改善"为衡量标准。每一个交付成果都追求可量化的业务价值。',
},
];
const TEAM_CULTURE = [
{
icon: Layers,
number: '01',
title: '扁平化协作',
description: '没有冗长的汇报链条,每个人都有机会直接参与决策。信息透明,沟通高效。',
},
{
icon: BookOpen,
number: '02',
title: '持续学习',
description: '技术日新月异,我们保持对新技术的好奇。内部分享、技术沙龙、外部培训——学习是日常的一部分。',
},
{
icon: Sparkles,
number: '03',
title: '客户成功',
description: '我们的成就感来自客户的成功。客户的业务增长,就是我们最好的成绩单。',
},
];
const STATS = [
{ value: 12, suffix: '+', label: '年行业经验' },
{ value: 10, suffix: '+', label: '核心成员' },
{ value: 5, suffix: '+', label: '行业覆盖' },
];
function useCountUp(target: number, start: boolean, duration: number = 2000) {
const [count, setCount] = useState(0);
const prefersReducedMotion = useReducedMotion();
useEffect(() => {
if (!start || prefersReducedMotion) {
setCount(target);
return;
}
const startTime = performance.now();
let animationId: number;
const animate = (currentTime: number) => {
const elapsed = currentTime - startTime;
const progress = Math.min(elapsed / duration, 1);
const easeOutQuart = 1 - Math.pow(1 - progress, 4);
setCount(Math.floor(target * easeOutQuart));
if (progress < 1) {
animationId = requestAnimationFrame(animate);
} else {
setCount(target);
}
};
animationId = requestAnimationFrame(animate);
return () => cancelAnimationFrame(animationId);
}, [target, start, duration, prefersReducedMotion]);
return count;
}
function StatItem({ value, suffix, label, start, delay }: { value: number; suffix: string; label: string; start: boolean; delay: number }) {
const count = useCountUp(value, start, 2000);
const shouldReduceMotion = useReducedMotion();
return (
<motion.div
initial={shouldReduceMotion ? {} : { opacity: 0, y: 20 }}
animate={start ? { opacity: 1, y: 0 } : {}}
transition={{ delay, duration: 0.6, ease: EASE_OUT }}
className="text-center"
>
<div className="text-4xl sm:text-5xl font-bold text-ink tracking-tight leading-none mb-2">
{count}<span className="text-brand">{suffix}</span>
</div>
<div className="text-sm text-text-muted tracking-wide">{label}</div>
</motion.div>
);
}
function StrengthCard({ strength, index }: { strength: typeof TEAM_STRENGTHS[0]; index: number }) {
const Icon = strength.icon;
const shouldReduceMotion = useReducedMotion();
return (
<motion.div
key={strength.title}
className="group relative border border-border-primary hover:border-brand/30 bg-white transition-all duration-500 overflow-hidden"
initial={shouldReduceMotion ? {} : { opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-80px' }}
transition={{ duration: 0.6, delay: index * 0.08, ease: EASE_OUT }}
whileHover={{ y: -6 }}
>
<div className="absolute top-0 left-0 bottom-0 w-1 bg-brand scale-y-0 group-hover:scale-y-100 transition-transform duration-500 origin-top" />
<div className="p-8 sm:p-10">
<div className="flex items-center gap-4 mb-6">
<div className="w-12 h-12 flex items-center justify-center border border-brand/30 bg-brand/[0.08] text-brand">
<Icon className="w-5 h-5" />
</div>
<span className="text-4xl font-bold text-text-muted/10 font-mono">{strength.number}</span>
</div>
<h3 className="text-xl font-bold text-ink mb-3">{strength.title}</h3>
<p className="text-text-secondary text-sm leading-relaxed">{strength.description}</p>
</div>
<div className="absolute bottom-0 left-0 right-0 h-px bg-gradient-to-r from-brand/40 via-brand/10 to-transparent scale-x-0 group-hover:scale-x-100 transition-transform duration-700 origin-left" />
</motion.div>
);
}
function CultureCard({ culture, index }: { culture: typeof TEAM_CULTURE[0]; index: number }) {
const Icon = culture.icon;
const shouldReduceMotion = useReducedMotion();
return (
<motion.div
key={culture.title}
className="group relative border border-border-primary hover:border-brand/30 bg-white transition-all duration-500 text-center overflow-hidden"
initial={shouldReduceMotion ? {} : { opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-80px' }}
transition={{ duration: 0.6, delay: index * 0.1, ease: EASE_OUT }}
whileHover={{ y: -8 }}
>
<div className="absolute top-0 left-0 right-0 h-1 bg-brand scale-x-0 group-hover:scale-x-100 transition-transform duration-700 origin-left" />
<div className="p-8 sm:p-10 lg:p-12">
<div className="w-16 h-16 mx-auto mb-6 flex items-center justify-center border border-brand/30 bg-brand/[0.08] text-brand">
<Icon className="w-7 h-7" />
</div>
<span className="block text-6xl font-bold text-text-muted/10 font-mono mb-4">{culture.number}</span>
<h3 className="text-2xl font-bold text-ink mb-4">{culture.title}</h3>
<p className="text-text-secondary leading-relaxed">{culture.description}</p>
</div>
<div className="absolute bottom-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-brand/20 to-transparent scale-x-0 group-hover:scale-x-100 transition-transform duration-700 origin-center" />
</motion.div>
);
}
export default function TeamContentV2() {
const statsRef = useRef(null);
const isInView = useInView(statsRef, { once: true, margin: '-100px' });
return (
<div className="min-h-screen bg-white text-ink overflow-x-hidden">
{/* Hero Section */}
<section className="relative min-h-[85vh] flex items-center overflow-hidden bg-white">
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10 w-full py-28 sm:py-36 md:py-44 lg:py-56">
<div className="max-w-4xl">
<ScrollReveal>
<SectionLabel>Our Team</SectionLabel>
</ScrollReveal>
<ScrollReveal delay={0.1}>
<h1 className="text-4xl sm:text-5xl md:text-6xl lg:text-7xl xl:text-8xl font-bold leading-[0.92] tracking-tighter mb-10 sm:mb-12 md:mb-16">
<span className="block mt-4 text-brand"></span>
</h1>
</ScrollReveal>
<ScrollReveal delay={0.2}>
<p className="text-lg md:text-xl text-text-secondary max-w-2xl leading-relaxed mb-12">
</p>
</ScrollReveal>
<ScrollReveal delay={0.3}>
<div className="flex flex-col sm:flex-row gap-4 sm:gap-6 mb-16">
<a
href="/contact"
className="group inline-flex items-center justify-center gap-3 px-12 py-6 font-semibold text-base bg-brand text-white transition-all duration-300 hover:bg-brand/90"
>
<ArrowRight className="w-4 h-4 transition-transform duration-300 group-hover:translate-x-1" />
</a>
<a
href="/about"
className="inline-flex items-center justify-center gap-3 px-12 py-6 font-semibold text-base text-ink border border-border-primary hover:border-border-secondary hover:bg-bg-secondary transition-all duration-300"
>
</a>
</div>
</ScrollReveal>
<div ref={statsRef} className="grid grid-cols-3 gap-6 sm:gap-8 md:gap-12 max-w-2xl">
{STATS.map((stat, idx) => (
<StatItem
key={stat.label}
value={stat.value}
suffix={stat.suffix}
label={stat.label}
start={isInView}
delay={idx * 0.15}
/>
))}
</div>
</div>
</div>
</section>
{/* Team Strengths Section */}
<section className="relative py-20 sm:py-28 md:py-36 lg:py-44 overflow-hidden bg-bg-secondary">
<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" />
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10">
<ScrollReveal className="text-center max-w-3xl mx-auto mb-16 sm:mb-20">
<SectionLabel className="justify-center">Team Strengths</SectionLabel>
<h2 className="text-3xl sm:text-4xl md:text-5xl font-bold tracking-tight leading-tight text-ink">
</h2>
<p className="mt-6 text-lg text-text-secondary leading-relaxed">
</p>
</ScrollReveal>
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6 sm:gap-8">
{TEAM_STRENGTHS.map((strength, idx) => (
<StrengthCard key={strength.title} strength={strength} index={idx} />
))}
</div>
</div>
</section>
{/* Team About Section */}
<section className="relative py-20 sm:py-28 md:py-36 lg:py-44 overflow-hidden bg-white">
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10">
<ScrollReveal>
<div className="max-w-4xl mx-auto p-8 sm:p-10 lg:p-16 border border-border-primary bg-bg-secondary/50">
<h2 className="text-2xl sm:text-3xl font-bold mb-8 text-center text-ink"></h2>
<div className="space-y-6 max-w-3xl mx-auto text-center">
<p className="text-text-secondary leading-relaxed text-base sm:text-lg">
<span className="text-brand font-semibold"></span><span className="text-brand font-semibold"></span> 12
</p>
<p className="text-text-secondary leading-relaxed text-base sm:text-lg">
<span className="text-brand font-semibold"> IT </span>
</p>
<p className="text-text-secondary leading-relaxed text-base sm:text-lg">
</p>
</div>
</div>
</ScrollReveal>
</div>
</section>
{/* Team Culture Section */}
<section className="relative py-20 sm:py-28 md:py-36 lg:py-44 overflow-hidden bg-bg-secondary">
<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" />
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10">
<ScrollReveal className="text-center max-w-3xl mx-auto mb-16 sm:mb-20">
<SectionLabel className="justify-center">Team Culture</SectionLabel>
<h2 className="text-3xl sm:text-4xl md:text-5xl font-bold tracking-tight leading-tight text-ink">
</h2>
<p className="mt-6 text-lg text-text-secondary leading-relaxed">
</p>
</ScrollReveal>
<div className="grid md:grid-cols-3 gap-8">
{TEAM_CULTURE.map((culture, idx) => (
<CultureCard key={culture.title} culture={culture} index={idx} />
))}
</div>
</div>
</section>
{/* CTA Section */}
<section className="relative py-20 sm:py-28 md:py-36 lg:py-44 overflow-hidden bg-white">
<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" />
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10">
<ScrollReveal className="text-center max-w-3xl mx-auto">
<h2 className="text-3xl sm:text-4xl md:text-5xl font-bold tracking-tight leading-tight text-ink">
</h2>
<p className="mt-6 text-lg text-text-secondary leading-relaxed">
</p>
<div className="mt-12 flex flex-col sm:flex-row justify-center gap-4 sm:gap-6">
<a
href="/contact"
className="group inline-flex items-center justify-center gap-3 px-12 py-6 font-semibold text-base bg-brand text-white transition-all duration-300 hover:bg-brand/90"
>
<ArrowRight className="w-4 h-4 transition-transform duration-300 group-hover:translate-x-1" />
</a>
<a
href="/about"
className="inline-flex items-center justify-center gap-3 px-12 py-6 font-semibold text-base text-ink border border-border-primary hover:border-border-secondary hover:bg-bg-secondary transition-all duration-300"
>
</a>
</div>
</ScrollReveal>
</div>
</section>
</div>
);
}
@@ -0,0 +1,373 @@
'use client';
import { useRef, useState, useEffect } from 'react';
import { motion, useInView } from 'framer-motion';
import { ScrollReveal } from '@/components/ui/scroll-reveal';
import { ArrowRight, Shield, Building2, Users, Code, Target, Layers, BookOpen, Sparkles, type LucideIcon } from 'lucide-react';
import { SectionLabel, EASE_OUT } from '@/components/ui/page-decoration';
import { useReducedMotion } from '@/hooks/use-reduced-motion';
/** Icon name-to-component mapping for CMS string-based icons */
const ICON_MAP: Record<string, LucideIcon> = {
Shield,
Building2,
Users,
Code,
Target,
Layers,
BookOpen,
Sparkles,
};
interface StrengthData {
number: string;
title: string;
description: string;
iconName: string;
}
interface CultureData {
number: string;
title: string;
description: string;
iconName: string;
}
interface StatData {
value: number;
suffix: string;
label: string;
}
interface ParagraphData {
text: string;
}
interface TeamData {
heroSubtitle?: string;
heroTitle?: string;
heroDescription?: string;
heroPrimaryCtaLabel?: string;
heroPrimaryCtaHref?: string;
heroSecondaryCtaLabel?: string;
heroSecondaryCtaHref?: string;
strengths?: StrengthData[];
culture?: CultureData[];
stats?: StatData[];
aboutTitle?: string;
aboutParagraphs?: ParagraphData[];
ctaTitle?: string;
ctaDescription?: string;
ctaPrimaryLabel?: string;
ctaPrimaryHref?: string;
ctaSecondaryLabel?: string;
ctaSecondaryHref?: string;
}
function useCountUp(target: number, start: boolean, duration: number = 2000) {
const [count, setCount] = useState(0);
const prefersReducedMotion = useReducedMotion();
useEffect(() => {
if (!start || prefersReducedMotion) {
setCount(target);
return;
}
const startTime = performance.now();
let animationId: number;
const animate = (currentTime: number) => {
const elapsed = currentTime - startTime;
const progress = Math.min(elapsed / duration, 1);
const easeOutQuart = 1 - Math.pow(1 - progress, 4);
setCount(Math.floor(target * easeOutQuart));
if (progress < 1) {
animationId = requestAnimationFrame(animate);
} else {
setCount(target);
}
};
animationId = requestAnimationFrame(animate);
return () => cancelAnimationFrame(animationId);
}, [target, start, duration, prefersReducedMotion]);
return count;
}
function StatItem({ value, suffix, label, start, delay }: { value: number; suffix: string; label: string; start: boolean; delay: number }) {
const count = useCountUp(value, start, 2000);
const shouldReduceMotion = useReducedMotion();
return (
<motion.div
initial={shouldReduceMotion ? {} : { opacity: 0, y: 20 }}
animate={start ? { opacity: 1, y: 0 } : {}}
transition={{ delay, duration: 0.6, ease: EASE_OUT }}
className="text-center"
>
<div className="text-4xl sm:text-5xl font-bold text-ink tracking-tight leading-none mb-2">
{count}<span className="text-brand">{suffix}</span>
</div>
<div className="text-sm text-text-muted tracking-wide">{label}</div>
</motion.div>
);
}
function StrengthCard({ strength, index }: { strength: StrengthData; index: number }) {
const Icon = ICON_MAP[strength.iconName] ?? Shield;
const shouldReduceMotion = useReducedMotion();
return (
<motion.div
key={strength.title}
className="group relative border border-border-primary hover:border-brand/30 bg-white transition-all duration-500 overflow-hidden"
initial={shouldReduceMotion ? {} : { opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-80px' }}
transition={{ duration: 0.6, delay: index * 0.08, ease: EASE_OUT }}
whileHover={{ y: -6 }}
>
<div className="absolute top-0 left-0 bottom-0 w-1 bg-brand scale-y-0 group-hover:scale-y-100 transition-transform duration-500 origin-top" />
<div className="p-8 sm:p-10">
<div className="flex items-center gap-4 mb-6">
<div className="w-12 h-12 flex items-center justify-center border border-brand/30 bg-brand/[0.08] text-brand">
<Icon className="w-5 h-5" />
</div>
<span className="text-4xl font-bold text-text-muted/10 font-mono">{strength.number}</span>
</div>
<h3 className="text-xl font-bold text-ink mb-3">{strength.title}</h3>
<p className="text-text-secondary text-sm leading-relaxed">{strength.description}</p>
</div>
<div className="absolute bottom-0 left-0 right-0 h-px bg-gradient-to-r from-brand/40 via-brand/10 to-transparent scale-x-0 group-hover:scale-x-100 transition-transform duration-700 origin-left" />
</motion.div>
);
}
function CultureCard({ culture, index }: { culture: CultureData; index: number }) {
const Icon = ICON_MAP[culture.iconName] ?? Layers;
const shouldReduceMotion = useReducedMotion();
return (
<motion.div
key={culture.title}
className="group relative border border-border-primary hover:border-brand/30 bg-white transition-all duration-500 text-center overflow-hidden"
initial={shouldReduceMotion ? {} : { opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-80px' }}
transition={{ duration: 0.6, delay: index * 0.1, ease: EASE_OUT }}
whileHover={{ y: -8 }}
>
<div className="absolute top-0 left-0 right-0 h-1 bg-brand scale-x-0 group-hover:scale-x-100 transition-transform duration-700 origin-left" />
<div className="p-8 sm:p-10 lg:p-12">
<div className="w-16 h-16 mx-auto mb-6 flex items-center justify-center border border-brand/30 bg-brand/[0.08] text-brand">
<Icon className="w-7 h-7" />
</div>
<span className="block text-6xl font-bold text-text-muted/10 font-mono mb-4">{culture.number}</span>
<h3 className="text-2xl font-bold text-ink mb-4">{culture.title}</h3>
<p className="text-text-secondary leading-relaxed">{culture.description}</p>
</div>
<div className="absolute bottom-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-brand/20 to-transparent scale-x-0 group-hover:scale-x-100 transition-transform duration-700 origin-center" />
</motion.div>
);
}
export default function TeamContentV3({ data }: { data: Record<string, unknown> }) {
const teamData = data as TeamData;
const statsRef = useRef(null);
const isInView = useInView(statsRef, { once: true, margin: '-100px' });
const strengths = teamData.strengths ?? [];
const culture = teamData.culture ?? [];
const stats = teamData.stats ?? [];
const aboutParagraphs = teamData.aboutParagraphs ?? [];
// Empty state when no CMS data is available
if (!data || Object.keys(data).length === 0) {
return (
<div className="min-h-screen bg-white flex items-center justify-center">
<div className="text-text-muted text-lg"></div>
</div>
);
}
return (
<div className="min-h-screen bg-white text-ink overflow-x-hidden">
{/* Hero Section */}
<section className="relative min-h-[85vh] flex items-center overflow-hidden bg-white">
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10 w-full py-28 sm:py-36 md:py-44 lg:py-56">
<div className="max-w-4xl">
<ScrollReveal>
<SectionLabel>{teamData.heroSubtitle || ''}</SectionLabel>
</ScrollReveal>
<ScrollReveal delay={0.1}>
<h1 className="text-4xl sm:text-5xl md:text-6xl lg:text-7xl xl:text-8xl font-bold leading-[0.92] tracking-tighter mb-10 sm:mb-12 md:mb-16">
{(teamData.heroTitle || '').split('\n').map((line, i) => (
<span key={i}>
{i > 0 && <span className="block mt-4" />}
{i === 0 ? line : <span className="text-brand">{line}</span>}
</span>
))}
</h1>
</ScrollReveal>
<ScrollReveal delay={0.2}>
<p className="text-lg md:text-xl text-text-secondary max-w-2xl leading-relaxed mb-12">
{teamData.heroDescription || ''}
</p>
</ScrollReveal>
<ScrollReveal delay={0.3}>
<div className="flex flex-col sm:flex-row gap-4 sm:gap-6 mb-16">
<a
href={teamData.heroPrimaryCtaHref || '#'}
className="group inline-flex items-center justify-center gap-3 px-12 py-6 font-semibold text-base bg-brand text-white transition-all duration-300 hover:bg-brand/90"
>
{teamData.heroPrimaryCtaLabel || ''}
<ArrowRight className="w-4 h-4 transition-transform duration-300 group-hover:translate-x-1" />
</a>
<a
href={teamData.heroSecondaryCtaHref || '#'}
className="inline-flex items-center justify-center gap-3 px-12 py-6 font-semibold text-base text-ink border border-border-primary hover:border-border-secondary hover:bg-bg-secondary transition-all duration-300"
>
{teamData.heroSecondaryCtaLabel || ''}
</a>
</div>
</ScrollReveal>
{stats.length > 0 && (
<div ref={statsRef} className="grid grid-cols-3 gap-6 sm:gap-8 md:gap-12 max-w-2xl">
{stats.map((stat, idx) => (
<StatItem
key={stat.label}
value={stat.value}
suffix={stat.suffix}
label={stat.label}
start={isInView}
delay={idx * 0.15}
/>
))}
</div>
)}
</div>
</div>
</section>
{/* Team Strengths Section */}
{strengths.length > 0 && (
<section className="relative py-20 sm:py-28 md:py-36 lg:py-44 overflow-hidden bg-bg-secondary">
<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" />
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10">
<ScrollReveal className="text-center max-w-3xl mx-auto mb-16 sm:mb-20">
<SectionLabel className="justify-center">Team Strengths</SectionLabel>
<h2 className="text-3xl sm:text-4xl md:text-5xl font-bold tracking-tight leading-tight text-ink">
</h2>
<p className="mt-6 text-lg text-text-secondary leading-relaxed">
</p>
</ScrollReveal>
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6 sm:gap-8">
{strengths.map((strength, idx) => (
<StrengthCard key={strength.title} strength={strength} index={idx} />
))}
</div>
</div>
</section>
)}
{/* Team About Section */}
{(teamData.aboutTitle || aboutParagraphs.length > 0) && (
<section className="relative py-20 sm:py-28 md:py-36 lg:py-44 overflow-hidden bg-white">
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10">
<ScrollReveal>
<div className="max-w-4xl mx-auto p-8 sm:p-10 lg:p-16 border border-border-primary bg-bg-secondary/50">
<h2 className="text-2xl sm:text-3xl font-bold mb-8 text-center text-ink">
{teamData.aboutTitle || ''}
</h2>
<div className="space-y-6 max-w-3xl mx-auto text-center">
{aboutParagraphs.map((p, i) => (
<p key={i} className="text-text-secondary leading-relaxed text-base sm:text-lg">
{p.text}
</p>
))}
</div>
</div>
</ScrollReveal>
</div>
</section>
)}
{/* Team Culture Section */}
{culture.length > 0 && (
<section className="relative py-20 sm:py-28 md:py-36 lg:py-44 overflow-hidden bg-bg-secondary">
<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" />
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10">
<ScrollReveal className="text-center max-w-3xl mx-auto mb-16 sm:mb-20">
<SectionLabel className="justify-center">Team Culture</SectionLabel>
<h2 className="text-3xl sm:text-4xl md:text-5xl font-bold tracking-tight leading-tight text-ink">
</h2>
<p className="mt-6 text-lg text-text-secondary leading-relaxed">
</p>
</ScrollReveal>
<div className="grid md:grid-cols-3 gap-8">
{culture.map((item, idx) => (
<CultureCard key={item.title} culture={item} index={idx} />
))}
</div>
</div>
</section>
)}
{/* CTA Section */}
<section className="relative py-20 sm:py-28 md:py-36 lg:py-44 overflow-hidden bg-white">
<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" />
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10">
<ScrollReveal className="text-center max-w-3xl mx-auto">
<h2 className="text-3xl sm:text-4xl md:text-5xl font-bold tracking-tight leading-tight text-ink">
{teamData.ctaTitle || ''}
</h2>
<p className="mt-6 text-lg text-text-secondary leading-relaxed">
{teamData.ctaDescription || ''}
</p>
<div className="mt-12 flex flex-col sm:flex-row justify-center gap-4 sm:gap-6">
<a
href={teamData.ctaPrimaryHref || '#'}
className="group inline-flex items-center justify-center gap-3 px-12 py-6 font-semibold text-base bg-brand text-white transition-all duration-300 hover:bg-brand/90"
>
{teamData.ctaPrimaryLabel || ''}
<ArrowRight className="w-4 h-4 transition-transform duration-300 group-hover:translate-x-1" />
</a>
<a
href={teamData.ctaSecondaryHref || '#'}
className="inline-flex items-center justify-center gap-3 px-12 py-6 font-semibold text-base text-ink border border-border-primary hover:border-border-secondary hover:bg-bg-secondary transition-all duration-300"
>
{teamData.ctaSecondaryLabel || ''}
</a>
</div>
</ScrollReveal>
</div>
</section>
</div>
);
}
@@ -0,0 +1,391 @@
'use client';
import { useEffect, useState } from 'react';
import { useRouter, useParams } from 'next/navigation';
import { useAuth } from '@/components/admin/auth-context';
import { adminApi } from '@/lib/admin-api';
import { ArrowLeft, Save } from 'lucide-react';
const MODEL_LABELS: Record<string, string> = {
news: '新闻资讯',
'case-study': '案例研究',
service: '服务管理',
product: '产品管理',
solution: '解决方案',
'hero-banner': 'Hero Banner',
'stat-item': '数据指标',
};
interface FieldDef {
name: string;
type: string;
label: string;
required?: boolean;
description?: string;
placeholder?: string;
defaultValue?: unknown;
options?: Array<{ label: string; value: string | number | boolean }>;
fields?: FieldDef[];
}
interface ModelData {
id: string;
code: string;
name: string;
fields: FieldDef[];
}
export default function ContentEditorPage() {
const params = useParams();
const modelCode = params.modelCode as string;
const itemId = params.itemId as string;
const isNew = itemId === 'new';
const { user, loading: authLoading } = useAuth();
const router = useRouter();
const [model, setModel] = useState<ModelData | null>(null);
const [formData, setFormData] = useState<Record<string, unknown>>({});
const [title, setTitle] = useState('');
const [slug, setSlug] = useState('');
const [status, setStatus] = useState('draft');
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [error, setError] = useState('');
const modelLabel = MODEL_LABELS[modelCode] || modelCode;
useEffect(() => {
if (!authLoading && !user) {
router.push('/admin/login');
return;
}
if (!user) return;
const load = async () => {
try {
// 获取模型定义
const modelsRes = await adminApi.getModels();
const models = (modelsRes as ModelData[]) || [];
const found = models.find((m) => m.code === modelCode);
if (!found) throw new Error('模型未找到');
setModel(found);
// 初始化默认值
const defaults: Record<string, unknown> = {};
found.fields.forEach((f) => {
if (f.defaultValue !== undefined) defaults[f.name] = f.defaultValue;
});
setFormData(defaults);
// 如果是编辑,加载现有数据
if (!isNew) {
const itemsRes = await adminApi.getItems({ modelCode, page: 1, pageSize: 100 });
const items = (itemsRes as { items: Array<{ id: string; title: string; slug: string; status: string; data: Record<string, unknown> }> }).items || [];
const item = items.find((i) => i.id === itemId);
if (item) {
setTitle(item.title);
setSlug(item.slug);
setStatus(item.status);
setFormData(item.data || {});
}
}
} catch (err) {
setError(err instanceof Error ? err.message : '加载失败');
} finally {
setLoading(false);
}
};
load();
}, [user, authLoading, router, modelCode, itemId, isNew]);
const handleFieldChange = (name: string, value: unknown) => {
setFormData((prev) => ({ ...prev, [name]: value }));
};
const handleSave = async () => {
if (!title.trim()) {
alert('请输入标题');
return;
}
setSaving(true);
setError('');
try {
if (isNew) {
await adminApi.createItem({
modelId: model?.id,
modelCode,
title: title.trim(),
slug: slug.trim() || undefined,
status,
data: formData,
});
} else {
await adminApi.updateItem(itemId, {
title: title.trim(),
slug: slug.trim() || undefined,
status,
data: formData,
});
}
router.push(`/admin/content/${modelCode}`);
} catch (err) {
setError(err instanceof Error ? err.message : '保存失败');
setSaving(false);
}
};
const renderField = (field: FieldDef, prefix = '') => {
const key = prefix + field.name;
const value = formData[field.name];
switch (field.type) {
case 'text':
return (
<div key={key}>
<label className="block text-sm font-medium text-gray-700 mb-1">
{field.label}
{field.required && <span className="text-red-500 ml-1">*</span>}
</label>
{field.description && (
<p className="text-xs text-gray-400 mb-1.5">{field.description}</p>
)}
<input
type="text"
value={(value as string) || ''}
onChange={(e) => handleFieldChange(field.name, e.target.value)}
placeholder={field.placeholder}
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-gray-900 focus:border-transparent"
/>
</div>
);
case 'textarea':
return (
<div key={key}>
<label className="block text-sm font-medium text-gray-700 mb-1">
{field.label}
{field.required && <span className="text-red-500 ml-1">*</span>}
</label>
{field.description && (
<p className="text-xs text-gray-400 mb-1.5">{field.description}</p>
)}
<textarea
value={(value as string) || ''}
onChange={(e) => handleFieldChange(field.name, e.target.value)}
placeholder={field.placeholder}
rows={4}
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-gray-900 focus:border-transparent resize-y"
/>
</div>
);
case 'number':
return (
<div key={key}>
<label className="block text-sm font-medium text-gray-700 mb-1">
{field.label}
{field.required && <span className="text-red-500 ml-1">*</span>}
</label>
<input
type="number"
value={(value as number) ?? ''}
onChange={(e) => handleFieldChange(field.name, parseFloat(e.target.value) || 0)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-gray-900 focus:border-transparent"
/>
</div>
);
case 'boolean':
return (
<div key={key} className="flex items-center gap-3">
<input
type="checkbox"
checked={!!value}
onChange={(e) => handleFieldChange(field.name, e.target.checked)}
className="w-4 h-4 rounded border-gray-300 text-gray-900 focus:ring-gray-900"
/>
<label className="text-sm font-medium text-gray-700">{field.label}</label>
{field.description && (
<span className="text-xs text-gray-400">{field.description}</span>
)}
</div>
);
case 'select':
case 'dropdown':
return (
<div key={key}>
<label className="block text-sm font-medium text-gray-700 mb-1">
{field.label}
</label>
<select
value={(value as string) || ''}
onChange={(e) => handleFieldChange(field.name, e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-gray-900 focus:border-transparent"
>
<option value=""></option>
{field.options?.map((opt) => (
<option key={String(opt.value)} value={String(opt.value)}>
{opt.label}
</option>
))}
</select>
</div>
);
case 'image':
return (
<div key={key}>
<label className="block text-sm font-medium text-gray-700 mb-1">
{field.label}
</label>
<input
type="text"
value={(value as string) || ''}
onChange={(e) => handleFieldChange(field.name, e.target.value)}
placeholder="输入图片 URL 或从媒体库选择"
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-gray-900 focus:border-transparent"
/>
{!!value && (
<img
src={String(value)}
alt="预览"
className="mt-2 max-h-32 rounded border border-gray-200"
/>
)}
</div>
);
case 'array':
case 'object':
return (
<div key={key} className="border border-gray-200 rounded-lg p-4">
<label className="block text-sm font-medium text-gray-700 mb-3">
{field.label}
</label>
{field.fields?.map((subField) => renderField(subField, `${field.name}.`))}
</div>
);
default:
return (
<div key={key}>
<label className="block text-sm font-medium text-gray-700 mb-1">
{field.label}
</label>
<input
type="text"
value={(value as string) || ''}
onChange={(e) => handleFieldChange(field.name, e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-gray-900 focus:border-transparent"
/>
</div>
);
}
};
if (authLoading || loading) {
return (
<div className="flex items-center justify-center h-64">
<div className="animate-spin w-8 h-8 border-2 border-gray-900 border-t-transparent rounded-full" />
</div>
);
}
return (
<div className="space-y-6 max-w-3xl">
{/* Header */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<button
onClick={() => router.push(`/admin/content/${modelCode}`)}
className="p-2 rounded hover:bg-gray-100 transition-colors"
>
<ArrowLeft className="w-5 h-5" />
</button>
<div>
<h1 className="text-xl font-bold text-gray-900">
{isNew ? `新增${modelLabel}` : `编辑${modelLabel}`}
</h1>
<p className="text-sm text-gray-500 mt-1">{model?.name}</p>
</div>
</div>
<button
onClick={handleSave}
disabled={saving}
className="flex items-center gap-2 bg-gray-900 text-white rounded-lg px-4 py-2 text-sm font-medium hover:bg-gray-800 disabled:opacity-50 transition-colors"
>
<Save className="w-4 h-4" />
{saving ? '保存中...' : '保存'}
</button>
</div>
{error && (
<div className="bg-red-50 border border-red-200 text-red-700 rounded-lg px-4 py-3 text-sm">
{error}
</div>
)}
{/* Form */}
<div className="bg-white rounded-lg border border-gray-200 p-6 space-y-5">
{/* 标题 */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
<span className="text-red-500">*</span>
</label>
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="请输入标题"
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-gray-900 focus:border-transparent"
/>
</div>
{/* Slug */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Slug</label>
<input
type="text"
value={slug}
onChange={(e) => setSlug(e.target.value)}
placeholder="URL 友好标识(留空自动生成)"
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-gray-900 focus:border-transparent"
/>
</div>
{/* 状态 */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1"></label>
<select
value={status}
onChange={(e) => setStatus(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-gray-900 focus:border-transparent"
>
<option value="draft">稿</option>
<option value="published"></option>
<option value="archived"></option>
</select>
</div>
{/* 模型字段 */}
{model?.fields?.map((field) => renderField(field))}
</div>
{/* Bottom save */}
<div className="flex justify-end">
<button
onClick={handleSave}
disabled={saving}
className="flex items-center gap-2 bg-gray-900 text-white rounded-lg px-6 py-2.5 text-sm font-medium hover:bg-gray-800 disabled:opacity-50 transition-colors"
>
<Save className="w-4 h-4" />
{saving ? '保存中...' : '保存'}
</button>
</div>
</div>
);
}
+239
View File
@@ -0,0 +1,239 @@
'use client';
import { useEffect, useState, useCallback } from 'react';
import { useRouter, useParams } from 'next/navigation';
import { useAuth } from '@/components/admin/auth-context';
import { adminApi } from '@/lib/admin-api';
import { Plus, Edit, Trash2, Search, ChevronLeft, ChevronRight } from 'lucide-react';
const MODEL_LABELS: Record<string, string> = {
news: '新闻资讯',
'case-study': '案例研究',
service: '服务管理',
product: '产品管理',
solution: '解决方案',
'hero-banner': 'Hero Banner',
'stat-item': '数据指标',
};
interface ContentItem {
id: string;
title: string;
slug: string;
status: string;
data: Record<string, unknown>;
sortOrder: number;
publishedAt: string | null;
updatedAt: string;
}
export default function ContentListPage() {
const params = useParams();
const modelCode = params.modelCode as string;
const { user, loading: authLoading } = useAuth();
const router = useRouter();
const [items, setItems] = useState<ContentItem[]>([]);
const [total, setTotal] = useState(0);
const [page, setPage] = useState(1);
const [search, setSearch] = useState('');
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const pageSize = 10;
const modelLabel = MODEL_LABELS[modelCode] || modelCode;
const fetchItems = useCallback(async () => {
setLoading(true);
setError('');
try {
const res = await adminApi.getItems({
modelCode,
page,
pageSize,
...(search ? { search } : {}),
});
const data = res as { items: ContentItem[]; total: number };
setItems(data?.items || []);
setTotal(data?.total || 0);
} catch (err) {
setError(err instanceof Error ? err.message : '加载失败');
} finally {
setLoading(false);
}
}, [modelCode, page, search]);
useEffect(() => {
if (!authLoading && !user) {
router.push('/admin/login');
return;
}
if (user) fetchItems();
}, [user, authLoading, router, fetchItems]);
const handleDelete = async (id: string) => {
if (!confirm('确定要删除此内容吗?')) return;
try {
await adminApi.deleteItem(id);
fetchItems();
} catch (err) {
alert(err instanceof Error ? err.message : '删除失败');
}
};
const totalPages = Math.ceil(total / pageSize);
if (authLoading || (!user && typeof window !== 'undefined')) {
return (
<div className="flex items-center justify-center h-64">
<div className="animate-spin w-8 h-8 border-2 border-gray-900 border-t-transparent rounded-full" />
</div>
);
}
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-xl font-bold text-gray-900">{modelLabel}</h1>
<p className="text-sm text-gray-500 mt-1"> {total} </p>
</div>
<button
onClick={() => router.push(`/admin/content/${modelCode}/new`)}
className="flex items-center gap-2 bg-gray-900 text-white rounded-lg px-4 py-2 text-sm font-medium hover:bg-gray-800 transition-colors"
>
<Plus className="w-4 h-4" />
</button>
</div>
{/* Search */}
<div className="flex gap-3">
<div className="relative flex-1 max-w-md">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
<input
type="text"
value={search}
onChange={(e) => { setSearch(e.target.value); setPage(1); }}
placeholder={`搜索${modelLabel}...`}
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-gray-900 focus:border-transparent"
/>
</div>
</div>
{/* Error */}
{error && (
<div className="bg-red-50 border border-red-200 text-red-700 rounded-lg px-4 py-3 text-sm">
{error}
</div>
)}
{/* Table */}
<div className="bg-white rounded-lg border border-gray-200 overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b border-gray-200 bg-gray-50">
<th className="text-left px-4 py-3 text-xs font-medium text-gray-500 uppercase"></th>
<th className="text-left px-4 py-3 text-xs font-medium text-gray-500 uppercase">Slug</th>
<th className="text-left px-4 py-3 text-xs font-medium text-gray-500 uppercase"></th>
<th className="text-left px-4 py-3 text-xs font-medium text-gray-500 uppercase"></th>
<th className="text-right px-4 py-3 text-xs font-medium text-gray-500 uppercase"></th>
</tr>
</thead>
<tbody className="divide-y divide-gray-100">
{loading ? (
<tr>
<td colSpan={5} className="px-4 py-12 text-center text-gray-400">
<div className="flex justify-center">
<div className="animate-spin w-6 h-6 border-2 border-gray-300 border-t-gray-900 rounded-full" />
</div>
</td>
</tr>
) : items.length === 0 ? (
<tr>
<td colSpan={5} className="px-4 py-12 text-center text-gray-400">
</td>
</tr>
) : (
items.map((item) => (
<tr key={item.id} className="hover:bg-gray-50 transition-colors">
<td className="px-4 py-3">
<span className="text-sm font-medium text-gray-900">{item.title}</span>
</td>
<td className="px-4 py-3">
<code className="text-xs text-gray-500 bg-gray-100 px-1.5 py-0.5 rounded">
{item.slug || '-'}
</code>
</td>
<td className="px-4 py-3">
<span
className={`inline-flex text-xs px-2 py-0.5 rounded-full font-medium ${
item.status === 'published'
? 'bg-green-50 text-green-700'
: item.status === 'draft'
? 'bg-yellow-50 text-yellow-700'
: 'bg-gray-100 text-gray-600'
}`}
>
{item.status === 'published' ? '已发布' : item.status === 'draft' ? '草稿' : '已归档'}
</span>
</td>
<td className="px-4 py-3 text-sm text-gray-500">
{item.updatedAt ? new Date(item.updatedAt).toLocaleDateString('zh-CN') : '-'}
</td>
<td className="px-4 py-3">
<div className="flex items-center justify-end gap-2">
<button
onClick={() => router.push(`/admin/content/${modelCode}/${item.id}`)}
className="p-1.5 rounded hover:bg-gray-100 text-gray-400 hover:text-gray-600 transition-colors"
title="编辑"
>
<Edit className="w-4 h-4" />
</button>
<button
onClick={() => handleDelete(item.id)}
className="p-1.5 rounded hover:bg-red-50 text-gray-400 hover:text-red-500 transition-colors"
title="删除"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
{/* Pagination */}
{totalPages > 1 && (
<div className="flex items-center justify-between px-4 py-3 border-t border-gray-200">
<span className="text-sm text-gray-500">
{page} / {totalPages}
</span>
<div className="flex items-center gap-1">
<button
onClick={() => setPage((p) => Math.max(1, p - 1))}
disabled={page <= 1}
className="p-1.5 rounded hover:bg-gray-100 disabled:opacity-30 disabled:cursor-not-allowed"
>
<ChevronLeft className="w-4 h-4" />
</button>
<button
onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
disabled={page >= totalPages}
className="p-1.5 rounded hover:bg-gray-100 disabled:opacity-30 disabled:cursor-not-allowed"
>
<ChevronRight className="w-4 h-4" />
</button>
</div>
</div>
)}
</div>
</div>
);
}
+20
View File
@@ -0,0 +1,20 @@
'use client';
import { AuthProvider } from '@/components/admin/auth-context';
import { AdminLayout } from '@/components/admin/admin-layout';
import { usePathname } from 'next/navigation';
export default function AdminRootLayout({ children }: { children: React.ReactNode }) {
const pathname = usePathname();
const isLoginPage = pathname === '/admin/login';
return (
<AuthProvider>
{isLoginPage ? (
children
) : (
<AdminLayout>{children}</AdminLayout>
)}
</AuthProvider>
);
}
+104
View File
@@ -0,0 +1,104 @@
'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { useAuth } from '@/components/admin/auth-context';
import { Eye, EyeOff, LogIn } from 'lucide-react';
export default function AdminLoginPage() {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [showPassword, setShowPassword] = useState(false);
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const { login } = useAuth();
const router = useRouter();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
setLoading(true);
try {
await login(username, password);
router.push('/admin');
} catch (err) {
setError(err instanceof Error ? err.message : '登录失败');
} finally {
setLoading(false);
}
};
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-4">
<div className="w-full max-w-md">
<div className="text-center mb-8">
<h1 className="text-2xl font-bold text-gray-900">Novalon CMS</h1>
<p className="text-sm text-gray-500 mt-1"></p>
</div>
<form
onSubmit={handleSubmit}
className="bg-white rounded-xl shadow-sm border border-gray-200 p-6 space-y-5"
>
{error && (
<div className="bg-red-50 border border-red-200 text-red-700 rounded-lg px-4 py-3 text-sm">
{error}
</div>
)}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1.5">
</label>
<input
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
className="w-full px-3 py-2.5 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-gray-900 focus:border-transparent transition-shadow"
placeholder="请输入用户名"
required
autoFocus
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1.5">
</label>
<div className="relative">
<input
type={showPassword ? 'text' : 'password'}
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full px-3 py-2.5 pr-10 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-gray-900 focus:border-transparent transition-shadow"
placeholder="请输入密码"
required
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
>
{showPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
</button>
</div>
</div>
<button
type="submit"
disabled={loading}
className="w-full flex items-center justify-center gap-2 bg-gray-900 text-white rounded-lg py-2.5 text-sm font-medium hover:bg-gray-800 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
{loading ? (
<span className="animate-spin w-4 h-4 border-2 border-white border-t-transparent rounded-full" />
) : (
<LogIn className="w-4 h-4" />
)}
{loading ? '登录中...' : '登 录'}
</button>
</form>
</div>
</div>
);
}
+177
View File
@@ -0,0 +1,177 @@
'use client';
import { useEffect, useState, useRef } from 'react';
import { useRouter } from 'next/navigation';
import { useAuth } from '@/components/admin/auth-context';
import { adminApi } from '@/lib/admin-api';
import { Upload, Trash2, Copy, Check, Image, File } from 'lucide-react';
interface MediaItem {
id: string;
name: string;
url: string;
mimeType: string;
size: number;
createdAt: string;
}
export default function MediaPage() {
const { user, loading: authLoading } = useAuth();
const router = useRouter();
const [items, setItems] = useState<MediaItem[]>([]);
const [loading, setLoading] = useState(true);
const [uploading, setUploading] = useState(false);
const [copiedId, setCopiedId] = useState<string | null>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const fetchMedia = async () => {
try {
const res = await adminApi.getMedia({ pageSize: 100 });
const data = (res as { items: MediaItem[] });
setItems(data?.items || []);
} catch (err) {
console.error(err);
} finally {
setLoading(false);
}
};
useEffect(() => {
if (!authLoading && !user) {
router.push('/admin/login');
return;
}
if (user) fetchMedia();
}, [user, authLoading, router]);
const handleUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
setUploading(true);
try {
await adminApi.uploadMedia(file);
await fetchMedia();
} catch (err) {
alert(err instanceof Error ? err.message : '上传失败');
} finally {
setUploading(false);
if (fileInputRef.current) fileInputRef.current.value = '';
}
};
const handleDelete = async (id: string) => {
if (!confirm('确定要删除此文件吗?')) return;
try {
await adminApi.deleteMedia(id);
await fetchMedia();
} catch (err) {
alert(err instanceof Error ? err.message : '删除失败');
}
};
const handleCopyUrl = (url: string, id: string) => {
navigator.clipboard.writeText(url).then(() => {
setCopiedId(id);
setTimeout(() => setCopiedId(null), 2000);
});
};
const formatSize = (bytes: number) => {
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
};
const isImage = (mimeType: string) => mimeType.startsWith('image/');
if (authLoading || loading) {
return (
<div className="flex items-center justify-center h-64">
<div className="animate-spin w-8 h-8 border-2 border-gray-900 border-t-transparent rounded-full" />
</div>
);
}
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-xl font-bold text-gray-900"></h1>
<p className="text-sm text-gray-500 mt-1"> {items.length} </p>
</div>
<label className="flex items-center gap-2 bg-gray-900 text-white rounded-lg px-4 py-2 text-sm font-medium hover:bg-gray-800 cursor-pointer transition-colors">
<Upload className="w-4 h-4" />
{uploading ? '上传中...' : '上传文件'}
<input
ref={fileInputRef}
type="file"
onChange={handleUpload}
className="hidden"
accept="image/*,.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.zip"
/>
</label>
</div>
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-4">
{items.map((item) => (
<div
key={item.id}
className="bg-white rounded-lg border border-gray-200 overflow-hidden group hover:shadow-md transition-shadow"
>
{isImage(item.mimeType) ? (
<div className="aspect-square bg-gray-100 flex items-center justify-center overflow-hidden">
<img
src={item.url}
alt={item.name}
className="w-full h-full object-cover"
/>
</div>
) : (
<div className="aspect-square bg-gray-100 flex items-center justify-center">
<File className="w-8 h-8 text-gray-400" />
</div>
)}
<div className="p-2">
<p className="text-xs text-gray-700 truncate" title={item.name}>
{item.name}
</p>
<p className="text-xs text-gray-400 mt-0.5">{formatSize(item.size)}</p>
</div>
<div className="flex border-t border-gray-100">
<button
onClick={() => handleCopyUrl(item.url, item.id)}
className="flex-1 flex items-center justify-center gap-1 py-1.5 text-xs text-gray-500 hover:bg-gray-50 transition-colors"
title="复制链接"
>
{copiedId === item.id ? (
<Check className="w-3 h-3 text-green-500" />
) : (
<Copy className="w-3 h-3" />
)}
</button>
<button
onClick={() => handleDelete(item.id)}
className="flex-1 flex items-center justify-center gap-1 py-1.5 text-xs text-gray-500 hover:bg-red-50 hover:text-red-500 transition-colors"
title="删除"
>
<Trash2 className="w-3 h-3" />
</button>
</div>
</div>
))}
</div>
{items.length === 0 && !loading && (
<div className="text-center py-12 text-gray-400">
<Image className="w-12 h-12 mx-auto mb-3 opacity-30" />
<p></p>
</div>
)}
</div>
);
}
+93
View File
@@ -0,0 +1,93 @@
'use client';
import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import { useAuth } from '@/components/admin/auth-context';
import { adminApi } from '@/lib/admin-api';
import { FileText, Briefcase, Box, Layers, Image, BarChart3, Newspaper, Settings } from 'lucide-react';
const modelCards = [
{ code: 'news', name: '新闻资讯', icon: Newspaper, color: 'bg-blue-50 text-blue-600' },
{ code: 'case-study', name: '案例研究', icon: Briefcase, color: 'bg-amber-50 text-amber-600' },
{ code: 'service', name: '服务管理', icon: Settings, color: 'bg-green-50 text-green-600' },
{ code: 'product', name: '产品管理', icon: Box, color: 'bg-purple-50 text-purple-600' },
{ code: 'solution', name: '解决方案', icon: Layers, color: 'bg-teal-50 text-teal-600' },
{ code: 'hero-banner', name: 'Hero Banner', icon: Image, color: 'bg-rose-50 text-rose-600' },
{ code: 'stat-item', name: '数据指标', icon: BarChart3, color: 'bg-indigo-50 text-indigo-600' },
];
export default function AdminDashboardPage() {
const { user, loading } = useAuth();
const router = useRouter();
const [stats, setStats] = useState({ models: 0, items: 0, zones: 0 });
const [fetching, setFetching] = useState(true);
useEffect(() => {
if (!loading && !user) {
router.push('/admin/login');
return;
}
if (user) {
adminApi.getStats().then((s) => setStats(s)).catch(console.error).finally(() => setFetching(false));
}
}, [user, loading, router]);
if (loading || (!user && typeof window !== 'undefined')) {
return (
<div className="flex items-center justify-center h-64">
<div className="animate-spin w-8 h-8 border-2 border-gray-900 border-t-transparent rounded-full" />
</div>
);
}
return (
<div className="space-y-6">
<div>
<h1 className="text-xl font-bold text-gray-900"></h1>
<p className="text-sm text-gray-500 mt-1">{user?.nickname || user?.username}</p>
</div>
{/* Stats */}
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
{[
{ label: '内容模型', value: stats.models, icon: FileText },
{ label: '内容条目', value: stats.items, icon: Layers },
{ label: '页面区域', value: stats.zones, icon: Briefcase },
].map((stat) => (
<div key={stat.label} className="bg-white rounded-lg border border-gray-200 p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-500">{stat.label}</p>
<p className="text-2xl font-bold text-gray-900 mt-1">
{fetching ? '-' : stat.value}
</p>
</div>
<div className="w-10 h-10 rounded-lg bg-gray-100 flex items-center justify-center">
<stat.icon className="w-5 h-5 text-gray-600" />
</div>
</div>
</div>
))}
</div>
{/* Quick links */}
<div className="bg-white rounded-lg border border-gray-200 p-6">
<h2 className="text-sm font-semibold text-gray-900 mb-4"></h2>
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-3">
{modelCards.map((card) => (
<button
key={card.code}
onClick={() => router.push(`/admin/content/${card.code}`)}
className="flex flex-col items-center gap-2 p-4 rounded-lg border border-gray-200 hover:border-gray-300 hover:bg-gray-50 transition-colors text-center"
>
<div className={`w-10 h-10 rounded-lg ${card.color} flex items-center justify-center`}>
<card.icon className="w-5 h-5" />
</div>
<span className="text-xs font-medium text-gray-700">{card.name}</span>
</button>
))}
</div>
</div>
</div>
);
}
+152
View File
@@ -0,0 +1,152 @@
'use client';
import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import { useAuth } from '@/components/admin/auth-context';
import { adminApi } from '@/lib/admin-api';
import { Save } from 'lucide-react';
interface ZoneItem {
itemId: string;
sortOrder: number;
variant: string;
}
interface Zone {
id: string;
code: string;
name: string;
pageCode: string;
zoneKey: string;
allowedModels: string[];
items: ZoneItem[];
settings: Record<string, unknown>;
}
export default function ZonesPage() {
const { user, loading: authLoading } = useAuth();
const router = useRouter();
const [zones, setZones] = useState<Zone[]>([]);
const [loading, setLoading] = useState(true);
const [editingZone, setEditingZone] = useState<Zone | null>(null);
const [saving, setSaving] = useState(false);
useEffect(() => {
if (!authLoading && !user) {
router.push('/admin/login');
return;
}
if (user) {
adminApi.getZones().then((res) => {
setZones((res as Zone[]) || []);
}).catch(console.error).finally(() => setLoading(false));
}
}, [user, authLoading, router]);
const handleSave = async () => {
if (!editingZone) return;
setSaving(true);
try {
await adminApi.saveZone({
id: editingZone.id,
code: editingZone.code,
name: editingZone.name,
pageCode: editingZone.pageCode,
zoneKey: editingZone.zoneKey,
allowedModels: editingZone.allowedModels,
items: editingZone.items,
settings: editingZone.settings,
});
setEditingZone(null);
// 重新加载
const res = await adminApi.getZones();
setZones((res as Zone[]) || []);
} catch (err) {
alert(err instanceof Error ? err.message : '保存失败');
} finally {
setSaving(false);
}
};
if (authLoading || loading) {
return (
<div className="flex items-center justify-center h-64">
<div className="animate-spin w-8 h-8 border-2 border-gray-900 border-t-transparent rounded-full" />
</div>
);
}
return (
<div className="space-y-6">
<div>
<h1 className="text-xl font-bold text-gray-900"></h1>
<p className="text-sm text-gray-500 mt-1"></p>
</div>
<div className="grid gap-4">
{zones.map((zone) => (
<div key={zone.id} className="bg-white rounded-lg border border-gray-200 p-4">
<div className="flex items-center justify-between">
<div>
<h3 className="text-sm font-semibold text-gray-900">{zone.name}</h3>
<p className="text-xs text-gray-500 mt-0.5">
Code: {zone.code} | : {zone.pageCode || '-'} | : {zone.zoneKey || '-'}
</p>
<p className="text-xs text-gray-400 mt-0.5">
: {zone.allowedModels?.join(', ') || '-'} | : {zone.items?.length || 0}
</p>
</div>
<button
onClick={() => setEditingZone(editingZone?.id === zone.id ? null : zone)}
className="text-sm text-gray-600 hover:text-gray-900 px-3 py-1.5 rounded border border-gray-300 hover:border-gray-400 transition-colors"
>
{editingZone?.id === zone.id ? '取消编辑' : '编辑'}
</button>
</div>
{editingZone?.id === zone.id && (
<div className="mt-4 pt-4 border-t border-gray-100 space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-xs font-medium text-gray-700 mb-1"></label>
<input
type="text"
value={editingZone.name}
onChange={(e) => setEditingZone({ ...editingZone, name: e.target.value })}
className="w-full px-3 py-1.5 border border-gray-300 rounded text-sm"
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1"></label>
<input
type="text"
value={editingZone.pageCode}
onChange={(e) => setEditingZone({ ...editingZone, pageCode: e.target.value })}
className="w-full px-3 py-1.5 border border-gray-300 rounded text-sm"
/>
</div>
</div>
<div className="flex justify-end gap-2">
<button
onClick={() => setEditingZone(null)}
className="text-sm text-gray-600 px-4 py-1.5 rounded border border-gray-300 hover:bg-gray-50"
>
</button>
<button
onClick={handleSave}
disabled={saving}
className="flex items-center gap-1.5 text-sm bg-gray-900 text-white px-4 py-1.5 rounded hover:bg-gray-800 disabled:opacity-50"
>
<Save className="w-3.5 h-3.5" />
</button>
</div>
</div>
)}
</div>
))}
</div>
</div>
);
}
+213
View File
@@ -0,0 +1,213 @@
import { NextRequest } from 'next/server';
import { prisma } from '@/lib/db';
import { authenticateRequest } from '@/lib/auth';
import {
success,
unauthorized,
notFound,
validationError,
internalError,
} from '@/lib/api-response';
function parseItem(item: { data: string; [key: string]: unknown }) {
return { ...item, data: JSON.parse(item.data as string) };
}
// GET /api/admin/items - 获取内容列表
export async function GET(request: NextRequest) {
const payload = authenticateRequest(request);
if (!payload) return unauthorized();
try {
const { searchParams } = new URL(request.url);
const modelCode = searchParams.get('modelCode');
const status = searchParams.get('status');
const search = searchParams.get('search');
const page = parseInt(searchParams.get('page') || '1');
const pageSize = parseInt(searchParams.get('pageSize') || '10');
const where: Record<string, unknown> = {};
if (modelCode) where.modelCode = modelCode;
if (status) where.status = status;
if (search) {
where.OR = [
{ title: { contains: search } },
{ slug: { contains: search } },
];
}
const [items, total] = await Promise.all([
prisma.contentItem.findMany({
where,
orderBy: { sortOrder: 'asc' },
skip: (page - 1) * pageSize,
take: pageSize,
}),
prisma.contentItem.count({ where }),
]);
return success({
items: items.map(parseItem),
total,
page,
pageSize,
totalPages: Math.ceil(total / pageSize),
});
} catch (error) {
console.error('Get items error:', error);
return internalError();
}
}
// POST /api/admin/items - 创建内容
export async function POST(request: NextRequest) {
const payload = authenticateRequest(request);
if (!payload) return unauthorized();
try {
const body = await request.json();
const { modelId, modelCode, title, slug, data, status, sortOrder } = body;
if (!modelId || !modelCode || !title) {
return validationError('缺少必填字段:modelId, modelCode, title');
}
// 检查 slug 唯一性
if (slug) {
const existing = await prisma.contentItem.findFirst({
where: { modelCode, slug },
});
if (existing) {
return validationError(`slug "${slug}" 已存在`);
}
}
const item = await prisma.contentItem.create({
data: {
modelId,
modelCode,
title,
slug: slug || '',
status: status || 'draft',
data: JSON.stringify(data || {}),
sortOrder: sortOrder || 0,
createdBy: payload.username,
updatedBy: payload.username,
publishedAt: status === 'published' ? new Date() : null,
},
});
// 记录操作日志
await prisma.auditLog.create({
data: {
module: 'item',
targetId: item.id,
action: 'create',
operator: payload.username,
afterData: JSON.stringify(item),
},
});
return success(parseItem(item), 201);
} catch (error) {
console.error('Create item error:', error);
return internalError();
}
}
// PUT /api/admin/items/[id] - 更新内容
export async function PUT(request: NextRequest) {
const payload = authenticateRequest(request);
if (!payload) return unauthorized();
try {
const { searchParams } = new URL(request.url);
const id = searchParams.get('id');
if (!id) return validationError('缺少 ID');
const existing = await prisma.contentItem.findUnique({ where: { id } });
if (!existing) return notFound('内容不存在');
const body = await request.json();
const { title, slug, data, status, sortOrder } = body;
// 检查 slug 唯一性
if (slug && slug !== existing.slug) {
const dup = await prisma.contentItem.findFirst({
where: { modelCode: existing.modelCode, slug, id: { not: id } },
});
if (dup) {
return validationError(`slug "${slug}" 已存在`);
}
}
const updateData: Record<string, unknown> = {
updatedBy: payload.username,
updatedAt: new Date(),
};
if (title !== undefined) updateData.title = title;
if (slug !== undefined) updateData.slug = slug;
if (data !== undefined) updateData.data = JSON.stringify(data);
if (status !== undefined) {
updateData.status = status;
if (status === 'published' && existing.status !== 'published') {
updateData.publishedAt = new Date();
}
}
if (sortOrder !== undefined) updateData.sortOrder = sortOrder;
const item = await prisma.contentItem.update({
where: { id },
data: updateData,
});
// 记录操作日志
await prisma.auditLog.create({
data: {
module: 'item',
targetId: id,
action: 'update',
operator: payload.username,
beforeData: JSON.stringify(existing),
afterData: JSON.stringify(item),
},
});
return success(parseItem(item));
} catch (error) {
console.error('Update item error:', error);
return internalError();
}
}
// DELETE /api/admin/items/[id] - 删除内容
export async function DELETE(request: NextRequest) {
const payload = authenticateRequest(request);
if (!payload) return unauthorized();
try {
const { searchParams } = new URL(request.url);
const id = searchParams.get('id');
if (!id) return validationError('缺少 ID');
const existing = await prisma.contentItem.findUnique({ where: { id } });
if (!existing) return notFound('内容不存在');
await prisma.contentItem.delete({ where: { id } });
await prisma.auditLog.create({
data: {
module: 'item',
targetId: id,
action: 'delete',
operator: payload.username,
beforeData: JSON.stringify(existing),
},
});
return success({ message: '删除成功' });
} catch (error) {
console.error('Delete item error:', error);
return internalError();
}
}
+156
View File
@@ -0,0 +1,156 @@
import { NextRequest } from 'next/server';
import { prisma } from '@/lib/db';
import { authenticateRequest } from '@/lib/auth';
import {
success,
unauthorized,
notFound,
validationError,
internalError,
} from '@/lib/api-response';
// GET /api/admin/media - 获取媒体列表
export async function GET(request: NextRequest) {
const payload = authenticateRequest(request);
if (!payload) return unauthorized();
try {
const { searchParams } = new URL(request.url);
const page = parseInt(searchParams.get('page') || '1');
const pageSize = parseInt(searchParams.get('pageSize') || '20');
const mimeType = searchParams.get('mimeType');
const where: Record<string, unknown> = {};
if (mimeType) {
where.mimeType = { startsWith: mimeType };
}
const [items, total] = await Promise.all([
prisma.mediaAsset.findMany({
where,
orderBy: { createdAt: 'desc' },
skip: (page - 1) * pageSize,
take: pageSize,
}),
prisma.mediaAsset.count({ where }),
]);
return success({
items,
total,
page,
pageSize,
totalPages: Math.ceil(total / pageSize),
});
} catch (error) {
console.error('Get media error:', error);
return internalError();
}
}
// POST /api/admin/media - 上传媒体文件
export async function POST(request: NextRequest) {
const payload = authenticateRequest(request);
if (!payload) return unauthorized();
try {
const formData = await request.formData();
const file = formData.get('file') as File | null;
if (!file) {
return validationError('请选择文件');
}
// 文件大小限制 10MB
if (file.size > 10 * 1024 * 1024) {
return validationError('文件大小不能超过 10MB');
}
const bytes = await file.arrayBuffer();
const buffer = Buffer.from(bytes);
// 生成唯一文件名
const ext = file.name.split('.').pop() || '';
const fileName = `${Date.now()}-${Math.random().toString(36).slice(2)}.${ext}`;
const uploadDir = 'public/uploads';
const filePath = `${uploadDir}/${fileName}`;
// 写入文件
const fs = await import('fs/promises');
const path = await import('path');
const dirPath = path.join(process.cwd(), uploadDir);
await fs.mkdir(dirPath, { recursive: true });
await fs.writeFile(path.join(process.cwd(), filePath), buffer);
const asset = await prisma.mediaAsset.create({
data: {
name: file.name,
path: filePath,
url: `/uploads/${fileName}`,
mimeType: file.type || 'application/octet-stream',
size: file.size,
width: 0,
height: 0,
alt: file.name,
storageType: 'local',
createdBy: payload.username,
},
});
await prisma.auditLog.create({
data: {
module: 'media',
targetId: asset.id,
action: 'create',
operator: payload.username,
afterData: JSON.stringify(asset),
},
});
return success(asset, 201);
} catch (error) {
console.error('Upload media error:', error);
return internalError();
}
}
// DELETE /api/admin/media/[id] - 删除媒体文件
export async function DELETE(request: NextRequest) {
const payload = authenticateRequest(request);
if (!payload) return unauthorized();
try {
const { searchParams } = new URL(request.url);
const id = searchParams.get('id');
if (!id) return validationError('缺少 ID');
const existing = await prisma.mediaAsset.findUnique({ where: { id } });
if (!existing) return notFound('文件不存在');
// 删除物理文件
try {
const fs = await import('fs/promises');
const path = await import('path');
await fs.unlink(path.join(process.cwd(), existing.path));
} catch {
// 文件可能已被删除,忽略
}
await prisma.mediaAsset.delete({ where: { id } });
await prisma.auditLog.create({
data: {
module: 'media',
targetId: id,
action: 'delete',
operator: payload.username,
beforeData: JSON.stringify(existing),
},
});
return success({ message: '删除成功' });
} catch (error) {
console.error('Delete media error:', error);
return internalError();
}
}
+24
View File
@@ -0,0 +1,24 @@
import { NextRequest } from 'next/server';
import { prisma } from '@/lib/db';
import { authenticateRequest } from '@/lib/auth';
import { success, unauthorized, internalError } from '@/lib/api-response';
// GET /api/admin/models - 获取所有内容模型
export async function GET(request: NextRequest) {
const payload = authenticateRequest(request);
if (!payload) return unauthorized();
try {
const models = await prisma.contentModel.findMany({
orderBy: { sortOrder: 'asc' },
});
return success(models.map((m) => ({
...m,
fields: JSON.parse(m.fields),
})));
} catch (error) {
console.error('Get models error:', error);
return internalError();
}
}
+136
View File
@@ -0,0 +1,136 @@
import { NextRequest } from 'next/server';
import { prisma } from '@/lib/db';
import { authenticateRequest } from '@/lib/auth';
import {
success,
unauthorized,
notFound,
validationError,
internalError,
} from '@/lib/api-response';
// GET /api/admin/zones - 获取所有内容区域
export async function GET(request: NextRequest) {
const payload = authenticateRequest(request);
if (!payload) return unauthorized();
try {
const { searchParams } = new URL(request.url);
const pageCode = searchParams.get('pageCode');
const where = pageCode ? { pageCode } : {};
const zones = await prisma.contentZone.findMany({ where });
return success(zones.map((z) => ({
...z,
allowedModels: JSON.parse(z.allowedModels),
items: JSON.parse(z.items),
settings: JSON.parse(z.settings),
})));
} catch (error) {
console.error('Get zones error:', error);
return internalError();
}
}
// POST /api/admin/zones - 创建/更新内容区域
export async function POST(request: NextRequest) {
const payload = authenticateRequest(request);
if (!payload) return unauthorized();
try {
const body = await request.json();
const { id, code, name, description, pageCode, zoneKey, allowedModels, items, settings } = body;
if (!code) return validationError('缺少必填字段:code');
const data = {
code,
name: name || code,
description: description || '',
pageCode: pageCode || '',
zoneKey: zoneKey || '',
allowedModels: JSON.stringify(allowedModels || []),
items: JSON.stringify(items || []),
settings: JSON.stringify(settings || {}),
};
let zone;
if (id) {
const existing = await prisma.contentZone.findUnique({ where: { id } });
if (!existing) return notFound('区域不存在');
zone = await prisma.contentZone.update({ where: { id }, data });
await prisma.auditLog.create({
data: {
module: 'zone',
targetId: id,
action: 'update',
operator: payload.username,
beforeData: JSON.stringify(existing),
afterData: JSON.stringify(zone),
},
});
} else {
zone = await prisma.contentZone.create({ data });
await prisma.auditLog.create({
data: {
module: 'zone',
targetId: zone.id,
action: 'create',
operator: payload.username,
afterData: JSON.stringify(zone),
},
});
}
return success({
...zone,
allowedModels: JSON.parse(zone.allowedModels),
items: JSON.parse(zone.items),
settings: JSON.parse(zone.settings),
});
} catch (error) {
console.error('Save zone error:', error);
return internalError();
}
}
// PUT /api/admin/zones - 更新区域设置
export async function PUT(request: NextRequest) {
return POST(request);
}
// DELETE /api/admin/zones/[id] - 删除内容区域
export async function DELETE(request: NextRequest) {
const payload = authenticateRequest(request);
if (!payload) return unauthorized();
try {
const { searchParams } = new URL(request.url);
const id = searchParams.get('id');
if (!id) return validationError('缺少 ID');
const existing = await prisma.contentZone.findUnique({ where: { id } });
if (!existing) return notFound('区域不存在');
await prisma.contentZone.delete({ where: { id } });
await prisma.auditLog.create({
data: {
module: 'zone',
targetId: id,
action: 'delete',
operator: payload.username,
beforeData: JSON.stringify(existing),
},
});
return success({ message: '删除成功' });
} catch (error) {
console.error('Delete zone error:', error);
return internalError();
}
}
+54
View File
@@ -0,0 +1,54 @@
import { NextRequest } from 'next/server';
import { prisma } from '@/lib/db';
import { verifyPassword, generateTokens, setTokenCookie } from '@/lib/auth';
import { success, unauthorized, validationError, internalError } from '@/lib/api-response';
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { username, password } = body;
if (!username || !password) {
return validationError('请输入用户名和密码');
}
const user = await prisma.user.findUnique({ where: { username } });
if (!user) {
return unauthorized('用户名或密码错误');
}
if (user.status === 0) {
return unauthorized('账号已被禁用,请联系管理员');
}
const isValid = await verifyPassword(password, user.password);
if (!isValid) {
return unauthorized('用户名或密码错误');
}
const { accessToken, refreshToken } = generateTokens({
userId: user.id,
username: user.username,
role: user.role,
});
const response = success({
user: {
id: user.id,
username: user.username,
nickname: user.nickname,
role: user.role,
avatar: user.avatar,
},
accessToken,
refreshToken,
});
setTokenCookie(response, accessToken, refreshToken);
return response;
} catch (error) {
console.error('Login error:', error);
return internalError();
}
}
+8
View File
@@ -0,0 +1,8 @@
import { clearTokenCookie } from '@/lib/auth';
import { success } from '@/lib/api-response';
export async function POST() {
const response = success({ message: '已登出' });
clearTokenCookie(response);
return response;
}
+21
View File
@@ -0,0 +1,21 @@
import { NextRequest } from 'next/server';
import { authenticateRequest } from '@/lib/auth';
import { success, unauthorized, internalError } from '@/lib/api-response';
export async function GET(request: NextRequest) {
try {
const payload = authenticateRequest(request);
if (!payload) {
return unauthorized();
}
return success({
userId: payload.userId,
username: payload.username,
role: payload.role,
});
} catch (error) {
console.error('Me error:', error);
return internalError();
}
}
+39
View File
@@ -0,0 +1,39 @@
import { NextRequest } from 'next/server';
import { verifyRefreshToken, generateTokens, setTokenCookie } from '@/lib/auth';
import { success, unauthorized, internalError } from '@/lib/api-response';
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { refreshToken } = body;
if (!refreshToken) {
return unauthorized('缺少刷新令牌');
}
let payload;
try {
payload = verifyRefreshToken(refreshToken);
} catch {
return unauthorized('刷新令牌无效或已过期');
}
const tokens = generateTokens({
userId: payload.userId,
username: payload.username,
role: payload.role,
});
const response = success({
accessToken: tokens.accessToken,
refreshToken: tokens.refreshToken,
});
setTokenCookie(response, tokens.accessToken, tokens.refreshToken);
return response;
} catch (error) {
console.error('Refresh error:', error);
return internalError();
}
}
+31
View File
@@ -0,0 +1,31 @@
import { NextRequest, NextResponse } from 'next/server';
import { draftMode } from 'next/headers';
// 静态导出(output: 'export')不支持 GET + searchParams 的 API 路由。
// 使用 POST 避免 Next.js 在构建时尝试预渲染。未来迁移到真实后端时可改回 GET。
export async function POST(request: NextRequest) {
try {
const body = await request.json().catch(() => ({}));
const redirect = typeof body.redirect === 'string' ? body.redirect : null;
draftMode().disable();
if (redirect) {
return NextResponse.redirect(new URL(redirect, request.url));
}
return NextResponse.json({
code: 0,
message: 'Draft mode disabled',
data: {
enabled: false,
timestamp: new Date().toISOString(),
},
});
} catch {
return NextResponse.json(
{ code: 500, message: 'Internal server error', data: null },
{ status: 500 }
);
}
}
+47
View File
@@ -0,0 +1,47 @@
import { NextRequest, NextResponse } from 'next/server';
import { draftMode } from 'next/headers';
// 静态导出(output: 'export')不支持 GET + searchParams 的 API 路由。
// 使用 POST 避免 Next.js 在构建时尝试预渲染。未来迁移到真实后端时可改回 GET。
export async function POST(request: NextRequest) {
try {
const body = await request.json().catch(() => ({}));
const secret = typeof body.secret === 'string' ? body.secret : null;
const slug = typeof body.slug === 'string' ? body.slug : null;
const modelCode = typeof body.modelCode === 'string' ? body.modelCode : null;
const redirect = typeof body.redirect === 'string' ? body.redirect : null;
const expectedSecret = process.env.CMS_PREVIEW_SECRET;
if (expectedSecret && secret !== expectedSecret) {
return NextResponse.json(
{ code: 401, message: 'Invalid secret token', data: null },
{ status: 401 }
);
}
draftMode().enable();
if (redirect) {
return NextResponse.redirect(new URL(redirect, request.url));
}
if (modelCode && slug) {
const redirectPath = `/cases/${slug}?preview=true`;
return NextResponse.redirect(new URL(redirectPath, request.url));
}
return NextResponse.json({
code: 0,
message: 'Draft mode enabled',
data: {
enabled: true,
timestamp: new Date().toISOString(),
},
});
} catch {
return NextResponse.json(
{ code: 500, message: 'Internal server error', data: null },
{ status: 500 }
);
}
}
+106
View File
@@ -0,0 +1,106 @@
import { NextRequest, NextResponse } from 'next/server';
import { revalidatePath, revalidateTag } from 'next/cache';
import {
getContentTypeConfig,
getAllContentTypeConfigs,
} from '@/lib/cms/registry';
interface RevalidateRequestBody {
event?: string;
modelCode?: string;
itemId?: string;
slug?: string;
paths?: string[];
tags?: string[];
secret?: string;
}
export async function POST(request: NextRequest) {
try {
const body = (await request.json()) as RevalidateRequestBody;
const secret = body.secret ?? request.headers.get('x-revalidate-secret');
const expectedSecret = process.env.CMS_REVALIDATE_SECRET;
if (expectedSecret && secret !== expectedSecret) {
return NextResponse.json(
{ code: 401, message: 'Invalid secret', data: null },
{ status: 401 }
);
}
const revalidatedPaths: string[] = [];
const revalidatedTags: string[] = [];
if (body.paths && body.paths.length > 0) {
for (const path of body.paths) {
revalidatePath(path);
revalidatedPaths.push(path);
}
}
if (body.tags && body.tags.length > 0) {
for (const tag of body.tags) {
revalidateTag(tag);
revalidatedTags.push(tag);
}
}
if (body.modelCode) {
const config = getContentTypeConfig(body.modelCode);
if (config) {
if (config.listPage?.route) {
revalidatePath(config.listPage.route);
revalidatedPaths.push(config.listPage.route);
}
if (config.detailPage?.routePattern && body.slug) {
const detailPath = config.detailPage.routePattern.replace('{slug}', body.slug);
revalidatePath(detailPath);
revalidatedPaths.push(detailPath);
}
const tag = `cms:${body.modelCode}`;
revalidateTag(tag);
revalidatedTags.push(tag);
}
}
if (body.event === 'content.published' || body.event === 'content.updated') {
const allConfigs = getAllContentTypeConfigs();
for (const config of allConfigs) {
if (config.listPage?.route) {
revalidatePath(config.listPage.route);
if (!revalidatedPaths.includes(config.listPage.route)) {
revalidatedPaths.push(config.listPage.route);
}
}
}
revalidateTag('cms:all');
revalidatedTags.push('cms:all');
}
return NextResponse.json({
code: 0,
message: 'success',
data: {
revalidatedPaths,
revalidatedTags,
timestamp: new Date().toISOString(),
},
});
} catch (error) {
console.error('Revalidation error:', error);
return NextResponse.json(
{
code: 500,
message: error instanceof Error ? error.message : 'Internal server error',
data: null,
},
{ status: 500 }
);
}
}
// 注:静态导出(output: 'export')不支持 GET + searchParams 的 API 路由。
// GET 方法已移除,使用 POST 触发 revalidation。未来迁移到真实后端时可按需恢复。
+223
View File
@@ -0,0 +1,223 @@
'use client';
import { useState } from 'react';
import Link from 'next/link';
import { usePathname, useRouter } from 'next/navigation';
import { useAuth } from './auth-context';
import {
LayoutDashboard,
FileText,
Briefcase,
Box,
Layers,
Image,
BarChart3,
Newspaper,
LogOut,
Menu,
X,
ChevronDown,
Settings,
Users,
Phone,
Shield,
} from 'lucide-react';
const menuItems = [
{
label: '内容管理',
icon: FileText,
children: [
{ label: '新闻资讯', href: '/admin/content/news', icon: Newspaper },
{ label: '案例研究', href: '/admin/content/case-study', icon: Briefcase },
{ label: '服务管理', href: '/admin/content/service', icon: Settings },
{ label: '产品管理', href: '/admin/content/product', icon: Box },
{ label: '解决方案', href: '/admin/content/solution', icon: Layers },
{ label: 'Hero Banner', href: '/admin/content/hero-banner', icon: Image },
{ label: '数据指标', href: '/admin/content/stat-item', icon: BarChart3 },
],
},
{
label: '页面管理',
icon: FileText,
children: [
{ label: '关于我们', href: '/admin/content/about-page', icon: Users },
{ label: '团队介绍', href: '/admin/content/team-page', icon: Users },
{ label: '联系我们', href: '/admin/content/contact-page', icon: Phone },
{ label: '法律页面', href: '/admin/content/legal-page', icon: Shield },
],
},
{
label: '页面配置',
icon: Layers,
children: [
{ label: '首页区域', href: '/admin/zones', icon: LayoutDashboard },
],
},
{
label: '资源管理',
icon: Image,
children: [
{ label: '媒体库', href: '/admin/media', icon: Image },
],
},
];
export function AdminLayout({ children }: { children: React.ReactNode }) {
const [sidebarOpen, setSidebarOpen] = useState(false);
const [expandedMenus, setExpandedMenus] = useState<Set<string>>(new Set(['内容管理']));
const { user, logout } = useAuth();
const pathname = usePathname();
const router = useRouter();
const toggleMenu = (label: string) => {
setExpandedMenus((prev) => {
const next = new Set(prev);
if (next.has(label)) next.delete(label);
else next.add(label);
return next;
});
};
const handleLogout = () => {
logout();
router.push('/admin/login');
};
const isActive = (href: string) => pathname === href || pathname?.startsWith(href + '/');
return (
<div className="min-h-screen bg-gray-50">
{/* Mobile overlay */}
{sidebarOpen && (
<div
className="fixed inset-0 bg-black/50 z-40 lg:hidden"
onClick={() => setSidebarOpen(false)}
/>
)}
{/* Sidebar */}
<aside
className={`fixed top-0 left-0 z-50 h-full w-64 bg-white border-r border-gray-200 transform transition-transform duration-200 ease-in-out lg:translate-x-0 ${
sidebarOpen ? 'translate-x-0' : '-translate-x-full'
}`}
>
<div className="flex items-center justify-between h-16 px-4 border-b border-gray-200">
<Link href="/admin" className="flex items-center gap-2">
<span className="text-lg font-bold text-gray-900">Novalon CMS</span>
</Link>
<button
onClick={() => setSidebarOpen(false)}
className="lg:hidden p-1 rounded hover:bg-gray-100"
>
<X className="w-5 h-5" />
</button>
</div>
<nav className="flex-1 overflow-y-auto p-3 space-y-1">
{/* Dashboard */}
<Link
href="/admin"
className={`flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transition-colors ${
pathname === '/admin'
? 'bg-gray-900 text-white'
: 'text-gray-600 hover:bg-gray-100'
}`}
>
<LayoutDashboard className="w-4 h-4" />
</Link>
{/* Menu groups */}
{menuItems.map((group) => (
<div key={group.label}>
<button
onClick={() => toggleMenu(group.label)}
className="flex items-center justify-between w-full px-3 py-2.5 rounded-lg text-sm font-medium text-gray-600 hover:bg-gray-100 transition-colors"
>
<span className="flex items-center gap-3">
<group.icon className="w-4 h-4" />
{group.label}
</span>
<ChevronDown
className={`w-4 h-4 transition-transform ${
expandedMenus.has(group.label) ? 'rotate-180' : ''
}`}
/>
</button>
{expandedMenus.has(group.label) && (
<div className="ml-4 mt-1 space-y-1">
{group.children.map((item) => (
<Link
key={item.href}
href={item.href}
className={`flex items-center gap-3 px-3 py-2 rounded-lg text-sm transition-colors ${
isActive(item.href)
? 'bg-gray-900 text-white'
: 'text-gray-500 hover:bg-gray-100'
}`}
>
<item.icon className="w-3.5 h-3.5" />
{item.label}
</Link>
))}
</div>
)}
</div>
))}
</nav>
{/* User info */}
<div className="border-t border-gray-200 p-3">
<div className="flex items-center gap-3 px-3 py-2">
<div className="w-8 h-8 rounded-full bg-gray-900 text-white flex items-center justify-center text-sm font-medium">
{user?.nickname?.charAt(0) || 'A'}
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-gray-900 truncate">
{user?.nickname || user?.username}
</p>
<p className="text-xs text-gray-500">{user?.role === 'admin' ? '管理员' : '编辑'}</p>
</div>
<button
onClick={handleLogout}
className="p-1.5 rounded hover:bg-gray-100 text-gray-400 hover:text-red-500 transition-colors"
title="退出登录"
>
<LogOut className="w-4 h-4" />
</button>
</div>
</div>
</aside>
{/* Main content */}
<div className="lg:pl-64">
{/* Top bar */}
<header className="sticky top-0 z-30 h-16 bg-white border-b border-gray-200 flex items-center justify-between px-4 lg:px-6">
<button
onClick={() => setSidebarOpen(true)}
className="lg:hidden p-2 rounded hover:bg-gray-100"
>
<Menu className="w-5 h-5" />
</button>
<div className="flex-1" />
<div className="flex items-center gap-4">
<Link
href="/"
target="_blank"
className="text-sm text-gray-500 hover:text-gray-700 transition-colors"
>
</Link>
</div>
</header>
{/* Page content */}
<main className="p-4 lg:p-6">{children}</main>
</div>
</div>
);
}
+87
View File
@@ -0,0 +1,87 @@
'use client';
import { createContext, useContext, useState, useEffect, useCallback, type ReactNode } from 'react';
import { useRouter } from 'next/navigation';
interface AuthUser {
id: string;
username: string;
nickname: string;
role: string;
avatar: string;
}
interface AuthContextType {
user: AuthUser | null;
token: string | null;
loading: boolean;
login: (username: string, password: string) => Promise<void>;
logout: () => void;
}
const AuthContext = createContext<AuthContextType>({
user: null,
token: null,
loading: true,
login: async () => {},
logout: () => {},
});
export function useAuth() {
return useContext(AuthContext);
}
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<AuthUser | null>(null);
const [token, setToken] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const router = useRouter();
useEffect(() => {
const savedToken = localStorage.getItem('novalon_admin_token');
const savedUser = localStorage.getItem('novalon_admin_user');
if (savedToken && savedUser) {
try {
setToken(savedToken);
setUser(JSON.parse(savedUser));
} catch {
localStorage.removeItem('novalon_admin_token');
localStorage.removeItem('novalon_admin_user');
}
}
setLoading(false);
}, []);
const login = useCallback(async (username: string, password: string) => {
const res = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password }),
});
if (!res.ok) {
const data = await res.json();
throw new Error(data.error || '登录失败');
}
const data = await res.json();
setToken(data.accessToken);
setUser(data.user);
localStorage.setItem('novalon_admin_token', data.accessToken);
localStorage.setItem('novalon_admin_user', JSON.stringify(data.user));
}, []);
const logout = useCallback(() => {
setToken(null);
setUser(null);
localStorage.removeItem('novalon_admin_token');
localStorage.removeItem('novalon_admin_user');
router.push('/admin/login');
}, [router]);
return (
<AuthContext.Provider value={{ user, token, loading, login, logout }}>
{children}
</AuthContext.Provider>
);
}
+473
View File
@@ -0,0 +1,473 @@
'use client';
import { useState, useMemo, useEffect, useCallback, createElement } from 'react';
import {
getAllContentModels,
getItemRenderer,
ContentZoneRenderer,
getContentItems,
saveContentItem,
getContentZone,
} from '@/lib/cms';
import type { ContentModel, ContentItem, FieldDefinition } from '@/lib/cms';
import { RichTextEditor } from './RichTextEditor';
const PREVIEW_ZONES: { key: string; name: string }[] = [
{ key: 'home-stats', name: '首页 · 数据指标区' },
{ key: 'home-services', name: '首页 · 核心服务区' },
{ key: 'home-solutions', name: '首页 · 解决方案区' },
{ key: 'home-cases', name: '首页 · 客户案例区' },
{ key: 'home-news', name: '首页 · 新闻动态区' },
];
function FieldEditor({ field, value, onChange }: {
field: FieldDefinition;
value: unknown;
onChange: (val: unknown) => void;
}) {
const baseClass = 'w-full px-3 py-2 bg-white/[0.05] border border-white/[0.1] rounded-lg text-white text-sm placeholder-white/30 focus:outline-none focus:border-brand/50 focus:ring-1 focus:ring-brand/30 transition-colors';
switch (field.type) {
case 'text':
return (
<input
type="text"
className={baseClass}
value={(value as string) || ''}
placeholder={field.label}
onChange={(e) => onChange(e.target.value)}
/>
);
case 'textarea':
return (
<textarea
className={`${baseClass} min-h-[100px] resize-y`}
value={(value as string) || ''}
placeholder={field.label}
onChange={(e) => onChange(e.target.value)}
/>
);
case 'richtext':
return (
<RichTextEditor
value={(value as string) || ''}
onChange={onChange}
placeholder={field.label}
/>
);
case 'number':
return (
<input
type="number"
className={baseClass}
value={(value as number) ?? ''}
placeholder={field.label}
onChange={(e) => onChange(e.target.value ? Number(e.target.value) : 0)}
/>
);
case 'boolean':
return (
<label className="inline-flex items-center gap-3 cursor-pointer">
<div
className={`relative w-11 h-6 rounded-full transition-colors ${value ? 'bg-brand' : 'bg-white/10'}`}
onClick={() => onChange(!value)}
>
<div
className={`absolute top-1 w-4 h-4 rounded-full bg-white transition-transform ${value ? 'translate-x-6' : 'translate-x-1'}`}
/>
</div>
<span className="text-sm text-white/70">{field.label}</span>
</label>
);
case 'date':
return (
<input
type="date"
className={baseClass}
value={(value as string) || ''}
onChange={(e) => onChange(e.target.value)}
/>
);
case 'select':
case 'dropdown':
return (
<select
className={baseClass}
value={String(value ?? '')}
onChange={(e) => {
const opt = field.options?.find((o) => String(o.value) === e.target.value);
onChange(opt ? opt.value : e.target.value);
}}
>
<option value="">...</option>
{field.options?.map((opt) => (
<option key={String(opt.value)} value={String(opt.value)}>
{opt.label}
</option>
))}
</select>
);
case 'image': {
const imgUrl = typeof value === 'string' ? value : '';
return (
<div className="space-y-2">
{imgUrl && (
<div className="aspect-video rounded-lg overflow-hidden bg-white/5 border border-white/10">
<img src={imgUrl} alt="" className="w-full h-full object-cover" />
</div>
)}
<input
type="text"
className={baseClass}
value={imgUrl}
placeholder="图片 URL"
onChange={(e) => onChange(e.target.value)}
/>
</div>
);
}
case 'array':
return (
<div className="space-y-2">
{(Array.isArray(value) ? value : []).map((item: unknown, idx: number) => (
<div key={idx} className="flex gap-2">
<input
type="text"
className={baseClass}
value={typeof item === 'string' ? item : item && typeof item === 'object' ? String((item as Record<string, unknown>).name || (item as Record<string, unknown>).text || '') : ''}
placeholder={`${idx + 1}`}
onChange={(e) => {
const newArr = [...(Array.isArray(value) ? value : [])];
if (field.fields?.[0]?.name) {
newArr[idx] = { ...(item as object), [field.fields[0].name]: e.target.value };
} else {
newArr[idx] = e.target.value;
}
onChange(newArr);
}}
/>
<button
onClick={() => {
const newArr = [...(Array.isArray(value) ? value : [])];
newArr.splice(idx, 1);
onChange(newArr);
}}
className="px-3 text-red-400 hover:text-red-300 text-sm"
>
×
</button>
</div>
))}
<button
onClick={() => {
const newItem = field.fields?.[0]?.name ? { [field.fields[0].name]: '' } : '';
onChange([...(Array.isArray(value) ? value : []), newItem]);
}}
className="text-sm text-brand hover:text-brand/80 font-medium"
>
+
</button>
</div>
);
default:
return (
<input
type="text"
className={baseClass}
value={(value as string) || ''}
placeholder={field.label}
onChange={(e) => onChange(e.target.value)}
/>
);
}
}
export default function ContentEditor() {
const [models, setModels] = useState<ContentModel[]>([]);
const [selectedModelCode, setSelectedModelCode] = useState<string>('case-study');
const [items, setItems] = useState<ContentItem[]>([]);
const [selectedItem, setSelectedItem] = useState<ContentItem | null>(null);
const [editData, setEditData] = useState<Record<string, unknown>>({});
const [previewZoneKey, setPreviewZoneKey] = useState('home-cases');
const loadItems = useCallback(() => {
const list = getContentItems(selectedModelCode);
setItems(list);
if (list.length > 0) {
const current = list.find((i) => i.id === selectedItem?.id);
if (current) {
setSelectedItem(current);
} else if (!selectedItem) {
setSelectedItem(list[0] || null);
setEditData({ ...(list[0]?.data || {}) });
}
} else {
setSelectedItem(null);
setEditData({});
}
}, [selectedModelCode, selectedItem]);
useEffect(() => {
setModels(getAllContentModels());
}, []);
useEffect(() => {
loadItems();
const handleCmsUpdate = () => loadItems();
window.addEventListener('cms-updated', handleCmsUpdate);
return () => window.removeEventListener('cms-updated', handleCmsUpdate);
}, [loadItems]);
const selectedModel = useMemo(
() => models.find((m) => m.code === selectedModelCode),
[models, selectedModelCode]
);
const handleFieldChange = (fieldName: string, value: unknown) => {
const newData = { ...editData, [fieldName]: value };
setEditData(newData);
if (selectedItem) {
const updated = { ...selectedItem, data: newData, title: fieldName === 'title' ? (value as string) : selectedItem.title };
setSelectedItem(updated);
}
};
const handleSave = () => {
if (!selectedItem) return;
const saved = { ...selectedItem, data: editData };
saveContentItem(selectedItem.modelCode, saved);
};
const handleSelectItem = (item: ContentItem) => {
setSelectedItem(item);
setEditData({ ...item.data });
};
const PreviewRenderer = useMemo(
() => (selectedItem ? getItemRenderer(selectedItem.modelCode) : null),
[selectedItem]
);
const previewZone = useMemo(() => {
const zoneData = getContentZone(previewZoneKey);
if (!zoneData || !selectedItem) return null;
const updatedItems = zoneData.items.map((zi) => {
if (zi.item?.id === selectedItem.id) {
return { ...zi, item: { ...selectedItem, data: editData } };
}
return zi;
});
return { ...zoneData, items: updatedItems };
}, [previewZoneKey, selectedItem, editData]);
return (
<div className="h-full flex">
<div className="w-80 flex-shrink-0 border-r border-white/[0.08] overflow-y-auto">
<div className="p-4 border-b border-white/[0.08]">
<label className="block text-xs font-medium text-white/60 mb-2"></label>
<select
className="w-full px-3 py-2 bg-white/[0.05] border border-white/[0.1] rounded-lg text-white text-sm focus:outline-none focus:border-brand/50"
value={selectedModelCode}
onChange={(e) => {
setSelectedModelCode(e.target.value);
setSelectedItem(null);
}}
>
{models.map((m) => (
<option key={m.code} value={m.code}>
{m.name}
</option>
))}
</select>
</div>
<div className="p-4">
<div className="flex items-center justify-between mb-3">
<span className="text-xs text-white/50 font-medium"></span>
<button className="text-xs text-brand hover:text-brand/80 font-medium">
+
</button>
</div>
<div className="space-y-2">
{items.map((item) => (
<div
key={item.id}
onClick={() => handleSelectItem(item)}
className={`p-3 rounded-lg cursor-pointer transition-all border ${
selectedItem?.id === item.id
? 'bg-brand/10 border-brand/30'
: 'bg-white/[0.02] border-white/[0.06] hover:bg-white/[0.05] hover:border-white/[0.1]'
}`}
>
<div className="text-sm font-medium text-white line-clamp-1">
{item.title}
</div>
<div className="mt-1 text-xs text-white/40 flex items-center gap-2">
<span>{item.slug}</span>
<span className="w-1 h-1 rounded-full bg-emerald-400" />
<span className="text-emerald-400"></span>
</div>
</div>
))}
</div>
</div>
</div>
<div className="flex-1 flex min-h-0">
<div className="flex-1 overflow-y-auto p-6">
{selectedItem ? (
<div className="max-w-2xl">
<div className="mb-6 flex items-center justify-between">
<div>
<h2 className="text-lg font-semibold text-white"></h2>
<p className="mt-1 text-sm text-white/40"></p>
</div>
<button
onClick={handleSave}
className="px-4 py-2 bg-brand hover:bg-brand-hover text-white text-sm font-medium rounded-lg transition-colors"
>
💾
</button>
</div>
<div className="space-y-5">
<div>
<label className="block text-sm font-medium text-white/80 mb-2">
</label>
<input
type="text"
className="w-full px-3 py-2 bg-white/[0.05] border border-white/[0.1] rounded-lg text-white text-sm focus:outline-none focus:border-brand/50 focus:ring-1 focus:ring-brand/30 transition-colors"
value={selectedItem.title}
onChange={(e) =>
setSelectedItem({ ...selectedItem, title: e.target.value })
}
/>
</div>
<div>
<label className="block text-sm font-medium text-white/80 mb-2">
SlugURL
</label>
<input
type="text"
className="w-full px-3 py-2 bg-white/[0.05] border border-white/[0.1] rounded-lg text-white text-sm font-mono focus:outline-none focus:border-brand/50 focus:ring-1 focus:ring-brand/30 transition-colors"
value={selectedItem.slug}
onChange={(e) =>
setSelectedItem({ ...selectedItem, slug: e.target.value })
}
/>
</div>
<div className="border-t border-white/[0.08] pt-5">
<h3 className="text-sm font-semibold text-white/80 mb-4"></h3>
<div className="space-y-5">
{selectedModel?.fields?.map((field) => (
<div key={field.name}>
<label className="block text-sm font-medium text-white/80 mb-2">
{field.label}
{field.required && (
<span className="text-red-400 ml-1">*</span>
)}
</label>
<FieldEditor
field={field}
value={editData[field.name]}
onChange={(val) => handleFieldChange(field.name, val)}
/>
{field.description && (
<p className="mt-1.5 text-xs text-white/35">
{field.description}
</p>
)}
</div>
))}
{!selectedModel?.fields?.length && (
<div className="text-sm text-white/40 py-8 text-center">
</div>
)}
</div>
</div>
<div className="border-t border-white/[0.08] pt-5">
<h3 className="text-sm font-semibold text-white/80 mb-4"></h3>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-xs font-medium text-white/60 mb-2"></label>
<select
value={selectedItem.status}
onChange={(e) =>
setSelectedItem({
...selectedItem,
status: e.target.value as ContentItem['status'],
})
}
className="w-full px-3 py-2 bg-white/[0.05] border border-white/[0.1] rounded-lg text-white text-sm focus:outline-none focus:border-brand/50"
>
<option value="published"></option>
<option value="draft">稿</option>
<option value="archived"></option>
</select>
</div>
<div>
<label className="block text-xs font-medium text-white/60 mb-2"></label>
<input
type="number"
className="w-full px-3 py-2 bg-white/[0.05] border border-white/[0.1] rounded-lg text-white text-sm focus:outline-none focus:border-brand/50"
value={selectedItem.sortOrder ?? 0}
onChange={(e) =>
setSelectedItem({
...selectedItem,
sortOrder: Number(e.target.value),
})
}
/>
</div>
</div>
</div>
</div>
</div>
) : (
<div className="h-full flex items-center justify-center">
<div className="text-center">
<div className="text-5xl mb-4 opacity-30">📝</div>
<div className="text-white/50"></div>
</div>
</div>
)}
</div>
<div className="w-[420px] flex-shrink-0 border-l border-white/[0.08] overflow-y-auto bg-ink">
<div className="p-4 border-b border-white/[0.08] bg-ink-light flex items-center justify-between">
<span className="text-xs font-medium text-white/60"></span>
<select
className="px-2 py-1 bg-white/[0.05] border border-white/[0.1] rounded-md text-white text-xs focus:outline-none"
value={previewZoneKey}
onChange={(e) => setPreviewZoneKey(e.target.value)}
>
<option value="single"></option>
{PREVIEW_ZONES.map((z) => (
<option key={z.key} value={z.key}>
{z.name}
</option>
))}
</select>
</div>
<div className="p-4">
{previewZoneKey === 'single' && selectedItem && PreviewRenderer
? createElement(PreviewRenderer, { item: { ...selectedItem, data: editData } })
: previewZone ? (
<ContentZoneRenderer zone={previewZone} />
) : (
<div className="text-sm text-white/40 py-8 text-center">
</div>
)}
</div>
</div>
</div>
</div>
);
}
+36
View File
@@ -0,0 +1,36 @@
'use client';
import * as React from 'react';
import type { ContentItem } from '@/lib/cms';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const componentRegistry = new Map<string, React.ComponentType<any>>();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function registerComponent(modelCode: string, Component: React.ComponentType<any>) {
componentRegistry.set(modelCode, Component);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function getRegisteredComponent(modelCode: string): React.ComponentType<any> | undefined {
return componentRegistry.get(modelCode);
}
export interface ContentRendererProps {
item: ContentItem;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
component?: React.ComponentType<any>;
fallback?: React.ReactNode;
}
export function ContentRenderer({ item, component: ComponentProp, fallback = null }: ContentRendererProps) {
const Component = React.useMemo(() => {
return ComponentProp || getRegisteredComponent(item.modelCode);
}, [ComponentProp, item.modelCode]);
if (!Component) {
return <>{fallback}</>;
}
return React.createElement(Component, { item, data: item.data });
}
+207
View File
@@ -0,0 +1,207 @@
'use client';
import * as React from 'react';
import type { FieldDefinition, FieldType } from '@/lib/cms';
export interface FieldRendererProps {
field: FieldDefinition;
value: unknown;
className?: string;
}
export function FieldRenderer({ field, value, className }: FieldRendererProps) {
if (value === undefined || value === null) {
return null;
}
const renderByType = (type: FieldType, val: unknown, fieldDef: FieldDefinition): React.ReactNode => {
switch (type) {
case 'text':
case 'textarea':
return <span className={className}>{String(val)}</span>;
case 'richtext':
return (
<div
className={className}
dangerouslySetInnerHTML={{ __html: String(val) }}
/>
);
case 'number':
return <span className={className}>{formatNumber(val)}</span>;
case 'boolean':
return <span className={className}>{val ? '是' : '否'}</span>;
case 'date':
return <span className={className}>{formatDate(val, 'date')}</span>;
case 'datetime':
return <span className={className}>{formatDate(val, 'datetime')}</span>;
case 'image':
case 'media':
return renderMedia(val, fieldDef, className);
case 'reference':
return <span className={className}>{String(val)}</span>;
case 'references':
if (Array.isArray(val)) {
return (
<div className={className}>
{val.map((item, index) => (
<span key={index}>{String(item)}</span>
))}
</div>
);
}
return null;
case 'object':
return renderObject(val, fieldDef, className);
case 'array':
return renderArray(val, fieldDef, className);
case 'json':
return (
<pre className={className}>
{JSON.stringify(val, null, 2)}
</pre>
);
default:
return <span className={className}>{String(val)}</span>;
}
};
return <>{renderByType(field.type, value, field)}</>;
}
function formatNumber(value: unknown): string {
const num = Number(value);
if (isNaN(num)) return String(value);
return num.toLocaleString('zh-CN');
}
function formatDate(value: unknown, format: 'date' | 'datetime'): string {
const date = new Date(String(value));
if (isNaN(date.getTime())) return String(value);
if (format === 'date') {
return date.toLocaleDateString('zh-CN', {
year: 'numeric',
month: 'long',
day: 'numeric',
});
}
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
});
}
function renderMedia(value: unknown, field: FieldDefinition, className?: string): React.ReactNode {
if (typeof value === 'string') {
return (
<img
src={value}
alt={field.label}
className={className}
loading="lazy"
/>
);
}
if (typeof value === 'object' && value !== null) {
const media = value as Record<string, unknown>;
const url = (media.url || media.path || media.src) as string | undefined;
const alt = (media.alt || field.label) as string;
if (url) {
return (
<img
src={url}
alt={alt}
className={className}
loading="lazy"
width={typeof media.width === 'number' ? media.width : undefined}
height={typeof media.height === 'number' ? media.height : undefined}
/>
);
}
}
return null;
}
function renderObject(value: unknown, field: FieldDefinition, className?: string): React.ReactNode {
if (!field.fields || !value || typeof value !== 'object') {
return null;
}
const obj = value as Record<string, unknown>;
return (
<div className={className}>
{field.fields.map((subField) => (
<div key={subField.name} className="mb-2">
<span className="font-medium text-text-secondary">{subField.label}</span>
<FieldRenderer field={subField} value={obj[subField.name]} />
</div>
))}
</div>
);
}
function renderArray(value: unknown, field: FieldDefinition, className?: string): React.ReactNode {
if (!Array.isArray(value)) {
return null;
}
const itemFields = field.fields;
if (itemFields && itemFields.length > 0) {
return (
<div className={className}>
{value.map((item, index) => (
<div key={index} className="mb-3">
<div className="font-medium text-text-secondary mb-1"> {index + 1} </div>
{renderArrayItem(item, itemFields)}
</div>
))}
</div>
);
}
return (
<ul className={className}>
{value.map((item, index) => (
<li key={index}>{String(item)}</li>
))}
</ul>
);
}
function renderArrayItem(item: unknown, fields: FieldDefinition[]): React.ReactNode {
if (typeof item === 'object' && item !== null) {
const obj = item as Record<string, unknown>;
return (
<div className="pl-4 border-l-2 border-border-primary">
{fields.map((subField) => (
<div key={subField.name} className="mb-1">
<span className="text-sm text-text-muted">{subField.label}</span>
<FieldRenderer field={subField} value={obj[subField.name]} />
</div>
))}
</div>
);
}
return <span>{String(item)}</span>;
}
+204
View File
@@ -0,0 +1,204 @@
'use client';
import { useEditor, EditorContent, type Editor } from '@tiptap/react';
import StarterKit from '@tiptap/starter-kit';
import Link from '@tiptap/extension-link';
import Placeholder from '@tiptap/extension-placeholder';
import { useEffect } from 'react';
import {
Bold,
Italic,
Heading2,
Heading3,
List,
ListOrdered,
Link as LinkIcon,
Quote,
Undo2,
Redo2,
} from 'lucide-react';
interface RichTextEditorProps {
value: string;
onChange: (html: string) => void;
placeholder?: string;
}
/**
* TipTap 富文本编辑器
* 用于 CMS ContentEditor 的 richtext 字段
* 支持:加粗、斜体、标题、列表、引用、链接
* 暗色主题适配(CMS Studio 暗色背景)
*/
export function RichTextEditor({ value, onChange, placeholder = '请输入内容...' }: RichTextEditorProps) {
const editor = useEditor({
extensions: [
StarterKit.configure({
heading: {
levels: [2, 3],
},
}),
Link.configure({
openOnClick: false,
HTMLAttributes: {
class: 'text-brand underline',
},
}),
Placeholder.configure({
placeholder,
}),
],
content: value,
immediatelyRender: false,
onUpdate: ({ editor: e }) => {
onChange(e.getHTML());
},
editorProps: {
attributes: {
class: 'rich-text-editor-content',
},
},
});
// 同步外部 value 变化(如切换内容条目时)
useEffect(() => {
if (editor && value !== editor.getHTML()) {
editor.commands.setContent(value || '');
}
}, [editor, value]);
if (!editor) {
return (
<div className="min-h-[120px] bg-white/[0.05] border border-white/[0.1] rounded-lg p-3 text-white/40 text-sm">
...
</div>
);
}
return (
<div className="rich-text-editor bg-white/[0.05] border border-white/[0.1] rounded-lg overflow-hidden focus-within:border-brand/50 focus-within:ring-1 focus-within:ring-brand/30 transition-colors">
<Toolbar editor={editor} />
<EditorContent editor={editor} />
</div>
);
}
function Toolbar({ editor }: { editor: Editor }) {
const btnClass = (isActive: boolean) =>
`p-1.5 rounded transition-colors ${
isActive
? 'bg-brand/20 text-brand'
: 'text-white/60 hover:bg-white/[0.05] hover:text-white'
}`;
const setLink = () => {
const previousUrl = editor.getAttributes('link').href;
const url = window.prompt('链接 URL', previousUrl);
if (url === null) return;
if (url === '') {
editor.chain().focus().extendMarkRange('link').unsetLink().run();
return;
}
editor.chain().focus().extendMarkRange('link').setLink({ href: url }).run();
};
return (
<div className="flex items-center gap-0.5 px-2 py-1.5 border-b border-white/[0.08] bg-white/[0.02]">
<button
type="button"
onClick={() => editor.chain().focus().toggleBold().run()}
className={btnClass(editor.isActive('bold'))}
title="加粗"
>
<Bold className="w-4 h-4" />
</button>
<button
type="button"
onClick={() => editor.chain().focus().toggleItalic().run()}
className={btnClass(editor.isActive('italic'))}
title="斜体"
>
<Italic className="w-4 h-4" />
</button>
<div className="w-px h-5 bg-white/[0.08] mx-1" />
<button
type="button"
onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}
className={btnClass(editor.isActive('heading', { level: 2 }))}
title="标题 2"
>
<Heading2 className="w-4 h-4" />
</button>
<button
type="button"
onClick={() => editor.chain().focus().toggleHeading({ level: 3 }).run()}
className={btnClass(editor.isActive('heading', { level: 3 }))}
title="标题 3"
>
<Heading3 className="w-4 h-4" />
</button>
<div className="w-px h-5 bg-white/[0.08] mx-1" />
<button
type="button"
onClick={() => editor.chain().focus().toggleBulletList().run()}
className={btnClass(editor.isActive('bulletList'))}
title="无序列表"
>
<List className="w-4 h-4" />
</button>
<button
type="button"
onClick={() => editor.chain().focus().toggleOrderedList().run()}
className={btnClass(editor.isActive('orderedList'))}
title="有序列表"
>
<ListOrdered className="w-4 h-4" />
</button>
<button
type="button"
onClick={() => editor.chain().focus().toggleBlockquote().run()}
className={btnClass(editor.isActive('blockquote'))}
title="引用"
>
<Quote className="w-4 h-4" />
</button>
<div className="w-px h-5 bg-white/[0.08] mx-1" />
<button
type="button"
onClick={setLink}
className={btnClass(editor.isActive('link'))}
title="链接"
>
<LinkIcon className="w-4 h-4" />
</button>
<div className="flex-1" />
<button
type="button"
onClick={() => editor.chain().focus().undo().run()}
disabled={!editor.can().undo()}
className="p-1.5 rounded text-white/60 hover:bg-white/[0.05] hover:text-white transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
title="撤销"
>
<Undo2 className="w-4 h-4" />
</button>
<button
type="button"
onClick={() => editor.chain().focus().redo().run()}
disabled={!editor.can().redo()}
className="p-1.5 rounded text-white/60 hover:bg-white/[0.05] hover:text-white transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
title="重做"
>
<Redo2 className="w-4 h-4" />
</button>
</div>
);
}
+93
View File
@@ -0,0 +1,93 @@
'use client';
import * as React from 'react';
import { ContentRenderer } from './ContentRenderer';
import { Spinner } from '@/components/ui/loading-state';
import type { ContentZone, ContentItem } from '@/lib/cms';
import { cmsClient } from '@/lib/cms';
export interface SectionRendererProps {
zoneCode: string;
fallback?: React.ReactNode;
loadingFallback?: React.ReactNode;
className?: string;
}
export function SectionRenderer({
zoneCode,
fallback = null,
loadingFallback,
className,
}: SectionRendererProps) {
const [zone, setZone] = React.useState<ContentZone | null>(null);
const [isLoading, setIsLoading] = React.useState(true);
const [error, setError] = React.useState<Error | null>(null);
React.useEffect(() => {
let mounted = true;
async function fetchZone() {
try {
setIsLoading(true);
setError(null);
if (cmsClient?.getZone) {
const result = await cmsClient.getZone(zoneCode);
if (mounted) {
setZone(result);
}
} else {
if (mounted) {
setZone(null);
}
}
} catch (err) {
if (mounted) {
setError(err instanceof Error ? err : new Error(String(err)));
}
} finally {
if (mounted) {
setIsLoading(false);
}
}
}
fetchZone();
return () => {
mounted = false;
};
}, [zoneCode]);
if (isLoading) {
if (loadingFallback) {
return <>{loadingFallback}</>;
}
return (
<div className="flex items-center justify-center py-12">
<Spinner size="lg" />
</div>
);
}
if (error || !zone) {
return <>{fallback}</>;
}
const sortedItems = [...zone.items].sort((a, b) => a.sortOrder - b.sortOrder);
return (
<div className={className}>
{sortedItems.map((zoneItem, index) => {
if (!zoneItem.item) return null;
return (
<ContentRenderer
key={zoneItem.itemId || index}
item={zoneItem.item as ContentItem}
fallback={null}
/>
);
})}
</div>
);
}
+394
View File
@@ -0,0 +1,394 @@
'use client';
import { useState, useMemo, useEffect, useCallback } from 'react';
import {
ContentZoneRenderer,
getContentZone,
getAllContentZones,
getContentItems,
reorderZoneItems,
removeItemFromZone,
addItemToZone,
updateZoneSettings,
resetCmsData,
exportCmsData,
} from '@/lib/cms';
import type { ContentZone, ContentItem } from '@/lib/cms';
const ZONE_META: Record<string, { name: string; page: string }> = {
'home-stats': { name: '首页 · 数据指标区', page: 'home' },
'home-services': { name: '首页 · 核心服务区', page: 'home' },
'home-solutions': { name: '首页 · 解决方案区', page: 'home' },
'home-cases': { name: '首页 · 客户案例区', page: 'home' },
'home-news': { name: '首页 · 新闻动态区', page: 'home' },
};
export function ZoneManager() {
const [zoneKey, setZoneKey] = useState('home-cases');
const [zone, setZone] = useState<ContentZone | null>(null);
const [showAddPanel, setShowAddPanel] = useState(false);
const [showTools, setShowTools] = useState(false);
const [dragIndex, setDragIndex] = useState<number | null>(null);
const [dragOverIndex, setDragOverIndex] = useState<number | null>(null);
const [zoneList, setZoneList] = useState<Record<string, ContentZone>>({});
const loadZones = useCallback(() => {
setZoneList(getAllContentZones());
const z = getContentZone(zoneKey);
setZone(z);
}, [zoneKey]);
useEffect(() => {
loadZones();
const handleCmsUpdate = () => loadZones();
window.addEventListener('cms-updated', handleCmsUpdate);
return () => window.removeEventListener('cms-updated', handleCmsUpdate);
}, [loadZones]);
const zoneItemModelCode = useMemo(() => {
return zone?.items?.[0]?.item?.modelCode || 'case-study';
}, [zone]);
const candidateItems = useMemo(() => {
if (!zone) return [];
const allItems = getContentItems(zoneItemModelCode);
const existingIds = new Set(zone.items?.map((zi) => zi.item?.id).filter(Boolean) || []);
return allItems.filter((item) => !existingIds.has(item.id));
}, [zone, zoneItemModelCode]);
const sortedItems = useMemo(() => {
return [...(zone?.items || [])]
.filter((zi) => zi.item)
.sort(
(a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0)
);
}, [zone]);
const handleDragStart = (index: number) => {
setDragIndex(index);
};
const handleDragOver = (e: React.DragEvent, index: number) => {
e.preventDefault();
setDragOverIndex(index);
};
const handleDrop = (targetIndex: number) => {
if (dragIndex === null || dragIndex === targetIndex) {
setDragIndex(null);
setDragOverIndex(null);
return;
}
reorderZoneItems(zoneKey, dragIndex, targetIndex);
setDragIndex(null);
setDragOverIndex(null);
};
const handleRemoveItem = (itemId: string) => {
removeItemFromZone(zoneKey, itemId);
};
const handleAddItem = (item: ContentItem) => {
addItemToZone(zoneKey, item);
setShowAddPanel(false);
};
const handleLayoutChange = (field: string, value: unknown) => {
updateZoneSettings(zoneKey, { [field]: value });
};
const handleReset = () => {
if (confirm('确定要重置所有 CMS 数据为默认值吗?此操作不可撤销。')) {
resetCmsData();
}
};
const handleExport = () => {
const data = exportCmsData();
const blob = new Blob([data], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `novalon-cms-export-${Date.now()}.json`;
a.click();
URL.revokeObjectURL(url);
};
const zoneMeta = ZONE_META[zoneKey];
const zoneKeys = Object.keys(zoneList).sort();
return (
<div className="h-full flex">
<div className="w-72 flex-shrink-0 border-r border-white/[0.08] overflow-y-auto flex flex-col">
<div className="p-4 flex-1">
<div className="text-xs text-white/40 font-medium uppercase tracking-wider mb-3 px-3">
</div>
<div className="space-y-1">
{zoneKeys.map((key) => (
<button
key={key}
onClick={() => setZoneKey(key)}
className={`w-full text-left px-3 py-2.5 rounded-lg text-sm transition-colors ${
zoneKey === key
? 'bg-brand/20 text-brand font-medium'
: 'text-white/60 hover:bg-white/[0.05] hover:text-white'
}`}
>
<div className="font-medium">{ZONE_META[key]?.name || key}</div>
<div className="text-xs opacity-60 mt-0.5">
{zoneList[key]?.items?.length || 0}
</div>
</button>
))}
</div>
</div>
<div className="p-3 border-t border-white/[0.08] space-y-2">
<button
onClick={() => setShowTools(!showTools)}
className="w-full flex items-center justify-between px-3 py-2 rounded-lg text-sm text-white/60 hover:bg-white/[0.05] hover:text-white transition-colors"
>
<span> </span>
<span>{showTools ? '▲' : '▼'}</span>
</button>
{showTools && (
<div className="space-y-1 px-2">
<button
onClick={handleExport}
className="w-full text-left px-3 py-2 rounded-lg text-sm text-white/60 hover:bg-white/[0.05] hover:text-white transition-colors"
>
📤
</button>
<button
onClick={handleReset}
className="w-full text-left px-3 py-2 rounded-lg text-sm text-red-400 hover:bg-red-500/10 transition-colors"
>
🔄
</button>
</div>
)}
</div>
</div>
<div className="flex-1 flex flex-col min-w-0">
<div className="flex-1 flex min-h-0">
<div className="flex-1 overflow-y-auto p-6">
<div className="mb-6 flex items-center justify-between">
<div>
<h2 className="text-lg font-semibold text-white">
{zoneMeta?.name || zoneKey}
</h2>
<p className="mt-1 text-sm text-white/40">
· × · {sortedItems.length}
</p>
</div>
<button
onClick={() => setShowAddPanel(true)}
className="px-4 py-2 bg-brand hover:bg-brand-hover text-white text-sm font-medium rounded-lg transition-colors"
>
+
</button>
</div>
<div className="space-y-3">
{sortedItems.map((zoneItem, index) => {
const item = zoneItem.item!;
return (
<div
key={item.id}
draggable
onDragStart={() => handleDragStart(index)}
onDragOver={(e) => handleDragOver(e, index)}
onDrop={() => handleDrop(index)}
onDragEnd={() => {
setDragIndex(null);
setDragOverIndex(null);
}}
className={`
relative flex items-center gap-4 p-4 rounded-xl border transition-all cursor-move
${dragOverIndex === index ? 'border-brand/50 bg-brand/5' : 'border-white/[0.08] bg-white/[0.02] hover:bg-white/[0.04]'}
${dragIndex === index ? 'opacity-40' : ''}
`}
>
<div className="flex-shrink-0 w-8 h-8 flex items-center justify-center rounded-lg bg-white/[0.05] text-white/40 text-sm">
</div>
<div className="flex-1 min-w-0">
<div className="text-sm font-medium text-white truncate">
{item.title}
</div>
<div className="mt-0.5 text-xs text-white/40">
{item.modelCode} · {item.slug}
</div>
</div>
<div className="flex-shrink-0 text-xs text-white/40">
#{index + 1}
</div>
<button
onClick={(e) => {
e.stopPropagation();
handleRemoveItem(item.id);
}}
className="flex-shrink-0 w-8 h-8 flex items-center justify-center rounded-lg text-white/40 hover:text-red-400 hover:bg-red-500/10 transition-colors"
>
×
</button>
</div>
);
})}
{sortedItems.length === 0 && (
<div className="py-16 text-center border-2 border-dashed border-white/[0.08] rounded-xl">
<div className="text-4xl mb-3 opacity-30">📦</div>
<div className="text-white/50 text-sm"></div>
</div>
)}
</div>
</div>
<div className="w-80 flex-shrink-0 border-l border-white/[0.08] overflow-y-auto">
<div className="p-4 border-b border-white/[0.08]">
<h3 className="text-sm font-semibold text-white"></h3>
</div>
<div className="p-4 space-y-5">
<div>
<label className="block text-xs font-medium text-white/60 mb-2">
</label>
<select
className="w-full px-3 py-2 bg-white/[0.05] border border-white/[0.1] rounded-lg text-white text-sm focus:outline-none focus:border-brand/50"
value={(zone?.settings?.layout as string) || 'grid'}
onChange={(e) => handleLayoutChange('layout', e.target.value)}
>
<option value="grid"></option>
<option value="list"></option>
<option value="carousel"></option>
</select>
</div>
<div>
<label className="block text-xs font-medium text-white/60 mb-2">
</label>
<select
className="w-full px-3 py-2 bg-white/[0.05] border border-white/[0.1] rounded-lg text-white text-sm focus:outline-none focus:border-brand/50"
value={(zone?.settings?.columns as number) || 3}
onChange={(e) => handleLayoutChange('columns', Number(e.target.value))}
>
<option value={1}>1 </option>
<option value={2}>2 </option>
<option value={3}>3 </option>
<option value={4}>4 </option>
<option value={5}>5 </option>
<option value={6}>6 </option>
</select>
</div>
<div>
<label className="block text-xs font-medium text-white/60 mb-2">
</label>
<select
className="w-full px-3 py-2 bg-white/[0.05] border border-white/[0.1] rounded-lg text-white text-sm focus:outline-none focus:border-brand/50"
value={(zone?.settings?.gap as string) || 'medium'}
onChange={(e) => handleLayoutChange('gap', e.target.value)}
>
<option value="small"></option>
<option value="medium"></option>
<option value="large"></option>
</select>
</div>
<div className="border-t border-white/[0.08] pt-5">
<h4 className="text-xs font-medium text-white/60 mb-3"></h4>
<div className="space-y-3 text-sm">
<div className="flex justify-between">
<span className="text-white/40">Zone Key</span>
<span className="text-white/70 font-mono text-xs">{zoneKey}</span>
</div>
<div className="flex justify-between">
<span className="text-white/40"></span>
<span className="text-white/70">{zoneMeta?.page || '-'}</span>
</div>
<div className="flex justify-between">
<span className="text-white/40"></span>
<span className="text-white/70">{zoneItemModelCode}</span>
</div>
<div className="flex justify-between">
<span className="text-white/40"></span>
<span className="text-white/70">{sortedItems.length}</span>
</div>
</div>
</div>
</div>
</div>
</div>
<div className="h-64 flex-shrink-0 border-t border-white/[0.08] bg-ink overflow-y-auto">
<div className="p-4 border-b border-white/[0.08] bg-ink-light flex items-center justify-between">
<span className="text-xs font-medium text-white/60"></span>
<span className="text-xs text-white/40">
: {(zone?.settings?.layout as string) || 'grid'} · {(zone?.settings?.columns as number) || 3}
</span>
</div>
<div className="p-6">
{zone ? (
<ContentZoneRenderer zone={zone} />
) : (
<div className="text-sm text-white/40 py-8 text-center">
</div>
)}
</div>
</div>
</div>
{showAddPanel && (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm"
onClick={() => setShowAddPanel(false)}
>
<div
className="bg-ink border border-white/[0.1] rounded-2xl w-full max-w-lg max-h-[70vh] overflow-hidden shadow-2xl"
onClick={(e) => e.stopPropagation()}
>
<div className="p-4 border-b border-white/[0.08] flex items-center justify-between">
<h3 className="text-base font-semibold text-white"></h3>
<button
onClick={() => setShowAddPanel(false)}
className="w-8 h-8 flex items-center justify-center rounded-lg text-white/40 hover:text-white hover:bg-white/10 transition-colors"
>
×
</button>
</div>
<div className="p-4 overflow-y-auto max-h-[60vh]">
{candidateItems.length > 0 ? (
<div className="space-y-2">
{candidateItems.map((item) => (
<button
key={item.id}
onClick={() => handleAddItem(item)}
className="w-full text-left p-3 rounded-lg border border-white/[0.08] bg-white/[0.02] hover:bg-white/[0.06] hover:border-brand/30 transition-all"
>
<div className="text-sm font-medium text-white">{item.title}</div>
<div className="mt-0.5 text-xs text-white/40">
{item.modelCode} · {item.slug}
</div>
</button>
))}
</div>
) : (
<div className="py-12 text-center">
<div className="text-4xl mb-3 opacity-30"></div>
<div className="text-white/50 text-sm"></div>
</div>
)}
</div>
</div>
</div>
)}
</div>
);
}
+8
View File
@@ -0,0 +1,8 @@
export { ContentRenderer, registerComponent, getRegisteredComponent } from './ContentRenderer';
export type { ContentRendererProps } from './ContentRenderer';
export { SectionRenderer } from './SectionRenderer';
export type { SectionRendererProps } from './SectionRenderer';
export { FieldRenderer } from './FieldRenderer';
export type { FieldRendererProps } from './FieldRenderer';
+391
View File
@@ -0,0 +1,391 @@
'use client';
import { motion } from 'framer-motion';
import { ScrollReveal, StaggerReveal } from '@/components/ui/scroll-reveal';
import { SectionLabel, EASE_OUT } from '@/components/ui/page-decoration';
import { CheckCircle2, ChevronDown } from 'lucide-react';
import { cn } from '@/lib/utils';
import { useState } from 'react';
export interface MethodologyLayer {
title: string;
description: string;
items: string[];
}
export interface MethodologyFrameworkProps {
title: string;
subtitle?: string;
layers: MethodologyLayer[];
accentColor?: string;
}
export function MethodologyFramework({
title,
subtitle,
layers,
accentColor = '#C41E3A',
}: MethodologyFrameworkProps) {
return (
<section className="relative py-20 sm:py-28 md:py-32 lg:py-40 overflow-hidden bg-bg-primary">
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10">
<ScrollReveal className="mb-16 sm:mb-20 max-w-3xl">
<SectionLabel>Methodology</SectionLabel>
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold text-ink tracking-tight leading-tight mb-6">
{title}
</h2>
{subtitle && (
<p className="text-text-secondary leading-relaxed text-lg">
{subtitle}
</p>
)}
</ScrollReveal>
<div className="relative">
<StaggerReveal staggerDelay={0.1} delayChildren={0.05}>
{layers.map((layer, index) => (
<motion.div
key={index}
className="relative group"
initial={{ opacity: 0, x: -30 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true, margin: '-50px' }}
transition={{ duration: 0.6, delay: index * 0.08, ease: EASE_OUT }}
>
<div className="flex gap-6 lg:gap-10">
<div className="flex flex-col items-center">
<div
className="w-12 h-12 rounded-xl flex items-center justify-center text-white font-bold text-lg shrink-0"
style={{ backgroundColor: accentColor }}
>
{String(index + 1).padStart(2, '0')}
</div>
{index < layers.length - 1 && (
<div className="w-px flex-1 bg-gradient-to-b from-border-primary to-transparent mt-3" />
)}
</div>
<div className="flex-1 pb-12 lg:pb-16">
<h3 className="text-xl lg:text-2xl font-bold text-text-primary mb-3">
{layer.title}
</h3>
<p className="text-text-secondary leading-relaxed mb-5">
{layer.description}
</p>
<div className="grid sm:grid-cols-2 gap-3">
{layer.items.map((item, i) => (
<div key={i} className="flex items-start gap-3">
<CheckCircle2 className="w-5 h-5 shrink-0 mt-0.5" style={{ color: accentColor }} />
<span className="text-text-secondary text-sm leading-relaxed">
{item}
</span>
</div>
))}
</div>
</div>
</div>
</motion.div>
))}
</StaggerReveal>
</div>
</div>
</section>
);
}
export interface TechStackCategory {
name: string;
items: string[];
}
export interface TechStackShowcaseProps {
title: string;
subtitle?: string;
categories: TechStackCategory[];
}
export function TechStackShowcase({ title, subtitle, categories }: TechStackShowcaseProps) {
return (
<section className="relative py-20 sm:py-28 md:py-32 lg:py-40 overflow-hidden bg-bg-secondary">
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10">
<ScrollReveal className="mb-16 sm:mb-20 max-w-3xl">
<SectionLabel>Tech Stack</SectionLabel>
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold text-ink tracking-tight leading-tight mb-6">
{title}
</h2>
{subtitle && (
<p className="text-text-secondary leading-relaxed text-lg">
{subtitle}
</p>
)}
</ScrollReveal>
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
<StaggerReveal staggerDelay={0.08} delayChildren={0.05}>
{categories.map((category, index) => (
<div
key={index}
className="group relative p-8 border border-border-primary bg-bg-primary hover:border-border-secondary hover:bg-bg-secondary transition-all duration-500"
>
<div className="absolute top-0 left-0 h-1 w-0 group-hover:w-full transition-all duration-700 bg-brand" />
<h3 className="text-lg font-bold text-text-primary mb-5">{category.name}</h3>
<div className="flex flex-wrap gap-2">
{category.items.map((item, i) => (
<span
key={i}
className="px-3 py-1.5 text-xs font-medium bg-bg-secondary text-text-secondary border border-border-primary hover:border-brand/30 hover:text-brand transition-colors duration-300"
>
{item}
</span>
))}
</div>
</div>
))}
</StaggerReveal>
</div>
</div>
</section>
);
}
export interface TimelineStep {
phase: string;
title: string;
description: string;
duration: string;
deliverables: string[];
}
export interface DeliveryTimelineProps {
title: string;
subtitle?: string;
steps: TimelineStep[];
accentColor?: string;
}
export function DeliveryTimeline({
title,
subtitle,
steps,
accentColor = '#C41E3A',
}: DeliveryTimelineProps) {
return (
<section className="relative py-20 sm:py-28 md:py-32 lg:py-40 overflow-hidden bg-bg-primary">
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10">
<ScrollReveal className="mb-16 sm:mb-20 max-w-3xl">
<SectionLabel>Delivery Process</SectionLabel>
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold text-ink tracking-tight leading-tight mb-6">
{title}
</h2>
{subtitle && (
<p className="text-text-secondary leading-relaxed text-lg">
{subtitle}
</p>
)}
</ScrollReveal>
<div className="relative">
<div className="absolute left-8 lg:left-1/2 top-0 bottom-0 w-px bg-gradient-to-b from-brand/50 via-border-primary to-brand/50" />
<div className="space-y-12 lg:space-y-16">
{steps.map((step, index) => (
<ScrollReveal key={index} delay={index * 0.08}>
<div className={cn(
'relative flex gap-8 lg:gap-12',
index % 2 === 0 ? 'lg:flex-row' : 'lg:flex-row-reverse'
)}>
<div className="hidden lg:block lg:w-1/2" />
<div className="absolute left-8 lg:left-1/2 -translate-x-1/2 w-4 h-4 rounded-full border-2 z-10"
style={{ borderColor: accentColor, backgroundColor: '#ffffff' }}
/>
<div className="lg:w-1/2 pl-16 lg:pl-0">
<div className={cn(
'p-8 lg:p-10 border border-border-primary bg-bg-secondary hover:border-border-secondary transition-all duration-500',
index % 2 === 0 ? 'lg:ml-12' : 'lg:mr-12'
)}>
<div className="flex items-center justify-between mb-4">
<span
className="text-xs font-bold tracking-[0.2em] uppercase"
style={{ color: accentColor }}
>
{step.phase}
</span>
<span className="text-sm text-text-muted">
{step.duration}
</span>
</div>
<h3 className="text-xl font-bold text-text-primary mb-3">{step.title}</h3>
<p className="text-text-secondary leading-relaxed mb-5">
{step.description}
</p>
<div className="pt-5 border-t border-border-primary">
<div className="text-xs text-text-muted mb-3 font-medium uppercase tracking-wider">
</div>
<div className="flex flex-wrap gap-2">
{step.deliverables.map((deliverable, i) => (
<span
key={i}
className="px-2.5 py-1 text-xs bg-bg-primary text-text-secondary border border-border-primary"
>
{deliverable}
</span>
))}
</div>
</div>
</div>
</div>
</div>
</ScrollReveal>
))}
</div>
</div>
</div>
</section>
);
}
export interface FAQItem {
question: string;
answer: string;
}
export interface FAQSectionProps {
title: string;
subtitle?: string;
items: FAQItem[];
}
export function FAQSection({ title, subtitle, items }: FAQSectionProps) {
const [openIndex, setOpenIndex] = useState<number | null>(0);
return (
<section className="relative py-20 sm:py-28 md:py-32 lg:py-40 overflow-hidden bg-bg-secondary">
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10">
<div className="grid lg:grid-cols-12 gap-12 lg:gap-20">
<ScrollReveal className="lg:col-span-4">
<SectionLabel>FAQ</SectionLabel>
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold text-ink tracking-tight leading-tight mb-6">
{title}
</h2>
{subtitle && (
<p className="text-text-secondary leading-relaxed text-lg">
{subtitle}
</p>
)}
</ScrollReveal>
<div className="lg:col-span-8 space-y-3">
<StaggerReveal staggerDelay={0.06} delayChildren={0.05}>
{items.map((item, index) => (
<div
key={index}
className="border border-border-primary bg-bg-primary hover:border-border-secondary transition-colors duration-300"
>
<button
onClick={() => setOpenIndex(openIndex === index ? null : index)}
className="w-full flex items-center justify-between gap-6 p-6 lg:p-7 text-left"
>
<span className="text-text-primary font-semibold text-base lg:text-lg">
{item.question}
</span>
<motion.div
animate={{ rotate: openIndex === index ? 180 : 0 }}
transition={{ duration: 0.3, ease: EASE_OUT }}
className="shrink-0"
>
<ChevronDown className="w-5 h-5 text-text-muted" />
</motion.div>
</button>
<motion.div
initial={false}
animate={{
height: openIndex === index ? 'auto' : 0,
opacity: openIndex === index ? 1 : 0,
}}
transition={{ duration: 0.4, ease: EASE_OUT }}
className="overflow-hidden"
>
<div className="px-6 lg:px-7 pb-7 pt-1 text-text-secondary leading-relaxed">
{item.answer}
</div>
</motion.div>
</div>
))}
</StaggerReveal>
</div>
</div>
</div>
</section>
);
}
export interface DataMetric {
value: string;
label: string;
description?: string;
trend?: string;
trendUp?: boolean;
}
export interface DataProofSectionProps {
title: string;
subtitle?: string;
metrics: DataMetric[];
}
export function DataProofSection({ title, subtitle, metrics }: DataProofSectionProps) {
return (
<section className="relative py-20 sm:py-28 md:py-32 lg:py-40 overflow-hidden bg-bg-primary">
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10">
<ScrollReveal className="mb-16 sm:mb-20 max-w-3xl">
<SectionLabel>Proven Results</SectionLabel>
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold text-ink tracking-tight leading-tight mb-6">
{title}
</h2>
{subtitle && (
<p className="text-text-secondary leading-relaxed text-lg">
{subtitle}
</p>
)}
</ScrollReveal>
<div className="grid md:grid-cols-2 lg:grid-cols-4 gap-px bg-border-primary">
<StaggerReveal staggerDelay={0.08} delayChildren={0.05}>
{metrics.map((metric, index) => (
<div
key={index}
className="group relative p-10 lg:p-12 bg-bg-secondary hover:bg-bg-secondary transition-all duration-500"
>
<div className="absolute top-0 left-0 h-1 w-0 group-hover:w-full transition-all duration-700 bg-brand" />
<div className="relative z-10">
<div className="text-5xl lg:text-6xl font-bold tracking-tighter mb-4">
<span className="text-text-primary">{metric.value}</span>
</div>
<div className="text-lg font-semibold text-text-primary mb-2">
{metric.label}
</div>
{metric.description && (
<p className="text-sm text-text-muted leading-relaxed">
{metric.description}
</p>
)}
{metric.trend && (
<div className={cn(
'mt-4 inline-flex items-center gap-1.5 text-sm font-medium',
metric.trendUp ? 'text-success' : 'text-brand'
)}>
<span>{metric.trend}</span>
</div>
)}
</div>
</div>
))}
</StaggerReveal>
</div>
</div>
</section>
);
}
+155
View File
@@ -0,0 +1,155 @@
'use client';
import { motion } from 'framer-motion';
import { ScrollReveal } from '@/components/ui/scroll-reveal';
import { SectionLabel, EASE_OUT } from '@/components/ui/page-decoration';
import { Quote } from 'lucide-react';
import { useState } from 'react';
import { cn } from '@/lib/utils';
export interface Testimonial {
quote: string;
author: string;
title: string;
company: string;
industry: string;
}
export interface TestimonialSectionProps {
title: string;
subtitle?: string;
testimonials: Testimonial[];
accentColor?: string;
}
export function TestimonialSection({
title,
subtitle,
testimonials,
accentColor = '#C41E3A',
}: TestimonialSectionProps) {
const [activeIndex, setActiveIndex] = useState(0);
return (
<section className="relative py-20 sm:py-28 md:py-32 lg:py-40 overflow-hidden bg-bg-secondary">
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10">
<ScrollReveal className="mb-16 sm:mb-20 max-w-3xl">
<SectionLabel>Client Stories</SectionLabel>
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold text-ink tracking-tight leading-tight mb-6">
{title}
</h2>
{subtitle && (
<p className="text-text-secondary leading-relaxed text-lg">
{subtitle}
</p>
)}
</ScrollReveal>
<div className="relative">
<div className="relative overflow-hidden">
<motion.div
className="flex"
animate={{ x: `-${activeIndex * 100}%` }}
transition={{ duration: 0.6, ease: EASE_OUT }}
>
{testimonials.map((testimonial, index) => (
<div key={index} className="w-full shrink-0">
<div className="max-w-4xl mx-auto">
<div className="relative p-10 lg:p-16 border border-border-primary bg-bg-primary">
<Quote className="absolute top-8 left-8 w-12 h-12 opacity-10 text-brand" />
<div className="relative z-10">
<p className="text-xl lg:text-2xl text-text-primary leading-relaxed font-light mb-10 lg:mb-14">
{testimonial.quote}
</p>
<div className="flex items-center gap-5">
<div
className="w-14 h-14 rounded-full flex items-center justify-center text-white font-bold text-lg"
style={{ backgroundColor: accentColor }}
>
{testimonial.author.charAt(0)}
</div>
<div>
<div className="text-text-primary font-semibold text-lg">
{testimonial.author}
</div>
<div className="text-text-secondary text-sm">
{testimonial.title} · {testimonial.company}
</div>
<div className="text-text-muted text-xs mt-0.5">
{testimonial.industry}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
))}
</motion.div>
</div>
<div className="flex justify-center gap-3 mt-10">
{testimonials.map((_, index) => (
<button
key={index}
onClick={() => setActiveIndex(index)}
className={cn(
'h-2 rounded-full transition-all duration-500',
activeIndex === index
? 'w-8'
: 'w-2 bg-border-primary hover:bg-border-secondary'
)}
style={activeIndex === index ? { backgroundColor: accentColor } : {}}
/>
))}
</div>
</div>
</div>
</section>
);
}
export interface LogoWallProps {
title: string;
logos: { name: string; industry?: string }[];
}
export function LogoWall({ title, logos }: LogoWallProps) {
return (
<section className="relative py-16 sm:py-20 overflow-hidden bg-bg-primary">
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10">
<ScrollReveal>
<div className="text-center mb-12">
<p className="text-sm text-text-muted tracking-[0.2em] uppercase font-medium">
{title}
</p>
</div>
</ScrollReveal>
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-px bg-border-primary">
{logos.map((logo, index) => (
<motion.div
key={index}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: index * 0.05, ease: EASE_OUT }}
className="flex items-center justify-center p-8 bg-bg-primary hover:bg-bg-secondary transition-colors duration-300"
>
<div className="text-center">
<div className="text-text-secondary font-semibold text-base hover:text-text-primary transition-colors duration-300">
{logo.name}
</div>
{logo.industry && (
<div className="text-text-muted text-xs mt-1">
{logo.industry}
</div>
)}
</div>
</motion.div>
))}
</div>
</div>
</section>
);
}