feat: 浮动编辑器 — 拖拽+缩放+全屏+文件树
功能: - 拖拽: 按住标题栏拖动编辑器面板 - 缩放: 右下角拖拽调整大小 - 最大化(⬜): 占满屏幕留边距 - 全屏(⤢): 完全覆盖页面 - 最小化(—): 收到底部标签栏 - 关闭(✕): 关闭全部标签 - 文件树(🌳): 左侧可折叠文件树, 点击文件打开/目录导航 - 文件树同步: 浏览新目录时自动更新 布局: ┌──────────────────────────────────┐ │ [🌳] [📄a.conf×][📄b.py×] [⬜][⤢][—][✕] │ ← 可拖拽 ├────────┬─────────────────────────┤ │ 📂 .. │ │ │ 📁 dir │ Monaco Editor │ │ 📄 a │ │ │ 📄 b │ │ │ │ Ln1,Col1|ini|UTF-8|file │ └────────┴─────────────────────────┘ ← 右下角可缩放
This commit is contained in:
+135
-22
@@ -59,21 +59,34 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- Floating Editor Panel -->
|
||||
<div id="editorPanel" class="hidden fixed z-40 bg-slate-900 border border-slate-700 rounded-xl shadow-2xl flex flex-col" style="right:24px;bottom:60px;width:70vw;height:65vh;min-width:500px;min-height:300px">
|
||||
<!-- Header: tabs + controls -->
|
||||
<div class="flex items-center border-b border-slate-800 shrink-0">
|
||||
<div id="editorTabBar" class="flex flex-1 overflow-x-auto"></div>
|
||||
<div class="flex items-center gap-1 px-2 shrink-0">
|
||||
<button onclick="toggleEditorSize()" class="text-slate-500 hover:text-white px-1.5 py-1 text-xs rounded hover:bg-slate-700" title="最大化/还原">⬜</button>
|
||||
<button onclick="minimizeEditor()" class="text-slate-500 hover:text-white px-1.5 py-1 text-xs rounded hover:bg-slate-700" title="最小化">—</button>
|
||||
<button onclick="closeAllTabs()" class="text-slate-500 hover:text-red-400 px-1.5 py-1 text-xs rounded hover:bg-red-900/30" title="关闭全部">✕</button>
|
||||
<!-- Floating Editor Panel (draggable + resizable + file tree) -->
|
||||
<div id="editorPanel" class="hidden fixed z-40 flex flex-col" style="right:24px;bottom:60px;width:75vw;height:65vh;min-width:500px;min-height:300px;border-radius:12px;overflow:hidden;box-shadow:0 25px 50px -12px rgba(0,0,0,0.5);border:1px solid rgb(51 65 85);background:rgb(15 23 42)">
|
||||
<!-- Drag handle + tabs + buttons -->
|
||||
<div id="editorHeader" class="flex items-center bg-slate-900 border-b border-slate-800 shrink-0 cursor-move select-none">
|
||||
<!-- File tree toggle -->
|
||||
<button onclick="toggleEditorTree()" id="treeToggleBtn" class="text-slate-500 hover:text-white px-2.5 py-2 text-sm hover:bg-slate-700 shrink-0" title="文件树">🌳</button>
|
||||
<!-- Tabs -->
|
||||
<div id="editorTabBar" class="flex flex-1 overflow-x-auto min-w-0"></div>
|
||||
<!-- Right buttons -->
|
||||
<div class="flex items-center shrink-0">
|
||||
<button onclick="toggleEditorSize()" id="sizeBtn" class="text-slate-500 hover:text-white px-2 py-2 text-sm hover:bg-slate-700" title="最大化/还原">⬜</button>
|
||||
<button onclick="toggleEditorFullscreen()" id="fsBtn" class="text-slate-500 hover:text-white px-2 py-2 text-sm hover:bg-slate-700" title="全屏">⤢</button>
|
||||
<button onclick="minimizeEditor()" class="text-slate-500 hover:text-white px-2 py-2 text-sm hover:bg-slate-700" title="最小化">—</button>
|
||||
<button onclick="closeAllTabs()" class="text-slate-500 hover:text-red-400 px-2 py-2 text-sm hover:bg-red-900/30" title="关闭全部">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Monaco container -->
|
||||
<div id="editorMonacoWrap" class="flex-1 relative min-h-0"></div>
|
||||
<!-- Body: file tree + editor -->
|
||||
<div class="flex flex-1 min-h-0">
|
||||
<!-- File tree sidebar -->
|
||||
<div id="editorTree" class="w-52 bg-slate-900 border-r border-slate-800 flex flex-col shrink-0 overflow-hidden">
|
||||
<div class="px-3 py-2 border-b border-slate-800 text-xs text-slate-500 font-medium">📁 文件列表</div>
|
||||
<div id="editorTreeList" class="flex-1 overflow-y-auto text-sm"></div>
|
||||
</div>
|
||||
<!-- Monaco container -->
|
||||
<div id="editorMonacoWrap" class="flex-1 relative min-w-0"></div>
|
||||
</div>
|
||||
<!-- Status bar -->
|
||||
<div class="flex items-center gap-4 px-4 py-1.5 bg-slate-800 border-t border-slate-700 text-xs text-slate-500 shrink-0 rounded-b-xl">
|
||||
<div class="flex items-center gap-4 px-4 py-1.5 bg-slate-800 border-t border-slate-700 text-xs text-slate-500 shrink-0">
|
||||
<span id="editorPos">Ln 1, Col 1</span>
|
||||
<span id="editorLang">plaintext</span>
|
||||
<span>UTF-8</span>
|
||||
@@ -81,6 +94,8 @@
|
||||
<div class="flex-1"></div>
|
||||
<span id="editorFileName" class="text-slate-400 truncate max-w-xs"></span>
|
||||
</div>
|
||||
<!-- Resize handle (bottom-right corner) -->
|
||||
<div id="editorResizeHandle" class="absolute bottom-0 right-0 w-4 h-4 cursor-nwse-resize z-10" style="background:linear-gradient(135deg,transparent 50%,rgb(100 116 139) 50%)"></div>
|
||||
</div>
|
||||
|
||||
<!-- Minimized editor bar -->
|
||||
@@ -387,7 +402,8 @@
|
||||
updateBreadcrumb(_currentPath);
|
||||
_allEntries=d.entries||[];
|
||||
renderFileList(_allEntries);
|
||||
preloadFiles(_allEntries); // Background preload small text files
|
||||
preloadFiles(_allEntries);
|
||||
if(_editorVisible)renderFileTree(); // Background preload small text files
|
||||
}catch(e){
|
||||
fileListEl.innerHTML=`<div class="px-4 py-12 text-center text-red-400">连接失败: ${esc(e.message)}</div>`;
|
||||
toast('error','连接失败');
|
||||
@@ -720,17 +736,25 @@
|
||||
}
|
||||
|
||||
let _editorMaximized=false;
|
||||
let _editorFullscreen=false;
|
||||
let _editorTreeVisible=true;
|
||||
const _defaultStyle='right:24px;bottom:60px;width:75vw;height:65vh;min-width:500px;min-height:300px';
|
||||
const _maxStyle='left:16px;right:16px;top:16px;bottom:60px;width:auto;height:auto;min-width:0;min-height:0';
|
||||
const _fsStyle='left:0;right:0;top:0;bottom:0;width:100vw;height:100vh;min-width:0;min-height:0;border-radius:0';
|
||||
|
||||
function showEditorPanel(){
|
||||
_editorVisible=true;_editorMinimized=false;
|
||||
document.getElementById('editorPanel').classList.remove('hidden');
|
||||
const p=document.getElementById('editorPanel');
|
||||
p.classList.remove('hidden');
|
||||
document.getElementById('editorMinBar').classList.add('hidden');
|
||||
if(!_editorFullscreen&&!_editorMaximized)p.style.cssText=_defaultStyle;
|
||||
renderFileTree();
|
||||
setTimeout(()=>{if(_activeTab>=0&&_openTabs[_activeTab].editor)_openTabs[_activeTab].editor.layout()},50);
|
||||
}
|
||||
function hideEditorPanel(){
|
||||
_editorVisible=false;_editorMaximized=false;
|
||||
_editorVisible=false;_editorMaximized=false;_editorFullscreen=false;
|
||||
const p=document.getElementById('editorPanel');
|
||||
p.classList.add('hidden');
|
||||
p.style.cssText=''; // Reset inline styles
|
||||
p.classList.add('hidden');p.style.cssText='';
|
||||
document.getElementById('editorMinBar').classList.add('hidden');
|
||||
renderMinBar();
|
||||
}
|
||||
@@ -744,18 +768,107 @@
|
||||
_editorMinimized=false;
|
||||
document.getElementById('editorPanel').classList.remove('hidden');
|
||||
document.getElementById('editorMinBar').classList.add('hidden');
|
||||
if(!_editorFullscreen&&!_editorMaximized)document.getElementById('editorPanel').style.cssText=_defaultStyle;
|
||||
renderFileTree();
|
||||
if(_activeTab>=0&&_openTabs[_activeTab].editor)setTimeout(()=>_openTabs[_activeTab].editor.layout(),50);
|
||||
}
|
||||
function toggleEditorSize(){
|
||||
const p=document.getElementById('editorPanel');
|
||||
_editorFullscreen=false;
|
||||
_editorMaximized=!_editorMaximized;
|
||||
if(_editorMaximized){
|
||||
p.style.cssText='left:16px;right:16px;top:16px;bottom:60px;width:auto;height:auto;min-width:0;min-height:0';
|
||||
}else{
|
||||
p.style.cssText='right:24px;bottom:60px;width:70vw;height:65vh;min-width:500px;min-height:300px';
|
||||
}
|
||||
p.style.cssText=_editorMaximized?_maxStyle:_defaultStyle;
|
||||
p.style.borderRadius=_editorMaximized?'12px':'12px';
|
||||
if(_activeTab>=0&&_openTabs[_activeTab].editor)setTimeout(()=>_openTabs[_activeTab].editor.layout(),50);
|
||||
}
|
||||
function toggleEditorFullscreen(){
|
||||
const p=document.getElementById('editorPanel');
|
||||
_editorMaximized=false;
|
||||
_editorFullscreen=!_editorFullscreen;
|
||||
p.style.cssText=_editorFullscreen?_fsStyle:_defaultStyle;
|
||||
p.style.borderRadius=_editorFullscreen?'0':'12px';
|
||||
if(_activeTab>=0&&_openTabs[_activeTab].editor)setTimeout(()=>_openTabs[_activeTab].editor.layout(),50);
|
||||
}
|
||||
function toggleEditorTree(){
|
||||
_editorTreeVisible=!_editorTreeVisible;
|
||||
document.getElementById('editorTree').classList.toggle('hidden',!_editorTreeVisible);
|
||||
if(_activeTab>=0&&_openTabs[_activeTab].editor)setTimeout(()=>_openTabs[_activeTab].editor.layout(),50);
|
||||
}
|
||||
function renderFileTree(){
|
||||
const list=document.getElementById('editorTreeList');
|
||||
if(!list)return;
|
||||
let html=`<div class="px-3 py-1.5 text-xs text-slate-600 cursor-pointer hover:bg-slate-800" onclick="browseDir('${esc(_currentPath.replace(/\/[^/]+\/?$/,'')||'/')}')">📂 ..</div>`;
|
||||
const sorted=sortEntries(_allEntries);
|
||||
sorted.forEach(e=>{
|
||||
const fp=_currentPath.replace(/\/$/,'')+'/'+e.name;
|
||||
if(e.is_dir){
|
||||
html+=`<div class="px-3 py-1 text-xs text-slate-400 cursor-pointer hover:bg-slate-800 truncate" onclick="browseDir('${esc(fp)}')">📁 ${esc(e.name)}</div>`;
|
||||
}else{
|
||||
const fi=fileIcon(e.name,false,e.is_link);
|
||||
const inTab=_openTabs.findIndex(t=>t.path===fp);
|
||||
const active=inTab===_activeTab;
|
||||
html+=`<div class="px-3 py-1 text-xs cursor-pointer hover:bg-slate-800 truncate ${active?'bg-slate-800 text-white':'text-slate-400'}" onclick="openFileFromTree('${esc(fp)}','${esc(e.name)}')">
|
||||
${fi.icon} ${esc(e.name)}${inTab>=0?'<span class="text-brand-light ml-1">●</span>':''}
|
||||
</div>`;
|
||||
}
|
||||
});
|
||||
list.innerHTML=html;
|
||||
}
|
||||
async function openFileFromTree(path,name){
|
||||
const cached=_fileCache[path];
|
||||
if(cached){openFileInEditor(path,name,cached.content);return}
|
||||
const r=await apiFetch(API+'/sync/read-file',{method:'POST',headers:apiHeadersJSON(),body:JSON.stringify({server_id:_currentServerId,path})});
|
||||
if(r&&r.ok){const d=await r.json();_fileCache[path]={content:d.content,size:d.size};openFileInEditor(path,name,d.content)}
|
||||
else{const e=r?await r.json().catch(()=>({})):{};toast('error','读取失败: '+(e.detail||''))}
|
||||
}
|
||||
|
||||
// ── Drag ──
|
||||
(function initDrag(){
|
||||
const header=document.getElementById('editorHeader');
|
||||
const panel=document.getElementById('editorPanel');
|
||||
let dragging=false,startX,startY,startLeft,startTop;
|
||||
header.addEventListener('mousedown',e=>{
|
||||
if(e.target.closest('button')||e.target.closest('#editorTabBar'))return;
|
||||
dragging=true;
|
||||
const rect=panel.getBoundingClientRect();
|
||||
startX=e.clientX;startY=e.clientY;
|
||||
startLeft=rect.left;startTop=rect.top;
|
||||
panel.style.right='auto';panel.style.bottom='auto';
|
||||
panel.style.left=startLeft+'px';panel.style.top=startTop+'px';
|
||||
document.body.style.userSelect='none';
|
||||
});
|
||||
document.addEventListener('mousemove',e=>{
|
||||
if(!dragging)return;
|
||||
panel.style.left=(startLeft+e.clientX-startX)+'px';
|
||||
panel.style.top=(startTop+e.clientY-startY)+'px';
|
||||
});
|
||||
document.addEventListener('mouseup',()=>{dragging=false;document.body.style.userSelect=''});
|
||||
})();
|
||||
|
||||
// ── Resize ──
|
||||
(function initResize(){
|
||||
const handle=document.getElementById('editorResizeHandle');
|
||||
const panel=document.getElementById('editorPanel');
|
||||
let resizing=false,startX,startY,startW,startH;
|
||||
handle.addEventListener('mousedown',e=>{
|
||||
e.stopPropagation();
|
||||
resizing=true;
|
||||
startX=e.clientX;startY=e.clientY;
|
||||
startW=panel.offsetWidth;startH=panel.offsetHeight;
|
||||
document.body.style.userSelect='none';
|
||||
});
|
||||
document.addEventListener('mousemove',e=>{
|
||||
if(!resizing)return;
|
||||
const newW=Math.max(500,startW+e.clientX-startX);
|
||||
const newH=Math.max(300,startH+e.clientY-startY);
|
||||
panel.style.width=newW+'px';panel.style.height=newH+'px';
|
||||
});
|
||||
document.addEventListener('mouseup',()=>{
|
||||
if(resizing){
|
||||
resizing=false;document.body.style.userSelect='';
|
||||
if(_activeTab>=0&&_openTabs[_activeTab].editor)_openTabs[_activeTab].editor.layout();
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
function renderTabBar(){
|
||||
const bar=document.getElementById('editorTabBar');
|
||||
|
||||
Reference in New Issue
Block a user