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

- 新增 Prisma + SQLite 数据库模型 (Category, Content, Media, User 等)
- 新增 Admin 管理后台 (认证、内容管理、媒体管理)
- 新增 CMS API 路由 (CRUD, 草稿/发布, 重新验证)
- 新增 CMS 内容版本的历史归档页面
- 新增 components/cms 内容渲染组件
- 新增 components/admin 管理后台 UI 组件
- 更新 Contact API 路由
This commit is contained in:
张翔
2026-07-07 06:53:58 +08:00
parent 829d83522c
commit b5245f9aa2
87 changed files with 25659 additions and 0 deletions
@@ -0,0 +1,126 @@
-- CreateTable
CREATE TABLE "User" (
"id" TEXT NOT NULL PRIMARY KEY,
"username" TEXT NOT NULL,
"password" TEXT NOT NULL,
"nickname" TEXT NOT NULL DEFAULT '',
"avatar" TEXT NOT NULL DEFAULT '',
"email" TEXT NOT NULL DEFAULT '',
"phone" TEXT NOT NULL DEFAULT '',
"status" INTEGER NOT NULL DEFAULT 1,
"role" TEXT NOT NULL DEFAULT 'admin',
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL
);
-- CreateTable
CREATE TABLE "ContentModel" (
"id" TEXT NOT NULL PRIMARY KEY,
"code" TEXT NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT NOT NULL DEFAULT '',
"fields" TEXT NOT NULL,
"isPageType" BOOLEAN NOT NULL DEFAULT false,
"urlPattern" TEXT NOT NULL DEFAULT '',
"hasVersions" BOOLEAN NOT NULL DEFAULT false,
"hasDraft" BOOLEAN NOT NULL DEFAULT false,
"icon" TEXT NOT NULL DEFAULT '',
"sortOrder" INTEGER NOT NULL DEFAULT 0,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL
);
-- CreateTable
CREATE TABLE "ContentItem" (
"id" TEXT NOT NULL PRIMARY KEY,
"modelId" TEXT NOT NULL,
"modelCode" TEXT NOT NULL,
"title" TEXT NOT NULL,
"slug" TEXT NOT NULL DEFAULT '',
"status" TEXT NOT NULL DEFAULT 'draft',
"data" TEXT NOT NULL,
"version" INTEGER NOT NULL DEFAULT 1,
"sortOrder" INTEGER NOT NULL DEFAULT 0,
"publishedAt" DATETIME,
"createdBy" TEXT NOT NULL DEFAULT 'system',
"updatedBy" TEXT NOT NULL DEFAULT 'system',
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "ContentItem_modelId_fkey" FOREIGN KEY ("modelId") REFERENCES "ContentModel" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "ContentZone" (
"id" TEXT NOT NULL PRIMARY KEY,
"code" TEXT NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT NOT NULL DEFAULT '',
"pageCode" TEXT NOT NULL DEFAULT '',
"zoneKey" TEXT NOT NULL DEFAULT '',
"allowedModels" TEXT NOT NULL DEFAULT '[]',
"items" TEXT NOT NULL DEFAULT '[]',
"settings" TEXT NOT NULL DEFAULT '{}',
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL
);
-- CreateTable
CREATE TABLE "MediaAsset" (
"id" TEXT NOT NULL PRIMARY KEY,
"name" TEXT NOT NULL,
"path" TEXT NOT NULL,
"url" TEXT NOT NULL,
"mimeType" TEXT NOT NULL,
"size" INTEGER NOT NULL DEFAULT 0,
"width" INTEGER NOT NULL DEFAULT 0,
"height" INTEGER NOT NULL DEFAULT 0,
"alt" TEXT NOT NULL DEFAULT '',
"storageType" TEXT NOT NULL DEFAULT 'local',
"createdBy" TEXT NOT NULL DEFAULT 'system',
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL
);
-- CreateTable
CREATE TABLE "AuditLog" (
"id" TEXT NOT NULL PRIMARY KEY,
"module" TEXT NOT NULL,
"targetId" TEXT NOT NULL,
"action" TEXT NOT NULL,
"operator" TEXT NOT NULL,
"beforeData" TEXT NOT NULL DEFAULT '{}',
"afterData" TEXT NOT NULL DEFAULT '{}',
"ip" TEXT NOT NULL DEFAULT '',
"userAgent" TEXT NOT NULL DEFAULT '',
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- CreateIndex
CREATE UNIQUE INDEX "User_username_key" ON "User"("username");
-- CreateIndex
CREATE UNIQUE INDEX "ContentModel_code_key" ON "ContentModel"("code");
-- CreateIndex
CREATE INDEX "ContentItem_modelCode_idx" ON "ContentItem"("modelCode");
-- CreateIndex
CREATE INDEX "ContentItem_status_idx" ON "ContentItem"("status");
-- CreateIndex
CREATE UNIQUE INDEX "ContentItem_modelCode_slug_key" ON "ContentItem"("modelCode", "slug");
-- CreateIndex
CREATE UNIQUE INDEX "ContentZone_code_key" ON "ContentZone"("code");
-- CreateIndex
CREATE INDEX "ContentZone_pageCode_idx" ON "ContentZone"("pageCode");
-- CreateIndex
CREATE INDEX "AuditLog_module_idx" ON "AuditLog"("module");
-- CreateIndex
CREATE INDEX "AuditLog_targetId_idx" ON "AuditLog"("targetId");
-- CreateIndex
CREATE INDEX "AuditLog_createdAt_idx" ON "AuditLog"("createdAt");
+3
View File
@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (e.g., Git)
provider = "sqlite"
+125
View File
@@ -0,0 +1,125 @@
generator client {
provider = "prisma-client"
output = "../src/generated/prisma"
}
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
// ============ 用户与认证 ============
model User {
id String @id @default(cuid())
username String @unique
password String
nickname String @default("")
avatar String @default("")
email String @default("")
phone String @default("")
status Int @default(1) // 1=正常, 0=禁用
role String @default("admin") // admin | editor
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
// ============ 内容模型定义 ============
model ContentModel {
id String @id @default(cuid())
code String @unique
name String
description String @default("")
fields String // JSON: FieldDefinition[]
isPageType Boolean @default(false)
urlPattern String @default("")
hasVersions Boolean @default(false)
hasDraft Boolean @default(false)
icon String @default("")
sortOrder Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
items ContentItem[]
}
// ============ 内容条目 ============
model ContentItem {
id String @id @default(cuid())
modelId String
modelCode String
title String
slug String @default("")
status String @default("draft") // draft | published | archived
data String // JSON: Record<string, unknown>
version Int @default(1)
sortOrder Int @default(0)
publishedAt DateTime?
createdBy String @default("system")
updatedBy String @default("system")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
model ContentModel @relation(fields: [modelId], references: [id])
@@unique([modelCode, slug])
@@index([modelCode])
@@index([status])
}
// ============ 内容区域 ============
model ContentZone {
id String @id @default(cuid())
code String @unique
name String
description String @default("")
pageCode String @default("")
zoneKey String @default("")
allowedModels String @default("[]") // JSON: string[]
items String @default("[]") // JSON: ContentZoneItem[]
settings String @default("{}") // JSON: ContentZoneSettings
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([pageCode])
}
// ============ 媒体资源 ============
model MediaAsset {
id String @id @default(cuid())
name String
path String
url String
mimeType String
size Int @default(0)
width Int @default(0)
height Int @default(0)
alt String @default("")
storageType String @default("local") // local | oss | s3
createdBy String @default("system")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
// ============ 操作日志 ============
model AuditLog {
id String @id @default(cuid())
module String // model | item | zone | theme | media
targetId String
action String // create | update | delete | publish | unpublish
operator String
beforeData String @default("{}") // JSON
afterData String @default("{}") // JSON
ip String @default("")
userAgent String @default("")
createdAt DateTime @default(now())
@@index([module])
@@index([targetId])
@@index([createdAt])
}
+601
View File
@@ -0,0 +1,601 @@
import 'dotenv/config';
import { PrismaClient } from '../src/generated/prisma/client';
import { hashPassword } from '../src/lib/auth';
import { CONTENT_TYPE_CONFIGS } from '../src/lib/cms/content-types';
import { CASE_STUDIES } from '../src/lib/constants/cases';
import { NEWS } from '../src/lib/constants/news';
import { SERVICES } from '../src/lib/constants/services';
import { PRODUCTS } from '../src/lib/constants/products';
import { SOLUTIONS } from '../src/lib/constants/solutions';
import { STATS } from '../src/lib/constants/stats';
const prisma = new PrismaClient();
async function getModelId(code: string): Promise<string> {
const model = await prisma.contentModel.findUnique({ where: { code } });
if (!model) throw new Error(`模型 ${code} 未找到`);
return model.id;
}
async function seedItems(
modelCode: string,
items: Record<string, unknown>[],
options: { slugField?: string; titleField?: string } = {},
) {
const modelId = await getModelId(modelCode);
const slugField = options.slugField || 'slug';
const titleField = options.titleField || 'title';
for (let i = 0; i < items.length; i++) {
const item = items[i]!;
const slug = String(item[slugField] || item.id || `${modelCode}-${i}`);
const title = String(item[titleField] || `未命名 ${i + 1}`);
const { id, slug: _s, title: _t, ...data } = item as Record<string, unknown>;
await prisma.contentItem.upsert({
where: { modelCode_slug: { modelCode, slug } },
update: {
title,
data: JSON.stringify(data),
sortOrder: i,
status: 'published',
publishedAt: new Date(),
},
create: {
modelId,
modelCode,
title,
slug,
data: JSON.stringify(data),
sortOrder: i,
status: 'published',
publishedAt: new Date(),
createdBy: 'system',
updatedBy: 'system',
},
});
}
}
async function main() {
console.log('🌱 开始数据库初始化...');
// ============ 1. 创建管理员用户 ============
console.log('📝 创建管理员用户...');
const adminPassword = await hashPassword('admin123');
await prisma.user.upsert({
where: { username: 'admin' },
update: {},
create: {
username: 'admin',
password: adminPassword,
nickname: '管理员',
role: 'admin',
status: 1,
},
});
console.log(' ✅ 管理员: admin / admin123');
// ============ 2. 创建内容模型 ============
console.log('📝 创建内容模型...');
const modelConfigs = [
...Object.values(CONTENT_TYPE_CONFIGS),
{
model: {
code: 'case-study',
name: '案例研究',
description: '客户成功案例与实践分享',
isPageType: true,
urlPattern: '/cases/{slug}',
hasVersions: true,
hasDraft: true,
icon: 'briefcase',
fields: [
{ name: 'client', type: 'text', label: '客户名称', required: true },
{ name: 'industry', type: 'text', label: '所属行业', required: true },
{ name: 'companySize', type: 'text', label: '公司规模', required: true },
{ name: 'subtitle', type: 'text', label: '副标题', required: true },
{ name: 'challenge', type: 'textarea', label: '挑战', required: true },
{ name: 'solution', type: 'textarea', label: '解决方案', required: true },
{ name: 'result', type: 'textarea', label: '实施成果', required: true },
{ name: 'color', type: 'text', label: '主题色', defaultValue: 'brand' },
{ name: 'featured', type: 'boolean', label: '精选案例', defaultValue: false },
],
},
},
// === about-page ===
{
model: {
code: 'about-page',
name: '关于我们',
description: '公司介绍、核心价值、发展历程与资质认证信息',
isPageType: true,
urlPattern: '/about',
hasVersions: true,
hasDraft: true,
icon: 'building2',
fields: [
{ name: 'heroSubtitle', type: 'text', label: 'Hero 标签', required: true },
{ name: 'heroTitle', type: 'text', label: 'Hero 主标题', required: true },
{ name: 'heroDescription', type: 'textarea', label: 'Hero 描述', required: true },
{ name: 'whoWeAreTitle', type: 'text', label: '"我们是谁"标题' },
{ name: 'whoWeAreDescription', type: 'textarea', label: '"我们是谁"描述' },
{ name: 'promise', type: 'textarea', label: '承诺标语', required: true },
{ name: 'coreValues', type: 'array', label: '核心价值观', fields: [
{ name: 'number', type: 'text', label: '编号' },
{ name: 'title', type: 'text', label: '标题' },
{ name: 'description', type: 'textarea', label: '描述' },
]},
{ name: 'milestones', type: 'array', label: '发展历程', fields: [
{ name: 'year', type: 'text', label: '年份' },
{ name: 'title', type: 'text', label: '标题' },
{ name: 'description', type: 'textarea', label: '描述' },
{ name: 'highlight', type: 'text', label: '亮点' },
]},
{ name: 'keyMetrics', type: 'array', label: '关键指标', fields: [
{ name: 'value', type: 'text', label: '数值' },
{ name: 'label', type: 'text', label: '标签' },
{ name: 'sub', type: 'text', label: '副文本' },
]},
{ name: 'certifications', type: 'array', label: '资质认证', fields: [
{ name: 'name', type: 'text', label: '名称' },
{ name: 'desc', type: 'text', label: '描述' },
{ name: 'icon', type: 'text', label: '图标名称' },
]},
{ name: 'partners', type: 'array', label: '合作伙伴', fields: [
{ name: 'name', type: 'text', label: '名称' },
]},
{ name: 'ctaTitle', type: 'text', label: 'CTA 标题' },
{ name: 'ctaDescription', type: 'textarea', label: 'CTA 描述' },
],
},
},
// === team-page ===
{
model: {
code: 'team-page',
name: '团队介绍',
description: '团队实力、文化氛围、数据指标',
isPageType: true,
urlPattern: '/team',
hasVersions: true,
hasDraft: true,
icon: 'users',
fields: [
{ name: 'heroSubtitle', type: 'text', label: 'Hero 标签', required: true },
{ name: 'heroTitle', type: 'textarea', label: 'Hero 主标题', required: true },
{ name: 'heroDescription', type: 'textarea', label: 'Hero 描述', required: true },
{ name: 'stats', type: 'array', label: '统计指标', fields: [
{ name: 'value', type: 'number', label: '数值' },
{ name: 'suffix', type: 'text', label: '后缀' },
{ name: 'label', type: 'text', label: '标签' },
]},
{ name: 'strengths', type: 'array', label: '团队优势', fields: [
{ name: 'icon', type: 'text', label: '图标名称' },
{ name: 'number', type: 'text', label: '编号' },
{ name: 'title', type: 'text', label: '标题' },
{ name: 'description', type: 'textarea', label: '描述' },
]},
{ name: 'culture', type: 'array', label: '团队文化', fields: [
{ name: 'icon', type: 'text', label: '图标名称' },
{ name: 'number', type: 'text', label: '编号' },
{ name: 'title', type: 'text', label: '标题' },
{ name: 'description', type: 'textarea', label: '描述' },
]},
{ name: 'teamAboutTitle', type: 'text', label: '团队介绍标题' },
{ name: 'ctaTitle', type: 'text', label: 'CTA 标题' },
{ name: 'ctaDescription', type: 'textarea', label: 'CTA 描述' },
],
},
},
// === contact-page ===
{
model: {
code: 'contact-page',
name: '联系我们',
description: '联系信息、工作时间与服务承诺',
isPageType: true,
urlPattern: '/contact',
hasVersions: true,
hasDraft: true,
icon: 'phone',
fields: [
{ name: 'heroSubtitle', type: 'text', label: 'Hero 标签', required: true },
{ name: 'heroTitle', type: 'textarea', label: 'Hero 主标题', required: true },
{ name: 'heroDescription', type: 'textarea', label: 'Hero 描述', required: true },
{ name: 'companyName', type: 'text', label: '公司名称', required: true },
{ name: 'companyEmail', type: 'text', label: '联系邮箱', required: true },
{ name: 'companyAddress', type: 'text', label: '公司地址', required: true },
{ name: 'workHoursWeekday', type: 'text', label: '工作日描述' },
{ name: 'workHoursTime', type: 'text', label: '工作时间段' },
{ name: 'promises', type: 'array', label: '服务承诺', fields: [
{ name: 'text', type: 'textarea', label: '承诺内容' },
]},
{ name: 'ctaTitle', type: 'text', label: 'CTA 标题' },
{ name: 'ctaDescription', type: 'textarea', label: 'CTA 描述' },
],
},
},
// === legal-page ===
{
model: {
code: 'legal-page',
name: '法律条款',
description: '隐私政策和服务条款的页面内容',
isPageType: true,
urlPattern: '/legal/{slug}',
hasVersions: true,
hasDraft: true,
icon: 'file-text',
fields: [
{ name: 'pageTitle', type: 'text', label: '页面标题', required: true },
{ name: 'pageSubtitle', type: 'text', label: '页面副标题' },
{ name: 'sections', type: 'array', label: '内容章节', fields: [
{ name: 'title', type: 'text', label: '章节标题' },
{ name: 'content', type: 'textarea', label: '章节内容' },
]},
{ name: 'lastUpdated', type: 'text', label: '最后更新日期' },
],
},
},
];
for (let i = 0; i < modelConfigs.length; i++) {
const config = modelConfigs[i]!;
await prisma.contentModel.upsert({
where: { code: config.model.code },
update: {
name: config.model.name,
description: config.model.description,
fields: JSON.stringify(config.model.fields),
isPageType: config.model.isPageType,
urlPattern: config.model.urlPattern || '',
hasVersions: config.model.hasVersions,
hasDraft: config.model.hasDraft,
icon: config.model.icon || '',
sortOrder: i,
},
create: {
code: config.model.code,
name: config.model.name,
description: config.model.description,
fields: JSON.stringify(config.model.fields),
isPageType: config.model.isPageType,
urlPattern: config.model.urlPattern || '',
hasVersions: config.model.hasVersions,
hasDraft: config.model.hasDraft,
icon: config.model.icon || '',
sortOrder: i,
},
});
console.log(` ✅ 模型: ${config.model.name} (${config.model.code})`);
}
// ============ 3. 导入内容数据 ============
console.log('📝 导入案例研究...');
await seedItems('case-study', CASE_STUDIES as unknown as Record<string, unknown>[]);
console.log(`${CASE_STUDIES.length} 个案例`);
console.log('📝 导入新闻...');
await seedItems('news', NEWS as unknown as Record<string, unknown>[]);
console.log(`${NEWS.length} 条新闻`);
console.log('📝 导入服务...');
await seedItems('service', SERVICES as unknown as Record<string, unknown>[]);
console.log(`${SERVICES.length} 个服务`);
console.log('📝 导入产品...');
await seedItems('product', PRODUCTS as unknown as Record<string, unknown>[]);
console.log(`${PRODUCTS.length} 个产品`);
console.log('📝 导入解决方案...');
await seedItems('solution', SOLUTIONS as unknown as Record<string, unknown>[]);
console.log(`${SOLUTIONS.length} 个解决方案`);
// 数据指标需要特殊处理
console.log('📝 导入数据指标...');
const statList = Array.isArray(STATS) ? STATS : (STATS as { stats?: unknown[] }).stats || [];
const statModelId = await getModelId('stat-item');
for (let i = 0; i < statList.length; i++) {
const s = statList[i] as Record<string, unknown>;
const slug = `stat-${i}`;
const title = String(s.label || `指标 ${i + 1}`);
const { id, ...data } = s;
await prisma.contentItem.upsert({
where: { modelCode_slug: { modelCode: 'stat-item', slug } },
update: {
title,
data: JSON.stringify({ ...data, accentColor: s.color }),
sortOrder: i,
status: 'published',
publishedAt: new Date(),
},
create: {
modelId: statModelId,
modelCode: 'stat-item',
title,
slug,
data: JSON.stringify({ ...data, accentColor: s.color }),
sortOrder: i,
status: 'published',
publishedAt: new Date(),
createdBy: 'system',
updatedBy: 'system',
},
});
}
console.log(`${statList.length} 个数据指标`);
// ============ 4. 导入页面内容 ============
console.log('📝 导入关于页面...');
await seedItems('about-page', [
{
slug: 'default',
title: '关于我们',
heroSubtitle: 'About Us',
heroTitle: '12 年深耕\n500+ 企业\n的转型伙伴',
heroDescription: '我们不把项目完成当作终点。您的业务增长了吗?您的团队能力提升了吗?\n这才是我们真正在意的事。',
heroCtaText: '预约咨询',
heroCtaLink: '/contact',
heroSecondaryCtaText: '了解服务',
heroSecondaryCtaLink: '/services',
whoWeAreTitle: '我们是谁',
whoWeAreDescription: '我们坚持对行业趋势的深度研究,不追逐昙花一现的概念。每一次方案,都源于对您业务场景的洞察;每一次连接,都为了让技术真正服务于您的未来。',
promise: '不卖您用不上的技术,不说不懂业务的术语,不做路过就忘的一锤子买卖。',
coreValues: [
{ number: '01', title: '结果导向', description: '不以「项目上线」为终点,以「客户业务是否真正改善」为衡量标准。每一次交付,都追求可量化的价值。' },
{ number: '02', title: '长期主义', description: '不追逐风口,只做真正为客户创造价值的事。您的下一次难题,还愿意第一个想到我们——这才是我们追求的成功。' },
{ number: '03', title: '技术驱动', description: '用扎实的工程能力和行业经验赢得信任。技术不是炫技的工具,而是解决真实问题的手段。' },
],
milestones: [
{ year: '2014', title: '团队启航', description: '核心团队始于大型企业数字化转型项目,深耕 ERP、CRM 实施与定制开发。', highlight: '服务首批 20+ 企业客户' },
{ year: '2018', title: '业务扩展', description: '从单一实施服务向全链路咨询转型,建立战略咨询 + 技术落地的双轮驱动模式。', highlight: '客户突破 100 家' },
{ year: '2021', title: '产品化起步', description: '开始自研产品矩阵,从项目制向「产品+服务」双模式升级。', highlight: '3 款自研产品上线' },
{ year: '2023', title: '行业深耕', description: '聚焦制造、零售、医疗三大核心行业,建立行业解决方案库与最佳实践沉淀。', highlight: '客户续约率 95%+' },
{ year: '2026', title: '睿新致远成立', description: '四川睿新致远科技有限公司在成都正式成立,以全新品牌开启下一段征程。', highlight: '全新启航,步履不停' },
],
keyMetrics: [
{ value: '500+', label: '服务企业', sub: '六大行业覆盖' },
{ value: '200+', label: '专业顾问', sub: '平均行业经验 8 年' },
{ value: '98%', label: '客户满意度', sub: '97% 续约率' },
{ value: '12年', label: '行业深耕', sub: 'since 2014' },
],
certifications: [
{ name: 'ISO 27001', description: '信息安全管理体系认证', icon: 'Shield' },
{ name: 'ISO 9001', description: '质量管理体系认证', icon: 'Award' },
{ name: 'CMMI 3级', description: '软件能力成熟度认证', icon: 'Zap' },
{ name: '高新技术企业', description: '国家高新技术企业认定', icon: 'Trophy' },
],
partners: [
'SAP', 'Oracle', 'Salesforce', 'Microsoft',
'阿里云', '腾讯云', '华为云', '金蝶',
],
ctaTitle: '准备好开始了吗?',
ctaDescription: '无论您是想了解合作机会,还是希望深入探讨具体方案,都欢迎随时联系。',
},
]);
console.log(' ✅ 关于页面');
console.log('📝 导入团队页面...');
await seedItems('team-page', [
{
slug: 'default',
title: '团队介绍',
heroSubtitle: 'Our Team',
heroTitle: '既懂技术又懂业务的\n复合型团队',
heroDescription: '我们的成员不只是写代码的工程师——我们是能理解您业务场景、能和您一起想清楚问题的合作伙伴。',
heroCtaText: '与我们交流',
heroCtaLink: '/contact',
heroSecondaryCtaText: '了解公司',
heroSecondaryCtaLink: '/about',
stats: [
{ value: 12, suffix: '+', label: '年行业经验' },
{ value: 10, suffix: '+', label: '核心成员' },
{ value: 5, suffix: '+', label: '行业覆盖' },
],
strengths: [
{ icon: 'Shield', number: '01', title: '12 年+ 行业深耕', description: '核心团队长期从事技术咨询、企业数字化等领域,服务覆盖金融、制造、零售、政务、农业等多个行业。' },
{ icon: 'Building2', number: '02', title: '大型 IT 企业背景', description: '开发团队成员来自多个大型传统 IT 企业,具备扎实的工程能力、规范化的交付流程和严格的质量意识。' },
{ icon: 'Users', number: '03', title: '复合型技术团队', description: '既懂技术又懂业务,能够深入理解客户的真实场景和痛点,提供真正可落地的解决方案。' },
{ icon: 'Code', number: '04', title: '全栈技术能力', description: '从前端到后端、从云原生到数据智能、从移动端到物联网——应对各种复杂技术挑战。' },
{ icon: 'Target', number: '05', title: '结果导向交付', description: '不以"项目上线"为终点,以"客户业务是否真正改善"为衡量标准。每一个交付成果都追求可量化的业务价值。' },
],
culture: [
{ icon: 'Layers', number: '01', title: '扁平化协作', description: '没有冗长的汇报链条,每个人都有机会直接参与决策。信息透明,沟通高效。' },
{ icon: 'BookOpen', number: '02', title: '持续学习', description: '技术日新月异,我们保持对新技术的好奇。内部分享、技术沙龙、外部培训——学习是日常的一部分。' },
{ icon: 'Sparkles', number: '03', title: '客户成功', description: '我们的成就感来自客户的成功。客户的业务增长,就是我们最好的成绩单。' },
],
teamAboutTitle: '关于我们的团队',
teamAboutParagraphs: [
'我们的核心团队长期从事技术咨询、企业数字化等行业,拥有 12 年以上的深厚积累。',
'开发团队成员来自于多个大型传统 IT 企业,具备扎实的工程能力和规范化的交付经验。',
'我们相信,优秀的技术咨询不仅需要过硬的技术能力,更需要深入理解客户的业务场景和真实需求。每一位成员都是既懂技术又懂业务的复合型人才。',
],
strengthsSectionTitle: '我们的底气,从何而来',
strengthsSectionDescription: '不是自吹自擂,而是多年实战积累的真实能力',
cultureSectionTitle: '我们怎样工作',
cultureSectionDescription: '好的团队文化,最终体现在为客户创造的价值上',
ctaTitle: '想成为我们的一员?',
ctaDescription: '我们始终在寻找既懂技术又热爱业务的伙伴。如果您认同我们的理念,欢迎聊聊。',
ctaPrimaryText: '联系我们',
ctaPrimaryLink: '/contact',
ctaSecondaryText: '了解公司',
ctaSecondaryLink: '/about',
},
]);
console.log(' ✅ 团队页面');
console.log('📝 导入联系页面...');
await seedItems('contact-page', [
{
slug: 'default',
title: '联系我们',
heroSubtitle: 'Contact Us',
heroTitle: '随时欢迎\n聊一聊',
heroDescription: '工作日 2 小时内快速响应,首次咨询免费。\n没有销售压力,只是坐下来把问题理清楚。',
companyName: '四川睿新致远科技有限公司',
companyEmail: 'contact@novalon.cn',
companyAddress: '中国四川省成都市龙泉驿区幸福路12号',
workHoursWeekday: '周一至周五',
workHoursTime: '9:00 - 18:00',
promises: [
{ text: '工作日 2 小时内快速响应您的咨询' },
{ text: '提供免费的业务咨询和方案评估服务' },
{ text: '根据您的需求量身定制最优解决方案' },
],
formTitle: '发送消息',
ctaTitle: '还是想直接聊?',
ctaDescription: '表单填起来太麻烦?直接发邮件到 contact@novalon.cn,我们同样会快速回复。',
},
]);
console.log(' ✅ 联系页面');
console.log('📝 导入法律页面...');
await seedItems('legal-page', [
{
slug: 'default',
title: '法律条款',
pageSubtitle: '隐私政策与服务条款',
lastUpdated: '2026-02-26',
privacyPolicy: {
title: '隐私政策',
subtitle: '我们重视您的隐私,致力于保护您的个人信息安全',
lastUpdated: '2026-02-26',
sections: [
{ title: '引言', content: '四川睿新致远科技有限公司(以下简称"我们"、"公司")深知个人信息对您的重要性,并会尽全力保护您的个人信息安全可靠。我们致力于维持您对我们的信任,恪守以下原则,保护您的个人信息:权责一致原则、目的明确原则、选择同意原则、最少够用原则、确保安全原则、主体参与原则、公开透明原则等。本隐私政策适用于您通过四川睿新致远科技有限公司官方网站、移动应用、产品服务等渠道访问和使用我们的产品和服务时,我们收集和使用您的个人信息的情形。' },
{ title: '一、我们如何收集和使用您的个人信息', content: '我们会遵循正当、合法、必要的原则,仅为实现产品功能,向您提供服务之目的,收集和使用您的个人信息。\n\n1.1 我们收集的个人信息:\n• 账户信息:当您注册账户时,我们可能收集您的姓名、电子邮箱地址、手机号码、公司名称等。\n• 联系信息:当您通过联系表单或客服与我们沟通时,我们可能收集您的姓名、电子邮箱地址、手机号码、留言内容等。\n• 使用信息:我们可能收集您使用我们产品和服务的信息,包括访问时间、浏览记录、操作日志等。\n• 设备信息:我们可能收集您使用的设备信息,包括设备型号、操作系统、浏览器类型等。\n\n1.2 我们如何使用您的个人信息:\n• 提供产品和服务:向您提供我们的产品和服务,处理您的请求和订单。\n• 客户服务:与您联系,提供客户支持,回复您的咨询和反馈。\n• 改进产品和服务:分析使用情况,改进我们的产品和服务质量。\n• 安全保障:检测、预防和处理安全事件,保护您和我们的合法权益。\n• 法律合规:遵守法律法规要求,履行法律义务。' },
{ title: '二、我们如何共享、转让、公开披露您的个人信息', content: '2.1 共享:我们不会向其他任何公司、组织和个人分享您的个人信息,但以下情况除外:在获取明确同意的情况下分享;根据法律法规、法律程序、强制性的行政或司法要求所必须的情况进行提供;在涉及合并、收购或破产清算时,如涉及到个人信息转让,我们会要求新的持有您个人信息的公司、组织继续受本隐私政策的约束。\n\n2.2 转让:我们不会将您的个人信息转让给任何公司、组织和个人,但以下情况除外:在获取明确同意的情况下转让;根据适用的法律法规、法律程序、强制性的行政或司法要求所必须的情况进行提供;在涉及合并、收购或破产清算时。\n\n2.3 公开披露:我们仅会在以下情况下公开披露您的个人信息:获得您的明确同意;基于法律法规或法律程序的要求;在涉及合并、收购或破产清算时;为保护我们或他人的合法权益免受损害。' },
{ title: '三、我们如何保护您的个人信息', content: '我们已使用符合业界标准的安全防护措施保护您提供的个人信息,防止数据遭到未经授权访问、公开披露、使用、修改、损坏或丢失。我们会采取一切合理可行的措施,保护您的个人信息:\n• 使用加密技术确保数据传输和存储安全。\n• 限制访问权限,仅授权人员可访问个人信息。\n• 定期进行安全审计和风险评估。\n• 建立数据备份和恢复机制。\n• 制定应急响应预案,及时处理安全事件。' },
{ title: '四、您的权利', content: '按照中国相关的法律、法规、标准,以及其他国家、地区的通行做法,我们保障您对自己的个人信息行使以下权利:访问您的个人信息;更正您的个人信息;删除您的个人信息;改变您授权同意的范围;注销您的账户;获取您的个人信息副本。如您需要行使上述权利,请通过本隐私政策提供的联系方式与我们联系。' },
{ title: '五、未成年人保护', content: '我们非常重视对未成年人个人信息的保护。如果您是18周岁以下的未成年人,在使用我们的产品和服务前,应事先取得您家长或法定监护人的同意。如您是未成年人的监护人,当您对您所监护的未成年人的个人信息处理存在疑问时,请通过本隐私政策提供的联系方式与我们联系。' },
{ title: '六、隐私政策的更新', content: '我们可能适时更新本隐私政策的条款,该等更新构成本隐私政策的一部分。如该等更新造成您在本隐私政策下权利的实质减少,我们将在更新生效前通过在主页上显著位置提示或向您发送电子邮件或其他方式通知您,在该种情况下,若您继续使用我们的服务,即表示同意受经修订的本隐私政策的约束。' },
{ title: '七、Cookie 和网站分析工具', content: '7.1 Cookie 使用说明:我们使用 Cookie 和类似技术来提供、保护和改进我们的服务。Cookie 是存储在您设备上的小型文本文件,帮助我们识别您的设备、记住您的偏好设置。\n\nCookie类型及用途:\n• 必要Cookie:网站基本功能运行,会话期间,必需\n• 分析Cookie:了解网站使用情况改进服务,14个月,非必需\n• 营销Cookie:个性化广告(当前未使用),非必需\n\n7.2 Google Analytics 使用说明:我们使用 Google Analytics 4(由 Google LLC 提供)分析网站使用情况。收集数据包括:访问的页面和停留时间;设备类型、浏览器类型;地理位置(国家/城市级别,IP地址已匿名化);访问来源。\n已采取的保护措施:IP地址匿名化;数据保留期限设为14个月;禁用广告个性化功能;禁用Google信号;不与Google其他服务共享数据用于广告目的。\n\n7.3 您的选择:您可以在首次访问时选择接受或拒绝分析Cookie;可以点击"Cookie设置"按钮随时更改偏好;可以通过浏览器设置删除或阻止Cookie。\n\n7.4 数据删除请求:如您希望删除我们持有的您的个人数据,请通过 privacy@novalon.cn 联系我们。联系地址:中国四川省成都市龙泉驿区幸福路12号。' },
{ title: '八、如何联系我们', content: '如果您对本隐私政策有任何疑问、意见或建议,或需要行使您的权利,请通过以下方式与我们联系:\n\n公司名称:四川睿新致远科技有限公司\n联系邮箱:contact@novalon.cn\n隐私邮箱:privacy@novalon.cn\n联系地址:中国四川省成都市龙泉驿区幸福路12号' },
],
},
termsOfService: {
title: '服务条款',
subtitle: '请仔细阅读以下服务条款,使用我们的服务即表示您同意这些条款',
lastUpdated: '2026-02-26',
sections: [
{ title: '引言', content: '欢迎使用四川睿新致远科技有限公司(以下简称"我们"、"公司")提供的产品和服务。在使用我们的产品和服务之前,请您仔细阅读并理解本服务条款。如果您不同意本服务条款的任何内容,请停止使用我们的产品和服务。本服务条款是您与四川睿新致远科技有限公司之间就使用我们的产品和服务所订立的协议。我们有权根据需要不时修改本服务条款,修改后的条款一旦公布即代替原条款,恕不另行通知。' },
{ title: '一、服务内容', content: '我们提供的产品和服务包括但不限于:软件开发、云服务、数据分析、信息安全、企业级软件产品(如ERP、CRM、后台管理系统、BI等)等。具体服务内容以我们官方网站或产品文档为准。我们保留随时修改、暂停或终止部分或全部服务的权利,无需事先通知。对于因服务修改、暂停或终止而给您造成的任何损失,我们不承担任何责任,除非法律另有规定。' },
{ title: '二、用户注册与账户', content: '2.1 注册资格:您确认,在您完成注册程序或以其他方式实际使用本服务时,您应当是具备完全民事权利能力和完全民事行为能力的自然人、法人或其他组织。若您不具备前述主体资格,则您及您的监护人应承担因此而导致的一切后果,且我们有权注销或永久冻结您的账户。\n\n2.2 账户安全:您有责任维护您账户的安全性和保密性。您不得向任何第三方泄露您的账户信息,也不得与他人共享您的账户。如果您发现任何未经授权使用您账户的情况,应立即通知我们。您对您账户下发生的所有活动负责。我们对因您未能维护账户安全而造成的任何损失不承担责任。' },
{ title: '三、用户行为规范', content: '在使用我们的产品和服务时,您同意遵守以下行为规范:\n• 遵守所有适用的法律法规和本服务条款。\n• 不得利用我们的产品和服务进行任何违法或不当活动。\n• 不得干扰或破坏我们的产品和服务或与我们的产品和服务相连的服务器和网络。\n• 不得上传、传播或存储任何违法、有害、威胁、诽谤、骚扰、侵权或其他不当内容。\n• 不得侵犯他人的知识产权、隐私权或其他合法权益。\n• 不得进行任何形式的商业欺诈或诈骗活动。\n• 不得恶意收集或获取其他用户的信息。\n如果我们认定您违反了本服务条款或任何适用法律,我们有权在不事先通知的情况下,暂停或终止您的账户,并拒绝您现在或将来使用我们的产品和服务。' },
{ title: '四、知识产权', content: '我们的产品和服务中包含的所有内容,包括但不限于软件、设计、文字、图片、音频、视频、商标、服务标识等,均受著作权法、商标法、专利法或其他适用法律的保护。除非另有明确说明,我们拥有或持有我们产品和服务中所有内容的所有知识产权。您不得以任何形式复制、修改、传播、展示、执行、创作衍生作品、转让、出售或以其他方式使用这些内容,除非获得我们的明确书面许可。您在使用我们的产品和服务时产生的任何内容,您仍保留其知识产权,但您授予我们全球性、非独占性、免版税的许可,以使用、复制、修改、传播、展示和执行这些内容,仅限于提供和改进我们的产品和服务。' },
{ title: '五、服务费用与支付', content: '5.1 费用标准:我们的产品和服务可能需要支付费用。具体费用标准以我们官方网站或产品文档公布的价格为准。我们保留随时调整价格的权利,调整后的价格适用于调整后的新订单或续费。\n\n5.2 支付方式:我们接受多种支付方式,包括但不限于银行转账、支付宝、微信支付等。\n\n5.3 退款政策:除非另有明确说明,我们提供的产品和服务一经售出,不予退款。如因我们的原因导致产品或服务无法正常使用,我们将根据实际情况提供相应的补偿或解决方案。' },
{ title: '六、免责声明', content: '我们的产品和服务按"现状"和"可用"基础提供,不提供任何明示或暗示的保证,包括但不限于对适销性、适用性、非侵权性或准确性、可靠性的保证。\n\n我们不对以下情况承担责任:\n• 因您违反本服务条款或任何适用法律而导致的任何损失或损害。\n• 因不可抗力、网络故障、设备故障等不可控因素导致的服务中断或数据丢失。\n• 因第三方服务或内容导致的任何损失或损害。\n• 因您使用或无法使用我们的产品和服务而导致的任何间接、附带、特殊或后果性损害。' },
{ title: '七、服务终止', content: '您可以随时停止使用我们的产品和服务,并注销您的账户。我们也有权在不事先通知的情况下,因以下原因暂停或终止您的账户:\n• 您违反本服务条款或任何适用法律。\n• 我们出于安全、法律或商业考虑,认为有必要终止您的账户。\n• 您长时间未使用您的账户。\n• 我们停止提供相关的产品和服务。' },
{ title: '八、争议解决', content: '本服务条款的订立、执行、解释及争议解决均适用中华人民共和国法律。如就本服务条款发生任何争议,双方应首先通过友好协商解决;协商不成的,任何一方均可向公司所在地有管辖权的人民法院提起诉讼。' },
{ title: '九、其他条款', content: '• 本服务条款构成您与我们就使用我们的产品和服务所达成的完整协议,取代之前的所有口头或书面协议。\n• 如本服务条款的任何条款被认定为无效或不可执行,该条款应被限制或排除,以使其有效和可执行,其余条款继续有效。\n• 我们未行使或延迟行使本服务条款项下的任何权利或规定,不构成对该权利或规定的放弃。\n• 本服务条款的标题仅为方便而设,不影响本服务条款的解释。' },
{ title: '十、联系我们', content: '如果您对本服务条款有任何疑问、意见或建议,请通过以下方式与我们联系:\n\n公司名称:四川睿新致远科技有限公司\n联系邮箱:contact@novalon.cn\n联系地址:中国四川省成都市龙泉驿区幸福路12号' },
],
},
},
]);
console.log(' ✅ 法律页面');
// ============ 5. 创建默认内容区域 ============
console.log('📝 创建默认内容区域...');
const defaultZones = [
{
code: 'home-stats',
name: '首页 - 数据指标',
pageCode: 'home',
zoneKey: 'stats',
allowedModels: ['stat-item'],
items: statList.slice(0, 4).map((_: unknown, i: number) => ({
itemId: `stat-${i}`,
sortOrder: i,
variant: 'default',
})),
},
{
code: 'home-services',
name: '首页 - 服务展示',
pageCode: 'home',
zoneKey: 'services',
allowedModels: ['service'],
items: SERVICES.slice(0, 6).map((_: unknown, i: number) => {
const s = _ as Record<string, unknown>;
return { itemId: String(s.id || `service-${i}`), sortOrder: i, variant: 'default' };
}),
},
{
code: 'home-solutions',
name: '首页 - 解决方案',
pageCode: 'home',
zoneKey: 'solutions',
allowedModels: ['solution'],
items: SOLUTIONS.slice(0, 6).map((_: unknown, i: number) => {
const s = _ as Record<string, unknown>;
return { itemId: String(s.id || `solution-${i}`), sortOrder: i, variant: 'default' };
}),
},
{
code: 'home-cases',
name: '首页 - 精选案例',
pageCode: 'home',
zoneKey: 'cases',
allowedModels: ['case-study'],
items: CASE_STUDIES.filter((cs) => cs.featured).slice(0, 3).map((cs, i: number) => ({
itemId: cs.id,
sortOrder: i,
variant: 'default',
})),
},
{
code: 'home-news',
name: '首页 - 新闻动态',
pageCode: 'home',
zoneKey: 'news',
allowedModels: ['news'],
items: NEWS.slice(0, 3).map((_: unknown, i: number) => {
const n = _ as Record<string, unknown>;
return { itemId: String(n.id || `news-${i}`), sortOrder: i, variant: 'default' };
}),
},
];
for (const zone of defaultZones) {
await prisma.contentZone.upsert({
where: { code: zone.code },
update: {
name: zone.name,
pageCode: zone.pageCode,
zoneKey: zone.zoneKey,
allowedModels: JSON.stringify(zone.allowedModels),
items: JSON.stringify(zone.items),
},
create: {
code: zone.code,
name: zone.name,
pageCode: zone.pageCode,
zoneKey: zone.zoneKey,
allowedModels: JSON.stringify(zone.allowedModels),
items: JSON.stringify(zone.items),
},
});
console.log(` ✅ 区域: ${zone.name}`);
}
// ============ 统计 ============
const itemCount = await prisma.contentItem.count();
const modelCount = await prisma.contentModel.count();
const zoneCount = await prisma.contentZone.count();
console.log('\n🎉 数据库初始化完成!');
console.log(` 📊 内容模型: ${modelCount}`);
console.log(` 📄 内容条目: ${itemCount}`);
console.log(` 🗂️ 内容区域: ${zoneCount}`);
console.log(` 👤 管理员: admin / admin123`);
}
main()
.catch((e) => {
console.error('❌ 种子数据初始化失败:', e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});