- 品牌叙事内核(价值主张/定位/承诺/三支柱/语气)下沉 site-config,经 SiteConfigProvider 注入全站 - 新增 page-copy 内容模型承载结构性文案(章节标题/眉标/描述/CTA/空状态),覆盖首页+服务/方案/产品/案例/新闻列表页,「CMS 优先 + 硬编码兜底」不白屏 - 首页「首批客户共创计划」板块(earlyAccessTitle/CtaLabel/Status 驱动)替代单行标签,如实呈现共创/内测/授权公开三档状态 - 关于页资质区数据空时如实展示「建设中」空态 - 零编造:不虚构客户/数据/资质
223 lines
6.2 KiB
TypeScript
223 lines
6.2 KiB
TypeScript
/**
|
||
* 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 }));
|
||
} |