存客宝 React
This commit is contained in:
479
Cunkebao/app/scenarios/new/steps/PosterEditor.tsx
Normal file
479
Cunkebao/app/scenarios/new/steps/PosterEditor.tsx
Normal file
@@ -0,0 +1,479 @@
|
||||
"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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user