Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions src/app/api/builder/pages/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { NextRequest, NextResponse } from "next/server";
import prisma from "@/lib/database/dbClient";
import { updatePageSchema } from "@/lib/builder/schemas";
import { componentDefaults } from "@/lib/builder/defaults";
import { z } from "zod";

export async function GET(
Expand Down Expand Up @@ -49,6 +50,25 @@ export async function PUT(
}
}

// 合并组件默认值,确保校验通过
if (Array.isArray(processedBody.components)) {
processedBody.components = processedBody.components.map((comp: any) => {
const componentType = comp.type as keyof typeof componentDefaults;
const defaults = { ...componentDefaults[componentType] };
return {
...comp,
props: {
...defaults.props,
...comp.props,
},
style: {
...defaults.style,
...comp.style,
},
};
});
}

const validatedData = updatePageSchema.parse(processedBody);

const existingPage = await prisma.page.findUnique({
Expand Down
71 changes: 50 additions & 21 deletions src/app/p/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,52 +8,81 @@ interface PublishedPageProps {
params: Promise<{ slug: string }>;
}

export async function generateMetadata({ params }: PublishedPageProps): Promise<Metadata> {
const { slug } = await params;
// 页面数据缓存
let pageCache: Map<string, { page: any; components: ComponentConfig[]; timestamp: number }> = new Map();
const CACHE_DURATION = 5 * 60 * 1000; // 5分钟缓存

// 组件默认值缓存
let componentDefaultsCache: Map<string, ComponentConfig> = new Map();

async function getPageData(slug: string) {
const cached = pageCache.get(slug);
const now = Date.now();

if (cached && now - cached.timestamp < CACHE_DURATION) {
return cached;
}

const page = await prisma.page.findUnique({
where: { slug },
select: {
id: true,
title: true,
slug: true,
description: true,
components: true,
},
});

if (!page) {
return null;
}

let components: ComponentConfig[] = [];
if (typeof page.components === "string") {
try {
components = JSON.parse(page.components) as ComponentConfig[];
} catch {
components = [];
}
} else if (Array.isArray(page.components)) {
components = JSON.parse(JSON.stringify(page.components)) as ComponentConfig[];
}

const result = { page, components, timestamp: now };
pageCache.set(slug, result);
return result;
}

export async function generateMetadata({ params }: PublishedPageProps): Promise<Metadata> {
const { slug } = await params;
const data = await getPageData(slug);

if (!data) {
return {
title: "页面不存在",
};
}

return {
title: page.title,
description: page.description || undefined,
title: data.page.title,
description: data.page.description || undefined,
};
}

export const revalidate = 60;

export default async function PublishedPage({ params }: PublishedPageProps) {
const { slug } = await params;
const data = await getPageData(slug);

const page = await prisma.page.findUnique({
where: { slug },
});

if (!page) {
if (!data) {
notFound();
}

let components: ComponentConfig[] = [];
if (typeof page.components === "string") {
try {
components = JSON.parse(page.components) as ComponentConfig[];
} catch {
components = [];
}
} else if (Array.isArray(page.components)) {
components = JSON.parse(JSON.stringify(page.components)) as ComponentConfig[];
}

return (
<main className="min-h-screen bg-white">
{components.map((component) => (
{data.components.map((component) => (
<PublishedComponentRenderer
key={component.id}
component={component}
Expand Down
145 changes: 16 additions & 129 deletions src/components/BuilderComponents/ComponentRenderer.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,12 @@
"use client";

import type { ComponentConfig } from "@/types/builder";
import { useBuilderContext } from "@/components/Builder/BuilderProvider";
import { componentDefaults } from "@/lib/builder/defaults";
import { HeroComponent } from "./HeroComponent";
import { AvatarComponent } from "./AvatarComponent";
import { TextComponent } from "./TextComponent";
import { ButtonComponent } from "./ButtonComponent";
import { GalleryComponent } from "./GalleryComponent";
import { ContactComponent } from "./ContactComponent";
import { SocialComponent } from "./SocialComponent";
import { DividerComponent } from "./DividerComponent";
import { CardComponent } from "./CardComponent";
import { SectionComponent } from "./SectionComponent";
/**
* 已弃用:请使用 UnifiedComponentRenderer 替代
* 此文件仅用于向后兼容
*/

import type { ComponentConfig } from '@/types/builder';
import { UnifiedComponentRenderer } from '@/components/UnifiedComponents';

interface ComponentRendererProps {
component: ComponentConfig;
Expand All @@ -22,123 +16,16 @@ interface ComponentRendererProps {
totalCount: number;
}

function isContentEmptyOrDefault(component: ComponentConfig): boolean {
const defaultConfig = componentDefaults[component.type];
if (!defaultConfig) return false;

const props = component.props;
const defaultProps = defaultConfig.props;

for (const key of Object.keys(props)) {
const value = props[key];
const defaultValue = defaultProps[key];

if (typeof value === "string" && (!value || value.trim() === "")) {
return true;
}

if (typeof value === "string" && typeof defaultValue === "string") {
if (value !== defaultValue && value.trim() !== "") {
return false;
}
} else if (JSON.stringify(value) !== JSON.stringify(defaultValue)) {
if (Array.isArray(value) && value.length > 0) {
return false;
}
}
}

return true;
}

export function ComponentRenderer({
component,
isSelected,
onSelect,
index,
totalCount,
}: ComponentRendererProps) {
const { moveComponentUp, moveComponentDown } = useBuilderContext();
const isEmptyOrDefault = isContentEmptyOrDefault(component);

const baseClassName = `relative p-4 border-2 cursor-pointer transition-all ${
isSelected
? "border-blue-500 bg-blue-50"
: isEmptyOrDefault
? "border-dashed border-orange-400 bg-orange-50/30"
: "border-transparent hover:border-gray-300"
}`;

const handleClick = (e: React.MouseEvent) => {
e.stopPropagation();
onSelect(component.id);
};

const handleMoveUp = (e: React.MouseEvent) => {
e.stopPropagation();
if (index > 0) {
moveComponentUp(component.id);
}
};

const handleMoveDown = (e: React.MouseEvent) => {
e.stopPropagation();
if (index < totalCount - 1) {
moveComponentDown(component.id);
}
};

const commonProps = {
component,
isSelected,
onSelect: handleClick,
};

component, isSelected, onSelect, index, totalCount }: ComponentRendererProps) {
return (
<div className={baseClassName} onClick={handleClick}>
{isSelected && (
<div className="absolute right-2 top-2 z-10 flex gap-1">
<button
onClick={handleMoveUp}
disabled={index === 0}
className={`rounded bg-white px-2 py-1 text-xs shadow ${
index === 0
? "cursor-not-allowed text-gray-300"
: "text-gray-700 hover:bg-gray-100"
}`}
title="上移"
>
</button>
<button
onClick={handleMoveDown}
disabled={index === totalCount - 1}
className={`rounded bg-white px-2 py-1 text-xs shadow ${
index === totalCount - 1
? "cursor-not-allowed text-gray-300"
: "text-gray-700 hover:bg-gray-100"
}`}
title="下移"
>
</button>
</div>
)}
{isEmptyOrDefault && !isSelected && (
<div className="absolute left-2 top-2 z-10 rounded bg-orange-100 px-2 py-0.5 text-xs text-orange-600">
请编辑内容
</div>
)}
{component.type === "hero" && <HeroComponent {...commonProps} />}
{component.type === "avatar" && <AvatarComponent {...commonProps} />}
{component.type === "text" && <TextComponent {...commonProps} />}
{component.type === "button" && <ButtonComponent {...commonProps} />}
{component.type === "gallery" && <GalleryComponent {...commonProps} />}
{component.type === "contact" && <ContactComponent {...commonProps} />}
{component.type === "social" && <SocialComponent {...commonProps} />}
{component.type === "divider" && <DividerComponent {...commonProps} />}
{component.type === "card" && <CardComponent {...commonProps} />}
{component.type === "section" && <SectionComponent {...commonProps} />}
</div>
<UnifiedComponentRenderer
component={component}
isEditorMode={true}
isSelected={isSelected}
onSelect={onSelect}
index={index}
totalCount={totalCount}
/>
);
}
Loading