Files
audi-rednote/audi-content-portal/src/components/car-library/CarLibrary.tsx
2026-04-13 20:28:09 +08:00

284 lines
10 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 { AlertCircle, CheckCircle2, Clock, Filter, Plus, RefreshCw, Search } 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 { Progress } from "@/components/ui/progress"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
type SyncStatus = "synced" | "pending" | "error"
type SourceCar = {
id: string
name: string
officialTagline: string
sourceUpdatedAt: string
syncStatus: SyncStatus
imageCount: number
}
type SyncLog = {
id: string
time: string
status: "success" | "failed"
message: string
}
type CarLibraryProps = {
roleView: RoleView
workflowConfig: WorkflowConfig
onAddAuditLog: (message: string) => void
onCreateContent?: (sourceCarId: string, sourceCarName: string) => void
}
const SOURCE_CARS: SourceCar[] = [
{
id: "a3",
name: "Audi A3 Sportback",
officialTagline: "进取,不负期待",
sourceUpdatedAt: "2026-04-11 09:30",
syncStatus: "synced",
imageCount: 8,
},
{
id: "a4l",
name: "Audi A4L",
officialTagline: "做更强大的自己",
sourceUpdatedAt: "2026-04-11 09:30",
syncStatus: "synced",
imageCount: 6,
},
{
id: "a6l",
name: "Audi A6L",
officialTagline: "懂你,更懂未来",
sourceUpdatedAt: "2026-04-10 14:20",
syncStatus: "pending",
imageCount: 5,
},
{
id: "q5l",
name: "Audi Q5L",
officialTagline: "自由,由我定义",
sourceUpdatedAt: "2026-04-10 09:15",
syncStatus: "error",
imageCount: 0,
},
]
const INITIAL_LOGS: SyncLog[] = [
{ id: "l1", time: "2026-04-11 09:30", status: "success", message: "同步成功4 个车型,素材 19 张" },
{ id: "l2", time: "2026-04-10 09:15", status: "failed", message: "Audi Q5L 图片拉取失败CDN 超时" },
]
function syncBadge(status: SyncStatus) {
if (status === "synced") {
return (
<Badge variant="outline" className="bg-emerald-50 text-emerald-700 border-emerald-200 gap-1">
<CheckCircle2 className="h-3 w-3" />
</Badge>
)
}
if (status === "pending") {
return (
<Badge variant="outline" className="bg-amber-50 text-amber-700 border-amber-200 gap-1">
<Clock className="h-3 w-3" />
</Badge>
)
}
return (
<Badge variant="outline" className="bg-rose-50 text-rose-700 border-rose-200 gap-1">
<AlertCircle className="h-3 w-3" />
</Badge>
)
}
function nowString() {
return new Date().toLocaleString("zh-CN", { hour12: false })
}
export function CarLibrary({ onAddAuditLog, onCreateContent }: CarLibraryProps) {
const [sourceCars, setSourceCars] = React.useState<SourceCar[]>(SOURCE_CARS)
const [query, setQuery] = React.useState("")
const [isSyncing, setIsSyncing] = React.useState(false)
const [progress, setProgress] = React.useState(0)
const [syncLogs, setSyncLogs] = React.useState<SyncLog[]>(INITIAL_LOGS)
const filtered = sourceCars.filter((car) => car.name.toLowerCase().includes(query.toLowerCase()))
const handleSync = () => {
setIsSyncing(true)
setProgress(0)
const interval = setInterval(() => {
setProgress((prev) => {
if (prev >= 100) {
clearInterval(interval)
const time = nowString()
setSourceCars((curr) =>
curr.map((car) => ({
...car,
sourceUpdatedAt: time,
syncStatus: "synced",
imageCount: car.imageCount || 4,
}))
)
setSyncLogs((curr) => [
{ id: String(Date.now()), time, status: "success", message: "手动同步完成4 个车型已更新" },
...curr,
])
onAddAuditLog("内容运营专员执行官网车型同步")
setTimeout(() => setIsSyncing(false), 300)
return 100
}
return prev + 4
})
}, 45)
}
const retryFailedSync = (logId: string) => {
const time = nowString()
setSyncLogs((curr) =>
curr.map((log) =>
log.id === logId ? { ...log, status: "success", time, message: `${log.message} -> 已重试成功` } : log
)
)
setSourceCars((curr) =>
curr.map((car) => (car.syncStatus === "error" ? { ...car, syncStatus: "synced", sourceUpdatedAt: time, imageCount: 4 } : car))
)
onAddAuditLog("项目与运维负责人重试官网同步失败任务")
}
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>
<Button onClick={handleSync} disabled={isSyncing} className="bg-audi-black hover:bg-audi-dark-gray text-white px-6">
{isSyncing ? <RefreshCw className="mr-2 h-4 w-4 animate-spin" /> : <RefreshCw className="mr-2 h-4 w-4" />}
</Button>
</div>
{isSyncing && (
<Card className="border-audi-red/20 bg-audi-red/5">
<CardContent className="pt-6">
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between text-sm">
<span className="font-medium text-audi-red">...</span>
<span className="text-muted-foreground">{Math.round(progress)}%</span>
</div>
<Progress value={progress} className="h-2 bg-audi-red/10" />
</div>
</CardContent>
</Card>
)}
<Card className="border-none shadow-sm bg-white">
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<CardTitle className="text-lg font-bold"></CardTitle>
<Badge variant="outline" className="text-xs">Source Entity: SourceCar</Badge>
</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>
{filtered.map((car) => (
<TableRow key={car.id} className="hover:bg-gray-50/50 border-b border-gray-50 transition-colors">
<TableCell className="font-medium">{car.name}</TableCell>
<TableCell className="text-muted-foreground">{car.officialTagline}</TableCell>
<TableCell>{car.imageCount} </TableCell>
<TableCell>{syncBadge(car.syncStatus)}</TableCell>
<TableCell className="text-muted-foreground text-xs">{car.sourceUpdatedAt}</TableCell>
<TableCell className="text-right">
{car.syncStatus === "synced" ? (
<Button
size="sm"
variant="outline"
onClick={() => {
onCreateContent?.(car.id, car.name)
onAddAuditLog(`内容运营专员基于官网 ${car.name} 新建内容草稿`)
}}
>
<Plus className="mr-1 h-3.5 w-3.5" />
</Button>
) : (
<Button
size="sm"
variant="outline"
disabled
title={car.syncStatus === "pending" ? "车型待同步,完成同步后可新建内容" : "车型同步失败,请先重试同步"}
>
<Plus className="mr-1 h-3.5 w-3.5" />
</Button>
)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
<Card className="border-none shadow-sm bg-white">
<CardHeader>
<CardTitle className="text-lg font-bold"></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent className="space-y-2">
{syncLogs.map((log) => (
<div key={log.id} className="flex items-center gap-3 rounded-lg border p-3 text-sm">
{log.status === "success" ? (
<CheckCircle2 className="h-4 w-4 text-emerald-600" />
) : (
<AlertCircle className="h-4 w-4 text-rose-600" />
)}
<div className="flex-1">
<p className="font-medium">{log.message}</p>
<p className="text-xs text-muted-foreground">{log.time}</p>
</div>
{log.status === "failed" && (
<Button size="sm" variant="outline" onClick={() => retryFailedSync(log.id)}>
<RefreshCw className="mr-1 h-3.5 w-3.5" />
</Button>
)}
</div>
))}
</CardContent>
</Card>
</div>
)
}