2026-04-13 20:28:09 +08:00
|
|
|
|
import * as React from "react"
|
|
|
|
|
|
import {
|
|
|
|
|
|
ArrowLeft,
|
|
|
|
|
|
Eye,
|
|
|
|
|
|
Pencil,
|
2026-04-15 17:08:17 +08:00
|
|
|
|
RotateCcw,
|
|
|
|
|
|
Search,
|
2026-04-13 20:28:09 +08:00
|
|
|
|
Send,
|
|
|
|
|
|
ShieldCheck,
|
2026-04-15 17:08:17 +08:00
|
|
|
|
Trash2,
|
2026-04-13 20:28:09 +08:00
|
|
|
|
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"
|
|
|
|
|
|
|
2026-04-15 17:08:17 +08:00
|
|
|
|
type WorkflowStatus = "draft" | "prepublished" | "published"
|
|
|
|
|
|
|
|
|
|
|
|
type SourceSnapshot = {
|
|
|
|
|
|
title: string
|
|
|
|
|
|
subtitle: string
|
|
|
|
|
|
highlights: string
|
|
|
|
|
|
description: string
|
|
|
|
|
|
ctaText: string
|
|
|
|
|
|
imageUrls: string[]
|
|
|
|
|
|
}
|
2026-04-13 20:28:09 +08:00
|
|
|
|
|
|
|
|
|
|
type MiniAppContent = {
|
|
|
|
|
|
id: string
|
|
|
|
|
|
sourceCarId: string
|
|
|
|
|
|
sourceCarName: string
|
2026-04-15 17:08:17 +08:00
|
|
|
|
pageType: "车型页" | "活动页" | "专题页"
|
2026-04-13 20:28:09 +08:00
|
|
|
|
title: string
|
|
|
|
|
|
subtitle: string
|
|
|
|
|
|
highlights: string
|
|
|
|
|
|
description: string
|
|
|
|
|
|
ctaText: string
|
|
|
|
|
|
scheduledPublishAt: string
|
|
|
|
|
|
imageUrls: string[]
|
|
|
|
|
|
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",
|
2026-04-15 17:08:17 +08:00
|
|
|
|
pageType: "车型页",
|
2026-04-13 20:28:09 +08:00
|
|
|
|
title: "Audi A3 Sportback",
|
|
|
|
|
|
subtitle: "进取,不负期待",
|
|
|
|
|
|
highlights: "数字座舱\n城市通勤\n智能互联",
|
|
|
|
|
|
description: "适合城市用户的豪华紧凑车型,兼顾效率与智能体验。",
|
|
|
|
|
|
ctaText: "立即预约试驾",
|
|
|
|
|
|
scheduledPublishAt: "",
|
|
|
|
|
|
imageUrls: [
|
2026-04-15 17:08:17 +08:00
|
|
|
|
"/images/cars/a3-1.jpg",
|
|
|
|
|
|
"/images/cars/a3-2.jpg",
|
2026-04-13 20:28:09 +08:00
|
|
|
|
],
|
|
|
|
|
|
workflowStatus: "published",
|
|
|
|
|
|
updatedBy: "内容运营专员",
|
|
|
|
|
|
updatedAt: "2026-04-11 10:02",
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: "c2",
|
|
|
|
|
|
sourceCarId: "a4l",
|
|
|
|
|
|
sourceCarName: "Audi A4L",
|
2026-04-15 17:08:17 +08:00
|
|
|
|
pageType: "活动页",
|
2026-04-13 20:28:09 +08:00
|
|
|
|
title: "Audi A4L 四月限时礼遇",
|
|
|
|
|
|
subtitle: "豪华与性能平衡",
|
|
|
|
|
|
highlights: "quattro\n商务舒适\n限时礼遇",
|
|
|
|
|
|
description: "面向商务人群的主推内容版本,突出舒适与操控。",
|
|
|
|
|
|
ctaText: "获取活动详情",
|
|
|
|
|
|
scheduledPublishAt: "2026-04-14T10:00",
|
|
|
|
|
|
imageUrls: [
|
2026-04-15 17:08:17 +08:00
|
|
|
|
"/images/cars/a4-1.jpg",
|
2026-04-13 20:28:09 +08:00
|
|
|
|
],
|
|
|
|
|
|
workflowStatus: "prepublished",
|
|
|
|
|
|
updatedBy: "业务/市场负责人",
|
|
|
|
|
|
updatedAt: "2026-04-11 09:48",
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: "c3",
|
|
|
|
|
|
sourceCarId: "a6l",
|
|
|
|
|
|
sourceCarName: "Audi A6L",
|
2026-04-15 17:08:17 +08:00
|
|
|
|
pageType: "车型页",
|
2026-04-13 20:28:09 +08:00
|
|
|
|
title: "Audi A6L",
|
|
|
|
|
|
subtitle: "懂你,更懂未来",
|
|
|
|
|
|
highlights: "行政旗舰\n长轴空间\n智能辅助",
|
|
|
|
|
|
description: "正在进行文案优化,待业务审核。",
|
|
|
|
|
|
ctaText: "预约顾问回电",
|
|
|
|
|
|
scheduledPublishAt: "",
|
|
|
|
|
|
imageUrls: [
|
2026-04-15 17:08:17 +08:00
|
|
|
|
"/images/cars/a6-1.jpg",
|
|
|
|
|
|
"/images/cars/a6-2.jpg",
|
2026-04-13 20:28:09 +08:00
|
|
|
|
],
|
2026-04-15 17:08:17 +08:00
|
|
|
|
workflowStatus: "prepublished",
|
2026-04-13 20:28:09 +08:00
|
|
|
|
updatedBy: "内容运营专员",
|
|
|
|
|
|
updatedAt: "2026-04-11 09:30",
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: "c4",
|
|
|
|
|
|
sourceCarId: "q3",
|
|
|
|
|
|
sourceCarName: "Audi Q3",
|
2026-04-15 17:08:17 +08:00
|
|
|
|
pageType: "专题页",
|
2026-04-13 20:28:09 +08:00
|
|
|
|
title: "Audi Q3 城市灵动版",
|
|
|
|
|
|
subtitle: "年轻进阶,灵动出行",
|
|
|
|
|
|
highlights: "紧凑SUV\n智能互联\n都市通勤",
|
|
|
|
|
|
description: "新建草稿,待进一步补充图文和活动权益信息。",
|
|
|
|
|
|
ctaText: "预约试驾",
|
|
|
|
|
|
scheduledPublishAt: "",
|
|
|
|
|
|
imageUrls: [
|
2026-04-15 17:08:17 +08:00
|
|
|
|
"/images/cars/q3-1.jpg",
|
2026-04-13 20:28:09 +08:00
|
|
|
|
],
|
|
|
|
|
|
workflowStatus: "draft",
|
|
|
|
|
|
updatedBy: "内容运营专员",
|
|
|
|
|
|
updatedAt: "2026-04-11 08:40",
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: "c5",
|
|
|
|
|
|
sourceCarId: "q5l",
|
|
|
|
|
|
sourceCarName: "Audi Q5L",
|
2026-04-15 17:08:17 +08:00
|
|
|
|
pageType: "活动页",
|
2026-04-13 20:28:09 +08:00
|
|
|
|
title: "Audi Q5L 周末试驾礼遇",
|
|
|
|
|
|
subtitle: "自由,由我定义",
|
|
|
|
|
|
highlights: "四驱性能\n家庭空间\n周末活动",
|
|
|
|
|
|
description: "因权益文案与活动规则不一致,被驳回待修订。",
|
|
|
|
|
|
ctaText: "了解礼遇",
|
|
|
|
|
|
scheduledPublishAt: "",
|
|
|
|
|
|
imageUrls: [
|
2026-04-15 17:08:17 +08:00
|
|
|
|
"/images/cars/q5-1.jpg",
|
|
|
|
|
|
"/images/cars/q5-2.jpg",
|
2026-04-13 20:28:09 +08:00
|
|
|
|
],
|
2026-04-15 17:08:17 +08:00
|
|
|
|
workflowStatus: "draft",
|
2026-04-13 20:28:09 +08:00
|
|
|
|
updatedBy: "业务/市场负责人",
|
|
|
|
|
|
updatedAt: "2026-04-11 08:10",
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: "c6",
|
|
|
|
|
|
sourceCarId: "a8l",
|
|
|
|
|
|
sourceCarName: "Audi A8L",
|
2026-04-15 17:08:17 +08:00
|
|
|
|
pageType: "车型页",
|
2026-04-13 20:28:09 +08:00
|
|
|
|
title: "Audi A8L 尊享礼宾版",
|
|
|
|
|
|
subtitle: "旗舰格局,沉稳之选",
|
|
|
|
|
|
highlights: "旗舰行政\n豪华座舱\n专属服务",
|
|
|
|
|
|
description: "已发布后因活动档期调整撤回,待重新排期。",
|
|
|
|
|
|
ctaText: "预约专属顾问",
|
|
|
|
|
|
scheduledPublishAt: "2026-04-15T09:30",
|
|
|
|
|
|
imageUrls: [
|
2026-04-15 17:08:17 +08:00
|
|
|
|
"/images/cars/a8-1.jpg",
|
2026-04-13 20:28:09 +08:00
|
|
|
|
],
|
2026-04-15 17:08:17 +08:00
|
|
|
|
workflowStatus: "published",
|
2026-04-13 20:28:09 +08:00
|
|
|
|
updatedBy: "业务/市场负责人",
|
|
|
|
|
|
updatedAt: "2026-04-11 07:55",
|
|
|
|
|
|
},
|
2026-04-15 17:08:17 +08:00
|
|
|
|
{
|
|
|
|
|
|
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",
|
|
|
|
|
|
},
|
2026-04-13 20:28:09 +08:00
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
const WORKFLOW_LABEL: Record<WorkflowStatus, string> = {
|
|
|
|
|
|
draft: "草稿",
|
|
|
|
|
|
prepublished: "预发布",
|
|
|
|
|
|
published: "已发布",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-15 17:08:17 +08:00
|
|
|
|
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
|
|
|
|
|
|
|
2026-04-13 20:28:09 +08:00
|
|
|
|
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)
|
2026-04-15 17:08:17 +08:00
|
|
|
|
const [query, setQuery] = React.useState("")
|
2026-04-13 20:28:09 +08:00
|
|
|
|
const [newImageUrl, setNewImageUrl] = React.useState("")
|
2026-04-15 17:08:17 +08:00
|
|
|
|
const [pendingResetPatch, setPendingResetPatch] = React.useState<Record<string, Partial<MiniAppContent>>>({})
|
|
|
|
|
|
const listPageSize = 7
|
|
|
|
|
|
const listTotalPages = 10
|
|
|
|
|
|
const listPage = 1
|
2026-04-13 20:28:09 +08:00
|
|
|
|
|
|
|
|
|
|
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,
|
2026-04-15 17:08:17 +08:00
|
|
|
|
pageType: "车型页",
|
2026-04-13 20:28:09 +08:00
|
|
|
|
title: newContentRequest.sourceCarName,
|
|
|
|
|
|
subtitle: "",
|
|
|
|
|
|
highlights: "",
|
|
|
|
|
|
description: "",
|
|
|
|
|
|
ctaText: "立即预约试驾",
|
|
|
|
|
|
scheduledPublishAt: "",
|
|
|
|
|
|
imageUrls: [],
|
|
|
|
|
|
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
|
2026-04-15 17:08:17 +08:00
|
|
|
|
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)
|
2026-04-13 20:28:09 +08:00
|
|
|
|
|
|
|
|
|
|
const updateContent = (id: string, patch: Partial<MiniAppContent>) => {
|
|
|
|
|
|
setContents((prev) =>
|
|
|
|
|
|
prev.map((item) => (item.id === id ? { ...item, ...patch, updatedAt: nowString() } : item))
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const saveDraft = (item: MiniAppContent) => {
|
2026-04-15 17:08:17 +08:00
|
|
|
|
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} 内容草稿`)
|
|
|
|
|
|
}
|
2026-04-13 20:28:09 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-15 17:08:17 +08:00
|
|
|
|
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: source.ctaText,
|
|
|
|
|
|
imageUrls: source.imageUrls,
|
|
|
|
|
|
},
|
|
|
|
|
|
}))
|
|
|
|
|
|
onAddAuditLog(`内容运营专员发起 ${item.sourceCarName} 源数据重置(待保存生效)`)
|
2026-04-13 20:28:09 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-15 17:08:17 +08:00
|
|
|
|
const submitForReview = (item: MiniAppContent) => {
|
|
|
|
|
|
updateContent(item.id, { workflowStatus: "prepublished", updatedBy: "内容运营专员" })
|
|
|
|
|
|
onAddAuditLog(`内容运营专员提交 ${item.sourceCarName} 进入预发布待审`)
|
2026-04-13 20:28:09 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const approve = (item: MiniAppContent) => {
|
2026-04-15 17:08:17 +08:00
|
|
|
|
updateContent(item.id, { workflowStatus: "published", updatedBy: "业务/市场负责人" })
|
|
|
|
|
|
onAddAuditLog(`业务/市场负责人通过 ${item.sourceCarName},状态变为已发布`)
|
2026-04-13 20:28:09 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const reject = (item: MiniAppContent) => {
|
2026-04-15 17:08:17 +08:00
|
|
|
|
updateContent(item.id, { workflowStatus: "draft", updatedBy: "业务/市场负责人" })
|
|
|
|
|
|
onAddAuditLog(`业务/市场负责人驳回 ${item.sourceCarName},状态退回草稿`)
|
2026-04-13 20:28:09 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-15 17:08:17 +08:00
|
|
|
|
const createNewVersion = (item: MiniAppContent) => {
|
|
|
|
|
|
updateContent(item.id, { workflowStatus: "draft", updatedBy: "内容运营专员" })
|
|
|
|
|
|
onAddAuditLog(`内容运营专员基于 ${item.sourceCarName} 发布版本创建新稿`)
|
2026-04-13 20:28:09 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-15 17:08:17 +08:00
|
|
|
|
const createNewContent = () => {
|
|
|
|
|
|
const id = `c${Date.now()}`
|
|
|
|
|
|
const draft: MiniAppContent = {
|
|
|
|
|
|
id,
|
|
|
|
|
|
sourceCarId: "",
|
|
|
|
|
|
sourceCarName: "",
|
|
|
|
|
|
pageType: "车型页",
|
|
|
|
|
|
title: "",
|
|
|
|
|
|
subtitle: "",
|
|
|
|
|
|
highlights: "",
|
|
|
|
|
|
description: "",
|
|
|
|
|
|
ctaText: "立即预约试驾",
|
|
|
|
|
|
scheduledPublishAt: "",
|
|
|
|
|
|
imageUrls: [],
|
|
|
|
|
|
workflowStatus: "draft",
|
|
|
|
|
|
updatedBy: "内容运营专员",
|
|
|
|
|
|
updatedAt: nowString(),
|
|
|
|
|
|
}
|
|
|
|
|
|
setContents((prev) => [draft, ...prev])
|
|
|
|
|
|
setSelectedId(id)
|
|
|
|
|
|
setViewMode("editor")
|
|
|
|
|
|
onAddAuditLog("内容运营专员新增小程序内容草稿")
|
2026-04-13 20:28:09 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-15 17:08:17 +08:00
|
|
|
|
const deleteContent = (item: MiniAppContent) => {
|
|
|
|
|
|
if (!window.confirm(`确认删除 ${item.sourceCarName || "当前草稿"} 吗?`)) return
|
2026-04-13 20:28:09 +08:00
|
|
|
|
|
2026-04-15 17:08:17 +08:00
|
|
|
|
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 || "未命名"} 内容草稿`)
|
2026-04-13 20:28:09 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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 renderActions = (item: MiniAppContent) => {
|
|
|
|
|
|
const actions: React.ReactNode[] = []
|
|
|
|
|
|
|
|
|
|
|
|
if (canEdit) {
|
2026-04-15 17:08:17 +08:00
|
|
|
|
if (item.workflowStatus === "draft") {
|
2026-04-13 20:28:09 +08:00
|
|
|
|
actions.push(
|
|
|
|
|
|
<Button key="submit" size="sm" variant="outline" onClick={() => submitForReview(item)}>
|
2026-04-15 17:08:17 +08:00
|
|
|
|
<Send className="mr-1 h-3.5 w-3.5" />提交审核
|
2026-04-13 20:28:09 +08:00
|
|
|
|
</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) {
|
2026-04-15 17:08:17 +08:00
|
|
|
|
if (item.workflowStatus === "prepublished") {
|
2026-04-13 20:28:09 +08:00
|
|
|
|
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>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-15 17:08:17 +08:00
|
|
|
|
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>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
2026-04-13 20:28:09 +08:00
|
|
|
|
|
|
|
|
|
|
return actions
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (viewMode === "editor" && selected) {
|
2026-04-15 17:08:17 +08:00
|
|
|
|
const isNewContent = !selected.sourceCarId
|
|
|
|
|
|
const canResetFromSource = Boolean(selected.sourceCarId && SOURCE_SNAPSHOTS[selected.sourceCarId])
|
|
|
|
|
|
const previewHeroImage = selected.imageUrls[0]
|
2026-04-13 20:28:09 +08:00
|
|
|
|
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>
|
2026-04-15 17:08:17 +08:00
|
|
|
|
{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]
|
|
|
|
|
|
updateContent(selected.id, {
|
|
|
|
|
|
sourceCarId: nextId,
|
|
|
|
|
|
sourceCarName: option?.name ?? "",
|
|
|
|
|
|
title: snapshot?.title ?? selected.title,
|
|
|
|
|
|
subtitle: snapshot?.subtitle ?? selected.subtitle,
|
|
|
|
|
|
highlights: snapshot?.highlights ?? selected.highlights,
|
|
|
|
|
|
description: snapshot?.description ?? selected.description,
|
|
|
|
|
|
ctaText: snapshot?.ctaText ?? selected.ctaText,
|
|
|
|
|
|
imageUrls: snapshot?.imageUrls ?? selected.imageUrls,
|
|
|
|
|
|
})
|
|
|
|
|
|
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>
|
|
|
|
|
|
|
|
|
|
|
|
<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) => updateContent(selected.id, { pageType: e.target.value as MiniAppContent["pageType"] })}
|
|
|
|
|
|
>
|
|
|
|
|
|
<option value="车型页">车型页</option>
|
|
|
|
|
|
<option value="活动页">活动页</option>
|
|
|
|
|
|
<option value="专题页">专题页</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<Input value={selected.pageType} disabled />
|
|
|
|
|
|
)}
|
2026-04-13 20:28:09 +08:00
|
|
|
|
</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>
|
|
|
|
|
|
|
|
|
|
|
|
{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>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
<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>
|
|
|
|
|
|
|
2026-04-15 17:08:17 +08:00
|
|
|
|
{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>
|
2026-04-13 20:28:09 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</CardContent>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
|
|
<Card className="border-none shadow-sm bg-white">
|
|
|
|
|
|
<CardHeader>
|
|
|
|
|
|
<CardTitle className="text-lg font-bold">小程序预览(手机窗口)</CardTitle>
|
2026-04-15 17:08:17 +08:00
|
|
|
|
<CardDescription>按钮上方展示预约试驾表单视觉区(仅演示,不提交;真实留资将直接进入企业 CRM)。</CardDescription>
|
2026-04-13 20:28:09 +08:00
|
|
|
|
</CardHeader>
|
|
|
|
|
|
<CardContent className="flex justify-center">
|
|
|
|
|
|
<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">
|
2026-04-15 17:08:17 +08:00
|
|
|
|
{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>
|
|
|
|
|
|
)}
|
2026-04-13 20:28:09 +08:00
|
|
|
|
<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>
|
2026-04-15 17:08:17 +08:00
|
|
|
|
我已阅读并同意《个人信息保护政策》,授权奥迪及授权经销商与我联系。信息提交后将直接进入企业 CRM,本平台不留存潜客数据。
|
2026-04-13 20:28:09 +08:00
|
|
|
|
</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>
|
2026-04-15 17:08:17 +08:00
|
|
|
|
<p className="text-muted-foreground">集中管理内容模板与内容版本。</p>
|
2026-04-13 20:28:09 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<Card className="border-none shadow-sm bg-white">
|
|
|
|
|
|
<CardHeader>
|
2026-04-15 17:08:17 +08:00
|
|
|
|
<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>
|
2026-04-13 20:28:09 +08:00
|
|
|
|
</CardHeader>
|
|
|
|
|
|
<CardContent>
|
|
|
|
|
|
<Table>
|
|
|
|
|
|
<TableHeader>
|
|
|
|
|
|
<TableRow className="hover:bg-transparent border-b border-gray-100">
|
|
|
|
|
|
<TableHead className="font-bold text-audi-black">来源车型</TableHead>
|
2026-04-15 17:08:17 +08:00
|
|
|
|
<TableHead className="font-bold text-audi-black">页面类型</TableHead>
|
2026-04-13 20:28:09 +08:00
|
|
|
|
<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>
|
2026-04-15 17:08:17 +08:00
|
|
|
|
{pagedContents.map((item) => (
|
2026-04-13 20:28:09 +08:00
|
|
|
|
<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>
|
2026-04-15 17:08:17 +08:00
|
|
|
|
<TableCell>
|
|
|
|
|
|
<Badge variant="outline">{item.pageType}</Badge>
|
|
|
|
|
|
</TableCell>
|
2026-04-13 20:28:09 +08:00
|
|
|
|
<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>
|
2026-04-15 17:08:17 +08:00
|
|
|
|
|
|
|
|
|
|
<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>
|
2026-04-13 20:28:09 +08:00
|
|
|
|
</CardContent>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|