优化角色列表弹出

This commit is contained in:
北枳 2025-08-18 21:42:20 +08:00
parent 541568f0f5
commit 5e2fd1fff9
3 changed files with 60 additions and 52 deletions

View File

@ -11,7 +11,7 @@ interface CharacterTokenOptions {
export function CharacterToken(props: ReactNodeViewProps) { export function CharacterToken(props: ReactNodeViewProps) {
const [showRoleList, setShowRoleList] = useState(false); const [showRoleList, setShowRoleList] = useState(false);
const [listPosition, setListPosition] = useState({ top: 0, left: 0 }); const [listPosition, setListPosition] = useState({ top: 0, left: 0, bottom: 0 });
const { name } = props.node.attrs as ScriptRoleEntity; const { name } = props.node.attrs as ScriptRoleEntity;
const extension = props.extension as Node<CharacterTokenOptions>; const extension = props.extension as Node<CharacterTokenOptions>;
const roles = extension.options.roles || []; const roles = extension.options.roles || [];
@ -31,6 +31,7 @@ export function CharacterToken(props: ReactNodeViewProps) {
// 计算理想的顶部位置在token下方 // 计算理想的顶部位置在token下方
let top = tokenRect.bottom + 8; // 8px 间距 let top = tokenRect.bottom + 8; // 8px 间距
let left = tokenRect.left; let left = tokenRect.left;
let bottom = tokenRect.top;
// 检查是否超出底部 // 检查是否超出底部
if (top + listRect.height > viewportHeight) { if (top + listRect.height > viewportHeight) {
@ -44,10 +45,18 @@ export function CharacterToken(props: ReactNodeViewProps) {
left = viewportWidth - listRect.width - 8; left = viewportWidth - listRect.width - 8;
} }
// 检查是否超出顶部
if (bottom - listRect.height < 0) {
// 如果超出顶部将列表显示在token下方
bottom = tokenRect.bottom + 8;
}
// 确保不会超出左侧 // 确保不会超出左侧
left = Math.max(8, left); left = Math.max(8, left);
// 确保不会超顶部
top = Math.max(0, top);
setListPosition({ top, left }); setListPosition({ top, left, bottom });
}; };
// 监听窗口大小变化 // 监听窗口大小变化
@ -107,7 +116,7 @@ export function CharacterToken(props: ReactNodeViewProps) {
exit={{ opacity: 0, y: 4 }} exit={{ opacity: 0, y: 4 }}
transition={{ duration: 0.2 }} transition={{ duration: 0.2 }}
ref={listRef} ref={listRef}
className="fixed w-64 rounded-lg backdrop-blur-md bg-white/10 border border-white/20 p-2 z-[51]" className="fixed w-64 rounded-lg backdrop-blur-md bg-white/10 border border-white/20 p-2 z-[51] overflow-y-auto"
style={{ style={{
top: listPosition.top, top: listPosition.top,
left: listPosition.left left: listPosition.left

View File

@ -26,17 +26,11 @@ const createEmptyShot = (): Shot => ({
name: `shot${Date.now()}`, name: `shot${Date.now()}`,
shotDescContent: [{ shotDescContent: [{
type: 'paragraph', type: 'paragraph',
content: [{ content: []
type: 'text',
text: 'Add shot description here...'
}]
}], }],
shotDialogsContent: [{ shotDialogsContent: [{
type: 'paragraph', type: 'paragraph',
content: [{ content: []
type: 'text',
text: 'Add shot dialogue here...'
}]
}] }]
}); });

View File

@ -342,55 +342,60 @@ export const ShotTabContent = (props: ShotTabContentProps) => {
<div className="space-y-4 col-span-1"> <div className="space-y-4 col-span-1">
{/* 选中的视频预览 */} {/* 选中的视频预览 */}
<> <>
{shotData[selectedIndex]?.status === 0 && ( {(shotData[selectedIndex]?.status === 0) && (
<div className="w-full h-full flex items-center gap-1 justify-center rounded-lg bg-black/30"> <div className="w-full h-full flex items-center gap-1 justify-center rounded-lg bg-black/30">
<Loader2 className="w-4 h-4 animate-spin text-blue-500" /> <Loader2 className="w-4 h-4 animate-spin text-blue-500" />
<span className="text-white/50">Loading...</span> <span className="text-white/50">Loading...</span>
</div> </div>
)} )}
{shotData[selectedIndex]?.status === 1 && ( <AnimatePresence mode="wait">
<motion.div {shotData[selectedIndex]?.status === 1 && shotData[selectedIndex]?.videoUrl.length && (
className="aspect-video rounded-lg overflow-hidden relative group" <motion.div
layoutId={`video-preview-${selectedIndex}`} className="aspect-video rounded-lg overflow-hidden relative group"
> key={`video-preview-${selectedIndex}`}
<PersonDetectionScene initial={{ opacity: 0 }}
videoSrc={shotData[selectedIndex]?.videoUrl[0].video_url} animate={{ opacity: 1 }}
detections={detections} exit={{ opacity: 0 }}
scanState={scanState} >
triggerScan={scanState === 'scanning'} <PersonDetectionScene
triggerSuccess={scanState === 'detected'} videoSrc={shotData[selectedIndex]?.videoUrl[0].video_url}
onScanTimeout={handleScanTimeout} detections={detections}
onScanExit={handleScanExit} scanState={scanState}
onDetectionsChange={handleDetectionsChange} triggerScan={scanState === 'scanning'}
onPersonClick={handlePersonClick} triggerSuccess={scanState === 'detected'}
/> onScanTimeout={handleScanTimeout}
<motion.div className='absolute top-4 right-4 flex gap-2'> onScanExit={handleScanExit}
{/* 人物替换按钮 */} onDetectionsChange={handleDetectionsChange}
<motion.button onPersonClick={handlePersonClick}
onClick={() => handleScan()} />
className={`p-2 backdrop-blur-sm transition-colors z-10 rounded-full <motion.div className='absolute top-4 right-4 flex gap-2'>
${scanState === 'detected' {/* 人物替换按钮 */}
? 'bg-cyan-500/50 hover:bg-cyan-500/70 text-white' <motion.button
: 'bg-black/50 hover:bg-black/70 text-white' onClick={() => handleScan()}
}`} className={`p-2 backdrop-blur-sm transition-colors z-10 rounded-full
whileHover={{ scale: 1.05 }} ${scanState === 'detected'
whileTap={{ scale: 0.95 }} ? 'bg-cyan-500/50 hover:bg-cyan-500/70 text-white'
> : 'bg-black/50 hover:bg-black/70 text-white'
{scanState === 'scanning' ? ( }`}
<Loader2 className="w-4 h-4 animate-spin" /> whileHover={{ scale: 1.05 }}
) : scanState === 'detected' ? ( whileTap={{ scale: 0.95 }}
<X className="w-4 h-4" /> >
) : ( {scanState === 'scanning' ? (
<User className="w-4 h-4" /> <Loader2 className="w-4 h-4 animate-spin" />
)} ) : scanState === 'detected' ? (
</motion.button> <X className="w-4 h-4" />
) : (
<User className="w-4 h-4" />
)}
</motion.button>
</motion.div>
</motion.div> </motion.div>
</motion.div> )}
)} </AnimatePresence>
{shotData[selectedIndex]?.status === 2 && ( {(shotData[selectedIndex]?.status === 2 || !shotData[selectedIndex]?.videoUrl.length) && (
<div className="w-full h-full flex gap-1 items-center justify-center rounded-lg bg-red-500/10"> <div className="w-full h-full flex gap-1 items-center justify-center rounded-lg bg-red-500/10">
<CircleX className="w-4 h-4 text-red-500" /> <CircleX className="w-4 h-4 text-red-500" />
<span className="text-white/50"></span> <span className="text-white/50">Failed, click to regenerate</span>
</div> </div>
)} )}
</> </>