refactor: overhaul UI for streamlined user experience

Redesign navigation, home overview, user portrait, and valuation pages
with improved functionality and responsive design.

Co-authored-by: null <4804959+fnvtk@users.noreply.github.com>
This commit is contained in:
v0
2025-07-18 13:47:12 +00:00
parent 440b310c6f
commit 2408d50cb0
316 changed files with 55785 additions and 0 deletions

View File

@@ -0,0 +1,79 @@
"use client"
import type React from "react"
import { useState } from "react"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Textarea } from "@/components/ui/textarea"
export function BasicSettings({ onNext }: { onNext: () => void }) {
const [name, setName] = useState("")
const [description, setDescription] = useState("")
const [category, setCategory] = useState("")
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
// 保存基本设置数据的逻辑
onNext()
}
return (
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<form onSubmit={handleSubmit}>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="name"></Label>
<Input
id="name"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="请输入场景名称"
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="category"></Label>
<Select value={category} onValueChange={setCategory} required>
<SelectTrigger id="category">
<SelectValue placeholder="选择场景类别" />
</SelectTrigger>
<SelectContent>
<SelectItem value="marketing"></SelectItem>
<SelectItem value="service"></SelectItem>
<SelectItem value="operation"></SelectItem>
<SelectItem value="sales"></SelectItem>
<SelectItem value="other"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="description"></Label>
<Textarea
id="description"
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="请输入场景描述"
rows={4}
/>
</div>
</CardContent>
<CardFooter className="flex justify-between">
<Button variant="outline" type="button">
</Button>
<Button type="submit"></Button>
</CardFooter>
</form>
</Card>
)
}

View File

@@ -0,0 +1,98 @@
"use client"
import type React from "react"
import { useState } from "react"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
import { Switch } from "@/components/ui/switch"
import { Label } from "@/components/ui/label"
import { Input } from "@/components/ui/input"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Textarea } from "@/components/ui/textarea"
export function FriendRequestSettings({ onBack, onNext }: { onBack: () => void; onNext: () => void }) {
const [enableFriendRequests, setEnableFriendRequests] = useState(false)
const [requestMessage, setRequestMessage] = useState("")
const [requestLimit, setRequestLimit] = useState("10")
const [requestInterval, setRequestInterval] = useState("daily")
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
// 保存好友请求设置数据的逻辑
onNext()
}
return (
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<form onSubmit={handleSubmit}>
<CardContent className="space-y-6">
<div className="flex items-center justify-between">
<Label htmlFor="enable-friend-requests" className="flex flex-col space-y-1">
<span></span>
<span className="font-normal text-sm text-muted-foreground"></span>
</Label>
<Switch
id="enable-friend-requests"
checked={enableFriendRequests}
onCheckedChange={setEnableFriendRequests}
/>
</div>
{enableFriendRequests && (
<>
<div className="space-y-2">
<Label htmlFor="request-message"></Label>
<Textarea
id="request-message"
value={requestMessage}
onChange={(e) => setRequestMessage(e.target.value)}
placeholder="请输入好友请求附带的消息"
rows={3}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="request-limit"></Label>
<Input
id="request-limit"
type="number"
min="1"
value={requestLimit}
onChange={(e) => setRequestLimit(e.target.value)}
placeholder="请求数量上限"
/>
</div>
<div className="space-y-2">
<Label htmlFor="request-interval"></Label>
<Select value={requestInterval} onValueChange={setRequestInterval}>
<SelectTrigger id="request-interval">
<SelectValue placeholder="选择请求频率" />
</SelectTrigger>
<SelectContent>
<SelectItem value="hourly"></SelectItem>
<SelectItem value="daily"></SelectItem>
<SelectItem value="weekly"></SelectItem>
</SelectContent>
</Select>
</div>
</div>
</>
)}
</CardContent>
<CardFooter className="flex justify-between">
<Button variant="outline" type="button" onClick={onBack}>
</Button>
<Button type="submit"></Button>
</CardFooter>
</form>
</Card>
)
}

View File

