feat(cms): integrate global layout with CMS and document decision
- Move site-config and navigation to CMS-backed SiteConfigProvider - Keep static constants as fallback for dev and degraded modes - Add ADR-0005 recording the global layout CMS strategy Refs ADR-0005
This commit is contained in:
+15
-13
@@ -15,6 +15,8 @@ import { BackToTop } from "@/components/ui/back-to-top";
|
||||
import { ClientLayout } from "@/components/layout/client-layout";
|
||||
import { getPublishedItems } from "@/lib/cms/data-server";
|
||||
import { SiteConfigProvider, type SiteConfig, type NavigationItem, type MegaDropdownGroup } from "@/lib/site-config";
|
||||
import { COMPANY_INFO } from "@/lib/constants/company";
|
||||
import { NAVIGATION_V2, MEGA_DROPDOWN_DATA } from "@/lib/constants/navigation";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
metadataBase: new URL("https://www.novalon.cn"),
|
||||
@@ -90,19 +92,19 @@ export default async function RootLayout({
|
||||
const configData = configItems[0]?.data as Record<string, unknown> | undefined;
|
||||
|
||||
const siteConfig: SiteConfig = {
|
||||
name: (configData?.name as string) || '',
|
||||
shortName: (configData?.shortName as string) || '',
|
||||
displayName: (configData?.displayName as string) || '',
|
||||
slogan: (configData?.slogan as string) || '',
|
||||
description: (configData?.description as string) || '',
|
||||
founded: (configData?.founded as string) || '',
|
||||
location: (configData?.location as string) || '',
|
||||
email: (configData?.email as string) || '',
|
||||
address: (configData?.address as string) || '',
|
||||
icp: (configData?.icp as string) || '',
|
||||
police: (configData?.police as string) || '',
|
||||
mainNav: (navData?.mainNav as NavigationItem[]) || [],
|
||||
megaDropdown: (navData?.megaDropdown as Record<string, MegaDropdownGroup[]>) || {},
|
||||
name: (configData?.name as string) || COMPANY_INFO.name,
|
||||
shortName: (configData?.shortName as string) || COMPANY_INFO.shortName,
|
||||
displayName: (configData?.displayName as string) || COMPANY_INFO.displayName,
|
||||
slogan: (configData?.slogan as string) || COMPANY_INFO.slogan,
|
||||
description: (configData?.description as string) || COMPANY_INFO.description,
|
||||
founded: (configData?.founded as string) || COMPANY_INFO.founded,
|
||||
location: (configData?.location as string) || COMPANY_INFO.location,
|
||||
email: (configData?.email as string) || COMPANY_INFO.email,
|
||||
address: (configData?.address as string) || COMPANY_INFO.address,
|
||||
icp: (configData?.icp as string) || COMPANY_INFO.icp,
|
||||
police: (configData?.police as string) || COMPANY_INFO.police,
|
||||
mainNav: (navData?.mainNav as NavigationItem[]) || NAVIGATION_V2,
|
||||
megaDropdown: (navData?.megaDropdown as Record<string, MegaDropdownGroup[]>) || MEGA_DROPDOWN_DATA,
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useRef, useEffect } from 'react';
|
||||
import { ChevronDown, Lock, Sparkles } from 'lucide-react';
|
||||
import { StaticLink } from '@/components/ui/static-link';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import type { MegaDropdownGroup } from '@/lib/constants/navigation';
|
||||
import type { MegaDropdownGroup } from '@/lib/site-config';
|
||||
|
||||
interface MegaDropdownProps {
|
||||
label: string;
|
||||
|
||||
@@ -233,17 +233,19 @@ const solutionFields: FieldDefinition[] = [
|
||||
|
||||
const heroBannerFields: FieldDefinition[] = [
|
||||
{
|
||||
name: 'heading',
|
||||
name: 'headingTop',
|
||||
type: 'text',
|
||||
label: '主标题',
|
||||
label: '顶部小标题',
|
||||
required: true,
|
||||
description: '显示在主标题上方的引导语',
|
||||
ui: { width: 'full' },
|
||||
},
|
||||
{
|
||||
name: 'subheading',
|
||||
type: 'textarea',
|
||||
label: '副标题',
|
||||
name: 'headingBottom',
|
||||
type: 'text',
|
||||
label: '主标题',
|
||||
required: true,
|
||||
description: 'Hero 区核心大标题',
|
||||
ui: { width: 'full' },
|
||||
},
|
||||
{
|
||||
@@ -252,36 +254,48 @@ const heroBannerFields: FieldDefinition[] = [
|
||||
label: '描述文本',
|
||||
},
|
||||
{
|
||||
name: 'primaryCtaText',
|
||||
name: 'ctaLabel',
|
||||
type: 'text',
|
||||
label: '主按钮文案',
|
||||
defaultValue: '了解更多',
|
||||
defaultValue: '预约咨询',
|
||||
},
|
||||
{
|
||||
name: 'primaryCtaLink',
|
||||
name: 'ctaHref',
|
||||
type: 'text',
|
||||
label: '主按钮链接',
|
||||
defaultValue: '/solutions',
|
||||
},
|
||||
{
|
||||
name: 'secondaryCtaText',
|
||||
type: 'text',
|
||||
label: '副按钮文案',
|
||||
defaultValue: '联系我们',
|
||||
},
|
||||
{
|
||||
name: 'secondaryCtaLink',
|
||||
type: 'text',
|
||||
label: '副按钮链接',
|
||||
defaultValue: '/contact',
|
||||
},
|
||||
{
|
||||
name: 'statValue',
|
||||
type: 'text',
|
||||
label: '统计数值',
|
||||
description: 'Hero 区右侧可选数据指标,如「500+」',
|
||||
},
|
||||
{
|
||||
name: 'statSubtext',
|
||||
type: 'text',
|
||||
label: '统计说明',
|
||||
description: '与统计数值配套的说明文字',
|
||||
},
|
||||
{
|
||||
name: 'ctaTitleLine1',
|
||||
type: 'text',
|
||||
label: 'CTA 标题第一行',
|
||||
description: '底部 CTA 区域第一行标题',
|
||||
},
|
||||
{
|
||||
name: 'ctaTitleLine2',
|
||||
type: 'text',
|
||||
label: 'CTA 标题第二行',
|
||||
description: '底部 CTA 区域第二行标题(通常使用品牌红色)',
|
||||
},
|
||||
{
|
||||
name: 'theme',
|
||||
type: 'text',
|
||||
label: '主题风格',
|
||||
defaultValue: 'brand',
|
||||
options: [
|
||||
{ label: '品牌绿', value: 'brand' },
|
||||
{ label: '品牌红', value: 'brand' },
|
||||
{ label: '蓝', value: 'blue' },
|
||||
{ label: '青', value: 'teal' },
|
||||
{ label: '琥珀', value: 'amber' },
|
||||
|
||||
@@ -14,6 +14,11 @@ export interface CaseStudyData {
|
||||
testimonial?: CaseTestimonial;
|
||||
color: 'brand' | 'blue' | 'teal' | 'amber' | 'purple';
|
||||
featured?: boolean;
|
||||
projectDuration?: string;
|
||||
departments?: string[];
|
||||
dataScale?: string;
|
||||
businessProblem?: string;
|
||||
verified?: boolean;
|
||||
}
|
||||
|
||||
export type FieldType =
|
||||
|
||||
Reference in New Issue
Block a user