From: wang.yu Date: Sun, 16 Aug 2026 02:34:49 +0000 (+0800) Subject: fix: 右下角 AI 图标改为可拖动并更换为机器人图标 X-Git-Url: http://acesimba.cloud/gitweb/?a=commitdiff_plain;h=6f9b19590b3cd5eb2a07a303a025203a1569d022;p=StationBuilder.git fix: 右下角 AI 图标改为可拖动并更换为机器人图标 问题:右下角固定的 AI 悬浮按钮有时会遮挡「下一步」按钮,且原 ✦ 图标不够突出。 改动: - 悬浮按钮支持拖拽移动:基于 Pointer 事件 + setPointerCapture 实现, 拖动实时更新位置并持久化到 localStorage(刷新后保持),超出视口自动夹紧; 拖动阈值 4px 内仍视为点击(打开对话框),双击复位到默认右下角。 - CSS 增加 cursor:grab/grabbing、touch-action:none、user-select:none, 触屏也可拖动。 - 图标由 ✦ 替换为机器人 SVG(屏幕+天线+双眼+嘴),在紫蓝渐变圆底上 更醒目,明确体现 AI 助手。 验证:tsc 对本项目改动文件无报错(db/vite/worker 的环境类型报错为既有问题, 与本改动无关)。 --- diff --git a/app/globals.css b/app/globals.css index 394705d..5bcf868 100644 --- a/app/globals.css +++ b/app/globals.css @@ -25,7 +25,8 @@ @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} diff --git a/app/page.tsx b/app/page.tsx index cb322d6..e91b0af 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,6 +1,6 @@ "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"; @@ -110,6 +110,37 @@ export default function Home(){ 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(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); @@ -315,7 +346,18 @@ export default function Home(){ {page==="preview"&&} {(["basic","assets","functions"] as PageKey[]).includes(page)&&
} {toast&&
✓{toast}
}{modal&&setModal("")} notify={notify} promptData={promptData}/>} - {!aiOpen&&} + {!aiOpen&&} {aiOpen&&
✦
StationBuilder AI模型 · {model}