This commit is contained in:
海龙 2025-08-26 15:53:05 +08:00
commit a4a41da618
2 changed files with 165 additions and 85 deletions

View File

@ -1,7 +1,22 @@
import React, { useRef, useState, useEffect } from "react"; import React, { useRef, useState, useEffect, useCallback } from "react";
import { Image as ImageIcon, Send, Trash2, ArrowUp } from "lucide-react"; import { Image as ImageIcon, Send, Trash2, ArrowUp } from "lucide-react";
import { MessageBlock } from "./types"; import { MessageBlock } from "./types";
import { useUploadFile } from "@/app/service/domain/service"; import { useUploadFile } from "@/app/service/domain/service";
import { motion, AnimatePresence } from "framer-motion";
// 防抖函数
function debounce<T extends (...args: any[]) => void>(func: T, wait: number) {
let timeout: NodeJS.Timeout | null = null;
return function executedFunction(...args: Parameters<T>) {
if (timeout) {
clearTimeout(timeout);
}
timeout = setTimeout(() => {
func(...args);
timeout = null;
}, wait);
};
};
interface InputBarProps { interface InputBarProps {
onSend: (blocks: MessageBlock[], videoId?: string) => void; onSend: (blocks: MessageBlock[], videoId?: string) => void;
@ -21,18 +36,47 @@ export function InputBar({ onSend, setVideoPreview, initialVideoUrl, initialVide
const textareaRef = useRef<HTMLTextAreaElement>(null); const textareaRef = useRef<HTMLTextAreaElement>(null);
const { uploadFile } = useUploadFile(); const { uploadFile } = useUploadFile();
const [isComposing, setIsComposing] = useState(false);
const adjustHeight = () => { const adjustHeight = useCallback(() => {
const textarea = textareaRef.current; const textarea = textareaRef.current;
if (textarea) { if (!textarea || isComposing) return;
textarea.style.height = 'auto';
const newHeight = Math.min(Math.max(textarea.scrollHeight, 48), 120); // 保存当前滚动位置
textarea.style.height = `${newHeight}px`; const scrollPos = window.scrollY;
// 检查是否超过一行48px 是单行高度) // 重置高度以获取实际内容高度
setIsMultiline(newHeight > 48); textarea.style.height = '0px';
// 强制浏览器重排,获取准确的 scrollHeight
const scrollHeight = textarea.scrollHeight;
const newHeight = Math.min(Math.max(scrollHeight, 48), 120);
// 设置新高度
textarea.style.height = `${newHeight}px`;
// 恢复滚动位置,避免页面跳动
window.scrollTo(0, scrollPos);
// 更新布局状态
if (!text.trim()) {
setIsMultiline(false);
} else if (!isMultiline && newHeight > 48) {
setIsMultiline(true);
} }
}; }, [text, isMultiline, isComposing]);
// 使用防抖包装 adjustHeight但确保在输入完成时立即调整
const debouncedAdjustHeight = useCallback(
debounce(() => {
requestAnimationFrame(() => {
if (!isComposing) {
adjustHeight();
}
});
}, 16), // 使用一帧的时间作为防抖时间
[adjustHeight, isComposing]
);
// 监听初始视频 URL 和 ID 的变化 // 监听初始视频 URL 和 ID 的变化
useEffect(() => { useEffect(() => {
@ -44,8 +88,8 @@ export function InputBar({ onSend, setVideoPreview, initialVideoUrl, initialVide
// 监听文本变化和组件挂载时调整高度 // 监听文本变化和组件挂载时调整高度
useEffect(() => { useEffect(() => {
adjustHeight(); debouncedAdjustHeight();
}, [text]); }, [text, debouncedAdjustHeight]);
const handleSend = () => { const handleSend = () => {
const blocks: MessageBlock[] = []; const blocks: MessageBlock[] = [];
@ -172,81 +216,117 @@ export function InputBar({ onSend, setVideoPreview, initialVideoUrl, initialVide
</div> </div>
)} )}
<div className={`${isMultiline ? 'flex flex-col' : 'flex items-center'} gap-2 px-3 m-3 border border-gray-700 rounded-[2rem]`}> <motion.div
{/* 图片上传按钮 - 单行时显示在左侧,多行时显示在底部 */} layout
{!isMultiline && ( className="px-3 m-3 border border-gray-700 rounded-[2rem]"
<label transition={{ duration: 0.2, ease: "easeInOut" }}
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" <motion.div
> layout
<ImageIcon size={16} /> className={`${isMultiline ? 'flex flex-col' : 'flex items-center'} gap-2`}
<input >
className="hidden" <AnimatePresence mode="popLayout">
type="file" {/* 图片上传按钮 - 单行时显示在左侧 */}
accept="image/*" {!isMultiline && (
onChange={onFileChange} <motion.label
disabled={isUploading} initial={{ opacity: 0, scale: 0.8 }}
/> animate={{ opacity: 1, scale: 1 }}
</label> exit={{ opacity: 0, scale: 0.8 }}
)} 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}
/>
</motion.label>
)}
{/* 文本输入 */} {/* 文本输入 */}
<textarea <motion.div layout className="flex-1">
ref={textareaRef} <textarea
placeholder="输入文字…" ref={textareaRef}
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" placeholder="输入文字…"
rows={1} className="w-full pl-[10px] pr-[10px] py-[1rem] 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"
value={text} rows={1}
onChange={(e) => setText(e.target.value)} value={text}
onKeyDown={(e) => { onChange={(e) => setText(e.target.value)}
if (e.key === "Enter" && !e.shiftKey) { onCompositionStart={() => setIsComposing(true)}
e.preventDefault(); onCompositionEnd={() => {
handleSend(); setIsComposing(false);
} // 输入法完成后等待下一帧再调整高度,确保文本内容已更新
}} requestAnimationFrame(() => {
data-alt="text-input" requestAnimationFrame(adjustHeight);
/> });
}}
{isMultiline ? ( onKeyDown={(e) => {
// 多行模式:底部按钮区域 if (e.key === "Enter" && !e.shiftKey && !isComposing) {
<div className="flex justify-between items-center pb-2"> e.preventDefault();
{/* 图片上传 */} handleSend();
<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" data-alt="text-input"
>
<ImageIcon size={16} />
<input
className="hidden"
type="file"
accept="image/*"
onChange={onFileChange}
disabled={isUploading}
/> />
</label> </motion.div>
{/* 发送按钮 必须有文字 */} {isMultiline ? (
<button // 多行模式:底部按钮区域
onClick={handleSend} <motion.div
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" layout
data-alt="send-button" initial={{ opacity: 0, y: -10 }}
disabled={!text.trim()} animate={{ opacity: 1, y: 0 }}
> exit={{ opacity: 0, y: -10 }}
<ArrowUp size={18} /> className="flex justify-between items-center w-full pb-2"
</button> >
</div> {/* 图片上传 */}
) : ( <motion.label
// 单行模式:发送按钮 whileHover={{ scale: 1.05 }}
<button whileTap={{ scale: 0.95 }}
onClick={handleSend} 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' : ''}`}
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="file-upload"
data-alt="send-button" >
disabled={!text.trim()} <ImageIcon size={16} />
> <input
<ArrowUp size={18} /> className="hidden"
</button> type="file"
)} accept="image/*"
</div> onChange={onFileChange}
disabled={isUploading}
/>
</motion.label>
{/* 发送按钮 必须有文字 */}
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
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()}
>
<ArrowUp size={18} />
</motion.button>
</motion.div>
) : (
// 单行模式:发送按钮
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
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()}
>
<ArrowUp size={18} />
</motion.button>
)}
</AnimatePresence>
</motion.div>
</motion.div>
</div> </div>
); );
} }

View File

@ -38,7 +38,7 @@ const stageIconMap = {
} }
} }
const TAG_COLORS = ['#FF5733', '#126821', '#8d3913', '#FF33A1', '#A133FF', '#FF3333', '#3333FF', '#A1A1A1', '#a1115e', '#30527f']; const TAG_COLORS = ['#126821', '#A133FF', '#3333FF', '#a1115e'];
// 阶段图标组件 // 阶段图标组件
const StageIcons = ({ currentStage, isExpanded, isPauseWorkFlow }: { currentStage: number, isExpanded: boolean, isPauseWorkFlow: boolean }) => { const StageIcons = ({ currentStage, isExpanded, isPauseWorkFlow }: { currentStage: number, isExpanded: boolean, isPauseWorkFlow: boolean }) => {