菜单添加页面模板管理页面,增加一个纯图平铺模板,优化预览手机屏效果
BIN
audi-content-portal/public/images/cars/a7-4.jpg
Normal file
|
After Width: | Height: | Size: 161 KiB |
|
Before Width: | Height: | Size: 933 KiB After Width: | Height: | Size: 933 KiB |
BIN
audi-content-portal/public/images/cars/ex7-1.jpg
Normal file
|
After Width: | Height: | Size: 819 KiB |
BIN
audi-content-portal/public/images/cars/ex7-2.jpg
Normal file
|
After Width: | Height: | Size: 86 KiB |
BIN
audi-content-portal/public/images/cars/ex7-2.png
Normal file
|
After Width: | Height: | Size: 603 KiB |
BIN
audi-content-portal/public/images/cars/ex7-3.jpg
Normal file
|
After Width: | Height: | Size: 91 KiB |
BIN
audi-content-portal/public/images/cars/ex7-3.png
Normal file
|
After Width: | Height: | Size: 562 KiB |
@@ -5,6 +5,7 @@ import { CarLibrary } from "@/components/car-library/CarLibrary"
|
||||
import { MiniAppContentLibrary } from "@/components/content-library/MiniAppContentLibrary"
|
||||
import { UserAccessManagement } from "@/components/users/UserAccessManagement"
|
||||
import { SystemSettings } from "@/components/settings/SystemSettings"
|
||||
import { TemplateManagement } from "@/components/templates/TemplateManagement"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import { Bell, Search, User } from "lucide-react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
@@ -65,6 +66,8 @@ export default function App() {
|
||||
newContentRequest={null}
|
||||
/>
|
||||
)
|
||||
case "templates":
|
||||
return <TemplateManagement />
|
||||
case "user-access":
|
||||
return <UserAccessManagement />
|
||||
case "settings":
|
||||
@@ -99,6 +102,7 @@ export default function App() {
|
||||
<div className="text-sm font-medium text-muted-foreground">
|
||||
{activeTab === "car-library" && "官网车型库"}
|
||||
{activeTab === "mini-content" && "小程序内容库"}
|
||||
{activeTab === "templates" && "小程序页面模板"}
|
||||
{activeTab === "user-access" && "用户与权限"}
|
||||
{activeTab === "settings" && "系统设置"}
|
||||
</div>
|
||||
|
||||
@@ -21,7 +21,7 @@ type SyncStatus = "synced" | "pending" | "error"
|
||||
type SourceCar = {
|
||||
id: string
|
||||
name: string
|
||||
pageType: "车型页" | "活动页" | "专题页"
|
||||
pageType: "车型页" | "活动页" | "专题页" | "纯图页"
|
||||
title: string
|
||||
subtitle: string
|
||||
highlights: string
|
||||
@@ -37,7 +37,7 @@ type SourceCar = {
|
||||
}
|
||||
|
||||
type SourceCarDraft = {
|
||||
pageType: "车型页" | "活动页" | "专题页"
|
||||
pageType: "车型页" | "活动页" | "专题页" | "纯图页"
|
||||
title: string
|
||||
subtitle: string
|
||||
highlights: string
|
||||
@@ -200,10 +200,53 @@ const SOURCE_CARS: SourceCar[] = [
|
||||
"/images/cars/rs7-2.jpg",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "e7x",
|
||||
name: "E7X",
|
||||
pageType: "纯图页",
|
||||
title: "E7X详情",
|
||||
subtitle: "先锋纯电旗舰",
|
||||
highlights: "纯电驱动\n先锋设计\n智能座舱",
|
||||
description: "E7X 纯图平铺模板内容,面向视觉导向场景。",
|
||||
ctaText: "预约体验",
|
||||
scheduledPublishAt: "",
|
||||
officialTagline: "先锋纯电旗舰",
|
||||
sourceUpdatedAt: "2026-04-16 10:30",
|
||||
updatedBy: "内容运营专员",
|
||||
syncStatus: "synced",
|
||||
imageCount: 3,
|
||||
imageUrls: [
|
||||
"/images/cars/ex7-1.jpg",
|
||||
"/images/cars/ex7-2.jpg",
|
||||
"/images/cars/ex7-3.jpg",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "a7",
|
||||
name: "A7",
|
||||
pageType: "专题页",
|
||||
title: "A7详情",
|
||||
subtitle: "",
|
||||
highlights: "RS竞速套件\n宽体低趴 超长轴距\n高性能美学,锋芒尽释\n懂你,更懂未来",
|
||||
description: "A7 专题图文模板内容,聚焦视觉叙事与图文模块表达。",
|
||||
ctaText: "预约试驾",
|
||||
scheduledPublishAt: "",
|
||||
officialTagline: "先锋设计",
|
||||
sourceUpdatedAt: "2026-04-16 10:40",
|
||||
updatedBy: "内容运营专员",
|
||||
syncStatus: "synced",
|
||||
imageCount: 4,
|
||||
imageUrls: [
|
||||
"/images/cars/a7-1.jpg",
|
||||
"/images/cars/a7-2.jpg",
|
||||
"/images/cars/a7-3.jpg",
|
||||
"/images/cars/a7-4.jpg",
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const INITIAL_LOGS: SyncLog[] = [
|
||||
{ id: "l1", time: "2026-04-11 09:30", status: "success", message: "同步成功:7 个车型,素材 31 张" },
|
||||
{ id: "l1", time: "2026-04-11 09:30", status: "success", message: "同步成功:9 个车型,素材 38 张" },
|
||||
{ id: "l2", time: "2026-04-10 09:15", status: "failed", message: "Audi Q5L 图片拉取失败:CDN 超时" },
|
||||
]
|
||||
|
||||
@@ -245,12 +288,12 @@ export function CarLibrary({ roleView, workflowConfig, onAddAuditLog }: CarLibra
|
||||
const [editingCarId, setEditingCarId] = React.useState<string | null>(null)
|
||||
const [editorDraft, setEditorDraft] = React.useState<SourceCarDraft | null>(null)
|
||||
const [newImageUrl, setNewImageUrl] = React.useState("")
|
||||
const [listPage, setListPage] = React.useState(1)
|
||||
const initialDraftRef = React.useRef<SourceCarDraft | null>(null)
|
||||
const listPageSize = 7
|
||||
const listTotalPages = 10
|
||||
const listPage = 1
|
||||
|
||||
const filtered = sourceCars.filter((car) => car.name.toLowerCase().includes(query.toLowerCase()))
|
||||
const listTotalPages = Math.max(1, Math.ceil(filtered.length / listPageSize))
|
||||
const pagedCars = filtered.slice((listPage - 1) * listPageSize, listPage * listPageSize)
|
||||
const previewCar = sourceCars.find((car) => car.id === previewCarId) ?? null
|
||||
const previewImages = previewCar?.imageUrls ?? []
|
||||
@@ -261,6 +304,16 @@ export function CarLibrary({ roleView, workflowConfig, onAddAuditLog }: CarLibra
|
||||
return JSON.stringify(editorDraft) !== JSON.stringify(initialDraftRef.current)
|
||||
}, [editorDraft])
|
||||
|
||||
React.useEffect(() => {
|
||||
setListPage(1)
|
||||
}, [query])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (listPage > listTotalPages) {
|
||||
setListPage(listTotalPages)
|
||||
}
|
||||
}, [listPage, listTotalPages])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!previewCarId || previewImages.length === 0) return
|
||||
|
||||
@@ -300,7 +353,7 @@ export function CarLibrary({ roleView, workflowConfig, onAddAuditLog }: CarLibra
|
||||
}))
|
||||
)
|
||||
setSyncLogs((curr) => [
|
||||
{ id: String(Date.now()), time, status: "success", message: "本周人工周更完成:7 个车型素材已核对并更新" },
|
||||
{ id: String(Date.now()), time, status: "success", message: "本周人工周更完成:9 个车型素材已核对并更新" },
|
||||
...curr,
|
||||
])
|
||||
onAddAuditLog("内容运营专员执行 JV 官网人工周更")
|
||||
@@ -636,21 +689,21 @@ export function CarLibrary({ roleView, workflowConfig, onAddAuditLog }: CarLibra
|
||||
</Table>
|
||||
|
||||
<div className="mt-4 flex items-center justify-between border-t pt-4">
|
||||
<p className="text-xs text-muted-foreground">每页 7 条,共 10 页</p>
|
||||
<p className="text-xs text-muted-foreground">每页 7 条,共 {listTotalPages} 页</p>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button size="sm" variant="outline" disabled>上一页</Button>
|
||||
<Button size="sm" variant="outline" disabled={listPage <= 1} onClick={() => setListPage((prev) => Math.max(1, prev - 1))}>上一页</Button>
|
||||
{Array.from({ length: listTotalPages }, (_, index) => (
|
||||
<Button
|
||||
key={`car-page-${index + 1}`}
|
||||
size="sm"
|
||||
variant={index + 1 === listPage ? "default" : "outline"}
|
||||
className={index + 1 === listPage ? "bg-audi-black hover:bg-audi-dark-gray text-white" : ""}
|
||||
disabled
|
||||
onClick={() => setListPage(index + 1)}
|
||||
>
|
||||
{index + 1}
|
||||
</Button>
|
||||
))}
|
||||
<Button size="sm" variant="outline" disabled>下一页</Button>
|
||||
<Button size="sm" variant="outline" disabled={listPage >= listTotalPages} onClick={() => setListPage((prev) => Math.min(listTotalPages, prev + 1))}>下一页</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
@@ -25,47 +25,10 @@ import {
|
||||
TableRow,
|
||||
} from "@/components/ui/table"
|
||||
|
||||
type WorkflowStatus = "draft" | "prepublished" | "published"
|
||||
|
||||
type TopicSection = {
|
||||
id: string
|
||||
title: string
|
||||
imageUrl: string
|
||||
specLeftLabel: string
|
||||
specRightLabel: string
|
||||
}
|
||||
|
||||
type SourceSnapshot = {
|
||||
title: string
|
||||
subtitle: string
|
||||
highlights: string
|
||||
description: string
|
||||
ctaText: string
|
||||
imageUrls: string[]
|
||||
topicNavTitle?: string
|
||||
topicHeroTitle?: string
|
||||
topicSections?: TopicSection[]
|
||||
}
|
||||
|
||||
type MiniAppContent = {
|
||||
id: string
|
||||
sourceCarId: string
|
||||
sourceCarName: string
|
||||
pageType: "车型页" | "活动页" | "专题页"
|
||||
title: string
|
||||
subtitle: string
|
||||
highlights: string
|
||||
description: string
|
||||
ctaText: string
|
||||
scheduledPublishAt: string
|
||||
imageUrls: string[]
|
||||
topicNavTitle?: string
|
||||
topicHeroTitle?: string
|
||||
topicSections?: TopicSection[]
|
||||
workflowStatus: WorkflowStatus
|
||||
updatedBy: string
|
||||
updatedAt: string
|
||||
}
|
||||
import type { WorkflowStatus, TopicSection, SourceSnapshot, MiniAppContent } from "./types"
|
||||
import { getTemplate, getAllTemplates } from "./template-registry"
|
||||
import "./register-templates"
|
||||
import { createTopicSectionsFromImages, buildTopicPatch } from "./register-templates"
|
||||
|
||||
type MiniAppContentLibraryProps = {
|
||||
roleView: RoleView
|
||||
@@ -227,8 +190,83 @@ const CONTENTS: MiniAppContent[] = [
|
||||
updatedBy: "内容运营专员",
|
||||
updatedAt: "2026-04-12 10:20",
|
||||
},
|
||||
{
|
||||
id: "c8",
|
||||
sourceCarId: "e7x",
|
||||
sourceCarName: "E7X",
|
||||
pageType: "纯图页",
|
||||
title: "E7X详情",
|
||||
subtitle: "",
|
||||
highlights: "",
|
||||
description: "",
|
||||
ctaText: "预约体验",
|
||||
scheduledPublishAt: "",
|
||||
imageUrls: [
|
||||
"/images/cars/ex7-1.jpg",
|
||||
"/images/cars/ex7-2.jpg",
|
||||
"/images/cars/ex7-3.jpg",
|
||||
],
|
||||
workflowStatus: "draft",
|
||||
updatedBy: "内容运营专员",
|
||||
updatedAt: "2026-04-16 10:30",
|
||||
},
|
||||
{
|
||||
id: "c9",
|
||||
sourceCarId: "a7",
|
||||
sourceCarName: "A7",
|
||||
pageType: "专题页",
|
||||
title: "A7详情",
|
||||
subtitle: "",
|
||||
highlights: "RS竞速套件\n宽体低趴 超长轴距\n高性能美学,锋芒尽释\n懂你,更懂未来",
|
||||
description: "",
|
||||
ctaText: "预约试驾",
|
||||
scheduledPublishAt: "",
|
||||
imageUrls: [
|
||||
"/images/cars/a7-1.jpg",
|
||||
"/images/cars/a7-2.jpg",
|
||||
"/images/cars/a7-3.jpg",
|
||||
"/images/cars/a7-4.jpg",
|
||||
],
|
||||
topicNavTitle: "A7详情",
|
||||
topicHeroTitle: "先锋设计",
|
||||
topicSections: [
|
||||
{
|
||||
id: "a7-topic-1",
|
||||
title: "RS竞速套件",
|
||||
imageUrl: "/images/cars/a7-1.jpg",
|
||||
specLeftLabel: "长",
|
||||
specRightLabel: "宽",
|
||||
},
|
||||
{
|
||||
id: "a7-topic-2",
|
||||
title: "宽体低趴 超长轴距",
|
||||
imageUrl: "/images/cars/a7-2.jpg",
|
||||
specLeftLabel: "长",
|
||||
specRightLabel: "宽",
|
||||
},
|
||||
{
|
||||
id: "a7-topic-3",
|
||||
title: "高性能美学,锋芒尽释",
|
||||
imageUrl: "/images/cars/a7-3.jpg",
|
||||
specLeftLabel: "长",
|
||||
specRightLabel: "宽",
|
||||
},
|
||||
{
|
||||
id: "a7-topic-4",
|
||||
title: "懂你,更懂未来",
|
||||
imageUrl: "/images/cars/a7-4.jpg",
|
||||
specLeftLabel: "长",
|
||||
specRightLabel: "宽",
|
||||
},
|
||||
],
|
||||
workflowStatus: "draft",
|
||||
updatedBy: "内容运营专员",
|
||||
updatedAt: "2026-04-16 10:40",
|
||||
},
|
||||
]
|
||||
|
||||
const INITIAL_CONTENT_IDS = new Set(CONTENTS.map((item) => item.id))
|
||||
|
||||
const WORKFLOW_LABEL: Record<WorkflowStatus, string> = {
|
||||
draft: "草稿",
|
||||
prepublished: "预发布",
|
||||
@@ -240,7 +278,7 @@ const SOURCE_SNAPSHOTS: Record<string, SourceSnapshot> = {
|
||||
title: "Audi A3 Sportback",
|
||||
subtitle: "进取,不负期待",
|
||||
highlights: "数字座舱\n城市通勤\n智能互联",
|
||||
description: "来自官网车型库的车型基线内容。",
|
||||
description: "适合城市用户的豪华紧凑车型,兼顾效率与智能体验。",
|
||||
ctaText: "立即预约试驾",
|
||||
imageUrls: [
|
||||
"/images/cars/a3-1.jpg",
|
||||
@@ -251,7 +289,7 @@ const SOURCE_SNAPSHOTS: Record<string, SourceSnapshot> = {
|
||||
title: "Audi A4L",
|
||||
subtitle: "做更强大的自己",
|
||||
highlights: "quattro\n商务舒适\n智能互联",
|
||||
description: "来自官网车型库的车型基线内容。",
|
||||
description: "面向商务人群的主推内容版本,突出舒适与操控。",
|
||||
ctaText: "了解更多",
|
||||
imageUrls: [
|
||||
"/images/cars/a4-1.jpg",
|
||||
@@ -261,7 +299,7 @@ const SOURCE_SNAPSHOTS: Record<string, SourceSnapshot> = {
|
||||
title: "Audi A6L",
|
||||
subtitle: "懂你,更懂未来",
|
||||
highlights: "行政旗舰\n长轴空间\n智能辅助",
|
||||
description: "来自官网车型库的车型基线内容。",
|
||||
description: "正在进行文案优化,待业务审核。",
|
||||
ctaText: "预约顾问回电",
|
||||
imageUrls: [
|
||||
"/images/cars/a6-1.jpg",
|
||||
@@ -272,7 +310,7 @@ const SOURCE_SNAPSHOTS: Record<string, SourceSnapshot> = {
|
||||
title: "Audi Q3",
|
||||
subtitle: "活出生命的辽阔",
|
||||
highlights: "紧凑SUV\n智能互联\n都市通勤",
|
||||
description: "来自官网车型库的车型基线内容。",
|
||||
description: "新建草稿,待进一步补充图文和活动权益信息。",
|
||||
ctaText: "立即预约试驾",
|
||||
imageUrls: [
|
||||
"/images/cars/q3-1.jpg",
|
||||
@@ -283,7 +321,7 @@ const SOURCE_SNAPSHOTS: Record<string, SourceSnapshot> = {
|
||||
title: "Audi Q5L",
|
||||
subtitle: "自由,由我定义",
|
||||
highlights: "四驱性能\n家庭空间\n全场景出行",
|
||||
description: "来自官网车型库的车型基线内容。",
|
||||
description: "因权益文案与活动规则不一致,被驳回待修订。",
|
||||
ctaText: "预约试驾",
|
||||
imageUrls: [
|
||||
"/images/cars/q5-1.jpg",
|
||||
@@ -294,7 +332,7 @@ const SOURCE_SNAPSHOTS: Record<string, SourceSnapshot> = {
|
||||
title: "Audi A8L",
|
||||
subtitle: "旗舰格局,沉稳之选",
|
||||
highlights: "旗舰行政\n豪华座舱\n专属服务",
|
||||
description: "来自官网车型库的车型基线内容。",
|
||||
description: "已发布后因活动档期调整撤回,待重新排期。",
|
||||
ctaText: "预约专属顾问",
|
||||
imageUrls: [
|
||||
"/images/cars/a8-1.jpg",
|
||||
@@ -305,13 +343,70 @@ const SOURCE_SNAPSHOTS: Record<string, SourceSnapshot> = {
|
||||
title: "Audi RS7 Performance",
|
||||
subtitle: "高性能美学,锋芒尽释",
|
||||
highlights: "V8双涡轮\nquattro四驱\nRS专属运动套件",
|
||||
description: "来自官网车型库的车型基线内容。",
|
||||
description: "聚焦高性能驾驶体验与豪华运动设计,适配高意向用户内容触达场景。",
|
||||
ctaText: "预约性能试驾",
|
||||
imageUrls: [
|
||||
"/images/cars/rs7-1.jpg",
|
||||
"/images/cars/rs7-2.jpg",
|
||||
],
|
||||
},
|
||||
e7x: {
|
||||
title: "E7X详情",
|
||||
subtitle: "先锋纯电旗舰",
|
||||
highlights: "纯电驱动\n先锋设计\n智能座舱",
|
||||
description: "",
|
||||
ctaText: "预约体验",
|
||||
imageUrls: [
|
||||
"/images/cars/ex7-1.jpg",
|
||||
"/images/cars/ex7-2.jpg",
|
||||
"/images/cars/ex7-3.jpg",
|
||||
],
|
||||
},
|
||||
a7: {
|
||||
title: "A7详情",
|
||||
subtitle: "先锋设计",
|
||||
highlights: "RS竞速套件\n宽体低趴 超长轴距\n高性能美学,锋芒尽释\n懂你,更懂未来",
|
||||
description: "",
|
||||
ctaText: "预约试驾",
|
||||
imageUrls: [
|
||||
"/images/cars/a7-1.jpg",
|
||||
"/images/cars/a7-2.jpg",
|
||||
"/images/cars/a7-3.jpg",
|
||||
"/images/cars/a7-4.jpg",
|
||||
],
|
||||
topicNavTitle: "A7详情",
|
||||
topicHeroTitle: "先锋设计",
|
||||
topicSections: [
|
||||
{
|
||||
id: "a7-topic-1",
|
||||
title: "RS竞速套件",
|
||||
imageUrl: "/images/cars/a7-1.jpg",
|
||||
specLeftLabel: "长",
|
||||
specRightLabel: "宽",
|
||||
},
|
||||
{
|
||||
id: "a7-topic-2",
|
||||
title: "宽体低趴 超长轴距",
|
||||
imageUrl: "/images/cars/a7-2.jpg",
|
||||
specLeftLabel: "长",
|
||||
specRightLabel: "宽",
|
||||
},
|
||||
{
|
||||
id: "a7-topic-3",
|
||||
title: "高性能美学,锋芒尽释",
|
||||
imageUrl: "/images/cars/a7-3.jpg",
|
||||
specLeftLabel: "长",
|
||||
specRightLabel: "宽",
|
||||
},
|
||||
{
|
||||
id: "a7-topic-4",
|
||||
title: "懂你,更懂未来",
|
||||
imageUrl: "/images/cars/a7-4.jpg",
|
||||
specLeftLabel: "长",
|
||||
specRightLabel: "宽",
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
const SOURCE_CAR_OPTIONS = [
|
||||
@@ -322,31 +417,10 @@ const SOURCE_CAR_OPTIONS = [
|
||||
{ id: "q5l", name: "Audi Q5L" },
|
||||
{ id: "a8l", name: "Audi A8L" },
|
||||
{ id: "rs7", name: "Audi RS7" },
|
||||
{ id: "e7x", name: "E7X" },
|
||||
{ id: "a7", name: "A7" },
|
||||
] as const
|
||||
|
||||
function createTopicSectionsFromImages(images: string[]) {
|
||||
const picks = images.slice(0, 2)
|
||||
const fallback = ["/images/cars/a7-1.jpg", "/images/cars/a7-2.jpg"]
|
||||
const selected = picks.length > 0 ? picks : fallback
|
||||
|
||||
return selected.map((imageUrl, index) => ({
|
||||
id: `topic-section-${index + 1}`,
|
||||
title: index === 0 ? "RS竞速套件" : "宽体低趴 超长轴距",
|
||||
imageUrl,
|
||||
specLeftLabel: "长",
|
||||
specRightLabel: "宽",
|
||||
}))
|
||||
}
|
||||
|
||||
function buildTopicPatch(sourceCarName: string, snapshot?: SourceSnapshot): Pick<MiniAppContent, "topicNavTitle" | "topicHeroTitle" | "topicSections"> {
|
||||
const hero = snapshot?.topicHeroTitle ?? snapshot?.subtitle ?? "先锋设计"
|
||||
return {
|
||||
topicNavTitle: snapshot?.topicNavTitle ?? `${sourceCarName || "车型"}详情`,
|
||||
topicHeroTitle: hero,
|
||||
topicSections: snapshot?.topicSections ?? createTopicSectionsFromImages(snapshot?.imageUrls ?? []),
|
||||
}
|
||||
}
|
||||
|
||||
function workflowBadgeClass(status: WorkflowStatus) {
|
||||
switch (status) {
|
||||
case "draft":
|
||||
@@ -369,9 +443,8 @@ export function MiniAppContentLibrary({ roleView, workflowConfig, onAddAuditLog,
|
||||
const [query, setQuery] = React.useState("")
|
||||
const [newImageUrl, setNewImageUrl] = React.useState("")
|
||||
const [pendingResetPatch, setPendingResetPatch] = React.useState<Record<string, Partial<MiniAppContent>>>({})
|
||||
const [listPage, setListPage] = React.useState(1)
|
||||
const listPageSize = 7
|
||||
const listTotalPages = 10
|
||||
const listPage = 1
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!newContentRequest) return
|
||||
@@ -379,16 +452,18 @@ export function MiniAppContentLibrary({ roleView, workflowConfig, onAddAuditLog,
|
||||
const id = `c${newContentRequest.requestId}`
|
||||
const existing = contents.find((item) => item.id === id)
|
||||
if (!existing) {
|
||||
const defaultPageType = getAllTemplates()[0]?.pageTypes[0] ?? "车型页"
|
||||
const tmpl = getTemplate(defaultPageType)
|
||||
const draft: MiniAppContent = {
|
||||
id,
|
||||
sourceCarId: newContentRequest.sourceCarId,
|
||||
sourceCarName: newContentRequest.sourceCarName,
|
||||
pageType: "车型页",
|
||||
pageType: defaultPageType,
|
||||
title: newContentRequest.sourceCarName,
|
||||
subtitle: "",
|
||||
highlights: "",
|
||||
description: "",
|
||||
ctaText: "立即预约试驾",
|
||||
ctaText: "",
|
||||
scheduledPublishAt: "",
|
||||
imageUrls: [],
|
||||
topicNavTitle: `${newContentRequest.sourceCarName}详情`,
|
||||
@@ -397,6 +472,7 @@ export function MiniAppContentLibrary({ roleView, workflowConfig, onAddAuditLog,
|
||||
workflowStatus: "draft",
|
||||
updatedBy: "内容运营专员",
|
||||
updatedAt: nowString(),
|
||||
...tmpl?.defaultValues,
|
||||
}
|
||||
setContents((prev) => [draft, ...prev])
|
||||
}
|
||||
@@ -412,8 +488,19 @@ export function MiniAppContentLibrary({ roleView, workflowConfig, onAddAuditLog,
|
||||
const key = `${item.sourceCarName} ${item.sourceCarId} ${item.pageType} ${item.title}`.toLowerCase()
|
||||
return key.includes(query.toLowerCase())
|
||||
})
|
||||
const listTotalPages = Math.max(1, Math.ceil(filteredContents.length / listPageSize))
|
||||
const pagedContents = filteredContents.slice((listPage - 1) * listPageSize, listPage * listPageSize)
|
||||
|
||||
React.useEffect(() => {
|
||||
setListPage(1)
|
||||
}, [query])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (listPage > listTotalPages) {
|
||||
setListPage(listTotalPages)
|
||||
}
|
||||
}, [listPage, listTotalPages])
|
||||
|
||||
const updateContent = (id: string, patch: Partial<MiniAppContent>) => {
|
||||
setContents((prev) =>
|
||||
prev.map((item) => (item.id === id ? { ...item, ...patch, updatedAt: nowString() } : item))
|
||||
@@ -430,7 +517,7 @@ export function MiniAppContentLibrary({ roleView, workflowConfig, onAddAuditLog,
|
||||
return next
|
||||
})
|
||||
if (Object.keys(stagedPatch).length > 0) {
|
||||
onAddAuditLog(`内容运营专员保存并重置 ${item.sourceCarName} 到官网源数据(回草稿)`)
|
||||
onAddAuditLog(`内容运营专员保存并重置 ${item.sourceCarName} 到来源车型同步内容(回草稿)`)
|
||||
} else {
|
||||
onAddAuditLog(`内容运营专员保存 ${item.sourceCarName} 内容草稿`)
|
||||
}
|
||||
@@ -439,19 +526,16 @@ export function MiniAppContentLibrary({ roleView, workflowConfig, onAddAuditLog,
|
||||
const stageResetFromSource = (item: MiniAppContent) => {
|
||||
const source = SOURCE_SNAPSHOTS[item.sourceCarId]
|
||||
if (!source) return
|
||||
if (!window.confirm("确认重置当前内容吗?此操作将删除当前编辑内容,并重新与所选“来源车型”进行同步。")) {
|
||||
return
|
||||
}
|
||||
const tmpl = getTemplate(item.pageType)
|
||||
const templatePatch = tmpl?.onSourceCarSelected?.(source, item) ?? {}
|
||||
setPendingResetPatch((prev) => ({
|
||||
...prev,
|
||||
[item.id]: {
|
||||
title: source.title,
|
||||
subtitle: source.subtitle,
|
||||
highlights: source.highlights,
|
||||
description: source.description,
|
||||
ctaText: item.pageType === "专题页" ? "预约体验" : source.ctaText,
|
||||
imageUrls: item.pageType === "专题页" ? [] : source.imageUrls,
|
||||
...buildTopicPatch(item.sourceCarName, source),
|
||||
},
|
||||
[item.id]: templatePatch,
|
||||
}))
|
||||
onAddAuditLog(`内容运营专员发起 ${item.sourceCarName} 源数据重置(待保存生效)`)
|
||||
onAddAuditLog(`内容运营专员发起 ${item.sourceCarName} 内容重置(待保存生效)`)
|
||||
}
|
||||
|
||||
const submitForReview = (item: MiniAppContent) => {
|
||||
@@ -476,16 +560,18 @@ export function MiniAppContentLibrary({ roleView, workflowConfig, onAddAuditLog,
|
||||
|
||||
const createNewContent = () => {
|
||||
const id = `c${Date.now()}`
|
||||
const defaultPageType = ""
|
||||
const tmpl = defaultPageType ? getTemplate(defaultPageType) : undefined
|
||||
const draft: MiniAppContent = {
|
||||
id,
|
||||
sourceCarId: "",
|
||||
sourceCarName: "",
|
||||
pageType: "车型页",
|
||||
pageType: defaultPageType,
|
||||
title: "",
|
||||
subtitle: "",
|
||||
highlights: "",
|
||||
description: "",
|
||||
ctaText: "立即预约试驾",
|
||||
ctaText: "",
|
||||
scheduledPublishAt: "",
|
||||
imageUrls: [],
|
||||
topicNavTitle: "车型详情",
|
||||
@@ -494,6 +580,7 @@ export function MiniAppContentLibrary({ roleView, workflowConfig, onAddAuditLog,
|
||||
workflowStatus: "draft",
|
||||
updatedBy: "内容运营专员",
|
||||
updatedAt: nowString(),
|
||||
...tmpl?.defaultValues,
|
||||
}
|
||||
setContents((prev) => [draft, ...prev])
|
||||
setSelectedId(id)
|
||||
@@ -527,14 +614,18 @@ export function MiniAppContentLibrary({ roleView, workflowConfig, onAddAuditLog,
|
||||
}
|
||||
|
||||
const updateTopicSection = (item: MiniAppContent, sectionId: string, patch: Partial<TopicSection>) => {
|
||||
const sections = item.topicSections ?? []
|
||||
const sections = (item.topicSections && item.topicSections.length > 0)
|
||||
? item.topicSections
|
||||
: createTopicSectionsFromImages(item.imageUrls)
|
||||
updateContent(item.id, {
|
||||
topicSections: sections.map((section) => (section.id === sectionId ? { ...section, ...patch } : section)),
|
||||
})
|
||||
}
|
||||
|
||||
const addTopicSection = (item: MiniAppContent) => {
|
||||
const sections = item.topicSections ?? []
|
||||
const sections = (item.topicSections && item.topicSections.length > 0)
|
||||
? item.topicSections
|
||||
: createTopicSectionsFromImages(item.imageUrls)
|
||||
if (sections.length >= 6) return
|
||||
updateContent(item.id, {
|
||||
topicSections: [
|
||||
@@ -551,7 +642,9 @@ export function MiniAppContentLibrary({ roleView, workflowConfig, onAddAuditLog,
|
||||
}
|
||||
|
||||
const removeTopicSection = (item: MiniAppContent, sectionId: string) => {
|
||||
const sections = item.topicSections ?? []
|
||||
const sections = (item.topicSections && item.topicSections.length > 0)
|
||||
? item.topicSections
|
||||
: createTopicSectionsFromImages(item.imageUrls)
|
||||
if (sections.length <= 1) return
|
||||
updateContent(item.id, {
|
||||
topicSections: sections.filter((section) => section.id !== sectionId),
|
||||
@@ -607,21 +700,38 @@ export function MiniAppContentLibrary({ roleView, workflowConfig, onAddAuditLog,
|
||||
if (viewMode === "editor" && selected) {
|
||||
const isNewContent = !selected.sourceCarId
|
||||
const canResetFromSource = Boolean(selected.sourceCarId && SOURCE_SNAPSHOTS[selected.sourceCarId])
|
||||
const isTopicPage = selected.pageType === "专题页"
|
||||
const currentTemplate = getTemplate(selected.pageType)
|
||||
const selectedTemplateId = currentTemplate?.templateId ?? ""
|
||||
const topicSections = (selected.topicSections && selected.topicSections.length > 0)
|
||||
? selected.topicSections
|
||||
: createTopicSectionsFromImages(selected.imageUrls)
|
||||
const previewHeroImage = selected.imageUrls[0]
|
||||
const highlights = selected.highlights
|
||||
.split("\n")
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean)
|
||||
const PreviewComp = currentTemplate?.PreviewComponent
|
||||
const EditorComp = currentTemplate?.EditorComponent
|
||||
|
||||
const handleBackToList = () => {
|
||||
setPendingResetPatch((prev) => {
|
||||
if (!prev[selected.id]) return prev
|
||||
const next = { ...prev }
|
||||
delete next[selected.id]
|
||||
return next
|
||||
})
|
||||
if (!INITIAL_CONTENT_IDS.has(selected.id)) {
|
||||
setContents((prev) => {
|
||||
const next = prev.filter((item) => item.id !== selected.id)
|
||||
if (next.length > 0) {
|
||||
setSelectedId(next[0].id)
|
||||
}
|
||||
return next
|
||||
})
|
||||
}
|
||||
setViewMode("list")
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6 p-8">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<Button variant="outline" size="sm" onClick={() => setViewMode("list")}>
|
||||
<Button variant="outline" size="sm" onClick={handleBackToList}>
|
||||
<ArrowLeft className="mr-1 h-3.5 w-3.5" />返回列表
|
||||
</Button>
|
||||
<div>
|
||||
@@ -642,31 +752,32 @@ export function MiniAppContentLibrary({ roleView, workflowConfig, onAddAuditLog,
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid gap-2">
|
||||
<label className="text-xs font-semibold tracking-wide text-muted-foreground uppercase">页面类型</label>
|
||||
<label className="text-xs font-semibold tracking-wide text-muted-foreground uppercase">页面模板</label>
|
||||
{isNewContent && canEdit ? (
|
||||
<select
|
||||
className="h-10 rounded-lg border border-input bg-background px-3 text-sm"
|
||||
value={selected.pageType}
|
||||
value={selectedTemplateId}
|
||||
onChange={(e) => {
|
||||
const nextPageType = e.target.value as MiniAppContent["pageType"]
|
||||
const nextTemplateId = e.target.value
|
||||
const nextTemplate = getAllTemplates().find((tmpl) => tmpl.templateId === nextTemplateId)
|
||||
const nextPageType = nextTemplate?.pageTypes[0] ?? ""
|
||||
const snapshot = SOURCE_SNAPSHOTS[selected.sourceCarId]
|
||||
const sourceName = selected.sourceCarName || "车型"
|
||||
const templateDefaults = nextTemplate?.defaultValues ?? {}
|
||||
const templatePatch = nextTemplate?.onSourceCarSelected?.(snapshot, { ...selected, pageType: nextPageType }) ?? {}
|
||||
updateContent(selected.id, {
|
||||
pageType: nextPageType,
|
||||
...(nextPageType === "专题页" ? {
|
||||
...buildTopicPatch(sourceName, snapshot),
|
||||
imageUrls: [],
|
||||
ctaText: "预约体验",
|
||||
} : {}),
|
||||
...templateDefaults,
|
||||
...templatePatch,
|
||||
})
|
||||
}}
|
||||
>
|
||||
<option value="车型页">车型页</option>
|
||||
<option value="活动页">活动页</option>
|
||||
<option value="专题页">专题页</option>
|
||||
<option value="">请选择页面模板</option>
|
||||
{getAllTemplates().map((tmpl) => (
|
||||
<option key={tmpl.templateId} value={tmpl.templateId}>{tmpl.displayName}</option>
|
||||
))}
|
||||
</select>
|
||||
) : (
|
||||
<Input value={selected.pageType} disabled />
|
||||
<Input value={getTemplate(selected.pageType)?.displayName ?? selected.pageType} disabled />
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -681,16 +792,11 @@ export function MiniAppContentLibrary({ roleView, workflowConfig, onAddAuditLog,
|
||||
const option = SOURCE_CAR_OPTIONS.find((item) => item.id === nextId)
|
||||
const snapshot = SOURCE_SNAPSHOTS[nextId]
|
||||
const sourceName = option?.name ?? ""
|
||||
const templatePatch = currentTemplate?.onSourceCarSelected?.(snapshot, { ...selected, sourceCarId: nextId, sourceCarName: sourceName }) ?? {}
|
||||
updateContent(selected.id, {
|
||||
sourceCarId: nextId,
|
||||
sourceCarName: sourceName,
|
||||
title: snapshot?.title ?? selected.title,
|
||||
subtitle: snapshot?.subtitle ?? selected.subtitle,
|
||||
highlights: snapshot?.highlights ?? selected.highlights,
|
||||
description: snapshot?.description ?? selected.description,
|
||||
ctaText: selected.pageType === "专题页" ? "预约体验" : (snapshot?.ctaText ?? selected.ctaText),
|
||||
imageUrls: selected.pageType === "专题页" ? [] : (snapshot?.imageUrls ?? selected.imageUrls),
|
||||
...buildTopicPatch(sourceName, snapshot),
|
||||
...templatePatch,
|
||||
})
|
||||
if (snapshot) {
|
||||
onAddAuditLog(`内容运营专员选择来源车型 ${option?.name ?? nextId} 并载入官网基线内容`)
|
||||
@@ -707,140 +813,28 @@ export function MiniAppContentLibrary({ roleView, workflowConfig, onAddAuditLog,
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isTopicPage ? (
|
||||
{selected.pageType ? (
|
||||
<>
|
||||
<div className="grid gap-2">
|
||||
<label className="text-xs font-semibold tracking-wide text-muted-foreground uppercase">导航标题</label>
|
||||
<Input
|
||||
value={selected.topicNavTitle ?? `${selected.sourceCarName || "车型"}详情`}
|
||||
disabled={!canEdit}
|
||||
onChange={(e) => updateContent(selected.id, { topicNavTitle: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<label className="text-xs font-semibold tracking-wide text-muted-foreground uppercase">主视觉标题</label>
|
||||
<Input
|
||||
value={selected.topicHeroTitle ?? "先锋设计"}
|
||||
disabled={!canEdit}
|
||||
onChange={(e) => updateContent(selected.id, { topicHeroTitle: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<label className="text-xs font-semibold tracking-wide text-muted-foreground uppercase">底部按钮文案</label>
|
||||
<Input value={selected.ctaText} disabled={!canEdit} onChange={(e) => updateContent(selected.id, { ctaText: e.target.value })} />
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-xs font-semibold tracking-wide text-muted-foreground uppercase">专题模块(1-6)</label>
|
||||
<Button size="sm" variant="outline" disabled={!canEdit || topicSections.length >= 6} onClick={() => addTopicSection(selected)}>
|
||||
新增模块
|
||||
</Button>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{topicSections.map((section, index) => (
|
||||
<div key={section.id} className="rounded-lg border p-3 space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-xs font-semibold text-muted-foreground">模块 {index + 1}</p>
|
||||
<Button size="sm" variant="ghost" disabled={!canEdit || topicSections.length <= 1} onClick={() => removeTopicSection(selected, section.id)}>
|
||||
删除
|
||||
</Button>
|
||||
</div>
|
||||
<Input
|
||||
value={section.title}
|
||||
disabled={!canEdit}
|
||||
onChange={(e) => updateTopicSection(selected, section.id, { title: e.target.value })}
|
||||
placeholder="模块标题"
|
||||
/>
|
||||
<Input
|
||||
value={section.imageUrl}
|
||||
disabled={!canEdit}
|
||||
onChange={(e) => updateTopicSection(selected, section.id, { imageUrl: e.target.value })}
|
||||
placeholder="模块图片 URL"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="grid gap-2">
|
||||
<label className="text-xs font-semibold tracking-wide text-muted-foreground uppercase">标题</label>
|
||||
<Input value={selected.title} disabled={!canEdit} onChange={(e) => updateContent(selected.id, { title: e.target.value })} />
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<label className="text-xs font-semibold tracking-wide text-muted-foreground uppercase">副标题</label>
|
||||
<Input value={selected.subtitle} disabled={!canEdit} onChange={(e) => updateContent(selected.id, { subtitle: e.target.value })} />
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<label className="text-xs font-semibold tracking-wide text-muted-foreground uppercase">卖点(每行一条)</label>
|
||||
<textarea
|
||||
className="min-h-24 rounded-lg border border-input bg-background px-3 py-2 text-sm outline-none focus-visible:border-ring"
|
||||
value={selected.highlights}
|
||||
disabled={!canEdit}
|
||||
onChange={(e) => updateContent(selected.id, { highlights: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<label className="text-xs font-semibold tracking-wide text-muted-foreground uppercase">图文描述(轻量编排)</label>
|
||||
<textarea
|
||||
className="min-h-28 rounded-lg border border-input bg-background px-3 py-2 text-sm outline-none focus-visible:border-ring"
|
||||
value={selected.description}
|
||||
disabled={!canEdit}
|
||||
onChange={(e) => updateContent(selected.id, { description: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<label className="text-xs font-semibold tracking-wide text-muted-foreground uppercase">按钮文案</label>
|
||||
<Input value={selected.ctaText} disabled={!canEdit} onChange={(e) => updateContent(selected.id, { ctaText: e.target.value })} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!isTopicPage && workflowConfig.publishStrategy === "scheduled" && (
|
||||
<div className="grid gap-2">
|
||||
<label className="text-xs font-semibold tracking-wide text-muted-foreground uppercase">预约发布时间</label>
|
||||
<Input
|
||||
type="datetime-local"
|
||||
disabled={!canEdit}
|
||||
value={selected.scheduledPublishAt}
|
||||
onChange={(e) => updateContent(selected.id, { scheduledPublishAt: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isTopicPage && (
|
||||
<div className="grid gap-2">
|
||||
<label className="text-xs font-semibold tracking-wide text-muted-foreground uppercase">图片素材(多图)</label>
|
||||
<div className="flex gap-2">
|
||||
<Input value={newImageUrl} disabled={!canEdit} onChange={(e) => setNewImageUrl(e.target.value)} placeholder="粘贴图片 URL" />
|
||||
<Button variant="outline" disabled={!canEdit} onClick={() => addImage(selected)}>新增</Button>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{selected.imageUrls.map((url, idx) => (
|
||||
<div key={`${url}-${idx}`} className="rounded-lg border p-2">
|
||||
<img src={url} alt={`${selected.sourceCarName}-${idx}`} className="aspect-video w-full rounded object-cover" />
|
||||
<div className="mt-2 flex justify-end">
|
||||
<Button size="sm" variant="ghost" disabled={!canEdit} onClick={() => removeImage(selected, idx)}>
|
||||
删除
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{EditorComp && (
|
||||
<EditorComp
|
||||
content={selected}
|
||||
canEdit={canEdit}
|
||||
onUpdate={(patch) => updateContent(selected.id, patch)}
|
||||
workflowConfig={workflowConfig}
|
||||
newImageUrl={newImageUrl}
|
||||
onNewImageUrlChange={setNewImageUrl}
|
||||
onAddImage={() => addImage(selected)}
|
||||
onRemoveImage={(idx) => removeImage(selected, idx)}
|
||||
topicSections={topicSections}
|
||||
onUpdateTopicSection={(sectionId, patch) => updateTopicSection(selected, sectionId, patch)}
|
||||
onAddTopicSection={() => addTopicSection(selected)}
|
||||
onRemoveTopicSection={(sectionId) => removeTopicSection(selected, sectionId)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{pendingResetPatch[selected.id] && (
|
||||
<div className="rounded-lg border border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-700">
|
||||
已暂存“重置为官网源数据”,请点击“保存草稿”生效并回到草稿状态。
|
||||
已暂存重置内容,点击“保存草稿”后将以来源车型内容覆盖当前编辑内容。
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -862,6 +856,10 @@ export function MiniAppContentLibrary({ roleView, workflowConfig, onAddAuditLog,
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="text-sm text-muted-foreground py-8 text-center">请先选择页面模板</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -869,150 +867,14 @@ export function MiniAppContentLibrary({ roleView, workflowConfig, onAddAuditLog,
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg font-bold">小程序预览(手机窗口)</CardTitle>
|
||||
<CardDescription>
|
||||
{isTopicPage
|
||||
? "专题页预览:按来源车型模板自动同步,可对模块图文做手工编辑。"
|
||||
: "按钮上方展示预约试驾表单视觉区(仅演示,不提交;真实留资将直接进入企业 CRM)。"}
|
||||
{currentTemplate?.previewDescription ?? "页面预览"}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex justify-center">
|
||||
{isTopicPage ? (
|
||||
<div className="min-h-screen w-[360px] bg-white text-[#1a1a1a] flex flex-col relative overflow-hidden shadow-2xl rounded-[28px] border border-gray-200">
|
||||
<div className="px-8 pt-4 pb-2 flex justify-between items-center text-sm font-semibold">
|
||||
<span>9:41</span>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="h-2.5 w-2.5 rotate-45 border border-current inline-block" />
|
||||
<span className="h-2.5 w-2.5 rotate-45 border border-current inline-block" />
|
||||
<div className="w-6 h-3 border border-current rounded-sm relative">
|
||||
<div className="absolute inset-0.5 bg-current rounded-[2px] w-3" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<header className="px-4 py-2 flex items-center justify-between sticky top-0 bg-white z-10">
|
||||
<button className="p-2 rounded-full">
|
||||
<ArrowLeft className="w-5 h-5" />
|
||||
</button>
|
||||
<h1 className="text-[17px] font-medium">{selected.topicNavTitle || `${selected.sourceCarName || "车型"}详情`}</h1>
|
||||
<div className="flex items-center bg-gray-50 border border-gray-200 rounded-full px-3 py-1.5 gap-3">
|
||||
<span className="text-xs text-gray-500">···</span>
|
||||
<div className="w-px h-4 bg-gray-300" />
|
||||
<span className="text-xs text-gray-500">x</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="flex-1 overflow-y-auto pb-32">
|
||||
<section className="pt-8 pb-6 text-center">
|
||||
<h2 className="text-[32px] font-bold tracking-tight mb-2">{selected.topicHeroTitle || "先锋设计"}</h2>
|
||||
<div className="w-12 h-[3px] bg-[#d51c2a] mx-auto" />
|
||||
</section>
|
||||
|
||||
<div className="space-y-4 px-0 max-h-[520px] overflow-y-auto">
|
||||
{topicSections.map((section) => (
|
||||
<section key={section.id} className="bg-[#f8f9fb] rounded-t-[32px] overflow-hidden">
|
||||
<div className="aspect-[16/9] relative overflow-hidden">
|
||||
{section.imageUrl ? (
|
||||
<img
|
||||
src={section.imageUrl}
|
||||
alt={section.title}
|
||||
className="w-full h-full object-cover mix-blend-multiply"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full h-full flex items-center justify-center text-xs text-gray-500 bg-gray-100">请添加模块图片</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="px-6 py-2">
|
||||
<h3 className="text-[22px] font-normal text-gray-700">{section.title}</h3>
|
||||
</div>
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer className="absolute bottom-0 left-0 right-0 bg-white/80 backdrop-blur-md px-4 pt-2 pb-8 border-t border-gray-100">
|
||||
<button className="w-full bg-[#1c1f23] text-white py-4 rounded-md text-[16px] font-medium">
|
||||
{selected.ctaText || "预约体验"}
|
||||
</button>
|
||||
<div className="mt-6 w-32 h-1.5 bg-black/80 mx-auto rounded-full" />
|
||||
</footer>
|
||||
</div>
|
||||
{selected.pageType && PreviewComp ? (
|
||||
<PreviewComp content={selected} workflowConfig={workflowConfig} />
|
||||
) : (
|
||||
<div className="w-[360px] rounded-[28px] border border-gray-200 bg-white shadow-2xl overflow-hidden">
|
||||
<div className="h-9 bg-black text-white px-4 flex items-center text-[10px] tracking-[0.18em] uppercase">Audi</div>
|
||||
<div className="relative">
|
||||
{previewHeroImage ? (
|
||||
<img src={previewHeroImage} alt={selected.title} className="h-48 w-full object-cover" />
|
||||
) : (
|
||||
<div className="flex h-48 w-full items-center justify-center bg-gray-100 text-xs text-gray-500">请先添加图片素材</div>
|
||||
)}
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/65 to-transparent" />
|
||||
<div className="absolute left-4 right-4 bottom-3 text-white">
|
||||
<p className="text-lg font-bold tracking-tight">{selected.title}</p>
|
||||
<p className="text-xs opacity-85">{selected.subtitle}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4">
|
||||
<div className="mb-3 grid grid-cols-3 gap-2">
|
||||
{selected.imageUrls.slice(0, 3).map((url, idx) => (
|
||||
<img key={`${url}-${idx}`} src={url} alt={`thumb-${idx}`} className="h-16 w-full rounded object-cover" />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
{highlights.map((item) => (
|
||||
<div key={item} className="inline-flex mr-2 mb-1 rounded-full border px-2 py-0.5 text-[10px] text-gray-600">
|
||||
{item}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<p className="mt-3 text-sm leading-6 text-gray-600">{selected.description}</p>
|
||||
|
||||
{workflowConfig.publishStrategy === "scheduled" && selected.scheduledPublishAt && (
|
||||
<div className="mt-3 rounded-md border border-blue-200 bg-blue-50 px-3 py-2 text-xs text-blue-700">
|
||||
预约发布时间:{selected.scheduledPublishAt.replace("T", " ")}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-4 space-y-3 rounded-lg border border-gray-200 bg-gray-50 p-3">
|
||||
<div className="grid gap-1">
|
||||
<label className="text-[10px] uppercase tracking-wider text-gray-500 font-semibold">姓名</label>
|
||||
<Input placeholder="请输入您的姓名" disabled />
|
||||
</div>
|
||||
<div className="grid gap-1">
|
||||
<label className="text-[10px] uppercase tracking-wider text-gray-500 font-semibold">手机号</label>
|
||||
<Input placeholder="请输入手机号" disabled />
|
||||
</div>
|
||||
<div className="grid gap-1">
|
||||
<label className="text-[10px] uppercase tracking-wider text-gray-500 font-semibold">意向城市</label>
|
||||
<div className="flex gap-2">
|
||||
<select className="h-8 flex-1 rounded-lg border border-input bg-white px-2 text-sm text-gray-500" disabled>
|
||||
<option>请选择城市</option>
|
||||
</select>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => alert("获取位置功能为原型占位提示")}
|
||||
>
|
||||
获取位置
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label className="flex items-start gap-2 text-[11px] leading-5 text-gray-600">
|
||||
<input type="checkbox" className="mt-0.5" />
|
||||
<span>
|
||||
我已阅读并同意《个人信息保护政策》,授权奥迪及授权经销商与我联系。信息提交后将直接进入企业 CRM,本平台不留存潜客数据。
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<Button className="mt-4 w-full bg-audi-black hover:bg-audi-dark-gray text-white" disabled>
|
||||
{selected.ctaText || "立即预约试驾"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground py-8">请先选择页面模板以预览</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -1056,7 +918,7 @@ export function MiniAppContentLibrary({ roleView, workflowConfig, onAddAuditLog,
|
||||
<TableHeader>
|
||||
<TableRow className="hover:bg-transparent border-b border-gray-100">
|
||||
<TableHead className="font-bold text-audi-black">来源车型</TableHead>
|
||||
<TableHead className="font-bold text-audi-black">页面类型</TableHead>
|
||||
<TableHead className="font-bold text-audi-black">页面模板</TableHead>
|
||||
<TableHead className="font-bold text-audi-black">内容标题</TableHead>
|
||||
<TableHead className="font-bold text-audi-black">状态</TableHead>
|
||||
<TableHead className="font-bold text-audi-black">最近更新</TableHead>
|
||||
@@ -1071,7 +933,7 @@ export function MiniAppContentLibrary({ roleView, workflowConfig, onAddAuditLog,
|
||||
<p className="text-xs text-muted-foreground">{item.sourceCarId}</p>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="outline">{item.pageType}</Badge>
|
||||
<Badge variant="outline">{getTemplate(item.pageType)?.displayName ?? item.pageType}</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="font-medium">{item.title}</TableCell>
|
||||
<TableCell>
|
||||
@@ -1092,21 +954,21 @@ export function MiniAppContentLibrary({ roleView, workflowConfig, onAddAuditLog,
|
||||
</Table>
|
||||
|
||||
<div className="mt-4 flex items-center justify-between border-t pt-4">
|
||||
<p className="text-xs text-muted-foreground">每页 7 条,共 10 页</p>
|
||||
<p className="text-xs text-muted-foreground">每页 7 条,共 {listTotalPages} 页</p>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button size="sm" variant="outline" disabled>上一页</Button>
|
||||
<Button size="sm" variant="outline" disabled={listPage <= 1} onClick={() => setListPage((prev) => Math.max(1, prev - 1))}>上一页</Button>
|
||||
{Array.from({ length: listTotalPages }, (_, index) => (
|
||||
<Button
|
||||
key={`mini-page-${index + 1}`}
|
||||
size="sm"
|
||||
variant={index + 1 === listPage ? "default" : "outline"}
|
||||
className={index + 1 === listPage ? "bg-audi-black hover:bg-audi-dark-gray text-white" : ""}
|
||||
disabled
|
||||
onClick={() => setListPage(index + 1)}
|
||||
>
|
||||
{index + 1}
|
||||
</Button>
|
||||
))}
|
||||
<Button size="sm" variant="outline" disabled>下一页</Button>
|
||||
<Button size="sm" variant="outline" disabled={listPage >= listTotalPages} onClick={() => setListPage((prev) => Math.min(listTotalPages, prev + 1))}>下一页</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
import { registerTemplate } from "./template-registry"
|
||||
import type { PageTemplate } from "./types"
|
||||
import { CarPagePreview } from "./templates/car-page/CarPagePreview"
|
||||
import { CarPageEditor } from "./templates/car-page/CarPageEditor"
|
||||
import { PureImagePagePreview } from "./templates/pure-image-page/PureImagePagePreview"
|
||||
import { PureImagePageEditor } from "./templates/pure-image-page/PureImagePageEditor"
|
||||
import { TopicPagePreview } from "./templates/topic-page/TopicPagePreview"
|
||||
import { TopicPageEditor } from "./templates/topic-page/TopicPageEditor"
|
||||
import { buildTopicPatch, createTopicSectionsFromImages } from "./templates/topic-page/helpers"
|
||||
|
||||
const carPageTemplate: PageTemplate = {
|
||||
templateId: "car-page",
|
||||
displayName: "车型营销模板",
|
||||
pageTypes: ["车型页", "活动页"],
|
||||
defaultValues: {
|
||||
ctaText: "立即预约试驾",
|
||||
imageUrls: [],
|
||||
},
|
||||
onSourceCarSelected(snapshot, current) {
|
||||
return {
|
||||
title: snapshot?.title ?? current.title,
|
||||
subtitle: snapshot?.subtitle ?? current.subtitle,
|
||||
highlights: snapshot?.highlights ?? current.highlights,
|
||||
description: snapshot?.description ?? current.description,
|
||||
ctaText: snapshot?.ctaText ?? current.ctaText,
|
||||
imageUrls: snapshot?.imageUrls ?? current.imageUrls,
|
||||
}
|
||||
},
|
||||
PreviewComponent: CarPagePreview,
|
||||
EditorComponent: CarPageEditor,
|
||||
previewDescription: "按钮上方展示预约试驾表单视觉区(仅演示,不提交;真实留资将直接进入企业 CRM)。",
|
||||
}
|
||||
|
||||
const topicPageTemplate: PageTemplate = {
|
||||
templateId: "topic-page",
|
||||
displayName: "专题图文模板",
|
||||
pageTypes: ["专题页"],
|
||||
defaultValues: {
|
||||
ctaText: "预约体验",
|
||||
imageUrls: [],
|
||||
topicNavTitle: "车型详情",
|
||||
topicHeroTitle: "先锋设计",
|
||||
},
|
||||
onSourceCarSelected(snapshot, current) {
|
||||
return {
|
||||
title: snapshot?.title ?? current.title,
|
||||
subtitle: snapshot?.subtitle ?? current.subtitle,
|
||||
highlights: snapshot?.highlights ?? current.highlights,
|
||||
description: snapshot?.description ?? current.description,
|
||||
ctaText: "预约体验",
|
||||
imageUrls: [],
|
||||
...buildTopicPatch(current.sourceCarName, snapshot),
|
||||
}
|
||||
},
|
||||
PreviewComponent: TopicPagePreview,
|
||||
EditorComponent: TopicPageEditor,
|
||||
previewDescription: "专题页预览:按来源车型模板自动同步,可对模块图文做手工编辑。",
|
||||
}
|
||||
|
||||
const pureImagePageTemplate: PageTemplate = {
|
||||
templateId: "pure-image-page",
|
||||
displayName: "纯图平铺模板",
|
||||
pageTypes: ["纯图页"],
|
||||
defaultValues: {
|
||||
title: "E7X详情",
|
||||
ctaText: "预约体验",
|
||||
subtitle: "",
|
||||
highlights: "",
|
||||
description: "",
|
||||
imageUrls: [
|
||||
"/images/cars/ex7-1.jpg",
|
||||
"/images/cars/ex7-2.jpg",
|
||||
"/images/cars/ex7-3.jpg",
|
||||
],
|
||||
},
|
||||
onSourceCarSelected(snapshot, current) {
|
||||
return {
|
||||
title: snapshot?.title ?? current.title,
|
||||
subtitle: snapshot?.subtitle ?? current.subtitle,
|
||||
highlights: snapshot?.highlights ?? current.highlights,
|
||||
description: snapshot?.description ?? current.description,
|
||||
ctaText: snapshot?.ctaText ?? current.ctaText,
|
||||
imageUrls: snapshot?.imageUrls ?? current.imageUrls,
|
||||
}
|
||||
},
|
||||
PreviewComponent: PureImagePagePreview,
|
||||
EditorComponent: PureImagePageEditor,
|
||||
previewDescription: "页面体仅保留纯图平铺内容,无文案、无圆角、无间距。",
|
||||
}
|
||||
|
||||
registerTemplate(carPageTemplate)
|
||||
registerTemplate(topicPageTemplate)
|
||||
registerTemplate(pureImagePageTemplate)
|
||||
|
||||
export { createTopicSectionsFromImages, buildTopicPatch }
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { PageTemplate } from "./types"
|
||||
|
||||
const templates: PageTemplate[] = []
|
||||
const pageTypeMap: Map<string, PageTemplate> = new Map()
|
||||
|
||||
export function registerTemplate(template: PageTemplate) {
|
||||
templates.push(template)
|
||||
for (const pt of template.pageTypes) {
|
||||
pageTypeMap.set(pt, template)
|
||||
}
|
||||
}
|
||||
|
||||
export function getTemplate(pageType: string): PageTemplate | undefined {
|
||||
return pageTypeMap.get(pageType)
|
||||
}
|
||||
|
||||
export function getAllPageTypes(): string[] {
|
||||
return Array.from(pageTypeMap.keys())
|
||||
}
|
||||
|
||||
export function getAllTemplates(): PageTemplate[] {
|
||||
return [...templates]
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import type { EditorProps } from "../../types"
|
||||
|
||||
export function CarPageEditor({
|
||||
content,
|
||||
canEdit,
|
||||
onUpdate,
|
||||
workflowConfig,
|
||||
newImageUrl,
|
||||
onNewImageUrlChange,
|
||||
onAddImage,
|
||||
onRemoveImage,
|
||||
}: EditorProps) {
|
||||
return (
|
||||
<>
|
||||
<div className="grid gap-2">
|
||||
<label className="text-xs font-semibold tracking-wide text-muted-foreground uppercase">标题</label>
|
||||
<Input value={content.title} disabled={!canEdit} onChange={(e) => onUpdate({ title: e.target.value })} />
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<label className="text-xs font-semibold tracking-wide text-muted-foreground uppercase">副标题</label>
|
||||
<Input value={content.subtitle} disabled={!canEdit} onChange={(e) => onUpdate({ subtitle: e.target.value })} />
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<label className="text-xs font-semibold tracking-wide text-muted-foreground uppercase">卖点(每行一条)</label>
|
||||
<textarea
|
||||
className="min-h-24 rounded-lg border border-input bg-background px-3 py-2 text-sm outline-none focus-visible:border-ring"
|
||||
value={content.highlights}
|
||||
disabled={!canEdit}
|
||||
onChange={(e) => onUpdate({ highlights: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<label className="text-xs font-semibold tracking-wide text-muted-foreground uppercase">图文描述(轻量编排)</label>
|
||||
<textarea
|
||||
className="min-h-28 rounded-lg border border-input bg-background px-3 py-2 text-sm outline-none focus-visible:border-ring"
|
||||
value={content.description}
|
||||
disabled={!canEdit}
|
||||
onChange={(e) => onUpdate({ description: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<label className="text-xs font-semibold tracking-wide text-muted-foreground uppercase">按钮文案</label>
|
||||
<Input value={content.ctaText} disabled={!canEdit} onChange={(e) => onUpdate({ ctaText: e.target.value })} />
|
||||
</div>
|
||||
|
||||
{workflowConfig.publishStrategy === "scheduled" && (
|
||||
<div className="grid gap-2">
|
||||
<label className="text-xs font-semibold tracking-wide text-muted-foreground uppercase">预约发布时间</label>
|
||||
<Input
|
||||
type="datetime-local"
|
||||
disabled={!canEdit}
|
||||
value={content.scheduledPublishAt}
|
||||
onChange={(e) => onUpdate({ scheduledPublishAt: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid gap-2">
|
||||
<label className="text-xs font-semibold tracking-wide text-muted-foreground uppercase">图片素材(多图)</label>
|
||||
<div className="flex gap-2">
|
||||
<Input value={newImageUrl ?? ""} disabled={!canEdit} onChange={(e) => onNewImageUrlChange?.(e.target.value)} placeholder="粘贴图片 URL" />
|
||||
<Button variant="outline" disabled={!canEdit} onClick={() => onAddImage?.()}>新增</Button>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{content.imageUrls.map((url, idx) => (
|
||||
<div key={`${url}-${idx}`} className="rounded-lg border p-2">
|
||||
<img src={url} alt={`${content.sourceCarName}-${idx}`} className="aspect-video w-full rounded object-cover" />
|
||||
<div className="mt-2 flex items-center justify-between gap-2">
|
||||
<span className="truncate text-[11px] text-muted-foreground">{url}</span>
|
||||
<Button size="sm" variant="ghost" disabled={!canEdit} onClick={() => onRemoveImage?.(idx)}>
|
||||
删除
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
import { ChevronLeft, MoreHorizontal, X } from "lucide-react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import type { PreviewProps } from "../../types"
|
||||
|
||||
export function CarPagePreview({ content, workflowConfig }: PreviewProps) {
|
||||
const previewHeroImage = content.imageUrls[0]
|
||||
const highlights = content.highlights
|
||||
.split("\n")
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean)
|
||||
|
||||
return (
|
||||
<div className="flex h-[780px] w-[360px] flex-col overflow-hidden rounded-[28px] border border-gray-200 bg-white shadow-2xl">
|
||||
<div className="flex h-11 items-end justify-between bg-black px-8 pb-2 text-[15px] font-semibold tracking-tight text-white">
|
||||
<span>9:41</span>
|
||||
<div className="flex items-center gap-1.5 pb-1">
|
||||
<svg width="17" height="11" viewBox="0 0 17 11" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||||
<path d="M0 8.35L2.83 8.35L2.83 10.5H0V8.35ZM4.25 6.2H7.08V10.5H4.25V6.2ZM8.5 4.05H11.33V10.5H8.5V4.05ZM12.75 1.9H15.58V10.5H12.75V1.9Z" fill="white" />
|
||||
</svg>
|
||||
<svg width="16" height="12" viewBox="0 0 16 12" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||||
<path d="M8 12L16 3.10931C11.5768 -0.493103 4.42323 -0.493103 0 3.10931L8 12Z" fill="white" />
|
||||
</svg>
|
||||
<div className="relative h-[11.33px] w-[24.33px] rounded-[3px] border border-white/35 p-[1px]">
|
||||
<div className="h-full w-[18px] rounded-[1px] bg-white" />
|
||||
<div className="absolute -right-[3px] top-[3px] h-[5px] w-[2px] rounded-r-[1px] bg-white/35" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<header className="flex h-14 items-center justify-between bg-black/85 px-4 backdrop-blur-md">
|
||||
<button className="p-2 text-white/90" type="button" aria-label="返回">
|
||||
<ChevronLeft className="h-6 w-6" />
|
||||
</button>
|
||||
<div className="h-7" />
|
||||
<div className="flex h-8 items-center rounded-full border border-white/10 bg-black/40 px-1">
|
||||
<button className="border-r border-white/10 px-2 text-white/90" type="button" aria-label="更多">
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</button>
|
||||
<button className="px-2 text-white/90" type="button" aria-label="关闭">
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="min-h-0 flex-1 overflow-y-auto bg-white [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
|
||||
<div className="relative">
|
||||
{previewHeroImage ? (
|
||||
<img src={previewHeroImage} alt={content.title} className="h-48 w-full object-cover" />
|
||||
) : (
|
||||
<div className="flex h-48 w-full items-center justify-center bg-gray-100 text-xs text-gray-500">请先添加图片素材</div>
|
||||
)}
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/65 to-transparent" />
|
||||
<div className="absolute left-4 right-4 bottom-3 text-white">
|
||||
<p className="text-lg font-bold tracking-tight">{content.title}</p>
|
||||
<p className="text-xs opacity-85">{content.subtitle}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4">
|
||||
<div className="mb-3 grid grid-cols-3 gap-2">
|
||||
{content.imageUrls.slice(0, 3).map((url, idx) => (
|
||||
<img key={`${url}-${idx}`} src={url} alt={`thumb-${idx}`} className="h-16 w-full rounded object-cover" />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
{highlights.map((item) => (
|
||||
<div key={item} className="inline-flex mr-2 mb-1 rounded-full border px-2 py-0.5 text-[10px] text-gray-600">
|
||||
{item}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<p className="mt-3 whitespace-pre-line text-sm leading-6 text-gray-600">{content.description}</p>
|
||||
|
||||
{workflowConfig.publishStrategy === "scheduled" && content.scheduledPublishAt && (
|
||||
<div className="mt-3 rounded-md border border-blue-200 bg-blue-50 px-3 py-2 text-xs text-blue-700">
|
||||
预约发布时间:{content.scheduledPublishAt.replace("T", " ")}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-4 space-y-3 rounded-lg border border-gray-200 bg-gray-50 p-3">
|
||||
<div className="grid gap-1">
|
||||
<label className="text-[10px] uppercase tracking-wider text-gray-500 font-semibold">姓名</label>
|
||||
<Input placeholder="请输入您的姓名" disabled />
|
||||
</div>
|
||||
<div className="grid gap-1">
|
||||
<label className="text-[10px] uppercase tracking-wider text-gray-500 font-semibold">手机号</label>
|
||||
<Input placeholder="请输入手机号" disabled />
|
||||
</div>
|
||||
<div className="grid gap-1">
|
||||
<label className="text-[10px] uppercase tracking-wider text-gray-500 font-semibold">意向城市</label>
|
||||
<div className="flex gap-2">
|
||||
<select className="h-8 flex-1 rounded-lg border border-input bg-white px-2 text-sm text-gray-500" disabled>
|
||||
<option>请选择城市</option>
|
||||
</select>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => alert("获取位置功能为原型占位提示")}
|
||||
>
|
||||
获取位置
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label className="flex items-start gap-2 text-[11px] leading-5 text-gray-600">
|
||||
<input type="checkbox" className="mt-0.5" />
|
||||
<span>
|
||||
我已阅读并同意《个人信息保护政策》,授权奥迪及授权经销商与我联系。信息提交后将直接进入企业 CRM,本平台不留存潜客数据。
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<Button className="mt-4 w-full bg-audi-black hover:bg-audi-dark-gray text-white" disabled>
|
||||
{content.ctaText || "立即预约试驾"}
|
||||
</Button>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import type { EditorProps } from "../../types"
|
||||
|
||||
export function PureImagePageEditor({
|
||||
content,
|
||||
canEdit,
|
||||
onUpdate,
|
||||
newImageUrl,
|
||||
onNewImageUrlChange,
|
||||
onAddImage,
|
||||
onRemoveImage,
|
||||
}: EditorProps) {
|
||||
return (
|
||||
<>
|
||||
<div className="grid gap-2">
|
||||
<label className="text-xs font-semibold tracking-wide text-muted-foreground uppercase">标题</label>
|
||||
<Input value={content.title} disabled={!canEdit} onChange={(e) => onUpdate({ title: e.target.value })} />
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<label className="text-xs font-semibold tracking-wide text-muted-foreground uppercase">按钮方案</label>
|
||||
<Input value={content.ctaText} disabled={!canEdit} onChange={(e) => onUpdate({ ctaText: e.target.value })} />
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<label className="text-xs font-semibold tracking-wide text-muted-foreground uppercase">图片素材(多图)</label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
value={newImageUrl ?? ""}
|
||||
disabled={!canEdit}
|
||||
onChange={(e) => onNewImageUrlChange?.(e.target.value)}
|
||||
placeholder="粘贴图片 URL"
|
||||
/>
|
||||
<Button variant="outline" disabled={!canEdit} onClick={() => onAddImage?.()}>
|
||||
新增
|
||||
</Button>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{content.imageUrls.map((url, idx) => (
|
||||
<div key={`${url}-${idx}`} className="rounded-lg border p-2">
|
||||
<img src={url} alt={`${content.title}-${idx}`} className="aspect-video w-full rounded object-cover" />
|
||||
<div className="mt-2 flex items-center justify-between gap-2">
|
||||
<span className="truncate text-[11px] text-muted-foreground">{url}</span>
|
||||
<Button size="sm" variant="ghost" disabled={!canEdit} onClick={() => onRemoveImage?.(idx)}>
|
||||
删除
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import { ChevronLeft, MoreHorizontal, X } from "lucide-react"
|
||||
import type { PreviewProps } from "../../types"
|
||||
|
||||
export function PureImagePagePreview({ content }: PreviewProps) {
|
||||
const imageUrls = content.imageUrls.filter(Boolean)
|
||||
|
||||
return (
|
||||
<div className="flex h-[780px] w-[360px] flex-col overflow-hidden rounded-[28px] border border-gray-200 bg-black text-white shadow-2xl">
|
||||
<div className="flex h-11 items-end justify-between px-8 pb-2 text-[15px] font-semibold tracking-tight">
|
||||
<span>9:41</span>
|
||||
<div className="flex items-center gap-1.5 pb-1">
|
||||
<svg width="17" height="11" viewBox="0 0 17 11" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||||
<path d="M0 8.35L2.83 8.35L2.83 10.5H0V8.35ZM4.25 6.2H7.08V10.5H4.25V6.2ZM8.5 4.05H11.33V10.5H8.5V4.05ZM12.75 1.9H15.58V10.5H12.75V1.9Z" fill="white" />
|
||||
</svg>
|
||||
<svg width="16" height="12" viewBox="0 0 16 12" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||||
<path d="M8 12L16 3.10931C11.5768 -0.493103 4.42323 -0.493103 0 3.10931L8 12Z" fill="white" />
|
||||
</svg>
|
||||
<div className="relative h-[11.33px] w-[24.33px] rounded-[3px] border border-white/35 p-[1px]">
|
||||
<div className="h-full w-[18px] rounded-[1px] bg-white" />
|
||||
<div className="absolute -right-[3px] top-[3px] h-[5px] w-[2px] rounded-r-[1px] bg-white/35" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<header className="flex h-14 items-center justify-between bg-black/85 px-4 backdrop-blur-md">
|
||||
<button className="p-2 text-white/90" type="button" aria-label="返回">
|
||||
<ChevronLeft className="h-6 w-6" />
|
||||
</button>
|
||||
<h1 className="text-lg font-medium tracking-wide">{content.title || "E7X详情"}</h1>
|
||||
<div className="flex h-8 items-center rounded-full border border-white/10 bg-black/40 px-1">
|
||||
<button className="border-r border-white/10 px-2 text-white/90" type="button" aria-label="更多">
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</button>
|
||||
<button className="px-2 text-white/90" type="button" aria-label="关闭">
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="min-h-0 flex-1 overflow-y-auto bg-black [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
|
||||
{imageUrls.length > 0 ? (
|
||||
imageUrls.map((url, idx) => (
|
||||
<img
|
||||
key={`${url}-${idx}`}
|
||||
src={url}
|
||||
alt={`${content.title || "E7X详情"}-${idx + 1}`}
|
||||
className="block w-full"
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<div className="flex min-h-[480px] items-center justify-center bg-neutral-900 text-sm text-white/60">
|
||||
请先添加图片素材
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
|
||||
<footer className="shrink-0 bg-black px-5 py-3.5">
|
||||
<button
|
||||
type="button"
|
||||
className="w-full border border-white bg-transparent py-2.5 text-sm font-medium text-white"
|
||||
>
|
||||
{content.ctaText || "预约体验"}
|
||||
</button>
|
||||
<div className="mt-3 flex justify-center">
|
||||
<div className="h-1 w-24 rounded-full bg-white/20" />
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import type { EditorProps } from "../../types"
|
||||
import { createTopicSectionsFromImages } from "./helpers"
|
||||
|
||||
export function TopicPageEditor({
|
||||
content,
|
||||
canEdit,
|
||||
onUpdate,
|
||||
topicSections: topicSectionsProp,
|
||||
onUpdateTopicSection,
|
||||
onAddTopicSection,
|
||||
onRemoveTopicSection,
|
||||
}: EditorProps) {
|
||||
const topicSections = topicSectionsProp
|
||||
?? ((content.topicSections && content.topicSections.length > 0)
|
||||
? content.topicSections
|
||||
: createTopicSectionsFromImages(content.imageUrls))
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="grid gap-2">
|
||||
<label className="text-xs font-semibold tracking-wide text-muted-foreground uppercase">导航标题</label>
|
||||
<Input
|
||||
value={content.topicNavTitle ?? `${content.sourceCarName || "车型"}详情`}
|
||||
disabled={!canEdit}
|
||||
onChange={(e) => onUpdate({ topicNavTitle: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<label className="text-xs font-semibold tracking-wide text-muted-foreground uppercase">主视觉标题</label>
|
||||
<Input
|
||||
value={content.topicHeroTitle ?? "先锋设计"}
|
||||
disabled={!canEdit}
|
||||
onChange={(e) => onUpdate({ topicHeroTitle: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<label className="text-xs font-semibold tracking-wide text-muted-foreground uppercase">底部按钮文案</label>
|
||||
<Input value={content.ctaText} disabled={!canEdit} onChange={(e) => onUpdate({ ctaText: e.target.value })} />
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-xs font-semibold tracking-wide text-muted-foreground uppercase">专题模块(多个图文)</label>
|
||||
<Button size="sm" variant="outline" disabled={!canEdit || topicSections.length >= 6} onClick={() => onAddTopicSection?.()}>
|
||||
新增模块
|
||||
</Button>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{topicSections.map((section, index) => (
|
||||
<div key={section.id} className="rounded-lg border p-3 space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-xs font-semibold text-muted-foreground">模块 {index + 1}</p>
|
||||
<Button size="sm" variant="ghost" disabled={!canEdit || topicSections.length <= 1} onClick={() => onRemoveTopicSection?.(section.id)}>
|
||||
删除
|
||||
</Button>
|
||||
</div>
|
||||
<Input
|
||||
value={section.title}
|
||||
disabled={!canEdit}
|
||||
onChange={(e) => onUpdateTopicSection?.(section.id, { title: e.target.value })}
|
||||
placeholder="模块标题"
|
||||
/>
|
||||
<Input
|
||||
value={section.imageUrl}
|
||||
disabled={!canEdit}
|
||||
onChange={(e) => onUpdateTopicSection?.(section.id, { imageUrl: e.target.value })}
|
||||
placeholder="模块图片 URL"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import { ChevronLeft, MoreHorizontal, X } from "lucide-react"
|
||||
import type { PreviewProps } from "../../types"
|
||||
import { createTopicSectionsFromImages } from "./helpers"
|
||||
|
||||
export function TopicPagePreview({ content }: PreviewProps) {
|
||||
const topicSections = (content.topicSections && content.topicSections.length > 0)
|
||||
? content.topicSections
|
||||
: createTopicSectionsFromImages(content.imageUrls)
|
||||
|
||||
return (
|
||||
<div className="relative flex h-[780px] w-[360px] flex-col overflow-hidden rounded-[28px] border border-gray-200 bg-white text-[#1a1a1a] shadow-2xl">
|
||||
<div className="flex h-11 items-end justify-between bg-white px-8 pb-2 text-[15px] font-semibold tracking-tight text-[#1a1a1a]">
|
||||
<span>9:41</span>
|
||||
<div className="flex items-center gap-1.5 pb-1">
|
||||
<svg width="17" height="11" viewBox="0 0 17 11" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||||
<path d="M0 8.35L2.83 8.35L2.83 10.5H0V8.35ZM4.25 6.2H7.08V10.5H4.25V6.2ZM8.5 4.05H11.33V10.5H8.5V4.05ZM12.75 1.9H15.58V10.5H12.75V1.9Z" fill="#1a1a1a" />
|
||||
</svg>
|
||||
<svg width="16" height="12" viewBox="0 0 16 12" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||||
<path d="M8 12L16 3.10931C11.5768 -0.493103 4.42323 -0.493103 0 3.10931L8 12Z" fill="#1a1a1a" />
|
||||
</svg>
|
||||
<div className="relative h-[11.33px] w-[24.33px] rounded-[3px] border border-black/35 p-[1px]">
|
||||
<div className="h-full w-[18px] rounded-[1px] bg-[#1a1a1a]" />
|
||||
<div className="absolute -right-[3px] top-[3px] h-[5px] w-[2px] rounded-r-[1px] bg-black/35" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<header className="flex h-14 items-center justify-between bg-white px-4">
|
||||
<button className="p-2 text-[#1a1a1a]" type="button" aria-label="返回">
|
||||
<ChevronLeft className="h-6 w-6" />
|
||||
</button>
|
||||
<h1 className="text-lg font-medium tracking-wide">{content.topicNavTitle || `${content.sourceCarName || "车型"}详情`}</h1>
|
||||
<div className="flex h-8 items-center rounded-full border border-gray-200 bg-gray-50 px-1">
|
||||
<button className="border-r border-gray-200 px-2 text-gray-500" type="button" aria-label="更多">
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</button>
|
||||
<button className="px-2 text-gray-500" type="button" aria-label="关闭">
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="min-h-0 flex-1 overflow-y-auto pb-2 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
|
||||
<section className="pt-4 pb-3 text-center">
|
||||
<h2 className="mb-1 text-[28px] font-bold tracking-tight">{content.topicHeroTitle || "先锋设计"}</h2>
|
||||
<div className="w-12 h-[3px] bg-[#d51c2a] mx-auto" />
|
||||
</section>
|
||||
|
||||
<div className="space-y-2 px-0">
|
||||
{topicSections.map((section) => (
|
||||
<section key={section.id} className="bg-[#f8f9fb] rounded-t-[32px] overflow-hidden">
|
||||
<div className="aspect-[16/9] relative overflow-hidden">
|
||||
{section.imageUrl ? (
|
||||
<img
|
||||
src={section.imageUrl}
|
||||
alt={section.title}
|
||||
className="w-full h-full object-cover mix-blend-multiply"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full h-full flex items-center justify-center text-xs text-gray-500 bg-gray-100">请添加模块图片</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="px-5 py-1.5">
|
||||
<h3 className="text-[19px] font-normal text-gray-700">{section.title}</h3>
|
||||
</div>
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer className="shrink-0 border-t border-gray-100 bg-white/80 px-4 pt-2 pb-4 backdrop-blur-md">
|
||||
<button className="w-full rounded-md bg-[#1c1f23] py-3 text-[15px] font-medium text-white">
|
||||
{content.ctaText || "预约体验"}
|
||||
</button>
|
||||
<div className="mx-auto mt-3 h-1 w-24 rounded-full bg-black/80" />
|
||||
</footer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { MiniAppContent, SourceSnapshot, TopicSection } from "../../types"
|
||||
|
||||
export function createTopicSectionsFromImages(images: string[]): TopicSection[] {
|
||||
const picks = images.slice(0, 2)
|
||||
const fallback = ["/images/cars/a7-1.jpg", "/images/cars/a7-2.jpg"]
|
||||
const selected = picks.length > 0 ? picks : fallback
|
||||
|
||||
return selected.map((imageUrl, index) => ({
|
||||
id: `topic-section-${index + 1}`,
|
||||
title: index === 0 ? "RS竞速套件" : "宽体低趴 超长轴距",
|
||||
imageUrl,
|
||||
specLeftLabel: "长",
|
||||
specRightLabel: "宽",
|
||||
}))
|
||||
}
|
||||
|
||||
export function buildTopicPatch(sourceCarName: string, snapshot?: SourceSnapshot): Pick<MiniAppContent, "topicNavTitle" | "topicHeroTitle" | "topicSections"> {
|
||||
const hero = snapshot?.topicHeroTitle ?? snapshot?.subtitle ?? "先锋设计"
|
||||
return {
|
||||
topicNavTitle: snapshot?.topicNavTitle ?? `${sourceCarName || "车型"}详情`,
|
||||
topicHeroTitle: hero,
|
||||
topicSections: snapshot?.topicSections ?? createTopicSectionsFromImages(snapshot?.imageUrls ?? []),
|
||||
}
|
||||
}
|
||||
85
audi-content-portal/src/components/content-library/types.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import type * as React from "react"
|
||||
import type { WorkflowConfig } from "@/App"
|
||||
|
||||
export type WorkflowStatus = "draft" | "prepublished" | "published"
|
||||
|
||||
export type TopicSection = {
|
||||
id: string
|
||||
title: string
|
||||
imageUrl: string
|
||||
specLeftLabel: string
|
||||
specRightLabel: string
|
||||
}
|
||||
|
||||
export type SourceSnapshot = {
|
||||
title: string
|
||||
subtitle: string
|
||||
highlights: string
|
||||
description: string
|
||||
ctaText: string
|
||||
imageUrls: string[]
|
||||
topicNavTitle?: string
|
||||
topicHeroTitle?: string
|
||||
topicSections?: TopicSection[]
|
||||
}
|
||||
|
||||
export type MiniAppContent = {
|
||||
id: string
|
||||
sourceCarId: string
|
||||
sourceCarName: string
|
||||
pageType: string
|
||||
title: string
|
||||
subtitle: string
|
||||
highlights: string
|
||||
description: string
|
||||
ctaText: string
|
||||
scheduledPublishAt: string
|
||||
imageUrls: string[]
|
||||
topicNavTitle?: string
|
||||
topicHeroTitle?: string
|
||||
topicSections?: TopicSection[]
|
||||
workflowStatus: WorkflowStatus
|
||||
updatedBy: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
export type PreviewProps = {
|
||||
content: MiniAppContent
|
||||
workflowConfig: WorkflowConfig
|
||||
}
|
||||
|
||||
export type EditorProps = {
|
||||
content: MiniAppContent
|
||||
canEdit: boolean
|
||||
onUpdate: (patch: Partial<MiniAppContent>) => void
|
||||
workflowConfig: WorkflowConfig
|
||||
/** For car-page image management */
|
||||
newImageUrl?: string
|
||||
onNewImageUrlChange?: (url: string) => void
|
||||
onAddImage?: () => void
|
||||
onRemoveImage?: (idx: number) => void
|
||||
/** For topic-page section management */
|
||||
onUpdateTopicSection?: (sectionId: string, patch: Partial<TopicSection>) => void
|
||||
onAddTopicSection?: () => void
|
||||
onRemoveTopicSection?: (sectionId: string) => void
|
||||
topicSections?: TopicSection[]
|
||||
}
|
||||
|
||||
export type PageTemplate = {
|
||||
/** Unique template identifier (kebab-case) */
|
||||
templateId: string
|
||||
/** Human-friendly template name for UI display */
|
||||
displayName: string
|
||||
/** Page type labels that map to this template */
|
||||
pageTypes: string[]
|
||||
/** Default values when creating content with this template */
|
||||
defaultValues: Partial<MiniAppContent>
|
||||
/** Called when a source car is selected */
|
||||
onSourceCarSelected?: (snapshot: SourceSnapshot | undefined, current: MiniAppContent) => Partial<MiniAppContent>
|
||||
/** Preview component for the phone mockup */
|
||||
PreviewComponent: React.ComponentType<PreviewProps>
|
||||
/** Editor fields component */
|
||||
EditorComponent: React.ComponentType<EditorProps>
|
||||
/** Description shown below the preview card */
|
||||
previewDescription?: string
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import * as React from "react"
|
||||
import {
|
||||
Car,
|
||||
NotebookPen,
|
||||
LayoutTemplate,
|
||||
UserCog,
|
||||
Settings,
|
||||
LogOut,
|
||||
@@ -34,6 +35,11 @@ const navItems = [
|
||||
icon: NotebookPen,
|
||||
id: "mini-content",
|
||||
},
|
||||
{
|
||||
title: "小程序页面模板",
|
||||
icon: LayoutTemplate,
|
||||
id: "templates",
|
||||
},
|
||||
{
|
||||
title: "用户与权限",
|
||||
icon: UserCog,
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
import * as React from "react"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { X } from "lucide-react"
|
||||
import { getAllTemplates } from "@/components/content-library/template-registry"
|
||||
import "@/components/content-library/register-templates"
|
||||
import type { MiniAppContent } from "@/components/content-library/types"
|
||||
import type { WorkflowConfig } from "@/App"
|
||||
|
||||
const defaultWorkflowConfig: WorkflowConfig = {
|
||||
enablePrePublish: true,
|
||||
allowRecall: true,
|
||||
publishStrategy: "manual",
|
||||
defaultPublishTime: "10:00",
|
||||
syncFrequency: "weekly",
|
||||
syncExecutionTime: "02:00",
|
||||
retryCount: 2,
|
||||
manualConfirmSync: true,
|
||||
}
|
||||
|
||||
function makeSampleContent(overrides: Partial<MiniAppContent> = {}): MiniAppContent {
|
||||
return {
|
||||
id: "sample",
|
||||
sourceCarId: "AUDI_A7SPB",
|
||||
sourceCarName: "Audi A7 Sportback",
|
||||
pageType: "车型页",
|
||||
title: "A7 Sportback",
|
||||
subtitle: "优雅轿跑",
|
||||
highlights: "340 马力 · quattro 四驱 · 5.3s 破百",
|
||||
description: "以优雅之姿,释放澎湃动力。",
|
||||
ctaText: "立即预约试驾",
|
||||
scheduledPublishAt: "",
|
||||
imageUrls: [
|
||||
"/images/cars/a7-1.jpg",
|
||||
"/images/cars/a7-2.jpg",
|
||||
"/images/cars/a7-3.jpg",
|
||||
],
|
||||
workflowStatus: "draft",
|
||||
updatedBy: "系统",
|
||||
updatedAt: "2026-04-16",
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
function getTemplateSampleContent(templateId: string, defaults: Partial<MiniAppContent>): MiniAppContent {
|
||||
if (templateId === "car-page") {
|
||||
return makeSampleContent({
|
||||
sourceCarId: "a3",
|
||||
sourceCarName: "Audi A3 Sportback",
|
||||
pageType: "车型页",
|
||||
title: "Audi A3 Sportback",
|
||||
subtitle: "进取,不负期待",
|
||||
highlights: "数字座舱\n城市通勤\n智能互联",
|
||||
description: "适合城市用户的豪华紧凑车型,兼顾效率与智能体验。",
|
||||
ctaText: "立即预约试驾",
|
||||
imageUrls: [
|
||||
"/images/cars/a3-1.jpg",
|
||||
"/images/cars/a3-2.jpg",
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
return makeSampleContent({
|
||||
pageType: defaults.pageType as string,
|
||||
title: (defaults.title as string) ?? "A7 Sportback",
|
||||
ctaText: (defaults.ctaText as string) ?? "了解更多",
|
||||
imageUrls: (defaults.imageUrls as string[]) ?? [
|
||||
"/images/cars/a7-1.jpg",
|
||||
"/images/cars/a7-2.jpg",
|
||||
"/images/cars/a7-3.jpg",
|
||||
],
|
||||
topicNavTitle: defaults.topicNavTitle as string | undefined,
|
||||
topicHeroTitle: defaults.topicHeroTitle as string | undefined,
|
||||
topicSections: defaults.topicSections as MiniAppContent["topicSections"],
|
||||
})
|
||||
}
|
||||
|
||||
export function TemplateManagement() {
|
||||
const templates = getAllTemplates()
|
||||
const [expandedTemplateId, setExpandedTemplateId] = React.useState<string | null>(null)
|
||||
|
||||
const expandedTemplate = expandedTemplateId
|
||||
? templates.find((t) => t.templateId === expandedTemplateId)
|
||||
: null
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
<div>
|
||||
<h2 className="text-lg font-bold">页面模板管理</h2>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
所有已注册的页面模板。模板仅通过代码注册,无需手动增删。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Preview cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
|
||||
{templates.map((t) => {
|
||||
const Preview = t.PreviewComponent
|
||||
const sampleContent = getTemplateSampleContent(t.templateId, {
|
||||
...t.defaultValues,
|
||||
pageType: t.pageTypes[0],
|
||||
})
|
||||
return (
|
||||
<Card key={t.templateId} className="overflow-hidden">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-semibold">{t.displayName}</CardTitle>
|
||||
{t.previewDescription && (
|
||||
<p className="text-xs text-muted-foreground mt-1">{t.previewDescription}</p>
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent className="flex justify-center">
|
||||
<button
|
||||
type="button"
|
||||
className="cursor-pointer rounded-xl border border-gray-200 overflow-hidden hover:shadow-lg transition-shadow w-[200px] h-[350px]"
|
||||
onClick={() => setExpandedTemplateId(t.templateId)}
|
||||
title="点击放大预览"
|
||||
>
|
||||
<div className="origin-top-left scale-[0.555] w-[360px] pointer-events-none">
|
||||
<Preview content={sampleContent} workflowConfig={defaultWorkflowConfig} />
|
||||
</div>
|
||||
</button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Fullscreen modal */}
|
||||
{expandedTemplate && (
|
||||
<div
|
||||
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm"
|
||||
onClick={() => setExpandedTemplateId(null)}
|
||||
>
|
||||
<div
|
||||
className="relative bg-white rounded-2xl shadow-2xl p-6 max-h-[90vh] overflow-auto"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<span className="font-semibold">{expandedTemplate.displayName}</span>
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8" onClick={() => setExpandedTemplateId(null)}>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex justify-center">
|
||||
<expandedTemplate.PreviewComponent
|
||||
content={getTemplateSampleContent(expandedTemplate.templateId, {
|
||||
...expandedTemplate.defaultValues,
|
||||
pageType: expandedTemplate.pageTypes[0],
|
||||
})}
|
||||
workflowConfig={defaultWorkflowConfig}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||