Files
audi-rednote/audi-content-portal/src/components/content-library/MiniAppContentLibrary.tsx

1117 lines
46 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import * as React from "react"
import {
ArrowLeft,
Eye,
Pencil,
RotateCcw,
Search,
Send,
ShieldCheck,
Trash2,
XCircle,
} from "lucide-react"
import type { RoleView, WorkflowConfig } from "@/App"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
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
}
type MiniAppContentLibraryProps = {
roleView: RoleView
workflowConfig: WorkflowConfig
onAddAuditLog: (message: string) => void
newContentRequest?: {
sourceCarId: string
sourceCarName: string
requestId: string
} | null
}
const CONTENTS: MiniAppContent[] = [
{
id: "c1",
sourceCarId: "a3",
sourceCarName: "Audi A3 Sportback",
pageType: "车型页",
title: "Audi A3 Sportback",
subtitle: "进取,不负期待",
highlights: "数字座舱\n城市通勤\n智能互联",
description: "适合城市用户的豪华紧凑车型,兼顾效率与智能体验。",
ctaText: "立即预约试驾",
scheduledPublishAt: "",
imageUrls: [
"/images/cars/a3-1.jpg",
"/images/cars/a3-2.jpg",
],
workflowStatus: "published",
updatedBy: "内容运营专员",
updatedAt: "2026-04-11 10:02",
},
{
id: "c2",
sourceCarId: "a4l",
sourceCarName: "Audi A4L",
pageType: "活动页",
title: "Audi A4L 四月限时礼遇",
subtitle: "豪华与性能平衡",
highlights: "quattro\n商务舒适\n限时礼遇",
description: "面向商务人群的主推内容版本,突出舒适与操控。",
ctaText: "获取活动详情",
scheduledPublishAt: "2026-04-14T10:00",
imageUrls: [
"/images/cars/a4-1.jpg",
],
workflowStatus: "prepublished",
updatedBy: "业务/市场负责人",
updatedAt: "2026-04-11 09:48",
},
{
id: "c3",
sourceCarId: "a6l",
sourceCarName: "Audi A6L",
pageType: "车型页",
title: "Audi A6L",
subtitle: "懂你,更懂未来",
highlights: "行政旗舰\n长轴空间\n智能辅助",
description: "正在进行文案优化,待业务审核。",
ctaText: "预约顾问回电",
scheduledPublishAt: "",
imageUrls: [
"/images/cars/a6-1.jpg",
"/images/cars/a6-2.jpg",
],
workflowStatus: "prepublished",
updatedBy: "内容运营专员",
updatedAt: "2026-04-11 09:30",
},
{
id: "c4",
sourceCarId: "q3",
sourceCarName: "Audi Q3",
pageType: "专题页",
title: "Audi Q3 城市灵动版",
subtitle: "年轻进阶,灵动出行",
highlights: "紧凑SUV\n智能互联\n都市通勤",
description: "新建草稿,待进一步补充图文和活动权益信息。",
ctaText: "预约试驾",
scheduledPublishAt: "",
imageUrls: [
"/images/cars/q3-1.jpg",
],
topicNavTitle: "Q3详情",
topicHeroTitle: "先锋设计",
topicSections: [
{
id: "q3-topic-1",
title: "RS竞速套件",
imageUrl: "/images/cars/q3-1.jpg",
specLeftLabel: "长",
specRightLabel: "宽",
},
{
id: "q3-topic-2",
title: "宽体低趴 超长轴距",
imageUrl: "/images/cars/q3-2.jpg",
specLeftLabel: "长",
specRightLabel: "宽",
},
],
workflowStatus: "draft",
updatedBy: "内容运营专员",
updatedAt: "2026-04-11 08:40",
},
{
id: "c5",
sourceCarId: "q5l",
sourceCarName: "Audi Q5L",
pageType: "活动页",
title: "Audi Q5L 周末试驾礼遇",
subtitle: "自由,由我定义",
highlights: "四驱性能\n家庭空间\n周末活动",
description: "因权益文案与活动规则不一致,被驳回待修订。",
ctaText: "了解礼遇",
scheduledPublishAt: "",
imageUrls: [
"/images/cars/q5-1.jpg",
"/images/cars/q5-2.jpg",
],
workflowStatus: "draft",
updatedBy: "业务/市场负责人",
updatedAt: "2026-04-11 08:10",
},
{
id: "c6",
sourceCarId: "a8l",
sourceCarName: "Audi A8L",
pageType: "车型页",
title: "Audi A8L 尊享礼宾版",
subtitle: "旗舰格局,沉稳之选",
highlights: "旗舰行政\n豪华座舱\n专属服务",
description: "已发布后因活动档期调整撤回,待重新排期。",
ctaText: "预约专属顾问",
scheduledPublishAt: "2026-04-15T09:30",
imageUrls: [
"/images/cars/a8-1.jpg",
],
workflowStatus: "published",
updatedBy: "业务/市场负责人",
updatedAt: "2026-04-11 07:55",
},
{
id: "c7",
sourceCarId: "rs7",
sourceCarName: "Audi RS7",
pageType: "专题页",
title: "Audi RS7 Performance",
subtitle: "高性能美学,锋芒尽释",
highlights: "V8双涡轮\nquattro四驱\nRS专属运动套件",
description: "聚焦高性能驾驶体验与豪华运动设计,适配高意向用户内容触达场景。",
ctaText: "预约性能试驾",
scheduledPublishAt: "",
imageUrls: [
"/images/cars/rs7-1.jpg",
"/images/cars/rs7-2.jpg",
],
workflowStatus: "draft",
updatedBy: "内容运营专员",
updatedAt: "2026-04-12 10:20",
},
]
const WORKFLOW_LABEL: Record<WorkflowStatus, string> = {
draft: "草稿",
prepublished: "预发布",
published: "已发布",
}
const SOURCE_SNAPSHOTS: Record<string, SourceSnapshot> = {
a3: {
title: "Audi A3 Sportback",
subtitle: "进取,不负期待",
highlights: "数字座舱\n城市通勤\n智能互联",
description: "来自官网车型库的车型基线内容。",
ctaText: "立即预约试驾",
imageUrls: [
"/images/cars/a3-1.jpg",
"/images/cars/a3-2.jpg",
],
},
a4l: {
title: "Audi A4L",
subtitle: "做更强大的自己",
highlights: "quattro\n商务舒适\n智能互联",
description: "来自官网车型库的车型基线内容。",
ctaText: "了解更多",
imageUrls: [
"/images/cars/a4-1.jpg",
],
},
a6l: {
title: "Audi A6L",
subtitle: "懂你,更懂未来",
highlights: "行政旗舰\n长轴空间\n智能辅助",
description: "来自官网车型库的车型基线内容。",
ctaText: "预约顾问回电",
imageUrls: [
"/images/cars/a6-1.jpg",
"/images/cars/a6-2.jpg",
],
},
q3: {
title: "Audi Q3",
subtitle: "活出生命的辽阔",
highlights: "紧凑SUV\n智能互联\n都市通勤",
description: "来自官网车型库的车型基线内容。",
ctaText: "立即预约试驾",
imageUrls: [
"/images/cars/q3-1.jpg",
"/images/cars/q3-2.jpg",
],
},
q5l: {
title: "Audi Q5L",
subtitle: "自由,由我定义",
highlights: "四驱性能\n家庭空间\n全场景出行",
description: "来自官网车型库的车型基线内容。",
ctaText: "预约试驾",
imageUrls: [
"/images/cars/q5-1.jpg",
"/images/cars/q5-2.jpg",
],
},
a8l: {
title: "Audi A8L",
subtitle: "旗舰格局,沉稳之选",
highlights: "旗舰行政\n豪华座舱\n专属服务",
description: "来自官网车型库的车型基线内容。",
ctaText: "预约专属顾问",
imageUrls: [
"/images/cars/a8-1.jpg",
"/images/cars/a8-2.jpg",
],
},
rs7: {
title: "Audi RS7 Performance",
subtitle: "高性能美学,锋芒尽释",
highlights: "V8双涡轮\nquattro四驱\nRS专属运动套件",
description: "来自官网车型库的车型基线内容。",
ctaText: "预约性能试驾",
imageUrls: [
"/images/cars/rs7-1.jpg",
"/images/cars/rs7-2.jpg",
],
},
}
const SOURCE_CAR_OPTIONS = [
{ id: "a3", name: "Audi A3 Sportback" },
{ id: "a4l", name: "Audi A4L" },
{ id: "a6l", name: "Audi A6L" },
{ id: "q3", name: "Audi Q3" },
{ id: "q5l", name: "Audi Q5L" },
{ id: "a8l", name: "Audi A8L" },
{ id: "rs7", name: "Audi RS7" },
] 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":
return "bg-gray-100 text-gray-700 border-gray-200"
case "prepublished":
return "bg-blue-50 text-blue-700 border-blue-200"
case "published":
return "bg-emerald-50 text-emerald-700 border-emerald-200"
}
}
function nowString() {
return new Date().toLocaleString("zh-CN", { hour12: false })
}
export function MiniAppContentLibrary({ roleView, workflowConfig, onAddAuditLog, newContentRequest }: MiniAppContentLibraryProps) {
const [contents, setContents] = React.useState<MiniAppContent[]>(CONTENTS)
const [viewMode, setViewMode] = React.useState<"list" | "editor">("list")
const [selectedId, setSelectedId] = React.useState<string>(CONTENTS[0].id)
const [query, setQuery] = React.useState("")
const [newImageUrl, setNewImageUrl] = React.useState("")
const [pendingResetPatch, setPendingResetPatch] = React.useState<Record<string, Partial<MiniAppContent>>>({})
const listPageSize = 7
const listTotalPages = 10
const listPage = 1
React.useEffect(() => {
if (!newContentRequest) return
const id = `c${newContentRequest.requestId}`
const existing = contents.find((item) => item.id === id)
if (!existing) {
const draft: MiniAppContent = {
id,
sourceCarId: newContentRequest.sourceCarId,
sourceCarName: newContentRequest.sourceCarName,
pageType: "车型页",
title: newContentRequest.sourceCarName,
subtitle: "",
highlights: "",
description: "",
ctaText: "立即预约试驾",
scheduledPublishAt: "",
imageUrls: [],
topicNavTitle: `${newContentRequest.sourceCarName}详情`,
topicHeroTitle: "先锋设计",
topicSections: createTopicSectionsFromImages([]),
workflowStatus: "draft",
updatedBy: "内容运营专员",
updatedAt: nowString(),
}
setContents((prev) => [draft, ...prev])
}
setSelectedId(id)
setViewMode("editor")
}, [newContentRequest?.requestId])
const canEdit = roleView === "content-ops"
const canApprove = roleView === "biz-market"
const selected = contents.find((item) => item.id === selectedId) ?? null
const filteredContents = contents.filter((item) => {
const key = `${item.sourceCarName} ${item.sourceCarId} ${item.pageType} ${item.title}`.toLowerCase()
return key.includes(query.toLowerCase())
})
const pagedContents = filteredContents.slice((listPage - 1) * listPageSize, listPage * listPageSize)
const updateContent = (id: string, patch: Partial<MiniAppContent>) => {
setContents((prev) =>
prev.map((item) => (item.id === id ? { ...item, ...patch, updatedAt: nowString() } : item))
)
}
const saveDraft = (item: MiniAppContent) => {
const stagedPatch = pendingResetPatch[item.id] ?? {}
updateContent(item.id, { ...stagedPatch, workflowStatus: "draft", updatedBy: "内容运营专员" })
setPendingResetPatch((prev) => {
if (!prev[item.id]) return prev
const next = { ...prev }
delete next[item.id]
return next
})
if (Object.keys(stagedPatch).length > 0) {
onAddAuditLog(`内容运营专员保存并重置 ${item.sourceCarName} 到官网源数据(回草稿)`)
} else {
onAddAuditLog(`内容运营专员保存 ${item.sourceCarName} 内容草稿`)
}
}
const stageResetFromSource = (item: MiniAppContent) => {
const source = SOURCE_SNAPSHOTS[item.sourceCarId]
if (!source) return
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),
},
}))
onAddAuditLog(`内容运营专员发起 ${item.sourceCarName} 源数据重置(待保存生效)`)
}
const submitForReview = (item: MiniAppContent) => {
updateContent(item.id, { workflowStatus: "prepublished", updatedBy: "内容运营专员" })
onAddAuditLog(`内容运营专员提交 ${item.sourceCarName} 进入预发布待审`)
}
const approve = (item: MiniAppContent) => {
updateContent(item.id, { workflowStatus: "published", updatedBy: "业务/市场负责人" })
onAddAuditLog(`业务/市场负责人通过 ${item.sourceCarName},状态变为已发布`)
}
const reject = (item: MiniAppContent) => {
updateContent(item.id, { workflowStatus: "draft", updatedBy: "业务/市场负责人" })
onAddAuditLog(`业务/市场负责人驳回 ${item.sourceCarName},状态退回草稿`)
}
const createNewVersion = (item: MiniAppContent) => {
updateContent(item.id, { workflowStatus: "draft", updatedBy: "内容运营专员" })
onAddAuditLog(`内容运营专员基于 ${item.sourceCarName} 发布版本创建新稿`)
}
const createNewContent = () => {
const id = `c${Date.now()}`
const draft: MiniAppContent = {
id,
sourceCarId: "",
sourceCarName: "",
pageType: "车型页",
title: "",
subtitle: "",
highlights: "",
description: "",
ctaText: "立即预约试驾",
scheduledPublishAt: "",
imageUrls: [],
topicNavTitle: "车型详情",
topicHeroTitle: "先锋设计",
topicSections: createTopicSectionsFromImages([]),
workflowStatus: "draft",
updatedBy: "内容运营专员",
updatedAt: nowString(),
}
setContents((prev) => [draft, ...prev])
setSelectedId(id)
setViewMode("editor")
onAddAuditLog("内容运营专员新增小程序内容草稿")
}
const deleteContent = (item: MiniAppContent) => {
if (!window.confirm(`确认删除 ${item.sourceCarName || "当前草稿"} 吗?`)) return
setContents((prev) => {
const next = prev.filter((content) => content.id !== item.id)
if (next.length > 0) {
setSelectedId(next[0].id)
}
return next
})
setViewMode("list")
onAddAuditLog(`内容运营专员删除 ${item.sourceCarName || "未命名"} 内容草稿`)
}
const addImage = (item: MiniAppContent) => {
const next = newImageUrl.trim()
if (!next) return
updateContent(item.id, { imageUrls: [...item.imageUrls, next] })
setNewImageUrl("")
}
const removeImage = (item: MiniAppContent, idx: number) => {
updateContent(item.id, { imageUrls: item.imageUrls.filter((_, i) => i !== idx) })
}
const updateTopicSection = (item: MiniAppContent, sectionId: string, patch: Partial<TopicSection>) => {
const sections = item.topicSections ?? []
updateContent(item.id, {
topicSections: sections.map((section) => (section.id === sectionId ? { ...section, ...patch } : section)),
})
}
const addTopicSection = (item: MiniAppContent) => {
const sections = item.topicSections ?? []
if (sections.length >= 6) return
updateContent(item.id, {
topicSections: [
...sections,
{
id: `${item.id}-topic-${Date.now()}`,
title: `专题模块 ${sections.length + 1}`,
imageUrl: item.imageUrls[0] ?? "",
specLeftLabel: "长",
specRightLabel: "宽",
},
],
})
}
const removeTopicSection = (item: MiniAppContent, sectionId: string) => {
const sections = item.topicSections ?? []
if (sections.length <= 1) return
updateContent(item.id, {
topicSections: sections.filter((section) => section.id !== sectionId),
})
}
const renderActions = (item: MiniAppContent) => {
const actions: React.ReactNode[] = []
if (canEdit) {
if (item.workflowStatus === "draft") {
actions.push(
<Button key="submit" size="sm" variant="outline" onClick={() => submitForReview(item)}>
<Send className="mr-1 h-3.5 w-3.5" />
</Button>
)
}
if (item.workflowStatus === "published") {
actions.push(
<Button key="new-version" size="sm" variant="outline" onClick={() => createNewVersion(item)}>
<Pencil className="mr-1 h-3.5 w-3.5" />
</Button>
)
}
}
if (canApprove) {
if (item.workflowStatus === "prepublished") {
actions.push(
<Button key="approve" size="sm" variant="outline" onClick={() => approve(item)}>
<ShieldCheck className="mr-1 h-3.5 w-3.5" />
</Button>
)
actions.push(
<Button key="reject" size="sm" variant="outline" onClick={() => reject(item)}>
<XCircle className="mr-1 h-3.5 w-3.5" />
</Button>
)
}
}
if (!canApprove) {
actions.push(
<Button key="detail" size="sm" variant="ghost" onClick={() => { setSelectedId(item.id); setViewMode("editor") }}>
<Eye className="mr-1 h-3.5 w-3.5" />
</Button>
)
}
return actions
}
if (viewMode === "editor" && selected) {
const isNewContent = !selected.sourceCarId
const canResetFromSource = Boolean(selected.sourceCarId && SOURCE_SNAPSHOTS[selected.sourceCarId])
const isTopicPage = selected.pageType === "专题页"
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)
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")}>
<ArrowLeft className="mr-1 h-3.5 w-3.5" />
</Button>
<div>
<h1 className="text-3xl font-bold tracking-tight"></h1>
<p className="text-muted-foreground"></p>
</div>
</div>
<Badge variant="outline" className={workflowBadgeClass(selected.workflowStatus)}>
{WORKFLOW_LABEL[selected.workflowStatus]}
</Badge>
</div>
<div className="grid gap-6 xl:grid-cols-2">
<Card className="border-none shadow-sm bg-white">
<CardHeader>
<CardTitle className="text-lg font-bold"></CardTitle>
<CardDescription>MiniAppContent</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid gap-2">
<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}
onChange={(e) => {
const nextPageType = e.target.value as MiniAppContent["pageType"]
const snapshot = SOURCE_SNAPSHOTS[selected.sourceCarId]
const sourceName = selected.sourceCarName || "车型"
updateContent(selected.id, {
pageType: nextPageType,
...(nextPageType === "专题页" ? {
...buildTopicPatch(sourceName, snapshot),
imageUrls: [],
ctaText: "预约体验",
} : {}),
})
}}
>
<option value="车型页"></option>
<option value="活动页"></option>
<option value="专题页"></option>
</select>
) : (
<Input value={selected.pageType} disabled />
)}
</div>
<div className="grid gap-2">
<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.sourceCarId}
onChange={(e) => {
const nextId = e.target.value
const option = SOURCE_CAR_OPTIONS.find((item) => item.id === nextId)
const snapshot = SOURCE_SNAPSHOTS[nextId]
const sourceName = option?.name ?? ""
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),
})
if (snapshot) {
onAddAuditLog(`内容运营专员选择来源车型 ${option?.name ?? nextId} 并载入官网基线内容`)
}
}}
>
<option value=""></option>
{SOURCE_CAR_OPTIONS.map((option) => (
<option key={option.id} value={option.id}>{option.name}</option>
))}
</select>
) : (
<Input value={selected.sourceCarName} disabled />
)}
</div>
{isTopicPage ? (
<>
<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>
)}
{pendingResetPatch[selected.id] && (
<div className="rounded-lg border border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-700">
稿稿
</div>
)}
<div className="flex items-center justify-between gap-3 pt-2">
<div className="flex gap-2">
<Button variant="outline" disabled={!canEdit} onClick={() => saveDraft(selected)}>
<Pencil className="mr-1 h-3.5 w-3.5" />稿
</Button>
<Button disabled={!canEdit || selected.workflowStatus !== "draft"} onClick={() => submitForReview(selected)}>
<Send className="mr-1 h-3.5 w-3.5" />
</Button>
</div>
<div className="flex gap-2">
<Button variant="outline" disabled={!canEdit || !canResetFromSource} onClick={() => stageResetFromSource(selected)}>
<RotateCcw className="mr-1 h-3.5 w-3.5" />
</Button>
<Button variant="outline" disabled={!canEdit} onClick={() => deleteContent(selected)}>
<Trash2 className="mr-1 h-3.5 w-3.5" />
</Button>
</div>
</div>
</CardContent>
</Card>
<Card className="border-none shadow-sm bg-white">
<CardHeader>
<CardTitle className="text-lg font-bold"></CardTitle>
<CardDescription>
{isTopicPage
? "专题页预览:按来源车型模板自动同步,可对模块图文做手工编辑。"
: "按钮上方展示预约试驾表单视觉区(仅演示,不提交;真实留资将直接进入企业 CRM。"}
</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>
) : (
<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>
)}
</CardContent>
</Card>
</div>
</div>
)
}
return (
<div className="flex flex-col gap-6 p-8">
<div className="flex items-center justify-between">
<div className="flex flex-col gap-1">
<h1 className="text-3xl font-bold tracking-tight"></h1>
<p className="text-muted-foreground"></p>
</div>
</div>
<Card className="border-none shadow-sm bg-white">
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle className="text-lg font-bold"></CardTitle>
<CardDescription></CardDescription>
</div>
<Button onClick={createNewContent} className="bg-audi-black hover:bg-audi-dark-gray text-white">
<Pencil className="mr-1 h-3.5 w-3.5" />
</Button>
</div>
<div className="flex items-center gap-2 w-full max-w-sm">
<Search className="h-4 w-4 text-muted-foreground absolute ml-3" />
<Input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="搜索官网车型名称或待处理项..."
className="pl-9 bg-gray-50/50 border-none"
/>
</div>
</CardHeader>
<CardContent>
<Table>
<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="text-right font-bold text-audi-black"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{pagedContents.map((item) => (
<TableRow key={item.id} className="hover:bg-gray-50/50 border-b border-gray-50 transition-colors">
<TableCell>
<p className="font-medium">{item.sourceCarName}</p>
<p className="text-xs text-muted-foreground">{item.sourceCarId}</p>
</TableCell>
<TableCell>
<Badge variant="outline">{item.pageType}</Badge>
</TableCell>
<TableCell className="font-medium">{item.title}</TableCell>
<TableCell>
<Badge variant="outline" className={workflowBadgeClass(item.workflowStatus)}>
{WORKFLOW_LABEL[item.workflowStatus]}
</Badge>
</TableCell>
<TableCell className="text-xs text-muted-foreground">
<p>{item.updatedAt}</p>
<p>{item.updatedBy}</p>
</TableCell>
<TableCell className="text-right">
<div className="flex flex-wrap justify-end gap-2">{renderActions(item)}</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
<div className="mt-4 flex items-center justify-between border-t pt-4">
<p className="text-xs text-muted-foreground"> 7 10 </p>
<div className="flex items-center gap-1">
<Button size="sm" variant="outline" disabled></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
>
{index + 1}
</Button>
))}
<Button size="sm" variant="outline" disabled></Button>
</div>
</div>
</CardContent>
</Card>
</div>
)
}