@@ -0,0 +1,128 @@
"use client"
import type React from "react"
import { useState } from "react"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
import { Switch } from "@/components/ui/switch"
import { Label } from "@/components/ui/label"
import { Textarea } from "@/components/ui/textarea"
import { Input } from "@/components/ui/input"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
export function MessageSettings({ onBack, onNext }: { onBack: () => void; onNext: () => void }) {
const [enableMessages, setEnableMessages] = useState(false)
const [messageType, setMessageType] = useState("text")
const [messageContent, setMessageContent] = useState("")
const [messageSchedule, setMessageSchedule] = useState("immediate")
const [customTime, setCustomTime] = useState("")
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
// 保存消息设置数据的逻辑
onNext()
}
return (
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<form onSubmit={handleSubmit}>
<CardContent className="space-y-6">
<div className="flex items-center justify-between">
<Label htmlFor="enable-messages" className="flex flex-col space-y-1">
<span></span>
<span className="font-normal text-sm text-muted-foreground"></span>
</Label>
<Switch id="enable-messages" checked={enableMessages} onCheckedChange={setEnableMessages} />
</div>
{enableMessages && (
<>
<div className="space-y-2">
<Label></Label>
<Tabs value={messageType} onValueChange={setMessageType}>
<TabsList className="grid grid-cols-3">
<TabsTrigger value="text"></TabsTrigger>
<TabsTrigger value="image"></TabsTrigger>
<TabsTrigger value="link"></TabsTrigger>
</TabsList>
<TabsContent value="text" className="mt-4">
<Textarea
value={messageContent}
onChange={(e) => setMessageContent(e.target.value)}
placeholder="请输入文本消息内容"
rows={4}
/>
</TabsContent>
<TabsContent value="image" className="mt-4">
<div className="space-y-4">
<Input type="file" accept="image/*" />
<Textarea
value={messageContent}
onChange={(e) => setMessageContent(e.target.value)}
placeholder="请输入图片描述(可选)"
rows={2}
/>
</div>
</TabsContent>
<TabsContent value="link" className="mt-4 space-y-4">
<Input
value={messageContent}
onChange={(e) => setMessageContent(e.target.value)}
placeholder="请输入链接地址"
/>
<Textarea placeholder="请输入链接描述(可选)" rows={2} />
</TabsContent>
</Tabs>
</div>
<div className="space-y-4">
<Label></Label>
<Select value={messageSchedule} onValueChange={setMessageSchedule}>
<SelectTrigger>
<SelectValue placeholder="选择发送时间" />
</SelectTrigger>
<SelectContent>
<SelectItem value="immediate"></SelectItem>
<SelectItem value="scheduled"></SelectItem>
<SelectItem value="triggered"></SelectItem>
</SelectContent>
</Select>
{messageSchedule === "scheduled" && (
<div className="pt-2">
<Label htmlFor="custom-time"></Label>
<Input
id="custom-time"
type="datetime-local"
value={customTime}
onChange={(e) => setCustomTime(e.target.value)}
className="mt-2"
/>
</div>
)}
{messageSchedule === "triggered" && (
<div className="border rounded-md p-4 mt-2">
<p className="text-sm text-muted-foreground"></p>
</div>
)}
</div>
</>
)}
</CardContent>
<CardFooter className="flex justify-between">
<Button variant="outline" type="button" onClick={onBack}>
</Button>
<Button type="submit"></Button>
</CardFooter>
</form>
</Card>
)
}

View File

@@ -0,0 +1,478 @@
"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>
)
}

View File

