var c=document.getElementById('c'),ctx=c.getContext('2d'); var ei=document.getElementById('ei'),ed=document.getElementById('editor'); var SCALE=1,ox=0,oy=0; var dragging=false,dragX=0,dragY=0; var selected=null,editing=null; var colors=['#8B5CF6','#4A90E2','#5CB85C','#E8985A','#E54D42','#06B6D4','#F59E0B','#EC4899']; var STORAGE_KEY='yuxingu_maps'; // ---- Multi-map state ---- var maps=[],currentIdx=-1; function defaultTree(){ return { id:'root',text:'新建脑图',color:colors[0],children:[] }; } function defaultTemplate(){ return { id:'root',text:'愈心谷小程序',color:colors[0], children:[ {id:'b1',text:'愈心魔方',color:colors[0],children:[ {id:'b1a',text:'急救魔方'},{id:'b1b',text:'亲子魔方'},{id:'b1c',text:'成长魔方'}, {id:'b1d',text:'解构魔方'},{id:'b1e',text:'艺术魔方'},{id:'b1f',text:'情感魔方'} ]}, {id:'b2',text:'私域活动',color:colors[1],children:[ {id:'b2a',text:'心理研学'},{id:'b2b',text:'心灵旅修'} ]}, {id:'b3',text:'心理咨询',color:colors[2]}, {id:'b4',text:'心理评估',color:colors[3],children:[ {id:'b4a',text:'AI面部识别'},{id:'b4b',text:'脑机接口'} ]}, {id:'b5',text:'健康管理',color:colors[4]} ] }; } // ---- Persistence ---- function loadMaps(){ try{ var d=localStorage.getItem(STORAGE_KEY); return d?JSON.parse(d):[]; }catch(e){return [];} } function saveMaps(){ try{localStorage.setItem(STORAGE_KEY,JSON.stringify(maps))}catch(e){} // sync to server (all maps) fetch('/save',{method:'POST',body:JSON.stringify(maps)}).catch(function(){}); } function syncServer(){ fetch('/load').then(function(r){return r.json()}).then(function(data){ if(Array.isArray(data)&&data.length>0){ maps=data;saveMaps();renderSidebar(); if(currentIdx<0||currentIdx>=maps.length)switchMap(0); } }).catch(function(){}); } // ---- Sidebar ---- function renderSidebar(){ var sl=document.getElementById('slist'); sl.innerHTML=''; maps.forEach(function(m,i){ var d=document.createElement('div'); d.className='item'+(i===currentIdx?' active':''); d.innerHTML=''+escHtml(m.name||'脑图'+(i+1))+''; d.onclick=function(){switchMap(i)}; sl.appendChild(d); }); } function escHtml(s){return s.replace(/&/g,'&').replace(//g,'>');} function switchMap(idx){ if(idx<0||idx>=maps.length)return; currentIdx=idx; tree=JSON.parse(JSON.stringify(maps[idx].tree)); selected=null; renderSidebar(); layout();draw(); // close sidebar on mobile after selection if(window.innerWidth<=600)closeSidebar(); } function newMap(){ var name=prompt('脑图名称:','脑图'+(maps.length+1)); if(!name)return; maps.push({name:name,tree:defaultTree()}); saveMaps(); renderSidebar(); switchMap(maps.length-1); } function renameMap(){ if(currentIdx<0)return; var name=prompt('新名称:',maps[currentIdx].name); if(!name)return; maps[currentIdx].name=name; saveMaps();renderSidebar(); } function deleteMap(idx){ if(!confirm('删除「'+maps[idx].name+'」?不可恢复。'))return; maps.splice(idx,1); saveMaps();renderSidebar(); if(maps.length===0){maps.push({name:'愈心谷小程序',tree:defaultTemplate()});saveMaps();renderSidebar();} if(currentIdx>=maps.length)currentIdx=maps.length-1; switchMap(currentIdx); } var _saveTimer=null; function autoSave(){ if(currentIdx<0)return; maps[currentIdx].tree=JSON.parse(JSON.stringify(exportTree(tree))); saveMaps(); var h=document.getElementById('saveHint'); if(h){h.textContent='💾 已自动保存';h.style.color='#aaa'; clearTimeout(_saveTimer); _saveTimer=setTimeout(function(){h.textContent='';},2000);} } // ---- Tree export ---- function exportTree(node){ var o={id:node.id,text:node.text}; if(node.color)o.color=node.color; if(node.children)o.children=node.children.map(exportTree); return o; } // ---- Layout & Draw ---- var tree=defaultTemplate(); var NODE_W=120,NODE_H=34,HGAP=60,VGAP=14; function uid(){return 'n'+Date.now()+Math.random().toString(36).slice(2,6)} function getColor(node,parentColor){ if(node.color)return node.color; if(parentColor)return parentColor; return colors[0]; } function layoutHeight(node){ if(!node.children||node.children.length===0)return NODE_H; var h=0; for(var i=0;i0){ var totalH=0; for(var i=0;i=node._x&&mx<=node._x+node._w&&my>=node._y&&my<=node._y+node._h)return node; if(node.children)for(var i=0;i0?0.9:1.1))); draw(); },{passive:false}); c.addEventListener('dblclick',function(e){ var rect=c.getBoundingClientRect(); var sx=(e.clientX-rect.left-c.width/2-ox)/SCALE; var sy=(e.clientY-rect.top-c.height/2-oy)/SCALE; var node=hitTest(sx,sy); if(node){ editing=node; var cx=rect.left+c.width/2+ox+node._x*SCALE; var cy=rect.top+c.height/2+oy+node._y*SCALE; ed.style.display='block';ed.style.left=cx+'px';ed.style.top=cy+'px'; ei.value=node.text;ei.focus();ei.select(); } }); // Touch c.addEventListener('touchstart',function(e){ if(e.touches.length===1){ var rect=c.getBoundingClientRect(); var sx=(e.touches[0].clientX-rect.left-c.width/2-ox)/SCALE; var sy=(e.touches[0].clientY-rect.top-c.height/2-oy)/SCALE; var node=hitTest(sx,sy); if(node){selected=node;draw();} else{dragging=true;dragX=e.touches[0].clientX;dragY=e.touches[0].clientY;} } },{passive:false}); c.addEventListener('touchmove',function(e){ if(dragging&&e.touches.length===1){ e.preventDefault(); ox+=e.touches[0].clientX-dragX;oy+=e.touches[0].clientY-dragY; dragX=e.touches[0].clientX;dragY=e.touches[0].clientY;draw(); } },{passive:false}); c.addEventListener('touchend',function(){dragging=false;}); // ---- Mobile: sidebar toggle ---- function toggleSidebar(){ var sb=document.querySelector('.sidebar'); var ov=document.getElementById('overlay'); sb.classList.toggle('open'); ov.classList.toggle('show'); } function closeSidebar(){ document.querySelector('.sidebar').classList.remove('open'); document.getElementById('overlay').classList.remove('show'); } // ---- Mobile: double-tap to edit ---- var _lastTap=0; c.addEventListener('touchend',function(e){ var now=Date.now(); if(now-_lastTap<300){ // double tap e.preventDefault(); var rect=c.getBoundingClientRect(); var sx=(e.changedTouches[0].clientX-rect.left-c.width/2-ox)/SCALE; var sy=(e.changedTouches[0].clientY-rect.top-c.height/2-oy)/SCALE; var node=hitTest(sx,sy); if(node){ editing=node; var cx=rect.left+c.width/2+ox+node._x*SCALE; var cy=rect.top+c.height/2+oy+node._y*SCALE; ed.style.display='block';ed.style.left=cx+'px';ed.style.top=cy+'px'; ei.value=node.text;ei.focus();ei.select(); } } _lastTap=now; // close sidebar when tapping canvas if(window.innerWidth<=600)closeSidebar(); }); // ---- Mobile: pinch-to-zoom ---- var _pinchDist=0; c.addEventListener('touchstart',function(e){ if(e.touches.length===2){ _pinchDist=Math.hypot( e.touches[0].clientX-e.touches[1].clientX, e.touches[0].clientY-e.touches[1].clientY ); } },{passive:false}); c.addEventListener('touchmove',function(e){ if(e.touches.length===2){ e.preventDefault(); var d=Math.hypot( e.touches[0].clientX-e.touches[1].clientX, e.touches[0].clientY-e.touches[1].clientY ); var ratio=d/(_pinchDist||1); SCALE=Math.max(0.3,Math.min(3,SCALE*ratio)); _pinchDist=d; draw(); } },{passive:false}); // ---- Keyboard shortcuts ---- document.addEventListener('keydown',function(e){ if(e.target.tagName==='INPUT'||e.target.tagName==='TEXTAREA')return; // 编辑中不触发 if(e.key==='Delete'||e.key==='Backspace'){ e.preventDefault(); delNode(); } }); // ---- Actions ---- function commitEdit(){if(editing)editing.text=ei.value||'新节点';ed.style.display='none';editing=null;draw();autoSave();} function cancelEdit(){ed.style.display='none';editing=null;} function addChild(){ if(!selected){alert('请先选中一个节点');return;} if(!selected.children)selected.children=[]; selected.children.push({id:uid(),text:'新节点'}); layout();draw();selected=selected.children[selected.children.length-1];draw();autoSave(); } function addSibling(){ if(!selected||selected===tree){alert('请选中一个非根节点');return;} function fp(node,target){ if(node.children)for(var i=0;i