2025-08-26 00:11:36 +08:00

252 lines
8.6 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.

import React, { useRef, useState, useEffect } from "react";
import { Image as ImageIcon, Send, Trash2, ArrowUp } 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 [isMultiline, setIsMultiline] = useState(false);
const textareaRef = useRef<HTMLTextAreaElement>(null);
const { uploadFile } = useUploadFile();
const adjustHeight = () => {
const textarea = textareaRef.current;
if (textarea) {
textarea.style.height = 'auto';
const newHeight = Math.min(Math.max(textarea.scrollHeight, 48), 120);
textarea.style.height = `${newHeight}px`;
// 检查是否超过一行48px 是单行高度)
setIsMultiline(newHeight > 48);
}
};
// 监听初始视频 URL 和 ID 的变化
useEffect(() => {
if (initialVideoUrl && initialVideoId) {
setVideoUrl(initialVideoUrl);
setVideoId(initialVideoId);
}
}, [initialVideoUrl, initialVideoId]);
// 监听文本变化和组件挂载时调整高度
useEffect(() => {
adjustHeight();
}, [text]);
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={`${isMultiline ? 'flex flex-col' : 'flex items-center'} gap-2 px-3 m-3 border border-gray-700 rounded-[2rem]`}>
{/* 图片上传按钮 - 单行时显示在左侧,多行时显示在底部 */}
{!isMultiline && (
<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>
)}
{/* 文本输入 */}
<textarea
ref={textareaRef}
placeholder="输入文字…"
className="w-full pl-[10px] pr-[10px] py-[14px] rounded-[10px] leading-[20px] text-sm border-none bg-transparent text-white placeholder:text-white/[0.40] focus:outline-none resize-none min-h-[48px] max-h-[120px] overflow-y-auto"
rows={1}
value={text}
onChange={(e) => setText(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
handleSend();
}
}}
data-alt="text-input"
/>
{isMultiline ? (
// 多行模式:底部按钮区域
<div className="flex justify-between items-center pb-2">
{/* 图片上传 */}
<label
className={`cursor-pointer inline-flex items-center gap-2 p-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>
{/* 发送按钮 */}
<button
onClick={handleSend}
className="inline-flex items-center gap-2 p-2 rounded-full bg-[#f1f3f4] text-[#25294b] shadow disabled:text-white/25 disabled:border disabled:border-white/10 disabled:bg-[#1b1b1b80] disabled:cursor-not-allowed"
data-alt="send-button"
disabled={!text.trim() && !imageUrl && !videoUrl}
>
<ArrowUp size={18} />
</button>
</div>
) : (
// 单行模式:发送按钮
<button
onClick={handleSend}
className="inline-flex items-center gap-2 p-2 my-2 rounded-full bg-[#f1f3f4] text-[#25294b] shadow disabled:text-white/25 disabled:border disabled:border-white/10 disabled:bg-[#1b1b1b80] disabled:cursor-not-allowed"
data-alt="send-button"
disabled={!text.trim() && !imageUrl && !videoUrl}
>
<ArrowUp size={18} />
</button>
)}
</div>
</div>
);
}