@@ -0,0 +1,104 @@
"use client"
import type React from "react"
import { useState } from "react"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
import { Switch } from "@/components/ui/switch"
import { Label } from "@/components/ui/label"
import { Input } from "@/components/ui/input"
import { Badge } from "@/components/ui/badge"
import { X } from "lucide-react"
export function TagSettings({ onBack, onComplete }: { onBack: () => void; onComplete: () => void }) {
const [enableTags, setEnableTags] = useState(false)
const [tagInput, setTagInput] = useState("")
const [tags, setTags] = useState<string[]>([])
const handleAddTag = () => {
if (tagInput.trim() && !tags.includes(tagInput.trim())) {
setTags([...tags, tagInput.trim()])
setTagInput("")
}
}
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "Enter") {
e.preventDefault()
handleAddTag()
}
}
const handleRemoveTag = (tagToRemove: string) => {
setTags(tags.filter((tag) => tag !== tagToRemove))
}
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
// 保存标签设置数据的逻辑
onComplete()
}
return (
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<form onSubmit={handleSubmit}>
<CardContent className="space-y-6">
<div className="flex items-center justify-between">
<Label htmlFor="enable-tags" className="flex flex-col space-y-1">
<span></span>
<span className="font-normal text-sm text-muted-foreground"></span>
</Label>
<Switch id="enable-tags" checked={enableTags} onCheckedChange={setEnableTags} />
</div>
{enableTags && (
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="tag-input"></Label>
<div className="flex space-x-2">
<Input
id="tag-input"
value={tagInput}
onChange={(e) => setTagInput(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="输入标签名称并按回车"
/>
<Button type="button" onClick={handleAddTag}>
</Button>
</div>
</div>
<div>
<Label></Label>
<div className="flex flex-wrap gap-2 mt-2">
{tags.length > 0 ? (
tags.map((tag) => (
<Badge key={tag} className="px-3 py-1 flex items-center gap-1">
{tag}
<X className="h-3 w-3 cursor-pointer" onClick={() => handleRemoveTag(tag)} />
</Badge>
))
) : (
<p className="text-sm text-muted-foreground"></p>
)}
</div>
</div>
</div>
)}
</CardContent>
<CardFooter className="flex justify-between">
<Button variant="outline" type="button" onClick={onBack}>
</Button>
<Button type="submit"></Button>
</CardFooter>
</form>
</Card>
)
}

View File

@@ -0,0 +1,209 @@
"use client"
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "@/components/ui/select"
import { useState } from "react"
import { Card } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Table, TableHeader, TableRow, TableHead, TableBody, TableCell } from "@/components/ui/table"
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"
import { Plus, Pencil, Trash2, Link2 } from "lucide-react"
import { toast } from "@/components/ui/use-toast"
interface Channel {
id: string
name: string
type: "team" | "other"
link?: string
}
interface TrafficChannelSettingsProps {
formData: any
onChange: (data: any) => void
onNext: () => void
onPrev: () => void
}
function isValidUrl(string: string) {
try {
new URL(string)
return true
} catch (_) {
return false
}
}
export function TrafficChannelSettings({ formData, onChange, onNext, onPrev }: TrafficChannelSettingsProps) {
const [channels, setChannels] = useState<Channel[]>(formData.channels || [])
const [isAddChannelOpen, setIsAddChannelOpen] = useState(false)
const [editingChannel, setEditingChannel] = useState<Channel | null>(null)
const [newChannel, setNewChannel] = useState<Partial<Channel>>({
name: "",
type: "team",
link: "",
})
const handleAddChannel = () => {
if (!newChannel.name) return
if (newChannel.link && !isValidUrl(newChannel.link)) {
toast({
title: "错误",
description: "请输入有效的URL",
variant: "destructive",
})
return
}
if (editingChannel) {
setChannels(
channels.map((channel) => (channel.id === editingChannel.id ? { ...channel, ...newChannel } : channel)),
)
} else {
setChannels([
...channels,
{
id: Date.now().toString(),
name: newChannel.name,
type: newChannel.type || "team",
link: newChannel.link,
} as Channel,
])
}
setIsAddChannelOpen(false)
setNewChannel({ name: "", type: "team", link: "" })
setEditingChannel(null)
onChange({ ...formData, channels })
}
const handleEditChannel = (channel: Channel) => {
setEditingChannel(channel)
setNewChannel(channel)
setIsAddChannelOpen(true)
}
const handleDeleteChannel = (channelId: string) => {
setChannels(channels.filter((channel) => channel.id !== channelId))
onChange({ ...formData, channels: channels.filter((channel) => channel.id !== channelId) })
}
return (
<Card className="p-6">
<div className="space-y-6">
<div className="flex justify-between items-center">
<h2 className="text-lg font-semibold">流量通道设置</h2>
<Button onClick={() => setIsAddChannelOpen(true)}>
<Plus className="h-4 w-4 mr-2" />
添加通道
</Button>
</div>
<div className="border rounded-lg">
<Table>
<TableHeader>
<TableRow>
<TableHead>通道名称</TableHead>
<TableHead>类型</TableHead>
<TableHead>链接</TableHead>
<TableHead className="text-right">操作</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{channels.length === 0 ? (
<TableRow>
<TableCell colSpan={4} className="text-center py-8 text-gray-500">
暂无数据
</TableCell>
</TableRow>
) : (
channels.map((channel) => (
<TableRow key={channel.id}>
<TableCell>{channel.name}</TableCell>
<TableCell>{channel.type === "team" ? "打粉团队" : "其他"}</TableCell>
<TableCell>
{channel.link && (
<div className="flex items-center space-x-2">
<Link2 className="h-4 w-4" />
<span className="text-blue-600">{channel.link}</span>
</div>
)}
</TableCell>
<TableCell className="text-right">
<div className="flex justify-end space-x-2">
<Button variant="ghost" size="sm" onClick={() => handleEditChannel(channel)}>
<Pencil className="h-4 w-4" />
</Button>
<Button variant="ghost" size="sm" onClick={() => handleDeleteChannel(channel.id)}>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
<div className="flex justify-between">
<Button variant="outline" onClick={onPrev}>
上一步
</Button>
<Button onClick={onNext}>完成</Button>
</div>
</div>
<Dialog open={isAddChannelOpen} onOpenChange={setIsAddChannelOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>{editingChannel ? "编辑通道" : "添加通道"}</DialogTitle>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label>通道名称</Label>
<Input
value={newChannel.name}
onChange={(e) => setNewChannel({ ...newChannel, name: e.target.value })}
placeholder="请输入通道名称"
/>
</div>
<div className="space-y-2">
<Label>通道类型</Label>
<Select
value={newChannel.type}
onValueChange={(value) => setNewChannel({ ...newChannel, type: value as "team" | "other" })}
>
<SelectTrigger>
<SelectValue placeholder="选择通道类型" />
</SelectTrigger>
<SelectContent>
<SelectItem value="team">打粉团队</SelectItem>
<SelectItem value="other">其他</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>通道链接(选填)</Label>
<Input
value={newChannel.link}
onChange={(e) => setNewChannel({ ...newChannel, link: e.target.value })}
placeholder="请输入通道链接"
/>
{newChannel.link && !isValidUrl(newChannel.link) && (
<p className="text-sm text-red-500">请输入有效的URL</p>
)}
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsAddChannelOpen(false)}>
取消
</Button>
<Button onClick={handleAddChannel}>{editingChannel ? "保存" : "添加"}</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</Card>
)
}