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); // 保存当前滚动位置
const scrollPos = window.scrollY;
// 重置高度以获取实际内容高度
textarea.style.height = '0px';
// 强制浏览器重排,获取准确的 scrollHeight
const scrollHeight = textarea.scrollHeight;
const newHeight = Math.min(Math.max(scrollHeight, 48), 120);
// 设置新高度
textarea.style.height = `${newHeight}px`; textarea.style.height = `${newHeight}px`;
// 检查是否超过一行48px 是单行高度) // 恢复滚动位置,避免页面跳动
setIsMultiline(newHeight > 48); 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,10 +216,22 @@ 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
className="px-3 m-3 border border-gray-700 rounded-[2rem]"
transition={{ duration: 0.2, ease: "easeInOut" }}
>
<motion.div
layout
className={`${isMultiline ? 'flex flex-col' : 'flex items-center'} gap-2`}
>
<AnimatePresence mode="popLayout">
{/* 图片上传按钮 - 单行时显示在左侧 */}
{!isMultiline && ( {!isMultiline && (
<label <motion.label
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
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' : ''}`} 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" data-alt="file-upload"
> >
@ -187,31 +243,49 @@ export function InputBar({ onSend, setVideoPreview, initialVideoUrl, initialVide
onChange={onFileChange} onChange={onFileChange}
disabled={isUploading} disabled={isUploading}
/> />
</label> </motion.label>
)} )}
{/* 文本输入 */} {/* 文本输入 */}
<motion.div layout className="flex-1">
<textarea <textarea
ref={textareaRef} ref={textareaRef}
placeholder="输入文字…" 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" 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"
rows={1} rows={1}
value={text} value={text}
onChange={(e) => setText(e.target.value)} onChange={(e) => setText(e.target.value)}
onCompositionStart={() => setIsComposing(true)}
onCompositionEnd={() => {
setIsComposing(false);
// 输入法完成后等待下一帧再调整高度,确保文本内容已更新
requestAnimationFrame(() => {
requestAnimationFrame(adjustHeight);
});
}}
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key === "Enter" && !e.shiftKey) { if (e.key === "Enter" && !e.shiftKey && !isComposing) {
e.preventDefault(); e.preventDefault();
handleSend(); handleSend();
} }
}} }}
data-alt="text-input" data-alt="text-input"
/> />
</motion.div>
{isMultiline ? ( {isMultiline ? (
// 多行模式:底部按钮区域 // 多行模式:底部按钮区域
<div className="flex justify-between items-center pb-2"> <motion.div
layout
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
className="flex justify-between items-center w-full pb-2"
>
{/* 图片上传 */} {/* 图片上传 */}
<label <motion.label
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
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={`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="file-upload"
> >
@ -223,30 +297,36 @@ export function InputBar({ onSend, setVideoPreview, initialVideoUrl, initialVide
onChange={onFileChange} onChange={onFileChange}
disabled={isUploading} disabled={isUploading}
/> />
</label> </motion.label>
{/* 发送按钮 必须有文字 */} {/* 发送按钮 必须有文字 */}
<button <motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={handleSend} 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" 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" data-alt="send-button"
disabled={!text.trim()} disabled={!text.trim()}
> >
<ArrowUp size={18} /> <ArrowUp size={18} />
</button> </motion.button>
</div> </motion.div>
) : ( ) : (
// 单行模式:发送按钮 // 单行模式:发送按钮
<button <motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={handleSend} 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" 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" data-alt="send-button"
disabled={!text.trim()} disabled={!text.trim()}
> >
<ArrowUp size={18} /> <ArrowUp size={18} />
</button> </motion.button>
)} )}
</div> </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 }) => {