Files
cunkebao_v3/Cunkebao/app/scenarios/new/steps/PosterEditor.tsx
2025-04-09 09:31:09 +08:00

480 lines
18 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.

"use client"
import { useState, useRef, useEffect } from "react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"
import { Card, CardFooter } from "@/components/ui/card"
import {
Upload,
PenTool,
Type,
QrCode,
ChevronRight,
ArrowRight,
Smartphone,
Monitor,
Tablet,
Save,
Share2,
Eye,
} from "lucide-react"
import { Switch } from "@/components/ui/switch"
import { cn } from "@/lib/utils"
const DEFAULT_TEMPLATES = [
{
id: "register",
name: "点击报名",
src: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/%E7%80%9B%E6%A8%BA%EE%85%B9%E7%80%B9%E6%BF%87%E6%8D%A3%E9%8E%B6_%E9%90%90%E7%91%B0%E5%9A%AE%E9%8E%B6%E3%83%A5%E6%82%95-vJDCYhJ9ENr8jN3YGP9jVeQ5Ub3czl.gif",
color: "#d32121",
textColor: "#ffffff",
},
{
id: "claim",
name: "点击领取",
src: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/%E7%80%9B%E6%A8%BA%EE%85%B9%E7%80%B9%E6%BF%87%E6%8D%A3%E9%8E%B6_%E9%90%90%E7%91%B0%E5%9A%AE%E6%A3%B0%E5%97%97%E5%BD%871-cskUmYR6oO0n4uHdZVeB4naKUSUilb.gif",
color: "#d32121",
textColor: "#ffffff",
},
{
id: "consult",
name: "点击咨询",
src: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/%E7%80%9B%E6%A8%BA%EE%85%B9%E7%80%B9%E6%BF%87%E6%8D%A3%E9%8E%B6_%E9%90%90%E7%91%B0%E5%9A%AE%E9%8D%9C%E3%84%A8%EE%87%97-OUtJxwRbr4ydYRjt8FLOCMELC16Vw6.gif",
color: "#d32121",
textColor: "#ffffff",
},
{
id: "checkin",
name: "点击签到",
src: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/%E7%80%9B%E6%A8%BA%EE%85%B9%E7%80%B9%E6%BF%87%E6%8D%A3%E9%8E%B6_%E9%90%90%E7%91%B0%E5%9A%AE%E7%BB%9B%E6%83%A7%E5%9F%8C-bYcTocSdNrcykfBXmt51q6D4Yzh26h.gif",
color: "#d32121",
textColor: "#ffffff",
},
{
id: "cooperation",
name: "点击合作",
src: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/%E7%80%9B%E6%A8%BA%EE%85%B9%E7%80%B9%E6%BF%87%E6%8D%A3%E9%8E%B6_%E9%90%90%E7%91%B0%E5%9A%AE%E9%8D%9A%E5%A0%9C%E7%B6%94-kisPT3kV9A0aB7YpxO6AHUZ8aHvFLT.gif",
color: "#d32121",
textColor: "#ffffff",
},
{
id: "learn",
name: "点击了解",
src: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/%E7%80%9B%E6%A8%BA%EE%85%B9%E7%80%B9%E6%BF%87%E6%8D%A3%E9%8E%B6_%E9%90%90%E7%91%B0%E5%9A%AE%E6%B5%9C%E5%97%9A%D0%92-iE654sFFuO1PuvwmccV67yVLQZoLcx.gif",
color: "#d32121",
textColor: "#ffffff",
},
{
id: "claim_static",
name: "点击领取(静态)",
src: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/%E7%80%9B%E6%A8%BA%EE%85%B9%E7%80%B9%E6%BF%87%E6%8D%A3%E9%8E%B6_%E9%90%90%E7%91%B0%E5%9A%AE%E6%A3%B0%E5%97%97%E5%BD%87-jO6FPRCCzz6Irkm5suKeNkUDd98Y0f.png",
color: "#d32121",
textColor: "#ffffff",
},
]
export function PosterEditor({
onChange,
initialValue = null,
}: {
onChange: (value: any) => void
initialValue?: any
}) {
const [selectedTemplate, setSelectedTemplate] = useState(initialValue?.template || DEFAULT_TEMPLATES[0])
const [customText, setCustomText] = useState(initialValue?.customText || selectedTemplate.name)
const [mainColor, setMainColor] = useState(initialValue?.mainColor || selectedTemplate.color)
const [textColor, setTextColor] = useState(initialValue?.textColor || selectedTemplate.textColor)
const [hasQrCode, setHasQrCode] = useState(initialValue?.hasQrCode || false)
const [qrCodeUrl, setQrCodeUrl] = useState(initialValue?.qrCodeUrl || "")
const [offerText, setOfferText] = useState(initialValue?.offerText || "")
const [previewDevice, setPreviewDevice] = useState("mobile")
const [isTemplateDialogOpen, setIsTemplateDialogOpen] = useState(false)
const canvasRef = useRef<HTMLCanvasElement>(null)
// 当任何相关状态变化时更新父组件
useEffect(() => {
onChange({
template: selectedTemplate,
customText,
mainColor,
textColor,
hasQrCode,
qrCodeUrl,
offerText,
})
}, [selectedTemplate, customText, mainColor, textColor, hasQrCode, qrCodeUrl, offerText, onChange])
// 预览海报的渲染
useEffect(() => {
const canvas = canvasRef.current
if (!canvas) return
const ctx = canvas.getContext("2d")
if (!ctx) return
// 清空画布
ctx.clearRect(0, 0, canvas.width, canvas.height)
// 获取原始海报图像
const img = new Image()
img.crossOrigin = "anonymous"
img.src = selectedTemplate.src
img.onload = () => {
// 绘制原始海报
ctx.drawImage(img, 0, 0, canvas.width, canvas.height)
// 自定义文本
if (customText !== selectedTemplate.name) {
// 首先绘制一个半透明背景,盖住原文字
ctx.fillStyle = "rgba(255, 255, 255, 0.85)"
ctx.fillRect(20, 30, canvas.width - 40, 150)
// 绘制自定义文字
ctx.fillStyle = mainColor
ctx.font = "bold 42px sans-serif"
ctx.textAlign = "center"
ctx.textBaseline = "middle"
ctx.fillText(customText, canvas.width / 2, 100)
}
// 如果添加了二维码
if (hasQrCode) {
// 添加一个二维码占位背景
ctx.fillStyle = "#ffffff"
ctx.fillRect(canvas.width - 120, canvas.height - 120, 100, 100)
ctx.strokeStyle = "#dddddd"
ctx.lineWidth = 1
ctx.strokeRect(canvas.width - 120, canvas.height - 120, 100, 100)
// 添加二维码图标占位
ctx.fillStyle = "#888888"
ctx.fillRect(canvas.width - 100, canvas.height - 100, 60, 60)
// 添加"扫码获取"文本
ctx.fillStyle = "#333333"
ctx.font = "14px sans-serif"
ctx.textAlign = "center"
ctx.fillText("扫码获取", canvas.width - 70, canvas.height - 20)
}
// 如果添加了优惠文本
if (offerText) {
// 添加一个醒目的优惠信息标签
ctx.fillStyle = "#ffeb3b"
ctx.beginPath()
ctx.moveTo(0, canvas.height - 200)
ctx.lineTo(200, canvas.height - 200)
ctx.lineTo(170, canvas.height - 150)
ctx.lineTo(0, canvas.height - 150)
ctx.closePath()
ctx.fill()
// 添加优惠文本
ctx.fillStyle = "#d32121"
ctx.font = "bold 18px sans-serif"
ctx.textAlign = "left"
ctx.fillText(offerText, 15, canvas.height - 175)
}
}
}, [selectedTemplate, customText, mainColor, hasQrCode, offerText])
return (
<div className="flex flex-col gap-6">
<h2 className="text-lg font-medium"></h2>
<Tabs defaultValue="design" className="w-full">
<TabsList className="w-full grid grid-cols-4">
<TabsTrigger value="design" className="flex items-center gap-1">
<PenTool className="h-4 w-4" />
</TabsTrigger>
<TabsTrigger value="content" className="flex items-center gap-1">
<Type className="h-4 w-4" />
</TabsTrigger>
<TabsTrigger value="interactive" className="flex items-center gap-1">
<QrCode className="h-4 w-4" />
</TabsTrigger>
<TabsTrigger value="preview" className="flex items-center gap-1">
<Eye className="h-4 w-4" />
</TabsTrigger>
</TabsList>
<TabsContent value="design" className="space-y-4 pt-4">
<div>
<Label></Label>
<div className="mt-2 flex justify-between">
<div className="w-24 h-40 bg-gray-100 rounded relative overflow-hidden">
{selectedTemplate && (
<img
src={selectedTemplate.src || "/placeholder.svg"}
alt={selectedTemplate.name}
className="object-cover w-full h-full"
/>
)}
</div>
<div className="ml-4 flex-1 flex flex-col justify-between">
<div>
<h4 className="font-medium">{selectedTemplate?.name || "请选择模板"}</h4>
<p className="text-sm text-gray-500 mt-1"></p>
</div>
<Dialog open={isTemplateDialogOpen} onOpenChange={setIsTemplateDialogOpen}>
<DialogTrigger asChild>
<Button variant="outline" className="mt-2">
<ChevronRight className="ml-1 h-4 w-4" />
</Button>
</DialogTrigger>
<DialogContent className="max-w-3xl">
<DialogHeader>
<DialogTitle></DialogTitle>
</DialogHeader>
<div className="grid grid-cols-3 gap-4 max-h-[60vh] overflow-y-auto p-2">
{DEFAULT_TEMPLATES.map((template) => (
<Card
key={template.id}
className={cn(
"overflow-hidden cursor-pointer transition-all transform hover:scale-105",
selectedTemplate.id === template.id && "ring-2 ring-blue-500",
)}
onClick={() => {
setSelectedTemplate(template)
setCustomText(template.name)
setMainColor(template.color)
setTextColor(template.textColor)
setIsTemplateDialogOpen(false)
}}
>
<div className="aspect-[9/16] bg-gray-100 relative">
<img
src={template.src || "/placeholder.svg"}
alt={template.name}
className="w-full h-full object-cover"
/>
</div>
<CardFooter className="p-2">
<p className="text-sm text-center w-full">{template.name}</p>
</CardFooter>
</Card>
))}
<Card className="overflow-hidden cursor-pointer transition-all transform hover:scale-105">
<div className="aspect-[9/16] bg-gray-100 flex flex-col items-center justify-center text-gray-500">
<Upload className="h-8 w-8 mb-2" />
<p className="text-sm"></p>
</div>
<CardFooter className="p-2">
<p className="text-sm text-center w-full"></p>
</CardFooter>
</Card>
</div>
</DialogContent>
</Dialog>
</div>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<Label htmlFor="mainColor"></Label>
<div className="flex mt-2">
<div className="w-10 h-8 rounded-l border" style={{ backgroundColor: mainColor }}></div>
<Input
id="mainColor"
type="text"
value={mainColor}
onChange={(e) => setMainColor(e.target.value)}
className="rounded-l-none"
/>
</div>
</div>
<div>
<Label htmlFor="textColor"></Label>
<div className="flex mt-2">
<div className="w-10 h-8 rounded-l border" style={{ backgroundColor: textColor }}></div>
<Input
id="textColor"
type="text"
value={textColor}
onChange={(e) => setTextColor(e.target.value)}
className="rounded-l-none"
/>
</div>
</div>
</div>
<div className="flex items-center justify-between">
<Button variant="outline" size="sm">
</Button>
<Button variant="outline" size="sm">
</Button>
</div>
</TabsContent>
<TabsContent value="content" className="space-y-4 pt-4">
<div>
<Label htmlFor="customText"></Label>
<Input
id="customText"
value={customText}
onChange={(e) => setCustomText(e.target.value)}
className="mt-2"
/>
</div>
<div>
<Label htmlFor="offerText">()</Label>
<Input
id="offerText"
value={offerText}
onChange={(e) => setOfferText(e.target.value)}
className="mt-2"
placeholder="例如: 限时8.5折 | 新人专享"
/>
<p className="text-xs text-gray-500 mt-1"></p>
</div>
<div>
<Label></Label>
<div className="flex flex-wrap gap-2 mt-2">
{["限时特惠", "新人专享", "首单立减", "买一送一", "折扣优惠", "免费领取"].map((tag) => (
<Button key={tag} variant="outline" size="sm" onClick={() => setOfferText(tag)}>
{tag}
</Button>
))}
</div>
</div>
</TabsContent>
<TabsContent value="interactive" className="space-y-4 pt-4">
<div className="flex items-center justify-between">
<div className="flex-1">
<Label htmlFor="qrcode-switch" className="cursor-pointer">
</Label>
<p className="text-xs text-gray-500 mt-1"></p>
</div>
<Switch id="qrcode-switch" checked={hasQrCode} onCheckedChange={setHasQrCode} />
</div>
{hasQrCode && (
<div>
<Label htmlFor="qrcode-url">()</Label>
<Input
id="qrcode-url"
value={qrCodeUrl}
onChange={(e) => setQrCodeUrl(e.target.value)}
className="mt-2"
placeholder="输入链接或小程序路径"
/>
<div className="mt-3 flex justify-between items-center">
<p className="text-sm"></p>
<Button variant="outline" size="sm" className="flex items-center">
<Upload className="h-3 w-3 mr-1" />
</Button>
</div>
</div>
)}
<div className="border-t pt-4 mt-4">
<h4 className="font-medium mb-2"></h4>
<Select defaultValue="form">
<SelectTrigger>
<SelectValue placeholder="选择点击行为" />
</SelectTrigger>
<SelectContent>
<SelectItem value="form"></SelectItem>
<SelectItem value="qrcode"></SelectItem>
<SelectItem value="call"></SelectItem>
<SelectItem value="link">访</SelectItem>
</SelectContent>
</Select>
<p className="text-xs text-gray-500 mt-2"></p>
</div>
</TabsContent>
<TabsContent value="preview" className="space-y-4 pt-4">
<div className="flex justify-center space-x-2">
<Button
variant={previewDevice === "mobile" ? "default" : "outline"}
size="sm"
onClick={() => setPreviewDevice("mobile")}
className="flex items-center"
>
<Smartphone className="h-4 w-4 mr-1" />
</Button>
<Button
variant={previewDevice === "tablet" ? "default" : "outline"}
size="sm"
onClick={() => setPreviewDevice("tablet")}
className="flex items-center"
>
<Tablet className="h-4 w-4 mr-1" />
</Button>
<Button
variant={previewDevice === "desktop" ? "default" : "outline"}
size="sm"
onClick={() => setPreviewDevice("desktop")}
className="flex items-center"
>
<Monitor className="h-4 w-4 mr-1" />
</Button>
</div>
<div className="flex justify-center">
<div
className={cn(
"bg-gray-100 border flex items-center justify-center p-2",
previewDevice === "mobile" && "w-[320px] h-[568px]",
previewDevice === "tablet" && "w-[480px] h-[640px]",
previewDevice === "desktop" && "w-[640px] h-[480px] max-w-full",
)}
>
<canvas
ref={canvasRef}
width={300}
height={534}
className={cn("w-full h-full object-contain", previewDevice === "desktop" && "max-h-[90%]")}
/>
</div>
</div>
<div className="flex justify-center gap-2 mt-4">
<Button variant="outline" size="sm" className="flex items-center">
<Save className="h-4 w-4 mr-1" />
</Button>
<Button variant="outline" size="sm" className="flex items-center">
<Share2 className="h-4 w-4 mr-1" />
</Button>
<Button className="flex items-center">
使
<ArrowRight className="h-4 w-4 ml-1" />
</Button>
</div>
</TabsContent>
</Tabs>
</div>
)
}