diff --git a/apps/studio/src/components/LayoutWorkbench.tsx b/apps/studio/src/components/LayoutWorkbench.tsx index f0f8797..1541a7f 100644 --- a/apps/studio/src/components/LayoutWorkbench.tsx +++ b/apps/studio/src/components/LayoutWorkbench.tsx @@ -18,10 +18,11 @@ function CurvedCopy({layout,slot,value,style,spacing=0}:{layout:Layout;slot:Slot export function LayoutWorkbench({layout,design:initialDesign}:LayoutPageProps){ const [params,setParams]=useState(initialDesign?.parameters||emptyParameters()),[design,setDesign]=useState(initialDesign),[editing,setEditing]=useState(false),[selected,setSelected]=useState(null),[name,setName]=useState(initialDesign?.name||layout.name),[status,setStatus]=useState(''),[busy,setBusy]=useState(false),[dirty,setDirty]=useState(false),[history,setHistory]=useState([]),[future,setFuture]=useState([]),[zoom,setZoom]=useState(1),[panel,setPanel]=useState<'layers'|'properties'|null>(null),[textEditing,setTextEditing]=useState(null),[conflict,setConflict]=useState(false); const canvas=useRef(null),viewport=useRef(null); + const paramsRef=useRef(params);paramsRef.current=params; const elements=[...layout.elements,...(params.added||[])]; const upload=useRef(null),slot=elements.find(s=>s.id===selected); useEffect(()=>{const warn=(event:BeforeUnloadEvent)=>{if(dirty){event.preventDefault();event.returnValue=''}};window.addEventListener('beforeunload',warn);return()=>window.removeEventListener('beforeunload',warn)},[dirty]); - function patch(next:Parameters){try{const valid=parameters(layout,next);setHistory(old=>[...old.slice(-39),params]);setFuture([]);setParams(valid);setDirty(true);setStatus('')}catch(e){setStatus(e instanceof Error?e.message:'修改失败')}} + function patch(next:Parameters){try{const valid=parameters(layout,next);setHistory(old=>[...old.slice(-39),params]);setFuture([]);paramsRef.current=valid;setParams(valid);setDirty(true);setStatus('')}catch(e){setStatus(e instanceof Error?e.message:'修改失败')}} function updateText(s:Slot,changes:Partial){patch({...params,texts:{...params.texts,[s.id]:{...params.texts[s.id],value:params.texts[s.id]?.value??defaultText(layout,s),...changes}}})} function layer(s:Slot):LayerValue{return params.layers?.[s.id]||{}} function updateLayer(s:Slot,changes:LayerValue){patch({...params,layers:{...params.layers,[s.id]:{...layer(s),...changes}}})} @@ -39,8 +40,10 @@ export function LayoutWorkbench({layout,design:initialDesign}:LayoutPageProps){ function reset(s:Slot){const group=s.kind==='text'?'texts':s.kind==='image'?'images':'decorations',values={...params[group]};delete values[s.id];patch({...params,[group]:values})} function keySelect(e:KeyboardEvent,s:Slot){if(e.key==='ContextMenu'||(e.shiftKey&&e.key==='F10')){e.preventDefault();select(s)}if(e.key==='Escape')setSelected(null)} async function save(copy=false){ + // Commit an active native text edit before keyboard-triggered saving. + if(document.activeElement instanceof HTMLElement)document.activeElement.blur(); setBusy(true);try{ - const valid=parameters(layout,params),updating=design&&!copy; + const valid=parameters(layout,paramsRef.current),updating=design&&!copy; const response=await fetch('/api/studio/designs'+(updating?'/'+design.id:''),{method:updating?'PATCH':'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(updating?{name,parameters:valid,revision:design.revision}:{name,layoutId:layout.id,parameters:valid})}); const data=await response.json();if(!response.ok){if(response.status===409)setConflict(true);throw Error(data.error||'保存失败')};setConflict(false); setDesign(data.design);setDirty(false);setStatus('已保存,当前网址可直接打开此页面');