Files
novalon-website/src/lib/cms/data-server.ts
T
zhangxiang 337e1e60c0 feat(cms): 品牌叙事与结构性文案全站 CMS 化并补齐信任证据阶段 0
- 品牌叙事内核(价值主张/定位/承诺/三支柱/语气)下沉 site-config,经 SiteConfigProvider 注入全站
- 新增 page-copy 内容模型承载结构性文案(章节标题/眉标/描述/CTA/空状态),覆盖首页+服务/方案/产品/案例/新闻列表页,「CMS 优先 + 硬编码兜底」不白屏
- 首页「首批客户共创计划」板块(earlyAccessTitle/CtaLabel/Status 驱动)替代单行标签,如实呈现共创/内测/授权公开三档状态
- 关于页资质区数据空时如实展示「建设中」空态
- 零编造:不虚构客户/数据/资质
2026-08-20 09:25:12 +08:00

223 lines
6.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* CMS 服务端数据访问层
* 从 Prisma 数据库直接读取已发布内容,供页面组件使用
*/
import { prisma } from '@/lib/db';
import { cache } from 'react';
import type { ContentItem, ContentStatus } from './types';
// ============ 类型转换 ============
function toContentItem(item: Record<string, unknown>): ContentItem {
return {
id: item.id as string,
modelId: item.modelId as string,
modelCode: item.modelCode as string,
title: item.title as string,
slug: (item.slug as string | null) ?? undefined,
locale: (item.locale as string | null) ?? 'zh-CN',
status: item.status as ContentStatus,
data: JSON.parse(item.data as string),
version: item.version as number,
sortOrder: (item.sortOrder as number | null) ?? undefined,
publishedAt: (item.publishedAt as Date | null)?.toISOString(),
createdBy: item.createdBy as string,
updatedBy: item.updatedBy as string,
createdAt: (item.createdAt as Date).toISOString(),
updatedAt: (item.updatedAt as Date).toISOString(),
};
}
// ============ 内容条目查询 ============
/**
* 获取指定模型的已发布内容列表
*/
export const getPublishedItems = cache(async (modelCode: string): Promise<ContentItem[]> => {
const items = await prisma.contentItem.findMany({
where: { modelCode, status: 'published' },
orderBy: { sortOrder: 'asc' },
});
return items.map((item) => toContentItem(item as unknown as Record<string, unknown>));
});
/**
* 根据 slug 获取单条已发布内容
*/
export const getPublishedItemBySlug = cache(async (modelCode: string, slug: string): Promise<ContentItem | null> => {
const item = await prisma.contentItem.findFirst({
where: { modelCode, slug, status: 'published' },
});
if (!item) return null;
return toContentItem(item as unknown as Record<string, unknown>);
});
/**
* 根据 ID 获取单条内容(不限状态)
*/
export const getItemById = cache(async (id: string): Promise<ContentItem | null> => {
const item = await prisma.contentItem.findUnique({ where: { id } });
if (!item) return null;
return toContentItem(item as unknown as Record<string, unknown>);
});
// ============ 内容区域查询 ============
/**
* 获取指定页面的内容区域配置
*/
export const getPageZones = cache(async (pageCode: string) => {
const zones = await prisma.contentZone.findMany({
where: { pageCode },
});
return zones.map((z) => ({
...z,
allowedModels: JSON.parse(z.allowedModels),
items: JSON.parse(z.items),
settings: JSON.parse(z.settings),
}));
});
/**
* 获取单个内容区域
*/
export const getZone = cache(async (code: string) => {
const zone = await prisma.contentZone.findUnique({ where: { code } });
if (!zone) return null;
return {
...zone,
allowedModels: JSON.parse(zone.allowedModels),
items: JSON.parse(zone.items),
settings: JSON.parse(zone.settings),
};
});
// ============ 内容模型查询 ============
/**
* 获取所有内容模型
*/
export const getContentModels = cache(async () => {
const models = await prisma.contentModel.findMany({
orderBy: { sortOrder: 'asc' },
});
return models.map((m) => ({
...m,
fields: JSON.parse(m.fields),
}));
});
// ============ 便捷函数:按业务类型获取 ============
export async function getCases() {
return getPublishedItems('case-study');
}
export async function getNews() {
return getPublishedItems('news');
}
export async function getServices() {
return getPublishedItems('service');
}
export async function getProducts() {
return getPublishedItems('product');
}
export async function getSolutions() {
return getPublishedItems('solution');
}
export async function getStats() {
return getPublishedItems('stat-item');
}
export async function getHeroBanners() {
return getPublishedItems('hero-banner');
}
export async function getCaseBySlug(slug: string) {
return getPublishedItemBySlug('case-study', slug);
}
export async function getNewsBySlug(slug: string) {
return getPublishedItemBySlug('news', slug);
}
export async function getServiceBySlug(slug: string) {
return getPublishedItemBySlug('service', slug);
}
export async function getProductBySlug(slug: string) {
return getPublishedItemBySlug('product', slug);
}
export async function getSolutionBySlug(slug: string) {
return getPublishedItemBySlug('solution', slug);
}
/**
* 获取指定页面的结构性文案配置(page-copy 模型,按页面 slug
*/
export async function getPageCopy(pageCode: string) {
return getPublishedItemBySlug('page-copy', pageCode);
}
/**
* 获取首页结构性文案配置(章节标题/信任信号/叙事情节/CTA 的 CMS 覆盖)
*/
export async function getHomePageCopy() {
return getPageCopy('home');
}
export async function getHomePageZones() {
return getPageZones('home');
}
/**
* 获取首页各 Zone 并解析引用的已发布内容条目
* 返回以 zoneKey 为键的内容条目数组映射
*/
export async function getResolvedHomeZones(): Promise<Record<string, ContentItem[]>> {
const zones = await getHomePageZones();
const result: Record<string, ContentItem[]> = {};
if (zones.length === 0) {
return result;
}
const itemIds = zones
.flatMap((z) => z.items.map((i: { itemId?: string }) => i.itemId).filter(Boolean));
if (itemIds.length > 0) {
const items = await prisma.contentItem.findMany({
where: { id: { in: itemIds as string[] }, status: 'published' },
});
const itemMap = new Map(
items.map((item) => [item.id, toContentItem(item as unknown as Record<string, unknown>)]),
);
for (const zone of zones) {
const key = zone.zoneKey || zone.code;
result[key] = zone.items
.map((zi: { itemId?: string }) => (zi.itemId ? itemMap.get(zi.itemId) : undefined))
.filter(Boolean) as ContentItem[];
}
}
return result;
}
// ============ 静态生成辅助 ============
/**
* 获取所有已发布内容的 slug 列表(用于 generateStaticParams
*/
export async function getAllPublishedSlugs(modelCode: string) {
const items = await prisma.contentItem.findMany({
where: { modelCode, status: 'published' },
select: { slug: true },
});
return items.filter((i) => i.slug).map((i) => ({ slug: i.slug }));
}