forked from 77media/video-flow
199 lines
6.4 KiB
TypeScript
199 lines
6.4 KiB
TypeScript
import React, { useRef, useState, useEffect } from "react";
|
|
import { Image as ImageIcon, Send, Trash2 } from "lucide-react";
|
|
import { MessageBlock } from "./types";
|
|
import { useUploadFile } from "@/app/service/domain/service";
|
|
|
|
interface InputBarProps {
|
|
onSend: (blocks: MessageBlock[], videoId?: string) => void;
|
|
setVideoPreview?: (url: string, id: string) => void;
|
|
initialVideoUrl?: string;
|
|
initialVideoId?: string;
|
|
}
|
|
|
|
export function InputBar({ onSend, setVideoPreview, initialVideoUrl, initialVideoId }: InputBarProps) {
|
|
const [text, setText] = useState("");
|
|
const [isUploading, setIsUploading] = useState(false);
|
|
const [uploadProgress, setUploadProgress] = useState(0);
|
|
const [imageUrl, setImageUrl] = useState<string | null>(null);
|
|
const [videoUrl, setVideoUrl] = useState<string | null>(initialVideoUrl || null);
|
|
const [videoId, setVideoId] = useState<string | null>(initialVideoId || null);
|
|
|
|
const { uploadFile } = useUploadFile();
|
|
|
|
// 监听初始视频 URL 和 ID 的变化
|
|
useEffect(() => {
|
|
if (initialVideoUrl && initialVideoId) {
|
|
setVideoUrl(initialVideoUrl);
|
|
setVideoId(initialVideoId);
|
|
}
|
|
}, [initialVideoUrl, initialVideoId]);
|
|
|
|
const handleSend = () => {
|
|
const blocks: MessageBlock[] = [];
|
|
if (text.trim()) blocks.push({ type: "text" as const, text: text.trim() });
|
|
if (imageUrl) blocks.push({ type: "image" as const, url: imageUrl });
|
|
if (videoUrl) blocks.push({ type: "video" as const, url: videoUrl });
|
|
if (!blocks.length) return;
|
|
|
|
onSend(blocks, videoId || undefined);
|
|
setText("");
|
|
setImageUrl(null);
|
|
if (videoUrl && videoId && setVideoPreview) {
|
|
setVideoPreview(videoUrl, videoId);
|
|
}
|
|
setVideoUrl(null);
|
|
setVideoId(null);
|
|
};
|
|
|
|
const handleFileUpload = async (file: File) => {
|
|
try {
|
|
setIsUploading(true);
|
|
const url = await uploadFile(file, (progress) => {
|
|
setUploadProgress(progress);
|
|
});
|
|
// 如果已经有视频,先保存视频状态
|
|
const prevVideoUrl = videoUrl;
|
|
const prevVideoId = videoId;
|
|
|
|
setImageUrl(url);
|
|
|
|
// 恢复视频状态
|
|
if (prevVideoUrl && prevVideoId) {
|
|
setVideoUrl(prevVideoUrl);
|
|
setVideoId(prevVideoId);
|
|
}
|
|
} catch (error) {
|
|
console.error("上传失败:", error);
|
|
// 可以添加错误提示
|
|
} finally {
|
|
setIsUploading(false);
|
|
setUploadProgress(0);
|
|
}
|
|
};
|
|
|
|
const onFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const file = e.target.files?.[0];
|
|
// 保存对 input 元素的引用
|
|
const inputElement = e.currentTarget;
|
|
|
|
if (file) {
|
|
if (!file.type.startsWith('image/')) {
|
|
alert('请选择图片文件');
|
|
inputElement.value = ""; // 重置 input
|
|
return;
|
|
}
|
|
await handleFileUpload(file);
|
|
// 使用保存的引用重置 input
|
|
inputElement.value = "";
|
|
}
|
|
};
|
|
|
|
const removeImage = () => {
|
|
setImageUrl(null);
|
|
};
|
|
|
|
return (
|
|
<div data-alt="input-bar">
|
|
{/* 媒体预览 */}
|
|
<div className="px-3 pt-3 flex gap-2" data-alt="media-preview">
|
|
{/* 图片预览 */}
|
|
{imageUrl && (
|
|
<div className="relative group w-24 h-24">
|
|
<img
|
|
src={imageUrl}
|
|
className="h-full w-full object-cover rounded-xl border border-white/10"
|
|
alt="预览图"
|
|
/>
|
|
<button
|
|
onClick={removeImage}
|
|
className="absolute top-1 right-1 opacity-0 group-hover:opacity-100 transition bg-black/60 text-white rounded-full p-1"
|
|
title="移除"
|
|
data-alt="remove-image-button"
|
|
>
|
|
<Trash2 size={14} />
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
{/* 视频预览 */}
|
|
{videoUrl && (
|
|
<div className="relative group w-24 h-24">
|
|
<video
|
|
src={videoUrl}
|
|
className="h-full w-full object-cover rounded-xl border border-white/10"
|
|
controls={false}
|
|
/>
|
|
<button
|
|
onClick={() => {
|
|
if (setVideoPreview) {
|
|
setVideoPreview(videoUrl!, videoId!);
|
|
}
|
|
setVideoUrl(null);
|
|
setVideoId(null);
|
|
}}
|
|
className="absolute top-1 right-1 opacity-0 group-hover:opacity-100 transition bg-black/60 text-white rounded-full p-1"
|
|
title="移除"
|
|
data-alt="remove-video-button"
|
|
>
|
|
<Trash2 size={14} />
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* 上传进度 */}
|
|
{isUploading && (
|
|
<div className="px-3 py-2">
|
|
<div className="w-full bg-gray-200 rounded-full h-1">
|
|
<div
|
|
className="bg-blue-500 h-1 rounded-full transition-all duration-300"
|
|
style={{ width: `${uploadProgress}%` }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex items-center gap-2 px-3 m-3 border border-gray-700 rounded-[2rem]">
|
|
{/* 图片上传 */}
|
|
<label
|
|
className={`cursor-pointer inline-flex items-center gap-2 p-2 my-2 rounded-full hover:bg-gray-700/50 text-gray-100 ${isUploading ? 'opacity-50 cursor-not-allowed' : ''}`}
|
|
data-alt="file-upload"
|
|
>
|
|
<ImageIcon size={16} />
|
|
<input
|
|
className="hidden"
|
|
type="file"
|
|
accept="image/*"
|
|
onChange={onFileChange}
|
|
disabled={isUploading}
|
|
/>
|
|
</label>
|
|
|
|
{/* 文本输入 */}
|
|
<input
|
|
className="flex-1 bg-transparent text-gray-100 px-3 py-2 outline-none placeholder:text-gray-400"
|
|
placeholder="输入文字…"
|
|
value={text}
|
|
onChange={(e) => setText(e.target.value)}
|
|
onKeyDown={(e) => {
|
|
if (e.key === "Enter" && !e.shiftKey) {
|
|
e.preventDefault();
|
|
handleSend();
|
|
}
|
|
}}
|
|
data-alt="text-input"
|
|
/>
|
|
|
|
{/* 发送按钮 */}
|
|
<button
|
|
onClick={handleSend}
|
|
className="inline-flex items-center gap-2 p-2 my-2 rounded-full bg-blue-500 hover:bg-blue-400 text-white shadow disabled:bg-gray-500 disabled:hover:bg-gray-500 disabled:cursor-not-allowed"
|
|
data-alt="send-button"
|
|
disabled={!text.trim() && !imageUrl && !videoUrl}
|
|
>
|
|
<Send size={18} />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
} |