@media(max-width:1100px){.app-shell{padding-left:42px}.side-nav{display:none}.workspace{min-width:900px}.sticky-actions{left:42px}.top-actions small{display:none}.result-layout{grid-template-columns:1fr}.preview-main{grid-template-columns:1fr 260px}.mu-grid{grid-template-columns:repeat(2,1fr)}}
/* 右下角浮动 AI 会话 */
-.ai-fab{position:fixed;right:22px;bottom:22px;z-index:80;width:54px;height:54px;border-radius:50%;border:0;background:linear-gradient(135deg,#6d5ee9,#0aa5e8);color:#fff;font-size:22px;box-shadow:0 8px 22px #0a85c355;display:grid;place-items:center}
+.ai-fab{position:fixed;right:22px;bottom:22px;z-index:80;width:54px;height:54px;border-radius:50%;border:0;background:linear-gradient(135deg,#6d5ee9,#0aa5e8);color:#fff;font-size:22px;box-shadow:0 8px 22px #0a85c355;display:grid;place-items:center;cursor:grab;touch-action:none;user-select:none}
+.ai-fab:active{cursor:grabbing}
.ai-fab:hover{transform:translateY(-1px);box-shadow:0 12px 26px #0a85c366}
.ai-dialog{position:fixed;right:22px;bottom:88px;z-index:81;width:25vw;min-width:320px;height:70vh;min-height:420px;background:#fff;border:1px solid #d2d6d9;border-radius:10px;box-shadow:0 18px 48px #0003;display:flex;flex-direction:column;overflow:hidden}
.ai-dialog header{display:flex;align-items:center;gap:9px;padding:10px 12px;border-bottom:1px solid #eceef0;background:linear-gradient(135deg,#6d5ee9,#0aa5e8);color:#fff}
"use client";
-import { useEffect, useMemo, useRef, useState } from "react";
+import { useEffect, useMemo, useRef, useState, type PointerEvent as ReactPointerEvent } from "react";
type PageKey = "login" | "projects" | "basic" | "assets" | "functions" | "generate" | "result" | "preview" | "domains" | "domainDetail";
type ModalKey = "" | "feedback" | "download" | "history" | "prompt";
const [model,setModel]=useState("Hy3");
const [modelOpen,setModelOpen]=useState(false);
const [aiOpen,setAiOpen]=useState(false);
+ const [fabPos,setFabPos]=useState<{x:number;y:number}|null>(()=>{ try{ const s=localStorage.getItem("ai-fab-pos"); return s?JSON.parse(s):null; }catch{ return null; } });
+ const fabRef=useRef<HTMLButtonElement|null>(null);
+ const dragState=useRef<{active:boolean;startX:number;startY:number;origX:number;origY:number;moved:boolean}|null>(null);
+ const lastPosRef=useRef<{x:number;y:number}|null>(null);
+ const onFabPointerDown=(e:ReactPointerEvent)=>{
+ const btn=fabRef.current; if(!btn) return;
+ const r=btn.getBoundingClientRect();
+ dragState.current={active:true,startX:e.clientX,startY:e.clientY,origX:r.left,origY:r.top,moved:false};
+ try{ btn.setPointerCapture(e.pointerId); }catch{}
+ };
+ const onFabPointerMove=(e:ReactPointerEvent)=>{
+ const ds=dragState.current; if(!ds||!ds.active) return;
+ const dx=e.clientX-ds.startX, dy=e.clientY-ds.startY;
+ if(Math.abs(dx)>4||Math.abs(dy)>4) ds.moved=true;
+ if(!ds.moved) return;
+ const btn=fabRef.current!; const w=btn.offsetWidth, h=btn.offsetHeight;
+ let x=Math.max(0,Math.min(ds.origX+dx, window.innerWidth-w));
+ let y=Math.max(0,Math.min(ds.origY+dy, window.innerHeight-h));
+ const p={x,y}; lastPosRef.current=p; setFabPos(p);
+ };
+ const onFabPointerUp=(e:ReactPointerEvent)=>{
+ const ds=dragState.current; dragState.current=null;
+ try{ fabRef.current?.releasePointerCapture(e.pointerId); }catch{}
+ if(!ds) return;
+ if(ds.moved){
+ if(lastPosRef.current) localStorage.setItem("ai-fab-pos", JSON.stringify(lastPosRef.current));
+ } else {
+ setAiOpen(true);
+ }
+ };
+ const resetFab=()=>{ setFabPos(null); lastPosRef.current=null; try{ localStorage.removeItem("ai-fab-pos"); }catch{} };
const [aiMessages,setAiMessages]=useState<{role:"user"|"assistant";content:string}[]>([]);
const [aiDraft,setAiDraft]=useState("");
const [aiBusy,setAiBusy]=useState(false);
{page==="preview"&&<Preview site={site} tab={previewTab} setTab={setPreviewTab} setModal={setModal} notify={notify}/>}
</div>{(["basic","assets","functions"] as PageKey[]).includes(page)&&<div className="sticky-actions"><Button secondary onClick={()=>go(page==="basic"?"projects":page==="assets"?"basic":"assets")}>上一步</Button><div><Button secondary onClick={()=>notify("草稿已保存")}>保存草稿</Button><Button onClick={()=>go(page==="basic"?"assets":page==="assets"?"functions":"generate")}>下一步</Button></div></div>}</main>
{toast&&<div className="toast"><b>✓</b>{toast}</div>}{modal&&<Modal type={modal} close={()=>setModal("")} notify={notify} promptData={promptData}/>}
- {!aiOpen&&<button className="ai-fab" title="AI 会话" onClick={()=>setAiOpen(true)}>✦</button>}
+ {!aiOpen&&<button ref={fabRef} className="ai-fab" title="AI 助手 · 拖动可移动位置,双击复位" aria-label="AI 助手"
+ style={fabPos?{left:fabPos.x,top:fabPos.y,right:"auto",bottom:"auto"}:undefined}
+ onPointerDown={onFabPointerDown} onPointerMove={onFabPointerMove} onPointerUp={onFabPointerUp} onDoubleClick={resetFab}>
+ <svg viewBox="0 0 24 24" width="28" height="28" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
+ <rect x="4" y="8" width="16" height="11" rx="3"/>
+ <path d="M12 8V4"/>
+ <circle cx="12" cy="3" r="1.4" fill="currentColor" stroke="none"/>
+ <circle cx="9" cy="13" r="1.3" fill="currentColor" stroke="none"/>
+ <circle cx="15" cy="13" r="1.3" fill="currentColor" stroke="none"/>
+ <path d="M9.5 16h5"/>
+ </svg>
+ </button>}
{aiOpen&&<div className="ai-dialog">
<header><span className="ai-dialog-logo">✦</span><div className="ai-dialog-title"><b>StationBuilder AI</b><small>模型 · {model}</small></div><button className="ai-close" onClick={()=>setAiOpen(false)}>×</button></header>
<div className="ai-messages">