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