Commit de08d506 by luoqi

feat(web): 助手窗口跟随钮位弹出 — 上方优先/左右半屏对齐/视窗内夹紧

点开瞬间按钮当前位置算窗口落点:水平按钮在左/右半屏决定对齐边;垂直优先钮上方,
放不下弹下方;整体 clamp 视窗内。拖钮避让区域的意义由此成立。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
parent b46a8211
......@@ -19,6 +19,22 @@ import { AssistantChat } from './assistant-chat';
export function AssistantWidget() {
const allowed = useHasPermission(Permission.AGENT_INVOKE);
const [open, setOpen] = useState(false);
// 窗口落点:打开瞬间按"钮的当前位置"计算(拖钮的意义所在),并夹紧在视窗内
const [winPos, setWinPos] = useState<{ x: number; y: number } | null>(null);
const openAt = (fab: DOMRect) => {
const W = Math.min(400, window.innerWidth - 16);
const H = Math.min(620, Math.round(window.innerHeight * 0.78));
// 水平:钮在屏幕右半 → 窗口右缘对齐钮右缘;左半 → 左缘对齐
let x = fab.left + fab.width / 2 > window.innerWidth / 2 ? fab.right - W : fab.left;
// 垂直:优先弹在钮上方,放不下弹下方
let y = fab.top - H - 8;
if (y < 8) y = fab.bottom + 8;
x = Math.min(Math.max(x, 8), window.innerWidth - W - 8);
y = Math.min(Math.max(y, 8), window.innerHeight - H - 8);
setWinPos({ x, y });
setOpen(true);
};
// 场景化开场建议(规则,非 AI):有当前患者 → 围绕该患者;否则兜底通用
const current = usePlanSyncStore((s) => s.current);
const examples = current?.patientName
......@@ -35,8 +51,10 @@ export function AssistantWidget() {
<>
{/* 吸附窗:常驻挂载,关着时 invisible(状态不丢) */}
<div
style={winPos ? { left: winPos.x, top: winPos.y } : undefined}
className={cn(
'fixed bottom-4 right-4 z-[60] flex flex-col overflow-hidden rounded-xl border border-slate-200 shadow-2xl',
'fixed z-[60] flex flex-col overflow-hidden rounded-xl border border-slate-200 shadow-2xl',
!winPos && 'bottom-4 right-4',
'h-[620px] max-h-[78vh] w-[400px] max-w-[calc(100vw-2rem)]',
'transition-[opacity,transform] duration-150',
open ? 'translate-y-0 opacity-100' : 'pointer-events-none invisible translate-y-2 opacity-0',
......@@ -45,7 +63,7 @@ export function AssistantWidget() {
<AssistantChat variant="widget" onClose={() => setOpen(false)} examples={examples} />
</div>
{/* 悬浮开关钮(可拖移:位移 >4px 判定为拖,松手即记忆位置;否则当点击打开) */}
{!open && <DraggableFab onOpen={() => setOpen(true)} />}
{!open && <DraggableFab onOpen={openAt} />}
</>
);
}
......@@ -54,7 +72,7 @@ export function AssistantWidget() {
const FAB_POS_KEY = 'pac-assistant-fab-pos';
/** 可拖移悬浮钮:pointer 事件拖动(阈值 4px 区分点击),位置 localStorage 记忆并随窗口缩放夹紧。 */
function DraggableFab({ onOpen }: { onOpen: () => void }) {
function DraggableFab({ onOpen }: { onOpen: (fabRect: DOMRect) => void }) {
const [pos, setPos] = useState<{ x: number; y: number } | null>(null); // null = 默认右下角
const drag = useRef<{ startX: number; startY: number; baseX: number; baseY: number; moved: boolean } | null>(null);
const btnRef = useRef<HTMLButtonElement>(null);
......@@ -92,7 +110,8 @@ function DraggableFab({ onOpen }: { onOpen: () => void }) {
return p;
});
} else {
onOpen();
const r = btnRef.current?.getBoundingClientRect();
if (r) onOpen(r);
}
};
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment