231 lines
6.3 KiB
TypeScript
231 lines
6.3 KiB
TypeScript
|
|
"use client"
|
|||
|
|
|
|||
|
|
import { useState, useRef, type ChangeEvent } from "react"
|
|||
|
|
import { Button } from "@/components/ui/button"
|
|||
|
|
import { Textarea } from "@/components/ui/textarea"
|
|||
|
|
import { Input } from "@/components/ui/input"
|
|||
|
|
import { Image, Video, Link, X } from "lucide-react"
|
|||
|
|
|
|||
|
|
interface MessageEditorProps {
|
|||
|
|
onMessageChange: (message: {
|
|||
|
|
text: string
|
|||
|
|
images: File[]
|
|||
|
|
video: File | null
|
|||
|
|
link: string
|
|||
|
|
}) => void
|
|||
|
|
defaultValues?: {
|
|||
|
|
text: string
|
|||
|
|
images: string[]
|
|||
|
|
video: string
|
|||
|
|
link: string
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function MessageEditor({ onMessageChange, defaultValues }: MessageEditorProps) {
|
|||
|
|
const [text, setText] = useState(defaultValues?.text || "")
|
|||
|
|
const [images, setImages] = useState<File[]>([])
|
|||
|
|
const [imageUrls, setImageUrls] = useState<string[]>(defaultValues?.images || [])
|
|||
|
|
const [video, setVideo] = useState<File | null>(null)
|
|||
|
|
const [videoUrl, setVideoUrl] = useState<string>(defaultValues?.video || "")
|
|||
|
|
const [link, setLink] = useState(defaultValues?.link || "")
|
|||
|
|
|
|||
|
|
const imageInputRef = useRef<HTMLInputElement>(null)
|
|||
|
|
const videoInputRef = useRef<HTMLInputElement>(null)
|
|||
|
|
|
|||
|
|
const handleTextChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
|
|||
|
|
const newText = e.target.value
|
|||
|
|
if (newText.length <= 800) {
|
|||
|
|
setText(newText)
|
|||
|
|
onMessageChange({
|
|||
|
|
text: newText,
|
|||
|
|
images,
|
|||
|
|
video,
|
|||
|
|
link,
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const handleImageUpload = (e: ChangeEvent<HTMLInputElement>) => {
|
|||
|
|
const files = Array.from(e.target.files || [])
|
|||
|
|
if (files.length > 0) {
|
|||
|
|
// 检查文件大小和数量限制
|
|||
|
|
const validFiles = files.filter((file) => file.size <= 20 * 1024 * 1024) // 20MB
|
|||
|
|
const newImages = [...images, ...validFiles].slice(0, 9) // 最多9张图片
|
|||
|
|
|
|||
|
|
setImages(newImages)
|
|||
|
|
|
|||
|
|
// 创建临时URL用于预览
|
|||
|
|
const newImageUrls = newImages.map((file) => URL.createObjectURL(file))
|
|||
|
|
setImageUrls(newImageUrls)
|
|||
|
|
|
|||
|
|
onMessageChange({
|
|||
|
|
text,
|
|||
|
|
images: newImages,
|
|||
|
|
video,
|
|||
|
|
link,
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const handleVideoUpload = (e: ChangeEvent<HTMLInputElement>) => {
|
|||
|
|
const file = e.target.files?.[0]
|
|||
|
|
if (file && file.size <= 100 * 1024 * 1024) {
|
|||
|
|
// 100MB
|
|||
|
|
setVideo(file)
|
|||
|
|
setVideoUrl(URL.createObjectURL(file))
|
|||
|
|
|
|||
|
|
onMessageChange({
|
|||
|
|
text,
|
|||
|
|
images,
|
|||
|
|
video: file,
|
|||
|
|
link,
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const handleLinkChange = (e: ChangeEvent<HTMLInputElement>) => {
|
|||
|
|
const newLink = e.target.value
|
|||
|
|
setLink(newLink)
|
|||
|
|
|
|||
|
|
onMessageChange({
|
|||
|
|
text,
|
|||
|
|
images,
|
|||
|
|
video,
|
|||
|
|
link: newLink,
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const removeImage = (index: number) => {
|
|||
|
|
const newImages = [...images]
|
|||
|
|
newImages.splice(index, 1)
|
|||
|
|
setImages(newImages)
|
|||
|
|
|
|||
|
|
const newImageUrls = [...imageUrls]
|
|||
|
|
newImageUrls.splice(index, 1)
|
|||
|
|
setImageUrls(newImageUrls)
|
|||
|
|
|
|||
|
|
onMessageChange({
|
|||
|
|
text,
|
|||
|
|
images: newImages,
|
|||
|
|
video,
|
|||
|
|
link,
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const removeVideo = () => {
|
|||
|
|
setVideo(null)
|
|||
|
|
setVideoUrl("")
|
|||
|
|
|
|||
|
|
onMessageChange({
|
|||
|
|
text,
|
|||
|
|
images,
|
|||
|
|
video: null,
|
|||
|
|
link,
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div className="space-y-4 border rounded-md p-4">
|
|||
|
|
<div>
|
|||
|
|
<Textarea
|
|||
|
|
placeholder="请输入消息内容,最多800字"
|
|||
|
|
value={text}
|
|||
|
|
onChange={handleTextChange}
|
|||
|
|
className="min-h-[120px]"
|
|||
|
|
/>
|
|||
|
|
<div className="text-xs text-gray-500 mt-1 text-right">{text.length}/800</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* 图片预览区域 */}
|
|||
|
|
{imageUrls.length > 0 && (
|
|||
|
|
<div className="grid grid-cols-3 gap-2">
|
|||
|
|
{imageUrls.map((url, index) => (
|
|||
|
|
<div key={index} className="relative group">
|
|||
|
|
<img
|
|||
|
|
src={url || "/placeholder.svg"}
|
|||
|
|
alt={`上传的图片 ${index + 1}`}
|
|||
|
|
className="h-24 w-full object-cover rounded-md"
|
|||
|
|
/>
|
|||
|
|
<button
|
|||
|
|
type="button"
|
|||
|
|
onClick={() => removeImage(index)}
|
|||
|
|
className="absolute top-1 right-1 bg-black bg-opacity-50 rounded-full p-1 text-white opacity-0 group-hover:opacity-100 transition-opacity"
|
|||
|
|
>
|
|||
|
|
<X className="h-4 w-4" />
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{/* 视频预览区域 */}
|
|||
|
|
{videoUrl && (
|
|||
|
|
<div className="relative group">
|
|||
|
|
<video src={videoUrl} controls className="w-full h-48 object-cover rounded-md" />
|
|||
|
|
<button
|
|||
|
|
type="button"
|
|||
|
|
onClick={removeVideo}
|
|||
|
|
className="absolute top-2 right-2 bg-black bg-opacity-50 rounded-full p-1 text-white opacity-0 group-hover:opacity-100 transition-opacity"
|
|||
|
|
>
|
|||
|
|
<X className="h-4 w-4" />
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{/* 链接输入区域 */}
|
|||
|
|
{link && (
|
|||
|
|
<div className="flex items-center gap-2 p-2 bg-gray-50 rounded-md">
|
|||
|
|
<Link className="h-4 w-4 text-blue-500" />
|
|||
|
|
<a href={link} target="_blank" rel="noopener noreferrer" className="text-blue-500 text-sm flex-1 truncate">
|
|||
|
|
{link}
|
|||
|
|
</a>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{/* 工具栏 */}
|
|||
|
|
<div className="flex items-center gap-2 pt-2 border-t">
|
|||
|
|
<input
|
|||
|
|
type="file"
|
|||
|
|
ref={imageInputRef}
|
|||
|
|
onChange={handleImageUpload}
|
|||
|
|
accept="image/*"
|
|||
|
|
multiple
|
|||
|
|
className="hidden"
|
|||
|
|
/>
|
|||
|
|
<Button
|
|||
|
|
type="button"
|
|||
|
|
variant="outline"
|
|||
|
|
size="sm"
|
|||
|
|
onClick={() => imageInputRef.current?.click()}
|
|||
|
|
disabled={images.length >= 9}
|
|||
|
|
>
|
|||
|
|
<Image className="h-4 w-4 mr-1" />
|
|||
|
|
图片
|
|||
|
|
</Button>
|
|||
|
|
|
|||
|
|
<input type="file" ref={videoInputRef} onChange={handleVideoUpload} accept="video/*" className="hidden" />
|
|||
|
|
<Button
|
|||
|
|
type="button"
|
|||
|
|
variant="outline"
|
|||
|
|
size="sm"
|
|||
|
|
onClick={() => videoInputRef.current?.click()}
|
|||
|
|
disabled={!!video}
|
|||
|
|
>
|
|||
|
|
<Video className="h-4 w-4 mr-1" />
|
|||
|
|
视频
|
|||
|
|
</Button>
|
|||
|
|
|
|||
|
|
<div className="flex-1">
|
|||
|
|
<Input type="url" placeholder="输入链接地址" value={link} onChange={handleLinkChange} className="h-9" />
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="text-xs text-gray-500">
|
|||
|
|
<p>图片:最多9张,每张不超过20MB</p>
|
|||
|
|
<p>视频:最多1个,不超过100MB</p>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
|