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:
@@ -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");
|
||||
@@ -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"
|
||||
@@ -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
@@ -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>我们不把“项目完成”当作终点。</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} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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} />;
|
||||
}
|
||||
@@ -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} />;
|
||||
}
|
||||
@@ -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">•</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>
|
||||
);
|
||||
}
|
||||
@@ -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
@@ -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">
|
||||
“{caseItem.quote}”
|
||||
</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>
|
||||
);
|
||||
}
|
||||
@@ -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 & 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 & 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">
|
||||
某大型制造集团,拥有5个生产基地,员工5000+人。原有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">
|
||||
某大型制造集团,拥有5个生产基地,员工5000+人。原有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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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。未来迁移到真实后端时可按需恢复。
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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">
|
||||
Slug(URL 标识)
|
||||
</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>
|
||||
);
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user